@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 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((r) => r.context.interactions);
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
- p(interact.content)
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 {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}
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 return { json: { success: "ok", response: answer, run_id: run.id } };
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.1",
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
 
@@ -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,