You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

517 lines
20 KiB
Python

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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 app.api.v1.module_system.dept.service import DeptService
from app.common.enums import RedisInitKeyConfig
from app.common.request import PaginationService
from app.core.base_schema import AuthSchema
from app.core.exceptions import CustomException
from app.core.logger import logger
from app.core.redis_crud import RedisCURD
from .crud import ChatSessionCRUD
from .schema import (
AiModelConfigSchema,
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(auth).detail(id=int(team_id))
result["team_name"] = dept_name.get("name")
elif isinstance(team_id, int):
dept_name = await DeptService(auth).detail(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")
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]:
"""流式 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()
dept_id = str(self.auth.user.dept_id) if self.auth and self.auth.user and hasattr(self.auth.user, "dept_id") and self.auth.user.dept_id else "default"
agent = agno_factory.create_agent(
user_id=self.auth.user.username if self.auth and self.auth.user else "user",
dept_id=dept_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:
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 chunk.content
else:
logger.debug("空 chunk 跳过: {}", type(chunk).__name__ if chunk else None)
else:
# 兼容非流式直接返回结果的场景
logger.warning("agent.arun 未返回异步迭代器,尝试按单次结果处理")
if stream and getattr(stream, "content", None):
chunk_count += 1
yield stream.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"抱歉,处理您的请求时出现错误:{str(e)}"
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()
dept_id = str(self.auth.user.dept_id) if self.auth and self.auth.user and hasattr(self.auth.user, "dept_id") and self.auth.user.dept_id else "default"
agent: Team = agno_factory.create_agent(
user_id=self.auth.user.username if self.auth and self.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
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"抱歉,处理您的请求时出现错误:{str(e)}",
"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,
) -> dict[str, 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 模型配置 ****************** #
# ================================================= #
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),
)
# 若用户尚未激活任何配置,自动激活新增的
if not await get_active_model_id(redis, user_id):
await RedisCURD(redis).set(_ai_model_active_key(user_id), item["id"])
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),
)
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),
)
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)
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:
if not self.auth or not self.auth.user:
raise CustomException(msg="未登录", code=10401, status_code=401)
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)