mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-12 23:01:41 +00:00
[Feat] UI - Show "get code" section for prompt management + minor polish of showing version history (#16941)
* add _get_prompt_data_from_dotprompt_content * fix pre call hook for prompt template * fix: get_latest_version_prompt_id * fix get_latest_version_prompt_id * test_get_latest_version_prompt_id * fx info and delete lookup for prompts * refactor prompt table * - rename to prompt studio * fix get_prompt_info * fix endpoints * add PromptCodeSnippets * prompt info view * add prompt info view * show correct version for prompts * fix version selector * fix endpoints and version * fix get_prompt_info * fix version display
This commit is contained in:
parent
c9ac1949ee
commit
fb38763eb4
10 changed files with 555 additions and 31 deletions
|
|
@ -38,7 +38,7 @@ def get_base_prompt_id(prompt_id: str) -> str:
|
|||
Extract the base prompt ID by stripping the version suffix if present.
|
||||
|
||||
Args:
|
||||
prompt_id: Prompt ID that may include version suffix (e.g., "jack_success.v1")
|
||||
prompt_id: Prompt ID that may include version suffix (e.g., "jack_success.v1" or "jack_success_v1")
|
||||
|
||||
Returns:
|
||||
Base prompt ID without version suffix (e.g., "jack_success")
|
||||
|
|
@ -46,10 +46,18 @@ def get_base_prompt_id(prompt_id: str) -> str:
|
|||
Examples:
|
||||
>>> get_base_prompt_id("jack_success.v1")
|
||||
"jack_success"
|
||||
>>> get_base_prompt_id("jack_success_v1")
|
||||
"jack_success"
|
||||
>>> get_base_prompt_id("jack_success")
|
||||
"jack_success"
|
||||
"""
|
||||
return prompt_id.split(".v")[0] if ".v" in prompt_id else prompt_id
|
||||
# Try dot separator first (.v)
|
||||
if ".v" in prompt_id:
|
||||
return prompt_id.split(".v")[0]
|
||||
# Try underscore separator (_v)
|
||||
if "_v" in prompt_id:
|
||||
return prompt_id.split("_v")[0]
|
||||
return prompt_id
|
||||
|
||||
|
||||
def get_version_number(prompt_id: str) -> int:
|
||||
|
|
@ -57,7 +65,7 @@ def get_version_number(prompt_id: str) -> int:
|
|||
Extract the version number from a versioned prompt ID.
|
||||
|
||||
Args:
|
||||
prompt_id: Prompt ID that may include version suffix (e.g., "jack_success.v2")
|
||||
prompt_id: Prompt ID that may include version suffix (e.g., "jack_success.v2" or "jack_success_v2")
|
||||
|
||||
Returns:
|
||||
Version number (defaults to 1 if no version suffix or invalid format)
|
||||
|
|
@ -65,15 +73,27 @@ def get_version_number(prompt_id: str) -> int:
|
|||
Examples:
|
||||
>>> get_version_number("jack_success.v2")
|
||||
2
|
||||
>>> get_version_number("jack_success_v2")
|
||||
2
|
||||
>>> get_version_number("jack_success")
|
||||
1
|
||||
"""
|
||||
# Try dot separator first (.v)
|
||||
if ".v" in prompt_id:
|
||||
version_str = prompt_id.split(".v")[1]
|
||||
try:
|
||||
return int(version_str)
|
||||
except ValueError:
|
||||
return 1
|
||||
pass
|
||||
|
||||
# Try underscore separator (_v)
|
||||
if "_v" in prompt_id:
|
||||
version_str = prompt_id.split("_v")[1]
|
||||
try:
|
||||
return int(version_str)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return 1
|
||||
|
||||
|
||||
|
|
@ -403,10 +423,31 @@ async def get_prompt_versions(
|
|||
status_code=404, detail=f"No versions found for prompt ID {base_prompt_id}"
|
||||
)
|
||||
|
||||
# Sort by version number (descending - newest first)
|
||||
prompt_versions.sort(key=lambda p: get_version_number(prompt_id=p.prompt_id), reverse=True)
|
||||
# Create response with explicit version field for each prompt
|
||||
versioned_prompts = []
|
||||
for prompt in prompt_versions:
|
||||
# Extract version number from the root prompt_id which has version suffix
|
||||
# (e.g., "jack-sparrow.v3" -> 3)
|
||||
version_number = get_version_number(prompt_id=prompt.prompt_id)
|
||||
|
||||
# Strip version from prompt_id for clean display
|
||||
base_prompt_id = get_base_prompt_id(prompt_id=prompt.prompt_id)
|
||||
|
||||
# Create a copy with explicit version field and clean prompt_id
|
||||
versioned_prompt = PromptSpec(
|
||||
prompt_id=base_prompt_id, # Clean ID without version (e.g., "jack-sparrow")
|
||||
litellm_params=prompt.litellm_params,
|
||||
prompt_info=prompt.prompt_info,
|
||||
created_at=prompt.created_at,
|
||||
updated_at=prompt.updated_at,
|
||||
version=version_number, # Explicit version field (e.g., 3)
|
||||
)
|
||||
versioned_prompts.append(versioned_prompt)
|
||||
|
||||
return ListPromptsResponse(prompts=prompt_versions)
|
||||
# Sort by version number (descending - newest first)
|
||||
versioned_prompts.sort(key=lambda p: p.version or 1, reverse=True)
|
||||
|
||||
return ListPromptsResponse(prompts=versioned_prompts)
|
||||
|
||||
|
||||
@router.get(
|
||||
|
|
@ -489,6 +530,20 @@ async def get_prompt_info(
|
|||
if prompt_spec is None:
|
||||
raise HTTPException(status_code=400, detail=f"Prompt {prompt_id} not found")
|
||||
|
||||
# Extract version number from the prompt_id
|
||||
version_number = get_version_number(prompt_id=prompt_spec.prompt_id)
|
||||
|
||||
# Create a copy of the prompt spec with the base prompt ID (stripped of version)
|
||||
# and explicit version field for consistency with list_prompts and versions endpoints
|
||||
prompt_spec_response = PromptSpec(
|
||||
prompt_id=get_base_prompt_id(prompt_id=prompt_spec.prompt_id),
|
||||
litellm_params=prompt_spec.litellm_params, # This preserves the versioned ID
|
||||
prompt_info=prompt_spec.prompt_info,
|
||||
created_at=prompt_spec.created_at,
|
||||
updated_at=prompt_spec.updated_at,
|
||||
version=version_number, # Explicit version field
|
||||
)
|
||||
|
||||
# Get prompt content from the callback
|
||||
prompt_template: Optional[PromptTemplateBase] = None
|
||||
try:
|
||||
|
|
@ -519,7 +574,7 @@ async def get_prompt_info(
|
|||
|
||||
# Create response with content
|
||||
return PromptInfoResponse(
|
||||
prompt_spec=prompt_spec,
|
||||
prompt_spec=prompt_spec_response,
|
||||
raw_prompt_template=prompt_template,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ class PromptSpec(BaseModel):
|
|||
prompt_info: PromptInfo
|
||||
created_at: Optional[datetime] = None
|
||||
updated_at: Optional[datetime] = None
|
||||
version: Optional[int] = None # Version number for version history
|
||||
|
||||
def __init__(self, **data):
|
||||
if "prompt_info" not in data:
|
||||
|
|
|
|||
|
|
@ -124,6 +124,7 @@ export interface PromptSpec {
|
|||
prompt_info: PromptInfo;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
version?: number; // Explicit version number for version history
|
||||
}
|
||||
|
||||
export interface PromptTemplateBase {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,284 @@
|
|||
import React, { useState } from "react";
|
||||
import { Modal, Select, Button as AntdButton, Tabs } from "antd";
|
||||
import { CodeOutlined } from "@ant-design/icons";
|
||||
import { Button as TremorButton, Text } from "@tremor/react";
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
import { coy } from "react-syntax-highlighter/dist/esm/styles/prism";
|
||||
import NotificationsManager from "../../molecules/notifications_manager";
|
||||
|
||||
interface PromptCodeSnippetsProps {
|
||||
promptId: string;
|
||||
model: string;
|
||||
promptVariables?: Record<string, string>;
|
||||
accessToken: string | null;
|
||||
version?: string;
|
||||
proxySettings?: {
|
||||
PROXY_BASE_URL?: string;
|
||||
LITELLM_UI_API_DOC_BASE_URL?: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
const PromptCodeSnippets: React.FC<PromptCodeSnippetsProps> = ({
|
||||
promptId,
|
||||
model,
|
||||
promptVariables = {},
|
||||
accessToken,
|
||||
version = "1",
|
||||
proxySettings,
|
||||
}) => {
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const [selectedLanguage, setSelectedLanguage] = useState<"curl" | "python" | "javascript">("curl");
|
||||
const [selectedTab, setSelectedTab] = useState("basic");
|
||||
const [generatedCode, setGeneratedCode] = useState("");
|
||||
|
||||
const showModal = () => {
|
||||
setIsModalVisible(true);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setIsModalVisible(false);
|
||||
};
|
||||
|
||||
// Determine base URL with priority: LITELLM_UI_API_DOC_BASE_URL > PROXY_BASE_URL > window.location.origin
|
||||
let apiBase = window.location.origin;
|
||||
const customDocBaseUrl = proxySettings?.LITELLM_UI_API_DOC_BASE_URL;
|
||||
if (customDocBaseUrl && customDocBaseUrl.trim()) {
|
||||
apiBase = customDocBaseUrl;
|
||||
} else if (proxySettings?.PROXY_BASE_URL) {
|
||||
apiBase = proxySettings.PROXY_BASE_URL;
|
||||
}
|
||||
|
||||
const effectiveApiKey = accessToken || "sk-1234";
|
||||
|
||||
// Generate code based on selected language and tab
|
||||
const generateCode = () => {
|
||||
const hasVariables = Object.keys(promptVariables).length > 0;
|
||||
|
||||
if (selectedLanguage === "curl") {
|
||||
if (selectedTab === "basic") {
|
||||
return `curl -X POST '${apiBase}/chat/completions' \\
|
||||
-H 'Content-Type: application/json' \\
|
||||
-H 'Authorization: Bearer ${effectiveApiKey}' \\
|
||||
-d '{
|
||||
"model": "${model}",
|
||||
"prompt_id": "${promptId}"${hasVariables ? `,
|
||||
"prompt_variables": ${JSON.stringify(promptVariables, null, 6).replace(/\n/g, '\n ')}` : ''}
|
||||
}' | jq`;
|
||||
} else if (selectedTab === "messages") {
|
||||
return `curl -X POST '${apiBase}/chat/completions' \\
|
||||
-H 'Content-Type: application/json' \\
|
||||
-H 'Authorization: Bearer ${effectiveApiKey}' \\
|
||||
-d '{
|
||||
"model": "${model}",
|
||||
"prompt_id": "${promptId}"${hasVariables ? `,
|
||||
"prompt_variables": ${JSON.stringify(promptVariables, null, 6).replace(/\n/g, '\n ')}` : ''},
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "hi"
|
||||
}
|
||||
]
|
||||
}' | jq`;
|
||||
} else {
|
||||
return `curl -X POST '${apiBase}/chat/completions' \\
|
||||
-H 'Content-Type: application/json' \\
|
||||
-H 'Authorization: Bearer ${effectiveApiKey}' \\
|
||||
-d '{
|
||||
"model": "${model}",
|
||||
"prompt_id": "${promptId}",
|
||||
"prompt_version": ${version},
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Who are u"
|
||||
}
|
||||
]
|
||||
}' | jq`;
|
||||
}
|
||||
} else if (selectedLanguage === "python") {
|
||||
const importCode = `import openai
|
||||
|
||||
client = openai.OpenAI(
|
||||
api_key="${effectiveApiKey}",
|
||||
base_url="${apiBase}"
|
||||
)
|
||||
`;
|
||||
if (selectedTab === "basic") {
|
||||
return `${importCode}
|
||||
response = client.chat.completions.create(
|
||||
model="${model}",
|
||||
extra_body={
|
||||
"prompt_id": "${promptId}"${hasVariables ? `,
|
||||
"prompt_variables": ${JSON.stringify(promptVariables, null, 8).replace(/\n/g, '\n ')}` : ''}
|
||||
}
|
||||
)
|
||||
|
||||
print(response)`;
|
||||
} else if (selectedTab === "messages") {
|
||||
return `${importCode}
|
||||
response = client.chat.completions.create(
|
||||
model="${model}",
|
||||
messages=[
|
||||
{"role": "user", "content": "hi"}
|
||||
],
|
||||
extra_body={
|
||||
"prompt_id": "${promptId}"${hasVariables ? `,
|
||||
"prompt_variables": ${JSON.stringify(promptVariables, null, 8).replace(/\n/g, '\n ')}` : ''}
|
||||
}
|
||||
)
|
||||
|
||||
print(response)`;
|
||||
} else {
|
||||
return `${importCode}
|
||||
response = client.chat.completions.create(
|
||||
model="${model}",
|
||||
messages=[
|
||||
{"role": "user", "content": "Who are u"}
|
||||
],
|
||||
extra_body={
|
||||
"prompt_id": "${promptId}",
|
||||
"prompt_version": ${version}
|
||||
}
|
||||
)
|
||||
|
||||
print(response)`;
|
||||
}
|
||||
} else {
|
||||
// JavaScript/Node.js
|
||||
const importCode = `import OpenAI from 'openai';
|
||||
|
||||
const client = new OpenAI({
|
||||
apiKey: "${effectiveApiKey}",
|
||||
baseURL: "${apiBase}"
|
||||
});
|
||||
`;
|
||||
if (selectedTab === "basic") {
|
||||
return `${importCode}
|
||||
async function main() {
|
||||
const response = await client.chat.completions.create({
|
||||
model: "${model}",
|
||||
${hasVariables ? `prompt_id: "${promptId}",
|
||||
prompt_variables: ${JSON.stringify(promptVariables, null, 8).replace(/\n/g, '\n ')}` : `prompt_id: "${promptId}"`}
|
||||
});
|
||||
|
||||
console.log(response);
|
||||
}
|
||||
|
||||
main();`;
|
||||
} else if (selectedTab === "messages") {
|
||||
return `${importCode}
|
||||
async function main() {
|
||||
const response = await client.chat.completions.create({
|
||||
model: "${model}",
|
||||
messages: [
|
||||
{ role: "user", content: "hi" }
|
||||
],
|
||||
${hasVariables ? `prompt_id: "${promptId}",
|
||||
prompt_variables: ${JSON.stringify(promptVariables, null, 8).replace(/\n/g, '\n ')}` : `prompt_id: "${promptId}"`}
|
||||
});
|
||||
|
||||
console.log(response);
|
||||
}
|
||||
|
||||
main();`;
|
||||
} else {
|
||||
return `${importCode}
|
||||
async function main() {
|
||||
const response = await client.chat.completions.create({
|
||||
model: "${model}",
|
||||
messages: [
|
||||
{ role: "user", content: "Who are u" }
|
||||
],
|
||||
prompt_id: "${promptId}",
|
||||
prompt_version: ${version}
|
||||
});
|
||||
|
||||
console.log(response);
|
||||
}
|
||||
|
||||
main();`;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Update generated code when language, tab or props change
|
||||
React.useEffect(() => {
|
||||
if (isModalVisible) {
|
||||
setGeneratedCode(generateCode());
|
||||
}
|
||||
}, [isModalVisible, selectedLanguage, selectedTab, promptId, model, promptVariables]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TremorButton
|
||||
variant="secondary"
|
||||
icon={CodeOutlined}
|
||||
onClick={showModal}
|
||||
>
|
||||
Get Code
|
||||
</TremorButton>
|
||||
|
||||
<Modal
|
||||
title="Generated Code"
|
||||
open={isModalVisible}
|
||||
onCancel={handleCancel}
|
||||
footer={null}
|
||||
width={800}
|
||||
>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<div>
|
||||
<Text className="font-medium block mb-1 text-gray-700">Language</Text>
|
||||
<Select
|
||||
value={selectedLanguage}
|
||||
onChange={(value) => setSelectedLanguage(value as "curl" | "python" | "javascript")}
|
||||
style={{ width: 180 }}
|
||||
options={[
|
||||
{ value: "curl", label: "cURL" },
|
||||
{ value: "python", label: "Python (OpenAI SDK)" },
|
||||
{ value: "javascript", label: "JavaScript (OpenAI SDK)" },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<AntdButton
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(generatedCode);
|
||||
NotificationsManager.success("Copied to clipboard!");
|
||||
}}
|
||||
>
|
||||
Copy to Clipboard
|
||||
</AntdButton>
|
||||
</div>
|
||||
|
||||
<Tabs
|
||||
activeKey={selectedTab}
|
||||
onChange={setSelectedTab}
|
||||
items={[
|
||||
{ label: "Basic", key: "basic" },
|
||||
{ label: "With Messages", key: "messages" },
|
||||
{ label: "With Version", key: "version" },
|
||||
]}
|
||||
/>
|
||||
|
||||
<SyntaxHighlighter
|
||||
language={selectedLanguage === "curl" ? "bash" : selectedLanguage === "python" ? "python" : "javascript"}
|
||||
style={coy as any}
|
||||
wrapLines={true}
|
||||
wrapLongLines={true}
|
||||
className="rounded-md mt-0"
|
||||
customStyle={{
|
||||
maxHeight: "60vh",
|
||||
overflowY: "auto",
|
||||
marginTop: 0,
|
||||
borderTopLeftRadius: 0,
|
||||
borderTopRightRadius: 0,
|
||||
}}
|
||||
>
|
||||
{generatedCode}
|
||||
</SyntaxHighlighter>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PromptCodeSnippets;
|
||||
|
||||
|
|
@ -2,6 +2,7 @@ import React from "react";
|
|||
import { Button as TremorButton } from "@tremor/react";
|
||||
import { Input } from "antd";
|
||||
import { ArrowLeftIcon, SaveIcon, ClockIcon } from "lucide-react";
|
||||
import PromptCodeSnippets from "./PromptCodeSnippets";
|
||||
|
||||
interface PromptEditorHeaderProps {
|
||||
promptName: string;
|
||||
|
|
@ -12,6 +13,13 @@ interface PromptEditorHeaderProps {
|
|||
editMode?: boolean;
|
||||
onShowHistory?: () => void;
|
||||
version?: string | null;
|
||||
promptModel?: string;
|
||||
promptVariables?: Record<string, string>;
|
||||
accessToken: string | null;
|
||||
proxySettings?: {
|
||||
PROXY_BASE_URL?: string;
|
||||
LITELLM_UI_API_DOC_BASE_URL?: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
const PromptEditorHeader: React.FC<PromptEditorHeaderProps> = ({
|
||||
|
|
@ -23,6 +31,10 @@ const PromptEditorHeader: React.FC<PromptEditorHeaderProps> = ({
|
|||
editMode = false,
|
||||
onShowHistory,
|
||||
version,
|
||||
promptModel = "gpt-4o",
|
||||
promptVariables = {},
|
||||
accessToken,
|
||||
proxySettings,
|
||||
}) => {
|
||||
return (
|
||||
<div className="bg-white border-b border-gray-200 px-6 py-3 flex items-center justify-between">
|
||||
|
|
@ -45,6 +57,14 @@ const PromptEditorHeader: React.FC<PromptEditorHeaderProps> = ({
|
|||
<span className="text-xs text-gray-400">Unsaved changes</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<PromptCodeSnippets
|
||||
promptId={promptName}
|
||||
model={promptModel}
|
||||
promptVariables={promptVariables}
|
||||
accessToken={accessToken}
|
||||
version={version?.replace('v', '') || "1"}
|
||||
proxySettings={proxySettings}
|
||||
/>
|
||||
{editMode && onShowHistory && (
|
||||
<TremorButton
|
||||
icon={ClockIcon}
|
||||
|
|
|
|||
|
|
@ -44,9 +44,19 @@ const VersionHistorySidePanel: React.FC<VersionHistorySidePanelProps> = ({
|
|||
}
|
||||
};
|
||||
|
||||
const getVersionNumber = (pid: string) => {
|
||||
if (pid.includes(".v")) {
|
||||
return `v${pid.split(".v")[1]}`;
|
||||
const getVersionNumber = (prompt: PromptSpec) => {
|
||||
// Use explicit version field if available, otherwise try to extract from litellm_params.prompt_id
|
||||
if (prompt.version) {
|
||||
return `v${prompt.version}`;
|
||||
}
|
||||
|
||||
// Fallback: try to extract from litellm_params.prompt_id
|
||||
const versionedId = (prompt.litellm_params as any)?.prompt_id || prompt.prompt_id;
|
||||
if (versionedId.includes(".v")) {
|
||||
return `v${versionedId.split(".v")[1]}`;
|
||||
}
|
||||
if (versionedId.includes("_v")) {
|
||||
return `v${versionedId.split("_v")[1]}`;
|
||||
}
|
||||
return "v1";
|
||||
};
|
||||
|
|
@ -74,10 +84,25 @@ const VersionHistorySidePanel: React.FC<VersionHistorySidePanelProps> = ({
|
|||
<List
|
||||
dataSource={versions}
|
||||
renderItem={(item, index) => {
|
||||
const isSelected = item.prompt_id === (activeVersionId || promptId);
|
||||
// Use version field for comparison since all items have the same prompt_id
|
||||
const itemVersionNum = item.version || parseInt(getVersionNumber(item).replace('v', ''));
|
||||
|
||||
// Extract version number from activeVersionId (may have .vX suffix)
|
||||
let activeVersionNum: number | null = null;
|
||||
if (activeVersionId) {
|
||||
if (activeVersionId.includes('.v')) {
|
||||
activeVersionNum = parseInt(activeVersionId.split('.v')[1]);
|
||||
} else if (activeVersionId.includes('_v')) {
|
||||
activeVersionNum = parseInt(activeVersionId.split('_v')[1]);
|
||||
}
|
||||
}
|
||||
|
||||
// Default to latest (first item) if no activeVersionId
|
||||
const isSelected = activeVersionNum ? itemVersionNum === activeVersionNum : index === 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.prompt_id}
|
||||
key={`${item.prompt_id}-v${item.version || itemVersionNum}`}
|
||||
className={`mb-4 p-4 rounded-lg border cursor-pointer transition-all hover:shadow-md ${
|
||||
isSelected ? "border-blue-500 bg-blue-50" : "border-gray-200 bg-white hover:border-blue-300"
|
||||
}`}
|
||||
|
|
@ -85,12 +110,10 @@ const VersionHistorySidePanel: React.FC<VersionHistorySidePanelProps> = ({
|
|||
>
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Tag className="m-0">{getVersionNumber(item.prompt_id)}</Tag>
|
||||
{index === 0 && (
|
||||
<Tag color="blue" className="m-0">
|
||||
Latest
|
||||
</Tag>
|
||||
)}
|
||||
<Tag className="m-0">
|
||||
{getVersionNumber(item)}
|
||||
</Tag>
|
||||
{index === 0 && <Tag color="blue" className="m-0">Latest</Tag>}
|
||||
</div>
|
||||
{isSelected && (
|
||||
<Tag color="green" className="m-0">
|
||||
|
|
|
|||
|
|
@ -45,7 +45,28 @@ const PromptEditorView: React.FC<PromptEditorViewProps> = ({ onClose, onSuccess,
|
|||
const [prompt, setPrompt] = useState<PromptType>(getInitialPrompt());
|
||||
const [editMode, setEditMode] = useState<boolean>(!!initialPromptData);
|
||||
const [showHistoryModal, setShowHistoryModal] = useState(false);
|
||||
const [activeVersionId, setActiveVersionId] = useState<string | undefined>(initialPromptData?.prompt_spec?.prompt_id);
|
||||
|
||||
// Construct versioned ID from prompt_id and version field
|
||||
const getInitialVersionId = () => {
|
||||
if (!initialPromptData?.prompt_spec) return undefined;
|
||||
const baseId = initialPromptData.prompt_spec.prompt_id;
|
||||
const version = initialPromptData.prompt_spec.version ||
|
||||
(initialPromptData.prompt_spec.litellm_params as any)?.prompt_id;
|
||||
|
||||
// If version is a number, construct versioned ID
|
||||
if (typeof version === 'number') {
|
||||
return `${baseId}.v${version}`;
|
||||
}
|
||||
|
||||
// If version is a string with version suffix, use it
|
||||
if (typeof version === 'string' && (version.includes('.v') || version.includes('_v'))) {
|
||||
return version;
|
||||
}
|
||||
|
||||
return baseId;
|
||||
};
|
||||
|
||||
const [activeVersionId, setActiveVersionId] = useState<string | undefined>(getInitialVersionId());
|
||||
|
||||
const [showToolModal, setShowToolModal] = useState(false);
|
||||
const [showNameModal, setShowNameModal] = useState(false);
|
||||
|
|
@ -144,8 +165,10 @@ const PromptEditorView: React.FC<PromptEditorViewProps> = ({ onClose, onSuccess,
|
|||
try {
|
||||
const loadedPrompt = parseExistingPrompt({ prompt_spec: versionData });
|
||||
setPrompt(loadedPrompt);
|
||||
setActiveVersionId(versionData.prompt_id);
|
||||
// NotificationsManager.success(`Loaded version ${versionData.prompt_id}`);
|
||||
// Store the version number or construct versioned ID for tracking
|
||||
const versionNum = versionData.version || 1;
|
||||
setActiveVersionId(`${versionData.prompt_id}.v${versionNum}`);
|
||||
// NotificationsManager.success(`Loaded version v${versionNum}`);
|
||||
} catch (error) {
|
||||
console.error("Error loading version:", error);
|
||||
NotificationsManager.fromBackend("Failed to load prompt version");
|
||||
|
|
@ -216,6 +239,25 @@ const PromptEditorView: React.FC<PromptEditorViewProps> = ({ onClose, onSuccess,
|
|||
|
||||
const currentVersion = getVersionNumber(activeVersionId);
|
||||
|
||||
// Extract template variables from prompt content for code examples
|
||||
const extractTemplateVariables = (): Record<string, string> => {
|
||||
const variables: Record<string, string> = {};
|
||||
const allContent = [
|
||||
prompt.developerMessage,
|
||||
...prompt.messages.map(m => m.content)
|
||||
].join(' ');
|
||||
|
||||
const variableRegex = /\{\{(\w+)\}\}/g;
|
||||
let match;
|
||||
while ((match = variableRegex.exec(allContent)) !== null) {
|
||||
const varName = match[1];
|
||||
if (!variables[varName]) {
|
||||
variables[varName] = `example_${varName}`;
|
||||
}
|
||||
}
|
||||
return variables;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full bg-white">
|
||||
<div className="flex-1 flex flex-col">
|
||||
|
|
@ -228,6 +270,9 @@ const PromptEditorView: React.FC<PromptEditorViewProps> = ({ onClose, onSuccess,
|
|||
editMode={editMode}
|
||||
onShowHistory={() => setShowHistoryModal(true)}
|
||||
version={currentVersion}
|
||||
promptModel={prompt.model}
|
||||
promptVariables={extractTemplateVariables()}
|
||||
accessToken={accessToken}
|
||||
/>
|
||||
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
|
|
|
|||
|
|
@ -146,8 +146,12 @@ export const parseExistingPrompt = (apiResponse: any): PromptType => {
|
|||
const tools: Tool[] = [];
|
||||
// TODO: Add tool parsing if needed
|
||||
|
||||
// Strip version suffix from prompt name for display
|
||||
const promptId = apiResponse?.prompt_spec?.prompt_id || "Unnamed Prompt";
|
||||
const baseName = stripVersionFromPromptId(promptId) || promptId;
|
||||
|
||||
return {
|
||||
name: apiResponse?.prompt_spec?.prompt_id || "Unnamed Prompt",
|
||||
name: baseName,
|
||||
model: metadata.model || "gpt-4o",
|
||||
config: {
|
||||
temperature: metadata.temperature,
|
||||
|
|
@ -159,3 +163,16 @@ export const parseExistingPrompt = (apiResponse: any): PromptType => {
|
|||
messages: messages.length > 0 ? messages : [{ role: "user", content: "Enter task specifics. Use {{template_variables}} for dynamic inputs" }],
|
||||
};
|
||||
};
|
||||
|
||||
export const getVersionNumber = (promptId?: string): string => {
|
||||
if (!promptId) return "1";
|
||||
// Match version with dot (.v), underscore (_v), or hyphen (-v) separator
|
||||
const match = promptId.match(/[._-]v(\d+)$/);
|
||||
return match ? match[1] : "1";
|
||||
};
|
||||
|
||||
export const stripVersionFromPromptId = (promptId?: string): string => {
|
||||
if (!promptId) return "";
|
||||
// Remove version suffix with dot (.v), underscore (_v), or hyphen (-v) separator
|
||||
return promptId.replace(/[._-]v\d+$/, "");
|
||||
};
|
||||
|
|
|
|||
|
|
@ -18,6 +18,13 @@ import { getPromptInfo, PromptSpec, PromptTemplateBase, deletePromptCall } from
|
|||
import { copyToClipboard as utilCopyToClipboard } from "@/utils/dataUtils";
|
||||
import { CheckIcon, CopyIcon } from "lucide-react";
|
||||
import NotificationsManager from "../molecules/notifications_manager";
|
||||
import PromptCodeSnippets from "./prompt_editor_view/PromptCodeSnippets";
|
||||
import {
|
||||
extractModel,
|
||||
extractTemplateVariables,
|
||||
getBasePromptId,
|
||||
getCurrentVersion
|
||||
} from "./prompt_utils";
|
||||
|
||||
export interface PromptInfoProps {
|
||||
promptId: string;
|
||||
|
|
@ -91,8 +98,8 @@ const PromptInfoView: React.FC<PromptInfoProps> = ({ promptId, onClose, accessTo
|
|||
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
await deletePromptCall(accessToken, promptData.prompt_id);
|
||||
NotificationsManager.success(`Prompt "${promptData.prompt_id}" deleted successfully`);
|
||||
await deletePromptCall(accessToken, basePromptId);
|
||||
NotificationsManager.success(`Prompt "${basePromptId}" deleted successfully`);
|
||||
onDelete?.(); // Call the callback to refresh the parent component
|
||||
onClose(); // Close the info view
|
||||
} catch (error) {
|
||||
|
|
@ -108,6 +115,11 @@ const PromptInfoView: React.FC<PromptInfoProps> = ({ promptId, onClose, accessTo
|
|||
setShowDeleteConfirm(false);
|
||||
};
|
||||
|
||||
// Use utility functions to extract prompt data
|
||||
const promptModel = promptData ? extractModel(promptData) || "gpt-4o" : "gpt-4o";
|
||||
const basePromptId = getBasePromptId(promptData);
|
||||
const currentVersion = getCurrentVersion(promptData);
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div>
|
||||
|
|
@ -118,12 +130,12 @@ const PromptInfoView: React.FC<PromptInfoProps> = ({ promptId, onClose, accessTo
|
|||
<div>
|
||||
<Title>Prompt Details</Title>
|
||||
<div className="flex items-center cursor-pointer">
|
||||
<Text className="text-gray-500 font-mono">{promptData.prompt_id}</Text>
|
||||
<Text className="text-gray-500 font-mono">{basePromptId}</Text>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={copiedStates["prompt-id"] ? <CheckIcon size={12} /> : <CopyIcon size={12} />}
|
||||
onClick={() => copyToClipboard(promptData.prompt_id, "prompt-id")}
|
||||
onClick={() => copyToClipboard(basePromptId, "prompt-id")}
|
||||
className={`left-2 z-10 transition-all duration-200 ${
|
||||
copiedStates["prompt-id"]
|
||||
? "text-green-600 bg-green-50 border-green-200"
|
||||
|
|
@ -133,13 +145,20 @@ const PromptInfoView: React.FC<PromptInfoProps> = ({ promptId, onClose, accessTo
|
|||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<PromptCodeSnippets
|
||||
promptId={basePromptId}
|
||||
model={promptModel}
|
||||
promptVariables={extractTemplateVariables(promptTemplate?.content)}
|
||||
accessToken={accessToken}
|
||||
version={currentVersion}
|
||||
/>
|
||||
<TremorButton
|
||||
icon={PencilIcon}
|
||||
variant="primary"
|
||||
onClick={() => onEdit?.(rawApiResponse)}
|
||||
className="flex items-center"
|
||||
>
|
||||
Edit Prompt
|
||||
Prompt Studio
|
||||
</TremorButton>
|
||||
{isAdmin && (
|
||||
<TremorButton
|
||||
|
|
@ -170,7 +189,17 @@ const PromptInfoView: React.FC<PromptInfoProps> = ({ promptId, onClose, accessTo
|
|||
<Card>
|
||||
<Text>Prompt ID</Text>
|
||||
<div className="mt-2">
|
||||
<Title className="font-mono text-sm">{promptData.prompt_id}</Title>
|
||||
<Title className="font-mono text-sm">{basePromptId}</Title>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Text>Version</Text>
|
||||
<div className="mt-2">
|
||||
<Title>{currentVersion}</Title>
|
||||
<Badge color="blue" className="mt-1">
|
||||
v{currentVersion}
|
||||
</Badge>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
|
|
@ -262,7 +291,7 @@ const PromptInfoView: React.FC<PromptInfoProps> = ({ promptId, onClose, accessTo
|
|||
<div className="space-y-4">
|
||||
<div>
|
||||
<Text className="font-medium">Prompt ID</Text>
|
||||
<div className="font-mono text-sm bg-gray-50 p-2 rounded">{promptData.prompt_id}</div>
|
||||
<div className="font-mono text-sm bg-gray-50 p-2 rounded">{basePromptId}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
|
@ -343,7 +372,7 @@ const PromptInfoView: React.FC<PromptInfoProps> = ({ promptId, onClose, accessTo
|
|||
okButtonProps={{ danger: true }}
|
||||
>
|
||||
<p>
|
||||
Are you sure you want to delete prompt: <strong>{promptData?.prompt_id}</strong>?
|
||||
Are you sure you want to delete prompt: <strong>{basePromptId}</strong>?
|
||||
</p>
|
||||
<p>This action cannot be undone.</p>
|
||||
</Modal>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { PromptSpec } from "@/components/networking";
|
||||
import { getVersionNumber } from "./prompt_editor_view/utils";
|
||||
|
||||
interface ModelGroupInfo {
|
||||
model_group: string;
|
||||
|
|
@ -6,6 +7,54 @@ interface ModelGroupInfo {
|
|||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract template variables from prompt content
|
||||
*/
|
||||
export const extractTemplateVariables = (content?: string): Record<string, string> => {
|
||||
if (!content) return {};
|
||||
|
||||
const variables: Record<string, string> = {};
|
||||
const variableRegex = /\{\{(\w+)\}\}/g;
|
||||
let match;
|
||||
while ((match = variableRegex.exec(content)) !== null) {
|
||||
const varName = match[1];
|
||||
if (!variables[varName]) {
|
||||
variables[varName] = `example_${varName}`;
|
||||
}
|
||||
}
|
||||
return variables;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get base prompt ID (stripped of version) from PromptSpec
|
||||
*/
|
||||
export const getBasePromptId = (promptData?: PromptSpec): string => {
|
||||
return promptData?.prompt_id || "";
|
||||
};
|
||||
|
||||
/**
|
||||
* Get versioned prompt ID from litellm_params (preserves version)
|
||||
*/
|
||||
export const getVersionedPromptId = (promptData?: PromptSpec): string => {
|
||||
const baseId = getBasePromptId(promptData);
|
||||
const versionedId = (promptData?.litellm_params as any)?.prompt_id || baseId;
|
||||
return versionedId;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get current version number from prompt data
|
||||
*/
|
||||
export const getCurrentVersion = (promptData?: PromptSpec): string => {
|
||||
// Use explicit version field if available (from API response)
|
||||
if (promptData?.version) {
|
||||
return String(promptData.version);
|
||||
}
|
||||
|
||||
// Fallback: extract from versioned ID in litellm_params
|
||||
const versionedId = getVersionedPromptId(promptData);
|
||||
return getVersionNumber(versionedId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract model from prompt litellm_params
|
||||
*/
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue