mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-14 16:25:29 +00:00
[Feat] Prompt Management - Allow storing prompt version in DB (#16848)
* test_dotprompt_auto_detection_with_model_only * fix _auto_detect_prompt_management_logger * test_dotprompt_with_prompt_version * add v1, v2 tests * add _compile_prompt_helper * fix _compile_prompt_helper * test_dotprompt_with_prompt_version * test_dotprompt_with_prompt_version, test_get_prompt_with_version * add version in schema * feat add _get_prompt_spec_for_db_prompt * add _get_prompt_spec_for_db_prompt * feat add _get_prompt_spec_for_db_prompt * update prompt table * add version in prompt DB * test_get_prompt_spec_for_db_prompt_with_versions
This commit is contained in:
+12
@@ -0,0 +1,12 @@
|
||||
-- DropIndex
|
||||
DROP INDEX "LiteLLM_PromptTable_prompt_id_key";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "LiteLLM_PromptTable" ADD COLUMN "version" INTEGER NOT NULL DEFAULT 1;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "LiteLLM_PromptTable_prompt_id_idx" ON "LiteLLM_PromptTable"("prompt_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "LiteLLM_PromptTable_prompt_id_version_key" ON "LiteLLM_PromptTable"("prompt_id", "version");
|
||||
|
||||
@@ -561,11 +561,15 @@ model LiteLLM_GuardrailsTable {
|
||||
// Prompt table for storing prompt configurations
|
||||
model LiteLLM_PromptTable {
|
||||
id String @id @default(uuid())
|
||||
prompt_id String @unique
|
||||
prompt_id String
|
||||
version Int @default(1)
|
||||
litellm_params Json
|
||||
prompt_info Json?
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
|
||||
@@unique([prompt_id, version])
|
||||
@@index([prompt_id])
|
||||
}
|
||||
|
||||
model LiteLLM_HealthCheckTable {
|
||||
|
||||
@@ -21,10 +21,76 @@ from litellm.types.prompts.init_prompts import (
|
||||
PromptTemplateBase,
|
||||
)
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
async def get_next_version_for_prompt(prisma_client, prompt_id: str) -> int:
|
||||
"""
|
||||
Get the next version number for a prompt.
|
||||
|
||||
Args:
|
||||
prisma_client: Prisma database client
|
||||
prompt_id: Base prompt ID
|
||||
|
||||
Returns:
|
||||
Next version number (1 if no versions exist, max_version + 1 otherwise)
|
||||
"""
|
||||
existing_prompts = await prisma_client.db.litellm_prompttable.find_many(
|
||||
where={"prompt_id": prompt_id}
|
||||
)
|
||||
|
||||
if existing_prompts:
|
||||
max_version = max(p.version for p in existing_prompts)
|
||||
return max_version + 1
|
||||
else:
|
||||
return 1
|
||||
|
||||
|
||||
def create_versioned_prompt_spec(db_prompt) -> PromptSpec:
|
||||
"""
|
||||
Helper function to create a PromptSpec with versioned prompt_id from a DB prompt entry.
|
||||
|
||||
Args:
|
||||
db_prompt: The DB prompt object (from prisma)
|
||||
|
||||
Returns:
|
||||
PromptSpec with versioned prompt_id (e.g., "chat_prompt.v1")
|
||||
"""
|
||||
import json
|
||||
|
||||
from litellm.types.prompts.init_prompts import PromptLiteLLMParams
|
||||
|
||||
prompt_dict = db_prompt.model_dump()
|
||||
base_prompt_id = prompt_dict["prompt_id"]
|
||||
version = prompt_dict.get("version", 1)
|
||||
|
||||
# Parse litellm_params
|
||||
litellm_params_data = prompt_dict.get("litellm_params")
|
||||
if isinstance(litellm_params_data, str):
|
||||
litellm_params_data = json.loads(litellm_params_data)
|
||||
litellm_params = PromptLiteLLMParams(**litellm_params_data)
|
||||
|
||||
# Parse prompt_info
|
||||
prompt_info_data = prompt_dict.get("prompt_info")
|
||||
if prompt_info_data:
|
||||
if isinstance(prompt_info_data, str):
|
||||
prompt_info_data = json.loads(prompt_info_data)
|
||||
prompt_info = PromptInfo(**prompt_info_data)
|
||||
else:
|
||||
prompt_info = PromptInfo(prompt_type="db")
|
||||
|
||||
# Create versioned prompt_id
|
||||
versioned_prompt_id = f"{base_prompt_id}.v{version}"
|
||||
|
||||
return PromptSpec(
|
||||
prompt_id=versioned_prompt_id,
|
||||
litellm_params=litellm_params,
|
||||
prompt_info=prompt_info,
|
||||
created_at=prompt_dict.get("created_at"),
|
||||
updated_at=prompt_dict.get("updated_at"),
|
||||
)
|
||||
|
||||
|
||||
class Prompt(BaseModel):
|
||||
prompt_id: str
|
||||
litellm_params: PromptLiteLLMParams
|
||||
@@ -261,19 +327,16 @@ async def create_prompt(
|
||||
)
|
||||
|
||||
try:
|
||||
# Create the prompt spec
|
||||
# Check if prompt exists and get current data
|
||||
existing_prompt = IN_MEMORY_PROMPT_REGISTRY.get_prompt_by_id(request.prompt_id)
|
||||
if existing_prompt is not None:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Prompt with ID {request.prompt_id} already exists",
|
||||
)
|
||||
# Get next version number
|
||||
new_version = await get_next_version_for_prompt(
|
||||
prisma_client=prisma_client, prompt_id=request.prompt_id
|
||||
)
|
||||
|
||||
# store prompt in db
|
||||
# Store prompt in db with version
|
||||
prompt_db_entry = await prisma_client.db.litellm_prompttable.create(
|
||||
data={
|
||||
"prompt_id": request.prompt_id,
|
||||
"version": new_version,
|
||||
"litellm_params": request.litellm_params.model_dump_json(),
|
||||
"prompt_info": (
|
||||
request.prompt_info.model_dump_json()
|
||||
@@ -283,7 +346,8 @@ async def create_prompt(
|
||||
}
|
||||
)
|
||||
|
||||
prompt_spec = PromptSpec(**prompt_db_entry.model_dump())
|
||||
# Create versioned prompt spec
|
||||
prompt_spec = create_versioned_prompt_spec(db_prompt=prompt_db_entry)
|
||||
|
||||
# Initialize the prompt
|
||||
initialized_prompt = IN_MEMORY_PROMPT_REGISTRY.initialize_prompt(
|
||||
@@ -354,45 +418,50 @@ async def update_prompt(
|
||||
)
|
||||
|
||||
try:
|
||||
# Check if prompt exists
|
||||
existing_prompt = IN_MEMORY_PROMPT_REGISTRY.get_prompt_by_id(prompt_id)
|
||||
if existing_prompt is None:
|
||||
# Check if any version exists
|
||||
existing_prompts = await prisma_client.db.litellm_prompttable.find_many(
|
||||
where={"prompt_id": request.prompt_id}
|
||||
)
|
||||
|
||||
if not existing_prompts:
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"Prompt with ID {prompt_id} not found"
|
||||
status_code=404, detail=f"Prompt with ID {request.prompt_id} not found"
|
||||
)
|
||||
|
||||
if existing_prompt.prompt_info.prompt_type == "config":
|
||||
# Check if it's a config prompt
|
||||
base_prompt_id = request.prompt_id
|
||||
existing_in_memory = IN_MEMORY_PROMPT_REGISTRY.get_prompt_by_id(base_prompt_id)
|
||||
if existing_in_memory and existing_in_memory.prompt_info.prompt_type == "config":
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Cannot update config prompts.",
|
||||
)
|
||||
|
||||
# Create updated prompt spec
|
||||
updated_prompt_spec = PromptSpec(
|
||||
prompt_id=prompt_id,
|
||||
litellm_params=request.litellm_params,
|
||||
prompt_info=request.prompt_info or PromptInfo(prompt_type="db"),
|
||||
created_at=existing_prompt.created_at,
|
||||
updated_at=datetime.now(),
|
||||
# Get next version number (UPDATE creates a new version)
|
||||
new_version = await get_next_version_for_prompt(
|
||||
prisma_client=prisma_client, prompt_id=request.prompt_id
|
||||
)
|
||||
|
||||
updated_prompt_db_entry = await prisma_client.db.litellm_prompttable.update(
|
||||
where={"prompt_id": prompt_id},
|
||||
# Store new version in db
|
||||
prompt_db_entry = await prisma_client.db.litellm_prompttable.create(
|
||||
data={
|
||||
"litellm_params": updated_prompt_spec.litellm_params.model_dump_json(),
|
||||
"prompt_info": updated_prompt_spec.prompt_info.model_dump_json(),
|
||||
},
|
||||
"prompt_id": request.prompt_id,
|
||||
"version": new_version,
|
||||
"litellm_params": request.litellm_params.model_dump_json(),
|
||||
"prompt_info": (
|
||||
request.prompt_info.model_dump_json()
|
||||
if request.prompt_info
|
||||
else PromptInfo(prompt_type="db").model_dump_json()
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
# Remove the old prompt from memory
|
||||
del IN_MEMORY_PROMPT_REGISTRY.IN_MEMORY_PROMPTS[prompt_id]
|
||||
if prompt_id in IN_MEMORY_PROMPT_REGISTRY.prompt_id_to_custom_prompt:
|
||||
del IN_MEMORY_PROMPT_REGISTRY.prompt_id_to_custom_prompt[prompt_id]
|
||||
# Create versioned prompt spec
|
||||
prompt_spec = create_versioned_prompt_spec(db_prompt=prompt_db_entry)
|
||||
|
||||
# Initialize the updated prompt
|
||||
# Initialize the new version
|
||||
initialized_prompt = IN_MEMORY_PROMPT_REGISTRY.initialize_prompt(
|
||||
prompt=PromptSpec(**updated_prompt_db_entry.model_dump()),
|
||||
config_file_path=None,
|
||||
prompt=prompt_spec, config_file_path=None
|
||||
)
|
||||
|
||||
if initialized_prompt is None:
|
||||
|
||||
@@ -273,7 +273,9 @@ from litellm.proxy.management_endpoints.customer_endpoints import (
|
||||
from litellm.proxy.management_endpoints.internal_user_endpoints import (
|
||||
router as internal_user_router,
|
||||
)
|
||||
from litellm.proxy.management_endpoints.internal_user_endpoints import user_update
|
||||
from litellm.proxy.management_endpoints.internal_user_endpoints import (
|
||||
user_update,
|
||||
)
|
||||
from litellm.proxy.management_endpoints.key_management_endpoints import (
|
||||
delete_verification_tokens,
|
||||
duration_in_seconds,
|
||||
@@ -327,7 +329,9 @@ from litellm.proxy.ocr_endpoints.endpoints import router as ocr_router
|
||||
from litellm.proxy.openai_files_endpoints.files_endpoints import (
|
||||
router as openai_files_router,
|
||||
)
|
||||
from litellm.proxy.openai_files_endpoints.files_endpoints import set_files_config
|
||||
from litellm.proxy.openai_files_endpoints.files_endpoints import (
|
||||
set_files_config,
|
||||
)
|
||||
from litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints import (
|
||||
passthrough_endpoint_router,
|
||||
)
|
||||
@@ -417,7 +421,9 @@ from litellm.types.proxy.management_endpoints.ui_sso import (
|
||||
LiteLLM_UpperboundKeyGenerateParams,
|
||||
)
|
||||
from litellm.types.realtime import RealtimeQueryParams
|
||||
from litellm.types.router import DeploymentTypedDict
|
||||
from litellm.types.router import (
|
||||
DeploymentTypedDict,
|
||||
)
|
||||
from litellm.types.router import ModelInfo as RouterModelInfo
|
||||
from litellm.types.router import (
|
||||
RouterGeneralSettings,
|
||||
@@ -3576,14 +3582,32 @@ class ProxyConfig:
|
||||
verbose_proxy_logger.exception(
|
||||
f"Error in _check_and_reload_model_cost_map: {str(e)}"
|
||||
)
|
||||
def _get_prompt_spec_for_db_prompt(self, db_prompt):
|
||||
"""
|
||||
Convert a DB prompt object to a PromptSpec object.
|
||||
|
||||
Handles the versioning of the prompt, if the DB prompt has a version, it will be used to create the versioned prompt_id.
|
||||
|
||||
Args:
|
||||
db_prompt: The DB prompt object
|
||||
|
||||
Returns:
|
||||
The PromptSpec object
|
||||
"""
|
||||
from litellm.proxy.prompts.prompt_endpoints import create_versioned_prompt_spec
|
||||
|
||||
return create_versioned_prompt_spec(db_prompt=db_prompt)
|
||||
|
||||
async def _init_prompts_in_db(self, prisma_client: PrismaClient):
|
||||
from litellm.proxy.prompts.prompt_registry import IN_MEMORY_PROMPT_REGISTRY
|
||||
from litellm.types.prompts.init_prompts import PromptSpec
|
||||
|
||||
try:
|
||||
prompts_in_db = await prisma_client.db.litellm_prompttable.find_many()
|
||||
for prompt in prompts_in_db:
|
||||
IN_MEMORY_PROMPT_REGISTRY.initialize_prompt(prompt=prompt)
|
||||
# Convert DB object to dict and create versioned prompt_id
|
||||
prompt_spec = self._get_prompt_spec_for_db_prompt(db_prompt=prompt)
|
||||
IN_MEMORY_PROMPT_REGISTRY.initialize_prompt(prompt=prompt_spec)
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.debug(
|
||||
"litellm.proxy.proxy_server.py::ProxyConfig:_init_prompts_in_db - {}".format(
|
||||
|
||||
@@ -561,11 +561,15 @@ model LiteLLM_GuardrailsTable {
|
||||
// Prompt table for storing prompt configurations
|
||||
model LiteLLM_PromptTable {
|
||||
id String @id @default(uuid())
|
||||
prompt_id String @unique
|
||||
prompt_id String
|
||||
version Int @default(1)
|
||||
litellm_params Json
|
||||
prompt_info Json?
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
|
||||
@@unique([prompt_id, version])
|
||||
@@index([prompt_id])
|
||||
}
|
||||
|
||||
model LiteLLM_HealthCheckTable {
|
||||
|
||||
+5
-1
@@ -561,11 +561,15 @@ model LiteLLM_GuardrailsTable {
|
||||
// Prompt table for storing prompt configurations
|
||||
model LiteLLM_PromptTable {
|
||||
id String @id @default(uuid())
|
||||
prompt_id String @unique
|
||||
prompt_id String
|
||||
version Int @default(1)
|
||||
litellm_params Json
|
||||
prompt_info Json?
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
|
||||
@@unique([prompt_id, version])
|
||||
@@index([prompt_id])
|
||||
}
|
||||
|
||||
model LiteLLM_HealthCheckTable {
|
||||
|
||||
@@ -2450,3 +2450,47 @@ async def test_init_sso_settings_in_db_empty_settings():
|
||||
# Verify empty dictionary
|
||||
assert uppercased_settings == {}
|
||||
|
||||
|
||||
def test_get_prompt_spec_for_db_prompt_with_versions():
|
||||
"""
|
||||
Test that _get_prompt_spec_for_db_prompt correctly converts database prompts
|
||||
to PromptSpec with versioned naming convention.
|
||||
"""
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from litellm.proxy.proxy_server import ProxyConfig
|
||||
|
||||
proxy_config = ProxyConfig()
|
||||
|
||||
# Mock database prompt version 1
|
||||
mock_prompt_v1 = MagicMock()
|
||||
mock_prompt_v1.model_dump.return_value = {
|
||||
"id": "uuid-1",
|
||||
"prompt_id": "chat_prompt",
|
||||
"version": 1,
|
||||
"litellm_params": '{"prompt_id": "chat_prompt", "prompt_integration": "dotprompt", "model": "gpt-3.5-turbo", "messages": [{"role": "user", "content": "v1 content"}]}',
|
||||
"prompt_info": '{"prompt_type": "db"}',
|
||||
"created_at": "2024-01-01T00:00:00",
|
||||
"updated_at": "2024-01-01T00:00:00",
|
||||
}
|
||||
|
||||
# Mock database prompt version 2
|
||||
mock_prompt_v2 = MagicMock()
|
||||
mock_prompt_v2.model_dump.return_value = {
|
||||
"id": "uuid-2",
|
||||
"prompt_id": "chat_prompt",
|
||||
"version": 2,
|
||||
"litellm_params": '{"prompt_id": "chat_prompt", "prompt_integration": "dotprompt", "model": "gpt-4", "messages": [{"role": "user", "content": "v2 content"}]}',
|
||||
"prompt_info": '{"prompt_type": "db"}',
|
||||
"created_at": "2024-01-02T00:00:00",
|
||||
"updated_at": "2024-01-02T00:00:00",
|
||||
}
|
||||
|
||||
# Test version 1
|
||||
prompt_spec_v1 = proxy_config._get_prompt_spec_for_db_prompt(db_prompt=mock_prompt_v1)
|
||||
assert prompt_spec_v1.prompt_id == "chat_prompt.v1"
|
||||
|
||||
# Test version 2
|
||||
prompt_spec_v2 = proxy_config._get_prompt_spec_for_db_prompt(db_prompt=mock_prompt_v2)
|
||||
assert prompt_spec_v2.prompt_id == "chat_prompt.v2"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user