@saltcorn/copilot 0.7.2 → 0.7.4
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 -11
- package/agent-skills/viewgen.js +198 -0
- package/agent-skills/workflow.js +638 -0
- package/builder-gen.js +17 -6
- package/chat-copilot.js +0 -2
- package/copilot-as-agent.js +19 -3
- package/index.js +2 -0
- package/package.json +1 -1
- package/page-gen-action.js +0 -5
|
@@ -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
|
|