@saltcorn/copilot 0.7.5 → 0.8.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/builder-schema.js CHANGED
@@ -41,6 +41,23 @@ const TOOLBOX_BY_MODE = {
41
41
  ],
42
42
  list: [
43
43
  "blank",
44
+ "list_columns",
45
+ "list_column",
46
+ "field",
47
+ "join_field",
48
+ "view_link",
49
+ "action",
50
+ "link",
51
+ "aggregation",
52
+ "view",
53
+ "container",
54
+ "dropdown_menu",
55
+ "line_break",
56
+ ],
57
+ listcolumns: [
58
+ "blank",
59
+ "list_columns",
60
+ "list_column",
44
61
  "field",
45
62
  "join_field",
46
63
  "view_link",
@@ -131,7 +148,6 @@ const buildSegmentDef = ({
131
148
  });
132
149
 
133
150
  const buildBuilderSchema = ({ mode, ctx }) => {
134
- console.log({ ctx });
135
151
  const normalizedMode = (mode || ctx?.mode || "show").toLowerCase();
136
152
  const fields = ctx?.fields || [];
137
153
  const actions = ctx?.actions || [];
@@ -141,9 +157,13 @@ const buildBuilderSchema = ({ mode, ctx }) => {
141
157
  Array.isArray(rawIcons) ? rawIcons : Object.keys(rawIcons)
142
158
  ).slice(0, 15);
143
159
 
144
- const fieldNames = fields.map((f) => f.name).filter(Boolean);
160
+ const isEditMode = normalizedMode === "edit" || normalizedMode === "filter";
161
+ const visibleFields = isEditMode
162
+ ? fields.filter((f) => !f.calculated)
163
+ : fields;
164
+ const fieldNames = visibleFields.map((f) => f.name).filter(Boolean);
145
165
  const fieldviewOptions = Array.from(
146
- new Set(fields.flatMap((f) => f.fieldviews || []).filter(Boolean)),
166
+ new Set(visibleFields.flatMap((f) => f.fieldviews || []).filter(Boolean))
147
167
  );
148
168
 
149
169
  const defs = {
@@ -240,11 +260,11 @@ const buildBuilderSchema = ({ mode, ctx }) => {
240
260
  },
241
261
  imageLocation: makeEnum(
242
262
  IMAGE_LOCATIONS,
243
- "Image location for card/container backgrounds.",
263
+ "Image location for card/container backgrounds."
244
264
  ),
245
265
  imageSize: makeEnum(
246
266
  IMAGE_SIZES,
247
- "Image sizing for Card or Body locations.",
267
+ "Image sizing for Card or Body locations."
248
268
  ),
249
269
  gradStartColor: {
250
270
  type: "string",
@@ -633,7 +653,7 @@ const buildBuilderSchema = ({ mode, ctx }) => {
633
653
  { $ref: "#/$defs/join_field" },
634
654
  { $ref: "#/$defs/list_column" },
635
655
  { $ref: "#/$defs/list_columns" },
636
- { $ref: "#/$defs/page" },
656
+ { $ref: "#/$defs/page" }
637
657
  );
638
658
 
639
659
  Object.assign(defs, {
package/common.js CHANGED
@@ -14,6 +14,24 @@ const MarkdownIt = require("markdown-it"),
14
14
  md = new MarkdownIt();
15
15
  const HTMLParser = require("node-html-parser");
16
16
 
17
+ const getLlmConfigurationSafe = async () => {
18
+ const fn = getState().functions?.llm_get_configuration;
19
+ if (!fn?.run) return null;
20
+ try {
21
+ return await fn.run();
22
+ } catch (err) {
23
+ return null;
24
+ }
25
+ };
26
+
27
+ const canUseResponseFormat = (llmConfig) => {
28
+ const backend = llmConfig?.backend;
29
+ if (!backend) return false;
30
+ if (backend === "AI SDK") return true;
31
+ if (backend === "OpenAI") return !!llmConfig?.responses_api;
32
+ return false;
33
+ };
34
+
17
35
  const boxHandledStyles = new Set([
18
36
  "margin",
19
37
  "margin-top",
@@ -364,4 +382,6 @@ module.exports = {
364
382
  splitContainerStyle,
365
383
  walk_response,
366
384
  parseHTML,
385
+ getLlmConfigurationSafe,
386
+ canUseResponseFormat,
367
387
  };
@@ -3,6 +3,7 @@ const Table = require("@saltcorn/data/models/table");
3
3
  const Form = require("@saltcorn/data/models/form");
4
4
  const View = require("@saltcorn/data/models/view");
5
5
  const Trigger = require("@saltcorn/data/models/trigger");
6
+ const Plugin = require("@saltcorn/data/models/plugin");
6
7
  const { findType } = require("@saltcorn/data/models/discovery");
7
8
  const { save_menu_items } = require("@saltcorn/data/models/config");
8
9
  const db = require("@saltcorn/data/db");
@@ -46,6 +47,11 @@ const get_agent_view = () => {
46
47
  { skill_type: "Database design" },
47
48
  { skill_type: "Generate Workflow" },
48
49
  { skill_type: "Generate View" },
50
+ { skill_type: "Registry editor" },
51
+ { skill_type: "Javascript Action" },
52
+ ...(typeof Plugin.loadAndSaveNewPlugin === "function"
53
+ ? [{ skill_type: "Install Plugin" }]
54
+ : []),
49
55
  ],
50
56
  },
51
57
  });
@@ -64,7 +70,6 @@ const run = async (table_id, viewname, cfg, state, reqres) => {
64
70
  };
65
71
 
66
72
  const interact = async (table_id, viewname, config, body, reqres) => {
67
- console.log("copilot interact with body", body);
68
73
  const view = get_agent_view();
69
74
  return await view.runRoute("interact", body, reqres.res, reqres);
70
75
  };
package/index.js CHANGED
@@ -1,15 +1,27 @@
1
1
  const Workflow = require("@saltcorn/data/models/workflow");
2
2
  const Form = require("@saltcorn/data/models/form");
3
3
  const { features } = require("@saltcorn/data/db/state");
4
+ const db = require("@saltcorn/data/db");
5
+ const Plugin = require("@saltcorn/data/models/plugin");
6
+ const { viewname } = require("./app-constructor/common.js");
7
+
8
+ const headers = [
9
+ {
10
+ script: `/static_assets/${db.connectObj.version_tag}/mermaid.min.js`,
11
+ onlyViews: [viewname],
12
+ },
13
+ ];
4
14
 
5
15
  module.exports = {
6
16
  sc_plugin_api_version: 1,
17
+ headers,
7
18
  dependencies: ["@saltcorn/large-language-model", "@saltcorn/agents"],
8
19
  viewtemplates: features.workflows
9
20
  ? [
10
21
  require("./chat-copilot"),
11
22
  require("./user-copilot"),
12
23
  require("./copilot-as-agent"),
24
+ require("./app-constructor/view.js"),
13
25
  ]
14
26
  : [require("./action-builder"), require("./database-designer")],
15
27
  functions: features.workflows
@@ -17,15 +29,25 @@ module.exports = {
17
29
  copilot_standard_prompt: require("./standard-prompt.js"),
18
30
  copilot_generate_layout: require("./builder-gen.js"),
19
31
  copilot_generate_workflow: require("./workflow-gen"),
32
+ copilot_generate_javascript: require("./js-code-gen.js"),
20
33
  }
21
34
  : {},
22
- actions: { copilot_generate_page: require("./page-gen-action") },
35
+ actions: {
36
+ copilot_generate_page: require("./page-gen-action"),
37
+ app_constructor_feedback: require("./app-constructor/feedback-action.js"),
38
+ },
23
39
  exchange: {
24
40
  agent_skills: [
25
41
  require("./agent-skills/pagegen.js"),
26
42
  require("./agent-skills/database-design.js"),
27
43
  require("./agent-skills/workflow.js"),
28
44
  require("./agent-skills/viewgen.js"),
45
+ require("./agent-skills/registry-editor.js"),
46
+ require("./agent-skills/js-action.js"),
47
+ require("./agent-skills/app-constructor-context.js"),
48
+ ...(typeof Plugin.loadAndSaveNewPlugin === "function"
49
+ ? [require("./agent-skills/install-plugin.js")]
50
+ : []),
29
51
  ],
30
52
  },
31
53
  };
package/js-code-gen.js ADDED
@@ -0,0 +1,65 @@
1
+ const { getState } = require("@saltcorn/data/db/state");
2
+ const { getPromptFromTemplate } = require("./common");
3
+ const Table = require("@saltcorn/data/models/table");
4
+
5
+ const stripCodeFences = (text) => {
6
+ let s = String(text || "").trim();
7
+ s = s.replace(/^```(?:javascript|js|ts|typescript)?\s*\n?/i, "");
8
+ s = s.replace(/\n?```\s*$/i, "");
9
+ return s.trim();
10
+ };
11
+
12
+ const getTableContext = (table_name) => {
13
+ if (!table_name) return null;
14
+ const table = Table.findOne({ name: table_name });
15
+ if (!table) return null;
16
+ const fields = table.getFields ? table.getFields() : table.fields || [];
17
+ return { table, fieldNames: fields.map((f) => f.name) };
18
+ };
19
+
20
+ module.exports = {
21
+ run: async (description, existing_code, table_name) => {
22
+ const systemPrompt = await getPromptFromTemplate(
23
+ "action-builder.txt",
24
+ description
25
+ );
26
+
27
+ const tableCtx = getTableContext(table_name);
28
+ let contextInfo;
29
+ if (tableCtx) {
30
+ contextInfo =
31
+ `\n\nThis action runs in the context of the "${table_name}" table. ` +
32
+ `Available variables: row (with fields: ${tableCtx.fieldNames.join(", ")}), ` +
33
+ `user, table, console, Actions, Table, File, User.`;
34
+ } else {
35
+ contextInfo =
36
+ "\n\nAvailable variables: user, console, Actions, Table, File, User.";
37
+ }
38
+
39
+ let prompt;
40
+ if (existing_code && existing_code.trim()) {
41
+ prompt =
42
+ `Modify the following JavaScript code based on this instruction: ${description}\n\n` +
43
+ `Existing code:\n${existing_code}` +
44
+ `${contextInfo}\n\n` +
45
+ `Return only the modified JavaScript code. Do not include any explanation or markdown code fences.`;
46
+ } else {
47
+ prompt =
48
+ `Generate JavaScript code for the following task: ${description}` +
49
+ `${contextInfo}\n\n` +
50
+ `Only return the JavaScript code. Do not include any explanation or markdown code fences.`;
51
+ }
52
+
53
+ const result = await getState().functions.llm_generate.run(prompt, {
54
+ systemPrompt,
55
+ });
56
+ return stripCodeFences(result);
57
+ },
58
+ isAsync: true,
59
+ description: "Generate JavaScript code for an action",
60
+ arguments: [
61
+ { name: "description", type: "String" },
62
+ { name: "existing_code", type: "String" },
63
+ { name: "table_name", type: "String" },
64
+ ],
65
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saltcorn/copilot",
3
- "version": "0.7.5",
3
+ "version": "0.8.0",
4
4
  "description": "AI assistant for building Saltcorn applications",
5
5
  "main": "index.js",
6
6
  "dependencies": {
@@ -0,0 +1,56 @@
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_layout 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
+ for (const mode of ["page", "show", "edit", "filter"])
43
+ it("generates simple layout in mode " + mode, async () => {
44
+ const genres = await getState().functions.copilot_generate_layout.run(
45
+ "A container with a text element that says Hello World in H3",
46
+ mode,
47
+ mode === "page" ? null : "books",
48
+ );
49
+ const innerRes = genres.above ? genres.above[0] : genres;
50
+ expect(innerRes.type).toBe("container");
51
+ expect(innerRes.contents.type).toBe("blank");
52
+ expect(innerRes.contents.contents).toBe("Hello World");
53
+ expect(innerRes.contents.textStyle).toBe("h3");
54
+ });
55
+ });
56
+ }