feat(agent): add event debugging and streamline configuration

This commit is contained in:
huanghuoguoguo
2026-08-23 20:32:51 +08:00
parent 5170e2e2f3
commit 0fbb1f897e
13 changed files with 1035 additions and 228 deletions
@@ -47,6 +47,24 @@ class AgentsRouterGroup(group.RouterGroup):
async def _(request_context: RequestContext) -> str: async def _(request_context: RequestContext) -> str:
return self.success(data=await self.ap.agent_service.get_agent_metadata(request_context)) return self.success(data=await self.ap.agent_service.get_agent_metadata(request_context))
@self.route(
'/<agent_uuid>/debug',
methods=['POST'],
auth_type=group.AuthType.USER_TOKEN_OR_API_KEY,
permission=Permission.RUNTIME_OPERATE,
)
async def _(agent_uuid: str, request_context: RequestContext) -> str:
json_data = await quart.request.json
try:
result = await self.ap.agent_service.debug_agent(
request_context,
agent_uuid,
json_data or {},
)
except ValueError as exc:
return self.http_status(400, -1, str(exc))
return self.success(data=result)
@self.route( @self.route(
'/<agent_uuid>', '/<agent_uuid>',
methods=['GET', 'PUT', 'DELETE'], methods=['GET', 'PUT', 'DELETE'],
+186 -1
View File
@@ -1,15 +1,33 @@
from __future__ import annotations from __future__ import annotations
import datetime import datetime
import fnmatch
import time
import uuid import uuid
import typing import typing
import sqlalchemy import sqlalchemy
from langbot_plugin.api.entities.builtin.agent_runner.delivery import DeliveryContext
from langbot_plugin.api.entities.builtin.agent_runner.event import (
ActorContext,
RawEventRef,
SubjectContext,
)
from langbot_plugin.api.entities.builtin.agent_runner.input import AgentInput
from ....core import app from ....core import app
from ....agent.runner.config_resolver import RunnerConfigResolver from ....agent.runner.config_resolver import RunnerConfigResolver
from ....agent.runner.host_models import (
AgentBinding,
AgentEventEnvelope,
BindingScope,
DeliveryPolicy,
StatePolicy,
)
from ....agent.runner.resource_policy import ResourcePolicyProjector
from ....entity.persistence import agent as persistence_agent from ....entity.persistence import agent as persistence_agent
from ....workspace.errors import WorkspaceNotFoundError from ....workspace.errors import WorkspaceNotFoundError
from ..context import ExecutionContext, RequestContext
from .tenant import TenantContext, require_workspace_uuid, scope_statement from .tenant import TenantContext, require_workspace_uuid, scope_statement
@@ -78,6 +96,173 @@ class AgentService:
return None return None
async def debug_agent(
self,
context: RequestContext,
agent_uuid: str,
payload: dict[str, typing.Any],
) -> dict[str, typing.Any]:
"""Execute one synthetic event against a configured Agent.
The debug surface uses a trusted Workspace execution context, never
delivers outputs to a real platform, and supports both message and
non-message event envelopes.
"""
agent = await self.get_agent(context, agent_uuid)
if agent is None or agent.get('kind') != AGENT_KIND_AGENT:
raise ValueError('Agent not found')
event_type = str(payload.get('event_type') or 'message.received').strip()
if not event_type or len(event_type) > 128:
raise ValueError('Invalid event_type')
if not self._supports_event_type(
agent.get('supported_event_patterns'),
event_type,
):
raise ValueError('Agent does not support this event type')
text = str(payload.get('text') or '').strip()
if len(text) > 20_000:
raise ValueError('Debug input is too long')
event_data = payload.get('data') or {}
if not isinstance(event_data, dict):
raise ValueError('Debug event data must be an object')
config = agent.get('config')
if not isinstance(config, dict):
raise ValueError('Agent configuration is invalid')
_, runner_id, runner_config = RunnerConfigResolver.resolve_agent_runner_config(config)
if not runner_id:
raise ValueError('Agent has no configured runner')
conversation_id = str(payload.get('conversation_id') or f'debug:{agent_uuid}').strip()
if not conversation_id or len(conversation_id) > 256:
raise ValueError('Invalid debug conversation_id')
actor_payload = payload.get('actor') or {
'actor_type': 'user',
'actor_id': 'debug-user',
'actor_name': 'Debug User',
}
subject_payload = payload.get('subject') or {
'subject_type': 'message' if event_type.startswith('message.') else event_type.split('.', 1)[0],
'subject_id': 'debug-subject',
'data': event_data,
}
if not isinstance(actor_payload, dict) or not isinstance(subject_payload, dict):
raise ValueError('Debug actor and subject must be objects')
event_id = f'debug:{agent_uuid}:{uuid.uuid4()}'
event = AgentEventEnvelope(
event_id=event_id,
event_type=event_type,
event_time=int(time.time()),
source='webui',
source_event_type=event_type,
workspace_id=context.workspace_uuid,
conversation_id=conversation_id,
actor=ActorContext.model_validate(actor_payload),
subject=SubjectContext.model_validate(subject_payload),
input=AgentInput.model_validate(
{
'text': text or event_type,
'contents': [
{'type': 'text', 'text': text or event_type},
],
'attachments': [],
}
),
delivery=DeliveryContext(
surface='webui',
reply_target=None,
supports_streaming=False,
supports_edit=False,
supports_reaction=False,
platform_capabilities={
'event_type': event_type,
'debug': True,
},
),
raw_ref=RawEventRef(ref_id=event_id, storage_key=None),
data=event_data,
)
binding = AgentBinding(
binding_id=f'debug:{agent_uuid}:{runner_id}',
scope=BindingScope(scope_type='agent', scope_id=agent_uuid),
event_types=[event_type],
runner_id=runner_id,
runner_config=runner_config,
resource_policy=ResourcePolicyProjector.from_runner_config(runner_config),
state_policy=StatePolicy(
state_scopes=['conversation', 'actor', 'subject', 'runner'],
),
delivery_policy=DeliveryPolicy(
enable_streaming=False,
enable_reply=False,
enable_interactions=False,
),
enabled=True,
agent_id=agent_uuid,
processor_type='agent',
processor_id=agent_uuid,
)
execution_context = ExecutionContext.from_request(
context,
query_uuid=event_id,
)
output_items: list[dict[str, typing.Any]] = []
final_text = ''
async for output in self.ap.agent_run_orchestrator.run(
event,
binding,
adapter_context={'_execution_context': execution_context},
):
output_text = self._provider_output_to_text(output)
if output_text:
final_text = output_text
output_items.append(
{
'kind': output.__class__.__name__,
'role': str(getattr(output, 'role', '') or ''),
'text': output_text,
}
)
return {
'event_id': event_id,
'event_type': event_type,
'conversation_id': conversation_id,
'final_text': final_text,
'outputs': output_items,
}
@staticmethod
def _supports_event_type(patterns: typing.Any, event_type: str) -> bool:
normalized = patterns if isinstance(patterns, list) else AGENT_DEFAULT_EVENT_PATTERNS
return any(isinstance(pattern, str) and fnmatch.fnmatchcase(event_type, pattern) for pattern in normalized)
@staticmethod
def _provider_output_to_text(output: typing.Any) -> str:
all_content = getattr(output, 'all_content', None)
if all_content:
return str(all_content)
content = getattr(output, 'content', None)
if content is None:
return ''
if isinstance(content, str):
return content
if isinstance(content, list):
parts: list[str] = []
for item in content:
item_data = item.model_dump(mode='json') if hasattr(item, 'model_dump') else item
if isinstance(item_data, dict) and item_data.get('text') is not None:
parts.append(str(item_data['text']))
elif item_data is not None and not isinstance(item_data, dict):
parts.append(str(item_data))
return ''.join(parts)
return str(content)
async def create_agent(self, context: TenantContext, agent_data: dict) -> dict[str, str]: async def create_agent(self, context: TenantContext, agent_data: dict) -> dict[str, str]:
workspace_uuid = require_workspace_uuid(context) workspace_uuid = require_workspace_uuid(context)
kind = agent_data.get('kind') or AGENT_KIND_AGENT kind = agent_data.get('kind') or AGENT_KIND_AGENT
@@ -89,7 +274,7 @@ class AgentService:
'description': agent_data.get('description') or '', 'description': agent_data.get('description') or '',
'emoji': agent_data.get('emoji') or '⚙️', 'emoji': agent_data.get('emoji') or '⚙️',
'config': {}, 'config': {},
} },
) )
return {'uuid': pipeline_uuid, 'kind': AGENT_KIND_PIPELINE} return {'uuid': pipeline_uuid, 'kind': AGENT_KIND_PIPELINE}
@@ -111,9 +111,7 @@ class TestAgentServiceMetadata:
) )
metadata = await AgentService(app).get_agent_metadata(WORKSPACE_UUID) metadata = await AgentService(app).get_agent_metadata(WORKSPACE_UUID)
app.pipeline_service.get_pipeline_metadata.assert_awaited_once_with( app.pipeline_service.get_pipeline_metadata.assert_awaited_once_with(WORKSPACE_UUID)
WORKSPACE_UUID
)
assert metadata['runner_config'] == ai_metadata assert metadata['runner_config'] == ai_metadata
assert metadata['kinds'] == [ assert metadata['kinds'] == [
@@ -130,6 +128,88 @@ class TestAgentServiceMetadata:
] ]
class TestAgentServiceDebug:
async def test_debug_agent_runs_configured_runner_with_synthetic_event(self):
app = _make_app()
async def run_agent(event, binding, adapter_context):
yield SimpleNamespace(
role='assistant',
content='debug result',
all_content=None,
)
app.agent_run_orchestrator = SimpleNamespace(run=Mock(side_effect=run_agent))
service = AgentService(app)
service.get_agent = AsyncMock(
return_value={
'uuid': 'agent-1',
'kind': AGENT_KIND_AGENT,
'enabled': True,
'supported_event_patterns': ['*'],
'config': _agent_row().config,
}
)
context = SimpleNamespace(
instance_uuid='instance-test',
workspace_uuid=WORKSPACE_UUID,
placement_generation=1,
principal=SimpleNamespace(account_uuid='account-test'),
entitlement_revision=0,
)
result = await service.debug_agent(
context,
'agent-1',
{
'event_type': 'group.member.joined',
'text': 'A member joined.',
'data': {'member_id': 'user-1'},
'conversation_id': 'debug-session',
},
)
assert result['event_type'] == 'group.member.joined'
assert result['conversation_id'] == 'debug-session'
assert result['final_text'] == 'debug result'
assert result['outputs'] == [
{
'kind': 'SimpleNamespace',
'role': 'assistant',
'text': 'debug result',
}
]
event, binding = app.agent_run_orchestrator.run.call_args.args
assert event.workspace_id == WORKSPACE_UUID
assert event.data == {'member_id': 'user-1'}
assert binding.agent_id == 'agent-1'
assert binding.runner_id == 'plugin:test/runner/default'
assert (
app.agent_run_orchestrator.run.call_args.kwargs['adapter_context']['_execution_context'].workspace_uuid
== WORKSPACE_UUID
)
async def test_debug_agent_rejects_unsupported_event_type(self):
app = _make_app()
service = AgentService(app)
service.get_agent = AsyncMock(
return_value={
'uuid': 'agent-1',
'kind': AGENT_KIND_AGENT,
'supported_event_patterns': ['message.*'],
'config': _agent_row().config,
}
)
context = SimpleNamespace(workspace_uuid=WORKSPACE_UUID)
with pytest.raises(ValueError, match='does not support'):
await service.debug_agent(
context,
'agent-1',
{'event_type': 'group.member.joined'},
)
class TestAgentServiceListAndLookup: class TestAgentServiceListAndLookup:
async def test_get_agents_merges_agents_and_pipelines_without_leaking_config(self): async def test_get_agents_merges_agents_and_pipelines_without_leaking_config(self):
app = _make_app() app = _make_app()
@@ -237,7 +317,7 @@ class TestAgentServiceCreateUpdateDelete:
'description': 'Handles support events', 'description': 'Handles support events',
'emoji': 'S', 'emoji': 'S',
'component_ref': 'plugin:caller/must-not-win/default', 'component_ref': 'plugin:caller/must-not-win/default',
} },
) )
insert_values = _compiled_params(app.persistence_mgr.execute_async.await_args.args[0]) insert_values = _compiled_params(app.persistence_mgr.execute_async.await_args.args[0])
@@ -309,7 +389,7 @@ class TestAgentServiceCreateUpdateDelete:
'runner': {'id': runner_id}, 'runner': {'id': runner_id},
'runner_config': {runner_id: {field_name: invalid_value}}, 'runner_config': {runner_id: {field_name: invalid_value}},
}, },
} },
) )
app.persistence_mgr.execute_async.assert_not_awaited() app.persistence_mgr.execute_async.assert_not_awaited()
@@ -334,7 +414,7 @@ class TestAgentServiceCreateUpdateDelete:
} }
}, },
}, },
} },
) )
app.persistence_mgr.execute_async.assert_not_awaited() app.persistence_mgr.execute_async.assert_not_awaited()
@@ -352,7 +432,7 @@ class TestAgentServiceCreateUpdateDelete:
'runner': {'id': ''}, 'runner': {'id': ''},
'runner_config': {}, 'runner_config': {},
}, },
} },
) )
insert_values = _compiled_params(app.persistence_mgr.execute_async.await_args.args[0]) insert_values = _compiled_params(app.persistence_mgr.execute_async.await_args.args[0])
@@ -491,7 +571,7 @@ class TestAgentServiceCreateUpdateDelete:
'name': 'Pipeline Agent', 'name': 'Pipeline Agent',
'description': 'Legacy pipeline', 'description': 'Legacy pipeline',
'emoji': 'P', 'emoji': 'P',
} },
) )
await service.update_agent( await service.update_agent(
WORKSPACE_UUID, WORKSPACE_UUID,
@@ -508,7 +588,7 @@ class TestAgentServiceCreateUpdateDelete:
'description': 'Legacy pipeline', 'description': 'Legacy pipeline',
'emoji': 'P', 'emoji': 'P',
'config': {}, 'config': {},
} },
) )
app.pipeline_service.update_pipeline.assert_awaited_once_with( app.pipeline_service.update_pipeline.assert_awaited_once_with(
WORKSPACE_UUID, WORKSPACE_UUID,
+57 -6
View File
@@ -41,12 +41,8 @@ async def _create_test_client(agent_service: SimpleNamespace):
ap = SimpleNamespace( ap = SimpleNamespace(
agent_service=agent_service, agent_service=agent_service,
user_service=user_service, user_service=user_service,
apikey_service=SimpleNamespace( apikey_service=SimpleNamespace(authenticate_api_key=AsyncMock(return_value=None)),
authenticate_api_key=AsyncMock(return_value=None) workspace_collaboration_service=SimpleNamespace(resolve_account_workspace=AsyncMock(return_value=access)),
),
workspace_collaboration_service=SimpleNamespace(
resolve_account_workspace=AsyncMock(return_value=access)
),
) )
AgentsRouterGroup = import_module('langbot.pkg.api.http.controller.groups.agents').AgentsRouterGroup AgentsRouterGroup = import_module('langbot.pkg.api.http.controller.groups.agents').AgentsRouterGroup
group = AgentsRouterGroup(ap, app) group = AgentsRouterGroup(ap, app)
@@ -91,3 +87,58 @@ async def test_update_agent_returns_bad_request_for_invalid_runner_config():
'agent-1', 'agent-1',
{'config': {'runner': {'id': 7}}}, {'config': {'runner': {'id': 7}}},
) )
async def test_debug_agent_executes_with_runtime_permission():
result = {
'event_id': 'debug-event',
'event_type': 'message.received',
'conversation_id': 'debug-session',
'final_text': 'hello',
'outputs': [],
}
agent_service = SimpleNamespace(debug_agent=AsyncMock(return_value=result))
client = await _create_test_client(agent_service)
response = await client.post(
'/api/v1/agents/agent-1/debug',
json={
'event_type': 'message.received',
'text': 'hello',
'data': {},
'conversation_id': 'debug-session',
},
headers={'Authorization': 'Bearer test-token'},
)
assert response.status_code == 200
assert (await response.get_json())['data'] == result
agent_service.debug_agent.assert_awaited_once_with(
ANY,
'agent-1',
{
'event_type': 'message.received',
'text': 'hello',
'data': {},
'conversation_id': 'debug-session',
},
)
async def test_debug_agent_returns_bad_request_for_invalid_event():
agent_service = SimpleNamespace(
debug_agent=AsyncMock(side_effect=ValueError('Invalid event_type')),
)
client = await _create_test_client(agent_service)
response = await client.post(
'/api/v1/agents/agent-1/debug',
json={'event_type': ''},
headers={'Authorization': 'Bearer test-token'},
)
assert response.status_code == 400
assert await response.get_json() == {
'code': -1,
'msg': 'Invalid event_type',
}
+62 -15
View File
@@ -1,23 +1,31 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Bug, Settings } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { httpClient } from '@/app/infra/http/HttpClient'; import { httpClient } from '@/app/infra/http/HttpClient';
import { useCurrentWorkspace } from '@/app/infra/http';
import { Agent } from '@/app/infra/entities/api'; import { Agent } from '@/app/infra/entities/api';
import { useSidebarData } from '@/app/home/components/home-sidebar/SidebarDataContext'; import { useSidebarData } from '@/app/home/components/home-sidebar/SidebarDataContext';
import PipelineDetailContent from '@/app/home/pipelines/PipelineDetailContent'; import PipelineDetailContent from '@/app/home/pipelines/PipelineDetailContent';
import AgentCreateContent from './components/AgentCreateContent'; import AgentCreateContent from './components/AgentCreateContent';
import AgentDebugPanel from './components/AgentDebugPanel';
import AgentFormComponent from './components/AgentFormComponent'; import AgentFormComponent from './components/AgentFormComponent';
export default function AgentDetailContent({ id }: { id: string }) { export default function AgentDetailContent({ id }: { id: string }) {
const isCreateMode = id === 'new'; const isCreateMode = id === 'new';
const navigate = useNavigate(); const navigate = useNavigate();
const { t } = useTranslation(); const { t } = useTranslation();
const currentWorkspace = useCurrentWorkspace();
const canOperate =
currentWorkspace?.permissions.includes('runtime.operate') ?? false;
const { refreshPipelines, pipelines, setDetailEntityName } = useSidebarData(); const { refreshPipelines, pipelines, setDetailEntityName } = useSidebarData();
const [agent, setAgent] = useState<Agent | null>(null); const [agent, setAgent] = useState<Agent | null>(null);
const [loading, setLoading] = useState(!isCreateMode); const [loading, setLoading] = useState(!isCreateMode);
const [formDirty, setFormDirty] = useState(false); const [formDirty, setFormDirty] = useState(false);
const [formSaving, setFormSaving] = useState(false); const [formSaving, setFormSaving] = useState(false);
const [activeTab, setActiveTab] = useState('config');
useEffect(() => { useEffect(() => {
if (isCreateMode) { if (isCreateMode) {
@@ -71,32 +79,71 @@ export default function AgentDetailContent({ id }: { id: string }) {
} }
return ( return (
<div className="flex h-full flex-col"> <div className="flex h-full min-w-0 flex-col">
<div className="flex items-center justify-between pb-4 shrink-0"> <div className="flex items-center justify-between pb-4 shrink-0">
<h1 className="text-xl font-semibold">{t('agents.editAgent')}</h1> <h1 className="text-xl font-semibold">{t('agents.editAgent')}</h1>
<Button <Button
type="submit" type="submit"
form="agent-form" form="agent-form"
disabled={!formDirty || formSaving} disabled={!formDirty || formSaving}
className={activeTab !== 'config' ? 'invisible' : ''}
> >
{t('common.save')} {t('common.save')}
</Button> </Button>
</div> </div>
<div className="flex-1 overflow-y-auto min-h-0"> <Tabs
<AgentFormComponent key={id}
agentId={id} value={activeTab}
onFinish={() => { onValueChange={setActiveTab}
refreshPipelines(); className="flex min-h-0 min-w-0 flex-1 flex-col"
}} >
onDeleted={() => { <TabsList className="shrink-0">
refreshPipelines(); <TabsTrigger value="config" className="gap-1.5">
navigate('/home/agents'); <Settings className="size-3.5" />
}} {t('pipelines.configuration')}
onDirtyChange={setFormDirty} </TabsTrigger>
onSavingChange={setFormSaving} {canOperate && (
/> <TabsTrigger value="debug" className="gap-1.5">
</div> <Bug className="size-3.5" />
{t('agents.debugTab')}
</TabsTrigger>
)}
</TabsList>
<TabsContent
value="config"
className="mt-4 min-h-0 min-w-0 flex-1 overflow-hidden"
>
<AgentFormComponent
agentId={id}
onFinish={() => {
refreshPipelines();
}}
onDeleted={() => {
refreshPipelines();
navigate('/home/agents');
}}
onDirtyChange={setFormDirty}
onSavingChange={setFormSaving}
/>
</TabsContent>
{canOperate && (
<TabsContent
value="debug"
className="mt-4 min-h-0 min-w-0 flex-1 overflow-y-auto overflow-x-hidden"
>
<AgentDebugPanel
agentId={id}
supportedEventPatterns={
agent.supported_event_patterns ??
agent.capability?.supported_event_patterns ?? ['*']
}
/>
</TabsContent>
)}
</Tabs>
</div> </div>
); );
} }
@@ -0,0 +1,366 @@
import { useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { toast } from 'sonner';
import {
AlertTriangle,
Braces,
LoaderCircle,
MessageSquare,
Play,
RotateCcw,
} from 'lucide-react';
import { httpClient } from '@/app/infra/http/HttpClient';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Textarea } from '@/components/ui/textarea';
interface AgentDebugPanelProps {
agentId: string;
supportedEventPatterns?: string[];
}
interface DebugEntry {
id: string;
direction: 'input' | 'output' | 'error';
eventType: string;
text: string;
}
const EVENT_PRESETS = [
{
value: 'message.received',
labelKey: 'agents.debugMessageReceived',
text: '',
data: {},
},
{
value: 'group.member.joined',
labelKey: 'agents.debugGroupMemberJoined',
text: 'A new member joined the group.',
data: {
group_id: 'debug-group',
member_id: 'debug-user',
member_name: 'Debug User',
},
},
{
value: 'group.member.left',
labelKey: 'agents.debugGroupMemberLeft',
text: 'A member left the group.',
data: {
group_id: 'debug-group',
member_id: 'debug-user',
member_name: 'Debug User',
},
},
{
value: 'friend.requested',
labelKey: 'agents.debugFriendRequested',
text: 'A user sent a friend request.',
data: {
requester_id: 'debug-user',
requester_name: 'Debug User',
message: 'Hello',
},
},
{
value: 'feedback.received',
labelKey: 'agents.debugFeedbackReceived',
text: 'The user submitted feedback.',
data: {
rating: 5,
content: 'Debug feedback',
},
},
{
value: 'custom',
labelKey: 'agents.debugCustomEvent',
text: '',
data: {},
},
] as const;
function createDebugSessionId(agentId: string) {
const nonce = globalThis.crypto?.randomUUID?.() ?? String(Date.now());
return `webui:${agentId}:${nonce}`;
}
export default function AgentDebugPanel({
agentId,
supportedEventPatterns = ['*'],
}: AgentDebugPanelProps) {
const { t } = useTranslation();
const [preset, setPreset] = useState('message.received');
const [customEventType, setCustomEventType] = useState('custom.event');
const [inputText, setInputText] = useState('');
const [eventDataText, setEventDataText] = useState('{}');
const [running, setRunning] = useState(false);
const [entries, setEntries] = useState<DebugEntry[]>([]);
const sessionIdRef = useRef(createDebugSessionId(agentId));
const eventType = preset === 'custom' ? customEventType.trim() : preset;
const isMessageEvent = eventType.startsWith('message.');
const supportedLabel = useMemo(
() => supportedEventPatterns.join(', '),
[supportedEventPatterns],
);
function selectPreset(value: string) {
setPreset(value);
const nextPreset = EVENT_PRESETS.find((item) => item.value === value);
if (!nextPreset) return;
setInputText(nextPreset.text);
setEventDataText(JSON.stringify(nextPreset.data, null, 2));
}
function resetSession() {
sessionIdRef.current = createDebugSessionId(agentId);
setEntries([]);
}
async function runDebugEvent() {
if (!eventType) {
toast.error(t('agents.debugEventTypeRequired'));
return;
}
if (isMessageEvent && !inputText.trim()) {
toast.error(t('agents.debugInputRequired'));
return;
}
let eventData: Record<string, unknown>;
try {
const parsed = JSON.parse(eventDataText || '{}');
if (!parsed || Array.isArray(parsed) || typeof parsed !== 'object') {
throw new Error('payload must be an object');
}
eventData = parsed as Record<string, unknown>;
} catch {
toast.error(t('agents.debugInvalidPayload'));
return;
}
const requestId = globalThis.crypto?.randomUUID?.() ?? String(Date.now());
setEntries((current) => [
...current,
{
id: `input:${requestId}`,
direction: 'input',
eventType,
text: inputText.trim() || JSON.stringify(eventData, null, 2),
},
]);
setRunning(true);
try {
const result = await httpClient.debugAgent(agentId, {
event_type: eventType,
text: inputText.trim(),
data: eventData,
conversation_id: sessionIdRef.current,
});
setEntries((current) => [
...current,
{
id: `output:${result.event_id}`,
direction: 'output',
eventType,
text: result.final_text || t('agents.debugNoTextOutput'),
},
]);
if (isMessageEvent) setInputText('');
} catch (error) {
const message =
typeof error === 'object' && error && 'msg' in error
? String((error as { msg?: string }).msg || '')
: t('agents.debugRunFailed');
setEntries((current) => [
...current,
{
id: `error:${requestId}`,
direction: 'error',
eventType,
text: message || t('agents.debugRunFailed'),
},
]);
} finally {
setRunning(false);
}
}
return (
<div className="mx-auto grid w-full min-w-0 max-w-6xl gap-6 pb-8 lg:grid-cols-[minmax(0,1fr)_minmax(22rem,0.8fr)]">
<Card className="min-w-0">
<CardHeader>
<div className="flex flex-wrap items-start justify-between gap-3">
<div className="space-y-1">
<CardTitle className="flex items-center gap-2">
{isMessageEvent ? (
<MessageSquare className="size-5" />
) : (
<Braces className="size-5" />
)}
{t('agents.debugTitle')}
</CardTitle>
<CardDescription>{t('agents.debugDescription')}</CardDescription>
</div>
<Button
type="button"
variant="outline"
size="sm"
onClick={resetSession}
>
<RotateCcw className="size-4" />
{t('agents.debugResetSession')}
</Button>
</div>
</CardHeader>
<CardContent className="space-y-5">
<Alert>
<AlertTriangle />
<AlertTitle>{t('agents.debugActualRun')}</AlertTitle>
<AlertDescription>
{t('agents.debugActualRunDescription')}
</AlertDescription>
</Alert>
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label>{t('agents.debugEventType')}</Label>
<Select value={preset} onValueChange={selectPreset}>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
{EVENT_PRESETS.map((item) => (
<SelectItem key={item.value} value={item.value}>
{t(item.labelKey)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{preset === 'custom' && (
<div className="space-y-2">
<Label htmlFor="agent-debug-custom-event">
{t('agents.debugCustomEventType')}
</Label>
<Input
id="agent-debug-custom-event"
value={customEventType}
onChange={(event) => setCustomEventType(event.target.value)}
placeholder="custom.event"
/>
</div>
)}
</div>
<div className="space-y-2">
<Label htmlFor="agent-debug-input">
{isMessageEvent
? t('agents.debugMessageInput')
: t('agents.debugEventSummary')}
</Label>
<Textarea
id="agent-debug-input"
value={inputText}
onChange={(event) => setInputText(event.target.value)}
className="min-h-24 resize-y"
placeholder={t('agents.debugInputPlaceholder')}
/>
</div>
<div className="space-y-2">
<div className="flex flex-wrap items-center justify-between gap-2">
<Label htmlFor="agent-debug-payload">
{t('agents.debugEventPayload')}
</Label>
<span className="text-xs text-muted-foreground">
{t('agents.debugSupportedEvents')}: {supportedLabel}
</span>
</div>
<Textarea
id="agent-debug-payload"
value={eventDataText}
onChange={(event) => setEventDataText(event.target.value)}
className="min-h-40 resize-y font-mono text-xs"
spellCheck={false}
/>
</div>
<div className="flex justify-end">
<Button type="button" disabled={running} onClick={runDebugEvent}>
{running ? (
<LoaderCircle className="size-4 animate-spin" />
) : (
<Play className="size-4" />
)}
{running ? t('agents.debugRunning') : t('agents.debugRun')}
</Button>
</div>
</CardContent>
</Card>
<Card className="min-h-[28rem] min-w-0">
<CardHeader>
<CardTitle>{t('agents.debugTranscript')}</CardTitle>
<CardDescription>
{t('agents.debugTranscriptDescription')}
</CardDescription>
</CardHeader>
<CardContent>
{entries.length === 0 ? (
<div className="flex min-h-72 items-center justify-center rounded-lg border border-dashed p-8 text-center text-sm text-muted-foreground">
{t('agents.debugEmptyTranscript')}
</div>
) : (
<div className="max-h-[42rem] space-y-4 overflow-y-auto pr-1">
{entries.map((entry) => (
<div
key={entry.id}
className={`rounded-lg border p-3 ${
entry.direction === 'output'
? 'border-primary/20 bg-primary/5'
: entry.direction === 'error'
? 'border-destructive/30 bg-destructive/5'
: 'bg-muted/40'
}`}
>
<div className="mb-2 flex items-center justify-between gap-2">
<Badge variant="outline">{entry.eventType}</Badge>
<span className="text-xs text-muted-foreground">
{entry.direction === 'output'
? t('agents.debugAgentOutput')
: entry.direction === 'error'
? t('common.error')
: t('agents.debugTestInput')}
</span>
</div>
<pre className="min-w-0 whitespace-pre-wrap break-words font-sans text-sm leading-relaxed">
{entry.text}
</pre>
</div>
))}
</div>
)}
</CardContent>
</Card>
</div>
);
}
@@ -1,5 +1,4 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type React from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
@@ -7,11 +6,8 @@ import { z } from 'zod';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { import {
Brain,
CircleAlert, CircleAlert,
CircleCheck, CircleCheck,
FileJson2,
Info,
LoaderCircle, LoaderCircle,
Power, Power,
RefreshCw, RefreshCw,
@@ -26,7 +22,6 @@ import {
} from '@/app/infra/entities/pipeline'; } from '@/app/infra/entities/pipeline';
import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent'; import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent';
import { extractI18nObject } from '@/i18n/I18nProvider'; import { extractI18nObject } from '@/i18n/I18nProvider';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Switch } from '@/components/ui/switch'; import { Switch } from '@/components/ui/switch';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
@@ -65,12 +60,6 @@ interface AgentFormComponentProps {
onSavingChange?: (saving: boolean) => void; onSavingChange?: (saving: boolean) => void;
} }
interface SectionItem {
label: string;
name: 'basic' | 'runner' | 'events';
icon: React.ElementType;
}
export default function AgentFormComponent({ export default function AgentFormComponent({
agentId, agentId,
onFinish, onFinish,
@@ -79,8 +68,6 @@ export default function AgentFormComponent({
onSavingChange, onSavingChange,
}: AgentFormComponentProps) { }: AgentFormComponentProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const [activeSection, setActiveSection] =
useState<SectionItem['name']>('basic');
const [runnerConfigSchema, setRunnerConfigSchema] = const [runnerConfigSchema, setRunnerConfigSchema] =
useState<PipelineConfigTab | null>(null); useState<PipelineConfigTab | null>(null);
const [pluginSystemStatus, setPluginSystemStatus] = const [pluginSystemStatus, setPluginSystemStatus] =
@@ -183,12 +170,6 @@ export default function AgentFormComponent({
void loadPluginSystemStatus(); void loadPluginSystemStatus();
}, [loadPluginSystemStatus]); }, [loadPluginSystemStatus]);
const sections: SectionItem[] = [
{ label: t('agents.basicInfo'), name: 'basic', icon: Info },
{ label: t('agents.runnerSettings'), name: 'runner', icon: Brain },
{ label: t('agents.advanced'), name: 'events', icon: FileJson2 },
];
const currentRunner = (form.watch('runner') as Record<string, any>)?.id; const currentRunner = (form.watch('runner') as Record<string, any>)?.id;
const runnerOptions = useMemo(() => { const runnerOptions = useMemo(() => {
const runnerStage = runnerConfigSchema?.stages.find( const runnerStage = runnerConfigSchema?.stages.find(
@@ -450,55 +431,65 @@ export default function AgentFormComponent({
<form <form
id="agent-form" id="agent-form"
onSubmit={form.handleSubmit(handleSubmit)} onSubmit={form.handleSubmit(handleSubmit)}
className="h-full flex flex-col flex-1 min-h-0 mb-2" className="mb-2 flex h-full min-h-0 min-w-0 flex-1 flex-col"
> >
<div className="flex-1 flex flex-col md:flex-row min-h-0"> <div className="flex min-h-0 min-w-0 flex-1 flex-col">
<nav className="shrink-0 mb-4 md:mb-0 md:w-44 md:pr-4 md:mr-4 md:border-r overflow-x-auto md:overflow-x-visible md:overflow-y-auto"> <div className="min-h-0 min-w-0 flex-1 overflow-y-auto overflow-x-hidden">
<ul className="flex md:flex-col gap-1 md:space-y-1"> <div className="mx-auto flex w-full min-w-0 max-w-5xl flex-col gap-6 pb-8">
{sections.map((section) => { {
const Icon = section.icon; <div className="contents">
return ( <Card className="order-2">
<li key={section.name}> <CardHeader>
<button <CardTitle>{t('agents.basicInfo')}</CardTitle>
type="button" <CardDescription>
onClick={() => setActiveSection(section.name)} {t('agents.basicInfoDescription')}
className={cn( </CardDescription>
'w-full flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-colors text-left cursor-pointer whitespace-nowrap', </CardHeader>
activeSection === section.name <CardContent className="space-y-4">
? 'bg-accent text-accent-foreground' <div className="flex gap-4 items-start">
: 'text-muted-foreground hover:bg-muted hover:text-foreground', <FormField
)} control={form.control}
> name="basic.name"
<Icon className="size-4 shrink-0" /> render={({ field }) => (
{section.label} <FormItem className="flex-1">
</button> <FormLabel>
</li> {t('common.name')}
); <span className="text-destructive">*</span>
})} </FormLabel>
</ul> <FormControl>
</nav> <Input
{...field}
value={field.value ?? ''}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="basic.emoji"
render={({ field }) => (
<FormItem>
<FormLabel>{t('common.icon')}</FormLabel>
<FormControl>
<EmojiPicker
value={field.value}
onChange={field.onChange}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex-1 overflow-y-auto min-h-0">
{activeSection === 'basic' && (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>{t('agents.basicInfo')}</CardTitle>
<CardDescription>
{t('agents.basicInfoDescription')}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex gap-4 items-start">
<FormField <FormField
control={form.control} control={form.control}
name="basic.name" name="basic.description"
render={({ field }) => ( render={({ field }) => (
<FormItem className="flex-1"> <FormItem>
<FormLabel> <FormLabel>{t('common.description')}</FormLabel>
{t('common.name')}
<span className="text-destructive">*</span>
</FormLabel>
<FormControl> <FormControl>
<Input {...field} value={field.value ?? ''} /> <Input {...field} value={field.value ?? ''} />
</FormControl> </FormControl>
@@ -506,150 +497,122 @@ export default function AgentFormComponent({
</FormItem> </FormItem>
)} )}
/> />
<FormField <FormField
control={form.control} control={form.control}
name="basic.emoji" name="basic.enabled"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem className="flex items-center justify-between rounded-lg border p-4">
<FormLabel>{t('common.icon')}</FormLabel> <div className="space-y-0.5">
<FormLabel className="flex items-center gap-2">
<Power className="size-4" />
{t('agents.enabled')}
</FormLabel>
<FormDescription>
{t('agents.enabledDescription')}
</FormDescription>
</div>
<FormControl> <FormControl>
<EmojiPicker <Switch
value={field.value} checked={field.value ?? true}
onChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
<FormMessage />
</FormItem> </FormItem>
)} )}
/> />
</div> </CardContent>
</Card>
<Card className="order-4 border-destructive/50">
<CardHeader>
<CardTitle className="text-destructive">
{t('agents.dangerZone')}
</CardTitle>
<CardDescription>
{t('agents.dangerZoneDescription')}
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<div className="space-y-1">
<p className="text-sm font-medium">
{t('agents.deleteAgentAction')}
</p>
<p className="text-sm text-muted-foreground">
{t('agents.deleteAgentHint')}
</p>
</div>
<Button
type="button"
variant="destructive"
size="sm"
disabled={isSaving}
onClick={() => setShowDeleteConfirm(true)}
>
<Trash2 className="size-4 mr-1.5" />
{t('common.delete')}
</Button>
</div>
</CardContent>
</Card>
</div>
}
{
<div className="order-1 space-y-6">
{renderRunnerStatus()}
{runnerConfigSchema?.stages.map((stage) =>
renderDynamicStage(stage),
)}
{!runnerConfigSchema && (
<Card>
<CardHeader>
<CardTitle>{t('agents.runnerSettings')}</CardTitle>
<CardDescription>
{t('agents.noRunnerMetadata')}
</CardDescription>
</CardHeader>
</Card>
)}
</div>
}
{
<Card className="order-3">
<CardHeader>
<CardTitle>{t('agents.bindableEvents')}</CardTitle>
<CardDescription>
{t('agents.bindableEventsDescription')}
</CardDescription>
</CardHeader>
<CardContent>
<FormField <FormField
control={form.control} control={form.control}
name="basic.description" name="supported_event_patterns_text"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t('common.description')}</FormLabel> <FormLabel>
{t('agents.supportedEvents')}
</FormLabel>
<FormControl> <FormControl>
<Input {...field} value={field.value ?? ''} /> <Textarea
{...field}
className="min-h-32 font-mono text-sm"
placeholder={'*\nmessage.received\ngroup.*'}
/>
</FormControl> </FormControl>
<FormDescription>
{t('agents.supportedEventsDescription')}
</FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="basic.enabled"
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel className="flex items-center gap-2">
<Power className="size-4" />
{t('agents.enabled')}
</FormLabel>
<FormDescription>
{t('agents.enabledDescription')}
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value ?? true}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
</CardContent> </CardContent>
</Card> </Card>
}
<Card className="border-destructive/50"> </div>
<CardHeader>
<CardTitle className="text-destructive">
{t('agents.dangerZone')}
</CardTitle>
<CardDescription>
{t('agents.dangerZoneDescription')}
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<div className="space-y-1">
<p className="text-sm font-medium">
{t('agents.deleteAgentAction')}
</p>
<p className="text-sm text-muted-foreground">
{t('agents.deleteAgentHint')}
</p>
</div>
<Button
type="button"
variant="destructive"
size="sm"
disabled={isSaving}
onClick={() => setShowDeleteConfirm(true)}
>
<Trash2 className="size-4 mr-1.5" />
{t('common.delete')}
</Button>
</div>
</CardContent>
</Card>
</div>
)}
{activeSection === 'runner' && (
<div className="space-y-6">
{renderRunnerStatus()}
{runnerConfigSchema?.stages.map((stage) =>
renderDynamicStage(stage),
)}
{!runnerConfigSchema && (
<Card>
<CardHeader>
<CardTitle>{t('agents.runnerSettings')}</CardTitle>
<CardDescription>
{t('agents.noRunnerMetadata')}
</CardDescription>
</CardHeader>
</Card>
)}
</div>
)}
{activeSection === 'events' && (
<Card>
<CardHeader>
<CardTitle>{t('agents.bindableEvents')}</CardTitle>
<CardDescription>
{t('agents.bindableEventsDescription')}
</CardDescription>
</CardHeader>
<CardContent>
<FormField
control={form.control}
name="supported_event_patterns_text"
render={({ field }) => (
<FormItem>
<FormLabel>{t('agents.supportedEvents')}</FormLabel>
<FormControl>
<Textarea
{...field}
className="min-h-32 font-mono text-sm"
placeholder={'*\nmessage.received\ngroup.*'}
/>
</FormControl>
<FormDescription>
{t('agents.supportedEventsDescription')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</CardContent>
</Card>
)}
</div> </div>
</div> </div>
</form> </form>
+7 -7
View File
@@ -133,7 +133,7 @@ export default function BotDetailContent({ id }: { id: string }) {
// ==================== Create Mode ==================== // ==================== Create Mode ====================
if (isCreateMode) { if (isCreateMode) {
return ( return (
<div className="flex h-full flex-col"> <div className="flex h-full min-w-0 flex-col">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between pb-4 shrink-0"> <div className="flex items-center justify-between pb-4 shrink-0">
<h1 className="text-xl font-semibold">{t('bots.createBot')}</h1> <h1 className="text-xl font-semibold">{t('bots.createBot')}</h1>
@@ -145,8 +145,8 @@ export default function BotDetailContent({ id }: { id: string }) {
</div> </div>
{/* Content */} {/* Content */}
<div className="flex-1 overflow-y-auto min-h-0"> <div className="min-h-0 min-w-0 flex-1 overflow-y-auto overflow-x-hidden">
<div className="mx-auto max-w-3xl pb-8"> <div className="mx-auto w-full min-w-0 max-w-3xl pb-8">
<fieldset className="contents" disabled={!canManage}> <fieldset className="contents" disabled={!canManage}>
<BotForm <BotForm
initBotId={undefined} initBotId={undefined}
@@ -163,7 +163,7 @@ export default function BotDetailContent({ id }: { id: string }) {
// ==================== Edit Mode ==================== // ==================== Edit Mode ====================
return ( return (
<> <>
<div className="flex h-full flex-col"> <div className="flex h-full min-w-0 flex-col">
{/* Sticky Header: title + enable switch + save button */} {/* Sticky Header: title + enable switch + save button */}
<div className="flex items-center justify-between pb-4 shrink-0"> <div className="flex items-center justify-between pb-4 shrink-0">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
@@ -202,7 +202,7 @@ export default function BotDetailContent({ id }: { id: string }) {
key={id} key={id}
value={activeTab} value={activeTab}
onValueChange={setActiveTab} onValueChange={setActiveTab}
className="flex flex-1 flex-col min-h-0" className="flex min-h-0 min-w-0 flex-1 flex-col"
> >
<div className="flex shrink-0 items-center gap-1"> <div className="flex shrink-0 items-center gap-1">
<TabsList> <TabsList>
@@ -253,9 +253,9 @@ export default function BotDetailContent({ id }: { id: string }) {
{/* Tab: Configuration */} {/* Tab: Configuration */}
<TabsContent <TabsContent
value="config" value="config"
className="flex-1 min-h-0 overflow-y-auto mt-4" className="mt-4 min-h-0 min-w-0 flex-1 overflow-y-auto overflow-x-hidden"
> >
<div className="mx-auto max-w-3xl space-y-6 pb-8"> <div className="mx-auto w-full min-w-0 max-w-3xl space-y-6 pb-8">
<fieldset className="contents" disabled={!canManage}> <fieldset className="contents" disabled={!canManage}>
<BotForm <BotForm
initBotId={id} initBotId={id}
@@ -410,8 +410,12 @@ export default function BotForm({
id="bot-form" id="bot-form"
onSubmit={form.handleSubmit(onDynamicFormSubmit)} onSubmit={form.handleSubmit(onDynamicFormSubmit)}
aria-busy={isLoading} aria-busy={isLoading}
className="w-full min-w-0 max-w-full"
> >
<fieldset className="space-y-6" disabled={isLoading}> <fieldset
className="w-full min-w-0 max-w-full space-y-6"
disabled={isLoading}
>
{/* Card 1: Basic Information */} {/* Card 1: Basic Information */}
<Card> <Card>
<CardHeader> <CardHeader>
@@ -181,13 +181,15 @@ function EmbedCodeField({
}; };
return ( return (
<div className="space-y-2"> <div className="min-w-0 max-w-full space-y-2">
<label className="text-sm font-medium leading-none">{label}</label> <label className="text-sm font-medium leading-none">{label}</label>
{description && ( {description && (
<p className="text-sm text-muted-foreground">{description}</p> <p className="break-words text-sm text-muted-foreground">
{description}
</p>
)} )}
<div className="flex items-center gap-2"> <div className="flex min-w-0 max-w-full items-center gap-2">
<pre className="flex-1 overflow-x-auto rounded-md bg-muted p-3 text-sm font-mono select-all"> <pre className="min-w-0 max-w-full flex-1 overflow-x-auto rounded-md bg-muted p-3 text-sm font-mono select-all">
<code>{snippet}</code> <code>{snippet}</code>
</pre> </pre>
<Button <Button
+24
View File
@@ -279,6 +279,30 @@ export class BackendClient extends BaseHttpClient {
return this.delete(`/api/v1/agents/${uuid}`); return this.delete(`/api/v1/agents/${uuid}`);
} }
public debugAgent(
uuid: string,
payload: {
event_type: string;
text?: string;
data?: Record<string, unknown>;
conversation_id?: string;
actor?: Record<string, unknown>;
subject?: Record<string, unknown>;
},
): Promise<{
event_id: string;
event_type: string;
conversation_id: string;
final_text: string;
outputs: Array<{
kind: string;
role: string;
text: string;
}>;
}> {
return this.post(`/api/v1/agents/${uuid}/debug`, payload);
}
public getGeneralPipelineMetadata(): Promise<GetPipelineMetadataResponseData> { public getGeneralPipelineMetadata(): Promise<GetPipelineMetadataResponseData> {
// as designed, this method will be deprecated, and only for developer to check the prefered config schema // as designed, this method will be deprecated, and only for developer to check the prefered config schema
return this.get('/api/v1/pipelines/_/metadata'); return this.get('/api/v1/pipelines/_/metadata');
+35
View File
@@ -730,6 +730,41 @@ const enUS = {
runnerReady: 'Runner ready', runnerReady: 'Runner ready',
runnerReadyDescription: runnerReadyDescription:
'{{runner}} is registered and the plugin runtime is connected.', '{{runner}} is registered and the plugin runtime is connected.',
debugTab: 'Event Debug',
debugTitle: 'Agent Event Debug',
debugDescription:
'Run the current Agent with a message or platform event and inspect the real output.',
debugResetSession: 'Reset session',
debugActualRun: 'This is a real run',
debugActualRunDescription:
'The test calls the configured runner, models, and authorized tools, but does not deliver output to a real chat platform.',
debugEventType: 'Event type',
debugMessageReceived: 'Message received',
debugGroupMemberJoined: 'Group member joined',
debugGroupMemberLeft: 'Group member left',
debugFriendRequested: 'Friend request received',
debugFeedbackReceived: 'Feedback received',
debugCustomEvent: 'Custom event',
debugCustomEventType: 'Custom event name',
debugMessageInput: 'Conversation input',
debugEventSummary: 'Event summary',
debugInputPlaceholder: 'Enter what the Agent should handle',
debugEventPayload: 'Event payload (JSON)',
debugSupportedEvents: 'Agent supports',
debugRun: 'Run test',
debugRunning: 'Running',
debugTranscript: 'Debug transcript',
debugTranscriptDescription:
'Inputs and Agent outputs from the current debug session.',
debugEmptyTranscript:
'Choose an event and run a test to see the result here.',
debugAgentOutput: 'Agent output',
debugTestInput: 'Test input',
debugNoTextOutput: 'The run completed without textual output.',
debugEventTypeRequired: 'Enter an event type',
debugInputRequired: 'Enter a conversation input',
debugInvalidPayload: 'The event payload must be a valid JSON object',
debugRunFailed: 'Agent debug run failed',
}, },
plugins: { plugins: {
title: 'Extensions', title: 'Extensions',
+32
View File
@@ -698,6 +698,38 @@ const zhHans = {
noRunnerSelected: '尚未选择运行器', noRunnerSelected: '尚未选择运行器',
runnerReady: '运行器已就绪', runnerReady: '运行器已就绪',
runnerReadyDescription: '{{runner}} 已注册,插件运行时连接正常。', runnerReadyDescription: '{{runner}} 已注册,插件运行时连接正常。',
debugTab: '事件调试',
debugTitle: 'Agent 事件调试',
debugDescription: '用消息或平台事件直接运行当前 Agent,并查看真实输出。',
debugResetSession: '重置会话',
debugActualRun: '这是真实运行',
debugActualRunDescription:
'测试会调用当前运行器、模型与已授权工具,但不会把输出发送到真实聊天平台。',
debugEventType: '事件类型',
debugMessageReceived: '收到消息',
debugGroupMemberJoined: '成员加入群组',
debugGroupMemberLeft: '成员离开群组',
debugFriendRequested: '收到好友请求',
debugFeedbackReceived: '收到反馈',
debugCustomEvent: '自定义事件',
debugCustomEventType: '自定义事件名称',
debugMessageInput: '对话内容',
debugEventSummary: '事件说明',
debugInputPlaceholder: '输入希望 Agent 处理的内容',
debugEventPayload: '事件载荷(JSON',
debugSupportedEvents: 'Agent 支持',
debugRun: '运行测试',
debugRunning: '运行中',
debugTranscript: '调试记录',
debugTranscriptDescription: '当前调试会话中的输入与 Agent 输出。',
debugEmptyTranscript: '选择事件并运行测试后,结果会显示在这里。',
debugAgentOutput: 'Agent 输出',
debugTestInput: '测试输入',
debugNoTextOutput: '运行完成,但没有产生文本输出。',
debugEventTypeRequired: '请输入事件类型',
debugInputRequired: '请输入对话内容',
debugInvalidPayload: '事件载荷必须是有效的 JSON 对象',
debugRunFailed: 'Agent 调试运行失败',
}, },
plugins: { plugins: {
title: '插件扩展', title: '插件扩展',