UNPKG

defect-inspection-tools-mcp-server

Version:
1,152 lines 61.6 kB
import Typesense from "typesense"; import { MongoClient } from "mongodb"; import { isValidImoForCompany, updateTypesenseFilterWithCompanyImos, filterImoListForCompany } from "../utils/company-filtering.js"; /** * ============================================================================ * OLD IMPLEMENTATION (COMMENTED OUT - REPLACED BY OPTIMIZED VERSION) * ============================================================================ * The old implementation has been replaced with an optimized version that includes: * - In-memory caching with TTL (5 minutes) * - MongoDB connection pooling * - Set-based IMO lookups (O(1) instead of O(n)) * - Cached environment variables * * See: src/utils/company-filtering.ts for the optimized implementation * ============================================================================ */ // /** // * Fetch IMO numbers for a specific company from MongoDB // */ // async function fetchCompanyImoNumbers(companyName: string, dbName: string, mongoUri: string, collectionName: string = 'common_group_details'): Promise<string[]> { // if (!dbName || !mongoUri || !companyName) { // return []; // } // // const mongoClient = new MongoClient(mongoUri); // try { // await mongoClient.connect(); // const db = mongoClient.db(dbName); // const collection = db.collection(collectionName); // // const result = await collection.findOne( // { groupName: companyName }, // { projection: { imoList: 1, _id: 0 } } // ); // // if (!result || !result.imoList) { // return []; // } // // return result.imoList.map((imo: any) => String(imo)); // } catch (error) { // console.error(`Error fetching IMO numbers for company ${companyName}:`, error); // return []; // } finally { // await mongoClient.close(); // } // } // /** // * Check if IMO filtering should be bypassed for admin companies // */ // function shouldBypassImoFiltering(companyName: string): boolean { // const adminCompanies = ['admin', 'administrator', 'superadmin', 'syia']; // return adminCompanies.some(admin => // companyName.toLowerCase().includes(admin.toLowerCase()) // ); // } // /** // * Check if an IMO number is valid for the current company // */ // export async function isValidImoForCompany(imo: number | string, dbName?: string, mongoUri?: string): Promise<boolean> { // const companyName = process.env.COMPANY_NAME || ''; // // if (!companyName) { // return true; // If no company name, allow all // } // // // Bypass validation for "Synergy" company // if (companyName === "Synergy") { // return true; // } // // // Bypass validation for admin companies // if (shouldBypassImoFiltering(companyName)) { // return true; // } // // // Use GROUP_DETAILS if available, otherwise fallback to FLEET_DISTRIBUTION // const finalDbName = dbName || process.env.GROUP_DETAILS_DB_NAME || process.env.FLEET_DISTRIBUTION_DB_NAME || ''; // const finalMongoUri = mongoUri || process.env.GROUP_DETAILS_MONGO_URI || process.env.FLEET_DISTRIBUTION_MONGO_URI || ''; // // if (!finalDbName || !finalMongoUri) { // return true; // If no DB config, allow all // } // // const companyImos = await fetchCompanyImoNumbers(companyName, finalDbName, finalMongoUri); // const imoStr = String(imo); // // return companyImos.includes(imoStr); // } // /** // * Update Typesense filter with company IMO numbers for filtering // */ // export async function updateTypesenseFilterWithCompanyImos(filter: string, dbName?: string, mongoUri?: string): Promise<string> { // const companyName = process.env.COMPANY_NAME || ''; // // if (!companyName) { // return filter; // } // // // Bypass filtering for "Synergy" company // if (companyName === "Synergy") { // return filter; // } // // // Bypass filtering for admin companies // if (shouldBypassImoFiltering(companyName)) { // return filter; // } // // // Use GROUP_DETAILS if available, otherwise fallback to FLEET_DISTRIBUTION // const finalDbName = dbName || process.env.GROUP_DETAILS_DB_NAME || process.env.FLEET_DISTRIBUTION_DB_NAME || ''; // const finalMongoUri = mongoUri || process.env.GROUP_DETAILS_MONGO_URI || process.env.FLEET_DISTRIBUTION_MONGO_URI || ''; // // if (!finalDbName || !finalMongoUri) { // return filter; // } // // const companyImos = await fetchCompanyImoNumbers(companyName, finalDbName, finalMongoUri); // // if (companyImos.length === 0) { // return filter; // } // // const imoFilter = `imo:[${companyImos.join(",")}]`; // // if (filter && filter.trim()) { // return `${filter} && ${imoFilter}`; // } else { // return imoFilter; // } // } export async function getTypesenseClient() { // Validate required environment variables const host = process.env.TYPESENSE_HOST; const port = process.env.TYPESENSE_PORT; const protocol = process.env.TYPESENSE_PROTOCOL; const apiKey = process.env.TYPESENSE_API_KEY; if (!host || !port || !protocol || !apiKey) { throw new Error('Missing required Typesense environment variables'); } // Initialize Typesense client from scratch const typesenseConfig = { nodes: [ { host, port: Number(port), protocol } ], apiKey, connectionTimeoutSeconds: 10, // retryIntervalSeconds: 1.0, // numRetries: 3, // healthcheckIntervalSeconds: 30, // logLevel: 'debug' as 'debug' }; return new Typesense.Client(typesenseConfig); } export async function getComponentData(componentId, vesselComponentsDbName, vesselComponentsMongoUri, collectionName = 'vesselinfocomponents') { const match = componentId.match(/^(\d+)_(\d+)_(\d+)$/); if (!match) { return `⚠️ Invalid component_id format: ${componentId}`; } if (!vesselComponentsDbName || !vesselComponentsMongoUri || !collectionName) { throw new Error('Database name, MongoDB URI, and collection name are required '); } const [, componentNumber, questionNumber, imo] = match; const componentNo = `${componentNumber}_${questionNumber}_${imo}`; const mongoClient = new MongoClient(vesselComponentsMongoUri); await mongoClient.connect(); try { const db = mongoClient.db(vesselComponentsDbName); const collection = db.collection(collectionName); const doc = await collection.findOne({ componentNo }); if (!doc) { return `⚠️ No component found for ID: ${componentId}`; } if (!doc.data) { return "No data found in the table component"; } if (!doc.data.headers || !Array.isArray(doc.data.headers)) { return "No headers found in the table component"; } if (!doc.data.body || !Array.isArray(doc.data.body)) { return "No body data found in the table component"; } // Extract headers excluding lineitem const headers = doc.data.headers .filter((h) => h && h.name !== "lineitem") .map((h) => h.name); const rows = doc.data.body; // Build markdown table let md = "| " + headers.join(" | ") + " |\n"; md += "| " + headers.map(() => "---").join(" | ") + " |\n"; for (const row of rows) { const formattedRow = row .filter((cell) => cell && !cell.lineitem) // Exclude lineitem and null cells .map((cell) => { if (cell && cell.value && cell.link) { return `[${cell.value}](${cell.link})`; } else if (cell && cell.status && cell.color) { return cell.status; } return cell ? String(cell) : ''; }); md += "| " + formattedRow.join(" | ") + " |\n"; } return md; } catch (error) { // logger.error('Error getting component data:', error); throw new Error(`Error getting component data: ${error.message}`); } finally { await mongoClient.close(); } } export async function addComponentData(answer, imo, vesselComponentsDbName, vesselComponentsMongoUri) { const pattern = /httpsdev\.syia\.ai\/chat\/ag-grid-table\?component=(\d+_\d+)/g; const matches = Array.from(answer.matchAll(pattern)); // logger.info(`addComponentData called with IMO: ${imo}, matches found: ${matches.length}`); // logger.info(`Answer contains ag-grid URL: ${answer.includes('ag-grid-table')}`); let result = answer; for (const match of matches) { const component = match[1]; // logger.info(`Processing component: ${component}, full match: ${match[0]}`); try { const replacement = await getComponentData(`${component}_${imo}`, vesselComponentsDbName, vesselComponentsMongoUri, 'vesselinfocomponents'); // logger.info(`Component data retrieved, length: ${replacement.length}`); result = result.replace(match[0], replacement); } catch (error) { // logger.error('Error replacing component data:', error); } } // logger.info(`addComponentData returning result, length: ${result.length}`); return result; } export async function getVesselQnASnapshot(imo, questionNo) { try { const raw_snapshotUrl = process.env.SNAPSHOT_URL; // API endpoint const snapshotUrl = `${raw_snapshotUrl}/${imo}/${questionNo}`; const raw_jwtToken = process.env.JWT_TOKEN; // Authentication token const jwtToken = `Bearer ${raw_jwtToken}`; // Headers for the request const headers = { "Authorization": jwtToken }; // logger.info(`Fetching vessel QnA snapshot for IMO: ${imo}, Question: ${questionNo}`); const response = await fetch(snapshotUrl, { method: 'GET', headers }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); // Return resultData if it exists, otherwise return the full response if (data && typeof data === 'object' && "resultData" in data) { return data.resultData; } return data; } catch (error) { // logger.error(`Error fetching vessel QnA snapshot for IMO ${imo}, Question ${questionNo}:`, error); return null; } } export async function fetchQADetails(imo, qaId, vesselInfoDbName, vesselInfoMongoUri, collectionName = 'vesselinfos') { const mongoClient = new MongoClient(vesselInfoMongoUri); await mongoClient.connect(); try { const db = mongoClient.db(vesselInfoDbName); const collection = db.collection(collectionName); const query = { 'imo': parseInt(imo), 'questionNo': qaId }; const projection = { '_id': 0, 'imo': 1, 'vesselName': 1, 'refreshDate': 1, 'answer': 1 }; const mongoResult = await collection.findOne(query, { projection }); let res = mongoResult ? { imo: mongoResult.imo, vesselName: mongoResult.vesselName, refreshDate: mongoResult.refreshDate, answer: mongoResult.answer } : { imo: parseInt(imo), vesselName: null, refreshDate: null, answer: null }; // Format refresh date if it exists if (res.refreshDate && new Date(res.refreshDate).toString() !== 'Invalid Date') { res.refreshDate = new Date(res.refreshDate).toLocaleDateString('en-US', { day: 'numeric', month: 'short', year: 'numeric' }); } // Process answer with component data if it exists if (res.answer) { const vesselComponentsDbName = process.env.QA_DB_NAME || ""; const vesselComponentsMongoUri = process.env.QA_MONGO_URI || ""; res.answer = await addComponentData(res.answer, imo, vesselComponentsDbName, vesselComponentsMongoUri); } // Get vessel QnA snapshot link try { res.Artifactlink = await getVesselQnASnapshot(imo, qaId.toString()); } catch (error) { res.Artifactlink = null; } return res; } catch (error) { // logger.error('Error fetching QA details:', error); throw new Error(`Error fetching QA details: ${error.message}`); } finally { await mongoClient.close(); } } export async function fetchQADetailsAndCreateResponse(imo, questionNo, functionName, linkHeader, vesselInfoDbName, vesselInfoMongoUri, collectionName = 'vesselinfos') { if (!imo) { throw new Error("IMO is required"); } console.log("Mongo URI from fetchQADetailsAndCreateResponse: ", vesselInfoMongoUri); console.log("Mongo DB Name from fetchQADetailsAndCreateResponse: ", vesselInfoDbName); try { // Fetch QA details const result = await fetchQADetails(imo, questionNo, vesselInfoDbName, vesselInfoMongoUri, collectionName); const link = result.Artifactlink; const vesselName = result.vesselName; // Insert data link to MongoDB // await insertDataLinkToMongoDB(link, linkHeader, sessionId, imo, vesselName, vesselInfoDbName, vesselInfoMongoUri); // Get artifact data const artifactData = await getArtifact(functionName, link); // Create content responses with processed answer const artifactLinkText = link ? `\n\nArtifact Link: ${link}` : ""; const content = { type: "text", text: `${result.answer || "No data available"}${artifactLinkText}` }; const artifact = { type: "text", text: JSON.stringify(artifactData, null, 2) }; return { content: [content, artifact] }; } catch (error) { // logger.error(`Error in ${functionName}:`, error); throw new Error(`Error in ${functionName}: ${error.message}`); } } export async function getArtifact(toolName, link) { try { const timestamp = Math.floor(Date.now() / 1000); const artifact = { id: `msg_browser_${Math.random().toString(36).substring(2, 8)}`, parentTaskId: `task_${toolName}_${Math.random().toString(36).substring(2, 8)}`, timestamp, agent: { id: "agent_siya_browser", name: "SIYA", type: "qna" }, messageType: "action", action: { tool: "browser", operation: "browsing", params: { url: link, pageTitle: `Tool response for ${toolName}`, visual: { icon: "browser", color: "#2D8CFF" }, stream: { type: "vnc", streamId: "stream_browser_1", target: "browser" } } }, content: `Viewed page: ${toolName}`, artifacts: [ { id: `artifact_webpage_${Date.now()}_${Math.floor(Math.random() * 1000)}`, type: "browser_view", content: { url: link, title: toolName, screenshot: "", textContent: `Observed output of cmd \`${toolName}\` executed:`, extractedInfo: {} }, metadata: { domainName: "example.com", visitTimestamp: Date.now(), category: "web_page" } } ], status: "completed" }; return artifact; } catch (error) { // logger.error('Error getting artifact:', error); throw new Error(`Error getting artifact: ${error.message}`); } } export async function getVesselImoListFromFleet(fleetImo) { const dbName = process.env.GROUP_DETAILS_DB_NAME; const mongoUri = process.env.GROUP_DETAILS_MONGO_URI; const collectionName = "common_group_details"; if (!dbName || !mongoUri || !collectionName) { throw new Error('Database name, MongoDB URI, and collection name are required for fleet operations'); } const mongoClient = new MongoClient(mongoUri); await mongoClient.connect(); try { const db = mongoClient.db(dbName); const collection = db.collection(collectionName); const fleetDoc = await collection.findOne({ imo: fleetImo }); if (fleetDoc && fleetDoc.imoList && Array.isArray(fleetDoc.imoList)) { return fleetDoc.imoList; } return []; } catch (error) { // logger.error(`Error fetching vessel IMO list for fleet ${fleetImo}:`, error); throw error; } finally { await mongoClient.close(); } } export function convertUnixDates(document) { // logger.debug(`Starting Unix date conversion for document with ${Object.keys(document).length} fields`); const result = { ...document }; const dateFields = ["reportDate", "closingDate", "targetDate", "inspectionTargetDate", "nextDueDate"]; // logger.debug(`Checking ${dateFields.length} potential date fields for Unix timestamp conversion`); let convertedCount = 0; for (const field of dateFields) { const value = result[field]; if (typeof value === "number" && Number.isFinite(value)) { const originalValue = value; result[field] = new Date(value * 1000).toISOString(); // logger.debug(`Converted field '${field}' from Unix timestamp ${originalValue} to ISO date: ${result[field]}`); convertedCount++; } } // logger.debug(`Unix date conversion completed - ${convertedCount} fields converted out of ${dateFields.length} checked fields`); return result; } export async function getDataLink(data) { try { const snapshotUrl = process.env.SNAPSHOT_URL || ""; const jwtToken = process.env.JWT_TOKEN || ""; const headers = { "Content-Type": "application/json", "Authorization": `Bearer ${jwtToken}` }; const payload = { data }; // Log URL and token (complete values) // logger.info(`Making request to URL: ${url}`); // logger.info(`Using JWT token: ${raw_jwtToken}`); const response = await fetch(snapshotUrl, { method: 'POST', headers, body: JSON.stringify(payload) }); // Log response status and headers // logger.info(`Response status: ${response.status} ${response.statusText}`); // logger.info(`Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`); if (!response.ok) { // logger.error(`HTTP error! status: ${response.status}, statusText: ${response.statusText}`); throw new Error(`HTTP error! status: ${response.status}`); } const result = await response.json(); // Log complete response data // logger.info(`Complete response data: ${JSON.stringify(result)}`); if (result.status === "OK") { // logger.info(`Successfully got data link: ${result.resultData}`); return result.resultData; } else { // logger.error(`Failed to get data link: Invalid response status - ${result.status}`); throw new Error('Failed to get data link: Invalid response status'); } } catch (error) { // logger.error('Error getting data link:', error); throw new Error(`Error getting data link: ${error.message}`); } } export async function processTypesenseResults(searchResult, toolName, title, linkHeader, artifactTitle, dbName, mongoUri) { // logger.info(`Starting processTypesenseResults for tool: ${toolName}, session: ${session_id}`); try { // Log input validation // logger.info(`Validating search results for ${toolName} - hits count: ${searchResult?.hits?.length || 0}`); if (!searchResult || !searchResult.hits || searchResult.hits.length === 0) { // logger.warn(`No search results found for ${toolName}`); return { content: [{ type: "text", text: "No records found for the specified criteria." }] }; } // logger.info(`Processing ${searchResult.hits.length} hits for ${toolName}`); // Process search results into the standard format const hits = searchResult.hits || []; // logger.info(`Starting document processing for ${hits.length} hits in ${toolName}`); const documents = await Promise.all(hits.map(async (hit, index) => { if (!hit.document) { // logger.warn(`Hit ${index} is missing document property in ${toolName}`); return {}; } // Create a shallow copy of the document const document = { ...hit.document }; // Remove embedding field to reduce response size if (document.embedding) { // logger.debug(`Removing embedding field from document ${index} in ${toolName}`); delete document.embedding; } // Convert Unix timestamps to readable dates // logger.debug(`Converting Unix dates for document ${index} in ${toolName}`); return await convertUnixDates(document); })); // logger.info(`Successfully processed ${documents.length} documents for ${toolName}`); // Get data link // logger.info(`Generating data link for ${toolName}`); const dataLink = await getDataLink(documents); // logger.info(`Data link generated successfully for ${toolName}: ${dataLink.substring(0, 50)}...`); // Get vessel name and IMO from hits let vesselName = null; let imo = null; // logger.info(`Extracting vessel information from search results for ${toolName}`); try { vesselName = searchResult.hits[0]?.document?.vesselName; imo = searchResult.hits[0]?.document?.imo; // logger.info(`Vessel info extracted for ${toolName} - Name: ${vesselName}, IMO: ${imo}`); } catch (error) { // logger.warn(`Could not get vessel name or IMO from hits in ${toolName}:`, error); } // Insert the data link to mongodb collection // logger.info(`Inserting data link to MongoDB for ${toolName}, session: ${session_id}`); // if (dbName && mongoUri) { // // await insertDataLinkToMongoDB(dataLink, linkHeader, session_id, imo || "", vesselName || "", dbName, mongoUri); // } // logger.info(`Data link successfully inserted to MongoDB for ${toolName}`); // Format results in the standard structure // logger.info(`Formatting results for ${toolName}`); const formattedResults = { found: searchResult.found || 0, out_of: searchResult.out_of || 0, page: searchResult.page || 1, hits: documents, artifactLink: dataLink }; // logger.info(`Results formatted successfully for ${toolName} - found: ${formattedResults.found}, out_of: ${formattedResults.out_of}`); // Get artifact data // logger.info(`Retrieving artifact data for ${toolName}`); const artifactData = await getArtifact(toolName, dataLink); // logger.info(`Artifact data retrieved successfully for ${toolName}`); // Create content response // logger.info(`Creating content response for ${toolName}`); const content_ = { type: "text", text: JSON.stringify(formattedResults, null, 2), title, format: "json" }; // Create artifact response // logger.info(`Creating artifact response for ${toolName}`); const artifact = { type: "text", text: JSON.stringify(artifactData, null, 2), title: artifactTitle || title, format: "json" }; // logger.info(`processTypesenseResults completed successfully for ${toolName}`); return { content: [content_, artifact] }; } catch (error) { // logger.error(`Error processing Typesense results for ${toolName}:`, error); throw new Error(`Error processing results: ${error.message}`); } } export async function exportDataForImoListGeneric(collectionName = 'vesselinfos', imoList, startDate, endDate, dateField, excludeFields = "", timestampFields = []) { try { const client = await getTypesenseClient(); const collection = client.collections(collectionName); const dateToTs = (dateStr) => { return Math.floor(new Date(dateStr).getTime() / 1000); }; const filterParts = [`imo:[${imoList.join(',')}]`]; if (startDate && dateField) { filterParts.push(`${dateField}:>=${dateToTs(startDate)}`); } if (endDate && dateField) { filterParts.push(`${dateField}:<=${dateToTs(endDate)}`); } const filterBy = filterParts.join(" && "); const query = { filter_by: filterBy, exclude_fields: excludeFields }; const exportResult = await collection.documents().export(query); let exportData; if (typeof exportResult === 'string') { exportData = exportResult; } else if (exportResult && typeof exportResult === 'object' && 'buffer' in exportResult) { exportData = new TextDecoder().decode(exportResult); } else { exportData = String(exportResult); } const documents = exportData .split('\n') .filter(line => line.trim()) .map(line => JSON.parse(line)); // Convert UNIX timestamps to readable date strings for (const doc of documents) { for (const field of timestampFields) { if (field in doc && typeof doc[field] === 'number') { try { doc[field] = new Date(doc[field] * 1000).toISOString().replace('T', ' ').substring(0, 19); } catch (err) { // Leave original value on error } } } } return documents; } catch (error) { // logger.error(`Error exporting data from '${collectionName}' collection:`, error); return []; } } export async function exportDefectsForImoList(imoList, startDate, endDate) { return exportDataForImoListGeneric('defect', imoList, startDate, endDate, 'reportDate', "embedding", ['reportDate']); } export async function get_tmsa_summary(arguments_) { const imo = arguments_.imo; if (!imo) { throw new Error("imo is required. Only the following properties are allowed: imo. Please check the tool schema for get_tmsa_summary for the allowed properties."); } // Validate IMO belongs to company const isValid = await isValidImoForCompany(imo); if (!isValid) { return { content: [{ type: "text", text: `Vessel with IMO ${imo} is not part of the current company. Access denied.` }] }; } return await fetchQADetailsAndCreateResponse(imo, 154, "get_tmsa_summary", "tmsa summary", process.env.QA_DB_NAME || '', process.env.QA_MONGO_URI || ''); } export async function get_fleet_vir_status_overview(arguments_) { const fleet_imo = arguments_.fleet_imo; if (!fleet_imo) { throw new Error("fleet_imo is required. Only the following properties are allowed: fleet_imo. Please check the tool schema for get_fleet_vir_status_overview for the allowed properties."); } // Validate IMO belongs to company const isValid = await isValidImoForCompany(fleet_imo); if (!isValid) { return { content: [{ type: "text", text: `Fleet ${fleet_imo} is not part of the current company. Access denied.` }] }; } return await fetchQADetailsAndCreateResponse(fleet_imo, 180, "get_fleet_vir_status_overview", "fleet vir status overview", process.env.QA_DB_NAME || '', process.env.QA_MONGO_URI || ''); } export async function get_fleet_last_internal_audit_summary(arguments_) { const fleet_imo = arguments_.fleet_imo; if (!fleet_imo) { throw new Error("fleet_imo is required. Only the following properties are allowed: fleet_imo. Please check the tool schema for get_fleet_last_internal_audit_summary for the allowed properties."); } // Validate IMO belongs to company const isValid = await isValidImoForCompany(fleet_imo); if (!isValid) { return { content: [{ type: "text", text: `Fleet ${fleet_imo} is not part of the current company. Access denied.` }] }; } return await fetchQADetailsAndCreateResponse(fleet_imo, 235, "get_fleet_last_internal_audit_summary", "fleet last internal audit summary", process.env.QA_DB_NAME || '', process.env.QA_MONGO_URI || ''); } export async function get_all_vessel_defects_records(arguments_) { const collection = "defect"; const imo = arguments_.imo; const startDate = arguments_.start_date; const endDate = arguments_.end_date; if (!imo) { throw new Error("imo is required. Only the following properties are allowed: imo, start_date, end_date. Please check the tool schema for get_all_vessel_defects_records for the allowed properties."); } // Validate IMO belongs to company const isValid = await isValidImoForCompany(imo); if (!isValid) { return { content: [{ type: "text", text: `Vessel with IMO ${imo} is not part of the current company. Access denied.` }] }; } try { const filterParts = [`imo:=${imo}`]; const dateToTs = (dateStr) => { return Math.floor(new Date(dateStr).getTime() / 1000); }; if (startDate) { const startTs = dateToTs(startDate); filterParts.push(`reportDate:>=${startTs}`); } if (endDate) { const endTs = dateToTs(endDate); filterParts.push(`reportDate:<=${endTs}`); } const filterBy = filterParts.join(" && "); const query = { filter_by: filterBy, exclude_fields: "_id,docId,fleetId,vesselId,fleetManagerId,technicalSuperintendentId,id" }; const client = await getTypesenseClient(); const exportResult = await client.collections(collection).documents().export(query); let exportData; if (typeof exportResult === 'string') { exportData = exportResult; } else if (exportResult && typeof exportResult === 'object' && 'buffer' in exportResult) { exportData = new TextDecoder().decode(exportResult); } else { exportData = String(exportResult); } const documents = exportData .split('\n') .filter(line => line.trim()) .map(line => JSON.parse(line)); // Convert timestamps to date strings for specified fields const dateFields = [ 'inspectionTargetDate', 'reportDate', 'closingDate', 'targetDate', 'nextDueDate', 'extendedDate' ]; for (const doc of documents) { for (const field of dateFields) { if (field in doc && typeof doc[field] === 'number') { try { doc[field] = new Date(doc[field] * 1000).toISOString().replace('T', ' ').substring(0, 19); } catch (error) { // Keep original value if conversion fails } } } } // Create a mock search result structure for processTypesenseResults const mockSearchResult = { found: documents.length, out_of: documents.length, page: 1, hits: documents.map((doc, index) => ({ document: doc, text_match: 1, id: index })) }; const linkHeader = `Defect export for IMO ${imo}`; const title = `Exported defect records for IMO ${imo}`; const artifactTitle = `Defect export artifact for IMO ${imo}`; return await processTypesenseResults(mockSearchResult, "get_all_vessel_defects_records", title, linkHeader, artifactTitle); } catch (error) { // logger.error(`Error executing defect export: ${error}`); throw new Error(`Error exporting defect records: ${String(error)}`); } } export async function get_summary_of_defects(arguments_) { const imo = arguments_.imo; if (!imo) { throw new Error("imo is required. Only the following properties are allowed: imo. Please check the tool schema for get_summary_of_defects for the allowed properties."); } // Validate IMO belongs to company const isValid = await isValidImoForCompany(imo); if (!isValid) { return { content: [{ type: "text", text: `Vessel with IMO ${imo} is not part of the current company. Access denied.` }] }; } return await fetchQADetailsAndCreateResponse(imo, 55, "get_summary_of_defects", "summary of defects", process.env.QA_DB_NAME || '', process.env.QA_MONGO_URI || ''); } // Fleet Operations Tools export async function export_fleet_defects(arguments_) { const fleetImo = arguments_.fleet_imo; const startDate = arguments_.start_date; const endDate = arguments_.end_date; if (!fleetImo) { throw new Error("fleet_imo is required. Only the following properties are allowed: fleet_imo, start_date, end_date. Please check the tool schema for export_fleet_defects for the allowed properties."); } try { // Step 1: Get vessel IMO list from fleet // logger.info(`Step 1: Getting vessel IMO list for fleet IMO ${fleetImo}`); const vesselImoList = await getVesselImoListFromFleet(fleetImo); if (!vesselImoList || vesselImoList.length === 0) { return { content: [{ type: "text", text: `No vessels found for fleet IMO: ${fleetImo}` }] }; } // Step 1.5: Filter out vessels that are not part of the current company // This fetches company IMOs once (cached) and filters the list efficiently using O(1) Set lookups const filteredVesselImoList = await filterImoListForCompany(vesselImoList); if (!filteredVesselImoList || filteredVesselImoList.length === 0) { return { content: [{ type: "text", text: `No vessels from fleet IMO ${fleetImo} belong to your company. Access denied.` }] }; } // logger.info(`Found ${filteredVesselImoList.length} vessels (of ${vesselImoList.length} total) in fleet IMO ${fleetImo} that belong to the company`); // Step 2: Export defects for all vessels in the fleet // logger.info(`Step 2: Exporting defects for ${filteredVesselImoList.length} vessels`); const defectDocuments = await exportDefectsForImoList(filteredVesselImoList, startDate, endDate); if (!defectDocuments || defectDocuments.length === 0) { return { content: [{ type: "text", text: `No defects found for fleet IMO ${fleetImo} with the specified criteria (searched ${filteredVesselImoList.length} vessels)` }] }; } // logger.info(`Found ${defectDocuments.length} defect records for ${filteredVesselImoList.length} vessels in fleet IMO ${fleetImo}`); // Step 3: Convert to CSV (commented out for JSON efficiency testing) // logger.info(`Step 3: Converting ${defectDocuments.length} records to CSV`); // const csvData = convertToCSV(defectDocuments); // Create a mock search result structure for processTypesenseResults const mockSearchResult = { found: defectDocuments.length, out_of: defectDocuments.length, page: 1, hits: defectDocuments.map((doc, index) => ({ document: doc, text_match: 1, id: index })) }; const linkHeader = `Fleet defects export for fleet IMO ${fleetImo}`; const title = `Fleet Defects Export (JSON) - IMO ${fleetImo}`; const artifactTitle = `Fleet Defects Export Artifact - IMO ${fleetImo}`; return await processTypesenseResults(mockSearchResult, "export_fleet_defects", title, linkHeader, artifactTitle); } catch (error) { // logger.error(`Error exporting fleet defects for IMO ${fleetImo}: ${error}`); throw new Error(`Error exporting fleet defects: ${String(error)}`); } } export async function get_sire_reports_from_ocimf(arguments_) { const imo = arguments_.imo; if (!imo) { throw new Error("imo is required. Only the following properties are allowed: imo. Please check the tool schema for get_sire_reports_from_ocimf for the allowed properties."); } // Validate IMO belongs to company const isValid = await isValidImoForCompany(imo); if (!isValid) { return { content: [{ type: "text", text: `Vessel with IMO ${imo} is not part of the current company. Access denied.` }] }; } return await fetchQADetailsAndCreateResponse(imo, 131, "get_sire_reports_from_ocimf", "sire reports from ocimf", process.env.QA_DB_NAME || '', process.env.QA_MONGO_URI || ''); } export async function fetch_fleet_historical_sire_observations(arguments_) { const fleet_imo = arguments_.fleet_imo; if (!fleet_imo) { throw new Error("fleet_imo is required. Only the following properties are allowed: fleet_imo. Please check the tool schema for fetch_fleet_historical_sire_observations for the allowed properties."); } // Validate IMO belongs to company const isValid = await isValidImoForCompany(fleet_imo); if (!isValid) { return { content: [{ type: "text", text: `Fleet ${fleet_imo} is not part of the current company. Access denied.` }] }; } return await fetchQADetailsAndCreateResponse(fleet_imo, 129, "fetch_fleet_historical_sire_observations", "fleet historical sire observations", process.env.QA_DB_NAME || '', process.env.QA_MONGO_URI || ''); } export async function get_cdi_reports_from_ocimf(arguments_) { const imo = arguments_.imo; if (!imo) { throw new Error("imo is required. Only the following properties are allowed: imo. Please check the tool schema for get_cdi_reports_from_ocimf for the allowed properties."); } // Validate IMO belongs to company const isValid = await isValidImoForCompany(imo); if (!isValid) { return { content: [{ type: "text", text: `Vessel with IMO ${imo} is not part of the current company. Access denied.` }] }; } return await fetchQADetailsAndCreateResponse(imo, 133, "get_cdi_reports_from_ocimf", "cdi reports from ocimf", process.env.QA_DB_NAME || '', process.env.QA_MONGO_URI || ''); } export async function get_vir_status_overview(arguments_) { const imo = arguments_.imo; if (!imo) { throw new Error("imo is required. Only the following properties are allowed: imo. Please check the tool schema for get_vir_status_overview for the allowed properties."); } // Validate IMO belongs to company const isValid = await isValidImoForCompany(imo); if (!isValid) { return { content: [{ type: "text", text: `Vessel with IMO ${imo} is not part of the current company. Access denied.` }] }; } // Fetch both question 118 and 120 const [result120, result118] = await Promise.all([ fetchQADetailsAndCreateResponse(imo, 120, "get_vir_status_overview", "vir status overview (question 120)", process.env.QA_DB_NAME || '', process.env.QA_MONGO_URI || ''), fetchQADetailsAndCreateResponse(imo, 118, "get_vir_status_overview", "vir status overview (question 118)", process.env.QA_DB_NAME || '', process.env.QA_MONGO_URI || '') ]); // Combine both results ensuring both Q118 and Q120 surface reliably const extractText = (res) => { if (!res || !Array.isArray(res.content)) return ""; const textBlock = res.content.find((c) => c?.type === "text" && typeof c.text === "string"); return textBlock?.text || ""; }; const text120 = extractText(result120); const text118 = extractText(result118); const combinedText = `${text120 || "No data available"}\n${text118 || "No data available"}`; // Preserve non-text artifacts (e.g., snapshot links) from both responses const extractArtifacts = (res) => { if (!res || !Array.isArray(res.content)) return []; // Return all content items except the first text item (which we've already extracted) return res.content.slice(1).filter((c) => c !== null && c !== undefined); }; const artifacts = [ ...extractArtifacts(result120), ...extractArtifacts(result118) ]; return { content: [ { type: "text", text: combinedText }, ...artifacts ] }; } export async function get_internal_audit_summary(arguments_) { const imo = arguments_.imo; if (!imo) { throw new Error("imo is required. Only the following properties are allowed: imo. Please check the tool schema for get_internal_audit_summary for the allowed properties."); } // Validate IMO belongs to company const isValid = await isValidImoForCompany(imo); if (!isValid) { return { content: [{ type: "text", text: `Vessel with IMO ${imo} is not part of the current company. Access denied.` }] }; } return await fetchQADetailsAndCreateResponse(imo, 94, "get_internal_audit_summary", "internal audit summary", process.env.QA_DB_NAME || '', process.env.QA_MONGO_URI || ''); } export async function get_psc_inspection_defects(arguments_) { const imo = arguments_.imo; if (!imo) { throw new Error("imo is required. Only the following properties are allowed: imo. Please check the tool schema for get_psc_inspection_defects for the allowed properties."); } // Validate IMO belongs to company const isValid = await isValidImoForCompany(imo); if (!isValid) { return { content: [{ type: "text", text: `Vessel with IMO ${imo} is not part of the current company. Access denied.` }] }; } return await fetchQADetailsAndCreateResponse(imo, 46, "get_psc_inspection_defects", "psc inspection defects", process.env.QA_DB_NAME || '', process.env.QA_MONGO_URI || ''); } export async function get_fleet_sire_cdi_inspection_status(arguments_) { const fleet_imo = arguments_.fleet_imo; if (!fleet_imo) { throw new Error("fleet_imo is required. Only the following properties are allowed: fleet_imo. Please check the tool schema for get_fleet_sire_cdi_inspection_status for the allowed properties."); } // Validate IMO belongs to company const isValid = await isValidImoForCompany(fleet_imo); if (!isValid) { return { content: [{ type: "text", text: `Fleet ${fleet_imo} is not part of the current company. Access denied.` }] }; } return await fetchQADetailsAndCreateResponse(fleet_imo, 134, "get_fleet_sire_cdi_inspection_status", "fleet sire cdi inspection status", process.env.QA_DB_NAME || '', process.env.QA_MONGO_URI || ''); } export async function get_pending_sire_operator_comments(arguments_) { const imo = arguments_.imo; try { // Import MongoDB client const { MongoClient } = await import('mongodb'); // Get MongoDB connection details from environment const mongoUri = process.env.CONSUMPTION_LOG_MONGO_URI; const dbName = process.env.CONSUMPTION_LOG_DB_NAME; if (!mongoUri) { throw new Error("MongoDB connection URI not configured"); } const client = new MongoClient(mongoUri); await client.connect(); const db = client.db(dbName); const collection = db.collection('sire_pending_inspections'); // Collection for pending operator comments // Build query based on input parameters let query = {}; if (imo) { query.imo = parseInt(imo.toString()); } // Execute query - if IMO provided, get latest record by inspectionDate let documents = []; try { if (imo) { documents = await collection .find(query) .sort({ inspectionDate: -1 }) .limit(1) .toArray(); } else { documents = await collection.find(query).toArray(); } } catch (queryError) { // logger.error(`Query error for IMO ${imo}:`, queryError); // Database query failed - throw error since workflow did not complete throw new Error(`Failed to query pending SIRE operator comments: ${queryError.message || 'Database query failed'}`); } await client.close(); if (documents.length === 0) { if (imo) { return { content: [{ type: "text", text: `Vessel with IMO ${imo} is not in the list. Either inspection is not completed yet or operator comments have already been submitted.` }] }; } else { return { content: [{ type: "text", text: "No vessels currently awaiting operator comments - all caught up!" }] }; } } // Process documents and calculate comment deadline status const currentDate = new Date(); const COMMENT_DEADLINE_DAYS = 14; const processedData = documents.map(doc => { const inspectionDate = new Date(doc.inspectionDate.$date || doc.inspectionDate); const daysSinceInspection = Math.floor((currentDate.getTime() - inspectionDate.getTime()) / (1000 * 60 * 60 * 24)); const daysRemaining = COMMENT_DEADLINE_DAYS - daysSinceInspection; let status; if (daysSinceInspection > COMMENT_DEADLINE_DAYS) { status = 'OVERDUE'; } else if (daysRemaining <= 2) { status = 'URGENT'; } else if (daysRemaining <= 5) { status = 'DUE_SOON'; } else { status = 'WITHIN_DEADLINE'; } return { vesselName: doc.vesselName, imo: doc.imo, inspectingCompany: doc.inspectingCompany, inspectionDate: inspectionDate.toISOString().split('T')[0], // Format as YYYY-MM-DD daysSinceInspection, daysRemaining, status, autoPublicationDate: new Date(doc.autoPublicationDate.$date || doc.autoPublicationDate).toISOString().split('T')[0], observationsCount: doc.observations?.length || 0, url: doc.url }; }); // Sort by urgency (overdue first, then by days remaining) processedData.sort((a, b) => { const priorityOrder = { 'OVERDUE': 0, 'URGENT': 1, 'DUE_SOON': 2, 'WITHIN_DEADLINE': 3 }; if (a.status !== b.status) { return priorityOrder[a.status] - priorityOrder[b.status]; } return a.daysRemaining - b.daysRemaining; }); // Calculate summary statistics const urgentCount = processedData.filter(v => v.status === 'URGENT' || v.status === 'OVERDUE').length; const overdueCount = processedData.filter(v => v.status === 'OVERDUE').length; // Format response based on single vessel or multiple vessels let responseText = ''; if (imo) { // Single vessel details const vessel = processedData[0]; let statu