noesium 0.1.0__py3-none-any.whl → 0.2.1__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.
- noesium/agents/askura_agent/__init__.py +22 -0
- noesium/agents/askura_agent/askura_agent.py +480 -0
- noesium/agents/askura_agent/conversation.py +164 -0
- noesium/agents/askura_agent/extractor.py +175 -0
- noesium/agents/askura_agent/memory.py +14 -0
- noesium/agents/askura_agent/models.py +239 -0
- noesium/agents/askura_agent/prompts.py +202 -0
- noesium/agents/askura_agent/reflection.py +234 -0
- noesium/agents/askura_agent/summarizer.py +30 -0
- noesium/agents/askura_agent/utils.py +6 -0
- noesium/agents/deep_research/__init__.py +13 -0
- noesium/agents/deep_research/agent.py +398 -0
- noesium/agents/deep_research/prompts.py +84 -0
- noesium/agents/deep_research/schemas.py +42 -0
- noesium/agents/deep_research/state.py +54 -0
- noesium/agents/search/__init__.py +5 -0
- noesium/agents/search/agent.py +474 -0
- noesium/agents/search/state.py +28 -0
- noesium/core/__init__.py +1 -1
- noesium/core/agent/base.py +10 -2
- noesium/core/goalith/decomposer/llm_decomposer.py +1 -1
- noesium/core/llm/__init__.py +1 -1
- noesium/core/llm/base.py +2 -2
- noesium/core/llm/litellm.py +42 -21
- noesium/core/llm/llamacpp.py +25 -4
- noesium/core/llm/ollama.py +43 -22
- noesium/core/llm/openai.py +25 -5
- noesium/core/llm/openrouter.py +1 -1
- noesium/core/toolify/base.py +9 -2
- noesium/core/toolify/config.py +2 -2
- noesium/core/toolify/registry.py +21 -5
- noesium/core/tracing/opik_tracing.py +7 -7
- noesium/core/vector_store/__init__.py +2 -2
- noesium/core/vector_store/base.py +1 -1
- noesium/core/vector_store/pgvector.py +10 -13
- noesium/core/vector_store/weaviate.py +2 -1
- noesium/toolkits/__init__.py +1 -0
- noesium/toolkits/arxiv_toolkit.py +310 -0
- noesium/toolkits/audio_aliyun_toolkit.py +441 -0
- noesium/toolkits/audio_toolkit.py +370 -0
- noesium/toolkits/bash_toolkit.py +332 -0
- noesium/toolkits/document_toolkit.py +454 -0
- noesium/toolkits/file_edit_toolkit.py +552 -0
- noesium/toolkits/github_toolkit.py +395 -0
- noesium/toolkits/gmail_toolkit.py +575 -0
- noesium/toolkits/image_toolkit.py +425 -0
- noesium/toolkits/memory_toolkit.py +398 -0
- noesium/toolkits/python_executor_toolkit.py +334 -0
- noesium/toolkits/search_toolkit.py +451 -0
- noesium/toolkits/serper_toolkit.py +623 -0
- noesium/toolkits/tabular_data_toolkit.py +537 -0
- noesium/toolkits/user_interaction_toolkit.py +365 -0
- noesium/toolkits/video_toolkit.py +168 -0
- noesium/toolkits/wikipedia_toolkit.py +420 -0
- noesium-0.2.1.dist-info/METADATA +253 -0
- {noesium-0.1.0.dist-info → noesium-0.2.1.dist-info}/RECORD +59 -23
- {noesium-0.1.0.dist-info → noesium-0.2.1.dist-info}/licenses/LICENSE +1 -1
- noesium-0.1.0.dist-info/METADATA +0 -525
- {noesium-0.1.0.dist-info → noesium-0.2.1.dist-info}/WHEEL +0 -0
- {noesium-0.1.0.dist-info → noesium-0.2.1.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
|
+
}
|