Skip to main content
Glama
ezhou89

Medical Research MCP Suite

by ezhou89
searchRefinementService.ts17.2 kB
// src/services/searchRefinementService.ts import { RefinementOption, RefinementAction, RefinementContext, RefinementSuggestion, RefinementResult, UserRefinementChoice, ResponseSizeExceededError, RefinementResponse, SearchRefinementHistory, ClinicalTrialsRefinementOptions, PubMedRefinementOptions, RefinementConfig, MCPPromptTemplate, RefinementPromptArgs, PaginationPromptArgs, FieldSelectionPromptArgs } from '../types/refinementTypes.js'; import { StudySearchParams } from '../apis/clinicalTrials.js'; import { ResponseSizeMonitor } from '../utils/responseSizeMonitor.js'; export class SearchRefinementService { private static instance: SearchRefinementService; private config: RefinementConfig; private refinementHistory: SearchRefinementHistory[] = []; private sizeMonitor: ResponseSizeMonitor; private constructor(config: Partial<RefinementConfig> = {}) { this.config = { enableAutoRefinement: true, maxRefinementAttempts: 3, defaultPageSize: 50, enableProgressiveLoading: true, enableUserFeedback: true, cacheRefinementHistory: true, refinementTimeoutMs: 30000, prioritizeRecentResults: true, enableSmartSuggestions: true, ...config }; this.sizeMonitor = ResponseSizeMonitor.getInstance(); } static getInstance(config?: Partial<RefinementConfig>): SearchRefinementService { if (!SearchRefinementService.instance) { SearchRefinementService.instance = new SearchRefinementService(config); } return SearchRefinementService.instance; } /** * Generate MCP prompt templates for user interaction */ getMCPPromptTemplates(): MCPPromptTemplate[] { return [ { name: 'search_refinement', description: 'Help users refine their search when results are too large', arguments: [ { name: 'originalQuery', description: 'The original search query that produced too many results', required: true, type: 'string' }, { name: 'api', description: 'The API being searched (clinicalTrials, pubmed, fda)', required: true, type: 'string' }, { name: 'errorMessage', description: 'The error message explaining why refinement is needed', required: true, type: 'string' }, { name: 'suggestedRefinements', description: 'Array of suggested refinement options', required: true, type: 'array' }, { name: 'currentResultCount', description: 'Current number of results (if known)', required: false, type: 'number' }, { name: 'canContinueWithoutRefinement', description: 'Whether the user can proceed without refinement', required: false, type: 'boolean' } ] }, { name: 'progressive_loading', description: 'Offer progressive loading options for large result sets', arguments: [ { name: 'totalResults', description: 'Total number of results available', required: true, type: 'number' }, { name: 'pageSize', description: 'Number of results per page', required: true, type: 'number' }, { name: 'currentPage', description: 'Current page number', required: true, type: 'number' }, { name: 'loadingOptions', description: 'Available loading strategies', required: true, type: 'array' } ] }, { name: 'field_selection', description: 'Help users select which fields to include to reduce response size', arguments: [ { name: 'availableFields', description: 'List of available fields', required: true, type: 'array' }, { name: 'currentFields', description: 'Currently selected fields', required: true, type: 'array' }, { name: 'estimatedSizeReduction', description: 'Estimated size reduction percentage', required: true, type: 'number' }, { name: 'fieldDescriptions', description: 'Descriptions of what each field contains', required: true, type: 'object' } ] } ]; } /** * Analyze a size exceeded error and generate refinement suggestions */ analyzeAndSuggestRefinements( originalQuery: any, api: string, actualSize: number, maxSize: number ): RefinementSuggestion { const refinementOptions: RefinementOption[] = []; // Generate API-specific refinement options if (api === 'clinicalTrials') { refinementOptions.push(...this.generateClinicalTrialsRefinements(originalQuery)); } else if (api === 'pubmed') { refinementOptions.push(...this.generatePubMedRefinements(originalQuery)); } // Add general refinement options refinementOptions.push(...this.generateGeneralRefinements(originalQuery, actualSize, maxSize)); // Sort by priority refinementOptions.sort((a, b) => { const priorityOrder = { high: 3, medium: 2, low: 1 }; return priorityOrder[b.priority] - priorityOrder[a.priority]; }); return { title: `Search Results Too Large (${this.sizeMonitor.formatSize(actualSize)})`, description: `Your search returned ${this.sizeMonitor.formatSize(actualSize)} of data, which exceeds the maximum allowed size of ${this.sizeMonitor.formatSize(maxSize)}. Please select one or more refinement options to narrow your search.`, options: refinementOptions, canContinueWithoutRefinement: actualSize < maxSize * 1.5, // Allow if not too far over alternativeActions: [ 'Load results in smaller batches', 'Export results to a file for offline analysis', 'Use more specific search terms', 'Contact support for assistance with large datasets' ] }; } /** * Generate Clinical Trials specific refinement options */ private generateClinicalTrialsRefinements(query: StudySearchParams): RefinementOption[] { const options: RefinementOption[] = []; // Phase filters if (!query.filter?.phase || query.filter.phase.length === 0) { options.push({ id: 'phase_3_only', label: 'Phase III Trials Only', description: 'Focus on Phase III clinical trials, which are typically the most relevant for clinical practice', action: { type: 'filter', parameters: { phase: ['PHASE3'] } }, priority: 'high' }); options.push({ id: 'phase_2_3', label: 'Phase II & III Trials', description: 'Include Phase II and III trials, excluding early phase studies', action: { type: 'filter', parameters: { phase: ['PHASE2', 'PHASE3'] } }, priority: 'medium' }); } // Status filters if (!query.filter?.overallStatus || query.filter.overallStatus.length === 0) { options.push({ id: 'active_recruiting', label: 'Active & Recruiting Trials', description: 'Show only trials that are currently active and recruiting participants', action: { type: 'filter', parameters: { overallStatus: ['ACTIVE_NOT_RECRUITING', 'RECRUITING'] } }, priority: 'high' }); options.push({ id: 'completed_recent', label: 'Recently Completed Trials', description: 'Show completed trials from the last 2 years', action: { type: 'filter', parameters: { overallStatus: ['COMPLETED'], dateRange: { from: new Date(Date.now() - 2 * 365 * 24 * 60 * 60 * 1000).toISOString() } } }, priority: 'medium' }); } // Date range filters options.push({ id: 'last_5_years', label: 'Last 5 Years', description: 'Include only trials started in the last 5 years', action: { type: 'date_range', parameters: { from: new Date(Date.now() - 5 * 365 * 24 * 60 * 60 * 1000).toISOString() } }, priority: 'medium' }); // Study type filters if (!query.filter?.studyType || query.filter.studyType.length === 0) { options.push({ id: 'interventional_only', label: 'Interventional Studies Only', description: 'Focus on interventional studies (exclude observational studies)', action: { type: 'filter', parameters: { studyType: ['INTERVENTIONAL'] } }, priority: 'medium' }); } // Pagination options options.push({ id: 'reduce_page_size', label: 'Load First 25 Results', description: 'Load the first 25 most relevant results with option to load more', action: { type: 'paginate', parameters: { pageSize: 25 } }, priority: 'low' }); return options; } /** * Generate PubMed specific refinement options */ private generatePubMedRefinements(query: any): RefinementOption[] { const options: RefinementOption[] = []; // Publication type filters options.push({ id: 'clinical_trials_only', label: 'Clinical Trials Only', description: 'Focus on clinical trial publications', action: { type: 'filter', parameters: { publicationType: ['Clinical Trial'] } }, priority: 'high' }); options.push({ id: 'systematic_reviews', label: 'Systematic Reviews & Meta-Analyses', description: 'Show only systematic reviews and meta-analyses', action: { type: 'filter', parameters: { publicationType: ['Systematic Review', 'Meta-Analysis'] } }, priority: 'high' }); // Date range filters options.push({ id: 'last_3_years', label: 'Last 3 Years', description: 'Include only publications from the last 3 years', action: { type: 'date_range', parameters: { from: new Date(Date.now() - 3 * 365 * 24 * 60 * 60 * 1000).toISOString() } }, priority: 'medium' }); // Language filters options.push({ id: 'english_only', label: 'English Language Only', description: 'Include only English language publications', action: { type: 'filter', parameters: { language: ['English'] } }, priority: 'medium' }); return options; } /** * Generate general refinement options applicable to all APIs */ private generateGeneralRefinements(query: any, actualSize: number, maxSize: number): RefinementOption[] { const options: RefinementOption[] = []; // Field selection options.push({ id: 'essential_fields_only', label: 'Essential Fields Only', description: 'Include only the most important fields to reduce response size', action: { type: 'field_selection', parameters: { fields: ['id', 'title', 'status', 'phase', 'conditions', 'interventions'] } }, priority: 'high' }); // Pagination const recommendedPageSize = Math.floor(maxSize / (actualSize / (query.pageSize || 100))); options.push({ id: 'progressive_loading', label: `Load ${recommendedPageSize} Results at a Time`, description: 'Load results in manageable chunks with option to continue', action: { type: 'paginate', parameters: { pageSize: Math.max(10, recommendedPageSize) } }, priority: 'medium' }); return options; } /** * Apply user-selected refinements to a query */ applyRefinements( originalQuery: any, selectedRefinements: RefinementOption[], customParameters?: Record<string, any> ): RefinementResult { let refinedQuery = { ...originalQuery }; const appliedRefinements: RefinementOption[] = []; try { for (const refinement of selectedRefinements) { refinedQuery = this.applyRefinementAction(refinedQuery, refinement.action); appliedRefinements.push(refinement); } // Apply custom parameters if provided if (customParameters) { refinedQuery = { ...refinedQuery, ...customParameters }; } // Record in history if (this.config.cacheRefinementHistory) { this.refinementHistory.push({ timestamp: new Date(), originalQuery, appliedRefinements, resultCount: 0, // Will be updated when results are fetched successful: true }); } return { success: true, refinedQuery, appliedRefinements, estimatedResultCount: this.estimateResultCount(refinedQuery, appliedRefinements), message: `Applied ${appliedRefinements.length} refinement(s) to your search`, nextSteps: [ 'Execute the refined search', 'Review results and apply additional refinements if needed', 'Use progressive loading for large result sets' ] }; } catch (error) { return { success: false, refinedQuery: originalQuery, appliedRefinements: [], estimatedResultCount: 0, message: `Failed to apply refinements: ${error instanceof Error ? error.message : 'Unknown error'}` }; } } /** * Apply a single refinement action to a query */ private applyRefinementAction(query: any, action: RefinementAction): any { const refinedQuery = { ...query }; switch (action.type) { case 'filter': refinedQuery.filter = { ...refinedQuery.filter, ...action.parameters }; break; case 'date_range': refinedQuery.dateRange = action.parameters; break; case 'paginate': refinedQuery.pageSize = action.parameters.pageSize; if (action.parameters.pageToken) { refinedQuery.pageToken = action.parameters.pageToken; } break; case 'field_selection': refinedQuery.fields = action.parameters.fields; break; case 'reduce_scope': // Apply scope reduction parameters Object.assign(refinedQuery, action.parameters); break; } return refinedQuery; } /** * Estimate result count based on applied refinements */ private estimateResultCount(query: any, refinements: RefinementOption[]): number { // This is a simplified estimation - in a real implementation, // you might use historical data or API-specific estimation logic let baseEstimate = 1000; let reductionFactor = 1; for (const refinement of refinements) { switch (refinement.action.type) { case 'filter': reductionFactor *= 0.3; // Filters typically reduce by ~70% break; case 'date_range': reductionFactor *= 0.4; // Date ranges reduce by ~60% break; case 'paginate': return Math.min(baseEstimate * reductionFactor, query.pageSize || 50); case 'field_selection': // Field selection doesn't reduce count, just response size break; } } return Math.floor(baseEstimate * reductionFactor); } /** * Create a user-friendly error response for oversized results */ createRefinementErrorResponse<T>( originalQuery: any, api: string, actualSize: number, maxSize: number ): RefinementResponse<T> { const suggestion = this.analyzeAndSuggestRefinements(originalQuery, api, actualSize, maxSize); return { success: false, error: new ResponseSizeExceededError( actualSize, maxSize, originalQuery, api, suggestion.options ), requiresUserInput: true, userPrompt: { type: 'refinement', message: suggestion.description, options: suggestion.options.map(option => ({ id: option.id, label: option.label, description: option.description, priority: option.priority })), defaultChoice: suggestion.options[0]?.id }, metadata: { originalSize: actualSize, processingTime: Date.now() } }; } /** * Get refinement history for analytics */ getRefinementHistory(): SearchRefinementHistory[] { return [...this.refinementHistory]; } /** * Clear refinement history */ clearRefinementHistory(): void { this.refinementHistory = []; } /** * Update configuration */ updateConfig(config: Partial<RefinementConfig>): void { this.config = { ...this.config, ...config }; } /** * Get current configuration */ getConfig(): RefinementConfig { return { ...this.config }; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ezhou89/medical-research-mcp-suite'

If you have feedback or need assistance with the MCP directory API, please join our Discord server