plugin-agent-orchestrator 1.0.20 → 1.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/hooks/useRunEventStream.d.ts +22 -0
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +1 -1
- package/dist/externalVersion.js +6 -6
- package/dist/server/collections/agent-execution-spans.js +24 -0
- package/dist/server/collections/agent-loop-runs.js +36 -0
- package/dist/server/collections/orchestrator-config.js +14 -0
- package/dist/server/migrations/20260601000000-add-token-fields.d.ts +7 -0
- package/dist/server/migrations/20260601000000-add-token-fields.js +101 -0
- package/dist/server/plugin.js +47 -0
- package/dist/server/resources/agent-loop.js +33 -25
- package/dist/server/resources/tracing.js +5 -8
- package/dist/server/services/AgentHarness.d.ts +2 -0
- package/dist/server/services/AgentHarness.js +56 -90
- package/dist/server/services/AgentLoopController.d.ts +33 -20
- package/dist/server/services/AgentLoopController.js +164 -125
- package/dist/server/services/AgentLoopRepository.js +16 -34
- package/dist/server/services/AgentLoopService.d.ts +28 -18
- package/dist/server/services/AgentLoopService.js +7 -1
- package/dist/server/services/AgentPlannerService.js +5 -25
- package/dist/server/services/AgentRegistryService.d.ts +8 -0
- package/dist/server/services/AgentRegistryService.js +34 -24
- package/dist/server/services/CircuitBreaker.d.ts +40 -0
- package/dist/server/services/CircuitBreaker.js +120 -0
- package/dist/server/services/ContextAggregator.d.ts +45 -0
- package/dist/server/services/ContextAggregator.js +201 -0
- package/dist/server/services/ExecutionSpanService.js +2 -5
- package/dist/server/services/RunEventBus.d.ts +9 -0
- package/dist/server/services/RunEventBus.js +73 -0
- package/dist/server/services/TokenTracker.d.ts +62 -0
- package/dist/server/services/TokenTracker.js +173 -0
- package/dist/server/tools/agent-loop.d.ts +8 -8
- package/dist/server/tools/agent-loop.js +30 -63
- package/dist/server/tools/delegate-task.js +14 -72
- package/dist/server/tools/orchestrator-plan.d.ts +6 -6
- package/dist/server/tools/orchestrator-plan.js +10 -47
- package/dist/server/types.d.ts +47 -0
- package/dist/server/types.js +24 -0
- package/dist/server/utils/ctx-utils.d.ts +30 -0
- package/dist/server/utils/ctx-utils.js +152 -0
- package/dist/server/utils/logging.d.ts +6 -0
- package/dist/server/utils/logging.js +86 -0
- package/package.json +44 -44
- package/src/client/AgentRunsTab.tsx +764 -764
- package/src/client/HarnessProfilesTab.tsx +247 -247
- package/src/client/OrchestratorSettings.tsx +106 -106
- package/src/client/RulesTab.tsx +716 -716
- package/src/client/hooks/useRunEventStream.ts +76 -0
- package/src/client/index.tsx +2 -1
- package/src/client/plugin.tsx +27 -27
- package/src/client/skill-hub/components/LoopSettings.tsx +331 -331
- package/src/client/skill-hub/index.tsx +51 -51
- package/src/client/skill-hub/tools/InteractionSchemasProvider.tsx +99 -99
- package/src/client/skill-hub/tools/SkillHubCard.tsx +109 -109
- package/src/client/skill-hub/tools/loopTemplates.ts +52 -52
- package/src/client/skill-hub/tools/registerSkillLoopCards.ts +58 -58
- package/src/client/tools/PlanApprovalCard.tsx +175 -175
- package/src/client/tools/registerOrchestratorCards.ts +7 -7
- package/src/server/__tests__/agent-loop-controller.test.ts +375 -0
- package/src/server/__tests__/circuit-breaker.test.ts +169 -0
- package/src/server/__tests__/context-aggregator.test.ts +222 -0
- package/src/server/__tests__/parallel-execution.test.ts +318 -0
- package/src/server/__tests__/smoke.test.ts +120 -0
- package/src/server/collections/agent-execution-spans.ts +24 -0
- package/src/server/collections/agent-harness-profiles.ts +59 -59
- package/src/server/collections/agent-loop-events.ts +71 -71
- package/src/server/collections/agent-loop-runs.ts +38 -1
- package/src/server/collections/agent-loop-steps.ts +144 -144
- package/src/server/collections/orchestrator-config.ts +14 -0
- package/src/server/collections/skill-executions.ts +106 -106
- package/src/server/collections/skill-loop-configs.ts +65 -65
- package/src/server/migrations/20260524000000-add-agent-loop-fields-to-skill-executions.ts +30 -30
- package/src/server/migrations/20260524001000-add-plan-approval-and-harness-profiles.ts +142 -142
- package/src/server/migrations/20260601000000-add-token-fields.ts +89 -0
- package/src/server/plugin.ts +53 -0
- package/src/server/resources/agent-loop.ts +21 -12
- package/src/server/resources/tracing.ts +3 -7
- package/src/server/services/AgentHarness.ts +78 -116
- package/src/server/services/AgentLoopController.ts +197 -122
- package/src/server/services/AgentLoopRepository.ts +9 -25
- package/src/server/services/AgentLoopService.ts +13 -1
- package/src/server/services/AgentPlanValidator.ts +73 -73
- package/src/server/services/AgentPlannerService.ts +2 -25
- package/src/server/services/AgentRegistryService.ts +40 -31
- package/src/server/services/CircuitBreaker.ts +116 -0
- package/src/server/services/ContextAggregator.ts +239 -0
- package/src/server/services/ExecutionSpanService.ts +2 -4
- package/src/server/services/RunEventBus.ts +45 -0
- package/src/server/services/TokenTracker.ts +209 -0
- package/src/server/skill-hub/plugin.ts +898 -898
- package/src/server/skill-hub/tasks/SkillExecutionTask.ts +460 -460
- package/src/server/tools/agent-loop.ts +18 -57
- package/src/server/tools/delegate-task.ts +11 -93
- package/src/server/tools/orchestrator-plan.ts +26 -50
- package/src/server/tools/skill-execute.ts +160 -160
- package/src/server/types.ts +55 -0
- package/src/server/utils/ctx-utils.ts +118 -0
- package/src/server/utils/logging.ts +63 -0
|
@@ -1,331 +1,331 @@
|
|
|
1
|
-
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
Button,
|
|
4
|
-
Card,
|
|
5
|
-
Form,
|
|
6
|
-
Input,
|
|
7
|
-
List,
|
|
8
|
-
message,
|
|
9
|
-
Modal,
|
|
10
|
-
Popconfirm,
|
|
11
|
-
Select,
|
|
12
|
-
Space,
|
|
13
|
-
Switch,
|
|
14
|
-
Tag,
|
|
15
|
-
Tooltip,
|
|
16
|
-
Typography,
|
|
17
|
-
} from 'antd';
|
|
18
|
-
import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons';
|
|
19
|
-
import { useAPIClient, useApp } from '@nocobase/client';
|
|
20
|
-
import { useT } from '../locale';
|
|
21
|
-
import { formatJsonText, parseJsonText, stringifyJsonText } from '../utils/jsonFields';
|
|
22
|
-
import { getLoopTemplate, LOOP_TEMPLATES } from '../tools/loopTemplates';
|
|
23
|
-
import { registerSkillLoopCards } from '../tools/registerSkillLoopCards';
|
|
24
|
-
|
|
25
|
-
const { TextArea } = Input;
|
|
26
|
-
|
|
27
|
-
const extractList = (data: any) => {
|
|
28
|
-
const value = data?.data?.data ?? data?.data ?? data ?? [];
|
|
29
|
-
return Array.isArray(value) ? value : [];
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
export const LoopSettings: React.FC = () => {
|
|
33
|
-
const api = useAPIClient();
|
|
34
|
-
const app = useApp();
|
|
35
|
-
const t = useT();
|
|
36
|
-
const [form] = Form.useForm();
|
|
37
|
-
const [skills, setSkills] = useState<any[]>([]);
|
|
38
|
-
const [configs, setConfigs] = useState<any[]>([]);
|
|
39
|
-
const [loading, setLoading] = useState(false);
|
|
40
|
-
const [editorVisible, setEditorVisible] = useState(false);
|
|
41
|
-
const [editingConfig, setEditingConfig] = useState<any>(null);
|
|
42
|
-
|
|
43
|
-
const skillsById = useMemo(() => new Map(skills.map((skill) => [String(skill.id), skill])), [skills]);
|
|
44
|
-
|
|
45
|
-
const fetchData = useCallback(async () => {
|
|
46
|
-
setLoading(true);
|
|
47
|
-
try {
|
|
48
|
-
const [skillsResponse, configsResponse] = await Promise.all([
|
|
49
|
-
api.request({
|
|
50
|
-
url: 'skillDefinitions:list',
|
|
51
|
-
params: {
|
|
52
|
-
filter: { enabled: true },
|
|
53
|
-
fields: ['id', 'name', 'title', 'language', 'autoCall'],
|
|
54
|
-
pageSize: 500,
|
|
55
|
-
},
|
|
56
|
-
}),
|
|
57
|
-
api.request({
|
|
58
|
-
url: 'skillLoopConfigs:list',
|
|
59
|
-
params: {
|
|
60
|
-
fields: ['id', 'skillId', 'enabled', 'title', 'templateKey', 'prompt', 'schema', 'config', 'updatedAt'],
|
|
61
|
-
sort: ['-updatedAt'],
|
|
62
|
-
pageSize: 500,
|
|
63
|
-
},
|
|
64
|
-
}),
|
|
65
|
-
]);
|
|
66
|
-
setSkills(extractList(skillsResponse.data));
|
|
67
|
-
setConfigs(extractList(configsResponse.data));
|
|
68
|
-
} catch {
|
|
69
|
-
message.error(t('Failed to load skill review settings'));
|
|
70
|
-
} finally {
|
|
71
|
-
setLoading(false);
|
|
72
|
-
}
|
|
73
|
-
}, [api, t]);
|
|
74
|
-
|
|
75
|
-
useEffect(() => {
|
|
76
|
-
fetchData();
|
|
77
|
-
}, [fetchData]);
|
|
78
|
-
|
|
79
|
-
const openCreate = () => {
|
|
80
|
-
const template = getLoopTemplate('confirm');
|
|
81
|
-
setEditingConfig(null);
|
|
82
|
-
form.resetFields();
|
|
83
|
-
form.setFieldsValue({
|
|
84
|
-
enabled: true,
|
|
85
|
-
templateKey: template.key,
|
|
86
|
-
prompt: template.schema.prompt,
|
|
87
|
-
schema: formatJsonText(template.schema),
|
|
88
|
-
config: '',
|
|
89
|
-
});
|
|
90
|
-
setEditorVisible(true);
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
const openEdit = (record: any) => {
|
|
94
|
-
const template = getLoopTemplate(record.templateKey);
|
|
95
|
-
setEditingConfig(record);
|
|
96
|
-
form.setFieldsValue({
|
|
97
|
-
...record,
|
|
98
|
-
templateKey: record.templateKey || template.key,
|
|
99
|
-
prompt: record.prompt || template.schema.prompt,
|
|
100
|
-
schema: formatJsonText(record.schema || template.schema),
|
|
101
|
-
config: formatJsonText(record.config, null),
|
|
102
|
-
});
|
|
103
|
-
setEditorVisible(true);
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
const closeEditor = () => {
|
|
107
|
-
setEditorVisible(false);
|
|
108
|
-
setEditingConfig(null);
|
|
109
|
-
form.resetFields();
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
const notifyLoopSettingsChanged = useCallback(async () => {
|
|
113
|
-
await registerSkillLoopCards(app);
|
|
114
|
-
window.dispatchEvent(new Event('skill-hub-loop-settings-changed'));
|
|
115
|
-
}, [app]);
|
|
116
|
-
|
|
117
|
-
const handleTemplateChange = (templateKey: string) => {
|
|
118
|
-
const template = getLoopTemplate(templateKey);
|
|
119
|
-
form.setFieldsValue({
|
|
120
|
-
prompt: template.schema.prompt,
|
|
121
|
-
schema: formatJsonText(template.schema),
|
|
122
|
-
});
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
const handleSave = async () => {
|
|
126
|
-
try {
|
|
127
|
-
const values = await form.validateFields();
|
|
128
|
-
const schema = parseJsonText<any>(values.schema, undefined);
|
|
129
|
-
if (!schema || typeof schema !== 'object') {
|
|
130
|
-
message.error(t('Invalid JSON in Review Schema'));
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const config = parseJsonText<any>(values.config, undefined);
|
|
135
|
-
if (values.config && config === undefined) {
|
|
136
|
-
message.error(t('Invalid JSON in Review Config'));
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const schemaWithPrompt = {
|
|
141
|
-
...schema,
|
|
142
|
-
prompt: values.prompt || schema.prompt,
|
|
143
|
-
};
|
|
144
|
-
const data = {
|
|
145
|
-
...values,
|
|
146
|
-
schema: stringifyJsonText(schemaWithPrompt),
|
|
147
|
-
config: values.config ? stringifyJsonText(config) : null,
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
if (editingConfig) {
|
|
151
|
-
await api.request({
|
|
152
|
-
url: 'skillLoopConfigs:update',
|
|
153
|
-
method: 'POST',
|
|
154
|
-
params: { filterByTk: editingConfig.id },
|
|
155
|
-
data,
|
|
156
|
-
});
|
|
157
|
-
} else {
|
|
158
|
-
await api.request({
|
|
159
|
-
url: 'skillLoopConfigs:create',
|
|
160
|
-
method: 'POST',
|
|
161
|
-
data,
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
message.success(t(editingConfig ? 'Skill review setting updated' : 'Skill review setting created'));
|
|
166
|
-
closeEditor();
|
|
167
|
-
fetchData();
|
|
168
|
-
void notifyLoopSettingsChanged();
|
|
169
|
-
} catch (err: any) {
|
|
170
|
-
if (err?.errorFields) return;
|
|
171
|
-
message.error(t('Failed to save skill review setting'));
|
|
172
|
-
}
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
const handleDelete = async (id: number) => {
|
|
176
|
-
try {
|
|
177
|
-
await api.request({ url: 'skillLoopConfigs:destroy', method: 'POST', params: { filterByTk: id } });
|
|
178
|
-
message.success(t('Deleted'));
|
|
179
|
-
fetchData();
|
|
180
|
-
void notifyLoopSettingsChanged();
|
|
181
|
-
} catch {
|
|
182
|
-
message.error(t('Failed to delete'));
|
|
183
|
-
}
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
const handleToggleEnabled = async (record: any) => {
|
|
187
|
-
try {
|
|
188
|
-
await api.request({
|
|
189
|
-
url: 'skillLoopConfigs:update',
|
|
190
|
-
method: 'POST',
|
|
191
|
-
params: { filterByTk: record.id },
|
|
192
|
-
data: { enabled: !record.enabled },
|
|
193
|
-
});
|
|
194
|
-
fetchData();
|
|
195
|
-
void notifyLoopSettingsChanged();
|
|
196
|
-
} catch {
|
|
197
|
-
message.error(t('Failed to update'));
|
|
198
|
-
}
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
return (
|
|
202
|
-
<Card
|
|
203
|
-
title={t('Skill Review Settings')}
|
|
204
|
-
extra={
|
|
205
|
-
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
|
206
|
-
{t('New Review')}
|
|
207
|
-
</Button>
|
|
208
|
-
}
|
|
209
|
-
>
|
|
210
|
-
<List
|
|
211
|
-
loading={loading}
|
|
212
|
-
dataSource={configs}
|
|
213
|
-
grid={{ gutter: 16, xs: 1, sm: 2, md: 3, lg: 3, xl: 4, xxl: 4 }}
|
|
214
|
-
renderItem={(config) => {
|
|
215
|
-
const skill = skillsById.get(String(config.skillId));
|
|
216
|
-
const template = getLoopTemplate(config.templateKey);
|
|
217
|
-
return (
|
|
218
|
-
<List.Item>
|
|
219
|
-
<Card
|
|
220
|
-
size="small"
|
|
221
|
-
title={
|
|
222
|
-
<Typography.Text ellipsis>
|
|
223
|
-
{config.title || skill?.title || skill?.name || t('Unknown skill')}
|
|
224
|
-
</Typography.Text>
|
|
225
|
-
}
|
|
226
|
-
extra={
|
|
227
|
-
<Tag color={config.enabled ? 'green' : 'default'}>
|
|
228
|
-
{config.enabled ? t('Enabled') : t('Disabled')}
|
|
229
|
-
</Tag>
|
|
230
|
-
}
|
|
231
|
-
actions={[
|
|
232
|
-
<Tooltip key="edit" title={t('Edit')}>
|
|
233
|
-
<EditOutlined onClick={() => openEdit(config)} />
|
|
234
|
-
</Tooltip>,
|
|
235
|
-
<Popconfirm key="delete" title={t('Delete?')} onConfirm={() => handleDelete(config.id)}>
|
|
236
|
-
<Tooltip title={t('Delete')}>
|
|
237
|
-
<DeleteOutlined style={{ color: 'red' }} />
|
|
238
|
-
</Tooltip>
|
|
239
|
-
</Popconfirm>,
|
|
240
|
-
]}
|
|
241
|
-
style={{ boxShadow: '0 2px 8px rgba(0,0,0,0.05)', borderRadius: 8 }}
|
|
242
|
-
>
|
|
243
|
-
<Space direction="vertical" size={8} style={{ width: '100%' }}>
|
|
244
|
-
<Typography.Text type="secondary" style={{ fontSize: 13 }}>
|
|
245
|
-
{skill?.name || `skillId=${config.skillId}`}
|
|
246
|
-
</Typography.Text>
|
|
247
|
-
<Tag color="blue" style={{ width: 'fit-content' }}>
|
|
248
|
-
{template.title}
|
|
249
|
-
</Tag>
|
|
250
|
-
<Typography.Paragraph
|
|
251
|
-
type="secondary"
|
|
252
|
-
ellipsis={{ rows: 2 }}
|
|
253
|
-
style={{ minHeight: 44, marginBottom: 0, fontSize: 13 }}
|
|
254
|
-
>
|
|
255
|
-
{config.prompt || template.schema.prompt}
|
|
256
|
-
</Typography.Paragraph>
|
|
257
|
-
<Space size={4}>
|
|
258
|
-
<Switch checked={config.enabled} onChange={() => handleToggleEnabled(config)} size="small" />
|
|
259
|
-
<span style={{ fontSize: 12 }}>{config.enabled ? t('Enabled') : t('Disabled')}</span>
|
|
260
|
-
</Space>
|
|
261
|
-
</Space>
|
|
262
|
-
</Card>
|
|
263
|
-
</List.Item>
|
|
264
|
-
);
|
|
265
|
-
}}
|
|
266
|
-
/>
|
|
267
|
-
|
|
268
|
-
<Modal
|
|
269
|
-
open={editorVisible}
|
|
270
|
-
title={editingConfig ? t('Edit Skill Review Setting') : t('New Skill Review Setting')}
|
|
271
|
-
onCancel={closeEditor}
|
|
272
|
-
onOk={handleSave}
|
|
273
|
-
width={760}
|
|
274
|
-
destroyOnClose
|
|
275
|
-
>
|
|
276
|
-
<Form form={form} layout="vertical">
|
|
277
|
-
<Form.Item name="enabled" valuePropName="checked" label={t('Enabled')}>
|
|
278
|
-
<Switch />
|
|
279
|
-
</Form.Item>
|
|
280
|
-
|
|
281
|
-
<Form.Item name="skillId" label={t('Skill')} rules={[{ required: true }]}>
|
|
282
|
-
<Select
|
|
283
|
-
showSearch
|
|
284
|
-
optionFilterProp="label"
|
|
285
|
-
placeholder={t('Select a skill')}
|
|
286
|
-
options={skills.map((skill) => ({
|
|
287
|
-
value: skill.id,
|
|
288
|
-
label: `${skill.title || skill.name} (${skill.name})`,
|
|
289
|
-
}))}
|
|
290
|
-
/>
|
|
291
|
-
</Form.Item>
|
|
292
|
-
|
|
293
|
-
<Form.Item name="title" label={t('Title')}>
|
|
294
|
-
<Input placeholder={t('Optional display title')} />
|
|
295
|
-
</Form.Item>
|
|
296
|
-
|
|
297
|
-
<Form.Item name="templateKey" label={t('Review Template')} rules={[{ required: true }]}>
|
|
298
|
-
<Select onChange={handleTemplateChange}>
|
|
299
|
-
{LOOP_TEMPLATES.map((template) => (
|
|
300
|
-
<Select.Option key={template.key} value={template.key}>
|
|
301
|
-
{template.title} - {template.description}
|
|
302
|
-
</Select.Option>
|
|
303
|
-
))}
|
|
304
|
-
</Select>
|
|
305
|
-
</Form.Item>
|
|
306
|
-
|
|
307
|
-
<Form.Item name="prompt" label={t('Prompt')} rules={[{ required: true }]}>
|
|
308
|
-
<TextArea rows={3} />
|
|
309
|
-
</Form.Item>
|
|
310
|
-
|
|
311
|
-
<Form.Item
|
|
312
|
-
name="schema"
|
|
313
|
-
label={t('Review Schema (JSON)')}
|
|
314
|
-
rules={[{ required: true }]}
|
|
315
|
-
extra={t('Standard interaction schema. Supported types: confirm, form, select.')}
|
|
316
|
-
>
|
|
317
|
-
<TextArea rows={10} style={{ fontFamily: 'monospace', fontSize: 13 }} />
|
|
318
|
-
</Form.Item>
|
|
319
|
-
|
|
320
|
-
<Form.Item name="config" label={t('Review Config (optional JSON)')}>
|
|
321
|
-
<TextArea
|
|
322
|
-
rows={4}
|
|
323
|
-
style={{ fontFamily: 'monospace', fontSize: 13 }}
|
|
324
|
-
placeholder={'{\n "maxRetries": 1\n}'}
|
|
325
|
-
/>
|
|
326
|
-
</Form.Item>
|
|
327
|
-
</Form>
|
|
328
|
-
</Modal>
|
|
329
|
-
</Card>
|
|
330
|
-
);
|
|
331
|
-
};
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
Card,
|
|
5
|
+
Form,
|
|
6
|
+
Input,
|
|
7
|
+
List,
|
|
8
|
+
message,
|
|
9
|
+
Modal,
|
|
10
|
+
Popconfirm,
|
|
11
|
+
Select,
|
|
12
|
+
Space,
|
|
13
|
+
Switch,
|
|
14
|
+
Tag,
|
|
15
|
+
Tooltip,
|
|
16
|
+
Typography,
|
|
17
|
+
} from 'antd';
|
|
18
|
+
import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons';
|
|
19
|
+
import { useAPIClient, useApp } from '@nocobase/client';
|
|
20
|
+
import { useT } from '../locale';
|
|
21
|
+
import { formatJsonText, parseJsonText, stringifyJsonText } from '../utils/jsonFields';
|
|
22
|
+
import { getLoopTemplate, LOOP_TEMPLATES } from '../tools/loopTemplates';
|
|
23
|
+
import { registerSkillLoopCards } from '../tools/registerSkillLoopCards';
|
|
24
|
+
|
|
25
|
+
const { TextArea } = Input;
|
|
26
|
+
|
|
27
|
+
const extractList = (data: any) => {
|
|
28
|
+
const value = data?.data?.data ?? data?.data ?? data ?? [];
|
|
29
|
+
return Array.isArray(value) ? value : [];
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const LoopSettings: React.FC = () => {
|
|
33
|
+
const api = useAPIClient();
|
|
34
|
+
const app = useApp();
|
|
35
|
+
const t = useT();
|
|
36
|
+
const [form] = Form.useForm();
|
|
37
|
+
const [skills, setSkills] = useState<any[]>([]);
|
|
38
|
+
const [configs, setConfigs] = useState<any[]>([]);
|
|
39
|
+
const [loading, setLoading] = useState(false);
|
|
40
|
+
const [editorVisible, setEditorVisible] = useState(false);
|
|
41
|
+
const [editingConfig, setEditingConfig] = useState<any>(null);
|
|
42
|
+
|
|
43
|
+
const skillsById = useMemo(() => new Map(skills.map((skill) => [String(skill.id), skill])), [skills]);
|
|
44
|
+
|
|
45
|
+
const fetchData = useCallback(async () => {
|
|
46
|
+
setLoading(true);
|
|
47
|
+
try {
|
|
48
|
+
const [skillsResponse, configsResponse] = await Promise.all([
|
|
49
|
+
api.request({
|
|
50
|
+
url: 'skillDefinitions:list',
|
|
51
|
+
params: {
|
|
52
|
+
filter: { enabled: true },
|
|
53
|
+
fields: ['id', 'name', 'title', 'language', 'autoCall'],
|
|
54
|
+
pageSize: 500,
|
|
55
|
+
},
|
|
56
|
+
}),
|
|
57
|
+
api.request({
|
|
58
|
+
url: 'skillLoopConfigs:list',
|
|
59
|
+
params: {
|
|
60
|
+
fields: ['id', 'skillId', 'enabled', 'title', 'templateKey', 'prompt', 'schema', 'config', 'updatedAt'],
|
|
61
|
+
sort: ['-updatedAt'],
|
|
62
|
+
pageSize: 500,
|
|
63
|
+
},
|
|
64
|
+
}),
|
|
65
|
+
]);
|
|
66
|
+
setSkills(extractList(skillsResponse.data));
|
|
67
|
+
setConfigs(extractList(configsResponse.data));
|
|
68
|
+
} catch {
|
|
69
|
+
message.error(t('Failed to load skill review settings'));
|
|
70
|
+
} finally {
|
|
71
|
+
setLoading(false);
|
|
72
|
+
}
|
|
73
|
+
}, [api, t]);
|
|
74
|
+
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
fetchData();
|
|
77
|
+
}, [fetchData]);
|
|
78
|
+
|
|
79
|
+
const openCreate = () => {
|
|
80
|
+
const template = getLoopTemplate('confirm');
|
|
81
|
+
setEditingConfig(null);
|
|
82
|
+
form.resetFields();
|
|
83
|
+
form.setFieldsValue({
|
|
84
|
+
enabled: true,
|
|
85
|
+
templateKey: template.key,
|
|
86
|
+
prompt: template.schema.prompt,
|
|
87
|
+
schema: formatJsonText(template.schema),
|
|
88
|
+
config: '',
|
|
89
|
+
});
|
|
90
|
+
setEditorVisible(true);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const openEdit = (record: any) => {
|
|
94
|
+
const template = getLoopTemplate(record.templateKey);
|
|
95
|
+
setEditingConfig(record);
|
|
96
|
+
form.setFieldsValue({
|
|
97
|
+
...record,
|
|
98
|
+
templateKey: record.templateKey || template.key,
|
|
99
|
+
prompt: record.prompt || template.schema.prompt,
|
|
100
|
+
schema: formatJsonText(record.schema || template.schema),
|
|
101
|
+
config: formatJsonText(record.config, null),
|
|
102
|
+
});
|
|
103
|
+
setEditorVisible(true);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const closeEditor = () => {
|
|
107
|
+
setEditorVisible(false);
|
|
108
|
+
setEditingConfig(null);
|
|
109
|
+
form.resetFields();
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const notifyLoopSettingsChanged = useCallback(async () => {
|
|
113
|
+
await registerSkillLoopCards(app);
|
|
114
|
+
window.dispatchEvent(new Event('skill-hub-loop-settings-changed'));
|
|
115
|
+
}, [app]);
|
|
116
|
+
|
|
117
|
+
const handleTemplateChange = (templateKey: string) => {
|
|
118
|
+
const template = getLoopTemplate(templateKey);
|
|
119
|
+
form.setFieldsValue({
|
|
120
|
+
prompt: template.schema.prompt,
|
|
121
|
+
schema: formatJsonText(template.schema),
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const handleSave = async () => {
|
|
126
|
+
try {
|
|
127
|
+
const values = await form.validateFields();
|
|
128
|
+
const schema = parseJsonText<any>(values.schema, undefined);
|
|
129
|
+
if (!schema || typeof schema !== 'object') {
|
|
130
|
+
message.error(t('Invalid JSON in Review Schema'));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const config = parseJsonText<any>(values.config, undefined);
|
|
135
|
+
if (values.config && config === undefined) {
|
|
136
|
+
message.error(t('Invalid JSON in Review Config'));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const schemaWithPrompt = {
|
|
141
|
+
...schema,
|
|
142
|
+
prompt: values.prompt || schema.prompt,
|
|
143
|
+
};
|
|
144
|
+
const data = {
|
|
145
|
+
...values,
|
|
146
|
+
schema: stringifyJsonText(schemaWithPrompt),
|
|
147
|
+
config: values.config ? stringifyJsonText(config) : null,
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
if (editingConfig) {
|
|
151
|
+
await api.request({
|
|
152
|
+
url: 'skillLoopConfigs:update',
|
|
153
|
+
method: 'POST',
|
|
154
|
+
params: { filterByTk: editingConfig.id },
|
|
155
|
+
data,
|
|
156
|
+
});
|
|
157
|
+
} else {
|
|
158
|
+
await api.request({
|
|
159
|
+
url: 'skillLoopConfigs:create',
|
|
160
|
+
method: 'POST',
|
|
161
|
+
data,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
message.success(t(editingConfig ? 'Skill review setting updated' : 'Skill review setting created'));
|
|
166
|
+
closeEditor();
|
|
167
|
+
fetchData();
|
|
168
|
+
void notifyLoopSettingsChanged();
|
|
169
|
+
} catch (err: any) {
|
|
170
|
+
if (err?.errorFields) return;
|
|
171
|
+
message.error(t('Failed to save skill review setting'));
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const handleDelete = async (id: number) => {
|
|
176
|
+
try {
|
|
177
|
+
await api.request({ url: 'skillLoopConfigs:destroy', method: 'POST', params: { filterByTk: id } });
|
|
178
|
+
message.success(t('Deleted'));
|
|
179
|
+
fetchData();
|
|
180
|
+
void notifyLoopSettingsChanged();
|
|
181
|
+
} catch {
|
|
182
|
+
message.error(t('Failed to delete'));
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const handleToggleEnabled = async (record: any) => {
|
|
187
|
+
try {
|
|
188
|
+
await api.request({
|
|
189
|
+
url: 'skillLoopConfigs:update',
|
|
190
|
+
method: 'POST',
|
|
191
|
+
params: { filterByTk: record.id },
|
|
192
|
+
data: { enabled: !record.enabled },
|
|
193
|
+
});
|
|
194
|
+
fetchData();
|
|
195
|
+
void notifyLoopSettingsChanged();
|
|
196
|
+
} catch {
|
|
197
|
+
message.error(t('Failed to update'));
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<Card
|
|
203
|
+
title={t('Skill Review Settings')}
|
|
204
|
+
extra={
|
|
205
|
+
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
|
206
|
+
{t('New Review')}
|
|
207
|
+
</Button>
|
|
208
|
+
}
|
|
209
|
+
>
|
|
210
|
+
<List
|
|
211
|
+
loading={loading}
|
|
212
|
+
dataSource={configs}
|
|
213
|
+
grid={{ gutter: 16, xs: 1, sm: 2, md: 3, lg: 3, xl: 4, xxl: 4 }}
|
|
214
|
+
renderItem={(config) => {
|
|
215
|
+
const skill = skillsById.get(String(config.skillId));
|
|
216
|
+
const template = getLoopTemplate(config.templateKey);
|
|
217
|
+
return (
|
|
218
|
+
<List.Item>
|
|
219
|
+
<Card
|
|
220
|
+
size="small"
|
|
221
|
+
title={
|
|
222
|
+
<Typography.Text ellipsis>
|
|
223
|
+
{config.title || skill?.title || skill?.name || t('Unknown skill')}
|
|
224
|
+
</Typography.Text>
|
|
225
|
+
}
|
|
226
|
+
extra={
|
|
227
|
+
<Tag color={config.enabled ? 'green' : 'default'}>
|
|
228
|
+
{config.enabled ? t('Enabled') : t('Disabled')}
|
|
229
|
+
</Tag>
|
|
230
|
+
}
|
|
231
|
+
actions={[
|
|
232
|
+
<Tooltip key="edit" title={t('Edit')}>
|
|
233
|
+
<EditOutlined onClick={() => openEdit(config)} />
|
|
234
|
+
</Tooltip>,
|
|
235
|
+
<Popconfirm key="delete" title={t('Delete?')} onConfirm={() => handleDelete(config.id)}>
|
|
236
|
+
<Tooltip title={t('Delete')}>
|
|
237
|
+
<DeleteOutlined style={{ color: 'red' }} />
|
|
238
|
+
</Tooltip>
|
|
239
|
+
</Popconfirm>,
|
|
240
|
+
]}
|
|
241
|
+
style={{ boxShadow: '0 2px 8px rgba(0,0,0,0.05)', borderRadius: 8 }}
|
|
242
|
+
>
|
|
243
|
+
<Space direction="vertical" size={8} style={{ width: '100%' }}>
|
|
244
|
+
<Typography.Text type="secondary" style={{ fontSize: 13 }}>
|
|
245
|
+
{skill?.name || `skillId=${config.skillId}`}
|
|
246
|
+
</Typography.Text>
|
|
247
|
+
<Tag color="blue" style={{ width: 'fit-content' }}>
|
|
248
|
+
{template.title}
|
|
249
|
+
</Tag>
|
|
250
|
+
<Typography.Paragraph
|
|
251
|
+
type="secondary"
|
|
252
|
+
ellipsis={{ rows: 2 }}
|
|
253
|
+
style={{ minHeight: 44, marginBottom: 0, fontSize: 13 }}
|
|
254
|
+
>
|
|
255
|
+
{config.prompt || template.schema.prompt}
|
|
256
|
+
</Typography.Paragraph>
|
|
257
|
+
<Space size={4}>
|
|
258
|
+
<Switch checked={config.enabled} onChange={() => handleToggleEnabled(config)} size="small" />
|
|
259
|
+
<span style={{ fontSize: 12 }}>{config.enabled ? t('Enabled') : t('Disabled')}</span>
|
|
260
|
+
</Space>
|
|
261
|
+
</Space>
|
|
262
|
+
</Card>
|
|
263
|
+
</List.Item>
|
|
264
|
+
);
|
|
265
|
+
}}
|
|
266
|
+
/>
|
|
267
|
+
|
|
268
|
+
<Modal
|
|
269
|
+
open={editorVisible}
|
|
270
|
+
title={editingConfig ? t('Edit Skill Review Setting') : t('New Skill Review Setting')}
|
|
271
|
+
onCancel={closeEditor}
|
|
272
|
+
onOk={handleSave}
|
|
273
|
+
width={760}
|
|
274
|
+
destroyOnClose
|
|
275
|
+
>
|
|
276
|
+
<Form form={form} layout="vertical">
|
|
277
|
+
<Form.Item name="enabled" valuePropName="checked" label={t('Enabled')}>
|
|
278
|
+
<Switch />
|
|
279
|
+
</Form.Item>
|
|
280
|
+
|
|
281
|
+
<Form.Item name="skillId" label={t('Skill')} rules={[{ required: true }]}>
|
|
282
|
+
<Select
|
|
283
|
+
showSearch
|
|
284
|
+
optionFilterProp="label"
|
|
285
|
+
placeholder={t('Select a skill')}
|
|
286
|
+
options={skills.map((skill) => ({
|
|
287
|
+
value: skill.id,
|
|
288
|
+
label: `${skill.title || skill.name} (${skill.name})`,
|
|
289
|
+
}))}
|
|
290
|
+
/>
|
|
291
|
+
</Form.Item>
|
|
292
|
+
|
|
293
|
+
<Form.Item name="title" label={t('Title')}>
|
|
294
|
+
<Input placeholder={t('Optional display title')} />
|
|
295
|
+
</Form.Item>
|
|
296
|
+
|
|
297
|
+
<Form.Item name="templateKey" label={t('Review Template')} rules={[{ required: true }]}>
|
|
298
|
+
<Select onChange={handleTemplateChange}>
|
|
299
|
+
{LOOP_TEMPLATES.map((template) => (
|
|
300
|
+
<Select.Option key={template.key} value={template.key}>
|
|
301
|
+
{template.title} - {template.description}
|
|
302
|
+
</Select.Option>
|
|
303
|
+
))}
|
|
304
|
+
</Select>
|
|
305
|
+
</Form.Item>
|
|
306
|
+
|
|
307
|
+
<Form.Item name="prompt" label={t('Prompt')} rules={[{ required: true }]}>
|
|
308
|
+
<TextArea rows={3} />
|
|
309
|
+
</Form.Item>
|
|
310
|
+
|
|
311
|
+
<Form.Item
|
|
312
|
+
name="schema"
|
|
313
|
+
label={t('Review Schema (JSON)')}
|
|
314
|
+
rules={[{ required: true }]}
|
|
315
|
+
extra={t('Standard interaction schema. Supported types: confirm, form, select.')}
|
|
316
|
+
>
|
|
317
|
+
<TextArea rows={10} style={{ fontFamily: 'monospace', fontSize: 13 }} />
|
|
318
|
+
</Form.Item>
|
|
319
|
+
|
|
320
|
+
<Form.Item name="config" label={t('Review Config (optional JSON)')}>
|
|
321
|
+
<TextArea
|
|
322
|
+
rows={4}
|
|
323
|
+
style={{ fontFamily: 'monospace', fontSize: 13 }}
|
|
324
|
+
placeholder={'{\n "maxRetries": 1\n}'}
|
|
325
|
+
/>
|
|
326
|
+
</Form.Item>
|
|
327
|
+
</Form>
|
|
328
|
+
</Modal>
|
|
329
|
+
</Card>
|
|
330
|
+
);
|
|
331
|
+
};
|