@saltcorn/agents 0.8.5 → 0.8.7
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/consolidate_agent_memory.js +65 -0
- package/index.js +3 -0
- package/package.json +1 -1
- package/skills/GenerateImage.js +101 -50
- package/skills/LongTermMemory.js +20 -2
- package/skills/WebSearch.js +74 -10
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const FieldRepeat = require("@saltcorn/data/models/fieldrepeat");
|
|
2
|
+
const Table = require("@saltcorn/data/models/table");
|
|
3
|
+
const View = require("@saltcorn/data/models/view");
|
|
4
|
+
const { p } = require("@saltcorn/markup/tags");
|
|
5
|
+
const { get_skills, process_interaction, wrapSegment } = require("./common");
|
|
6
|
+
const { applyAsync } = require("@saltcorn/data/utils");
|
|
7
|
+
const WorkflowRun = require("@saltcorn/data/models/workflow_run");
|
|
8
|
+
const { interpolate, escapeHtml } = require("@saltcorn/data/utils");
|
|
9
|
+
const { getState } = require("@saltcorn/data/db/state");
|
|
10
|
+
|
|
11
|
+
module.exports = {
|
|
12
|
+
//configFields: async ({ table, mode }) => [],
|
|
13
|
+
run: async ({ configuration, user, table, req, ...rest }) => {
|
|
14
|
+
const memtable = Table.findOne("AgentLongTermMemory");
|
|
15
|
+
if (!memtable) return;
|
|
16
|
+
// write personal preferences
|
|
17
|
+
|
|
18
|
+
// get all personal observations
|
|
19
|
+
const personalmems = await memtable.getRows(
|
|
20
|
+
{ personal: true },
|
|
21
|
+
{ orderBy: "written_at" },
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const user_ids = new Set(personalmems.map((p) => p.user_id));
|
|
25
|
+
|
|
26
|
+
for (const uid of user_ids.values()) {
|
|
27
|
+
const all_umems = personalmems.filter((m) => m.user_id === uid);
|
|
28
|
+
const existingPP = all_umems.find(
|
|
29
|
+
(m) => m.memory_type === "PersonalPreferences",
|
|
30
|
+
);
|
|
31
|
+
const nonPPs = all_umems.filter(
|
|
32
|
+
(m) => m.memory_type !== "PersonalPreferences",
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
if (
|
|
36
|
+
existingPP &&
|
|
37
|
+
nonPPs.every((m) => m.written_at < existingPP.written_at)
|
|
38
|
+
)
|
|
39
|
+
continue; // no new memories
|
|
40
|
+
const prompt = `This is a set of observations about a user:
|
|
41
|
+
${nonPPs.map((m) => `* ${m.contents}`).join("\n")}
|
|
42
|
+
|
|
43
|
+
They are in ascending chronological order so if there are any contradictions, the latter observation take precedence.
|
|
44
|
+
|
|
45
|
+
Write a succinct summary of these observations which captures all the essential facts. Start the summary with the words
|
|
46
|
+
"The user..." (in the language in which the observations appear) and then write what you have learned about the user.`;
|
|
47
|
+
|
|
48
|
+
const answer = await getState().functions.llm_generate.run(prompt);
|
|
49
|
+
|
|
50
|
+
if (answer && typeof answer === "string") {
|
|
51
|
+
await memtable.deleteRows({
|
|
52
|
+
memory_type: "PersonalPreferences",
|
|
53
|
+
user_id: uid,
|
|
54
|
+
});
|
|
55
|
+
await memtable.insertRow({
|
|
56
|
+
user_id: uid,
|
|
57
|
+
written_at: new Date(),
|
|
58
|
+
memory_type: "PersonalPreferences",
|
|
59
|
+
contents: answer,
|
|
60
|
+
personal: true,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
};
|
package/index.js
CHANGED
|
@@ -36,6 +36,7 @@ module.exports = {
|
|
|
36
36
|
],
|
|
37
37
|
actions: {
|
|
38
38
|
Agent: require("./action"),
|
|
39
|
+
consolidate_agent_memory: require("./consolidate_agent_memory"),
|
|
39
40
|
},
|
|
40
41
|
functions: {
|
|
41
42
|
inspect_agent: {
|
|
@@ -69,6 +70,7 @@ module.exports = {
|
|
|
69
70
|
};
|
|
70
71
|
},
|
|
71
72
|
isAsync: true,
|
|
73
|
+
hidden: true,
|
|
72
74
|
description: "Return system prompt, tools and action of an agent",
|
|
73
75
|
},
|
|
74
76
|
agent_generate: {
|
|
@@ -115,6 +117,7 @@ module.exports = {
|
|
|
115
117
|
};
|
|
116
118
|
},
|
|
117
119
|
isAsync: true,
|
|
120
|
+
hidden: true,
|
|
118
121
|
description: "Run an agent on a prompt",
|
|
119
122
|
arguments: [
|
|
120
123
|
{ name: "agent_name", type: "String" },
|
package/package.json
CHANGED
package/skills/GenerateImage.js
CHANGED
|
@@ -1,19 +1,11 @@
|
|
|
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 File = require("@saltcorn/data/models/file");
|
|
6
|
-
const View = require("@saltcorn/data/models/view");
|
|
7
1
|
const { getState } = require("@saltcorn/data/db/state");
|
|
8
|
-
const
|
|
9
|
-
const { eval_expression } = require("@saltcorn/data/models/expression");
|
|
10
|
-
const { interpolate } = require("@saltcorn/data/utils");
|
|
2
|
+
const File = require("@saltcorn/data/models/file");
|
|
11
3
|
|
|
12
4
|
class GenerateImage {
|
|
13
5
|
static skill_name = "Image generation";
|
|
14
6
|
|
|
15
7
|
get skill_label() {
|
|
16
|
-
return
|
|
8
|
+
return "Image generation";
|
|
17
9
|
}
|
|
18
10
|
|
|
19
11
|
constructor(cfg) {
|
|
@@ -23,20 +15,31 @@ class GenerateImage {
|
|
|
23
15
|
static async configFields() {
|
|
24
16
|
return [
|
|
25
17
|
{
|
|
26
|
-
name: "
|
|
27
|
-
label: "
|
|
18
|
+
name: "size",
|
|
19
|
+
label: "Size",
|
|
28
20
|
type: "String",
|
|
29
21
|
required: true,
|
|
30
|
-
attributes: {
|
|
22
|
+
attributes: {
|
|
23
|
+
options: [
|
|
24
|
+
"auto",
|
|
25
|
+
"1024x1024",
|
|
26
|
+
"1536x1024",
|
|
27
|
+
"1024x1536",
|
|
28
|
+
"1792x1024",
|
|
29
|
+
"1024x1792",
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
default: "auto",
|
|
31
33
|
},
|
|
32
34
|
{
|
|
33
|
-
name: "
|
|
34
|
-
label: "
|
|
35
|
+
name: "quality",
|
|
36
|
+
label: "Quality",
|
|
35
37
|
type: "String",
|
|
36
38
|
required: true,
|
|
37
39
|
attributes: {
|
|
38
|
-
options: ["auto", "
|
|
40
|
+
options: ["auto", "low", "medium", "high", "standard", "hd"],
|
|
39
41
|
},
|
|
42
|
+
default: "auto",
|
|
40
43
|
},
|
|
41
44
|
{
|
|
42
45
|
name: "format",
|
|
@@ -44,54 +47,102 @@ class GenerateImage {
|
|
|
44
47
|
type: "String",
|
|
45
48
|
required: true,
|
|
46
49
|
attributes: { options: ["png", "jpeg", "webp"] },
|
|
50
|
+
default: "png",
|
|
47
51
|
},
|
|
48
52
|
{
|
|
49
53
|
name: "transparent",
|
|
50
|
-
label: "Transparent",
|
|
54
|
+
label: "Transparent background",
|
|
51
55
|
type: "Bool",
|
|
52
|
-
|
|
53
|
-
{
|
|
54
|
-
name: "save_file",
|
|
55
|
-
label: "Save file",
|
|
56
|
-
type: "String",
|
|
57
|
-
required: true,
|
|
58
|
-
attributes: { options: ["Always", "Never"] }, //, "Button"] },
|
|
56
|
+
sublabel: "Only supported with gpt-image-1.",
|
|
59
57
|
},
|
|
60
58
|
];
|
|
61
59
|
}
|
|
62
60
|
|
|
63
61
|
provideTools() {
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
62
|
+
const skill = this;
|
|
63
|
+
return {
|
|
64
|
+
type: "function",
|
|
65
|
+
function: {
|
|
66
|
+
name: "generate_image",
|
|
67
|
+
description:
|
|
68
|
+
"Generate an image from a text prompt. The image is automatically " +
|
|
69
|
+
"displayed to the user in the chat — DO NOT include the image URL or " +
|
|
70
|
+
"any markdown image syntax in your reply. Just confirm in words what " +
|
|
71
|
+
"was generated.",
|
|
72
|
+
parameters: {
|
|
73
|
+
type: "object",
|
|
74
|
+
properties: {
|
|
75
|
+
prompt: {
|
|
76
|
+
type: "string",
|
|
77
|
+
description: "Detailed description of the image to generate.",
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
required: ["prompt"],
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
process: async (input, { req }) => {
|
|
84
|
+
const fn = getState().functions.llm_image_generate;
|
|
85
|
+
if (!fn)
|
|
86
|
+
throw new Error(
|
|
87
|
+
"LLM plugin not available (llm_image_generate not registered)",
|
|
77
88
|
);
|
|
78
|
-
|
|
79
|
-
|
|
89
|
+
const opts = {};
|
|
90
|
+
if (skill.size && skill.size !== "auto") opts.size = skill.size;
|
|
91
|
+
if (skill.quality && skill.quality !== "auto")
|
|
92
|
+
opts.quality = skill.quality;
|
|
93
|
+
if (skill.format) opts.output_format = skill.format;
|
|
94
|
+
if (skill.transparent) opts.background = "transparent";
|
|
95
|
+
// gpt-image-* family supports the rich options above (quality, format,
|
|
96
|
+
// background). The LLM-plugin's image gen will pick them up.
|
|
97
|
+
const result = await fn.run(input.prompt, opts);
|
|
98
|
+
const b64 = result?.b64_json;
|
|
99
|
+
if (!b64) throw new Error("Image generation returned no data");
|
|
100
|
+
const ext = result?.output_format || skill.format || "png";
|
|
101
|
+
const buf = Buffer.from(b64, "base64");
|
|
102
|
+
const file = await File.from_contents(
|
|
103
|
+
`genimg.${ext}`,
|
|
104
|
+
`image/${ext}`,
|
|
105
|
+
buf,
|
|
106
|
+
req?.user?.id,
|
|
107
|
+
100,
|
|
108
|
+
);
|
|
109
|
+
const sizeOut =
|
|
110
|
+
skill.size && skill.size !== "auto" ? skill.size : "1024x1024";
|
|
111
|
+
// Two response paths:
|
|
112
|
+
// - add_response: rendered HTML the user actually sees in chat
|
|
113
|
+
// (with download overlay). Saved in interactions.
|
|
114
|
+
// - return shape consumed by the LLM tool_response — kept terse
|
|
115
|
+
// and free of any URL so the model cannot leak a broken markdown
|
|
116
|
+
// image link into its textual reply.
|
|
117
|
+
return {
|
|
118
|
+
ok: true,
|
|
119
|
+
message:
|
|
120
|
+
"Image generated and displayed to the user. Do NOT include the image URL or any markdown image syntax in your textual reply.",
|
|
121
|
+
// Hidden internal fields used only by renderToolResponse:
|
|
122
|
+
filename: file.path_to_serve,
|
|
123
|
+
output_format: ext,
|
|
124
|
+
size: sizeOut,
|
|
125
|
+
};
|
|
80
126
|
},
|
|
81
127
|
renderToolResponse: (v) => {
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
128
|
+
const sz = v?.size || "1024x1024";
|
|
129
|
+
const [ws, hs] = sz.split("x");
|
|
130
|
+
const w = +ws / 4;
|
|
131
|
+
const h = +hs / 4;
|
|
132
|
+
if (!v?.filename) return "";
|
|
133
|
+
const ext = v.output_format || "png";
|
|
134
|
+
const url = "/files/serve/" + v.filename;
|
|
135
|
+
const dlName = "image-" + Date.now() + "." + ext;
|
|
136
|
+
return (
|
|
137
|
+
`<div class="agent-generated-image">` +
|
|
138
|
+
`<img height="${h}" width="${w}" src="${url}" />` +
|
|
139
|
+
`<a href="${url}" download="${dlName}" ` +
|
|
140
|
+
`class="agent-image-download" title="Download image">` +
|
|
141
|
+
`<i class="fas fa-download"></i></a>` +
|
|
142
|
+
`</div>`
|
|
143
|
+
);
|
|
91
144
|
},
|
|
92
145
|
};
|
|
93
|
-
if (this.transparent) tool.background = "transparent";
|
|
94
|
-
return tool;
|
|
95
146
|
}
|
|
96
147
|
}
|
|
97
148
|
|
package/skills/LongTermMemory.js
CHANGED
|
@@ -20,12 +20,23 @@ class LongTermMemory {
|
|
|
20
20
|
Object.assign(this, cfg);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
systemPrompt() {
|
|
23
|
+
async systemPrompt({ user }) {
|
|
24
|
+
let pp = "";
|
|
25
|
+
if (this.inject_personal_prefs && user?.id) {
|
|
26
|
+
const table = Table.findOne("AgentLongTermMemory");
|
|
27
|
+
if (table) {
|
|
28
|
+
const ppRow = await table.getRow({
|
|
29
|
+
memory_type: "PersonalPreferences",
|
|
30
|
+
user_id: user.id,
|
|
31
|
+
});
|
|
32
|
+
if (ppRow) pp = "\n\n" + ppRow.contents;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
24
35
|
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
36
|
this.add_sys_prompt
|
|
26
37
|
? ` Additional instructions for the memory tools: ${this.add_sys_prompt}`
|
|
27
38
|
: ""
|
|
28
|
-
}`;
|
|
39
|
+
}${pp}`;
|
|
29
40
|
}
|
|
30
41
|
|
|
31
42
|
static async configFields() {
|
|
@@ -36,6 +47,13 @@ class LongTermMemory {
|
|
|
36
47
|
type: "String",
|
|
37
48
|
fieldview: "textarea",
|
|
38
49
|
},
|
|
50
|
+
{
|
|
51
|
+
name: "inject_personal_prefs",
|
|
52
|
+
label: "Personal preferences",
|
|
53
|
+
type: "Bool",
|
|
54
|
+
sublabel:
|
|
55
|
+
"Inject personal preferences generated by the consolidate_agent_memory action in the system prompt",
|
|
56
|
+
},
|
|
39
57
|
];
|
|
40
58
|
}
|
|
41
59
|
|
package/skills/WebSearch.js
CHANGED
|
@@ -37,7 +37,7 @@ class WebSearchSkill {
|
|
|
37
37
|
label: "Search provider",
|
|
38
38
|
type: "String",
|
|
39
39
|
required: true,
|
|
40
|
-
attributes: { options: ["By URL template"] },
|
|
40
|
+
attributes: { options: ["By URL template", "Firecrawl", "Tavily"] },
|
|
41
41
|
},
|
|
42
42
|
{
|
|
43
43
|
name: "url_template",
|
|
@@ -47,6 +47,13 @@ class WebSearchSkill {
|
|
|
47
47
|
required: true,
|
|
48
48
|
showIf: { search_provider: "By URL template" },
|
|
49
49
|
},
|
|
50
|
+
{
|
|
51
|
+
name: "api_key",
|
|
52
|
+
label: "API key",
|
|
53
|
+
type: "String",
|
|
54
|
+
required: true,
|
|
55
|
+
showIf: { search_provider: ["Firecrawl", "Tavily"] },
|
|
56
|
+
},
|
|
50
57
|
{
|
|
51
58
|
name: "header",
|
|
52
59
|
label: "Header",
|
|
@@ -64,16 +71,73 @@ class WebSearchSkill {
|
|
|
64
71
|
return {
|
|
65
72
|
type: "function",
|
|
66
73
|
process: async (row) => {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
74
|
+
switch (this.search_provider) {
|
|
75
|
+
case "Firecrawl":
|
|
76
|
+
{
|
|
77
|
+
const url = "https://api.firecrawl.dev/v2/search";
|
|
78
|
+
const options = {
|
|
79
|
+
method: "POST",
|
|
80
|
+
headers: {
|
|
81
|
+
Authorization: "Bearer " + this.api_key,
|
|
82
|
+
"Content-Type": "application/json",
|
|
83
|
+
},
|
|
84
|
+
body: JSON.stringify({
|
|
85
|
+
query: row.search_phrase,
|
|
86
|
+
sources: ["web"],
|
|
87
|
+
categories: [],
|
|
88
|
+
limit: 10,
|
|
89
|
+
scrapeOptions: {
|
|
90
|
+
onlyMainContent: false,
|
|
91
|
+
maxAge: 172800000,
|
|
92
|
+
parsers: ["pdf"],
|
|
93
|
+
formats: [],
|
|
94
|
+
},
|
|
95
|
+
}),
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const response = await fetch(url, options);
|
|
99
|
+
const data = await response.json();
|
|
100
|
+
return data.data.web;
|
|
101
|
+
}
|
|
102
|
+
break;
|
|
103
|
+
case "Tavily":
|
|
104
|
+
{
|
|
105
|
+
const url = "https://api.tavily.com/search";
|
|
106
|
+
const options = {
|
|
107
|
+
method: "POST",
|
|
108
|
+
headers: {
|
|
109
|
+
Authorization: "Bearer " + this.api_key,
|
|
110
|
+
"Content-Type": "application/json",
|
|
111
|
+
},
|
|
112
|
+
body: JSON.stringify({
|
|
113
|
+
query: row.search_phrase,
|
|
114
|
+
search_depth: "advanced",
|
|
115
|
+
}),
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const response = await fetch(url, options);
|
|
119
|
+
const data = await response.json();
|
|
120
|
+
return data.results;
|
|
121
|
+
}
|
|
122
|
+
break;
|
|
123
|
+
case "By URL template":
|
|
124
|
+
default:
|
|
125
|
+
{
|
|
126
|
+
const fOpts = { method: "GET" };
|
|
127
|
+
if (this.header) {
|
|
128
|
+
const [key, val] = this.header.split(":");
|
|
129
|
+
const myHeaders = new Headers();
|
|
130
|
+
myHeaders.append(key, val.trim());
|
|
131
|
+
fOpts.headers = myHeaders;
|
|
132
|
+
}
|
|
133
|
+
const url = interpolate(this.url_template, {
|
|
134
|
+
q: row.search_phrase,
|
|
135
|
+
});
|
|
136
|
+
const resp = await fetch(url);
|
|
137
|
+
return await resp.text();
|
|
138
|
+
}
|
|
139
|
+
break;
|
|
73
140
|
}
|
|
74
|
-
const url = interpolate(this.url_template, { q: row.search_phrase });
|
|
75
|
-
const resp = await fetch(url);
|
|
76
|
-
return await resp.text();
|
|
77
141
|
},
|
|
78
142
|
function: {
|
|
79
143
|
name: "web_search",
|