2026-03-20 15:15:35 +08:00
|
|
|
|
"""
|
|
|
|
|
|
会话 transcript 与 Redis 历史条目的纯映射(无 I/O)。
|
|
|
|
|
|
|
|
|
|
|
|
仅由 ConversationService 使用:对齐 ChatOrchestrator 经 save_message 写入 Redis 的字段形状,
|
|
|
|
|
|
不属于 Agent 层 —— 多 Agent 模块只消费已就绪的 history,不负责从 DB 重建。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
from datetime import timezone
|
|
|
|
|
|
from typing import Any, Dict, List
|
|
|
|
|
|
|
|
|
|
|
|
from app.features.conversation.models import Segment
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _voice_session_id_from_audio_url(audio_url: str | None) -> str | None:
|
|
|
|
|
|
if not audio_url:
|
|
|
|
|
|
return None
|
|
|
|
|
|
prefix = "audio-segment:"
|
|
|
|
|
|
if not audio_url.startswith(prefix):
|
|
|
|
|
|
return None
|
|
|
|
|
|
payload = audio_url[len(prefix) :]
|
|
|
|
|
|
voice_session_id_raw, sep, _ = payload.rpartition(":")
|
|
|
|
|
|
if sep and voice_session_id_raw:
|
|
|
|
|
|
return voice_session_id_raw
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _segment_timestamp_iso(seg: Segment) -> str | None:
|
|
|
|
|
|
if not seg.created_at:
|
|
|
|
|
|
return None
|
|
|
|
|
|
dt = seg.created_at
|
|
|
|
|
|
if dt.tzinfo is None:
|
|
|
|
|
|
dt = dt.replace(tzinfo=timezone.utc)
|
|
|
|
|
|
return dt.isoformat()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def segments_to_redis_history(segments: List[Segment]) -> List[Dict[str, Any]]:
|
|
|
|
|
|
"""Segment 行 → Redis conversation history 项(与 ChatOrchestrator 写入格式一致)。"""
|
|
|
|
|
|
history: List[Dict[str, Any]] = []
|
|
|
|
|
|
for seg in segments:
|
|
|
|
|
|
ts = _segment_timestamp_iso(seg)
|
|
|
|
|
|
is_voice = bool(seg.audio_url)
|
|
|
|
|
|
human: Dict[str, Any] = {
|
|
|
|
|
|
"role": "human",
|
|
|
|
|
|
"content": seg.transcript_text or "",
|
|
|
|
|
|
"messageType": "audio" if is_voice else "text",
|
|
|
|
|
|
"timestamp": ts,
|
|
|
|
|
|
}
|
|
|
|
|
|
vsid = _voice_session_id_from_audio_url(seg.audio_url)
|
|
|
|
|
|
if vsid:
|
|
|
|
|
|
human["voiceSessionId"] = vsid
|
2026-03-20 16:36:42 +08:00
|
|
|
|
ads = getattr(seg, "audio_duration_seconds", None)
|
|
|
|
|
|
if ads is not None and ads > 0:
|
|
|
|
|
|
human["durationSeconds"] = int(ads)
|
2026-03-20 15:15:35 +08:00
|
|
|
|
history.append(human)
|
|
|
|
|
|
if seg.agent_response and seg.agent_response.strip():
|
2026-03-20 16:36:42 +08:00
|
|
|
|
ai_item: Dict[str, Any] = {
|
|
|
|
|
|
"role": "ai",
|
|
|
|
|
|
"content": seg.agent_response.strip(),
|
|
|
|
|
|
"messageType": "text",
|
|
|
|
|
|
"timestamp": ts,
|
|
|
|
|
|
}
|
|
|
|
|
|
tts = getattr(seg, "tts_audio_urls", None)
|
|
|
|
|
|
if isinstance(tts, list) and tts:
|
|
|
|
|
|
ai_item["ttsAudioUrls"] = [u for u in tts if isinstance(u, str)]
|
|
|
|
|
|
history.append(ai_item)
|
2026-03-20 15:15:35 +08:00
|
|
|
|
return history
|