@saltcorn/copilot 0.8.1 → 0.8.3

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.
@@ -1,15 +1,15 @@
1
- const Field = require("@saltcorn/data/models/field");
2
1
  const Table = require("@saltcorn/data/models/table");
3
- const Form = require("@saltcorn/data/models/form");
4
2
  const MetaData = require("@saltcorn/data/models/metadata");
5
3
  const View = require("@saltcorn/data/models/view");
4
+ const Page = require("@saltcorn/data/models/page");
6
5
  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");
6
+ const Plugin = require("@saltcorn/data/models/plugin");
9
7
  const db = require("@saltcorn/data/db");
10
8
  const WorkflowRun = require("@saltcorn/data/models/workflow_run");
11
9
  const User = require("@saltcorn/data/models/user");
12
- const { viewname } = require("./common");
10
+ const { getState } = require("@saltcorn/data/db/state");
11
+ const { viewname, TaskType } = require("./common");
12
+ const { PromptGenerator } = require("./prompt-generator");
13
13
 
14
14
  /**
15
15
  * @param {number} md_id - MetaData id of the task to run
@@ -21,86 +21,233 @@ const runTask = async (md_id, req) => {
21
21
  });
22
22
 
23
23
  if (!md) return { error: "Task not found" };
24
- const spec = await MetaData.findOne({
25
- type: "CopilotConstructMgr",
26
- name: "spec",
27
- });
28
- if (!spec) return { error: "Specification not found" };
24
+
25
+ const taskType = md.body.task_type || TaskType.FEATURE;
26
+
29
27
  const agent_action = new Trigger({
30
28
  action: "Agent",
31
29
  when_trigger: "Never",
32
30
  configuration: {
33
31
  viewname: viewname,
34
32
  sys_prompt:
35
- "Each task creates exactly one view or one page. " +
36
- "Never create more than one view or page per task, even if the description mentions multiple. " +
37
- "Call the view or page tool exactly once and then stop.",
33
+ taskType === TaskType.PLUGIN
34
+ ? "Each task installs exactly one plugin from the Saltcorn plugin store. " +
35
+ "Use the Install Plugin skill to find and install it. Call the skill once and then stop."
36
+ : taskType === TaskType.DATA_MODEL
37
+ ? "Each task creates or modifies database tables/fields or configures platform-level settings (such as custom roles). " +
38
+ "Use the database design tool for schema changes. Use the Registry editor (set_entity) for platform configuration such as creating custom roles. " +
39
+ "Call only the tools needed for the task and then stop. Do not create any views, pages, or triggers."
40
+ : "Each task creates exactly one primary artifact: one view, one page, or one workflow trigger. " +
41
+ "Never create more than one view or page per task, even if the description mentions multiple. " +
42
+ "Exception: if the task description explicitly says to both create a workflow trigger AND update an existing view to add an action button for it, do both — create the workflow first, then update the specified view. " +
43
+ "After completing the primary artifact (and the explicitly described action button update, if any), stop.",
38
44
  prompt: "{{prompt}}",
39
- skills: [
40
- { skill_type: "Generate Page", yoloMode: true },
41
- { skill_type: "Database design", yoloMode: true },
42
- { skill_type: "Generate Workflow", yoloMode: true },
43
- { skill_type: "Generate trigger", yoloMode: true },
44
- { skill_type: "Generate View", yoloMode: true },
45
- { skill_type: "Install Plugin", yoloMode: true },
46
- ],
45
+ skills:
46
+ taskType === TaskType.PLUGIN
47
+ ? [{ skill_type: "Install Plugin", yoloMode: true }]
48
+ : taskType === TaskType.DATA_MODEL
49
+ ? [
50
+ { skill_type: "Database design", yoloMode: true },
51
+ { skill_type: "Registry editor", yoloMode: true },
52
+ ]
53
+ : [
54
+ { skill_type: "Generate Page", yoloMode: true },
55
+ { skill_type: "Generate Workflow", yoloMode: true },
56
+ { skill_type: "Generate trigger", yoloMode: true },
57
+ { skill_type: "Generate View", yoloMode: true },
58
+ { skill_type: "Install Plugin", yoloMode: true },
59
+ { skill_type: "Registry editor", yoloMode: true },
60
+ ],
47
61
  },
48
62
  });
49
- const prompt = `You are engaged in building the following application:
50
-
51
- Description: ${spec.body.description}
52
- Audience: ${spec.body.audience}
53
- Core features: ${spec.body.core_features}
54
- Out of scope: ${spec.body.out_of_scope}
55
- Visual style: ${spec.body.visual_style}
56
63
 
57
- 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.
64
+ const generator = await PromptGenerator.createInstance();
65
+ if (!generator.spec) return { error: "Specification not found" };
66
+ const prompt = generator.taskExecPrompt(taskType, md.body.description);
58
67
 
59
- 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.
68
+ const safeReq =
69
+ req?.__ && req?.getLocale
70
+ ? req
71
+ : {
72
+ ...req,
73
+ __: req?.__ || ((s) => s),
74
+ getLocale: req?.getLocale || (() => "en"),
75
+ user: req?.user,
76
+ };
60
77
 
61
- Important: The "users" table is built-in. Passwords are platform-managed — never add a password field to a view. Signup uses a built-in form, not an Edit view.
62
-
63
- Your task now is:
64
- ${md.body.description}`;
65
- const safeReq = req?.__
66
- ? req
67
- : { ...req, __: (s) => s, user: req?.user };
78
+ const tableNamesBefore =
79
+ taskType === TaskType.DATA_MODEL
80
+ ? new Set((await Table.find({})).map((t) => t.name))
81
+ : null;
82
+ const viewNamesBefore =
83
+ taskType === TaskType.FEATURE && md.body.phase_idx !== undefined
84
+ ? new Set((await View.find({})).map((v) => v.name))
85
+ : null;
86
+ const pageNamesBefore =
87
+ taskType === TaskType.FEATURE && md.body.phase_idx !== undefined
88
+ ? new Set((await Page.find({})).map((p) => p.name))
89
+ : null;
90
+ const pluginNamesBefore =
91
+ taskType === TaskType.PLUGIN && md.body.phase_idx !== undefined
92
+ ? new Set((await Plugin.find({})).map((p) => p.name))
93
+ : null;
68
94
 
69
95
  await md.update({ body: { ...md.body, status: "Running" } });
70
- const actionres = await agent_action.runWithoutRow({
71
- row: { prompt },
72
- req: safeReq,
73
- user: safeReq.user,
74
- });
75
- const run_id = actionres.json.run_id;
76
- const run = await WorkflowRun.findOne({ id: run_id });
77
- await agent_action.runWithoutRow({
78
- row: {
79
- prompt:
80
- "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",
81
- },
82
- req: safeReq,
83
- run,
84
- user: safeReq.user,
85
- });
86
- const lastInteraction =
87
- run.context.interactions[run.context.interactions.length - 1];
88
- const lastText =
89
- typeof lastInteraction.content === "string"
90
- ? lastInteraction.content
91
- : lastInteraction.content.text
92
- ? lastInteraction.content.text
93
- : Array.isArray(lastInteraction.content)
94
- ? lastInteraction.content[0].text
95
- : lastInteraction.content;
96
- await MetaData.create({
97
- type: "CopilotConstructMgr",
98
- name: "progress",
99
- body: { text: lastText, run_id, task_id: md.id },
100
- user_id: req?.user?.id,
101
- });
102
-
103
- await md.update({ body: { ...md.body, status: "Done", run_id } });
96
+ try {
97
+ getState().emitDynamicUpdate(db.getTenantSchema(), {
98
+ eval_js:
99
+ "if(typeof copilotRefreshTasks==='function')copilotRefreshTasks();",
100
+ });
101
+ } catch (_) {}
102
+ try {
103
+ const actionres = await agent_action.runWithoutRow({
104
+ row: { prompt },
105
+ req: safeReq,
106
+ user: safeReq.user,
107
+ });
108
+ const run_id = actionres.json.run_id;
109
+ const run = await WorkflowRun.findOne({ id: run_id });
110
+ await agent_action.runWithoutRow({
111
+ row: {
112
+ prompt:
113
+ "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",
114
+ },
115
+ req: safeReq,
116
+ run,
117
+ user: safeReq.user,
118
+ });
119
+ const updatedRun = await WorkflowRun.findOne({ id: run_id });
120
+ const extractText = (content) => {
121
+ if (!content) return "";
122
+ if (typeof content === "string") return content;
123
+ if (
124
+ typeof content === "object" &&
125
+ !Array.isArray(content) &&
126
+ content.text
127
+ )
128
+ return content.text;
129
+ if (Array.isArray(content)) {
130
+ const tb = content.find((b) => b?.type === "text" && b?.text);
131
+ return tb?.text || "";
132
+ }
133
+ return "";
134
+ };
135
+ const interactions = updatedRun.context.interactions || [];
136
+ let lastText = "";
137
+ for (let i = interactions.length - 1; i >= 0; i--) {
138
+ const t = extractText(interactions[i]?.content);
139
+ if (t) {
140
+ lastText = t;
141
+ break;
142
+ }
143
+ }
144
+ if (!lastText) lastText = md.body.description || md.body.name || "";
145
+ await MetaData.create({
146
+ type: "CopilotConstructMgr",
147
+ name: "progress",
148
+ body: {
149
+ text: lastText,
150
+ run_id,
151
+ task_id: md.id,
152
+ phase_idx: md.body.phase_idx ?? null,
153
+ },
154
+ user_id: req?.user?.id,
155
+ });
156
+ await md.update({ body: { ...md.body, status: "Done", run_id } });
157
+ if (
158
+ taskType === TaskType.DATA_MODEL &&
159
+ tableNamesBefore &&
160
+ md.body.phase_idx !== undefined
161
+ ) {
162
+ const tablesAfter = await Table.find({});
163
+ const newTables = tablesAfter.filter(
164
+ (t) => !t.name.startsWith("_sc_") && !tableNamesBefore.has(t.name)
165
+ );
166
+ for (const table of newTables) {
167
+ await MetaData.create({
168
+ type: "CopilotConstructMgr",
169
+ name: "table_phase",
170
+ body: {
171
+ table_name: table.name,
172
+ phase_idx: md.body.phase_idx,
173
+ phase_name: md.body.phase_name,
174
+ },
175
+ user_id: req?.user?.id,
176
+ });
177
+ }
178
+ }
179
+ if (
180
+ taskType === TaskType.PLUGIN &&
181
+ pluginNamesBefore &&
182
+ md.body.phase_idx !== undefined
183
+ ) {
184
+ const pluginsAfter = await Plugin.find({});
185
+ for (const p of pluginsAfter.filter(
186
+ (p) => !pluginNamesBefore.has(p.name)
187
+ )) {
188
+ await MetaData.create({
189
+ type: "CopilotConstructMgr",
190
+ name: "plugin_phase",
191
+ body: {
192
+ plugin_name: p.name,
193
+ phase_idx: md.body.phase_idx,
194
+ phase_name: md.body.phase_name,
195
+ },
196
+ user_id: req?.user?.id,
197
+ });
198
+ }
199
+ }
200
+ if (
201
+ taskType === TaskType.FEATURE &&
202
+ viewNamesBefore &&
203
+ md.body.phase_idx !== undefined
204
+ ) {
205
+ const viewsAfter = await View.find({});
206
+ for (const v of viewsAfter.filter((v) => !viewNamesBefore.has(v.name))) {
207
+ await MetaData.create({
208
+ type: "CopilotConstructMgr",
209
+ name: "view_phase",
210
+ body: {
211
+ view_name: v.name,
212
+ viewtemplate: v.viewtemplate,
213
+ phase_idx: md.body.phase_idx,
214
+ phase_name: md.body.phase_name,
215
+ },
216
+ user_id: req?.user?.id,
217
+ });
218
+ }
219
+ const pagesAfter = await Page.find({});
220
+ for (const p of pagesAfter.filter((p) => !pageNamesBefore.has(p.name))) {
221
+ await MetaData.create({
222
+ type: "CopilotConstructMgr",
223
+ name: "view_phase",
224
+ body: {
225
+ view_name: p.name,
226
+ viewtemplate: "page",
227
+ phase_idx: md.body.phase_idx,
228
+ phase_name: md.body.phase_name,
229
+ },
230
+ user_id: req?.user?.id,
231
+ });
232
+ }
233
+ }
234
+ try {
235
+ const phaseIdx = md.body.phase_idx;
236
+ getState().emitDynamicUpdate(db.getTenantSchema(), {
237
+ eval_js:
238
+ "if(typeof copilotRefreshTasks==='function')copilotRefreshTasks();" +
239
+ (taskType === TaskType.DATA_MODEL
240
+ ? "if(typeof copilotRefreshSchema==='function')copilotRefreshSchema();"
241
+ : "") +
242
+ (phaseIdx != null
243
+ ? `if(typeof copilotRefreshPhaseProgress==='function')copilotRefreshPhaseProgress(${phaseIdx});`
244
+ : ""),
245
+ });
246
+ } catch (_) {}
247
+ } catch (e) {
248
+ await md.update({ body: { ...md.body, status: "To do" } });
249
+ throw e;
250
+ }
104
251
  };
105
252
 
106
253
  /**
@@ -128,9 +275,12 @@ const runNextTask = async (once = false) => {
128
275
  );
129
276
  const done = tasks.filter((t) => t.body.status === "Done");
130
277
  const done_names = new Set(done.map((t) => t.body.name));
278
+ const all_task_names = new Set(tasks.map((t) => t.body.name).filter(Boolean));
131
279
 
132
280
  const startable = todos.filter((t) =>
133
- t.body.depends_on.every((nm) => done_names.has(nm))
281
+ (t.body.depends_on || []).every(
282
+ (nm) => done_names.has(nm) || !all_task_names.has(nm)
283
+ )
134
284
  );
135
285
 
136
286
  if (startable[0]) {
@@ -138,8 +288,19 @@ const runNextTask = async (once = false) => {
138
288
  const taskUser = startable[0].user_id
139
289
  ? await User.findOne({ id: startable[0].user_id })
140
290
  : null;
141
- await runTask(startable[0].id, { user: taskUser, __: (s) => s });
291
+ await runTask(startable[0].id, {
292
+ user: taskUser,
293
+ __: (s) => s,
294
+ getLocale: () => "en",
295
+ });
142
296
  if (!once) await runNextTask();
297
+ } else if (!once) {
298
+ const settings = await MetaData.findOne({
299
+ type: "CopilotConstructMgr",
300
+ name: "settings",
301
+ });
302
+ if (settings?.body?.running)
303
+ await settings.update({ body: { ...settings.body, running: false } });
143
304
  }
144
305
  };
145
306