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 "" if not active_template: raise PromptRuntimeError(f"active prompt release not found: {prompt_key}") 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) source = "active_release" if active_template and runtime_tenant_id == int(tenant_id) else "platform_active_release" try: prompt = active_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"active prompt release 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