mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-03 04:24:36 +00:00
feat(agent-runner): integrate AgentRunner Protocol v1 with plugin system
Phase 0 integration complete - verified minimal loop with local-agent stub runner. Changes: - Add AgentRunOrchestrator for plugin-based agent execution - Add AgentResultNormalizer for Protocol v1 result conversion - Add AgentRunnerDescriptor for runner ID parsing (plugin:author/name/runner) - Update chat handler to use new orchestrator instead of direct runner lookup - Add plugin handler methods for list_agent_runners and run_agent - Add connector methods for AgentRunner protocol forwarding - Update pipeline API to include runner options in metadata - Add integration docs and implementation plan Integration verified: - Runner: plugin:langbot/local-agent/default - Input: "你好" - Output: [stub] Echo: 你好 - Date: 2026-05-10 10:09 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -600,14 +600,16 @@ class PluginRuntimeConnector:
|
||||
yield cmd_ret
|
||||
|
||||
# AgentRunner methods
|
||||
async def list_agent_runners(self, bound_plugins: list[str] | None = None) -> list[ComponentManifest]:
|
||||
"""List all available AgentRunner components."""
|
||||
async def list_agent_runners(self, bound_plugins: list[str] | None = None) -> list[dict[str, Any]]:
|
||||
"""List all available AgentRunner components.
|
||||
|
||||
Returns list of dicts with plugin_author, plugin_name, runner_name, manifest, etc.
|
||||
"""
|
||||
if not self.is_enable_plugin:
|
||||
return []
|
||||
|
||||
runners_data = await self.handler.list_agent_runners(include_plugins=bound_plugins)
|
||||
runners = [ComponentManifest.model_validate(runner) for runner in runners_data]
|
||||
return runners
|
||||
return runners_data
|
||||
|
||||
async def run_agent(
|
||||
self,
|
||||
@@ -625,10 +627,18 @@ class PluginRuntimeConnector:
|
||||
context: AgentRunContext as dict
|
||||
|
||||
Yields:
|
||||
AgentRunReturn results as dicts
|
||||
AgentRunResult dicts per Protocol v1
|
||||
"""
|
||||
if not self.is_enable_plugin:
|
||||
yield {'type': 'finish', 'finish_reason': 'error', 'content': 'Plugin system is disabled'}
|
||||
# Return v1 protocol run.failed
|
||||
yield {
|
||||
'type': 'run.failed',
|
||||
'data': {
|
||||
'error': 'Plugin system is disabled',
|
||||
'code': 'plugin.disabled',
|
||||
'retryable': False,
|
||||
},
|
||||
}
|
||||
return
|
||||
|
||||
gen = self.handler.run_agent(plugin_author, plugin_name, runner_name, context)
|
||||
|
||||
@@ -419,76 +419,6 @@ class RuntimeConnectionHandler(handler.Handler):
|
||||
message=f'Failed to execute tool {tool_name}: {e}',
|
||||
)
|
||||
|
||||
@self.action(PluginToRuntimeAction.RETRIEVE_KNOWLEDGE)
|
||||
async def retrieve_knowledge(data: dict[str, Any]) -> handler.ActionResponse:
|
||||
"""Retrieve knowledge from a knowledge base"""
|
||||
kb_uuid = data['kb_uuid']
|
||||
query = data['query']
|
||||
top_k = data.get('top_k', 5)
|
||||
|
||||
try:
|
||||
kb = await self.ap.rag_mgr.get_knowledge_base_by_uuid(kb_uuid)
|
||||
if kb is None:
|
||||
return handler.ActionResponse.error(
|
||||
message=f'Knowledge base with uuid {kb_uuid} not found',
|
||||
)
|
||||
|
||||
results = await kb.retrieve(query=query, top_k=top_k)
|
||||
|
||||
# Convert results to dict format
|
||||
results_data = [
|
||||
{
|
||||
'id': r.id,
|
||||
'content': [c.model_dump() for c in r.content],
|
||||
'metadata': r.metadata,
|
||||
}
|
||||
for r in results
|
||||
]
|
||||
|
||||
return handler.ActionResponse.success(
|
||||
data={
|
||||
'results': results_data,
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return handler.ActionResponse.error(
|
||||
message=f'Failed to retrieve knowledge: {e}',
|
||||
)
|
||||
|
||||
@self.action(PluginToRuntimeAction.INVOKE_EMBEDDING)
|
||||
async def invoke_embedding(data: dict[str, Any]) -> handler.ActionResponse:
|
||||
"""Invoke an embedding model"""
|
||||
embedding_model_uuid = data['embedding_model_uuid']
|
||||
texts = data['texts']
|
||||
|
||||
try:
|
||||
embedding_model = await self.ap.model_mgr.get_embedding_model_by_uuid(embedding_model_uuid)
|
||||
if embedding_model is None:
|
||||
return handler.ActionResponse.error(
|
||||
message=f'Embedding model with uuid {embedding_model_uuid} not found',
|
||||
)
|
||||
|
||||
# Call embedding model to generate embeddings
|
||||
embeddings = []
|
||||
for text in texts:
|
||||
embedding = await embedding_model.provider.invoke_embedding(
|
||||
model=embedding_model,
|
||||
text=text,
|
||||
)
|
||||
embeddings.append(embedding)
|
||||
|
||||
return handler.ActionResponse.success(
|
||||
data={
|
||||
'embeddings': embeddings,
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return handler.ActionResponse.error(
|
||||
message=f'Failed to invoke embedding model: {e}',
|
||||
)
|
||||
|
||||
@self.action(RuntimeToLangBotAction.SET_BINARY_STORAGE)
|
||||
async def set_binary_storage(data: dict[str, Any]) -> handler.ActionResponse:
|
||||
"""Set binary storage"""
|
||||
@@ -856,10 +786,11 @@ class RuntimeConnectionHandler(handler.Handler):
|
||||
# Validate kb_id is in pipeline's allowed list
|
||||
allowed_kb_uuids = []
|
||||
if query.pipeline_config:
|
||||
local_agent_config = query.pipeline_config.get('ai', {}).get('local-agent', {})
|
||||
allowed_kb_uuids = local_agent_config.get('knowledge-bases', [])
|
||||
from langbot.pkg.agent.runner.config_migration import ConfigMigration
|
||||
runner_config = ConfigMigration.resolve_runner_config(query.pipeline_config, None)
|
||||
allowed_kb_uuids = runner_config.get('knowledge-bases', [])
|
||||
if not allowed_kb_uuids:
|
||||
old_kb_uuid = local_agent_config.get('knowledge-base', '')
|
||||
old_kb_uuid = runner_config.get('knowledge-base', '')
|
||||
if old_kb_uuid and old_kb_uuid != '__none__':
|
||||
allowed_kb_uuids = [old_kb_uuid]
|
||||
|
||||
@@ -1025,6 +956,55 @@ class RuntimeConnectionHandler(handler.Handler):
|
||||
|
||||
return result['tools']
|
||||
|
||||
async def list_agent_runners(self, include_plugins: list[str] | None = None) -> list[dict[str, Any]]:
|
||||
"""List agent runners from plugin runtime.
|
||||
|
||||
Returns list of dicts with:
|
||||
- plugin_author
|
||||
- plugin_name
|
||||
- runner_name
|
||||
- runner_description
|
||||
- manifest
|
||||
- protocol_version
|
||||
- capabilities
|
||||
- permissions
|
||||
- config
|
||||
"""
|
||||
result = await self.call_action(
|
||||
LangBotToRuntimeAction.LIST_AGENT_RUNNERS,
|
||||
{
|
||||
'include_plugins': include_plugins,
|
||||
},
|
||||
timeout=20,
|
||||
)
|
||||
|
||||
return result['runners']
|
||||
|
||||
async def run_agent(
|
||||
self,
|
||||
plugin_author: str,
|
||||
plugin_name: str,
|
||||
runner_name: str,
|
||||
context: dict[str, Any],
|
||||
) -> typing.AsyncGenerator[dict[str, Any], None]:
|
||||
"""Run an AgentRunner component.
|
||||
|
||||
Yields AgentRunResult dicts per Protocol v1.
|
||||
"""
|
||||
gen = self.call_action_generator(
|
||||
LangBotToRuntimeAction.RUN_AGENT,
|
||||
{
|
||||
'plugin_author': plugin_author,
|
||||
'plugin_name': plugin_name,
|
||||
'runner_name': runner_name,
|
||||
'context': context,
|
||||
},
|
||||
timeout=300,
|
||||
)
|
||||
|
||||
async for ret in gen:
|
||||
yield ret
|
||||
|
||||
async def get_plugin_icon(self, plugin_author: str, plugin_name: str) -> dict[str, Any]:
|
||||
"""Get plugin icon"""
|
||||
result = await self.call_action(
|
||||
|
||||
Reference in New Issue
Block a user