feat(memory,conversation): 记忆富化/证据包、时间线幂等字段与对话分段全链路
数据库 - 新增迁移 0003:timeline_events.memory_source_id 外键 → memory_sources,便于按 ingest 源做时间线幂等 后端 - 记忆 - 新增 ingest 后 LLM 富化(摘要/事实/时间线),可配置开关与最大字符数 - 新增证据包组装:合并 chunk、摘要、事实、时间线、故事等检索结果;支持空 query 时是否仍带 rolling 等开关 - repo/retriever/service/router/schemas/summarizer/timeline/extractor 等扩展;文档 memory-retrieval.md 更新 后端 - 对话 WS - 增加 PING/PONG;分段 ASR 日志与空音频处理;转写失败与「无助手回复」错误提示更明确 - 助手多段回复持久化使用统一分隔符,与分段逻辑一致 后端 - Agent - reply_limits:按 [SPLIT] 与段落拆段,并保证非空 fallback,供 WS 与 TTS 多段下发 后端 - 回忆录任务 - transcript ingest 记录 source_id;任务成功结?
This commit is contained in:
@@ -1,16 +1,16 @@
|
||||
"""
|
||||
ClassificationAgent:将内容分类到 8 个章节类别,或判定无价值返回 None。
|
||||
对应现有逻辑:_classify_chapter_category
|
||||
ClassificationAgent:将内容分类到 8 个章节类别之一。
|
||||
|
||||
返回 None 表示本段不进入回忆录 Story/章节流水线;与 StoryRoute 中「可独立讲述的一段人生经历」
|
||||
(见 prompts.get_story_route_prompt)在标准上对齐:零散档案点不进 Story,记忆与 slot 抽取仍由上游完成。
|
||||
原「LLM 返回 none / 零散档案启发式」不再跳过 Story:统一映射为 ``summary`` 章节,
|
||||
仍走叙事流水线落库;与 StoryRoute 仍兼容(批次内 new/append 规划不变)。
|
||||
Memory ingest 由 Celery 任务在批次级先行完成,与分类结果独立。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from typing import Any, Optional
|
||||
from typing import Any
|
||||
|
||||
from app.agents.memoir.prompts import (
|
||||
CHAPTER_CATEGORIES,
|
||||
@@ -22,6 +22,9 @@ from app.features.memoir.memoir_images.json_payload import extract_json_payload
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# 模型判定 none 或启发式命中零散档案时,仍写入回忆录正文所用的兜底章节
|
||||
_SUMMARY_FALLBACK_CATEGORY = "summary"
|
||||
|
||||
# 与「仅档案句式」组合使用;过短但明显为叙事句的仍交 LLM 判断
|
||||
_FRAGMENT_SHORT_MAX_LEN = 48
|
||||
|
||||
@@ -67,8 +70,8 @@ def _detect_stage(text: str, fallback_stage: str) -> str:
|
||||
|
||||
def _looks_like_fragment_only(text: str) -> bool:
|
||||
"""
|
||||
保守启发式:明显为档案点/标签句,不足以作为 Story 叙事单元。
|
||||
与 get_chapter_classification_prompt 中「应返回 none」的情形一致;误判风险通过窄正则控制。
|
||||
保守启发式:明显为档案点/标签句。
|
||||
命中时仍进回忆录正文,章节映射为 ``summary``(与 LLM 返回 none 一致)。
|
||||
"""
|
||||
s = (text or "").strip()
|
||||
if not s:
|
||||
@@ -107,26 +110,30 @@ def _parse_category_from_llm_response(raw: str) -> str:
|
||||
|
||||
|
||||
class ClassificationAgent:
|
||||
"""将内容分类到 8 个章节类别之一,或判定无价值返回 None"""
|
||||
"""将内容分类到 8 个章节类别之一;none/零散档案映射为 ``summary`` 仍进 Story。"""
|
||||
|
||||
def classify(
|
||||
self,
|
||||
text: str,
|
||||
fallback_stage: str,
|
||||
llm: Any,
|
||||
) -> Optional[str]:
|
||||
*,
|
||||
segment_id: str | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
分类到 8 个章节类别之一。
|
||||
若 LLM 判定内容不足以独立成篇(none)或启发式判定为零散档案点,返回 None。
|
||||
LLM 返回 none 或启发式为零散档案时,返回 ``summary``(仍走回忆录流水线)。
|
||||
llm 需支持 .invoke(prompt) 同步调用。
|
||||
"""
|
||||
if _looks_like_fragment_only(text):
|
||||
logger.debug(
|
||||
"零散档案/极短标签句,跳过回忆录 Story: text_len={} text={}",
|
||||
logger.info(
|
||||
"event=chapter_classification_summary_fallback reason=fragment_heuristic "
|
||||
"segment_id={} text_len={} category={}",
|
||||
segment_id or "",
|
||||
len(text or ""),
|
||||
text or "",
|
||||
_SUMMARY_FALLBACK_CATEGORY,
|
||||
)
|
||||
return None
|
||||
return _SUMMARY_FALLBACK_CATEGORY
|
||||
|
||||
if llm:
|
||||
try:
|
||||
@@ -139,12 +146,14 @@ class ClassificationAgent:
|
||||
)
|
||||
category = _parse_category_from_llm_response(raw)
|
||||
if category == "none":
|
||||
logger.debug(
|
||||
"LLM 判定内容不足以成篇,跳过: text_len={} text={}",
|
||||
logger.info(
|
||||
"event=chapter_classification_summary_fallback reason=llm_none "
|
||||
"segment_id={} text_len={} category={}",
|
||||
segment_id or "",
|
||||
len(text or ""),
|
||||
text or "",
|
||||
_SUMMARY_FALLBACK_CATEGORY,
|
||||
)
|
||||
return None
|
||||
return _SUMMARY_FALLBACK_CATEGORY
|
||||
if category in CHAPTER_CATEGORIES:
|
||||
return category
|
||||
except Exception as e:
|
||||
|
||||
Reference in New Issue
Block a user