UNPKG

pms-analysis-reports-mcp-server

Version:

PMS analysis reports server handling maintenance reports, equipment analysis, compliance tracking, and performance metrics with ERP access for data extraction

1,166 lines 150 kB
import { logger } from "../utils/logger.js"; import { getDatabase, getELTDatabase, getEngineDataDatabase } from "../utils/mongodb.js"; import { searchDocuments, typesenseClient } from "../utils/typesense.js"; import { getArtifact, getDataLink, insertDataLinkToMongodb } from '../utils/helpers.js'; import { parseDocumentLink } from '../utils/documentParser.js'; import { getMongoClient } from '../utils/mongodb.js'; import { ObjectId } from 'mongodb'; import markdown from '../utils/markdown.js'; const { markdownToHtmlLink } = markdown; import { pushToTypesense } from '../utils/typesense.js'; import { config } from "../utils/config.js"; // MongoDB database name const MONGODB_DATABASE = config.secondaryDbName; // Engine names mapping const ENGINE_NAMES_MAPPING = { "AE1": "Auxillary Engine 1", "AE2": "Auxillary Engine 2", "AE3": "Auxillary Engine 3", "AE4": "Auxillary Engine 4", "AE5": "Auxillary Engine 5", "ME": "Main Engine" }; // Helper functions function getDocumentsFromHits(hits) { return (hits || []).map((hit) => hit.document); } function stringToPromise(value) { return Promise.resolve(value ?? null); } async function getDataLinkHelper(documents) { return getDataLink(documents); } function getDataLinkWithDefault(dataLink) { return dataLink ?? "no_link"; } async function insertDataLinkToMongodbHelper(dataLink, linkHeader, sessionId, imo, vesselName) { if (!dataLink) return; await insertDataLinkToMongodb(dataLink, linkHeader, sessionId, String(imo), vesselName ?? undefined); } function getArtifactHelper(toolName, url) { return getArtifact(toolName, url ?? "no_link"); } async function handleReportLink(reportLink) { if (!reportLink) return null; try { return await Promise.resolve(parseDocumentLink(reportLink)); } catch (error) { return null; } } function convertPmsDates(doc) { // Convert date strings to Date objects for PMS collection if (!doc) return doc; const dateFields = ['jobDueDate', 'jobDoneDate', 'defermentRequestedDate', 'createdDate', 'lastModified']; const convertedDoc = { ...doc }; dateFields.forEach(field => { if (convertedDoc[field] && typeof convertedDoc[field] === 'string') { convertedDoc[field] = new Date(convertedDoc[field]); } }); return convertedDoc; } function convertLubeOilDates(doc) { // Convert Unix timestamps to readable dates for fuel oil data if (!doc) return doc; const timestampFields = ['bunkerDate', 'testDate', 'sampleDate', 'createdDate', 'lastModified']; const convertedDoc = { ...doc }; timestampFields.forEach(field => { if (convertedDoc[field] && typeof convertedDoc[field] === 'number') { // Convert Unix timestamp to ISO string convertedDoc[field] = new Date(convertedDoc[field] * 1000).toISOString(); } }); return convertedDoc; } // Typesense tool implementations export async function lubeReportTableQuery(args) { logger.info("Executing lube_report_table_query", { args }); try { const collection = args.collection; const query = args.query || {}; if (!collection) { throw new Error("Missing required parameter: 'collection'."); } // Validate required query fields const requiredQueryFields = ["q", "query_by"]; for (const field of requiredQueryFields) { if (!query[field] || !query[field]) { throw new Error(`Missing required query field: '${field}'.`); } } logger.debug(`Querying Typesense collection '${collection}' with:`, query); // Use the existing typesense client to search const results = await typesenseClient.collections(collection).documents().search(query); const hits = results.hits || []; const formattedHits = []; for (const hit of hits) { let formattedDoc = hit.document || {}; if (collection === "pms") { formattedDoc = convertPmsDates(formattedDoc); } formattedHits.push(formattedDoc); } const formattedResults = { found: results.found || 0, out_of: results.out_of || 0, page: results.page || 1, hits: formattedHits }; // Generate link and artifact data const link = await getDataLinkHelper(formattedHits) || ''; const artifactData = getArtifactHelper("typesense_query", link); const content = { type: "text", text: JSON.stringify(formattedResults, null, 2), title: `Search results for ${collection}`, format: "json" }; const artifact = { type: "text", text: JSON.stringify(artifactData, null, 2), title: `search results for ${collection}`, format: "json" }; return [content, artifact]; } catch (error) { logger.error("Error in lubeReportTableQuery", { error }); throw new Error(`Typesense query failed: ${error instanceof Error ? error.message : String(error)}`); } } export async function smartFuelOilTableSearch(args) { logger.info("Executing universal_fuel_analysis_search", { args }); try { const collection = "fuel_oil_data"; const sessionId = args.session_id || "testing"; const queryText = args.query || "*"; const filters = args.filters || {}; const sortBy = args.sort_by || "relevance"; const sortOrder = args.sort_order || "desc"; const maxResults = args.max_results || 10; // Compose filter_by string from filters const filterParts = []; if (filters) { for (const [key, value] of Object.entries(filters)) { if (key === "bunkerDate_range" && typeof value === 'object' && value !== null) { const dateRange = value; const startDate = dateRange.start_date; const endDate = dateRange.end_date; if (startDate) { // Convert to Unix timestamp if it's not already if (typeof startDate === 'string') { try { const dtObj = new Date(startDate.replace('Z', '+00:00')); const startTs = Math.floor(dtObj.getTime() / 1000); filterParts.push(`bunkerDate:>=${startTs}`); } catch (error) { logger.warn(`Invalid start_date format: ${startDate}`); } } else { filterParts.push(`bunkerDate:>=${startDate}`); } } if (endDate) { // Convert to Unix timestamp if it's not already if (typeof endDate === 'string') { try { const dtObj = new Date(endDate.replace('Z', '+00:00')); const endTs = Math.floor(dtObj.getTime() / 1000); filterParts.push(`bunkerDate:<=${endTs}`); } catch (error) { logger.warn(`Invalid end_date format: ${endDate}`); } } else { filterParts.push(`bunkerDate:<=${endDate}`); } } } else if (key === "sulfur_range" && typeof value === 'object' && value !== null) { const sulfurRange = value; const minSulfur = sulfurRange.min_sulfur; const maxSulfur = sulfurRange.max_sulfur; if (minSulfur !== null && minSulfur !== undefined) { filterParts.push(`sulfur:>=${minSulfur}`); } if (maxSulfur !== null && maxSulfur !== undefined) { filterParts.push(`sulfur:<=${maxSulfur}`); } } else if (key === "density_range" && typeof value === 'object' && value !== null) { const densityRange = value; const minDensity = densityRange.min_density; const maxDensity = densityRange.max_density; if (minDensity !== null && minDensity !== undefined) { filterParts.push(`density:>=${minDensity}`); } if (maxDensity !== null && maxDensity !== undefined) { filterParts.push(`density:<=${maxDensity}`); } } else if (value !== null && value !== undefined) { filterParts.push(`${key}:=${value}`); } } } const filterBy = filterParts.length > 0 ? filterParts.join(" && ") : ""; // Prepare query parameters const queryBy = "vesselName,bunkerPort,fuelType,supplier,testLab,rating,samplingMethod,samplingPoint,barge,complianceComments"; // Exclude fields to reduce response size const excludeFields = "vesselId,docId,fleetId,fleetManagerId,technicalSuperintendentId,_id,ownerId"; const query = { q: queryText, query_by: queryBy, per_page: maxResults, exclude_fields: excludeFields }; if (filterBy) { query.filter_by = filterBy; } // Apply sorting if not relevance if (sortBy !== "relevance") { const sortDirection = sortOrder === "asc" ? "" : ":desc"; query.sort_by = `${sortBy}${sortDirection}`; } logger.debug(`Querying Typesense collection '${collection}' with:`, query); // Execute the search const results = await typesenseClient.collections(collection).documents().search(query); // Process results const hits = results.hits || []; const filteredHits = []; for (const hit of hits) { const documentData = convertPmsDates(hit.document || {}); filteredHits.push({ id: documentData.id, score: hit.text_match || 0, document: documentData }); } // Format the results const formattedResults = { found: results.found || 0, out_of: results.out_of || 0, page: results.page || 1, hits: filteredHits }; const content = { type: "text", text: JSON.stringify(formattedResults, null, 2), title: `Fuel oil analysis search results for '${queryText}'`, format: "json" }; // Generate artifact for visualization if there are results let artifactData = null; if (filteredHits.length > 0) { const documents = hits.map((hit) => hit.document); const link = await getDataLinkHelper(documents); artifactData = getArtifactHelper("universal_fuel_analysis_search", link || ''); const artifact = { type: "text", text: JSON.stringify(artifactData, null, 2), title: `Fuel oil analysis search results for ${collection}`, format: "json" }; return [content, artifact]; } return [content]; } catch (error) { logger.error("Error in smartFuelOilTableSearch", { error }); const errorMessage = `Error searching fuel oil analysis data: ${error instanceof Error ? error.message : String(error)}`; return [ { type: "error", content: errorMessage, title: "Error searching fuel oil analysis data" } ]; } } export async function smartPmsSearch(args) { logger.info("Executing universal_pms_maintenance_search", { args }); try { const collection = "pms"; const sessionId = args.session_id || "testing"; const queryText = args.query || "*"; const filters = args.filters || {}; const sortBy = args.sort_by || "relevance"; const sortOrder = args.sort_order || "asc"; const maxResults = args.max_results || 10; // Check for IMO in filters and validate it before processing if (filters && filters.imo) { const { validateImoNumber } = await import('../utils/imoUtils.js'); const { config } = await import('../utils/config.js'); const validation = validateImoNumber(filters.imo, config.companyName); if (!validation.isValid) { logger.warn(`Invalid IMO number in universal_pms_maintenance_search: ${filters.imo}`); return [{ type: "text", text: validation.errorMessage || "Invalid IMO number", title: "IMO Validation Error" }]; } } // Compose filter_by string from filters const filterParts = []; if (filters) { for (const [key, value] of Object.entries(filters)) { if (value === null || value === undefined) { continue; } if (key.endsWith("_range") && typeof value === 'object' && value !== null) { if (key === "remainingHrsNextOverhaul_range") { const hoursRange = value; const minHours = hoursRange.min_hours; const maxHours = hoursRange.max_hours; if (minHours !== null && minHours !== undefined) { filterParts.push(`remainingHrsNextOverhaul:>=${minHours}`); } if (maxHours !== null && maxHours !== undefined) { filterParts.push(`remainingHrsNextOverhaul:<=${maxHours}`); } } else if (key === "remainingDaysNextOverhaul_range") { const daysRange = value; const minDays = daysRange.min_days; const maxDays = daysRange.max_days; if (minDays !== null && minDays !== undefined) { filterParts.push(`remainingDaysNextOverhaul:>=${minDays}`); } if (maxDays !== null && maxDays !== undefined) { filterParts.push(`remainingDaysNextOverhaul:<=${maxDays}`); } } else if (key === "jobDueDate_range" || key === "jobDoneDate_range" || key === "defermentRequestedDate_range") { const dateRange = value; const startDate = dateRange.start_date; const endDate = dateRange.end_date; const fieldBase = key.replace("_range", ""); if (startDate) { try { const startTs = Math.floor(new Date(startDate).getTime() / 1000); filterParts.push(`${fieldBase}:>=${startTs}`); } catch (error) { logger.warn(`Invalid start_date format for ${key}: ${startDate}`); } } if (endDate) { try { const endTs = Math.floor(new Date(endDate).getTime() / 1000); filterParts.push(`${fieldBase}:<=${endTs}`); } catch (error) { logger.warn(`Invalid end_date format for ${key}: ${endDate}`); } } } } else if (value !== null && value !== undefined) { if (typeof value === 'string') { // Remove quotes from JSON stringified value to match Python behavior const cleanValue = JSON.stringify(value).replace(/^"|"$/g, ''); filterParts.push(`${key}:=${cleanValue}`); } else { filterParts.push(`${key}:=${value}`); } } } } const filterBy = filterParts.length > 0 ? filterParts.join(" && ") : ""; // Prepare query parameters const queryBy = "jobTitle,component,vesselName"; const excludeFields = "embedding,_id,vesselId,docId,fleetId,fleetManagerId,technicalSuperintendentId"; const query = { q: queryText, query_by: queryBy, per_page: maxResults, exclude_fields: excludeFields }; if (filterBy) { query.filter_by = filterBy; } if (sortBy !== "relevance") { const sortDirection = sortOrder === "asc" ? "" : ":desc"; query.sort_by = `${sortBy}${sortDirection}`; } logger.debug(`Querying Typesense collection '${collection}' with:`, query); // Execute the search const results = await typesenseClient.collections(collection).documents().search(query); // Process results const hits = results.hits || []; const filteredHits = []; for (const hit of hits) { const documentData = convertPmsDates(hit.document || {}); filteredHits.push({ id: documentData.id, score: hit.text_match || 0, document: documentData }); } // Format the results const formattedResults = { found: results.found || 0, out_of: results.out_of || 0, page: results.page || 1, hits: filteredHits }; // Create the main content const content = { type: "text", text: JSON.stringify(formattedResults, null, 2), title: `PMS jobs search results for '${queryText}'`, format: "json" }; // Generate artifact for visualization if there are results let artifactData = null; if (filteredHits.length > 0) { const documents = hits.map((hit) => hit.document); const link = await getDataLinkHelper(documents); artifactData = getArtifactHelper("universal_pms_maintenance_search", link || ''); const artifact = { type: "text", text: JSON.stringify(artifactData, null, 2), title: `PMS jobs search results for ${collection}`, format: "json" }; return [content, artifact]; } return [content]; } catch (error) { logger.error("Error in smartPmsSearch", { error }); const errorMessage = `Error searching PMS jobs data: ${error instanceof Error ? error.message : String(error)}`; return [ { type: "error", content: errorMessage, title: "Error searching PMS jobs data" } ]; } } export async function listPmsJobsByCategoryAndStatus(args) { logger.info("Executing list_pms_jobs_by_category_and_status", { args }); try { const imo = args.imo; const jobCategory = args.jobCategory; const jobStatus = args.jobStatus; const perPage = args.per_page || 250; const sessionId = args.session_id || "testing"; if (!imo || !jobCategory || !jobStatus) { throw new Error("IMO number, job category, and job status are required"); } const collection = "pms"; const includeFields = "imo,vesselName,jobTitle,component,jobDueDate,jobDoneDate,jobCode,defermentRequestedDate,jobCategory,scheduledOverhaulInterval,currentCounter,defermentApplied,jobStatus,currentStatus,remainingHrsNextOverhaul,remainingDaysNextOverhaul"; const query = { q: "*", filter_by: `imo:${imo} && jobCategory:${jobCategory} && jobStatus:${jobStatus}`, per_page: perPage, include_fields: includeFields }; // Execute the search logger.info(`Searching for ${jobCategory} PMS jobs with status ${jobStatus} for vessel ${imo}`); const results = await typesenseClient.collections(collection).documents().search(query); const hits = results.hits || []; const filteredHits = []; for (const hit of hits) { const documentData = convertPmsDates(hit.document || {}); filteredHits.push({ id: documentData.id, score: hit.text_match || 0, document: documentData }); } // Get documents for data link const documents = getDocumentsFromHits(hits); // Get vessel name from hits let vesselName = null; // Get data link const dataLink = await getDataLinkWithDefault(await getDataLinkHelper(documents)); await insertDataLinkToMongodbHelper(dataLink, 'Maintenance Summary', sessionId || '', imo || '', vesselName || ''); try { vesselName = hits[0]?.document?.vesselName || null; } catch (error) { vesselName = null; } // Format the results const formattedResults = { found: results.found || 0, out_of: results.out_of || 0, page: results.page || 1, hits: filteredHits }; // Create artifact const artifactData = getArtifactHelper("list_pms_jobs_by_category_and_status", dataLink || ''); const content = { type: "text", text: JSON.stringify(formattedResults, null, 2), title: `PMS jobs search results for ${collection}`, format: "json" }; const artifact = { type: "text", text: JSON.stringify(artifactData, null, 2), title: `PMS jobs search results for ${collection}`, format: "json" }; return [content, artifact]; } catch (error) { logger.error("Error in listPmsJobsByCategoryAndStatus", { error }); const errorMessage = `Error retrieving PMS jobs: ${error instanceof Error ? error.message : String(error)}`; return [ { type: "error", content: errorMessage, title: "Error retrieving PMS jobs" } ]; } } export async function listPmsJobsForComponent(args) { logger.info("Executing list_pms_jobs_for_component", { args }); try { const imo = args.imo; const component = args.component; const perPage = args.per_page || 250; const sessionId = args.session_id || "testing"; if (!imo || !component) { throw new Error("IMO number and component name are required"); } const collection = "pms"; const includeFields = "imo,vesselName,jobTitle,component,jobDueDate,jobDoneDate,jobCode,defermentRequestedDate,jobCategory,scheduledOverhaulInterval,currentCounter,defermentApplied,jobStatus,currentStatus,remainingHrsNextOverhaul,remainingDaysNextOverhaul"; const query = { q: "*", filter_by: `imo:${imo} && component:${component}`, sort_by: "jobDueDate:asc", per_page: perPage, include_fields: includeFields }; // Execute the search logger.info(`Searching for PMS jobs for component '${component}' on vessel ${imo}`); const results = await typesenseClient.collections(collection).documents().search(query); const hits = results.hits || []; const filteredHits = []; for (const hit of hits) { const documentData = convertPmsDates(hit.document || {}); filteredHits.push({ id: documentData.id, score: hit.text_match || 0, document: documentData }); } // Get documents for data link const documents = getDocumentsFromHits(hits); // Get data link const dataLink = await getDataLinkHelper(documents); // Get vessel name from hits let vesselName = null; try { vesselName = hits[0]?.document?.vesselName || null; } catch (error) { vesselName = null; } // Insert the data link to mongodb collection const linkHeader = `pms jobs for component ${component}`; await insertDataLinkToMongodbHelper(dataLink ?? "", linkHeader, sessionId, imo, vesselName ?? undefined); // Format the results const formattedResults = { found: results.found || 0, out_of: results.out_of || 0, page: results.page || 1, hits: filteredHits }; // Create artifact const artifactData = getArtifactHelper("list_pms_jobs_for_component", dataLink ?? ''); const content = { type: "text", text: JSON.stringify(formattedResults, null, 2), title: `PMS jobs for component '${component}' on vessel ${imo}`, format: "json" }; const artifact = { type: "text", text: JSON.stringify(artifactData, null, 2), title: `PMS jobs for component '${component}' on vessel ${imo}`, format: "json" }; return [content, artifact]; } catch (error) { logger.error("Error in listPmsJobsForComponent", { error }); const errorMessage = `Error retrieving PMS jobs: ${error instanceof Error ? error.message : String(error)}`; return { type: "error", content: errorMessage, metadata: { source: "list_pms_jobs_for_component", error: errorMessage } }; } } export async function listPmsJobsForComponentDueWithin(args) { logger.info("Executing list_pms_jobs_for_component_due_within", { args }); try { const imo = args.imo; const component = args.component; const days = args.days; const perPage = args.per_page || 250; const sessionId = args.session_id || "testing"; if (!imo || !component || days === undefined || days === null) { throw new Error("IMO number, component name, and days are required"); } // Calculate future date based on days const futureDate = new Date(); futureDate.setDate(futureDate.getDate() + days); const futureTs = Math.floor(futureDate.getTime() / 1000); const collection = "pms"; const includeFields = "imo,vesselName,jobTitle,component,jobDueDate,jobDoneDate,jobCode,defermentRequestedDate,jobCategory,scheduledOverhaulInterval,currentCounter,defermentApplied,jobStatus,currentStatus,remainingHrsNextOverhaul,remainingDaysNextOverhaul"; const query = { q: "*", filter_by: `imo:${imo} && component:${component} && jobDueDate:<${futureTs}`, sort_by: "jobDueDate:asc", per_page: perPage, include_fields: includeFields }; // Execute the search logger.info(`Searching for PMS jobs for component '${component}' due within ${days} days on vessel ${imo}`); const results = await typesenseClient.collections(collection).documents().search(query); const hits = results.hits || []; const filteredHits = []; for (const hit of hits) { const documentData = convertPmsDates(hit.document || {}); filteredHits.push({ id: documentData.id, score: hit.text_match || 0, document: documentData }); } // Get documents for data link const documents = getDocumentsFromHits(hits); // Get data link const dataLink = await getDataLinkHelper(documents); // Get vessel name from hits let vesselName = null; try { vesselName = hits[0]?.document?.vesselName || null; } catch (error) { vesselName = null; } // Insert the data link to mongodb collection const linkHeader = `pms jobs for component ${component} due within ${days} days`; await insertDataLinkToMongodbHelper(dataLink ?? "", linkHeader, sessionId, imo, vesselName ?? undefined); // Format the results const formattedResults = { found: results.found || 0, out_of: results.out_of || 0, page: results.page || 1, hits: filteredHits }; // Create human-readable date for the title const futureDateStr = futureDate.toISOString().split('T')[0]; // YYYY-MM-DD format // Create artifact const artifactData = getArtifactHelper("list_pms_jobs_for_component_due_within", dataLink ?? "no_link"); const content = { type: "text", text: JSON.stringify(formattedResults, null, 2), title: `PMS jobs for component '${component}' due within ${days} days (by ${futureDateStr}) on vessel ${imo}`, format: "json" }; const artifact = { type: "text", text: JSON.stringify(artifactData, null, 2), title: `PMS jobs for component '${component}' due within ${days} days (by ${futureDateStr}) on vessel ${imo}`, format: "json" }; return [content, artifact]; } catch (error) { logger.error("Error in listPmsJobsForComponentDueWithin", { error }); const errorMessage = `Error retrieving PMS jobs: ${error instanceof Error ? error.message : String(error)}`; return [ { type: "error", content: errorMessage, title: "Error retrieving PMS jobs" } ]; } } export async function listOverdueLubeOilSamples(args) { logger.info("Executing list_overdue_lube_oil_samples", { args }); try { const imo = args.imo; const sessionId = args.session_id || "testing"; if (!imo) { throw new Error("IMO number is required"); } // Build filter_by string let filterBy = `imo:${imo} && dueStatus:"OVERDUE"`; // Add optional machinery name filter if (args.machineryName) { filterBy += ` && machineryName:${args.machineryName}`; } // Execute search const searchParameters = { q: "*", filter_by: filterBy, sort_by: "nextDue:asc", per_page: args.per_page || 30, include_fields: "imo,vesselName,machineryName,reportStatus,sampleDate,nextDue,frequency,dueStatus,testLab,report" }; const searchResult = await typesenseClient.collections("lube_oil_reports").documents().search(searchParameters); if (!searchResult || !searchResult.hits) { return { type: "text", content: "No overdue lube oil samples found for the specified criteria.", metadata: { source: "list_overdue_lube_oil_samples", title: "No Overdue Samples Found" } }; } // Process results const hits = searchResult.hits || []; const samples = []; for (const hit of hits) { let documentData = hit.document || {}; // Convert dates if needed documentData = convertLubeOilDates(documentData); samples.push({ imo: documentData.imo, vesselName: documentData.vesselName, machineryName: documentData.machineryName, reportStatus: documentData.reportStatus, sampleDate: documentData.sampleDate, nextDue: documentData.nextDue, frequency: documentData.frequency, dueStatus: documentData.dueStatus, testLab: documentData.testLab, report: documentData.report }); } // Get documents for data link const documents = getDocumentsFromHits(hits); // Get data link const dataLink = await getDataLinkHelper(documents); // Get vessel name from hits let vesselName = null; try { vesselName = hits[0]?.document?.vesselName || null; } catch (error) { vesselName = null; } // Insert the data link to mongodb collection const machineryName = args.machineryName || "all machinery"; const linkHeader = `overdue lube oil samples for ${machineryName}`; await insertDataLinkToMongodbHelper(dataLink ?? undefined, linkHeader, sessionId, imo, vesselName ?? undefined); // Format results const formattedText = JSON.stringify(samples, null, 2); // Create artifact const artifactData = getArtifactHelper("list_overdue_lube_oil_samples", dataLink ?? "no_link"); const content = { type: "text", text: formattedText, title: `Overdue Lube Oil Samples`, format: "json" }; const artifact = { type: "text", text: JSON.stringify(artifactData, null, 2), title: `Overdue Lube Oil Samples`, format: "json" }; return [content, artifact]; } catch (error) { logger.error("Error in listOverdueLubeOilSamples", { error }); const errorMessage = `Error retrieving overdue lube oil samples: ${error instanceof Error ? error.message : String(error)}`; return [ { type: "error", content: errorMessage, title: "Error retrieving overdue lube oil samples" } ]; } } export async function machineryWithWarningLubeOilAnalysis(args) { logger.info("Executing machinery_with_warning_lube_oil_analysis", { args }); try { const imo = args.imo; const sessionId = args.session_id || "testing"; if (!imo) { throw new Error("IMO number is required"); } // Build filter_by string let filterBy = `imo:${imo} && reportStatus:"WARNING"`; // Add optional machinery name filter if (args.machineryName) { filterBy += ` && machineryName:${args.machineryName}`; } // Execute search const searchParameters = { q: "*", filter_by: filterBy, sort_by: "sampleDate:desc", per_page: args.per_page || 30, include_fields: "imo,vesselName,machineryName,reportStatus,sampleDate,nextDue,frequency,dueStatus,testLab,report" }; const searchResult = await typesenseClient.collections("lube_oil_reports").documents().search(searchParameters); if (!searchResult || !searchResult.hits) { return { type: "text", content: "No warning status oil analysis reports found for the specified criteria.", metadata: { source: "machinery_with_warning_lube_oil_analysis", title: "No Warning Reports Found" } }; } // Process results const hits = searchResult.hits || []; const reports = []; for (const hit of hits) { let documentData = hit.document || {}; // Convert dates if needed documentData = convertLubeOilDates(documentData); reports.push({ imo: documentData.imo, vesselName: documentData.vesselName, machineryName: documentData.machineryName, reportStatus: documentData.reportStatus, sampleDate: documentData.sampleDate, nextDue: documentData.nextDue, frequency: documentData.frequency, dueStatus: documentData.dueStatus, testLab: documentData.testLab, report: documentData.report }); } // Get documents for data link const documents = getDocumentsFromHits(hits); // Get data link const dataLink = await getDataLinkHelper(documents); // Get vessel name from hits let vesselName = null; try { vesselName = hits[0]?.document?.vesselName || null; } catch (error) { vesselName = null; } // Insert the data link to mongodb collection const machineryName = args.machineryName || "all machinery"; const linkHeader = `warning oil analysis reports for ${machineryName}`; await insertDataLinkToMongodbHelper(dataLink ?? undefined, linkHeader, sessionId, imo, vesselName ?? undefined); // Format results const formattedText = JSON.stringify(reports, null, 2); // Create artifact const artifactData = getArtifactHelper("machinery_with_warning_lube_oil_analysis", dataLink ?? "no_link"); const content = { type: "text", text: formattedText, title: `Machinery with Warning Oil Analysis`, format: "json" }; const artifact = { type: "text", text: JSON.stringify(artifactData, null, 2), title: `Machinery with Warning Oil Analysis`, format: "json" }; return [content, artifact]; } catch (error) { logger.error("Error in machineryWithWarningLubeOilAnalysis", { error }); const errorMessage = `Error retrieving warning status oil analysis reports: ${error instanceof Error ? error.message : String(error)}`; return [ { type: "error", content: errorMessage, title: "Error retrieving warning status oil analysis reports" } ]; } } export async function getLatestLubeOilAnalysisForMachinery(args) { logger.info("Executing get_latest_lube_oil_analysis_for_machinery", { args }); try { const imo = args.imo; const machineryName = args.machineryName; const sessionId = args.session_id || "testing"; if (!imo || !machineryName) { throw new Error("IMO number and machinery name are required"); } // Build filter_by string const filterBy = `imo:${imo} && machineryName:${machineryName}`; // Execute search const searchParameters = { q: "*", filter_by: filterBy, sort_by: "sampleDate:desc", per_page: args.per_page || 10, include_fields: "imo,vesselName,machineryName,reportStatus,sampleDate,nextDue,frequency,dueStatus,testLab,report" }; const searchResult = await typesenseClient.collections("lube_oil_reports").documents().search(searchParameters); if (!searchResult || !searchResult.hits || searchResult.hits.length === 0) { return { type: "text", content: `No oil analysis records found for ${machineryName} on vessel ${imo}.`, metadata: { source: "get_latest_lube_oil_analysis_for_machinery", title: "No Analysis Found" } }; } // Get the latest record const hits = searchResult.hits; let document = hits[0].document; // Convert date fields to human readable format document = convertLubeOilDates(document); const result = { imo: document.imo, vesselName: document.vesselName, machineryName: document.machineryName, reportStatus: document.reportStatus, sampleDate: document.sampleDate, nextDue: document.nextDue, frequency: document.frequency, dueStatus: document.dueStatus, testLab: document.testLab, report: document.report }; // Get documents for data link const documents = getDocumentsFromHits(hits); // Get data link const dataLink = await getDataLinkHelper(documents); // Get vessel name from document const vesselName = document.vesselName || null; // Insert the data link to mongodb collection const linkHeader = `latest oil analysis for ${machineryName}`; await insertDataLinkToMongodbHelper(dataLink ?? undefined, linkHeader, sessionId, imo, vesselName ?? undefined); // Format results const formattedText = JSON.stringify(result, null, 2); // Create artifact const artifactData = getArtifactHelper("get_latest_oil_analysis_for_machinery", dataLink ?? "no_link"); const content = { type: "text", text: formattedText, title: `Latest Oil Analysis for ${machineryName}`, format: "json" }; const artifact = { type: "text", text: JSON.stringify(artifactData, null, 2), title: `Latest Oil Analysis for ${machineryName}`, format: "json" }; return [content, artifact]; } catch (error) { logger.error("Error in getLatestLubeOilAnalysisForMachinery", { error }); const errorMessage = `Error retrieving latest oil analysis: ${error instanceof Error ? error.message : String(error)}`; return [ { type: "error", content: errorMessage, title: "Error retrieving latest oil analysis" } ]; } } export async function listLubeOilSamplesByFrequency(args) { logger.info("Executing list_lube_oil_samples_by_frequency", { args }); try { const imo = args.imo; const frequency = args.frequency; const sessionId = args.session_id || "testing"; if (!imo || !frequency) { throw new Error("IMO number and frequency are required"); } // Build filter_by string const filterBy = `imo:${imo} && frequency:"${frequency}"`; // Execute search const searchParameters = { q: "*", filter_by: filterBy, sort_by: "nextDue:asc", per_page: args.per_page || 30, include_fields: "imo,vesselName,machineryName,reportStatus,sampleDate,nextDue,frequency,dueStatus,testLab,report" }; const searchResult = await typesenseClient.collections("lube_oil_reports").documents().search(searchParameters); if (!searchResult || !searchResult.hits || searchResult.hits.length === 0) { return { type: "text", content: `No ${frequency} frequency lube oil samples found for vessel ${imo}.`, metadata: { source: "list_lube_oil_samples_by_frequency", title: "No Samples Found" } }; } // Process results const hits = searchResult.hits; const samples = []; for (const hit of hits) { let sampleData = hit.document || {}; // Convert dates if needed sampleData = convertLubeOilDates(sampleData); samples.push({ imo: sampleData.imo, vesselName: sampleData.vesselName, machineryName: sampleData.machineryName, reportStatus: sampleData.reportStatus, sampleDate: sampleData.sampleDate, nextDue: sampleData.nextDue, frequency: sampleData.frequency, dueStatus: sampleData.dueStatus, testLab: sampleData.testLab, report: sampleData.report }); } // Get documents for data link const documents = getDocumentsFromHits(hits); // Get data link const dataLink = await getDataLinkHelper(documents); // Get vessel name from hits let vesselName = null; try { vesselName = hits[0]?.document?.vesselName || null; } catch (error) { vesselName = null; } // Insert the data link to mongodb collection const linkHeader = `${frequency} frequency lube oil samples`; await insertDataLinkToMongodbHelper(dataLink ?? undefined, linkHeader, sessionId, imo, vesselName ?? undefined); // Format results const formattedText = JSON.stringify(samples, null, 2); // Create artifact const artifactData = getArtifactHelper("list_lube_oil_samples_by_frequency", dataLink ?? "no_link"); const content = { type: "text", text: formattedText, title: `${frequency} Frequency Lube Oil Samples`, format: "json" }; const artifact = { type: "text", text: JSON.stringify(artifactData, null, 2), title: `${frequency} Frequency Lube Oil Samples`, format: "json" }; return [content, artifact]; } catch (error) { logger.error("Error in listLubeOilSamplesByFrequency", { error }); const errorMessage = `Error retrieving lube oil samples: ${error instanceof Error ? error.message : String(error)}`; return [ { type: "error", content: errorMessage, title: "Error retrieving lube oil samples" } ]; } } export async function fetchLubeOilAnalysisWithLink(args) { logger.info("Executing fetch_lube_oil_analysis_with_link", { args }); try { const vesselName = args.vesselName; const sessionId = args.session_id || "testing"; let imo = args.imo; if (!vesselName) { throw new Error("Vessel name is required"); } // Build filter_by string let filterBy = `vesselName:${vesselName}`; // Add optional machinery name filter if (args.machineryName) { filterBy += ` && machineryName:${args.machineryName}`; } // Execute search const searchParameters = { q: "*", filter_by: filterBy, sort_by: "sampleDate:desc", per_page: args.per_page || 10, include_fields: "imo,vesselName,machineryName,reportStatus,sampleDate,nextDue,frequency,dueStatus,testLab,report" }; const searchResult = await typesenseClient.collections("lube_oil_reports").documents().search(searchParameters); if (!searchResult || !searchResult.hits || searchResult.hits.length === 0) { return { type: "text", content: `No oil analysis reports found for vessel ${vesselName}.`, metadata: { source: "fetch_lube_oil_analysis_with_link", title: "No Reports Found" } }; } // Get the latest record const hits = searchResult.hits; let latest = hits[0].document; // Convert date fields to human readable format latest = convertLubeOilDates(latest); const reportLink = latest?.report || null; // const report = await parseDocumentLink({ document_link: reportLink }) : null; const result = { machineryName: latest.machineryName, sampleDate: latest.sampleDate, // report: report, reportLink: reportLink }; // If imo wasn't provided in arguments, get it from the search result if (!imo) { imo = latest.imo; } // Get documents for data link const documents = hits.map((hit) => hit.document); // Get data link const dataLink = await getDataLinkHelper(documents); // Get machinery name for the header const machineryName = args.machineryName || latest.machineryName || "machinery"; // Insert the data link to mongodb collection const linkHeader = `latest oil analysis report link for ${machineryName}`; await insertDataLinkToMongodbHelper(dataLink ?? undefined, linkHeader, sessionId, imo, vesselName ?? undefined); // Format results const formattedText = JSON.stringify(result, null, 2); // Create artifact const artifactData = getArtifactHelper("fetch_lube_oil_analysis_with_link", dataLink ?? "no_link"); const content = { type: "text", text: formattedText, title: `Latest Oil Analysis Report Link`, format: "json" }; const artifact = { type: "text", text: JSON.stringify(artifactData, null, 2), title: `Latest Oil Analysis Report Link`, format: "json" }; return [content, artifact]; } catch (error) { logger.error("Error in fetchLubeOilAnalysisWithLink", { error }); const errorMessage = `Error retrieving latest oil analysis report link: ${error instanceof Error ? error.message : String(error)}`; return { type: "erro