UNPKG

assistant-stream

Version:

Streaming utilities for AI assistants

463 lines (462 loc) • 13.5 kB
import { createJsonParserBackend } from "./json-parser-backend.js"; import { getPartialJsonObjectMeta, parsePartialJsonObject } from "./parse-partial-json-object.js"; // src/utils/json/incremental-json-object-parser.ts const COMPLETE_NUMBER = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/; const PARTIAL_NUMBER = /^-?$|^-?(?:0|[1-9]\d*)(?:\.\d*)?$|^-?(?:0|[1-9]\d*)(?:\.\d+)?[eE][+-]?\d*$/; const JSON_WHITESPACE = /^[\t\n\r ]$/; const HEX_DIGIT = /^[0-9a-fA-F]$/; const SINGLE_ESCAPES = { "\"": "\"", "\\": "\\", "/": "/", b: "\b", f: "\f", n: "\n", r: "\r", t: " " }; const isContainer = (value) => typeof value === "object" && value !== null; const setValue = (target, key, value) => { if (key === "__proto__" || key === "constructor" && isContainer(value) && Object.hasOwn(value, "prototype")) throw new Error("Unsafe argument key"); target[key] = value; }; const cloneContainer = (value) => { if (Array.isArray(value)) return [...value]; const copy = {}; for (const key of Object.keys(value)) setValue(copy, key, value[key]); return copy; }; const writeAtPath = (root, path, value, ownedContainers) => { const update = (container, depth) => { const target = ownedContainers.has(container) ? container : cloneContainer(container); ownedContainers.add(target); const key = path[depth]; if (depth === path.length - 1) { setValue(target, key, value); if (isContainer(value)) ownedContainers.add(value); return target; } const child = target[key]; if (!isContainer(child)) throw new Error("Invalid incremental JSON path"); setValue(target, key, update(child, depth + 1)); return target; }; return update(root, 0); }; const createArgsSnapshot = (root, state, partialPath) => { const result = parsePartialJsonObject(""); for (const key of Object.keys(root)) setValue(result, key, root[key]); const meta = getPartialJsonObjectMeta(result); meta.state = state; meta.partialPath = partialPath.map(String); return result; }; const materializeArgsSnapshot = (args) => { const pending = [args]; while (pending.length > 0) { const container = pending.pop(); for (const key of Object.keys(container)) { const value = container[key]; if (!isContainer(value)) continue; const copy = cloneContainer(value); setValue(container, key, copy); pending.push(copy); } } return args; }; var IncrementalJsonObjectParser = class IncrementalJsonObjectParser { backend; text; mode = "start"; root = {}; frames = []; token; args; publishedArgs; ownedContainers; constructor(fallback) { this.args = fallback; this.publishedArgs = fallback; } static from(text = "", fallback = parsePartialJsonObject("")) { return IncrementalJsonObjectParser.create(text, fallback, true); } static create(text, fallback, allowBackend) { const parser = new IncrementalJsonObjectParser(fallback); if (text.length !== 0) parser.text = { length: text.length, previous: void 0, value: text }; const complete = text.trimEnd().endsWith("}") ? parsePartialJsonObject(text) : void 0; if (complete && getPartialJsonObjectMeta(complete)?.state === "complete") { parser.args = complete; parser.publishedArgs = complete; parser.mode = "complete"; return parser; } if (allowBackend) try { parser.backend = createJsonParserBackend({ text, fallback, createSnapshot: (value, state, partialPath) => materializeArgsSnapshot(createArgsSnapshot(value, state, [...partialPath])) }); if (parser.backend) { parser.publishedArgs = parser.backend.currentArgs; return parser; } } catch { parser.backend = void 0; } parser.consumeDelta(text, true); parser.args = parser.snapshot(fallback); if (parser.args !== fallback) parser.publishedArgs = parser.mode === "fallback" ? parser.args : void 0; return parser; } get currentTextLength() { return this.text?.length ?? 0; } get currentArgs() { return this.publishedArgs ??= materializeArgsSnapshot(this.args); } append(delta) { if (delta.length === 0) return this; if (this.backend) { let backend; let args; try { backend = this.backend.append(delta); args = backend?.currentArgs; } catch { backend = void 0; } if (!backend) return IncrementalJsonObjectParser.create(this.accumulatedText() + delta, this.currentArgs, false); const parser = new IncrementalJsonObjectParser(this.args); parser.backend = backend; parser.publishedArgs = args; parser.text = { length: this.currentTextLength + delta.length, previous: this.text, value: delta }; return parser; } if (this.currentTextLength === 0) return IncrementalJsonObjectParser.create(delta, this.args, false); if (this.mode === "complete") { const parser = IncrementalJsonObjectParser.create(this.accumulatedText() + delta, this.args, false); if (parser.args === this.args) parser.publishedArgs = this.currentArgs; return parser; } const parser = this.clone(); const fallback = this.args; parser.consumeDelta(delta); parser.text = { length: (this.text?.length ?? 0) + delta.length, previous: this.text, value: delta }; parser.args = parser.snapshot(fallback); parser.publishedArgs = parser.args === fallback ? this.currentArgs : parser.mode === "fallback" ? parser.args : void 0; return parser; } clone() { const parser = new IncrementalJsonObjectParser(this.args); parser.text = this.text; parser.mode = this.mode; parser.root = this.root; parser.frames = this.frames.map((frame) => ({ ...frame, path: [...frame.path] })); parser.token = this.token ? { ...this.token, ..."path" in this.token && this.token.path ? { path: [...this.token.path] } : void 0 } : void 0; return parser; } snapshot(fallback) { if (this.mode === "fallback") return parsePartialJsonObject(this.accumulatedText()) ?? fallback; if (this.mode === "start") return fallback; return createArgsSnapshot(this.root, this.mode === "complete" ? "complete" : "partial", this.partialPath()); } accumulatedText() { const chunks = []; for (let chunk = this.text; chunk; chunk = chunk.previous) chunks.push(chunk.value); return chunks.reverse().join(""); } partialPath() { if (this.mode === "complete") return []; if (this.token && (this.token.kind === "number" || this.token.kind === "string" && this.token.role === "value")) return this.token.path; return this.frames.at(-1)?.path ?? []; } isFallback() { return this.mode === "fallback"; } consumeDelta(delta, ownsRoot = false) { if (this.isFallback()) return; this.ownedContainers = ownsRoot ? /* @__PURE__ */ new Set([this.root]) : /* @__PURE__ */ new Set(); for (const char of delta) { this.consumeCharacter(char); if (this.isFallback()) break; } if (!this.isFallback() && this.token?.kind === "string" && this.token.role === "value") this.writeValue(this.token.path, this.token.value); else if (!this.isFallback() && this.token?.kind === "number" && COMPLETE_NUMBER.test(this.token.value)) this.writeValue(this.token.path, Number(this.token.value)); this.ownedContainers = void 0; } consumeCharacter(char) { let reconsume = true; while (reconsume && this.mode !== "fallback") { reconsume = false; if (this.token) { reconsume = this.consumeToken(char); continue; } if (this.mode === "complete") { if (!JSON_WHITESPACE.test(char)) this.mode = "fallback"; continue; } if (this.mode === "start") { if (JSON_WHITESPACE.test(char)) continue; if (char !== "{") { this.mode = "fallback"; continue; } this.mode = "parsing"; this.frames.push({ kind: "object", path: [], state: "first-key-or-end" }); continue; } const frame = this.frames.at(-1); if (!frame) { this.mode = "fallback"; continue; } if (frame.kind === "object") this.consumeObjectCharacter(frame, char); else this.consumeArrayCharacter(frame, char); } } consumeObjectCharacter(frame, char) { if (JSON_WHITESPACE.test(char)) return; switch (frame.state) { case "first-key-or-end": if (char === "}") this.closeContainer(); else if (char === "\"") { frame.state = "key"; this.startKeyString(); } else this.mode = "fallback"; break; case "key": if (char === "\"") this.startKeyString(); else this.mode = "fallback"; break; case "colon": if (char === ":") frame.state = "value"; else this.mode = "fallback"; break; case "value": this.startValue(char, [...frame.path, frame.key]); break; case "comma-or-end": if (char === ",") { frame.state = "key"; frame.key = void 0; } else if (char === "}") this.closeContainer(); else this.mode = "fallback"; } } consumeArrayCharacter(frame, char) { if (JSON_WHITESPACE.test(char)) return; switch (frame.state) { case "first-value-or-end": if (char === "]") this.closeContainer(); else { frame.state = "value"; this.startValue(char, [...frame.path, frame.index]); } break; case "value": this.startValue(char, [...frame.path, frame.index]); break; case "comma-or-end": if (char === ",") { frame.index += 1; frame.state = "value"; } else if (char === "]") this.closeContainer(); else this.mode = "fallback"; } } startValue(char, path) { if (char === "{") { this.writeValue(path, {}); this.frames.push({ kind: "object", path, state: "first-key-or-end" }); } else if (char === "[") { this.writeValue(path, []); this.frames.push({ kind: "array", path, state: "first-value-or-end", index: 0 }); } else if (char === "\"") { this.writeValue(path, ""); this.startValueString(path); } else if (char === "t" || char === "f" || char === "n") { const value = char === "t" ? "true" : char === "f" ? "false" : "null"; this.writeValue(path, value === "true" ? true : value === "false" ? false : null); this.token = { kind: "literal", value, offset: 1 }; } else if (char === "-" || char >= "0" && char <= "9") { this.token = { kind: "number", path, value: char }; if (COMPLETE_NUMBER.test(char)) this.writeValue(path, Number(char)); } else this.mode = "fallback"; } startKeyString() { this.token = { kind: "string", role: "key", value: "", escape: "none", unicode: "" }; } startValueString(path) { this.token = { kind: "string", role: "value", path, value: "", escape: "none", unicode: "" }; } consumeToken(char) { const token = this.token; if (token.kind === "string") return this.consumeString(token, char); if (token.kind === "number") return this.consumeNumber(token, char); return this.consumeLiteral(token, char); } consumeString(token, char) { if (token.escape === "unicode") { if (!HEX_DIGIT.test(char)) { this.mode = "fallback"; return false; } token.unicode += char; if (token.unicode.length === 4) { token.value += String.fromCharCode(Number.parseInt(token.unicode, 16)); token.escape = "none"; token.unicode = ""; } return false; } if (token.escape === "single") { if (char === "u") { token.escape = "unicode"; return false; } const decoded = SINGLE_ESCAPES[char]; if (decoded === void 0) this.mode = "fallback"; else { token.value += decoded; token.escape = "none"; } return false; } if (char === "\\") { token.escape = "single"; return false; } if (char.charCodeAt(0) < 32) { this.mode = "fallback"; return false; } if (char !== "\"") { token.value += char; return false; } this.token = void 0; if (token.role === "key") { const frame = this.frames.at(-1); if (!frame || frame.kind !== "object") { this.mode = "fallback"; return false; } if (token.value === "__proto__" || token.value === "prototype" && frame.path.at(-1) === "constructor") { this.mode = "fallback"; return false; } frame.key = token.value; frame.state = "colon"; } else { this.writeValue(token.path, token.value); this.finishValue(); } return false; } consumeNumber(token, char) { if (char >= "0" && char <= "9" || char === "e" || char === "E" || char === "+" || char === "-" || char === ".") { const value = token.value + char; if (!PARTIAL_NUMBER.test(value)) { this.mode = "fallback"; return false; } token.value = value; if (COMPLETE_NUMBER.test(token.value)) this.writeValue(token.path, Number(token.value)); return false; } if (!COMPLETE_NUMBER.test(token.value)) { this.mode = "fallback"; return false; } this.token = void 0; this.finishValue(); return true; } consumeLiteral(token, char) { if (char !== token.value[token.offset]) { this.mode = "fallback"; return false; } token.offset += 1; if (token.offset === token.value.length) { this.token = void 0; this.finishValue(); } return false; } finishValue() { const frame = this.frames.at(-1); if (!frame || frame.state !== "value") { this.mode = "fallback"; return; } frame.state = "comma-or-end"; } closeContainer() { this.frames.pop(); if (this.frames.length === 0) this.mode = "complete"; else this.finishValue(); } writeValue(path, value) { try { this.root = writeAtPath(this.root, path, value, this.ownedContainers ?? /* @__PURE__ */ new Set()); } catch { this.mode = "fallback"; } } }; export { IncrementalJsonObjectParser }; //# sourceMappingURL=incremental-json-object-parser.js.map