Skip to content

Commit

Permalink
feat: discover engine & manifests for platform adapters
Browse files Browse the repository at this point in the history
  • Loading branch information
RockChinQ committed Feb 22, 2025
1 parent 71ecfc2 commit d92ee23
Show file tree
Hide file tree
Showing 34 changed files with 802 additions and 75 deletions.
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
10 changes: 5 additions & 5 deletions pkg/platform/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from .types import events as platform_events


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

def adapter_class(
name: str
Expand All @@ -23,14 +23,14 @@ def adapter_class(
Returns:
typing.Callable[[typing.Type[MessageSourceAdapter]], typing.Type[MessageSourceAdapter]]: 装饰器
"""
def decorator(cls: typing.Type[MessageSourceAdapter]) -> typing.Type[MessageSourceAdapter]:
def decorator(cls: typing.Type[MessagePlatformAdapter]) -> typing.Type[MessagePlatformAdapter]:
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 +89,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 +102,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
15 changes: 15 additions & 0 deletions pkg/platform/adapter.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
apiVersion: v1
kind: ComponentTemplate
metadata:
icon:
name: MessagePlatformAdapter
label:
en_US: Message Platform Adapter
zh_CN: 消息平台适配器模板类
spec:
type:
- python
execution:
python:
path: ./adapter.py
attr: MessagePlatformAdapter
Loading

0 comments on commit d92ee23

Please sign in to comment.