@saltcorn/copilot 0.3.2 → 0.4.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/chat-copilot.js +8 -2
- package/common.js +1 -1
- package/index.js +1 -6
- package/package.json +1 -1
- package/prompts/action-builder.txt +1 -1
- package/user-copilot.js +787 -0
- package/workflow-gen.js +2 -2
package/chat-copilot.js
CHANGED
|
@@ -41,10 +41,14 @@ const get_state_fields = () => [];
|
|
|
41
41
|
const run = async (table_id, viewname, cfg, state, { res, req }) => {
|
|
42
42
|
const prevRuns = (
|
|
43
43
|
await WorkflowRun.find(
|
|
44
|
-
{ trigger_id: null },
|
|
44
|
+
{ trigger_id: null /*started_by: req.user?.id*/ }, //todo uncomment
|
|
45
45
|
{ orderBy: "started_at", orderDesc: true, limit: 30 }
|
|
46
46
|
)
|
|
47
|
-
).filter(
|
|
47
|
+
).filter(
|
|
48
|
+
(r) =>
|
|
49
|
+
r.context.interactions &&
|
|
50
|
+
(r.context.copilot === "_system" || !r.context.copilot)
|
|
51
|
+
);
|
|
48
52
|
const cfgMsg = incompleteCfgMsg();
|
|
49
53
|
if (cfgMsg) return cfgMsg;
|
|
50
54
|
let runInteractions = "";
|
|
@@ -336,7 +340,9 @@ const interact = async (table_id, viewname, config, body, { req }) => {
|
|
|
336
340
|
if (!run_id || run_id === "undefined")
|
|
337
341
|
run = await WorkflowRun.create({
|
|
338
342
|
status: "Running",
|
|
343
|
+
started_by: req.user?.id,
|
|
339
344
|
context: {
|
|
345
|
+
copilot: "_system",
|
|
340
346
|
implemented_fcall_ids: [],
|
|
341
347
|
interactions: [{ role: "user", content: userinput }],
|
|
342
348
|
funcalls: {},
|
package/common.js
CHANGED
|
@@ -27,7 +27,7 @@ const getPromptFromTemplate = async (tmplName, userPrompt, extraCtx = {}) => {
|
|
|
27
27
|
interpolate: /\{\{([^#].+?)\}\}/g,
|
|
28
28
|
});
|
|
29
29
|
const prompt = template(context);
|
|
30
|
-
console.log("Full prompt:\n", prompt);
|
|
30
|
+
//console.log("Full prompt:\n", prompt);
|
|
31
31
|
return prompt;
|
|
32
32
|
};
|
|
33
33
|
|
package/index.js
CHANGED
|
@@ -2,17 +2,12 @@ const Workflow = require("@saltcorn/data/models/workflow");
|
|
|
2
2
|
const Form = require("@saltcorn/data/models/form");
|
|
3
3
|
const { features } = require("@saltcorn/data/db/state");
|
|
4
4
|
|
|
5
|
-
const configuration_workflow = () =>
|
|
6
|
-
new Workflow({
|
|
7
|
-
steps: [],
|
|
8
|
-
});
|
|
9
5
|
|
|
10
6
|
module.exports = {
|
|
11
7
|
sc_plugin_api_version: 1,
|
|
12
|
-
//configuration_workflow,
|
|
13
8
|
dependencies: ["@saltcorn/large-language-model"],
|
|
14
9
|
viewtemplates: features.workflows
|
|
15
|
-
? [require("./chat-copilot")]
|
|
10
|
+
? [require("./chat-copilot"), require("./user-copilot")]
|
|
16
11
|
: [require("./action-builder"), require("./database-designer")],
|
|
17
12
|
functions: features.workflows
|
|
18
13
|
? { copilot_generate_workflow: require("./workflow-gen") }
|
package/package.json
CHANGED
|
@@ -251,7 +251,7 @@ The following tables are present in the database:
|
|
|
251
251
|
|
|
252
252
|
{{# for (const table of tables) { }}
|
|
253
253
|
{{ table.name }} table with name = "{{ table.name }}" which has the following fields:
|
|
254
|
-
{{# for (const field of table.fields) { }} - {{ field.name }} of type {{ field.pretty_type }}
|
|
254
|
+
{{# for (const field of table.fields) { }} - {{ field.name }} of type {{ field.pretty_type.replace("Key to","ForeignKey referencing") }}
|
|
255
255
|
{{# } }}
|
|
256
256
|
{{# } }}
|
|
257
257
|
|
package/user-copilot.js
ADDED
|
@@ -0,0 +1,787 @@
|
|
|
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 FieldRepeat = require("@saltcorn/data/models/fieldrepeat");
|
|
7
|
+
const db = require("@saltcorn/data/db");
|
|
8
|
+
const WorkflowRun = require("@saltcorn/data/models/workflow_run");
|
|
9
|
+
const Workflow = require("@saltcorn/data/models/workflow");
|
|
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
|
+
fieldProperties,
|
|
36
|
+
} = require("./common");
|
|
37
|
+
const MarkdownIt = require("markdown-it"),
|
|
38
|
+
md = new MarkdownIt();
|
|
39
|
+
|
|
40
|
+
const configuration_workflow = (req) =>
|
|
41
|
+
new Workflow({
|
|
42
|
+
steps: [
|
|
43
|
+
{
|
|
44
|
+
name: req.__("Prompt"),
|
|
45
|
+
form: async (context) => {
|
|
46
|
+
return new Form({
|
|
47
|
+
fields: [
|
|
48
|
+
{
|
|
49
|
+
name: "sys_prompt",
|
|
50
|
+
label: "System prompt",
|
|
51
|
+
sublabel: "Additional information for the system prompt",
|
|
52
|
+
type: "String",
|
|
53
|
+
fieldview: "textarea",
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: req.__("Tables"),
|
|
61
|
+
form: async (context) => {
|
|
62
|
+
const tables = await Table.find({});
|
|
63
|
+
const show_view_opts = {};
|
|
64
|
+
const list_view_opts = {};
|
|
65
|
+
for (const t of tables) {
|
|
66
|
+
const views = await View.find({
|
|
67
|
+
table_id: t.id,
|
|
68
|
+
viewtemplate: "Show",
|
|
69
|
+
});
|
|
70
|
+
show_view_opts[t.name] = views.map((v) => v.name);
|
|
71
|
+
const lviews = await View.find({
|
|
72
|
+
table_id: t.id,
|
|
73
|
+
viewtemplate: "List",
|
|
74
|
+
});
|
|
75
|
+
list_view_opts[t.name] = lviews.map((v) => v.name);
|
|
76
|
+
}
|
|
77
|
+
return new Form({
|
|
78
|
+
fields: [
|
|
79
|
+
new FieldRepeat({
|
|
80
|
+
name: "tables",
|
|
81
|
+
label: "Tables",
|
|
82
|
+
fields: [
|
|
83
|
+
{
|
|
84
|
+
name: "table_name",
|
|
85
|
+
label: "Table",
|
|
86
|
+
sublabel:
|
|
87
|
+
"Only tables with a description can be enabled for access",
|
|
88
|
+
type: "String",
|
|
89
|
+
required: true,
|
|
90
|
+
attributes: { options: tables.map((a) => a.name) },
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: "show_view",
|
|
94
|
+
label: "Show view",
|
|
95
|
+
type: "String",
|
|
96
|
+
attributes: {
|
|
97
|
+
calcOptions: ["table_name", show_view_opts],
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: "list_view",
|
|
102
|
+
label: "List view",
|
|
103
|
+
type: "String",
|
|
104
|
+
attributes: {
|
|
105
|
+
calcOptions: ["table_name", list_view_opts],
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
}),
|
|
110
|
+
],
|
|
111
|
+
});
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: req.__("Actions"),
|
|
116
|
+
form: async (context) => {
|
|
117
|
+
const actions = (await Trigger.find({})).filter(
|
|
118
|
+
(action) => action.description
|
|
119
|
+
);
|
|
120
|
+
const hasTable = actions.filter((a) => a.table_id).map((a) => a.name);
|
|
121
|
+
const confirm_view_opts = {};
|
|
122
|
+
for (const a of actions) {
|
|
123
|
+
if (!a.table_id) continue;
|
|
124
|
+
const views = await View.find({ table_id: a.table_id });
|
|
125
|
+
confirm_view_opts[a.name] = views.map((v) => v.name);
|
|
126
|
+
}
|
|
127
|
+
return new Form({
|
|
128
|
+
fields: [
|
|
129
|
+
new FieldRepeat({
|
|
130
|
+
name: "actions",
|
|
131
|
+
label: "Actions",
|
|
132
|
+
fields: [
|
|
133
|
+
{
|
|
134
|
+
name: "trigger_name",
|
|
135
|
+
label: "Action",
|
|
136
|
+
sublabel: "Only actions with a description can be enabled",
|
|
137
|
+
type: "String",
|
|
138
|
+
required: true,
|
|
139
|
+
attributes: { options: actions.map((a) => a.name) },
|
|
140
|
+
},
|
|
141
|
+
/*{ name: "confirm", label: "User confirmation", type: "Bool" },
|
|
142
|
+
{
|
|
143
|
+
name: "confirm_view",
|
|
144
|
+
label: "Confirm view",
|
|
145
|
+
type: "String",
|
|
146
|
+
showIf: { confirm: true, trigger_name: hasTable },
|
|
147
|
+
attributes: {
|
|
148
|
+
calcOptions: ["trigger_name", confirm_view_opts],
|
|
149
|
+
},
|
|
150
|
+
},*/
|
|
151
|
+
],
|
|
152
|
+
}),
|
|
153
|
+
],
|
|
154
|
+
});
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const get_state_fields = () => [];
|
|
161
|
+
|
|
162
|
+
const run = async (table_id, viewname, config, state, { res, req }) => {
|
|
163
|
+
const prevRuns = (
|
|
164
|
+
await WorkflowRun.find(
|
|
165
|
+
{ trigger_id: null, started_by: req.user?.id },
|
|
166
|
+
{ orderBy: "started_at", orderDesc: true, limit: 30 }
|
|
167
|
+
)
|
|
168
|
+
).filter((r) => r.context.interactions && r.context.copilot === viewname);
|
|
169
|
+
|
|
170
|
+
const cfgMsg = incompleteCfgMsg();
|
|
171
|
+
if (cfgMsg) return cfgMsg;
|
|
172
|
+
let runInteractions = "";
|
|
173
|
+
if (state.run_id) {
|
|
174
|
+
const run = prevRuns.find((r) => r.id == state.run_id);
|
|
175
|
+
const interactMarkups = [];
|
|
176
|
+
for (const interact of run.context.interactions) {
|
|
177
|
+
switch (interact.role) {
|
|
178
|
+
case "user":
|
|
179
|
+
interactMarkups.push(
|
|
180
|
+
div(
|
|
181
|
+
{ class: "interaction-segment" },
|
|
182
|
+
span({ class: "badge bg-secondary" }, "You"),
|
|
183
|
+
md.render(interact.content)
|
|
184
|
+
)
|
|
185
|
+
);
|
|
186
|
+
break;
|
|
187
|
+
case "assistant":
|
|
188
|
+
case "system":
|
|
189
|
+
if (interact.tool_calls) {
|
|
190
|
+
for (const tool_call of interact.tool_calls) {
|
|
191
|
+
const action = config.actions.find(
|
|
192
|
+
(a) => a.trigger_name === tool_call.function.name
|
|
193
|
+
);
|
|
194
|
+
if (action) {
|
|
195
|
+
const row = JSON.parse(tool_call.function.arguments);
|
|
196
|
+
interactMarkups.push(
|
|
197
|
+
wrapSegment(
|
|
198
|
+
wrapCard(
|
|
199
|
+
action.trigger_name,
|
|
200
|
+
pre(JSON.stringify(row, null, 2))
|
|
201
|
+
),
|
|
202
|
+
"Copilot"
|
|
203
|
+
)
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
} else
|
|
208
|
+
interactMarkups.push(
|
|
209
|
+
div(
|
|
210
|
+
{ class: "interaction-segment" },
|
|
211
|
+
span({ class: "badge bg-secondary" }, "Copilot"),
|
|
212
|
+
typeof interact.content === "string"
|
|
213
|
+
? md.render(interact.content)
|
|
214
|
+
: interact.content
|
|
215
|
+
)
|
|
216
|
+
);
|
|
217
|
+
break;
|
|
218
|
+
case "tool":
|
|
219
|
+
if (interact.name.startsWith("Query")) {
|
|
220
|
+
const table = Table.findOne({
|
|
221
|
+
name: interact.name.replace("Query", ""),
|
|
222
|
+
});
|
|
223
|
+
interactMarkups.push(
|
|
224
|
+
await renderQueryInteraction(
|
|
225
|
+
table,
|
|
226
|
+
JSON.parse(interact.content),
|
|
227
|
+
config,
|
|
228
|
+
req
|
|
229
|
+
)
|
|
230
|
+
);
|
|
231
|
+
} else if (interact.content !== "Action run") {
|
|
232
|
+
interactMarkups.push(
|
|
233
|
+
wrapSegment(
|
|
234
|
+
wrapCard(
|
|
235
|
+
interact.name,
|
|
236
|
+
pre(
|
|
237
|
+
interact.name.startsWith("Query")
|
|
238
|
+
? JSON.stringify(JSON.parse(interact.content), null, 2)
|
|
239
|
+
: JSON.stringify(interact.content, null, 2)
|
|
240
|
+
)
|
|
241
|
+
),
|
|
242
|
+
"Copilot"
|
|
243
|
+
)
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
runInteractions = interactMarkups.join("");
|
|
250
|
+
}
|
|
251
|
+
const input_form = form(
|
|
252
|
+
{
|
|
253
|
+
onsubmit: `event.preventDefault();spin_send_button();view_post('${viewname}', 'interact', $(this).serialize(), processCopilotResponse);return false;`,
|
|
254
|
+
class: "form-namespace copilot mt-2",
|
|
255
|
+
method: "post",
|
|
256
|
+
},
|
|
257
|
+
input({
|
|
258
|
+
type: "hidden",
|
|
259
|
+
name: "_csrf",
|
|
260
|
+
value: req.csrfToken(),
|
|
261
|
+
}),
|
|
262
|
+
input({
|
|
263
|
+
type: "hidden",
|
|
264
|
+
class: "form-control ",
|
|
265
|
+
name: "run_id",
|
|
266
|
+
value: state.run_id ? +state.run_id : undefined,
|
|
267
|
+
}),
|
|
268
|
+
div(
|
|
269
|
+
{ class: "copilot-entry" },
|
|
270
|
+
textarea({
|
|
271
|
+
class: "form-control",
|
|
272
|
+
name: "userinput",
|
|
273
|
+
"data-fieldname": "userinput",
|
|
274
|
+
placeholder: "How can I help you?",
|
|
275
|
+
id: "inputuserinput",
|
|
276
|
+
rows: "3",
|
|
277
|
+
autofocus: true,
|
|
278
|
+
}),
|
|
279
|
+
span(
|
|
280
|
+
{ class: "submit-button p-2", onclick: "$('form.copilot').submit()" },
|
|
281
|
+
i({ id: "sendbuttonicon", class: "far fa-paper-plane" })
|
|
282
|
+
)
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
/*i(
|
|
286
|
+
small(
|
|
287
|
+
"Skills you can request: " +
|
|
288
|
+
actionClasses.map((ac) => ac.title).join(", ")
|
|
289
|
+
)
|
|
290
|
+
)*/
|
|
291
|
+
);
|
|
292
|
+
return {
|
|
293
|
+
widths: [3, 9],
|
|
294
|
+
gx: 3,
|
|
295
|
+
besides: [
|
|
296
|
+
{
|
|
297
|
+
type: "container",
|
|
298
|
+
contents: div(
|
|
299
|
+
div(
|
|
300
|
+
{
|
|
301
|
+
class: "d-flex justify-content-between align-middle mb-2",
|
|
302
|
+
},
|
|
303
|
+
h5("Sessions"),
|
|
304
|
+
|
|
305
|
+
button(
|
|
306
|
+
{
|
|
307
|
+
type: "button",
|
|
308
|
+
class: "btn btn-secondary btn-sm py-0",
|
|
309
|
+
style: "font-size: 0.9em;height:1.5em",
|
|
310
|
+
onclick: "unset_state_field('run_id')",
|
|
311
|
+
title: "New session",
|
|
312
|
+
},
|
|
313
|
+
i({ class: "fas fa-redo fa-sm" })
|
|
314
|
+
)
|
|
315
|
+
),
|
|
316
|
+
prevRuns.map((run) =>
|
|
317
|
+
div(
|
|
318
|
+
{
|
|
319
|
+
onclick: `set_state_field('run_id',${run.id})`,
|
|
320
|
+
class: "prevcopilotrun border p-2",
|
|
321
|
+
},
|
|
322
|
+
localeDateTime(run.started_at),
|
|
323
|
+
|
|
324
|
+
p(
|
|
325
|
+
{ class: "prevrun_content" },
|
|
326
|
+
run.context.interactions[0]?.content
|
|
327
|
+
)
|
|
328
|
+
)
|
|
329
|
+
)
|
|
330
|
+
),
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
type: "container",
|
|
334
|
+
contents: div(
|
|
335
|
+
{ class: "card" },
|
|
336
|
+
div(
|
|
337
|
+
{ class: "card-body" },
|
|
338
|
+
script({
|
|
339
|
+
src: `/static_assets/${db.connectObj.version_tag}/mermaid.min.js`,
|
|
340
|
+
}),
|
|
341
|
+
script(
|
|
342
|
+
{ type: "module" },
|
|
343
|
+
`mermaid.initialize({securityLevel: 'loose'${
|
|
344
|
+
getState().getLightDarkMode(req.user) === "dark"
|
|
345
|
+
? ",theme: 'dark',"
|
|
346
|
+
: ""
|
|
347
|
+
}});`
|
|
348
|
+
),
|
|
349
|
+
div({ id: "copilotinteractions" }, runInteractions),
|
|
350
|
+
input_form,
|
|
351
|
+
style(
|
|
352
|
+
`div.interaction-segment:not(:first-child) {border-top: 1px solid #e7e7e7; }
|
|
353
|
+
div.interaction-segment {padding-top: 5px;padding-bottom: 5px;}
|
|
354
|
+
div.interaction-segment p {margin-bottom: 0px;}
|
|
355
|
+
div.interaction-segment div.card {margin-top: 0.5rem;}
|
|
356
|
+
div.prevcopilotrun:hover {cursor: pointer; background-color: var(--tblr-secondary-bg-subtle, var(--bs-secondary-bg-subtle, gray));}
|
|
357
|
+
.copilot-entry .submit-button:hover { cursor: pointer}
|
|
358
|
+
|
|
359
|
+
.copilot-entry .submit-button {
|
|
360
|
+
position: relative;
|
|
361
|
+
top: -1.8rem;
|
|
362
|
+
left: 0.1rem;
|
|
363
|
+
}
|
|
364
|
+
.copilot-entry {margin-bottom: -1.25rem; margin-top: 1rem;}
|
|
365
|
+
p.prevrun_content {
|
|
366
|
+
white-space: nowrap;
|
|
367
|
+
overflow: hidden;
|
|
368
|
+
margin-bottom: 0px;
|
|
369
|
+
display: block;
|
|
370
|
+
text-overflow: ellipsis;}`
|
|
371
|
+
),
|
|
372
|
+
script(`function processCopilotResponse(res) {
|
|
373
|
+
$("#sendbuttonicon").attr("class","far fa-paper-plane");
|
|
374
|
+
const $runidin= $("input[name=run_id")
|
|
375
|
+
if(res.run_id && (!$runidin.val() || $runidin.val()=="undefined"))
|
|
376
|
+
$runidin.val(res.run_id);
|
|
377
|
+
const wrapSegment = (html, who) => '<div class="interaction-segment"><span class="badge bg-secondary">'+who+'</span>'+html+'</div>'
|
|
378
|
+
$("#copilotinteractions").append(wrapSegment('<p>'+$("textarea[name=userinput]").val()+'</p>', "You"))
|
|
379
|
+
$("textarea[name=userinput]").val("")
|
|
380
|
+
|
|
381
|
+
if(res.response)
|
|
382
|
+
$("#copilotinteractions").append(res.response)
|
|
383
|
+
}
|
|
384
|
+
function restore_old_button_elem(btn) {
|
|
385
|
+
const oldText = $(btn).data("old-text");
|
|
386
|
+
btn.html(oldText);
|
|
387
|
+
btn.css({ width: "" }).prop("disabled", false);
|
|
388
|
+
btn.removeData("old-text");
|
|
389
|
+
}
|
|
390
|
+
function processExecuteResponse(res) {
|
|
391
|
+
const btn = $("#exec-"+res.fcall_id)
|
|
392
|
+
restore_old_button_elem($("#exec-"+res.fcall_id))
|
|
393
|
+
btn.prop('disabled', true);
|
|
394
|
+
btn.html('<i class="fas fa-check me-1"></i>Applied')
|
|
395
|
+
btn.removeClass("btn-primary")
|
|
396
|
+
btn.addClass("btn-secondary")
|
|
397
|
+
if(res.postExec) {
|
|
398
|
+
$('#postexec-'+res.fcall_id).html(res.postExec)
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
function submitOnEnter(event) {
|
|
402
|
+
if (event.which === 13) {
|
|
403
|
+
if (!event.repeat) {
|
|
404
|
+
const newEvent = new Event("submit", {cancelable: true});
|
|
405
|
+
event.target.form.dispatchEvent(newEvent);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
event.preventDefault(); // Prevents the addition of a new line in the text field
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
document.getElementById("inputuserinput").addEventListener("keydown", submitOnEnter);
|
|
412
|
+
function spin_send_button() {
|
|
413
|
+
$("#sendbuttonicon").attr("class","fas fa-spinner fa-spin");
|
|
414
|
+
}
|
|
415
|
+
`)
|
|
416
|
+
)
|
|
417
|
+
),
|
|
418
|
+
},
|
|
419
|
+
],
|
|
420
|
+
};
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
const getCompletionArguments = async (config) => {
|
|
424
|
+
let tools = [];
|
|
425
|
+
const sysPrompts = [];
|
|
426
|
+
for (const tableCfg of config?.tables || []) {
|
|
427
|
+
let properties = {};
|
|
428
|
+
|
|
429
|
+
const table = Table.findOne({ name: tableCfg.table_name });
|
|
430
|
+
|
|
431
|
+
table.fields
|
|
432
|
+
.filter((f) => !f.primary_key)
|
|
433
|
+
.forEach((field) => {
|
|
434
|
+
properties[field.name] = {
|
|
435
|
+
description: field.label + " " + field.description || "",
|
|
436
|
+
};
|
|
437
|
+
if (field.type.name === "String") {
|
|
438
|
+
properties[field.name].anyOf = [
|
|
439
|
+
{ type: "string", description: "Match this value exactly" },
|
|
440
|
+
{
|
|
441
|
+
type: "object",
|
|
442
|
+
properties: {
|
|
443
|
+
ilike: {
|
|
444
|
+
type: "string",
|
|
445
|
+
description: "Case insensitive substring match.",
|
|
446
|
+
},
|
|
447
|
+
in: {
|
|
448
|
+
type: "array",
|
|
449
|
+
description: "Match any one of these values",
|
|
450
|
+
items: {
|
|
451
|
+
type: "string",
|
|
452
|
+
},
|
|
453
|
+
},
|
|
454
|
+
},
|
|
455
|
+
},
|
|
456
|
+
];
|
|
457
|
+
}
|
|
458
|
+
if (field.type.name === "Integer" || field.type.name === "Float") {
|
|
459
|
+
const basetype = field.type.name === "Integer" ? "integer" : "number";
|
|
460
|
+
properties[field.name].anyOf = [
|
|
461
|
+
{ type: basetype, description: "Match this value exactly" },
|
|
462
|
+
{
|
|
463
|
+
type: "object",
|
|
464
|
+
properties: {
|
|
465
|
+
gt: {
|
|
466
|
+
type: basetype,
|
|
467
|
+
description: "Greater than this value",
|
|
468
|
+
},
|
|
469
|
+
lt: {
|
|
470
|
+
type: basetype,
|
|
471
|
+
description: "Less than this value",
|
|
472
|
+
},
|
|
473
|
+
in: {
|
|
474
|
+
type: "array",
|
|
475
|
+
description: "Match any one of these values",
|
|
476
|
+
items: {
|
|
477
|
+
type: basetype,
|
|
478
|
+
},
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
},
|
|
482
|
+
];
|
|
483
|
+
} else Object.assign(fieldProperties(field), properties[field.name]);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
tools.push({
|
|
487
|
+
type: "function",
|
|
488
|
+
function: {
|
|
489
|
+
name: "Query" + table.name,
|
|
490
|
+
description: `Query the ${table.name} table. ${
|
|
491
|
+
table.description || ""
|
|
492
|
+
}`,
|
|
493
|
+
parameters: {
|
|
494
|
+
type: "object",
|
|
495
|
+
//required: ["action_javascript_code", "action_name"],
|
|
496
|
+
properties,
|
|
497
|
+
},
|
|
498
|
+
},
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
for (const action of config?.actions || []) {
|
|
502
|
+
let properties = {};
|
|
503
|
+
|
|
504
|
+
const trigger = Trigger.findOne({ name: action.trigger_name });
|
|
505
|
+
if (trigger.table_id) {
|
|
506
|
+
const table = Table.findOne({ id: trigger.table_id });
|
|
507
|
+
|
|
508
|
+
table.fields
|
|
509
|
+
.filter((f) => !f.primary_key)
|
|
510
|
+
.forEach((field) => {
|
|
511
|
+
properties[field.name] = {
|
|
512
|
+
description: field.label + " " + field.description || "",
|
|
513
|
+
...fieldProperties(field),
|
|
514
|
+
};
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
tools.push({
|
|
518
|
+
type: "function",
|
|
519
|
+
function: {
|
|
520
|
+
name: action.trigger_name,
|
|
521
|
+
description: trigger.description,
|
|
522
|
+
parameters: {
|
|
523
|
+
type: "object",
|
|
524
|
+
//required: ["action_javascript_code", "action_name"],
|
|
525
|
+
properties,
|
|
526
|
+
},
|
|
527
|
+
},
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
const systemPrompt =
|
|
531
|
+
"You are helping users retrieve information and perform actions on a relational database" +
|
|
532
|
+
config.sys_prompt;
|
|
533
|
+
|
|
534
|
+
if (tools.length === 0) tools = undefined;
|
|
535
|
+
return { tools, systemPrompt };
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
/*
|
|
539
|
+
|
|
540
|
+
build a workflow that asks the user for their name and age
|
|
541
|
+
|
|
542
|
+
*/
|
|
543
|
+
|
|
544
|
+
const interact = async (table_id, viewname, config, body, { req, res }) => {
|
|
545
|
+
const { userinput, run_id } = body;
|
|
546
|
+
let run;
|
|
547
|
+
if (!run_id || run_id === "undefined")
|
|
548
|
+
run = await WorkflowRun.create({
|
|
549
|
+
status: "Running",
|
|
550
|
+
started_by: req.user?.id,
|
|
551
|
+
context: {
|
|
552
|
+
copilot: viewname,
|
|
553
|
+
implemented_fcall_ids: [],
|
|
554
|
+
interactions: [{ role: "user", content: userinput }],
|
|
555
|
+
funcalls: {},
|
|
556
|
+
},
|
|
557
|
+
});
|
|
558
|
+
else {
|
|
559
|
+
run = await WorkflowRun.findOne({ id: +run_id });
|
|
560
|
+
await addToContext(run, {
|
|
561
|
+
interactions: [{ role: "user", content: userinput }],
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
return await process_interaction(run, userinput, config, req);
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
const renderQueryInteraction = async (table, result, config, req) => {
|
|
568
|
+
if (result.length === 0) return "No rows found";
|
|
569
|
+
|
|
570
|
+
const tableCfg = config.tables.find((t) => t.table_name === table.name);
|
|
571
|
+
let viewRes = "";
|
|
572
|
+
|
|
573
|
+
if (result.length === 1) {
|
|
574
|
+
const view = View.findOne({ name: tableCfg.show_view });
|
|
575
|
+
if (view) {
|
|
576
|
+
viewRes = await view.run(
|
|
577
|
+
{ [table.pk_name]: result[0][table.pk_name] },
|
|
578
|
+
{ req }
|
|
579
|
+
);
|
|
580
|
+
} else viewRes = pre(JSON.stringify(result[0], null, 2));
|
|
581
|
+
} else {
|
|
582
|
+
const view = View.findOne({ name: tableCfg.list_view });
|
|
583
|
+
if (view) {
|
|
584
|
+
viewRes = await view.run(
|
|
585
|
+
{ [table.pk_name]: { in: result.map((r) => r[table.pk_name]) } },
|
|
586
|
+
{ req }
|
|
587
|
+
);
|
|
588
|
+
} else viewRes = pre(JSON.stringify(result, null, 2));
|
|
589
|
+
}
|
|
590
|
+
return wrapSegment(
|
|
591
|
+
wrapCard(
|
|
592
|
+
"Query " + table.name,
|
|
593
|
+
//div("Query: ", code(JSON.stringify(query))),
|
|
594
|
+
viewRes
|
|
595
|
+
),
|
|
596
|
+
"Copilot"
|
|
597
|
+
);
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
const process_interaction = async (
|
|
601
|
+
run,
|
|
602
|
+
input,
|
|
603
|
+
config,
|
|
604
|
+
req,
|
|
605
|
+
prevResponses = []
|
|
606
|
+
) => {
|
|
607
|
+
const complArgs = await getCompletionArguments(config);
|
|
608
|
+
complArgs.chat = run.context.interactions;
|
|
609
|
+
//console.log(complArgs);
|
|
610
|
+
console.log("complArgs", JSON.stringify(complArgs, null, 2));
|
|
611
|
+
|
|
612
|
+
const answer = await getState().functions.llm_generate.run(input, complArgs);
|
|
613
|
+
console.log("answer", answer);
|
|
614
|
+
await addToContext(run, {
|
|
615
|
+
interactions:
|
|
616
|
+
typeof answer === "object" && answer.tool_calls
|
|
617
|
+
? [{ role: "assistant", tool_calls: answer.tool_calls }]
|
|
618
|
+
: [{ role: "assistant", content: answer }],
|
|
619
|
+
});
|
|
620
|
+
const responses = [];
|
|
621
|
+
|
|
622
|
+
if (typeof answer === "object" && answer.tool_calls) {
|
|
623
|
+
//const actions = [];
|
|
624
|
+
let hasResult = false;
|
|
625
|
+
for (const tool_call of answer.tool_calls) {
|
|
626
|
+
console.log("call function", tool_call.function);
|
|
627
|
+
|
|
628
|
+
await addToContext(run, {
|
|
629
|
+
funcalls: { [tool_call.id]: tool_call.function },
|
|
630
|
+
});
|
|
631
|
+
const action = config.actions.find(
|
|
632
|
+
(a) => a.trigger_name === tool_call.function.name
|
|
633
|
+
);
|
|
634
|
+
console.log({ action });
|
|
635
|
+
|
|
636
|
+
if (action) {
|
|
637
|
+
const trigger = Trigger.findOne({ name: action.trigger_name });
|
|
638
|
+
const row = JSON.parse(tool_call.function.arguments);
|
|
639
|
+
responses.push(
|
|
640
|
+
wrapSegment(
|
|
641
|
+
wrapCard(action.trigger_name, pre(JSON.stringify(row, null, 2))),
|
|
642
|
+
"Copilot"
|
|
643
|
+
)
|
|
644
|
+
);
|
|
645
|
+
const result = await trigger.runWithoutRow({ user: req.user, row });
|
|
646
|
+
console.log("ran trigger with result", {
|
|
647
|
+
name: trigger.name,
|
|
648
|
+
row,
|
|
649
|
+
result,
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
if (typeof result === "object" && Object.keys(result || {}).length) {
|
|
653
|
+
responses.push(
|
|
654
|
+
wrapSegment(
|
|
655
|
+
wrapCard(
|
|
656
|
+
action.trigger_name + " result",
|
|
657
|
+
pre(JSON.stringify(result, null, 2))
|
|
658
|
+
),
|
|
659
|
+
"Copilot"
|
|
660
|
+
)
|
|
661
|
+
);
|
|
662
|
+
hasResult = true;
|
|
663
|
+
}
|
|
664
|
+
await addToContext(run, {
|
|
665
|
+
interactions: [
|
|
666
|
+
{
|
|
667
|
+
role: "tool",
|
|
668
|
+
tool_call_id: tool_call.id,
|
|
669
|
+
name: tool_call.function.name,
|
|
670
|
+
content: result || "Action run",
|
|
671
|
+
},
|
|
672
|
+
],
|
|
673
|
+
});
|
|
674
|
+
} else if (tool_call.function.name.startsWith("Query")) {
|
|
675
|
+
const table = Table.findOne({
|
|
676
|
+
name: tool_call.function.name.replace("Query", ""),
|
|
677
|
+
});
|
|
678
|
+
const query = JSON.parse(tool_call.function.arguments);
|
|
679
|
+
const result = await table.getRows(query);
|
|
680
|
+
await addToContext(run, {
|
|
681
|
+
interactions: [
|
|
682
|
+
{
|
|
683
|
+
role: "tool",
|
|
684
|
+
tool_call_id: tool_call.id,
|
|
685
|
+
name: tool_call.function.name,
|
|
686
|
+
content: JSON.stringify(result),
|
|
687
|
+
},
|
|
688
|
+
],
|
|
689
|
+
});
|
|
690
|
+
responses.push(
|
|
691
|
+
await renderQueryInteraction(table, result, config, req)
|
|
692
|
+
);
|
|
693
|
+
hasResult = true;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
if (hasResult)
|
|
697
|
+
return await process_interaction(run, "", config, req, [
|
|
698
|
+
...prevResponses,
|
|
699
|
+
...responses,
|
|
700
|
+
]);
|
|
701
|
+
} else responses.push(wrapSegment(md.render(answer), "Copilot"));
|
|
702
|
+
|
|
703
|
+
return {
|
|
704
|
+
json: {
|
|
705
|
+
success: "ok",
|
|
706
|
+
response: [...prevResponses, ...responses].join(""),
|
|
707
|
+
run_id: run.id,
|
|
708
|
+
},
|
|
709
|
+
};
|
|
710
|
+
};
|
|
711
|
+
|
|
712
|
+
const wrapSegment = (html, who) =>
|
|
713
|
+
'<div class="interaction-segment"><span class="badge bg-secondary">' +
|
|
714
|
+
who +
|
|
715
|
+
"</span>" +
|
|
716
|
+
html +
|
|
717
|
+
"</div>";
|
|
718
|
+
|
|
719
|
+
const wrapCard = (title, ...inners) =>
|
|
720
|
+
span({ class: "badge bg-info ms-1" }, title) +
|
|
721
|
+
div(
|
|
722
|
+
{ class: "card mb-3 bg-secondary-subtle" },
|
|
723
|
+
div({ class: "card-body" }, inners)
|
|
724
|
+
);
|
|
725
|
+
|
|
726
|
+
const wrapAction = (
|
|
727
|
+
inner_markup,
|
|
728
|
+
viewname,
|
|
729
|
+
tool_call,
|
|
730
|
+
actionClass,
|
|
731
|
+
implemented,
|
|
732
|
+
run
|
|
733
|
+
) =>
|
|
734
|
+
wrapCard(
|
|
735
|
+
actionClass.title,
|
|
736
|
+
inner_markup + implemented
|
|
737
|
+
? button(
|
|
738
|
+
{
|
|
739
|
+
type: "button",
|
|
740
|
+
class: "btn btn-secondary d-block mt-3 float-end",
|
|
741
|
+
disabled: true,
|
|
742
|
+
},
|
|
743
|
+
i({ class: "fas fa-check me-1" }),
|
|
744
|
+
"Applied"
|
|
745
|
+
)
|
|
746
|
+
: button(
|
|
747
|
+
{
|
|
748
|
+
type: "button",
|
|
749
|
+
id: "exec-" + tool_call.id,
|
|
750
|
+
class: "btn btn-primary d-block mt-3 float-end",
|
|
751
|
+
onclick: `press_store_button(this, true);view_post('${viewname}', 'execute', {fcall_id: '${tool_call.id}', run_id: ${run.id}}, processExecuteResponse)`,
|
|
752
|
+
},
|
|
753
|
+
"Apply"
|
|
754
|
+
) + div({ id: "postexec-" + tool_call.id })
|
|
755
|
+
);
|
|
756
|
+
|
|
757
|
+
const addToContext = async (run, newCtx) => {
|
|
758
|
+
if (run.addToContext) return await run.addToContext(newCtx);
|
|
759
|
+
let changed = true;
|
|
760
|
+
Object.keys(newCtx).forEach((k) => {
|
|
761
|
+
if (Array.isArray(run.context[k])) {
|
|
762
|
+
if (!Array.isArray(newCtx[k]))
|
|
763
|
+
throw new Error("Must be array to append to array");
|
|
764
|
+
run.context[k].push(...newCtx[k]);
|
|
765
|
+
changed = true;
|
|
766
|
+
} else if (typeof run.context[k] === "object") {
|
|
767
|
+
if (typeof newCtx[k] !== "object")
|
|
768
|
+
throw new Error("Must be object to append to object");
|
|
769
|
+
Object.assign(run.context[k], newCtx[k]);
|
|
770
|
+
changed = true;
|
|
771
|
+
} else {
|
|
772
|
+
run.context[k] = newCtx[k];
|
|
773
|
+
changed = true;
|
|
774
|
+
}
|
|
775
|
+
});
|
|
776
|
+
if (changed) await run.update({ context: run.context });
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
module.exports = {
|
|
780
|
+
name: "User Copilot",
|
|
781
|
+
configuration_workflow,
|
|
782
|
+
display_state_form: false,
|
|
783
|
+
get_state_fields,
|
|
784
|
+
tableless: true,
|
|
785
|
+
run,
|
|
786
|
+
routes: { interact },
|
|
787
|
+
};
|
package/workflow-gen.js
CHANGED
|
@@ -27,8 +27,8 @@ module.exports = {
|
|
|
27
27
|
systemPrompt,
|
|
28
28
|
};
|
|
29
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));
|
|
30
|
+
//console.log(prompt);
|
|
31
|
+
//console.log(JSON.stringify(toolargs, null, 2));
|
|
32
32
|
|
|
33
33
|
const answer = await getState().functions.llm_generate.run(
|
|
34
34
|
prompt,
|