plugin-agent-orchestrator 1.0.16 → 1.0.18
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/AgentRunsTab.d.ts +2 -0
- package/dist/client/HarnessProfilesTab.d.ts +2 -0
- package/dist/client/index.js +1 -1
- package/dist/client/skill-hub/components/LoopSettings.d.ts +2 -0
- package/dist/client/skill-hub/index.d.ts +2 -1
- package/dist/client/skill-hub/tools/InteractionSchemasProvider.d.ts +1 -14
- package/dist/client/skill-hub/tools/loopTemplates.d.ts +22 -0
- package/dist/client/skill-hub/tools/registerSkillLoopCards.d.ts +1 -0
- package/dist/client/tools/PlanApprovalCard.d.ts +3 -0
- package/dist/client/tools/registerOrchestratorCards.d.ts +1 -0
- package/dist/externalVersion.js +6 -6
- package/dist/server/collections/agent-harness-profiles.d.ts +2 -0
- package/dist/server/collections/agent-harness-profiles.js +89 -0
- package/dist/server/collections/agent-loop-events.d.ts +2 -0
- package/dist/server/collections/agent-loop-events.js +101 -0
- package/dist/server/collections/agent-loop-runs.d.ts +2 -0
- package/dist/server/collections/agent-loop-runs.js +188 -0
- package/dist/server/collections/agent-loop-steps.d.ts +2 -0
- package/dist/server/collections/agent-loop-steps.js +174 -0
- package/dist/server/collections/orchestrator-config.js +7 -0
- package/dist/server/collections/skill-executions.js +12 -0
- package/dist/server/collections/skill-loop-configs.d.ts +3 -0
- package/dist/server/collections/skill-loop-configs.js +94 -0
- package/dist/server/migrations/20260524000000-add-agent-loop-fields-to-skill-executions.d.ts +7 -0
- package/dist/server/migrations/20260524000000-add-agent-loop-fields-to-skill-executions.js +55 -0
- package/dist/server/migrations/20260524001000-add-plan-approval-and-harness-profiles.d.ts +12 -0
- package/dist/server/migrations/20260524001000-add-plan-approval-and-harness-profiles.js +162 -0
- package/dist/server/plugin.d.ts +2 -0
- package/dist/server/plugin.js +13 -0
- package/dist/server/resources/agent-loop.d.ts +3 -0
- package/dist/server/resources/agent-loop.js +205 -0
- package/dist/server/services/AgentHarness.d.ts +42 -0
- package/dist/server/services/AgentHarness.js +565 -0
- package/dist/server/services/AgentLoopController.d.ts +205 -0
- package/dist/server/services/AgentLoopController.js +940 -0
- package/dist/server/services/AgentLoopRepository.d.ts +20 -0
- package/dist/server/services/AgentLoopRepository.js +210 -0
- package/dist/server/services/AgentLoopService.d.ts +149 -0
- package/dist/server/services/AgentLoopService.js +133 -0
- package/dist/server/services/AgentPlanValidator.d.ts +4 -0
- package/dist/server/services/AgentPlanValidator.js +99 -0
- package/dist/server/services/AgentPlannerService.d.ts +8 -0
- package/dist/server/services/AgentPlannerService.js +119 -0
- package/dist/server/services/AgentRegistryService.d.ts +13 -0
- package/dist/server/services/AgentRegistryService.js +178 -0
- package/dist/server/services/ExecutionSpanService.d.ts +2 -0
- package/dist/server/skill-hub/plugin.d.ts +3 -0
- package/dist/server/skill-hub/plugin.js +137 -54
- package/dist/server/tools/agent-loop.d.ts +235 -0
- package/dist/server/tools/agent-loop.js +406 -0
- package/dist/server/tools/delegate-task.js +37 -350
- package/dist/server/tools/orchestrator-plan.d.ts +205 -0
- package/dist/server/tools/orchestrator-plan.js +291 -0
- package/dist/server/tools/skill-execute.js +2 -0
- package/package.json +2 -2
- package/src/client/AgentRunsTab.tsx +764 -0
- package/src/client/HarnessProfilesTab.tsx +247 -0
- package/src/client/OrchestratorSettings.tsx +40 -2
- package/src/client/RulesTab.tsx +103 -6
- package/src/client/plugin.tsx +27 -54
- package/src/client/skill-hub/components/LoopSettings.tsx +331 -0
- package/src/client/skill-hub/index.tsx +51 -75
- package/src/client/skill-hub/tools/InteractionSchemasProvider.tsx +56 -16
- package/src/client/skill-hub/tools/SkillHubCard.tsx +35 -4
- package/src/client/skill-hub/tools/loopTemplates.ts +52 -0
- package/src/client/skill-hub/tools/registerSkillLoopCards.ts +58 -0
- package/src/client/tools/PlanApprovalCard.tsx +175 -0
- package/src/client/tools/registerOrchestratorCards.ts +7 -0
- package/src/server/collections/agent-harness-profiles.ts +59 -0
- package/src/server/collections/agent-loop-events.ts +71 -0
- package/src/server/collections/agent-loop-runs.ts +158 -0
- package/src/server/collections/agent-loop-steps.ts +144 -0
- package/src/server/collections/orchestrator-config.ts +7 -0
- package/src/server/collections/skill-executions.ts +63 -51
- package/src/server/collections/skill-loop-configs.ts +65 -0
- package/src/server/migrations/20260524000000-add-agent-loop-fields-to-skill-executions.ts +30 -0
- package/src/server/migrations/20260524001000-add-plan-approval-and-harness-profiles.ts +142 -0
- package/src/server/plugin.ts +15 -0
- package/src/server/resources/agent-loop.ts +183 -0
- package/src/server/services/AgentHarness.ts +663 -0
- package/src/server/services/AgentLoopController.ts +1128 -0
- package/src/server/services/AgentLoopRepository.ts +194 -0
- package/src/server/services/AgentLoopService.ts +161 -0
- package/src/server/services/AgentPlanValidator.ts +73 -0
- package/src/server/services/AgentPlannerService.ts +93 -0
- package/src/server/services/AgentRegistryService.ts +169 -0
- package/src/server/services/ExecutionSpanService.ts +2 -0
- package/src/server/skill-hub/plugin.ts +881 -771
- package/src/server/tools/agent-loop.ts +399 -0
- package/src/server/tools/delegate-task.ts +48 -463
- package/src/server/tools/orchestrator-plan.ts +279 -0
- package/src/server/tools/skill-execute.ts +68 -64
|
@@ -0,0 +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,75 +1,51 @@
|
|
|
1
|
-
import { Plugin } from '@nocobase/client';
|
|
2
|
-
import { SkillManager } from './components/SkillManager';
|
|
3
|
-
import { ExecutionHistory } from './components/ExecutionHistory';
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
import { InteractionSchemasProvider } from './tools/InteractionSchemasProvider';
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
await this.
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
try {
|
|
54
|
-
const { data } = await (this as any).app.apiClient.request({
|
|
55
|
-
url: 'skillDefinitions:list',
|
|
56
|
-
params: {
|
|
57
|
-
filter: { enabled: true },
|
|
58
|
-
fields: ['name', 'autoCall', 'interactionSchema'],
|
|
59
|
-
pageSize: 200,
|
|
60
|
-
},
|
|
61
|
-
});
|
|
62
|
-
const list = (data as any)?.data ?? [];
|
|
63
|
-
for (const s of list) {
|
|
64
|
-
if (s.autoCall) continue;
|
|
65
|
-
if (!parseJsonText(s.interactionSchema, null)) continue;
|
|
66
|
-
toolsManager.registerTools(`skill_hub_${sanitize(s.name)}`, { ui: { card: SkillHubCard } });
|
|
67
|
-
}
|
|
68
|
-
} catch {
|
|
69
|
-
// user without ACL or backend unavailable — skip silently
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export { SkillManager, ExecutionHistory, SkillMetrics };
|
|
75
|
-
export default PluginSkillHubClient;
|
|
1
|
+
import { Plugin } from '@nocobase/client';
|
|
2
|
+
import { SkillManager } from './components/SkillManager';
|
|
3
|
+
import { ExecutionHistory } from './components/ExecutionHistory';
|
|
4
|
+
import { SkillMetrics } from './components/SkillMetrics';
|
|
5
|
+
import { LoopSettings } from './components/LoopSettings';
|
|
6
|
+
import { InteractionSchemasProvider } from './tools/InteractionSchemasProvider';
|
|
7
|
+
import { registerSkillLoopCards } from './tools/registerSkillLoopCards';
|
|
8
|
+
|
|
9
|
+
export class PluginSkillHubClient extends Plugin {
|
|
10
|
+
async load() {
|
|
11
|
+
(this as any).app.use(InteractionSchemasProvider);
|
|
12
|
+
|
|
13
|
+
(this as any).app.pluginSettingsManager.add('skill-hub', {
|
|
14
|
+
title: (this as any).t('Skill Hub'),
|
|
15
|
+
icon: 'CodeOutlined',
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
(this as any).app.pluginSettingsManager.add('skill-hub.definitions', {
|
|
19
|
+
title: (this as any).t('Skill Definitions'),
|
|
20
|
+
Component: SkillManager,
|
|
21
|
+
aclSnippet: 'pm.skill-hub',
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
(this as any).app.pluginSettingsManager.add('skill-hub.loop-settings', {
|
|
25
|
+
title: (this as any).t('Skill Review Settings'),
|
|
26
|
+
Component: LoopSettings,
|
|
27
|
+
aclSnippet: 'pm.skill-hub',
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
(this as any).app.pluginSettingsManager.add('skill-hub.executions', {
|
|
31
|
+
title: (this as any).t('Execution History'),
|
|
32
|
+
Component: ExecutionHistory,
|
|
33
|
+
aclSnippet: 'pm.skill-hub',
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
(this as any).app.pluginSettingsManager.add('skill-hub.metrics', {
|
|
37
|
+
title: (this as any).t('Dashboard Metrics'),
|
|
38
|
+
Component: SkillMetrics,
|
|
39
|
+
aclSnippet: 'pm.skill-hub',
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
await this.registerSkillUiCards();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private async registerSkillUiCards() {
|
|
46
|
+
await registerSkillLoopCards((this as any).app);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export { SkillManager, ExecutionHistory, SkillMetrics, LoopSettings };
|
|
51
|
+
export default PluginSkillHubClient;
|
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
import React, { createContext, useContext, useEffect, useState } from 'react';
|
|
2
2
|
import { useAPIClient } from '@nocobase/client';
|
|
3
3
|
import { parseJsonText } from '../utils/jsonFields';
|
|
4
|
-
|
|
5
|
-
export type InteractionSchema = {
|
|
6
|
-
type: 'form' | 'select' | 'confirm';
|
|
7
|
-
prompt: string;
|
|
8
|
-
options?: { label: string; value: string | number }[];
|
|
9
|
-
fields?: Record<string, { type?: string; title?: string; required?: boolean; enum?: any[] }>;
|
|
10
|
-
};
|
|
4
|
+
import { InteractionSchema } from './loopTemplates';
|
|
11
5
|
|
|
12
6
|
const Ctx = createContext<Map<string, InteractionSchema>>(new Map());
|
|
13
7
|
|
|
@@ -23,28 +17,74 @@ const sanitize = (name: string) =>
|
|
|
23
17
|
export const InteractionSchemasProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
|
|
24
18
|
const api = useAPIClient();
|
|
25
19
|
const [map, setMap] = useState<Map<string, InteractionSchema>>(new Map());
|
|
20
|
+
const [version, setVersion] = useState(0);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const refresh = () => setVersion((current) => current + 1);
|
|
24
|
+
window.addEventListener('skill-hub-loop-settings-changed', refresh);
|
|
25
|
+
return () => window.removeEventListener('skill-hub-loop-settings-changed', refresh);
|
|
26
|
+
}, []);
|
|
26
27
|
|
|
27
28
|
useEffect(() => {
|
|
28
29
|
let cancelled = false;
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
const extractList = (data: any) => {
|
|
31
|
+
const value = data?.data?.data ?? data?.data ?? data ?? [];
|
|
32
|
+
return Array.isArray(value) ? value : [];
|
|
33
|
+
};
|
|
34
|
+
const schemaFromLoopConfig = (config: any): InteractionSchema | null => {
|
|
35
|
+
const schema = parseJsonText<InteractionSchema | null>(config.schema, null);
|
|
36
|
+
if (schema) {
|
|
37
|
+
return config.prompt && !schema.prompt ? { ...schema, prompt: config.prompt } : schema;
|
|
38
|
+
}
|
|
39
|
+
if (config.prompt) {
|
|
40
|
+
return {
|
|
41
|
+
type: 'confirm',
|
|
42
|
+
prompt: config.prompt,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
Promise.all([
|
|
49
|
+
api.request({
|
|
31
50
|
url: 'skillDefinitions:list',
|
|
32
51
|
params: {
|
|
33
52
|
filter: { enabled: true },
|
|
34
|
-
fields: ['name', 'autoCall', 'interactionSchema'],
|
|
35
|
-
pageSize:
|
|
53
|
+
fields: ['id', 'name', 'autoCall', 'interactionSchema'],
|
|
54
|
+
pageSize: 500,
|
|
36
55
|
},
|
|
37
|
-
})
|
|
38
|
-
.
|
|
56
|
+
}),
|
|
57
|
+
api.request({
|
|
58
|
+
url: 'skillLoopConfigs:list',
|
|
59
|
+
params: {
|
|
60
|
+
filter: { enabled: true },
|
|
61
|
+
fields: ['skillId', 'enabled', 'schema', 'prompt', 'templateKey'],
|
|
62
|
+
pageSize: 500,
|
|
63
|
+
},
|
|
64
|
+
}).catch(() => ({ data: [] })),
|
|
65
|
+
])
|
|
66
|
+
.then(([skillsResponse, loopConfigsResponse]) => {
|
|
39
67
|
if (cancelled) return;
|
|
40
68
|
const next = new Map<string, InteractionSchema>();
|
|
41
|
-
const
|
|
42
|
-
|
|
69
|
+
const skills = extractList(skillsResponse.data);
|
|
70
|
+
const loopConfigs = extractList(loopConfigsResponse.data);
|
|
71
|
+
const skillsById = new Map(skills.map((skill: any) => [String(skill.id), skill]));
|
|
72
|
+
|
|
73
|
+
for (const s of skills) {
|
|
43
74
|
if (s.autoCall) continue;
|
|
44
75
|
const schema = parseJsonText<InteractionSchema | null>(s.interactionSchema, null);
|
|
45
76
|
if (!schema) continue;
|
|
46
77
|
next.set(sanitize(s.name), schema);
|
|
47
78
|
}
|
|
79
|
+
|
|
80
|
+
for (const config of loopConfigs) {
|
|
81
|
+
const skill = skillsById.get(String(config.skillId));
|
|
82
|
+
if (!skill?.name) continue;
|
|
83
|
+
const schema = schemaFromLoopConfig(config);
|
|
84
|
+
if (!schema) continue;
|
|
85
|
+
next.set(sanitize(skill.name), schema);
|
|
86
|
+
}
|
|
87
|
+
|
|
48
88
|
setMap(next);
|
|
49
89
|
})
|
|
50
90
|
.catch(() => {
|
|
@@ -53,7 +93,7 @@ export const InteractionSchemasProvider: React.FC<{ children?: React.ReactNode }
|
|
|
53
93
|
return () => {
|
|
54
94
|
cancelled = true;
|
|
55
95
|
};
|
|
56
|
-
}, [api]);
|
|
96
|
+
}, [api, version]);
|
|
57
97
|
|
|
58
98
|
return <Ctx.Provider value={map}>{children}</Ctx.Provider>;
|
|
59
99
|
};
|
|
@@ -7,17 +7,48 @@ export const SkillHubCard: React.FC<ToolsUIProperties> = ({ toolCall, decisions
|
|
|
7
7
|
const schemas = useInteractionSchemas();
|
|
8
8
|
const [form] = Form.useForm();
|
|
9
9
|
|
|
10
|
-
const
|
|
10
|
+
const sanitize = (name: string) =>
|
|
11
|
+
name
|
|
12
|
+
.toLowerCase()
|
|
13
|
+
.replace(/[^a-z0-9_]/g, '_')
|
|
14
|
+
.replace(/_+/g, '_')
|
|
15
|
+
.replace(/^_|_$/g, '');
|
|
16
|
+
const isGenericExecutor = toolCall.name === 'skill_hub_execute';
|
|
17
|
+
const rawArgs = (toolCall.args as Record<string, any>) || {};
|
|
18
|
+
const skillKey = isGenericExecutor ? sanitize(rawArgs.skillName || '') : toolCall.name.replace(/^skill_hub_/, '');
|
|
11
19
|
const schema = schemas.get(skillKey);
|
|
12
20
|
|
|
13
21
|
const interrupted = toolCall.invokeStatus === 'init' || toolCall.invokeStatus === 'interrupted';
|
|
14
|
-
if (!
|
|
22
|
+
if (!interrupted) {
|
|
15
23
|
return null;
|
|
16
24
|
}
|
|
17
25
|
|
|
26
|
+
if (!schema) {
|
|
27
|
+
return (
|
|
28
|
+
<Card size="small" style={{ marginTop: 8 }}>
|
|
29
|
+
<Typography.Paragraph style={{ marginBottom: 12 }}>
|
|
30
|
+
Review this Skill Hub tool call before execution.
|
|
31
|
+
</Typography.Paragraph>
|
|
32
|
+
<Space>
|
|
33
|
+
<Button type="primary" onClick={() => decisions.approve()}>
|
|
34
|
+
Run
|
|
35
|
+
</Button>
|
|
36
|
+
<Button onClick={() => decisions.reject('user_cancel')}>Cancel</Button>
|
|
37
|
+
</Space>
|
|
38
|
+
</Card>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
18
42
|
const onSubmit = async () => {
|
|
19
43
|
const values = await form.validateFields();
|
|
20
|
-
const args =
|
|
44
|
+
const args = isGenericExecutor
|
|
45
|
+
? {
|
|
46
|
+
...rawArgs,
|
|
47
|
+
input: schema.type === 'select' ? values : { ...(rawArgs.input || {}), ...values },
|
|
48
|
+
}
|
|
49
|
+
: schema.type === 'select'
|
|
50
|
+
? values
|
|
51
|
+
: { ...rawArgs, ...values };
|
|
21
52
|
await decisions.edit(args);
|
|
22
53
|
};
|
|
23
54
|
|
|
@@ -39,7 +70,7 @@ export const SkillHubCard: React.FC<ToolsUIProperties> = ({ toolCall, decisions
|
|
|
39
70
|
<Form
|
|
40
71
|
form={form}
|
|
41
72
|
layout="vertical"
|
|
42
|
-
initialValues={(
|
|
73
|
+
initialValues={(isGenericExecutor ? rawArgs.input : rawArgs) || {}}
|
|
43
74
|
style={{ marginBottom: 8 }}
|
|
44
75
|
>
|
|
45
76
|
{schema.type === 'select' && (
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export type InteractionSchema = {
|
|
2
|
+
type: 'form' | 'select' | 'confirm';
|
|
3
|
+
prompt: string;
|
|
4
|
+
options?: { label: string; value: string | number }[];
|
|
5
|
+
fields?: Record<string, { type?: string; title?: string; required?: boolean; enum?: any[] }>;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type LoopTemplate = {
|
|
9
|
+
key: string;
|
|
10
|
+
title: string;
|
|
11
|
+
description: string;
|
|
12
|
+
schema: InteractionSchema;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const LOOP_TEMPLATES: LoopTemplate[] = [
|
|
16
|
+
{
|
|
17
|
+
key: 'confirm',
|
|
18
|
+
title: 'Confirm before run',
|
|
19
|
+
description: 'Ask the user to approve or cancel the skill call.',
|
|
20
|
+
schema: {
|
|
21
|
+
type: 'confirm',
|
|
22
|
+
prompt: 'Review this skill call before execution.',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
key: 'review-input',
|
|
27
|
+
title: 'Review editable input',
|
|
28
|
+
description: 'Show selected input fields so the user can edit them before running.',
|
|
29
|
+
schema: {
|
|
30
|
+
type: 'form',
|
|
31
|
+
prompt: 'Review and edit the skill input before execution.',
|
|
32
|
+
fields: {},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
key: 'choose-option',
|
|
37
|
+
title: 'Choose one option',
|
|
38
|
+
description: 'Ask the user to choose one option before running.',
|
|
39
|
+
schema: {
|
|
40
|
+
type: 'select',
|
|
41
|
+
prompt: 'Choose how to run this skill.',
|
|
42
|
+
options: [
|
|
43
|
+
{ label: 'Default', value: 'default' },
|
|
44
|
+
{ label: 'Careful', value: 'careful' },
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
export function getLoopTemplate(key?: string) {
|
|
51
|
+
return LOOP_TEMPLATES.find((template) => template.key === key) || LOOP_TEMPLATES[0];
|
|
52
|
+
}
|