Add conversation history management and API endpoints
- Introduce new database models for conversation sessions, messages, and artifacts to support conversation history tracking. - Implement API routes for listing conversations and retrieving detailed conversation data, enhancing user interaction with historical records. - Add a conversation recorder service to persist conversation messages in real-time without disrupting ongoing calls. - Update the frontend to display conversation history, including filtering and sorting options, improving user experience. - Enhance the pipeline to integrate conversation history recording seamlessly during interactions.
This commit is contained in:
137
backend/services/conversation_history.py
Normal file
137
backend/services/conversation_history.py
Normal file
@@ -0,0 +1,137 @@
|
||||
"""对话历史持久化。
|
||||
|
||||
只依赖管线已经发给客户端的最终文本事件,不侵入 Pipecat。媒体历史以后写入
|
||||
conversation_artifacts,并把对象存储地址关联到会话或消息。
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from datetime import UTC, datetime
|
||||
from uuid import uuid4
|
||||
|
||||
from db.models import ConversationMessage, ConversationSession
|
||||
from db.session import SessionLocal
|
||||
from loguru import logger
|
||||
|
||||
|
||||
def _parse_timestamp(value: object) -> datetime:
|
||||
if not isinstance(value, str) or not value:
|
||||
return datetime.now(UTC)
|
||||
try:
|
||||
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||
return parsed if parsed.tzinfo else parsed.replace(tzinfo=UTC)
|
||||
except ValueError:
|
||||
return datetime.now(UTC)
|
||||
|
||||
|
||||
class ConversationRecorder:
|
||||
"""按事件顺序写入一通会话;写库失败不应中断实时通话。"""
|
||||
|
||||
def __init__(self, session_id: str):
|
||||
self.session_id = session_id
|
||||
self._sequence = 0
|
||||
self._lock = asyncio.Lock()
|
||||
self._seen_events: set[str] = set()
|
||||
|
||||
@classmethod
|
||||
async def start(
|
||||
cls,
|
||||
*,
|
||||
assistant_id: str | None,
|
||||
assistant_name: str,
|
||||
channel: str,
|
||||
runtime_mode: str,
|
||||
) -> "ConversationRecorder | None":
|
||||
session_id = f"conv_{uuid4().hex[:20]}"
|
||||
try:
|
||||
async with SessionLocal() as db:
|
||||
db.add(
|
||||
ConversationSession(
|
||||
id=session_id,
|
||||
assistant_id=assistant_id,
|
||||
assistant_name=assistant_name,
|
||||
channel=channel,
|
||||
runtime_mode=runtime_mode,
|
||||
status="active",
|
||||
message_count=0,
|
||||
extra={},
|
||||
)
|
||||
)
|
||||
await db.commit()
|
||||
return cls(session_id)
|
||||
except Exception as exc:
|
||||
logger.error(f"创建对话历史会话失败,不影响本次通话: {exc}")
|
||||
return None
|
||||
|
||||
async def record_transport_message(self, message: object) -> None:
|
||||
if not isinstance(message, dict):
|
||||
return
|
||||
event_type = message.get("type")
|
||||
role = ""
|
||||
content = ""
|
||||
extra: dict = {}
|
||||
timestamp = message.get("timestamp")
|
||||
event_key = ""
|
||||
|
||||
if event_type == "transcript":
|
||||
role = str(message.get("role") or "")
|
||||
content = str(message.get("content") or "").strip()
|
||||
event_key = f"transcript:{role}:{timestamp}:{content}"
|
||||
elif event_type == "assistant-text-end":
|
||||
role = "assistant"
|
||||
content = str(message.get("content") or "").strip()
|
||||
turn_id = str(message.get("turn_id") or "")
|
||||
event_key = f"assistant:{turn_id}"
|
||||
extra = {
|
||||
"turn_id": turn_id,
|
||||
"interrupted": bool(message.get("interrupted", False)),
|
||||
}
|
||||
else:
|
||||
return
|
||||
|
||||
if role not in {"user", "assistant"} or not content or event_key in self._seen_events:
|
||||
return
|
||||
self._seen_events.add(event_key)
|
||||
await self._append(role, content, timestamp, extra)
|
||||
|
||||
async def _append(
|
||||
self,
|
||||
role: str,
|
||||
content: str,
|
||||
timestamp: object,
|
||||
extra: dict,
|
||||
) -> None:
|
||||
async with self._lock:
|
||||
next_sequence = self._sequence + 1
|
||||
try:
|
||||
async with SessionLocal() as db:
|
||||
db.add(
|
||||
ConversationMessage(
|
||||
id=f"msg_{uuid4().hex[:20]}",
|
||||
session_id=self.session_id,
|
||||
sequence=next_sequence,
|
||||
role=role,
|
||||
content_type="text",
|
||||
content=content,
|
||||
occurred_at=_parse_timestamp(timestamp),
|
||||
extra=extra,
|
||||
)
|
||||
)
|
||||
conversation = await db.get(ConversationSession, self.session_id)
|
||||
if conversation:
|
||||
conversation.message_count = next_sequence
|
||||
await db.commit()
|
||||
self._sequence = next_sequence
|
||||
except Exception as exc:
|
||||
logger.error(f"保存对话文本失败,不影响本次通话: {exc}")
|
||||
|
||||
async def finish(self, *, status: str = "completed") -> None:
|
||||
try:
|
||||
async with SessionLocal() as db:
|
||||
conversation = await db.get(ConversationSession, self.session_id)
|
||||
if conversation:
|
||||
conversation.status = status
|
||||
conversation.ended_at = datetime.now(UTC)
|
||||
conversation.message_count = self._sequence
|
||||
await db.commit()
|
||||
except Exception as exc:
|
||||
logger.error(f"结束对话历史会话失败: {exc}")
|
||||
Reference in New Issue
Block a user