@saltcorn/agents 0.6.12 → 0.7.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/action.js CHANGED
@@ -4,6 +4,7 @@ const { get_skills, process_interaction } = require("./common");
4
4
  const { applyAsync } = require("@saltcorn/data/utils");
5
5
  const WorkflowRun = require("@saltcorn/data/models/workflow_run");
6
6
  const { interpolate } = require("@saltcorn/data/utils");
7
+ const { getState } = require("@saltcorn/data/db/state");
7
8
 
8
9
  module.exports = {
9
10
  disableInBuilder: true,
@@ -22,12 +23,10 @@ module.exports = {
22
23
  }
23
24
  }
24
25
  }
25
- const tables_with_json_field = (await Table.find({})).filter(
26
- (table) =>
27
- !table.external &&
28
- !table.provider_name &&
29
- table.fields.some((f) => f.type?.name === "JSON"),
30
- );
26
+ const llm_cfg_fun = getState().functions.llm_get_configuration;
27
+ const alt_config_options = llm_cfg_fun
28
+ ? llm_cfg_fun.run().alt_config_names || []
29
+ : [];
31
30
  return [
32
31
  ...(table
33
32
  ? [
@@ -50,6 +49,17 @@ module.exports = {
50
49
  type: "String",
51
50
  fieldview: "textarea",
52
51
  },
52
+ ...(alt_config_options.length
53
+ ? [
54
+ {
55
+ name: "alt_config",
56
+ label: "Alternative configuration",
57
+ sublabel: "Use this configuration for LLM interactions",
58
+ type: "String",
59
+ attributes: { options: alt_config_options },
60
+ },
61
+ ]
62
+ : []),
53
63
  {
54
64
  name: "model",
55
65
  label: "Model",
package/common.js CHANGED
@@ -37,6 +37,7 @@ const get_skills = () => {
37
37
  require("./skills/RunJsCode"),
38
38
  require("./skills/GenerateAndRunJsCode"),
39
39
  require("./skills/Fetch"),
40
+ require("./skills/Subagent"),
40
41
  //require("./skills/AdaptiveFeedback"),
41
42
  ...exchange_skills,
42
43
  ];
@@ -140,6 +141,7 @@ const getCompletionArguments = async (
140
141
  if (tools.length === 0) tools = undefined;
141
142
  const complArgs = { tools, systemPrompt: sysPrompts.join("\n\n") };
142
143
  if (config.model) complArgs.model = config.model;
144
+ if (config.alt_config) complArgs.alt_config = config.alt_config;
143
145
  return complArgs;
144
146
  };
145
147
 
@@ -250,7 +252,14 @@ const process_interaction = async (
250
252
  }
251
253
  };
252
254
  }
253
- const answer = await sysState.functions.llm_generate.run("", complArgs);
255
+
256
+ const lastInteract =
257
+ run.context.interactions[run.context.interactions.length - 1];
258
+
259
+ const answer = await sysState.functions.llm_generate.run(
260
+ lastInteract?.role === "user" ? "" : "Continue",
261
+ complArgs,
262
+ );
254
263
 
255
264
  //console.log("answer", answer);
256
265
 
@@ -330,7 +339,7 @@ const process_interaction = async (
330
339
  if ((answer.mcp_calls || []).length && !answer.content) hasResult = true;
331
340
  if (answer.hasToolCalls)
332
341
  for (const tool_call of answer.getToolCalls()) {
333
- console.log("call function", tool_call.tool_name);
342
+ getState().log(6, "call function " + tool_call.tool_name);
334
343
 
335
344
  await addToContext(run, {
336
345
  funcalls: {
@@ -434,12 +443,14 @@ const process_interaction = async (
434
443
  result,
435
444
  chat,
436
445
  req,
446
+ run,
437
447
  async generate(prompt, opts = {}) {
438
448
  generateUsed = true;
439
449
  return await sysState.functions.llm_generate.run(prompt, {
440
450
  chat,
441
451
  appendToChat: true,
442
452
  systemPrompt,
453
+ alt_config: config.alt_config,
443
454
  ...opts,
444
455
  });
445
456
  },
@@ -481,7 +492,7 @@ const process_interaction = async (
481
492
  // run.context.interactions.forEach((ic) => {});
482
493
  const result = postprocres.add_response;
483
494
  await sysState.functions.llm_add_message.run(
484
- "tool_response",
495
+ "assistant",
485
496
  !result || typeof result === "string"
486
497
  ? {
487
498
  type: "text",
@@ -493,9 +504,19 @@ const process_interaction = async (
493
504
  },
494
505
  {
495
506
  chat: run.context.interactions,
496
- tool_call,
497
507
  },
498
508
  );
509
+ if (!postprocres.stop)
510
+ await sysState.functions.llm_add_message.run(
511
+ "user",
512
+ {
513
+ type: "text",
514
+ value: "Continue",
515
+ },
516
+ {
517
+ chat: run.context.interactions,
518
+ },
519
+ );
499
520
  }
500
521
  if (postprocres.add_user_action && viewname) {
501
522
  const user_actions = Array.isArray()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saltcorn/agents",
3
- "version": "0.6.12",
3
+ "version": "0.7.1",
4
4
  "description": "AI agents for Saltcorn",
5
5
  "main": "index.js",
6
6
  "dependencies": {
@@ -19,7 +19,7 @@
19
19
  "jest": "^29.7.0"
20
20
  },
21
21
  "scripts": {
22
- "test": "jest tests --runInBand"
22
+ "test": "jest tests --runInBand --verbose"
23
23
  },
24
24
  "eslintConfig": {
25
25
  "extends": "eslint:recommended",
package/skills/Fetch.js CHANGED
@@ -33,6 +33,9 @@ class FetchSkill {
33
33
  static async configFields() {
34
34
  return [];
35
35
  }
36
+ systemPrompt() {
37
+ return "If you need to retrieve the contents of a web page, use the fetch_web_page to make a GET request to a specified URL.";
38
+ }
36
39
 
37
40
  provideTools = () => {
38
41
  return {
@@ -142,7 +142,29 @@ ${this.allow_fetch ? "\nYou can use the standard fetch JavaScript function to ma
142
142
  ${this.allow_table ? getTablePrompt(this.read_only) : ""}
143
143
 
144
144
  The code you write can use await at the top level, and should return
145
- (at the top level) a string (which can contain HTML tags) with the response which will be shown to the user.
145
+ (at the top level) either a string (which can contain HTML tags) with the response which will be shown
146
+ to the user, or a JSON object which will then be further summarized for the user.
147
+
148
+ Example:
149
+
150
+ \`\`\`javascript
151
+
152
+ const x = await myAsyncFunction()
153
+ const y = await anotherAsyncFunction(x)
154
+
155
+ return \`The eggs are \${x} and the why is \${y}\`
156
+ \`\`\`
157
+
158
+ or
159
+
160
+ \`\`\`javascript
161
+
162
+ const x = await myAsyncFunction()
163
+ const y = await anotherAsyncFunction(x)
164
+
165
+ return { x, y }
166
+ \`\`\`
167
+
146
168
 
147
169
  Now generate the JavaScript code required by the user.`,
148
170
  );
@@ -156,9 +178,9 @@ Now generate the JavaScript code required by the user.`,
156
178
  emit_update("Running code");
157
179
  const res = await this.runCode(js_code, { user: req.user });
158
180
  //console.log("code response", res);
159
- getState().log(6, "Code answer: " + JSON.stringify(res));
181
+ getState().log(6, "Code answer: " + JSON.stringify(res));
160
182
  return {
161
- //stop: true,
183
+ stop: typeof res ==="string",
162
184
  add_response: res,
163
185
  };
164
186
  },
@@ -0,0 +1,94 @@
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 View = require("@saltcorn/data/models/view");
6
+ const Trigger = require("@saltcorn/data/models/trigger");
7
+ const { getState } = require("@saltcorn/data/db/state");
8
+ const db = require("@saltcorn/data/db");
9
+ const { fieldProperties } = require("./helpers");
10
+ const agent_action = require("../action");
11
+
12
+ class SubagentToSkill {
13
+ static skill_name = "Subagent";
14
+
15
+ get skill_label() {
16
+ return this.agent_name;
17
+ }
18
+
19
+ constructor(cfg) {
20
+ Object.assign(this, cfg);
21
+ }
22
+
23
+ systemPrompt() {
24
+ const trigger = Trigger.findOne({ name: this.agent_name });
25
+ if (trigger.description)
26
+ return `${this.agent_name} tool: ${trigger.description}`;
27
+ else return "";
28
+ }
29
+
30
+ static async configFields() {
31
+ const actions = await Trigger.find({ action: "Agent" });
32
+
33
+ return [
34
+ {
35
+ name: "agent_name",
36
+ label: "Agent",
37
+ sublabel: a(
38
+ {
39
+ "data-dyn-href": `\`/actions/configure/\${agent_name}\``,
40
+ target: "_blank",
41
+ },
42
+ "Configure",
43
+ ),
44
+ type: "String",
45
+ required: true,
46
+ attributes: { options: actions.map((a) => a.name) },
47
+ },
48
+ // TODO: confirm, show response, show argument
49
+ ];
50
+ }
51
+
52
+ provideTools = () => {
53
+ let properties = {};
54
+
55
+ const trigger = Trigger.findOne({ name: this.agent_name });
56
+ if (!trigger)
57
+ throw new Error(`Trigger skill: cannot find trigger ${this.agent_name}`);
58
+
59
+ return {
60
+ type: "function",
61
+ process: async (row, { req }) => {
62
+ //const result = await trigger.runWithoutRow({ user: req?.user, row });
63
+ return "Workflow started";
64
+ },
65
+ /*renderToolCall({ phrase }, { req }) {
66
+ return div({ class: "border border-primary p-2 m-2" }, phrase);
67
+ },*/
68
+ postProcess: async ({ tool_call, req, generate, emit_update, run }) => {
69
+ await agent_action.run({
70
+ row: {},
71
+ configuration: { ...trigger.configuration, prompt: "continue" },
72
+ user: req.user,
73
+ run_id: run.id,
74
+ req,
75
+ });
76
+ return {
77
+ //stop: true,
78
+ //add_response: result,
79
+ };
80
+ },
81
+ function: {
82
+ name: trigger.name,
83
+ description: trigger.description,
84
+ parameters: {
85
+ type: "object",
86
+ //required: ["action_javascript_code", "action_name"],
87
+ properties,
88
+ },
89
+ },
90
+ };
91
+ };
92
+ }
93
+
94
+ module.exports = SubagentToSkill;
@@ -3,6 +3,7 @@ const View = require("@saltcorn/data/models/view");
3
3
  const WorkflowRuns = require("@saltcorn/data/models/workflow_run");
4
4
  const Table = require("@saltcorn/data/models/table");
5
5
  const Plugin = require("@saltcorn/data/models/plugin");
6
+ const Trigger = require("@saltcorn/data/models/trigger");
6
7
 
7
8
  const { mockReqRes } = require("@saltcorn/data/tests/mocks");
8
9
  const { afterAll, beforeAll, describe, it, expect } = require("@jest/globals");
@@ -20,6 +21,17 @@ beforeAll(async () => {
20
21
  await require("@saltcorn/data/db/fixtures")();
21
22
 
22
23
  getState().registerPlugin("base", require("@saltcorn/data/base-plugin"));
24
+
25
+ await Trigger.create({
26
+ name: "MathsAgent",
27
+ description: "Answer questions about arithmetic",
28
+ action: "Agent",
29
+ when_trigger: "Never",
30
+ configuration: require("./agentcfg").maths_agent_cfg,
31
+ });
32
+
33
+ await getState().refresh_triggers(false);
34
+ //await getState().setConfig("log_level", 6);
23
35
  });
24
36
 
25
37
  jest.setTimeout(30000);
@@ -52,7 +64,7 @@ for (const nameconfig of require("./configs")) {
52
64
  it("generates text", async () => {
53
65
  const result = await action.run({
54
66
  row: { theprompt: "What is the word of the day?" },
55
- configuration: require("./agentcfg"),
67
+ configuration: require("./agentcfg").agent1,
56
68
  user,
57
69
  req: { user },
58
70
  });
@@ -67,7 +79,7 @@ for (const nameconfig of require("./configs")) {
67
79
  theprompt:
68
80
  "How many pages are there in the book by Herman Melville in the database?",
69
81
  },
70
- configuration: require("./agentcfg"),
82
+ configuration: require("./agentcfg").agent1,
71
83
  user,
72
84
  run_id: run.id,
73
85
  req: { ...mockReqRes.req, user },
@@ -75,6 +87,28 @@ for (const nameconfig of require("./configs")) {
75
87
  expect(result.json.response).toContain("967");
76
88
  //const run1 = await WorkflowRuns.findOne({});
77
89
  });
90
+ it("generates and runs js code", async () => {
91
+ const result = await action.run({
92
+ row: {
93
+ theprompt: "What is the 16th Fibonacci number (when F1=1 and F2=1) ?",
94
+ },
95
+ configuration: require("./agentcfg").maths_agent_cfg,
96
+ user,
97
+ req: { user },
98
+ });
99
+ expect(result.json.response).toContain("987");
100
+ });
101
+ it("run subagent", async () => {
102
+ const result = await action.run({
103
+ row: {
104
+ theprompt: "What is the 16th Fibonacci number (when F1=1 and F2=1) ?",
105
+ },
106
+ configuration: require("./agentcfg").agent1,
107
+ user,
108
+ req: { user },
109
+ });
110
+ expect(result.json.response).toContain("987");
111
+ });
78
112
  });
79
113
  //break;
80
114
  }
package/tests/agentcfg.js CHANGED
@@ -1,4 +1,4 @@
1
- module.exports = {
1
+ const agent1 = {
2
2
  model: "",
3
3
  prompt: "{{theprompt}}",
4
4
  skills: [
@@ -41,6 +41,28 @@ module.exports = {
41
41
  add_sys_prompt:
42
42
  "Use this tool to search information about books in a book database. Each book is indexed by author and has page counts. If the user asks for information about books by a specific author, use this tool.",
43
43
  },
44
+ {
45
+ agent_name: "MathsAgent",
46
+ skill_type: "Subagent",
47
+ },
44
48
  ],
45
49
  sys_prompt: "",
46
50
  };
51
+
52
+ const maths_agent_cfg = {
53
+ model: "",
54
+ prompt: "{{theprompt}}",
55
+ skills: [
56
+ {
57
+ tool_name: "generate_arithmetic_code",
58
+ skill_type: "Generate and run JavaScript code",
59
+ add_sys_prompt: "",
60
+ code_description: "",
61
+ tool_description: "Generate Javascript code to solve arithmetic problems",
62
+ },
63
+ ],
64
+ sys_prompt:
65
+ "If the user asks an arithmetic question, generate javascript code to solve it with the generate_arithmetic_code tool",
66
+ };
67
+
68
+ module.exports = { agent1, maths_agent_cfg };
package/tests/configs.js CHANGED
@@ -31,4 +31,13 @@ module.exports = [
31
31
  temperature: 0.7,
32
32
  ai_sdk_provider: "OpenAI",
33
33
  },
34
+ {
35
+ name: "AI SDK Anthropic",
36
+ model: "claude-sonnet-4-6",
37
+ api_key: process.env.ANTHROPIC_API_KEY,
38
+ backend: "AI SDK",
39
+ image_model: "gpt-image-1",
40
+ temperature: 0.7,
41
+ ai_sdk_provider: "Anthropic",
42
+ },
34
43
  ];
@@ -42,10 +42,10 @@ for (const nameconfig of require("./configs")) {
42
42
  description: "",
43
43
  action: "Agent",
44
44
  when_trigger: "Never",
45
- configuration: require("./agentcfg"),
45
+ configuration: require("./agentcfg").agent1,
46
46
  });
47
-
48
- await getState().refresh_triggers(false)
47
+
48
+ await getState().refresh_triggers(false);
49
49
  const view = await View.create({
50
50
  name: "AgentView",
51
51
  description: "",
@@ -79,10 +79,10 @@ for (const nameconfig of require("./configs")) {
79
79
  default_render_page: "",
80
80
  exttable_name: null,
81
81
  });
82
- await getState().refresh_views(false)
82
+ await getState().refresh_views(false);
83
83
 
84
- const result = await view.run({}, mockReqRes);
85
- expect(result).toContain(">Pirate<")
84
+ const result = await view.run({}, mockReqRes);
85
+ expect(result).toContain(">Pirate<");
86
86
  });
87
87
  });
88
88
  break; //only need to test one config iteration