From 6f4d173e93e788c592bf3c32f12b0d2bb1c53a35 Mon Sep 17 00:00:00 2001 From: Stephen Curry Date: Mon, 28 Apr 2025 09:41:11 +0800 Subject: [PATCH 1/4] 630 MCP api dev --- apps/entities/appcenter.py | 21 +- apps/entities/enum_var.py | 34 ++- apps/entities/flow.py | 4 +- apps/entities/mcp.py | 90 ++++++++ apps/entities/model.py | 41 ++++ apps/entities/pool.py | 74 +++++- apps/entities/prompt.py | 31 +++ apps/entities/request_data.py | 24 +- apps/entities/response_data.py | 173 +++++++++++++- apps/exceptions.py | 5 + apps/manager/appcenter.py | 262 +++++++++++++-------- apps/manager/mcp_service.py | 233 +++++++++++++++++++ apps/manager/model.py | 222 ++++++++++++++++++ apps/manager/prompt.py | 163 +++++++++++++ apps/routers/appcenter.py | 20 +- apps/routers/mcp_service.py | 252 ++++++++++++++++++++ apps/routers/model.py | 304 +++++++++++++++++++++++++ apps/routers/prompt.py | 177 ++++++++++++++ apps/scheduler/pool/check.py | 23 +- apps/scheduler/pool/loader/app.py | 124 ++++++---- apps/scheduler/pool/loader/mcp.py | 125 ++++++++++ apps/scheduler/pool/loader/metadata.py | 88 ++++++- apps/scheduler/pool/loader/model.py | 109 +++++++++ apps/scheduler/pool/loader/prompt.py | 109 +++++++++ apps/scheduler/pool/pool.py | 2 +- 25 files changed, 2522 insertions(+), 188 deletions(-) create mode 100644 apps/entities/mcp.py create mode 100644 apps/entities/model.py create mode 100644 apps/entities/prompt.py create mode 100644 apps/manager/mcp_service.py create mode 100644 apps/manager/model.py create mode 100644 apps/manager/prompt.py create mode 100644 apps/routers/mcp_service.py create mode 100644 apps/routers/model.py create mode 100644 apps/routers/prompt.py create mode 100644 apps/scheduler/pool/loader/mcp.py create mode 100644 apps/scheduler/pool/loader/model.py create mode 100644 apps/scheduler/pool/loader/prompt.py diff --git a/apps/entities/appcenter.py b/apps/entities/appcenter.py index bf021c82..e4d7902f 100644 --- a/apps/entities/appcenter.py +++ b/apps/entities/appcenter.py @@ -6,13 +6,16 @@ Copyright (c) Huawei Technologies Co., Ltd. 2024-2025. All rights reserved. from pydantic import BaseModel, Field -from apps.entities.enum_var import PermissionType +from apps.entities.enum_var import PermissionType, AppType +from apps.entities.model import ModelMetadata +from apps.entities.mcp import MCPServiceMetadata class AppCenterCardItem(BaseModel): """应用中心卡片数据结构""" app_id: str = Field(..., alias="appId", description="应用ID") + app_type: AppType = Field(..., alias="appType", description="应用类型") icon: str = Field(..., description="应用图标") name: str = Field(..., description="应用名称") description: str = Field(..., description="应用简介") @@ -55,13 +58,23 @@ class AppFlowInfo(BaseModel): class AppData(BaseModel): """应用信息数据结构""" + app_type: AppType = Field(..., alias="type", description="应用类型") icon: str = Field(default="", description="图标") name: str = Field(..., max_length=20, description="应用名称") description: str = Field(..., max_length=150, description="应用简介") - links: list[AppLink] = Field(default=[], description="相关链接", max_length=5) - first_questions: list[str] = Field( - default=[], alias="recommendedQuestions", description="推荐问题", max_length=3) history_len: int = Field(3, alias="dialogRounds", ge=1, le=10, description="对话轮次(1~10)") permission: AppPermissionData = Field( default_factory=lambda: AppPermissionData(authorizedUsers=None), description="权限配置") + + # FLow APP + links: list[AppLink] = Field(default=[], description="相关链接", max_length=5) + first_questions: list[str] = Field( + default=[], alias="recommendedQuestions", description="推荐问题", max_length=3) + workflows: list[AppFlowInfo] = Field(default=[], description="工作流信息列表") + + # Agent APP + model: ModelMetadata = Field(default=None, description="模型选择") + prompt: str = Field(default="", description="提示词") + mcp_service: list[MCPServiceMetadata] = Field(default=[], alias="mcpService", description="MCP服务") + knowledge: list[str] = Field(default=None, description="知识库") diff --git a/apps/entities/enum_var.py b/apps/entities/enum_var.py index 281d53e0..e31e3f1d 100644 --- a/apps/entities/enum_var.py +++ b/apps/entities/enum_var.py @@ -57,9 +57,12 @@ class CallType(str, Enum): class MetadataType(str, Enum): """元数据类型""" - + MCP_SERVICE = "mcp_service" + AGENT_APP = "agent" SERVICE = "service" - APP = "app" + FLOW_APP = "flow" + MODEL = "model" + PROMPT = "prompt" class EdgeType(str, Enum): @@ -106,9 +109,8 @@ class SearchType(str, Enum): """搜索类型""" ALL = "all" - NAME = "name" - DESCRIPTION = "description" - AUTHOR = "author" + AGENT = "agent" + FLOW = "flow" class HTTPMethod(str, Enum): @@ -155,3 +157,25 @@ class CommentType(str, Enum): LIKE = "liked" DISLIKE = "disliked" NONE = "none" + + +class MCPTransmitProto(str, Enum): + """MCP传输方式""" + + STDIO = "Stdio" + SSE = "SSE" + STREAMABLE = "Streamable" + + +class MCPServiceToolsArgsType(str, Enum): + """MCPService tool参数数据类型""" + STRING = "string" + DOUBLE = "double" + INTEGER = "integer" + BOOLEAN = "boolean" + + +class AppType(str, Enum): + """应用中心应用类型""" + FLOW = "flow" + AGENT = "agent" diff --git a/apps/entities/flow.py b/apps/entities/flow.py index 22f278b5..74e8e9cc 100644 --- a/apps/entities/flow.py +++ b/apps/entities/flow.py @@ -13,6 +13,7 @@ from apps.entities.enum_var import ( EdgeType, MetadataType, PermissionType, + AppType, ) from apps.entities.flow_topology import PositionItem @@ -133,7 +134,8 @@ class AppFlow(BaseModel): class AppMetadata(MetadataBase): """App的元数据""" - type: MetadataType = MetadataType.APP + type: MetadataType = MetadataType.FLOW_APP + app_type: AppType = AppType.FLOW published: bool = Field(description="是否发布", default=False) links: list[AppLink] = Field(description="相关链接", default=[]) first_questions: list[str] = Field(description="首次提问", default=[]) diff --git a/apps/entities/mcp.py b/apps/entities/mcp.py new file mode 100644 index 00000000..76fffecf --- /dev/null +++ b/apps/entities/mcp.py @@ -0,0 +1,90 @@ +""" +App、Flow和Service等外置配置数据结构 + +Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. +""" + +from typing import Any + +from pydantic import BaseModel, Field + +from apps.entities.model import ModelMetadata +from apps.entities.enum_var import ( + MCPTransmitProto, + MetadataType, + MCPServiceToolsArgsType, + PermissionType, + AppType, +) + + +class MCPMetadataBase(BaseModel): + """ + MCPService或MCPApp的元数据 + + 注意:hash字段在save和load的时候exclude + """ + + type: MetadataType = Field(description="元数据类型") + id: str = Field(description="元数据ID") + icon: str = Field(description="图标", default="") + name: str = Field(description="元数据名称") + description: str = Field(description="元数据描述") + author: str = Field(description="创建者的用户名") + hashes: dict[str, str] | None = Field(description="资源(App、Service等)下所有文件的hash值", default=None) + + +class MCPServiceConfig(BaseModel): + """MCPService的API配置""" + + type: MCPTransmitProto = Field( + default=MCPTransmitProto.STDIO, + alias="transmitProto", + description="传输协议(Stdio/SSE/Streamable)", + ) + config: dict[str, Any] = Field(..., description="对应MCP的配置") + + +class MCPServiceToolsArgs(BaseModel): + """MCP Service中tool参数信息""" + name: str = Field(description="Tool参数名称") + description: str = Field(description="Tool参数描述") + type: MCPServiceToolsArgsType = Field(description="Tool参数类型") + + +class MCPServiceToolsdata(BaseModel): + """MCP Service中tool信息""" + name: str = Field(description="Tool名称") + description: str = Field(description="Tool功能描述") + input_args: list[MCPServiceToolsArgs] = Field(description="Tool参数列表") + output_args: list[MCPServiceToolsArgs] = Field(description="Tool参数列表") + + +class MCPServiceMetadata(MCPMetadataBase): + """MCPService的元数据""" + + type: MetadataType = MetadataType.MCP_SERVICE + config: MCPServiceConfig = Field(description="MCP服务配置") + tools: list[MCPServiceToolsdata] = Field(description="MCP服务Tools列表") + + +class Permission(BaseModel): + """权限配置""" + + type: PermissionType = Field(description="权限类型", default=PermissionType.PRIVATE) + users: list[str] = Field(description="可访问的用户列表", default=[]) + + +class AgentAppMetadata(MCPMetadataBase): + """智能体App的元数据""" + + type: MetadataType = MetadataType.AGENT_APP + app_type: AppType = AppType.AGENT + published: bool = Field(description="是否发布", default=False) + history_len: int = Field(description="对话轮次", default=3, le=10) + permission: Permission | None = Field(description="应用权限配置", default=None) + model: ModelMetadata = Field(default=None, description="模型选择") + prompt: str = Field(default="", description="提示词") + mcp_service: list[MCPServiceMetadata] = Field(default=[], alias="mcpService", description="MCP服务") + # TODO 知识库怎么处理 + knowledge: list[str] = Field(default=None, description="知识库") diff --git a/apps/entities/model.py b/apps/entities/model.py new file mode 100644 index 00000000..647a630e --- /dev/null +++ b/apps/entities/model.py @@ -0,0 +1,41 @@ +""" +模型配置相关 API 基础数据结构定义 + +Copyright (c) Huawei Technologies Co., Ltd. 2024-2025. All rights reserved. +""" + +from pydantic import BaseModel, Field + + +class ModelCenterCardItem(BaseModel): + """模型配置卡片数据结构""" + + model_id: str = Field(..., alias="modelId", description="模型ID") + icon: str = Field(description="图标", default="") + model: str = Field(..., description="模型") + + +class ModelData(BaseModel): + """模型信息数据结构""" + + provider: str = Field(..., description="模型供应商") + icon: str = Field(default="", description="图标") + url: str = Field(..., description="链接地址", pattern=r"^(https|http)://.*$") + model: str = Field(..., description="模型") + api_key: str = Field(..., alias="apiKey", description="模型API密钥") + max_token: int = Field(..., alias="maxToken", description="最大 token 数量限制") + + +class ModelMetadata(ModelData): + """Model的元数据""" + + id: str = Field(..., description="模型ID") + author: str = Field(description="创建者的用户名") + hashes: dict[str, str] | None = Field(description="模型资源下所有文件的hash值", default=None) + + +class ProviderCardItem(BaseModel): + """供应商卡片数据结构""" + + provider: str = Field(..., description="供应商名称") + description: str = Field(default="", description="供应商介绍") diff --git a/apps/entities/pool.py b/apps/entities/pool.py index 807a55a3..d1c2428d 100644 --- a/apps/entities/pool.py +++ b/apps/entities/pool.py @@ -10,8 +10,10 @@ from typing import Any from pydantic import BaseModel, Field from apps.entities.appcenter import AppLink -from apps.entities.enum_var import CallType, PermissionType +from apps.entities.enum_var import CallType, PermissionType, AppType from apps.entities.flow import AppFlow, Permission +from apps.entities.mcp import MCPServiceConfig, MCPServiceToolsdata, MCPServiceMetadata +from apps.entities.model import ModelMetadata class BaseData(BaseModel): @@ -43,6 +45,20 @@ class ServicePool(BaseData): hashes: dict[str, str] = Field(description="服务关联的 OpenAPI YAML 和元数据文件哈希") +class MCPServicePool(BaseData): + """ + 外部服务信息 + + collection: service + """ + + author: str = Field(description="作者的用户ID") + icon: str = Field(description="图标", default="") + config: MCPServiceConfig = Field(description="MCP服务配置") + hashes: dict[str, str] = Field(description="MCP服务关联的元数据文件哈希") + tools: list[MCPServiceToolsdata] = Field(description="MCP服务Tools列表") + + class CallPool(BaseData): """ Call信息 @@ -102,12 +118,60 @@ class AppPool(BaseData): """ author: str = Field(description="作者的用户ID") - type: str = Field(description="应用类型", default="default") + type: str = Field(description="类型", default="default") + app_type: AppType = Field(description="应用类型") icon: str = Field(description="应用图标", default="") published: bool = Field(description="是否发布", default=False) - links: list[AppLink] = Field(description="相关链接", default=[]) - first_questions: list[str] = Field(description="推荐问题", default=[]) history_len: int = Field(3, ge=1, le=10, description="对话轮次(1~10)") permission: Permission = Field(description="应用权限配置", default=Permission()) - flows: list[AppFlow] = Field(description="Flow列表", default=[]) hashes: dict[str, str] = Field(description="关联文件的hash值", default={}) + + # Flow APP + links: list[AppLink] = Field(description="相关链接", default=[]) + first_questions: list[str] = Field(description="推荐问题", default=[]) + flows: list[AppFlow] = Field(description="Flow列表", default=[]) + + # Agent APP + model: ModelMetadata = Field(default=None, description="模型选择") + prompt: str = Field(default="", description="提示词") + mcp_service: list[MCPServiceMetadata] = Field(default=[], alias="mcpService", description="MCP服务") + knowledge: list[str] = Field(default=None, description="知识库") + + +class ModelPool(BaseData): + """ + 模型信息 + + collection: model + """ + + author: str = Field(description="作者的用户ID") + icon: str = Field(default="", description="图标") + url: str = Field(..., description="链接地址", pattern=r"^(https|http)://.*$") + model: str = Field(..., description="模型") + api_key: str = Field(..., alias="apiKey", description="模型API密钥") + max_token: int = Field(..., alias="maxToken", description="最大 token 数量限制") + + +class ProviderModelPool(BaseModel): + """ + 供应商对应的模型信息 + + collection: model + """ + + provider: str = Field(..., description="模型供应商") + model: str = Field(..., description="模型") + + +class PromptPool(BaseData): + """ + 提示词信息 + + collection: prompt + """ + + author: str = Field(description="作者的用户ID") + name: str = Field(..., description="提示词名称") + description: str = Field(description="提示词描述", default="") + prompt: str = Field(default="", description="提示词") diff --git a/apps/entities/prompt.py b/apps/entities/prompt.py new file mode 100644 index 00000000..ddaa46fa --- /dev/null +++ b/apps/entities/prompt.py @@ -0,0 +1,31 @@ +""" +Prompt相关 API 基础数据结构定义 + +Copyright (c) Huawei Technologies Co., Ltd. 2024-2025. All rights reserved. +""" + +from pydantic import BaseModel, Field + + +class PromptCardItem(BaseModel): + """Prompt卡片数据结构""" + + prompt_id: str = Field(..., alias="promptId", description="提示词ID") + name: str = Field(..., description="提示词名称") + description: str = Field(description="提示词描述", default="") + + +class PromptData(BaseModel): + """Prompt数据结构""" + + name: str = Field(..., description="提示词名称") + description: str = Field(description="提示词描述", default="") + prompt: str = Field(default="", description="提示词") + + +class PromptMetadata(PromptData): + """Prompt的元数据""" + + id: str = Field(..., description="提示词ID") + author: str = Field(description="创建者的用户名") + hashes: dict[str, str] | None = Field(description="提示词资源下所有文件的hash值", default=None) diff --git a/apps/entities/request_data.py b/apps/entities/request_data.py index 89e94ae4..ef21d7f7 100644 --- a/apps/entities/request_data.py +++ b/apps/entities/request_data.py @@ -12,7 +12,9 @@ from apps.common.config import Config from apps.entities.appcenter import AppData from apps.entities.enum_var import CommentType from apps.entities.flow_topology import FlowItem - +from apps.entities.mcp import MCPServiceConfig +from apps.entities.model import ModelData +from apps.entities.prompt import PromptData class RequestDataApp(BaseModel): """模型对话中包含的app信息""" @@ -93,6 +95,16 @@ class ModFavAppRequest(BaseModel): favorited: bool = Field(..., description="是否收藏") +class UpdateMCPServiceRequest(BaseModel): + """POST /api/mcpservice 请求数据结构""" + + service_id: str | None = Field(None, alias="serviceId", description="服务ID(更新时传递)") + icon: str = Field(description="图标", default="") + name: str = Field(..., description="MCP服务名称") + description: str = Field(..., description="MCP服务描述") + config: MCPServiceConfig = Field(..., description="MCP服务配置") + + class UpdateServiceRequest(BaseModel): """POST /api/service 请求数据结构""" @@ -152,3 +164,13 @@ class PutFlowReq(BaseModel): """创建/修改流拓扑结构""" flow: FlowItem + + +class CreateModelRequest(ModelData): + """POST /api/model 请求数据结构""" + model_id: str | None = Field(None, alias="modelId", description="模型ID") + + +class CreatePromptRequest(PromptData): + """POST /api/prompt 请求数据结构""" + prompt_id: str | None = Field(None, alias="promptId", description="提示词ID") diff --git a/apps/entities/response_data.py b/apps/entities/response_data.py index decb55dc..2c929843 100644 --- a/apps/entities/response_data.py +++ b/apps/entities/response_data.py @@ -9,6 +9,8 @@ from typing import Any from pydantic import BaseModel, Field from apps.entities.appcenter import AppCenterCardItem, AppData +from apps.entities.model import ModelCenterCardItem, ModelData, ProviderCardItem +from apps.entities.prompt import PromptCardItem from apps.entities.collection import Blacklist, Document from apps.entities.enum_var import DocumentStatus from apps.entities.flow_topology import ( @@ -17,6 +19,7 @@ from apps.entities.flow_topology import ( NodeServiceItem, PositionItem, ) +from apps.entities.mcp import MCPServiceConfig, MCPServiceToolsdata from apps.entities.record import RecordData from apps.entities.user import UserInfo @@ -298,7 +301,7 @@ class GetRecentAppListRsp(ResponseData): class ServiceCardItem(BaseModel): - """语义接口中心:服务卡片数据结构""" + """插件中心:语义接口服务卡片数据结构""" service_id: str = Field(..., alias="serviceId", description="服务ID") name: str = Field(..., description="服务名称") @@ -309,7 +312,7 @@ class ServiceCardItem(BaseModel): class ServiceApiData(BaseModel): - """语义接口中心:服务 API 接口属性数据结构""" + """插件中心:语义接口服务 API 接口属性数据结构""" name: str = Field(..., description="接口名称") path: str = Field(..., description="接口路径") @@ -317,7 +320,7 @@ class ServiceApiData(BaseModel): class BaseServiceOperationMsg(BaseModel): - """语义接口中心:基础服务操作Result数据结构""" + """插件中心:语义接口基础服务操作Result数据结构""" service_id: str = Field(..., alias="serviceId", description="服务ID") @@ -337,7 +340,7 @@ class GetServiceListRsp(ResponseData): class UpdateServiceMsg(BaseModel): - """语义接口中心:服务属性数据结构""" + """插件中心:语义接口服务属性数据结构""" service_id: str = Field(..., alias="serviceId", description="服务ID") name: str = Field(..., description="服务名称") @@ -396,6 +399,81 @@ class NodeServiceListRsp(ResponseData): result: NodeServiceListMsg +# TODO MCP服务response data +class MCPServiceCardItem(BaseModel): + """插件中心:MCP服务卡片数据结构""" + + mcpservice_id: str = Field(..., alias="mcpserviceId", description="mcp服务ID") + name: str = Field(..., description="mcp服务名称") + description: str = Field(..., description="mcp服务简介") + icon: str = Field(..., description="mcp服务图标") + author: str = Field(..., description="mcp服务作者") + + +class MCPServiceApiData(BaseModel): + """插件中心:MCP服务 API 接口属性数据结构""" + + name: str = Field(..., description="接口名称") + path: str = Field(..., description="接口路径") + description: str = Field(..., description="接口描述") + + +class BaseMCPServiceOperationMsg(BaseModel): + """插件中心:MCP服务操作Result数据结构""" + + service_id: str = Field(..., alias="serviceId", description="服务ID") + + +class GetMCPServiceListMsg(BaseModel): + """GET /api/service Result数据结构""" + + current_page: int = Field(..., alias="currentPage", description="当前页码") + total_count: int = Field(..., alias="totalCount", description="总服务数") + services: list[MCPServiceCardItem] = Field(..., description="解析后的服务列表") + + +class GetMCPServiceListRsp(ResponseData): + """GET /api/service 返回数据结构""" + + result: GetMCPServiceListMsg = Field(..., title="Result") + + +class UpdateMCPServiceMsg(BaseModel): + """插件中心:MCP服务属性数据结构""" + + service_id: str = Field(..., alias="serviceId", description="MCP服务ID") + name: str = Field(..., description="MCP服务名称") + + +class UpdateMCPServiceRsp(ResponseData): + """POST /api/mcp_service 返回数据结构""" + + result: UpdateMCPServiceMsg = Field(..., title="Result") + + +class GetMCPServiceDetailMsg(BaseModel): + """GET /api/mcp_service/{serviceId} Result数据结构""" + + service_id: str = Field(..., alias="serviceId", description="MCP服务ID") + icon: str = Field(description="图标", default="") + name: str = Field(..., description="MCP服务名称") + description: str = Field(description="MCP服务描述") + data: MCPServiceConfig = Field(description="MCP服务配置") + tools: list[MCPServiceToolsdata] = Field(description="MCP服务Tools列表") + + +class GetMCPServiceDetailRsp(ResponseData): + """GET /api/service/{serviceId} 返回数据结构""" + + result: GetMCPServiceDetailMsg = Field(..., title="Result") + + +class DeleteMCPServiceRsp(ResponseData): + """DELETE /api/service/{serviceId} 返回数据结构""" + + result: BaseMCPServiceOperationMsg = Field(..., title="Result") + + class NodeMetaDataRsp(ResponseData): """GET /api/flow/service/node 返回数据结构""" @@ -444,12 +522,97 @@ class FlowStructureDeleteRsp(ResponseData): result: FlowStructureDeleteMsg + class UserGetMsp(BaseModel): """GET /api/user result""" - user_info_list : list[UserInfo] = Field(alias="userInfoList", default=[]) + user_info_list: list[UserInfo] = Field(alias="userInfoList", default=[]) + class UserGetRsp(ResponseData): """GET /api/user 返回数据结构""" result: UserGetMsp + + +class BaseModelOperationMsg(BaseModel): + """基础模型操作Result数据结构""" + + model_id: str = Field(..., alias="modelId", description="模型ID") + + +class BaseModelOperationRsp(ResponseData): + """基础模型操作返回数据结构""" + + result: BaseModelOperationMsg + + +class GetModelListMsg(BaseModel): + """GET /api/model Result数据结构""" + + model_count: int = Field(..., alias="totalModels", description="总模型数") + models: list[ModelCenterCardItem] = Field(..., description="模型列表") + + +class GetModelListRsp(ResponseData): + """GET /api/model 返回数据结构""" + + result: GetModelListMsg + + +class GetModelPropertyMsg(ModelData): + """GET /api/model/{modelId} Result数据结构""" + + model_id: str = Field(..., alias="modelId", description="模型ID") + + +class GetModelPropertyRsp(ResponseData): + """GET /api/model/{modelId} 返回数据结构""" + + result: GetModelPropertyMsg + + +class GetProviderModelListMsg(BaseModel): + """GET /api/model/model 返回数据结构""" + models: list[str] = Field(..., description="模型列表") + model_count: int = Field(..., alias="modelCount", description="总模型数") + + +class GetProviderModelListRsp(ResponseData): + """GET /api/model/model 返回数据结构""" + result: GetProviderModelListMsg + + +class GetProviderListMsg(BaseModel): + """GET /api/model/provider 返回数据结构""" + providers: list[ProviderCardItem] = Field(..., description="供应商列表") + + +class GetProviderListRsp(ResponseData): + """GET /api/model/provider 返回数据结构""" + result: GetProviderListMsg + + +class BasePromptOperationMsg(BaseModel): + """基础提示词操作Result数据结构""" + + prompt_id: str = Field(..., alias="promptId", description="提示词ID") + + +class BasePromptOperationRsp(ResponseData): + """基础提示词操作返回数据结构""" + + result: BasePromptOperationMsg + + +class GetPromptListMsg(BaseModel): + """GET /api/prompt Result数据结构""" + + prompt_count: int = Field(..., alias="totalPrompts", description="总提示词数") + prompts: list[PromptCardItem] = Field(..., description="提示词列表") + + +class GetPromptListRsp(ResponseData): + """GET /api/prompt 返回数据结构""" + + result: GetPromptListMsg diff --git a/apps/exceptions.py b/apps/exceptions.py index e68918ab..e7ac719d 100644 --- a/apps/exceptions.py +++ b/apps/exceptions.py @@ -4,10 +4,15 @@ Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. """ + class ServiceIDError(Exception): """Service ID错误""" +class MCPServiceIDError(Exception): + """MCPService ID错误""" + + class InstancePermissionError(Exception): """App/Service实例的权限错误""" diff --git a/apps/manager/appcenter.py b/apps/manager/appcenter.py index 6e45308a..f0f66439 100644 --- a/apps/manager/appcenter.py +++ b/apps/manager/appcenter.py @@ -12,8 +12,9 @@ from typing import Any from apps.entities.appcenter import AppCenterCardItem, AppData from apps.entities.collection import User -from apps.entities.enum_var import SearchType +from apps.entities.enum_var import SearchType, AppType from apps.entities.flow import AppMetadata, MetadataType, Permission +from apps.entities.mcp import AgentAppMetadata from apps.entities.pool import AppPool from apps.entities.response_data import RecentAppList, RecentAppListItem from apps.exceptions import InstancePermissionError @@ -29,11 +30,11 @@ class AppCenterManager: @staticmethod async def fetch_all_apps( - user_sub: str, - search_type: SearchType, - keyword: str | None, - page: int, - page_size: int, + user_sub: str, + search_type: SearchType, + keyword: str | None, + page: int, + page_size: int, ) -> tuple[list[AppCenterCardItem], int]: """ 获取所有应用列表 @@ -54,16 +55,18 @@ class AppCenterManager: search_type, keyword, ) - if keyword + if keyword or search_type != SearchType.ALL else base_filter ) # 执行应用搜索 + # TODO 过滤权限中仅部分可见的 apps, total_apps = await AppCenterManager._search_apps_by_filter(filters, page, page_size) fav_apps = await AppCenterManager._get_favorite_app_ids_by_user(user_sub) # 构建返回的应用卡片列表 return [ AppCenterCardItem( appId=app.id, + app_type=app.app_type, icon=app.icon, name=app.name, description=app.description, @@ -80,11 +83,11 @@ class AppCenterManager: @staticmethod async def fetch_user_apps( - user_sub: str, - search_type: SearchType, - keyword: str | None, - page: int, - page_size: int, + user_sub: str, + search_type: SearchType, + keyword: str | None, + page: int, + page_size: int, ) -> tuple[list[AppCenterCardItem], int]: """ 获取用户应用列表 @@ -97,11 +100,6 @@ class AppCenterManager: :return: 应用列表, 总应用数 """ try: - if search_type == SearchType.AUTHOR: - if keyword is not None and keyword not in user_sub: - return [], 0 - else: - keyword = user_sub base_filter = {"author": user_sub} filters: dict[str, Any] = ( AppCenterManager._build_filters( @@ -109,7 +107,7 @@ class AppCenterManager: search_type, keyword, ) - if keyword + if keyword or search_type != SearchType.ALL else base_filter ) apps, total_apps = await AppCenterManager._search_apps_by_filter(filters, page, page_size) @@ -117,6 +115,7 @@ class AppCenterManager: return [ AppCenterCardItem( appId=app.id, + app_type=app.app_type, icon=app.icon, name=app.name, description=app.description, @@ -132,11 +131,11 @@ class AppCenterManager: @staticmethod async def fetch_favorite_apps( - user_sub: str, - search_type: SearchType, - keyword: str | None, - page: int, - page_size: int, + user_sub: str, + search_type: SearchType, + keyword: str | None, + page: int, + page_size: int, ) -> tuple[list[AppCenterCardItem], int]: """ 获取用户收藏的应用列表 @@ -161,13 +160,14 @@ class AppCenterManager: search_type, keyword, ) - if keyword + if keyword or search_type != SearchType.ALL else base_filter ) apps, total_apps = await AppCenterManager._search_apps_by_filter(filters, page, page_size) return [ AppCenterCardItem( appId=app.id, + app_type=app.app_type, icon=app.icon, name=app.name, description=app.description, @@ -197,60 +197,71 @@ class AppCenterManager: return AppPool.model_validate(db_data) @staticmethod - async def create_app(user_sub: str, data: AppData) -> str: + async def create_app(user_sub: str, app_type: AppType, data: AppData) -> str: """ 创建应用 :param user_sub: 用户唯一标识 + :param app_type: 应用类型 :param data: 应用数据 :return: 应用ID """ app_id = str(uuid.uuid4()) - metadata = AppMetadata( - type=MetadataType.APP, - id=app_id, - icon=data.icon, - name=data.name, - description=data.description, - version="1.0.0", - author=user_sub, - links=data.links, - first_questions=data.first_questions, - history_len=data.history_len, - permission=Permission( - type=data.permission.type, - users=data.permission.users or [], - ), - ) + if app_type == AppType.FLOW: + metadata = AppMetadata( + type=MetadataType.FLOW_APP, + app_type=app_type, + id=app_id, + icon=data.icon, + name=data.name, + description=data.description, + version="1.0.0", + author=user_sub, + links=data.links, + first_questions=data.first_questions, + history_len=data.history_len, + permission=Permission( + type=data.permission.type, + users=data.permission.users or [], + ), + ) + elif app_type == AppType.AGENT: + metadata = AgentAppMetadata( + type=MetadataType.AGENT_APP, + app_type=app_type, + id=app_id, + icon=data.icon, + name=data.name, + description=data.description, + version="1.0.0", + author=user_sub, + model=data.model, + mcp_service=data.mcp_service, + prompt=data.prompt, + knowledge=data.knowledge, + history_len=data.history_len, + permission=Permission( + type=data.permission.type, + users=data.permission.users or [], + ), + ) + else: + msg = "Invalid app type" + raise ValueError(msg) app_loader = AppLoader() await app_loader.save(metadata, app_id) return app_id @staticmethod - async def update_app(user_sub: str, app_id: str, data: AppData) -> None: + async def update_app(user_sub: str, app_id: str, app_type: AppType, data: AppData) -> None: """ 更新应用 :param user_sub: 用户唯一标识 :param app_id: 应用唯一标识 + :param app_type: 应用类型 :param data: 应用数据 """ - metadata = AppMetadata( - type=MetadataType.APP, - id=app_id, - icon=data.icon, - name=data.name, - description=data.description, - version="1.0.0", - author=user_sub, - links=data.links, - first_questions=data.first_questions, - history_len=data.history_len, - permission=Permission( - type=data.permission.type, - users=data.permission.users or [], - ), - ) app_collection = MongoDB.get_collection("app") app_data = AppPool.model_validate(await app_collection.find_one({"_id": app_id})) if not app_data: @@ -259,13 +270,54 @@ class AppCenterManager: if app_data.author != user_sub: msg = "Permission denied" raise InstancePermissionError(msg) - metadata.flows = app_data.flows - metadata.published = app_data.published + if app_type == AppType.FLOW: + metadata = AppMetadata( + type=MetadataType.FLOW_APP, + app_type=data.app_type, + id=app_id, + icon=data.icon, + name=data.name, + description=data.description, + version="1.0.0", + author=user_sub, + links=data.links, + first_questions=data.first_questions, + history_len=data.history_len, + permission=Permission( + type=data.permission.type, + users=data.permission.users or [], + ), + ) + metadata.flows = app_data.flows + metadata.published = app_data.published + elif app_type == AppType.AGENT: + metadata = AgentAppMetadata( + type=MetadataType.AGENT_APP, + app_type=data.app_type, + id=app_id, + icon=data.icon, + name=data.name, + description=data.description, + version="1.0.0", + author=user_sub, + model=data.model, + mcp_service=data.mcp_service, + prompt=data.prompt, + knowledge=data.knowledge, + history_len=data.history_len, + permission=Permission( + type=data.permission.type, + users=data.permission.users or [], + ), + ) + else: + msg = "Invalid app type" + raise ValueError(msg) app_loader = AppLoader() await app_loader.save(metadata, app_id) @staticmethod - async def update_app_publish_status(app_id: str, user_sub: str) -> None: + async def update_app_publish_status(app_id: str, app_type: AppType, user_sub: str) -> None: """ 发布应用 @@ -289,21 +341,46 @@ class AppCenterManager: {"_id": app_id}, {"$set": {"published": published}}, ) - metadata = AppMetadata( - type=MetadataType.APP, - id=app_id, - icon=app_data.icon, - name=app_data.name, - description=app_data.description, - version="1.0.0", - author=user_sub, - links=app_data.links, - first_questions=app_data.first_questions, - history_len=app_data.history_len, - permission=app_data.permission, - published=published, - flows=app_data.flows, - ) + if app_type == AppType.FLOW: + metadata = AppMetadata( + type=MetadataType.FLOW_APP, + app_type=app_data.app_type, + id=app_id, + icon=app_data.icon, + name=app_data.name, + description=app_data.description, + version="1.0.0", + author=user_sub, + links=app_data.links, + first_questions=app_data.first_questions, + history_len=app_data.history_len, + permission=app_data.permission, + published=published, + flows=app_data.flows, + ) + metadata.flows = app_data.flows + metadata.published = app_data.published + elif app_type == AppType.AGENT: + metadata = AgentAppMetadata( + type=MetadataType.AGENT_APP, + app_type=app_data.app_type, + id=app_id, + icon=app_data.icon, + name=app_data.name, + description=app_data.description, + version="1.0.0", + author=user_sub, + model=app_data.model, + mcp_service=app_data.mcp_service, + prompt=app_data.prompt, + knowledge=app_data.knowledge, + history_len=app_data.history_len, + permission=app_data.permission, + published=published, + ) + else: + msg = "Invalid app type" + raise ValueError(msg) app_loader = AppLoader() await app_loader.save(metadata, app_id) return published @@ -346,11 +423,12 @@ class AppCenterManager: ) @staticmethod - async def delete_app(app_id: str, user_sub: str) -> None: + async def delete_app(app_id: str, app_type: AppType, user_sub: str) -> None: """ 删除应用 :param app_id: 应用唯一标识 + :param app_type: 应用类型 :param user_sub: 用户唯一标识 """ app_collection = MongoDB.get_collection("app") @@ -363,7 +441,7 @@ class AppCenterManager: raise InstancePermissionError(msg) # 删除应用 app_loader = AppLoader() - await app_loader.delete(app_id) + await app_loader.delete(app_id, app_type) # 删除应用相关的工作流 for flow in app_data.flows: await FlowManager.delete_flow_by_app_and_flow_id(app_id, flow.id) @@ -459,30 +537,30 @@ class AppCenterManager: @staticmethod def _build_filters( - base_filters: dict[str, Any], - search_type: SearchType, - keyword: str, + base_filters: dict[str, Any], + search_type: SearchType, + keyword: str, ) -> dict[str, Any]: search_filters = [ {"name": {"$regex": keyword, "$options": "i"}}, {"description": {"$regex": keyword, "$options": "i"}}, {"author": {"$regex": keyword, "$options": "i"}}, ] - if search_type == SearchType.ALL: - base_filters["$or"] = search_filters - elif search_type == SearchType.NAME: - base_filters["name"] = {"$regex": keyword, "$options": "i"} - elif search_type == SearchType.DESCRIPTION: - base_filters["description"] = {"$regex": keyword, "$options": "i"} - elif search_type == SearchType.AUTHOR: - base_filters["author"] = {"$regex": keyword, "$options": "i"} + + base_filters["$or"] = search_filters + # 添加 app_type 的过滤条件 + if search_type == SearchType.FLOW: + base_filters["app_type"] = AppType.FLOW + elif search_type == SearchType.AGENT: + base_filters["app_type"] = AppType.AGENT + return base_filters @staticmethod async def _search_apps_by_filter( - search_conditions: dict[str, Any], - page: int, - page_size: int, + search_conditions: dict[str, Any], + page: int, + page_size: int, ) -> tuple[list[AppPool], int]: """根据过滤条件搜索应用并计算总页数""" try: diff --git a/apps/manager/mcp_service.py b/apps/manager/mcp_service.py new file mode 100644 index 00000000..715de5b8 --- /dev/null +++ b/apps/manager/mcp_service.py @@ -0,0 +1,233 @@ +""" +语义接口中心 Manager + +Copyright (c) Huawei Technologies Co., Ltd. 2024-2025. All rights reserved. +""" + +import logging +import uuid +from typing import Any + +from apps.entities.enum_var import SearchType +from apps.entities.mcp import MCPServiceConfig, MCPServiceMetadata, MCPServiceToolsdata +from apps.entities.pool import MCPServicePool +from apps.entities.response_data import MCPServiceCardItem +from apps.exceptions import InstancePermissionError, MCPServiceIDError +from apps.models.mongo import MongoDB +from apps.scheduler.openapi import ReducedOpenAPISpec +from apps.scheduler.pool.loader.openapi import OpenAPILoader +from apps.scheduler.pool.loader.mcp import MCPServiceLoader + +logger = logging.getLogger(__name__) + + +class MCPServiceManager: + """MCP服务管理器""" + + @staticmethod + async def fetch_all_mcpservices( + search_type: SearchType, + keyword: str | None, + page: int, + page_size: int, + ) -> tuple[list[MCPServiceCardItem], int]: + """获取所有MCP服务列表""" + filters = MCPServiceManager._build_filters({}, search_type, keyword) if keyword else {} + mcpservice_pools, total_count = await MCPServiceManager._search_mcpservice(filters, page, page_size) + mcpservices = [ + MCPServiceCardItem( + mcpserviceId=mcpservice_pool.id, + icon="", + name=mcpservice_pool.name, + description=mcpservice_pool.description, + author=mcpservice_pool.author, + ) + for mcpservice_pool in mcpservice_pools + ] + return mcpservices, total_count + + @staticmethod + async def create_mcpservice( + user_sub: str, + name: str, + icon: str, + description: str, + config: MCPServiceConfig, + ) -> str: + """创建MCP服务""" + mcpservice_id = str(uuid.uuid4()) + # 检查是否存在相同服务 + service_collection = MongoDB.get_collection("mcp_service") + db_service = await service_collection.find_one( + { + "name": name, + "description": description, + "config": config, + }, + ) + if db_service: + msg = "[MCPServiceCenterManager] 已存在相同名称和描述的MCP服务" + raise MCPServiceIDError(msg) + # TODO 与MCP引擎交互获取MCP服务的详细信息(工具等) + tools = [] + + # 存入数据库 + service_metadata = MCPServiceMetadata( + id=mcpservice_id, + name=name, + icon=icon, + description=description, + author=user_sub, + config=config, + tools=tools, + ) + mcpservice_loader = MCPServiceLoader() + await mcpservice_loader.save(mcpservice_id, service_metadata) + # 返回服务ID + return mcpservice_id + + @staticmethod + async def update_mcpservice( + user_sub: str, + mcpservice_id: str, + name: str, + icon: str, + description: str, + config: MCPServiceConfig, + ) -> str: + """更新服务""" + # 验证用户权限 + mcpservice_collection = MongoDB.get_collection("mcp_service") + db_service = await mcpservice_collection.find_one({"_id": mcpservice_id}) + if not db_service: + msg = "MCPService not found" + raise MCPServiceIDError(msg) + service_pool_store = MCPServicePool.model_validate(db_service) + if service_pool_store.author != user_sub: + msg = "Permission denied" + raise InstancePermissionError(msg) + # TODO 与MCP引擎交互获取MCP服务的详细信息(工具等) + tools = [] + # 存入数据库 + mcpservice_metadata = MCPServiceMetadata( + id=mcpservice_id, + name=name, + icon=icon, + description=description, + author=user_sub, + config=config, + tools=tools, + ) + mcpservice_loader = MCPServiceLoader() + await mcpservice_loader.save(mcpservice_id, mcpservice_metadata) + # 返回服务ID + return mcpservice_id + + @staticmethod + async def get_mcpservice_data( + user_sub: str, + mcpservice_id: str, + ) -> tuple[str, str, str, MCPServiceConfig]: + """获取服务数据""" + # 验证用户权限 + mcpservice_collection = MongoDB.get_collection("mcp_service") + match_conditions = { + {"author": user_sub} + } + query = {"$and": [{"service_id": mcpservice_id}, match_conditions]} + db_service = await mcpservice_collection.find_one(query) + if not db_service: + msg = "MCPService not found" + raise MCPServiceIDError(msg) + mcpservice_pool_store = MCPServicePool.model_validate(db_service) + if mcpservice_pool_store.author != user_sub: + msg = "Permission denied" + raise InstancePermissionError(msg) + + return mcpservice_pool_store.icon, mcpservice_pool_store.name, mcpservice_pool_store.description, mcpservice_pool_store.config + + @staticmethod + async def get_service_details( + service_id: str, + ) -> tuple[str, str, list[MCPServiceToolsdata]]: + """获取服务API列表""" + # 获取服务名称 + service_collection = MongoDB.get_collection("service") + db_service = await service_collection.find_one({"_id": service_id}) + if not db_service: + msg = "MCPService not found" + raise MCPServiceIDError(msg) + mcpservice_pool_store = MCPServicePool.model_validate(db_service) + + return mcpservice_pool_store.name, mcpservice_pool_store.description, mcpservice_pool_store.tools + + @staticmethod + async def delete_mcpservice( + user_sub: str, + service_id: str, + ) -> bool: + """删除服务""" + service_collection = MongoDB.get_collection("mcp_service") + db_service = await service_collection.find_one({"_id": service_id}) + if not db_service: + msg = "[MCPServiceCenterManager] Service未找到" + raise MCPServiceIDError(msg) + # 验证用户权限 + service_pool_store = MCPServiceManager.model_validate(db_service) + if service_pool_store.author != user_sub: + msg = "Permission denied" + raise InstancePermissionError(msg) + # 删除服务 + service_loader = MCPServiceLoader() + await service_loader.delete(service_id) + return True + + @staticmethod + async def _search_mcpservice( + search_conditions: dict, + page: int, + page_size: int, + ) -> tuple[list[MCPServicePool], int]: + """基于输入条件获取MCP服务数据""" + mcpservice_collection = MongoDB.get_collection("mcp_service") + # 获取服务总数 + total = await mcpservice_collection.count_documents(search_conditions) + # 分页查询 + skip = (page - 1) * page_size + db_mcpservices = await mcpservice_collection.find(search_conditions).skip(skip).limit(page_size).to_list() + if not db_mcpservices and total > 0: + logger.warning("[MCPServiceManager] 没有找到符合条件的MCP服务: %s", search_conditions) + return [], -1 + mcpservice_pools = [MCPServicePool.model_validate(db_mcpservice) for db_mcpservice in db_mcpservices] + return mcpservice_pools, total + + @staticmethod + async def _validate_mcpservice_data(data: dict[str, Any]) -> ReducedOpenAPISpec: + """验证服务数据""" + # 验证数据是否为空 + if not data: + msg = "[MCPServiceCenterManager] MCP服务数据为空" + raise ValueError(msg) + return await OpenAPILoader().load_dict(data) + + # TODO 和service.py中代码重复,是否考虑建一个basemanager + @staticmethod + def _build_filters( + base_filters: dict[str, Any], + search_type: SearchType, + keyword: str, + ) -> dict[str, Any]: + search_filters = [ + {"name": {"$regex": keyword, "$options": "i"}}, + {"description": {"$regex": keyword, "$options": "i"}}, + {"author": {"$regex": keyword, "$options": "i"}}, + ] + if search_type == SearchType.ALL: + base_filters["$or"] = search_filters + elif search_type == SearchType.NAME: + base_filters["name"] = {"$regex": keyword, "$options": "i"} + elif search_type == SearchType.DESCRIPTION: + base_filters["description"] = {"$regex": keyword, "$options": "i"} + elif search_type == SearchType.AUTHOR: + base_filters["author"] = {"$regex": keyword, "$options": "i"} + return base_filters diff --git a/apps/manager/model.py b/apps/manager/model.py new file mode 100644 index 00000000..3d5bcf9f --- /dev/null +++ b/apps/manager/model.py @@ -0,0 +1,222 @@ +""" +模型配置Manager + +Copyright (c) Huawei Technologies Co., Ltd. 2024-2025. All rights reserved. +""" + +import logging +import uuid +from typing import Any + +from apps.entities.model import ModelData, ModelMetadata, ModelCenterCardItem, ProviderCardItem +from apps.entities.pool import ModelPool, ProviderModelPool +from apps.exceptions import InstancePermissionError +from apps.models.mongo import MongoDB +from apps.scheduler.pool.loader.model import ModelLoader + +logger = logging.getLogger(__name__) + + +class ModelCenterManager: + """模型管理器""" + + @staticmethod + async def fetch_user_models( + user_sub: str, + ) -> tuple[list[ModelCenterCardItem], int]: + """ + 获取用户模型列表 + + :param user_sub: 用户唯一标识 + :return: 模型列表, 总模型数 + """ + try: + base_filter = {"author": user_sub} + models, total_models = await ModelCenterManager._search_models_by_filter(base_filter) + return [ + ModelCenterCardItem( + modelId=model.id, + model=model.model, + icon=model.icon, + ) + for model in models + ], total_models + except Exception: + logger.exception("[ModelCenterManager] 获取用户模型列表失败") + return [], -1 + + @staticmethod + async def fetch_model_by_keyword(keyword: str | None, + provider: str + ) -> tuple[list[str], int]: + """ + 根据搜索关键字获取模型列表 + + :param keyword: 搜索关键字 + :param provider: 供应商 + :return: 模型列表, 搜索到的模型个数 + """ + try: + base_filter = {"model": keyword, "provider": provider} + models, total_models = await ModelCenterManager._search_models_in_model_lib(base_filter) + return [model.name for model in models], total_models + except Exception: + logger.exception("[ModelCenterManager] 获取模型列表失败") + return [], -1 + + @staticmethod + async def fetch_provider(): + """ + 获取供应商列表 + + :return: 供应商列表,供应商个数 + """ + providers = { + "Ollama": "Ollama 的介绍", + "VLLM": "VLLM 的介绍", + "Tongyi-Qianwen": "Tongyi-Qianwen 的介绍", + "XunFei Spark": "XunFei Spark 的介绍", + "BaiChuan": "BaiChuan 的介绍", + "BaiduYiyan": "BaiduYiyan 的介绍", + "ModelScope": "ModelScope 的介绍", + } + provider_list = list[ProviderCardItem] + for key, value in providers: + provider_list.append( + ProviderCardItem( + provider=key, + description=value, + ) + ) + return provider_list + + @staticmethod + async def fetch_model_data_by_id(model_id: str) -> ModelPool: + """ + 根据模型ID获取模型元数据 + + :param model_id: 模型ID + :return: 模型元数据 + """ + model_collection = MongoDB.get_collection("model") + db_data = await model_collection.find_one({"_id": model_id}) + if not db_data: + msg = "Model not found" + raise ValueError(msg) + return ModelPool.model_validate(db_data) + + @staticmethod + async def create_model(user_sub: str, data: ModelData) -> str: + """ + 创建模型 + + :param user_sub: 用户唯一标识 + :param data: 模型数据 + :return: 模型ID + """ + model_id = str(uuid.uuid4()) + metadata = ModelMetadata( + id=model_id, + icon=data.icon, + provider=data.provider, + url=data.url, + model=data.model, + api_key=data.api_key, + max_token=data.max_token, + author=user_sub, + + ) + model_loader = ModelLoader() + await model_loader.save(metadata, model_id) + return model_id + + @staticmethod + async def update_model(user_sub: str, model_id: str, data: ModelData) -> None: + """ + 更新模型 + + :param user_sub: 用户唯一标识 + :param model_id: 模型唯一标识 + :param data: 模型数据 + """ + model_collection = MongoDB.get_collection("model") + model_data = ModelPool.model_validate(await model_collection.find_one({"_id": model_id})) + if not model_data: + msg = "Model not found" + raise ValueError(msg) + if model_data.author != user_sub: + msg = "Permission denied" + raise InstancePermissionError(msg) + metadata = ModelMetadata( + id=model_id, + icon=data.icon, + provider=data.provider, + url=data.url, + model=data.model, + api_key=data.api_key, + max_token=data.max_token, + author=user_sub, + + ) + model_loader = ModelLoader() + await model_loader.save(metadata, model_id) + + @staticmethod + async def delete_model(model_id: str, user_sub: str) -> None: + """ + 删除模型 + + :param model_id: 模型唯一标识 + :param user_sub: 用户唯一标识 + """ + model_collection = MongoDB.get_collection("model") + model_data = ModelPool.model_validate(await model_collection.find_one({"_id": model_id})) + if not model_data: + msg = "Model not found" + raise ValueError(msg) + if model_data.author != user_sub: + msg = "Permission denied" + raise InstancePermissionError(msg) + # 删除模型 + model_loader = ModelLoader() + await model_loader.delete(model_id) + + @staticmethod + async def _search_models_by_filter( + search_conditions: dict[str, Any], + ) -> tuple[list[ModelPool], int]: + """根据过滤条件搜索模型""" + try: + model_collection = MongoDB.get_collection("model") + total_models = await model_collection.count_documents(search_conditions) + db_data = ( + await model_collection.find(search_conditions) + .sort("created_at", -1) + .to_list() + ) + models = [ModelPool.model_validate(doc) for doc in db_data] + except Exception: + logger.exception("[ModelCenterManager] 根据过滤条件搜索模型失败") + return [], -1 + else: + return models, total_models + + @staticmethod + async def _search_models_in_model_lib( + search_conditions: dict[str, Any], + ) -> tuple[list[ModelPool], int]: + """根据过滤条件搜索模型""" + try: + model_collection = MongoDB.get_collection("model_lib") + total_models = await model_collection.count_documents(search_conditions) + db_data = ( + await model_collection.find(search_conditions) + .sort("model") + .to_list() + ) + models = [ProviderModelPool.model_validate(doc) for doc in db_data] + except Exception: + logger.exception("[ModelCenterManager] 根据过滤条件搜索模型失败") + return [], -1 + else: + return models, total_models diff --git a/apps/manager/prompt.py b/apps/manager/prompt.py new file mode 100644 index 00000000..431df822 --- /dev/null +++ b/apps/manager/prompt.py @@ -0,0 +1,163 @@ +""" +Prompt配置Manager + +Copyright (c) Huawei Technologies Co., Ltd. 2024-2025. All rights reserved. +""" + +import logging +import uuid +from typing import Any + +from apps.entities.prompt import PromptData, PromptMetadata, PromptCardItem +from apps.entities.pool import PromptPool +from apps.exceptions import InstancePermissionError +from apps.models.mongo import MongoDB +from apps.scheduler.pool.loader.prompt import PromptLoader + +logger = logging.getLogger(__name__) + + +class PromptManager: + """Prompt管理器""" + + @staticmethod + async def fetch_user_prompts( + user_sub: str, + keyword: str | None, + ) -> tuple[list[PromptCardItem], int]: + """ + 获取用户Prompt列表 + + :param user_sub: 用户唯一标识 + :return: Prompt列表, 总Prompt数 + """ + try: + base_filter = {"author": {"$in": [user_sub, "public"]}} + filters = PromptManager._build_filters(base_filter, keyword) if keyword else base_filter + prompts, total_prompts = await PromptManager._search_prompts_by_filter(filters) + return [ + PromptCardItem( + promptId=prompt.id, + name=prompt.name, + description=prompt.description, + ) + for prompt in prompts + ], total_prompts + except Exception: + logger.exception("[PromptManager] 获取用户Prompt列表失败") + return [], -1 + + @staticmethod + async def fetch_prompt_data_by_id(prompt_id: str) -> PromptPool: + """ + 根据PromptID获取Prompt元数据 + + :param prompt_id: PromptID + :return: Prompt元数据 + """ + prompt_collection = MongoDB.get_collection("prompt") + db_data = await prompt_collection.find_one({"_id": prompt_id}) + if not db_data: + msg = "Prompt not found" + raise ValueError(msg) + return PromptPool.model_validate(db_data) + + @staticmethod + async def create_prompt(user_sub: str, data: PromptData) -> str: + """ + 创建Prompt + + :param user_sub: 用户唯一标识 + :param data: Prompt数据 + :return: PromptID + """ + prompt_id = str(uuid.uuid4()) + metadata = PromptMetadata( + id=prompt_id, + name=data.name, + description=data.description, + prompt=data.prompt, + author=user_sub, + ) + prompt_loader = PromptLoader() + await prompt_loader.save(metadata, prompt_id) + return prompt_id + + @staticmethod + async def update_prompt(user_sub: str, prompt_id: str, data: PromptData) -> None: + """ + 更新Prompt + + :param user_sub: 用户唯一标识 + :param prompt_id: Prompt唯一标识 + :param data: Prompt数据 + """ + prompt_collection = MongoDB.get_collection("prompt") + prompt_data = PromptPool.model_validate(await prompt_collection.find_one({"_id": prompt_id})) + if not prompt_data: + msg = "Prompt not found" + raise ValueError(msg) + if prompt_data.author != user_sub: + msg = "Permission denied" + raise InstancePermissionError(msg) + metadata = PromptMetadata( + id=prompt_id, + name=data.name, + description=data.description, + prompt=data.prompt, + author=user_sub, + ) + prompt_loader = PromptLoader() + await prompt_loader.save(metadata, prompt_id) + + @staticmethod + async def delete_prompt(prompt_id: str, user_sub: str) -> None: + """ + 删除Prompt + + :param prompt_id: Prompt唯一标识 + :param user_sub: 用户唯一标识 + """ + prompt_collection = MongoDB.get_collection("prompt") + prompt_data = PromptPool.model_validate(await prompt_collection.find_one({"_id": prompt_id})) + if not prompt_data: + msg = "Prompt not found" + raise ValueError(msg) + if prompt_data.author != user_sub: + msg = "Permission denied" + raise InstancePermissionError(msg) + # 删除Prompt + prompt_loader = PromptLoader() + await prompt_loader.delete(prompt_id) + + @staticmethod + async def _search_prompts_by_filter( + search_conditions: dict[str, Any], + ) -> tuple[list[PromptPool], int]: + """根据过滤条件搜索Prompt""" + try: + prompt_collection = MongoDB.get_collection("prompt") + total_models = await prompt_collection.count_documents(search_conditions) + db_data = ( + await prompt_collection.find(search_conditions) + .sort("created_at", -1) + .to_list() + ) + models = [PromptPool.model_validate(doc) for doc in db_data] + except Exception: + logger.exception("[PromptManager] 根据过滤条件搜索Prompt失败") + return [], -1 + else: + return models, total_models + + @staticmethod + def _build_filters( + base_filters: dict[str, Any], + keyword: str, + ) -> dict[str, Any]: + search_filters = [ + {"name": {"$regex": keyword, "$options": "i"}}, + {"description": {"$regex": keyword, "$options": "i"}}, + ] + base_filters["$or"] = search_filters + return base_filters diff --git a/apps/routers/appcenter.py b/apps/routers/appcenter.py index 2c545518..f1e82558 100644 --- a/apps/routers/appcenter.py +++ b/apps/routers/appcenter.py @@ -13,7 +13,7 @@ from fastapi.responses import JSONResponse from apps.dependency.csrf import verify_csrf_token from apps.dependency.user import get_user, verify_user from apps.entities.appcenter import AppFlowInfo, AppPermissionData -from apps.entities.enum_var import SearchType +from apps.entities.enum_var import SearchType, AppType from apps.entities.request_data import CreateAppRequest, ModFavAppRequest from apps.entities.response_data import ( BaseAppOperationMsg, @@ -103,9 +103,10 @@ async def create_or_update_application( ) -> JSONResponse: """创建或更新应用""" app_id = request.app_id + app_type = request.app_type if app_id: # 更新应用 try: - await AppCenterManager.update_app(user_sub, app_id, request) + await AppCenterManager.update_app(user_sub, app_id, app_type, request) except ValueError: logger.exception("[AppCenter] 更新应用请求无效") return JSONResponse( @@ -138,7 +139,7 @@ async def create_or_update_application( ) else: # 创建应用 try: - app_id = await AppCenterManager.create_app(user_sub, request) + app_id = await AppCenterManager.create_app(user_sub, app_type, request) except Exception: logger.exception("[AppCenter] 创建应用失败") return JSONResponse( @@ -189,7 +190,7 @@ async def get_recently_used_applications( @router.get("/{appId}", response_model=GetAppPropertyRsp | ResponseData) async def get_application( - app_id: Annotated[str, Path(..., alias="appId", description="应用ID")], + app_id: Annotated[str, Path(..., alias="appId", description="应用ID")], ) -> JSONResponse: """获取应用详情""" try: @@ -230,6 +231,7 @@ async def get_application( message="OK", result=GetAppPropertyMsg( appId=app_data.id, + app_type=app_data.app_type, published=app_data.published, name=app_data.name, description=app_data.description, @@ -242,6 +244,10 @@ async def get_application( authorizedUsers=app_data.permission.users, ), workflows=workflows, + model=app_data.model, + prompt=app_data.prompt, + mcp_service=app_data.mcp_service, + knowledge=app_data.knowledge, ), ).model_dump(exclude_none=True, by_alias=True), ) @@ -254,11 +260,12 @@ async def get_application( ) async def delete_application( app_id: Annotated[str, Path(..., alias="appId", description="应用ID")], + app_type: Annotated[AppType, Path(..., alias="appType", description="应用类型")], user_sub: Annotated[str, Depends(get_user)], ) -> JSONResponse: """删除应用""" try: - await AppCenterManager.delete_app(app_id, user_sub) + await AppCenterManager.delete_app(app_id, app_type, user_sub) except ValueError: logger.exception("[AppCenter] 删除应用请求无效") return JSONResponse( @@ -302,11 +309,12 @@ async def delete_application( @router.post("/{appId}", dependencies=[Depends(verify_csrf_token)], response_model=BaseAppOperationRsp) async def publish_application( app_id: Annotated[str, Path(..., alias="appId", description="应用ID")], + app_type: Annotated[AppType, Path(..., alias="appType", description="应用类型")], user_sub: Annotated[str, Depends(get_user)], ) -> JSONResponse: """发布应用""" try: - published = await AppCenterManager.update_app_publish_status(app_id, user_sub) + published = await AppCenterManager.update_app_publish_status(app_id, app_type, user_sub) if not published: msg = "发布应用失败" raise ValueError(msg) diff --git a/apps/routers/mcp_service.py b/apps/routers/mcp_service.py new file mode 100644 index 00000000..885c4d8e --- /dev/null +++ b/apps/routers/mcp_service.py @@ -0,0 +1,252 @@ +""" +FastAPI 语义接口中心相关路由 + +Copyright (c) Huawei Technologies Co., Ltd. 2024-2025. All rights reserved. +""" + +import logging +from typing import Annotated + +from fastapi import APIRouter, Body, Depends, Path, Query, status +from fastapi.responses import JSONResponse + +from apps.dependency.csrf import verify_csrf_token +from apps.dependency.user import get_user, verify_user +from apps.entities.enum_var import SearchType +from apps.entities.request_data import UpdateMCPServiceRequest +from apps.entities.response_data import ( + BaseMCPServiceOperationMsg, + DeleteMCPServiceRsp, + GetMCPServiceDetailMsg, + GetMCPServiceDetailRsp, + GetMCPServiceListMsg, + GetMCPServiceListRsp, + ResponseData, + UpdateMCPServiceMsg, + UpdateMCPServiceRsp, +) +from apps.exceptions import InstancePermissionError, ServiceIDError +from apps.manager.mcp_service import MCPServiceManager + +logger = logging.getLogger(__name__) +router = APIRouter( + prefix="/api/mcpservice", + tags=["mcp-service"], + dependencies=[Depends(verify_user)], +) + + +@router.get("", response_model=GetMCPServiceListRsp | ResponseData) +async def get_mcpservice_list( # noqa: PLR0913 + *, + search_type: Annotated[SearchType, Query(..., alias="searchType", description="搜索类型")] = SearchType.ALL, + keyword: Annotated[str | None, Query(..., alias="keyword", description="搜索关键字")] = None, + page: Annotated[int, Query(..., alias="page", ge=1, description="页码")] = 1, + page_size: Annotated[int, Query(..., alias="pageSize", ge=1, le=100, description="每页数量")] = 16, +) -> JSONResponse: + """获取服务列表""" + service_cards, total_count = [], -1 + try: + service_cards, total_count = await MCPServiceManager.fetch_all_mcpservices( + search_type, + keyword, + page, + page_size, + ) + except Exception: + logger.exception("[MCPServiceCenter] 获取MCP服务列表失败") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=ResponseData( + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + message="ERROR", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + if total_count == -1: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=ResponseData( + code=status.HTTP_400_BAD_REQUEST, + message="INVALID_PARAMETER", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + return JSONResponse( + status_code=status.HTTP_200_OK, + content=GetMCPServiceListRsp( + code=status.HTTP_200_OK, + message="OK", + result=GetMCPServiceListMsg( + currentPage=page, + totalCount=total_count, + services=service_cards, + ), + ).model_dump(exclude_none=True, by_alias=True), + ) + + +@router.post("", response_model=UpdateMCPServiceRsp, dependencies=[Depends(verify_csrf_token)]) +async def update_mcpservice( + user_sub: Annotated[str, Depends(get_user)], + data: Annotated[UpdateMCPServiceRequest, Body(..., description="MCP服务对应数据对象")], +) -> JSONResponse: + """新建或更新MCP服务""" + if not data.service_id: + try: + service_id = await MCPServiceManager.create_mcpservice(user_sub, data.name, data.icon, data.description, + data.config) + except Exception as e: + logger.exception("[MCPServiceCenter] 创建MCP服务失败") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=ResponseData( + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + message=f"OpenAPI解析错误: {e!s}", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + else: + try: + service_id = await MCPServiceManager.update_mcpservice(user_sub, data.name, data.icon, data.description, + data.config) + except ServiceIDError: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=ResponseData( + code=status.HTTP_400_BAD_REQUEST, + message="MCPService ID错误", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + except InstancePermissionError: + return JSONResponse( + status_code=status.HTTP_403_FORBIDDEN, + content=ResponseData( + code=status.HTTP_403_FORBIDDEN, + message="未授权访问", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + except Exception as e: + logger.exception("[MCPService] 更新MCP服务失败") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=ResponseData( + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + message=f"更新MCP服务失败: {e!s}", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + msg = UpdateMCPServiceMsg(serviceId=service_id, name=data.name) + rsp = UpdateMCPServiceRsp(code=status.HTTP_200_OK, message="OK", result=msg) + return JSONResponse(status_code=status.HTTP_200_OK, content=rsp.model_dump(exclude_none=True, by_alias=True)) + + +@router.get("/{serviceId}", response_model=GetMCPServiceDetailRsp) +async def get_service_detail( + user_sub: Annotated[str, Depends(get_user)], + service_id: Annotated[str, Path(..., alias="serviceId", description="服务ID")], + *, + edit: Annotated[bool, Query(..., description="是否为编辑模式")] = False, +) -> JSONResponse: + """获取MCP服务详情""" + # 示例:返回指定MCP服务的详情 + if edit: + try: + icon, name, description, data = await MCPServiceManager.get_mcpservice_data(user_sub, service_id) + except ServiceIDError: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=ResponseData( + code=status.HTTP_400_BAD_REQUEST, + message="MCPService ID错误", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + except InstancePermissionError: + return JSONResponse( + status_code=status.HTTP_403_FORBIDDEN, + content=ResponseData( + code=status.HTTP_403_FORBIDDEN, + message="未授权访问", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + except Exception: + logger.exception("[MCPService] 获取MCP服务数据失败") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=ResponseData( + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + message="ERROR", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + detail = GetMCPServiceDetailMsg(serviceId=service_id, icon=icon, name=name, description=description, data=data) + else: + try: + name, description, tools = await MCPServiceManager.get_service_details(service_id) + except ServiceIDError: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=ResponseData( + code=status.HTTP_400_BAD_REQUEST, + message="MCPService ID错误", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + except Exception: + logger.exception("[MCPService] 获取MCP服务API失败") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=ResponseData( + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + message="ERROR", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + detail = GetMCPServiceDetailMsg(serviceId=service_id, name=name, description=description, tools=tools) + rsp = GetMCPServiceDetailRsp(code=status.HTTP_200_OK, message="OK", result=detail) + return JSONResponse(status_code=status.HTTP_200_OK, content=rsp.model_dump(exclude_none=True, by_alias=True)) + + +@router.delete("/{serviceId}", response_model=DeleteMCPServiceRsp, dependencies=[Depends(verify_csrf_token)]) +async def delete_service( + user_sub: Annotated[str, Depends(get_user)], + service_id: Annotated[str, Path(..., alias="serviceId", description="服务ID")], +) -> JSONResponse: + """删除服务""" + try: + await MCPServiceManager.delete_mcpservice(user_sub, service_id) + except ServiceIDError: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=ResponseData( + code=status.HTTP_400_BAD_REQUEST, + message="MCPService ID错误", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + except InstancePermissionError: + return JSONResponse( + status_code=status.HTTP_403_FORBIDDEN, + content=ResponseData( + code=status.HTTP_403_FORBIDDEN, + message="未授权访问", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + except Exception: + logger.exception("[MCPServiceManager] 删除MCP服务失败") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=ResponseData( + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + message="ERROR", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + msg = BaseMCPServiceOperationMsg(serviceId=service_id) + rsp = DeleteMCPServiceRsp(code=status.HTTP_200_OK, message="OK", result=msg) + return JSONResponse(status_code=status.HTTP_200_OK, content=rsp.model_dump(exclude_none=True, by_alias=True)) diff --git a/apps/routers/model.py b/apps/routers/model.py new file mode 100644 index 00000000..2a5bc036 --- /dev/null +++ b/apps/routers/model.py @@ -0,0 +1,304 @@ +""" +FastAPI 模型中心相关路由 + +Copyright (c) Huawei Technologies Co., Ltd. 2024-2025. All rights reserved. +""" + +import logging +from typing import Annotated + +from fastapi import APIRouter, Body, Depends, Path, Query, status +from fastapi.responses import JSONResponse + +from apps.dependency.csrf import verify_csrf_token +from apps.dependency.user import get_user, verify_user +from apps.entities.request_data import CreateModelRequest +from apps.entities.response_data import ( + BaseModelOperationMsg, + BaseModelOperationRsp, + GetModelListMsg, + GetModelListRsp, + ResponseData, + GetModelPropertyRsp, + GetModelPropertyMsg, + GetProviderModelListRsp, + GetProviderModelListMsg, + GetProviderListRsp, + GetProviderListMsg, +) +from apps.exceptions import InstancePermissionError +from apps.manager.model import ModelCenterManager + +logger = logging.getLogger(__name__) +router = APIRouter( + prefix="/api/model", + tags=["model"], + dependencies=[Depends(verify_user)], +) + + +@router.get("", response_model=GetModelListRsp | ResponseData) +async def get_added_models( + user_sub: Annotated[str, Depends(get_user)], +) -> JSONResponse: + """获取已添加的模型列表""" + + model_cards, total_models = await ModelCenterManager.fetch_user_models(user_sub) + + if total_models == -1: + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=ResponseData( + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + message="ERROR", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + return JSONResponse( + status_code=status.HTTP_200_OK, + content=GetModelListRsp( + code=status.HTTP_200_OK, + message="OK", + result=GetModelListMsg( + totalModels=total_models, + models=model_cards, + ), + ).model_dump(exclude_none=True, by_alias=True), + ) + + +@router.post("", dependencies=[Depends(verify_csrf_token)], response_model=BaseModelOperationRsp | ResponseData) +async def create_or_update_model( + request: Annotated[CreateModelRequest, Body(...)], + user_sub: Annotated[str, Depends(get_user)], +) -> JSONResponse: + """创建或更新模型""" + model_id = request.model_id + if model_id: # 更新模型 + try: + await ModelCenterManager.update_model(user_sub, model_id, request) + except ValueError: + logger.exception("[ModelCenter] 更新模型请求无效") + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=ResponseData( + code=status.HTTP_400_BAD_REQUEST, + message="BAD_REQUEST", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + except InstancePermissionError: + logger.exception("[ModelCenter] 更新模型鉴权失败") + return JSONResponse( + status_code=status.HTTP_403_FORBIDDEN, + content=ResponseData( + code=status.HTTP_403_FORBIDDEN, + message="UNAUTHORIZED", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + except Exception: + logger.exception("[ModelCenter] 更新模型失败") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=ResponseData( + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + message="ERROR", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + else: # 创建模型 + try: + model_id = await ModelCenterManager.create_model(user_sub, request) + except Exception: + logger.exception("[ModelCenter] 创建模型失败") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=ResponseData( + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + message="ERROR", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + return JSONResponse( + status_code=status.HTTP_200_OK, + content=BaseModelOperationRsp( + code=status.HTTP_200_OK, + message="OK", + result=BaseModelOperationMsg(modelId=model_id), + ).model_dump(exclude_none=True, by_alias=True), + ) + + +@router.delete( + "/{modelId}", + dependencies=[Depends(verify_csrf_token)], + response_model=BaseModelOperationRsp | ResponseData, +) +async def delete_model( + model_id: Annotated[str, Path(..., alias="modelId", description="模型ID")], + user_sub: Annotated[str, Depends(get_user)], +) -> JSONResponse: + """删除模型""" + try: + await ModelCenterManager.delete_model(model_id, user_sub) + except ValueError: + logger.exception("[ModelCenter] 删除模型请求无效") + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=ResponseData( + code=status.HTTP_400_BAD_REQUEST, + message="INVALID_APP_ID", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + except InstancePermissionError: + logger.exception("[ModelCenter] 删除模型鉴权失败") + return JSONResponse( + status_code=status.HTTP_403_FORBIDDEN, + content=ResponseData( + code=status.HTTP_403_FORBIDDEN, + message="UNAUTHORIZED", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + except Exception: + logger.exception("[ModelCenter] 删除模型失败") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=ResponseData( + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + message="ERROR", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + return JSONResponse( + status_code=status.HTTP_200_OK, + content=BaseModelOperationRsp( + code=status.HTTP_200_OK, + message="OK", + result=BaseModelOperationMsg(modelId=model_id), + ).model_dump(exclude_none=True, by_alias=True), + ) + + +@router.get("/{modelId}", response_model=GetModelPropertyRsp | ResponseData) +async def get_model( + model_id: Annotated[str, Path(..., alias="modelId", description="模型ID")], +) -> JSONResponse: + """获取应用详情""" + try: + model_data = await ModelCenterManager.fetch_model_data_by_id(model_id) + except ValueError: + logger.exception("[ModelCenter] 获取应用详情请求无效") + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=ResponseData( + code=status.HTTP_400_BAD_REQUEST, + message="INVALID_APP_ID", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + except Exception: + logger.exception("[ModelCenter] 获取应用详情失败") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=ResponseData( + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + message="ERROR", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + return JSONResponse( + status_code=status.HTTP_200_OK, + content=GetModelPropertyRsp( + code=status.HTTP_200_OK, + message="OK", + result=GetModelPropertyMsg( + modelId=model_data.id, + url=model_data.url, + api_key=model_data.api_key, + model=model_data.model, + icon=model_data.icon, + max_token=model_data.max_token, + ), + ).model_dump(exclude_none=True, by_alias=True), + ) + + +@router.get("/model", response_model=GetProviderModelListRsp | ResponseData) +async def get_model_by_keyword( + keyword: Annotated[str | None, Query(..., alias="keyword", description="搜索关键字")] = None, + provider: Annotated[str, Query(..., alias="provider", description="供应商")] = None, +) -> JSONResponse: + """根据搜索关键字获取模型""" + try: + models, total_models = await ModelCenterManager.fetch_model_by_keyword(keyword, provider) + except ValueError: + logger.exception("[ModelCenter] 根据搜索关键字获取模型请求无效") + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=ResponseData( + code=status.HTTP_400_BAD_REQUEST, + message="INVALID_APP_ID", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + except Exception: + logger.exception("[ModelCenter] 根据搜索关键字获取模型失败") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=ResponseData( + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + message="ERROR", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + return JSONResponse( + status_code=status.HTTP_200_OK, + content=GetProviderModelListRsp( + code=status.HTTP_200_OK, + message="OK", + result=GetProviderModelListMsg( + models=models, + modelCount=total_models, + ), + ).model_dump(exclude_none=True, by_alias=True), + ) + + +@router.get("/provider", response_model=GetProviderListRsp | ResponseData) +async def get_provider() -> JSONResponse: + """获取可用的供应商列表""" + try: + providers = await ModelCenterManager.fetch_provider() + except ValueError: + logger.exception("[ModelCenter] 获取供应商请求无效") + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=ResponseData( + code=status.HTTP_400_BAD_REQUEST, + message="INVALID_APP_ID", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + except Exception: + logger.exception("[ModelCenter] 获取供应商失败") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=ResponseData( + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + message="ERROR", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + return JSONResponse( + status_code=status.HTTP_200_OK, + content=GetProviderListRsp( + code=status.HTTP_200_OK, + message="OK", + result=GetProviderListMsg( + providers=providers, + ), + ).model_dump(exclude_none=True, by_alias=True), + ) diff --git a/apps/routers/prompt.py b/apps/routers/prompt.py new file mode 100644 index 00000000..c2a20a7b --- /dev/null +++ b/apps/routers/prompt.py @@ -0,0 +1,177 @@ +""" +FastAPI Prompt中心相关路由 + +Copyright (c) Huawei Technologies Co., Ltd. 2024-2025. All rights reserved. +""" + +import logging +from typing import Annotated + +from fastapi import APIRouter, Body, Depends, Path, Query, status +from fastapi.responses import JSONResponse + +from apps.dependency.csrf import verify_csrf_token +from apps.dependency.user import get_user, verify_user +from apps.entities.request_data import CreatePromptRequest +from apps.entities.response_data import ( + BasePromptOperationMsg, + BasePromptOperationRsp, + GetPromptListMsg, + GetPromptListRsp, + ResponseData, +) +from apps.exceptions import InstancePermissionError +from apps.manager.prompt import PromptManager + +logger = logging.getLogger(__name__) +router = APIRouter( + prefix="/api/prompt", + tags=["model"], + dependencies=[Depends(verify_user)], +) + + +@router.get("", response_model=GetPromptListRsp | ResponseData) +async def get_prompts( + user_sub: Annotated[str, Depends(get_user)], + keyword: Annotated[str | None, Query(..., alias="keyword", description="搜索关键字")] = None, +) -> JSONResponse: + """获取已添加的Prompt列表""" + + prompt_cards, total_prompts = await PromptManager.fetch_user_prompts(user_sub, keyword) + + if total_prompts == -1: + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=ResponseData( + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + message="ERROR", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + return JSONResponse( + status_code=status.HTTP_200_OK, + content=GetPromptListRsp( + code=status.HTTP_200_OK, + message="OK", + result=GetPromptListMsg( + totalPrompts=total_prompts, + prompts=prompt_cards, + ), + ).model_dump(exclude_none=True, by_alias=True), + ) + + +@router.post("", dependencies=[Depends(verify_csrf_token)], response_model=BasePromptOperationRsp | ResponseData) +async def create_or_update_prompt( + request: Annotated[CreatePromptRequest, Body(...)], + user_sub: Annotated[str, Depends(get_user)], +) -> JSONResponse: + """创建或更新Prompt""" + prompt_id = request.prompt_id + if prompt_id: # 更新Prompt + try: + await PromptManager.update_prompt(user_sub, prompt_id, request) + except ValueError: + logger.exception("[Prompt] 更新Prompt请求无效") + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=ResponseData( + code=status.HTTP_400_BAD_REQUEST, + message="BAD_REQUEST", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + except InstancePermissionError: + logger.exception("[Prompt] 更新Prompt鉴权失败") + return JSONResponse( + status_code=status.HTTP_403_FORBIDDEN, + content=ResponseData( + code=status.HTTP_403_FORBIDDEN, + message="UNAUTHORIZED", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + except Exception: + logger.exception("[Prompt] 更新Prompt失败") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=ResponseData( + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + message="ERROR", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + else: # 创建Prompt + try: + prompt_id = await PromptManager.create_prompt(user_sub, request) + except Exception: + logger.exception("[Prompt] 创建Prompt失败") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=ResponseData( + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + message="ERROR", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + return JSONResponse( + status_code=status.HTTP_200_OK, + content=BasePromptOperationRsp( + code=status.HTTP_200_OK, + message="OK", + result=BasePromptOperationMsg(promptId=prompt_id), + ).model_dump(exclude_none=True, by_alias=True), + ) + + +@router.delete( + "/{promptId}", + dependencies=[Depends(verify_csrf_token)], + response_model=BasePromptOperationRsp | ResponseData, +) +async def delete_prompt( + prompt_id: Annotated[str, Path(..., alias="promptId", description="PromptID")], + user_sub: Annotated[str, Depends(get_user)], +) -> JSONResponse: + """删除Prompt""" + try: + await PromptManager.delete_prompt(prompt_id, user_sub) + except ValueError: + logger.exception("[Prompt] 删除Prompt请求无效") + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=ResponseData( + code=status.HTTP_400_BAD_REQUEST, + message="INVALID_APP_ID", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + except InstancePermissionError: + logger.exception("[Prompt] 删除Prompt鉴权失败") + return JSONResponse( + status_code=status.HTTP_403_FORBIDDEN, + content=ResponseData( + code=status.HTTP_403_FORBIDDEN, + message="UNAUTHORIZED", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + except Exception: + logger.exception("[Prompt] 删除Prompt失败") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=ResponseData( + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + message="ERROR", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + return JSONResponse( + status_code=status.HTTP_200_OK, + content=BasePromptOperationRsp( + code=status.HTTP_200_OK, + message="OK", + result=BasePromptOperationMsg(promptId=prompt_id), + ).model_dump(exclude_none=True, by_alias=True), + ) diff --git a/apps/scheduler/pool/check.py b/apps/scheduler/pool/check.py index 562343bb..6ef834c1 100644 --- a/apps/scheduler/pool/check.py +++ b/apps/scheduler/pool/check.py @@ -19,10 +19,11 @@ logger = logging.getLogger(__name__) class FileChecker: """文件检查器""" - def __init__(self) -> None: + def __init__(self, service_type="semantic") -> None: """初始化文件检查器""" + self.type = service_type self.hashes = {} - self._dir_path = Path(Config().get_config().deploy.data_dir) / "semantics" + self._dir_path = Path(Config().get_config().deploy.data_dir) / self.type async def check_one(self, path: Path) -> dict[str, str]: """检查单个App/Service文件是否有变动""" @@ -43,24 +44,22 @@ class FileChecker: return hashes - async def diff_one(self, path: Path, previous_hashes: dict[str, str] | None = None) -> bool: """检查文件是否发生变化""" self._resource_path = path - semantics_path = Path(Config().get_config().deploy.data_dir) / "semantics" - path_diff = self._resource_path.relative_to(semantics_path) + service_path = Path(Config().get_config().deploy.data_dir) / self.type + path_diff = self._resource_path.relative_to(service_path) self.hashes[path_diff.as_posix()] = await self.check_one(path) return self.hashes[path_diff.as_posix()] != previous_hashes - async def diff(self, check_type: MetadataType) -> tuple[list[str], list[str]]: """生成更新列表和删除列表""" - if check_type == MetadataType.APP: + if check_type == MetadataType.FLOW_APP: collection = MongoDB.get_collection("app") - self._dir_path = Path(Config().get_config().deploy.data_dir) / "semantics" / "app" + self._dir_path = Path(Config().get_config().deploy.data_dir) / self.type / "app" elif check_type == MetadataType.SERVICE: collection = MongoDB.get_collection("service") - self._dir_path = Path(Config().get_config().deploy.data_dir) / "semantics" / "service" + self._dir_path = Path(Config().get_config().deploy.data_dir) / self.type / "service" changed_list = [] deleted_list = [] @@ -89,9 +88,9 @@ class FileChecker: async for service_folder in self._dir_path.iterdir(): # 判断是否新增? if ( - service_folder.name not in item_names - and service_folder.name not in deleted_list - and service_folder.name not in changed_list + service_folder.name not in item_names + and service_folder.name not in deleted_list + and service_folder.name not in changed_list ): changed_list += [service_folder.name] # 触发一次hash计算 diff --git a/apps/scheduler/pool/loader/app.py b/apps/scheduler/pool/loader/app.py index 3f8d9bb5..683460c7 100644 --- a/apps/scheduler/pool/loader/app.py +++ b/apps/scheduler/pool/loader/app.py @@ -11,7 +11,9 @@ from anyio import Path from fastapi.encoders import jsonable_encoder from apps.common.config import Config +from apps.entities.enum_var import AppType from apps.entities.flow import AppFlow, AppMetadata, MetadataType, Permission +from apps.entities.mcp import AgentAppMetadata, MCPServiceMetadata from apps.entities.pool import AppPool from apps.models.mongo import MongoDB from apps.scheduler.pool.check import FileChecker @@ -24,13 +26,14 @@ logger = logging.getLogger(__name__) class AppLoader: """应用加载器""" - async def load(self, app_id: str, hashes: dict[str, str]) -> None: + async def load(self, app_id: str, app_type: str, hashes: dict[str, str]) -> None: """ 从文件系统中加载应用 :param app_id: 应用 ID + :param app_type: 应用类型对应的存放地址 """ - app_path = Path(Config().get_config().deploy.data_dir) / "semantics" / "app" / app_id + app_path = Path(Config().get_config().deploy.data_dir) / app_type / "app" / app_id metadata_path = app_path / "metadata.yaml" metadata = await MetadataLoader().load_one(metadata_path) if not metadata: @@ -38,45 +41,69 @@ class AppLoader: raise ValueError(err) metadata.hashes = hashes - if not isinstance(metadata, AppMetadata): + if not isinstance(metadata, (AppMetadata, AgentAppMetadata)): err = f"[AppLoader] 元数据类型错误: {metadata_path}" raise TypeError(err) - # 加载工作流 - flow_path = app_path / "flow" - flow_loader = FlowLoader() - - flow_ids = [app_flow.id for app_flow in metadata.flows] - new_flows: list[AppFlow] = [] - async for flow_file in flow_path.rglob("*.yaml"): - if flow_file.stem not in flow_ids: - logger.warning("[AppLoader] 工作流 %s 不在元数据中", flow_file) - flow = await flow_loader.load(app_id, flow_file.stem) - if not flow: - err = f"[AppLoader] 工作流 {flow_file} 加载失败" - raise ValueError(err) - if not flow.debug: - metadata.published = False - new_flows.append( - AppFlow( - id=flow_file.stem, - name=flow.name, - description=flow.description, - path=flow_file.as_posix(), - debug=flow.debug, - ), - ) - metadata.flows = new_flows - try: - metadata = AppMetadata.model_validate(metadata) - except Exception as e: - err = "[AppLoader] 元数据验证失败" - logger.exception(err) - raise RuntimeError(err) from e + if app_type == "semantics": + # 加载工作流 + flow_path = app_path / "flow" + flow_loader = FlowLoader() + + flow_ids = [app_flow.id for app_flow in metadata.flows] + new_flows: list[AppFlow] = [] + async for flow_file in flow_path.rglob("*.yaml"): + if flow_file.stem not in flow_ids: + logger.warning("[AppLoader] 工作流 %s 不在元数据中", flow_file) + flow = await flow_loader.load(app_id, flow_file.stem) + if not flow: + err = f"[AppLoader] 工作流 {flow_file} 加载失败" + raise ValueError(err) + if not flow.debug: + metadata.published = False + new_flows.append( + AppFlow( + id=flow_file.stem, + name=flow.name, + description=flow.description, + path=flow_file.as_posix(), + debug=flow.debug, + ), + ) + metadata.flows = new_flows + try: + metadata = AppMetadata.model_validate(metadata) + except Exception as e: + err = "[AppLoader] Flow应用元数据验证失败" + logger.exception(err) + raise RuntimeError(err) from e + elif app_type == "agent": + metadata_loader = MetadataLoader() + # 加载模型 + model_id = metadata.model.id + model_path = Path(Config().get_config().deploy.data_dir) / "modelcenter" / "model" / model_id + metadata_path = model_path / "metadata.yaml" + model = await metadata_loader.load_one(metadata_path) + metadata.model = model + # 加载MCP服务 + mcpservices = metadata.mcp_service + mcpservice_path = Path(Config().get_config().deploy.data_dir) / "mcp" / "service" + new_mcpservices: list[MCPServiceMetadata] = [] + for mcpservice in mcpservices: + mcpservice = await metadata_loader.load_one(mcpservice_path / mcpservice.id / "metadata.yaml") + new_mcpservices.append(mcpservice) + metadata.mcp_service = new_mcpservices + # TODO 加载知识库 + try: + metadata = AgentAppMetadata.model_validate(metadata) + except Exception as e: + err = "[AppLoader] Agent应用元数据验证失败" + logger.exception(err) + raise RuntimeError(err) from e + pass await self._update_db(metadata) - - async def save(self, metadata: AppMetadata, app_id: str) -> None: + async def save(self, metadata: AppMetadata | AgentAppMetadata, app_id: str) -> None: """ 保存应用 @@ -84,22 +111,29 @@ class AppLoader: :param app_id: 应用 ID """ # 创建文件夹 - app_path = Path(Config().get_config().deploy.data_dir) / "semantics" / "app" / app_id + if metadata.type == MetadataType.FLOW_APP: + app_type = "semantics" + elif metadata.type == MetadataType.AGENT_APP: + app_type = "agent" + else: + msg = "Invalid app type" + raise ValueError(msg) + app_path = Path(Config().get_config().deploy.data_dir) / app_type / "app" / app_id if not await app_path.exists(): await app_path.mkdir(parents=True, exist_ok=True) # 保存元数据 - await MetadataLoader().save_one(MetadataType.APP, metadata, app_id) + await MetadataLoader().save_one(metadata.type, metadata, app_id) # 重新载入 - file_checker = FileChecker() + file_checker = FileChecker(service_type=app_type) await file_checker.diff_one(app_path) - await self.load(app_id, file_checker.hashes[f"app/{app_id}"]) - + await self.load(app_id, app_type, file_checker.hashes[f"app/{app_id}"]) - async def delete(self, app_id: str, *, is_reload: bool = False) -> None: + async def delete(self, app_id: str, app_type: AppType, *, is_reload: bool = False) -> None: """ 删除App,并更新数据库 :param app_id: 应用 ID + :param app_type: 应用类型 """ try: app_collection = MongoDB.get_collection("app") @@ -118,13 +152,13 @@ class AppLoader: except Exception: logger.exception("[AppLoader] MongoDB删除App失败") + type = "semantics" if app_type == AppType.FLOW else "agent" if not is_reload: - app_path = Path(Config().get_config().deploy.data_dir) / "semantics" / "app" / app_id + app_path = Path(Config().get_config().deploy.data_dir) / type / "app" / app_id if await app_path.exists(): shutil.rmtree(str(app_path), ignore_errors=True) - - async def _update_db(self, metadata: AppMetadata) -> None: + async def _update_db(self, metadata: AppMetadata | AgentAppMetadata) -> None: """更新数据库""" if not metadata.hashes: err = f"[AppLoader] 应用 {metadata.id} 的哈希值为空" diff --git a/apps/scheduler/pool/loader/mcp.py b/apps/scheduler/pool/loader/mcp.py new file mode 100644 index 00000000..2d14d3da --- /dev/null +++ b/apps/scheduler/pool/loader/mcp.py @@ -0,0 +1,125 @@ +""" +MCPService 加载器 + +Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. +""" + +import logging +import shutil + +from anyio import Path +from fastapi.encoders import jsonable_encoder + +from apps.common.config import Config +from apps.entities.mcp import MCPServiceMetadata +from apps.entities.pool import MCPServicePool +from apps.entities.vector import ServicePoolVector +from apps.llm.embedding import Embedding +from apps.models.lance import LanceDB +from apps.models.mongo import MongoDB +from apps.scheduler.pool.check import FileChecker +from apps.scheduler.pool.loader.metadata import MetadataLoader, MetadataType + +logger = logging.getLogger(__name__) + + +class MCPServiceLoader: + """MCPService 加载器""" + + async def load(self, service_id: str, hashes: dict[str, str]) -> None: + """加载单个MCPService""" + service_path = Path(Config().get_config().deploy.data_dir) / "mcp" / "service" / service_id + # 载入元数据 + metadata = await MetadataLoader().load_one(service_path / "metadata.yaml") + if not isinstance(metadata, MCPServiceMetadata): + err = f"[MCPServiceLoader] 元数据类型错误: {service_path}/metadata.yaml" + logger.error(err) + raise TypeError(err) + metadata.hashes = hashes + + # 更新数据库 + await self._update_db(metadata) + + async def save(self, service_id: str, metadata: MCPServiceMetadata) -> None: + """在文件系统上保存MCPService,并更新数据库""" + mcpservice_path = Path(Config().get_config().deploy.data_dir) / "mcp" / "service" / service_id + # 创建文件夹 + if not await mcpservice_path.exists(): + await mcpservice_path.mkdir(parents=True, exist_ok=True) + # 保存元数据 + await MetadataLoader().save_one(MetadataType.SERVICE, metadata, service_id) + # 重新载入 + file_checker = FileChecker(service_type="mcp") + await file_checker.diff_one(mcpservice_path) + await self.load(service_id, file_checker.hashes[f"service/{service_id}"]) + + async def delete(self, service_id: str, *, is_reload: bool = False) -> None: + """删除MCPService,并更新数据库""" + service_collection = MongoDB.get_collection("mcp_service") + try: + await service_collection.delete_one({"_id": service_id}) + except Exception: + logger.exception("[MCPServiceLoader] 删除MCPService失败") + + try: + # 获取 LanceDB 表 + service_table = await LanceDB().get_table("mcp_service") + + # 删除数据 + await service_table.delete(f"id = '{service_id}'") + except Exception: + logger.exception("[MCPServiceLoader] MCP服务删除数据库失败") + + if not is_reload: + path = Path(Config().get_config().deploy.data_dir) / "mcp" / "service" / service_id + if await path.exists(): + shutil.rmtree(path) + + async def _update_db(self, metadata: MCPServiceMetadata) -> None: + """更新数据库""" + if not metadata.hashes: + err = f"[MCPServiceLoader] MCP服务 {metadata.id} 的哈希值为空" + logger.error(err) + raise ValueError(err) + # 更新MongoDB + service_collection = MongoDB.get_collection("mcp_service") + try: + # 插入或更新 Service + await service_collection.update_one( + {"_id": metadata.id}, + { + "$set": jsonable_encoder( + MCPServicePool( + _id=metadata.id, + name=metadata.name, + description=metadata.description, + author=metadata.author, + icon=metadata.icon, + hashes=metadata.hashes, + config=metadata.config, + tools=metadata.tools, + ), + ), + }, + upsert=True, + ) + except Exception as e: + err = f"[MCPServiceLoader] 更新 MongoDB 失败:{e}" + logger.exception(err) + raise RuntimeError(err) from e + + # 向量化所有数据并保存 + service_table = await LanceDB().get_table("mcp_service") + + # 删除重复的ID + await service_table.delete(f"id = '{metadata.id}'") + + # 进行向量化,更新LanceDB + service_vecs = await Embedding.get_embedding([metadata.description]) + service_vector_data = [ + ServicePoolVector( + id=metadata.id, + embedding=service_vecs[0], + ), + ] + await service_table.add(service_vector_data) diff --git a/apps/scheduler/pool/loader/metadata.py b/apps/scheduler/pool/loader/metadata.py index 84f11044..310ea0a3 100644 --- a/apps/scheduler/pool/loader/metadata.py +++ b/apps/scheduler/pool/loader/metadata.py @@ -17,6 +17,12 @@ from apps.entities.flow import ( AppMetadata, ServiceMetadata, ) +from apps.entities.mcp import ( + MCPServiceMetadata, + AgentAppMetadata +) +from apps.entities.model import ModelMetadata +from apps.entities.prompt import PromptMetadata from apps.scheduler.util import yaml_str_presenter logger = logging.getLogger(__name__) @@ -25,7 +31,8 @@ logger = logging.getLogger(__name__) class MetadataLoader: """元数据加载器""" - async def load_one(self, file_path: Path) -> AppMetadata | ServiceMetadata | None: + async def load_one(self, + file_path: Path) -> AppMetadata | ServiceMetadata | MCPServiceMetadata | AgentAppMetadata | PromptMetadata | None: """加载单个元数据""" # 检查yaml格式 try: @@ -43,7 +50,7 @@ class MetadataLoader: raise RuntimeError(err) from e # 尝试匹配格式 - if metadata_type == MetadataType.APP.value: + if metadata_type == MetadataType.FLOW_APP.value: try: app_id = file_path.parent.name metadata = AppMetadata(id=app_id, **metadata_dict) @@ -59,6 +66,39 @@ class MetadataLoader: err = "[MetadataLoader] Service metadata.yaml格式错误" logger.exception(err) raise RuntimeError(err) from e + elif metadata_type == MetadataType.MCP_SERVICE.value: + try: + mcpservice_id = file_path.parent.name + metadata = MCPServiceMetadata(id=mcpservice_id, **metadata_dict) + except Exception as e: + err = "[MetadataLoader] MCP Service metadata.yaml格式错误" + logger.exception(err) + raise RuntimeError(err) from e + pass + elif metadata_type == MetadataType.AGENT_APP.value: + try: + app_id = file_path.parent.name + metadata = AgentAppMetadata(id=app_id, **metadata_dict) + except Exception as e: + err = "[MetadataLoader] Agent APP metadata.yaml格式错误" + logger.exception(err) + raise RuntimeError(err) from e + elif metadata_type == MetadataType.MODEL.value: + try: + model_id = file_path.parent.name + metadata = ModelMetadata(id=model_id, **metadata_dict) + except Exception as e: + err = "[MetadataLoader] Model metadata.yaml格式错误" + logger.exception(err) + raise RuntimeError(err) from e + elif metadata_type == MetadataType.PROMPT.value: + try: + prompt_id = file_path.parent.name + metadata = PromptMetadata(id=prompt_id, **metadata_dict) + except Exception as e: + err = "[MetadataLoader] Model metadata.yaml格式错误" + logger.exception(err) + raise RuntimeError(err) from e else: err = f"[MetadataLoader] metadata.yaml类型错误: {metadata_type}" logger.error(err) @@ -67,25 +107,49 @@ class MetadataLoader: return metadata async def save_one( - self, - metadata_type: MetadataType, - metadata: dict[str, Any] | AppMetadata | ServiceMetadata, - resource_id: str, + self, + metadata_type: MetadataType, + metadata: + dict[str, Any] | AppMetadata | MCPServiceMetadata | ServiceMetadata | ModelMetadata | AgentAppMetadata | PromptMetadata, + resource_id: str, ) -> None: """保存单个元数据""" class_dict = { - MetadataType.APP: AppMetadata, + MetadataType.MCP_SERVICE: MCPServiceMetadata, + MetadataType.AGENT_APP: AgentAppMetadata, + MetadataType.FLOW_APP: AppMetadata, MetadataType.SERVICE: ServiceMetadata, + MetadataType.MODEL: ModelMetadata, + MetadataType.PROMPT: PromptMetadata, } # 检查资源路径 - if metadata_type == MetadataType.APP.value: + if metadata_type == MetadataType.FLOW_APP.value: resource_path = ( - Path(Config().get_config().deploy.data_dir) / "semantics" / "app" / resource_id / "metadata.yaml" + Path(Config().get_config().deploy.data_dir) / "semantics" / "app" / resource_id / "metadata.yaml" ) elif metadata_type == MetadataType.SERVICE.value: resource_path = ( - Path(Config().get_config().deploy.data_dir) / "semantics" / "service" / resource_id / "metadata.yaml" + Path( + Config().get_config().deploy.data_dir) / "semantics" / "service" / resource_id / "metadata.yaml" + ) + elif metadata_type == MetadataType.MCP_SERVICE.value: + resource_path = ( + Path(Config().get_config().deploy.data_dir) / "mcp" / "service" / resource_id / "metadata.yaml" + ) + elif metadata_type == MetadataType.AGENT_APP.value: + resource_path = ( + Path(Config().get_config().deploy.data_dir) / "agent" / "app" / resource_id / "metadata.yaml" + ) + elif metadata_type == MetadataType.MODEL.value: + resource_path = ( + Path( + Config().get_config().deploy.data_dir) / "modelcenter" / "model" / resource_id / "metadata.yaml" + ) + elif metadata_type == MetadataType.PROMPT.value: + resource_path = ( + Path( + Config().get_config().deploy.data_dir) / "prompt" / "prompt" / resource_id / "metadata.yaml" ) else: err = f"[MetadataLoader] metadata_type类型错误: {metadata_type}" @@ -96,7 +160,9 @@ class MetadataLoader: if isinstance(metadata, dict): try: # 检查类型匹配 - metadata_class: type[AppMetadata | ServiceMetadata] = class_dict[metadata_type] + metadata_class: type[ + AppMetadata | MCPServiceMetadata | ServiceMetadata | ModelMetadata | AgentAppMetadata | PromptMetadata] = class_dict[ + metadata_type] data = metadata_class(**metadata) except Exception as e: err = "[MetadataLoader] metadata.yaml格式错误" diff --git a/apps/scheduler/pool/loader/model.py b/apps/scheduler/pool/loader/model.py new file mode 100644 index 00000000..b9d46553 --- /dev/null +++ b/apps/scheduler/pool/loader/model.py @@ -0,0 +1,109 @@ +""" +App加载器 + +Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. +""" + +import logging +import shutil + +from anyio import Path +from fastapi.encoders import jsonable_encoder + +from apps.common.config import Config +from apps.entities.model import ModelMetadata +from apps.entities.pool import ModelPool +from apps.models.mongo import MongoDB +from apps.scheduler.pool.check import FileChecker +from apps.scheduler.pool.loader.metadata import MetadataLoader, MetadataType + +logger = logging.getLogger(__name__) + + +class ModelLoader: + """模型加载器""" + + async def load(self, model_id: str, hashes: dict[str, str]) -> None: + """ + 从文件系统中加载模型 + + :param model_id: 模型 ID + """ + model_path = Path(Config().get_config().deploy.data_dir) / "modelcenter" / "model" / model_id + metadata_path = model_path / "metadata.yaml" + metadata = await MetadataLoader().load_one(metadata_path) + if not metadata: + err = f"[ModelLoader] 元数据不存在: {metadata_path}" + raise ValueError(err) + metadata.hashes = hashes + + if not isinstance(metadata, ModelMetadata): + err = f"[ModelLoader] 元数据类型错误: {metadata_path}" + raise TypeError(err) + + try: + metadata = ModelMetadata.model_validate(metadata) + except Exception as e: + err = "[ModelLoader] 元数据验证失败" + logger.exception(err) + raise RuntimeError(err) from e + await self._update_db(metadata) + + async def save(self, metadata: ModelMetadata, model_id: str) -> None: + """ + 保存模型 + + :param metadata: 模型元数据 + :param model_id: 模型 ID + """ + # 创建文件夹 + model_path = Path(Config().get_config().deploy.data_dir) / "modelcenter" / "model" / model_id + if not await model_path.exists(): + await model_path.mkdir(parents=True, exist_ok=True) + # 保存元数据 + await MetadataLoader().save_one(MetadataType.MODEL, metadata, model_id) + # 重新载入 + file_checker = FileChecker(service_type="modelcenter") + await file_checker.diff_one(model_path) + await self.load(model_id, file_checker.hashes[f"model/{model_id}"]) + + async def delete(self, model_id: str, *, is_reload: bool = False) -> None: + """ + 删除Model,并更新数据库 + + :param model_id: 模型 ID + """ + try: + model_collection = MongoDB.get_collection("model") + await model_collection.delete_one({"_id": model_id}) # 删除模型数据 + except Exception: + logger.exception("[ModelLoader] MongoDB删除Model失败") + + if not is_reload: + model_path = Path(Config().get_config().deploy.data_dir) / "modelcenter" / "model" / model_id + if await model_path.exists(): + shutil.rmtree(str(model_path), ignore_errors=True) + + async def _update_db(self, metadata: ModelMetadata) -> None: + """更新数据库""" + if not metadata.hashes: + err = f"[ModelLoader] 模型 {metadata.id} 的哈希值为空" + logger.error(err) + raise ValueError(err) + # 更新模型数据 + try: + model_collection = MongoDB.get_collection("model") + await model_collection.update_one( + {"_id": metadata.id}, + { + "$set": jsonable_encoder( + ModelPool( + _id=metadata.id, + **(metadata.model_dump(by_alias=True)), + ), + ), + }, + upsert=True, + ) + except Exception: + logger.exception("[ModelLoader] 更新 MongoDB 失败") diff --git a/apps/scheduler/pool/loader/prompt.py b/apps/scheduler/pool/loader/prompt.py new file mode 100644 index 00000000..1b779c9c --- /dev/null +++ b/apps/scheduler/pool/loader/prompt.py @@ -0,0 +1,109 @@ +""" +App加载器 + +Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. +""" + +import logging +import shutil + +from anyio import Path +from fastapi.encoders import jsonable_encoder + +from apps.common.config import Config +from apps.entities.prompt import PromptMetadata +from apps.entities.pool import PromptPool +from apps.models.mongo import MongoDB +from apps.scheduler.pool.check import FileChecker +from apps.scheduler.pool.loader.metadata import MetadataLoader, MetadataType + +logger = logging.getLogger(__name__) + + +class PromptLoader: + """Prompt加载器""" + + async def load(self, prompt_id: str, hashes: dict[str, str]) -> None: + """ + 从文件系统中加载Prompt + + :param prompt_id: Prompt ID + """ + prompt_path = Path(Config().get_config().deploy.data_dir) / "prompt" / "prompt" / prompt_id + metadata_path = prompt_path / "metadata.yaml" + metadata = await MetadataLoader().load_one(metadata_path) + if not metadata: + err = f"[PromptLoader] 元数据不存在: {metadata_path}" + raise ValueError(err) + metadata.hashes = hashes + + if not isinstance(metadata, PromptMetadata): + err = f"[PromptLoader] 元数据类型错误: {metadata_path}" + raise TypeError(err) + + try: + metadata = PromptMetadata.model_validate(metadata) + except Exception as e: + err = "[PromptLoader] 元数据验证失败" + logger.exception(err) + raise RuntimeError(err) from e + await self._update_db(metadata) + + async def save(self, metadata: PromptMetadata, prompt_id: str) -> None: + """ + 保存Prompt + + :param metadata: Prompt元数据 + :param prompt_id: Prompt ID + """ + # 创建文件夹 + prompt_path = Path(Config().get_config().deploy.data_dir) / "prompt" / "prompt" / prompt_id + if not await prompt_path.exists(): + await prompt_path.mkdir(parents=True, exist_ok=True) + # 保存元数据 + await MetadataLoader().save_one(MetadataType.MODEL, metadata, prompt_id) + # 重新载入 + file_checker = FileChecker(service_type="prompt") + await file_checker.diff_one(prompt_path) + await self.load(prompt_id, file_checker.hashes[f"prompt/{prompt_id}"]) + + async def delete(self, prompt_id: str, *, is_reload: bool = False) -> None: + """ + 删除Model,并更新数据库 + + :param prompt_id: Prompt ID + """ + try: + prompt_collection = MongoDB.get_collection("prompt") + await prompt_collection.delete_one({"_id": prompt_id}) # 删除Prompt数据 + except Exception: + logger.exception("[PromptLoader] MongoDB删除Model失败") + + if not is_reload: + prompt_path = Path(Config().get_config().deploy.data_dir) / "prompt" / "prompt" / prompt_id + if await prompt_path.exists(): + shutil.rmtree(str(prompt_path), ignore_errors=True) + + async def _update_db(self, metadata: PromptMetadata) -> None: + """更新数据库""" + if not metadata.hashes: + err = f"[PromptLoader] Prompt {metadata.id} 的哈希值为空" + logger.error(err) + raise ValueError(err) + # 更新Prompt数据 + try: + model_collection = MongoDB.get_collection("prompt") + await model_collection.update_one( + {"_id": metadata.id}, + { + "$set": jsonable_encoder( + PromptPool( + _id=metadata.id, + **(metadata.model_dump(by_alias=True)), + ), + ), + }, + upsert=True, + ) + except Exception: + logger.exception("[PromptLoader] 更新 MongoDB 失败") diff --git a/apps/scheduler/pool/pool.py b/apps/scheduler/pool/pool.py index e45926a2..5364fab9 100644 --- a/apps/scheduler/pool/pool.py +++ b/apps/scheduler/pool/pool.py @@ -76,7 +76,7 @@ class Pool(metaclass=SingletonMeta): # 加载App logger.info("[Pool] 载入App") - changed_app, deleted_app = await checker.diff(MetadataType.APP) + changed_app, deleted_app = await checker.diff(MetadataType.FLOW_APP) app_loader = AppLoader() # 批量删除App -- Gitee From 0ac120606480ad03bd392a9131a272b52128d3d5 Mon Sep 17 00:00:00 2001 From: Wang Kui Date: Wed, 30 Apr 2025 10:35:50 +0800 Subject: [PATCH 2/4] add team kb and model_list kb_list in conversion --- apps/common/provider.yaml | 25 ++++++++ apps/common/read_conf_yaml.py | 35 +++++++++++ apps/entities/appcenter.py | 4 +- apps/entities/collection.py | 4 ++ apps/entities/enum_var.py | 32 ++++++++++ apps/entities/knowledge.py | 22 +++++++ apps/entities/mcp.py | 9 ++- apps/entities/model.py | 1 + apps/entities/response_data.py | 54 +++++++++++++++++ apps/entities/team.py | 40 +++++++++++++ apps/main.py | 10 ++++ apps/manager/model.py | 17 ++---- apps/routers/conversation.py | 31 +++++++++- apps/routers/knowledge_base.py | 103 +++++++++++++++++++++++++++++++++ apps/routers/mcp_service.py | 4 +- apps/routers/prompt.py | 2 +- apps/routers/team.py | 102 ++++++++++++++++++++++++++++++++ apps/service/rag.py | 44 ++++++++++++++ 18 files changed, 519 insertions(+), 20 deletions(-) create mode 100644 apps/common/provider.yaml create mode 100644 apps/common/read_conf_yaml.py create mode 100644 apps/entities/knowledge.py create mode 100644 apps/entities/team.py create mode 100644 apps/routers/knowledge_base.py create mode 100644 apps/routers/team.py diff --git a/apps/common/provider.yaml b/apps/common/provider.yaml new file mode 100644 index 00000000..e4aabfa4 --- /dev/null +++ b/apps/common/provider.yaml @@ -0,0 +1,25 @@ +providers: +- provider: Ollama + description: "LLM, TEXT, EMBEDDING, SPEECH2TEXT, MODERATION" + url: "" +- provider: VLLM + description: "LLM, TEXT, EMBEDDING, SPEECH2TEXT, MODERATION" + url: "" +- provider: Tongyi-Qianwen + description: "LLM, TEXT, EMBEDDING, SPEECH2TEXT, MODERATION" + url: "" +- provider: XunFei Spark + description: "LLM" + url: "" +- provider: BaiChuan + description: "LLM, TEXT, EMBEDDING" + url: "" +- provider: BaiduYiyan + description: "LLM" + url: "" +- provider: ModelScope + description: "LLM" + url: "" +- provider: SICICONFLOW + description: "LLM, TEXT, EMBEDDING, TEXT RE-RANK" + url: "" diff --git a/apps/common/read_conf_yaml.py b/apps/common/read_conf_yaml.py new file mode 100644 index 00000000..6bb2036b --- /dev/null +++ b/apps/common/read_conf_yaml.py @@ -0,0 +1,35 @@ +import os + +import yaml + + +class ReadConfYaml: + def __init__(self, config_path): + self.config_path = config_path + self.conf = {} + self.__init_conf() + + def __init_conf(self): + if os.path.exists(self.config_path): + with open(self.config_path, 'r', encoding="utf-8") as f: + self.conf = yaml.load(f.read(), Loader=yaml.FullLoader) + + def get(self, primary_parameter, secondary_parameter=None, tertiary_parameter=None): + result = "" + try: + result = self.conf[primary_parameter] + if secondary_parameter: + result = result[secondary_parameter] + if tertiary_parameter: + result = result[tertiary_parameter] + except Exception as e: + raise Exception(f'read config failed: {e}') + return result + + def write_yaml(self): + os.makedirs(os.path.dirname(self.config_path), exist_ok=True) + with open(self.config_path, encoding='utf-8', mode='w') as f: + yaml.dump(self.conf, stream=f, allow_unicode=True, sort_keys=False) + + +provider_conf = ReadConfYaml(os.path.abspath("provider.yaml")) diff --git a/apps/entities/appcenter.py b/apps/entities/appcenter.py index e4d7902f..1ce73dc5 100644 --- a/apps/entities/appcenter.py +++ b/apps/entities/appcenter.py @@ -8,7 +8,7 @@ from pydantic import BaseModel, Field from apps.entities.enum_var import PermissionType, AppType from apps.entities.model import ModelMetadata -from apps.entities.mcp import MCPServiceMetadata +from apps.entities.mcp import MCPMeta class AppCenterCardItem(BaseModel): @@ -76,5 +76,5 @@ class AppData(BaseModel): # Agent APP model: ModelMetadata = Field(default=None, description="模型选择") prompt: str = Field(default="", description="提示词") - mcp_service: list[MCPServiceMetadata] = Field(default=[], alias="mcpService", description="MCP服务") + mcp_service: list[MCPMeta] = Field(default=[], alias="mcpService", description="MCP服务") knowledge: list[str] = Field(default=None, description="知识库") diff --git a/apps/entities/collection.py b/apps/entities/collection.py index af0b5a88..4dd90742 100644 --- a/apps/entities/collection.py +++ b/apps/entities/collection.py @@ -10,6 +10,8 @@ from datetime import UTC, datetime from pydantic import BaseModel, Field from apps.constants import NEW_CHAT +from apps.entities.model import ModelCenterCardItem +from apps.entities.knowledge import KnowledgeBaseMetaData class Blacklist(BaseModel): @@ -81,6 +83,8 @@ class Conversation(BaseModel): unused_docs: list[str] = [] record_groups: list[str] = [] debug : bool = Field(default=False) + model: ModelCenterCardItem | None = Field(default=[]) + kb_list: list[KnowledgeBaseMetaData] = Field(alias="kbList", default=[]) class Document(BaseModel): diff --git a/apps/entities/enum_var.py b/apps/entities/enum_var.py index e31e3f1d..e536ce3a 100644 --- a/apps/entities/enum_var.py +++ b/apps/entities/enum_var.py @@ -175,7 +175,39 @@ class MCPServiceToolsArgsType(str, Enum): BOOLEAN = "boolean" +class MCPSearchType(str, Enum): + """搜索类型""" + + ALL = "all" + NAME = "name" + AUTHOR = "author" + + class AppType(str, Enum): """应用中心应用类型""" FLOW = "flow" AGENT = "agent" + + +class Tokenizer(str, Enum): + """搜索类型""" + + CHINESE = "中文" + ENGLISH = "en" + MIXTURE = "mix" + + +class ParserMethod(str, Enum): + """搜索类型""" + + GENERAL = "general" + OCR = "ocr" + ENHANCED = "enhanced" + QUESTION_ANSWER = "qa" + + +class TeamSearchType(str, Enum): + """团队类型""" + MYCREATED = "mycreated" + MYJOINED = "myjoined" + ALL = "all" diff --git a/apps/entities/knowledge.py b/apps/entities/knowledge.py new file mode 100644 index 00000000..12137f2a --- /dev/null +++ b/apps/entities/knowledge.py @@ -0,0 +1,22 @@ +""" +模型配置相关 API 基础数据结构定义 + +Copyright (c) Huawei Technologies Co., Ltd. 2024-2025. All rights reserved. +""" + +from pydantic import BaseModel, Field +from apps.entities.enum_var import Tokenizer, ParserMethod + + +class KnowledgeBaseMetaData(BaseModel): + """知识库元数据结构""" + + kb_id: str = Field(alias="kbId", description="知识库ID") + kb_name: str = Field(alias="kbName", description="知识库名称") + description: str = Field(description="知识库描述") + + +class KnowledgeBaseItem(KnowledgeBaseMetaData): + """知识库数据结构""" + + is_used: bool = Field(alias="isUsed", description="是否已使用", default=False) diff --git a/apps/entities/mcp.py b/apps/entities/mcp.py index 17ec2ee2..7cd8d3cf 100644 --- a/apps/entities/mcp.py +++ b/apps/entities/mcp.py @@ -18,8 +18,14 @@ from apps.entities.enum_var import ( AppType, ) +class MCPMeta(BaseModel): + """ + MCPService或MCPApp的查询元数据 + """ + id: str = Field(description="元数据ID") -class MCPMetadataBase(BaseModel): + +class MCPMetadataBase(MCPMeta): """ MCPService或MCPApp的元数据 @@ -27,7 +33,6 @@ class MCPMetadataBase(BaseModel): """ type: MetadataType = Field(description="元数据类型") - id: str = Field(description="元数据ID") icon: str = Field(description="图标", default="") name: str = Field(description="元数据名称") description: str = Field(description="元数据描述") diff --git a/apps/entities/model.py b/apps/entities/model.py index 647a630e..9064d1e7 100644 --- a/apps/entities/model.py +++ b/apps/entities/model.py @@ -39,3 +39,4 @@ class ProviderCardItem(BaseModel): provider: str = Field(..., description="供应商名称") description: str = Field(default="", description="供应商介绍") + url: str = Field(default="", description="供应商api地址") diff --git a/apps/entities/response_data.py b/apps/entities/response_data.py index 2c929843..0e9e84e8 100644 --- a/apps/entities/response_data.py +++ b/apps/entities/response_data.py @@ -22,6 +22,9 @@ from apps.entities.flow_topology import ( from apps.entities.mcp import MCPServiceConfig, MCPServiceToolsdata from apps.entities.record import RecordData from apps.entities.user import UserInfo +from apps.entities.team import TeamKnowledgeBase +from apps.entities.knowledge import KnowledgeBaseItem, KnowledgeBaseMetaData +from apps.entities.team import TeamItem class ResponseData(BaseModel): @@ -97,6 +100,8 @@ class ConversationListItem(BaseModel): created_time: str = Field(alias="createdTime") app_id: str = Field(alias="appId") debug: bool = Field(alias="debug") + model: ModelCenterCardItem | None = Field(default=[]) + kb_list: list[KnowledgeBaseMetaData] = Field(alias="kbList", default=[]) class ConversationListMsg(BaseModel): @@ -616,3 +621,52 @@ class GetPromptListRsp(ResponseData): """GET /api/prompt 返回数据结构""" result: GetPromptListMsg + + +class GetTeamKbListMsg(BaseModel): + """GET /api/kb Result数据结构""" + + tkb_list: list[TeamKnowledgeBase] = Field(..., alias="tkbList", description="团队知识库列表") + + +class GetTeamKbListRsp(ResponseData): + """GET /api/kb 返回数据结构""" + + result: GetTeamKbListMsg + + +class GetKbDetailMsg(BaseModel): + """GET /api/kb Result数据结构""" + + kb_detail: KnowledgeBaseItem = Field(..., alias="kbDetail", description="知识库详情") + + +class GetKbDetailRsp(ResponseData): + """GET /api/kb 返回数据结构""" + + result: GetKbDetailMsg + + +class GetTeamsMsg(BaseModel): + """GET /api/kb Result数据结构""" + + total: int = Field(..., description="团队总数") + teams: list[TeamItem] = Field(..., description="团队列表") + + +class GetTeamsRsp(ResponseData): + """GET /api/kb 返回数据结构""" + + result: GetTeamsMsg + + +class GetTeamDetailMsg(BaseModel): + """GET /api/kb Result数据结构""" + + team: TeamItem = Field(..., description="团队详情") + + +class GetTeamDetailRsp(ResponseData): + """GET /api/kb 返回数据结构""" + + result: GetTeamDetailMsg diff --git a/apps/entities/team.py b/apps/entities/team.py new file mode 100644 index 00000000..0dcdb655 --- /dev/null +++ b/apps/entities/team.py @@ -0,0 +1,40 @@ +""" +模型配置相关 API 基础数据结构定义 + +Copyright (c) Huawei Technologies Co., Ltd. 2024-2025. All rights reserved. +""" + +from pydantic import BaseModel, Field +from enum import Enum + +from apps.entities.knowledge import KnowledgeBaseItem +from apps.entities.enum_var import TeamSearchType + + +class TeamQueryReq(BaseModel): + """查询团队时的POST请求体""" + + team_type: TeamSearchType = Field(default=TeamSearchType.ALL, alias="teamType", description="筛选团队类型") + team_id: str = Field(alias="teamId", description="团队id", default=None) + team_name: str = Field(alias="teamName", description="团队名称", default=None) + page: int = Field(alias="page", ge=1, description="页码", default=1) + page_size: int = Field(alias="pageSize", ge=1, le=100, description="每页条数", default=40) + + +class TeamItem(BaseModel): + """团队数据结构""" + + team_id: str = Field(..., alias="teamId", description="团队ID") + team_name: str = Field(..., alias="teamName", description="团队名称") + description: str = Field(..., description="团队描述") + author_name: str = Field(..., alias="authorName", description="团队创建者用户ID") + member_count: int = Field(..., alias="memberCount", description="团队成员数量") + is_public: bool = Field(..., alias="isPublic", description="是否为公开团队") + + +class TeamKnowledgeBase(BaseModel): + """团队知识库数据结构""" + + team_id: str = Field(..., alias="teamId", description="团队ID") + team_name: str = Field(..., alias="teamName", description="团队名称") + kb_list: list[KnowledgeBaseItem] = Field(..., alias="kbList", description="知识库列表") diff --git a/apps/main.py b/apps/main.py index 702a512c..b8a1b5e0 100644 --- a/apps/main.py +++ b/apps/main.py @@ -33,8 +33,13 @@ from apps.routers import ( flow, health, knowledge, + knowledge_base, + mcp_service, + model, + prompt, record, service, + team, user, ) from apps.scheduler.pool.pool import Pool @@ -64,8 +69,13 @@ app.include_router(client.router) app.include_router(blacklist.router) app.include_router(document.router) app.include_router(knowledge.router) +app.include_router(knowledge_base.router) +app.include_router(mcp_service.router) +app.include_router(model.router) +app.include_router(prompt.router) app.include_router(flow.router) app.include_router(user.router) +app.include_router(team.router) # logger配置 LOGGER_FORMAT = "%(funcName)s() - %(message)s" diff --git a/apps/manager/model.py b/apps/manager/model.py index 3d5bcf9f..dd4802ca 100644 --- a/apps/manager/model.py +++ b/apps/manager/model.py @@ -8,6 +8,7 @@ import logging import uuid from typing import Any +from apps.common.read_conf_yaml import provider_conf from apps.entities.model import ModelData, ModelMetadata, ModelCenterCardItem, ProviderCardItem from apps.entities.pool import ModelPool, ProviderModelPool from apps.exceptions import InstancePermissionError @@ -71,21 +72,13 @@ class ModelCenterManager: :return: 供应商列表,供应商个数 """ - providers = { - "Ollama": "Ollama 的介绍", - "VLLM": "VLLM 的介绍", - "Tongyi-Qianwen": "Tongyi-Qianwen 的介绍", - "XunFei Spark": "XunFei Spark 的介绍", - "BaiChuan": "BaiChuan 的介绍", - "BaiduYiyan": "BaiduYiyan 的介绍", - "ModelScope": "ModelScope 的介绍", - } provider_list = list[ProviderCardItem] - for key, value in providers: + for provider in provider_conf.get("providers", {}): provider_list.append( ProviderCardItem( - provider=key, - description=value, + provider=provider.get("provider", ""), + description=provider.get("description", ""), + url=provider.get("url", ""), ) ) return provider_list diff --git a/apps/routers/conversation.py b/apps/routers/conversation.py index 92872ed3..547df3f8 100644 --- a/apps/routers/conversation.py +++ b/apps/routers/conversation.py @@ -98,6 +98,8 @@ async def get_conversation_list(user_sub: Annotated[str, Depends(get_user)]) -> ), appId=conv.app_id if conv.app_id else "", debug=conv.debug if conv.debug else False, + modelId = conv.modelId, + kbIds = conv.kbIds, ) for conv in conversations ] @@ -126,6 +128,8 @@ async def get_conversation_list(user_sub: Annotated[str, Depends(get_user)]) -> ), appId=new_conv.app_id if new_conv.app_id else "", debug=new_conv.debug if new_conv.debug else False, + modelId = new_conv.modelId, + kbIds = new_conv.kbIds, ), ) @@ -187,6 +191,8 @@ async def update_conversation( post_body: ModifyConversationData, conversation_id: Annotated[str, Query(..., alias="conversationId")], user_sub: Annotated[str, Depends(get_user)], + model_id: Annotated[str, Query(alias="modelId")] = "", + kb_ids: Annotated[list[str], Query(alias="kbIds")] = [], ) -> JSONResponse: """更新特定Conversation的数据""" # 判断Conversation是否合法 @@ -201,13 +207,34 @@ async def update_conversation( result={}, ).model_dump(exclude_none=True, by_alias=True), ) - + model = conv.model + if model_id: + try: + model_data = await ModelCenterManager.fetch_model_data_by_id(model_id) + except Exception as exp: + logger.warning(exp) + model = model_data + kb_list = conv.kbList + if kb_ids: + team_knowledge = await RAG.get_rag_kb_result(user_sub) + for kb_id in kb_ids: + flag = False + for tkb in team_knowledge: + for kb in tkb.kb_list: + if kb.kb_id == kb_id: + kb_list.append(kb) + flag = True + break + if flag: + break # 更新Conversation数据 change_status = await ConversationManager.update_conversation_by_conversation_id( user_sub, conversation_id, { "title": post_body.title, + "model": model, + "kbList": kb_list }, ) @@ -235,6 +262,8 @@ async def update_conversation( ), appId=conv.app_id if conv.app_id else "", debug=conv.debug if conv.debug else False, + model = model, + kbList = kb_list ), ).model_dump(exclude_none=True, by_alias=True), ) diff --git a/apps/routers/knowledge_base.py b/apps/routers/knowledge_base.py new file mode 100644 index 00000000..3863f32b --- /dev/null +++ b/apps/routers/knowledge_base.py @@ -0,0 +1,103 @@ +""" +FastAPI 模型中心相关路由 + +Copyright (c) Huawei Technologies Co., Ltd. 2024-2025. All rights reserved. +""" + +import logging +from typing import Annotated + +from fastapi import APIRouter, Depends, status, Query +from fastapi.responses import JSONResponse + +from apps.dependency.user import get_user, verify_user +from apps.entities.response_data import ( + ResponseData, + GetTeamKbListMsg, + GetTeamKbListRsp, + GetKbDetailMsg, + GetKbDetailRsp +) +from apps.service.rag import RAG + +logger = logging.getLogger(__name__) +router = APIRouter( + prefix="/api/kb", + tags=["knowledge-base"], + dependencies=[Depends(verify_user)], +) + + +@router.get("", response_model=GetTeamKbListRsp | ResponseData) +async def get_team_knowledge_base_list( + user_sub: Annotated[str, Depends(get_user)], + keyword: Annotated[str | None, Query(..., alias="keyword", description="搜索关键字")] = None, +) -> JSONResponse: + """获取资产库列表""" + + try: + team_knowledge = await RAG.get_rag_kb_result(user_sub) + except Exception as exp: + logger.error(exp) + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=ResponseData( + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + message="ERROR", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + + if keyword: + pass + return JSONResponse( + status_code=status.HTTP_200_OK, + content=GetTeamKbListRsp( + code=status.HTTP_200_OK, + message="OK", + result=GetTeamKbListMsg( + result=team_knowledge, + ), + ).model_dump(exclude_none=True, by_alias=True), + ) + + +@router.get("/{kb_id}", response_model=GetKbDetailRsp | ResponseData) +async def get_knowledge_base_detail( + user_sub: Annotated[str, Depends(get_user)], +) -> JSONResponse: + """获取资产库详情""" + try: + team_knowledge = await RAG.get_rag_kb_result_by_kbid(user_sub, kb_id) + except Exception as exp: + logger.error(exp) + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=ResponseData( + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + message="ERROR", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + for tkb in team_knowledge: + for kb in tkb.kb_list: + if kb.kb_id == kb_id: + return JSONResponse( + status_code=status.HTTP_200_OK, + content=GetKbDetailRsp( + code=status.HTTP_200_OK, + message="OK", + result=GetKbDetailMsg( + result=kb, + ), + ).model_dump(exclude_none=True, by_alias=True), + ) + + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=ResponseData( + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + message="ERROR", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) diff --git a/apps/routers/mcp_service.py b/apps/routers/mcp_service.py index 885c4d8e..6541ac44 100644 --- a/apps/routers/mcp_service.py +++ b/apps/routers/mcp_service.py @@ -12,7 +12,7 @@ from fastapi.responses import JSONResponse from apps.dependency.csrf import verify_csrf_token from apps.dependency.user import get_user, verify_user -from apps.entities.enum_var import SearchType +from apps.entities.enum_var import MCPSearchType from apps.entities.request_data import UpdateMCPServiceRequest from apps.entities.response_data import ( BaseMCPServiceOperationMsg, @@ -39,7 +39,7 @@ router = APIRouter( @router.get("", response_model=GetMCPServiceListRsp | ResponseData) async def get_mcpservice_list( # noqa: PLR0913 *, - search_type: Annotated[SearchType, Query(..., alias="searchType", description="搜索类型")] = SearchType.ALL, + search_type: Annotated[MCPSearchType, Query(..., alias="searchType", description="搜索类型")] = MCPSearchType.ALL, keyword: Annotated[str | None, Query(..., alias="keyword", description="搜索关键字")] = None, page: Annotated[int, Query(..., alias="page", ge=1, description="页码")] = 1, page_size: Annotated[int, Query(..., alias="pageSize", ge=1, le=100, description="每页数量")] = 16, diff --git a/apps/routers/prompt.py b/apps/routers/prompt.py index c2a20a7b..4483acf7 100644 --- a/apps/routers/prompt.py +++ b/apps/routers/prompt.py @@ -26,7 +26,7 @@ from apps.manager.prompt import PromptManager logger = logging.getLogger(__name__) router = APIRouter( prefix="/api/prompt", - tags=["model"], + tags=["prompt"], dependencies=[Depends(verify_user)], ) diff --git a/apps/routers/team.py b/apps/routers/team.py new file mode 100644 index 00000000..9aaf4a5f --- /dev/null +++ b/apps/routers/team.py @@ -0,0 +1,102 @@ +""" +FastAPI 模型中心相关路由 + +Copyright (c) Huawei Technologies Co., Ltd. 2024-2025. All rights reserved. +""" + +import logging +from typing import Annotated + +from fastapi import APIRouter, Depends, status +from fastapi.responses import JSONResponse + +from apps.dependency.user import get_user, verify_user +from apps.entities.response_data import ( + ResponseData, + GetTeamsMsg, + GetTeamsRsp, + GetTeamDetailMsg, + GetTeamDetailRsp +) +from apps.service.rag import RAG +from apps.entities.team import TeamQueryReq + + +logger = logging.getLogger(__name__) +router = APIRouter( + prefix="/api/team", + tags=["team"], + dependencies=[Depends(verify_user)], +) + + +@router.get("", response_model=GetTeamsRsp | ResponseData) +async def get_team_list( + user_sub: Annotated[str, Depends(get_user)], + data: TeamQueryReq +) -> JSONResponse: + """获取团队列表""" + try: + teams, total = await RAG.get_rag_team_result(user_sub, data) + except Exception as exp: + logger.error(exp) + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=ResponseData( + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + message="ERROR", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + return JSONResponse( + status_code=status.HTTP_200_OK, + content=GetTeamsRsp( + code=status.HTTP_200_OK, + message="OK", + result=GetTeamsMsg( + total = total, + teams = teams + ), + ).model_dump(exclude_none=True, by_alias=True), + ) + + +@router.get("/{team_id}", response_model=GetTeamDetailRsp | ResponseData) +async def get_team_detail( + user_sub: Annotated[str, Depends(get_user)], +) -> JSONResponse: + """获取团队详情""" + + try: + teams, total = await RAG.get_rag_team_result(user_sub) + except Exception as exp: + logger.error(exp) + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=ResponseData( + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + message="ERROR", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + for team in teams: + if team.team_id == team_id: + return JSONResponse( + status_code=status.HTTP_200_OK, + content=GetTeamDetailRsp( + code=status.HTTP_200_OK, + message="OK", + result=GetTeamDetailMsg( + result=team, + ), + ).model_dump(exclude_none=True, by_alias=True), + ) + + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=ResponseData( + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + message="ERROR", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) diff --git a/apps/service/rag.py b/apps/service/rag.py index 485d6791..2668fe9b 100644 --- a/apps/service/rag.py +++ b/apps/service/rag.py @@ -12,7 +12,9 @@ import httpx from fastapi import status from apps.common.config import Config +from apps.entities.knowledge import KnowledgeBaseItem from apps.entities.rag_data import RAGQueryReq +from apps.entities.team import TeamKnowledgeBase, TeamQueryReq, TeamItem from apps.service import Activity logger = logging.getLogger(__name__) @@ -48,3 +50,45 @@ class RAG: return yield line + + @staticmethod + async def get_rag_kb_result(user_sub: str) -> AsyncGenerator[TeamKnowledgeBase, []]: + """获取RAG服务的结果""" + url = Config().get_config().rag.rag_service.rstrip("/") + "/kb" + headers = { + "Content-Type": "application/json", + } + + # asyncio HTTP请求 + async with ( + httpx.AsyncClient(timeout=300, verify=False) as client, # noqa: S501 + client.stream("GET", url, headers=headers) as response, + ): + if response.status_code != status.HTTP_200_OK: + logger.error("[RAG] RAG服务返回错误码: %s\n%s", response.status_code, await response.aread()) + return [] + response_data = response.json() + base_filter = {} + return [TeamKnowledgeBase.model_validate(item) for item in response_data["data"]] + + @staticmethod + async def get_rag_team_result(user_sub: str, data: TeamQueryReq) -> tuple[AsyncGenerator[TeamItem, []], int]: + """获取RAG服务的结果""" + url = Config().get_config().rag.rag_service.rstrip("/") + "/team/list" + headers = { + "Content-Type": "application/json", + } + body = json.dumps(data.model_dump(exclude_none=True, by_alias=True), ensure_ascii=False) + + # asyncio HTTP请求 + async with ( + httpx.AsyncClient(timeout=300, verify=False) as client, # noqa: S501 + client.stream("POST", url, headers=headers, content=body) as response, + ): + if response.status_code != status.HTTP_200_OK: + logger.error("[RAG] RAG服务返回错误码: %s\n%s", response.status_code, await response.aread()) + return + + response_data = response.json() + data = response_data["data"] + return [TeamItem.model_validate(item) for item in data["teams"]], data["total"] -- Gitee From 5b7af0c8709d34b67daf41bae3dfd71fa15dcbdb Mon Sep 17 00:00:00 2001 From: Wang Kui Date: Fri, 9 May 2025 04:55:47 +0800 Subject: [PATCH 3/4] merge conflict file and add registry active deactive mcp --- apps/common/read_conf_yaml.py | 7 +- apps/entities/collection.py | 1 + apps/entities/enum_var.py | 4 +- apps/entities/mcp.py | 24 +++-- apps/entities/model.py | 2 +- apps/entities/response_data.py | 24 +++++ apps/manager/mcp_service.py | 12 +-- apps/routers/appcenter.py | 3 - apps/routers/knowledge_base.py | 6 +- apps/routers/mcp_service.py | 160 +++++++++++++++++++++++++++++- apps/routers/model.py | 4 +- apps/routers/prompt.py | 4 +- apps/scheduler/pool/loader/mcp.py | 38 ++++--- apps/service/rag.py | 4 +- 14 files changed, 229 insertions(+), 64 deletions(-) diff --git a/apps/common/read_conf_yaml.py b/apps/common/read_conf_yaml.py index 6bb2036b..c3c4a443 100644 --- a/apps/common/read_conf_yaml.py +++ b/apps/common/read_conf_yaml.py @@ -12,7 +12,7 @@ class ReadConfYaml: def __init_conf(self): if os.path.exists(self.config_path): with open(self.config_path, 'r', encoding="utf-8") as f: - self.conf = yaml.load(f.read(), Loader=yaml.FullLoader) + self.conf = yaml.safe_load(f.read()) def get(self, primary_parameter, secondary_parameter=None, tertiary_parameter=None): result = "" @@ -26,10 +26,5 @@ class ReadConfYaml: raise Exception(f'read config failed: {e}') return result - def write_yaml(self): - os.makedirs(os.path.dirname(self.config_path), exist_ok=True) - with open(self.config_path, encoding='utf-8', mode='w') as f: - yaml.dump(self.conf, stream=f, allow_unicode=True, sort_keys=False) - provider_conf = ReadConfYaml(os.path.abspath("provider.yaml")) diff --git a/apps/entities/collection.py b/apps/entities/collection.py index 4dd90742..6ecf3fe8 100644 --- a/apps/entities/collection.py +++ b/apps/entities/collection.py @@ -64,6 +64,7 @@ class User(BaseModel): app_usage: dict[str, AppUsageData] = {} fav_apps: list[str] = [] fav_services: list[str] = [] + is_admin: bool = Field(default=False, alias="isAdmin") class Conversation(BaseModel): diff --git a/apps/entities/enum_var.py b/apps/entities/enum_var.py index e536ce3a..af2c8706 100644 --- a/apps/entities/enum_var.py +++ b/apps/entities/enum_var.py @@ -159,8 +159,8 @@ class CommentType(str, Enum): NONE = "none" -class MCPTransmitProto(str, Enum): - """MCP传输方式""" +class MCPType(str, Enum): + """MCP 类型""" STDIO = "Stdio" SSE = "SSE" diff --git a/apps/entities/mcp.py b/apps/entities/mcp.py index c7ee0015..9966212c 100644 --- a/apps/entities/mcp.py +++ b/apps/entities/mcp.py @@ -9,9 +9,9 @@ from pydantic import BaseModel, Field from apps.entities.model import ModelMetadata from apps.entities.enum_var import ( - MCPTransmitProto, MetadataType, MCPServiceToolsArgsType, + MCPType, PermissionType, AppType, ) @@ -41,12 +41,12 @@ class MCPMetadataBase(MCPMeta): class MCPServiceConfig(BaseModel): """MCPService的API配置""" - type: MCPTransmitProto = Field( - default=MCPTransmitProto.STDIO, + type: MCPType = Field( + default=MCPType.STDIO, alias="transmitProto", description="传输协议(Stdio/SSE/Streamable)", ) - config: dict[str, Any] = Field(..., description="对应MCP的配置") + config: str = Field(..., description="对应MCP的配置") class MCPServiceToolsArgs(BaseModel): @@ -93,13 +93,6 @@ class AgentAppMetadata(MCPMetadataBase): # TODO 知识库怎么处理 knowledge: list[str] = Field(default=None, description="知识库") -class MCPType(str, Enum): - """MCP 类型""" - - SSE = "sse" - STDIO = "stdio" - STREAMABLE = "stream" - class MCPServerConfig(BaseModel): """MCP 服务器配置""" @@ -150,3 +143,12 @@ class MCPVector(LanceModel): id: str = Field(description="MCP ID") embedding: Vector(dim=1024) = Field(description="MCP描述的向量信息") # type: ignore[call-arg] + + +class MCPMessageType(str, Enum): + """MCP 消息类型""" + + INIT = "initialize" + TOOL_LIST = "tools/list" + TOOL_CALL = "tools/call" + PING = "ping" diff --git a/apps/entities/model.py b/apps/entities/model.py index 9064d1e7..9d953081 100644 --- a/apps/entities/model.py +++ b/apps/entities/model.py @@ -23,7 +23,7 @@ class ModelData(BaseModel): url: str = Field(..., description="链接地址", pattern=r"^(https|http)://.*$") model: str = Field(..., description="模型") api_key: str = Field(..., alias="apiKey", description="模型API密钥") - max_token: int = Field(..., alias="maxToken", description="最大 token 数量限制") + max_token: int | None = Field(default=None, alias="maxToken", description="最大 token 数量限制") class ModelMetadata(ModelData): diff --git a/apps/entities/response_data.py b/apps/entities/response_data.py index 0e9e84e8..22892d7c 100644 --- a/apps/entities/response_data.py +++ b/apps/entities/response_data.py @@ -670,3 +670,27 @@ class GetTeamDetailRsp(ResponseData): """GET /api/kb 返回数据结构""" result: GetTeamDetailMsg + + +class RegistryMCPServiceRsp(ResponseData): + """POST /api/mcp/registry/{serviceId} 返回数据结构""" + + result: BaseMCPServiceOperationMsg = Field(..., title="Result") + + +class ActiveMCPServiceRsp(ResponseData): + """POST /api/mcp/active/{serviceId} 返回数据结构""" + + result: BaseMCPServiceOperationMsg = Field(..., title="Result") + + +class DeactiveMCPServiceRsp(ResponseData): + """POST /api/mcp/deactive/{serviceId} 返回数据结构""" + + result: BaseMCPServiceOperationMsg = Field(..., title="Result") + + +class LoadMCPServiceRsp(ResponseData): + """POST /api/mcp/load/{serviceId} 返回数据结构""" + + result: BaseMCPServiceOperationMsg = Field(..., title="Result") diff --git a/apps/manager/mcp_service.py b/apps/manager/mcp_service.py index 715de5b8..9922ff38 100644 --- a/apps/manager/mcp_service.py +++ b/apps/manager/mcp_service.py @@ -57,7 +57,7 @@ class MCPServiceManager: """创建MCP服务""" mcpservice_id = str(uuid.uuid4()) # 检查是否存在相同服务 - service_collection = MongoDB.get_collection("mcp_service") + service_collection = MongoDB.get_collection("mcp") db_service = await service_collection.find_one( { "name": name, @@ -97,7 +97,7 @@ class MCPServiceManager: ) -> str: """更新服务""" # 验证用户权限 - mcpservice_collection = MongoDB.get_collection("mcp_service") + mcpservice_collection = MongoDB.get_collection("mcp") db_service = await mcpservice_collection.find_one({"_id": mcpservice_id}) if not db_service: msg = "MCPService not found" @@ -130,7 +130,7 @@ class MCPServiceManager: ) -> tuple[str, str, str, MCPServiceConfig]: """获取服务数据""" # 验证用户权限 - mcpservice_collection = MongoDB.get_collection("mcp_service") + mcpservice_collection = MongoDB.get_collection("mcp") match_conditions = { {"author": user_sub} } @@ -152,7 +152,7 @@ class MCPServiceManager: ) -> tuple[str, str, list[MCPServiceToolsdata]]: """获取服务API列表""" # 获取服务名称 - service_collection = MongoDB.get_collection("service") + service_collection = MongoDB.get_collection("mcp") db_service = await service_collection.find_one({"_id": service_id}) if not db_service: msg = "MCPService not found" @@ -167,7 +167,7 @@ class MCPServiceManager: service_id: str, ) -> bool: """删除服务""" - service_collection = MongoDB.get_collection("mcp_service") + service_collection = MongoDB.get_collection("mcp") db_service = await service_collection.find_one({"_id": service_id}) if not db_service: msg = "[MCPServiceCenterManager] Service未找到" @@ -189,7 +189,7 @@ class MCPServiceManager: page_size: int, ) -> tuple[list[MCPServicePool], int]: """基于输入条件获取MCP服务数据""" - mcpservice_collection = MongoDB.get_collection("mcp_service") + mcpservice_collection = MongoDB.get_collection("mcp") # 获取服务总数 total = await mcpservice_collection.count_documents(search_conditions) # 分页查询 diff --git a/apps/routers/appcenter.py b/apps/routers/appcenter.py index 52988323..6deb6fdf 100644 --- a/apps/routers/appcenter.py +++ b/apps/routers/appcenter.py @@ -346,9 +346,6 @@ async def publish_application( result={}, ).model_dump(exclude_none=True, by_alias=True), ) - if not published: - msg = "发布应用失败" - raise ValueError(msg) return JSONResponse( status_code=status.HTTP_200_OK, content=BaseAppOperationRsp( diff --git a/apps/routers/knowledge_base.py b/apps/routers/knowledge_base.py index 3863f32b..a3ed48bb 100644 --- a/apps/routers/knowledge_base.py +++ b/apps/routers/knowledge_base.py @@ -31,12 +31,12 @@ router = APIRouter( @router.get("", response_model=GetTeamKbListRsp | ResponseData) async def get_team_knowledge_base_list( user_sub: Annotated[str, Depends(get_user)], - keyword: Annotated[str | None, Query(..., alias="keyword", description="搜索关键字")] = None, + keyword: Annotated[str | None, Query(alias="keyword", description="搜索关键字")] = None, ) -> JSONResponse: """获取资产库列表""" try: - team_knowledge = await RAG.get_rag_kb_result(user_sub) + team_knowledge = await RAG.get_rag_kb_result(user_sub, keyword) except Exception as exp: logger.error(exp) return JSONResponse( @@ -48,8 +48,6 @@ async def get_team_knowledge_base_list( ).model_dump(exclude_none=True, by_alias=True), ) - if keyword: - pass return JSONResponse( status_code=status.HTTP_200_OK, content=GetTeamKbListRsp( diff --git a/apps/routers/mcp_service.py b/apps/routers/mcp_service.py index 6541ac44..e8d1c760 100644 --- a/apps/routers/mcp_service.py +++ b/apps/routers/mcp_service.py @@ -10,7 +10,6 @@ from typing import Annotated from fastapi import APIRouter, Body, Depends, Path, Query, status from fastapi.responses import JSONResponse -from apps.dependency.csrf import verify_csrf_token from apps.dependency.user import get_user, verify_user from apps.entities.enum_var import MCPSearchType from apps.entities.request_data import UpdateMCPServiceRequest @@ -24,9 +23,14 @@ from apps.entities.response_data import ( ResponseData, UpdateMCPServiceMsg, UpdateMCPServiceRsp, + RegistryMCPServiceRsp, + ActiveMCPServiceRsp, + DeactiveMCPServiceRsp, + LoadMCPServiceRsp ) from apps.exceptions import InstancePermissionError, ServiceIDError from apps.manager.mcp_service import MCPServiceManager +from apps.manager.user import UserManager logger = logging.getLogger(__name__) router = APIRouter( @@ -86,7 +90,7 @@ async def get_mcpservice_list( # noqa: PLR0913 ) -@router.post("", response_model=UpdateMCPServiceRsp, dependencies=[Depends(verify_csrf_token)]) +@router.post("", response_model=UpdateMCPServiceRsp) async def update_mcpservice( user_sub: Annotated[str, Depends(get_user)], data: Annotated[UpdateMCPServiceRequest, Body(..., description="MCP服务对应数据对象")], @@ -211,7 +215,7 @@ async def get_service_detail( return JSONResponse(status_code=status.HTTP_200_OK, content=rsp.model_dump(exclude_none=True, by_alias=True)) -@router.delete("/{serviceId}", response_model=DeleteMCPServiceRsp, dependencies=[Depends(verify_csrf_token)]) +@router.delete("/{serviceId}", response_model=DeleteMCPServiceRsp) async def delete_service( user_sub: Annotated[str, Depends(get_user)], service_id: Annotated[str, Path(..., alias="serviceId", description="服务ID")], @@ -250,3 +254,153 @@ async def delete_service( msg = BaseMCPServiceOperationMsg(serviceId=service_id) rsp = DeleteMCPServiceRsp(code=status.HTTP_200_OK, message="OK", result=msg) return JSONResponse(status_code=status.HTTP_200_OK, content=rsp.model_dump(exclude_none=True, by_alias=True)) + + +@router.post("/registry/{serviceId}", response_model=RegistryMCPServiceRsp) +async def register_mcp_service( + user_sub: Annotated[str, Depends(get_user)], + service_id: Annotated[str, Path(..., alias="serviceId", description="服务ID")], +) -> JSONResponse: + """注册mcp""" + try: + user_data = await UserManager.get_userinfo_by_user_sub(user_sub) + if not user_data or user_data.is_admin: + return JSONResponse( + status_code=status.HTTP_403_FORBIDDEN, + content=ResponseData( + code=status.HTTP_403_FORBIDDEN, + message="非管理员无法注册mcp", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + icon, name, description, config = MCPServiceManager.get_mcpservice_data(user_sub, service_id) + MCPLoader().init_one_template(service_id, config) + except MCPServiceIDError: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=ResponseData( + code=status.HTTP_400_BAD_REQUEST, + message="MCPService ID错误", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + except InstancePermissionError: + return JSONResponse( + status_code=status.HTTP_403_FORBIDDEN, + content=ResponseData( + code=status.HTTP_403_FORBIDDEN, + message="未授权访问", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + except Exception as exp: + logging.error(exp) + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=ResponseData( + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + message="ERROR", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + msg = BaseMCPServiceOperationMsg(serviceId=service_id) + rsp = RegistryMCPServiceRsp(code=status.HTTP_200_OK, message="OK", result=msg) + return JSONResponse(status_code=status.HTTP_200_OK, content=rsp.model_dump(exclude_none=True, by_alias=True)) + + +@router.post("/active/{serviceId}", response_model=ActiveMCPServiceRsp) +async def active_mcp_service( + user_sub: Annotated[str, Depends(get_user)], + service_id: Annotated[str, Path(..., alias="serviceId", description="服务ID")], +) -> JSONResponse: + """激活mcp""" + try: + MCPLoader().user_active_template(usr_sub, service_id) + except FileExistsError as exp: + logging.error(exp) + return JSONResponse( + status_code=status.HTTP_403_FORBIDDEN, + content=ResponseData( + code=status.HTTP_403_FORBIDDEN, + message="文件已存在", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + except Exception as exp: + logging.error(exp) + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=ResponseData( + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + message="ERROR", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + msg = BaseMCPServiceOperationMsg(serviceId=service_id) + rsp = ActiveMCPServiceRsp(code=status.HTTP_200_OK, message="OK", result=msg) + return JSONResponse(status_code=status.HTTP_200_OK, content=rsp.model_dump(exclude_none=True, by_alias=True)) + + +@router.post("/deactive/{serviceId}", response_model=DeactiveMCPServiceRsp) +async def deactive_mcp_service( + user_sub: Annotated[str, Depends(get_user)], + service_id: Annotated[str, Path(..., alias="serviceId", description="服务ID")], +) -> JSONResponse: + """取消激活mcp""" + try: + MCPLoader().user_deactive_template(usr_sub, service_id) + except Exception as exp: + logging.error(exp) + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=ResponseData( + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + message="ERROR", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + msg = BaseMCPServiceOperationMsg(serviceId=service_id) + rsp = DeactiveMCPServiceRsp(code=status.HTTP_200_OK, message="OK", result=msg) + return JSONResponse(status_code=status.HTTP_200_OK, content=rsp.model_dump(exclude_none=True, by_alias=True)) + + +@router.post("/load/{serviceId}", response_model=DeactiveMCPServiceRsp) +async def load_mcp_service( + user_sub: Annotated[str, Depends(get_user)], + service_id: Annotated[str, Path(..., alias="serviceId", description="服务ID")], +) -> JSONResponse: + """取消激活mcp""" + try: + icon, name, description, config = MCPServiceManager.get_mcpservice_data(user_sub, service_id) + MCPLoader().load_one_user(usr_sub, config) + except MCPServiceIDError: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=ResponseData( + code=status.HTTP_400_BAD_REQUEST, + message="MCPService ID错误", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + except InstancePermissionError: + return JSONResponse( + status_code=status.HTTP_403_FORBIDDEN, + content=ResponseData( + code=status.HTTP_403_FORBIDDEN, + message="未授权访问", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + except Exception as exp: + logging.error(exp) + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=ResponseData( + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + message="ERROR", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + msg = BaseMCPServiceOperationMsg(serviceId=service_id) + rsp = DeactiveMCPServiceRsp(code=status.HTTP_200_OK, message="OK", result=msg) + return JSONResponse(status_code=status.HTTP_200_OK, content=rsp.model_dump(exclude_none=True, by_alias=True)) diff --git a/apps/routers/model.py b/apps/routers/model.py index 2a5bc036..47071280 100644 --- a/apps/routers/model.py +++ b/apps/routers/model.py @@ -10,7 +10,6 @@ from typing import Annotated from fastapi import APIRouter, Body, Depends, Path, Query, status from fastapi.responses import JSONResponse -from apps.dependency.csrf import verify_csrf_token from apps.dependency.user import get_user, verify_user from apps.entities.request_data import CreateModelRequest from apps.entities.response_data import ( @@ -67,7 +66,7 @@ async def get_added_models( ) -@router.post("", dependencies=[Depends(verify_csrf_token)], response_model=BaseModelOperationRsp | ResponseData) +@router.post("", response_model=BaseModelOperationRsp | ResponseData) async def create_or_update_model( request: Annotated[CreateModelRequest, Body(...)], user_sub: Annotated[str, Depends(get_user)], @@ -132,7 +131,6 @@ async def create_or_update_model( @router.delete( "/{modelId}", - dependencies=[Depends(verify_csrf_token)], response_model=BaseModelOperationRsp | ResponseData, ) async def delete_model( diff --git a/apps/routers/prompt.py b/apps/routers/prompt.py index 4483acf7..adbf252e 100644 --- a/apps/routers/prompt.py +++ b/apps/routers/prompt.py @@ -10,7 +10,6 @@ from typing import Annotated from fastapi import APIRouter, Body, Depends, Path, Query, status from fastapi.responses import JSONResponse -from apps.dependency.csrf import verify_csrf_token from apps.dependency.user import get_user, verify_user from apps.entities.request_data import CreatePromptRequest from apps.entities.response_data import ( @@ -62,7 +61,7 @@ async def get_prompts( ) -@router.post("", dependencies=[Depends(verify_csrf_token)], response_model=BasePromptOperationRsp | ResponseData) +@router.post("", response_model=BasePromptOperationRsp | ResponseData) async def create_or_update_prompt( request: Annotated[CreatePromptRequest, Body(...)], user_sub: Annotated[str, Depends(get_user)], @@ -127,7 +126,6 @@ async def create_or_update_prompt( @router.delete( "/{promptId}", - dependencies=[Depends(verify_csrf_token)], response_model=BasePromptOperationRsp | ResponseData, ) async def delete_prompt( diff --git a/apps/scheduler/pool/loader/mcp.py b/apps/scheduler/pool/loader/mcp.py index 5659af1a..3e80e2e2 100644 --- a/apps/scheduler/pool/loader/mcp.py +++ b/apps/scheduler/pool/loader/mcp.py @@ -13,14 +13,25 @@ from anyio import Path from fastapi.encoders import jsonable_encoder from apps.common.config import Config -from apps.entities.mcp import MCPServiceMetadata, MCPConfig from apps.entities.pool import MCPServicePool from apps.entities.vector import ServicePoolVector +from apps.entities.mcp import ( + MCPServiceMetadata, + MCPCollection, + MCPConfig, + MCPServerSSEConfig, + MCPServerStdioConfig, + MCPType, +) from apps.llm.embedding import Embedding from apps.models.lance import LanceDB from apps.models.mongo import MongoDB -from apps.scheduler.pool.check import FileChecker -from apps.scheduler.pool.loader.metadata import MetadataLoader, MetadataType +from apps.common.singleton import SingletonMeta +from apps.scheduler.pool.mcp.client import SSEMCPClient, StdioMCPClient +from apps.scheduler.pool.mcp.install import install_npx, install_uvx + +logger = logging.getLogger(__name__) +MCP_PATH = Path(Config().get_config().deploy.data_dir) / "semantics" / "mcp" logger = logging.getLogger(__name__) SERVICE_PATH = Path(Config().get_config().deploy.data_dir) / "semantics" / "service" @@ -60,7 +71,7 @@ class MCPServiceLoader: async def delete(self, service_id: str, *, is_reload: bool = False) -> None: """删除MCPService,并更新数据库""" - service_collection = MongoDB.get_collection("mcp_service") + service_collection = MongoDB.get_collection("mcp") try: await service_collection.delete_one({"_id": service_id}) except Exception: @@ -68,7 +79,7 @@ class MCPServiceLoader: try: # 获取 LanceDB 表 - service_table = await LanceDB().get_table("mcp_service") + service_table = await LanceDB().get_table("mcp") # 删除数据 await service_table.delete(f"id = '{service_id}'") @@ -87,7 +98,7 @@ class MCPServiceLoader: logger.error(err) raise ValueError(err) # 更新MongoDB - service_collection = MongoDB.get_collection("mcp_service") + service_collection = MongoDB.get_collection("mcp") try: # 插入或更新 Service await service_collection.update_one( @@ -128,21 +139,6 @@ class MCPServiceLoader: ), ] await service_table.add(service_vector_data) -from apps.common.singleton import SingletonMeta -from apps.entities.mcp import ( - MCPCollection, - MCPConfig, - MCPServerSSEConfig, - MCPServerStdioConfig, - MCPType, -) -from apps.models.lance import LanceDB -from apps.models.mongo import MongoDB -from apps.scheduler.pool.mcp.client import SSEMCPClient, StdioMCPClient -from apps.scheduler.pool.mcp.install import install_npx, install_uvx - -logger = logging.getLogger(__name__) -MCP_PATH = Path(Config().get_config().deploy.data_dir) / "semantics" / "mcp" class MCPLoader(metaclass=SingletonMeta): diff --git a/apps/service/rag.py b/apps/service/rag.py index 2668fe9b..0f3a4640 100644 --- a/apps/service/rag.py +++ b/apps/service/rag.py @@ -52,9 +52,11 @@ class RAG: yield line @staticmethod - async def get_rag_kb_result(user_sub: str) -> AsyncGenerator[TeamKnowledgeBase, []]: + async def get_rag_kb_result(user_sub: str, keyword: str | None=None) -> AsyncGenerator[TeamKnowledgeBase, []]: """获取RAG服务的结果""" url = Config().get_config().rag.rag_service.rstrip("/") + "/kb" + if keyword: + url += f'?kbName={keyword}' headers = { "Content-Type": "application/json", } -- Gitee From db452554de40847d1b964a7a3c42008f7325254b Mon Sep 17 00:00:00 2001 From: wangkui Date: Sun, 11 May 2025 16:19:47 +0800 Subject: [PATCH 4/4] refactor --- apps/entities/enum_var.py | 8 ---- apps/entities/mcp.py | 69 +++++++++++++++---------------- apps/entities/model.py | 16 ++++++- apps/entities/pool.py | 46 +++++++++++---------- apps/entities/request_data.py | 5 ++- apps/entities/response_data.py | 4 +- apps/manager/appcenter.py | 60 +++++++++++++-------------- apps/manager/mcp_service.py | 51 ++++++++++++++--------- apps/routers/appcenter.py | 8 ++-- apps/routers/mcp_service.py | 48 +++++++++++++-------- apps/scheduler/pool/loader/mcp.py | 17 ++++---- 11 files changed, 178 insertions(+), 154 deletions(-) diff --git a/apps/entities/enum_var.py b/apps/entities/enum_var.py index af2c8706..cf06095f 100644 --- a/apps/entities/enum_var.py +++ b/apps/entities/enum_var.py @@ -159,14 +159,6 @@ class CommentType(str, Enum): NONE = "none" -class MCPType(str, Enum): - """MCP 类型""" - - STDIO = "Stdio" - SSE = "SSE" - STREAMABLE = "Streamable" - - class MCPServiceToolsArgsType(str, Enum): """MCPService tool参数数据类型""" STRING = "string" diff --git a/apps/entities/mcp.py b/apps/entities/mcp.py index 49ee57f7..815718e7 100644 --- a/apps/entities/mcp.py +++ b/apps/entities/mcp.py @@ -1,7 +1,6 @@ # Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. """MCP 相关数据结构""" -from typing import Any import random from enum import Enum from typing import Any @@ -14,12 +13,20 @@ from apps.entities.model import ModelMetadata from apps.entities.enum_var import ( MetadataType, MCPServiceToolsArgsType, - MCPType, PermissionType, AppType, ) sqids = Sqids(min_length=10) + +class MCPType(str, Enum): + """MCP 类型""" + + STDIO = "Stdio" + SSE = "SSE" + STREAMABLE = "Streamable" + + class MCPMeta(BaseModel): """ MCPService或MCPApp的查询元数据 @@ -42,17 +49,6 @@ class MCPMetadataBase(MCPMeta): hashes: dict[str, str] | None = Field(description="资源(App、Service等)下所有文件的hash值", default=None) -class MCPServiceConfig(BaseModel): - """MCPService的API配置""" - - type: MCPType = Field( - default=MCPType.STDIO, - alias="transmitProto", - description="传输协议(Stdio/SSE/Streamable)", - ) - config: str = Field(..., description="对应MCP的配置") - - class MCPServiceToolsArgs(BaseModel): """MCP Service中tool参数信息""" name: str = Field(description="Tool参数名称") @@ -68,14 +64,6 @@ class MCPServiceToolsdata(BaseModel): output_args: list[MCPServiceToolsArgs] = Field(description="Tool参数列表") -class MCPServiceMetadata(MCPMetadataBase): - """MCPService的元数据""" - - type: MetadataType = MetadataType.MCP_SERVICE - config: MCPServiceConfig = Field(description="MCP服务配置") - tools: list[MCPServiceToolsdata] = Field(description="MCP服务Tools列表") - - class Permission(BaseModel): """权限配置""" @@ -83,21 +71,6 @@ class Permission(BaseModel): users: list[str] = Field(description="可访问的用户列表", default=[]) -class AgentAppMetadata(MCPMetadataBase): - """智能体App的元数据""" - - type: MetadataType = MetadataType.AGENT_APP - app_type: AppType = AppType.AGENT - published: bool = Field(description="是否发布", default=False) - history_len: int = Field(description="对话轮次", default=3, le=10) - permission: Permission | None = Field(description="应用权限配置", default=None) - model: ModelMetadata = Field(default=None, description="模型选择") - prompt: str = Field(default="", description="提示词") - mcp_service: list[MCPServiceMetadata] = Field(default=[], alias="mcpService", description="MCP服务") - # TODO 知识库怎么处理 - knowledge: list[str] = Field(default=None, description="知识库") - - class MCPServerConfig(BaseModel): """MCP 服务器配置""" @@ -141,6 +114,7 @@ class MCPTool(BaseModel): description: str = Field(description="MCP工具描述") input_schema: dict[str, Any] = Field(description="MCP工具输入参数") + class MCPCollection(BaseModel): """MCP相关信息,存储在MongoDB的 ``mcp`` 集合中""" @@ -169,3 +143,26 @@ class MCPToolVector(LanceModel): tool_id: str = Field(description="工具ID") mcp_id: str = Field(description="MCP ID") embedding: Vector(dim=1024) = Field(description="MCP工具描述的向量信息") # type: ignore[call-arg] + + +class MCPServiceMetadata(MCPMetadataBase): + """MCPService的元数据""" + + type: MetadataType = MetadataType.MCP_SERVICE + config: MCPConfig = Field(description="MCP服务配置") + tools: list[MCPServiceToolsdata] = Field(description="MCP服务Tools列表") + + +class AgentAppMetadata(MCPMetadataBase): + """智能体App的元数据""" + + type: MetadataType = MetadataType.AGENT_APP + app_type: AppType = AppType.AGENT + published: bool = Field(description="是否发布", default=False) + history_len: int = Field(description="对话轮次", default=3, le=10) + permission: Permission | None = Field(description="应用权限配置", default=None) + model: ModelMetadata = Field(default=None, description="模型选择") + prompt: str = Field(default="", description="提示词") + mcp_service: list[MCPServiceMetadata] = Field(default=[], alias="mcpService", description="MCP服务") + # TODO 知识库怎么处理 + knowledge: list[str] = Field(default=None, description="知识库") diff --git a/apps/entities/model.py b/apps/entities/model.py index 9d953081..a3594ab1 100644 --- a/apps/entities/model.py +++ b/apps/entities/model.py @@ -3,14 +3,28 @@ Copyright (c) Huawei Technologies Co., Ltd. 2024-2025. All rights reserved. """ +from enum import Enum from pydantic import BaseModel, Field +class Provider(str, Enum): + """MCP 类型""" + + STDIO = "Ollama" + VLLM = "VLLM" + STREAMABLE = "Tongyi-Qianwen" + XUNFEI_SPARK = "XunFei Spark" + BAICHUAN = "BaiChuan" + BAIDUYIYUAN = "BaiduYiyan" + MODELSCOPE = "ModelScope" + SICICONFLOW = "SICICONFLOW" + + class ModelCenterCardItem(BaseModel): """模型配置卡片数据结构""" - model_id: str = Field(..., alias="modelId", description="模型ID") + model_id: Provider = Field(..., alias="modelId", description="模型ID") icon: str = Field(description="图标", default="") model: str = Field(..., description="模型") diff --git a/apps/entities/pool.py b/apps/entities/pool.py index d1c2428d..69dbb1ef 100644 --- a/apps/entities/pool.py +++ b/apps/entities/pool.py @@ -12,7 +12,7 @@ from pydantic import BaseModel, Field from apps.entities.appcenter import AppLink from apps.entities.enum_var import CallType, PermissionType, AppType from apps.entities.flow import AppFlow, Permission -from apps.entities.mcp import MCPServiceConfig, MCPServiceToolsdata, MCPServiceMetadata +from apps.entities.mcp import MCPServiceMetadata from apps.entities.model import ModelMetadata @@ -45,20 +45,6 @@ class ServicePool(BaseData): hashes: dict[str, str] = Field(description="服务关联的 OpenAPI YAML 和元数据文件哈希") -class MCPServicePool(BaseData): - """ - 外部服务信息 - - collection: service - """ - - author: str = Field(description="作者的用户ID") - icon: str = Field(description="图标", default="") - config: MCPServiceConfig = Field(description="MCP服务配置") - hashes: dict[str, str] = Field(description="MCP服务关联的元数据文件哈希") - tools: list[MCPServiceToolsdata] = Field(description="MCP服务Tools列表") - - class CallPool(BaseData): """ Call信息 @@ -110,6 +96,27 @@ class NodePool(BaseData): ) +class FlowApp(BaseData): + """ + Flow APP 信息 + + """ + links: list[AppLink] = Field(description="相关链接", default=[]) + first_questions: list[str] = Field(description="推荐问题", default=[]) + flows: list[AppFlow] = Field(description="Flow列表", default=[]) + + +class AgentApp(BaseData): + """ + Agent APP 信息 + + """ + model: ModelMetadata = Field(default=None, description="模型选择") + prompt: str = Field(default="", description="提示词") + mcp_service: list[MCPServiceMetadata] = Field(default=[], alias="mcpService", description="MCP服务") + knowledge: list[str] = Field(default=None, description="知识库") + + class AppPool(BaseData): """ 应用信息 @@ -127,15 +134,10 @@ class AppPool(BaseData): hashes: dict[str, str] = Field(description="关联文件的hash值", default={}) # Flow APP - links: list[AppLink] = Field(description="相关链接", default=[]) - first_questions: list[str] = Field(description="推荐问题", default=[]) - flows: list[AppFlow] = Field(description="Flow列表", default=[]) + flow_app: FlowApp | None = Field(default=None, alias="flowApp", description="Flow app信息") # Agent APP - model: ModelMetadata = Field(default=None, description="模型选择") - prompt: str = Field(default="", description="提示词") - mcp_service: list[MCPServiceMetadata] = Field(default=[], alias="mcpService", description="MCP服务") - knowledge: list[str] = Field(default=None, description="知识库") + agent_app: FlowApp | None = Field(default=None, alias="agentApp", description="Agent app信息") class ModelPool(BaseData): diff --git a/apps/entities/request_data.py b/apps/entities/request_data.py index ef21d7f7..abc03649 100644 --- a/apps/entities/request_data.py +++ b/apps/entities/request_data.py @@ -12,10 +12,11 @@ from apps.common.config import Config from apps.entities.appcenter import AppData from apps.entities.enum_var import CommentType from apps.entities.flow_topology import FlowItem -from apps.entities.mcp import MCPServiceConfig +from apps.entities.mcp import MCPConfig from apps.entities.model import ModelData from apps.entities.prompt import PromptData + class RequestDataApp(BaseModel): """模型对话中包含的app信息""" @@ -102,7 +103,7 @@ class UpdateMCPServiceRequest(BaseModel): icon: str = Field(description="图标", default="") name: str = Field(..., description="MCP服务名称") description: str = Field(..., description="MCP服务描述") - config: MCPServiceConfig = Field(..., description="MCP服务配置") + config: str = Field(..., description="MCP服务配置") class UpdateServiceRequest(BaseModel): diff --git a/apps/entities/response_data.py b/apps/entities/response_data.py index 22892d7c..5b4c3c4e 100644 --- a/apps/entities/response_data.py +++ b/apps/entities/response_data.py @@ -19,7 +19,7 @@ from apps.entities.flow_topology import ( NodeServiceItem, PositionItem, ) -from apps.entities.mcp import MCPServiceConfig, MCPServiceToolsdata +from apps.entities.mcp import MCPServiceToolsdata, MCPConfig from apps.entities.record import RecordData from apps.entities.user import UserInfo from apps.entities.team import TeamKnowledgeBase @@ -463,7 +463,7 @@ class GetMCPServiceDetailMsg(BaseModel): icon: str = Field(description="图标", default="") name: str = Field(..., description="MCP服务名称") description: str = Field(description="MCP服务描述") - data: MCPServiceConfig = Field(description="MCP服务配置") + data: MCPConfig = Field(description="MCP服务配置") tools: list[MCPServiceToolsdata] = Field(description="MCP服务Tools列表") diff --git a/apps/manager/appcenter.py b/apps/manager/appcenter.py index f1ffb50b..dcd04674 100644 --- a/apps/manager/appcenter.py +++ b/apps/manager/appcenter.py @@ -5,7 +5,6 @@ Copyright (c) Huawei Technologies Co., Ltd. 2024-2025. All rights reserved. """ import logging -import re import uuid from datetime import UTC, datetime from typing import Any @@ -30,11 +29,11 @@ class AppCenterManager: @staticmethod async def fetch_all_apps( - user_sub: str, - search_type: SearchType, - keyword: str | None, - page: int, - page_size: int, + user_sub: str, + search_type: SearchType, + keyword: str | None, + page: int, + page_size: int, ) -> tuple[list[AppCenterCardItem], int]: """ 获取所有应用列表 @@ -83,11 +82,11 @@ class AppCenterManager: @staticmethod async def fetch_user_apps( - user_sub: str, - search_type: SearchType, - keyword: str | None, - page: int, - page_size: int, + user_sub: str, + search_type: SearchType, + keyword: str | None, + page: int, + page_size: int, ) -> tuple[list[AppCenterCardItem], int]: """ 获取用户应用列表 @@ -131,11 +130,11 @@ class AppCenterManager: @staticmethod async def fetch_favorite_apps( - user_sub: str, - search_type: SearchType, - keyword: str | None, - page: int, - page_size: int, + user_sub: str, + search_type: SearchType, + keyword: str | None, + page: int, + page_size: int, ) -> tuple[list[AppCenterCardItem], int]: """ 获取用户收藏的应用列表 @@ -253,7 +252,7 @@ class AppCenterManager: return app_id @staticmethod - async def update_app(user_sub: str, app_id: str, app_type: AppType, data: AppData) -> None: + async def update_app(user_sub: str, app_id: str, data: AppData) -> None: """ 更新应用 @@ -270,7 +269,7 @@ class AppCenterManager: if app_data.author != user_sub: msg = "Permission denied" raise InstancePermissionError(msg) - if app_type == AppType.FLOW: + if app_data.app_type == AppType.FLOW: metadata = AppMetadata( type=MetadataType.FLOW_APP, app_type=data.app_type, @@ -290,7 +289,7 @@ class AppCenterManager: ) metadata.flows = app_data.flows metadata.published = app_data.published - elif app_type == AppType.AGENT: + elif app_data.app_type == AppType.AGENT: metadata = AgentAppMetadata( type=MetadataType.AGENT_APP, app_type=data.app_type, @@ -317,7 +316,7 @@ class AppCenterManager: await app_loader.save(metadata, app_id) @staticmethod - async def update_app_publish_status(app_id: str, app_type: AppType, user_sub: str) -> None: + async def update_app_publish_status(app_id: str, user_sub: str) -> bool: """ 发布应用 @@ -341,7 +340,7 @@ class AppCenterManager: {"_id": app_id}, {"$set": {"published": published}}, ) - if app_type == AppType.FLOW: + if app_data.app_type == AppType.FLOW: metadata = AppMetadata( type=MetadataType.FLOW_APP, app_type=app_data.app_type, @@ -360,7 +359,7 @@ class AppCenterManager: ) metadata.flows = app_data.flows metadata.published = app_data.published - elif app_type == AppType.AGENT: + elif app_data.app_type == AppType.AGENT: metadata = AgentAppMetadata( type=MetadataType.AGENT_APP, app_type=app_data.app_type, @@ -423,12 +422,11 @@ class AppCenterManager: ) @staticmethod - async def delete_app(app_id: str, app_type: AppType, user_sub: str) -> None: + async def delete_app(app_id: str, user_sub: str) -> None: """ 删除应用 :param app_id: 应用唯一标识 - :param app_type: 应用类型 :param user_sub: 用户唯一标识 """ app_collection = MongoDB.get_collection("app") @@ -441,7 +439,7 @@ class AppCenterManager: raise InstancePermissionError(msg) # 删除应用 app_loader = AppLoader() - await app_loader.delete(app_id, app_type) + await app_loader.delete(app_id, app_data.app_type) # 删除应用相关的工作流 for flow in app_data.flows: await FlowManager.delete_flow_by_app_and_flow_id(app_id, flow.id) @@ -537,9 +535,9 @@ class AppCenterManager: @staticmethod def _build_filters( - base_filters: dict[str, Any], - search_type: SearchType, - keyword: str, + base_filters: dict[str, Any], + search_type: SearchType, + keyword: str, ) -> dict[str, Any]: search_filters = [ {"name": {"$regex": keyword, "$options": "i"}}, @@ -558,9 +556,9 @@ class AppCenterManager: @staticmethod async def _search_apps_by_filter( - search_conditions: dict[str, Any], - page: int, - page_size: int, + search_conditions: dict[str, Any], + page: int, + page_size: int, ) -> tuple[list[AppPool], int]: """根据过滤条件搜索应用并计算总页数""" try: diff --git a/apps/manager/mcp_service.py b/apps/manager/mcp_service.py index 9922ff38..e3eaa03a 100644 --- a/apps/manager/mcp_service.py +++ b/apps/manager/mcp_service.py @@ -7,10 +7,10 @@ Copyright (c) Huawei Technologies Co., Ltd. 2024-2025. All rights reserved. import logging import uuid from typing import Any +import json -from apps.entities.enum_var import SearchType -from apps.entities.mcp import MCPServiceConfig, MCPServiceMetadata, MCPServiceToolsdata -from apps.entities.pool import MCPServicePool +from apps.entities.enum_var import MCPSearchType +from apps.entities.mcp import MCPServiceMetadata, MCPServiceToolsdata, MCPConfig, MCPServerSSEConfig, MCPServerStdioConfig from apps.entities.response_data import MCPServiceCardItem from apps.exceptions import InstancePermissionError, MCPServiceIDError from apps.models.mongo import MongoDB @@ -24,9 +24,21 @@ logger = logging.getLogger(__name__) class MCPServiceManager: """MCP服务管理器""" + @staticmethod + async def load_mcp_config(config_str) -> MCPConfig: + """字符串转换为MCPConfig""" + result = MCPConfig() + mcp_servers = json.loads(config_str) + for name, config in mcp_servers.get("mcp_servers", {}): + if 'url' in config: + result.mcp_servers[name] = MCPServerSSEConfig.model_validate(config) + else: + result.mcp_servers[name] = MCPServerStdioConfig.model_validate(config) + return result + @staticmethod async def fetch_all_mcpservices( - search_type: SearchType, + search_type: MCPSearchType, keyword: str | None, page: int, page_size: int, @@ -52,7 +64,7 @@ class MCPServiceManager: name: str, icon: str, description: str, - config: MCPServiceConfig, + config: MCPConfig, ) -> str: """创建MCP服务""" mcpservice_id = str(uuid.uuid4()) @@ -93,7 +105,7 @@ class MCPServiceManager: name: str, icon: str, description: str, - config: MCPServiceConfig, + config: MCPConfig, ) -> str: """更新服务""" # 验证用户权限 @@ -102,7 +114,7 @@ class MCPServiceManager: if not db_service: msg = "MCPService not found" raise MCPServiceIDError(msg) - service_pool_store = MCPServicePool.model_validate(db_service) + service_pool_store = MCPServiceMetadata.model_validate(db_service) if service_pool_store.author != user_sub: msg = "Permission denied" raise InstancePermissionError(msg) @@ -127,7 +139,7 @@ class MCPServiceManager: async def get_mcpservice_data( user_sub: str, mcpservice_id: str, - ) -> tuple[str, str, str, MCPServiceConfig]: + ) -> tuple[str, str, str, MCPConfig]: """获取服务数据""" # 验证用户权限 mcpservice_collection = MongoDB.get_collection("mcp") @@ -139,12 +151,13 @@ class MCPServiceManager: if not db_service: msg = "MCPService not found" raise MCPServiceIDError(msg) - mcpservice_pool_store = MCPServicePool.model_validate(db_service) + mcpservice_pool_store = MCPServiceMetadata.model_validate(db_service) if mcpservice_pool_store.author != user_sub: msg = "Permission denied" raise InstancePermissionError(msg) - return mcpservice_pool_store.icon, mcpservice_pool_store.name, mcpservice_pool_store.description, mcpservice_pool_store.config + return mcpservice_pool_store.icon, mcpservice_pool_store.name, mcpservice_pool_store.description, \ + mcpservice_pool_store.config @staticmethod async def get_service_details( @@ -157,7 +170,7 @@ class MCPServiceManager: if not db_service: msg = "MCPService not found" raise MCPServiceIDError(msg) - mcpservice_pool_store = MCPServicePool.model_validate(db_service) + mcpservice_pool_store = MCPServiceMetadata.model_validate(db_service) return mcpservice_pool_store.name, mcpservice_pool_store.description, mcpservice_pool_store.tools @@ -173,7 +186,7 @@ class MCPServiceManager: msg = "[MCPServiceCenterManager] Service未找到" raise MCPServiceIDError(msg) # 验证用户权限 - service_pool_store = MCPServiceManager.model_validate(db_service) + service_pool_store = MCPServiceMetadata.model_validate(db_service) if service_pool_store.author != user_sub: msg = "Permission denied" raise InstancePermissionError(msg) @@ -187,7 +200,7 @@ class MCPServiceManager: search_conditions: dict, page: int, page_size: int, - ) -> tuple[list[MCPServicePool], int]: + ) -> tuple[list[MCPServiceMetadata], int]: """基于输入条件获取MCP服务数据""" mcpservice_collection = MongoDB.get_collection("mcp") # 获取服务总数 @@ -198,7 +211,7 @@ class MCPServiceManager: if not db_mcpservices and total > 0: logger.warning("[MCPServiceManager] 没有找到符合条件的MCP服务: %s", search_conditions) return [], -1 - mcpservice_pools = [MCPServicePool.model_validate(db_mcpservice) for db_mcpservice in db_mcpservices] + mcpservice_pools = [MCPServiceMetadata.model_validate(db_mcpservice) for db_mcpservice in db_mcpservices] return mcpservice_pools, total @staticmethod @@ -214,7 +227,7 @@ class MCPServiceManager: @staticmethod def _build_filters( base_filters: dict[str, Any], - search_type: SearchType, + search_type: MCPSearchType, keyword: str, ) -> dict[str, Any]: search_filters = [ @@ -222,12 +235,10 @@ class MCPServiceManager: {"description": {"$regex": keyword, "$options": "i"}}, {"author": {"$regex": keyword, "$options": "i"}}, ] - if search_type == SearchType.ALL: + if search_type == MCPSearchType.ALL: base_filters["$or"] = search_filters - elif search_type == SearchType.NAME: + elif search_type == MCPSearchType.NAME: base_filters["name"] = {"$regex": keyword, "$options": "i"} - elif search_type == SearchType.DESCRIPTION: - base_filters["description"] = {"$regex": keyword, "$options": "i"} - elif search_type == SearchType.AUTHOR: + elif search_type == MCPSearchType.AUTHOR: base_filters["author"] = {"$regex": keyword, "$options": "i"} return base_filters diff --git a/apps/routers/appcenter.py b/apps/routers/appcenter.py index 6deb6fdf..e3377764 100644 --- a/apps/routers/appcenter.py +++ b/apps/routers/appcenter.py @@ -105,7 +105,7 @@ async def create_or_update_application( app_type = request.app_type if app_id: # 更新应用 try: - await AppCenterManager.update_app(user_sub, app_id, app_type, request) + await AppCenterManager.update_app(user_sub, app_id, request) except ValueError: logger.exception("[AppCenter] 更新应用请求无效") return JSONResponse( @@ -258,12 +258,11 @@ async def get_application( ) async def delete_application( app_id: Annotated[str, Path(..., alias="appId", description="应用ID")], - app_type: Annotated[AppType, Path(..., alias="appType", description="应用类型")], user_sub: Annotated[str, Depends(get_user)], ) -> JSONResponse: """删除应用""" try: - await AppCenterManager.delete_app(app_id, app_type, user_sub) + await AppCenterManager.delete_app(app_id, user_sub) except ValueError: logger.exception("[AppCenter] 删除应用请求无效") return JSONResponse( @@ -307,12 +306,11 @@ async def delete_application( @router.post("/{appId}", response_model=BaseAppOperationRsp) async def publish_application( app_id: Annotated[str, Path(..., alias="appId", description="应用ID")], - app_type: Annotated[AppType, Path(..., alias="appType", description="应用类型")], user_sub: Annotated[str, Depends(get_user)], ) -> JSONResponse: """发布应用""" try: - published = await AppCenterManager.update_app_publish_status(app_id, app_type, user_sub) + published = await AppCenterManager.update_app_publish_status(app_id, user_sub) if not published: msg = "发布应用失败" raise ValueError(msg) diff --git a/apps/routers/mcp_service.py b/apps/routers/mcp_service.py index e8d1c760..1d6f025e 100644 --- a/apps/routers/mcp_service.py +++ b/apps/routers/mcp_service.py @@ -10,6 +10,7 @@ from typing import Annotated from fastapi import APIRouter, Body, Depends, Path, Query, status from fastapi.responses import JSONResponse +from apps.exceptions import MCPServiceIDError from apps.dependency.user import get_user, verify_user from apps.entities.enum_var import MCPSearchType from apps.entities.request_data import UpdateMCPServiceRequest @@ -26,11 +27,11 @@ from apps.entities.response_data import ( RegistryMCPServiceRsp, ActiveMCPServiceRsp, DeactiveMCPServiceRsp, - LoadMCPServiceRsp -) + ) from apps.exceptions import InstancePermissionError, ServiceIDError from apps.manager.mcp_service import MCPServiceManager from apps.manager.user import UserManager +from apps.scheduler.pool.loader.mcp import MCPLoader logger = logging.getLogger(__name__) router = APIRouter( @@ -49,7 +50,6 @@ async def get_mcpservice_list( # noqa: PLR0913 page_size: Annotated[int, Query(..., alias="pageSize", ge=1, le=100, description="每页数量")] = 16, ) -> JSONResponse: """获取服务列表""" - service_cards, total_count = [], -1 try: service_cards, total_count = await MCPServiceManager.fetch_all_mcpservices( search_type, @@ -57,8 +57,8 @@ async def get_mcpservice_list( # noqa: PLR0913 page, page_size, ) - except Exception: - logger.exception("[MCPServiceCenter] 获取MCP服务列表失败") + except Exception as exp: + logger.exception(f"[MCPServiceCenter] 获取MCP服务列表失败: {exp}") return JSONResponse( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=ResponseData( @@ -96,10 +96,22 @@ async def update_mcpservice( data: Annotated[UpdateMCPServiceRequest, Body(..., description="MCP服务对应数据对象")], ) -> JSONResponse: """新建或更新MCP服务""" + try: + config = await MCPServiceManager.load_mcp_config(data.config) + except Exception as exp: + logger.exception(exp) + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=ResponseData( + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + message=f"更新MCP服务失败: {exp!s}", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) if not data.service_id: try: service_id = await MCPServiceManager.create_mcpservice(user_sub, data.name, data.icon, data.description, - data.config) + config) except Exception as e: logger.exception("[MCPServiceCenter] 创建MCP服务失败") return JSONResponse( @@ -112,8 +124,8 @@ async def update_mcpservice( ) else: try: - service_id = await MCPServiceManager.update_mcpservice(user_sub, data.name, data.icon, data.description, - data.config) + service_id = await MCPServiceManager.update_mcpservice(user_sub, data.service_id, data.name, data.icon, + data.description, config) except ServiceIDError: return JSONResponse( status_code=status.HTTP_400_BAD_REQUEST, @@ -177,8 +189,8 @@ async def get_service_detail( result={}, ).model_dump(exclude_none=True, by_alias=True), ) - except Exception: - logger.exception("[MCPService] 获取MCP服务数据失败") + except Exception as exp: + logger.exception(f"[MCPService] 获取MCP服务数据失败: {exp}") return JSONResponse( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=ResponseData( @@ -200,8 +212,8 @@ async def get_service_detail( result={}, ).model_dump(exclude_none=True, by_alias=True), ) - except Exception: - logger.exception("[MCPService] 获取MCP服务API失败") + except Exception as exp: + logger.exception(f"[MCPService] 获取MCP服务API失败: {exp}") return JSONResponse( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=ResponseData( @@ -241,8 +253,8 @@ async def delete_service( result={}, ).model_dump(exclude_none=True, by_alias=True), ) - except Exception: - logger.exception("[MCPServiceManager] 删除MCP服务失败") + except Exception as exp: + logger.exception(f"[MCPServiceManager] 删除MCP服务失败: {exp}") return JSONResponse( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=ResponseData( @@ -274,7 +286,7 @@ async def register_mcp_service( ).model_dump(exclude_none=True, by_alias=True), ) icon, name, description, config = MCPServiceManager.get_mcpservice_data(user_sub, service_id) - MCPLoader().init_one_template(service_id, config) + await MCPLoader().save_one_template(service_id, config) except MCPServiceIDError: return JSONResponse( status_code=status.HTTP_400_BAD_REQUEST, @@ -315,7 +327,7 @@ async def active_mcp_service( ) -> JSONResponse: """激活mcp""" try: - MCPLoader().user_active_template(usr_sub, service_id) + await MCPLoader().user_active_template(user_sub, service_id) except FileExistsError as exp: logging.error(exp) return JSONResponse( @@ -348,7 +360,7 @@ async def deactive_mcp_service( ) -> JSONResponse: """取消激活mcp""" try: - MCPLoader().user_deactive_template(usr_sub, service_id) + await MCPLoader().user_deactive_template(user_sub, service_id) except Exception as exp: logging.error(exp) return JSONResponse( @@ -372,7 +384,7 @@ async def load_mcp_service( """取消激活mcp""" try: icon, name, description, config = MCPServiceManager.get_mcpservice_data(user_sub, service_id) - MCPLoader().load_one_user(usr_sub, config) + await MCPLoader().load_one_user(user_sub, config) except MCPServiceIDError: return JSONResponse( status_code=status.HTTP_400_BAD_REQUEST, diff --git a/apps/scheduler/pool/loader/mcp.py b/apps/scheduler/pool/loader/mcp.py index a5acde69..f7b182be 100644 --- a/apps/scheduler/pool/loader/mcp.py +++ b/apps/scheduler/pool/loader/mcp.py @@ -13,7 +13,6 @@ from anyio import Path from fastapi.encoders import jsonable_encoder from apps.common.config import Config -from apps.entities.pool import MCPServicePool from apps.entities.vector import ServicePoolVector from apps.entities.mcp import ( MCPServiceMetadata, @@ -29,11 +28,11 @@ from apps.models.mongo import MongoDB from apps.common.singleton import SingletonMeta from apps.scheduler.pool.mcp.client import SSEMCPClient, StdioMCPClient from apps.scheduler.pool.mcp.install import install_npx, install_uvx +from apps.scheduler.pool.check import FileChecker +from apps.scheduler.pool.loader.metadata import MetadataLoader, MetadataType -logger = logging.getLogger(__name__) MCP_PATH = Path(Config().get_config().deploy.data_dir) / "semantics" / "mcp" -logger = logging.getLogger(__name__) SERVICE_PATH = Path(Config().get_config().deploy.data_dir) / "semantics" / "service" PROGRAM_PATH = Path(Config().get_config().deploy.data_dir) / "semantics" / "mcp" logger = logging.getLogger(__name__) @@ -74,8 +73,8 @@ class MCPServiceLoader: service_collection = MongoDB.get_collection("mcp") try: await service_collection.delete_one({"_id": service_id}) - except Exception: - logger.exception("[MCPServiceLoader] 删除MCPService失败") + except Exception as exp: + logger.exception(f"[MCPServiceLoader] 删除MCPService失败: {exp}") try: # 获取 LanceDB 表 @@ -83,8 +82,8 @@ class MCPServiceLoader: # 删除数据 await service_table.delete(f"id = '{service_id}'") - except Exception: - logger.exception("[MCPServiceLoader] MCP服务删除数据库失败") + except Exception as exp: + logger.exception(f"[MCPServiceLoader] MCP服务删除数据库失败: {exp}") if not is_reload: path = Path(Config().get_config().deploy.data_dir) / "mcp" / "service" / service_id @@ -105,8 +104,9 @@ class MCPServiceLoader: {"_id": metadata.id}, { "$set": jsonable_encoder( - MCPServicePool( + MCPServiceMetadata( _id=metadata.id, + type=metadata.type, name=metadata.name, description=metadata.description, author=metadata.author, @@ -263,7 +263,6 @@ class MCPLoader(metaclass=SingletonMeta): await f.write(json.dumps(config_data, indent=4, ensure_ascii=False)) await f.aclose() - async def _insert_template_db(self, mcp_id: str, config: MCPServerSSEConfig | MCPServerStdioConfig) -> None: """ 插入MCP模板信息到数据库 -- Gitee