UNPKG

solver-sdk

Version:

SDK for WorkAI API - AI-powered code analysis with WorkCoins billing system

316 lines 12.1 kB
/** * Утилиты для обработки потоковых данных от API чата */ /** * Парсит строку SSE потока согласно официальному Anthropic API */ export function parseStreamLine(line) { // Пропускаем пустые строки и комментарии if (!line.trim() || line.startsWith(':')) { return {}; } // Обрабатываем только data: события if (!line.startsWith('data: ')) { return {}; } const data = line.slice(6).trim(); // Пропускаем пустые данные if (!data) { return {}; } try { const jsonData = JSON.parse(data); // ✅ Все события Anthropic API имеют поле type if (!jsonData.type) { return {}; } // Создаем chunk согласно официальному формату Anthropic const chunk = { type: jsonData.type, ...jsonData }; return { chunk }; } catch (error) { console.warn('Failed to parse SSE line:', line, error); return {}; } } /** * Проверяет завершен ли поток согласно Anthropic API */ export function isStreamComplete(chunk) { return chunk.type === 'message_stop'; } /** * Обрабатывает чанк потоковой передачи данных в формате Server-Sent Events * Работает только с официальными событиями Anthropic API * * @param line Строка из SSE потока * @param logger Логгер для отладки * @param options Опции обработки * @returns Результат обработки чанка */ export function processStreamChunk(line, logger, options) { try { // Игнорируем пустые строки и комментарии if (!line.trim() || line.startsWith(':')) { return {}; } // Проверяем формат SSE if (!line.startsWith('data: ')) { return {}; } const data = line.slice(6).trim(); // Пропускаем пустые данные if (!data) { return {}; } // Парсим JSON данные let jsonData; try { jsonData = JSON.parse(data); } catch (parseError) { if (logger) { logger.error('Ошибка парсинга JSON', { error: parseError, data: data.substring(0, 100) }); } return {}; } // Валидируем что это официальное событие Anthropic API + кастомные события сервера const validEvents = [ 'message_start', 'content_block_start', 'content_block_delta', 'content_block_stop', 'message_delta', 'message_stop', 'ping', 'error', 'pause_turn', // 🔍 НОВОЕ: Server-side tool execution pause (web_search, code_execution) 'tool_use_ready', // 🔧 Кастомное событие сервера для уведомления о готовности инструмента 'partial_content_complete', // 🆘 НОВОЕ: Accumulated partial content при max_tokens 'incomplete_tool_retry', // 🔄 НОВОЕ: Retry при incomplete tool из-за max_tokens 'heartbeat', // 💓 Heartbeat для длинных операций 'processing_started', // 🚀 Early streaming событие 'resume_started', // 🔄 Resume событие 'todo_update', // 📋 TODO progress tracking ]; if (!validEvents.includes(jsonData.type)) { if (logger) { logger.warn(`⚠️ Неизвестное событие: ${jsonData.type}`, jsonData); } return {}; } // Создаем ChatStreamChunk в соответствии с официальным API const chunk = { type: jsonData.type }; // Добавляем поля в зависимости от типа события switch (jsonData.type) { case 'message_start': if (jsonData.message) { chunk.message = jsonData.message; } break; case 'content_block_start': if (jsonData.index !== undefined) { chunk.index = jsonData.index; } if (jsonData.content_block) { chunk.content_block = jsonData.content_block; } break; case 'content_block_delta': if (jsonData.index !== undefined) { chunk.index = jsonData.index; } if (jsonData.delta) { chunk.delta = jsonData.delta; } break; case 'content_block_stop': if (jsonData.index !== undefined) { chunk.index = jsonData.index; } break; case 'message_delta': if (jsonData.delta) { chunk.message_delta = jsonData.delta; } break; case 'error': if (jsonData.error) { chunk.error = jsonData.error; } break; case 'message_stop': // 🎯 ИСПРАВЛЕНИЕ: Добавляем поддержку stop_reason в message_stop if (jsonData.stop_reason !== undefined) { chunk.message = { id: '', type: 'message', role: 'assistant', model: '', content: [], stop_reason: jsonData.stop_reason }; } break; case 'pause_turn': // 🔍 НОВОЕ: Server-side tool execution pause // Происходит при выполнении web_search или code_execution на стороне Anthropic chunk.message = { id: '', type: 'message', role: 'assistant', model: '', content: [], stop_reason: 'pause_turn' }; break; case 'tool_use_ready': // 🔧 Кастомное событие сервера - передаем как есть if (jsonData.tool_use) { chunk.tool_use = jsonData.tool_use; } break; case 'todo_update': // 📋 НОВОЕ: TODO Update событие if (jsonData.operation) { chunk.operation = jsonData.operation; } if (jsonData.todos) { chunk.todos = jsonData.todos; } if (jsonData.tool_use_id) { chunk.tool_use_id = jsonData.tool_use_id; } if (jsonData.progress) { chunk.progress = jsonData.progress; } break; // ping и error не требуют дополнительных полей } return { chunk }; } catch (error) { if (logger) { logger.error('Ошибка обработки чанка', { error, line: line.substring(0, 100) }); } return {}; } } /** * Обрабатывает ошибки потоковой передачи * @param error Ошибка для обработки * @returns Обработанная ошибка */ export function handleStreamError(error) { if (error instanceof Error) { return error; } if (typeof error === 'string') { return new Error(error); } if (error && typeof error === 'object') { const message = error.message || error.error || JSON.stringify(error); return new Error(message); } return new Error('Неизвестная ошибка потока'); } /** * 🛡️ Безопасный парсинг partial JSON с восстановлением * Исправляет типичные проблемы чанкинга: * - Незакрытые кавычки * - Незакрытые скобки * - Обрезанные строки */ export function safeParsePartialJson(json, logger) { // Сначала пробуем прямой парсинг try { return JSON.parse(json); } catch (e) { // Игнорируем - пробуем восстановить } // Попытка восстановления let fixed = json.trim(); // 1. Закрываем незавершенные строки const quoteCount = (fixed.match(/(?<!\\)"/g) || []).length; if (quoteCount % 2 !== 0) { // Нечетное количество кавычек - добавляем закрывающую fixed += '"'; if (logger) { logger.debug('🔧 [JSON_RECOVERY] Добавлена закрывающая кавычка'); } } // 2. Балансируем скобки const openBraces = (fixed.match(/\{/g) || []).length; const closeBraces = (fixed.match(/\}/g) || []).length; const openBrackets = (fixed.match(/\[/g) || []).length; const closeBrackets = (fixed.match(/\]/g) || []).length; // Добавляем недостающие закрывающие скобки for (let i = 0; i < openBraces - closeBraces; i++) { fixed += '}'; } for (let i = 0; i < openBrackets - closeBrackets; i++) { fixed += ']'; } if (openBraces !== closeBraces || openBrackets !== closeBrackets) { if (logger) { logger.debug(`🔧 [JSON_RECOVERY] Добавлены скобки: {} +${openBraces - closeBraces}, [] +${openBrackets - closeBrackets}`); } } // Пробуем парсить исправленный JSON try { const result = JSON.parse(fixed); if (logger) { logger.debug('✅ [JSON_RECOVERY] Успешно восстановлен JSON'); } return result; } catch (e) { if (logger) { logger.warn('❌ [JSON_RECOVERY] Не удалось восстановить JSON', { original: json.substring(0, 100), fixed: fixed.substring(0, 100), error: e instanceof Error ? e.message : String(e) }); } return null; } } /** * 🔄 Accumulator для безопасного накопления partial JSON chunks * Использует умный парсинг с восстановлением */ export class PartialJsonAccumulator { constructor(logger) { this.accumulated = ''; this.logger = logger; } /** * Добавляет новый чанк */ add(chunk) { this.accumulated += chunk; } /** * Пытается распарсить накопленный JSON * @returns Распарсенный объект или null если еще не готов */ tryParse() { if (!this.accumulated.trim()) { return null; } return safeParsePartialJson(this.accumulated, this.logger); } /** * Возвращает накопленную строку */ getAccumulated() { return this.accumulated; } /** * Сбрасывает накопленные данные */ reset() { this.accumulated = ''; } } //# sourceMappingURL=stream-utils.js.map