@saltcorn/copilot 0.7.4 → 0.8.0
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/actions/generate-js-action.js +43 -5
- package/actions/generate-tables.js +281 -96
- package/actions/generate-workflow.js +83 -36
- package/actions/install-plugin-action.js +103 -0
- package/agent-skills/app-constructor-context.js +25 -0
- package/agent-skills/database-design.js +139 -87
- package/agent-skills/install-plugin.js +111 -0
- package/agent-skills/js-action.js +183 -0
- package/agent-skills/registry-editor.js +911 -0
- package/agent-skills/viewgen.js +302 -53
- package/agent-skills/workflow.js +52 -2
- package/app-constructor/common.js +12 -0
- package/app-constructor/errors.js +102 -0
- package/app-constructor/feedback-action.js +175 -0
- package/app-constructor/feedback.js +112 -0
- package/app-constructor/progress.js +116 -0
- package/app-constructor/prompts.js +58 -0
- package/app-constructor/requirements.js +156 -0
- package/app-constructor/run_task.js +128 -0
- package/app-constructor/schema.js +186 -0
- package/app-constructor/taskchart.js +70 -0
- package/app-constructor/tasks.js +401 -0
- package/app-constructor/tools.js +81 -0
- package/app-constructor/view.js +209 -0
- package/builder-gen.js +1505 -24
- package/builder-schema.js +706 -0
- package/common.js +20 -0
- package/copilot-as-agent.js +6 -1
- package/index.js +24 -1
- package/js-code-gen.js +65 -0
- package/package.json +1 -1
- package/standard-prompt.js +83 -0
- package/tests/builder-gen.test.js +56 -0
package/agent-skills/viewgen.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const Table = require("@saltcorn/data/models/table");
|
|
2
2
|
const View = require("@saltcorn/data/models/view");
|
|
3
3
|
const { fieldProperties } = require("../common");
|
|
4
|
+
const { initial_config_all_fields } = require("@saltcorn/data/plugin-helper");
|
|
4
5
|
const { getState } = require("@saltcorn/data/db/state");
|
|
5
6
|
const {
|
|
6
7
|
div,
|
|
@@ -12,6 +13,91 @@ const {
|
|
|
12
13
|
iframe,
|
|
13
14
|
text_attr,
|
|
14
15
|
} = require("@saltcorn/markup/tags");
|
|
16
|
+
const builderGen = require("../builder-gen");
|
|
17
|
+
|
|
18
|
+
const collectLayoutFieldNames = (segment, out = new Set()) => {
|
|
19
|
+
if (!segment || typeof segment !== "object") return out;
|
|
20
|
+
if (Array.isArray(segment)) {
|
|
21
|
+
segment.forEach((s) => collectLayoutFieldNames(s, out));
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
if (segment.type === "field" && segment.field_name)
|
|
25
|
+
out.add(segment.field_name);
|
|
26
|
+
if (segment.above) collectLayoutFieldNames(segment.above, out);
|
|
27
|
+
if (segment.besides) collectLayoutFieldNames(segment.besides, out);
|
|
28
|
+
if (segment.contents) collectLayoutFieldNames(segment.contents, out);
|
|
29
|
+
if (Array.isArray(segment.tabs))
|
|
30
|
+
segment.tabs.forEach((t) => collectLayoutFieldNames(t?.contents, out));
|
|
31
|
+
return out;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const findFilterFieldSegment = (segment) => {
|
|
35
|
+
if (!segment || typeof segment !== "object") return null;
|
|
36
|
+
if (segment.type === "field") return segment;
|
|
37
|
+
if (segment.type === "dropdown_filter" || segment.type === "toggle_filter") {
|
|
38
|
+
return { field_name: segment.field_name, fieldview: "edit" };
|
|
39
|
+
}
|
|
40
|
+
if (Array.isArray(segment.above)) {
|
|
41
|
+
for (const item of segment.above) {
|
|
42
|
+
const found = findFilterFieldSegment(item);
|
|
43
|
+
if (found) return found;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (Array.isArray(segment.besides)) {
|
|
47
|
+
for (const item of segment.besides) {
|
|
48
|
+
const found = findFilterFieldSegment(item);
|
|
49
|
+
if (found) return found;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (segment.contents) {
|
|
53
|
+
if (Array.isArray(segment.contents)) {
|
|
54
|
+
for (const item of segment.contents) {
|
|
55
|
+
const found = findFilterFieldSegment(item);
|
|
56
|
+
if (found) return found;
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
const found = findFilterFieldSegment(segment.contents);
|
|
60
|
+
if (found) return found;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (Array.isArray(segment.tabs)) {
|
|
64
|
+
for (const tab of segment.tabs) {
|
|
65
|
+
if (tab?.contents) {
|
|
66
|
+
const found = findFilterFieldSegment(tab.contents);
|
|
67
|
+
if (found) return found;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (Array.isArray(segment.contents) && Array.isArray(segment.contents[0])) {
|
|
72
|
+
for (const row of segment.contents) {
|
|
73
|
+
if (Array.isArray(row)) {
|
|
74
|
+
for (const cell of row) {
|
|
75
|
+
const found = findFilterFieldSegment(cell);
|
|
76
|
+
if (found) return found;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const normalizeFilterField = (segment) => ({
|
|
85
|
+
type: "field",
|
|
86
|
+
field_name: segment.field_name,
|
|
87
|
+
fieldview: segment.fieldview || "edit",
|
|
88
|
+
textStyle: segment.textStyle || "",
|
|
89
|
+
block: segment.block ?? false,
|
|
90
|
+
configuration: segment.configuration || {},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const toFilterColumn = (segment) => ({
|
|
94
|
+
type: "Field",
|
|
95
|
+
field_name: segment.field_name,
|
|
96
|
+
fieldview: segment.fieldview || "edit",
|
|
97
|
+
textStyle: segment.textStyle || "",
|
|
98
|
+
block: segment.block ?? false,
|
|
99
|
+
configuration: segment.configuration || {},
|
|
100
|
+
});
|
|
15
101
|
|
|
16
102
|
class GenerateViewSkill {
|
|
17
103
|
static skill_name = "Generate View";
|
|
@@ -25,8 +111,11 @@ class GenerateViewSkill {
|
|
|
25
111
|
}
|
|
26
112
|
|
|
27
113
|
async systemPrompt() {
|
|
28
|
-
return
|
|
29
|
-
|
|
114
|
+
return (
|
|
115
|
+
`If the user asks to generate a view, use the generate_view tool to enter ` +
|
|
116
|
+
`a view generation mode. The tool call only requires high-level details to start this sequence.\n` +
|
|
117
|
+
`The Edit viewtemplate serves both create (no id in state) and edit (id in state) — one view covers both modes.`
|
|
118
|
+
);
|
|
30
119
|
}
|
|
31
120
|
|
|
32
121
|
get userActions() {
|
|
@@ -38,13 +127,20 @@ a view generation mode. The tool call only requires high-level details to start
|
|
|
38
127
|
table,
|
|
39
128
|
min_role,
|
|
40
129
|
}) {
|
|
130
|
+
const normalizedRole = min_role || "public";
|
|
131
|
+
const tableRow = table ? Table.findOne({ name: table }) : null;
|
|
41
132
|
await View.create({
|
|
42
133
|
name,
|
|
43
134
|
viewtemplate: viewpattern,
|
|
44
|
-
|
|
45
|
-
|
|
135
|
+
table_id: tableRow?.id,
|
|
136
|
+
table: tableRow,
|
|
137
|
+
min_role: { admin: 1, public: 100, user: 80 }[normalizedRole],
|
|
46
138
|
configuration: wfctx,
|
|
47
139
|
});
|
|
140
|
+
const vt = getState().viewtemplates[viewpattern];
|
|
141
|
+
if (vt?.copilot_post_create) {
|
|
142
|
+
await vt.copilot_post_create({ name, configuration: wfctx });
|
|
143
|
+
}
|
|
48
144
|
setTimeout(() => getState().refresh_views(), 200);
|
|
49
145
|
return {
|
|
50
146
|
notify: `View saved: <a target="_blank" href="/view/${name}">${name}</a>`,
|
|
@@ -61,12 +157,17 @@ a view generation mode. The tool call only requires high-level details to start
|
|
|
61
157
|
const enabled_vt_names = all_vt_names.filter(
|
|
62
158
|
(vtnm) =>
|
|
63
159
|
vts[vtnm].enable_copilot_viewgen ||
|
|
64
|
-
vts[vtnm].copilot_generate_view_prompt
|
|
160
|
+
vts[vtnm].copilot_generate_view_prompt
|
|
65
161
|
);
|
|
162
|
+
if (!enabled_vt_names.includes("Show")) enabled_vt_names.push("Show");
|
|
163
|
+
if (!enabled_vt_names.includes("Edit")) enabled_vt_names.push("Edit");
|
|
164
|
+
if (!enabled_vt_names.includes("List")) enabled_vt_names.push("List");
|
|
165
|
+
if (!enabled_vt_names.includes("Filter")) enabled_vt_names.push("Filter");
|
|
66
166
|
//const roles = await User.get_roles();
|
|
67
167
|
const tableless = enabled_vt_names.filter(
|
|
68
|
-
(vtnm) => vts[vtnm].tableless === true
|
|
168
|
+
(vtnm) => vts[vtnm].tableless === true
|
|
69
169
|
);
|
|
170
|
+
const roles = state.roles;
|
|
70
171
|
const parameters = {
|
|
71
172
|
type: "object",
|
|
72
173
|
required: ["name", "viewpattern"],
|
|
@@ -76,7 +177,9 @@ a view generation mode. The tool call only requires high-level details to start
|
|
|
76
177
|
type: "string",
|
|
77
178
|
},
|
|
78
179
|
viewpattern: {
|
|
79
|
-
description: `The type of view to generate. Some of the view descriptions: ${enabled_vt_names
|
|
180
|
+
description: `The type of view to generate. Some of the view descriptions: ${enabled_vt_names
|
|
181
|
+
.map((vtnm) => `${vtnm}: ${vts[vtnm].description}.`)
|
|
182
|
+
.join(" ")}`,
|
|
80
183
|
type: "string",
|
|
81
184
|
enum: enabled_vt_names,
|
|
82
185
|
},
|
|
@@ -91,7 +194,7 @@ a view generation mode. The tool call only requires high-level details to start
|
|
|
91
194
|
description:
|
|
92
195
|
"The minimum role needed to access the view. For views accessible only by admin, use 'admin', pages with min_role 'public' is publicly accessible and also available to all users",
|
|
93
196
|
type: "string",
|
|
94
|
-
enum: ["admin", "user", "public"],
|
|
197
|
+
enum: roles ? roles.map((r) => r.role) : ["admin", "user", "public"],
|
|
95
198
|
},
|
|
96
199
|
},
|
|
97
200
|
};
|
|
@@ -107,73 +210,219 @@ a view generation mode. The tool call only requires high-level details to start
|
|
|
107
210
|
process: async (input) => {
|
|
108
211
|
return "Metadata received";
|
|
109
212
|
},
|
|
110
|
-
postProcess: async ({ tool_call, req, generate }) => {
|
|
213
|
+
postProcess: async ({ tool_call, req, generate, chat }) => {
|
|
111
214
|
const state = getState();
|
|
112
215
|
const vt = state.viewtemplates[tool_call.input.viewpattern];
|
|
113
216
|
const table =
|
|
114
217
|
vt.tableless === true
|
|
115
218
|
? null
|
|
116
219
|
: Table.findOne({ name: tool_call.input.table });
|
|
117
|
-
|
|
220
|
+
|
|
118
221
|
const wfctx = { viewname: tool_call.input.name, table_id: table?.id };
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
222
|
+
const viewpattern = tool_call.input.viewpattern;
|
|
223
|
+
const builderModeByPattern = {
|
|
224
|
+
Show: "show",
|
|
225
|
+
Edit: "edit",
|
|
226
|
+
List: "listcolumns",
|
|
227
|
+
Filter: "filter",
|
|
228
|
+
};
|
|
229
|
+
const builderMode = builderModeByPattern[viewpattern];
|
|
230
|
+
if (builderMode) {
|
|
231
|
+
const extractText = (c) => {
|
|
232
|
+
if (typeof c === "string") return c;
|
|
233
|
+
if (Array.isArray(c)) {
|
|
234
|
+
const textPart = c.find(
|
|
235
|
+
(p) => p?.type === "text" || typeof p === "string"
|
|
236
|
+
);
|
|
237
|
+
return (
|
|
238
|
+
textPart?.text || (typeof textPart === "string" ? textPart : "")
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
return "";
|
|
242
|
+
};
|
|
243
|
+
const isToolResultMessage = (item) => {
|
|
244
|
+
if (!Array.isArray(item?.content)) return false;
|
|
245
|
+
return item.content.every((p) => p?.type === "tool_result");
|
|
246
|
+
};
|
|
247
|
+
const promptFromChat = Array.isArray(chat)
|
|
248
|
+
? (() => {
|
|
249
|
+
const userMsgs = chat.filter(
|
|
250
|
+
(item) =>
|
|
251
|
+
item?.role === "user" &&
|
|
252
|
+
item?.content &&
|
|
253
|
+
!isToolResultMessage(item)
|
|
254
|
+
);
|
|
255
|
+
return userMsgs.length ? extractText(userMsgs[0].content) : "";
|
|
256
|
+
})()
|
|
257
|
+
: "";
|
|
258
|
+
const layoutPrompt = promptFromChat || tool_call.input.name || "";
|
|
259
|
+
wfctx.layout = await builderGen.run(
|
|
260
|
+
layoutPrompt,
|
|
261
|
+
builderMode,
|
|
262
|
+
table?.name,
|
|
263
|
+
null,
|
|
264
|
+
chat
|
|
265
|
+
);
|
|
266
|
+
if (table && viewpattern !== "Filter") {
|
|
267
|
+
const baseCfg = await initial_config_all_fields(false)({
|
|
268
|
+
table_id: table.id,
|
|
269
|
+
});
|
|
270
|
+
if (baseCfg?.columns) wfctx.columns = baseCfg.columns;
|
|
271
|
+
}
|
|
272
|
+
if (viewpattern === "Edit" && table) {
|
|
273
|
+
const layoutFieldNames = collectLayoutFieldNames(wfctx.layout);
|
|
274
|
+
const fields = table.fields || [];
|
|
275
|
+
const fixed = {};
|
|
276
|
+
for (const f of fields) {
|
|
277
|
+
if (f.primary_key || f.calculated) continue;
|
|
278
|
+
if (layoutFieldNames.has(f.name)) continue;
|
|
279
|
+
if (f.type === "Key" && f.reftable_name === "users") {
|
|
280
|
+
fixed[`preset_${f.name}`] = "LoggedIn";
|
|
281
|
+
fixed[`_block_${f.name}`] = true;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (Object.keys(fixed).length > 0) wfctx.fixed = fixed;
|
|
285
|
+
wfctx.destination_type = "Back to referer";
|
|
286
|
+
}
|
|
287
|
+
if (viewpattern === "Filter") {
|
|
288
|
+
const filterFieldSegment = findFilterFieldSegment(wfctx.layout);
|
|
289
|
+
if (filterFieldSegment) {
|
|
290
|
+
const normalized = normalizeFilterField(filterFieldSegment);
|
|
291
|
+
wfctx.layout = normalized;
|
|
292
|
+
wfctx.columns = [toFilterColumn(normalized)];
|
|
293
|
+
}
|
|
294
|
+
}
|
|
125
295
|
}
|
|
126
296
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
297
|
+
if (
|
|
298
|
+
viewpattern === "Show" ||
|
|
299
|
+
viewpattern === "Edit" ||
|
|
300
|
+
viewpattern === "Filter"
|
|
301
|
+
) {
|
|
302
|
+
// No extra configuration steps for these modes.
|
|
303
|
+
} else {
|
|
304
|
+
const flow = vt.configuration_workflow(req);
|
|
305
|
+
let vt_prompt = "";
|
|
306
|
+
if (vt.copilot_generate_view_prompt) {
|
|
307
|
+
if (typeof vt.copilot_generate_view_prompt === "string")
|
|
308
|
+
vt_prompt = vt.copilot_generate_view_prompt;
|
|
309
|
+
else if (typeof vt.copilot_generate_view_prompt === "function")
|
|
310
|
+
vt_prompt = await vt.copilot_generate_view_prompt(
|
|
311
|
+
tool_call.input
|
|
312
|
+
);
|
|
139
313
|
}
|
|
140
314
|
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
315
|
+
const prefilledFields = new Set();
|
|
316
|
+
if (wfctx.layout !== undefined) prefilledFields.add("layout");
|
|
317
|
+
if (wfctx.columns !== undefined) prefilledFields.add("columns");
|
|
318
|
+
|
|
319
|
+
// For List views: pre-fill view_to_create with the best Edit view for the table
|
|
320
|
+
if (viewpattern === "List" && table) {
|
|
321
|
+
const candidateViews = await View.find_table_views_where(
|
|
322
|
+
table.id,
|
|
323
|
+
({ state_fields, viewrow }) =>
|
|
324
|
+
viewrow.name !== tool_call.input.name &&
|
|
325
|
+
state_fields.every((sf) => !sf.required)
|
|
326
|
+
);
|
|
327
|
+
if (candidateViews.length > 0) {
|
|
328
|
+
const editView =
|
|
329
|
+
candidateViews.find((v) =>
|
|
330
|
+
v.name.toLowerCase().includes("edit")
|
|
331
|
+
) || candidateViews[0];
|
|
332
|
+
wfctx.view_to_create =
|
|
333
|
+
editView.select_option?.name || editView.name;
|
|
334
|
+
wfctx.create_view_display = "Popup";
|
|
335
|
+
wfctx.create_view_location = "Top right";
|
|
336
|
+
prefilledFields.add("view_to_create");
|
|
337
|
+
prefilledFields.add("create_view_display");
|
|
338
|
+
prefilledFields.add("create_view_location");
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
for (const step of flow.steps) {
|
|
343
|
+
if (typeof step.form !== "function") continue;
|
|
344
|
+
const form = await step.form(wfctx);
|
|
345
|
+
const properties = {};
|
|
346
|
+
//TODO onlyWhen
|
|
347
|
+
for (const field of form.fields) {
|
|
348
|
+
if (prefilledFields.has(field.name)) continue;
|
|
349
|
+
//TODO showIf
|
|
350
|
+
properties[field.name] = {
|
|
351
|
+
description:
|
|
352
|
+
field.copilot_description ||
|
|
353
|
+
`${field.label}.${
|
|
354
|
+
field.sublabel ? ` ${field.sublabel}` : ""
|
|
355
|
+
}`,
|
|
356
|
+
...fieldProperties(field),
|
|
357
|
+
};
|
|
358
|
+
if (!properties[field.name].type) {
|
|
359
|
+
properties[field.name].type = "string";
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (!Object.keys(properties).length) continue;
|
|
364
|
+
|
|
365
|
+
const answer = await generate(
|
|
366
|
+
`${vt_prompt ? vt_prompt + "\n\n" : ""}Now generate the ${
|
|
367
|
+
step.name
|
|
368
|
+
} details of the view by calling the generate_view_details tool`,
|
|
369
|
+
{
|
|
370
|
+
tools: [
|
|
371
|
+
{
|
|
372
|
+
type: "function",
|
|
373
|
+
function: {
|
|
374
|
+
name: "generate_view_details",
|
|
375
|
+
description: "Provide view details",
|
|
376
|
+
parameters: {
|
|
377
|
+
type: "object",
|
|
378
|
+
properties,
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
],
|
|
383
|
+
tool_choice: {
|
|
146
384
|
type: "function",
|
|
147
385
|
function: {
|
|
148
386
|
name: "generate_view_details",
|
|
149
|
-
description: "Provide view details",
|
|
150
|
-
parameters: {
|
|
151
|
-
type: "object",
|
|
152
|
-
properties,
|
|
153
|
-
},
|
|
154
387
|
},
|
|
155
388
|
},
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
Object.assign(wfctx, tc.input);
|
|
389
|
+
}
|
|
390
|
+
);
|
|
391
|
+
const tc = answer.getToolCalls()[0];
|
|
392
|
+
await getState().functions.llm_add_message.run(
|
|
393
|
+
"tool_response",
|
|
394
|
+
{ type: "text", value: "Details provided" },
|
|
395
|
+
{ chat, tool_call: tc }
|
|
396
|
+
);
|
|
397
|
+
Object.assign(wfctx, tc.input);
|
|
398
|
+
}
|
|
167
399
|
}
|
|
400
|
+
const roleName = tool_call.input.min_role || "public";
|
|
401
|
+
const rolesState = getState().roles;
|
|
402
|
+
const min_role = rolesState
|
|
403
|
+
? (rolesState.find((r) => r.role === roleName) || { id: 100 }).id
|
|
404
|
+
: { admin: 1, public: 100, user: 80 }[roleName] ?? 100;
|
|
168
405
|
const view = new View({
|
|
169
406
|
name: tool_call.input.name,
|
|
170
407
|
viewtemplate: tool_call.input.viewpattern,
|
|
171
408
|
table,
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
],
|
|
409
|
+
table_id: table?.id,
|
|
410
|
+
min_role,
|
|
175
411
|
configuration: wfctx,
|
|
176
412
|
});
|
|
413
|
+
if (this.yoloMode) {
|
|
414
|
+
await this.userActions.build_copilot_view_gen({
|
|
415
|
+
wfctx,
|
|
416
|
+
name: tool_call.input.name,
|
|
417
|
+
viewpattern: tool_call.input.viewpattern,
|
|
418
|
+
table: tool_call.input.table,
|
|
419
|
+
min_role: tool_call.input.min_role,
|
|
420
|
+
});
|
|
421
|
+
return {
|
|
422
|
+
stop: true,
|
|
423
|
+
add_response: `View ${tool_call.input.name} created.`,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
177
426
|
const runres = await view.run({}, { req });
|
|
178
427
|
return {
|
|
179
428
|
stop: true,
|
|
@@ -181,7 +430,7 @@ a view generation mode. The tool call only requires high-level details to start
|
|
|
181
430
|
pre(JSON.stringify(wfctx, null, 2)) +
|
|
182
431
|
div(
|
|
183
432
|
{ style: { maxHeight: 800, maxWidth: 500, overflow: "scroll" } },
|
|
184
|
-
runres
|
|
433
|
+
runres
|
|
185
434
|
),
|
|
186
435
|
add_user_action: {
|
|
187
436
|
name: "build_copilot_view_gen",
|
package/agent-skills/workflow.js
CHANGED
|
@@ -550,11 +550,50 @@ class GenerateWorkflowSkill {
|
|
|
550
550
|
ensureActionCatalog();
|
|
551
551
|
}
|
|
552
552
|
|
|
553
|
+
static async configFields() {
|
|
554
|
+
return [
|
|
555
|
+
{
|
|
556
|
+
name: "context_vars",
|
|
557
|
+
label: "Initial context variables",
|
|
558
|
+
input_type: "code",
|
|
559
|
+
attributes: { mode: "application/json" },
|
|
560
|
+
sublabel:
|
|
561
|
+
'JSON object of key-value pairs pre-loaded into the workflow context at startup (e.g. {"ELEVENLABS_API_KEY": "sk-..."}). Keys are available by name in every run_js_code step.',
|
|
562
|
+
},
|
|
563
|
+
];
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
_parseContextVars() {
|
|
567
|
+
if (!this.context_vars) return null;
|
|
568
|
+
try {
|
|
569
|
+
const vars =
|
|
570
|
+
typeof this.context_vars === "string"
|
|
571
|
+
? JSON.parse(this.context_vars)
|
|
572
|
+
: this.context_vars;
|
|
573
|
+
return Object.keys(vars).length ? vars : null;
|
|
574
|
+
} catch {
|
|
575
|
+
return null;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
553
579
|
async systemPrompt() {
|
|
554
|
-
|
|
580
|
+
const base = await GenerateWorkflow.system_prompt();
|
|
581
|
+
const vars = this._parseContextVars();
|
|
582
|
+
if (!vars) return base;
|
|
583
|
+
const keyList = Object.keys(vars).join(", ");
|
|
584
|
+
return (
|
|
585
|
+
base +
|
|
586
|
+
`\n\nThe following values are pre-loaded into the workflow context before the first step runs: ${keyList}. ` +
|
|
587
|
+
`Use them directly by name in run_js_code steps (e.g. \`${
|
|
588
|
+
Object.keys(vars)[0]
|
|
589
|
+
}\`) ` +
|
|
590
|
+
`or via the context object (e.g. \`context.${Object.keys(vars)[0]}\`). ` +
|
|
591
|
+
`Do not ask the user to supply these values — they are already available.`
|
|
592
|
+
);
|
|
555
593
|
}
|
|
556
594
|
|
|
557
595
|
get userActions() {
|
|
596
|
+
const context_vars = this._parseContextVars();
|
|
558
597
|
return {
|
|
559
598
|
async apply_copilot_workflow({ user, ...raw }) {
|
|
560
599
|
const payload = ensureWorkflowHasSteps(normalizeWorkflowPayload(raw));
|
|
@@ -563,7 +602,11 @@ class GenerateWorkflowSkill {
|
|
|
563
602
|
return {
|
|
564
603
|
notify: `Cannot create workflow: ${analysis.blocking.join("; ")}`,
|
|
565
604
|
};
|
|
566
|
-
const result = await GenerateWorkflow.execute(
|
|
605
|
+
const result = await GenerateWorkflow.execute(
|
|
606
|
+
payload,
|
|
607
|
+
{ user },
|
|
608
|
+
context_vars
|
|
609
|
+
);
|
|
567
610
|
return {
|
|
568
611
|
notify:
|
|
569
612
|
result?.postExec ||
|
|
@@ -608,6 +651,13 @@ class GenerateWorkflowSkill {
|
|
|
608
651
|
const canCreate =
|
|
609
652
|
analysis.blocking.length === 0 &&
|
|
610
653
|
preparedPayload.workflow_steps.length > 0;
|
|
654
|
+
if (this.yoloMode && canCreate) {
|
|
655
|
+
await this.userActions.apply_copilot_workflow(preparedPayload);
|
|
656
|
+
return {
|
|
657
|
+
stop: true,
|
|
658
|
+
add_response: `Workflow ${preparedPayload.workflow_name || "(unnamed)"} created.`,
|
|
659
|
+
};
|
|
660
|
+
}
|
|
611
661
|
return {
|
|
612
662
|
stop: true,
|
|
613
663
|
add_response: `${issuesHtml}${previewHtml}`,
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
const Field = require("@saltcorn/data/models/field");
|
|
2
|
+
const Table = require("@saltcorn/data/models/table");
|
|
3
|
+
const Form = require("@saltcorn/data/models/form");
|
|
4
|
+
const MetaData = require("@saltcorn/data/models/metadata");
|
|
5
|
+
const View = require("@saltcorn/data/models/view");
|
|
6
|
+
const Trigger = require("@saltcorn/data/models/trigger");
|
|
7
|
+
const { findType } = require("@saltcorn/data/models/discovery");
|
|
8
|
+
const { save_menu_items } = require("@saltcorn/data/models/config");
|
|
9
|
+
const db = require("@saltcorn/data/db");
|
|
10
|
+
const WorkflowRun = require("@saltcorn/data/models/workflow_run");
|
|
11
|
+
const {
|
|
12
|
+
localeDateTime,
|
|
13
|
+
renderForm,
|
|
14
|
+
mkTable,
|
|
15
|
+
post_delete_btn,
|
|
16
|
+
} = require("@saltcorn/markup");
|
|
17
|
+
const {
|
|
18
|
+
div,
|
|
19
|
+
script,
|
|
20
|
+
domReady,
|
|
21
|
+
pre,
|
|
22
|
+
code,
|
|
23
|
+
input,
|
|
24
|
+
h4,
|
|
25
|
+
style,
|
|
26
|
+
h5,
|
|
27
|
+
button,
|
|
28
|
+
text_attr,
|
|
29
|
+
i,
|
|
30
|
+
p,
|
|
31
|
+
span,
|
|
32
|
+
small,
|
|
33
|
+
form,
|
|
34
|
+
textarea,
|
|
35
|
+
} = require("@saltcorn/markup/tags");
|
|
36
|
+
const { getState } = require("@saltcorn/data/db/state");
|
|
37
|
+
const renderLayout = require("@saltcorn/markup/layout");
|
|
38
|
+
const { viewname } = require("./common");
|
|
39
|
+
|
|
40
|
+
const errorList = async (req) => {
|
|
41
|
+
const errs = await MetaData.find({
|
|
42
|
+
type: "CopilotConstructMgr",
|
|
43
|
+
name: "error",
|
|
44
|
+
});
|
|
45
|
+
if (errs.length) {
|
|
46
|
+
return div(
|
|
47
|
+
{ class: "mt-2" },
|
|
48
|
+
mkTable(
|
|
49
|
+
[
|
|
50
|
+
{ label: "Status", key: (m) => m.body.status },
|
|
51
|
+
{
|
|
52
|
+
label: "Error",
|
|
53
|
+
key: (m) => pre(JSON.stringify(m.body.error, null, 2)),
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
label: "Delete",
|
|
57
|
+
key: (r) =>
|
|
58
|
+
button(
|
|
59
|
+
{
|
|
60
|
+
class: "btn btn-outline-danger btn-sm",
|
|
61
|
+
onclick: `view_post("${viewname}", "del_err", {id:${r.id}})`,
|
|
62
|
+
},
|
|
63
|
+
i({ class: "fas fa-trash-alt" }),
|
|
64
|
+
),
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
errs,
|
|
68
|
+
),
|
|
69
|
+
button(
|
|
70
|
+
{
|
|
71
|
+
class: "btn btn-outline-danger",
|
|
72
|
+
onclick: `view_post("${viewname}", "del_all_errs")`,
|
|
73
|
+
},
|
|
74
|
+
"Delete all",
|
|
75
|
+
),
|
|
76
|
+
);
|
|
77
|
+
} else {
|
|
78
|
+
return div({ class: "mt-2" }, p("No errors"));
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const del_err = async (table_id, viewname, config, body, { req, res }) => {
|
|
83
|
+
const r = await MetaData.findOne({
|
|
84
|
+
id: body.id,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (!r) throw new Error("Error not found");
|
|
88
|
+
await r.delete();
|
|
89
|
+
return { json: { reload_page: true } };
|
|
90
|
+
};
|
|
91
|
+
const del_all_errs = async (table_id, viewname, config, body, { req, res }) => {
|
|
92
|
+
const rs = await MetaData.find({
|
|
93
|
+
type: "CopilotConstructMgr",
|
|
94
|
+
name: "error",
|
|
95
|
+
});
|
|
96
|
+
for (const r of rs) await r.delete();
|
|
97
|
+
return { json: { reload_page: true } };
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const error_routes = { del_err, del_all_errs };
|
|
101
|
+
|
|
102
|
+
module.exports = { errorList, error_routes };
|