@saltcorn/copilot 0.7.0 → 0.7.2
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 +7 -2
- package/agent-skills/database-design.js +185 -0
- package/agent-skills/pagegen.js +51 -5
- package/builder-gen.js +28 -0
- package/chat-copilot.js +176 -4
- package/copilot-as-agent.js +75 -0
- package/index.js +14 -4
- package/package.json +1 -1
- package/page-gen-action.js +316 -87
package/actions/generate-page.js
CHANGED
|
@@ -28,7 +28,7 @@ class GeneratePage {
|
|
|
28
28
|
let namedescription = `The name of the page, this should be a short name which is part of the url. `;
|
|
29
29
|
if (allPageNames.length) {
|
|
30
30
|
namedescription += `These are the names of the exising pages: ${allPageNames.join(
|
|
31
|
-
", "
|
|
31
|
+
", ",
|
|
32
32
|
)}. Do not pick a name that is identical but follow the same naming convention.`;
|
|
33
33
|
}
|
|
34
34
|
const roles = await User.get_roles();
|
|
@@ -65,7 +65,7 @@ class GeneratePage {
|
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
67
|
static async system_prompt() {
|
|
68
|
-
return `
|
|
68
|
+
return `If the user asks to generate a page, a web page or a landing page, use the generate_page to generate a page.`;
|
|
69
69
|
}
|
|
70
70
|
static async follow_on_generate({ name, page_type }) {
|
|
71
71
|
if (page_type === "Marketing page") {
|
|
@@ -272,7 +272,7 @@ class GeneratePage {
|
|
|
272
272
|
}
|
|
273
273
|
if (segment.type === "container") {
|
|
274
274
|
const { customStyle, style, display, overflow } = splitContainerStyle(
|
|
275
|
-
segment.style
|
|
275
|
+
segment.style,
|
|
276
276
|
);
|
|
277
277
|
return {
|
|
278
278
|
...segment,
|
|
@@ -310,17 +310,17 @@ class GeneratePage {
|
|
|
310
310
|
JSON.stringify(
|
|
311
311
|
GeneratePage.walk_response(JSON.parse(contents)),
|
|
312
312
|
null,
|
|
313
|
-
2
|
|
314
|
-
)
|
|
315
|
-
)
|
|
316
|
-
)
|
|
313
|
+
2,
|
|
314
|
+
),
|
|
315
|
+
),
|
|
316
|
+
),
|
|
317
317
|
)
|
|
318
318
|
);
|
|
319
319
|
}
|
|
320
320
|
static async execute(
|
|
321
321
|
{ name, title, description, min_role, page_type },
|
|
322
322
|
req,
|
|
323
|
-
contents
|
|
323
|
+
contents,
|
|
324
324
|
) {
|
|
325
325
|
console.log("execute", name, contents);
|
|
326
326
|
const roles = await User.get_roles();
|
|
@@ -341,12 +341,12 @@ class GeneratePage {
|
|
|
341
341
|
"Page created. " +
|
|
342
342
|
a(
|
|
343
343
|
{ target: "_blank", href: `/page/${name}`, class: "me-1" },
|
|
344
|
-
"Go to page"
|
|
344
|
+
"Go to page",
|
|
345
345
|
) +
|
|
346
346
|
" | " +
|
|
347
347
|
a(
|
|
348
348
|
{ target: "_blank", href: `/pageedit/edit/${name}`, class: "ms-1" },
|
|
349
|
-
"Configure page"
|
|
349
|
+
"Configure page",
|
|
350
350
|
),
|
|
351
351
|
};
|
|
352
352
|
}
|
|
@@ -13,7 +13,7 @@ class GenerateTables {
|
|
|
13
13
|
static function_name = "generate_tables";
|
|
14
14
|
static description = "Generate database tables";
|
|
15
15
|
|
|
16
|
-
static
|
|
16
|
+
static json_schema() {
|
|
17
17
|
const types = Object.values(getState().types);
|
|
18
18
|
const fieldTypeCfg = types.map((ty) => {
|
|
19
19
|
const properties = {
|
|
@@ -175,9 +175,14 @@ class GenerateTables {
|
|
|
175
175
|
|
|
176
176
|
static process_tables(tables) {
|
|
177
177
|
return tables.map((table) => {
|
|
178
|
+
const sanitizedFields = Array.isArray(table.fields)
|
|
179
|
+
? table.fields.filter(
|
|
180
|
+
(f) => (f?.name || "").toLowerCase() !== "id"
|
|
181
|
+
)
|
|
182
|
+
: [];
|
|
178
183
|
return new Table({
|
|
179
184
|
name: table.table_name,
|
|
180
|
-
fields:
|
|
185
|
+
fields: sanitizedFields.map((f) => {
|
|
181
186
|
const { data_type, reference_table, ...attributes } =
|
|
182
187
|
f.type_and_configuration;
|
|
183
188
|
let type = data_type;
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
const GenerateTables = require("../actions/generate-tables");
|
|
2
|
+
|
|
3
|
+
const normalizeTablesPayload = (rawPayload) => {
|
|
4
|
+
if (!rawPayload) return { tables: [] };
|
|
5
|
+
let payload = rawPayload;
|
|
6
|
+
if (typeof payload === "string") {
|
|
7
|
+
try {
|
|
8
|
+
payload = JSON.parse(payload);
|
|
9
|
+
} catch (e) {
|
|
10
|
+
console.error("Failed to parse generate_tables payload", e);
|
|
11
|
+
return { tables: [] };
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
if (typeof payload !== "object") return { tables: [] };
|
|
15
|
+
const normalized = { ...payload };
|
|
16
|
+
normalized.tables = Array.isArray(normalized.tables)
|
|
17
|
+
? normalized.tables.filter(Boolean).map((table) => ({
|
|
18
|
+
...table,
|
|
19
|
+
fields: Array.isArray(table?.fields)
|
|
20
|
+
? table.fields.filter(Boolean).map((field) => ({
|
|
21
|
+
...field,
|
|
22
|
+
type_and_configuration: field?.type_and_configuration || {},
|
|
23
|
+
}))
|
|
24
|
+
: [],
|
|
25
|
+
}))
|
|
26
|
+
: [];
|
|
27
|
+
return normalized;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const summarizeTables = (tables) =>
|
|
31
|
+
tables.map((table, idx) => {
|
|
32
|
+
const fields = (table.fields || []).slice(0, 5);
|
|
33
|
+
const fieldSummary = fields
|
|
34
|
+
.map((field) => {
|
|
35
|
+
const fname = field?.name || "(missing name)";
|
|
36
|
+
const ftype = field?.type_and_configuration?.data_type || "Unknown";
|
|
37
|
+
return `${fname}:${ftype}`;
|
|
38
|
+
})
|
|
39
|
+
.join(", ");
|
|
40
|
+
const ellipsis = table.fields.length > fields.length ? "..." : "";
|
|
41
|
+
return `${idx + 1}. ${table.table_name || "(missing name)"} – ${
|
|
42
|
+
table.fields.length
|
|
43
|
+
} field(s)${fieldSummary ? ` (${fieldSummary}${ellipsis})` : ""}`;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const collectTableWarnings = (tables) => {
|
|
47
|
+
const warnings = [];
|
|
48
|
+
const seenTables = new Set();
|
|
49
|
+
tables.forEach((table, tableIdx) => {
|
|
50
|
+
const tableLabel = table.table_name || `Table #${tableIdx + 1}`;
|
|
51
|
+
if (!table.table_name)
|
|
52
|
+
warnings.push(`${tableLabel} is missing a table_name.`);
|
|
53
|
+
else if (seenTables.has(table.table_name))
|
|
54
|
+
warnings.push(`Duplicate table name "${table.table_name}".`);
|
|
55
|
+
else seenTables.add(table.table_name);
|
|
56
|
+
|
|
57
|
+
if (!Array.isArray(table.fields) || table.fields.length === 0)
|
|
58
|
+
warnings.push(`${tableLabel} does not define any fields.`);
|
|
59
|
+
|
|
60
|
+
const fieldNames = new Set();
|
|
61
|
+
(table.fields || []).forEach((field, fieldIdx) => {
|
|
62
|
+
const fieldLabel = field?.name || `Field #${fieldIdx + 1}`;
|
|
63
|
+
if (!field?.name)
|
|
64
|
+
warnings.push(`${tableLabel} has a field without a name.`);
|
|
65
|
+
else if (fieldNames.has(field.name))
|
|
66
|
+
warnings.push(`${tableLabel} repeats the field name "${field.name}".`);
|
|
67
|
+
else fieldNames.add(field.name);
|
|
68
|
+
|
|
69
|
+
if (!field?.type_and_configuration?.data_type)
|
|
70
|
+
warnings.push(
|
|
71
|
+
`${tableLabel}.${fieldLabel} must include type_and_configuration.data_type.`
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
if ((field?.name || "").toLowerCase() === "id")
|
|
75
|
+
warnings.push(
|
|
76
|
+
`${tableLabel}.${fieldLabel} should be omitted because every table already has an auto-increment id.`
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
return warnings;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const payloadFromToolCall = (tool_call) => {
|
|
84
|
+
if (!tool_call) return { tables: [] };
|
|
85
|
+
if (tool_call.input) return normalizeTablesPayload(tool_call.input);
|
|
86
|
+
if (tool_call.function?.arguments)
|
|
87
|
+
return normalizeTablesPayload(tool_call.function.arguments);
|
|
88
|
+
return { tables: [] };
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
class GenerateTablesSkill {
|
|
92
|
+
static skill_name = "Database design";
|
|
93
|
+
|
|
94
|
+
get skill_label() {
|
|
95
|
+
return "Database Design";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
constructor(cfg) {
|
|
99
|
+
console.log("GenerateTablesSkill.constructor called", { cfg });
|
|
100
|
+
Object.assign(this, cfg);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async systemPrompt() {
|
|
104
|
+
console.log("GenerateTablesSkill.systemPrompt called");
|
|
105
|
+
return await GenerateTables.system_prompt();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
get userActions() {
|
|
109
|
+
console.log("GenerateTablesSkill.userActions getter accessed");
|
|
110
|
+
return {
|
|
111
|
+
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
|
+
if (!tables?.length) return { notify: "Nothing to create." };
|
|
117
|
+
await GenerateTables.execute({ tables }, { user });
|
|
118
|
+
return {
|
|
119
|
+
notify: `Created tables: ${tables.map((t) => t.table_name).join(", ")}`,
|
|
120
|
+
};
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
provideTools = () => {
|
|
126
|
+
console.log("GenerateTablesSkill.provideTools called");
|
|
127
|
+
const parameters = GenerateTables.json_schema();
|
|
128
|
+
return {
|
|
129
|
+
type: "function",
|
|
130
|
+
process: async (input) => {
|
|
131
|
+
const payload = normalizeTablesPayload(input);
|
|
132
|
+
const tables = payload.tables || [];
|
|
133
|
+
console.log("GenerateTablesSkill.provideTools.process called", {
|
|
134
|
+
table_count: tables.length,
|
|
135
|
+
});
|
|
136
|
+
if (!tables.length) return "No tables were provided for generate_tables.";
|
|
137
|
+
const summaryLines = summarizeTables(tables).map((line) => `- ${line}`);
|
|
138
|
+
const warnings = collectTableWarnings(tables);
|
|
139
|
+
const warningLines = warnings.length
|
|
140
|
+
? ["Warnings:", ...warnings.map((w) => `- ${w}`)]
|
|
141
|
+
: [];
|
|
142
|
+
return [
|
|
143
|
+
`Received ${tables.length} table definition${tables.length === 1 ? "" : "s"}:`,
|
|
144
|
+
...summaryLines,
|
|
145
|
+
...warningLines,
|
|
146
|
+
].join("\n");
|
|
147
|
+
},
|
|
148
|
+
postProcess: async ({ tool_call }) => {
|
|
149
|
+
console.log("GenerateTablesSkill.provideTools.postProcess called", {
|
|
150
|
+
has_input: !!tool_call?.input,
|
|
151
|
+
});
|
|
152
|
+
const payload = payloadFromToolCall(tool_call);
|
|
153
|
+
const tables = payload.tables || [];
|
|
154
|
+
let preview = "";
|
|
155
|
+
try {
|
|
156
|
+
preview = GenerateTables.render_html({ tables });
|
|
157
|
+
} catch (e) {
|
|
158
|
+
preview = `<pre>${JSON.stringify(payload, null, 2)}</pre>`;
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
stop: true,
|
|
162
|
+
add_response: preview,
|
|
163
|
+
add_user_action:
|
|
164
|
+
tables.length > 0
|
|
165
|
+
? {
|
|
166
|
+
name: "apply_copilot_tables",
|
|
167
|
+
type: "button",
|
|
168
|
+
label: `Create tables (${tables
|
|
169
|
+
.map((t) => t.table_name)
|
|
170
|
+
.join(", ")})`,
|
|
171
|
+
input: { tables },
|
|
172
|
+
}
|
|
173
|
+
: undefined,
|
|
174
|
+
};
|
|
175
|
+
},
|
|
176
|
+
function: {
|
|
177
|
+
name: GenerateTables.function_name,
|
|
178
|
+
description: GenerateTables.description,
|
|
179
|
+
parameters,
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
module.exports = GenerateTablesSkill;
|
package/agent-skills/pagegen.js
CHANGED
|
@@ -33,6 +33,7 @@ class GeneratePageSkill {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
constructor(cfg) {
|
|
36
|
+
console.log("GeneratePageSkill.constructor called", { cfg });
|
|
36
37
|
Object.assign(this, cfg);
|
|
37
38
|
}
|
|
38
39
|
|
|
@@ -88,12 +89,43 @@ class GeneratePageSkill {
|
|
|
88
89
|
|
|
89
90
|
return {
|
|
90
91
|
type: "function",
|
|
91
|
-
process: async ({ name }) => {
|
|
92
|
-
|
|
92
|
+
process: async ({ name, existing_page_name }) => {
|
|
93
|
+
if (existing_page_name) {
|
|
94
|
+
const page = Page.findOne({ name: existing_page_name });
|
|
95
|
+
|
|
96
|
+
if (!page)
|
|
97
|
+
return `Existing page ${existing_page_name} not found. Unable to provide HTML for this page`;
|
|
98
|
+
if (!page.layout.html_file)
|
|
99
|
+
return `Existing page ${existing_page_name} is not HTML-based. Unable to provide HTML for this page`;
|
|
100
|
+
const file = await File.findOne(page.layout.html_file);
|
|
101
|
+
const html = await file.get_contents("utf8");
|
|
102
|
+
return (
|
|
103
|
+
`The HTML code for the ${existing_page_name} page is:` +
|
|
104
|
+
"\n```html\n" +
|
|
105
|
+
html +
|
|
106
|
+
"\n```\n"
|
|
107
|
+
);
|
|
108
|
+
} else return "Metadata recieved";
|
|
93
109
|
},
|
|
94
110
|
postProcess: async ({ tool_call, generate }) => {
|
|
95
111
|
const str = await generate(
|
|
96
|
-
`Now generate the contents of the ${tool_call.input.name} page
|
|
112
|
+
`Now generate the contents of the ${tool_call.input.name} HTML page. If I asked you to embed a view,
|
|
113
|
+
use the <embed-view> self-closing tag to do so, setting the view name in the viewname attribute. For example,
|
|
114
|
+
to embed the view LeadForm inside a div, write: <div><embed-view viewname="LeadForm"></div>
|
|
115
|
+
|
|
116
|
+
If you need to include the standard bootstrap CSS and javascript files, they are available as:
|
|
117
|
+
|
|
118
|
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
|
|
119
|
+
|
|
120
|
+
and
|
|
121
|
+
<script src="/static_assets/js/jquery-3.6.0.min.js"></script>
|
|
122
|
+
<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>
|
|
123
|
+
|
|
124
|
+
If you are embedding views with <embed-view>, you should also embed the following script sources at the end of the <body> tag to make sure the content inside those views works:
|
|
125
|
+
|
|
126
|
+
<script src="/static_assets/js/saltcorn-common.js"></script>
|
|
127
|
+
<script src="/static_assets/js/saltcorn.js">
|
|
128
|
+
`,
|
|
97
129
|
);
|
|
98
130
|
const html = str.includes("```html")
|
|
99
131
|
? str.split("```html")[1].split("```")[0]
|
|
@@ -114,7 +146,7 @@ class GeneratePageSkill {
|
|
|
114
146
|
add_user_action: {
|
|
115
147
|
name: "build_copilot_page_gen",
|
|
116
148
|
type: "button",
|
|
117
|
-
label: "Save page",
|
|
149
|
+
label: "Save page "+tool_call.input.name,
|
|
118
150
|
input: { html },
|
|
119
151
|
},
|
|
120
152
|
};
|
|
@@ -124,6 +156,16 @@ class GeneratePageSkill {
|
|
|
124
156
|
return div({ class: "border border-primary p-2 m-2" }, phrase);
|
|
125
157
|
},*/
|
|
126
158
|
renderToolResponse: async (response, { req }) => {
|
|
159
|
+
if (
|
|
160
|
+
typeof response === "string" &&
|
|
161
|
+
response.includes("Unable to provide HTML for this page")
|
|
162
|
+
)
|
|
163
|
+
return response;
|
|
164
|
+
if (
|
|
165
|
+
typeof response === "string" &&
|
|
166
|
+
response.includes("The HTML code for the ")
|
|
167
|
+
)
|
|
168
|
+
return `Existing page retrieved...`;
|
|
127
169
|
return null;
|
|
128
170
|
},
|
|
129
171
|
function: {
|
|
@@ -133,8 +175,12 @@ class GeneratePageSkill {
|
|
|
133
175
|
type: "object",
|
|
134
176
|
required: ["name", "title", "min_role", "page_type"],
|
|
135
177
|
properties: {
|
|
178
|
+
existing_page_name: {
|
|
179
|
+
description: `If the user asks to modify or change a page, or create a new page based on an existing page, set this to retrieve the contents of the existing page.`,
|
|
180
|
+
type: "string",
|
|
181
|
+
},
|
|
136
182
|
name: {
|
|
137
|
-
description: `The name of the page, this should be a short name which is part of the url. `,
|
|
183
|
+
description: `The name of the new page to generate, this should be a short name which is part of the url. If an existing page name if given, set this to the same name to modify the existing page, and a different name to create a new page based on the existing page`,
|
|
138
184
|
type: "string",
|
|
139
185
|
},
|
|
140
186
|
title: {
|
package/builder-gen.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const { getState } = require("@saltcorn/data/db/state");
|
|
2
|
+
const WorkflowStep = require("@saltcorn/data/models/workflow_step");
|
|
3
|
+
const Trigger = require("@saltcorn/data/models/trigger");
|
|
4
|
+
const Table = require("@saltcorn/data/models/table");
|
|
5
|
+
const { getActionConfigFields } = require("@saltcorn/data/plugin-helper");
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
run: async (prompt, mode, table) => {
|
|
9
|
+
await new Promise((resolve) => {
|
|
10
|
+
setTimeout(resolve, 5000);
|
|
11
|
+
});
|
|
12
|
+
return {
|
|
13
|
+
type: "blank",
|
|
14
|
+
isHTML: true,
|
|
15
|
+
contents: `<h4>Placeholder copilot response</h4>
|
|
16
|
+
<blockquote class="blockquote"><p>${prompt}</p></blockquote>
|
|
17
|
+
<pre>mode=${mode} table=${table}</pre>`,
|
|
18
|
+
text_strings: [],
|
|
19
|
+
};
|
|
20
|
+
},
|
|
21
|
+
isAsync: true,
|
|
22
|
+
description: "Generate a builder layout",
|
|
23
|
+
arguments: [
|
|
24
|
+
{ name: "prompt", type: "String" },
|
|
25
|
+
{ name: "mode", type: "String" },
|
|
26
|
+
{ name: "table", type: "String" },
|
|
27
|
+
],
|
|
28
|
+
};
|
package/chat-copilot.js
CHANGED
|
@@ -36,9 +36,81 @@ const {
|
|
|
36
36
|
const MarkdownIt = require("markdown-it"),
|
|
37
37
|
md = new MarkdownIt();
|
|
38
38
|
|
|
39
|
+
const AGENT_VIEWTEMPLATE_NAME = "Agent Chat";
|
|
40
|
+
const COPILOT_AGENT_TRIGGER_NAME = "Saltcorn Copilot";
|
|
41
|
+
|
|
42
|
+
const getAgentViewtemplate = () => {
|
|
43
|
+
const state = getState();
|
|
44
|
+
return state?.viewtemplates?.[AGENT_VIEWTEMPLATE_NAME];
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const agentActionRegistered = () => {
|
|
48
|
+
const state = getState();
|
|
49
|
+
return !!(state?.actions && (state.actions.Agent || state.actions["Agent"]));
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
let copilotAgentTriggerCreateFailed = false;
|
|
53
|
+
let copilotAgentTriggerCreateLogged = false;
|
|
54
|
+
|
|
55
|
+
const ensureCopilotAgentTrigger = async () => {
|
|
56
|
+
if (!agentActionRegistered()) return null;
|
|
57
|
+
let trigger = await Trigger.findOne({
|
|
58
|
+
name: COPILOT_AGENT_TRIGGER_NAME,
|
|
59
|
+
action: "Agent",
|
|
60
|
+
});
|
|
61
|
+
if (trigger) return trigger;
|
|
62
|
+
|
|
63
|
+
if (copilotAgentTriggerCreateFailed) return null;
|
|
64
|
+
try {
|
|
65
|
+
return await Trigger.create({
|
|
66
|
+
action: "Agent",
|
|
67
|
+
when_trigger: "Never",
|
|
68
|
+
name: COPILOT_AGENT_TRIGGER_NAME,
|
|
69
|
+
configuration: {
|
|
70
|
+
prompt: "",
|
|
71
|
+
sys_prompt:
|
|
72
|
+
"You are Saltcorn Copilot. Help users build Saltcorn applications by proposing database schema, pages, views, workflows and actions.",
|
|
73
|
+
skills: [
|
|
74
|
+
{ skill_type: "Database design" },
|
|
75
|
+
{ skill_type: "Generate Page" },
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
} catch (e) {
|
|
80
|
+
copilotAgentTriggerCreateFailed = true;
|
|
81
|
+
const state = getState();
|
|
82
|
+
const msg =
|
|
83
|
+
e && e.message
|
|
84
|
+
? e.message
|
|
85
|
+
: "Unknown error while auto-creating Copilot Agent trigger";
|
|
86
|
+
if (!copilotAgentTriggerCreateLogged) {
|
|
87
|
+
copilotAgentTriggerCreateLogged = true;
|
|
88
|
+
if (state?.log) state.log(2, `Copilot: ${msg}`);
|
|
89
|
+
else console.error("Copilot:", msg);
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const getAgentChatCfg = async () => {
|
|
96
|
+
const trigger = await ensureCopilotAgentTrigger();
|
|
97
|
+
if (!trigger) return null;
|
|
98
|
+
return {
|
|
99
|
+
action_id: trigger.id,
|
|
100
|
+
show_prev_runs: true,
|
|
101
|
+
prev_runs_closed: false,
|
|
102
|
+
placeholder: "How can I help you?",
|
|
103
|
+
explainer: "",
|
|
104
|
+
image_upload: false,
|
|
105
|
+
stream: true,
|
|
106
|
+
audio_recorder: false,
|
|
107
|
+
layout: "Vertical",
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
|
|
39
111
|
const get_state_fields = () => [];
|
|
40
112
|
|
|
41
|
-
const
|
|
113
|
+
const runLegacy = async (table_id, viewname, cfg, state, { res, req }) => {
|
|
42
114
|
const prevRuns = (
|
|
43
115
|
await WorkflowRun.find(
|
|
44
116
|
{ trigger_id: null /*started_by: req.user?.id*/ }, //todo uncomment
|
|
@@ -49,6 +121,7 @@ const run = async (table_id, viewname, cfg, state, { res, req }) => {
|
|
|
49
121
|
r.context.interactions &&
|
|
50
122
|
(r.context.copilot === "_system" || !r.context.copilot)
|
|
51
123
|
);
|
|
124
|
+
console.log({prevRuns})
|
|
52
125
|
const cfgMsg = incompleteCfgMsg();
|
|
53
126
|
if (cfgMsg) return cfgMsg;
|
|
54
127
|
let runInteractions = "";
|
|
@@ -284,6 +357,23 @@ const run = async (table_id, viewname, cfg, state, { res, req }) => {
|
|
|
284
357
|
};
|
|
285
358
|
};
|
|
286
359
|
|
|
360
|
+
const run = async (table_id, viewname, cfg, state, extra) => {
|
|
361
|
+
const agentVt = getAgentViewtemplate();
|
|
362
|
+
if (!agentVt) return await runLegacy(table_id, viewname, cfg, state, extra);
|
|
363
|
+
|
|
364
|
+
const agentCfg = await getAgentChatCfg();
|
|
365
|
+
if (!agentCfg) return await runLegacy(table_id, viewname, cfg, state, extra);
|
|
366
|
+
|
|
367
|
+
const agentView = new View({
|
|
368
|
+
name: viewname,
|
|
369
|
+
viewtemplate: AGENT_VIEWTEMPLATE_NAME,
|
|
370
|
+
configuration: agentCfg,
|
|
371
|
+
min_role: 100,
|
|
372
|
+
});
|
|
373
|
+
console.log({agentView})
|
|
374
|
+
return await agentView.run(state, extra);
|
|
375
|
+
};
|
|
376
|
+
|
|
287
377
|
const ellipsize = (s, nchars) => {
|
|
288
378
|
if (!s || !s.length) return "";
|
|
289
379
|
if (s.length <= (nchars || 20)) return text_attr(s);
|
|
@@ -330,7 +420,7 @@ build a workflow that asks the user for their name and age
|
|
|
330
420
|
|
|
331
421
|
*/
|
|
332
422
|
|
|
333
|
-
const
|
|
423
|
+
const executeLegacy = async (table_id, viewname, config, body, { req }) => {
|
|
334
424
|
const { fcall_id, run_id } = body;
|
|
335
425
|
|
|
336
426
|
const run = await WorkflowRun.findOne({ id: +run_id });
|
|
@@ -360,7 +450,7 @@ const execute = async (table_id, viewname, config, body, { req }) => {
|
|
|
360
450
|
return { json: { success: "ok", fcall_id, ...(result || {}) } };
|
|
361
451
|
};
|
|
362
452
|
|
|
363
|
-
const
|
|
453
|
+
const interactLegacy = async (table_id, viewname, config, body, { req }) => {
|
|
364
454
|
const { userinput, run_id } = body;
|
|
365
455
|
let run;
|
|
366
456
|
if (!run_id || run_id === "undefined")
|
|
@@ -487,6 +577,81 @@ const interact = async (table_id, viewname, config, body, { req }) => {
|
|
|
487
577
|
};
|
|
488
578
|
};
|
|
489
579
|
|
|
580
|
+
const runAgentRoute = async (routeName, table_id, viewname, body, extra) => {
|
|
581
|
+
const agentVt = getAgentViewtemplate();
|
|
582
|
+
if (!agentVt?.routes?.[routeName]) return null;
|
|
583
|
+
const agentCfg = await getAgentChatCfg();
|
|
584
|
+
if (!agentCfg) return null;
|
|
585
|
+
return await agentVt.routes[routeName](table_id, viewname, agentCfg, body, extra);
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
const interact = async (table_id, viewname, config, body, extra) => {
|
|
589
|
+
const agentResp = await runAgentRoute(
|
|
590
|
+
"interact",
|
|
591
|
+
table_id,
|
|
592
|
+
viewname,
|
|
593
|
+
body,
|
|
594
|
+
extra,
|
|
595
|
+
);
|
|
596
|
+
if (agentResp) return agentResp;
|
|
597
|
+
return await interactLegacy(table_id, viewname, config, body, extra);
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
// Legacy route used only by the legacy UI. The Agent Chat UI uses `execute_user_action`.
|
|
601
|
+
const execute = async (table_id, viewname, config, body, extra) => {
|
|
602
|
+
return await executeLegacy(table_id, viewname, config, body, extra);
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
const delprevrun = async (table_id, viewname, config, body, extra) => {
|
|
606
|
+
const agentResp = await runAgentRoute(
|
|
607
|
+
"delprevrun",
|
|
608
|
+
table_id,
|
|
609
|
+
viewname,
|
|
610
|
+
body,
|
|
611
|
+
extra,
|
|
612
|
+
);
|
|
613
|
+
if (agentResp) return agentResp;
|
|
614
|
+
return { json: { error: "delprevrun is only available in Agent Chat mode" } };
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
const debug_info = async (table_id, viewname, config, body, extra) => {
|
|
618
|
+
const agentResp = await runAgentRoute(
|
|
619
|
+
"debug_info",
|
|
620
|
+
table_id,
|
|
621
|
+
viewname,
|
|
622
|
+
body,
|
|
623
|
+
extra,
|
|
624
|
+
);
|
|
625
|
+
if (agentResp) return agentResp;
|
|
626
|
+
return { json: { error: "debug_info is only available in Agent Chat mode" } };
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
const skillroute = async (table_id, viewname, config, body, extra) => {
|
|
630
|
+
const agentResp = await runAgentRoute(
|
|
631
|
+
"skillroute",
|
|
632
|
+
table_id,
|
|
633
|
+
viewname,
|
|
634
|
+
body,
|
|
635
|
+
extra,
|
|
636
|
+
);
|
|
637
|
+
if (agentResp) return agentResp;
|
|
638
|
+
return { json: { error: "skillroute is only available in Agent Chat mode" } };
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
const execute_user_action = async (table_id, viewname, config, body, extra) => {
|
|
642
|
+
const agentResp = await runAgentRoute(
|
|
643
|
+
"execute_user_action",
|
|
644
|
+
table_id,
|
|
645
|
+
viewname,
|
|
646
|
+
body,
|
|
647
|
+
extra,
|
|
648
|
+
);
|
|
649
|
+
if (agentResp) return agentResp;
|
|
650
|
+
return {
|
|
651
|
+
json: { error: "execute_user_action is only available in Agent Chat mode" },
|
|
652
|
+
};
|
|
653
|
+
};
|
|
654
|
+
|
|
490
655
|
const getFollowOnGeneration = async (tool_call) => {
|
|
491
656
|
const fname = tool_call.function?.name || tool_call.toolName;
|
|
492
657
|
const actionClass = classesWithSkills().find(
|
|
@@ -595,5 +760,12 @@ module.exports = {
|
|
|
595
760
|
tableless: true,
|
|
596
761
|
singleton: true,
|
|
597
762
|
run,
|
|
598
|
-
routes: {
|
|
763
|
+
routes: {
|
|
764
|
+
interact,
|
|
765
|
+
execute,
|
|
766
|
+
delprevrun,
|
|
767
|
+
debug_info,
|
|
768
|
+
skillroute,
|
|
769
|
+
execute_user_action,
|
|
770
|
+
},
|
|
599
771
|
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
const Field = require("@saltcorn/data/models/field");
|
|
2
|
+
const Table = require("@saltcorn/data/models/table");
|
|
3
|
+
const Form = require("@saltcorn/data/models/form");
|
|
4
|
+
const View = require("@saltcorn/data/models/view");
|
|
5
|
+
const Trigger = require("@saltcorn/data/models/trigger");
|
|
6
|
+
const { findType } = require("@saltcorn/data/models/discovery");
|
|
7
|
+
const { save_menu_items } = require("@saltcorn/data/models/config");
|
|
8
|
+
const db = require("@saltcorn/data/db");
|
|
9
|
+
const WorkflowRun = require("@saltcorn/data/models/workflow_run");
|
|
10
|
+
const { localeDateTime } = require("@saltcorn/markup");
|
|
11
|
+
const {
|
|
12
|
+
div,
|
|
13
|
+
script,
|
|
14
|
+
domReady,
|
|
15
|
+
pre,
|
|
16
|
+
code,
|
|
17
|
+
input,
|
|
18
|
+
h4,
|
|
19
|
+
style,
|
|
20
|
+
h5,
|
|
21
|
+
button,
|
|
22
|
+
text_attr,
|
|
23
|
+
i,
|
|
24
|
+
p,
|
|
25
|
+
span,
|
|
26
|
+
small,
|
|
27
|
+
form,
|
|
28
|
+
textarea,
|
|
29
|
+
} = require("@saltcorn/markup/tags");
|
|
30
|
+
const { getState } = require("@saltcorn/data/db/state");
|
|
31
|
+
|
|
32
|
+
const get_state_fields = () => [];
|
|
33
|
+
|
|
34
|
+
const sys_prompt = ``;
|
|
35
|
+
|
|
36
|
+
const get_agent_view = () => {
|
|
37
|
+
const agent_action = new Trigger({
|
|
38
|
+
action: "Agent",
|
|
39
|
+
when_trigger: "Never",
|
|
40
|
+
configuration: {
|
|
41
|
+
sys_prompt,
|
|
42
|
+
skills: [
|
|
43
|
+
{ skill_type: "Generate Page" },
|
|
44
|
+
{ skill_type: "Database design" },
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
return new View({
|
|
49
|
+
viewtemplate: "Agent Chat",
|
|
50
|
+
name: "Saltcorn Agent copilot",
|
|
51
|
+
min_role: 1,
|
|
52
|
+
configuration: {
|
|
53
|
+
agent_action,
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
const run = async (table_id, viewname, cfg, state, reqres) => {
|
|
58
|
+
return await get_agent_view().run(state, reqres);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const interact = async (table_id, viewname, config, body, reqres) => {
|
|
62
|
+
console.log("copilot interact with body", body);
|
|
63
|
+
const view = get_agent_view();
|
|
64
|
+
return await view.runRoute("interact", body, reqres.res, reqres);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
module.exports = {
|
|
68
|
+
name: "Saltcorn Agent copilot",
|
|
69
|
+
display_state_form: false,
|
|
70
|
+
get_state_fields,
|
|
71
|
+
tableless: true,
|
|
72
|
+
singleton: true,
|
|
73
|
+
run,
|
|
74
|
+
routes: { interact },
|
|
75
|
+
};
|
package/index.js
CHANGED
|
@@ -4,15 +4,25 @@ const { features } = require("@saltcorn/data/db/state");
|
|
|
4
4
|
|
|
5
5
|
module.exports = {
|
|
6
6
|
sc_plugin_api_version: 1,
|
|
7
|
-
dependencies: ["@saltcorn/large-language-model"],
|
|
7
|
+
dependencies: ["@saltcorn/large-language-model", "@saltcorn/agents"],
|
|
8
8
|
viewtemplates: features.workflows
|
|
9
|
-
? [
|
|
9
|
+
? [
|
|
10
|
+
require("./chat-copilot"),
|
|
11
|
+
require("./user-copilot"),
|
|
12
|
+
require("./copilot-as-agent"),
|
|
13
|
+
]
|
|
10
14
|
: [require("./action-builder"), require("./database-designer")],
|
|
11
15
|
functions: features.workflows
|
|
12
|
-
? {
|
|
16
|
+
? {
|
|
17
|
+
copilot_generate_layout: require("./builder-gen.js"),
|
|
18
|
+
copilot_generate_workflow: require("./workflow-gen"),
|
|
19
|
+
}
|
|
13
20
|
: {},
|
|
14
21
|
actions: { copilot_generate_page: require("./page-gen-action") },
|
|
15
22
|
exchange: {
|
|
16
|
-
agent_skills: [
|
|
23
|
+
agent_skills: [
|
|
24
|
+
require("./agent-skills/pagegen.js"),
|
|
25
|
+
require("./agent-skills/database-design.js"),
|
|
26
|
+
],
|
|
17
27
|
},
|
|
18
28
|
};
|
package/package.json
CHANGED
package/page-gen-action.js
CHANGED
|
@@ -7,9 +7,129 @@ 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 }) => {
|
|
129
|
+
console.log({
|
|
130
|
+
table,
|
|
131
|
+
mode,
|
|
132
|
+
}, "OOOOOOOOOO");
|
|
13
133
|
if (mode === "workflow") {
|
|
14
134
|
return [
|
|
15
135
|
{
|
|
@@ -36,6 +156,30 @@ module.exports = {
|
|
|
36
156
|
class: "validate-expression",
|
|
37
157
|
type: "String",
|
|
38
158
|
},
|
|
159
|
+
{
|
|
160
|
+
name: "design_image_expression",
|
|
161
|
+
label: "Design image expression",
|
|
162
|
+
sublabel:
|
|
163
|
+
"Optional expression returning a file, file id, data URL, or array with reference design images to guide generation",
|
|
164
|
+
class: "validate-expression",
|
|
165
|
+
type: "String",
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
name: "feedback_image_expression",
|
|
169
|
+
label: "Feedback image expression",
|
|
170
|
+
sublabel:
|
|
171
|
+
"Optional expression returning a file, file id, data URL, or array with annotated feedback screenshots",
|
|
172
|
+
class: "validate-expression",
|
|
173
|
+
type: "String",
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
name: "existing_page_name",
|
|
177
|
+
label: "Existing page name",
|
|
178
|
+
sublabel:
|
|
179
|
+
"Optional expression evaluating to the name of the page to use as a baseline. If provided, its HTML will be included automatically",
|
|
180
|
+
class: "validate-expression",
|
|
181
|
+
type: "String",
|
|
182
|
+
},
|
|
39
183
|
{
|
|
40
184
|
name: "existing_page_html",
|
|
41
185
|
label: "Existing page HTML",
|
|
@@ -66,9 +210,13 @@ module.exports = {
|
|
|
66
210
|
},
|
|
67
211
|
];
|
|
68
212
|
} else if (table) {
|
|
213
|
+
console.log(JSON.stringify(table, null, 2), "TABLE");
|
|
69
214
|
const textFields = table.fields
|
|
70
215
|
.filter((f) => f.type?.sql_name === "text")
|
|
71
216
|
.map((f) => f.name);
|
|
217
|
+
const fileFields = table.fields
|
|
218
|
+
.filter((f) => ["File", "Image"].includes(f.type?.name))
|
|
219
|
+
.map((f) => f.name);
|
|
72
220
|
|
|
73
221
|
return [
|
|
74
222
|
{
|
|
@@ -93,6 +241,30 @@ module.exports = {
|
|
|
93
241
|
required: true,
|
|
94
242
|
attributes: { options: textFields },
|
|
95
243
|
},
|
|
244
|
+
{
|
|
245
|
+
name: "existing_page_field",
|
|
246
|
+
label: "Existing page name field",
|
|
247
|
+
sublabel:
|
|
248
|
+
"Optional text field storing the name of the page to update",
|
|
249
|
+
type: "String",
|
|
250
|
+
attributes: { options: textFields },
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
name: "design_image_field",
|
|
254
|
+
label: "Design image field",
|
|
255
|
+
sublabel:
|
|
256
|
+
"Optional file/image field containing a reference design supplied by the user",
|
|
257
|
+
type: "String",
|
|
258
|
+
attributes: { options: fileFields },
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
name: "feedback_image_field",
|
|
262
|
+
label: "Feedback image field",
|
|
263
|
+
sublabel:
|
|
264
|
+
"Optional file/image field with annotated feedback screenshots to consider during updates",
|
|
265
|
+
type: "String",
|
|
266
|
+
attributes: { options: fileFields },
|
|
267
|
+
},
|
|
96
268
|
// ...override_fields,
|
|
97
269
|
];
|
|
98
270
|
}
|
|
@@ -109,6 +281,12 @@ module.exports = {
|
|
|
109
281
|
prompt_template,
|
|
110
282
|
answer_field,
|
|
111
283
|
image_prompt,
|
|
284
|
+
design_image_expression,
|
|
285
|
+
feedback_image_expression,
|
|
286
|
+
design_image_field,
|
|
287
|
+
feedback_image_field,
|
|
288
|
+
existing_page_field,
|
|
289
|
+
existing_page_name,
|
|
112
290
|
existing_page_html,
|
|
113
291
|
chat_history_field,
|
|
114
292
|
convert_to_saltcorn,
|
|
@@ -117,7 +295,7 @@ module.exports = {
|
|
|
117
295
|
}) => {
|
|
118
296
|
let prompt;
|
|
119
297
|
if (mode === "workflow") prompt = interpolate(prompt_template, row, user);
|
|
120
|
-
else if (prompt_field === "Formula"
|
|
298
|
+
else if (prompt_field === "Formula")
|
|
121
299
|
prompt = eval_expression(
|
|
122
300
|
prompt_formula,
|
|
123
301
|
row,
|
|
@@ -125,61 +303,104 @@ module.exports = {
|
|
|
125
303
|
"copilot_generate_page prompt formula",
|
|
126
304
|
);
|
|
127
305
|
else prompt = row[prompt_field];
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
306
|
+
|
|
307
|
+
const resolvedExistingName = normalizeName(
|
|
308
|
+
mode === "workflow"
|
|
309
|
+
? existing_page_name
|
|
310
|
+
? eval_expression(
|
|
311
|
+
existing_page_name,
|
|
312
|
+
row,
|
|
313
|
+
user,
|
|
314
|
+
"copilot_generate_page existing page name",
|
|
315
|
+
)
|
|
316
|
+
: null
|
|
317
|
+
: existing_page_field
|
|
318
|
+
? row?.[existing_page_field]
|
|
319
|
+
: null,
|
|
320
|
+
);
|
|
321
|
+
const existingAssets = resolvedExistingName
|
|
322
|
+
? await loadExistingPageAssets(resolvedExistingName)
|
|
323
|
+
: { page: null, html: null };
|
|
324
|
+
|
|
325
|
+
let manualExistingHtml;
|
|
326
|
+
if (existing_page_html) {
|
|
327
|
+
manualExistingHtml = eval_expression(
|
|
131
328
|
existing_page_html,
|
|
132
329
|
row,
|
|
133
330
|
user,
|
|
134
331
|
"copilot_generate_page existing page html",
|
|
135
332
|
);
|
|
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
333
|
}
|
|
138
|
-
|
|
139
|
-
|
|
334
|
+
const existingHtmlForPrompt = manualExistingHtml || existingAssets.html;
|
|
335
|
+
if (existingHtmlForPrompt) {
|
|
336
|
+
const label = resolvedExistingName ? ` ${resolvedExistingName}` : "";
|
|
337
|
+
prompt = `This is the HTML code for the existing page${label} you should edit:\n\n\`\`\`html\n${existingHtmlForPrompt}\n\`\`\`\n\n${prompt}`;
|
|
338
|
+
}
|
|
140
339
|
|
|
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(
|
|
340
|
+
const chatMessages = [];
|
|
341
|
+
if (mode === "workflow") {
|
|
342
|
+
const referenceImages = await gatherImagesFromExpression(
|
|
156
343
|
image_prompt,
|
|
157
344
|
row,
|
|
158
345
|
user,
|
|
159
346
|
"copilot_generate_page image prompt",
|
|
160
347
|
);
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
348
|
+
chatMessages.push(
|
|
349
|
+
...buildImageMessages(referenceImages, "Reference image"),
|
|
350
|
+
);
|
|
351
|
+
const designImages = await gatherImagesFromExpression(
|
|
352
|
+
design_image_expression,
|
|
353
|
+
row,
|
|
354
|
+
user,
|
|
355
|
+
"copilot_generate_page design image",
|
|
356
|
+
);
|
|
357
|
+
chatMessages.push(
|
|
358
|
+
...buildImageMessages(designImages, "Design reference"),
|
|
359
|
+
);
|
|
360
|
+
const feedbackImages = await gatherImagesFromExpression(
|
|
361
|
+
feedback_image_expression,
|
|
362
|
+
row,
|
|
363
|
+
user,
|
|
364
|
+
"copilot_generate_page feedback image",
|
|
365
|
+
);
|
|
366
|
+
chatMessages.push(
|
|
367
|
+
...buildImageMessages(feedbackImages, "Feedback reference"),
|
|
368
|
+
);
|
|
369
|
+
} else if (row) {
|
|
370
|
+
chatMessages.push(
|
|
371
|
+
...buildImageMessages(
|
|
372
|
+
await gatherImageDataFromValue(row[design_image_field]),
|
|
373
|
+
"Design reference",
|
|
374
|
+
),
|
|
375
|
+
);
|
|
376
|
+
chatMessages.push(
|
|
377
|
+
...buildImageMessages(
|
|
378
|
+
await gatherImageDataFromValue(row[feedback_image_field]),
|
|
379
|
+
"Feedback reference",
|
|
380
|
+
),
|
|
381
|
+
);
|
|
177
382
|
}
|
|
178
|
-
const
|
|
383
|
+
const chat = chatMessages.length ? chatMessages : undefined;
|
|
384
|
+
|
|
385
|
+
const systemPrompt = await GeneratePage.system_prompt();
|
|
386
|
+
const tools = [
|
|
387
|
+
{
|
|
388
|
+
type: "function",
|
|
389
|
+
function: {
|
|
390
|
+
name: GeneratePage.function_name,
|
|
391
|
+
description: GeneratePage.description,
|
|
392
|
+
parameters: await GeneratePage.json_schema(),
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
];
|
|
396
|
+
const { llm_generate } = getState().functions;
|
|
397
|
+
const llmOptions = {
|
|
179
398
|
tools,
|
|
180
399
|
chat,
|
|
181
400
|
systemPrompt,
|
|
182
|
-
|
|
401
|
+
...(model ? { model } : {}),
|
|
402
|
+
};
|
|
403
|
+
const initial_ans = await llm_generate.run(prompt, llmOptions);
|
|
183
404
|
const initial_info =
|
|
184
405
|
initial_ans.tool_calls[0].input ||
|
|
185
406
|
JSON.parse(initial_ans.tool_calls[0].function.arguments);
|
|
@@ -195,55 +416,55 @@ module.exports = {
|
|
|
195
416
|
and
|
|
196
417
|
|
|
197
418
|
<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
|
-
|
|
419
|
+
const generationPrompt = `${prompt}
|
|
420
|
+
|
|
421
|
+
The page title is: ${initial_info.title}.
|
|
422
|
+
Further page description: ${initial_info.description}.
|
|
423
|
+
|
|
424
|
+
Generate the HTML for the web page using the Bootstrap 5 CSS framework.
|
|
425
|
+
|
|
426
|
+
${prompt_part_2}
|
|
427
|
+
|
|
428
|
+
Just generate HTML code, do not wrap in markdown code tags`;
|
|
429
|
+
const page_html = await llm_generate.run(generationPrompt, {
|
|
430
|
+
debugResult: true,
|
|
431
|
+
chat,
|
|
432
|
+
...(model ? { model } : {}),
|
|
433
|
+
response_format: full.response_schema
|
|
434
|
+
? {
|
|
435
|
+
type: "json_schema",
|
|
436
|
+
json_schema: {
|
|
437
|
+
name: "generate_page",
|
|
438
|
+
schema: full.response_schema,
|
|
439
|
+
},
|
|
440
|
+
}
|
|
441
|
+
: undefined,
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
const requestedName = page_name ? interpolate(page_name, row, user) : "";
|
|
445
|
+
const targetPageName = normalizeName(
|
|
446
|
+
requestedName || resolvedExistingName || "",
|
|
222
447
|
);
|
|
448
|
+
const updatingExisting =
|
|
449
|
+
targetPageName &&
|
|
450
|
+
resolvedExistingName &&
|
|
451
|
+
targetPageName === resolvedExistingName &&
|
|
452
|
+
existingAssets.page;
|
|
223
453
|
|
|
224
|
-
|
|
225
|
-
if (use_page_name) {
|
|
454
|
+
if (targetPageName) {
|
|
226
455
|
let layout;
|
|
227
456
|
if (convert_to_saltcorn) {
|
|
228
457
|
layout = parseHTML(page_html, true);
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
`${use_page_name}.html`,
|
|
232
|
-
"text/html",
|
|
458
|
+
await upsertHtmlPreviewPage(
|
|
459
|
+
`${targetPageName}_html`,
|
|
233
460
|
wrapExample(page_html),
|
|
234
|
-
|
|
235
|
-
|
|
461
|
+
initial_info.title,
|
|
462
|
+
initial_info.description,
|
|
463
|
+
user,
|
|
236
464
|
);
|
|
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
465
|
} else {
|
|
245
466
|
const file = await File.from_contents(
|
|
246
|
-
`${
|
|
467
|
+
`${targetPageName}.html`,
|
|
247
468
|
"text/html",
|
|
248
469
|
page_html,
|
|
249
470
|
user.id,
|
|
@@ -251,18 +472,26 @@ module.exports = {
|
|
|
251
472
|
);
|
|
252
473
|
layout = { html_file: file.path_to_serve };
|
|
253
474
|
}
|
|
254
|
-
//save to a file
|
|
255
475
|
|
|
256
|
-
|
|
257
|
-
await Page.create({
|
|
258
|
-
name: use_page_name,
|
|
476
|
+
const pagePayload = {
|
|
259
477
|
title: initial_info.title,
|
|
260
478
|
description: initial_info.description,
|
|
261
|
-
min_role: 100,
|
|
262
479
|
layout,
|
|
263
|
-
}
|
|
264
|
-
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
if (updatingExisting) {
|
|
483
|
+
await existingAssets.page.update(pagePayload);
|
|
484
|
+
refreshPagesSoon();
|
|
485
|
+
} else {
|
|
486
|
+
await Page.create({
|
|
487
|
+
name: targetPageName,
|
|
488
|
+
min_role: 100,
|
|
489
|
+
...pagePayload,
|
|
490
|
+
});
|
|
491
|
+
refreshPagesSoon();
|
|
492
|
+
}
|
|
265
493
|
}
|
|
494
|
+
|
|
266
495
|
const upd = answer_field ? { [answer_field]: page_html } : {};
|
|
267
496
|
if (mode === "workflow") return upd;
|
|
268
497
|
else if (answer_field) await table.updateRow(upd, row[table.pk_name]);
|