@saltcorn/agents 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent-view.js +18 -5
- package/common.js +4 -1
- package/index.js +3 -2
- package/package.json +1 -1
- package/skills/EmbeddingRetrieval.js +1 -0
- 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
|
@@ -45,7 +45,7 @@ const configuration_workflow = (req) =>
|
|
|
45
45
|
new Workflow({
|
|
46
46
|
steps: [
|
|
47
47
|
{
|
|
48
|
-
name:
|
|
48
|
+
name: "Agent action",
|
|
49
49
|
form: async (context) => {
|
|
50
50
|
const agent_actions = await Trigger.find({ action: "Agent" });
|
|
51
51
|
return new Form({
|
|
@@ -68,7 +68,7 @@ const configuration_workflow = (req) =>
|
|
|
68
68
|
"data-dyn-href": `\`/actions/configure/\${action_id}\``,
|
|
69
69
|
target: "_blank",
|
|
70
70
|
},
|
|
71
|
-
|
|
71
|
+
"Configure"
|
|
72
72
|
),
|
|
73
73
|
},
|
|
74
74
|
{
|
|
@@ -80,7 +80,14 @@ const configuration_workflow = (req) =>
|
|
|
80
80
|
name: "placeholder",
|
|
81
81
|
label: "Placeholder",
|
|
82
82
|
type: "String",
|
|
83
|
-
default: "How can I help you?"
|
|
83
|
+
default: "How can I help you?",
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: "explainer",
|
|
87
|
+
label: "Explainer",
|
|
88
|
+
type: "String",
|
|
89
|
+
sublabel:
|
|
90
|
+
"Appears below the input box. Use for additional instructions.",
|
|
84
91
|
},
|
|
85
92
|
],
|
|
86
93
|
});
|
|
@@ -94,7 +101,7 @@ const get_state_fields = () => [];
|
|
|
94
101
|
const run = async (
|
|
95
102
|
table_id,
|
|
96
103
|
viewname,
|
|
97
|
-
{ action_id, show_prev_runs, placeholder },
|
|
104
|
+
{ action_id, show_prev_runs, placeholder, explainer },
|
|
98
105
|
state,
|
|
99
106
|
{ res, req }
|
|
100
107
|
) => {
|
|
@@ -238,7 +245,8 @@ const run = async (
|
|
|
238
245
|
span(
|
|
239
246
|
{ class: "submit-button p-2", onclick: "$('form.copilot').submit()" },
|
|
240
247
|
i({ id: "sendbuttonicon", class: "far fa-paper-plane" })
|
|
241
|
-
)
|
|
248
|
+
),
|
|
249
|
+
explainer && small({ class: "explainer" }, i(explainer))
|
|
242
250
|
)
|
|
243
251
|
);
|
|
244
252
|
|
|
@@ -310,6 +318,11 @@ const run = async (
|
|
|
310
318
|
position: relative;
|
|
311
319
|
top: -1.8rem;
|
|
312
320
|
left: 0.1rem;
|
|
321
|
+
}
|
|
322
|
+
.copilot-entry .explainer {
|
|
323
|
+
position: relative;
|
|
324
|
+
top: -1.2rem;
|
|
325
|
+
display: block;
|
|
313
326
|
}
|
|
314
327
|
.copilot-entry {margin-bottom: -1.25rem; margin-top: 1rem;}
|
|
315
328
|
p.prevrun_content {
|
package/common.js
CHANGED
|
@@ -9,6 +9,8 @@ const get_skills = () => {
|
|
|
9
9
|
return [
|
|
10
10
|
require("./skills/FTSRetrieval"),
|
|
11
11
|
require("./skills/EmbeddingRetrieval"),
|
|
12
|
+
require("./skills/Trigger"),
|
|
13
|
+
require("./skills/Table"),
|
|
12
14
|
//require("./skills/AdaptiveFeedback"),
|
|
13
15
|
];
|
|
14
16
|
};
|
|
@@ -181,7 +183,8 @@ const process_interaction = async (
|
|
|
181
183
|
}
|
|
182
184
|
hasResult = true;
|
|
183
185
|
const result = await tool.tool.process(
|
|
184
|
-
JSON.parse(tool_call.function.arguments)
|
|
186
|
+
JSON.parse(tool_call.function.arguments),
|
|
187
|
+
{ req }
|
|
185
188
|
);
|
|
186
189
|
if (
|
|
187
190
|
(typeof result === "object" && Object.keys(result || {}).length) ||
|
package/index.js
CHANGED
package/package.json
CHANGED
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 };
|