@saltcorn/agents 0.7.6 → 0.7.8

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/action.js CHANGED
@@ -92,29 +92,32 @@ module.exports = {
92
92
  is_sub_agent,
93
93
  agent_view_config,
94
94
  dyn_updates,
95
+ agent_label,
95
96
  ...rest
96
97
  }) => {
97
98
  const userinput = interpolate(configuration.prompt, row, user);
98
99
 
99
- const run = run_id
100
- ? await WorkflowRun.findOne({ id: run_id })
101
- : await WorkflowRun.create({
102
- status: "Running",
103
- started_by: user?.id,
104
- trigger_id: trigger_id || undefined,
105
- context: {
106
- implemented_fcall_ids: [],
107
- interactions: [{ role: "user", content: userinput }],
108
- funcalls: {},
109
- },
110
- });
111
- if (run_id)
112
- run.context.interactions.push({ role: "user", content: userinput });
100
+ const run =
101
+ rest.run ||
102
+ (run_id
103
+ ? await WorkflowRun.findOne({ id: run_id })
104
+ : await WorkflowRun.create({
105
+ status: "Running",
106
+ started_by: user?.id,
107
+ trigger_id: trigger_id || undefined,
108
+ context: {
109
+ implemented_fcall_ids: [],
110
+ interactions: [],
111
+ funcalls: {},
112
+ },
113
+ }));
114
+
115
+ run.context.interactions.push({ role: "user", content: userinput });
113
116
  return await process_interaction(
114
117
  run,
115
118
  configuration,
116
119
  req,
117
- undefined,
120
+ agent_label || undefined,
118
121
  [],
119
122
  row,
120
123
  agent_view_config || { stream: false },
package/agent-view.js CHANGED
@@ -18,6 +18,7 @@ const {
18
18
  input,
19
19
  h4,
20
20
  h3,
21
+ h2,
21
22
  style,
22
23
  h5,
23
24
  button,
@@ -821,6 +822,21 @@ const run = async (
821
822
  .modern-chat-layout .chat-user .chat-bubble table th {
822
823
  background: rgba(255,255,255,0.1);
823
824
  }
825
+ /* Skill attribution badge */
826
+ .modern-chat-layout .chat-bubble .badge.bg-info {
827
+ display: inline-block;
828
+ margin-bottom: 6px;
829
+ font-size: 0.7rem;
830
+ font-weight: 600;
831
+ letter-spacing: 0.3px;
832
+ text-transform: uppercase;
833
+ opacity: 0.85;
834
+ }
835
+ .modern-chat-layout .chat-bubble .card.bg-secondary-subtle {
836
+ border: none;
837
+ background-color: rgba(0,0,0,0.03) !important;
838
+ margin-bottom: 0.5rem;
839
+ }
824
840
  /* Input area for modern chat */
825
841
  .modern-chat-layout .copilot-entry {
826
842
  border-top: 1px solid var(--tblr-border-color, var(--bs-border-color, #dee2e6));
@@ -1273,12 +1289,80 @@ const debug_info = async (table_id, viewname, config, body, { req, res }) => {
1273
1289
  );
1274
1290
  sysPrompt = complArgs.systemPrompt;
1275
1291
  }
1292
+ const apiJson = JSON.stringify(run.context.api_interactions, null, 2);
1276
1293
  const debug_html = div(
1277
- div(h4("System prompt"), pre(text(escapeHtml(sysPrompt)))),
1294
+ { class: "accordion", id: "debugAccordion" },
1278
1295
  div(
1279
- h4("API interactions"),
1280
- pre(
1281
- text(escapeHtml(JSON.stringify(run.context.api_interactions, null, 2))),
1296
+ { class: "accordion-item" },
1297
+ h2(
1298
+ { class: "accordion-header", id: "debugHeadPrompt" },
1299
+ button(
1300
+ {
1301
+ class: "accordion-button collapsed",
1302
+ type: "button",
1303
+ "data-bs-toggle": "collapse",
1304
+ "data-bs-target": "#debugCollapsePrompt",
1305
+ "aria-expanded": "false",
1306
+ "aria-controls": "debugCollapsePrompt",
1307
+ },
1308
+ "System prompt",
1309
+ ),
1310
+ ),
1311
+ div(
1312
+ {
1313
+ id: "debugCollapsePrompt",
1314
+ class: "accordion-collapse collapse",
1315
+ "aria-labelledby": "debugHeadPrompt",
1316
+ "data-bs-parent": "#debugAccordion",
1317
+ },
1318
+ div(
1319
+ { class: "accordion-body" },
1320
+ pre({ style: "white-space:pre-wrap" }, text(escapeHtml(sysPrompt))),
1321
+ ),
1322
+ ),
1323
+ ),
1324
+ div(
1325
+ { class: "accordion-item" },
1326
+ h2(
1327
+ { class: "accordion-header", id: "debugHeadAPI" },
1328
+ button(
1329
+ {
1330
+ class: "accordion-button",
1331
+ type: "button",
1332
+ "data-bs-toggle": "collapse",
1333
+ "data-bs-target": "#debugCollapseAPI",
1334
+ "aria-expanded": "true",
1335
+ "aria-controls": "debugCollapseAPI",
1336
+ },
1337
+ "API interactions",
1338
+ ),
1339
+ ),
1340
+ div(
1341
+ {
1342
+ id: "debugCollapseAPI",
1343
+ class: "accordion-collapse collapse show",
1344
+ "aria-labelledby": "debugHeadAPI",
1345
+ "data-bs-parent": "#debugAccordion",
1346
+ },
1347
+ div(
1348
+ { class: "accordion-body" },
1349
+ button(
1350
+ {
1351
+ class: "btn btn-sm btn-outline-secondary mb-2",
1352
+ onclick: `
1353
+ var t=document.getElementById('debugApiPre').textContent;
1354
+ navigator.clipboard.writeText(t).then(function(){
1355
+ var b=event.target;b.textContent='Copied!';
1356
+ setTimeout(function(){b.textContent='Copy to clipboard'},1500)
1357
+ })`,
1358
+ },
1359
+ "Copy to clipboard",
1360
+ ),
1361
+ pre(
1362
+ { id: "debugApiPre", style: "white-space:pre-wrap" },
1363
+ text(escapeHtml(apiJson)),
1364
+ ),
1365
+ ),
1282
1366
  ),
1283
1367
  ),
1284
1368
  );
package/common.js CHANGED
@@ -234,7 +234,7 @@ const process_interaction = async (
234
234
  run,
235
235
  config,
236
236
  req,
237
- agent_label = "Copilot",
237
+ agent_label = "Agent",
238
238
  prevResponses = [],
239
239
  triggering_row = {},
240
240
  agentsViewCfg = { stream: false },
@@ -391,6 +391,9 @@ const process_interaction = async (
391
391
  },
392
392
  );
393
393
  }
394
+ const response_label = is_sub_agent
395
+ ? agent_label
396
+ : tool.skill.skill_label || tool.skill.constructor.skill_name;
394
397
  if (tool.tool.renderToolCall) {
395
398
  const row = tool_call.input;
396
399
 
@@ -400,10 +403,7 @@ const process_interaction = async (
400
403
  if (rendered)
401
404
  add_response(
402
405
  wrapSegment(
403
- wrapCard(
404
- tool.skill.skill_label || tool.skill.constructor.skill_name,
405
- rendered,
406
- ),
406
+ wrapCard(response_label, rendered),
407
407
  agent_label,
408
408
  false,
409
409
  layout,
@@ -427,11 +427,7 @@ const process_interaction = async (
427
427
  if (rendered)
428
428
  add_response(
429
429
  wrapSegment(
430
- wrapCard(
431
- tool.skill.skill_label ||
432
- tool.skill.constructor.skill_name,
433
- rendered,
434
- ),
430
+ wrapCard(response_label, rendered),
435
431
  agent_label,
436
432
  false,
437
433
  layout,
@@ -473,8 +469,10 @@ const process_interaction = async (
473
469
  let stop = false,
474
470
  myHasResult = false;
475
471
  if (tool.tool.postProcess && !stop) {
476
- let result = toolResults[tool_call.tool_call_id];
477
-
472
+ let result = toolResults[tool_call.tool_call_id];
473
+ const response_label = is_sub_agent
474
+ ? agent_label
475
+ : tool.skill.skill_label || tool.skill.constructor.skill_name;
478
476
  const chat = run.context.interactions;
479
477
  let generateUsed = false;
480
478
  const systemPrompt = await getSystemPrompt(
@@ -515,7 +513,7 @@ const process_interaction = async (
515
513
  });
516
514
  if (generateUsed)
517
515
  await addToContext(run, {
518
- interactions: chat,
516
+ interactions: run.context.interactions,
519
517
  });
520
518
  if (postprocres.stop) stop = true;
521
519
  if (postprocres.add_system_prompt)
@@ -532,49 +530,76 @@ const process_interaction = async (
532
530
  }
533
531
 
534
532
  for (const add_resp of postprocres.add_responses || []) {
535
- raw_responses.push(add_resp);
536
- const renderedAddResponse =
537
- typeof add_resp === "string" ? md.render(add_resp) : add_resp;
538
- add_response(
539
- wrapSegment(
540
- wrapCard(
541
- tool.skill.skill_label || tool.skill.constructor.skill_name,
542
- renderedAddResponse,
533
+ const content =
534
+ add_resp.role && add_resp.content ? add_resp.content : add_resp;
535
+ raw_responses.push(content);
536
+ if (add_resp.md_response !== null) {
537
+ const renderedAddResponse = add_resp.md_response
538
+ ? md.render(add_resp.md_response)
539
+ : typeof content === "string"
540
+ ? md.render(content)
541
+ : content;
542
+ add_response(
543
+ wrapSegment(
544
+ wrapCard(response_label, renderedAddResponse),
545
+ agent_label,
546
+ false,
547
+ layout,
543
548
  ),
544
- agent_label,
545
- false,
546
- layout,
547
- ),
548
- );
549
- //replace tool response with this
550
- // run.context.interactions.forEach((ic) => {});
551
- const result = add_resp;
552
- await sysState.functions.llm_add_message.run(
553
- "assistant",
554
- !result || typeof result === "string"
555
- ? {
556
- type: "text",
557
- value: result || "Action run",
558
- }
559
- : {
560
- type: "json",
561
- value: JSON.parse(JSON.stringify(result)),
562
- },
563
- {
564
- chat: run.context.interactions,
565
- },
566
- );
567
- if (!postprocres.stop)
549
+ );
550
+ }
551
+ if (typeof add_resp.md_response !== "undefined")
552
+ delete add_resp.md_response;
553
+
554
+ const result = content;
555
+
556
+ if (add_resp.role && add_resp.content) {
568
557
  await sysState.functions.llm_add_message.run(
569
- "user",
558
+ add_resp.role,
559
+ add_resp.content,
570
560
  {
571
- type: "text",
572
- value: "Continue",
561
+ chat: run.context.interactions,
573
562
  },
563
+ );
564
+ } else
565
+ await sysState.functions.llm_add_message.run(
566
+ "assistant",
567
+
568
+ !result || typeof result === "string"
569
+ ? result || "Action run"
570
+ : JSON.stringify(result),
571
+
574
572
  {
575
573
  chat: run.context.interactions,
576
574
  },
577
575
  );
576
+
577
+ await addToContext(run, {
578
+ interactions: run.context.interactions,
579
+ });
580
+ }
581
+ if (!postprocres.stop) {
582
+ const lastInteract =
583
+ run.context.interactions[run.context.interactions.length - 1];
584
+
585
+ if (
586
+ postprocres.follow_up_prompt ||
587
+ !(
588
+ lastInteract?.role === "user" || lastInteract?.role === "tool"
589
+ )
590
+ ) {
591
+ await sysState.functions.llm_add_message.run(
592
+ "user",
593
+ postprocres.follow_up_prompt || "Continue with the query",
594
+ {
595
+ chat: run.context.interactions,
596
+ },
597
+ );
598
+ await addToContext(run, {
599
+ interactions: run.context.interactions,
600
+ });
601
+ }
602
+ myHasResult = true;
578
603
  }
579
604
  if (postprocres.add_user_action && viewname) {
580
605
  const user_actions = Array.isArray()
@@ -624,7 +649,7 @@ const process_interaction = async (
624
649
  ? answer
625
650
  : wrapSegment(md.render(answer), agent_label, false, layout),
626
651
  );
627
- if (dyn_updates)
652
+ if (dyn_updates && !is_sub_agent)
628
653
  getState().emitDynamicUpdate(
629
654
  db.getTenantSchema(),
630
655
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saltcorn/agents",
3
- "version": "0.7.6",
3
+ "version": "0.7.8",
4
4
  "description": "AI agents for Saltcorn",
5
5
  "main": "index.js",
6
6
  "dependencies": {
@@ -18,6 +18,24 @@ const { validID } = require("@saltcorn/markup/layout_utils");
18
18
  const vm = require("vm");
19
19
  const { replaceUserContinue } = require("../common");
20
20
 
21
+ function extractCode(str) {
22
+ // Try ```javascript fence
23
+ if (str.includes("```javascript")) {
24
+ return str.split("```javascript")[1].split("```")[0];
25
+ }
26
+ // Try ```js fence
27
+ if (str.includes("```js\n") || str.includes("```js\r")) {
28
+ return str.split(/```js\s/)[1].split("```")[0];
29
+ }
30
+ // Try generic ``` fence (code is between first and second ```)
31
+ const fenceMatch = str.match(/```\s*\n([\s\S]*?)```/);
32
+ if (fenceMatch) {
33
+ return fenceMatch[1];
34
+ }
35
+ // No fences found - return raw string
36
+ return str;
37
+ }
38
+
21
39
  //const { fieldProperties } = require("./helpers");
22
40
 
23
41
  class GenerateAndRunJsCodeSkill {
@@ -105,6 +123,14 @@ class GenerateAndRunJsCodeSkill {
105
123
  sublabel: "Allow calls to functions from codepages and modules",
106
124
  type: "Bool",
107
125
  },
126
+ {
127
+ name: "follow_up_prompt",
128
+ label: "Follow-up prompt",
129
+ sublabel:
130
+ "If set, the agent will continue processing after code execution with this prompt. Leave empty to stop after code result.",
131
+ type: "String",
132
+ fieldview: "textarea",
133
+ },
108
134
  ...(Table.subClass
109
135
  ? [
110
136
  {
@@ -171,26 +197,53 @@ return { x, y }
171
197
 
172
198
  ${extra || ""}
173
199
 
200
+ CRITICAL: Your response must contain ONLY a single JavaScript code block wrapped in \`\`\`javascript ... \`\`\` fences. Do not include any text, explanation, or commentary before or after the code block.
201
+
174
202
  Now generate the JavaScript code required by the user.`,
175
203
  );
176
204
  getState().log(
177
205
  6,
178
206
  "Generated code:\n--BEGIN CODE--\n" + str + "\n--END CODE--\n",
179
207
  );
180
- const js_code = str.includes("```javascript")
181
- ? str.split("```javascript")[1].split("```")[0]
182
- : str;
208
+ const js_code = extractCode(str);
183
209
  return js_code;
184
210
  };
185
211
  const js_code = await gen_the_code();
186
212
  emit_update("Running code");
213
+ const ensureResult = (res) => {
214
+ if (res && typeof res === "object") return JSON.stringify(res);
215
+ if (res !== undefined && res !== null && res !== "") return res;
216
+ return "Code executed successfully but returned no output.";
217
+ };
218
+ const mkMdResponse = (result, code) =>
219
+ req?.user?.role_id === 1
220
+ ? `<details>
221
+
222
+ <summary>Show code</summary>
223
+
224
+ \`\`\`javascript
225
+ ${code}
226
+ \`\`\`
227
+
228
+
229
+ </details>
230
+
231
+ ${result}`
232
+ : result;
187
233
  try {
188
234
  const res = await this.runCode(js_code, { user: req.user });
189
- //console.log("code response", res);
190
235
  getState().log(6, "Code answer: " + JSON.stringify(res));
236
+ const effectiveRes = ensureResult(res);
191
237
  return {
192
- stop: typeof res === "string",
193
- add_response: res,
238
+ stop: typeof res === "string" && !this.follow_up_prompt,
239
+ add_response: {
240
+ role: "user",
241
+ content: `The result of running the code is: ${effectiveRes}`,
242
+ md_response: mkMdResponse(effectiveRes, js_code),
243
+ },
244
+ ...(this.follow_up_prompt
245
+ ? { follow_up_prompt: this.follow_up_prompt }
246
+ : {}),
194
247
  };
195
248
  } catch (err) {
196
249
  console.error(err);
@@ -206,15 +259,33 @@ this code produced the following error:
206
259
  ${err.message}
207
260
  \`\`\`
208
261
 
209
- Correct this error.
262
+ Correct this error and generate the new Javascript code to run
210
263
  `);
211
- const res = await this.runCode(retry_js_code, { user: req.user });
212
- //console.log("code response", res);
213
- getState().log(6, "Code retry answer: " + JSON.stringify(res));
214
- return {
215
- stop: typeof res === "string",
216
- add_response: res,
217
- };
264
+ try {
265
+ const res = await this.runCode(retry_js_code, {
266
+ user: req.user,
267
+ });
268
+ getState().log(6, "Code retry answer: " + JSON.stringify(res));
269
+ const effectiveRes = ensureResult(res);
270
+ return {
271
+ stop: typeof res === "string" && !this.follow_up_prompt,
272
+ add_response: {
273
+ role: "user",
274
+ content: `The result of running the code is: ${effectiveRes}`,
275
+ md_response: mkMdResponse(effectiveRes, retry_js_code),
276
+ },
277
+ ...(this.follow_up_prompt
278
+ ? { follow_up_prompt: this.follow_up_prompt }
279
+ : {}),
280
+ };
281
+ } catch (retryErr) {
282
+ console.error(retryErr);
283
+ return {
284
+ add_response:
285
+ "Error: code generation failed after retry: " +
286
+ retryErr.message,
287
+ };
288
+ }
218
289
  }
219
290
  },
220
291
  function: {
@@ -52,6 +52,12 @@ class SubagentToSkill {
52
52
  sublabel: `Optional. The prompt initialising the subagent. Example: "Continue answering my query using the tool now at you disposal"`,
53
53
  type: "String",
54
54
  },
55
+ {
56
+ name: "handoff_prompt",
57
+ label: "Handoff prompt",
58
+ sublabel: `Optional. A prompt to process the results of the subagent. Example: "Analyze this response in relation to my query"`,
59
+ type: "String",
60
+ },
55
61
  ];
56
62
  }
57
63
 
@@ -91,21 +97,21 @@ class SubagentToSkill {
91
97
  "Your instructions and tools have changed. Continue answering my query using the instructions and tools at you disposal, if any",
92
98
  },
93
99
  user: req.user,
94
- run_id: run.id,
100
+ run,
95
101
  is_sub_agent: true,
96
102
  agent_view_config,
97
103
  dyn_updates,
98
104
  req,
105
+ agent_label: this.agent_name,
99
106
  });
100
- getState().log(
101
- 6,
102
- "Subagent response",
103
- subres?.json?.raw_responses || "No response",
104
- );
105
-
106
- if (subres.json.raw_responses)
107
- return { add_responses: subres.json.raw_responses };
107
+ getState().log(6, "Subagent response", JSON.stringify(subres, null, 2));
108
+ //if (subres.json.raw_responses)
109
+ // return { add_responses: subres.json.raw_responses };
108
110
  return {
111
+ ...(this.handoff_prompt
112
+ ? { follow_up_prompt: this.handoff_prompt }
113
+ : { stop: true }),
114
+
109
115
  //stop: true,
110
116
  //add_response: result,
111
117
  };
@@ -38,7 +38,7 @@ beforeAll(async () => {
38
38
  });
39
39
 
40
40
  await getState().refresh_triggers(false);
41
- //await getState().setConfig("log_level", 6);
41
+ await getState().setConfig("log_level", 6);
42
42
  });
43
43
 
44
44
  jest.setTimeout(40000);
@@ -46,6 +46,13 @@ jest.setTimeout(40000);
46
46
  const user = { id: 1, role_id: 1 };
47
47
  const action = require("../action");
48
48
 
49
+ const getLastInteraction = async ({ run_id }) => {
50
+ const run = await WorkflowRuns.findOne({ id: run_id });
51
+ return JSON.stringify(
52
+ run.context.interactions[run.context.interactions.length - 1],
53
+ );
54
+ };
55
+
49
56
  for (const nameconfig of require("./configs")) {
50
57
  const { name, ...config } = nameconfig;
51
58
  describe("agent action with " + name, () => {
@@ -114,7 +121,10 @@ for (const nameconfig of require("./configs")) {
114
121
  user,
115
122
  req: { user },
116
123
  });
117
- expect(result.json.response).toContain("987");
124
+
125
+ const lastInteraction = await getLastInteraction(result.json);
126
+
127
+ expect(result.json.response || lastInteraction).toContain("987");
118
128
  });
119
129
  it("run multiple subagents concurrenty", async () => {
120
130
  const configuration = { ...require("./agentcfg").agent1 };
@@ -134,7 +144,8 @@ for (const nameconfig of require("./configs")) {
134
144
  user,
135
145
  req: { user },
136
146
  });
137
- expect(result.json.response).toContain("987");
147
+ const lastInteraction = await getLastInteraction(result.json);
148
+ expect(lastInteraction).toContain("987");
138
149
  });
139
150
  });
140
151