@saltcorn/copilot 0.8.0 → 0.8.1
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-trigger.js +61 -0
- package/actions/generate-workflow.js +6 -1
- package/agent-skills/pagegen.js +19 -6
- package/agent-skills/triggergen.js +263 -0
- package/agent-skills/viewgen.js +203 -19
- package/app-constructor/prompts.js +65 -3
- package/app-constructor/run_task.js +61 -43
- package/app-constructor/schema.js +14 -1
- package/app-constructor/tasks.js +241 -57
- package/builder-gen.js +84 -30
- package/chat-copilot.js +1 -0
- package/copilot-as-agent.js +1 -0
- package/index.js +1 -1
- package/package.json +1 -1
- package/relation-paths.js +236 -0
- package/agent-skills/app-constructor-context.js +0 -25
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
const { RelationsFinder, RelationType } = require("@saltcorn/common-code");
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Relation path documentation included in LLM system prompts (viewgen, builder-gen).
|
|
5
|
+
*
|
|
6
|
+
* TWO FORMATS EXIST:
|
|
7
|
+
*
|
|
8
|
+
* New format (always generate this):
|
|
9
|
+
* view: "viewname" + relation: ".sourcetable.segment1.segment2..."
|
|
10
|
+
*
|
|
11
|
+
* Segment types:
|
|
12
|
+
* Outbound FK (to parent): FK field name alone, e.g. trip_id
|
|
13
|
+
* Inbound FK (child rows): childtable$fkfield, e.g. packing_items$trip_id
|
|
14
|
+
*
|
|
15
|
+
* Examples:
|
|
16
|
+
* .trips.packing_items$trip_id ChildList: all packing_items for a trip
|
|
17
|
+
* .packing_items.trip_id ParentShow: the trip that owns a packing item
|
|
18
|
+
* .artists.artist_plays_on_album$artist.album ChildList through a join table
|
|
19
|
+
* .users.orders$user_id.order_lines$order_id RelationPath: multi-level
|
|
20
|
+
*
|
|
21
|
+
* Legacy format (may appear in existing configs — do not generate, understand only):
|
|
22
|
+
* The type and path are encoded together in the view field, no separate relation field.
|
|
23
|
+
* "Own:viewname" → same table, no relation
|
|
24
|
+
* "ParentShow:viewname.table.fkfield" → outbound FK to parent
|
|
25
|
+
* "ChildList:viewname.table.inbkey" → inbound FK, one-to-many
|
|
26
|
+
* "OneToOneShow:viewname.table.inbkey" → inbound FK, unique
|
|
27
|
+
* "Independent:viewname" → no FK relationship
|
|
28
|
+
*
|
|
29
|
+
* Relation types by new-format path structure:
|
|
30
|
+
* Own – zero segments, source and target are the same table
|
|
31
|
+
* ParentShow – single outbound-FK segment
|
|
32
|
+
* OneToOneShow – single inbound-FK segment on a unique field
|
|
33
|
+
* ChildList – one or more inbound-FK segments (may mix outbound for join tables)
|
|
34
|
+
* RelationPath – complex multi-level path mixing both segment types
|
|
35
|
+
*/
|
|
36
|
+
const RELATION_PATH_DOC = `
|
|
37
|
+
## Relation paths
|
|
38
|
+
|
|
39
|
+
Relation paths connect a view_link column or embedded view (type "view") segment to its
|
|
40
|
+
target view. There are two formats — **new** (preferred) and **legacy** (read-only).
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
### New format (always use this when generating or updating)
|
|
45
|
+
|
|
46
|
+
Two separate fields:
|
|
47
|
+
- \`view\`: just the view name, e.g. \`"packing_items_list"\`
|
|
48
|
+
- \`relation\`: a dot-separated path string, e.g. \`".trips.packing_items$trip_id"\`
|
|
49
|
+
|
|
50
|
+
**Path string format:** \`.sourcetable.segment1.segment2...\`
|
|
51
|
+
|
|
52
|
+
Segment types:
|
|
53
|
+
- **Outbound FK** (navigate to a parent): FK field name alone — e.g. \`trip_id\`
|
|
54
|
+
- **Inbound FK** (collect child rows): \`childtable$fkfield\` — e.g. \`packing_items$trip_id\`
|
|
55
|
+
|
|
56
|
+
Examples:
|
|
57
|
+
| relation string | type | meaning |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| \`.trips.packing_items$trip_id\` | ChildList | all packing_items for a trip |
|
|
60
|
+
| \`.packing_items.trip_id\` | ParentShow | the trip that owns a packing_item |
|
|
61
|
+
| \`.artists.artist_plays_on_album$artist.album\` | ChildList | albums via join table |
|
|
62
|
+
| \`.users.orders$user_id.order_lines$order_id\` | RelationPath | multi-level |
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
### Legacy format (you may encounter this in existing configs — do not generate it)
|
|
67
|
+
|
|
68
|
+
The type and path are encoded together inside the \`view\` field as a colon-prefixed string.
|
|
69
|
+
There is no separate \`relation\` field.
|
|
70
|
+
|
|
71
|
+
| legacy view field | equivalent new relation |
|
|
72
|
+
|---|---|
|
|
73
|
+
| \`"Own:viewname"\` | \`view: "viewname"\`, no relation |
|
|
74
|
+
| \`"ParentShow:viewname.table.fkfield"\` | \`relation: ".sourcetable.fkfield"\` |
|
|
75
|
+
| \`"ChildList:viewname.table.inbkey"\` | \`relation: ".sourcetable.childtable$inbkey"\` |
|
|
76
|
+
| \`"OneToOneShow:viewname.table.inbkey"\` | \`relation: ".sourcetable.childtable$inbkey"\` |
|
|
77
|
+
| \`"Independent:viewname"\` | \`view: "viewname"\`, no relation |
|
|
78
|
+
|
|
79
|
+
When you read an existing view config and see a legacy \`view\` value like \`"ChildList:trips_list.packing_items.trip_id"\`, parse it as: type=ChildList, view=trips_list, relation=.sourcetable.packing_items$trip_id. When writing back, convert to the new format.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
### Using get_relation_paths
|
|
84
|
+
|
|
85
|
+
Call it **once** with all source_table/target_view pairs you need — do not make separate calls per pair. The tool returns paths in the new RelationPath format; always write back in the new format, even if you read legacy strings from an existing config.
|
|
86
|
+
|
|
87
|
+
When multiple paths are returned for one pair, pick by matching the relation type to what the target view is meant to show:
|
|
88
|
+
- **ChildList** — target view shows multiple rows belonging to the current row (e.g. packing items for a trip).
|
|
89
|
+
- **ParentShow** — target view shows the single parent the current row belongs to (e.g. the trip for a packing item).
|
|
90
|
+
- **OneToOneShow** — exactly one related child row via a unique FK.
|
|
91
|
+
- **Own** — target view is on the same table (no FK traversal needed).
|
|
92
|
+
- If multiple paths of the same type exist (e.g. a table has two FKs pointing to the same target), pick the one whose FK field name best matches the semantic relationship in the task.
|
|
93
|
+
- Prefer shorter paths (fewer segments) unless a longer one is clearly more appropriate.
|
|
94
|
+
`;
|
|
95
|
+
|
|
96
|
+
const typeToLabel = (type) => {
|
|
97
|
+
if (type === RelationType.OWN)
|
|
98
|
+
return "Own – source and target are the same table (no relation needed)";
|
|
99
|
+
if (type === RelationType.INDEPENDENT)
|
|
100
|
+
return "Independent – no FK relationship exists";
|
|
101
|
+
if (type === RelationType.PARENT_SHOW)
|
|
102
|
+
return "ParentShow – outbound FK to a parent record (many-to-one)";
|
|
103
|
+
if (type === RelationType.ONE_TO_ONE_SHOW)
|
|
104
|
+
return "OneToOneShow – unique inbound FK (one-to-one)";
|
|
105
|
+
if (type === RelationType.CHILD_LIST)
|
|
106
|
+
return "ChildList – inbound FK, one parent → many child rows";
|
|
107
|
+
return "RelationPath – complex multi-level path";
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @param {string} sourceTableName
|
|
112
|
+
* @param {string} targetViewName
|
|
113
|
+
* @param {{ tables, views }} schemaData pre-fetched via build_schema_data()
|
|
114
|
+
* @returns {Array<Relation>} raw Relation objects from RelationsFinder
|
|
115
|
+
*/
|
|
116
|
+
function getRelationPaths(sourceTableName, targetViewName, schemaData) {
|
|
117
|
+
if (!schemaData) return [];
|
|
118
|
+
try {
|
|
119
|
+
const finder = new RelationsFinder(schemaData.tables, schemaData.views, 6);
|
|
120
|
+
return finder.findRelations(sourceTableName, targetViewName, []);
|
|
121
|
+
} catch {
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Resolve multiple source_table/target_view pairs against pre-fetched schema data.
|
|
128
|
+
* All per-pair work is synchronous — call build_schema_data() once before invoking this.
|
|
129
|
+
* @param {Array<{source_table: string, target_view: string}>} pairs
|
|
130
|
+
* @param {{ tables, views }} schemaData
|
|
131
|
+
* @returns {Array<string>} one formatted result string per pair
|
|
132
|
+
*/
|
|
133
|
+
function getRelationPathsForPairs(pairs, schemaData) {
|
|
134
|
+
if (!schemaData) return pairs.map(({ source_table, target_view }) =>
|
|
135
|
+
formatRelationPathResult(source_table, target_view, { error: "Schema data unavailable" })
|
|
136
|
+
);
|
|
137
|
+
const finder = new RelationsFinder(schemaData.tables, schemaData.views, 6);
|
|
138
|
+
return pairs.map(({ source_table, target_view }) => {
|
|
139
|
+
const targetView = (schemaData.views || []).find((v) => v.name === target_view);
|
|
140
|
+
if (!targetView)
|
|
141
|
+
return formatRelationPathResult(source_table, target_view, {
|
|
142
|
+
error: `View "${target_view}" not found in current schema`,
|
|
143
|
+
});
|
|
144
|
+
let relations;
|
|
145
|
+
try {
|
|
146
|
+
relations = finder.findRelations(source_table, target_view, []);
|
|
147
|
+
} catch (e) {
|
|
148
|
+
return formatRelationPathResult(source_table, target_view, {
|
|
149
|
+
error: `Failed to find relations: ${e.message}`,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return formatRelationPathResult(source_table, target_view, {
|
|
153
|
+
paths: relations.map((r) => ({
|
|
154
|
+
relation_string: r.relationString,
|
|
155
|
+
type: String(r.type),
|
|
156
|
+
label: typeToLabel(r.type),
|
|
157
|
+
})),
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Pick the most useful relation from a list: Own > Parent > Child > first.
|
|
164
|
+
* Used as a fallback in builder-gen when the model doesn't specify a relation.
|
|
165
|
+
*/
|
|
166
|
+
function pickBestRelation(relations) {
|
|
167
|
+
if (!relations.length) return null;
|
|
168
|
+
let own = null,
|
|
169
|
+
parent = null,
|
|
170
|
+
child = null;
|
|
171
|
+
for (const r of relations) {
|
|
172
|
+
if (r.type === RelationType.OWN) own = r;
|
|
173
|
+
else if (r.type === RelationType.PARENT_SHOW) parent = r;
|
|
174
|
+
else if (
|
|
175
|
+
r.type === RelationType.CHILD_LIST ||
|
|
176
|
+
r.type === RelationType.ONE_TO_ONE_SHOW
|
|
177
|
+
)
|
|
178
|
+
child = r;
|
|
179
|
+
}
|
|
180
|
+
return own || parent || child || relations[0];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Format the result of getRelationPaths into a human-readable string for the model.
|
|
185
|
+
* Handles both found and not-found cases for one source_table/target_view pair.
|
|
186
|
+
*/
|
|
187
|
+
function formatRelationPathResult(source_table, target_view, result) {
|
|
188
|
+
if (result.error) return `${source_table} → ${target_view}: ${result.error}`;
|
|
189
|
+
if (!result.paths.length)
|
|
190
|
+
return `${source_table} → ${target_view}: no relation paths found (no FK relationship)`;
|
|
191
|
+
const lines = result.paths
|
|
192
|
+
.map((p) => ` "${p.relation_string}" — ${p.label}`)
|
|
193
|
+
.join("\n");
|
|
194
|
+
return `${source_table} → ${target_view}:\n${lines}`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const GET_RELATION_PATHS_FUNCTION = {
|
|
198
|
+
name: "get_relation_paths",
|
|
199
|
+
description:
|
|
200
|
+
"Get all valid relation path strings for one or more source_table/target_view pairs. " +
|
|
201
|
+
"Call this ONCE with all pairs you need before setting any 'relation' property on " +
|
|
202
|
+
"view_link columns or embedded view (type 'view') segments.",
|
|
203
|
+
parameters: {
|
|
204
|
+
type: "object",
|
|
205
|
+
required: ["pairs"],
|
|
206
|
+
properties: {
|
|
207
|
+
pairs: {
|
|
208
|
+
type: "array",
|
|
209
|
+
description:
|
|
210
|
+
"All source_table/target_view pairs you need relation paths for. Include every pair in one call.",
|
|
211
|
+
items: {
|
|
212
|
+
type: "object",
|
|
213
|
+
required: ["source_table", "target_view"],
|
|
214
|
+
properties: {
|
|
215
|
+
source_table: {
|
|
216
|
+
type: "string",
|
|
217
|
+
description: "The table of the view being built or updated.",
|
|
218
|
+
},
|
|
219
|
+
target_view: {
|
|
220
|
+
type: "string",
|
|
221
|
+
description: "The view to link to or embed.",
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
module.exports = {
|
|
231
|
+
RELATION_PATH_DOC,
|
|
232
|
+
GET_RELATION_PATHS_FUNCTION,
|
|
233
|
+
getRelationPaths,
|
|
234
|
+
getRelationPathsForPairs,
|
|
235
|
+
pickBestRelation,
|
|
236
|
+
};
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
const user_registration_hints = `User registration notes:
|
|
2
|
-
* The "users" table is built-in. Passwords are platform-managed — never add a password field to a view.
|
|
3
|
-
* Signup uses a built-in form, not an Edit view.
|
|
4
|
-
* For verification/confirmation emails after registration, create a Trigger with event "Insert" on "users".
|
|
5
|
-
The trigger must use a Workflow action with a single step of type "run_js_code". Do NOT use the send_email action — it will not work for verification. The run_js_code step must contain exactly:
|
|
6
|
-
const { send_verification_email } = require("@saltcorn/data/models/email");
|
|
7
|
-
await send_verification_email(row, req);`;
|
|
8
|
-
|
|
9
|
-
class AppConstructorContextSkill {
|
|
10
|
-
static skill_name = "AppConstructor Context";
|
|
11
|
-
|
|
12
|
-
get skill_label() {
|
|
13
|
-
return "AppConstructor Context";
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
constructor(cfg) {
|
|
17
|
-
Object.assign(this, cfg);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
async systemPrompt() {
|
|
21
|
-
return user_registration_hints;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
module.exports = AppConstructorContextSkill;
|