feat(ai): 新增智能助手功能模块

feat(backend): 添加ChromaDB向量数据库支持
feat(backend): 实现智能体配置、知识库和文档管理
feat(backend): 重构WebSocket聊天服务为AgentService
feat(frontend): 实现完整的聊天界面组件
feat(frontend): 添加智能体配置、知识库和文档管理页面
fix(user): 修复用户导入时性别转换问题
style(import): 优化导入组件加载状态处理
This commit is contained in:
zhangtao
2026-02-09 01:22:48 +08:00
parent b4dcdfa1ab
commit 45dad1e256
34 changed files with 5803 additions and 1214 deletions
@@ -0,0 +1,153 @@
import os
from typing import Any
from langchain_chroma import Chroma
from langchain_core.documents import Document
from langchain_core.embeddings import Embeddings
from app.config.setting import settings
class DummyEmbeddings(Embeddings):
"""虚拟嵌入类,用于在没有嵌入模型时使用"""
def embed_documents(self, texts: list[str]) -> list[list[float]]:
return [[0.0] * 1536 for _ in texts]
def embed_query(self, text: str) -> list[float]:
return [0.0] * 1536
class ChromaDBManager:
"""ChromaDB 管理类 - 使用 langchain-chroma"""
_instance = None
_vectorstore = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
if self._vectorstore is None:
self._initialize_vectorstore()
def _initialize_vectorstore(self):
"""初始化 ChromaDB 向量存储"""
os.makedirs(settings.CHROMA_PERSIST_DIR, exist_ok=True)
self._vectorstore = Chroma(
collection_name=settings.CHROMA_COLLECTION_NAME,
persist_directory=settings.CHROMA_PERSIST_DIR,
embedding_function=DummyEmbeddings(),
)
def get_vectorstore(self) -> Chroma:
"""获取向量存储实例"""
if self._vectorstore is None:
self._initialize_vectorstore()
assert self._vectorstore is not None
return self._vectorstore
def add_documents(
self,
ids: list[str],
embeddings: list[list[float]],
documents: list[str],
metadatas: list[dict[str, Any]] | None = None,
):
"""添加文档到 ChromaDB"""
vectorstore = self.get_vectorstore()
docs = [
Document(page_content=doc, metadata=meta if meta else {})
for doc, meta in zip(documents, metadatas or [{}] * len(documents), strict=False)
]
vectorstore.add_documents(
documents=docs,
ids=ids,
embeddings=embeddings,
)
def query_documents(
self,
query_embeddings: list[list[float]],
n_results: int = 5,
where: dict[str, Any] | None = None,
where_document: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""查询文档"""
vectorstore = self.get_vectorstore()
results = vectorstore.similarity_search_by_vector(
embedding=query_embeddings[0],
k=n_results,
filter=where,
)
return {
"documents": [[doc.page_content for doc in results]],
"metadatas": [[doc.metadata for doc in results]],
}
def delete_documents(
self,
ids: list[str],
):
"""删除文档"""
vectorstore = self.get_vectorstore()
vectorstore.delete(ids=ids)
def get_documents(
self,
ids: list[str] | None = None,
where: dict[str, Any] | None = None,
limit: int | None = None,
offset: int | None = None,
) -> dict[str, Any]:
"""获取文档"""
vectorstore = self.get_vectorstore()
if ids:
results = vectorstore.get_by_ids(ids)
else:
results = []
return {
"documents": [[doc.page_content for doc in results]] if results else [[]],
"metadatas": [[doc.metadata for doc in results]] if results else [[]],
}
def update_documents(
self,
ids: list[str],
embeddings: list[list[float]] | None = None,
documents: list[str] | None = None,
metadatas: list[dict[str, Any]] | None = None,
):
"""更新文档 - 通过删除旧文档再添加新文档的方式实现"""
vectorstore = self.get_vectorstore()
if documents or metadatas:
vectorstore.delete(ids=ids)
if documents:
docs = [
Document(page_content=doc, metadata=meta if meta else {})
for doc, meta in zip(documents, metadatas or [{}] * len(documents), strict=False)
]
vectorstore.add_documents(
documents=docs,
ids=ids,
embeddings=embeddings,
)
def reset(self):
"""重置 ChromaDB"""
if self._vectorstore is not None:
self._vectorstore.delete_collection()
self._vectorstore = None
chroma_manager = ChromaDBManager()
@@ -1,7 +1,6 @@
from typing import Annotated
from fastapi import APIRouter, Body, Depends, Path
from fastapi.responses import JSONResponse, StreamingResponse
from app.api.v1.module_system.auth.schema import AuthSchema
from app.common.request import PaginationService
@@ -12,26 +11,190 @@ from app.core.logger import log
from app.core.router_class import OperationLogRoute
from .schema import (
AgentConfigCreateSchema,
AgentConfigOutSchema,
AgentConfigQueryParam,
AgentConfigUpdateSchema,
ChatQuerySchema,
McpCreateSchema,
McpOutSchema,
McpQueryParam,
McpUpdateSchema,
KnowledgeCreateSchema,
KnowledgeDocumentCreateSchema,
KnowledgeDocumentOutSchema,
KnowledgeDocumentQueryParam,
KnowledgeDocumentUpdateSchema,
KnowledgeOutSchema,
KnowledgeQueryParam,
KnowledgeUpdateSchema,
)
from .service import McpService
from .service import AgentConfigService, AgentService, KnowledgeService
AIRouter = APIRouter(route_class=OperationLogRoute, prefix="/ai", tags=["MCP智能助手"])
AIRouter = APIRouter(route_class=OperationLogRoute, prefix="/ai", tags=["智能助手"])
@AIRouter.get(
"/agent-config/detail/{id}",
summary="获取智能体配置详情",
description="获取智能体配置详情",
response_model=ResponseSchema[AgentConfigOutSchema],
)
async def agent_config_detail_controller(
id: Annotated[int, Path(description="智能体配置ID")],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_application:agent-config:detail"]))],
) -> SuccessResponse:
"""
获取智能体配置详情接口
参数:
- id (int): 智能体配置ID
返回:
- SuccessResponse: 包含智能体配置详情的响应
"""
result_dict = await AgentConfigService.get_by_id_service(auth=auth, id=id)
log.info(f"获取智能体配置详情成功 {id}")
return SuccessResponse(data=result_dict, msg="获取智能体配置详情成功")
@AIRouter.get(
"/agent-config/default",
summary="获取默认智能体配置",
description="获取默认智能体配置",
response_model=ResponseSchema[AgentConfigOutSchema],
)
async def agent_config_default_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_application:agent-config:query"]))],
) -> SuccessResponse:
"""
获取默认智能体配置接口
参数:
- auth (AuthSchema): 认证信息模型
返回:
- SuccessResponse: 包含默认智能体配置的响应
"""
result_dict = await AgentConfigService.get_default_service(auth=auth)
log.info("获取默认智能体配置成功")
return SuccessResponse(data=result_dict, msg="获取默认智能体配置成功")
@AIRouter.get(
"/agent-config/list",
summary="查询智能体配置列表",
description="查询智能体配置列表",
response_model=ResponseSchema[list[AgentConfigOutSchema]],
)
async def agent_config_list_controller(
page: Annotated[PaginationQueryParam, Depends()],
search: Annotated[AgentConfigQueryParam, Depends()],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_application:agent-config:query"]))],
) -> SuccessResponse:
"""
查询智能体配置列表接口
参数:
- page (PaginationQueryParam): 分页查询参数模型
- search (AgentConfigQueryParam): 查询参数模型
- auth (AuthSchema): 认证信息模型
返回:
- SuccessResponse: 包含智能体配置列表的响应
"""
result_dict = await AgentConfigService.get_list_service(auth=auth, query_params=search)
result_dict = await PaginationService.paginate(
data_list=result_dict["data"],
page_no=page.page_no,
page_size=page.page_size,
)
log.info("查询智能体配置列表成功")
return SuccessResponse(data=result_dict, msg="查询智能体配置列表成功")
@AIRouter.post(
"/agent-config/create",
summary="创建智能体配置",
description="创建智能体配置",
response_model=ResponseSchema[AgentConfigOutSchema],
)
async def agent_config_create_controller(
data: AgentConfigCreateSchema,
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_application:agent-config:create"]))],
) -> SuccessResponse:
"""
创建智能体配置接口
参数:
- data (AgentConfigCreateSchema): 创建智能体配置模型
- auth (AuthSchema): 认证信息模型
返回:
- SuccessResponse: 包含创建智能体配置结果的响应
"""
result_dict = await AgentConfigService.create_service(auth=auth, data=data)
log.info(f"创建智能体配置成功: {result_dict}")
return SuccessResponse(data=result_dict, msg="创建智能体配置成功")
@AIRouter.put(
"/agent-config/update/{id}",
summary="修改智能体配置",
description="修改智能体配置",
response_model=ResponseSchema[AgentConfigOutSchema],
)
async def agent_config_update_controller(
data: AgentConfigUpdateSchema,
id: Annotated[int, Path(description="智能体配置ID")],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_application:agent-config:update"]))],
) -> SuccessResponse:
"""
修改智能体配置接口
参数:
- data (AgentConfigUpdateSchema): 修改智能体配置模型
- id (int): 智能体配置ID
- auth (AuthSchema): 认证信息模型
返回:
- SuccessResponse: 包含修改智能体配置结果的响应
"""
result_dict = await AgentConfigService.update_service(auth=auth, id=id, data=data)
log.info(f"修改智能体配置成功: {result_dict}")
return SuccessResponse(data=result_dict, msg="修改智能体配置成功")
@AIRouter.delete(
"/agent-config/delete",
summary="删除智能体配置",
description="删除智能体配置",
response_model=ResponseSchema[None],
)
async def agent_config_delete_controller(
ids: Annotated[list[int], Body(description="ID列表")],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_application:agent-config:delete"]))],
) -> SuccessResponse:
"""
删除智能体配置接口
参数:
- ids (list[int]): 智能体配置ID列表
- auth (AuthSchema): 认证信息模型
返回:
- SuccessResponse: 包含删除智能体配置结果的响应
"""
await AgentConfigService.delete_service(auth=auth, ids=ids)
log.info(f"删除智能体配置成功: {ids}")
return SuccessResponse(msg="删除智能体配置成功")
@AIRouter.post(
"/chat",
summary="智能对话",
description="MCP智能助手进行对话",
description="与智能助手进行对话",
)
async def chat_controller(
query: ChatQuerySchema,
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_application:ai:chat"]))],
) -> StreamingResponse:
) -> StreamResponse:
"""
智能对话接口
@@ -46,8 +209,7 @@ async def chat_controller(
async def generate_response():
try:
async for chunk in McpService.chat_query(query=query):
# 确保返回的是字节串
async for chunk in AgentService.chat_query(query=query):
if chunk:
yield (chunk.encode("utf-8") if isinstance(chunk, str) else chunk)
except Exception as e:
@@ -58,52 +220,52 @@ async def chat_controller(
@AIRouter.get(
"/detail/{id}",
summary="获取 MCP 服务器详情",
description="获取 MCP 服务器详情",
response_model=ResponseSchema[McpOutSchema],
"/knowledge/detail/{id}",
summary="获取知识库详情",
description="获取知识库详情",
response_model=ResponseSchema[KnowledgeOutSchema],
)
async def detail_controller(
id: Annotated[int, Path(description="MCP ID")],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_application:ai:query"]))],
) -> JSONResponse:
async def knowledge_detail_controller(
id: Annotated[int, Path(description="知识库ID")],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_application:knowledge:query"]))],
) -> SuccessResponse:
"""
获取 MCP 服务器详情接口
获取知识库详情接口
参数:
- id (int): MCP 服务器ID
- id (int): 知识库ID
返回:
- JSONResponse: 包含 MCP 服务器详情的 JSON 响应
- SuccessResponse: 包含知识库详情的响应
"""
result_dict = await McpService.detail_service(auth=auth, id=id)
log.info(f"获取 MCP 服务器详情成功 {id}")
return SuccessResponse(data=result_dict, msg="获取 MCP 服务器详情成功")
result_dict = await KnowledgeService.detail_service(auth=auth, id=id)
log.info(f"获取知识库详情成功 {id}")
return SuccessResponse(data=result_dict, msg="获取知识库详情成功")
@AIRouter.get(
"/list",
summary="查询 MCP 服务器列表",
description="查询 MCP 服务器列表",
response_model=ResponseSchema[list[McpOutSchema]],
"/knowledge/list",
summary="查询知识库列表",
description="查询知识库列表",
response_model=ResponseSchema[list[KnowledgeOutSchema]],
)
async def list_controller(
async def knowledge_list_controller(
page: Annotated[PaginationQueryParam, Depends()],
search: Annotated[McpQueryParam, Depends()],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_application:ai:query"]))],
) -> JSONResponse:
search: Annotated[KnowledgeQueryParam, Depends()],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_application:knowledge:query"]))],
) -> SuccessResponse:
"""
查询 MCP 服务器列表接口
查询知识库列表接口
参数:
- page (PaginationQueryParam): 分页查询参数模型
- search (McpQueryParam): 查询参数模型
- search (KnowledgeQueryParam): 查询参数模型
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 包含 MCP 服务器列表的 JSON 响应
- SuccessResponse: 包含知识库列表的响应
"""
result_dict_list = await McpService.list_service(
result_dict_list = await KnowledgeService.list_service(
auth=auth, search=search, order_by=page.order_by
)
result_dict = await PaginationService.paginate(
@@ -111,82 +273,217 @@ async def list_controller(
page_no=page.page_no,
page_size=page.page_size,
)
log.info("查询 MCP 服务器列表成功")
return SuccessResponse(data=result_dict, msg="查询 MCP 服务器列表成功")
log.info("查询知识库列表成功")
return SuccessResponse(data=result_dict, msg="查询知识库列表成功")
@AIRouter.post(
"/create",
summary="创建 MCP 服务器",
description="创建 MCP 服务器",
response_model=ResponseSchema[McpOutSchema],
"/knowledge/create",
summary="创建知识库",
description="创建知识库",
response_model=ResponseSchema[KnowledgeOutSchema],
)
async def create_controller(
data: McpCreateSchema,
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_application:ai:create"]))],
) -> JSONResponse:
async def knowledge_create_controller(
data: KnowledgeCreateSchema,
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_application:knowledge:create"]))],
) -> SuccessResponse:
"""
创建 MCP 服务器接口
创建知识库接口
参数:
- data (McpCreateSchema): 创建 MCP 服务器模型
- data (KnowledgeCreateSchema): 创建知识库模型
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 包含创建 MCP 服务器结果的 JSON 响应
- SuccessResponse: 包含创建知识库结果的响应
"""
result_dict = await McpService.create_service(auth=auth, data=data)
log.info(f"创建 MCP 服务器成功: {result_dict}")
return SuccessResponse(data=result_dict, msg="创建 MCP 服务器成功")
result_dict = await KnowledgeService.create_service(auth=auth, data=data)
log.info(f"创建知识库成功: {result_dict}")
return SuccessResponse(data=result_dict, msg="创建知识库成功")
@AIRouter.put(
"/update/{id}",
summary="修改 MCP 服务器",
description="修改 MCP 服务器",
response_model=ResponseSchema[McpOutSchema],
"/knowledge/update/{id}",
summary="修改知识库",
description="修改知识库",
response_model=ResponseSchema[KnowledgeOutSchema],
)
async def update_controller(
data: McpUpdateSchema,
id: Annotated[int, Path(description="MCP ID")],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_application:ai:update"]))],
) -> JSONResponse:
async def knowledge_update_controller(
data: KnowledgeUpdateSchema,
id: Annotated[int, Path(description="知识库ID")],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_application:knowledge:update"]))],
) -> SuccessResponse:
"""
修改 MCP 服务器接口
修改知识库接口
参数:
- data (McpUpdateSchema): 修改 MCP 服务器模型
- id (int): MCP 服务器ID
- data (KnowledgeUpdateSchema): 修改知识库模型
- id (int): 知识库ID
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 包含修改 MCP 服务器结果的 JSON 响应
- SuccessResponse: 包含修改知识库结果的响应
"""
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 服务器成功")
result_dict = await KnowledgeService.update_service(auth=auth, id=id, data=data)
log.info(f"修改知识库成功: {result_dict}")
return SuccessResponse(data=result_dict, msg="修改知识库成功")
@AIRouter.delete(
"/delete",
summary="删除 MCP 服务器",
description="删除 MCP 服务器",
"/knowledge/delete",
summary="删除知识库",
description="删除知识库",
response_model=ResponseSchema[None],
)
async def delete_controller(
async def knowledge_delete_controller(
ids: Annotated[list[int], Body(description="ID列表")],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_application:ai:delete"]))],
) -> JSONResponse:
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_application:knowledge:delete"]))],
) -> SuccessResponse:
"""
删除 MCP 服务器接口
删除知识库接口
参数:
- ids (list[int]): MCP 服务器ID列表
- ids (list[int]): 知识库ID列表
- auth (AuthSchema): 认证信息模型
返回:
- JSONResponse: 包含删除 MCP 服务器结果的 JSON 响应
- SuccessResponse: 包含删除知识库结果的响应
"""
await McpService.delete_service(auth=auth, ids=ids)
log.info(f"删除 MCP 服务器成功: {ids}")
return SuccessResponse(msg="删除 MCP 服务器成功")
await KnowledgeService.delete_service(auth=auth, ids=ids)
log.info(f"删除知识库成功: {ids}")
return SuccessResponse(msg="删除知识库成功")
@AIRouter.get(
"/document/detail/{id}",
summary="获取知识库文档详情",
description="获取知识库文档详情",
response_model=ResponseSchema[KnowledgeDocumentOutSchema],
)
async def document_detail_controller(
id: Annotated[int, Path(description="文档ID")],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_application:document:query"]))],
) -> SuccessResponse:
"""
获取知识库文档详情接口
参数:
- id (int): 文档ID
返回:
- SuccessResponse: 包含文档详情的响应
"""
result_dict = await KnowledgeService.document_detail_service(auth=auth, id=id)
log.info(f"获取知识库文档详情成功 {id}")
return SuccessResponse(data=result_dict, msg="获取知识库文档详情成功")
@AIRouter.get(
"/document/list",
summary="查询知识库文档列表",
description="查询知识库文档列表",
response_model=ResponseSchema[list[KnowledgeDocumentOutSchema]],
)
async def document_list_controller(
page: Annotated[PaginationQueryParam, Depends()],
search: Annotated[KnowledgeDocumentQueryParam, Depends()],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_application:document:query"]))],
) -> SuccessResponse:
"""
查询知识库文档列表接口
参数:
- page (PaginationQueryParam): 分页查询参数模型
- search (KnowledgeDocumentQueryParam): 查询参数模型
- auth (AuthSchema): 认证信息模型
返回:
- SuccessResponse: 包含文档列表的响应
"""
result_dict_list = await KnowledgeService.document_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("查询知识库文档列表成功")
return SuccessResponse(data=result_dict, msg="查询知识库文档列表成功")
@AIRouter.post(
"/document/create",
summary="创建知识库文档",
description="创建知识库文档",
response_model=ResponseSchema[KnowledgeDocumentOutSchema],
)
async def document_create_controller(
data: KnowledgeDocumentCreateSchema,
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_application:document:create"]))],
) -> SuccessResponse:
"""
创建知识库文档接口
参数:
- data (KnowledgeDocumentCreateSchema): 创建文档模型
- auth (AuthSchema): 认证信息模型
返回:
- SuccessResponse: 包含创建文档结果的响应
"""
result_dict = await KnowledgeService.document_create_service(auth=auth, data=data)
log.info(f"创建知识库文档成功: {result_dict}")
return SuccessResponse(data=result_dict, msg="创建知识库文档成功")
@AIRouter.put(
"/document/update/{id}",
summary="修改知识库文档",
description="修改知识库文档",
response_model=ResponseSchema[KnowledgeDocumentOutSchema],
)
async def document_update_controller(
data: KnowledgeDocumentUpdateSchema,
id: Annotated[int, Path(description="文档ID")],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_application:document:update"]))],
) -> SuccessResponse:
"""
修改知识库文档接口
参数:
- data (KnowledgeDocumentUpdateSchema): 修改文档模型
- id (int): 文档ID
- auth (AuthSchema): 认证信息模型
返回:
- SuccessResponse: 包含修改文档结果的响应
"""
result_dict = await KnowledgeService.document_update_service(auth=auth, id=id, data=data)
log.info(f"修改知识库文档成功: {result_dict}")
return SuccessResponse(data=result_dict, msg="修改知识库文档成功")
@AIRouter.delete(
"/document/delete",
summary="删除知识库文档",
description="删除知识库文档",
response_model=ResponseSchema[None],
)
async def document_delete_controller(
ids: Annotated[list[int], Body(description="ID列表")],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_application:document:delete"]))],
) -> SuccessResponse:
"""
删除知识库文档接口
参数:
- ids (list[int]): 文档ID列表
- auth (AuthSchema): 认证信息模型
返回:
- SuccessResponse: 包含删除文档结果的响应
"""
await KnowledgeService.document_delete_service(auth=auth, ids=ids)
log.info(f"删除知识库文档成功: {ids}")
return SuccessResponse(msg="删除知识库文档成功")
+251 -27
View File
@@ -4,12 +4,19 @@ 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
from .model import AgentConfigModel, KnowledgeDocumentModel, KnowledgeModel
from .schema import (
AgentConfigCreateSchema,
AgentConfigUpdateSchema,
KnowledgeCreateSchema,
KnowledgeDocumentCreateSchema,
KnowledgeDocumentUpdateSchema,
KnowledgeUpdateSchema,
)
class McpCRUD(CRUDBase[McpModel, McpCreateSchema, McpUpdateSchema]):
"""MCP 服务器数据层"""
class AgentConfigCRUD(CRUDBase[AgentConfigModel, AgentConfigCreateSchema, AgentConfigUpdateSchema]):
"""智能体配置数据层"""
def __init__(self, auth: AuthSchema) -> None:
"""
@@ -19,35 +26,142 @@ class McpCRUD(CRUDBase[McpModel, McpCreateSchema, McpUpdateSchema]):
- auth (AuthSchema): 认证信息模型
"""
self.auth = auth
super().__init__(model=McpModel, auth=auth)
super().__init__(model=AgentConfigModel, auth=auth)
async def get_by_id_crud(
self, id: int, preload: list[str | Any] | None = None
) -> McpModel | None:
) -> AgentConfigModel | None:
"""
获取MCP服务器详情
获取智能体配置详情
参数:
- id (int): MCP服务器ID
- id (int): 智能体配置ID
- preload (list[str | Any] | None): 预加载关系,未提供时使用模型默认项
返回:
- McpModel | None: MCP服务器模型实例(如果存在)
- AgentConfigModel | None: 智能体配置模型实例(如果存在)
"""
return await self.get(id=id, preload=preload)
async def get_default_crud(
self, preload: list[str | Any] | None = None
) -> AgentConfigModel | None:
"""
获取默认智能体配置
参数:
- preload (list[str | Any] | None): 预加载关系,未提供时使用模型默认项
返回:
- AgentConfigModel | None: 智能体配置模型实例(如果存在)
"""
return await self.get(is_default=1, 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[AgentConfigModel]:
"""
列表查询智能体配置
参数:
- search (dict | None): 查询参数字典
- order_by (list[dict[str, str]] | None): 排序参数列表
- preload (list[str | Any] | None): 预加载关系,未提供时使用模型默认项
返回:
- Sequence[AgentConfigModel]: 智能体配置模型实例序列
"""
return await self.list(
search=search or {},
order_by=order_by or [{"id": "asc"}],
preload=preload,
)
async def create_crud(
self, data: AgentConfigCreateSchema
) -> AgentConfigModel | None:
"""
创建智能体配置
参数:
- data (AgentConfigCreateSchema): 创建智能体配置模型
返回:
- Optional[AgentConfigModel]: 创建的智能体配置模型实例(如果成功)
"""
return await self.create(data=data)
async def update_crud(
self, id: int, data: AgentConfigUpdateSchema
) -> AgentConfigModel | None:
"""
更新智能体配置
参数:
- id (int): 智能体配置ID
- data (AgentConfigUpdateSchema): 更新智能体配置模型
返回:
- AgentConfigModel | None: 更新的智能体配置模型实例(如果成功)
"""
return await self.update(id=id, data=data)
async def delete_crud(self, ids: list[int]) -> None:
"""
批量删除智能体配置
参数:
- ids (list[int]): 智能体配置ID列表
返回:
- None
"""
return await self.delete(ids=ids)
class KnowledgeCRUD(CRUDBase[KnowledgeModel, KnowledgeCreateSchema, KnowledgeUpdateSchema]):
"""知识库数据层"""
def __init__(self, auth: AuthSchema) -> None:
"""
初始化CRUD
参数:
- auth (AuthSchema): 认证信息模型
"""
self.auth = auth
super().__init__(model=KnowledgeModel, auth=auth)
async def get_by_id_crud(
self, id: int, preload: list[str | Any] | None = None
) -> KnowledgeModel | None:
"""
获取知识库详情
参数:
- id (int): 知识库ID
- preload (list[str | Any] | None): 预加载关系,未提供时使用模型默认项
返回:
- KnowledgeModel | None: 知识库模型实例(如果存在)
"""
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:
) -> KnowledgeModel | None:
"""
通过名称获取MCP服务器
通过名称获取知识库
参数:
- name (str): MCP服务器名称
- name (str): 知识库名称
- preload (list[str | Any] | None): 预加载关系,未提供时使用模型默认项
返回:
- Optional[McpModel]: MCP服务器模型实例(如果存在)
- Optional[KnowledgeModel]: 知识库模型实例(如果存在)
"""
return await self.get(name=name, preload=preload)
@@ -56,9 +170,9 @@ class McpCRUD(CRUDBase[McpModel, McpCreateSchema, McpUpdateSchema]):
search: dict | None = None,
order_by: list[dict[str, str]] | None = None,
preload: list[str | Any] | None = None,
) -> Sequence[McpModel]:
) -> Sequence[KnowledgeModel]:
"""
列表查询MCP服务器
列表查询知识库
参数:
- search (dict | None): 查询参数字典
@@ -66,7 +180,7 @@ class McpCRUD(CRUDBase[McpModel, McpCreateSchema, McpUpdateSchema]):
- preload (list[str | Any] | None): 预加载关系,未提供时使用模型默认项
返回:
- Sequence[McpModel]: MCP服务器模型实例序列
- Sequence[KnowledgeModel]: 知识库模型实例序列
"""
return await self.list(
search=search or {},
@@ -74,37 +188,147 @@ class McpCRUD(CRUDBase[McpModel, McpCreateSchema, McpUpdateSchema]):
preload=preload,
)
async def create_crud(self, data: McpCreateSchema) -> McpModel | None:
async def create_crud(self, data: KnowledgeCreateSchema) -> KnowledgeModel | None:
"""
创建MCP服务器
创建知识库
参数:
- data (McpCreateSchema): 创建MCP服务器模型
- data (KnowledgeCreateSchema): 创建知识库模型
返回:
- Optional[McpModel]: 创建的MCP服务器模型实例(如果成功)
- Optional[KnowledgeModel]: 创建的知识库模型实例(如果成功)
"""
return await self.create(data=data)
async def update_crud(self, id: int, data: McpUpdateSchema) -> McpModel | None:
async def update_crud(
self, id: int, data: KnowledgeUpdateSchema
) -> KnowledgeModel | None:
"""
更新MCP服务器
更新知识库
参数:
- id (int): MCP服务器ID
- data (McpUpdateSchema): 更新MCP服务器模型
- id (int): 知识库ID
- data (KnowledgeUpdateSchema): 更新知识库模型
返回:
- McpModel | None: 更新的MCP服务器模型实例(如果成功)
- KnowledgeModel | None: 更新的知识库模型实例(如果成功)
"""
return await self.update(id=id, data=data)
async def delete_crud(self, ids: list[int]) -> None:
"""
批量删除MCP服务器
批量删除知识库
参数:
- ids (list[int]): MCP服务器ID列表
- ids (list[int]): 知识库ID列表
返回:
- None
"""
return await self.delete(ids=ids)
class KnowledgeDocumentCRUD(CRUDBase[KnowledgeDocumentModel, KnowledgeDocumentCreateSchema, KnowledgeDocumentUpdateSchema]):
"""知识库文档数据层"""
def __init__(self, auth: AuthSchema) -> None:
"""
初始化CRUD
参数:
- auth (AuthSchema): 认证信息模型
"""
self.auth = auth
super().__init__(model=KnowledgeDocumentModel, auth=auth)
async def get_by_id_crud(
self, id: int, preload: list[str | Any] | None = None
) -> KnowledgeDocumentModel | None:
"""
获取知识库文档详情
参数:
- id (int): 文档ID
- preload (list[str | Any] | None): 预加载关系,未提供时使用模型默认项
返回:
- KnowledgeDocumentModel | None: 知识库文档模型实例(如果存在)
"""
return await self.get(id=id, preload=preload)
async def get_by_knowledge_id_crud(
self, knowledge_id: int, preload: list[str | Any] | None = None
) -> Sequence[KnowledgeDocumentModel]:
"""
通过知识库ID获取文档列表
参数:
- knowledge_id (int): 知识库ID
- preload (list[str | Any] | None): 预加载关系,未提供时使用模型默认项
返回:
- Sequence[KnowledgeDocumentModel]: 知识库文档模型实例序列
"""
return await self.list(search={"knowledge_id": (0, knowledge_id)}, 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[KnowledgeDocumentModel]:
"""
列表查询知识库文档
参数:
- search (dict | None): 查询参数字典
- order_by (list[dict[str, str]] | None): 排序参数列表
- preload (list[str | Any] | None): 预加载关系,未提供时使用模型默认项
返回:
- Sequence[KnowledgeDocumentModel]: 知识库文档模型实例序列
"""
return await self.list(
search=search or {},
order_by=order_by or [{"id": "asc"}],
preload=preload,
)
async def create_crud(
self, data: KnowledgeDocumentCreateSchema
) -> KnowledgeDocumentModel | None:
"""
创建知识库文档
参数:
- data (KnowledgeDocumentCreateSchema): 创建知识库文档模型
返回:
- Optional[KnowledgeDocumentModel]: 创建的知识库文档模型实例(如果成功)
"""
return await self.create(data=data)
async def update_crud(
self, id: int, data: KnowledgeDocumentUpdateSchema
) -> KnowledgeDocumentModel | None:
"""
更新知识库文档
参数:
- id (int): 文档ID
- data (KnowledgeDocumentUpdateSchema): 更新知识库文档模型
返回:
- KnowledgeDocumentModel | None: 更新的知识库文档模型实例(如果成功)
"""
return await self.update(id=id, data=data)
async def delete_crud(self, ids: list[int]) -> None:
"""
批量删除知识库文档
参数:
- ids (list[int]): 文档ID列表
返回:
- None
@@ -1,24 +1,60 @@
from sqlalchemy import JSON, Integer, String
from sqlalchemy import JSON, Integer, String, Text
from sqlalchemy.orm import Mapped, mapped_column
from app.core.base_model import ModelMixin, UserMixin
class McpModel(ModelMixin, UserMixin):
class AgentConfigModel(ModelMixin, UserMixin):
"""
MCP 服务器
MCP类型:
- 0: stdio (标准输入输出)
- 1: sse (Server-Sent Events)
智能体配置
"""
__tablename__: str = "app_ai_mcp"
__table_args__: dict[str, str] = {"comment": "MCP 服务器"}
__tablename__: str = "app_ai_agent_config"
__table_args__: dict[str, str] = {"comment": "智能体配置"}
__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 环境变量")
name: Mapped[str] = mapped_column(String(100), comment="智能体名称")
provider: Mapped[str] = mapped_column(String(50), default="openai", comment="LLM 供应商")
model: Mapped[str] = mapped_column(String(100), comment="LLM 名称")
api_key: Mapped[str] = mapped_column(String(500), comment="LLM API Key")
base_url: Mapped[str | None] = mapped_column(String(500), default=None, comment="自定义 LLM API 地址")
temperature: Mapped[float] = mapped_column(Integer, default=70, comment="温度参数 (0-100)")
system_prompt: Mapped[str] = mapped_column(Text, comment="系统提示词")
is_default: Mapped[bool] = mapped_column(Integer, default=0, comment="是否默认配置")
is_active: Mapped[bool] = mapped_column(Integer, default=1, comment="是否启用")
class KnowledgeModel(ModelMixin, UserMixin):
"""
知识库表
"""
__tablename__: str = "app_ai_knowledge"
__table_args__: dict[str, str] = {"comment": "知识库表"}
__loader_options__: list[str] = ["created_by", "updated_by"]
name: Mapped[str] = mapped_column(String(100), comment="知识库名称")
description: Mapped[str | None] = mapped_column(String(500), default=None, comment="知识库描述")
embedding_model: Mapped[str] = mapped_column(String(100), default="openai", comment="嵌入模型")
chunk_size: Mapped[int] = mapped_column(Integer, default=500, comment="分块大小")
chunk_overlap: Mapped[int] = mapped_column(Integer, default=50, comment="分块重叠大小")
is_active: Mapped[bool] = mapped_column(Integer, default=1, comment="是否启用")
class KnowledgeDocumentModel(ModelMixin, UserMixin):
"""
知识库文档表
"""
__tablename__: str = "app_ai_knowledge_document"
__table_args__: dict[str, str] = {"comment": "知识库文档表"}
__loader_options__: list[str] = ["created_by", "updated_by"]
knowledge_id: Mapped[int] = mapped_column(Integer, comment="知识库ID")
title: Mapped[str] = mapped_column(String(200), comment="文档标题")
content: Mapped[str] = mapped_column(Text, comment="文档内容")
file_type: Mapped[str] = mapped_column(String(50), default="text", comment="文件类型")
file_path: Mapped[str | None] = mapped_column(String(500), default=None, comment="文件路径")
meta_data: Mapped[dict[str, str] | None] = mapped_column(JSON(), default=None, comment="元数据")
chunk_count: Mapped[int] = mapped_column(Integer, default=0, comment="分块数量")
is_indexed: Mapped[bool] = mapped_column(Integer, default=0, comment="是否已索引")
@@ -1,7 +1,8 @@
from fastapi import Query
from pydantic import BaseModel, ConfigDict, Field, HttpUrl
from app.common.enums import McpLLMProvider, McpType, QueueEnum
from fastapi import Query
from pydantic import BaseModel, ConfigDict, Field
from app.common.enums import QueueEnum
from app.core.base_schema import BaseSchema, UserBySchema
from app.core.validator import DateTimeStr
@@ -10,37 +11,67 @@ class ChatQuerySchema(BaseModel):
"""聊天查询模型"""
message: str = Field(..., min_length=1, max_length=4000, description="聊天消息")
knowledge_ids: list[int] | None = Field(None, description="知识库ID列表,用于RAG检索")
agent_config_id: int | None = Field(None, description="智能体配置ID,用于指定使用的智能体配置")
class McpCreateSchema(BaseModel):
"""创建 MCP 服务器参数"""
class AgentConfigSchema(BaseModel):
"""智能体配置参数"""
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 环境变量")
provider: str = Field("openai", description="LLM 供应商")
model: str = Field(..., description="LLM 名称")
api_key: str = Field(..., description="LLM API Key")
base_url: str | None = Field(None, description="自定义 LLM API 地址")
temperature: float = Field(0.7, description="温度参数,控制随机性")
system_prompt: str = Field("你是一个有用的AI助手,可以帮助用户回答问题和提供帮助。请用中文回答用户的问题。", description="系统提示词")
class McpUpdateSchema(McpCreateSchema):
"""更新 MCP 服务器参数"""
class AgentConfigCreateSchema(BaseModel):
"""创建智能体配置参数"""
name: str = Field(..., max_length=100, description="智能体名称")
provider: str = Field("openai", max_length=50, description="LLM 供应商")
model: str = Field(..., max_length=100, description="LLM 名称")
api_key: str = Field(..., max_length=500, description="LLM API Key")
base_url: str | None = Field(None, max_length=500, description="自定义 LLM API 地址")
temperature: float = Field(0.7, ge=0.0, le=2.0, description="温度参数,控制随机性")
system_prompt: str = Field(
"你是一个有用的AI助手,可以帮助用户回答问题和提供帮助。请用中文回答用户的问题。",
description="系统提示词",
)
is_default: bool = Field(False, description="是否默认配置")
is_active: bool = Field(True, description="是否启用")
class McpOutSchema(McpCreateSchema, BaseSchema, UserBySchema):
"""MCP 服务器详情"""
class AgentConfigUpdateSchema(BaseModel):
"""更新智能体配置参数"""
name: str | None = Field(None, max_length=100, description="智能体名称")
provider: str | None = Field(None, max_length=50, description="LLM 供应商")
model: str | None = Field(None, max_length=100, description="LLM 名称")
api_key: str | None = Field(None, max_length=500, description="LLM API Key")
base_url: str | None = Field(None, max_length=500, description="自定义 LLM API 地址")
temperature: float | None = Field(None, ge=0.0, le=2.0, description="温度参数,控制随机性")
system_prompt: str | None = Field(None, description="系统提示词")
is_default: bool | None = Field(None, description="是否默认配置")
is_active: bool | None = Field(None, description="是否启用")
class AgentConfigOutSchema(AgentConfigCreateSchema, BaseSchema, UserBySchema):
"""智能体配置详情"""
model_config = ConfigDict(from_attributes=True)
class McpQueryParam:
"""MCP 服务器查询参数"""
class AgentConfigQueryParam:
"""智能体配置查询参数"""
def __init__(
self,
name: str | None = Query(None, description="MCP 名称"),
type: McpType | None = Query(None, description="MCP 类型"),
name: str | None = Query(None, description="智能体名称"),
provider: str | None = Query(None, description="LLM 供应商"),
is_default: bool | None = Query(None, description="是否默认配置"),
is_active: bool | None = Query(None, description="是否启用"),
created_time: list[DateTimeStr] | None = Query(
None,
description="创建时间范围",
@@ -54,27 +85,132 @@ class McpQueryParam:
created_id: int | None = Query(None, description="创建人"),
updated_id: int | None = Query(None, description="更新人"),
) -> None:
# 模糊查询字段
self.name = (QueueEnum.like.value, name)
# 精确查询字段
self.type = (QueueEnum.eq.value, type)
self.provider = (QueueEnum.eq.value, provider)
self.is_default = (QueueEnum.eq.value, is_default)
self.is_active = (QueueEnum.eq.value, is_active)
self.created_id = (QueueEnum.eq.value, created_id)
self.updated_id = (QueueEnum.eq.value, updated_id)
# 时间范围查询
if created_time and len(created_time) == 2:
self.created_time = (QueueEnum.between.value, (created_time[0], created_time[1]))
if updated_time and len(updated_time) == 2:
self.updated_time = (QueueEnum.between.value, (updated_time[0], updated_time[1]))
class McpChatParam(BaseSchema):
"""MCP 聊天参数"""
class KnowledgeCreateSchema(BaseModel):
"""创建知识库参数"""
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="户提示词")
name: str = Field(..., max_length=100, description="知识库名称")
description: str | None = Field(None, max_length=500, description="知识库描述")
embedding_model: str = Field("openai", max_length=100, description="嵌入模型")
chunk_size: int = Field(500, ge=100, le=2000, description="分块大小")
chunk_overlap: int = Field(50, ge=0, le=500, description="分块重叠大小")
is_active: bool = Field(True, description="是否启")
class KnowledgeUpdateSchema(KnowledgeCreateSchema):
"""更新知识库参数"""
class KnowledgeOutSchema(KnowledgeCreateSchema, BaseSchema, UserBySchema):
"""知识库详情"""
model_config = ConfigDict(from_attributes=True)
class KnowledgeQueryParam:
"""知识库查询参数"""
def __init__(
self,
name: str | None = Query(None, description="知识库名称"),
is_active: bool | None = Query(None, description="是否启用"),
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 = (QueueEnum.like.value, name)
self.is_active = (QueueEnum.eq.value, is_active)
self.created_id = (QueueEnum.eq.value, created_id)
self.updated_id = (QueueEnum.eq.value, updated_id)
if created_time and len(created_time) == 2:
self.created_time = (QueueEnum.between.value, (created_time[0], created_time[1]))
if updated_time and len(updated_time) == 2:
self.updated_time = (QueueEnum.between.value, (updated_time[0], updated_time[1]))
class KnowledgeDocumentCreateSchema(BaseModel):
"""创建知识库文档参数"""
knowledge_id: int = Field(..., description="知识库ID")
title: str = Field(..., max_length=200, description="文档标题")
content: str = Field(..., description="文档内容")
file_type: str = Field("text", max_length=50, description="文件类型")
file_path: str | None = Field(None, max_length=500, description="文件路径")
meta_data: dict[str, str] | None = Field(None, description="元数据")
class KnowledgeDocumentUpdateSchema(BaseModel):
"""更新知识库文档参数"""
title: str | None = Field(None, max_length=200, description="文档标题")
content: str | None = Field(None, description="文档内容")
meta_data: dict[str, str] | None = Field(None, description="元数据")
chunk_count: int | None = Field(None, description="分块数量")
is_indexed: bool | None = Field(None, description="是否已索引")
model_config = ConfigDict(extra="allow")
class KnowledgeDocumentOutSchema(KnowledgeDocumentCreateSchema, BaseSchema, UserBySchema):
"""知识库文档详情"""
chunk_count: int = Field(..., description="分块数量")
is_indexed: bool = Field(..., description="是否已索引")
model_config = ConfigDict(from_attributes=True)
class KnowledgeDocumentQueryParam:
"""知识库文档查询参数"""
def __init__(
self,
knowledge_id: int | None = Query(None, description="知识库ID"),
title: str | None = Query(None, description="文档标题"),
file_type: str | None = Query(None, description="文件类型"),
is_indexed: bool | None = Query(None, description="是否已索引"),
created_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="创建人"),
) -> None:
self.knowledge_id = (QueueEnum.eq.value, knowledge_id)
self.title = (QueueEnum.like.value, title)
self.file_type = (QueueEnum.eq.value, file_type)
self.is_indexed = (QueueEnum.eq.value, is_indexed)
self.created_id = (QueueEnum.eq.value, created_id)
if created_time and len(created_time) == 2:
self.created_time = (QueueEnum.between.value, (created_time[0], created_time[1]))
class RAGQuerySchema(BaseModel):
"""RAG检索查询参数"""
query: str = Field(..., description="检索查询")
knowledge_ids: list[int] = Field(..., description="知识库ID列表")
top_k: int = Field(3, ge=1, le=10, description="返回最相关的文档数量")
@@ -1,151 +1,304 @@
from collections.abc import AsyncGenerator
from typing import Any
from collections.abc import AsyncGenerator
from typing import Any
from langchain_core.documents import Document
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_openai import ChatOpenAI
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter
from app.api.v1.module_system.auth.schema import AuthSchema
from app.core.exceptions import CustomException
from app.config.setting import settings
from app.core.exceptions import CustomException
from app.core.logger import log
from .crud import McpCRUD
from .chroma import chroma_manager
from .crud import AgentConfigCRUD, KnowledgeCRUD, KnowledgeDocumentCRUD
from .schema import (
AgentConfigCreateSchema,
AgentConfigOutSchema,
AgentConfigSchema,
AgentConfigUpdateSchema,
ChatQuerySchema,
McpCreateSchema,
McpOutSchema,
McpQueryParam,
McpUpdateSchema,
KnowledgeCreateSchema,
KnowledgeDocumentCreateSchema,
KnowledgeDocumentOutSchema,
KnowledgeDocumentUpdateSchema,
KnowledgeOutSchema,
KnowledgeQueryParam,
KnowledgeUpdateSchema,
)
class McpService:
"""MCP服务层"""
class AgentConfigService:
"""智能体配置服务层"""
@classmethod
async def detail_service(cls, auth: AuthSchema, id: int) -> dict[str, Any]:
async def get_by_id_service(
cls, auth: AuthSchema, id: int
) -> dict[str, Any]:
"""
获取MCP服务器详情
获取智能体配置详情
参数:
- auth (AuthSchema): 认证信息模型
- id (int): MCP服务器ID
- id (int): 智能体配置ID
返回:
- dict[str, Any]: MCP服务器详情字典
- dict[str, Any]: 智能体配置详情字典
"""
obj = await McpCRUD(auth).get_by_id_crud(id=id)
obj = await AgentConfigCRUD(auth).get_by_id_crud(id=id)
if not obj:
raise CustomException(msg="MCP 服务器不存在")
return McpOutSchema.model_validate(obj).model_dump()
raise CustomException(msg="智能体配置不存在")
return AgentConfigOutSchema.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]]:
async def get_default_service(
cls, auth: AuthSchema
) -> dict[str, Any]:
"""
列表查询MCP服务器
获取默认智能体配置
参数:
- auth (AuthSchema): 认证信息模型
- search (McpQueryParam | None): 查询参数模型
- order_by (list[dict[str, str]] | None): 排序参数列表
返回:
- list[dict[str, Any]]: MCP服务器详情字典列表
- dict[str, Any]: 智能体配置详情字典
"""
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]
obj = await AgentConfigCRUD(auth).get_default_crud()
if not obj:
raise CustomException(msg="默认智能体配置不存在")
return AgentConfigOutSchema.model_validate(obj).model_dump()
@classmethod
async def create_service(cls, auth: AuthSchema, data: McpCreateSchema) -> dict[str, Any]:
async def get_list_service(
cls, auth: AuthSchema, query_params: Any
) -> dict[str, Any]:
"""
创建MCP服务器
列表查询智能体配置
参数:
- auth (AuthSchema): 认证信息模型
- data (McpCreateSchema): 创建MCP服务器模型
- query_params (Any): 查询参数
返回:
- dict[str, Any]: 创建的MCP服务器详情字典
- dict[str, Any]: 智能体配置列表字典
"""
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()
search = {}
if query_params.name:
search["name"] = query_params.name
if query_params.provider:
search["provider"] = query_params.provider
if query_params.is_default is not None:
search["is_default"] = query_params.is_default
if query_params.is_active is not None:
search["is_active"] = query_params.is_active
if query_params.created_id:
search["created_id"] = query_params.created_id
if query_params.updated_id:
search["updated_id"] = query_params.updated_id
if hasattr(query_params, "created_time") and query_params.created_time:
search["created_time"] = query_params.created_time
if hasattr(query_params, "updated_time") and query_params.updated_time:
search["updated_time"] = query_params.updated_time
objs = await AgentConfigCRUD(auth).get_list_crud(
search=search, order_by=[{"id": "desc"}]
)
data = [AgentConfigOutSchema.model_validate(obj).model_dump() for obj in objs]
return {"total": len(data), "data": data}
@classmethod
async def create_service(
cls, auth: AuthSchema, data: AgentConfigCreateSchema
) -> dict[str, Any]:
"""
创建智能体配置
参数:
- auth (AuthSchema): 认证信息模型
- data (AgentConfigCreateSchema): 创建智能体配置模型
返回:
- dict[str, Any]: 创建的智能体配置字典
"""
if data.is_default:
existing_default = await AgentConfigCRUD(auth).get_default_crud()
if existing_default:
update_data = AgentConfigUpdateSchema.model_construct(is_default=False)
await AgentConfigCRUD(auth).update_crud(id=existing_default.id, data=update_data)
obj = await AgentConfigCRUD(auth).create_crud(data=data)
if not obj:
raise CustomException(msg="创建智能体配置失败")
return AgentConfigOutSchema.model_validate(obj).model_dump()
@classmethod
async def update_service(
cls, auth: AuthSchema, id: int, data: McpUpdateSchema
cls, auth: AuthSchema, id: int, data: AgentConfigUpdateSchema
) -> dict[str, Any]:
"""
更新MCP服务器
更新智能体配置
参数:
- auth (AuthSchema): 认证信息模型
- id (int): MCP服务器ID
- data (McpUpdateSchema): 更新MCP服务器模型
- id (int): 智能体配置ID
- data (AgentConfigUpdateSchema): 更新智能体配置模型
返回:
- dict[str, Any]: 更新的MCP服务器详情字典
- dict[str, Any]: 更新的智能体配置字典
"""
obj = await McpCRUD(auth).get_by_id_crud(id=id)
obj = await AgentConfigCRUD(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()
raise CustomException(msg="智能体配置不存在")
if data.is_default:
existing_default = await AgentConfigCRUD(auth).get_default_crud()
if existing_default and existing_default.id != id:
update_data = AgentConfigUpdateSchema.model_construct(is_default=False)
await AgentConfigCRUD(auth).update_crud(id=existing_default.id, data=update_data)
obj = await AgentConfigCRUD(auth).update_crud(id=id, data=data)
if not obj:
raise CustomException(msg="更新智能体配置失败")
return AgentConfigOutSchema.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列表
- ids (list[int]): 智能体配置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)
await AgentConfigCRUD(auth).delete_crud(ids=ids)
class RAGService:
"""RAG 检索增强生成服务层"""
@classmethod
async def chat_query(cls, query: ChatQuerySchema) -> AsyncGenerator[str, Any]:
async def retrieve_documents(
cls,
query: str,
knowledge_ids: list[int],
top_k: int = 3,
auth: AuthSchema | None = None,
) -> list[Document]:
"""
从知识库中检索相关文档
参数:
- query (str): 查询文本
- knowledge_ids (list[int]): 知识库ID列表
- top_k (int): 返回最相关的文档数量
- auth (AuthSchema | None): 认证信息模型
返回:
- list[Document]: 相关文档列表
"""
embeddings = OpenAIEmbeddings(
api_key=lambda: settings.OPENAI_API_KEY,
base_url=settings.OPENAI_BASE_URL,
)
query_embedding = await embeddings.aembed_query(query)
results = chroma_manager.query_documents(
query_embeddings=[query_embedding],
n_results=top_k,
where={"knowledge_id": {"$in": knowledge_ids}},
)
documents = []
if results and "documents" in results and len(results["documents"]) > 0:
for i, doc_content in enumerate(results["documents"][0]):
metadata = results["metadatas"][0][i] if "metadatas" in results and len(results["metadatas"]) > 0 else {}
documents.append(
Document(
page_content=doc_content,
metadata=metadata,
)
)
return documents
@classmethod
def format_context(cls, documents: list[Document]) -> str:
"""
格式化检索到的文档为上下文
参数:
- documents (list[Document]): 文档列表
返回:
- str: 格式化的上下文文本
"""
if not documents:
return "没有找到相关的知识库内容。"
context_parts = []
for i, doc in enumerate(documents, 1):
title = doc.metadata.get("title", "未知文档")
content = doc.page_content
context_parts.append(f"[文档 {i}] {title}\n{content}")
return "\n\n".join(context_parts)
class AgentService:
"""智能体服务层"""
@classmethod
async def chat_query(
cls, query: ChatQuerySchema, config: AgentConfigSchema | None = None
) -> AsyncGenerator[str, Any]:
"""
处理聊天查询
参数:
- query (ChatQuerySchema): 聊天查询模型
- config (AgentConfigSchema | None): 智能体配置模型
返回:
- AsyncGenerator[str, None]: 异步生成器,每次返回一个聊天响应
"""
# 创建MCP客户端实例
lll_model = ChatOpenAI(
api_key=lambda: settings.OPENAI_API_KEY,
model=settings.OPENAI_MODEL,
base_url=settings.OPENAI_BASE_URL,
temperature=0.7,
streaming=True,
)
if config is None:
config = AgentConfigSchema(
provider="openai",
model=settings.OPENAI_MODEL,
api_key=settings.OPENAI_API_KEY,
base_url=settings.OPENAI_BASE_URL,
temperature=0.7,
system_prompt="你是一个有用的AI助手,可以帮助用户回答问题和提供帮助。请用中文回答用户的问题。",
)
system_prompt = (
"""你是一个有用的AI助手,可以帮助用户回答问题和提供帮助。请用中文回答用户的问题。"""
system_prompt = config.system_prompt
if query.knowledge_ids:
retrieved_docs = await RAGService.retrieve_documents(
query=query.message,
knowledge_ids=query.knowledge_ids,
top_k=3,
)
context = RAGService.format_context(retrieved_docs)
system_prompt = f"""{config.system_prompt}
以下是从知识库中检索到的相关内容请参考这些内容回答用户的问题
{context}
如果检索到的内容与问题无关请忽略这些内容直接回答用户的问题"""
llm = ChatOpenAI(
api_key=lambda: config.api_key,
model=config.model,
base_url=config.base_url,
temperature=config.temperature,
streaming=True,
)
messages = [
@@ -154,13 +307,12 @@ class McpService:
]
try:
# 使用LangChain的流式响应
async for chunk in lll_model.astream(messages):
async for chunk in llm.astream(messages):
yield chunk.text
except Exception as e:
log.debug(f"关闭AIClient时发生异常(预期行为,服务可能正在关闭): {e}")
log.debug(f"关闭 LLM 客户端时发生异常(预期行为,服务可能正在关闭): {e}")
status_code = getattr(e, "status_code", None)
body = getattr(e, "body", None)
message = None
@@ -178,36 +330,400 @@ class McpService:
text = str(e)
msg = message or text
# 特定错误映射
# 欠费/账户状态异常
if (
(error_code == "Arrearage")
or (error_type == "Arrearage")
or ("in good standing" in (msg or ""))
):
raise ValueError("账户欠费或结算异常,访问被拒绝。请检查账号状态或更换有效的 API Key。")
# 鉴权失败
raise ValueError(
"账户欠费或结算异常,访问被拒绝。请检查账号状态或更换有效的 API Key。"
)
if status_code == 401 or "invalid api key" in msg.lower():
raise ValueError("鉴权失败,API Key 无效或已过期。请检查系统配置中的 API Key。")
# 权限不足或被拒绝
if status_code == 403 or error_type in {
"PermissionDenied",
"permission_denied",
}:
raise ValueError("访问被拒绝,权限不足或账号受限。请检查账户权限设置。")
# 配额不足或限流
if status_code == 429 or error_type in {
"insufficient_quota",
"rate_limit_exceeded",
}:
raise ValueError("请求过于频繁或配额已用尽。请稍后重试或提升账户配额。")
# 客户端错误
if status_code == 400:
raise ValueError(f"请求参数错误或服务拒绝:{message or '请检查输入内容。'}")
# 服务端错误
if status_code in {500, 502, 503, 504}:
raise ValueError("服务暂时不可用,请稍后重试。")
# 默认兜底
raise CustomException(f"处理您的请求时出现错误:{msg}")
class KnowledgeService:
"""知识库服务层"""
@classmethod
async def detail_service(cls, auth: AuthSchema, id: int) -> dict[str, Any]:
"""
获取知识库详情
参数:
- auth (AuthSchema): 认证信息模型
- id (int): 知识库ID
返回:
- dict[str, Any]: 知识库详情字典
"""
obj = await KnowledgeCRUD(auth).get_by_id_crud(id=id)
if not obj:
raise CustomException(msg="知识库不存在")
return KnowledgeOutSchema.model_validate(obj).model_dump()
@classmethod
async def list_service(
cls,
auth: AuthSchema,
search: KnowledgeQueryParam | None = None,
order_by: list[dict[str, str]] | None = None,
) -> list[dict[str, Any]]:
"""
列表查询知识库
参数:
- auth (AuthSchema): 认证信息模型
- search (KnowledgeQueryParam | None): 查询参数模型
- order_by (list[dict[str, str]] | None): 排序参数列表
返回:
- list[dict[str, Any]]: 知识库详情字典列表
"""
search_dict = search.__dict__ if search else None
obj_list = await KnowledgeCRUD(auth).get_list_crud(search=search_dict, order_by=order_by)
return [KnowledgeOutSchema.model_validate(obj).model_dump() for obj in obj_list]
@classmethod
async def create_service(cls, auth: AuthSchema, data: KnowledgeCreateSchema) -> dict[str, Any]:
"""
创建知识库
参数:
- auth (AuthSchema): 认证信息模型
- data (KnowledgeCreateSchema): 创建知识库模型
返回:
- dict[str, Any]: 创建的知识库详情字典
"""
obj = await KnowledgeCRUD(auth).get_by_name_crud(name=data.name)
if obj:
raise CustomException(msg="创建失败,知识库已存在")
obj = await KnowledgeCRUD(auth).create_crud(data=data)
return KnowledgeOutSchema.model_validate(obj).model_dump()
@classmethod
async def update_service(
cls, auth: AuthSchema, id: int, data: KnowledgeUpdateSchema
) -> dict[str, Any]:
"""
更新知识库
参数:
- auth (AuthSchema): 认证信息模型
- id (int): 知识库ID
- data (KnowledgeUpdateSchema): 更新知识库模型
返回:
- dict[str, Any]: 更新的知识库详情字典
"""
obj = await KnowledgeCRUD(auth).get_by_id_crud(id=id)
if not obj:
raise CustomException(msg="更新失败,该数据不存在")
exist_obj = await KnowledgeCRUD(auth).get_by_name_crud(name=data.name)
if exist_obj and exist_obj.id != id:
raise CustomException(msg="更新失败,知识库名称重复")
obj = await KnowledgeCRUD(auth).update_crud(id=id, data=data)
return KnowledgeOutSchema.model_validate(obj).model_dump()
@classmethod
async def delete_service(cls, auth: AuthSchema, ids: list[int]) -> None:
"""
批量删除知识库
参数:
- auth (AuthSchema): 认证信息模型
- ids (list[int]): 知识库ID列表
返回:
- None
"""
if len(ids) < 1:
raise CustomException(msg="删除失败,删除对象不能为空")
for id in ids:
obj = await KnowledgeCRUD(auth).get_by_id_crud(id=id)
if not obj:
raise CustomException(msg="删除失败,该数据不存在")
await KnowledgeCRUD(auth).delete_crud(ids=ids)
@classmethod
async def document_detail_service(cls, auth: AuthSchema, id: int) -> dict[str, Any]:
"""
获取知识库文档详情
参数:
- auth (AuthSchema): 认证信息模型
- id (int): 文档ID
返回:
- dict[str, Any]: 文档详情字典
"""
obj = await KnowledgeDocumentCRUD(auth).get_by_id_crud(id=id)
if not obj:
raise CustomException(msg="文档不存在")
return KnowledgeDocumentOutSchema.model_validate(obj).model_dump()
@classmethod
async def document_list_service(
cls,
auth: AuthSchema,
search: Any | None = None,
order_by: list[dict[str, str]] | None = None,
) -> list[dict[str, Any]]:
"""
列表查询知识库文档
参数:
- auth (AuthSchema): 认证信息模型
- search (Any | None): 查询参数模型
- order_by (list[dict[str, str]] | None): 排序参数列表
返回:
- list[dict[str, Any]]: 文档详情字典列表
"""
search_dict = search.__dict__ if search else None
obj_list = await KnowledgeDocumentCRUD(auth).get_list_crud(
search=search_dict, order_by=order_by
)
return [KnowledgeDocumentOutSchema.model_validate(obj).model_dump() for obj in obj_list]
@classmethod
async def document_create_service(
cls, auth: AuthSchema, data: KnowledgeDocumentCreateSchema
) -> dict[str, Any]:
"""
创建知识库文档
参数:
- auth (AuthSchema): 认证信息模型
- data (KnowledgeDocumentCreateSchema): 创建文档模型
返回:
- dict[str, Any]: 创建的文档详情字典
"""
knowledge = await KnowledgeCRUD(auth).get_by_id_crud(id=data.knowledge_id)
if not knowledge:
raise CustomException(msg="创建失败,知识库不存在")
obj = await KnowledgeDocumentCRUD(auth).create_crud(data=data)
if not obj:
raise CustomException(msg="创建文档失败")
try:
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=knowledge.chunk_size,
chunk_overlap=knowledge.chunk_overlap,
)
chunks = text_splitter.split_text(data.content)
if settings.OPENAI_API_KEY and settings.OPENAI_BASE_URL:
try:
embeddings = OpenAIEmbeddings(
model=settings.OPENAI_MODEL,
)
chunk_embeddings = await embeddings.aembed_documents(chunks)
ids = []
documents = []
metadatas = []
for i, chunk in enumerate(chunks):
chunk_id = f"{obj.id}_chunk_{i}"
ids.append(chunk_id)
documents.append(chunk)
metadatas.append(
{
"document_id": obj.id,
"knowledge_id": obj.knowledge_id,
"title": obj.title,
"chunk_index": i,
}
)
chroma_manager.add_documents(
ids=ids,
embeddings=chunk_embeddings,
documents=documents,
metadatas=metadatas,
)
update_data = KnowledgeDocumentUpdateSchema.model_construct(
chunk_count=len(chunks), is_indexed=True
)
except Exception as e:
log.warning(f"嵌入生成失败,使用虚拟嵌入: {e!s}")
chunk_embeddings = [[0.0] * 1536 for _ in chunks]
ids = []
documents = []
metadatas = []
for i, chunk in enumerate(chunks):
chunk_id = f"{obj.id}_chunk_{i}"
ids.append(chunk_id)
documents.append(chunk)
metadatas.append(
{
"document_id": obj.id,
"knowledge_id": obj.knowledge_id,
"title": obj.title,
"chunk_index": i,
}
)
chroma_manager.add_documents(
ids=ids,
embeddings=chunk_embeddings,
documents=documents,
metadatas=metadatas,
)
update_data = KnowledgeDocumentUpdateSchema.model_construct(
chunk_count=len(chunks), is_indexed=True
)
else:
log.info("未配置嵌入模型,跳过向量索引")
update_data = KnowledgeDocumentUpdateSchema.model_construct(
chunk_count=len(chunks), is_indexed=False
)
await KnowledgeDocumentCRUD(auth).update_crud(id=obj.id, data=update_data)
updated_obj = await KnowledgeDocumentCRUD(auth).get_by_id_crud(id=obj.id)
return KnowledgeDocumentOutSchema.model_validate(updated_obj).model_dump()
except CustomException:
raise
except Exception as e:
log.error(f"创建知识库文档时发生错误: {e!s}")
raise CustomException(msg=f"创建知识库文档失败: {e!s}")
@classmethod
async def document_update_service(
cls, auth: AuthSchema, id: int, data: KnowledgeDocumentUpdateSchema
) -> dict[str, Any]:
"""
更新知识库文档
参数:
- auth (AuthSchema): 认证信息模型
- id (int): 文档ID
- data (KnowledgeDocumentUpdateSchema): 更新文档模型
返回:
- dict[str, Any]: 更新的文档详情字典
"""
obj = await KnowledgeDocumentCRUD(auth).get_by_id_crud(id=id)
if not obj:
raise CustomException(msg="更新失败,该数据不存在")
content_changed = data.content is not None and data.content != obj.content
if content_changed:
chunk_ids = [f"{obj.id}_chunk_{i}" for i in range(obj.chunk_count or 0)]
chroma_manager.delete_documents(ids=chunk_ids)
obj = await KnowledgeDocumentCRUD(auth).update_crud(id=id, data=data)
if not obj:
raise CustomException(msg="更新文档失败")
if content_changed and obj.content:
knowledge = await KnowledgeCRUD(auth).get_by_id_crud(id=obj.knowledge_id)
if knowledge:
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=knowledge.chunk_size,
chunk_overlap=knowledge.chunk_overlap,
)
chunks = text_splitter.split_text(obj.content)
if settings.OPENAI_API_KEY and settings.OPENAI_BASE_URL:
try:
embeddings = OpenAIEmbeddings(
api_key=lambda: settings.OPENAI_API_KEY,
base_url=settings.OPENAI_BASE_URL,
)
chunk_embeddings = await embeddings.aembed_documents(chunks)
ids = []
documents = []
metadatas = []
for i, chunk in enumerate(chunks):
chunk_id = f"{obj.id}_chunk_{i}"
ids.append(chunk_id)
documents.append(chunk)
metadatas.append(
{
"document_id": obj.id,
"knowledge_id": obj.knowledge_id,
"title": obj.title,
"chunk_index": i,
}
)
chroma_manager.add_documents(
ids=ids,
embeddings=chunk_embeddings,
documents=documents,
metadatas=metadatas,
)
update_data = KnowledgeDocumentUpdateSchema.model_construct(
chunk_count=len(chunks), is_indexed=True
)
except Exception as e:
log.warning(f"嵌入生成失败,跳过向量索引: {e!s}")
update_data = KnowledgeDocumentUpdateSchema.model_construct(
chunk_count=len(chunks), is_indexed=False
)
else:
log.info("未配置嵌入模型,跳过向量索引")
update_data = KnowledgeDocumentUpdateSchema.model_construct(
chunk_count=len(chunks), is_indexed=False
)
obj = await KnowledgeDocumentCRUD(auth).update_crud(id=obj.id, data=update_data)
return KnowledgeDocumentOutSchema.model_validate(obj).model_dump()
@classmethod
async def document_delete_service(cls, auth: AuthSchema, ids: list[int]) -> None:
"""
批量删除知识库文档
参数:
- auth (AuthSchema): 认证信息模型
- ids (list[int]): 文档ID列表
返回:
- None
"""
if len(ids) < 1:
raise CustomException(msg="删除失败,删除对象不能为空")
chunk_ids = []
for id in ids:
obj = await KnowledgeDocumentCRUD(auth).get_by_id_crud(id=id)
if not obj:
raise CustomException(msg="删除失败,该数据不存在")
if obj.chunk_count:
chunk_ids.extend([f"{obj.id}_chunk_{i}" for i in range(obj.chunk_count)])
if chunk_ids:
chroma_manager.delete_documents(ids=chunk_ids)
await KnowledgeDocumentCRUD(auth).delete_crud(ids=ids)
@@ -4,12 +4,12 @@ from app.core.logger import log
from app.core.router_class import OperationLogRoute
from .schema import ChatQuerySchema
from .service import McpService
from .service import AgentService
WS_AI = APIRouter(
route_class=OperationLogRoute,
prefix="/application/ai",
tags=["MCP智能助手WebSocket"],
tags=["智能助手WebSocket"],
)
@@ -20,15 +20,18 @@ async def websocket_chat_controller(
"""
WebSocket聊天接口
支持两种消息格式
1. 纯文本直接发送消息内容
2. JSON格式{"message": "消息内容", "knowledge_ids": [1, 2], "agent_config_id": 1}
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)):
async for chunk in AgentService.chat_query(query=ChatQuerySchema(message=data, knowledge_ids=[], agent_config_id=None)):
if chunk:
await websocket.send_text(chunk)
except Exception as e:
@@ -38,7 +41,6 @@ async def websocket_chat_controller(
log.error(f"WebSocket聊天出错: {e!s}")
finally:
try:
# 检查WebSocket连接状态,避免重复关闭已关闭的连接
if websocket.client_state != websocket.client_state.DISCONNECTED:
await websocket.close()
except Exception as e: