@saltcorn/copilot 0.7.5 → 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.
@@ -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;