@saltcorn/copilot 0.2.0 → 0.3.0
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/action-builder.js +5 -1
- package/actions/generate-js-action.js +108 -0
- package/actions/generate-tables.js +259 -0
- package/actions/generate-workflow.js +320 -0
- package/chat-copilot.js +471 -0
- package/common.js +47 -1
- package/index.js +3 -1
- package/package.json +2 -2
- package/prompts/action-builder.txt +1 -5
- package/workflow-gen.js +4 -258
- package/GenWorkflowCodePage.js +0 -333
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
const { getState } = require("@saltcorn/data/db/state");
|
|
2
|
+
const WorkflowStep = require("@saltcorn/data/models/workflow_step");
|
|
3
|
+
const Trigger = require("@saltcorn/data/models/trigger");
|
|
4
|
+
const Table = require("@saltcorn/data/models/table");
|
|
5
|
+
const { getActionConfigFields } = require("@saltcorn/data/plugin-helper");
|
|
6
|
+
const { a, pre, script, div } = require("@saltcorn/markup/tags");
|
|
7
|
+
const { fieldProperties } = require("../common");
|
|
8
|
+
|
|
9
|
+
const steps = async () => {
|
|
10
|
+
const actionExplainers = WorkflowStep.builtInActionExplainers();
|
|
11
|
+
const actionFields = await WorkflowStep.builtInActionConfigFields();
|
|
12
|
+
|
|
13
|
+
let stateActions = getState().actions;
|
|
14
|
+
const stateActionList = Object.entries(stateActions).filter(
|
|
15
|
+
([k, v]) => !v.disableInWorkflow
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
const stepTypeAndCfg = Object.keys(actionExplainers).map((actionName) => {
|
|
19
|
+
const properties = { step_type: { const: actionName } };
|
|
20
|
+
const myFields = actionFields.filter(
|
|
21
|
+
(f) => f.showIf?.wf_action_name === actionName
|
|
22
|
+
);
|
|
23
|
+
const required = ["step_type"];
|
|
24
|
+
myFields.forEach((f) => {
|
|
25
|
+
if (f.required) required.push(f.name);
|
|
26
|
+
properties[f.name] = {
|
|
27
|
+
description: f.sublabel || f.label,
|
|
28
|
+
...fieldProperties(f),
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
return {
|
|
32
|
+
type: "object",
|
|
33
|
+
description: actionExplainers[actionName],
|
|
34
|
+
properties,
|
|
35
|
+
required,
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
for (const [actionName, action] of stateActionList) {
|
|
39
|
+
try {
|
|
40
|
+
const properties = { step_type: { const: actionName } };
|
|
41
|
+
const cfgFields = await getActionConfigFields(action, null, {
|
|
42
|
+
mode: "workflow",
|
|
43
|
+
copilot: true,
|
|
44
|
+
});
|
|
45
|
+
const required = ["step_type"];
|
|
46
|
+
cfgFields.forEach((f) => {
|
|
47
|
+
if (f.input_type === "section_header") return;
|
|
48
|
+
if (f.required) required.push(f.name);
|
|
49
|
+
properties[f.name] = {
|
|
50
|
+
description: f.sublabel || f.label,
|
|
51
|
+
...fieldProperties(f),
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
stepTypeAndCfg.push({
|
|
55
|
+
type: "object",
|
|
56
|
+
description:
|
|
57
|
+
actionExplainers[actionName] ||
|
|
58
|
+
`${actionName}.${action.description ? ` ${action.description}` : ""}`,
|
|
59
|
+
properties,
|
|
60
|
+
required,
|
|
61
|
+
});
|
|
62
|
+
} catch (e) {
|
|
63
|
+
//ignore
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const triggers = Trigger.find({
|
|
67
|
+
when_trigger: { or: ["API call", "Never"] },
|
|
68
|
+
}).filter((tr) => tr.description && tr.name && tr !== "Workflow");
|
|
69
|
+
//TODO workflows
|
|
70
|
+
for (const trigger of triggers) {
|
|
71
|
+
const properties = {
|
|
72
|
+
step_type: { const: trigger.name },
|
|
73
|
+
};
|
|
74
|
+
if (trigger.table_id) {
|
|
75
|
+
const table = Table.findOne({ id: trigger.table_id });
|
|
76
|
+
const fieldSpecs = [];
|
|
77
|
+
table.fields.forEach((f) => {
|
|
78
|
+
// TODO fkeys dereferenced.
|
|
79
|
+
fieldSpecs.push(`${f.name} with ${f.pretty_type} type`);
|
|
80
|
+
});
|
|
81
|
+
properties.row_expr = {
|
|
82
|
+
type: "string",
|
|
83
|
+
description: `JavaScript expression for the input to the action. This should be an expression for an object, with the following field name and types: ${fieldSpecs.join(
|
|
84
|
+
"; "
|
|
85
|
+
)}.`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const required = ["step_type"];
|
|
89
|
+
stepTypeAndCfg.push({
|
|
90
|
+
type: "object",
|
|
91
|
+
description: `${trigger.name}: ${trigger.description}`,
|
|
92
|
+
properties,
|
|
93
|
+
required,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
const properties = {
|
|
97
|
+
step_name: {
|
|
98
|
+
description: "The name of this step as a valid Javascript identifier",
|
|
99
|
+
type: "string",
|
|
100
|
+
},
|
|
101
|
+
only_if: {
|
|
102
|
+
description:
|
|
103
|
+
"Optional JavaScript expression based on the context. If given, the chosen action will only be executed if evaluates to true",
|
|
104
|
+
type: "string",
|
|
105
|
+
},
|
|
106
|
+
/*step_type: {
|
|
107
|
+
description: "The type of workflow step",
|
|
108
|
+
type: "string",
|
|
109
|
+
enum: Object.keys(actionExplainers),
|
|
110
|
+
},*/
|
|
111
|
+
next_step: {
|
|
112
|
+
description:
|
|
113
|
+
"The next step in the workflow, as a JavaScript expression based on the context.",
|
|
114
|
+
type: "string",
|
|
115
|
+
},
|
|
116
|
+
step_configuration: { anyOf: stepTypeAndCfg },
|
|
117
|
+
};
|
|
118
|
+
return {
|
|
119
|
+
type: "array",
|
|
120
|
+
items: {
|
|
121
|
+
type: "object",
|
|
122
|
+
properties,
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
class GenerateWorkflow {
|
|
128
|
+
static title = "Generate Workflow";
|
|
129
|
+
static function_name = "generate_workflow";
|
|
130
|
+
static description = "Generate the steps in a workflow";
|
|
131
|
+
|
|
132
|
+
static async json_schema() {
|
|
133
|
+
return {
|
|
134
|
+
type: "object",
|
|
135
|
+
properties: {
|
|
136
|
+
workflow_steps: await steps(),
|
|
137
|
+
workflow_name: {
|
|
138
|
+
description:
|
|
139
|
+
"The name of the workflow. Can include spaces and mixed case, should be 1-5 words.",
|
|
140
|
+
type: "string",
|
|
141
|
+
},
|
|
142
|
+
when_trigger: {
|
|
143
|
+
description:
|
|
144
|
+
"When the workflow should trigger. Optional, leave blank if unspecified or workflow will be run on button click",
|
|
145
|
+
type: "string",
|
|
146
|
+
enum: ["Insert", "Delete", "Update", "Daily", "Hourly", "Weekly"],
|
|
147
|
+
},
|
|
148
|
+
trigger_table: {
|
|
149
|
+
description:
|
|
150
|
+
"If the workflow trigger is Insert, Delete or Update, the name of the table that triggers the workflow",
|
|
151
|
+
type: "string",
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
static async system_prompt() {
|
|
158
|
+
const actionExplainers = WorkflowStep.builtInActionExplainers();
|
|
159
|
+
let stateActions = getState().actions;
|
|
160
|
+
const stateActionList = Object.entries(stateActions).filter(
|
|
161
|
+
([k, v]) => !v.disableInWorkflow
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
return `Use the generate_workflow tool to construct computational workflows according to specifications. You must create
|
|
165
|
+
the workflow by calling the generate_workflow tool, with the step required to implement the specification.
|
|
166
|
+
|
|
167
|
+
The steps are specified as JSON objects. Each step has a name, specified in the step_name key in the JSON object.
|
|
168
|
+
The step name should be a valid JavaScript identifier.
|
|
169
|
+
|
|
170
|
+
Each run of the workflow is executed in the presence of a context, which is a JavaScript object that individual
|
|
171
|
+
steps can read values from and write values to. This context is a state that is persisted on disk for each workflow
|
|
172
|
+
run.
|
|
173
|
+
|
|
174
|
+
Each step can have a next_step key which is the name of the next step, or a JavaScript expression which evaluates
|
|
175
|
+
to the name of the next step based on the context. In the evaluation of the next step, each value in the context is
|
|
176
|
+
in scope and can be addressed directly. Identifiers for the step names are also in scope, the name of the next step
|
|
177
|
+
can be used directly without enclosing it in quotes to form a string.
|
|
178
|
+
|
|
179
|
+
For example, if the context contains a value x which is an integer and you have steps named "too_low" and "too_high",
|
|
180
|
+
and you would like the next step to be too_low if x is less than 10 and too_high otherwise,
|
|
181
|
+
use this as the next_step expression: x<10 ? too_low : too_high
|
|
182
|
+
|
|
183
|
+
If the next_step is omitted then the workflow terminates.
|
|
184
|
+
|
|
185
|
+
Each step has a step_configuration object which contains the step type and the specific parameters of
|
|
186
|
+
that step type. You should specify the step type in the step_type subfield of the step_configuration
|
|
187
|
+
field. The available step types are:
|
|
188
|
+
|
|
189
|
+
${Object.entries(actionExplainers)
|
|
190
|
+
.map(([k, v]) => `* ${k}: ${v}`)
|
|
191
|
+
.join("\n")}
|
|
192
|
+
${stateActionList
|
|
193
|
+
.map(([k, v]) => `* ${k}: ${v.description || ""}`)
|
|
194
|
+
.join("\n")}
|
|
195
|
+
|
|
196
|
+
Most of them are are explained by their parameter descriptions. Here are some additional information for some
|
|
197
|
+
step types:
|
|
198
|
+
|
|
199
|
+
run_js_code: if the step_type is "run_js_code" then the step object should include the JavaScript code to be executed in the "code"
|
|
200
|
+
key. You can use await in the code if you need to run asynchronous code. The values in the context are directly in scope and can be accessed using their name. In addition, the variable
|
|
201
|
+
"context" is also in scope and can be used to address the context as a whole. To write values to the context, return an
|
|
202
|
+
object. The values in this object will be written into the current context. If a value already exists in the context
|
|
203
|
+
it will be overwritten. For example, If the context contains values x and y which are numbers and you would like to push
|
|
204
|
+
the value "sum" which is the sum of x and y, then use this as the code: return {sum: x+y}. You cannot set the next step in the
|
|
205
|
+
return object or by returning a string from a run_js_code step, this will not work. To set the next step from a code action, always use the next_step property of the step object.
|
|
206
|
+
This expression for the next step can depend on value pushed to the context (by the return object in the code) as these values are in scope.
|
|
207
|
+
|
|
208
|
+
ForLoop: ForLoop steps loop over an array which is specified by the array_expression JavaScript expression. Execution of the workflow steps is temporarily diverted to another set
|
|
209
|
+
of steps, starting from the step specified by the loop_body_inital_step value, and runs until it encounters a
|
|
210
|
+
step with nothing specified for next_step at which point the next iteration (over the next item in the array) is started. When all items have
|
|
211
|
+
been iterated over, the for loop is complete and execution continues with the next_step of the ForLoop step. During each iteration
|
|
212
|
+
of the loop, the current array item is temporarily set to a variable in the context specified by the item_variable variable. The steps between
|
|
213
|
+
in the loop body can access this current array items in the context by the context item_variable name.
|
|
214
|
+
When all items have been iterated, the for loop will continue from the step indicated by its next_step.
|
|
215
|
+
|
|
216
|
+
llm_generate: use a llm_generate step to consult an artificial intelligence language processor to ask a question in natural language in which the answer is given in natural language. The answer is based on a
|
|
217
|
+
question, specified as a string in the step conmfiguration "prompt_template" key in which you can user interpolation ({{ }}) to access context variables. Running the step will provide an answer by a
|
|
218
|
+
highly capable artificial intelligence processor who however does not have in-depth knowledge of the subject matter or any case specifics at hand - you
|
|
219
|
+
must provide all these details in the question string, which should concatenate multiple background documents before asking the
|
|
220
|
+
actual question. You must also provide a variable name (in the answer_field key in the step definition) where the answer
|
|
221
|
+
will be pushed to the context as a string. If you specificy a variable name in chat_history_field, the invocation of subsequent llm_generate
|
|
222
|
+
steps in the same workflow will contain the interaction history of previous invocations, so you don't have to repeat information in the prompt and can
|
|
223
|
+
maintain a conversational interaction.
|
|
224
|
+
|
|
225
|
+
llm_generate_json: use llm_generate_json steps to extract structured information from text. llm_generate_json uses natural language processing to read a document,
|
|
226
|
+
and to generate JSON objects with specified fields. A llm_generate_json step requires four settings in the step object: gen_description,
|
|
227
|
+
a general description of what it is that should be extracted; fields, which is an array of the fields in each object that is
|
|
228
|
+
extracted from the text, each with a name, a type and a description; multiple a boolean and that indicates whether exactly one object
|
|
229
|
+
or an array with any number of objects should be extracted; and answer_field, the name of the variable should be written to in the
|
|
230
|
+
context (as an object if multiple is false and as an array if multiple is true).`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
static async execute(
|
|
234
|
+
{ workflow_steps, workflow_name, when_trigger, trigger_table },
|
|
235
|
+
req
|
|
236
|
+
) {
|
|
237
|
+
const steps = this.process_all_steps(workflow_steps);
|
|
238
|
+
let table_id;
|
|
239
|
+
if (trigger_table) {
|
|
240
|
+
const table = Table.findOne({ name: trigger_table });
|
|
241
|
+
if (!table) return { postExec: `Table not found: ${trigger_table}` };
|
|
242
|
+
table_id = table.id;
|
|
243
|
+
}
|
|
244
|
+
const trigger = await Trigger.create({
|
|
245
|
+
name: workflow_name,
|
|
246
|
+
when_trigger: when_trigger || "Never",
|
|
247
|
+
table_id,
|
|
248
|
+
action: "Workflow",
|
|
249
|
+
configuration: {},
|
|
250
|
+
});
|
|
251
|
+
for (const step of steps) {
|
|
252
|
+
step.trigger_id = trigger.id;
|
|
253
|
+
await WorkflowStep.create(step);
|
|
254
|
+
}
|
|
255
|
+
Trigger.emitEvent("AppChange", `Trigger ${trigger.name}`, req?.user, {
|
|
256
|
+
entity_type: "Trigger",
|
|
257
|
+
entity_name: trigger.name,
|
|
258
|
+
});
|
|
259
|
+
return {
|
|
260
|
+
postExec:
|
|
261
|
+
"Workflow created. " +
|
|
262
|
+
a(
|
|
263
|
+
{ target: "_blank", href: `/actions/configure/${trigger.id}` },
|
|
264
|
+
"Configure workflow."
|
|
265
|
+
),
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
static render_html({
|
|
270
|
+
workflow_steps,
|
|
271
|
+
workflow_name,
|
|
272
|
+
when_trigger,
|
|
273
|
+
trigger_table,
|
|
274
|
+
}) {
|
|
275
|
+
const steps = this.process_all_steps(workflow_steps);
|
|
276
|
+
|
|
277
|
+
if (WorkflowStep.generate_diagram) {
|
|
278
|
+
steps.forEach((step, ix) => {
|
|
279
|
+
step.id = ix + 1;
|
|
280
|
+
});
|
|
281
|
+
const mmdia = WorkflowStep.generate_diagram(
|
|
282
|
+
steps.map((s) => new WorkflowStep(s))
|
|
283
|
+
);
|
|
284
|
+
return (
|
|
285
|
+
div(
|
|
286
|
+
`${workflow_name}${when_trigger ? `: ${when_trigger}` : ""}${
|
|
287
|
+
trigger_table ? ` on ${trigger_table}` : ""
|
|
288
|
+
}`
|
|
289
|
+
) +
|
|
290
|
+
pre({ class: "mermaid" }, mmdia) +
|
|
291
|
+
script(`mermaid.run({querySelector: 'pre.mermaid'});`)
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return `A workflow! Step names: ${workflow_steps.map(
|
|
296
|
+
(s) => s.step_name
|
|
297
|
+
)}. Upgrade Saltcorn to see diagrams in copilot`;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
//specific methods
|
|
301
|
+
|
|
302
|
+
static process_all_steps(steps) {
|
|
303
|
+
const scsteps = steps.map((s) => this.to_saltcorn_step(s));
|
|
304
|
+
if (scsteps.length) scsteps[0].initial_step = true;
|
|
305
|
+
return scsteps;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
static to_saltcorn_step(llm_step) {
|
|
309
|
+
const { step_type, ...configuration } = llm_step.step_configuration;
|
|
310
|
+
return {
|
|
311
|
+
name: llm_step.step_name,
|
|
312
|
+
action_name: step_type,
|
|
313
|
+
next_step: llm_step.next_step,
|
|
314
|
+
only_if: llm_step.only_if,
|
|
315
|
+
configuration,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
module.exports = GenerateWorkflow;
|