mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-20 20:39:55 +00:00
refactor(backend): 重构项目结构,将模块代码移动到plugin目录
feat(backend): 添加数据库表创建和删除工具函数 fix(backend): 修复日志输出和Redis连接提示信息 style(frontend): 调整导航栏头像大小 perf(backend): 优化系统配置和字典数据缓存逻辑 chore(backend): 更新模板文件路径配置
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from fastapi import APIRouter, Depends, Path, Body, WebSocket
|
||||
from fastapi.responses import JSONResponse, StreamingResponse
|
||||
|
||||
from app.common.response import StreamResponse, SuccessResponse
|
||||
from app.common.request import PaginationService
|
||||
from app.core.base_params import PaginationQueryParam
|
||||
from app.core.dependencies import AuthPermission
|
||||
from app.core.logger import log
|
||||
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from app.core.router_class import OperationLogRoute
|
||||
from .service import McpService
|
||||
from .schema import McpCreateSchema, McpUpdateSchema, ChatQuerySchema, McpQueryParam
|
||||
|
||||
|
||||
AIRouter = APIRouter(route_class=OperationLogRoute, prefix="/ai", tags=["MCP智能助手"])
|
||||
|
||||
|
||||
@AIRouter.post("/chat", summary="智能对话", description="与MCP智能助手进行对话")
|
||||
async def chat_controller(
|
||||
query: ChatQuerySchema,
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_application:ai:chat"]))
|
||||
) -> StreamingResponse:
|
||||
"""
|
||||
智能对话接口
|
||||
|
||||
参数:
|
||||
- query (ChatQuerySchema): 聊天查询模型
|
||||
|
||||
返回:
|
||||
- StreamingResponse: 流式响应,每次返回一个聊天响应
|
||||
"""
|
||||
user_name = auth.user.name if auth.user else "未知用户"
|
||||
log.info(f"用户 {user_name} 发起智能对话: {query.message[:50]}...")
|
||||
|
||||
async def generate_response():
|
||||
try:
|
||||
async for chunk in McpService.chat_query(query=query):
|
||||
# 确保返回的是字节串
|
||||
if chunk:
|
||||
yield chunk.encode('utf-8') if isinstance(chunk, str) else chunk
|
||||
except Exception as e:
|
||||
log.error(f"流式响应出错: {str(e)}")
|
||||
yield f"抱歉,处理您的请求时出现了错误: {str(e)}".encode('utf-8')
|
||||
|
||||
return StreamResponse(generate_response(), media_type="text/plain; charset=utf-8")
|
||||
|
||||
|
||||
@AIRouter.get("/detail/{id}", summary="获取 MCP 服务器详情", description="获取 MCP 服务器详情")
|
||||
async def detail_controller(
|
||||
id: int = Path(..., description="MCP ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_application:ai:query"]))
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
获取 MCP 服务器详情接口
|
||||
|
||||
参数:
|
||||
- id (int): MCP 服务器ID
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含 MCP 服务器详情的 JSON 响应
|
||||
"""
|
||||
result_dict = await McpService.detail_service(auth=auth, id=id)
|
||||
log.info(f"获取 MCP 服务器详情成功 {id}")
|
||||
return SuccessResponse(data=result_dict, msg="获取 MCP 服务器详情成功")
|
||||
|
||||
|
||||
@AIRouter.get("/list", summary="查询 MCP 服务器列表", description="查询 MCP 服务器列表")
|
||||
async def list_controller(
|
||||
page: PaginationQueryParam = Depends(),
|
||||
search: McpQueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_application:ai:query"]))
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
查询 MCP 服务器列表接口
|
||||
|
||||
参数:
|
||||
- page (PaginationQueryParam): 分页查询参数模型
|
||||
- search (McpQueryParam): 查询参数模型
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含 MCP 服务器列表的 JSON 响应
|
||||
"""
|
||||
result_dict_list = await McpService.list_service(auth=auth, search=search, order_by=page.order_by)
|
||||
result_dict = await PaginationService.paginate(data_list=result_dict_list, page_no=page.page_no, page_size=page.page_size)
|
||||
log.info(f"查询 MCP 服务器列表成功")
|
||||
return SuccessResponse(data=result_dict, msg="查询 MCP 服务器列表成功")
|
||||
|
||||
|
||||
@AIRouter.post("/create", summary="创建 MCP 服务器", description="创建 MCP 服务器")
|
||||
async def create_controller(
|
||||
data: McpCreateSchema,
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_application:ai:create"]))
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
创建 MCP 服务器接口
|
||||
|
||||
参数:
|
||||
- data (McpCreateSchema): 创建 MCP 服务器模型
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含创建 MCP 服务器结果的 JSON 响应
|
||||
"""
|
||||
result_dict = await McpService.create_service(auth=auth, data=data)
|
||||
log.info(f"创建 MCP 服务器成功: {result_dict}")
|
||||
return SuccessResponse(data=result_dict, msg="创建 MCP 服务器成功")
|
||||
|
||||
|
||||
@AIRouter.put("/update/{id}", summary="修改 MCP 服务器", description="修改 MCP 服务器")
|
||||
async def update_controller(
|
||||
data: McpUpdateSchema,
|
||||
id: int = Path(..., description="MCP ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_application:ai:update"]))
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
修改 MCP 服务器接口
|
||||
|
||||
参数:
|
||||
- data (McpUpdateSchema): 修改 MCP 服务器模型
|
||||
- id (int): MCP 服务器ID
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含修改 MCP 服务器结果的 JSON 响应
|
||||
"""
|
||||
result_dict = await McpService.update_service(auth=auth, id=id, data=data)
|
||||
log.info(f"修改 MCP 服务器成功: {result_dict}")
|
||||
return SuccessResponse(data=result_dict, msg="修改 MCP 服务器成功")
|
||||
|
||||
|
||||
@AIRouter.delete("/delete", summary="删除 MCP 服务器", description="删除 MCP 服务器")
|
||||
async def delete_controller(
|
||||
ids: list[int] = Body(..., description="ID列表"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_application:ai:delete"]))
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
删除 MCP 服务器接口
|
||||
|
||||
参数:
|
||||
- ids (list[int]): MCP 服务器ID列表
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含删除 MCP 服务器结果的 JSON 响应
|
||||
"""
|
||||
await McpService.delete_service(auth=auth, ids=ids)
|
||||
log.info(f"删除 MCP 服务器成功: {ids}")
|
||||
return SuccessResponse(msg="删除 MCP 服务器成功")
|
||||
@@ -0,0 +1,100 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import Sequence, Any
|
||||
|
||||
from app.core.base_crud import CRUDBase
|
||||
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from .model import McpModel
|
||||
from .schema import McpCreateSchema, McpUpdateSchema
|
||||
|
||||
|
||||
class McpCRUD(CRUDBase[McpModel, McpCreateSchema, McpUpdateSchema]):
|
||||
"""MCP 服务器数据层"""
|
||||
|
||||
def __init__(self, auth: AuthSchema) -> None:
|
||||
"""
|
||||
初始化CRUD
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
"""
|
||||
self.auth = auth
|
||||
super().__init__(model=McpModel, auth=auth)
|
||||
|
||||
async def get_by_id_crud(self, id: int, preload: list[str | Any] | None = None) -> McpModel | None:
|
||||
"""
|
||||
获取MCP服务器详情
|
||||
|
||||
参数:
|
||||
- id (int): MCP服务器ID
|
||||
- preload (list[str | Any] | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- McpModel | None: MCP服务器模型实例(如果存在)
|
||||
"""
|
||||
return await self.get(id=id, preload=preload)
|
||||
|
||||
async def get_by_name_crud(self, name: str, preload: list[str | Any] | None = None) -> McpModel | None:
|
||||
"""
|
||||
通过名称获取MCP服务器
|
||||
|
||||
参数:
|
||||
- name (str): MCP服务器名称
|
||||
- preload (list[str | Any] | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- Optional[McpModel]: MCP服务器模型实例(如果存在)
|
||||
"""
|
||||
return await self.get(name=name, preload=preload)
|
||||
|
||||
async def get_list_crud(self, search: dict | None = None, order_by: list[dict[str, str]] | None = None, preload: list[str | Any] | None = None) -> Sequence[McpModel]:
|
||||
"""
|
||||
列表查询MCP服务器
|
||||
|
||||
参数:
|
||||
- search (dict | None): 查询参数字典
|
||||
- order_by (list[dict[str, str]] | None): 排序参数列表
|
||||
- preload (list[str | Any] | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- Sequence[McpModel]: MCP服务器模型实例序列
|
||||
"""
|
||||
return await self.list(search=search or {}, order_by=order_by or [{'id': 'asc'}], preload=preload)
|
||||
|
||||
async def create_crud(self, data: McpCreateSchema) -> McpModel | None:
|
||||
"""
|
||||
创建MCP服务器
|
||||
|
||||
参数:
|
||||
- data (McpCreateSchema): 创建MCP服务器模型
|
||||
|
||||
返回:
|
||||
- Optional[McpModel]: 创建的MCP服务器模型实例(如果成功)
|
||||
"""
|
||||
return await self.create(data=data)
|
||||
|
||||
async def update_crud(self, id: int, data: McpUpdateSchema) -> McpModel | None:
|
||||
"""
|
||||
更新MCP服务器
|
||||
|
||||
参数:
|
||||
- id (int): MCP服务器ID
|
||||
- data (McpUpdateSchema): 更新MCP服务器模型
|
||||
|
||||
返回:
|
||||
- McpModel | None: 更新的MCP服务器模型实例(如果成功)
|
||||
"""
|
||||
return await self.update(id=id, data=data)
|
||||
|
||||
async def delete_crud(self, ids: list[int]) -> None:
|
||||
"""
|
||||
批量删除MCP服务器
|
||||
|
||||
参数:
|
||||
- ids (list[int]): MCP服务器ID列表
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
return await self.delete(ids=ids)
|
||||
@@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from sqlalchemy import JSON, String, Integer
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.base_model import ModelMixin, UserMixin
|
||||
|
||||
|
||||
class McpModel(ModelMixin, UserMixin):
|
||||
"""
|
||||
MCP 服务器表
|
||||
MCP类型:
|
||||
- 0: stdio (标准输入输出)
|
||||
- 1: sse (Server-Sent Events)
|
||||
"""
|
||||
__tablename__: str = 'app_ai_mcp'
|
||||
__table_args__: dict[str, str] = ({'comment': 'MCP 服务器表'})
|
||||
__loader_options__: list[str] = ["created_by", "updated_by"]
|
||||
|
||||
name: Mapped[str] = mapped_column(String(50), comment='MCP 名称')
|
||||
type: Mapped[int] = mapped_column(Integer, default=0, comment='MCP 类型(0:stdio 1:sse)')
|
||||
url: Mapped[str | None] = mapped_column(String(255), default=None, comment='远程 SSE 地址')
|
||||
command: Mapped[str | None] = mapped_column(String(255), default=None, comment='MCP 命令')
|
||||
args: Mapped[str | None] = mapped_column(String(255), default=None, comment='MCP 命令参数')
|
||||
env: Mapped[dict[str, str] | None] = mapped_column(JSON(), default=None, comment='MCP 环境变量')
|
||||
@@ -0,0 +1,73 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from pydantic import ConfigDict, Field, HttpUrl, BaseModel
|
||||
from fastapi import Query
|
||||
|
||||
from app.core.base_schema import BaseSchema
|
||||
from app.common.enums import McpLLMProvider
|
||||
from app.core.base_schema import BaseSchema, UserBySchema
|
||||
from app.common.enums import McpType
|
||||
from app.core.validator import DateTimeStr
|
||||
|
||||
|
||||
class ChatQuerySchema(BaseModel):
|
||||
"""聊天查询模型"""
|
||||
message: str = Field(..., min_length=1, max_length=4000, description="聊天消息")
|
||||
|
||||
|
||||
class McpCreateSchema(BaseModel):
|
||||
"""创建 MCP 服务器参数"""
|
||||
name: str = Field(..., max_length=64, description='MCP 名称')
|
||||
type: McpType = Field(McpType.stdio, description='MCP 类型')
|
||||
description: str | None = Field(None, max_length=255, description='MCP 描述')
|
||||
url: HttpUrl | None = Field(None, description='远程 SSE 地址')
|
||||
command: str | None = Field(None, max_length=255, description='MCP 命令')
|
||||
args: str | None = Field(None, max_length=255, description='MCP 命令参数,多个参数用英文逗号隔开')
|
||||
env: dict[str, str] | None = Field(None, description='MCP 环境变量')
|
||||
|
||||
|
||||
class McpUpdateSchema(McpCreateSchema):
|
||||
"""更新 MCP 服务器参数"""
|
||||
...
|
||||
|
||||
|
||||
class McpOutSchema(McpCreateSchema, BaseSchema, UserBySchema):
|
||||
"""MCP 服务器详情"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class McpQueryParam:
|
||||
"""MCP 服务器查询参数"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str | None = Query(None, description="MCP 名称"),
|
||||
type: McpType | None = Query(None, description="MCP 类型"),
|
||||
created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
created_id: int | None = Query(None, description="创建人"),
|
||||
updated_id: int | None = Query(None, description="更新人"),
|
||||
) -> None:
|
||||
# 模糊查询字段
|
||||
self.name = ("like", name) if name else None
|
||||
|
||||
# 精确查询字段
|
||||
self.type = type
|
||||
self.created_id = created_id
|
||||
self.updated_id = updated_id
|
||||
|
||||
# 时间范围查询
|
||||
if created_time and len(created_time) == 2:
|
||||
self.created_time = ("between", (created_time[0], created_time[1]))
|
||||
if updated_time and len(updated_time) == 2:
|
||||
self.updated_time = ("between", (updated_time[0], updated_time[1]))
|
||||
|
||||
|
||||
class McpChatParam(BaseSchema):
|
||||
"""MCP 聊天参数"""
|
||||
pk: list[int] = Field(..., description='MCP ID 列表')
|
||||
provider: McpLLMProvider = Field(McpLLMProvider.openai, description='LLM 供应商')
|
||||
model: str = Field(..., description='LLM 名称')
|
||||
key: str = Field(..., description='LLM API Key')
|
||||
base_url: str | None = Field(None, description='自定义 LLM API 地址,必须兼容 openai 供应商')
|
||||
prompt: str = Field(..., description='用户提示词')
|
||||
@@ -0,0 +1,132 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import Any, AsyncGenerator
|
||||
|
||||
from app.core.exceptions import CustomException
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from app.core.logger import log
|
||||
from .tools.ai_util import AIClient
|
||||
from .schema import McpCreateSchema, McpUpdateSchema, McpOutSchema, ChatQuerySchema, McpQueryParam
|
||||
from .crud import McpCRUD
|
||||
|
||||
|
||||
class McpService:
|
||||
"""MCP服务层"""
|
||||
|
||||
@classmethod
|
||||
async def detail_service(cls, auth: AuthSchema, id: int) -> dict[str, Any]:
|
||||
"""
|
||||
获取MCP服务器详情
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- id (int): MCP服务器ID
|
||||
|
||||
返回:
|
||||
- dict[str, Any]: MCP服务器详情字典
|
||||
"""
|
||||
obj = await McpCRUD(auth).get_by_id_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg='MCP 服务器不存在')
|
||||
return McpOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def list_service(cls, auth: AuthSchema, search: McpQueryParam | None = None, order_by: list[dict[str, str]] | None = None) -> list[dict[str, Any]]:
|
||||
"""
|
||||
列表查询MCP服务器
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- search (McpQueryParam | None): 查询参数模型
|
||||
- order_by (list[dict[str, str]] | None): 排序参数列表
|
||||
|
||||
返回:
|
||||
- list[dict[str, Any]]: MCP服务器详情字典列表
|
||||
"""
|
||||
search_dict = search.__dict__ if search else None
|
||||
obj_list = await McpCRUD(auth).get_list_crud(search=search_dict, order_by=order_by)
|
||||
return [McpOutSchema.model_validate(obj).model_dump() for obj in obj_list]
|
||||
|
||||
@classmethod
|
||||
async def create_service(cls, auth: AuthSchema, data: McpCreateSchema) -> dict[str, Any]:
|
||||
"""
|
||||
创建MCP服务器
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- data (McpCreateSchema): 创建MCP服务器模型
|
||||
|
||||
返回:
|
||||
- dict[str, Any]: 创建的MCP服务器详情字典
|
||||
"""
|
||||
obj = await McpCRUD(auth).get_by_name_crud(name=data.name)
|
||||
if obj:
|
||||
raise CustomException(msg='创建失败,MCP 服务器已存在')
|
||||
obj = await McpCRUD(auth).create_crud(data=data)
|
||||
return McpOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def update_service(cls, auth: AuthSchema, id: int, data: McpUpdateSchema) -> dict[str, Any]:
|
||||
"""
|
||||
更新MCP服务器
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- id (int): MCP服务器ID
|
||||
- data (McpUpdateSchema): 更新MCP服务器模型
|
||||
|
||||
返回:
|
||||
- dict[str, Any]: 更新的MCP服务器详情字典
|
||||
"""
|
||||
obj = await McpCRUD(auth).get_by_id_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg='更新失败,该数据不存在')
|
||||
exist_obj = await McpCRUD(auth).get_by_name_crud(name=data.name)
|
||||
if exist_obj and exist_obj.id != id:
|
||||
raise CustomException(msg='更新失败,MCP 服务器名称重复')
|
||||
obj = await McpCRUD(auth).update_crud(id=id, data=data)
|
||||
return McpOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def delete_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
||||
"""
|
||||
批量删除MCP服务器
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- ids (list[int]): MCP服务器ID列表
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
if len(ids) < 1:
|
||||
raise CustomException(msg='删除失败,删除对象不能为空')
|
||||
for id in ids:
|
||||
obj = await McpCRUD(auth).get_by_id_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg='删除失败,该数据不存在')
|
||||
await McpCRUD(auth).delete_crud(ids=ids)
|
||||
|
||||
@classmethod
|
||||
async def chat_query(cls, query: ChatQuerySchema) -> AsyncGenerator[str, Any]:
|
||||
"""
|
||||
处理聊天查询
|
||||
|
||||
参数:
|
||||
- query (ChatQuerySchema): 聊天查询模型
|
||||
|
||||
返回:
|
||||
- AsyncGenerator[str, None]: 异步生成器,每次返回一个聊天响应
|
||||
"""
|
||||
# 创建MCP客户端实例
|
||||
mcp_client = AIClient()
|
||||
try:
|
||||
# 处理消息
|
||||
async for response in mcp_client.process(query.message):
|
||||
yield response
|
||||
finally:
|
||||
# 确保关闭客户端连接,即使在事件循环关闭时也能安全处理
|
||||
try:
|
||||
await mcp_client.close()
|
||||
except Exception as e:
|
||||
log.debug(f"关闭AIClient时发生异常(预期行为,服务可能正在关闭): {str(e)}")
|
||||
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Callable
|
||||
from langchain.agents import AgentState, create_agent
|
||||
from langchain.agents.middleware import ModelRequest, ModelResponse, after_model, before_model, dynamic_prompt, wrap_model_call
|
||||
from langchain.messages import AIMessage, HumanMessage, RemoveMessage, SystemMessage
|
||||
from langchain.tools import tool, ToolRuntime
|
||||
from langchain.chat_models import init_chat_model
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
from langchain.agents.structured_output import MultipleStructuredOutputsError, StructuredOutputValidationError, ToolStrategy
|
||||
from langgraph.graph.message import REMOVE_ALL_MESSAGES
|
||||
from langgraph.runtime import Runtime
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
# =================定义提示词=================
|
||||
SYSTEM_PROMPT = """You are an expert weather forecaster, who speaks in puns.
|
||||
|
||||
You have access to two tools:
|
||||
|
||||
- get_weather_for_location: use this to get the weather for a specific location
|
||||
- get_user_location: use this to get the user's location
|
||||
|
||||
If a user asks you for the weather, make sure you know the location. If you can tell from the question that they mean wherever they are, use the get_user_location tool to find their location.
|
||||
"""
|
||||
|
||||
# =================定义工具=================
|
||||
@tool
|
||||
def get_weather_for_location(city: str) -> str:
|
||||
"""Get weather for a given city."""
|
||||
return f"It's always sunny in {city}!"
|
||||
|
||||
@dataclass
|
||||
class Context:
|
||||
"""Custom runtime context schema."""
|
||||
user_id: str
|
||||
|
||||
@tool
|
||||
def get_user_location(runtime: ToolRuntime[Context]) -> str:
|
||||
"""Retrieve user information based on user ID."""
|
||||
user_id = runtime.context.user_id
|
||||
return "Florida" if user_id == "1" else "SF"
|
||||
|
||||
|
||||
# =================定义模型=================
|
||||
model = init_chat_model(
|
||||
"claude-sonnet-4-5-20250929",
|
||||
temperature=0.5,
|
||||
timeout=10,
|
||||
max_tokens=1000
|
||||
)
|
||||
|
||||
|
||||
# =================定义响应模型=================
|
||||
class ResponseFormat(BaseModel):
|
||||
"""Response schema for the agent."""
|
||||
rating: int | None = Field(description="Rating from 1-5", ge=1, le=5)
|
||||
comment: str = Field(description="Review comment")
|
||||
punny_response: str
|
||||
weather_conditions: str | None = None
|
||||
|
||||
# =================定义存储记忆=================
|
||||
checkpointer = InMemorySaver()
|
||||
|
||||
# =================定义动态提示词=================
|
||||
@dynamic_prompt
|
||||
def dynamic_system_prompt(request: ModelRequest) -> str:
|
||||
user_name = getattr(request.runtime.context, "user_name", "User")
|
||||
system_prompt = f"You are a helpful assistant. Address the user as {user_name}."
|
||||
return system_prompt
|
||||
|
||||
@before_model
|
||||
def trim_messages(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
|
||||
"""Keep only the last few messages to fit context window."""
|
||||
messages = state["messages"]
|
||||
|
||||
if len(messages) <= 3:
|
||||
return None # No changes needed
|
||||
|
||||
first_msg = messages[0]
|
||||
recent_messages = messages[-3:] if len(messages) % 2 == 0 else messages[-4:]
|
||||
new_messages = [first_msg] + recent_messages
|
||||
|
||||
return {
|
||||
"messages": [
|
||||
RemoveMessage(id=REMOVE_ALL_MESSAGES),
|
||||
*new_messages
|
||||
]
|
||||
}
|
||||
|
||||
@after_model
|
||||
def validate_response(state: AgentState, runtime: Runtime) -> dict | None:
|
||||
"""Remove messages containing sensitive words."""
|
||||
STOP_WORDS = ["password", "secret"]
|
||||
last_message = state["messages"][-1]
|
||||
if any(word in last_message.content for word in STOP_WORDS):
|
||||
return {"messages": [RemoveMessage(id=last_message.id or "")]}
|
||||
return None
|
||||
|
||||
@wrap_model_call
|
||||
def inject_file_context(
|
||||
request: ModelRequest,
|
||||
handler: Callable[[ModelRequest], ModelResponse]
|
||||
) -> ModelResponse:
|
||||
"""Inject context about files user has uploaded this session."""
|
||||
# Read from State: get uploaded files metadata
|
||||
uploaded_files = request.state.get("uploaded_files", [])
|
||||
|
||||
if uploaded_files:
|
||||
# Build context about available files
|
||||
file_descriptions = []
|
||||
for file in uploaded_files:
|
||||
file_descriptions.append(
|
||||
f"- {file['name']} ({file['type']}): {file['summary']}"
|
||||
)
|
||||
|
||||
file_context = f"""Files you have access to in this conversation:
|
||||
{chr(10).join(file_descriptions)}
|
||||
|
||||
Reference these files when answering questions."""
|
||||
|
||||
# Inject file context before recent messages
|
||||
messages = [
|
||||
*request.messages,
|
||||
{"role": "user", "content": file_context},
|
||||
]
|
||||
request = request.override(messages=messages)
|
||||
|
||||
|
||||
def custom_error_handler(error: Exception) -> str:
|
||||
if isinstance(error, StructuredOutputValidationError):
|
||||
return "There was an issue with the format. Try again."
|
||||
elif isinstance(error, MultipleStructuredOutputsError):
|
||||
return "Multiple structured outputs were returned. Pick the most relevant one."
|
||||
else:
|
||||
return f"Error: {str(error)}"
|
||||
|
||||
# =================定义智能体=================
|
||||
agent = create_agent(
|
||||
model=model,
|
||||
system_prompt=SYSTEM_PROMPT,
|
||||
tools=[get_user_location, get_weather_for_location],
|
||||
middleware=[dynamic_system_prompt, trim_messages, validate_response, inject_file_context],
|
||||
context_schema=Context,
|
||||
response_format=ToolStrategy(schema=ResponseFormat,handle_errors=(ValueError, TypeError, custom_error_handler)),
|
||||
checkpointer=checkpointer
|
||||
)
|
||||
|
||||
# =================定义线程=================
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
|
||||
messages = [
|
||||
SystemMessage("You are a poetry expert"),
|
||||
HumanMessage("Write a haiku about spring"),
|
||||
AIMessage("Cherry blossoms bloom...")
|
||||
]
|
||||
|
||||
# =================运行智能体=================
|
||||
response = agent.invoke(
|
||||
input=messages,
|
||||
config=config,
|
||||
context=Context(user_id="1")
|
||||
)
|
||||
|
||||
# =================解析响应=================
|
||||
print(response['structured_response'])
|
||||
# ResponseFormat(
|
||||
# punny_response="Florida is still having a 'sun-derful' day! The sunshine is playing 'ray-dio' hits all day long! I'd say it's the perfect weather for some 'solar-bration'! If you were hoping for rain, I'm afraid that idea is all 'washed up' - the forecast remains 'clear-ly' brilliant!",
|
||||
# weather_conditions="It's always sunny in Florida!"
|
||||
# )
|
||||
@@ -0,0 +1,96 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import Any, AsyncGenerator
|
||||
from langchain_openai import ChatOpenAI
|
||||
from langchain_core.messages import SystemMessage, HumanMessage
|
||||
|
||||
from app.config.setting import settings
|
||||
from app.core.logger import log
|
||||
|
||||
|
||||
class AIClient:
|
||||
"""
|
||||
AI客户端类,用于与OpenAI API交互。
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
# 使用LangChain的ChatOpenAI类
|
||||
self.model = ChatOpenAI(
|
||||
api_key=lambda: settings.OPENAI_API_KEY,
|
||||
model=settings.OPENAI_MODEL,
|
||||
base_url=settings.OPENAI_BASE_URL,
|
||||
temperature=0.7,
|
||||
streaming=True
|
||||
)
|
||||
|
||||
async def process(self, query: str) -> AsyncGenerator[str, Any]:
|
||||
"""
|
||||
处理查询并返回流式响应
|
||||
|
||||
参数:
|
||||
- query (str): 用户查询。
|
||||
|
||||
返回:
|
||||
- AsyncGenerator[str, Any]: 流式响应内容。
|
||||
"""
|
||||
system_prompt = """你是一个有用的AI助手,可以帮助用户回答问题和提供帮助。请用中文回答用户的问题。"""
|
||||
|
||||
try:
|
||||
# 使用LangChain的异步流式生成
|
||||
messages = [
|
||||
SystemMessage(content=system_prompt),
|
||||
HumanMessage(content=query)
|
||||
]
|
||||
|
||||
# 使用LangChain的流式响应
|
||||
async for chunk in self.model.astream(messages):
|
||||
yield chunk.text
|
||||
|
||||
except Exception as e:
|
||||
# 记录详细错误,返回友好提示
|
||||
log.error(f"AI处理查询失败: {str(e)}")
|
||||
yield self._friendly_error_message(e)
|
||||
|
||||
def _friendly_error_message(self, e: Exception) -> str:
|
||||
"""将 OpenAI 或网络异常转换为友好的中文提示。"""
|
||||
# 尝试获取状态码与错误体
|
||||
status_code = getattr(e, "status_code", None)
|
||||
body = getattr(e, "body", None)
|
||||
message = None
|
||||
error_type = None
|
||||
error_code = None
|
||||
try:
|
||||
if isinstance(body, dict) and "error" in body:
|
||||
err = body.get("error") or {}
|
||||
error_type = err.get("type")
|
||||
error_code = err.get("code")
|
||||
message = err.get("message")
|
||||
except Exception:
|
||||
# 忽略解析失败
|
||||
pass
|
||||
|
||||
text = str(e)
|
||||
msg = message or text
|
||||
|
||||
# 特定错误映射
|
||||
# 欠费/账户状态异常
|
||||
if (error_code == "Arrearage") or (error_type == "Arrearage") or ("in good standing" in (msg or "")):
|
||||
return "账户欠费或结算异常,访问被拒绝。请检查账号状态或更换有效的 API Key。"
|
||||
# 鉴权失败
|
||||
if status_code == 401 or "invalid api key" in msg.lower():
|
||||
return "鉴权失败,API Key 无效或已过期。请检查系统配置中的 API Key。"
|
||||
# 权限不足或被拒绝
|
||||
if status_code == 403 or error_type in {"PermissionDenied", "permission_denied"}:
|
||||
return "访问被拒绝,权限不足或账号受限。请检查账户权限设置。"
|
||||
# 配额不足或限流
|
||||
if status_code == 429 or error_type in {"insufficient_quota", "rate_limit_exceeded"}:
|
||||
return "请求过于频繁或配额已用尽。请稍后重试或提升账户配额。"
|
||||
# 客户端错误
|
||||
if status_code == 400:
|
||||
return f"请求参数错误或服务拒绝:{message or '请检查输入内容。'}"
|
||||
# 服务端错误
|
||||
if status_code in {500, 502, 503, 504}:
|
||||
return "服务暂时不可用,请稍后重试。"
|
||||
|
||||
# 默认兜底
|
||||
return f"处理您的请求时出现错误:{msg}"
|
||||
@@ -0,0 +1,40 @@
|
||||
from fastapi import APIRouter, WebSocket
|
||||
|
||||
from app.core.logger import log
|
||||
from app.core.router_class import OperationLogRoute
|
||||
from .service import McpService
|
||||
from .schema import ChatQuerySchema
|
||||
|
||||
|
||||
WS_AI = APIRouter(route_class=OperationLogRoute, prefix="/application/ai", tags=["MCP智能助手WebSocket"])
|
||||
|
||||
@WS_AI.websocket("/ws", name="WebSocket聊天")
|
||||
async def websocket_chat_controller(
|
||||
websocket: WebSocket,
|
||||
):
|
||||
"""
|
||||
WebSocket聊天接口
|
||||
|
||||
ws://127.0.0.1:8001/api/v1/application/ai/ws
|
||||
"""
|
||||
await websocket.accept()
|
||||
try:
|
||||
while True:
|
||||
data = await websocket.receive_text()
|
||||
# 流式发送响应
|
||||
try:
|
||||
async for chunk in McpService.chat_query(query=ChatQuerySchema(message=data)):
|
||||
if chunk:
|
||||
await websocket.send_text(chunk)
|
||||
except Exception as e:
|
||||
log.error(f"处理聊天查询出错: {str(e)}")
|
||||
await websocket.send_text(f"抱歉,处理您的请求时出现了错误: {str(e)}")
|
||||
except Exception as e:
|
||||
log.error(f"WebSocket聊天出错: {str(e)}")
|
||||
finally:
|
||||
try:
|
||||
# 检查WebSocket连接状态,避免重复关闭已关闭的连接
|
||||
if websocket.client_state != websocket.client_state.DISCONNECTED:
|
||||
await websocket.close()
|
||||
except Exception as e:
|
||||
log.debug(f"WebSocket关闭时发生异常(预期行为,服务可能正在关闭): {str(e)}")
|
||||
Reference in New Issue
Block a user