@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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saltcorn/agents",
3
- "version": "0.8.5",
3
+ "version": "0.8.7",
4
4
  "description": "AI agents for Saltcorn",
5
5
  "main": "index.js",
6
6
  "dependencies": {
@@ -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 db = require("@saltcorn/data/db");
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 `Image generation`;
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: "quality",
27
- label: "Quality",
18
+ name: "size",
19
+ label: "Size",
28
20
  type: "String",
29
21
  required: true,
30
- attributes: { options: ["auto", "low", "medium", "high"] },
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: "size",
34
- label: "Size",
35
+ name: "quality",
36
+ label: "Quality",
35
37
  type: "String",
36
38
  required: true,
37
39
  attributes: {
38
- options: ["auto", "1024x1024", "1536x1024", "1024x1536"],
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 tool = {
65
- type: "image_generation",
66
- size: this.size,
67
- quality: this.quality,
68
- process: async (v, { req }) => {
69
- if (this.save_file === "Always") {
70
- const buf = Buffer.from(v.result, "base64");
71
- const file = await File.from_contents(
72
- `genimg.${v.output_format}`,
73
- `image/${v.output_format}`,
74
- buf,
75
- req?.user?.id,
76
- 100
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
- return { filename: file.path_to_serve, result: null };
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 [ws, hs] = v.size.split("x");
83
- if (v.filename)
84
- return `<img height="${+hs / 4}" width="${
85
- +ws / 4
86
- }" src="/files/serve/${v.filename}" />`;
87
- else
88
- return `<img height="${+hs / 4}" width="${+ws / 4}" src="data:image/${
89
- v.output_format
90
- };base64, ${v.result}" />`;
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
 
@@ -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
 
@@ -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
- const fOpts = { method: "GET" };
68
- if (this.header) {
69
- const [key, val] = this.header.split(":");
70
- const myHeaders = new Headers();
71
- myHeaders.append(key, val.trim());
72
- fOpts.headers = myHeaders;
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",