Files
ai-video-fullstack/backend/routes/conversations.py

201 lines
6.8 KiB
Python

"""对话历史查询 API。"""
import asyncio
from db.models import ConversationArtifact, ConversationMessage, ConversationSession
from db.session import get_session
from fastapi import APIRouter, Depends, HTTPException, Query, Response
from loguru import logger
from schemas import (
ConversationArtifactOut,
ConversationDetailOut,
ConversationListOut,
ConversationMessageOut,
ConversationOut,
)
from services.auth import require_admin
from services.object_storage import delete_object, get_object, key_from_storage_uri
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()
artifacts = (
await session.execute(
select(ConversationArtifact)
.where(ConversationArtifact.session_id == conversation_id)
.order_by(ConversationArtifact.created_at)
)
).scalars().all()
artifacts_by_message: dict[str, list[ConversationArtifact]] = {}
for artifact in artifacts:
if artifact.message_id:
artifacts_by_message.setdefault(artifact.message_id, []).append(artifact)
return ConversationDetailOut(
**_session_out(conversation).model_dump(),
extra=conversation.extra or {},
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 {},
artifacts=[
ConversationArtifactOut(
id=artifact.id,
kind=artifact.kind,
content_url=(
f"/api/conversations/{conversation_id}/artifacts/"
f"{artifact.id}/content"
),
mime_type=artifact.mime_type,
size_bytes=artifact.size_bytes,
duration_ms=artifact.duration_ms,
extra=artifact.extra or {},
)
for artifact in artifacts_by_message.get(message.id, [])
],
)
for message in messages
],
)
@router.get("/{conversation_id}/artifacts/{artifact_id}/content")
async def get_conversation_artifact(
conversation_id: str,
artifact_id: str,
session: AsyncSession = Depends(get_session),
):
artifact = await session.get(ConversationArtifact, artifact_id)
if not artifact or artifact.session_id != conversation_id:
raise HTTPException(404, "会话附件不存在")
try:
key = key_from_storage_uri(artifact.storage_uri)
data = await asyncio.to_thread(get_object, key)
except Exception as exc:
logger.warning(f"读取会话附件失败: {artifact_id}: {exc}")
raise HTTPException(404, "会话附件不可用") from exc
return Response(
content=data,
media_type=artifact.mime_type or "application/octet-stream",
headers={
"Cache-Control": "private, max-age=300",
"Content-Disposition": f'inline; filename="{artifact.id}"',
},
)
@router.delete("/{conversation_id}")
async def delete_conversation(
conversation_id: str,
session: AsyncSession = Depends(get_session),
):
conversation = await session.get(ConversationSession, conversation_id)
if not conversation:
raise HTTPException(404, "对话记录不存在")
storage_uris = (
await session.execute(
select(ConversationArtifact.storage_uri).where(
ConversationArtifact.session_id == conversation_id
)
)
).scalars().all()
await session.delete(conversation)
await session.commit()
for storage_uri in storage_uris:
try:
key = key_from_storage_uri(storage_uri)
await asyncio.to_thread(delete_object, key)
except Exception as exc:
logger.warning(f"清理已删除会话的附件失败: {storage_uri}: {exc}")
return {"ok": True}