@saltcorn/agents 0.8.4 → 0.8.5
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 +12 -7
- package/agents.css +1 -1
- package/common.js +11 -2
- package/package.json +1 -1
- package/skills/Fetch.js +100 -5
- package/skills/LongTermMemory.js +229 -0
package/agent-view.js
CHANGED
|
@@ -49,6 +49,7 @@ const {
|
|
|
49
49
|
get_initial_interactions,
|
|
50
50
|
get_skill_instances,
|
|
51
51
|
saveInteractions,
|
|
52
|
+
extractText,
|
|
52
53
|
} = require("./common");
|
|
53
54
|
const MarkdownIt = require("markdown-it"),
|
|
54
55
|
md = new MarkdownIt({ html: true, breaks: true, linkify: true });
|
|
@@ -306,12 +307,13 @@ const run = async (
|
|
|
306
307
|
},
|
|
307
308
|
{ orderBy: "started_at", orderDesc: true, limit: 30 },
|
|
308
309
|
)
|
|
309
|
-
).filter((r) => r.context.interactions)
|
|
310
|
+
).filter((r) => r.context.interactions || r.context.html_interactions)
|
|
310
311
|
: null;
|
|
311
312
|
|
|
312
313
|
const cfgMsg = incompleteCfgMsg();
|
|
313
314
|
if (cfgMsg) return cfgMsg;
|
|
314
315
|
let runInteractions = "";
|
|
316
|
+
let hasInputForm = true;
|
|
315
317
|
|
|
316
318
|
const initial_q = state.run_id ? undefined : state._q;
|
|
317
319
|
if (state.run_id) {
|
|
@@ -325,6 +327,9 @@ const run = async (
|
|
|
325
327
|
const interactMarkups = [];
|
|
326
328
|
if (run.context.html_interactions) {
|
|
327
329
|
interactMarkups.push(...run.context.html_interactions);
|
|
330
|
+
// no input if interactions deleted
|
|
331
|
+
if (!run.context.interactions && run.context.html_interactions.length)
|
|
332
|
+
hasInputForm = false;
|
|
328
333
|
} else
|
|
329
334
|
for (const interact of run.context.interactions) {
|
|
330
335
|
//legacy
|
|
@@ -596,11 +601,11 @@ const run = async (
|
|
|
596
601
|
),
|
|
597
602
|
prevRuns.map((run) => {
|
|
598
603
|
const isActive = state.run_id && +state.run_id === run.id;
|
|
599
|
-
const
|
|
600
|
-
run.context.interactions
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
);
|
|
604
|
+
const previewHtml =
|
|
605
|
+
(run.context.interactions || []).find(
|
|
606
|
+
(ix) => typeof ix?.content === "string",
|
|
607
|
+
)?.content || extractText(run.context.html_interactions[0] || "");
|
|
608
|
+
const preview = escapeHtml(previewHtml?.substring?.(0, 80));
|
|
604
609
|
return isModernSidebar
|
|
605
610
|
? div(
|
|
606
611
|
{
|
|
@@ -661,7 +666,7 @@ const run = async (
|
|
|
661
666
|
),
|
|
662
667
|
div({ id: "copilotinteractions" }, runInteractions),
|
|
663
668
|
stream ? div({ class: "next_response_scratch" }) : "",
|
|
664
|
-
input_form,
|
|
669
|
+
hasInputForm && input_form,
|
|
665
670
|
style(agents_css),
|
|
666
671
|
script(domReady(`$( "#inputuserinput" ).autogrow({paddingBottom: 20});`)),
|
|
667
672
|
script(
|
package/agents.css
CHANGED
package/common.js
CHANGED
|
@@ -43,6 +43,7 @@ const get_skills = () => {
|
|
|
43
43
|
require("./skills/Subagent"),
|
|
44
44
|
require("./skills/ExternalSkill"),
|
|
45
45
|
require("./skills/PlanApproval"),
|
|
46
|
+
require("./skills/LongTermMemory"),
|
|
46
47
|
//require("./skills/AdaptiveFeedback"),
|
|
47
48
|
...exchange_skills,
|
|
48
49
|
];
|
|
@@ -57,8 +58,10 @@ const get_skill_instances = (config) => {
|
|
|
57
58
|
const instances = [];
|
|
58
59
|
for (const skillCfg of config.skills) {
|
|
59
60
|
const klass = get_skill_class(skillCfg.skill_type);
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
if (klass) {
|
|
62
|
+
const skill = new klass(skillCfg);
|
|
63
|
+
instances.push(skill);
|
|
64
|
+
}
|
|
62
65
|
}
|
|
63
66
|
return instances;
|
|
64
67
|
};
|
|
@@ -237,6 +240,10 @@ const wrapCard = (title, ...inners) =>
|
|
|
237
240
|
|
|
238
241
|
const is_debug_mode = (config, user) => user?.role_id === 1;
|
|
239
242
|
|
|
243
|
+
function extractText(html) {
|
|
244
|
+
return html.replace(/<[^>]*>/g, '');
|
|
245
|
+
}
|
|
246
|
+
|
|
240
247
|
const process_interaction = async (
|
|
241
248
|
run,
|
|
242
249
|
config,
|
|
@@ -414,6 +421,7 @@ const process_interaction = async (
|
|
|
414
421
|
myHasResult = true;
|
|
415
422
|
let result = await tool.tool.process(tool_call.input, {
|
|
416
423
|
req,
|
|
424
|
+
run,
|
|
417
425
|
});
|
|
418
426
|
const tool_response = result.add_response || result;
|
|
419
427
|
toolResults[tool_call.tool_call_id] = result;
|
|
@@ -744,4 +752,5 @@ module.exports = {
|
|
|
744
752
|
is_debug_mode,
|
|
745
753
|
get_initial_interactions,
|
|
746
754
|
nubBy,
|
|
755
|
+
extractText
|
|
747
756
|
};
|
package/package.json
CHANGED
package/skills/Fetch.js
CHANGED
|
@@ -15,9 +15,69 @@ const { features } = require("@saltcorn/data/db/state");
|
|
|
15
15
|
const { button } = require("@saltcorn/markup/tags");
|
|
16
16
|
const { validID } = require("@saltcorn/markup/layout_utils");
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
class CookieJar {
|
|
19
|
+
constructor(source) {
|
|
20
|
+
this.cookies = new Map();
|
|
19
21
|
|
|
20
|
-
|
|
22
|
+
if (source instanceof CookieJar) {
|
|
23
|
+
// Copy from another CookieJar instance
|
|
24
|
+
for (const [name, value] of source.cookies) {
|
|
25
|
+
this.cookies.set(name, value);
|
|
26
|
+
}
|
|
27
|
+
} else if (source && typeof source === "object") {
|
|
28
|
+
// Hydrate from a plain object (e.g. output of toObject())
|
|
29
|
+
for (const [name, value] of Object.entries(source)) {
|
|
30
|
+
this.cookies.set(name, String(value));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// If source is undefined/null, start empty
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Parse Set-Cookie headers from a response and store them
|
|
37
|
+
storeFromResponse(response) {
|
|
38
|
+
const setCookieHeaders = response.headers.getSetCookie
|
|
39
|
+
? response.headers.getSetCookie()
|
|
40
|
+
: [response.headers.get("set-cookie")].filter(Boolean);
|
|
41
|
+
|
|
42
|
+
for (const header of setCookieHeaders) {
|
|
43
|
+
this._parseAndStore(header);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Parse a single "name=value; Path=/; HttpOnly; ..." string
|
|
48
|
+
_parseAndStore(header) {
|
|
49
|
+
const [nameValue] = header.split(";");
|
|
50
|
+
const eqIdx = nameValue.indexOf("=");
|
|
51
|
+
if (eqIdx > 0) {
|
|
52
|
+
const name = nameValue.slice(0, eqIdx).trim();
|
|
53
|
+
const value = nameValue.slice(eqIdx + 1).trim();
|
|
54
|
+
this.cookies.set(name, value);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Build a Cookie header string suitable for outgoing requests
|
|
59
|
+
toHeader() {
|
|
60
|
+
return Array.from(this.cookies.entries())
|
|
61
|
+
.map(([name, value]) => `${name}=${value}`)
|
|
62
|
+
.join("; ");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Apply cookies to a headers object (mutates and returns it)
|
|
66
|
+
applyTo(headers = {}) {
|
|
67
|
+
if (this.cookies.size > 0) {
|
|
68
|
+
headers["Cookie"] = this.toHeader();
|
|
69
|
+
}
|
|
70
|
+
return headers;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
get size() {
|
|
74
|
+
return this.cookies.size;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
toObject() {
|
|
78
|
+
return Object.fromEntries(this.cookies);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
21
81
|
|
|
22
82
|
class FetchSkill {
|
|
23
83
|
static skill_name = "Fetch";
|
|
@@ -31,7 +91,7 @@ class FetchSkill {
|
|
|
31
91
|
}
|
|
32
92
|
|
|
33
93
|
static async configFields() {
|
|
34
|
-
return [];
|
|
94
|
+
return [{ name: "cookiejar", label: "Cookie Jar", type: "Bool" }];
|
|
35
95
|
}
|
|
36
96
|
systemPrompt() {
|
|
37
97
|
return "If you need to retrieve the contents of a web page, use the fetch_web_page to make a GET request to a specified URL.";
|
|
@@ -40,8 +100,29 @@ class FetchSkill {
|
|
|
40
100
|
provideTools = () => {
|
|
41
101
|
return {
|
|
42
102
|
type: "function",
|
|
43
|
-
process: async (row) => {
|
|
44
|
-
const
|
|
103
|
+
process: async (row, { run }) => {
|
|
104
|
+
const opts = { headers: {} };
|
|
105
|
+
const jar = new CookieJar(run.context.cookiejar || {});
|
|
106
|
+
if (this.cookiejar) {
|
|
107
|
+
opts.headers = jar.applyTo();
|
|
108
|
+
opts.credentials = "same-origin";
|
|
109
|
+
opts.redirect = "manual";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (row.method) opts.method = row.method;
|
|
113
|
+
if (row.content_type) opts.headers["Content-Type"] = row.content_type;
|
|
114
|
+
if (row.body) opts.body = row.body;
|
|
115
|
+
|
|
116
|
+
let resp = await fetch(row.url, opts);
|
|
117
|
+
if (resp.status == 302 && (!row.method || row.method === "GET")) {
|
|
118
|
+
if (this.cookiejar) jar.storeFromResponse(resp);
|
|
119
|
+
resp = await fetch(resp.headers.get("location"), opts);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (this.cookiejar) {
|
|
123
|
+
jar.storeFromResponse(resp);
|
|
124
|
+
run.context.cookiejar = jar.toObject();
|
|
125
|
+
}
|
|
45
126
|
return await resp.text();
|
|
46
127
|
},
|
|
47
128
|
function: {
|
|
@@ -55,6 +136,20 @@ class FetchSkill {
|
|
|
55
136
|
description: "The URL to fetch with HTTP",
|
|
56
137
|
type: "string",
|
|
57
138
|
},
|
|
139
|
+
method: {
|
|
140
|
+
description: "The HTTP method",
|
|
141
|
+
type: "string",
|
|
142
|
+
enum: ["GET", "POST", "PUT", "DELETE"],
|
|
143
|
+
},
|
|
144
|
+
body: {
|
|
145
|
+
description: "The request body as a string (POST and PUT only)",
|
|
146
|
+
type: "string",
|
|
147
|
+
},
|
|
148
|
+
content_type: {
|
|
149
|
+
description:
|
|
150
|
+
"The request body content type, e.g. application/x-www-form-urlencoded or application/json (POST and PUT only)",
|
|
151
|
+
type: "string",
|
|
152
|
+
},
|
|
58
153
|
},
|
|
59
154
|
},
|
|
60
155
|
},
|
|
@@ -0,0 +1,229 @@
|
|
|
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 Field = require("@saltcorn/data/models/field");
|
|
6
|
+
const View = require("@saltcorn/data/models/view");
|
|
7
|
+
const { getState } = require("@saltcorn/data/db/state");
|
|
8
|
+
const db = require("@saltcorn/data/db");
|
|
9
|
+
const { interpolate } = require("@saltcorn/data/utils");
|
|
10
|
+
const { nubBy } = require("../common");
|
|
11
|
+
|
|
12
|
+
class LongTermMemory {
|
|
13
|
+
static skill_name = "Memory";
|
|
14
|
+
|
|
15
|
+
get skill_label() {
|
|
16
|
+
return `Memory`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
constructor(cfg) {
|
|
20
|
+
Object.assign(this, cfg);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
systemPrompt() {
|
|
24
|
+
return `You have access to a memory bank you can read from or write to. You should search the memory bank with the search_memory tool with any search terms that might be relevant to the user's query or the result of a tool call. When you learn something noteworthy (from the user or from the result of a tool call) store it in memory with the store_in_memory tool. Mark it as personal if it is only true or relevant for the specific user. Don't tell the user when you are storing to and retrieving from memory. ${
|
|
25
|
+
this.add_sys_prompt
|
|
26
|
+
? ` Additional instructions for the memory tools: ${this.add_sys_prompt}`
|
|
27
|
+
: ""
|
|
28
|
+
}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
static async configFields() {
|
|
32
|
+
return [
|
|
33
|
+
{
|
|
34
|
+
name: "add_sys_prompt",
|
|
35
|
+
label: "Additional prompt",
|
|
36
|
+
type: "String",
|
|
37
|
+
fieldview: "textarea",
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async get_table() {
|
|
43
|
+
const table0 = Table.findOne("AgentLongTermMemory");
|
|
44
|
+
if (table0) return table0;
|
|
45
|
+
const tables = await Table.find({ name: "AgentLongTermMemory" });
|
|
46
|
+
if (tables.length) return tables[0];
|
|
47
|
+
|
|
48
|
+
//does not exist, create it
|
|
49
|
+
const table = await Table.create("AgentLongTermMemory", {});
|
|
50
|
+
await getState().refresh_tables(true);
|
|
51
|
+
await Field.create({
|
|
52
|
+
table,
|
|
53
|
+
name: "run_id",
|
|
54
|
+
label: "Run ID",
|
|
55
|
+
type: "Integer",
|
|
56
|
+
});
|
|
57
|
+
const uid_field = await Field.create({
|
|
58
|
+
table,
|
|
59
|
+
name: "user_id",
|
|
60
|
+
label: "User ID",
|
|
61
|
+
type: "Key to users",
|
|
62
|
+
attributes: {
|
|
63
|
+
on_delete: "Set null",
|
|
64
|
+
include_fts: false,
|
|
65
|
+
summary_field: "email",
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
await Field.create({
|
|
69
|
+
table,
|
|
70
|
+
name: "written_at",
|
|
71
|
+
label: "Written at",
|
|
72
|
+
type: "Date",
|
|
73
|
+
});
|
|
74
|
+
await Field.create({
|
|
75
|
+
table,
|
|
76
|
+
name: "agent_trigger_id",
|
|
77
|
+
label: "Agent trigger ID",
|
|
78
|
+
type: "Integer",
|
|
79
|
+
});
|
|
80
|
+
await Field.create({
|
|
81
|
+
table,
|
|
82
|
+
name: "memory_type",
|
|
83
|
+
label: "Memory type",
|
|
84
|
+
type: "String",
|
|
85
|
+
});
|
|
86
|
+
await Field.create({
|
|
87
|
+
table,
|
|
88
|
+
name: "personal",
|
|
89
|
+
label: "Personal",
|
|
90
|
+
type: "Bool",
|
|
91
|
+
});
|
|
92
|
+
await Field.create({
|
|
93
|
+
table,
|
|
94
|
+
name: "topic",
|
|
95
|
+
label: "Topic",
|
|
96
|
+
type: "String",
|
|
97
|
+
});
|
|
98
|
+
await Field.create({
|
|
99
|
+
table,
|
|
100
|
+
name: "contents",
|
|
101
|
+
label: "Contents",
|
|
102
|
+
type: "String",
|
|
103
|
+
});
|
|
104
|
+
await table.update({ ownership_field_id: uid_field.id });
|
|
105
|
+
await getState().refresh_tables();
|
|
106
|
+
return Table.findOne("AgentLongTermMemory");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
provideTools() {
|
|
110
|
+
return [
|
|
111
|
+
{
|
|
112
|
+
type: "function",
|
|
113
|
+
function: {
|
|
114
|
+
name: "store_in_memory",
|
|
115
|
+
description: `Store a fact or observation in long-term memory`,
|
|
116
|
+
parameters: {
|
|
117
|
+
type: "object",
|
|
118
|
+
required: ["contents"],
|
|
119
|
+
properties: {
|
|
120
|
+
contents: {
|
|
121
|
+
type: "string",
|
|
122
|
+
description: "The contents of the fact or observations",
|
|
123
|
+
},
|
|
124
|
+
personal: {
|
|
125
|
+
type: "boolean",
|
|
126
|
+
description:
|
|
127
|
+
"Is this a fact or observation specifically about the person interacting with you now, which may not be true or relevant for another person",
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
process: async (arg, { req, run }) => {
|
|
133
|
+
const table = await this.get_table();
|
|
134
|
+
await table.insertRow({
|
|
135
|
+
run_id: run.id,
|
|
136
|
+
user_id: req.user?.id,
|
|
137
|
+
written_at: new Date(),
|
|
138
|
+
agent_trigger_id: run.trigger_id,
|
|
139
|
+
memory_type: "Episodic",
|
|
140
|
+
contents: arg.contents,
|
|
141
|
+
personal: arg.personal,
|
|
142
|
+
});
|
|
143
|
+
return "Recorded";
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
type: "function",
|
|
148
|
+
process: async (arg, { req }) => {
|
|
149
|
+
const table = await this.get_table();
|
|
150
|
+
|
|
151
|
+
const scState = getState();
|
|
152
|
+
const language = scState.pg_ts_config;
|
|
153
|
+
const use_websearch = scState.getConfig(
|
|
154
|
+
"search_use_websearch",
|
|
155
|
+
false,
|
|
156
|
+
);
|
|
157
|
+
let rows = [];
|
|
158
|
+
const user_id = req.user?.id;
|
|
159
|
+
const phrases =
|
|
160
|
+
typeof arg.phrases === "string" ? [arg.phrases] : arg.phrases;
|
|
161
|
+
|
|
162
|
+
if (use_websearch)
|
|
163
|
+
rows = await table.getRows({
|
|
164
|
+
_fts: {
|
|
165
|
+
fields: table.fields,
|
|
166
|
+
searchTerm: phrases.join(" OR "),
|
|
167
|
+
language,
|
|
168
|
+
use_websearch,
|
|
169
|
+
table: table.name,
|
|
170
|
+
schema: db.isSQLite ? undefined : db.getTenantSchema(),
|
|
171
|
+
},
|
|
172
|
+
...(user_id
|
|
173
|
+
? { or: [{ personal: false }, { personal: true, user_id }] }
|
|
174
|
+
: [{ personal: false }]),
|
|
175
|
+
});
|
|
176
|
+
else
|
|
177
|
+
for (const phrase of phrases) {
|
|
178
|
+
const my_rows = await table.getRows({
|
|
179
|
+
_fts: {
|
|
180
|
+
fields: table.fields,
|
|
181
|
+
searchTerm: phrase,
|
|
182
|
+
language,
|
|
183
|
+
use_websearch,
|
|
184
|
+
table: table.name,
|
|
185
|
+
schema: db.isSQLite ? undefined : db.getTenantSchema(),
|
|
186
|
+
},
|
|
187
|
+
...(user_id
|
|
188
|
+
? { or: [{ personal: false }, { personal: true, user_id }] }
|
|
189
|
+
: [{ personal: false }]),
|
|
190
|
+
});
|
|
191
|
+
rows.push(...my_rows);
|
|
192
|
+
}
|
|
193
|
+
const pk = table.pk_name;
|
|
194
|
+
rows = nubBy((r) => r[pk], rows);
|
|
195
|
+
//TODO sort most recent, only N memories
|
|
196
|
+
if (rows.length)
|
|
197
|
+
return (
|
|
198
|
+
"These memories were retrieved:\n\n" +
|
|
199
|
+
rows.map((r) => r.contents).join("\n")
|
|
200
|
+
);
|
|
201
|
+
else
|
|
202
|
+
return "There are no memories related to: " + phrases.join(" or ");
|
|
203
|
+
},
|
|
204
|
+
function: {
|
|
205
|
+
name: "search_memory",
|
|
206
|
+
description: `Search the memory bank by a search phrase`,
|
|
207
|
+
parameters: {
|
|
208
|
+
type: "object",
|
|
209
|
+
required: ["phrases"],
|
|
210
|
+
description:
|
|
211
|
+
"Search the memory bank by any of a number of phrases. This will return any memories that matches one or the other of the phrases",
|
|
212
|
+
properties: {
|
|
213
|
+
phrases: {
|
|
214
|
+
type: "array",
|
|
215
|
+
description:
|
|
216
|
+
"A phrase to search the memory bank with. The search phrase is the synatx used by web search engines: use double quotes for exact match, unquoted text for words in any order, dash (minus sign) to exclude a word. Do not use SQL or any other formal query language.",
|
|
217
|
+
items: {
|
|
218
|
+
type: "string",
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
];
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
module.exports = LongTermMemory;
|