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