diff --git a/apps/common/provider.yaml b/apps/common/provider.yaml new file mode 100644 index 0000000000000000000000000000000000000000..e4aabfa4b568c0e49db7ea2ce67377bdaf63c073 --- /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 0000000000000000000000000000000000000000..c3c4a443aeb083956bcacb0c1e749c097b05e7f1 --- /dev/null +++ b/apps/common/read_conf_yaml.py @@ -0,0 +1,30 @@ +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.safe_load(f.read()) + + 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 + + +provider_conf = ReadConfYaml(os.path.abspath("provider.yaml")) diff --git a/apps/entities/appcenter.py b/apps/entities/appcenter.py index bf021c826b552ca64e7277fa52b311db0f0f03b6..1ce73dc5109df7b3b960073ebb09269a32cefa4b 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 MCPMeta 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[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 af0b5a88e128b2c40c6e86eb814a9889bde654aa..6ecf3fe883738ef5accb1ea7a567b83ea95d1c02 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): @@ -62,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): @@ -81,6 +84,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 281d53e07d3ecf9756bf7bf01ce28b1fad6a21b2..cf06095f90553727c8563b37876997e924a6892f 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,49 @@ class CommentType(str, Enum): LIKE = "liked" DISLIKE = "disliked" NONE = "none" + + +class MCPServiceToolsArgsType(str, Enum): + """MCPService tool参数数据类型""" + STRING = "string" + DOUBLE = "double" + INTEGER = "integer" + 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/flow.py b/apps/entities/flow.py index 22f278b5d7e8d71d5879e48e4c7b3bec9b3a6f50..74e8e9cca0d9af68be86e14fada362fff66e3354 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/knowledge.py b/apps/entities/knowledge.py new file mode 100644 index 0000000000000000000000000000000000000000..12137f2aea5d1123df7f86f0da20923224d33600 --- /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 6b54772023e7f5221e5a962add5bdda4d96885aa..815718e739df7e5047bd1b84fcfb4567543f563a 100644 --- a/apps/entities/mcp.py +++ b/apps/entities/mcp.py @@ -9,14 +9,66 @@ from lancedb.pydantic import LanceModel, Vector from pydantic import BaseModel, Field from sqids.sqids import Sqids +from apps.entities.model import ModelMetadata +from apps.entities.enum_var import ( + MetadataType, + MCPServiceToolsArgsType, + PermissionType, + AppType, +) sqids = Sqids(min_length=10) + class MCPType(str, Enum): """MCP 类型""" - SSE = "sse" - STDIO = "stdio" - STREAMABLE = "stream" + STDIO = "Stdio" + SSE = "SSE" + STREAMABLE = "Streamable" + + +class MCPMeta(BaseModel): + """ + MCPService或MCPApp的查询元数据 + """ + id: str = Field(description="元数据ID") + + +class MCPMetadataBase(MCPMeta): + """ + MCPService或MCPApp的元数据 + + 注意:hash字段在save和load的时候exclude + """ + + type: MetadataType = Field(description="元数据类型") + 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 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 Permission(BaseModel): + """权限配置""" + + type: PermissionType = Field(description="权限类型", default=PermissionType.PRIVATE) + users: list[str] = Field(description="可访问的用户列表", default=[]) class MCPServerConfig(BaseModel): @@ -62,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`` 集合中""" @@ -90,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 new file mode 100644 index 0000000000000000000000000000000000000000..a3594ab1b3ac917dbfbd698f8059486a8e1291cb --- /dev/null +++ b/apps/entities/model.py @@ -0,0 +1,56 @@ +""" +模型配置相关 API 基础数据结构定义 + +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: Provider = 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 | None = Field(default=None, 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="供应商介绍") + url: str = Field(default="", description="供应商api地址") diff --git a/apps/entities/pool.py b/apps/entities/pool.py index 807a55a33e3ec5b2e11b0915db8689d021c2fb21..69dbb1ef7cda3fb86f5f3586a568f568d03a5307 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 MCPServiceMetadata +from apps.entities.model import ModelMetadata class BaseData(BaseModel): @@ -94,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): """ 应用信息 @@ -102,12 +125,55 @@ 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 + flow_app: FlowApp | None = Field(default=None, alias="flowApp", description="Flow app信息") + + # Agent APP + agent_app: FlowApp | None = Field(default=None, alias="agentApp", description="Agent app信息") + + +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 0000000000000000000000000000000000000000..ddaa46fa844884ff5d0d09a07f2d32dd26f325d9 --- /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 89e94ae47e969e869e35f7dc596384efdceb9e10..abc03649c1b3a6f9c82d168b02e5f5a325c61e8d 100644 --- a/apps/entities/request_data.py +++ b/apps/entities/request_data.py @@ -12,6 +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 MCPConfig +from apps.entities.model import ModelData +from apps.entities.prompt import PromptData class RequestDataApp(BaseModel): @@ -93,6 +96,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: str = Field(..., description="MCP服务配置") + + class UpdateServiceRequest(BaseModel): """POST /api/service 请求数据结构""" @@ -152,3 +165,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 decb55dc5502a6676e684724f477e53e238b9be3..5b4c3c4e713173feb2bd2537e47b9e21a78d0aab 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,8 +19,12 @@ from apps.entities.flow_topology import ( NodeServiceItem, PositionItem, ) +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 +from apps.entities.knowledge import KnowledgeBaseItem, KnowledgeBaseMetaData +from apps.entities.team import TeamItem class ResponseData(BaseModel): @@ -94,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): @@ -298,7 +306,7 @@ class GetRecentAppListRsp(ResponseData): class ServiceCardItem(BaseModel): - """语义接口中心:服务卡片数据结构""" + """插件中心:语义接口服务卡片数据结构""" service_id: str = Field(..., alias="serviceId", description="服务ID") name: str = Field(..., description="服务名称") @@ -309,7 +317,7 @@ class ServiceCardItem(BaseModel): class ServiceApiData(BaseModel): - """语义接口中心:服务 API 接口属性数据结构""" + """插件中心:语义接口服务 API 接口属性数据结构""" name: str = Field(..., description="接口名称") path: str = Field(..., description="接口路径") @@ -317,7 +325,7 @@ class ServiceApiData(BaseModel): class BaseServiceOperationMsg(BaseModel): - """语义接口中心:基础服务操作Result数据结构""" + """插件中心:语义接口基础服务操作Result数据结构""" service_id: str = Field(..., alias="serviceId", description="服务ID") @@ -337,7 +345,7 @@ class GetServiceListRsp(ResponseData): class UpdateServiceMsg(BaseModel): - """语义接口中心:服务属性数据结构""" + """插件中心:语义接口服务属性数据结构""" service_id: str = Field(..., alias="serviceId", description="服务ID") name: str = Field(..., description="服务名称") @@ -396,6 +404,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: MCPConfig = 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 +527,170 @@ 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 + + +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 + + +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/entities/team.py b/apps/entities/team.py new file mode 100644 index 0000000000000000000000000000000000000000..0dcdb655e982d6023c2b2605e640054b8c8dab16 --- /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/exceptions.py b/apps/exceptions.py index e68918abeea0d8726284acb227d3e44bfb78e329..e7ac719d8240ecfde0e5a5d1a5fdaa9aaf770b42 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/main.py b/apps/main.py index a14f024392dd2908c33426b0530a96ede6c25624..19f7e509756dced286060d29310efb25633ddbea 100644 --- a/apps/main.py +++ b/apps/main.py @@ -32,8 +32,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 @@ -62,8 +67,13 @@ app.include_router(chat.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/appcenter.py b/apps/manager/appcenter.py index 286341b424a881f47432b289438df64599323821..dcd046741c3bf40e7df8995789cc17b76339158f 100644 --- a/apps/manager/appcenter.py +++ b/apps/manager/appcenter.py @@ -5,15 +5,15 @@ 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 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 @@ -54,16 +54,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, @@ -97,11 +99,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 +106,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 +114,7 @@ class AppCenterManager: return [ AppCenterCardItem( appId=app.id, + app_type=app.app_type, icon=app.icon, name=app.name, description=app.description, @@ -161,13 +159,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,31 +196,57 @@ 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 @@ -233,24 +258,9 @@ class AppCenterManager: :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,8 +269,49 @@ 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_data.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_data.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) @@ -289,21 +340,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_data.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_data.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 @@ -363,7 +439,7 @@ class AppCenterManager: raise InstancePermissionError(msg) # 删除应用 app_loader = AppLoader() - await app_loader.delete(app_id) + 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) @@ -468,14 +544,14 @@ class AppCenterManager: {"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 diff --git a/apps/manager/mcp_service.py b/apps/manager/mcp_service.py new file mode 100644 index 0000000000000000000000000000000000000000..e3eaa03a9a8efc74b6568e88edb01937e7bfd4c7 --- /dev/null +++ b/apps/manager/mcp_service.py @@ -0,0 +1,244 @@ +""" +语义接口中心 Manager + +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 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 +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 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: MCPSearchType, + 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: MCPConfig, + ) -> str: + """创建MCP服务""" + mcpservice_id = str(uuid.uuid4()) + # 检查是否存在相同服务 + service_collection = MongoDB.get_collection("mcp") + 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: MCPConfig, + ) -> str: + """更新服务""" + # 验证用户权限 + mcpservice_collection = MongoDB.get_collection("mcp") + db_service = await mcpservice_collection.find_one({"_id": mcpservice_id}) + if not db_service: + msg = "MCPService not found" + raise MCPServiceIDError(msg) + service_pool_store = MCPServiceMetadata.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, MCPConfig]: + """获取服务数据""" + # 验证用户权限 + mcpservice_collection = MongoDB.get_collection("mcp") + 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 = 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 + + @staticmethod + async def get_service_details( + service_id: str, + ) -> tuple[str, str, list[MCPServiceToolsdata]]: + """获取服务API列表""" + # 获取服务名称 + service_collection = MongoDB.get_collection("mcp") + db_service = await service_collection.find_one({"_id": service_id}) + if not db_service: + msg = "MCPService not found" + raise MCPServiceIDError(msg) + mcpservice_pool_store = MCPServiceMetadata.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") + db_service = await service_collection.find_one({"_id": service_id}) + if not db_service: + msg = "[MCPServiceCenterManager] Service未找到" + raise MCPServiceIDError(msg) + # 验证用户权限 + service_pool_store = MCPServiceMetadata.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[MCPServiceMetadata], int]: + """基于输入条件获取MCP服务数据""" + mcpservice_collection = MongoDB.get_collection("mcp") + # 获取服务总数 + 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 = [MCPServiceMetadata.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: MCPSearchType, + 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 == MCPSearchType.ALL: + base_filters["$or"] = search_filters + elif search_type == MCPSearchType.NAME: + base_filters["name"] = {"$regex": keyword, "$options": "i"} + elif search_type == MCPSearchType.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 0000000000000000000000000000000000000000..dd4802cabacaa8d890f77c89dda36f0cf48b4331 --- /dev/null +++ b/apps/manager/model.py @@ -0,0 +1,215 @@ +""" +模型配置Manager + +Copyright (c) Huawei Technologies Co., Ltd. 2024-2025. All rights reserved. +""" + +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 +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: 供应商列表,供应商个数 + """ + provider_list = list[ProviderCardItem] + for provider in provider_conf.get("providers", {}): + provider_list.append( + ProviderCardItem( + provider=provider.get("provider", ""), + description=provider.get("description", ""), + url=provider.get("url", ""), + ) + ) + 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 0000000000000000000000000000000000000000..431df822c6f40fc4ce14c8f9707c726c3bed8022 --- /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 60824888727ca5b70ad507c2568ea34be49d199b..e33777648d08697da52d9ea8e976e2bd758d8e11 100644 --- a/apps/routers/appcenter.py +++ b/apps/routers/appcenter.py @@ -12,7 +12,7 @@ from fastapi.responses import JSONResponse 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, @@ -102,9 +102,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, request) except ValueError: logger.exception("[AppCenter] 更新应用请求无效") return JSONResponse( @@ -137,7 +138,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( @@ -188,7 +189,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: @@ -229,6 +230,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, @@ -241,6 +243,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), ) @@ -305,6 +311,9 @@ async def publish_application( """发布应用""" try: published = await AppCenterManager.update_app_publish_status(app_id, user_sub) + if not published: + msg = "发布应用失败" + raise ValueError(msg) except ValueError: logger.exception("[AppCenter] 发布应用请求无效") return JSONResponse( @@ -335,9 +344,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/conversation.py b/apps/routers/conversation.py index 321ff818a7e73d19e327d581173394968780b149..6d43d1d6a2d6017d7e97f9cf41c4e9f6f4dcfadf 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 0000000000000000000000000000000000000000..a3ed48bbaa827b8d125f7e10a19c93aec1231d2d --- /dev/null +++ b/apps/routers/knowledge_base.py @@ -0,0 +1,101 @@ +""" +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, keyword) + 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=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 new file mode 100644 index 0000000000000000000000000000000000000000..1d6f025e344add18f46708d93c5d1312ab68e6b8 --- /dev/null +++ b/apps/routers/mcp_service.py @@ -0,0 +1,418 @@ +""" +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.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 +from apps.entities.response_data import ( + BaseMCPServiceOperationMsg, + DeleteMCPServiceRsp, + GetMCPServiceDetailMsg, + GetMCPServiceDetailRsp, + GetMCPServiceListMsg, + GetMCPServiceListRsp, + ResponseData, + UpdateMCPServiceMsg, + UpdateMCPServiceRsp, + RegistryMCPServiceRsp, + ActiveMCPServiceRsp, + DeactiveMCPServiceRsp, + ) +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( + 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[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, +) -> JSONResponse: + """获取服务列表""" + try: + service_cards, total_count = await MCPServiceManager.fetch_all_mcpservices( + search_type, + keyword, + page, + page_size, + ) + except Exception as exp: + logger.exception(f"[MCPServiceCenter] 获取MCP服务列表失败: {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 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) +async def update_mcpservice( + user_sub: Annotated[str, Depends(get_user)], + 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, + 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.service_id, data.name, data.icon, + data.description, 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 as exp: + logger.exception(f"[MCPService] 获取MCP服务数据失败: {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), + ) + 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 as exp: + logger.exception(f"[MCPService] 获取MCP服务API失败: {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), + ) + 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) +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 as exp: + logger.exception(f"[MCPServiceManager] 删除MCP服务失败: {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 = 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) + await MCPLoader().save_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: + await MCPLoader().user_active_template(user_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: + await MCPLoader().user_deactive_template(user_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) + await MCPLoader().load_one_user(user_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 new file mode 100644 index 0000000000000000000000000000000000000000..470712805fad2d86645a134aa101cb00c055ebe1 --- /dev/null +++ b/apps/routers/model.py @@ -0,0 +1,302 @@ +""" +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.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("", 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}", + 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 0000000000000000000000000000000000000000..adbf252e1a58a6e019449d7d67560b01acf70af5 --- /dev/null +++ b/apps/routers/prompt.py @@ -0,0 +1,175 @@ +""" +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.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=["prompt"], + 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("", 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}", + 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/routers/team.py b/apps/routers/team.py new file mode 100644 index 0000000000000000000000000000000000000000..9aaf4a5f87603b74d3e776ec695e03c9166446e4 --- /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/scheduler/pool/check.py b/apps/scheduler/pool/check.py index 562343bb96474ab601dd78036ab1a94b1fbb32aa..6ef834c15cb5c6610c03c246b380a61fe0ce6bf5 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 b5877e989892597caf8ac57994660f5cffc899a8..683460c79fea68b25bd1da604d9709b65db239d1 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 @@ -19,19 +21,19 @@ from apps.scheduler.pool.loader.flow import FlowLoader from apps.scheduler.pool.loader.metadata import MetadataLoader logger = logging.getLogger(__name__) -BASE_PATH = Path(Config().get_config().deploy.data_dir) / "semantics" / "app" 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 = BASE_PATH / 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: @@ -39,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: """ 保存应用 @@ -85,22 +111,29 @@ class AppLoader: :param app_id: 应用 ID """ # 创建文件夹 - app_path = BASE_PATH / 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") @@ -119,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 = BASE_PATH / 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 index c9433c3e0ef9645e6f1923eb74c6a9a020cebd95..f7b182be8466a5c284801d5abfccff29481e990f 100644 --- a/apps/scheduler/pool/loader/mcp.py +++ b/apps/scheduler/pool/loader/mcp.py @@ -3,32 +3,143 @@ MCP 加载器 Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. """ - -import json import logging import shutil +import json from typing import ClassVar import asyncer from anyio import Path +from fastapi.encoders import jsonable_encoder from apps.common.config import Config -from apps.common.singleton import SingletonMeta +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.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" +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__) + + +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") + try: + await service_collection.delete_one({"_id": service_id}) + except Exception as exp: + logger.exception(f"[MCPServiceLoader] 删除MCPService失败: {exp}") + + try: + # 获取 LanceDB 表 + service_table = await LanceDB().get_table("mcp") + + # 删除数据 + await service_table.delete(f"id = '{service_id}'") + 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 + 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") + try: + # 插入或更新 Service + await service_collection.update_one( + {"_id": metadata.id}, + { + "$set": jsonable_encoder( + MCPServiceMetadata( + _id=metadata.id, + type=metadata.type, + 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) + class MCPLoader(metaclass=SingletonMeta): """ @@ -152,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模板信息到数据库 diff --git a/apps/scheduler/pool/loader/metadata.py b/apps/scheduler/pool/loader/metadata.py index ac5d348558e51888700a0c5d0f35fcbe1107b2c9..ad51d396a7b56c9dabf83f1403d725d4a33dbd0b 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__) @@ -26,7 +32,8 @@ BASE_PATH = Path(Config().get_config().deploy.data_dir) / "semantics" 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: @@ -44,7 +51,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) @@ -60,6 +67,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) @@ -68,22 +108,50 @@ 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: - resource_path = BASE_PATH / "app" / resource_id / "metadata.yaml" + if metadata_type == MetadataType.FLOW_APP.value: + resource_path = ( + Path(Config().get_config().deploy.data_dir) / "semantics" / "app" / resource_id / "metadata.yaml" + ) elif metadata_type == MetadataType.SERVICE.value: - resource_path = BASE_PATH / "service" / resource_id / "metadata.yaml" + resource_path = ( + 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}" logger.error(err) @@ -93,7 +161,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 0000000000000000000000000000000000000000..b9d46553b2e39b0d1634df899ef8ce87a7a0a3b4 --- /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 0000000000000000000000000000000000000000..1b779c9cae0aa579b12f022a9ea008f4729b60e7 --- /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 9826d1cda890fb9b933b0f443d13b093f3adf480..c3bbe33cd8144a3af4676e0883df1d88c4e16f3a 100644 --- a/apps/scheduler/pool/pool.py +++ b/apps/scheduler/pool/pool.py @@ -116,7 +116,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 diff --git a/apps/service/rag.py b/apps/service/rag.py index 485d6791e2a4752502ab5fba59f71f8d413c2f61..0f3a4640fa4e54526d4f79d5567e4195bc01a02c 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,47 @@ class RAG: return yield line + + @staticmethod + 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", + } + + # 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"]