UNPKG

jobnimbus-mcp-client

Version:

JobNimbus MCP Client - Connect Claude Desktop to remote JobNimbus MCP server

299 lines 14.3 kB
/** * Analyze Job Attachments Tool * * Advanced file content analysis for JobNimbus attachments. * Supports PDFs, images, and documents with AI-powered extraction. * * CAPABILITIES: * - PDF Analysis: Text extraction, document type detection, key info extraction * - Image Analysis: Visual description, damage detection, severity assessment * - Multi-file processing: Parallel analysis with consolidated results * - Smart filtering: By file type, size limits, date ranges */ import { BaseTool } from '../baseTool.js'; export class AnalyzeJobAttachmentsTool extends BaseTool { get definition() { return { name: 'analyze_job_attachments', description: 'Analyze content of job attachments (PDFs, images, documents) with AI-powered extraction. Extracts text from PDFs, analyzes images visually, detects amounts, dates, and key information. Perfect for insurance scopes, estimates, damage photos, and contracts.', inputSchema: { type: 'object', properties: { job_id: { type: 'string', description: 'Job ID or number to analyze attachments from', }, file_types: { type: 'array', items: { type: 'string' }, description: 'Filter by file types (e.g., ["pdf", "jpg", "png"]). If omitted, analyzes all supported types.', }, max_files: { type: 'number', description: 'Maximum number of files to analyze (default: 10, max: 50). Processes newest files first.', default: 10, }, max_file_size_mb: { type: 'number', description: 'Skip files larger than this size in MB (default: 10, max: 50)', default: 10, }, include_text_extraction: { type: 'boolean', description: 'Extract full text from PDFs and documents (default: true)', default: true, }, include_visual_analysis: { type: 'boolean', description: 'Perform AI visual analysis on images (default: true)', default: true, }, }, required: ['job_id'], }, }; } /** * Extract text from PDF buffer */ async extractPdfText(buffer) { try { // Convert buffer to string and try to extract text const text = buffer.toString('utf-8'); // Simple text extraction - look for readable text patterns const lines = text.split('\n').filter(line => { // Filter out binary/control characters const cleaned = line.replace(/[^\x20-\x7E]/g, ''); return cleaned.length > 3; }); return lines.join('\n').trim(); } catch (error) { return `[PDF text extraction failed: ${error instanceof Error ? error.message : 'unknown error'}]`; } } /** * Analyze image with AI (placeholder for future Claude Vision integration) */ analyzeImage(buffer, filename) { // For now, return basic info // TODO: Integrate Claude Vision API for actual image analysis const sizeKB = buffer.length / 1024; return `Image analysis: ${filename} (${sizeKB.toFixed(1)}KB). Visual analysis requires Claude Vision API integration. File is ready for processing.`; } /** * Extract key information from text using pattern matching */ extractKeyInfo(text) { const keyInfo = {}; // Extract amounts ($ patterns) const amountRegex = /\$[\d,]+\.?\d*/g; const amounts = text.match(amountRegex); if (amounts && amounts.length > 0) { keyInfo.amounts = [...new Set(amounts)].slice(0, 10); // Unique, max 10 } // Extract dates (various formats) const dateRegex = /\b\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4}\b|\b(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]* \d{1,2},? \d{4}\b/gi; const dates = text.match(dateRegex); if (dates && dates.length > 0) { keyInfo.dates = [...new Set(dates)].slice(0, 10); } // Extract potential entity names (capitalized words, 2-4 words) const entityRegex = /\b[A-Z][a-z]+ (?:[A-Z][a-z]+ ){0,2}(?:LLC|Inc|Corp|Co|Company)?\b/g; const entities = text.match(entityRegex); if (entities && entities.length > 0) { keyInfo.entities = [...new Set(entities)].slice(0, 5); } return keyInfo; } /** * Detect document type from filename and content */ detectDocumentType(filename, text) { const lower = filename.toLowerCase(); const textLower = text.toLowerCase(); // Check filename patterns if (lower.includes('estimate')) return 'estimate'; if (lower.includes('invoice') || lower.includes('bill')) return 'invoice'; if (lower.includes('contract') || lower.includes('agreement')) return 'contract'; if (lower.includes('scope') || lower.includes('insurance')) return 'insurance_scope'; if (lower.includes('supplement')) return 'supplement'; if (lower.includes('photo') || lower.includes('img') || lower.includes('pic')) return 'photo'; // Check content patterns if (textLower.includes('estimate') && textLower.includes('total')) return 'estimate'; if (textLower.includes('invoice') || textLower.includes('amount due')) return 'invoice'; if (textLower.includes('agreement') || textLower.includes('contract')) return 'contract'; if (textLower.includes('scope') || textLower.includes('insurance claim')) return 'insurance_scope'; return 'unknown'; } async execute(input, context) { const maxFiles = Math.min(input.max_files || 10, 50); const maxFileSizeMB = Math.min(input.max_file_size_mb || 10, 50); const includeText = input.include_text_extraction !== false; const includeVisual = input.include_visual_analysis !== false; try { // Step 1: Get job information const jobResponse = await this.client.get(context.apiKey, `jobs/${input.job_id}`); const job = jobResponse.data; // Step 2: Get attachments for this job const filesResponse = await this.client.get(context.apiKey, 'files', { size: 500, // Fetch more for filtering }); const allFiles = filesResponse.data?.files || []; const jobJnid = job.jnid || input.job_id; // Filter files for this job let jobFiles = allFiles.filter((file) => { if (file.primary?.id === jobJnid) return true; if (file.related && Array.isArray(file.related)) { return file.related.some((rel) => rel.id === jobJnid); } return false; }); // Apply file type filter if provided if (input.file_types && input.file_types.length > 0) { const allowedTypes = input.file_types.map(t => t.toLowerCase()); jobFiles = jobFiles.filter((file) => { const ext = file.filename?.split('.').pop()?.toLowerCase() || ''; return allowedTypes.includes(ext); }); } // Sort by date (newest first) and limit jobFiles.sort((a, b) => (b.date_created || 0) - (a.date_created || 0)); jobFiles = jobFiles.slice(0, maxFiles); // Step 3: Analyze each file const analyses = []; let totalSizeMB = 0; for (const file of jobFiles) { const sizeMB = (file.size || 0) / (1024 * 1024); totalSizeMB += sizeMB; const analysis = { filename: file.filename || 'unknown', file_type: file.content_type || 'unknown', size_mb: sizeMB.toFixed(2), url: file.url, analysis_status: 'skipped', }; // Skip if file too large if (sizeMB > maxFileSizeMB) { analysis.skip_reason = `File too large (${sizeMB.toFixed(1)}MB > ${maxFileSizeMB}MB limit)`; analyses.push(analysis); continue; } // Skip if no URL if (!file.url) { analysis.skip_reason = 'No download URL available'; analyses.push(analysis); continue; } try { // Download file const response = await fetch(file.url); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const buffer = Buffer.from(await response.arrayBuffer()); const fileExt = file.filename?.split('.').pop()?.toLowerCase() || ''; analysis.content_analysis = {}; // Analyze based on file type if (fileExt === 'pdf' && includeText) { // PDF Analysis const extractedText = await this.extractPdfText(buffer); const docType = this.detectDocumentType(file.filename, extractedText); const keyInfo = this.extractKeyInfo(extractedText); analysis.content_analysis = { document_type: docType, extracted_text: extractedText.length > 5000 ? extractedText.slice(0, 5000) + '...' : extractedText, text_preview: extractedText.slice(0, 500), key_information: keyInfo, }; } else if (['jpg', 'jpeg', 'png', 'gif'].includes(fileExt) && includeVisual) { // Image Analysis const visualDesc = this.analyzeImage(buffer, file.filename); const docType = this.detectDocumentType(file.filename, ''); analysis.content_analysis = { document_type: docType, visual_description: visualDesc, key_information: { detected_elements: ['Image analysis pending AI integration'], }, }; } else { analysis.skip_reason = `File type '${fileExt}' not supported for analysis`; analyses.push(analysis); continue; } analysis.analysis_status = 'success'; } catch (error) { analysis.analysis_status = 'error'; analysis.error = error instanceof Error ? error.message : 'Unknown error'; } analyses.push(analysis); } // Step 4: Generate summary const successfulAnalyses = analyses.filter(a => a.analysis_status === 'success'); const documentTypes = {}; const allAmounts = []; const allDates = []; for (const analysis of successfulAnalyses) { const docType = analysis.content_analysis?.document_type || 'unknown'; documentTypes[docType] = (documentTypes[docType] || 0) + 1; if (analysis.content_analysis?.key_information?.amounts) { allAmounts.push(...analysis.content_analysis.key_information.amounts); } if (analysis.content_analysis?.key_information?.dates) { allDates.push(...analysis.content_analysis.key_information.dates); } } return { job_id: input.job_id, job_name: job.display_name || job.name || 'Unknown', job_number: job.number, analysis_summary: { total_files_found: jobFiles.length, files_analyzed: successfulAnalyses.length, files_skipped: analyses.filter(a => a.analysis_status === 'skipped').length, files_errored: analyses.filter(a => a.analysis_status === 'error').length, total_size_mb: totalSizeMB.toFixed(2), document_types: documentTypes, total_amounts_detected: allAmounts.length, unique_amounts: [...new Set(allAmounts)].slice(0, 10), total_dates_detected: allDates.length, unique_dates: [...new Set(allDates)].slice(0, 10), }, files: analyses, filters_applied: { file_types: input.file_types, max_files: maxFiles, max_file_size_mb: maxFileSizeMB, }, _notes: { text_extraction: includeText ? 'enabled' : 'disabled', visual_analysis: includeVisual ? 'enabled (basic mode - AI integration pending)' : 'disabled', recommendation: 'For full AI-powered image analysis, integrate Claude Vision API', }, }; } catch (error) { return { error: error instanceof Error ? error.message : 'Failed to analyze attachments', status: 'error', job_id: input.job_id, }; } } } //# sourceMappingURL=analyzeJobAttachments.js.map