mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-07 14:26:03 +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:
254
src/langbot/pkg/agent/runner/context_builder.py
Normal file
254
src/langbot/pkg/agent/runner/context_builder.py
Normal file
@@ -0,0 +1,254 @@
|
||||
"""Agent run context builder for converting Query to SDK v1 AgentRunContext."""
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
import time
|
||||
import typing
|
||||
|
||||
from langbot_plugin.api.entities.builtin.pipeline import query as pipeline_query
|
||||
|
||||
from ...core import app
|
||||
from .descriptor import AgentRunnerDescriptor
|
||||
from .config_migration import ConfigMigration
|
||||
|
||||
|
||||
# Internal models for SDK v1 context protocol matching SDK v1 resources.py
|
||||
|
||||
|
||||
class AgentTrigger(typing.TypedDict):
|
||||
"""Agent trigger information."""
|
||||
type: str
|
||||
source: str # 'pipeline' or 'event_router'
|
||||
timestamp: int | None
|
||||
|
||||
|
||||
class ConversationContext(typing.TypedDict):
|
||||
"""Conversation context."""
|
||||
session_id: str | None
|
||||
conversation_id: str | None
|
||||
launcher_type: str | None
|
||||
launcher_id: str | None
|
||||
sender_id: str | None
|
||||
bot_uuid: str | None
|
||||
pipeline_uuid: str | None
|
||||
|
||||
|
||||
class AgentInput(typing.TypedDict):
|
||||
"""Agent input."""
|
||||
text: str | None
|
||||
contents: list[dict[str, typing.Any]]
|
||||
message_chain: dict[str, typing.Any] | None
|
||||
attachments: list[dict[str, typing.Any]]
|
||||
|
||||
|
||||
# SDK v1 Protocol resource models - matching langbot-plugin-sdk/resources.py
|
||||
|
||||
|
||||
class ModelResource(typing.TypedDict):
|
||||
"""Model resource per SDK v1."""
|
||||
model_id: str
|
||||
model_type: str | None
|
||||
provider: str | None
|
||||
|
||||
|
||||
class ToolResource(typing.TypedDict):
|
||||
"""Tool resource per SDK v1."""
|
||||
tool_name: str
|
||||
tool_type: str | None
|
||||
description: str | None
|
||||
|
||||
|
||||
class KnowledgeBaseResource(typing.TypedDict):
|
||||
"""Knowledge base resource per SDK v1."""
|
||||
kb_id: str
|
||||
kb_name: str | None
|
||||
kb_type: str | None
|
||||
|
||||
|
||||
class FileResource(typing.TypedDict):
|
||||
"""File resource per SDK v1."""
|
||||
file_id: str
|
||||
file_name: str | None
|
||||
mime_type: str | None
|
||||
source: str | None
|
||||
|
||||
|
||||
class StorageResource(typing.TypedDict):
|
||||
"""Storage resource per SDK v1."""
|
||||
plugin_storage: bool
|
||||
workspace_storage: bool
|
||||
|
||||
|
||||
class AgentResources(typing.TypedDict):
|
||||
"""Agent resources per SDK v1."""
|
||||
models: list[ModelResource]
|
||||
tools: list[ToolResource]
|
||||
knowledge_bases: list[KnowledgeBaseResource]
|
||||
files: list[FileResource]
|
||||
storage: StorageResource
|
||||
platform_capabilities: dict[str, typing.Any]
|
||||
|
||||
|
||||
class AgentRuntimeContext(typing.TypedDict):
|
||||
"""Agent runtime context."""
|
||||
langbot_version: str | None
|
||||
sdk_protocol_version: str
|
||||
query_id: int | None
|
||||
trace_id: str | None
|
||||
deadline_at: int | None
|
||||
metadata: dict[str, typing.Any]
|
||||
|
||||
|
||||
class AgentRunContextV1(typing.TypedDict):
|
||||
"""SDK v1 AgentRunContext per PROTOCOL_V1.md."""
|
||||
run_id: str
|
||||
trigger: AgentTrigger
|
||||
conversation: ConversationContext | None
|
||||
event: dict[str, typing.Any] | None # Reserved for EBA
|
||||
actor: dict[str, typing.Any] | None # Reserved for EBA
|
||||
subject: dict[str, typing.Any] | None # Reserved for EBA
|
||||
messages: list[dict[str, typing.Any]]
|
||||
input: AgentInput
|
||||
resources: AgentResources
|
||||
runtime: AgentRuntimeContext
|
||||
config: dict[str, typing.Any]
|
||||
|
||||
|
||||
class AgentRunContextBuilder:
|
||||
"""Builder for converting Query to SDK v1 AgentRunContext.
|
||||
|
||||
Responsibilities:
|
||||
- Generate new run_id (UUID, not query id)
|
||||
- Set trigger type to 'message.received' for pipeline
|
||||
- Build conversation context from session
|
||||
- Convert messages to SDK format
|
||||
- Build input from user_message and message_chain
|
||||
- Set resources from AgentResourceBuilder result
|
||||
- Build runtime context with host info, trace_id, deadline
|
||||
- Set config from runner instance configuration
|
||||
"""
|
||||
|
||||
ap: app.Application
|
||||
|
||||
def __init__(self, ap: app.Application):
|
||||
self.ap = ap
|
||||
|
||||
async def build_context(
|
||||
self,
|
||||
query: pipeline_query.Query,
|
||||
descriptor: AgentRunnerDescriptor,
|
||||
resources: AgentResources,
|
||||
) -> AgentRunContextV1:
|
||||
"""Build AgentRunContext from Query.
|
||||
|
||||
Args:
|
||||
query: Pipeline query
|
||||
descriptor: Runner descriptor
|
||||
resources: Built resources from AgentResourceBuilder
|
||||
|
||||
Returns:
|
||||
AgentRunContextV1 dict matching PROTOCOL_V1.md
|
||||
"""
|
||||
# Generate new run_id
|
||||
run_id = str(uuid.uuid4())
|
||||
|
||||
# Build trigger
|
||||
trigger: AgentTrigger = {
|
||||
'type': 'message.received',
|
||||
'source': 'pipeline',
|
||||
'timestamp': int(time.time()),
|
||||
}
|
||||
|
||||
# Build conversation context
|
||||
conversation: ConversationContext | None = None
|
||||
if query.session:
|
||||
conversation = {
|
||||
'session_id': f'{query.session.launcher_type.value}_{query.session.launcher_id}',
|
||||
'conversation_id': getattr(query.session.using_conversation, 'uuid', None),
|
||||
'launcher_type': query.session.launcher_type.value,
|
||||
'launcher_id': query.session.launcher_id,
|
||||
'sender_id': str(query.sender_id),
|
||||
'bot_uuid': query.bot_uuid,
|
||||
'pipeline_uuid': query.pipeline_uuid,
|
||||
}
|
||||
|
||||
# Build input
|
||||
input: AgentInput = self._build_input(query)
|
||||
|
||||
# Build messages
|
||||
messages = self._build_messages(query)
|
||||
|
||||
# Get runner config
|
||||
runner_config = ConfigMigration.resolve_runner_config(
|
||||
query.pipeline_config,
|
||||
descriptor.id,
|
||||
)
|
||||
|
||||
# Build runtime context
|
||||
runtime: AgentRuntimeContext = {
|
||||
'langbot_version': self.ap.ver_mgr.get_current_version(),
|
||||
'sdk_protocol_version': descriptor.protocol_version,
|
||||
'query_id': query.query_id,
|
||||
'trace_id': run_id, # Use run_id as trace_id for now
|
||||
'deadline_at': None, # TODO: set from runner config timeout
|
||||
'metadata': {
|
||||
'bot_name': query.variables.get('_monitoring_bot_name', 'Unknown'),
|
||||
'pipeline_name': query.variables.get('_monitoring_pipeline_name', 'Unknown'),
|
||||
},
|
||||
}
|
||||
|
||||
# Build full context
|
||||
context: AgentRunContextV1 = {
|
||||
'run_id': run_id,
|
||||
'trigger': trigger,
|
||||
'conversation': conversation,
|
||||
'event': None, # Reserved for EBA
|
||||
'actor': None, # Reserved for EBA
|
||||
'subject': None, # Reserved for EBA
|
||||
'messages': messages,
|
||||
'input': input,
|
||||
'resources': resources,
|
||||
'runtime': runtime,
|
||||
'config': runner_config,
|
||||
}
|
||||
|
||||
return context
|
||||
|
||||
def _build_input(self, query: pipeline_query.Query) -> AgentInput:
|
||||
"""Build AgentInput from query."""
|
||||
text = None
|
||||
contents: list[dict[str, typing.Any]] = []
|
||||
|
||||
if query.user_message:
|
||||
# Extract text if content is single text element
|
||||
if isinstance(query.user_message.content, list):
|
||||
for elem in query.user_message.content:
|
||||
contents.append(elem.model_dump(mode='json'))
|
||||
if elem.type == 'text':
|
||||
text = getattr(elem, 'text', None)
|
||||
else:
|
||||
# Single string content
|
||||
text = str(query.user_message.content)
|
||||
contents.append({'type': 'text', 'text': text})
|
||||
|
||||
# Include message_chain for platform-specific format
|
||||
message_chain_dict = None
|
||||
if query.message_chain:
|
||||
message_chain_dict = query.message_chain.model_dump(mode='json')
|
||||
|
||||
return {
|
||||
'text': text,
|
||||
'contents': contents,
|
||||
'message_chain': message_chain_dict,
|
||||
'attachments': [], # TODO: extract attachments from message_chain
|
||||
}
|
||||
|
||||
def _build_messages(self, query: pipeline_query.Query) -> list[dict[str, typing.Any]]:
|
||||
"""Build messages list from query."""
|
||||
messages: list[dict[str, typing.Any]] = []
|
||||
|
||||
if query.messages:
|
||||
for msg in query.messages:
|
||||
messages.append(msg.model_dump(mode='json'))
|
||||
|
||||
return messages
|
||||
Reference in New Issue
Block a user