@saltcorn/agents 0.1.1 → 0.1.3

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
@@ -6,3 +6,5 @@ To use this plugin:
6
6
 
7
7
  1. Create an action of type Agent. Configure with system prompt and skills
8
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
@@ -45,7 +45,7 @@ const configuration_workflow = (req) =>
45
45
  new Workflow({
46
46
  steps: [
47
47
  {
48
- name: req.__("Agent action"),
48
+ name: "Agent action",
49
49
  form: async (context) => {
50
50
  const agent_actions = await Trigger.find({ action: "Agent" });
51
51
  return new Form({
@@ -68,7 +68,7 @@ const configuration_workflow = (req) =>
68
68
  "data-dyn-href": `\`/actions/configure/\${action_id}\``,
69
69
  target: "_blank",
70
70
  },
71
- req.__("Configure")
71
+ "Configure"
72
72
  ),
73
73
  },
74
74
  {
@@ -80,7 +80,14 @@ const configuration_workflow = (req) =>
80
80
  name: "placeholder",
81
81
  label: "Placeholder",
82
82
  type: "String",
83
- default: "How can I help you?"
83
+ default: "How can I help you?",
84
+ },
85
+ {
86
+ name: "explainer",
87
+ label: "Explainer",
88
+ type: "String",
89
+ sublabel:
90
+ "Appears below the input box. Use for additional instructions.",
84
91
  },
85
92
  ],
86
93
  });
@@ -94,7 +101,7 @@ const get_state_fields = () => [];
94
101
  const run = async (
95
102
  table_id,
96
103
  viewname,
97
- { action_id, show_prev_runs, placeholder },
104
+ { action_id, show_prev_runs, placeholder, explainer },
98
105
  state,
99
106
  { res, req }
100
107
  ) => {
@@ -238,7 +245,8 @@ const run = async (
238
245
  span(
239
246
  { class: "submit-button p-2", onclick: "$('form.copilot').submit()" },
240
247
  i({ id: "sendbuttonicon", class: "far fa-paper-plane" })
241
- )
248
+ ),
249
+ explainer && small({ class: "explainer" }, i(explainer))
242
250
  )
243
251
  );
244
252
 
@@ -310,6 +318,11 @@ const run = async (
310
318
  position: relative;
311
319
  top: -1.8rem;
312
320
  left: 0.1rem;
321
+ }
322
+ .copilot-entry .explainer {
323
+ position: relative;
324
+ top: -1.2rem;
325
+ display: block;
313
326
  }
314
327
  .copilot-entry {margin-bottom: -1.25rem; margin-top: 1rem;}
