mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-11 00:06:04 +00:00
feat: discovering plugins by manifests
This commit is contained in:
@@ -8,6 +8,7 @@ import enum
|
||||
from . import events
|
||||
from ..provider.tools import entities as tools_entities
|
||||
from ..core import app
|
||||
from ..discover import engine as discover_engine
|
||||
from ..platform.types import message as platform_message
|
||||
from ..platform import adapter as platform_adapter
|
||||
|
||||
@@ -308,7 +309,10 @@ class RuntimeContainer(pydantic.BaseModel):
|
||||
plugin_name: str
|
||||
"""插件名称"""
|
||||
|
||||
plugin_description: str
|
||||
plugin_label: discover_engine.I18nString
|
||||
"""插件标签"""
|
||||
|
||||
plugin_description: discover_engine.I18nString
|
||||
"""插件描述"""
|
||||
|
||||
plugin_version: str
|
||||
@@ -317,7 +321,7 @@ class RuntimeContainer(pydantic.BaseModel):
|
||||
plugin_author: str
|
||||
"""插件作者"""
|
||||
|
||||
plugin_source: str
|
||||
plugin_repository: str
|
||||
"""插件源码地址"""
|
||||
|
||||
main_file: str
|
||||
@@ -343,7 +347,7 @@ class RuntimeContainer(pydantic.BaseModel):
|
||||
]] = {}
|
||||
"""事件处理器"""
|
||||
|
||||
content_functions: list[tools_entities.LLMFunction] = []
|
||||
tools: list[tools_entities.LLMFunction] = []
|
||||
"""内容函数"""
|
||||
|
||||
status: RuntimeContainerStatus = RuntimeContainerStatus.MOUNTED
|
||||
@@ -355,10 +359,10 @@ class RuntimeContainer(pydantic.BaseModel):
|
||||
def to_setting_dict(self):
|
||||
return {
|
||||
'name': self.plugin_name,
|
||||
'description': self.plugin_description,
|
||||
'description': self.plugin_description.to_dict(),
|
||||
'version': self.plugin_version,
|
||||
'author': self.plugin_author,
|
||||
'source': self.plugin_source,
|
||||
'source': self.plugin_repository,
|
||||
'main_file': self.main_file,
|
||||
'pkg_path': self.pkg_path,
|
||||
'priority': self.priority,
|
||||
@@ -369,17 +373,18 @@ class RuntimeContainer(pydantic.BaseModel):
|
||||
self,
|
||||
setting: dict
|
||||
):
|
||||
self.plugin_source = setting['source']
|
||||
self.plugin_repository = setting['source']
|
||||
self.priority = setting['priority']
|
||||
self.enabled = setting['enabled']
|
||||
|
||||
def model_dump(self, *args, **kwargs):
|
||||
return {
|
||||
'name': self.plugin_name,
|
||||
'description': self.plugin_description,
|
||||
'label': self.plugin_label.to_dict(),
|
||||
'description': self.plugin_description.to_dict(),
|
||||
'version': self.plugin_version,
|
||||
'author': self.plugin_author,
|
||||
'source': self.plugin_source,
|
||||
'repository': self.plugin_repository,
|
||||
'main_file': self.main_file,
|
||||
'pkg_path': self.pkg_path,
|
||||
'enabled': self.enabled,
|
||||
@@ -388,7 +393,7 @@ class RuntimeContainer(pydantic.BaseModel):
|
||||
event_name.__name__: handler.__name__
|
||||
for event_name, handler in self.event_handlers.items()
|
||||
},
|
||||
'content_functions': [
|
||||
'tools': [
|
||||
{
|
||||
'name': function.name,
|
||||
'human_desc': function.human_desc,
|
||||
@@ -396,7 +401,7 @@ class RuntimeContainer(pydantic.BaseModel):
|
||||
'parameters': function.parameters,
|
||||
'func': function.func.__name__,
|
||||
}
|
||||
for function in self.content_functions
|
||||
for function in self.tools
|
||||
],
|
||||
'status': self.status.value,
|
||||
}
|
||||
|
||||
@@ -129,8 +129,8 @@ class GitHubRepoInstaller(installer.PluginInstaller):
|
||||
if plugin_container is None:
|
||||
raise errors.PluginInstallerError('插件不存在或未成功加载')
|
||||
else:
|
||||
if plugin_container.plugin_source:
|
||||
plugin_source = plugin_container.plugin_source
|
||||
if plugin_container.plugin_repository:
|
||||
plugin_source = plugin_container.plugin_repository
|
||||
task_context.trace("转交安装任务.", "update-plugin")
|
||||
await self.install_plugin(plugin_source, task_context)
|
||||
else:
|
||||
|
||||
@@ -9,7 +9,7 @@ from .. import loader, events, context, models
|
||||
from ...core import entities as core_entities
|
||||
from ...provider.tools import entities as tools_entities
|
||||
from ...utils import funcschema
|
||||
|
||||
from ...discover import engine as discover_engine
|
||||
|
||||
class PluginLoader(loader.PluginLoader):
|
||||
"""加载 plugins/ 目录下的插件"""
|
||||
@@ -31,13 +31,6 @@ class PluginLoader(loader.PluginLoader):
|
||||
|
||||
async def initialize(self):
|
||||
"""初始化"""
|
||||
setattr(models, 'register', self.register)
|
||||
setattr(models, 'on', self.on)
|
||||
setattr(models, 'func', self.func)
|
||||
|
||||
setattr(context, 'register', self.register)
|
||||
setattr(context, 'handler', self.handler)
|
||||
setattr(context, 'llm_func', self.llm_func)
|
||||
|
||||
def register(
|
||||
self,
|
||||
@@ -49,14 +42,15 @@ class PluginLoader(loader.PluginLoader):
|
||||
self.ap.logger.debug(f'注册插件 {name} {version} by {author}')
|
||||
container = context.RuntimeContainer(
|
||||
plugin_name=name,
|
||||
plugin_description=description,
|
||||
plugin_label=discover_engine.I18nString(en_US=name, zh_CN=name),
|
||||
plugin_description=discover_engine.I18nString(en_US=description, zh_CN=description),
|
||||
plugin_version=version,
|
||||
plugin_author=author,
|
||||
plugin_source='',
|
||||
plugin_repository='',
|
||||
pkg_path=self._current_pkg_path,
|
||||
main_file=self._current_module_path,
|
||||
event_handlers={},
|
||||
content_functions=[],
|
||||
tools=[],
|
||||
)
|
||||
|
||||
self._current_container = container
|
||||
@@ -126,7 +120,7 @@ class PluginLoader(loader.PluginLoader):
|
||||
func=handler,
|
||||
)
|
||||
|
||||
self._current_container.content_functions.append(llm_function)
|
||||
self._current_container.tools.append(llm_function)
|
||||
|
||||
return func
|
||||
|
||||
@@ -165,7 +159,7 @@ class PluginLoader(loader.PluginLoader):
|
||||
func=func,
|
||||
)
|
||||
|
||||
self._current_container.content_functions.append(llm_function)
|
||||
self._current_container.tools.append(llm_function)
|
||||
|
||||
return func
|
||||
|
||||
@@ -205,4 +199,11 @@ class PluginLoader(loader.PluginLoader):
|
||||
async def load_plugins(self):
|
||||
"""加载插件
|
||||
"""
|
||||
setattr(models, 'register', self.register)
|
||||
setattr(models, 'on', self.on)
|
||||
setattr(models, 'func', self.func)
|
||||
|
||||
setattr(context, 'register', self.register)
|
||||
setattr(context, 'handler', self.handler)
|
||||
setattr(context, 'llm_func', self.llm_func)
|
||||
await self._walk_plugin_path(__import__("plugins", fromlist=[""]))
|
||||
|
||||
95
pkg/plugin/loaders/manifest.py
Normal file
95
pkg/plugin/loaders/manifest.py
Normal file
@@ -0,0 +1,95 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
import abc
|
||||
import os
|
||||
import traceback
|
||||
|
||||
from ...core import app
|
||||
from .. import context, events, models
|
||||
from .. import loader
|
||||
from ...utils import funcschema
|
||||
from ...provider.tools import entities as tools_entities
|
||||
|
||||
|
||||
class PluginManifestLoader(loader.PluginLoader):
|
||||
"""通过插件清单发现插件"""
|
||||
|
||||
_current_container: context.RuntimeContainer = None
|
||||
|
||||
def __init__(self, ap: app.Application):
|
||||
super().__init__(ap)
|
||||
|
||||
def handler(
|
||||
self,
|
||||
event: typing.Type[events.BaseEventModel]
|
||||
) -> typing.Callable[[typing.Callable], typing.Callable]:
|
||||
"""注册事件处理器"""
|
||||
self.ap.logger.debug(f'注册事件处理器 {event.__name__}')
|
||||
def wrapper(func: typing.Callable) -> typing.Callable:
|
||||
|
||||
self._current_container.event_handlers[event] = func
|
||||
|
||||
return func
|
||||
|
||||
return wrapper
|
||||
|
||||
def llm_func(
|
||||
self,
|
||||
name: str=None,
|
||||
) -> typing.Callable:
|
||||
"""注册内容函数"""
|
||||
self.ap.logger.debug(f'注册内容函数 {name}')
|
||||
def wrapper(func: typing.Callable) -> typing.Callable:
|
||||
|
||||
function_schema = funcschema.get_func_schema(func)
|
||||
function_name = self._current_container.plugin_name + '-' + (func.__name__ if name is None else name)
|
||||
|
||||
llm_function = tools_entities.LLMFunction(
|
||||
name=function_name,
|
||||
human_desc='',
|
||||
description=function_schema['description'],
|
||||
parameters=function_schema['parameters'],
|
||||
func=func,
|
||||
)
|
||||
|
||||
self._current_container.tools.append(llm_function)
|
||||
|
||||
return func
|
||||
|
||||
return wrapper
|
||||
|
||||
async def load_plugins(self):
|
||||
"""加载插件"""
|
||||
setattr(context, 'handler', self.handler)
|
||||
setattr(context, 'llm_func', self.llm_func)
|
||||
|
||||
plugin_manifests = self.ap.discover.get_components_by_kind('Plugin')
|
||||
|
||||
for plugin_manifest in plugin_manifests:
|
||||
try:
|
||||
current_plugin_container = context.RuntimeContainer(
|
||||
plugin_name=plugin_manifest.metadata.name,
|
||||
plugin_label=plugin_manifest.metadata.label,
|
||||
plugin_description=plugin_manifest.metadata.description,
|
||||
plugin_version=plugin_manifest.metadata.version,
|
||||
plugin_author=plugin_manifest.metadata.author,
|
||||
plugin_repository=plugin_manifest.metadata.repository,
|
||||
main_file=os.path.join(plugin_manifest.rel_dir, plugin_manifest.execution.python.path),
|
||||
pkg_path=plugin_manifest.rel_dir,
|
||||
event_handlers={},
|
||||
tools=[],
|
||||
)
|
||||
|
||||
self._current_container = current_plugin_container
|
||||
|
||||
# extract the plugin class
|
||||
# this step will load the plugin module,
|
||||
# so the event handlers and tools will be registered
|
||||
plugin_class = plugin_manifest.get_python_component_class()
|
||||
current_plugin_container.plugin_class = plugin_class
|
||||
|
||||
self.plugins.append(current_plugin_container)
|
||||
except Exception as e:
|
||||
self.ap.logger.error(f'加载插件 {plugin_manifest.metadata.name} 时发生错误')
|
||||
traceback.print_exc()
|
||||
@@ -5,7 +5,7 @@ import traceback
|
||||
|
||||
from ..core import app, taskmgr
|
||||
from . import context, loader, events, installer, setting, models
|
||||
from .loaders import classic
|
||||
from .loaders import classic, manifest
|
||||
from .installers import github
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ class PluginManager:
|
||||
|
||||
ap: app.Application
|
||||
|
||||
loader: loader.PluginLoader
|
||||
loaders: list[loader.PluginLoader]
|
||||
|
||||
installer: installer.PluginInstaller
|
||||
|
||||
@@ -22,6 +22,8 @@ class PluginManager:
|
||||
|
||||
api_host: context.APIHost
|
||||
|
||||
plugin_containers: list[context.RuntimeContainer]
|
||||
|
||||
def plugins(
|
||||
self,
|
||||
enabled: bool=None,
|
||||
@@ -29,7 +31,7 @@ class PluginManager:
|
||||
) -> list[context.RuntimeContainer]:
|
||||
"""获取插件列表
|
||||
"""
|
||||
plugins = self.loader.plugins
|
||||
plugins = self.plugin_containers
|
||||
|
||||
if enabled is not None:
|
||||
plugins = [plugin for plugin in plugins if plugin.enabled == enabled]
|
||||
@@ -41,13 +43,18 @@ class PluginManager:
|
||||
|
||||
def __init__(self, ap: app.Application):
|
||||
self.ap = ap
|
||||
self.loader = classic.PluginLoader(ap)
|
||||
self.loaders = [
|
||||
classic.PluginLoader(ap),
|
||||
manifest.PluginManifestLoader(ap),
|
||||
]
|
||||
self.installer = github.GitHubRepoInstaller(ap)
|
||||
self.setting = setting.SettingManager(ap)
|
||||
self.api_host = context.APIHost(ap)
|
||||
self.plugin_containers = []
|
||||
|
||||
async def initialize(self):
|
||||
await self.loader.initialize()
|
||||
for loader in self.loaders:
|
||||
await loader.initialize()
|
||||
await self.installer.initialize()
|
||||
await self.setting.initialize()
|
||||
await self.api_host.initialize()
|
||||
@@ -55,14 +62,16 @@ class PluginManager:
|
||||
setattr(models, 'require_ver', self.api_host.require_ver)
|
||||
|
||||
async def load_plugins(self):
|
||||
await self.loader.load_plugins()
|
||||
for loader in self.loaders:
|
||||
await loader.load_plugins()
|
||||
self.plugin_containers.extend(loader.plugins)
|
||||
|
||||
await self.setting.sync_setting(self.loader.plugins)
|
||||
await self.setting.sync_setting(self.plugin_containers)
|
||||
|
||||
# 按优先级倒序
|
||||
self.loader.plugins.sort(key=lambda x: x.priority, reverse=True)
|
||||
self.plugin_containers.sort(key=lambda x: x.priority, reverse=True)
|
||||
|
||||
self.ap.logger.debug(f'优先级排序后的插件列表 {self.loader.plugins}')
|
||||
self.ap.logger.debug(f'优先级排序后的插件列表 {self.plugin_containers}')
|
||||
|
||||
async def initialize_plugin(self, plugin: context.RuntimeContainer):
|
||||
self.ap.logger.debug(f'初始化插件 {plugin.plugin_name}')
|
||||
@@ -147,7 +156,7 @@ class PluginManager:
|
||||
await self.ap.ctr_mgr.plugin.post_remove_record(
|
||||
{
|
||||
"name": plugin_name,
|
||||
"remote": plugin_container.plugin_source,
|
||||
"remote": plugin_container.plugin_repository,
|
||||
"author": plugin_container.plugin_author,
|
||||
"version": plugin_container.plugin_version
|
||||
}
|
||||
@@ -171,7 +180,7 @@ class PluginManager:
|
||||
await self.ap.ctr_mgr.plugin.post_update_record(
|
||||
plugin={
|
||||
"name": plugin_name,
|
||||
"remote": plugin_container.plugin_source,
|
||||
"remote": plugin_container.plugin_repository,
|
||||
"author": plugin_container.plugin_author,
|
||||
"version": plugin_container.plugin_version
|
||||
},
|
||||
@@ -238,7 +247,7 @@ class PluginManager:
|
||||
plugins_info: list[dict] = [
|
||||
{
|
||||
'name': plugin.plugin_name,
|
||||
'remote': plugin.plugin_source,
|
||||
'remote': plugin.plugin_repository,
|
||||
'version': plugin.plugin_version,
|
||||
'author': plugin.plugin_author
|
||||
} for plugin in emitted_plugins
|
||||
@@ -266,7 +275,7 @@ class PluginManager:
|
||||
|
||||
plugin.enabled = new_status
|
||||
|
||||
await self.setting.dump_container_setting(self.loader.plugins)
|
||||
await self.setting.dump_container_setting(self.plugin_containers)
|
||||
|
||||
break
|
||||
|
||||
@@ -280,11 +289,11 @@ class PluginManager:
|
||||
plugin_name = plugin.get('name')
|
||||
plugin_priority = plugin.get('priority')
|
||||
|
||||
for plugin in self.loader.plugins:
|
||||
for plugin in self.plugin_containers:
|
||||
if plugin.plugin_name == plugin_name:
|
||||
plugin.priority = plugin_priority
|
||||
break
|
||||
|
||||
self.loader.plugins.sort(key=lambda x: x.priority, reverse=True)
|
||||
self.plugin_containers.sort(key=lambda x: x.priority, reverse=True)
|
||||
|
||||
await self.setting.dump_container_setting(self.loader.plugins)
|
||||
await self.setting.dump_container_setting(self.plugin_containers)
|
||||
|
||||
@@ -36,7 +36,7 @@ class SettingManager:
|
||||
if plugin_container.pkg_path == value['pkg_path']:
|
||||
matched = True
|
||||
|
||||
plugin_container.plugin_source = value['source']
|
||||
plugin_container.plugin_repository = value['source']
|
||||
break
|
||||
|
||||
if not matched:
|
||||
|
||||
Reference in New Issue
Block a user