MCPcopy
hub / github.com/Doorman11991/smallcode / extractFromMessage

Function extractFromMessage

src/tools/tool_call_extractor.js:57–163  ·  view source on GitHub ↗

* @param {object} message OpenAI-style choice.message; has .content + maybe .tool_calls * @param {Array} toolSchemas Same shape passed to the model: [{ function: { name, ... } }, ...] * @returns {{ patched: boolean, addedCalls: number }} * * Mutates `message` in place when extraction su

(message, toolSchemas)

Source from the content-addressed store, hash-verified

55 * Mutates `message` in place when extraction succeeds.
56 */
57function extractFromMessage(message, toolSchemas) {
58 if (!message) return { patched: false, addedCalls: 0 };
59 // Already has structured tool_calls — leave it alone.
60 if (Array.isArray(message.tool_calls) && message.tool_calls.length > 0) {
61 return { patched: false, addedCalls: 0 };
62 }
63 // Some local providers (LM Studio with Liquid AI lfm2.x, llama.cpp with
64 // Qwen3 reasoning) split the response: visible text goes into `content`
65 // and chain-of-thought goes into `reasoning_content`. When the budget is
66 // tight the model can emit its tool call in reasoning_content and leave
67 // content empty. Fall back to scanning reasoning_content if content is empty.
68 const primary = typeof message.content === 'string' ? message.content : '';
69 const fallback = typeof message.reasoning_content === 'string' ? message.reasoning_content : '';
70 const content = primary && primary.trim().length > 0 ? primary : fallback;
71 if (!content) return { patched: false, addedCalls: 0 };
72 const usingReasoningFallback = content === fallback && content !== primary;
73
74 const known = new Set();
75 if (Array.isArray(toolSchemas)) {
76 for (const t of toolSchemas) {
77 const n = t?.function?.name || t?.name;
78 if (typeof n === 'string') known.add(n);
79 }
80 }
81
82 const calls = [];
83 const consumedRanges = []; // [start, end) of content we transferred into tool_calls
84
85 // 0. Liquid AI tool_call markers — `<|tool_call_start|>[func(kw=val)]<|tool_call_end|>`.
86 // Strongest signal when present; processed first so the rest of the
87 // pipeline doesn't try to interpret the Python-syntax payload as JSON.
88 try {
89 const { parseLiquidToolCalls } = require('./liquid_tool_parser');
90 const { calls: liquidCalls, ranges: liquidRanges } = parseLiquidToolCalls(content);
91 for (const c of liquidCalls) {
92 if (known.size > 0 && !known.has(c.name)) continue;
93 calls.push(c);
94 }
95 if (liquidCalls.length > 0) {
96 for (const r of liquidRanges) consumedRanges.push(r);
97 }
98 } catch {}
99
100 // 1. Tagged tool calls — strongest JSON-shaped signal.
101 for (const m of content.matchAll(TOOL_CALL_TAG_RE)) {
102 const parsed = _safeParseAny(m[1]);
103 for (const tc of _normalize(parsed, known)) calls.push(tc);
104 if (parsed) consumedRanges.push([m.index, m.index + m[0].length]);
105 }
106
107 // 2. Fenced JSON blocks. Skipped if we already got tagged calls.
108 if (calls.length === 0) {
109 for (const m of content.matchAll(FENCED_RE)) {
110 const parsed = _safeParseAny(m[1]);
111 const normalized = _normalize(parsed, known);
112 if (normalized.length > 0) {
113 for (const tc of normalized) calls.push(tc);
114 consumedRanges.push([m.index, m.index + m[0].length]);

Callers 4

executeBatchFunction · 0.85
runMethod · 0.85
runAgentLoopFunction · 0.85

Calls 5

parseLiquidToolCallsFunction · 0.85
_safeParseAnyFunction · 0.85
_normalizeFunction · 0.85
addMethod · 0.80
hasMethod · 0.45

Tested by

no test coverage detected