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:
@@ -22,7 +22,10 @@ from app.core.config import settings
|
||||
from app.core.cos_url_keys import TTS_PRESIGNED_EXPIRES_SEC
|
||||
from app.core.db import AsyncSessionLocal
|
||||
from app.core.dependencies import get_asr_provider, get_object_storage, get_tts_provider
|
||||
from app.features.conversation.history_store import ConversationHistoryStore
|
||||
from app.features.conversation.history_store import (
|
||||
AI_RESPONSE_SEGMENT_JOIN,
|
||||
ConversationHistoryStore,
|
||||
)
|
||||
from app.features.conversation.models import Conversation, Segment
|
||||
from app.features.conversation.ws.connection_manager import manager
|
||||
from app.features.conversation.ws.message_types import MessageType
|
||||
@@ -369,6 +372,16 @@ async def process_audio_segment(
|
||||
) -> None:
|
||||
"""分段语音的异步处理:并行 ASR + 幂等落库 + 有序聚合触发 Agent。"""
|
||||
state = get_or_create_segment_state(conversation_id, voice_session_id)
|
||||
logger.info(
|
||||
"process_audio_segment 开始: conversation_id={} voice_session_id={} "
|
||||
"segment_index={} is_last={} duration_s={} audio_b64_len={}",
|
||||
conversation_id,
|
||||
voice_session_id,
|
||||
segment_index,
|
||||
is_last,
|
||||
audio_duration,
|
||||
len(audio_base64 or ""),
|
||||
)
|
||||
|
||||
try:
|
||||
async with AsyncSessionLocal() as db:
|
||||
@@ -420,6 +433,12 @@ async def process_audio_segment(
|
||||
audio_bytes = base64.b64decode(audio_base64)
|
||||
except Exception:
|
||||
audio_bytes = b""
|
||||
if not audio_bytes:
|
||||
logger.warning(
|
||||
"process_audio_segment: 解码后音频为空 conversation_id={} segment_index={}",
|
||||
conversation_id,
|
||||
segment_index,
|
||||
)
|
||||
transcript_text = await get_asr_provider().transcribe(
|
||||
audio_bytes, format="m4a"
|
||||
)
|
||||
@@ -440,12 +459,19 @@ async def process_audio_segment(
|
||||
)
|
||||
|
||||
if _is_transcribe_failure(transcript_text):
|
||||
detail = (transcript_text or "").strip()
|
||||
if detail.startswith("转写失败"):
|
||||
user_msg = f"分段 {segment_index} {detail}"
|
||||
elif not detail:
|
||||
user_msg = f"分段 {segment_index} 转写失败:未识别到内容(请检查后端 ASR 配置)"
|
||||
else:
|
||||
user_msg = f"分段 {segment_index} 转写失败:{detail[:400]}"
|
||||
await manager.send_message(
|
||||
conversation_id,
|
||||
{
|
||||
"type": MessageType.ERROR,
|
||||
"data": {
|
||||
"message": f"分段 {segment_index} 转写失败,请重试该片段",
|
||||
"message": user_msg,
|
||||
"segment_index": segment_index,
|
||||
},
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
@@ -553,6 +579,12 @@ async def process_user_message(
|
||||
store = ConversationHistoryStore(db)
|
||||
tts_urls: list[str] = []
|
||||
try:
|
||||
logger.info(
|
||||
"process_user_message 开始: conversation_id={} segment_id={} user_chars={}",
|
||||
conversation_id,
|
||||
segment.id,
|
||||
len(user_message or ""),
|
||||
)
|
||||
is_from_voice = bool(segment.audio_url)
|
||||
voice_session_id = _voice_session_id_from_audio_url(segment.audio_url)
|
||||
audio_dur = getattr(segment, "audio_duration_seconds", None)
|
||||
@@ -586,7 +618,7 @@ async def process_user_message(
|
||||
responses = turn.messages
|
||||
skip_tts = turn.skip_tts
|
||||
|
||||
segment.agent_response = "\n\n".join(responses)
|
||||
segment.agent_response = AI_RESPONSE_SEGMENT_JOIN.join(responses)
|
||||
_mark_conversation_active(conversation)
|
||||
ai_msg_id = await store.record_human_ai_turn(
|
||||
conversation_id=conversation_id,
|
||||
@@ -600,6 +632,22 @@ async def process_user_message(
|
||||
segment_id=segment.id,
|
||||
)
|
||||
if not ai_msg_id:
|
||||
logger.warning(
|
||||
"process_user_message: 无有效助手段落(responses 为空),conversation_id={} segment_id={}",
|
||||
conversation_id,
|
||||
segment.id,
|
||||
)
|
||||
if conversation_id in manager.active_connections:
|
||||
await manager.send_message(
|
||||
conversation_id,
|
||||
{
|
||||
"type": MessageType.ERROR,
|
||||
"data": {
|
||||
"message": "未生成回复,请重试或稍后再试",
|
||||
},
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
},
|
||||
)
|
||||
return
|
||||
|
||||
tts_epoch_start = _tts_epoch_value(conversation_id)
|
||||
@@ -614,6 +662,7 @@ async def process_user_message(
|
||||
"text": response_text,
|
||||
"index": i,
|
||||
"total": n,
|
||||
"assistant_message_id": ai_msg_id,
|
||||
},
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user