@saltcorn/copilot 0.6.2 → 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.
@@ -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/common.js CHANGED
@@ -7,6 +7,7 @@ const Form = require("@saltcorn/data/models/form");
7
7
  const View = require("@saltcorn/data/models/view");
8
8
  const Trigger = require("@saltcorn/data/models/trigger");
9
9
  const { getState } = require("@saltcorn/data/db/state");
10
+ const voidHtmlTags = new Set(require("html-tags/void"));
10
11
 
11
12
  const parseCSS = require("style-to-object").default;
12
13
  const MarkdownIt = require("markdown-it"),
@@ -259,13 +260,6 @@ function parseHTML(str, processAll) {
259
260
  .join("")}</style>`,
260
261
  text_strings: [node.childNodes.map((n) => n.toString()).join("")],
261
262
  };
262
- case "input":
263
- return {
264
- type: "blank",
265
- isHTML: true,
266
- contents: node.toString(),
267
- text_strings: [],
268
- };
269
263
  case "a":
270
264
  return {
271
265
  type: "link",
@@ -331,15 +325,23 @@ function parseHTML(str, processAll) {
331
325
  : node.childNodes.length === 1
332
326
  ? go(node.childNodes[0]) || ""
333
327
  : { above: node.childNodes.map(go).filter(Boolean) };
334
- return {
335
- type: "container",
336
- ...(node.rawTagName && node.rawTagName !== "div"
337
- ? { htmlElement: node.rawTagName }
338
- : {}),
339
- ...(node.id ? { customId: node.id } : {}),
340
- customClass: (node.classList.value || []).join(" "),
341
- contents: containerContents,
342
- };
328
+ if (voidHtmlTags.has(node.rawTagName))
329
+ return {
330
+ type: "blank",
331
+ isHTML: true,
332
+ contents: node.toString(),
333
+ text_strings: [],
334
+ };
335
+ else
336
+ return {
337
+ type: "container",
338
+ ...(node.rawTagName && node.rawTagName !== "div"
339
+ ? { htmlElement: node.rawTagName }
340
+ : {}),
341
+ ...(node.id ? { customId: node.id } : {}),
342
+ customClass: (node.classList.value || []).join(" "),
343
+ contents: containerContents,
344
+ };
343
345
  }
344
346
  } else if (node.constructor.name === "TextNode") {
345
347
  if (!node._rawText || !node._rawText.trim()) return null;
package/index.js CHANGED
@@ -12,4 +12,7 @@ module.exports = {
12
12
  ? { copilot_generate_workflow: require("./workflow-gen") }
13
13
  : {},
14
14
  actions: { copilot_generate_page: require("./page-gen-action") },
15
+ exchange: {
16
+ agent_skills: [require("./agent-skills/pagegen.js")],
17
+ },
15
18
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saltcorn/copilot",
3
- "version": "0.6.2",
3
+ "version": "0.7.0",
4
4
  "description": "AI assistant for building Saltcorn applications",
5
5
  "main": "index.js",
6
6
  "dependencies": {
@@ -9,7 +9,14 @@
9
9
  "node-sql-parser": "4.15.0",
10
10
  "markdown-it": "14.1.0",
11
11
  "style-to-object": "1.0.8",
12
- "node-html-parser": "7.0.1"
12
+ "node-html-parser": "7.0.1",
13
+ "html-tags": "3.3.1"
14
+ },
15
+ "devDependencies": {
16
+ "jest": "^29.7.0"
17
+ },
18
+ "scripts": {
19
+ "test": "jest tests --runInBand"
13
20
  },
14
21
  "author": "Tom Nielsen",
15
22
  "license": "MIT",
@@ -21,7 +28,8 @@
21
28
  },
22
29
  "env": {
23
30
  "node": true,
24
- "es6": true
31
+ "es6": true,
32
+ "jest/globals": true
25
33
  },
26
34
  "rules": {
27
35
  "no-unused-vars": "off",
@@ -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 = initial_ans.tool_calls[0].input;
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
  }
@@ -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
+ }