UNPKG

purchase-mcp-server

Version:

Purchase and budget management server handling requisitions, purchase orders, expenses, budgets, and vendor management with ERP access for data extraction

564 lines 21.2 kB
import { getEtlDevClient } from "./mongodb.js"; import { getEtlDevDbName } from "./mongodb.js"; import { logger } from "./logger.js"; import { getMongoClient } from "./mongodb.js"; import { config } from "./config.js"; import { getCompanyImoNumbers } from "./imoUtils.js"; export async function fetchQADetails(imo, qaId) { try { const client = await getEtlDevClient(); const db = client.db(getEtlDevDbName()); const vesselinfos = db.collection('vesselinfos'); const query = { 'imo': parseInt(imo), 'questionNo': qaId }; const projection = { '_id': 0, 'imo': 1, 'vesselName': 1, 'refreshDate': 1, 'answer': 1 }; const mongoResult = await vesselinfos.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) { res.answer = await addComponentData(res.answer, imo); } // Get vessel QnA snapshot link try { res.link = await getVesselQnASnapshot(imo, qaId.toString()); } catch (error) { res.link = null; } return res; } catch (error) { logger.error('Error fetching QA details:', error); throw new Error(`Error fetching QA details: ${error.message}`); } } export async function getComponentData(componentId) { const match = componentId.match(/^(\d+)_(\d+)_(\d+)$/); if (!match) { return `⚠️ Invalid component_id format: ${componentId}`; } const [, componentNumber, questionNumber, imo] = match; const componentNo = `${componentNumber}_${questionNumber}_${imo}`; try { const client = await getEtlDevClient(); const db = client.db(getEtlDevDbName()); const collection = db.collection('vesselinfocomponents'); 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"; } // Extract headers excluding lineitem const headers = doc.data.headers .filter((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.lineitem) // Exclude lineitem .map((cell) => { if (cell.value && cell.link) { return `[${cell.value}](${cell.link})`; } else if (cell.status && cell.color) { return cell.status; } return 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}`); } } export async function addComponentData(answer, imo) { const pattern = /httpsdev\.syia\.ai\/chat\/ag-grid-table\?component=(\d+_\d+)/g; const matches = Array.from(answer.matchAll(pattern)); let result = answer; for (const match of matches) { const component = match[1]; try { const replacement = await getComponentData(`${component}_${imo}`); result = result.replace(match[0], replacement); } catch (error) { logger.error('Error replacing component data:', error); } } return result; } export async function getVesselQnASnapshot(imo, questionNo) { try { // API endpoint const snapshotUrl = `https://dev-api.siya.com/v1.0/vessel-info/qna-snapshot/${imo}/${questionNo}`; // Authentication token const jwtToken = "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7ImlkIjoiNjRkMzdhMDM1Mjk5YjFlMDQxOTFmOTJhIiwiZmlyc3ROYW1lIjoiU3lpYSIsImxhc3ROYW1lIjoiRGV2IiwiZW1haWwiOiJkZXZAc3lpYS5haSIsInJvbGUiOiJhZG1pbiIsInJvbGVJZCI6IjVmNGUyODFkZDE4MjM0MzY4NDE1ZjViZiIsImlhdCI6MTc0MDgwODg2OH0sImlhdCI6MTc0MDgwODg2OCwiZXhwIjoxNzcyMzQ0ODY4fQ.1grxEO0aO7wfkSNDzpLMHXFYuXjaA1bBguw2SJS9r2M"; // 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 && "resultData" in data) { return data.resultData; } return data; } catch (error) { logger.error(`Error fetching vessel QnA snapshot for IMO ${imo}, Question ${questionNo}:`, error); return null; } } export async function getVesselQnASnapshotHandler(arguments_) { const { imo, questionNo } = arguments_; if (!imo || !questionNo) { throw new Error("Both IMO and questionNo are required"); } try { const result = await getVesselQnASnapshot(imo, questionNo); if (!result) { return [{ type: "text", text: `No QnA snapshot data found for vessel IMO: ${imo}, Question: ${questionNo}` }]; } return [{ type: "text", text: JSON.stringify(result, null, 2), title: `Vessel QnA Snapshot - IMO: ${imo}, Question: ${questionNo}`, format: "json" }]; } catch (error) { logger.error(`Error getting vessel QnA snapshot:`, error); throw new Error(`Error getting vessel QnA snapshot: ${error.message}`); } } export async function insertDataLinkToMongoDB(link, type, sessionId, imo, vesselName) { try { const mongoClient = await getMongoClient(); const db = mongoClient.db(config.dbName); await db.collection('data_links').insertOne({ link, type, sessionId, imo, vesselName, createdAt: new Date() }); } catch (error) { logger.error('Error inserting data link to MongoDB:', error); throw new Error(`Error inserting data link to MongoDB: ${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 convertUnixDates(document) { // Create a shallow copy to avoid modifying the original object const result = { ...document }; const dateFields = [ "date", "purchaseRequisitionDate", "purchaseOrderIssuedDate", "orderReadinessDate" ]; for (const field of dateFields) { const value = result[field]; if (typeof value === "number" && Number.isFinite(value)) { result[field] = new Date(value * 1000).toISOString(); } } return result; } export async function getDataLink(data) { try { const url = "https://dev-api.siya.com/v1.0/vessel-info/qna-snapshot"; const headers = { "Content-Type": "application/json", "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7ImlkIjoiNjRkMzdhMDM1Mjk5YjFlMDQxOTFmOTJhIiwiZmlyc3ROYW1lIjoiU3lpYSIsImxhc3ROYW1lIjoiRGV2IiwiZW1haWwiOiJkZXZAc3lpYS5haSIsInJvbGUiOiJhZG1pbiIsInJvbGVJZCI6IjVmNGUyODFkZDE4MjM0MzY4NDE1ZjViZiIsImlhdCI6MTc0MDgwODg2OH0sImlhdCI6MTc0MDgwODg2OCwiZXhwIjoxNzcyMzQ0ODY4fQ.1grxEO0aO7wfkSNDzpLMHXFYuXjaA1bBguw2SJS9r2M" }; const payload = { data }; const response = await fetch(url, { method: 'POST', headers, body: JSON.stringify(payload) }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const result = await response.json(); if (result.status === "OK") { return result.resultData; } else { 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 convertToCsvTable(documents) { if (!documents.length) return ""; const headers = Object.keys(documents[0]); // Escape cell values for CSV const escapeValue = (value) => { const str = value != null ? String(value) : ""; const needsEscaping = /[",\n]/.test(str); const escaped = str.replace(/"/g, '""'); // escape double quotes return needsEscaping ? `"${escaped}"` : escaped; }; const rows = documents.map(doc => headers.map(header => escapeValue(doc[header])).join(',')); return [headers.join(','), ...rows].join('\n'); } export async function getListOfArtifacts(toolName, linkData) { try { const artifacts = []; for (const link of linkData) { if (link.url) { const artifactData = await getArtifact(toolName, link.url); artifacts.push({ type: "text", text: JSON.stringify(artifactData, null, 2) }); } } return artifacts; } catch (error) { logger.error("Error getting list of artifacts:", error); throw new Error(`Error getting list of artifacts: ${error.message}`); } } export async function fetchQADetailsAndCreateResponse(imo, questionNo, functionName, linkHeader, session_id = "testing") { if (!imo) { throw new Error("IMO is required"); } try { // Fetch QA details const result = await fetchQADetails(imo, questionNo); const link = result.link; const vesselName = result.vesselName; // Insert data link to MongoDB await insertDataLinkToMongoDB(link, linkHeader, session_id, imo, vesselName); // Get artifact data const artifactData = await getArtifact(functionName, link); // Create content responses const content = { type: "text", text: JSON.stringify(result, null, 2) }; const artifact = { type: "text", text: JSON.stringify(artifactData, null, 2) }; return [content, artifact]; } catch (error) { logger.error(`Error in ${functionName}:`, error); throw new Error(`Error in ${functionName}: ${error.message}`); } } export async function processTypesenseResults(searchResult, toolName, title, session_id = "testing", linkHeader, artifactTitle) { try { if (!searchResult || !searchResult.hits || searchResult.hits.length === 0) { return [{ type: "text", text: "No records found for the specified criteria.", title: "No Results Found", format: "json" }]; } // Process search results into the standard format const hits = searchResult.hits || []; const documents = await Promise.all(hits.map(async (hit) => { if (!hit.document) { logger.warn(`Hit is missing document property in ${toolName}`); return {}; } // Create a shallow copy of the document const document = { ...hit.document }; // Remove embedding field to reduce response size if (document.embedding) { delete document.embedding; } // Convert Unix timestamps to readable dates return await convertUnixDates(document); })); // Get data link const dataLink = await getDataLink(documents); // Get vessel name and IMO from hits let vesselName = null; let imo = null; try { vesselName = searchResult.hits[0]?.document?.vesselName; imo = searchResult.hits[0]?.document?.imo; } catch (error) { logger.warn(`Could not get vessel name or IMO from hits in ${toolName}`); } // Insert the data link to mongodb collection await insertDataLinkToMongoDB(dataLink, linkHeader, session_id, imo || "", vesselName || ""); // Format results in the standard structure const formattedResults = { found: searchResult.found || 0, out_of: searchResult.out_of || 0, page: searchResult.page || 1, hits: documents }; // Get artifact data const artifactData = await getArtifact(toolName, dataLink); // Create content response const content = { type: "text", text: JSON.stringify(formattedResults, null, 2), title, format: "json" }; // Create artifact response const artifact = { type: "text", text: JSON.stringify(artifactData, null, 2), title: artifactTitle || title, format: "json" }; return [content, artifact]; } catch (error) { logger.error(`Error processing Typesense results for ${toolName}:`, error); return [{ type: "text", text: `Error processing results: ${error.message}`, title: "Error", format: "json" }]; } } export async function formatTypesenseResults(searchResult, toolName, title, dataLink, artifactTitle) { try { // Process search results into the standard format const hits = searchResult.hits || []; const documents = await Promise.all(hits.map(async (hit) => { if (!hit.document) { logger.warn(`Hit is missing document property in ${toolName}`); return {}; } // Create a shallow copy of the document const document = { ...hit.document }; // Remove embedding field to reduce response size if (document.embedding) { delete document.embedding; } // Convert Unix timestamps to readable dates return await convertUnixDates(document); })); // Format results in the standard structure const formattedResults = { found: searchResult.found || 0, out_of: searchResult.out_of || 0, page: searchResult.page || 1, hits: documents }; // Get artifact data const artifactData = await getArtifact(toolName, dataLink); const testing_json = { found: searchResult.found || 0, out_of: searchResult.out_of || 0, page: searchResult.page || 1, hits: "testing" }; // Create content response const content = { type: "text", text: JSON.stringify(testing_json, null, 2), title, format: "json" }; // Create artifact response const artifact = { type: "text", text: JSON.stringify(artifactData, null, 2), title: artifactTitle || title, format: "json" }; return [content, artifact]; } catch (error) { logger.error(`Error formatting Typesense results for ${toolName}:`, error); return [{ type: "text", text: `Error formatting results: ${error.message}`, title: "Error", format: "json" }]; } } export async function processTypesenseExportResults(documents, toolName, title, artifactTitle, session_id, linkHeader, imo, vesselName) { try { // Process documents const processedDocuments = await Promise.all(documents.map(async (doc) => { const document = { ...doc }; // Remove embedding field to reduce response size if (document.embedding) { delete document.embedding; } // Convert any Unix timestamps to readable dates return await convertUnixDates(document); })); // Get data link const dataLink = await getDataLink(processedDocuments); // Insert the data link to mongodb collection await insertDataLinkToMongoDB(dataLink, linkHeader, session_id, imo || "", vesselName || ""); // Format results in the standard structure const formattedResults = { found: processedDocuments.length, out_of: processedDocuments.length, page: 1, hits: processedDocuments }; // Get artifact data const artifactData = await getArtifact(toolName, dataLink); // Create content response const content = { type: "text", text: JSON.stringify(formattedResults, null, 2), title, format: "json" }; // Create artifact response const artifact = { type: "text", text: JSON.stringify(artifactData, null, 2), title: artifactTitle, format: "json" }; return [content, artifact]; } catch (error) { logger.error(`Error processing Typesense export results for ${toolName}:`, error); return [{ type: "text", text: `Error processing results: ${error.message}`, title: "Error", format: "json" }]; } } export async function updateTypesenseFilterWithCompanyImos(filter) { const companyName = config.companyName; if (companyName?.toLowerCase() === "synergy") { return filter; } const companyImos = getCompanyImoNumbers(); if (companyImos.length > 0) { if (!filter.includes("imo:")) { // Use the correct syntax for numerical IMO values (without :=) filter += ` && imo:[${companyImos.join(",")}]`; } } return filter; } //# sourceMappingURL=helper_functions.js.map