style: 移除Python文件中的编码声明并优化代码格式

refactor: 重构前端组件和样式,添加AI助手功能

docs: 更新README文档,添加ruff代码检查说明

feat: 新增AI助手相关API和前端组件

chore: 更新.gitignore文件,添加ruff缓存配置

fix: 修复前端布局和设置相关的问题

perf: 优化代码结构和性能,移除冗余代码

test: 更新测试文件,移除编码声明

build: 更新依赖版本,调整requirements.txt
This commit is contained in:
zhangtao
2026-01-16 01:13:59 +08:00
parent 59c1ce1104
commit d50dd9dd1e
210 changed files with 5236 additions and 4037 deletions
@@ -1,19 +1,18 @@
# -*- coding: utf-8 -*-
from typing import Annotated
from fastapi import APIRouter, Depends, Path, Body, WebSocket
from fastapi import APIRouter, Body, Depends, Path
from fastapi.responses import JSONResponse, StreamingResponse
from app.common.response import StreamResponse, SuccessResponse
from app.api.v1.module_system.auth.schema import AuthSchema
from app.common.request import PaginationService
from app.common.response import StreamResponse, SuccessResponse
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
from .schema import ChatQuerySchema, McpCreateSchema, McpQueryParam, McpUpdateSchema
from .service import McpService
AIRouter = APIRouter(route_class=OperationLogRoute, prefix="/ai", tags=["MCP智能助手"])
@@ -21,20 +20,20 @@ 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"]))
auth: Annotated[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):
@@ -42,23 +41,23 @@ async def chat_controller(
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')
log.error(f"流式响应出错: {e!s}")
yield f"抱歉,处理您的请求时出现了错误: {e!s}".encode()
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"]))
id: Annotated[int, Path(description="MCP ID")],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_application:ai:query"]))]
) -> JSONResponse:
"""
获取 MCP 服务器详情接口
参数:
- id (int): MCP 服务器ID
返回:
- JSONResponse: 包含 MCP 服务器详情的 JSON 响应
"""
@@ -69,39 +68,39 @@ async def detail_controller(
@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"]))
page: Annotated[PaginationQueryParam, Depends()],
search: Annotated[McpQueryParam, Depends()],
auth: Annotated[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 服务器列表成功")
log.info("查询 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"]))
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_application:ai:create"]))]
) -> JSONResponse:
"""
创建 MCP 服务器接口
参数:
- data (McpCreateSchema): 创建 MCP 服务器模型
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 包含创建 MCP 服务器结果的 JSON 响应
"""
@@ -113,17 +112,17 @@ async def create_controller(
@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"]))
id: Annotated[int, Path(description="MCP ID")],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_application:ai:update"]))]
) -> JSONResponse:
"""
修改 MCP 服务器接口
参数:
- data (McpUpdateSchema): 修改 MCP 服务器模型
- id (int): MCP 服务器ID
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 包含修改 MCP 服务器结果的 JSON 响应
"""
@@ -134,16 +133,16 @@ async def update_controller(
@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"]))
ids: Annotated[list[int], Body(description="ID列表")],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_application:ai:delete"]))]
) -> JSONResponse:
"""
删除 MCP 服务器接口
参数:
- ids (list[int]): MCP 服务器ID列表
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 包含删除 MCP 服务器结果的 JSON 响应
"""
@@ -1,10 +1,9 @@
# -*- coding: utf-8 -*-
from typing import Sequence, Any
from app.core.base_crud import CRUDBase
from collections.abc import Sequence
from typing import Any
from app.api.v1.module_system.auth.schema import AuthSchema
from app.core.base_crud import CRUDBase
from .model import McpModel
from .schema import McpCreateSchema, McpUpdateSchema
@@ -15,7 +14,7 @@ class McpCRUD(CRUDBase[McpModel, McpCreateSchema, McpUpdateSchema]):
def __init__(self, auth: AuthSchema) -> None:
"""
初始化CRUD
参数:
- auth (AuthSchema): 认证信息模型
"""
@@ -25,76 +24,76 @@ class McpCRUD(CRUDBase[McpModel, McpCreateSchema, McpUpdateSchema]):
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)
return await self.delete(ids=ids)
@@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
from sqlalchemy import JSON, String, Integer
from sqlalchemy import JSON, Integer, String
from sqlalchemy.orm import Mapped, mapped_column
from app.core.base_model import ModelMixin, UserMixin
@@ -1,12 +1,8 @@
# -*- coding: utf-8 -*-
from pydantic import ConfigDict, Field, HttpUrl, BaseModel
from fastapi import Query
from pydantic import BaseModel, ConfigDict, Field, HttpUrl
from app.core.base_schema import BaseSchema
from app.common.enums import McpLLMProvider
from app.common.enums import McpLLMProvider, McpType
from app.core.base_schema import BaseSchema, UserBySchema
from app.common.enums import McpType
from app.core.validator import DateTimeStr
@@ -28,7 +24,6 @@ class McpCreateSchema(BaseModel):
class McpUpdateSchema(McpCreateSchema):
"""更新 MCP 服务器参数"""
...
class McpOutSchema(McpCreateSchema, BaseSchema, UserBySchema):
@@ -70,4 +65,4 @@ class McpChatParam(BaseSchema):
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='用户提示词')
prompt: str = Field(..., description='用户提示词')
@@ -1,13 +1,13 @@
# -*- coding: utf-8 -*-
from collections.abc import AsyncGenerator
from typing import Any
from typing import Any, AsyncGenerator
from app.core.exceptions import CustomException
from app.api.v1.module_system.auth.schema import AuthSchema
from app.core.exceptions import CustomException
from app.core.logger import log
from .tools.ai_util import AIClient
from .schema import McpCreateSchema, McpUpdateSchema, McpOutSchema, ChatQuerySchema, McpQueryParam
from .crud import McpCRUD
from .schema import ChatQuerySchema, McpCreateSchema, McpOutSchema, McpQueryParam, McpUpdateSchema
from .tools.ai_util import AIClient
class McpService:
@@ -17,11 +17,11 @@ class McpService:
async def detail_service(cls, auth: AuthSchema, id: int) -> dict[str, Any]:
"""
获取MCP服务器详情
参数:
- auth (AuthSchema): 认证信息模型
- id (int): MCP服务器ID
返回:
- dict[str, Any]: MCP服务器详情字典
"""
@@ -29,33 +29,33 @@ class McpService:
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服务器详情字典
"""
@@ -64,17 +64,17 @@ class McpService:
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服务器详情字典
"""
@@ -86,16 +86,16 @@ class McpService:
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
"""
@@ -106,15 +106,15 @@ class McpService:
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]: 异步生成器,每次返回一个聊天响应
"""
@@ -129,4 +129,4 @@ class McpService:
try:
await mcp_client.close()
except Exception as e:
log.debug(f"关闭AIClient时发生异常(预期行为,服务可能正在关闭): {str(e)}")
log.debug(f"关闭AIClient时发生异常(预期行为,服务可能正在关闭): {e!s}")
@@ -1,2 +1 @@
# -*- coding: utf-8 -*-
@@ -1,17 +1,29 @@
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any, Callable
from typing import Any
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.agents.middleware import (
ModelRequest,
ModelResponse,
after_model,
before_model,
dynamic_prompt,
wrap_model_call,
)
from langchain.agents.structured_output import (
MultipleStructuredOutputsError,
StructuredOutputValidationError,
ToolStrategy,
)
from langchain.chat_models import init_chat_model
from langchain.messages import AIMessage, HumanMessage, RemoveMessage, SystemMessage
from langchain.tools import ToolRuntime, tool
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.
@@ -23,17 +35,20 @@ You have access to two tools:
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."""
@@ -58,9 +73,11 @@ class ResponseFormat(BaseModel):
punny_response: str
weather_conditions: str | None = None
# =================定义存储记忆=================
checkpointer = InMemorySaver()
# =================定义动态提示词=================
@dynamic_prompt
def dynamic_system_prompt(request: ModelRequest) -> str:
@@ -68,6 +85,7 @@ def dynamic_system_prompt(request: ModelRequest) -> str:
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."""
@@ -78,7 +96,7 @@ def trim_messages(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
first_msg = messages[0]
recent_messages = messages[-3:] if len(messages) % 2 == 0 else messages[-4:]
new_messages = [first_msg] + recent_messages
new_messages = [first_msg, *recent_messages]
return {
"messages": [
@@ -87,6 +105,7 @@ def trim_messages(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
]
}
@after_model
def validate_response(state: AgentState, runtime: Runtime) -> dict | None:
"""Remove messages containing sensitive words."""
@@ -96,6 +115,7 @@ def validate_response(state: AgentState, runtime: Runtime) -> dict | None:
return {"messages": [RemoveMessage(id=last_message.id or "")]}
return None
@wrap_model_call
def inject_file_context(
request: ModelRequest,
@@ -103,15 +123,11 @@ def inject_file_context(
) -> ModelResponse:
"""Inject context about files user has uploaded this session."""
# Read from State: get uploaded files metadata
uploaded_files = request.state.get("uploaded_files", [])
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_descriptions = [f"- {file['name']} ({file['type']}): {file['summary']}" for file in uploaded_files]
file_context = f"""Files you have access to in this conversation:
{chr(10).join(file_descriptions)}
@@ -119,20 +135,20 @@ def inject_file_context(
Reference these files when answering questions."""
# Inject file context before recent messages
messages = [
messages = [
*request.messages,
{"role": "user", "content": file_context},
]
request = request.override(messages=messages)
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):
if isinstance(error, MultipleStructuredOutputsError):
return "Multiple structured outputs were returned. Pick the most relevant one."
else:
return f"Error: {str(error)}"
return f"Error: {error!s}"
# =================定义智能体=================
agent = create_agent(
@@ -141,7 +157,7 @@ agent = create_agent(
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)),
response_format=ToolStrategy(schema=ResponseFormat, handle_errors=(ValueError, TypeError, custom_error_handler)),
checkpointer=checkpointer
)
@@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
from collections.abc import AsyncGenerator
from typing import Any
from typing import Any, AsyncGenerator
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage, HumanMessage
from app.config.setting import settings
from app.core.logger import log
@@ -13,10 +13,10 @@ class AIClient:
AI客户端类,用于与OpenAI API交互。
"""
def __init__(self):
def __init__(self) -> None:
# 使用LangChain的ChatOpenAI类
self.model = ChatOpenAI(
api_key=lambda: settings.OPENAI_API_KEY,
api_key=settings.OPENAI_API_KEY,
model=settings.OPENAI_MODEL,
base_url=settings.OPENAI_BASE_URL,
temperature=0.7,
@@ -41,16 +41,16 @@ class AIClient:
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)}")
log.error(f"AI处理查询失败: {e!s}")
yield self._friendly_error_message(e)
def _friendly_error_message(self, e: Exception) -> str:
"""将 OpenAI 或网络异常转换为友好的中文提示。"""
# 尝试获取状态码与错误体
@@ -2,19 +2,20 @@ 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
from .schema import ChatQuerySchema
from .service import McpService
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,
):
) -> None:
"""
WebSocket聊天接口
ws://127.0.0.1:8001/api/v1/application/ai/ws
"""
await websocket.accept()
@@ -27,14 +28,14 @@ async def websocket_chat_controller(
if chunk:
await websocket.send_text(chunk)
except Exception as e:
log.error(f"处理聊天查询出错: {str(e)}")
await websocket.send_text(f"抱歉,处理您的请求时出现了错误: {str(e)}")
log.error(f"处理聊天查询出错: {e!s}")
await websocket.send_text(f"抱歉,处理您的请求时出现了错误: {e!s}")
except Exception as e:
log.error(f"WebSocket聊天出错: {str(e)}")
log.error(f"WebSocket聊天出错: {e!s}")
finally:
try:
# 检查WebSocket连接状态,避免重复关闭已关闭的连接
if websocket.client_state != websocket.client_state.DISCONNECTED:
await websocket.close()
except Exception as e:
log.debug(f"WebSocket关闭时发生异常(预期行为,服务可能正在关闭): {str(e)}")
log.debug(f"WebSocket关闭时发生异常(预期行为,服务可能正在关闭): {e!s}")