mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-26 12:17:14 +00:00
fix(agents): align debug event selection
This commit is contained in:
@@ -49,6 +49,12 @@ export default function AgentDetailContent({ id }: { id: string }) {
|
||||
const [runnerStatus, setRunnerStatus] = useState<AgentRunnerStatus | null>(
|
||||
null,
|
||||
);
|
||||
const [availableEventTypes, setAvailableEventTypes] = useState<string[]>([
|
||||
'message.received',
|
||||
]);
|
||||
const [supportedEventPatterns, setSupportedEventPatterns] = useState<
|
||||
string[]
|
||||
>(['*']);
|
||||
const agentFormRef = useRef<AgentFormHandle>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -70,10 +76,25 @@ export default function AgentDetailContent({ id }: { id: string }) {
|
||||
if (isCreateMode) return;
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
httpClient
|
||||
.getAgent(id)
|
||||
.then((resp) => {
|
||||
if (!cancelled) setAgent(resp.agent);
|
||||
Promise.all([
|
||||
httpClient.getAgent(id),
|
||||
httpClient.getAdapters().catch(() => ({ adapters: [] })),
|
||||
])
|
||||
.then(([resp, adaptersResp]) => {
|
||||
if (cancelled) return;
|
||||
const adapterEvents = adaptersResp.adapters.flatMap(
|
||||
(adapter) => adapter.spec.supported_events ?? [],
|
||||
);
|
||||
setAvailableEventTypes(
|
||||
adapterEvents.length > 0
|
||||
? Array.from(new Set(adapterEvents)).sort()
|
||||
: ['message.received'],
|
||||
);
|
||||
setSupportedEventPatterns(
|
||||
resp.agent.supported_event_patterns ??
|
||||
resp.agent.capability?.supported_event_patterns ?? ['*'],
|
||||
);
|
||||
setAgent(resp.agent);
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setLoading(false);
|
||||
@@ -177,6 +198,7 @@ export default function AgentDetailContent({ id }: { id: string }) {
|
||||
<AgentFormComponent
|
||||
ref={agentFormRef}
|
||||
agentId={id}
|
||||
availableEventTypes={availableEventTypes}
|
||||
onFinish={(updatedAgent) => {
|
||||
if (updatedAgent) {
|
||||
setAgent((current) =>
|
||||
@@ -188,6 +210,7 @@ export default function AgentDetailContent({ id }: { id: string }) {
|
||||
onDirtyChange={setFormDirty}
|
||||
onSavingChange={setFormSaving}
|
||||
onRunnerStatusChange={setRunnerStatus}
|
||||
onSupportedEventPatternsChange={setSupportedEventPatterns}
|
||||
/>
|
||||
</fieldset>
|
||||
}
|
||||
@@ -201,10 +224,8 @@ export default function AgentDetailContent({ id }: { id: string }) {
|
||||
onOpenRunnerConfig={() =>
|
||||
agentFormRef.current?.openSection('runner_config')
|
||||
}
|
||||
supportedEventPatterns={
|
||||
agent.supported_event_patterns ??
|
||||
agent.capability?.supported_event_patterns ?? ['*']
|
||||
}
|
||||
supportedEventPatterns={supportedEventPatterns}
|
||||
availableEventTypes={availableEventTypes}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
|
||||
@@ -17,7 +17,9 @@ import { Label } from '@/components/ui/label';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
@@ -28,9 +30,16 @@ import {
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from '@/components/ui/collapsible';
|
||||
import {
|
||||
eventGroupLabel,
|
||||
eventPatternDescription,
|
||||
eventPatternLabel,
|
||||
groupEventPatterns,
|
||||
} from '@/app/home/components/event-patterns/event-pattern-groups';
|
||||
|
||||
interface AgentDebugPanelProps {
|
||||
agentId: string;
|
||||
availableEventTypes: string[];
|
||||
supportedEventPatterns?: string[];
|
||||
beforeRun?: () => Promise<boolean>;
|
||||
hasUnsavedChanges?: boolean;
|
||||
@@ -46,16 +55,15 @@ interface DebugEntry {
|
||||
detail?: string;
|
||||
}
|
||||
|
||||
const EVENT_PRESETS = [
|
||||
{
|
||||
value: 'message.received',
|
||||
labelKey: 'agents.debugMessageReceived',
|
||||
const EVENT_PRESET_DATA: Record<
|
||||
string,
|
||||
{ text: string; data: Record<string, unknown> }
|
||||
> = {
|
||||
'message.received': {
|
||||
text: '',
|
||||
data: {},
|
||||
},
|
||||
{
|
||||
value: 'group.member.joined',
|
||||
labelKey: 'agents.debugGroupMemberJoined',
|
||||
'group.member_joined': {
|
||||
text: 'A new member joined the group.',
|
||||
data: {
|
||||
group_id: 'debug-group',
|
||||
@@ -63,9 +71,7 @@ const EVENT_PRESETS = [
|
||||
member_name: 'Debug User',
|
||||
},
|
||||
},
|
||||
{
|
||||
value: 'group.member.left',
|
||||
labelKey: 'agents.debugGroupMemberLeft',
|
||||
'group.member_left': {
|
||||
text: 'A member left the group.',
|
||||
data: {
|
||||
group_id: 'debug-group',
|
||||
@@ -73,9 +79,7 @@ const EVENT_PRESETS = [
|
||||
member_name: 'Debug User',
|
||||
},
|
||||
},
|
||||
{
|
||||
value: 'friend.requested',
|
||||
labelKey: 'agents.debugFriendRequested',
|
||||
'friend.request_received': {
|
||||
text: 'A user sent a friend request.',
|
||||
data: {
|
||||
requester_id: 'debug-user',
|
||||
@@ -83,22 +87,14 @@ const EVENT_PRESETS = [
|
||||
message: 'Hello',
|
||||
},
|
||||
},
|
||||
{
|
||||
value: 'feedback.received',
|
||||
labelKey: 'agents.debugFeedbackReceived',
|
||||
'feedback.received': {
|
||||
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());
|
||||
@@ -112,6 +108,7 @@ function matchesEventPattern(pattern: string, eventType: string) {
|
||||
|
||||
export default function AgentDebugPanel({
|
||||
agentId,
|
||||
availableEventTypes,
|
||||
supportedEventPatterns = ['*'],
|
||||
beforeRun,
|
||||
hasUnsavedChanges = false,
|
||||
@@ -132,27 +129,39 @@ export default function AgentDebugPanel({
|
||||
() => supportedEventPatterns.join(', '),
|
||||
[supportedEventPatterns],
|
||||
);
|
||||
const availablePresets = useMemo(
|
||||
() =>
|
||||
EVENT_PRESETS.filter(
|
||||
(item) =>
|
||||
item.value === 'custom' ||
|
||||
supportedEventPatterns.some((pattern) =>
|
||||
matchesEventPattern(pattern, item.value),
|
||||
),
|
||||
),
|
||||
[supportedEventPatterns],
|
||||
const availableEvents = useMemo(() => {
|
||||
const concretePatterns = supportedEventPatterns.filter(
|
||||
(pattern) => pattern !== '*' && !pattern.endsWith('.*'),
|
||||
);
|
||||
return Array.from(new Set([...availableEventTypes, ...concretePatterns]))
|
||||
.filter((candidate) =>
|
||||
supportedEventPatterns.some((pattern) =>
|
||||
matchesEventPattern(pattern, candidate),
|
||||
),
|
||||
)
|
||||
.sort();
|
||||
}, [availableEventTypes, supportedEventPatterns]);
|
||||
const eventGroups = useMemo(
|
||||
() => groupEventPatterns(availableEvents),
|
||||
[availableEvents],
|
||||
);
|
||||
const supportsCustomEvent = supportedEventPatterns.some(
|
||||
(pattern) => pattern === '*' || pattern.endsWith('.*'),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (availablePresets.some((item) => item.value === preset)) return;
|
||||
selectPreset(availablePresets[0]?.value ?? 'custom');
|
||||
}, [availablePresets, preset]);
|
||||
if (
|
||||
availableEvents.includes(preset) ||
|
||||
(preset === 'custom' && supportsCustomEvent)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
selectPreset(availableEvents[0] ?? 'custom');
|
||||
}, [availableEvents, preset, supportsCustomEvent]);
|
||||
|
||||
function selectPreset(value: string) {
|
||||
setPreset(value);
|
||||
const nextPreset = EVENT_PRESETS.find((item) => item.value === value);
|
||||
if (!nextPreset) return;
|
||||
const nextPreset = EVENT_PRESET_DATA[value] ?? { text: '', data: {} };
|
||||
setInputText(nextPreset.text);
|
||||
setEventDataText(JSON.stringify(nextPreset.data, null, 2));
|
||||
}
|
||||
@@ -275,15 +284,42 @@ export default function AgentDebugPanel({
|
||||
<div className="min-w-0 flex-1 space-y-1.5">
|
||||
<Label>{t('agents.debugEventType')}</Label>
|
||||
<Select value={preset} onValueChange={selectPreset}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectTrigger
|
||||
className="w-full"
|
||||
aria-label={t('agents.debugEventType')}
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availablePresets.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{t(item.labelKey)}
|
||||
</SelectItem>
|
||||
<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"
|
||||
>
|
||||
{eventPatternLabel(event, t)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
))}
|
||||
{supportsCustomEvent && (
|
||||
<SelectGroup>
|
||||
<SelectLabel>{t('agents.debugCustomEvent')}</SelectLabel>
|
||||
<SelectItem
|
||||
value="custom"
|
||||
description={t('bots.eventDescriptions.custom')}
|
||||
className="py-2"
|
||||
>
|
||||
{t('agents.debugCustomEvent')}
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
@@ -20,6 +20,8 @@ import { cn } from '@/lib/utils';
|
||||
import {
|
||||
eventGroupLabel,
|
||||
eventNamespaces,
|
||||
eventPatternDescription,
|
||||
eventPatternLabel,
|
||||
groupEventPatterns,
|
||||
} from '@/app/home/components/event-patterns/event-pattern-groups';
|
||||
|
||||
@@ -61,30 +63,6 @@ export default function AgentEventPatternPicker({
|
||||
}, [events, selectedPatterns]);
|
||||
const optionGroups = useMemo(() => groupEventPatterns(options), [options]);
|
||||
|
||||
function eventLabel(pattern: string) {
|
||||
if (pattern === '*') return t('bots.eventWildcard');
|
||||
if (pattern.endsWith('.*')) {
|
||||
return t('bots.eventNamespaceWildcard', {
|
||||
namespace: pattern.replace('.*', ''),
|
||||
});
|
||||
}
|
||||
const key = `bots.eventNames.${pattern.replace(/\./g, '_')}`;
|
||||
const label = t(key);
|
||||
return label === key ? pattern : label;
|
||||
}
|
||||
|
||||
function eventDescription(pattern: string) {
|
||||
if (pattern === '*') return t('bots.eventDescriptions.all');
|
||||
if (pattern.endsWith('.*')) {
|
||||
return t('bots.eventDescriptions.namespace');
|
||||
}
|
||||
const key = `bots.eventDescriptions.${pattern.replace(/\./g, '_')}`;
|
||||
const description = t(key);
|
||||
return description === key
|
||||
? t('bots.eventDescriptions.custom')
|
||||
: description;
|
||||
}
|
||||
|
||||
function togglePattern(pattern: string) {
|
||||
if (pattern === '*') {
|
||||
onChange(['*']);
|
||||
@@ -127,7 +105,9 @@ export default function AgentEventPatternPicker({
|
||||
variant="secondary"
|
||||
className="max-w-full rounded-md font-normal"
|
||||
>
|
||||
<span className="truncate">{eventLabel(pattern)}</span>
|
||||
<span className="truncate">
|
||||
{eventPatternLabel(pattern, t)}
|
||||
</span>
|
||||
</Badge>
|
||||
))}
|
||||
{selectedPatterns.length > 3 && (
|
||||
@@ -157,7 +137,7 @@ export default function AgentEventPatternPicker({
|
||||
return (
|
||||
<CommandItem
|
||||
key={pattern}
|
||||
value={`${eventLabel(pattern)} ${pattern}`}
|
||||
value={`${eventPatternLabel(pattern, t)} ${pattern}`}
|
||||
onSelect={() => togglePattern(pattern)}
|
||||
className="items-start gap-2 py-2"
|
||||
>
|
||||
@@ -170,14 +150,14 @@ export default function AgentEventPatternPicker({
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="flex min-w-0 items-center gap-2">
|
||||
<span className="truncate font-medium">
|
||||
{eventLabel(pattern)}
|
||||
{eventPatternLabel(pattern, t)}
|
||||
</span>
|
||||
<code className="shrink-0 text-[10px] text-muted-foreground">
|
||||
{pattern}
|
||||
</code>
|
||||
</span>
|
||||
<span className="mt-0.5 block text-xs text-muted-foreground">
|
||||
{eventDescription(pattern)}
|
||||
{eventPatternDescription(pattern, t)}
|
||||
</span>
|
||||
</span>
|
||||
</CommandItem>
|
||||
|
||||
@@ -47,10 +47,12 @@ export interface AgentRunnerStatus {
|
||||
|
||||
interface AgentFormComponentProps {
|
||||
agentId: string;
|
||||
availableEventTypes: string[];
|
||||
onFinish: (agent?: Partial<Agent>) => void;
|
||||
onDirtyChange?: (dirty: boolean) => void;
|
||||
onSavingChange?: (saving: boolean) => void;
|
||||
onRunnerStatusChange?: (status: AgentRunnerStatus) => void;
|
||||
onSupportedEventPatternsChange?: (patterns: string[]) => void;
|
||||
}
|
||||
|
||||
export type AgentConfigSection = 'events' | 'runner' | 'runner_config';
|
||||
@@ -98,10 +100,12 @@ function isRunnerFieldVisible(
|
||||
function AgentFormComponent(
|
||||
{
|
||||
agentId,
|
||||
availableEventTypes,
|
||||
onFinish,
|
||||
onDirtyChange,
|
||||
onSavingChange,
|
||||
onRunnerStatusChange,
|
||||
onSupportedEventPatternsChange,
|
||||
}: AgentFormComponentProps,
|
||||
ref: ForwardedRef<AgentFormHandle>,
|
||||
) {
|
||||
@@ -112,9 +116,6 @@ function AgentFormComponent(
|
||||
useState<ApiRespPluginSystemStatus | null>(null);
|
||||
const [pluginStatusLoading, setPluginStatusLoading] = useState(true);
|
||||
const [pluginStatusError, setPluginStatusError] = useState(false);
|
||||
const [availableEventTypes, setAvailableEventTypes] = useState<string[]>([
|
||||
'message.received',
|
||||
]);
|
||||
const [activeSection, setActiveSection] =
|
||||
useState<AgentConfigSection>('runner');
|
||||
const isSavingRef = useRef(false);
|
||||
@@ -159,24 +160,17 @@ function AgentFormComponent(
|
||||
onDirtyChange?.(hasUnsavedChanges);
|
||||
}, [hasUnsavedChanges, onDirtyChange]);
|
||||
|
||||
const supportedEventPatterns = form.watch('supported_event_patterns');
|
||||
useEffect(() => {
|
||||
onSupportedEventPatternsChange?.(supportedEventPatterns);
|
||||
}, [onSupportedEventPatternsChange, supportedEventPatterns]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
Promise.all([
|
||||
httpClient.getAgentMetadata(),
|
||||
httpClient.getAgent(agentId),
|
||||
httpClient.getAdapters().catch(() => ({ adapters: [] })),
|
||||
])
|
||||
.then(([metadata, resp, adaptersResp]) => {
|
||||
Promise.all([httpClient.getAgentMetadata(), httpClient.getAgent(agentId)])
|
||||
.then(([metadata, resp]) => {
|
||||
if (cancelled) return;
|
||||
setRunnerConfigSchema(metadata.runner_config ?? null);
|
||||
const adapterEvents = adaptersResp.adapters.flatMap(
|
||||
(adapter) => adapter.spec.supported_events ?? [],
|
||||
);
|
||||
setAvailableEventTypes(
|
||||
adapterEvents.length > 0
|
||||
? Array.from(new Set(adapterEvents)).sort()
|
||||
: ['message.received'],
|
||||
);
|
||||
const agent = resp.agent;
|
||||
const config = (agent.config ?? {}) as Record<string, any>;
|
||||
const loadedValues: FormValues = {
|
||||
|
||||
@@ -55,3 +55,25 @@ export function eventGroupLabel(namespace: string, t: TFunction) {
|
||||
const label = t(key);
|
||||
return label === key ? namespace : label;
|
||||
}
|
||||
|
||||
export function eventPatternLabel(pattern: string, t: TFunction) {
|
||||
if (pattern === '*') return t('bots.eventWildcard');
|
||||
if (pattern.endsWith('.*')) {
|
||||
return t('bots.eventNamespaceWildcard', {
|
||||
namespace: pattern.replace('.*', ''),
|
||||
});
|
||||
}
|
||||
const key = `bots.eventNames.${pattern.replace(/\./g, '_')}`;
|
||||
const label = t(key);
|
||||
return label === key ? pattern : label;
|
||||
}
|
||||
|
||||
export function eventPatternDescription(pattern: string, t: TFunction) {
|
||||
if (pattern === '*') return t('bots.eventDescriptions.all');
|
||||
if (pattern.endsWith('.*')) {
|
||||
return t('bots.eventDescriptions.namespace');
|
||||
}
|
||||
const key = `bots.eventDescriptions.${pattern.replace(/\./g, '_')}`;
|
||||
const description = t(key);
|
||||
return description === key ? t('bots.eventDescriptions.custom') : description;
|
||||
}
|
||||
|
||||
@@ -27,6 +27,21 @@ test.describe('processor detail workbench', () => {
|
||||
expect(debugBox!.x).toBeLessThan(configBox!.x);
|
||||
expect(configBox!.width).toBeGreaterThan(debugBox!.width);
|
||||
|
||||
const debugEventPicker = debugPanel.getByRole('combobox', {
|
||||
name: 'Event type',
|
||||
});
|
||||
await debugEventPicker.click();
|
||||
await expect(page.getByRole('group', { name: 'Messages' })).toBeVisible();
|
||||
await expect(page.getByRole('group', { name: 'Groups' })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('option').filter({ hasText: 'Member joined group' }),
|
||||
).toContainText('A member joins a group where the bot is present.');
|
||||
await expect(
|
||||
page.getByRole('option').filter({ hasText: 'Message edited' }),
|
||||
).toContainText('The platform reports that an existing message changed.');
|
||||
await expect(page.getByRole('option')).toHaveCount(6);
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
const appShell = page.locator('[class*="group/sidebar-wrapper"]');
|
||||
const sidebarInset = page.locator('[data-slot="sidebar-inset"]');
|
||||
await expect(appShell).toHaveCSS('overflow', 'clip');
|
||||
@@ -108,6 +123,25 @@ test.describe('processor detail workbench', () => {
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole('group', { name: 'Messages' })).toHaveCount(1);
|
||||
await expect(page.getByRole('group', { name: 'Groups' })).toHaveCount(1);
|
||||
await page
|
||||
.getByRole('option')
|
||||
.filter({ hasText: 'message.*' })
|
||||
.first()
|
||||
.click();
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
await debugEventPicker.click();
|
||||
await expect(
|
||||
page.getByRole('option').filter({ hasText: 'Message edited' }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('option').filter({ hasText: 'Member joined group' }),
|
||||
).toHaveCount(0);
|
||||
await expect(page.getByRole('option')).toHaveCount(3);
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
await eventPicker.click();
|
||||
await page.getByRole('option').filter({ hasText: 'All events' }).click();
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
await flow.getByRole('tab').nth(1).click();
|
||||
|
||||
Reference in New Issue
Block a user