@saltcorn/agents 0.3.0 → 0.3.2

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/agent-view.js CHANGED
@@ -39,6 +39,7 @@ const {
39
39
  wrapCard,
40
40
  wrapSegment,
41
41
  process_interaction,
42
+ find_image_tool,
42
43
  } = require("./common");
43
44
  const MarkdownIt = require("markdown-it"),
44
45
  md = new MarkdownIt();
@@ -157,7 +158,6 @@ const run = async (
157
158
  for (const interact of run.context.interactions) {
158
159
  switch (interact.role) {
159
160
  case "user":
160
- console.log(interact.content);
161
161
  if (interact.content?.[0]?.type === "image_url") {
162
162
  const image_url = interact.content[0].image_url.url;
163
163
  if (image_url.startsWith("data"))
@@ -187,58 +187,74 @@ const run = async (
187
187
  break;
188
188
  case "assistant":
189
189
  case "system":
190
- if (interact.tool_calls) {
191
- if (interact.content) {
192
- interactMarkups.push(
193
- div(
194
- { class: "interaction-segment" },
195
- span({ class: "badge bg-secondary" }, action.name),
196
- typeof interact.content === "string"
197
- ? md.render(interact.content)
198
- : interact.content
199
- )
200
- );
190
+
191
+ for (const tool_call of interact.tool_calls || []) {
192
+ const toolSkill = find_tool(
193
+ tool_call.function.name,
194
+ action.configuration
195
+ );
196
+ if (toolSkill) {
197
+ const row = JSON.parse(tool_call.function.arguments);
198
+ if (toolSkill.tool.renderToolCall) {
199
+ const rendered = await toolSkill.tool.renderToolCall(row, {
200
+ req,
201
+ });
202
+ if (rendered)
203
+ interactMarkups.push(
204
+ wrapSegment(
205
+ wrapCard(
206
+ toolSkill.skill.skill_label ||
207
+ toolSkill.skill.constructor.skill_name,
208
+ rendered
209
+ ),
210
+ action.name
211
+ )
212
+ );
213
+ }
201
214
  }
202
- for (const tool_call of interact.tool_calls) {
203
- const toolSkill = find_tool(
204
- tool_call.function.name,
205
- action.configuration
206
- );
207
- if (toolSkill) {
208
- const row = JSON.parse(tool_call.function.arguments);
209
- if (toolSkill.tool.renderToolCall) {
210
- const rendered = await toolSkill.tool.renderToolCall(row, {
215
+ }
216
+ for (const image_call of interact.content?.image_calls || []) {
217
+
218
+ const toolSkill = find_image_tool(action.configuration);
219
+ if (toolSkill) {
220
+ if (toolSkill.tool.renderToolResponse) {
221
+ const rendered = await toolSkill.tool.renderToolResponse(
222
+ image_call,
223
+ {
211
224
  req,
212
- });
213
- if (rendered)
214
- interactMarkups.push(
215
- wrapSegment(
216
- wrapCard(
217
- toolSkill.skill.skill_label ||
218
- toolSkill.skill.constructor.skill_name,
219
- rendered
220
- ),
221
- action.name
222
- )
223
- );
224
- }
225
+ }
226
+ );
227
+
228
+ if (rendered)
229
+ interactMarkups.push(
230
+ wrapSegment(
231
+ wrapCard(
232
+ toolSkill.skill.skill_label ||
233
+ toolSkill.skill.constructor.skill_name,
234
+ rendered
235
+ ),
236
+ action.name
237
+ )
238
+ );
225
239
  }
226
240
  }
227
- } else
228
- interactMarkups.push(
229
- div(
230
- { class: "interaction-segment" },
231
- span({ class: "badge bg-secondary" }, action.name),
232
- typeof interact.content === "string"
233
- ? md.render(interact.content)
234
- : interact.content
235
- )
236
- );
241
+ }
242
+
243
+ interactMarkups.push(
244
+ div(
245
+ { class: "interaction-segment" },
246
+ span({ class: "badge bg-secondary" }, action.name),
247
+ typeof interact.content === "string"
248
+ ? md.render(interact.content)
249
+ : typeof interact.content?.content === "string"
250
+ ? md.render(interact.content.content)
251
+ : interact.content
252
+ )
253
+ );
237
254
  break;
238
255
  case "tool":
239
256
  if (interact.content !== "Action run") {
240
257
  let markupContent;
241
- console.log("interact", interact);
242
258
  const toolSkill = find_tool(interact.name, action.configuration);
243
259
  try {
244
260
  if (toolSkill?.tool?.renderToolResponse)
package/common.js CHANGED
@@ -12,6 +12,8 @@ const get_skills = () => {
12
12
  require("./skills/Trigger"),
13
13
  require("./skills/Table"),
14
14
  require("./skills/PreloadData"),
15
+ require("./skills/GenerateImage"),
16
+ require("./skills/ModelContextProtocol"),
15
17
  //require("./skills/AdaptiveFeedback"),
16
18
  ];
17
19
  };
@@ -34,7 +36,7 @@ const get_skill_instances = (config) => {
34
36
  const find_tool = (name, config) => {
35
37
  const skills = get_skill_instances(config);
36
38
  for (const skill of skills) {
37
- const skillTools = skill.provideTools();
39
+ const skillTools = skill.provideTools?.();
38
40
  const tools = !skillTools
39
41
  ? []
40
42
  : Array.isArray(skillTools)
@@ -45,6 +47,20 @@ const find_tool = (name, config) => {
45
47
  }
46
48
  };
47
49
 
50
+ const find_image_tool = (config) => {
51
+ const skills = get_skill_instances(config);
52
+ for (const skill of skills) {
53
+ const skillTools = skill.provideTools?.();
54
+ const tools = !skillTools
55
+ ? []
56
+ : Array.isArray(skillTools)
57
+ ? skillTools
58
+ : [skillTools];
59
+ const found = tools.find((t) => t?.type === "image_generation");
60
+ if (found) return { tool: found, skill };
61
+ }
62
+ };
63
+
48
64
  const getCompletionArguments = async (config, user) => {
49
65
  let tools = [];
50
66
 
@@ -52,7 +68,7 @@ const getCompletionArguments = async (config, user) => {
52
68
 
53
69
  const skills = get_skill_instances(config);
54
70
  for (const skill of skills) {
55
- const sysPr = await skill.systemPrompt({ user });
71
+ const sysPr = await skill.systemPrompt?.({ user });
56
72
  if (sysPr) sysPrompts.push(sysPr);
57
73
  const skillTools = skill.provideTools?.();
58
74
  if (skillTools && Array.isArray(skillTools)) tools.push(...skillTools);
@@ -133,10 +149,43 @@ const process_interaction = async (
133
149
  const complArgs = await getCompletionArguments(config, req.user);
134
150
  complArgs.chat = run.context.interactions;
135
151
  //complArgs.debugResult = true;
136
- console.log("complArgs", JSON.stringify(complArgs, null, 2));
152
+ //console.log("complArgs", JSON.stringify(complArgs, null, 2));
137
153
 
138
154
  const answer = await getState().functions.llm_generate.run("", complArgs);
139
155
  console.log("answer", answer);
156
+
157
+ const responses = [];
158
+ if (answer && typeof answer === "object" && answer.image_calls) {
159
+ for (const image_call of answer.image_calls) {
160
+ const tool = find_image_tool(config);
161
+ let prcRes;
162
+ if (tool?.tool.process) {
163
+ prcRes = await tool.tool.process(image_call, { req });
164
+ if (prcRes?.result === null) delete image_call.result;
165
+ if (prcRes?.filename) image_call.filename = prcRes?.filename;
166
+ }
167
+ if (tool?.tool.renderToolResponse) {
168
+ const rendered = await tool.tool.renderToolResponse(
169
+ { ...image_call, ...(prcRes || {}) },
170
+ {
171
+ req,
172
+ }
173
+ );
174
+ if (rendered)
175
+ responses.push(
176
+ wrapSegment(
177
+ wrapCard(
178
+ tool.skill.skill_label || tool.skill.constructor.skill_name,
179
+ rendered
180
+ ),
181
+ agent_label
182
+ )
183
+ );
184
+ }
185
+ }
186
+ if (answer.content && !answer.tool_calls)
187
+ responses.push(wrapSegment(md.render(answer.content), agent_label));
188
+ }
140
189
  await addToContext(run, {
141
190
  interactions:
142
191
  typeof answer === "object" && answer.tool_calls
@@ -149,14 +198,13 @@ const process_interaction = async (
149
198
  ]
150
199
  : [{ role: "assistant", content: answer }],
151
200
  });
152
- const responses = [];
153
-
154
- if (typeof answer === "object" && answer.tool_calls) {
201
+ if (answer && typeof answer === "object" && (answer.tool_calls || answer.mcp_calls)) {
155
202
  if (answer.content)
156
203
  responses.push(wrapSegment(md.render(answer.content), agent_label));
157
204
  //const actions = [];
158
205
  let hasResult = false;
159
- for (const tool_call of answer.tool_calls) {
206
+ if ((answer.mcp_calls || []).length && !answer.content) hasResult = true;
207
+ for (const tool_call of answer.tool_calls || []) {
160
208
  console.log("call function", tool_call.function);
161
209
 
162
210
  await addToContext(run, {
@@ -213,6 +261,7 @@ const process_interaction = async (
213
261
  {
214
262
  role: "tool",
215
263
  tool_call_id: tool_call.id,
264
+ call_id: tool_call.call_id,
216
265
  name: tool_call.function.name,
217
266
  content:
218
267
  result && typeof result !== "string"
@@ -228,7 +277,8 @@ const process_interaction = async (
228
277
  ...prevResponses,
229
278
  ...responses,
230
279
  ]);
231
- } else responses.push(wrapSegment(md.render(answer), agent_label));
280
+ } else if (typeof answer === "string")
281
+ responses.push(wrapSegment(md.render(answer), agent_label));
232
282
 
233
283
  return {
234
284
  json: {
@@ -251,4 +301,5 @@ module.exports = {
251
301
  wrapCard,
252
302
  wrapSegment,
253
303
  process_interaction,
304
+ find_image_tool,
254
305
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saltcorn/agents",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "AI agents for Saltcorn",
5
5
  "main": "index.js",
6
6
  "dependencies": {
@@ -0,0 +1,98 @@
1
+ const { div, pre } = 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 File = require("@saltcorn/data/models/file");
6
+ const View = require("@saltcorn/data/models/view");
7
+ const { getState } = require("@saltcorn/data/db/state");
8
+ const db = require("@saltcorn/data/db");
9
+ const { eval_expression } = require("@saltcorn/data/models/expression");
10
+ const { interpolate } = require("@saltcorn/data/utils");
11
+
12
+ class GenerateImage {
13
+ static skill_name = "Image generation";
14
+
15
+ get skill_label() {
16
+ return `Image generation`;
17
+ }
18
+
19
+ constructor(cfg) {
20
+ Object.assign(this, cfg);
21
+ }
22
+
23
+ static async configFields() {
24
+ return [
25
+ {
26
+ name: "quality",
27
+ label: "Quality",
28
+ type: "String",
29
+ required: true,
30
+ attributes: { options: ["auto", "low", "medium", "high"] },
31
+ },
32
+ {
33
+ name: "size",
34
+ label: "Size",
35
+ type: "String",
36
+ required: true,
37
+ attributes: {
38
+ options: ["auto", "1024x1024", "1536x1024", "1024x1536"],
39
+ },
40
+ },
41
+ {
42
+ name: "format",
43
+ label: "Format",
44
+ type: "String",
45
+ required: true,
46
+ attributes: { options: ["png", "jpeg", "webp"] },
47
+ },
48
+ {
49
+ name: "transparent",
50
+ label: "Transparent",
51
+ type: "Bool",
52
+ },
53
+ {
54
+ name: "save_file",
55
+ label: "Save file",
56
+ type: "String",
57
+ required: true,
58
+ attributes: { options: ["Always", "Never"] }, //, "Button"] },
59
+ },
60
+ ];
61
+ }
62
+
63
+ provideTools() {
64
+ const tool = {
65
+ type: "image_generation",
66
+ size: this.size,
67
+ quality: this.quality,
68
+ process: async (v, { req }) => {
69
+ if (this.save_file === "Always") {
70
+ const buf = Buffer.from(v.result, "base64");
71
+ const file = await File.from_contents(
72
+ `genimg.${v.output_format}`,
73
+ `image/${v.output_format}`,
74
+ buf,
75
+ req?.user?.id,
76
+ 100
77
+ );
78
+ return { filename: file.path_to_serve, result: null };
79
+ }
80
+ },
81
+ renderToolResponse: (v) => {
82
+ const [ws, hs] = v.size.split("x");
83
+ if (v.filename)
84
+ return `<img height="${+hs / 4}" width="${
85
+ +ws / 4
86
+ }" src="/files/serve/${v.filename}" />`;
87
+ else
88
+ return `<img height="${+hs / 4}" width="${+ws / 4}" src="data:image/${
89
+ v.output_format
90
+ };base64, ${v.result}" />`;
91
+ },
92
+ };
93
+ if (this.transparent) tool.background = "transparent";
94
+ return tool;
95
+ }
96
+ }
97
+
98
+ module.exports = GenerateImage;
@@ -0,0 +1,67 @@
1
+ const { div, pre } = 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 File = require("@saltcorn/data/models/file");
6
+ const View = require("@saltcorn/data/models/view");
7
+ const { getState } = require("@saltcorn/data/db/state");
8
+ const db = require("@saltcorn/data/db");
9
+ const { eval_expression } = require("@saltcorn/data/models/expression");
10
+ const { interpolate } = require("@saltcorn/data/utils");
11
+
12
+ class ModelContextProtocol {
13
+ static skill_name = "Model Context Protocol";
14
+
15
+ get skill_label() {
16
+ return `Model Context Protocol: ${this.server_label}`;
17
+ }
18
+
19
+ constructor(cfg) {
20
+ Object.assign(this, cfg);
21
+ }
22
+
23
+ static async configFields() {
24
+ return [
25
+ {
26
+ name: "server_label",
27
+ label: "Server label",
28
+ type: "String",
29
+ required: true,
30
+ },
31
+ {
32
+ name: "server_url",
33
+ label: "Server URL",
34
+ type: "String",
35
+ required: true,
36
+ },
37
+ {
38
+ name: "header_key",
39
+ label: "Header key",
40
+ type: "String",
41
+ sublabel:
42
+ "Optional, for authorization. Which HTTP header should be set? For example <code>Authorization</code>",
43
+ },
44
+ {
45
+ name: "header_value",
46
+ label: "Header value",
47
+ type: "String",
48
+ sublabel:
49
+ "Optional, for authorization. What is the value of the HTTP header? For example <code>Bearer {api_key}</code>",
50
+ },
51
+ ];
52
+ }
53
+
54
+ provideTools() {
55
+ const tool = {
56
+ type: "mcp",
57
+ server_label: this.server_label,
58
+ server_url: this.server_url,
59
+ require_approval: "never",
60
+ };
61
+ if (this.header_key && this.header_value)
62
+ tool.headers = { [this.header_key]: this.header_value };
63
+ return tool;
64
+ }
65
+ }
66
+
67
+ module.exports = ModelContextProtocol;
@@ -31,7 +31,6 @@ class PreloadData {
31
31
  );
32
32
 
33
33
  const rows = await table.getRows(q);
34
- console.log("preload data rows", rows);
35
34
  if (this.contents_expr) {
36
35
  for (const row of rows)
37
36
  prompts.push(interpolate(this.contents_expr, row, user));