@saltcorn/copilot 0.7.4 → 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/actions/generate-js-action.js +43 -5
- package/actions/generate-tables.js +281 -96
- package/actions/generate-workflow.js +83 -36
- package/actions/install-plugin-action.js +103 -0
- package/agent-skills/app-constructor-context.js +25 -0
- package/agent-skills/database-design.js +139 -87
- package/agent-skills/install-plugin.js +111 -0
- package/agent-skills/js-action.js +183 -0
- package/agent-skills/registry-editor.js +911 -0
- package/agent-skills/viewgen.js +302 -53
- package/agent-skills/workflow.js +52 -2
- package/app-constructor/common.js +12 -0
- package/app-constructor/errors.js +102 -0
- package/app-constructor/feedback-action.js +175 -0
- package/app-constructor/feedback.js +112 -0
- package/app-constructor/progress.js +116 -0
- package/app-constructor/prompts.js +58 -0
- package/app-constructor/requirements.js +156 -0
- package/app-constructor/run_task.js +128 -0
- package/app-constructor/schema.js +186 -0
- package/app-constructor/taskchart.js +70 -0
- package/app-constructor/tasks.js +401 -0
- package/app-constructor/tools.js +81 -0
- package/app-constructor/view.js +209 -0
- package/builder-gen.js +1505 -24
- package/builder-schema.js +706 -0
- package/common.js +20 -0
- package/copilot-as-agent.js +6 -1
- package/index.js +24 -1
- package/js-code-gen.js +65 -0
- package/package.json +1 -1
- package/standard-prompt.js +83 -0
- package/tests/builder-gen.test.js +56 -0
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
|
};
|
package/copilot-as-agent.js
CHANGED
|
@@ -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,30 +1,53 @@
|
|
|
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
|
|
16
28
|
? {
|
|
29
|
+
copilot_standard_prompt: require("./standard-prompt.js"),
|
|
17
30
|
copilot_generate_layout: require("./builder-gen.js"),
|
|
18
31
|
copilot_generate_workflow: require("./workflow-gen"),
|
|
32
|
+
copilot_generate_javascript: require("./js-code-gen.js"),
|
|
19
33
|
}
|
|
20
34
|
: {},
|
|
21
|
-
actions: {
|
|
35
|
+
actions: {
|
|
36
|
+
copilot_generate_page: require("./page-gen-action"),
|
|
37
|
+
app_constructor_feedback: require("./app-constructor/feedback-action.js"),
|
|
38
|
+
},
|
|
22
39
|
exchange: {
|
|
23
40
|
agent_skills: [
|
|
24
41
|
require("./agent-skills/pagegen.js"),
|
|
25
42
|
require("./agent-skills/database-design.js"),
|
|
26
43
|
require("./agent-skills/workflow.js"),
|
|
27
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
|
+
: []),
|
|
28
51
|
],
|
|
29
52
|
},
|
|
30
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
|
@@ -0,0 +1,83 @@
|
|
|
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
|
+
const { getPromptFromTemplate } = require("./common");
|
|
7
|
+
|
|
8
|
+
const scTypeToTsType = (type, field) => {
|
|
9
|
+
if (field?.is_fkey) {
|
|
10
|
+
if (field.reftype) return scTypeToTsType(field.reftype);
|
|
11
|
+
}
|
|
12
|
+
return (
|
|
13
|
+
{
|
|
14
|
+
String: "string",
|
|
15
|
+
Integer: "number",
|
|
16
|
+
Float: "number",
|
|
17
|
+
Bool: "boolean",
|
|
18
|
+
Date: "Date",
|
|
19
|
+
HTML: "string",
|
|
20
|
+
}[type?.name || type] || "any"
|
|
21
|
+
);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
module.exports = {
|
|
25
|
+
run: async ({ table, language, has_table, has_functions }) => {
|
|
26
|
+
if (language === "javascript") {
|
|
27
|
+
let prompts = [``];
|
|
28
|
+
if (has_table)
|
|
29
|
+
prompts.push(await getPromptFromTemplate("action-builder.txt", ""));
|
|
30
|
+
if (!has_table)
|
|
31
|
+
prompts.push(`Your code can can manipulate rows in the database, manipulate files, interact
|
|
32
|
+
with remote APIs, or issue directives for the user's display.
|
|
33
|
+
|
|
34
|
+
Your code can use await at the top level, and should do so whenever calling
|
|
35
|
+
database queries or other aynchronous code (see examples below)
|
|
36
|
+
`);
|
|
37
|
+
if (has_functions) {
|
|
38
|
+
const ds = [];
|
|
39
|
+
for (const [nm, f] of Object.entries(getState().functions)) {
|
|
40
|
+
const comment = f.description ? " // " + f.description : "";
|
|
41
|
+
const returns =
|
|
42
|
+
f.returns || f.tsreturns
|
|
43
|
+
? ": " + (f.tsreturns || scTypeToTsType(f.returns))
|
|
44
|
+
: "";
|
|
45
|
+
if (nm === "today") {
|
|
46
|
+
ds.push(
|
|
47
|
+
`function today(offset_days?: number | {startOf: "year" | "quarter" | "month" | "week" | "day" | "hour"} | {endOf: "year" | "quarter" | "month" | "week" | "day" | "hour"}): Date`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
if (nm === "slugify") {
|
|
51
|
+
ds.push(`function slugify(s: string): string`);
|
|
52
|
+
} else if (f.run) {
|
|
53
|
+
if (f["arguments"]) {
|
|
54
|
+
const args = (f["arguments"] || []).map(
|
|
55
|
+
({ name, type, tstype, required }) =>
|
|
56
|
+
`${name}${required ? "" : "?"}: ${tstype || scTypeToTsType(type)}`,
|
|
57
|
+
);
|
|
58
|
+
ds.push(
|
|
59
|
+
`${f.isAsync ? "async " : ""}function ${nm}(${args.join(", ")})${returns}${comment}`,
|
|
60
|
+
);
|
|
61
|
+
} else
|
|
62
|
+
ds.push(
|
|
63
|
+
`declare var ${nm}: ${f.isAsync ? "AsyncFunction" : "Function"}${comment}`,
|
|
64
|
+
);
|
|
65
|
+
} else ds.push(`declare const ${nm}: Function;${comment}`);
|
|
66
|
+
}
|
|
67
|
+
prompts.push(`You can also call some functions, here with TypeScript declarations (although you are writing JavaScript):
|
|
68
|
+
|
|
69
|
+
${ds.join("\n")}`);
|
|
70
|
+
}
|
|
71
|
+
return prompts.join("\n");
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
isAsync: true,
|
|
75
|
+
description: "Return a standard prompt for writing code",
|
|
76
|
+
arguments: [
|
|
77
|
+
{
|
|
78
|
+
name: "options",
|
|
79
|
+
type: "JSON",
|
|
80
|
+
tstype: `{language: "javascript", table?:string, has_table?: boolean, has_functions?:boolean}`,
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
};
|
|
@@ -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
|
+
}
|