Files
ai-agent-admin/backend-fastapi/ai_platform/api/chat_api.py
T
2026-06-08 18:14:59 +08:00

338 lines
11 KiB
Python

"""
对话 API
"""
import json
import logging
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.responses import StreamingResponse
from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.base_schema import PaginatedResponse, ResponseModel
from utils.context import get_current_user_id_from_context
from ai_platform.models import Conversation, Message, AIApp
from ai_platform.schemas.chat_schema import (
ConversationCreate,
ConversationUpdate,
ConversationResponse,
ConversationListResponse,
MessageResponse,
SendMessageInput,
MessageFeedbackInput,
)
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/chat", tags=["AI-对话"])
@router.get("/conversations", response_model=PaginatedResponse[ConversationListResponse], summary="对话列表")
async def list_conversations(
app_id: str = Query(..., description="应用 ID"),
page: int = Query(1, ge=1, description="页码"),
page_size: int = Query(20, ge=1, le=100, alias="pageSize", description="每页数量"),
db: AsyncSession = Depends(get_db),
):
"""获取对话列表"""
query = select(Conversation).where(
Conversation.app_id == app_id,
Conversation.is_deleted == False
)
# 获取总数
count_result = await db.execute(select(func.count()).select_from(query.subquery()))
total = count_result.scalar() or 0
# 分页
offset = (page - 1) * page_size
query = query.order_by(
Conversation.is_pinned.desc(),
Conversation.sys_update_datetime.desc()
)
query = query.offset(offset).limit(page_size)
result = await db.execute(query)
conversations = result.scalars().all()
items = [
{
"id": c.id,
"title": c.title or "",
"message_count": c.message_count or 0,
"is_pinned": c.is_pinned or False,
"sys_update_datetime": c.sys_update_datetime,
}
for c in conversations
]
return PaginatedResponse(items=items, total=total)
@router.post("/conversations", response_model=ConversationResponse, summary="创建对话")
async def create_conversation(data: ConversationCreate, db: AsyncSession = Depends(get_db)):
"""创建对话"""
# 验证应用
app_result = await db.execute(
select(AIApp).where(AIApp.id == data.app_id, AIApp.is_deleted == False)
)
app = app_result.scalar_one_or_none()
if not app:
raise HTTPException(status_code=400, detail="应用不存在")
user_id = get_current_user_id_from_context()
if not user_id:
raise HTTPException(status_code=401, detail="未提供认证凭据")
conversation = Conversation(
app_id=data.app_id,
user_id=user_id,
title=data.title or "新对话",
)
db.add(conversation)
await db.commit()
await db.refresh(conversation)
return await _build_conversation_response(conversation, db)
@router.get("/conversations/{conversation_id}", response_model=ConversationResponse, summary="对话详情")
async def get_conversation(conversation_id: str, db: AsyncSession = Depends(get_db)):
"""获取对话详情"""
result = await db.execute(
select(Conversation).where(
Conversation.id == conversation_id,
Conversation.is_deleted == False
)
)
conversation = result.scalar_one_or_none()
if not conversation:
raise HTTPException(status_code=404, detail="对话不存在")
return await _build_conversation_response(conversation, db)
@router.put("/conversations/{conversation_id}", response_model=ConversationResponse, summary="更新对话")
async def update_conversation(
conversation_id: str,
data: ConversationUpdate,
db: AsyncSession = Depends(get_db),
):
"""更新对话"""
result = await db.execute(
select(Conversation).where(
Conversation.id == conversation_id,
Conversation.is_deleted == False
)
)
conversation = result.scalar_one_or_none()
if not conversation:
raise HTTPException(status_code=404, detail="对话不存在")
if data.title is not None:
conversation.title = data.title
if data.is_pinned is not None:
conversation.is_pinned = data.is_pinned
await db.commit()
await db.refresh(conversation)
return await _build_conversation_response(conversation, db)
@router.delete("/conversations/{conversation_id}", response_model=ResponseModel, summary="删除对话")
async def delete_conversation(conversation_id: str, db: AsyncSession = Depends(get_db)):
"""删除对话"""
result = await db.execute(
select(Conversation).where(
Conversation.id == conversation_id,
Conversation.is_deleted == False
)
)
conversation = result.scalar_one_or_none()
if not conversation:
raise HTTPException(status_code=404, detail="对话不存在")
conversation.is_deleted = True
await db.commit()
return ResponseModel(message="删除成功")
@router.get("/conversations/{conversation_id}/messages", response_model=List[MessageResponse], summary="获取消息列表")
async def get_messages(
conversation_id: str,
limit: int = Query(50, ge=1, le=200, description="限制数量"),
db: AsyncSession = Depends(get_db),
):
"""获取对话消息"""
# 验证对话存在
conv_result = await db.execute(
select(Conversation).where(
Conversation.id == conversation_id,
Conversation.is_deleted == False
)
)
if not conv_result.scalar_one_or_none():
raise HTTPException(status_code=404, detail="对话不存在")
query = select(Message).where(
Message.conversation_id == conversation_id,
Message.is_deleted == False
).order_by(Message.sys_create_datetime).limit(limit)
result = await db.execute(query)
messages = result.scalars().all()
return [
{
"id": m.id,
"role": m.role,
"content": m.content or "",
"status": m.status or "completed",
"prompt_tokens": m.prompt_tokens or 0,
"completion_tokens": m.completion_tokens or 0,
"total_tokens": m.total_tokens or 0,
"model_name": m.model_name or "",
"latency": m.latency or 0,
"error_message": m.error_message or "",
"feedback": m.feedback or "",
"sys_create_datetime": m.sys_create_datetime,
}
for m in messages
]
@router.post("/conversations/{conversation_id}/messages", response_model=MessageResponse, summary="发送消息")
async def send_message(
conversation_id: str,
data: SendMessageInput,
db: AsyncSession = Depends(get_db),
):
"""发送消息并获取 AI 回复"""
from ai_platform.services.chat_service import ChatService
# 验证对话存在
conv_result = await db.execute(
select(Conversation).where(
Conversation.id == conversation_id,
Conversation.is_deleted == False
)
)
conversation = conv_result.scalar_one_or_none()
if not conversation:
raise HTTPException(status_code=404, detail="对话不存在")
# 使用ChatService发送消息
chat_service = ChatService(db)
_, assistant_message = await chat_service.send_message(
conversation_id=conversation_id,
content=data.content,
)
return {
"id": assistant_message.id,
"role": assistant_message.role,
"content": assistant_message.content or "",
"status": assistant_message.status or "completed",
"prompt_tokens": assistant_message.prompt_tokens or 0,
"completion_tokens": assistant_message.completion_tokens or 0,
"total_tokens": assistant_message.total_tokens or 0,
"model_name": assistant_message.model_name or "",
"latency": assistant_message.latency or 0,
"error_message": assistant_message.error_message or "",
"feedback": assistant_message.feedback or "",
"sys_create_datetime": assistant_message.sys_create_datetime,
}
@router.post("/conversations/{conversation_id}/messages/stream", summary="流式发送消息")
async def send_message_stream(
conversation_id: str,
data: SendMessageInput,
db: AsyncSession = Depends(get_db),
):
"""发送消息并获取 AI 流式回复(SSE)"""
from ai_platform.services.chat_service import ChatService
# 验证对话存在
conv_result = await db.execute(
select(Conversation).where(
Conversation.id == conversation_id,
Conversation.is_deleted == False
)
)
conversation = conv_result.scalar_one_or_none()
if not conversation:
raise HTTPException(status_code=404, detail="对话不存在")
async def generate():
chat_service = ChatService(db)
async for event in chat_service.send_message_stream(
conversation_id=conversation_id,
content=data.content,
):
yield f"data: {json.dumps(event, ensure_ascii=False)}\n\n"
yield "data: [DONE]\n\n"
return StreamingResponse(
generate(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
}
)
@router.post("/messages/{message_id}/feedback", response_model=ResponseModel, summary="消息反馈")
async def message_feedback(
message_id: str,
data: MessageFeedbackInput,
db: AsyncSession = Depends(get_db),
):
"""对消息进行反馈"""
result = await db.execute(
select(Message).where(
Message.id == message_id,
Message.is_deleted == False
)
)
message = result.scalar_one_or_none()
if not message:
raise HTTPException(status_code=404, detail="消息不存在")
if data.feedback not in ("like", "dislike", ""):
raise HTTPException(status_code=400, detail="无效的反馈类型")
message.feedback = data.feedback
await db.commit()
return ResponseModel(message="反馈成功")
async def _build_conversation_response(conversation: Conversation, db: AsyncSession) -> dict:
"""构建对话输出"""
app_name = ""
if conversation.app_id:
app_result = await db.execute(
select(AIApp).where(AIApp.id == conversation.app_id)
)
app = app_result.scalar_one_or_none()
app_name = app.name if app else ""
return {
"id": conversation.id,
"app_id": conversation.app_id,
"app_name": app_name,
"title": conversation.title or "",
"message_count": conversation.message_count or 0,
"total_tokens": conversation.total_tokens or 0,
"is_pinned": conversation.is_pinned or False,
"sort": conversation.sort or 0,
"sys_create_datetime": conversation.sys_create_datetime,
"sys_update_datetime": conversation.sys_update_datetime,
}