@saltcorn/agents 0.1.3 → 0.2.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/agent-view.js CHANGED
@@ -2,6 +2,7 @@ const Field = require("@saltcorn/data/models/field");
2
2
  const Table = require("@saltcorn/data/models/table");
3
3
  const Form = require("@saltcorn/data/models/form");
4
4
  const View = require("@saltcorn/data/models/view");
5
+ const File = require("@saltcorn/data/models/file");
5
6
  const Trigger = require("@saltcorn/data/models/trigger");
6
7
  const FieldRepeat = require("@saltcorn/data/models/fieldrepeat");
7
8
  const db = require("@saltcorn/data/db");
@@ -26,6 +27,7 @@ const {
26
27
  small,
27
28
  form,
28
29
  textarea,
30
+ label,
29
31
  a,
30
32
  } = require("@saltcorn/markup/tags");
31
33
  const { getState } = require("@saltcorn/data/db/state");
@@ -89,6 +91,19 @@ const configuration_workflow = (req) =>
89
91
  sublabel:
90
92
  "Appears below the input box. Use for additional instructions.",
91
93
  },
94
+ {
95
+ name: "image_upload",
96
+ label: "Upload images",
97
+ sublabel: "Allow the user to upload images",
98
+ type: "Bool",
99
+ },
100
+ {
101
+ name: "image_base64",
102
+ label: "base64 encode",
103
+ sublabel: "Use base64 encoding in the OpenAI API",
104
+ type: "Bool",
105
+ showIf: { image_upload: true },
106
+ },
92
107
  ],
93
108
  });
94
109
  },
@@ -98,10 +113,30 @@ const configuration_workflow = (req) =>
98
113
 
99
114
  const get_state_fields = () => [];
100
115
 
