Atualização automática - 2026-06-24 10:23:23

This commit is contained in:
2026-06-24 10:23:23 +00:00
parent 9b13e3545c
commit 4599398fb6
3 changed files with 332 additions and 68 deletions
+72 -68
View File
@@ -1,6 +1,7 @@
import { GoogleGenAI, Type } from "@google/genai";
import type { AIProvider } from '../types/providers';
import type { ReportData } from '../types';
import type { ReportData, Standard } from '../types';
import { validateCertificateAgainstStandard } from './validationEngine';
interface AnalyzeOptions {
provider: AIProvider;
@@ -73,7 +74,7 @@ const reportSchema = {
compliance: {
type: Type.OBJECT,
properties: {
status: { type: Type.STRING, description: "'CONFORME' ou 'NÃO CONFORME'" },
status: { type: Type.STRING, description: "'CONFORME' ou 'NÃO CONFORME' (deixe vazio se não souber)" },
nonComplianceReason: { type: Type.STRING },
mechanical: {
type: Type.ARRAY,
@@ -81,12 +82,9 @@ const reportSchema = {
type: Type.OBJECT,
properties: {
property: { type: Type.STRING },
norm: { type: Type.STRING },
certificate: { type: Type.STRING },
status: { type: Type.STRING, description: "'OK' ou 'REPROVADO'" },
differencePercentage: { type: Type.STRING }
certificate: { type: Type.STRING }
},
required: ["property", "norm", "certificate", "status", "differencePercentage"]
required: ["property", "certificate"]
}
},
chemical: {
@@ -95,11 +93,9 @@ const reportSchema = {
type: Type.OBJECT,
properties: {
element: { type: Type.STRING },
norm: { type: Type.STRING },
certificate: { type: Type.STRING },
status: { type: Type.STRING, description: "'OK' ou 'REPROVADO'" }
certificate: { type: Type.STRING }
},
required: ["element", "norm", "certificate", "status"]
required: ["element", "certificate"]
}
},
otherTests: {
@@ -108,14 +104,13 @@ const reportSchema = {
type: Type.OBJECT,
properties: {
test: { type: Type.STRING },
result: { type: Type.STRING },
status: { type: Type.STRING }
result: { type: Type.STRING }
},
required: ["test", "result", "status"]
required: ["test", "result"]
}
}
},
required: ["status", "nonComplianceReason", "mechanical", "chemical", "otherTests"]
required: ["mechanical", "chemical", "otherTests"]
},
overPerformance: {
type: Type.ARRAY,
@@ -150,39 +145,20 @@ const arraySchema = {
items: reportSchema
};
export const buildAnalysisPrompt = (standardsContext?: any[]) => {
let prompt = `Você é o "SteelCheck", um especialista sênior em engenharia de materiais, metalurgia, calibração e controle de qualidade industrial.
export const buildAnalysisPrompt = () => {
let prompt = `Você é o "SteelCheck", um especialista sênior em extração de dados de Certificados de Qualidade.
Sua tarefa principal é transcrever com precisão matemática e zero alucinação os dados de um Certificado de Qualidade (Mill Test Report) e gerar um "Relatório de Análise Técnica" em formato JSON.
Sua tarefa principal é transcrever com precisão matemática e zero alucinação os dados de um Certificado de Qualidade (Mill Test Report) e gerar um JSON estruturado.
### DIRETRIZES RÍGIDAS DE TRANSCRIÇÃO (OCR DE ALTA PRECISÃO):
1. **Zero Alucinação de Normas:** No campo "standards", extraia APENAS e EXATAMENTE o que estiver escrito na imagem.
2. **Extração de Identificação:** Transcreva números de certificado, lotes, corridas e quantidades exatamente como aparecem.
3. **Mapeamento Preciso de Colunas:** Mapeie composição química e propriedades mecânicas com precisão absoluta.
2. **Extração de Identificação:** Transcreva o produto (incluindo dimensões/bitola, ex: "CHAPA 50MM" ou "PERFIL W360x44.6"), números de certificado, lotes, corridas e quantidades exatamente como aparecem.
3. **Mapeamento Preciso de Colunas:** Mapeie composição química e propriedades mecânicas com precisão absoluta para a chave "certificate". Não preencha "norm" nem "status".
4. **Múltiplas Linhas:** Se houver múltiplas linhas de teste, gere um objeto para cada uma.
5. **Orientação:** Rotacione mentalmente a imagem para ler corretamente.
### ANÁLISE DE CONFORMIDADE:
- Compare os valores reais extraídos com os requisitos mínimos/máximos da norma técnica declarada no certificado.
- FORMATO DO CAMPO "NORM": Sempre que a norma (ou o Banco Local) estabelecer uma FAIXA (um mínimo e um máximo) para um elemento, você DEVE transcrever AMBOS os limites no campo "norm" do JSON (Exemplo: "Min: 0.005% - Max: 0.05%"). Nunca oculte o limite máximo ou mínimo se ambos existirem.
- LAUDOS COM DUPLA ESPECIFICAÇÃO (MÚLTIPLAS NORMAS): Se o certificado citar mais de uma norma (ex: "ASTM A572 GR50 / NBR 7007 AR350"), você DEVE adotar sempre o critério MAIS RÍGIDO (o mais conservador e restritivo) entre as normas citadas para garantir que o material atenda a ambas. Exemplo: se a ASTM não possui limite para Carbono Equivalente (CEQ), mas a NBR exige Máx: 0.47%, você DEVE usar Máx: 0.47% como parâmetro de reprovação.
- REGRA CRÍTICA DE STATUS GERAL: Se QUALQUER item (mecânico, químico ou outro) for avaliado como "REPROVADO", o status geral do laudo ("compliance.status") DEVE ser OBRIGATORIAMENTE "NÃO CONFORME". Só atribua "CONFORME" se ABSOLUTAMENTE TODOS os itens individuais forem "OK".`;
- No campo "analysisSource" do JSON, preencha EXATAMENTE com "Rede Neural".
if (standardsContext && standardsContext.length > 0) {
prompt += `\n\n=== BANCO DE NORMAS LOCAL (USO OBRIGATÓRIO) ===\n`;
prompt += `Você DEVE utilizar as regras rígidas do seguinte banco de dados local para confrontar e validar os resultados do certificado. SE a norma citada no laudo estiver presente na lista abaixo, USE ESTRITAMENTE os valores desta lista para verificar o "status" (CONFORME / REPROVADO):\n\n`;
prompt += JSON.stringify(standardsContext, null, 2);
prompt += `\n\n- No campo "analysisSource" do JSON, preencha EXATAMENTE com "Banco Local" se você usou as regras deste banco.`;
prompt += `\n=================================================\n`;
} else {
prompt += `\n- Utilize o seu conhecimento paramétrico interno de engenharia de materiais para aplicar os limites corretos da norma encontrada no laudo.\n`;
prompt += `- No campo "analysisSource" do JSON, preencha EXATAMENTE com "Rede Neural".\n`;
}
prompt += `
- **Fórmula de Variação (differencePercentage):** Calcule a variação percentual do valor do certificado em relação ao mínimo exigido pela norma.
USE ESTRITAMENTE A FÓRMULA: ((Valor_Certificado - Valor_Norma) / Valor_Norma) * 100.
Exemplo: Se a Norma exige Mínimo 345, e o Certificado acusa 415, o resultado é ((415-345)/345)*100 = +20.3%.
Retorne APENAS o JSON no formato do array especificado, sem explicações ou markdown.
@@ -202,13 +178,13 @@ FORMATO DO RETORNO:
"quantity": "8,560 T"
},
"compliance": {
"status": "CONFORME",
"status": "",
"nonComplianceReason": "",
"mechanical": [
{ "property": "Yield Strength (Limite de Escoamento)", "norm": "Min: 345 MPa", "certificate": "415 MPa", "status": "OK", "differencePercentage": "+20.3%" }
{ "property": "Yield Strength (Limite de Escoamento)", "certificate": "415 MPa" }
],
"chemical": [
{ "element": "C (Carbon)", "norm": "Max: 0.23%", "certificate": "0.22%", "status": "OK" }
{ "element": "C (Carbon)", "certificate": "0.22%" }
],
"otherTests": []
},
@@ -227,7 +203,7 @@ export const analyzeWithGemini = async (file: File, apiKey: string, model: strin
const ai = new GoogleGenAI({ apiKey: apiKey });
const imagePart = await fileToGenerativePart(file);
const prompt = buildAnalysisPrompt(standardsContext);
const prompt = buildAnalysisPrompt();
const response = await ai.models.generateContent({
model,
@@ -241,8 +217,14 @@ export const analyzeWithGemini = async (file: File, apiKey: string, model: strin
try {
const text = response.text;
const data = cleanAndParseJson(text);
return data as ReportData[];
const data = cleanAndParseJson(text) as ReportData[];
// Apply programmatic validation
if (standardsContext && standardsContext.length > 0) {
// Assume the first standard matched is the main one for now
return data.map(report => validateCertificateAgainstStandard(report, standardsContext[0] as Standard));
}
return data;
} catch (e) {
if (e instanceof Error) {
throw e;
@@ -262,7 +244,7 @@ export const analyzeWithOpenAI = async (file: File, apiKey: string, model: strin
reader.readAsDataURL(file);
});
const prompt = buildAnalysisPrompt(standardsContext);
const prompt = buildAnalysisPrompt();
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
@@ -302,7 +284,11 @@ export const analyzeWithOpenAI = async (file: File, apiKey: string, model: strin
throw new Error("Resposta vazia da OpenAI");
}
return cleanAndParseJson(content) as ReportData[];
const dataJson = cleanAndParseJson(content) as ReportData[];
if (standardsContext && standardsContext.length > 0) {
return dataJson.map(report => validateCertificateAgainstStandard(report, standardsContext[0] as Standard));
}
return dataJson;
};
export const analyzeCertificate = async (options: AnalyzeOptions): Promise<ReportData[]> => {
@@ -334,7 +320,7 @@ export const analyzeWithOllama = async (file: File, endpoint: string, model: str
reader.readAsDataURL(file);
});
const prompt = buildAnalysisPrompt(standardsContext);
const prompt = buildAnalysisPrompt();
let url = `${endpoint}/api/chat`;
@@ -392,7 +378,11 @@ export const analyzeWithOllama = async (file: File, endpoint: string, model: str
throw new Error("Resposta vazia do Ollama/OpenWebUI");
}
return cleanAndParseJson(content) as ReportData[];
const dataJson = cleanAndParseJson(content) as ReportData[];
if (standardsContext && standardsContext.length > 0) {
return dataJson.map(report => validateCertificateAgainstStandard(report, standardsContext[0] as Standard));
}
return dataJson;
};
export const analyzeWithOpenRouter = async (file: File, apiKey: string, model: string = 'openai/gpt-4o', standardsContext?: any[]): Promise<ReportData[]> => {
@@ -406,7 +396,7 @@ export const analyzeWithOpenRouter = async (file: File, apiKey: string, model: s
reader.readAsDataURL(file);
});
const prompt = buildAnalysisPrompt(standardsContext);
const prompt = buildAnalysisPrompt();
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
method: 'POST',
@@ -452,7 +442,11 @@ export const analyzeWithOpenRouter = async (file: File, apiKey: string, model: s
throw new Error("Resposta vazia do OpenRouter");
}
return cleanAndParseJson(content) as ReportData[];
const dataJson = cleanAndParseJson(content) as ReportData[];
if (standardsContext && standardsContext.length > 0) {
return dataJson.map(report => validateCertificateAgainstStandard(report, standardsContext[0] as Standard));
}
return dataJson;
};
export const buildStandardWithAI = async (
@@ -463,32 +457,42 @@ export const buildStandardWithAI = async (
const prompt = `Você é um engenheiro de materiais sênior. O usuário solicitou a criação de uma tabela de limites para a norma técnica: "${standardName}".
A categoria deste material é descrita como: "${categoryDescription}".
Sua tarefa é acessar seu conhecimento sobre esta norma e gerar os limites estritos (mínimos e máximos) exigidos.
Sua tarefa é acessar seu conhecimento sobre esta norma e gerar os limites estritos, incluindo as variações por faixa de espessura (bitola), grupos de perfil e tolerâncias de análise de produto.
RESPONDA APENAS UM JSON VÁLIDO no seguinte formato EXATO:
{
"codigo": "Nome oficial da norma (ex: ASTM A36/A36M-19)",
"produto_especifico": "Descreva o tipo de produto que essa norma cobre",
"id_norma": "id_unico_da_norma_sem_espacos",
"codigo": "Nome oficial da norma (ex: ASTM A572/A572M)",
"grau": "Grau se aplicável (ex: 50)",
"produto_especifico": "Descreva o tipo de produto",
"limites_quimicos_corrida": {
"produtos_gerais": [
{
"espessura_max_mm": valor_numerico_ou_null,
"carbono_max": valor_numerico,
"manganes_max": valor_numerico
}
]
},
"tolerancias_analise_produto_astma6": {
"carbono": {
"tabela": [
{ "limite_corrida_ate": 0.25, "tolerancia_mais": 0.03 }
]
}
},
"parametros_controle": {
"mecanicos": {
"nome_do_parametro_1": valor_numerico,
"nome_do_parametro_2": valor_numerico
},
"quimicos": {
"nome_do_parametro": valor_numerico
},
"fisicos": {
"nome_do_parametro": valor_numerico
"limite_escoamento_min": valor_numerico
}
}
}
Regras Cruciais:
1. USE APENAS NÚMEROS (floats/ints) nos valores. NUNCA use strings como "0.25" ou "250". Use 0.25 e 250.
2. Se a norma não especifica um limite máximo ou mínimo para um elemento, coloque null (sem aspas).
3. O nome da chave dentro de mecânicos/químicos/físicos deve ser em minúsculo, sem acentos, com underscores, e DEVE conter a palavra "_min_" ou "_max_" (ex: "limite_escoamento_min_mpa", "carbono_max_percentual"). Se a norma define uma FAIXA (um mínimo e um máximo) para o mesmo elemento, você DEVE criar DUAS chaves separadas (ex: "columbio_min_percentual" e "columbio_max_percentual"). NUNCA combine min e max na mesma chave.
4. Crie APENAS os grupos (mecanicos, quimicos, fisicos) que fazem sentido para a norma solicitada.
5. DEVOLVA APENAS O JSON, sem nenhuma formatação ou texto explicativo ao redor.`;
1. USE APENAS NÚMEROS (floats/ints) nos valores. NUNCA use strings como "0.25".
2. Estruture as matrizes de limite químico considerando as faixas de espessura de acordo com a norma.
3. Inclua a chave tolerancias_analise_produto_astma6 se aplicável.
4. DEVOLVA APENAS O JSON, sem nenhuma formatação ou texto explicativo ao redor.`;
try {
let resultJson = '';
+217
View File
@@ -0,0 +1,217 @@
import { ReportData, Standard, ComplianceItem } from '../types';
/**
* Extracts a numeric value from a string like "0.22%", "415 MPa", etc.
*/
function extractNumber(val: string): number | null {
if (!val) return null;
const match = val.match(/-?\d+(\.\d+)?/);
return match ? parseFloat(match[0]) : null;
}
/**
* Parses the product string to try and find a thickness in mm.
* E.g., "CHAPA 50MM", "PLATE 25.4 MM"
*/
function parseThickness(productDesc: string): number {
if (!productDesc) return 0;
const match = productDesc.match(/(\d+(\.\d+)?)\s*(mm)/i);
if (match) return parseFloat(match[1]);
return 0; // Default or unknown
}
function normalizeElementName(element: string): string {
const map: Record<string, string> = {
'c': 'carbono',
'carbon': 'carbono',
'carbono': 'carbono',
'mn': 'manganes',
'manganese': 'manganes',
'manganes': 'manganes',
'si': 'silicio',
'silicon': 'silicio',
'silicio': 'silicio',
'p': 'fosforo',
'phosphorus': 'fosforo',
'fosforo': 'fosforo',
's': 'enxofre',
'sulfur': 'enxofre',
'enxofre': 'enxofre',
'cu': 'cobre',
'copper': 'cobre',
'cobre': 'cobre',
'ni': 'niquel',
'nickel': 'niquel',
'niquel': 'niquel',
'cr': 'cromo',
'chromium': 'cromo',
'cromo': 'cromo',
'mo': 'molibdenio',
'molybdenum': 'molibdenio',
'molibdenio': 'molibdenio',
'v': 'vanadio',
'vanadium': 'vanadio',
'vanadio': 'vanadio',
'nb': 'niobio',
'columbium': 'niobio',
'niobium': 'niobio',
'niobio': 'niobio',
'ti': 'titanio',
'titanium': 'titanio',
'titanio': 'titanio',
'al': 'aluminio',
'aluminum': 'aluminio',
'aluminio': 'aluminio'
};
const lower = element.toLowerCase().trim();
// Remove content in parentheses, e.g. "C (Carbon)" -> "c"
const clean = lower.replace(/\(.*?\)/g, '').trim();
return map[clean] || clean;
}
function getTolerance(elementName: string, productVal: number, thickness: number, norm: Standard): number {
if (!norm.tolerancias_analise_produto_astma6) return 0;
const tolRules = norm.tolerancias_analise_produto_astma6[elementName];
if (!tolRules) return 0;
if (tolRules.tabela && tolRules.tabela.length > 0) {
for (const faixa of tolRules.tabela) {
if (faixa.espessura_ate_mm && thickness <= faixa.espessura_ate_mm) {
return faixa.tolerancia_mais || 0;
}
if (faixa.espessura_maior_que_mm && thickness > faixa.espessura_maior_que_mm) {
return faixa.tolerancia_mais || 0;
}
if (faixa.limite_corrida_ate && productVal <= faixa.limite_corrida_ate) {
return faixa.tolerancia_mais || 0;
}
}
}
return tolRules.tolerancia_mais || 0;
}
export function validateCertificateAgainstStandard(extractedReport: ReportData, standard: Standard): ReportData {
const report = JSON.parse(JSON.stringify(extractedReport)) as ReportData;
const thickness = parseThickness(report.identification.product);
let allOk = true;
// Encontrar limites baseados no produto (simplificado: pegamos o primeiro grupo que atende a espessura, ou o flat limits)
let activeLimits: Record<string, number | null> = {};
if (standard.limites_quimicos_corrida) {
const grupos = Object.values(standard.limites_quimicos_corrida)[0] || [];
for (const limite of grupos) {
if (!limite.espessura_max_mm || thickness <= limite.espessura_max_mm) {
// Encontrou a faixa certa
for (const [key, val] of Object.entries(limite)) {
if (typeof val === 'number') {
activeLimits[key] = val;
}
}
break; // Pega o primeiro correspondente
}
}
} else if (standard.parametros_controle?.quimicos) {
activeLimits = { ...standard.parametros_controle.quimicos };
}
// Validate Chemical
if (report.compliance.chemical) {
for (const item of report.compliance.chemical) {
if (!item.element) continue;
const normElement = normalizeElementName(item.element);
const val = extractNumber(item.certificate);
if (val !== null) {
// Look for max limit
const maxLimit = activeLimits[`${normElement}_max`] || activeLimits[normElement];
const minLimit = activeLimits[`${normElement}_min`];
let status: 'OK' | 'FALHA' = 'OK';
let normStr = [];
// Product tolerance
const tolerance = getTolerance(normElement, maxLimit as number || 0, thickness, standard);
if (minLimit !== undefined && minLimit !== null) {
normStr.push(`Min: ${minLimit}`);
if (val < (minLimit - tolerance)) status = 'FALHA';
}
if (maxLimit !== undefined && maxLimit !== null) {
normStr.push(`Max: ${maxLimit}`);
if (val > (maxLimit + tolerance)) status = 'FALHA';
}
if (normStr.length > 0) {
item.norm = normStr.join(' - ');
} else {
item.norm = 'Não especificado';
}
item.status = status;
if (status === 'FALHA') allOk = false;
}
}
}
// Validate Mechanical
const mechLimits = standard.parametros_controle?.mecanicos || {};
if (report.compliance.mechanical) {
for (const item of report.compliance.mechanical) {
if (!item.property) continue;
const val = extractNumber(item.certificate);
if (val !== null) {
const propLower = item.property.toLowerCase();
let maxLimit: number | null = null;
let minLimit: number | null = null;
if (propLower.includes('yield') || propLower.includes('escoamento')) {
minLimit = mechLimits['limite_escoamento_min'] || mechLimits['yield_strength_min'] || null;
maxLimit = mechLimits['limite_escoamento_max'] || mechLimits['yield_strength_max'] || null;
} else if (propLower.includes('tensile') || propLower.includes('resistencia')) {
minLimit = mechLimits['limite_resistencia_min'] || mechLimits['tensile_strength_min'] || null;
maxLimit = mechLimits['limite_resistencia_max'] || mechLimits['tensile_strength_max'] || null;
} else if (propLower.includes('elongation') || propLower.includes('alongamento')) {
minLimit = mechLimits['alongamento_min'] || mechLimits['elongation_min'] || null;
}
let status: 'OK' | 'FALHA' = 'OK';
let normStr = [];
if (minLimit !== null) {
normStr.push(`Min: ${minLimit}`);
if (val < minLimit) status = 'FALHA';
item.differencePercentage = `${val >= minLimit ? '+' : ''}${(((val - minLimit) / minLimit) * 100).toFixed(1)}%`;
}
if (maxLimit !== null) {
normStr.push(`Max: ${maxLimit}`);
if (val > maxLimit) status = 'FALHA';
}
if (normStr.length > 0) {
item.norm = normStr.join(' - ');
} else {
item.norm = 'Não especificado';
}
item.status = status;
if (status === 'FALHA') allOk = false;
}
}
}
report.compliance.status = allOk ? 'CONFORME' : 'NÃO CONFORME';
if (!allOk) {
report.compliance.nonComplianceReason = 'Valores fora dos limites da norma técnica considerando bitola e tolerâncias de produto.';
}
return report;
}
+43
View File
@@ -46,3 +46,46 @@ export interface ReportData {
overPerformance: OverPerformanceItem[];
equivalents: EquivalentItem[];
}
// --- NEW STANDARD DEFINITIONS ---
export interface ToleranciaTabela {
limite_corrida_ate?: number;
espessura_ate_mm?: number;
espessura_maior_que_mm?: number;
tolerancia_mais?: number;
tolerancia_menos?: number;
}
export interface RegraTolerancia {
regra?: string;
tabela?: ToleranciaTabela[];
tolerancia_mais?: number;
tolerancia_menos?: number;
}
export interface ToleranciasAnaliseProduto {
[elemento: string]: RegraTolerancia;
}
export interface LimitesQuimicos {
espessura_max_mm?: number;
grupos_astm_a6?: number[];
nota_exemplo?: string;
[elemento: string]: number | string | number[] | undefined;
}
export interface Standard {
id_norma: string;
codigo: string;
grau?: string;
produto_especifico?: string;
limites_quimicos_corrida?: {
[grupo_produto: string]: LimitesQuimicos[];
};
tolerancias_analise_produto_astma6?: ToleranciasAnaliseProduto;
parametros_controle?: {
mecanicos?: Record<string, number | null>;
quimicos?: Record<string, number | null>;
fisicos?: Record<string, number | null>;
};
}