Files
FastapiAdmin/backend/app/plugin/module_ai/chat/service.py
T
zhangtao e03692467b feat(ai): 重构AI聊天模块,使用agno框架替代langchain
- 移除langchain相关依赖,引入agno作为大模型开发框架
- 重构聊天会话和消息存储,使用agno的TeamSession管理
- 合并chat_message和chat_session模块为统一的chat模块
- 更新前端API和类型定义,适配新的后端接口
- 优化数据库日志字段类型,根据数据库类型自动选择
- 重构AI助手组件,使用新的聊天API
- 清理不再使用的模型、控制器和服务代码
2026-03-13 00:41:44 +08:00

358 lines
14 KiB
Python

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 app.api.v1.module_system.auth.schema import AuthSchema
from app.api.v1.module_system.dept.service import DeptService
from app.common.request import PaginationService
from app.core.exceptions import CustomException
from app.core.logger import log
from .crud import ChatSessionCRUD
from .schema import (
ChatQuerySchema,
ChatSessionCreateSchema,
ChatSessionQueryParam,
ChatSessionUpdateSchema,
)
from .utils import AgnoFactory
async def _format_session_data(session: TeamSession, auth: AuthSchema | 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,查询部门名称
if auth and session_dict.get("team_id"):
try:
team_id = session_dict.get("team_id")
if isinstance(team_id, str):
dept_name = await DeptService.get_dept_detail_service(auth=auth, id=int(team_id))
result["team_name"] = dept_name.get("name")
elif isinstance(team_id, int):
dept_name = await DeptService.get_dept_detail_service(auth=auth, id=team_id)
result["team_name"] = dept_name.get("name")
else:
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") or 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:
"""聊天会话管理模块服务层"""
@classmethod
async def chat_query(
cls, query: ChatQuerySchema, auth: AuthSchema
) -> AsyncGenerator[str, None]:
"""处理聊天查询并返回流式响应"""
try:
# 创建 CRUD 实例获取数据库连接
crud = ChatSessionCRUD(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
# 创建 AgnoFactory 实例并创建 Team,传入数据库连接
agno_factory = AgnoFactory()
dept_id = str(auth.user.dept_id) if auth and auth.user and hasattr(auth.user, 'dept_id') and auth.user.dept_id else "default"
agent = agno_factory.create_agent(
user_id=auth.user.username if auth and auth.user else "user",
dept_id=dept_id,
session_id=session_id,
db=crud.db
)
# 执行聊天查询 - 使用流式输出
async for chunk in agent.arun(input=query.message, stream=True):
if chunk and chunk.content:
yield chunk.content
except Exception as e:
log.error(f"聊天查询失败: {e}")
yield f"抱歉,处理您的请求时出现错误:{str(e)}"
@classmethod
async def chat_non_stream(
cls, message: str, session_id: str | None, auth: AuthSchema
) -> dict[str, Any]:
"""处理聊天查询并返回非流式响应"""
try:
# 创建 CRUD 实例获取数据库连接
crud = ChatSessionCRUD(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
# 创建 AgnoFactory 实例并创建 Team,传入数据库连接
agno_factory = AgnoFactory()
dept_id = str(auth.user.dept_id) if auth and auth.user and hasattr(auth.user, 'dept_id') and auth.user.dept_id else "default"
agent: Team = agno_factory.create_agent(
user_id=auth.user.username if auth and auth.user else "user",
dept_id=dept_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
# 尝试从 response 中解析操作建议
# 如果 AI 返回了 JSON 格式的操作建议
import json
try:
# 检查响应是否包含 JSON 格式的操作建议
if response_text.strip().startswith('{') and response_text.strip().endswith('}'):
action = json.loads(response_text)
elif '```json' in response_text:
# 提取 JSON 代码块
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
# 如果没有解析到 JSON,尝试从文本中提取操作信息
if not action:
action = cls._parse_action_from_response(response_text)
return {
"response": response_text,
"session_id": session_id,
"function_calls": None,
"action": action,
}
except Exception as e:
log.error(f"聊天查询失败: {e}")
return {
"response": f"抱歉,处理您的请求时出现错误:{str(e)}",
"session_id": session_id if 'session_id' in locals() else None,
"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
@classmethod
async def create_service(
cls, auth: AuthSchema, data: ChatSessionCreateSchema
) -> dict[str, Any] | None:
"""创建会话"""
crud = ChatSessionCRUD(auth)
session = await crud.create_crud(data=data)
if session:
return await _format_session_data(session, auth)
return None
@classmethod
async def get_session_service(
cls, auth: AuthSchema, session_id: str
) -> dict[str, Any] | None:
"""获取单个会话详情"""
crud = ChatSessionCRUD(auth)
session: TeamSession | None = await crud.get_by_id_crud(session_id=session_id)
if session:
return await _format_session_data(session, auth)
return None
@classmethod
async def page_service(
cls,
auth: AuthSchema,
page_no: int,
page_size: int,
search: ChatSessionQueryParam,
order_by: list[dict[str, str]] | None = None,
) -> dict[str, Any]:
"""分页获取会话列表 - 使用内存分页"""
crud = ChatSessionCRUD(auth)
# 获取所有会话
sessions = await crud.list_crud()
# 转换为响应模型 - 使用 TeamSession 内置的 to_dict 方法并格式化
items = [await _format_session_data(s, auth) for s in sessions]
# 使用 PaginationService 进行内存分页
result = await PaginationService.paginate(
data_list=items,
page_no=page_no,
page_size=page_size,
)
return result
@classmethod
async def update_service(
cls, auth: AuthSchema, session_id: str, data: ChatSessionUpdateSchema
) -> bool:
"""更新会话"""
crud = ChatSessionCRUD(auth)
success = await crud.update_crud(session_id=session_id, data=data)
return success
@classmethod
async def delete_service(cls, auth: AuthSchema, session_ids: list[str]) -> None:
"""删除会话"""
await ChatSessionCRUD(auth).delete_crud(session_ids=session_ids)