UNPKG

@juspay/neurolink

Version:

The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002

299 lines (298 loc) • 9.16 kB
/** * JSON Extraction Utilities * * Utilities for extracting JSON from mixed text content. * Particularly useful for parsing AI responses that contain JSON within prose. */ import { parseJsonOrNull } from "./safeParse.js"; /** * Find the first balanced JSON object/array span starting at or after * `fromIndex`. Quote- and escape-aware: braces inside string literals do not * affect depth. Returns the matched substring and the index just past it, or * null if no balanced span exists. */ export function nextBalancedJsonSpan(text, fromIndex = 0) { for (let start = fromIndex; start < text.length; start++) { const openChar = text[start]; if (openChar !== "{" && openChar !== "[") { continue; } const closeChar = openChar === "{" ? "}" : "]"; let depth = 0; let inString = false; let escapeNext = false; for (let i = start; i < text.length; i++) { const ch = text[i]; if (escapeNext) { escapeNext = false; continue; } if (ch === "\\") { escapeNext = true; continue; } if (ch === '"') { inString = !inString; continue; } if (inString) { continue; } if (ch === openChar) { depth++; } else if (ch === closeChar) { depth--; if (depth === 0) { return { span: text.substring(start, i + 1), end: i + 1 }; } } } // Unbalanced from this start — try the next opening char. } return null; } /** * Extract JSON string from text that may contain surrounding content. * * Searches for valid JSON in the following order: * 1. Direct parse of the entire text * 2. JSON within markdown code blocks (```json ... ``` or ``` ... ```) * 3. JSON object pattern ({ ... }) * 4. JSON array pattern ([ ... ]) * * @param text - Text that may contain JSON * @returns Extracted JSON string or null if none found * * @example * ```typescript * const response = "Here's the data: {\"name\": \"test\"} Let me know if you need more."; * const json = extractJsonStringFromText(response); * // Returns: '{"name": "test"}' * ``` */ export function extractJsonStringFromText(text) { // Try direct parse first - maybe the whole text is valid JSON try { JSON.parse(text); return text; } catch { // Continue to extraction patterns } // Try to find JSON in code blocks (```json ... ``` or ``` ... ```) const codeBlockMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/); if (codeBlockMatch?.[1]) { const potentialJson = codeBlockMatch[1].trim(); try { JSON.parse(potentialJson); return potentialJson; } catch { // Continue to other patterns } } // Scan for balanced JSON object/array spans (quote/escape aware) and return // the first one that parses. Unlike a non-greedy regex, this never stops at a // "}" that lives inside a string value, so nested objects are preserved. let searchFrom = 0; for (;;) { const found = nextBalancedJsonSpan(text, searchFrom); if (!found) { break; } try { JSON.parse(found.span); return found.span; } catch { // Not valid JSON — resume scanning just past this opening character so a // valid inner object/array can still be found. } searchFrom = found.end - found.span.length + 1; } return null; } /** * Extract and parse JSON from mixed text content. * * Useful for parsing AI responses that contain JSON within prose. * Combines extraction and parsing in one step. * * @param text - Text that may contain JSON * @returns Parsed JSON value or null if not found/invalid * * @example * ```typescript * const response = ` * Here is your configuration: * \`\`\`json * {"theme": "dark", "fontSize": 14} * \`\`\` * Let me know if you need changes. * `; * const config = extractJsonFromText(response); * // Returns: { theme: "dark", fontSize: 14 } * ``` */ export function extractJsonFromText(text) { const jsonString = extractJsonStringFromText(text); if (!jsonString) { return null; } return parseJsonOrNull(jsonString); } /** * Parse JSON from text with optional type validation. * * Extracts JSON from text and optionally validates it against a type guard. * Useful when you need type-safe parsing of AI responses. * * @param text - Text that may contain JSON * @param validator - Optional type guard to validate the parsed result * @returns Parsed and validated JSON or null if not found/invalid/fails validation * * @example * ```typescript * interface UserConfig { * theme: string; * fontSize: number; * } * * function isUserConfig(obj: unknown): obj is UserConfig { * return ( * typeof obj === 'object' && * obj !== null && * 'theme' in obj && * 'fontSize' in obj && * typeof (obj as UserConfig).theme === 'string' && * typeof (obj as UserConfig).fontSize === 'number' * ); * } * * const config = parseJsonFromText<UserConfig>(aiResponse, isUserConfig); * if (config) { * // config is typed as UserConfig * console.log(config.theme, config.fontSize); * } * ``` */ export function parseJsonFromText(text, validator) { const parsed = extractJsonFromText(text); if (parsed === null) { return null; } if (validator && !validator(parsed)) { return null; } return parsed; } /** * Extract all JSON objects/arrays from text. * * Useful when text contains multiple JSON blocks. * * @param text - Text that may contain multiple JSON values * @returns Array of parsed JSON values * * @example * ```typescript * const text = 'First: {"a": 1} Second: {"b": 2}'; * const results = extractAllJsonFromText(text); * // Returns: [{ a: 1 }, { b: 2 }] * ``` */ export function extractAllJsonFromText(text) { const results = []; // Extract from code blocks first const codeBlockRegex = /```(?:json)?\s*([\s\S]*?)```/g; let match = codeBlockRegex.exec(text); while (match !== null) { if (match[1]) { const potentialJson = match[1].trim(); try { results.push(JSON.parse(potentialJson)); } catch { // Not valid JSON, skip } } match = codeBlockRegex.exec(text); } // If we found code blocks, return those results if (results.length > 0) { return results; } // Otherwise, try to find all JSON objects and arrays // This is a simplified approach - it finds top-level JSON structures const remaining = text; let searchStart = 0; while (searchStart < remaining.length) { // Find next potential JSON start const objectStart = remaining.indexOf("{", searchStart); const arrayStart = remaining.indexOf("[", searchStart); let start; let isObject; if (objectStart === -1 && arrayStart === -1) { break; } else if (objectStart === -1) { start = arrayStart; isObject = false; } else if (arrayStart === -1) { start = objectStart; isObject = true; } else { start = Math.min(objectStart, arrayStart); isObject = objectStart < arrayStart; } // Try to find matching end const openChar = isObject ? "{" : "["; const closeChar = isObject ? "}" : "]"; let depth = 0; let inString = false; let escapeNext = false; for (let i = start; i < remaining.length; i++) { const char = remaining[i]; if (escapeNext) { escapeNext = false; continue; } if (char === "\\") { escapeNext = true; continue; } if (char === '"') { inString = !inString; continue; } if (inString) { continue; } if (char === openChar) { depth++; } else if (char === closeChar) { depth--; if (depth === 0) { const potentialJson = remaining.substring(start, i + 1); try { results.push(JSON.parse(potentialJson)); searchStart = i + 1; } catch { searchStart = start + 1; } break; } } } if (depth !== 0) { // Unbalanced brackets, move past this start searchStart = start + 1; } } return results; }