@saltcorn/agents 0.1.0 → 0.1.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/README.md CHANGED
@@ -1,2 +1,10 @@
1
1
  # agents
2
+
2
3
  AI Agents for Saltcorn
4
+
5
+ To use this plugin:
6
+
7
+ 1. Create an action of type Agent. Configure with system prompt and skills
8
+ 2. Create a view of type Agent chat, picking that agent in the configuration
9
+
10
+ The module is currently unstable. Some skills will be moved to other modules which will have to be installed as well.
package/agent-view.js CHANGED
@@ -33,6 +33,10 @@ const {
33
33
  incompleteCfgMsg,
34
34
  getCompletionArguments,
35
35
  find_tool,
36
+ addToContext,
37
+ wrapCard,
38
+ wrapSegment,
39
+ process_interaction,
36
40
  } = require("./common");
37
41
  const MarkdownIt = require("markdown-it"),
38
42
  md = new MarkdownIt();
@@ -72,6 +76,12 @@ const configuration_workflow = (req) =>
72
76
  label: "Show previous runs",
73
77
  type: "Bool",
74
78
  },
79
+ {
80
+ name: "placeholder",
81
+ label: "Placeholder",
82
+ type: "String",
83
+ default: "How can I help you?"
84
+ },
75
85
  ],
76
86
  });
77
87
  },
@@ -84,7 +94,7 @@ const get_state_fields = () => [];
84
94
  const run = async (
85
95
  table_id,
86
96
  viewname,
87
- { action_id, show_prev_runs },
97
+ { action_id, show_prev_runs, placeholder },
88
98
  state,
89
99
  { res, req }
90
100
  ) => {
@@ -120,7 +130,7 @@ const run = async (
120
130
  interactMarkups.push(
121
131
  div(
122
132
  { class: "interaction-segment" },
123
- span({ class: "badge bg-secondary" }, "Copilot"),
133
+ span({ class: "badge bg-secondary" }, action.name),
124
134
  typeof interact.content === "string"
125
135
  ? md.render(interact.content)
126
136
  : interact.content
@@ -146,7 +156,7 @@ const run = async (
146
156
  toolSkill.skill.constructor.skill_name,
147
157
  rendered
148
158
  ),
149
- "Copilot"
159
+ action.name
150
160
  )
151
161
  );
152
162
  }
@@ -156,7 +166,7 @@ const run = async (
156
166
  interactMarkups.push(
157
167
  div(
158
168
  { class: "interaction-segment" },
159
- span({ class: "badge bg-secondary" }, "Copilot"),
169
+ span({ class: "badge bg-secondary" }, action.name),
160
170
  typeof interact.content === "string"
161
171
  ? md.render(interact.content)
162
172
  : interact.content
@@ -188,7 +198,7 @@ const run = async (
188
198
  interact.name,
189
199
  markupContent
190
200
  ),
191
- "Copilot"
201
+ action.name
192
202
  )
193
203
  );
194
204
  }
@@ -220,7 +230,7 @@ const run = async (
220
230
  class: "form-control",
221
231
  name: "userinput",
222
232
  "data-fieldname": "userinput",
223
- placeholder: "How can I help you?",
233
+ placeholder: placeholder || "How can I help you?",
224
234
  id: "inputuserinput",
225
235
  rows: "3",
226
236
  autofocus: true,
@@ -406,7 +416,9 @@ const interact = async (table_id, viewname, config, body, { req, res }) => {
406
416
  interactions: [{ role: "user", content: userinput }],
407
417
  });
408
418
  }
409
- return await process_interaction(run, config, req);
419
+ const action = await Trigger.findOne({ id: config.action_id });
420
+
421
+ return await process_interaction(run, action.configuration, req, action.name);
410
422
  };
411
423
 
