From ea3e32c904a759007680ab018883b297964b2c5e Mon Sep 17 00:00:00 2001 From: RockChinQ Date: Tue, 8 Sep 2026 02:30:59 +0800 Subject: [PATCH] feat(processors): streamline event debugging and run inspection --- .../event-based-agents/09-event-processors.md | 24 +- pyproject.toml | 2 +- skills/skills/langbot-mcp-ops/SKILL.md | 11 +- .../pkg/agent/runner/run_ledger_store.py | 3 + src/langbot/pkg/api/http/service/agent.py | 3 + src/langbot/pkg/api/mcp/server.py | 10 +- .../unit_tests/agent/test_run_ledger_store.py | 17 + .../api/service/test_agent_service.py | 42 ++ uv.lock | 4 +- .../app/home/agents/AgentDetailContent.tsx | 2 + .../agents/EventProcessorDetailContent.tsx | 467 ++++++++++-------- .../agents/components/AgentCreateContent.tsx | 59 +-- .../agents/components/AgentDebugPanel.tsx | 146 ++++-- .../components/AgentEventDataEditor.tsx | 33 +- .../components/EventProcessorSettings.tsx | 213 ++++++-- .../agents/components/EventProcessorTrace.tsx | 86 ++++ .../agents/components/ProcessorRunList.tsx | 83 ++++ .../agents/components/debug-event-data.ts | 173 ++++++- .../agents/components/processor-run-timing.ts | 23 + .../components/home-sidebar/HomeSidebar.tsx | 2 +- .../ProcessorDetailWorkbench.tsx | 58 ++- web/src/app/infra/entities/api/index.ts | 5 + web/src/i18n/locales/en-US.ts | 13 + web/src/i18n/locales/es-ES.ts | 14 + web/src/i18n/locales/ja-JP.ts | 13 + web/src/i18n/locales/ru-RU.ts | 13 + web/src/i18n/locales/th-TH.ts | 13 + web/src/i18n/locales/vi-VN.ts | 13 + web/src/i18n/locales/zh-Hans.ts | 12 + web/src/i18n/locales/zh-Hant.ts | 12 + web/tests/e2e/agent-event-data.spec.ts | 2 +- web/tests/e2e/event-processor.spec.ts | 245 +++++++-- .../unit/agent-debug-event-data.test.mjs | 61 +++ web/tests/unit/processor-run-timing.test.mjs | 53 ++ 34 files changed, 1464 insertions(+), 466 deletions(-) create mode 100644 web/src/app/home/agents/components/EventProcessorTrace.tsx create mode 100644 web/src/app/home/agents/components/ProcessorRunList.tsx create mode 100644 web/src/app/home/agents/components/processor-run-timing.ts create mode 100644 web/tests/unit/processor-run-timing.test.mjs diff --git a/docs/event-based-agents/09-event-processors.md b/docs/event-based-agents/09-event-processors.md index 5c4155f98..430ed899b 100644 --- a/docs/event-based-agents/09-event-processors.md +++ b/docs/event-based-agents/09-event-processors.md @@ -99,7 +99,7 @@ The activation sequence is explicit: 1. Install a plugin containing an EventProcessor component. 2. Create an Event processor in the Processors area. -3. Select its plugin component and enter any component-defined configuration. +3. Open its detail page, select a plugin component, and save its configuration. 4. Bind a Bot event to that processor instance in the existing event routing UI. Installation and processor creation alone do not subscribe to Bot events. @@ -112,20 +112,26 @@ There is no automatic EBA broadcast to installed EventListeners. Keep Pipeline h plugins must explicitly adopt the new component and be bound by the user; do not create subscriptions during migration. -Validate component availability, event compatibility, Workspace ownership, and -instance identity at creation/update and again at invocation. A disabled or +An unconfigured instance has no supported events and cannot execute. Validate +component availability, event compatibility, Workspace ownership, and instance +identity when configuring the instance and again at invocation. A disabled or unavailable plugin leaves the instance visible with an actionable unavailable status. It must not silently fall back to Agent or Pipeline. ## Compact UI -Creation adds a third type next to Agent and Pipeline, followed by a component -selector and basic instance information. Show configuration fields only when the -component declares them. If no component is installed, show a relevant plugin -installation entry point; installing still does not create a binding. +Creation adds a third type next to Agent and Pipeline and asks only for basic +instance information. Select the plugin component in the detail-page header. +Keep component-defined configuration in the adjacent Plugin settings popover. +If no component is installed, show a relevant plugin installation entry point; +installing still does not create a binding. -The detail page prioritizes a single run list. Selecting a run shows a chronological -trace of the incoming event, handler logs, outgoing actions/messages, and outcome. +The detail page shows event debugging on the left and logs on the right without +view-switching tabs. A compact run list shows event type, time, status and known +processing duration. Selecting a row shows that run's identity, input, logs, +actions and outcome below. There is no shared timeline between unrelated runs. +The additive `created_at_ms`, `started_at_ms`, and `finished_at_ms` fields retain +Host lifecycle precision for elapsed-time display. Keep payloads and error details collapsed until expanded. Distinguish attempted delivery from confirmed delivery and display the actual destination. diff --git a/pyproject.toml b/pyproject.toml index 09bd38e7f..9e7353795 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -232,4 +232,4 @@ line-ending = "auto" [tool.uv.sources] # Development contract: update to the matching SDK release before publishing. -langbot-plugin = { git = "https://github.com/langbot-app/langbot-plugin-sdk", rev = "23011398160cedcd4ef090054692bc2a4b34ee9f" } +langbot-plugin = { git = "https://github.com/langbot-app/langbot-plugin-sdk", rev = "f82b3ce935f9a33afee389fb39fe8dc29a45b615" } diff --git a/skills/skills/langbot-mcp-ops/SKILL.md b/skills/skills/langbot-mcp-ops/SKILL.md index 22cef1800..314e8c9f2 100644 --- a/skills/skills/langbot-mcp-ops/SKILL.md +++ b/skills/skills/langbot-mcp-ops/SKILL.md @@ -116,9 +116,14 @@ already have a default pipeline. ## Event processors -Install the plugin, discover its component with `get_processor_metadata`, then -create a processor with `kind: "event_processor"`, `component_ref` and optional -`parameters`. Bind bot events to this instance with `target_type: "event_processor"` +Create a processor with `kind: "event_processor"` and basic information. Without +a component it supports no events. Discover installed components with +`get_processor_metadata`, then use `update_processor` with `component_ref` and +optional `parameters`. API callers may also supply these when creating an instance. Bind bot events to this instance with `target_type: "event_processor"` and `target_id` equal to its UUID. Installation alone never activates a handler. `debug_agent` accepts the complete typed EBA event in `payload.data` for this kind. Legacy EventListener plugins remain in the Pipeline lifecycle. + +`list_processor_runs` includes `created_at_ms`, `started_at_ms`, and +`finished_at_ms`: Host lifecycle times in epoch milliseconds. Use the start and finish times for elapsed processing time; select a run and call `get_processor_run_events` for its +logs and action results. These times are not internal plugin profiling data. diff --git a/src/langbot/pkg/agent/runner/run_ledger_store.py b/src/langbot/pkg/agent/runner/run_ledger_store.py index 41f835a01..76e9dc40a 100644 --- a/src/langbot/pkg/agent/runner/run_ledger_store.py +++ b/src/langbot/pkg/agent/runner/run_ledger_store.py @@ -776,6 +776,9 @@ class RunLedgerStore: 'dispatch_attempts': row.dispatch_attempts, 'last_claimed_at': _datetime_to_epoch(row.last_claimed_at), 'created_at': _datetime_to_epoch(row.created_at), + 'created_at_ms': round(_as_utc(row.created_at).timestamp() * 1000) if row.created_at else None, + 'started_at_ms': round(_as_utc(row.started_at).timestamp() * 1000) if row.started_at else None, + 'finished_at_ms': round(_as_utc(row.finished_at).timestamp() * 1000) if row.finished_at else None, 'started_at': _datetime_to_epoch(row.started_at), 'finished_at': _datetime_to_epoch(row.finished_at), 'updated_at': _datetime_to_epoch(row.updated_at), diff --git a/src/langbot/pkg/api/http/service/agent.py b/src/langbot/pkg/api/http/service/agent.py index c2a0b3172..51b8db6eb 100644 --- a/src/langbot/pkg/api/http/service/agent.py +++ b/src/langbot/pkg/api/http/service/agent.py @@ -488,6 +488,9 @@ class AgentService: 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 component_ref is None and not config and not data.get('parameters'): + # An unconfigured instance cannot subscribe to or execute any events. + return {}, None, [] if not isinstance(component_ref, str) or not component_ref.startswith('event_processor:'): raise ValueError('Select an installed EventProcessor component') try: diff --git a/src/langbot/pkg/api/mcp/server.py b/src/langbot/pkg/api/mcp/server.py index 819e9ac06..06765db6e 100644 --- a/src/langbot/pkg/api/mcp/server.py +++ b/src/langbot/pkg/api/mcp/server.py @@ -188,8 +188,9 @@ class LangBotMCPServer: @mcp.tool( description=( '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.' + '`agent`, `pipeline` or `event_processor`. Event processors may be created without a component; ' + 'then use update_processor with an installed component_ref from get_processor_metadata and optional ' + 'parameters. Unconfigured instances support no events. Returns UUID and kind.' ) ) async def create_processor(processor_data: dict) -> str: @@ -213,7 +214,10 @@ class LangBotMCPServer: 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.') + @mcp.tool( + description='List one Event processor instance run history; use before_id to page older runs. ' + 'created_at_ms, started_at_ms and finished_at_ms are Host lifecycle times in epoch milliseconds.' + ) 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)) diff --git a/tests/unit_tests/agent/test_run_ledger_store.py b/tests/unit_tests/agent/test_run_ledger_store.py index 0a747f7e0..64684fc12 100644 --- a/tests/unit_tests/agent/test_run_ledger_store.py +++ b/tests/unit_tests/agent/test_run_ledger_store.py @@ -455,3 +455,20 @@ async def test_processor_instance_history_filters_count_and_pages(store): ) assert (total, more) == (2, False) assert second[0]['run_id'] == 'run-0' + + +@pytest.mark.asyncio +async def test_run_lifecycle_retains_milliseconds(store, monkeypatch): + started = datetime.datetime(2026, 9, 8, 0, 0, 0, 123000, tzinfo=UTC) + monkeypatch.setattr('langbot.pkg.agent.runner.run_ledger_store._utc_now', lambda: started) + run = await store.create_run( + run_id='run-ms', event_id='evt-ms', binding_id='binding-ms', runner_id='runner-ms', status='running' + ) + assert run['created_at_ms'] == round(started.timestamp() * 1000) + assert run['started_at_ms'] == run['created_at_ms'] + assert run['finished_at_ms'] is None + finished = started + datetime.timedelta(milliseconds=275) + monkeypatch.setattr('langbot.pkg.agent.runner.run_ledger_store._utc_now', lambda: finished) + await store.finalize_run(run_id='run-ms', status='completed') + saved = await store.get_run('run-ms') + assert saved['finished_at_ms'] - saved['started_at_ms'] == 275 diff --git a/tests/unit_tests/api/service/test_agent_service.py b/tests/unit_tests/api/service/test_agent_service.py index e234ab414..31eea9bee 100644 --- a/tests/unit_tests/api/service/test_agent_service.py +++ b/tests/unit_tests/api/service/test_agent_service.py @@ -770,6 +770,20 @@ class TestAgentServiceCreateUpdateDelete: ) +async def test_event_processor_can_be_created_before_selecting_a_plugin(): + app = _make_app() + service = AgentService(app) + result = await service.create_agent( + WORKSPACE_UUID, + {'kind': 'event_processor', 'name': 'Unconfigured', 'supported_event_patterns': ['*']}, + ) + values = _compiled_params(app.persistence_mgr.execute_async.call_args.args[0]) + assert result['kind'] == 'event_processor' + assert values['component_ref'] is None + assert values['supported_event_patterns'] == [] + assert values['config'] == {} + + async def test_event_processor_creation_uses_installed_component_scope(): app = _make_app() ref = 'event_processor:test/welcome/default' @@ -797,6 +811,34 @@ async def test_event_processor_creation_uses_installed_component_scope(): assert values['config']['runner_config'][ref] == {'greeting': 'Hi'} +async def test_unconfigured_event_processor_can_select_a_plugin_after_creation(): + app = _make_app() + row = _agent_row(config={}) + row.kind = 'event_processor' + row.component_ref = None + ref = 'event_processor:test/welcome/default' + app.agent_runner_registry = SimpleNamespace( + get=AsyncMock( + return_value=SimpleNamespace( + component_kind='EventProcessor', + supported_event_patterns=['group.member_joined'], + config_schema=[{'name': 'greeting', 'required': True}], + ) + ) + ) + service = AgentService(app) + service._get_agent_row = AsyncMock(return_value=row) + await service.update_agent( + WORKSPACE_UUID, + row.uuid, + {'component_ref': ref, 'parameters': {'greeting': 'Hi'}, 'supported_event_patterns': ['*']}, + ) + values = _compiled_update_values(app.persistence_mgr.execute_async.call_args.args[0]) + assert values['component_ref'] == ref + assert values['supported_event_patterns'] == ['group.member_joined'] + assert values['config']['runner_config'][ref] == {'greeting': 'Hi'} + + async def test_event_processor_rejects_invalid_component_and_missing_parameters(): app = _make_app() service = AgentService(app) diff --git a/uv.lock b/uv.lock index c93fba292..0a4bdf08b 100644 --- a/uv.lock +++ b/uv.lock @@ -2119,7 +2119,7 @@ requires-dist = [ { name = "ebooklib", specifier = ">=0.18" }, { name = "gewechat-client", specifier = ">=0.1.5" }, { name = "html2text", specifier = ">=2024.2.26" }, - { name = "langbot-plugin", git = "https://github.com/langbot-app/langbot-plugin-sdk?rev=23011398160cedcd4ef090054692bc2a4b34ee9f" }, + { name = "langbot-plugin", git = "https://github.com/langbot-app/langbot-plugin-sdk?rev=f82b3ce935f9a33afee389fb39fe8dc29a45b615" }, { name = "langchain", specifier = ">=1.3.9" }, { name = "langchain-core", specifier = ">=1.3.3" }, { name = "langchain-text-splitters", specifier = ">=1.1.2" }, @@ -2186,7 +2186,7 @@ dev = [ [[package]] name = "langbot-plugin" version = "0.5.5" -source = { git = "https://github.com/langbot-app/langbot-plugin-sdk?rev=23011398160cedcd4ef090054692bc2a4b34ee9f#23011398160cedcd4ef090054692bc2a4b34ee9f" } +source = { git = "https://github.com/langbot-app/langbot-plugin-sdk?rev=f82b3ce935f9a33afee389fb39fe8dc29a45b615#f82b3ce935f9a33afee389fb39fe8dc29a45b615" } dependencies = [ { name = "aiofiles" }, { name = "aiohttp" }, diff --git a/web/src/app/home/agents/AgentDetailContent.tsx b/web/src/app/home/agents/AgentDetailContent.tsx index b27508687..16df5a8ff 100644 --- a/web/src/app/home/agents/AgentDetailContent.tsx +++ b/web/src/app/home/agents/AgentDetailContent.tsx @@ -174,6 +174,8 @@ export default function AgentDetailContent({ id }: { id: string }) { id={id} agent={agent} canManage={canManage} + canOperate={canOperate} + availableEventTypes={availableEventTypes} onDelete={() => setDeleteConfirmOpen(true)} onEdit={() => setBasicInfoOpen(true)} onSaved={() => { diff --git a/web/src/app/home/agents/EventProcessorDetailContent.tsx b/web/src/app/home/agents/EventProcessorDetailContent.tsx index c66f19a27..21d55d686 100644 --- a/web/src/app/home/agents/EventProcessorDetailContent.tsx +++ b/web/src/app/home/agents/EventProcessorDetailContent.tsx @@ -1,7 +1,9 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; -import { FileCode2, RefreshCw, Settings2, Trash2, Pencil } from 'lucide-react'; +import { eventPatternLabel } from '@/app/home/components/event-patterns/event-pattern-groups'; +import { RefreshCw, Trash2, ScrollText } from 'lucide-react'; +import isEqual from 'lodash/isEqual'; import { toast } from 'sonner'; import type { Agent, @@ -14,12 +16,23 @@ import { httpClient } from '@/app/infra/http/HttpClient'; import { Button } from '@/components/ui/button'; import { extractI18nObject } from '@/i18n/I18nProvider'; import { Badge } from '@/components/ui/badge'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import ProcessorDetailWorkbench from '@/app/home/components/processor-detail/ProcessorDetailWorkbench'; +import EntityTitleEditButton from '@/app/home/components/entity-basic-info/EntityTitleEditButton'; +import AgentDebugPanel from './components/AgentDebugPanel'; +import EventProcessorTrace, { + ProcessorPayload, +} from './components/EventProcessorTrace'; +import ProcessorRunList from './components/ProcessorRunList'; import EventProcessorSettings from './components/EventProcessorSettings'; export default function EventProcessorDetailContent({ agent, id, canManage, + canOperate, + availableEventTypes, onDelete, onEdit, onSaved, @@ -27,6 +40,8 @@ export default function EventProcessorDetailContent({ agent: Agent; id: string; canManage: boolean; + canOperate: boolean; + availableEventTypes: string[]; onDelete: () => void; onEdit: () => void; onSaved: () => void; @@ -46,6 +61,13 @@ export default function EventProcessorDetailContent({ > )[agent.component_ref ?? ''] ?? {}; const [parameters, setParameters] = useState(initialParameters); + const [savedConfig, setSavedConfig] = useState({ + componentRef, + parameters: initialParameters, + }); + const dirty = + componentRef !== savedConfig.componentRef || + !isEqual(parameters, savedConfig.parameters); const [runs, setRuns] = useState([]); const [cursor, setCursor] = useState(null); const [selected, setSelected] = useState(null); @@ -55,11 +77,11 @@ export default function EventProcessorDetailContent({ const [saving, setSaving] = useState(false); const [pagingRuns, setPagingRuns] = useState(false); const [pagingEvents, setPagingEvents] = useState(false); - const [configOpen, setConfigOpen] = useState(false); const [failed, setFailed] = useState(false); const validate = useRef<(() => Promise) | null>(null); const requestVersion = useRef(0); - const available = components.some((item) => item.id === agent.component_ref); + const component = components.find((item) => item.id === componentRef); + const available = Boolean(component); const load = useCallback(async () => { setFailed(false); @@ -88,20 +110,38 @@ export default function EventProcessorDetailContent({ [id], ); - async function openRun(run: ProcessorRun) { - const version = ++requestVersion.current; - setSelected(run); - setEvents([]); - setEventCursor(null); + const openRun = useCallback( + async (run: ProcessorRun) => { + const version = ++requestVersion.current; + setSelected(run); + setEvents([]); + setEventCursor(null); + try { + const page = await httpClient.getProcessorRunEvents(id, run.run_id); + if (version !== requestVersion.current) return; + setSelected(page.run); + setEvents(page.items); + setEventCursor(page.has_more ? page.next_cursor : null); + } catch { + if (version === requestVersion.current) + toast.error(t('agents.eventProcessor.loadError')); + } + }, + [id, t], + ); + + useEffect(() => { + if (!selected && runs.length > 0) void openRun(runs[0]); + }, [selected, runs, openRun]); + + async function refreshLatestRun() { try { - const page = await httpClient.getProcessorRunEvents(id, run.run_id); - if (version !== requestVersion.current) return; - setSelected(page.run); - setEvents(page.items); - setEventCursor(page.has_more ? page.next_cursor : null); + const page = await httpClient.getProcessorRuns(id); + setRuns(page.items); + setCursor(page.has_more ? page.next_cursor : null); + if (page.items[0]) await openRun(page.items[0]); } catch { - if (version === requestVersion.current) - toast.error(t('agents.eventProcessor.loadError')); + toast.error(t('agents.eventProcessor.loadError')); } } @@ -195,7 +235,13 @@ export default function EventProcessorDetailContent({ }, [id, selected, eventCursor, events]); async function save() { - if (!componentRef || !((await validate.current?.()) ?? true)) return; + if ( + !canManage || + saving || + !component || + !((await validate.current?.()) ?? true) + ) + return false; setSaving(true); try { await httpClient.updateAgent(id, { @@ -208,230 +254,213 @@ export default function EventProcessorDetailContent({ }); toast.success(t('agents.saveSuccess')); onSaved(); - setConfigOpen(false); + setSavedConfig({ componentRef, parameters }); await load(); + return true; } catch { toast.error(t('agents.saveError')); + return false; } finally { setSaving(false); } } - function payload(value: unknown) { - return ( -
-        {JSON.stringify(value, null, 2)}
-      
- ); - } - return ( -
-
- -

{agent.name}

- {canManage && ( - - )} - {t('agents.eventProcessor.type')} - {!loading && !failed && !available && ( - - {t('agents.eventProcessor.unavailable')} - - )} -
- - {canManage && ( - <> - - - - )} -
-
-

- {agent.component_ref} -

- {configOpen && ( -
- { - setComponentRef(value); - setParameters({}); - validate.current = null; - }} - onParametersChange={setParameters} - onValidate={(fn) => { - validate.current = fn; + ) : undefined + } + configTitle={t('agents.eventProcessor.trace')} + configIcon={} + configContent={ +
+
{ + event.preventDefault(); + void save(); }} /> - -
- )} - {failed && ( -

- {t('agents.eventProcessor.loadError')} -

- )} -
-
-

