@saltcorn/copilot 0.3.1 → 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 +26 -13
- package/common.js +1 -2
- package/index.js +1 -6
- package/package.json +3 -3
- package/prompts/action-builder.txt +1 -1
- package/user-copilot.js +787 -0
- package/workflow-gen.js +2 -2
package/chat-copilot.js
CHANGED
|
@@ -33,16 +33,22 @@ const {
|
|
|
33
33
|
getPromptFromTemplate,
|
|
34
34
|
incompleteCfgMsg,
|
|
35
35
|
} = require("./common");
|
|
36
|
+
const MarkdownIt = require("markdown-it"),
|
|
37
|
+
md = new MarkdownIt();
|
|
36
38
|
|
|
37
39
|
const get_state_fields = () => [];
|
|
38
40
|
|
|
39
41
|
const run = async (table_id, viewname, cfg, state, { res, req }) => {
|
|
40
42
|
const prevRuns = (
|
|
41
43
|
await WorkflowRun.find(
|
|
42
|
-
{ trigger_id: null },
|
|
44
|
+
{ trigger_id: null /*started_by: req.user?.id*/ }, //todo uncomment
|
|
43
45
|
{ orderBy: "started_at", orderDesc: true, limit: 30 }
|
|
44
46
|
)
|
|
45
|
-
).filter(
|
|
47
|
+
).filter(
|
|
48
|
+
(r) =>
|
|
49
|
+
r.context.interactions &&
|
|
50
|
+
(r.context.copilot === "_system" || !r.context.copilot)
|
|
51
|
+
);
|
|
46
52
|
const cfgMsg = incompleteCfgMsg();
|
|
47
53
|
if (cfgMsg) return cfgMsg;
|
|
48
54
|
let runInteractions = "";
|
|
@@ -85,7 +91,9 @@ const run = async (table_id, viewname, cfg, state, { res, req }) => {
|
|
|
85
91
|
div(
|
|
86
92
|
{ class: "interaction-segment" },
|
|
87
93
|
span({ class: "badge bg-secondary" }, "Copilot"),
|
|
88
|
-
|
|
94
|
+
typeof interact.content === "string"
|
|
95
|
+
? md.render(interact.content)
|
|
96
|
+
: interact.content
|
|
89
97
|
)
|
|
90
98
|
);
|
|
91
99
|
break;
|
|
@@ -154,18 +162,19 @@ const run = async (table_id, viewname, cfg, state, { res, req }) => {
|
|
|
154
162
|
button(
|
|
155
163
|
{
|
|
156
164
|
type: "button",
|
|
157
|
-
class: "btn btn-secondary btn-sm",
|
|
165
|
+
class: "btn btn-secondary btn-sm py-0",
|
|
166
|
+
style: "font-size: 0.9em;height:1.5em",
|
|
158
167
|
onclick: "unset_state_field('run_id')",
|
|
159
168
|
title: "New session",
|
|
160
169
|
},
|
|
161
|
-
i({ class: "fas fa-redo" })
|
|
170
|
+
i({ class: "fas fa-redo fa-sm" })
|
|
162
171
|
)
|
|
163
172
|
),
|
|
164
173
|
prevRuns.map((run) =>
|
|
165
174
|
div(
|
|
166
175
|
{
|
|
167
176
|
onclick: `set_state_field('run_id',${run.id})`,
|
|
168
|
-
class: "prevcopilotrun",
|
|
177
|
+
class: "prevcopilotrun border p-2",
|
|
169
178
|
},
|
|
170
179
|
localeDateTime(run.started_at),
|
|
171
180
|
|
|
@@ -200,9 +209,8 @@ const run = async (table_id, viewname, cfg, state, { res, req }) => {
|
|
|
200
209
|
`div.interaction-segment:not(:first-child) {border-top: 1px solid #e7e7e7; }
|
|
201
210
|
div.interaction-segment {padding-top: 5px;padding-bottom: 5px;}
|
|
202
211
|
div.interaction-segment p {margin-bottom: 0px;}
|
|
203
|
-
div.interaction-segment div.card {margin-top: 0.5rem;}
|
|
204
|
-
div.prevcopilotrun {
|
|
205
|
-
div.prevcopilotrun:hover {cursor: pointer}
|
|
212
|
+
div.interaction-segment div.card {margin-top: 0.5rem;}
|
|
213
|
+
div.prevcopilotrun:hover {cursor: pointer; background-color: var(--tblr-secondary-bg-subtle, var(--bs-secondary-bg-subtle, gray));}
|
|
206
214
|
.copilot-entry .submit-button:hover { cursor: pointer}
|
|
207
215
|
|
|
208
216
|
.copilot-entry .submit-button {
|
|
@@ -238,7 +246,7 @@ const run = async (table_id, viewname, cfg, state, { res, req }) => {
|
|
|
238
246
|
function restore_old_button_elem(btn) {
|
|
239
247
|
const oldText = $(btn).data("old-text");
|
|
240
248
|
btn.html(oldText);
|
|
241
|
-
btn.css({ width: "" });
|
|
249
|
+
btn.css({ width: "" }).prop("disabled", false);
|
|
242
250
|
btn.removeData("old-text");
|
|
243
251
|
}
|
|
244
252
|
function processExecuteResponse(res) {
|
|
@@ -332,7 +340,9 @@ const interact = async (table_id, viewname, config, body, { req }) => {
|
|
|
332
340
|
if (!run_id || run_id === "undefined")
|
|
333
341
|
run = await WorkflowRun.create({
|
|
334
342
|
status: "Running",
|
|
343
|
+
started_by: req.user?.id,
|
|
335
344
|
context: {
|
|
345
|
+
copilot: "_system",
|
|
336
346
|
implemented_fcall_ids: [],
|
|
337
347
|
interactions: [{ role: "user", content: userinput }],
|
|
338
348
|
funcalls: {},
|
|
@@ -382,7 +392,10 @@ const interact = async (table_id, viewname, config, body, { req }) => {
|
|
|
382
392
|
actions.push(markup);
|
|
383
393
|
}
|
|
384
394
|
return { json: { success: "ok", actions, run_id: run.id } };
|
|
385
|
-
} else
|
|
395
|
+
} else
|
|
396
|
+
return {
|
|
397
|
+
json: { success: "ok", response: md.render(answer), run_id: run.id },
|
|
398
|
+
};
|
|
386
399
|
};
|
|
387
400
|
|
|
388
401
|
const renderToolcall = async (tool_call, viewname, implemented, run) => {
|
|
@@ -409,9 +422,9 @@ const wrapAction = (
|
|
|
409
422
|
implemented,
|
|
410
423
|
run
|
|
411
424
|
) =>
|
|
425
|
+
span({ class: "badge bg-info ms-1" }, actionClass.title) +
|
|
412
426
|
div(
|
|
413
|
-
{ class: "card mb-3" },
|
|
414
|
-
div({ class: "card-header" }, h5(actionClass.title)),
|
|
427
|
+
{ class: "card mb-3 bg-secondary-subtle" },
|
|
415
428
|
div(
|
|
416
429
|
{ class: "card-body" },
|
|
417
430
|
inner_markup,
|
package/common.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
const axios = require("axios");
|
|
2
1
|
const fsp = require("fs").promises;
|
|
3
2
|
const _ = require("underscore");
|
|
4
3
|
const path = require("path");
|
|
@@ -28,7 +27,7 @@ const getPromptFromTemplate = async (tmplName, userPrompt, extraCtx = {}) => {
|
|
|
28
27
|
interpolate: /\{\{([^#].+?)\}\}/g,
|
|
29
28
|
});
|
|
30
29
|
const prompt = template(context);
|
|
31
|
-
console.log("Full prompt:\n", prompt);
|
|
30
|
+
//console.log("Full prompt:\n", prompt);
|
|
32
31
|
return prompt;
|
|
33
32
|
};
|
|
34
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
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saltcorn/copilot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "AI assistant for building Saltcorn applications",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"@saltcorn/data": "^0.9.0",
|
|
8
|
-
"axios": "0.16.2",
|
|
9
8
|
"underscore": "1.13.6",
|
|
10
|
-
"node-sql-parser": "4.15.0"
|
|
9
|
+
"node-sql-parser": "4.15.0",
|
|
10
|
+
"markdown-it": "14.1.0"
|
|
11
11
|
},
|
|
12
12
|
"author": "Tom Nielsen",
|
|
13
13
|
"license": "MIT",
|
|
@@ -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,
|