@saltcorn/agents 0.8.4 → 0.8.6

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 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 preview = escapeHtml(
600
- run.context.interactions
601
- .find((ix) => typeof ix?.content === "string")
602
- ?.content?.substring?.(0, 80),
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
@@ -149,7 +149,7 @@ p.prevrun_content {
149
149
  height: 100%;
150
150
  }
151
151
  .modern-chat-layout #copilotinteractions {
152
- max-height: 70vh;
152
+ /*max-height: 70vh;*/
153
153
  overflow-y: auto;
154
154
  padding: 1rem;
155
155
  display: flex;
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
- const skill = new klass(skillCfg);
61
- instances.push(skill);
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
  };
@@ -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.4",
3
+ "version": "0.8.6",
4
4
  "description": "AI agents for Saltcorn",
5
5
  "main": "index.js",
6
6
  "dependencies": {
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
- const vm = require("vm");
18
+ class CookieJar {
19
+ constructor(source) {
20
+ this.cookies = new Map();
19
21
 
20
- //const { fieldProperties } = require("./helpers");
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 resp = await fetch(row.url);
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,247 @@
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
+ 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
+ }
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. ${
36
+ this.add_sys_prompt
37
+ ? ` Additional instructions for the memory tools: ${this.add_sys_prompt}`
38
+ : ""
39
+ }${pp}`;
40
+ }
41
+
42
+ static async configFields() {
43
+ return [
44
+ {
45
+ name: "add_sys_prompt",
46
+ label: "Additional prompt",
47
+ type: "String",
48
+ fieldview: "textarea",
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
+ },
57
+ ];
58
+ }
59
+
60
+ async get_table() {
61
+ const table0 = Table.findOne("AgentLongTermMemory");
62
+ if (table0) return table0;
63
+ const tables = await Table.find({ name: "AgentLongTermMemory" });
64
+ if (tables.length) return tables[0];
65
+
66
+ //does not exist, create it
67
+ const table = await Table.create("AgentLongTermMemory", {});
68
+ await getState().refresh_tables(true);
69
+ await Field.create({
70
+ table,
71
+ name: "run_id",
72
+ label: "Run ID",
73
+ type: "Integer",
74
+ });
75
+ const uid_field = await Field.create({
76
+ table,
77
+ name: "user_id",
78
+ label: "User ID",
79
+ type: "Key to users",
80
+ attributes: {
81
+ on_delete: "Set null",
82
+ include_fts: false,
83
+ summary_field: "email",
84
+ },
85
+ });
86
+ await Field.create({
87
+ table,
88
+ name: "written_at",
89
+ label: "Written at",
90
+ type: "Date",
91
+ });
92
+ await Field.create({
93
+ table,
94
+ name: "agent_trigger_id",
95
+ label: "Agent trigger ID",
96
+ type: "Integer",
97
+ });
98
+ await Field.create({
99
+ table,
100
+ name: "memory_type",
101
+ label: "Memory type",
102
+ type: "String",
103
+ });
104
+ await Field.create({
105
+ table,
106
+ name: "personal",
107
+ label: "Personal",
108
+ type: "Bool",
109
+ });
110
+ await Field.create({
111
+ table,
112
+ name: "topic",
113
+ label: "Topic",
114
+ type: "String",
115
+ });
116
+ await Field.create({
117
+ table,
118
+ name: "contents",
119
+ label: "Contents",
120
+ type: "String",
121
+ });
122
+ await table.update({ ownership_field_id: uid_field.id });
123
+ await getState().refresh_tables();
124
+ return Table.findOne("AgentLongTermMemory");
125
+ }
126
+
127
+ provideTools() {
128
+ return [
129
+ {
130
+ type: "function",
131
+ function: {
132
+ name: "store_in_memory",
133
+ description: `Store a fact or observation in long-term memory`,
134
+ parameters: {
135
+ type: "object",
136
+ required: ["contents"],
137
+ properties: {
138
+ contents: {
139
+ type: "string",
140
+ description: "The contents of the fact or observations",
141
+ },
142
+ personal: {
143
+ type: "boolean",
144
+ description:
145
+ "Is this a fact or observation specifically about the person interacting with you now, which may not be true or relevant for another person",
146
+ },
147
+ },
148
+ },
149
+ },
150
+ process: async (arg, { req, run }) => {
151
+ const table = await this.get_table();
152
+ await table.insertRow({
153
+ run_id: run.id,
154
+ user_id: req.user?.id,
155
+ written_at: new Date(),
156
+ agent_trigger_id: run.trigger_id,
157
+ memory_type: "Episodic",
158
+ contents: arg.contents,
159
+ personal: arg.personal,
160
+ });
161
+ return "Recorded";
162
+ },
163
+ },
164
+ {
165
+ type: "function",
166
+ process: async (arg, { req }) => {
167
+ const table = await this.get_table();
168
+
169
+ const scState = getState();
170
+ const language = scState.pg_ts_config;
171
+ const use_websearch = scState.getConfig(
172
+ "search_use_websearch",
173
+ false,
174
+ );
175
+ let rows = [];
176
+ const user_id = req.user?.id;
177
+ const phrases =
178
+ typeof arg.phrases === "string" ? [arg.phrases] : arg.phrases;
179
+
180
+ if (use_websearch)
181
+ rows = await table.getRows({
182
+ _fts: {
183
+ fields: table.fields,
184
+ searchTerm: phrases.join(" OR "),
185
+ language,
186
+ use_websearch,
187
+ table: table.name,
188
+ schema: db.isSQLite ? undefined : db.getTenantSchema(),
189
+ },
190
+ ...(user_id
191
+ ? { or: [{ personal: false }, { personal: true, user_id }] }
192
+ : [{ personal: false }]),
193
+ });
194
+ else
195
+ for (const phrase of phrases) {
196
+ const my_rows = await table.getRows({
197
+ _fts: {
198
+ fields: table.fields,
199
+ searchTerm: phrase,
200
+ language,
201
+ use_websearch,
202
+ table: table.name,
203
+ schema: db.isSQLite ? undefined : db.getTenantSchema(),
204
+ },
205
+ ...(user_id
206
+ ? { or: [{ personal: false }, { personal: true, user_id }] }
207
+ : [{ personal: false }]),
208
+ });
209
+ rows.push(...my_rows);
210
+ }
211
+ const pk = table.pk_name;
212
+ rows = nubBy((r) => r[pk], rows);
213
+ //TODO sort most recent, only N memories
214
+ if (rows.length)
215
+ return (
216
+ "These memories were retrieved:\n\n" +
217
+ rows.map((r) => r.contents).join("\n")
218
+ );
219
+ else
220
+ return "There are no memories related to: " + phrases.join(" or ");
221
+ },
222
+ function: {
223
+ name: "search_memory",
224
+ description: `Search the memory bank by a search phrase`,
225
+ parameters: {
226
+ type: "object",
227
+ required: ["phrases"],
228
+ description:
229
+ "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",
230
+ properties: {
231
+ phrases: {
232
+ type: "array",
233
+ description:
234
+ "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.",
235
+ items: {
236
+ type: "string",
237
+ },
238
+ },
239
+ },
240
+ },
241
+ },
242
+ },
243
+ ];
244
+ }
245
+ }
246
+
247
+ module.exports = LongTermMemory;