UNPKG

playwright-ai-auto-debug

Version:

Automatic Playwright test debugging with AI assistance + UI Test Coverage Analysis

246 lines (213 loc) 7.68 kB
// src/infrastructure/ai/LocalAIProvider.js /** * Локальный AI провайдер для работы с LM Studio, Ollama и другими локальными серверами * Поддерживает потоковую передачу ответов */ export class LocalAIProvider { constructor() { this.providerName = 'Local AI'; } /** * Генерирует ответ от локального AI сервера с поддержкой стриминга * @param {string} prompt - промпт для анализа * @param {Object} config - конфигурация * @param {string} domSnapshot - DOM snapshot (опционально) * @returns {Promise<string>} - ответ от AI */ async generateResponse(prompt, config, domSnapshot = null) { const requestBody = { model: config.model || 'auto', messages: [ ...config.messages || [], { role: 'user', content: this.buildPromptContent(prompt, domSnapshot) } ], max_tokens: 2000, temperature: 0.1, stream: config.stream || false }; try { const response = await fetch(config.ai_server, { method: 'POST', headers: { 'Content-Type': 'application/json', ...(config.api_key && config.api_key !== '' ? { 'Authorization': `Bearer ${config.api_key}` } : {}) }, body: JSON.stringify(requestBody) }); if (!response.ok) { const errorText = await response.text().catch(() => ''); throw new Error(`Local AI server error: ${response.status} ${response.statusText} - ${errorText}`); } // Обработка потокового ответа if (config.stream) { return await this.handleStreamResponse(response); } else { return await this.handleRegularResponse(response); } } catch (error) { if (error.name === 'TypeError' && error.message.includes('fetch')) { throw new Error(`Network error: Unable to connect to local AI server at ${config.ai_server}. Make sure the server is running.`); } throw error; } } /** * Обрабатывает потоковый ответ от AI сервера * @param {Response} response - ответ от fetch * @returns {Promise<string>} - полный ответ */ async handleStreamResponse(response) { const reader = response.body.getReader(); const decoder = new TextDecoder(); let fullResponse = ''; console.log('📡 Receiving streaming response...'); try { while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value, { stream: true }); const lines = chunk.split('\n'); for (const line of lines) { if (line.startsWith('data: ')) { const data = line.slice(6).trim(); if (data === '[DONE]') { console.log('\n✅ Stream completed'); return fullResponse.trim(); } try { const parsed = JSON.parse(data); const content = parsed.choices?.[0]?.delta?.content || ''; if (content) { fullResponse += content; // Выводим получаемый текст в реальном времени process.stdout.write(content); } } catch (parseError) { // Игнорируем ошибки парсинга отдельных чанков continue; } } } } } finally { reader.releaseLock(); } if (fullResponse.trim().length === 0) { throw new Error('Empty response received from streaming API'); } console.log(`\n✅ Received ${fullResponse.length} characters`); return fullResponse.trim(); } /** * Обрабатывает обычный (не потоковый) ответ * @param {Response} response - ответ от fetch * @returns {Promise<string>} - ответ от AI */ async handleRegularResponse(response) { const data = await response.json(); if (!data.choices || data.choices.length === 0) { throw new Error('No response choices returned from local AI server'); } const content = data.choices[0].message?.content; if (!content) { throw new Error('Empty content in response from local AI server'); } return content.trim(); } /** * Строит содержимое промпта с учетом DOM snapshot * @param {string} prompt - основной промпт * @param {string} domSnapshot - DOM snapshot * @returns {string} - полный промпт */ buildPromptContent(prompt, domSnapshot) { let content = prompt; if (domSnapshot) { content += '\n\n## DOM Snapshot\n'; content += '```html\n'; content += domSnapshot; content += '\n```'; content += '\n\nИспользуй эту информацию о структуре DOM для более точного анализа селекторов и элементов страницы.'; } return content; } /** * Возвращает имя провайдера * @returns {string} */ getProviderName() { return this.providerName; } /** * Возвращает список поддерживаемых моделей * @returns {string[]} */ getSupportedModels() { // Возвращаем null чтобы указать что поддерживаем любые модели return null; } /** * Валидирует конфигурацию провайдера * @param {Object} config - конфигурация * @returns {Promise<{isValid: boolean, issues: string[]}>} */ async validateConfiguration(config) { const issues = []; if (!config.ai_server) { issues.push('AI server URL is required'); } else { // Проверяем что это локальный сервер try { const url = new URL(config.ai_server); if (!['localhost', '127.0.0.1'].includes(url.hostname) && !url.hostname.startsWith('192.168.') && !url.hostname.startsWith('10.') && !url.hostname.startsWith('172.')) { console.warn(`⚠️ Server ${url.hostname} doesn't appear to be local. Make sure it's accessible.`); } } catch (urlError) { issues.push('Invalid AI server URL format'); } } if (!config.model) { issues.push('Model is required'); } return { isValid: issues.length === 0, issues }; } /** * Проверяет доступность локального AI сервера * @param {Object} config - конфигурация * @returns {Promise<boolean>} */ async checkApiAvailability(config) { try { // Отправляем простой тестовый запрос const response = await fetch(config.ai_server, { method: 'POST', headers: { 'Content-Type': 'application/json', ...(config.api_key && config.api_key !== '' ? { 'Authorization': `Bearer ${config.api_key}` } : {}) }, body: JSON.stringify({ model: config.model || 'auto', messages: [{ role: 'user', content: 'test' }], max_tokens: 1, stream: false }) }); return response.ok; } catch { return false; } } }