| | import enum |
| | import importlib.util |
| | import json |
| | import logging |
| | import os |
| | from pathlib import Path |
| | from typing import Any, Optional |
| |
|
| | from pydantic import BaseModel |
| |
|
| | from core.helper.position_helper import sort_to_dict_by_position_map |
| |
|
| |
|
| | class ExtensionModule(enum.Enum): |
| | MODERATION = "moderation" |
| | EXTERNAL_DATA_TOOL = "external_data_tool" |
| |
|
| |
|
| | class ModuleExtension(BaseModel): |
| | extension_class: Any = None |
| | name: str |
| | label: Optional[dict] = None |
| | form_schema: Optional[list] = None |
| | builtin: bool = True |
| | position: Optional[int] = None |
| |
|
| |
|
| | class Extensible: |
| | module: ExtensionModule |
| |
|
| | name: str |
| | tenant_id: str |
| | config: Optional[dict] = None |
| |
|
| | def __init__(self, tenant_id: str, config: Optional[dict] = None) -> None: |
| | self.tenant_id = tenant_id |
| | self.config = config |
| |
|
| | @classmethod |
| | def scan_extensions(cls): |
| | extensions: list[ModuleExtension] = [] |
| | position_map = {} |
| |
|
| | |
| | current_path = os.path.abspath(cls.__module__.replace(".", os.path.sep) + ".py") |
| | current_dir_path = os.path.dirname(current_path) |
| |
|
| | |
| | for subdir_name in os.listdir(current_dir_path): |
| | if subdir_name.startswith("__"): |
| | continue |
| |
|
| | subdir_path = os.path.join(current_dir_path, subdir_name) |
| | extension_name = subdir_name |
| | if os.path.isdir(subdir_path): |
| | file_names = os.listdir(subdir_path) |
| |
|
| | |
| | |
| | builtin = False |
| | position = None |
| | if "__builtin__" in file_names: |
| | builtin = True |
| |
|
| | builtin_file_path = os.path.join(subdir_path, "__builtin__") |
| | if os.path.exists(builtin_file_path): |
| | position = int(Path(builtin_file_path).read_text(encoding="utf-8").strip()) |
| | position_map[extension_name] = position |
| |
|
| | if (extension_name + ".py") not in file_names: |
| | logging.warning(f"Missing {extension_name}.py file in {subdir_path}, Skip.") |
| | continue |
| |
|
| | |
| | py_path = os.path.join(subdir_path, extension_name + ".py") |
| | spec = importlib.util.spec_from_file_location(extension_name, py_path) |
| | if not spec or not spec.loader: |
| | raise Exception(f"Failed to load module {extension_name} from {py_path}") |
| | mod = importlib.util.module_from_spec(spec) |
| | spec.loader.exec_module(mod) |
| |
|
| | extension_class = None |
| | for name, obj in vars(mod).items(): |
| | if isinstance(obj, type) and issubclass(obj, cls) and obj != cls: |
| | extension_class = obj |
| | break |
| |
|
| | if not extension_class: |
| | logging.warning(f"Missing subclass of {cls.__name__} in {py_path}, Skip.") |
| | continue |
| |
|
| | json_data = {} |
| | if not builtin: |
| | if "schema.json" not in file_names: |
| | logging.warning(f"Missing schema.json file in {subdir_path}, Skip.") |
| | continue |
| |
|
| | json_path = os.path.join(subdir_path, "schema.json") |
| | json_data = {} |
| | if os.path.exists(json_path): |
| | with open(json_path, encoding="utf-8") as f: |
| | json_data = json.load(f) |
| |
|
| | extensions.append( |
| | ModuleExtension( |
| | extension_class=extension_class, |
| | name=extension_name, |
| | label=json_data.get("label"), |
| | form_schema=json_data.get("form_schema"), |
| | builtin=builtin, |
| | position=position, |
| | ) |
| | ) |
| |
|
| | sorted_extensions = sort_to_dict_by_position_map( |
| | position_map=position_map, data=extensions, name_func=lambda x: x.name |
| | ) |
| |
|
| | return sorted_extensions |
| |
|