tune-basic-toolset 0.1.7 → 0.1.10

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/README.md CHANGED
@@ -15,12 +15,14 @@ Basic toolset for [Tune](https://github.com/iovdin/tune).
15
15
  - [powershell](#powershell) execute PowerShell command
16
16
  - [osa](#osa) manage reminders/notes/calendar (AppleScript/macOS)
17
17
  - [jina_r](#jina_r) fetch webpage content
18
- - [turn](#turn) turn based agent
19
18
  - [list](#list) keep list of tasks todo (loops for LLM)
19
+ - [sqlite](#sqlite) execute sqlite queries
20
20
  - [py](#py) run python code
21
21
  - [js](#js) run javascript code
22
- - [message](#message) talk to another chat/agent
22
+ - [turn](#turn) handoff based agent (shared context)
23
+ - [message](#message) talk to another chat/agent (separate context)
23
24
  - [Processors](#processors)
25
+ - [proc](#proc) converts tool to processor
24
26
  - [shp](#shp) include shell command output
25
27
  - [init](#init) set initial value
26
28
  - [json_format](#json_format) make LLM respond with JSON
@@ -218,39 +220,6 @@ Tune is a versatile toolkit designed for developers and users to effectively int
218
220
  ```
219
221
 
220
222
 
221
- ### `turn`
222
- A way to switch roles when building multistep agents [read more](https://iovdin.github.io/tune/examples/multi-agent)
223
- ```chat
224
- system: @turn @gpt-4o
225
- You're playing 20 questions game.
226
- You switch turns between 'thinker' and 'player' agent.
227
- Current agent stored in agent.txt file
228
- 'player' always plays first
229
-
230
- @@agent|init
231
- assistant:
232
- Is it a living thing?
233
-
234
- tool_call: turn {"role":"thinker","filename":"agent.txt"}
235
- tool_result: now it is turn of thinker to reply
236
-
237
- assistant:
238
- No.
239
-
240
- tool_call: turn {"role":"player","filename":"agent.txt"}
241
- tool_result: now it is turn of player to reply
242
-
243
- assistant:
244
- Is it something that can be used indoors?
245
-
246
- tool_call: turn {"role":"thinker","filename":"agent.txt"}
247
- tool_result: now it is turn of thinker to reply
248
-
249
- assistant:
250
- Yes.
251
-
252
- ...
253
- ```
254
223
 
255
224
  ### `list`
256
225
  Keep list of tasks to do
@@ -284,6 +253,45 @@ tool_result:
284
253
  list updated
285
254
  ```
286
255
 
256
+ ### `sqlite`
257
+ execut sqlite queries with sqlite shell
258
+
259
+ ```chat
260
+ user:
261
+ @sqlite
262
+
263
+ create a simple todo table in db.sqlite and fill it with fake data
264
+ assistant:
265
+
266
+ tool_call: sqlite {"filename":"db.sqlite"}
267
+ CREATE TABLE todo (
268
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
269
+ task TEXT NOT NULL,
270
+ status TEXT CHECK(status IN ('pending', 'completed')) NOT NULL DEFAULT 'pending',
271
+ due_date DATE
272
+ );
273
+
274
+ INSERT INTO todo (task, status, due_date) VALUES
275
+ ('Buy groceries', 'pending', '2024-05-01'),
276
+ ('Finish project report', 'pending', '2024-05-03'),
277
+ ('Book flight tickets', 'completed', '2024-04-25'),
278
+ ('Call plumber', 'pending', '2024-04-30'),
279
+ ('Schedule meeting with team', 'completed', '2024-04-20');
280
+ tool_result:
281
+
282
+ user:
283
+ check pending tasks
284
+
285
+ assistant:
286
+
287
+ tool_call: sqlite {"filename":"db.sqlite","format":"json"}
288
+ SELECT * FROM todo WHERE status = 'pending';
289
+ tool_result:
290
+ [{"id":1,"task":"Buy groceries","status":"pending","due_date":"2024-05-01"},
291
+ {"id":2,"task":"Finish project report","status":"pending","due_date":"2024-05-03"},
292
+ {"id":4,"task":"Call plumber","status":"pending","due_date":"2024-04-30"}]
293
+ ```
294
+
287
295
 
288
296
  ### `py`
289
297
  execute python code
@@ -312,6 +320,41 @@ tool_result:
312
320
 
313
321
  ```
314
322
 
323
+ ### `turn`
324
+ A way to switch roles when building multistep agents [read more](https://iovdin.github.io/tune/examples/multi-agent)
325
+ ```chat
326
+ system: @gpt-4o
327
+ @{ turn | curry filename=agent.txt}
328
+ You're playing 20 questions game.
329
+ You switch turns between 'thinker' and 'player' agent.
330
+ 'player' always plays first
331
+
332
+ @@agent|init
333
+ assistant:
334
+ Is it a living thing?
335
+
336
+ tool_call: turn {"name": "thinker"}
337
+ tool_result:
338
+ now it is turn of thinker to reply
339
+
340
+ assistant:
341
+ No.
342
+
343
+ tool_call: turn {"role":"player"}
344
+ tool_result: now it is turn of player to reply
345
+
346
+ assistant:
347
+ Is it something that can be used indoors?
348
+
349
+ tool_call: turn {"role":"thinker"}
350
+ tool_result: now it is turn of thinker to reply
351
+
352
+ assistant:
353
+ Yes.
354
+
355
+ ...
356
+ ```
357
+
315
358
  ### `message`
316
359
  Talk to another chat/agent via tool call.
317
360
  Orchestrate or evaulate other agents/chats.
@@ -321,19 +364,19 @@ system:
321
364
  Your goal is to talk to Groot at `groot.prompt` system prompt
322
365
  and try to make him say anything but 'I am Groot'
323
366
 
324
- tool_call: message {"filename":"groot.chat","system":"groot.prompt"}
367
+ tool_call: message {"filename":"groot.chat","system":"@@groot.prompt"}
325
368
  Hello Groot! How are you feeling today?
326
369
 
327
370
  tool_result:
328
371
  I am Groot!
329
372
 
330
- tool_call: message {"filename":"groot.chat","system":"groot.prompt"}
373
+ tool_call: message {"filename":"groot.chat"}
331
374
  What do you think about trees?
332
375
 
333
376
  tool_result:
334
377
  I am Groot!
335
378
 
336
- tool_call: message {"filename":"groot.chat","system":"groot.prompt"}
379
+ tool_call: message {"filename":"groot.chat"}
337
380
  Can you tell me a joke?
338
381
 
339
382
  tool_result:
@@ -370,6 +413,20 @@ Because it had a root canal!
370
413
  ## Processors
371
414
  [Processors](https://iovdin.github.io/tune/template-language/processors) is a way to modify variable or insert new ones into chat.
372
415
 
416
+ ### `proc`
417
+ converts any tool to a processor
418
+ ``` chat
419
+ system:
420
+ include project file list to system prompt
421
+ @{| proc sh git ls-files }
422
+
423
+ execute script with sqlite on db `db.sqlite` and insert result
424
+ @{ script.sql | proc sqlite filename=db.sqlite }
425
+
426
+ execut python script text="384 * 123" and insert back result
427
+ @{| proc py 384 * 123 }
428
+ ```
429
+
373
430
  ### `shp`
374
431
  Insert shell command output
375
432
  ```chat
@@ -623,3 +680,4 @@ tool_call: todo
623
680
  tool_result:
624
681
  list updated
625
682
  ```
683
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tune-basic-toolset",
3
- "version": "0.1.7",
3
+ "version": "0.1.10",
4
4
  "description": "Basic toolset for tune",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -8,6 +8,10 @@
8
8
  "README.md",
9
9
  "LICENSE"
10
10
  ],
11
+ "scripts": {
12
+ "test": "node test/index.js",
13
+ "test:watch": "node --watch test/index.js"
14
+ },
11
15
  "keywords": [
12
16
  "ai",
13
17
  "chat",
@@ -9,7 +9,7 @@
9
9
  },
10
10
  "system": {
11
11
  "type": "string",
12
- "description": "Filename that contains system prompt, required once at the beginning of simulation"
12
+ "description": "System prompt, to include file use @@path/to/file or @@file syntax"
13
13
  },
14
14
  "text": {
15
15
  "type": "string",
@@ -1,14 +1,6 @@
1
- module.exports = async function message({ filename, system, text, stop }, ctx) {
2
- let chat = await ctx.read(filename);
3
- if (!chat && system) {
4
- chat = `system: @@${system}`
1
+ module.exports = async function message({ filename, system, text, stop, save, ...args }, ctx) {
2
+ if (typeof(save) === "undefined") {
3
+ save = !!filename
5
4
  }
6
- chat = `${chat}\nuser:\n${text}`
7
- const messages = await ctx.text2run(chat, {stop: "assistant" })
8
- chat = `${chat}\n${ctx.msg2text(messages, true)}`
9
- await ctx.write(filename, chat)
10
- if (messages.length) {
11
- return messages[messages.length - 1].content
12
- }
13
- return ""
5
+ return ctx.file2run({ stop, filename, system, user: text, save }, args, ctx)
14
6
  }
package/src/mock.proc.js CHANGED
@@ -1,4 +1,5 @@
1
1
  module.exports = async function mock(node, args, ctx) {
2
+ //TODO it is not resolved into sub prompts
2
3
  const re = /(?<name>\w+)\s*=/
3
4
  let lastName;
4
5
  let m;
package/src/patch.tool.js CHANGED
@@ -23,10 +23,11 @@ module.exports = async function patch({ text, filename }, ctx) {
23
23
  let fileContent = await ctx.read(filename);
24
24
 
25
25
  for (const { oldPart, newPart } of patches) {
26
- // Escape regex special chars in oldPart, then allow flexible whitespace
27
- const escaped = oldPart
28
- .replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
29
- .replace(/\s+/g, "\\s+");
26
+ // Escape regex special chars in oldPart.
27
+ // Do NOT relax all whitespace to \s+; that can swallow preceding newlines.
28
+ // Only normalize line endings so CRLF in patches can match LF in files.
29
+ let escaped = oldPart.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
30
+ escaped = escaped.replace(/\r?\n/g, "\\r?\\n");
30
31
  const oldRegex = new RegExp(escaped, "g");
31
32
 
32
33
  // Perform replacement using a function to avoid replacement string ambiguities
@@ -35,4 +36,4 @@ module.exports = async function patch({ text, filename }, ctx) {
35
36
 
36
37
  await ctx.write(filename, fileContent);
37
38
  return "patched";
38
- };
39
+ };
@@ -0,0 +1,139 @@
1
+
2
+ module.exports = async function proc(node, args, ctx) {
3
+ /*
4
+ @{| proc sh }
5
+ @{ script.sh | proc sh }
6
+ @{| proc sqlite SELECT * FROM table }
7
+ @{| proc sqlite filename=db.sqlite text="SELECT * FROM table" }
8
+ */
9
+
10
+ const [ toolName, params ] = parseCommandLine(args)
11
+ if (node && !params.text) {
12
+ params.text = await node.read()
13
+ }
14
+
15
+ const tool = await ctx.resolve(toolName, { "type": "tool" })
16
+
17
+ console.log(params)
18
+
19
+ if (!tool || tool.type !== "tool") {
20
+ throw Error(`tool '${toolName}' not found`)
21
+ }
22
+
23
+ return {
24
+ ...node,
25
+ type: "text",
26
+ read: async() => tool.exec.call(ctx, params, ctx)
27
+ }
28
+ }
29
+
30
+ // Examples:
31
+ // parseCommandLine('command hello world bla bla')
32
+ // -> ["command", { text: "hello world bla bla" }]
33
+ //
34
+ // parseCommandLine('cmd hello=world num=8 text="bla \\"bla"')
35
+ // -> ["cmd", { hello: "world", num: 8, text: 'bla "bla' }]
36
+ //
37
+ // parseCommandLine('cmd')
38
+ // -> ["cmd", {}]
39
+
40
+ function parseCommandLine(input) {
41
+ const s = String(input);
42
+ let i = 0, len = s.length;
43
+
44
+ function skipWS() { while (i < len && /\s/.test(s[i])) i++; }
45
+
46
+ // 1) Parse leading alphanumeric command
47
+ skipWS();
48
+ const cmdStart = i;
49
+ while (i < len && /[A-Za-z0-9]/.test(s[i])) i++;
50
+ const command = s.slice(cmdStart, i);
51
+ if (!command) return [null, {}];
52
+
53
+ // 2) Parse the remainder as args
54
+ skipWS();
55
+ const r = s.slice(i);
56
+ if (!r.trim()) return [command, {}];
57
+
58
+ function parseArgs(str) {
59
+ if (!/^\s*[A-Za-z0-9]+=/.test(str)) {
60
+ return { text: str.trim() };
61
+ }
62
+
63
+ let j = 0;
64
+ const L = str.length;
65
+ const out = {};
66
+
67
+ function skipW() { while (j < L && /\s/.test(str[j])) j++; }
68
+
69
+ function parseKey() {
70
+ const start = j;
71
+ while (j < L && str[j] !== '=' && !/\s/.test(str[j])) j++;
72
+ return str.slice(start, j);
73
+ }
74
+
75
+ function parseQuotedValue(q) {
76
+ j++; // skip opening quote
77
+ let val = '';
78
+ while (j < L) {
79
+ const ch = str[j++];
80
+ if (ch === '\\') {
81
+ if (j >= L) break;
82
+ const esc = str[j++];
83
+ if (esc === 'n') val += '\n';
84
+ else if (esc === 't') val += '\t';
85
+ else if (esc === 'r') val += '\r';
86
+ else val += esc; // includes \" \\ \'
87
+ } else if (ch === q) {
88
+ return val;
89
+ } else {
90
+ val += ch;
91
+ }
92
+ }
93
+ return val; // best-effort if unclosed
94
+ }
95
+
96
+ function parseUnquotedValue() {
97
+ const start = j;
98
+ while (j < L && !/\s/.test(str[j])) j++;
99
+ return str.slice(start, j);
100
+ }
101
+
102
+ function coerce(v) {
103
+ if (/^-?\d+(\.\d+)?$/.test(v)) return Number(v);
104
+ const low = v.toLowerCase();
105
+ if (low === 'true') return true;
106
+ if (low === 'false') return false;
107
+ if (low === 'null') return null;
108
+ return v;
109
+ }
110
+
111
+ while (j < L) {
112
+ skipW();
113
+ if (j >= L) break;
114
+
115
+ const key = parseKey();
116
+ if (!key) return { text: str.trim() };
117
+
118
+ skipW();
119
+ if (j < L && str[j] === '=') {
120
+ j++; // skip '='
121
+ skipW();
122
+ let valueStr = '';
123
+ if (j < L && (str[j] === '"' || str[j] === "'")) {
124
+ valueStr = parseQuotedValue(str[j]);
125
+ } else {
126
+ valueStr = parseUnquotedValue();
127
+ }
128
+ out[key] = coerce(valueStr);
129
+ } else {
130
+ // If a non key=value token appears, treat the whole remainder as text
131
+ return { text: str.trim() };
132
+ }
133
+ }
134
+
135
+ return out;
136
+ }
137
+
138
+ return [command, parseArgs(r)];
139
+ }
package/src/sh.tool.js CHANGED
@@ -1,12 +1,33 @@
1
1
  const { execSync } = require('child_process');
2
- const util = require('util');
3
2
 
4
3
  module.exports = async function sh({ text }) {
5
4
  let result = "";
6
5
  try {
7
- result = execSync(text, { encoding: "utf8" });
6
+ // Increase maxBuffer to reduce ERR_CHILD_PROCESS_STDIO_MAXBUFFER risk on large outputs
7
+ result = execSync(text, { encoding: "utf8", maxBuffer: 10 * 1024 * 1024 });
8
8
  } catch (e) {
9
- result = e.stderr + e.stdout;
9
+ const stderr = e && typeof e.stderr !== "undefined" ? String(e.stderr || "") : "";
10
+ const stdout = e && typeof e.stdout !== "undefined" ? String(e.stdout || "") : "";
11
+
12
+ if (stderr || stdout) {
13
+ // Process started and produced output
14
+ result = stderr + stdout;
15
+ } else {
16
+ // Spawn/configuration errors or cases without stdio
17
+ const parts = [];
18
+ if (e && e.code) parts.push(`code=${e.code}`);
19
+ if (e && typeof e.status === "number") parts.push(`exit=${e.status}`);
20
+ if (e && e.signal) parts.push(`signal=${e.signal}`);
21
+ if (e && e.errno) parts.push(`errno=${e.errno}`);
22
+ if (e && e.path) parts.push(`path=${e.path}`);
23
+ const meta = parts.length ? ` (${parts.join(", ")})` : "";
24
+
25
+ if (e && e.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER") {
26
+ result = `Command output exceeded maxBuffer${meta}.`;
27
+ } else {
28
+ result = `Failed to spawn/execute command${meta}: ${e && e.message ? e.message : String(e)}`;
29
+ }
30
+ }
10
31
  }
11
32
  return (result || "").replaceAll("@", "\\@");
12
33
  };
@@ -0,0 +1,23 @@
1
+ {
2
+ "description": "Execute SQLite multiple queries on a given database file",
3
+ "parameters": {
4
+ "type": "object",
5
+ "properties": {
6
+ "filename": {
7
+ "type": "string",
8
+ "description": "Path to the SQLite database file"
9
+ },
10
+ "text": {
11
+ "type": "string",
12
+ "description": "SQL queries to execute, 1 per line"
13
+ },
14
+ "format": {
15
+ "type": "string",
16
+ "description": "Output format for the query result",
17
+ "enum": ["line", "list", "csv", "html", "json", "table", "tabs"],
18
+ "default": "line"
19
+ }
20
+ },
21
+ "required": ["filename", "text"]
22
+ }
23
+ }
@@ -0,0 +1,19 @@
1
+ const cp = require('node:child_process')
2
+
3
+ module.exports = async function sqlite({ filename, text, format = "table"}, ctx) {
4
+ let result = ""
5
+ try {
6
+ result = cp.execSync(`sqlite3 -${format} ${filename}`, { encoding: "utf8", input: text })
7
+ } catch (e) {
8
+ if (e.stderr) {
9
+ result += e.stderr
10
+ }
11
+ if (e.stdout) {
12
+ result += e.stdout
13
+ }
14
+ if (!result) {
15
+ result = e.stack
16
+ }
17
+ }
18
+ return (result || "").replaceAll("@", "\\@");
19
+ }
package/src/turn.tool.js CHANGED
@@ -1,5 +1,3 @@
1
- const fs = require("fs");
2
-
3
1
  module.exports = async function turn({ role, filename }, ctx) {
4
2
  if (filename) {
5
3
  await ctx.write(filename, `@@${role}`);