@hunterzhu/pulse-adapters 0.4.3 → 0.4.4
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.
|
@@ -232,23 +232,81 @@ function parseDsmlJson(value) {
|
|
|
232
232
|
return undefined;
|
|
233
233
|
}
|
|
234
234
|
}
|
|
235
|
-
/** DeepSeek sometimes writes
|
|
235
|
+
/** DeepSeek sometimes writes tool calls as JSON or markdown fences instead of tool_calls. A bare object still needs an explicit tool name. Argument-shaped JSON, a single tool object after prose, and any trailing prose stay text. */
|
|
236
236
|
function recoverDeepSeekJsonToolCalls(text, toolNameAliases) {
|
|
237
|
-
const
|
|
237
|
+
const trimmed = text.trim();
|
|
238
|
+
return recoverStrictJsonToolCalls(trimmed, toolNameAliases) ?? recoverMarkdownToolFences(trimmed, toolNameAliases);
|
|
239
|
+
}
|
|
240
|
+
function recoverStrictJsonToolCalls(text, toolNameAliases) {
|
|
241
|
+
const leading = takeLeadingJsonValues(text);
|
|
238
242
|
if (leading === undefined || leading.values.length === 0 || leading.rest.trim().length > 0)
|
|
239
243
|
return undefined;
|
|
240
244
|
const objects = leading.values.filter((value) => !isEmptyJsonValue(value));
|
|
241
245
|
if (objects.length === 0)
|
|
242
246
|
return undefined;
|
|
247
|
+
if (objects.length === 1 && Array.isArray(objects[0]) && objects[0].every((item) => argumentObject(item) !== undefined))
|
|
248
|
+
return toolCallsFrom(objects[0], toolNameAliases);
|
|
243
249
|
if (objects.length === 1 && objects[0] && typeof objects[0] === 'object' && !Array.isArray(objects[0])) {
|
|
244
250
|
const batch = objects[0].tool_calls ?? objects[0].toolCalls;
|
|
245
|
-
if (Array.isArray(batch))
|
|
246
|
-
|
|
247
|
-
|
|
251
|
+
if (Array.isArray(batch))
|
|
252
|
+
return toolCallsFrom(batch, toolNameAliases);
|
|
253
|
+
}
|
|
254
|
+
return toolCallsFrom(objects, toolNameAliases);
|
|
255
|
+
}
|
|
256
|
+
/** A message that is only `tool.name` headings followed by JSON fences is a tool batch, not a finished answer. */
|
|
257
|
+
function recoverMarkdownToolFences(text, toolNameAliases) {
|
|
258
|
+
const lines = text.split(/\r?\n/);
|
|
259
|
+
const calls = [];
|
|
260
|
+
let index = 0;
|
|
261
|
+
const skipBlank = () => { while (index < lines.length && lines[index].trim() === '')
|
|
262
|
+
index++; };
|
|
263
|
+
skipBlank();
|
|
264
|
+
if (index >= lines.length)
|
|
265
|
+
return undefined;
|
|
266
|
+
while (index < lines.length) {
|
|
267
|
+
skipBlank();
|
|
268
|
+
if (index >= lines.length)
|
|
269
|
+
break;
|
|
270
|
+
const providerToolName = lines[index].trim();
|
|
271
|
+
if (!/^[A-Za-z][\w-]*(?:\.[\w-]+)+$/.test(providerToolName))
|
|
272
|
+
return undefined;
|
|
273
|
+
index++;
|
|
274
|
+
if (lines[index]?.trim() !== '```json' && lines[index]?.trim() !== '```')
|
|
275
|
+
return undefined;
|
|
276
|
+
index++;
|
|
277
|
+
const body = [];
|
|
278
|
+
while (index < lines.length && lines[index].trim() !== '```')
|
|
279
|
+
body.push(lines[index++]);
|
|
280
|
+
if (index >= lines.length)
|
|
281
|
+
return undefined;
|
|
282
|
+
index++;
|
|
283
|
+
let parsed;
|
|
284
|
+
try {
|
|
285
|
+
parsed = JSON.parse(body.join('\n'));
|
|
248
286
|
}
|
|
287
|
+
catch {
|
|
288
|
+
return undefined;
|
|
289
|
+
}
|
|
290
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
291
|
+
return undefined;
|
|
292
|
+
const input = parsed;
|
|
293
|
+
calls.push({ providerToolName, name: toolNameAliases?.get(providerToolName) ?? providerToolName, input, providerToolArguments: JSON.stringify(input) });
|
|
249
294
|
}
|
|
250
|
-
|
|
251
|
-
|
|
295
|
+
return calls.length > 0 ? calls : undefined;
|
|
296
|
+
}
|
|
297
|
+
function toolCallsFrom(values, toolNameAliases) {
|
|
298
|
+
const calls = values.map((value) => jsonToolCall(value, toolNameAliases));
|
|
299
|
+
return calls.every((call) => call !== undefined) && calls.length > 0 ? calls : undefined;
|
|
300
|
+
}
|
|
301
|
+
function argumentObject(value) {
|
|
302
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
303
|
+
return undefined;
|
|
304
|
+
const record = value;
|
|
305
|
+
const named = typeof record.tool === 'string' && record.tool.length > 0 ? record.tool : typeof record.name === 'string' && record.name.length > 0 ? record.name : undefined;
|
|
306
|
+
const raw = record.arguments ?? record.input ?? record.parameters ?? record.args;
|
|
307
|
+
if (named === undefined || !raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
308
|
+
return undefined;
|
|
309
|
+
return raw;
|
|
252
310
|
}
|
|
253
311
|
function isEmptyJsonValue(value) {
|
|
254
312
|
if (Array.isArray(value))
|
|
@@ -263,7 +321,7 @@ function jsonToolCall(value, toolNameAliases) {
|
|
|
263
321
|
const providerToolName = named;
|
|
264
322
|
if (providerToolName === undefined)
|
|
265
323
|
return undefined;
|
|
266
|
-
const raw = record.arguments ?? record.input ?? record.parameters;
|
|
324
|
+
const raw = record.arguments ?? record.input ?? record.parameters ?? record.args;
|
|
267
325
|
const input = raw && typeof raw === 'object' && !Array.isArray(raw) ? { ...raw } : { ...record };
|
|
268
326
|
if (raw === undefined) {
|
|
269
327
|
delete input.tool;
|
|
@@ -47,6 +47,53 @@ class SlotPool {
|
|
|
47
47
|
this.slots.set(key, slot);
|
|
48
48
|
} ; return slot; }
|
|
49
49
|
}
|
|
50
|
+
function jsonCandidates(text) {
|
|
51
|
+
const found = [];
|
|
52
|
+
const push = (value) => {
|
|
53
|
+
try {
|
|
54
|
+
const parsed = JSON.parse(value);
|
|
55
|
+
found.push(parsed);
|
|
56
|
+
if (typeof parsed === 'string' && parsed !== value)
|
|
57
|
+
push(parsed);
|
|
58
|
+
}
|
|
59
|
+
catch { /* keep scanning */ }
|
|
60
|
+
};
|
|
61
|
+
const trimmed = text.trim();
|
|
62
|
+
if (trimmed)
|
|
63
|
+
push(trimmed);
|
|
64
|
+
for (let start = 0; start < text.length; start++) {
|
|
65
|
+
if (text[start] !== '{')
|
|
66
|
+
continue;
|
|
67
|
+
let depth = 0;
|
|
68
|
+
let inString = false;
|
|
69
|
+
let escaped = false;
|
|
70
|
+
for (let end = start; end < text.length; end++) {
|
|
71
|
+
const char = text[end];
|
|
72
|
+
if (inString) {
|
|
73
|
+
if (escaped)
|
|
74
|
+
escaped = false;
|
|
75
|
+
else if (char === '\\')
|
|
76
|
+
escaped = true;
|
|
77
|
+
else if (char === '"')
|
|
78
|
+
inString = false;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (char === '"')
|
|
82
|
+
inString = true;
|
|
83
|
+
else if (char === '{')
|
|
84
|
+
depth++;
|
|
85
|
+
else if (char === '}') {
|
|
86
|
+
depth--;
|
|
87
|
+
if (depth === 0) {
|
|
88
|
+
push(text.slice(start, end + 1));
|
|
89
|
+
start = end;
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return found;
|
|
96
|
+
}
|
|
50
97
|
function toJson(value, seen = new Set()) {
|
|
51
98
|
if (value === null || typeof value === 'string' || typeof value === 'boolean')
|
|
52
99
|
return value;
|
|
@@ -172,13 +219,15 @@ export function createModelEffectExecutor(config) {
|
|
|
172
219
|
throw new OutputValidationError('adapter', 'MODEL_REFUSAL', output.refusal ?? 'Provider refused the request.');
|
|
173
220
|
}
|
|
174
221
|
if (input.outputSchema !== undefined) {
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
222
|
+
const candidates = output.structured === undefined ? jsonCandidates(output.text) : [output.structured, ...jsonCandidates(output.text)];
|
|
223
|
+
const matched = candidates.find((candidate) => validateJsonSchema(candidate, input.outputSchema));
|
|
224
|
+
if (matched === undefined) {
|
|
225
|
+
lastSchemaViolation = toJson(candidates[0] ?? output.text);
|
|
178
226
|
failedForSchema = true;
|
|
179
227
|
recordFeedback('schema_rejected', 0);
|
|
180
228
|
throw new OutputValidationError('structured', 'OUTPUT_SCHEMA_VIOLATION', 'Provider output did not match the declared schema');
|
|
181
229
|
}
|
|
230
|
+
output.structured = matched;
|
|
182
231
|
}
|
|
183
232
|
failedForSchema = false;
|
|
184
233
|
recordFeedback('succeeded', 1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hunterzhu/pulse-adapters",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"license": "PolyForm-Noncommercial-1.0.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@anthropic-ai/sandbox-runtime": "0.0.77",
|
|
21
|
-
"@hunterzhu/pulse-runtime": "0.4.
|
|
22
|
-
"@hunterzhu/pulse-tool-sdk": "0.4.
|
|
21
|
+
"@hunterzhu/pulse-runtime": "0.4.4",
|
|
22
|
+
"@hunterzhu/pulse-tool-sdk": "0.4.4",
|
|
23
23
|
"exceljs": "4.4.0",
|
|
24
24
|
"pdfjs-dist": "4.10.38"
|
|
25
25
|
}
|