llm-json-fix
Version:
Fix malformed JSON outputs from Large Language Models (LLMs)
170 lines (169 loc) • 5.56 kB
JavaScript
import { BufferLimitExceededError } from '../utils/errors';
/**
* A circular buffer for efficient streaming operations
*/
export class StreamBuffer {
/**
* Create a new StreamBuffer with a specified maximum size
*/
constructor(capacity) {
this.capacity = capacity;
this.position = 0;
this.size = 0;
this.buffer = ' '.repeat(capacity);
}
/**
* Reset the buffer to its initial state
*/
reset() {
this.position = 0;
this.size = 0;
this.buffer = ' '.repeat(this.capacity);
}
/**
* Get number of characters in the buffer
*/
getSize() {
return this.size;
}
/**
* Get the specified number of characters starting at the given index
*/
substr(start, length) {
if (start >= this.size) {
return '';
}
// Adjust length if it would exceed the buffer size
if (start + length > this.size) {
length = this.size - start;
}
// Calculate position in the circular buffer
// This calculation is needed for internal buffer management
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const physicalStart = (this.position - this.size + start) % this.capacity;
// Check if the substring wraps around the buffer
if (physicalStart + length <= this.capacity) {
// No wrap, return directly
return this.buffer.substring(physicalStart, physicalStart + length);
}
else {
// Substring wraps around the buffer
const firstPart = this.buffer.substring(physicalStart, this.capacity);
const secondPart = this.buffer.substring(0, (physicalStart + length) % this.capacity);
return firstPart + secondPart;
}
}
/**
* Get the last `length` characters of the buffer
*/
getLastChars(length) {
return this.substr(this.size - length, length);
}
/**
* Get the first `length` characters of the buffer
*/
getFirstChars(length) {
return this.substr(0, length);
}
/**
* Get a character at the specified position
*/
charAt(index) {
if (index < 0 || index >= this.size) {
return '';
}
const physicalPos = (this.position - this.size + index) % this.capacity;
return this.buffer[physicalPos];
}
/**
* Append a chunk of text to the buffer
*/
append(text) {
if (text.length > this.capacity) {
throw new BufferLimitExceededError(`Cannot append string of length ${text.length} to buffer with capacity ${this.capacity}`, this.size);
}
// Remove characters that would exceed capacity
const removeCount = Math.max(0, this.size + text.length - this.capacity);
if (removeCount > 0) {
this.size -= removeCount;
}
// Append each character
for (let i = 0; i < text.length; i++) {
this.buffer = this.buffer.substring(0, this.position) +
text[i] +
this.buffer.substring(this.position + 1);
this.position = (this.position + 1) % this.capacity;
if (this.size < this.capacity) {
this.size++;
}
}
}
/**
* Check if the buffer includes the given string
*/
includes(str) {
if (str.length > this.size) {
return false;
}
// Check if the string is present in the buffer
for (let i = 0; i <= this.size - str.length; i++) {
const substring = this.substr(i, str.length);
if (substring === str) {
return true;
}
}
return false;
}
/**
* Find the position of a string in the buffer
*/
indexOf(str, fromIndex = 0) {
if (str.length > this.size) {
return -1;
}
// Find the position of the string in the buffer
for (let i = fromIndex; i <= this.size - str.length; i++) {
const substring = this.substr(i, str.length);
if (substring === str) {
return i;
}
}
return -1;
}
/**
* Replace part of the buffer with a new string
*/
replace(start, length, replacement) {
if (start < 0 || start >= this.size) {
throw new Error(`Invalid start index: ${start}`);
}
if (length <= 0) {
throw new Error(`Invalid length: ${length}`);
}
// Calculate position in the circular buffer
// This calculation is needed for internal buffer management
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const physicalStart = (this.position - this.size + start) % this.capacity;
// Remove the specified portion and insert the replacement
let newBuffer = '';
// First part before the replacement
for (let i = 0; i < start; i++) {
newBuffer += this.charAt(i);
}
// Add the replacement
newBuffer += replacement;
// Last part after the replacement
for (let i = start + length; i < this.size; i++) {
newBuffer += this.charAt(i);
}
// Reset and fill the buffer with the new content
this.reset();
this.append(newBuffer);
}
/**
* Get the entire content of the buffer
*/
toString() {
return this.substr(0, this.size);
}
}