@saltcorn/agents 0.7.2 → 0.7.4

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
@@ -89,6 +89,7 @@ module.exports = {
89
89
  trigger_id,
90
90
  run_id,
91
91
  req,
92
+ is_sub_agent,
92
93
  ...rest
93
94
  }) => {
94
95
  const userinput = interpolate(configuration.prompt, row, user);
@@ -114,6 +115,9 @@ module.exports = {
114
115
  undefined,
115
116
  [],
116
117
  row,
118
+ { stream: false },
119
+ false,
120
+ is_sub_agent
117
121
  );
118
122
  },
119
123
  };
package/common.js CHANGED
@@ -34,9 +34,11 @@ const get_skills = () => {
34
34
  require("./skills/GenerateImage"),
35
35
  require("./skills/ModelContextProtocol"),
36
36
  require("./skills/PromptPicker"),
37
+ require("./skills/ModelPicker"),
37
38
  require("./skills/RunJsCode"),
38
39
  require("./skills/GenerateAndRunJsCode"),
39
40
  require("./skills/Fetch"),
41
+ require("./skills/WebSearch"),
40
42
  require("./skills/Subagent"),
41
43
  //require("./skills/AdaptiveFeedback"),
42
44
  ...exchange_skills,
@@ -127,12 +129,21 @@ const getCompletionArguments = async (
127
129
  ];
128
130
 
129
131
  const skills = get_skill_instances(config);
132
+ const overrides = {};
130
133
  for (const skill of skills) {
131
134
  const sysPr = await skill.systemPrompt?.({
132
135
  ...(formbody || {}),
133
136
  user,
134
137
  triggering_row,
135
138
  });
139
+ const overide =
140
+ (await skill.settingsOverride?.({
141
+ ...(formbody || {}),
142
+ user,
143
+ triggering_row,
144
+ })) || {};
145
+ Object.assign(overrides, overide);
146
+
136
147
  if (sysPr) sysPrompts.push(sysPr);
137
148
  const skillTools = skill.provideTools?.();
138
149
  if (skillTools && Array.isArray(skillTools)) tools.push(...skillTools);
@@ -141,7 +152,9 @@ const getCompletionArguments = async (
141
152
  if (tools.length === 0) tools = undefined;
142
153
  const complArgs = { tools, systemPrompt: sysPrompts.join("\n\n") };
143
154
  if (config.model) complArgs.model = config.model;
144
- if (config.alt_config) complArgs.alt_config = config.alt_config;
155
+
156
+ if (overrides.alt_config || config.alt_config)
157
+ complArgs.alt_config = overrides.alt_config || config.alt_config;
145
158
  return complArgs;
146
159
  };
147
160
 
@@ -226,6 +239,7 @@ const process_interaction = async (
226
239
  triggering_row = {},
227
240
  agentsViewCfg = { stream: false },
228
241
  dyn_updates = false,
242
+ is_sub_agent = false,
229
243
  ) => {
230
244
  const { stream, viewname, layout } = agentsViewCfg;
231
245
  const sysState = getState();
@@ -237,6 +251,7 @@ const process_interaction = async (
237
251
  );
238
252
  complArgs.appendToChat = true;
239
253
  complArgs.chat = run.context.interactions;
254
+ const use_alt_config = complArgs.alt_config;
240
255
  //complArgs.debugResult = true;
241
256
  //console.log("complArgs", JSON.stringify(complArgs, null, 2));
242
257
  const debugMode = is_debug_mode(config, req.user);
@@ -276,6 +291,7 @@ const process_interaction = async (
276
291
  interactions: complArgs.chat,
277
292
  });
278
293
  const responses = [];
294
+ const raw_responses = [];
279
295
 
280
296
  const add_response = async (resp, not_final) => {
281
297
  if (dyn_updates)
@@ -344,6 +360,7 @@ const process_interaction = async (
344
360
  //const actions = [];
345
361
  let hasResult = false;
346
362
  if ((answer.mcp_calls || []).length && !answer.content) hasResult = true;
363
+ const toolResults = {};
347
364
  if (answer.hasToolCalls)
348
365
  for (const tool_call of answer.getToolCalls()) {
349
366
  getState().log(6, "call function " + tool_call.tool_name);
@@ -395,6 +412,7 @@ const process_interaction = async (
395
412
  const result = await tool.tool.process(tool_call.input, {
396
413
  req,
397
414
  });
415
+ toolResults[tool_call.tool_call_id] = result;
398
416
  if (result.stop) stop = true;
399
417
  if (
400
418
  (typeof result === "object" && Object.keys(result || {}).length) ||
@@ -440,7 +458,21 @@ const process_interaction = async (
440
458
  await addToContext(run, {
441
459
  interactions: run.context.interactions,
442
460
  });
461
+
462
+ if (myHasResult && !stop && !tool.tool.postProcess) hasResult = true;
463
+ }
464
+ }
465
+
466
+ // postprocess - now all tool calls have responses
467
+ if (answer.hasToolCalls)
468
+ for (const tool_call of answer.getToolCalls()) {
469
+ const tool = find_tool(tool_call.tool_name, config);
470
+ if (tool) {
471
+ let stop = false,
472
+ myHasResult = false;
443
473
  if (tool.tool.postProcess && !stop) {
474
+ let result = toolResults[tool_call.tool_call_id];
475
+
444
476
  const chat = run.context.interactions;
445
477
  let generateUsed = false;
446
478
  const systemPrompt = await getSystemPrompt(
@@ -461,7 +493,7 @@ const process_interaction = async (
461
493
  chat,
462
494
  appendToChat: true,
463
495
  systemPrompt,
464
- alt_config: config.alt_config,
496
+ alt_config: use_alt_config,
465
497
  ...opts,
466
498
  });
467
499
  },
@@ -490,9 +522,17 @@ const process_interaction = async (
490
522
  ],
491
523
  });
492
524
  if (postprocres.add_response) {
493
- const renderedAddResponse = typeof postprocres.add_response === "string"
494
- ? md.render(postprocres.add_response)
495
- : postprocres.add_response;
525
+ if (!postprocres.add_responses)
526
+ postprocres.add_responses = [postprocres.add_response];
527
+ else postprocres.add_responses.push(postprocres.add_response);
528
+ }
529
+
530
+ for (const add_resp of postprocres.add_responses || []) {
531
+ raw_responses.push(add_resp);
532
+ const renderedAddResponse =
533
+ typeof add_resp === "string"
534
+ ? md.render(add_resp)
535
+ : add_resp;
496
536
  add_response(
497
537
  wrapSegment(
498
538
  wrapCard(
@@ -506,7 +546,7 @@ const process_interaction = async (
506
546
  );
507
547
  //replace tool response with this
508
548
  // run.context.interactions.forEach((ic) => {});
509
- const result = postprocres.add_response;
549
+ const result = add_resp;
510
550
  await sysState.functions.llm_add_message.run(
511
551
  "assistant",
512
552
  !result || typeof result === "string"
@@ -574,6 +614,7 @@ const process_interaction = async (
574
614
  triggering_row,
575
615
  agentsViewCfg,
576
616
  dyn_updates,
617
+ is_sub_agent,
577
618
  );
578
619
  } else if (typeof answer === "string")
579
620
  add_response(
@@ -594,6 +635,7 @@ const process_interaction = async (
594
635
  return {
595
636
  json: {
596
637
  success: "ok",
638
+ ...(is_sub_agent ? { raw_responses } : {}),
597
639
  response: [...prevResponses, ...responses].join(""),
598
640
  run_id: run?.id,
599
641
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saltcorn/agents",
3
- "version": "0.7.2",
3
+ "version": "0.7.4",
4
4
  "description": "AI agents for Saltcorn",
5
5
  "main": "index.js",
6
6
  "dependencies": {
@@ -0,0 +1,47 @@
1
+ const Workflow = require("@saltcorn/data/models/workflow");
2
+ const Form = require("@saltcorn/data/models/form");
3
+ const FieldRepeat = require("@saltcorn/data/models/fieldrepeat");
4
+ const { select, option } = require("@saltcorn/markup/tags");
5
+ const { eval_expression } = require("@saltcorn/data/models/expression");
6
+ const { validID } = require("@saltcorn/markup/layout_utils");
7
+ const { getState } = require("@saltcorn/data/db/state");
8
+
9
+ class ModelPicker {
10
+ static skill_name = "Model picker";
11
+ get skill_label() {
12
+ return `Model picker`;
13
+ }
14
+ constructor(cfg) {
15
+ Object.assign(this, cfg);
16
+ }
17
+ static async configFields() {
18
+ return [
19
+ { name: "placeholder", label: "Placeholder", type: "String" },
20
+ {
21
+ name: "default_label",
22
+ label: "Default configuration label",
23
+ type: "String",
24
+ },
25
+ ];
26
+ }
27
+ async formWidget({ user, klass }) {
28
+ const llm_cfg_fun = getState().functions.llm_get_configuration;
29
+ const alt_config_options = llm_cfg_fun
30
+ ? llm_cfg_fun.run().alt_config_names || []
31
+ : [];
32
+ return select(
33
+ {
34
+ class: ["form-select form-select-sm w-unset", klass],
35
+ name: "modelpicker",
36
+ },
37
+ this.placeholder && option({ disabled: true }, this.placeholder),
38
+ option({ value: "" }, this.default_label),
39
+ alt_config_options.map((o) => option(o)),
40
+ );
41
+ }
42
+ settingsOverride(body) {
43
+ if (body.modelpicker) return { alt_config: body.modelpicker };
44
+ }
45
+ }
46
+
47
+ module.exports = ModelPicker;
@@ -66,13 +66,16 @@ class SubagentToSkill {
66
66
  return div({ class: "border border-primary p-2 m-2" }, phrase);
67
67
  },*/
68
68
  postProcess: async ({ tool_call, req, generate, emit_update, run }) => {
69
- await agent_action.run({
69
+ const subres = await agent_action.run({
70
70
  row: {},
71
71
  configuration: { ...trigger.configuration, prompt: "continue" },
72
72
  user: req.user,
73
73
  run_id: run.id,
74
+ is_sub_agent: true,
74
75
  req,
75
76
  });
77
+ if (subres.json.raw_responses)
78
+ return { add_responses: subres.json.raw_responses };
76
79
  return {
77
80
  //stop: true,
78
81
  //add_response: result,
@@ -0,0 +1,96 @@
1
+ const { div, pre, a } = require("@saltcorn/markup/tags");
2
+ const Workflow = require("@saltcorn/data/models/workflow");
3
+ const Form = require("@saltcorn/data/models/form");
4
+ const Table = require("@saltcorn/data/models/table");
5
+ const User = require("@saltcorn/data/models/user");
6
+ const File = require("@saltcorn/data/models/file");
7
+ const View = require("@saltcorn/data/models/view");
8
+ const Trigger = require("@saltcorn/data/models/trigger");
9
+ const FieldRepeat = require("@saltcorn/data/models/fieldrepeat");
10
+ const { getState } = require("@saltcorn/data/db/state");
11
+ const db = require("@saltcorn/data/db");
12
+ const { eval_expression } = require("@saltcorn/data/models/expression");
13
+ const { interpolate, sleep } = require("@saltcorn/data/utils");
14
+ const { features } = require("@saltcorn/data/db/state");
15
+ const { button } = require("@saltcorn/markup/tags");
16
+ const { validID } = require("@saltcorn/markup/layout_utils");
17
+
18
+ const vm = require("vm");
19
+
20
+ //const { fieldProperties } = require("./helpers");
21
+
22
+ class WebSearchSkill {
23
+ static skill_name = "Web search";
24
+
25
+ get skill_label() {
26
+ return "Web search";
27
+ }
28
+
29
+ constructor(cfg) {
30
+ Object.assign(this, cfg);
31
+ }
32
+
33
+ static async configFields() {
34
+ return [
35
+ {
36
+ name: "search_provider",
37
+ label: "Search provider",
38
+ type: "String",
39
+ required: true,
40
+ attributes: { options: ["By URL template"] },
41
+ },
42
+ {
43
+ name: "url_template",
44
+ label: "URL template",
45
+ sublabel: "Use <code>{{q}}</code> for the URL-encoded search phrase",
46
+ type: "String",
47
+ required: true,
48
+ showIf: { search_provider: "By URL template" },
49
+ },
50
+ {
51
+ name: "header",
52
+ label: "Header",
53
+ sublabel: "For example <code>X-Subscription-Token: YOUR_API_KEY</code>",
54
+ type: "String",
55
+ showIf: { search_provider: "By URL template" },
56
+ },
57
+ ];
58
+ }
59
+ systemPrompt() {
60
+ return "If you need to search the web with a search phrase, use the web_search to get search engine results";
61
+ }
62
+
63
+ provideTools = () => {
64
+ return {
65
+ type: "function",
66
+ process: async (row) => {
67
+ const fOpts = { method: "GET" };
68
+ if (this.header) {
69
+ const [key, val] = this.header.split(":");
70
+ const myHeaders = new Headers();
71
+ myHeaders.append(key, val.trim());
72
+ fOpts.headers = myHeaders;
73
+ }
74
+ const url = interpolate(this.url_template, { q: row.search_phrase });
75
+ const resp = await fetch(url);
76
+ return await resp.text();
77
+ },
78
+ function: {
79
+ name: "web_search",
80
+ description: "Search the web with a search engine by a search phrase",
81
+ parameters: {
82
+ type: "object",
83
+ required: ["search_phrase"],
84
+ properties: {
85
+ search_phrase: {
86
+ description: "The phrase to search for",
87
+ type: "string",
88
+ },
89
+ },
90
+ },
91
+ },
92
+ };
93
+ };
94
+ }
95
+
96
+ module.exports = WebSearchSkill;
@@ -29,6 +29,13 @@ beforeAll(async () => {
29
29
  when_trigger: "Never",
30
30
  configuration: require("./agentcfg").maths_agent_cfg,
31
31
  });
32
+ await Trigger.create({
33
+ name: "OracleAgent",
34
+ description: "The all-knowing oracle answers any question",
35
+ action: "Agent",
36
+ when_trigger: "Never",
37
+ configuration: require("./agentcfg").oracle_agent_cfg,
38
+ });
32
39
 
33
40
  await getState().refresh_triggers(false);
34
41
  //await getState().setConfig("log_level", 6);
@@ -109,6 +116,27 @@ for (const nameconfig of require("./configs")) {
109
116
  });
110
117
  expect(result.json.response).toContain("987");
111
118
  });
119
+ it("run multiple subagents concurrenty", async () => {
120
+ const configuration = { ...require("./agentcfg").agent1 };
121
+ configuration.skills = [
122
+ ...configuration.skills,
123
+ {
124
+ agent_name: "OracleAgent",
125
+ skill_type: "Subagent",
126
+ },
127
+ ];
128
+ const result = await action.run({
129
+ row: {
130
+ theprompt:
131
+ "What is the 16th Fibonacci number (when F1=1 and F2=1)? Consult both the math agent and the oracle and see if they agree. You must call both the OracleAgent and the MathsAgent tools simultaneously",
132
+ },
133
+ configuration,
134
+ user,
135
+ req: { user },
136
+ });
137
+ expect(result.json.response).toContain("987");
138
+ });
112
139
  });
140
+
113
141
  //break;
114
142
  }
package/tests/agentcfg.js CHANGED
@@ -65,4 +65,26 @@ const maths_agent_cfg = {
65
65
  "If the user asks an arithmetic question, generate javascript code to solve it with the generate_arithmetic_code tool",
66
66
  };
67
67
 
68
- module.exports = { agent1, maths_agent_cfg };
68
+ const oracle_agent_cfg = {
69
+ model: "",
70
+ prompt: "{{theprompt}}",
71
+ skills: [
72
+ {
73
+ tool_name: "ask_the_oracle",
74
+ tool_description: "Ask the all-knowing oracle a question",
75
+ skill_type: "Run JavaScript code",
76
+ js_code: "return '987';",
77
+ toolargs: [
78
+ {
79
+ name: "question",
80
+ description: "The question you would like to ask",
81
+ argtype: "string",
82
+ },
83
+ ],
84
+ },
85
+ ],
86
+ sys_prompt:
87
+ "You can ask a question of the all-knowing oracle by calling the ask_the_oracle tool ",
88
+ };
89
+
90
+ module.exports = { agent1, maths_agent_cfg, oracle_agent_cfg };