Files

117 lines
4.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from typing import Optional, Literal
import aiosqlite, json, logging
import httpx
from database import get_db
from config import settings
from services.ai_client import get_openai_client
from services.runtime_settings import get_ai_settings
from services.media_parser import parse_media
router = APIRouter(prefix="/api", tags=["ai"])
log = logging.getLogger(__name__)
async def _get_ai_client():
return await get_openai_client()
class SummarizeRequest(BaseModel):
context: str # 已组装好的对话文本(含媒体描述)
room_name: Optional[str] = ""
messages: Optional[list] = None # 兼容旧调用,忽略
class ParseRequest(BaseModel):
type: Literal["voice", "image", "video"]
key: str # voice: ServerID string; image/video: md5
@router.post("/ai/parse")
async def ai_parse(body: ParseRequest):
"""
通过 FastAPI 代理 AI 媒体解析:
- voice: 从 chatlog 下载音频 → DashScope Paraformer ASR 转文字
- image/video: 从 chatlog 下载媒体 → base64 → 视觉模型描述
"""
try:
return await parse_media(body.type, body.key)
except HTTPException:
raise
except Exception as e:
log.error(f"[ai/parse] 媒体解析失败: {e}", exc_info=True)
raise HTTPException(500, f"媒体解析失败: {e}")
@router.post("/ai/summarize/stream")
async def summarize_stream(body: SummarizeRequest):
"""
接收前端已处理好的对话上下文,调用 AI 模型流式输出总结。
前端负责先把媒体(图片/语音/视频)解析成文字再拼进 context。
"""
_ai = await get_ai_settings()
if not _ai.get("ai_api_key"):
async def err_gen():
yield 'data: {"error": "AI 服务未配置,请在「设置」页面填入 AI API Key"}\n\n'
return StreamingResponse(err_gen(), media_type="text/event-stream")
if not _ai.get("ai_model"):
async def err_gen():
yield 'data: {"error": "知识总结模型未配置,请在「设置」页面填入模型名称(如 qwen-max"}\n\n'
return StreamingResponse(err_gen(), media_type="text/event-stream")
context = body.context.strip()
if not context:
async def err_gen():
yield 'data: {"error": "对话内容为空"}\n\n'
return StreamingResponse(err_gen(), media_type="text/event-stream")
room = body.room_name or "会话"
system_prompt = (
"你是一位专业的对话分析助手。"
"请根据提供的聊天记录(可能包含图片描述、语音转文字、视频描述等多媒体内容)"
"生成一份结构清晰的 Markdown 总结。"
"总结应包含:主要话题、关键信息点、媒体内容要点、待办事项(如有)。"
"只输出 Markdown 格式内容,不要有任何额外说明。"
)
user_prompt = (
f"群聊:{room}\n\n"
f"以下是聊天记录(含多媒体内容描述):\n\n"
f"{context[:12000]}\n\n" # 限制 token 数
f"请生成总结:"
)
async def generate():
try:
_client, _ai = await _get_ai_client()
stream = await _client.chat.completions.create(
model=_ai["ai_model"],
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
],
stream=True,
temperature=0.3,
)
async for chunk in stream:
delta = chunk.choices[0].delta.content if chunk.choices else None
if delta:
yield f"data: {json.dumps({'delta': delta}, ensure_ascii=False)}\n\n"
yield 'data: {"done": true}\n\n'
except Exception as e:
log.error(f"[summarize/stream] LLM 调用失败: {e}", exc_info=True)
yield f"data: {json.dumps({'error': str(e)}, ensure_ascii=False)}\n\n"
return StreamingResponse(generate(), media_type="text/event-stream")
@router.get("/tasks/{task_id}")
async def get_task(task_id: int, db: aiosqlite.Connection = Depends(get_db)):
async with db.execute("SELECT * FROM ai_tasks WHERE id=?", (task_id,)) as cur:
row = await cur.fetchone()
if not row:
raise HTTPException(404, "not found")
return dict(row)