Files
life-echo/api/app/features/conversation/input_normalize.py
Kevin 69a673e6c6 feat(api): 访谈人格/回复长度策略、口述归一、背景语气与输入净稿全链路
Chat 访谈
- 新增 persona 系统(default / warm_listener / curious_guide)与 background_voice 语气层
- 回复长度由 compute_reply_plan 统一决策(brief / standard / expanded),融合信息密度启发式
- 输入净稿(input_normalize):编排层可选 rules/llm 归一用户口语后再喂模型与记忆检索
- 记忆证据注入:按用户话检索 memory evidence 并注入 prompt

Memoir 回忆录
- 口述归一(oral_normalize):segment 原文保留,story 管线取派生净稿作叙事输入
- segment 入队批次门闸:累计字数 + 最长等待秒数,减少零碎提交
- fidelity_check / prompts / narrative_agent 微调
- Alembic 0005:清理跨章节 story 外键

Infra
- Dockerfile 加入 ffmpeg
- pyproject.toml 新增依赖并同步 uv.lock
- .env.example / .env.production 补全新配置项

Tests
- 新增 test_background_voice、test_chat_input_normalize、test_experience_regressions
- 扩展 test_interview_prompts、test_interview_reply_length、test_story_route_oral_invariant

Made-with: Cursor
2026-03-31 23:55:26 +08:00

99 lines
3.3 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 / 编排层对 ASR 与键盘输入做可控预处理(规则 / 可选 LLM
不改变 segment 落库原文;仅作为模型侧派生净稿。
与 memoir 共用同一套确定性规则,避免聊天与回忆录对同一句理解割裂。
"""
from __future__ import annotations
import json
import re
from typing import Any
from app.core.config import settings
from app.core.langchain_llm import invoke_json_object
from app.core.logging import get_logger
from app.features.memoir.memoir_images.json_payload import extract_json_payload
logger = get_logger(__name__)
# 口语/ASR 常见同音:「没」误为「美」且与「看上」搭配(避免误伤「美容」「选美」等)
_MEI_KANSHANG_RE = re.compile(r"美(?=看上[我你他她它])")
def apply_conversation_input_rules(text: str) -> str:
"""确定性规则;保守替换,仅覆盖高频误听误打模式。与 memoir 共用。"""
s = text or ""
if not s:
return s
return _MEI_KANSHANG_RE.sub("", s)
def _llm_normalize_chat_input(text: str, llm: Any) -> str | None:
"""仅修正明显错字与同音字,不增事实;失败返回 None。"""
if not llm or not (text or "").strip():
return None
max_in = int(settings.chat_input_normalize_llm_max_input_chars)
t = (text or "").strip()
if len(t) > max_in:
logger.debug(
"event=chat_input_normalize_llm_skip reason=input_too_long len={} max={}",
len(t),
max_in,
)
return None
prompt = f"""你是口述转写纠错助手。只修正明显的同音错别字、别字与标点,使句子通顺可读。
禁止增加事实、不补充细节、不摘要、不改写句式风格;不得新增人名、地名、数字、事件。
若原文已通顺或无法确定错误,则照抄输入。
【用户口述】
{t}
**JSON 输出**:只输出一个合法 JSON 对象。
{{"normalized_text": "纠错后的完整文本(与输入等意,仅修错字与标点)"}}
只输出 JSON不要其它文字。"""
try:
raw = invoke_json_object(
llm,
prompt,
max_tokens=int(settings.chat_input_normalize_llm_max_tokens),
agent="chat_input_normalize.llm",
)
data = json.loads(extract_json_payload(raw))
if not isinstance(data, dict):
return None
out = (data.get("normalized_text") or "").strip()
if not out:
return None
return out
except Exception as e:
logger.warning("chat_input_normalize LLM 失败,回退规则结果: {}", e)
return None
def normalize_chat_input_for_agent(text: str, *, llm: Any | None = None) -> str:
"""
聊天侧单一出口:编排层与 InterviewAgent 共用。
- 全局关闭:原文
- off原文
- rules仅规则
- llm先规则可选LLM无 llm 或失败则保留规则结果
"""
if not settings.chat_input_normalize_enabled:
return text or ""
mode = (settings.chat_input_normalize_mode or "rules").strip().lower()
if mode == "off":
return text or ""
base = apply_conversation_input_rules(text or "")
if mode != "llm":
return base
refined = _llm_normalize_chat_input(base, llm)
if refined is not None:
return refined
return base