Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: component discovering engine #1113

Merged
merged 2 commits into from
Feb 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ qcapi
claude.json
bard.json
/*yaml
!components.yaml
!/docker-compose.yaml
data/labels/instance_id.json
.DS_Store
Expand Down
15 changes: 15 additions & 0 deletions components.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
apiVersion: v1
kind: Blueprint
metadata:
name: builtin-components
label:
en_US: Builtin Components
zh_CN: 内置组件
spec:
components:
ComponentTemplate:
fromFiles:
- pkg/platform/adapter.yaml
MessagePlatformAdapter:
fromDirs:
- path: pkg/platform/sources/
3 changes: 3 additions & 0 deletions pkg/core/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from ..persistence import mgr as persistencemgr
from ..api.http.controller import main as http_controller
from ..api.http.service import user as user_service
from ..discover import engine as discover_engine
from ..utils import logcache, ip
from . import taskmgr
from . import entities as core_entities
Expand All @@ -38,6 +39,8 @@ class Application:
# asyncio_tasks: list[asyncio.Task] = []
task_mgr: taskmgr.AsyncTaskManager = None

discover: discover_engine.ComponentDiscoveryEngine = None

platform_mgr: im_mgr.PlatformManager = None

cmd_mgr: cmdmgr.CommandManager = None
Expand Down
2 changes: 1 addition & 1 deletion pkg/core/entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ class Query(pydantic.BaseModel):
message_chain: platform_message.MessageChain
"""消息链,platform收到的原始消息链"""

adapter: msadapter.MessageSourceAdapter
adapter: msadapter.MessagePlatformAdapter
"""消息平台适配器对象,单个app中可能启用了多个消息平台适配器,此对象表明发起此query的适配器"""

session: typing.Optional[Session] = None
Expand Down
7 changes: 7 additions & 0 deletions pkg/core/stages/build_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from ...persistence import mgr as persistencemgr
from ...api.http.controller import main as http_controller
from ...api.http.service import user as user_service
from ...discover import engine as discover_engine
from ...utils import logcache
from .. import taskmgr

Expand All @@ -32,6 +33,12 @@ async def run(self, ap: app.Application):
"""
ap.task_mgr = taskmgr.AsyncTaskManager(ap)

discover = discover_engine.ComponentDiscoveryEngine(ap)
discover.discover_blueprint(
"components.yaml"
)
ap.discover = discover

proxy_mgr = proxy.ProxyManager(ap)
await proxy_mgr.initialize()
ap.proxy_mgr = proxy_mgr
Expand Down
Empty file added pkg/discover/__init__.py
Empty file.
197 changes: 197 additions & 0 deletions pkg/discover/engine.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
from __future__ import annotations

import typing
import importlib
import os
import inspect

import yaml
import pydantic

from ..core import app


class I18nString(pydantic.BaseModel):
"""国际化字符串"""

en_US: str
"""英文"""

zh_CN: typing.Optional[str] = None
"""中文"""

ja_JP: typing.Optional[str] = None
"""日文"""


class Metadata(pydantic.BaseModel):
"""元数据"""

name: str
"""名称"""

label: I18nString
"""标签"""

description: typing.Optional[I18nString] = None
"""描述"""

icon: typing.Optional[str] = None
"""图标"""


class PythonExecution(pydantic.BaseModel):
"""Python执行"""

path: str
"""路径"""

attr: str
"""属性"""

def __init__(self, **kwargs):
super().__init__(**kwargs)

if self.path.startswith('./'):
self.path = self.path[2:]


class Execution(pydantic.BaseModel):
"""执行"""

python: PythonExecution
"""Python执行"""


class Component(pydantic.BaseModel):
"""组件清单"""

owner: str
"""组件所属"""

manifest: typing.Dict[str, typing.Any]
"""组件清单内容"""

rel_path: str
"""组件清单相对main.py的路径"""

_metadata: Metadata
"""组件元数据"""

_spec: typing.Dict[str, typing.Any]
"""组件规格"""

_execution: Execution
"""组件执行"""

def __init__(self, owner: str, manifest: typing.Dict[str, typing.Any], rel_path: str):
super().__init__(
owner=owner,
manifest=manifest,
rel_path=rel_path
)
self._metadata = Metadata(**manifest['metadata'])
self._spec = manifest['spec']
self._execution = Execution(**manifest['execution']) if 'execution' in manifest else None

@property
def kind(self) -> str:
"""组件类型"""
return self.manifest['kind']

@property
def metadata(self) -> Metadata:
"""组件元数据"""
return self._metadata

@property
def spec(self) -> typing.Dict[str, typing.Any]:
"""组件规格"""
return self._spec

@property
def execution(self) -> Execution:
"""组件执行"""
return self._execution

def get_python_component_class(self) -> typing.Type[typing.Any]:
"""获取Python组件类"""
parent_path = os.path.dirname(self.rel_path)
module_path = os.path.join(parent_path, self.execution.python.path)
if module_path.endswith('.py'):
module_path = module_path[:-3]
module_path = module_path.replace('/', '.').replace('\\', '.')
module = importlib.import_module(module_path)
return getattr(module, self.execution.python.attr)


class ComponentDiscoveryEngine:
"""组件发现引擎"""

ap: app.Application
"""应用实例"""

components: typing.Dict[str, typing.List[Component]] = {}
"""组件列表"""

def __init__(self, ap: app.Application):
self.ap = ap

def load_component_manifest(self, path: str, owner: str = 'builtin', no_save: bool = False) -> Component:
"""加载组件清单"""
with open(path, 'r') as f:
manifest = yaml.safe_load(f)
comp = Component(
owner=owner,
manifest=manifest,
rel_path=path
)
if not no_save:
if comp.kind not in self.components:
self.components[comp.kind] = []
self.components[comp.kind].append(comp)
return comp

def load_component_manifests_in_dir(self, path: str, owner: str = 'builtin', no_save: bool = False) -> typing.List[Component]:
"""加载目录中的组件清单"""
components: typing.List[Component] = []
for file in os.listdir(path):
if file.endswith('.yaml') or file.endswith('.yml'):
components.append(self.load_component_manifest(os.path.join(path, file), owner, no_save))
return components

def load_blueprint_comp_group(self, group: dict, owner: str = 'builtin', no_save: bool = False) -> typing.List[Component]:
"""加载蓝图组件组"""
components: typing.List[Component] = []
if 'fromFiles' in group:
for file in group['fromFiles']:
components.append(self.load_component_manifest(file, owner, no_save))
if 'fromDirs' in group:
for dir in group['fromDirs']:
path = dir['path']
# depth = dir['depth']
components.extend(self.load_component_manifests_in_dir(path, owner, no_save))
return components

def discover_blueprint(self, blueprint_manifest_path: str, owner: str = 'builtin'):
"""发现蓝图"""
blueprint_manifest = self.load_component_manifest(blueprint_manifest_path, owner, no_save=True)
assert blueprint_manifest.kind == 'Blueprint', '`Kind` must be `Blueprint`'
components: typing.Dict[str, typing.List[Component]] = {}

# load ComponentTemplate first
if 'ComponentTemplate' in blueprint_manifest.spec['components']:
components['ComponentTemplate'] = self.load_blueprint_comp_group(blueprint_manifest.spec['components']['ComponentTemplate'], owner)

for name, component in blueprint_manifest.spec['components'].items():
if name == 'ComponentTemplate':
continue
components[name] = self.load_blueprint_comp_group(component, owner)
return blueprint_manifest, components


def get_components_by_kind(self, kind: str) -> typing.List[Component]:
"""获取指定类型的组件"""
if kind not in self.components:
raise ValueError(f'No components found for kind: {kind}')
return self.components[kind]
2 changes: 1 addition & 1 deletion pkg/pipeline/pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ async def add_query(
sender_id: typing.Union[int, str],
message_event: platform_events.MessageEvent,
message_chain: platform_message.MessageChain,
adapter: msadapter.MessageSourceAdapter
adapter: msadapter.MessagePlatformAdapter
) -> entities.Query:
async with self.condition:
query = entities.Query(
Expand Down
26 changes: 3 additions & 23 deletions pkg/platform/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,7 @@
from .types import events as platform_events


preregistered_adapters: list[typing.Type[MessageSourceAdapter]] = []

def adapter_class(
name: str
):
"""消息平台适配器类装饰器

Args:
name (str): 适配器名称

Returns:
typing.Callable[[typing.Type[MessageSourceAdapter]], typing.Type[MessageSourceAdapter]]: 装饰器
"""
def decorator(cls: typing.Type[MessageSourceAdapter]) -> typing.Type[MessageSourceAdapter]:
cls.name = name
preregistered_adapters.append(cls)
return cls
return decorator


class MessageSourceAdapter(metaclass=abc.ABCMeta):
class MessagePlatformAdapter(metaclass=abc.ABCMeta):
"""消息平台适配器基类"""

name: str
Expand Down Expand Up @@ -89,7 +69,7 @@ async def is_muted(self, group_id: int) -> bool:
def register_listener(
self,
event_type: typing.Type[platform_message.Event],
callback: typing.Callable[[platform_message.Event, MessageSourceAdapter], None]
callback: typing.Callable[[platform_message.Event, MessagePlatformAdapter], None]
):
"""注册事件监听器

Expand All @@ -102,7 +82,7 @@ def register_listener(
def unregister_listener(
self,
event_type: typing.Type[platform_message.Event],
callback: typing.Callable[[platform_message.Event, MessageSourceAdapter], None]
callback: typing.Callable[[platform_message.Event, MessagePlatformAdapter], None]
):
"""注销事件监听器

Expand Down
14 changes: 14 additions & 0 deletions pkg/platform/adapter.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
apiVersion: v1
kind: ComponentTemplate
metadata:
name: MessagePlatformAdapter
label:
en_US: Message Platform Adapter
zh_CN: 消息平台适配器模板类
spec:
type:
- python
execution:
python:
path: ./adapter.py
attr: MessagePlatformAdapter
Loading
Loading