Files
life-echo/api/app/agents/chat/helpers.py
Kevin bb16d3a5c9 refactor(agents): 抽取阶段常量与对话上下文;快档 LLM;图片 prompt 可禁止回退
访谈与阶段
- 新增 app/agents/stage_constants.py:集中 CHAT_STAGES、章节分类/顺序、阶段到默认 memoir 类别等,与 MemoirState 默认槽位顺序对齐;减少散落在 prompts 内的重复常量。
- 新增 app/agents/chat/prompt_context.py:以 ChatPromptContext 汇总 guided 系统提示所需字段(阶段、槽位、轮次、人设、记忆证据、回复长度模式、背景声线、职业等),统一走 get_guided_conversation_prompt。
- 大幅收敛 app/agents/chat/prompts_conversation.py;调整 prompts.py、stage_prompts.py、stage_detection.py;同步 interview_agent、profile_agent、helpers 与 state_schema,使对话侧构造提示的方式一致、可测。

回忆录流水线
- memoir/prompts.py 删除已迁至 stage_constants / 独立模板的大段常量与图片占位相关逻辑;classification / extraction / fidelity / narrative agents 与 orchest(全量历史仍可用于计数,注入模型时按轮次与字符上限截断)、image_prompt_fallback_disabled。
- dependencies 增加 get_llm_provider_fast(LRU 缓存,可与默认共用密钥与 base_url)。

任务与编排
- memoir_tasks:prepare_batches 注入 llm_fast;开启独立快档模型时打结构化日志。
- chapter_cover_tasks、story_image_tasks:与图片 prompt / JSON 工具路径或策略变更对齐(import 与行为一致)。
- story_pipeline_sync 等小处同步。

其它核心
- langchain_llm、text_normalize 随上述调用链微调。

开发者体验
- .cursor/settings.json:启用 redis-development、postman 插件。

测试
- 新增 test_image_prompt_policy:覆盖「禁止回退」等图片 prompt 策略。
- 更新 test_interview_prompts、test_interview_reply_length、test_experience_regressions、test_json_and_memory_utils,匹配新常量位置、json_utils 与对话/长度行为。
2026-04-02 12:00:00 +08:00

108 lines
3.6 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.
"""聊天 Agent 共享工具:历史获取、格式化、存储"""
from dataclasses import dataclass
from datetime import datetime
from typing import Any, List
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
from app.core.redis import redis_service
def _human_ai_rows(history: list[dict]) -> list[dict]:
"""与 get_history_messages 一致:仅保留 human/ai顺序与 Redis 列表一致。"""
return [m for m in history if m.get("role") in ("human", "ai")]
def _lc_messages_from_rows(rows: list[dict]) -> list[HumanMessage | AIMessage]:
out: list[HumanMessage | AIMessage] = []
for msg in rows:
role = msg.get("role")
if role == "human":
out.append(HumanMessage(content=msg["content"]))
elif role == "ai":
out.append(AIMessage(content=msg["content"]))
return out
@dataclass(frozen=True)
class HistoryWithWindow:
"""单次 Redis 读取后的全量轮次计数 + 截断后注入 LLM 的消息列表。"""
turn_total: int
window: list[HumanMessage | AIMessage]
async def get_history_with_window(
conversation_id: str,
*,
max_pairs: int,
max_chars: int,
) -> HistoryWithWindow:
"""一次读取 Redisturn_total 由全量 human/ai 条数得到;仅对窗口切片构造 LangChain 消息。"""
history = await redis_service.get_conversation_history(conversation_id)
human_ai = _human_ai_rows(history)
turn_total = len(human_ai) // 2
window_raw = human_ai[-(max_pairs * 2) :] if max_pairs > 0 else human_ai[:]
window = _lc_messages_from_rows(window_raw)
total_chars = 0
start = len(window)
for i in range(len(window) - 1, -1, -1):
msg = window[i]
content = getattr(msg, "content", "") or ""
total_chars += len(content)
if total_chars > max_chars:
start = i + 1
break
else:
start = 0
if start < len(window) and isinstance(window[start], AIMessage):
start += 1
trimmed = window[start:]
return HistoryWithWindow(turn_total=turn_total, window=trimmed)
async def get_history_messages(conversation_id: str) -> List[Any]:
"""从 Redis 获取对话历史"""
history = await redis_service.get_conversation_history(conversation_id)
return _lc_messages_from_rows(_human_ai_rows(history))
def format_history_string(messages: List[Any]) -> str:
"""将 LangChain 消息列表格式化为调试日志用多段文本(含 System不静默跳过"""
history_parts: list[str] = []
for msg in messages:
if isinstance(msg, SystemMessage):
history_parts.append(f"System: {msg.content}")
elif isinstance(msg, HumanMessage):
history_parts.append(f"Human: {msg.content}")
elif isinstance(msg, AIMessage):
history_parts.append(f"Assistant: {msg.content}")
else:
content = getattr(msg, "content", None)
history_parts.append(f"{type(msg).__name__}: {content}")
return "\n\n".join(history_parts)
async def save_message(
conversation_id: str,
role: str,
content: str,
message_type: str = "text",
voice_session_id: str | None = None,
timestamp: datetime | str | int | None = None,
audio_duration_seconds: int | None = None,
) -> None:
"""保存消息到 Redis"""
await redis_service.add_message(
conversation_id,
role,
content,
message_type=message_type,
voice_session_id=voice_session_id,
timestamp=timestamp.isoformat()
if isinstance(timestamp, datetime)
else timestamp,
audio_duration_seconds=audio_duration_seconds,
)