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
JavaScript
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