@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.
@@ -0,0 +1,128 @@
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 MetaData = require("@saltcorn/data/models/metadata");
5
+ const View = require("@saltcorn/data/models/view");
6
+ const Trigger = require("@saltcorn/data/models/trigger");
7
+ const { findType } = require("@saltcorn/data/models/discovery");
8
+ const { save_menu_items } = require("@saltcorn/data/models/config");
9
+ const db = require("@saltcorn/data/db");
10
+ const WorkflowRun = require("@saltcorn/data/models/workflow_run");
11
+ const { viewname } = require("./common");
12
+
13
+ const runNextTask = async (alwaysRun) => {
14
+ if (!alwaysRun) {
15
+ const settings = await MetaData.findOne({
16
+ type: "CopilotConstructMgr",
17
+ name: "settings",
18
+ });
19
+ if (!settings?.body?.running) return;
20
+ }
21
+ const tasks = await MetaData.find(
22
+ {
23
+ type: "CopilotConstructMgr",
24
+ name: "task",
25
+ },
26
+ { orderBy: "id" }
27
+ );
28
+ const todos = tasks.filter(
29
+ (t) => !t.body.status || t.body.status === "To do"
30
+ );
31
+ const done = tasks.filter((t) => t.body.status === "Done");
32
+ const done_names = new Set(done.map((t) => t.body.name));
33
+
34
+ const startable = todos.filter((t) =>
35
+ t.body.depends_on.every((nm) => done_names.has(nm))
36
+ );
37
+
38
+ if (startable[0]) {
39
+ console.log("running task", startable[0]);
40
+
41
+ return await runTask(startable[0].id, {});
42
+ }
43
+ //not done
44
+ };
45
+
46
+ const runTask = async (md_id, req) => {
47
+ const md = await MetaData.findOne({
48
+ id: md_id,
49
+ });
50
+
51
+ if (!md) return { error: "Task not found" };
52
+ const spec = await MetaData.findOne({
53
+ type: "CopilotConstructMgr",
54
+ name: "spec",
55
+ });
56
+ if (!spec) return { error: "Specification not found" };
57
+ const agent_action = new Trigger({
58
+ action: "Agent",
59
+ when_trigger: "Never",
60
+ configuration: {
61
+ viewname: viewname,
62
+ sys_prompt: "",
63
+ prompt: "{{prompt}}",
64
+ skills: [
65
+ { skill_type: "Generate Page", yoloMode: true },
66
+ { skill_type: "Database design", yoloMode: true },
67
+ { skill_type: "Generate Workflow", yoloMode: true },
68
+ { skill_type: "Generate View", yoloMode: true },
69
+ { skill_type: "Install Plugin", yoloMode: true },
70
+ { skill_type: "AppConstructor Context" },
71
+ ],
72
+ },
73
+ });
74
+ const prompt = `You are engaged in building the following application:
75
+
76
+ Description: ${spec.body.description}
77
+ Audience: ${spec.body.audience}
78
+ Core features: ${spec.body.core_features}
79
+ Out of scope: ${spec.body.out_of_scope}
80
+ Visual style: ${spec.body.visual_style}
81
+
82
+ Important: The database schema is already fully implemented. Do NOT use generate_tables or modify any tables or fields — all tables and fields already exist.
83
+
84
+ Important: Some fields are non-stored (virtual) calculated fields — they have no database column and are computed on-the-fly by Saltcorn. Never include such fields in modify_row, SQL UPDATE statements, or recalculate_stored_fields calls. Only fields that exist as actual database columns (regular fields and stored calculated fields) can be written. If a calculated field needs updating, it will refresh automatically when the fields it depends on change.
85
+
86
+ Your task now is:
87
+ ${md.body.description}`;
88
+
89
+ await md.update({ body: { ...md.body, status: "Running" } });
90
+ const actionres = await agent_action.runWithoutRow({
91
+ row: { prompt },
92
+ req,
93
+ user: req?.user,
94
+ });
95
+ //console.log("actionres", actionres);
96
+ const run_id = actionres.json.run_id;
97
+ const run = await WorkflowRun.findOne({ id: run_id });
98
+ await agent_action.runWithoutRow({
99
+ row: {
100
+ prompt:
101
+ "Write a description of what you did, for the purposes of a progress report. Write 1-4 sentences. Do not use any tools or write any code",
102
+ },
103
+ req,
104
+ run,
105
+ user: req?.user,
106
+ });
107
+ const lastInteraction =
108
+ run.context.interactions[run.context.interactions.length - 1];
109
+ const lastText =
110
+ typeof lastInteraction.content === "string"
111
+ ? lastInteraction.content
112
+ : lastInteraction.content.text
113
+ ? lastInteraction.content.text
114
+ : Array.isArray(lastInteraction.content)
115
+ ? lastInteraction.content[0].text
116
+ : lastInteraction.content;
117
+ await MetaData.create({
118
+ type: "CopilotConstructMgr",
119
+ name: "progress",
120
+ body: { text: lastText, run_id, task_id: md.id },
121
+ user_id: req?.user?.id,
122
+ });
123
+
124
+ //console.log("run", run);
125
+ await md.update({ body: { ...md.body, status: "Done", run_id } });
126
+ };
127
+
128
+ module.exports = { runTask, runNextTask };
@@ -0,0 +1,186 @@
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 MetaData = require("@saltcorn/data/models/metadata");
5
+ const View = require("@saltcorn/data/models/view");
6
+ const Trigger = require("@saltcorn/data/models/trigger");
7
+ const { findType } = require("@saltcorn/data/models/discovery");
8
+ const { save_menu_items } = require("@saltcorn/data/models/config");
9
+ const db = require("@saltcorn/data/db");
10
+ const WorkflowRun = require("@saltcorn/data/models/workflow_run");
11
+ const {
12
+ localeDateTime,
13
+ renderForm,
14
+ mkTable,
15
+ post_delete_btn,
16
+ } = require("@saltcorn/markup");
17
+ const {
18
+ div,
19
+ script,
20
+ domReady,
21
+ pre,
22
+ code,
23
+ input,
24
+ h4,
25
+ style,
26
+ h5,
27
+ button,
28
+ text_attr,
29
+ i,
30
+ p,
31
+ span,
32
+ small,
33
+ form,
34
+ textarea,
35
+ } = require("@saltcorn/markup/tags");
36
+ const { getState } = require("@saltcorn/data/db/state");
37
+ const renderLayout = require("@saltcorn/markup/layout");
38
+ const { viewname, tool_choice } = require("./common");
39
+ const { requirements_tool } = require("./tools");
40
+ const { saltcorn_description, existing_tables_list } = require("./prompts");
41
+ const GenerateTables = require("../actions/generate-tables");
42
+ const GenerateTablesSkill = require("../agent-skills/database-design");
43
+
44
+ const showSchema = async (req) => {
45
+ const schema = await MetaData.findOne({
46
+ type: "CopilotConstructMgr",
47
+ name: "schema",
48
+ });
49
+
50
+ if (schema) {
51
+ const preview = GenerateTables.render_html(
52
+ { tables: schema.body.tables },
53
+ true
54
+ );
55
+
56
+ return div(
57
+ { class: "mt-2" },
58
+ preview,
59
+ !schema.body.implemented &&
60
+ div(
61
+ { class: "mb-4 d-block mt-3" },
62
+ button(
63
+ {
64
+ class: "btn btn-primary me-2",
65
+ onclick: `view_post("${viewname}", "implement_schema")`,
66
+ },
67
+ "Implement schema"
68
+ ),
69
+ button(
70
+ {
71
+ class: "btn btn-outline-danger",
72
+ onclick: `view_post("${viewname}", "del_schema")`,
73
+ },
74
+ "Delete schema"
75
+ )
76
+ )
77
+ );
78
+ } else {
79
+ return div(
80
+ { class: "mt-2" },
81
+ p("Schema not found"),
82
+ button(
83
+ {
84
+ class: "btn btn-primary",
85
+ onclick: `press_store_button(this);view_post("${viewname}", "gen_schema")`,
86
+ },
87
+ "Generate schema"
88
+ )
89
+ );
90
+ }
91
+ };
92
+
93
+ const gen_schema = async (table_id, viewname, config, body, { req, res }) => {
94
+ const spec = await MetaData.findOne({
95
+ type: "CopilotConstructMgr",
96
+ name: "spec",
97
+ });
98
+ if (!spec) throw new Error("Specification not found");
99
+ const rs = await MetaData.find({
100
+ type: "CopilotConstructMgr",
101
+ name: "requirement",
102
+ });
103
+ if (!rs.length) throw new Error("No requirements found");
104
+
105
+ const databaseDesignTool = new GenerateTablesSkill({}).provideTools();
106
+ const existing_tables = await Table.find({});
107
+ const answer = await getState().functions.llm_generate.run(
108
+ `Generate the database schema for this application:
109
+
110
+ Description: ${spec.body.description}
111
+ Audience: ${spec.body.audience}
112
+ Core features: ${spec.body.core_features}
113
+ Out of scope: ${spec.body.out_of_scope}
114
+ Visual style: ${spec.body.visual_style}
115
+
116
+ These are the requirements of the application:
117
+
118
+ ${rs.map((r) => `* ${r.body.requirement}`).join("\n")}
119
+
120
+ ${saltcorn_description}
121
+
122
+ ${existing_tables_list(existing_tables)}
123
+
124
+ Design a complete database schema that covers ALL requirements listed above. Every distinct entity in the application must have its own table. Do not produce a minimal or partial schema — all tables needed to implement every requirement must be included in this single call. Do not leave any tables for a later step.
125
+
126
+ For every field that must be unique (e.g. unique email, unique slug, unique combination keys expressed as individual unique fields), set unique=true on that field.
127
+ For every field that must not be empty, set not_null=true.
128
+ Do NOT leave uniqueness or required constraints for a later step — express them fully in this schema.
129
+
130
+ Note: ownership configuration (automatically populating a FK-to-users field from the logged-in user) is a VIEW-level concern and cannot be expressed in the schema. Do not attempt to annotate fields as "ownership fields" here — simply define the foreign key field normally. Ownership will be configured when the Edit views are generated.
131
+
132
+ Now use the ${
133
+ databaseDesignTool.function.name
134
+ } tool to generate the complete database schema for this software application
135
+ `,
136
+ {
137
+ tools: [databaseDesignTool],
138
+ ...tool_choice(databaseDesignTool.function.name),
139
+ systemPrompt:
140
+ "You are a database designer. The user wants to build an application, and you must analyse their application description and requirements and design a complete schema. Every entity needed by any requirement must have its own table. Never produce a partial schema.",
141
+ }
142
+ );
143
+
144
+ const tc = answer.getToolCalls()[0];
145
+
146
+ await MetaData.create({
147
+ type: "CopilotConstructMgr",
148
+ name: "schema",
149
+ body: { tables: tc.input.tables, implemented: false },
150
+ user_id: req.user?.id,
151
+ });
152
+ return { json: { reload_page: true } };
153
+ };
154
+
155
+ const del_schema = async (table_id, viewname, config, body, { req, res }) => {
156
+ const rs = await MetaData.find({
157
+ type: "CopilotConstructMgr",
158
+ name: "schema",
159
+ });
160
+ for (const r of rs) await r.delete();
161
+ return { json: { reload_page: true } };
162
+ };
163
+
164
+ const implement_schema = async (
165
+ table_id,
166
+ viewname,
167
+ config,
168
+ body,
169
+ { req, res }
170
+ ) => {
171
+ const md = await MetaData.findOne({
172
+ type: "CopilotConstructMgr",
173
+ name: "schema",
174
+ });
175
+
176
+ const { apply_copilot_tables } = new GenerateTablesSkill({}).userActions;
177
+ await apply_copilot_tables({ tables: md.body.tables, user: req.user });
178
+ md.body.implemented = true;
179
+ await md.update({ body: md.body });
180
+
181
+ return { json: { reload_page: true } };
182
+ };
183
+
184
+ const schema_routes = { gen_schema, del_schema, implement_schema };
185
+
186
+ module.exports = { showSchema, schema_routes };
@@ -0,0 +1,70 @@
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 MetaData = require("@saltcorn/data/models/metadata");
5
+ const View = require("@saltcorn/data/models/view");
6
+ const Trigger = require("@saltcorn/data/models/trigger");
7
+ const { findType } = require("@saltcorn/data/models/discovery");
8
+ const { save_menu_items } = require("@saltcorn/data/models/config");
9
+ const db = require("@saltcorn/data/db");
10
+ const WorkflowRun = require("@saltcorn/data/models/workflow_run");
11
+ const {
12
+ localeDateTime,
13
+ renderForm,
14
+ mkTable,
15
+ post_delete_btn,
16
+ } = require("@saltcorn/markup");
17
+ const {
18
+ div,
19
+ script,
20
+ domReady,
21
+ pre,
22
+ code,
23
+ input,
24
+ h4,
25
+ style,
26
+ h5,
27
+ button,
28
+ text_attr,
29
+ i,
30
+ p,
31
+ span,
32
+ small,
33
+ a,
34
+ textarea,
35
+ } = require("@saltcorn/markup/tags");
36
+ const { getState } = require("@saltcorn/data/db/state");
37
+ const renderLayout = require("@saltcorn/markup/layout");
38
+ const { viewname } = require("./common");
39
+ const { runTask, runNextTask } = require("./run_task");
40
+
41
+ const makeTaskChart = async (req) => {
42
+ const rs = await MetaData.find(
43
+ {
44
+ type: "CopilotConstructMgr",
45
+ name: "task",
46
+ },
47
+ { orderBy: "written_at" },
48
+ );
49
+
50
+ const taskIds = {};
51
+ rs.forEach((md) => {
52
+ taskIds[md.body.name] = md.id;
53
+ });
54
+ return div(
55
+ pre({
56
+ class: "mermaid",
57
+ "mm-src": `flowchart LR
58
+ ${rs.map((md) => ` task${md.id}["${md.body.name}"]`).join("\n")}
59
+ ${rs.map((md) => (md.body.depends_on || []).map((depon) => ` task${taskIds[depon]} --> task${md.id}`).join("\n")).join("\n")}
60
+ ${rs
61
+ .filter((m) => m.body.status === "Done")
62
+ .map((md) => ` style task${md.id} fill:#777`)
63
+ .join("\n")}
64
+
65
+ `,
66
+ }),
67
+ );
68
+ };
69
+
70
+ module.exports = { makeTaskChart };