@kaleidorg/mind 0.5.0 → 0.5.1
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/dist/qvac/parse.d.ts +15 -0
- package/dist/qvac/parse.d.ts.map +1 -1
- package/dist/qvac/parse.js +68 -5
- package/dist/qvac/parse.js.map +1 -1
- package/dist/qvac/text.d.ts.map +1 -1
- package/dist/qvac/text.js +4 -0
- package/dist/qvac/text.js.map +1 -1
- package/package.json +1 -1
- package/src/qvac/parse.test.ts +70 -1
- package/src/qvac/parse.ts +71 -5
- package/src/qvac/text.ts +4 -0
package/dist/qvac/parse.d.ts
CHANGED
|
@@ -35,10 +35,25 @@ export interface ParsedTurn {
|
|
|
35
35
|
/** Raw stop reason from the SDK, when provided. */
|
|
36
36
|
stopReason?: string;
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Recover tool calls a model emitted as PLAIN TEXT instead of structured frames
|
|
40
|
+
* — `<tool_call>{"name":…,"arguments":…}</tool_call>` (Qwen/Hermes) or a bare
|
|
41
|
+
* leading `{"name":…,"arguments":…}`. Small local models (and SDK builds that
|
|
42
|
+
* don't apply the tool grammar) do this; without recovery the call leaks into
|
|
43
|
+
* the visible answer and never runs.
|
|
44
|
+
*/
|
|
45
|
+
export declare function extractTextToolCalls(text: string): Array<{
|
|
46
|
+
name: string;
|
|
47
|
+
arguments: Record<string, unknown>;
|
|
48
|
+
}>;
|
|
38
49
|
/**
|
|
39
50
|
* Map a completion `final` (plus the streamed fallback text) into a ParsedTurn.
|
|
40
51
|
* `rawContent` prefers the SDK's framed `raw.fullText` so the Engine can anchor
|
|
41
52
|
* the next turn; falls back to the visible text when a provider has no raw form.
|
|
53
|
+
*
|
|
54
|
+
* When the SDK reports no structured tool calls, we re-scan the raw text for
|
|
55
|
+
* tool calls the model emitted inline (see `extractTextToolCalls`) so they still
|
|
56
|
+
* execute instead of leaking into the chat.
|
|
42
57
|
*/
|
|
43
58
|
export declare function finalToTurn(final: QvacFinalLike, streamed?: string): ParsedTurn;
|
|
44
59
|
//# sourceMappingURL=parse.d.ts.map
|
package/dist/qvac/parse.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../src/qvac/parse.ts"],"names":[],"mappings":"AAQA,qEAAqE;AACrE,MAAM,WAAW,aAAa;IAC5B,6DAA6D;IAC7D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2EAA2E;IAC3E,GAAG,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5B,uEAAuE;IACvE,SAAS,CAAC,EAAE,KAAK,CAAC;QAAE,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IACtF;;;;OAIG;IACH,UAAU,CAAC,EAAE,QAAQ,GAAG,WAAW,GAAG,MAAM,CAAC;CAC9C;AAED,MAAM,WAAW,UAAU;IACzB,6CAA6C;IAC7C,IAAI,EAAE,MAAM,CAAC;IACb,uEAAuE;IACvE,UAAU,EAAE,MAAM,CAAC;IACnB,oEAAoE;IACpE,SAAS,EAAE,KAAK,CAAC;QAAE,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IACpF,gFAAgF;IAChF,SAAS,EAAE,OAAO,CAAC;IACnB,mDAAmD;IACnD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED
|
|
1
|
+
{"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../src/qvac/parse.ts"],"names":[],"mappings":"AAQA,qEAAqE;AACrE,MAAM,WAAW,aAAa;IAC5B,6DAA6D;IAC7D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2EAA2E;IAC3E,GAAG,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5B,uEAAuE;IACvE,SAAS,CAAC,EAAE,KAAK,CAAC;QAAE,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IACtF;;;;OAIG;IACH,UAAU,CAAC,EAAE,QAAQ,GAAG,WAAW,GAAG,MAAM,CAAC;CAC9C;AAED,MAAM,WAAW,UAAU;IACzB,6CAA6C;IAC7C,IAAI,EAAE,MAAM,CAAC;IACb,uEAAuE;IACvE,UAAU,EAAE,MAAM,CAAC;IACnB,oEAAoE;IACpE,SAAS,EAAE,KAAK,CAAC;QAAE,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IACpF,gFAAgF;IAChF,SAAS,EAAE,OAAO,CAAC;IACnB,mDAAmD;IACnD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAkCD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,MAAM,GACX,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAC,CAc7D;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,aAAa,EAAE,QAAQ,SAAK,GAAG,UAAU,CAmB3E"}
|
package/dist/qvac/parse.js
CHANGED
|
@@ -5,22 +5,85 @@
|
|
|
5
5
|
* desktop, and the eval harness.
|
|
6
6
|
*/
|
|
7
7
|
import { cleanAssistantVisibleText } from './text.js';
|
|
8
|
+
/** Parse the first balanced `{…}` from a string as a `{name, arguments}` call. */
|
|
9
|
+
function parseCallObject(s) {
|
|
10
|
+
const start = s.indexOf('{');
|
|
11
|
+
if (start < 0)
|
|
12
|
+
return null;
|
|
13
|
+
let depth = 0;
|
|
14
|
+
for (let i = start; i < s.length; i++) {
|
|
15
|
+
const ch = s[i];
|
|
16
|
+
if (ch === '{')
|
|
17
|
+
depth++;
|
|
18
|
+
else if (ch === '}' && --depth === 0) {
|
|
19
|
+
try {
|
|
20
|
+
const obj = JSON.parse(s.slice(start, i + 1));
|
|
21
|
+
if (obj && typeof obj.name === 'string') {
|
|
22
|
+
const args = obj.arguments && typeof obj.arguments === 'object'
|
|
23
|
+
? obj.arguments
|
|
24
|
+
: {};
|
|
25
|
+
return { name: obj.name, arguments: args };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
/* malformed JSON — give up on this fragment */
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Recover tool calls a model emitted as PLAIN TEXT instead of structured frames
|
|
38
|
+
* — `<tool_call>{"name":…,"arguments":…}</tool_call>` (Qwen/Hermes) or a bare
|
|
39
|
+
* leading `{"name":…,"arguments":…}`. Small local models (and SDK builds that
|
|
40
|
+
* don't apply the tool grammar) do this; without recovery the call leaks into
|
|
41
|
+
* the visible answer and never runs.
|
|
42
|
+
*/
|
|
43
|
+
export function extractTextToolCalls(text) {
|
|
44
|
+
const calls = [];
|
|
45
|
+
for (const m of text.matchAll(/<tool_call\b[^>]*>([\s\S]*?)<\/tool_call>/gi)) {
|
|
46
|
+
const c = parseCallObject(m[1] ?? '');
|
|
47
|
+
if (c)
|
|
48
|
+
calls.push(c);
|
|
49
|
+
}
|
|
50
|
+
if (calls.length)
|
|
51
|
+
return calls;
|
|
52
|
+
// No tags — accept a bare tool-call object only at the very start of the
|
|
53
|
+
// text (so we don't misread JSON the model is merely talking about).
|
|
54
|
+
if (/^\s*\{?\s*"name"\s*:/i.test(text)) {
|
|
55
|
+
const c = parseCallObject(text);
|
|
56
|
+
if (c)
|
|
57
|
+
calls.push(c);
|
|
58
|
+
}
|
|
59
|
+
return calls;
|
|
60
|
+
}
|
|
8
61
|
/**
|
|
9
62
|
* Map a completion `final` (plus the streamed fallback text) into a ParsedTurn.
|
|
10
63
|
* `rawContent` prefers the SDK's framed `raw.fullText` so the Engine can anchor
|
|
11
64
|
* the next turn; falls back to the visible text when a provider has no raw form.
|
|
65
|
+
*
|
|
66
|
+
* When the SDK reports no structured tool calls, we re-scan the raw text for
|
|
67
|
+
* tool calls the model emitted inline (see `extractTextToolCalls`) so they still
|
|
68
|
+
* execute instead of leaking into the chat.
|
|
12
69
|
*/
|
|
13
70
|
export function finalToTurn(final, streamed = '') {
|
|
14
71
|
const rawText = final.contentText || streamed;
|
|
15
72
|
const text = cleanAssistantVisibleText(rawText);
|
|
73
|
+
let toolCalls = (final.toolCalls ?? []).map((c) => ({
|
|
74
|
+
id: c.id,
|
|
75
|
+
name: c.name,
|
|
76
|
+
arguments: c.arguments ?? {},
|
|
77
|
+
}));
|
|
78
|
+
if (toolCalls.length === 0) {
|
|
79
|
+
const recovered = extractTextToolCalls(final.raw?.fullText ?? rawText);
|
|
80
|
+
if (recovered.length)
|
|
81
|
+
toolCalls = recovered.map((c) => ({ id: undefined, ...c }));
|
|
82
|
+
}
|
|
16
83
|
return {
|
|
17
84
|
text,
|
|
18
85
|
rawContent: final.raw?.fullText ?? rawText,
|
|
19
|
-
toolCalls
|
|
20
|
-
id: c.id,
|
|
21
|
-
name: c.name,
|
|
22
|
-
arguments: c.arguments ?? {},
|
|
23
|
-
})),
|
|
86
|
+
toolCalls,
|
|
24
87
|
truncated: final.stopReason === 'length',
|
|
25
88
|
stopReason: final.stopReason,
|
|
26
89
|
};
|
package/dist/qvac/parse.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parse.js","sourceRoot":"","sources":["../../src/qvac/parse.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,yBAAyB,EAAE,MAAM,WAAW,CAAC;AA+BtD
|
|
1
|
+
{"version":3,"file":"parse.js","sourceRoot":"","sources":["../../src/qvac/parse.ts"],"names":[],"mappings":"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"}
|
package/dist/qvac/text.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"text.d.ts","sourceRoot":"","sources":["../../src/qvac/text.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"text.d.ts","sourceRoot":"","sources":["../../src/qvac/text.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAsB9D;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAsB1D"}
|
package/dist/qvac/text.js
CHANGED
|
@@ -14,6 +14,10 @@ export function cleanAssistantVisibleText(text) {
|
|
|
14
14
|
// Qwen-style reasoning sometimes arrives in contentText. Never show/speak it.
|
|
15
15
|
.replace(/<think\b[\s\S]*?<\/think>/gi, ' ')
|
|
16
16
|
.replace(/<think\b[\s\S]*$/gi, ' ')
|
|
17
|
+
// Tool calls some models emit as text (<tool_call>{…}</tool_call>) are
|
|
18
|
+
// extracted + executed by the Engine (see parse.ts); never show the tags.
|
|
19
|
+
.replace(/<tool_call\b[^>]*>[\s\S]*?<\/tool_call>/gi, ' ')
|
|
20
|
+
.replace(/<tool_call\b[^>]*>[\s\S]*$/gi, ' ')
|
|
17
21
|
.replace(/\s+/g, ' ')
|
|
18
22
|
.trim();
|
|
19
23
|
// Some small local models emit a tool-call object as plain text. Drop the
|
package/dist/qvac/text.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"text.js","sourceRoot":"","sources":["../../src/qvac/text.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;;;GAKG;AACH,MAAM,UAAU,yBAAyB,CAAC,IAAY;IACpD,IAAI,OAAO,GAAG,IAAI;QAChB,8EAA8E;SAC7E,OAAO,CAAC,6BAA6B,EAAE,GAAG,CAAC;SAC3C,OAAO,CAAC,oBAAoB,EAAE,GAAG,CAAC;
|
|
1
|
+
{"version":3,"file":"text.js","sourceRoot":"","sources":["../../src/qvac/text.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;;;GAKG;AACH,MAAM,UAAU,yBAAyB,CAAC,IAAY;IACpD,IAAI,OAAO,GAAG,IAAI;QAChB,8EAA8E;SAC7E,OAAO,CAAC,6BAA6B,EAAE,GAAG,CAAC;SAC3C,OAAO,CAAC,oBAAoB,EAAE,GAAG,CAAC;QACnC,uEAAuE;QACvE,0EAA0E;SACzE,OAAO,CAAC,2CAA2C,EAAE,GAAG,CAAC;SACzD,OAAO,CAAC,8BAA8B,EAAE,GAAG,CAAC;SAC5C,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;SACpB,IAAI,EAAE,CAAC;IAEV,0EAA0E;IAC1E,wEAAwE;IACxE,MAAM,UAAU,GAAG,OAAO,CAAC,KAAK,CAAC,0DAA0D,CAAC,CAAC;IAC7F,IAAI,UAAU,EAAE,CAAC;QACf,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACjF,CAAC;IAED,OAAO,OAAO;SACX,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;SACpB,IAAI,EAAE,CAAC;AACZ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CAAC,IAAY;IAChD,MAAM,UAAU,GAAG,IAAI;SACpB,OAAO,CAAC,oDAAoD,EAAE,mBAAmB,CAAC;SAClF,OAAO,CAAC,0BAA0B,EAAE,wBAAwB,CAAC;SAC7D,OAAO,CAAC,iBAAiB,EAAE,GAAG,CAAC;SAC/B,OAAO,CAAC,YAAY,EAAE,IAAI,CAAC;SAC3B,OAAO,CAAC,qBAAqB,EAAE,GAAG,CAAC;SACnC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC;SACrB,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC;SACrB,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC;SACtB,OAAO,CAAC,2BAA2B,EAAE,GAAG,CAAC;SACzC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAExB,OAAO,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC;SAC1B,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE;QACb,MAAM,IAAI,GAAG,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;QAC9B,OAAO,CAAC,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,IAAI,IAAI,CAAC,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,CAAC,CAAC;YACxF,IAAI,KAAK,IAAI,CAAC;IAClB,CAAC,CAAC;SACD,IAAI,CAAC,EAAE,CAAC;SACR,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;SACpB,IAAI,EAAE,CAAC;AACZ,CAAC"}
|
package/package.json
CHANGED
package/src/qvac/parse.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { finalToTurn } from './parse.js';
|
|
2
|
+
import { finalToTurn, extractTextToolCalls } from './parse.js';
|
|
3
3
|
|
|
4
4
|
describe('finalToTurn', () => {
|
|
5
5
|
it('uses contentText for visible text and strips reasoning', () => {
|
|
@@ -49,4 +49,73 @@ describe('finalToTurn', () => {
|
|
|
49
49
|
const out = finalToTurn({});
|
|
50
50
|
expect(out).toEqual({ text: '', rawContent: '', toolCalls: [], truncated: false, stopReason: undefined });
|
|
51
51
|
});
|
|
52
|
+
|
|
53
|
+
// The QVAC SDK / small models sometimes emit tool calls as plain text instead
|
|
54
|
+
// of structured frames; finalToTurn must recover them so they still execute.
|
|
55
|
+
describe('inline tool-call recovery (SDK gave no structured toolCalls)', () => {
|
|
56
|
+
it('recovers a <tool_call> block and hides the tags from the answer', () => {
|
|
57
|
+
const out = finalToTurn({
|
|
58
|
+
contentText:
|
|
59
|
+
'<tool_call> {"name": "rln_create_rgb_invoice", "arguments": {}} </tool_call>',
|
|
60
|
+
});
|
|
61
|
+
expect(out.toolCalls).toEqual([{ name: 'rln_create_rgb_invoice', arguments: {} }]);
|
|
62
|
+
expect(out.text).toBe('');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('keeps the trailing sentence after the tag out of the answer but runs the call', () => {
|
|
66
|
+
const out = finalToTurn({
|
|
67
|
+
contentText:
|
|
68
|
+
'<tool_call> {"name": "rln_create_rgb_invoice", "arguments": {}} </tool_call> Please specify the asset ID.',
|
|
69
|
+
});
|
|
70
|
+
expect(out.toolCalls).toEqual([{ name: 'rln_create_rgb_invoice', arguments: {} }]);
|
|
71
|
+
expect(out.text).toBe('Please specify the asset ID.');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('recovers nested arguments', () => {
|
|
75
|
+
const out = finalToTurn({
|
|
76
|
+
contentText:
|
|
77
|
+
'<tool_call> {"name": "lsp_get_order", "arguments": {"order_id": "latest", "access_token": "latest"}} </tool_call>',
|
|
78
|
+
});
|
|
79
|
+
expect(out.toolCalls).toEqual([
|
|
80
|
+
{ name: 'lsp_get_order', arguments: { order_id: 'latest', access_token: 'latest' } },
|
|
81
|
+
]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('recovers a bare leading tool-call object', () => {
|
|
85
|
+
const out = finalToTurn({ contentText: '{"name": "get_balances", "arguments": {}}' });
|
|
86
|
+
expect(out.toolCalls).toEqual([{ name: 'get_balances', arguments: {} }]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('does NOT recover when the SDK already returned structured calls', () => {
|
|
90
|
+
const out = finalToTurn({
|
|
91
|
+
contentText: '<tool_call> {"name": "ghost", "arguments": {}} </tool_call>',
|
|
92
|
+
toolCalls: [{ name: 'real_tool', arguments: { a: 1 } }],
|
|
93
|
+
});
|
|
94
|
+
expect(out.toolCalls).toEqual([{ id: undefined, name: 'real_tool', arguments: { a: 1 } }]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('ignores JSON the model is merely talking about (not a call)', () => {
|
|
98
|
+
const out = finalToTurn({
|
|
99
|
+
contentText: 'A tool call looks like {"name": "x", "arguments": {}} in JSON.',
|
|
100
|
+
});
|
|
101
|
+
expect(out.toolCalls).toEqual([]);
|
|
102
|
+
expect(out.text).toContain('A tool call looks like');
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('extractTextToolCalls', () => {
|
|
107
|
+
it('extracts multiple tagged calls', () => {
|
|
108
|
+
const calls = extractTextToolCalls(
|
|
109
|
+
'<tool_call>{"name":"a","arguments":{}}</tool_call> and <tool_call>{"name":"b","arguments":{"x":1}}</tool_call>',
|
|
110
|
+
);
|
|
111
|
+
expect(calls).toEqual([
|
|
112
|
+
{ name: 'a', arguments: {} },
|
|
113
|
+
{ name: 'b', arguments: { x: 1 } },
|
|
114
|
+
]);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('returns [] for plain prose', () => {
|
|
118
|
+
expect(extractTextToolCalls('just a normal answer')).toEqual([]);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
52
121
|
});
|
package/src/qvac/parse.ts
CHANGED
|
@@ -35,22 +35,88 @@ export interface ParsedTurn {
|
|
|
35
35
|
stopReason?: string;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/** Parse the first balanced `{…}` from a string as a `{name, arguments}` call. */
|
|
39
|
+
function parseCallObject(
|
|
40
|
+
s: string,
|
|
41
|
+
): { name: string; arguments: Record<string, unknown> } | null {
|
|
42
|
+
const start = s.indexOf('{');
|
|
43
|
+
if (start < 0) return null;
|
|
44
|
+
let depth = 0;
|
|
45
|
+
for (let i = start; i < s.length; i++) {
|
|
46
|
+
const ch = s[i];
|
|
47
|
+
if (ch === '{') depth++;
|
|
48
|
+
else if (ch === '}' && --depth === 0) {
|
|
49
|
+
try {
|
|
50
|
+
const obj = JSON.parse(s.slice(start, i + 1)) as {
|
|
51
|
+
name?: unknown;
|
|
52
|
+
arguments?: unknown;
|
|
53
|
+
};
|
|
54
|
+
if (obj && typeof obj.name === 'string') {
|
|
55
|
+
const args =
|
|
56
|
+
obj.arguments && typeof obj.arguments === 'object'
|
|
57
|
+
? (obj.arguments as Record<string, unknown>)
|
|
58
|
+
: {};
|
|
59
|
+
return { name: obj.name, arguments: args };
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
/* malformed JSON — give up on this fragment */
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Recover tool calls a model emitted as PLAIN TEXT instead of structured frames
|
|
72
|
+
* — `<tool_call>{"name":…,"arguments":…}</tool_call>` (Qwen/Hermes) or a bare
|
|
73
|
+
* leading `{"name":…,"arguments":…}`. Small local models (and SDK builds that
|
|
74
|
+
* don't apply the tool grammar) do this; without recovery the call leaks into
|
|
75
|
+
* the visible answer and never runs.
|
|
76
|
+
*/
|
|
77
|
+
export function extractTextToolCalls(
|
|
78
|
+
text: string,
|
|
79
|
+
): Array<{ name: string; arguments: Record<string, unknown> }> {
|
|
80
|
+
const calls: Array<{ name: string; arguments: Record<string, unknown> }> = [];
|
|
81
|
+
for (const m of text.matchAll(/<tool_call\b[^>]*>([\s\S]*?)<\/tool_call>/gi)) {
|
|
82
|
+
const c = parseCallObject(m[1] ?? '');
|
|
83
|
+
if (c) calls.push(c);
|
|
84
|
+
}
|
|
85
|
+
if (calls.length) return calls;
|
|
86
|
+
// No tags — accept a bare tool-call object only at the very start of the
|
|
87
|
+
// text (so we don't misread JSON the model is merely talking about).
|
|
88
|
+
if (/^\s*\{?\s*"name"\s*:/i.test(text)) {
|
|
89
|
+
const c = parseCallObject(text);
|
|
90
|
+
if (c) calls.push(c);
|
|
91
|
+
}
|
|
92
|
+
return calls;
|
|
93
|
+
}
|
|
94
|
+
|
|
38
95
|
/**
|
|
39
96
|
* Map a completion `final` (plus the streamed fallback text) into a ParsedTurn.
|
|
40
97
|
* `rawContent` prefers the SDK's framed `raw.fullText` so the Engine can anchor
|
|
41
98
|
* the next turn; falls back to the visible text when a provider has no raw form.
|
|
99
|
+
*
|
|
100
|
+
* When the SDK reports no structured tool calls, we re-scan the raw text for
|
|
101
|
+
* tool calls the model emitted inline (see `extractTextToolCalls`) so they still
|
|
102
|
+
* execute instead of leaking into the chat.
|
|
42
103
|
*/
|
|
43
104
|
export function finalToTurn(final: QvacFinalLike, streamed = ''): ParsedTurn {
|
|
44
105
|
const rawText = final.contentText || streamed;
|
|
45
106
|
const text = cleanAssistantVisibleText(rawText);
|
|
107
|
+
let toolCalls = (final.toolCalls ?? []).map((c) => ({
|
|
108
|
+
id: c.id,
|
|
109
|
+
name: c.name,
|
|
110
|
+
arguments: c.arguments ?? {},
|
|
111
|
+
}));
|
|
112
|
+
if (toolCalls.length === 0) {
|
|
113
|
+
const recovered = extractTextToolCalls(final.raw?.fullText ?? rawText);
|
|
114
|
+
if (recovered.length) toolCalls = recovered.map((c) => ({ id: undefined, ...c }));
|
|
115
|
+
}
|
|
46
116
|
return {
|
|
47
117
|
text,
|
|
48
118
|
rawContent: final.raw?.fullText ?? rawText,
|
|
49
|
-
toolCalls
|
|
50
|
-
id: c.id,
|
|
51
|
-
name: c.name,
|
|
52
|
-
arguments: c.arguments ?? {},
|
|
53
|
-
})),
|
|
119
|
+
toolCalls,
|
|
54
120
|
truncated: final.stopReason === 'length',
|
|
55
121
|
stopReason: final.stopReason,
|
|
56
122
|
};
|
package/src/qvac/text.ts
CHANGED
|
@@ -15,6 +15,10 @@ export function cleanAssistantVisibleText(text: string): string {
|
|
|
15
15
|
// Qwen-style reasoning sometimes arrives in contentText. Never show/speak it.
|
|
16
16
|
.replace(/<think\b[\s\S]*?<\/think>/gi, ' ')
|
|
17
17
|
.replace(/<think\b[\s\S]*$/gi, ' ')
|
|
18
|
+
// Tool calls some models emit as text (<tool_call>{…}</tool_call>) are
|
|
19
|
+
// extracted + executed by the Engine (see parse.ts); never show the tags.
|
|
20
|
+
.replace(/<tool_call\b[^>]*>[\s\S]*?<\/tool_call>/gi, ' ')
|
|
21
|
+
.replace(/<tool_call\b[^>]*>[\s\S]*$/gi, ' ')
|
|
18
22
|
.replace(/\s+/g, ' ')
|
|
19
23
|
.trim();
|
|
20
24
|
|