@vtstech/pi-react-fallback 1.1.1 → 1.1.3

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.
Files changed (2) hide show
  1. package/package.json +2 -2
  2. package/react-fallback.js +23 -378
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vtstech/pi-react-fallback",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "description": "ReAct fallback extension for Pi Coding Agent",
5
5
  "main": "react-fallback.js",
6
6
  "keywords": ["pi-extensions"],
@@ -14,7 +14,7 @@
14
14
  "url": "https://github.com/VTSTech/pi-coding-agent"
15
15
  },
16
16
  "dependencies": {
17
- "@vtstech/pi-shared": "1.1.1"
17
+ "@vtstech/pi-shared": "1.1.3"
18
18
  },
19
19
  "peerDependencies": {
20
20
  "@mariozechner/pi-coding-agent": ">=0.66"
package/react-fallback.js CHANGED
@@ -4,373 +4,19 @@ import * as fs from "node:fs";
4
4
  import * as path from "node:path";
5
5
  import { section, ok, fail, warn, info } from "@vtstech/pi-shared/format";
6
6
  import { EXTENSION_VERSION } from "@vtstech/pi-shared/ollama";
7
- function sanitizeModelJson(text) {
8
- text = text.replace(/:\s*True\b/g, ": true");
9
- text = text.replace(/:\s*False\b/g, ": false");
10
- text = text.replace(/:\s*None\b/g, ": null");
11
- text = text.replace(/\[\s*True\b/g, "[true");
12
- text = text.replace(/\[\s*False\b/g, "[false");
13
- text = text.replace(/\[\s*None\b/g, "[null");
14
- text = text.replace(/("(?:[^"\\]|\\.)*")\s*\+\s*[^,}'"\]\n]+/g, "$1");
15
- text = text.replace(/,\s*([}\]])/g, "$1");
16
- text = text.replace(/\\\\\\\\/g, "\\\\");
17
- return text;
18
- }
19
- var REACT_DIALECTS = [
20
- {
21
- name: "react",
22
- actionTag: "Action:",
23
- inputTag: "Action Input:",
24
- thoughtTag: "Thought:",
25
- stopTags: ["Observation:", "Thought:", "Final Answer:", "Action:"],
26
- finalTag: "Final Answer:"
27
- },
28
- {
29
- name: "function",
30
- actionTag: "Function:",
31
- inputTag: "Function Input:",
32
- thoughtTag: "Thought:",
33
- stopTags: ["Observation:", "Thought:", "Final Answer:", "Function:", "Action:"],
34
- finalTag: "Final Answer:"
35
- },
36
- {
37
- name: "tool",
38
- actionTag: "Tool:",
39
- inputTag: "Tool Input:",
40
- thoughtTag: "Thought:",
41
- stopTags: ["Observation:", "Thought:", "Final Answer:", "Tool:", "Action:"],
42
- finalTag: "Final Answer:"
43
- },
44
- {
45
- name: "call",
46
- actionTag: "Call:",
47
- inputTag: "Input:",
48
- thoughtTag: "Thought:",
49
- stopTags: ["Observation:", "Thought:", "Final Answer:", "Call:", "Action:"],
50
- finalTag: "Final Answer:"
51
- }
52
- ];
53
- function buildDialectPatterns(d) {
54
- const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
55
- const aT = esc(d.actionTag);
56
- const iT = esc(d.inputTag);
57
- const stopAlt = d.stopTags.map(esc).join("|");
58
- const tT = d.thoughtTag ? esc(d.thoughtTag) : void 0;
59
- const fT = d.finalTag ? esc(d.finalTag) : void 0;
60
- const thoughtRe = tT ? new RegExp(`${tT}\\s*(.*?)(?=${aT}|${fT}|$)`, "is") : void 0;
61
- const actionRe = new RegExp(
62
- `${aT}\\s*[\\x60"']?(\\w+)[\\x60"']?\\s*\\n?\\s*${iT}\\s*(.*?)(?=\\n\\s*(?:${stopAlt})|$)`,
63
- "is"
64
- );
65
- const actionReSameline = new RegExp(
66
- `${aT}\\s*[\\x60"']?(\\w+)[\\x60"']?\\s+${iT}\\s*(.*?)(?=\\n\\s*(?:${stopAlt})|$)`,
67
- "is"
68
- );
69
- const actionReLoose = new RegExp(
70
- `${aT}\\s*(.+?)\\n\\s*${iT}\\s*(.*?)(?=\\n\\s*(?:${stopAlt})|$)`,
71
- "is"
72
- );
73
- const actionReParen = new RegExp(`${aT}\\s*(\\w+)\\s*\\(([^)]*)\\)`, "i");
74
- const finalAnswerRe = fT ? new RegExp(`${fT}\\s*([\\s\\S]*?)$`, "i") : void 0;
75
- return { thoughtRe, actionRe, actionReSameline, actionReLoose, actionReParen, finalAnswerRe, dialect: d };
76
- }
77
- var ALL_DIALECT_PATTERNS = REACT_DIALECTS.map(buildDialectPatterns);
78
- var CLASSIC_PATTERNS = ALL_DIALECT_PATTERNS[0];
79
- var THOUGHT_RE = CLASSIC_PATTERNS.thoughtRe;
80
- var ACTION_RE = CLASSIC_PATTERNS.actionRe;
81
- var ACTION_RE_SAMELINE = CLASSIC_PATTERNS.actionReSameline;
82
- var ACTION_RE_LOOSE = CLASSIC_PATTERNS.actionReLoose;
83
- var ACTION_RE_PAREN = CLASSIC_PATTERNS.actionReParen;
84
- var FINAL_ANSWER_RE = CLASSIC_PATTERNS.finalAnswerRe;
85
- function extractJsonArgs(rawArgs) {
86
- const start = rawArgs.indexOf("{");
87
- if (start === -1) return null;
88
- let depth = 0;
89
- let end = -1;
90
- for (let i = start; i < rawArgs.length; i++) {
91
- if (rawArgs[i] === "{") depth++;
92
- else if (rawArgs[i] === "}") {
93
- depth--;
94
- if (depth === 0) {
95
- end = i;
96
- break;
97
- }
98
- }
99
- }
100
- if (end === -1) return null;
101
- const jsonStr = rawArgs.slice(start, end + 1);
102
- try {
103
- const parsed = JSON.parse(jsonStr);
104
- return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : { input: String(parsed) };
105
- } catch {
106
- }
107
- try {
108
- const sanitized = sanitizeModelJson(jsonStr);
109
- const parsed = JSON.parse(sanitized);
110
- return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : { input: String(parsed) };
111
- } catch {
112
- }
113
- const exprMatch = jsonStr.match(/['"]expression['"]:\s*['"]([^'"]+)['"]/);
114
- if (exprMatch) return { expression: exprMatch[1] };
115
- const cmdMatch = jsonStr.match(/['"]command['"]:\s*['"]([^'"]+)['"]/);
116
- if (cmdMatch) return { command: cmdMatch[1] };
117
- return { input: jsonStr };
118
- }
119
- function parseReact(text) {
120
- for (const dp of ALL_DIALECT_PATTERNS) {
121
- const result = parseReactWithPatterns(text, dp);
122
- if (result) return result;
123
- }
124
- return null;
125
- }
126
- function parseReactWithPatterns(text, dp, tightLoose = false) {
127
- let thought;
128
- if (dp.thoughtRe) {
129
- const thoughtMatch = dp.thoughtRe.exec(text);
130
- if (thoughtMatch) thought = thoughtMatch[1].trim();
131
- }
132
- let match = dp.actionRe.exec(text);
133
- if (!match) match = dp.actionReSameline.exec(text);
134
- let looseMatch = false;
135
- if (!match) {
136
- const looseResult = dp.actionReLoose.exec(text);
137
- if (looseResult) {
138
- if (tightLoose) {
139
- const candidate = looseResult[1].trim().replace(/[`"']/g, "");
140
- const isToolIdentifier = /^\w+$/.test(candidate) && (candidate.includes("_") || candidate.includes("-"));
141
- const isKnownTool = /^(get_weather|calculate)$/i.test(candidate);
142
- if (isToolIdentifier || isKnownTool) {
143
- match = looseResult;
144
- looseMatch = true;
145
- }
146
- } else {
147
- match = looseResult;
148
- looseMatch = true;
149
- }
150
- }
151
- }
152
- let parenMatch = false;
153
- if (!match) match = dp.actionReParen.exec(text), parenMatch = true;
154
- if (match) {
155
- let toolName = match[1].trim().replace(/[`"']/g, "");
156
- if (looseMatch && !tightLoose && pi.context?.session?.tools) {
157
- const availableTools = pi.context.session.tools || [];
158
- for (const real of availableTools) {
159
- const rl = real.toLowerCase().replace(/_/g, "");
160
- if (toolName.toLowerCase().includes(rl)) {
161
- toolName = real;
162
- break;
163
- }
164
- }
165
- if (toolName.includes(" ")) {
166
- const words = toolName.split(/\s+/);
167
- for (const w of words) {
168
- const wc = w.replace(/[^a-zA-Z0-9_-]/g, "");
169
- if (wc.length < 3) continue;
170
- for (const real of availableTools) {
171
- const rl = real.toLowerCase().replace(/_/g, "");
172
- if (rl.includes(wc.toLowerCase())) {
173
- toolName = real;
174
- break;
175
- }
176
- }
177
- if (!toolName.includes(" ")) break;
178
- }
179
- }
180
- }
181
- const rawArgs = match[2].trim().replace(/^```\w*\s*/gm, "").replace(/```\s*$/gm, "").trim();
182
- let args;
183
- if (parenMatch && rawArgs && !rawArgs.startsWith("{")) {
184
- const pairs = rawArgs.match(/(\w+)\s*:\s*("[^"]*"|'[^']*'|\S+)/g);
185
- if (pairs) {
186
- const obj = {};
187
- for (const p of pairs) {
188
- const colonIdx = p.indexOf(":");
189
- const key = p.slice(0, colonIdx).trim();
190
- let val = p.slice(colonIdx + 1).trim();
191
- if (val.startsWith('"') && val.endsWith('"') || val.startsWith("'") && val.endsWith("'")) {
192
- val = val.slice(1, -1);
193
- }
194
- obj[key] = val;
195
- }
196
- args = obj;
197
- } else {
198
- args = { input: rawArgs };
199
- }
200
- } else {
201
- args = extractJsonArgs(rawArgs) || { input: rawArgs };
202
- }
203
- let finalAnswer;
204
- if (dp.finalAnswerRe) {
205
- const faMatch = dp.finalAnswerRe.exec(text);
206
- if (faMatch) finalAnswer = faMatch[1].trim();
207
- }
208
- return { name: toolName, args, thought, finalAnswer, raw: match[0], dialect: dp.dialect.name };
209
- }
210
- return null;
211
- }
212
- function detectReactDialect(text) {
213
- for (const dp of ALL_DIALECT_PATTERNS) {
214
- const tagPattern = new RegExp(`^\\s*${dp.dialect.actionTag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*`, "im");
215
- if (tagPattern.test(text)) return dp.dialect;
216
- }
217
- return null;
218
- }
219
- function extractToolFromJson(obj) {
220
- if (!obj || typeof obj !== "object") return null;
221
- let name = obj.name || obj.function || obj.tool || obj.action;
222
- let args = obj.arguments || obj.parameters || obj.args || obj.actionInput || {};
223
- if (!name) {
224
- for (const key of Object.keys(obj)) {
225
- const kl = key.toLowerCase();
226
- if (kl === "action" && typeof obj[key] === "string") {
227
- name = obj[key];
228
- }
229
- if (kl === "action input" || kl === "actioninput" || kl === "action_input") {
230
- const val = obj[key];
231
- if (typeof val === "object" && val !== null) args = val;
232
- else if (val) args = { input: val };
233
- }
234
- }
235
- }
236
- if (!name) {
237
- const argToTool = { expression: "calculator", command: "shell" };
238
- const nonToolKeys = /* @__PURE__ */ new Set(["response", "method", "answer", "result", "explanation", "output", "text"]);
239
- const objKeys = Object.keys(obj);
240
- if (!objKeys.some((k) => nonToolKeys.has(k))) {
241
- for (const key of objKeys) {
242
- if (key in argToTool) {
243
- name = argToTool[key];
244
- args = obj;
245
- break;
246
- }
247
- }
248
- }
249
- }
250
- if (!name || typeof args !== "object" || args === null) return null;
251
- return { name, args };
252
- }
253
- var WORD_MAPPINGS = {
254
- calculate: ["calculator"],
255
- calc: ["calculator"],
256
- math: ["calculator"],
257
- compute: ["calculator"],
258
- eval: ["calculator"],
259
- expression: ["calculator"],
260
- power: ["calculator"],
261
- pow: ["calculator"],
262
- sqrt: ["calculator"],
263
- python: ["shell"],
264
- repl: ["shell"],
265
- code: ["shell"],
266
- execute: ["shell"],
267
- shell: ["bash"],
268
- bash: ["bash"],
269
- cmd: ["bash"],
270
- command: ["bash"],
271
- ls: ["bash"],
272
- cat: ["bash"],
273
- echo: ["bash"],
274
- grep: ["bash"],
275
- read: ["read"],
276
- write: ["write"],
277
- file: ["read"],
278
- weather: ["get_weather"],
279
- search: ["bash"]
280
- };
281
- function fuzzyMatchToolName(hallucinated, availableTools) {
282
- const lower = hallucinated.toLowerCase().replace(/_/g, "");
283
- if (availableTools.includes(hallucinated)) return hallucinated;
284
- for (const real of availableTools) {
285
- const rl = real.toLowerCase().replace(/_/g, "");
286
- if (rl === lower || rl.includes(lower) || lower.includes(rl)) return real;
287
- }
288
- for (const [keyword, hints] of Object.entries(WORD_MAPPINGS)) {
289
- if (lower.includes(keyword)) {
290
- for (const hint of hints) {
291
- for (const real of availableTools) {
292
- if (real.includes(hint) || real === hint) return real;
293
- }
294
- }
295
- }
296
- }
297
- if (lower.length >= 4) {
298
- for (const real of availableTools) {
299
- const rl = real.toLowerCase();
300
- if (rl.length >= 4 && rl.slice(0, 4) === lower.slice(0, 4)) return real;
301
- }
302
- }
303
- return null;
304
- }
305
- var ARG_ALIASES = {
306
- expression: ["expr", "exp", "formula", "calculation", "math"],
307
- file_path: ["path", "filepath", "file", "filename", "location"],
308
- content: ["text", "data", "body", "value"],
309
- command: ["cmd", "shell", "script", "exec"],
310
- url: ["uri", "link", "endpoint", "address"],
311
- query: ["search", "term", "keywords", "q"],
312
- input: ["value", "arg", "parameter"],
313
- timeout: ["time_limit", "max_time", "seconds"]
314
- };
315
- function normalizeArguments(args, expectedParams) {
316
- if (!args || typeof args !== "object") return args;
317
- const expectedSet = new Set(expectedParams.map((p) => p.toLowerCase()));
318
- const normalized = {};
319
- const powerParts = {};
320
- for (const [key, value] of Object.entries(args)) {
321
- const keyLower = key.toLowerCase().replace(/-/g, "_");
322
- let targetParam = null;
323
- for (const param of expectedParams) {
324
- if (param.toLowerCase() === keyLower) {
325
- targetParam = param;
326
- break;
327
- }
328
- }
329
- if (!targetParam) {
330
- for (const [canonical, aliases] of Object.entries(ARG_ALIASES)) {
331
- if (aliases.includes(keyLower) && expectedSet.has(canonical.toLowerCase())) {
332
- targetParam = canonical;
333
- break;
334
- }
335
- }
336
- }
337
- if (!targetParam) {
338
- for (const param of expectedParams) {
339
- if (keyLower.includes(param.toLowerCase()) || keyLower.startsWith(param.toLowerCase())) {
340
- targetParam = param;
341
- break;
342
- }
343
- }
344
- }
345
- if (["base", "value", "x"].includes(keyLower) || ["exponent", "power", "n", "p", "exp"].includes(keyLower)) {
346
- powerParts[keyLower] = value;
347
- continue;
348
- }
349
- const finalKey = targetParam || key;
350
- if (!(finalKey in normalized)) normalized[finalKey] = value;
351
- }
352
- if (powerParts && expectedSet.has("expression")) {
353
- const base = powerParts.base ?? powerParts.value ?? powerParts.x;
354
- const exp = powerParts.exponent ?? powerParts.power ?? powerParts.n ?? powerParts.p ?? powerParts.exp;
355
- if (base !== void 0 && exp !== void 0) normalized.expression = `${base} ** ${exp}`;
356
- else if (base !== void 0) normalized.expression = String(base);
357
- }
358
- return normalized;
359
- }
360
- function looksLikeSchemaDump(text) {
361
- if (!text) return false;
362
- const indicators = [
363
- '{"function <nil>',
364
- '"type":"function"',
365
- '"parameters":{"type":"object"',
366
- '[{"type":',
367
- '"required":',
368
- '"properties":'
369
- ];
370
- const lower = text.toLowerCase();
371
- const matches = indicators.filter((i) => lower.includes(i.toLowerCase())).length;
372
- return matches >= 2;
373
- }
7
+ import {
8
+ sanitizeModelJson,
9
+ extractToolFromJson,
10
+ parseReact,
11
+ parseReactWithPatterns,
12
+ detectReactDialect,
13
+ fuzzyMatchToolName,
14
+ normalizeArguments,
15
+ looksLikeSchemaDump,
16
+ REACT_DIALECTS,
17
+ ALL_DIALECT_PATTERNS,
18
+ FINAL_ANSWER_RE
19
+ } from "@vtstech/pi-shared/react-parser";
374
20
  var REACT_CONFIG_PATH = path.join(os.homedir(), ".pi", "agent", "react-mode.json");
375
21
  function readReactConfig() {
376
22
  try {
@@ -387,7 +33,7 @@ function writeReactConfig(config) {
387
33
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
388
34
  fs.writeFileSync(REACT_CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", "utf-8");
389
35
  }
390
- function react_fallback_temp_default(pi2) {
36
+ function react_fallback_temp_default(pi) {
391
37
  let reactModeEnabled = readReactConfig().enabled;
392
38
  let stats = { bridgeCalls: 0, fuzzyMatches: 0, argNormalizations: 0, parseFailures: 0 };
393
39
  const branding = [
@@ -397,7 +43,7 @@ function react_fallback_temp_default(pi2) {
397
43
  ` Website: www.vts-tech.org`
398
44
  ].join("\n");
399
45
  function registerBridgeTool() {
400
- pi2.registerTool({
46
+ pi.registerTool({
401
47
  name: "tool_call",
402
48
  label: "Universal Tool Call",
403
49
  description: `Universal tool call bridge. Use this to call any available tool by specifying its name and arguments as JSON.
@@ -434,7 +80,7 @@ The bridge will match your tool name (fuzzy matching supported) and normalize ar
434
80
  } catch {
435
81
  args = { input: argsStr };
436
82
  }
437
- const allTools = pi2.getAllTools();
83
+ const allTools = pi.getAllTools();
438
84
  let targetToolName = null;
439
85
  if (allTools.includes(requestedName)) {
440
86
  targetToolName = requestedName;
@@ -468,7 +114,7 @@ ${argsJson}`
468
114
  if (reactModeEnabled) {
469
115
  registerBridgeTool();
470
116
  }
471
- pi2.on("context", (event) => {
117
+ pi.on("context", (event) => {
472
118
  if (!reactModeEnabled) return;
473
119
  const model = event.messages;
474
120
  for (let i = model.length - 1; i >= 0; i--) {
@@ -482,7 +128,7 @@ ${argsJson}`
482
128
  }
483
129
  }
484
130
  });
485
- pi2.registerCommand("react-mode", {
131
+ pi.registerCommand("react-mode", {
486
132
  description: "Toggle ReAct fallback mode for models without native tool calling",
487
133
  handler: async (_args, ctx) => {
488
134
  reactModeEnabled = !reactModeEnabled;
@@ -507,14 +153,14 @@ ${argsJson}`
507
153
  lines.push(info("Run /reload to remove the tool from the current model"));
508
154
  }
509
155
  const report = lines.join("\n");
510
- pi2.sendMessage({
156
+ pi.sendMessage({
511
157
  customType: "react-mode-report",
512
158
  content: report,
513
159
  display: { type: "content", content: report }
514
160
  });
515
161
  }
516
162
  });
517
- pi2.registerCommand("react-parse", {
163
+ pi.registerCommand("react-parse", {
518
164
  description: "Test the ReAct parser against a text input: /react-parse <text>",
519
165
  handler: async (args, ctx) => {
520
166
  const text = args.trim();
@@ -568,14 +214,14 @@ ${argsJson}`
568
214
  const fa = faMatch[1].trim();
569
215
  lines.push(ok(`Final Answer: ${fa}`));
570
216
  }
571
- pi2.sendMessage({
217
+ pi.sendMessage({
572
218
  customType: "react-parse-report",
573
219
  content: lines.join("\n"),
574
220
  display: { type: "content", content: lines.join("\n") }
575
221
  });
576
222
  }
577
223
  });
578
- pi2._reactParser = {
224
+ pi._reactParser = {
579
225
  parseReact,
580
226
  parseReactWithPatterns,
581
227
  detectReactDialect,
@@ -589,6 +235,5 @@ ${argsJson}`
589
235
  };
590
236
  }
591
237
  export {
592
- react_fallback_temp_default as default,
593
- detectReactDialect
238
+ react_fallback_temp_default as default
594
239
  };