- {t('agents.eventProcessor.runs')} -

- {loading ? ( -

{t('common.loading')}

- ) : runs.length === 0 && !failed ? ( -
-

{t('agents.eventProcessor.noRuns')}

- - {t('agents.eventProcessor.bindBot')} - -
- ) : ( - runs.map((run) => ( - - )) + {failed && ( + + + {t('agents.eventProcessor.loadError')} + + )} - {cursor !== null && ( +
+ + {t('agents.eventProcessor.runs')}{' '} + ({runs.length}) + +
+ {runs.length > 0 && ( + void openRun(run)} + footer={ + cursor !== null ? ( + + ) : undefined + } + /> )} -
-
-

- {t('agents.eventProcessor.trace')} -

- {!selected ? ( -

- {t('agents.eventProcessor.selectRun')} -

- ) : ( -
-
- - {t('agents.eventProcessor.input')} - - {payload(selected.metadata.input_event)} -
- {selected.metadata.delivery != null && ( -
- - {t('agents.eventProcessor.destination')} - - {payload(selected.metadata.delivery)} -
- )} - {events.map((event) => - event.type === 'processor.log' ? ( -
- - {String(event.data.level)} - - - {String(event.data.text)} - + +
+ {!selected ? ( + + + {loading + ? t('common.loading') + : t('agents.eventProcessor.noRuns')} + + + + ) : ( + <> +
+

+ {eventPatternLabel(selected.metadata.event_type ?? '', t)} +

+

+ {new Date(selected.created_at * 1000).toLocaleString()} +

- ) : ( -
- - {t( - `agents.eventProcessor.trace_${event.type.replaceAll('.', '_')}`, - { defaultValue: event.type }, - )} - {typeof event.data.tool_name === 'string' && ( - - {toolLabels[event.data.tool_name] || - event.data.tool_name} - - )} - - {payload(event.data)} -
- ), - )} - {selected.status === 'failed' && selected.status_reason && ( -

- {selected.status_reason} -

- )} - {eventCursor !== null && ( - + {t(`agents.eventProcessor.status_${selected.status}`, { + defaultValue: selected.status, + })} + + + {selected.metadata.delivery != null && ( + + )} + + {selected.status === 'failed' && selected.status_reason && ( + + + {selected.status_reason} + + + )} + {eventCursor !== null && ( + + )} + )}
- )} -
-
-
+ +
+ } + debugTitle={canOperate ? t('agents.debugTab') : undefined} + debugDescription={t('agents.eventProcessor.debugNotice')} + debugContent={ + canOperate ? ( + !component ? ( + + + {t('agents.eventProcessor.selectToDebug')} + + + ) : ( + { + void refreshLatestRun(); + }} + supportedEventPatterns={component.supported_event_patterns} + availableEventTypes={availableEventTypes} + /> + ) + ) : undefined + } + unsavedLabel={t('pipelines.unsavedChanges')} + /> ); } diff --git a/web/src/app/home/agents/components/AgentCreateContent.tsx b/web/src/app/home/agents/components/AgentCreateContent.tsx index c212e34f4..d9d6ada94 100644 --- a/web/src/app/home/agents/components/AgentCreateContent.tsx +++ b/web/src/app/home/agents/components/AgentCreateContent.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from 'react'; +import { useState } from 'react'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; @@ -6,8 +6,7 @@ import { useTranslation } from 'react-i18next'; import { toast } from 'sonner'; import { Bot, Workflow, FileCode2 } from 'lucide-react'; import { httpClient } from '@/app/infra/http/HttpClient'; -import { AgentKind, EventProcessorDescriptor } from '@/app/infra/entities/api'; -import EventProcessorSettings from './EventProcessorSettings'; +import { AgentKind } from '@/app/infra/entities/api'; import { Button } from '@/components/ui/button'; import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'; import { @@ -36,23 +35,6 @@ export default function AgentCreateContent({ }) { const { t } = useTranslation(); const [kind, setKind] = useState('agent'); - const [components, setComponents] = useState([]); - const [componentRef, setComponentRef] = useState(''); - const [parameters, setParameters] = useState>({}); - const validateParameters = useRef<(() => Promise) | null>(null); - useEffect(() => { - if (kind !== 'event_processor') return; - let cancelled = false; - httpClient - .getAgentMetadata() - .then((metadata) => { - if (!cancelled) setComponents(metadata.event_processors ?? []); - }) - .catch(() => toast.error(t('agents.eventProcessor.loadError'))); - return () => { - cancelled = true; - }; - }, [kind, t]); const formSchema = z.object({ name: z.string().min(1, { message: t('agents.nameRequired') }), description: z.string().optional(), @@ -85,23 +67,9 @@ export default function AgentCreateContent({ } async function handleSubmit(values: FormValues) { - if ( - kind === 'event_processor' && - (!componentRef || !((await validateParameters.current?.()) ?? true)) - ) - return; - httpClient + return httpClient .createAgent({ kind, - ...(kind === 'event_processor' - ? { - component_ref: componentRef, - config: { - runner: { id: componentRef }, - runner_config: { [componentRef]: parameters }, - }, - } - : {}), name: values.name, description: values.description ?? '', emoji: values.emoji || (kind === 'pipeline' ? '⚙️' : '🤖'), @@ -143,10 +111,7 @@ export default function AgentCreateContent({ @@ -209,22 +174,6 @@ export default function AgentCreateContent({ - {kind === 'event_processor' && ( - { - setComponentRef(value); - setParameters({}); - validateParameters.current = null; - }} - onParametersChange={setParameters} - onValidate={(validate) => { - validateParameters.current = validate; - }} - /> - )} {t('agents.basicInfo')} diff --git a/web/src/app/home/agents/components/AgentDebugPanel.tsx b/web/src/app/home/agents/components/AgentDebugPanel.tsx index f3525ce61..ee0c5a985 100644 --- a/web/src/app/home/agents/components/AgentDebugPanel.tsx +++ b/web/src/app/home/agents/components/AgentDebugPanel.tsx @@ -46,6 +46,7 @@ import { groupEventPatterns, } from '@/app/home/components/event-patterns/event-pattern-groups'; import EventSelectOptionContent from '@/app/home/components/event-patterns/EventSelectOptionContent'; +import EventProcessorTrace from './EventProcessorTrace'; import AgentExecutionTrace from './AgentExecutionTrace'; import AgentEventDataEditor from './AgentEventDataEditor'; import { @@ -53,15 +54,18 @@ import { debugEventInputText, invalidDebugEventField, parseDebugEventData, + processorDebugEventTypes, } from './debug-event-data'; import { executionSteps, type DebugExecutionEvent } from './debug-execution'; interface AgentDebugPanelProps { agentId: string; + processor?: boolean; availableEventTypes: string[]; platformTools?: AgentPlatformTool[]; supportedEventPatterns?: string[]; beforeRun?: () => Promise; + onRunFinished?: () => void; hasUnsavedChanges?: boolean; onOpenRunnerConfig?: () => void; } @@ -89,10 +93,12 @@ function matchesEventPattern(pattern: string, eventType: string) { export default function AgentDebugPanel({ agentId, + processor = false, availableEventTypes, platformTools = [], supportedEventPatterns = ['*'], beforeRun, + onRunFinished, hasUnsavedChanges = false, onOpenRunnerConfig, }: AgentDebugPanelProps) { @@ -105,15 +111,19 @@ export default function AgentDebugPanel({ const newEventData = useCallback( (type: string) => JSON.stringify( - createDebugEventData(type, { - user: t('agents.debugData.sampleUser'), - message: t('agents.debugData.sampleMessage'), - feedback: t('agents.debugData.sampleFeedback'), - }), + createDebugEventData( + type, + { + user: t('agents.debugData.sampleUser'), + message: t('agents.debugData.sampleMessage'), + feedback: t('agents.debugData.sampleFeedback'), + }, + processor, + ), null, 2, ), - [t], + [t, processor], ); const [eventDataText, setEventDataText] = useState(() => newEventData('message.received'), @@ -140,21 +150,28 @@ export default function AgentDebugPanel({ const concretePatterns = supportedEventPatterns.filter( (pattern) => pattern !== '*' && !pattern.endsWith('.*'), ); - return Array.from(new Set([...availableEventTypes, ...concretePatterns])) + return Array.from( + new Set([ + ...(processor ? processorDebugEventTypes : availableEventTypes), + ...concretePatterns, + ]), + ) .filter((candidate) => supportedEventPatterns.some((pattern) => matchesEventPattern(pattern, candidate), ), ) .sort(); - }, [availableEventTypes, supportedEventPatterns]); + }, [availableEventTypes, supportedEventPatterns, processor]); const eventGroups = useMemo( () => groupEventPatterns(availableEvents), [availableEvents], ); - const supportsCustomEvent = supportedEventPatterns.some( - (pattern) => pattern === '*' || pattern.endsWith('.*'), - ); + const supportsCustomEvent = + !processor && + supportedEventPatterns.some( + (pattern) => pattern === '*' || pattern.endsWith('.*'), + ); const selectPreset = useCallback( (value: string) => { @@ -193,7 +210,11 @@ export default function AgentDebugPanel({ toast.error(t('agents.debugInvalidPayload')); return; } - const invalidField = invalidDebugEventField(eventType, eventData); + const invalidField = invalidDebugEventField( + eventType, + eventData, + processor, + ); if (invalidField) { toast.error( t('agents.debugData.invalidField', { @@ -202,7 +223,7 @@ export default function AgentDebugPanel({ ); return; } - const inputText = debugEventInputText(eventType, eventData); + const inputText = debugEventInputText(eventType, eventData, processor); let mockOptions: Record; try { const parsed = JSON.parse(mockOptionsText || '{}'); @@ -275,12 +296,14 @@ export default function AgentDebugPanel({ ? { ...entry, finished: true, - text: executionSteps(entry.events ?? []).some( - (step) => - step.kind === 'tool' || step.text || step.reasoning, - ) - ? '' - : result.final_text || t('agents.debugNoTextOutput'), + text: + processor || + executionSteps(entry.events ?? []).some( + (step) => + step.kind === 'tool' || step.text || step.reasoning, + ) + ? '' + : result.final_text || t('agents.debugNoTextOutput'), } : entry, ) @@ -354,6 +377,7 @@ export default function AgentDebugPanel({ if (requestRef.current === controller) { requestRef.current = null; setRunning(false); + onRunFinished?.(); } } } @@ -364,15 +388,25 @@ export default function AgentDebugPanel({

{t('agents.debugTranscript')}

- {t('agents.debugTranscriptDescription')} + {t( + processor + ? 'agents.eventProcessor.debugDescription' + : 'agents.debugTranscriptDescription', + )}

{entries.length === 0 ? ( - {t('agents.debugEmptyTitle')} + + {t(processor ? 'agents.debugTab' : 'agents.debugEmptyTitle')} + - {t('agents.debugEmptyTranscript')} + {t( + processor + ? 'agents.eventProcessor.debugDescription' + : 'agents.debugEmptyTranscript', + )} ) : ( @@ -382,6 +416,7 @@ export default function AgentDebugPanel({ (entry) => entry.direction !== 'output' || entry.text || + processor || executionSteps(entry.events ?? []).some( (step) => step.kind === 'tool' || step.text || step.reasoning, @@ -412,19 +447,29 @@ export default function AgentDebugPanel({ {entry.direction === 'output' - ? t('agents.debugAgentOutput') + ? t( + processor + ? 'agents.eventProcessor.debugOutput' + : 'agents.debugAgentOutput', + ) : entry.direction === 'error' ? t('common.error') : t('agents.debugTestInput')} - {entry.events && ( - - )} + {entry.events && + (processor ? ( + + ) : ( + + ))} {entry.text && (
                         {entry.text}
@@ -546,26 +591,37 @@ export default function AgentDebugPanel({
               
 
-              
- - {t('agents.debugMockOptions')} - -

- {t('agents.debugMockOptionsHelp')} -

-