@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/page-gen-action.js
CHANGED
|
@@ -7,6 +7,122 @@ const Page = require("@saltcorn/data/models/page");
|
|
|
7
7
|
const GeneratePage = require("./actions/generate-page");
|
|
8
8
|
const { parseHTML } = require("./common");
|
|
9
9
|
|
|
10
|
+
const numericIdRE = /^\d+$/;
|
|
11
|
+
|
|
12
|
+
const arrayify = (value) => {
|
|
13
|
+
if (value === null || typeof value === "undefined") return [];
|
|
14
|
+
return Array.isArray(value) ? value : [value];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const normalizeName = (value) =>
|
|
18
|
+
typeof value === "string" ? value.trim() : value || "";
|
|
19
|
+
|
|
20
|
+
const findFileSafe = async (identifier) => {
|
|
21
|
+
if (identifier === null || typeof identifier === "undefined") return null;
|
|
22
|
+
if (
|
|
23
|
+
typeof identifier === "object" &&
|
|
24
|
+
identifier.mimetype &&
|
|
25
|
+
typeof identifier.get_contents === "function"
|
|
26
|
+
)
|
|
27
|
+
return identifier;
|
|
28
|
+
const attempts = [];
|
|
29
|
+
if (typeof identifier === "object" && identifier.id)
|
|
30
|
+
attempts.push({ id: identifier.id });
|
|
31
|
+
if (typeof identifier === "object" && identifier.path_to_serve)
|
|
32
|
+
attempts.push(identifier.path_to_serve);
|
|
33
|
+
if (typeof identifier === "number") attempts.push({ id: identifier });
|
|
34
|
+
if (typeof identifier === "string") {
|
|
35
|
+
const trimmed = identifier.trim();
|
|
36
|
+
if (trimmed) {
|
|
37
|
+
attempts.push(trimmed);
|
|
38
|
+
if (numericIdRE.test(trimmed)) attempts.push({ id: +trimmed });
|
|
39
|
+
attempts.push({ name: trimmed });
|
|
40
|
+
attempts.push({ filename: trimmed });
|
|
41
|
+
attempts.push({ path_to_serve: trimmed });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
for (const attempt of attempts) {
|
|
45
|
+
try {
|
|
46
|
+
const file = await File.findOne(attempt);
|
|
47
|
+
if (file) return file;
|
|
48
|
+
} catch (e) {
|
|
49
|
+
// ignore lookup errors
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const valueToDataUrl = async (value) => {
|
|
56
|
+
if (value === null || typeof value === "undefined") return null;
|
|
57
|
+
if (typeof value === "string" && value.trim().startsWith("data:"))
|
|
58
|
+
return value.trim();
|
|
59
|
+
const file = await findFileSafe(value);
|
|
60
|
+
if (!file) return null;
|
|
61
|
+
const base64 = await file.get_contents("base64");
|
|
62
|
+
return `data:${file.mimetype};base64,${base64}`;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const gatherImageDataFromValue = async (value) => {
|
|
66
|
+
const urls = [];
|
|
67
|
+
for (const entry of arrayify(value)) {
|
|
68
|
+
const url = await valueToDataUrl(entry);
|
|
69
|
+
if (url) urls.push(url);
|
|
70
|
+
}
|
|
71
|
+
return urls;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const gatherImagesFromExpression = async (expression, row, user, label) => {
|
|
75
|
+
if (!expression) return [];
|
|
76
|
+
const resolved = eval_expression(expression, row, user, label);
|
|
77
|
+
return await gatherImageDataFromValue(resolved);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const buildImageMessages = (dataUrls, label) =>
|
|
81
|
+
(dataUrls || []).map((image) => ({
|
|
82
|
+
role: "user",
|
|
83
|
+
content: [
|
|
84
|
+
...(label ? [{ type: "text", text: label }] : []),
|
|
85
|
+
{ type: "image", image },
|
|
86
|
+
],
|
|
87
|
+
}));
|
|
88
|
+
|
|
89
|
+
const loadExistingPageAssets = async (pageName) => {
|
|
90
|
+
if (!pageName) return { page: null, html: null };
|
|
91
|
+
const page = await Page.findOne({ name: pageName });
|
|
92
|
+
if (!page) return { page: null, html: null };
|
|
93
|
+
let html = null;
|
|
94
|
+
if (page.layout?.html_file) {
|
|
95
|
+
const file = await findFileSafe(page.layout.html_file);
|
|
96
|
+
if (file) html = await file.get_contents("utf8");
|
|
97
|
+
}
|
|
98
|
+
return { page, html };
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const refreshPagesSoon = () =>
|
|
102
|
+
setTimeout(() => getState().refresh_pages(), 200);
|
|
103
|
+
|
|
104
|
+
const upsertHtmlPreviewPage = async (name, html, title, description, user) => {
|
|
105
|
+
const file = await File.from_contents(
|
|
106
|
+
`${name}.html`,
|
|
107
|
+
"text/html",
|
|
108
|
+
html,
|
|
109
|
+
user.id,
|
|
110
|
+
100,
|
|
111
|
+
);
|
|
112
|
+
const layout = { html_file: file.path_to_serve };
|
|
113
|
+
const existing = await Page.findOne({ name });
|
|
114
|
+
if (existing) await existing.update({ title, description, layout });
|
|
115
|
+
else
|
|
116
|
+
await Page.create({
|
|
117
|
+
name,
|
|
118
|
+
title,
|
|
119
|
+
description,
|
|
120
|
+
min_role: 100,
|
|
121
|
+
layout,
|
|
122
|
+
});
|
|
123
|
+
refreshPagesSoon();
|
|
124
|
+
};
|
|
125
|
+
|
|
10
126
|
module.exports = {
|
|
11
127
|
description: "Generate page with AI copilot",
|
|
12
128
|
configFields: ({ table, mode }) => {
|
|
@@ -36,6 +152,30 @@ module.exports = {
|
|
|
36
152
|
class: "validate-expression",
|
|
37
153
|
type: "String",
|
|
38
154
|
},
|
|
155
|
+
{
|
|
156
|
+
name: "design_image_expression",
|
|
157
|
+
label: "Design image expression",
|
|
158
|
+
sublabel:
|
|
159
|
+
"Optional expression returning a file, file id, data URL, or array with reference design images to guide generation",
|
|
160
|
+
class: "validate-expression",
|
|
161
|
+
type: "String",
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: "feedback_image_expression",
|
|
165
|
+
label: "Feedback image expression",
|
|
166
|
+
sublabel:
|
|
167
|
+
"Optional expression returning a file, file id, data URL, or array with annotated feedback screenshots",
|
|
168
|
+
class: "validate-expression",
|
|
169
|
+
type: "String",
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
name: "existing_page_name",
|
|
173
|
+
label: "Existing page name",
|
|
174
|
+
sublabel:
|
|
175
|
+
"Optional expression evaluating to the name of the page to use as a baseline. If provided, its HTML will be included automatically",
|
|
176
|
+
class: "validate-expression",
|
|
177
|
+
type: "String",
|
|
178
|
+
},
|
|
39
179
|
{
|
|
40
180
|
name: "existing_page_html",
|
|
41
181
|
label: "Existing page HTML",
|
|
@@ -69,6 +209,9 @@ module.exports = {
|
|
|
69
209
|
const textFields = table.fields
|
|
70
210
|
.filter((f) => f.type?.sql_name === "text")
|
|
71
211
|
.map((f) => f.name);
|
|
212
|
+
const fileFields = table.fields
|
|
213
|
+
.filter((f) => ["File", "Image"].includes(f.type?.name))
|
|
214
|
+
.map((f) => f.name);
|
|
72
215
|
|
|
73
216
|
return [
|
|
74
217
|
{
|
|
@@ -93,6 +236,30 @@ module.exports = {
|
|
|
93
236
|
required: true,
|
|
94
237
|
attributes: { options: textFields },
|
|
95
238
|
},
|
|
239
|
+
{
|
|
240
|
+
name: "existing_page_field",
|
|
241
|
+
label: "Existing page name field",
|
|
242
|
+
sublabel:
|
|
243
|
+
"Optional text field storing the name of the page to update",
|
|
244
|
+
type: "String",
|
|
245
|
+
attributes: { options: textFields },
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
name: "design_image_field",
|
|
249
|
+
label: "Design image field",
|
|
250
|
+
sublabel:
|
|
251
|
+
"Optional file/image field containing a reference design supplied by the user",
|
|
252
|
+
type: "String",
|
|
253
|
+
attributes: { options: fileFields },
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
name: "feedback_image_field",
|
|
257
|
+
label: "Feedback image field",
|
|
258
|
+
sublabel:
|
|
259
|
+
"Optional file/image field with annotated feedback screenshots to consider during updates",
|
|
260
|
+
type: "String",
|
|
261
|
+
attributes: { options: fileFields },
|
|
262
|
+
},
|
|
96
263
|
// ...override_fields,
|
|
97
264
|
];
|
|
98
265
|
}
|
|
@@ -109,6 +276,12 @@ module.exports = {
|
|
|
109
276
|
prompt_template,
|
|
110
277
|
answer_field,
|
|
111
278
|
image_prompt,
|
|
279
|
+
design_image_expression,
|
|
280
|
+
feedback_image_expression,
|
|
281
|
+
design_image_field,
|
|
282
|
+
feedback_image_field,
|
|
283
|
+
existing_page_field,
|
|
284
|
+
existing_page_name,
|
|
112
285
|
existing_page_html,
|
|
113
286
|
chat_history_field,
|
|
114
287
|
convert_to_saltcorn,
|
|
@@ -117,7 +290,7 @@ module.exports = {
|
|
|
117
290
|
}) => {
|
|
118
291
|
let prompt;
|
|
119
292
|
if (mode === "workflow") prompt = interpolate(prompt_template, row, user);
|
|
120
|
-
else if (prompt_field === "Formula"
|
|
293
|
+
else if (prompt_field === "Formula")
|
|
121
294
|
prompt = eval_expression(
|
|
122
295
|
prompt_formula,
|
|
123
296
|
row,
|
|
@@ -125,61 +298,104 @@ module.exports = {
|
|
|
125
298
|
"copilot_generate_page prompt formula",
|
|
126
299
|
);
|
|
127
300
|
else prompt = row[prompt_field];
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
301
|
+
|
|
302
|
+
const resolvedExistingName = normalizeName(
|
|
303
|
+
mode === "workflow"
|
|
304
|
+
? existing_page_name
|
|
305
|
+
? eval_expression(
|
|
306
|
+
existing_page_name,
|
|
307
|
+
row,
|
|
308
|
+
user,
|
|
309
|
+
"copilot_generate_page existing page name",
|
|
310
|
+
)
|
|
311
|
+
: null
|
|
312
|
+
: existing_page_field
|
|
313
|
+
? row?.[existing_page_field]
|
|
314
|
+
: null,
|
|
315
|
+
);
|
|
316
|
+
const existingAssets = resolvedExistingName
|
|
317
|
+
? await loadExistingPageAssets(resolvedExistingName)
|
|
318
|
+
: { page: null, html: null };
|
|
319
|
+
|
|
320
|
+
let manualExistingHtml;
|
|
321
|
+
if (existing_page_html) {
|
|
322
|
+
manualExistingHtml = eval_expression(
|
|
131
323
|
existing_page_html,
|
|
132
324
|
row,
|
|
133
325
|
user,
|
|
134
326
|
"copilot_generate_page existing page html",
|
|
135
327
|
);
|
|
136
|
-
prompt = "This is the HTML code for the existing pages you should edit:\n\n```html\n"+exist_html+"\n```\n\n"+prompt
|
|
137
328
|
}
|
|
138
|
-
|
|
139
|
-
|
|
329
|
+
const existingHtmlForPrompt = manualExistingHtml || existingAssets.html;
|
|
330
|
+
if (existingHtmlForPrompt) {
|
|
331
|
+
const label = resolvedExistingName ? ` ${resolvedExistingName}` : "";
|
|
332
|
+
prompt = `This is the HTML code for the existing page${label} you should edit:\n\n\`\`\`html\n${existingHtmlForPrompt}\n\`\`\`\n\n${prompt}`;
|
|
333
|
+
}
|
|
140
334
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
tools.push({
|
|
145
|
-
type: "function",
|
|
146
|
-
function: {
|
|
147
|
-
name: GeneratePage.function_name,
|
|
148
|
-
description: GeneratePage.description,
|
|
149
|
-
parameters: await GeneratePage.json_schema(),
|
|
150
|
-
},
|
|
151
|
-
});
|
|
152
|
-
const { llm_generate } = getState().functions;
|
|
153
|
-
let chat;
|
|
154
|
-
if (image_prompt) {
|
|
155
|
-
const from_ctx = eval_expression(
|
|
335
|
+
const chatMessages = [];
|
|
336
|
+
if (mode === "workflow") {
|
|
337
|
+
const referenceImages = await gatherImagesFromExpression(
|
|
156
338
|
image_prompt,
|
|
157
339
|
row,
|
|
158
340
|
user,
|
|
159
341
|
"copilot_generate_page image prompt",
|
|
160
342
|
);
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
343
|
+
chatMessages.push(
|
|
344
|
+
...buildImageMessages(referenceImages, "Reference image"),
|
|
345
|
+
);
|
|
346
|
+
const designImages = await gatherImagesFromExpression(
|
|
347
|
+
design_image_expression,
|
|
348
|
+
row,
|
|
349
|
+
user,
|
|
350
|
+
"copilot_generate_page design image",
|
|
351
|
+
);
|
|
352
|
+
chatMessages.push(
|
|
353
|
+
...buildImageMessages(designImages, "Design reference"),
|
|
354
|
+
);
|
|
355
|
+
const feedbackImages = await gatherImagesFromExpression(
|
|
356
|
+
feedback_image_expression,
|
|
357
|
+
row,
|
|
358
|
+
user,
|
|
359
|
+
"copilot_generate_page feedback image",
|
|
360
|
+
);
|
|
361
|
+
chatMessages.push(
|
|
362
|
+
...buildImageMessages(feedbackImages, "Feedback reference"),
|
|
363
|
+
);
|
|
364
|
+
} else if (row) {
|
|
365
|
+
chatMessages.push(
|
|
366
|
+
...buildImageMessages(
|
|
367
|
+
await gatherImageDataFromValue(row[design_image_field]),
|
|
368
|
+
"Design reference",
|
|
369
|
+
),
|
|
370
|
+
);
|
|
371
|
+
chatMessages.push(
|
|
372
|
+
...buildImageMessages(
|
|
373
|
+
await gatherImageDataFromValue(row[feedback_image_field]),
|
|
374
|
+
"Feedback reference",
|
|
375
|
+
),
|
|
376
|
+
);
|
|
177
377
|
}
|
|
178
|
-
const
|
|
378
|
+
const chat = chatMessages.length ? chatMessages : undefined;
|
|
379
|
+
|
|
380
|
+
const systemPrompt = await GeneratePage.system_prompt();
|
|
381
|
+
const tools = [
|
|
382
|
+
{
|
|
383
|
+
type: "function",
|
|
384
|
+
function: {
|
|
385
|
+
name: GeneratePage.function_name,
|
|
386
|
+
description: GeneratePage.description,
|
|
387
|
+
parameters: await GeneratePage.json_schema(),
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
];
|
|
391
|
+
const { llm_generate } = getState().functions;
|
|
392
|
+
const llmOptions = {
|
|
179
393
|
tools,
|
|
180
394
|
chat,
|
|
181
395
|
systemPrompt,
|
|
182
|
-
|
|
396
|
+
...(model ? { model } : {}),
|
|
397
|
+
};
|
|
398
|
+
const initial_ans = await llm_generate.run(prompt, llmOptions);
|
|
183
399
|
const initial_info =
|
|
184
400
|
initial_ans.tool_calls[0].input ||
|
|
185
401
|
JSON.parse(initial_ans.tool_calls[0].function.arguments);
|
|
@@ -195,55 +411,55 @@ module.exports = {
|
|
|
195
411
|
and
|
|
196
412
|
|
|
197
413
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>`;
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
414
|
+
const generationPrompt = `${prompt}
|
|
415
|
+
|
|
416
|
+
The page title is: ${initial_info.title}.
|
|
417
|
+
Further page description: ${initial_info.description}.
|
|
418
|
+
|
|
419
|
+
Generate the HTML for the web page using the Bootstrap 5 CSS framework.
|
|
420
|
+
|
|
421
|
+
${prompt_part_2}
|
|
422
|
+
|
|
423
|
+
Just generate HTML code, do not wrap in markdown code tags`;
|
|
424
|
+
const page_html = await llm_generate.run(generationPrompt, {
|
|
425
|
+
debugResult: true,
|
|
426
|
+
chat,
|
|
427
|
+
...(model ? { model } : {}),
|
|
428
|
+
response_format: full.response_schema
|
|
429
|
+
? {
|
|
430
|
+
type: "json_schema",
|
|
431
|
+
json_schema: {
|
|
432
|
+
name: "generate_page",
|
|
433
|
+
schema: full.response_schema,
|
|
434
|
+
},
|
|
435
|
+
}
|
|
436
|
+
: undefined,
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
const requestedName = page_name ? interpolate(page_name, row, user) : "";
|
|
440
|
+
const targetPageName = normalizeName(
|
|
441
|
+
requestedName || resolvedExistingName || "",
|
|
222
442
|
);
|
|
443
|
+
const updatingExisting =
|
|
444
|
+
targetPageName &&
|
|
445
|
+
resolvedExistingName &&
|
|
446
|
+
targetPageName === resolvedExistingName &&
|
|
447
|
+
existingAssets.page;
|
|
223
448
|
|
|
224
|
-
|
|
225
|
-
if (use_page_name) {
|
|
449
|
+
if (targetPageName) {
|
|
226
450
|
let layout;
|
|
227
451
|
if (convert_to_saltcorn) {
|
|
228
452
|
layout = parseHTML(page_html, true);
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
`${use_page_name}.html`,
|
|
232
|
-
"text/html",
|
|
453
|
+
await upsertHtmlPreviewPage(
|
|
454
|
+
`${targetPageName}_html`,
|
|
233
455
|
wrapExample(page_html),
|
|
234
|
-
|
|
235
|
-
|
|
456
|
+
initial_info.title,
|
|
457
|
+
initial_info.description,
|
|
458
|
+
user,
|
|
236
459
|
);
|
|
237
|
-
await Page.create({
|
|
238
|
-
name: use_page_name + "_html",
|
|
239
|
-
title: initial_info.title,
|
|
240
|
-
description: initial_info.description,
|
|
241
|
-
min_role: 100,
|
|
242
|
-
layout: { html_file: file.path_to_serve },
|
|
243
|
-
});
|
|
244
460
|
} else {
|
|
245
461
|
const file = await File.from_contents(
|
|
246
|
-
`${
|
|
462
|
+
`${targetPageName}.html`,
|
|
247
463
|
"text/html",
|
|
248
464
|
page_html,
|
|
249
465
|
user.id,
|
|
@@ -251,18 +467,26 @@ module.exports = {
|
|
|
251
467
|
);
|
|
252
468
|
layout = { html_file: file.path_to_serve };
|
|
253
469
|
}
|
|
254
|
-
//save to a file
|
|
255
470
|
|
|
256
|
-
|
|
257
|
-
await Page.create({
|
|
258
|
-
name: use_page_name,
|
|
471
|
+
const pagePayload = {
|
|
259
472
|
title: initial_info.title,
|
|
260
473
|
description: initial_info.description,
|
|
261
|
-
min_role: 100,
|
|
262
474
|
layout,
|
|
263
|
-
}
|
|
264
|
-
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
if (updatingExisting) {
|
|
478
|
+
await existingAssets.page.update(pagePayload);
|
|
479
|
+
refreshPagesSoon();
|
|
480
|
+
} else {
|
|
481
|
+
await Page.create({
|
|
482
|
+
name: targetPageName,
|
|
483
|
+
min_role: 100,
|
|
484
|
+
...pagePayload,
|
|
485
|
+
});
|
|
486
|
+
refreshPagesSoon();
|
|
487
|
+
}
|
|
265
488
|
}
|
|
489
|
+
|
|
266
490
|
const upd = answer_field ? { [answer_field]: page_html } : {};
|
|
267
491
|
if (mode === "workflow") return upd;
|
|
268
492
|
else if (answer_field) await table.updateRow(upd, row[table.pk_name]);
|