webbrain-compass-tiny-v2.1 / integration /minicpm5-tool-parser.mjs
webbrain-one's picture
Release Compass Tiny v2.1: tested v24 q4f16 WebGPU, validation and caveats
3632834 verified
Raw History Blame Contribute Delete
5.31 kB
// MiniCPM5 native XML uses Python literals for typed parameters. No eval/Function.
export function parseMiniCpmParamValue(input, schema = {}) {
const raw = String(input ?? '');
const cdata = /^\s*<!\[CDATA\[([\s\S]*?)\]\]>\s*$/.exec(raw);
let decoded=cdata ? cdata[1] : raw;
if(!cdata) {
if(raw.includes('<')) throw Error('Malformed XML parameter: use CDATA for literal markup');
decoded=raw.replace(/&(#x[0-9a-f]+|#\d+|amp|lt|gt|quot|apos);/gi,(_,entity)=>{
if(entity.startsWith('#')) { const n=entity[1].toLowerCase()==='x'?parseInt(entity.slice(2),16):parseInt(entity.slice(1),10);return String.fromCodePoint(n); }
return {amp:'&',lt:'<',gt:'>',quot:'"',apos:"'"}[entity];
});
if(/&(?!(?:#x[0-9a-f]+|#\d+|amp|lt|gt|quot|apos);)/i.test(raw)) throw Error('Unescaped XML ampersand');
}
if(schema.type==='string') return decoded;
const text = decoded.trim();
if (!text) return '';
let i = 0;
const ws = () => { while (i < text.length && /\s/u.test(text[i])) i++; };
const take = char => { ws(); if (text[i++] !== char) throw Error('Invalid literal'); };
function value(depth = 0) {
if (depth > 32) throw Error('Literal nesting limit');
ws();
const c = text[i];
if (c === "'" || c === '"') {
const quote = text[i++]; let result = '';
while (i < text.length) {
const ch = text[i++];
if (ch === quote) return result;
if (ch !== '\\') { result += ch; continue; }
const e = text[i++];
const simple = { '\\':'\\', "'":"'", '"':'"', n:'\n', r:'\r', t:'\t', b:'\b', f:'\f', '/':'/' };
if (Object.hasOwn(simple,e)) { result += simple[e]; continue; }
const width = e === 'u' ? 4 : e === 'x' ? 2 : 0;
const hex = text.slice(i,i+width);
if (!width || !new RegExp(`^[a-fA-F0-9]{${width}}$`).test(hex)) throw Error('Invalid escape');
result += String.fromCharCode(parseInt(hex,16)); i += width;
}
throw Error('Unterminated string');
}
if (c === '[' || c === '{') {
i++; const array = c === '['; const end = array ? ']' : '}';
const result = array ? [] : Object.create(null);
ws(); if (text[i] === end) { i++; return array ? result : {...result}; }
while (i < text.length) {
if (array) result.push(value(depth+1));
else {
ws(); if (!['"',"'"].includes(text[i])) throw Error('Expected string key');
const key = value(depth+1);
if (Object.hasOwn(result,key)) throw Error('Duplicate key');
take(':'); result[key] = value(depth+1);
}
ws(); if (text[i] === end) { i++; return array ? result : {...result}; }
take(','); ws();
if (text[i] === end) { i++; return array ? result : {...result}; }
}
throw Error('Unterminated container');
}
const scalar = /^(True|False|None|true|false|null)(?![\w])/u.exec(text.slice(i));
if (scalar) { i += scalar[0].length; return {True:true,False:false,None:null,true:true,false:false,null:null}[scalar[0]]; }
const number = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/u.exec(text.slice(i));
if (number) { i += number[0].length; const result=Number(number[0]); if(Number.isFinite(result)) return result; }
throw Error('Not a literal');
}
try { const result=value(); ws(); if(i===text.length) return result; } catch { /* native strings are also unquoted */ }
return text;
}
// Strict standalone adapter. Keep downstream tool schema/approval validation in place.
export function parseMiniCpmToolCalls(text, toolsOrNames) {
const tools=Array.isArray(toolsOrNames)?toolsOrNames:[];
const allowedNames=Array.isArray(toolsOrNames)?new Set(tools.map(t=>t.function.name)):toolsOrNames;
const schemas=new Map(tools.map(t=>[t.function.name,t.function.parameters || {}]));
const source=String(text ?? '').trim();
if (!source || source.length>10000 || !(allowedNames instanceof Set)) return [];
// Native MiniCPM may explain an error before emitting the XML tool call.
// Accept that prose, but never skip over a malformed XML call or parameter.
const start=source.indexOf('<function');
if(start<0 || /<\/?(?:function|param)\b/.test(source.slice(0,start))) return [];
const calls=[]; let cursor=start;
const functions=/<function\s+name=["']([A-Za-z_]\w*)["']\s*>([\s\S]*?)<\/function>/gy;
const params=/<param\s+name=["']([A-Za-z_]\w*)["']\s*>([\s\S]*?)<\/param>/gy;
while(cursor<source.length) {
while (/\s/u.test(source[cursor] || '')) cursor++;
if(cursor===source.length) break;
functions.lastIndex=cursor; const fn=functions.exec(source);
if(!fn || !allowedNames.has(fn[1])) return [];
const args=Object.create(null); let pos=0;
while(pos<fn[2].length) {
while(/\s/u.test(fn[2][pos] || '')) pos++;
if(pos===fn[2].length) break;
params.lastIndex=pos; const param=params.exec(fn[2]);
if(!param || Object.hasOwn(args,param[1])) return [];
try { args[param[1]]=parseMiniCpmParamValue(param[2],schemas.get(fn[1])?.properties?.[param[1]]); }
catch { return []; }
pos=params.lastIndex;
}
calls.push({id:`compass_call_${Date.now()}_${calls.length}`,type:'function',function:{name:fn[1],arguments:JSON.stringify(args)}});
cursor=functions.lastIndex;
}
return calls;
}