2026-03-19 10:38:11 +08:00
|
|
|
|
"""
|
|
|
|
|
|
NarrativeAgent:生成创意标题和叙事改写。
|
2026-03-27 16:01:28 +08:00
|
|
|
|
叙事正文走 `get_narrative_json_prompt` / `get_narrative_merge_json_prompt`(传记作家式书面语 + 事实边界)。
|
2026-03-19 10:38:11 +08:00
|
|
|
|
"""
|
2026-03-19 14:36:14 +08:00
|
|
|
|
|
2026-03-19 10:38:11 +08:00
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
from typing import Any, Dict, Optional
|
|
|
|
|
|
|
2026-03-19 10:54:48 +08:00
|
|
|
|
from app.agents.memoir.prompts import (
|
2026-03-26 12:13:36 +08:00
|
|
|
|
get_creative_title_json_prompt,
|
2026-03-19 11:18:58 +08:00
|
|
|
|
get_narrative_json_prompt,
|
2026-03-26 12:13:36 +08:00
|
|
|
|
get_narrative_merge_json_prompt,
|
2026-03-19 10:38:11 +08:00
|
|
|
|
)
|
2026-04-03 13:34:27 +08:00
|
|
|
|
from app.agents.memoir.schemas import MemoirTitleOutput
|
feat(i18n): persist language preference and thread through chat, memoir, TTS
- Add users.language_preference (Alembic 0018, default zh); capture at signup/SMS
only; expose on auth and profile APIs
- Lite English prompts for chat and memoir; localized stage labels and agent
names (Life Echo / 岁月知己)
- Tencent TTS: language-aware synthesis, ModelType=1 for 501004, English chunking
- WebSocket pipeline: emit all AGENT_RESPONSE segments when TTS cancels; INFO logs
for tts_this_turn and TTS decisions; on-demand TTS logging
- Expo: device language on auth, i18n tiers/agent name, [SPLIT] streaming UX fixes
- Tests for migration, prompts, pipeline, router tts_this_turn, reply segments
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 16:16:49 +08:00
|
|
|
|
from app.agents.stage_constants import CHAPTER_CATEGORIES, chapter_category_display
|
2026-04-03 13:34:27 +08:00
|
|
|
|
from app.core.config import settings
|
2026-03-26 12:13:36 +08:00
|
|
|
|
from app.core.langchain_llm import invoke_json_object
|
2026-04-03 13:34:27 +08:00
|
|
|
|
from app.core.llm_call import llm_json_call
|
2026-03-20 15:15:35 +08:00
|
|
|
|
from app.core.logging import get_logger
|
2026-05-22 13:44:50 +08:00
|
|
|
|
from app.features.memoir.constants import memoir
|
2026-03-19 10:38:11 +08:00
|
|
|
|
|
|
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
feat(i18n): persist language preference and thread through chat, memoir, TTS
- Add users.language_preference (Alembic 0018, default zh); capture at signup/SMS
only; expose on auth and profile APIs
- Lite English prompts for chat and memoir; localized stage labels and agent
names (Life Echo / 岁月知己)
- Tencent TTS: language-aware synthesis, ModelType=1 for 501004, English chunking
- WebSocket pipeline: emit all AGENT_RESPONSE segments when TTS cancels; INFO logs
for tts_this_turn and TTS decisions; on-demand TTS logging
- Expo: device language on auth, i18n tiers/agent name, [SPLIT] streaming UX fixes
- Tests for migration, prompts, pipeline, router tts_this_turn, reply segments
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 16:16:49 +08:00
|
|
|
|
def _default_title_for(stage: str, language: str) -> str:
|
|
|
|
|
|
if language == "en":
|
|
|
|
|
|
cat = chapter_category_display(stage, language="en") or stage
|
|
|
|
|
|
return f"{cat} Memory"
|
|
|
|
|
|
return f"{CHAPTER_CATEGORIES.get(stage, stage)} 回忆"
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-19 10:38:11 +08:00
|
|
|
|
class NarrativeAgent:
|
|
|
|
|
|
"""生成章节标题和叙事正文"""
|
|
|
|
|
|
|
|
|
|
|
|
def generate_title(
|
|
|
|
|
|
self,
|
|
|
|
|
|
stage: str,
|
|
|
|
|
|
emotion: str,
|
|
|
|
|
|
slots: Dict[str, str],
|
|
|
|
|
|
user_profile: str = "",
|
|
|
|
|
|
birth_year: Optional[int] = None,
|
|
|
|
|
|
llm: Any = None,
|
feat(i18n): persist language preference and thread through chat, memoir, TTS
- Add users.language_preference (Alembic 0018, default zh); capture at signup/SMS
only; expose on auth and profile APIs
- Lite English prompts for chat and memoir; localized stage labels and agent
names (Life Echo / 岁月知己)
- Tencent TTS: language-aware synthesis, ModelType=1 for 501004, English chunking
- WebSocket pipeline: emit all AGENT_RESPONSE segments when TTS cancels; INFO logs
for tts_this_turn and TTS decisions; on-demand TTS logging
- Expo: device language on auth, i18n tiers/agent name, [SPLIT] streaming UX fixes
- Tests for migration, prompts, pipeline, router tts_this_turn, reply segments
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 16:16:49 +08:00
|
|
|
|
language: str = "zh",
|
2026-03-19 10:38:11 +08:00
|
|
|
|
) -> str:
|
|
|
|
|
|
"""生成创意标题。若无 LLM 则返回默认标题"""
|
|
|
|
|
|
if not llm:
|
feat(i18n): persist language preference and thread through chat, memoir, TTS
- Add users.language_preference (Alembic 0018, default zh); capture at signup/SMS
only; expose on auth and profile APIs
- Lite English prompts for chat and memoir; localized stage labels and agent
names (Life Echo / 岁月知己)
- Tencent TTS: language-aware synthesis, ModelType=1 for 501004, English chunking
- WebSocket pipeline: emit all AGENT_RESPONSE segments when TTS cancels; INFO logs
for tts_this_turn and TTS decisions; on-demand TTS logging
- Expo: device language on auth, i18n tiers/agent name, [SPLIT] streaming UX fixes
- Tests for migration, prompts, pipeline, router tts_this_turn, reply segments
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 16:16:49 +08:00
|
|
|
|
return _default_title_for(stage, language)
|
2026-03-19 10:38:11 +08:00
|
|
|
|
try:
|
2026-03-26 12:13:36 +08:00
|
|
|
|
prompt = get_creative_title_json_prompt(
|
2026-03-19 10:38:11 +08:00
|
|
|
|
stage=stage,
|
|
|
|
|
|
emotion=emotion,
|
|
|
|
|
|
slots=slots,
|
|
|
|
|
|
user_profile=user_profile,
|
|
|
|
|
|
birth_year=birth_year,
|
feat(i18n): persist language preference and thread through chat, memoir, TTS
- Add users.language_preference (Alembic 0018, default zh); capture at signup/SMS
only; expose on auth and profile APIs
- Lite English prompts for chat and memoir; localized stage labels and agent
names (Life Echo / 岁月知己)
- Tencent TTS: language-aware synthesis, ModelType=1 for 501004, English chunking
- WebSocket pipeline: emit all AGENT_RESPONSE segments when TTS cancels; INFO logs
for tts_this_turn and TTS decisions; on-demand TTS logging
- Expo: device language on auth, i18n tiers/agent name, [SPLIT] streaming UX fixes
- Tests for migration, prompts, pipeline, router tts_this_turn, reply segments
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 16:16:49 +08:00
|
|
|
|
language=language,
|
2026-03-19 10:38:11 +08:00
|
|
|
|
)
|
feat(i18n): persist language preference and thread through chat, memoir, TTS
- Add users.language_preference (Alembic 0018, default zh); capture at signup/SMS
only; expose on auth and profile APIs
- Lite English prompts for chat and memoir; localized stage labels and agent
names (Life Echo / 岁月知己)
- Tencent TTS: language-aware synthesis, ModelType=1 for 501004, English chunking
- WebSocket pipeline: emit all AGENT_RESPONSE segments when TTS cancels; INFO logs
for tts_this_turn and TTS decisions; on-demand TTS logging
- Expo: device language on auth, i18n tiers/agent name, [SPLIT] streaming UX fixes
- Tests for migration, prompts, pipeline, router tts_this_turn, reply segments
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 16:16:49 +08:00
|
|
|
|
default_title = _default_title_for(stage, language)
|
2026-04-03 13:34:27 +08:00
|
|
|
|
|
|
|
|
|
|
def _title_fallback() -> MemoirTitleOutput:
|
|
|
|
|
|
return MemoirTitleOutput(title=default_title)
|
|
|
|
|
|
|
|
|
|
|
|
out = llm_json_call(
|
2026-03-26 12:13:36 +08:00
|
|
|
|
llm,
|
|
|
|
|
|
prompt,
|
2026-04-03 13:34:27 +08:00
|
|
|
|
MemoirTitleOutput,
|
2026-05-22 13:44:50 +08:00
|
|
|
|
max_tokens=memoir.title_max_tokens,
|
2026-03-26 12:13:36 +08:00
|
|
|
|
agent="NarrativeAgent.generate_title",
|
2026-04-03 13:34:27 +08:00
|
|
|
|
fallback_factory=_title_fallback,
|
2026-03-26 12:13:36 +08:00
|
|
|
|
)
|
2026-04-03 13:34:27 +08:00
|
|
|
|
title = (out.title or "").strip()
|
2026-03-26 12:13:36 +08:00
|
|
|
|
if title:
|
|
|
|
|
|
return title.strip('"')
|
2026-04-03 13:34:27 +08:00
|
|
|
|
return default_title
|
2026-03-19 10:38:11 +08:00
|
|
|
|
except Exception as e:
|
2026-03-26 12:13:36 +08:00
|
|
|
|
logger.warning("NarrativeAgent 生成标题失败: {}", e)
|
feat(i18n): persist language preference and thread through chat, memoir, TTS
- Add users.language_preference (Alembic 0018, default zh); capture at signup/SMS
only; expose on auth and profile APIs
- Lite English prompts for chat and memoir; localized stage labels and agent
names (Life Echo / 岁月知己)
- Tencent TTS: language-aware synthesis, ModelType=1 for 501004, English chunking
- WebSocket pipeline: emit all AGENT_RESPONSE segments when TTS cancels; INFO logs
for tts_this_turn and TTS decisions; on-demand TTS logging
- Expo: device language on auth, i18n tiers/agent name, [SPLIT] streaming UX fixes
- Tests for migration, prompts, pipeline, router tts_this_turn, reply segments
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 16:16:49 +08:00
|
|
|
|
return _default_title_for(stage, language)
|
2026-03-19 10:38:11 +08:00
|
|
|
|
|
|
|
|
|
|
def generate_narrative(
|
|
|
|
|
|
self,
|
|
|
|
|
|
stage: str,
|
|
|
|
|
|
slots: Dict[str, str],
|
|
|
|
|
|
new_content: str,
|
|
|
|
|
|
existing_content: str = "",
|
|
|
|
|
|
user_profile: str = "",
|
|
|
|
|
|
birth_year: Optional[int] = None,
|
|
|
|
|
|
llm: Any = None,
|
2026-03-31 23:55:26 +08:00
|
|
|
|
background_voice: str = "default",
|
2026-04-01 11:49:33 +08:00
|
|
|
|
occupation: str = "",
|
2026-04-03 10:12:59 +08:00
|
|
|
|
*,
|
|
|
|
|
|
fallback_plain_oral: str = "",
|
feat(i18n): persist language preference and thread through chat, memoir, TTS
- Add users.language_preference (Alembic 0018, default zh); capture at signup/SMS
only; expose on auth and profile APIs
- Lite English prompts for chat and memoir; localized stage labels and agent
names (Life Echo / 岁月知己)
- Tencent TTS: language-aware synthesis, ModelType=1 for 501004, English chunking
- WebSocket pipeline: emit all AGENT_RESPONSE segments when TTS cancels; INFO logs
for tts_this_turn and TTS decisions; on-demand TTS logging
- Expo: device language on auth, i18n tiers/agent name, [SPLIT] streaming UX fixes
- Tests for migration, prompts, pipeline, router tts_this_turn, reply segments
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 16:16:49 +08:00
|
|
|
|
language: str = "zh",
|
2026-03-19 10:38:11 +08:00
|
|
|
|
) -> str:
|
2026-03-26 12:13:36 +08:00
|
|
|
|
"""将新对话改写为叙述。若无 LLM 则直接拼接。
|
|
|
|
|
|
|
|
|
|
|
|
若 `existing_content` 非空(append 路径),使用整篇合并提示,输出覆盖全篇的有序段落。
|
2026-04-03 10:12:59 +08:00
|
|
|
|
|
|
|
|
|
|
`fallback_plain_oral`:仅含本段口述(勿传含 evidence 的组装串)。LLM 异常时只回退到
|
|
|
|
|
|
口述/旧正文拼接,避免把「本段用户口述+摘录」整包写入 story。
|
2026-03-26 12:13:36 +08:00
|
|
|
|
"""
|
2026-04-03 10:12:59 +08:00
|
|
|
|
oral_fb = (fallback_plain_oral or "").strip()
|
2026-03-19 10:38:11 +08:00
|
|
|
|
if not llm:
|
|
|
|
|
|
if existing_content:
|
2026-04-03 10:12:59 +08:00
|
|
|
|
if oral_fb:
|
|
|
|
|
|
return f"{existing_content}\n\n{oral_fb}"
|
2026-03-19 10:38:11 +08:00
|
|
|
|
return f"{existing_content}\n\n{new_content}"
|
2026-04-03 10:12:59 +08:00
|
|
|
|
return oral_fb or new_content
|
2026-03-19 10:38:11 +08:00
|
|
|
|
try:
|
2026-03-26 12:13:36 +08:00
|
|
|
|
merge_mode = bool((existing_content or "").strip())
|
|
|
|
|
|
if merge_mode:
|
|
|
|
|
|
prompt = get_narrative_merge_json_prompt(
|
|
|
|
|
|
stage=stage,
|
|
|
|
|
|
slots=slots,
|
|
|
|
|
|
new_content=new_content,
|
|
|
|
|
|
existing_content=existing_content,
|
|
|
|
|
|
user_profile=user_profile,
|
|
|
|
|
|
birth_year=birth_year,
|
2026-03-31 23:55:26 +08:00
|
|
|
|
background_voice=background_voice,
|
2026-04-01 11:49:33 +08:00
|
|
|
|
occupation=occupation,
|
feat(i18n): persist language preference and thread through chat, memoir, TTS
- Add users.language_preference (Alembic 0018, default zh); capture at signup/SMS
only; expose on auth and profile APIs
- Lite English prompts for chat and memoir; localized stage labels and agent
names (Life Echo / 岁月知己)
- Tencent TTS: language-aware synthesis, ModelType=1 for 501004, English chunking
- WebSocket pipeline: emit all AGENT_RESPONSE segments when TTS cancels; INFO logs
for tts_this_turn and TTS decisions; on-demand TTS logging
- Expo: device language on auth, i18n tiers/agent name, [SPLIT] streaming UX fixes
- Tests for migration, prompts, pipeline, router tts_this_turn, reply segments
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 16:16:49 +08:00
|
|
|
|
language=language,
|
2026-03-26 12:13:36 +08:00
|
|
|
|
)
|
2026-05-22 13:44:50 +08:00
|
|
|
|
max_tokens = int(memoir.narrative_merge_max_tokens)
|
2026-03-26 12:13:36 +08:00
|
|
|
|
agent_name = "NarrativeAgent.generate_narrative_merge"
|
|
|
|
|
|
else:
|
|
|
|
|
|
prompt = get_narrative_json_prompt(
|
|
|
|
|
|
stage=stage,
|
|
|
|
|
|
slots=slots,
|
|
|
|
|
|
new_content=new_content,
|
|
|
|
|
|
existing_content=existing_content,
|
|
|
|
|
|
user_profile=user_profile,
|
|
|
|
|
|
birth_year=birth_year,
|
2026-03-31 23:55:26 +08:00
|
|
|
|
background_voice=background_voice,
|
2026-04-01 11:49:33 +08:00
|
|
|
|
occupation=occupation,
|
feat(i18n): persist language preference and thread through chat, memoir, TTS
- Add users.language_preference (Alembic 0018, default zh); capture at signup/SMS
only; expose on auth and profile APIs
- Lite English prompts for chat and memoir; localized stage labels and agent
names (Life Echo / 岁月知己)
- Tencent TTS: language-aware synthesis, ModelType=1 for 501004, English chunking
- WebSocket pipeline: emit all AGENT_RESPONSE segments when TTS cancels; INFO logs
for tts_this_turn and TTS decisions; on-demand TTS logging
- Expo: device language on auth, i18n tiers/agent name, [SPLIT] streaming UX fixes
- Tests for migration, prompts, pipeline, router tts_this_turn, reply segments
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 16:16:49 +08:00
|
|
|
|
language=language,
|
2026-03-26 12:13:36 +08:00
|
|
|
|
)
|
2026-05-22 13:44:50 +08:00
|
|
|
|
max_tokens = int(memoir.narrative_max_tokens)
|
2026-03-26 12:13:36 +08:00
|
|
|
|
agent_name = "NarrativeAgent.generate_narrative"
|
|
|
|
|
|
return invoke_json_object(
|
|
|
|
|
|
llm,
|
|
|
|
|
|
prompt,
|
|
|
|
|
|
max_tokens=max_tokens,
|
|
|
|
|
|
agent=agent_name,
|
|
|
|
|
|
).strip()
|
2026-03-19 10:38:11 +08:00
|
|
|
|
except Exception as e:
|
2026-03-26 12:13:36 +08:00
|
|
|
|
logger.warning("NarrativeAgent 生成叙事失败: {}", e)
|
2026-04-03 10:12:59 +08:00
|
|
|
|
ex = (existing_content or "").strip()
|
|
|
|
|
|
if ex and oral_fb:
|
|
|
|
|
|
return f"{existing_content}\n\n{oral_fb}"
|
|
|
|
|
|
if oral_fb:
|
|
|
|
|
|
return oral_fb
|
|
|
|
|
|
if ex:
|
|
|
|
|
|
return str(existing_content)
|
|
|
|
|
|
return ""
|