@saltcorn/copilot 0.7.1 → 0.7.3
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-page.js +10 -10
- package/actions/generate-tables.js +20 -4
- package/actions/generate-workflow.js +106 -20
- package/agent-skills/database-design.js +349 -0
- package/agent-skills/pagegen.js +17 -1
- package/agent-skills/workflow.js +638 -0
- package/builder-gen.js +39 -0
- package/chat-copilot.js +174 -4
- package/copilot-as-agent.js +90 -0
- package/index.js +15 -4
- package/package.json +1 -1
- package/page-gen-action.js +311 -87
package/actions/generate-page.js
CHANGED
|
@@ -28,7 +28,7 @@ class GeneratePage {
|
|
|
28
28
|
let namedescription = `The name of the page, this should be a short name which is part of the url. `;
|
|
29
29
|
if (allPageNames.length) {
|
|
30
30
|
namedescription += `These are the names of the exising pages: ${allPageNames.join(
|
|
31
|
-
", "
|
|
31
|
+
", ",
|
|
32
32
|
)}. Do not pick a name that is identical but follow the same naming convention.`;
|
|
33
33
|
}
|
|
34
34
|
const roles = await User.get_roles();
|
|
@@ -65,7 +65,7 @@ class GeneratePage {
|
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
67
|
static async system_prompt() {
|
|
68
|
-
return `
|
|
68
|
+
return `If the user asks to generate a page, a web page or a landing page, use the generate_page to generate a page.`;
|
|
69
69
|
}
|
|
70
70
|
static async follow_on_generate({ name, page_type }) {
|
|
71
71
|
if (page_type === "Marketing page") {
|
|
@@ -272,7 +272,7 @@ class GeneratePage {
|
|
|
272
272
|
}
|
|
273
273
|
if (segment.type === "container") {
|
|
274
274
|
const { customStyle, style, display, overflow } = splitContainerStyle(
|
|
275
|
-
segment.style
|
|
275
|
+
segment.style,
|
|
276
276
|
);
|
|
277
277
|
return {
|
|
278
278
|
...segment,
|
|
@@ -310,17 +310,17 @@ class GeneratePage {
|
|
|
310
310
|
JSON.stringify(
|
|
311
311
|
GeneratePage.walk_response(JSON.parse(contents)),
|
|
312
312
|
null,
|
|
313
|
-
2
|
|
314
|
-
)
|
|
315
|
-
)
|
|
316
|
-
)
|
|
313
|
+
2,
|
|
314
|
+
),
|
|
315
|
+
),
|
|
316
|
+
),
|
|
317
317
|
)
|
|
318
318
|
);
|
|
319
319
|
}
|
|
320
320
|
static async execute(
|
|
321
321
|
{ name, title, description, min_role, page_type },
|
|
322
322
|
req,
|
|
323
|
-
contents
|
|
323
|
+
contents,
|
|
324
324
|
) {
|
|
325
325
|
console.log("execute", name, contents);
|
|
326
326
|
const roles = await User.get_roles();
|
|
@@ -341,12 +341,12 @@ class GeneratePage {
|
|
|
341
341
|
"Page created. " +
|
|
342
342
|
a(
|
|
343
343
|
{ target: "_blank", href: `/page/${name}`, class: "me-1" },
|
|
344
|
-
"Go to page"
|
|
344
|
+
"Go to page",
|
|
345
345
|
) +
|
|
346
346
|
" | " +
|
|
347
347
|
a(
|
|
348
348
|
{ target: "_blank", href: `/pageedit/edit/${name}`, class: "ms-1" },
|
|
349
|
-
"Configure page"
|
|
349
|
+
"Configure page",
|
|
350
350
|
),
|
|
351
351
|
};
|
|
352
352
|
}
|
|
@@ -5,7 +5,7 @@ const Table = require("@saltcorn/data/models/table");
|
|
|
5
5
|
const Field = require("@saltcorn/data/models/field");
|
|
6
6
|
const { apply, removeAllWhiteSpace } = require("@saltcorn/data/utils");
|
|
7
7
|
const { getActionConfigFields } = require("@saltcorn/data/plugin-helper");
|
|
8
|
-
const { a, pre, script, div } = require("@saltcorn/markup/tags");
|
|
8
|
+
const { a, pre, script, div, domReady } = require("@saltcorn/markup/tags");
|
|
9
9
|
const { fieldProperties } = require("../common");
|
|
10
10
|
|
|
11
11
|
class GenerateTables {
|
|
@@ -13,7 +13,7 @@ class GenerateTables {
|
|
|
13
13
|
static function_name = "generate_tables";
|
|
14
14
|
static description = "Generate database tables";
|
|
15
15
|
|
|
16
|
-
static
|
|
16
|
+
static json_schema() {
|
|
17
17
|
const types = Object.values(getState().types);
|
|
18
18
|
const fieldTypeCfg = types.map((ty) => {
|
|
19
19
|
const properties = {
|
|
@@ -143,6 +143,10 @@ class GenerateTables {
|
|
|
143
143
|
need to specify a primary key, a primary key called id with autoincrementing integers is
|
|
144
144
|
autmatically generated.
|
|
145
145
|
|
|
146
|
+
Only include brand-new tables in the generate_tables arguments. If the user references a table
|
|
147
|
+
that already exists, explain that generate_tables can only add new tables (not modify existing
|
|
148
|
+
ones) and omit those tables from the tool call.
|
|
149
|
+
|
|
146
150
|
The database already contains the following tables:
|
|
147
151
|
|
|
148
152
|
${tableLines.join("\n\n")}
|
|
@@ -154,7 +158,14 @@ class GenerateTables {
|
|
|
154
158
|
const mmdia = buildMermaidMarkup(sctables);
|
|
155
159
|
return (
|
|
156
160
|
pre({ class: "mermaid" }, mmdia) +
|
|
157
|
-
script(
|
|
161
|
+
script(
|
|
162
|
+
domReady(`
|
|
163
|
+
ensure_script_loaded("/static_assets/"+_sc_version_tag+"/mermaid.min.js", () => {
|
|
164
|
+
mermaid.initialize({ startOnLoad: false });
|
|
165
|
+
mermaid.run({ querySelector: ".mermaid" });
|
|
166
|
+
});
|
|
167
|
+
`),
|
|
168
|
+
)
|
|
158
169
|
);
|
|
159
170
|
}
|
|
160
171
|
|
|
@@ -175,9 +186,14 @@ class GenerateTables {
|
|
|
175
186
|
|
|
176
187
|
static process_tables(tables) {
|
|
177
188
|
return tables.map((table) => {
|
|
189
|
+
const sanitizedFields = Array.isArray(table.fields)
|
|
190
|
+
? table.fields.filter(
|
|
191
|
+
(f) => (f?.name || "").toLowerCase() !== "id"
|
|
192
|
+
)
|
|
193
|
+
: [];
|
|
178
194
|
return new Table({
|
|
179
195
|
name: table.table_name,
|
|
180
|
-
fields:
|
|
196
|
+
fields: sanitizedFields.map((f) => {
|
|
181
197
|
const { data_type, reference_table, ...attributes } =
|
|
182
198
|
f.type_and_configuration;
|
|
183
199
|
let type = data_type;
|
|
@@ -3,24 +3,89 @@ const WorkflowStep = require("@saltcorn/data/models/workflow_step");
|
|
|
3
3
|
const Trigger = require("@saltcorn/data/models/trigger");
|
|
4
4
|
const Table = require("@saltcorn/data/models/table");
|
|
5
5
|
const { getActionConfigFields } = require("@saltcorn/data/plugin-helper");
|
|
6
|
-
const { a, pre, script, div } = require("@saltcorn/markup/tags");
|
|
6
|
+
const { a, pre, script, div, domReady } = require("@saltcorn/markup/tags");
|
|
7
7
|
const { fieldProperties } = require("../common");
|
|
8
8
|
|
|
9
|
+
const table_triggers = ["Insert", "Update", "Delete", "Validate"];
|
|
10
|
+
const additional_triggers_with_onlyif = ["Login", "PageLoad"];
|
|
11
|
+
|
|
12
|
+
const optionNames = (options = []) =>
|
|
13
|
+
options
|
|
14
|
+
.map((opt) =>
|
|
15
|
+
typeof opt === "string"
|
|
16
|
+
? opt
|
|
17
|
+
: opt?.name || opt?.label || opt?.value || "",
|
|
18
|
+
)
|
|
19
|
+
.filter(Boolean);
|
|
20
|
+
|
|
21
|
+
const joinOptionNames = (options = [], limit = 10) => {
|
|
22
|
+
const names = optionNames(options);
|
|
23
|
+
if (!names.length) return "none";
|
|
24
|
+
const shown = names.slice(0, limit).join(", ");
|
|
25
|
+
return names.length > limit ? `${shown}, ...` : shown;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const summarizeTriggerActionMatrix = () => {
|
|
29
|
+
try {
|
|
30
|
+
const allActions = Trigger.action_options({
|
|
31
|
+
notRequireRow: false,
|
|
32
|
+
workflow: true,
|
|
33
|
+
});
|
|
34
|
+
const noRowActions = Trigger.action_options({
|
|
35
|
+
notRequireRow: true,
|
|
36
|
+
workflow: true,
|
|
37
|
+
});
|
|
38
|
+
const lines = Trigger.when_options.map((when) => {
|
|
39
|
+
const pool = table_triggers.includes(when) ? allActions : noRowActions;
|
|
40
|
+
return `* ${when}: ${joinOptionNames(pool)}`;
|
|
41
|
+
});
|
|
42
|
+
const notes = `Triggers requiring a table context: ${table_triggers.join(", ")}.
|
|
43
|
+
Additional triggers that commonly use contextual only_if checks: ${additional_triggers_with_onlyif.join(", ")}.`;
|
|
44
|
+
return `${notes}
|
|
45
|
+
${lines.join("\n")}`;
|
|
46
|
+
} catch (e) {
|
|
47
|
+
console.error("GenerateWorkflow: action matrix failed", e);
|
|
48
|
+
return "";
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const summarizeTables = async () => {
|
|
53
|
+
try {
|
|
54
|
+
const tables = await Table.find({});
|
|
55
|
+
if (!tables?.length) return "";
|
|
56
|
+
const fieldType = (f = {}) =>
|
|
57
|
+
f.pretty_type || f.type?.name || f.type || f.input_type || "unknown";
|
|
58
|
+
const rows = tables.map((table) => {
|
|
59
|
+
const description = table.description ? ` – ${table.description}` : "";
|
|
60
|
+
const fields = (table.fields || [])
|
|
61
|
+
.slice(0, 8)
|
|
62
|
+
.map((f) => `${f.name}:${fieldType(f)}`)
|
|
63
|
+
.join(", ");
|
|
64
|
+
const fieldText = fields ? ` fields: ${fields}` : "";
|
|
65
|
+
return `* ${table.name}${description}${fieldText}`;
|
|
66
|
+
});
|
|
67
|
+
return rows.join("\n");
|
|
68
|
+
} catch (e) {
|
|
69
|
+
console.error("GenerateWorkflow: table summary failed", e);
|
|
70
|
+
return "";
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
9
74
|
const steps = async () => {
|
|
10
75
|
const actionExplainers = WorkflowStep.builtInActionExplainers();
|
|
11
76
|
const actionFields = await WorkflowStep.builtInActionConfigFields();
|
|
12
77
|
|
|
13
78
|
let stateActions = getState().actions;
|
|
14
79
|
const stateActionList = Object.entries(stateActions).filter(
|
|
15
|
-
([k, v]) => !v.disableInWorkflow
|
|
80
|
+
([k, v]) => !v.disableInWorkflow,
|
|
16
81
|
);
|
|
17
82
|
|
|
18
83
|
const stepTypeAndCfg = Object.keys(actionExplainers).map((actionName) => {
|
|
19
|
-
const properties = {
|
|
20
|
-
step_type: { type: "string", enum: [actionName] }
|
|
84
|
+
const properties = {
|
|
85
|
+
step_type: { type: "string", enum: [actionName] },
|
|
21
86
|
};
|
|
22
87
|
const myFields = actionFields.filter(
|
|
23
|
-
(f) => f.showIf?.wf_action_name === actionName
|
|
88
|
+
(f) => f.showIf?.wf_action_name === actionName,
|
|
24
89
|
);
|
|
25
90
|
const required = ["step_type"];
|
|
26
91
|
myFields.forEach((f) => {
|
|
@@ -39,8 +104,8 @@ const steps = async () => {
|
|
|
39
104
|
});
|
|
40
105
|
for (const [actionName, action] of stateActionList) {
|
|
41
106
|
try {
|
|
42
|
-
const properties = {
|
|
43
|
-
step_type: { type: "string", enum: [actionName] }
|
|
107
|
+
const properties = {
|
|
108
|
+
step_type: { type: "string", enum: [actionName] },
|
|
44
109
|
};
|
|
45
110
|
const cfgFields = await getActionConfigFields(action, null, {
|
|
46
111
|
mode: "workflow",
|
|
@@ -73,9 +138,9 @@ const steps = async () => {
|
|
|
73
138
|
//TODO workflows
|
|
74
139
|
for (const trigger of triggers) {
|
|
75
140
|
const properties = {
|
|
76
|
-
step_type: {
|
|
141
|
+
step_type: {
|
|
77
142
|
type: "string",
|
|
78
|
-
enum: [trigger.name],
|
|
143
|
+
enum: [trigger.name],
|
|
79
144
|
},
|
|
80
145
|
};
|
|
81
146
|
if (trigger.table_id) {
|
|
@@ -88,7 +153,7 @@ const steps = async () => {
|
|
|
88
153
|
properties.row_expr = {
|
|
89
154
|
type: "string",
|
|
90
155
|
description: `JavaScript expression for the input to the action. This should be an expression for an object, with the following field name and types: ${fieldSpecs.join(
|
|
91
|
-
"; "
|
|
156
|
+
"; ",
|
|
92
157
|
)}.`,
|
|
93
158
|
};
|
|
94
159
|
}
|
|
@@ -150,11 +215,10 @@ class GenerateWorkflow {
|
|
|
150
215
|
description:
|
|
151
216
|
"When the workflow should trigger. Optional, leave blank if unspecified or workflow will be run on button click",
|
|
152
217
|
type: "string",
|
|
153
|
-
enum:
|
|
218
|
+
enum: Trigger.when_options,
|
|
154
219
|
},
|
|
155
220
|
trigger_table: {
|
|
156
|
-
description:
|
|
157
|
-
"If the workflow trigger is Insert, Delete or Update, the name of the table that triggers the workflow",
|
|
221
|
+
description: `If the workflow trigger is ${table_triggers.join(", ")}, the name of the table that triggers the workflow`,
|
|
158
222
|
type: "string",
|
|
159
223
|
},
|
|
160
224
|
},
|
|
@@ -165,15 +229,29 @@ class GenerateWorkflow {
|
|
|
165
229
|
const actionExplainers = WorkflowStep.builtInActionExplainers();
|
|
166
230
|
let stateActions = getState().actions;
|
|
167
231
|
const stateActionList = Object.entries(stateActions).filter(
|
|
168
|
-
([k, v]) => !v.disableInWorkflow
|
|
232
|
+
([k, v]) => !v.disableInWorkflow,
|
|
169
233
|
);
|
|
234
|
+
const [tableSummary, triggerMatrix] = await Promise.all([
|
|
235
|
+
summarizeTables(),
|
|
236
|
+
Promise.resolve(summarizeTriggerActionMatrix()),
|
|
237
|
+
]);
|
|
238
|
+
const contextBlocks = [
|
|
239
|
+
tableSummary && `Current tables and key fields:\n${tableSummary}`,
|
|
240
|
+
triggerMatrix && `Workflow trigger compatibility:\n${triggerMatrix}`,
|
|
241
|
+
]
|
|
242
|
+
.filter(Boolean)
|
|
243
|
+
.join("\n\n");
|
|
170
244
|
|
|
171
245
|
return `Use the generate_workflow tool to construct computational workflows according to specifications. You must create
|
|
172
246
|
the workflow by calling the generate_workflow tool, with the step required to implement the specification.
|
|
173
247
|
|
|
248
|
+
${contextBlocks}
|
|
249
|
+
|
|
174
250
|
The steps are specified as JSON objects. Each step has a name, specified in the step_name key in the JSON object.
|
|
175
251
|
The step name should be a valid JavaScript identifier.
|
|
176
252
|
|
|
253
|
+
When the user explicitly names an action/step type (for example "ForLoop" or "Toast"), ensure the generated workflow contains at least one step whose step_configuration.step_type exactly matches every requested action. Only skip this if the action genuinely does not exist; in that case, clearly explain the omission in the workflow description.
|
|
254
|
+
|
|
177
255
|
Each run of the workflow is executed in the presence of a context, which is a JavaScript object that individual
|
|
178
256
|
steps can read values from and write values to. This context is a state that is persisted on disk for each workflow
|
|
179
257
|
run.
|
|
@@ -239,7 +317,7 @@ class GenerateWorkflow {
|
|
|
239
317
|
|
|
240
318
|
static async execute(
|
|
241
319
|
{ workflow_steps, workflow_name, when_trigger, trigger_table },
|
|
242
|
-
req
|
|
320
|
+
req,
|
|
243
321
|
) {
|
|
244
322
|
const steps = this.process_all_steps(workflow_steps);
|
|
245
323
|
let table_id;
|
|
@@ -268,7 +346,7 @@ class GenerateWorkflow {
|
|
|
268
346
|
"Workflow created. " +
|
|
269
347
|
a(
|
|
270
348
|
{ target: "_blank", href: `/actions/configure/${trigger.id}` },
|
|
271
|
-
"Configure workflow."
|
|
349
|
+
"Configure workflow.",
|
|
272
350
|
),
|
|
273
351
|
};
|
|
274
352
|
}
|
|
@@ -286,21 +364,29 @@ class GenerateWorkflow {
|
|
|
286
364
|
step.id = ix + 1;
|
|
287
365
|
});
|
|
288
366
|
const mmdia = WorkflowStep.generate_diagram(
|
|
289
|
-
steps.map((s) => new WorkflowStep(s))
|
|
367
|
+
steps.map((s) => new WorkflowStep(s)),
|
|
290
368
|
);
|
|
369
|
+
console.log({ mmdia });
|
|
291
370
|
return (
|
|
292
371
|
div(
|
|
293
372
|
`${workflow_name}${when_trigger ? `: ${when_trigger}` : ""}${
|
|
294
373
|
trigger_table ? ` on ${trigger_table}` : ""
|
|
295
|
-
}
|
|
374
|
+
}`,
|
|
296
375
|
) +
|
|
297
376
|
pre({ class: "mermaid" }, mmdia) +
|
|
298
|
-
script(
|
|
377
|
+
script(
|
|
378
|
+
domReady(`
|
|
379
|
+
ensure_script_loaded("/static_assets/"+_sc_version_tag+"/mermaid.min.js", () => {
|
|
380
|
+
mermaid.initialize({ startOnLoad: false });
|
|
381
|
+
mermaid.run({ querySelector: ".mermaid" });
|
|
382
|
+
});
|
|
383
|
+
`),
|
|
384
|
+
)
|
|
299
385
|
);
|
|
300
386
|
}
|
|
301
387
|
|
|
302
388
|
return `A workflow! Step names: ${workflow_steps.map(
|
|
303
|
-
(s) => s.step_name
|
|
389
|
+
(s) => s.step_name,
|
|
304
390
|
)}. Upgrade Saltcorn to see diagrams in copilot`;
|
|
305
391
|
}
|
|
306
392
|
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
const GenerateTables = require("../actions/generate-tables");
|
|
2
|
+
const Table = require("@saltcorn/data/models/table");
|
|
3
|
+
|
|
4
|
+
const normalizeTablesPayload = (rawPayload) => {
|
|
5
|
+
if (!rawPayload) return { tables: [] };
|
|
6
|
+
let payload = rawPayload;
|
|
7
|
+
if (typeof payload === "string") {
|
|
8
|
+
try {
|
|
9
|
+
payload = JSON.parse(payload);
|
|
10
|
+
} catch (e) {
|
|
11
|
+
console.error("Failed to parse generate_tables payload", e);
|
|
12
|
+
return { tables: [] };
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
if (typeof payload !== "object") return { tables: [] };
|
|
16
|
+
const normalized = { ...payload };
|
|
17
|
+
normalized.tables = Array.isArray(normalized.tables)
|
|
18
|
+
? normalized.tables.filter(Boolean).map((table) => ({
|
|
19
|
+
...table,
|
|
20
|
+
fields: Array.isArray(table?.fields)
|
|
21
|
+
? table.fields.filter(Boolean).map((field) => ({
|
|
22
|
+
...field,
|
|
23
|
+
type_and_configuration: field?.type_and_configuration || {},
|
|
24
|
+
}))
|
|
25
|
+
: [],
|
|
26
|
+
}))
|
|
27
|
+
: [];
|
|
28
|
+
return normalized;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const summarizeTables = (tables) =>
|
|
32
|
+
tables.map((table, idx) => {
|
|
33
|
+
const fields = (table.fields || []).slice(0, 5);
|
|
34
|
+
const fieldSummary = fields
|
|
35
|
+
.map((field) => {
|
|
36
|
+
const fname = field?.name || "(missing name)";
|
|
37
|
+
const ftype = field?.type_and_configuration?.data_type || "Unknown";
|
|
38
|
+
return `${fname}:${ftype}`;
|
|
39
|
+
})
|
|
40
|
+
.join(", ");
|
|
41
|
+
const ellipsis = table.fields.length > fields.length ? "..." : "";
|
|
42
|
+
return `${idx + 1}. ${table.table_name || "(missing name)"} – ${
|
|
43
|
+
table.fields.length
|
|
44
|
+
} field(s)${fieldSummary ? ` (${fieldSummary}${ellipsis})` : ""}`;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const collectTableWarnings = (tables) => {
|
|
48
|
+
const warnings = [];
|
|
49
|
+
const seenTables = new Set();
|
|
50
|
+
tables.forEach((table, tableIdx) => {
|
|
51
|
+
const tableLabel = table.table_name || `Table #${tableIdx + 1}`;
|
|
52
|
+
if (!table.table_name)
|
|
53
|
+
warnings.push(`${tableLabel} is missing a table_name.`);
|
|
54
|
+
else if (seenTables.has(table.table_name))
|
|
55
|
+
warnings.push(`Duplicate table name "${table.table_name}".`);
|
|
56
|
+
else seenTables.add(table.table_name);
|
|
57
|
+
|
|
58
|
+
if (!Array.isArray(table.fields) || table.fields.length === 0)
|
|
59
|
+
warnings.push(`${tableLabel} does not define any fields.`);
|
|
60
|
+
|
|
61
|
+
const fieldNames = new Set();
|
|
62
|
+
(table.fields || []).forEach((field, fieldIdx) => {
|
|
63
|
+
const fieldLabel = field?.name || `Field #${fieldIdx + 1}`;
|
|
64
|
+
if (!field?.name)
|
|
65
|
+
warnings.push(`${tableLabel} has a field without a name.`);
|
|
66
|
+
else if (fieldNames.has(field.name))
|
|
67
|
+
warnings.push(`${tableLabel} repeats the field name "${field.name}".`);
|
|
68
|
+
else fieldNames.add(field.name);
|
|
69
|
+
|
|
70
|
+
if (!field?.type_and_configuration?.data_type)
|
|
71
|
+
warnings.push(
|
|
72
|
+
`${tableLabel}.${fieldLabel} must include type_and_configuration.data_type.`,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
if ((field?.name || "").toLowerCase() === "id")
|
|
76
|
+
warnings.push(
|
|
77
|
+
`${tableLabel}.${fieldLabel} should be omitted because every table already has an auto-increment id.`,
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
return warnings;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const fetchExistingTableNameSet = async () => {
|
|
85
|
+
const existingTables = await Table.find({});
|
|
86
|
+
const names = new Set();
|
|
87
|
+
existingTables.forEach((table) => {
|
|
88
|
+
if (table?.name) names.add(table.name.toLowerCase());
|
|
89
|
+
});
|
|
90
|
+
return names;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const partitionTablesByExistence = async (tables = []) => {
|
|
94
|
+
const existingNames = await fetchExistingTableNameSet();
|
|
95
|
+
const seenNewNames = new Set();
|
|
96
|
+
const newTables = [];
|
|
97
|
+
const skippedExisting = [];
|
|
98
|
+
const skippedDuplicates = [];
|
|
99
|
+
tables.forEach((table) => {
|
|
100
|
+
const tableName =
|
|
101
|
+
typeof table?.table_name === "string" ? table.table_name.trim() : "";
|
|
102
|
+
const normalized = tableName.toLowerCase();
|
|
103
|
+
if (tableName && existingNames.has(normalized)) {
|
|
104
|
+
skippedExisting.push(tableName);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (tableName && seenNewNames.has(normalized)) {
|
|
108
|
+
skippedDuplicates.push(tableName);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (tableName) seenNewNames.add(normalized);
|
|
112
|
+
newTables.push(table);
|
|
113
|
+
});
|
|
114
|
+
return { newTables, skippedExisting, skippedDuplicates };
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const partitionTablesByValidity = (tables = []) => {
|
|
118
|
+
const validTables = [];
|
|
119
|
+
const skippedMissingNames = [];
|
|
120
|
+
const skippedMissingFields = [];
|
|
121
|
+
tables.forEach((table, idx) => {
|
|
122
|
+
const rawName =
|
|
123
|
+
typeof table?.table_name === "string" ? table.table_name.trim() : "";
|
|
124
|
+
const fallbackLabel = rawName || `Table #${idx + 1}`;
|
|
125
|
+
if (!rawName) {
|
|
126
|
+
skippedMissingNames.push(fallbackLabel);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const fields = Array.isArray(table?.fields) ? table.fields : [];
|
|
130
|
+
if (!fields.length) {
|
|
131
|
+
skippedMissingFields.push(rawName);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
validTables.push({ ...table, table_name: rawName, fields });
|
|
135
|
+
});
|
|
136
|
+
return { validTables, skippedMissingNames, skippedMissingFields };
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const payloadFromToolCall = (tool_call) => {
|
|
140
|
+
if (!tool_call) return { tables: [] };
|
|
141
|
+
if (tool_call.input) return normalizeTablesPayload(tool_call.input);
|
|
142
|
+
if (tool_call.function?.arguments)
|
|
143
|
+
return normalizeTablesPayload(tool_call.function.arguments);
|
|
144
|
+
return { tables: [] };
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
class GenerateTablesSkill {
|
|
148
|
+
static skill_name = "Database design";
|
|
149
|
+
|
|
150
|
+
get skill_label() {
|
|
151
|
+
return "Database Design";
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
constructor(cfg) {
|
|
155
|
+
Object.assign(this, cfg);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async systemPrompt() {
|
|
159
|
+
return await GenerateTables.system_prompt();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
get userActions() {
|
|
163
|
+
return {
|
|
164
|
+
async apply_copilot_tables({ user, tables }) {
|
|
165
|
+
if (!tables?.length) return { notify: "Nothing to create." };
|
|
166
|
+
const { newTables, skippedExisting, skippedDuplicates } =
|
|
167
|
+
await partitionTablesByExistence(tables);
|
|
168
|
+
const { validTables, skippedMissingNames, skippedMissingFields } =
|
|
169
|
+
partitionTablesByValidity(newTables);
|
|
170
|
+
if (!validTables.length) {
|
|
171
|
+
const skippedMessages = [];
|
|
172
|
+
if (skippedExisting.length)
|
|
173
|
+
skippedMessages.push(
|
|
174
|
+
`Existing tables: ${skippedExisting.join(", ")}`,
|
|
175
|
+
);
|
|
176
|
+
if (skippedDuplicates.length)
|
|
177
|
+
skippedMessages.push(
|
|
178
|
+
`Duplicate definitions: ${skippedDuplicates.join(", ")}`,
|
|
179
|
+
);
|
|
180
|
+
if (skippedMissingNames.length)
|
|
181
|
+
skippedMessages.push(
|
|
182
|
+
`Missing table_name: ${skippedMissingNames.join(", ")}`,
|
|
183
|
+
);
|
|
184
|
+
if (skippedMissingFields.length)
|
|
185
|
+
skippedMessages.push(
|
|
186
|
+
`Tables without fields: ${skippedMissingFields.join(", ")}`,
|
|
187
|
+
);
|
|
188
|
+
return {
|
|
189
|
+
notify:
|
|
190
|
+
skippedMessages.length > 0
|
|
191
|
+
? `Nothing to create. Skipped ${skippedMessages.join("; ")}.`
|
|
192
|
+
: "Nothing to create.",
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
await GenerateTables.execute({ tables: validTables }, { user });
|
|
196
|
+
const createdNames = validTables.map((t) => t.table_name).join(", ");
|
|
197
|
+
const skippedMessages = [];
|
|
198
|
+
if (skippedExisting.length)
|
|
199
|
+
skippedMessages.push(
|
|
200
|
+
`Skipped existing tables: ${skippedExisting.join(", ")}`,
|
|
201
|
+
);
|
|
202
|
+
if (skippedDuplicates.length)
|
|
203
|
+
skippedMessages.push(
|
|
204
|
+
`Ignored duplicate definitions: ${skippedDuplicates.join(", ")}`,
|
|
205
|
+
);
|
|
206
|
+
if (skippedMissingNames.length)
|
|
207
|
+
skippedMessages.push(
|
|
208
|
+
`Missing table_name: ${skippedMissingNames.join(", ")}`,
|
|
209
|
+
);
|
|
210
|
+
if (skippedMissingFields.length)
|
|
211
|
+
skippedMessages.push(
|
|
212
|
+
`Tables without fields: ${skippedMissingFields.join(", ")}`,
|
|
213
|
+
);
|
|
214
|
+
return {
|
|
215
|
+
notify: [`Created tables: ${createdNames}`, ...skippedMessages].join(
|
|
216
|
+
". ",
|
|
217
|
+
),
|
|
218
|
+
};
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
provideTools = () => {
|
|
224
|
+
const parameters = GenerateTables.json_schema();
|
|
225
|
+
return {
|
|
226
|
+
type: "function",
|
|
227
|
+
process: async (input) => {
|
|
228
|
+
const payload = normalizeTablesPayload(input);
|
|
229
|
+
const tables = payload.tables || [];
|
|
230
|
+
if (!tables.length) {
|
|
231
|
+
return "No tables were provided for generate_tables.";
|
|
232
|
+
}
|
|
233
|
+
const { newTables, skippedExisting, skippedDuplicates } =
|
|
234
|
+
await partitionTablesByExistence(tables);
|
|
235
|
+
const { validTables, skippedMissingNames, skippedMissingFields } =
|
|
236
|
+
partitionTablesByValidity(newTables);
|
|
237
|
+
const summaryLines = validTables.length
|
|
238
|
+
? summarizeTables(validTables).map((line) => `- ${line}`)
|
|
239
|
+
: [];
|
|
240
|
+
const warnings = collectTableWarnings(tables);
|
|
241
|
+
if (skippedExisting.length)
|
|
242
|
+
skippedExisting.forEach((name) =>
|
|
243
|
+
warnings.push(
|
|
244
|
+
`Table "${name}" already exists and will not be recreated by generate_tables.`,
|
|
245
|
+
),
|
|
246
|
+
);
|
|
247
|
+
if (skippedDuplicates.length)
|
|
248
|
+
skippedDuplicates.forEach((name) =>
|
|
249
|
+
warnings.push(
|
|
250
|
+
`Table "${name}" was defined multiple times in this request; only the first definition will be used.`,
|
|
251
|
+
),
|
|
252
|
+
);
|
|
253
|
+
skippedMissingNames.forEach((label) =>
|
|
254
|
+
warnings.push(
|
|
255
|
+
`${label} is skipped because it does not include a table_name.`,
|
|
256
|
+
),
|
|
257
|
+
);
|
|
258
|
+
skippedMissingFields.forEach((label) =>
|
|
259
|
+
warnings.push(
|
|
260
|
+
`Table "${label}" is skipped because it does not define any fields.`,
|
|
261
|
+
),
|
|
262
|
+
);
|
|
263
|
+
const warningLines = warnings.length
|
|
264
|
+
? ["Warnings:", ...warnings.map((w) => `- ${w}`)]
|
|
265
|
+
: [];
|
|
266
|
+
const summarySection = summaryLines.length
|
|
267
|
+
? [
|
|
268
|
+
`Ready to create ${validTables.length} new table${
|
|
269
|
+
validTables.length === 1 ? "" : "s"
|
|
270
|
+
}:`,
|
|
271
|
+
...summaryLines,
|
|
272
|
+
]
|
|
273
|
+
: [
|
|
274
|
+
"No new tables remain after removing existing, duplicate, or invalid table definitions.",
|
|
275
|
+
];
|
|
276
|
+
return [
|
|
277
|
+
`Received ${tables.length} table definition${tables.length === 1 ? "" : "s"}.`,
|
|
278
|
+
...summarySection,
|
|
279
|
+
...warningLines,
|
|
280
|
+
].join("\n");
|
|
281
|
+
},
|
|
282
|
+
postProcess: async ({ tool_call }) => {
|
|
283
|
+
const payload = payloadFromToolCall(tool_call);
|
|
284
|
+
const tables = payload.tables || [];
|
|
285
|
+
const { newTables, skippedExisting, skippedDuplicates } =
|
|
286
|
+
await partitionTablesByExistence(tables);
|
|
287
|
+
const { validTables, skippedMissingNames, skippedMissingFields } =
|
|
288
|
+
partitionTablesByValidity(newTables);
|
|
289
|
+
let preview = "";
|
|
290
|
+
try {
|
|
291
|
+
if (validTables.length) {
|
|
292
|
+
preview = GenerateTables.render_html({ tables: validTables });
|
|
293
|
+
} else {
|
|
294
|
+
preview =
|
|
295
|
+
'<div class="alert alert-info">No new tables to preview because every provided table already exists or was invalid.</div>';
|
|
296
|
+
}
|
|
297
|
+
} catch (e) {
|
|
298
|
+
console.log("We are in postProcess but rendering failed", {
|
|
299
|
+
e,
|
|
300
|
+
time: new Date(),
|
|
301
|
+
});
|
|
302
|
+
preview = `<pre>${JSON.stringify(payload, null, 2)}</pre>`;
|
|
303
|
+
}
|
|
304
|
+
const warningChunks = [];
|
|
305
|
+
if (skippedExisting.length)
|
|
306
|
+
warningChunks.push(
|
|
307
|
+
`Skipped existing tables: ${skippedExisting.join(", ")}`,
|
|
308
|
+
);
|
|
309
|
+
if (skippedDuplicates.length)
|
|
310
|
+
warningChunks.push(
|
|
311
|
+
`Ignored duplicate definitions: ${skippedDuplicates.join(", ")}`,
|
|
312
|
+
);
|
|
313
|
+
if (skippedMissingNames.length)
|
|
314
|
+
warningChunks.push(
|
|
315
|
+
`Missing table_name: ${skippedMissingNames.join(", ")}`,
|
|
316
|
+
);
|
|
317
|
+
if (skippedMissingFields.length)
|
|
318
|
+
warningChunks.push(
|
|
319
|
+
`Tables without fields: ${skippedMissingFields.join(", ")}`,
|
|
320
|
+
);
|
|
321
|
+
const warningHtml = warningChunks.length
|
|
322
|
+
? `<div class="alert alert-warning">${warningChunks.join("<br/>")}</div>`
|
|
323
|
+
: "";
|
|
324
|
+
return {
|
|
325
|
+
stop: true,
|
|
326
|
+
add_response: `${warningHtml}${preview}`,
|
|
327
|
+
add_user_action:
|
|
328
|
+
validTables.length > 0
|
|
329
|
+
? {
|
|
330
|
+
name: "apply_copilot_tables",
|
|
331
|
+
type: "button",
|
|
332
|
+
label: `Create tables (${validTables
|
|
333
|
+
.map((t) => t.table_name)
|
|
334
|
+
.join(", ")})`,
|
|
335
|
+
input: { tables: validTables },
|
|
336
|
+
}
|
|
337
|
+
: undefined,
|
|
338
|
+
};
|
|
339
|
+
},
|
|
340
|
+
function: {
|
|
341
|
+
name: GenerateTables.function_name,
|
|
342
|
+
description: GenerateTables.description,
|
|
343
|
+
parameters,
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
module.exports = GenerateTablesSkill;
|