mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-06 17:47:14 +00:00
feat(agent-debug): edit event data with compact forms
This commit is contained in:
@@ -1,4 +1,11 @@
|
||||
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
@@ -40,6 +47,13 @@ import {
|
||||
} from '@/app/home/components/event-patterns/event-pattern-groups';
|
||||
import EventSelectOptionContent from '@/app/home/components/event-patterns/EventSelectOptionContent';
|
||||
import AgentExecutionTrace from './AgentExecutionTrace';
|
||||
import AgentEventDataEditor from './AgentEventDataEditor';
|
||||
import {
|
||||
createDebugEventData,
|
||||
debugEventInputText,
|
||||
invalidDebugEventField,
|
||||
parseDebugEventData,
|
||||
} from './debug-event-data';
|
||||
import { executionSteps, type DebugExecutionEvent } from './debug-execution';
|
||||
|
||||
interface AgentDebugPanelProps {
|
||||
@@ -63,104 +77,6 @@ interface DebugEntry {
|
||||
finished?: boolean;
|
||||
}
|
||||
|
||||
const EVENT_PRESET_DATA: Record<
|
||||
string,
|
||||
{ text: string; data: Record<string, unknown> }
|
||||
> = {
|
||||
'message.received': {
|
||||
text: '',
|
||||
data: {},
|
||||
},
|
||||
'group.member_joined': {
|
||||
text: 'A new member joined the group.',
|
||||
data: {
|
||||
group_id: 'debug-group',
|
||||
member_id: 'debug-user',
|
||||
member_name: 'Debug User',
|
||||
},
|
||||
},
|
||||
'group.member_left': {
|
||||
text: 'A member left the group.',
|
||||
data: {
|
||||
group_id: 'debug-group',
|
||||
member_id: 'debug-user',
|
||||
member_name: 'Debug User',
|
||||
},
|
||||
},
|
||||
'friend.request_received': {
|
||||
text: 'A user sent a friend request.',
|
||||
data: {
|
||||
requester_id: 'debug-user',
|
||||
requester_name: 'Debug User',
|
||||
request_id: 'debug-friend-request',
|
||||
message: 'Hello',
|
||||
},
|
||||
},
|
||||
'feedback.received': {
|
||||
text: 'The user submitted feedback.',
|
||||
data: {
|
||||
rating: 5,
|
||||
content: 'Debug feedback',
|
||||
},
|
||||
},
|
||||
'friend.added': {
|
||||
text: 'A friend was added.',
|
||||
data: { user_id: 'debug-user', user_name: 'Debug User' },
|
||||
},
|
||||
'group.member_banned': {
|
||||
text: 'A member was banned.',
|
||||
data: {
|
||||
group_id: 'debug-group',
|
||||
member_id: 'debug-user',
|
||||
member_name: 'Debug User',
|
||||
},
|
||||
},
|
||||
'bot.invited_to_group': {
|
||||
text: 'The bot was invited to a group.',
|
||||
data: {
|
||||
group_id: 'debug-group',
|
||||
request_id: 'debug-group-request',
|
||||
requester_id: 'debug-user',
|
||||
},
|
||||
},
|
||||
'bot.muted': {
|
||||
text: 'The bot was muted.',
|
||||
data: { group_id: 'debug-group', duration: 60 },
|
||||
},
|
||||
'bot.unmuted': {
|
||||
text: 'The bot was unmuted.',
|
||||
data: { group_id: 'debug-group' },
|
||||
},
|
||||
'bot.removed_from_group': {
|
||||
text: 'The bot was removed from the group.',
|
||||
data: { group_id: 'debug-group' },
|
||||
},
|
||||
'message.edited': {
|
||||
text: 'A message was edited.',
|
||||
data: {
|
||||
group_id: 'debug-group',
|
||||
message_id: 'debug-message',
|
||||
text: 'Edited message',
|
||||
},
|
||||
},
|
||||
'message.deleted': {
|
||||
text: 'A message was deleted.',
|
||||
data: { group_id: 'debug-group', message_id: 'debug-message' },
|
||||
},
|
||||
'message.reaction': {
|
||||
text: 'A reaction was added.',
|
||||
data: {
|
||||
group_id: 'debug-group',
|
||||
message_id: 'debug-message',
|
||||
reaction: '👍',
|
||||
},
|
||||
},
|
||||
'platform.specific': {
|
||||
text: 'A platform-specific event occurred.',
|
||||
data: { event_name: 'debug-platform-event' },
|
||||
},
|
||||
};
|
||||
|
||||
function createDebugSessionId(agentId: string) {
|
||||
const nonce = globalThis.crypto?.randomUUID?.() ?? String(Date.now());
|
||||
return `webui:${agentId}:${nonce}`;
|
||||
@@ -186,8 +102,22 @@ export default function AgentDebugPanel({
|
||||
);
|
||||
const [preset, setPreset] = useState('message.received');
|
||||
const [customEventType, setCustomEventType] = useState('custom.event');
|
||||
const [inputText, setInputText] = useState('');
|
||||
const [eventDataText, setEventDataText] = useState('{}');
|
||||
const newEventData = useCallback(
|
||||
(type: string) =>
|
||||
JSON.stringify(
|
||||
createDebugEventData(type, {
|
||||
user: t('agents.debugData.sampleUser'),
|
||||
message: t('agents.debugData.sampleMessage'),
|
||||
feedback: t('agents.debugData.sampleFeedback'),
|
||||
}),
|
||||
null,
|
||||
2,
|
||||
),
|
||||
[t],
|
||||
);
|
||||
const [eventDataText, setEventDataText] = useState(() =>
|
||||
newEventData('message.received'),
|
||||
);
|
||||
const [mockOptionsText, setMockOptionsText] = useState('{}');
|
||||
const [running, setRunning] = useState(false);
|
||||
const [entries, setEntries] = useState<DebugEntry[]>([]);
|
||||
@@ -205,11 +135,7 @@ export default function AgentDebugPanel({
|
||||
}, [entries]);
|
||||
|
||||
const eventType = preset === 'custom' ? customEventType.trim() : preset;
|
||||
const isMessageEvent = eventType.startsWith('message.');
|
||||
const supportedLabel = useMemo(
|
||||
() => supportedEventPatterns.join(', '),
|
||||
[supportedEventPatterns],
|
||||
);
|
||||
const eventDataValid = parseDebugEventData(eventDataText) !== null;
|
||||
const availableEvents = useMemo(() => {
|
||||
const concretePatterns = supportedEventPatterns.filter(
|
||||
(pattern) => pattern !== '*' && !pattern.endsWith('.*'),
|
||||
@@ -230,6 +156,14 @@ export default function AgentDebugPanel({
|
||||
(pattern) => pattern === '*' || pattern.endsWith('.*'),
|
||||
);
|
||||
|
||||
const selectPreset = useCallback(
|
||||
(value: string) => {
|
||||
setPreset(value);
|
||||
setEventDataText(newEventData(value));
|
||||
},
|
||||
[newEventData],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
availableEvents.includes(preset) ||
|
||||
@@ -238,24 +172,13 @@ export default function AgentDebugPanel({
|
||||
return;
|
||||
}
|
||||
selectPreset(availableEvents[0] ?? 'custom');
|
||||
}, [availableEvents, preset, supportsCustomEvent]);
|
||||
|
||||
function selectPreset(value: string) {
|
||||
setPreset(value);
|
||||
const nextPreset = EVENT_PRESET_DATA[value] ?? { text: '', data: {} };
|
||||
setInputText(nextPreset.text);
|
||||
setEventDataText(JSON.stringify(nextPreset.data, null, 2));
|
||||
}
|
||||
}, [availableEvents, preset, supportsCustomEvent, selectPreset]);
|
||||
|
||||
async function runDebugEvent() {
|
||||
if (!eventType) {
|
||||
toast.error(t('agents.debugEventTypeRequired'));
|
||||
return;
|
||||
}
|
||||
if (isMessageEvent && !inputText.trim()) {
|
||||
toast.error(t('agents.debugInputRequired'));
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!supportedEventPatterns.some((pattern) =>
|
||||
matchesEventPattern(pattern, eventType),
|
||||
@@ -265,18 +188,22 @@ export default function AgentDebugPanel({
|
||||
return;
|
||||
}
|
||||
|
||||
let eventData: Record<string, unknown>;
|
||||
let mockOptions: 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 {
|
||||
const eventData = parseDebugEventData(eventDataText);
|
||||
if (!eventData) {
|
||||
toast.error(t('agents.debugInvalidPayload'));
|
||||
return;
|
||||
}
|
||||
const invalidField = invalidDebugEventField(eventType, eventData);
|
||||
if (invalidField) {
|
||||
toast.error(
|
||||
t('agents.debugData.invalidField', {
|
||||
field: t(`agents.debugData.${invalidField.label}`),
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const inputText = debugEventInputText(eventType, eventData);
|
||||
let mockOptions: Record<string, unknown>;
|
||||
try {
|
||||
const parsed = JSON.parse(mockOptionsText || '{}');
|
||||
if (!parsed || Array.isArray(parsed) || typeof parsed !== 'object')
|
||||
@@ -367,8 +294,6 @@ export default function AgentDebugPanel({
|
||||
},
|
||||
],
|
||||
);
|
||||
if (isMessageEvent)
|
||||
setInputText((current) => (current === inputText ? '' : current));
|
||||
} catch (error) {
|
||||
if (controller.signal.aborted) {
|
||||
if (controller.signal.reason === 'user') {
|
||||
@@ -541,131 +466,115 @@ export default function AgentDebugPanel({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 space-y-3 border-t p-3">
|
||||
{supportedEventPatterns.length === 0 ? (
|
||||
<Alert className="bg-amber-500/5 text-amber-800 dark:text-amber-200">
|
||||
<AlertTriangle className="size-4" />
|
||||
<AlertTitle>{t('agents.debugNoEventsTitle')}</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t('agents.debugNoEventsDescription')}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : (
|
||||
<>
|
||||
<div className="space-y-1.5">
|
||||
<Label>{t('agents.debugEventType')}</Label>
|
||||
<Select value={preset} onValueChange={selectPreset}>
|
||||
<SelectTrigger
|
||||
className="w-full"
|
||||
aria-label={t('agents.debugEventType')}
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="w-[var(--radix-select-trigger-width)] max-w-[calc(100vw-2rem)]">
|
||||
{eventGroups.map((group) => (
|
||||
<SelectGroup key={group.namespace}>
|
||||
<SelectLabel>
|
||||
{eventGroupLabel(group.namespace, t)}
|
||||
</SelectLabel>
|
||||
{group.patterns.map((event) => (
|
||||
<div className="flex max-h-[60%] min-h-0 shrink-0 flex-col border-t">
|
||||
<div className="min-h-0 space-y-3 overflow-y-auto p-3">
|
||||
{supportedEventPatterns.length === 0 ? (
|
||||
<Alert className="bg-amber-500/5 text-amber-800 dark:text-amber-200">
|
||||
<AlertTriangle className="size-4" />
|
||||
<AlertTitle>{t('agents.debugNoEventsTitle')}</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t('agents.debugNoEventsDescription')}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : (
|
||||
<>
|
||||
<div className="space-y-1.5">
|
||||
<Label>{t('agents.debugEventType')}</Label>
|
||||
<Select value={preset} onValueChange={selectPreset}>
|
||||
<SelectTrigger
|
||||
className="w-full"
|
||||
aria-label={t('agents.debugEventType')}
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="w-[var(--radix-select-trigger-width)] max-w-[calc(100vw-2rem)]">
|
||||
{eventGroups.map((group) => (
|
||||
<SelectGroup key={group.namespace}>
|
||||
<SelectLabel>
|
||||
{eventGroupLabel(group.namespace, t)}
|
||||
</SelectLabel>
|
||||
{group.patterns.map((event) => (
|
||||
<SelectItem
|
||||
key={event}
|
||||
value={event}
|
||||
description={eventPatternDescription(event, t)}
|
||||
className="py-2"
|
||||
>
|
||||
<EventSelectOptionContent
|
||||
event={event}
|
||||
label={eventPatternLabel(event, t)}
|
||||
/>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
))}
|
||||
{supportsCustomEvent && (
|
||||
<SelectGroup>
|
||||
<SelectLabel>
|
||||
{t('agents.debugCustomEvent')}
|
||||
</SelectLabel>
|
||||
<SelectItem
|
||||
key={event}
|
||||
value={event}
|
||||
description={eventPatternDescription(event, t)}
|
||||
value="custom"
|
||||
description={t('bots.eventDescriptions.custom')}
|
||||
className="py-2"
|
||||
>
|
||||
<EventSelectOptionContent
|
||||
event={event}
|
||||
label={eventPatternLabel(event, t)}
|
||||
event="custom.event"
|
||||
label={t('agents.debugCustomEvent')}
|
||||
/>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
))}
|
||||
{supportsCustomEvent && (
|
||||
<SelectGroup>
|
||||
<SelectLabel>{t('agents.debugCustomEvent')}</SelectLabel>
|
||||
<SelectItem
|
||||
value="custom"
|
||||
description={t('bots.eventDescriptions.custom')}
|
||||
className="py-2"
|
||||
>
|
||||
<EventSelectOptionContent
|
||||
event="custom.event"
|
||||
label={t('agents.debugCustomEvent')}
|
||||
/>
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{preset === 'custom' && (
|
||||
<div className="space-y-1.5">
|
||||
<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"
|
||||
/>
|
||||
</SelectGroup>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<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-20 resize-y"
|
||||
placeholder={t('agents.debugInputPlaceholder')}
|
||||
{preset === 'custom' && (
|
||||
<div className="space-y-1.5">
|
||||
<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>
|
||||
)}
|
||||
|
||||
<AgentEventDataEditor
|
||||
key={eventType}
|
||||
eventType={eventType}
|
||||
custom={preset === 'custom'}
|
||||
value={eventDataText}
|
||||
onChange={setEventDataText}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<details className="rounded-md border bg-muted/20 px-3 py-2">
|
||||
<summary className="cursor-pointer text-xs font-medium">
|
||||
{t('agents.debugEventPayload')}
|
||||
</summary>
|
||||
<div className="mt-2 space-y-2">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t('agents.debugSupportedEvents')}: {supportedLabel}
|
||||
<details className="text-muted-foreground">
|
||||
<summary className="cursor-pointer text-xs font-medium">
|
||||
{t('agents.debugMockOptions')}
|
||||
</summary>
|
||||
<p className="my-2 text-xs text-muted-foreground">
|
||||
{t('agents.debugMockOptionsHelp')}
|
||||
</p>
|
||||
<Textarea
|
||||
id="agent-debug-payload"
|
||||
value={eventDataText}
|
||||
onChange={(event) => setEventDataText(event.target.value)}
|
||||
className="min-h-28 resize-y font-mono text-xs"
|
||||
aria-label={t('agents.debugMockOptions')}
|
||||
value={mockOptionsText}
|
||||
onChange={(event) => setMockOptionsText(event.target.value)}
|
||||
className="min-h-24 font-mono text-xs"
|
||||
spellCheck={false}
|
||||
/>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details className="rounded-md border bg-muted/20 px-3 py-2">
|
||||
<summary className="cursor-pointer text-xs font-medium">
|
||||
{t('agents.debugMockOptions')}
|
||||
</summary>
|
||||
<p className="my-2 text-xs text-muted-foreground">
|
||||
{t('agents.debugMockOptionsHelp')}
|
||||
</p>
|
||||
<Textarea
|
||||
aria-label={t('agents.debugMockOptions')}
|
||||
value={mockOptionsText}
|
||||
onChange={(event) => setMockOptionsText(event.target.value)}
|
||||
className="min-h-24 font-mono text-xs"
|
||||
spellCheck={false}
|
||||
/>
|
||||
</details>
|
||||
</details>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{supportedEventPatterns.length > 0 && (
|
||||
<div className="shrink-0 px-3 pb-3">
|
||||
<Button
|
||||
type="button"
|
||||
className="w-full"
|
||||
disabled={!running && !eventDataValid}
|
||||
onClick={() =>
|
||||
running ? requestRef.current?.abort('user') : runDebugEvent()
|
||||
}
|
||||
@@ -681,7 +590,7 @@ export default function AgentDebugPanel({
|
||||
? t('agents.debugSaveAndRun')
|
||||
: t('agents.debugRun')}
|
||||
</Button>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ArrowLeftRight } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { debugEventDefinition, parseDebugEventData } from './debug-event-data';
|
||||
|
||||
export default function AgentEventDataEditor({
|
||||
eventType,
|
||||
value,
|
||||
onChange,
|
||||
custom = false,
|
||||
}: {
|
||||
eventType: string;
|
||||
custom?: boolean;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [showJson, setShowJson] = useState(false);
|
||||
const fields = custom ? [] : (debugEventDefinition(eventType)?.fields ?? []);
|
||||
const data = parseDebugEventData(value);
|
||||
const jsonMode = showJson || !fields.length;
|
||||
|
||||
function updateField(key: string, next: string | number | undefined) {
|
||||
if (data) onChange(JSON.stringify({ ...data, [key]: next }, null, 2));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-xs font-medium">
|
||||
{t('agents.debugData.title')}
|
||||
</span>
|
||||
{fields.length > 0 && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 gap-1.5 px-1.5 text-xs text-muted-foreground"
|
||||
disabled={jsonMode && !data}
|
||||
onClick={() => setShowJson(!showJson)}
|
||||
>
|
||||
<ArrowLeftRight className="size-3.5" aria-hidden="true" />
|
||||
{t(jsonMode ? 'agents.debugData.form' : 'agents.debugData.json')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{jsonMode ? (
|
||||
<>
|
||||
<Textarea
|
||||
aria-label={t('agents.debugData.json')}
|
||||
value={value}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
rows={6}
|
||||
className="min-h-28 font-mono text-xs"
|
||||
spellCheck={false}
|
||||
aria-invalid={!data}
|
||||
/>
|
||||
{!data && (
|
||||
<p role="alert" className="text-xs text-destructive">
|
||||
{t('agents.debugInvalidPayload')}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 gap-x-3 gap-y-2">
|
||||
{fields.map((field) => {
|
||||
const id = `agent-debug-data-${field.key}`;
|
||||
const current = data?.[field.key];
|
||||
const text =
|
||||
typeof current === 'string' || typeof current === 'number'
|
||||
? current
|
||||
: '';
|
||||
return (
|
||||
<div
|
||||
key={field.key}
|
||||
className={`min-w-0 space-y-1 ${field.multiline ? 'col-span-2' : ''}`}
|
||||
>
|
||||
<Label htmlFor={id} className="text-xs text-muted-foreground">
|
||||
{t(`agents.debugData.${field.label}`)}
|
||||
</Label>
|
||||
{field.multiline ? (
|
||||
<Textarea
|
||||
id={id}
|
||||
value={text}
|
||||
rows={2}
|
||||
required={field.required}
|
||||
onChange={(event) =>
|
||||
updateField(field.key, event.target.value)
|
||||
}
|
||||
className="min-h-16 resize-y text-sm"
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
id={id}
|
||||
value={text}
|
||||
type={typeof field.value === 'number' ? 'number' : 'text'}
|
||||
min={field.min}
|
||||
max={field.max}
|
||||
step={1}
|
||||
className="h-8 text-sm"
|
||||
placeholder={
|
||||
field.placeholder
|
||||
? t(`agents.debugData.${field.placeholder}`)
|
||||
: undefined
|
||||
}
|
||||
onChange={(event) =>
|
||||
updateField(
|
||||
field.key,
|
||||
typeof field.value === 'number'
|
||||
? event.target.value === ''
|
||||
? undefined
|
||||
: Number(event.target.value)
|
||||
: event.target.value,
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
export interface DebugEventField {
|
||||
key: string;
|
||||
label: string;
|
||||
value: string | number;
|
||||
sample?: 'user' | 'message' | 'feedback';
|
||||
multiline?: boolean;
|
||||
required?: boolean;
|
||||
min?: number;
|
||||
max?: number;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
interface DebugEventDefinition {
|
||||
fields: DebugEventField[];
|
||||
defaults?: Record<string, unknown>;
|
||||
messageField?: string;
|
||||
}
|
||||
|
||||
const group: DebugEventField = {
|
||||
key: 'group_id',
|
||||
label: 'groupId',
|
||||
value: 'debug-group',
|
||||
};
|
||||
const member: DebugEventField = {
|
||||
key: 'member_id',
|
||||
label: 'memberId',
|
||||
value: 'debug-user',
|
||||
};
|
||||
const memberName: DebugEventField = {
|
||||
key: 'member_name',
|
||||
label: 'memberName',
|
||||
value: '',
|
||||
sample: 'user',
|
||||
};
|
||||
const user: DebugEventField = {
|
||||
key: 'user_id',
|
||||
label: 'userId',
|
||||
value: 'debug-user',
|
||||
};
|
||||
const userName: DebugEventField = {
|
||||
key: 'user_name',
|
||||
label: 'userName',
|
||||
value: '',
|
||||
sample: 'user',
|
||||
};
|
||||
const requester: DebugEventField = {
|
||||
key: 'requester_id',
|
||||
label: 'requesterId',
|
||||
value: 'debug-user',
|
||||
};
|
||||
const messageId: DebugEventField = {
|
||||
key: 'message_id',
|
||||
label: 'messageId',
|
||||
value: 'debug-message',
|
||||
};
|
||||
const duration: DebugEventField = {
|
||||
key: 'duration',
|
||||
label: 'duration',
|
||||
value: 60,
|
||||
min: 0,
|
||||
};
|
||||
const message: DebugEventField = {
|
||||
key: 'text',
|
||||
label: 'message',
|
||||
value: '',
|
||||
sample: 'message',
|
||||
multiline: true,
|
||||
required: true,
|
||||
};
|
||||
const membership: DebugEventDefinition = {
|
||||
fields: [memberName, member, group],
|
||||
};
|
||||
const friendship: DebugEventDefinition = { fields: [userName, user] };
|
||||
const deletion: DebugEventDefinition = { fields: [messageId, group] };
|
||||
|
||||
// The compact form projects the existing debug API data fields. Fields omitted
|
||||
// from the form, including custom fields, remain editable in the same JSON data.
|
||||
const DEBUG_EVENT_DEFINITIONS: Record<string, DebugEventDefinition> = {
|
||||
'message.received': {
|
||||
fields: [
|
||||
message,
|
||||
userName,
|
||||
{ ...group, value: '', placeholder: 'privateChat' },
|
||||
],
|
||||
defaults: { user_id: 'debug-user' },
|
||||
messageField: 'text',
|
||||
},
|
||||
'message.edited': {
|
||||
fields: [{ ...message, label: 'newMessage' }, messageId, group],
|
||||
messageField: 'text',
|
||||
},
|
||||
'message.deleted': deletion,
|
||||
'message.recalled': deletion,
|
||||
'message.reaction': {
|
||||
fields: [
|
||||
{ key: 'reaction', label: 'reaction', value: '👍' },
|
||||
messageId,
|
||||
group,
|
||||
],
|
||||
defaults: { is_add: true },
|
||||
},
|
||||
'group.member_joined': membership,
|
||||
'group.member_left': membership,
|
||||
'group.member_banned': { fields: [member, group, duration] },
|
||||
'group.info_updated': {
|
||||
fields: [
|
||||
{ key: 'group_name', label: 'groupName', value: 'debug-group' },
|
||||
group,
|
||||
],
|
||||
},
|
||||
'friend.request_received': {
|
||||
fields: [
|
||||
{
|
||||
key: 'message',
|
||||
label: 'verificationMessage',
|
||||
value: '',
|
||||
sample: 'message',
|
||||
multiline: true,
|
||||
},
|
||||
{
|
||||
key: 'requester_name',
|
||||
label: 'requesterName',
|
||||
value: '',
|
||||
sample: 'user',
|
||||
},
|
||||
requester,
|
||||
],
|
||||
defaults: { request_id: 'debug-friend-request' },
|
||||
},
|
||||
'friend.added': friendship,
|
||||
'friend.removed': friendship,
|
||||
'bot.invited_to_group': {
|
||||
fields: [group, requester],
|
||||
defaults: { request_id: 'debug-group-request' },
|
||||
},
|
||||
'bot.muted': { fields: [group, duration] },
|
||||
'bot.unmuted': { fields: [group] },
|
||||
'bot.removed_from_group': { fields: [group] },
|
||||
'feedback.received': {
|
||||
fields: [
|
||||
{
|
||||
key: 'content',
|
||||
label: 'feedback',
|
||||
value: '',
|
||||
sample: 'feedback',
|
||||
multiline: true,
|
||||
},
|
||||
{ key: 'rating', label: 'rating', value: 5, min: 1, max: 5 },
|
||||
],
|
||||
},
|
||||
'platform.specific': {
|
||||
fields: [
|
||||
{ key: 'event_name', label: 'eventName', value: 'debug-platform-event' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export function debugEventDefinition(
|
||||
eventType: string,
|
||||
): DebugEventDefinition | undefined {
|
||||
return Object.hasOwn(DEBUG_EVENT_DEFINITIONS, eventType)
|
||||
? DEBUG_EVENT_DEFINITIONS[eventType]
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export function createDebugEventData(
|
||||
eventType: string,
|
||||
samples: Record<'user' | 'message' | 'feedback', string>,
|
||||
) {
|
||||
const definition = debugEventDefinition(eventType);
|
||||
return {
|
||||
...definition?.defaults,
|
||||
...Object.fromEntries(
|
||||
(definition?.fields ?? []).map((field) => [
|
||||
field.key,
|
||||
field.sample ? samples[field.sample] : field.value,
|
||||
]),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export function parseDebugEventData(
|
||||
text: string,
|
||||
): Record<string, unknown> | null {
|
||||
try {
|
||||
const value = JSON.parse(text);
|
||||
return value && !Array.isArray(value) && typeof value === 'object'
|
||||
? value
|
||||
: null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function invalidDebugEventField(
|
||||
eventType: string,
|
||||
data: Record<string, unknown>,
|
||||
) {
|
||||
return debugEventDefinition(eventType)?.fields.find((field) => {
|
||||
const value = data[field.key];
|
||||
if (
|
||||
value === undefined ||
|
||||
value === null ||
|
||||
(typeof value === 'string' && !value.trim())
|
||||
)
|
||||
return field.required;
|
||||
if (typeof field.value === 'number') {
|
||||
return (
|
||||
typeof value !== 'number' ||
|
||||
!Number.isFinite(value) ||
|
||||
!Number.isInteger(value) ||
|
||||
(field.min !== undefined && value < field.min) ||
|
||||
(field.max !== undefined && value > field.max)
|
||||
);
|
||||
}
|
||||
return (
|
||||
typeof value !== 'string' &&
|
||||
!(field.key.endsWith('_id') && typeof value === 'number')
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function debugEventInputText(
|
||||
eventType: string,
|
||||
data: Record<string, unknown>,
|
||||
) {
|
||||
const key = debugEventDefinition(eventType)?.messageField;
|
||||
return key && typeof data[key] === 'string' ? data[key].trim() : '';
|
||||
}
|
||||
@@ -704,6 +704,33 @@ const enUS = {
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
debugData: {
|
||||
title: 'Event data',
|
||||
form: 'Common fields',
|
||||
json: 'Full JSON',
|
||||
groupId: 'Group ID',
|
||||
memberId: 'Member ID',
|
||||
memberName: 'Member name',
|
||||
userId: 'User ID',
|
||||
userName: 'User name',
|
||||
requesterId: 'Requester ID',
|
||||
requesterName: 'Requester name',
|
||||
messageId: 'Message ID',
|
||||
duration: 'Duration (seconds)',
|
||||
message: 'Message content',
|
||||
newMessage: 'New message content',
|
||||
verificationMessage: 'Verification message',
|
||||
reaction: 'Reaction',
|
||||
groupName: 'Group name',
|
||||
feedback: 'Feedback',
|
||||
rating: 'Rating',
|
||||
eventName: 'Event name',
|
||||
privateChat: 'Leave empty for private chat',
|
||||
sampleUser: 'Test User',
|
||||
sampleMessage: 'Hello',
|
||||
sampleFeedback: 'Very helpful',
|
||||
invalidField: 'Check {{field}}',
|
||||
},
|
||||
title: 'Processors',
|
||||
description: 'Create reusable processors and use them in bot event routing',
|
||||
create: 'Create Processor',
|
||||
|
||||
@@ -506,6 +506,33 @@ const esES = {
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
debugData: {
|
||||
title: 'Datos del evento',
|
||||
form: 'Campos comunes',
|
||||
json: 'JSON completo',
|
||||
groupId: 'ID del grupo',
|
||||
memberId: 'ID del miembro',
|
||||
memberName: 'Nombre del miembro',
|
||||
userId: 'ID del usuario',
|
||||
userName: 'Nombre del usuario',
|
||||
requesterId: 'ID del solicitante',
|
||||
requesterName: 'Nombre del solicitante',
|
||||
messageId: 'ID del mensaje',
|
||||
duration: 'Duración (segundos)',
|
||||
message: 'Contenido del mensaje',
|
||||
newMessage: 'Contenido editado',
|
||||
verificationMessage: 'Mensaje de verificación',
|
||||
reaction: 'Reacción',
|
||||
groupName: 'Nombre del grupo',
|
||||
feedback: 'Comentarios',
|
||||
rating: 'Puntuación',
|
||||
eventName: 'Nombre del evento',
|
||||
privateChat: 'Vacío para chat privado',
|
||||
sampleUser: 'Usuario de prueba',
|
||||
sampleMessage: 'Hola',
|
||||
sampleFeedback: 'Muy útil',
|
||||
invalidField: 'Revisa {{field}}',
|
||||
},
|
||||
title: 'Procesadores',
|
||||
description:
|
||||
'Crea procesadores reutilizables y úsalos en el enrutamiento de eventos del bot',
|
||||
|
||||
@@ -716,6 +716,33 @@ const jaJP = {
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
debugData: {
|
||||
title: 'イベントデータ',
|
||||
form: '基本項目',
|
||||
json: '完全な JSON',
|
||||
groupId: 'グループ ID',
|
||||
memberId: 'メンバー ID',
|
||||
memberName: 'メンバー名',
|
||||
userId: 'ユーザー ID',
|
||||
userName: 'ユーザー名',
|
||||
requesterId: '申請者 ID',
|
||||
requesterName: '申請者名',
|
||||
messageId: 'メッセージ ID',
|
||||
duration: '時間(秒)',
|
||||
message: 'メッセージ内容',
|
||||
newMessage: '編集後の内容',
|
||||
verificationMessage: '申請メッセージ',
|
||||
reaction: 'リアクション',
|
||||
groupName: 'グループ名',
|
||||
feedback: 'フィードバック',
|
||||
rating: '評価',
|
||||
eventName: 'イベント名',
|
||||
privateChat: '空欄で個別チャット',
|
||||
sampleUser: 'テストユーザー',
|
||||
sampleMessage: 'こんにちは',
|
||||
sampleFeedback: 'とても役立ちました',
|
||||
invalidField: '{{field}}を確認してください',
|
||||
},
|
||||
debugReasoning: '思考内容',
|
||||
debugTextOutput: 'テキスト出力',
|
||||
debugPlatformNotice:
|
||||
|
||||
@@ -503,6 +503,33 @@ const ruRU = {
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
debugData: {
|
||||
title: 'Данные события',
|
||||
form: 'Основные поля',
|
||||
json: 'Полный JSON',
|
||||
groupId: 'ID группы',
|
||||
memberId: 'ID участника',
|
||||
memberName: 'Имя участника',
|
||||
userId: 'ID пользователя',
|
||||
userName: 'Имя пользователя',
|
||||
requesterId: 'ID заявителя',
|
||||
requesterName: 'Имя заявителя',
|
||||
messageId: 'ID сообщения',
|
||||
duration: 'Длительность (секунды)',
|
||||
message: 'Текст сообщения',
|
||||
newMessage: 'Новый текст',
|
||||
verificationMessage: 'Текст заявки',
|
||||
reaction: 'Реакция',
|
||||
groupName: 'Название группы',
|
||||
feedback: 'Отзыв',
|
||||
rating: 'Оценка',
|
||||
eventName: 'Название события',
|
||||
privateChat: 'Пусто для личного чата',
|
||||
sampleUser: 'Тестовый пользователь',
|
||||
sampleMessage: 'Привет',
|
||||
sampleFeedback: 'Очень полезно',
|
||||
invalidField: 'Проверьте {{field}}',
|
||||
},
|
||||
title: 'Обработчики',
|
||||
description:
|
||||
'Создавайте переиспользуемые обработчики и используйте их в маршрутизации событий бота',
|
||||
|
||||
@@ -490,6 +490,33 @@ const thTH = {
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
debugData: {
|
||||
title: 'ข้อมูลเหตุการณ์',
|
||||
form: 'ฟิลด์ทั่วไป',
|
||||
json: 'JSON ทั้งหมด',
|
||||
groupId: 'ID กลุ่ม',
|
||||
memberId: 'ID สมาชิก',
|
||||
memberName: 'ชื่อสมาชิก',
|
||||
userId: 'ID ผู้ใช้',
|
||||
userName: 'ชื่อผู้ใช้',
|
||||
requesterId: 'ID ผู้ส่งคำขอ',
|
||||
requesterName: 'ชื่อผู้ส่งคำขอ',
|
||||
messageId: 'ID ข้อความ',
|
||||
duration: 'ระยะเวลา (วินาที)',
|
||||
message: 'เนื้อหาข้อความ',
|
||||
newMessage: 'เนื้อหาที่แก้ไข',
|
||||
verificationMessage: 'ข้อความยืนยัน',
|
||||
reaction: 'อีโมจิ',
|
||||
groupName: 'ชื่อกลุ่ม',
|
||||
feedback: 'ความคิดเห็น',
|
||||
rating: 'คะแนน',
|
||||
eventName: 'ชื่อเหตุการณ์',
|
||||
privateChat: 'เว้นว่างสำหรับแชทส่วนตัว',
|
||||
sampleUser: 'ผู้ใช้ทดสอบ',
|
||||
sampleMessage: 'สวัสดี',
|
||||
sampleFeedback: 'มีประโยชน์มาก',
|
||||
invalidField: 'โปรดตรวจสอบ {{field}}',
|
||||
},
|
||||
title: 'ตัวประมวลผล',
|
||||
description: 'สร้างตัวประมวลผลที่ใช้ซ้ำได้และใช้ในเส้นทางเหตุการณ์ของบอท',
|
||||
create: 'สร้างตัวประมวลผล',
|
||||
|
||||
@@ -499,6 +499,33 @@ const viVN = {
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
debugData: {
|
||||
title: 'Dữ liệu sự kiện',
|
||||
form: 'Trường thường dùng',
|
||||
json: 'JSON đầy đủ',
|
||||
groupId: 'ID nhóm',
|
||||
memberId: 'ID thành viên',
|
||||
memberName: 'Tên thành viên',
|
||||
userId: 'ID người dùng',
|
||||
userName: 'Tên người dùng',
|
||||
requesterId: 'ID người yêu cầu',
|
||||
requesterName: 'Tên người yêu cầu',
|
||||
messageId: 'ID tin nhắn',
|
||||
duration: 'Thời lượng (giây)',
|
||||
message: 'Nội dung tin nhắn',
|
||||
newMessage: 'Nội dung đã sửa',
|
||||
verificationMessage: 'Tin nhắn xác minh',
|
||||
reaction: 'Biểu cảm',
|
||||
groupName: 'Tên nhóm',
|
||||
feedback: 'Phản hồi',
|
||||
rating: 'Đánh giá',
|
||||
eventName: 'Tên sự kiện',
|
||||
privateChat: 'Để trống cho trò chuyện riêng',
|
||||
sampleUser: 'Người dùng thử',
|
||||
sampleMessage: 'Xin chào',
|
||||
sampleFeedback: 'Rất hữu ích',
|
||||
invalidField: 'Kiểm tra {{field}}',
|
||||
},
|
||||
title: 'Bộ xử lý',
|
||||
description:
|
||||
'Tạo bộ xử lý có thể tái sử dụng và dùng chúng trong định tuyến sự kiện của bot',
|
||||
|
||||
@@ -670,6 +670,33 @@ const zhHans = {
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
debugData: {
|
||||
title: '事件数据',
|
||||
form: '常用字段',
|
||||
json: '完整 JSON',
|
||||
groupId: '群组 ID',
|
||||
memberId: '成员 ID',
|
||||
memberName: '成员昵称',
|
||||
userId: '用户 ID',
|
||||
userName: '用户昵称',
|
||||
requesterId: '申请者 ID',
|
||||
requesterName: '申请者昵称',
|
||||
messageId: '消息 ID',
|
||||
duration: '时长(秒)',
|
||||
message: '消息内容',
|
||||
newMessage: '修改后的内容',
|
||||
verificationMessage: '验证消息',
|
||||
reaction: '表情',
|
||||
groupName: '群组名称',
|
||||
feedback: '反馈内容',
|
||||
rating: '评分',
|
||||
eventName: '事件名称',
|
||||
privateChat: '留空为私聊',
|
||||
sampleUser: '测试用户',
|
||||
sampleMessage: '你好',
|
||||
sampleFeedback: '很有帮助',
|
||||
invalidField: '请检查{{field}}',
|
||||
},
|
||||
title: '处理器',
|
||||
description: '创建可复用的处理器,并在机器人事件路由中使用',
|
||||
create: '创建处理器',
|
||||
|
||||
@@ -474,6 +474,33 @@ const zhHant = {
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
debugData: {
|
||||
title: '事件資料',
|
||||
form: '常用欄位',
|
||||
json: '完整 JSON',
|
||||
groupId: '群組 ID',
|
||||
memberId: '成員 ID',
|
||||
memberName: '成員暱稱',
|
||||
userId: '使用者 ID',
|
||||
userName: '使用者暱稱',
|
||||
requesterId: '申請者 ID',
|
||||
requesterName: '申請者暱稱',
|
||||
messageId: '訊息 ID',
|
||||
duration: '時長(秒)',
|
||||
message: '訊息內容',
|
||||
newMessage: '修改後的內容',
|
||||
verificationMessage: '驗證訊息',
|
||||
reaction: '表情',
|
||||
groupName: '群組名稱',
|
||||
feedback: '回饋內容',
|
||||
rating: '評分',
|
||||
eventName: '事件名稱',
|
||||
privateChat: '留空為私聊',
|
||||
sampleUser: '測試使用者',
|
||||
sampleMessage: '你好',
|
||||
sampleFeedback: '很有幫助',
|
||||
invalidField: '請檢查{{field}}',
|
||||
},
|
||||
title: '處理器',
|
||||
description: '建立可重用的處理器,並在機器人事件路由中使用',
|
||||
create: '建立處理器',
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { installLangBotApiMocks } from './fixtures/langbot-api';
|
||||
|
||||
test('event data stays in sync across the compact form, JSON and the request', async ({
|
||||
page,
|
||||
}) => {
|
||||
await installLangBotApiMocks(page, {
|
||||
authenticated: true,
|
||||
withAdapterEvents: true,
|
||||
});
|
||||
await page.route(
|
||||
'**/api/v1/agents/agent-workbench/debug/stream',
|
||||
async (route) => {
|
||||
await route.fulfill({
|
||||
contentType: 'application/x-ndjson',
|
||||
body:
|
||||
JSON.stringify({ kind: 'completed', data: { final_text: 'Done' } }) +
|
||||
'\n',
|
||||
});
|
||||
},
|
||||
);
|
||||
await page.goto('/home/agents?id=agent-workbench');
|
||||
const panel = page.getByRole('region', { name: 'Event Debug' });
|
||||
await panel
|
||||
.getByRole('textbox', { name: 'Message content', exact: true })
|
||||
.fill('Hello Alice');
|
||||
await panel.getByRole('button', { name: 'Full JSON' }).click();
|
||||
const json = panel.getByRole('textbox', { name: 'Full JSON' });
|
||||
expect(JSON.parse(await json.inputValue()).text).toBe('Hello Alice');
|
||||
await json.fill('{');
|
||||
await expect(panel.getByRole('button', { name: 'Run test' })).toBeDisabled();
|
||||
await expect(panel.getByRole('alert').last()).toContainText('valid JSON');
|
||||
await json.fill(
|
||||
JSON.stringify({
|
||||
text: 'Edited in JSON',
|
||||
user_name: 'Alice',
|
||||
extra: { keep: true },
|
||||
}),
|
||||
);
|
||||
await panel.getByRole('button', { name: 'Common fields' }).click();
|
||||
await expect(
|
||||
panel.getByRole('textbox', { name: 'Message content', exact: true }),
|
||||
).toHaveValue('Edited in JSON');
|
||||
await panel.getByRole('textbox', { name: 'User name' }).fill('Bob');
|
||||
await panel.getByRole('button', { name: 'Full JSON' }).click();
|
||||
expect(JSON.parse(await json.inputValue())).toMatchObject({
|
||||
user_name: 'Bob',
|
||||
extra: { keep: true },
|
||||
});
|
||||
|
||||
await panel.getByRole('combobox', { name: 'Event type' }).click();
|
||||
await page
|
||||
.getByRole('option')
|
||||
.filter({ hasText: 'group.member_joined' })
|
||||
.click();
|
||||
await expect(
|
||||
panel.getByRole('textbox', { name: 'Message content', exact: true }),
|
||||
).toHaveCount(0);
|
||||
await expect(
|
||||
panel.getByRole('textbox', { name: 'Event summary' }),
|
||||
).toHaveCount(0);
|
||||
await panel.getByRole('textbox', { name: 'Member name' }).fill('Carol');
|
||||
await panel.getByRole('textbox', { name: 'Member ID' }).fill('member-42');
|
||||
await panel.getByRole('textbox', { name: 'Group ID' }).fill('group-42');
|
||||
const request = page.waitForRequest(
|
||||
(req) => req.method() === 'POST' && req.url().endsWith('/debug/stream'),
|
||||
);
|
||||
await panel.getByRole('button', { name: 'Run test' }).click();
|
||||
expect((await request).postDataJSON()).toMatchObject({
|
||||
event_type: 'group.member_joined',
|
||||
text: '',
|
||||
data: {
|
||||
member_name: 'Carol',
|
||||
member_id: 'member-42',
|
||||
group_id: 'group-42',
|
||||
},
|
||||
});
|
||||
await expect(panel.getByText('Done', { exact: true })).toBeVisible();
|
||||
await page.setViewportSize({ width: 1280, height: 650 });
|
||||
await panel.getByRole('button', { name: 'Full JSON' }).click();
|
||||
await panel.locator('summary').filter({ hasText: 'Mock scenario' }).click();
|
||||
await expect(
|
||||
panel.getByRole('button', { name: 'Run test' }),
|
||||
).toBeInViewport();
|
||||
|
||||
await panel.getByRole('combobox', { name: 'Event type' }).click();
|
||||
await page.getByRole('option').filter({ hasText: 'Custom event' }).click();
|
||||
await expect(panel.getByRole('textbox', { name: 'Full JSON' })).toBeVisible();
|
||||
await expect(
|
||||
panel.getByRole('button', { name: 'Common fields' }),
|
||||
).toHaveCount(0);
|
||||
});
|
||||
@@ -36,7 +36,7 @@ test.describe('processor detail workbench', () => {
|
||||
.boundingBox();
|
||||
const eventPickerBox = await debugEventPicker.boundingBox();
|
||||
const conversationInputBox = await debugPanel
|
||||
.getByRole('textbox', { name: 'Conversation input' })
|
||||
.getByRole('textbox', { name: 'Message content' })
|
||||
.boundingBox();
|
||||
expect(transcriptBox).not.toBeNull();
|
||||
expect(eventPickerBox).not.toBeNull();
|
||||
@@ -204,9 +204,7 @@ test.describe('processor detail workbench', () => {
|
||||
.fill('Updated before debugging');
|
||||
await basicInfoDialog.getByRole('button', { name: 'Save' }).click();
|
||||
await expect(basicInfoDialog).toHaveCount(0);
|
||||
await page
|
||||
.getByRole('textbox', { name: 'Conversation input' })
|
||||
.fill('Hello');
|
||||
await page.getByRole('textbox', { name: 'Message content' }).fill('Hello');
|
||||
await page.getByRole('button', { name: 'Run test' }).click();
|
||||
|
||||
await expect(page.getByText('Mock Agent response')).toBeVisible();
|
||||
@@ -253,9 +251,7 @@ test.describe('processor detail workbench', () => {
|
||||
);
|
||||
|
||||
await page.goto('/home/agents?id=agent-workbench');
|
||||
await page
|
||||
.getByRole('textbox', { name: 'Conversation input' })
|
||||
.fill('Hello');
|
||||
await page.getByRole('textbox', { name: 'Message content' }).fill('Hello');
|
||||
await page.getByRole('button', { name: 'Run test' }).click();
|
||||
|
||||
await expect(
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import test from 'node:test';
|
||||
import ts from 'typescript';
|
||||
|
||||
const source = fs.readFileSync(
|
||||
new URL(
|
||||
'../../src/app/home/agents/components/debug-event-data.ts',
|
||||
import.meta.url,
|
||||
),
|
||||
'utf8',
|
||||
);
|
||||
const module = { exports: {} };
|
||||
new Function(
|
||||
'exports',
|
||||
ts.transpileModule(source, {
|
||||
compilerOptions: {
|
||||
module: ts.ModuleKind.CommonJS,
|
||||
target: ts.ScriptTarget.ES2022,
|
||||
},
|
||||
}).outputText,
|
||||
)(module.exports);
|
||||
const {
|
||||
createDebugEventData,
|
||||
parseDebugEventData,
|
||||
debugEventInputText,
|
||||
invalidDebugEventField,
|
||||
} = module.exports;
|
||||
const samples = { user: '测试用户', message: '你好', feedback: '很有帮助' };
|
||||
|
||||
test('message text and common fields belong to the same event data', () => {
|
||||
const data = createDebugEventData('message.received', samples);
|
||||
data.text = 'A different message';
|
||||
data.user_name = 'Alice';
|
||||
data.custom = { nested: true };
|
||||
const restored = parseDebugEventData(JSON.stringify(data));
|
||||
assert.deepEqual(restored, data);
|
||||
assert.equal(
|
||||
debugEventInputText('message.received', restored),
|
||||
'A different message',
|
||||
);
|
||||
assert.equal(restored.user_name, 'Alice');
|
||||
assert.equal(createDebugEventData('message.received', samples).text, '你好');
|
||||
});
|
||||
|
||||
test('non-message events and deleted messages do not require fabricated conversation text', () => {
|
||||
for (const event of [
|
||||
'group.member_left',
|
||||
'friend.request_received',
|
||||
'message.deleted',
|
||||
'message.reaction',
|
||||
]) {
|
||||
const data = createDebugEventData(event, samples);
|
||||
assert.equal(invalidDebugEventField(event, data), undefined);
|
||||
assert.equal(debugEventInputText(event, data), '');
|
||||
assert.equal(data.text, undefined);
|
||||
}
|
||||
});
|
||||
|
||||
test('edited message uses the edited content, not an event description', () => {
|
||||
const data = createDebugEventData('message.edited', samples);
|
||||
data.text = 'Corrected message';
|
||||
assert.equal(
|
||||
debugEventInputText('message.edited', data),
|
||||
'Corrected message',
|
||||
);
|
||||
assert.equal(
|
||||
invalidDebugEventField('message.edited', { ...data, text: ' ' }).key,
|
||||
'text',
|
||||
);
|
||||
});
|
||||
|
||||
test('invalid JSON cannot silently become an empty event', () => {
|
||||
for (const text of ['', '{', 'null', '[]', '"hello"', '12']) {
|
||||
assert.equal(parseDebugEventData(text), null);
|
||||
}
|
||||
const data = { arbitrary: { list: [1, true] } };
|
||||
assert.deepEqual(parseDebugEventData(JSON.stringify(data)), data);
|
||||
assert.equal(invalidDebugEventField('custom.example', data), undefined);
|
||||
assert.equal(invalidDebugEventField('constructor', data), undefined);
|
||||
});
|
||||
|
||||
test('duration and rating remain numbers and reject invalid values', () => {
|
||||
const data = createDebugEventData('bot.muted', samples);
|
||||
assert.equal(data.duration, 60);
|
||||
for (const duration of [-1, 0.5, '60']) {
|
||||
assert.equal(
|
||||
invalidDebugEventField('bot.muted', { ...data, duration }).key,
|
||||
'duration',
|
||||
);
|
||||
}
|
||||
assert.equal(
|
||||
invalidDebugEventField('bot.muted', { ...data, duration: 0 }),
|
||||
undefined,
|
||||
);
|
||||
assert.equal(
|
||||
invalidDebugEventField('feedback.received', { rating: 6 }).key,
|
||||
'rating',
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user