@saltcorn/agents 0.1.2 → 0.2.0
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 +132 -31
- package/common.js +4 -1
- package/index.js +3 -2
- package/package.json +1 -1
- package/skills/EmbeddingRetrieval.js +46 -10
- package/skills/FTSRetrieval.js +5 -0
- package/skills/Table.js +241 -0
- package/skills/Trigger.js +101 -0
- package/skills/helpers.js +54 -0
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");
|
|
@@ -45,7 +47,7 @@ const configuration_workflow = (req) =>
|
|
|
45
47
|
new Workflow({
|
|
46
48
|
steps: [
|
|
47
49
|
{
|
|
48
|
-
name:
|
|
50
|
+
name: "Agent action",
|
|
49
51
|
form: async (context) => {
|
|
50
52
|
const agent_actions = await Trigger.find({ action: "Agent" });
|
|
51
53
|
return new Form({
|
|
@@ -68,7 +70,7 @@ const configuration_workflow = (req) =>
|
|
|
68
70
|
"data-dyn-href": `\`/actions/configure/\${action_id}\``,
|
|
69
71
|
target: "_blank",
|
|
70
72
|
},
|
|
71
|
-
|
|
73
|
+
"Configure"
|
|
72
74
|
),
|
|
73
75
|
},
|
|
74
76
|
{
|
|
@@ -80,7 +82,27 @@ const configuration_workflow = (req) =>
|
|
|
80
82
|
name: "placeholder",
|
|
81
83
|
label: "Placeholder",
|
|
82
84
|
type: "String",
|
|
83
|
-
default: "How can I help you?"
|
|
85
|
+
default: "How can I help you?",
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: "explainer",
|
|
89
|
+
label: "Explainer",
|
|
90
|
+
type: "String",
|
|
91
|
+
sublabel:
|
|
92
|
+
"Appears below the input box. Use for additional instructions.",
|
|
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 },
|
|
84
106
|
},
|
|
85
107
|
],
|
|
86
108
|
});
|
|
@@ -91,10 +113,30 @@ const configuration_workflow = (req) =>
|
|
|
91
113
|
|
|
92
114
|
const get_state_fields = () => [];
|
|
93
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
|
+
|
|
94
136
|
const run = async (
|
|
95
137
|
table_id,
|
|
96
138
|
viewname,
|
|
97
|
-
{ action_id, show_prev_runs, placeholder },
|
|
139
|
+
{ action_id, show_prev_runs, placeholder, explainer, image_upload },
|
|
98
140
|
state,
|
|
99
141
|
{ res, req }
|
|
100
142
|
) => {
|
|
@@ -115,13 +157,33 @@ const run = async (
|
|
|
115
157
|
for (const interact of run.context.interactions) {
|
|
116
158
|
switch (interact.role) {
|
|
117
159
|
case "user":
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
+
);
|
|
125
187
|
break;
|
|
126
188
|
case "assistant":
|
|
127
189
|
case "system":
|
|
@@ -209,7 +271,7 @@ const run = async (
|
|
|
209
271
|
}
|
|
210
272
|
const input_form = form(
|
|
211
273
|
{
|
|
212
|
-
onsubmit: `event.preventDefault();spin_send_button();view_post('${viewname}', 'interact',
|
|
274
|
+
onsubmit: `event.preventDefault();spin_send_button();view_post('${viewname}', 'interact', new FormData(this), processCopilotResponse);return false;`,
|
|
213
275
|
class: "form-namespace copilot mt-2",
|
|
214
276
|
method: "post",
|
|
215
277
|
},
|
|
@@ -238,7 +300,9 @@ const run = async (
|
|
|
238
300
|
span(
|
|
239
301
|
{ class: "submit-button p-2", onclick: "$('form.copilot').submit()" },
|
|
240
302
|
i({ id: "sendbuttonicon", class: "far fa-paper-plane" })
|
|
241
|
-
)
|
|
303
|
+
),
|
|
304
|
+
image_upload && uploadForm(viewname, req),
|
|
305
|
+
explainer && small({ class: "explainer" }, i(explainer))
|
|
242
306
|
)
|
|
243
307
|
);
|
|
244
308
|
|
|
@@ -283,17 +347,6 @@ const run = async (
|
|
|
283
347
|
{ class: "card" },
|
|
284
348
|
div(
|
|
285
349
|
{ class: "card-body" },
|
|
286
|
-
script({
|
|
287
|
-
src: `/static_assets/${db.connectObj.version_tag}/mermaid.min.js`,
|
|
288
|
-
}),
|
|
289
|
-
script(
|
|
290
|
-
{ type: "module" },
|
|
291
|
-
`mermaid.initialize({securityLevel: 'loose'${
|
|
292
|
-
getState().getLightDarkMode(req.user) === "dark"
|
|
293
|
-
? ",theme: 'dark',"
|
|
294
|
-
: ""
|
|
295
|
-
}});`
|
|
296
|
-
),
|
|
297
350
|
div({ id: "copilotinteractions" }, runInteractions),
|
|
298
351
|
input_form,
|
|
299
352
|
style(
|
|
@@ -305,11 +358,22 @@ const run = async (
|
|
|
305
358
|
div.prevcopilotrun i.fa-trash-alt {display: none;}
|
|
306
359
|
div.prevcopilotrun:hover i.fa-trash-alt {display: block;}
|
|
307
360
|
.copilot-entry .submit-button:hover { cursor: pointer}
|
|
361
|
+
.copilot-entry span.attach_agent_image_wrap i:hover { cursor: pointer}
|
|
308
362
|
|
|
309
363
|
.copilot-entry .submit-button {
|
|
310
364
|
position: relative;
|
|
311
365
|
top: -1.8rem;
|
|
312
366
|
left: 0.1rem;
|
|
367
|
+
}
|
|
368
|
+
.copilot-entry span.attach_agent_image_wrap {
|
|
369
|
+
position: relative;
|
|
370
|
+
top: -1.8rem;
|
|
371
|
+
left: 0.2rem;
|
|
372
|
+
}
|
|
373
|
+
.copilot-entry .explainer {
|
|
374
|
+
position: relative;
|
|
375
|
+
top: -1.2rem;
|
|
376
|
+
display: block;
|
|
313
377
|
}
|
|
314
378
|
.copilot-entry {margin-bottom: -1.25rem; margin-top: 1rem;}
|
|
315
379
|
p.prevrun_content {
|
|
@@ -320,17 +384,25 @@ const run = async (
|
|
|
320
384
|
text-overflow: ellipsis;}`
|
|
321
385
|
),
|
|
322
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);
|
|
323
390
|
$("#sendbuttonicon").attr("class","far fa-paper-plane");
|
|
324
391
|
const $runidin= $("input[name=run_id")
|
|
325
392
|
if(res.run_id && (!$runidin.val() || $runidin.val()=="undefined"))
|
|
326
393
|
$runidin.val(res.run_id);
|
|
327
394
|
const wrapSegment = (html, who) => '<div class="interaction-segment"><span class="badge bg-secondary">'+who+'</span>'+html+'</div>'
|
|
328
395
|
$("#copilotinteractions").append(wrapSegment('<p>'+$("textarea[name=userinput]").val()+'</p>', "You"))
|
|
329
|
-
|
|
396
|
+
if(hadFile)
|
|
397
|
+
$("#copilotinteractions").append(wrapSegment('File', "You"))
|
|
398
|
+
$("textarea[name=userinput]").val("")
|
|
330
399
|
|
|
331
400
|
if(res.response)
|
|
332
401
|
$("#copilotinteractions").append(res.response)
|
|
333
402
|
}
|
|
403
|
+
function agent_file_attach(e) {
|
|
404
|
+
$(".attach_agent_image_wrap span.filename-label").text(e.target.files[0].name)
|
|
405
|
+
}
|
|
334
406
|
function restore_old_button_elem(btn) {
|
|
335
407
|
const oldText = $(btn).data("old-text");
|
|
336
408
|
btn.html(oldText);
|
|
@@ -390,12 +462,6 @@ const run = async (
|
|
|
390
462
|
: main_chat;
|
|
391
463
|
};
|
|
392
464
|
|
|
393
|
-
/*
|
|
394
|
-
|
|
395
|
-
build a workflow that asks the user for their name and age
|
|
396
|
-
|
|
397
|
-
*/
|
|
398
|
-
|
|
399
465
|
const interact = async (table_id, viewname, config, body, { req, res }) => {
|
|
400
466
|
const { userinput, run_id } = body;
|
|
401
467
|
let run;
|
|
@@ -416,6 +482,41 @@ const interact = async (table_id, viewname, config, body, { req, res }) => {
|
|
|
416
482
|
interactions: [{ role: "user", content: userinput }],
|
|
417
483
|
});
|
|
418
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
|
+
}
|
|
419
520
|
const action = await Trigger.findOne({ id: config.action_id });
|
|
420
521
|
|
|
421
522
|
return await process_interaction(run, action.configuration, req, action.name);
|
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
package/package.json
CHANGED
|
@@ -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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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: ["table_name", list_view_opts],
|
|
92
|
+
},
|
|
93
|
+
},
|
|
77
94
|
{
|
|
78
95
|
name: "hidden_fields",
|
|
79
96
|
label: "Hide fields",
|
|
@@ -90,6 +107,7 @@ class RetrievalByEmbedding {
|
|
|
90
107
|
name: "add_sys_prompt",
|
|
91
108
|
label: "Additional prompt",
|
|
92
109
|
type: "String",
|
|
110
|
+
fieldview: "textarea",
|
|
93
111
|
},
|
|
94
112
|
];
|
|
95
113
|
}
|
|
@@ -152,6 +170,24 @@ class RetrievalByEmbedding {
|
|
|
152
170
|
return { rows: docs };
|
|
153
171
|
}
|
|
154
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
|
+
},
|
|
155
191
|
function: {
|
|
156
192
|
name: this.toolName,
|
|
157
193
|
description: `Search the ${table_docs.name``} archive${
|
package/skills/FTSRetrieval.js
CHANGED
|
@@ -26,6 +26,10 @@ class RetrievalByFullTextSearch {
|
|
|
26
26
|
return `Use the ${this.toolName} tool to search the ${
|
|
27
27
|
this.table_name
|
|
28
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
|
+
}${
|
|
29
33
|
this.add_sys_prompt
|
|
30
34
|
? ` Additional information for the ${this.toolName} tool: ${this.add_sys_prompt}`
|
|
31
35
|
: ""
|
|
@@ -78,6 +82,7 @@ class RetrievalByFullTextSearch {
|
|
|
78
82
|
name: "add_sys_prompt",
|
|
79
83
|
label: "Additional prompt",
|
|
80
84
|
type: "String",
|
|
85
|
+
fieldview: "textarea",
|
|
81
86
|
},
|
|
82
87
|
/*{
|
|
83
88
|
name: "contents_expr",
|
package/skills/Table.js
ADDED
|
@@ -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 };
|