@saltcorn/copilot 0.8.2 → 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 +56 -6
- package/actions/generate-workflow.js +54 -3
- package/agent-skills/pagegen.js +155 -57
- package/agent-skills/registry-editor.js +15 -2
- 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 +113 -54
- package/app-constructor/research.js +350 -0
- package/app-constructor/run_task.js +195 -64
- package/app-constructor/schema.js +163 -324
- package/app-constructor/tasks.js +90 -886
- package/app-constructor/tools.js +13 -1
- package/app-constructor/view.js +307 -54
- package/builder-gen.js +11 -8
- package/builder-schema.js +6 -0
- package/package.json +1 -1
- package/app-constructor/prompts.js +0 -120
|
@@ -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,65 +21,84 @@ 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
|
-
|
|
47
|
-
|
|
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
|
+
],
|
|
48
61
|
},
|
|
49
62
|
});
|
|
50
|
-
const prompt = `You are engaged in building the following application:
|
|
51
|
-
|
|
52
|
-
Description: ${spec.body.description}
|
|
53
|
-
Audience: ${spec.body.audience}
|
|
54
|
-
Core features: ${spec.body.core_features}
|
|
55
|
-
Out of scope: ${spec.body.out_of_scope}
|
|
56
|
-
Visual style: ${spec.body.visual_style}
|
|
57
|
-
|
|
58
|
-
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.
|
|
59
|
-
|
|
60
|
-
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.
|
|
61
|
-
|
|
62
|
-
Important: The "users" table is built-in. Passwords are platform-managed — never add a password field to a view. Signup uses the built-in page at /auth/signup, login at /auth/login. Do NOT create triggers for registration or email verification — the platform handles this natively.
|
|
63
|
-
|
|
64
|
-
Important: On landing pages, place Log in / Create account buttons in no more than two locations (e.g. navbar and one hero call-to-action). Do not repeat them in a third "Get started" section or anywhere else. For links that take an already-authenticated user to their dashboard, use href="/" — not /auth/login.
|
|
65
|
-
|
|
66
|
-
Important: Do not name any page or view "Admin dashboard" — that name is reserved by the Saltcorn platform. For pages intended for role 1 (admin), use a name like "App admin dashboard" or prefix it with the application name (e.g. "Law Firm admin dashboard").
|
|
67
|
-
|
|
68
|
-
Important: When creating a page or view, always set min_role based on the intended audience: 1 for admin-only, 40 for staff and above, 80 for logged-in users and above, 100 for public. Never default to public (100) unless the page or view is explicitly intended for unauthenticated users (e.g. a landing page). A dashboard or view for clients/users is role 80, a staff page or view is role 40, an admin page or view is role 1.
|
|
69
63
|
|
|
70
|
-
|
|
64
|
+
const generator = await PromptGenerator.createInstance();
|
|
65
|
+
if (!generator.spec) return { error: "Specification not found" };
|
|
66
|
+
const prompt = generator.taskExecPrompt(taskType, md.body.description);
|
|
71
67
|
|
|
72
|
-
|
|
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
|
+
};
|
|
73
77
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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;
|
|
81
94
|
|
|
82
95
|
await md.update({ body: { ...md.body, status: "Running" } });
|
|
96
|
+
try {
|
|
97
|
+
getState().emitDynamicUpdate(db.getTenantSchema(), {
|
|
98
|
+
eval_js:
|
|
99
|
+
"if(typeof copilotRefreshTasks==='function')copilotRefreshTasks();",
|
|
100
|
+
});
|
|
101
|
+
} catch (_) {}
|
|
83
102
|
try {
|
|
84
103
|
const actionres = await agent_action.runWithoutRow({
|
|
85
104
|
row: { prompt },
|
|
@@ -98,25 +117,133 @@ ${md.body.description}`;
|
|
|
98
117
|
user: safeReq.user,
|
|
99
118
|
});
|
|
100
119
|
const updatedRun = await WorkflowRun.findOne({ id: run_id });
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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 || "";
|
|
113
145
|
await MetaData.create({
|
|
114
146
|
type: "CopilotConstructMgr",
|
|
115
147
|
name: "progress",
|
|
116
|
-
body: {
|
|
148
|
+
body: {
|
|
149
|
+
text: lastText,
|
|
150
|
+
run_id,
|
|
151
|
+
task_id: md.id,
|
|
152
|
+
phase_idx: md.body.phase_idx ?? null,
|
|
153
|
+
},
|
|
117
154
|
user_id: req?.user?.id,
|
|
118
155
|
});
|
|
119
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 (_) {}
|
|
120
247
|
} catch (e) {
|
|
121
248
|
await md.update({ body: { ...md.body, status: "To do" } });
|
|
122
249
|
throw e;
|
|
@@ -161,7 +288,11 @@ const runNextTask = async (once = false) => {
|
|
|
161
288
|
const taskUser = startable[0].user_id
|
|
162
289
|
? await User.findOne({ id: startable[0].user_id })
|
|
163
290
|
: null;
|
|
164
|
-
await runTask(startable[0].id, {
|
|
291
|
+
await runTask(startable[0].id, {
|
|
292
|
+
user: taskUser,
|
|
293
|
+
__: (s) => s,
|
|
294
|
+
getLocale: () => "en",
|
|
295
|
+
});
|
|
165
296
|
if (!once) await runNextTask();
|
|
166
297
|
} else if (!once) {
|
|
167
298
|
const settings = await MetaData.findOne({
|