diff --git a/litellm/types/prompts/init_prompts.py b/litellm/types/prompts/init_prompts.py index 184f0448b33..102ace93a52 100644 --- a/litellm/types/prompts/init_prompts.py +++ b/litellm/types/prompts/init_prompts.py @@ -23,6 +23,11 @@ class PromptLiteLLMParams(BaseModel): prompt_id: str prompt_integration: str + dotprompt_content: Optional[str] = None + """ + allows saving the dotprompt file content + """ + model_config = ConfigDict(extra="allow", protected_namespaces=()) diff --git a/ui/litellm-dashboard/src/components/prompts.tsx b/ui/litellm-dashboard/src/components/prompts.tsx index cf41f36f6c5..a8cdb923f32 100644 --- a/ui/litellm-dashboard/src/components/prompts.tsx +++ b/ui/litellm-dashboard/src/components/prompts.tsx @@ -6,6 +6,7 @@ import { getPromptsList, PromptSpec, ListPromptsResponse, deletePromptCall } fro import PromptTable from "./prompts/prompt_table"; import PromptInfoView from "./prompts/prompt_info"; import AddPromptForm from "./prompts/add_prompt_form"; +import PromptEditorView from "./prompts/prompt_editor_view"; import NotificationsManager from "./molecules/notifications_manager"; import { isAdminRole } from "@/utils/roles"; @@ -19,6 +20,7 @@ const PromptsPanel: React.FC = ({ accessToken, userRole }) => { const [isLoading, setIsLoading] = useState(false); const [selectedPromptId, setSelectedPromptId] = useState(null); const [isAddModalVisible, setIsAddModalVisible] = useState(false); + const [showEditorView, setShowEditorView] = useState(false); const [isDeleting, setIsDeleting] = useState(false); const [promptToDelete, setPromptToDelete] = useState<{ id: string; name: string } | null>(null); @@ -50,6 +52,13 @@ const PromptsPanel: React.FC = ({ accessToken, userRole }) => { }; const handleAddPrompt = () => { + if (selectedPromptId) { + setSelectedPromptId(null); + } + setShowEditorView(true); + }; + + const handleAddPromptFromFile = () => { if (selectedPromptId) { setSelectedPromptId(null); } @@ -60,6 +69,10 @@ const PromptsPanel: React.FC = ({ accessToken, userRole }) => { setIsAddModalVisible(false); }; + const handleCloseEditor = () => { + setShowEditorView(false); + }; + const handleSuccess = () => { fetchPrompts(); }; @@ -91,7 +104,13 @@ const PromptsPanel: React.FC = ({ accessToken, userRole }) => { return (
- {selectedPromptId ? ( + {showEditorView ? ( + + ) : selectedPromptId ? ( setSelectedPromptId(null)} @@ -102,9 +121,14 @@ const PromptsPanel: React.FC = ({ accessToken, userRole }) => { ) : ( <>
+
+ +
{ + return ( +
+
+
+
+ +
+

Your conversation will appear here

+

Save the prompt to test it

+
+
+
+ ); +}; + +export default ConversationPanel; + diff --git a/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/DeveloperMessageCard.tsx b/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/DeveloperMessageCard.tsx new file mode 100644 index 00000000000..8f920ea5534 --- /dev/null +++ b/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/DeveloperMessageCard.tsx @@ -0,0 +1,31 @@ +import React from "react"; +import { Card, Text } from "@tremor/react"; +import VariableTextArea from "../variable_textarea"; + +interface DeveloperMessageCardProps { + value: string; + onChange: (value: string) => void; +} + +const DeveloperMessageCard: React.FC = ({ + value, + onChange, +}) => { + return ( + + Developer message + + Optional system instructions for the model + + + + ); +}; + +export default DeveloperMessageCard; + diff --git a/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/DotpromptViewTab.tsx b/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/DotpromptViewTab.tsx new file mode 100644 index 00000000000..7791789fab9 --- /dev/null +++ b/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/DotpromptViewTab.tsx @@ -0,0 +1,32 @@ +import React from "react"; +import { PromptType } from "./types"; +import { convertToDotPrompt } from "./utils"; + +interface DotpromptViewTabProps { + prompt: PromptType; +} + +const DotpromptViewTab: React.FC = ({ prompt }) => { + const dotpromptContent = convertToDotPrompt(prompt); + + return ( +
+
+

+ Generated .prompt file +

+

+ This is the dotprompt format that will be saved to the database +

+
+
+
+          {dotpromptContent}
+        
+
+
+ ); +}; + +export default DotpromptViewTab; + diff --git a/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/ModelConfigCard.tsx b/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/ModelConfigCard.tsx new file mode 100644 index 00000000000..1752fdc22b5 --- /dev/null +++ b/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/ModelConfigCard.tsx @@ -0,0 +1,98 @@ +import React, { useState } from "react"; +import { Text } from "@tremor/react"; +import { Input } from "antd"; +import { SettingsIcon } from "lucide-react"; +import ModelSelector from "../../common_components/ModelSelector"; + +interface ModelConfigCardProps { + model: string; + temperature?: number; + maxTokens?: number; + accessToken: string | null; + onModelChange: (model: string) => void; + onTemperatureChange: (temp: number) => void; + onMaxTokensChange: (tokens: number) => void; +} + +const ModelConfigCard: React.FC = ({ + model, + temperature = 1, + maxTokens = 1000, + accessToken, + onModelChange, + onTemperatureChange, + onMaxTokensChange, +}) => { + const [showConfig, setShowConfig] = useState(false); + + return ( +
+
+ +
+ + + + {showConfig && ( +
+
+
+

Model Parameters

+ +
+
+
+
+ Temperature + onTemperatureChange(parseFloat(e.target.value) || 0)} + className="w-20" + /> +
+
+
+
+ Max Tokens + onMaxTokensChange(parseInt(e.target.value) || 1000)} + className="w-24" + /> +
+
+
+
+
+ )} +
+ ); +}; + +export default ModelConfigCard; + diff --git a/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/PromptEditorHeader.tsx b/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/PromptEditorHeader.tsx new file mode 100644 index 00000000000..5339e437a0e --- /dev/null +++ b/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/PromptEditorHeader.tsx @@ -0,0 +1,51 @@ +import React from "react"; +import { Button as TremorButton } from "@tremor/react"; +import { Input } from "antd"; +import { ArrowLeftIcon, SaveIcon } from "lucide-react"; + +interface PromptEditorHeaderProps { + promptName: string; + onNameChange: (name: string) => void; + onBack: () => void; + onSave: () => void; + isSaving: boolean; +} + +const PromptEditorHeader: React.FC = ({ + promptName, + onNameChange, + onBack, + onSave, + isSaving, +}) => { + return ( +
+
+ + Back + + onNameChange(e.target.value)} + className="text-base font-medium border-none shadow-none" + style={{ width: "200px" }} + /> + Draft + Unsaved changes +
+
+ + Save + +
+
+ ); +}; + +export default PromptEditorHeader; + diff --git a/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/PromptMessagesCard.tsx b/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/PromptMessagesCard.tsx new file mode 100644 index 00000000000..eb037ff5fc1 --- /dev/null +++ b/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/PromptMessagesCard.tsx @@ -0,0 +1,121 @@ +import React, { useState } from "react"; +import { Card, Text } from "@tremor/react"; +import { Select } from "antd"; +import { PlusIcon, TrashIcon, GripVerticalIcon } from "lucide-react"; +import VariableTextArea from "../variable_textarea"; +import { Message } from "./types"; + +const { Option } = Select; + +interface PromptMessagesCardProps { + messages: Message[]; + onAddMessage: () => void; + onUpdateMessage: (index: number, field: "role" | "content", value: string) => void; + onRemoveMessage: (index: number) => void; + onMoveMessage: (fromIndex: number, toIndex: number) => void; +} + +const PromptMessagesCard: React.FC = ({ + messages, + onAddMessage, + onUpdateMessage, + onRemoveMessage, + onMoveMessage, +}) => { + const [draggedIndex, setDraggedIndex] = useState(null); + const [dragOverIndex, setDragOverIndex] = useState(null); + + const handleDragStart = (index: number) => { + setDraggedIndex(index); + }; + + const handleDragOver = (e: React.DragEvent, index: number) => { + e.preventDefault(); + setDragOverIndex(index); + }; + + const handleDrop = (e: React.DragEvent, dropIndex: number) => { + e.preventDefault(); + if (draggedIndex !== null && draggedIndex !== dropIndex) { + onMoveMessage(draggedIndex, dropIndex); + } + setDraggedIndex(null); + setDragOverIndex(null); + }; + + const handleDragEnd = () => { + setDraggedIndex(null); + setDragOverIndex(null); + }; + + return ( + +
+ Prompt messages + + Use {'{{variable}}'} syntax for template variables + +
+
+ {messages.map((message, index) => ( +
handleDragStart(index)} + onDragOver={(e) => handleDragOver(e, index)} + onDrop={(e) => handleDrop(e, index)} + onDragEnd={handleDragEnd} + className={`border border-gray-300 rounded overflow-hidden bg-white transition-all ${ + draggedIndex === index ? "opacity-50" : "" + } ${dragOverIndex === index && draggedIndex !== index ? "border-blue-500 border-2" : ""}`} + > +
+ +
+ {messages.length > 1 && ( + + )} +
+ +
+
+
+
+ onUpdateMessage(index, "content", value)} + rows={3} + placeholder="Enter prompt content..." + /> +
+
+ ))} +
+ +
+ ); +}; + +export default PromptMessagesCard; + diff --git a/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/PublishModal.tsx b/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/PublishModal.tsx new file mode 100644 index 00000000000..411a463c45c --- /dev/null +++ b/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/PublishModal.tsx @@ -0,0 +1,56 @@ +import React from "react"; +import { Button as TremorButton, Text } from "@tremor/react"; +import { Input, Modal } from "antd"; + +interface PublishModalProps { + visible: boolean; + promptName: string; + isSaving: boolean; + onNameChange: (name: string) => void; + onPublish: () => void; + onCancel: () => void; +} + +const PublishModal: React.FC = ({ + visible, + promptName, + isSaving, + onNameChange, + onPublish, + onCancel, +}) => { + return ( + + + Cancel + + + Publish + +
+ ]} + > +
+ Name + onNameChange(e.target.value)} + placeholder="Enter prompt name" + onPressEnter={onPublish} + autoFocus + /> + + Published prompts can be used in API calls and are versioned for easy tracking. + +
+ + ); +}; + +export default PublishModal; + diff --git a/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/ToolsCard.tsx b/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/ToolsCard.tsx new file mode 100644 index 00000000000..53444c5d8e9 --- /dev/null +++ b/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/ToolsCard.tsx @@ -0,0 +1,67 @@ +import React from "react"; +import { Card, Text } from "@tremor/react"; +import { PlusIcon, TrashIcon } from "lucide-react"; +import { Tool } from "./types"; + +interface ToolsCardProps { + tools: Tool[]; + onAddTool: () => void; + onEditTool: (index: number) => void; + onRemoveTool: (index: number) => void; +} + +const ToolsCard: React.FC = ({ + tools, + onAddTool, + onEditTool, + onRemoveTool, +}) => { + return ( + +
+ Tools + +
+ {tools.length === 0 ? ( + No tools added + ) : ( +
+ {tools.map((tool, index) => ( +
+
+
{tool.name}
+
{tool.description}
+
+
+ + +
+
+ ))} +
+ )} +
+ ); +}; + +export default ToolsCard; + diff --git a/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/index.tsx b/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/index.tsx new file mode 100644 index 00000000000..8ea8f7d5b7a --- /dev/null +++ b/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/index.tsx @@ -0,0 +1,290 @@ +import React, { useState } from "react"; +import ToolModal from "../tool_modal"; +import NotificationsManager from "../../molecules/notifications_manager"; +import { createPromptCall } from "../../networking"; +import { PromptType, PromptEditorViewProps, Tool } from "./types"; +import { convertToDotPrompt } from "./utils"; +import PromptEditorHeader from "./PromptEditorHeader"; +import ModelConfigCard from "./ModelConfigCard"; +import ToolsCard from "./ToolsCard"; +import DeveloperMessageCard from "./DeveloperMessageCard"; +import PromptMessagesCard from "./PromptMessagesCard"; +import ConversationPanel from "./ConversationPanel"; +import PublishModal from "./PublishModal"; +import DotpromptViewTab from "./DotpromptViewTab"; + +const PromptEditorView: React.FC = ({ onClose, onSuccess, accessToken }) => { + const [prompt, setPrompt] = useState({ + name: "New prompt", + model: "gpt-4o", + config: { + temperature: 1, + max_tokens: 1000, + }, + tools: [], + developerMessage: "", + messages: [ + { + role: "user", + content: "Enter task specifics. Use {{template_variables}} for dynamic inputs", + }, + ], + }); + + const [showToolModal, setShowToolModal] = useState(false); + const [showNameModal, setShowNameModal] = useState(false); + const [editingToolIndex, setEditingToolIndex] = useState(null); + const [isSaving, setIsSaving] = useState(false); + const [viewMode, setViewMode] = useState<"pretty" | "dotprompt">("pretty"); + + const addMessage = () => { + setPrompt({ + ...prompt, + messages: [ + ...prompt.messages, + { + role: "user", + content: "", + }, + ], + }); + }; + + const updateMessage = (index: number, field: "role" | "content", value: string) => { + const newMessages = [...prompt.messages]; + newMessages[index][field] = value; + setPrompt({ + ...prompt, + messages: newMessages, + }); + }; + + const removeMessage = (index: number) => { + if (prompt.messages.length > 1) { + setPrompt({ + ...prompt, + messages: prompt.messages.filter((_, i) => i !== index), + }); + } + }; + + const moveMessage = (fromIndex: number, toIndex: number) => { + const newMessages = [...prompt.messages]; + const [movedMessage] = newMessages.splice(fromIndex, 1); + newMessages.splice(toIndex, 0, movedMessage); + setPrompt({ + ...prompt, + messages: newMessages, + }); + }; + + const addTool = (json: string) => { + try { + const parsed = JSON.parse(json); + const tool: Tool = { + name: parsed.function?.name || "Unnamed Tool", + description: parsed.function?.description || "", + json: json, + }; + + if (editingToolIndex !== null) { + const newTools = [...prompt.tools]; + newTools[editingToolIndex] = tool; + setPrompt({ + ...prompt, + tools: newTools, + }); + } else { + setPrompt({ + ...prompt, + tools: [...prompt.tools, tool], + }); + } + + setShowToolModal(false); + setEditingToolIndex(null); + } catch (error) { + NotificationsManager.fromBackend("Invalid JSON format"); + } + }; + + const removeTool = (index: number) => { + setPrompt({ + ...prompt, + tools: prompt.tools.filter((_, i) => i !== index), + }); + }; + + const openToolModal = (index?: number) => { + if (index !== undefined) { + setEditingToolIndex(index); + } else { + setEditingToolIndex(null); + } + setShowToolModal(true); + }; + + const handleSaveClick = () => { + if (!prompt.name || prompt.name.trim() === "" || prompt.name === "New prompt") { + setShowNameModal(true); + } else { + handleSave(); + } + }; + + const handleSave = async () => { + if (!accessToken) { + NotificationsManager.fromBackend("Access token is required"); + return; + } + + if (!prompt.name || prompt.name.trim() === "") { + NotificationsManager.fromBackend("Please enter a valid prompt name"); + return; + } + + setIsSaving(true); + try { + const promptId = prompt.name.replace(/[^a-zA-Z0-9_-]/g, "_").toLowerCase(); + const dotpromptContent = convertToDotPrompt(prompt); + + const promptData = { + prompt_id: promptId, + litellm_params: { + prompt_integration: "dotprompt", + prompt_id: promptId, + dotprompt_content: dotpromptContent, + }, + prompt_info: { + prompt_type: "db", + }, + }; + + await createPromptCall(accessToken, promptData); + NotificationsManager.success("Prompt created successfully!"); + onSuccess(); + onClose(); + } catch (error) { + console.error("Error saving prompt:", error); + NotificationsManager.fromBackend("Failed to save prompt"); + } finally { + setIsSaving(false); + setShowNameModal(false); + } + }; + + return ( +
+
+ setPrompt({ ...prompt, name })} + onBack={onClose} + onSave={handleSaveClick} + isSaving={isSaving} + /> + +
+
+
+ setPrompt({ ...prompt, model })} + onTemperatureChange={(temperature) => + setPrompt({ + ...prompt, + config: { ...prompt.config, temperature }, + }) + } + onMaxTokensChange={(max_tokens) => + setPrompt({ + ...prompt, + config: { ...prompt.config, max_tokens }, + }) + } + /> + +
+ + +
+
+ + {viewMode === "pretty" ? ( +
+ openToolModal()} + onEditTool={openToolModal} + onRemoveTool={removeTool} + /> + + setPrompt({ ...prompt, developerMessage })} + /> + + +
+ ) : ( + + )} +
+ + +
+
+ + setPrompt({ ...prompt, name })} + onPublish={handleSave} + onCancel={() => setShowNameModal(false)} + /> + + {showToolModal && ( + { + setShowToolModal(false); + setEditingToolIndex(null); + }} + /> + )} +
+ ); +}; + +export default PromptEditorView; + diff --git a/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/types.ts b/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/types.ts new file mode 100644 index 00000000000..36376e00554 --- /dev/null +++ b/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/types.ts @@ -0,0 +1,30 @@ +export interface Message { + role: string; + content: string; +} + +export interface Tool { + name: string; + description: string; + json: string; +} + +export interface PromptType { + name: string; + model: string; + config: { + temperature?: number; + max_tokens?: number; + top_p?: number; + }; + tools: Tool[]; + developerMessage: string; + messages: Message[]; +} + +export interface PromptEditorViewProps { + onClose: () => void; + onSuccess: () => void; + accessToken: string | null; +} + diff --git a/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/utils.ts b/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/utils.ts new file mode 100644 index 00000000000..6da9c44d863 --- /dev/null +++ b/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/utils.ts @@ -0,0 +1,76 @@ +import { PromptType } from "./types"; + +export const extractVariables = (prompt: PromptType): string[] => { + const variableSet = new Set(); + const variableRegex = /\{\{(\w+)\}\}/g; + + prompt.messages.forEach((message) => { + let match; + while ((match = variableRegex.exec(message.content)) !== null) { + variableSet.add(match[1]); + } + }); + + if (prompt.developerMessage) { + let match; + while ((match = variableRegex.exec(prompt.developerMessage)) !== null) { + variableSet.add(match[1]); + } + } + + return Array.from(variableSet); +}; + +export const convertToDotPrompt = (prompt: PromptType): string => { + const variables = extractVariables(prompt); + let result = `---\nmodel: ${prompt.model}\n`; + + // Add temperature if set + if (prompt.config.temperature !== undefined) { + result += `temperature: ${prompt.config.temperature}\n`; + } + + // Add max_tokens if set + if (prompt.config.max_tokens !== undefined) { + result += `max_tokens: ${prompt.config.max_tokens}\n`; + } + + // Add top_p if set + if (prompt.config.top_p !== undefined) { + result += `top_p: ${prompt.config.top_p}\n`; + } + + // Add input schema + result += `input:\n schema:\n`; + variables.forEach((variable) => { + result += ` ${variable}: string\n`; + }); + + // Add output format + result += `output:\n format: text\n`; + + // Add tools if present + if (prompt.tools && prompt.tools.length > 0) { + result += `tools:\n`; + prompt.tools.forEach((tool) => { + const toolObj = JSON.parse(tool.json); + result += ` - ${JSON.stringify(toolObj)}\n`; + }); + } + + result += `---\n\n`; + + // Add developer message if present + if (prompt.developerMessage && prompt.developerMessage.trim() !== "") { + result += `Developer: ${prompt.developerMessage.trim()}\n\n`; + } + + // Add messages with role prefixes + prompt.messages.forEach((message) => { + const role = message.role.charAt(0).toUpperCase() + message.role.slice(1); + result += `${role}: ${message.content}\n\n`; + }); + + return result.trim(); +}; + diff --git a/ui/litellm-dashboard/src/components/prompts/tool_modal.tsx b/ui/litellm-dashboard/src/components/prompts/tool_modal.tsx new file mode 100644 index 00000000000..0496eea2bc5 --- /dev/null +++ b/ui/litellm-dashboard/src/components/prompts/tool_modal.tsx @@ -0,0 +1,89 @@ +import React, { useState } from "react"; +import { Modal, Button } from "antd"; + +interface ToolModalProps { + visible: boolean; + initialJson: string; + onSave: (json: string) => void; + onClose: () => void; +} + +const defaultToolJson = `{ + "type": "function", + "function": { + "name": "get_current_weather", + "description": "Get the current weather in a given location", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state, e.g. San Francisco, CA" + }, + "unit": { + "type": "string", + "enum": ["celsius", "fahrenheit"] + } + }, + "required": ["location"] + } + } +}`; + +const ToolModal: React.FC = ({ visible, initialJson, onSave, onClose }) => { + const [json, setJson] = useState(initialJson || defaultToolJson); + const [error, setError] = useState(null); + + const handleSave = () => { + try { + JSON.parse(json); + setError(null); + onSave(json); + } catch (e) { + setError("Invalid JSON format. Please check your syntax."); + } + }; + + const handleClose = () => { + setError(null); + onClose(); + }; + + return ( + + Add Tool + + } + open={visible} + onCancel={handleClose} + width={800} + footer={[ + , + , + ]} + > +
+ {error && ( +
+ {error} +
+ )} +