@saltcorn/agents 0.6.6 → 0.6.8

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
@@ -652,7 +652,7 @@ const run = async (
652
652
  ).join(" ");
653
653
  }
654
654
  $("span.filename-label").text("").removeClass("me-2");
655
- _agentDT.items.clear();
655
+ window._agentDT.items.clear();
656
656
  $("input#attach_agent_image").val(null);
657
657
  if(!not_final || (!${JSON.stringify(dyn_updates)})) $("#sendbuttonicon").attr("class","far fa-paper-plane");
658
658
  const $runidin= $("input[name=run_id")
@@ -672,20 +672,20 @@ const run = async (
672
672
  window.final_agent_response = () => {
673
673
  $("#sendbuttonicon").attr("class","far fa-paper-plane");
674
674
  }
675
- const _agentDT = new DataTransfer();
675
+ window._agentDT = new DataTransfer();
676
676
  function setAgentFiles(files) {
677
- for (const f of files) _agentDT.items.add(f);
678
- document.getElementById('attach_agent_image').files = _agentDT.files;
677
+ for (const f of files) window._agentDT.items.add(f);
678
+ document.getElementById('attach_agent_image').files = window._agentDT.files;
679
679
  updateFileLabel();
680
680
  }
681
681
  function updateFileLabel() {
682
- const n = _agentDT.files.length;
682
+ const n = window._agentDT.files.length;
683
683
  const $label = $(".attach_agent_image_wrap span.filename-label");
684
684
  if (n === 0) {
685
685
  $label.html("").removeClass("me-2");
686
686
  } else {
687
687
  $label.addClass("me-2");
688
- const text = n === 1 ? _agentDT.files[0].name : n + " files";
688
+ const text = n === 1 ? window._agentDT.files[0].name : n + " files";
689
689
  $label.html(${
690
690
  isWeb(req)
691
691
  ? `text + ' <span class="badge text-bg-secondary" style="cursor:pointer;font-size:.65em;vertical-align:middle" onclick="clearAgentFiles()" title="Remove files">&times;</span>'`
@@ -694,13 +694,13 @@ const run = async (
694
694
  }
695
695
  }
696
696
  function clearAgentFiles() {
697
- _agentDT.items.clear();
697
+ window._agentDT.items.clear();
698
698
  $("input#attach_agent_image").val(null);
699
699
  updateFileLabel();
700
700
  }
701
701
  window.clearAgentFiles = clearAgentFiles;
702
702
  function agent_file_attach(e) {
703
- _agentDT.items.clear();
703
+ window._agentDT.items.clear();
704
704
  setAgentFiles(e.target.files);
705
705
  }
706
706
  function restore_old_button_elem(btn) {
package/common.js CHANGED
@@ -35,6 +35,8 @@ const get_skills = () => {
35
35
  require("./skills/ModelContextProtocol"),
36
36
  require("./skills/PromptPicker"),
37
37
  require("./skills/RunJsCode"),
38
+ require("./skills/GenerateAndRunJsCode"),
39
+ require("./skills/Fetch"),
38
40
  //require("./skills/AdaptiveFeedback"),
39
41
  ...exchange_skills,
40
42
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saltcorn/agents",
3
- "version": "0.6.6",
3
+ "version": "0.6.8",
4
4
  "description": "AI agents for Saltcorn",
5
5
  "main": "index.js",
6
6
  "dependencies": {
@@ -0,0 +1,62 @@
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 User = require("@saltcorn/data/models/user");
6
+ const File = require("@saltcorn/data/models/file");
7
+ const View = require("@saltcorn/data/models/view");
8
+ const Trigger = require("@saltcorn/data/models/trigger");
9
+ const FieldRepeat = require("@saltcorn/data/models/fieldrepeat");
10
+ const { getState } = require("@saltcorn/data/db/state");
11
+ const db = require("@saltcorn/data/db");
12
+ const { eval_expression } = require("@saltcorn/data/models/expression");
13
+ const { interpolate, sleep } = require("@saltcorn/data/utils");
14
+ const { features } = require("@saltcorn/data/db/state");
15
+ const { button } = require("@saltcorn/markup/tags");
16
+ const { validID } = require("@saltcorn/markup/layout_utils");
17
+
18
+ const vm = require("vm");
19
+
20
+ //const { fieldProperties } = require("./helpers");
21
+
22
+ class FetchSkill {
23
+ static skill_name = "Fetch";
24
+
25
+ get skill_label() {
26
+ return "Fetch";
27
+ }
28
+
29
+ constructor(cfg) {
30
+ Object.assign(this, cfg);
31
+ }
32
+
33
+ static async configFields() {
34
+ return [];
35
+ }
36
+
37
+ provideTools = () => {
38
+ return {
39
+ type: "function",
40
+ process: async (row) => {
41
+ const resp = await fetch(row.url);
42
+ return await resp.text();
43
+ },
44
+ function: {
45
+ name: "fetch_web_page",
46
+ description: "fetch a web page with HTTP(S) GET",
47
+ parameters: {
48
+ type: "object",
49
+ required: ["url"],
50
+ properties: {
51
+ url: {
52
+ description: "The URL to fetch with HTTP",
53
+ type: "string",
54
+ },
55
+ },
56
+ },
57
+ },
58
+ };
59
+ };
60
+ }
61
+
62
+ module.exports = FetchSkill;
@@ -0,0 +1,422 @@
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 User = require("@saltcorn/data/models/user");
6
+ const File = require("@saltcorn/data/models/file");
7
+ const View = require("@saltcorn/data/models/view");
8
+ const Trigger = require("@saltcorn/data/models/trigger");
9
+ const FieldRepeat = require("@saltcorn/data/models/fieldrepeat");
10
+ const { getState } = require("@saltcorn/data/db/state");
11
+ const db = require("@saltcorn/data/db");
12
+ const { eval_expression } = require("@saltcorn/data/models/expression");
13
+ const { interpolate, sleep } = require("@saltcorn/data/utils");
14
+ const { features } = require("@saltcorn/data/db/state");
15
+ const { button } = require("@saltcorn/markup/tags");
16
+ const { validID } = require("@saltcorn/markup/layout_utils");
17
+
18
+ const vm = require("vm");
19
+
20
+ //const { fieldProperties } = require("./helpers");
21
+
22
+ class GenerateAndRunJsCodeSkill {
23
+ static skill_name = "Generate and run JavaScript code";
24
+
25
+ get skill_label() {
26
+ return "Generate and run JavaScript code";
27
+ }
28
+
29
+ constructor(cfg) {
30
+ Object.assign(this, cfg);
31
+ }
32
+
33
+ async runCode(code, { user, req }) {
34
+ const sysState = getState();
35
+
36
+ const f = vm.runInNewContext(`async () => {${code}\n}`, {
37
+ ...(this.allow_table
38
+ ? {
39
+ Table: Table.subClass
40
+ ? Table.subClass({ user, read_only: this.read_only })
41
+ : Table,
42
+ }
43
+ : {}),
44
+ ...(this.allow_fetch ? { fetch } : {}),
45
+ user,
46
+ console,
47
+ sleep,
48
+ setTimeout,
49
+ });
50
+ return await f();
51
+ }
52
+
53
+ async systemPrompt({ triggering_row, user }) {
54
+ return this.add_sys_prompt || "";
55
+ }
56
+
57
+ async skillRoute({ run, triggering_row, req }) {
58
+ return await this.runCode({ row: triggering_row, run, user: req.user });
59
+ }
60
+
61
+ static async configFields() {
62
+ return [
63
+ {
64
+ name: "tool_name",
65
+ label: "Tool name",
66
+ type: "String",
67
+ class: "validate-identifier",
68
+ },
69
+ {
70
+ name: "tool_description",
71
+ label: "Tool description",
72
+ type: "String",
73
+ },
74
+
75
+ {
76
+ name: "code_description",
77
+ label: "Code description",
78
+ type: "String",
79
+ fieldview: "textarea",
80
+ },
81
+ {
82
+ name: "add_sys_prompt",
83
+ label: "Additional system prompt",
84
+ type: "String",
85
+ fieldview: "textarea",
86
+ },
87
+ {
88
+ name: "allow_fetch",
89
+ label: "Allow HTTP fetch",
90
+ type: "Bool",
91
+ },
92
+ {
93
+ name: "allow_table",
94
+ label: "Allow access to tables",
95
+ type: "Bool",
96
+ },
97
+ ...(Table.subClass
98
+ ? [
99
+ {
100
+ name: "table_read_only",
101
+ label: "Read only?",
102
+ type: "Bool",
103
+ showIf: { allow_table: true },
104
+ },
105
+ ]
106
+ : []),
107
+ ];
108
+ }
109
+
110
+ provideTools = () => {
111
+ return {
112
+ type: "function",
113
+ process: async (row, { req }) => {
114
+ return "Code generation tool activated";
115
+ //return await this.runCode({ row, user: req.user });
116
+ },
117
+ /*renderToolCall({ phrase }, { req }) {
118
+ return div({ class: "border border-primary p-2 m-2" }, phrase);
119
+ },*/
120
+ postProcess: async ({ tool_call, req, generate, ...rest }) => {
121
+ //console.log("postprocess args", { tool_call, ...rest });
122
+
123
+ const str = await generate(
124
+ `You will now be asked to write JavaScript code.
125
+ ${this.code_description ? "\nSome more information: " + this.code_description : ""}
126
+ ${this.allow_fetch ? "\nYou can use the standard fetch JavaScript function to make HTTP(S) requests." : ""}
127
+ ${this.allow_table ? getTablePrompt(this.read_only) : ""}
128
+
129
+ The code you write can use await at the top level, and should return
130
+ (at the top level) a string (which can contain HTML tags) with the response which will be shown to the user.
131
+
132
+ Now generate the JavaScript code required by the user.`,
133
+ );
134
+ //console.log("gen answer", str);
135
+
136
+ const js_code = str.includes("```javascript")
137
+ ? str.split("```javascript")[1].split("```")[0]
138
+ : str;
139
+
140
+ const res = await this.runCode(js_code, { user: req.user });
141
+ //console.log("code response", res);
142
+
143
+ return {
144
+ stop: true,
145
+ add_response: res,
146
+ };
147
+ },
148
+ function: {
149
+ name: this.tool_name,
150
+ description: this.tool_description,
151
+ parameters: {
152
+ type: "object",
153
+ properties: {},
154
+ },
155
+ },
156
+ };
157
+ };
158
+ }
159
+
160
+ const getTablePrompt = (read_only) => {
161
+ const state = getState();
162
+ const tables = state.tables;
163
+ const tableLines = [];
164
+ tables.forEach((table) => {
165
+ const fieldLines = table.fields.map(
166
+ (f) =>
167
+ ` * ${f.name} with type: ${f.pretty_type.replace(
168
+ "Key to",
169
+ "ForeignKey referencing",
170
+ )}.${f.description ? ` ${f.description}` : ""}`,
171
+ );
172
+ tableLines.push(
173
+ `${table.name}${
174
+ table.description ? `: ${table.description}.` : "."
175
+ } Contains the following fields:\n${fieldLines.join("\n")}`,
176
+ );
177
+ });
178
+ return `Use the Table variable to access the Table class which gives you access to database tables
179
+
180
+ Example:
181
+
182
+ const someTable = Table.findOne({name: "Orders"})
183
+ await someTable.insertRow({name: "Alex", age: 43})
184
+ await someTable.deleteRows({id: order})
185
+
186
+
187
+ You can use the Table class to access database tables. Use this to create or delete tables and
188
+ their properties, or to query or change table rows.
189
+
190
+ To query, update, insert or delete rows in an existing table, first you should find the
191
+ table object with findOne.
192
+
193
+ Example:
194
+
195
+ Table.findOne({name: "Customers"}) // find the table with name "Customers"
196
+ Table.findOne("Customers") // find the table with name "Customers" (shortcut)
197
+ Table.findOne({ id: 5 }) // find the table with id=5
198
+ Table.findOne(5) // find the table with id=5 (shortcut)
199
+
200
+ Table.findOne is synchronous (no need to await), But the functions that query and manipulate
201
+ (such as insertRow, getRows, updateRow, deleteRows) rows are mostly asyncronous, so you can
202
+ put the await in front of the whole expression.
203
+
204
+ Example:
205
+ To count the number of rows in the customer table:
206
+
207
+ const nrows = await Table.findOne("Customers").countRows({})
208
+
209
+ Querying table rows
210
+
211
+ There are several methods you can use to retrieve rows in the database:
212
+
213
+ countRows: Count the number of rows in db table. The argument is a where-expression with conditions the
214
+ counted rows should match. countRows returns the number of matching rows wrapped in a promise.
215
+
216
+ countRows(where?): Promise<number>
217
+ Count amount of rows in db table
218
+
219
+ Parameters
220
+ Optional where: Where
221
+ Returns Promise<number>
222
+
223
+ Example of using countRows:
224
+ const bookTable = Table.findOne({name: "books"})
225
+
226
+ // Count the total number of rows in the books table
227
+ const totalNumberOfBooks = await bookTable.countRows({})
228
+
229
+ // Count the number of books where the cover_color field has the value is "Red"
230
+ const numberOfRedBooks = await bookTable.countRows({cover_color: "Red"})
231
+
232
+ // Count number of books with more than 500 pages
233
+ const numberOfLongBooks = await bookTable.countRows({pages: {gt: 500}})
234
+
235
+ getRows: Get all matching rows from the table in the database.
236
+
237
+ The arguments are the same as for getRow. The first argument is where-expression
238
+ with the conditions to match, and the second argument is an optional object and
239
+ allows you to set ordering and limit options. Keywords that can be used in the
240
+ second argument are orderBy, orderDesc, limit and offset.
241
+
242
+ getRows will return an array of rows matching the where-expression in the first
243
+ argument, wrapped in a Promise (use await to read the array).
244
+
245
+
246
+ getRows(where?, selopts?): Promise<Row[]>
247
+ Get rows from Table in db
248
+
249
+ Parameters
250
+ where: Where = {}
251
+ selopts: SelectOptions & ForUserRequest = {}
252
+ Returns Promise<Row[]>
253
+
254
+ Example of using getRows:
255
+
256
+ const bookTable = Table.findOne({name: "books"})
257
+
258
+ // get the rows in the book table with author = "Henrik Pontoppidan"
259
+ const myBooks = await bookTable.getRows({author: "Henrik Pontoppidan"})
260
+
261
+ // get the 3 most recent books written by "Henrik Pontoppidan" with more that 500 pages
262
+ const myBooks = await bookTable.getRows({author: "Henrik Pontoppidan", pages: {gt: 500}}, {orderBy: "published", orderDesc: true})
263
+
264
+ getRow: Get one row from the table in the database. The matching row will be returned in a promise -
265
+ use await to read the value. If no matching rule can be found, null will be returned. If more than one
266
+ row matches, the first found row will be returned.
267
+
268
+ The first argument to get row is a where-expression With the conditions the returned row should match.
269
+
270
+ The second document is optional and is an object that can modify the search. This is mainly useful in
271
+ case there is more than one matching row for the where-expression in the first argument and you want to
272
+ give an explicit order. For example, use {orderBy: "name"} as the second argument to pick the first
273
+ row by the name field, ordered ascending. {orderBy: "name", orderDesc: true} to order by name, descending
274
+
275
+ This is however rare and usually getRow is run with a single argument of a Where expression that uniquely
276
+ determines the row to return, if it exisits.
277
+
278
+ getRow(where?, selopts?): Promise<null | Row>
279
+ Get one row from table in db
280
+
281
+ Parameters
282
+ where: Where = {}
283
+ selopts: SelectOptions & ForUserRequest = {}
284
+ Returns Promise<null | Row>
285
+
286
+ Example of using getRow:
287
+ const bookTable = Table.findOne({name: "books"})
288
+
289
+ // get the row in the book table with id = 5
290
+ const myBook = await bookTable.getRow({id: 5})
291
+
292
+ // get the row for the last book published by Leo Tolstoy
293
+ const myBook = await bookTable.getRow({author: "Leo Tolstoy"}, {orderBy: "published", orderDesc: true})
294
+
295
+ getJoinedRows: To retrieve rows together with joinfields and aggregations
296
+
297
+ getJoinedRows(opts?): Promise<Row[]>
298
+ Get rows along with joined and aggregated fields. The argument to getJoinedRows is an object with several different possible fields, all of which are optional
299
+
300
+ where: A Where expression indicating the criterion to match
301
+ joinFields: An object with the joinfields to retrieve
302
+ aggregations: An object with the aggregations to retrieve
303
+ orderBy: A string with the name of the field to order by
304
+ orderDesc: If true, descending order
305
+ limit: A number with the maximum number of rows to retrieve
306
+ offset: The number of rows to skip in the result before returning rows
307
+ Parameters
308
+ Optional opts: any = {}
309
+ Returns Promise<Row[]>
310
+
311
+ Example of using getJoinedRows:
312
+
313
+ const patients = Table.findOne({ name: "patients" });
314
+ const patients_rows = await patients.getJoinedRows({
315
+ where: { age: { gt: 65 } },
316
+ orderBy: "id",
317
+ aggregations: {
318
+ avg_temp: {
319
+ table: "readings",
320
+ ref: "patient_id",
321
+ field: "temperature",
322
+ aggregate: "avg",
323
+ },
324
+ },
325
+ joinFields: {
326
+ pages: { ref: "favbook", target: "pages" },
327
+ author: { ref: "favbook", target: "author" },
328
+ },
329
+ });
330
+
331
+ These functions all take "Where expressions" which are JavaScript objects describing
332
+ the criterion to match to. Some examples:
333
+
334
+ { name: "Jim" }: Match all rows with name="Jim"
335
+ { name: { ilike: "im"} }: Match all rows where name contains "im" (case insensitive)
336
+ { name: /im/ }: Match all rows with name matching regular expression "im"
337
+ { age: { lt: 18 } }: Match all rows with age<18
338
+ { age: { lt: 18, equal: true } }: Match all rows with age<=18
339
+ { age: { gt: 18, lt: 65} }: Match all rows with 18<age<65
340
+ { name: { or: ["Harry", "Sally"] } }: Match all rows with name="Harry" or "Sally"
341
+ { or: [{ name: "Joe"}, { age: 37 }] }: Match all rows with name="Joe" or age=37
342
+ { not: { id: 5 } }: All rows except id=5
343
+ { id: { in: [1, 2, 3] } }: Rows with id 1, 2, or 3
344
+
345
+ There are two nearly identical functions for updating rows depending on how you want failures treated
346
+
347
+ ${
348
+ !read_only
349
+ ? `updateRow Update a row in the database table, throws an exception if update is invalid
350
+
351
+ updateRow(v_in, id, user?): Promise<string | void>
352
+ Update row
353
+
354
+ Parameters
355
+ v_in: any. columns with values to update
356
+
357
+ id: number. id value, table primary key
358
+
359
+ Optional user: Row
360
+
361
+ Example of using updateRow:
362
+
363
+ const bookTable = Table.findOne({name: "books"})
364
+
365
+ // get the row in the book table for Moby Dick
366
+ const moby_dick = await bookTable.getRow({title: "Moby Dick"})
367
+
368
+ // Update the read field to true and the rating field to 5 in the retrieved row.
369
+ await bookTable.updateRow({read: true, rating: 5}, moby_dick.id)
370
+
371
+ // if you want to update more than one row, you must first retrieve all the rows and
372
+ // then update them individually
373
+
374
+ const allBooks = await bookTable.getRows()
375
+ for(const book of allBooks) {
376
+ await bookTable.updateRow({price: book.price*0.8}, book.id)
377
+ }
378
+
379
+ tryUpdateRow Update a row, return an error message if update is invalid
380
+
381
+ There are two nearly identical functions for inserting a new row depending on how you want failures treated
382
+
383
+ insertRow insert a row, throws an exception if it is invalid
384
+ insertRow(v_in, user): Promise<any>
385
+ Insert row into the table. By passing in the user as the second argument, tt will check write rights. If a user object is not supplied, the insert goes ahead without checking write permissions.
386
+
387
+ Returns the primary key value of the inserted row.
388
+
389
+ This will throw an exception if the row does not conform to the table constraints. If you would like to insert a row with a function that can return an error message, use tryInsertRow instead.
390
+
391
+ Parameters
392
+ v_in: Row
393
+ Optional user: Row
394
+ Returns Promise<any>
395
+
396
+ Example of using insertRow:
397
+ await Table.findOne("People").insertRow({ name: "Jim", age: 35 })
398
+
399
+ tryInsertRow insert a row, return an error message if it is invalid
400
+
401
+ Use deleteRows to delete any number (zero, one or many) of rows matching a criterion. It uses the same where expression as the functions for querying rows
402
+ deleteRows(where, user?, noTrigger?): Promise<void>
403
+ Delete rows from table
404
+
405
+ Parameters
406
+ where: Where
407
+ condition
408
+
409
+ Optional user: Row
410
+ optional user, if null then no authorization will be checked
411
+
412
+ Optional noTrigger: boolean
413
+ Returns Promise<void>`
414
+ : ""
415
+ }
416
+
417
+ The following tables are present in the database:
418
+
419
+ ${tableLines.join("\n\n")}`;
420
+ };
421
+
422
+ module.exports = GenerateAndRunJsCodeSkill;
@@ -48,15 +48,15 @@ class RunJsCodeSkill {
48
48
  if (!enabled) {
49
49
  sysState.log(
50
50
  5,
51
- "emit_to_client called, but dynamic updates are disabled"
51
+ "emit_to_client called, but dynamic updates are disabled",
52
52
  );
53
53
  return;
54
54
  }
55
55
  const safeIds = Array.isArray(userIds)
56
56
  ? userIds
57
57
  : userIds
58
- ? [userIds]
59
- : [];
58
+ ? [userIds]
59
+ : [];
60
60
  sysState.emitDynamicUpdate(db.getTenantSchema(), data, safeIds);
61
61
  },
62
62
  tryCatchInTransaction: db.tryCatchInTransaction,
@@ -184,23 +184,25 @@ class RunJsCodeSkill {
184
184
  class: ["btn btn-outline-secondary btn-sm me-1", klass],
185
185
  onclick: `view_post('${viewname}', 'skillroute', {skillid: '${this.skillid}', triggering_row_id: $('input[name=triggering_row_id').val(), run_id: get_run_id(this)});`,
186
186
  },
187
- this.button_label
187
+ this.button_label,
188
188
  );
189
189
  }
190
190
 
191
191
  provideTools = () => {
192
192
  if (this.mode === "Button") return;
193
193
  let properties = {};
194
- (this.toolargs || []).forEach((arg) => {
195
- properties[arg.name] = {
196
- description: arg.description,
197
- type: arg.argtype,
198
- };
199
- });
194
+ (this.toolargs || [])
195
+ .filter((arg) => arg.name)
196
+ .forEach((arg) => {
197
+ properties[arg.name] = {
198
+ description: arg.description,
199
+ type: arg.argtype,
200
+ };
201
+ });
200
202
  return {
201
203
  type: "function",
202
204
  process: async (row, { req }) => {
203
- return await this.runCode({ row, user: req.user.req });
205
+ return await this.runCode({ row, user: req.user });
204
206
  },
205
207
  /*renderToolCall({ phrase }, { req }) {
206
208
  return div({ class: "border border-primary p-2 m-2" }, phrase);
@@ -209,7 +211,9 @@ class RunJsCodeSkill {
209
211
  ? async (response, { req }) => {
210
212
  return div(
211
213
  { class: "border border-success p-2 m-2" },
212
- typeof response === "string" ? response : JSON.stringify(response)
214
+ typeof response === "string"
215
+ ? response
216
+ : JSON.stringify(response),
213
217
  );
214
218
  }
215
219
  : undefined,
@@ -218,7 +222,7 @@ class RunJsCodeSkill {
218
222
  description: this.tool_description,
219
223
  parameters: {
220
224
  type: "object",
221
- required: (this.toolargs || []).map((a) => a.name),
225
+ required: (this.toolargs || []).filter((arg) => arg.name).map((a) => a.name),
222
226
  properties,
223
227
  },
224
228
  },