Agent/utils/tool-call-parse.ts
2026-08-06 22:17:39 +08:00

308 lines
10 KiB
TypeScript

// @ts-nocheck
function parseToolCallJson(jsonStr, calls, nameHint = '') {
const raw = String(jsonStr).trim();
const candidates = [raw, raw.replace(/\{\{/g, '{').replace(/\}\}/g, '}')];
for (const s of candidates) {
try {
const obj = JSON.parse(s);
if (!obj || typeof obj !== 'object') continue;
const name = obj.name || nameHint;
if (!name) continue;
const args = obj.arguments != null
? obj.arguments
: (obj.parameters != null ? obj.parameters : {});
calls.push({
id: `toolu_local_${Date.now()}_${calls.length}`,
name: String(name),
arguments: typeof args === 'string' ? safeJson(args) : (args || {}),
});
return true;
} catch (_) { }
}
console.warn('[tool-call-parse] JSON parse failed:', raw.slice(0, 100));
return false;
}
function safeJson(s) {
try { return JSON.parse(s); } catch (_) { return { _raw: s }; }
}
function findBalancedJsonFrom(text, pos = 0) {
const open = text.indexOf('{', pos);
if (open === -1) return null;
let depth = 0;
let inStr = false;
let esc = false;
for (let i = open; i < text.length; i++) {
const c = text[i];
if (esc) { esc = false; continue; }
if (inStr) {
if (c === '\\') esc = true;
else if (c === '"') inStr = false;
continue;
}
if (c === '"') { inStr = true; continue; }
if (c === '{') depth++;
else if (c === '}') {
depth--;
if (depth === 0) {
return { json: text.slice(open, i + 1), start: open, end: i + 1 };
}
}
}
return { incomplete: true, start: open };
}
function findLastBalancedJson(text) {
let best = null;
let pos = 0;
while (pos < text.length) {
const hit = findBalancedJsonFrom(text, pos);
if (!hit) break;
if (hit.incomplete) break;
best = hit;
pos = hit.end;
}
return best;
}
function skipWs(text, pos) {
while (pos < text.length && /\s/.test(text[pos])) pos++;
return pos;
}
function hasClosingMarker(text, pos, marker) {
const rest = text.slice(pos).trimStart();
return rest.toUpperCase().startsWith(marker);
}
const OPEN_TAG_RE = /(?:<\s*\|?\s*tool_calls?\s*\|?\s*>|<\s*tool_calls?\b[^>]*>|<\s*tool_call\b(?=[\s{]))/gi;
const CLOSE_TAG_RE = /<\s*\/\s*\|?\s*tool_calls?\s*\|?\s*>/gi;
function findCloseTag(s, from) {
CLOSE_TAG_RE.lastIndex = from;
const m = CLOSE_TAG_RE.exec(s);
return m ? { index: m.index, end: m.index + m[0].length, text: m[0] } : null;
}
function extractNameHint(inner, jsonStart) {
const before = inner.slice(0, jsonStart).trim();
if (!before) return '';
const line = before.split(/\n/).map(x => x.trim()).filter(Boolean).pop() || '';
if (/^[a-z][a-z0-9_]*$/i.test(line)) return line;
return '';
}
function collectToolCallSpans(buf, { allowOpenMarkers = false } = {}) {
const spans = [];
const s = String(buf);
OPEN_TAG_RE.lastIndex = 0;
let m;
while ((m = OPEN_TAG_RE.exec(s)) !== null) {
const afterTag = m.index + m[0].length;
const close = findCloseTag(s, afterTag);
if (!close) {
if (allowOpenMarkers) {
const hit = findBalancedJsonFrom(s, afterTag);
if (hit?.json) {
const nameHint = extractNameHint(s.slice(afterTag, hit.start), hit.start - afterTag);
spans.push({
index: m.index,
end: hit.end,
json: hit.json,
nameHint,
});
}
}
continue;
}
const inner = s.slice(afterTag, close.index);
const hit = findBalancedJsonFrom(inner, 0);
if (hit?.json) {
spans.push({
index: m.index,
end: close.end,
json: hit.json,
nameHint: extractNameHint(inner, hit.start),
});
} else {
spans.push({ index: m.index, end: close.end, json: null, nameHint: '' });
}
}
CLOSE_TAG_RE.lastIndex = 0;
while ((m = CLOSE_TAG_RE.exec(s)) !== null) {
const before = s.slice(0, m.index);
if (spans.some(sp => m.index >= sp.index && m.index < sp.end)) continue;
const hit = findLastBalancedJson(before);
if (!hit?.json) continue;
const between = before.slice(hit.end).trim();
if (between && !/^[a-z][a-z0-9_]*$/i.test(between)) continue;
const nameHint = between || '';
let start = hit.start;
const head = before.slice(0, hit.start);
const junk = head.match(/(?:^|\n)[ \t]*([A-Za-z|]{3,24})[ \t]*\n?[ \t]*$/);
if (junk && /tool|call|ronic|function|invoke|xml/i.test(junk[1])) {
const idx = head.lastIndexOf(junk[1]);
if (idx >= 0) start = idx;
}
spans.push({ index: start, end: m.index + m[0].length, json: hit.json, nameHint });
}
let searchFrom = 0;
while (searchFrom < s.length) {
const hit = findBalancedJsonFrom(s, searchFrom);
if (!hit?.json) break;
if (hit.incomplete) break;
if (!spans.some(sp => hit.start >= sp.index && hit.start < sp.end)) {
try {
const obj = JSON.parse(hit.json);
const n = obj?.name;
if (n && /^(profile_|metro_)/.test(n) && (obj.arguments != null || obj.parameters != null)) {
spans.push({ index: hit.start, end: hit.end, json: hit.json, nameHint: '' });
}
} catch (_) { }
}
searchFrom = hit.end;
}
for (const marker of ['ICF', 'ICO']) {
const re = new RegExp(marker, 'gi');
re.lastIndex = 0;
while ((m = re.exec(s)) !== null) {
const afterMarker = skipWs(s, m.index + m[0].length);
const hit = findBalancedJsonFrom(s, afterMarker);
if (!hit?.json) continue;
let end = hit.end;
if (hasClosingMarker(s, hit.end, marker)) {
end = skipWs(s, hit.end) + marker.length;
} else if (!allowOpenMarkers && marker !== 'ICO') {
continue;
} else if (!allowOpenMarkers && marker === 'ICO') {
end = hit.end;
}
spans.push({ index: m.index, end, json: hit.json, nameHint: '' });
}
}
spans.sort((a, b) => a.index - b.index);
const merged = [];
for (const span of spans) {
if (merged.length && span.index < merged[merged.length - 1].end) continue;
merged.push(span);
}
return merged;
}
function consumeSpans(buf, spans, calls) {
let safeText = '';
let cursor = 0;
for (const hit of spans) {
if (hit.index < cursor) continue;
safeText += buf.slice(cursor, hit.index);
if (hit.json) parseToolCallJson(hit.json, calls, hit.nameHint || '');
cursor = hit.end;
}
return { safeText, cursor };
}
export function scanToolCallBuffer(buf) {
const calls = [];
const spans = collectToolCallSpans(buf, { allowOpenMarkers: false });
const { safeText: head, cursor } = consumeSpans(buf, spans, calls);
const tail = buf.slice(cursor);
OPEN_TAG_RE.lastIndex = 0;
let openM = null;
let tm;
while ((tm = OPEN_TAG_RE.exec(tail)) !== null) openM = tm;
if (openM && !findCloseTag(tail, openM.index + openM[0].length)) {
return {
safeText: head + tail.slice(0, openM.index),
remainder: tail.slice(openM.index),
toolCalls: calls,
};
}
for (const marker of ['ICF', 'ICO']) {
const re = new RegExp(`${marker}(?!\\w)`, 'i');
const idx = tail.search(re);
if (idx === -1) continue;
const afterMarker = skipWs(tail, idx + marker.length);
const hit = findBalancedJsonFrom(tail, afterMarker);
if (hit?.incomplete) {
return { safeText: head + tail.slice(0, idx), remainder: tail.slice(idx), toolCalls: calls };
}
}
const bare = tail.match(/\{[^{}]*"name"\s*:\s*"(?:profile_|metro_)[^"]*"\s*,\s*"arguments"\s*:\s*\{[^}]*$/);
if (bare) {
return {
safeText: head + tail.slice(0, bare.index),
remainder: tail.slice(bare.index),
toolCalls: calls,
};
}
const partial = tail.match(/(?:<\s*\|?\s*t(?:o(?:o(?:l(?:_(?:c(?:a(?:l(?:l(?:s)?)?)?)?)?)?)?)?)?|IC(?:O|F)?)?$/i);
if (partial) {
return { safeText: head + tail.slice(0, partial.index), remainder: tail.slice(partial.index), toolCalls: calls };
}
return { safeText: head + tail, remainder: '', toolCalls: calls };
}
export function finalizeToolCallBuffer(buf) {
if (!buf) return { safeText: '', toolCalls: [] };
const calls = [];
const stripped = String(buf);
const spans = collectToolCallSpans(stripped, { allowOpenMarkers: true });
const { safeText, cursor } = consumeSpans(stripped, spans, calls);
const looseText = cleanToolCallArtifacts(safeText + stripped.slice(cursor));
return { safeText: looseText, toolCalls: calls };
}
export function cleanToolCallArtifacts(fullText) {
let s = String(fullText || '');
const spans = collectToolCallSpans(s, { allowOpenMarkers: true });
if (spans.length) {
let out = '';
let cursor = 0;
for (const hit of spans) {
if (hit.index < cursor) continue;
out += s.slice(cursor, hit.index);
cursor = hit.end;
}
out += s.slice(cursor);
s = out;
}
return s
.replace(/<\s*\|?\s*\/?\s*tool_calls?\s*\|?\s*>/gi, '')
.replace(/<\/?tool_call>/gi, '')
.replace(/(?:^|\n)\s*(?:ICO|ICF)\s*(?=\n|\{|$)/gi, '\n')
.replace(/(?:^|\n)\s*[A-Za-z|]{0,24}\s*\n?\s*\{[^{}]*"name"\s*:\s*"(?:profile_|metro_)[^"]*"[\s\S]*?\}\s*(?:\n\s*<\s*\/\s*\|?\s*tool_calls?\s*\|?\s*>)?/gi, '\n')
.replace(/\n{3,}/g, '\n\n')
.trim();
}