116
+ const uploadForm = (viewname, req) =>
117
+ span(
118
+ {
119
+ class: "attach_agent_image_wrap",
120
+ },
121
+ label(
122
+ { class: "btn-link", for: "attach_agent_image" },
123
+ i({ class: "fas fa-paperclip" })
124
+ ),
125
+ input({
126
+ id: "attach_agent_image",
127
+ name: "file",
128
+ type: "file",
129
+ class: "d-none",
130
+ accept: "image/*",
131
+ onchange: `agent_file_attach(event)`,
132
+ }),
133
+ span({ class: "ms-2 filename-label" })
134
+ );
135
+
101
136
  const run = async (
102
137
  table_id,
103
138
  viewname,
104
- { action_id, show_prev_runs, placeholder, explainer },
139
+ { action_id, show_prev_runs, placeholder, explainer, image_upload },
105
140
  state,
106
141
  { res, req }
107
142
  ) => {
@@ -122,13 +157,33 @@ const run = async (
122
157
  for (const interact of run.context.interactions) {
123
158
  switch (interact.role) {
124
159
  case "user":
125
- interactMarkups.push(
126
- div(
127
- { class: "interaction-segment" },
128
- span({ class: "badge bg-secondary" }, "You"),
129
- md.render(interact.content)
130
- )
131
- );
160
+ console.log(interact.content);
161
+ if (interact.content?.[0]?.type === "image_url") {
162
+ const image_url = interact.content[0].image_url.url;
163
+ if (image_url.startsWith("data"))
164
+ interactMarkups.push(
165
+ div(
166
+ { class: "interaction-segment" },
167
+ span({ class: "badge bg-secondary" }, "You"),
168
+ "File"
169
+ )
170
+ );
171
+ else
172
+ interactMarkups.push(
173
+ div(
174
+ { class: "interaction-segment" },
175
+ span({ class: "badge bg-secondary" }, "You"),
176
+ a({ href: image_url, target: "_blank" }, "File")
177
+ )
178
+ );
179
+ } else
180
+ interactMarkups.push(
181
+ div(
182
+ { class: "interaction-segment" },
183
+ span({ class: "badge bg-secondary" }, "You"),
184
+ md.render(interact.content)
185
+ )
186
+ );
132
187
  break;
133
188
  case "assistant":
134
189
  case "system":
@@ -216,7 +271,7 @@ const run = async (
216
271
  }
217
272
  const input_form = form(
218
273
  {
219
- onsubmit: `event.preventDefault();spin_send_button();view_post('${viewname}', 'interact', $(this).serialize(), processCopilotResponse);return false;`,
274
+ onsubmit: `event.preventDefault();spin_send_button();view_post('${viewname}', 'interact', new FormData(this), processCopilotResponse);return false;`,
220
275
  class: "form-namespace copilot mt-2",
221
276
  method: "post",
222
277
  },
@@ -246,6 +301,7 @@ const run = async (
246
301
  { class: "submit-button p-2", onclick: "$('form.copilot').submit()" },
247
302
  i({ id: "sendbuttonicon", class: "far fa-paper-plane" })
248
303
  ),
304
+ image_upload && uploadForm(viewname, req),
249
305
  explainer && small({ class: "explainer" }, i(explainer))
250
306
  )
251
307
  );
@@ -291,17 +347,6 @@ const run = async (
291
347
  { class: "card" },
292
348
  div(
293
349
  { class: "card-body" },
294
- script({
295
- src: `/static_assets/${db.connectObj.version_tag}/mermaid.min.js`,
296
- }),
297
- script(
298
- { type: "module" },
299
- `mermaid.initialize({securityLevel: 'loose'${
300
- getState().getLightDarkMode(req.user) === "dark"
301
- ? ",theme: 'dark',"
302
- : ""
303
- }});`
304
- ),
305
350
  div({ id: "copilotinteractions" }, runInteractions),
306
351
  input_form,
307
352
  style(
@@ -313,11 +358,17 @@ const run = async (
313
358
  div.prevcopilotrun i.fa-trash-alt {display: none;}
314
359
  div.prevcopilotrun:hover i.fa-trash-alt {display: block;}
315
360
  .copilot-entry .submit-button:hover { cursor: pointer}
361
+ .copilot-entry span.attach_agent_image_wrap i:hover { cursor: pointer}
316
362
 
317
363
  .copilot-entry .submit-button {
318
364
  position: relative;
319
365
  top: -1.8rem;
320
366
  left: 0.1rem;
367
+ }
368
+ .copilot-entry span.attach_agent_image_wrap {
369
+ position: relative;
370
+ top: -1.8rem;
371
+ left: 0.2rem;
321
372
  }
322
373
  .copilot-entry .explainer {
323
374
  position: relative;
@@ -333,17 +384,25 @@ const run = async (
333
384
  text-overflow: ellipsis;}`
334
385
  ),
335
386
  script(`function processCopilotResponse(res) {
387
+ const hadFile = $("input#attach_agent_image").val();
388
+ $("span.filename-label").text("");
389
+ $("input#attach_agent_image").val(null);
336
390
  $("#sendbuttonicon").attr("class","far fa-paper-plane");
337
391
  const $runidin= $("input[name=run_id")
338
392
  if(res.run_id && (!$runidin.val() || $runidin.val()=="undefined"))
339
393
  $runidin.val(res.run_id);
340
394
  const wrapSegment = (html, who) => '<div class="interaction-segment"><span class="badge bg-secondary">'+who+'</span>'+html+'</div>'
341
395
  $("#copilotinteractions").append(wrapSegment('<p>'+$("textarea[name=userinput]").val()+'</p>', "You"))
342
- $("textarea[name=userinput]").val("")
396
+ if(hadFile)
397
+ $("#copilotinteractions").append(wrapSegment('File', "You"))
398
+ $("textarea[name=userinput]").val("")
343
399
 
344
400
  if(res.response)
345
401
  $("#copilotinteractions").append(res.response)
346
402
  }
403
+ function agent_file_attach(e) {
404
+ $(".attach_agent_image_wrap span.filename-label").text(e.target.files[0].name)
405
+ }
347
406
  function restore_old_button_elem(btn) {
348
407
  const oldText = $(btn).data("old-text");
349
408
  btn.html(oldText);
@@ -403,12 +462,6 @@ const run = async (
403
462
  : main_chat;
404
463
  };
405
464
 
406
- /*
407
-
408
- build a workflow that asks the user for their name and age
409
-
410
- */
411
-
412
465
  const interact = async (table_id, viewname, config, body, { req, res }) => {
413
466
  const { userinput, run_id } = body;
414
467
  let run;
@@ -429,6 +482,41 @@ const interact = async (table_id, viewname, config, body, { req, res }) => {
429
482
  interactions: [{ role: "user", content: userinput }],
430
483
  });
431
484
  }
485
+ if (config.image_upload && req.files?.file) {
486
+ const file = await File.from_req_files(
487
+ req.files.file,
488
+ req.user ? req.user.id : null,
489
+ 100
490
+ // file_field?.attributes?.folder
491
+ );
492
+ const baseUrl = getState().getConfig("base_url").replace(/\/$/, "");
493
+ let imageurl;
494
+ if (
495
+ !config.image_base64 &&
496
+ baseUrl &&
497
+ !baseUrl.includes("http://localhost:")
498
+ ) {
499
+ imageurl = `${baseUrl}/files/serve/${file.path_to_serve}`;
500
+ } else {
501
+ const b64 = await file.get_contents("base64");
502
+ imageurl = `data:${file.mimetype};base64,${b64}`;
503
+ }
504
+ await addToContext(run, {
505
+ interactions: [
506
+ {
507
+ role: "user",
508
+ content: [
509
+ {
510
+ type: "image_url",
511
+ image_url: {
512
+ url: imageurl,
513
+ },
514
+ },
515
+ ],
516
+ },
517
+ ],
518
+ });
519
+ }
432
520
  const action = await Trigger.findOne({ id: config.action_id });
433
521
 
434
522
  return await process_interaction(run, action.configuration, req, action.name);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saltcorn/agents",
3
- "version": "0.1.3",
3
+ "version": "0.2.1",
4
4
  "description": "AI agents for Saltcorn",
5
5
  "main": "index.js",
6
6
  "dependencies": {
@@ -37,17 +37,26 @@ class RetrievalByEmbedding {
37
37
  const allTables = await Table.find();
38
38
  const tableOpts = [];
39
39
  const relation_opts = {};
40
+ const list_view_opts = {};
40
41
  for (const table of allTables) {
41
- table.fields
42
- .filter((f) => f.type?.name === "PGVector")
43
- .forEach((f) => {
44
- const relNm = `${table.name}.${f.name}`;
45
- tableOpts.push(relNm);
46
- const fkeys = table.fields
47
- .filter((f) => f.is_fkey)
48
- .map((f) => f.name);
49
- relation_opts[relNm] = ["", ...fkeys];
50
- });
42
+ for (const f of table.fields) {
43
+ if (f.type?.name !== "PGVector") continue;
44
+ const relNm = `${table.name}.${f.name}`;
45
+ tableOpts.push(relNm);
46
+ list_view_opts[relNm] = [""];
47
+ const fkeys = table.fields.filter((f) => f.is_fkey).map((f) => f.name);
48
+ relation_opts[relNm] = ["", ...fkeys];
49
+ for (const fkeyField of table.fields.filter((f) => f.is_fkey)) {
50
+ const t = Table.findOne(fkeyField.reftable_name);
51
+ const lviews = await View.find_table_views_where(
52
+ t.id,
53
+ ({ state_fields, viewrow }) =>
54
+ viewrow.viewtemplate !== "Edit" &&
55
+ state_fields.every((sf) => !sf.required)
56
+ );
57
+ list_view_opts[relNm].push(...lviews.map((v) => v.name));
58
+ }
59
+ }
51
60
  }
52
61
  return [
53
62
  {
@@ -74,6 +83,14 @@ class RetrievalByEmbedding {
74
83
  required: true,
75
84
  attributes: { calcOptions: ["vec_field", relation_opts] },
76
85
  },
86
+ {
87
+ name: "list_view",
88
+ label: "List view",
89
+ type: "String",
90
+ attributes: {
91
+ calcOptions: ["vec_field", list_view_opts],
92
+ },
93
+ },
77
94
  {
78
95
  name: "hidden_fields",
79
96
  label: "Hide fields",
@@ -90,7 +107,7 @@ class RetrievalByEmbedding {
90
107
  name: "add_sys_prompt",
91
108
  label: "Additional prompt",
92
109
  type: "String",
93
- fieldview: "textarea"
110
+ fieldview: "textarea",
94
111
  },
95
112
  ];
96
113
  }
@@ -153,6 +170,24 @@ class RetrievalByEmbedding {
153
170
  return { rows: docs };
154
171
  }
155
172
  },
173
+ renderToolResponse: async ({ response, rows }, { req }) => {
174
+ if (rows) {
175
+ const view = View.findOne({ name: this.list_view });
176
+
177
+ if (view) {
178
+ const viewRes = await view.run(
179
+ {
180
+ [table_docs.pk_name]: {
181
+ in: rows.map((r) => r[table_docs.pk_name]),
182
+ },
183
+ },
184
+ { req }
185
+ );
186
+ return viewRes;
187
+ } else return "";
188
+ }
189
+ return div({ class: "border border-success p-2 m-2" }, response);
190
+ },
156
191
  function: {
157
192
  name: this.toolName,
158
193
  description: `Search the ${table_docs.name``} archive${