noesium 0.1.0__py3-none-any.whl → 0.2.0__py3-none-any.whl

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.
Files changed (59) hide show
  1. noesium/agents/askura_agent/__init__.py +22 -0
  2. noesium/agents/askura_agent/askura_agent.py +480 -0
  3. noesium/agents/askura_agent/conversation.py +164 -0
  4. noesium/agents/askura_agent/extractor.py +175 -0
  5. noesium/agents/askura_agent/memory.py +14 -0
  6. noesium/agents/askura_agent/models.py +239 -0
  7. noesium/agents/askura_agent/prompts.py +202 -0
  8. noesium/agents/askura_agent/reflection.py +234 -0
  9. noesium/agents/askura_agent/summarizer.py +30 -0
  10. noesium/agents/askura_agent/utils.py +6 -0
  11. noesium/agents/deep_research/__init__.py +13 -0
  12. noesium/agents/deep_research/agent.py +398 -0
  13. noesium/agents/deep_research/prompts.py +84 -0
  14. noesium/agents/deep_research/schemas.py +42 -0
  15. noesium/agents/deep_research/state.py +54 -0
  16. noesium/agents/search/__init__.py +5 -0
  17. noesium/agents/search/agent.py +474 -0
  18. noesium/agents/search/state.py +28 -0
  19. noesium/core/__init__.py +1 -1
  20. noesium/core/agent/base.py +10 -2
  21. noesium/core/goalith/decomposer/llm_decomposer.py +1 -1
  22. noesium/core/llm/__init__.py +1 -1
  23. noesium/core/llm/base.py +2 -2
  24. noesium/core/llm/litellm.py +42 -21
  25. noesium/core/llm/llamacpp.py +25 -4
  26. noesium/core/llm/ollama.py +43 -22
  27. noesium/core/llm/openai.py +25 -5
  28. noesium/core/llm/openrouter.py +1 -1
  29. noesium/core/toolify/base.py +9 -2
  30. noesium/core/toolify/config.py +2 -2
  31. noesium/core/toolify/registry.py +21 -5
  32. noesium/core/tracing/opik_tracing.py +7 -7
  33. noesium/core/vector_store/__init__.py +2 -2
  34. noesium/core/vector_store/base.py +1 -1
  35. noesium/core/vector_store/pgvector.py +10 -13
  36. noesium/core/vector_store/weaviate.py +2 -1
  37. noesium/toolkits/__init__.py +1 -0
  38. noesium/toolkits/arxiv_toolkit.py +310 -0
  39. noesium/toolkits/audio_aliyun_toolkit.py +441 -0
  40. noesium/toolkits/audio_toolkit.py +370 -0
  41. noesium/toolkits/bash_toolkit.py +332 -0
  42. noesium/toolkits/document_toolkit.py +454 -0
  43. noesium/toolkits/file_edit_toolkit.py +552 -0
  44. noesium/toolkits/github_toolkit.py +395 -0
  45. noesium/toolkits/gmail_toolkit.py +575 -0
  46. noesium/toolkits/image_toolkit.py +425 -0
  47. noesium/toolkits/memory_toolkit.py +398 -0
  48. noesium/toolkits/python_executor_toolkit.py +334 -0
  49. noesium/toolkits/search_toolkit.py +451 -0
  50. noesium/toolkits/serper_toolkit.py +623 -0
  51. noesium/toolkits/tabular_data_toolkit.py +537 -0
  52. noesium/toolkits/user_interaction_toolkit.py +365 -0
  53. noesium/toolkits/video_toolkit.py +168 -0
  54. noesium/toolkits/wikipedia_toolkit.py +420 -0
  55. {noesium-0.1.0.dist-info → noesium-0.2.0.dist-info}/METADATA +56 -48
  56. {noesium-0.1.0.dist-info → noesium-0.2.0.dist-info}/RECORD +59 -23
  57. {noesium-0.1.0.dist-info → noesium-0.2.0.dist-info}/licenses/LICENSE +1 -1
  58. {noesium-0.1.0.dist-info → noesium-0.2.0.dist-info}/WHEEL +0 -0
  59. {noesium-0.1.0.dist-info → noesium-0.2.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,575 @@
1
+ """
2
+ Gmail toolkit for email reading and authentication.
3
+
4
+ Provides tools for Gmail API integration including 2FA code retrieval,
5
+ email reading, and authentication management. Based on the browser-use
6
+ Gmail integration but adapted for the Noesium toolkit framework.
7
+ """
8
+
9
+ import base64
10
+ import os
11
+ from pathlib import Path
12
+ from typing import Any, Callable, Dict, List, Optional
13
+
14
+ import aiofiles
15
+
16
+ try:
17
+ from google.auth.transport.requests import Request
18
+ from google.oauth2.credentials import Credentials
19
+ from google_auth_oauthlib.flow import InstalledAppFlow
20
+ from googleapiclient.discovery import build
21
+ from googleapiclient.errors import HttpError
22
+
23
+ GOOGLE_AVAILABLE = True
24
+ except ImportError:
25
+ Request = None
26
+ Credentials = None
27
+ InstalledAppFlow = None
28
+ build = None
29
+ HttpError = None
30
+ GOOGLE_AVAILABLE = False
31
+
32
+ from noesium.core.toolify.base import AsyncBaseToolkit
33
+ from noesium.core.toolify.config import ToolkitConfig
34
+ from noesium.core.toolify.registry import register_toolkit
35
+ from noesium.core.utils.logging import get_logger
36
+
37
+ logger = get_logger(__name__)
38
+
39
+
40
+ class GmailService:
41
+ """
42
+ Gmail API service for email reading.
43
+ Provides functionality to:
44
+ - Authenticate with Gmail API using OAuth2
45
+ - Read recent emails with filtering
46
+ - Return full email content for agent analysis
47
+ """
48
+
49
+ # Gmail API scopes
50
+ SCOPES = ["https://www.googleapis.com/auth/gmail.readonly"]
51
+
52
+ def __init__(
53
+ self,
54
+ credentials_file: str | None = None,
55
+ token_file: str | None = None,
56
+ config_dir: str | None = None,
57
+ access_token: str | None = None,
58
+ ):
59
+ """
60
+ Initialize Gmail Service
61
+ Args:
62
+ credentials_file: Path to OAuth credentials JSON from Google Cloud Console
63
+ token_file: Path to store/load access tokens
64
+ config_dir: Directory to store config files (defaults to ~/.noesium)
65
+ access_token: Direct access token (skips file-based auth if provided)
66
+ """
67
+ if not GOOGLE_AVAILABLE:
68
+ raise ImportError("Google packages are not installed. Install them with: pip install 'noesium[google]'")
69
+
70
+ # Set up configuration directory
71
+ if config_dir is None:
72
+ self.config_dir = Path.home() / ".noesium"
73
+ else:
74
+ self.config_dir = Path(config_dir).expanduser().resolve()
75
+
76
+ # Ensure config directory exists (only if not using direct token)
77
+ if access_token is None:
78
+ self.config_dir.mkdir(parents=True, exist_ok=True)
79
+
80
+ # Set up credential paths
81
+ self.credentials_file = credentials_file or self.config_dir / "gmail_credentials.json"
82
+ self.token_file = token_file or self.config_dir / "gmail_token.json"
83
+
84
+ # Direct access token support
85
+ self.access_token = access_token
86
+
87
+ self.service = None
88
+ self.creds = None
89
+ self._authenticated = False
90
+
91
+ def is_authenticated(self) -> bool:
92
+ """Check if Gmail service is authenticated"""
93
+ return self._authenticated and self.service is not None
94
+
95
+ async def authenticate(self) -> bool:
96
+ """
97
+ Handle OAuth authentication and token management
98
+ Returns:
99
+ bool: True if authentication successful, False otherwise
100
+ """
101
+ try:
102
+ logger.info("🔐 Authenticating with Gmail API...")
103
+
104
+ # Check if using direct access token
105
+ if self.access_token:
106
+ logger.info("🔑 Using provided access token")
107
+ # Create credentials from access token
108
+ self.creds = Credentials(token=self.access_token, scopes=self.SCOPES)
109
+ # Test token validity by building service
110
+ self.service = build("gmail", "v1", credentials=self.creds)
111
+ self._authenticated = True
112
+ logger.info("✅ Gmail API ready with access token!")
113
+ return True
114
+
115
+ # Original file-based authentication flow
116
+ # Try to load existing tokens
117
+ if os.path.exists(self.token_file):
118
+ self.creds = Credentials.from_authorized_user_file(str(self.token_file), self.SCOPES)
119
+ logger.debug("📁 Loaded existing tokens")
120
+
121
+ # If no valid credentials, run OAuth flow
122
+ if not self.creds or not self.creds.valid:
123
+ if self.creds and self.creds.expired and self.creds.refresh_token:
124
+ logger.info("🔄 Refreshing expired tokens...")
125
+ self.creds.refresh(Request())
126
+ else:
127
+ logger.info("🌐 Starting OAuth flow...")
128
+ if not os.path.exists(self.credentials_file):
129
+ logger.error(
130
+ f"❌ Gmail credentials file not found: {self.credentials_file}\n"
131
+ "Please download it from Google Cloud Console:\n"
132
+ "1. Go to https://console.cloud.google.com/\n"
133
+ "2. APIs & Services > Credentials\n"
134
+ "3. Download OAuth 2.0 Client JSON\n"
135
+ f"4. Save as 'gmail_credentials.json' in {self.config_dir}/"
136
+ )
137
+ return False
138
+
139
+ flow = InstalledAppFlow.from_client_secrets_file(str(self.credentials_file), self.SCOPES)
140
+ # Use specific redirect URI to match OAuth credentials
141
+ self.creds = flow.run_local_server(port=8080, open_browser=True)
142
+
143
+ # Save tokens for next time
144
+ async with aiofiles.open(self.token_file, "w") as token:
145
+ await token.write(self.creds.to_json())
146
+ logger.info(f"💾 Tokens saved to {self.token_file}")
147
+
148
+ # Build Gmail service
149
+ self.service = build("gmail", "v1", credentials=self.creds)
150
+ self._authenticated = True
151
+ logger.info("✅ Gmail API ready!")
152
+ return True
153
+
154
+ except Exception as e:
155
+ logger.error(f"❌ Gmail authentication failed: {e}")
156
+ return False
157
+
158
+ async def get_recent_emails(
159
+ self, max_results: int = 10, query: str = "", time_filter: str = "1h"
160
+ ) -> List[Dict[str, Any]]:
161
+ """
162
+ Get recent emails with optional query filter
163
+ Args:
164
+ max_results: Maximum number of emails to fetch
165
+ query: Gmail search query (e.g., 'from:noreply@example.com')
166
+ time_filter: Time filter (e.g., '5m', '1h', '1d')
167
+ Returns:
168
+ List of email dictionaries with parsed content
169
+ """
170
+ if not self.is_authenticated():
171
+ logger.error("❌ Gmail service not authenticated. Call authenticate() first.")
172
+ return []
173
+
174
+ try:
175
+ # Add time filter to query if provided
176
+ if time_filter and "newer_than:" not in query:
177
+ query = f"newer_than:{time_filter} {query}".strip()
178
+
179
+ logger.info(f"📧 Fetching {max_results} recent emails...")
180
+ if query:
181
+ logger.debug(f"🔍 Query: {query}")
182
+
183
+ # Get message list
184
+ assert self.service is not None
185
+ results = self.service.users().messages().list(userId="me", maxResults=max_results, q=query).execute()
186
+
187
+ messages = results.get("messages", [])
188
+ if not messages:
189
+ logger.info("📭 No messages found")
190
+ return []
191
+
192
+ logger.info(f"📨 Found {len(messages)} messages, fetching details...")
193
+
194
+ # Get full message details
195
+ emails = []
196
+ for i, message in enumerate(messages, 1):
197
+ logger.debug(f"📖 Reading email {i}/{len(messages)}...")
198
+
199
+ full_message = (
200
+ self.service.users().messages().get(userId="me", id=message["id"], format="full").execute()
201
+ )
202
+
203
+ email_data = self._parse_email(full_message)
204
+ emails.append(email_data)
205
+
206
+ return emails
207
+
208
+ except HttpError as error:
209
+ logger.error(f"❌ Gmail API error: {error}")
210
+ return []
211
+ except Exception as e:
212
+ logger.error(f"❌ Unexpected error fetching emails: {e}")
213
+ return []
214
+
215
+ def _parse_email(self, message: Dict[str, Any]) -> Dict[str, Any]:
216
+ """Parse Gmail message into readable format"""
217
+ headers = {h["name"]: h["value"] for h in message["payload"]["headers"]}
218
+
219
+ return {
220
+ "id": message["id"],
221
+ "thread_id": message["threadId"],
222
+ "subject": headers.get("Subject", ""),
223
+ "from": headers.get("From", ""),
224
+ "to": headers.get("To", ""),
225
+ "date": headers.get("Date", ""),
226
+ "timestamp": int(message["internalDate"]),
227
+ "body": self._extract_body(message["payload"]),
228
+ "raw_message": message,
229
+ }
230
+
231
+ def _extract_body(self, payload: Dict[str, Any]) -> str:
232
+ """Extract email body from payload"""
233
+ body = ""
234
+
235
+ if payload.get("body", {}).get("data"):
236
+ # Simple email body
237
+ body = base64.urlsafe_b64decode(payload["body"]["data"]).decode("utf-8")
238
+ elif payload.get("parts"):
239
+ # Multi-part email
240
+ for part in payload["parts"]:
241
+ if part["mimeType"] == "text/plain" and part.get("body", {}).get("data"):
242
+ part_body = base64.urlsafe_b64decode(part["body"]["data"]).decode("utf-8")
243
+ body += part_body
244
+ elif part["mimeType"] == "text/html" and not body and part.get("body", {}).get("data"):
245
+ # Fallback to HTML if no plain text
246
+ body = base64.urlsafe_b64decode(part["body"]["data"]).decode("utf-8")
247
+
248
+ return body
249
+
250
+
251
+ @register_toolkit("gmail")
252
+ class GmailToolkit(AsyncBaseToolkit):
253
+ """
254
+ Toolkit for Gmail integration.
255
+
256
+ Provides functionality for:
257
+ - Gmail API authentication via OAuth2 or access tokens
258
+ - Reading recent emails with filtering and search
259
+ - 2FA code extraction and verification
260
+ - Email content analysis
261
+
262
+ Required configuration:
263
+ - GMAIL_ACCESS_TOKEN: Direct access token (optional, alternative to OAuth)
264
+ - GMAIL_CREDENTIALS_FILE: Path to OAuth credentials JSON (optional)
265
+ - GMAIL_TOKEN_FILE: Path to store/load access tokens (optional)
266
+
267
+ Environment variables:
268
+ - GMAIL_ACCESS_TOKEN: Direct Gmail access token
269
+ - GMAIL_CREDENTIALS_PATH: Path to OAuth credentials file
270
+ - GMAIL_TOKEN_PATH: Path to token storage file
271
+ """
272
+
273
+ def __init__(self, config: ToolkitConfig = None):
274
+ """
275
+ Initialize the GmailToolkit.
276
+
277
+ Args:
278
+ config: Toolkit configuration containing Gmail settings
279
+ """
280
+ super().__init__(config)
281
+
282
+ # Get configuration from environment or config
283
+ access_token = self.config.config.get("GMAIL_ACCESS_TOKEN") or os.getenv("GMAIL_ACCESS_TOKEN")
284
+ credentials_file = self.config.config.get("GMAIL_CREDENTIALS_FILE") or os.getenv("GMAIL_CREDENTIALS_PATH")
285
+ token_file = self.config.config.get("GMAIL_TOKEN_FILE") or os.getenv("GMAIL_TOKEN_PATH")
286
+
287
+ # Initialize Gmail service
288
+ self.gmail_service = GmailService(
289
+ credentials_file=credentials_file, token_file=token_file, access_token=access_token
290
+ )
291
+
292
+ if not access_token and not credentials_file:
293
+ self.logger.warning(
294
+ "No Gmail access token or credentials file configured. "
295
+ "Set GMAIL_ACCESS_TOKEN or GMAIL_CREDENTIALS_PATH environment variable "
296
+ "or provide via ToolkitConfig"
297
+ )
298
+
299
+ async def authenticate_gmail(self) -> str:
300
+ """
301
+ Authenticate with Gmail API using OAuth2 or provided access token.
302
+
303
+ Returns:
304
+ Authentication status message
305
+ """
306
+ try:
307
+ self.logger.info("Authenticating with Gmail API...")
308
+
309
+ if await self.gmail_service.authenticate():
310
+ return "✅ Successfully authenticated with Gmail API"
311
+ else:
312
+ return "❌ Failed to authenticate with Gmail API. Please check credentials."
313
+
314
+ except Exception as e:
315
+ error_msg = f"Gmail authentication error: {str(e)}"
316
+ self.logger.error(error_msg)
317
+ return f"❌ {error_msg}"
318
+
319
+ async def get_recent_emails(self, keyword: str = "", max_results: int = 10, time_filter: str = "1h") -> str:
320
+ """
321
+ Get recent emails from the mailbox with optional keyword filtering.
322
+
323
+ This tool is particularly useful for:
324
+ - Retrieving verification codes, OTP, 2FA tokens from recent emails
325
+ - Finding magic links for account verification
326
+ - Reading recent email content for specific services
327
+ - Monitoring for new messages from specific senders
328
+
329
+ Args:
330
+ keyword: A single keyword for search (e.g., 'github', 'airbnb', 'verification', 'otp')
331
+ max_results: Maximum number of emails to retrieve (1-50, default: 10)
332
+ time_filter: Time window for search ('5m', '1h', '1d', '1w', default: '1h')
333
+
334
+ Returns:
335
+ Formatted string with email details including subject, sender, date, and content
336
+ """
337
+ try:
338
+ # Ensure authentication
339
+ if not self.gmail_service.is_authenticated():
340
+ self.logger.info("📧 Gmail not authenticated, attempting authentication...")
341
+ if not await self.gmail_service.authenticate():
342
+ return "❌ Failed to authenticate with Gmail. Please ensure Gmail credentials are set up properly."
343
+
344
+ # Validate parameters
345
+ max_results = max(1, min(max_results, 50)) # Clamp between 1-50
346
+
347
+ # Build query with time filter and optional keyword
348
+ query_parts = [f"newer_than:{time_filter}"]
349
+ if keyword.strip():
350
+ query_parts.append(keyword.strip())
351
+
352
+ query = " ".join(query_parts)
353
+ self.logger.info(f"🔍 Gmail search query: {query}")
354
+
355
+ # Get emails
356
+ emails = await self.gmail_service.get_recent_emails(
357
+ max_results=max_results, query=query, time_filter=time_filter
358
+ )
359
+
360
+ if not emails:
361
+ query_info = f" matching '{keyword}'" if keyword.strip() else ""
362
+ return f"📭 No recent emails found from last {time_filter}{query_info}"
363
+
364
+ # Format results
365
+ content = (
366
+ f'📧 Found {len(emails)} recent email{"s" if len(emails) > 1 else ""} from the last {time_filter}:\n\n'
367
+ )
368
+
369
+ for i, email in enumerate(emails, 1):
370
+ content += f"Email {i}:\n"
371
+ content += f'From: {email["from"]}\n'
372
+ content += f'Subject: {email["subject"]}\n'
373
+ content += f'Date: {email["date"]}\n'
374
+ content += f'Content:\n{email["body"]}\n'
375
+ content += "-" * 50 + "\n\n"
376
+
377
+ self.logger.info(f"📧 Retrieved {len(emails)} recent emails")
378
+ return content
379
+
380
+ except Exception as e:
381
+ error_msg = f"Error getting recent emails: {str(e)}"
382
+ self.logger.error(error_msg)
383
+ return f"❌ {error_msg}"
384
+
385
+ async def search_emails(self, query: str, max_results: int = 10, time_filter: Optional[str] = None) -> str:
386
+ """
387
+ Search emails using Gmail search syntax.
388
+
389
+ Supports full Gmail search operators:
390
+ - from:sender@example.com - emails from specific sender
391
+ - to:recipient@example.com - emails to specific recipient
392
+ - subject:keyword - emails with keyword in subject
393
+ - has:attachment - emails with attachments
394
+ - is:unread - unread emails only
395
+ - newer_than:1d - emails newer than 1 day
396
+ - older_than:1w - emails older than 1 week
397
+ - label:inbox - emails in specific label
398
+
399
+ Args:
400
+ query: Gmail search query using Gmail search operators
401
+ max_results: Maximum number of emails to retrieve (1-50, default: 10)
402
+ time_filter: Optional time filter to add to query ('5m', '1h', '1d', '1w')
403
+
404
+ Returns:
405
+ Formatted string with matching email details
406
+
407
+ Example queries:
408
+ - "from:noreply@github.com" - All emails from GitHub
409
+ - "subject:verification code" - Emails with "verification code" in subject
410
+ - "from:security@google.com has:attachment" - Security emails from Google with attachments
411
+ """
412
+ try:
413
+ # Ensure authentication
414
+ if not self.gmail_service.is_authenticated():
415
+ if not await self.gmail_service.authenticate():
416
+ return "❌ Failed to authenticate with Gmail. Please ensure Gmail credentials are set up properly."
417
+
418
+ # Validate parameters
419
+ max_results = max(1, min(max_results, 50))
420
+
421
+ # Add time filter if provided
422
+ full_query = query
423
+ if time_filter and "newer_than:" not in query and "older_than:" not in query:
424
+ full_query = f"newer_than:{time_filter} {query}".strip()
425
+
426
+ self.logger.info(f"🔍 Gmail search query: {full_query}")
427
+
428
+ # Search emails
429
+ emails = await self.gmail_service.get_recent_emails(
430
+ max_results=max_results,
431
+ query=full_query,
432
+ time_filter=time_filter or "30d", # Default to 30 days if no time filter
433
+ )
434
+
435
+ if not emails:
436
+ return f"📭 No emails found matching query: {full_query}"
437
+
438
+ # Format results
439
+ content = f'🔍 Found {len(emails)} email{"s" if len(emails) > 1 else ""} matching "{query}":\n\n'
440
+
441
+ for i, email in enumerate(emails, 1):
442
+ content += f"Email {i}:\n"
443
+ content += f'From: {email["from"]}\n'
444
+ content += f'Subject: {email["subject"]}\n'
445
+ content += f'Date: {email["date"]}\n'
446
+ content += f'Content:\n{email["body"][:500]}{"..." if len(email["body"]) > 500 else ""}\n'
447
+ content += "-" * 50 + "\n\n"
448
+
449
+ self.logger.info(f"🔍 Found {len(emails)} emails matching search")
450
+ return content
451
+
452
+ except Exception as e:
453
+ error_msg = f"Error searching emails: {str(e)}"
454
+ self.logger.error(error_msg)
455
+ return f"❌ {error_msg}"
456
+
457
+ async def get_verification_codes(self, sender_keyword: str = "", time_filter: str = "10m") -> str:
458
+ """
459
+ Extract verification codes, OTP tokens, and 2FA codes from recent emails.
460
+
461
+ This tool specifically looks for common patterns in verification emails:
462
+ - Numeric verification codes (4-8 digits)
463
+ - Alphanumeric codes
464
+ - Terms like "verification code", "OTP", "2FA", "authentication"
465
+ - Magic links for verification
466
+
467
+ Args:
468
+ sender_keyword: Filter by sender keyword (e.g., 'google', 'github', 'apple')
469
+ time_filter: Time window for search (default: '10m' for recent codes)
470
+
471
+ Returns:
472
+ Extracted verification codes and relevant email excerpts
473
+ """
474
+ try:
475
+ # Build search query focused on verification emails
476
+ query_parts = ["newer_than:" + time_filter]
477
+
478
+ # Add verification-related keywords
479
+ verification_keywords = [
480
+ "verification",
481
+ "OTP",
482
+ "2FA",
483
+ "authentication",
484
+ "code",
485
+ "verify",
486
+ "confirm",
487
+ "security",
488
+ "sign in",
489
+ "login",
490
+ ]
491
+
492
+ if sender_keyword.strip():
493
+ query_parts.append(f"from:{sender_keyword}")
494
+
495
+ # Add verification keywords to query
496
+ keyword_query = " OR ".join(verification_keywords)
497
+ query_parts.append(f"({keyword_query})")
498
+
499
+ query = " ".join(query_parts)
500
+
501
+ # Get emails
502
+ emails = await self.gmail_service.get_recent_emails(
503
+ max_results=20, query=query, time_filter=time_filter # Check more emails for verification codes
504
+ )
505
+
506
+ if not emails:
507
+ return f"📭 No verification emails found in the last {time_filter}"
508
+
509
+ # Extract codes from emails
510
+ codes_found = []
511
+
512
+ import re
513
+
514
+ # Patterns for common verification codes - more specific patterns
515
+ code_patterns = [
516
+ r"(?:code|otp|verification)[\s:]+(\d{4,8})", # "code: 123456", "OTP: 123456"
517
+ r"\b(\d{6})\b", # Common 6-digit codes
518
+ r"\b(\d{4,5})\b(?=\s*(?:is|to|for|$))", # 4-5 digit codes followed by context
519
+ r"(?:your|the)\s+(?:code|otp)[\s:]+(\d{4,8})", # "your code: 123456"
520
+ r"enter[\s\w]*?(\d{4,8})", # "enter this code 123456"
521
+ ]
522
+
523
+ for email in emails:
524
+ email_content = f"{email['subject']} {email['body']}"
525
+
526
+ for pattern in code_patterns:
527
+ matches = re.findall(pattern, email_content, re.IGNORECASE)
528
+ for match in matches:
529
+ # Filter out obviously non-code numbers (years, common numbers)
530
+ if isinstance(match, str) and len(match) >= 4:
531
+ if not match in ["2023", "2024", "2025", "1234", "0000"]:
532
+ codes_found.append(
533
+ {
534
+ "code": match,
535
+ "from": email["from"],
536
+ "subject": email["subject"],
537
+ "date": email["date"],
538
+ }
539
+ )
540
+
541
+ if not codes_found:
542
+ return f"🔍 Found {len(emails)} verification emails but no codes could be extracted. Check emails manually."
543
+
544
+ # Format results
545
+ content = (
546
+ f"🔐 Found {len(codes_found)} potential verification code{'s' if len(codes_found) > 1 else ''}:\n\n"
547
+ )
548
+
549
+ for i, code_info in enumerate(codes_found[:5], 1): # Show top 5 codes
550
+ content += f"Code {i}: {code_info['code']}\n"
551
+ content += f"From: {code_info['from']}\n"
552
+ content += f"Subject: {code_info['subject']}\n"
553
+ content += f"Date: {code_info['date']}\n"
554
+ content += "-" * 30 + "\n\n"
555
+
556
+ return content
557
+
558
+ except Exception as e:
559
+ error_msg = f"Error extracting verification codes: {str(e)}"
560
+ self.logger.error(error_msg)
561
+ return f"❌ {error_msg}"
562
+
563
+ async def get_tools_map(self) -> Dict[str, Callable]:
564
+ """
565
+ Get the mapping of tool names to their implementation functions.
566
+
567
+ Returns:
568
+ Dictionary mapping tool names to callable functions
569
+ """
570
+ return {
571
+ "authenticate_gmail": self.authenticate_gmail,
572
+ "get_recent_emails": self.get_recent_emails,
573
+ "search_emails": self.search_emails,
574
+ "get_verification_codes": self.get_verification_codes,
575
+ }