jobnimbus-mcp-client
Version:
JobNimbus MCP Client - Connect Claude Desktop to remote JobNimbus MCP server
299 lines • 14.3 kB
JavaScript
/**
* 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