@saltcorn/agents 0.7.6 → 0.7.7

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
@@ -96,20 +96,22 @@ module.exports = {
96
96
  }) => {
97
97
  const userinput = interpolate(configuration.prompt, row, user);
98
98
 
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 });
99
+ const run =
100
+ rest.run ||
101
+ (run_id
102
+ ? await WorkflowRun.findOne({ id: run_id })
103
+ : await WorkflowRun.create({
104
+ status: "Running",
105
+ started_by: user?.id,
106
+ trigger_id: trigger_id || undefined,
107
+ context: {
108
+ implemented_fcall_ids: [],
109
+ interactions: [],
110
+ funcalls: {},
111
+ },
112
+ }));
113
+
114
+ run.context.interactions.push({ role: "user", content: userinput });
113
115
  return await process_interaction(
114
116
  run,
115
117
  configuration,
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
@@ -515,7 +515,7 @@ const process_interaction = async (
515
515
  });
516
516
  if (generateUsed)
517
517
  await addToContext(run, {
518
- interactions: chat,
518
+ interactions: run.context.interactions,
519
519
  });
520
520
  if (postprocres.stop) stop = true;
521
521
  if (postprocres.add_system_prompt)
@@ -546,35 +546,35 @@ const process_interaction = async (
546
546
  layout,
547
547
  ),
548
548
  );
549
- //replace tool response with this
550
- // run.context.interactions.forEach((ic) => {});
549
+
551
550
  const result = add_resp;
552
551
  await sysState.functions.llm_add_message.run(
553
552
  "assistant",
553
+
554
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
- },
555
+ ? result || "Action run"
556
+ : JSON.stringify(result),
557
+
563
558
  {
564
559
  chat: run.context.interactions,
565
560
  },
566
561
  );
567
- if (!postprocres.stop)
568
- await sysState.functions.llm_add_message.run(
569
- "user",
570
- {
571
- type: "text",
572
- value: "Continue",
573
- },
574
- {
575
- chat: run.context.interactions,
576
- },
577
- );
562
+ await addToContext(run, {
563
+ interactions: run.context.interactions,
564
+ });
565
+ }
566
+ if (!postprocres.stop) {
567
+ await sysState.functions.llm_add_message.run(
568
+ "user",
569
+ postprocres.follow_up_prompt || "Continue with the query",
570
+ {
571
+ chat: run.context.interactions,
572
+ },
573
+ );
574
+ await addToContext(run, {
575
+ interactions: run.context.interactions,
576
+ });
577
+ myHasResult = true;
578
578
  }
579
579
  if (postprocres.add_user_action && viewname) {
580
580
  const user_actions = Array.isArray()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saltcorn/agents",
3
- "version": "0.7.6",
3
+ "version": "0.7.7",
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,34 @@ 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
+ };
187
218
  try {
188
219
  const res = await this.runCode(js_code, { user: req.user });
189
- //console.log("code response", res);
190
220
  getState().log(6, "Code answer: " + JSON.stringify(res));
221
+ const effectiveRes = ensureResult(res);
191
222
  return {
192
- stop: typeof res === "string",
193
- add_response: res,
223
+ stop: typeof res === "string" && !this.follow_up_prompt,
224
+ add_response: effectiveRes,
225
+ ...(this.follow_up_prompt
226
+ ? { follow_up_prompt: this.follow_up_prompt }
227
+ : {}),
194
228
  };
195
229
  } catch (err) {
196
230
  console.error(err);
@@ -208,13 +242,27 @@ ${err.message}
208
242
 
209
243
  Correct this error.
210
244
  `);
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
- };
245
+ try {
246
+ const res = await this.runCode(retry_js_code, {
247
+ user: req.user,
248
+ });
249
+ getState().log(6, "Code retry answer: " + JSON.stringify(res));
250
+ const effectiveRes = ensureResult(res);
251
+ return {
252
+ stop: typeof res === "string" && !this.follow_up_prompt,
253
+ add_response: effectiveRes,
254
+ ...(this.follow_up_prompt
255
+ ? { follow_up_prompt: this.follow_up_prompt }
256
+ : {}),
257
+ };
258
+ } catch (retryErr) {
259
+ console.error(retryErr);
260
+ return {
261
+ add_response:
262
+ "Error: code generation failed after retry: " +
263
+ retryErr.message,
264
+ };
265
+ }
218
266
  }
219
267
  },
220
268
  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,20 @@ 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,
99
105
  });
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 };
106
+ getState().log(6, "Subagent response", JSON.stringify(subres, null, 2));
107
+ //if (subres.json.raw_responses)
108
+ // return { add_responses: subres.json.raw_responses };
108
109
  return {
110
+ ...(this.handoff_prompt
111
+ ? { follow_up_prompt: this.handoff_prompt }
112
+ : { stop: true }),
113
+
109
114
  //stop: true,
110
115
  //add_response: result,
111
116
  };
@@ -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