Files
life-echo/api/app/tasks/story_title_tasks.py
Sully 53e0065e3e refactor(api): TOML 配置 SSOT、统一错误契约、Auth/事务加固与可观测性 (#33)
配置 SSOT(TOML + .env)
统一错误契约
Auth 与事务边界
Redis / Celery 可靠性:业务 Redis(DB/0)与 Celery broker/backend(DB/1)显式拆分;连接池、sync client
可观测性(OpenTelemetry + LGTM)
2026-05-22 13:44:50 +08:00

152 lines
5.8 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.
"""Async story title refinement after new story create (placeholder first)."""
import time
from celery import shared_task
from app.core.db import get_sync_db, transactional_sync
from app.core.llm_gateway import LlmGateway, LlmUseCase
from app.core.logging import get_logger
logger = get_logger(__name__)
@shared_task(bind=True, max_retries=2, default_retry_delay=15, ignore_result=True)
def generate_story_title_after_create(
self,
story_id: str,
chapter_category: str,
oral_scope: str,
user_id: str,
):
"""Replace placeholder title with LLM title when body is long enough."""
from app.agents.chat.prompts_profile import format_user_profile_context
from app.agents.memoir.narrative_agent import NarrativeAgent
from app.features.memoir.state_service import get_or_create_state_sync
from app.features.memoir.story_pipeline_sync import (
_maybe_generate_title,
_placeholder_title,
_slot_snippets_for_narrative,
)
from app.features.story.models import Story
from app.features.user.models import User
t0 = time.perf_counter()
logger.info(
"event=story_title_task_start story_id={} user_id={} chapter_category={} "
"msg=故事标题精修任务开始",
story_id,
user_id,
chapter_category,
)
try:
with get_sync_db() as db:
st = db.get(Story, story_id)
if not st or str(st.user_id) != str(user_id):
ms = (time.perf_counter() - t0) * 1000
logger.info(
"event=story_title_task_skip story_id={} reason=not_found duration_ms={:.1f} "
"msg=标题精修跳过(故事不存在或无权限)",
story_id,
ms,
)
return {"status": "skip_not_found"}
user_obj_pre = db.get(User, user_id)
user_language = (
"en"
if user_obj_pre is not None
and str(getattr(user_obj_pre, "language_preference", "zh") or "zh").lower()
== "en"
else "zh"
)
expected_ph_zh = _placeholder_title(chapter_category, language="zh")
expected_ph_en = _placeholder_title(chapter_category, language="en")
expected_ph = _placeholder_title(chapter_category, language=user_language)
current = (st.title or "").strip()
if current and current not in (expected_ph_zh, expected_ph_en):
ms = (time.perf_counter() - t0) * 1000
logger.info(
"event=story_title_task_skip story_id={} reason=user_modified duration_ms={:.1f} "
"msg=标题精修跳过(用户已改标题)",
story_id,
ms,
)
return {"status": "skip_user_modified"}
llm = LlmGateway().langchain_llm_for(LlmUseCase("story_title"))
if not llm:
ms = (time.perf_counter() - t0) * 1000
logger.info(
"event=story_title_task_skip story_id={} reason=no_llm duration_ms={:.1f} "
"msg=标题精修跳过(无 LLM",
story_id,
ms,
)
return {"status": "skip_no_llm"}
user_obj = user_obj_pre
user_profile = ""
birth_year = None
if user_obj:
birth_year = user_obj.birth_year
user_profile = format_user_profile_context(
birth_year=user_obj.birth_year,
birth_place=user_obj.birth_place,
grew_up_place=user_obj.grew_up_place,
occupation=user_obj.occupation,
language=user_language,
)
state = get_or_create_state_sync(user_id, db)
slot_snippets = _slot_snippets_for_narrative(
state=state,
chapter_category=chapter_category,
user_id=user_id,
)
md = (st.canonical_markdown or "").strip()
new_title = _maybe_generate_title(
NarrativeAgent(),
chapter_category=chapter_category,
md=md,
slot_snippets=slot_snippets,
user_profile=user_profile,
user_birth_year=birth_year,
llm=llm,
oral_scope=oral_scope or "",
language=user_language,
)
if not new_title.strip() or new_title.strip() in (
expected_ph_zh,
expected_ph_en,
expected_ph,
):
ms = (time.perf_counter() - t0) * 1000
logger.info(
"event=story_title_task_skip story_id={} reason=placeholder duration_ms={:.1f} "
"msg=标题精修跳过(仍为占位)",
story_id,
ms,
)
return {"status": "skip_placeholder"}
with transactional_sync(db):
st.title = new_title
ms = (time.perf_counter() - t0) * 1000
logger.info(
"event=story_title_task_done story_id={} user_id={} duration_ms={:.1f} "
"msg=故事标题精修完成",
story_id,
user_id,
ms,
)
return {"status": "ok", "title": new_title}
except Exception as exc:
ms = (time.perf_counter() - t0) * 1000
logger.warning(
"event=generate_story_title_after_create_failed story_id={} duration_ms={:.1f} err={} "
"msg=故事标题精修失败",
story_id,
ms,
exc,
)
raise self.retry(exc=exc) from exc