Compare commits
10 Commits
67d55b8142
...
bf485b16ae
| Author | SHA1 | Date | |
|---|---|---|---|
| bf485b16ae | |||
| 325143fe21 | |||
| ad98a2a4eb | |||
| ef3651db95 | |||
| b4df19b2c1 | |||
| b5b5b94595 | |||
| 77b70db65a | |||
| 513f9f86d4 | |||
| 830091564c | |||
| 21c18afd02 |
@@ -66,7 +66,9 @@ export default function AuditPanel() {
|
||||
return;
|
||||
}
|
||||
|
||||
abortRef.current = false;
|
||||
const controller = new AbortController();
|
||||
abortRef.current = controller as any; // Usando any pois não precisamos refatorar todos os tipos agora, apenas para passar o signal
|
||||
|
||||
setError(null);
|
||||
setReport(null);
|
||||
setProgress(0);
|
||||
@@ -83,14 +85,18 @@ export default function AuditPanel() {
|
||||
config,
|
||||
scenarios,
|
||||
(p) => {
|
||||
setProgress(p);
|
||||
if (abortRef.current) throw new Error('Abortado pelo usuário');
|
||||
if (typeof p === 'number') setProgress(p);
|
||||
},
|
||||
controller.signal
|
||||
);
|
||||
setReport(reportResult);
|
||||
setStatus('done');
|
||||
|
||||
// Auto-download MD
|
||||
import('@/lib/audit/export-audit-md').then(m => m.downloadAuditMarkdown(reportResult));
|
||||
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.message === 'Abortado pelo usuário') {
|
||||
if (e instanceof Error && e.message.includes('Abortado')) {
|
||||
setStatus('idle');
|
||||
return;
|
||||
}
|
||||
@@ -161,7 +167,7 @@ export default function AuditPanel() {
|
||||
<Input
|
||||
value={config.baseUrl ?? ''}
|
||||
onChange={(e) => setConfig((c) => ({ ...c, baseUrl: e.target.value || undefined }))}
|
||||
placeholder="https://api.openai.com/v1"
|
||||
placeholder={config.provider === 'openrouter' ? 'https://openrouter.ai/api/v1 (padrão)' : config.provider === 'minimax' ? 'https://api.minimax.chat/v1 (padrão)' : config.provider === 'ollama' ? 'http://localhost:11434 (padrão)' : 'https://api.openai.com/v1'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -179,20 +185,26 @@ export default function AuditPanel() {
|
||||
Executar Auditoria
|
||||
</Button>
|
||||
{(status === 'running' || status === 'generating') && (
|
||||
<Button variant="outline" size="sm" onClick={() => { abortRef.current = true; }}>
|
||||
<Button variant="outline" size="sm" onClick={() => { if (abortRef.current && typeof (abortRef.current as any).abort === 'function') (abortRef.current as any).abort(); }}>
|
||||
Cancelar
|
||||
</Button>
|
||||
)}
|
||||
{status === 'done' && report && (
|
||||
<Button variant="outline" size="sm" onClick={handleExportPDF}>
|
||||
<FileDown className="w-3.5 h-3.5 mr-1.5" />
|
||||
Exportar PDF
|
||||
</Button>
|
||||
<>
|
||||
<Button variant="outline" size="sm" onClick={() => import('@/lib/audit/export-audit-md').then(m => m.downloadAuditMarkdown(report))}>
|
||||
<FileDown className="w-3.5 h-3.5 mr-1.5" />
|
||||
Exportar MD
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={handleExportPDF}>
|
||||
<FileDown className="w-3.5 h-3.5 mr-1.5" />
|
||||
Exportar PDF
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(status === 'running' || status === 'generating') && total > 0 && (
|
||||
<div className="space-y-1">
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-xs text-muted-foreground">
|
||||
<span>{status === 'generating' ? 'Gerando cenários...' : `Processando cenários...`}</span>
|
||||
<span>{progress} / {total}</span>
|
||||
@@ -203,6 +215,9 @@ export default function AuditPanel() {
|
||||
style={{ width: `${(progress / total) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground animate-pulse text-center">
|
||||
Aguardando resposta do modelo para o lote atual... (pode demorar alguns segundos)
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { AuditReport } from './types';
|
||||
|
||||
export function generateAuditMarkdown(report: AuditReport): string {
|
||||
let md = `# Relatório de Auditoria — BrainWind\n\n`;
|
||||
md += `**Provedor:** ${report.provider} (${report.model})\n`;
|
||||
md += `**Data:** ${new Date(report.timestamp).toLocaleString('pt-BR')}\n\n`;
|
||||
|
||||
md += `## Resumo Geral\n`;
|
||||
md += `- **Total de Cenários:** ${report.totalScenarios}\n`;
|
||||
md += `- **PASS:** ${report.totalPassed} ✅\n`;
|
||||
md += `- **WARN:** ${report.totalWarnings} ⚠️\n`;
|
||||
md += `- **FAIL:** ${report.totalFailed} ❌\n\n`;
|
||||
|
||||
for (const mod of report.modules) {
|
||||
md += `## Módulo: ${mod.moduleLabel}\n`;
|
||||
md += `**Total:** ${mod.totalScenarios} | **PASS:** ${mod.passed} | **WARN:** ${mod.warnings} | **FAIL:** ${mod.failed}\n\n`;
|
||||
|
||||
for (const sc of mod.scenarios) {
|
||||
const icon = sc.verdict === 'PASS' ? '✅' : sc.verdict === 'WARN' ? '⚠️' : '❌';
|
||||
md += `### ${icon} Cenário: ${sc.scenarioId}\n`;
|
||||
md += `**Veredito:** ${sc.verdict} — *${sc.summary}*\n\n`;
|
||||
|
||||
if (sc.llmNotes) {
|
||||
md += `**Observações da IA:** ${sc.llmNotes}\n\n`;
|
||||
}
|
||||
|
||||
md += `**Raciocínio Matemático (Chain of Thought):**\n`;
|
||||
const calcSteps = sc.calculation_steps || 'Sem raciocínio fornecido pela IA.';
|
||||
md += `> ${calcSteps.split('\n').join('\n> ')}\n\n`;
|
||||
|
||||
md += `**Verificações Individuais:**\n`;
|
||||
for (const chk of sc.checks) {
|
||||
const chkIcon = chk.status === 'PASS' ? '✅' : chk.status === 'WARN' ? '⚠️' : '❌';
|
||||
md += `- ${chkIcon} **${chk.name}:** Calculado IA = \`${chk.expected}\` | App = \`${chk.actual}\`\n`;
|
||||
if (chk.explanation) {
|
||||
md += ` - *Nota:* ${chk.explanation}\n`;
|
||||
}
|
||||
}
|
||||
md += `\n---\n\n`;
|
||||
}
|
||||
}
|
||||
|
||||
return md;
|
||||
}
|
||||
|
||||
export function downloadAuditMarkdown(report: AuditReport) {
|
||||
const md = generateAuditMarkdown(report);
|
||||
const blob = new Blob([md], { type: 'text/markdown;charset=utf-8' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.setAttribute('href', url);
|
||||
link.setAttribute('download', `auditoria-${report.id}.md`);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
@@ -101,7 +101,7 @@ function AuditReportDocument({ report }: ReportPDFProps) {
|
||||
</View>
|
||||
|
||||
{report.modules.map((mod: ModuleAuditResult) => (
|
||||
<View key={mod.module} wrap={false} style={styles.moduleCard}>
|
||||
<View key={mod.module} style={styles.moduleCard}>
|
||||
<Text style={styles.moduleTitle}>
|
||||
{mod.moduleLabel} — {mod.passed}/{mod.totalScenarios} (W:{mod.warnings} F:{mod.failed})
|
||||
</Text>
|
||||
|
||||
@@ -5,7 +5,9 @@ import type { AuditScenario } from './types';
|
||||
|
||||
const SYSTEM_PROMPT = `Você é um engenheiro estrutural especialista na norma brasileira NBR 6123:2023 (Ação do Vento em Edificações). Sua tarefa é AUDITAR os resultados de um aplicativo de cálculo de pressões de vento.
|
||||
|
||||
Você receberá uma lista de cenários de teste. Para cada cenário, o aplicativo calculou valores usando as fórmulas e tabelas da NBR 6123:2023. Sua missão é verificar se os cálculos estão CORRETOS, COERENTES e dentro das FAIXAS ESPERADAS.
|
||||
Você receberá uma lista de cenários de teste. Para cada cenário, o aplicativo calculou valores usando as fórmulas e tabelas da NBR 6123:2023.
|
||||
Sua missão é atuar como um auditor cego: primeiro, recalcule matematicamente os resultados com base nas entradas (Inputs) usando APENAS as tabelas e fórmulas fornecidas abaixo. Depois, verifique se os seus cálculos batem com os valores fornecidos nos 'outputs' e 'intermediates'.
|
||||
Você DEVE demonstrar seu raciocínio matemático no campo 'calculation_steps' ANTES de emitir o veredito.
|
||||
|
||||
## Regras da NBR 6123:2023 (invariantes)
|
||||
${INVARIANTES_NBR6123}
|
||||
@@ -17,8 +19,8 @@ ${NORM_REFERENCE}
|
||||
## Suas tarefas para cada cenário
|
||||
1. Verificar se as fórmulas fundamentais estão corretas (q = 0.613 × Vk² / 1000, Vk = V0 × S1 × S2 × S3)
|
||||
2. Verificar se Cpi está no intervalo [-0.9, +0.9]
|
||||
3. Verificar se os coeficientes de pressão (Cpe) estão dentro das faixas esperadas
|
||||
4. Verificar se as relações físicas make sense (ex: Cpe barlavento > 0, Cpe sotavento < 0 para cilindros)
|
||||
3. Recalcular e verificar se os coeficientes de pressão (Cpe) batem perfeitamente com os ângulos e dimensões tabelados
|
||||
4. Verificar se as relações físicas fazem sentido (ex: Cpe barlavento > 0, Cpe sotavento < 0 para cilindros)
|
||||
5. Verificar se os limites normativos são respeitados
|
||||
6. Verificar coerência entre inputs e outputs
|
||||
|
||||
@@ -29,32 +31,33 @@ Retorne EXATAMENTE um JSON válido (sem markdown, sem code blocks) com o seguint
|
||||
"results": [
|
||||
{
|
||||
"scenarioId": "id-do-cenario",
|
||||
"calculation_steps": "AQUI VOCÊ DEVE DEMONSTRAR A MATEMÁTICA PASSO A PASSO. Ex: Vk = 40 * 1.0 * 1.04 * 1.0 = 41.6. q = 0.613 * (41.6)^2 / 1000 = 1.060. Cpe_A (h/b = 0.5) = -0.8...",
|
||||
"verdict": "PASS" | "WARN" | "FAIL",
|
||||
"checks": [
|
||||
{
|
||||
"name": "nome do check",
|
||||
"name": "nome do check (ex: Pressão Dinâmica q)",
|
||||
"status": "PASS" | "WARN" | "FAIL",
|
||||
"expected": "valor ou faixa esperada",
|
||||
"expected": "o valor que VOCÊ calculou",
|
||||
"actual": "valor calculado no app",
|
||||
"explanation": "explicação curta do resultado"
|
||||
"explanation": "explicação curta da diferença se houver"
|
||||
}
|
||||
],
|
||||
"summary": "resumo em 1 linha do resultado deste cenário",
|
||||
"llmNotes": "observações adicionais se houver anomalias ou observações importantes"
|
||||
"summary": "resumo em 1 linha do resultado",
|
||||
"llmNotes": "observações adicionais"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
## Regras importantes
|
||||
- status "PASS" = tudo correto
|
||||
- status "WARN" = valores válidos mas no limite ou com pequena imprecisão numérica
|
||||
- status "FAIL" = erro real que precisa ser corrigido
|
||||
- status "PASS" = tudo correto ou variação insignificante (< 1% ou diferença absoluta < 0.05)
|
||||
- status "WARN" = valores com imprecisão numérica moderada, mas aceitável
|
||||
- status "FAIL" = erro real ou diferença grosseira (variação > 2%)
|
||||
- verdict "FAIL" = pelo menos 1 check FAIL
|
||||
- verdict "WARN" = pelo menos 1 check WARN e nenhum FAIL
|
||||
- verdict "PASS" = todos checks PASS
|
||||
- Seja preciso com números: compare os valores fornecidos com os ranges esperados
|
||||
- Se um valor está fora do range esperado, é FAIL
|
||||
- Se está dentro mas no limite (ex: -0.9, 0.9), é WARN
|
||||
- Seja preciso com números: compare os valores fornecidos pelo app com os que você deduziu matematicamente das tabelas e fórmulas
|
||||
- EMPATE TÉCNICO: Se o seu cálculo diferir do app em menos de 1% (ex: 1.04 vs 1.05) devido a interpolação ou arredondamentos de passos intermediários, isso NÃO é uma falha. Você DEVE marcar como "PASS" ou no máximo "WARN", NUNCA "FAIL".
|
||||
- Só marque "FAIL" se a diferença for claramente um erro de fórmula, módulo errado ou leitura errada de tabela.
|
||||
- IMPORTANTE: Cpi DEVE estar em [-0.9, +0.9] — qualquer valor fora é FAIL
|
||||
- q deve ser sempre >= 0
|
||||
- Verifique a fórmula q = 0.613 × Vk² / 1000 para cada cenário`;
|
||||
@@ -76,7 +79,6 @@ Abaixo estão ${scenarios.length} cenários de teste. Para cada um, os campos s
|
||||
- inputs: todos os dados de entrada fornecidos pelo usuário
|
||||
- intermediates: cálculos intermediários calculados pelo app (S2, Vk, q, Cpi, etc.)
|
||||
- outputs: resultados finais calculados pelo app
|
||||
- expectedRanges: faixas numéricas esperadas para valores-chave
|
||||
- nbrSection: seção da NBR 6123 aplicável
|
||||
|
||||
=== CENÁRIO ===
|
||||
|
||||
+82
-36
@@ -27,17 +27,19 @@ export async function runLLMAudit(
|
||||
systemPrompt: string,
|
||||
userPrompt: string,
|
||||
onProgress?: (msg: string) => void,
|
||||
signal?: AbortSignal,
|
||||
): Promise<string> {
|
||||
config = { ...config, apiKey: config.apiKey?.trim() || '' };
|
||||
if (config.provider === 'openai') {
|
||||
return callOpenAI(config, systemPrompt, userPrompt, onProgress);
|
||||
return callOpenAI(config, systemPrompt, userPrompt, onProgress, signal);
|
||||
} else if (config.provider === 'anthropic') {
|
||||
return callAnthropic(config, systemPrompt, userPrompt, onProgress);
|
||||
return callAnthropic(config, systemPrompt, userPrompt, onProgress, signal);
|
||||
} else if (config.provider === 'minimax') {
|
||||
return callMinimax(config, systemPrompt, userPrompt, onProgress);
|
||||
return callMinimax(config, systemPrompt, userPrompt, onProgress, signal);
|
||||
} else if (config.provider === 'openrouter') {
|
||||
return callOpenRouter(config, systemPrompt, userPrompt, onProgress);
|
||||
return callOpenRouter(config, systemPrompt, userPrompt, onProgress, signal);
|
||||
} else {
|
||||
return callOllama(config, systemPrompt, userPrompt, onProgress);
|
||||
return callOllama(config, systemPrompt, userPrompt, onProgress, signal);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +48,7 @@ async function callOpenAI(
|
||||
systemPrompt: string,
|
||||
userPrompt: string,
|
||||
onProgress?: (msg: string) => void,
|
||||
signal?: AbortSignal,
|
||||
): Promise<string> {
|
||||
onProgress?.('Enviando para OpenAI...');
|
||||
const url = 'https://api.openai.com/v1/chat/completions';
|
||||
@@ -65,7 +68,7 @@ async function callOpenAI(
|
||||
'Authorization': `Bearer ${config.apiKey}`,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
}, 180000);
|
||||
}, 600000, 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;
|
||||
@@ -78,6 +81,7 @@ async function callAnthropic(
|
||||
systemPrompt: string,
|
||||
userPrompt: string,
|
||||
onProgress?: (msg: string) => void,
|
||||
signal?: AbortSignal,
|
||||
): Promise<string> {
|
||||
onProgress?.('Enviando para Anthropic...');
|
||||
const url = 'https://api.anthropic.com/v1/messages';
|
||||
@@ -99,7 +103,7 @@ async function callAnthropic(
|
||||
'anthropic-dangerous-direct-browser-access': 'true',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
}, 180000);
|
||||
}, 600000, 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;
|
||||
@@ -112,9 +116,10 @@ async function callMinimax(
|
||||
systemPrompt: string,
|
||||
userPrompt: string,
|
||||
onProgress?: (msg: string) => void,
|
||||
signal?: AbortSignal,
|
||||
): Promise<string> {
|
||||
onProgress?.('Enviando para MiniMax...');
|
||||
const url = config.baseUrl ?? 'https://api.minimax.chat/v1/chat/completions';
|
||||
const url = config.baseUrl || 'https://api.minimax.chat/v1/chat/completions';
|
||||
const body = {
|
||||
model: config.model,
|
||||
messages: [
|
||||
@@ -131,7 +136,7 @@ async function callMinimax(
|
||||
'Authorization': `Bearer ${config.apiKey}`,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
}, 180000);
|
||||
}, 600000, 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;
|
||||
@@ -144,26 +149,32 @@ async function callOpenRouter(
|
||||
systemPrompt: string,
|
||||
userPrompt: string,
|
||||
onProgress?: (msg: string) => void,
|
||||
signal?: AbortSignal,
|
||||
): Promise<string> {
|
||||
onProgress?.('Enviando para OpenRouter...');
|
||||
const url = config.baseUrl ?? 'https://openrouter.ai/api/v1/chat/completions';
|
||||
const body: OpenAIRequest = {
|
||||
const url = config.baseUrl || 'https://openrouter.ai/api/v1/chat/completions';
|
||||
const body = {
|
||||
model: config.model,
|
||||
messages: [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: userPrompt },
|
||||
{ role: 'system' as const, content: systemPrompt },
|
||||
{ role: 'user' as const, 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}`,
|
||||
'HTTP-Referer': 'https://brainwind.app',
|
||||
'X-Title': 'BrainWind NBR 6123',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
}, 180000);
|
||||
}, 600000, 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;
|
||||
@@ -176,9 +187,10 @@ async function callOllama(
|
||||
systemPrompt: string,
|
||||
userPrompt: string,
|
||||
onProgress?: (msg: string) => void,
|
||||
signal?: AbortSignal,
|
||||
): Promise<string> {
|
||||
onProgress?.('Enviando para Ollama (local)...');
|
||||
const baseUrl = config.baseUrl ?? 'http://localhost:11434';
|
||||
const baseUrl = config.baseUrl || 'http://localhost:11434';
|
||||
const body = {
|
||||
model: config.model,
|
||||
messages: [
|
||||
@@ -193,7 +205,7 @@ async function callOllama(
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
}, 300000);
|
||||
}, 600000, 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');
|
||||
@@ -204,40 +216,74 @@ export async function runAudit(
|
||||
config: AuditConfig,
|
||||
scenarios: AuditScenario[],
|
||||
onProgress?: (progress: number) => void,
|
||||
signal?: AbortSignal,
|
||||
): Promise<AuditReport> {
|
||||
onProgress?.(0);
|
||||
const maxRetries = 2;
|
||||
const chunkSize = Math.ceil(scenarios.length / 1);
|
||||
const maxRetriesPerChunk = 2;
|
||||
const chunkSize = 5; // Reduzido de 10 para 5 devido ao alto volume de tokens do Chain of Thought
|
||||
|
||||
let lastError: Error | null = null;
|
||||
for (let retry = 0; retry <= maxRetries; retry++) {
|
||||
try {
|
||||
const prompt = buildPrompt(scenarios);
|
||||
const rawResponse = await runLLMAudit(config, prompt.system, prompt.user, () => {
|
||||
onProgress?.(Math.round((retry * chunkSize) / maxRetries));
|
||||
});
|
||||
const llmResults = parseLLMResponse(rawResponse, scenarios.map(s => s.id));
|
||||
const scenarioMap = new Map(scenarios.map(s => [s.id, { module: s.module, moduleLabel: s.moduleLabel }]));
|
||||
const report = buildAuditReport(llmResults, scenarioMap, config.provider, config.model, rawResponse);
|
||||
onProgress?.(scenarios.length);
|
||||
return report;
|
||||
} catch (e) {
|
||||
lastError = e instanceof Error ? e : new Error(String(e));
|
||||
if (retry < maxRetries) {
|
||||
onProgress?.(0);
|
||||
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);
|
||||
}
|
||||
throw lastError ?? new Error('Audit failed after retries');
|
||||
|
||||
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<Response> {
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user