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:
117
backend/routes/conversations.py
Normal file
117
backend/routes/conversations.py
Normal file
@@ -0,0 +1,117 @@
|
||||
"""对话历史查询 API。"""
|
||||
|
||||
from db.models import ConversationMessage, ConversationSession
|
||||
from db.session import get_session
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from schemas import (
|
||||
ConversationDetailOut,
|
||||
ConversationListOut,
|
||||
ConversationMessageOut,
|
||||
ConversationOut,
|
||||
)
|
||||
from services.auth import require_admin
|
||||
from sqlalchemy import func, or_, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/api/conversations",
|
||||
tags=["conversations"],
|
||||
dependencies=[Depends(require_admin)],
|
||||
)
|
||||
|
||||
|
||||
def _session_out(row: ConversationSession) -> ConversationOut:
|
||||
return ConversationOut(
|
||||
id=row.id,
|
||||
assistant_id=row.assistant_id,
|
||||
assistant_name=row.assistant_name,
|
||||
channel=row.channel,
|
||||
runtime_mode=row.runtime_mode,
|
||||
status=row.status,
|
||||
message_count=row.message_count,
|
||||
started_at=row.started_at,
|
||||
ended_at=row.ended_at,
|
||||
)
|
||||
|
||||
|
||||
@router.get("", response_model=ConversationListOut)
|
||||
async def list_conversations(
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
assistant_id: str | None = None,
|
||||
search: str | None = Query(None, max_length=128),
|
||||
channel: str | None = None,
|
||||
status: str | None = None,
|
||||
sort_order: str = Query("newest", pattern="^(newest|oldest)$"),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
filters = []
|
||||
if assistant_id:
|
||||
filters.append(ConversationSession.assistant_id == assistant_id)
|
||||
if search and search.strip():
|
||||
keyword = f"%{search.strip()}%"
|
||||
filters.append(
|
||||
or_(
|
||||
ConversationSession.assistant_name.ilike(keyword),
|
||||
ConversationSession.id.ilike(keyword),
|
||||
)
|
||||
)
|
||||
if channel:
|
||||
filters.append(ConversationSession.channel == channel)
|
||||
if status:
|
||||
filters.append(ConversationSession.status == status)
|
||||
total = await session.scalar(
|
||||
select(func.count()).select_from(ConversationSession).where(*filters)
|
||||
)
|
||||
rows = (
|
||||
await session.execute(
|
||||
select(ConversationSession)
|
||||
.where(*filters)
|
||||
.order_by(
|
||||
ConversationSession.started_at.asc()
|
||||
if sort_order == "oldest"
|
||||
else ConversationSession.started_at.desc()
|
||||
)
|
||||
.offset((page - 1) * page_size)
|
||||
.limit(page_size)
|
||||
)
|
||||
).scalars().all()
|
||||
return ConversationListOut(
|
||||
items=[_session_out(row) for row in rows],
|
||||
total=int(total or 0),
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{conversation_id}", response_model=ConversationDetailOut)
|
||||
async def get_conversation(
|
||||
conversation_id: str,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
conversation = await session.get(ConversationSession, conversation_id)
|
||||
if not conversation:
|
||||
raise HTTPException(404, "对话记录不存在")
|
||||
messages = (
|
||||
await session.execute(
|
||||
select(ConversationMessage)
|
||||
.where(ConversationMessage.session_id == conversation_id)
|
||||
.order_by(ConversationMessage.sequence)
|
||||
)
|
||||
).scalars().all()
|
||||
return ConversationDetailOut(
|
||||
**_session_out(conversation).model_dump(),
|
||||
messages=[
|
||||
ConversationMessageOut(
|
||||
id=message.id,
|
||||
sequence=message.sequence,
|
||||
role=message.role,
|
||||
content_type=message.content_type,
|
||||
content=message.content,
|
||||
occurred_at=message.occurred_at,
|
||||
extra=message.extra or {},
|
||||
)
|
||||
for message in messages
|
||||
],
|
||||
)
|
||||
@@ -120,7 +120,13 @@ async def _handle_offer(websocket, payload, peers):
|
||||
video_in_enabled=vision_enabled,
|
||||
)
|
||||
asyncio.create_task(
|
||||
run_pipeline(transport, cfg, vision_enabled=vision_enabled)
|
||||
run_pipeline(
|
||||
transport,
|
||||
cfg,
|
||||
vision_enabled=vision_enabled,
|
||||
assistant_id=offer.assistant_id,
|
||||
channel="webrtc",
|
||||
)
|
||||
)
|
||||
|
||||
answer = pc.get_answer()
|
||||
|
||||
@@ -24,13 +24,16 @@ from starlette.websockets import WebSocketDisconnect
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
async def _resolve_start_config(raw: str) -> AssistantConfig:
|
||||
async def _resolve_start_config(raw: str) -> tuple[AssistantConfig, str | None]:
|
||||
data = json.loads(raw)
|
||||
if data.get("assistant_id"):
|
||||
async with SessionLocal() as session:
|
||||
return await resolve_runtime_config(session, data["assistant_id"])
|
||||
return (
|
||||
await resolve_runtime_config(session, data["assistant_id"]),
|
||||
data["assistant_id"],
|
||||
)
|
||||
if data.get("inline_config"):
|
||||
return AssistantConfig(**data["inline_config"])
|
||||
return AssistantConfig(**data["inline_config"]), None
|
||||
raise ValueError("启动参数缺少 assistant_id 或 inline_config")
|
||||
|
||||
|
||||
@@ -43,10 +46,10 @@ async def voice_stream(websocket: WebSocket):
|
||||
return
|
||||
await websocket.accept()
|
||||
try:
|
||||
cfg = await _resolve_start_config(await websocket.receive_text())
|
||||
cfg, assistant_id = await _resolve_start_config(await websocket.receive_text())
|
||||
transport = build_ws_transport(websocket)
|
||||
# 直接 await:管线持续读这条 WS 的音频帧,直到对端断开
|
||||
await run_pipeline(transport, cfg)
|
||||
await run_pipeline(transport, cfg, assistant_id=assistant_id, channel="websocket")
|
||||
except WebSocketDisconnect:
|
||||
logger.info("WS 音频流断开")
|
||||
except Exception as e:
|
||||
|
||||
Reference in New Issue
Block a user