@saltcorn/agents 0.5.8 → 0.6.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/common.js CHANGED
@@ -1,5 +1,5 @@
1
1
  const { getState } = require("@saltcorn/data/db/state");
2
- const { div, span } = require("@saltcorn/markup/tags");
2
+ const { div, span, button } = require("@saltcorn/markup/tags");
3
3
  const Trigger = require("@saltcorn/data/models/trigger");
4
4
  const View = require("@saltcorn/data/models/view");
5
5
  const { interpolate } = require("@saltcorn/data/utils");
@@ -21,8 +21,8 @@ const nubBy = (f, xs) => {
21
21
  const get_skills = () => {
22
22
  const state = getState();
23
23
  const exchange_skills = nubBy(
24
- (c) => c.constructor.name,
25
- state.exchange?.agent_skills || []
24
+ (c) => c.skill_name,
25
+ state.exchange?.agent_skills || [],
26
26
  );
27
27
 
28
28
  return [
@@ -62,8 +62,8 @@ const find_tool = (name, config) => {
62
62
  const tools = !skillTools
63
63
  ? []
64
64
  : Array.isArray(skillTools)
65
- ? skillTools
66
- : [skillTools];
65
+ ? skillTools
66
+ : [skillTools];
67
67
  const found = tools.find((t) => t?.function?.name === name);
68
68
  if (found) return { tool: found, skill };
69
69
  }
@@ -76,8 +76,8 @@ const find_image_tool = (config) => {
76
76
  const tools = !skillTools
77
77
  ? []
78
78
  : Array.isArray(skillTools)
79
- ? skillTools
80
- : [skillTools];
79
+ ? skillTools
80
+ : [skillTools];
81
81
  const found = tools.find((t) => t?.type === "image_generation");
82
82
  if (found) return { tool: found, skill };
83
83
  }
@@ -97,7 +97,7 @@ const getCompletionArguments = async (
97
97
  config,
98
98
  user,
99
99
  triggering_row,
100
- formbody
100
+ formbody,
101
101
  ) => {
102
102
  let tools = [];
103
103
 
@@ -131,11 +131,11 @@ const incompleteCfgMsg = () => {
131
131
  !plugin_cfgs["large-language-model"]
132
132
  ) {
133
133
  const modName = Object.keys(plugin_cfgs).find((m) =>
134
- m.includes("large-language-model")
134
+ m.includes("large-language-model"),
135
135
  );
136
136
  if (modName)
137
137
  return `LLM module not configured. Please configure <a href="/plugins/configure/${encodeURIComponent(
138
- modName
138
+ modName,
139
139
  )}">here<a> before using copilot.`;
140
140
  else
141
141
  return `LLM module not configured. Please install and configure <a href="/plugins">here<a> before using copilot.`;
@@ -150,7 +150,8 @@ const addToContext = async (run, newCtx) => {
150
150
  if (Array.isArray(run.context[k])) {
151
151
  if (!Array.isArray(newCtx[k]))
152
152
  throw new Error("Must be array to append to array");
153
- run.context[k].push(...newCtx[k]);
153
+ if (k === "interactions") run.context[k] = newCtx[k];
154
+ else run.context[k].push(...newCtx[k]);
154
155
  changed = true;
155
156
  } else if (typeof run.context[k] === "object") {
156
157
  if (typeof newCtx[k] !== "object")
@@ -178,26 +179,9 @@ const wrapCard = (title, ...inners) =>
178
179
  span({ class: "badge bg-info ms-1" }, title) +
179
180
  div(
180
181
  { class: "card mb-3 bg-secondary-subtle" },
181
- div({ class: "card-body" }, inners)
182
+ div({ class: "card-body" }, inners),
182
183
  );
183
184
 
184
- const only_response_text_if_present = (interact) => {
185
- if (
186
- interact.role === "tool" &&
187
- interact.call_id &&
188
- interact.content?.[0] === "{"
189
- ) {
190
- try {
191
- const result = JSON.parse(interact.content);
192
- if (result.responseText)
193
- return { ...interact, content: result.responseText };
194
- } catch {
195
- //ignore, not json content
196
- }
197
- }
198
- return interact;
199
- };
200
-
201
185
  const is_debug_mode = (config, user) => user?.role_id === 1;
202
186
 
203
187
  const process_interaction = async (
@@ -207,7 +191,8 @@ const process_interaction = async (
207
191
  agent_label = "Copilot",
208
192
  prevResponses = [],
209
193
  triggering_row = {},
210
- agentsViewCfg = { stream: false }
194
+ agentsViewCfg = { stream: false },
195
+ dyn_updates = false,
211
196
  ) => {
212
197
  const { stream, viewname } = agentsViewCfg;
213
198
  const sysState = getState();
@@ -215,9 +200,10 @@ const process_interaction = async (
215
200
  config,
216
201
  req.user,
217
202
  triggering_row,
218
- req.body
203
+ req.body,
219
204
  );
220
- complArgs.chat = run.context.interactions.map(only_response_text_if_present);
205
+ complArgs.appendToChat = true;
206
+ complArgs.chat = run.context.interactions;
221
207
  //complArgs.debugResult = true;
222
208
  //console.log("complArgs", JSON.stringify(complArgs, null, 2));
223
209
  const debugMode = is_debug_mode(config, req.user);
@@ -240,13 +226,30 @@ const process_interaction = async (
240
226
  }
241
227
  const answer = await sysState.functions.llm_generate.run("", complArgs);
242
228
 
243
- //console.log({answer});
229
+ //console.log("answer", answer);
244
230
 
245
231
  if (debugMode)
246
232
  await addToContext(run, {
247
233
  api_interactions: [debugCollector],
248
234
  });
235
+ await addToContext(run, {
236
+ interactions: complArgs.chat,
237
+ });
249
238
  const responses = [];
239
+ const add_response = async (resp) => {
240
+ if (dyn_updates)
241
+ getState().emitDynamicUpdate(
242
+ db.getTenantSchema(),
243
+ {
244
+ eval_js: `processCopilotResponse({response: ${JSON.stringify(resp)}, run_id: ${run.id}})`,
245
+ },
246
+ [req.user.id],
247
+ );
248
+ else responses.push(resp);
249
+ await addToContext(run, {
250
+ html_interactions: [resp],
251
+ });
252
+ };
250
253
  if (answer && typeof answer === "object" && answer.image_calls) {
251
254
  for (const image_call of answer.image_calls) {
252
255
  const tool = find_image_tool(config);
@@ -261,176 +264,203 @@ const process_interaction = async (
261
264
  { ...image_call, ...(prcRes || {}) },
262
265
  {
263
266
  req,
264
- }
267
+ },
265
268
  );
266
269
  if (rendered)
267
- responses.push(
270
+ add_response(
268
271
  wrapSegment(
269
272
  wrapCard(
270
273
  tool.skill.skill_label || tool.skill.constructor.skill_name,
271
- rendered
274
+ rendered,
272
275
  ),
273
- agent_label
274
- )
276
+ agent_label,
277
+ ),
275
278
  );
276
279
  }
277
280
  }
278
281
  if (answer.content && !answer.tool_calls)
279
- responses.push(
282
+ add_response(
280
283
  req.disable_markdown_render
281
284
  ? answer
282
- : wrapSegment(md.render(answer.content), agent_label)
285
+ : wrapSegment(md.render(answer.content), agent_label),
283
286
  );
284
287
  }
285
- if (answer.ai_sdk)
286
- await addToContext(run, {
287
- interactions: answer.messages,
288
- });
289
- else
290
- await addToContext(run, {
291
- interactions:
292
- typeof answer === "object" && answer.tool_calls
293
- ? [
294
- {
295
- role: "assistant",
296
- tool_calls: answer.tool_calls,
297
- content: answer.content,
298
- },
299
- ]
300
- : [{ role: "assistant", content: answer }],
301
- });
288
+
302
289
  if (
303
290
  answer &&
304
291
  typeof answer === "object" &&
305
- (answer.tool_calls || answer.mcp_calls)
292
+ (answer.hasToolCalls || answer.mcp_calls)
306
293
  ) {
307
294
  if (answer.content)
308
- responses.push(
295
+ add_response(
309
296
  req.disable_markdown_render
310
297
  ? answer
311
- : wrapSegment(md.render(answer.content), agent_label)
298
+ : wrapSegment(md.render(answer.content), agent_label),
312
299
  );
313
300
  //const actions = [];
314
301
  let hasResult = false;
315
302
  if ((answer.mcp_calls || []).length && !answer.content) hasResult = true;
316
- for (const tool_call of answer.tool_calls || []) {
317
- console.log(
318
- "call function",
319
- tool_call.toolName || tool_call.function?.name
320
- );
303
+ if (answer.hasToolCalls)
304
+ for (const tool_call of answer.getToolCalls()) {
305
+ console.log("call function", tool_call.tool_name);
321
306
 
322
- await addToContext(run, {
323
- funcalls: {
324
- [tool_call.id || tool_call.toolCallId]: answer.ai_sdk
325
- ? tool_call
326
- : tool_call.function,
327
- },
328
- });
307
+ await addToContext(run, {
308
+ funcalls: {
309
+ [tool_call.tool_call_id]: tool_call,
310
+ },
311
+ });
329
312
 
330
- const tool = find_tool(
331
- tool_call.toolName || tool_call.function?.name,
332
- config
333
- );
313
+ const tool = find_tool(tool_call.tool_name, config);
334
314
 
335
- if (tool) {
336
- if (stream && viewname) {
337
- let content =
338
- "Using skill: " + tool.skill.skill_label ||
339
- tool.skill.constructor.skill_name;
340
- const view = View.findOne({ name: viewname });
341
- const pageLoadTag = req.body.page_load_tag;
342
- view.emitRealTimeEvent(`STREAM_CHUNK?page_load_tag=${pageLoadTag}`, {
343
- content,
344
- });
345
- }
346
- if (tool.tool.renderToolCall) {
347
- const row = answer.ai_sdk
348
- ? tool_call.input
349
- : JSON.parse(tool_call.function.arguments);
350
- const rendered = await tool.tool.renderToolCall(row, {
351
- req,
352
- });
353
- if (rendered)
354
- responses.push(
355
- wrapSegment(
356
- wrapCard(
357
- tool.skill.skill_label || tool.skill.constructor.skill_name,
358
- rendered
359
- ),
360
- agent_label
361
- )
315
+ if (tool) {
316
+ let stop = false,
317
+ myHasResult = false;
318
+ if (stream && viewname) {
319
+ let content =
320
+ "Using skill: " + tool.skill.skill_label ||
321
+ tool.skill.constructor.skill_name;
322
+ const view = View.findOne({ name: viewname });
323
+ const pageLoadTag = req.body.page_load_tag;
324
+ view.emitRealTimeEvent(
325
+ `STREAM_CHUNK?page_load_tag=${pageLoadTag}`,
326
+ {
327
+ content,
328
+ },
362
329
  );
363
- }
364
- hasResult = true;
365
- const result = await tool.tool.process(
366
- answer.ai_sdk
367
- ? tool_call.input
368
- : JSON.parse(tool_call.function.arguments),
369
- { req }
370
- );
371
- if (
372
- (typeof result === "object" && Object.keys(result || {}).length) ||
373
- typeof result === "string"
374
- ) {
375
- if (tool.tool.renderToolResponse) {
376
- const rendered = await tool.tool.renderToolResponse(result, {
330
+ }
331
+ if (tool.tool.renderToolCall) {
332
+ const row = tool_call.input;
333
+
334
+ const rendered = await tool.tool.renderToolCall(row, {
377
335
  req,
378
336
  });
379
337
  if (rendered)
380
- responses.push(
338
+ add_response(
381
339
  wrapSegment(
382
340
  wrapCard(
383
341
  tool.skill.skill_label || tool.skill.constructor.skill_name,
384
- rendered
342
+ rendered,
385
343
  ),
386
- agent_label
387
- )
344
+ agent_label,
345
+ ),
388
346
  );
389
347
  }
390
- hasResult = true;
391
- }
392
- if (answer.ai_sdk)
393
- await addToContext(run, {
394
- interactions: [
395
- {
396
- role: "tool",
397
- content: [
398
- {
399
- type: "tool-result",
400
- toolCallId: tool_call.toolCallId,
401
- toolName: tool_call.toolName,
402
- output:
403
- !result || typeof result === "string"
404
- ? {
405
- type: "text",
406
- value: result || "Action run",
407
- }
408
- : {
409
- type: "json",
410
- value: JSON.parse(JSON.stringify(result)),
411
- },
412
- },
413
- ],
414
- },
415
- ],
348
+ myHasResult = true;
349
+ const result = await tool.tool.process(tool_call.input, {
350
+ req,
416
351
  });
417
- else
352
+ if (result.stop) stop = true;
353
+ if (
354
+ (typeof result === "object" && Object.keys(result || {}).length) ||
355
+ typeof result === "string"
356
+ ) {
357
+ if (tool.tool.renderToolResponse) {
358
+ const rendered = await tool.tool.renderToolResponse(result, {
359
+ req,
360
+ });
361
+ if (rendered)
362
+ add_responses(
363
+ wrapSegment(
364
+ wrapCard(
365
+ tool.skill.skill_label ||
366
+ tool.skill.constructor.skill_name,
367
+ rendered,
368
+ ),
369
+ agent_label,
370
+ ),
371
+ );
372
+ }
373
+ myHasResult = true;
374
+ }
375
+ await sysState.functions.llm_add_message.run(
376
+ "tool_response",
377
+ !result || typeof result === "string"
378
+ ? {
379
+ type: "text",
380
+ value: result || "Action run",
381
+ }
382
+ : {
383
+ type: "json",
384
+ value: JSON.parse(JSON.stringify(result)),
385
+ },
386
+ {
387
+ chat: run.context.interactions,
388
+ tool_call,
389
+ },
390
+ );
391
+
418
392
  await addToContext(run, {
419
- interactions: [
420
- {
421
- role: "tool",
422
- tool_call_id: tool_call.toolCallId || tool_call.id,
423
- call_id: tool_call.call_id,
424
- name: tool_call.toolName || tool_call.function.name,
425
- content:
426
- result && typeof result !== "string"
427
- ? JSON.stringify(result)
428
- : result || "Action run",
429
- },
430
- ],
393
+ interactions: run.context.interactions,
431
394
  });
395
+ if (tool.tool.postProcess && !stop) {
396
+ const chat = run.context.interactions;
397
+ let generateUsed = false;
398
+ const postprocres = await tool.tool.postProcess({
399
+ tool_call,
400
+ result,
401
+ chat,
402
+ req,
403
+ async generate(prompt, opts = {}) {
404
+ generateUsed = true;
405
+ return await sysState.functions.llm_generate.run(prompt, {
406
+ chat,
407
+ appendToChat: true,
408
+ ...opts,
409
+ });
410
+ },
411
+ });
412
+ if (generateUsed)
413
+ await addToContext(run, {
414
+ interactions: chat,
415
+ });
416
+ if (postprocres.stop) stop = true;
417
+ if (postprocres.add_system_prompt)
418
+ await addToContext(run, {
419
+ interactions: [
420
+ ...chat,
421
+ { role: "system", content: postprocres.add_system_prompt },
422
+ ],
423
+ });
424
+ if (postprocres.add_response)
425
+ add_response(
426
+ wrapSegment(
427
+ wrapCard(
428
+ tool.skill.skill_label || tool.skill.constructor.skill_name,
429
+ postprocres.add_response,
430
+ ),
431
+ agent_label,
432
+ ),
433
+ );
434
+ if (postprocres.add_user_action && viewname) {
435
+ const user_actions = Array.isArray()
436
+ ? postprocres.add_user_action
437
+ : [postprocres.add_user_action];
438
+ for (const uact of user_actions) {
439
+ uact.rndid = Math.floor(Math.random() * 16777215).toString(16);
440
+ uact.tool_call = tool_call;
441
+ }
442
+ await addToContext(run, {
443
+ user_actions,
444
+ });
445
+ add_response(
446
+ div(
447
+ { class: "d-flex mb-2" },
448
+ user_actions.map((ua) =>
449
+ button(
450
+ {
451
+ class: "btn btn-primary", //press_store_button(this, true);
452
+ onclick: `view_post('${viewname}', 'execute_user_action', {uaname: "${ua.name}",rndid: "${ua.rndid}", run_id: ${run.id}}, processExecuteResponse)`,
453
+ },
454
+ ua.label,
455
+ ),
456
+ ),
457
+ ),
458
+ );
459
+ }
460
+ }
461
+ if (myHasResult && !stop) hasResult = true;
462
+ }
432
463
  }
433
- }
434
464
  if (hasResult)
435
465
  return await process_interaction(
436
466
  run,
@@ -439,13 +469,14 @@ const process_interaction = async (
439
469
  agent_label,
440
470
  [...prevResponses, ...responses],
441
471
  triggering_row,
442
- agentsViewCfg
472
+ agentsViewCfg,
473
+ dyn_updates,
443
474
  );
444
475
  } else if (typeof answer === "string")
445
- responses.push(
476
+ add_response(
446
477
  req.disable_markdown_render
447
478
  ? answer
448
- : wrapSegment(md.render(answer), agent_label)
479
+ : wrapSegment(md.render(answer), agent_label),
449
480
  );
450
481
 
451
482
  return {
package/index.js CHANGED
@@ -25,97 +25,15 @@ module.exports = {
25
25
  }/markdown-it.min.js`,
26
26
  onlyViews: ["Agent Chat"],
27
27
  },
28
+ {
29
+ script: `/plugins/public/agents@${
30
+ require("./package.json").version
31
+ }/jquery.autogrow-textarea.js`,
32
+ onlyViews: ["Agent Chat"],
33
+ },
28
34
  ],
29
35
  actions: {
30
- Agent: {
31
- disableInBuilder: true,
32
- disableInList: true,
33
- disableInWorkflow: true,
34
- configFields: async ({ table, mode }) => {
35
- const skills = get_skills();
36
- const skills_fields = [];
37
- for (const skill of skills) {
38
- if (skill.configFields) {
39
- const fields = await applyAsync(skill.configFields, undefined);
40
- for (const field of fields) {
41
- if (!field.showIf) field.showIf = {};
42
- field.showIf.skill_type = skill.skill_name;
43
- skills_fields.push(field);
44
- }
45
- }
46
- }
47
- const tables_with_json_field = (await Table.find({})).filter(
48
- (table) =>
49
- !table.external &&
50
- !table.provider_name &&
51
- table.fields.some((f) => f.type?.name === "JSON")
52
- );
53
- return [
54
- ...(table
55
- ? [
56
- {
57
- name: "prompt",
58
- label: "Prompt",
59
- sublabel:
60
- "When triggered from table event or table view button. Use handlebars <code>{{}}</code> to access table fields. Ignored if run in Agent Chat view.",
61
- type: "String",
62
- required: true,
63
- attributes: { options: table.fields.map((f) => f.name) },
64
- },
65
- ]
66
- : []),
67
- {
68
- name: "sys_prompt",
69
- label: "System prompt",
70
- sublabel:
71
- "Additional information for the system prompt. Use interpolations <code>{{ }}</code> to access triggering row variables or user",
72
- type: "String",
73
- fieldview: "textarea",
74
- },
75
- {
76
- name: "model",
77
- label: "Model",
78
- sublabel: "Override default model name",
79
- type: "String",
80
- },
81
- new FieldRepeat({
82
- name: "skills",
83
- label: "Skills",
84
- fields: [
85
- {
86
- name: "skill_type",
87
- label: "Type",
88
- type: "String",
89
- required: true,
90
- attributes: { options: skills.map((s) => s.skill_name) },
91
- },
92
- ...skills_fields,
93
- ],
94
- }),
95
- ];
96
- },
97
- run: async ({ configuration, user, row, trigger_id, req, ...rest }) => {
98
- const userinput = interpolate(configuration.prompt, row, user);
99
- const run = await WorkflowRun.create({
100
- status: "Running",
101
- started_by: user?.id,
102
- trigger_id: trigger_id || undefined,
103
- context: {
104
- implemented_fcall_ids: [],
105
- interactions: [{ role: "user", content: userinput }],
106
- funcalls: {},
107
- },
108
- });
109
- return await process_interaction(
110
- run,
111
- configuration,
112
- req,
113
- undefined,
114
- [],
115
- row
116
- );
117
- },
118
- },
36
+ Agent: require("./action"),
119
37
  },
120
38
  functions: {
121
39
  agent_generate: {
@@ -153,7 +71,7 @@ module.exports = {
153
71
  ? opts.disable_markdown_render
154
72
  : !opts?.render_markdown,
155
73
  },
156
- null
74
+ null,
157
75
  );
158
76
  return {
159
77
  text: result.json.response,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saltcorn/agents",
3
- "version": "0.5.8",
3
+ "version": "0.6.1",
4
4
  "description": "AI agents for Saltcorn",
5
5
  "main": "index.js",
6
6
  "dependencies": {
@@ -15,6 +15,12 @@
15
15
  "author": "Tom Nielsen",
16
16
  "license": "MIT",
17
17
  "repository": "github:saltcorn/agents",
18
+ "devDependencies": {
19
+ "jest": "^29.7.0"
20
+ },
21
+ "scripts": {
22
+ "test": "jest tests --runInBand"
23
+ },
18
24
  "eslintConfig": {
19
25
  "extends": "eslint:recommended",
20
26
  "parserOptions": {
@@ -22,7 +28,8 @@
22
28
  },
23
29
  "env": {
24
30
  "node": true,
25
- "es6": true
31
+ "es6": true,
32
+ "jest/globals": true
26
33
  },
27
34
  "rules": {
28
35
  "no-unused-vars": "off",