@saltcorn/copilot 0.8.0 → 0.8.2
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-tables.js +14 -0
- package/actions/generate-trigger.js +61 -0
- package/actions/generate-workflow.js +6 -1
- package/agent-skills/pagegen.js +39 -12
- package/agent-skills/registry-editor.js +15 -0
- package/agent-skills/triggergen.js +259 -0
- package/agent-skills/viewgen.js +248 -22
- package/app-constructor/prompts.js +65 -3
- package/app-constructor/requirements.js +98 -36
- package/app-constructor/run_task.js +116 -68
- package/app-constructor/schema.js +227 -49
- package/app-constructor/tasks.js +622 -83
- package/app-constructor/tools.js +5 -4
- package/app-constructor/view.js +7 -0
- package/builder-gen.js +135 -35
- package/copilot-as-agent.js +2 -0
- package/index.js +1 -2
- package/js-code-gen.js +1 -0
- package/package.json +1 -1
- package/relation-paths.js +269 -0
- package/standard-prompt.js +1 -0
- package/user-copilot.js +2 -0
- package/workflow-gen.js +1 -0
- package/agent-skills/app-constructor-context.js +0 -25
- package/chat-copilot.js +0 -769
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,26 @@ 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 collectViewLinks = (segment, out = []) => {
|
|
27
|
+
if (!segment || typeof segment !== "object") return out;
|
|
28
|
+
if (Array.isArray(segment)) {
|
|
29
|
+
segment.forEach((s) => collectViewLinks(s, out));
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
if (segment.type === "view_link" && segment.view) out.push(segment);
|
|
33
|
+
if (segment.above) collectViewLinks(segment.above, out);
|
|
34
|
+
if (segment.besides) collectViewLinks(segment.besides, out);
|
|
35
|
+
if (segment.contents) collectViewLinks(segment.contents, out);
|
|
36
|
+
if (Array.isArray(segment.tabs))
|
|
37
|
+
segment.tabs.forEach((t) => collectViewLinks(t?.contents, out));
|
|
38
|
+
return out;
|
|
39
|
+
};
|
|
17
40
|
|
|
18
41
|
const collectLayoutFieldNames = (segment, out = new Set()) => {
|
|
19
42
|
if (!segment || typeof segment !== "object") return out;
|
|
@@ -112,14 +135,35 @@ class GenerateViewSkill {
|
|
|
112
135
|
|
|
113
136
|
async systemPrompt() {
|
|
114
137
|
return (
|
|
115
|
-
`If the user asks to generate a view, use the generate_view tool
|
|
116
|
-
`a view
|
|
117
|
-
`The Edit viewtemplate serves both create (no id in state) and edit (id in state) — one view covers both
|
|
138
|
+
`If the user asks to generate a view, use the generate_view tool — but ONLY if the view does not already exist. ` +
|
|
139
|
+
`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` +
|
|
140
|
+
`The Edit viewtemplate serves both create (no id in state) and edit (id in state) — one view covers both.\n\n` +
|
|
141
|
+
`**Modifying an existing view — required sequence:**\n` +
|
|
142
|
+
`(1) Call get_view_config to fetch the current configuration.\n` +
|
|
143
|
+
`(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` +
|
|
144
|
+
`(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` +
|
|
145
|
+
`(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` +
|
|
146
|
+
`**Generating a new view that contains view_links or embedded views:**\n` +
|
|
147
|
+
`If the task or prompt mentions a viewlink, a link to another view, or a button that opens another view from a list row, that view_link column is REQUIRED — do not omit it. ` +
|
|
148
|
+
`You MUST call get_relation_paths with all source_table/target_view pairs before constructing the layout. Never skip this step when view_links are needed.\n\n` +
|
|
149
|
+
`**Embedded view segment format (for Show layouts):**\n` +
|
|
150
|
+
` { "type": "view", "view": "<viewName>", "name": "<viewName>", "relation": "<from get_relation_paths>" }\n` +
|
|
151
|
+
`Do NOT use blank text segments as placeholders — always use a real view segment with a relation string from get_relation_paths.\n\n` +
|
|
152
|
+
RELATION_PATH_DOC
|
|
118
153
|
);
|
|
119
154
|
}
|
|
120
155
|
|
|
121
156
|
get userActions() {
|
|
122
157
|
return {
|
|
158
|
+
async build_copilot_view_update({ name, configuration }) {
|
|
159
|
+
const existingView = View.findOne({ name });
|
|
160
|
+
if (!existingView) return { error: `View "${name}" not found` };
|
|
161
|
+
await View.update({ configuration }, existingView.id);
|
|
162
|
+
setTimeout(() => getState().refresh_views(), 200);
|
|
163
|
+
return {
|
|
164
|
+
notify: `View updated: <a target="_blank" href="/view/${name}">${name}</a>`,
|
|
165
|
+
};
|
|
166
|
+
},
|
|
123
167
|
async build_copilot_view_gen({
|
|
124
168
|
wfctx,
|
|
125
169
|
name,
|
|
@@ -127,14 +171,28 @@ class GenerateViewSkill {
|
|
|
127
171
|
table,
|
|
128
172
|
min_role,
|
|
129
173
|
}) {
|
|
130
|
-
const
|
|
174
|
+
const existing = View.findOne({ name });
|
|
175
|
+
if (existing)
|
|
176
|
+
return {
|
|
177
|
+
error: `View "${name}" already exists. Use get_view_config and apply_view_config to update it.`,
|
|
178
|
+
};
|
|
131
179
|
const tableRow = table ? Table.findOne({ name: table }) : null;
|
|
180
|
+
const roleName =
|
|
181
|
+
typeof min_role === "number" ? null : min_role || "public";
|
|
182
|
+
const resolvedRole =
|
|
183
|
+
typeof min_role === "number"
|
|
184
|
+
? min_role
|
|
185
|
+
: (
|
|
186
|
+
(getState().roles || []).find((r) => r.role === roleName) || {
|
|
187
|
+
id: 100,
|
|
188
|
+
}
|
|
189
|
+
).id;
|
|
132
190
|
await View.create({
|
|
133
191
|
name,
|
|
134
192
|
viewtemplate: viewpattern,
|
|
135
193
|
table_id: tableRow?.id,
|
|
136
194
|
table: tableRow,
|
|
137
|
-
min_role:
|
|
195
|
+
min_role: resolvedRole,
|
|
138
196
|
configuration: wfctx,
|
|
139
197
|
});
|
|
140
198
|
const vt = getState().viewtemplates[viewpattern];
|
|
@@ -199,12 +257,12 @@ class GenerateViewSkill {
|
|
|
199
257
|
},
|
|
200
258
|
};
|
|
201
259
|
|
|
202
|
-
|
|
260
|
+
const generateViewTool = {
|
|
203
261
|
type: "function",
|
|
204
262
|
function: {
|
|
205
263
|
name: "generate_view",
|
|
206
264
|
description:
|
|
207
|
-
"Generate a view by supplying high-level details.
|
|
265
|
+
"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.",
|
|
208
266
|
parameters,
|
|
209
267
|
},
|
|
210
268
|
process: async (input) => {
|
|
@@ -264,23 +322,55 @@ class GenerateViewSkill {
|
|
|
264
322
|
chat
|
|
265
323
|
);
|
|
266
324
|
if (table && viewpattern !== "Filter") {
|
|
267
|
-
|
|
325
|
+
// isEdit=true: FK fields get Field+select columns; false gives JoinField (display-only)
|
|
326
|
+
const isEditView = viewpattern === "Edit";
|
|
327
|
+
const baseCfg = await initial_config_all_fields(isEditView)({
|
|
268
328
|
table_id: table.id,
|
|
269
329
|
});
|
|
270
330
|
if (baseCfg?.columns) wfctx.columns = baseCfg.columns;
|
|
271
331
|
}
|
|
332
|
+
if (viewpattern === "List" && wfctx.layout) {
|
|
333
|
+
// initial_config_all_fields never generates ViewLink columns — inject them from layout
|
|
334
|
+
const viewLinks = collectViewLinks(wfctx.layout);
|
|
335
|
+
const viewLinkColumns = viewLinks.map((seg) => ({
|
|
336
|
+
type: "ViewLink",
|
|
337
|
+
view: seg.view,
|
|
338
|
+
block: seg.block || false,
|
|
339
|
+
label: seg.view_label || "",
|
|
340
|
+
minRole: seg.minRole || 100,
|
|
341
|
+
...(seg.relation ? { relation: seg.relation } : {}),
|
|
342
|
+
isFormula: seg.isFormula || {},
|
|
343
|
+
}));
|
|
344
|
+
if (viewLinkColumns.length > 0)
|
|
345
|
+
wfctx.columns = [...(wfctx.columns || []), ...viewLinkColumns];
|
|
346
|
+
}
|
|
272
347
|
if (viewpattern === "Edit" && table) {
|
|
273
348
|
const layoutFieldNames = collectLayoutFieldNames(wfctx.layout);
|
|
274
349
|
const fields = table.fields || [];
|
|
275
350
|
const fixed = {};
|
|
351
|
+
const usersFkColumnsToAdd = [];
|
|
276
352
|
for (const f of fields) {
|
|
277
353
|
if (f.primary_key || f.calculated) continue;
|
|
278
|
-
if (layoutFieldNames.has(f.name)) continue;
|
|
279
354
|
if (f.type === "Key" && f.reftable_name === "users") {
|
|
280
|
-
|
|
281
|
-
|
|
355
|
+
if (layoutFieldNames.has(f.name)) {
|
|
356
|
+
// Explicitly placed in layout — add a select column so getForm renders it
|
|
357
|
+
usersFkColumnsToAdd.push({
|
|
358
|
+
field_name: f.name,
|
|
359
|
+
type: "Field",
|
|
360
|
+
fieldview: "select",
|
|
361
|
+
state_field: true,
|
|
362
|
+
});
|
|
363
|
+
} else {
|
|
364
|
+
fixed[`preset_${f.name}`] = "LoggedIn";
|
|
365
|
+
fixed[`_block_${f.name}`] = true;
|
|
366
|
+
}
|
|
282
367
|
}
|
|
283
368
|
}
|
|
369
|
+
if (usersFkColumnsToAdd.length > 0)
|
|
370
|
+
wfctx.columns = [
|
|
371
|
+
...(wfctx.columns || []),
|
|
372
|
+
...usersFkColumnsToAdd,
|
|
373
|
+
];
|
|
284
374
|
if (Object.keys(fixed).length > 0) wfctx.fixed = fixed;
|
|
285
375
|
wfctx.destination_type = "Back to referer";
|
|
286
376
|
}
|
|
@@ -347,12 +437,15 @@ class GenerateViewSkill {
|
|
|
347
437
|
for (const field of form.fields) {
|
|
348
438
|
if (prefilledFields.has(field.name)) continue;
|
|
349
439
|
//TODO showIf
|
|
440
|
+
const isShowif = field.name.endsWith("_showif");
|
|
350
441
|
properties[field.name] = {
|
|
351
442
|
description:
|
|
352
443
|
field.copilot_description ||
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
444
|
+
(isShowif
|
|
445
|
+
? `${field.label}. The correct default is an empty string — leave it blank to always show this element. Only provide a JavaScript expression if the task description explicitly states that this element should be conditionally hidden based on a URL state variable or the current user. Never invent field names or copy examples.`
|
|
446
|
+
: `${field.label}.${
|
|
447
|
+
field.sublabel ? ` ${field.sublabel}` : ""
|
|
448
|
+
}`),
|
|
356
449
|
...fieldProperties(field),
|
|
357
450
|
};
|
|
358
451
|
if (!properties[field.name].type) {
|
|
@@ -388,13 +481,18 @@ class GenerateViewSkill {
|
|
|
388
481
|
},
|
|
389
482
|
}
|
|
390
483
|
);
|
|
391
|
-
const tc =
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
484
|
+
const tc =
|
|
485
|
+
typeof answer?.getToolCalls === "function"
|
|
486
|
+
? answer.getToolCalls()[0]
|
|
487
|
+
: null;
|
|
488
|
+
if (tc) {
|
|
489
|
+
await getState().functions.llm_add_message.run(
|
|
490
|
+
"tool_response",
|
|
491
|
+
{ type: "text", value: "Details provided" },
|
|
492
|
+
{ chat, tool_call: tc }
|
|
493
|
+
);
|
|
494
|
+
Object.assign(wfctx, tc.input);
|
|
495
|
+
}
|
|
398
496
|
}
|
|
399
497
|
}
|
|
400
498
|
const roleName = tool_call.input.min_role || "public";
|
|
@@ -402,6 +500,13 @@ class GenerateViewSkill {
|
|
|
402
500
|
const min_role = rolesState
|
|
403
501
|
? (rolesState.find((r) => r.role === roleName) || { id: 100 }).id
|
|
404
502
|
: { admin: 1, public: 100, user: 80 }[roleName] ?? 100;
|
|
503
|
+
const existingView = View.findOne({ name: tool_call.input.name });
|
|
504
|
+
if (existingView) {
|
|
505
|
+
return {
|
|
506
|
+
stop: true,
|
|
507
|
+
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.`,
|
|
508
|
+
};
|
|
509
|
+
}
|
|
405
510
|
const view = new View({
|
|
406
511
|
name: tool_call.input.name,
|
|
407
512
|
viewtemplate: tool_call.input.viewpattern,
|
|
@@ -441,6 +546,127 @@ class GenerateViewSkill {
|
|
|
441
546
|
};
|
|
442
547
|
},
|
|
443
548
|
};
|
|
549
|
+
|
|
550
|
+
const getViewConfigTool = {
|
|
551
|
+
type: "function",
|
|
552
|
+
function: {
|
|
553
|
+
name: "get_view_config",
|
|
554
|
+
description:
|
|
555
|
+
"Retrieve the current configuration of an existing view. " +
|
|
556
|
+
"Call this first to inspect the layout before calling apply_view_config to save changes. " +
|
|
557
|
+
"Returns the full configuration JSON and the viewtemplate name.",
|
|
558
|
+
parameters: {
|
|
559
|
+
type: "object",
|
|
560
|
+
required: ["name"],
|
|
561
|
+
properties: {
|
|
562
|
+
name: {
|
|
563
|
+
description: "The name of the existing view to inspect.",
|
|
564
|
+
type: "string",
|
|
565
|
+
},
|
|
566
|
+
},
|
|
567
|
+
},
|
|
568
|
+
},
|
|
569
|
+
process: async ({ name }) => {
|
|
570
|
+
const existingView = View.findOne({ name });
|
|
571
|
+
if (!existingView)
|
|
572
|
+
return `View "${name}" not found. Use generate_view to create a new view instead.`;
|
|
573
|
+
return (
|
|
574
|
+
`Current configuration of view "${name}" (viewtemplate: ${existingView.viewtemplate}):\n` +
|
|
575
|
+
JSON.stringify(existingView.configuration, null, 2)
|
|
576
|
+
);
|
|
577
|
+
},
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
const applyViewConfigTool = {
|
|
581
|
+
type: "function",
|
|
582
|
+
function: {
|
|
583
|
+
name: "apply_view_config",
|
|
584
|
+
description:
|
|
585
|
+
"Save an updated configuration to an existing view. " +
|
|
586
|
+
"STRICT PRECONDITION: you must have already called get_view_config AND written out the complete merged configuration JSON before calling this tool. " +
|
|
587
|
+
"Do NOT call this tool as a placeholder or before the configuration is fully constructed. " +
|
|
588
|
+
"Calling this tool without a complete configuration object is always wrong and will fail.",
|
|
589
|
+
parameters: {
|
|
590
|
+
type: "object",
|
|
591
|
+
required: ["name", "configuration"],
|
|
592
|
+
properties: {
|
|
593
|
+
name: {
|
|
594
|
+
description: "The name of the existing view to update.",
|
|
595
|
+
type: "string",
|
|
596
|
+
},
|
|
597
|
+
configuration: {
|
|
598
|
+
type: "object",
|
|
599
|
+
description:
|
|
600
|
+
"REQUIRED. The complete updated configuration object — every key from the existing config preserved, with only your changes merged in. " +
|
|
601
|
+
"You MUST have the full object written out before calling this tool. " +
|
|
602
|
+
"Passing null, an empty object, or a partial object (e.g. only the name) is always wrong and will return an error.",
|
|
603
|
+
},
|
|
604
|
+
},
|
|
605
|
+
},
|
|
606
|
+
},
|
|
607
|
+
process: async ({ name, configuration }) => {
|
|
608
|
+
const existingView = View.findOne({ name });
|
|
609
|
+
if (!existingView) return `View "${name}" not found.`;
|
|
610
|
+
if (!configuration || typeof configuration !== "object")
|
|
611
|
+
return (
|
|
612
|
+
`ERROR: configuration is missing. ` +
|
|
613
|
+
`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.`
|
|
614
|
+
);
|
|
615
|
+
return { name, configuration, view_id: existingView.id };
|
|
616
|
+
},
|
|
617
|
+
postProcess: async ({ tool_call, req }) => {
|
|
618
|
+
const { name, configuration } = tool_call.input;
|
|
619
|
+
const existingView = View.findOne({ name });
|
|
620
|
+
if (!existingView)
|
|
621
|
+
return { stop: true, add_response: `View "${name}" not found.` };
|
|
622
|
+
if (!configuration || typeof configuration !== "object")
|
|
623
|
+
return {
|
|
624
|
+
stop: true,
|
|
625
|
+
add_response:
|
|
626
|
+
`apply_view_config called for "${name}" without a configuration object. ` +
|
|
627
|
+
`Call get_view_config first, merge your changes into the full existing configuration, then call apply_view_config again with the complete configuration.`,
|
|
628
|
+
};
|
|
629
|
+
const cfg = configuration;
|
|
630
|
+
|
|
631
|
+
if (this.yoloMode) {
|
|
632
|
+
await View.update({ configuration: cfg }, existingView.id);
|
|
633
|
+
setTimeout(() => getState().refresh_views(), 200);
|
|
634
|
+
return { stop: true, add_response: `View ${name} updated.` };
|
|
635
|
+
}
|
|
636
|
+
return {
|
|
637
|
+
stop: true,
|
|
638
|
+
add_response: pre(JSON.stringify(cfg, null, 2)),
|
|
639
|
+
add_user_action: {
|
|
640
|
+
name: "build_copilot_view_update",
|
|
641
|
+
type: "button",
|
|
642
|
+
label: "Save updated view " + name,
|
|
643
|
+
input: { name, configuration: cfg },
|
|
644
|
+
},
|
|
645
|
+
};
|
|
646
|
+
},
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
const getRelationPathsTool = {
|
|
650
|
+
type: "function",
|
|
651
|
+
function: GET_RELATION_PATHS_FUNCTION,
|
|
652
|
+
process: async ({ pairs }) => {
|
|
653
|
+
const schemaData = await build_schema_data();
|
|
654
|
+
const sections = getRelationPathsForPairs(pairs || [], schemaData);
|
|
655
|
+
return (
|
|
656
|
+
sections.join("\n\n") +
|
|
657
|
+
`\n\nFor each pair, set the "relation" property to one of the strings listed above.\n` +
|
|
658
|
+
`Pick by type: ChildList = multiple child rows, ParentShow = single parent, OneToOneShow = unique child. ` +
|
|
659
|
+
`If multiple paths of the same type exist, choose the one whose FK field name best matches the task. Prefer shorter paths.`
|
|
660
|
+
);
|
|
661
|
+
},
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
return [
|
|
665
|
+
generateViewTool,
|
|
666
|
+
getViewConfigTool,
|
|
667
|
+
applyViewConfigTool,
|
|
668
|
+
getRelationPathsTool,
|
|
669
|
+
];
|
|
444
670
|
};
|
|
445
671
|
}
|
|
446
672
|
|
|
@@ -42,12 +42,14 @@ const existing_tables_list = (tables) => {
|
|
|
42
42
|
tables.forEach((table) => {
|
|
43
43
|
const fieldLines = table.fields.map(
|
|
44
44
|
(f) =>
|
|
45
|
-
` * ${f.name} with type: ${f.pretty_type}.${
|
|
45
|
+
` * ${f.name} with type: ${f.pretty_type}.${
|
|
46
|
+
f.description ? ` ${f.description}` : ""
|
|
47
|
+
}`
|
|
46
48
|
);
|
|
47
49
|
tableLines.push(
|
|
48
50
|
`${table.name}${
|
|
49
51
|
table.description ? `: ${table.description}.` : "."
|
|
50
|
-
} Contains the following fields:\n${fieldLines.join("\n")}
|
|
52
|
+
} Contains the following fields:\n${fieldLines.join("\n")}`
|
|
51
53
|
);
|
|
52
54
|
});
|
|
53
55
|
return `The database already contains the following tables:
|
|
@@ -55,4 +57,64 @@ const existing_tables_list = (tables) => {
|
|
|
55
57
|
${tableLines.join("\n\n")}`;
|
|
56
58
|
};
|
|
57
59
|
|
|
58
|
-
|
|
60
|
+
const existing_entities_list = ({ views, triggers, pages, tableById = {} }) => {
|
|
61
|
+
const sections = [];
|
|
62
|
+
if (views.length)
|
|
63
|
+
sections.push(
|
|
64
|
+
`The following views are already implemented — do NOT plan tasks to create them:\n` +
|
|
65
|
+
views
|
|
66
|
+
.map((v) => {
|
|
67
|
+
const tablePart =
|
|
68
|
+
v.table?.name ||
|
|
69
|
+
(v.table_id && tableById[v.table_id]) ||
|
|
70
|
+
v.exttable_name;
|
|
71
|
+
return `- ${v.name} (${v.viewtemplate}${
|
|
72
|
+
tablePart ? ` on ${tablePart}` : ""
|
|
73
|
+
})`;
|
|
74
|
+
})
|
|
75
|
+
.join("\n")
|
|
76
|
+
);
|
|
77
|
+
if (triggers.length)
|
|
78
|
+
sections.push(
|
|
79
|
+
`The following triggers are already implemented — do NOT plan tasks to create them:\n` +
|
|
80
|
+
triggers
|
|
81
|
+
.map(
|
|
82
|
+
(t) =>
|
|
83
|
+
`- ${t.name} (${t.action}${
|
|
84
|
+
t.when_trigger ? `, ${t.when_trigger}` : ""
|
|
85
|
+
})`
|
|
86
|
+
)
|
|
87
|
+
.join("\n")
|
|
88
|
+
);
|
|
89
|
+
if (pages.length)
|
|
90
|
+
sections.push(
|
|
91
|
+
`The following pages are already implemented — do NOT plan tasks to create them:\n` +
|
|
92
|
+
pages.map((p) => `- ${p.name}`).join("\n")
|
|
93
|
+
);
|
|
94
|
+
return sections.join("\n\n");
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const available_plugins_list = (storePlugins, installedNames) => {
|
|
98
|
+
const uninstalled = storePlugins.filter((p) => !installedNames.has(p.name));
|
|
99
|
+
if (!uninstalled.length) return "";
|
|
100
|
+
const lines = uninstalled.map((p) => {
|
|
101
|
+
let line = `### ${p.name}`;
|
|
102
|
+
if (p.description) line += `\n${p.description}`;
|
|
103
|
+
if (p.contents) line += `\n${p.contents}`;
|
|
104
|
+
return line;
|
|
105
|
+
});
|
|
106
|
+
return (
|
|
107
|
+
`The following plugins are available in the Saltcorn store but not yet installed. ` +
|
|
108
|
+
`If a task requires functionality provided by one of these plugins (e.g. a specific view template, field type, or action), ` +
|
|
109
|
+
`include an explicit "Install plugin <name>" task before it with the exact plugin name as listed here. ` +
|
|
110
|
+
`The executor will use that name directly without needing to look it up.\n\n` +
|
|
111
|
+
lines.join("\n\n")
|
|
112
|
+
);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
module.exports = {
|
|
116
|
+
saltcorn_description,
|
|
117
|
+
existing_tables_list,
|
|
118
|
+
existing_entities_list,
|
|
119
|
+
available_plugins_list,
|
|
120
|
+
};
|
|
@@ -44,7 +44,7 @@ const requirementsList = async (req) => {
|
|
|
44
44
|
type: "CopilotConstructMgr",
|
|
45
45
|
name: "requirement",
|
|
46
46
|
},
|
|
47
|
-
{ orderBy: "written_at" }
|
|
47
|
+
{ orderBy: "written_at" }
|
|
48
48
|
);
|
|
49
49
|
const starFieldview = getState().types.Integer.fieldviews.show_star_rating;
|
|
50
50
|
|
|
@@ -67,43 +67,85 @@ const requirementsList = async (req) => {
|
|
|
67
67
|
class: "btn btn-outline-danger btn-sm",
|
|
68
68
|
onclick: `view_post("${viewname}", "del_req", {id:${r.id}})`,
|
|
69
69
|
},
|
|
70
|
-
i({ class: "fas fa-trash-alt" })
|
|
70
|
+
i({ class: "fas fa-trash-alt" })
|
|
71
71
|
),
|
|
72
72
|
},
|
|
73
73
|
],
|
|
74
|
-
rs
|
|
74
|
+
rs
|
|
75
75
|
),
|
|
76
76
|
button(
|
|
77
77
|
{
|
|
78
78
|
class: "btn btn-outline-danger mb-4",
|
|
79
79
|
onclick: `view_post("${viewname}", "del_all_reqs")`,
|
|
80
80
|
},
|
|
81
|
-
"Delete all"
|
|
82
|
-
)
|
|
81
|
+
"Delete all"
|
|
82
|
+
)
|
|
83
83
|
);
|
|
84
|
-
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const generating = await MetaData.findOne({
|
|
87
|
+
type: "CopilotConstructMgr",
|
|
88
|
+
name: "generating_requirements",
|
|
89
|
+
});
|
|
90
|
+
if (generating) {
|
|
85
91
|
return div(
|
|
86
92
|
{ class: "mt-2" },
|
|
87
|
-
p(
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
class: "btn btn-primary",
|
|
91
|
-
onclick: `press_store_button(this);view_post("${viewname}", "gen_reqs")`,
|
|
92
|
-
},
|
|
93
|
-
"Generate requirements",
|
|
93
|
+
p(
|
|
94
|
+
i({ class: "fas fa-spinner fa-spin me-2" }),
|
|
95
|
+
"Generating requirements, please wait..."
|
|
94
96
|
),
|
|
97
|
+
script(
|
|
98
|
+
domReady(`
|
|
99
|
+
(function() {
|
|
100
|
+
const poll = () => {
|
|
101
|
+
view_post(${JSON.stringify(viewname)}, 'req_status', {}, (resp) => {
|
|
102
|
+
if (resp && !resp.generating) location.reload();
|
|
103
|
+
else setTimeout(poll, 3000);
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
setTimeout(poll, 3000);
|
|
107
|
+
})();
|
|
108
|
+
`)
|
|
109
|
+
)
|
|
95
110
|
);
|
|
96
111
|
}
|
|
112
|
+
|
|
113
|
+
return div(
|
|
114
|
+
{ class: "mt-2", id: "req-gen-area" },
|
|
115
|
+
p("No requirements found"),
|
|
116
|
+
button(
|
|
117
|
+
{ class: "btn btn-primary", onclick: `copilotGenReqs()` },
|
|
118
|
+
"Generate requirements"
|
|
119
|
+
),
|
|
120
|
+
script(
|
|
121
|
+
domReady(`
|
|
122
|
+
window.copilotGenReqs = () => {
|
|
123
|
+
document.getElementById('req-gen-area').innerHTML =
|
|
124
|
+
'<p><i class="fas fa-spinner fa-spin me-2"></i>Generating requirements, please wait...</p>';
|
|
125
|
+
view_post(${JSON.stringify(viewname)}, 'gen_reqs', {}, () => {});
|
|
126
|
+
const poll = () => {
|
|
127
|
+
view_post(${JSON.stringify(viewname)}, 'req_status', {}, (resp) => {
|
|
128
|
+
if (resp && !resp.generating) location.reload();
|
|
129
|
+
else setTimeout(poll, 3000);
|
|
130
|
+
});
|
|
131
|
+
};
|
|
132
|
+
setTimeout(poll, 3000);
|
|
133
|
+
};
|
|
134
|
+
`)
|
|
135
|
+
)
|
|
136
|
+
);
|
|
97
137
|
};
|
|
98
138
|
|
|
99
|
-
const
|
|
100
|
-
const
|
|
139
|
+
const doGenReqs = async (spec, userId) => {
|
|
140
|
+
const generatingMd = await MetaData.create({
|
|
101
141
|
type: "CopilotConstructMgr",
|
|
102
|
-
name: "
|
|
142
|
+
name: "generating_requirements",
|
|
143
|
+
body: {},
|
|
144
|
+
user_id: userId,
|
|
103
145
|
});
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
146
|
+
try {
|
|
147
|
+
const answer = await getState().functions.llm_generate.run(
|
|
148
|
+
`Generate the requirements for this application:
|
|
107
149
|
|
|
108
150
|
Description: ${spec.body.description}
|
|
109
151
|
Audience: ${spec.body.audience}
|
|
@@ -113,24 +155,44 @@ Visual style: ${spec.body.visual_style}
|
|
|
113
155
|
|
|
114
156
|
Now use the make_requirements tool to list the requirements for this software application
|
|
115
157
|
`,
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
158
|
+
{
|
|
159
|
+
tools: [requirements_tool],
|
|
160
|
+
...tool_choice("make_requirements"),
|
|
161
|
+
systemPrompt:
|
|
162
|
+
"You are a project manager. The user wants to build an application, and you must analyse their application description",
|
|
163
|
+
}
|
|
164
|
+
);
|
|
165
|
+
const tc = answer.getToolCalls()[0];
|
|
166
|
+
for (const reqm of tc.input.requirements)
|
|
167
|
+
await MetaData.create({
|
|
168
|
+
type: "CopilotConstructMgr",
|
|
169
|
+
name: "requirement",
|
|
170
|
+
body: reqm,
|
|
171
|
+
user_id: userId,
|
|
172
|
+
});
|
|
173
|
+
} finally {
|
|
174
|
+
await generatingMd.delete();
|
|
175
|
+
}
|
|
176
|
+
};
|
|
123
177
|
|
|
124
|
-
|
|
178
|
+
const gen_reqs = async (table_id, viewname, config, body, { req, res }) => {
|
|
179
|
+
const spec = await MetaData.findOne({
|
|
180
|
+
type: "CopilotConstructMgr",
|
|
181
|
+
name: "spec",
|
|
182
|
+
});
|
|
183
|
+
if (!spec) throw new Error("Specification not found");
|
|
184
|
+
doGenReqs(spec, req.user?.id).catch((e) =>
|
|
185
|
+
console.error("gen_reqs error", e)
|
|
186
|
+
);
|
|
187
|
+
return { json: { success: true } };
|
|
188
|
+
};
|
|
125
189
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
});
|
|
133
|
-
return { json: { reload_page: true } };
|
|
190
|
+
const req_status = async (table_id, viewname, config, body, { req, res }) => {
|
|
191
|
+
const generating = await MetaData.findOne({
|
|
192
|
+
type: "CopilotConstructMgr",
|
|
193
|
+
name: "generating_requirements",
|
|
194
|
+
});
|
|
195
|
+
return { json: { generating: !!generating } };
|
|
134
196
|
};
|
|
135
197
|
|
|
136
198
|
const del_req = async (table_id, viewname, config, body, { req, res }) => {
|
|
@@ -151,6 +213,6 @@ const del_all_reqs = async (table_id, viewname, config, body, { req, res }) => {
|
|
|
151
213
|
return { json: { reload_page: true } };
|
|
152
214
|
};
|
|
153
215
|
|
|
154
|
-
const req_routes = { gen_reqs, del_req, del_all_reqs };
|
|
216
|
+
const req_routes = { gen_reqs, req_status, del_req, del_all_reqs };
|
|
155
217
|
|
|
156
218
|
module.exports = { requirementsList, req_routes };
|