@saltcorn/agents 0.4.0 → 0.4.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
@@ -42,6 +42,7 @@ const {
42
42
  process_interaction,
43
43
  find_image_tool,
44
44
  is_debug_mode,
45
+ get_initial_interactions,
45
46
  } = require("./common");
46
47
  const MarkdownIt = require("markdown-it"),
47
48
  md = new MarkdownIt();
@@ -81,6 +82,11 @@ const configuration_workflow = (req) =>
81
82
  label: "Show previous runs",
82
83
  type: "Bool",
83
84
  },
85
+ {
86
+ name: "prev_runs_closed",
87
+ label: "Initially closed",
88
+ type: "Bool",
89
+ },
84
90
  {
85
91
  name: "placeholder",
86
92
  label: "Placeholder",
@@ -148,7 +154,14 @@ const uploadForm = (viewname, req) =>
148
154
  const run = async (
149
155
  table_id,
150
156
  viewname,
151
- { action_id, show_prev_runs, placeholder, explainer, image_upload },
157
+ {
158
+ action_id,
159
+ show_prev_runs,
160
+ prev_runs_closed,
161
+ placeholder,
162
+ explainer,
163
+ image_upload,
164
+ },
152
165
  state,
153
166
  { res, req }
154
167
  ) => {
@@ -171,6 +184,7 @@ const run = async (
171
184
  //triggering_row = await table.getRow({ [pk]: state[pk] });
172
185
  triggering_row_id = state[pk];
173
186
  }
187
+ const initial_q = state.run_id ? undefined : state._q;
174
188
  if (state.run_id) {
175
189
  const run = prevRuns.find((r) => r.id == state.run_id);
176
190
  const interactMarkups = [];
@@ -328,15 +342,18 @@ const run = async (
328
342
  }),
329
343
  div(
330
344
  { class: "copilot-entry" },
331
- textarea({
332
- class: "form-control",
333
- name: "userinput",
334
- "data-fieldname": "userinput",
335
- placeholder: placeholder || "How can I help you?",
336
- id: "inputuserinput",
337
- rows: "3",
338
- autofocus: true,
339
- }),
345
+ textarea(
346
+ {
347
+ class: "form-control",
348
+ name: "userinput",
349
+ "data-fieldname": "userinput",
350
+ placeholder: placeholder || "How can I help you?",
351
+ id: "inputuserinput",
352
+ rows: "3",
353
+ autofocus: true,
354
+ },
355
+ initial_q
356
+ ),
340
357
  span(
341
358
  { class: "submit-button p-2", onclick: "$('form.copilot').submit()" },
342
359
  i({ id: "sendbuttonicon", class: "far fa-paper-plane" })
@@ -356,8 +373,14 @@ const run = async (
356
373
  {
357
374
  class: "d-flex justify-content-between align-middle mb-2",
358
375
  },
359
- h5("Sessions"),
360
-
376
+ div(
377
+ { class: "d-flex" },
378
+ i({
379
+ class: "fas fa-caret-down me-1 session-open-sessions",
380
+ onclick: "close_session_list()",
381
+ }),
382
+ h5(req.__("Sessions"))
383
+ ),
361
384
  button(
362
385
  {
363
386
  type: "button",
@@ -392,6 +415,17 @@ const run = async (
392
415
  { class: "card" },
393
416
  div(
394
417
  { class: "card-body" },
418
+ div(
419
+ {
420
+ class: "open-prev-runs",
421
+ style: prev_runs_closed ? {} : { display: "none" },
422
+ onclick: "open_session_list()",
423
+ },
424
+ i({
425
+ class: "fas fa-caret-right me-1",
426
+ }),
427
+ req.__("Sessions")
428
+ ),
395
429
  div({ id: "copilotinteractions" }, runInteractions),
396
430
  input_form,
397
431
  style(
@@ -416,6 +450,9 @@ const run = async (
416
450
  left: 0.1rem;
417
451
  cursor: pointer;
418
452
  }
453
+ .session-open-sessions, .open-prev-runs {
454
+ cursor: pointer;
455
+ }
419
456
  .copilot-entry span.attach_agent_image_wrap {
420
457
  position: relative;
421
458
  top: -1.8rem;
@@ -426,6 +463,9 @@ const run = async (
426
463
  top: -1.2rem;
427
464
  display: block;
428
465
  }
466
+ .col-0 {
467
+ width: 0%
468
+ }
429
469
  .copilot-entry {margin-bottom: -1.25rem; margin-top: 1rem;}
430
470
  p.prevrun_content {
431
471
  white-space: nowrap;
@@ -434,7 +474,17 @@ const run = async (
434
474
  display: block;
435
475
  text-overflow: ellipsis;}`
436
476
  ),
437
- script(`function processCopilotResponse(res) {
477
+ script(
478
+ `
479
+ function close_session_list() {
480
+ $("div.prev-runs-list").hide().parents(".col-3").removeClass("col-3").addClass("was-col-3").parent().children(".col-9").removeClass("col-9").addClass("col-12")
481
+ $("div.open-prev-runs").show()
482
+ }
483
+ function open_session_list() {
484
+ $("div.prev-runs-list").show().parents(".was-col-3").removeClass(["was-col-3","col-0","d-none"]).addClass("col-3").parent().children(".col-12").removeClass("col-12").addClass("col-9")
485
+ $("div.open-prev-runs").hide()
486
+ }
487
+ function processCopilotResponse(res) {
438
488
  const hadFile = $("input#attach_agent_image").val();
439
489
  $("span.filename-label").text("");
440
490
  $("input#attach_agent_image").val(null);
@@ -506,17 +556,20 @@ const run = async (
506
556
  document.getElementById("inputuserinput").addEventListener("keydown", submitOnEnter);
507
557
  function spin_send_button() {
508
558
  $("#sendbuttonicon").attr("class","fas fa-spinner fa-spin");
509
- }
510
- `)
559
+ };`,
560
+ initial_q && domReady("$('form.copilot').submit()")
561
+ )
511
562
  )
512
563
  );
513
564
  return show_prev_runs
514
565
  ? {
515
- widths: [3, 9],
566
+ widths: prev_runs_closed ? [0, 12] : [3, 9],
567
+ colClasses: prev_runs_closed ? ["was-col-3 d-none"] : undefined,
516
568
  gx: 3,
517
569
  besides: [
518
570
  {
519
571
  type: "container",
572
+ customClass: "prev-runs-list",
520
573
  contents: prev_runs_side_bar,
521
574
  },
522
575
  {
@@ -530,6 +583,8 @@ const run = async (
530
583
 
531
584
  const interact = async (table_id, viewname, config, body, { req, res }) => {
532
585
  const { userinput, run_id, triggering_row_id } = body;
586
+ const action = await Trigger.findOne({ id: config.action_id });
587
+
533
588
  let run;
534
589
  let triggering_row;
535
590
  if (table_id && triggering_row_id) {
@@ -537,19 +592,24 @@ const interact = async (table_id, viewname, config, body, { req, res }) => {
537
592
  const pk = table?.pk_name;
538
593
  if (table) triggering_row = await table.getRow({ [pk]: triggering_row_id });
539
594
  }
540
- if (!run_id || run_id === "undefined")
595
+ if (!run_id || run_id === "undefined") {
596
+ const ini_interacts = await get_initial_interactions(
597
+ action.configuration,
598
+ req.user,
599
+ triggering_row
600
+ );
541
601
  run = await WorkflowRun.create({
542
602
  status: "Running",
543
603
  started_by: req.user?.id,
544
604
  trigger_id: config.action_id,
545
605
  context: {
546
606
  implemented_fcall_ids: [],
547
- interactions: [{ role: "user", content: userinput }],
607
+ interactions: [...ini_interacts, { role: "user", content: userinput }],
548
608
  funcalls: {},
549
609
  triggering_row_id,
550
610
  },
551
611
  });
552
- else {
612
+ } else {
553
613
  run = await WorkflowRun.findOne({ id: +run_id });
554
614
  await addToContext(run, {
555
615
  interactions: [{ role: "user", content: userinput }],
@@ -590,7 +650,6 @@ const interact = async (table_id, viewname, config, body, { req, res }) => {
590
650
  ],
591
651
  });
592
652
  }
593
- const action = await Trigger.findOne({ id: config.action_id });
594
653
 
595
654
  return await process_interaction(
596
655
  run,
package/common.js CHANGED
@@ -79,6 +79,16 @@ const find_image_tool = (config) => {
79
79
  }
80
80
  };
81
81
 
82
+ const get_initial_interactions = async (config, user, triggering_row) => {
83
+ const interacts = [];
84
+ const skills = get_skill_instances(config);
85
+ for (const skill of skills) {
86
+ const its = await skill.initialInteractions?.({ user, triggering_row });
87
+ if (its) interacts.push(...its);
88
+ }
89
+ return interacts;
90
+ };
91
+
82
92
  const getCompletionArguments = async (config, user, triggering_row) => {
83
93
  let tools = [];
84
94
 
@@ -359,4 +369,6 @@ module.exports = {
359
369
  process_interaction,
360
370
  find_image_tool,
361
371
  is_debug_mode,
372
+ get_initial_interactions,
373
+ nubBy,
362
374
  };
@@ -0,0 +1 @@
1
+ { "Sessions": "Sitzungen" }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saltcorn/agents",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "AI agents for Saltcorn",
5
5
  "main": "index.js",
6
6
  "dependencies": {
@@ -6,6 +6,7 @@ const View = require("@saltcorn/data/models/view");
6
6
  const { getState } = require("@saltcorn/data/db/state");
7
7
  const db = require("@saltcorn/data/db");
8
8
  const { interpolate } = require("@saltcorn/data/utils");
9
+ const { nubBy } = require("../common");
9
10
 
10
11
  class RetrievalByFullTextSearch {
11
12
  static skill_name = "Retrieval by full-text search";
@@ -105,30 +106,45 @@ class RetrievalByFullTextSearch {
105
106
  const table = Table.findOne(this.table_name);
106
107
  return {
107
108
  type: "function",
108
- process: async ({ phrase }, { req }) => {
109
+ process: async (arg, { req }) => {
109
110
  const scState = getState();
110
111
  const language = scState.pg_ts_config;
111
112
  const use_websearch = scState.getConfig("search_use_websearch", false);
112
- const rows = await table.getRows({
113
- _fts: {
114
- fields: table.fields,
115
- searchTerm: phrase,
116
- language,
117
- use_websearch,
118
- table: table.name,
119
- schema: db.isSQLite ? undefined : db.getTenantSchema(),
120
- },
121
- });
122
- if (this.hidden_fields) {
123
- const hidden_fields = this.hidden_fields
124
- .split(",")
125
- .map((s) => s.trim());
126
- rows.forEach((r) => {
127
- hidden_fields.forEach((k) => {
128
- delete r[k];
129
- });
113
+ let rows = [];
114
+ const phrases =
115
+ arg.query?.phrases ||
116
+ arg.phrases ||
117
+ (arg.phrase
118
+ ? [arg.phrase]
119
+ : arg.query?.phrase
120
+ ? [arg.query?.phrase]
121
+ : []);
122
+ for (const phrase of phrases) {
123
+ const my_rows = await table.getRows({
124
+ _fts: {
125
+ fields: table.fields,
126
+ searchTerm: phrase,
127
+ language,
128
+ use_websearch,
129
+ table: table.name,
130
+ schema: db.isSQLite ? undefined : db.getTenantSchema(),
131
+ },
130
132
  });
133
+ if (this.hidden_fields) {
134
+ const hidden_fields = this.hidden_fields
135
+ .split(",")
136
+ .map((s) => s.trim());
137
+ my_rows.forEach((r) => {
138
+ hidden_fields.forEach((k) => {
139
+ delete r[k];
140
+ });
141
+ });
142
+ }
143
+ rows.push(...my_rows);
131
144
  }
145
+ const pk = table.pk_name;
146
+ rows = nubBy((r) => r[pk], rows);
147
+
132
148
  if (rows.length)
133
149
  if (!this.doc_format) return { rows };
134
150
  else {
@@ -139,7 +155,8 @@ class RetrievalByFullTextSearch {
139
155
  }
140
156
  else
141
157
  return {
142
- responseText: "There are no rows related to: " + phrase,
158
+ responseText:
159
+ "There are no rows related to: " + phrases.join(" or "),
143
160
  };
144
161
  },
145
162
  /*renderToolCall({ phrase }, { req }) {
@@ -163,15 +180,41 @@ class RetrievalByFullTextSearch {
163
180
  name: this.toolName,
164
181
  description: `Search the ${this.table_name} database table${
165
182
  table.description ? ` (${table.description})` : ""
166
- } by a search phrase matched against all fields in the table with full text search. The retrieved rows will be returned`,
183
+ } by one or several search phrase or multiple search phrases matched against all fields in the table with full text search. The retrieved rows will be returned. If you want to search for documents matching any of several phrases, call this tool once with the phrases argument and give all the phrases you want to search for in one tool call.`,
167
184
  parameters: {
168
185
  type: "object",
169
- required: ["phrase"],
186
+ required: ["query"],
170
187
  properties: {
171
- phrase: {
172
- type: "string",
173
- description:
174
- "The phrase to search the table with. The search phrase is the synatx used by web search engines: use double quotes for exact match, unquoted text for words in any order, dash (minus sign) to exclude a word. Do not use SQL or any other formal query language.",
188
+ query: {
189
+ anyOf: [
190
+ {
191
+ type: "object",
192
+ required: ["phrase"],
193
+ properties: {
194
+ phrase: {
195
+ type: "string",
196
+ description:
197
+ "The phrase to search the table with. The search phrase is the synatx used by web search engines: use double quotes for exact match, unquoted text for words in any order, dash (minus sign) to exclude a word. Do not use SQL or any other formal query language.",
198
+ },
199
+ },
200
+ },
201
+ {
202
+ type: "object",
203
+ required: ["phrases"],
204
+ description:
205
+ "Search the table by any of a number of phrases. This will return any document that matches one or the other of the phrases",
206
+ properties: {
207
+ phrases: {
208
+ type: "array",
209
+ description:
210
+ "A phrase to search the table with. The search phrase is the synatx used by web search engines: use double quotes for exact match, unquoted text for words in any order, dash (minus sign) to exclude a word. Do not use SQL or any other formal query language.",
211
+ items: {
212
+ type: "string",
213
+ },
214
+ },
215
+ },
216
+ },
217
+ ],
175
218
  },
176
219
  },
177
220
  },
@@ -2,11 +2,15 @@ const { div, pre } = require("@saltcorn/markup/tags");
2
2
  const Workflow = require("@saltcorn/data/models/workflow");
3
3
  const Form = require("@saltcorn/data/models/form");
4
4
  const Table = require("@saltcorn/data/models/table");
5
+ const File = require("@saltcorn/data/models/file");
6
+ const Trigger = require("@saltcorn/data/models/trigger");
7
+ const User = require("@saltcorn/data/models/user");
5
8
  const View = require("@saltcorn/data/models/view");
6
9
  const { getState } = require("@saltcorn/data/db/state");
7
10
  const db = require("@saltcorn/data/db");
8
11
  const { eval_expression } = require("@saltcorn/data/models/expression");
9
12
  const { interpolate } = require("@saltcorn/data/utils");
13
+ const vm = require("vm");
10
14
 
11
15
  class PreloadData {
12
16
  static skill_name = "Preload Data";
@@ -19,28 +23,69 @@ class PreloadData {
19
23
  Object.assign(this, cfg);
20
24
  }
21
25
 
22
- async systemPrompt({ user }) {
23
- const prompts = [];
24
- if (this.add_sys_prompt) prompts.push(this.add_sys_prompt);
25
- const table = Table.findOne(this.table_name);
26
- const q = eval_expression(
27
- this.preload_query,
28
- {},
26
+ async run_the_code({ user, triggering_row }) {
27
+ const sysState = getState();
28
+ const f = vm.runInNewContext(`async () => {${this.code}\n}`, {
29
+ Table,
30
+ row: triggering_row,
31
+ context: triggering_row,
29
32
  user,
30
- "PreloadData query"
31
- );
33
+ User,
34
+ File,
35
+ Buffer,
36
+ Trigger,
37
+ setTimeout,
38
+ interpolate,
39
+ require,
40
+ getConfig: (k) =>
41
+ sysState.isFixedConfig(k) ? undefined : sysState.getConfig(k),
42
+ ...(triggering_row || {}),
43
+ ...sysState.eval_context,
44
+ });
45
+ return await f();
46
+ }
32
47
 
33
- const rows = await table.getRows(q);
34
- if (this.contents_expr) {
35
- for (const row of rows)
36
- prompts.push(interpolate(this.contents_expr, row, user));
48
+ async initialInteractions({ user, triggering_row }) {
49
+ if (this._stashed_initial_interactions)
50
+ return this._stashed_initial_interactions;
51
+ if (this.data_source === "Code") {
52
+ const result = await this.run_the_code({ user, triggering_row });
53
+ return result?.interactions;
54
+ }
55
+ }
56
+
57
+ async systemPrompt({ user, triggering_row }) {
58
+ const prompts = [];
59
+ if (this.data_source === "Code") {
60
+ const result = await this.run_the_code({ user, triggering_row });
61
+ if (typeof result === "string") return result;
62
+ if (result?.interactions)
63
+ this._stashed_initial_interactions = result?.interactions;
64
+ if (result?.systemPrompt) return result.systemPrompt;
37
65
  } else {
38
- const hidden_fields = this.hidden_fields.split(",").map((s) => s.trim());
39
- for (const row of rows) {
40
- hidden_fields.forEach((k) => {
41
- delete row[k];
42
- });
43
- prompts.push(JSON.stringify(row));
66
+ if (this.add_sys_prompt) prompts.push(this.add_sys_prompt);
67
+ const table = Table.findOne(this.table_name);
68
+ const q = eval_expression(
69
+ this.preload_query,
70
+ {},
71
+ user,
72
+ "PreloadData query"
73
+ );
74
+
75
+ const rows = await table.getRows(q);
76
+ if (this.contents_expr) {
77
+ for (const row of rows)
78
+ prompts.push(interpolate(this.contents_expr, row, user));
79
+ } else {
80
+ const hidden_fields = this.hidden_fields
81
+ .split(",")
82
+ .map((s) => s.trim());
83
+ for (const row of rows) {
84
+ hidden_fields.forEach((k) => {
85
+ delete row[k];
86
+ });
87
+ prompts.push(JSON.stringify(row));
88
+ }
44
89
  }
45
90
  }
46
91
  return prompts.join("\n");
@@ -50,6 +95,34 @@ class PreloadData {
50
95
  const allTables = await Table.find();
51
96
 
52
97
  return [
98
+ {
99
+ name: "data_source",
100
+ label: "Data source",
101
+ type: "String",
102
+ required: true,
103
+ attributes: { options: ["Table", "Code"] },
104
+ },
105
+ {
106
+ name: "code",
107
+ label: "Code",
108
+ input_type: "code",
109
+ attributes: { mode: "application/javascript" },
110
+ showIf: { data_source: "Code" },
111
+ class: "validate-statements",
112
+ sublabel:
113
+ "Return string or object <code>{systemPrompt: string, interactions: Interaction[]}</code>",
114
+ validator(s) {
115
+ try {
116
+ let AsyncFunction = Object.getPrototypeOf(
117
+ async function () {}
118
+ ).constructor;
119
+ AsyncFunction(s);
120
+ return true;
121
+ } catch (e) {
122
+ return e.message;
123
+ }
124
+ },
125
+ },
53
126
  {
54
127
  name: "table_name",
55
128
  label: "Table",
@@ -57,17 +130,20 @@ class PreloadData {
57
130
  type: "String",
58
131
  required: true,
59
132
  attributes: { options: allTables.map((t) => t.name) },
133
+ showIf: { data_source: "Table" },
60
134
  },
61
135
  {
62
136
  name: "preload_query",
63
137
  label: "Query",
64
138
  type: "String",
65
139
  class: "validate-expression",
140
+ showIf: { data_source: "Table" },
66
141
  },
67
142
  {
68
143
  name: "add_sys_prompt",
69
144
  label: "Additional prompt",
70
145
  type: "String",
146
+ showIf: { data_source: "Table" },
71
147
  fieldview: "textarea",
72
148
  },
73
149
  {
@@ -75,6 +151,7 @@ class PreloadData {
75
151
  label: "Contents string",
76
152
  type: "String",
77
153
  fieldview: "textarea",
154
+ showIf: { data_source: "Table" },
78
155
  sublabel:
79
156
  "Use handlebars (<code>{{ }}</code>) to access fields in each retrieved row",
80
157
  },
@@ -82,6 +159,7 @@ class PreloadData {
82
159
  name: "hidden_fields",
83
160
  label: "Hide fields",
84
161
  type: "String",
162
+ showIf: { data_source: "Table" },
85
163
  sublabel:
86
164
  "Comma-separated list of fields to hide from the prompt, if not using the contents string",
87
165
  },