@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,61 @@
|
|
|
1
|
+
const Trigger = require("@saltcorn/data/models/trigger");
|
|
2
|
+
const Table = require("@saltcorn/data/models/table");
|
|
3
|
+
const { a, div, pre } = require("@saltcorn/markup/tags");
|
|
4
|
+
|
|
5
|
+
class GenerateTrigger {
|
|
6
|
+
static title = "Generate Trigger";
|
|
7
|
+
static function_name = "generate_trigger";
|
|
8
|
+
static description =
|
|
9
|
+
"Generate a Saltcorn trigger with any available action type";
|
|
10
|
+
|
|
11
|
+
static render_html({
|
|
12
|
+
action_name,
|
|
13
|
+
action_type,
|
|
14
|
+
when_trigger,
|
|
15
|
+
trigger_table,
|
|
16
|
+
action_config,
|
|
17
|
+
}) {
|
|
18
|
+
const summary =
|
|
19
|
+
`<strong>${action_name}</strong> — ${action_type}` +
|
|
20
|
+
(when_trigger ? `: ${when_trigger}` : "") +
|
|
21
|
+
(trigger_table ? ` on ${trigger_table}` : "");
|
|
22
|
+
const configSection =
|
|
23
|
+
action_config && Object.keys(action_config).length
|
|
24
|
+
? pre({ class: "mt-2" }, JSON.stringify(action_config, null, 2))
|
|
25
|
+
: "";
|
|
26
|
+
return div({ class: "mb-3" }, summary) + configSection;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
static async execute(
|
|
30
|
+
{ action_name, action_type, when_trigger, trigger_table, action_config },
|
|
31
|
+
req,
|
|
32
|
+
) {
|
|
33
|
+
let table_id;
|
|
34
|
+
if (trigger_table) {
|
|
35
|
+
const table = Table.findOne({ name: trigger_table });
|
|
36
|
+
if (!table) return { postExec: `Table not found: ${trigger_table}` };
|
|
37
|
+
table_id = table.id;
|
|
38
|
+
}
|
|
39
|
+
const trigger = await Trigger.create({
|
|
40
|
+
name: action_name,
|
|
41
|
+
when_trigger: when_trigger || "Never",
|
|
42
|
+
table_id,
|
|
43
|
+
action: action_type,
|
|
44
|
+
configuration: action_config || {},
|
|
45
|
+
});
|
|
46
|
+
Trigger.emitEvent("AppChange", `Trigger ${trigger.name}`, req?.user, {
|
|
47
|
+
entity_type: "Trigger",
|
|
48
|
+
entity_name: trigger.name,
|
|
49
|
+
});
|
|
50
|
+
return {
|
|
51
|
+
postExec:
|
|
52
|
+
"Trigger created. " +
|
|
53
|
+
a(
|
|
54
|
+
{ target: "_blank", href: `/actions/configure/${trigger.id}` },
|
|
55
|
+
"Configure trigger.",
|
|
56
|
+
),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = GenerateTrigger;
|
|
@@ -215,7 +215,8 @@ class GenerateWorkflow {
|
|
|
215
215
|
workflow_steps: await steps(),
|
|
216
216
|
workflow_name: {
|
|
217
217
|
description:
|
|
218
|
-
"The name of the workflow. Can include spaces and mixed case, should be 1-5 words."
|
|
218
|
+
"The name of the workflow. Can include spaces and mixed case, should be 1-5 words. " +
|
|
219
|
+
"Must be unique across all workflows. When creating variants for different events on the same table (e.g. insert, update, delete), include the event in each name — e.g. 'Recalc trip packing insert', 'Recalc trip packing update'.",
|
|
219
220
|
type: "string",
|
|
220
221
|
},
|
|
221
222
|
when_trigger: {
|
|
@@ -254,6 +255,10 @@ class GenerateWorkflow {
|
|
|
254
255
|
return `Use the generate_workflow tool to construct computational workflows according to specifications. You must create
|
|
255
256
|
the workflow by calling the generate_workflow tool, with the step required to implement the specification.
|
|
256
257
|
|
|
258
|
+
**Trigger vs workflow:** If the task can be completed in a single step (e.g. updating one field with modify_row, sending a single notification), use the generate_trigger tool instead — a workflow is only appropriate when multiple steps, branching, or looping are required. If the task requires several independent single-step actions (e.g. "mark complete" and "mark incomplete"), create a separate trigger for each — do NOT bundle them into one workflow.
|
|
259
|
+
|
|
260
|
+
**Naming:** Workflow names must be unique. When creating variants for different events on the same table (e.g. insert, update, delete), include the event in each name — e.g. "Recalc trip packing insert", "Recalc trip packing update", "Recalc trip packing delete".
|
|
261
|
+
|
|
257
262
|
${contextBlocks}
|
|
258
263
|
|
|
259
264
|
The steps are specified as JSON objects. Each step has a name, specified in the step_name key in the JSON object.
|
package/agent-skills/pagegen.js
CHANGED
|
@@ -57,7 +57,7 @@ class GeneratePageSkill {
|
|
|
57
57
|
`${name}.html`,
|
|
58
58
|
"text/html",
|
|
59
59
|
html,
|
|
60
|
-
user
|
|
60
|
+
user?.id,
|
|
61
61
|
100,
|
|
62
62
|
);
|
|
63
63
|
|
|
@@ -96,7 +96,7 @@ class GeneratePageSkill {
|
|
|
96
96
|
);
|
|
97
97
|
} else return "Metadata recieved";
|
|
98
98
|
},
|
|
99
|
-
postProcess: async ({ tool_call, generate }) => {
|
|
99
|
+
postProcess: async ({ tool_call, generate, req }) => {
|
|
100
100
|
const str = await generate(
|
|
101
101
|
`Now generate the contents of the ${tool_call.input.name} HTML page. If I asked you to embed a view,
|
|
102
102
|
use the <embed-view> self-closing tag to do so, setting the view name in the viewname attribute. For example,
|
|
@@ -120,6 +120,19 @@ class GeneratePageSkill {
|
|
|
120
120
|
? str.split("```html")[1].split("```")[0]
|
|
121
121
|
: str;
|
|
122
122
|
|
|
123
|
+
if (this.yoloMode) {
|
|
124
|
+
await this.userActions.build_copilot_page_gen({
|
|
125
|
+
user: req?.user,
|
|
126
|
+
name: tool_call.input.name,
|
|
127
|
+
title: tool_call.input.title,
|
|
128
|
+
description: tool_call.input.description,
|
|
129
|
+
html,
|
|
130
|
+
});
|
|
131
|
+
return {
|
|
132
|
+
stop: true,
|
|
133
|
+
add_response: `Page ${tool_call.input.name} created.`,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
123
136
|
return {
|
|
124
137
|
stop: true,
|
|
125
138
|
add_response: iframe({
|
|
@@ -127,10 +140,10 @@ class GeneratePageSkill {
|
|
|
127
140
|
width: 500,
|
|
128
141
|
height: 800,
|
|
129
142
|
}),
|
|
130
|
-
add_system_prompt: `If the user asks you to regenerate the page,
|
|
131
|
-
you must run the generate_page tool again. After running this tool
|
|
132
|
-
you will be prompted to generate the html again. You should repeat
|
|
133
|
-
the html from the previous answer except for the changes the user
|
|
143
|
+
add_system_prompt: `If the user asks you to regenerate the page,
|
|
144
|
+
you must run the generate_page tool again. After running this tool
|
|
145
|
+
you will be prompted to generate the html again. You should repeat
|
|
146
|
+
the html from the previous answer except for the changes the user
|
|
134
147
|
is requesting.`,
|
|
135
148
|
add_user_action: {
|
|
136
149
|
name: "build_copilot_page_gen",
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
const Trigger = require("@saltcorn/data/models/trigger");
|
|
2
|
+
const Table = require("@saltcorn/data/models/table");
|
|
3
|
+
const { getState } = require("@saltcorn/data/db/state");
|
|
4
|
+
const { getActionConfigFields } = require("@saltcorn/data/plugin-helper");
|
|
5
|
+
const { fieldProperties } = require("../common");
|
|
6
|
+
const GenerateAnyAction = require("../actions/generate-trigger");
|
|
7
|
+
|
|
8
|
+
const flattenOptionGroups = (options = []) =>
|
|
9
|
+
options.flatMap((opt) =>
|
|
10
|
+
opt?.optgroup && Array.isArray(opt.options) ? opt.options : [opt],
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
class AnyActionSkill {
|
|
14
|
+
static skill_name = "Generate trigger";
|
|
15
|
+
|
|
16
|
+
get skill_label() {
|
|
17
|
+
return "Generate trigger";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
constructor(cfg) {
|
|
21
|
+
Object.assign(this, cfg);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async systemPrompt() {
|
|
25
|
+
return (
|
|
26
|
+
`If the user asks to create an action or trigger, use the generate_trigger tool. ` +
|
|
27
|
+
`Pick the most appropriate action_type from the available options. ` +
|
|
28
|
+
`Only set when_trigger and trigger_table if the user has specified them. ` +
|
|
29
|
+
`Trigger names must be unique — when creating variants for different events on the same table, include the event in each name (e.g. "Recalc trip packing insert", "Recalc trip packing update", "Recalc trip packing delete").\n\n` +
|
|
30
|
+
`**Trigger vs workflow:** Use a trigger (this tool) when the action is a single step — ` +
|
|
31
|
+
`for example, setting a field value with modify_row, sending a notification, or calling an API. ` +
|
|
32
|
+
`Only use a workflow when the logic requires multiple steps, branching, or looping. ` +
|
|
33
|
+
`If the task requires several independent single-step actions (e.g. "mark complete" and "mark incomplete"), call this tool once per action — do NOT bundle them into one workflow.\n\n` +
|
|
34
|
+
`**Navigation is a view concern:** If a task description says "return the user to X" or "navigate back", ` +
|
|
35
|
+
`do NOT add a navigation step inside the trigger. Triggers only handle data operations. ` +
|
|
36
|
+
`Navigation (GoBack) is configured on the button in the list view, not inside the trigger itself.\n\n` +
|
|
37
|
+
`**Verification/confirmation emails after user registration:** Create a trigger with event "Insert" on the "users" table, ` +
|
|
38
|
+
`action_type "run_js_code". Do NOT use the send_email action — it will not work for verification. ` +
|
|
39
|
+
`Set the action_config "code" field to exactly:\n` +
|
|
40
|
+
`const { send_verification_email } = require("@saltcorn/data/models/email");\nawait send_verification_email(row, req);`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get userActions() {
|
|
45
|
+
return {
|
|
46
|
+
async build_copilot_any_action(input) {
|
|
47
|
+
const {
|
|
48
|
+
name,
|
|
49
|
+
action_type,
|
|
50
|
+
when_trigger,
|
|
51
|
+
trigger_table,
|
|
52
|
+
action_config,
|
|
53
|
+
user,
|
|
54
|
+
} = input;
|
|
55
|
+
if (!name || !action_type) {
|
|
56
|
+
return { notify: "Action name and type are required." };
|
|
57
|
+
}
|
|
58
|
+
const result = await GenerateAnyAction.execute(
|
|
59
|
+
{
|
|
60
|
+
action_name: name,
|
|
61
|
+
action_type,
|
|
62
|
+
when_trigger,
|
|
63
|
+
trigger_table,
|
|
64
|
+
action_config,
|
|
65
|
+
},
|
|
66
|
+
{ user },
|
|
67
|
+
);
|
|
68
|
+
return { notify: result?.postExec || `Action saved: ${name}` };
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
provideTools = () => {
|
|
74
|
+
const state = getState();
|
|
75
|
+
const allActionOptions = (() => {
|
|
76
|
+
try {
|
|
77
|
+
return (
|
|
78
|
+
Trigger.action_options({ notRequireRow: false, workflow: false }) ||
|
|
79
|
+
[]
|
|
80
|
+
);
|
|
81
|
+
} catch (_) {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
})();
|
|
85
|
+
const stateActions = state?.actions || {};
|
|
86
|
+
const stateActionNames = Object.keys(stateActions);
|
|
87
|
+
const catalogNames = flattenOptionGroups(allActionOptions);
|
|
88
|
+
const actionEnum = Array.from(
|
|
89
|
+
new Set([...catalogNames, ...stateActionNames]),
|
|
90
|
+
).sort();
|
|
91
|
+
|
|
92
|
+
const tables = (state.tables || []).map((t) => t.name);
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
type: "function",
|
|
96
|
+
function: {
|
|
97
|
+
name: GenerateAnyAction.function_name,
|
|
98
|
+
description: GenerateAnyAction.description,
|
|
99
|
+
parameters: {
|
|
100
|
+
type: "object",
|
|
101
|
+
required: ["name", "action_type"],
|
|
102
|
+
properties: {
|
|
103
|
+
name: {
|
|
104
|
+
type: "string",
|
|
105
|
+
description:
|
|
106
|
+
"A human-readable name for the trigger/action (1–5 words). " +
|
|
107
|
+
"Must be unique across all triggers. When creating multiple triggers for different events on the same table (e.g. insert, update, delete), include the event in the name — e.g. 'Recalc trip packing insert', 'Recalc trip packing update'.",
|
|
108
|
+
},
|
|
109
|
+
action_type: {
|
|
110
|
+
type: "string",
|
|
111
|
+
enum: actionEnum.length ? actionEnum : undefined,
|
|
112
|
+
description: "The action to run when the trigger fires.",
|
|
113
|
+
},
|
|
114
|
+
when_trigger: {
|
|
115
|
+
type: "string",
|
|
116
|
+
enum: Trigger.when_options,
|
|
117
|
+
description:
|
|
118
|
+
"When to fire this trigger. Only set if the user has specified. Leave unset for manual/API-call triggers.",
|
|
119
|
+
},
|
|
120
|
+
trigger_table: {
|
|
121
|
+
type: "string",
|
|
122
|
+
enum: tables,
|
|
123
|
+
description:
|
|
124
|
+
"Table for row-level triggers (Insert/Update/Delete/Validate). Only set when when_trigger requires a table.",
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
process: async (input) => {
|
|
130
|
+
const { name, action_type, when_trigger, trigger_table } = input || {};
|
|
131
|
+
return [
|
|
132
|
+
`Generating ${action_type} action: ${name}.`,
|
|
133
|
+
when_trigger ? `Trigger: ${when_trigger}` : null,
|
|
134
|
+
trigger_table ? `Table: ${trigger_table}` : null,
|
|
135
|
+
]
|
|
136
|
+
.filter(Boolean)
|
|
137
|
+
.join("\n");
|
|
138
|
+
},
|
|
139
|
+
postProcess: async ({ tool_call, generate }) => {
|
|
140
|
+
const { name, action_type, when_trigger, trigger_table } =
|
|
141
|
+
tool_call.input || {};
|
|
142
|
+
if (!name || !action_type) {
|
|
143
|
+
return {
|
|
144
|
+
stop: true,
|
|
145
|
+
add_response: "Action name and type are required.",
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const stateAction = getState()?.actions?.[action_type];
|
|
150
|
+
let action_config = {};
|
|
151
|
+
|
|
152
|
+
if (stateAction) {
|
|
153
|
+
const table = trigger_table
|
|
154
|
+
? Table.findOne({ name: trigger_table })
|
|
155
|
+
: null;
|
|
156
|
+
let cfgFields = [];
|
|
157
|
+
try {
|
|
158
|
+
cfgFields = await getActionConfigFields(stateAction, table, {
|
|
159
|
+
copilot: true,
|
|
160
|
+
});
|
|
161
|
+
} catch (_) {}
|
|
162
|
+
|
|
163
|
+
const configurable = cfgFields.filter(
|
|
164
|
+
(f) => f.input_type !== "section_header",
|
|
165
|
+
);
|
|
166
|
+
if (configurable.length > 0) {
|
|
167
|
+
const properties = {};
|
|
168
|
+
for (const f of configurable) {
|
|
169
|
+
properties[f.name] = {
|
|
170
|
+
description: f.sublabel || f.label || f.name,
|
|
171
|
+
...fieldProperties(f),
|
|
172
|
+
};
|
|
173
|
+
if (!properties[f.name].type) properties[f.name].type = "string";
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
let actionPrompt = "";
|
|
177
|
+
if (stateAction.copilot_generate_trigger_prompt) {
|
|
178
|
+
if (
|
|
179
|
+
typeof stateAction.copilot_generate_trigger_prompt === "string"
|
|
180
|
+
)
|
|
181
|
+
actionPrompt = stateAction.copilot_generate_trigger_prompt;
|
|
182
|
+
else if (
|
|
183
|
+
typeof stateAction.copilot_generate_trigger_prompt ===
|
|
184
|
+
"function"
|
|
185
|
+
)
|
|
186
|
+
actionPrompt =
|
|
187
|
+
await stateAction.copilot_generate_trigger_prompt(
|
|
188
|
+
tool_call.input,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const llm = getState().functions.llm_generate;
|
|
193
|
+
const answer = await llm.run(
|
|
194
|
+
`${actionPrompt ? actionPrompt + "\n\n" : ""}Configure the "${action_type}" action named "${name}". ` +
|
|
195
|
+
`Fill in the configuration by calling the generate_action_config tool.`,
|
|
196
|
+
{
|
|
197
|
+
tools: [
|
|
198
|
+
{
|
|
199
|
+
type: "function",
|
|
200
|
+
function: {
|
|
201
|
+
name: "generate_action_config",
|
|
202
|
+
description: `Provide configuration fields for the ${action_type} action`,
|
|
203
|
+
parameters: { type: "object", properties },
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
tool_choice: {
|
|
208
|
+
type: "function",
|
|
209
|
+
function: { name: "generate_action_config" },
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
const tc = answer.getToolCalls()[0];
|
|
215
|
+
if (tc?.input) action_config = tc.input;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (this.yoloMode) {
|
|
220
|
+
const result = await GenerateAnyAction.execute(
|
|
221
|
+
{
|
|
222
|
+
action_name: name,
|
|
223
|
+
action_type,
|
|
224
|
+
when_trigger,
|
|
225
|
+
trigger_table,
|
|
226
|
+
action_config,
|
|
227
|
+
},
|
|
228
|
+
{},
|
|
229
|
+
);
|
|
230
|
+
return {
|
|
231
|
+
stop: true,
|
|
232
|
+
add_response: result?.postExec || `Action ${name} created.`,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
stop: true,
|
|
238
|
+
add_response: GenerateAnyAction.render_html({
|
|
239
|
+
action_name: name,
|
|
240
|
+
action_type,
|
|
241
|
+
when_trigger,
|
|
242
|
+
trigger_table,
|
|
243
|
+
action_config,
|
|
244
|
+
}),
|
|
245
|
+
add_user_action: {
|
|
246
|
+
name: "build_copilot_any_action",
|
|
247
|
+
type: "button",
|
|
248
|
+
label: `Save action (${name})`,
|
|
249
|
+
input: {
|
|
250
|
+
name,
|
|
251
|
+
action_type,
|
|
252
|
+
when_trigger,
|
|
253
|
+
trigger_table,
|
|
254
|
+
action_config,
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
module.exports = AnyActionSkill;
|