@vtstech/pi-react-fallback 1.0.7 → 1.0.9
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 +2 -2
- package/react-fallback.js +214 -76
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vtstech/pi-react-fallback",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
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.0.
|
|
17
|
+
"@vtstech/pi-shared": "1.0.9"
|
|
18
18
|
},
|
|
19
19
|
"peerDependencies": {
|
|
20
20
|
"@mariozechner/pi-coding-agent": ">=0.66"
|
package/react-fallback.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
// .build-npm/react-fallback/react-fallback.temp.ts
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as path from "node:path";
|
|
2
5
|
import { section, ok, fail, warn, info } from "@vtstech/pi-shared/format";
|
|
3
6
|
function sanitizeModelJson(text) {
|
|
4
7
|
text = text.replace(/:\s*True\b/g, ": true");
|
|
@@ -12,12 +15,72 @@ function sanitizeModelJson(text) {
|
|
|
12
15
|
text = text.replace(/\\\\\\\\/g, "\\\\");
|
|
13
16
|
return text;
|
|
14
17
|
}
|
|
15
|
-
var
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
var REACT_DIALECTS = [
|
|
19
|
+
{
|
|
20
|
+
name: "react",
|
|
21
|
+
actionTag: "Action:",
|
|
22
|
+
inputTag: "Action Input:",
|
|
23
|
+
thoughtTag: "Thought:",
|
|
24
|
+
stopTags: ["Observation:", "Thought:", "Final Answer:", "Action:"],
|
|
25
|
+
finalTag: "Final Answer:"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: "function",
|
|
29
|
+
actionTag: "Function:",
|
|
30
|
+
inputTag: "Function Input:",
|
|
31
|
+
thoughtTag: "Thought:",
|
|
32
|
+
stopTags: ["Observation:", "Thought:", "Final Answer:", "Function:", "Action:"],
|
|
33
|
+
finalTag: "Final Answer:"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: "tool",
|
|
37
|
+
actionTag: "Tool:",
|
|
38
|
+
inputTag: "Tool Input:",
|
|
39
|
+
thoughtTag: "Thought:",
|
|
40
|
+
stopTags: ["Observation:", "Thought:", "Final Answer:", "Tool:", "Action:"],
|
|
41
|
+
finalTag: "Final Answer:"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: "call",
|
|
45
|
+
actionTag: "Call:",
|
|
46
|
+
inputTag: "Input:",
|
|
47
|
+
thoughtTag: "Thought:",
|
|
48
|
+
stopTags: ["Observation:", "Thought:", "Final Answer:", "Call:", "Action:"],
|
|
49
|
+
finalTag: "Final Answer:"
|
|
50
|
+
}
|
|
51
|
+
];
|
|
52
|
+
function buildDialectPatterns(d) {
|
|
53
|
+
const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
54
|
+
const aT = esc(d.actionTag);
|
|
55
|
+
const iT = esc(d.inputTag);
|
|
56
|
+
const stopAlt = d.stopTags.map(esc).join("|");
|
|
57
|
+
const tT = d.thoughtTag ? esc(d.thoughtTag) : void 0;
|
|
58
|
+
const fT = d.finalTag ? esc(d.finalTag) : void 0;
|
|
59
|
+
const thoughtRe = tT ? new RegExp(`${tT}\\s*(.*?)(?=${aT}|${fT}|$)`, "is") : void 0;
|
|
60
|
+
const actionRe = new RegExp(
|
|
61
|
+
`${aT}\\s*[\\x60"']?(\\w+)[\\x60"']?\\s*\\n?\\s*${iT}\\s*(.*?)(?=\\n\\s*(?:${stopAlt})|$)`,
|
|
62
|
+
"is"
|
|
63
|
+
);
|
|
64
|
+
const actionReSameline = new RegExp(
|
|
65
|
+
`${aT}\\s*[\\x60"']?(\\w+)[\\x60"']?\\s+${iT}\\s*(.*?)(?=\\n\\s*(?:${stopAlt})|$)`,
|
|
66
|
+
"is"
|
|
67
|
+
);
|
|
68
|
+
const actionReLoose = new RegExp(
|
|
69
|
+
`${aT}\\s*(.+?)\\n\\s*${iT}\\s*(.*?)(?=\\n\\s*(?:${stopAlt})|$)`,
|
|
70
|
+
"is"
|
|
71
|
+
);
|
|
72
|
+
const actionReParen = new RegExp(`${aT}\\s*(\\w+)\\s*\\(([^)]*)\\)`, "i");
|
|
73
|
+
const finalAnswerRe = fT ? new RegExp(`${fT}\\s*([\\s\\S]*?)$`, "i") : void 0;
|
|
74
|
+
return { thoughtRe, actionRe, actionReSameline, actionReLoose, actionReParen, finalAnswerRe, dialect: d };
|
|
75
|
+
}
|
|
76
|
+
var ALL_DIALECT_PATTERNS = REACT_DIALECTS.map(buildDialectPatterns);
|
|
77
|
+
var CLASSIC_PATTERNS = ALL_DIALECT_PATTERNS[0];
|
|
78
|
+
var THOUGHT_RE = CLASSIC_PATTERNS.thoughtRe;
|
|
79
|
+
var ACTION_RE = CLASSIC_PATTERNS.actionRe;
|
|
80
|
+
var ACTION_RE_SAMELINE = CLASSIC_PATTERNS.actionReSameline;
|
|
81
|
+
var ACTION_RE_LOOSE = CLASSIC_PATTERNS.actionReLoose;
|
|
82
|
+
var ACTION_RE_PAREN = CLASSIC_PATTERNS.actionReParen;
|
|
83
|
+
var FINAL_ANSWER_RE = CLASSIC_PATTERNS.finalAnswerRe;
|
|
21
84
|
function extractJsonArgs(rawArgs) {
|
|
22
85
|
const start = rawArgs.indexOf("{");
|
|
23
86
|
if (start === -1) return null;
|
|
@@ -53,18 +116,43 @@ function extractJsonArgs(rawArgs) {
|
|
|
53
116
|
return { input: jsonStr };
|
|
54
117
|
}
|
|
55
118
|
function parseReact(text) {
|
|
119
|
+
for (const dp of ALL_DIALECT_PATTERNS) {
|
|
120
|
+
const result = parseReactWithPatterns(text, dp);
|
|
121
|
+
if (result) return result;
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
function parseReactWithPatterns(text, dp, tightLoose = false) {
|
|
56
126
|
let thought;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
127
|
+
if (dp.thoughtRe) {
|
|
128
|
+
const thoughtMatch = dp.thoughtRe.exec(text);
|
|
129
|
+
if (thoughtMatch) thought = thoughtMatch[1].trim();
|
|
130
|
+
}
|
|
131
|
+
let match = dp.actionRe.exec(text);
|
|
132
|
+
if (!match) match = dp.actionReSameline.exec(text);
|
|
61
133
|
let looseMatch = false;
|
|
62
|
-
if (!match)
|
|
134
|
+
if (!match) {
|
|
135
|
+
const looseResult = dp.actionReLoose.exec(text);
|
|
136
|
+
if (looseResult) {
|
|
137
|
+
if (tightLoose) {
|
|
138
|
+
const candidate = looseResult[1].trim().replace(/[`"']/g, "");
|
|
139
|
+
const isToolIdentifier = /^\w+$/.test(candidate) && (candidate.includes("_") || candidate.includes("-"));
|
|
140
|
+
const isKnownTool = /^(get_weather|calculate)$/i.test(candidate);
|
|
141
|
+
if (isToolIdentifier || isKnownTool) {
|
|
142
|
+
match = looseResult;
|
|
143
|
+
looseMatch = true;
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
match = looseResult;
|
|
147
|
+
looseMatch = true;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
63
151
|
let parenMatch = false;
|
|
64
|
-
if (!match) match =
|
|
152
|
+
if (!match) match = dp.actionReParen.exec(text), parenMatch = true;
|
|
65
153
|
if (match) {
|
|
66
154
|
let toolName = match[1].trim().replace(/[`"']/g, "");
|
|
67
|
-
if (looseMatch && pi.context?.session?.tools) {
|
|
155
|
+
if (looseMatch && !tightLoose && pi.context?.session?.tools) {
|
|
68
156
|
const availableTools = pi.context.session.tools || [];
|
|
69
157
|
for (const real of availableTools) {
|
|
70
158
|
const rl = real.toLowerCase().replace(/_/g, "");
|
|
@@ -112,9 +200,18 @@ function parseReact(text) {
|
|
|
112
200
|
args = extractJsonArgs(rawArgs) || { input: rawArgs };
|
|
113
201
|
}
|
|
114
202
|
let finalAnswer;
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
203
|
+
if (dp.finalAnswerRe) {
|
|
204
|
+
const faMatch = dp.finalAnswerRe.exec(text);
|
|
205
|
+
if (faMatch) finalAnswer = faMatch[1].trim();
|
|
206
|
+
}
|
|
207
|
+
return { name: toolName, args, thought, finalAnswer, raw: match[0], dialect: dp.dialect.name };
|
|
208
|
+
}
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
function detectReactDialect(text) {
|
|
212
|
+
for (const dp of ALL_DIALECT_PATTERNS) {
|
|
213
|
+
const tagPattern = new RegExp(`^\\s*${dp.dialect.actionTag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*`, "im");
|
|
214
|
+
if (tagPattern.test(text)) return dp.dialect;
|
|
118
215
|
}
|
|
119
216
|
return null;
|
|
120
217
|
}
|
|
@@ -273,82 +370,103 @@ function looksLikeSchemaDump(text) {
|
|
|
273
370
|
const matches = indicators.filter((i) => lower.includes(i.toLowerCase())).length;
|
|
274
371
|
return matches >= 2;
|
|
275
372
|
}
|
|
373
|
+
var REACT_CONFIG_PATH = path.join(os.homedir(), ".pi", "agent", "react-mode.json");
|
|
374
|
+
function readReactConfig() {
|
|
375
|
+
try {
|
|
376
|
+
if (fs.existsSync(REACT_CONFIG_PATH)) {
|
|
377
|
+
const raw = JSON.parse(fs.readFileSync(REACT_CONFIG_PATH, "utf-8"));
|
|
378
|
+
if (typeof raw.enabled === "boolean") return raw;
|
|
379
|
+
}
|
|
380
|
+
} catch {
|
|
381
|
+
}
|
|
382
|
+
return { enabled: false };
|
|
383
|
+
}
|
|
384
|
+
function writeReactConfig(config) {
|
|
385
|
+
const dir = path.dirname(REACT_CONFIG_PATH);
|
|
386
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
387
|
+
fs.writeFileSync(REACT_CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
388
|
+
}
|
|
276
389
|
function react_fallback_temp_default(pi2) {
|
|
277
|
-
let reactModeEnabled =
|
|
390
|
+
let reactModeEnabled = readReactConfig().enabled;
|
|
278
391
|
let stats = { bridgeCalls: 0, fuzzyMatches: 0, argNormalizations: 0, parseFailures: 0 };
|
|
279
392
|
const branding = [
|
|
280
|
-
` \u26A1 Pi ReAct Fallback Extension v1.0.
|
|
393
|
+
` \u26A1 Pi ReAct Fallback Extension v1.0.9`,
|
|
281
394
|
` Written by VTSTech`,
|
|
282
395
|
` GitHub: https://github.com/VTSTech`,
|
|
283
396
|
` Website: www.vts-tech.org`
|
|
284
397
|
].join("\n");
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
398
|
+
function registerBridgeTool() {
|
|
399
|
+
pi2.registerTool({
|
|
400
|
+
name: "tool_call",
|
|
401
|
+
label: "Universal Tool Call",
|
|
402
|
+
description: `Universal tool call bridge. Use this to call any available tool by specifying its name and arguments as JSON.
|
|
289
403
|
|
|
290
404
|
To use: call tool_call with:
|
|
291
405
|
- name: the exact tool name (e.g. "bash", "read", "write", "edit")
|
|
292
406
|
- arguments: a JSON string of the tool's arguments (e.g. '{"command": "ls -la"}')
|
|
293
407
|
|
|
294
408
|
The bridge will match your tool name (fuzzy matching supported) and normalize argument names automatically.`,
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
409
|
+
promptSnippet: "tool_call - universal bridge for calling any tool",
|
|
410
|
+
promptGuidelines: [
|
|
411
|
+
"When you need to use a tool but are unsure of the exact name, use tool_call with the tool name and arguments.",
|
|
412
|
+
`Example: tool_call(name='bash', arguments='{"command": "ls -la"}')`
|
|
413
|
+
],
|
|
414
|
+
parameters: {
|
|
415
|
+
type: "object",
|
|
416
|
+
properties: {
|
|
417
|
+
name: { type: "string", description: "Name of the tool to call (fuzzy matching supported)" },
|
|
418
|
+
arguments: { type: "string", description: "Tool arguments as a JSON object string" }
|
|
419
|
+
},
|
|
420
|
+
required: ["name", "arguments"]
|
|
305
421
|
},
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
422
|
+
execute: async (toolCallId, params, signal, onUpdate, ctx) => {
|
|
423
|
+
const p = params;
|
|
424
|
+
const requestedName = p.name || "";
|
|
425
|
+
const argsStr = p.arguments || "{}";
|
|
426
|
+
stats.bridgeCalls++;
|
|
427
|
+
let args;
|
|
428
|
+
try {
|
|
429
|
+
args = JSON.parse(argsStr);
|
|
430
|
+
if (typeof args !== "object" || args === null || Array.isArray(args)) {
|
|
431
|
+
args = { input: argsStr };
|
|
432
|
+
}
|
|
433
|
+
} catch {
|
|
317
434
|
args = { input: argsStr };
|
|
318
435
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
436
|
+
const allTools = pi2.getAllTools();
|
|
437
|
+
let targetToolName = null;
|
|
438
|
+
if (allTools.includes(requestedName)) {
|
|
439
|
+
targetToolName = requestedName;
|
|
440
|
+
} else {
|
|
441
|
+
targetToolName = fuzzyMatchToolName(requestedName, allTools);
|
|
442
|
+
if (targetToolName) stats.fuzzyMatches++;
|
|
443
|
+
}
|
|
444
|
+
if (!targetToolName) {
|
|
445
|
+
stats.parseFailures++;
|
|
446
|
+
return {
|
|
447
|
+
content: [{ type: "text", text: `Error: Unknown tool "${requestedName}". Available tools: ${allTools.join(", ")}` }],
|
|
448
|
+
isError: true
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
const normalizedArgs = Object.keys(args).length > 0 ? args : {};
|
|
452
|
+
stats.argNormalizations++;
|
|
453
|
+
const argsJson = JSON.stringify(normalizedArgs);
|
|
332
454
|
return {
|
|
333
|
-
content: [{
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
}
|
|
337
|
-
const normalizedArgs = Object.keys(args).length > 0 ? args : {};
|
|
338
|
-
stats.argNormalizations++;
|
|
339
|
-
const argsJson = JSON.stringify(normalizedArgs);
|
|
340
|
-
return {
|
|
341
|
-
content: [{
|
|
342
|
-
type: "text",
|
|
343
|
-
text: `[ReAct Bridge] Tool resolved: ${requestedName} \u2192 ${targetToolName}${targetToolName !== requestedName ? " (fuzzy matched)" : ""}
|
|
455
|
+
content: [{
|
|
456
|
+
type: "text",
|
|
457
|
+
text: `[ReAct Bridge] Tool resolved: ${requestedName} \u2192 ${targetToolName}${targetToolName !== requestedName ? " (fuzzy matched)" : ""}
|
|
344
458
|
|
|
345
459
|
Please call ${targetToolName} with these arguments:
|
|
346
460
|
${argsJson}`
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
461
|
+
}],
|
|
462
|
+
isError: false
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
if (reactModeEnabled) {
|
|
468
|
+
registerBridgeTool();
|
|
469
|
+
}
|
|
352
470
|
pi2.on("context", (event) => {
|
|
353
471
|
if (!reactModeEnabled) return;
|
|
354
472
|
const model = event.messages;
|
|
@@ -367,18 +485,25 @@ ${argsJson}`
|
|
|
367
485
|
description: "Toggle ReAct fallback mode for models without native tool calling",
|
|
368
486
|
handler: async (_args, ctx) => {
|
|
369
487
|
reactModeEnabled = !reactModeEnabled;
|
|
488
|
+
writeReactConfig({ enabled: reactModeEnabled });
|
|
370
489
|
const status = reactModeEnabled ? "ENABLED" : "DISABLED";
|
|
371
490
|
ctx.ui.notify(`ReAct mode ${status}`, "success");
|
|
372
491
|
const lines = [branding];
|
|
373
492
|
lines.push(section("REACT FALLBACK MODE"));
|
|
374
493
|
lines.push(info(`Status: ${status}`));
|
|
494
|
+
lines.push(info(`Config: ${REACT_CONFIG_PATH}`));
|
|
375
495
|
lines.push(info(`Bridge calls: ${stats.bridgeCalls}`));
|
|
376
496
|
lines.push(info(`Fuzzy matches: ${stats.fuzzyMatches}`));
|
|
377
497
|
lines.push(info(`Argument normalizations: ${stats.argNormalizations}`));
|
|
378
498
|
lines.push(info(`Parse failures: ${stats.parseFailures}`));
|
|
379
499
|
if (reactModeEnabled) {
|
|
500
|
+
registerBridgeTool();
|
|
380
501
|
lines.push(ok("The tool_call bridge tool is now available to the model"));
|
|
381
502
|
lines.push(info("ReAct system prompt instructions have been added"));
|
|
503
|
+
lines.push(info("Run /reload to make the bridge tool available to the current model"));
|
|
504
|
+
} else {
|
|
505
|
+
lines.push(warn("The tool_call bridge tool has been unregistered"));
|
|
506
|
+
lines.push(info("Run /reload to remove the tool from the current model"));
|
|
382
507
|
}
|
|
383
508
|
const report = lines.join("\n");
|
|
384
509
|
pi2.sendMessage({
|
|
@@ -399,15 +524,23 @@ ${argsJson}`
|
|
|
399
524
|
const lines = [branding];
|
|
400
525
|
lines.push(section("REACT PARSER TEST"));
|
|
401
526
|
lines.push(info(`Input: ${text.slice(0, 100)}${text.length > 100 ? "..." : ""}`));
|
|
527
|
+
const detectedDialect = detectReactDialect(text);
|
|
402
528
|
const reactResult = parseReact(text);
|
|
403
529
|
if (reactResult) {
|
|
404
|
-
lines.push(ok(
|
|
530
|
+
lines.push(ok(`ReAct format detected! (dialect: ${reactResult.dialect || "react"})`));
|
|
405
531
|
lines.push(info(`Tool: ${reactResult.name}`));
|
|
406
532
|
lines.push(info(`Args: ${JSON.stringify(reactResult.args)}`));
|
|
407
533
|
if (reactResult.thought) lines.push(info(`Thought: ${reactResult.thought}`));
|
|
408
534
|
if (reactResult.finalAnswer) lines.push(info(`Final Answer: ${reactResult.finalAnswer}`));
|
|
409
535
|
} else {
|
|
410
|
-
|
|
536
|
+
if (detectedDialect) {
|
|
537
|
+
lines.push(warn(`Dialect tag "${detectedDialect.actionTag}" detected but no valid tool call parsed`));
|
|
538
|
+
} else {
|
|
539
|
+
lines.push(fail("No ReAct format detected"));
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
if (detectedDialect && detectedDialect.name !== "react") {
|
|
543
|
+
lines.push(info(`Detected dialect: ${detectedDialect.name} (${detectedDialect.actionTag} / ${detectedDialect.inputTag})`));
|
|
411
544
|
}
|
|
412
545
|
try {
|
|
413
546
|
const firstBrace = text.indexOf("{");
|
|
@@ -442,13 +575,18 @@ ${argsJson}`
|
|
|
442
575
|
});
|
|
443
576
|
pi2._reactParser = {
|
|
444
577
|
parseReact,
|
|
578
|
+
parseReactWithPatterns,
|
|
579
|
+
detectReactDialect,
|
|
445
580
|
sanitizeModelJson,
|
|
446
581
|
extractToolFromJson,
|
|
447
582
|
fuzzyMatchToolName,
|
|
448
583
|
normalizeArguments,
|
|
449
|
-
looksLikeSchemaDump
|
|
584
|
+
looksLikeSchemaDump,
|
|
585
|
+
REACT_DIALECTS,
|
|
586
|
+
ALL_DIALECT_PATTERNS
|
|
450
587
|
};
|
|
451
588
|
}
|
|
452
589
|
export {
|
|
453
|
-
react_fallback_temp_default as default
|
|
590
|
+
react_fallback_temp_default as default,
|
|
591
|
+
detectReactDialect
|
|
454
592
|
};
|