PMFlow/backend/app/services/prompt_release/service.py

157 lines
5.3 KiB
Python

from __future__ import annotations
from typing import Any, Optional
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from ...config.ai_prompt_registry import get_prompt_registry_entry
from ...models import PromptRelease
from ...utils.timezone import now_cn_naive
class PromptReleaseService:
def __init__(self, db: AsyncSession):
self.db = db
@staticmethod
def _snapshot(row: PromptRelease) -> dict[str, Any]:
return {
"id": int(row.id),
"tenant_id": int(row.tenant_id),
"prompt_key": row.prompt_key,
"version": int(row.version),
"body_snapshot": row.body_snapshot,
"status": row.status,
"notes": row.notes,
"created_by": int(row.created_by) if row.created_by is not None else None,
"activated_by": int(row.activated_by) if row.activated_by is not None else None,
"activated_at": row.activated_at,
}
async def create_release(
self,
*,
tenant_id: int,
prompt_key: str,
body_snapshot: str,
created_by: Optional[int],
notes: Optional[str] = None,
) -> dict[str, Any]:
prompt_key = str(prompt_key or "").strip()
if not prompt_key:
raise ValueError("prompt_key is required")
if get_prompt_registry_entry(prompt_key) is None:
raise ValueError("unsupported prompt_key")
body_snapshot = str(body_snapshot or "").strip()
if not body_snapshot:
raise ValueError("body_snapshot is required")
current_max = (
await self.db.execute(
select(func.max(PromptRelease.version)).where(
PromptRelease.tenant_id == int(tenant_id),
PromptRelease.prompt_key == prompt_key,
)
)
).scalar_one_or_none()
row = PromptRelease(
tenant_id=int(tenant_id),
prompt_key=prompt_key,
version=int(current_max or 0) + 1,
body_snapshot=body_snapshot,
status="draft",
notes=str(notes or "").strip() or None,
created_by=created_by,
)
self.db.add(row)
await self.db.commit()
await self.db.refresh(row)
return self._snapshot(row)
async def activate_release(
self,
*,
tenant_id: int,
release_id: int,
actor_id: Optional[int],
) -> dict[str, Any]:
row = (
await self.db.execute(
select(PromptRelease).where(
PromptRelease.tenant_id == int(tenant_id),
PromptRelease.id == int(release_id),
)
)
).scalar_one_or_none()
if row is None:
raise ValueError("prompt release not found")
previous = (
await self.db.execute(
select(PromptRelease).where(
PromptRelease.tenant_id == int(tenant_id),
PromptRelease.prompt_key == row.prompt_key,
PromptRelease.status == "active",
)
)
).scalars().all()
for item in previous:
item.status = "archived"
row.status = "active"
row.activated_by = actor_id
row.activated_at = now_cn_naive().isoformat()
await self.db.commit()
await self.db.refresh(row)
return self._snapshot(row)
async def list_releases(
self,
*,
tenant_id: int,
prompt_key: Optional[str] = None,
) -> dict[str, Any]:
stmt = select(PromptRelease).where(PromptRelease.tenant_id == int(tenant_id))
if prompt_key:
stmt = stmt.where(PromptRelease.prompt_key == str(prompt_key).strip())
rows = (
await self.db.execute(
stmt.order_by(PromptRelease.prompt_key.asc(), PromptRelease.version.desc(), PromptRelease.id.desc())
)
).scalars().all()
return {"items": [self._snapshot(row) for row in rows], "total": len(rows)}
async def get_active_template(
self,
*,
tenant_id: int,
prompt_key: str,
) -> Optional[str]:
row = (
await self.db.execute(
select(PromptRelease.body_snapshot).where(
PromptRelease.tenant_id == int(tenant_id),
PromptRelease.prompt_key == str(prompt_key or "").strip(),
PromptRelease.status == "active",
)
)
).scalar_one_or_none()
return str(row).strip() if row else None
async def render_prompt(
self,
*,
tenant_id: int,
prompt_key: str,
fallback_template: str,
_override_template: Optional[str] = None,
**kwargs: Any,
) -> str:
template = str(_override_template or "").strip() if _override_template is not None else ""
if not template:
template = str(await self.get_active_template(tenant_id=tenant_id, prompt_key=prompt_key) or "").strip()
if not template:
template = str(fallback_template or "")
try:
return str(template).format(**kwargs).strip()
except Exception:
return str(fallback_template or "").format(**kwargs).strip()