@saltcorn/copilot 0.8.1 → 0.8.2

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 DELETED
@@ -1,770 +0,0 @@
1
- const Field = require("@saltcorn/data/models/field");
2
- const Table = require("@saltcorn/data/models/table");
3
- const Form = require("@saltcorn/data/models/form");
4
- const View = require("@saltcorn/data/models/view");
5
- const Trigger = require("@saltcorn/data/models/trigger");
6
- const { findType } = require("@saltcorn/data/models/discovery");
7
- const { save_menu_items } = require("@saltcorn/data/models/config");
8
- const db = require("@saltcorn/data/db");
9
- const WorkflowRun = require("@saltcorn/data/models/workflow_run");
10
- const { localeDateTime } = require("@saltcorn/markup");
11
- const {
12
- div,
13
- script,
14
- domReady,
15
- pre,
16
- code,
17
- input,
18
- h4,
19
- style,
20
- h5,
21
- button,
22
- text_attr,
23
- i,
24
- p,
25
- span,
26
- small,
27
- form,
28
- textarea,
29
- } = require("@saltcorn/markup/tags");
30
- const { getState } = require("@saltcorn/data/db/state");
31
- const {
32
- getCompletion,
33
- getPromptFromTemplate,
34
- incompleteCfgMsg,
35
- } = require("./common");
36
- const MarkdownIt = require("markdown-it"),
37
- md = new MarkdownIt();
38
-
39
- const AGENT_VIEWTEMPLATE_NAME = "Agent Chat";
40
- const COPILOT_AGENT_TRIGGER_NAME = "Saltcorn Copilot";
41
-
42
- const getAgentViewtemplate = () => {
43
- const state = getState();
44
- return state?.viewtemplates?.[AGENT_VIEWTEMPLATE_NAME];
45
- };
46
-
47
- const agentActionRegistered = () => {
48
- const state = getState();
49
- return !!(state?.actions && (state.actions.Agent || state.actions["Agent"]));
50
- };
51
-
52
- let copilotAgentTriggerCreateFailed = false;
53
- let copilotAgentTriggerCreateLogged = false;
54
-
55
- const ensureCopilotAgentTrigger = async () => {
56
- if (!agentActionRegistered()) return null;
57
- let trigger = await Trigger.findOne({
58
- name: COPILOT_AGENT_TRIGGER_NAME,
59
- action: "Agent",
60
- });
61
- if (trigger) return trigger;
62
-
63
- if (copilotAgentTriggerCreateFailed) return null;
64
- try {
65
- return await Trigger.create({
66
- action: "Agent",
67
- when_trigger: "Never",
68
- name: COPILOT_AGENT_TRIGGER_NAME,
69
- configuration: {
70
- prompt: "",
71
- sys_prompt:
72
- "You are Saltcorn Copilot. Help users build Saltcorn applications by proposing database schema, pages, views, workflows and actions.",
73
- skills: [
74
- { skill_type: "Database design" },
75
- { skill_type: "Generate Page" },
76
- ],
77
- },
78
- });
79
- } catch (e) {
80
- copilotAgentTriggerCreateFailed = true;
81
- const state = getState();
82
- const msg =
83
- e && e.message
84
- ? e.message
85
- : "Unknown error while auto-creating Copilot Agent trigger";
86
- if (!copilotAgentTriggerCreateLogged) {
87
- copilotAgentTriggerCreateLogged = true;
88
- if (state?.log) state.log(2, `Copilot: ${msg}`);
89
- else console.error("Copilot:", msg);
90
- }
91
- return null;
92
- }
93
- };
94
-
95
- const getAgentChatCfg = async () => {
96
- const trigger = await ensureCopilotAgentTrigger();
97
- if (!trigger) return null;
98
- return {
99
- action_id: trigger.id,
100
- show_prev_runs: true,
101
- prev_runs_closed: false,
102
- placeholder: "How can I help you?",
103
- explainer: "",
104
- image_upload: false,
105
- stream: true,
106
- audio_recorder: false,
107
- layout: "Vertical",
108
- };
109
- };
110
-
111
- const get_state_fields = () => [];
112
-
113
- const runLegacy = async (table_id, viewname, cfg, state, { res, req }) => {
114
- const prevRuns = (
115
- await WorkflowRun.find(
116
- { trigger_id: null /*started_by: req.user?.id*/ }, //todo uncomment
117
- { orderBy: "started_at", orderDesc: true, limit: 30 }
118
- )
119
- ).filter(
120
- (r) =>
121
- r.context.interactions &&
122
- (r.context.copilot === "_system" || !r.context.copilot)
123
- );
124
- const cfgMsg = incompleteCfgMsg();
125
- if (cfgMsg) return cfgMsg;
126
- let runInteractions = "";
127
- if (state.run_id) {
128
- const run = prevRuns.find((r) => r.id == state.run_id);
129
- const interactMarkups = [];
130
- for (const interact of run.context.interactions) {
131
- switch (interact.role) {
132
- case "user":
133
- interactMarkups.push(
134
- div(
135
- { class: "interaction-segment" },
136
- span({ class: "badge bg-secondary" }, "You"),
137
- p(interact.content)
138
- )
139
- );
140
- break;
141
- case "assistant":
142
- case "system":
143
- if (interact.tool_calls) {
144
- for (const tool_call of interact.tool_calls) {
145
- const markup = await renderToolcall(
146
- tool_call,
147
- viewname,
148
- (run.context.implemented_fcall_ids || []).includes(
149
- tool_call.id
150
- ),
151
- run
152
- );
153
- interactMarkups.push(
154
- div(
155
- { class: "interaction-segment" },
156
- span({ class: "badge bg-secondary" }, "Copilot"),
157
- markup
158
- )
159
- );
160
- }
161
- } else
162
- interactMarkups.push(
163
- div(
164
- { class: "interaction-segment" },
165
- span({ class: "badge bg-secondary" }, "Copilot"),
166
- typeof interact.content === "string"
167
- ? md.render(interact.content)
168
- : interact.content
169
- )
170
- );
171
- break;
172
- case "tool":
173
- //ignore
174
- break;
175
- }
176
- }
177
- runInteractions = interactMarkups.join("");
178
- }
179
- const input_form = form(
180
- {
181
- onsubmit:
182
- "event.preventDefault();spin_send_button();view_post('Saltcorn Copilot', 'interact', $(this).serialize(), processCopilotResponse);return false;",
183
- class: "form-namespace copilot mt-2",
184
- method: "post",
185
- },
186
- input({
187
- type: "hidden",
188
- name: "_csrf",
189
- value: req.csrfToken(),
190
- }),
191
- input({
192
- type: "hidden",
193
- class: "form-control ",
194
- name: "run_id",
195
- value: state.run_id ? +state.run_id : undefined,
196
- }),
197
- div(
198
- { class: "copilot-entry" },
199
- textarea({
200
- class: "form-control",
201
- name: "userinput",
202
- "data-fieldname": "userinput",
203
- placeholder: "How can I help you?",
204
- id: "inputuserinput",
205
- rows: "3",
206
- autofocus: true,
207
- }),
208
- span(
209
- { class: "submit-button p-2", onclick: "$('form.copilot').submit()" },
210
- i({ id: "sendbuttonicon", class: "far fa-paper-plane" })
211
- )
212
- ),
213
-
214
- i(
215
- small(
216
- "Skills you can request: " +
217
- classesWithSkills()
218
- .map((ac) => ac.title)
219
- .join(", ")
220
- )
221
- )
222
- );
223
- return {
224
- widths: [3, 9],
225
- gx: 3,
226
- besides: [
227
- {
228
- type: "container",
229
- contents: div(
230
- div(
231
- {
232
- class: "d-flex justify-content-between align-middle mb-2",
233
- },
234
- h5("Sessions"),
235
-
236
- button(
237
- {
238
- type: "button",
239
- class: "btn btn-secondary btn-sm py-0",
240
- style: "font-size: 0.9em;height:1.5em",
241
- onclick: "unset_state_field('run_id')",
242
- title: "New session",
243
- },
244
- i({ class: "fas fa-redo fa-sm" })
245
- )
246
- ),
247
- prevRuns.map((run) =>
248
- div(
249
- {
250
- onclick: `set_state_field('run_id',${run.id})`,
251
- class: "prevcopilotrun border p-2",
252
- },
253
- localeDateTime(run.started_at),
254
-
255
- p(
256
- { class: "prevrun_content" },
257
- run.context.interactions[0]?.content
258
- )
259
- )
260
- )
261
- ),
262
- },
263
- {
264
- type: "container",
265
- contents: div(
266
- { class: "card" },
267
- div(
268
- { class: "card-body" },
269
- script({
270
- src: `/static_assets/${db.connectObj.version_tag}/mermaid.min.js`,
271
- }),
272
- script(
273
- { type: "module" },
274
- `mermaid.initialize({securityLevel: 'loose'${
275
- getState().getLightDarkMode(req.user) === "dark"
276
- ? ",theme: 'dark',"
277
- : ""
278
- }});`
279
- ),
280
- div({ id: "copilotinteractions" }, runInteractions),
281
- input_form,
282
- style(
283
- `div.interaction-segment:not(:first-child) {border-top: 1px solid #e7e7e7; }
284
- div.interaction-segment {padding-top: 5px;padding-bottom: 5px;}
285
- div.interaction-segment p {margin-bottom: 0px;}
286
- div.interaction-segment div.card {margin-top: 0.5rem;}
287
- div.prevcopilotrun:hover {cursor: pointer; background-color: var(--tblr-secondary-bg-subtle, var(--bs-secondary-bg-subtle, gray));}
288
- .copilot-entry .submit-button:hover { cursor: pointer}
289
-
290
- .copilot-entry .submit-button {
291
- position: relative;
292
- top: -1.8rem;
293
- left: 0.1rem;
294
- }
295
- .copilot-entry {margin-bottom: -1.25rem; margin-top: 1rem;}
296
- p.prevrun_content {
297
- white-space: nowrap;
298
- overflow: hidden;
299
- margin-bottom: 0px;
300
- display: block;
301
- text-overflow: ellipsis;}`
302
- ),
303
- script(`function processCopilotResponse(res) {
304
- $("#sendbuttonicon").attr("class","far fa-paper-plane");
305
- const $runidin= $("input[name=run_id")
306
- if(res.run_id && (!$runidin.val() || $runidin.val()=="undefined"))
307
- $runidin.val(res.run_id);
308
- const wrapSegment = (html, who) => '<div class="interaction-segment"><span class="badge bg-secondary">'+who+'</span>'+html+'</div>'
309
- $("#copilotinteractions").append(wrapSegment('<p>'+$("textarea[name=userinput]").val()+'</p>', "You"))
310
- $("textarea[name=userinput]").val("")
311
-
312
- for(const action of res.actions||[]) {
313
- $("#copilotinteractions").append(wrapSegment(action, "Copilot"))
314
-
315
- }
316
-
317
- if(res.response)
318
- $("#copilotinteractions").append(wrapSegment('<p>'+res.response+'</p>', "Copilot"))
319
- }
320
- function restore_old_button_elem(btn) {
321
- const oldText = $(btn).data("old-text");
322
- btn.html(oldText);
323
- btn.css({ width: "" }).prop("disabled", false);
324
- btn.removeData("old-text");
325
- }
326
- function processExecuteResponse(res) {
327
- const btn = $("#exec-"+res.fcall_id)
328
- restore_old_button_elem($("#exec-"+res.fcall_id))
329
- btn.prop('disabled', true);
330
- btn.html('<i class="fas fa-check me-1"></i>Applied')
331
- btn.removeClass("btn-primary")
332
- btn.addClass("btn-secondary")
333
- if(res.postExec) {
334
- $('#postexec-'+res.fcall_id).html(res.postExec)
335
- }
336
- }
337
- function submitOnEnter(event) {
338
- if (event.which === 13) {
339
- if (!event.repeat) {
340
- const newEvent = new Event("submit", {cancelable: true});
341
- event.target.form.dispatchEvent(newEvent);
342
- }
343
-
344
- event.preventDefault(); // Prevents the addition of a new line in the text field
345
- }
346
- }
347
- document.getElementById("inputuserinput").addEventListener("keydown", submitOnEnter);
348
- function spin_send_button() {
349
- $("#sendbuttonicon").attr("class","fas fa-spinner fa-spin");
350
- }
351
- `)
352
- )
353
- ),
354
- },
355
- ],
356
- };
357
- };
358
-
359
- const run = async (table_id, viewname, cfg, state, extra) => {
360
- const agentVt = getAgentViewtemplate();
361
- if (!agentVt) return await runLegacy(table_id, viewname, cfg, state, extra);
362
-
363
- const agentCfg = await getAgentChatCfg();
364
- if (!agentCfg) return await runLegacy(table_id, viewname, cfg, state, extra);
365
-
366
- const agentView = new View({
367
- name: viewname,
368
- viewtemplate: AGENT_VIEWTEMPLATE_NAME,
369
- configuration: agentCfg,
370
- min_role: 100,
371
- });
372
- return await agentView.run(state, extra);
373
- };
374
-
375
- const ellipsize = (s, nchars) => {
376
- if (!s || !s.length) return "";
377
- if (s.length <= (nchars || 20)) return text_attr(s);
378
- return text_attr(s.substr(0, (nchars || 20) - 3)) + "...";
379
- };
380
-
381
- const actionClasses = [
382
- require("./actions/generate-workflow"),
383
- require("./actions/generate-tables"),
384
- require("./actions/generate-js-action"),
385
- require("./actions/generate-trigger"),
386
- require("./actions/generate-page"),
387
- require("./actions/generate-view"),
388
- ];
389
-
390
- const classesWithSkills = () => {
391
- const state = getState();
392
- const skills = state.copilot_skills || [];
393
- return [...actionClasses, ...skills];
394
- };
395
-
396
- const getCompletionArguments = async () => {
397
- const tools = [];
398
- const sysPrompts = [];
399
- for (const actionClass of classesWithSkills()) {
400
- tools.push({
401
- type: "function",
402
- function: {
403
- name: actionClass.function_name,
404
- description: actionClass.description,
405
- parameters: await actionClass.json_schema(),
406
- },
407
- });
408
- sysPrompts.push(await actionClass.system_prompt());
409
- }
410
- const systemPrompt =
411
- "You are building application components in a database application builder called Saltcorn.\n\n" +
412
- sysPrompts.join("\n\n");
413
- return { tools, systemPrompt };
414
- };
415
-
416
- /*
417
-
418
- build a workflow that asks the user for their name and age
419
-
420
- */
421
-
422
- const executeLegacy = async (table_id, viewname, config, body, { req }) => {
423
- const { fcall_id, run_id } = body;
424
-
425
- const run = await WorkflowRun.findOne({ id: +run_id });
426
-
427
- const fcall = run.context.funcalls[fcall_id];
428
- const actionClass = classesWithSkills().find(
429
- (ac) => ac.function_name === (fcall.name || fcall.toolName)
430
- );
431
- let result;
432
- const args = fcall.arguments ? JSON.parse(fcall.arguments) : fcall.input;
433
-
434
- if (actionClass.follow_on_generate) {
435
- const toolCallIndex = run.context.interactions.findIndex(
436
- (i) =>
437
- i.tool_call_id === fcall_id ||
438
- (Array.isArray(i.content) &&
439
- i.content.some((c) => c.toolCallId === fcall_id))
440
- );
441
- const follow_on_gen = run.context.interactions.find(
442
- (i, ix) => i.role === "assistant" && ix > toolCallIndex
443
- );
444
- result = await actionClass.execute(args, req, follow_on_gen.content);
445
- } else {
446
- result = await actionClass.execute(args, req);
447
- }
448
- await addToContext(run, { implemented_fcall_ids: [fcall_id] });
449
- return { json: { success: "ok", fcall_id, ...(result || {}) } };
450
- };
451
-
452
- const interactLegacy = async (table_id, viewname, config, body, { req }) => {
453
- const { userinput, run_id } = body;
454
- let run;
455
- if (!run_id || run_id === "undefined")
456
- run = await WorkflowRun.create({
457
- status: "Running",
458
- started_by: req.user?.id,
459
- context: {
460
- copilot: "_system",
461
- implemented_fcall_ids: [],
462
- interactions: [{ role: "user", content: userinput }],
463
- funcalls: {},
464
- },
465
- });
466
- else {
467
- run = await WorkflowRun.findOne({ id: +run_id });
468
- await addToContext(run, {
469
- interactions: [{ role: "user", content: userinput }],
470
- });
471
- }
472
- const complArgs = await getCompletionArguments();
473
- complArgs.chat = run.context.interactions;
474
- //console.log(complArgs);
475
-
476
- //build a database for a bicycle rental company
477
- //add a boolean field called "paid" to the payments table
478
-
479
- const answer = await getState().functions.llm_generate.run(
480
- userinput,
481
- complArgs
482
- );
483
- if (answer.ai_sdk)
484
- for (const tool_call of answer.tool_calls)
485
- await addToContext(run, {
486
- interactions: [
487
- ...answer.messages,
488
- {
489
- role: "tool",
490
- content: [
491
- {
492
- type: "tool-result",
493
- toolCallId: tool_call.toolCallId,
494
- toolName: tool_call.toolName,
495
- output: {
496
- type: "text",
497
- value: "Action suggested to user.",
498
- },
499
- },
500
- ],
501
- },
502
- ],
503
- });
504
- else
505
- await addToContext(run, {
506
- interactions:
507
- typeof answer === "object" && answer.tool_calls
508
- ? [
509
- { role: "assistant", tool_calls: answer.tool_calls },
510
- ...answer.tool_calls.map((tc) => ({
511
- role: "tool",
512
- tool_call_id: tc.id || tc.toolCallId,
513
- name: tc.function?.name || tc.toolName,
514
- content: "Action suggested to user.",
515
- })),
516
- ]
517
- : [{ role: "assistant", content: answer }],
518
- });
519
- if (typeof answer === "object" && answer.tool_calls) {
520
- const actions = [];
521
- for (const tool_call of answer.tool_calls) {
522
- await addToContext(run, {
523
- funcalls: {
524
- [tool_call.id || tool_call.toolCallId]:
525
- tool_call.function || tool_call,
526
- },
527
- });
528
-
529
- const followOnGen = await getFollowOnGeneration(tool_call);
530
- if (followOnGen) {
531
- const { response_schema, prompt } = followOnGen;
532
- const follow_on_answer = await getState().functions.llm_generate.run(
533
- prompt,
534
- {
535
- debugResult: true,
536
- chat: run.context.interactions,
537
- response_format: response_schema
538
- ? {
539
- type: "json_schema",
540
- json_schema: {
541
- name: "generate_page",
542
- schema: response_schema,
543
- },
544
- }
545
- : undefined,
546
- }
547
- );
548
-
549
-
550
- await addToContext(run, {
551
- interactions: [
552
- { role: "user", content: prompt },
553
- { role: "assistant", content: follow_on_answer },
554
- ],
555
- });
556
-
557
- const markup = await renderToolcall(
558
- tool_call,
559
- viewname,
560
- false,
561
- run,
562
- follow_on_answer
563
- );
564
-
565
- actions.push(markup);
566
- } else {
567
- const markup = await renderToolcall(tool_call, viewname, false, run);
568
-
569
- actions.push(markup);
570
- }
571
- }
572
- return { json: { success: "ok", actions, run_id: run.id } };
573
- } else
574
- return {
575
- json: { success: "ok", response: md.render(answer), run_id: run.id },
576
- };
577
- };
578
-
579
- const runAgentRoute = async (routeName, table_id, viewname, body, extra) => {
580
- const agentVt = getAgentViewtemplate();
581
- if (!agentVt?.routes?.[routeName]) return null;
582
- const agentCfg = await getAgentChatCfg();
583
- if (!agentCfg) return null;
584
- return await agentVt.routes[routeName](table_id, viewname, agentCfg, body, extra);
585
- };
586
-
587
- const interact = async (table_id, viewname, config, body, extra) => {
588
- const agentResp = await runAgentRoute(
589
- "interact",
590
- table_id,
591
- viewname,
592
- body,
593
- extra,
594
- );
595
- if (agentResp) return agentResp;
596
- return await interactLegacy(table_id, viewname, config, body, extra);
597
- };
598
-
599
- // Legacy route used only by the legacy UI. The Agent Chat UI uses `execute_user_action`.
600
- const execute = async (table_id, viewname, config, body, extra) => {
601
- return await executeLegacy(table_id, viewname, config, body, extra);
602
- };
603
-
604
- const delprevrun = async (table_id, viewname, config, body, extra) => {
605
- const agentResp = await runAgentRoute(
606
- "delprevrun",
607
- table_id,
608
- viewname,
609
- body,
610
- extra,
611
- );
612
- if (agentResp) return agentResp;
613
- return { json: { error: "delprevrun is only available in Agent Chat mode" } };
614
- };
615
-
616
- const debug_info = async (table_id, viewname, config, body, extra) => {
617
- const agentResp = await runAgentRoute(
618
- "debug_info",
619
- table_id,
620
- viewname,
621
- body,
622
- extra,
623
- );
624
- if (agentResp) return agentResp;
625
- return { json: { error: "debug_info is only available in Agent Chat mode" } };
626
- };
627
-
628
- const skillroute = async (table_id, viewname, config, body, extra) => {
629
- const agentResp = await runAgentRoute(
630
- "skillroute",
631
- table_id,
632
- viewname,
633
- body,
634
- extra,
635
- );
636
- if (agentResp) return agentResp;
637
- return { json: { error: "skillroute is only available in Agent Chat mode" } };
638
- };
639
-
640
- const execute_user_action = async (table_id, viewname, config, body, extra) => {
641
- const agentResp = await runAgentRoute(
642
- "execute_user_action",
643
- table_id,
644
- viewname,
645
- body,
646
- extra,
647
- );
648
- if (agentResp) return agentResp;
649
- return {
650
- json: { error: "execute_user_action is only available in Agent Chat mode" },
651
- };
652
- };
653
-
654
- const getFollowOnGeneration = async (tool_call) => {
655
- const fname = tool_call.function?.name || tool_call.toolName;
656
- const actionClass = classesWithSkills().find(
657
- (ac) => ac.function_name === fname
658
- );
659
- const args = tool_call.function?.arguments
660
- ? JSON.parse(tool_call.function?.arguments)
661
- : tool_call.input;
662
-
663
- if (actionClass.follow_on_generate) {
664
- return await actionClass.follow_on_generate(args);
665
- } else return null;
666
- };
667
-
668
- const renderToolcall = async (
669
- tool_call,
670
- viewname,
671
- implemented,
672
- run,
673
- follow_on_answer
674
- ) => {
675
- const fname = tool_call.function?.name || tool_call.toolName;
676
- const actionClass = classesWithSkills().find(
677
- (ac) => ac.function_name === fname
678
- );
679
- const args = tool_call.function?.arguments
680
- ? JSON.parse(tool_call.function.arguments)
681
- : tool_call.input;
682
-
683
- const inner_markup = await actionClass.render_html(args, follow_on_answer);
684
- return wrapAction(
685
- inner_markup,
686
- viewname,
687
- tool_call,
688
- actionClass,
689
- implemented,
690
- run
691
- );
692
- };
693
-
694
- const wrapAction = (
695
- inner_markup,
696
- viewname,
697
- tool_call,
698
- actionClass,
699
- implemented,
700
- run
701
- ) =>
702
- span({ class: "badge bg-info ms-1" }, actionClass.title) +
703
- div(
704
- { class: "card mb-3 bg-secondary-subtle" },
705
- div(
706
- { class: "card-body" },
707
- inner_markup,
708
- implemented
709
- ? button(
710
- {
711
- type: "button",
712
- class: "btn btn-secondary d-block mt-3 float-end",
713
- disabled: true,
714
- },
715
- i({ class: "fas fa-check me-1" }),
716
- "Applied"
717
- )
718
- : button(
719
- {
720
- type: "button",
721
- id: "exec-" + tool_call.id,
722
- class: "btn btn-primary d-block mt-3 float-end",
723
- onclick: `press_store_button(this, true);view_post('${viewname}', 'execute', {fcall_id: '${
724
- tool_call.id || tool_call.toolCallId
725
- }', run_id: ${run.id}}, processExecuteResponse)`,
726
- },
727
- "Apply"
728
- ),
729
- div({ id: "postexec-" + tool_call.id })
730
- )
731
- );
732
-
733
- const addToContext = async (run, newCtx) => {
734
- if (run.addToContext) return await run.addToContext(newCtx);
735
- let changed = true;
736
- Object.keys(newCtx).forEach((k) => {
737
- if (Array.isArray(run.context[k])) {
738
- if (!Array.isArray(newCtx[k]))
739
- throw new Error("Must be array to append to array");
740
- run.context[k].push(...newCtx[k]);
741
- changed = true;
742
- } else if (typeof run.context[k] === "object") {
743
- if (typeof newCtx[k] !== "object")
744
- throw new Error("Must be object to append to object");
745
- Object.assign(run.context[k], newCtx[k]);
746
- changed = true;
747
- } else {
748
- run.context[k] = newCtx[k];
749
- changed = true;
750
- }
751
- });
752
- if (changed) await run.update({ context: run.context });
753
- };
754
-
755
- module.exports = {
756
- name: "Saltcorn Copilot",
757
- display_state_form: false,
758
- get_state_fields,
759
- tableless: true,
760
- singleton: true,
761
- run,
762
- routes: {
763
- interact,
764
- execute,
765
- delprevrun,
766
- debug_info,
767
- skillroute,
768
- execute_user_action,
769
- },
770
- };