init: 初始化 dpb 桃育种系统代码库

前后端 + 后端 FastAPI 全量源码、部署脚本与文档。
This commit is contained in:
34047007@qq.com
2026-08-06 00:17:49 +08:00
commit b95053c52c
1469 changed files with 322298 additions and 0 deletions
@@ -0,0 +1,302 @@
import asyncio
import json
from typing import Annotated, Any
from fastapi import APIRouter, Body, Depends, Path, Query, Security, WebSocket, WebSocketDisconnect, status
from fastapi.responses import JSONResponse
from redis.asyncio import Redis
from app.common.response import ResponseSchema, SuccessResponse
from app.core.base_schema import AuthSchema, PaginationQueryParam
from app.core.database import async_db_session
from app.core.dependencies import AuthPermission, _authenticate, redis_getter
from app.core.exceptions import CustomException
from app.core.logger import logger
from app.core.router_class import OperationLogRoute
from .schema import (
AiChatRequestSchema,
AiChatResponseSchema,
AiModelConfigListResponse,
AiModelConfigSchema,
AiModelConfigUpdateSchema,
ChatQuerySchema,
ChatSessionCreateSchema,
ChatSessionQueryParam,
ChatSessionUpdateSchema,
)
from .service import AiModelConfigService, ChatService, get_user_model_config
ChatRouter = APIRouter(route_class=OperationLogRoute, prefix="/chat", tags=["AI管理"])
@ChatRouter.get("/detail/{session_id}", summary="获取会话详情", response_model=ResponseSchema[dict[str, Any]])
async def get_session_detail_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_ai:chat:detail"]))],
session_id: Annotated[str, Path(description="会话ID")],
) -> JSONResponse:
service = ChatService(auth)
result = await service.get_session(session_id=session_id)
return SuccessResponse(data=result, msg="获取会话详情成功")
@ChatRouter.get("/list", summary="查询会话列表", response_model=ResponseSchema[dict])
async def get_session_list_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_ai:chat:query"]))],
page: Annotated[PaginationQueryParam, Depends()],
search: Annotated[ChatSessionQueryParam, Query()],
) -> JSONResponse:
service = ChatService(auth)
result_dict = await service.page(
page_no=page.page_no,
page_size=page.page_size,
search=search,
order_by=page.order_by,
)
return SuccessResponse(data=result_dict, msg="查询会话列表成功")
@ChatRouter.post("/create", status_code=status.HTTP_201_CREATED, summary="创建会话", response_model=ResponseSchema[dict[str, Any]])
async def create_session_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_ai:chat:create"]))],
data: Annotated[ChatSessionCreateSchema, Body(description="会话创建参数")],
) -> JSONResponse:
service = ChatService(auth)
result = await service.create(data=data)
return SuccessResponse(data=result, msg="创建会话成功")
@ChatRouter.put("/update/{session_id}", summary="更新会话", response_model=ResponseSchema[None])
async def update_session_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_ai:chat:update"]))],
session_id: Annotated[str, Path(description="会话ID")],
data: Annotated[ChatSessionUpdateSchema, Body(description="会话更新参数")],
) -> JSONResponse:
service = ChatService(auth)
await service.update(session_id=session_id, data=data)
return SuccessResponse(data=None, msg="更新会话成功")
@ChatRouter.delete("/delete", summary="删除会话", response_model=ResponseSchema[None])
async def delete_session_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_ai:chat:delete"]))],
session_ids: Annotated[list[str], Body(description="会话ID列表")],
) -> JSONResponse:
service = ChatService(auth)
await service.delete(session_ids=session_ids)
return SuccessResponse(data=None, msg="删除会话成功")
@ChatRouter.post("/ai-chat", summary="AI 对话(非流式)", response_model=ResponseSchema[AiChatResponseSchema])
async def ai_chat_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_ai:chat:query"]))],
data: Annotated[AiChatRequestSchema, Body(description="对话请求")],
) -> JSONResponse:
service = ChatService(auth)
result = await service.chat_non_stream(
message=data.message,
session_id=data.session_id,
)
return SuccessResponse(
data=AiChatResponseSchema(
response=result["response"],
session_id=result["session_id"],
function_calls=result.get("function_calls"),
action=result.get("action"),
),
msg="对话成功",
)
@ChatRouter.get("/model", summary="获取 AI 模型配置列表", response_model=ResponseSchema[AiModelConfigListResponse])
async def list_model_config_controller(
redis: Annotated[Redis, Depends(redis_getter)],
auth: Annotated[AuthSchema, Security(AuthPermission(["module_ai:chat:query"]))],
) -> JSONResponse:
service = AiModelConfigService(auth, redis)
result = await service.list()
return SuccessResponse(data=result, msg="获取模型配置列表成功")
@ChatRouter.post("/model", status_code=status.HTTP_201_CREATED, summary="新增一个 AI 模型配置", response_model=ResponseSchema[dict[str, Any]])
async def create_model_config_controller(
redis: Annotated[Redis, Depends(redis_getter)],
auth: Annotated[AuthSchema, Security(AuthPermission(["module_ai:chat:update"]))],
data: Annotated[AiModelConfigUpdateSchema, Body(description="模型配置参数")],
) -> JSONResponse:
service = AiModelConfigService(auth, redis)
payload = AiModelConfigSchema(**data.model_dump())
result = await service.create(payload)
return SuccessResponse(data=result, msg="模型配置已新增")
@ChatRouter.put("/model/{config_id}", summary="更新指定 ID 的 AI 模型配置", response_model=ResponseSchema[dict[str, Any]])
async def update_model_config_controller(
redis: Annotated[Redis, Depends(redis_getter)],
auth: Annotated[AuthSchema, Security(AuthPermission(["module_ai:chat:update"]))],
config_id: Annotated[str, Path(description="配置项 ID")],
data: Annotated[AiModelConfigUpdateSchema, Body(description="模型配置参数")],
) -> JSONResponse:
service = AiModelConfigService(auth, redis)
payload = AiModelConfigSchema(**data.model_dump())
result = await service.update(config_id, payload)
return SuccessResponse(data=result, msg="模型配置已更新")
@ChatRouter.delete("/model/{config_id}", summary="删除指定 ID 的 AI 模型配置", response_model=ResponseSchema[None])
async def delete_model_config_controller(
redis: Annotated[Redis, Depends(redis_getter)],
auth: Annotated[AuthSchema, Security(AuthPermission(["module_ai:chat:update"]))],
config_id: Annotated[str, Path(description="配置项 ID")],
) -> JSONResponse:
service = AiModelConfigService(auth, redis)
await service.delete(config_id)
return SuccessResponse(data=None, msg="模型配置已删除")
@ChatRouter.post("/model/{config_id}/activate", summary="切换激活的 AI 模型配置", response_model=ResponseSchema[None])
async def activate_model_config_controller(
redis: Annotated[Redis, Depends(redis_getter)],
auth: Annotated[AuthSchema, Security(AuthPermission(["module_ai:chat:update"]))],
config_id: Annotated[str, Path(description="配置项 ID;传 __default__ 使用系统默认")],
) -> JSONResponse:
service = AiModelConfigService(auth, redis)
await service.set_active(config_id)
return SuccessResponse(data=None, msg="已切换模型")
async def _send_error_and_close(websocket: WebSocket, message: str) -> None:
"""发送错误消息并关闭连接"""
try:
await websocket.send_text(f"错误: {message}")
except RuntimeError:
pass
finally:
try:
await websocket.close()
except RuntimeError:
pass
@ChatRouter.websocket("/ws", name="WebSocket聊天")
async def websocket_chat_controller(websocket: WebSocket) -> None:
"""WebSocket 聊天接口。
支持的消息格式(JSON):
- 对话:{"message": "...", "session_id": "...", "files": [...]}
- 停止:{"action": "stop", "session_id": "..."}
ws://127.0.0.1:8001/api/v1/ai/chat/ws?token=xxx
"""
# 接收客户端 subprotocol:约定客户端在 Sec-WebSocket-Protocol 中以 "access_token.<jwt>" 携带
# 推荐方式:subprotocol 不会进 URL,不出现在 Nginx access log / 浏览器历史 / 抓包日志
# 同时兼容旧版:用 query_params 传 token(不推荐,仅作向后兼容)
#
# 浏览器侧示例:
# new WebSocket(url, ["access_token", "access_token." + jwt])
# Python websocket-client 示例:
# websockets.connect(url, subprotocols=["access_token", f"access_token.{jwt}"])
token = None
use_subprotocol = False
if websocket.headers.get("sec-websocket-protocol"):
for proto in websocket.headers["sec-websocket-protocol"].split(","):
proto = proto.strip()
if proto.startswith("access_token."):
token = proto[len("access_token.") :]
use_subprotocol = True
break
if not token:
# 旧版/非浏览器客户端兼容:保留 query ?token=
token = websocket.query_params.get("token")
if not token:
await _send_error_and_close(websocket, "未提供认证token,请重新登录")
return
if use_subprotocol:
await websocket.accept(subprotocol="access_token")
else:
await websocket.accept()
# 跨消息循环共享的停止信号:客户端发送 stop 时 set,生成器检测到后退出
stop_event = asyncio.Event()
# 标记当前是否在生成中,便于 stop 校验
is_generating = asyncio.Event()
try:
redis = websocket.app.state.redis
async with async_db_session() as db:
auth = await _authenticate(token, db, redis)
logger.info("WebSocket连接已建立: {} - 用户: {}", websocket.client, auth.user.username or "未认证")
chat_service = ChatService(auth)
# 消息循环
while True:
try:
data = await websocket.receive_text()
try:
message_data = json.loads(data)
query = ChatQuerySchema(**message_data)
except json.JSONDecodeError:
logger.warning("收到非JSON消息: {}", data)
await websocket.send_text("消息格式错误,请发送JSON格式的消息")
continue
except Exception as e:
logger.warning("消息校验失败: {}", e)
await websocket.send_text(f"消息格式错误: {e}")
continue
# 处理停止指令
if query.action == "stop":
if is_generating.is_set():
stop_event.set()
logger.info("收到停止指令: session={}", query.session_id)
await websocket.send_text("[STOPPED]")
else:
await websocket.send_text("当前没有正在进行的生成任务")
continue
# 对话指令
logger.info("收到聊天查询: session_id={}", query.session_id)
is_generating.set()
stop_event.clear()
# 读取用户的 AI 模型配置(每次可动态切换)
model_config = await get_user_model_config(redis, auth.user.id)
try:
async for chunk in chat_service.chat_query(
query=query,
stop_event=stop_event,
model_config=model_config,
):
if not chunk:
continue
try:
await websocket.send_text(chunk)
except RuntimeError:
logger.warning("WebSocket连接已关闭,停止发送消息")
return
finally:
is_generating.clear()
stop_event.clear()
# 告知前端生成结束
try:
await websocket.send_text("[DONE]")
except RuntimeError:
return
except WebSocketDisconnect:
logger.info("WebSocket连接已断开: {}", websocket.client)
return
except CustomException as e:
# 认证失败等业务异常
logger.warning("WebSocket认证失败: {}", e.msg)
await _send_error_and_close(websocket, e.msg)
except Exception as e:
# 未知异常
logger.exception("WebSocket未知异常: {}", e)
await _send_error_and_close(websocket, "服务器内部错误")
+158
View File
@@ -0,0 +1,158 @@
from typing import Any
from agno.db.base import SessionType
from agno.db.mysql import MySQLDb
from agno.db.postgres import PostgresDb
from agno.db.sqlite import SqliteDb
from agno.session.team import TeamSession
from app.config.setting import settings
from app.core.base_schema import AuthSchema
from app.core.logger import logger
from .schema import ChatSessionCreateSchema, ChatSessionUpdateSchema
class ChatSessionCRUD:
"""聊天会话数据层 - 使用 agno 数据库存储"""
# 会话类型配置 - 使用 TEAM 类型因为创建的是 Team
SESSION_TYPE = SessionType.TEAM
def __init__(self, auth: AuthSchema) -> None:
"""初始化CRUD数据层"""
self.auth = auth
self.user_id = auth.user.username or "user"
self.team_id = "default"
self.db = self._get_db()
def _get_db(self) -> Any:
"""获取数据库连接"""
db_type = settings.DATABASE_TYPE
db_uri = settings.DB_URI
db_mapping = {
"mysql": lambda: MySQLDb(db_url=db_uri, db_schema=settings.DATABASE_NAME, create_schema=False),
"postgres": lambda: PostgresDb(db_url=db_uri, db_schema="public", create_schema=False),
"sqlite": lambda: SqliteDb(db_file=db_uri.replace("sqlite:///", "")),
}
if db_type not in db_mapping:
raise ValueError(f"不支持的数据库类型: {db_type}")
return db_mapping[db_type]()
async def get_by_id_crud(self, session_id: str) -> TeamSession | None:
"""获取会话详情。
参数:
- session_id (str): 会话 ID。
返回:
- TeamSession | None: 会话对象;失败或不存在时为 None。
"""
try:
return self.db.get_session(session_id=session_id, session_type=self.SESSION_TYPE, user_id=self.user_id)
except Exception as e:
logger.error(f"获取会话详情失败: {e}")
return None
async def list_crud(
self,
search: dict[str, Any] | None = None,
order_by: list[dict[str, str]] | None = None,
) -> list[TeamSession]:
"""列表查询,获取当前用户的所有会话。
参数:
- search (dict[str, Any] | None): 预留查询条件(当前实现未使用)。
- order_by (list[dict[str, str]] | None): 预留排序(当前实现未使用)。
返回:
- list[TeamSession]: 会话列表;失败时为空列表。
"""
try:
result = self.db.get_sessions(session_type=self.SESSION_TYPE, user_id=self.user_id)
if isinstance(result, tuple) and len(result) == 2:
return result[0]
return result if isinstance(result, list) else []
except Exception as e:
logger.error(f"获取会话列表失败: {e}")
return []
async def create_crud(self, data: ChatSessionCreateSchema) -> TeamSession | None:
"""创建会话(Team 在运行时自动创建并管理 session)。
参数:
- data (ChatSessionCreateSchema): 创建参数(如标题)。
返回:
- TeamSession | None: 新建会话;失败时为 None。
"""
import time
import uuid
try:
session_id = str(uuid.uuid4())
now = int(time.time())
# 创建 session_data,包含 session_name
session_data = {}
if data.title:
session_data["session_name"] = data.title
# 创建 TeamSession 对象
session = TeamSession(
session_id=session_id,
user_id=self.user_id,
team_id=self.team_id,
session_data=session_data,
created_at=now,
updated_at=now,
)
# 保存会话
result = self.db.upsert_session(session=session)
return result
except Exception as e:
logger.exception(f"创建会话失败: {e}")
return None
async def update_crud(self, session_id: str, data: ChatSessionUpdateSchema) -> bool:
"""更新会话(如重命名)。
参数:
- session_id (str): 会话 ID。
- data (ChatSessionUpdateSchema): 更新数据。
返回:
- bool: 是否成功。
"""
try:
self.db.rename_session(
session_id=session_id,
session_type=self.SESSION_TYPE,
session_name=data.title,
user_id=self.user_id,
)
return True
except Exception as e:
logger.error(f"更新会话失败: {e}")
return False
async def delete_crud(self, session_ids: list[str]) -> bool:
"""批量删除会话。
参数:
- session_ids (list[str]): 会话 ID 列表。
返回:
- bool: 是否全部处理成功(任一出错则记日志并返回 False)。
"""
try:
for session_id in session_ids:
self.db.delete_session(session_id=session_id, user_id=self.user_id)
return True
except Exception as e:
logger.error(f"删除会话失败: {e}")
return False
+129
View File
@@ -0,0 +1,129 @@
from typing import Any
from pydantic import BaseModel, ConfigDict, Field, field_validator
from app.core.base_schema import BaseQueryParam, UserByQueryParam
class ChatQuerySchema(BaseModel):
"""WebSocket聊天查询模型"""
message: str | None = Field("", description="消息内容(停止时可为空)")
session_id: str | None = Field(None, description="会话ID")
files: list[dict[str, Any]] | None = Field(None, description="文件信息")
action: str | None = Field(None, description="动作类型:stop=停止生成 | None=对话")
class ChatSessionCreateSchema(BaseModel):
"""创建会话模型"""
title: str = Field(..., min_length=1, max_length=200, description="会话标题")
@field_validator("title")
@classmethod
def validate_title(cls, v: str) -> str:
v = v.strip()
if len(v) < 1 or len(v) > 200:
raise ValueError("会话标题长度必须在1-200个字符之间")
return v
class ChatSessionUpdateSchema(BaseModel):
"""更新会话模型"""
title: str = Field(..., min_length=1, max_length=200, description="会话标题")
@field_validator("title")
@classmethod
def validate_title(cls, v: str) -> str:
v = v.strip()
if len(v) < 1 or len(v) > 200:
raise ValueError("会话标题长度必须在1-200个字符之间")
return v
class ChatSessionMessageSchema(BaseModel):
"""会话消息模型"""
id: str = Field(..., description="消息ID")
role: str = Field(..., description="消息角色")
content: str = Field(..., description="消息内容")
created_at: int | None = Field(None, description="创建时间(Unix时间戳)")
model_config = ConfigDict(from_attributes=True)
class ChatSessionQueryParam(BaseQueryParam, UserByQueryParam):
"""会话查询参数"""
title: str | None = Field(None, description="会话标题")
class AiChatRequestSchema(BaseModel):
"""AI 对话请求模型(非流式)"""
message: str = Field(..., min_length=1, description="用户消息内容")
session_id: str | None = Field(None, description="会话ID,不传则创建新会话")
@field_validator("message")
@classmethod
def validate_message(cls, v: str) -> str:
v = v.strip()
if len(v) < 1:
raise ValueError("用户消息内容不能为空")
return v
class AiChatResponseSchema(BaseModel):
"""AI 对话响应模型(非流式)"""
response: str = Field(..., description="AI 回复内容")
session_id: str = Field(..., description="会话ID")
function_calls: list[dict[str, Any]] | None = Field(None, description="函数调用信息")
action: dict[str, Any] | None = Field(None, description="建议执行的操作")
class AiModelConfigSchema(BaseModel):
"""AI 模型配置项"""
name: str = Field(..., min_length=1, max_length=50, description="配置名称(用户可读)")
base_url: str = Field(..., min_length=1, max_length=500, description="API Base URL,如 https://api.openai.com/v1")
api_key: str = Field(..., min_length=1, max_length=500, description="API 密钥")
model_id: str = Field(..., min_length=1, max_length=100, description="模型 ID")
temperature: float = Field(0.7, ge=0.0, le=2.0, description="温度参数")
@field_validator("base_url")
@classmethod
def validate_base_url(cls, v: str) -> str:
v = v.strip().rstrip("/")
if not v.startswith(("http://", "https://")):
raise ValueError("Base URL 必须以 http:// 或 https:// 开头")
return v
@field_validator("model_id")
@classmethod
def validate_model_id(cls, v: str) -> str:
v = v.strip()
if not v:
raise ValueError("模型 ID 不能为空")
return v
class AiModelConfigItemSchema(AiModelConfigSchema):
"""带 ID 的模型配置项(存储与返回)"""
id: str = Field(..., min_length=1, max_length=64, description="配置项唯一 ID")
created_time: str | None = Field(None, description="创建时间(ISO 字符串)")
class AiModelConfigUpdateSchema(AiModelConfigSchema):
"""更新 AI 模型配置(与创建结构相同,不含 id)"""
class AiModelConfigListResponse(BaseModel):
"""模型配置列表响应"""
items: list[AiModelConfigItemSchema] = Field(default_factory=list, description="配置项列表")
active_id: str | None = Field(None, description="当前激活的配置项 ID;为空表示使用系统默认")
@@ -0,0 +1,513 @@
import asyncio
import json
from collections.abc import AsyncGenerator
from datetime import datetime
from typing import Any
from agno.run.team import TeamRunOutput
from agno.session.team import TeamSession
from agno.team.team import Team
from redis.asyncio import Redis
from sqlalchemy.ext.asyncio import AsyncSession
from app.common.enums import RedisInitKeyConfig
from app.common.request import PaginationService
from app.core.base_schema import AuthSchema, PageResultSchema
from app.core.exceptions import CustomException
from app.core.logger import logger
from app.core.redis_crud import RedisCURD
from app.utils.ai_factory import AgnoFactory
from .crud import ChatSessionCRUD
from .schema import (
AiModelConfigSchema,
ChatQuerySchema,
ChatSessionCreateSchema,
ChatSessionQueryParam,
ChatSessionUpdateSchema,
)
async def _format_session_data(session: TeamSession, auth: AuthSchema | None = None, db: AsyncSession | None = None) -> dict[str, Any]:
"""格式化会话数据,添加前端需要的字段"""
if hasattr(session, "to_dict"):
session_dict = session.to_dict()
else:
session_dict = {
"session_id": getattr(session, "session_id", ""),
"agent_id": getattr(session, "agent_id", None),
"team_id": getattr(session, "team_id", None),
"workflow_id": getattr(session, "workflow_id", None),
"user_id": getattr(session, "user_id", None),
"session_data": getattr(session, "session_data", None),
"agent_data": getattr(session, "agent_data", None),
"team_data": getattr(session, "team_data", None),
"workflow_data": getattr(session, "workflow_data", None),
"metadata": getattr(session, "metadata", None),
"runs": getattr(session, "runs", []),
"summary": getattr(session, "summary", None),
"created_at": getattr(session, "created_at", None),
"updated_at": getattr(session, "updated_at", None),
}
session_data = session_dict.get("session_data") or {}
runs = session_dict.get("runs") or []
messages = _extract_messages(runs)
# 从 session_data 中获取 session_name 作为标题
session_name = session_data.get("session_name") if session_data else None
result = {
**session_dict,
"id": session_dict.get("session_id"),
"title": session_name or session_dict.get("session_id", "")[:8] or "未命名会话",
"created_time": _unix_to_datetime(session_dict.get("created_at")),
"updated_time": _unix_to_datetime(session_dict.get("updated_at")),
"message_count": len(messages),
"messages": messages,
}
# 如果有 auth 和 db,查询部门名称
if auth and db and session_dict.get("team_id"):
try:
team_id_str = session_dict.get("team_id")
if team_id_str:
result["team_name"] = None
except Exception:
result["team_name"] = None
else:
result["team_name"] = None
# 如果 summary 是 SessionSummary 对象,提取 summary 字段
summary = session_dict.get("summary")
if summary:
if isinstance(summary, dict):
result["summary"] = summary.get("summary")
else:
result["summary"] = str(summary)
return result
def _unix_to_datetime(timestamp: int | None) -> str | None:
"""将Unix时间戳转换为日期时间字符串"""
if timestamp is None:
return None
try:
dt = datetime.fromtimestamp(timestamp)
return dt.strftime("%Y-%m-%d %H:%M:%S")
except (ValueError, TypeError, OSError):
return None
def _extract_messages(runs: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""从 runs 中提取消息"""
messages = []
if not runs:
return messages
for run in runs:
if not isinstance(run, dict):
continue
run_messages = run.get("messages", [])
if run_messages and isinstance(run_messages, list):
for msg in run_messages:
if isinstance(msg, dict):
role = msg.get("role")
if role in ("user", "assistant"):
messages.append(
{
"id": msg.get("id"),
"role": role,
"content": msg.get("content", ""),
"created_at": msg.get("created_at"),
},
)
return messages
class ChatService:
"""聊天会话管理模块服务层"""
def __init__(self, auth: AuthSchema) -> None:
self.auth = auth
async def chat_query(
self,
query: ChatQuerySchema,
stop_event: asyncio.Event | None = None,
model_config: dict[str, Any] | None = None,
) -> AsyncGenerator[str | None, Any]:
"""流式 AI 对话"""
try:
crud = ChatSessionCRUD(self.auth)
session_id = query.session_id
if not session_id:
import uuid
session_id = str(uuid.uuid4())
session: TeamSession | None = await crud.create_crud(data=ChatSessionCreateSchema(title="新对话"))
if not session:
raise CustomException(msg="创建会话失败")
session_id = session.session_id
agno_factory = AgnoFactory()
team_id = "default"
agent = agno_factory.create_agent(
user_id=self.auth.user.username or "user",
team_id=team_id,
session_id=session_id,
db=crud.db,
model_config=model_config,
)
message = (query.message or "").strip()
if not message:
yield "请输入消息内容"
return
logger.info("开始流式生成: session_id={} message={!r}", session_id, message[:80])
chunk_count = 0
try:
stream = agent.arun(input=message, stream=True)
logger.info("agent.arun 返回对象类型: {}", type(stream).__name__)
if hasattr(stream, "__aiter__"):
async for chunk in stream: # type: ignore[union-attr]
if stop_event is not None and stop_event.is_set():
logger.info("用户主动停止生成: session_id={}", session_id)
return
if chunk and getattr(chunk, "content", None):
chunk_count += 1
yield str(chunk.content)
else:
logger.debug("空 chunk 跳过: {}", type(chunk).__name__ if chunk else None)
else:
# 兼容非流式直接返回结果的场景
logger.warning("agent.arun 未返回异步迭代器,尝试按单次结果处理")
result: Any = stream
if result and getattr(result, "content", None):
chunk_count += 1
yield str(result.content)
except asyncio.CancelledError:
logger.info("生成任务被取消: session_id={}", session_id)
return
logger.info("流式生成结束: session_id={} chunk_count={}", session_id, chunk_count)
except Exception as e:
logger.error(f"聊天查询失败: {e}", exc_info=True)
yield f"抱歉,处理您的请求时出现错误:{e!s}"
async def chat_non_stream(self, message: str, session_id: str | None) -> dict[str, Any]:
"""非流式 AI 对话"""
try:
crud = ChatSessionCRUD(self.auth)
if not session_id:
import uuid
session_id = str(uuid.uuid4())
session: TeamSession | None = await crud.create_crud(data=ChatSessionCreateSchema(title="新对话"))
if not session:
raise CustomException(msg="创建会话失败")
session_id = session.session_id
agno_factory = AgnoFactory()
team_id = "default"
agent: Team = agno_factory.create_agent(
user_id=self.auth.user.username or "user",
team_id=team_id,
session_id=session_id,
db=crud.db,
)
response: TeamRunOutput = await agent.arun(input=message)
response_text = ""
action = None
if response and response.content:
response_text = response.content
try:
if response_text.strip().startswith("{") and response_text.strip().endswith("}"):
action = json.loads(response_text)
elif "```json" in response_text:
json_start = response_text.find("```json") + 7
json_end = response_text.find("```", json_start)
if json_end > json_start:
json_str = response_text[json_start:json_end].strip()
action = json.loads(json_str)
except (json.JSONDecodeError, Exception):
pass
if not action:
action = self._parse_action_from_response(response_text)
return {
"response": response_text,
"session_id": session_id,
"function_calls": None,
"action": action,
}
except Exception as e:
logger.error(f"聊天查询失败: {e}")
return {
"response": f"抱歉,处理您的请求时出现错误:{e!s}",
"session_id": session_id,
"function_calls": None,
"action": None,
}
@staticmethod
def _parse_action_from_response(response_text: str) -> dict[str, Any] | None:
"""从响应文本中解析操作建议"""
route_config = {
"用户管理": {"path": "/system/user", "name": "用户管理"},
"角色管理": {"path": "/system/role", "name": "角色管理"},
"菜单管理": {"path": "/system/menu", "name": "菜单管理"},
"部门管理": {"path": "/system/dept", "name": "部门管理"},
"字典管理": {"path": "/system/dict", "name": "字典管理"},
"系统日志": {"path": "/system/log", "name": "系统日志"},
}
navigation_keywords = ["跳转", "打开", "进入", "前往", "", "浏览", "查看"]
has_navigation = any(keyword in response_text for keyword in navigation_keywords)
if not has_navigation:
return None
for page_name, route_info in route_config.items():
if page_name in response_text:
return {
"type": "navigate",
"path": route_info["path"],
"name": route_info["name"],
}
keyword_mapping = {
"用户": {"path": "/system/user", "name": "用户管理"},
"角色": {"path": "/system/role", "name": "角色管理"},
"菜单": {"path": "/system/menu", "name": "菜单管理"},
"部门": {"path": "/system/dept", "name": "部门管理"},
"字典": {"path": "/system/dict", "name": "字典管理"},
"日志": {"path": "/system/log", "name": "系统日志"},
}
for keyword, route_info in keyword_mapping.items():
if keyword in response_text:
return {
"type": "navigate",
"path": route_info["path"],
"name": route_info["name"],
}
return None
async def get_session(self, session_id: str) -> dict[str, Any] | None:
crud = ChatSessionCRUD(self.auth)
session: TeamSession | None = await crud.get_by_id_crud(session_id=session_id)
if session:
return await _format_session_data(session, self.auth)
return None
async def create(self, data: ChatSessionCreateSchema) -> dict[str, Any] | None:
crud = ChatSessionCRUD(self.auth)
session = await crud.create_crud(data=data)
if session:
return await _format_session_data(session, self.auth)
return None
async def page(
self,
page_no: int,
page_size: int,
search: ChatSessionQueryParam,
order_by: list[dict[str, str]] | None = None,
) -> PageResultSchema[Any]:
crud = ChatSessionCRUD(self.auth)
sessions = await crud.list_crud()
items = [await _format_session_data(s, self.auth) for s in sessions]
return await PaginationService.paginate(
data_list=items,
page_no=page_no,
page_size=page_size,
)
async def update(self, session_id: str, data: ChatSessionUpdateSchema) -> bool:
crud = ChatSessionCRUD(self.auth)
return await crud.update_crud(session_id=session_id, data=data)
async def delete(self, session_ids: list[str]) -> None:
await ChatSessionCRUD(self.auth).delete_crud(session_ids=session_ids)
# ================================================= #
# ******************* AI 模型配置 ****************** #
# ================================================= #
_AI_MODEL_TTL = 604800 # AI 模型配置缓存 7 天,不活跃用户自动清理
def _ai_model_items_key(user_id: int) -> str:
return f"{RedisInitKeyConfig.AI_MODEL_CONFIG.key}:items:{user_id}"
def _ai_model_active_key(user_id: int) -> str:
return f"{RedisInitKeyConfig.AI_MODEL_CONFIG.key}:active:{user_id}"
async def get_user_model_config(redis: Redis, user_id: int) -> dict[str, Any] | None:
"""读取当前激活的 AI 模型配置;不存在或未激活返回 None。"""
active_id = await RedisCURD(redis).get(_ai_model_active_key(user_id))
if not active_id:
return None
items = await list_user_model_configs(redis, user_id)
for item in items:
if item.get("id") == active_id:
return item
return None
async def list_user_model_configs(redis: Redis, user_id: int) -> list[dict[str, Any]]:
"""列出用户的所有模型配置项。"""
raw = await RedisCURD(redis).get(_ai_model_items_key(user_id))
if not raw:
return []
try:
data = json.loads(raw)
if isinstance(data, list):
return data
return []
except (json.JSONDecodeError, TypeError):
logger.warning("AI 模型配置列表 JSON 解析失败: user_id={}", user_id)
return []
async def get_active_model_id(redis: Redis, user_id: int) -> str | None:
"""读取当前激活的模型配置 ID;为空表示使用系统默认。"""
return await RedisCURD(redis).get(_ai_model_active_key(user_id))
async def create_user_model_config(
redis: Redis,
user_id: int,
config: AiModelConfigSchema,
) -> dict[str, Any]:
"""新增一个模型配置项。"""
import uuid
from datetime import datetime
items = await list_user_model_configs(redis, user_id)
item = {
**config.model_dump(),
"id": uuid.uuid4().hex,
"created_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
}
items.append(item)
await RedisCURD(redis).set(
_ai_model_items_key(user_id),
json.dumps(items, ensure_ascii=False),
expire=_AI_MODEL_TTL,
)
# 若用户尚未激活任何配置,自动激活新增的
if not await get_active_model_id(redis, user_id):
await RedisCURD(redis).set(_ai_model_active_key(user_id), item["id"], expire=_AI_MODEL_TTL)
logger.info("已新增 AI 模型配置: user_id={} name={} id={}", user_id, config.name, item["id"])
return item
async def update_user_model_config(
redis: Redis,
user_id: int,
config_id: str,
config: AiModelConfigSchema,
) -> dict[str, Any] | None:
"""更新指定 ID 的模型配置项;不存在返回 None。"""
items = await list_user_model_configs(redis, user_id)
target = next((it for it in items if it.get("id") == config_id), None)
if not target:
return None
target.update(config.model_dump())
await RedisCURD(redis).set(
_ai_model_items_key(user_id),
json.dumps(items, ensure_ascii=False),
expire=_AI_MODEL_TTL,
)
logger.info("已更新 AI 模型配置: user_id={} id={}", user_id, config_id)
return target
async def delete_user_model_config(redis: Redis, user_id: int, config_id: str) -> bool:
"""删除指定 ID 的模型配置项;若该 ID 是当前激活则清空激活。"""
items = await list_user_model_configs(redis, user_id)
new_items = [it for it in items if it.get("id") != config_id]
if len(new_items) == len(items):
return False
await RedisCURD(redis).set(
_ai_model_items_key(user_id),
json.dumps(new_items, ensure_ascii=False),
expire=_AI_MODEL_TTL,
)
active_id = await get_active_model_id(redis, user_id)
if active_id == config_id:
await RedisCURD(redis).delete(_ai_model_active_key(user_id))
logger.info("已删除 AI 模型配置: user_id={} id={}", user_id, config_id)
return True
async def set_active_model_config(redis: Redis, user_id: int, config_id: str) -> bool:
"""设置当前激活的模型配置项;id 为空字符串或 "__default__" 表示使用系统默认。"""
if config_id in ("", "__default__"):
await RedisCURD(redis).delete(_ai_model_active_key(user_id))
logger.info("已切换到系统默认模型: user_id={}", user_id)
return True
items = await list_user_model_configs(redis, user_id)
if not any(it.get("id") == config_id for it in items):
return False
await RedisCURD(redis).set(_ai_model_active_key(user_id), config_id, expire=_AI_MODEL_TTL)
logger.info("已切换 AI 模型: user_id={} id={}", user_id, config_id)
return True
class AiModelConfigService:
"""AI 模型配置业务服务(多配置 + 激活切换)"""
def __init__(self, auth: AuthSchema, redis: Redis) -> None:
self.auth = auth
self.redis = redis
@property
def _user_id(self) -> int:
return self.auth.user.id
async def list(self) -> dict[str, Any]:
"""获取配置列表 + 当前激活 ID。"""
items = await list_user_model_configs(self.redis, self._user_id)
active_id = await get_active_model_id(self.redis, self._user_id)
return {"items": items, "active_id": active_id}
async def get_active(self) -> dict[str, Any] | None:
return await get_user_model_config(self.redis, self._user_id)
async def create(self, config: AiModelConfigSchema) -> dict[str, Any]:
return await create_user_model_config(self.redis, self._user_id, config)
async def update(self, config_id: str, config: AiModelConfigSchema) -> dict[str, Any] | None:
result = await update_user_model_config(self.redis, self._user_id, config_id, config)
if result is None:
raise CustomException(msg="模型配置不存在", code=10404, status_code=404)
return result
async def delete(self, config_id: str) -> None:
ok = await delete_user_model_config(self.redis, self._user_id, config_id)
if not ok:
raise CustomException(msg="模型配置不存在", code=10404, status_code=404)
async def set_active(self, config_id: str) -> None:
ok = await set_active_model_config(self.redis, self._user_id, config_id)
if not ok:
raise CustomException(msg="模型配置不存在", code=10404, status_code=404)