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