412
424
  const delprevrun = async (table_id, viewname, config, body, { req, res }) => {
@@ -420,130 +432,6 @@ const delprevrun = async (table_id, viewname, config, body, { req, res }) => {
420
432
  return;
421
433
  };
422
434
 
423
- const process_interaction = async (run, config, req, prevResponses = []) => {
424
- const action = await Trigger.findOne({ id: config.action_id });
425
- const complArgs = await getCompletionArguments(action.configuration);
426
- complArgs.chat = run.context.interactions;
427
- //complArgs.debugResult = true;
428
- console.log("complArgs", JSON.stringify(complArgs, null, 2));
429
-
430
- const answer = await getState().functions.llm_generate.run("", complArgs);
431
- console.log("answer", answer);
432
- await addToContext(run, {
433
- interactions:
434
- typeof answer === "object" && answer.tool_calls
435
- ? [
436
- {
437
- role: "assistant",
438
- tool_calls: answer.tool_calls,
439
- content: answer.content,
440
- },
441
- ]
442
- : [{ role: "assistant", content: answer }],
443
- });
444
- const responses = [];
445
-
446
- if (typeof answer === "object" && answer.tool_calls) {
447
- if (answer.content)
448
- responses.push(wrapSegment(md.render(answer.content), "Copilot"));
449
- //const actions = [];
450
- let hasResult = false;
451
- for (const tool_call of answer.tool_calls) {
452
- console.log("call function", tool_call.function);
453
-
454
- await addToContext(run, {
455
- funcalls: { [tool_call.id]: tool_call.function },
456
- });
457
-
458
- const tool = find_tool(tool_call.function.name, action.configuration);
459
-
460
- if (tool) {
461
- if (tool.tool.renderToolCall) {
462
- const row = JSON.parse(tool_call.function.arguments);
463
- const rendered = await tool.tool.renderToolCall(row, {
464
- req,
465
- });
466
- if (rendered)
467
- responses.push(
468
- wrapSegment(
469
- wrapCard(
470
- tool.skill.skill_label || tool.skill.constructor.skill_name,
471
- rendered
472
- ),
473
- "Copilot"
474
- )
475
- );
476
- }
477
- hasResult = true;
478
- const result = await tool.tool.process(
479
- JSON.parse(tool_call.function.arguments)
480
- );
481
- if (
482
- (typeof result === "object" && Object.keys(result || {}).length) ||
483
- typeof result === "string"
484
- ) {
485
- if (tool.tool.renderToolResponse) {
486
- const rendered = await tool.tool.renderToolResponse(result, {
487
- req,
488
- });
489
- if (rendered)
490
- responses.push(
491
- wrapSegment(
492
- wrapCard(
493
- tool.skill.skill_label || tool.skill.constructor.skill_name,
494
- rendered
495
- ),
496
- "Copilot"
497
- )
498
- );
499
- }
500
- hasResult = true;
501
- }
502
- await addToContext(run, {
503
- interactions: [
504
- {
505
- role: "tool",
506
- tool_call_id: tool_call.id,
507
- name: tool_call.function.name,
508
- content:
509
- result && typeof result !== "string"
510
- ? JSON.stringify(result)
511
- : result || "Action run",
512
- },
513
- ],
514
- });
515
- }
516
- }
517
- if (hasResult)
518
- return await process_interaction(run, config, req, [
519
- ...prevResponses,
520
- ...responses,
521
- ]);
522
- } else responses.push(wrapSegment(md.render(answer), "Copilot"));
523
-
524
- return {
525
- json: {
526
- success: "ok",
527
- response: [...prevResponses, ...responses].join(""),
528
- run_id: run.id,
529
- },
530
- };
531
- };
532
-
533
- const wrapSegment = (html, who) =>
534
- '<div class="interaction-segment"><span class="badge bg-secondary">' +
535
- who +
536
- "</span>" +
537
- html +
538
- "</div>";
539
-
540
- const wrapCard = (title, ...inners) =>
541
- span({ class: "badge bg-info ms-1" }, title) +
542
- div(
543
- { class: "card mb-3 bg-secondary-subtle" },
544
- div({ class: "card-body" }, inners)
545
- );
546
-
547
435
  const wrapAction = (
548
436
  inner_markup,
549
437
  viewname,
@@ -575,28 +463,6 @@ const wrapAction = (
575
463
  ) + div({ id: "postexec-" + tool_call.id })
576
464
  );
577
465
 
578
- const addToContext = async (run, newCtx) => {
579
- if (run.addToContext) return await run.addToContext(newCtx);
580
- let changed = true;
581
- Object.keys(newCtx).forEach((k) => {
582
- if (Array.isArray(run.context[k])) {
583
- if (!Array.isArray(newCtx[k]))
584
- throw new Error("Must be array to append to array");
585
- run.context[k].push(...newCtx[k]);
586
- changed = true;
587
- } else if (typeof run.context[k] === "object") {
588
- if (typeof newCtx[k] !== "object")
589
- throw new Error("Must be object to append to object");
590
- Object.assign(run.context[k], newCtx[k]);
591
- changed = true;
592
- } else {
593
- run.context[k] = newCtx[k];
594
- changed = true;
595
- }
596
- });
597
- if (changed) await run.update({ context: run.context });
598
- };
599
-
600
466
  module.exports = {
601
467
  name: "Agent Chat",
602
468
  configuration_workflow,
package/common.js CHANGED
@@ -1,9 +1,14 @@
1
1
  const { getState } = require("@saltcorn/data/db/state");
2
+ const { div, span } = require("@saltcorn/markup/tags");
3
+ const Trigger = require("@saltcorn/data/models/trigger");
4
+
5
+ const MarkdownIt = require("markdown-it"),
6
+ md = new MarkdownIt();
2
7
 
3
8
  const get_skills = () => {
4
9
  return [
5
- //require("./skills/EmbeddingRetrieval"),
6
10
  require("./skills/FTSRetrieval"),
11
+ require("./skills/EmbeddingRetrieval"),
7
12
  //require("./skills/AdaptiveFeedback"),
8
13
  ];
9
14
  };
@@ -37,7 +42,6 @@ const find_tool = (name, config) => {
37
42
  }
38
43
  };
39
44
 
40
-
41
45
  const getCompletionArguments = async (config) => {
42
46
  let tools = [];
43
47
 
@@ -55,7 +59,6 @@ const getCompletionArguments = async (config) => {
55
59
  return { tools, systemPrompt: sysPrompts.join("\n\n") };
56
60
  };
57
61
 
58
-
59
62
  const getCompletion = async (language, prompt) => {
60
63
  return getState().functions.llm_generate.run(prompt, {
61
64
  systemPrompt: `You are a helpful code assistant. Your language of choice is ${language}. Do not include any explanation, just generate the code block itself.`,
@@ -81,6 +84,157 @@ const incompleteCfgMsg = () => {
81
84
  }
82
85
  };
83
86
 
87
+ const addToContext = async (run, newCtx) => {
88
+ if (run.addToContext) return await run.addToContext(newCtx);
89
+ let changed = true;
90
+ Object.keys(newCtx).forEach((k) => {
91
+ if (Array.isArray(run.context[k])) {
92
+ if (!Array.isArray(newCtx[k]))
93
+ throw new Error("Must be array to append to array");
94
+ run.context[k].push(...newCtx[k]);
95
+ changed = true;
96
+ } else if (typeof run.context[k] === "object") {
97
+ if (typeof newCtx[k] !== "object")
98
+ throw new Error("Must be object to append to object");
99
+ Object.assign(run.context[k], newCtx[k]);
100
+ changed = true;
101
+ } else {
102
+ run.context[k] = newCtx[k];
103
+ changed = true;
104
+ }
105
+ });
106
+ if (changed) await run.update({ context: run.context });
107
+ };
108
+
109
+ const wrapSegment = (html, who) =>
110
+ '<div class="interaction-segment"><span class="badge bg-secondary">' +
111
+ who +
112
+ "</span>" +
113
+ html +
114
+ "</div>";
115
+
116
+ const wrapCard = (title, ...inners) =>
117
+ span({ class: "badge bg-info ms-1" }, title) +
118
+ div(
119
+ { class: "card mb-3 bg-secondary-subtle" },
120
+ div({ class: "card-body" }, inners)
121
+ );
122
+
123
+ const process_interaction = async (
124
+ run,
125
+ config,
126
+ req,
127
+ agent_label = "Copilot",
128
+ prevResponses = []
129
+ ) => {
130
+ const complArgs = await getCompletionArguments(config);
131
+ complArgs.chat = run.context.interactions;
132
+ //complArgs.debugResult = true;
133
+ console.log("complArgs", JSON.stringify(complArgs, null, 2));
134
+
135
+ const answer = await getState().functions.llm_generate.run("", complArgs);
136
+ console.log("answer", answer);
137
+ await addToContext(run, {
138
+ interactions:
139
+ typeof answer === "object" && answer.tool_calls
140
+ ? [
141
+ {
142
+ role: "assistant",
143
+ tool_calls: answer.tool_calls,
144
+ content: answer.content,
145
+ },
146
+ ]
147
+ : [{ role: "assistant", content: answer }],
148
+ });
149
+ const responses = [];
150
+
151
+ if (typeof answer === "object" && answer.tool_calls) {
152
+ if (answer.content)
153
+ responses.push(wrapSegment(md.render(answer.content), agent_label));
154
+ //const actions = [];
155
+ let hasResult = false;
156
+ for (const tool_call of answer.tool_calls) {
157
+ console.log("call function", tool_call.function);
158
+
159
+ await addToContext(run, {
160
+ funcalls: { [tool_call.id]: tool_call.function },
161
+ });
162
+
163
+ const tool = find_tool(tool_call.function.name, config);
164
+
165
+ if (tool) {
166
+ if (tool.tool.renderToolCall) {
167
+ const row = JSON.parse(tool_call.function.arguments);
168
+ const rendered = await tool.tool.renderToolCall(row, {
169
+ req,
170
+ });
171
+ if (rendered)
172
+ responses.push(
173
+ wrapSegment(
174
+ wrapCard(
175
+ tool.skill.skill_label || tool.skill.constructor.skill_name,
176
+ rendered
177
+ ),
178
+ agent_label
179
+ )
180
+ );
181
+ }
182
+ hasResult = true;
183
+ const result = await tool.tool.process(
184
+ JSON.parse(tool_call.function.arguments)
185
+ );
186
+ if (
187
+ (typeof result === "object" && Object.keys(result || {}).length) ||
188
+ typeof result === "string"
189
+ ) {
190
+ if (tool.tool.renderToolResponse) {
191
+ const rendered = await tool.tool.renderToolResponse(result, {
192
+ req,
193
+ });
194
+ if (rendered)
195
+ responses.push(
196
+ wrapSegment(
197
+ wrapCard(
198
+ tool.skill.skill_label || tool.skill.constructor.skill_name,
199
+ rendered
200
+ ),
201
+ agent_label
202
+ )
203
+ );
204
+ }
205
+ hasResult = true;
206
+ }
207
+ await addToContext(run, {
208
+ interactions: [
209
+ {
210
+ role: "tool",
211
+ tool_call_id: tool_call.id,
212
+ name: tool_call.function.name,
213
+ content:
214
+ result && typeof result !== "string"
215
+ ? JSON.stringify(result)
216
+ : result || "Action run",
217
+ },
218
+ ],
219
+ });
220
+ }
221
+ }
222
+ if (hasResult)
223
+ return await process_interaction(run, config, req, agent_label, [
224
+ ...prevResponses,
225
+ ...responses,
226
+ ]);
227
+ } else responses.push(wrapSegment(md.render(answer), agent_label));
228
+
229
+ return {
230
+ json: {
231
+ success: "ok",
232
+ response: [...prevResponses, ...responses].join(""),
233
+ run_id: run.id,
234
+ },
235
+ };
236
+ };
237
+
84
238
  module.exports = {
85
239
  get_skills,
86
240
  get_skill_class,
@@ -88,5 +242,9 @@ module.exports = {
88
242
  getCompletion,
89
243
  find_tool,
90
244
  get_skill_instances,
91
- getCompletionArguments
245
+ getCompletionArguments,
246
+ addToContext,
247
+ wrapCard,
248
+ wrapSegment,
249
+ process_interaction,
92
250
  };
package/index.js CHANGED
@@ -2,7 +2,11 @@ const Workflow = require("@saltcorn/data/models/workflow");
2
2
  const Form = require("@saltcorn/data/models/form");
3
3
  const FieldRepeat = require("@saltcorn/data/models/fieldrepeat");
4
4
  const { features } = require("@saltcorn/data/db/state");
5
- const { get_skills, getCompletionArguments } = require("./common");
5
+ const {
6
+ get_skills,
7
+ getCompletionArguments,
8
+ process_interaction,
9
+ } = require("./common");
6
10
  const { applyAsync } = require("@saltcorn/data/utils");
7
11
  const WorkflowRun = require("@saltcorn/data/models/workflow_run");
8
12
  const { interpolate } = require("@saltcorn/data/utils");
@@ -68,7 +72,7 @@ module.exports = {
68
72
  }),
69
73
  ];
70
74
  },
71
- run: async ({ configuration, user, row, trigger_id, ...rest }) => {
75
+ run: async ({ configuration, user, row, trigger_id, req, ...rest }) => {
72
76
  const userinput = interpolate(configuration.prompt, row, user);
73
77
  const run = await WorkflowRun.create({
74
78
  status: "Running",
@@ -80,11 +84,7 @@ module.exports = {
80
84
  funcalls: {},
81
85
  },
82
86
  });
83
- const complArgs = await getCompletionArguments(configuration);
84
- const answer = await getState().functions.llm_generate.run(
85
- "",
86
- complArgs
87
- );
87
+ return await process_interaction(run, configuration, req);
88
88
  },
89
89
  },
90
90
  },
@@ -93,8 +93,9 @@ module.exports = {
93
93
  /*
94
94
  TODO
95
95
 
96
- -embedding retrieval
97
- -run as action
98
- -view: set placeholder, labels
96
+ -embedding retrieval test
97
+ -promote triggers to skills (optional user confirm)
98
+ -sql access
99
+ -memory
99
100
 
100
101
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saltcorn/agents",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "AI agents for Saltcorn",
5
5
  "main": "index.js",
6
6
  "dependencies": {
@@ -25,7 +25,11 @@ class RetrievalByEmbedding {
25
25
  table.name
26
26
  }${
27
27
  table.description ? ` (${table.description})` : ""
28
- } for documents related to a search phrase or a question`;
28
+ } for documents related to a search phrase or a question.${
29
+ this.add_sys_prompt
30
+ ? ` Additional information for the ${this.toolName} tool: ${this.add_sys_prompt}`
31
+ : ""
32
+ }`;
29
33
  }
30
34
  }
31
35
 
@@ -33,7 +37,6 @@ class RetrievalByEmbedding {
33
37
  const allTables = await Table.find();
34
38
  const tableOpts = [];
35
39
  const relation_opts = {};
36
- const list_view_opts = {};
37
40
  for (const table of allTables) {
38
41
  table.fields
39
42
  .filter((f) => f.type?.name === "PGVector")
@@ -44,8 +47,6 @@ class RetrievalByEmbedding {
44
47
  .filter((f) => f.is_fkey)
45
48
  .map((f) => f.name);
46
49
  relation_opts[relNm] = ["", ...fkeys];
47
-
48
- list_view_opts[relNm] = []
49
50
  });
50
51
  }
51
52
  return [
@@ -73,20 +74,22 @@ class RetrievalByEmbedding {
73
74
  required: true,
74
75
  attributes: { calcOptions: ["vec_field", relation_opts] },
75
76
  },
76
- {
77
- name: "list_view",
78
- label: "List view",
77
+ {
78
+ name: "hidden_fields",
79
+ label: "Hide fields",
79
80
  type: "String",
80
- attributes: {
81
- calcOptions: ["vec_field", list_view_opts],
82
- },
81
+ sublabel: "Comma-separated list of fields to hide from the prompt",
83
82
  },
84
83
  {
85
- name: "contents_expr",
86
- label: "Contents string",
84
+ name: "limit",
85
+ label: "Limit",
86
+ sublabel: "Max number of rows to find",
87
+ type: "String",
88
+ },
89
+ {
90
+ name: "add_sys_prompt",
91
+ label: "Additional prompt",
87
92
  type: "String",
88
- sublabel:
89
- "Use handlebars (<code>{{ }}</code>) to access fields in the retrieved rows",
90
93
  },
91
94
  ];
92
95
  }
@@ -103,28 +106,51 @@ class RetrievalByEmbedding {
103
106
  : table0;
104
107
  return {
105
108
  type: "function",
106
- process({ phrase_or_question }) {
107
- return {
108
- response: "There are no documents related to: " + phrase_or_question,
109
- };
110
- },
111
- renderToolResponse: async ({ response, rows }, { req }) => {
112
- if (rows) {
113
- const view = View.findOne({ name: this.list_view });
114
-
115
- if (view) {
116
- const viewRes = await view.run(
117
- {
118
- [table_docs.pk_name]: {
119
- in: rows.map((r) => r[table_docs.pk_name]),
120
- },
121
- },
122
- { req }
123
- );
124
- return viewRes;
125
- } else return "";
109
+ process: async ({ phrase_or_question }) => {
110
+ const [table_name, field_name] = this.vec_field.split(".");
111
+ const table = Table.findOne({ name: table_name });
112
+ if (!table)
113
+ throw new Error(
114
+ `Table ${table_name} not found in vector_similarity_search action`
115
+ );
116
+ const embedF = getState().functions.llm_embedding;
117
+ const opts = {};
118
+ const qembed = await embedF.run(phrase_or_question, opts);
119
+ const selLimit = +(this.limit || 10);
120
+ const rows = await table.getRows(
121
+ {},
122
+ {
123
+ orderBy: {
124
+ operator: "nearL2",
125
+ field: field_name,
126
+ target: JSON.stringify(qembed),
127
+ },
128
+ limit: this.doc_relation ? 5 * selLimit : selLimit,
129
+ }
130
+ );
131
+ rows.forEach((r) => {
132
+ delete r[field_name];
133
+ });
134
+ if (!rows.length)
135
+ return {
136
+ response:
137
+ "There are no documents related to: " + phrase_or_question,
138
+ };
139
+ else if (!this.doc_relation) return { rows };
140
+ else {
141
+ const relField = table.getField(this.doc_relation);
142
+ const relTable = Table.findOne(relField.reftable_name);
143
+ const ids = [];
144
+ rows.forEach((vrow) => {
145
+ if (ids.length < selLimit) ids.push(vrow[this.doc_relation]);
146
+ });
147
+ const docsUnsorted = await relTable.getRows({ id: { in: ids } });
148
+ //ensure order
149
+ const docs = ids
150
+ .map((id) => docsUnsorted.find((d) => d.id == id))
151
+ .filter(Boolean);
152
+ return { rows: docs };
126
153
  }
127
- return div({ class: "border border-success p-2 m-2" }, response);
128
154
  },
129
155
  function: {
130
156
  name: this.toolName,
@@ -23,7 +23,13 @@ class RetrievalByFullTextSearch {
23
23
 
24
24
  systemPrompt() {
25
25
  if (this.mode === "Tool")
26
- return `Use the ${this.toolName} tool to search the ${this.table_name} database by a search phrase which will locate rows where any field match that query`;
26
+ return `Use the ${this.toolName} tool to search the ${
27
+ this.table_name
28
+ } database by a search phrase which will locate rows where any field match that query.${
29
+ this.add_sys_prompt
30
+ ? ` Additional information for the ${this.toolName} tool: ${this.add_sys_prompt}`
31
+ : ""
32
+ }`;
27
33
  }
28
34
 
29
35
  static async configFields() {
@@ -68,6 +74,11 @@ class RetrievalByFullTextSearch {
68
74
  type: "String",
69
75
  sublabel: "Comma-separated list of fields to hide from the prompt",
70
76
  },
77
+ {
78
+ name: "add_sys_prompt",
79
+ label: "Additional prompt",
80
+ type: "String",
81
+ },
71
82
  /*{
72
83
  name: "contents_expr",
73
84
  label: "Contents string",