import type { AuditConfig, AuditScenario, AuditReport } from './types'; import { buildPrompt } from './prompt-builder'; import { parseLLMResponse, buildAuditReport } from './parser'; interface OpenAIMessage { role: 'system' | 'user' | 'assistant'; content: string; } interface OpenAIRequest { model: string; messages: OpenAIMessage[]; temperature: number; response_format: { type: 'json_object' }; } interface AnthropicRequest { model: string; messages: { role: 'user' | 'assistant'; content: string }[]; system?: string; temperature: number; max_tokens: number; } export async function runLLMAudit( config: AuditConfig, systemPrompt: string, userPrompt: string, onProgress?: (msg: string) => void, signal?: AbortSignal, ): Promise { config = { ...config, apiKey: config.apiKey?.trim() || '' }; if (config.provider === 'openai') { return callOpenAI(config, systemPrompt, userPrompt, onProgress, signal); } else if (config.provider === 'anthropic') { return callAnthropic(config, systemPrompt, userPrompt, onProgress, signal); } else if (config.provider === 'minimax') { return callMinimax(config, systemPrompt, userPrompt, onProgress, signal); } else if (config.provider === 'openrouter') { return callOpenRouter(config, systemPrompt, userPrompt, onProgress, signal); } else { return callOllama(config, systemPrompt, userPrompt, onProgress, signal); } } async function callOpenAI( config: AuditConfig, systemPrompt: string, userPrompt: string, onProgress?: (msg: string) => void, signal?: AbortSignal, ): Promise { onProgress?.('Enviando para OpenAI...'); const url = 'https://api.openai.com/v1/chat/completions'; const body: OpenAIRequest = { model: config.model, messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: userPrompt }, ], temperature: 0, response_format: { type: 'json_object' }, }; const response = await fetchWithTimeout(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${config.apiKey}`, }, body: JSON.stringify(body), }, 180000, signal); const data = await response.json() as { choices?: { message?: { content?: string } }[]; error?: { message?: string } }; if (data.error) throw new Error(`OpenAI error: ${data.error.message}`); const content = data.choices?.[0]?.message?.content; if (!content) throw new Error('OpenAI returned empty response'); return content; } async function callAnthropic( config: AuditConfig, systemPrompt: string, userPrompt: string, onProgress?: (msg: string) => void, signal?: AbortSignal, ): Promise { onProgress?.('Enviando para Anthropic...'); const url = 'https://api.anthropic.com/v1/messages'; const body: AnthropicRequest = { model: config.model, messages: [ { role: 'user', content: `${systemPrompt}\n\n${userPrompt}` }, ], system: systemPrompt, temperature: 0, max_tokens: 8192, }; const response = await fetchWithTimeout(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': config.apiKey, 'anthropic-version': '2023-06-01', 'anthropic-dangerous-direct-browser-access': 'true', }, body: JSON.stringify(body), }, 180000, signal); const data = await response.json() as { content?: { text?: string }[]; error?: { type?: string; message?: string } }; if (data.error) throw new Error(`Anthropic error: ${data.error.message}`); const text = data.content?.[0]?.text; if (!text) throw new Error('Anthropic returned empty response'); return text; } async function callMinimax( config: AuditConfig, systemPrompt: string, userPrompt: string, onProgress?: (msg: string) => void, signal?: AbortSignal, ): Promise { onProgress?.('Enviando para MiniMax...'); const url = config.baseUrl || 'https://api.minimax.chat/v1/chat/completions'; const body = { model: config.model, messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: userPrompt }, ], temperature: 0, response_format: { type: 'json_object' }, }; const response = await fetchWithTimeout(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${config.apiKey}`, }, body: JSON.stringify(body), }, 180000, signal); const data = await response.json() as { choices?: { message?: { content?: string } }[]; error?: { message?: string } }; if (data.error) throw new Error(`MiniMax error: ${data.error.message}`); const content = data.choices?.[0]?.message?.content; if (!content) throw new Error('MiniMax returned empty response'); return content; } async function callOpenRouter( config: AuditConfig, systemPrompt: string, userPrompt: string, onProgress?: (msg: string) => void, signal?: AbortSignal, ): Promise { onProgress?.('Enviando para OpenRouter...'); const url = config.baseUrl || 'https://openrouter.ai/api/v1/chat/completions'; const body = { model: config.model, messages: [ { role: 'system' as const, content: systemPrompt }, { role: 'user' as const, content: userPrompt }, ], temperature: 0, }; const response = await fetchWithTimeout(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${config.apiKey}`, 'HTTP-Referer': 'https://brainwind.app', 'X-Title': 'BrainWind NBR 6123', }, body: JSON.stringify(body), }, 180000, signal); if (!response.ok) { const errText = await response.text(); throw new Error(`OpenRouter HTTP ${response.status}: ${errText}`); } const data = await response.json() as { choices?: { message?: { content?: string } }[]; error?: { message?: string } }; if (data.error) throw new Error(`OpenRouter error: ${data.error.message}`); const content = data.choices?.[0]?.message?.content; if (!content) throw new Error('OpenRouter returned empty response'); return content; } async function callOllama( config: AuditConfig, systemPrompt: string, userPrompt: string, onProgress?: (msg: string) => void, signal?: AbortSignal, ): Promise { onProgress?.('Enviando para Ollama (local)...'); const baseUrl = config.baseUrl || 'http://localhost:11434'; const body = { model: config.model, messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: userPrompt }, ], stream: false, format: 'json', options: { temperature: 0 }, }; const response = await fetchWithTimeout(`${baseUrl}/api/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }, 300000, signal); const data = await response.json() as { message?: { content?: string }; error?: string }; if (data.error) throw new Error(`Ollama error: ${data.error}`); if (!data.message?.content) throw new Error('Ollama returned empty response'); return data.message.content; } export async function runAudit( config: AuditConfig, scenarios: AuditScenario[], onProgress?: (progress: number) => void, signal?: AbortSignal, ): Promise { onProgress?.(0); const maxRetriesPerChunk = 2; const chunkSize = 5; // Reduzido de 10 para 5 devido ao alto volume de tokens do Chain of Thought const allLlmResults = []; let completed = 0; let rawResponseFull = ''; for (let i = 0; i < scenarios.length; i += chunkSize) { if (signal?.aborted) { throw new Error('Abortado pelo usuário'); } const chunk = scenarios.slice(i, i + chunkSize); let chunkSuccess = false; let chunkError: Error | null = null; for (let retry = 0; retry <= maxRetriesPerChunk; retry++) { if (signal?.aborted) { throw new Error('Abortado pelo usuário'); } try { const prompt = buildPrompt(chunk); // Avisar a UI do progresso absoluto onProgress?.(completed); const rawResponse = await runLLMAudit(config, prompt.system, prompt.user, (msg) => { // opcional }, signal); rawResponseFull += rawResponse + '\n\n'; const llmResults = parseLLMResponse(rawResponse, chunk.map(s => s.id)); allLlmResults.push(...llmResults); chunkSuccess = true; break; // Sucesso, sai do retry loop } catch (e) { chunkError = e instanceof Error ? e : new Error(String(e)); console.warn(`Erro no chunk ${i} (tentativa ${retry}):`, chunkError); } } if (!chunkSuccess) { throw chunkError ?? new Error(`Falha ao auditar cenários no índice ${i} após retentativas.`); } completed += chunk.length; onProgress?.(completed); } const scenarioMap = new Map(scenarios.map(s => [s.id, { module: s.module, moduleLabel: s.moduleLabel }])); const report = buildAuditReport(allLlmResults, scenarioMap, config.provider, config.model, rawResponseFull); return report; } async function fetchWithTimeout( url: string, init: RequestInit, timeoutMs: number, parentSignal?: AbortSignal, ): Promise { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), timeoutMs); if (parentSignal) { parentSignal.addEventListener('abort', () => controller.abort()); if (parentSignal.aborted) controller.abort(); } try { const response = await fetch(url, { ...init, signal: controller.signal }); return response; } finally { clearTimeout(timeout); } }