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
JavaScript
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' }
];