@browser-ai/web-llm 2.0.2 → 2.0.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.
- package/dist/index.js +354 -248
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +354 -248
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
package/dist/index.mjs
CHANGED
|
@@ -317,7 +317,28 @@ var UnsupportedFunctionalityError = class extends (_b14 = AISDKError, _a14 = sym
|
|
|
317
317
|
}
|
|
318
318
|
};
|
|
319
319
|
|
|
320
|
-
// src/tool-
|
|
320
|
+
// ../shared/src/utils/tool-utils.ts
|
|
321
|
+
function isFunctionTool(tool) {
|
|
322
|
+
return tool.type === "function";
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ../shared/src/utils/warnings.ts
|
|
326
|
+
function createUnsupportedSettingWarning(feature, details) {
|
|
327
|
+
return {
|
|
328
|
+
type: "unsupported",
|
|
329
|
+
feature,
|
|
330
|
+
details
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
function createUnsupportedToolWarning(tool, details) {
|
|
334
|
+
return {
|
|
335
|
+
type: "unsupported",
|
|
336
|
+
feature: `tool:${tool.name}`,
|
|
337
|
+
details
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ../shared/src/tool-calling/build-json-system-prompt.ts
|
|
321
342
|
function buildJsonToolSystemPrompt(originalSystemPrompt, tools, options) {
|
|
322
343
|
if (!tools || tools.length === 0) {
|
|
323
344
|
return originalSystemPrompt || "";
|
|
@@ -371,34 +392,112 @@ function getParameters(tool) {
|
|
|
371
392
|
return tool.inputSchema;
|
|
372
393
|
}
|
|
373
394
|
|
|
374
|
-
// src/tool-calling/
|
|
375
|
-
|
|
395
|
+
// ../shared/src/tool-calling/format-tool-results.ts
|
|
396
|
+
function buildResultPayload(result) {
|
|
397
|
+
const payload = {
|
|
398
|
+
name: result.toolName,
|
|
399
|
+
result: result.result ?? null,
|
|
400
|
+
error: Boolean(result.isError)
|
|
401
|
+
};
|
|
402
|
+
if (result.toolCallId) {
|
|
403
|
+
payload.id = result.toolCallId;
|
|
404
|
+
}
|
|
405
|
+
return payload;
|
|
406
|
+
}
|
|
407
|
+
function formatToolResults(results) {
|
|
408
|
+
if (!results || results.length === 0) {
|
|
409
|
+
return "";
|
|
410
|
+
}
|
|
411
|
+
const payloads = results.map(
|
|
412
|
+
(result) => JSON.stringify(buildResultPayload(result))
|
|
413
|
+
);
|
|
414
|
+
return `\`\`\`tool_result
|
|
415
|
+
${payloads.join("\n")}
|
|
416
|
+
\`\`\``;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// ../shared/src/tool-calling/parse-json-function-calls.ts
|
|
420
|
+
var DEFAULT_OPTIONS = {
|
|
421
|
+
supportXmlTags: true,
|
|
422
|
+
supportPythonStyle: true,
|
|
423
|
+
supportParametersField: true
|
|
424
|
+
};
|
|
376
425
|
function generateToolCallId() {
|
|
377
426
|
return `call_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
|
378
427
|
}
|
|
379
|
-
function
|
|
380
|
-
const
|
|
381
|
-
|
|
428
|
+
function buildRegex(options) {
|
|
429
|
+
const patterns = [];
|
|
430
|
+
patterns.push("```tool[_-]?call\\s*([\\s\\S]*?)```");
|
|
431
|
+
if (options.supportXmlTags) {
|
|
432
|
+
patterns.push("<tool_call>\\s*([\\s\\S]*?)\\s*</tool_call>");
|
|
433
|
+
}
|
|
434
|
+
if (options.supportPythonStyle) {
|
|
435
|
+
patterns.push("\\[(\\w+)\\(([^)]*)\\)\\]");
|
|
436
|
+
}
|
|
437
|
+
return new RegExp(patterns.join("|"), "gi");
|
|
438
|
+
}
|
|
439
|
+
function parseJsonFunctionCalls(response, options = DEFAULT_OPTIONS) {
|
|
440
|
+
const mergedOptions = { ...DEFAULT_OPTIONS, ...options };
|
|
441
|
+
const regex = buildRegex(mergedOptions);
|
|
442
|
+
const matches = Array.from(response.matchAll(regex));
|
|
443
|
+
regex.lastIndex = 0;
|
|
382
444
|
if (matches.length === 0) {
|
|
383
445
|
return { toolCalls: [], textContent: response };
|
|
384
446
|
}
|
|
385
447
|
const toolCalls = [];
|
|
386
448
|
let textContent = response;
|
|
387
449
|
for (const match of matches) {
|
|
388
|
-
const
|
|
389
|
-
textContent = textContent.replace(
|
|
450
|
+
const fullMatch = match[0];
|
|
451
|
+
textContent = textContent.replace(fullMatch, "");
|
|
390
452
|
try {
|
|
453
|
+
if (mergedOptions.supportPythonStyle && match[0].startsWith("[")) {
|
|
454
|
+
const pythonMatch = /\[(\w+)\(([^)]*)\)\]/.exec(match[0]);
|
|
455
|
+
if (pythonMatch) {
|
|
456
|
+
const [, funcName, pythonArgs] = pythonMatch;
|
|
457
|
+
const args = {};
|
|
458
|
+
if (pythonArgs && pythonArgs.trim()) {
|
|
459
|
+
const argPairs = pythonArgs.split(",").map((s) => s.trim());
|
|
460
|
+
for (const pair of argPairs) {
|
|
461
|
+
const equalIndex = pair.indexOf("=");
|
|
462
|
+
if (equalIndex > 0) {
|
|
463
|
+
const key = pair.substring(0, equalIndex).trim();
|
|
464
|
+
let value = pair.substring(equalIndex + 1).trim();
|
|
465
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
466
|
+
value = value.substring(1, value.length - 1);
|
|
467
|
+
}
|
|
468
|
+
args[key] = value;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
toolCalls.push({
|
|
473
|
+
type: "tool-call",
|
|
474
|
+
toolCallId: generateToolCallId(),
|
|
475
|
+
toolName: funcName,
|
|
476
|
+
args
|
|
477
|
+
});
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
const innerContent = match[1] || match[2] || "";
|
|
391
482
|
const trimmed = innerContent.trim();
|
|
483
|
+
if (!trimmed) continue;
|
|
392
484
|
try {
|
|
393
485
|
const parsed = JSON.parse(trimmed);
|
|
394
486
|
const callsArray = Array.isArray(parsed) ? parsed : [parsed];
|
|
395
487
|
for (const call of callsArray) {
|
|
396
488
|
if (!call.name) continue;
|
|
489
|
+
let args = call.arguments || (mergedOptions.supportParametersField ? call.parameters : null) || {};
|
|
490
|
+
if (typeof args === "string") {
|
|
491
|
+
try {
|
|
492
|
+
args = JSON.parse(args);
|
|
493
|
+
} catch {
|
|
494
|
+
}
|
|
495
|
+
}
|
|
397
496
|
toolCalls.push({
|
|
398
497
|
type: "tool-call",
|
|
399
498
|
toolCallId: call.id || generateToolCallId(),
|
|
400
499
|
toolName: call.name,
|
|
401
|
-
args
|
|
500
|
+
args
|
|
402
501
|
});
|
|
403
502
|
}
|
|
404
503
|
} catch {
|
|
@@ -407,11 +506,18 @@ function parseJsonFunctionCalls(response) {
|
|
|
407
506
|
try {
|
|
408
507
|
const call = JSON.parse(line.trim());
|
|
409
508
|
if (!call.name) continue;
|
|
509
|
+
let args = call.arguments || (mergedOptions.supportParametersField ? call.parameters : null) || {};
|
|
510
|
+
if (typeof args === "string") {
|
|
511
|
+
try {
|
|
512
|
+
args = JSON.parse(args);
|
|
513
|
+
} catch {
|
|
514
|
+
}
|
|
515
|
+
}
|
|
410
516
|
toolCalls.push({
|
|
411
517
|
type: "tool-call",
|
|
412
518
|
toolCallId: call.id || generateToolCallId(),
|
|
413
519
|
toolName: call.name,
|
|
414
|
-
args
|
|
520
|
+
args
|
|
415
521
|
});
|
|
416
522
|
} catch {
|
|
417
523
|
continue;
|
|
@@ -427,24 +533,245 @@ function parseJsonFunctionCalls(response) {
|
|
|
427
533
|
return { toolCalls, textContent: textContent.trim() };
|
|
428
534
|
}
|
|
429
535
|
|
|
430
|
-
// src/tool-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
536
|
+
// ../shared/src/streaming/tool-call-detector.ts
|
|
537
|
+
var DEFAULT_FENCE_PATTERNS = [
|
|
538
|
+
{ start: "```tool_call", end: "```", reconstructStart: "```tool_call\n" },
|
|
539
|
+
{ start: "```tool-call", end: "```", reconstructStart: "```tool-call\n" }
|
|
540
|
+
];
|
|
541
|
+
var EXTENDED_FENCE_PATTERNS = [
|
|
542
|
+
...DEFAULT_FENCE_PATTERNS,
|
|
543
|
+
{
|
|
544
|
+
start: "<tool_call>",
|
|
545
|
+
end: "</tool_call>",
|
|
546
|
+
reconstructStart: "<tool_call>"
|
|
547
|
+
}
|
|
548
|
+
];
|
|
549
|
+
var ToolCallFenceDetector = class {
|
|
550
|
+
constructor(options = {}) {
|
|
551
|
+
this.pythonStyleRegex = /\[(\w+)\(/g;
|
|
552
|
+
this.buffer = "";
|
|
553
|
+
this.inFence = false;
|
|
554
|
+
this.fenceStartBuffer = "";
|
|
555
|
+
// Accumulated fence content
|
|
556
|
+
this.currentFencePattern = null;
|
|
557
|
+
this.fencePatterns = options.patterns ?? EXTENDED_FENCE_PATTERNS;
|
|
558
|
+
this.enablePythonStyle = options.enablePythonStyle ?? true;
|
|
559
|
+
this.fenceStarts = this.fencePatterns.map((p) => p.start);
|
|
434
560
|
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
561
|
+
addChunk(chunk) {
|
|
562
|
+
this.buffer += chunk;
|
|
563
|
+
}
|
|
564
|
+
getBuffer() {
|
|
565
|
+
return this.buffer;
|
|
566
|
+
}
|
|
567
|
+
clearBuffer() {
|
|
568
|
+
this.buffer = "";
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Detects if there's a complete fence in the buffer
|
|
572
|
+
* @returns Detection result with fence info and safe text
|
|
573
|
+
*/
|
|
574
|
+
detectFence() {
|
|
575
|
+
const {
|
|
576
|
+
index: startIdx,
|
|
577
|
+
prefix: matchedPrefix,
|
|
578
|
+
pattern
|
|
579
|
+
} = this.findFenceStart(this.buffer);
|
|
580
|
+
if (startIdx === -1) {
|
|
581
|
+
const overlap = this.computeOverlapLength(this.buffer, this.fenceStarts);
|
|
582
|
+
const safeTextLength = this.buffer.length - overlap;
|
|
583
|
+
const prefixText2 = safeTextLength > 0 ? this.buffer.slice(0, safeTextLength) : "";
|
|
584
|
+
const remaining = overlap > 0 ? this.buffer.slice(-overlap) : "";
|
|
585
|
+
this.buffer = remaining;
|
|
586
|
+
return {
|
|
587
|
+
fence: null,
|
|
588
|
+
prefixText: prefixText2,
|
|
589
|
+
remainingText: "",
|
|
590
|
+
overlapLength: overlap
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
const prefixText = this.buffer.slice(0, startIdx);
|
|
594
|
+
this.buffer = this.buffer.slice(startIdx);
|
|
595
|
+
const prefixLength = matchedPrefix?.length ?? 0;
|
|
596
|
+
const fenceEnd = pattern?.end ?? "```";
|
|
597
|
+
const closingIdx = this.buffer.indexOf(fenceEnd, prefixLength);
|
|
598
|
+
if (closingIdx === -1) {
|
|
599
|
+
return {
|
|
600
|
+
fence: null,
|
|
601
|
+
prefixText,
|
|
602
|
+
remainingText: "",
|
|
603
|
+
overlapLength: 0
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
const endPos = closingIdx + fenceEnd.length;
|
|
607
|
+
const fence = this.buffer.slice(0, endPos);
|
|
608
|
+
const remainingText = this.buffer.slice(endPos);
|
|
609
|
+
this.buffer = "";
|
|
610
|
+
return {
|
|
611
|
+
fence,
|
|
612
|
+
prefixText,
|
|
613
|
+
remainingText,
|
|
614
|
+
overlapLength: 0
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Finds the first occurrence of any fence start marker
|
|
619
|
+
*
|
|
620
|
+
* @param text - Text to search in
|
|
621
|
+
* @returns Index of first fence start and which pattern matched
|
|
622
|
+
* @private
|
|
623
|
+
*/
|
|
624
|
+
findFenceStart(text) {
|
|
625
|
+
let bestIndex = -1;
|
|
626
|
+
let matchedPrefix = null;
|
|
627
|
+
let matchedPattern = null;
|
|
628
|
+
for (const pattern of this.fencePatterns) {
|
|
629
|
+
const idx = text.indexOf(pattern.start);
|
|
630
|
+
if (idx !== -1 && (bestIndex === -1 || idx < bestIndex)) {
|
|
631
|
+
bestIndex = idx;
|
|
632
|
+
matchedPrefix = pattern.start;
|
|
633
|
+
matchedPattern = pattern;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
if (this.enablePythonStyle) {
|
|
637
|
+
this.pythonStyleRegex.lastIndex = 0;
|
|
638
|
+
const pythonMatch = this.pythonStyleRegex.exec(text);
|
|
639
|
+
if (pythonMatch && (bestIndex === -1 || pythonMatch.index < bestIndex)) {
|
|
640
|
+
bestIndex = pythonMatch.index;
|
|
641
|
+
matchedPrefix = pythonMatch[0];
|
|
642
|
+
matchedPattern = {
|
|
643
|
+
start: pythonMatch[0],
|
|
644
|
+
end: ")]",
|
|
645
|
+
reconstructStart: pythonMatch[0],
|
|
646
|
+
isRegex: true
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
return { index: bestIndex, prefix: matchedPrefix, pattern: matchedPattern };
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Computes the maximum overlap between the end of text and the start of any prefix
|
|
654
|
+
* @param text - Text to check for overlap
|
|
655
|
+
* @param prefixes - List of prefixes to check against
|
|
656
|
+
* @returns Length of the maximum overlap found
|
|
657
|
+
*/
|
|
658
|
+
computeOverlapLength(text, prefixes) {
|
|
659
|
+
let overlap = 0;
|
|
660
|
+
for (const prefix of prefixes) {
|
|
661
|
+
const maxLength = Math.min(text.length, prefix.length - 1);
|
|
662
|
+
for (let size = maxLength; size > 0; size -= 1) {
|
|
663
|
+
if (prefix.startsWith(text.slice(-size))) {
|
|
664
|
+
overlap = Math.max(overlap, size);
|
|
665
|
+
break;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
return overlap;
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Checks if the buffer currently contains any text
|
|
673
|
+
*/
|
|
674
|
+
hasContent() {
|
|
675
|
+
return this.buffer.length > 0;
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Gets the buffer size
|
|
679
|
+
*/
|
|
680
|
+
getBufferSize() {
|
|
681
|
+
return this.buffer.length;
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Detect and stream fence content in real-time for true incremental streaming
|
|
685
|
+
* @returns Streaming result with current state and safe content to emit
|
|
686
|
+
*/
|
|
687
|
+
detectStreamingFence() {
|
|
688
|
+
if (!this.inFence) {
|
|
689
|
+
const {
|
|
690
|
+
index: startIdx,
|
|
691
|
+
prefix: matchedPrefix,
|
|
692
|
+
pattern
|
|
693
|
+
} = this.findFenceStart(this.buffer);
|
|
694
|
+
if (startIdx === -1) {
|
|
695
|
+
const overlap = this.computeOverlapLength(
|
|
696
|
+
this.buffer,
|
|
697
|
+
this.fenceStarts
|
|
698
|
+
);
|
|
699
|
+
const safeTextLength = this.buffer.length - overlap;
|
|
700
|
+
const safeContent = safeTextLength > 0 ? this.buffer.slice(0, safeTextLength) : "";
|
|
701
|
+
this.buffer = this.buffer.slice(safeTextLength);
|
|
702
|
+
return {
|
|
703
|
+
inFence: false,
|
|
704
|
+
safeContent,
|
|
705
|
+
completeFence: null,
|
|
706
|
+
textAfterFence: ""
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
const prefixText = this.buffer.slice(0, startIdx);
|
|
710
|
+
const fenceStartLength = matchedPrefix?.length ?? 0;
|
|
711
|
+
this.buffer = this.buffer.slice(startIdx + fenceStartLength);
|
|
712
|
+
if (pattern && pattern.start.startsWith("```") && this.buffer.startsWith("\n")) {
|
|
713
|
+
this.buffer = this.buffer.slice(1);
|
|
714
|
+
}
|
|
715
|
+
this.inFence = true;
|
|
716
|
+
this.fenceStartBuffer = "";
|
|
717
|
+
this.currentFencePattern = pattern;
|
|
718
|
+
return {
|
|
719
|
+
inFence: true,
|
|
720
|
+
safeContent: prefixText,
|
|
721
|
+
// Emit any text before the fence
|
|
722
|
+
completeFence: null,
|
|
723
|
+
textAfterFence: ""
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
const fenceEnd = this.currentFencePattern?.end ?? "```";
|
|
727
|
+
const closingIdx = this.buffer.indexOf(fenceEnd);
|
|
728
|
+
if (closingIdx === -1) {
|
|
729
|
+
const overlap = this.computeOverlapLength(this.buffer, [fenceEnd]);
|
|
730
|
+
const safeContentLength = this.buffer.length - overlap;
|
|
731
|
+
if (safeContentLength > 0) {
|
|
732
|
+
const safeContent = this.buffer.slice(0, safeContentLength);
|
|
733
|
+
this.fenceStartBuffer += safeContent;
|
|
734
|
+
this.buffer = this.buffer.slice(safeContentLength);
|
|
735
|
+
return {
|
|
736
|
+
inFence: true,
|
|
737
|
+
safeContent,
|
|
738
|
+
completeFence: null,
|
|
739
|
+
textAfterFence: ""
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
return {
|
|
743
|
+
inFence: true,
|
|
744
|
+
safeContent: "",
|
|
745
|
+
completeFence: null,
|
|
746
|
+
textAfterFence: ""
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
const fenceContent = this.buffer.slice(0, closingIdx);
|
|
750
|
+
this.fenceStartBuffer += fenceContent;
|
|
751
|
+
const reconstructStart = this.currentFencePattern?.reconstructStart ?? "```tool_call\n";
|
|
752
|
+
const completeFence = `${reconstructStart}${this.fenceStartBuffer}${fenceEnd}`;
|
|
753
|
+
const textAfterFence = this.buffer.slice(closingIdx + fenceEnd.length);
|
|
754
|
+
this.inFence = false;
|
|
755
|
+
this.fenceStartBuffer = "";
|
|
756
|
+
this.currentFencePattern = null;
|
|
757
|
+
this.buffer = textAfterFence;
|
|
758
|
+
return {
|
|
759
|
+
inFence: false,
|
|
760
|
+
safeContent: fenceContent,
|
|
761
|
+
// Emit the last bit of fence content
|
|
762
|
+
completeFence,
|
|
763
|
+
textAfterFence
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
isInFence() {
|
|
767
|
+
return this.inFence;
|
|
768
|
+
}
|
|
769
|
+
resetStreamingState() {
|
|
770
|
+
this.inFence = false;
|
|
771
|
+
this.fenceStartBuffer = "";
|
|
772
|
+
this.currentFencePattern = null;
|
|
773
|
+
}
|
|
774
|
+
};
|
|
448
775
|
|
|
449
776
|
// src/convert-to-webllm-messages.tsx
|
|
450
777
|
function convertToolResultOutput(output) {
|
|
@@ -591,27 +918,6 @@ import {
|
|
|
591
918
|
MLCEngine
|
|
592
919
|
} from "@mlc-ai/web-llm";
|
|
593
920
|
|
|
594
|
-
// src/utils/warnings.ts
|
|
595
|
-
function createUnsupportedSettingWarning(feature, details) {
|
|
596
|
-
return {
|
|
597
|
-
type: "unsupported",
|
|
598
|
-
feature,
|
|
599
|
-
details
|
|
600
|
-
};
|
|
601
|
-
}
|
|
602
|
-
function createUnsupportedToolWarning(tool, details) {
|
|
603
|
-
return {
|
|
604
|
-
type: "unsupported",
|
|
605
|
-
feature: `tool:${tool.name}`,
|
|
606
|
-
details
|
|
607
|
-
};
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
// src/utils/tool-utils.ts
|
|
611
|
-
function isFunctionTool(tool) {
|
|
612
|
-
return tool.type === "function";
|
|
613
|
-
}
|
|
614
|
-
|
|
615
921
|
// src/utils/prompt-utils.ts
|
|
616
922
|
function extractSystemPrompt(messages) {
|
|
617
923
|
const systemMessages = messages.filter((msg) => msg.role === "system");
|
|
@@ -651,206 +957,6 @@ ${existingContent}` : "")
|
|
|
651
957
|
];
|
|
652
958
|
}
|
|
653
959
|
|
|
654
|
-
// src/streaming/tool-call-detector.ts
|
|
655
|
-
var ToolCallFenceDetector = class {
|
|
656
|
-
constructor() {
|
|
657
|
-
this.FENCE_STARTS = ["```tool_call"];
|
|
658
|
-
this.FENCE_END = "```";
|
|
659
|
-
this.buffer = "";
|
|
660
|
-
this.inFence = false;
|
|
661
|
-
this.fenceStartBuffer = "";
|
|
662
|
-
}
|
|
663
|
-
addChunk(chunk) {
|
|
664
|
-
this.buffer += chunk;
|
|
665
|
-
}
|
|
666
|
-
getBuffer() {
|
|
667
|
-
return this.buffer;
|
|
668
|
-
}
|
|
669
|
-
clearBuffer() {
|
|
670
|
-
this.buffer = "";
|
|
671
|
-
}
|
|
672
|
-
/**
|
|
673
|
-
* Detects if there's a complete fence in the buffer
|
|
674
|
-
*
|
|
675
|
-
* 1. Searches for fence start markers
|
|
676
|
-
* 2. If found, looks for closing fence
|
|
677
|
-
* 3. Computes overlap for partial fences
|
|
678
|
-
* 4. Returns safe text that can be emitted
|
|
679
|
-
*
|
|
680
|
-
* @returns Detection result with fence info and safe text
|
|
681
|
-
*/
|
|
682
|
-
detectFence() {
|
|
683
|
-
const { index: startIdx, prefix: matchedPrefix } = this.findFenceStart(
|
|
684
|
-
this.buffer
|
|
685
|
-
);
|
|
686
|
-
if (startIdx === -1) {
|
|
687
|
-
const overlap = this.computeOverlapLength(this.buffer, this.FENCE_STARTS);
|
|
688
|
-
const safeTextLength = this.buffer.length - overlap;
|
|
689
|
-
const prefixText2 = safeTextLength > 0 ? this.buffer.slice(0, safeTextLength) : "";
|
|
690
|
-
const remaining = overlap > 0 ? this.buffer.slice(-overlap) : "";
|
|
691
|
-
this.buffer = remaining;
|
|
692
|
-
return {
|
|
693
|
-
fence: null,
|
|
694
|
-
prefixText: prefixText2,
|
|
695
|
-
remainingText: "",
|
|
696
|
-
overlapLength: overlap
|
|
697
|
-
};
|
|
698
|
-
}
|
|
699
|
-
const prefixText = this.buffer.slice(0, startIdx);
|
|
700
|
-
this.buffer = this.buffer.slice(startIdx);
|
|
701
|
-
const prefixLength = matchedPrefix?.length ?? 0;
|
|
702
|
-
const closingIdx = this.buffer.indexOf(this.FENCE_END, prefixLength);
|
|
703
|
-
if (closingIdx === -1) {
|
|
704
|
-
return {
|
|
705
|
-
fence: null,
|
|
706
|
-
prefixText,
|
|
707
|
-
remainingText: "",
|
|
708
|
-
overlapLength: 0
|
|
709
|
-
};
|
|
710
|
-
}
|
|
711
|
-
const endPos = closingIdx + this.FENCE_END.length;
|
|
712
|
-
const fence = this.buffer.slice(0, endPos);
|
|
713
|
-
const remainingText = this.buffer.slice(endPos);
|
|
714
|
-
this.buffer = "";
|
|
715
|
-
return {
|
|
716
|
-
fence,
|
|
717
|
-
prefixText,
|
|
718
|
-
remainingText,
|
|
719
|
-
overlapLength: 0
|
|
720
|
-
};
|
|
721
|
-
}
|
|
722
|
-
/**
|
|
723
|
-
* Finds the first occurrence of any fence start marker
|
|
724
|
-
*
|
|
725
|
-
* @param text - Text to search in
|
|
726
|
-
* @returns Index of first fence start and which prefix matched
|
|
727
|
-
* @private
|
|
728
|
-
*/
|
|
729
|
-
findFenceStart(text) {
|
|
730
|
-
let bestIndex = -1;
|
|
731
|
-
let matchedPrefix = null;
|
|
732
|
-
for (const prefix of this.FENCE_STARTS) {
|
|
733
|
-
const idx = text.indexOf(prefix);
|
|
734
|
-
if (idx !== -1 && (bestIndex === -1 || idx < bestIndex)) {
|
|
735
|
-
bestIndex = idx;
|
|
736
|
-
matchedPrefix = prefix;
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
return { index: bestIndex, prefix: matchedPrefix };
|
|
740
|
-
}
|
|
741
|
-
computeOverlapLength(text, prefixes) {
|
|
742
|
-
let overlap = 0;
|
|
743
|
-
for (const prefix of prefixes) {
|
|
744
|
-
const maxLength = Math.min(text.length, prefix.length - 1);
|
|
745
|
-
for (let size = maxLength; size > 0; size -= 1) {
|
|
746
|
-
if (prefix.startsWith(text.slice(-size))) {
|
|
747
|
-
overlap = Math.max(overlap, size);
|
|
748
|
-
break;
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
return overlap;
|
|
753
|
-
}
|
|
754
|
-
hasContent() {
|
|
755
|
-
return this.buffer.length > 0;
|
|
756
|
-
}
|
|
757
|
-
getBufferSize() {
|
|
758
|
-
return this.buffer.length;
|
|
759
|
-
}
|
|
760
|
-
/**
|
|
761
|
-
* Detect and stream fence content in real-time for true incremental streaming
|
|
762
|
-
*
|
|
763
|
-
* This method is designed for streaming tool calls as they arrive:
|
|
764
|
-
* 1. Detects when a fence starts and transitions to "inFence" state
|
|
765
|
-
* 2. While inFence, emits safe content that won't conflict with fence end marker
|
|
766
|
-
* 3. When fence ends, returns the complete fence for parsing
|
|
767
|
-
*
|
|
768
|
-
* @returns Streaming result with current state and safe content to emit
|
|
769
|
-
*/
|
|
770
|
-
detectStreamingFence() {
|
|
771
|
-
if (!this.inFence) {
|
|
772
|
-
const { index: startIdx, prefix: matchedPrefix } = this.findFenceStart(
|
|
773
|
-
this.buffer
|
|
774
|
-
);
|
|
775
|
-
if (startIdx === -1) {
|
|
776
|
-
const overlap = this.computeOverlapLength(
|
|
777
|
-
this.buffer,
|
|
778
|
-
this.FENCE_STARTS
|
|
779
|
-
);
|
|
780
|
-
const safeTextLength = this.buffer.length - overlap;
|
|
781
|
-
const safeContent = safeTextLength > 0 ? this.buffer.slice(0, safeTextLength) : "";
|
|
782
|
-
this.buffer = this.buffer.slice(safeTextLength);
|
|
783
|
-
return {
|
|
784
|
-
inFence: false,
|
|
785
|
-
safeContent,
|
|
786
|
-
completeFence: null,
|
|
787
|
-
textAfterFence: ""
|
|
788
|
-
};
|
|
789
|
-
}
|
|
790
|
-
const prefixText = this.buffer.slice(0, startIdx);
|
|
791
|
-
const fenceStartLength = matchedPrefix?.length ?? 0;
|
|
792
|
-
this.buffer = this.buffer.slice(startIdx + fenceStartLength);
|
|
793
|
-
if (this.buffer.startsWith("\n")) {
|
|
794
|
-
this.buffer = this.buffer.slice(1);
|
|
795
|
-
}
|
|
796
|
-
this.inFence = true;
|
|
797
|
-
this.fenceStartBuffer = "";
|
|
798
|
-
return {
|
|
799
|
-
inFence: true,
|
|
800
|
-
safeContent: prefixText,
|
|
801
|
-
completeFence: null,
|
|
802
|
-
textAfterFence: ""
|
|
803
|
-
};
|
|
804
|
-
}
|
|
805
|
-
const closingIdx = this.buffer.indexOf(this.FENCE_END);
|
|
806
|
-
if (closingIdx === -1) {
|
|
807
|
-
const overlap = this.computeOverlapLength(this.buffer, [this.FENCE_END]);
|
|
808
|
-
const safeContentLength = this.buffer.length - overlap;
|
|
809
|
-
if (safeContentLength > 0) {
|
|
810
|
-
const safeContent = this.buffer.slice(0, safeContentLength);
|
|
811
|
-
this.fenceStartBuffer += safeContent;
|
|
812
|
-
this.buffer = this.buffer.slice(safeContentLength);
|
|
813
|
-
return {
|
|
814
|
-
inFence: true,
|
|
815
|
-
safeContent,
|
|
816
|
-
completeFence: null,
|
|
817
|
-
textAfterFence: ""
|
|
818
|
-
};
|
|
819
|
-
}
|
|
820
|
-
return {
|
|
821
|
-
inFence: true,
|
|
822
|
-
safeContent: "",
|
|
823
|
-
completeFence: null,
|
|
824
|
-
textAfterFence: ""
|
|
825
|
-
};
|
|
826
|
-
}
|
|
827
|
-
const fenceContent = this.buffer.slice(0, closingIdx);
|
|
828
|
-
this.fenceStartBuffer += fenceContent;
|
|
829
|
-
const completeFence = `${this.FENCE_STARTS[0]}
|
|
830
|
-
${this.fenceStartBuffer}
|
|
831
|
-
${this.FENCE_END}`;
|
|
832
|
-
const textAfterFence = this.buffer.slice(
|
|
833
|
-
closingIdx + this.FENCE_END.length
|
|
834
|
-
);
|
|
835
|
-
this.inFence = false;
|
|
836
|
-
this.fenceStartBuffer = "";
|
|
837
|
-
this.buffer = textAfterFence;
|
|
838
|
-
return {
|
|
839
|
-
inFence: false,
|
|
840
|
-
safeContent: fenceContent,
|
|
841
|
-
completeFence,
|
|
842
|
-
textAfterFence
|
|
843
|
-
};
|
|
844
|
-
}
|
|
845
|
-
isInFence() {
|
|
846
|
-
return this.inFence;
|
|
847
|
-
}
|
|
848
|
-
resetStreamingState() {
|
|
849
|
-
this.inFence = false;
|
|
850
|
-
this.fenceStartBuffer = "";
|
|
851
|
-
}
|
|
852
|
-
};
|
|
853
|
-
|
|
854
960
|
// src/web-llm-language-model.ts
|
|
855
961
|
function isMobile() {
|
|
856
962
|
if (typeof navigator === "undefined") return false;
|