mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-16 14:57:15 +00:00
feat(processors): add explicitly bound plugin event processors
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
"""Agent runner descriptor."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
@@ -44,19 +45,18 @@ class AgentRunnerDescriptor(pydantic.BaseModel):
|
||||
config_schema: list[dict[str, typing.Any]] = pydantic.Field(default_factory=list)
|
||||
"""Configuration schema using DynamicForm format"""
|
||||
|
||||
capabilities: AgentRunnerCapabilities = pydantic.Field(
|
||||
default_factory=AgentRunnerCapabilities
|
||||
)
|
||||
capabilities: AgentRunnerCapabilities = pydantic.Field(default_factory=AgentRunnerCapabilities)
|
||||
"""Runner capabilities: streaming, tool_calling, knowledge_retrieval, etc."""
|
||||
|
||||
permissions: AgentRunnerPermissions = pydantic.Field(
|
||||
default_factory=AgentRunnerPermissions
|
||||
)
|
||||
permissions: AgentRunnerPermissions = pydantic.Field(default_factory=AgentRunnerPermissions)
|
||||
"""Requested LangBot resource permissions."""
|
||||
|
||||
raw_manifest: dict[str, typing.Any] = pydantic.Field(default_factory=dict)
|
||||
"""Original manifest for reference"""
|
||||
|
||||
component_kind: typing.Literal['AgentRunner', 'EventProcessor'] = 'AgentRunner'
|
||||
supported_event_patterns: list[str] = pydantic.Field(default_factory=lambda: ['*'])
|
||||
|
||||
model_config = pydantic.ConfigDict(
|
||||
extra='allow',
|
||||
)
|
||||
|
||||
@@ -160,7 +160,7 @@ class AgentConfig(pydantic.BaseModel):
|
||||
agent_id: str | None = None
|
||||
"""Host-side Agent/config identifier."""
|
||||
|
||||
processor_type: typing.Literal['agent', 'pipeline'] = 'agent'
|
||||
processor_type: typing.Literal['agent', 'pipeline', 'event_processor'] = 'agent'
|
||||
"""Product processor kind represented by this runtime config."""
|
||||
|
||||
processor_id: str | None = None
|
||||
@@ -222,7 +222,7 @@ class AgentBinding(pydantic.BaseModel):
|
||||
agent_id: str | None = None
|
||||
"""Host-side Agent/config identifier for this binding."""
|
||||
|
||||
processor_type: typing.Literal['agent', 'pipeline'] = 'agent'
|
||||
processor_type: typing.Literal['agent', 'pipeline', 'event_processor'] = 'agent'
|
||||
"""Product processor kind selected for this binding."""
|
||||
|
||||
processor_id: str | None = None
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Agent runner ID parsing and formatting."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
@@ -7,6 +8,7 @@ import dataclasses
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class RunnerIdParts:
|
||||
"""Parsed runner ID components."""
|
||||
|
||||
source: str # 'plugin' (future: 'builtin')
|
||||
plugin_author: str
|
||||
plugin_name: str
|
||||
@@ -29,31 +31,27 @@ def parse_runner_id(runner_id: str) -> RunnerIdParts:
|
||||
Raises:
|
||||
ValueError: If runner_id format is invalid
|
||||
"""
|
||||
if runner_id.startswith('plugin:'):
|
||||
parts = runner_id[7:].split('/')
|
||||
if runner_id.startswith(('plugin:', 'event_processor:')):
|
||||
source, value = runner_id.split(':', 1)
|
||||
parts = value.split('/')
|
||||
if len(parts) != 3:
|
||||
raise ValueError(
|
||||
f'Invalid plugin runner ID format: {runner_id}. '
|
||||
f'Expected: plugin:author/plugin_name/runner_name'
|
||||
f'Invalid plugin runner ID format: {runner_id}. Expected: plugin:author/plugin_name/runner_name'
|
||||
)
|
||||
plugin_author, plugin_name, runner_name = parts
|
||||
if not plugin_author or not plugin_name or not runner_name:
|
||||
raise ValueError(
|
||||
f'Invalid plugin runner ID: {runner_id}. '
|
||||
f'author, plugin_name, and runner_name must be non-empty'
|
||||
f'Invalid plugin runner ID: {runner_id}. author, plugin_name, and runner_name must be non-empty'
|
||||
)
|
||||
return RunnerIdParts(
|
||||
source='plugin',
|
||||
source=source,
|
||||
plugin_author=plugin_author,
|
||||
plugin_name=plugin_name,
|
||||
runner_name=runner_name,
|
||||
)
|
||||
else:
|
||||
# Only plugin runner IDs are valid at the protocol boundary.
|
||||
raise ValueError(
|
||||
f'Invalid runner ID format: {runner_id}. '
|
||||
f'Expected: plugin:author/plugin_name/runner_name'
|
||||
)
|
||||
raise ValueError(f'Invalid runner ID format: {runner_id}. Expected: plugin:author/plugin_name/runner_name')
|
||||
|
||||
|
||||
def format_runner_id(
|
||||
@@ -73,8 +71,8 @@ def format_runner_id(
|
||||
Returns:
|
||||
Runner ID string
|
||||
"""
|
||||
if source == 'plugin':
|
||||
return f'plugin:{plugin_author}/{plugin_name}/{runner_name}'
|
||||
if source in {'plugin', 'event_processor'}:
|
||||
return f'{source}:{plugin_author}/{plugin_name}/{runner_name}'
|
||||
else:
|
||||
raise ValueError(f'Invalid runner source: {source}')
|
||||
|
||||
@@ -88,4 +86,4 @@ def is_plugin_runner_id(runner_id: str) -> bool:
|
||||
Returns:
|
||||
True if runner ID starts with 'plugin:'
|
||||
"""
|
||||
return runner_id.startswith('plugin:')
|
||||
return runner_id.startswith(('plugin:', 'event_processor:'))
|
||||
|
||||
@@ -41,6 +41,17 @@ class AgentRunnerInvoker:
|
||||
)
|
||||
|
||||
try:
|
||||
if descriptor.component_kind == 'EventProcessor':
|
||||
context = {
|
||||
**context,
|
||||
'runtime': {
|
||||
**context['runtime'],
|
||||
'metadata': {
|
||||
**context['runtime'].get('metadata', {}),
|
||||
'component_kind': 'EventProcessor',
|
||||
},
|
||||
},
|
||||
}
|
||||
gen = self.ap.plugin_connector.run_agent(
|
||||
plugin_author=descriptor.plugin_author,
|
||||
plugin_name=descriptor.plugin_name,
|
||||
|
||||
@@ -102,6 +102,10 @@ class AgentRunOrchestrator:
|
||||
bound_plugins,
|
||||
)
|
||||
|
||||
expected_kind = 'EventProcessor' if binding.processor_type == 'event_processor' else 'AgentRunner'
|
||||
if descriptor.component_kind != expected_kind:
|
||||
raise ValueError('Processor kind does not match the selected plugin component')
|
||||
|
||||
if execution_query is None:
|
||||
execution_query = build_execution_query(event, [])
|
||||
# Synthetic events must expose the same trusted scope as pipeline queries.
|
||||
|
||||
@@ -110,19 +110,19 @@ class AgentRunnerRegistry:
|
||||
|
||||
manifest = runner_data.get('manifest', {})
|
||||
runner_id = format_runner_id(
|
||||
source='plugin',
|
||||
source='event_processor' if manifest.get('component_kind') == 'EventProcessor' else 'plugin',
|
||||
plugin_author=plugin_author,
|
||||
plugin_name=plugin_name,
|
||||
runner_name=runner_name,
|
||||
)
|
||||
|
||||
typed_manifest = AgentRunnerManifest.model_validate(manifest)
|
||||
config_schema = [
|
||||
item.model_dump(mode='json') for item in typed_manifest.config_schema
|
||||
]
|
||||
config_schema = [item.model_dump(mode='json') for item in typed_manifest.config_schema]
|
||||
|
||||
return AgentRunnerDescriptor(
|
||||
id=runner_id,
|
||||
component_kind=typed_manifest.component_kind,
|
||||
supported_event_patterns=typed_manifest.supported_event_patterns,
|
||||
source='plugin',
|
||||
label=typed_manifest.label,
|
||||
description=typed_manifest.description,
|
||||
@@ -152,6 +152,7 @@ class AgentRunnerRegistry:
|
||||
context: TenantContext,
|
||||
bound_plugins: list[str] | None = None,
|
||||
use_cache: bool = True,
|
||||
component_kind: str = 'AgentRunner',
|
||||
) -> list[AgentRunnerDescriptor]:
|
||||
"""List available runners.
|
||||
|
||||
@@ -169,7 +170,11 @@ class AgentRunnerRegistry:
|
||||
# Filter from cache. Do not treat an empty cache as final because the
|
||||
# plugin runtime may still be launching installed plugins when the
|
||||
# first metadata request arrives.
|
||||
return self._filter_runners_by_bound_plugins(cached, bound_plugins)
|
||||
return [
|
||||
r
|
||||
for r in self._filter_runners_by_bound_plugins(cached, bound_plugins)
|
||||
if r.component_kind == component_kind
|
||||
]
|
||||
|
||||
# Discover fresh (always full list)
|
||||
runners = await self._discover_runners()
|
||||
@@ -179,7 +184,11 @@ class AgentRunnerRegistry:
|
||||
self._cache[cache_key] = runners
|
||||
|
||||
# Filter locally
|
||||
return self._filter_runners_by_bound_plugins(runners, bound_plugins)
|
||||
return [
|
||||
r
|
||||
for r in self._filter_runners_by_bound_plugins(runners, bound_plugins)
|
||||
if r.component_kind == component_kind
|
||||
]
|
||||
|
||||
def _filter_runners_by_bound_plugins(
|
||||
self,
|
||||
@@ -233,7 +242,8 @@ class AgentRunnerRegistry:
|
||||
except ValueError as e:
|
||||
raise RunnerNotFoundError(runner_id) from e
|
||||
|
||||
runners = await self.list_runners(context, bound_plugins=None)
|
||||
component_kind = 'EventProcessor' if runner_id.startswith('event_processor:') else 'AgentRunner'
|
||||
runners = await self.list_runners(context, bound_plugins=None, component_kind=component_kind)
|
||||
descriptor = next((item for item in runners if item.id == runner_id), None)
|
||||
if descriptor is None:
|
||||
# The runtime launches installed plugins asynchronously, so an
|
||||
@@ -242,6 +252,7 @@ class AgentRunnerRegistry:
|
||||
context,
|
||||
bound_plugins=None,
|
||||
use_cache=False,
|
||||
component_kind=component_kind,
|
||||
)
|
||||
descriptor = next((item for item in runners if item.id == runner_id), None)
|
||||
if descriptor is None:
|
||||
|
||||
@@ -10,6 +10,7 @@ from langbot_plugin.api.entities.builtin.agent_runner.result import (
|
||||
MessageCompletedPayload,
|
||||
MessageDeltaPayload,
|
||||
RunCompletedPayload,
|
||||
ProcessorLogPayload,
|
||||
RunFailedPayload,
|
||||
StateUpdatedPayload,
|
||||
ToolCallCompletedPayload,
|
||||
@@ -34,6 +35,7 @@ STRICT_RESULT_PAYLOADS: dict[str, type[pydantic.BaseModel]] = {
|
||||
'action.requested': ActionRequestedPayload,
|
||||
'run.completed': RunCompletedPayload,
|
||||
'run.failed': RunFailedPayload,
|
||||
'processor.log': ProcessorLogPayload,
|
||||
}
|
||||
|
||||
|
||||
@@ -114,6 +116,9 @@ class AgentResultNormalizer:
|
||||
if not self.validate_payload(result_type, data, descriptor):
|
||||
return None
|
||||
|
||||
if result_type == 'processor.log':
|
||||
return None
|
||||
|
||||
if result_type == 'message.delta':
|
||||
return self._normalize_message_delta(data, descriptor)
|
||||
|
||||
|
||||
@@ -93,6 +93,13 @@ class AgentRunJournal:
|
||||
metadata={
|
||||
'event_type': event.event_type,
|
||||
'source': event.source,
|
||||
'processor_id': binding.processor_id,
|
||||
'processor_type': binding.processor_type,
|
||||
**(
|
||||
{'input_event': event.data, 'delivery': event.delivery.model_dump(mode='json')}
|
||||
if binding.processor_type == 'event_processor'
|
||||
else {}
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -188,8 +188,8 @@ class RunLedgerStore:
|
||||
query = query.where(AgentRun.conversation_id == conversation_id)
|
||||
query = self._apply_scope_filters(query, bot_id, workspace_id, thread_id, strict_thread)
|
||||
|
||||
query = query.order_by(AgentRun.priority.desc(), AgentRun.id.asc()).limit(1).with_for_update(
|
||||
skip_locked=True
|
||||
query = (
|
||||
query.order_by(AgentRun.priority.desc(), AgentRun.id.asc()).limit(1).with_for_update(skip_locked=True)
|
||||
)
|
||||
result = await session.execute(query)
|
||||
run = result.scalars().first()
|
||||
@@ -571,8 +571,7 @@ class RunLedgerStore:
|
||||
|
||||
# Filter by labels
|
||||
runtimes = [
|
||||
rt for rt in all_runtimes
|
||||
if all(rt.get('labels', {}).get(k) == v for k, v in labels.items())
|
||||
rt for rt in all_runtimes if all(rt.get('labels', {}).get(k) == v for k, v in labels.items())
|
||||
]
|
||||
total_count = len(runtimes)
|
||||
|
||||
@@ -636,6 +635,7 @@ class RunLedgerStore:
|
||||
thread_id: str | None = None,
|
||||
strict_thread: bool = False,
|
||||
runner_id: str | None = None,
|
||||
binding_id: str | None = None,
|
||||
) -> tuple[list[dict[str, typing.Any]], int | None, bool, int]:
|
||||
"""Page runs by scope.
|
||||
|
||||
@@ -652,6 +652,8 @@ class RunLedgerStore:
|
||||
count_query = count_query.where(AgentRun.status.in_(statuses))
|
||||
if runner_id is not None:
|
||||
count_query = count_query.where(AgentRun.runner_id == runner_id)
|
||||
if binding_id is not None:
|
||||
count_query = count_query.where(AgentRun.binding_id == binding_id)
|
||||
count_query = self._apply_scope_filters(count_query, bot_id, workspace_id, thread_id, strict_thread)
|
||||
count_result = await session.execute(count_query)
|
||||
total_count = count_result.scalar() or 0
|
||||
@@ -664,6 +666,8 @@ class RunLedgerStore:
|
||||
query = query.where(AgentRun.status.in_(statuses))
|
||||
if runner_id is not None:
|
||||
query = query.where(AgentRun.runner_id == runner_id)
|
||||
if binding_id is not None:
|
||||
query = query.where(AgentRun.binding_id == binding_id)
|
||||
if before_id is not None:
|
||||
query = query.where(AgentRun.id < before_id)
|
||||
query = self._apply_scope_filters(query, bot_id, workspace_id, thread_id, strict_thread)
|
||||
@@ -848,10 +852,7 @@ class RunLedgerStore:
|
||||
|
||||
# Count by status
|
||||
status_query = (
|
||||
sqlalchemy.select(
|
||||
AgentRun.status,
|
||||
func.count(AgentRun.id).label('count')
|
||||
)
|
||||
sqlalchemy.select(AgentRun.status, func.count(AgentRun.id).label('count'))
|
||||
.where(*base_filter)
|
||||
.group_by(AgentRun.status)
|
||||
)
|
||||
@@ -873,18 +874,15 @@ class RunLedgerStore:
|
||||
avg_queue_wait_seconds = None
|
||||
|
||||
# Fetch completed runs with timing data
|
||||
timing_query = (
|
||||
sqlalchemy.select(
|
||||
AgentRun.started_at,
|
||||
AgentRun.finished_at,
|
||||
AgentRun.created_at,
|
||||
)
|
||||
.where(
|
||||
AgentRun.status == 'completed',
|
||||
AgentRun.started_at.is_not(None),
|
||||
AgentRun.finished_at.is_not(None),
|
||||
*base_filter
|
||||
)
|
||||
timing_query = sqlalchemy.select(
|
||||
AgentRun.started_at,
|
||||
AgentRun.finished_at,
|
||||
AgentRun.created_at,
|
||||
).where(
|
||||
AgentRun.status == 'completed',
|
||||
AgentRun.started_at.is_not(None),
|
||||
AgentRun.finished_at.is_not(None),
|
||||
*base_filter,
|
||||
)
|
||||
timing_result = await session.execute(timing_query)
|
||||
timing_rows = timing_result.all()
|
||||
@@ -899,16 +897,10 @@ class RunLedgerStore:
|
||||
avg_duration_seconds = round(sum(durations) / len(durations), 2)
|
||||
|
||||
# Queue wait time - compute in Python
|
||||
queue_query = (
|
||||
sqlalchemy.select(
|
||||
AgentRun.created_at,
|
||||
AgentRun.started_at,
|
||||
)
|
||||
.where(
|
||||
AgentRun.started_at.is_not(None),
|
||||
*base_filter
|
||||
)
|
||||
)
|
||||
queue_query = sqlalchemy.select(
|
||||
AgentRun.created_at,
|
||||
AgentRun.started_at,
|
||||
).where(AgentRun.started_at.is_not(None), *base_filter)
|
||||
queue_result = await session.execute(queue_query)
|
||||
queue_rows = queue_result.all()
|
||||
|
||||
@@ -957,12 +949,8 @@ class RunLedgerStore:
|
||||
|
||||
async with self._session_factory() as session:
|
||||
# Count by status
|
||||
status_query = (
|
||||
sqlalchemy.select(
|
||||
AgentRuntime.status,
|
||||
func.count(AgentRuntime.id).label('count')
|
||||
)
|
||||
.group_by(AgentRuntime.status)
|
||||
status_query = sqlalchemy.select(AgentRuntime.status, func.count(AgentRuntime.id).label('count')).group_by(
|
||||
AgentRuntime.status
|
||||
)
|
||||
status_result = await session.execute(status_query)
|
||||
status_counts = {row.status: row.count for row in status_result}
|
||||
@@ -975,9 +963,8 @@ class RunLedgerStore:
|
||||
avg_heartbeat_age = None
|
||||
max_heartbeat_age = None
|
||||
|
||||
heartbeat_query = (
|
||||
sqlalchemy.select(AgentRuntime.last_heartbeat_at)
|
||||
.where(AgentRuntime.last_heartbeat_at.is_not(None))
|
||||
heartbeat_query = sqlalchemy.select(AgentRuntime.last_heartbeat_at).where(
|
||||
AgentRuntime.last_heartbeat_at.is_not(None)
|
||||
)
|
||||
heartbeat_result = await session.execute(heartbeat_query)
|
||||
heartbeat_rows = heartbeat_result.all()
|
||||
@@ -995,16 +982,12 @@ class RunLedgerStore:
|
||||
avg_heartbeat_age = round(sum(ages) / len(ages), 2)
|
||||
max_heartbeat_age = round(max(ages), 2)
|
||||
|
||||
active_runs_query = (
|
||||
sqlalchemy.select(func.count(AgentRun.id))
|
||||
.where(AgentRun.status.in_(['running', 'claimed']))
|
||||
active_runs_query = sqlalchemy.select(func.count(AgentRun.id)).where(
|
||||
AgentRun.status.in_(['running', 'claimed'])
|
||||
)
|
||||
active_runs_result = await session.execute(active_runs_query)
|
||||
active_runs = active_runs_result.scalar() or 0
|
||||
claimed_runs_query = (
|
||||
sqlalchemy.select(func.count(AgentRun.id))
|
||||
.where(AgentRun.status == 'claimed')
|
||||
)
|
||||
claimed_runs_query = sqlalchemy.select(func.count(AgentRun.id)).where(AgentRun.status == 'claimed')
|
||||
claimed_runs_result = await session.execute(claimed_runs_query)
|
||||
claimed_runs = claimed_runs_result.scalar() or 0
|
||||
|
||||
@@ -1048,23 +1031,10 @@ class RunLedgerStore:
|
||||
AgentRun.runner_id,
|
||||
func.count(AgentRun.id).label('total'),
|
||||
func.sum(
|
||||
sqlalchemy.case(
|
||||
(AgentRun.status.in_(['queued', 'claimed', 'running']), 1),
|
||||
else_=0
|
||||
)
|
||||
sqlalchemy.case((AgentRun.status.in_(['queued', 'claimed', 'running']), 1), else_=0)
|
||||
).label('active'),
|
||||
func.sum(
|
||||
sqlalchemy.case(
|
||||
(AgentRun.status == 'completed', 1),
|
||||
else_=0
|
||||
)
|
||||
).label('completed'),
|
||||
func.sum(
|
||||
sqlalchemy.case(
|
||||
(AgentRun.status.in_(['failed', 'timeout']), 1),
|
||||
else_=0
|
||||
)
|
||||
).label('failed'),
|
||||
func.sum(sqlalchemy.case((AgentRun.status == 'completed', 1), else_=0)).label('completed'),
|
||||
func.sum(sqlalchemy.case((AgentRun.status.in_(['failed', 'timeout']), 1), else_=0)).label('failed'),
|
||||
)
|
||||
.where(
|
||||
AgentRun.created_at >= start_dt,
|
||||
@@ -1087,16 +1057,18 @@ class RunLedgerStore:
|
||||
failed = row.failed or 0
|
||||
success_rate = completed / total if total > 0 else None
|
||||
|
||||
stats.append({
|
||||
'runner_id': runner_id,
|
||||
'runner_label': None, # Would need to join with runner descriptors
|
||||
'plugin_identity': None,
|
||||
'total_runs': total,
|
||||
'active_runs': row.active or 0,
|
||||
'completed_runs': completed,
|
||||
'failed_runs': failed,
|
||||
'success_rate': round(success_rate, 4) if success_rate is not None else None,
|
||||
'avg_duration_seconds': None, # Would need more complex query
|
||||
})
|
||||
stats.append(
|
||||
{
|
||||
'runner_id': runner_id,
|
||||
'runner_label': None, # Would need to join with runner descriptors
|
||||
'plugin_identity': None,
|
||||
'total_runs': total,
|
||||
'active_runs': row.active or 0,
|
||||
'completed_runs': completed,
|
||||
'failed_runs': failed,
|
||||
'success_rate': round(success_rate, 4) if success_rate is not None else None,
|
||||
'avg_duration_seconds': None, # Would need more complex query
|
||||
}
|
||||
)
|
||||
|
||||
return stats
|
||||
|
||||
@@ -18,6 +18,43 @@ from .agent_debug_stream import debug_stream_response
|
||||
@group.group_class('agents', '/api/v1/agents')
|
||||
class AgentsRouterGroup(group.RouterGroup):
|
||||
async def initialize(self) -> None:
|
||||
@self.route(
|
||||
'/<agent_uuid>/runs',
|
||||
methods=['GET'],
|
||||
auth_type=group.AuthType.USER_TOKEN_OR_API_KEY,
|
||||
permission=Permission.RESOURCE_VIEW,
|
||||
)
|
||||
async def processor_runs(agent_uuid: str, request_context: RequestContext):
|
||||
try:
|
||||
cursor = quart.request.args.get('before_id')
|
||||
result = await self.ap.agent_service.get_processor_runs(
|
||||
request_context,
|
||||
agent_uuid,
|
||||
before_id=int(cursor) if cursor else None,
|
||||
)
|
||||
return self.success(data=result)
|
||||
except ValueError as exc:
|
||||
return self.http_status(400, -1, str(exc))
|
||||
|
||||
@self.route(
|
||||
'/<agent_uuid>/runs/<run_id>/events',
|
||||
methods=['GET'],
|
||||
auth_type=group.AuthType.USER_TOKEN_OR_API_KEY,
|
||||
permission=Permission.RESOURCE_VIEW,
|
||||
)
|
||||
async def processor_run_events(agent_uuid: str, run_id: str, request_context: RequestContext):
|
||||
try:
|
||||
cursor = quart.request.args.get('after_sequence')
|
||||
result = await self.ap.agent_service.get_processor_run_events(
|
||||
request_context,
|
||||
agent_uuid,
|
||||
run_id,
|
||||
after_sequence=int(cursor) if cursor else None,
|
||||
)
|
||||
return self.success(data=result)
|
||||
except ValueError as exc:
|
||||
return self.http_status(400, -1, str(exc))
|
||||
|
||||
@self.route(
|
||||
'/<agent_uuid>/debug/stream',
|
||||
methods=['POST'],
|
||||
|
||||
@@ -40,6 +40,7 @@ from .tenant import TenantContext, require_workspace_uuid, scope_statement
|
||||
|
||||
AGENT_KIND_AGENT = 'agent'
|
||||
AGENT_KIND_PIPELINE = 'pipeline'
|
||||
AGENT_KIND_EVENT_PROCESSOR = 'event_processor'
|
||||
PIPELINE_EVENT_PATTERNS = ['message.*']
|
||||
AGENT_DEFAULT_EVENT_PATTERNS = ['*']
|
||||
|
||||
@@ -67,7 +68,19 @@ class AgentService:
|
||||
)
|
||||
except Exception as exc:
|
||||
self.ap.logger.warning(f'Failed to load Agent Host tool catalog: {exc}')
|
||||
event_processors = []
|
||||
registry = getattr(self.ap, 'agent_runner_registry', None)
|
||||
if registry is not None:
|
||||
event_processors = [
|
||||
item.model_dump(mode='json')
|
||||
for item in await registry.list_runners(
|
||||
context,
|
||||
component_kind='EventProcessor',
|
||||
use_cache=False,
|
||||
)
|
||||
]
|
||||
return {
|
||||
'event_processors': event_processors,
|
||||
'runner_config': ai_metadata,
|
||||
'platform_tools': platform_tool_catalog(),
|
||||
'host_tools': host_tools,
|
||||
@@ -82,6 +95,7 @@ class AgentService:
|
||||
'supported_event_patterns': PIPELINE_EVENT_PATTERNS,
|
||||
'message_only': True,
|
||||
},
|
||||
{'name': AGENT_KIND_EVENT_PROCESSOR, 'supported_event_patterns': ['*'], 'message_only': False},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -131,7 +145,7 @@ class AgentService:
|
||||
non-message event envelopes.
|
||||
"""
|
||||
agent = await self.get_agent(context, agent_uuid)
|
||||
if agent is None or agent.get('kind') != AGENT_KIND_AGENT:
|
||||
if agent is None or agent.get('kind') not in {AGENT_KIND_AGENT, AGENT_KIND_EVENT_PROCESSOR}:
|
||||
raise ValueError('Agent not found')
|
||||
|
||||
event_type = str(payload.get('event_type', 'message.received')).strip()
|
||||
@@ -242,8 +256,28 @@ class AgentService:
|
||||
raw_ref=RawEventRef(ref_id=event_id, storage_key=None),
|
||||
data=event_data,
|
||||
)
|
||||
if agent.get('kind') == AGENT_KIND_EVENT_PROCESSOR:
|
||||
from langbot_plugin.api.entities.builtin.platform.events import parse_eba_event
|
||||
|
||||
typed_event = parse_eba_event({**event_data, 'type': event_type})
|
||||
from ....platform.botmgr import RuntimeBot
|
||||
|
||||
event.actor = RuntimeBot._infer_actor_context(typed_event)
|
||||
event.subject = RuntimeBot._infer_subject_context(typed_event)
|
||||
target_type, target_id, target_metadata = RuntimeBot._infer_reply_target(typed_event)
|
||||
if target_id is not None:
|
||||
event.delivery.reply_target = {
|
||||
'target_type': target_type,
|
||||
'target_id': str(target_id),
|
||||
**target_metadata,
|
||||
}
|
||||
event.data = typed_event.model_dump(mode='json', exclude={'source_platform_object', 'legacy_event'})
|
||||
binding = AgentBinding(
|
||||
binding_id=f'debug:{agent_uuid}:{runner_id}',
|
||||
binding_id=(
|
||||
f'event_processor:{agent_uuid}'
|
||||
if agent.get('kind') == AGENT_KIND_EVENT_PROCESSOR
|
||||
else f'debug:{agent_uuid}:{runner_id}'
|
||||
),
|
||||
scope=BindingScope(scope_type='agent', scope_id=agent_uuid),
|
||||
event_types=[event_type],
|
||||
runner_id=runner_id,
|
||||
@@ -263,7 +297,7 @@ class AgentService:
|
||||
enable_interactions=False,
|
||||
),
|
||||
agent_id=agent_uuid,
|
||||
processor_type='agent',
|
||||
processor_type=agent.get('kind', 'agent'),
|
||||
processor_id=agent_uuid,
|
||||
)
|
||||
execution_context = ExecutionContext.from_request(
|
||||
@@ -282,6 +316,7 @@ class AgentService:
|
||||
'tool.call.completed',
|
||||
'run.completed',
|
||||
'run.failed',
|
||||
'processor.log',
|
||||
}:
|
||||
return
|
||||
visible_result = copy.deepcopy(
|
||||
@@ -363,11 +398,17 @@ class AgentService:
|
||||
)
|
||||
return {'uuid': pipeline_uuid, 'kind': AGENT_KIND_PIPELINE}
|
||||
|
||||
if kind != AGENT_KIND_AGENT:
|
||||
if kind not in {AGENT_KIND_AGENT, AGENT_KIND_EVENT_PROCESSOR}:
|
||||
raise ValueError(f'Unsupported agent kind: {kind}')
|
||||
|
||||
config = agent_data['config'] if 'config' in agent_data else await self._get_default_agent_config(context)
|
||||
config, runner_id, _ = RunnerConfigResolver.resolve_agent_runner_config(config)
|
||||
if kind == AGENT_KIND_EVENT_PROCESSOR:
|
||||
config, runner_id, patterns = await self._prepare_event_processor(context, agent_data)
|
||||
else:
|
||||
config = agent_data['config'] if 'config' in agent_data else await self._get_default_agent_config(context)
|
||||
config, runner_id, _ = RunnerConfigResolver.resolve_agent_runner_config(config)
|
||||
if (runner_id or '').startswith('event_processor:'):
|
||||
raise ValueError('EventProcessor components require an event processor instance')
|
||||
patterns = agent_data.get('supported_event_patterns', AGENT_DEFAULT_EVENT_PATTERNS)
|
||||
new_uuid = str(uuid.uuid4())
|
||||
values = {
|
||||
'workspace_uuid': workspace_uuid,
|
||||
@@ -375,17 +416,13 @@ class AgentService:
|
||||
'name': agent_data.get('name') or 'New Agent',
|
||||
'description': agent_data.get('description') or '',
|
||||
'emoji': agent_data.get('emoji') or '🤖',
|
||||
'kind': AGENT_KIND_AGENT,
|
||||
'kind': kind,
|
||||
'component_ref': runner_id,
|
||||
'config': config,
|
||||
'supported_event_patterns': (
|
||||
agent_data['supported_event_patterns']
|
||||
if 'supported_event_patterns' in agent_data
|
||||
else AGENT_DEFAULT_EVENT_PATTERNS
|
||||
),
|
||||
'supported_event_patterns': patterns,
|
||||
}
|
||||
await self.ap.persistence_mgr.execute_async(sqlalchemy.insert(persistence_agent.Agent).values(**values))
|
||||
return {'uuid': new_uuid, 'kind': AGENT_KIND_AGENT}
|
||||
return {'uuid': new_uuid, 'kind': kind}
|
||||
|
||||
async def update_agent(self, context: TenantContext, agent_uuid: str, agent_data: dict) -> None:
|
||||
existing_agent = await self._get_agent_row(context, agent_uuid)
|
||||
@@ -401,12 +438,19 @@ class AgentService:
|
||||
for field in ('name', 'description', 'emoji', 'config', 'supported_event_patterns')
|
||||
if field in agent_data
|
||||
}
|
||||
if existing_agent.kind == AGENT_KIND_EVENT_PROCESSOR and any(
|
||||
field in agent_data for field in ('config', 'component_ref', 'parameters', 'supported_event_patterns')
|
||||
):
|
||||
config, runner_id, patterns = await self._prepare_event_processor(context, agent_data, existing_agent)
|
||||
update_data.update(config=config, component_ref=runner_id, supported_event_patterns=patterns)
|
||||
if 'config' in update_data:
|
||||
config, runner_id, _ = RunnerConfigResolver.resolve_agent_runner_config(update_data['config'])
|
||||
update_data['config'] = config
|
||||
else:
|
||||
_, runner_id, _ = RunnerConfigResolver.resolve_agent_runner_config(existing_agent.config)
|
||||
update_data['component_ref'] = runner_id
|
||||
if existing_agent.kind == AGENT_KIND_AGENT and (runner_id or '').startswith('event_processor:'):
|
||||
raise ValueError('EventProcessor components require an event processor instance')
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
scope_statement(
|
||||
sqlalchemy.update(persistence_agent.Agent)
|
||||
@@ -438,6 +482,78 @@ class AgentService:
|
||||
raise ValueError(f'Agent {agent_uuid} not found')
|
||||
await self.ap.pipeline_service.delete_pipeline(context, agent_uuid)
|
||||
|
||||
async def _prepare_event_processor(self, context, data, existing=None):
|
||||
"""Resolve an installed component and keep its capability declaration authoritative."""
|
||||
config = copy.deepcopy(data.get('config', existing.config if existing is not None else {}))
|
||||
if not isinstance(config, dict):
|
||||
raise ValueError('Processor configuration must be an object')
|
||||
component_ref = data.get('component_ref') or (existing.component_ref if existing is not None else None)
|
||||
if not isinstance(component_ref, str) or not component_ref.startswith('event_processor:'):
|
||||
raise ValueError('Select an installed EventProcessor component')
|
||||
try:
|
||||
descriptor = await self.ap.agent_runner_registry.get(context, component_ref)
|
||||
except Exception as exc:
|
||||
from ....agent.runner.errors import RunnerNotFoundError
|
||||
|
||||
if isinstance(exc, RunnerNotFoundError):
|
||||
raise ValueError('EventProcessor component is unavailable') from exc
|
||||
raise
|
||||
if descriptor.component_kind != 'EventProcessor' or not descriptor.supported_event_patterns:
|
||||
raise ValueError('The component does not declare supported EBA events')
|
||||
config['runner'] = {'id': component_ref}
|
||||
parameters = data.get('parameters')
|
||||
if parameters is None:
|
||||
runner_config = config.get('runner_config', {})
|
||||
if not isinstance(runner_config, dict):
|
||||
raise ValueError('Runner configuration must be an object')
|
||||
parameters = runner_config.get(component_ref)
|
||||
if parameters is None:
|
||||
parameters = self.ap.pipeline_service._get_default_values_from_schema(descriptor.config_schema)
|
||||
if not isinstance(parameters, dict):
|
||||
raise ValueError('Processor parameters must be an object')
|
||||
for field in descriptor.config_schema:
|
||||
if field.get('required') and parameters.get(field['name']) in (None, ''):
|
||||
raise ValueError(f'Required processor parameter: {field["name"]}')
|
||||
config['runner_config'] = {component_ref: parameters}
|
||||
return config, component_ref, descriptor.supported_event_patterns
|
||||
|
||||
async def get_processor_runs(self, context, processor_id, *, before_id=None):
|
||||
"""Read only this Workspace's explicitly created processor instance."""
|
||||
from ....agent.runner.run_ledger_store import RunLedgerStore
|
||||
|
||||
processor = await self.get_agent(context, processor_id)
|
||||
if processor is None or processor.get('kind') != AGENT_KIND_EVENT_PROCESSOR:
|
||||
raise ValueError('Event processor not found')
|
||||
store = RunLedgerStore(self.ap.persistence_mgr.get_db_engine())
|
||||
items, cursor, has_more, total = await store.list_runs(
|
||||
workspace_id=require_workspace_uuid(context),
|
||||
binding_id=f'event_processor:{processor_id}',
|
||||
before_id=before_id,
|
||||
)
|
||||
return {'items': items, 'next_cursor': cursor, 'has_more': has_more, 'total': total}
|
||||
|
||||
async def get_processor_run_events(self, context, processor_id, run_id, *, after_sequence=None):
|
||||
"""Authorize the parent run before exposing any trace events."""
|
||||
from ....agent.runner.run_ledger_store import RunLedgerStore
|
||||
|
||||
processor = await self.get_agent(context, processor_id)
|
||||
if processor is None or processor.get('kind') != AGENT_KIND_EVENT_PROCESSOR:
|
||||
raise ValueError('Event processor not found')
|
||||
store = RunLedgerStore(self.ap.persistence_mgr.get_db_engine())
|
||||
run = await store.get_run(run_id)
|
||||
if (
|
||||
run is None
|
||||
or run.get('workspace_id') != require_workspace_uuid(context)
|
||||
or run.get('binding_id') != f'event_processor:{processor_id}'
|
||||
):
|
||||
raise ValueError('Processor run not found')
|
||||
items, next_cursor, _, has_more = await store.page_run_events(
|
||||
run_id=run_id,
|
||||
after_sequence=after_sequence,
|
||||
limit=100,
|
||||
)
|
||||
return {'run': run, 'items': items, 'next_cursor': next_cursor, 'has_more': has_more}
|
||||
|
||||
async def _get_agent_rows(self, context: TenantContext) -> list[persistence_agent.Agent]:
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
scope_statement(
|
||||
@@ -490,7 +606,7 @@ class AgentService:
|
||||
include_config: bool = False,
|
||||
) -> dict[str, typing.Any]:
|
||||
item = self.ap.persistence_mgr.serialize_model(persistence_agent.Agent, agent)
|
||||
item['kind'] = AGENT_KIND_AGENT
|
||||
item['kind'] = item.get('kind') or AGENT_KIND_AGENT
|
||||
supported_event_patterns = item.get('supported_event_patterns')
|
||||
item['capability'] = {
|
||||
'supported_event_patterns': (
|
||||
|
||||
@@ -149,7 +149,7 @@ class BotService:
|
||||
return target_kind
|
||||
if target_type == 'discard':
|
||||
return 'discard'
|
||||
if target_type in {'agent', 'pipeline'}:
|
||||
if target_type in {'agent', 'pipeline', 'event_processor'}:
|
||||
return str(target_type)
|
||||
return None
|
||||
|
||||
@@ -416,9 +416,9 @@ class BotService:
|
||||
diagnostic_steps=diagnostic_steps,
|
||||
)
|
||||
|
||||
if target_type == 'agent':
|
||||
if target_type in {'agent', 'event_processor'}:
|
||||
agent = await self._get_agent_entity(tenant_context, target_uuid)
|
||||
if agent is None or getattr(agent, 'kind', 'agent') != 'agent':
|
||||
if agent is None or getattr(agent, 'kind', 'agent') != target_type:
|
||||
return self._diagnostic_result(
|
||||
matched=False,
|
||||
binding=selected_binding,
|
||||
@@ -514,7 +514,7 @@ class BotService:
|
||||
)
|
||||
if result.first() is None:
|
||||
raise ValueError('Pipeline not found')
|
||||
elif target_type == 'agent':
|
||||
elif target_type in {'agent', 'event_processor'}:
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
scope_statement(
|
||||
sqlalchemy.select(persistence_agent.Agent).where(persistence_agent.Agent.uuid == target_uuid),
|
||||
@@ -523,8 +523,8 @@ class BotService:
|
||||
)
|
||||
)
|
||||
agent = result.first()
|
||||
if agent is None:
|
||||
raise ValueError('Agent not found')
|
||||
if agent is None or agent.kind != target_type:
|
||||
raise ValueError('Processor not found')
|
||||
if not self._agent_supports_event_pattern(agent.supported_event_patterns, event_pattern):
|
||||
raise ValueError('Agent does not support this event pattern')
|
||||
elif target_type == 'discard':
|
||||
|
||||
@@ -175,44 +175,72 @@ class LangBotMCPServer:
|
||||
return _dump({'ok': True})
|
||||
|
||||
# ----- Processors ---------------------------------------------- #
|
||||
@mcp.tool(description='List product-level processors, including Agents and Pipelines.')
|
||||
@mcp.tool(description='List product-level processors, including Agents, Pipelines and Event processors.')
|
||||
async def list_processors() -> str:
|
||||
context = _authorized(Permission.RESOURCE_VIEW)
|
||||
return _dump(await ap.agent_service.get_agents(context))
|
||||
|
||||
@mcp.tool(description='Get an Agent or Pipeline processor by UUID.')
|
||||
@mcp.tool(description='Get an Agent, Pipeline or Event processor by UUID.')
|
||||
async def get_processor(processor_uuid: str) -> str:
|
||||
context = _authorized(Permission.RESOURCE_VIEW)
|
||||
return _dump(await ap.agent_service.get_agent(context, processor_uuid))
|
||||
|
||||
@mcp.tool(
|
||||
description=(
|
||||
'Create an Agent or Pipeline processor. Set `processor_data.kind` to '
|
||||
'`agent` or `pipeline`. Returns the new UUID and kind.'
|
||||
'Create an Agent, Pipeline or Event processor. Set `processor_data.kind` to '
|
||||
'`agent`, `pipeline` or `event_processor`. Event processors require an installed component_ref '
|
||||
'from get_processor_metadata; optional parameters configure the instance. Returns UUID and kind.'
|
||||
)
|
||||
)
|
||||
async def create_processor(processor_data: dict) -> str:
|
||||
context = _authorized(Permission.RESOURCE_MANAGE)
|
||||
return _dump(await ap.agent_service.create_agent(context, processor_data))
|
||||
|
||||
@mcp.tool(description='Update an Agent or Pipeline processor by UUID.')
|
||||
@mcp.tool(description='Update an Agent, Pipeline or Event processor by UUID.')
|
||||
async def update_processor(processor_uuid: str, processor_data: dict) -> str:
|
||||
context = _authorized(Permission.RESOURCE_MANAGE)
|
||||
await ap.agent_service.update_agent(context, processor_uuid, processor_data)
|
||||
return _dump({'ok': True})
|
||||
|
||||
@mcp.tool(description='Delete an Agent or Pipeline processor by UUID.')
|
||||
@mcp.tool(description='Delete an Agent, Pipeline or Event processor by UUID.')
|
||||
async def delete_processor(processor_uuid: str) -> str:
|
||||
context = _authorized(Permission.RESOURCE_MANAGE)
|
||||
await ap.agent_service.delete_agent(context, processor_uuid)
|
||||
return _dump({'ok': True})
|
||||
|
||||
@mcp.tool(description='Get processor kinds and installed EventProcessor components with configuration schemas.')
|
||||
async def get_processor_metadata() -> str:
|
||||
context = _authorized(Permission.RESOURCE_VIEW)
|
||||
return _dump(await ap.agent_service.get_agent_metadata(context))
|
||||
|
||||
@mcp.tool(description='List one Event processor instance run history; use before_id to page older runs.')
|
||||
async def list_processor_runs(processor_uuid: str, before_id: int | None = None) -> str:
|
||||
context = _authorized(Permission.RESOURCE_VIEW)
|
||||
return _dump(await ap.agent_service.get_processor_runs(context, processor_uuid, before_id=before_id))
|
||||
|
||||
@mcp.tool(description='Read logs and action results for an Event processor run; page using after_sequence.')
|
||||
async def get_processor_run_events(
|
||||
processor_uuid: str,
|
||||
run_id: str,
|
||||
after_sequence: int | None = None,
|
||||
) -> str:
|
||||
context = _authorized(Permission.RESOURCE_VIEW)
|
||||
return _dump(
|
||||
await ap.agent_service.get_processor_run_events(
|
||||
context,
|
||||
processor_uuid,
|
||||
run_id,
|
||||
after_sequence=after_sequence,
|
||||
)
|
||||
)
|
||||
|
||||
# ----- Models -------------------------------------------------- #
|
||||
@mcp.tool(
|
||||
description=(
|
||||
'Run a synthetic event against an Agent processor without platform delivery. '
|
||||
'Run a synthetic event against an Agent or Event processor without platform delivery. '
|
||||
'Returns final text and execution_events containing reported messages/thinking and tool calls. '
|
||||
'Platform tools use mock adapters; other tools execute normally. '
|
||||
'For Event processors, data contains the complete typed EBA event fields. '
|
||||
'Requires runtime.operate; payload accepts event_type, text, data, conversation_id, actor, subject and '
|
||||
'mock (errors/results keyed by platform tool name; unsupported_apis lists unavailable platform APIs).'
|
||||
)
|
||||
|
||||
@@ -62,12 +62,25 @@ class AiocqhttpEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
||||
chat_id = getattr(event, 'group_id', '')
|
||||
group = AiocqhttpEventConverter.group_from_event(event)
|
||||
|
||||
sender = AiocqhttpEventConverter.user_from_sender(event)
|
||||
sender_data = getattr(event, 'sender', {}) or {}
|
||||
role = sender_data.get('role', 'member')
|
||||
membership = None
|
||||
if group is not None:
|
||||
membership = platform_entities.UserGroupMember(
|
||||
user=sender,
|
||||
group_id=group.id,
|
||||
role=role if role in {'owner', 'admin', 'member'} else 'member',
|
||||
display_name=sender_data.get('card') or sender.nickname,
|
||||
title=sender_data.get('title'),
|
||||
)
|
||||
return platform_events.MessageReceivedEvent(
|
||||
type='message.received',
|
||||
adapter_name='aiocqhttp',
|
||||
message_id=getattr(event, 'message_id', ''),
|
||||
message_chain=message_chain,
|
||||
sender=AiocqhttpEventConverter.user_from_sender(event),
|
||||
sender=sender,
|
||||
sender_member=membership,
|
||||
chat_type=chat_type,
|
||||
chat_id=chat_id,
|
||||
group=group,
|
||||
|
||||
@@ -630,6 +630,7 @@ class RuntimeBot:
|
||||
name=event.group.name,
|
||||
)
|
||||
return platform_events.MessageReceivedEvent(
|
||||
legacy_event=event,
|
||||
message_id=self._extract_message_id(event.message_chain),
|
||||
message_chain=event.message_chain,
|
||||
sender=platform_entities.User(
|
||||
@@ -646,6 +647,7 @@ class RuntimeBot:
|
||||
)
|
||||
|
||||
return platform_events.MessageReceivedEvent(
|
||||
legacy_event=event,
|
||||
message_id=self._extract_message_id(event.message_chain),
|
||||
message_chain=event.message_chain,
|
||||
sender=platform_entities.User(
|
||||
@@ -805,7 +807,11 @@ class RuntimeBot:
|
||||
return None
|
||||
|
||||
return AgentBinding(
|
||||
binding_id=f'bot:{bot_uuid}:{event_binding.get("id") or uuid.uuid4()}',
|
||||
binding_id=(
|
||||
f'event_processor:{agent["uuid"]}'
|
||||
if agent.get('kind') == 'event_processor'
|
||||
else f'bot:{bot_uuid}:{event_binding.get("id") or uuid.uuid4()}'
|
||||
),
|
||||
scope=BindingScope(scope_type='bot', scope_id=bot_uuid),
|
||||
event_types=[event_type],
|
||||
runner_id=runner_id,
|
||||
@@ -820,10 +826,10 @@ class RuntimeBot:
|
||||
delivery_policy=DeliveryPolicy(
|
||||
enable_streaming=False,
|
||||
enable_reply=True,
|
||||
enable_interactions=True,
|
||||
enable_interactions=agent.get('kind') != 'event_processor',
|
||||
),
|
||||
agent_id=agent.get('uuid'),
|
||||
processor_type='agent',
|
||||
processor_type=agent.get('kind', 'agent'),
|
||||
processor_id=agent.get('uuid'),
|
||||
)
|
||||
|
||||
@@ -898,14 +904,8 @@ class RuntimeBot:
|
||||
await self._handle_interaction_submission(event, adapter)
|
||||
return
|
||||
|
||||
plugin_event = self._eba_event_to_plugin_event(event)
|
||||
|
||||
if plugin_event is not None:
|
||||
try:
|
||||
await self.ap.plugin_connector.emit_event(plugin_event)
|
||||
except Exception:
|
||||
await self.logger.error(f'Failed to dispatch platform event to plugins: {traceback.format_exc()}')
|
||||
|
||||
# Legacy listeners run inside Pipeline stages. EBA handlers require an
|
||||
# explicitly created and routed EventProcessor instance.
|
||||
await self._dispatch_eba_event_to_processor(event, adapter)
|
||||
|
||||
async def _dispatch_eba_event_to_processor(
|
||||
@@ -983,7 +983,7 @@ class RuntimeBot:
|
||||
target_uuid=event_binding.get('target_uuid'),
|
||||
text=f'EBA event {event_type} delivered to Pipeline {event_binding.get("target_uuid") or ""}'.strip(),
|
||||
)
|
||||
if target_type != 'agent':
|
||||
if target_type not in {'agent', 'event_processor'}:
|
||||
return await self._record_event_route_trace(
|
||||
event_type=event_type,
|
||||
status='failed',
|
||||
@@ -998,7 +998,7 @@ class RuntimeBot:
|
||||
|
||||
target_uuid = event_binding.get('target_uuid')
|
||||
agent = await self.ap.agent_service.get_agent(self.execution_context, target_uuid)
|
||||
if not agent or agent.get('kind') != 'agent':
|
||||
if not agent or agent.get('kind') != target_type:
|
||||
return await self._record_event_route_trace(
|
||||
event_type=event_type,
|
||||
status='failed',
|
||||
@@ -1050,6 +1050,8 @@ class RuntimeBot:
|
||||
)
|
||||
|
||||
envelope = self._eba_event_to_agent_envelope(event, adapter)
|
||||
if target_type == 'event_processor':
|
||||
envelope.data = event.model_dump(mode='json', exclude={'source_platform_object', 'legacy_event'})
|
||||
outputs: list[provider_message.Message | provider_message.MessageChunk] = []
|
||||
try:
|
||||
async for output in self.ap.agent_run_orchestrator.run(
|
||||
|
||||
Reference in New Issue
Block a user