Files
xiaoxia-saas/packages/application/viral_video/prompt_loader.py
T
xiaoxia a6f89067e7
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 4s
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 19s
CI/CD Pipeline / Validate - Style (pull_request) Has been skipped
CI/CD Pipeline / Validate - Security (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 35s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 36s
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 55s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 55s
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 43s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m12s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 2m59s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m22s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 3m34s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 4m13s
CI/CD Pipeline / Validate - Style (push) Successful in 4m37s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 4m48s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m33s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m54s
AI Code Review / AI Code Review (pull_request) Successful in 6m46s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 4m30s
CI/CD Pipeline / Validate - Security (push) Successful in 10m55s
CI/CD Pipeline / Unit Tests (push) Successful in 11m17s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
fix(viral_video): #2040 loader在api进程内DB热加载(懒建同步引擎) (#2176)
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-10-04 16:07:25 +08:00

161 lines
5.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Prompt 模板加载器:从 viral_video_prompt_templates 读模板,30 秒 TTL 热加载。
DB 不可用或没有数据时自动回落到 prompts.DEFAULT_TEMPLATES,保证流程不阻断。
"""
from __future__ import annotations
import threading
import time
from dataclasses import dataclass
from typing import Optional
import sqlalchemy as sa
from packages.adapters.sqlalchemy_impl import session as _session_mod
from packages.application.viral_video.prompts import DEFAULT_TEMPLATES
CACHE_TTL_SECONDS = 30.0
_VALID_TYPES = {"image_analysis", "intent_parsing", "copy_fusion", "storyboard", "review"}
@dataclass
class PromptTemplate:
name: str
prompt_type: str
version: int
system_prompt: str
user_prompt_template: str
example_output: str = ""
is_active: bool = True
_lock = threading.Lock()
_cache: dict[str, tuple[float, PromptTemplate]] = {}
def _fallback(prompt_type: str) -> Optional[PromptTemplate]:
for item in DEFAULT_TEMPLATES:
if item["prompt_type"] == prompt_type:
return PromptTemplate(
name=item["name"],
prompt_type=item["prompt_type"],
version=item["version"],
system_prompt=item["system_prompt"],
user_prompt_template=item["user_prompt_template"],
example_output=item["example_output"] or "",
is_active=bool(item["is_active"]),
)
return None
_lazy_session = None
def _get_session():
"""优先用全局 SessionLocal(worker);否则按应用配置懒建同步引擎(api)。"""
global _lazy_session
if _session_mod.SessionLocal is not None:
return _session_mod.SessionLocal()
if _lazy_session is not None:
return _lazy_session()
try:
from packages.config import get_shared_settings
url = str(get_shared_settings().database_url)
except Exception: # noqa: BLE001
return None
if not url:
return None
url = url.replace("postgresql+asyncpg://", "postgresql+psycopg://")
url = url.replace("postgresql://", "postgresql+psycopg://") if url.startswith("postgresql://") else url
engine = sa.create_engine(url, pool_pre_ping=True, pool_size=2, max_overflow=2)
from sqlalchemy.orm import sessionmaker
_lazy_session = sessionmaker(bind=engine)
return _lazy_session()
def _load_from_db(prompt_type: str) -> Optional[PromptTemplate]:
session = None
try:
session = _get_session()
if session is None:
return None
sql = sa.text("""
SELECT name, prompt_type, version, system_prompt,
user_prompt_template, COALESCE(example_output, '') AS example_output,
is_active
FROM viral_video_prompt_templates
WHERE prompt_type = :pt AND is_active = TRUE
ORDER BY version DESC
LIMIT 1
""")
row = session.execute(sql, {"pt": prompt_type}).first()
if row is None:
return None
return PromptTemplate(
name=row[0],
prompt_type=row[1],
version=int(row[2]),
system_prompt=row[3],
user_prompt_template=row[4],
example_output=row[5] or "",
is_active=bool(row[6]),
)
except Exception: # noqa: BLE001 - 表不存在/DB 不可用时静默回落
return None
finally:
if session is not None:
try:
session.close()
except Exception: # noqa: BLE001
pass
def get_template(prompt_type: str, *, force_refresh: bool = False) -> Optional[PromptTemplate]:
"""取某类型当前启用模板,30 秒缓存;DB 无数据则回落到代码默认模板。"""
if prompt_type not in _VALID_TYPES:
raise ValueError(f"未知 prompt_type: {prompt_type}")
now = time.monotonic()
with _lock:
cached = _cache.get(prompt_type)
if not force_refresh and cached and now - cached[0] < CACHE_TTL_SECONDS:
return cached[1]
template = _load_from_db(prompt_type) or _fallback(prompt_type)
if template is not None:
with _lock:
_cache[prompt_type] = (now, template)
return template
def invalidate() -> None:
"""清空缓存(测试用)。"""
with _lock:
_cache.clear()
class _SafeDict(dict):
def __missing__(self, key: str) -> str:
return "{" + key + "}"
def _safe_format(text: str, kwargs: dict) -> str:
try:
return text.format_map(_SafeDict(kwargs))
except Exception: # noqa: BLE001
return text
def render_user_prompt(template: PromptTemplate, **kwargs) -> str:
"""填充 user_prompt_template 占位符,缺键原样保留不报错。"""
return _safe_format(template.user_prompt_template, kwargs)
def render_system_prompt(template: PromptTemplate, **kwargs) -> str:
"""copy_fusion 等 system_prompt 含运行时变量时填充。"""
return _safe_format(template.system_prompt, kwargs)