@saltcorn/copilot 0.7.2 → 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-tables.js +13 -2
- package/actions/generate-workflow.js +106 -20
- package/agent-skills/database-design.js +191 -27
- package/agent-skills/pagegen.js +0 -1
- package/agent-skills/workflow.js +638 -0
- package/builder-gen.js +17 -6
- package/chat-copilot.js +0 -2
- package/copilot-as-agent.js +18 -3
- package/index.js +1 -0
- package/package.json +1 -1
- package/page-gen-action.js +0 -5
|
@@ -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 {
|
|
@@ -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
|
|
|
@@ -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
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const GenerateTables = require("../actions/generate-tables");
|
|
2
|
+
const Table = require("@saltcorn/data/models/table");
|
|
2
3
|
|
|
3
4
|
const normalizeTablesPayload = (rawPayload) => {
|
|
4
5
|
if (!rawPayload) return { tables: [] };
|
|
@@ -68,18 +69,73 @@ const collectTableWarnings = (tables) => {
|
|
|
68
69
|
|
|
69
70
|
if (!field?.type_and_configuration?.data_type)
|
|
70
71
|
warnings.push(
|
|
71
|
-
`${tableLabel}.${fieldLabel} must include type_and_configuration.data_type
|
|
72
|
+
`${tableLabel}.${fieldLabel} must include type_and_configuration.data_type.`,
|
|
72
73
|
);
|
|
73
74
|
|
|
74
75
|
if ((field?.name || "").toLowerCase() === "id")
|
|
75
76
|
warnings.push(
|
|
76
|
-
`${tableLabel}.${fieldLabel} should be omitted because every table already has an auto-increment id
|
|
77
|
+
`${tableLabel}.${fieldLabel} should be omitted because every table already has an auto-increment id.`,
|
|
77
78
|
);
|
|
78
79
|
});
|
|
79
80
|
});
|
|
80
81
|
return warnings;
|
|
81
82
|
};
|
|
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
|
+
|
|
83
139
|
const payloadFromToolCall = (tool_call) => {
|
|
84
140
|
if (!tool_call) return { tables: [] };
|
|
85
141
|
if (tool_call.input) return normalizeTablesPayload(tool_call.input);
|
|
@@ -96,79 +152,187 @@ class GenerateTablesSkill {
|
|
|
96
152
|
}
|
|
97
153
|
|
|
98
154
|
constructor(cfg) {
|
|
99
|
-
console.log("GenerateTablesSkill.constructor called", { cfg });
|
|
100
155
|
Object.assign(this, cfg);
|
|
101
156
|
}
|
|
102
157
|
|
|
103
158
|
async systemPrompt() {
|
|
104
|
-
console.log("GenerateTablesSkill.systemPrompt called");
|
|
105
159
|
return await GenerateTables.system_prompt();
|
|
106
160
|
}
|
|
107
161
|
|
|
108
162
|
get userActions() {
|
|
109
|
-
console.log("GenerateTablesSkill.userActions getter accessed");
|
|
110
163
|
return {
|
|
111
164
|
async apply_copilot_tables({ user, tables }) {
|
|
112
|
-
console.log("GenerateTablesSkill.userActions.apply_copilot_tables called", {
|
|
113
|
-
user_id: user?.id,
|
|
114
|
-
table_count: tables?.length,
|
|
115
|
-
});
|
|
116
165
|
if (!tables?.length) return { notify: "Nothing to create." };
|
|
117
|
-
|
|
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
|
+
);
|
|
118
214
|
return {
|
|
119
|
-
notify: `Created tables: ${
|
|
215
|
+
notify: [`Created tables: ${createdNames}`, ...skippedMessages].join(
|
|
216
|
+
". ",
|
|
217
|
+
),
|
|
120
218
|
};
|
|
121
219
|
},
|
|
122
220
|
};
|
|
123
221
|
}
|
|
124
222
|
|
|
125
223
|
provideTools = () => {
|
|
126
|
-
console.log("GenerateTablesSkill.provideTools called");
|
|
127
224
|
const parameters = GenerateTables.json_schema();
|
|
128
225
|
return {
|
|
129
226
|
type: "function",
|
|
130
227
|
process: async (input) => {
|
|
131
228
|
const payload = normalizeTablesPayload(input);
|
|
132
229
|
const tables = payload.tables || [];
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
|
|
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
|
+
: [];
|
|
138
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
|
+
);
|
|
139
263
|
const warningLines = warnings.length
|
|
140
264
|
? ["Warnings:", ...warnings.map((w) => `- ${w}`)]
|
|
141
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
|
+
];
|
|
142
276
|
return [
|
|
143
|
-
`Received ${tables.length} table definition${tables.length === 1 ? "" : "s"}
|
|
144
|
-
...
|
|
277
|
+
`Received ${tables.length} table definition${tables.length === 1 ? "" : "s"}.`,
|
|
278
|
+
...summarySection,
|
|
145
279
|
...warningLines,
|
|
146
280
|
].join("\n");
|
|
147
281
|
},
|
|
148
282
|
postProcess: async ({ tool_call }) => {
|
|
149
|
-
console.log("GenerateTablesSkill.provideTools.postProcess called", {
|
|
150
|
-
has_input: !!tool_call?.input,
|
|
151
|
-
});
|
|
152
283
|
const payload = payloadFromToolCall(tool_call);
|
|
153
284
|
const tables = payload.tables || [];
|
|
285
|
+
const { newTables, skippedExisting, skippedDuplicates } =
|
|
286
|
+
await partitionTablesByExistence(tables);
|
|
287
|
+
const { validTables, skippedMissingNames, skippedMissingFields } =
|
|
288
|
+
partitionTablesByValidity(newTables);
|
|
154
289
|
let preview = "";
|
|
155
290
|
try {
|
|
156
|
-
|
|
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
|
+
}
|
|
157
297
|
} catch (e) {
|
|
298
|
+
console.log("We are in postProcess but rendering failed", {
|
|
299
|
+
e,
|
|
300
|
+
time: new Date(),
|
|
301
|
+
});
|
|
158
302
|
preview = `<pre>${JSON.stringify(payload, null, 2)}</pre>`;
|
|
159
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
|
+
: "";
|
|
160
324
|
return {
|
|
161
325
|
stop: true,
|
|
162
|
-
add_response: preview
|
|
326
|
+
add_response: `${warningHtml}${preview}`,
|
|
163
327
|
add_user_action:
|
|
164
|
-
|
|
328
|
+
validTables.length > 0
|
|
165
329
|
? {
|
|
166
330
|
name: "apply_copilot_tables",
|
|
167
331
|
type: "button",
|
|
168
|
-
label: `Create tables (${
|
|
332
|
+
label: `Create tables (${validTables
|
|
169
333
|
.map((t) => t.table_name)
|
|
170
334
|
.join(", ")})`,
|
|
171
|
-
input: { tables },
|
|
335
|
+
input: { tables: validTables },
|
|
172
336
|
}
|
|
173
337
|
: undefined,
|
|
174
338
|
};
|
package/agent-skills/pagegen.js
CHANGED
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
const Trigger = require("@saltcorn/data/models/trigger");
|
|
2
|
+
const WorkflowStep = require("@saltcorn/data/models/workflow_step");
|
|
3
|
+
const { getState } = require("@saltcorn/data/db/state");
|
|
4
|
+
const GenerateWorkflow = require("../actions/generate-workflow");
|
|
5
|
+
|
|
6
|
+
const table_triggers = ["Insert", "Update", "Delete", "Validate"];
|
|
7
|
+
|
|
8
|
+
const TABLE_TRIGGER_WHEN = new Set(
|
|
9
|
+
Trigger.when_options.filter((opt) => table_triggers.includes(opt)),
|
|
10
|
+
);
|
|
11
|
+
const ALLOWED_WHEN = new Set(Trigger.when_options);
|
|
12
|
+
|
|
13
|
+
const FALLBACK_ACTION_CATALOG = { namespaces: [], byName: {} };
|
|
14
|
+
const ACTION_SUMMARY_LIMIT = 5;
|
|
15
|
+
const ACTION_HTML_LIMIT = 12;
|
|
16
|
+
const RANDOM_STEP_COUNT = { min: 2, max: 3 };
|
|
17
|
+
const SIMPLE_FIELD_TYPES = new Set(["string", "number", "integer", "boolean"]);
|
|
18
|
+
|
|
19
|
+
let workflowSchemaCache = null;
|
|
20
|
+
let workflowSchemaLoading = null;
|
|
21
|
+
let actionCatalogCache = null;
|
|
22
|
+
|
|
23
|
+
const ensureWorkflowParameters = () => {
|
|
24
|
+
if (!workflowSchemaCache && !workflowSchemaLoading) {
|
|
25
|
+
workflowSchemaLoading = GenerateWorkflow.json_schema()
|
|
26
|
+
.then((schema) => {
|
|
27
|
+
workflowSchemaCache = schema;
|
|
28
|
+
})
|
|
29
|
+
.catch((error) => {
|
|
30
|
+
console.error("GenerateWorkflowSkill: failed to load schema", error);
|
|
31
|
+
})
|
|
32
|
+
.finally(() => {
|
|
33
|
+
workflowSchemaLoading = null;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
//console.log({ workflowSchemaCache }, "Workflow schema load");
|
|
37
|
+
return workflowSchemaCache;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const defaultWorkflowPayload = () => ({
|
|
41
|
+
workflow_steps: [],
|
|
42
|
+
workflow_name: "",
|
|
43
|
+
when_trigger: "",
|
|
44
|
+
trigger_table: "",
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const randomInt = (min, max) =>
|
|
48
|
+
Math.floor(Math.random() * (max - min + 1)) + min;
|
|
49
|
+
|
|
50
|
+
const randomChoice = (items) =>
|
|
51
|
+
items.length ? items[randomInt(0, items.length - 1)] : undefined;
|
|
52
|
+
|
|
53
|
+
const randomBool = () => Math.random() < 0.5;
|
|
54
|
+
|
|
55
|
+
const generateWorkflowName = () =>
|
|
56
|
+
`Workflow ${Math.floor(Math.random() * 9000) + 1000}`;
|
|
57
|
+
|
|
58
|
+
const sanitizeString = (value) =>
|
|
59
|
+
typeof value === "string" ? value.trim() : "";
|
|
60
|
+
|
|
61
|
+
const buildActionCatalog = () => {
|
|
62
|
+
try {
|
|
63
|
+
const byName = {};
|
|
64
|
+
const namespaceMap = new Map();
|
|
65
|
+
const register = (name, namespace, description, origin) => {
|
|
66
|
+
const cleanName = sanitizeString(name);
|
|
67
|
+
if (!cleanName) return;
|
|
68
|
+
const cleanNamespace = sanitizeString(namespace) || "Other";
|
|
69
|
+
const entry = {
|
|
70
|
+
name: cleanName,
|
|
71
|
+
namespace: cleanNamespace,
|
|
72
|
+
description: sanitizeString(description),
|
|
73
|
+
origin: origin || "core",
|
|
74
|
+
};
|
|
75
|
+
if (byName[cleanName]) return;
|
|
76
|
+
byName[cleanName] = entry;
|
|
77
|
+
if (!namespaceMap.has(cleanNamespace))
|
|
78
|
+
namespaceMap.set(cleanNamespace, []);
|
|
79
|
+
namespaceMap.get(cleanNamespace).push(entry);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const builtInExplain = WorkflowStep.builtInActionExplainers({
|
|
84
|
+
api_call: true,
|
|
85
|
+
});
|
|
86
|
+
Object.entries(builtInExplain).forEach(([name, description]) =>
|
|
87
|
+
register(name, "Workflow Actions", description, "built-in"),
|
|
88
|
+
);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.error("GenerateWorkflowSkill: built-in actions failed", error);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const state = getState?.();
|
|
95
|
+
const stateActions = state?.actions || {};
|
|
96
|
+
Object.entries(stateActions)
|
|
97
|
+
.filter(([_, action]) => !action.disableInWorkflow)
|
|
98
|
+
.forEach(([name, action]) =>
|
|
99
|
+
register(
|
|
100
|
+
name,
|
|
101
|
+
action.namespace || action.plugin_name || "Other",
|
|
102
|
+
action.description,
|
|
103
|
+
action.plugin_name || "core",
|
|
104
|
+
),
|
|
105
|
+
);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.error(
|
|
108
|
+
"GenerateWorkflowSkill: failed to read state actions",
|
|
109
|
+
error,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const triggers = Trigger.find({
|
|
115
|
+
when_trigger: { or: ["API call", "Never"] },
|
|
116
|
+
});
|
|
117
|
+
triggers.forEach((tr) => {
|
|
118
|
+
const namespace = tr.action === "Workflow" ? "Workflows" : "Triggers";
|
|
119
|
+
register(tr.name, namespace, tr.description, "trigger");
|
|
120
|
+
});
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.error("GenerateWorkflowSkill: trigger lookup failed", error);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const namespaces = Array.from(namespaceMap.entries())
|
|
126
|
+
.map(([namespace, actions]) => ({
|
|
127
|
+
namespace,
|
|
128
|
+
label: namespace,
|
|
129
|
+
actions: actions.sort((a, b) => a.name.localeCompare(b.name)),
|
|
130
|
+
}))
|
|
131
|
+
.sort((a, b) => a.label.localeCompare(b.label));
|
|
132
|
+
|
|
133
|
+
return { byName, namespaces };
|
|
134
|
+
} catch (error) {
|
|
135
|
+
console.error("GenerateWorkflowSkill: action catalog failed", error);
|
|
136
|
+
return { namespaces: [], byName: {} };
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const ensureActionCatalog = () => {
|
|
141
|
+
if (!actionCatalogCache) actionCatalogCache = buildActionCatalog();
|
|
142
|
+
return actionCatalogCache || FALLBACK_ACTION_CATALOG;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const summarizeActionCatalog = (limit = ACTION_SUMMARY_LIMIT) => {
|
|
146
|
+
const catalog = ensureActionCatalog();
|
|
147
|
+
if (!catalog.namespaces.length) return "";
|
|
148
|
+
return catalog.namespaces
|
|
149
|
+
.map(({ label, actions }) => {
|
|
150
|
+
const names = actions
|
|
151
|
+
.slice(0, limit)
|
|
152
|
+
.map((a) => a.name)
|
|
153
|
+
.join(", ");
|
|
154
|
+
const suffix = actions.length > limit ? " ..." : "";
|
|
155
|
+
return `${label}: ${names}${suffix}`;
|
|
156
|
+
})
|
|
157
|
+
.join("\n");
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const toPlainObject = (value) =>
|
|
161
|
+
value && typeof value === "object" && !Array.isArray(value)
|
|
162
|
+
? { ...value }
|
|
163
|
+
: {};
|
|
164
|
+
|
|
165
|
+
const mergeStepConfiguration = (step) => {
|
|
166
|
+
const config = toPlainObject(step.step_configuration);
|
|
167
|
+
if (typeof config.step_type === "string")
|
|
168
|
+
config.step_type = sanitizeString(config.step_type);
|
|
169
|
+
else if (config.step_type != null)
|
|
170
|
+
config.step_type = sanitizeString(String(config.step_type));
|
|
171
|
+
else {
|
|
172
|
+
const fallbackType = sanitizeString(step.step_type);
|
|
173
|
+
if (fallbackType) config.step_type = fallbackType;
|
|
174
|
+
}
|
|
175
|
+
const reserved = new Set([
|
|
176
|
+
"step_name",
|
|
177
|
+
"only_if",
|
|
178
|
+
"next_step",
|
|
179
|
+
"step_configuration",
|
|
180
|
+
"step_type",
|
|
181
|
+
]);
|
|
182
|
+
Object.entries(step || {}).forEach(([key, value]) => {
|
|
183
|
+
if (reserved.has(key)) return;
|
|
184
|
+
if (config[key] === undefined) config[key] = value;
|
|
185
|
+
});
|
|
186
|
+
return config;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const normalizeWorkflowPayload = (rawPayload) => {
|
|
190
|
+
if (!rawPayload) return defaultWorkflowPayload();
|
|
191
|
+
let payload = rawPayload;
|
|
192
|
+
if (typeof payload === "string") {
|
|
193
|
+
try {
|
|
194
|
+
payload = JSON.parse(payload);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
console.error("GenerateWorkflowSkill: failed to parse payload", error);
|
|
197
|
+
return defaultWorkflowPayload();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (typeof payload !== "object" || Array.isArray(payload))
|
|
201
|
+
return defaultWorkflowPayload();
|
|
202
|
+
const normalizedSteps = Array.isArray(payload.workflow_steps)
|
|
203
|
+
? payload.workflow_steps.filter(Boolean).map((step) => {
|
|
204
|
+
const plain = toPlainObject(step);
|
|
205
|
+
const step_configuration = mergeStepConfiguration(plain);
|
|
206
|
+
return {
|
|
207
|
+
step_name: sanitizeString(plain.step_name || plain.name),
|
|
208
|
+
only_if: sanitizeString(plain.only_if),
|
|
209
|
+
next_step: sanitizeString(plain.next_step),
|
|
210
|
+
step_configuration,
|
|
211
|
+
};
|
|
212
|
+
})
|
|
213
|
+
: [];
|
|
214
|
+
const normalized = {
|
|
215
|
+
workflow_steps: normalizedSteps,
|
|
216
|
+
workflow_name: sanitizeString(payload.workflow_name),
|
|
217
|
+
trigger_table: sanitizeString(payload.trigger_table),
|
|
218
|
+
when_trigger: ALLOWED_WHEN.has(payload.when_trigger)
|
|
219
|
+
? payload.when_trigger
|
|
220
|
+
: "",
|
|
221
|
+
};
|
|
222
|
+
if (!normalized.when_trigger) normalized.when_trigger = "Never";
|
|
223
|
+
if (
|
|
224
|
+
normalized.when_trigger !== "Never" &&
|
|
225
|
+
!ALLOWED_WHEN.has(normalized.when_trigger)
|
|
226
|
+
)
|
|
227
|
+
normalized.when_trigger = "Never";
|
|
228
|
+
return normalized;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const analyzeWorkflowPayload = (payload) => {
|
|
232
|
+
const warnings = [];
|
|
233
|
+
const blocking = [];
|
|
234
|
+
const actionCatalog = ensureActionCatalog();
|
|
235
|
+
if (!payload.workflow_name) blocking.push("Workflow name is required.");
|
|
236
|
+
if (!payload.workflow_steps.length)
|
|
237
|
+
blocking.push("At least one workflow step is required.");
|
|
238
|
+
const seenNames = new Set();
|
|
239
|
+
const identifierStyle = /^[A-Za-z_][0-9A-Za-z_]*$/;
|
|
240
|
+
const knownTargets = new Set(
|
|
241
|
+
payload.workflow_steps.map((s) => s.step_name).filter(Boolean),
|
|
242
|
+
);
|
|
243
|
+
payload.workflow_steps.forEach((step, idx) => {
|
|
244
|
+
const label = step.step_name || `Step #${idx + 1}`;
|
|
245
|
+
if (!step.step_name) blocking.push(`${label} is missing step_name.`);
|
|
246
|
+
else if (seenNames.has(step.step_name))
|
|
247
|
+
blocking.push(`Duplicate step_name "${step.step_name}".`);
|
|
248
|
+
else seenNames.add(step.step_name);
|
|
249
|
+
const cfg = step.step_configuration;
|
|
250
|
+
if (!cfg || typeof cfg !== "object" || Array.isArray(cfg))
|
|
251
|
+
blocking.push(`${label} is missing step_configuration.`);
|
|
252
|
+
else {
|
|
253
|
+
const stepType = sanitizeString(cfg.step_type);
|
|
254
|
+
if (!stepType)
|
|
255
|
+
blocking.push(`${label} must specify step_configuration.step_type.`);
|
|
256
|
+
else if (!actionCatalog.byName[stepType])
|
|
257
|
+
warnings.push(`${label} uses unknown action "${stepType}".`);
|
|
258
|
+
}
|
|
259
|
+
const next = sanitizeString(step.next_step);
|
|
260
|
+
if (next && identifierStyle.test(next) && !knownTargets.has(next))
|
|
261
|
+
warnings.push(`${label} references unknown next_step "${next}".`);
|
|
262
|
+
});
|
|
263
|
+
if (TABLE_TRIGGER_WHEN.has(payload.when_trigger) && !payload.trigger_table)
|
|
264
|
+
blocking.push(`${payload.when_trigger} triggers require a trigger_table.`);
|
|
265
|
+
if (!TABLE_TRIGGER_WHEN.has(payload.when_trigger) && payload.trigger_table)
|
|
266
|
+
warnings.push(
|
|
267
|
+
`Trigger table "${payload.trigger_table}" will be ignored unless when_trigger is ${Array.from(TABLE_TRIGGER_WHEN).join("/")}.`,
|
|
268
|
+
);
|
|
269
|
+
return { warnings, blocking };
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const describeWorkflow = (payload) => {
|
|
273
|
+
const title = payload.workflow_name || "Unnamed workflow";
|
|
274
|
+
const triggerDescription = TABLE_TRIGGER_WHEN.has(payload.when_trigger)
|
|
275
|
+
? payload.trigger_table
|
|
276
|
+
? `${payload.when_trigger} on ${payload.trigger_table}`
|
|
277
|
+
: `${payload.when_trigger} (table not set)`
|
|
278
|
+
: payload.when_trigger === "Never" || !payload.when_trigger
|
|
279
|
+
? "Manual run (Never)"
|
|
280
|
+
: payload.when_trigger;
|
|
281
|
+
const stepLines = payload.workflow_steps.length
|
|
282
|
+
? payload.workflow_steps.map((step, idx) =>
|
|
283
|
+
formatStepDescription(step, idx),
|
|
284
|
+
)
|
|
285
|
+
: ["(no steps provided)"];
|
|
286
|
+
return [
|
|
287
|
+
`${title} – trigger: ${triggerDescription}`,
|
|
288
|
+
"Steps:",
|
|
289
|
+
...stepLines,
|
|
290
|
+
].join("\n");
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const escapeHtml = (str) =>
|
|
294
|
+
String(str || "")
|
|
295
|
+
.replace(/&/g, "&")
|
|
296
|
+
.replace(/</g, "<")
|
|
297
|
+
.replace(/>/g, ">")
|
|
298
|
+
.replace(/"/g, """)
|
|
299
|
+
.replace(/'/g, "'");
|
|
300
|
+
|
|
301
|
+
const buildIssuesHtml = ({ warnings, blocking }) => {
|
|
302
|
+
const chunks = [];
|
|
303
|
+
if (blocking.length) {
|
|
304
|
+
const items = blocking
|
|
305
|
+
.map((issue) => `<li>${escapeHtml(issue)}</li>`)
|
|
306
|
+
.join("");
|
|
307
|
+
chunks.push(
|
|
308
|
+
`<div class="alert alert-danger"><strong>Blocking issues</strong><ul class="mb-0">${items}</ul></div>`,
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
if (warnings.length) {
|
|
312
|
+
const items = warnings
|
|
313
|
+
.map((issue) => `<li>${escapeHtml(issue)}</li>`)
|
|
314
|
+
.join("");
|
|
315
|
+
chunks.push(
|
|
316
|
+
`<div class="alert alert-warning"><strong>Warnings</strong><ul class="mb-0">${items}</ul></div>`,
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
return chunks.join("");
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const renderWorkflowPreview = (payload) => {
|
|
323
|
+
if (!payload.workflow_steps.length) return "";
|
|
324
|
+
try {
|
|
325
|
+
return GenerateWorkflow.render_html(payload);
|
|
326
|
+
} catch (error) {
|
|
327
|
+
console.error("GenerateWorkflowSkill: preview failed", error);
|
|
328
|
+
return `<pre>${escapeHtml(JSON.stringify(payload, null, 2))}</pre>`;
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const formatIssueSection = (label, issues) =>
|
|
333
|
+
issues.length
|
|
334
|
+
? [label, ...issues.map((issue) => `- ${issue}`)].join("\n")
|
|
335
|
+
: `${label}: none`;
|
|
336
|
+
|
|
337
|
+
const payloadFromToolCall = (tool_call) => {
|
|
338
|
+
if (!tool_call) return normalizeWorkflowPayload();
|
|
339
|
+
if (Object.prototype.hasOwnProperty.call(tool_call, "input"))
|
|
340
|
+
return normalizeWorkflowPayload(tool_call.input);
|
|
341
|
+
if (
|
|
342
|
+
tool_call.function &&
|
|
343
|
+
Object.prototype.hasOwnProperty.call(tool_call.function, "arguments")
|
|
344
|
+
)
|
|
345
|
+
return normalizeWorkflowPayload(tool_call.function.arguments);
|
|
346
|
+
return normalizeWorkflowPayload();
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const buildEmptyStateText = () =>
|
|
350
|
+
[
|
|
351
|
+
"No workflow steps were generated yet.",
|
|
352
|
+
"Describe what the workflow should do (even at a high level) and run the tool again—I'll turn that into concrete steps automatically.",
|
|
353
|
+
].join("\n\n");
|
|
354
|
+
|
|
355
|
+
const buildEmptyStateHtml = () =>
|
|
356
|
+
'<div class="alert alert-info">No steps yet. Tell me what should happen in the workflow (simple or detailed) and rerun the generate_workflow tool—I will draft the steps for you.</div>';
|
|
357
|
+
|
|
358
|
+
const getStepConfigurationSchemas = () => {
|
|
359
|
+
ensureWorkflowParameters();
|
|
360
|
+
const schema = workflowSchemaCache;
|
|
361
|
+
const stepConfig =
|
|
362
|
+
schema?.properties?.workflow_steps?.items?.properties?.step_configuration;
|
|
363
|
+
return Array.isArray(stepConfig?.anyOf) ? stepConfig.anyOf : [];
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const isSimpleDescriptor = (descriptor) => {
|
|
367
|
+
if (!descriptor) return false;
|
|
368
|
+
if (Array.isArray(descriptor.enum) && descriptor.enum.length) return true;
|
|
369
|
+
const type = descriptor.type;
|
|
370
|
+
return SIMPLE_FIELD_TYPES.has(type);
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
const isSupportedActionSchema = (actionSchema) => {
|
|
374
|
+
if (!actionSchema || typeof actionSchema !== "object") return false;
|
|
375
|
+
const props = actionSchema.properties || {};
|
|
376
|
+
const typeField = props.step_type;
|
|
377
|
+
if (!typeField || !Array.isArray(typeField.enum) || !typeField.enum.length)
|
|
378
|
+
return false;
|
|
379
|
+
const requiredFields = (actionSchema.required || []).filter(
|
|
380
|
+
(field) => field !== "step_type",
|
|
381
|
+
);
|
|
382
|
+
return requiredFields.every((field) => isSimpleDescriptor(props[field]));
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
const buildValueForDescriptor = (name, descriptor, idx) => {
|
|
386
|
+
if (!descriptor) return undefined;
|
|
387
|
+
if (Array.isArray(descriptor.enum) && descriptor.enum.length)
|
|
388
|
+
return randomChoice(descriptor.enum);
|
|
389
|
+
switch (descriptor.type) {
|
|
390
|
+
case "string":
|
|
391
|
+
return descriptor.default || `${name}_${idx + 1}`;
|
|
392
|
+
case "integer":
|
|
393
|
+
case "number":
|
|
394
|
+
if (typeof descriptor.default === "number") return descriptor.default;
|
|
395
|
+
return randomInt(1, 10);
|
|
396
|
+
case "boolean":
|
|
397
|
+
return randomBool();
|
|
398
|
+
default:
|
|
399
|
+
return undefined;
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
const generateConfigFromSchema = (actionSchema, idx) => {
|
|
404
|
+
const props = actionSchema?.properties || {};
|
|
405
|
+
const stepTypeEnum = props.step_type?.enum;
|
|
406
|
+
if (!Array.isArray(stepTypeEnum) || !stepTypeEnum.length) return null;
|
|
407
|
+
const config = { step_type: stepTypeEnum[0] };
|
|
408
|
+
const requiredFields = (actionSchema.required || []).filter(
|
|
409
|
+
(field) => field !== "step_type",
|
|
410
|
+
);
|
|
411
|
+
for (const field of requiredFields) {
|
|
412
|
+
const descriptor = props[field];
|
|
413
|
+
const value = buildValueForDescriptor(field, descriptor, idx);
|
|
414
|
+
if (value === undefined) return null;
|
|
415
|
+
config[field] = value;
|
|
416
|
+
}
|
|
417
|
+
return config;
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
const buildRunJsFallbackSteps = (count = 2) => {
|
|
421
|
+
const catalog = ensureActionCatalog();
|
|
422
|
+
const hasRunJs = Boolean(catalog.byName["run_js_code"]);
|
|
423
|
+
const actionName = hasRunJs
|
|
424
|
+
? "run_js_code"
|
|
425
|
+
: Object.keys(catalog.byName)[0] || "run_js_code";
|
|
426
|
+
const steps = [];
|
|
427
|
+
for (let i = 0; i < count; i += 1) {
|
|
428
|
+
const stepName = `step_${i + 1}`;
|
|
429
|
+
const config = { step_type: actionName };
|
|
430
|
+
if (actionName === "run_js_code") {
|
|
431
|
+
config.run_where = "Server";
|
|
432
|
+
config.code = `return { auto_message_${i + 1}: "Step ${i + 1} executed" };`;
|
|
433
|
+
}
|
|
434
|
+
steps.push({
|
|
435
|
+
step_name: stepName,
|
|
436
|
+
only_if: "",
|
|
437
|
+
next_step: "",
|
|
438
|
+
step_configuration: config,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
steps.forEach((step, idx) => {
|
|
442
|
+
step.next_step = idx < steps.length - 1 ? steps[idx + 1].step_name : "";
|
|
443
|
+
});
|
|
444
|
+
return steps;
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
const buildRandomWorkflowSteps = () => {
|
|
448
|
+
const availableSchemas = getStepConfigurationSchemas().filter((schema) =>
|
|
449
|
+
isSupportedActionSchema(schema),
|
|
450
|
+
);
|
|
451
|
+
const desiredSteps = randomInt(RANDOM_STEP_COUNT.min, RANDOM_STEP_COUNT.max);
|
|
452
|
+
const steps = [];
|
|
453
|
+
let guard = desiredSteps * 3;
|
|
454
|
+
while (steps.length < desiredSteps && guard > 0) {
|
|
455
|
+
guard -= 1;
|
|
456
|
+
const schema = randomChoice(availableSchemas);
|
|
457
|
+
if (!schema) break;
|
|
458
|
+
const config = generateConfigFromSchema(schema, steps.length);
|
|
459
|
+
if (!config) continue;
|
|
460
|
+
steps.push({
|
|
461
|
+
step_name: `step_${steps.length + 1}`,
|
|
462
|
+
only_if: "",
|
|
463
|
+
next_step: "",
|
|
464
|
+
step_configuration: config,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
if (!steps.length) return buildRunJsFallbackSteps(desiredSteps);
|
|
468
|
+
steps.forEach((step, idx) => {
|
|
469
|
+
step.next_step = idx < steps.length - 1 ? steps[idx + 1].step_name : "";
|
|
470
|
+
});
|
|
471
|
+
return steps;
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
const ensureWorkflowHasSteps = (payload) => {
|
|
475
|
+
if (payload.workflow_steps.length) return payload;
|
|
476
|
+
const seeded = {
|
|
477
|
+
...payload,
|
|
478
|
+
workflow_steps: buildRandomWorkflowSteps(),
|
|
479
|
+
workflow_name: payload.workflow_name || generateWorkflowName(),
|
|
480
|
+
when_trigger: payload.when_trigger || "Never",
|
|
481
|
+
};
|
|
482
|
+
return seeded.workflow_steps.length
|
|
483
|
+
? normalizeWorkflowPayload(seeded)
|
|
484
|
+
: payload;
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
const summarizeConfigValue = (value) => {
|
|
488
|
+
if (value === null || value === undefined) return "null";
|
|
489
|
+
if (typeof value === "string") {
|
|
490
|
+
const trimmed = value.trim();
|
|
491
|
+
if (!trimmed) return '""';
|
|
492
|
+
return trimmed.length > 70 ? `${trimmed.slice(0, 67)}...` : trimmed;
|
|
493
|
+
}
|
|
494
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
495
|
+
return String(value);
|
|
496
|
+
try {
|
|
497
|
+
const json = JSON.stringify(value);
|
|
498
|
+
return json.length > 70 ? `${json.slice(0, 67)}...` : json;
|
|
499
|
+
} catch (error) {
|
|
500
|
+
console.error("GenerateWorkflowSkill: config summary failed", error);
|
|
501
|
+
return "[complex value]";
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
const describeStepSettings = (step) => {
|
|
506
|
+
const items = [];
|
|
507
|
+
if (step.only_if) items.push(`only_if=${step.only_if}`);
|
|
508
|
+
if (step.next_step) items.push(`next=${step.next_step}`);
|
|
509
|
+
return items.length ? items.join("; ") : "defaults";
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
const describeActionSettings = (config) => {
|
|
513
|
+
if (!config) return "defaults";
|
|
514
|
+
const pairs = Object.entries(config).filter(([key]) => key !== "step_type");
|
|
515
|
+
if (!pairs.length) return "defaults";
|
|
516
|
+
return pairs
|
|
517
|
+
.map(([key, value]) => `${key}=${summarizeConfigValue(value)}`)
|
|
518
|
+
.join("; ");
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
const formatStepDescription = (step, idx) => {
|
|
522
|
+
const actionCatalog = ensureActionCatalog();
|
|
523
|
+
const stepName = step.step_name || `Step ${idx + 1}`;
|
|
524
|
+
const actionName = sanitizeString(step.step_configuration?.step_type) || "?";
|
|
525
|
+
const actionMeta = actionCatalog.byName[actionName];
|
|
526
|
+
const namespaceLabel = actionMeta?.namespace || "Unknown group";
|
|
527
|
+
const extraInfo = actionMeta?.description
|
|
528
|
+
? ` – ${actionMeta.description}`
|
|
529
|
+
: "";
|
|
530
|
+
const settings = describeStepSettings(step);
|
|
531
|
+
const actionSettings = describeActionSettings(step.step_configuration);
|
|
532
|
+
return [
|
|
533
|
+
`${idx + 1}. ${stepName}`,
|
|
534
|
+
` Step settings: ${settings}`,
|
|
535
|
+
` Action: ${actionName} (${namespaceLabel})${extraInfo}`,
|
|
536
|
+
` Action settings: ${actionSettings}`,
|
|
537
|
+
].join("\n");
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
class GenerateWorkflowSkill {
|
|
541
|
+
static skill_name = "Generate Workflow";
|
|
542
|
+
|
|
543
|
+
get skill_label() {
|
|
544
|
+
return "Generate Workflow";
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
constructor(cfg) {
|
|
548
|
+
Object.assign(this, cfg);
|
|
549
|
+
ensureWorkflowParameters();
|
|
550
|
+
ensureActionCatalog();
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
async systemPrompt() {
|
|
554
|
+
return await GenerateWorkflow.system_prompt();
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
get userActions() {
|
|
558
|
+
return {
|
|
559
|
+
async apply_copilot_workflow({ user, ...raw }) {
|
|
560
|
+
const payload = ensureWorkflowHasSteps(normalizeWorkflowPayload(raw));
|
|
561
|
+
const analysis = analyzeWorkflowPayload(payload);
|
|
562
|
+
if (analysis.blocking.length)
|
|
563
|
+
return {
|
|
564
|
+
notify: `Cannot create workflow: ${analysis.blocking.join("; ")}`,
|
|
565
|
+
};
|
|
566
|
+
const result = await GenerateWorkflow.execute(payload, { user });
|
|
567
|
+
return {
|
|
568
|
+
notify:
|
|
569
|
+
result?.postExec ||
|
|
570
|
+
`Workflow created: ${payload.workflow_name || "(unnamed)"}`,
|
|
571
|
+
};
|
|
572
|
+
},
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
provideTools = () => {
|
|
577
|
+
const parameters = ensureWorkflowParameters();
|
|
578
|
+
return {
|
|
579
|
+
type: "function",
|
|
580
|
+
process: async (input) => {
|
|
581
|
+
const payload = normalizeWorkflowPayload(input);
|
|
582
|
+
const preparedPayload = ensureWorkflowHasSteps(payload);
|
|
583
|
+
const hasSteps = preparedPayload.workflow_steps.length > 0;
|
|
584
|
+
if (!hasSteps) return buildEmptyStateText();
|
|
585
|
+
const analysis = analyzeWorkflowPayload(preparedPayload);
|
|
586
|
+
const summary = describeWorkflow(preparedPayload);
|
|
587
|
+
const actionSummary = summarizeActionCatalog();
|
|
588
|
+
const sections = [
|
|
589
|
+
summary,
|
|
590
|
+
formatIssueSection("Blocking issues", analysis.blocking),
|
|
591
|
+
formatIssueSection("Warnings", analysis.warnings),
|
|
592
|
+
];
|
|
593
|
+
if (actionSummary) sections.push(`Action palette:\n${actionSummary}`);
|
|
594
|
+
return sections.join("\n\n");
|
|
595
|
+
},
|
|
596
|
+
postProcess: async ({ tool_call }) => {
|
|
597
|
+
const payload = payloadFromToolCall(tool_call);
|
|
598
|
+
const preparedPayload = ensureWorkflowHasSteps(payload);
|
|
599
|
+
const hasSteps = preparedPayload.workflow_steps.length > 0;
|
|
600
|
+
if (!hasSteps)
|
|
601
|
+
return {
|
|
602
|
+
stop: true,
|
|
603
|
+
add_response: buildEmptyStateHtml(),
|
|
604
|
+
};
|
|
605
|
+
const analysis = analyzeWorkflowPayload(preparedPayload);
|
|
606
|
+
const issuesHtml = buildIssuesHtml(analysis);
|
|
607
|
+
const previewHtml = renderWorkflowPreview(preparedPayload);
|
|
608
|
+
const canCreate =
|
|
609
|
+
analysis.blocking.length === 0 &&
|
|
610
|
+
preparedPayload.workflow_steps.length > 0;
|
|
611
|
+
return {
|
|
612
|
+
stop: true,
|
|
613
|
+
add_response: `${issuesHtml}${previewHtml}`,
|
|
614
|
+
add_user_action: canCreate
|
|
615
|
+
? {
|
|
616
|
+
name: "apply_copilot_workflow",
|
|
617
|
+
type: "button",
|
|
618
|
+
label: `Create workflow ${
|
|
619
|
+
preparedPayload.workflow_name || "(unnamed)"
|
|
620
|
+
}`,
|
|
621
|
+
input: preparedPayload,
|
|
622
|
+
}
|
|
623
|
+
: undefined,
|
|
624
|
+
};
|
|
625
|
+
},
|
|
626
|
+
function: {
|
|
627
|
+
name: GenerateWorkflow.function_name,
|
|
628
|
+
description: GenerateWorkflow.description,
|
|
629
|
+
parameters,
|
|
630
|
+
},
|
|
631
|
+
};
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
ensureWorkflowParameters();
|
|
636
|
+
ensureActionCatalog();
|
|
637
|
+
|
|
638
|
+
module.exports = GenerateWorkflowSkill;
|
package/builder-gen.js
CHANGED
|
@@ -6,15 +6,26 @@ const { getActionConfigFields } = require("@saltcorn/data/plugin-helper");
|
|
|
6
6
|
|
|
7
7
|
module.exports = {
|
|
8
8
|
run: async (prompt, mode, table) => {
|
|
9
|
-
await
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
const str = await getState().functions.llm_generate.run(
|
|
10
|
+
`Generate an HTML snippet according to the requirement below. Your snippet will be
|
|
11
|
+
placed inside a page which has loaded the Bootstrap 5 CSS framework, so you can use any
|
|
12
|
+
Bootstrap 5 classes.
|
|
13
|
+
|
|
14
|
+
If you need to run javascript in script tag that depends on external reosurces, you wrap this
|
|
15
|
+
in a DOMContentLoaded event handler as external javascript resources may be loaded after your HTML snippet is included.
|
|
16
|
+
|
|
17
|
+
Include only the HTML snippet with no explanation before or after the code snippet.
|
|
18
|
+
|
|
19
|
+
Generate the HTML5 snippet for this request: ${prompt}
|
|
20
|
+
`,
|
|
21
|
+
);
|
|
22
|
+
const strHtml = str.includes("```html")
|
|
23
|
+
? str.split("```html")[1].split("```")[0]
|
|
24
|
+
: str;
|
|
12
25
|
return {
|
|
13
26
|
type: "blank",
|
|
14
27
|
isHTML: true,
|
|
15
|
-
contents:
|
|
16
|
-
<blockquote class="blockquote"><p>${prompt}</p></blockquote>
|
|
17
|
-
<pre>mode=${mode} table=${table}</pre>`,
|
|
28
|
+
contents: strHtml,
|
|
18
29
|
text_strings: [],
|
|
19
30
|
};
|
|
20
31
|
},
|
package/chat-copilot.js
CHANGED
|
@@ -121,7 +121,6 @@ const runLegacy = async (table_id, viewname, cfg, state, { res, req }) => {
|
|
|
121
121
|
r.context.interactions &&
|
|
122
122
|
(r.context.copilot === "_system" || !r.context.copilot)
|
|
123
123
|
);
|
|
124
|
-
console.log({prevRuns})
|
|
125
124
|
const cfgMsg = incompleteCfgMsg();
|
|
126
125
|
if (cfgMsg) return cfgMsg;
|
|
127
126
|
let runInteractions = "";
|
|
@@ -370,7 +369,6 @@ const run = async (table_id, viewname, cfg, state, extra) => {
|
|
|
370
369
|
configuration: agentCfg,
|
|
371
370
|
min_role: 100,
|
|
372
371
|
});
|
|
373
|
-
console.log({agentView})
|
|
374
372
|
return await agentView.run(state, extra);
|
|
375
373
|
};
|
|
376
374
|
|
package/copilot-as-agent.js
CHANGED
|
@@ -32,25 +32,29 @@ const { getState } = require("@saltcorn/data/db/state");
|
|
|
32
32
|
const get_state_fields = () => [];
|
|
33
33
|
|
|
34
34
|
const sys_prompt = ``;
|
|
35
|
+
const viewname = "Saltcorn Agent copilot";
|
|
35
36
|
|
|
36
37
|
const get_agent_view = () => {
|
|
37
38
|
const agent_action = new Trigger({
|
|
38
39
|
action: "Agent",
|
|
39
40
|
when_trigger: "Never",
|
|
40
41
|
configuration: {
|
|
42
|
+
viewname,
|
|
41
43
|
sys_prompt,
|
|
42
44
|
skills: [
|
|
43
45
|
{ skill_type: "Generate Page" },
|
|
44
46
|
{ skill_type: "Database design" },
|
|
47
|
+
{ skill_type: "Generate Workflow" },
|
|
45
48
|
],
|
|
46
49
|
},
|
|
47
50
|
});
|
|
48
51
|
return new View({
|
|
49
52
|
viewtemplate: "Agent Chat",
|
|
50
|
-
name:
|
|
53
|
+
name: viewname,
|
|
51
54
|
min_role: 1,
|
|
52
55
|
configuration: {
|
|
53
56
|
agent_action,
|
|
57
|
+
viewname,
|
|
54
58
|
},
|
|
55
59
|
});
|
|
56
60
|
};
|
|
@@ -64,12 +68,23 @@ const interact = async (table_id, viewname, config, body, reqres) => {
|
|
|
64
68
|
return await view.runRoute("interact", body, reqres.res, reqres);
|
|
65
69
|
};
|
|
66
70
|
|
|
71
|
+
const execute_user_action = async (
|
|
72
|
+
table_id,
|
|
73
|
+
viewname,
|
|
74
|
+
config,
|
|
75
|
+
body,
|
|
76
|
+
reqres,
|
|
77
|
+
) => {
|
|
78
|
+
const view = get_agent_view();
|
|
79
|
+
return await view.runRoute("execute_user_action", body, reqres.res, reqres);
|
|
80
|
+
};
|
|
81
|
+
|
|
67
82
|
module.exports = {
|
|
68
|
-
name:
|
|
83
|
+
name: viewname,
|
|
69
84
|
display_state_form: false,
|
|
70
85
|
get_state_fields,
|
|
71
86
|
tableless: true,
|
|
72
87
|
singleton: true,
|
|
73
88
|
run,
|
|
74
|
-
routes: { interact },
|
|
89
|
+
routes: { interact, execute_user_action },
|
|
75
90
|
};
|
package/index.js
CHANGED
package/package.json
CHANGED
package/page-gen-action.js
CHANGED
|
@@ -126,10 +126,6 @@ const upsertHtmlPreviewPage = async (name, html, title, description, user) => {
|
|
|
126
126
|
module.exports = {
|
|
127
127
|
description: "Generate page with AI copilot",
|
|
128
128
|
configFields: ({ table, mode }) => {
|
|
129
|
-
console.log({
|
|
130
|
-
table,
|
|
131
|
-
mode,
|
|
132
|
-
}, "OOOOOOOOOO");
|
|
133
129
|
if (mode === "workflow") {
|
|
134
130
|
return [
|
|
135
131
|
{
|
|
@@ -210,7 +206,6 @@ module.exports = {
|
|
|
210
206
|
},
|
|
211
207
|
];
|
|
212
208
|
} else if (table) {
|
|
213
|
-
console.log(JSON.stringify(table, null, 2), "TABLE");
|
|
214
209
|
const textFields = table.fields
|
|
215
210
|
.filter((f) => f.type?.sql_name === "text")
|
|
216
211
|
.map((f) => f.name);
|