@saltcorn/agents 0.1.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/LICENSE +21 -0
- package/README.md +2 -0
- package/agent-view.js +608 -0
- package/common.js +92 -0
- package/index.js +100 -0
- package/package.json +33 -0
- package/skills/AdaptiveFeedback.js +21 -0
- package/skills/EmbeddingRetrieval.js +149 -0
- package/skills/FTSRetrieval.js +157 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Saltcorn
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
package/agent-view.js
ADDED
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
const Field = require("@saltcorn/data/models/field");
|
|
2
|
+
const Table = require("@saltcorn/data/models/table");
|
|
3
|
+
const Form = require("@saltcorn/data/models/form");
|
|
4
|
+
const View = require("@saltcorn/data/models/view");
|
|
5
|
+
const Trigger = require("@saltcorn/data/models/trigger");
|
|
6
|
+
const FieldRepeat = require("@saltcorn/data/models/fieldrepeat");
|
|
7
|
+
const db = require("@saltcorn/data/db");
|
|
8
|
+
const WorkflowRun = require("@saltcorn/data/models/workflow_run");
|
|
9
|
+
const Workflow = require("@saltcorn/data/models/workflow");
|
|
10
|
+
const { localeDateTime } = require("@saltcorn/markup");
|
|
11
|
+
const {
|
|
12
|
+
div,
|
|
13
|
+
script,
|
|
14
|
+
domReady,
|
|
15
|
+
pre,
|
|
16
|
+
code,
|
|
17
|
+
input,
|
|
18
|
+
h4,
|
|
19
|
+
style,
|
|
20
|
+
h5,
|
|
21
|
+
button,
|
|
22
|
+
text_attr,
|
|
23
|
+
i,
|
|
24
|
+
p,
|
|
25
|
+
span,
|
|
26
|
+
small,
|
|
27
|
+
form,
|
|
28
|
+
textarea,
|
|
29
|
+
a,
|
|
30
|
+
} = require("@saltcorn/markup/tags");
|
|
31
|
+
const { getState } = require("@saltcorn/data/db/state");
|
|
32
|
+
const {
|
|
33
|
+
incompleteCfgMsg,
|
|
34
|
+
getCompletionArguments,
|
|
35
|
+
find_tool,
|
|
36
|
+
} = require("./common");
|
|
37
|
+
const MarkdownIt = require("markdown-it"),
|
|
38
|
+
md = new MarkdownIt();
|
|
39
|
+
|
|
40
|
+
const configuration_workflow = (req) =>
|
|
41
|
+
new Workflow({
|
|
42
|
+
steps: [
|
|
43
|
+
{
|
|
44
|
+
name: req.__("Agent action"),
|
|
45
|
+
form: async (context) => {
|
|
46
|
+
const agent_actions = await Trigger.find({ action: "Agent" });
|
|
47
|
+
return new Form({
|
|
48
|
+
fields: [
|
|
49
|
+
{
|
|
50
|
+
name: "action_id",
|
|
51
|
+
label: "Agent action",
|
|
52
|
+
type: "String",
|
|
53
|
+
required: true,
|
|
54
|
+
attributes: {
|
|
55
|
+
options: agent_actions.map((a) => ({
|
|
56
|
+
label: a.name,
|
|
57
|
+
name: a.id,
|
|
58
|
+
})),
|
|
59
|
+
},
|
|
60
|
+
sublabel:
|
|
61
|
+
"A trigger with <code>Agent</code> action. " +
|
|
62
|
+
a(
|
|
63
|
+
{
|
|
64
|
+
"data-dyn-href": `\`/actions/configure/\${action_id}\``,
|
|
65
|
+
target: "_blank",
|
|
66
|
+
},
|
|
67
|
+
req.__("Configure")
|
|
68
|
+
),
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: "show_prev_runs",
|
|
72
|
+
label: "Show previous runs",
|
|
73
|
+
type: "Bool",
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
});
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const get_state_fields = () => [];
|
|
83
|
+
|
|
84
|
+
const run = async (
|
|
85
|
+
table_id,
|
|
86
|
+
viewname,
|
|
87
|
+
{ action_id, show_prev_runs },
|
|
88
|
+
state,
|
|
89
|
+
{ res, req }
|
|
90
|
+
) => {
|
|
91
|
+
const action = await Trigger.findOne({ id: action_id });
|
|
92
|
+
const prevRuns = (
|
|
93
|
+
await WorkflowRun.find(
|
|
94
|
+
{ trigger_id: action.id, started_by: req.user?.id },
|
|
95
|
+
{ orderBy: "started_at", orderDesc: true, limit: 30 }
|
|
96
|
+
)
|
|
97
|
+
).filter((r) => r.context.interactions);
|
|
98
|
+
|
|
99
|
+
const cfgMsg = incompleteCfgMsg();
|
|
100
|
+
if (cfgMsg) return cfgMsg;
|
|
101
|
+
let runInteractions = "";
|
|
102
|
+
if (state.run_id) {
|
|
103
|
+
const run = prevRuns.find((r) => r.id == state.run_id);
|
|
104
|
+
const interactMarkups = [];
|
|
105
|
+
for (const interact of run.context.interactions) {
|
|
106
|
+
switch (interact.role) {
|
|
107
|
+
case "user":
|
|
108
|
+
interactMarkups.push(
|
|
109
|
+
div(
|
|
110
|
+
{ class: "interaction-segment" },
|
|
111
|
+
span({ class: "badge bg-secondary" }, "You"),
|
|
112
|
+
md.render(interact.content)
|
|
113
|
+
)
|
|
114
|
+
);
|
|
115
|
+
break;
|
|
116
|
+
case "assistant":
|
|
117
|
+
case "system":
|
|
118
|
+
if (interact.tool_calls) {
|
|
119
|
+
if (interact.content) {
|
|
120
|
+
interactMarkups.push(
|
|
121
|
+
div(
|
|
122
|
+
{ class: "interaction-segment" },
|
|
123
|
+
span({ class: "badge bg-secondary" }, "Copilot"),
|
|
124
|
+
typeof interact.content === "string"
|
|
125
|
+
? md.render(interact.content)
|
|
126
|
+
: interact.content
|
|
127
|
+
)
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
for (const tool_call of interact.tool_calls) {
|
|
131
|
+
const toolSkill = find_tool(
|
|
132
|
+
tool_call.function.name,
|
|
133
|
+
action.configuration
|
|
134
|
+
);
|
|
135
|
+
if (toolSkill) {
|
|
136
|
+
const row = JSON.parse(tool_call.function.arguments);
|
|
137
|
+
if (toolSkill.tool.renderToolCall) {
|
|
138
|
+
const rendered = await toolSkill.tool.renderToolCall(row, {
|
|
139
|
+
req,
|
|
140
|
+
});
|
|
141
|
+
if (rendered)
|
|
142
|
+
interactMarkups.push(
|
|
143
|
+
wrapSegment(
|
|
144
|
+
wrapCard(
|
|
145
|
+
toolSkill.skill.skill_label ||
|
|
146
|
+
toolSkill.skill.constructor.skill_name,
|
|
147
|
+
rendered
|
|
148
|
+
),
|
|
149
|
+
"Copilot"
|
|
150
|
+
)
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
} else
|
|
156
|
+
interactMarkups.push(
|
|
157
|
+
div(
|
|
158
|
+
{ class: "interaction-segment" },
|
|
159
|
+
span({ class: "badge bg-secondary" }, "Copilot"),
|
|
160
|
+
typeof interact.content === "string"
|
|
161
|
+
? md.render(interact.content)
|
|
162
|
+
: interact.content
|
|
163
|
+
)
|
|
164
|
+
);
|
|
165
|
+
break;
|
|
166
|
+
case "tool":
|
|
167
|
+
if (interact.content !== "Action run") {
|
|
168
|
+
let markupContent;
|
|
169
|
+
console.log("interact", interact);
|
|
170
|
+
const toolSkill = find_tool(interact.name, action.configuration);
|
|
171
|
+
try {
|
|
172
|
+
if (toolSkill?.tool?.renderToolResponse)
|
|
173
|
+
markupContent = await toolSkill?.tool?.renderToolResponse?.(
|
|
174
|
+
JSON.parse(interact.content),
|
|
175
|
+
{
|
|
176
|
+
req,
|
|
177
|
+
}
|
|
178
|
+
);
|
|
179
|
+
} catch {
|
|
180
|
+
markupContent = pre(interact.content);
|
|
181
|
+
}
|
|
182
|
+
if (markupContent)
|
|
183
|
+
interactMarkups.push(
|
|
184
|
+
wrapSegment(
|
|
185
|
+
wrapCard(
|
|
186
|
+
toolSkill?.skill?.skill_label ||
|
|
187
|
+
toolSkill?.skill?.constructor.skill_name ||
|
|
188
|
+
interact.name,
|
|
189
|
+
markupContent
|
|
190
|
+
),
|
|
191
|
+
"Copilot"
|
|
192
|
+
)
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
runInteractions = interactMarkups.join("");
|
|
199
|
+
}
|
|
200
|
+
const input_form = form(
|
|
201
|
+
{
|
|
202
|
+
onsubmit: `event.preventDefault();spin_send_button();view_post('${viewname}', 'interact', $(this).serialize(), processCopilotResponse);return false;`,
|
|
203
|
+
class: "form-namespace copilot mt-2",
|
|
204
|
+
method: "post",
|
|
205
|
+
},
|
|
206
|
+
input({
|
|
207
|
+
type: "hidden",
|
|
208
|
+
name: "_csrf",
|
|
209
|
+
value: req.csrfToken(),
|
|
210
|
+
}),
|
|
211
|
+
input({
|
|
212
|
+
type: "hidden",
|
|
213
|
+
class: "form-control ",
|
|
214
|
+
name: "run_id",
|
|
215
|
+
value: state.run_id ? +state.run_id : undefined,
|
|
216
|
+
}),
|
|
217
|
+
div(
|
|
218
|
+
{ class: "copilot-entry" },
|
|
219
|
+
textarea({
|
|
220
|
+
class: "form-control",
|
|
221
|
+
name: "userinput",
|
|
222
|
+
"data-fieldname": "userinput",
|
|
223
|
+
placeholder: "How can I help you?",
|
|
224
|
+
id: "inputuserinput",
|
|
225
|
+
rows: "3",
|
|
226
|
+
autofocus: true,
|
|
227
|
+
}),
|
|
228
|
+
span(
|
|
229
|
+
{ class: "submit-button p-2", onclick: "$('form.copilot').submit()" },
|
|
230
|
+
i({ id: "sendbuttonicon", class: "far fa-paper-plane" })
|
|
231
|
+
)
|
|
232
|
+
)
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
const prev_runs_side_bar = div(
|
|
236
|
+
div(
|
|
237
|
+
{
|
|
238
|
+
class: "d-flex justify-content-between align-middle mb-2",
|
|
239
|
+
},
|
|
240
|
+
h5("Sessions"),
|
|
241
|
+
|
|
242
|
+
button(
|
|
243
|
+
{
|
|
244
|
+
type: "button",
|
|
245
|
+
class: "btn btn-secondary btn-sm py-0",
|
|
246
|
+
style: "font-size: 0.9em;height:1.5em",
|
|
247
|
+
onclick: "unset_state_field('run_id')",
|
|
248
|
+
title: "New session",
|
|
249
|
+
},
|
|
250
|
+
i({ class: "fas fa-redo fa-sm" })
|
|
251
|
+
)
|
|
252
|
+
),
|
|
253
|
+
prevRuns.map((run) =>
|
|
254
|
+
div(
|
|
255
|
+
{
|
|
256
|
+
onclick: `set_state_field('run_id',${run.id})`,
|
|
257
|
+
class: "prevcopilotrun border p-2",
|
|
258
|
+
},
|
|
259
|
+
div(
|
|
260
|
+
{ class: "d-flex justify-content-between" },
|
|
261
|
+
localeDateTime(run.started_at),
|
|
262
|
+
i({
|
|
263
|
+
class: "far fa-trash-alt",
|
|
264
|
+
onclick: `delprevrun(event, ${run.id})`,
|
|
265
|
+
})
|
|
266
|
+
),
|
|
267
|
+
|
|
268
|
+
p({ class: "prevrun_content" }, run.context.interactions[0]?.content)
|
|
269
|
+
)
|
|
270
|
+
)
|
|
271
|
+
);
|
|
272
|
+
const main_chat = div(
|
|
273
|
+
{ class: "card" },
|
|
274
|
+
div(
|
|
275
|
+
{ class: "card-body" },
|
|
276
|
+
script({
|
|
277
|
+
src: `/static_assets/${db.connectObj.version_tag}/mermaid.min.js`,
|
|
278
|
+
}),
|
|
279
|
+
script(
|
|
280
|
+
{ type: "module" },
|
|
281
|
+
`mermaid.initialize({securityLevel: 'loose'${
|
|
282
|
+
getState().getLightDarkMode(req.user) === "dark"
|
|
283
|
+
? ",theme: 'dark',"
|
|
284
|
+
: ""
|
|
285
|
+
}});`
|
|
286
|
+
),
|
|
287
|
+
div({ id: "copilotinteractions" }, runInteractions),
|
|
288
|
+
input_form,
|
|
289
|
+
style(
|
|
290
|
+
`div.interaction-segment:not(:first-child) {border-top: 1px solid #e7e7e7; }
|
|
291
|
+
div.interaction-segment {padding-top: 5px;padding-bottom: 5px;}
|
|
292
|
+
div.interaction-segment p {margin-bottom: 0px;}
|
|
293
|
+
div.interaction-segment div.card {margin-top: 0.5rem;}
|
|
294
|
+
div.prevcopilotrun:hover {cursor: pointer; background-color: var(--tblr-secondary-bg-subtle, var(--bs-secondary-bg-subtle, gray));}
|
|
295
|
+
div.prevcopilotrun i.fa-trash-alt {display: none;}
|
|
296
|
+
div.prevcopilotrun:hover i.fa-trash-alt {display: block;}
|
|
297
|
+
.copilot-entry .submit-button:hover { cursor: pointer}
|
|
298
|
+
|
|
299
|
+
.copilot-entry .submit-button {
|
|
300
|
+
position: relative;
|
|
301
|
+
top: -1.8rem;
|
|
302
|
+
left: 0.1rem;
|
|
303
|
+
}
|
|
304
|
+
.copilot-entry {margin-bottom: -1.25rem; margin-top: 1rem;}
|
|
305
|
+
p.prevrun_content {
|
|
306
|
+
white-space: nowrap;
|
|
307
|
+
overflow: hidden;
|
|
308
|
+
margin-bottom: 0px;
|
|
309
|
+
display: block;
|
|
310
|
+
text-overflow: ellipsis;}`
|
|
311
|
+
),
|
|
312
|
+
script(`function processCopilotResponse(res) {
|
|
313
|
+
$("#sendbuttonicon").attr("class","far fa-paper-plane");
|
|
314
|
+
const $runidin= $("input[name=run_id")
|
|
315
|
+
if(res.run_id && (!$runidin.val() || $runidin.val()=="undefined"))
|
|
316
|
+
$runidin.val(res.run_id);
|
|
317
|
+
const wrapSegment = (html, who) => '<div class="interaction-segment"><span class="badge bg-secondary">'+who+'</span>'+html+'</div>'
|
|
318
|
+
$("#copilotinteractions").append(wrapSegment('<p>'+$("textarea[name=userinput]").val()+'</p>', "You"))
|
|
319
|
+
$("textarea[name=userinput]").val("")
|
|
320
|
+
|
|
321
|
+
if(res.response)
|
|
322
|
+
$("#copilotinteractions").append(res.response)
|
|
323
|
+
}
|
|
324
|
+
function restore_old_button_elem(btn) {
|
|
325
|
+
const oldText = $(btn).data("old-text");
|
|
326
|
+
btn.html(oldText);
|
|
327
|
+
btn.css({ width: "" }).prop("disabled", false);
|
|
328
|
+
btn.removeData("old-text");
|
|
329
|
+
}
|
|
330
|
+
function delprevrun(e, runid) {
|
|
331
|
+
e.preventDefault();
|
|
332
|
+
e.stopPropagation();
|
|
333
|
+
view_post('${viewname}', 'delprevrun', {run_id:runid})
|
|
334
|
+
$(e.target).closest(".prevcopilotrun").remove()
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
function processExecuteResponse(res) {
|
|
338
|
+
const btn = $("#exec-"+res.fcall_id)
|
|
339
|
+
restore_old_button_elem($("#exec-"+res.fcall_id))
|
|
340
|
+
btn.prop('disabled', true);
|
|
341
|
+
btn.html('<i class="fas fa-check me-1"></i>Applied')
|
|
342
|
+
btn.removeClass("btn-primary")
|
|
343
|
+
btn.addClass("btn-secondary")
|
|
344
|
+
if(res.postExec) {
|
|
345
|
+
$('#postexec-'+res.fcall_id).html(res.postExec)
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
function submitOnEnter(event) {
|
|
349
|
+
if (event.which === 13) {
|
|
350
|
+
if (!event.repeat) {
|
|
351
|
+
const newEvent = new Event("submit", {cancelable: true});
|
|
352
|
+
event.target.form.dispatchEvent(newEvent);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
event.preventDefault(); // Prevents the addition of a new line in the text field
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
document.getElementById("inputuserinput").addEventListener("keydown", submitOnEnter);
|
|
359
|
+
function spin_send_button() {
|
|
360
|
+
$("#sendbuttonicon").attr("class","fas fa-spinner fa-spin");
|
|
361
|
+
}
|
|
362
|
+
`)
|
|
363
|
+
)
|
|
364
|
+
);
|
|
365
|
+
return show_prev_runs
|
|
366
|
+
? {
|
|
367
|
+
widths: [3, 9],
|
|
368
|
+
gx: 3,
|
|
369
|
+
besides: [
|
|
370
|
+
{
|
|
371
|
+
type: "container",
|
|
372
|
+
contents: prev_runs_side_bar,
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
type: "container",
|
|
376
|
+
contents: main_chat,
|
|
377
|
+
},
|
|
378
|
+
],
|
|
379
|
+
}
|
|
380
|
+
: main_chat;
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
/*
|
|
384
|
+
|
|
385
|
+
build a workflow that asks the user for their name and age
|
|
386
|
+
|
|
387
|
+
*/
|
|
388
|
+
|
|
389
|
+
const interact = async (table_id, viewname, config, body, { req, res }) => {
|
|
390
|
+
const { userinput, run_id } = body;
|
|
391
|
+
let run;
|
|
392
|
+
if (!run_id || run_id === "undefined")
|
|
393
|
+
run = await WorkflowRun.create({
|
|
394
|
+
status: "Running",
|
|
395
|
+
started_by: req.user?.id,
|
|
396
|
+
trigger_id: config.action_id,
|
|
397
|
+
context: {
|
|
398
|
+
implemented_fcall_ids: [],
|
|
399
|
+
interactions: [{ role: "user", content: userinput }],
|
|
400
|
+
funcalls: {},
|
|
401
|
+
},
|
|
402
|
+
});
|
|
403
|
+
else {
|
|
404
|
+
run = await WorkflowRun.findOne({ id: +run_id });
|
|
405
|
+
await addToContext(run, {
|
|
406
|
+
interactions: [{ role: "user", content: userinput }],
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
return await process_interaction(run, config, req);
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
const delprevrun = async (table_id, viewname, config, body, { req, res }) => {
|
|
413
|
+
const { run_id } = body;
|
|
414
|
+
let run;
|
|
415
|
+
|
|
416
|
+
run = await WorkflowRun.findOne({ id: +run_id });
|
|
417
|
+
if (req.user?.role_id === 1 || req.user?.id === run.started_by)
|
|
418
|
+
await run.delete();
|
|
419
|
+
|
|
420
|
+
return;
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
const process_interaction = async (run, config, req, prevResponses = []) => {
|
|
424
|
+
const action = await Trigger.findOne({ id: config.action_id });
|
|
425
|
+
const complArgs = await getCompletionArguments(action.configuration);
|
|
426
|
+
complArgs.chat = run.context.interactions;
|
|
427
|
+
//complArgs.debugResult = true;
|
|
428
|
+
console.log("complArgs", JSON.stringify(complArgs, null, 2));
|
|
429
|
+
|
|
430
|
+
const answer = await getState().functions.llm_generate.run("", complArgs);
|
|
431
|
+
console.log("answer", answer);
|
|
432
|
+
await addToContext(run, {
|
|
433
|
+
interactions:
|
|
434
|
+
typeof answer === "object" && answer.tool_calls
|
|
435
|
+
? [
|
|
436
|
+
{
|
|
437
|
+
role: "assistant",
|
|
438
|
+
tool_calls: answer.tool_calls,
|
|
439
|
+
content: answer.content,
|
|
440
|
+
},
|
|
441
|
+
]
|
|
442
|
+
: [{ role: "assistant", content: answer }],
|
|
443
|
+
});
|
|
444
|
+
const responses = [];
|
|
445
|
+
|
|
446
|
+
if (typeof answer === "object" && answer.tool_calls) {
|
|
447
|
+
if (answer.content)
|
|
448
|
+
responses.push(wrapSegment(md.render(answer.content), "Copilot"));
|
|
449
|
+
//const actions = [];
|
|
450
|
+
let hasResult = false;
|
|
451
|
+
for (const tool_call of answer.tool_calls) {
|
|
452
|
+
console.log("call function", tool_call.function);
|
|
453
|
+
|
|
454
|
+
await addToContext(run, {
|
|
455
|
+
funcalls: { [tool_call.id]: tool_call.function },
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
const tool = find_tool(tool_call.function.name, action.configuration);
|
|
459
|
+
|
|
460
|
+
if (tool) {
|
|
461
|
+
if (tool.tool.renderToolCall) {
|
|
462
|
+
const row = JSON.parse(tool_call.function.arguments);
|
|
463
|
+
const rendered = await tool.tool.renderToolCall(row, {
|
|
464
|
+
req,
|
|
465
|
+
});
|
|
466
|
+
if (rendered)
|
|
467
|
+
responses.push(
|
|
468
|
+
wrapSegment(
|
|
469
|
+
wrapCard(
|
|
470
|
+
tool.skill.skill_label || tool.skill.constructor.skill_name,
|
|
471
|
+
rendered
|
|
472
|
+
),
|
|
473
|
+
"Copilot"
|
|
474
|
+
)
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
hasResult = true;
|
|
478
|
+
const result = await tool.tool.process(
|
|
479
|
+
JSON.parse(tool_call.function.arguments)
|
|
480
|
+
);
|
|
481
|
+
if (
|
|
482
|
+
(typeof result === "object" && Object.keys(result || {}).length) ||
|
|
483
|
+
typeof result === "string"
|
|
484
|
+
) {
|
|
485
|
+
if (tool.tool.renderToolResponse) {
|
|
486
|
+
const rendered = await tool.tool.renderToolResponse(result, {
|
|
487
|
+
req,
|
|
488
|
+
});
|
|
489
|
+
if (rendered)
|
|
490
|
+
responses.push(
|
|
491
|
+
wrapSegment(
|
|
492
|
+
wrapCard(
|
|
493
|
+
tool.skill.skill_label || tool.skill.constructor.skill_name,
|
|
494
|
+
rendered
|
|
495
|
+
),
|
|
496
|
+
"Copilot"
|
|
497
|
+
)
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
hasResult = true;
|
|
501
|
+
}
|
|
502
|
+
await addToContext(run, {
|
|
503
|
+
interactions: [
|
|
504
|
+
{
|
|
505
|
+
role: "tool",
|
|
506
|
+
tool_call_id: tool_call.id,
|
|
507
|
+
name: tool_call.function.name,
|
|
508
|
+
content:
|
|
509
|
+
result && typeof result !== "string"
|
|
510
|
+
? JSON.stringify(result)
|
|
511
|
+
: result || "Action run",
|
|
512
|
+
},
|
|
513
|
+
],
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (hasResult)
|
|
518
|
+
return await process_interaction(run, config, req, [
|
|
519
|
+
...prevResponses,
|
|
520
|
+
...responses,
|
|
521
|
+
]);
|
|
522
|
+
} else responses.push(wrapSegment(md.render(answer), "Copilot"));
|
|
523
|
+
|
|
524
|
+
return {
|
|
525
|
+
json: {
|
|
526
|
+
success: "ok",
|
|
527
|
+
response: [...prevResponses, ...responses].join(""),
|
|
528
|
+
run_id: run.id,
|
|
529
|
+
},
|
|
530
|
+
};
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
const wrapSegment = (html, who) =>
|
|
534
|
+
'<div class="interaction-segment"><span class="badge bg-secondary">' +
|
|
535
|
+
who +
|
|
536
|
+
"</span>" +
|
|
537
|
+
html +
|
|
538
|
+
"</div>";
|
|
539
|
+
|
|
540
|
+
const wrapCard = (title, ...inners) =>
|
|
541
|
+
span({ class: "badge bg-info ms-1" }, title) +
|
|
542
|
+
div(
|
|
543
|
+
{ class: "card mb-3 bg-secondary-subtle" },
|
|
544
|
+
div({ class: "card-body" }, inners)
|
|
545
|
+
);
|
|
546
|
+
|
|
547
|
+
const wrapAction = (
|
|
548
|
+
inner_markup,
|
|
549
|
+
viewname,
|
|
550
|
+
tool_call,
|
|
551
|
+
actionClass,
|
|
552
|
+
implemented,
|
|
553
|
+
run
|
|
554
|
+
) =>
|
|
555
|
+
wrapCard(
|
|
556
|
+
actionClass.title,
|
|
557
|
+
inner_markup + implemented
|
|
558
|
+
? button(
|
|
559
|
+
{
|
|
560
|
+
type: "button",
|
|
561
|
+
class: "btn btn-secondary d-block mt-3 float-end",
|
|
562
|
+
disabled: true,
|
|
563
|
+
},
|
|
564
|
+
i({ class: "fas fa-check me-1" }),
|
|
565
|
+
"Applied"
|
|
566
|
+
)
|
|
567
|
+
: button(
|
|
568
|
+
{
|
|
569
|
+
type: "button",
|
|
570
|
+
id: "exec-" + tool_call.id,
|
|
571
|
+
class: "btn btn-primary d-block mt-3 float-end",
|
|
572
|
+
onclick: `press_store_button(this, true);view_post('${viewname}', 'execute', {fcall_id: '${tool_call.id}', run_id: ${run.id}}, processExecuteResponse)`,
|
|
573
|
+
},
|
|
574
|
+
"Apply"
|
|
575
|
+
) + div({ id: "postexec-" + tool_call.id })
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
const addToContext = async (run, newCtx) => {
|
|
579
|
+
if (run.addToContext) return await run.addToContext(newCtx);
|
|
580
|
+
let changed = true;
|
|
581
|
+
Object.keys(newCtx).forEach((k) => {
|
|
582
|
+
if (Array.isArray(run.context[k])) {
|
|
583
|
+
if (!Array.isArray(newCtx[k]))
|
|
584
|
+
throw new Error("Must be array to append to array");
|
|
585
|
+
run.context[k].push(...newCtx[k]);
|
|
586
|
+
changed = true;
|
|
587
|
+
} else if (typeof run.context[k] === "object") {
|
|
588
|
+
if (typeof newCtx[k] !== "object")
|
|
589
|
+
throw new Error("Must be object to append to object");
|
|
590
|
+
Object.assign(run.context[k], newCtx[k]);
|
|
591
|
+
changed = true;
|
|
592
|
+
} else {
|
|
593
|
+
run.context[k] = newCtx[k];
|
|
594
|
+
changed = true;
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
if (changed) await run.update({ context: run.context });
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
module.exports = {
|
|
601
|
+
name: "Agent Chat",
|
|
602
|
+
configuration_workflow,
|
|
603
|
+
display_state_form: false,
|
|
604
|
+
get_state_fields,
|
|
605
|
+
tableless: true,
|
|
606
|
+
run,
|
|
607
|
+
routes: { interact, delprevrun },
|
|
608
|
+
};
|
package/common.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
const { getState } = require("@saltcorn/data/db/state");
|
|
2
|
+
|
|
3
|
+
const get_skills = () => {
|
|
4
|
+
return [
|
|
5
|
+
//require("./skills/EmbeddingRetrieval"),
|
|
6
|
+
require("./skills/FTSRetrieval"),
|
|
7
|
+
//require("./skills/AdaptiveFeedback"),
|
|
8
|
+
];
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const get_skill_class = (type) => {
|
|
12
|
+
const classes = get_skills();
|
|
13
|
+
return classes.find((c) => c.skill_name === type);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const get_skill_instances = (config) => {
|
|
17
|
+
const instances = [];
|
|
18
|
+
for (const skillCfg of config.skills) {
|
|
19
|
+
const klass = get_skill_class(skillCfg.skill_type);
|
|
20
|
+
const skill = new klass(skillCfg);
|
|
21
|
+
instances.push(skill);
|
|
22
|
+
}
|
|
23
|
+
return instances;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const find_tool = (name, config) => {
|
|
27
|
+
const skills = get_skill_instances(config);
|
|
28
|
+
for (const skill of skills) {
|
|
29
|
+
const skillTools = skill.provideTools();
|
|
30
|
+
const tools = !skillTools
|
|
31
|
+
? []
|
|
32
|
+
: Array.isArray(skillTools)
|
|
33
|
+
? skillTools
|
|
34
|
+
: [skillTools];
|
|
35
|
+
const found = tools.find((t) => t?.function.name === name);
|
|
36
|
+
if (found) return { tool: found, skill };
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
const getCompletionArguments = async (config) => {
|
|
42
|
+
let tools = [];
|
|
43
|
+
|
|
44
|
+
let sysPrompts = [config.sys_prompt];
|
|
45
|
+
|
|
46
|
+
const skills = get_skill_instances(config);
|
|
47
|
+
for (const skill of skills) {
|
|
48
|
+
const sysPr = skill.systemPrompt();
|
|
49
|
+
if (sysPr) sysPrompts.push(sysPr);
|
|
50
|
+
const skillTools = skill.provideTools();
|
|
51
|
+
if (skillTools && Array.isArray(skillTools)) tools.push(...skillTools);
|
|
52
|
+
else if (skillTools) tools.push(skillTools);
|
|
53
|
+
}
|
|
54
|
+
if (tools.length === 0) tools = undefined;
|
|
55
|
+
return { tools, systemPrompt: sysPrompts.join("\n\n") };
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
const getCompletion = async (language, prompt) => {
|
|
60
|
+
return getState().functions.llm_generate.run(prompt, {
|
|
61
|
+
systemPrompt: `You are a helpful code assistant. Your language of choice is ${language}. Do not include any explanation, just generate the code block itself.`,
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const incompleteCfgMsg = () => {
|
|
66
|
+
const plugin_cfgs = getState().plugin_cfgs;
|
|
67
|
+
|
|
68
|
+
if (
|
|
69
|
+
!plugin_cfgs["@saltcorn/large-language-model"] &&
|
|
70
|
+
!plugin_cfgs["large-language-model"]
|
|
71
|
+
) {
|
|
72
|
+
const modName = Object.keys(plugin_cfgs).find((m) =>
|
|
73
|
+
m.includes("large-language-model")
|
|
74
|
+
);
|
|
75
|
+
if (modName)
|
|
76
|
+
return `LLM module not configured. Please configure <a href="/plugins/configure/${encodeURIComponent(
|
|
77
|
+
modName
|
|
78
|
+
)}">here<a> before using copilot.`;
|
|
79
|
+
else
|
|
80
|
+
return `LLM module not configured. Please install and configure <a href="/plugins">here<a> before using copilot.`;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
module.exports = {
|
|
85
|
+
get_skills,
|
|
86
|
+
get_skill_class,
|
|
87
|
+
incompleteCfgMsg,
|
|
88
|
+
getCompletion,
|
|
89
|
+
find_tool,
|
|
90
|
+
get_skill_instances,
|
|
91
|
+
getCompletionArguments
|
|
92
|
+
};
|
package/index.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
const Workflow = require("@saltcorn/data/models/workflow");
|
|
2
|
+
const Form = require("@saltcorn/data/models/form");
|
|
3
|
+
const FieldRepeat = require("@saltcorn/data/models/fieldrepeat");
|
|
4
|
+
const { features } = require("@saltcorn/data/db/state");
|
|
5
|
+
const { get_skills, getCompletionArguments } = require("./common");
|
|
6
|
+
const { applyAsync } = require("@saltcorn/data/utils");
|
|
7
|
+
const WorkflowRun = require("@saltcorn/data/models/workflow_run");
|
|
8
|
+
const { interpolate } = require("@saltcorn/data/utils");
|
|
9
|
+
const { getState } = require("@saltcorn/data/db/state");
|
|
10
|
+
|
|
11
|
+
module.exports = {
|
|
12
|
+
sc_plugin_api_version: 1,
|
|
13
|
+
dependencies: ["@saltcorn/large-language-model"],
|
|
14
|
+
viewtemplates: [require("./agent-view")],
|
|
15
|
+
actions: {
|
|
16
|
+
Agent: {
|
|
17
|
+
disableInBuilder: true,
|
|
18
|
+
disableInList: true,
|
|
19
|
+
disableInWorkflow: true,
|
|
20
|
+
configFields: async ({ table, mode }) => {
|
|
21
|
+
const skills = get_skills();
|
|
22
|
+
const skills_fields = [];
|
|
23
|
+
for (const skill of skills) {
|
|
24
|
+
if (skill.configFields) {
|
|
25
|
+
const fields = await applyAsync(skill.configFields, undefined);
|
|
26
|
+
for (const field of fields) {
|
|
27
|
+
if (!field.showIf) field.showIf = {};
|
|
28
|
+
field.showIf.skill_type = skill.skill_name;
|
|
29
|
+
skills_fields.push(field);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return [
|
|
34
|
+
...(table
|
|
35
|
+
? [
|
|
36
|
+
{
|
|
37
|
+
name: "prompt",
|
|
38
|
+
label: "Prompt",
|
|
39
|
+
sublabel:
|
|
40
|
+
"When triggered from table event or table view button. Use handlebars <code>{{}}</code> to access table fields. Ignored if run in Agent Chat view.",
|
|
41
|
+
type: "String",
|
|
42
|
+
required: true,
|
|
43
|
+
attributes: { options: table.fields.map((f) => f.name) },
|
|
44
|
+
},
|
|
45
|
+
]
|
|
46
|
+
: []),
|
|
47
|
+
{
|
|
48
|
+
name: "sys_prompt",
|
|
49
|
+
label: "System prompt",
|
|
50
|
+
sublabel: "Additional information for the system prompt",
|
|
51
|
+
type: "String",
|
|
52
|
+
fieldview: "textarea",
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
new FieldRepeat({
|
|
56
|
+
name: "skills",
|
|
57
|
+
label: "Skills",
|
|
58
|
+
fields: [
|
|
59
|
+
{
|
|
60
|
+
name: "skill_type",
|
|
61
|
+
label: "Type",
|
|
62
|
+
type: "String",
|
|
63
|
+
required: true,
|
|
64
|
+
attributes: { options: skills.map((s) => s.skill_name) },
|
|
65
|
+
},
|
|
66
|
+
...skills_fields,
|
|
67
|
+
],
|
|
68
|
+
}),
|
|
69
|
+
];
|
|
70
|
+
},
|
|
71
|
+
run: async ({ configuration, user, row, trigger_id, ...rest }) => {
|
|
72
|
+
const userinput = interpolate(configuration.prompt, row, user);
|
|
73
|
+
const run = await WorkflowRun.create({
|
|
74
|
+
status: "Running",
|
|
75
|
+
started_by: user?.id,
|
|
76
|
+
trigger_id: trigger_id || undefined,
|
|
77
|
+
context: {
|
|
78
|
+
implemented_fcall_ids: [],
|
|
79
|
+
interactions: [{ role: "user", content: userinput }],
|
|
80
|
+
funcalls: {},
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
const complArgs = await getCompletionArguments(configuration);
|
|
84
|
+
const answer = await getState().functions.llm_generate.run(
|
|
85
|
+
"",
|
|
86
|
+
complArgs
|
|
87
|
+
);
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/*
|
|
94
|
+
TODO
|
|
95
|
+
|
|
96
|
+
-embedding retrieval
|
|
97
|
+
-run as action
|
|
98
|
+
-view: set placeholder, labels
|
|
99
|
+
|
|
100
|
+
*/
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@saltcorn/agents",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "AI agents for Saltcorn",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"@saltcorn/data": "^0.9.0",
|
|
8
|
+
"underscore": "1.13.6",
|
|
9
|
+
"node-sql-parser": "4.15.0",
|
|
10
|
+
"markdown-it": "14.1.0",
|
|
11
|
+
"style-to-object": "1.0.8",
|
|
12
|
+
"node-html-parser": "7.0.1"
|
|
13
|
+
},
|
|
14
|
+
"author": "Tom Nielsen",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"repository": "github:saltcorn/agents",
|
|
17
|
+
"eslintConfig": {
|
|
18
|
+
"extends": "eslint:recommended",
|
|
19
|
+
"parserOptions": {
|
|
20
|
+
"ecmaVersion": 2022
|
|
21
|
+
},
|
|
22
|
+
"env": {
|
|
23
|
+
"node": true,
|
|
24
|
+
"es6": true
|
|
25
|
+
},
|
|
26
|
+
"rules": {
|
|
27
|
+
"no-unused-vars": "off",
|
|
28
|
+
"no-case-declarations": "off",
|
|
29
|
+
"no-empty": "warn",
|
|
30
|
+
"no-fallthrough": "warn"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const Workflow = require("@saltcorn/data/models/workflow");
|
|
2
|
+
const Form = require("@saltcorn/data/models/form");
|
|
3
|
+
|
|
4
|
+
class AdaptiveFeedback {
|
|
5
|
+
static skill_name = "Adaptive Feedback";
|
|
6
|
+
static async configFields() {
|
|
7
|
+
return [
|
|
8
|
+
{
|
|
9
|
+
name: "label",
|
|
10
|
+
label: "Label",
|
|
11
|
+
type: "String",
|
|
12
|
+
},
|
|
13
|
+
];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
onEvolve() {}
|
|
17
|
+
|
|
18
|
+
provideTools() {}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = AdaptiveFeedback;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
const { div } = 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
|
+
|
|
9
|
+
class RetrievalByEmbedding {
|
|
10
|
+
static skill_name = "Retrieval by embedding";
|
|
11
|
+
|
|
12
|
+
constructor(cfg) {
|
|
13
|
+
Object.assign(this, cfg);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
get toolName() {
|
|
17
|
+
return `search_${this.table_name.replaceAll(" ", "")}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
systemPrompt() {
|
|
21
|
+
if (this.mode === "Tool") {
|
|
22
|
+
const table = Table.findOne(this.vec_field.split["."][0]);
|
|
23
|
+
|
|
24
|
+
return `Use the ${this.toolName} tool to search an archive named ${
|
|
25
|
+
table.name
|
|
26
|
+
}${
|
|
27
|
+
table.description ? ` (${table.description})` : ""
|
|
28
|
+
} for documents related to a search phrase or a question`;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
static async configFields() {
|
|
33
|
+
const allTables = await Table.find();
|
|
34
|
+
const tableOpts = [];
|
|
35
|
+
const relation_opts = {};
|
|
36
|
+
const list_view_opts = {};
|
|
37
|
+
for (const table of allTables) {
|
|
38
|
+
table.fields
|
|
39
|
+
.filter((f) => f.type?.name === "PGVector")
|
|
40
|
+
.forEach((f) => {
|
|
41
|
+
const relNm = `${table.name}.${f.name}`;
|
|
42
|
+
tableOpts.push(relNm);
|
|
43
|
+
const fkeys = table.fields
|
|
44
|
+
.filter((f) => f.is_fkey)
|
|
45
|
+
.map((f) => f.name);
|
|
46
|
+
relation_opts[relNm] = ["", ...fkeys];
|
|
47
|
+
|
|
48
|
+
list_view_opts[relNm] = []
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return [
|
|
52
|
+
{
|
|
53
|
+
name: "mode",
|
|
54
|
+
label: "Mode",
|
|
55
|
+
type: "String",
|
|
56
|
+
required: true,
|
|
57
|
+
attributes: { options: ["Tool", "Search on every user input"] },
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: "vec_field",
|
|
61
|
+
label: "Vector field",
|
|
62
|
+
sublabel: "Field to search for vector similarity",
|
|
63
|
+
type: "String",
|
|
64
|
+
required: true,
|
|
65
|
+
attributes: { options: tableOpts },
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: "doc_relation",
|
|
69
|
+
label: "Document relation",
|
|
70
|
+
sublabel:
|
|
71
|
+
"Optional. For each vector match, retrieve row in the table related by this key instead",
|
|
72
|
+
type: "String",
|
|
73
|
+
required: true,
|
|
74
|
+
attributes: { calcOptions: ["vec_field", relation_opts] },
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: "list_view",
|
|
78
|
+
label: "List view",
|
|
79
|
+
type: "String",
|
|
80
|
+
attributes: {
|
|
81
|
+
calcOptions: ["vec_field", list_view_opts],
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: "contents_expr",
|
|
86
|
+
label: "Contents string",
|
|
87
|
+
type: "String",
|
|
88
|
+
sublabel:
|
|
89
|
+
"Use handlebars (<code>{{ }}</code>) to access fields in the retrieved rows",
|
|
90
|
+
},
|
|
91
|
+
];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
onMessage(msgs) {
|
|
95
|
+
if (this.mode !== "Search on every user input") return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
provideTools() {
|
|
99
|
+
if (this.mode !== "Tool") return [];
|
|
100
|
+
const table0 = Table.findOne(this.vec_field.split["."][0]);
|
|
101
|
+
const table_docs = this.doc_relation
|
|
102
|
+
? Table.findOne(table0.getField(this.doc_relation).reftable_name)
|
|
103
|
+
: table0;
|
|
104
|
+
return {
|
|
105
|
+
type: "function",
|
|
106
|
+
process({ phrase_or_question }) {
|
|
107
|
+
return {
|
|
108
|
+
response: "There are no documents related to: " + phrase_or_question,
|
|
109
|
+
};
|
|
110
|
+
},
|
|
111
|
+
renderToolResponse: async ({ response, rows }, { req }) => {
|
|
112
|
+
if (rows) {
|
|
113
|
+
const view = View.findOne({ name: this.list_view });
|
|
114
|
+
|
|
115
|
+
if (view) {
|
|
116
|
+
const viewRes = await view.run(
|
|
117
|
+
{
|
|
118
|
+
[table_docs.pk_name]: {
|
|
119
|
+
in: rows.map((r) => r[table_docs.pk_name]),
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
{ req }
|
|
123
|
+
);
|
|
124
|
+
return viewRes;
|
|
125
|
+
} else return "";
|
|
126
|
+
}
|
|
127
|
+
return div({ class: "border border-success p-2 m-2" }, response);
|
|
128
|
+
},
|
|
129
|
+
function: {
|
|
130
|
+
name: this.toolName,
|
|
131
|
+
description: `Search the ${table_docs.name``} archive${
|
|
132
|
+
table_docs.description ? ` (${table_docs.description})` : ""
|
|
133
|
+
} for information related to a search phrase or a question. The relevant documents will be returned`,
|
|
134
|
+
parameters: {
|
|
135
|
+
type: "object",
|
|
136
|
+
required: ["phrase_or_question"],
|
|
137
|
+
properties: {
|
|
138
|
+
phrase_or_question: {
|
|
139
|
+
type: "string",
|
|
140
|
+
description: "The phrase or question to search the archive with",
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = RetrievalByEmbedding;
|
|
@@ -0,0 +1,157 @@
|
|
|
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
|
+
|
|
9
|
+
class RetrievalByFullTextSearch {
|
|
10
|
+
static skill_name = "Retrieval by full-text search";
|
|
11
|
+
|
|
12
|
+
get skill_label() {
|
|
13
|
+
return `Search ${this.table_name}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
constructor(cfg) {
|
|
17
|
+
Object.assign(this, cfg);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
get toolName() {
|
|
21
|
+
return `search_${this.table_name.replaceAll(" ", "")}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
systemPrompt() {
|
|
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`;
|
|
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: "mode",
|
|
44
|
+
label: "Mode",
|
|
45
|
+
type: "String",
|
|
46
|
+
required: true,
|
|
47
|
+
attributes: { options: ["Tool", "Search on every user input"] },
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "table_name",
|
|
51
|
+
label: "Table",
|
|
52
|
+
sublabel: "Which table to search",
|
|
53
|
+
type: "String",
|
|
54
|
+
required: true,
|
|
55
|
+
attributes: { options: allTables.map((t) => t.name) },
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: "list_view",
|
|
59
|
+
label: "List view",
|
|
60
|
+
type: "String",
|
|
61
|
+
attributes: {
|
|
62
|
+
calcOptions: ["table_name", list_view_opts],
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: "hidden_fields",
|
|
67
|
+
label: "Hide fields",
|
|
68
|
+
type: "String",
|
|
69
|
+
sublabel: "Comma-separated list of fields to hide from the prompt",
|
|
70
|
+
},
|
|
71
|
+
/*{
|
|
72
|
+
name: "contents_expr",
|
|
73
|
+
label: "Contents string",
|
|
74
|
+
type: "String",
|
|
75
|
+
sublabel:
|
|
76
|
+
"Use handlebars (<code>{{ }}</code>) to access fields in the retrieved rows",
|
|
77
|
+
},*/
|
|
78
|
+
];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
onMessage(msgs) {
|
|
82
|
+
if (this.mode !== "Search on every user input") return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
provideTools() {
|
|
86
|
+
if (this.mode !== "Tool") return [];
|
|
87
|
+
const table = Table.findOne(this.table_name);
|
|
88
|
+
return {
|
|
89
|
+
type: "function",
|
|
90
|
+
process: async ({ phrase }) => {
|
|
91
|
+
const scState = getState();
|
|
92
|
+
const language = scState.pg_ts_config;
|
|
93
|
+
const use_websearch = scState.getConfig("search_use_websearch", false);
|
|
94
|
+
const rows = await table.getRows({
|
|
95
|
+
_fts: {
|
|
96
|
+
fields: table.fields,
|
|
97
|
+
searchTerm: phrase,
|
|
98
|
+
language,
|
|
99
|
+
use_websearch,
|
|
100
|
+
table: table.name,
|
|
101
|
+
schema: db.isSQLite ? undefined : db.getTenantSchema(),
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
if (this.hidden_fields) {
|
|
105
|
+
const hidden_fields = this.hidden_fields
|
|
106
|
+
.split(",")
|
|
107
|
+
.map((s) => s.trim());
|
|
108
|
+
rows.forEach((r) => {
|
|
109
|
+
hidden_fields.forEach((k) => {
|
|
110
|
+
delete r[k];
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if (rows.length) return { rows };
|
|
115
|
+
else
|
|
116
|
+
return {
|
|
117
|
+
response: "There are no rows related to: " + phrase,
|
|
118
|
+
};
|
|
119
|
+
},
|
|
120
|
+
/*renderToolCall({ phrase }, { req }) {
|
|
121
|
+
return div({ class: "border border-primary p-2 m-2" }, phrase);
|
|
122
|
+
},*/
|
|
123
|
+
renderToolResponse: async ({ response, rows }, { req }) => {
|
|
124
|
+
if (rows) {
|
|
125
|
+
const view = View.findOne({ name: this.list_view });
|
|
126
|
+
|
|
127
|
+
if (view) {
|
|
128
|
+
const viewRes = await view.run(
|
|
129
|
+
{ [table.pk_name]: { in: rows.map((r) => r[table.pk_name]) } },
|
|
130
|
+
{ req }
|
|
131
|
+
);
|
|
132
|
+
return viewRes;
|
|
133
|
+
} else return "";
|
|
134
|
+
}
|
|
135
|
+
return div({ class: "border border-success p-2 m-2" }, response);
|
|
136
|
+
},
|
|
137
|
+
function: {
|
|
138
|
+
name: this.toolName,
|
|
139
|
+
description: `Search the ${this.table_name} database table${
|
|
140
|
+
table.description ? ` (${table.description})` : ""
|
|
141
|
+
} by a search phrase matched against all fields in the table with full text search. The retrieved rows will be returned`,
|
|
142
|
+
parameters: {
|
|
143
|
+
type: "object",
|
|
144
|
+
required: ["phrase"],
|
|
145
|
+
properties: {
|
|
146
|
+
phrase: {
|
|
147
|
+
type: "string",
|
|
148
|
+
description: "The phrase to search the table with",
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
module.exports = RetrievalByFullTextSearch;
|