purchase-mcp-server
Version:
1,660 lines • 77.3 kB
JavaScript
import Typesense from "typesense";
import { MongoClient } from "mongodb";
import { updateTypesenseFilterWithCompanyImos, isValidVesselImoForCompany, isValidFleetImoForCompany, filterImoListByCompany } from "../utils/company-filtering.js";
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.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 mongoClient.close();
}
}
export async function fetchQADetailsAndCreateResponse(imo, questionNo, functionName, linkHeader, vesselInfoDbName, vesselInfoMongoUri, collectionName = 'vesselinfos') {
if (!imo) {
throw new Error("IMO is required");
}
// Debug logging removed - use console.error for stderr if needed
// console.error("Mongo URI from fetchQADetailsAndCreateResponse: ", vesselInfoMongoUri);
// console.error("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 = [
'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;
}
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);
return {
content: [{
type: "text",
text: `Error processing results: ${error.message}`,
title: "Error",
format: "json"
}]
};
}
}
export async function exportDataForImoListGeneric(collectionName, imoList, startDate, endDate, dateField, excludeFields = "", timestampFields = []) {
try {
// Filter IMO list to only include company IMOs
const filteredImoList = await filterImoListByCompany(imoList);
if (filteredImoList.length === 0) {
return []; // Return empty array if no valid IMOs
}
const client = await getTypesenseClient();
const collection = client.collections(collectionName);
const dateToTs = (dateStr) => {
return Math.floor(new Date(dateStr).getTime() / 1000);
};
const filterParts = [`imo:[${filteredImoList.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 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,vesselId,fleetId,docId,fleetManagerId,technicalSuperintendentId,ownerId", [
'expenseDate',
'poDate'
]);
}
export async function universal_purchase_search(arguments_) {
const collection = "purchase";
// Validate that only allowed properties are present
// const providedProperties = Object.keys(arguments_);
// const invalidProperties = providedProperties.filter(prop => !UniversalPurchaseSearchKeys.includes(prop as any));
// if (invalidProperties.length > 0) {
// throw new Error(`Invalid properties provided: ${invalidProperties.join(', ')}. Only the following properties are allowed: ${UniversalPurchaseSearchKeys.join(', ')}`);
// }
// Extract new schema parameters
const args = arguments_;
const query_by = args.query_by;
const query_text = (args.q || "").trim() || "*";
// Validate that query_by is provided when q is specified
if (args.q && args.q.trim() !== "*" && !query_by) {
throw new Error("query_by parameter is required when using full-text search with q parameter. Only the following properties are allowed: query_by, q, filter_by, sort_by, page, per_page. Please check the tool schema for universal_purchase_search for the allowed properties.");
}
const filter_by = args.filter_by || "";
const sort_by = args.sort_by || "relevance";
const page = args.page || 1;
const per_page = args.per_page || 50;
try {
let filterBy = filter_by;
// Convert date fields from yyyy-mm-dd to Unix timestamps in filter_by
if (filterBy) {
const dateFields = ["date", "purchaseRequisitionDate", "purchaseOrderIssuedDate", "orderReadinessDate"];
for (const dateField of dateFields) {
// Handle date range operations (>=, <=, >, <, =)
const dateRegexPatterns = [
{ pattern: new RegExp(`${dateField}:>=(\\d{4}-\\d{2}-\\d{2})`, 'g'), operator: ':>=' },
{ pattern: new RegExp(`${dateField}:<=(\\d{4}-\\d{2}-\\d{2})`, 'g'), operator: ':<=' },
{ pattern: new RegExp(`${dateField}:>(\\d{4}-\\d{2}-\\d{2})`, 'g'), operator: ':>' },
{ pattern: new RegExp(`${dateField}:<(\\d{4}-\\d{2}-\\d{2})`, 'g'), operator: ':<' },
{ pattern: new RegExp(`${dateField}:(\\d{4}-\\d{2}-\\d{2})`, 'g'), operator: ':' }
];
for (const { pattern, operator } of dateRegexPatterns) {
filterBy = filterBy.replace(pattern, (match, dateStr) => {
try {
const timestamp = Math.floor(new Date(dateStr + 'T00:00:00.000Z').getTime() / 1000);
return `${dateField}${operator}${timestamp}`;
}
catch (error) {
return match; // Keep original if conversion fails
}
});
}
}
}
// Apply company IMO restrictions
filterBy = await updateTypesenseFilterWithCompanyImos(filterBy || "");
// Set up all available fields for inclusion (based on schema)
const includeFields = "imo,vesselName,purchaseRequisitionDate,prDescription," +
"purchaseRequisitionStatus,poValue,purchaseOrderIssuedDate,vendorOrSupplierName," +
"accountCode,invoiceStatus,invoiceValue,prStatusColor,orderReadinessDate," +
"orderReadinessColor,purchaseRequisitionNumber,purchaseRequisitionLink," +
"purchaseOrderNumber,purchaseOrderLink,prType,prTypeColor,invoiceStatusColor," +
"scanID,scanIDLink,orderPriority,currencyCode,purchaseOrderStatus,poCreatedBy," +
"orderType,invoiceAmount,invoiceApproverName,qtcNo,qtcLink,forwarderName," +
"forwarderRemarks,warehouseLocation,cargoType,weight,purchaseOrderAmount," +
"purchaseRequisitionType,directPo,poInvoiceDiscrepency,purchaseRequisitionSummary," +
"poStatusColor,purchaseOrderStage,purchaseRequisitionDescription," +
"isNewPurchaseRequisition,isPurchaseRequisitionNotOrdered," +
"isPurchaseRequisitionSupplied,isPurchaseOrderReady,purchaseOverviewUpdate";
const excludeFields = "embedding,vesselId,docId,fleetManagerId,technicalSuperintendentId,ownerId";
const query = {
q: query_text,
query_by: query_by,
include_fields: includeFields,
exclude_fields: excludeFields,
page: page,
per_page: per_page
};
if (filterBy) {
query.filter_by = filterBy;
}
// Handle sorting
if (sort_by && sort_by !== "relevance") {
query.sort_by = sort_by;
}
// logger.debug(`[Typesense Query] ${JSON.stringify(query)}`);
const typesenseClient = await getTypesenseClient();
const results = await typesenseClient.collections(collection).documents().search(query);
// Debug: Log the structure of the search results
// logger.info(`UniversalPurchaseSearchHandler - Results structure: ${JSON.stringify(Object.keys(results))}`);
if (results.hits && results.hits.length > 0) {
const sampleHit = results.hits[0];
// logger.info(`UniversalPurchaseSearchHandler - Sample hit structure: ${JSON.stringify(Object.keys(sampleHit))}`);
}
if (!results || !results.hits || results.hits.length === 0) {
return {
content: [{
type: "text",
text: `No purchase records found.`
}]
};
}
// Post-filter: Remove any results with forbidden IMOs (additional security layer)
// Even though we filter at query level, we also filter results to ensure no forbidden IMOs slip through
const originalHitsCount = results.hits.length;
const filteredHits = [];
const forbiddenImos = [];
for (const hit of results.hits) {
const document = hit.document;
if (document && document.imo !== undefined && document.imo !== null) {
const imo = String(document.imo);
const isValid = await isValidVesselImoForCompany(imo);
if (isValid) {
filteredHits.push(hit);
}
else {
forbiddenImos.push(imo);
}
}
else {
// If no IMO field, include it (shouldn't happen, but safe fallback)
filteredHits.push(hit);
}
}
// Update results with filtered hits
results.hits = filteredHits;
results.found = filteredHits.length;
if (filteredHits.length === 0) {
return {
content: [{
type: "text",
text: `No purchase records found after filtering by company IMOs.`
}]
};
}
// Format results using the utility function
let title = `Universal Purchase Search Results for '${query_text}'`;
const linkHeader = `Universal purchase search result for query: '${query_text}' in fields: ${query_by}`;
return await processTypesenseResults(results, "universal_purchase_search", title, linkHeader);
}
catch (error) {
// logger.error('Error performing universal purchase search:', error);
throw new Error(`Error performing search: ${error.message}`);
}
}
export async function universal_budget_search(arguments_) {
const collection = "budget";
// Validate that only allowed properties are present
// const providedProperties = Object.keys(arguments_);
// const invalidProperties = providedProperties.filter(prop => !UniversalBudgetSearchKeys.includes(prop as any));
// if (invalidProperties.length > 0) {
// throw new Error(`Invalid properties provided: ${invalidProperties.join(', ')}. Only the following properties are allowed: ${UniversalBudgetSearchKeys.join(', ')}`);
// }
// Extract new schema parameters
const args = arguments_;
const query_by = args.query_by;
const query_text = (args.q || "").trim() || "*";
// Validate that query_by is provided when q is specified
if (args.q && args.q.trim() !== "*" && !query_by) {
throw new Error("query_by parameter is required when using full-text search with q parameter. Only the following properties are allowed: query_by, q, filter_by, sort_by, page, per_page. Please check the tool schema for universal_budget_search for the allowed properties.");
}
const filter_by = args.filter_by || "";
const sort_by = args.sort_by || "relevance";
const page = args.page || 1;
const per_page = args.per_page || 50;
try {
let filterBy = filter_by;
// Convert date fields from yyyy-mm-dd to Unix timestamps in filter_by
if (filterBy) {
const dateFields = ["date"];
for (const dateField of dateFields) {
// Handle date range operations (>=, <=, >, <, =)
const dateRegexPatterns = [
{ pattern: new RegExp(`${dateField}:>=(\\d{4}-\\d{2}-\\d{2})`, 'g'), operator: ':>=' },
{ pattern: new RegExp(`${dateField}:<=(\\d{4}-\\d{2}-\\d{2})`, 'g'), operator: ':<=' },
{ pattern: new RegExp(`${dateField}:>(\\d{4}-\\d{2}-\\d{2})`, 'g'), operator: ':>' },
{ pattern: new RegExp(`${dateField}:<(\\d{4}-\\d{2}-\\d{2})`, 'g'), operator: ':<' },
{ pattern: new RegExp(`${dateField}:(\\d{4}-\\d{2}-\\d{2})`, 'g'), operator: ':' }
];
for (const { pattern, operator } of dateRegexPatterns) {
filterBy = filterBy.replace(pattern, (match, dateStr) => {
try {
const timestamp = Math.floor(new Date(dateStr + 'T00:00:00.000Z').getTime() / 1000);
return `${dateField}${operator}${timestamp}`;
}
catch (error) {
// logger.warn(`Failed to convert date ${dateStr} for field ${dateField}:`, error);
return match; // Keep original if conversion fails
}
});
}
}
}
// Apply company IMO restrictions
filterBy = await updateTypesenseFilterWithCompanyImos(filterBy || "");
// Set up all available fields for inclusion (based on schema)
const includeFields = "imo,vesselName,category,group,period,budgetAmount,expenseAmount,date";
const excludeFields = "embedding";
const query = {
q: query_text,
query_by: query_by || "vesselName",
include_fields: includeFields,
exclude_fields: excludeFields,
page: page,
per_page: per_page
};
if (filterBy) {
query.filter_by = filterBy;
}
// Handle sorting
if (sort_by && sort_by !== "relevance") {
query.sort_by = sort_by;
}
// logger.debug(`[Typesense Query] ${JSON.stringify(query)}`);
const typesenseClient = await getTypesenseClient();
const results = await typesenseClient.collections(collection).documents().search(query);
if (!results || !results.hits || results.hits.length === 0) {
return {
content: [{
type: "text",
text: `No budget records found for the specified criteria.`
}]
};
}
// Post-filter: Remove any results with forbidden IMOs (additional security layer)
// Even though we filter at query level, we also filter results to ensure no forbidden IMOs slip through
const filteredHits = [];
const forbiddenImos = [];
for (const hit of results.hits) {
const document = hit.document;
if (document && document.imo !== undefined && document.imo !== null) {
const imo = String(document.imo);
const isValid = await isValidVesselImoForCompany(imo);
if (isValid) {
filteredHits.push(hit);
}
else {
forbiddenImos.push(imo);
}
}
else {
// If no IMO field, include it (shouldn't happen, but safe fallback)
filteredHits.push(hit);
}
}
// Update results with filtered hits
results.hits = filteredHits;
results.found = filteredHits.length;
if (filteredHits.length === 0) {
return {
content: [{
type: "text",
text: `No budget records found after filtering by company IMOs.`
}]
};
}
// Format results using the utility function
let title = `Universal Budget Search Results for '${query_text}'`;
const linkHeader = `Universal budget search result for query: '${query_text}' in fields: ${query_by}`;
return await processTypesenseResults(results, "universal_budget_search", title, linkHeader);
}
catch (error) {
// logger.error('Error performing universal budget search:', error);
throw new Error(`Error performing search: ${error.message}`);
}
}
export async function universal_expense_search(arguments_) {
const collection = "expense";
// Validate that only allowed properties are present
// const providedProperties = Object.keys(arguments_);
// const invalidProperties = providedProperties.filter(prop => !UniversalExpenseSearchKeys.includes(prop as any));
// if (invalidProperties.length > 0) {
// throw new Error(`Invalid properties provided: ${invalidProperties.join(', ')}. Only the following properties are allowed: ${UniversalExpenseSearchKeys.join(', ')}`);
// }
// Extract new schema parameters
const args = arguments_;
const query_by = args.query_by;
const query_text = (args.q || "").trim() || "*";
// Validate that query_by is provided when q is specified
if (args.q && args.q.trim() !== "*" && !query_by) {
throw new Error("query_by parameter is required when using full-text search with q parameter. Only the following properties are allowed: query_by, q, filter_by, sort_by, page, per_page. Please check the tool schema for universal_expense_search for the allowed properties.");
}
const filter_by = args.filter_by || "";
const sort_by = args.sort_by || "relevance";
const page = args.page || 1;
const per_page = args.per_page || 50;
try {
let filterBy = filter_by;
// Convert date fields from yyyy-mm-dd to Unix timestamps in filter_by
if (filterBy) {
const dateFields = ["expenseDate", "poDate"];
for (const dateField of dateFields) {
// Handle date range operations (>=, <=, >, <, =)
const dateRegexPatterns = [
{ pattern: new RegExp(`${dateField}:>=(\\d{4}-\\d{2}-\\d{2})`, 'g'), operator: ':>=' },
{ pattern: new RegExp(`${dateField}:<=(\\d{4}-\\d{2}-\\d{2})`, 'g'), operator: ':<=' },
{ pattern: new RegExp(`${dateField}:>(\\d{4}-\\d{2}-\\d{2})`, 'g'), operator: ':>' },
{ pattern: new RegExp(`${dateField}:<(\\d{4}-\\d{2}-\\d{2})`, 'g'), operator: ':<' },
{ pattern: new RegExp(`${dateField}:(\\d{4}-\\d{2}-\\d{2})`, 'g'), operator: ':' }
];
for (const { pattern, operator } of dateRegexPatterns) {
filterBy = filterBy.replace(pattern, (match, dateStr) => {
try {
const timestamp = Math.floor(new Date(dateStr + 'T00:00:00.000Z').getTime() / 1000);
return `${dateField}${operator}${timestamp}`;
}
catch (error) {
// logger.warn(`Failed to convert date ${dateStr} for field ${dateField}:`, error);
return match; // Keep original if conversion fails
}
});
}
}
}
// Apply company IMO restrictions
filterBy = await updateTypesenseFilterWithCompanyImos(filterBy || "");
// Set up all available fields for inclusion (based on schema)
const includeFields = "imo,vesselName,group,category,accountNo,accountDescription,vendor,expenseDate,expenseAmount,poAmount,expenseCategory";
const excludeFields = "embedding";
const query = {
q: query_text,
query_by: query_by || "vesselName",
include_fields: includeFields,
exclude_fields: excludeFields,
page: page,
per_page: per_page
};
if (filterBy) {
query.filter_by = filterBy;
}
// Handle sorting
if (sort_by && sort_by !== "relevance") {
query.sort_by = sort_by;
}
// logger.debug(`[Typesense Query] ${JSON.stringify(query)}`);
const typesenseClient = await getTypesenseClient();
const results = await typesenseClient.collections(collection).documents().search(query);
if (!results || !results.hits || results.hits.length === 0) {
return {
content: [{
type: "text",
text: `No expense records found for the specified criteria.`
}]
};
}
// Post-filter: Remove any results with forbidden IMOs (additional security layer)
// Even though we filter at query level, we also filter results to ensure no forbidden IMOs slip through
const filteredHits = [];
const forbiddenImos = [];
for (const hit of results.hits) {
const document = hit.document;
if (document && document.imo !== undefined && document.imo !== null) {
const imo = String(document.imo);
const isValid = await isValidVesselImoForCompany(imo);
if (isValid) {
filteredHits.push(hit);
}
else {
forbiddenImos.push(imo);
}
}
else {
// If no IMO field, include it (shouldn't happen, but safe fallback)
filteredHits.push(hit);
}
}
// Update results with filtered hits
results.hits = filteredHits;
results.found = filteredHits.length;
if (filteredHits.length === 0) {
return {
content: [{
type: "text",
text: `No expense records found after filtering by company IMOs.`
}]
};
}
// Format results using the utility function
let title = `Universal Expense Search Results for '${query_text}'`;
const linkHeader = `Universal expense search result for query: '${query_text}' in fields: ${query_by}`;
return await processTypesenseResults(results, "universal_expense_search", title, linkHeader);
}
catch (error) {
// logger.error('Error performing universal expense search:', error);
throw new Error(`Error performing search: ${error.message}`);
}
}
export async function get_complete_vessel_purchase_requisition_data(arguments_) {
// Validate that only allowed properties are present
// const providedProperties = Object.keys(arguments_);
// const invalidProperties = providedProperties.filter(prop => !GetCompleteVesselPurchaseRequisitionDataKeys.includes(prop as any));
// if (invalidProperties.length > 0) {
// throw new Error(`Invalid properties provided: ${invalidProperties.join(', ')}. Only the following properties are allowed: ${GetCompleteVesselPurchaseRequisitionDataKeys.join(', ')}`);
// }
const collection = "purchase";
const imo = arguments_.imo;
const start_date = arguments_.start_date;
const end_date = arguments_.end_date;
if (!imo) {
throw new Error("IMO number is required");
}
try {
// Validate vessel IMO belongs to company
const isValid = await isValidVesselImoForCompany(imo);
if (!isValid) {
return {
content: [{
type: "text",
text: `Access denied: Vessel IMO ${imo} does not belong to your company.`,
title: "Access Denied"
}]
};
}
// Compose filter_by string from inputs
const filterParts = [`imo:${imo}`];
if (start_date) {
const startTs = Math.floor(new Date(start_date).getTime() / 1000);
filterParts.push(`purchaseRequisitionDate:>=${startTs}`);
}
if (end_date) {
const endTs = Math.floor(new Date(end_date).getTime() / 1000);
filterParts.push(`purchaseRequisitionDate:<=${endTs}`);
}
const filterBy = filterParts.join(" && ");
// Build query for export
const query = {
filter_by: filterBy
};
// logger.debug(`[Typesense Query] ${JSON.stringify(query)}`);
// Use export method to get all documents
const typesenseClient = await getTypesenseClient();
const exportResult = await typesenseClient.collections(collection).documents().export(query);
// Debug the export result
// logger.debug(`Export result type: ${typeof exportResult}`);
// logger.debug(`Export result length: ${exportResult?.length || 'undefined'}`);
// logger.debug(`First 200 chars of export result: ${exportResult?.substring(0, 200)}`);
// Parse the export result (it's typically JSONL format)
const lines = exportResult.split('\n').filter((line) => line.trim());
// logger.debug(`Number of lines after split: ${lines.length}`);
// logger.debug(`First line: ${lines[0]}`);
const documents = lines.map((line, index) => {
try {
const doc = JSON.parse(line);
// Remove embedding field and convert dates
delete doc.embedding;
const convertedDoc = convertUnixDates(doc);
// if (index === 0) {
// // logger.debug(`First converted document keys: ${Object.keys(convertedDoc).join(', ')}`);
// }
return convertedDoc;
}
catch (error) {
// logger.error(`Error parsing line ${index}: ${line}`);
return {};
}
});
if (documents.length === 0) {
return {
content: [{
type: "text",
text: `No purchase requisitions found for vessel with IMO ${imo}.`,
}]
};
}
const vesselName = documents[0]?.vesselName || null;
const title = `Exported purchase requisitions for IMO ${imo}`;
const artifactTitle = `Purchase requisition export artifact for IMO ${imo}`;
const linkHeader = `Purchase requisition export for IMO ${imo}`;
// Process results for export tool
return {
content: [{
type: "text",
text: JSON.stringify({
summary: {
imo: imo,
vesselName: vesselName,
recordCount: documents.length,
exportType: "purchase_requisition_data"
},
data: documents
}, null, 2),
title: title,
format: "json"
}]
};
}
catch (error) {
// logger.error(`Error executing purchase requisition export: ${error}`);
throw new Error(`Error exporting purchase requisitions: ${error.message}`);
}
}
export async function get_complete_vessel_expense_data(arguments_) {
// Validate that only allowed properties are present
// const providedProperties = Object.keys(arguments_);
// const invalidProperties = providedProperties.filter(prop => !GetCompleteVesselExpenseDataKeys.includes(prop as any));
// if (invalidProperties.length > 0) {
// throw new Error(`Invalid properties provided: ${invalidProperties.join(', ')}. Only the following properties are allowed: ${GetCompleteVesselExpenseDataKeys.join(', ')}`);
// }
const collection = "expense";
const imo = arguments_.imo;
const start_date = arguments_.start_date;
const end_date = arguments_.end_date;
const excludeFieldsStr = "_id,docId,fleetId,vesselId,fleetManagerId,technicalSuperintendentId";
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_complete_vessel_expense_data for the allowed properties.");
}
try {
// Validate vessel IMO belongs to company
const isValid = await isValidVesselImoForCompany(imo);
if (!isValid) {
return {
content: [{
type: "text",
text: `Access denied: Vessel IMO ${imo} does not belong to your company.`,
title: "Access Denied"
}]
};
}
// Build filter_by string from inputs
const filterParts = [`imo:${imo}`];
if (start_date) {
const startTs = Math.floor(new Date(start_date).getTime() / 1000);
filterParts.push(`expenseDate:>=${startTs}`);
}
if (end_date) {
const endTs = Math.floor(new Date(end_date).getTime() / 1000);
filterParts.push(`expenseDate:<=${endTs}`);
}
const filterBy = filterParts.join(" && ");
// Build query for export
const query = {
filter_by: filterBy,
exclude_fields: excludeFieldsStr
};
// logger.debug(`[Typesense Query] ${JSON.stringify(query)}`);
// Use export method to get all documents
const typesenseClient = await getTypesenseClient();
const exportResult = await typesenseClient.collections(collection).documents().export(query);
// Debug the export result
// logger.debug(`Export result type: ${typeof exportResult}`);
// logger.debug(`Export result length: ${exportResult?.length || 'undefined'}`);
// logger.debug(`First 200 chars of export result: ${exportResult?.substring(0, 200)}`);
// Parse the export result (it's typically JSONL format)
const lines = exportResult.split('\n').filter((line) => line.trim());
// logger.debug(`Number of lines after split: ${lines.length}`);
// logger.debug(`First line: ${lines[0]}`);
const documents = lines.map((line, index) => {
try {
const doc = JSON.parse(line);
// Remove embedding field and convert dates
delete doc.embedding;
const convertedDoc = convertUnixDates(doc);
if (index === 0) {
// logger.debug(`First converted document keys: ${Object.keys(convertedDoc).join(', ')}`);
}
return convertedDoc;
}
catch (error) {
// logger.error(`Error parsing line ${index}: ${line}`);
return {};
}
});
if (documents.length === 0) {
return {
content: [{
type: "text",
text: `No expense records found for vessel with IMO ${imo}.`,
title: "No Expense Records Found",
format: "json"
}]
};
}
const vesselName = documents[0]?.vesselName || null;
const title = `Exported expense records for IMO ${imo}`;
// Process results for export tool
return {
content: [{
type: "text",
text: JSON.stringify({
summary: {
imo: imo,
vesselName: vesselName,
recordCount: documents.length,
exportType: "complete_expense_data"
},
data: documents
}, null, 2),
title: title,
format: "json"
}]
};
}
catch (error) {
// logger.error(`Error executing expense export: ${error}`);
throw new Error(`Error exporting expense records: ${error.message}`);
}
}
export async function get_complete_vessel_budget_data(arguments_) {
// Validate that only allowed properties are present
// const providedProperties = Object.keys(arguments_);
// const invalidProperties = providedProperties.filter(prop => !GetCompleteVesselBudgetDataKeys.includes(prop as any));
// if (invalidProperties.length > 0) {
// throw new Error(`Invalid properties provided: ${invalidProperties.join(', ')}. Only the following properties are allowed: ${GetCompleteVesselBudgetDataKeys.join(', ')}`);
// }
const collection = "budget";
const imo = arguments_.imo;
const start_date = arguments_.start_date;
const end_date = arguments_.end_date;
const excludeFieldsStr = "_id,docId,fleetId,vesselId,fleetManagerId,technicalSuperintendentId";
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_complete_vessel_budget_data for the allowed properties.");
}
try {
// Validate vessel IMO belongs to company
const isValid = await isValidVesselImoForCompany(imo);
if (!isValid) {
return {
content: [{
type: "text",
text: `Access denied: Vessel IMO ${imo} does not belong to your company.`,
title: "Access Denied"
}]
};
}
// Build filter_by string from inputs
const filterParts = [`imo:${imo}`];
if (start_date) {
const startTs = Math.floor(new Date(start_date).getTime() / 1000);
filterParts.push(`date:>=${startTs}`);
}
if (end_date) {
const endTs = Math.floor(new Date(end_date).getTime() / 1000);
filterParts.push(`date:<=${endTs}`);
}
const filterBy = filterParts.join(" && ");
// Build query for export
const query = {
filter_by: filterBy,
exclude_fields: excludeFieldsStr
};
// logger.debug(`[Typesense Query] ${JSON.stringify(query)}`);
// Use export method to get all documents
const typesenseClient = await getTypesenseClient();
const exportResult = await typesenseClient.collections(collection).documents().export(query);
// Debug the export result
// logger.debug(`Export result type: ${typeof exportResult}`);
// logger.debug(`Export result length: ${exportResult?.length || 'undefined'}`);
// logger.debug(`First 200 chars of export result: ${exportResult?.substring(0, 200)}`);
// Parse the export result (it's typically JSONL format)
const lines = exportResult.split('\n').filter((line) => line.trim());
// logger.debug(`Number of lines after split: ${lines.length}`);
// logger.debug(`First line: ${lines[0]}`);
const documents = lines.map((line, index) => {
try {
const doc = JSON.parse(line);
// Remove embedding field
delete doc.embedding;
// if (index === 0) {
// logger.debug(`Original document keys before conversion: ${Object.keys(doc).join(', ')}`);
// }
// Convert Unix timestamps to ISO strings
const convertedDoc = convertUnixDates(doc);
if (index === 0) {
// logger.debug(`Final document keys: ${Object.keys(convertedDoc).join(', ')}`);
}
return convertedDoc;
}
catch (error) {
// logger.error(`Error parsing line ${index}: ${line}`);
return {};
}
});
if (documents.length === 0) {
return {
content: [{
type: "text",
text: `No budget records found for vessel with IMO ${imo}.`,
title: "No Budget Records Found",
format: "json"
}]
};
}
const vesselName = documents[0]?.vesselName || null;
const title = `Exported budget records for IMO ${imo}`;
// Process results for export tool
return {
content: [{
type: "text",
text: JSON.stringify({
summary: {
imo: imo,
vesselName: vesselName,
recordCount: documents.length,
exportType: "complete_budget_data"
},
data: documents
}, null, 2),
title: title,
format: "json"
}]
};
}
catch (error) {
// logger.error(`Error executing budget export: ${error}`);
throw new Error(`Error exporting budget records: ${error.message}`);
}
}
export async function get_complete_fleet_purchases(arguments_) {
// logger.info("exportFleetPurchases called", 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 get_complete_fleet_purchases for the allowed properties.");
}
try {
// Validate fleet IMO belongs to company
const isValidFleetImo = await isValidFleetImoForCompany(fleetImo);
if (!isValidFleetImo) {
return {
content: [{
type: "text",
text: `Access denied: Fleet IMO ${fleetImo} does not belong to your company.`,
title: "Access Denied"
}]
};
}
// Step 1: Get vessel IMO list from fleet
// logger.info(`Step 1: Getting vessel IMO list for fleet IMO ${fleetImo}`);
const vesselImoList = await getVesselImoListFromFleet(Number(fleetImo));
if (!vesselImoList || vesselImoList.length === 0) {
return {
content: [{
type: "text",
text: `No vessels found for fleet IMO: ${fleetImo}`,
title: "No Vessels Found"
}]
};
}
// Filter IMO list to only include company IMOs
const filteredImoList = await filterImoListByCompany(vesselImoList);
if (filteredImoList.length === 0) {
return {
content: [{
type: "text",
text: `No vessels found for fleet IMO ${fleetImo} that belong to your company.`,
title: "No Accessible Vessels Found"
}]
};
}
// logger.info(`Found ${filteredImoList.length} vessels in fleet IMO ${fleetImo} (filtered from ${vesselImoList.length} total)`);
// Step 2: Export purchases for all vessels in the fleet
// logger.info(`Step 2: Exporting purchases for ${filteredImoList.length} vessels`);
const purchaseDocuments = await exportPurchasesForImoList(filteredImoList, startDate, endDate);
if (!purchaseDocuments || purchaseDocuments.length === 0) {
return {
content: [{
type: "text",
text: `No purchase records found for fleet IMO ${fleetImo} with the specified criteria`,
title: "No Purchase Records Found"
}]
};
}
// logger.info(`Found ${purchaseDocuments.length} purchase records for fleet IMO ${fleetImo}`);
// Step 3: Process documents and convert Unix dates
// logger.info(`Step 3: Processing ${purchaseDocuments.length} records for JSON export`);
const processedDocuments = purchaseDocuments.map(doc => {
// Remove embedding field and convert dates
delete doc.embedding;
return convertUnixDates(doc);
});
const fleetName = purchaseDocuments.length > 0 ? purchaseDocuments[0].fleetName : null;
const title = `Exported purchase records for fleet IMO ${fleetImo}`;
// Process results for export tool
return {
content: [{
type: "text",
text: JSON.stringify({
summary: {
fleetImo: fleetImo,
fleetName: fleetName,
recordCount: processedDocuments.length,
exportType: "fleet_purchase_data",
vesselCount: vesselImoList.length
},
data: processedDocuments
}, null, 2),
title: title,
format: "json"
}]
};
}
catch (error) {
// logger.error(`Error exporting fleet purchases for IMO ${fleetImo}: ${error}`);
throw new Error(`Error exporting fleet purchases: ${String(error)}`);
}
}
export async function get_complete_fleet_budgets(arguments_) {
// logger.info("exportFleetBudgets called", 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 get_complete_fleet_budgets 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(Number(fleetImo));
if (!vesselImoList || vesselImoList.length === 0) {
return {
content: [{
type: "text",
text: `No vessels found for fleet IMO: ${fleetImo}`,
title: "No Vessels Found"
}]
};
}
// Filter IMO list to only include company IMOs
const filteredImoList = await filterImoListByCompany(vesselImoList);
if (filteredImoList.length === 0) {
return {
content: [{
type: "text",
text: `No vessels found for fleet IMO ${fleetImo} that belong to your company.`,
title: "No Accessible Vessels Found"
}]
};
}
// logger.info(`Found ${filteredImoList.length} vessels in fleet IMO ${fleetImo} (filtered from ${vesselImoList.length} total)`);
// Step 2: Export budgets for all vessels in the fleet
// logger.info(`Step 2: Exporting budgets for ${filteredImoList.length} vessels`);
const budgetDocuments = await exportBudgetsForImoList(filteredImoList, startDate, endDate);
if (!budgetDocuments || budgetDocuments.length === 0) {
return {
content: [{
type: "text",
text: `No budget records found for fleet IMO ${fleetImo} with the specified criteria`,
title: "No Budget Records Found"
}]
};
}
// logger.info(`Found ${budgetDocuments.length} budget records for fleet IMO ${fleetImo}`);
// Step 3: Process documents and convert Unix dates
// logger.info(`Step 3: Processing ${budgetDocuments.length} records for JSON export`);
const processedDocuments = budgetDocuments.map(doc => {
// Remove embedding field and convert dates
delete doc.embedding;
return convertUnixDates(doc);
});
const fleetName = budgetDocuments.length > 0 ? budgetDocuments[0].fleetName : null;
const title = `Exported budget records for fleet IMO ${fleetImo}`;
// Process results for export tool
return {
content: [{
type: "text",
text: JSON.stringify({
summary: {
fleetImo: fleetImo,
fleetName: fleetName,
recordCount: processedDocuments.length,
exportType: "fleet_budget_data",
vesselCount: vesselImoList.length
},
data: processedDocuments
}, null, 2),
title: title,
format: "json"
}]
};
}
catch (error) {
// logger.error(`Error exporting fleet budgets for IMO ${fleetImo}: ${error}`);
throw new Error(`Error exporting fleet budgets: ${String(error)}`);
}
}
export async function get_complete_fleet_expenses(arguments_) {
// logger.info("exportFleetExpenses called", 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 get_complete_fleet_expenses 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(Number(fleetImo));
if (!vesselImoList || vesselImoList.length === 0) {
return {
content: [{
type: "text",
text: `No vessels found for fleet IMO: ${fleetImo}`,
title: "No Vessels Found"
}]
};
}
// Filter IMO list to only include company IMOs
const filteredImoList = await filterImoListByCompany(vesselImoList);
if (filteredImoList.length === 0) {
return {
content: [{
type: "text",
text: `No vessels found for fleet IMO ${fleetImo} that belong to your company.`,
title: "No Accessible Vessels Found"
}]
};
}
// logger.info(`Found ${filteredImoList.length} vessels in fleet IMO ${fleetImo} (filtered from ${vesselImoList.length} total)`);
// Step 2: Export expenses for all vessels in the fleet
// logger.info(`Step 2: Exporting expenses for ${filteredImoList.length} vessels`);
const expenseDocuments = await exportExpensesForImoList(filteredImoList, startDate, endDate);
if (!expenseDocuments || expenseDocuments.length === 0) {
return {
content: [{
type: "text",
text: `No expense records found for fleet IMO ${fleetImo} with the specified criteria`,
title: "No Expense Records Found"
}]
};
}
// logger.info(`Found ${expenseDocuments.length} expense records for fleet IMO ${fleetImo}`);
// Step 3: Process documents and convert Unix dates
// logger.info(`Step 3: Processing ${expenseDocuments.length} records for JSON export`);
const processedDocuments = expenseDocuments.map(doc => {
// Remove embedding field and convert dates
delete doc.embedding;
return convertUnixDates(doc);
});
const fleetName = expenseDocuments.length > 0 ? expenseDocuments[0].fleetName : null;
const title = `Exported expense records for fleet IMO ${fleetImo}`;
// Process results for export tool
return {
content: [{
type: "text",
text: JSON.stringify({
summary: {
fleetImo: fleetImo,
fleetName: fleetName,
recordCount: processedDocuments.length,
exportType: "fleet_expense_data",
vesselCount: vesselImoList.length
},
data: processedDocuments
}, null, 2),
title: title,
format: "json"
}]
};
}
catch (error) {
// logger.error(`Error exporting fleet expenses for IMO ${fleetImo}: ${error}`);
throw new Error(`Error exporting fleet expenses: ${String(error)}`);
}
}
// To Do - Add validations for the argument
export async function fleet_procurement_and_budget_information(args) {
// logger.info("fleet_procurement_and_budget_information called", args);
try {
const imo = args.imo;
const questionNo = args.question_no;
if (!imo) {
throw new Error("imo is required. Only the following properties are allowed: imo, question_no. Please check the tool schema for fleet_procurement_and_budget_information for the allowed properties.");
}
if (!questionNo) {
throw new Error("Question number is required");
}
// Validate fleet IMO belongs to company (this tool uses fleet IMO)
const isValidFleetImo = await isValidFleetImoForCompany(imo);
if (!isValidFleetImo) {
return {
content: [{
type: "text",
text: `Access denied: Fleet IMO ${imo} does not belong to your company.`,
title: "Access Denied"
}]
};
}
// Validate question number against allowed values
const allowedQuestions = [11, 121, 257];
if (!allowedQuestions.includes(questionNo)) {
return {
content: [{
type: "text",
text: `Invalid question number: ${questionNo}. Allowed values are: ${allowedQuestions.join(', ')}`
}]
};
}
// logger.info(`Fetching fleet management information for questionNo: ${questionNo}, fleet IMO: ${imo}`);
// Initialize database connection
const qaDbName = process.env.QA_DB_NAME || 'syia-etl-dev';
const qaMongoUri = process.env.QA_MONGO_URI || '';
// Use the helper function directly
const result = await fetchQADetailsAndCreateResponse(imo, questionNo, `fleet_procurement_and_budget_information_q${questionNo}`, `fleet_procurement_and_budget_information_q${questionNo}`, qaDbName, qaMongoUri, 'vesselinfos');
return result;
}
catch (error) {
// logger.error("Error in getFleetManagementInformation", { error });
throw new Error(`Error retrieving fleet management information: ${error instanceof Error ? error.message : String(error)}`);
}
}
export async function get_vessel_purchase_order_information(args) {
// logger.info("getVesselPurchaseOrderInformation called", args);
// Initialize database connection (moved outside try block for error handling)
const qaDbName = process.env.QA_DB_NAME || 'syia-etl-dev';
const qaMongoUri = process.env.QA_MONGO_URI || '';
try {
const imo = args.imo;
const questionNo = args.question_no;
if (!imo) {
throw new Error("imo is required. Only the following properties are allowed: imo, question_no. Please check the tool schema for get_vessel_purchase_order_information for the allowed properties.");
}
if (!questionNo) {
throw new Error("Question number is required");
}
// Validate vessel IMO belongs to company
const isValid = await isValidVesselImoForCompany(imo);
if (!isValid) {
return {
content: [{
type: "text",
text: `Access denied: Vessel IMO ${imo} does not belong to your company.`,
title: "Access Denied"
}]
};
}
// Validate question number against allowed values
const allowedQuestions = [122, 123];
if (!allowedQuestions.includes(questionNo)) {
return {
content: [{
type: "text",
text: `Invalid question number: ${questionNo}. Allowed values are: ${allowedQuestions.join(', ')}`
}]
};
}
// logger.info(`Fetching vessel purchase order information for questionNo: ${questionNo}, vessel IMO: ${imo}`);
// Use the helper function directly
const result = await fetchQADetailsAndCreateResponse(imo, questionNo, `get_vessel_purchase_order_information_q${questionNo}`, `vessel_purchase_order_q${questionNo}`, qaDbName, qaMongoUri, 'vesselinfos');
return result;
}
catch (error) {
// logger.error("Error in getVesselPurchaseOrderInformation", { error });
throw new Error(`Error retrieving vessel purchase order information: URI=${qaMongoUri} DB=${qaDbName} - ${error instanceof Error ? error.message : String(error)}`);
}
}
export async function get_vessel_budget_expense_information(args) {
// logger.info("getVesselBudgetExpenseInformation called", args);
try {
const imo = args.imo;
const questionNo = args.question_no;
if (!imo) {
throw new Error("imo is required. Only the following properties are allowed: imo, question_no. Please check the tool schema for get_vessel_budget_expense_information for the allowed properties.");
}
if (!questionNo) {
throw new Error("Question number is required");
}
// Validate vessel IMO belongs to company
const isValid = await isValidVesselImoForCompany(imo);
if (!isValid) {
return {
content: [{
type: "text",
text: `Access denied: Vessel IMO ${imo} does not belong to your company.`,
title: "Access Denied"
}]
};
}
// Validate question number against allowed values
const allowedQuestions = [9, 12, 13, 14, 15, 16, 17];
if (!allowedQuestions.includes(questionNo)) {
return {
content: [{
type: "text",
text: `Invalid question number: ${questionNo}. Allowed values are: ${allowedQuestions.join(', ')}`
}]
};
}
// logger.info(`Fetching vessel budget expense information for questionNo: ${questionNo}, vessel IMO: ${imo}`);
// Initialize database connection
const qaDbName = process.env.QA_DB_NAME || 'syia-etl-dev';
const qaMongoUri = process.env.QA_MONGO_URI || '';
// Use the helper function directly
const result = await fetchQADetailsAndCreateResponse(imo, questionNo, `get_vessel_budget_expense_information_q${questionNo}`, `vessel_budget_expense_q${questionNo}`, qaDbName, qaMongoUri, 'vesselinfos');
return result;
}
catch (error) {
// logger.error("Error in getVesselBudgetExpenseInformation", { error });
throw new Error(`Error retrieving vessel budget expense information: ${error instanceof Error ? error.message : String(error)}`);
}
}