@saltcorn/copilot 0.6.3 → 0.7.0
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/agent-skills/pagegen.js +162 -0
- package/index.js +3 -0
- package/package.json +9 -2
- package/page-gen-action.js +29 -7
- package/tests/configs.js +34 -0
- package/tests/copilot.test.js +76 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
const {
|
|
2
|
+
div,
|
|
3
|
+
pre,
|
|
4
|
+
code,
|
|
5
|
+
a,
|
|
6
|
+
text,
|
|
7
|
+
escape,
|
|
8
|
+
iframe,
|
|
9
|
+
text_attr,
|
|
10
|
+
} = require("@saltcorn/markup/tags");
|
|
11
|
+
const Workflow = require("@saltcorn/data/models/workflow");
|
|
12
|
+
const Form = require("@saltcorn/data/models/form");
|
|
13
|
+
const File = require("@saltcorn/data/models/file");
|
|
14
|
+
const Table = require("@saltcorn/data/models/table");
|
|
15
|
+
const View = require("@saltcorn/data/models/view");
|
|
16
|
+
const Page = require("@saltcorn/data/models/page");
|
|
17
|
+
const Trigger = require("@saltcorn/data/models/trigger");
|
|
18
|
+
const FieldRepeat = require("@saltcorn/data/models/fieldrepeat");
|
|
19
|
+
const { getState } = require("@saltcorn/data/db/state");
|
|
20
|
+
const db = require("@saltcorn/data/db");
|
|
21
|
+
const { eval_expression } = require("@saltcorn/data/models/expression");
|
|
22
|
+
const { interpolate } = require("@saltcorn/data/utils");
|
|
23
|
+
const { features } = require("@saltcorn/data/db/state");
|
|
24
|
+
const GeneratePage = require("../actions/generate-page");
|
|
25
|
+
|
|
26
|
+
//const { fieldProperties } = require("./helpers");
|
|
27
|
+
|
|
28
|
+
class GeneratePageSkill {
|
|
29
|
+
static skill_name = "Generate Page";
|
|
30
|
+
|
|
31
|
+
get skill_label() {
|
|
32
|
+
return "Generate Page";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
constructor(cfg) {
|
|
36
|
+
Object.assign(this, cfg);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async systemPrompt() {
|
|
40
|
+
return await GeneratePage.system_prompt();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static async configFields() {
|
|
44
|
+
return [
|
|
45
|
+
{
|
|
46
|
+
name: "convert_to_saltcorn",
|
|
47
|
+
label: "Editable format",
|
|
48
|
+
sublabel: "Convert to Saltcorn editable pages",
|
|
49
|
+
type: "Bool",
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
}
|
|
53
|
+
get userActions() {
|
|
54
|
+
return {
|
|
55
|
+
async build_copilot_page_gen({ user, name, title, description, html }) {
|
|
56
|
+
const file = await File.from_contents(
|
|
57
|
+
`${name}.html`,
|
|
58
|
+
"text/html",
|
|
59
|
+
html,
|
|
60
|
+
user.id,
|
|
61
|
+
100,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
await Page.create({
|
|
65
|
+
name,
|
|
66
|
+
title,
|
|
67
|
+
description,
|
|
68
|
+
min_role: 100,
|
|
69
|
+
layout: { html_file: file.path_to_serve },
|
|
70
|
+
});
|
|
71
|
+
setTimeout(() => getState().refresh_pages(), 200);
|
|
72
|
+
return {
|
|
73
|
+
notify: `Page saved: <a target="_blank" href="/page/${name}">${name}</a>`,
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
provideTools = () => {
|
|
79
|
+
let properties = {};
|
|
80
|
+
(this.toolargs || []).forEach((arg) => {
|
|
81
|
+
properties[arg.name] = {
|
|
82
|
+
description: arg.description,
|
|
83
|
+
type: arg.argtype,
|
|
84
|
+
};
|
|
85
|
+
if (arg.options && arg.argtype === "string")
|
|
86
|
+
properties[arg.name].enum = arg.options.split(",").map((s) => s.trim());
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
type: "function",
|
|
91
|
+
process: async ({ name }) => {
|
|
92
|
+
return "Metadata recieved";
|
|
93
|
+
},
|
|
94
|
+
postProcess: async ({ tool_call, generate }) => {
|
|
95
|
+
const str = await generate(
|
|
96
|
+
`Now generate the contents of the ${tool_call.input.name} page with HTML`,
|
|
97
|
+
);
|
|
98
|
+
const html = str.includes("```html")
|
|
99
|
+
? str.split("```html")[1].split("```")[0]
|
|
100
|
+
: str;
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
stop: true,
|
|
104
|
+
add_response: iframe({
|
|
105
|
+
srcdoc: text_attr(html),
|
|
106
|
+
width: 500,
|
|
107
|
+
height: 800,
|
|
108
|
+
}),
|
|
109
|
+
add_system_prompt: `If the user asks you to regenerate the page,
|
|
110
|
+
you must run the generate_page tool again. After running this tool
|
|
111
|
+
you will be prompted to generate the html again. You should repeat
|
|
112
|
+
the html from the previous answer except for the changes the user
|
|
113
|
+
is requesting.`,
|
|
114
|
+
add_user_action: {
|
|
115
|
+
name: "build_copilot_page_gen",
|
|
116
|
+
type: "button",
|
|
117
|
+
label: "Save page",
|
|
118
|
+
input: { html },
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
/*renderToolCall({ phrase }, { req }) {
|
|
124
|
+
return div({ class: "border border-primary p-2 m-2" }, phrase);
|
|
125
|
+
},*/
|
|
126
|
+
renderToolResponse: async (response, { req }) => {
|
|
127
|
+
return null;
|
|
128
|
+
},
|
|
129
|
+
function: {
|
|
130
|
+
name: "generate_page",
|
|
131
|
+
description: "Generate a page with HTML.",
|
|
132
|
+
parameters: {
|
|
133
|
+
type: "object",
|
|
134
|
+
required: ["name", "title", "min_role", "page_type"],
|
|
135
|
+
properties: {
|
|
136
|
+
name: {
|
|
137
|
+
description: `The name of the page, this should be a short name which is part of the url. `,
|
|
138
|
+
type: "string",
|
|
139
|
+
},
|
|
140
|
+
title: {
|
|
141
|
+
description: "Page title, this is in the <title> tag.",
|
|
142
|
+
type: "string",
|
|
143
|
+
},
|
|
144
|
+
description: {
|
|
145
|
+
description:
|
|
146
|
+
"A longer description that is not visible but appears in the page header and is indexed by search engines",
|
|
147
|
+
type: "string",
|
|
148
|
+
},
|
|
149
|
+
page_type: {
|
|
150
|
+
description:
|
|
151
|
+
"The type of page to generate: a Marketing page if for promotional purposes, such as a landing page or a brouchure, with an appealing design. An Application page is simpler and an integrated part of the application",
|
|
152
|
+
type: "string",
|
|
153
|
+
enum: ["Marketing page", "Application page"],
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
module.exports = GeneratePageSkill;
|
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saltcorn/copilot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "AI assistant for building Saltcorn applications",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"dependencies": {
|
|
@@ -12,6 +12,12 @@
|
|
|
12
12
|
"node-html-parser": "7.0.1",
|
|
13
13
|
"html-tags": "3.3.1"
|
|
14
14
|
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"jest": "^29.7.0"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"test": "jest tests --runInBand"
|
|
20
|
+
},
|
|
15
21
|
"author": "Tom Nielsen",
|
|
16
22
|
"license": "MIT",
|
|
17
23
|
"repository": "github:saltcorn/copilot",
|
|
@@ -22,7 +28,8 @@
|
|
|
22
28
|
},
|
|
23
29
|
"env": {
|
|
24
30
|
"node": true,
|
|
25
|
-
"es6": true
|
|
31
|
+
"es6": true,
|
|
32
|
+
"jest/globals": true
|
|
26
33
|
},
|
|
27
34
|
"rules": {
|
|
28
35
|
"no-unused-vars": "off",
|
package/page-gen-action.js
CHANGED
|
@@ -36,6 +36,14 @@ module.exports = {
|
|
|
36
36
|
class: "validate-expression",
|
|
37
37
|
type: "String",
|
|
38
38
|
},
|
|
39
|
+
{
|
|
40
|
+
name: "existing_page_html",
|
|
41
|
+
label: "Existing page HTML",
|
|
42
|
+
sublabel:
|
|
43
|
+
"Optional. An expression, based on the context, for the existing page HTML to be edited",
|
|
44
|
+
class: "validate-expression",
|
|
45
|
+
type: "String",
|
|
46
|
+
},
|
|
39
47
|
{
|
|
40
48
|
name: "answer_field",
|
|
41
49
|
label: "Answer variable",
|
|
@@ -101,6 +109,7 @@ module.exports = {
|
|
|
101
109
|
prompt_template,
|
|
102
110
|
answer_field,
|
|
103
111
|
image_prompt,
|
|
112
|
+
existing_page_html,
|
|
104
113
|
chat_history_field,
|
|
105
114
|
convert_to_saltcorn,
|
|
106
115
|
model,
|
|
@@ -113,9 +122,20 @@ module.exports = {
|
|
|
113
122
|
prompt_formula,
|
|
114
123
|
row,
|
|
115
124
|
user,
|
|
116
|
-
"copilot_generate_page prompt formula"
|
|
125
|
+
"copilot_generate_page prompt formula",
|
|
117
126
|
);
|
|
118
127
|
else prompt = row[prompt_field];
|
|
128
|
+
|
|
129
|
+
if(existing_page_html) {
|
|
130
|
+
exist_html = eval_expression(
|
|
131
|
+
existing_page_html,
|
|
132
|
+
row,
|
|
133
|
+
user,
|
|
134
|
+
"copilot_generate_page existing page html",
|
|
135
|
+
);
|
|
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
|
+
}
|
|
138
|
+
|
|
119
139
|
const opts = {};
|
|
120
140
|
|
|
121
141
|
if (model) opts.model = model;
|
|
@@ -136,7 +156,7 @@ module.exports = {
|
|
|
136
156
|
image_prompt,
|
|
137
157
|
row,
|
|
138
158
|
user,
|
|
139
|
-
"copilot_generate_page image prompt"
|
|
159
|
+
"copilot_generate_page image prompt",
|
|
140
160
|
);
|
|
141
161
|
|
|
142
162
|
chat = [];
|
|
@@ -159,8 +179,10 @@ module.exports = {
|
|
|
159
179
|
tools,
|
|
160
180
|
chat,
|
|
161
181
|
systemPrompt,
|
|
162
|
-
});
|
|
163
|
-
const initial_info =
|
|
182
|
+
});
|
|
183
|
+
const initial_info =
|
|
184
|
+
initial_ans.tool_calls[0].input ||
|
|
185
|
+
JSON.parse(initial_ans.tool_calls[0].function.arguments);
|
|
164
186
|
const full = await GeneratePage.follow_on_generate(initial_info);
|
|
165
187
|
const prompt_part_2 = convert_to_saltcorn
|
|
166
188
|
? `Only generate the inner part of the body.
|
|
@@ -196,7 +218,7 @@ module.exports = {
|
|
|
196
218
|
},
|
|
197
219
|
}
|
|
198
220
|
: undefined,
|
|
199
|
-
}
|
|
221
|
+
},
|
|
200
222
|
);
|
|
201
223
|
|
|
202
224
|
const use_page_name = page_name ? interpolate(page_name, row, user) : "";
|
|
@@ -210,7 +232,7 @@ module.exports = {
|
|
|
210
232
|
"text/html",
|
|
211
233
|
wrapExample(page_html),
|
|
212
234
|
user.id,
|
|
213
|
-
100
|
|
235
|
+
100,
|
|
214
236
|
);
|
|
215
237
|
await Page.create({
|
|
216
238
|
name: use_page_name + "_html",
|
|
@@ -225,7 +247,7 @@ module.exports = {
|
|
|
225
247
|
"text/html",
|
|
226
248
|
page_html,
|
|
227
249
|
user.id,
|
|
228
|
-
100
|
|
250
|
+
100,
|
|
229
251
|
);
|
|
230
252
|
layout = { html_file: file.path_to_serve };
|
|
231
253
|
}
|
package/tests/configs.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module.exports = [
|
|
2
|
+
{
|
|
3
|
+
name: "OpenAI completions",
|
|
4
|
+
model: "gpt-4.1-mini",
|
|
5
|
+
api_key: process.env.OPENAI_API_KEY,
|
|
6
|
+
backend: "OpenAI",
|
|
7
|
+
embed_model: "text-embedding-3-small",
|
|
8
|
+
image_model: "gpt-image-1",
|
|
9
|
+
temperature: 0.7,
|
|
10
|
+
responses_api: false,
|
|
11
|
+
ai_sdk_provider: "OpenAI",
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
name: "OpenAI responses",
|
|
15
|
+
model: "gpt-4.1-mini",
|
|
16
|
+
api_key: process.env.OPENAI_API_KEY,
|
|
17
|
+
backend: "OpenAI",
|
|
18
|
+
embed_model: "text-embedding-3-small",
|
|
19
|
+
image_model: "gpt-image-1",
|
|
20
|
+
temperature: 0.7,
|
|
21
|
+
responses_api: true,
|
|
22
|
+
ai_sdk_provider: "OpenAI",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: "AI SDK OpenAI",
|
|
26
|
+
model: "gpt-4.1-mini",
|
|
27
|
+
api_key: process.env.OPENAI_API_KEY,
|
|
28
|
+
backend: "AI SDK",
|
|
29
|
+
embed_model: "text-embedding-3-small",
|
|
30
|
+
image_model: "gpt-image-1",
|
|
31
|
+
temperature: 0.7,
|
|
32
|
+
ai_sdk_provider: "OpenAI",
|
|
33
|
+
},
|
|
34
|
+
];
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const { getState } = require("@saltcorn/data/db/state");
|
|
2
|
+
const View = require("@saltcorn/data/models/view");
|
|
3
|
+
const Table = require("@saltcorn/data/models/table");
|
|
4
|
+
const Plugin = require("@saltcorn/data/models/plugin");
|
|
5
|
+
const Trigger = require("@saltcorn/data/models/trigger");
|
|
6
|
+
const WorkflowStep = require("@saltcorn/data/models/workflow_step");
|
|
7
|
+
const WorkflowRun = require("@saltcorn/data/models/workflow_run");
|
|
8
|
+
|
|
9
|
+
const { mockReqRes } = require("@saltcorn/data/tests/mocks");
|
|
10
|
+
const { afterAll, beforeAll, describe, it, expect } = require("@jest/globals");
|
|
11
|
+
|
|
12
|
+
afterAll(require("@saltcorn/data/db").close);
|
|
13
|
+
beforeAll(async () => {
|
|
14
|
+
await require("@saltcorn/data/db/reset_schema")();
|
|
15
|
+
await require("@saltcorn/data/db/fixtures")();
|
|
16
|
+
|
|
17
|
+
getState().registerPlugin("base", require("@saltcorn/data/base-plugin"));
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
/*
|
|
21
|
+
|
|
22
|
+
RUN WITH:
|
|
23
|
+
saltcorn dev:plugin-test -d ~/copilot -o ~/large-language-model/
|
|
24
|
+
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
jest.setTimeout(60000);
|
|
28
|
+
|
|
29
|
+
const configs = require("./configs.js");
|
|
30
|
+
|
|
31
|
+
for (const nameconfig of configs) {
|
|
32
|
+
const { name, ...config } = nameconfig;
|
|
33
|
+
describe("copilot_generate_page action with " + name, () => {
|
|
34
|
+
beforeAll(async () => {
|
|
35
|
+
getState().registerPlugin(
|
|
36
|
+
"@saltcorn/large-language-model",
|
|
37
|
+
require("@saltcorn/large-language-model"),
|
|
38
|
+
config,
|
|
39
|
+
);
|
|
40
|
+
getState().registerPlugin("@saltcorn/copilot", require(".."));
|
|
41
|
+
});
|
|
42
|
+
it("generates page", async () => {
|
|
43
|
+
const trigger = await Trigger.create({
|
|
44
|
+
action: "Workflow",
|
|
45
|
+
when_trigger: "Never",
|
|
46
|
+
name: "genpagewf",
|
|
47
|
+
});
|
|
48
|
+
await WorkflowStep.create({
|
|
49
|
+
trigger_id: trigger.id,
|
|
50
|
+
name: "first_step",
|
|
51
|
+
next_step: "second_step",
|
|
52
|
+
action_name: "SetContext",
|
|
53
|
+
initial_step: true,
|
|
54
|
+
configuration: {
|
|
55
|
+
ctx_values: `{prompt:"Generate a page for a rural bakery located in a big city"}`,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
await WorkflowStep.create({
|
|
59
|
+
trigger_id: trigger.id,
|
|
60
|
+
name: "second_step",
|
|
61
|
+
next_step: "",
|
|
62
|
+
action_name: "copilot_generate_page",
|
|
63
|
+
configuration: {
|
|
64
|
+
prompt_template: `{{ prompt }}`,
|
|
65
|
+
answer_field: "pagecode",
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
const wfrun = await WorkflowRun.create({
|
|
69
|
+
trigger_id: trigger.id,
|
|
70
|
+
});
|
|
71
|
+
const result = await wfrun.run({ user: { role_id: 1 } });
|
|
72
|
+
//console.log("result", result, wfrun.context);
|
|
73
|
+
expect(result.pagecode).toContain("<!DOCTYPE html>");
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
}
|