feat(audit): adicionar módulo de auditoria + rebranding BrainWind
- AuditPanel, AuditDetailPopup, parser, runner, prompt-builder, serializer, scenarios, export-audit-pdf - i18n: strings novas para auditoria - Settings: integração com módulo de auditoria - types: tipos compartilhados do módulo audit
This commit is contained in:
@@ -0,0 +1,247 @@
|
||||
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,
|
||||
): Promise<string> {
|
||||
if (config.provider === 'openai') {
|
||||
return callOpenAI(config, systemPrompt, userPrompt, onProgress);
|
||||
} else if (config.provider === 'anthropic') {
|
||||
return callAnthropic(config, systemPrompt, userPrompt, onProgress);
|
||||
} else if (config.provider === 'minimax') {
|
||||
return callMinimax(config, systemPrompt, userPrompt, onProgress);
|
||||
} else if (config.provider === 'openrouter') {
|
||||
return callOpenRouter(config, systemPrompt, userPrompt, onProgress);
|
||||
} else {
|
||||
return callOllama(config, systemPrompt, userPrompt, onProgress);
|
||||
}
|
||||
}
|
||||
|
||||
async function callOpenAI(
|
||||
config: AuditConfig,
|
||||
systemPrompt: string,
|
||||
userPrompt: string,
|
||||
onProgress?: (msg: string) => void,
|
||||
): Promise<string> {
|
||||
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);
|
||||
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,
|
||||
): Promise<string> {
|
||||
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);
|
||||
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,
|
||||
): Promise<string> {
|
||||
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);
|
||||
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,
|
||||
): Promise<string> {
|
||||
onProgress?.('Enviando para OpenRouter...');
|
||||
const url = config.baseUrl ?? 'https://openrouter.ai/api/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);
|
||||
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,
|
||||
): Promise<string> {
|
||||
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);
|
||||
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,
|
||||
): Promise<AuditReport> {
|
||||
onProgress?.(0);
|
||||
const maxRetries = 2;
|
||||
const chunkSize = Math.ceil(scenarios.length / 1);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
throw lastError ?? new Error('Audit failed after retries');
|
||||
}
|
||||
|
||||
async function fetchWithTimeout(
|
||||
url: string,
|
||||
init: RequestInit,
|
||||
timeoutMs: number,
|
||||
): Promise<Response> {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
||||
try {
|
||||
const response = await fetch(url, { ...init, signal: controller.signal });
|
||||
return response;
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user