@saltcorn/agents 0.8.6 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saltcorn/agents",
3
- "version": "0.8.6",
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
 
@@ -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",