UNPKG

defect-inspection-tools-mcp-server

Version:

Defect inspection tools server handling defect detection, analysis, and reporting with AI/ML capabilities for quality control

408 lines 17 kB
import { logger } from './logger.js'; import { SanitizationMiddleware } from '../middleware/sanitization-middleware.js'; export class TypesenseService { constructor(typesenseService) { this.typesenseService = typesenseService; } // Search across maritime collections async searchMaritimeData(query, collections = TypesenseService.MARITIME_COLLECTIONS, params = {}) { try { const sanitizedQuery = SanitizationMiddleware.sanitizeSearchQuery(query); const searchParams = { ...TypesenseService.DEFAULT_SEARCH_PARAMS, ...params, q: sanitizedQuery }; logger.info('Starting Typesense maritime search', { query: SanitizationMiddleware.sanitizeForLogging(sanitizedQuery), collections, params: searchParams }); // Multi-collection search const results = []; for (const collection of collections) { try { const collectionResult = await this.searchCollection(collection, searchParams); if (collectionResult.found > 0) { results.push(collectionResult); } } catch (error) { logger.warn(`Search failed for collection ${collection}`, { collection, error: error.message }); } } // Combine results const combinedResult = this.combineSearchResults(results); logger.info('Typesense maritime search completed', { query: SanitizationMiddleware.sanitizeForLogging(sanitizedQuery), totalFound: combinedResult.found, searchTime: combinedResult.search_time_ms, collectionsSearched: collections.length, resultsReturned: combinedResult.hits.length }); return combinedResult; } catch (error) { logger.error('Typesense maritime search failed', { query: SanitizationMiddleware.sanitizeForLogging(query), error: error.message }); throw error; } } // Search specific collection async searchCollection(collection, params) { try { const sanitizedCollection = SanitizationMiddleware.sanitizeString(collection, 100); const sanitizedParams = this.sanitizeSearchParams(params); const response = await this.typesenseService.get(`/collections/${sanitizedCollection}/documents/search`, sanitizedParams); if (!response.success) { throw new Error(`Typesense search failed: ${response.error}`); } const result = response.data?.data; // Sanitize search result const sanitizedResult = SanitizationMiddleware.sanitizeToolResponse(result); return sanitizedResult; } catch (error) { logger.error('Typesense collection search failed', { collection, error: error.message }); throw error; } } // Search defects with maritime-specific filters async searchDefects(query, filters = {}, params = {}) { try { const sanitizedQuery = SanitizationMiddleware.sanitizeSearchQuery(query); const sanitizedFilters = SanitizationMiddleware.sanitizeToolArguments(filters); // Build filter string const filterParts = []; if (sanitizedFilters.vessel_name) { filterParts.push(`vessel_name:${sanitizedFilters.vessel_name}`); } if (sanitizedFilters.imo_number) { filterParts.push(`imo_number:${sanitizedFilters.imo_number}`); } if (sanitizedFilters.defect_type) { filterParts.push(`defect_type:${sanitizedFilters.defect_type}`); } if (sanitizedFilters.severity) { filterParts.push(`severity:${sanitizedFilters.severity}`); } if (sanitizedFilters.status) { filterParts.push(`status:${sanitizedFilters.status}`); } if (sanitizedFilters.inspection_date_range) { filterParts.push(`inspection_date:${sanitizedFilters.inspection_date_range}`); } if (sanitizedFilters.stage) { filterParts.push(`stage:${sanitizedFilters.stage}`); } const searchParams = { ...TypesenseService.DEFAULT_SEARCH_PARAMS, ...params, q: sanitizedQuery, query_by: 'defect_type,description,location,vessel_name,corrective_action', filter_by: filterParts.join(' && '), facet_by: 'defect_type,severity,status,stage,vessel_name', sort_by: 'inspection_date:desc,severity:desc', highlight_fields: 'description,corrective_action' }; return await this.searchCollection('defect', searchParams); } catch (error) { logger.error('Defect search failed', { query: SanitizationMiddleware.sanitizeForLogging(query), filters: SanitizationMiddleware.sanitizeForLogging(filters), error: error.message }); throw error; } } // Search vessels with maritime-specific filters async searchVessels(query, filters = {}, params = {}) { try { const sanitizedQuery = SanitizationMiddleware.sanitizeSearchQuery(query); const sanitizedFilters = SanitizationMiddleware.sanitizeToolArguments(filters); // Build filter string const filterParts = []; if (sanitizedFilters.imo_number) { filterParts.push(`imo_number:${sanitizedFilters.imo_number}`); } if (sanitizedFilters.vessel_type) { filterParts.push(`vessel_type:${sanitizedFilters.vessel_type}`); } if (sanitizedFilters.flag_state) { filterParts.push(`flag_state:${sanitizedFilters.flag_state}`); } if (sanitizedFilters.class_society) { filterParts.push(`class_society:${sanitizedFilters.class_society}`); } if (sanitizedFilters.built_year_range) { filterParts.push(`built_year:${sanitizedFilters.built_year_range}`); } if (sanitizedFilters.deadweight_range) { filterParts.push(`deadweight:${sanitizedFilters.deadweight_range}`); } const searchParams = { ...TypesenseService.DEFAULT_SEARCH_PARAMS, ...params, q: sanitizedQuery, query_by: 'vessel_name,imo_number,vessel_type,flag_state,class_society,owner,manager', filter_by: filterParts.join(' && '), facet_by: 'vessel_type,flag_state,class_society,built_year', sort_by: 'vessel_name:asc', highlight_fields: 'vessel_name,owner,manager' }; return await this.searchCollection('vessel', searchParams); } catch (error) { logger.error('Vessel search failed', { query: SanitizationMiddleware.sanitizeForLogging(query), filters: SanitizationMiddleware.sanitizeForLogging(filters), error: error.message }); throw error; } } // Search inspections with maritime-specific filters async searchInspections(query, filters = {}, params = {}) { try { const sanitizedQuery = SanitizationMiddleware.sanitizeSearchQuery(query); const sanitizedFilters = SanitizationMiddleware.sanitizeToolArguments(filters); // Build filter string const filterParts = []; if (sanitizedFilters.vessel_name) { filterParts.push(`vessel_name:${sanitizedFilters.vessel_name}`); } if (sanitizedFilters.imo_number) { filterParts.push(`imo_number:${sanitizedFilters.imo_number}`); } if (sanitizedFilters.inspection_type) { filterParts.push(`inspection_type:${sanitizedFilters.inspection_type}`); } if (sanitizedFilters.inspector) { filterParts.push(`inspector:${sanitizedFilters.inspector}`); } if (sanitizedFilters.port) { filterParts.push(`port:${sanitizedFilters.port}`); } if (sanitizedFilters.date_range) { filterParts.push(`inspection_date:${sanitizedFilters.date_range}`); } if (sanitizedFilters.status) { filterParts.push(`status:${sanitizedFilters.status}`); } const searchParams = { ...TypesenseService.DEFAULT_SEARCH_PARAMS, ...params, q: sanitizedQuery, query_by: 'inspection_type,inspector,port,vessel_name,findings,recommendations', filter_by: filterParts.join(' && '), facet_by: 'inspection_type,inspector,port,status', sort_by: 'inspection_date:desc', highlight_fields: 'findings,recommendations' }; return await this.searchCollection('inspection', searchParams); } catch (error) { logger.error('Inspection search failed', { query: SanitizationMiddleware.sanitizeForLogging(query), filters: SanitizationMiddleware.sanitizeForLogging(filters), error: error.message }); throw error; } } // Get collection information async getCollectionInfo(collection) { try { const sanitizedCollection = SanitizationMiddleware.sanitizeString(collection, 100); const response = await this.typesenseService.get(`/collections/${sanitizedCollection}`); if (!response.success) { throw new Error(`Failed to get collection info: ${response.error}`); } const collectionInfo = response.data?.data; logger.info('Collection info retrieved', { collection: sanitizedCollection, numDocuments: collectionInfo.num_documents, fieldsCount: collectionInfo.fields.length }); return collectionInfo; } catch (error) { logger.error('Failed to get collection info', { collection, error: error.message }); throw error; } } // List all collections async listCollections() { try { const response = await this.typesenseService.get('/collections'); if (!response.success) { throw new Error(`Failed to list collections: ${response.error}`); } const collections = response.data?.data; logger.info('Collections listed', { collectionsCount: collections.length, collections: collections.map(c => c.name) }); return collections; } catch (error) { logger.error('Failed to list collections', { error: error.message }); throw error; } } // Upsert document async upsertDocument(collection, document) { try { const sanitizedCollection = SanitizationMiddleware.sanitizeString(collection, 100); const sanitizedDocument = SanitizationMiddleware.sanitizeToolArguments(document); const response = await this.typesenseService.post(`/collections/${sanitizedCollection}/documents`, sanitizedDocument, { params: { action: sanitizedDocument.action || 'upsert' } }); if (!response.success) { throw new Error(`Failed to upsert document: ${response.error}`); } logger.info('Document upserted successfully', { collection: sanitizedCollection, documentId: sanitizedDocument.id, action: sanitizedDocument.action || 'upsert' }); return response.data?.data; } catch (error) { logger.error('Failed to upsert document', { collection, error: error.message }); throw error; } } // Delete document async deleteDocument(collection, documentId) { try { const sanitizedCollection = SanitizationMiddleware.sanitizeString(collection, 100); const sanitizedDocumentId = SanitizationMiddleware.sanitizeString(documentId, 100); const response = await this.typesenseService.delete(`/collections/${sanitizedCollection}/documents/${sanitizedDocumentId}`); if (!response.success) { throw new Error(`Failed to delete document: ${response.error}`); } logger.info('Document deleted successfully', { collection: sanitizedCollection, documentId: sanitizedDocumentId }); return response.data?.data; } catch (error) { logger.error('Failed to delete document', { collection, documentId, error: error.message }); throw error; } } // Private helper methods sanitizeSearchParams(params) { const sanitized = { ...params }; // Sanitize string parameters if (sanitized.q) { sanitized.q = SanitizationMiddleware.sanitizeSearchQuery(sanitized.q); } if (sanitized.query_by) { sanitized.query_by = SanitizationMiddleware.sanitizeString(sanitized.query_by, 500); } if (sanitized.filter_by) { sanitized.filter_by = SanitizationMiddleware.sanitizeString(sanitized.filter_by, 1000); } if (sanitized.sort_by) { sanitized.sort_by = SanitizationMiddleware.sanitizeString(sanitized.sort_by, 200); } if (sanitized.facet_by) { sanitized.facet_by = SanitizationMiddleware.sanitizeString(sanitized.facet_by, 200); } if (sanitized.include_fields) { sanitized.include_fields = SanitizationMiddleware.sanitizeString(sanitized.include_fields, 200); } if (sanitized.exclude_fields) { sanitized.exclude_fields = SanitizationMiddleware.sanitizeString(sanitized.exclude_fields, 200); } // Validate numeric parameters if (sanitized.page && sanitized.page < 1) { sanitized.page = 1; } if (sanitized.per_page && sanitized.per_page > 250) { sanitized.per_page = 250; } if (sanitized.max_facet_values && sanitized.max_facet_values > 100) { sanitized.max_facet_values = 100; } return sanitized; } combineSearchResults(results) { if (results.length === 0) { return { found: 0, out_of: 0, page: 1, request_params: {}, search_time_ms: 0, hits: [] }; } if (results.length === 1) { return results[0]; } // Combine results from multiple collections const combined = { found: results.reduce((sum, r) => sum + r.found, 0), out_of: results.reduce((sum, r) => sum + r.out_of, 0), page: results[0].page, request_params: results[0].request_params, search_time_ms: Math.max(...results.map(r => r.search_time_ms)), hits: [] }; // Merge hits and sort by relevance for (const result of results) { combined.hits.push(...result.hits); } // Sort by text match score combined.hits.sort((a, b) => (b.text_match || 0) - (a.text_match || 0)); // Limit to reasonable number of results const maxResults = combined.request_params.per_page || 10; combined.hits = combined.hits.slice(0, maxResults); return combined; } } TypesenseService.DEFAULT_SEARCH_PARAMS = { page: 1, per_page: 10, num_typos: '2', prefix: true, drop_tokens_threshold: 1, typo_tokens_threshold: 1, snippet_threshold: 30, highlight_affix_num_tokens: 4, max_facet_values: 10 }; TypesenseService.MARITIME_COLLECTIONS = [ 'defect', 'vessel', 'inspection', 'compliance', 'audit', 'report' ]; //# sourceMappingURL=typesense.js.map