|
|
|
|
|
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)
|