UNPKG

ms365-mcp-server

Version:

Microsoft 365 MCP Server for managing Microsoft 365 email through natural language interactions with full OAuth2 authentication support

500 lines (499 loc) 19.4 kB
import { EnhancedFuzzySearch } from './enhanced-fuzzy-search.js'; import { ProactiveIntelligence, EmailCategory } from './proactive-intelligence.js'; import { logger } from './api.js'; export class ContextAwareSearch { constructor(ms365Operations) { this.ms365Operations = ms365Operations; this.fuzzySearch = new EnhancedFuzzySearch(ms365Operations); this.proactiveIntelligence = new ProactiveIntelligence(ms365Operations); } /** * Perform context-aware search */ async search(query, emails) { logger.log(`🧭 Context-aware search for: "${query}"`); // Parse the query to understand context and intent const parsedQuery = this.parseQuery(query); // Execute search based on parsed context const results = await this.executeContextualSearch(parsedQuery, emails); // Generate explanations and suggestions const explanation = this.generateExplanation(parsedQuery, results); const suggestions = this.generateSuggestions(parsedQuery, results); return { originalQuery: query, parsedQuery, emails: results.emails, searchStrategy: results.strategy, confidence: results.confidence, suggestions, explanation }; } /** * Parse natural language query to extract context and intent */ parseQuery(query) { const lowerQuery = query.toLowerCase(); // Extract intent const intent = this.extractIntent(lowerQuery); // Extract entities const entities = this.extractEntities(query); // Extract time context const timeContext = this.extractTimeContext(lowerQuery); // Extract sender context const senderContext = this.extractSenderContext(lowerQuery, entities); // Extract category context const categoryContext = this.extractCategoryContext(lowerQuery); // Extract priority context const priorityContext = this.extractPriorityContext(lowerQuery); // Calculate overall confidence const confidence = this.calculateParsingConfidence(intent, entities, timeContext); return { originalQuery: query, intent, entities, timeContext, senderContext, categoryContext, priorityContext, confidence }; } /** * Extract search intent from query */ extractIntent(query) { let intentType = 'general_search'; const verbs = []; const objects = []; const modifiers = []; // Check intent patterns for (const [type, patterns] of Object.entries(ContextAwareSearch.INTENT_PATTERNS)) { for (const pattern of patterns) { if (pattern.test(query)) { intentType = type; break; } } if (intentType !== 'general_search') break; } // Extract verbs const verbMatches = query.match(/\b(find|search|look|get|show|need|want)\b/g); if (verbMatches) verbs.push(...verbMatches); // Extract objects const objectMatches = query.match(/\b(email|message|document|file|attachment|notice|letter)\b/g); if (objectMatches) objects.push(...objectMatches); // Extract modifiers const modifierMatches = query.match(/\b(recent|latest|new|old|important|urgent)\b/g); if (modifierMatches) modifiers.push(...modifierMatches); return { type: intentType, verbs, objects, modifiers }; } /** * Extract entities from query */ extractEntities(query) { const entities = []; for (const { pattern, type } of ContextAwareSearch.ENTITY_PATTERNS) { let match; while ((match = pattern.exec(query)) !== null) { entities.push({ type: type, value: match[0], confidence: 0.8, position: { start: match.index, end: match.index + match[0].length } }); } } return entities; } /** * Extract time context from query */ extractTimeContext(query) { for (const { pattern, type } of ContextAwareSearch.TIME_PATTERNS) { const match = pattern.exec(query); if (match) { if (type === 'relative') { return this.parseRelativeTime(match[0]); } else if (type === 'absolute') { return this.parseAbsoluteTime(match[0]); } } } return undefined; } /** * Parse relative time expressions */ parseRelativeTime(timeExpression) { const now = new Date(); let relativeAmount = 1; let relativeUnit = 'days'; let startDate; if (timeExpression.includes('few') || timeExpression.includes('couple')) { relativeAmount = 3; } else if (timeExpression.includes('several')) { relativeAmount = 5; } else { const numberMatch = timeExpression.match(/(\d+)/); if (numberMatch) { relativeAmount = parseInt(numberMatch[1]); } } if (timeExpression.includes('week')) { relativeUnit = 'weeks'; startDate = new Date(now.getTime() - relativeAmount * 7 * 24 * 60 * 60 * 1000); } else if (timeExpression.includes('month')) { relativeUnit = 'months'; startDate = new Date(now.getFullYear(), now.getMonth() - relativeAmount, now.getDate()); } else if (timeExpression.includes('year')) { relativeUnit = 'years'; startDate = new Date(now.getFullYear() - relativeAmount, now.getMonth(), now.getDate()); } else if (timeExpression.includes('today')) { startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate()); } else if (timeExpression.includes('yesterday')) { startDate = new Date(now.getTime() - 24 * 60 * 60 * 1000); } else if (timeExpression.includes('recent')) { relativeAmount = 7; relativeUnit = 'days'; startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); } else { // Default to days startDate = new Date(now.getTime() - relativeAmount * 24 * 60 * 60 * 1000); } return { type: 'relative', startDate, endDate: now, relativeAmount, relativeUnit, description: timeExpression }; } /** * Parse absolute time expressions */ parseAbsoluteTime(timeExpression) { const date = new Date(timeExpression); return { type: 'absolute', startDate: date, endDate: date, description: timeExpression }; } /** * Extract sender context */ extractSenderContext(query, entities) { const emailEntities = entities.filter(e => e.type === 'email_address'); const personEntities = entities.filter(e => e.type === 'person'); if (emailEntities.length > 0) { const email = emailEntities[0].value; return { email, domain: email.split('@')[1] }; } if (personEntities.length > 0) { return { name: personEntities[0].value }; } // Look for "from" patterns const fromMatch = query.match(/from\s+([a-zA-Z\s]+)/i); if (fromMatch) { return { name: fromMatch[1].trim() }; } return undefined; } /** * Extract category context */ extractCategoryContext(query) { const categories = []; // Map keywords to categories const categoryKeywords = { [EmailCategory.GOVERNMENT]: ['government', 'federal', 'state', 'official', 'agency'], [EmailCategory.TAX]: ['tax', 'irs', 'refund', 'filing', 'return'], [EmailCategory.LEGAL]: ['legal', 'court', 'lawyer', 'attorney', 'lawsuit'], [EmailCategory.FINANCIAL]: ['bank', 'financial', 'account', 'payment', 'invoice'], [EmailCategory.HEALTHCARE]: ['medical', 'health', 'doctor', 'hospital', 'clinic'], [EmailCategory.URGENT]: ['urgent', 'important', 'critical', 'asap', 'priority'] }; for (const [category, keywords] of Object.entries(categoryKeywords)) { for (const keyword of keywords) { if (query.includes(keyword)) { categories.push({ category: category, confidence: 0.8 }); break; } } } return categories; } /** * Extract priority context */ extractPriorityContext(query) { const urgentKeywords = ['urgent', 'asap', 'critical', 'emergency']; const highKeywords = ['important', 'priority', 'significant']; const foundUrgent = urgentKeywords.filter(keyword => query.includes(keyword)); const foundHigh = highKeywords.filter(keyword => query.includes(keyword)); if (foundUrgent.length > 0) { return { level: 'urgent', keywords: foundUrgent }; } else if (foundHigh.length > 0) { return { level: 'high', keywords: foundHigh }; } return undefined; } /** * Calculate parsing confidence */ calculateParsingConfidence(intent, entities, timeContext) { let confidence = 0.5; // Base confidence // Intent confidence if (intent.type !== 'general_search') confidence += 0.2; if (intent.verbs.length > 0) confidence += 0.1; if (intent.objects.length > 0) confidence += 0.1; // Entity confidence confidence += Math.min(entities.length * 0.05, 0.2); // Time context confidence if (timeContext) confidence += 0.1; return Math.min(confidence, 1.0); } /** * Execute contextual search based on parsed query */ async executeContextualSearch(parsedQuery, emails) { let filteredEmails = emails; let strategy = 'general'; let confidence = parsedQuery.confidence; // Apply time filtering if (parsedQuery.timeContext) { filteredEmails = this.applyTimeFilter(filteredEmails, parsedQuery.timeContext); strategy += '_time_filtered'; } // Apply sender filtering if (parsedQuery.senderContext) { filteredEmails = this.applySenderFilter(filteredEmails, parsedQuery.senderContext); strategy += '_sender_filtered'; } // Apply category filtering if (parsedQuery.categoryContext && parsedQuery.categoryContext.length > 0) { filteredEmails = await this.applyCategoryFilter(filteredEmails, parsedQuery.categoryContext); strategy += '_category_filtered'; } // Apply priority filtering if (parsedQuery.priorityContext) { filteredEmails = this.applyPriorityFilter(filteredEmails, parsedQuery.priorityContext); strategy += '_priority_filtered'; } // Perform fuzzy search on remaining emails const cleanQuery = this.extractCleanQuery(parsedQuery); if (cleanQuery) { const fuzzyResults = await this.fuzzySearch.search(cleanQuery, filteredEmails); filteredEmails = fuzzyResults.map(result => result.email); strategy += '_fuzzy_search'; } return { emails: filteredEmails, strategy, confidence }; } /** * Apply time filter */ applyTimeFilter(emails, timeContext) { if (!timeContext.startDate) return emails; return emails.filter(email => { const emailDate = new Date(email.receivedDateTime); if (timeContext.endDate) { return emailDate >= timeContext.startDate && emailDate <= timeContext.endDate; } else { return emailDate >= timeContext.startDate; } }); } /** * Apply sender filter */ applySenderFilter(emails, senderContext) { return emails.filter(email => { if (senderContext.email) { return email.from.address.toLowerCase().includes(senderContext.email.toLowerCase()); } if (senderContext.name) { return email.from.name.toLowerCase().includes(senderContext.name.toLowerCase()); } if (senderContext.domain) { return email.from.address.toLowerCase().includes(senderContext.domain.toLowerCase()); } return true; }); } /** * Apply category filter */ async applyCategoryFilter(emails, categoryContexts) { const classifications = await this.proactiveIntelligence.batchClassifyEmails(emails); const targetCategories = categoryContexts.map(c => c.category); return emails.filter(email => { const classification = classifications.get(email.id); if (!classification) return false; return classification.categories.some(category => targetCategories.includes(category)); }); } /** * Apply priority filter */ applyPriorityFilter(emails, priorityContext) { return emails.filter(email => { const emailText = `${email.subject} ${email.bodyPreview}`.toLowerCase(); if (priorityContext.level === 'urgent') { return email.importance === 'high' || priorityContext.keywords.some(keyword => emailText.includes(keyword)); } else if (priorityContext.level === 'high') { return email.importance === 'high' || email.importance === 'normal'; } return true; }); } /** * Extract clean query for fuzzy search */ extractCleanQuery(parsedQuery) { let query = parsedQuery.originalQuery.toLowerCase(); // Remove time expressions if (parsedQuery.timeContext) { query = query.replace(parsedQuery.timeContext.description.toLowerCase(), ''); } // Remove sender expressions if (parsedQuery.senderContext?.name) { query = query.replace(parsedQuery.senderContext.name.toLowerCase(), ''); } // Remove category keywords if (parsedQuery.categoryContext) { for (const context of parsedQuery.categoryContext) { query = query.replace(context.category, ''); } } // Remove common words query = query.replace(/\b(find|search|look|from|in|the|and|or|for|with)\b/g, ''); return query.trim(); } /** * Generate explanation of search results */ generateExplanation(parsedQuery, results) { const parts = []; parts.push(`Found ${results.emails.length} emails`); if (parsedQuery.timeContext) { parts.push(`from ${parsedQuery.timeContext.description}`); } if (parsedQuery.senderContext) { if (parsedQuery.senderContext.name) { parts.push(`from ${parsedQuery.senderContext.name}`); } if (parsedQuery.senderContext.email) { parts.push(`from ${parsedQuery.senderContext.email}`); } } if (parsedQuery.categoryContext && parsedQuery.categoryContext.length > 0) { const categories = parsedQuery.categoryContext.map(c => c.category).join(', '); parts.push(`in categories: ${categories}`); } if (parsedQuery.priorityContext) { parts.push(`with ${parsedQuery.priorityContext.level} priority`); } parts.push(`using ${results.strategy} strategy`); return parts.join(' '); } /** * Generate search suggestions */ generateSuggestions(parsedQuery, results) { const suggestions = []; if (results.emails.length === 0) { suggestions.push('Try broadening your search terms'); suggestions.push('Check spelling of names or keywords'); if (parsedQuery.timeContext) { suggestions.push('Try expanding the time range'); } } if (results.emails.length > 50) { suggestions.push('Try adding more specific criteria to narrow results'); if (!parsedQuery.timeContext) { suggestions.push('Add a time range like "last month" or "recent"'); } } if (!parsedQuery.senderContext && parsedQuery.intent.type !== 'find_from_sender') { suggestions.push('Try searching by sender: "from John" or "emails from manager"'); } if (!parsedQuery.categoryContext || parsedQuery.categoryContext.length === 0) { suggestions.push('Try category-specific searches: "tax emails" or "government notices"'); } return suggestions; } } // Intent patterns ContextAwareSearch.INTENT_PATTERNS = { find_documents: [/find.*documents?/i, /looking for.*files?/i, /need.*attachments?/i], find_emails: [/find.*emails?/i, /search.*messages?/i, /looking for.*mail/i], find_from_sender: [/from\s+(\w+)/i, /sent by\s+(\w+)/i, /emails? from/i], find_by_category: [/tax.*emails?/i, /government.*mail/i, /legal.*documents?/i], find_urgent: [/urgent/i, /important/i, /priority/i, /asap/i], general_search: [/.*/] }; // Time expression patterns ContextAwareSearch.TIME_PATTERNS = [ { pattern: /last (\d+) (days?|weeks?|months?|years?)/i, type: 'relative' }, { pattern: /(few|several|couple of) (days?|weeks?|months?)/i, type: 'relative' }, { pattern: /past (week|month|year)/i, type: 'relative' }, { pattern: /this (week|month|year)/i, type: 'relative' }, { pattern: /recent(ly)?/i, type: 'relative' }, { pattern: /today/i, type: 'relative' }, { pattern: /yesterday/i, type: 'relative' }, { pattern: /(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{2,4})/i, type: 'absolute' }, { pattern: /(january|february|march|april|may|june|july|august|september|october|november|december)/i, type: 'absolute' } ]; // Entity patterns ContextAwareSearch.ENTITY_PATTERNS = [ { pattern: /\b[A-Z][a-z]+ [A-Z][a-z]+\b/g, type: 'person' }, { pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, type: 'email_address' }, { pattern: /\b(government|irs|tax|court|legal|bank|hospital)\b/ig, type: 'organization' }, { pattern: /\b(urgent|important|critical|asap|priority)\b/ig, type: 'urgency' }, { pattern: /\b(pdf|doc|docx|excel|spreadsheet|document|file|attachment)\b/ig, type: 'document_type' } ];