@saltcorn/copilot 0.8.1 → 0.8.2
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-tables.js +14 -0
- package/agent-skills/pagegen.js +20 -6
- package/agent-skills/registry-editor.js +15 -0
- package/agent-skills/triggergen.js +1 -5
- package/agent-skills/viewgen.js +49 -7
- package/app-constructor/requirements.js +98 -36
- package/app-constructor/run_task.js +67 -37
- package/app-constructor/schema.js +214 -49
- package/app-constructor/tasks.js +401 -46
- package/app-constructor/tools.js +5 -4
- package/app-constructor/view.js +7 -0
- package/builder-gen.js +79 -33
- 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/chat-copilot.js +0 -770
|
@@ -39,6 +39,7 @@ const { viewname, tool_choice } = require("./common");
|
|
|
39
39
|
const { requirements_tool } = require("./tools");
|
|
40
40
|
const { saltcorn_description, existing_tables_list } = require("./prompts");
|
|
41
41
|
const GenerateTables = require("../actions/generate-tables");
|
|
42
|
+
const { buildMermaidMarkup } = GenerateTables;
|
|
42
43
|
const GenerateTablesSkill = require("../agent-skills/database-design");
|
|
43
44
|
|
|
44
45
|
const showSchema = async (req) => {
|
|
@@ -48,15 +49,94 @@ const showSchema = async (req) => {
|
|
|
48
49
|
});
|
|
49
50
|
|
|
50
51
|
if (schema) {
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
const newTableDefs = schema.body.tables || [];
|
|
53
|
+
const newTableInstances = GenerateTables.process_tables(newTableDefs);
|
|
54
|
+
const reusedMd = await MetaData.findOne({
|
|
55
|
+
type: "CopilotConstructMgr",
|
|
56
|
+
name: "reused_schema",
|
|
57
|
+
});
|
|
58
|
+
const reusedNames = reusedMd?.body?.table_names || [];
|
|
59
|
+
const reusedInstances = reusedNames
|
|
60
|
+
.map((n) => Table.findOne({ name: n }))
|
|
61
|
+
.filter(Boolean);
|
|
62
|
+
const allTables = [...newTableInstances, ...reusedInstances];
|
|
63
|
+
const mmdia = buildMermaidMarkup(allTables);
|
|
64
|
+
const implemented = !!schema.body.implemented;
|
|
65
|
+
|
|
66
|
+
const newNames = newTableDefs.map((t) => t.table_name).filter(Boolean);
|
|
67
|
+
|
|
68
|
+
const colorMap = Object.fromEntries([
|
|
69
|
+
...newNames.map((n) => [n, "#198754"]),
|
|
70
|
+
...reusedNames.map((n) => [n, "#6c757d"]),
|
|
71
|
+
]);
|
|
72
|
+
const colorScript = script(
|
|
73
|
+
domReady(`
|
|
74
|
+
const colors = ${JSON.stringify(colorMap)};
|
|
75
|
+
const pre = document.querySelector('.schema-mermaid');
|
|
76
|
+
if (!pre) return;
|
|
77
|
+
|
|
78
|
+
const doRender = () => {
|
|
79
|
+
mermaid.run({ nodes: [pre], suppressErrors: true, postRenderCallback: () => {
|
|
80
|
+
for (const g of pre.querySelectorAll('g[id^="entity-"]')) {
|
|
81
|
+
const name = g.id.replace(/^entity-/, '').replace(/-\\d+$/, '');
|
|
82
|
+
const color = colors[name];
|
|
83
|
+
if (color) {
|
|
84
|
+
const p = g.querySelector('path');
|
|
85
|
+
if (p) p.setAttribute('fill', color);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
for (const el of pre.querySelectorAll('g.label.name .nodeLabel')) {
|
|
89
|
+
el.style.color = 'white';
|
|
90
|
+
el.style.fontWeight = 'bold';
|
|
91
|
+
}
|
|
92
|
+
}});
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// Defer render until tab is visible, then colorize nodes.
|
|
96
|
+
const pane = pre.closest('.tab-pane');
|
|
97
|
+
if (pane && !pane.classList.contains('active')) {
|
|
98
|
+
const link = document.querySelector('[href="#' + pane.id + '"]');
|
|
99
|
+
if (link) link.addEventListener('shown.bs.tab', doRender, { once: true });
|
|
100
|
+
else {
|
|
101
|
+
const o = new MutationObserver(() => {
|
|
102
|
+
if (pane.classList.contains('active')) { o.disconnect(); doRender(); }
|
|
103
|
+
});
|
|
104
|
+
o.observe(pane, { attributes: true, attributeFilter: ['class'] });
|
|
105
|
+
}
|
|
106
|
+
} else doRender();
|
|
107
|
+
`)
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const legend = div(
|
|
111
|
+
{ class: "mt-3 d-flex flex-wrap gap-3 align-items-start" },
|
|
112
|
+
newNames.length
|
|
113
|
+
? div(
|
|
114
|
+
{ class: "d-flex flex-wrap align-items-center gap-1" },
|
|
115
|
+
span(
|
|
116
|
+
{ class: "me-1 text-muted small" },
|
|
117
|
+
implemented ? "Was created:" : "Will be created:"
|
|
118
|
+
),
|
|
119
|
+
...newNames.map((n) => span({ class: "badge bg-success" }, n))
|
|
120
|
+
)
|
|
121
|
+
: "",
|
|
122
|
+
reusedNames.length
|
|
123
|
+
? div(
|
|
124
|
+
{ class: "d-flex flex-wrap align-items-center gap-1" },
|
|
125
|
+
span(
|
|
126
|
+
{ class: "me-1 text-muted small" },
|
|
127
|
+
implemented ? "Already existed:" : "Already exists:"
|
|
128
|
+
),
|
|
129
|
+
...reusedNames.map((n) => span({ class: "badge bg-secondary" }, n))
|
|
130
|
+
)
|
|
131
|
+
: ""
|
|
54
132
|
);
|
|
55
133
|
|
|
56
134
|
return div(
|
|
57
135
|
{ class: "mt-2" },
|
|
58
|
-
|
|
59
|
-
|
|
136
|
+
pre({ class: "schema-mermaid" }, mmdia),
|
|
137
|
+
colorScript,
|
|
138
|
+
legend,
|
|
139
|
+
!implemented &&
|
|
60
140
|
div(
|
|
61
141
|
{ class: "mb-4 d-block mt-3" },
|
|
62
142
|
button(
|
|
@@ -75,37 +155,71 @@ const showSchema = async (req) => {
|
|
|
75
155
|
)
|
|
76
156
|
)
|
|
77
157
|
);
|
|
78
|
-
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const generating = await MetaData.findOne({
|
|
161
|
+
type: "CopilotConstructMgr",
|
|
162
|
+
name: "generating_schema",
|
|
163
|
+
});
|
|
164
|
+
if (generating) {
|
|
79
165
|
return div(
|
|
80
166
|
{ class: "mt-2" },
|
|
81
|
-
p(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
167
|
+
p(
|
|
168
|
+
i({ class: "fas fa-spinner fa-spin me-2" }),
|
|
169
|
+
"Generating schema, please wait..."
|
|
170
|
+
),
|
|
171
|
+
script(
|
|
172
|
+
domReady(`
|
|
173
|
+
const poll = () => {
|
|
174
|
+
view_post(${JSON.stringify(viewname)}, 'schema_status', {}, (resp) => {
|
|
175
|
+
if (resp && !resp.generating) location.reload();
|
|
176
|
+
else setTimeout(poll, 3000);
|
|
177
|
+
});
|
|
178
|
+
};
|
|
179
|
+
setTimeout(poll, 3000);
|
|
180
|
+
`)
|
|
88
181
|
)
|
|
89
182
|
);
|
|
90
183
|
}
|
|
184
|
+
|
|
185
|
+
return div(
|
|
186
|
+
{ class: "mt-2", id: "schema-gen-area" },
|
|
187
|
+
p("Schema not found"),
|
|
188
|
+
button(
|
|
189
|
+
{ class: "btn btn-primary", onclick: `copilotGenSchema()` },
|
|
190
|
+
"Generate schema"
|
|
191
|
+
),
|
|
192
|
+
script(
|
|
193
|
+
domReady(`
|
|
194
|
+
window.copilotGenSchema = () => {
|
|
195
|
+
document.getElementById('schema-gen-area').innerHTML =
|
|
196
|
+
'<p><i class="fas fa-spinner fa-spin me-2"></i>Generating schema, please wait...</p>';
|
|
197
|
+
view_post(${JSON.stringify(viewname)}, 'gen_schema', {}, () => {});
|
|
198
|
+
const poll = () => {
|
|
199
|
+
view_post(${JSON.stringify(viewname)}, 'schema_status', {}, (resp) => {
|
|
200
|
+
if (resp && !resp.generating) location.reload();
|
|
201
|
+
else setTimeout(poll, 3000);
|
|
202
|
+
});
|
|
203
|
+
};
|
|
204
|
+
setTimeout(poll, 3000);
|
|
205
|
+
};
|
|
206
|
+
`)
|
|
207
|
+
)
|
|
208
|
+
);
|
|
91
209
|
};
|
|
92
210
|
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
type: "CopilotConstructMgr",
|
|
96
|
-
name: "spec",
|
|
97
|
-
});
|
|
98
|
-
if (!spec) throw new Error("Specification not found");
|
|
99
|
-
const rs = await MetaData.find({
|
|
211
|
+
const doGenSchema = async (spec, rs, userId) => {
|
|
212
|
+
const generatingMd = await MetaData.create({
|
|
100
213
|
type: "CopilotConstructMgr",
|
|
101
|
-
name: "
|
|
214
|
+
name: "generating_schema",
|
|
215
|
+
body: {},
|
|
216
|
+
user_id: userId,
|
|
102
217
|
});
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
`Generate the database schema for this application:
|
|
218
|
+
try {
|
|
219
|
+
const databaseDesignTool = new GenerateTablesSkill({}).provideTools();
|
|
220
|
+
const existing_tables = await Table.find({});
|
|
221
|
+
const answer = await getState().functions.llm_generate.run(
|
|
222
|
+
`Generate the database schema for this application:
|
|
109
223
|
|
|
110
224
|
Description: ${spec.body.description}
|
|
111
225
|
Audience: ${spec.body.audience}
|
|
@@ -113,7 +227,7 @@ Core features: ${spec.body.core_features}
|
|
|
113
227
|
Out of scope: ${spec.body.out_of_scope}
|
|
114
228
|
Visual style: ${spec.body.visual_style}
|
|
115
229
|
|
|
116
|
-
These are the requirements of the application:
|
|
230
|
+
These are the requirements of the application:
|
|
117
231
|
|
|
118
232
|
${rs.map((r) => `* ${r.body.requirement}`).join("\n")}
|
|
119
233
|
|
|
@@ -123,7 +237,9 @@ ${existing_tables_list(existing_tables)}
|
|
|
123
237
|
|
|
124
238
|
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
239
|
|
|
126
|
-
The tables listed above
|
|
240
|
+
The tables listed above already exist in the database. Do NOT modify or extend them — treat them as fixed. Handle them as follows:
|
|
241
|
+
- If an existing table is already complete and used as-is: add its name to reused_table_names. Do NOT define its fields again in the tables array.
|
|
242
|
+
- New tables not yet in the database: include them in the tables array with all their fields as usual.
|
|
127
243
|
|
|
128
244
|
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.
|
|
129
245
|
For every field that must not be empty, set not_null=true.
|
|
@@ -132,34 +248,78 @@ Do NOT leave uniqueness or required constraints for a later step — express the
|
|
|
132
248
|
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.
|
|
133
249
|
|
|
134
250
|
Now use the ${
|
|
135
|
-
|
|
136
|
-
|
|
251
|
+
databaseDesignTool.function.name
|
|
252
|
+
} tool to generate the complete database schema for this software application
|
|
137
253
|
`,
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
254
|
+
{
|
|
255
|
+
tools: [databaseDesignTool],
|
|
256
|
+
...tool_choice(databaseDesignTool.function.name),
|
|
257
|
+
systemPrompt:
|
|
258
|
+
"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.",
|
|
259
|
+
}
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
const tc = answer.getToolCalls()[0];
|
|
145
263
|
|
|
146
|
-
|
|
264
|
+
const noNewTables = !tc.input.tables || tc.input.tables.length === 0;
|
|
265
|
+
await MetaData.create({
|
|
266
|
+
type: "CopilotConstructMgr",
|
|
267
|
+
name: "schema",
|
|
268
|
+
body: { tables: tc.input.tables || [], implemented: noNewTables },
|
|
269
|
+
user_id: userId,
|
|
270
|
+
});
|
|
147
271
|
|
|
148
|
-
|
|
272
|
+
const reusedNames = tc.input.reused_table_names || [];
|
|
273
|
+
if (reusedNames.length) {
|
|
274
|
+
await MetaData.create({
|
|
275
|
+
type: "CopilotConstructMgr",
|
|
276
|
+
name: "reused_schema",
|
|
277
|
+
body: { table_names: reusedNames },
|
|
278
|
+
user_id: userId,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
} finally {
|
|
282
|
+
await generatingMd.delete();
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const gen_schema = async (table_id, viewname, config, body, { req, res }) => {
|
|
287
|
+
const spec = await MetaData.findOne({
|
|
149
288
|
type: "CopilotConstructMgr",
|
|
150
|
-
name: "
|
|
151
|
-
body: { tables: tc.input.tables, implemented: false },
|
|
152
|
-
user_id: req.user?.id,
|
|
289
|
+
name: "spec",
|
|
153
290
|
});
|
|
154
|
-
|
|
291
|
+
if (!spec) throw new Error("Specification not found");
|
|
292
|
+
const rs = await MetaData.find({
|
|
293
|
+
type: "CopilotConstructMgr",
|
|
294
|
+
name: "requirement",
|
|
295
|
+
});
|
|
296
|
+
if (!rs.length) throw new Error("No requirements found");
|
|
297
|
+
|
|
298
|
+
doGenSchema(spec, rs, req.user?.id).catch((e) =>
|
|
299
|
+
console.error("gen_schema error", e)
|
|
300
|
+
);
|
|
301
|
+
return { json: { success: true } };
|
|
155
302
|
};
|
|
156
303
|
|
|
157
|
-
const
|
|
158
|
-
|
|
304
|
+
const schema_status = async (
|
|
305
|
+
table_id,
|
|
306
|
+
viewname,
|
|
307
|
+
config,
|
|
308
|
+
body,
|
|
309
|
+
{ req, res }
|
|
310
|
+
) => {
|
|
311
|
+
const generating = await MetaData.findOne({
|
|
159
312
|
type: "CopilotConstructMgr",
|
|
160
|
-
name: "
|
|
313
|
+
name: "generating_schema",
|
|
161
314
|
});
|
|
162
|
-
|
|
315
|
+
return { json: { generating: !!generating } };
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const del_schema = async (table_id, viewname, config, body, { req, res }) => {
|
|
319
|
+
for (const name of ["schema", "reused_schema", "generating_schema"]) {
|
|
320
|
+
const rs = await MetaData.find({ type: "CopilotConstructMgr", name });
|
|
321
|
+
for (const r of rs) await r.delete();
|
|
322
|
+
}
|
|
163
323
|
return { json: { reload_page: true } };
|
|
164
324
|
};
|
|
165
325
|
|
|
@@ -194,6 +354,11 @@ const implement_schema = async (
|
|
|
194
354
|
return { json: { reload_page: true } };
|
|
195
355
|
};
|
|
196
356
|
|
|
197
|
-
const schema_routes = {
|
|
357
|
+
const schema_routes = {
|
|
358
|
+
gen_schema,
|
|
359
|
+
schema_status,
|
|
360
|
+
del_schema,
|
|
361
|
+
implement_schema,
|
|
362
|
+
};
|
|
198
363
|
|
|
199
364
|
module.exports = { showSchema, schema_routes };
|