@saltcorn/agents 0.5.8 → 0.6.1

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,205 @@
1
+ // Based off https://code.google.com/p/gaequery/source/browse/trunk/src/static/scripts/jquery.autogrow-textarea.js?r=2
2
+ // Modified by David Beck
3
+
4
+ ( function( factory ) {
5
+ // UMD wrapper
6
+ if ( typeof define === 'function' && define.amd ) {
7
+ // AMD
8
+ define( [ 'jquery' ], factory );
9
+ } else if ( typeof exports !== 'undefined' ) {
10
+ // Node/CommonJS
11
+ module.exports = factory( require( 'jquery' ) );
12
+ } else {
13
+ // Browser globals
14
+ factory( jQuery );
15
+ }
16
+ }( function( $ ) {
17
+
18
+ /*
19
+ * Auto-growing textareas; technique ripped from Facebook
20
+ */
21
+ $.fn.autogrow = function(options) {
22
+
23
+ options = $.extend( {
24
+ vertical: true,
25
+ horizontal: false,
26
+ characterSlop: 0,
27
+ paddingBottom: 0
28
+ }, options);
29
+
30
+ this.filter('textarea,input').each(function() {
31
+
32
+ var $this = $(this),
33
+ borderBox = $this.css( 'box-sizing' ) === 'border-box',
34
+ // minHeight = borderBox ? $this.outerHeight() : $this.height(),
35
+ maxHeight = $this.attr( "maxHeight" ),
36
+ minWidth = typeof( $this.attr( "minWidth" ) ) == "undefined" ? 0 : $this.attr( "minWidth" );
37
+
38
+ if( typeof( maxHeight ) == "undefined" ) maxHeight = 1000000;
39
+
40
+ var shadow = $('<div class="autogrow-shadow"></div>').css( {
41
+ position: 'absolute',
42
+ top: -10000,
43
+ left: -10000,
44
+ fontSize: $this.css('fontSize'),
45
+ fontFamily: $this.css('fontFamily'),
46
+ fontWeight: $this.css('fontWeight'),
47
+ lineHeight: $this.css('lineHeight'),
48
+ paddingLeft: $this.css('paddingLeft'),
49
+ paddingRight: $this.css('paddingRight'),
50
+ paddingTop: $this.css('paddingTop'),
51
+ paddingBottom: $this.css('paddingBottom'),
52
+ borderTop: $this.css('borderTop'),
53
+ borderBottom: $this.css('borderBottom'),
54
+ borderLeft: $this.css('borderLeft'),
55
+ borderRight: $this.css('borderRight'),
56
+ resize: 'none'
57
+ } ).appendTo(document.body);
58
+
59
+ shadow.html( 'a' );
60
+ var characterWidth = shadow.width();
61
+ shadow.html( '' );
62
+
63
+ var update = function( val ) {
64
+
65
+ var times = function(string, number) {
66
+ for (var i = 0, r = ''; i < number; i ++) r += string;
67
+ return r;
68
+ };
69
+
70
+ if( typeof val === 'undefined' ) val = this.value;
71
+ if( val === '' && $(this).attr("placeholder") ) val = $(this).attr("placeholder");
72
+
73
+ if( options.vertical )
74
+ val = val.replace(/&/g, '&amp;')
75
+ .replace(/</g, '&lt;')
76
+ .replace(/>/g, '&gt;')
77
+ .replace(/\n$/, '<br/>&nbsp;')
78
+ .replace(/\n/g, '<br/>')
79
+ .replace(/ {2,}/g, function(space) { return times('&nbsp;', space.length -1) + ' '; });
80
+ else
81
+ val = escapeHtml( val );
82
+
83
+ //if( options.horizontal )
84
+ // val = $.trim( val );
85
+
86
+ // if( $(this).prop( 'tagName' ).toUpperCase() === 'INPUT' )
87
+ // shadow.text(val).css( "width", "auto" );
88
+ // else
89
+ shadow.html( val ).css( "width", "auto" ); // need to use html here otherwise no way to count spaces (with html we can use &nbsp;)
90
+
91
+ if( options.horizontal )
92
+ {
93
+ var slopWidth = options.characterSlop * characterWidth + 2;
94
+
95
+ var newWidth = Math.max( shadow.width() + slopWidth, minWidth );
96
+ var maxWidth = options.maxWidth;
97
+ //if( typeof( maxWidth ) === "undefined" ) maxWidth = $this.parent().width() - 12; // not sure why we were doing this but seems like a bad idea. doesn't work with inline-block parents for one thing, since it is the text area that should be "pushing" them to be wider
98
+ if( maxWidth ) newWidth = Math.min( newWidth, maxWidth );
99
+ $(this).css( "width", newWidth );
100
+ }
101
+
102
+ if( options.vertical )
103
+ {
104
+ var shadowWidth = $(this).width();
105
+ if( ! borderBox ) shadowWidth = shadowWidth - parseInt($this.css('paddingLeft'),10) - parseInt($this.css('paddingRight'),10);
106
+ shadow.css( "width", shadowWidth );
107
+ var shadowHeight = (borderBox ? shadow.outerHeight() : shadow.height())+options.paddingBottom;
108
+
109
+ $(this).css( "height", "auto" );
110
+ minHeight = borderBox ? $this.outerHeight() : $this.height();
111
+
112
+ var newHeight = Math.min( Math.max( shadowHeight, minHeight ), maxHeight );
113
+ $(this).css( "height", newHeight );
114
+ $(this).css( "overflow", newHeight == maxHeight ? "auto" : "hidden" );
115
+ }
116
+ };
117
+
118
+ $(this)
119
+ .change(function(){update.call( this );return true;})
120
+ .keyup(function(){update.call( this );return true;})
121
+ .keypress(function( event ) {
122
+ if( event.ctrlKey || event.metaKey ) return;
123
+
124
+ var val = this.value;
125
+ var caretInfo = _getCaretInfo( this );
126
+
127
+ var typedChar = event.which === 13 ? "\n" : String.fromCharCode( event.which );
128
+ var valAfterKeypress = val.slice( 0, caretInfo.start ) + typedChar + val.slice( caretInfo.end );
129
+ update.call( this, valAfterKeypress );
130
+ return true;
131
+ })
132
+ .bind( "update.autogrow", function(){ update.apply(this); } )
133
+ .bind( "remove.autogrow", function() {
134
+ shadow.remove();
135
+ } );
136
+
137
+ update.apply(this);
138
+
139
+ });
140
+
141
+ return this;
142
+ };
143
+
144
+ // comes from https://github.com/madapaja/jquery.selection/blob/master/src/jquery.selection.js
145
+ var _getCaretInfo = function(element){
146
+ var res = {
147
+ text: '',
148
+ start: 0,
149
+ end: 0
150
+ };
151
+
152
+ if (!element.value) {
153
+ /* no value or empty string */
154
+ return res;
155
+ }
156
+
157
+ try {
158
+ if (window.getSelection) {
159
+ /* except IE */
160
+ res.start = element.selectionStart;
161
+ res.end = element.selectionEnd;
162
+ res.text = element.value.slice(res.start, res.end);
163
+ } else if (doc.selection) {
164
+ /* for IE */
165
+ element.focus();
166
+
167
+ var range = doc.selection.createRange(),
168
+ range2 = doc.body.createTextRange();
169
+
170
+ res.text = range.text;
171
+
172
+ try {
173
+ range2.moveToElementText(element);
174
+ range2.setEndPoint('StartToStart', range);
175
+ } catch (e) {
176
+ range2 = element.createTextRange();
177
+ range2.setEndPoint('StartToStart', range);
178
+ }
179
+
180
+ res.start = element.value.length - range2.text.length;
181
+ res.end = res.start + range.text.length;
182
+ }
183
+ } catch (e) {
184
+ /* give up */
185
+ }
186
+
187
+ return res;
188
+ };
189
+
190
+ var entityMap = {
191
+ "&": "&amp;",
192
+ "<": "&lt;",
193
+ ">": "&gt;",
194
+ '"': '&quot;',
195
+ "'": '&#39;',
196
+ "/": '&#x2F;',
197
+ " ": '&nbsp;'
198
+ };
199
+
200
+ function escapeHtml(string) {
201
+ return String(string).replace(/[&<>"'\/\ ]/g, function (s) {
202
+ return entityMap[s];
203
+ } );
204
+ }
205
+ } ) );
@@ -128,6 +128,8 @@ class RetrievalByEmbedding {
128
128
  provideTools() {
129
129
  if (this.mode !== "Tool") return [];
130
130
  const table0 = Table.findOne(this.vec_field.split(".")[0]);
131
+ if(!table0) throw new Error(`Embedding Retrieval skill: cannot find table ${this.vec_field.split(".")[0]}`)
132
+
131
133
  const table_docs = this.doc_relation
132
134
  ? Table.findOne(table0.getField(this.doc_relation).reftable_name)
133
135
  : table0;
@@ -104,6 +104,7 @@ class RetrievalByFullTextSearch {
104
104
  provideTools() {
105
105
  if (this.mode !== "Tool") return [];
106
106
  const table = Table.findOne(this.table_name);
107
+ if(!table) throw new Error(`FTSRetrieval skill: cannot find table ${this.table_name}`)
107
108
  return {
108
109
  type: "function",
109
110
  process: async (arg, { req }) => {
@@ -67,11 +67,16 @@ class PreloadData {
67
67
  } else {
68
68
  if (this.add_sys_prompt) prompts.push(this.add_sys_prompt);
69
69
  const table = Table.findOne(this.table_name);
70
+ if (!table)
71
+ throw new Error(
72
+ `Preload Data skill: cannot find table ${this.table_name}`,
73
+ );
74
+
70
75
  const q = eval_expression(
71
76
  this.preload_query,
72
77
  triggering_row || {},
73
78
  user,
74
- "PreloadData query"
79
+ "PreloadData query",
75
80
  );
76
81
 
77
82
  const rows = await table.getRows(q);
@@ -116,7 +121,7 @@ class PreloadData {
116
121
  validator(s) {
117
122
  try {
118
123
  let AsyncFunction = Object.getPrototypeOf(
119
- async function () {}
124
+ async function () {},
120
125
  ).constructor;
121
126
  AsyncFunction(s);
122
127
  return true;
package/skills/Table.js CHANGED
@@ -87,6 +87,7 @@ class TableToSkill {
87
87
 
88
88
  provideTools() {
89
89
  const table = Table.findOne(this.table_name);
90
+ if(!table) throw new Error(`Table skill: cannot find table ${this.table_name}`)
90
91
  const tools = [];
91
92
  let queryProperties = {};
92
93
  let required = [];
package/skills/Trigger.js CHANGED
@@ -61,6 +61,8 @@ class TriggerToSkill {
61
61
  let properties = {};
62
62
 
63
63
  const trigger = Trigger.findOne({ name: this.trigger_name });
64
+ if(!trigger) throw new Error(`Trigger skill: cannot find trigger ${this.trigger_name}`)
65
+
64
66
  if (trigger.table_id) {
65
67
  const table = Table.findOne({ id: trigger.table_id });
66
68
 
@@ -0,0 +1,80 @@
1
+ const { getState } = require("@saltcorn/data/db/state");
2
+ const View = require("@saltcorn/data/models/view");
3
+ const WorkflowRuns = require("@saltcorn/data/models/workflow_run");
4
+ const Table = require("@saltcorn/data/models/table");
5
+ const Plugin = require("@saltcorn/data/models/plugin");
6
+
7
+ const { mockReqRes } = require("@saltcorn/data/tests/mocks");
8
+ const { afterAll, beforeAll, describe, it, expect } = require("@jest/globals");
9
+
10
+ /*
11
+
12
+ RUN WITH:
13
+ saltcorn dev:plugin-test -d ~/agents -o ~/large-language-model/
14
+
15
+ */
16
+
17
+ afterAll(require("@saltcorn/data/db").close);
18
+ beforeAll(async () => {
19
+ await require("@saltcorn/data/db/reset_schema")();
20
+ await require("@saltcorn/data/db/fixtures")();
21
+
22
+ getState().registerPlugin("base", require("@saltcorn/data/base-plugin"));
23
+ });
24
+
25
+ jest.setTimeout(30000);
26
+
27
+ const user = { id: 1, role_id: 1 };
28
+ const action = require("../action");
29
+
30
+ for (const nameconfig of require("./configs")) {
31
+ const { name, ...config } = nameconfig;
32
+ describe("agent action with " + name, () => {
33
+ let run_id;
34
+ beforeAll(async () => {
35
+ getState().registerPlugin(
36
+ "@saltcorn/large-language-model",
37
+ require("@saltcorn/large-language-model"),
38
+ config,
39
+ );
40
+ getState().registerPlugin("@saltcorn/agents", require(".."));
41
+ const runs = await WorkflowRuns.find({});
42
+ for (const run of runs) await run.delete();
43
+ });
44
+ it("has config fields", async () => {
45
+ const cfgFldsNoTable = await action.configFields({});
46
+ expect(cfgFldsNoTable.length).toBe(3);
47
+ const cfgFldsWithTable = await action.configFields({
48
+ table: Table.findOne("books"),
49
+ });
50
+ expect(cfgFldsWithTable.length).toBe(4);
51
+ });
52
+ it("generates text", async () => {
53
+ const result = await action.run({
54
+ row: { theprompt: "What is the word of the day?" },
55
+ configuration: require("./agentcfg"),
56
+ user,
57
+ req: { user },
58
+ });
59
+ expect(result.json.response).toContain("trawberry");
60
+ run_id = result.json.run_id;
61
+ });
62
+ it("queries table", async () => {
63
+ const run = await WorkflowRuns.findOne({ id: run_id });
64
+ expect(!!run).toBe(true);
65
+ const result = await action.run({
66
+ row: {
67
+ theprompt:
68
+ "How many pages are there in the book by Herman Melville in the database?",
69
+ },
70
+ configuration: require("./agentcfg"),
71
+ user,
72
+ run_id: run.id,
73
+ req: { ...mockReqRes.req, user },
74
+ });
75
+ expect(result.json.response).toContain("967");
76
+ //const run1 = await WorkflowRuns.findOne({});
77
+ });
78
+ });
79
+ //break;
80
+ }
@@ -0,0 +1,46 @@
1
+ module.exports = {
2
+ model: "",
3
+ prompt: "{{theprompt}}",
4
+ skills: [
5
+ {
6
+ mode: "Tool",
7
+ js_code: 'return "Strawberry"',
8
+ toolargs: [
9
+ {
10
+ name: "",
11
+ argtype: "string",
12
+ description: "",
13
+ },
14
+ ],
15
+ tool_name: "word_of_the_day",
16
+ skill_type: "Run JavaScript code",
17
+ add_sys_prompt: "",
18
+ tool_description: "return the word of the day",
19
+ },
20
+ {
21
+ skill_type: "Prompt picker",
22
+ placeholder: "Pick voice",
23
+ options_array: [
24
+ {
25
+ promptpicker_label: "Pirate",
26
+ promptpicker_sysprompt: "Speak like a pirate",
27
+ },
28
+ {
29
+ promptpicker_label: "Lawyer",
30
+ promptpicker_sysprompt: "Speak like a lawyer",
31
+ },
32
+ ],
33
+ },
34
+ {
35
+ mode: "Tool",
36
+ list_view: "",
37
+ doc_format: "",
38
+ skill_type: "Retrieval by full-text search",
39
+ table_name: "books",
40
+ hidden_fields: "",
41
+ add_sys_prompt:
42
+ "Use this tool to search information about books in a book database. Each book is indexed by author and has page counts. If the user asks for information about books by a specific author, use this tool.",
43
+ },
44
+ ],
45
+ sys_prompt: "",
46
+ };
@@ -0,0 +1,34 @@
1
+ module.exports = [
2
+ {
3
+ name: "OpenAI completions",
4
+ model: "gpt-5.1",
5
+ api_key: process.env.OPENAI_API_KEY,
6
+ backend: "OpenAI",
7
+ embed_model: "text-embedding-3-small",
8
+ image_model: "gpt-image-1",
9
+ temperature: 0.7,
10
+ responses_api: false,
11
+ ai_sdk_provider: "OpenAI",
12
+ },
13
+ {
14
+ name: "OpenAI responses",
15
+ model: "gpt-5.1",
16
+ api_key: process.env.OPENAI_API_KEY,
17
+ backend: "OpenAI",
18
+ embed_model: "text-embedding-3-small",
19
+ image_model: "gpt-image-1",
20
+ temperature: 0.7,
21
+ responses_api: true,
22
+ ai_sdk_provider: "OpenAI",
23
+ },
24
+ {
25
+ name: "AI SDK OpenAI",
26
+ model: "gpt-5.1",
27
+ api_key: process.env.OPENAI_API_KEY,
28
+ backend: "AI SDK",
29
+ embed_model: "text-embedding-3-small",
30
+ image_model: "gpt-image-1",
31
+ temperature: 0.7,
32
+ ai_sdk_provider: "OpenAI",
33
+ },
34
+ ];
@@ -0,0 +1,89 @@
1
+ const { getState } = require("@saltcorn/data/db/state");
2
+ const View = require("@saltcorn/data/models/view");
3
+ const Trigger = require("@saltcorn/data/models/trigger");
4
+ const Table = require("@saltcorn/data/models/table");
5
+ const Plugin = require("@saltcorn/data/models/plugin");
6
+
7
+ const { mockReqRes } = require("@saltcorn/data/tests/mocks");
8
+ const { afterAll, beforeAll, describe, it, expect } = require("@jest/globals");
9
+
10
+ /*
11
+
12
+ RUN WITH:
13
+ saltcorn dev:plugin-test -d ~/agents -o ~/large-language-model/
14
+
15
+ */
16
+
17
+ afterAll(require("@saltcorn/data/db").close);
18
+ beforeAll(async () => {
19
+ await require("@saltcorn/data/db/reset_schema")();
20
+ await require("@saltcorn/data/db/fixtures")();
21
+
22
+ getState().registerPlugin("base", require("@saltcorn/data/base-plugin"));
23
+ });
24
+
25
+ jest.setTimeout(30000);
26
+
27
+ for (const nameconfig of require("./configs")) {
28
+ const { name, ...config } = nameconfig;
29
+ describe("agent view with " + name, () => {
30
+ beforeAll(async () => {
31
+ getState().registerPlugin(
32
+ "@saltcorn/large-language-model",
33
+ require("@saltcorn/large-language-model"),
34
+ config,
35
+ );
36
+ getState().registerPlugin("@saltcorn/agents", require(".."));
37
+ });
38
+
39
+ it("creates action and view", async () => {
40
+ const trigger = await Trigger.create({
41
+ name: "AgentTest",
42
+ description: "",
43
+ action: "Agent",
44
+ when_trigger: "Never",
45
+ configuration: require("./agentcfg"),
46
+ });
47
+
48
+ await getState().refresh_triggers(false)
49
+ const view = await View.create({
50
+ name: "AgentView",
51
+ description: "",
52
+ viewtemplate: "Agent Chat",
53
+ configuration: {
54
+ stream: true,
55
+ viewname: "AgentView",
56
+ action_id: trigger.id,
57
+ explainer: "",
58
+ placeholder: "How can I help you?",
59
+ image_base64: true,
60
+ image_upload: true,
61
+ exttable_name: null,
62
+ show_prev_runs: false,
63
+ prev_runs_closed: false,
64
+ display_tool_output: true,
65
+ },
66
+ min_role: 1,
67
+ table: "books",
68
+ slug: null,
69
+ attributes: {
70
+ no_menu: false,
71
+ page_title: "",
72
+ popup_title: "",
73
+ popup_width: 800,
74
+ popup_link_out: true,
75
+ popup_minwidth: null,
76
+ page_description: "",
77
+ popup_save_indicator: false,
78
+ },
79
+ default_render_page: "",
80
+ exttable_name: null,
81
+ });
82
+ await getState().refresh_views(false)
83
+
84
+ const result = await view.run({}, mockReqRes);
85
+ expect(result).toContain(">Pirate<")
86
+ });
87
+ });
88
+ break; //only need to test one config iteration
89
+ }