@saltcorn/copilot 0.1.1 → 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 +7 -1
- package/package.json +2 -2
- package/prompts/action-builder.txt +1 -5
- package/workflow-gen.js +45 -0
package/action-builder.js
CHANGED
|
@@ -128,10 +128,14 @@ const runPost = async (
|
|
|
128
128
|
form.hasErrors = false;
|
|
129
129
|
form.errors = {};
|
|
130
130
|
|
|
131
|
-
const
|
|
131
|
+
const partPrompt = await getPromptFromTemplate(
|
|
132
132
|
"action-builder.txt",
|
|
133
133
|
form.values.prompt
|
|
134
134
|
);
|
|
135
|
+
const fullPrompt =
|
|
136
|
+
partPrompt +
|
|
137
|
+
"\n\nWrite the JavaScript code to implement the following functionality:\n\n" +
|
|
138
|
+
form.values.prompt;
|
|
135
139
|
|
|
136
140
|
const completion = await getCompletion("JavaScript", fullPrompt);
|
|
137
141
|
|
|
@@ -0,0 +1,108 @@
|
|
|
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 Field = require("@saltcorn/data/models/field");
|
|
6
|
+
const { apply, removeAllWhiteSpace } = require("@saltcorn/data/utils");
|
|
7
|
+
const { getActionConfigFields } = require("@saltcorn/data/plugin-helper");
|
|
8
|
+
const { a, pre, script, div, code } = require("@saltcorn/markup/tags");
|
|
9
|
+
const { fieldProperties, getPromptFromTemplate } = require("../common");
|
|
10
|
+
|
|
11
|
+
class GenerateJsAction {
|
|
12
|
+
static title = "Generate JavaScript Action";
|
|
13
|
+
static function_name = "generate_js_action";
|
|
14
|
+
static description = "Generate Javascript Action";
|
|
15
|
+
|
|
16
|
+
static async json_schema() {
|
|
17
|
+
return {
|
|
18
|
+
type: "object",
|
|
19
|
+
required: ["action_javascript_code", "action_name"],
|
|
20
|
+
properties: {
|
|
21
|
+
action_javascript_code: {
|
|
22
|
+
description: "JavaScript code that constitutes the action",
|
|
23
|
+
type: "string",
|
|
24
|
+
},
|
|
25
|
+
action_name: {
|
|
26
|
+
description:
|
|
27
|
+
"A human-readable label for the action. Can include spaces and mixed case, should be 1-5 words.",
|
|
28
|
+
type: "string",
|
|
29
|
+
},
|
|
30
|
+
action_description: {
|
|
31
|
+
description: "A description of the purpose of the action.",
|
|
32
|
+
type: "string",
|
|
33
|
+
},
|
|
34
|
+
when_trigger: {
|
|
35
|
+
description:
|
|
36
|
+
"When the action should trigger. Optional, leave blank if unspecified or workflow will be run on button click",
|
|
37
|
+
type: "string",
|
|
38
|
+
enum: ["Insert", "Delete", "Update", "Daily", "Hourly", "Weekly"],
|
|
39
|
+
},
|
|
40
|
+
trigger_table: {
|
|
41
|
+
description:
|
|
42
|
+
"If the action trigger is Insert, Delete or Update, the name of the table that triggers the workflow",
|
|
43
|
+
type: "string",
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
static async system_prompt() {
|
|
49
|
+
const partPrompt = await getPromptFromTemplate("action-builder.txt", "");
|
|
50
|
+
return (
|
|
51
|
+
`Use the generate_js_action to generate actions based on JavaScript code. ` +
|
|
52
|
+
partPrompt
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
static render_html({
|
|
56
|
+
action_javascript_code,
|
|
57
|
+
action_name,
|
|
58
|
+
action_description,
|
|
59
|
+
when_trigger,
|
|
60
|
+
trigger_table,
|
|
61
|
+
}) {
|
|
62
|
+
return (
|
|
63
|
+
div({class: "mb-3"},
|
|
64
|
+
`${action_name}${when_trigger ? `: ${when_trigger}` : ""}${
|
|
65
|
+
trigger_table ? ` on ${trigger_table}` : ""
|
|
66
|
+
}`
|
|
67
|
+
) + pre(code(action_javascript_code))
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
static async execute(
|
|
71
|
+
{
|
|
72
|
+
action_javascript_code,
|
|
73
|
+
action_name,
|
|
74
|
+
action_description,
|
|
75
|
+
when_trigger,
|
|
76
|
+
trigger_table,
|
|
77
|
+
},
|
|
78
|
+
req
|
|
79
|
+
) {
|
|
80
|
+
let table_id;
|
|
81
|
+
if (trigger_table) {
|
|
82
|
+
const table = Table.findOne({ name: trigger_table });
|
|
83
|
+
if (!table) return { postExec: `Table not found: ${trigger_table}` };
|
|
84
|
+
table_id = table.id;
|
|
85
|
+
}
|
|
86
|
+
const trigger = await Trigger.create({
|
|
87
|
+
name: action_name,
|
|
88
|
+
when_trigger: when_trigger || "Never",
|
|
89
|
+
table_id,
|
|
90
|
+
action: "run_js_code",
|
|
91
|
+
configuration: {code: action_javascript_code},
|
|
92
|
+
});
|
|
93
|
+
Trigger.emitEvent("AppChange", `Trigger ${trigger.name}`, req?.user, {
|
|
94
|
+
entity_type: "Trigger",
|
|
95
|
+
entity_name: trigger.name,
|
|
96
|
+
});
|
|
97
|
+
return {
|
|
98
|
+
postExec:
|
|
99
|
+
"Action created. " +
|
|
100
|
+
a(
|
|
101
|
+
{ target: "_blank", href: `/actions/configure/${trigger.id}` },
|
|
102
|
+
"Configure action."
|
|
103
|
+
),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = GenerateJsAction;
|
|
@@ -0,0 +1,259 @@
|
|
|
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 Field = require("@saltcorn/data/models/field");
|
|
6
|
+
const { apply, removeAllWhiteSpace } = require("@saltcorn/data/utils");
|
|
7
|
+
const { getActionConfigFields } = require("@saltcorn/data/plugin-helper");
|
|
8
|
+
const { a, pre, script, div } = require("@saltcorn/markup/tags");
|
|
9
|
+
const { fieldProperties } = require("../common");
|
|
10
|
+
|
|
11
|
+
class GenerateTables {
|
|
12
|
+
static title = "Generate Tables";
|
|
13
|
+
static function_name = "generate_tables";
|
|
14
|
+
static description = "Generate database tables";
|
|
15
|
+
|
|
16
|
+
static async json_schema() {
|
|
17
|
+
const types = Object.values(getState().types);
|
|
18
|
+
const fieldTypeCfg = types.map((ty) => {
|
|
19
|
+
const properties = {
|
|
20
|
+
data_type: { const: ty.name },
|
|
21
|
+
};
|
|
22
|
+
const attrs = apply(ty.attributes, {}) || [];
|
|
23
|
+
attrs.forEach((a) => {
|
|
24
|
+
properties[a.name] = {
|
|
25
|
+
description:
|
|
26
|
+
a.copilot_description ||
|
|
27
|
+
`${a.label}.${a.sublabel ? ` ${a.sublabel}` : ""}`,
|
|
28
|
+
...fieldProperties(a),
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
return {
|
|
32
|
+
type: "object",
|
|
33
|
+
description: ty.copilot_description || ty.description,
|
|
34
|
+
properties,
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
fieldTypeCfg.push({
|
|
38
|
+
type: "object",
|
|
39
|
+
description:
|
|
40
|
+
"A foreign key to a different table. This will reference the primary key on another table.",
|
|
41
|
+
properties: {
|
|
42
|
+
data_type: { const: "ForeignKey" },
|
|
43
|
+
reference_table: {
|
|
44
|
+
type: "string",
|
|
45
|
+
description: "Name of the table being referenced",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
fieldTypeCfg.push({
|
|
50
|
+
type: "object",
|
|
51
|
+
description:
|
|
52
|
+
"A reference (file path) to a file on disk. This can be used for example to hold images or documents",
|
|
53
|
+
properties: {
|
|
54
|
+
data_type: { const: "File" },
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
return {
|
|
58
|
+
type: "object",
|
|
59
|
+
required: ["tables"],
|
|
60
|
+
properties: {
|
|
61
|
+
tables: {
|
|
62
|
+
type: "array",
|
|
63
|
+
items: {
|
|
64
|
+
type: "object",
|
|
65
|
+
required: ["table_name", "fields"],
|
|
66
|
+
properties: {
|
|
67
|
+
table_name: {
|
|
68
|
+
type: "string",
|
|
69
|
+
description: "The name of the table",
|
|
70
|
+
},
|
|
71
|
+
fields: {
|
|
72
|
+
type: "array",
|
|
73
|
+
items: {
|
|
74
|
+
type: "object",
|
|
75
|
+
required: [
|
|
76
|
+
"name",
|
|
77
|
+
"label",
|
|
78
|
+
"type_and_configuration",
|
|
79
|
+
"importance",
|
|
80
|
+
],
|
|
81
|
+
properties: {
|
|
82
|
+
name: {
|
|
83
|
+
type: "string",
|
|
84
|
+
description:
|
|
85
|
+
"The field name. Must be a valid identifier in both SQL and JavaScript, all lower case, snake_case (underscore instead of spaces)",
|
|
86
|
+
},
|
|
87
|
+
label: {
|
|
88
|
+
type: "string",
|
|
89
|
+
description:
|
|
90
|
+
"A human-readable label for the field. Should be short, 1-4 words, can have spaces and mixed case.",
|
|
91
|
+
},
|
|
92
|
+
not_null: {
|
|
93
|
+
type: "boolean",
|
|
94
|
+
description:
|
|
95
|
+
"A value is required and the field will be NOT NULL in the database",
|
|
96
|
+
},
|
|
97
|
+
unique: {
|
|
98
|
+
type: "boolean",
|
|
99
|
+
description:
|
|
100
|
+
"The value is unique - different rows must have different values for this field",
|
|
101
|
+
},
|
|
102
|
+
type_and_configuration: { anyOf: fieldTypeCfg },
|
|
103
|
+
importance: {
|
|
104
|
+
type: "number",
|
|
105
|
+
description:
|
|
106
|
+
"How important is this field if only some fields can be displayed to the user. From 1 (least important) to 10 (most important).",
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
static async system_prompt() {
|
|
119
|
+
const tableLines = [];
|
|
120
|
+
const tables = await Table.find({});
|
|
121
|
+
tables.forEach((table) => {
|
|
122
|
+
const fieldLines = table.fields.map(
|
|
123
|
+
(f) =>
|
|
124
|
+
` * ${f.name} with type: ${f.pretty_type.replace(
|
|
125
|
+
"Key to",
|
|
126
|
+
"ForeignKey referencing"
|
|
127
|
+
)}.${f.description ? ` ${f.description}` : ""}`
|
|
128
|
+
);
|
|
129
|
+
tableLines.push(
|
|
130
|
+
`${table.name}${
|
|
131
|
+
table.description ? `: ${table.description}.` : "."
|
|
132
|
+
} Contains the following fields:\n${fieldLines.join("\n")}`
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
return `Use the generate_tables tool to construct one or more database tables.
|
|
136
|
+
|
|
137
|
+
Do not call this tool more than once. It should only be called once. If you are
|
|
138
|
+
building more than one table, use one call to the generate_tables tool to build all the
|
|
139
|
+
tables.
|
|
140
|
+
|
|
141
|
+
The argument to generate_tables is an array of tables, each with an array of fields. You do not
|
|
142
|
+
need to specify a primary key, a primary key called id with autoincrementing integers is
|
|
143
|
+
autmatically generated.
|
|
144
|
+
|
|
145
|
+
The database already contains the following tables:
|
|
146
|
+
|
|
147
|
+
${tableLines.join("\n\n")}
|
|
148
|
+
|
|
149
|
+
`;
|
|
150
|
+
}
|
|
151
|
+
static render_html({ tables }) {
|
|
152
|
+
const sctables = this.process_tables(tables);
|
|
153
|
+
const mmdia = buildMermaidMarkup(sctables);
|
|
154
|
+
return (
|
|
155
|
+
pre({ class: "mermaid" }, mmdia) +
|
|
156
|
+
script(`mermaid.run({querySelector: 'pre.mermaid'});`)
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
static async execute({ tables }, req) {
|
|
161
|
+
const sctables = this.process_tables(tables);
|
|
162
|
+
for (const table of sctables) await Table.create(table.name);
|
|
163
|
+
for (const table of sctables) {
|
|
164
|
+
for (const field of table.fields) {
|
|
165
|
+
field.table = Table.findOne({ name: table.name });
|
|
166
|
+
await Field.create(field);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
Trigger.emitEvent("AppChange", `Tables created`, req?.user, {
|
|
170
|
+
entity_type: "Table",
|
|
171
|
+
entity_names: sctables.map((t) => t.name),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
static process_tables(tables) {
|
|
176
|
+
return tables.map((table) => {
|
|
177
|
+
return new Table({
|
|
178
|
+
name: table.table_name,
|
|
179
|
+
fields: table.fields.map((f) => {
|
|
180
|
+
const { data_type, reference_table, ...attributes } =
|
|
181
|
+
f.type_and_configuration;
|
|
182
|
+
let type = data_type;
|
|
183
|
+
const scattributes = { ...attributes, importance: f.importance };
|
|
184
|
+
if (data_type === "ForeignKey") {
|
|
185
|
+
type = `Key to ${reference_table}`;
|
|
186
|
+
let refTableHere = tables.find(
|
|
187
|
+
(t) => t.table_name === reference_table
|
|
188
|
+
);
|
|
189
|
+
if (refTableHere) {
|
|
190
|
+
const strFields = refTableHere.fields.filter(
|
|
191
|
+
(f) => f.type_and_configuration.data_type === "String"
|
|
192
|
+
);
|
|
193
|
+
if (strFields.length) {
|
|
194
|
+
const maxImp = strFields.reduce(function (prev, current) {
|
|
195
|
+
return prev && prev.importance > current.importance
|
|
196
|
+
? prev
|
|
197
|
+
: current;
|
|
198
|
+
});
|
|
199
|
+
if (maxImp) scattributes.summary_field = maxImp.name;
|
|
200
|
+
}
|
|
201
|
+
} else if (reference_table === "users") {
|
|
202
|
+
scattributes.summary_field = "email";
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
...f,
|
|
208
|
+
type,
|
|
209
|
+
required: f.not_null,
|
|
210
|
+
attributes: scattributes,
|
|
211
|
+
};
|
|
212
|
+
}),
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const EOL = "\n";
|
|
219
|
+
const indentString = (str, indent) => `${" ".repeat(indent)}${str}`;
|
|
220
|
+
|
|
221
|
+
const srcCardinality = (field) => (field.required ? "||" : "|o");
|
|
222
|
+
|
|
223
|
+
const buildTableMarkup = (table) => {
|
|
224
|
+
const fields = table.getFields();
|
|
225
|
+
const members = fields
|
|
226
|
+
// .filter((f) => !f.reftable_name)
|
|
227
|
+
.map((f) =>
|
|
228
|
+
indentString(`${removeAllWhiteSpace(f.type_name)} ${f.name}`, 6)
|
|
229
|
+
)
|
|
230
|
+
.join(EOL);
|
|
231
|
+
const keys = table
|
|
232
|
+
.getForeignKeys()
|
|
233
|
+
.map((f) =>
|
|
234
|
+
indentString(
|
|
235
|
+
`"${table.name}"${srcCardinality(f)}--|| "${f.reftable_name}" : "${
|
|
236
|
+
f.name
|
|
237
|
+
}"`,
|
|
238
|
+
2
|
|
239
|
+
)
|
|
240
|
+
)
|
|
241
|
+
.join(EOL);
|
|
242
|
+
return `${keys}
|
|
243
|
+
"${table.name}" {${EOL}${members}${EOL} }`;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const buildMermaidMarkup = (tables) => {
|
|
247
|
+
const lines = tables.map((table) => buildTableMarkup(table)).join(EOL);
|
|
248
|
+
return `${indentString("erDiagram", 2)}${EOL}${lines}`;
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
module.exports = GenerateTables;
|
|
252
|
+
|
|
253
|
+
/* todo
|
|
254
|
+
|
|
255
|
+
- tag
|
|
256
|
+
- generate descriptions
|
|
257
|
+
- generate views
|
|
258
|
+
|
|
259
|
+
*/
|
|
@@ -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;
|
package/chat-copilot.js
ADDED
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
const Field = require("@saltcorn/data/models/field");
|
|
2
|
+
const Table = require("@saltcorn/data/models/table");
|
|
3
|
+
const Form = require("@saltcorn/data/models/form");
|
|
4
|
+
const View = require("@saltcorn/data/models/view");
|
|
5
|
+
const Trigger = require("@saltcorn/data/models/trigger");
|
|
6
|
+
const { findType } = require("@saltcorn/data/models/discovery");
|
|
7
|
+
const { save_menu_items } = require("@saltcorn/data/models/config");
|
|
8
|
+
const db = require("@saltcorn/data/db");
|
|
9
|
+
const WorkflowRun = require("@saltcorn/data/models/workflow_run");
|
|
10
|
+
const { localeDateTime } = require("@saltcorn/markup");
|
|
11
|
+
const {
|
|
12
|
+
div,
|
|
13
|
+
script,
|
|
14
|
+
domReady,
|
|
15
|
+
pre,
|
|
16
|
+
code,
|
|
17
|
+
input,
|
|
18
|
+
h4,
|
|
19
|
+
style,
|
|
20
|
+
h5,
|
|
21
|
+
button,
|
|
22
|
+
text_attr,
|
|
23
|
+
i,
|
|
24
|
+
p,
|
|
25
|
+
span,
|
|
26
|
+
small,
|
|
27
|
+
form,
|
|
28
|
+
textarea,
|
|
29
|
+
} = require("@saltcorn/markup/tags");
|
|
30
|
+
const { getState } = require("@saltcorn/data/db/state");
|
|
31
|
+
const {
|
|
32
|
+
getCompletion,
|
|
33
|
+
getPromptFromTemplate,
|
|
34
|
+
incompleteCfgMsg,
|
|
35
|
+
} = require("./common");
|
|
36
|
+
|
|
37
|
+
const get_state_fields = () => [];
|
|
38
|
+
|
|
39
|
+
const run = async (table_id, viewname, cfg, state, { res, req }) => {
|
|
40
|
+
const prevRuns = (
|
|
41
|
+
await WorkflowRun.find(
|
|
42
|
+
{ trigger_id: null },
|
|
43
|
+
{ orderBy: "started_at", orderDesc: true, limit: 30 }
|
|
44
|
+
)
|
|
45
|
+
).filter((r) => r.context.interactions);
|
|
46
|
+
const cfgMsg = incompleteCfgMsg();
|
|
47
|
+
if (cfgMsg) return cfgMsg;
|
|
48
|
+
let runInteractions = "";
|
|
49
|
+
if (state.run_id) {
|
|
50
|
+
const run = prevRuns.find((r) => r.id == state.run_id);
|
|
51
|
+
const interactMarkups = [];
|
|
52
|
+
for (const interact of run.context.interactions) {
|
|
53
|
+
switch (interact.role) {
|
|
54
|
+
case "user":
|
|
55
|
+
interactMarkups.push(
|
|
56
|
+
div(
|
|
57
|
+
{ class: "interaction-segment" },
|
|
58
|
+
span({ class: "badge bg-secondary" }, "You"),
|
|
59
|
+
p(interact.content)
|
|
60
|
+
)
|
|
61
|
+
);
|
|
62
|
+
break;
|
|
63
|
+
case "assistant":
|
|
64
|
+
case "system":
|
|
65
|
+
if (interact.tool_calls) {
|
|
66
|
+
for (const tool_call of interact.tool_calls) {
|
|
67
|
+
const markup = await renderToolcall(
|
|
68
|
+
tool_call,
|
|
69
|
+
viewname,
|
|
70
|
+
(run.context.implemented_fcall_ids || []).includes(
|
|
71
|
+
tool_call.id
|
|
72
|
+
),
|
|
73
|
+
run
|
|
74
|
+
);
|
|
75
|
+
interactMarkups.push(
|
|
76
|
+
div(
|
|
77
|
+
{ class: "interaction-segment" },
|
|
78
|
+
span({ class: "badge bg-secondary" }, "Copilot"),
|
|
79
|
+
markup
|
|
80
|
+
)
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
} else
|
|
84
|
+
interactMarkups.push(
|
|
85
|
+
div(
|
|
86
|
+
{ class: "interaction-segment" },
|
|
87
|
+
span({ class: "badge bg-secondary" }, "Copilot"),
|
|
88
|
+
p(interact.content)
|
|
89
|
+
)
|
|
90
|
+
);
|
|
91
|
+
break;
|
|
92
|
+
case "tool":
|
|
93
|
+
//ignore
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
runInteractions = interactMarkups.join("");
|
|
98
|
+
}
|
|
99
|
+
const input_form = form(
|
|
100
|
+
{
|
|
101
|
+
onsubmit:
|
|
102
|
+
"event.preventDefault();spin_send_button();view_post('Saltcorn Copilot', 'interact', $(this).serialize(), processCopilotResponse);return false;",
|
|
103
|
+
class: "form-namespace copilot mt-2",
|
|
104
|
+
method: "post",
|
|
105
|
+
},
|
|
106
|
+
input({
|
|
107
|
+
type: "hidden",
|
|
108
|
+
name: "_csrf",
|
|
109
|
+
value: req.csrfToken(),
|
|
110
|
+
}),
|
|
111
|
+
input({
|
|
112
|
+
type: "hidden",
|
|
113
|
+
class: "form-control ",
|
|
114
|
+
name: "run_id",
|
|
115
|
+
value: state.run_id ? +state.run_id : undefined,
|
|
116
|
+
}),
|
|
117
|
+
div(
|
|
118
|
+
{ class: "copilot-entry" },
|
|
119
|
+
textarea({
|
|
120
|
+
class: "form-control",
|
|
121
|
+
name: "userinput",
|
|
122
|
+
"data-fieldname": "userinput",
|
|
123
|
+
placeholder: "How can I help you?",
|
|
124
|
+
id: "inputuserinput",
|
|
125
|
+
rows: "3",
|
|
126
|
+
autofocus: true,
|
|
127
|
+
}),
|
|
128
|
+
span(
|
|
129
|
+
{ class: "submit-button p-2", onclick: "$('form.copilot').submit()" },
|
|
130
|
+
i({ id: "sendbuttonicon", class: "far fa-paper-plane" })
|
|
131
|
+
)
|
|
132
|
+
),
|
|
133
|
+
|
|
134
|
+
i(
|
|
135
|
+
small(
|
|
136
|
+
"Skills you can request: " +
|
|
137
|
+
actionClasses.map((ac) => ac.title).join(", ")
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
);
|
|
141
|
+
return {
|
|
142
|
+
widths: [3, 9],
|
|
143
|
+
gx: 3,
|
|
144
|
+
besides: [
|
|
145
|
+
{
|
|
146
|
+
type: "container",
|
|
147
|
+
contents: div(
|
|
148
|
+
div(
|
|
149
|
+
{
|
|
150
|
+
class: "d-flex justify-content-between align-middle mb-2",
|
|
151
|
+
},
|
|
152
|
+
h5("Sessions"),
|
|
153
|
+
|
|
154
|
+
button(
|
|
155
|
+
{
|
|
156
|
+
type: "button",
|
|
157
|
+
class: "btn btn-secondary btn-sm",
|
|
158
|
+
onclick: "unset_state_field('run_id')",
|
|
159
|
+
title: "New session",
|
|
160
|
+
},
|
|
161
|
+
i({ class: "fas fa-redo" })
|
|
162
|
+
)
|
|
163
|
+
),
|
|
164
|
+
prevRuns.map((run) =>
|
|
165
|
+
div(
|
|
166
|
+
{
|
|
167
|
+
onclick: `set_state_field('run_id',${run.id})`,
|
|
168
|
+
class: "prevcopilotrun",
|
|
169
|
+
},
|
|
170
|
+
localeDateTime(run.started_at),
|
|
171
|
+
|
|
172
|
+
p(
|
|
173
|
+
{ class: "prevrun_content" },
|
|
174
|
+
run.context.interactions[0]?.content
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
)
|
|
178
|
+
),
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
type: "container",
|
|
182
|
+
contents: div(
|
|
183
|
+
{ class: "card" },
|
|
184
|
+
div(
|
|
185
|
+
{ class: "card-body" },
|
|
186
|
+
script({
|
|
187
|
+
src: `/static_assets/${db.connectObj.version_tag}/mermaid.min.js`,
|
|
188
|
+
}),
|
|
189
|
+
script(
|
|
190
|
+
{ type: "module" },
|
|
191
|
+
`mermaid.initialize({securityLevel: 'loose'${
|
|
192
|
+
getState().getLightDarkMode(req.user) === "dark"
|
|
193
|
+
? ",theme: 'dark',"
|
|
194
|
+
: ""
|
|
195
|
+
}});`
|
|
196
|
+
),
|
|
197
|
+
div({ id: "copilotinteractions" }, runInteractions),
|
|
198
|
+
input_form,
|
|
199
|
+
style(
|
|
200
|
+
`div.interaction-segment:not(:first-child) {border-top: 1px solid #e7e7e7; }
|
|
201
|
+
div.interaction-segment {padding-top: 5px;padding-bottom: 5px;}
|
|
202
|
+
div.interaction-segment p {margin-bottom: 0px;}
|
|
203
|
+
div.interaction-segment div.card {margin-top: 0.5rem;}
|
|
204
|
+
div.prevcopilotrun {border-top: 1px solid #e7e7e7;border-right: 1px solid #e7e7e7;padding-top:3px; padding-bottom:3px; padding-right: 1rem;}
|
|
205
|
+
div.prevcopilotrun:hover {cursor: pointer}
|
|
206
|
+
.copilot-entry .submit-button:hover { cursor: pointer}
|
|
207
|
+
|
|
208
|
+
.copilot-entry .submit-button {
|
|
209
|
+
position: relative;
|
|
210
|
+
top: -1.8rem;
|
|
211
|
+
left: 0.1rem;
|
|
212
|
+
}
|
|
213
|
+
.copilot-entry {margin-bottom: -1.25rem; margin-top: 1rem;}
|
|
214
|
+
p.prevrun_content {
|
|
215
|
+
white-space: nowrap;
|
|
216
|
+
overflow: hidden;
|
|
217
|
+
margin-bottom: 0px;
|
|
218
|
+
display: block;
|
|
219
|
+
text-overflow: ellipsis;}`
|
|
220
|
+
),
|
|
221
|
+
script(`function processCopilotResponse(res) {
|
|
222
|
+
$("#sendbuttonicon").attr("class","far fa-paper-plane");
|
|
223
|
+
const $runidin= $("input[name=run_id")
|
|
224
|
+
if(res.run_id && (!$runidin.val() || $runidin.val()=="undefined"))
|
|
225
|
+
$runidin.val(res.run_id);
|
|
226
|
+
const wrapSegment = (html, who) => '<div class="interaction-segment"><span class="badge bg-secondary">'+who+'</span>'+html+'</div>'
|
|
227
|
+
$("#copilotinteractions").append(wrapSegment('<p>'+$("textarea[name=userinput]").val()+'</p>', "You"))
|
|
228
|
+
$("textarea[name=userinput]").val("")
|
|
229
|
+
|
|
230
|
+
for(const action of res.actions||[]) {
|
|
231
|
+
$("#copilotinteractions").append(wrapSegment(action, "Copilot"))
|
|
232
|
+
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if(res.response)
|
|
236
|
+
$("#copilotinteractions").append(wrapSegment('<p>'+res.response+'</p>', "Copilot"))
|
|
237
|
+
}
|
|
238
|
+
function restore_old_button_elem(btn) {
|
|
239
|
+
const oldText = $(btn).data("old-text");
|
|
240
|
+
btn.html(oldText);
|
|
241
|
+
btn.css({ width: "" });
|
|
242
|
+
btn.removeData("old-text");
|
|
243
|
+
}
|
|
244
|
+
function processExecuteResponse(res) {
|
|
245
|
+
const btn = $("#exec-"+res.fcall_id)
|
|
246
|
+
restore_old_button_elem($("#exec-"+res.fcall_id))
|
|
247
|
+
btn.prop('disabled', true);
|
|
248
|
+
btn.html('<i class="fas fa-check me-1"></i>Applied')
|
|
249
|
+
btn.removeClass("btn-primary")
|
|
250
|
+
btn.addClass("btn-secondary")
|
|
251
|
+
if(res.postExec) {
|
|
252
|
+
$('#postexec-'+res.fcall_id).html(res.postExec)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function submitOnEnter(event) {
|
|
256
|
+
if (event.which === 13) {
|
|
257
|
+
if (!event.repeat) {
|
|
258
|
+
const newEvent = new Event("submit", {cancelable: true});
|
|
259
|
+
event.target.form.dispatchEvent(newEvent);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
event.preventDefault(); // Prevents the addition of a new line in the text field
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
document.getElementById("inputuserinput").addEventListener("keydown", submitOnEnter);
|
|
266
|
+
function spin_send_button() {
|
|
267
|
+
$("#sendbuttonicon").attr("class","fas fa-spinner fa-spin");
|
|
268
|
+
}
|
|
269
|
+
`)
|
|
270
|
+
)
|
|
271
|
+
),
|
|
272
|
+
},
|
|
273
|
+
],
|
|
274
|
+
};
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const ellipsize = (s, nchars) => {
|
|
278
|
+
if (!s || !s.length) return "";
|
|
279
|
+
if (s.length <= (nchars || 20)) return text_attr(s);
|
|
280
|
+
return text_attr(s.substr(0, (nchars || 20) - 3)) + "...";
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const actionClasses = [
|
|
284
|
+
require("./actions/generate-workflow"),
|
|
285
|
+
require("./actions/generate-tables"),
|
|
286
|
+
require("./actions/generate-js-action"),
|
|
287
|
+
];
|
|
288
|
+
|
|
289
|
+
const getCompletionArguments = async () => {
|
|
290
|
+
const tools = [];
|
|
291
|
+
const sysPrompts = [];
|
|
292
|
+
for (const actionClass of actionClasses) {
|
|
293
|
+
tools.push({
|
|
294
|
+
type: "function",
|
|
295
|
+
function: {
|
|
296
|
+
name: actionClass.function_name,
|
|
297
|
+
description: actionClass.description,
|
|
298
|
+
parameters: await actionClass.json_schema(),
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
sysPrompts.push(await actionClass.system_prompt());
|
|
302
|
+
}
|
|
303
|
+
const systemPrompt =
|
|
304
|
+
"You are building application components in a database application builder called Saltcorn.\n\n" +
|
|
305
|
+
sysPrompts.join("\n\n");
|
|
306
|
+
return { tools, systemPrompt };
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
/*
|
|
310
|
+
|
|
311
|
+
build a workflow that asks the user for their name and age
|
|
312
|
+
|
|
313
|
+
*/
|
|
314
|
+
|
|
315
|
+
const execute = async (table_id, viewname, config, body, { req }) => {
|
|
316
|
+
const { fcall_id, run_id } = body;
|
|
317
|
+
|
|
318
|
+
const run = await WorkflowRun.findOne({ id: +run_id });
|
|
319
|
+
|
|
320
|
+
const fcall = run.context.funcalls[fcall_id];
|
|
321
|
+
const actionClass = actionClasses.find(
|
|
322
|
+
(ac) => ac.function_name === fcall.name
|
|
323
|
+
);
|
|
324
|
+
const result = await actionClass.execute(JSON.parse(fcall.arguments), req);
|
|
325
|
+
await addToContext(run, { implemented_fcall_ids: [fcall_id] });
|
|
326
|
+
return { json: { success: "ok", fcall_id, ...(result || {}) } };
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const interact = async (table_id, viewname, config, body, { req }) => {
|
|
330
|
+
const { userinput, run_id } = body;
|
|
331
|
+
let run;
|
|
332
|
+
if (!run_id || run_id === "undefined")
|
|
333
|
+
run = await WorkflowRun.create({
|
|
334
|
+
status: "Running",
|
|
335
|
+
context: {
|
|
336
|
+
implemented_fcall_ids: [],
|
|
337
|
+
interactions: [{ role: "user", content: userinput }],
|
|
338
|
+
funcalls: {},
|
|
339
|
+
},
|
|
340
|
+
});
|
|
341
|
+
else {
|
|
342
|
+
run = await WorkflowRun.findOne({ id: +run_id });
|
|
343
|
+
await addToContext(run, {
|
|
344
|
+
interactions: [{ role: "user", content: userinput }],
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
const complArgs = await getCompletionArguments();
|
|
348
|
+
complArgs.chat = run.context.interactions;
|
|
349
|
+
//console.log(complArgs);
|
|
350
|
+
|
|
351
|
+
//build a database for a bicycle rental company
|
|
352
|
+
//add a boolean field called "paid" to the payments table
|
|
353
|
+
|
|
354
|
+
const answer = await getState().functions.llm_generate.run(
|
|
355
|
+
userinput,
|
|
356
|
+
complArgs
|
|
357
|
+
);
|
|
358
|
+
await addToContext(run, {
|
|
359
|
+
interactions:
|
|
360
|
+
typeof answer === "object" && answer.tool_calls
|
|
361
|
+
? [
|
|
362
|
+
{ role: "assistant", tool_calls: answer.tool_calls },
|
|
363
|
+
...answer.tool_calls.map((tc) => ({
|
|
364
|
+
role: "tool",
|
|
365
|
+
tool_call_id: tc.id,
|
|
366
|
+
name: tc.function.name,
|
|
367
|
+
content: "Action suggested to user.",
|
|
368
|
+
})),
|
|
369
|
+
]
|
|
370
|
+
: [{ role: "assistant", content: answer }],
|
|
371
|
+
});
|
|
372
|
+
console.log("answer", answer);
|
|
373
|
+
|
|
374
|
+
if (typeof answer === "object" && answer.tool_calls) {
|
|
375
|
+
const actions = [];
|
|
376
|
+
for (const tool_call of answer.tool_calls) {
|
|
377
|
+
await addToContext(run, {
|
|
378
|
+
funcalls: { [tool_call.id]: tool_call.function },
|
|
379
|
+
});
|
|
380
|
+
const markup = await renderToolcall(tool_call, viewname, false, run);
|
|
381
|
+
|
|
382
|
+
actions.push(markup);
|
|
383
|
+
}
|
|
384
|
+
return { json: { success: "ok", actions, run_id: run.id } };
|
|
385
|
+
} else return { json: { success: "ok", response: answer, run_id: run.id } };
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
const renderToolcall = async (tool_call, viewname, implemented, run) => {
|
|
389
|
+
const fname = tool_call.function.name;
|
|
390
|
+
const actionClass = actionClasses.find((ac) => ac.function_name === fname);
|
|
391
|
+
const args = JSON.parse(tool_call.function.arguments);
|
|
392
|
+
|
|
393
|
+
const inner_markup = await actionClass.render_html(args);
|
|
394
|
+
return wrapAction(
|
|
395
|
+
inner_markup,
|
|
396
|
+
viewname,
|
|
397
|
+
tool_call,
|
|
398
|
+
actionClass,
|
|
399
|
+
implemented,
|
|
400
|
+
run
|
|
401
|
+
);
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
const wrapAction = (
|
|
405
|
+
inner_markup,
|
|
406
|
+
viewname,
|
|
407
|
+
tool_call,
|
|
408
|
+
actionClass,
|
|
409
|
+
implemented,
|
|
410
|
+
run
|
|
411
|
+
) =>
|
|
412
|
+
div(
|
|
413
|
+
{ class: "card mb-3" },
|
|
414
|
+
div({ class: "card-header" }, h5(actionClass.title)),
|
|
415
|
+
div(
|
|
416
|
+
{ class: "card-body" },
|
|
417
|
+
inner_markup,
|
|
418
|
+
implemented
|
|
419
|
+
? button(
|
|
420
|
+
{
|
|
421
|
+
type: "button",
|
|
422
|
+
class: "btn btn-secondary d-block mt-3 float-end",
|
|
423
|
+
disabled: true,
|
|
424
|
+
},
|
|
425
|
+
i({ class: "fas fa-check me-1" }),
|
|
426
|
+
"Applied"
|
|
427
|
+
)
|
|
428
|
+
: button(
|
|
429
|
+
{
|
|
430
|
+
type: "button",
|
|
431
|
+
id: "exec-" + tool_call.id,
|
|
432
|
+
class: "btn btn-primary d-block mt-3 float-end",
|
|
433
|
+
onclick: `press_store_button(this, true);view_post('${viewname}', 'execute', {fcall_id: '${tool_call.id}', run_id: ${run.id}}, processExecuteResponse)`,
|
|
434
|
+
},
|
|
435
|
+
"Apply"
|
|
436
|
+
),
|
|
437
|
+
div({ id: "postexec-" + tool_call.id })
|
|
438
|
+
)
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
const addToContext = async (run, newCtx) => {
|
|
442
|
+
if (run.addToContext) return await run.addToContext(newCtx);
|
|
443
|
+
let changed = true;
|
|
444
|
+
Object.keys(newCtx).forEach((k) => {
|
|
445
|
+
if (Array.isArray(run.context[k])) {
|
|
446
|
+
if (!Array.isArray(newCtx[k]))
|
|
447
|
+
throw new Error("Must be array to append to array");
|
|
448
|
+
run.context[k].push(...newCtx[k]);
|
|
449
|
+
changed = true;
|
|
450
|
+
} else if (typeof run.context[k] === "object") {
|
|
451
|
+
if (typeof newCtx[k] !== "object")
|
|
452
|
+
throw new Error("Must be object to append to object");
|
|
453
|
+
Object.assign(run.context[k], newCtx[k]);
|
|
454
|
+
changed = true;
|
|
455
|
+
} else {
|
|
456
|
+
run.context[k] = newCtx[k];
|
|
457
|
+
changed = true;
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
if (changed) await run.update({ context: run.context });
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
module.exports = {
|
|
464
|
+
name: "Saltcorn Copilot",
|
|
465
|
+
display_state_form: false,
|
|
466
|
+
get_state_fields,
|
|
467
|
+
tableless: true,
|
|
468
|
+
singleton: true,
|
|
469
|
+
run,
|
|
470
|
+
routes: { interact, execute },
|
|
471
|
+
};
|
package/common.js
CHANGED
|
@@ -57,4 +57,50 @@ const incompleteCfgMsg = () => {
|
|
|
57
57
|
}
|
|
58
58
|
};
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
const toArrayOfStrings = (opts) => {
|
|
61
|
+
if (typeof opts === "string") return opts.split(",").map((s) => s.trim());
|
|
62
|
+
if (Array.isArray(opts))
|
|
63
|
+
return opts.map((o) => (typeof o === "string" ? o : o.value || o.name));
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const fieldProperties = (field) => {
|
|
67
|
+
const props = {};
|
|
68
|
+
const typeName = field.type?.name || field.type || field.input_type;
|
|
69
|
+
if (field.isRepeat) {
|
|
70
|
+
props.type = "array";
|
|
71
|
+
const properties = {};
|
|
72
|
+
field.fields.map((f) => {
|
|
73
|
+
properties[f.name] = {
|
|
74
|
+
description: f.sublabel || f.label,
|
|
75
|
+
...fieldProperties(f),
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
props.items = {
|
|
79
|
+
type: "object",
|
|
80
|
+
properties,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
switch (typeName) {
|
|
84
|
+
case "String":
|
|
85
|
+
props.type = "string";
|
|
86
|
+
if (field.attributes?.options)
|
|
87
|
+
props.enum = toArrayOfStrings(field.attributes.options);
|
|
88
|
+
break;
|
|
89
|
+
case "Bool":
|
|
90
|
+
props.type = "boolean";
|
|
91
|
+
break;
|
|
92
|
+
case "Integer":
|
|
93
|
+
props.type = "integer";
|
|
94
|
+
break;
|
|
95
|
+
case "Float":
|
|
96
|
+
props.type = "number";
|
|
97
|
+
break;
|
|
98
|
+
case "select":
|
|
99
|
+
props.type = "string";
|
|
100
|
+
if (field.options) props.enum = toArrayOfStrings(field.options);
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
return props;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
module.exports = { getCompletion, getPromptFromTemplate, incompleteCfgMsg, fieldProperties };
|
package/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const Workflow = require("@saltcorn/data/models/workflow");
|
|
2
2
|
const Form = require("@saltcorn/data/models/form");
|
|
3
|
+
const { features } = require("@saltcorn/data/db/state");
|
|
3
4
|
|
|
4
5
|
const configuration_workflow = () =>
|
|
5
6
|
new Workflow({
|
|
@@ -10,5 +11,10 @@ module.exports = {
|
|
|
10
11
|
sc_plugin_api_version: 1,
|
|
11
12
|
//configuration_workflow,
|
|
12
13
|
dependencies: ["@saltcorn/large-language-model"],
|
|
13
|
-
viewtemplates:
|
|
14
|
+
viewtemplates: features.workflows
|
|
15
|
+
? [require("./chat-copilot")]
|
|
16
|
+
: [require("./action-builder"), require("./database-designer")],
|
|
17
|
+
functions: features.workflows
|
|
18
|
+
? { copilot_generate_workflow: require("./workflow-gen") }
|
|
19
|
+
: {},
|
|
14
20
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saltcorn/copilot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "AI assistant for building Saltcorn applications",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"dependencies": {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"eslintConfig": {
|
|
16
16
|
"extends": "eslint:recommended",
|
|
17
17
|
"parserOptions": {
|
|
18
|
-
"ecmaVersion":
|
|
18
|
+
"ecmaVersion": 2022
|
|
19
19
|
},
|
|
20
20
|
"env": {
|
|
21
21
|
"node": true,
|
|
@@ -300,8 +300,4 @@ value should be an object with keys that are field variable names. Example:
|
|
|
300
300
|
return { set_fields: {
|
|
301
301
|
zidentifier: `${name.toUpperCase()}-${id}`
|
|
302
302
|
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
Write the JavaScript code to implement the following functionality:
|
|
306
|
-
|
|
307
|
-
{{ userPrompt }}
|
|
303
|
+
}
|
package/workflow-gen.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
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 { getActionConfigFields } = require("@saltcorn/data/plugin-helper");
|
|
5
|
+
const GenerateWorkflow = require("./actions/generate-workflow");
|
|
6
|
+
|
|
7
|
+
const workflow_function = async () => ({
|
|
8
|
+
type: "function",
|
|
9
|
+
function: {
|
|
10
|
+
name: "generate_workflow",
|
|
11
|
+
description: "Generate the steps in a workflow",
|
|
12
|
+
parameters: await GenerateWorkflow.json_schema(),
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
module.exports = {
|
|
17
|
+
run: async (description) => {
|
|
18
|
+
const rnd = Math.round(100 * Math.random());
|
|
19
|
+
const systemPrompt = await GenerateWorkflow.system_prompt();
|
|
20
|
+
|
|
21
|
+
const toolargs = {
|
|
22
|
+
tools: [await workflow_function()],
|
|
23
|
+
tool_choice: {
|
|
24
|
+
type: "function",
|
|
25
|
+
function: { name: "generate_workflow" },
|
|
26
|
+
},
|
|
27
|
+
systemPrompt,
|
|
28
|
+
};
|
|
29
|
+
const prompt = `Design a workflow to implement a workflow accorfing to the following specification: ${description}`;
|
|
30
|
+
console.log(prompt);
|
|
31
|
+
console.log(JSON.stringify(toolargs, null, 2));
|
|
32
|
+
|
|
33
|
+
const answer = await getState().functions.llm_generate.run(
|
|
34
|
+
prompt,
|
|
35
|
+
toolargs
|
|
36
|
+
);
|
|
37
|
+
const resp = JSON.parse(answer.tool_calls[0].function.arguments);
|
|
38
|
+
const scsteps = resp.workflow_steps.map(GenerateWorkflow.to_saltcorn_step);
|
|
39
|
+
|
|
40
|
+
return scsteps;
|
|
41
|
+
},
|
|
42
|
+
isAsync: true,
|
|
43
|
+
description: "Generate a workflow",
|
|
44
|
+
arguments: [{ name: "description", type: "String" }],
|
|
45
|
+
};
|