UNPKG

survey-mcp-server

Version:

Survey management server handling survey creation, response collection, analysis, and reporting with database access for data management

1,076 lines 155 kB
import { config } from "../utils/config.js"; import { logger } from "../utils/logger.js"; import { typesenseClient } from "../utils/typesense.js"; import { fetchQaDetails } from "../utils/mongodb.js"; import { getCompanyImoNumbers, shouldBypassImoFiltering } from '../utils/imoUtils.js'; import { updateTypesenseFilterWithCompanyImos } from '../utils/helpers.js'; import { filterResponseByCompanyImos } from '../utils/responseFilter.js'; export class ToolHandler { async safeTypesenseOperation(operation, fallbackValue) { try { if (!typesenseClient.getAvailability()) { logger.warn("Typesense is not available, returning fallback value"); return fallbackValue; } return await operation(); } catch (error) { logger.error("Typesense operation failed:", error.message); return fallbackValue; } } constructor(server) { this.server = server; } // Get company IMO numbers getCompanyImos() { return getCompanyImoNumbers(); } // Validate IMO number against company list validateImoNumber(imoNumber) { const companyImos = this.getCompanyImos(); if (companyImos.length === 0) { return { isValid: true }; // No restrictions } const imoNum = Number(imoNumber); const companyImosNum = companyImos.map(imo => Number(imo)); if (!companyImosNum.includes(imoNum)) { return { isValid: false, errorMessage: `Error: IMO number ${imoNumber} is not associated with the current company. Please provide a valid IMO number. Available IMO numbers: ${companyImos.slice(0, 10).join(", ")}${companyImos.length > 10 ? '...' : ''}` }; } return { isValid: true }; } async handleCallTool(name, arguments_) { try { // Define tools that require IMO validation (all have required: ["imo"] in schema) const imoRequiredTools = [ // Certificate/Survey listing tools "list_extended_certificate_records", "list_records_expiring_within_days", "list_records_by_status", // Vessel-specific status tools "get_class_certificate_status", "get_class_survey_status", "get_coc_notes_memo_status", "get_vessel_dry_docking_status", "get_next_periodical_survey_details", "get_cms_items_status", "get_expired_certificates_from_shippalm", // Fleet-wide tools (all require IMO parameter) "get_fleet_dry_docking_status", "get_fleet_annual_survey_status", "get_fleet_ihm_certificate_status", "get_fleet_lsa_ffa_certificate_status", // Classification society download tools "class_ccs_survey_status_download", "class_nk_survey_status_download", "class_kr_survey_status_download", "class_dnv_survey_status_download", "class_lr_survey_status_download", "class_bv_survey_status_download", "class_abs_survey_status_download" ]; // Centralized IMO validation (skip for admin companies) if (imoRequiredTools.includes(name) && !shouldBypassImoFiltering(config.companyName || "")) { const imoParam = arguments_.imo || arguments_.filters?.imo; if (!imoParam) { return [{ type: "text", text: "IMO number is required for this operation", title: "Missing IMO Parameter", format: "json" }]; } const validation = this.validateImoNumber(imoParam); if (!validation.isValid) { return [{ type: "text", text: validation.errorMessage || "Invalid IMO number", title: "Invalid IMO Number", format: "json" }]; } } // Execute the tool and capture the response let response; switch (name) { case "universal_certificate_survey_search": response = await this.universalCertificateSurveySearch(arguments_); break; case "list_extended_certificate_records": response = await this.listExtendedCertificateRecords(arguments_); break; case "list_records_expiring_within_days": response = await this.listRecordsExpiringWithinDays(arguments_); break; case "list_records_by_status": response = await this.listRecordsByStatus(arguments_); break; case "get_vessel_details": response = await this.getVesselDetails(arguments_); break; case "get_user_associated_vessels": response = await this.getUserAssociatedVessels(arguments_); break; case "get_class_survey_report": response = await this.getClassSurveyReport(arguments_); break; case "get_class_certificate_status": response = await this.getClassCertificateStatus(arguments_); break; case "get_class_survey_status": response = await this.getClassSurveyStatus(arguments_); break; case "get_coc_notes_memo_status": response = await this.getCocNotesMemoStatus(arguments_); break; case "get_vessel_dry_docking_status": response = await this.getVesselDryDockingStatus(arguments_); break; case "get_next_periodical_survey_details": response = await this.getNextPeriodicalSurveyDetails(arguments_); break; case "get_cms_items_status": response = await this.getCmsItemsStatus(arguments_); break; case "get_expired_certificates_from_shippalm": response = await this.getExpiredCertificatesFromShippalm(arguments_); break; case "get_vessel_class_by_imo": response = await this.getVesselClassByImo(arguments_); break; case "google_search": response = await this.googleSearch(arguments_); break; case "parse_document_link": response = await this.parseDocumentLink(arguments_); break; case "class_ccs_survey_status_download": response = await this.classCcsSurveyStatusDownload(arguments_); break; case "class_nk_survey_status_download": response = await this.classNkSurveyStatusDownload(arguments_); break; case "class_kr_survey_status_download": response = await this.classKrSurveyStatusDownload(arguments_); break; case "class_dnv_survey_status_download": response = await this.classDnvSurveyStatusDownload(arguments_); break; case "class_lr_survey_status_download": response = await this.classLrSurveyStatusDownload(arguments_); break; case "class_bv_survey_status_download": response = await this.classBvSurveyStatusDownload(arguments_); break; case "class_abs_survey_status_download": response = await this.classAbsSurveyStatusDownload(arguments_); break; case "write_casefile_data": response = await this.writeCasefileData(arguments_); break; case "retrieve_casefile_data": response = await this.retrieveCasefileData(arguments_); break; case "get_fleet_annual_survey_status": response = await this.getFleetAnnualSurveyStatus(arguments_); break; case "get_fleet_dry_docking_status": response = await this.getFleetDryDockingStatus(arguments_); break; case "get_fleet_ihm_certificate_status": response = await this.getFleetIhmCertificateStatus(arguments_); break; case "get_fleet_lsa_ffa_certificate_status": response = await this.getFleetLsaFfaCertificateStatus(arguments_); break; default: throw new Error(`Unknown tool: ${name}`); } // Apply universal post-response filtering for IMO-based access control const startTime = Date.now(); const filteredResponse = await filterResponseByCompanyImos(response); const filteringTime = Date.now() - startTime; // Log filtering activity for monitoring logger.debug(`Tool ${name} completed with response filtering`, { toolName: name, companyName: config.companyName, filteringTimeMs: filteringTime, originalResponseLength: response.length, filteredResponseLength: filteredResponse.length, hasImoFiltering: !shouldBypassImoFiltering(config.companyName || ""), companyImoCount: getCompanyImoNumbers().length }); return filteredResponse; } catch (error) { logger.error(`Error calling tool ${name}:`, error); throw error; } } async createSurvey(arguments_) { return [{ type: "text", text: "createSurvey placeholder implementation", title: "Placeholder" }]; } async searchSurveys(arguments_) { return [{ type: "text", text: "searchSurveys placeholder implementation", title: "Placeholder" }]; } async universalCertificateSurveySearch(arguments_) { const collection = "certificate"; const sessionId = arguments_.session_id || "testing"; const searchType = arguments_.search_type || "keyword"; const queryText = (arguments_.query || "").trim() || "*"; const filters = arguments_.filters || {}; const sortBy = arguments_.sort_by || "relevance"; const sortOrder = arguments_.sort_order || "asc"; const maxResults = arguments_.max_results || 10; try { // Compose filter_by string from filters const filterParts = []; if (filters && Object.keys(filters).length > 0) { for (const [key, value] of Object.entries(filters)) { if (value === null || value === undefined) { continue; } if (key.endsWith("_range")) { // Handle range filters const fieldBase = key.replace("_range", ""); const rangeValue = value; let minVal = rangeValue?.min_days || rangeValue?.start_date; let maxVal = rangeValue?.max_days || rangeValue?.end_date; // Convert date strings to Unix timestamps if (typeof minVal === 'string') { const date = new Date(minVal); minVal = Math.floor(date.getTime() / 1000); } if (typeof maxVal === 'string') { const date = new Date(maxVal); maxVal = Math.floor(date.getTime() / 1000); } if (minVal !== null && minVal !== undefined) { filterParts.push(`${fieldBase}:>=${minVal}`); } if (maxVal !== null && maxVal !== undefined) { filterParts.push(`${fieldBase}:<=${maxVal}`); } } else if (typeof value === 'boolean') { filterParts.push(`${key}:=${value.toString().toLowerCase()}`); } else if (typeof value === 'string') { // Remove quotes from JSON.stringify and escape properly const escapedValue = JSON.stringify(value).slice(1, -1); filterParts.push(`${key}:=${escapedValue}`); } else { filterParts.push(`${key}:=${value}`); } } } let filterBy = filterParts.length > 0 ? filterParts.join(" && ") : ""; // Apply company IMO filtering filterBy = await updateTypesenseFilterWithCompanyImos(filterBy); // Additional validation: if IMO is specifically requested in filters, validate it if (filters.imo) { const validation = this.validateImoNumber(filters.imo); if (!validation.isValid) { return [{ type: "text", text: validation.errorMessage || "Invalid IMO number", title: "Invalid IMO Number", format: "json" }]; } } // Decide query behavior const q = searchType === "browse" ? "*" : queryText; const queryBy = "certificateSurveyEquipmentName,certificateNumber,issuingAuthority"; // Sort expression let sortByExpr; if (sortBy !== "relevance") { sortByExpr = `${sortBy}:${sortOrder}`; } // Fields to return const includeFields = ("imo,vesselName,certificateSurveyEquipmentName,isExtended,issuingAuthority,currentStatus," + "dataSource,type,issueDate,expiryDate,windowStartDate,windowEndDate,daysToExpiry,certificateNumber,certificateLink"); const query = { q: q, query_by: queryBy, include_fields: includeFields, per_page: maxResults, }; if (filterBy && filterBy.trim() !== "") { query.filter_by = filterBy; } if (sortByExpr) { query.sort_by = sortByExpr; } logger.debug(`[Typesense Query] ${JSON.stringify(query)}`); // Use Typesense client const results = await typesenseClient.search(collection, query); const hits = results.hits || []; const filteredHits = []; for (const hit of hits) { const document = hit.document || {}; delete document.embedding; // Remove embedding field const convertedDocument = this.convertCertificateDates(document); filteredHits.push({ id: document.id || document._id, score: hit.text_match || 0, document: convertedDocument }); } const documents = filteredHits.map(hit => hit.document); const dataLink = await this.getDataLink(documents); const vesselName = hits.length > 0 ? hits[0].document?.vesselName : null; const linkHeader = `Smart search result for query: '${queryText}'`; await this.insertDataLinkToMongoDB(dataLink, linkHeader, sessionId, filters.imo, vesselName); 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: `Search results for '${collection}'`, format: "json" }; const artifactData = await this.getArtifact("universal_certificate_survey_search", dataLink); const artifact = { type: "text", text: JSON.stringify(artifactData, null, 2), title: `Universal certificate survey search artifact for query '${queryText}'`, format: "json" }; return [content, artifact]; } catch (error) { logger.error(`Error executing smart certificate search:`, error); throw new Error(`Error performing smart certificate search: ${error.message}`); } } convertCertificateDates(data) { if (!data) return data; const dateFields = [ 'issueDate', 'extensionDate', 'expiryDate', 'windowStartDate', 'windowEndDate' ]; // Helper function to convert a single timestamp const convertTimestamp = (timestamp) => { try { // Skip if already a readable date string if (typeof timestamp === 'string' && timestamp.includes('-') && timestamp.length > 10) { return timestamp; } // Convert to number if it's a string number let numericTimestamp = timestamp; if (typeof timestamp === 'string') { numericTimestamp = parseFloat(timestamp); } // Check if it's a valid Unix timestamp (both seconds and milliseconds) if (typeof numericTimestamp === 'number' && !isNaN(numericTimestamp) && numericTimestamp > 0) { let date; // If timestamp is in seconds (typical Unix timestamp) if (numericTimestamp < 10000000000) { date = new Date(numericTimestamp * 1000); } else { // If timestamp is already in milliseconds date = new Date(numericTimestamp); } // Format as readable date: DD-MMM-YYYY (e.g., "18-Jun-2024") const options = { day: '2-digit', month: 'short', year: 'numeric' }; return date.toLocaleDateString('en-GB', options); } } catch (error) { logger.warn(`Failed to convert timestamp (value: ${timestamp}): ${error}`); } // Return original value if conversion fails return timestamp; }; // Recursive function to process nested objects and arrays const processData = (obj) => { if (obj === null || obj === undefined) { return obj; } if (Array.isArray(obj)) { return obj.map(item => processData(item)); } if (typeof obj === 'object') { const result = {}; for (const [key, value] of Object.entries(obj)) { if (dateFields.includes(key)) { result[key] = convertTimestamp(value); } else { result[key] = processData(value); } } return result; } return obj; }; return processData(data); } async getDataLink(documents) { try { const axios = await import('axios'); const { config } = await import('../utils/config.js'); const response = await axios.default.post('https://app-api.siya.com/v1.0/vessel-info/qna-snapshot', { data: documents }, { headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${config.api.token}` } }); if (response.data && response.data.status === 'OK') { return response.data.resultData; } return `data_link_${Date.now()}`; } catch (error) { logger.error('Error generating data link:', error); return `data_link_${Date.now()}`; } } async insertDataLinkToMongoDB(dataLink, linkHeader, sessionId, imo, vesselName) { try { const { MongoDBClient } = await import('../utils/mongodb.js'); const mongoClient = MongoDBClient.getInstance(); const db = await mongoClient.getDatabase('devSyiaApi'); const collection = db.collection('casefile_data'); const sessionExists = await collection.findOne({ sessionId }); const linkData = { link: dataLink, linkHeader }; if (sessionExists) { await collection.updateOne({ sessionId }, { $push: { links: linkData }, $set: { datetime: new Date() } }); } else { const toInsert = { sessionId, imo, vesselName, links: [linkData], datetime: new Date() }; await collection.insertOne(toInsert); } logger.info(`Data link inserted for session ${sessionId}: ${linkHeader}`); } catch (error) { logger.error(`Error inserting data link to MongoDB:`, error); // Don't throw error to avoid breaking the main operation } } async getArtifact(toolName, dataLink) { return { id: "msg_browser_ghi789", parentTaskId: `task_${toolName}_${Date.now()}`, timestamp: Math.floor(Date.now() / 1000), agent: { id: "agent_siya_browser", name: "SIYA", type: "qna" }, messageType: "action", action: { tool: "browser", operation: "browsing", params: { url: dataLink, 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_1746018877304_994", type: "browser_view", content: { url: dataLink, title: toolName, screenshot: "", textContent: `Observed output of cmd \`${toolName}\` executed:`, extractedInfo: {} }, metadata: { domainName: "example.com", visitTimestamp: Date.now(), category: "web_page" } } ], status: "completed" }; } async listExtendedCertificateRecords(arguments_) { const imo = arguments_.imo; const recordType = arguments_.recordType || ""; const perPage = arguments_.per_page || 250; const sessionId = arguments_.session_id || "testing"; if (!imo) { throw new Error("IMO number is required"); } try { // Using direct import of typesenseClient const collection = "certificate"; const includeFields = "imo,vesselName,certificateSurveyEquipmentName,isExtended,issuingAuthority,currentStatus,dataSource,type,issueDate,expiryDate,windowStartDate,windowEndDate,postponedDate,daysToExpiry"; // Build filter_by string let filterBy = `imo:${imo} && isExtended:true`; if (recordType) { // Support both a single string and a list of types if (Array.isArray(recordType)) { // Join multiple types with comma for Typesense syntax const typesStr = recordType.join(','); filterBy += ` && type:=[${typesStr}]`; } else { filterBy += ` && type:${recordType}`; } } const query = { q: "*", filter_by: filterBy, include_fields: includeFields, per_page: perPage }; const results = await typesenseClient.search(collection, query); // Convert results to JSON string const hits = results.hits || []; const filteredHits = []; for (const hit of hits) { const document = hit.document || {}; // Remove embedding field to reduce response size if it exists delete document.embedding; // Convert date fields to human readable format const convertedDocument = this.convertCertificateDates(document); filteredHits.push({ id: document.id, score: hit.text_match || 0, document: convertedDocument }); } // Get documents for data link const documents = filteredHits.map(hit => hit.document); // Get data link const dataLink = await this.getDataLink(documents); // Get vessel name from hits let vesselName = null; try { vesselName = hits[0]?.document?.vesselName || null; } catch { vesselName = null; } // Insert the data link to mongodb collection const linkHeader = recordType ? `extended certificates of type ${recordType}` : "extended certificates (all types)"; await this.insertDataLinkToMongoDB(dataLink, linkHeader, sessionId, imo, vesselName); // Format the results const formattedResults = { found: results.found || 0, out_of: results.out_of || 0, page: results.page || 1, hits: filteredHits }; const formattedText = JSON.stringify(formattedResults, null, 2); const content = { type: "text", text: formattedText, title: `Search results for '${collection}'`, format: "json" }; return [content]; } catch (error) { logger.error(`Error searching collection certificate:`, error); throw new Error(`Error searching collection: ${error.message}`); } } async listRecordsExpiringWithinDays(arguments_) { const imo = arguments_.imo; const recordType = arguments_.recordType; const daysToExpiry = arguments_.daysToExpiry; const perPage = arguments_.per_page || 250; const sessionId = arguments_.session_id || "testing"; if (!imo || !recordType || !daysToExpiry) { throw new Error("IMO numbers, record types, and days to expiry are required"); } try { // Using direct import of typesenseClient // Convert days_to_expiry to integer const daysToExpiryInt = parseInt(String(daysToExpiry)); // Calculate expiry date as current date plus days_to_expiry const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() + daysToExpiryInt); const cutoffDateTs = Math.floor(cutoffDate.getTime() / 1000); const collection = "certificate"; const includeFields = "imo,vesselName,certificateSurveyEquipmentName,isExtended,issuingAuthority,currentStatus,dataSource,type,issueDate,expiryDate,windowStartDate,windowEndDate,postponedDate,daysToExpiry"; const query = { q: "*", filter_by: `imo:${imo} && type: ${recordType} && daysToExpiry:<${cutoffDateTs}`, include_fields: includeFields, per_page: perPage }; const results = await typesenseClient.search(collection, query); // Convert results to JSON string const hits = results.hits || []; const filteredHits = []; for (const hit of hits) { const document = hit.document || {}; // Remove embedding field to reduce response size if it exists delete document.embedding; // Convert date fields to human readable format const convertedDocument = this.convertCertificateDates(document); filteredHits.push({ id: document.id, score: hit.text_match || 0, document: convertedDocument }); } // Get documents for data link const documents = filteredHits.map(hit => hit.document); // Get data link const dataLink = await this.getDataLink(documents); // Get vessel name from hits let vesselName = null; try { vesselName = hits[0]?.document?.vesselName || null; } catch { vesselName = null; } // Insert the data link to mongodb collection const linkHeader = `certificates of type ${recordType} expiring within ${daysToExpiryInt} days`; await this.insertDataLinkToMongoDB(dataLink, linkHeader, sessionId, imo, vesselName); const formattedResults = { found: results.found || 0, out_of: results.out_of || 0, page: results.page || 1, hits: filteredHits }; const formattedText = JSON.stringify(formattedResults, null, 2); const content = { type: "text", text: formattedText, title: `Search results for '${collection}'`, format: "json" }; const artifactData = await this.getArtifact("list_records_expiring_within_days", dataLink); const artifact = { type: "text", text: JSON.stringify(artifactData, null, 2), title: `Records expiring within ${daysToExpiryInt} days for IMO ${imo}`, format: "json" }; return [content, artifact]; } catch (error) { logger.error(`Error searching collection certificate:`, error); throw new Error(`Error searching collection: ${error.message}`); } } async listRecordsByStatus(arguments_) { const imo = arguments_.imo; const recordType = arguments_.recordType; const status = arguments_.status; const perPage = arguments_.perPage || 100; const sessionId = arguments_.session_id || "testing"; // Only require imo and recordType if (!imo || !recordType) { throw new Error("IMO numbers and record types are required"); } try { // Using direct import of typesenseClient const collection = "certificate"; const includeFields = "imo,vesselName,certificateSurveyEquipmentName,isExtended,issuingAuthority,currentStatus,dataSource,type,issueDate,expiryDate,windowStartDate,windowEndDate,postponedDate,daysToExpiry"; // Build filter_by string let filterBy = `imo:${imo} && type: ${recordType}`; if (status) { filterBy += ` && currentStatus: ${status}`; } const query = { q: "*", filter_by: filterBy, include_fields: includeFields, per_page: perPage }; const results = await typesenseClient.search(collection, query); // Convert results to JSON string const hits = results.hits || []; const filteredHits = []; for (const hit of hits) { const document = hit.document || {}; // Remove embedding field to reduce response size if it exists delete document.embedding; // Convert date fields to human readable format const convertedDocument = this.convertCertificateDates(document); filteredHits.push({ id: document.id, score: hit.text_match || 0, document: convertedDocument }); } // Get documents for data link const documents = filteredHits.map(hit => hit.document); // Get data link const dataLink = await this.getDataLink(documents); // Get vessel name from hits let vesselName = null; try { vesselName = hits[0]?.document?.vesselName || null; } catch { vesselName = null; } // Insert the data link to mongodb collection const linkHeader = `certificates of type ${recordType}` + (status ? ` with status ${status}` : ""); await this.insertDataLinkToMongoDB(dataLink, linkHeader, sessionId, imo, vesselName); const formattedResults = { found: results.found || 0, out_of: results.out_of || 0, page: results.page || 1, hits: filteredHits }; const formattedText = JSON.stringify(formattedResults, null, 2); const content = { type: "text", text: formattedText, title: `Search results for '${collection}'`, format: "json" }; const artifactData = await this.getArtifact("list_records_by_status", dataLink); const artifact = { type: "text", text: JSON.stringify(artifactData, null, 2), title: status ? `Records with status ${status} for IMO ${imo}` : `Records for IMO ${imo}`, format: "json" }; return [content, artifact]; } catch (error) { logger.error(`Error searching collection certificate:`, error); throw new Error(`Error searching collection: ${error.message}`); } } async getVesselDetails(arguments_) { const query = arguments_.query; if (!query) { return [{ type: "text", text: "Error: 'query' parameter is required for vessel details search" }]; } try { logger.info(`Searching for vessel details with vessel name: ${query}`); // Set up search parameters for the fleet-vessel-lookup collection const searchParameters = { q: query, query_by: 'vesselName', per_page: 1, include_fields: 'vesselName,imo,class,flag,shippalmDoc,isV3', prefix: false, num_typos: 2, }; // Execute search // Using direct import of typesenseClient const raw = await typesenseClient.search('fleet-vessel-lookup', searchParameters); const hits = raw.hits || []; if (!hits.length) { return [{ type: "text", text: `No vessels found named '${query}'.` }]; } // Process and format results const doc = hits[0].document || {}; const results = { vesselName: doc.vesselName, imo: doc.imo, class: doc.class, flag: doc.flag, shippalmDoc: doc.shippalmDoc, isV3: doc.isV3, score: hits[0].text_match || 0 }; // Return formatted response const content = { type: "text", text: JSON.stringify(results, null, 2), title: `Vessel details for '${query}'`, format: "json" }; return [content]; } catch (error) { logger.error(`Error searching for vessel details: ${error}`); return [{ type: "text", text: `Error querying vessel details: ${error.message}` }]; } } async getUserAssociatedVessels(arguments_) { const mailId = arguments_.mailId; if (!mailId) { throw new Error("mailId (email) is required"); } try { // Import MongoDB client const { MongoDBClient } = await import('../utils/mongodb.js'); const mongoClient = MongoDBClient.getInstance(); // Get connection to dev-syia-api database const db = await mongoClient.getDatabase('devSyiaApi'); // Fetch user details from users collection using email const userCollection = db.collection("users"); const userInfo = await userCollection.findOne({ email: mailId }); if (!userInfo) { return [{ type: "text", text: JSON.stringify({ error: "User not found for email" }, null, 2), title: `Error for mailId ${mailId}`, format: "json" }]; } // Get associated vessel IDs from user info const associatedVesselIds = userInfo.associatedVessels || []; // Query the fleet_distributions_overviews collection const fleetDistributionsOverviewsCollection = db.collection("fleet_distributions_overviews"); const vessels = await fleetDistributionsOverviewsCollection.find({ vesselId: { $in: associatedVesselIds } }, { projection: { _id: 0, vesselName: 1, imo: 1 } }).limit(5).toArray(); // Format vessel info const formatVesselInfo = (vessels) => { if (!vessels || vessels.length === 0) { return "No vessels found associated with this user."; } const formattedText = [`- Associated Vessels: ${vessels.length} vessels`]; for (let i = 0; i < vessels.length; i++) { const vessel = vessels[i]; formattedText.push(`${i + 1}. ${vessel.vesselName || 'Unknown'}`); formattedText.push(` • IMO: ${vessel.imo || 'Unknown'}`); } return formattedText.join("\n"); }; const formattedText = formatVesselInfo(vessels); const content = { type: "text", text: formattedText, title: `Vessels associated with mailId ${mailId}` }; return [content]; } catch (error) { logger.error(`Error retrieving vessels for mailId ${mailId}:`, error); throw new Error(`Error retrieving associated vessels: ${error.message}`); } } async getClassSurveyReport(arguments_) { const imo = arguments_.imo; if (!imo) { throw new Error("IMO is required"); } try { const result = await fetchQaDetails(parseInt(imo), '19'); // Get link and vessel name for MongoDB const link = result.link || null; const vesselName = result.vesselName || null; const sessionId = arguments_.session_id || "testing"; await this.insertDataLinkToMongoDB(link, "class survey report", sessionId, imo, vesselName); // Format the results as JSON const formattedText = JSON.stringify(result, null, 2); // Create TextContent const content = { type: "text", text: formattedText, title: `Class survey report for IMO ${imo}`, format: "json" }; const artifactData = await this.getArtifact("get_class_survey_report", link); const artifact = { type: "text", text: JSON.stringify(artifactData, null, 2), title: `Class survey report for IMO ${imo}`, format: "json" }; return [content, artifact]; } catch (error) { logger.error(`Error getting class survey report for IMO ${imo}:`, error); throw new Error(`Error getting class survey report: ${error.message}`); } } async getClassCertificateStatus(arguments_) { const imo = arguments_.imo; if (!imo) { throw new Error("IMO is required"); } try { // Get both question 19 and 20 results const surveyReportResult = await fetchQaDetails(parseInt(imo), '19'); const result = await fetchQaDetails(parseInt(imo), '20'); // Apply date conversion to the result if it contains certificate data const convertedResult = this.convertCertificateDates(result); // Get links and vessel name for MongoDB const surveyReportLink = surveyReportResult.link || null; const link = convertedResult.link || null; const vesselName = convertedResult.vesselName || null; const sessionId = arguments_.session_id || "testing"; await this.insertDataLinkToMongoDB(link, "class certificate status", sessionId, imo, vesselName); await this.insertDataLinkToMongoDB(surveyReportLink, "class survey report", sessionId, imo, vesselName); // Create TextContent with combined format const combinedFormatText = `# Class Survey Report:\n${surveyReportResult.answer || "No data available"}\n\n# Class Certificate Status:\n${convertedResult.answer || "No data available"}`; const content = { type: "text", text: combinedFormatText, title: `Class certificate report for IMO ${imo}`, format: "text" }; // Create artifacts for both questions const surveyReportArtifact = await this.getArtifact("get_class_survey_report", surveyReportLink); const artifact1 = { type: "text", text: JSON.stringify(surveyReportArtifact, null, 2), title: `Class survey report for IMO ${imo}`, format: "json" }; const certificateArtifact = await this.getArtifact("get_class_certificate_status", link); const artifact2 = { type: "text", text: JSON.stringify(certificateArtifact, null, 2), title: `Class certificate status for IMO ${imo}`, format: "json" }; return [content, artifact1, artifact2]; } catch (error) { logger.error(`Error getting class certificate status for IMO ${imo}:`, error); throw new Error(`Error getting class certificate status: ${error.message}`); } } async getClassSurveyStatus(arguments_) { const imo = arguments_.imo; if (!imo) { throw new Error("IMO is required"); } try { // Get both question 19 and 21 results const surveyReportResult = await fetchQaDetails(parseInt(imo), '19'); const result = await fetchQaDetails(parseInt(imo), '21'); // Get links and vessel name for MongoDB const surveyReportLink = surveyReportResult.link || null; const link = result.link || null; const vesselName = result.vesselName || null; const sessionId = arguments_.session_id || "testing"; await this.insertDataLinkToMongoDB(link, "class survey status", sessionId, imo, vesselName); await this.insertDataLinkToMongoDB(surveyReportLink, "class survey report", sessionId, imo, vesselName); // Create TextContent with combined format const combinedFormatText = `# Class Survey Report:\n${surveyReportResult.answer || "No data available"}\n\n# Class Survey Status:\n${result.answer || "No data available"}`; const content = { type: "text", text: combinedFormatText, title: `Class survey status for IMO ${imo}`, format: "text" }; // Create artifacts for both questions const surveyReportArtifact = await this.getArtifact("get_class_survey_report", surveyReportLink); const artifact1 = { type: "text", text: JSON.stringify(surveyReportArtifact, null, 2), title: `Class survey report for IMO ${imo}`, format: "json" }; const surveyStatusArtifact = await this.getArtifact("get_class_survey_status", link); const artifact2 = { type: "text", text: JSON.stringify(surveyStatusArtifact, null, 2), title: `Class survey status for IMO ${imo}`, format: "json" }; return [content, artifact1, artifact2]; } catch (error) { logger.error(`Error getting class survey status for IMO ${imo}:`, error); throw new Error(`Error getting class survey status: ${error.message}`); } } async getCocNotesMemoStatus(arguments_) { const imo = arguments_.imo; if (!imo) { throw new Error("IMO is required"); } try { // Get both question 19 and 22 results const surveyReportResult = await fetchQaDetails(parseInt(imo), '19'); const result = await fetchQaDetails(parseInt(imo), '22'); // Get links and vessel name for MongoDB const surveyReportLink = surveyReportResult.link || null; const link = result.link || null; const vesselName = result.vesselName || null; const sessionId = arguments_.session_id || "testing"; await this.insertDataLinkToMongoDB(link, "coc notes memo status", sessionId, imo, vesselName); await this.insertDataLinkToMongoDB(surveyReportLink, "class survey report", sessionId, imo, vesselName); // Create TextContent with combined format const combinedFormatText = `# Class Survey Report:\n${surveyReportResult.answer || "No data available"}\n\n# CoC Notes Memo Status:\n${result.answer || "No data available"}`; const content = { type: "text", text: combinedFormatText, title: `CoC notes memo status for IMO ${imo}`, format: "text" }; // Create artifacts for both questions const surveyReportArtifact = await this.getArtifact("get_class_survey_report", surveyReportLink); const artifact1 = { type: "text", text: JSON.stringify(surveyReportArtifact, null, 2), title: `Class survey report for IMO ${imo}`, format: "json" }; const cocArtifact = await this.getArtifact("get_coc_notes_memo_status", link); const artifact2 = { type: "text", text: JSON.stringify(cocArtifact, null, 2), title: `CoC notes memo status for IMO ${imo}`, format: "json" }; return [content, artifact1, artifact2]; } catch (error) { logger.error(`Error getting CoC notes memo status for IMO ${imo}:`, error); throw new Error(`Error getting CoC notes memo status: ${error.message}`); } } async getVesselDryDockingStatus(arguments_) { const imo = arguments_.imo; if (!imo) { throw new Error("IMO is required"); } try { // Get both question 19 and 23 results const surveyReportResult = await fetchQaDetails(parseInt(imo), '19'); const result = await fetchQaDetails(parseInt(imo), '23'); // Get links and vessel name for MongoDB const surveyReportLink = surveyReportResult.link || null; const link = result.link || null; const vesselName = result.vesselName || null; const sessionId = arguments_.session_id || "testing"; await this.insertDataLinkToMongoDB(link, "vessel dry docking status", sessionId, imo, vesselNam