@saltcorn/agents 0.6.11 → 0.7.0

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/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
  ];
@@ -95,6 +96,24 @@ const get_initial_interactions = async (config, user, triggering_row) => {
95
96
  return interacts;
96
97
  };
97
98
 
99
+ const getSystemPrompt = async (config, user, triggering_row, formbody) => {
100
+ let sysPrompts = [
101
+ interpolate(config.sys_prompt, triggering_row || {}, user, "System prompt"),
102
+ ];
103
+
104
+ const skills = get_skill_instances(config);
105
+ for (const skill of skills) {
106
+ const sysPr = await skill.systemPrompt?.({
107
+ ...(formbody || {}),
108
+ user,
109
+ triggering_row,
110
+ });
111
+ if (sysPr) sysPrompts.push(sysPr);
112
+ }
113
+
114
+ return sysPrompts.join("\n\n");
115
+ };
116
+
98
117
  const getCompletionArguments = async (
99
118
  config,
100
119
  user,
@@ -232,7 +251,14 @@ const process_interaction = async (
232
251
  }
233
252
  };
234
253
  }
235
- const answer = await sysState.functions.llm_generate.run("", complArgs);
254
+
255
+ const lastInteract =
256
+ run.context.interactions[run.context.interactions.length - 1];
257
+
258
+ const answer = await sysState.functions.llm_generate.run(
259
+ lastInteract?.role === "user" ? "" : "Continue",
260
+ complArgs,
261
+ );
236
262
 
237
263
  //console.log("answer", answer);
238
264
 
@@ -312,7 +338,7 @@ const process_interaction = async (
312
338
  if ((answer.mcp_calls || []).length && !answer.content) hasResult = true;
313
339
  if (answer.hasToolCalls)
314
340
  for (const tool_call of answer.getToolCalls()) {
315
- console.log("call function", tool_call.tool_name);
341
+ getState().log(6, "call function " + tool_call.tool_name);
316
342
 
317
343
  await addToContext(run, {
318
344
  funcalls: {
@@ -326,10 +352,9 @@ const process_interaction = async (
326
352
  let stop = false,
327
353
  myHasResult = false;
328
354
  if (stream && viewname) {
329
- let content = span(
330
- { class: "badge text-bg-secondary me-1" },
331
- tool.skill.skill_label || tool.skill.constructor.skill_name,
332
- );
355
+ let content =
356
+ (tool.skill.skill_label || tool.skill.constructor.skill_name) +
357
+ " ";
333
358
  const view = View.findOne({ name: viewname });
334
359
  const pageLoadTag = req.body.page_load_tag;
335
360
  view.emitRealTimeEvent(
@@ -406,16 +431,24 @@ const process_interaction = async (
406
431
  if (tool.tool.postProcess && !stop) {
407
432
  const chat = run.context.interactions;
408
433
  let generateUsed = false;
434
+ const systemPrompt = await getSystemPrompt(
435
+ config,
436
+ req.user,
437
+ triggering_row,
438
+ req.body,
439
+ );
409
440
  const postprocres = await tool.tool.postProcess({
410
441
  tool_call,
411
442
  result,
412
443
  chat,
413
444
  req,
445
+ run,
414
446
  async generate(prompt, opts = {}) {
415
447
  generateUsed = true;
416
448
  return await sysState.functions.llm_generate.run(prompt, {
417
449
  chat,
418
450
  appendToChat: true,
451
+ systemPrompt,
419
452
  ...opts,
420
453
  });
421
454
  },
@@ -426,7 +459,7 @@ const process_interaction = async (
426
459
  view.emitRealTimeEvent(
427
460
  `STREAM_CHUNK?page_load_tag=${pageLoadTag}`,
428
461
  {
429
- content: span({ class: "badge text-bg-secondary me-1" }, s),
462
+ content: s + " ",
430
463
  },
431
464
  );
432
465
  },
@@ -457,7 +490,7 @@ const process_interaction = async (
457
490
  // run.context.interactions.forEach((ic) => {});
458
491
  const result = postprocres.add_response;
459
492
  await sysState.functions.llm_add_message.run(
460
- "tool_response",
493
+ "assistant",
461
494
  !result || typeof result === "string"
462
495
  ? {
463
496
  type: "text",
@@ -469,9 +502,19 @@ const process_interaction = async (
469
502
  },
470
503
  {
471
504
  chat: run.context.interactions,
472
- tool_call,
473
505
  },
474
506
  );
507
+ if (!postprocres.stop)
508
+ await sysState.functions.llm_add_message.run(
509
+ "user",
510
+ {
511
+ type: "text",
512
+ value: "Continue",
513
+ },
514
+ {
515
+ chat: run.context.interactions,
516
+ },
517
+ );
475
518
  }
476
519
  if (postprocres.add_user_action && viewname) {
477
520
  const user_actions = Array.isArray()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saltcorn/agents",
3
- "version": "0.6.11",
3
+ "version": "0.7.0",
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,20 +142,45 @@ ${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
  );
149
- getState().log(6, "Generated code: \n" + str);
171
+ getState().log(
172
+ 6,
173
+ "Generated code:\n--BEGIN CODE--\n" + str + "\n--END CODE--\n",
174
+ );
150
175
  const js_code = str.includes("```javascript")
151
176
  ? str.split("```javascript")[1].split("```")[0]
152
177
  : str;
153
178
  emit_update("Running code");
154
179
  const res = await this.runCode(js_code, { user: req.user });
155
180
  //console.log("code response", res);
156
- getState().log(6, "Code answer: " + JSON.stringify(res));
181
+ getState().log(6, "Code answer: " + JSON.stringify(res));
157
182
  return {
158
- //stop: true,
183
+ stop: typeof res ==="string",
159
184
  add_response: res,
160
185
  };
161
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