UNPKG

syia-mcp-vessel-accounts

Version:

MCP server for vessel account management including EyeShare API integration, vessel expenses, and purchase orders

929 lines 43.4 kB
import { logger } from "../utils/logger.js"; import { eyeShareApi } from "../utils/api.js"; import { mongodbTools } from "./mongodb.js"; export class ToolHandler { constructor(server, samplingHandler) { this.server = server; this.samplingHandler = samplingHandler; } async handleCallTool(name, arguments_) { logger.info(`Handling tool call: ${name}`, { arguments: arguments_ }); try { switch (name) { case "get_vessels": return await this.getVessels(arguments_); case "search_invoices": return await this.searchInvoices(arguments_); case "download_attachment": return await this.downloadAttachment(arguments_); // MongoDB Database Tools case "vessel_expenses": return await this.handleVesselExpenses(arguments_); case "vessel_expenses_previous_year": return await this.handleVesselExpensesPreviousYear(arguments_); case "purchase_orders": return await this.handlePurchaseOrders(arguments_); case "get_purchase_order_by_invoice": return await this.getPurchaseOrderByInvoice(arguments_); case "generate_committed_cost_report": return await this.generateCommittedCostReport(arguments_); case "check_report_status": return await this.checkReportStatus(arguments_); // AI Sampling-based Analysis Tools case "analyze_vessel_expenses": return await this.analyzeVesselExpenses(arguments_); case "analyze_vessel_invoices": return await this.analyzeVesselInvoices(arguments_); case "analyze_purchase_orders": return await this.analyzePurchaseOrders(arguments_); case "summarize_committed_cost_report": return await this.summarizeCommittedCostReport(arguments_); case "detect_data_anomalies": return await this.detectDataAnomalies(arguments_); default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { logger.error(`Error handling tool call ${name}:`, error); throw error; } } async getVessels(arguments_) { logger.info("Getting vessels..."); const vessels = await eyeShareApi.getVessels(); return [{ type: "text", text: JSON.stringify(vessels, null, 2) }]; } async searchInvoices(arguments_) { logger.info("Searching invoices...", arguments_); const searchRequest = { Page: { Skip: arguments_.skip || 0, Limit: arguments_.limit || 10000 }, Filters: { Equality: [], Contains: [], OpenInterval: [], ClosedInterval: [], Regex: [], Arrays: [], SubSet: [], TextIndex: [], StartsWith: [], WildCard: [], Features: { ExtendedAccess: true, View: "search" } }, Sort: [], CountOnly: false }; // If invoiceId is provided, search for specific invoice if (arguments_.invoiceId) { searchRequest.Filters.Equality.push({ Field: "Id", StringValue: arguments_.invoiceId, ExactMatch: true }); } // Otherwise, add vessel filter if provided else if (arguments_.vesselKey || arguments_.vesselCode) { const vesselCode = arguments_.vesselKey || arguments_.vesselCode; searchRequest.Filters.SubSet.push({ FilterSet: { Equality: [], Contains: [], OpenInterval: [], ClosedInterval: [], Regex: [], Arrays: [], SubSet: [], TextIndex: [], StartsWith: [ { Field: "Head.Ship.Key", Value: vesselCode, CaseSensitive: false, AllowStartWithWildcard: false }, { Field: "Head.Ship.Description", Value: vesselCode, CaseSensitive: false, AllowStartWithWildcard: false } ], WildCard: [] }, Or: true }); } // Add credit note filter if provided if (arguments_.creditNote !== undefined) { searchRequest.Filters.Equality.push({ Field: "Head.CreditNote", BoolValue: arguments_.creditNote }); } // Add vessel code filter if provided (CompanyCode field) if (arguments_.vesselCode) { searchRequest.Filters.Contains.push({ Field: "CompanyCode", StringList: [arguments_.vesselCode] }); } // Add date range filter if provided if (arguments_.fromDate || arguments_.toDate) { const dateField = arguments_.dateField || "Head.InvoiceDate"; searchRequest.Filters.ClosedInterval.push({ Field: dateField, FromDateTimeValue: arguments_.fromDate || "", ToDateTimeValue: arguments_.toDate || "" }); } // Add amount range filter if provided if (arguments_.minAmount || arguments_.maxAmount) { searchRequest.Filters.ClosedInterval.push({ Field: "Head.TotalAmount", FromDateTimeValue: arguments_.minAmount?.toString() || "", ToDateTimeValue: arguments_.maxAmount?.toString() || "" }); } // Add status filter if provided if (arguments_.status) { searchRequest.Filters.Equality.push({ Field: "Head.Status", StringValue: arguments_.status, ExactMatch: true }); } // Add supplier filter if provided if (arguments_.supplier) { searchRequest.Filters.Contains.push({ Field: "Head.Supplier.Name", StringValue: arguments_.supplier, ExactMatch: false }); } // Add currency filter if provided if (arguments_.currency) { searchRequest.Filters.SubSet.push({ FilterSet: { Equality: [{ Field: "Head.Currency.Key", StringValue: arguments_.currency, ExactMatch: false }], Contains: [], OpenInterval: [], ClosedInterval: [], Regex: [], Arrays: [], SubSet: [], TextIndex: [], StartsWith: [], WildCard: [] } }); } // Add urgent filter if provided if (arguments_.urgent !== undefined) { searchRequest.Filters.SubSet.push({ FilterSet: { Equality: [{ Field: "Head.Urgent", StringValue: arguments_.urgent ? "YES" : "NO" }], Contains: [], OpenInterval: [], ClosedInterval: [], Regex: [], Arrays: [], SubSet: [], TextIndex: [], StartsWith: [], WildCard: [] }, Or: true }); } // Add invoice number filter if provided if (arguments_.invoiceNumber) { searchRequest.Filters.StartsWith.push({ Field: "Head.InvoiceNumber", Value: arguments_.invoiceNumber, AllowStartWithWildcard: false }); } const result = await eyeShareApi.searchInvoices(searchRequest); return [{ type: "text", text: JSON.stringify(result, null, 2) }]; } async downloadAttachment(arguments_) { logger.info("Downloading attachment...", arguments_); if (!arguments_.attachmentId || !arguments_.documentId) { throw new Error("attachmentId and documentId are required"); } const attachmentData = await eyeShareApi.getAttachment(arguments_.attachmentId, arguments_.documentId, arguments_.version || 0); // Convert to base64 for safe transmission const base64Data = attachmentData.toString('base64'); const attachmentInfo = { attachmentId: arguments_.attachmentId, documentId: arguments_.documentId, version: arguments_.version || 0, data: base64Data, size: attachmentData.length }; return [{ type: "text", text: JSON.stringify(attachmentInfo, null, 2) }]; } // MongoDB Database Tool Handlers async handleVesselExpenses(arguments_) { const vesselTool = mongodbTools.find(tool => tool.name === 'vessel_expenses'); if (!vesselTool) { throw new Error('Vessel expenses tool not found'); } const result = await vesselTool.execute(arguments_); return result.content; } async handleVesselExpensesPreviousYear(arguments_) { const vesselTool = mongodbTools.find(tool => tool.name === 'vessel_expenses_previous_year'); if (!vesselTool) { throw new Error('Vessel expenses previous year tool not found'); } const result = await vesselTool.execute(arguments_); return result.content; } async handlePurchaseOrders(arguments_) { const purchaseOrderTool = mongodbTools.find(tool => tool.name === 'purchase_orders'); if (!purchaseOrderTool) { throw new Error('Purchase orders tool not found'); } const result = await purchaseOrderTool.execute(arguments_); return result.content; } async getPurchaseOrderByInvoice(arguments_) { logger.info("Getting purchase order by invoice...", arguments_); logger.info("Arguments received:", arguments_); const extractPoLinesOnly = arguments_.extractPoLinesOnly === true; // Default to false const extractPurchaseOrdersOnly = arguments_.extractPurchaseOrdersOnly !== false && !extractPoLinesOnly; // Default to true, but false if extractPoLinesOnly is true logger.info(`extractPoLinesOnly: ${extractPoLinesOnly}, extractPurchaseOrdersOnly: ${extractPurchaseOrdersOnly}`); // Check if this is parallel processing (multiple invoices) if (arguments_.invoiceConfigs && Array.isArray(arguments_.invoiceConfigs)) { if (arguments_.invoiceConfigs.length === 0) { throw new Error("invoiceConfigs array cannot be empty"); } // Validate each config for (const config of arguments_.invoiceConfigs) { if (!config.invoiceId || !config.vesselCode) { throw new Error("Each invoiceConfig must have invoiceId and vesselCode"); } } const maxWorkers = Math.min(arguments_.maxWorkers || 100, 100); logger.info(`Starting parallel processing for ${arguments_.invoiceConfigs.length} invoices with ${maxWorkers} workers`); const result = await eyeShareApi.getPurchaseOrdersByInvoicesParallel(arguments_.invoiceConfigs, maxWorkers); // Apply clean format if extractPurchaseOrdersOnly is true if (extractPurchaseOrdersOnly) { logger.info(`Applying clean format transformation. extractPurchaseOrdersOnly: ${extractPurchaseOrdersOnly}`); logger.info(`Result structure:`, { resultKeys: Object.keys(result), resultsLength: result.results?.length }); // Transform results to clean format const cleanResults = result.results.map((invoiceResult) => { const allPoLines = []; // Extract PoLines from all purchase orders for this invoice if (invoiceResult.purchaseOrders && Array.isArray(invoiceResult.purchaseOrders)) { invoiceResult.purchaseOrders.forEach((po) => { if (po.PoLines && Array.isArray(po.PoLines)) { allPoLines.push(...po.PoLines); } }); } return { invoiceId: invoiceResult.invoiceId, vesselCode: invoiceResult.vesselCode || "BWTR", purchaseOrdersCount: invoiceResult.purchaseOrders?.length || 0, PoLines: allPoLines }; }); const response = { summary: result.summary, results: cleanResults, errors: result.errors.length > 0 ? result.errors : undefined }; return [{ type: "text", text: JSON.stringify(response, null, 2) }]; } // Format the response for better readability (original nested format) const response = { summary: result.summary, results: result.results, errors: result.errors.length > 0 ? result.errors : undefined }; return [{ type: "text", text: JSON.stringify(response, null, 2) }]; } // Single invoice processing (original functionality) if (!arguments_.invoiceId || !arguments_.vesselCode) { throw new Error("For single invoice processing: invoiceId and vesselCode are required. For parallel processing: use invoiceConfigs array."); } const result = await eyeShareApi.getPurchaseOrderByInvoice(arguments_.invoiceId, arguments_.vesselCode); // Extract only PoLines if requested if (extractPoLinesOnly) { const purchaseOrders = result.PurchaseOrders || []; const allPoLines = []; // Extract PoLines from all purchase orders purchaseOrders.forEach((po) => { if (po.PoLines && Array.isArray(po.PoLines)) { allPoLines.push(...po.PoLines); } }); const response = { invoiceId: arguments_.invoiceId, vesselCode: arguments_.vesselCode, poLinesCount: allPoLines.length, poLines: allPoLines }; return [{ type: "text", text: JSON.stringify(response, null, 2) }]; } // Extract only PurchaseOrders if requested if (extractPurchaseOrdersOnly) { const purchaseOrders = result.PurchaseOrders || []; // Extract all PoLines from all purchase orders and flatten them const allPoLines = []; purchaseOrders.forEach((po) => { if (po.PoLines && Array.isArray(po.PoLines)) { allPoLines.push(...po.PoLines); } }); const response = { invoiceId: arguments_.invoiceId, vesselCode: arguments_.vesselCode, purchaseOrdersCount: purchaseOrders.length, PoLines: allPoLines }; return [{ type: "text", text: JSON.stringify(response, null, 2) }]; } // Return full response if extraction is disabled return [{ type: "text", text: JSON.stringify(result, null, 2) }]; } async generateCommittedCostReport(arguments_) { logger.info("Generating committed cost report", { arguments: arguments_ }); const vesselCodeInput = arguments_.vesselCode; const endDate = arguments_.endDate || new Date().toISOString().split('T')[0]; // Parse vessel codes (single or comma-separated) const vesselCodes = vesselCodeInput.includes(',') ? vesselCodeInput.split(',').map(code => code.trim()) : [vesselCodeInput]; // Validate each vessel code for (const code of vesselCodes) { if (!code || !/^[A-Z]{4}$/.test(code)) { throw new Error(`Invalid vessel code: ${code}. Must be exactly 4 uppercase letters (e.g., BASC, BWET, GEVI, ACET)`); } } // If multiple vessels, return instructions for SIYA to make multiple calls if (vesselCodes.length > 1) { const response = { status: "multi_vessel_detected", message: `Multiple vessels detected: ${vesselCodes.join(', ')}. SIYA should make separate tool calls for each vessel to avoid timeout issues.`, vessel_codes: vesselCodes, recommended_approach: "Make parallel tool calls", instructions: vesselCodes.map(code => ({ toolName: "generate_committed_cost_report", vesselCode: code, endDate: endDate, outputPath: arguments_.outputPath })) }; return [{ type: "text", text: JSON.stringify(response, null, 2) }]; } // Single vessel processing const vesselCode = vesselCodes[0]; // Validate date format if (!/^\d{4}-\d{2}-\d{2}$/.test(endDate)) { throw new Error(`Invalid date format: ${endDate}. Must be YYYY-MM-DD format (e.g., 2025-07-20)`); } // Use provided outputPath, or try to detect SIYA workspace from common patterns let outputPath = arguments_.outputPath; if (!outputPath) { // Try to get SIYA workspace from environment if (process.env.SIYA_WORKSPACE) { outputPath = process.env.SIYA_WORKSPACE; } else { // Try to detect from PWD if it contains .siya/workspaces const pwd = process.env.PWD || process.cwd(); if (pwd.includes('.siya/workspaces')) { outputPath = pwd; } else { // Look for .siya workspace directories and use the most recent one const fs = await import('fs'); const path = await import('path'); const workspacesPath = path.join(process.env.HOME || '', '.siya', 'workspaces'); try { if (fs.existsSync(workspacesPath)) { const workspaceDirs = fs.readdirSync(workspacesPath, { withFileTypes: true }) .filter(dirent => dirent.isDirectory()) .map(dirent => ({ name: dirent.name, path: path.join(workspacesPath, dirent.name), mtime: fs.statSync(path.join(workspacesPath, dirent.name)).mtime })) .sort((a, b) => b.mtime.getTime() - a.mtime.getTime()); if (workspaceDirs.length > 0) { outputPath = workspaceDirs[0].path; logger.info(`Using most recent workspace: ${outputPath}`); } } } catch (error) { logger.warn('Could not access workspaces directory:', error); } // Final fallback to current directory if (!outputPath) { outputPath = process.cwd(); } } } } // If outputPath is ".", try to use SIYA workspace from environment first // This helps ensure files are saved in the user's current workspace if (outputPath === '.' && process.env.SIYA_WORKSPACE) { outputPath = process.env.SIYA_WORKSPACE; logger.info(`Using SIYA workspace path: ${outputPath}`); } else if (outputPath === '.' && process.env.PWD) { outputPath = process.env.PWD; logger.info(`Using PWD environment variable: ${outputPath}`); } else if (outputPath === '.') { outputPath = process.cwd(); logger.info(`Using current working directory: ${outputPath}`); } else { logger.info(`Using determined output path: ${outputPath}`); } try { // Import required modules const { spawn } = await import('child_process'); const path = await import('path'); const { fileURLToPath } = await import('url'); // Get the script path const __dirname = path.dirname(fileURLToPath(import.meta.url)); const scriptPath = path.join(__dirname, '..', 'resources', 'scripts', 'vessel_committed_cost_analyzer.py'); // Prepare environment variables from MCP config const env = { ...process.env, MONGODB_URI: process.env.MONGODB_URI || '', MONGODB_DATABASE: process.env.MONGODB_DATABASE || 'syia-etl-dev', EYESHARE_BASE_URL: process.env.EYESHARE_BASE_URL || '', EYESHARE_CLIENT_ID: process.env.EYESHARE_CLIENT_ID || '', EYESHARE_CLIENT_SECRET: process.env.EYESHARE_CLIENT_SECRET || '', EYESHARE_MODULE: process.env.EYESHARE_MODULE || 'purchaseorder' }; // Generate process ID for tracking const processId = `${vesselCode}_${new Date().toISOString().replace(/[:.]/g, '')}`; const absoluteOutputPath = path.isAbsolute(outputPath) ? outputPath : path.resolve(outputPath); // Start the process and wait for completion const pythonProcess = spawn('python3', [scriptPath, vesselCode, '--end-date', endDate, '--output-dir', absoluteOutputPath], { env: env, stdio: ['pipe', 'pipe', 'pipe'] // Capture stdout and stderr }); logger.info(`Started analysis for ${vesselCode} with process ID: ${processId}`); // Wait for process completion return new Promise((resolve, reject) => { let stdout = ''; let stderr = ''; pythonProcess.stdout?.on('data', (data) => { stdout += data.toString(); }); pythonProcess.stderr?.on('data', (data) => { stderr += data.toString(); }); pythonProcess.on('close', async (code) => { logger.info(`Analysis process completed with code: ${code}`); if (code === 0) { // Success - try to read the summary file try { const fs = await import('fs'); const files = fs.readdirSync(absoluteOutputPath); const summaryFiles = files.filter(file => file.startsWith(`${vesselCode}_Analysis_Summary_`) && file.endsWith('.json')); let summaryData = null; if (summaryFiles.length > 0) { const latestSummaryFile = summaryFiles.sort().reverse()[0]; const summaryPath = path.join(absoluteOutputPath, latestSummaryFile); const summaryContent = fs.readFileSync(summaryPath, 'utf8'); summaryData = JSON.parse(summaryContent); } const response = { status: "completed_successfully", message: `Vessel committed cost analysis completed successfully for ${vesselCode}`, vessel_code: vesselCode, end_date: endDate, output_location: absoluteOutputPath, summary: summaryData, process_output: stdout, timestamp: new Date().toISOString() }; resolve([{ type: "text", text: JSON.stringify(response, null, 2) }]); } catch (error) { logger.error("Error reading summary data:", error); resolve([{ type: "text", text: JSON.stringify({ status: "completed_with_warnings", message: `Analysis completed but summary data could not be read: ${error}`, vessel_code: vesselCode, process_output: stdout, error: stderr }, null, 2) }]); } } else { // Process failed logger.error(`Analysis failed with code ${code}: ${stderr}`); reject(new Error(`Analysis failed: ${stderr || 'Unknown error'}`)); } }); pythonProcess.on('error', (error) => { logger.error("Process error:", error); reject(error); }); }); } catch (error) { logger.error("Error starting committed cost report generation:", error); throw error; } } async checkReportStatus(arguments_) { logger.info("Checking report status", { arguments: arguments_ }); const vesselCode = arguments_.vesselCode; let outputPath = arguments_.outputPath; // Validate vessel code if (!vesselCode || !/^[A-Z]{4}$/.test(vesselCode)) { throw new Error(`Invalid vessel code: ${vesselCode}. Must be exactly 4 uppercase letters`); } // Determine output path (same logic as generate tool) if (!outputPath) { if (process.env.SIYA_WORKSPACE) { outputPath = process.env.SIYA_WORKSPACE; } else if (process.env.PWD && process.env.PWD.includes('.siya/workspaces')) { outputPath = process.env.PWD; } else { outputPath = process.cwd(); } } try { const fs = await import('fs'); const path = await import('path'); const absoluteOutputPath = path.isAbsolute(outputPath) ? outputPath : path.resolve(outputPath); // Check if directory exists if (!fs.existsSync(absoluteOutputPath)) { return [{ type: "text", text: JSON.stringify({ status: "directory_not_found", message: `Output directory does not exist: ${absoluteOutputPath}`, vessel_code: vesselCode, checked_path: absoluteOutputPath }, null, 2) }]; } // Look for report files matching the vessel code pattern const files = fs.readdirSync(absoluteOutputPath); const reportFiles = files.filter(file => file.startsWith(vesselCode) && file.includes('Cost') && file.endsWith('.xlsx')); // Look for summary JSON file const summaryFiles = files.filter(file => file.startsWith(`${vesselCode}_Analysis_Summary_`) && file.endsWith('.json')); // Read the most recent summary file if available let summaryData = null; if (summaryFiles.length > 0) { try { const latestSummaryFile = summaryFiles.sort().reverse()[0]; // Get most recent const summaryPath = path.join(absoluteOutputPath, latestSummaryFile); const summaryContent = fs.readFileSync(summaryPath, 'utf8'); summaryData = JSON.parse(summaryContent); logger.info(`Found summary data in: ${latestSummaryFile}`); } catch (error) { logger.warn(`Failed to read summary file: ${error}`); } } const allExpectedPatterns = [ `${vesselCode}_Complete_Committed_Cost_Analysis_`, `${vesselCode}_Committed_Cost_Final_`, `${vesselCode}_Vessel_Expenses_Final_`, `${vesselCode}_Purchase_Orders_Final_`, `${vesselCode}_PO_Lines_Final_` ]; // Check which types of reports are available const foundReportTypes = allExpectedPatterns.filter(pattern => reportFiles.some(file => file.startsWith(pattern))); // Get file details const fileDetails = reportFiles.map(file => { const filePath = path.join(absoluteOutputPath, file); const stats = fs.statSync(filePath); return { filename: file, size_bytes: stats.size, size_mb: Math.round(stats.size / 1024 / 1024 * 100) / 100, created: stats.birthtime.toISOString(), modified: stats.mtime.toISOString() }; }); const isComplete = foundReportTypes.length >= 4; // At least 4 main report types const response = { status: isComplete ? "reports_ready" : reportFiles.length > 0 ? "partially_ready" : "not_ready", message: isComplete ? `All committed cost reports are ready for vessel ${vesselCode}` : reportFiles.length > 0 ? `Some reports are ready for vessel ${vesselCode}, generation may still be in progress` : `No reports found yet for vessel ${vesselCode}, generation may still be in progress`, vessel_code: vesselCode, output_path: absoluteOutputPath, reports_found: reportFiles.length, expected_report_types: allExpectedPatterns.length, found_report_types: foundReportTypes.length, files: fileDetails, is_complete: isComplete, check_timestamp: new Date().toISOString(), summary: summaryData // Include the analysis summary data }; return [{ type: "text", text: JSON.stringify(response, null, 2) }]; } catch (error) { logger.error("Error checking report status:", error); throw error; } } // AI Sampling-based Analysis Methods async analyzeVesselExpenses(arguments_) { if (!this.samplingHandler) { throw new Error("Sampling capability not available. AI analysis requires sampling support."); } const { vesselCode, limit = 1000 } = arguments_; if (!vesselCode) { throw new Error("vesselCode is required for expense analysis"); } try { logger.info(`Getting vessel expenses for AI analysis: ${vesselCode}`); // Use existing vessel_expenses tool call const expenseResponse = await this.handleCallTool("vessel_expenses", { vesselCode, limit }); const expenseText = expenseResponse[0].text; // Extract data from the response text const dataMatch = expenseText.match(/Results:\n(.*)/s); if (!dataMatch) { return [{ type: "text", text: `No expense data found for vessel ${vesselCode}` }]; } const expenseData = JSON.parse(dataMatch[1]); if (!expenseData || expenseData.length === 0) { return [{ type: "text", text: `No expense data found for vessel ${vesselCode}` }]; } // Request AI analysis const analysis = await this.samplingHandler.summarizeVesselExpenses(expenseData, vesselCode); return [{ type: "text", text: analysis }]; } catch (error) { logger.error("Error in analyzeVesselExpenses:", error); throw error; } } async analyzeVesselInvoices(arguments_) { if (!this.samplingHandler) { throw new Error("Sampling capability not available. AI analysis requires sampling support."); } const { vesselCode, limit = 100, fromDate, toDate } = arguments_; if (!vesselCode) { throw new Error("vesselCode is required for invoice analysis"); } try { logger.info(`Getting vessel invoices for AI analysis: ${vesselCode}`); // Get invoice data using existing search_invoices functionality const searchParams = { vesselCode, limit }; if (fromDate) searchParams.fromDate = fromDate; if (toDate) searchParams.toDate = toDate; const invoiceData = await this.searchInvoices(searchParams); const invoices = JSON.parse(invoiceData[0].text); if (!invoices || invoices.length === 0) { return [{ type: "text", text: `No invoice data found for vessel ${vesselCode}` }]; } // Request AI analysis const analysis = await this.samplingHandler.analyzeInvoices(invoices, vesselCode); return [{ type: "text", text: `# AI Analysis: Vessel Invoices for ${vesselCode}\n\n${analysis}\n\n---\n*Analysis based on ${invoices.length} invoice records*` }]; } catch (error) { logger.error("Error in analyzeVesselInvoices:", error); throw error; } } async analyzePurchaseOrders(arguments_) { if (!this.samplingHandler) { throw new Error("Sampling capability not available. AI analysis requires sampling support."); } const { vesselCode, limit = 500 } = arguments_; if (!vesselCode) { throw new Error("vesselCode is required for purchase order analysis"); } try { logger.info(`Getting purchase orders for AI analysis: ${vesselCode}`); // Use existing purchase_orders tool call const poResponse = await this.handleCallTool("purchase_orders", { vesselCode, limit }); const poText = poResponse[0].text; // Extract data from the response text const dataMatch = poText.match(/Results:\n(.*)/s); if (!dataMatch) { return [{ type: "text", text: `No purchase order data found for vessel ${vesselCode}` }]; } const poData = JSON.parse(dataMatch[1]); if (!poData || poData.length === 0) { return [{ type: "text", text: `No purchase order data found for vessel ${vesselCode}` }]; } // Request AI analysis const analysis = await this.samplingHandler.analyzePurchaseOrders(poData, vesselCode); return [{ type: "text", text: analysis }]; } catch (error) { logger.error("Error in analyzePurchaseOrders:", error); throw error; } } async summarizeCommittedCostReport(arguments_) { if (!this.samplingHandler) { throw new Error("Sampling capability not available. AI analysis requires sampling support."); } const { vesselCode, outputPath = '.' } = arguments_; if (!vesselCode) { throw new Error("vesselCode is required for committed cost report summary"); } try { logger.info(`Summarizing committed cost reports for vessel: ${vesselCode}`); // Check if reports exist first const statusResponse = await this.handleCallTool("check_report_status", { vesselCode, outputPath }); const statusData = JSON.parse(statusResponse[0].text); if (!statusData.is_complete) { return [{ type: "text", text: `⚠️ Committed cost reports not found or incomplete for vessel ${vesselCode}.\n\nPlease run the 'generate_committed_cost_report' tool first and wait for completion before requesting summary.\n\nCurrent status: ${statusData.found_report_types}/${statusData.expected_report_types} reports found.` }]; } // Extract summary data from the status check const summaryData = statusData.summary; if (!summaryData) { return [{ type: "text", text: `Reports exist but summary data could not be extracted for vessel ${vesselCode}. Please regenerate reports.` }]; } // Generate AI-powered executive summary using the actual committed cost data const analysis = await this.samplingHandler.summarizeCommittedCostReports(summaryData, vesselCode); return [{ type: "text", text: analysis }]; } catch (error) { logger.error("Error in summarizeCommittedCostReport:", error); throw error; } } async detectDataAnomalies(arguments_) { if (!this.samplingHandler) { throw new Error("Sampling capability not available. AI analysis requires sampling support."); } const { vesselCode, dataType = "all", limit = 500 } = arguments_; if (!vesselCode) { throw new Error("vesselCode is required for anomaly detection"); } try { logger.info(`Detecting anomalies for vessel ${vesselCode}, data type: ${dataType}`); const analyses = []; // Analyze expenses if requested if (dataType === "expenses" || dataType === "all") { try { const expenseResponse = await this.handleCallTool("vessel_expenses", { vesselCode, limit }); const expenseDataMatch = expenseResponse[0].text.match(/Results:\n(.*)/s); if (expenseDataMatch) { const expenseData = JSON.parse(expenseDataMatch[1]); if (expenseData && expenseData.length > 0) { const expenseAnalysis = await this.samplingHandler.detectAnomalies(expenseData, "expenses", vesselCode); analyses.push(expenseAnalysis); } } } catch (error) { logger.warn("Could not analyze expense data for anomalies:", error); } } // Analyze invoices if requested if (dataType === "invoices" || dataType === "all") { try { const invoiceResponse = await this.searchInvoices({ vesselCode, limit }); const invoices = JSON.parse(invoiceResponse[0].text); if (invoices && invoices.length > 0) { const invoiceAnalysis = await this.samplingHandler.detectAnomalies(invoices, "invoices", vesselCode); analyses.push(invoiceAnalysis); } } catch (error) { logger.warn("Could not analyze invoice data for anomalies:", error); } } // Analyze purchase orders if requested if (dataType === "purchase_orders" || dataType === "all") { try { const poResponse = await this.handleCallTool("purchase_orders", { vesselCode, limit }); const poDataMatch = poResponse[0].text.match(/Results:\n(.*)/s); if (poDataMatch) { const poData = JSON.parse(poDataMatch[1]); if (poData && poData.length > 0) { const poAnalysis = await this.samplingHandler.detectAnomalies(poData, "purchase orders", vesselCode); analyses.push(poAnalysis); } } } catch (error) { logger.warn("Could not analyze purchase order data for anomalies:", error); } } if (analyses.length === 0) { return [{ type: "text", text: `No data found for anomaly detection for vessel ${vesselCode}` }]; } const combinedAnalysis = analyses.join("\n\n---\n\n"); return [{ type: "text", text: `# AI Anomaly Detection: Vessel ${vesselCode}\n\n${combinedAnalysis}` }]; } catch (error) { logger.error("Error in detectDataAnomalies:", error); throw error; } } } //# sourceMappingURL=index.js.map