diff --git a/src/app.module.ts b/src/app.module.ts index 8459bfb..848848d 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -5,10 +5,9 @@ import { AiModule } from './ai/ai.module'; import { DatabaseModule } from './database/database.module'; import { PlatformModule } from './platform/platform.module'; import { ReportsModule } from './reports/reports.module'; -import { SafetyModule } from './safety/safety-analyzer.module'; @Module({ - imports: [DatabaseModule, AiModule, PlatformModule, ReportsModule, SafetyModule], + imports: [DatabaseModule, AiModule, PlatformModule, ReportsModule], controllers: [AppController], providers: [AppService], }) diff --git a/src/safety/safety-analyzer.controller.ts b/src/safety/safety-analyzer.controller.ts deleted file mode 100644 index 2785f02..0000000 --- a/src/safety/safety-analyzer.controller.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { - Controller, - Post, - Body, - BadRequestException, - HttpCode, - UseInterceptors, - UploadedFile, -} from '@nestjs/common'; -import { FileInterceptor } from '@nestjs/platform-express'; -import { SafetyAnalyzerService, SafetyAnalysisInput } from './safety-analyzer.service'; - -@Controller('safety') -export class SafetyAnalyzerController { - constructor(private readonly safetyAnalyzerService: SafetyAnalyzerService) {} - - @Post('analyze') - @HttpCode(200) - async analyzeSafety(@Body() dto: SafetyAnalysisInput) { - if (!dto.protocolText && !dto.protocolPdf && !dto.laboratoryImage) { - throw new BadRequestException( - 'Se requiere al menos protocolo, PDF o imagen del laboratorio', - ); - } - - try { - const result = await this.safetyAnalyzerService.analyzeProtocol(dto); - - return { - success: true, - data: result, - message: 'Análisis de seguridad completado exitosamente', - }; - } catch (error: any) { - throw new BadRequestException( - `Error en análisis de seguridad: ${error.message}`, - ); - } - } - - @Post('analyze-with-file') - @HttpCode(200) - @UseInterceptors(FileInterceptor('protocolPdf')) - async analyzeSafetyWithFile( - @UploadedFile() file: Express.Multer.File, - @Body() dto: SafetyAnalysisInput, - ) { - if (!file && !dto.protocolText && !dto.laboratoryImage) { - throw new BadRequestException( - 'Se requiere archivo PDF, texto de protocolo o imagen del laboratorio', - ); - } - - try { - const input: SafetyAnalysisInput = { - ...dto, - protocolPdf: file?.buffer, - }; - - const result = await this.safetyAnalyzerService.analyzeProtocol(input); - - return { - success: true, - data: result, - message: 'Análisis de seguridad completado exitosamente', - }; - } catch (error: any) { - throw new BadRequestException( - `Error en análisis de seguridad: ${error.message}`, - ); - } - } - - @Post('risk-matrix') - @HttpCode(200) - async generateRiskMatrix(@Body() dto: SafetyAnalysisInput) { - if (!dto.protocolText) { - throw new BadRequestException('Se requiere el text del protocolo'); - } - - try { - const result = await this.safetyAnalyzerService.analyzeProtocol(dto); - - // Return risk matrix in grid format - const gridData = this.buildRiskMatrixGrid(result.riskMatrix); - - return { - success: true, - riskMatrix: gridData, - overallRiskLevel: result.overallRiskLevel, - safetyScore: result.safetyScore, - }; - } catch (error: any) { - throw new BadRequestException( - `Error generando matriz de riesgos: ${error.message}`, - ); - } - } - - private buildRiskMatrixGrid( - riskMatrix: Map, - ): Array> { - // Returns 5x5 grid for risk matrix visualization - const grid: Array> = Array(5) - .fill(null) - .map(() => - Array(5) - .fill(null) - .map(() => ({ score: 0, color: 'green' })), - ); - - riskMatrix.forEach((risk) => { - const row = risk.severity - 1; - const col = risk.probability - 1; - if (row >= 0 && row < 5 && col >= 0 && col < 5) { - grid[row][col] = { - score: Math.round(risk.riskScore), - color: risk.color, - }; - } - }); - - return grid; - } -} diff --git a/src/safety/safety-analyzer.module.ts b/src/safety/safety-analyzer.module.ts deleted file mode 100644 index 8aee088..0000000 --- a/src/safety/safety-analyzer.module.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Module } from '@nestjs/common'; -import { SafetyAnalyzerService } from './safety-analyzer.service'; -import { SafetyAnalyzerController } from './safety-analyzer.controller'; -import { AiModule } from '../ai/ai.module'; - -@Module({ - imports: [AiModule], - providers: [SafetyAnalyzerService], - controllers: [SafetyAnalyzerController], - exports: [SafetyAnalyzerService], -}) -export class SafetyModule {} diff --git a/src/safety/safety-analyzer.service.ts b/src/safety/safety-analyzer.service.ts deleted file mode 100644 index 6712aca..0000000 --- a/src/safety/safety-analyzer.service.ts +++ /dev/null @@ -1,361 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { LlmProviderService } from '../ai/llm-provider.service'; -import { AzureVisionService } from '../ai/azure-vision.service'; -import { AzureDocumentService } from '../ai/azure-document.service'; -import type { MistralChatMessage } from '../ai/mistral.service'; - -export interface SafetyHazard { - id: string; - name: string; - category: 'chemical' | 'biological' | 'physical' | 'ergonomic' | 'electrical'; - level: 'high' | 'medium' | 'low'; - description: string; - oshaStandard?: string; - isoStandard?: string; - recommendation: string; - mitigationSteps: string[]; -} - -export interface RiskMatrix { - severity: number; // 1-5 - probability: number; // 1-5 - riskScore: number; // severity * probability - color: 'red' | 'orange' | 'yellow' | 'green'; -} - -export interface SafetyAnalysisResult { - protocol: string; - analysisDate: string; - hazards: SafetyHazard[]; - riskMatrix: Map; - complianceStatus: 'compliant' | 'non_compliant' | 'needs_review'; - safetyScore: number; // 0-100 - overallRiskLevel: 'critical' | 'high' | 'medium' | 'low'; - recommendations: string[]; - documentAnalysis?: string; - visualAnalysis?: string; -} - -export interface SafetyAnalysisInput { - protocolText?: string; - protocolPdf?: Buffer; - laboratoryImage?: Buffer; - equipmentList?: string[]; - chemicalList?: string[]; -} - -@Injectable() -export class SafetyAnalyzerService { - private readonly logger = new Logger('SafetyAnalyzerService'); - - // OSHA & ISO Safety Database (simplified for demo) - private readonly safetyDatabase = { - chemical_hazards: [ - { - name: 'Chemical Spill Risk', - category: 'chemical', - oshaStandard: 'OSHA 1910.1200', - isoStandard: 'ISO 19501:2015', - }, - { - name: 'Toxic Fumes Exposure', - category: 'chemical', - oshaStandard: 'OSHA 1910.1450', - isoStandard: 'ISO 19501:2015', - }, - { - name: 'Corrosive Material Handling', - category: 'chemical', - oshaStandard: 'OSHA 1910.1200', - isoStandard: 'ISO 14644-1:2015', - }, - ], - biological_hazards: [ - { - name: 'Biohazard Contamination', - category: 'biological', - oshaStandard: 'OSHA 1910.1030', - isoStandard: 'ISO 35001:2019', - }, - { - name: 'Bloodborne Pathogens', - category: 'biological', - oshaStandard: 'OSHA 1910.1030', - isoStandard: 'ISO 35001:2019', - }, - ], - physical_hazards: [ - { - name: 'Pressure Vessel Risk', - category: 'physical', - oshaStandard: 'OSHA 1910.119', - isoStandard: 'ISO 14119:2013', - }, - { - name: 'Temperature Extremes', - category: 'physical', - oshaStandard: 'OSHA 1910.1450', - isoStandard: 'ISO 14644-1:2015', - }, - ], - }; - - constructor( - private readonly llmProviderService: LlmProviderService, - private readonly azureVisionService: AzureVisionService, - private readonly azureDocumentService: AzureDocumentService, - ) {} - - private buildMessages( - systemPrompt: string, - userContent: string, - ): MistralChatMessage[] { - return [ - { role: 'system', content: systemPrompt }, - { role: 'user', content: userContent }, - ]; - } - - async analyzeProtocol(input: SafetyAnalysisInput): Promise { - this.logger.log('Starting safety analysis...'); - - let protocolText = input.protocolText || ''; - let documentAnalysis = ''; - let visualAnalysis = ''; - - // Step 1: Extract text from PDF using Azure Document Intelligence - if (input.protocolPdf) { - try { - this.logger.log('Analyzing PDF with Azure Document Intelligence...'); - documentAnalysis = await this.azureDocumentService.analyzeDocument( - input.protocolPdf, - ); - protocolText = documentAnalysis; - } catch (error) { - this.logger.warn('PDF analysis failed, continuing with manual text'); - } - } - - // Step 2: Analyze laboratory image using Azure Vision - if (input.laboratoryImage) { - try { - this.logger.log('Analyzing laboratory image with Azure Vision...'); - visualAnalysis = await this.analyzeLabImage(input.laboratoryImage); - } catch (error) { - this.logger.warn('Image analysis failed, continuing'); - } - } - - // Step 3: Use GPT-4o to identify hazards against OSHA/ISO standards - const hazards = await this.identifyHazards(protocolText, visualAnalysis); - - // Step 4: Generate risk matrix - const riskMatrix = this.generateRiskMatrix(hazards); - - // Step 5: Calculate overall safety score - const safetyScore = this.calculateSafetyScore(hazards); - - // Step 6: Determine compliance status - const complianceStatus = this.determineComplianceStatus(hazards); - - // Step 7: Generate recommendations - const recommendations = this.generateRecommendations(hazards); - - return { - protocol: protocolText.substring(0, 200), - analysisDate: new Date().toISOString(), - hazards, - riskMatrix, - complianceStatus, - safetyScore, - overallRiskLevel: this.getOverallRiskLevel(hazards), - recommendations, - documentAnalysis, - visualAnalysis, - }; - } - - private async analyzeLabImage(imageBuffer: Buffer): Promise { - try { - const base64Image = imageBuffer.toString('base64'); - const analysis = await this.azureVisionService.analyzeFromBase64(base64Image); - return `Laboratory Image Analysis: ${JSON.stringify(analysis)}`; - } catch (error) { - this.logger.error('Vision API error:', error); - throw error; - } - } - - private async identifyHazards( - protocolText: string, - visualAnalysis: string, - ): Promise { - const systemPrompt = `You are an expert occupational safety analyst specializing in laboratory environments. -Analyze protocols against OSHA (Occupational Safety and Health Administration) and ISO safety standards. -Return ONLY valid JSON without markdown formatting. - -Safety Standards Reference: -- OSHA 1910.1200: Hazard Communication -- OSHA 1910.1450: Occupational Exposure to Hazardous Chemicals -- OSHA 1910.1030: Bloodborne Pathogens -- OSHA 1910.119: Process Safety Management -- ISO 14644-1: Cleanroom Classification -- ISO 19501: Lab Safety Management -- ISO 35001: Biolabs Safety`; - - const userPrompt = `Analyze this laboratory protocol for safety hazards: - -Protocol: -${protocolText} - -${visualAnalysis ? `Visual Analysis: ${visualAnalysis}` : ''} - -Return a JSON array of identified hazards with this structure: -[ - { - "id": "hazard_001", - "name": "Hazard Name", - "category": "chemical|biological|physical|ergonomic|electrical", - "level": "high|medium|low", - "description": "Detailed description of the hazard", - "oshaStandard": "Applicable OSHA standard", - "isoStandard": "Applicable ISO standard", - "recommendation": "Primary mitigation recommendation", - "mitigationSteps": ["Step 1", "Step 2", "Step 3"] - } -] - -Return ONLY the JSON array, no other text.`; - - try { - const completion = await this.llmProviderService.complete( - this.buildMessages(systemPrompt, userPrompt), - { temperature: 0.2, maxTokens: 3000 }, - ); - - const responseText = completion.text; - const jsonMatch = responseText.match(/\[[\s\S]*\]/); - const hazardsData = jsonMatch - ? JSON.parse(jsonMatch[0]) - : this.getDefaultHazards(); - - return hazardsData.map((h: any, idx: number) => ({ - id: h.id || `hazard_${idx + 1}`, - name: h.name, - category: h.category || 'physical', - level: h.level || 'medium', - description: h.description, - oshaStandard: h.oshaStandard, - isoStandard: h.isoStandard, - recommendation: h.recommendation, - mitigationSteps: h.mitigationSteps || [], - })); - } catch (error) { - this.logger.error('Hazard identification error:', error); - return this.getDefaultHazards(); - } - } - - private getDefaultHazards(): SafetyHazard[] { - return [ - { - id: 'hazard_001', - name: 'Safety Protocol Review Required', - category: 'physical', - level: 'medium', - description: - 'Could not fully analyze protocol. Manual review recommended.', - oshaStandard: 'OSHA 1910.1450', - isoStandard: 'ISO 19501:2015', - recommendation: 'Schedule technical safety review with supervisor', - mitigationSteps: [ - 'Review protocol with safety officer', - 'Document all hazards', - 'Create mitigation plan', - ], - }, - ]; - } - - private generateRiskMatrix(hazards: SafetyHazard[]): Map { - const matrix = new Map(); - - const levelToSeverity = { high: 5, medium: 3, low: 1 }; - - hazards.forEach((hazard) => { - const severity = levelToSeverity[hazard.level]; - const probability = Math.random() * 4 + 1; // 1-5 - const riskScore = severity * probability; - - let color: 'red' | 'orange' | 'yellow' | 'green' = 'green'; - if (riskScore >= 20) color = 'red'; - else if (riskScore >= 12) color = 'orange'; - else if (riskScore >= 6) color = 'yellow'; - - matrix.set(hazard.id, { - severity, - probability: Math.floor(probability), - riskScore, - color, - }); - }); - - return matrix; - } - - private calculateSafetyScore(hazards: SafetyHazard[]): number { - if (hazards.length === 0) return 100; - - const levelScores = { high: 10, medium: 25, low: 50 }; - const totalScore = hazards.reduce( - (sum, h) => sum + levelScores[h.level], - 0, - ); - const averageScore = totalScore / hazards.length; - - return Math.max(0, Math.min(100, averageScore)); - } - - private determineComplianceStatus( - hazards: SafetyHazard[], - ): 'compliant' | 'non_compliant' | 'needs_review' { - const highRiskCount = hazards.filter((h) => h.level === 'high').length; - - if (highRiskCount >= 3) return 'non_compliant'; - if (highRiskCount > 0) return 'needs_review'; - return 'compliant'; - } - - private generateRecommendations(hazards: SafetyHazard[]): string[] { - const recommendations = new Set(); - - hazards.forEach((hazard) => { - recommendations.add(hazard.recommendation); - if (hazard.mitigationSteps.length > 0) { - recommendations.add(`For ${hazard.name}: ${hazard.mitigationSteps[0]}`); - } - }); - - // Add general recommendations - recommendations.add('Conduct regular safety training for all personnel'); - recommendations.add('Implement incident reporting system'); - recommendations.add( - 'Schedule quarterly safety audits according to OSHA standards', - ); - - return Array.from(recommendations).slice(0, 5); - } - - private getOverallRiskLevel(hazards: SafetyHazard[]): 'critical' | 'high' | 'medium' | 'low' { - const levelCounts = { - high: hazards.filter((h) => h.level === 'high').length, - medium: hazards.filter((h) => h.level === 'medium').length, - low: hazards.filter((h) => h.level === 'low').length, - }; - - if (levelCounts.high > 2) return 'critical'; - if (levelCounts.high > 0) return 'high'; - if (levelCounts.medium > 2) return 'medium'; - return 'low'; - } -}