@saltcorn/copilot 0.7.5 → 0.8.1
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-trigger.js +61 -0
- package/actions/generate-workflow.js +89 -37
- package/actions/install-plugin-action.js +103 -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/pagegen.js +19 -6
- package/agent-skills/registry-editor.js +911 -0
- package/agent-skills/triggergen.js +263 -0
- package/agent-skills/viewgen.js +431 -29
- 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 +120 -0
- package/app-constructor/requirements.js +156 -0
- package/app-constructor/run_task.js +146 -0
- package/app-constructor/schema.js +199 -0
- package/app-constructor/taskchart.js +70 -0
- package/app-constructor/tasks.js +585 -0
- package/app-constructor/tools.js +81 -0
- package/app-constructor/view.js +209 -0
- package/builder-gen.js +590 -68
- package/builder-schema.js +26 -6
- package/chat-copilot.js +1 -0
- package/common.js +20 -0
- package/copilot-as-agent.js +7 -1
- package/index.js +23 -1
- package/js-code-gen.js +65 -0
- package/package.json +1 -1
- package/relation-paths.js +236 -0
- package/tests/builder-gen.test.js +56 -0
package/agent-skills/viewgen.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
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 {
|
|
4
|
+
const {
|
|
5
|
+
initial_config_all_fields,
|
|
6
|
+
build_schema_data,
|
|
7
|
+
} = require("@saltcorn/data/plugin-helper");
|
|
5
8
|
const { getState } = require("@saltcorn/data/db/state");
|
|
6
9
|
const {
|
|
7
10
|
div,
|
|
@@ -14,6 +17,95 @@ const {
|
|
|
14
17
|
text_attr,
|
|
15
18
|
} = require("@saltcorn/markup/tags");
|
|
16
19
|
const builderGen = require("../builder-gen");
|
|
20
|
+
const {
|
|
21
|
+
RELATION_PATH_DOC,
|
|
22
|
+
GET_RELATION_PATHS_FUNCTION,
|
|
23
|
+
getRelationPathsForPairs,
|
|
24
|
+
} = require("../relation-paths");
|
|
25
|
+
|
|
26
|
+
const collectLayoutFieldNames = (segment, out = new Set()) => {
|
|
27
|
+
if (!segment || typeof segment !== "object") return out;
|
|
28
|
+
if (Array.isArray(segment)) {
|
|
29
|
+
segment.forEach((s) => collectLayoutFieldNames(s, out));
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
if (segment.type === "field" && segment.field_name)
|
|
33
|
+
out.add(segment.field_name);
|
|
34
|
+
if (segment.above) collectLayoutFieldNames(segment.above, out);
|
|
35
|
+
if (segment.besides) collectLayoutFieldNames(segment.besides, out);
|
|
36
|
+
if (segment.contents) collectLayoutFieldNames(segment.contents, out);
|
|
37
|
+
if (Array.isArray(segment.tabs))
|
|
38
|
+
segment.tabs.forEach((t) => collectLayoutFieldNames(t?.contents, out));
|
|
39
|
+
return out;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const findFilterFieldSegment = (segment) => {
|
|
43
|
+
if (!segment || typeof segment !== "object") return null;
|
|
44
|
+
if (segment.type === "field") return segment;
|
|
45
|
+
if (segment.type === "dropdown_filter" || segment.type === "toggle_filter") {
|
|
46
|
+
return { field_name: segment.field_name, fieldview: "edit" };
|
|
47
|
+
}
|
|
48
|
+
if (Array.isArray(segment.above)) {
|
|
49
|
+
for (const item of segment.above) {
|
|
50
|
+
const found = findFilterFieldSegment(item);
|
|
51
|
+
if (found) return found;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (Array.isArray(segment.besides)) {
|
|
55
|
+
for (const item of segment.besides) {
|
|
56
|
+
const found = findFilterFieldSegment(item);
|
|
57
|
+
if (found) return found;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (segment.contents) {
|
|
61
|
+
if (Array.isArray(segment.contents)) {
|
|
62
|
+
for (const item of segment.contents) {
|
|
63
|
+
const found = findFilterFieldSegment(item);
|
|
64
|
+
if (found) return found;
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
const found = findFilterFieldSegment(segment.contents);
|
|
68
|
+
if (found) return found;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (Array.isArray(segment.tabs)) {
|
|
72
|
+
for (const tab of segment.tabs) {
|
|
73
|
+
if (tab?.contents) {
|
|
74
|
+
const found = findFilterFieldSegment(tab.contents);
|
|
75
|
+
if (found) return found;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (Array.isArray(segment.contents) && Array.isArray(segment.contents[0])) {
|
|
80
|
+
for (const row of segment.contents) {
|
|
81
|
+
if (Array.isArray(row)) {
|
|
82
|
+
for (const cell of row) {
|
|
83
|
+
const found = findFilterFieldSegment(cell);
|
|
84
|
+
if (found) return found;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const normalizeFilterField = (segment) => ({
|
|
93
|
+
type: "field",
|
|
94
|
+
field_name: segment.field_name,
|
|
95
|
+
fieldview: segment.fieldview || "edit",
|
|
96
|
+
textStyle: segment.textStyle || "",
|
|
97
|
+
block: segment.block ?? false,
|
|
98
|
+
configuration: segment.configuration || {},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const toFilterColumn = (segment) => ({
|
|
102
|
+
type: "Field",
|
|
103
|
+
field_name: segment.field_name,
|
|
104
|
+
fieldview: segment.fieldview || "edit",
|
|
105
|
+
textStyle: segment.textStyle || "",
|
|
106
|
+
block: segment.block ?? false,
|
|
107
|
+
configuration: segment.configuration || {},
|
|
108
|
+
});
|
|
17
109
|
|
|
18
110
|
class GenerateViewSkill {
|
|
19
111
|
static skill_name = "Generate View";
|
|
@@ -27,12 +119,35 @@ class GenerateViewSkill {
|
|
|
27
119
|
}
|
|
28
120
|
|
|
29
121
|
async systemPrompt() {
|
|
30
|
-
return
|
|
31
|
-
a view
|
|
122
|
+
return (
|
|
123
|
+
`If the user asks to generate a view, use the generate_view tool — but ONLY if the view does not already exist. ` +
|
|
124
|
+
`If a view with that name already exists, do NOT call generate_view — doing so will create a duplicate. Instead follow the modification sequence below.\n` +
|
|
125
|
+
`The Edit viewtemplate serves both create (no id in state) and edit (id in state) — one view covers both.\n\n` +
|
|
126
|
+
`**Modifying an existing view — required sequence:**\n` +
|
|
127
|
+
`(1) Call get_view_config to fetch the current configuration.\n` +
|
|
128
|
+
`(2) Only if you are adding view_link columns or embedded view (type "view") segments: call get_relation_paths once with all the source_table/target_view pairs you need. For changes that don't involve linking or embedding views (e.g. adding a field, changing a label), skip this step.\n` +
|
|
129
|
+
`(3) Write out the complete updated configuration JSON in full — every key from the existing config must be present, with only your targeted changes merged in.\n` +
|
|
130
|
+
`(4) Call apply_view_config with that complete object. NEVER call apply_view_config before step (3) is finished. NEVER call it with only the name or a partial object — the configuration field is mandatory and must be the full merged result from step (3). Calling apply_view_config without a complete configuration is an error.\n\n` +
|
|
131
|
+
`**Generating a new view that contains view_links or embedded views:**\n` +
|
|
132
|
+
`Call get_relation_paths once with all source_table/target_view pairs you need before constructing the layout.\n\n` +
|
|
133
|
+
`**Embedded view segment format (for Show layouts):**\n` +
|
|
134
|
+
` { "type": "view", "view": "<viewName>", "name": "<viewName>", "relation": "<from get_relation_paths>" }\n` +
|
|
135
|
+
`Do NOT use blank text segments as placeholders — always use a real view segment with a relation string from get_relation_paths.\n\n` +
|
|
136
|
+
RELATION_PATH_DOC
|
|
137
|
+
);
|
|
32
138
|
}
|
|
33
139
|
|
|
34
140
|
get userActions() {
|
|
35
141
|
return {
|
|
142
|
+
async build_copilot_view_update({ name, configuration }) {
|
|
143
|
+
const existingView = View.findOne({ name });
|
|
144
|
+
if (!existingView) return { error: `View "${name}" not found` };
|
|
145
|
+
await View.update({ configuration }, existingView.id);
|
|
146
|
+
setTimeout(() => getState().refresh_views(), 200);
|
|
147
|
+
return {
|
|
148
|
+
notify: `View updated: <a target="_blank" href="/view/${name}">${name}</a>`,
|
|
149
|
+
};
|
|
150
|
+
},
|
|
36
151
|
async build_copilot_view_gen({
|
|
37
152
|
wfctx,
|
|
38
153
|
name,
|
|
@@ -40,16 +155,29 @@ a view generation mode. The tool call only requires high-level details to start
|
|
|
40
155
|
table,
|
|
41
156
|
min_role,
|
|
42
157
|
}) {
|
|
43
|
-
const
|
|
158
|
+
const existing = View.findOne({ name });
|
|
159
|
+
if (existing)
|
|
160
|
+
return {
|
|
161
|
+
error: `View "${name}" already exists. Use get_view_config and apply_view_config to update it.`,
|
|
162
|
+
};
|
|
44
163
|
const tableRow = table ? Table.findOne({ name: table }) : null;
|
|
164
|
+
const roleName = typeof min_role === "number" ? null : (min_role || "public");
|
|
165
|
+
const resolvedRole =
|
|
166
|
+
typeof min_role === "number"
|
|
167
|
+
? min_role
|
|
168
|
+
: ((getState().roles || []).find((r) => r.role === roleName) || { id: 100 }).id;
|
|
45
169
|
await View.create({
|
|
46
170
|
name,
|
|
47
171
|
viewtemplate: viewpattern,
|
|
48
172
|
table_id: tableRow?.id,
|
|
49
173
|
table: tableRow,
|
|
50
|
-
min_role:
|
|
174
|
+
min_role: resolvedRole,
|
|
51
175
|
configuration: wfctx,
|
|
52
176
|
});
|
|
177
|
+
const vt = getState().viewtemplates[viewpattern];
|
|
178
|
+
if (vt?.copilot_post_create) {
|
|
179
|
+
await vt.copilot_post_create({ name, configuration: wfctx });
|
|
180
|
+
}
|
|
53
181
|
setTimeout(() => getState().refresh_views(), 200);
|
|
54
182
|
return {
|
|
55
183
|
notify: `View saved: <a target="_blank" href="/view/${name}">${name}</a>`,
|
|
@@ -66,13 +194,17 @@ a view generation mode. The tool call only requires high-level details to start
|
|
|
66
194
|
const enabled_vt_names = all_vt_names.filter(
|
|
67
195
|
(vtnm) =>
|
|
68
196
|
vts[vtnm].enable_copilot_viewgen ||
|
|
69
|
-
vts[vtnm].copilot_generate_view_prompt
|
|
197
|
+
vts[vtnm].copilot_generate_view_prompt
|
|
70
198
|
);
|
|
71
199
|
if (!enabled_vt_names.includes("Show")) enabled_vt_names.push("Show");
|
|
200
|
+
if (!enabled_vt_names.includes("Edit")) enabled_vt_names.push("Edit");
|
|
201
|
+
if (!enabled_vt_names.includes("List")) enabled_vt_names.push("List");
|
|
202
|
+
if (!enabled_vt_names.includes("Filter")) enabled_vt_names.push("Filter");
|
|
72
203
|
//const roles = await User.get_roles();
|
|
73
204
|
const tableless = enabled_vt_names.filter(
|
|
74
|
-
(vtnm) => vts[vtnm].tableless === true
|
|
205
|
+
(vtnm) => vts[vtnm].tableless === true
|
|
75
206
|
);
|
|
207
|
+
const roles = state.roles;
|
|
76
208
|
const parameters = {
|
|
77
209
|
type: "object",
|
|
78
210
|
required: ["name", "viewpattern"],
|
|
@@ -82,7 +214,9 @@ a view generation mode. The tool call only requires high-level details to start
|
|
|
82
214
|
type: "string",
|
|
83
215
|
},
|
|
84
216
|
viewpattern: {
|
|
85
|
-
description: `The type of view to generate. Some of the view descriptions: ${enabled_vt_names
|
|
217
|
+
description: `The type of view to generate. Some of the view descriptions: ${enabled_vt_names
|
|
218
|
+
.map((vtnm) => `${vtnm}: ${vts[vtnm].description}.`)
|
|
219
|
+
.join(" ")}`,
|
|
86
220
|
type: "string",
|
|
87
221
|
enum: enabled_vt_names,
|
|
88
222
|
},
|
|
@@ -97,17 +231,17 @@ a view generation mode. The tool call only requires high-level details to start
|
|
|
97
231
|
description:
|
|
98
232
|
"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",
|
|
99
233
|
type: "string",
|
|
100
|
-
enum: ["admin", "user", "public"],
|
|
234
|
+
enum: roles ? roles.map((r) => r.role) : ["admin", "user", "public"],
|
|
101
235
|
},
|
|
102
236
|
},
|
|
103
237
|
};
|
|
104
238
|
|
|
105
|
-
|
|
239
|
+
const generateViewTool = {
|
|
106
240
|
type: "function",
|
|
107
241
|
function: {
|
|
108
242
|
name: "generate_view",
|
|
109
243
|
description:
|
|
110
|
-
"Generate a view by supplying high-level details.
|
|
244
|
+
"Generate a NEW view by supplying high-level details. Only call this for views that do not yet exist — if the view already exists, use get_view_config + apply_view_config instead.",
|
|
111
245
|
parameters,
|
|
112
246
|
},
|
|
113
247
|
process: async (input) => {
|
|
@@ -122,25 +256,101 @@ a view generation mode. The tool call only requires high-level details to start
|
|
|
122
256
|
: Table.findOne({ name: tool_call.input.table });
|
|
123
257
|
|
|
124
258
|
const wfctx = { viewname: tool_call.input.name, table_id: table?.id };
|
|
125
|
-
|
|
259
|
+
const viewpattern = tool_call.input.viewpattern;
|
|
260
|
+
const builderModeByPattern = {
|
|
261
|
+
Show: "show",
|
|
262
|
+
Edit: "edit",
|
|
263
|
+
List: "listcolumns",
|
|
264
|
+
Filter: "filter",
|
|
265
|
+
};
|
|
266
|
+
const builderMode = builderModeByPattern[viewpattern];
|
|
267
|
+
if (builderMode) {
|
|
268
|
+
const extractText = (c) => {
|
|
269
|
+
if (typeof c === "string") return c;
|
|
270
|
+
if (Array.isArray(c)) {
|
|
271
|
+
const textPart = c.find(
|
|
272
|
+
(p) => p?.type === "text" || typeof p === "string"
|
|
273
|
+
);
|
|
274
|
+
return (
|
|
275
|
+
textPart?.text || (typeof textPart === "string" ? textPart : "")
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
return "";
|
|
279
|
+
};
|
|
280
|
+
const isToolResultMessage = (item) => {
|
|
281
|
+
if (!Array.isArray(item?.content)) return false;
|
|
282
|
+
return item.content.every((p) => p?.type === "tool_result");
|
|
283
|
+
};
|
|
126
284
|
const promptFromChat = Array.isArray(chat)
|
|
127
|
-
?
|
|
128
|
-
.
|
|
129
|
-
|
|
285
|
+
? (() => {
|
|
286
|
+
const userMsgs = chat.filter(
|
|
287
|
+
(item) =>
|
|
288
|
+
item?.role === "user" &&
|
|
289
|
+
item?.content &&
|
|
290
|
+
!isToolResultMessage(item)
|
|
291
|
+
);
|
|
292
|
+
return userMsgs.length ? extractText(userMsgs[0].content) : "";
|
|
293
|
+
})()
|
|
130
294
|
: "";
|
|
131
295
|
const layoutPrompt = promptFromChat || tool_call.input.name || "";
|
|
132
296
|
wfctx.layout = await builderGen.run(
|
|
133
297
|
layoutPrompt,
|
|
134
|
-
|
|
298
|
+
builderMode,
|
|
135
299
|
table?.name,
|
|
136
|
-
|
|
300
|
+
null,
|
|
301
|
+
chat
|
|
137
302
|
);
|
|
138
|
-
if (table) {
|
|
139
|
-
|
|
303
|
+
if (table && viewpattern !== "Filter") {
|
|
304
|
+
// isEdit=true: FK fields get Field+select columns; false gives JoinField (display-only)
|
|
305
|
+
const isEditView = viewpattern === "Edit";
|
|
306
|
+
const baseCfg = await initial_config_all_fields(isEditView)({
|
|
140
307
|
table_id: table.id,
|
|
141
308
|
});
|
|
142
309
|
if (baseCfg?.columns) wfctx.columns = baseCfg.columns;
|
|
143
310
|
}
|
|
311
|
+
if (viewpattern === "Edit" && table) {
|
|
312
|
+
const layoutFieldNames = collectLayoutFieldNames(wfctx.layout);
|
|
313
|
+
const fields = table.fields || [];
|
|
314
|
+
const fixed = {};
|
|
315
|
+
const usersFkColumnsToAdd = [];
|
|
316
|
+
for (const f of fields) {
|
|
317
|
+
if (f.primary_key || f.calculated) continue;
|
|
318
|
+
if (f.type === "Key" && f.reftable_name === "users") {
|
|
319
|
+
if (layoutFieldNames.has(f.name)) {
|
|
320
|
+
// Explicitly placed in layout — add a select column so getForm renders it
|
|
321
|
+
usersFkColumnsToAdd.push({
|
|
322
|
+
field_name: f.name,
|
|
323
|
+
type: "Field",
|
|
324
|
+
fieldview: "select",
|
|
325
|
+
state_field: true,
|
|
326
|
+
});
|
|
327
|
+
} else {
|
|
328
|
+
fixed[`preset_${f.name}`] = "LoggedIn";
|
|
329
|
+
fixed[`_block_${f.name}`] = true;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (usersFkColumnsToAdd.length > 0)
|
|
334
|
+
wfctx.columns = [...(wfctx.columns || []), ...usersFkColumnsToAdd];
|
|
335
|
+
if (Object.keys(fixed).length > 0) wfctx.fixed = fixed;
|
|
336
|
+
wfctx.destination_type = "Back to referer";
|
|
337
|
+
}
|
|
338
|
+
if (viewpattern === "Filter") {
|
|
339
|
+
const filterFieldSegment = findFilterFieldSegment(wfctx.layout);
|
|
340
|
+
if (filterFieldSegment) {
|
|
341
|
+
const normalized = normalizeFilterField(filterFieldSegment);
|
|
342
|
+
wfctx.layout = normalized;
|
|
343
|
+
wfctx.columns = [toFilterColumn(normalized)];
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (
|
|
349
|
+
viewpattern === "Show" ||
|
|
350
|
+
viewpattern === "Edit" ||
|
|
351
|
+
viewpattern === "Filter"
|
|
352
|
+
) {
|
|
353
|
+
// No extra configuration steps for these modes.
|
|
144
354
|
} else {
|
|
145
355
|
const flow = vt.configuration_workflow(req);
|
|
146
356
|
let vt_prompt = "";
|
|
@@ -149,26 +359,64 @@ a view generation mode. The tool call only requires high-level details to start
|
|
|
149
359
|
vt_prompt = vt.copilot_generate_view_prompt;
|
|
150
360
|
else if (typeof vt.copilot_generate_view_prompt === "function")
|
|
151
361
|
vt_prompt = await vt.copilot_generate_view_prompt(
|
|
152
|
-
tool_call.input
|
|
362
|
+
tool_call.input
|
|
153
363
|
);
|
|
154
364
|
}
|
|
155
365
|
|
|
366
|
+
const prefilledFields = new Set();
|
|
367
|
+
if (wfctx.layout !== undefined) prefilledFields.add("layout");
|
|
368
|
+
if (wfctx.columns !== undefined) prefilledFields.add("columns");
|
|
369
|
+
|
|
370
|
+
// For List views: pre-fill view_to_create with the best Edit view for the table
|
|
371
|
+
if (viewpattern === "List" && table) {
|
|
372
|
+
const candidateViews = await View.find_table_views_where(
|
|
373
|
+
table.id,
|
|
374
|
+
({ state_fields, viewrow }) =>
|
|
375
|
+
viewrow.name !== tool_call.input.name &&
|
|
376
|
+
state_fields.every((sf) => !sf.required)
|
|
377
|
+
);
|
|
378
|
+
if (candidateViews.length > 0) {
|
|
379
|
+
const editView =
|
|
380
|
+
candidateViews.find((v) =>
|
|
381
|
+
v.name.toLowerCase().includes("edit")
|
|
382
|
+
) || candidateViews[0];
|
|
383
|
+
wfctx.view_to_create =
|
|
384
|
+
editView.select_option?.name || editView.name;
|
|
385
|
+
wfctx.create_view_display = "Popup";
|
|
386
|
+
wfctx.create_view_location = "Top right";
|
|
387
|
+
prefilledFields.add("view_to_create");
|
|
388
|
+
prefilledFields.add("create_view_display");
|
|
389
|
+
prefilledFields.add("create_view_location");
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
156
393
|
for (const step of flow.steps) {
|
|
394
|
+
if (typeof step.form !== "function") continue;
|
|
157
395
|
const form = await step.form(wfctx);
|
|
158
396
|
const properties = {};
|
|
159
397
|
//TODO onlyWhen
|
|
160
398
|
for (const field of form.fields) {
|
|
399
|
+
if (prefilledFields.has(field.name)) continue;
|
|
161
400
|
//TODO showIf
|
|
162
401
|
properties[field.name] = {
|
|
163
402
|
description:
|
|
164
403
|
field.copilot_description ||
|
|
165
|
-
`${field.label}.${
|
|
404
|
+
`${field.label}.${
|
|
405
|
+
field.sublabel ? ` ${field.sublabel}` : ""
|
|
406
|
+
}`,
|
|
166
407
|
...fieldProperties(field),
|
|
167
408
|
};
|
|
409
|
+
if (!properties[field.name].type) {
|
|
410
|
+
properties[field.name].type = "string";
|
|
411
|
+
}
|
|
168
412
|
}
|
|
169
413
|
|
|
414
|
+
if (!Object.keys(properties).length) continue;
|
|
415
|
+
|
|
170
416
|
const answer = await generate(
|
|
171
|
-
`${vt_prompt ? vt_prompt + "\n\n" : ""}Now generate the ${
|
|
417
|
+
`${vt_prompt ? vt_prompt + "\n\n" : ""}Now generate the ${
|
|
418
|
+
step.name
|
|
419
|
+
} details of the view by calling the generate_view_details tool`,
|
|
172
420
|
{
|
|
173
421
|
tools: [
|
|
174
422
|
{
|
|
@@ -189,22 +437,55 @@ a view generation mode. The tool call only requires high-level details to start
|
|
|
189
437
|
name: "generate_view_details",
|
|
190
438
|
},
|
|
191
439
|
},
|
|
192
|
-
}
|
|
440
|
+
}
|
|
193
441
|
);
|
|
194
|
-
const tc =
|
|
195
|
-
|
|
442
|
+
const tc =
|
|
443
|
+
typeof answer?.getToolCalls === "function"
|
|
444
|
+
? answer.getToolCalls()[0]
|
|
445
|
+
: null;
|
|
446
|
+
if (tc) {
|
|
447
|
+
await getState().functions.llm_add_message.run(
|
|
448
|
+
"tool_response",
|
|
449
|
+
{ type: "text", value: "Details provided" },
|
|
450
|
+
{ chat, tool_call: tc }
|
|
451
|
+
);
|
|
452
|
+
Object.assign(wfctx, tc.input);
|
|
453
|
+
}
|
|
196
454
|
}
|
|
197
455
|
}
|
|
456
|
+
const roleName = tool_call.input.min_role || "public";
|
|
457
|
+
const rolesState = getState().roles;
|
|
458
|
+
const min_role = rolesState
|
|
459
|
+
? (rolesState.find((r) => r.role === roleName) || { id: 100 }).id
|
|
460
|
+
: { admin: 1, public: 100, user: 80 }[roleName] ?? 100;
|
|
461
|
+
const existingView = View.findOne({ name: tool_call.input.name });
|
|
462
|
+
if (existingView) {
|
|
463
|
+
return {
|
|
464
|
+
stop: true,
|
|
465
|
+
add_response: `Error: view "${tool_call.input.name}" already exists. Do NOT call generate_view again — use get_view_config to inspect the current configuration and apply_view_config to update it.`,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
198
468
|
const view = new View({
|
|
199
469
|
name: tool_call.input.name,
|
|
200
470
|
viewtemplate: tool_call.input.viewpattern,
|
|
201
471
|
table,
|
|
202
472
|
table_id: table?.id,
|
|
203
|
-
min_role
|
|
204
|
-
tool_call.input.min_role || "public"
|
|
205
|
-
],
|
|
473
|
+
min_role,
|
|
206
474
|
configuration: wfctx,
|
|
207
475
|
});
|
|
476
|
+
if (this.yoloMode) {
|
|
477
|
+
await this.userActions.build_copilot_view_gen({
|
|
478
|
+
wfctx,
|
|
479
|
+
name: tool_call.input.name,
|
|
480
|
+
viewpattern: tool_call.input.viewpattern,
|
|
481
|
+
table: tool_call.input.table,
|
|
482
|
+
min_role: tool_call.input.min_role,
|
|
483
|
+
});
|
|
484
|
+
return {
|
|
485
|
+
stop: true,
|
|
486
|
+
add_response: `View ${tool_call.input.name} created.`,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
208
489
|
const runres = await view.run({}, { req });
|
|
209
490
|
return {
|
|
210
491
|
stop: true,
|
|
@@ -212,7 +493,7 @@ a view generation mode. The tool call only requires high-level details to start
|
|
|
212
493
|
pre(JSON.stringify(wfctx, null, 2)) +
|
|
213
494
|
div(
|
|
214
495
|
{ style: { maxHeight: 800, maxWidth: 500, overflow: "scroll" } },
|
|
215
|
-
runres
|
|
496
|
+
runres
|
|
216
497
|
),
|
|
217
498
|
add_user_action: {
|
|
218
499
|
name: "build_copilot_view_gen",
|
|
@@ -223,6 +504,127 @@ a view generation mode. The tool call only requires high-level details to start
|
|
|
223
504
|
};
|
|
224
505
|
},
|
|
225
506
|
};
|
|
507
|
+
|
|
508
|
+
const getViewConfigTool = {
|
|
509
|
+
type: "function",
|
|
510
|
+
function: {
|
|
511
|
+
name: "get_view_config",
|
|
512
|
+
description:
|
|
513
|
+
"Retrieve the current configuration of an existing view. " +
|
|
514
|
+
"Call this first to inspect the layout before calling apply_view_config to save changes. " +
|
|
515
|
+
"Returns the full configuration JSON and the viewtemplate name.",
|
|
516
|
+
parameters: {
|
|
517
|
+
type: "object",
|
|
518
|
+
required: ["name"],
|
|
519
|
+
properties: {
|
|
520
|
+
name: {
|
|
521
|
+
description: "The name of the existing view to inspect.",
|
|
522
|
+
type: "string",
|
|
523
|
+
},
|
|
524
|
+
},
|
|
525
|
+
},
|
|
526
|
+
},
|
|
527
|
+
process: async ({ name }) => {
|
|
528
|
+
const existingView = View.findOne({ name });
|
|
529
|
+
if (!existingView)
|
|
530
|
+
return `View "${name}" not found. Use generate_view to create a new view instead.`;
|
|
531
|
+
return (
|
|
532
|
+
`Current configuration of view "${name}" (viewtemplate: ${existingView.viewtemplate}):\n` +
|
|
533
|
+
JSON.stringify(existingView.configuration, null, 2)
|
|
534
|
+
);
|
|
535
|
+
},
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
const applyViewConfigTool = {
|
|
539
|
+
type: "function",
|
|
540
|
+
function: {
|
|
541
|
+
name: "apply_view_config",
|
|
542
|
+
description:
|
|
543
|
+
"Save an updated configuration to an existing view. " +
|
|
544
|
+
"STRICT PRECONDITION: you must have already called get_view_config AND written out the complete merged configuration JSON before calling this tool. " +
|
|
545
|
+
"Do NOT call this tool as a placeholder or before the configuration is fully constructed. " +
|
|
546
|
+
"Calling this tool without a complete configuration object is always wrong and will fail.",
|
|
547
|
+
parameters: {
|
|
548
|
+
type: "object",
|
|
549
|
+
required: ["name", "configuration"],
|
|
550
|
+
properties: {
|
|
551
|
+
name: {
|
|
552
|
+
description: "The name of the existing view to update.",
|
|
553
|
+
type: "string",
|
|
554
|
+
},
|
|
555
|
+
configuration: {
|
|
556
|
+
type: "object",
|
|
557
|
+
description:
|
|
558
|
+
"REQUIRED. The complete updated configuration object — every key from the existing config preserved, with only your changes merged in. " +
|
|
559
|
+
"You MUST have the full object written out before calling this tool. " +
|
|
560
|
+
"Passing null, an empty object, or a partial object (e.g. only the name) is always wrong and will return an error.",
|
|
561
|
+
},
|
|
562
|
+
},
|
|
563
|
+
},
|
|
564
|
+
},
|
|
565
|
+
process: async ({ name, configuration }) => {
|
|
566
|
+
const existingView = View.findOne({ name });
|
|
567
|
+
if (!existingView) return `View "${name}" not found.`;
|
|
568
|
+
if (!configuration || typeof configuration !== "object")
|
|
569
|
+
return (
|
|
570
|
+
`ERROR: configuration is missing. ` +
|
|
571
|
+
`You must call get_view_config first, merge your changes into the full existing configuration, then call apply_view_config again with the complete configuration object.`
|
|
572
|
+
);
|
|
573
|
+
return { name, configuration, view_id: existingView.id };
|
|
574
|
+
},
|
|
575
|
+
postProcess: async ({ tool_call, req }) => {
|
|
576
|
+
const { name, configuration } = tool_call.input;
|
|
577
|
+
const existingView = View.findOne({ name });
|
|
578
|
+
if (!existingView)
|
|
579
|
+
return { stop: true, add_response: `View "${name}" not found.` };
|
|
580
|
+
if (!configuration || typeof configuration !== "object")
|
|
581
|
+
return {
|
|
582
|
+
stop: true,
|
|
583
|
+
add_response:
|
|
584
|
+
`apply_view_config called for "${name}" without a configuration object. ` +
|
|
585
|
+
`Call get_view_config first, merge your changes into the full existing configuration, then call apply_view_config again with the complete configuration.`,
|
|
586
|
+
};
|
|
587
|
+
const cfg = configuration;
|
|
588
|
+
|
|
589
|
+
if (this.yoloMode) {
|
|
590
|
+
await View.update({ configuration: cfg }, existingView.id);
|
|
591
|
+
setTimeout(() => getState().refresh_views(), 200);
|
|
592
|
+
return { stop: true, add_response: `View ${name} updated.` };
|
|
593
|
+
}
|
|
594
|
+
return {
|
|
595
|
+
stop: true,
|
|
596
|
+
add_response: pre(JSON.stringify(cfg, null, 2)),
|
|
597
|
+
add_user_action: {
|
|
598
|
+
name: "build_copilot_view_update",
|
|
599
|
+
type: "button",
|
|
600
|
+
label: "Save updated view " + name,
|
|
601
|
+
input: { name, configuration: cfg },
|
|
602
|
+
},
|
|
603
|
+
};
|
|
604
|
+
},
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
const getRelationPathsTool = {
|
|
608
|
+
type: "function",
|
|
609
|
+
function: GET_RELATION_PATHS_FUNCTION,
|
|
610
|
+
process: async ({ pairs }) => {
|
|
611
|
+
const schemaData = await build_schema_data();
|
|
612
|
+
const sections = getRelationPathsForPairs(pairs || [], schemaData);
|
|
613
|
+
return (
|
|
614
|
+
sections.join("\n\n") +
|
|
615
|
+
`\n\nFor each pair, set the "relation" property to one of the strings listed above.\n` +
|
|
616
|
+
`Pick by type: ChildList = multiple child rows, ParentShow = single parent, OneToOneShow = unique child. ` +
|
|
617
|
+
`If multiple paths of the same type exist, choose the one whose FK field name best matches the task. Prefer shorter paths.`
|
|
618
|
+
);
|
|
619
|
+
},
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
return [
|
|
623
|
+
generateViewTool,
|
|
624
|
+
getViewConfigTool,
|
|
625
|
+
applyViewConfigTool,
|
|
626
|
+
getRelationPathsTool,
|
|
627
|
+
];
|
|
226
628
|
};
|
|
227
629
|
}
|
|
228
630
|
|
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}`,
|