315
328
  p.prevrun_content {
package/common.js CHANGED
@@ -9,6 +9,8 @@ const get_skills = () => {
9
9
  return [
10
10
  require("./skills/FTSRetrieval"),
11
11
  require("./skills/EmbeddingRetrieval"),
12
+ require("./skills/Trigger"),
13
+ require("./skills/Table"),
12
14
  //require("./skills/AdaptiveFeedback"),
13
15
  ];
14
16
  };
@@ -181,7 +183,8 @@ const process_interaction = async (
181
183
  }
182
184
  hasResult = true;
183
185
  const result = await tool.tool.process(
184
- JSON.parse(tool_call.function.arguments)
186
+ JSON.parse(tool_call.function.arguments),
187
+ { req }
185
188
  );
186
189
  if (
187
190
  (typeof result === "object" && Object.keys(result || {}).length) ||
package/index.js CHANGED
@@ -93,8 +93,9 @@ module.exports = {
93
93
  /*
94
94
  TODO
95
95
 
96
- -embedding retrieval test
97
- -promote triggers to skills (optional user confirm)
96
+ -embedding retrieval list view
97
+ -optional user confirm: action, insert
98
+ -Preload data
98
99
  -sql access
99
100
  -memory
100
101
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saltcorn/agents",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
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
 
@@ -82,6 +86,12 @@ class RetrievalByEmbedding {
82
86
  sublabel: "Max number of rows to find",
83
87
  type: "String",
84
88
  },
89
+ {
90
+ name: "add_sys_prompt",
91
+ label: "Additional prompt",
92
+ type: "String",
93
+ fieldview: "textarea"
94
+ },
85
95
  ];
86
96
  }
87
97
 
@@ -23,7 +23,17 @@ 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.list_view
30
+ ? ` When the tool call returns rows, do not describe them or repeat the information to the user. The results are already displayed to the user automatically.`
31
+ : ""
32
+ }${
33
+ this.add_sys_prompt
34
+ ? ` Additional information for the ${this.toolName} tool: ${this.add_sys_prompt}`
35
+ : ""
36
+ }`;
27
37
  }
28
38
 
29
39
  static async configFields() {
@@ -68,6 +78,12 @@ class RetrievalByFullTextSearch {
68
78
  type: "String",
69
79
  sublabel: "Comma-separated list of fields to hide from the prompt",
70
80
  },
81
+ {
82
+ name: "add_sys_prompt",
83
+ label: "Additional prompt",
84
+ type: "String",
85
+ fieldview: "textarea",
86
+ },
71
87
  /*{
72
88
  name: "contents_expr",
73
89
  label: "Contents string",
@@ -0,0 +1,241 @@
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 View = require("@saltcorn/data/models/view");
6
+ const { getState } = require("@saltcorn/data/db/state");
7
+ const db = require("@saltcorn/data/db");
8
+ const { fieldProperties } = require("./helpers");
9
+
10
+ class TableToSkill {
11
+ static skill_name = "Table";
12
+
13
+ get skill_label() {
14
+ return `${this.table_name} table`;
15
+ }
16
+
17
+ constructor(cfg) {
18
+ Object.assign(this, cfg);
19
+ }
20
+
21
+ systemPrompt() {
22
+ return `Use the query_${this.table_name} tool to search the ${
23
+ this.table_name
24
+ } database by a search phrase which will locate rows where any field match that query.${
25
+ this.add_sys_prompt ? ` ${this.add_sys_prompt}` : ""
26
+ }`;
27
+ }
28
+
29
+ static async configFields() {
30
+ const allTables = await Table.find();
31
+ const list_view_opts = {};
32
+ for (const t of allTables) {
33
+ const lviews = await View.find_table_views_where(
34
+ t.id,
35
+ ({ state_fields, viewrow }) =>
36
+ viewrow.viewtemplate !== "Edit" &&
37
+ state_fields.every((sf) => !sf.required)
38
+ );
39
+ list_view_opts[t.name] = ["", ...lviews.map((v) => v.name)];
40
+ }
41
+ return [
42
+ {
43
+ name: "table_name",
44
+ label: "Table",
45
+ sublabel: "Which table to link to the agent",
46
+ type: "String",
47
+ required: true,
48
+ attributes: { options: allTables.map((t) => t.name) },
49
+ },
50
+ {
51
+ name: "query",
52
+ label: "Query",
53
+ type: "Bool",
54
+ sublabel: "Allow the agent to query from this table",
55
+ },
56
+ {
57
+ name: "insert",
58
+ label: "Insert",
59
+ type: "Bool",
60
+ sublabel: "Allow the agent to insert new rows into this table",
61
+ },
62
+ {
63
+ name: "list_view",
64
+ label: "List view",
65
+ type: "String",
66
+ attributes: {
67
+ calcOptions: ["table_name", list_view_opts],
68
+ },
69
+ },
70
+ {
71
+ name: "hidden_fields",
72
+ label: "Hide fields",
73
+ type: "String",
74
+ sublabel: "Comma-separated list of fields to hide from the prompt",
75
+ showIf: { query: true },
76
+ },
77
+ {
78
+ name: "add_sys_prompt",
79
+ label: "Additional system prompt",
80
+ type: "String",
81
+ fieldview: "textarea"
82
+ },
83
+ ];
84
+ }
85
+
86
+ provideTools() {
87
+ const table = Table.findOne(this.table_name);
88
+ const tools = [];
89
+ let queryProperties = {};
90
+ let required = [];
91
+
92
+ table.fields
93
+ .filter((f) => !f.primary_key)
94
+ .forEach((field) => {
95
+ if (field.required && !field.primary_key) required.push(field.name);
96
+ queryProperties[field.name] = {
97
+ description: field.label + " " + field.description || "",
98
+ ...fieldProperties(field),
99
+ };
100
+ });
101
+
102
+ if (this.query)
103
+ tools.push({
104
+ type: "function",
105
+ process: async (q) => {
106
+ console.log("Table search", q);
107
+
108
+ let rows = [];
109
+ if (q.query?.full_text_search || q.full_text_search) {
110
+ const scState = getState();
111
+ const language = scState.pg_ts_config;
112
+ const use_websearch = scState.getConfig(
113
+ "search_use_websearch",
114
+ false
115
+ );
116
+ rows = await table.getRows({
117
+ _fts: {
118
+ fields: table.fields,
119
+ searchTerm: q.query?.full_text_search || q.full_text_search,
120
+ language,
121
+ use_websearch,
122
+ table: table.name,
123
+ schema: db.isSQLite ? undefined : db.getTenantSchema(),
124
+ },
125
+ });
126
+ } else {
127
+ rows = await table.getRows(q.query || q);
128
+ }
129
+ if (this.hidden_fields) {
130
+ const hidden_fields = this.hidden_fields
131
+ .split(",")
132
+ .map((s) => s.trim());
133
+ rows.forEach((r) => {
134
+ hidden_fields.forEach((k) => {
135
+ delete r[k];
136
+ });
137
+ });
138
+ }
139
+
140
+ if (rows.length) return { rows };
141
+ else
142
+ return {
143
+ response: "No rows found",
144
+ };
145
+ },
146
+ /*renderToolCall({ phrase }, { req }) {
147
+ return div({ class: "border border-primary p-2 m-2" }, phrase);
148
+ },*/
149
+ renderToolResponse: async ({ response, rows }, { req }) => {
150
+ if (rows) {
151
+ const view = View.findOne({ name: this.list_view });
152
+
153
+ if (view) {
154
+ const viewRes = await view.run(
155
+ { [table.pk_name]: { in: rows.map((r) => r[table.pk_name]) } },
156
+ { req }
157
+ );
158
+ return viewRes;
159
+ } else return "";
160
+ }
161
+ return div({ class: "border border-success p-2 m-2" }, response);
162
+ },
163
+ function: {
164
+ name: `query_${this.table_name}`,
165
+ description: `Search the ${this.table_name} database table${
166
+ table.description ? ` (${table.description})` : ""
167
+ } by a search phrase matched against all fields in the table with full text search or a more specific query. The retrieved rows will be returned`,
168
+ parameters: {
169
+ type: "object",
170
+ properties: {
171
+ query: {
172
+ anyOf: [
173
+ {
174
+ type: "object",
175
+ description:
176
+ "Search the table by a phrase matched against any string field",
177
+ properties: {
178
+ full_text_search: {
179
+ type: "string",
180
+ description: "A phrase to search the table with",
181
+ },
182
+ },
183
+ },
184
+ {
185
+ type: "object",
186
+ description: "Search the table by individual fields",
187
+ properties: queryProperties,
188
+ },
189
+ ],
190
+ },
191
+ },
192
+ },
193
+ },
194
+ });
195
+
196
+ if (this.insert)
197
+ tools.push({
198
+ type: "function",
199
+ process: async (rows, { req }) => {
200
+ const ids = [];
201
+ if (Array.isArray(rows)) {
202
+ for (const row of rows)
203
+ ids.push(await table.insertRow(row, req?.user));
204
+ } else ids.push(await table.insertRow(rows, req?.user));
205
+ return { created_ids: ids };
206
+ },
207
+ /*renderToolCall({ phrase }, { req }) {
208
+ return div({ class: "border border-primary p-2 m-2" }, phrase);
209
+ },*/
210
+ renderToolResponse: async ({ created_ids }, { req }) => {
211
+ if (created_ids) {
212
+ const view = View.findOne({ name: this.list_view });
213
+
214
+ if (view) {
215
+ const viewRes = await view.run(
216
+ { [table.pk_name]: { in: created_ids } },
217
+ { req }
218
+ );
219
+ return viewRes;
220
+ } else return "";
221
+ }
222
+ },
223
+ function: {
224
+ name: `insert_${this.table_name}`,
225
+ description: `Insert rows into the ${
226
+ this.table_name
227
+ } database table${
228
+ table.description ? ` (${table.description})` : ""
229
+ }`,
230
+ parameters: {
231
+ type: "object",
232
+ required,
233
+ properties: queryProperties,
234
+ },
235
+ },
236
+ });
237
+ return tools;
238
+ }
239
+ }
240
+
241
+ module.exports = TableToSkill;
@@ -0,0 +1,101 @@
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
+
11
+ class TriggerToSkill {
12
+ static skill_name = "Trigger";
13
+
14
+ get skill_label() {
15
+ return this.trigger_name;
16
+ }
17
+
18
+ constructor(cfg) {
19
+ Object.assign(this, cfg);
20
+ }
21
+
22
+ systemPrompt() {
23
+ const trigger = Trigger.findOne({ name: this.trigger_name });
24
+
25
+ return `${this.trigger_name} tool: ${trigger.description}`;
26
+ }
27
+
28
+ static async configFields() {
29
+ const actions = (await Trigger.find({})).filter(
30
+ (action) => action.description
31
+ );
32
+ const hasTable = actions.filter((a) => a.table_id).map((a) => a.name);
33
+ const confirm_view_opts = {};
34
+ for (const a of actions) {
35
+ if (!a.table_id) continue;
36
+ const views = await View.find({ table_id: a.table_id });
37
+ confirm_view_opts[a.name] = views.map((v) => v.name);
38
+ }
39
+ return [
40
+ {
41
+ name: "trigger_name",
42
+ label: "Action",
43
+ sublabel:
44
+ "Only actions with a description can be enabled. " +
45
+ a(
46
+ {
47
+ "data-dyn-href": `\`/actions/configure/\${trigger_name}\``,
48
+ target: "_blank",
49
+ },
50
+ "Configure"
51
+ ),
52
+ type: "String",
53
+ required: true,
54
+ attributes: { options: actions.map((a) => a.name) },
55
+ },
56
+ // TODO: confirm, show response, show argument
57
+ ];
58
+ }
59
+
60
+ provideTools = () => {
61
+ let properties = {};
62
+
63
+ const trigger = Trigger.findOne({ name: this.trigger_name });
64
+ if (trigger.table_id) {
65
+ const table = Table.findOne({ id: trigger.table_id });
66
+
67
+ table.fields
68
+ .filter((f) => !f.primary_key)
69
+ .forEach((field) => {
70
+ properties[field.name] = {
71
+ description: field.label + " " + field.description || "",
72
+ ...fieldProperties(field),
73
+ };
74
+ });
75
+ }
76
+ return {
77
+ type: "function",
78
+ process: async (row, { req }) => {
79
+ const result = await trigger.runWithoutRow({ user: req?.user, row });
80
+ return result;
81
+ },
82
+ /*renderToolCall({ phrase }, { req }) {
83
+ return div({ class: "border border-primary p-2 m-2" }, phrase);
84
+ },*/
85
+ renderToolResponse: async (response, { req }) => {
86
+ return div({ class: "border border-success p-2 m-2" }, response);
87
+ },
88
+ function: {
89
+ name: trigger.name,
90
+ description: trigger.description,
91
+ parameters: {
92
+ type: "object",
93
+ //required: ["action_javascript_code", "action_name"],
94
+ properties,
95
+ },
96
+ },
97
+ };
98
+ };
99
+ }
100
+
101
+ module.exports = TriggerToSkill;
@@ -0,0 +1,54 @@
1
+ const toArrayOfStrings = (opts) => {
2
+ if (typeof opts === "string") return opts.split(",").map((s) => s.trim());
3
+ if (Array.isArray(opts))
4
+ return opts.map((o) => (typeof o === "string" ? o : o.value || o.name));
5
+ };
6
+
7
+ const fieldProperties = (field) => {
8
+ const props = {};
9
+ const typeName = field.type?.name || field.type || field.input_type;
10
+ if (field.isRepeat) {
11
+ props.type = "array";
12
+ const properties = {};
13
+ field.fields.map((f) => {
14
+ properties[f.name] = {
15
+ description: f.sublabel || f.label,
16
+ ...fieldProperties(f),
17
+ };
18
+ });
19
+ props.items = {
20
+ type: "object",
21
+ properties,
22
+ };
23
+ }
24
+ switch (typeName) {
25
+ case "String":
26
+ props.type = "string";
27
+ if (field.attributes?.options)
28
+ props.enum = toArrayOfStrings(field.attributes.options);
29
+ break;
30
+ case "Bool":
31
+ props.type = "boolean";
32
+ break;
33
+ case "Integer":
34
+ props.type = "integer";
35
+ break;
36
+ case "Float":
37
+ props.type = "number";
38
+ break;
39
+ case "select":
40
+ props.type = "string";
41
+ if (field.options) props.enum = toArrayOfStrings(field.options);
42
+ break;
43
+ }
44
+ if (!props.type) {
45
+ switch (field.input_type) {
46
+ case "code":
47
+ props.type = "string";
48
+ break;
49
+ }
50
+ }
51
+ return props;
52
+ };
53
+
54
+ module.exports = { fieldProperties };