UNPKG

solver-sdk

Version:

SDK для интеграции с Code Solver Backend API

348 lines 17.8 kB
import { generateId, setupSocketEventHandlers } from './websocket-helpers'; import { handleStreamError, processStreamChunk } from './stream-utils'; // Экспортируем все типы и интерфейсы для внешнего использования export * from './models'; export * from './interfaces'; /** * API для работы с чатом */ export class ChatApi { /** * Создает новый экземпляр API для работы с чатом * @param {IHttpClient} httpClient HTTP клиент */ constructor(httpClient) { this.httpClient = httpClient; } /** * Отправляет сообщение в чат и получает ответ от модели * @param {ChatMessage[]} messages Массив сообщений для отправки * @param {ChatOptions} [options] Дополнительные параметры * @returns {Promise<ChatResponse>} Ответ модели */ async chat(messages, options) { if (!messages || messages.length === 0) { throw new Error('Необходимо предоставить хотя бы одно сообщение'); } // Проверяем наличие хотя бы одного сообщения от пользователя const hasUserMessage = messages.some(msg => msg.role === 'user'); if (!hasUserMessage) { throw new Error('В сообщениях должно быть хотя бы одно сообщение с ролью "user"'); } // Отправляем запрос к API чата return this.httpClient.post('/api/v1/chat', { model: options?.model || 'Claude', messages, temperature: options?.temperature, maxTokens: options?.maxTokens, stopSequences: options?.stopSequences, functions: options?.functions, functionCall: options?.functionCall, thinking: options?.thinking, region: options?.region }); } /** * Алиас для метода chat для совместимости с другими SDK * @param {ChatMessage[]} messages Массив сообщений для отправки * @param {ChatOptions} [options] Дополнительные параметры * @returns {Promise<ChatResponse>} Ответ модели */ async chatCompletion(messages, options) { return this.chat(messages, options); } /** * Проверяет доступность API чата * @returns {Promise<boolean>} Результат проверки */ async checkAvailability() { try { // Используем HEAD запрос вместо GET, так как эндпоинт поддерживает только HEAD/POST await this.httpClient.request({ method: 'HEAD', url: '/api/v1/chat' }); return true; } catch (error) { return false; } } /** * Отправляет сообщение в чат и получает ответ от модели * с автоматическим переключением между регионами при ошибках перегрузки * @param {ChatMessage[]} messages Массив сообщений для отправки * @param {ChatOptions} [options] Дополнительные параметры * @returns {Promise<ChatResponse>} Ответ модели */ async chatWithRegionFailover(messages, options) { // Список всех доступных регионов const allRegions = ['us-east-1', 'eu-west-1', 'ap-southeast-2']; // Начинаем с региона из параметров, или первого в списке let startRegionIndex = 0; if (options?.region) { const regionIndex = allRegions.indexOf(options.region); if (regionIndex !== -1) { startRegionIndex = regionIndex; } } // Реорганизуем массив, чтобы начать с указанного региона const regions = [ ...allRegions.slice(startRegionIndex), ...allRegions.slice(0, startRegionIndex) ]; // Последняя ошибка, будет возвращена если все регионы недоступны let lastError = null; // Пробуем каждый регион по очереди for (let i = 0; i < regions.length; i++) { const region = regions[i]; try { console.log(`Попытка запроса к Anthropic API в регионе ${region}`); // Копируем опции и устанавливаем текущий регион const regionOptions = { ...options, region }; // Отправляем запрос с конкретным регионом return await this.chat(messages, regionOptions); } catch (error) { lastError = error; // Проверяем, является ли ошибка ошибкой перегрузки (код 529) const isOverloadError = error.status === 529 || error.code === 529 || (error.response?.status === 529) || (error.message && error.message.includes('overloaded')) || (error.error?.type === 'overloaded_error'); if (isOverloadError) { console.warn(`Регион ${region} перегружен, пробуем следующий регион`); // Продолжаем цикл и пробуем следующий регион continue; } else { // Если ошибка не связана с перегрузкой, прекращаем попытки console.error(`Ошибка в регионе ${region}, не связанная с перегрузкой:`, error); throw error; } } } // Если мы здесь, значит все регионы перегружены throw lastError || new Error('Все регионы Anthropic API перегружены, попробуйте позже'); } /** * Отправляет одиночный запрос к модели с автоматическим переключением регионов * @param {string} prompt Запрос к модели * @param {ChatOptions} [options] Дополнительные параметры * @returns {Promise<string>} Текстовый ответ модели */ async sendPromptWithRegionFailover(prompt, options) { const messages = [ { role: 'user', content: prompt } ]; const response = await this.chatWithRegionFailover(messages, options); if (response.choices && response.choices.length > 0) { return response.choices[0].message.content; } throw new Error('Модель не вернула ответ'); } /** * Отправляет сообщение в чат в потоковом режиме * @param {ChatMessage[]} messages Массив сообщений для отправки * @param {ChatStreamOptions} [options] Дополнительные параметры * @returns {AsyncGenerator<ChatStreamChunk>} Асинхронный генератор чанков ответа */ async *streamChat(messages, options) { if (!messages || messages.length === 0) { throw new Error('Необходимо предоставить хотя бы одно сообщение'); } // Проверяем наличие хотя бы одного сообщения от пользователя const hasUserMessage = messages.some(msg => msg.role === 'user'); if (!hasUserMessage) { throw new Error('В сообщениях должно быть хотя бы одно сообщение с ролью "user"'); } try { // Создаем параметры запроса const params = { model: options?.model || 'Claude', messages, temperature: options?.temperature, maxTokens: options?.maxTokens, stopSequences: options?.stopSequences, functions: options?.functions, functionCall: options?.functionCall, thinking: options?.thinking, stream: true }; // Получаем поток данных от API const response = await this.httpClient.request({ url: '/api/v1/chat/stream', method: 'POST', data: params, headers: { 'Accept': 'text/event-stream', 'Cache-Control': 'no-cache' } }); if (!response || !response.body) { throw new Error('Сервер не вернул поток данных'); } // Обрабатываем поток событий const reader = response.body.getReader(); const decoder = new TextDecoder(); let isInThinkingBlock = false; let thinkingContent = ''; let textContent = ''; let thinkingSignature = ''; while (true) { const { done, value } = await reader.read(); if (done) { break; } const chunk = decoder.decode(value); const lines = chunk.split('\n').filter(line => line.trim() !== ''); for (const line of lines) { if (!line.startsWith('data:')) continue; const result = processStreamChunk(line, isInThinkingBlock, thinkingContent, textContent, thinkingSignature, options?.onToken); isInThinkingBlock = result.isInThinkingBlock; thinkingContent = result.thinkingContent; textContent = result.textContent; thinkingSignature = result.thinkingSignature; if (result.chunk) { yield result.chunk; } if (result.isDone) { // Поток завершен if (options?.onComplete) { options.onComplete(textContent); } return; } } } // Финальный чанк, если поток завершился без [DONE] if (options?.onComplete) { options.onComplete(textContent); } yield { text: '', isComplete: true, thinkingContent: thinkingContent.length > 0 ? thinkingContent : undefined, thinkingSignature: thinkingSignature.length > 0 ? thinkingSignature : undefined }; } catch (error) { const errorObj = handleStreamError(error); if (options?.onError) { options.onError(errorObj); } throw errorObj; } } /** * Отправляет запрос к модели в потоковом режиме (упрощенный интерфейс) * @param {string} prompt Запрос к модели * @param {ChatStreamOptions} [options] Дополнительные параметры * @returns {AsyncGenerator<ChatStreamChunk>} Асинхронный генератор чанков ответа */ async *streamPrompt(prompt, options) { const messages = [ { role: 'user', content: prompt } ]; yield* this.streamChat(messages, options); } /** * Создает новое WebSocket соединение для потокового чата * @returns {Promise<WebSocketConnectResponse>} Информация о созданном соединении */ async connectWebSocket() { return this.httpClient.post('/api/v1/chat/connect'); } /** * Отправляет сообщение в чат в потоковом режиме с поддержкой thinking через WebSocket * @param {ChatMessage[]} messages Массив сообщений для отправки * @param {ChatStreamOptions} [options] Дополнительные параметры * @param {EventHandler} [onEvent] Обработчик событий WebSocket * @returns {Promise<ThinkingStreamResponse>} Информация о потоковом запросе */ async streamChatWithThinking(messages, options = {}, onEvent) { if (!messages || messages.length === 0) { throw new Error('Необходимо предоставить хотя бы одно сообщение'); } try { // 1. Создаем параметры для запроса const socketId = `socket_${Date.now()}_${generateId(10)}`; // Создаем sessionId для отслеживания контекста между запросами const sessionId = options.sessionId || `thinking-${Date.now()}`; let socket = null; // 2. Подключаемся к WebSocket и ждем успешного подключения if (onEvent) { await new Promise((resolve, reject) => { try { const socketOptions = { transports: ['websocket'], reconnection: true, reconnectionAttempts: 3, reconnectionDelay: 1000, timeout: 10000, query: { socketId: socketId, token: options.authToken || 'test-token' } }; // Динамически импортируем socket.io-client import('socket.io-client').then((socketIoModule) => { const io = socketIoModule.default || socketIoModule.io; if (!io) { reject(new Error('Не удалось импортировать socket.io-client')); return; } // Создаем соединение с сервером WebSocket const serverUrl = this.httpClient.getBaseURL?.() || 'http://localhost:3000'; socket = io(`${serverUrl}/reasoning`, socketOptions); // Устанавливаем обработчики событий setupSocketEventHandlers(socket, socketId, sessionId, onEvent); // Разрешаем промис, продолжаем выполнение resolve(); // Обработчик таймаута соединения setTimeout(() => { if (socket && !socket.connected) { reject(new Error('WebSocket connection timeout')); } }, 10000); }).catch(error => { const errorMessage = error instanceof Error ? error.message : String(error); reject(new Error(`Ошибка импорта socket.io-client: ${errorMessage}`)); }); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); reject(new Error(`Ошибка WebSocket: ${errorMessage}`)); } }); } // 3. Отправляем HTTP запрос для инициации потока return this.httpClient.post('/api/v1/chat/stream-chat', { model: options.model || 'claude-3-7-sonnet-20240229', messages, temperature: options.temperature, maxTokens: options.maxTokens, socketId: socketId, sessionId: sessionId, // Добавляем sessionId для кэширования блоков мышления thinking: true, region: options.region }, { 'X-Socket-ID': socketId, 'X-Session-ID': sessionId // Добавляем ID сессии в заголовки }); } catch (error) { const errorObj = handleStreamError(error); if (options.onError) { options.onError(errorObj); } throw errorObj; } } } //# sourceMappingURL=index.js.map