@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 +16 -6
- package/common.js +25 -4
- package/package.json +2 -2
- package/skills/Fetch.js +3 -0
- package/skills/GenerateAndRunJsCode.js +25 -3
- package/skills/Subagent.js +94 -0
- package/tests/action.test.js +36 -2
- package/tests/agentcfg.js +23 -1
- package/tests/configs.js +9 -0
- package/tests/view.test.js +6 -6
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
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
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.
|
|
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
|
|
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
|
-
|
|
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;
|
package/tests/action.test.js
CHANGED
|
@@ -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
|
-
|
|
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
|
];
|
package/tests/view.test.js
CHANGED
|
@@ -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
|