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,164 lines • 62.7 kB
JavaScript
import { DatabaseManager } from "./mongodb.js";
import { logger } from "./logger.js";
import { getConfig } from "./config.js";
import { fetchCompanyImoNumbers, shouldBypassImoFiltering, updateTypesenseFilterWithCompanyImos as updateTypesenseFilterWithCompanyImosOptimized } from "./company-filtering.js";
import { getTypesenseClient } from "./typesense.js";
export async function fetchQADetails(imo, qaId, vesselInfoDbName, vesselInfoMongoUri, collectionName = 'vesselinfos') {
// Debug logging
logger.info(`fetchQADetails called with IMO: ${imo}, qaId: ${qaId}`);
logger.info(`vesselInfoDbName: ${vesselInfoDbName}`);
logger.info(`vesselInfoMongoUri: ${vesselInfoMongoUri ? 'Set' : 'Not set'}`);
const databaseManager = new DatabaseManager();
try {
await databaseManager.initializeDatabase(vesselInfoDbName, vesselInfoMongoUri);
const vesselInfoDb = databaseManager.getDb();
const collection = vesselInfoDb.collection(collectionName);
const query = {
'imo': parseInt(imo),
'questionNo': qaId
};
const projection = {
'_id': 0,
'imo': 1,
'vesselName': 1,
'refreshDate': 1,
'answer': 1
};
const mongoResult = await collection.findOne(query, { projection });
let res = mongoResult ? {
imo: mongoResult.imo,
vesselName: mongoResult.vesselName,
refreshDate: mongoResult.refreshDate,
answer: mongoResult.answer
} : {
imo: parseInt(imo),
vesselName: null,
refreshDate: null,
answer: null
};
// Format refresh date if it exists
if (res.refreshDate && new Date(res.refreshDate).toString() !== 'Invalid Date') {
res.refreshDate = new Date(res.refreshDate).toLocaleDateString('en-US', {
day: 'numeric',
month: 'short',
year: 'numeric'
});
}
// Process answer with component data if it exists
if (res.answer) {
const vesselComponentsDbName = process.env.vesselComponentsDbName || vesselInfoDbName;
const vesselComponentsMongoUri = process.env.vesselComponentsMongoUri || vesselInfoMongoUri;
res.answer = await addComponentData(res.answer, imo, vesselComponentsDbName, vesselComponentsMongoUri);
}
// Get vessel QnA snapshot link
try {
res.Artifactlink = await getVesselQnASnapshot(imo, qaId.toString());
}
catch (error) {
res.Artifactlink = null;
}
return res;
}
catch (error) {
logger.error('Error fetching QA details:', error);
throw new Error(`Error fetching QA details: ${error.message}`);
}
finally {
await databaseManager.closeDatabase();
}
}
export async function fetchQADetailsAndCreateResponse(imo, questionNo, functionName, linkHeader, sessionId = "testing", vesselInfoDbName, vesselInfoMongoUri, collectionName = 'vesselinfos') {
if (!imo) {
throw new Error("IMO is required");
}
const databaseManager = new DatabaseManager();
try {
// Fetch QA details
const result = await fetchQADetails(imo, questionNo, vesselInfoDbName, vesselInfoMongoUri, collectionName);
const link = result.Artifactlink;
const vesselName = result.vesselName;
// Insert data link to MongoDB
// await insertDataLinkToMongoDB(link, linkHeader, sessionId, imo, vesselName, vesselInfoDbName, vesselInfoMongoUri);
// Get artifact data
const artifactData = await getArtifact(functionName, link);
// Create content responses with processed answer
logger.info(`Link value: ${link}, type: ${typeof link}, is null: ${link === null}, is undefined: ${link === undefined}`);
logger.info(`Result answer: ${result.answer}`);
const artifactLinkText = link ? `\n\nArtifact Link: ${link}` : "";
logger.info(`Artifact link text: "${artifactLinkText}"`);
const content = {
type: "text",
text: `${result.answer || "No data available"}${artifactLinkText}`
};
const artifact = {
type: "text",
text: JSON.stringify(artifactData, null, 2)
};
return [content, artifact];
}
catch (error) {
logger.error(`Error in ${functionName}:`, error);
throw new Error(`Error in ${functionName}: ${error.message}`);
}
finally {
await databaseManager.closeDatabase();
}
}
export async function getComponentData(componentId, vesselComponentsDbName, vesselComponentsMongoUri, collectionName = 'vesselinfocomponents') {
const match = componentId.match(/^(\d+)_(\d+)_(\d+)$/);
if (!match) {
return `⚠️ Invalid component_id format: ${componentId}`;
}
if (!vesselComponentsDbName || !vesselComponentsMongoUri || !collectionName) {
throw new Error('Database name, MongoDB URI, and collection name are required');
}
const [, componentNumber, questionNumber, imo] = match;
const componentNo = `${componentNumber}_${questionNumber}_${imo}`;
const databaseManager = new DatabaseManager();
try {
await databaseManager.initializeDatabase(vesselComponentsDbName, vesselComponentsMongoUri);
const vesselComponentsDb = databaseManager.getDb();
const collection = vesselComponentsDb.collection(collectionName);
const doc = await collection.findOne({ componentNo });
if (!doc) {
return `⚠️ No component found for ID: ${componentId}`;
}
if (!doc.data) {
return "No data found in the table component";
}
if (!doc.data.headers || !Array.isArray(doc.data.headers)) {
return "No headers found in the table component";
}
if (!doc.data.body || !Array.isArray(doc.data.body)) {
return "No body data found in the table component";
}
// Extract headers excluding lineitem
const headers = doc.data.headers
.filter((h) => h && h.name !== "lineitem")
.map((h) => h.name);
const rows = doc.data.body;
// Build markdown table
let md = "| " + headers.join(" | ") + " |\n";
md += "| " + headers.map(() => "---").join(" | ") + " |\n";
for (const row of rows) {
const formattedRow = row
.filter((cell) => cell && !cell.lineitem) // Exclude lineitem and null cells
.map((cell) => {
if (cell && cell.value && cell.link) {
return `[${cell.value}](${cell.link})`;
}
else if (cell && cell.status && cell.color) {
return cell.status;
}
return cell ? String(cell) : '';
});
md += "| " + formattedRow.join(" | ") + " |\n";
}
return md;
}
catch (error) {
logger.error('Error getting component data:', error);
throw new Error(`Error getting component data: ${error.message}`);
}
finally {
await databaseManager.closeDatabase();
}
}
export async function addComponentData(answer, imo, vesselComponentsDbName, vesselComponentsMongoUri) {
const pattern = /httpsdev\.syia\.ai\/chat\/ag-grid-table\?component=(\d+_\d+)/g;
const matches = Array.from(answer.matchAll(pattern));
logger.info(`addComponentData called with IMO: ${imo}, matches found: ${matches.length}`);
logger.info(`Answer contains ag-grid URL: ${answer.includes('ag-grid-table')}`);
let result = answer;
for (const match of matches) {
const component = match[1];
logger.info(`Processing component: ${component}, full match: ${match[0]}`);
try {
const replacement = await getComponentData(`${component}_${imo}`, vesselComponentsDbName, vesselComponentsMongoUri, 'vesselinfocomponents');
logger.info(`Component data retrieved, length: ${replacement.length}`);
result = result.replace(match[0], replacement);
}
catch (error) {
logger.error('Error replacing component data:', error);
}
}
logger.info(`addComponentData returning result, length: ${result.length}`);
return result;
}
export async function getVesselQnASnapshot(imo, questionNo) {
try {
const config = getConfig();
const raw_snapshotUrl = config.snapshotUrl;
// API endpoint
const snapshotUrl = `${raw_snapshotUrl}/${imo}/${questionNo}`;
const raw_jwtToken = config.jwtToken;
// Authentication token
const jwtToken = `Bearer ${raw_jwtToken}`;
// Headers for the request
const headers = {
"Authorization": jwtToken
};
logger.info(`Fetching vessel QnA snapshot for IMO: ${imo}, Question: ${questionNo}`);
const response = await fetch(snapshotUrl, {
method: 'GET',
headers
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
// Return resultData if it exists, otherwise return the full response
if (data && typeof data === 'object' && "resultData" in data) {
return data.resultData;
}
return data;
}
catch (error) {
logger.error(`Error fetching vessel QnA snapshot for IMO ${imo}, Question ${questionNo}:`, error);
return null;
}
}
export async function getVesselQnASnapshotHandler(arguments_) {
const { imo, question_no } = arguments_;
const questionNo = question_no;
if (!imo || !questionNo) {
throw new Error("Both IMO and question_no are required");
}
try {
const result = await getVesselQnASnapshot(imo, questionNo);
if (!result) {
return [{
type: "text",
text: `No QnA snapshot data found for vessel IMO: ${imo}, Question: ${questionNo}`
}];
}
return [{
type: "text",
text: JSON.stringify(result, null, 2),
title: `Vessel QnA Snapshot - IMO: ${imo}, Question: ${questionNo}`,
format: "json"
}];
}
catch (error) {
logger.error(`Error getting vessel QnA snapshot:`, error);
throw new Error(`Error getting vessel QnA snapshot: ${error.message}`);
}
}
export async function getDataLink(data) {
try {
const config = getConfig();
const raw_url = config.snapshotUrl;
const url = raw_url;
const raw_jwtToken = config.jwtToken;
const headers = {
"Content-Type": "application/json",
"Authorization": `Bearer ${raw_jwtToken}`
};
const payload = {
data
};
// Log URL and token (complete values)
logger.info(`Making request to URL: ${url}`);
logger.info(`Using JWT token: ${raw_jwtToken}`);
const response = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify(payload)
});
// Log response status and headers
logger.info(`Response status: ${response.status} ${response.statusText}`);
logger.info(`Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
if (!response.ok) {
logger.error(`HTTP error! status: ${response.status}, statusText: ${response.statusText}`);
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
// Log complete response data
logger.info(`Complete response data: ${JSON.stringify(result)}`);
if (result.status === "OK") {
logger.info(`Successfully got data link: ${result.resultData}`);
return result.resultData;
}
else {
logger.error(`Failed to get data link: Invalid response status - ${result.status}`);
throw new Error('Failed to get data link: Invalid response status');
}
}
catch (error) {
logger.error('Error getting data link:', error);
throw new Error(`Error getting data link: ${error.message}`);
}
}
// /**
// * Generic function to insert data links into MongoDB
// */
// export async function insertDataLinkToMongoDBGeneric(options: InsertDataLinkOptions, dbName: string, mongoUri: string): Promise<void> {
// try {
// if (!dbName || !mongoUri) {
// throw new Error('Database name and MongoDB URI are required');
// }
// const databaseManager = new DatabaseManager();
// await databaseManager.initializeDatabase(dbName, mongoUri);
// const db = databaseManager.getDb();
// const collection = db.collection(options.collectionName);
// if (options.mode === 'general') {
// await collection.insertOne({
// link: options.dataLink,
// type: options.type,
// sessionId: options.sessionId,
// imo: options.imo,
// vesselName: options.vesselName,
// createdAt: new Date()
// });
// } else if (options.mode === 'pms') {
// const linkData = {
// link: options.dataLink,
// linkHeader: options.linkHeader || ''
// };
// const sessionExists = await collection.findOne({ sessionId: options.sessionId });
// if (sessionExists) {
// await collection.updateOne(
// { sessionId: options.sessionId },
// {
// $push: { links: { $each: [linkData] } },
// $set: { datetime: new Date().toISOString() }
// }
// );
// } else {
// const newEntry: CasefileData = {
// sessionId: options.sessionId,
// imo: options.imo ?? '',
// vesselName: options.vesselName ?? '',
// links: [linkData],
// datetime: new Date().toISOString()
// };
// await collection.insertOne(newEntry);
// }
// } else {
// throw new Error(`Unsupported insert mode: ${options.mode}`);
// }
// await databaseManager.closeDatabase();
// } catch (error: any) {
// logger.error(`Error inserting data link to MongoDB [${options.mode}]:`, error);
// throw new Error(`Error inserting data link to MongoDB: ${error.message}`);
// }
// }
// // export async function insertDataLinkToMongoDB(
// // link: string,
// // type: string,
// // sessionId: string,
// // imo: string,
// // vesselName: string,
// // dataLinksDbName: string,
// // dataLinksMongoUri: string
// // ): Promise<void> {
// // return insertDataLinkToMongoDBGeneric({
// // mode: 'general',
// // collectionName: 'data_links',
// // dataLink: link,
// // sessionId,
// // imo,
// // vesselName,
// // type
// // }, dataLinksDbName, dataLinksMongoUri);
// // }
// export async function insertPmsDataLinkToMongodb(
// dataLink: string,
// linkHeader: string,
// sessionId: string,
// imo?: string,
// vesselName?: string,
// dbName?: string,
// mongoUri?: string
// ): Promise<void> {
// if (!dbName || !mongoUri) {
// throw new Error('Database name and MongoDB URI are required');
// }
// return insertDataLinkToMongoDBGeneric({
// mode: 'pms',
// collectionName: 'casefile_data',
// dataLink,
// linkHeader,
// sessionId,
// imo,
// vesselName
// }, dbName, mongoUri);
// }
export async function getArtifact(toolName, link) {
try {
const timestamp = Math.floor(Date.now() / 1000);
const artifact = {
id: `msg_browser_${Math.random().toString(36).substring(2, 8)}`,
parentTaskId: `task_${toolName}_${Math.random().toString(36).substring(2, 8)}`,
timestamp,
agent: {
id: "agent_siya_browser",
name: "SIYA",
type: "qna"
},
messageType: "action",
action: {
tool: "browser",
operation: "browsing",
params: {
url: link,
pageTitle: `Tool response for ${toolName}`,
visual: {
icon: "browser",
color: "#2D8CFF"
},
stream: {
type: "vnc",
streamId: "stream_browser_1",
target: "browser"
}
}
},
content: `Viewed page: ${toolName}`,
artifacts: [
{
id: `artifact_webpage_${Date.now()}_${Math.floor(Math.random() * 1000)}`,
type: "browser_view",
content: {
url: link,
title: toolName,
screenshot: "",
textContent: `Observed output of cmd \`${toolName}\` executed:`,
extractedInfo: {}
},
metadata: {
domainName: "example.com",
visitTimestamp: Date.now(),
category: "web_page"
}
}
],
status: "completed"
};
return artifact;
}
catch (error) {
logger.error('Error getting artifact:', error);
throw new Error(`Error getting artifact: ${error.message}`);
}
}
export async function getListOfArtifacts(toolName, linkData) {
try {
const artifacts = [];
for (const link of linkData) {
if (link.url) {
const artifactData = await getArtifact(toolName, link.url);
artifacts.push({
type: "text",
text: JSON.stringify(artifactData, null, 2)
});
}
}
return artifacts;
}
catch (error) {
logger.error("Error getting list of artifacts:", error);
throw new Error(`Error getting list of artifacts: ${error.message}`);
}
}
export function convertUnixDates(document) {
logger.debug(`Starting Unix date conversion for document with ${Object.keys(document).length} fields`);
const result = { ...document };
const dateFields = [
'purchaseRequisitionDate',
'purchaseOrderIssuedDate',
'orderReadinessDate',
'date',
'poDate',
'expenseDate',
"inspectionTargetDate",
"reportDate",
"closingDate",
"targetDate",
"nextDueDate",
"extendedDate"
];
logger.debug(`Checking ${dateFields.length} potential date fields for Unix timestamp conversion`);
let convertedCount = 0;
for (const field of dateFields) {
const value = result[field];
if (typeof value === "number" && Number.isFinite(value)) {
const originalValue = value;
result[field] = new Date(value * 1000).toISOString();
logger.debug(`Converted field '${field}' from Unix timestamp ${originalValue} to ISO date: ${result[field]}`);
convertedCount++;
}
}
logger.debug(`Unix date conversion completed - ${convertedCount} fields converted out of ${dateFields.length} checked fields`);
return result;
}
/**
* Convert data to CSV format
* @param data - Array of objects to convert
* @returns CSV string
*/
export function convertToCSV(data) {
if (!data.length)
return "";
const headers = Object.keys(data[0]);
const escapeCSV = (value) => {
const str = value != null ? String(value) : "";
const needsEscaping = /[",\n]/.test(str);
const escaped = str.replace(/"/g, '""'); // escape double quotes
return needsEscaping ? `"${escaped}"` : escaped;
};
const rows = data.map(doc => headers.map(header => escapeCSV(doc[header])).join(','));
return [headers.join(','), ...rows].join('\n');
}
export async function processTypesenseResults(searchResult, toolName, title, session_id = "testing", linkHeader, artifactTitle, dbName, mongoUri) {
logger.info(`Starting processTypesenseResults for tool: ${toolName}, session: ${session_id}`);
try {
// Log input validation
logger.info(`Validating search results for ${toolName} - hits count: ${searchResult?.hits?.length || 0}`);
if (!searchResult || !searchResult.hits || searchResult.hits.length === 0) {
logger.warn(`No search results found for ${toolName}`);
return [{
type: "text",
text: "No records found for the specified criteria.",
title: "No Results Found",
format: "json"
}];
}
logger.info(`Processing ${searchResult.hits.length} hits for ${toolName}`);
// Process search results into the standard format
const hits = searchResult.hits || [];
logger.info(`Starting document processing for ${hits.length} hits in ${toolName}`);
const documents = await Promise.all(hits.map(async (hit, index) => {
if (!hit.document) {
logger.warn(`Hit ${index} is missing document property in ${toolName}`);
return {};
}
// Create a shallow copy of the document
const document = { ...hit.document };
// Remove embedding field to reduce response size
if (document.embedding) {
logger.debug(`Removing embedding field from document ${index} in ${toolName}`);
delete document.embedding;
}
// Convert Unix timestamps to readable dates
logger.debug(`Converting Unix dates for document ${index} in ${toolName}`);
return await convertUnixDates(document);
}));
logger.info(`Successfully processed ${documents.length} documents for ${toolName}`);
// Get data link
logger.info(`Generating data link for ${toolName}`);
const dataLink = await getDataLink(documents);
logger.info(`Data link generated successfully for ${toolName}: ${dataLink.substring(0, 50)}...`);
// Get vessel name and IMO from hits
let vesselName = null;
let imo = null;
logger.info(`Extracting vessel information from search results for ${toolName}`);
try {
vesselName = searchResult.hits[0]?.document?.vesselName;
imo = searchResult.hits[0]?.document?.imo;
logger.info(`Vessel info extracted for ${toolName} - Name: ${vesselName}, IMO: ${imo}`);
}
catch (error) {
logger.warn(`Could not get vessel name or IMO from hits in ${toolName}:`, error);
}
// Insert the data link to mongodb collection
logger.info(`Inserting data link to MongoDB for ${toolName}, session: ${session_id}`);
if (dbName && mongoUri) {
// await insertDataLinkToMongoDB(dataLink, linkHeader, session_id, imo || "", vesselName || "", dbName, mongoUri);
}
logger.info(`Data link successfully inserted to MongoDB for ${toolName}`);
// Format results in the standard structure
logger.info(`Formatting results for ${toolName}`);
const formattedResults = {
found: searchResult.found || 0,
out_of: searchResult.out_of || 0,
page: searchResult.page || 1,
hits: documents,
artifactLink: dataLink
};
logger.info(`Results formatted successfully for ${toolName} - found: ${formattedResults.found}, out_of: ${formattedResults.out_of}`);
// Get artifact data
logger.info(`Retrieving artifact data for ${toolName}`);
const artifactData = await getArtifact(toolName, dataLink);
logger.info(`Artifact data retrieved successfully for ${toolName}`);
// Create content response
logger.info(`Creating content response for ${toolName}`);
const content = {
type: "text",
text: JSON.stringify(formattedResults, null, 2),
title,
format: "json"
};
// Create artifact response
logger.info(`Creating artifact response for ${toolName}`);
const artifact = {
type: "text",
text: JSON.stringify(artifactData, null, 2),
title: artifactTitle || title,
format: "json"
};
logger.info(`processTypesenseResults completed successfully for ${toolName}`);
return [content, artifact];
}
catch (error) {
logger.error(`Error processing Typesense results for ${toolName}:`, error);
return [{
type: "text",
text: `Error processing results: ${error.message}`,
title: "Error",
format: "json"
}];
}
}
export async function processTypesenseExportResults(documents, toolName, title, artifactTitle, session_id, linkHeader, imo, vesselName, dbName, mongoUri) {
try {
// Process documents
const processedDocuments = await Promise.all(documents.map(async (doc) => {
const document = { ...doc };
// Remove embedding field to reduce response size
if (document.embedding) {
delete document.embedding;
}
// Convert any Unix timestamps to readable dates
return await convertUnixDates(document);
}));
// Get data link
const dataLink = await getDataLink(processedDocuments);
// Insert the data link to mongodb collection
if (dbName && mongoUri) {
// await insertDataLinkToMongoDB(dataLink, linkHeader, session_id, imo || "", vesselName || "", dbName, mongoUri);
}
// Format results in the standard structure
const formattedResults = {
found: processedDocuments.length,
out_of: processedDocuments.length,
page: 1,
hits: processedDocuments,
artifactLink: dataLink
};
// Get artifact data
const artifactData = await getArtifact(toolName, dataLink);
// Create content response
const content = {
type: "text",
text: JSON.stringify(formattedResults, null, 2),
title,
format: "json"
};
// Create artifact response
const artifact = {
type: "text",
text: JSON.stringify(artifactData, null, 2),
title: artifactTitle,
format: "json"
};
return [content, artifact];
}
catch (error) {
logger.error(`Error processing Typesense export results for ${toolName}:`, error);
return [{
type: "text",
text: `Error processing results: ${error.message}`,
title: "Error",
format: "json"
}];
}
}
export async function formatTypesenseResults(searchResult, toolName, title, dataLink, artifactTitle) {
try {
// Process search results into the standard format
const hits = searchResult.hits || [];
const documents = await Promise.all(hits.map(async (hit) => {
if (!hit.document) {
logger.warn(`Hit is missing document property in ${toolName}`);
return {};
}
// Create a shallow copy of the document
const document = { ...hit.document };
// Remove embedding field to reduce response size
if (document.embedding) {
delete document.embedding;
}
// Convert Unix timestamps to readable dates
return await convertUnixDates(document);
}));
// Format results in the standard structure
const formattedResults = {
found: searchResult.found || 0,
out_of: searchResult.out_of || 0,
page: searchResult.page || 1,
hits: documents
};
// Get artifact data
const artifactData = await getArtifact(toolName, dataLink);
const testing_json = {
found: searchResult.found || 0,
out_of: searchResult.out_of || 0,
page: searchResult.page || 1,
hits: "testing"
};
// Create content response
const content = {
type: "text",
text: JSON.stringify(testing_json, null, 2),
title,
format: "json"
};
// Create artifact response
const artifact = {
type: "text",
text: JSON.stringify(artifactData, null, 2),
title: artifactTitle || title,
format: "json"
};
return [content, artifact];
}
catch (error) {
logger.error(`Error formatting Typesense results for ${toolName}:`, error);
return [{
type: "text",
text: `Error formatting results: ${error.message}`,
title: "Error",
format: "json"
}];
}
}
/**
* Step 1: Query Typesense fleet-details collection to get fleet IMO by name
* @param fleetName - Name of the fleet (e.g., "SMPL DRY")
* @returns Promise<number | null> - Fleet IMO number or null if not found
*/
export async function getFleetImoByName(fleetName) {
try {
const client = getTypesenseClient();
const searchResult = await client.collections('fleet-details').documents().search({
q: fleetName,
query_by: 'name',
per_page: 1
});
if (searchResult.hits && searchResult.hits.length > 0) {
const fleetDoc = searchResult.hits[0].document;
return fleetDoc.imo || null;
}
return null;
}
catch (error) {
logger.error(`Error fetching fleet IMO for ${fleetName}:`, error);
return null;
}
}
/**
* Step 2: Query MongoDB common_group_details collection to get vessel IMO list
* @param fleetImo - IMO number of the fleet
* @returns Promise<number[]> - Array of vessel IMO numbers
*/
export async function getVesselImoListFromFleet(fleetImo, dbName, mongoUri, collectionName = 'common_group_details') {
if (!dbName || !mongoUri || !collectionName) {
throw new Error('Database name, MongoDB URI, and collection name are required for fleet operations');
}
const databaseManager = new DatabaseManager();
try {
await databaseManager.initializeDatabase(dbName, mongoUri);
const db = databaseManager.getDb();
const collection = db.collection(collectionName);
const fleetDoc = await collection.findOne({ imo: fleetImo });
if (fleetDoc && fleetDoc.imoList && Array.isArray(fleetDoc.imoList)) {
return fleetDoc.imoList;
}
return [];
}
catch (error) {
logger.error(`Error fetching vessel IMO list for fleet ${fleetImo}:`, error);
throw error;
}
finally {
await databaseManager.closeDatabase();
}
}
/**
* ============================================================================
* OLD IMPLEMENTATION (COMMENTED OUT - REPLACED BY OPTIMIZED VERSION)
* ============================================================================
* The old implementation has been replaced with an optimized version that includes:
* - In-memory caching with TTL (5 minutes)
* - MongoDB connection pooling
* - Set-based IMO lookups (O(1) instead of O(n))
* - Cached environment variables and config
*
* See: src/utils/company-filtering.ts for the optimized implementation
* ============================================================================
*/
// async function updateTypesenseFilterWithCompanyImosGeneric(
// filter: string,
// options?: {
// bypassForSynergy?: boolean;
// bypassForAdminCompanies?: boolean;
// loggerTag?: string; // For identifying the context: "PMS", "Defect", etc.
// },
// dbName?: string,
// mongoUri?: string
// ): Promise<string> {
// const { bypassForSynergy = false, bypassForAdminCompanies = false, loggerTag = "" } = options || {};
// const companyName = getConfig().companyName;
//
// if (!companyName) {
// logger.warn(`[${loggerTag}] Company name is missing in config.`);
// return filter;
// }
//
// if (bypassForSynergy && companyName === "Synergy") {
// return filter;
// }
//
// if (bypassForAdminCompanies && shouldBypassImoFiltering(companyName)) {
// logger.debug(`[${loggerTag}] Skipping Typesense IMO filtering for admin company: ${companyName}`);
// return filter;
// }
//
// // Use provided dbName/mongoUri or fallback to environment variables
// const finalDbName = dbName || process.env.GROUP_DETAILS_DB_NAME || process.env.FLEET_DISTRIBUTION_DB_NAME || process.env.COMPANY_DB_NAME || '';
// const finalMongoUri = mongoUri || process.env.GROUP_DETAILS_MONGO_URI || process.env.FLEET_DISTRIBUTION_MONGO_URI || process.env.COMPANY_DB_URI || '';
//
// const companyImos = finalDbName && finalMongoUri ? await fetchCompanyImoNumbers(companyName, finalDbName, finalMongoUri) : [];
// if (companyImos.length === 0) {
// logger.warn(`[${loggerTag}] No company IMO numbers configured. Skipping Typesense IMO filtering.`);
// return filter;
// }
//
// const imoFilter = `imo:[${companyImos.join(",")}]`;
//
// if (filter && filter.trim()) {
// const combinedFilter = `${filter} && ${imoFilter}`;
// logger.debug(`[${loggerTag}] Applied Typesense IMO filter: ${combinedFilter}`);
// return combinedFilter;
// } else {
// logger.debug(`[${loggerTag}] Applied Typesense IMO filter: ${imoFilter}`);
// return imoFilter;
// }
// }
// Specific wrappers - now using optimized version
export async function updateTypesenseFilterWithCompanyImos(filter, dbName, mongoUri) {
return updateTypesenseFilterWithCompanyImosOptimized(filter, dbName, mongoUri);
}
export async function updateTypesenseFilterWithCompanyImosPms(filter, dbName, mongoUri) {
// PMS version: bypass for admin companies (handled in optimized version)
return updateTypesenseFilterWithCompanyImosOptimized(filter, dbName, mongoUri);
}
export async function updateTypesenseFilterWithCompanyImosDefect(filter, dbName, mongoUri) {
// Defect version: bypass for admin companies (handled in optimized version)
return updateTypesenseFilterWithCompanyImosOptimized(filter, dbName, mongoUri);
}
/**
* Update MongoDB filter with company IMO numbers for filtering (PMS version)
* @param filter - Existing MongoDB filter object
* @returns Updated filter object with IMO restrictions
*/
export async function updateMongoFilterWithCompanyImos(filter, dbName, mongoUri) {
const companyName = getConfig().companyName;
// Skip filtering for admin companies
if (!companyName || shouldBypassImoFiltering(companyName)) {
logger.debug(`Skipping MongoDB IMO filtering for admin company: ${companyName}`);
return filter;
}
const companyImos = dbName && mongoUri ? await fetchCompanyImoNumbers(companyName, dbName, mongoUri) : [];
// If no IMO numbers configured, return original filter
if (companyImos.length === 0) {
logger.warn('No company IMO numbers configured. Skipping MongoDB IMO filtering.');
return filter;
}
// Create a copy of the filter to avoid modifying the original
const updatedFilter = { ...filter };
// Convert IMO numbers to integers for MongoDB query
const imoNumbers = companyImos.map((imo) => Number(imo));
// Add IMO restriction to the filter
updatedFilter.imo = { $in: imoNumbers };
logger.debug(`Applied MongoDB IMO filter: ${JSON.stringify(updatedFilter)}`);
return updatedFilter;
}
/**
* Update MongoDB aggregation pipeline with company IMO numbers for filtering (PMS version)
* @param pipeline - Existing MongoDB aggregation pipeline
* @returns Updated pipeline with IMO restrictions
*/
export async function updateMongoAggregationWithCompanyImos(pipeline, dbName, mongoUri) {
const companyName = getConfig().companyName;
// Skip filtering for admin companies
if (!companyName || shouldBypassImoFiltering(companyName)) {
logger.debug(`Skipping MongoDB aggregation IMO filtering for admin company: ${companyName}`);
return pipeline;
}
const companyImos = dbName && mongoUri ? await fetchCompanyImoNumbers(companyName, dbName, mongoUri) : [];
// If no IMO numbers configured, return original pipeline
if (companyImos.length === 0) {
logger.warn('No company IMO numbers configured. Skipping MongoDB aggregation IMO filtering.');
return pipeline;
}
// Convert IMO numbers to integers for MongoDB query
const imoNumbers = companyImos.map((imo) => Number(imo));
// Create IMO match stage
const imoMatchStage = {
$match: {
imo: { $in: imoNumbers }
}
};
// Add IMO filter as the first stage in the pipeline
const updatedPipeline = [imoMatchStage, ...pipeline];
logger.debug(`Applied MongoDB aggregation IMO filter: ${JSON.stringify(imoMatchStage)}`);
return updatedPipeline;
}
/**
* Update search query parameters with company IMO filtering (PMS version)
* @param searchParams - Search parameters object
* @returns Updated search parameters with IMO restrictions
*/
export async function updateSearchParamsWithCompanyImos(searchParams, dbName, mongoUri) {
const companyName = getConfig().companyName;
// Skip filtering for admin companies
if (!companyName || shouldBypassImoFiltering(companyName)) {
logger.debug(`Skipping search params IMO filtering for admin company: ${companyName}`);
return searchParams;
}
const companyImos = dbName && mongoUri ? await fetchCompanyImoNumbers(companyName, dbName, mongoUri) : [];
// If no IMO numbers configured, return original params
if (companyImos.length === 0) {
logger.warn('No company IMO numbers configured. Skipping search params IMO filtering.');
return searchParams;
}
// Create a copy of the search params
const updatedParams = { ...searchParams };
// Update filter_by parameter for Typesense
if (updatedParams.filter_by) {
updatedParams.filter_by = updateTypesenseFilterWithCompanyImosPms(updatedParams.filter_by);
}
else {
updatedParams.filter_by = updateTypesenseFilterWithCompanyImosPms('');
}
logger.debug(`Updated search params with IMO filtering: ${JSON.stringify(updatedParams)}`);
return updatedParams;
}
/**
* Check if a vessel IMO is authorized for the current company (PMS version)
* @param imo - IMO number to check
* @returns True if authorized, false otherwise
*/
export async function isVesselAuthorizedForCompany(imo, dbName, mongoUri) {
const companyName = getConfig().companyName;
// Allow access for admin companies
if (!companyName || shouldBypassImoFiltering(companyName)) {
return true;
}
const companyImos = dbName && mongoUri ? await fetchCompanyImoNumbers(companyName, dbName, mongoUri) : [];
// If no IMO numbers configured, deny access
if (companyImos.length === 0) {
return false;
}
const imoNumber = Number(imo);
const companyImoNumbers = companyImos.map((imo) => Number(imo));
return companyImoNumbers.includes(imoNumber);
}
/**
* Get authorized IMO numbers for the current company (PMS version)
* @returns Array of authorized IMO numbers
*/
export async function getAuthorizedImoNumbers(dbName, mongoUri) {
const companyName = getConfig().companyName;
// For admin companies, return empty array (no restrictions)
if (!companyName || shouldBypassImoFiltering(companyName)) {
return [];
}
if (!dbName || !mongoUri) {
throw new Error('Database name and MongoDB URI are required');
}
return await fetchCompanyImoNumbers(companyName, dbName, mongoUri);
}
/**
* Log IMO filtering activity for monitoring (PMS version)
* @param action - Action being performed
* @param details - Additional details about the filtering
*/
export async function logImoFilteringActivity(action, details = {}, dbName, mongoUri) {
const companyName = getConfig().companyName;
const companyImos = dbName && mongoUri ? await fetchCompanyImoNumbers(companyName, dbName, mongoUri) : [];
logger.info(`IMO filtering activity: ${action}`, {
companyName,
companyImoCount: companyImos.length,
isAdminCompany: companyName ? shouldBypassImoFiltering(companyName) : false,
...details
});
}
/**
* Generic function to export Typesense data for a given collection and IMO list
* @param collectionName - Name of the Typesense collection
* @param imoList - Array of IMO numbers
* @param startDate - Optional start date filter (ISO string)
* @param endDate - Optional end date filter (ISO string)
* @param dateField - Name of the date field to filter on
* @param excludeFields - Fields to exclude in the export
* @param timestampFields - Fields to convert from UNIX timestamp to ISO string
* @returns Array of parsed and processed documents
*/
export async function exportDataForImoListGeneric(collectionName = 'vesselinfos', imoList, startDate, endDate, dateField, excludeFields = "", timestampFields = []) {
try {
const client = getTypesenseClient();
const collection = client.collections(collectionName);
const dateToTs = (dateStr) => {
return Math.floor(new Date(dateStr).getTime() / 1000);
};
const filterParts = [`imo:[${imoList.join(',')}]`];
if (startDate && dateField) {
filterParts.push(`${dateField}:>=${dateToTs(startDate)}`);
}
if (endDate && dateField) {
filterParts.push(`${dateField}:<=${dateToTs(endDate)}`);
}
const filterBy = filterParts.join(" && ");
const query = {
filter_by: filterBy,
exclude_fields: excludeFields
};
const exportResult = await collection.documents().export(query);
let exportData;
if (typeof exportResult === 'string') {
exportData = exportResult;
}
else if (exportResult && typeof exportResult === 'object' && 'buffer' in exportResult) {
exportData = new TextDecoder().decode(exportResult);
}
else {
exportData = String(exportResult);
}
const documents = exportData
.split('\n')
.filter(line => line.trim())
.map(line => JSON.parse(line));
// Convert UNIX timestamps to readable date strings
for (const doc of documents) {
for (const field of timestampFields) {
if (field in doc && typeof doc[field] === 'number') {
try {
doc[field] = new Date(doc[field] * 1000).toISOString().replace('T', ' ').substring(0, 19);
}
catch (err) {
// Leave original value on error
}
}
}
}
return documents;
}
catch (error) {
logger.error(`Error exporting data from '${collectionName}' collection:`, error);
return [];
}
}
export async function exportDefectsForImoList(imoList, startDate, endDate) {
return exportDataForImoListGeneric('defect', imoList, startDate, endDate, 'reportDate', "_id,docId,fleetId,vesselId,fleetManagerId,technicalSuperintendentId,id", [
'inspectionTargetDate',
'reportDate',
'closingDate',
'targetDate',
'nextDueDate',
'extendedDate'
]);
}
export async function exportPurchasesForImoList(imoList, startDate, endDate) {
return exportDataForImoListGeneric('purchase', imoList, startDate, endDate, 'purchaseRequisitionDate', "embedding", [
'purchaseRequisitionDate',
'purchaseOrderIssuedDate',
'orderReadinessDate'
]);
}
export async function exportBudgetsForImoList(imoList, startDate, endDate) {
return exportDataForImoListGeneric('budget', imoList, startDate, endDate, 'date', "embedding", ['date']);
}
export async function exportExpensesForImoList(imoList, startDate, endDate) {
return exportDataForImoListGeneric('expense', imoList, startDate, endDate, 'expenseDate', "embedding", [
'expenseDate',
'poDate'
]);
}
export async function exportSurveysForImoList(imoList, startDate, endDate) {
return exportDataForImoListGeneric('survey', imoList, startDate, endDate, 'surveyDate', "embedding", ['surveyDate']);
}
// type DatabaseContext = 'pms' | 'pms-etl' | 'pms-engine' | 'defect' | 'defect-secondary';
// /**
// * Get MongoDB database instance by context
// * @param context - Determines the client and database name to use
// * @returns MongoDB Database instance
// */
// export async function getDatabaseInstance(context: DatabaseContext, dbName: string, mongoUri: string): Promise<DatabaseManager> {
// if (!dbName || !mongoUri) {
// throw new Error('Database name and MongoDB URI are required');
// }
// const databaseManager = new DatabaseManager();
// await databaseManager.initializeDatabase(dbName, mongoUri);
// return databaseManager;
// }
// export async function getPmsDatabase(dbName: string, mongoUri: string) {
// return getDatabaseInstance('pms', dbName, mongoUri);
// }
// export async function getPmsEtlDatabase(dbName: string, mongoUri: string) {
// return getDatabaseInstance('pms-etl', dbName, mongoUri);
// }
// export async function getPmsEngineDataDatabase(dbName: string, mongoUri: string) {
// return getDatabaseInstance('pms-engine', dbName, mongoUri);
// }
// export async function getDefectDatabase(dbName: string, mongoUri: string) {
// return getDatabaseInstance('defect', dbName, mongoUri);
// }
// export async function getDefectSecondaryDatabase(dbName: string, mongoUri: string) {
// return getDatabaseInstance('defect-secondary', dbName, mongoUri);
// }
export async function combined_mongotools_with_single_category_mapping(args, allQuestions = [7, 30, 112, 116, 128, 177, 261], toolNamePrefix = "combined_mongotools_single_category", questionMappings = {
7: "Aux Engine Maintenance Status Summary",
30: "Compressor Maintenance Status Summary",
112: "Main Engine Maintenance Status Summary",
116: "PMS Summary",
128: "Purifier Maintenance Status Summary",
177: "Critical Spare List Compliance",
261: "Saturday routine jobs (Weekly LSA/FFA jobs) as per Ship Palm (ERP system)"
}, qaDbName = '', qaMongoUri = '') {
logger.info("combined_mongotools_with_single_category_mapping called", args);
try {
const { imo, question_no } = args;
const questionNo = question_no;
// Early validation with destructuring
if (!imo)
throw new Error("IMO number is required");
// Use provided questions or default to all questions
const questionsToFetch = questionNo ? [questionNo] : allQuestions;
// Fast validation using Set for O(1) lookup
const ALLOWED_QUESTION_NOS = new Set(allQuestions);
if (questionNo && !ALLOWED_QUESTION_NOS.has(questionNo)) {
return [{
type: "text",
text: `Invalid questionNo: ${questionNo}. Allowed values are: ${Array.from(ALLOWED_QUESTION_NOS).join(', ')}`
}];
}
// Use provided question mappings and database connection parameters
// Create base summary text
const summaryText = `${questionNo ? ` - ${questionMappings[questionNo]?.toUpperCase()}` : ' - Complete Overview'}\n\n` +
`**Vessel IMO:** ${imo}\n` +
`**Questions Fetched:** ${questionsToFetch.join(', ')}\n\n`;
const allResponses = [];
// Process questions (single or multiple)
if (questionsToFetch.length === 1) {
// Single question - use existing logic
const qNo = questionsToFetch[0];
const questionDescription = questionMappings[qNo];
logger.info(`Fetching ${questionDescription} for vessel IMO: ${imo}`);
const result = await fetchQADetailsAndCreateResponse(imo, qNo, `${toolNamePrefix}_q${qNo}`, questionDescription.toLowerCase(), "testing", // sessionId
qaDbName, qaMongoUri, 'vesselinfos');
// Create summary for single question response (following survey_mcp_server pattern)
let vesselName = "";
let singleSummaryText = summaryText + `**Question Number:** ${qNo}\n`;
singleSummaryText += `**Description:** ${questionDescription}\n\n`;
// Extract vessel name from result if available
if (result && result.length > 0 && result[0].text) {
const text = String(result[0].text);
const vesselNameMatch = text.match(/\*\*Vessel Name:\*\*\s*([^\n]+)/);
if (vesselNameMatch) {
vesselName = vesselNameMatch[1].trim();
}
}
// Add vessel name to summary if