@saltcorn/copilot 0.8.0 → 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/actions/generate-trigger.js +61 -0
- package/actions/generate-workflow.js +6 -1
- package/agent-skills/pagegen.js +39 -12
- package/agent-skills/registry-editor.js +15 -0
- package/agent-skills/triggergen.js +259 -0
- package/agent-skills/viewgen.js +248 -22
- package/app-constructor/prompts.js +65 -3
- package/app-constructor/requirements.js +98 -36
- package/app-constructor/run_task.js +116 -68
- package/app-constructor/schema.js +227 -49
- package/app-constructor/tasks.js +622 -83
- package/app-constructor/tools.js +5 -4
- package/app-constructor/view.js +7 -0
- package/builder-gen.js +135 -35
- package/copilot-as-agent.js +2 -0
- package/index.js +1 -2
- package/js-code-gen.js +1 -0
- package/package.json +1 -1
- package/relation-paths.js +269 -0
- package/standard-prompt.js +1 -0
- package/user-copilot.js +2 -0
- package/workflow-gen.js +1 -0
- package/agent-skills/app-constructor-context.js +0 -25
- package/chat-copilot.js +0 -769
|
@@ -8,41 +8,13 @@ const { findType } = require("@saltcorn/data/models/discovery");
|
|
|
8
8
|
const { save_menu_items } = require("@saltcorn/data/models/config");
|
|
9
9
|
const db = require("@saltcorn/data/db");
|
|
10
10
|
const WorkflowRun = require("@saltcorn/data/models/workflow_run");
|
|
11
|
+
const User = require("@saltcorn/data/models/user");
|
|
11
12
|
const { viewname } = require("./common");
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
name: "settings",
|
|
18
|
-
});
|
|
19
|
-
if (!settings?.body?.running) return;
|
|
20
|
-
}
|
|
21
|
-
const tasks = await MetaData.find(
|
|
22
|
-
{
|
|
23
|
-
type: "CopilotConstructMgr",
|
|
24
|
-
name: "task",
|
|
25
|
-
},
|
|
26
|
-
{ orderBy: "id" }
|
|
27
|
-
);
|
|
28
|
-
const todos = tasks.filter(
|
|
29
|
-
(t) => !t.body.status || t.body.status === "To do"
|
|
30
|
-
);
|
|
31
|
-
const done = tasks.filter((t) => t.body.status === "Done");
|
|
32
|
-
const done_names = new Set(done.map((t) => t.body.name));
|
|
33
|
-
|
|
34
|
-
const startable = todos.filter((t) =>
|
|
35
|
-
t.body.depends_on.every((nm) => done_names.has(nm))
|
|
36
|
-
);
|
|
37
|
-
|
|
38
|
-
if (startable[0]) {
|
|
39
|
-
console.log("running task", startable[0]);
|
|
40
|
-
|
|
41
|
-
return await runTask(startable[0].id, {});
|
|
42
|
-
}
|
|
43
|
-
//not done
|
|
44
|
-
};
|
|
45
|
-
|
|
14
|
+
/**
|
|
15
|
+
* @param {number} md_id - MetaData id of the task to run
|
|
16
|
+
* @param {object} req - Express request (may be empty `{}` from scheduler)
|
|
17
|
+
*/
|
|
46
18
|
const runTask = async (md_id, req) => {
|
|
47
19
|
const md = await MetaData.findOne({
|
|
48
20
|
id: md_id,
|
|
@@ -59,15 +31,19 @@ const runTask = async (md_id, req) => {
|
|
|
59
31
|
when_trigger: "Never",
|
|
60
32
|
configuration: {
|
|
61
33
|
viewname: viewname,
|
|
62
|
-
sys_prompt:
|
|
34
|
+
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.",
|
|
63
38
|
prompt: "{{prompt}}",
|
|
64
39
|
skills: [
|
|
65
40
|
{ skill_type: "Generate Page", yoloMode: true },
|
|
66
41
|
{ skill_type: "Database design", yoloMode: true },
|
|
67
42
|
{ skill_type: "Generate Workflow", yoloMode: true },
|
|
43
|
+
{ skill_type: "Generate trigger", yoloMode: true },
|
|
68
44
|
{ skill_type: "Generate View", yoloMode: true },
|
|
69
45
|
{ skill_type: "Install Plugin", yoloMode: true },
|
|
70
|
-
{ skill_type: "
|
|
46
|
+
{ skill_type: "Registry editor", yoloMode: true },
|
|
71
47
|
],
|
|
72
48
|
},
|
|
73
49
|
});
|
|
@@ -83,46 +59,118 @@ Important: The database schema is already fully implemented. Do NOT use generate
|
|
|
83
59
|
|
|
84
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.
|
|
85
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
|
+
|
|
70
|
+
Important: Two-factor authentication (2FA/TOTP) is fully built into the platform. To configure it, call set_entity directly with entity_type "system-configuration-value" and entity_name "twofa_policy_by_role". The entity_definition must be the plain JSON object itself — for example: {"1": "Mandatory", "100": "Disabled"}. Do NOT wrap it in {"type": "json", "value": ...} or any other envelope. Read the current value first with get_entity and merge rather than overwrite. Do NOT create a workflow or trigger to do this.
|
|
71
|
+
|
|
72
|
+
Important: To set a page as the home page for a role, call set_entity directly with entity_type "system-configuration-value" and entity_name "home_page_by_role". The value is a JSON object mapping role IDs to page names — Role IDs: public=100, user=80, staff=40, admin=1. The entity_definition must be the plain JSON object itself — for example: {"100": "landing", "80": "client_dashboard"}. Do NOT wrap it in {"type": "json", "value": ...} or any other envelope. Read the current value first with get_entity so you can merge rather than overwrite. Do NOT create a workflow or trigger to do this — use set_entity directly.
|
|
73
|
+
|
|
74
|
+
Important: If the task description mentions adding a viewlink, linking rows to another view, or a button that opens another view from a list — that viewlink column MUST be present in the finished view. Do not skip it. Viewlinks require calling get_relation_paths first to obtain the relation string before generating the layout.
|
|
75
|
+
|
|
76
|
+
Important: Before creating or updating any view or page that embeds, links to, or opens another view (including viewlinks, action buttons, and ajax_modal calls), call list_entities (entity_type "view") to get all existing view names. Only reference views that appear in that list — never invent a name or assume a view exists. If a view is not in the list, omit the reference entirely. Do the same for pages: call list_entities (entity_type "page") before linking to any page by name.
|
|
77
|
+
|
|
86
78
|
Your task now is:
|
|
87
79
|
${md.body.description}`;
|
|
80
|
+
const safeReq = req?.__ ? req : { ...req, __: (s) => s, user: req?.user };
|
|
88
81
|
|
|
89
82
|
await md.update({ body: { ...md.body, status: "Running" } });
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
83
|
+
try {
|
|
84
|
+
const actionres = await agent_action.runWithoutRow({
|
|
85
|
+
row: { prompt },
|
|
86
|
+
req: safeReq,
|
|
87
|
+
user: safeReq.user,
|
|
88
|
+
});
|
|
89
|
+
const run_id = actionres.json.run_id;
|
|
90
|
+
const run = await WorkflowRun.findOne({ id: run_id });
|
|
91
|
+
await agent_action.runWithoutRow({
|
|
92
|
+
row: {
|
|
93
|
+
prompt:
|
|
94
|
+
"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",
|
|
95
|
+
},
|
|
96
|
+
req: safeReq,
|
|
97
|
+
run,
|
|
98
|
+
user: safeReq.user,
|
|
99
|
+
});
|
|
100
|
+
const updatedRun = await WorkflowRun.findOne({ id: run_id });
|
|
101
|
+
const lastInteraction =
|
|
102
|
+
updatedRun.context.interactions[
|
|
103
|
+
updatedRun.context.interactions.length - 1
|
|
104
|
+
];
|
|
105
|
+
const lastText =
|
|
106
|
+
typeof lastInteraction.content === "string"
|
|
107
|
+
? lastInteraction.content
|
|
108
|
+
: lastInteraction.content.text
|
|
113
109
|
? lastInteraction.content.text
|
|
114
110
|
: Array.isArray(lastInteraction.content)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
111
|
+
? lastInteraction.content[0].text
|
|
112
|
+
: lastInteraction.content;
|
|
113
|
+
await MetaData.create({
|
|
114
|
+
type: "CopilotConstructMgr",
|
|
115
|
+
name: "progress",
|
|
116
|
+
body: { text: lastText, run_id, task_id: md.id },
|
|
117
|
+
user_id: req?.user?.id,
|
|
118
|
+
});
|
|
119
|
+
await md.update({ body: { ...md.body, status: "Done", run_id } });
|
|
120
|
+
} catch (e) {
|
|
121
|
+
await md.update({ body: { ...md.body, status: "To do" } });
|
|
122
|
+
throw e;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Run the next startable task
|
|
128
|
+
* @param {boolean} [once=false] - true: run one task and stop, false: iterate all tasks
|
|
129
|
+
*/
|
|
130
|
+
const runNextTask = async (once = false) => {
|
|
131
|
+
if (!once) {
|
|
132
|
+
const settings = await MetaData.findOne({
|
|
133
|
+
type: "CopilotConstructMgr",
|
|
134
|
+
name: "settings",
|
|
135
|
+
});
|
|
136
|
+
if (!settings?.body?.running) return;
|
|
137
|
+
}
|
|
138
|
+
const tasks = await MetaData.find(
|
|
139
|
+
{
|
|
140
|
+
type: "CopilotConstructMgr",
|
|
141
|
+
name: "task",
|
|
142
|
+
},
|
|
143
|
+
{ orderBy: "id" }
|
|
144
|
+
);
|
|
145
|
+
if (tasks.some((t) => t.body.status === "Running")) return;
|
|
146
|
+
const todos = tasks.filter(
|
|
147
|
+
(t) => !t.body.status || t.body.status === "To do"
|
|
148
|
+
);
|
|
149
|
+
const done = tasks.filter((t) => t.body.status === "Done");
|
|
150
|
+
const done_names = new Set(done.map((t) => t.body.name));
|
|
151
|
+
const all_task_names = new Set(tasks.map((t) => t.body.name).filter(Boolean));
|
|
123
152
|
|
|
124
|
-
|
|
125
|
-
|
|
153
|
+
const startable = todos.filter((t) =>
|
|
154
|
+
(t.body.depends_on || []).every(
|
|
155
|
+
(nm) => done_names.has(nm) || !all_task_names.has(nm)
|
|
156
|
+
)
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
if (startable[0]) {
|
|
160
|
+
console.log("running task", startable[0]);
|
|
161
|
+
const taskUser = startable[0].user_id
|
|
162
|
+
? await User.findOne({ id: startable[0].user_id })
|
|
163
|
+
: null;
|
|
164
|
+
await runTask(startable[0].id, { user: taskUser, __: (s) => s });
|
|
165
|
+
if (!once) await runNextTask();
|
|
166
|
+
} else if (!once) {
|
|
167
|
+
const settings = await MetaData.findOne({
|
|
168
|
+
type: "CopilotConstructMgr",
|
|
169
|
+
name: "settings",
|
|
170
|
+
});
|
|
171
|
+
if (settings?.body?.running)
|
|
172
|
+
await settings.update({ body: { ...settings.body, running: false } });
|
|
173
|
+
}
|
|
126
174
|
};
|
|
127
175
|
|
|
128
176
|
module.exports = { runTask, runNextTask };
|
|
@@ -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,6 +237,10 @@ ${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
|
|
|
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.
|
|
243
|
+
|
|
126
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.
|
|
127
245
|
For every field that must not be empty, set not_null=true.
|
|
128
246
|
Do NOT leave uniqueness or required constraints for a later step — express them fully in this schema.
|
|
@@ -130,34 +248,78 @@ Do NOT leave uniqueness or required constraints for a later step — express the
|
|
|
130
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.
|
|
131
249
|
|
|
132
250
|
Now use the ${
|
|
133
|
-
|
|
134
|
-
|
|
251
|
+
databaseDesignTool.function.name
|
|
252
|
+
} tool to generate the complete database schema for this software application
|
|
135
253
|
`,
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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];
|
|
143
263
|
|
|
144
|
-
|
|
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
|
+
});
|
|
145
271
|
|
|
146
|
-
|
|
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({
|
|
147
288
|
type: "CopilotConstructMgr",
|
|
148
|
-
name: "
|
|
149
|
-
body: { tables: tc.input.tables, implemented: false },
|
|
150
|
-
user_id: req.user?.id,
|
|
289
|
+
name: "spec",
|
|
151
290
|
});
|
|
152
|
-
|
|
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 } };
|
|
153
302
|
};
|
|
154
303
|
|
|
155
|
-
const
|
|
156
|
-
|
|
304
|
+
const schema_status = async (
|
|
305
|
+
table_id,
|
|
306
|
+
viewname,
|
|
307
|
+
config,
|
|
308
|
+
body,
|
|
309
|
+
{ req, res }
|
|
310
|
+
) => {
|
|
311
|
+
const generating = await MetaData.findOne({
|
|
157
312
|
type: "CopilotConstructMgr",
|
|
158
|
-
name: "
|
|
313
|
+
name: "generating_schema",
|
|
159
314
|
});
|
|
160
|
-
|
|
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
|
+
}
|
|
161
323
|
return { json: { reload_page: true } };
|
|
162
324
|
};
|
|
163
325
|
|
|
@@ -174,13 +336,29 @@ const implement_schema = async (
|
|
|
174
336
|
});
|
|
175
337
|
|
|
176
338
|
const { apply_copilot_tables } = new GenerateTablesSkill({}).userActions;
|
|
177
|
-
await
|
|
339
|
+
const existingNames = new Set((await Table.find({})).map((t) => t.name));
|
|
340
|
+
const newTables = md.body.tables.filter((t) => {
|
|
341
|
+
if (existingNames.has(t.name)) {
|
|
342
|
+
getState().log(
|
|
343
|
+
2,
|
|
344
|
+
`AppConstructor: skipping table "${t.name}" — already exists in database`
|
|
345
|
+
);
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
return true;
|
|
349
|
+
});
|
|
350
|
+
await apply_copilot_tables({ tables: newTables, user: req.user });
|
|
178
351
|
md.body.implemented = true;
|
|
179
352
|
await md.update({ body: md.body });
|
|
180
353
|
|
|
181
354
|
return { json: { reload_page: true } };
|
|
182
355
|
};
|
|
183
356
|
|
|
184
|
-
const schema_routes = {
|
|
357
|
+
const schema_routes = {
|
|
358
|
+
gen_schema,
|
|
359
|
+
schema_status,
|
|
360
|
+
del_schema,
|
|
361
|
+
implement_schema,
|
|
362
|
+
};
|
|
185
363
|
|
|
186
364
|
module.exports = { showSchema, schema_routes };
|