UNPKG

playwright-ai-auto-debug

Version:

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

232 lines (197 loc) 7.51 kB
// src/infrastructure/ai/MistralProvider.js /** * Mistral AI провайдер для отправки запросов к Mistral API */ export class MistralProvider { constructor() { this.providerName = 'Mistral'; } /** * Генерирует ответ от Mistral API * @param {string} prompt - промпт для анализа * @param {Object} config - конфигурация * @param {string} domSnapshot - DOM snapshot (опционально) * @returns {Promise<string>} - ответ от AI */ async generateResponse(prompt, config, domSnapshot = null) { if (!config.api_key) { throw new Error('Mistral API key is required. Set API_KEY environment variable or configure api_key in ai.conf.js'); } const requestBody = { model: config.model || 'mistral-medium', 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', 'Authorization': `Bearer ${config.api_key}` }, body: JSON.stringify(requestBody) }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(`Mistral API error: ${response.status} ${response.statusText} - ${errorData.message || 'Unknown error'}`); } // Обработка потокового ответа if (config.stream) { return await this.handleStreamResponse(response); } // Обработка обычного ответа const data = await response.json(); if (!data.choices || data.choices.length === 0) { throw new Error('No response choices returned from Mistral API'); } return data.choices[0].message.content.trim(); } catch (error) { if (error.name === 'TypeError' && error.message.includes('fetch')) { throw new Error(`Network error: Unable to connect to Mistral API. Check your internet connection and API endpoint: ${config.ai_server}`); } // Обработка ошибок парсинга JSON для потоковых ответов if (error.message.includes('Unexpected token') && error.message.includes('data:')) { throw new Error('Stream parsing error: Mistral API returned streaming data but stream handling failed. Try setting stream: false in configuration.'); } throw error; } } /** * Обрабатывает потоковый ответ от Mistral API * @param {Response} response - ответ от fetch * @returns {Promise<string>} - собранный текст ответа */ async handleStreamResponse(response) { const reader = response.body.getReader(); const decoder = new TextDecoder(); let result = ''; let isFirstChunk = true; 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'); // Новая строка после завершения return result.trim(); } try { const parsed = JSON.parse(data); if (parsed.choices && parsed.choices[0] && parsed.choices[0].delta && parsed.choices[0].delta.content) { const content = parsed.choices[0].delta.content; // Показываем заголовок только при первом чанке if (isFirstChunk && content.trim()) { console.log('🤖 AI Response (streaming):'); console.log('─'.repeat(50)); isFirstChunk = false; } // Выводим контент в реальном времени process.stdout.write(content); result += content; } } catch (parseError) { // Игнорируем ошибки парсинга отдельных чанков continue; } } } } console.log('\n'); // Новая строка после завершения return result.trim(); } finally { reader.releaseLock(); } } /** * Строит содержимое промпта с учетом 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.api_key) { issues.push('API key is required'); } if (!config.ai_server) { issues.push('AI server URL is required'); } else if (!config.ai_server.startsWith('https://')) { issues.push('AI server URL should use HTTPS'); } if (!config.model) { issues.push('Model is required'); } // Убираем проверку поддерживаемых моделей - позволяем пользователю использовать любые модели return { isValid: issues.length === 0, issues }; } /** * Проверяет доступность API * @param {Object} config - конфигурация * @returns {Promise<boolean>} */ async checkApiAvailability(config) { try { const response = await fetch(config.ai_server, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${config.api_key}` }, body: JSON.stringify({ model: config.model, messages: [{ role: 'user', content: 'test' }], max_tokens: 1 }) }); return response.status !== 401 && response.status !== 403; } catch { return false; } } }