@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.
- package/actions/generate-page.js +6 -2
- package/actions/generate-tables.js +70 -6
- package/actions/generate-workflow.js +54 -3
- package/agent-skills/pagegen.js +171 -59
- package/agent-skills/registry-editor.js +30 -2
- package/agent-skills/triggergen.js +1 -5
- package/agent-skills/viewgen.js +49 -7
- package/app-constructor/common.js +7 -1
- package/app-constructor/errors.js +749 -61
- package/app-constructor/feedback-action.js +62 -60
- package/app-constructor/feedback.js +1294 -67
- package/app-constructor/fixed-prompts.js +829 -0
- package/app-constructor/phases.js +1485 -0
- package/app-constructor/prompt-generator.js +587 -0
- package/app-constructor/requirements.js +171 -50
- package/app-constructor/research.js +350 -0
- package/app-constructor/run_task.js +234 -73
- package/app-constructor/schema.js +173 -169
- package/app-constructor/tasks.js +96 -537
- package/app-constructor/tools.js +17 -4
- package/app-constructor/view.js +314 -54
- package/builder-gen.js +90 -41
- package/builder-schema.js +6 -0
- package/copilot-as-agent.js +1 -0
- package/index.js +0 -1
- package/js-code-gen.js +1 -0
- package/package.json +1 -1
- package/relation-paths.js +73 -40
- package/standard-prompt.js +1 -0
- package/user-copilot.js +2 -0
- package/workflow-gen.js +1 -0
- package/app-constructor/prompts.js +0 -120
- package/chat-copilot.js +0 -770
|
@@ -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
|
|
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 {
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
run
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
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, {
|
|
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
|
|