PMFlow/backend/app/services/prompt_runtime_service.py

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