diff --git a/apps/entities/enum_var.py b/apps/entities/enum_var.py index e8eac33bfd91999d29a0d4205ac9acbbf0443f08..fea0806e6ec1c2c28ad3980fba0037f78b78f76e 100644 --- a/apps/entities/enum_var.py +++ b/apps/entities/enum_var.py @@ -69,3 +69,11 @@ class MetadataType(str, Enum): SERVICE = "service" APP = "app" + + +class AppPermissionType(str, Enum): + """App的权限类型""" + + PROTECTED = "protected" + PUBLIC = "public" + PRIVATE = "private" diff --git a/apps/entities/flow.py b/apps/entities/flow.py index 27c20b1c795b2229c1be305fedb8082ea39429af..85aa23ca585ef9a398f702d52784f8a518ee4b32 100644 --- a/apps/entities/flow.py +++ b/apps/entities/flow.py @@ -7,7 +7,11 @@ from typing import Any, Optional from pydantic import BaseModel, Field -from apps.entities.enum_var import CallType, MetadataType +from apps.entities.enum_var import ( + AppPermissionType, + CallType, + MetadataType, +) class Step(BaseModel): @@ -42,18 +46,6 @@ class Flow(BaseModel): next_flow: Optional[list[NextFlow]] = None -class Service(BaseModel): - """外部服务信息 - - collection: service - """ - - id: str = Field(alias="_id") - name: str - description: str - dir_path: str - - class StepPool(BaseModel): """Step信息 @@ -80,17 +72,21 @@ class FlowPool(BaseModel): class CallMetadata(BaseModel): """Call工具信息 - key: call_metadata + key: call """ - id: str = Field(alias="_id", description="Call的ID") + id: str = Field(alias="_id", description="Call的ID", default_factory=lambda: str(uuid.uuid4())) type: CallType = Field(description="Call的类型") name: str = Field(description="Call的名称") description: str = Field(description="Call的描述") - path: str = Field(description="Call的路径;当为系统Call时,形如 system::LLM;当为Python Call时,形如 python::tune::call.tune.CheckSystem") + path: str = Field(description=""" + Call的路径。 + 当为系统Call时,路径就是ID,例如:“LLM”; + 当为Python Call时,要加上Service名称,例如 “tune::call.tune.CheckSystem” + """) -class Metadata(BaseModel): +class MetadataBase(BaseModel): """Service或App的元数据""" type: MetadataType = Field(description="元数据类型") @@ -98,4 +94,99 @@ class Metadata(BaseModel): name: str = Field(description="元数据名称") description: str = Field(description="元数据描述") version: str = Field(description="元数据版本") - + author: str = Field(description="创建者的用户名") + + +class ServiceApiAuthOidc(BaseModel): + """Service的API鉴权方式的OIDC配置""" + + client_id: str = Field(description="OIDC客户端ID") + client_secret: str = Field(description="OIDC客户端密钥") + + +class ServiceApiAuthKeyVal(BaseModel): + """Service的API鉴权方式的键值对""" + + name: str = Field(description="鉴权参数名称") + value: str = Field(description="鉴权参数值") + + +class ServiceApiAuth(BaseModel): + """Service的API鉴权方式""" + + header: list[ServiceApiAuthKeyVal] = Field(description="HTTP头鉴权配置", default=[]) + cookie: list[ServiceApiAuthKeyVal] = Field(description="HTTP Cookie鉴权配置", default=[]) + query: list[ServiceApiAuthKeyVal] = Field(description="HTTP URL参数鉴权配置", default=[]) + oidc: Optional[ServiceApiAuthOidc] = Field(description="OIDC鉴权配置", default=None) + + +class ServiceApiConfig(BaseModel): + """Service的API配置""" + + server: str = Field(description="服务器地址", pattern=r"^(https|http)://.*$") + auth: Optional[ServiceApiAuth] = Field(description="API鉴权方式", default=None) + + +class ServiceMetadata(MetadataBase): + """Service的元数据""" + + type: MetadataType = MetadataType.SERVICE + api: ServiceApiConfig = Field(description="API配置") + + +class AppLink(BaseModel): + """App的相关链接""" + + title: str = Field(description="链接标题") + url: str = Field(description="链接URL") + + +class AppPermission(BaseModel): + """App的权限配置""" + + type: AppPermissionType = Field(description="权限类型", default=AppPermissionType.PRIVATE) + users: list[str] = Field(description="可访问的用户列表", default=[]) + + +class AppMetadata(MetadataBase): + """App的元数据""" + + type: MetadataType = MetadataType.APP + links: list[AppLink] = Field(description="相关链接", default=[]) + first_questions: list[str] = Field(description="首次提问", default=[]) + history_len: int = Field(description="对话轮次", default=3, le=10) + permissions: Optional[AppPermission] = Field(description="应用权限配置", default=None) + + +class ServiceApiSpec(BaseModel): + """外部服务API信息""" + + name: str = Field(description="OpenAPI文件名") + description: str = Field(description="OpenAPI中关于API的Summary") + size: int = Field(description="OpenAPI文件大小(单位:KB)") + path: str = Field(description="OpenAPI文件路径") + hash: str = Field(description="OpenAPI文件的hash值") + + +class Service(BaseModel): + """外部服务信息 + + collection: service + """ + + metadata: ServiceMetadata + name: str + description: str + dir_path: str + + +class App(BaseModel): + """应用信息 + + collection: app + """ + + metadata: AppMetadata + name: str + description: str + dir_path: str diff --git a/apps/scheduler/call/api/api.py b/apps/scheduler/call/api/api.py index a102cb2005e9320e27b0e32a44e4bbfdb9235e88..3a4353732dcfe4b9b243e51b3aef7f23a4b8b369 100644 --- a/apps/scheduler/call/api/api.py +++ b/apps/scheduler/call/api/api.py @@ -146,26 +146,6 @@ class API(CoreCall): err = "Data type not implemented." raise NotImplementedError(err) - # def _file_to_lists(self, spec: dict[str, Any]) -> aiohttp.FormData: - # file_form = aiohttp.FormData() - - # if self._params.files is None: - # return file_form - - # file_names = [] - # for file in self._params.files: - # file_names.append(Files.get_by_id(file)["name"]) - - # file_spec = check_upload_file(spec, file_names) - # selected_file = choose_file(file_names, file_spec, self.params_obj.question, self.params_obj.background, self.usage) - - # for key, val in json.loads(selected_file).items(): - # if isinstance(val, str): - # file_form.add_field(key, open(Files.get_by_name(val)["path"], "rb"), filename=val) - # else: - # for item in val: - # file_form.add_field(key, open(Files.get_by_name(item)["path"], "rb"), filename=item) - # return file_form async def _call_api(self, method: str, url: str, slot_data: Optional[dict[str, Any]] = None) -> CallResult: LOGGER.info(f"调用接口{url},请求数据为{slot_data}") diff --git a/apps/scheduler/pool/entities.py b/apps/scheduler/pool/entities.py deleted file mode 100644 index 80ad85364161090e2c0767c091498ddf411be28e..0000000000000000000000000000000000000000 --- a/apps/scheduler/pool/entities.py +++ /dev/null @@ -1,40 +0,0 @@ -"""内存SQLite中的表结构 - -Copyright (c) Huawei Technologies Co., Ltd. 2023-2024. All rights reserved. -""" -from sqlalchemy import Column, Integer, LargeBinary, String -from sqlalchemy.orm import declarative_base - -Base = declarative_base() - - -class FlowItem(Base): - """Flow数据表""" - - __tablename__ = "flow" - id = Column(Integer, primary_key=True, autoincrement=True) - plugin = Column(String(length=100), nullable=False) - name = Column(String(length=100), nullable=False, unique=True) - description = Column(String(length=1500), nullable=False) - - -class PluginItem(Base): - """Plugin数据表""" - - __tablename__ = "plugin" - id = Column(String(length=100), primary_key=True, nullable=False, unique=True) - show_name = Column(String(length=100), nullable=False, unique=True) - description = Column(String(length=1500), nullable=False) - auth = Column(String(length=500), nullable=True) - spec = Column(LargeBinary, nullable=False) - signature = Column(String(length=100), nullable=False) - - -class CallItem(Base): - """Call数据表""" - - __tablename__ = "call" - id = Column(Integer, primary_key=True, autoincrement=True) - plugin = Column(String(length=100), nullable=True) - name = Column(String(length=100), nullable=False) - description = Column(String(length=1500), nullable=False) diff --git a/apps/scheduler/pool/loader.py b/apps/scheduler/pool/loader.py deleted file mode 100644 index 2c512e38a84030ab68c4c55ee13f3733fb294bda..0000000000000000000000000000000000000000 --- a/apps/scheduler/pool/loader.py +++ /dev/null @@ -1,237 +0,0 @@ -"""Pool:载入器 - -Copyright (c) Huawei Technologies Co., Ltd. 2023-2024. All rights reserved. -""" -import importlib.util -import json -import sys -import traceback -from pathlib import Path -from typing import Any, ClassVar, Optional - -import yaml -from langchain_community.agent_toolkits.openapi.spec import ( - ReducedOpenAPISpec, - reduce_openapi_spec, -) - -import apps.scheduler.call as system_call -from apps.common.config import config -from apps.common.singleton import Singleton -from apps.constants import LOGGER -from apps.entities.flow import Flow, NextFlow, Step -from apps.scheduler.pool.pool import Pool - -OPENAPI_FILENAME = "openapi.yaml" -METADATA_FILENAME = "plugin.json" -FLOW_DIR = "flows" -LIB_DIR = "lib" - - -class PluginLoader: - """载入单个插件的Loader。""" - - def __init__(self, plugin_id: str) -> None: - """初始化Loader。 - - 设置插件目录,随后遍历每一个 - """ - self._plugin_location = Path(config["PLUGIN_DIR"]) / plugin_id - self.plugin_name = plugin_id - - metadata = self._load_metadata() - spec = self._load_openapi_spec() - Pool().add_plugin(plugin_id=plugin_id, spec=spec, metadata=metadata) - - if "automatic_flow" in metadata and metadata["automatic_flow"] is True: - flows = self._single_api_to_flow(spec) - else: - flows = [] - flows += self._load_flow() - Pool().add_flows(plugin=plugin_id, flows=flows) - - calls = self._load_lib() - Pool().add_calls(plugin=plugin_id, calls=calls) - - def _load_openapi_spec(self) -> Optional[ReducedOpenAPISpec]: - spec_path = self._plugin_location / OPENAPI_FILENAME - - if spec_path.exists(): - with Path(spec_path).open(encoding="utf-8") as f: - spec = yaml.safe_load(f) - return reduce_openapi_spec(spec) - return None - - def _load_metadata(self) -> dict[str, Any]: - metadata_path = self._plugin_location / METADATA_FILENAME - return json.load(Path(metadata_path).open(encoding="utf-8")) - - @staticmethod - def _single_api_to_flow(spec: Optional[ReducedOpenAPISpec] = None) -> list[dict[str, Any]]: - if not spec: - return [] - - flows = [] - for endpoint in spec.endpoints: - # 构造Step - step_dict = { - "start": Step( - name="start", - call_type="api", - params={ - "endpoint": endpoint[0], - }, - next="end", - ), - "end": Step( - name="end", - call_type="none", - ), - } - - # 构造Flow - flow = { - "id": endpoint[0], - "description": endpoint[1], - "data": Flow(steps=step_dict), - } - - flows.append(flow) - return flows - - def _load_flow(self) -> list[dict[str, Any]]: - flow_path = self._plugin_location / FLOW_DIR - flows = [] - if flow_path.is_dir(): - for current_flow_path in flow_path.iterdir(): - LOGGER.info(f"载入Flow: {current_flow_path}") - - with Path(current_flow_path).open(encoding="utf-8") as f: - flow_yaml = yaml.safe_load(f) - - if "/" in flow_yaml["id"]: - err = "Flow名称包含非法字符!" - raise ValueError(err) - - if "on_error" in flow_yaml: - error_step = Step(name="error", **flow_yaml["on_error"]) - else: - error_step = Step( - name="error", - call_type="llm", - params={ - "user_prompt": "当前工具执行发生错误,原始错误信息为:{data}. 请向用户展示错误信息,并给出可能的解决方案。\n\n背景信息:{context}", - }, - ) - - steps = {} - for step in flow_yaml["steps"]: - steps[step["name"]] = Step(**step) - - if "next_flow" not in flow_yaml: - next_flow = None - else: - next_flow = [] - for next_flow_item in flow_yaml["next_flow"]: - next_flow.append(NextFlow( - id=next_flow_item["id"], - question=next_flow_item["question"], - )) - flows.append({ - "id": flow_yaml["id"], - "description": flow_yaml["description"], - "data": Flow(on_error=error_step, steps=steps, next_flow=next_flow), - }) - return flows - - def _load_lib(self) -> list[Any]: - lib_path = self._plugin_location / LIB_DIR - if lib_path.is_dir(): - LOGGER.info(f"载入Lib:{lib_path}") - # 插件lib载入到特定模块 - try: - spec = importlib.util.spec_from_file_location( - "apps.plugins." + self.plugin_name, - lib_path, - ) - - if spec is None: - return [] - - module = importlib.util.module_from_spec(spec) - sys.modules["apps.plugins." + self.plugin_name] = module - - loader = spec.loader - if loader is None: - return [] - - loader.exec_module(module) - except Exception as e: - LOGGER.info(msg=f"Failed to load plugin lib: {e}") - return [] - - # 注册模块所有工具 - calls = [] - for cls in sys.modules["apps.plugins." + self.plugin_name].exported: - try: - if self.check_user_class(cls): - calls.append(cls) - except Exception as e: # noqa: PERF203 - LOGGER.info(msg=f"Failed to register tools: {e}") - return calls - return [] - - @staticmethod - def check_user_class(user_cls) -> bool: # noqa: ANN001 - """检查用户类是否符合Call标准要求""" - flag = True - - if not hasattr(user_cls, "name") or not isinstance(user_cls.name, str): - flag = False - if not hasattr(user_cls, "description") or not isinstance(user_cls.description, str): - flag = False - if not hasattr(user_cls, "spec") or not isinstance(user_cls.spec, dict): - flag = False - if not callable(user_cls) or not callable(user_cls.__call__): - flag = False - - if not flag: - LOGGER.info(msg=f"类{user_cls.__name__}不符合Call标准要求。") - - return flag - - -class Loader(metaclass=Singleton): - """载入全部插件""" - - exclude_list: ClassVar[list[str]] = [ - ".git", - "example", - ] - path: str = config["PLUGIN_DIR"] - - @classmethod - def load_predefined_call(cls) -> None: - """载入apps/scheduler/call下面的所有工具""" - calls = [getattr(system_call, name) for name in system_call.__all__] - try: - Pool().add_calls(None, calls) - except Exception as e: - LOGGER.info(msg=f"Failed to load predefined call: {e!s}\n{traceback.format_exc()}") - - @classmethod - def init(cls) -> None: - """初始化插件""" - cls.load_predefined_call() - for item in Path(cls.path).iterdir(): - if item.is_dir() and item.name not in cls.exclude_list: - try: - PluginLoader(plugin_id=item.name) - except Exception as e: - LOGGER.info(msg=f"Failed to load plugin: {e!s}\n{traceback.format_exc()}") - - @classmethod - def reload(cls) -> None: - """热重载插件""" - Pool().clean_db() - cls.init() diff --git a/apps/scheduler/pool/loader/call.py b/apps/scheduler/pool/loader/call.py new file mode 100644 index 0000000000000000000000000000000000000000..36fae5961ee61789b3d9c27ad36cb6d1d6fff820 --- /dev/null +++ b/apps/scheduler/pool/loader/call.py @@ -0,0 +1,161 @@ +"""Call 加载器 + +Copyright (c) Huawei Technologies Co., Ltd. 2023-2024. All rights reserved. +""" +import importlib +import sys +from pathlib import Path + +from pydantic import BaseModel + +import apps.scheduler.call as system_call +from apps.common.config import config +from apps.constants import CALL_DIR, LOGGER +from apps.entities.enum_var import CallType +from apps.entities.flow import CallMetadata +from apps.models.mongo import MongoDB + + +class CallLoader: + """Call 加载器""" + + @staticmethod + def _check_class(user_cls) -> bool: # noqa: ANN001 + """检查用户类是否符合Call标准要求""" + flag = True + + if not hasattr(user_cls, "name") or not isinstance(user_cls.name, str): + flag = False + if not hasattr(user_cls, "description") or not isinstance(user_cls.description, str): + flag = False + if not hasattr(user_cls, "params") or not issubclass(user_cls.params, BaseModel): + flag = False + if not hasattr(user_cls, "init") or not callable(user_cls.init): + flag = False + if not hasattr(user_cls, "call") or not callable(user_cls.call): + flag = False + + if not flag: + LOGGER.info(msg=f"类{user_cls.__name__}不符合Call标准要求。") + + return flag + + @staticmethod + def _check_package(package_name: str) -> bool: + """检查包是否符合要求""" + package = sys.modules["call." + package_name] + + if not hasattr(package, "service") or not isinstance(package.service, str) or not package.service: + return False + + if not hasattr(package, "__all__") or not isinstance(package.__all__, list) or not package.__all__: # noqa: SIM103 + return False + + return True + + @staticmethod + async def _load_system_call() -> list[CallMetadata]: + """加载系统Call""" + metadata = [] + + for call_name in system_call.__all__: + call_cls = getattr(system_call, call_name) + if not CallLoader._check_class(call_cls): + err = f"类{call_cls.__name__}不符合Call标准要求。" + LOGGER.info(msg=err) + continue + + metadata.append( + CallMetadata( + _id=call_name, + type=CallType.SYSTEM, + name=call_cls.name, + description=call_cls.description, + path=call_name, + ), + ) + + return metadata + + @classmethod + async def _load_python_call(cls) -> list[CallMetadata]: + """加载Python Call""" + call_dir = Path(config["SERVICE_DIR"]) / CALL_DIR + metadata = [] + + # 检查是否存在__init__.py + if not (call_dir / "__init__.py").exists(): + LOGGER.info(msg=f"目录{call_dir}不存在__init__.py文件。") + (Path(call_dir) / "__init__.py").touch() + + # 载入整个包 + try: + sys.path.insert(0, str(call_dir)) + importlib.import_module("call") + except Exception as e: + err = f"载入包{call_dir}失败:{e}" + raise RuntimeError(err) from e + + # 处理每一个子包 + for call_file in Path(call_dir).rglob("*"): + if not call_file.is_dir(): + continue + + if not cls._check_package(call_file.name): + LOGGER.info(msg=f"包call.{call_file.name}不符合Call标准要求,跳过载入。") + continue + + # 载入包 + try: + call_package = importlib.import_module("call." + call_file.name) + if not CallLoader._check_class(call_package.service): + LOGGER.info(msg=f"包call.{call_file.name}不符合Call标准要求,跳过载入。") + continue + + for call_id in call_package.__all__: + call_cls = getattr(call_package, call_id) + if not CallLoader._check_class(call_cls): + LOGGER.info(msg=f"类{call_cls.__name__}不符合Call标准要求,跳过载入。") + continue + + metadata.append( + CallMetadata( + _id=call_id, + type=CallType.PYTHON, + name=call_cls.name, + description=call_cls.description, + path=f"{call_package.service}::call.{call_file.name}.{call_id}", + ), + ) + except Exception as e: + err = f"载入包{call_file}失败:{e},跳过载入" + LOGGER.info(msg=err) + continue + + return metadata + + + @staticmethod + async def load_one() + + + @staticmethod + async def load() -> None: + """加载Call""" + call_metadata = await CallLoader._load_system_call() + call_metadata.extend(await CallLoader._load_python_call()) + + + @staticmethod + async def get() -> list[CallMetadata]: + """获取当前已知的所有Call元数据""" + call_collection = MongoDB.get_collection("call") + result: list[CallMetadata] = [] + try: + cursor = call_collection.find({}) + async for item in cursor: + result.extend([CallMetadata(**item)]) + except Exception as e: + LOGGER.error(msg=f"获取Call元数据失败:{e}") + + return result diff --git a/apps/scheduler/pool/loader/flow.py b/apps/scheduler/pool/loader/flow.py new file mode 100644 index 0000000000000000000000000000000000000000..a254546ba350561444c0ff357934e502c0413cda --- /dev/null +++ b/apps/scheduler/pool/loader/flow.py @@ -0,0 +1,60 @@ +class FlowLoader: + """工作流加载器""" + + @staticmethod + async def generate(flow_id: str) -> None: + """从数据库中加载工作流""" + pass + + + @staticmethod + def load() -> None: + """执行工作流加载""" + pass + + + +def _load_flow(self) -> list[dict[str, Any]]: + flow_path = self._plugin_location / FLOW_DIR + flows = [] + if flow_path.is_dir(): + for current_flow_path in flow_path.iterdir(): + LOGGER.info(f"载入Flow: {current_flow_path}") + + with Path(current_flow_path).open(encoding="utf-8") as f: + flow_yaml = yaml.safe_load(f) + + if "/" in flow_yaml["id"]: + err = "Flow名称包含非法字符!" + raise ValueError(err) + + if "on_error" in flow_yaml: + error_step = Step(name="error", **flow_yaml["on_error"]) + else: + error_step = Step( + name="error", + call_type="llm", + params={ + "user_prompt": "当前工具执行发生错误,原始错误信息为:{data}. 请向用户展示错误信息,并给出可能的解决方案。\n\n背景信息:{context}", + }, + ) + + steps = {} + for step in flow_yaml["steps"]: + steps[step["name"]] = Step(**step) + + if "next_flow" not in flow_yaml: + next_flow = None + else: + next_flow = [] + for next_flow_item in flow_yaml["next_flow"]: + next_flow.append(NextFlow( + id=next_flow_item["id"], + question=next_flow_item["question"], + )) + flows.append({ + "id": flow_yaml["id"], + "description": flow_yaml["description"], + "data": Flow(on_error=error_step, steps=steps, next_flow=next_flow), + }) + return flows \ No newline at end of file diff --git a/apps/scheduler/pool/loader/metadata.py b/apps/scheduler/pool/loader/metadata.py index 5a81c3f8acabe4ee43620164d927357f6222c25e..d4568b64b3543c5382372dd8580c4a8b73c73a89 100644 --- a/apps/scheduler/pool/loader/metadata.py +++ b/apps/scheduler/pool/loader/metadata.py @@ -3,29 +3,53 @@ Copyright (c) Huawei Technologies Co., Ltd. 2023-2024. All rights reserved. """ from pathlib import Path +from typing import Any, Union import yaml from apps.constants import LOGGER -from apps.entities.flow import Metadata +from apps.entities.enum_var import MetadataType +from apps.entities.flow import ( + AppMetadata, + ServiceMetadata, +) class MetadataLoader: """元数据加载器""" @staticmethod - def check_metadata(dir: Path) -> bool: + async def load(file_path: Path) -> Union[AppMetadata, ServiceMetadata]: """检查metadata.yaml是否正确""" # 检查yaml格式 try: - metadata = yaml.safe_load(Path(dir, "metadata.yaml").read_text()) + metadata_dict = yaml.safe_load(file_path.read_text()) + metadata_type = metadata_dict["type"] except Exception as e: - LOGGER.error("metadata.yaml读取失败: %s", e) - return False - - - - @classmethod - async def load(cls) -> None: - """执行元数据加载""" + err = f"metadata.yaml读取失败: {e}" + LOGGER.error(err) + raise RuntimeError(err) from e + + if metadata_type not in MetadataType: + err = f"metadata.yaml类型错误: {metadata_type}" + LOGGER.error(err) + raise RuntimeError(err) + + # 尝试匹配格式 + try: + if metadata_type == MetadataType.APP: + metadata = AppMetadata(**metadata_dict) + elif metadata_type == MetadataType.SERVICE: + metadata = ServiceMetadata(**metadata_dict) + except Exception as e: + err = f"metadata.yaml格式错误: {e}" + LOGGER.error(err) + raise RuntimeError(err) from e + + return metadata + + + @staticmethod + async def save(metadata: dict[str, Any], file_path: Path) -> None: + """将元数据保存到文件""" pass diff --git a/apps/scheduler/pool/loader/openapi.py b/apps/scheduler/pool/loader/openapi.py index b498588fc11a79c825ba1bc2b40ab0afabf15e2a..13291edeceae1a61aaa033bf01e93388b0fad9fb 100644 --- a/apps/scheduler/pool/loader/openapi.py +++ b/apps/scheduler/pool/loader/openapi.py @@ -7,15 +7,19 @@ from typing import Any import yaml -from apps.scheduler.openapi import ReducedOpenAPISpec, reduce_openapi_spec +from apps.constants import LOGGER +from apps.scheduler.openapi import ( + ReducedOpenAPISpec, + reduce_openapi_spec, +) from apps.scheduler.pool.util import get_bytes_hash class OpenAPILoader: """OpenAPI文档载入器""" - @classmethod - def load_from_disk(cls, yaml_path: str) -> tuple[str, ReducedOpenAPISpec]: + @staticmethod + def _load_spec(yaml_path: str) -> tuple[str, ReducedOpenAPISpec]: """从本地磁盘加载OpenAPI文档""" path = Path(yaml_path) if not path.exists(): @@ -28,18 +32,27 @@ class OpenAPILoader: spec = yaml.safe_load(content) return hash_value, reduce_openapi_spec(spec) + @classmethod - def load_from_minio(cls, yaml_path: str) -> tuple[str, ReducedOpenAPISpec]: - """从MinIO加载OpenAPI文档""" + def _process_spec(cls, spec: ReducedOpenAPISpec) -> dict[str, Any]: + """处理OpenAPI文档""" pass + + @staticmethod + async def load_one(yaml_path: str) -> None: + """加载单个OpenAPI文档,可以直接指定路径""" + try: + hash_val, spec_raw = OpenAPILoader._load_spec(yaml_path) + except Exception as e: + err = f"加载OpenAPI文档失败:{e}" + LOGGER.error(msg=err) + raise RuntimeError(err) from e + + + @classmethod def load(cls) -> ReducedOpenAPISpec: """执行OpenAPI文档的加载""" pass - @classmethod - def process(cls, spec: ReducedOpenAPISpec) -> dict[str, Any]: - """处理OpenAPI文档""" - pass - diff --git a/apps/scheduler/pool/util.py b/apps/scheduler/pool/util.py new file mode 100644 index 0000000000000000000000000000000000000000..1beb7930828e9254fe0daa97cf2b27af2d369f05 --- /dev/null +++ b/apps/scheduler/pool/util.py @@ -0,0 +1,15 @@ +"""工具函数 + +Copyright (c) Huawei Technologies Co., Ltd. 2023-2024. All rights reserved. +""" +import hashlib + + +def get_bytes_hash(s: bytes) -> str: + """获取字节串的哈希值""" + return hashlib.sha256(s).hexdigest() + + +def get_str_hash(s: str) -> str: + """获取字符串的哈希值""" + return get_bytes_hash(s.encode("utf-8")) diff --git a/requirements.txt b/requirements.txt index 0dddae7c394ac8cb437b7a6bf07fb8aee0918925..1c2d791b156978267e952dcd96e09ca878ff7ab1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -27,6 +27,7 @@ openai==1.57.0 openpyxl==3.1.5 paramiko==3.4.0 pgvector==0.3.6 +pillow==11.1.0 psycopg2-binary==2.9.9 pydantic==2.9.2 python-magic==0.4.27 diff --git a/sample/apps/test_app/icon.ico b/sample/apps/test_app/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..ba6134a0ab94b8dd83d098e059d3c4dd93dd1041 Binary files /dev/null and b/sample/apps/test_app/icon.ico differ diff --git a/sample/apps/test_app/metadata.yaml b/sample/apps/test_app/metadata.yaml index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..cc717e78002f194a0cb866fd4af8912977964737 100644 --- a/sample/apps/test_app/metadata.yaml +++ b/sample/apps/test_app/metadata.yaml @@ -0,0 +1,34 @@ +# 元数据种类 +type: app + +# 应用的ID +id: test_app +# 应用的名称(展示用) +name: 测试应用 +# 应用的描述(展示用,需少于150字) +description: | + 这是一个测试应用。 + 可以在该文件夹中放置应用必需的配置,例如OpenAPI文档等。 +# 应用包版本(展示用) +version: "1.0.0" +# 关联的用户账号 +author: zjq + +# 相关链接 +links: + - title: 测试链接 + url: https://www.example.com +# 首次提问 +first_questions: + - 这是测试指令 + - 样例问题? +# 对话轮次 +history_len: 3 +# 应用权限配置 +permissions: + # 权限类型(protected: 部分人可用,public: 公开, private: 私有) + type: protected + # 可访问的用户列表 + users: + - test_user1 + - test_user2 diff --git a/sample/services/test_service/metadata.yaml b/sample/services/test_service/metadata.yaml index 364a313de34b73fdf69bb7a6326376ce2186c910..8d13eaf892fa121751bb45312ac8e410531bae1f 100644 --- a/sample/services/test_service/metadata.yaml +++ b/sample/services/test_service/metadata.yaml @@ -7,12 +7,32 @@ id: test_service name: 测试服务 # 服务的描述(展示用) description: | - 这是一个测试服务!可以在该文件夹中放置连接服务必需的配置,例如OpenAPI文档等。 + 这是一个测试服务。 + 可以在该文件夹中放置连接服务必需的配置,例如OpenAPI鉴权设置等。 # Service包版本(展示用) version: "1.0.0" # 关联的用户账号 -user: zjq +author: zjq # API相关设置项 -openapi: +api: + # 服务器地址 + server: https://api.example.com + # API鉴权方式;支持header、cookie、query、oidc方式,可自由组合 + auth: + # 鉴权参数 + header: + # 鉴权参数名称 + - name: Authorization + # 鉴权参数值 + value: "Bearer sk-123456" + cookie: + - name: JSESSIONID + value: "cookie_value" + query: + - name: token + value: "token_value" + oidc: + client_id: "app_id" + client_secret: "app_secret" \ No newline at end of file