Initial upload for secondary development
This commit is contained in:
116
chatlog_fastAPI/routers/ai.py
Normal file
116
chatlog_fastAPI/routers/ai.py
Normal file
@@ -0,0 +1,116 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user