@saltcorn/copilot 0.3.2 → 0.4.1

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
@@ -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((r) => r.context.interactions);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saltcorn/copilot",
3
- "version": "0.3.2",
3
+ "version": "0.4.1",
4
4
  "description": "AI assistant for building Saltcorn applications",
5
5
  "main": "index.js",
6
6
  "dependencies": {
@@ -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,788 @@
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
+ if (Object.keys(row || {}).length)
197
+ interactMarkups.push(
198
+ wrapSegment(
199
+ wrapCard(
200
+ action.trigger_name,
201
+ pre(JSON.stringify(row, null, 2))
202
+ ),
203
+ "Copilot"
204
+ )
205
+ );
206
+ }
207
+ }
208
+ } else
209
+ interactMarkups.push(
210
+ div(
211
+ { class: "interaction-segment" },
212
+ span({ class: "badge bg-secondary" }, "Copilot"),
213
+ typeof interact.content === "string"
214
+ ? md.render(interact.content)
215
+ : interact.content
216
+ )
217
+ );
218
+ break;
219
+ case "tool":
220
+ if (interact.name.startsWith("Query")) {
221
+ const table = Table.findOne({
222
+ name: interact.name.replace("Query", ""),
223
+ });
224
+ interactMarkups.push(
225
+ await renderQueryInteraction(
226
+ table,
227
+ JSON.parse(interact.content),
228
+ config,
229
+ req
230
+ )
231
+ );
232
+ } else if (interact.content !== "Action run") {
233
+ interactMarkups.push(
234
+ wrapSegment(
235
+ wrapCard(
236
+ interact.name,
237
+ pre(JSON.stringify(JSON.parse(interact.content), null, 2))
238
+ ),
239
+ "Copilot"
240
+ )
241
+ );
242
+ }
243
+ break;
244
+ }
245
+ }
246
+ runInteractions = interactMarkups.join("");
247
+ }
248
+ const input_form = form(
249
+ {
250
+ onsubmit: `event.preventDefault();spin_send_button();view_post('${viewname}', 'interact', $(this).serialize(), processCopilotResponse);return false;`,
251
+ class: "form-namespace copilot mt-2",
252
+ method: "post",
253
+ },
254
+ input({
255
+ type: "hidden",
256
+ name: "_csrf",
257
+ value: req.csrfToken(),
258
+ }),
259
+ input({
260
+ type: "hidden",
261
+ class: "form-control ",
262
+ name: "run_id",
263
+ value: state.run_id ? +state.run_id : undefined,
264
+ }),
265
+ div(
266
+ { class: "copilot-entry" },
267
+ textarea({
268
+ class: "form-control",
269
+ name: "userinput",
270
+ "data-fieldname": "userinput",
271
+ placeholder: "How can I help you?",
272
+ id: "inputuserinput",
273
+ rows: "3",
274
+ autofocus: true,
275
+ }),
276
+ span(
277
+ { class: "submit-button p-2", onclick: "$('form.copilot').submit()" },
278
+ i({ id: "sendbuttonicon", class: "far fa-paper-plane" })
279
+ )
280
+ )
281
+
282
+ /*i(
283
+ small(
284
+ "Skills you can request: " +
285
+ actionClasses.map((ac) => ac.title).join(", ")
286
+ )
287
+ )*/
288
+ );
289
+ return {
290
+ widths: [3, 9],
291
+ gx: 3,
292
+ besides: [
293
+ {
294
+ type: "container",
295
+ contents: div(
296
+ div(
297
+ {
298
+ class: "d-flex justify-content-between align-middle mb-2",
299
+ },
300
+ h5("Sessions"),
301
+
302
+ button(
303
+ {
304
+ type: "button",
305
+ class: "btn btn-secondary btn-sm py-0",
306
+ style: "font-size: 0.9em;height:1.5em",
307
+ onclick: "unset_state_field('run_id')",
308
+ title: "New session",
309
+ },
310
+ i({ class: "fas fa-redo fa-sm" })
311
+ )
312
+ ),
313
+ prevRuns.map((run) =>
314
+ div(
315
+ {
316
+ onclick: `set_state_field('run_id',${run.id})`,
317
+ class: "prevcopilotrun border p-2",
318
+ },
319
+ localeDateTime(run.started_at),
320
+
321
+ p(
322
+ { class: "prevrun_content" },
323
+ run.context.interactions[0]?.content
324
+ )
325
+ )
326
+ )
327
+ ),
328
+ },
329
+ {
330
+ type: "container",
331
+ contents: div(
332
+ { class: "card" },
333
+ div(
334
+ { class: "card-body" },
335
+ script({
336
+ src: `/static_assets/${db.connectObj.version_tag}/mermaid.min.js`,
337
+ }),
338
+ script(
339
+ { type: "module" },
340
+ `mermaid.initialize({securityLevel: 'loose'${
341
+ getState().getLightDarkMode(req.user) === "dark"
342
+ ? ",theme: 'dark',"
343
+ : ""
344
+ }});`
345
+ ),
346
+ div({ id: "copilotinteractions" }, runInteractions),
347
+ input_form,
348
+ style(
349
+ `div.interaction-segment:not(:first-child) {border-top: 1px solid #e7e7e7; }
350
+ div.interaction-segment {padding-top: 5px;padding-bottom: 5px;}
351
+ div.interaction-segment p {margin-bottom: 0px;}
352
+ div.interaction-segment div.card {margin-top: 0.5rem;}
353
+ div.prevcopilotrun:hover {cursor: pointer; background-color: var(--tblr-secondary-bg-subtle, var(--bs-secondary-bg-subtle, gray));}
354
+ .copilot-entry .submit-button:hover { cursor: pointer}
355
+
356
+ .copilot-entry .submit-button {
357
+ position: relative;
358
+ top: -1.8rem;
359
+ left: 0.1rem;
360
+ }
361
+ .copilot-entry {margin-bottom: -1.25rem; margin-top: 1rem;}
362
+ p.prevrun_content {
363
+ white-space: nowrap;
364
+ overflow: hidden;
365
+ margin-bottom: 0px;
366
+ display: block;
367
+ text-overflow: ellipsis;}`
368
+ ),
369
+ script(`function processCopilotResponse(res) {
370
+ $("#sendbuttonicon").attr("class","far fa-paper-plane");
371
+ const $runidin= $("input[name=run_id")
372
+ if(res.run_id && (!$runidin.val() || $runidin.val()=="undefined"))
373
+ $runidin.val(res.run_id);
374
+ const wrapSegment = (html, who) => '<div class="interaction-segment"><span class="badge bg-secondary">'+who+'</span>'+html+'</div>'
375
+ $("#copilotinteractions").append(wrapSegment('<p>'+$("textarea[name=userinput]").val()+'</p>', "You"))
376
+ $("textarea[name=userinput]").val("")
377
+
378
+ if(res.response)
379
+ $("#copilotinteractions").append(res.response)
380
+ }
381
+ function restore_old_button_elem(btn) {
382
+ const oldText = $(btn).data("old-text");
383
+ btn.html(oldText);
384
+ btn.css({ width: "" }).prop("disabled", false);
385
+ btn.removeData("old-text");
386
+ }
387
+ function processExecuteResponse(res) {
388
+ const btn = $("#exec-"+res.fcall_id)
389
+ restore_old_button_elem($("#exec-"+res.fcall_id))
390
+ btn.prop('disabled', true);
391
+ btn.html('<i class="fas fa-check me-1"></i>Applied')
392
+ btn.removeClass("btn-primary")
393
+ btn.addClass("btn-secondary")
394
+ if(res.postExec) {
395
+ $('#postexec-'+res.fcall_id).html(res.postExec)
396
+ }
397
+ }
398
+ function submitOnEnter(event) {
399
+ if (event.which === 13) {
400
+ if (!event.repeat) {
401
+ const newEvent = new Event("submit", {cancelable: true});
402
+ event.target.form.dispatchEvent(newEvent);
403
+ }
404
+
405
+ event.preventDefault(); // Prevents the addition of a new line in the text field
406
+ }
407
+ }
408
+ document.getElementById("inputuserinput").addEventListener("keydown", submitOnEnter);
409
+ function spin_send_button() {
410
+ $("#sendbuttonicon").attr("class","fas fa-spinner fa-spin");
411
+ }
412
+ `)
413
+ )
414
+ ),
415
+ },
416
+ ],
417
+ };
418
+ };
419
+
420
+ const getCompletionArguments = async (config) => {
421
+ let tools = [];
422
+ const sysPrompts = [];
423
+ for (const tableCfg of config?.tables || []) {
424
+ let properties = {};
425
+
426
+ const table = Table.findOne({ name: tableCfg.table_name });
427
+
428
+ table.fields
429
+ .filter((f) => !f.primary_key)
430
+ .forEach((field) => {
431
+ properties[field.name] = {
432
+ description: field.label + " " + field.description || "",
433
+ };
434
+ if (field.type.name === "String") {
435
+ properties[field.name].anyOf = [
436
+ { type: "string", description: "Match this value exactly" },
437
+ {
438
+ type: "object",
439
+ properties: {
440
+ ilike: {
441
+ type: "string",
442
+ description: "Case insensitive substring match.",
443
+ },
444
+ in: {
445
+ type: "array",
446
+ description: "Match any one of these values",
447
+ items: {
448
+ type: "string",
449
+ },
450
+ },
451
+ },
452
+ },
453
+ ];
454
+ }
455
+ if (field.type.name === "Integer" || field.type.name === "Float") {
456
+ const basetype = field.type.name === "Integer" ? "integer" : "number";
457
+ properties[field.name].anyOf = [
458
+ { type: basetype, description: "Match this value exactly" },
459
+ {
460
+ type: "object",
461
+ properties: {
462
+ gt: {
463
+ type: basetype,
464
+ description: "Greater than this value",
465
+ },
466
+ lt: {
467
+ type: basetype,
468
+ description: "Less than this value",
469
+ },
470
+ in: {
471
+ type: "array",
472
+ description: "Match any one of these values",
473
+ items: {
474
+ type: basetype,
475
+ },
476
+ },
477
+ },
478
+ },
479
+ ];
480
+ } else Object.assign(fieldProperties(field), properties[field.name]);
481
+ });
482
+
483
+ tools.push({
484
+ type: "function",
485
+ function: {
486
+ name: "Query" + table.name,
487
+ description: `Query the ${table.name} table. ${
488
+ table.description || ""
489
+ }`,
490
+ parameters: {
491
+ type: "object",
492
+ //required: ["action_javascript_code", "action_name"],
493
+ properties,
494
+ },
495
+ },
496
+ });
497
+ }
498
+ for (const action of config?.actions || []) {
499
+ let properties = {};
500
+
501
+ const trigger = Trigger.findOne({ name: action.trigger_name });
502
+ if (trigger.table_id) {
503
+ const table = Table.findOne({ id: trigger.table_id });
504
+
505
+ table.fields
506
+ .filter((f) => !f.primary_key)
507
+ .forEach((field) => {
508
+ properties[field.name] = {
509
+ description: field.label + " " + field.description || "",
510
+ ...fieldProperties(field),
511
+ };
512
+ });
513
+ }
514
+ tools.push({
515
+ type: "function",
516
+ function: {
517
+ name: action.trigger_name,
518
+ description: trigger.description,
519
+ parameters: {
520
+ type: "object",
521
+ //required: ["action_javascript_code", "action_name"],
522
+ properties,
523
+ },
524
+ },
525
+ });
526
+ }
527
+ const systemPrompt =
528
+ "You are helping users retrieve information and perform actions on a relational database" +
529
+ config.sys_prompt;
530
+
531
+ if (tools.length === 0) tools = undefined;
532
+ return { tools, systemPrompt };
533
+ };
534
+
535
+ /*
536
+
537
+ build a workflow that asks the user for their name and age
538
+
539
+ */
540
+
541
+ const interact = async (table_id, viewname, config, body, { req, res }) => {
542
+ const { userinput, run_id } = body;
543
+ let run;
544
+ if (!run_id || run_id === "undefined")
545
+ run = await WorkflowRun.create({
546
+ status: "Running",
547
+ started_by: req.user?.id,
548
+ context: {
549
+ copilot: viewname,
550
+ implemented_fcall_ids: [],
551
+ interactions: [{ role: "user", content: userinput }],
552
+ funcalls: {},
553
+ },
554
+ });
555
+ else {
556
+ run = await WorkflowRun.findOne({ id: +run_id });
557
+ await addToContext(run, {
558
+ interactions: [{ role: "user", content: userinput }],
559
+ });
560
+ }
561
+ return await process_interaction(run, userinput, config, req);
562
+ };
563
+
564
+ const renderQueryInteraction = async (table, result, config, req) => {
565
+ if (result.length === 0) return "No rows found";
566
+
567
+ const tableCfg = config.tables.find((t) => t.table_name === table.name);
568
+ let viewRes = "";
569
+
570
+ if (result.length === 1) {
571
+ const view = View.findOne({ name: tableCfg.show_view });
572
+ if (view) {
573
+ viewRes = await view.run(
574
+ { [table.pk_name]: result[0][table.pk_name] },
575
+ { req }
576
+ );
577
+ } else viewRes = pre(JSON.stringify(result[0], null, 2));
578
+ } else {
579
+ const view = View.findOne({ name: tableCfg.list_view });
580
+ if (view) {
581
+ viewRes = await view.run(
582
+ { [table.pk_name]: { in: result.map((r) => r[table.pk_name]) } },
583
+ { req }
584
+ );
585
+ } else viewRes = pre(JSON.stringify(result, null, 2));
586
+ }
587
+ return wrapSegment(
588
+ wrapCard(
589
+ "Query " + table.name,
590
+ //div("Query: ", code(JSON.stringify(query))),
591
+ viewRes
592
+ ),
593
+ "Copilot"
594
+ );
595
+ };
596
+
597
+ const process_interaction = async (
598
+ run,
599
+ input,
600
+ config,
601
+ req,
602
+ prevResponses = []
603
+ ) => {
604
+ const complArgs = await getCompletionArguments(config);
605
+ complArgs.chat = run.context.interactions;
606
+ //console.log(complArgs);
607
+ console.log("complArgs", JSON.stringify(complArgs, null, 2));
608
+
609
+ const answer = await getState().functions.llm_generate.run(input, complArgs);
610
+ console.log("answer", answer);
611
+ await addToContext(run, {
612
+ interactions:
613
+ typeof answer === "object" && answer.tool_calls
614
+ ? [{ role: "assistant", tool_calls: answer.tool_calls }]
615
+ : [{ role: "assistant", content: answer }],
616
+ });
617
+ const responses = [];
618
+
619
+ if (typeof answer === "object" && answer.tool_calls) {
620
+ //const actions = [];
621
+ let hasResult = false;
622
+ for (const tool_call of answer.tool_calls) {
623
+ console.log("call function", tool_call.function);
624
+
625
+ await addToContext(run, {
626
+ funcalls: { [tool_call.id]: tool_call.function },
627
+ });
628
+ const action = config.actions.find(
629
+ (a) => a.trigger_name === tool_call.function.name
630
+ );
631
+ console.log({ action });
632
+
633
+ if (action) {
634
+ const trigger = Trigger.findOne({ name: action.trigger_name });
635
+ const row = JSON.parse(tool_call.function.arguments);
636
+ if (Object.keys(row || {}).length)
637
+ responses.push(
638
+ wrapSegment(
639
+ wrapCard(action.trigger_name, pre(JSON.stringify(row, null, 2))),
640
+ "Copilot"
641
+ )
642
+ );
643
+ const result = await trigger.runWithoutRow({ user: req.user, row });
644
+ console.log("ran trigger with result", {
645
+ name: trigger.name,
646
+ row,
647
+ result,
648
+ });
649
+
650
+ if (typeof result === "object" && Object.keys(result || {}).length) {
651
+ responses.push(
652
+ wrapSegment(
653
+ wrapCard(
654
+ action.trigger_name + " result",
655
+ pre(JSON.stringify(result, null, 2))
656
+ ),
657
+ "Copilot"
658
+ )
659
+ );
660
+ hasResult = true;
661
+ }
662
+ await addToContext(run, {
663
+ interactions: [
664
+ {
665
+ role: "tool",
666
+ tool_call_id: tool_call.id,
667
+ name: tool_call.function.name,
668
+ content: result ? JSON.stringify(result) : "Action run",
669
+ },
670
+ ],
671
+ });
672
+ } else if (tool_call.function.name.startsWith("Query")) {
673
+ const table = Table.findOne({
674
+ name: tool_call.function.name.replace("Query", ""),
675
+ });
676
+ const query = JSON.parse(tool_call.function.arguments);
677
+ const result = await table.getRows(query, {
678
+ forUser: req.user,
679
+ forPublic: !req.user,
680
+ });
681
+ await addToContext(run, {
682
+ interactions: [
683
+ {
684
+ role: "tool",
685
+ tool_call_id: tool_call.id,
686
+ name: tool_call.function.name,
687
+ content: JSON.stringify(result),
688
+ },
689
+ ],
690
+ });
691
+ responses.push(
692
+ await renderQueryInteraction(table, result, config, req)
693
+ );
694
+ hasResult = true;
695
+ }
696
+ }
697
+ if (hasResult)
698
+ return await process_interaction(run, "", config, req, [
699
+ ...prevResponses,
700
+ ...responses,
701
+ ]);
702
+ } else responses.push(wrapSegment(md.render(answer), "Copilot"));
703
+
704
+ return {
705
+ json: {
706
+ success: "ok",
707
+ response: [...prevResponses, ...responses].join(""),
708
+ run_id: run.id,
709
+ },
710
+ };
711
+ };
712
+
713
+ const wrapSegment = (html, who) =>
714
+ '<div class="interaction-segment"><span class="badge bg-secondary">' +
715
+ who +
716
+ "</span>" +
717
+ html +
718
+ "</div>";
719
+
720
+ const wrapCard = (title, ...inners) =>
721
+ span({ class: "badge bg-info ms-1" }, title) +
722
+ div(
723
+ { class: "card mb-3 bg-secondary-subtle" },
724
+ div({ class: "card-body" }, inners)
725
+ );
726
+
727
+ const wrapAction = (
728
+ inner_markup,
729
+ viewname,
730
+ tool_call,
731
+ actionClass,
732
+ implemented,
733
+ run
734
+ ) =>
735
+ wrapCard(
736
+ actionClass.title,
737
+ inner_markup + implemented
738
+ ? button(
739
+ {
740
+ type: "button",
741
+ class: "btn btn-secondary d-block mt-3 float-end",
742
+ disabled: true,
743
+ },
744
+ i({ class: "fas fa-check me-1" }),
745
+ "Applied"
746
+ )
747
+ : button(
748
+ {
749
+ type: "button",
750
+ id: "exec-" + tool_call.id,
751
+ class: "btn btn-primary d-block mt-3 float-end",
752
+ onclick: `press_store_button(this, true);view_post('${viewname}', 'execute', {fcall_id: '${tool_call.id}', run_id: ${run.id}}, processExecuteResponse)`,
753
+ },
754
+ "Apply"
755
+ ) + div({ id: "postexec-" + tool_call.id })
756
+ );
757
+
758
+ const addToContext = async (run, newCtx) => {
759
+ if (run.addToContext) return await run.addToContext(newCtx);
760
+ let changed = true;
761
+ Object.keys(newCtx).forEach((k) => {
762
+ if (Array.isArray(run.context[k])) {
763
+ if (!Array.isArray(newCtx[k]))
764
+ throw new Error("Must be array to append to array");
765
+ run.context[k].push(...newCtx[k]);
766
+ changed = true;
767
+ } else if (typeof run.context[k] === "object") {
768
+ if (typeof newCtx[k] !== "object")
769
+ throw new Error("Must be object to append to object");
770
+ Object.assign(run.context[k], newCtx[k]);
771
+ changed = true;
772
+ } else {
773
+ run.context[k] = newCtx[k];
774
+ changed = true;
775
+ }
776
+ });
777
+ if (changed) await run.update({ context: run.context });
778
+ };
779
+
780
+ module.exports = {
781
+ name: "User Copilot",
782
+ configuration_workflow,
783
+ display_state_form: false,
784
+ get_state_fields,
785
+ tableless: true,
786
+ run,
787
+ routes: { interact },
788
+ };
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,