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