@saltcorn/copilot 0.8.0 → 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-trigger.js +61 -0
- package/actions/generate-workflow.js +6 -1
- package/agent-skills/pagegen.js +19 -6
- package/agent-skills/triggergen.js +263 -0
- package/agent-skills/viewgen.js +203 -19
- package/app-constructor/prompts.js +65 -3
- package/app-constructor/run_task.js +61 -43
- package/app-constructor/schema.js +14 -1
- package/app-constructor/tasks.js +241 -57
- package/builder-gen.js +84 -30
- package/chat-copilot.js +1 -0
- package/copilot-as-agent.js +1 -0
- package/index.js +1 -1
- package/package.json +1 -1
- package/relation-paths.js +236 -0
- package/agent-skills/app-constructor-context.js +0 -25
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,11 @@ 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");
|
|
17
25
|
|
|
18
26
|
const collectLayoutFieldNames = (segment, out = new Set()) => {
|
|
19
27
|
if (!segment || typeof segment !== "object") return out;
|
|
@@ -112,14 +120,34 @@ class GenerateViewSkill {
|
|
|
112
120
|
|
|
113
121
|
async systemPrompt() {
|
|
114
122
|
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
|
|
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
|
|
118
137
|
);
|
|
119
138
|
}
|
|
120
139
|
|
|
121
140
|
get userActions() {
|
|
122
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
|
+
},
|
|
123
151
|
async build_copilot_view_gen({
|
|
124
152
|
wfctx,
|
|
125
153
|
name,
|
|
@@ -127,14 +155,23 @@ class GenerateViewSkill {
|
|
|
127
155
|
table,
|
|
128
156
|
min_role,
|
|
129
157
|
}) {
|
|
130
|
-
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
|
+
};
|
|
131
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;
|
|
132
169
|
await View.create({
|
|
133
170
|
name,
|
|
134
171
|
viewtemplate: viewpattern,
|
|
135
172
|
table_id: tableRow?.id,
|
|
136
173
|
table: tableRow,
|
|
137
|
-
min_role:
|
|
174
|
+
min_role: resolvedRole,
|
|
138
175
|
configuration: wfctx,
|
|
139
176
|
});
|
|
140
177
|
const vt = getState().viewtemplates[viewpattern];
|
|
@@ -199,12 +236,12 @@ class GenerateViewSkill {
|
|
|
199
236
|
},
|
|
200
237
|
};
|
|
201
238
|
|
|
202
|
-
|
|
239
|
+
const generateViewTool = {
|
|
203
240
|
type: "function",
|
|
204
241
|
function: {
|
|
205
242
|
name: "generate_view",
|
|
206
243
|
description:
|
|
207
|
-
"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.",
|
|
208
245
|
parameters,
|
|
209
246
|
},
|
|
210
247
|
process: async (input) => {
|
|
@@ -264,7 +301,9 @@ class GenerateViewSkill {
|
|
|
264
301
|
chat
|
|
265
302
|
);
|
|
266
303
|
if (table && viewpattern !== "Filter") {
|
|
267
|
-
|
|
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)({
|
|
268
307
|
table_id: table.id,
|
|
269
308
|
});
|
|
270
309
|
if (baseCfg?.columns) wfctx.columns = baseCfg.columns;
|
|
@@ -273,14 +312,26 @@ class GenerateViewSkill {
|
|
|
273
312
|
const layoutFieldNames = collectLayoutFieldNames(wfctx.layout);
|
|
274
313
|
const fields = table.fields || [];
|
|
275
314
|
const fixed = {};
|
|
315
|
+
const usersFkColumnsToAdd = [];
|
|
276
316
|
for (const f of fields) {
|
|
277
317
|
if (f.primary_key || f.calculated) continue;
|
|
278
|
-
if (layoutFieldNames.has(f.name)) continue;
|
|
279
318
|
if (f.type === "Key" && f.reftable_name === "users") {
|
|
280
|
-
|
|
281
|
-
|
|
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
|
+
}
|
|
282
331
|
}
|
|
283
332
|
}
|
|
333
|
+
if (usersFkColumnsToAdd.length > 0)
|
|
334
|
+
wfctx.columns = [...(wfctx.columns || []), ...usersFkColumnsToAdd];
|
|
284
335
|
if (Object.keys(fixed).length > 0) wfctx.fixed = fixed;
|
|
285
336
|
wfctx.destination_type = "Back to referer";
|
|
286
337
|
}
|
|
@@ -388,13 +439,18 @@ class GenerateViewSkill {
|
|
|
388
439
|
},
|
|
389
440
|
}
|
|
390
441
|
);
|
|
391
|
-
const tc =
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
+
}
|
|
398
454
|
}
|
|
399
455
|
}
|
|
400
456
|
const roleName = tool_call.input.min_role || "public";
|
|
@@ -402,6 +458,13 @@ class GenerateViewSkill {
|
|
|
402
458
|
const min_role = rolesState
|
|
403
459
|
? (rolesState.find((r) => r.role === roleName) || { id: 100 }).id
|
|
404
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
|
+
}
|
|
405
468
|
const view = new View({
|
|
406
469
|
name: tool_call.input.name,
|
|
407
470
|
viewtemplate: tool_call.input.viewpattern,
|
|
@@ -441,6 +504,127 @@ class GenerateViewSkill {
|
|
|
441
504
|
};
|
|
442
505
|
},
|
|
443
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
|
+
];
|
|
444
628
|
};
|
|
445
629
|
}
|
|
446
630
|
|
|
@@ -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
|
+
};
|
|
@@ -8,41 +8,13 @@ const { findType } = require("@saltcorn/data/models/discovery");
|
|
|
8
8
|
const { save_menu_items } = require("@saltcorn/data/models/config");
|
|
9
9
|
const db = require("@saltcorn/data/db");
|
|
10
10
|
const WorkflowRun = require("@saltcorn/data/models/workflow_run");
|
|
11
|
+
const User = require("@saltcorn/data/models/user");
|
|
11
12
|
const { viewname } = require("./common");
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
name: "settings",
|
|
18
|
-
});
|
|
19
|
-
if (!settings?.body?.running) return;
|
|
20
|
-
}
|
|
21
|
-
const tasks = await MetaData.find(
|
|
22
|
-
{
|
|
23
|
-
type: "CopilotConstructMgr",
|
|
24
|
-
name: "task",
|
|
25
|
-
},
|
|
26
|
-
{ orderBy: "id" }
|
|
27
|
-
);
|
|
28
|
-
const todos = tasks.filter(
|
|
29
|
-
(t) => !t.body.status || t.body.status === "To do"
|
|
30
|
-
);
|
|
31
|
-
const done = tasks.filter((t) => t.body.status === "Done");
|
|
32
|
-
const done_names = new Set(done.map((t) => t.body.name));
|
|
33
|
-
|
|
34
|
-
const startable = todos.filter((t) =>
|
|
35
|
-
t.body.depends_on.every((nm) => done_names.has(nm))
|
|
36
|
-
);
|
|
37
|
-
|
|
38
|
-
if (startable[0]) {
|
|
39
|
-
console.log("running task", startable[0]);
|
|
40
|
-
|
|
41
|
-
return await runTask(startable[0].id, {});
|
|
42
|
-
}
|
|
43
|
-
//not done
|
|
44
|
-
};
|
|
45
|
-
|
|
14
|
+
/**
|
|
15
|
+
* @param {number} md_id - MetaData id of the task to run
|
|
16
|
+
* @param {object} req - Express request (may be empty `{}` from scheduler)
|
|
17
|
+
*/
|
|
46
18
|
const runTask = async (md_id, req) => {
|
|
47
19
|
const md = await MetaData.findOne({
|
|
48
20
|
id: md_id,
|
|
@@ -59,15 +31,18 @@ const runTask = async (md_id, req) => {
|
|
|
59
31
|
when_trigger: "Never",
|
|
60
32
|
configuration: {
|
|
61
33
|
viewname: viewname,
|
|
62
|
-
sys_prompt:
|
|
34
|
+
sys_prompt:
|
|
35
|
+
"Each task creates exactly one view or one page. " +
|
|
36
|
+
"Never create more than one view or page per task, even if the description mentions multiple. " +
|
|
37
|
+
"Call the view or page tool exactly once and then stop.",
|
|
63
38
|
prompt: "{{prompt}}",
|
|
64
39
|
skills: [
|
|
65
40
|
{ skill_type: "Generate Page", yoloMode: true },
|
|
66
41
|
{ skill_type: "Database design", yoloMode: true },
|
|
67
42
|
{ skill_type: "Generate Workflow", yoloMode: true },
|
|
43
|
+
{ skill_type: "Generate trigger", yoloMode: true },
|
|
68
44
|
{ skill_type: "Generate View", yoloMode: true },
|
|
69
45
|
{ skill_type: "Install Plugin", yoloMode: true },
|
|
70
|
-
{ skill_type: "AppConstructor Context" },
|
|
71
46
|
],
|
|
72
47
|
},
|
|
73
48
|
});
|
|
@@ -83,16 +58,20 @@ Important: The database schema is already fully implemented. Do NOT use generate
|
|
|
83
58
|
|
|
84
59
|
Important: Some fields are non-stored (virtual) calculated fields — they have no database column and are computed on-the-fly by Saltcorn. Never include such fields in modify_row, SQL UPDATE statements, or recalculate_stored_fields calls. Only fields that exist as actual database columns (regular fields and stored calculated fields) can be written. If a calculated field needs updating, it will refresh automatically when the fields it depends on change.
|
|
85
60
|
|
|
61
|
+
Important: The "users" table is built-in. Passwords are platform-managed — never add a password field to a view. Signup uses a built-in form, not an Edit view.
|
|
62
|
+
|
|
86
63
|
Your task now is:
|
|
87
64
|
${md.body.description}`;
|
|
65
|
+
const safeReq = req?.__
|
|
66
|
+
? req
|
|
67
|
+
: { ...req, __: (s) => s, user: req?.user };
|
|
88
68
|
|
|
89
69
|
await md.update({ body: { ...md.body, status: "Running" } });
|
|
90
70
|
const actionres = await agent_action.runWithoutRow({
|
|
91
71
|
row: { prompt },
|
|
92
|
-
req,
|
|
93
|
-
user:
|
|
72
|
+
req: safeReq,
|
|
73
|
+
user: safeReq.user,
|
|
94
74
|
});
|
|
95
|
-
//console.log("actionres", actionres);
|
|
96
75
|
const run_id = actionres.json.run_id;
|
|
97
76
|
const run = await WorkflowRun.findOne({ id: run_id });
|
|
98
77
|
await agent_action.runWithoutRow({
|
|
@@ -100,18 +79,18 @@ ${md.body.description}`;
|
|
|
100
79
|
prompt:
|
|
101
80
|
"Write a description of what you did, for the purposes of a progress report. Write 1-4 sentences. Do not use any tools or write any code",
|
|
102
81
|
},
|
|
103
|
-
req,
|
|
82
|
+
req: safeReq,
|
|
104
83
|
run,
|
|
105
|
-
user:
|
|
84
|
+
user: safeReq.user,
|
|
106
85
|
});
|
|
107
86
|
const lastInteraction =
|
|
108
87
|
run.context.interactions[run.context.interactions.length - 1];
|
|
109
88
|
const lastText =
|
|
110
89
|
typeof lastInteraction.content === "string"
|
|
111
90
|
? lastInteraction.content
|
|
112
|
-
|
|
91
|
+
: lastInteraction.content.text
|
|
113
92
|
? lastInteraction.content.text
|
|
114
|
-
|
|
93
|
+
: Array.isArray(lastInteraction.content)
|
|
115
94
|
? lastInteraction.content[0].text
|
|
116
95
|
: lastInteraction.content;
|
|
117
96
|
await MetaData.create({
|
|
@@ -121,8 +100,47 @@ ${md.body.description}`;
|
|
|
121
100
|
user_id: req?.user?.id,
|
|
122
101
|
});
|
|
123
102
|
|
|
124
|
-
//console.log("run", run);
|
|
125
103
|
await md.update({ body: { ...md.body, status: "Done", run_id } });
|
|
126
104
|
};
|
|
127
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Run the next startable task
|
|
108
|
+
* @param {boolean} [once=false] - true: run one task and stop, false: iterate all tasks
|
|
109
|
+
*/
|
|
110
|
+
const runNextTask = async (once = false) => {
|
|
111
|
+
if (!once) {
|
|
112
|
+
const settings = await MetaData.findOne({
|
|
113
|
+
type: "CopilotConstructMgr",
|
|
114
|
+
name: "settings",
|
|
115
|
+
});
|
|
116
|
+
if (!settings?.body?.running) return;
|
|
117
|
+
}
|
|
118
|
+
const tasks = await MetaData.find(
|
|
119
|
+
{
|
|
120
|
+
type: "CopilotConstructMgr",
|
|
121
|
+
name: "task",
|
|
122
|
+
},
|
|
123
|
+
{ orderBy: "id" }
|
|
124
|
+
);
|
|
125
|
+
if (tasks.some((t) => t.body.status === "Running")) return;
|
|
126
|
+
const todos = tasks.filter(
|
|
127
|
+
(t) => !t.body.status || t.body.status === "To do"
|
|
128
|
+
);
|
|
129
|
+
const done = tasks.filter((t) => t.body.status === "Done");
|
|
130
|
+
const done_names = new Set(done.map((t) => t.body.name));
|
|
131
|
+
|
|
132
|
+
const startable = todos.filter((t) =>
|
|
133
|
+
t.body.depends_on.every((nm) => done_names.has(nm))
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
if (startable[0]) {
|
|
137
|
+
console.log("running task", startable[0]);
|
|
138
|
+
const taskUser = startable[0].user_id
|
|
139
|
+
? await User.findOne({ id: startable[0].user_id })
|
|
140
|
+
: null;
|
|
141
|
+
await runTask(startable[0].id, { user: taskUser, __: (s) => s });
|
|
142
|
+
if (!once) await runNextTask();
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
128
146
|
module.exports = { runTask, runNextTask };
|
|
@@ -123,6 +123,8 @@ ${existing_tables_list(existing_tables)}
|
|
|
123
123
|
|
|
124
124
|
Design a complete database schema that covers ALL requirements listed above. Every distinct entity in the application must have its own table. Do not produce a minimal or partial schema — all tables needed to implement every requirement must be included in this single call. Do not leave any tables for a later step.
|
|
125
125
|
|
|
126
|
+
The tables listed above are already implemented in the database — include them in the schema as-is so the full data model is visible, but do not change their fields. Only add new tables for entities not yet covered. The implementation step will skip any table whose name already exists.
|
|
127
|
+
|
|
126
128
|
For every field that must be unique (e.g. unique email, unique slug, unique combination keys expressed as individual unique fields), set unique=true on that field.
|
|
127
129
|
For every field that must not be empty, set not_null=true.
|
|
128
130
|
Do NOT leave uniqueness or required constraints for a later step — express them fully in this schema.
|
|
@@ -174,7 +176,18 @@ const implement_schema = async (
|
|
|
174
176
|
});
|
|
175
177
|
|
|
176
178
|
const { apply_copilot_tables } = new GenerateTablesSkill({}).userActions;
|
|
177
|
-
await
|
|
179
|
+
const existingNames = new Set((await Table.find({})).map((t) => t.name));
|
|
180
|
+
const newTables = md.body.tables.filter((t) => {
|
|
181
|
+
if (existingNames.has(t.name)) {
|
|
182
|
+
getState().log(
|
|
183
|
+
2,
|
|
184
|
+
`AppConstructor: skipping table "${t.name}" — already exists in database`
|
|
185
|
+
);
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
return true;
|
|
189
|
+
});
|
|
190
|
+
await apply_copilot_tables({ tables: newTables, user: req.user });
|
|
178
191
|
md.body.implemented = true;
|
|
179
192
|
await md.update({ body: md.body });
|
|
180
193
|
|