106 lines
3.5 KiB
Python
106 lines
3.5 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
import os
|
|
from typing import Any
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from .prompt_release import PromptReleaseService
|
|
|
|
|
|
PLATFORM_RUNTIME_TENANT_ID = int(os.getenv("PMFLOW_PLATFORM_RUNTIME_TENANT_ID", "1") or "1")
|
|
|
|
|
|
class PromptRuntimeError(RuntimeError):
|
|
"""Raised when a runtime prompt cannot be rendered from an active release."""
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class PromptRuntimeRenderResult:
|
|
prompt: str
|
|
prompt_key: str
|
|
source: str
|
|
release_id: int | None = None
|
|
runtime_tenant_id: int | None = None
|
|
format_error: bool = False
|
|
|
|
def routing_meta(self) -> dict[str, Any]:
|
|
return {
|
|
"prompt_key": self.prompt_key,
|
|
"prompt_release_id": self.release_id,
|
|
"prompt_source": self.source,
|
|
"prompt_runtime_tenant_id": self.runtime_tenant_id,
|
|
"prompt_format_error": self.format_error,
|
|
}
|
|
|
|
|
|
class PromptRuntimeService:
|
|
"""Runtime access layer for tenant-managed prompt releases.
|
|
|
|
The service keeps call sites away from direct prompt release storage details.
|
|
"""
|
|
|
|
def __init__(self, db: AsyncSession) -> None:
|
|
self.db = db
|
|
|
|
async def render(
|
|
self,
|
|
*,
|
|
tenant_id: int,
|
|
prompt_key: str,
|
|
fallback_template: str = "",
|
|
**variables: Any,
|
|
) -> PromptRuntimeRenderResult:
|
|
try:
|
|
release_service = PromptReleaseService(self.db)
|
|
active_release = await release_service.get_active_release(
|
|
tenant_id=tenant_id,
|
|
prompt_key=prompt_key,
|
|
)
|
|
if not active_release and int(tenant_id) != PLATFORM_RUNTIME_TENANT_ID:
|
|
active_release = await release_service.get_active_release(
|
|
tenant_id=PLATFORM_RUNTIME_TENANT_ID,
|
|
prompt_key=prompt_key,
|
|
)
|
|
except Exception:
|
|
active_release = None
|
|
active_template = str(active_release.get("body_snapshot") or "") if active_release else ""
|
|
fallback = str(fallback_template or "").strip()
|
|
if not active_template and not fallback:
|
|
raise PromptRuntimeError(f"active prompt release not found: {prompt_key}")
|
|
template = active_template or fallback
|
|
release_id = int(active_release["id"]) if active_release and active_release.get("id") is not None else None
|
|
runtime_tenant_id = int(active_release["tenant_id"]) if active_release and active_release.get("tenant_id") is not None else int(tenant_id)
|
|
if active_template:
|
|
source = "active_release" if runtime_tenant_id == int(tenant_id) else "platform_active_release"
|
|
else:
|
|
source = "default_fallback"
|
|
try:
|
|
prompt = template.format(**variables).strip()
|
|
return PromptRuntimeRenderResult(
|
|
prompt=prompt,
|
|
prompt_key=prompt_key,
|
|
source=source,
|
|
release_id=release_id,
|
|
runtime_tenant_id=runtime_tenant_id,
|
|
)
|
|
except Exception as exc:
|
|
raise PromptRuntimeError(f"{source} prompt render failed: {prompt_key}") from exc
|
|
|
|
async def render_text(
|
|
self,
|
|
*,
|
|
tenant_id: int,
|
|
prompt_key: str,
|
|
fallback_template: str = "",
|
|
**variables: Any,
|
|
) -> str:
|
|
result = await self.render(
|
|
tenant_id=tenant_id,
|
|
prompt_key=prompt_key,
|
|
fallback_template=fallback_template,
|
|
**variables,
|
|
)
|
|
return result.prompt
|