"""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 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) 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"} st.title = new_title db.commit() 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