Files
life-echo/api/app/agents/memoir/narrative_agent.py

141 lines
5.0 KiB
Python
Raw Normal View History

2026-03-19 10:38:11 +08:00
"""
NarrativeAgent生成创意标题和叙事改写
叙事正文走 `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
from app.agents.stage_constants import CHAPTER_CATEGORIES
from app.agents.memoir.prompts import (
get_creative_title_json_prompt,
get_narrative_json_prompt,
get_narrative_merge_json_prompt,
2026-03-19 10:38:11 +08:00
)
from app.agents.memoir.schemas import MemoirTitleOutput
from app.core.config import settings
from app.core.langchain_llm import invoke_json_object
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-03-19 10:38:11 +08:00
logger = get_logger(__name__)
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,
) -> str:
"""生成创意标题。若无 LLM 则返回默认标题"""
if not llm:
return f"{CHAPTER_CATEGORIES.get(stage, stage)} 回忆"
2026-03-19 10:38:11 +08:00
try:
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,
)
default_title = f"{CHAPTER_CATEGORIES.get(stage, stage)} 回忆"
def _title_fallback() -> MemoirTitleOutput:
return MemoirTitleOutput(title=default_title)
out = llm_json_call(
llm,
prompt,
MemoirTitleOutput,
max_tokens=settings.memoir_title_max_tokens,
agent="NarrativeAgent.generate_title",
fallback_factory=_title_fallback,
)
title = (out.title or "").strip()
if title:
return title.strip('"')
return default_title
2026-03-19 10:38:11 +08:00
except Exception as e:
logger.warning("NarrativeAgent 生成标题失败: {}", e)
return f"{CHAPTER_CATEGORIES.get(stage, stage)} 回忆"
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,
background_voice: str = "default",
occupation: str = "",
*,
fallback_plain_oral: str = "",
2026-03-19 10:38:11 +08:00
) -> str:
"""将新对话改写为叙述。若无 LLM 则直接拼接。
`existing_content` 非空append 路径使用整篇合并提示输出覆盖全篇的有序段落
`fallback_plain_oral`仅含本段口述勿传含 evidence 的组装串LLM 异常时只回退到
口述/旧正文拼接避免把本段用户口述+摘录整包写入 story
"""
oral_fb = (fallback_plain_oral or "").strip()
2026-03-19 10:38:11 +08:00
if not llm:
if existing_content:
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}"
return oral_fb or new_content
2026-03-19 10:38:11 +08:00
try:
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,
background_voice=background_voice,
occupation=occupation,
)
max_tokens = int(settings.memoir_narrative_merge_max_tokens)
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,
background_voice=background_voice,
occupation=occupation,
)
max_tokens = int(settings.memoir_narrative_max_tokens)
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:
logger.warning("NarrativeAgent 生成叙事失败: {}", e)
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 ""