feat(plugin): basic communication

This commit is contained in:
Junyan Qin
2025-06-14 17:27:21 +08:00
parent 2d06f1cadb
commit 6f2fd72af6
4 changed files with 150 additions and 86 deletions
+12 -4
View File
@@ -29,17 +29,23 @@ class PluginRuntimeConnector:
async def initialize(self): async def initialize(self):
async def new_connection_callback(connection: stdio_connection.StdioConnection): async def new_connection_callback(connection: stdio_connection.StdioConnection):
self.ap.logger.info('Connected to plugin runtime.') self.handler = handler.RuntimeConnectionHandler(connection, self.ap)
self.handler = handler.RuntimeConnectionHandler(connection)
self.handler_task = asyncio.create_task(self.handler.run()) self.handler_task = asyncio.create_task(self.handler.run())
_ = await self.handler.ping()
self.ap.logger.info('Connected to plugin runtime.')
await self.handler_task
task: asyncio.Task | None = None
if platform.get_platform() == 'docker': # use websocket if platform.get_platform() == 'docker': # use websocket
self.ap.logger.info('use websocket to connect to plugin runtime')
ws_url = self.ap.instance_config.data['plugin']['runtime_ws_url'] ws_url = self.ap.instance_config.data['plugin']['runtime_ws_url']
ctrl = ws_client_controller.WebSocketClientController( ctrl = ws_client_controller.WebSocketClientController(
ws_url=ws_url, ws_url=ws_url,
) )
await ctrl.run(new_connection_callback) task = ctrl.run(new_connection_callback)
else: # stdio else: # stdio
self.ap.logger.info('use stdio to connect to plugin runtime')
# cmd: lbp rt -s # cmd: lbp rt -s
python_path = sys.executable python_path = sys.executable
env = os.environ.copy() env = os.environ.copy()
@@ -48,7 +54,9 @@ class PluginRuntimeConnector:
args=['-m', 'langbot_plugin.cli.__init__', 'rt', '-s'], args=['-m', 'langbot_plugin.cli.__init__', 'rt', '-s'],
env=env, env=env,
) )
await ctrl.run(new_connection_callback) task = ctrl.run(new_connection_callback)
asyncio.create_task(task)
async def run(self): async def run(self):
pass pass
+77 -81
View File
@@ -2,13 +2,9 @@ from __future__ import annotations
import typing import typing
import abc import abc
import pydantic.v1 as pydantic
import enum
from . import events from . import events
from ..provider.tools import entities as tools_entities
from ..core import app from ..core import app
from ..discover import engine as discover_engine
from ..platform.types import message as platform_message from ..platform.types import message as platform_message
from ..platform import adapter as platform_adapter from ..platform import adapter as platform_adapter
@@ -285,104 +281,104 @@ class EventContext:
EventContext.eid += 1 EventContext.eid += 1
class RuntimeContainerStatus(enum.Enum): # class RuntimeContainerStatus(enum.Enum):
"""插件容器状态""" # """插件容器状态"""
MOUNTED = 'mounted' # MOUNTED = 'mounted'
"""已加载进内存,所有位于运行时记录中的 RuntimeContainer 至少是这个状态""" # """已加载进内存,所有位于运行时记录中的 RuntimeContainer 至少是这个状态"""
INITIALIZED = 'initialized' # INITIALIZED = 'initialized'
"""已初始化""" # """已初始化"""
class RuntimeContainer(pydantic.BaseModel): # class RuntimeContainer(pydantic.BaseModel):
"""运行时的插件容器 # """运行时的插件容器
运行期间存储单个插件的信息 # 运行期间存储单个插件的信息
""" # """
plugin_name: str # plugin_name: str
"""插件名称""" # """插件名称"""
plugin_label: discover_engine.I18nString # plugin_label: discover_engine.I18nString
"""插件标签""" # """插件标签"""
plugin_description: discover_engine.I18nString # plugin_description: discover_engine.I18nString
"""插件描述""" # """插件描述"""
plugin_version: str # plugin_version: str
"""插件版本""" # """插件版本"""
plugin_author: str # plugin_author: str
"""插件作者""" # """插件作者"""
plugin_repository: str # plugin_repository: str
"""插件源码地址""" # """插件源码地址"""
main_file: str # main_file: str
"""插件主文件路径""" # """插件主文件路径"""
pkg_path: str # pkg_path: str
"""插件包路径""" # """插件包路径"""
plugin_class: typing.Type[BasePlugin] = None # plugin_class: typing.Type[BasePlugin] = None
"""插件类""" # """插件类"""
enabled: typing.Optional[bool] = True # enabled: typing.Optional[bool] = True
"""是否启用""" # """是否启用"""
priority: typing.Optional[int] = 0 # priority: typing.Optional[int] = 0
"""优先级""" # """优先级"""
config_schema: typing.Optional[list[dict]] = [] # config_schema: typing.Optional[list[dict]] = []
"""插件配置模板""" # """插件配置模板"""
plugin_config: typing.Optional[dict] = {} # plugin_config: typing.Optional[dict] = {}
"""插件配置""" # """插件配置"""
plugin_inst: typing.Optional[BasePlugin] = None # plugin_inst: typing.Optional[BasePlugin] = None
"""插件实例""" # """插件实例"""
event_handlers: dict[ # event_handlers: dict[
typing.Type[events.BaseEventModel], # typing.Type[events.BaseEventModel],
typing.Callable[[BasePlugin, EventContext], typing.Awaitable[None]], # typing.Callable[[BasePlugin, EventContext], typing.Awaitable[None]],
] = {} # ] = {}
"""事件处理器""" # """事件处理器"""
tools: list[tools_entities.LLMFunction] = [] # tools: list[tools_entities.LLMFunction] = []
"""内容函数""" # """内容函数"""
status: RuntimeContainerStatus = RuntimeContainerStatus.MOUNTED # status: RuntimeContainerStatus = RuntimeContainerStatus.MOUNTED
"""插件状态""" # """插件状态"""
class Config: # class Config:
arbitrary_types_allowed = True # arbitrary_types_allowed = True
def model_dump(self, *args, **kwargs): # def model_dump(self, *args, **kwargs):
return { # return {
'name': self.plugin_name, # 'name': self.plugin_name,
'label': self.plugin_label.to_dict(), # 'label': self.plugin_label.to_dict(),
'description': self.plugin_description.to_dict(), # 'description': self.plugin_description.to_dict(),
'version': self.plugin_version, # 'version': self.plugin_version,
'author': self.plugin_author, # 'author': self.plugin_author,
'repository': self.plugin_repository, # 'repository': self.plugin_repository,
'main_file': self.main_file, # 'main_file': self.main_file,
'pkg_path': self.pkg_path, # 'pkg_path': self.pkg_path,
'enabled': self.enabled, # 'enabled': self.enabled,
'priority': self.priority, # 'priority': self.priority,
'config_schema': self.config_schema, # 'config_schema': self.config_schema,
'event_handlers': { # 'event_handlers': {
event_name.__name__: handler.__name__ for event_name, handler in self.event_handlers.items() # event_name.__name__: handler.__name__ for event_name, handler in self.event_handlers.items()
}, # },
'tools': [ # 'tools': [
{ # {
'name': function.name, # 'name': function.name,
'human_desc': function.human_desc, # 'human_desc': function.human_desc,
'description': function.description, # 'description': function.description,
'parameters': function.parameters, # 'parameters': function.parameters,
'func': function.func.__name__, # 'func': function.func.__name__,
} # }
for function in self.tools # for function in self.tools
], # ],
'status': self.status.value, # 'status': self.status.value,
} # }
+57
View File
@@ -1,7 +1,64 @@
from __future__ import annotations from __future__ import annotations
from typing import Any
import sqlalchemy
from langbot_plugin.runtime.io import handler from langbot_plugin.runtime.io import handler
from langbot_plugin.runtime.io.connection import Connection
from langbot_plugin.entities.io.actions.enums import (
CommonAction,
RuntimeToLangBotAction,
)
from ..entity.persistence import plugin as persistence_plugin
from ..core import app
class RuntimeConnectionHandler(handler.Handler): class RuntimeConnectionHandler(handler.Handler):
"""Runtime connection handler""" """Runtime connection handler"""
ap: app.Application
def __init__(self, connection: Connection, ap: app.Application):
super().__init__(connection)
self.ap = ap
@self.action(RuntimeToLangBotAction.GET_PLUGIN_SETTINGS)
async def get_plugin_settings(data: dict[str, Any]) -> handler.ActionResponse:
"""Get plugin settings"""
plugin_author = data['plugin_author']
plugin_name = data['plugin_name']
result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.select(persistence_plugin.PluginSetting)
.where(persistence_plugin.PluginSetting.plugin_author == plugin_author)
.where(persistence_plugin.PluginSetting.plugin_name == plugin_name)
)
data = {
'enabled': False,
'priority': 0,
'plugin_config': {},
}
setting = result.first()
if setting is not None:
data['enabled'] = setting.enabled
data['priority'] = setting.priority
data['plugin_config'] = setting.config
return handler.ActionResponse.success(
data=data,
)
async def ping(self) -> dict[str, Any]:
"""Ping the runtime"""
return await self.call_action(
CommonAction.PING,
{},
timeout=10,
)
+4 -1
View File
@@ -5,7 +5,10 @@ import sys
def get_platform() -> str: def get_platform() -> str:
"""获取当前平台""" """获取当前平台"""
# 检查是不是在 docker 里 # 检查是不是在 docker 里
if os.path.exists('/.dockerenv'):
DOCKER_ENV = os.environ.get('DOCKER_ENV', 'false')
if os.path.exists('/.dockerenv') or DOCKER_ENV == 'true':
return 'docker' return 'docker'
return sys.platform return sys.platform