@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,401 @@
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, tool_choice } = require("./common");
39
+ const { runTask, runNextTask } = require("./run_task");
40
+ const { task_tool } = require("./tools");
41
+ const { saltcorn_description, existing_tables_list } = require("./prompts");
42
+
43
+ const makeTaskList = async (req) => {
44
+ const rs = await MetaData.find(
45
+ {
46
+ type: "CopilotConstructMgr",
47
+ name: "task",
48
+ },
49
+ { orderBy: "written_at" }
50
+ );
51
+ const settings = await MetaData.findOne({
52
+ type: "CopilotConstructMgr",
53
+ name: "settings",
54
+ });
55
+ const running = !!settings?.body?.running;
56
+ const status = div(
57
+ running ? "Currently running" : "Currently not running",
58
+ running
59
+ ? button(
60
+ {
61
+ class: "btn btn-danger ms-2",
62
+ onclick: `view_post("${viewname}", "stop", {})`,
63
+ },
64
+ i({ class: "fas fa-stop me-1" }),
65
+ "Stop running"
66
+ )
67
+ : button(
68
+ {
69
+ class: "btn btn-success ms-2",
70
+ onclick: `view_post("${viewname}", "start", {})`,
71
+ },
72
+ i({ class: "fas fa-play me-1" }),
73
+ "Start running now"
74
+ ),
75
+ button(
76
+ {
77
+ class: "btn btn-outline-success ms-2",
78
+ onclick: `press_store_button(this);view_post("${viewname}", "run_task", {})`,
79
+ },
80
+ i({ class: "fas fa-play me-1" }),
81
+ "Run next task"
82
+ )
83
+ );
84
+ if (rs.length) {
85
+ return div(
86
+ { class: "mt-2" },
87
+ status,
88
+ mkTable(
89
+ [
90
+ { label: "Name", key: (m) => m.body.name },
91
+ { label: "Description", key: (m) => m.body.description },
92
+ {
93
+ label: "Depends on",
94
+ key: (m) => (m.body.depends_on || []).join(", "),
95
+ },
96
+ { label: "Priority", key: (m) => m.body.priority },
97
+ { label: "Status", key: (m) => m.body.status || "To do" },
98
+ {
99
+ label: "Run",
100
+ key: (r) =>
101
+ r.body.status === "Running"
102
+ ? span(
103
+ {
104
+ class: "task-spinner",
105
+ "data-task-id": r.id,
106
+ },
107
+ i({ class: "fas fa-spinner fa-spin text-warning" })
108
+ )
109
+ : r.body.status === "Done"
110
+ ? r.body.run_id
111
+ ? a(
112
+ {
113
+ target: "_blank",
114
+ href: `/view/Saltcorn%20Agent%20copilot?run_id=${r.body.run_id}`,
115
+ },
116
+ i({ class: "fas fa-external-link-alt" })
117
+ )
118
+ : ""
119
+ : button(
120
+ {
121
+ class: "btn btn-outline-success btn-sm",
122
+ onclick: `press_store_button(this);view_post("${viewname}", "run_task", {id:${r.id}})`,
123
+ },
124
+ i({ class: "fas fa-play" })
125
+ ),
126
+ },
127
+ {
128
+ label: "",
129
+ key: (r) =>
130
+ r.body.status === "To do" || !r.body.status
131
+ ? button(
132
+ {
133
+ class: "btn btn-outline-secondary btn-sm",
134
+ title: "Mark as done without running",
135
+ onclick: `view_post("${viewname}", "mark_done_task", {id:${r.id}})`,
136
+ },
137
+ i({ class: "fas fa-check" })
138
+ )
139
+ : "",
140
+ },
141
+ {
142
+ label: "Delete",
143
+ key: (r) =>
144
+ button(
145
+ {
146
+ class: "btn btn-outline-danger btn-sm",
147
+ onclick: `view_post("${viewname}", "del_task", {id:${r.id}})`,
148
+ },
149
+ i({ class: "fas fa-trash-alt" })
150
+ ),
151
+ },
152
+ ],
153
+ {
154
+ "To do": rs.filter(
155
+ (t) => !t.body.status || t.body.status === "To do"
156
+ ),
157
+ Running: rs.filter((t) => t.body.status === "Running"),
158
+ Done: rs.filter((t) => t.body.status === "Done"),
159
+ },
160
+ { grouped: true }
161
+ ),
162
+ rs.some((t) => t.body.status === "Running")
163
+ ? script(
164
+ domReady(`
165
+ (function() {
166
+ function pollTasks() {
167
+ var spinners = document.querySelectorAll('.task-spinner[data-task-id]');
168
+ if (!spinners.length) return;
169
+ var ids = Array.from(spinners).map(function(el) { return el.getAttribute('data-task-id'); });
170
+ view_post(${JSON.stringify(
171
+ viewname
172
+ )}, 'task_status', { ids: ids }, function(resp) {
173
+ if (resp && resp.any_done) {
174
+ location.reload();
175
+ } else {
176
+ setTimeout(pollTasks, 3000);
177
+ }
178
+ });
179
+ }
180
+ setTimeout(pollTasks, 3000);
181
+ })();
182
+ `)
183
+ )
184
+ : "",
185
+ button(
186
+ {
187
+ class: "btn btn-outline-danger mb-4",
188
+ onclick: `view_post("${viewname}", "del_all_tasks")`,
189
+ },
190
+ "Delete all"
191
+ )
192
+ );
193
+ } else {
194
+ return div(
195
+ { class: "mt-2" },
196
+ p("No tasks found"),
197
+ button(
198
+ {
199
+ class: "btn btn-primary",
200
+ onclick: `press_store_button(this);view_post("${viewname}", "gen_tasks")`,
201
+ },
202
+ "Plan tasks"
203
+ )
204
+ );
205
+ }
206
+ };
207
+
208
+ const gen_tasks = async (table_id, viewname, config, body, { req, res }) => {
209
+ const spec = await MetaData.findOne({
210
+ type: "CopilotConstructMgr",
211
+ name: "spec",
212
+ });
213
+ if (!spec) throw new Error("Specification not found");
214
+ const rs = await MetaData.find({
215
+ type: "CopilotConstructMgr",
216
+ name: "requirement",
217
+ });
218
+ if (!rs.length) throw new Error("No requirements found");
219
+ const schema = await MetaData.findOne({
220
+ type: "CopilotConstructMgr",
221
+ name: "schema",
222
+ });
223
+ if (!schema) throw new Error("No schema found");
224
+ if (!schema.body.implemented) throw new Error("Schema not implemented");
225
+ const tables = await Table.find({});
226
+
227
+ const answer = await getState().functions.llm_generate.run(
228
+ `Generate a plan for building this application:
229
+
230
+ Description: ${spec.body.description}
231
+ Audience: ${spec.body.audience}
232
+ Core features: ${spec.body.core_features}
233
+ Out of scope: ${spec.body.out_of_scope}
234
+ Visual style: ${spec.body.visual_style}
235
+
236
+ These are the requirements of the application:
237
+
238
+ ${rs.map((r) => `* ${r.body.requirement}`).join("\n")}
239
+
240
+ ${saltcorn_description}
241
+
242
+ The database has already been built. The following tables are now present in the database:
243
+
244
+ ${existing_tables_list(tables)}
245
+
246
+ The plan should outline the continued development of the application on top of this database.
247
+ Your plan can add additional tables if needed or adjust the table fields, but normally the tables
248
+ should be designed optimally for this application.
249
+
250
+ The plan should focus on building views, triggers (including workflows) and pages.
251
+
252
+ Important view planning rules:
253
+ * Do NOT plan separate tasks for "create" and "edit" on the same table. In Saltcorn, a single Edit view handles both (no id = create, id present = edit). Plan one task covering both, and write task descriptions that say "create and edit" rather than treating them as two separate items.
254
+ * When a table has foreign key fields referencing the users table, the task description must explicitly state for each one whether it is an ownership field (automatically set from the logged-in user, omit from the form) or a selector field (the user picks a value, include a selector in the form). Example: "user_id records the owner and is set automatically; shared_with_user_id must have a user selector."
255
+ * For FK fields that represent a parent context (e.g. trip_id on packing_items), always include the field as a normal selector in the Edit view form. Do NOT say to omit it. Saltcorn automatically pre-fills the selector from the URL query parameter when the view is opened from a parent context, and the user can select it manually when the view is used standalone.
256
+ * A List view that includes an edit link (viewlink column pointing to an Edit view) depends on that Edit view already existing. Always plan the Edit view task before the List view task, and set the List view task's depends_on to include the Edit view task name.
257
+ * If a List view includes a viewlink to show record details (a Show view), the Show view must be created as a separate task before the List view task, and the List view task must depend on it. A Show view is a distinct viewtemplate — do NOT use an Edit view or a page as a substitute for it. The Show view and Edit view for the same table are independent — neither depends on the other; they can be planned in any order or in parallel.
258
+ * In general, if a view embeds or links to another view, the linked view must be created first and listed as a dependency.
259
+
260
+ * For every task that creates a view, include the exact view name in the task description. View names must be lowercase, snake_case, unique across all tasks in the plan (no two tasks may produce a view with the same name), and descriptive enough to identify the table and purpose — for example 'packing_items_edit' rather than just 'edit'.
261
+
262
+ Important schema/table rules:
263
+ * The database schema is already fully designed and implemented before task planning begins. ALL tables and fields needed by the application already exist. Do NOT plan any tasks that create tables, add fields, modify fields, or change the schema in any way. If you find yourself writing a task whose output is a table or a field, delete it — that work is already done.
264
+ * Ownership behaviour (auto-setting a FK-to-users field from the logged-in user) is configured in the Edit view, not in the database. Do not create tasks for it at the schema level.
265
+ * Do NOT plan tasks to add uniqueness constraints or validation to existing fields — those are already in the schema.
266
+
267
+ Your plan should not include any clarification or questions to the product owner. The
268
+ information you have been given so far is all that is available. Every step in the plan
269
+ should be immediately implementable in Saltcorn. You are writing the steps in the plan
270
+ for a person who is competent in using saltcorn but has no other business knowledge.
271
+
272
+ Do not include any steps that contain planning, design or review instructions. You are only writing a
273
+ plan for the engineer building the application. Every step in the plan should have the construction or the modification
274
+ of one or several application entity types.
275
+
276
+ Now use the plan_tasks tool to make a plan of tasks for building software application
277
+ `,
278
+ {
279
+ tools: [task_tool],
280
+ ...tool_choice("plan_tasks"),
281
+ systemPrompt:
282
+ "You are a project manager. The user wants to build an application, and you must analyse their application description",
283
+ }
284
+ );
285
+
286
+ const tc = answer.getToolCalls()[0];
287
+
288
+ for (const task of tc.input.tasks)
289
+ await MetaData.create({
290
+ type: "CopilotConstructMgr",
291
+ name: "task",
292
+ body: task,
293
+ user_id: req.user?.id,
294
+ });
295
+ return { json: { reload_page: true } };
296
+ };
297
+
298
+ const del_task = async (table_id, viewname, config, body, { req, res }) => {
299
+ const r = await MetaData.findOne({
300
+ id: body.id,
301
+ });
302
+
303
+ if (!r) throw new Error("Task not found");
304
+ await r.delete();
305
+ return { json: { reload_page: true } };
306
+ };
307
+ const run_task = async (table_id, viewname, config, body, { req, res }) => {
308
+ const reqUser = req?.user;
309
+ setImmediate(async () => {
310
+ try {
311
+ if (body.id) await runTask(body.id, { user: reqUser, __: req.__ });
312
+ else await runNextTask(true);
313
+ } catch (e) {
314
+ console.error("run_task background error", e);
315
+ }
316
+ });
317
+ return { json: { reload_page: true } };
318
+ };
319
+
320
+ const task_status = async (table_id, viewname, config, body, { req, res }) => {
321
+ const ids = body.ids || [];
322
+ const tasks = await MetaData.find({
323
+ type: "CopilotConstructMgr",
324
+ name: "task",
325
+ });
326
+ const relevant = tasks.filter((t) => ids.includes(String(t.id)));
327
+ const any_done = relevant.some((t) => t.body.status !== "Running");
328
+ return { json: { any_done } };
329
+ };
330
+
331
+ const start = async (table_id, viewname, config, body, { req, res }) => {
332
+ const settings = await MetaData.findOne({
333
+ type: "CopilotConstructMgr",
334
+ name: "settings",
335
+ });
336
+ if (settings)
337
+ await settings.update({ body: { ...settings.body, running: true } });
338
+ else
339
+ await MetaData.create({
340
+ type: "CopilotConstructMgr",
341
+ name: "settings",
342
+ body: { running: true },
343
+ });
344
+ return { json: { reload_page: true } };
345
+ };
346
+ const stop = async (table_id, viewname, config, body, { req, res }) => {
347
+ const settings = await MetaData.findOne({
348
+ type: "CopilotConstructMgr",
349
+ name: "settings",
350
+ });
351
+ if (settings)
352
+ await settings.update({ body: { ...settings.body, running: false } });
353
+ else
354
+ await MetaData.create({
355
+ type: "CopilotConstructMgr",
356
+ name: "settings",
357
+ body: { running: false },
358
+ });
359
+ return { json: { reload_page: true } };
360
+ };
361
+
362
+ const mark_done_task = async (
363
+ table_id,
364
+ viewname,
365
+ config,
366
+ body,
367
+ { req, res }
368
+ ) => {
369
+ const r = await MetaData.findOne({ id: body.id });
370
+ if (!r) throw new Error("Task not found");
371
+ await r.update({ body: { ...r.body, status: "Done" } });
372
+ return { json: { reload_page: true } };
373
+ };
374
+
375
+ const del_all_tasks = async (
376
+ table_id,
377
+ viewname,
378
+ config,
379
+ body,
380
+ { req, res }
381
+ ) => {
382
+ const rs = await MetaData.find({
383
+ type: "CopilotConstructMgr",
384
+ name: "task",
385
+ });
386
+ for (const r of rs) await r.delete();
387
+ return { json: { reload_page: true } };
388
+ };
389
+
390
+ const task_routes = {
391
+ gen_tasks,
392
+ del_task,
393
+ del_all_tasks,
394
+ mark_done_task,
395
+ run_task,
396
+ task_status,
397
+ start,
398
+ stop,
399
+ };
400
+
401
+ module.exports = { makeTaskList, task_routes };
@@ -0,0 +1,81 @@
1
+ const requirements_tool = {
2
+ type: "function",
3
+ function: {
4
+ name: "make_requirements",
5
+ description: "Provide a list of requirements for the application",
6
+ parameters: {
7
+ type: "object",
8
+ required: ["requirements"],
9
+ additionalProperties: false,
10
+ properties: {
11
+ requirements: {
12
+ type: "array",
13
+ items: {
14
+ type: "object",
15
+ required: ["requirement", "priority"],
16
+ additionalProperties: false,
17
+ properties: {
18
+ requirement: {
19
+ type: "string",
20
+ description: "A statement of the requirement",
21
+ },
22
+ priority: {
23
+ type: "number",
24
+ description:
25
+ "Priority 1-5. 5: Most important, 1: Least important",
26
+ },
27
+ },
28
+ },
29
+ },
30
+ },
31
+ },
32
+ },
33
+ };
34
+
35
+ const task_tool = {
36
+ type: "function",
37
+ function: {
38
+ name: "plan_tasks",
39
+ description: "Provide a series of tasks for building the application",
40
+ parameters: {
41
+ type: "object",
42
+ required: ["tasks"],
43
+ additionalProperties: false,
44
+ properties: {
45
+ tasks: {
46
+ type: "array",
47
+ items: {
48
+ type: "object",
49
+ required: ["requirement", "priority"],
50
+ additionalProperties: false,
51
+ properties: {
52
+ name: {
53
+ type: "string",
54
+ description: "A short name for the task",
55
+ },
56
+ description: {
57
+ type: "string",
58
+ description: "A full description of the task",
59
+ },
60
+ priority: {
61
+ type: "number",
62
+ description:
63
+ "Priority 1-5. 5: Most important, 1: Least important",
64
+ },
65
+ depends_on: {
66
+ type: "array",
67
+ description:
68
+ "The names of the tasks that must be completed before this tasks can be started",
69
+ items: {
70
+ type: "string",
71
+ },
72
+ },
73
+ },
74
+ },
75
+ },
76
+ },
77
+ },
78
+ },
79
+ };
80
+
81
+ module.exports = { requirements_tool,task_tool };
@@ -0,0 +1,209 @@
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 } = require("./common");
39
+ const { requirementsList, req_routes } = require("./requirements");
40
+ const { showSchema, schema_routes } = require("./schema");
41
+ const { makeTaskList, task_routes } = require("./tasks");
42
+ const { errorList, error_routes } = require("./errors");
43
+ const { feedbackList, feedback_routes } = require("./feedback");
44
+ const { progressList, progress_routes } = require("./progress");
45
+ const { runNextTask } = require("./run_task");
46
+ const { makeTaskChart } = require("./taskchart");
47
+
48
+ const get_state_fields = () => [];
49
+
50
+ const sys_prompt = ``;
51
+
52
+ const makeSpecForm = async (req) => {
53
+ const spec = await MetaData.findOne({
54
+ type: "CopilotConstructMgr",
55
+ name: "spec",
56
+ });
57
+
58
+ return new Form({
59
+ blurb: "Provide a high-level description of the application",
60
+ fields: [
61
+ {
62
+ name: "description",
63
+ label: "Description",
64
+ type: "String",
65
+ fieldview: "textarea",
66
+ },
67
+ {
68
+ name: "audience",
69
+ label: "Audience",
70
+ type: "String",
71
+ fieldview: "textarea",
72
+ },
73
+ {
74
+ name: "core_features",
75
+ label: "Core features",
76
+ type: "String",
77
+ fieldview: "textarea",
78
+ },
79
+ {
80
+ name: "out_of_scope",
81
+ label: "Out of scope",
82
+ type: "String",
83
+ fieldview: "textarea",
84
+ },
85
+ {
86
+ name: "visual_style",
87
+ label: "Visual style",
88
+ type: "String",
89
+ fieldview: "textarea",
90
+ },
91
+ ],
92
+ xhrSubmit: true,
93
+ action: `/view/${encodeURIComponent(viewname)}/submit_specs`,
94
+ values: spec?.body || {},
95
+ });
96
+ };
97
+
98
+ const run = async (table_id, viewname, cfg, state, { req, res }) => {
99
+ const specForm = await makeSpecForm(req);
100
+ const reqList = await requirementsList(req);
101
+ const taskList = await makeTaskList(req);
102
+ const errList = await errorList(req);
103
+ const feedbacks = await feedbackList(req);
104
+ const progress = await progressList(req);
105
+ const taskChart = await makeTaskChart(req);
106
+ const schema = await showSchema(req);
107
+ const layout = {
108
+ type: "tabs",
109
+ ntabs: 5,
110
+ tabId: "",
111
+ lazyLoadViews: true,
112
+ titles: [
113
+ "Specification",
114
+ "Requirements",
115
+ "Schema",
116
+ "Tasks",
117
+ "Task chart",
118
+ "Progress",
119
+ "Feedback",
120
+ "Errors",
121
+ ],
122
+ contents: [
123
+ {
124
+ type: "blank",
125
+ contents: div({ class: "mt-2" }, renderForm(specForm, req.csrfToken())),
126
+ },
127
+ { type: "blank", contents: reqList },
128
+ { type: "blank", contents: schema },
129
+ { type: "blank", contents: taskList },
130
+ { type: "blank", contents: taskChart },
131
+ { type: "blank", contents: progress },
132
+ { type: "blank", contents: feedbacks },
133
+ { type: "blank", contents: errList },
134
+ ],
135
+ deeplink: true,
136
+ tabsStyle: "Tabs",
137
+ };
138
+ return renderLayout({
139
+ blockDispatch: {},
140
+ layout,
141
+ role: req.user?.role_id || 100,
142
+ req,
143
+ hints: getState().getLayout(req.user).hints || {},
144
+ });
145
+ };
146
+
147
+ const submit_specs = async (table_id, viewname, config, body, { req, res }) => {
148
+ const { _csrf, ...spec } = body;
149
+ const existing = await MetaData.findOne({
150
+ type: "CopilotConstructMgr",
151
+ name: "spec",
152
+ });
153
+
154
+ if (existing) await db.update("_sc_metadata", { body: spec }, existing.id);
155
+ else
156
+ await MetaData.create({
157
+ type: "CopilotConstructMgr",
158
+ name: "spec",
159
+ user_id: req.user?.id || undefined,
160
+ body: spec,
161
+ });
162
+ };
163
+
164
+ const virtual_triggers = () => {
165
+ return [
166
+ {
167
+ when_trigger: "Error",
168
+ run: async (row) => {
169
+ const existing = await MetaData.find({
170
+ type: "CopilotConstructMgr",
171
+ name: "error",
172
+ });
173
+ const messages = new Set(existing.map((m) => m.body?.error?.stack));
174
+ if (!messages.has(row.stack))
175
+ await MetaData.create({
176
+ type: "CopilotConstructMgr",
177
+ name: "error",
178
+ body: { status: "New", error: row },
179
+ user_id: null,
180
+ });
181
+ },
182
+ },
183
+ {
184
+ when_trigger: "Often",
185
+ run: async () => {
186
+ await runNextTask();
187
+ },
188
+ },
189
+ ];
190
+ };
191
+
192
+ module.exports = {
193
+ name: viewname,
194
+ display_state_form: false,
195
+ get_state_fields,
196
+ tableless: true,
197
+ singleton: true,
198
+ run,
199
+ routes: {
200
+ submit_specs,
201
+ ...req_routes,
202
+ ...task_routes,
203
+ ...error_routes,
204
+ ...feedback_routes,
205
+ ...progress_routes,
206
+ ...schema_routes,
207
+ },
208
+ virtual_triggers,
209
+ };