ms365-mcp-server 1.1.16 → 1.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,499 @@
1
+ import { EnhancedFuzzySearch } from './enhanced-fuzzy-search.js';
2
+ import { ProactiveIntelligence, EmailCategory } from './proactive-intelligence.js';
3
+ import { logger } from './api.js';
4
+ export class ContextAwareSearch {
5
+ constructor(ms365Operations) {
6
+ this.ms365Operations = ms365Operations;
7
+ this.fuzzySearch = new EnhancedFuzzySearch(ms365Operations);
8
+ this.proactiveIntelligence = new ProactiveIntelligence(ms365Operations);
9
+ }
10
+ /**
11
+ * Perform context-aware search
12
+ */
13
+ async search(query, emails) {
14
+ logger.log(`🧭 Context-aware search for: "${query}"`);
15
+ // Parse the query to understand context and intent
16
+ const parsedQuery = this.parseQuery(query);
17
+ // Execute search based on parsed context
18
+ const results = await this.executeContextualSearch(parsedQuery, emails);
19
+ // Generate explanations and suggestions
20
+ const explanation = this.generateExplanation(parsedQuery, results);
21
+ const suggestions = this.generateSuggestions(parsedQuery, results);
22
+ return {
23
+ originalQuery: query,
24
+ parsedQuery,
25
+ emails: results.emails,
26
+ searchStrategy: results.strategy,
27
+ confidence: results.confidence,
28
+ suggestions,
29
+ explanation
30
+ };
31
+ }
32
+ /**
33
+ * Parse natural language query to extract context and intent
34
+ */
35
+ parseQuery(query) {
36
+ const lowerQuery = query.toLowerCase();
37
+ // Extract intent
38
+ const intent = this.extractIntent(lowerQuery);
39
+ // Extract entities
40
+ const entities = this.extractEntities(query);
41
+ // Extract time context
42
+ const timeContext = this.extractTimeContext(lowerQuery);
43
+ // Extract sender context
44
+ const senderContext = this.extractSenderContext(lowerQuery, entities);
45
+ // Extract category context
46
+ const categoryContext = this.extractCategoryContext(lowerQuery);
47
+ // Extract priority context
48
+ const priorityContext = this.extractPriorityContext(lowerQuery);
49
+ // Calculate overall confidence
50
+ const confidence = this.calculateParsingConfidence(intent, entities, timeContext);
51
+ return {
52
+ originalQuery: query,
53
+ intent,
54
+ entities,
55
+ timeContext,
56
+ senderContext,
57
+ categoryContext,
58
+ priorityContext,
59
+ confidence
60
+ };
61
+ }
62
+ /**
63
+ * Extract search intent from query
64
+ */
65
+ extractIntent(query) {
66
+ let intentType = 'general_search';
67
+ const verbs = [];
68
+ const objects = [];
69
+ const modifiers = [];
70
+ // Check intent patterns
71
+ for (const [type, patterns] of Object.entries(ContextAwareSearch.INTENT_PATTERNS)) {
72
+ for (const pattern of patterns) {
73
+ if (pattern.test(query)) {
74
+ intentType = type;
75
+ break;
76
+ }
77
+ }
78
+ if (intentType !== 'general_search')
79
+ break;
80
+ }
81
+ // Extract verbs
82
+ const verbMatches = query.match(/\b(find|search|look|get|show|need|want)\b/g);
83
+ if (verbMatches)
84
+ verbs.push(...verbMatches);
85
+ // Extract objects
86
+ const objectMatches = query.match(/\b(email|message|document|file|attachment|notice|letter)\b/g);
87
+ if (objectMatches)
88
+ objects.push(...objectMatches);
89
+ // Extract modifiers
90
+ const modifierMatches = query.match(/\b(recent|latest|new|old|important|urgent)\b/g);
91
+ if (modifierMatches)
92
+ modifiers.push(...modifierMatches);
93
+ return {
94
+ type: intentType,
95
+ verbs,
96
+ objects,
97
+ modifiers
98
+ };
99
+ }
100
+ /**
101
+ * Extract entities from query
102
+ */
103
+ extractEntities(query) {
104
+ const entities = [];
105
+ for (const { pattern, type } of ContextAwareSearch.ENTITY_PATTERNS) {
106
+ let match;
107
+ while ((match = pattern.exec(query)) !== null) {
108
+ entities.push({
109
+ type: type,
110
+ value: match[0],
111
+ confidence: 0.8,
112
+ position: { start: match.index, end: match.index + match[0].length }
113
+ });
114
+ }
115
+ }
116
+ return entities;
117
+ }
118
+ /**
119
+ * Extract time context from query
120
+ */
121
+ extractTimeContext(query) {
122
+ for (const { pattern, type } of ContextAwareSearch.TIME_PATTERNS) {
123
+ const match = pattern.exec(query);
124
+ if (match) {
125
+ if (type === 'relative') {
126
+ return this.parseRelativeTime(match[0]);
127
+ }
128
+ else if (type === 'absolute') {
129
+ return this.parseAbsoluteTime(match[0]);
130
+ }
131
+ }
132
+ }
133
+ return undefined;
134
+ }
135
+ /**
136
+ * Parse relative time expressions
137
+ */
138
+ parseRelativeTime(timeExpression) {
139
+ const now = new Date();
140
+ let relativeAmount = 1;
141
+ let relativeUnit = 'days';
142
+ let startDate;
143
+ if (timeExpression.includes('few') || timeExpression.includes('couple')) {
144
+ relativeAmount = 3;
145
+ }
146
+ else if (timeExpression.includes('several')) {
147
+ relativeAmount = 5;
148
+ }
149
+ else {
150
+ const numberMatch = timeExpression.match(/(\d+)/);
151
+ if (numberMatch) {
152
+ relativeAmount = parseInt(numberMatch[1]);
153
+ }
154
+ }
155
+ if (timeExpression.includes('week')) {
156
+ relativeUnit = 'weeks';
157
+ startDate = new Date(now.getTime() - relativeAmount * 7 * 24 * 60 * 60 * 1000);
158
+ }
159
+ else if (timeExpression.includes('month')) {
160
+ relativeUnit = 'months';
161
+ startDate = new Date(now.getFullYear(), now.getMonth() - relativeAmount, now.getDate());
162
+ }
163
+ else if (timeExpression.includes('year')) {
164
+ relativeUnit = 'years';
165
+ startDate = new Date(now.getFullYear() - relativeAmount, now.getMonth(), now.getDate());
166
+ }
167
+ else if (timeExpression.includes('today')) {
168
+ startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate());
169
+ }
170
+ else if (timeExpression.includes('yesterday')) {
171
+ startDate = new Date(now.getTime() - 24 * 60 * 60 * 1000);
172
+ }
173
+ else if (timeExpression.includes('recent')) {
174
+ relativeAmount = 7;
175
+ relativeUnit = 'days';
176
+ startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
177
+ }
178
+ else {
179
+ // Default to days
180
+ startDate = new Date(now.getTime() - relativeAmount * 24 * 60 * 60 * 1000);
181
+ }
182
+ return {
183
+ type: 'relative',
184
+ startDate,
185
+ endDate: now,
186
+ relativeAmount,
187
+ relativeUnit,
188
+ description: timeExpression
189
+ };
190
+ }
191
+ /**
192
+ * Parse absolute time expressions
193
+ */
194
+ parseAbsoluteTime(timeExpression) {
195
+ const date = new Date(timeExpression);
196
+ return {
197
+ type: 'absolute',
198
+ startDate: date,
199
+ endDate: date,
200
+ description: timeExpression
201
+ };
202
+ }
203
+ /**
204
+ * Extract sender context
205
+ */
206
+ extractSenderContext(query, entities) {
207
+ const emailEntities = entities.filter(e => e.type === 'email_address');
208
+ const personEntities = entities.filter(e => e.type === 'person');
209
+ if (emailEntities.length > 0) {
210
+ const email = emailEntities[0].value;
211
+ return {
212
+ email,
213
+ domain: email.split('@')[1]
214
+ };
215
+ }
216
+ if (personEntities.length > 0) {
217
+ return {
218
+ name: personEntities[0].value
219
+ };
220
+ }
221
+ // Look for "from" patterns
222
+ const fromMatch = query.match(/from\s+([a-zA-Z\s]+)/i);
223
+ if (fromMatch) {
224
+ return {
225
+ name: fromMatch[1].trim()
226
+ };
227
+ }
228
+ return undefined;
229
+ }
230
+ /**
231
+ * Extract category context
232
+ */
233
+ extractCategoryContext(query) {
234
+ const categories = [];
235
+ // Map keywords to categories
236
+ const categoryKeywords = {
237
+ [EmailCategory.GOVERNMENT]: ['government', 'federal', 'state', 'official', 'agency'],
238
+ [EmailCategory.TAX]: ['tax', 'irs', 'refund', 'filing', 'return'],
239
+ [EmailCategory.LEGAL]: ['legal', 'court', 'lawyer', 'attorney', 'lawsuit'],
240
+ [EmailCategory.FINANCIAL]: ['bank', 'financial', 'account', 'payment', 'invoice'],
241
+ [EmailCategory.HEALTHCARE]: ['medical', 'health', 'doctor', 'hospital', 'clinic'],
242
+ [EmailCategory.URGENT]: ['urgent', 'important', 'critical', 'asap', 'priority']
243
+ };
244
+ for (const [category, keywords] of Object.entries(categoryKeywords)) {
245
+ for (const keyword of keywords) {
246
+ if (query.includes(keyword)) {
247
+ categories.push({
248
+ category: category,
249
+ confidence: 0.8
250
+ });
251
+ break;
252
+ }
253
+ }
254
+ }
255
+ return categories;
256
+ }
257
+ /**
258
+ * Extract priority context
259
+ */
260
+ extractPriorityContext(query) {
261
+ const urgentKeywords = ['urgent', 'asap', 'critical', 'emergency'];
262
+ const highKeywords = ['important', 'priority', 'significant'];
263
+ const foundUrgent = urgentKeywords.filter(keyword => query.includes(keyword));
264
+ const foundHigh = highKeywords.filter(keyword => query.includes(keyword));
265
+ if (foundUrgent.length > 0) {
266
+ return { level: 'urgent', keywords: foundUrgent };
267
+ }
268
+ else if (foundHigh.length > 0) {
269
+ return { level: 'high', keywords: foundHigh };
270
+ }
271
+ return undefined;
272
+ }
273
+ /**
274
+ * Calculate parsing confidence
275
+ */
276
+ calculateParsingConfidence(intent, entities, timeContext) {
277
+ let confidence = 0.5; // Base confidence
278
+ // Intent confidence
279
+ if (intent.type !== 'general_search')
280
+ confidence += 0.2;
281
+ if (intent.verbs.length > 0)
282
+ confidence += 0.1;
283
+ if (intent.objects.length > 0)
284
+ confidence += 0.1;
285
+ // Entity confidence
286
+ confidence += Math.min(entities.length * 0.05, 0.2);
287
+ // Time context confidence
288
+ if (timeContext)
289
+ confidence += 0.1;
290
+ return Math.min(confidence, 1.0);
291
+ }
292
+ /**
293
+ * Execute contextual search based on parsed query
294
+ */
295
+ async executeContextualSearch(parsedQuery, emails) {
296
+ let filteredEmails = emails;
297
+ let strategy = 'general';
298
+ let confidence = parsedQuery.confidence;
299
+ // Apply time filtering
300
+ if (parsedQuery.timeContext) {
301
+ filteredEmails = this.applyTimeFilter(filteredEmails, parsedQuery.timeContext);
302
+ strategy += '_time_filtered';
303
+ }
304
+ // Apply sender filtering
305
+ if (parsedQuery.senderContext) {
306
+ filteredEmails = this.applySenderFilter(filteredEmails, parsedQuery.senderContext);
307
+ strategy += '_sender_filtered';
308
+ }
309
+ // Apply category filtering
310
+ if (parsedQuery.categoryContext && parsedQuery.categoryContext.length > 0) {
311
+ filteredEmails = await this.applyCategoryFilter(filteredEmails, parsedQuery.categoryContext);
312
+ strategy += '_category_filtered';
313
+ }
314
+ // Apply priority filtering
315
+ if (parsedQuery.priorityContext) {
316
+ filteredEmails = this.applyPriorityFilter(filteredEmails, parsedQuery.priorityContext);
317
+ strategy += '_priority_filtered';
318
+ }
319
+ // Perform fuzzy search on remaining emails
320
+ const cleanQuery = this.extractCleanQuery(parsedQuery);
321
+ if (cleanQuery) {
322
+ const fuzzyResults = await this.fuzzySearch.search(cleanQuery, filteredEmails);
323
+ filteredEmails = fuzzyResults.map(result => result.email);
324
+ strategy += '_fuzzy_search';
325
+ }
326
+ return {
327
+ emails: filteredEmails,
328
+ strategy,
329
+ confidence
330
+ };
331
+ }
332
+ /**
333
+ * Apply time filter
334
+ */
335
+ applyTimeFilter(emails, timeContext) {
336
+ if (!timeContext.startDate)
337
+ return emails;
338
+ return emails.filter(email => {
339
+ const emailDate = new Date(email.receivedDateTime);
340
+ if (timeContext.endDate) {
341
+ return emailDate >= timeContext.startDate && emailDate <= timeContext.endDate;
342
+ }
343
+ else {
344
+ return emailDate >= timeContext.startDate;
345
+ }
346
+ });
347
+ }
348
+ /**
349
+ * Apply sender filter
350
+ */
351
+ applySenderFilter(emails, senderContext) {
352
+ return emails.filter(email => {
353
+ if (senderContext.email) {
354
+ return email.from.address.toLowerCase().includes(senderContext.email.toLowerCase());
355
+ }
356
+ if (senderContext.name) {
357
+ return email.from.name.toLowerCase().includes(senderContext.name.toLowerCase());
358
+ }
359
+ if (senderContext.domain) {
360
+ return email.from.address.toLowerCase().includes(senderContext.domain.toLowerCase());
361
+ }
362
+ return true;
363
+ });
364
+ }
365
+ /**
366
+ * Apply category filter
367
+ */
368
+ async applyCategoryFilter(emails, categoryContexts) {
369
+ const classifications = await this.proactiveIntelligence.batchClassifyEmails(emails);
370
+ const targetCategories = categoryContexts.map(c => c.category);
371
+ return emails.filter(email => {
372
+ const classification = classifications.get(email.id);
373
+ if (!classification)
374
+ return false;
375
+ return classification.categories.some(category => targetCategories.includes(category));
376
+ });
377
+ }
378
+ /**
379
+ * Apply priority filter
380
+ */
381
+ applyPriorityFilter(emails, priorityContext) {
382
+ return emails.filter(email => {
383
+ const emailText = `${email.subject} ${email.bodyPreview}`.toLowerCase();
384
+ if (priorityContext.level === 'urgent') {
385
+ return email.importance === 'high' ||
386
+ priorityContext.keywords.some(keyword => emailText.includes(keyword));
387
+ }
388
+ else if (priorityContext.level === 'high') {
389
+ return email.importance === 'high' || email.importance === 'normal';
390
+ }
391
+ return true;
392
+ });
393
+ }
394
+ /**
395
+ * Extract clean query for fuzzy search
396
+ */
397
+ extractCleanQuery(parsedQuery) {
398
+ let query = parsedQuery.originalQuery.toLowerCase();
399
+ // Remove time expressions
400
+ if (parsedQuery.timeContext) {
401
+ query = query.replace(parsedQuery.timeContext.description.toLowerCase(), '');
402
+ }
403
+ // Remove sender expressions
404
+ if (parsedQuery.senderContext?.name) {
405
+ query = query.replace(parsedQuery.senderContext.name.toLowerCase(), '');
406
+ }
407
+ // Remove category keywords
408
+ if (parsedQuery.categoryContext) {
409
+ for (const context of parsedQuery.categoryContext) {
410
+ query = query.replace(context.category, '');
411
+ }
412
+ }
413
+ // Remove common words
414
+ query = query.replace(/\b(find|search|look|from|in|the|and|or|for|with)\b/g, '');
415
+ return query.trim();
416
+ }
417
+ /**
418
+ * Generate explanation of search results
419
+ */
420
+ generateExplanation(parsedQuery, results) {
421
+ const parts = [];
422
+ parts.push(`Found ${results.emails.length} emails`);
423
+ if (parsedQuery.timeContext) {
424
+ parts.push(`from ${parsedQuery.timeContext.description}`);
425
+ }
426
+ if (parsedQuery.senderContext) {
427
+ if (parsedQuery.senderContext.name) {
428
+ parts.push(`from ${parsedQuery.senderContext.name}`);
429
+ }
430
+ if (parsedQuery.senderContext.email) {
431
+ parts.push(`from ${parsedQuery.senderContext.email}`);
432
+ }
433
+ }
434
+ if (parsedQuery.categoryContext && parsedQuery.categoryContext.length > 0) {
435
+ const categories = parsedQuery.categoryContext.map(c => c.category).join(', ');
436
+ parts.push(`in categories: ${categories}`);
437
+ }
438
+ if (parsedQuery.priorityContext) {
439
+ parts.push(`with ${parsedQuery.priorityContext.level} priority`);
440
+ }
441
+ parts.push(`using ${results.strategy} strategy`);
442
+ return parts.join(' ');
443
+ }
444
+ /**
445
+ * Generate search suggestions
446
+ */
447
+ generateSuggestions(parsedQuery, results) {
448
+ const suggestions = [];
449
+ if (results.emails.length === 0) {
450
+ suggestions.push('Try broadening your search terms');
451
+ suggestions.push('Check spelling of names or keywords');
452
+ if (parsedQuery.timeContext) {
453
+ suggestions.push('Try expanding the time range');
454
+ }
455
+ }
456
+ if (results.emails.length > 50) {
457
+ suggestions.push('Try adding more specific criteria to narrow results');
458
+ if (!parsedQuery.timeContext) {
459
+ suggestions.push('Add a time range like "last month" or "recent"');
460
+ }
461
+ }
462
+ if (!parsedQuery.senderContext && parsedQuery.intent.type !== 'find_from_sender') {
463
+ suggestions.push('Try searching by sender: "from John" or "emails from manager"');
464
+ }
465
+ if (!parsedQuery.categoryContext || parsedQuery.categoryContext.length === 0) {
466
+ suggestions.push('Try category-specific searches: "tax emails" or "government notices"');
467
+ }
468
+ return suggestions;
469
+ }
470
+ }
471
+ // Intent patterns
472
+ ContextAwareSearch.INTENT_PATTERNS = {
473
+ find_documents: [/find.*documents?/i, /looking for.*files?/i, /need.*attachments?/i],
474
+ find_emails: [/find.*emails?/i, /search.*messages?/i, /looking for.*mail/i],
475
+ find_from_sender: [/from\s+(\w+)/i, /sent by\s+(\w+)/i, /emails? from/i],
476
+ find_by_category: [/tax.*emails?/i, /government.*mail/i, /legal.*documents?/i],
477
+ find_urgent: [/urgent/i, /important/i, /priority/i, /asap/i],
478
+ general_search: [/.*/]
479
+ };
480
+ // Time expression patterns
481
+ ContextAwareSearch.TIME_PATTERNS = [
482
+ { pattern: /last (\d+) (days?|weeks?|months?|years?)/i, type: 'relative' },
483
+ { pattern: /(few|several|couple of) (days?|weeks?|months?)/i, type: 'relative' },
484
+ { pattern: /past (week|month|year)/i, type: 'relative' },
485
+ { pattern: /this (week|month|year)/i, type: 'relative' },
486
+ { pattern: /recent(ly)?/i, type: 'relative' },
487
+ { pattern: /today/i, type: 'relative' },
488
+ { pattern: /yesterday/i, type: 'relative' },
489
+ { pattern: /(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{2,4})/i, type: 'absolute' },
490
+ { pattern: /(january|february|march|april|may|june|july|august|september|october|november|december)/i, type: 'absolute' }
491
+ ];
492
+ // Entity patterns
493
+ ContextAwareSearch.ENTITY_PATTERNS = [
494
+ { pattern: /\b[A-Z][a-z]+ [A-Z][a-z]+\b/g, type: 'person' },
495
+ { pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, type: 'email_address' },
496
+ { pattern: /\b(government|irs|tax|court|legal|bank|hospital)\b/ig, type: 'organization' },
497
+ { pattern: /\b(urgent|important|critical|asap|priority)\b/ig, type: 'urgency' },
498
+ { pattern: /\b(pdf|doc|docx|excel|spreadsheet|document|file|attachment)\b/ig, type: 'document_type' }
499
+ ];