.gitignore DELETED
@@ -1,13 +0,0 @@
1
- .env
2
- myenv/
3
- venv/
4
-
5
- __pycache__/
6
- *.py[cod]
7
- *.pyo
8
- *.pyd
9
- *.log
10
-
11
- agentic_implementation/*.json
12
- agentic_implementation/*.db
13
- logs/
 
 
 
 
 
 
 
 
 
 
 
 
 
 
README.md CHANGED
@@ -1,19 +1,13 @@
1
  ---
2
- tag: "mcp-server-track"
3
  title: MailQuery
4
  emoji: 💬
5
  colorFrom: yellow
6
  colorTo: purple
7
  sdk: gradio
8
  sdk_version: 5.0.1
9
- app_file: agentic_implementation/email_mcp_server_oauth.py
10
  pinned: false
11
  short_description: Answer any questions you have about the content of your mail
12
  ---
13
 
14
-
15
- Our Google authentication verification is currently pending. If you would like to proceed with testing, we can add you as a test user. To facilitate this, please complete the Google Form provided below.
16
-
17
- https://forms.gle/FRWwhhMeKaiXDAJQ9
18
-
19
- Link to the vide demo: https://www.youtube.com/watch?v=Ie8kdGU6bjY
 
1
  ---
 
2
  title: MailQuery
3
  emoji: 💬
4
  colorFrom: yellow
5
  colorTo: purple
6
  sdk: gradio
7
  sdk_version: 5.0.1
8
+ app_file: app.py
9
  pinned: false
10
  short_description: Answer any questions you have about the content of your mail
11
  ---
12
 
13
+ An example chatbot using [Gradio](https://gradio.app), [`huggingface_hub`](https://huggingface.co/docs/huggingface_hub/v0.22.2/en/index), and the [Hugging Face Inference API](https://huggingface.co/docs/api-inference/index).
 
 
 
 
 
agentic_implementation/README_OAuth.md DELETED
@@ -1,258 +0,0 @@
1
- # Gmail MCP Server with OAuth Authentication
2
-
3
- This is an enhanced version of the Gmail MCP (Model Context Protocol) server that uses **OAuth 2.0 authentication** instead of requiring users to provide email credentials for each query.
4
-
5
- ## 🚀 Key Features
6
-
7
- - **OAuth 2.0 Authentication**: Secure authentication flow using Google's OAuth system
8
- - **One-time Setup**: Authenticate once, use anywhere
9
- - **Automatic Token Refresh**: Handles token expiration automatically
10
- - **Encrypted Storage**: Credentials are encrypted and stored securely
11
- - **No More Password Sharing**: No need to provide email/password to Claude
12
-
13
- ## 📋 Prerequisites
14
-
15
- 1. **Google Account**: You need a Gmail account
16
- 2. **Google Cloud Project**: Free to create
17
- 3. **Python 3.8+**: Required for running the server
18
-
19
- ## 🛠️ Setup Instructions
20
-
21
- ### Step 1: Install Dependencies
22
-
23
- ```bash
24
- pip install -r requirements_oauth.txt
25
- ```
26
-
27
- ### Step 2: Run the Interactive Setup
28
-
29
- The setup script will guide you through the entire process:
30
-
31
- ```bash
32
- python setup_oauth.py
33
- ```
34
-
35
- This will walk you through:
36
- 1. Creating a Google Cloud project
37
- 2. Enabling the Gmail API
38
- 3. Setting up OAuth consent screen
39
- 4. Creating OAuth credentials
40
- 5. Testing the authentication flow
41
-
42
- ### Step 3: Start the MCP Server
43
-
44
- ```bash
45
- python email_mcp_server_oauth.py
46
- ```
47
-
48
- The server will start and show you:
49
- - Authentication status
50
- - MCP endpoint URL
51
- - Web interface URL
52
-
53
- ## 🔧 Claude Desktop Configuration
54
-
55
- Add this configuration to your Claude Desktop MCP settings:
56
-
57
- ```json
58
- {
59
- "mcpServers": {
60
- "gmail-oauth": {
61
- "command": "npx",
62
- "args": [
63
- "mcp-remote",
64
- "http://localhost:7860/gradio_api/mcp/sse"
65
- ]
66
- }
67
- }
68
- }
69
- ```
70
-
71
- ## 🔍 Available Tools
72
-
73
- ### 1. search_emails
74
- Search your emails using natural language queries - **no credentials needed!**
75
-
76
- **Parameters:**
77
- - `query`: Natural language query (e.g., "show me emails from amazon last week")
78
-
79
- **Example Usage in Claude:**
80
- > "Can you search my emails for messages from Swiggy in the last week?"
81
-
82
- ### 2. get_email_details
83
- Get full details of a specific email by message ID.
84
-
85
- **Parameters:**
86
- - `message_id`: Message ID from search results
87
-
88
- ### 3. analyze_email_patterns
89
- Analyze email patterns from a specific sender over time.
90
-
91
- **Parameters:**
92
- - `sender_keyword`: Sender to analyze (e.g., "amazon", "google")
93
- - `days_back`: Number of days to analyze (default: "30")
94
-
95
- ### 4. authenticate_user
96
- Trigger the OAuth authentication flow from Claude Desktop.
97
-
98
- **Parameters:** None
99
-
100
- ### 5. get_authentication_status
101
- Check current authentication status.
102
-
103
- **Parameters:** None
104
-
105
- ## 🔐 Security Features
106
-
107
- ### Encrypted Storage
108
- - All credentials are encrypted using Fernet encryption
109
- - Encryption keys are stored securely with proper permissions
110
- - No plaintext credentials are ever stored
111
-
112
- ### OAuth Benefits
113
- - No need to share Gmail passwords
114
- - Granular permission control
115
- - Easy revocation from Google Account settings
116
- - Automatic token refresh
117
-
118
- ### Local Storage
119
- - All data stored locally on your machine
120
- - No cloud storage of credentials
121
- - You maintain full control
122
-
123
- ## 🔧 Advanced Usage
124
-
125
- ### Command Line Tools
126
-
127
- Check authentication status:
128
- ```bash
129
- python setup_oauth.py --status
130
- ```
131
-
132
- Re-authenticate:
133
- ```bash
134
- python setup_oauth.py --auth
135
- ```
136
-
137
- Clear stored credentials:
138
- ```bash
139
- python setup_oauth.py --clear
140
- ```
141
-
142
- Show help:
143
- ```bash
144
- python setup_oauth.py --help
145
- ```
146
-
147
- ### Web Interface
148
-
149
- When the server is running, you can access the web interface at:
150
- ```
151
- http://localhost:7860
152
- ```
153
-
154
- Use this interface to:
155
- - Check authentication status
156
- - Trigger authentication flow
157
- - Test email search functionality
158
-
159
- ## 🆚 Comparison: OAuth vs App Passwords
160
-
161
- | Feature | App Password (Old) | OAuth (New) |
162
- |---------|-------------------|-------------|
163
- | **Setup Complexity** | Simple | One-time setup required |
164
- | **Security** | Share app password | No password sharing |
165
- | **User Experience** | Enter credentials each time | Authenticate once |
166
- | **Revocation** | Change app password | Revoke from Google Account |
167
- | **Token Management** | Manual | Automatic refresh |
168
- | **Scope Control** | Full Gmail access | Granular permissions |
169
-
170
- ## 🐛 Troubleshooting
171
-
172
- ### Authentication Issues
173
-
174
- **"OAuth not configured" error:**
175
- ```bash
176
- python setup_oauth.py
177
- ```
178
-
179
- **"Not authenticated" error:**
180
- ```bash
181
- python setup_oauth.py --auth
182
- ```
183
-
184
- **Authentication timeout:**
185
- - Check if port 8080 is available
186
- - Try disabling firewall temporarily
187
- - Ensure browser can access localhost:8080
188
-
189
- ### Common Issues
190
-
191
- **"No module named 'google.auth'" error:**
192
- ```bash
193
- pip install -r requirements_oauth.txt
194
- ```
195
-
196
- **"Permission denied" on credential files:**
197
- ```bash
198
- # Check permissions
199
- ls -la ~/.mailquery_oauth/
200
- # Should show restricted permissions (600/700)
201
- ```
202
-
203
- **Browser doesn't open:**
204
- - Copy the authorization URL manually
205
- - Paste it in your browser
206
- - Complete the flow manually
207
-
208
- ### Getting Help
209
-
210
- 1. Check authentication status: `python setup_oauth.py --status`
211
- 2. Review server logs for detailed error messages
212
- 3. Ensure Google Cloud project is properly configured
213
- 4. Verify OAuth consent screen is set up correctly
214
-
215
- ## 📁 File Structure
216
-
217
- ```
218
- ~/.mailquery_oauth/
219
- ├── client_secret.json # OAuth client configuration
220
- ├── token.pickle # Encrypted access/refresh tokens
221
- └── key.key # Encryption key (secure permissions)
222
- ```
223
-
224
- ## 🔄 Migration from App Password Version
225
-
226
- If you're migrating from the app password version:
227
-
228
- 1. Run the new OAuth setup: `python setup_oauth.py`
229
- 2. Update your Claude Desktop configuration to use the new server
230
- 3. The old environment variables (EMAIL_ID, APP_PASSWORD) are no longer needed
231
-
232
- ## 📞 Support
233
-
234
- For issues or questions:
235
- 1. Check the troubleshooting section above
236
- 2. Review the setup script output for specific guidance
237
- 3. Ensure all prerequisites are met
238
- 4. Verify Google Cloud project configuration
239
-
240
- ## 🎯 Example Queries for Claude
241
-
242
- Once set up, you can ask Claude:
243
-
244
- - "Search my emails for messages from Amazon in the last month"
245
- - "Show me emails from my bank from last week"
246
- - "Analyze my LinkedIn email patterns over the last 60 days"
247
- - "Find emails from Swiggy today"
248
- - "Get details of the email with ID xyz123"
249
-
250
- Claude will automatically use the OAuth-authenticated tools without asking for credentials!
251
-
252
- ## 🔒 Privacy & Data
253
-
254
- - **No data leaves your machine**: All processing happens locally
255
- - **Google only provides**: Access to your Gmail via official APIs
256
- - **We store**: Encrypted authentication tokens only
257
- - **We never store**: Email content, passwords, or personal data
258
- - **You control**: Access can be revoked anytime from Google Account settings
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
agentic_implementation/email_mcp_server_oauth.py DELETED
@@ -1,786 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Gmail MCP Server with OAuth Authentication and Multi-Account Support
4
- """
5
-
6
- import gradio as gr
7
- import json
8
- import base64
9
- from email.mime.text import MIMEText
10
- from googleapiclient.errors import HttpError
11
- import os
12
- from typing import Dict, List
13
- from datetime import datetime, timedelta
14
- from dotenv import load_dotenv
15
- from fastapi.responses import HTMLResponse
16
- from fastapi import FastAPI,Request
17
- from fastapi.routing import APIRoute
18
- # Import OAuth-enabled modules
19
- # from tools import extract_query_info, analyze_emails
20
- from gmail_api_scraper import GmailAPIScraper
21
- from oauth_manager import oauth_manager
22
- from logger import logger
23
-
24
- load_dotenv()
25
-
26
- if not oauth_manager.client_secrets_file.exists():
27
- print("Setupy")
28
- oauth_manager.setup_client_secrets(
29
- os.environ["GOOGLE_CLIENT_ID"],
30
- os.environ["GOOGLE_CLIENT_SECRET"]
31
- )
32
-
33
- # Initialize Gmail API scraper
34
- gmail_scraper = GmailAPIScraper()
35
-
36
- def check_authentication() -> tuple[bool, str]:
37
- """Check if user is authenticated and return status"""
38
- current_account = oauth_manager.get_current_account()
39
- if current_account and oauth_manager.is_authenticated():
40
- return True, current_account
41
- else:
42
- return False, "Not authenticated"
43
-
44
- def simple_analyze_emails(emails) -> dict:
45
- """
46
- Simple email analysis without OpenAI - just basic statistics and patterns
47
- """
48
- if not emails:
49
- return {"summary": "No emails to analyze.", "insights": []}
50
-
51
- # Basic statistics
52
- total_count = len(emails)
53
-
54
- # Group by sender
55
- senders = {}
56
- subjects = []
57
- dates = []
58
-
59
- for email in emails:
60
- sender = email.get("from", "Unknown")
61
- # Extract just the email domain for grouping
62
- if "<" in sender and ">" in sender:
63
- email_part = sender.split("<")[1].split(">")[0]
64
- else:
65
- email_part = sender
66
-
67
- domain = email_part.split("@")[-1] if "@" in email_part else sender
68
-
69
- senders[domain] = senders.get(domain, 0) + 1
70
- subjects.append(email.get("subject", ""))
71
- dates.append(email.get("date", ""))
72
-
73
- # Create insights
74
- insights = []
75
- insights.append(f"Found {total_count} emails total")
76
-
77
- if senders:
78
- top_sender = max(senders.items(), key=lambda x: x[1])
79
- insights.append(f"Most emails from: {top_sender[0]} ({top_sender[1]} emails)")
80
-
81
- if len(senders) > 1:
82
- insights.append(f"Emails from {len(senders)} different domains")
83
-
84
- # Date range
85
- if dates:
86
- unique_dates = list(set(dates))
87
- if len(unique_dates) > 1:
88
- insights.append(f"Spanning {len(unique_dates)} different days")
89
-
90
- # Subject analysis
91
- if subjects:
92
- # Count common words in subjects (simple approach)
93
- all_words = []
94
- for subject in subjects:
95
- words = subject.lower().split()
96
- all_words.extend([w for w in words if len(w) > 3]) # Only words longer than 3 chars
97
-
98
- if all_words:
99
- word_counts = {}
100
- for word in all_words:
101
- word_counts[word] = word_counts.get(word, 0) + 1
102
-
103
- if word_counts:
104
- common_word = max(word_counts.items(), key=lambda x: x[1])
105
- if common_word[1] > 1:
106
- insights.append(f"Common subject word: '{common_word[0]}' appears {common_word[1]} times")
107
-
108
- summary = f"Analysis of {total_count} emails from {len(senders)} sender(s)"
109
-
110
- return {
111
- "summary": summary,
112
- "insights": insights
113
- }
114
-
115
- def authenticate_user() -> str:
116
- """
117
- Start OAuth authentication flow for Gmail access.
118
- Opens a browser window for user to authenticate with Google.
119
-
120
- Returns:
121
- str: JSON string containing authentication result
122
- """
123
- try:
124
- logger.info("Starting OAuth authentication flow...")
125
-
126
- if oauth_manager.is_authenticated():
127
- user_email = oauth_manager.get_current_account()
128
- return json.dumps({
129
- "success": True,
130
- "message": "Already authenticated!",
131
- "user_email": user_email,
132
- "instructions": [
133
- "You are already authenticated and ready to use email tools",
134
- f"Currently authenticated as: {user_email}"
135
- ]
136
- }, indent=2)
137
- # Check if OAuth is configured
138
- if not oauth_manager.client_secrets_file.exists():
139
- return json.dumps({
140
- "error": "OAuth not configured",
141
- "message": "Please run 'python setup_oauth.py' first to configure OAuth credentials.",
142
- "success": False
143
- }, indent=2)
144
-
145
- # Start authentication
146
- success = oauth_manager.authenticate_interactive()
147
-
148
- if success:
149
- user_email = oauth_manager.get_current_account()
150
- result = {
151
- "success": True,
152
- "message": "Authentication successful! You can now use the email tools.",
153
- "user_email": user_email,
154
- "instructions": [
155
- "Authentication completed successfully",
156
- "You can now search emails, get email details, and analyze patterns",
157
- f"Currently authenticated as: {user_email}"
158
- ]
159
- }
160
- else:
161
- # Authentication not completed, provide manual instructions
162
- auth_url = oauth_manager.get_pending_auth_url()
163
- callback_url = oauth_manager.get_hf_redirect_uri()
164
-
165
- if auth_url:
166
- result = {
167
- "success": False,
168
- "message": "Manual authentication required",
169
- "auth_url": auth_url,
170
- "callback_url": callback_url,
171
- "instructions": [
172
- "Authentication URL has been generated",
173
- "Please click the link below to authenticate:",
174
- "1. Open the authentication URL(auth_url) in a new browser tab",
175
- "2. Sign in with your Google account",
176
- "3. Grant Gmail access permissions",
177
- "4. You'll be redirected back automatically",
178
- "5. Try clicking 'Submit' again after completing authentication"
179
- ],
180
- "note": "After completing authentication in the popup, click Submit again to verify"
181
- }
182
- else:
183
- result = {
184
- "success": False,
185
- "error": "Failed to generate authentication URL",
186
- "message": "Could not start authentication process. Check your OAuth configuration."
187
- }
188
-
189
- return json.dumps(result, indent=2)
190
-
191
- except Exception as e:
192
- logger.error("Error in authenticate_user: %s", e)
193
- error_result = {
194
- "success": False,
195
- "error": str(e),
196
- "message": "Authentication failed due to an error."
197
- }
198
- return json.dumps(error_result, indent=2)
199
-
200
-
201
- def handle_oauth_callback(auth_code: str) -> str:
202
- """Handle OAuth callback for Hugging Face Spaces
203
-
204
- Args:
205
- auth_code: Authorization code from OAuth callback
206
-
207
- Returns:
208
- HTML response string
209
- """
210
- try:
211
- if not auth_code:
212
- return """
213
- <html>
214
- <head><title>OAuth Error</title></head>
215
- <body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;">
216
- <h1 style="color: #d32f2f;">Authentication Error</h1>
217
- <p>No authorization code received.</p>
218
- <button onclick="window.close()" style="padding: 10px 20px; margin: 20px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer;">Close Window</button>
219
- </body>
220
- </html>
221
- """
222
- print(f"Received OAuth callback with code: {auth_code}")
223
- success = oauth_manager.complete_hf_spaces_auth(auth_code)
224
-
225
- if success:
226
- user_email = oauth_manager.get_current_account()
227
- return f"""
228
- <html>
229
- <head><title>OAuth Success</title></head>
230
- <body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;">
231
- <h1 style="color: #2e7d32;">🎉 Authentication Successful!</h1>
232
- <p>You are now authenticated as:</p>
233
- <p style="font-weight: bold; font-size: 18px; color: #1976d2;">{user_email}</p>
234
- <p>You can now close this window and return to the main application.</p>
235
- <p style="color: #666; font-size: 14px;">This window will close automatically in 5 seconds...</p>
236
- <button onclick="window.close()" style="padding: 10px 20px; margin: 20px; background: #2e7d32; color: white; border: none; border-radius: 4px; cursor: pointer;">Close Window</button>
237
- <script>
238
- setTimeout(function() {{
239
- window.close();
240
- }}, 5000);
241
- </script>
242
- </body>
243
- </html>
244
- """
245
- else:
246
- return """
247
- <html>
248
- <head><title>OAuth Error</title></head>
249
- <body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;">
250
- <h1 style="color: #d32f2f;">Authentication Failed</h1>
251
- <p>Unable to complete authentication. Please try again.</p>
252
- <p>Make sure you granted all required permissions.</p>
253
- <button onclick="window.close()" style="padding: 10px 20px; margin: 20px; background: #d32f2f; color: white; border: none; border-radius: 4px; cursor: pointer;">Close Window</button>
254
- </body>
255
- </html>
256
- """
257
-
258
- except Exception as e:
259
- logger.error(f"Error handling OAuth callback: {e}")
260
- return f"""
261
- <html>
262
- <head><title>OAuth Error</title></head>
263
- <body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;">
264
- <h1 style="color: #d32f2f;">Authentication Error</h1>
265
- <p>An error occurred during authentication:</p>
266
- <pre style="background: #f5f5f5; padding: 10px; border-radius: 4px; text-align: left; max-width: 500px; margin: 0 auto;">{str(e)}</pre>
267
- <button onclick="window.close()" style="padding: 10px 20px; margin: 20px; background: #d32f2f; color: white; border: none; border-radius: 4px; cursor: pointer;">Close Window</button>
268
- </body>
269
- </html>
270
- """
271
-
272
-
273
- def switch_account(target_email: str) -> str:
274
- """
275
- Switch to a different authenticated Gmail account.
276
-
277
- Args:
278
- target_email (str): Email address to switch to
279
-
280
- Returns:
281
- str: JSON string containing switch result
282
- """
283
- try:
284
- logger.info("Switching to account: %s", target_email)
285
-
286
- # Check if target account is authenticated
287
- if not oauth_manager.is_authenticated(target_email):
288
- return json.dumps({
289
- "error": "Account not authenticated",
290
- "message": f"Account '{target_email}' is not authenticated. Please authenticate first.",
291
- "target_email": target_email,
292
- "authenticated_accounts": list(oauth_manager.list_accounts().keys())
293
- }, indent=2)
294
-
295
- # Switch account
296
- success = oauth_manager.switch_account(target_email)
297
-
298
- if success:
299
- result = {
300
- "success": True,
301
- "message": f"Successfully switched to account: {target_email}",
302
- "current_account": oauth_manager.get_current_account(),
303
- "previous_account": None # Could track this if needed
304
- }
305
- else:
306
- result = {
307
- "success": False,
308
- "error": "Failed to switch account",
309
- "message": f"Could not switch to account: {target_email}",
310
- "current_account": oauth_manager.get_current_account()
311
- }
312
-
313
- return json.dumps(result, indent=2)
314
-
315
- except Exception as e:
316
- logger.error("Error switching account: %s", e)
317
- error_result = {
318
- "success": False,
319
- "error": str(e),
320
- "message": f"Failed to switch to account: {target_email}"
321
- }
322
- return json.dumps(error_result, indent=2)
323
-
324
- def list_accounts() -> str:
325
- """
326
- List all authenticated Gmail accounts and their status.
327
-
328
- Returns:
329
- str: JSON string containing all accounts and their authentication status
330
- """
331
- try:
332
- logger.info("Listing all accounts")
333
-
334
- accounts = oauth_manager.list_accounts()
335
- current_account = oauth_manager.get_current_account()
336
-
337
- result = {
338
- "accounts": accounts,
339
- "current_account": current_account,
340
- "total_accounts": len(accounts),
341
- "authenticated_accounts": [email for email, is_auth in accounts.items() if is_auth],
342
- "message": f"Found {len(accounts)} stored accounts, currently using: {current_account or 'None'}"
343
- }
344
-
345
- return json.dumps(result, indent=2)
346
-
347
- except Exception as e:
348
- logger.error("Error listing accounts: %s", e)
349
- error_result = {
350
- "error": str(e),
351
- "message": "Failed to list accounts"
352
- }
353
- return json.dumps(error_result, indent=2)
354
-
355
- def remove_account(email_to_remove: str) -> str:
356
- """
357
- Remove an authenticated Gmail account and its stored credentials.
358
-
359
- Args:
360
- email_to_remove (str): Email address to remove
361
-
362
- Returns:
363
- str: JSON string containing removal result
364
- """
365
- try:
366
- logger.info("Removing account: %s", email_to_remove)
367
-
368
- # Check if account exists
369
- accounts = oauth_manager.list_accounts()
370
- if email_to_remove not in accounts:
371
- return json.dumps({
372
- "error": "Account not found",
373
- "message": f"Account '{email_to_remove}' not found in stored accounts.",
374
- "available_accounts": list(accounts.keys())
375
- }, indent=2)
376
-
377
- # Remove account
378
- oauth_manager.remove_account(email_to_remove)
379
-
380
- result = {
381
- "success": True,
382
- "message": f"Successfully removed account: {email_to_remove}",
383
- "removed_account": email_to_remove,
384
- "current_account": oauth_manager.get_current_account(),
385
- "remaining_accounts": list(oauth_manager.list_accounts().keys())
386
- }
387
-
388
- return json.dumps(result, indent=2)
389
-
390
- except Exception as e:
391
- logger.error("Error removing account: %s", e)
392
- error_result = {
393
- "success": False,
394
- "error": str(e),
395
- "message": f"Failed to remove account: {email_to_remove}"
396
- }
397
- return json.dumps(error_result, indent=2)
398
-
399
- def search_emails(sender_keyword: str, start_date: str = "", end_date: str = "") -> str:
400
- """
401
- Search for emails from a specific sender within a date range using OAuth authentication.
402
-
403
- Args:
404
- sender_keyword (str): The sender/company keyword to search for (e.g., "apple", "amazon")
405
- start_date (str): Start date in DD-MMM-YYYY format (e.g., "01-Jan-2025"). If empty, defaults to 7 days ago.
406
- end_date (str): End date in DD-MMM-YYYY format (e.g., "07-Jan-2025"). If empty, defaults to today.
407
-
408
- Returns:
409
- str: JSON string containing email search results and analysis
410
- """
411
- try:
412
- logger.info("OAuth Email search tool called with sender: %s, dates: %s to %s", sender_keyword, start_date, end_date)
413
-
414
- # Check authentication
415
- is_auth, auth_info = check_authentication()
416
- if not is_auth:
417
- return json.dumps({
418
- "error": "Not authenticated",
419
- "message": "Please authenticate first using the authenticate_user tool or run 'python setup_oauth.py'",
420
- "auth_status": auth_info
421
- }, indent=2)
422
-
423
- # Set default date range if not provided
424
- if not start_date or not end_date:
425
- today = datetime.today()
426
- if not end_date:
427
- end_date = today.strftime("%d-%b-%Y")
428
- if not start_date:
429
- start_date = (today - timedelta(days=7)).strftime("%d-%b-%Y")
430
-
431
- logger.info(f"Searching for emails with keyword '{sender_keyword}' between {start_date} and {end_date}")
432
-
433
- # Use Gmail API scraper with OAuth
434
- full_emails = gmail_scraper.search_emails(sender_keyword, start_date, end_date)
435
-
436
- if not full_emails:
437
- result = {
438
- "sender_keyword": sender_keyword,
439
- "date_range": f"{start_date} to {end_date}",
440
- "email_summary": [],
441
- "analysis": {"summary": f"No emails found for '{sender_keyword}' in the specified date range.", "insights": []},
442
- "email_count": 0,
443
- "user_email": auth_info
444
- }
445
- return json.dumps(result, indent=2)
446
-
447
- # Create summary version without full content
448
- email_summary = []
449
- for email in full_emails:
450
- summary_email = {
451
- "date": email.get("date"),
452
- "time": email.get("time"),
453
- "subject": email.get("subject"),
454
- "from": email.get("from", "Unknown Sender"),
455
- "message_id": email.get("message_id"),
456
- "gmail_id": email.get("gmail_id")
457
- }
458
- email_summary.append(summary_email)
459
-
460
- # Auto-analyze the emails for insights (no OpenAI)
461
- analysis = simple_analyze_emails(full_emails)
462
-
463
- # Return summary info with analysis
464
- result = {
465
- "sender_keyword": sender_keyword,
466
- "date_range": f"{start_date} to {end_date}",
467
- "email_summary": email_summary,
468
- "analysis": analysis,
469
- "email_count": len(full_emails),
470
- "user_email": auth_info
471
- }
472
-
473
- return json.dumps(result, indent=2)
474
-
475
- except Exception as e:
476
- logger.error("Error in search_emails: %s", e)
477
- error_result = {
478
- "error": str(e),
479
- "sender_keyword": sender_keyword,
480
- "message": "Failed to search emails."
481
- }
482
- return json.dumps(error_result, indent=2)
483
-
484
- def get_email_details(message_id: str) -> str:
485
- """
486
- Get full details of a specific email by its message ID using OAuth authentication.
487
-
488
- Args:
489
- message_id (str): The message ID of the email to retrieve
490
-
491
- Returns:
492
- str: JSON string containing the full email details
493
- """
494
- try:
495
- logger.info("Getting email details for message_id: %s", message_id)
496
-
497
- # Check authentication
498
- is_auth, auth_info = check_authentication()
499
- if not is_auth:
500
- return json.dumps({
501
- "error": "Not authenticated",
502
- "message": "Please authenticate first using the authenticate_user tool or run 'python setup_oauth.py'",
503
- "auth_status": auth_info
504
- }, indent=2)
505
-
506
- # Get email using Gmail API
507
- email = gmail_scraper.get_email_by_id(message_id)
508
-
509
- if email:
510
- email["user_email"] = auth_info
511
- return json.dumps(email, indent=2)
512
- else:
513
- error_result = {
514
- "error": f"No email found with message_id '{message_id}'",
515
- "message": "Email may not exist or you may not have access to it.",
516
- "user_email": auth_info
517
- }
518
- return json.dumps(error_result, indent=2)
519
-
520
- except Exception as e:
521
- logger.error("Error in get_email_details: %s", e)
522
- error_result = {
523
- "error": str(e),
524
- "message_id": message_id,
525
- "message": "Failed to retrieve email details."
526
- }
527
- return json.dumps(error_result, indent=2)
528
-
529
- def analyze_email_patterns(sender_keyword: str, days_back: str = "30") -> str:
530
- """
531
- Analyze email patterns from a specific sender over a given time period using OAuth authentication.
532
-
533
- Args:
534
- sender_keyword (str): The sender/company keyword to analyze (e.g., "amazon", "google")
535
- days_back (str): Number of days to look back (default: "30")
536
-
537
- Returns:
538
- str: JSON string containing email pattern analysis
539
- """
540
- try:
541
- logger.info("Analyzing email patterns for sender: %s, days_back: %s", sender_keyword, days_back)
542
-
543
- # Check authentication
544
- is_auth, auth_info = check_authentication()
545
- if not is_auth:
546
- return json.dumps({
547
- "error": "Not authenticated",
548
- "message": "Please authenticate first using the authenticate_user tool or run 'python setup_oauth.py'",
549
- "auth_status": auth_info
550
- }, indent=2)
551
-
552
- # Calculate date range
553
- days_int = int(days_back)
554
- end_date = datetime.today()
555
- start_date = end_date - timedelta(days=days_int)
556
-
557
- start_date_str = start_date.strftime("%d-%b-%Y")
558
- end_date_str = end_date.strftime("%d-%b-%Y")
559
-
560
- # Search for emails using Gmail API
561
- full_emails = gmail_scraper.search_emails(sender_keyword, start_date_str, end_date_str)
562
-
563
- if not full_emails:
564
- result = {
565
- "sender_keyword": sender_keyword,
566
- "date_range": f"{start_date_str} to {end_date_str}",
567
- "analysis": {"summary": f"No emails found from '{sender_keyword}' in the last {days_back} days.", "insights": []},
568
- "email_count": 0,
569
- "user_email": auth_info
570
- }
571
- return json.dumps(result, indent=2)
572
-
573
- # Analyze the emails (no OpenAI)
574
- analysis = simple_analyze_emails(full_emails)
575
-
576
- result = {
577
- "sender_keyword": sender_keyword,
578
- "date_range": f"{start_date_str} to {end_date_str}",
579
- "analysis": analysis,
580
- "email_count": len(full_emails),
581
- "user_email": auth_info
582
- }
583
-
584
- return json.dumps(result, indent=2)
585
-
586
- except Exception as e:
587
- logger.error("Error in analyze_email_patterns: %s", e)
588
- error_result = {
589
- "error": str(e),
590
- "sender_keyword": sender_keyword,
591
- "message": "Failed to analyze email patterns."
592
- }
593
- return json.dumps(error_result, indent=2)
594
-
595
- def get_authentication_status() -> str:
596
- """
597
- Get current authentication status and account information.
598
-
599
- Returns:
600
- str: JSON string containing authentication status
601
- """
602
- try:
603
- current_account = oauth_manager.get_current_account()
604
- is_auth = oauth_manager.is_authenticated() if current_account else False
605
- all_accounts = oauth_manager.list_accounts()
606
-
607
- result = {
608
- "authenticated": is_auth,
609
- "current_account": current_account,
610
- "status": "authenticated" if is_auth else "not_authenticated",
611
- "message": f"Current account: {current_account}" if is_auth else "No account selected or not authenticated",
612
- "all_accounts": all_accounts,
613
- "total_accounts": len(all_accounts),
614
- "authenticated_accounts": [email for email, auth in all_accounts.items() if auth]
615
- }
616
-
617
- if not is_auth and not oauth_manager.client_secrets_file.exists():
618
- result["setup_required"] = True
619
- result["message"] = "OAuth not configured. Please run 'python setup_oauth.py' first."
620
- elif not is_auth and current_account:
621
- result["message"] = f"Account {current_account} needs re-authentication"
622
- elif not current_account and all_accounts:
623
- result["message"] = "Accounts available but none selected. Use switch_account to select one."
624
-
625
- return json.dumps(result, indent=2)
626
-
627
- except Exception as e:
628
- logger.error("Error checking authentication status: %s", e)
629
- return json.dumps({
630
- "error": str(e),
631
- "message": "Failed to check authentication status"
632
- }, indent=2)
633
-
634
- def send_email(recipient: str, subject: str, body: str) -> str:
635
- """
636
- Send a plain-text email via the authenticated Gmail account.
637
- Returns JSON with either:
638
- {"success": true, "message_id": "..."}
639
- or
640
- {"success": false, "error": "..."}
641
- """
642
- # Use the correct method on your OAuth manager:
643
- service = oauth_manager.get_gmail_service()
644
- if service is None:
645
- return json.dumps(
646
- {"success": False, "error": "Not authenticated or failed to build service."},
647
- indent=2,
648
- )
649
-
650
- # Build the MIME message
651
- mime_msg = MIMEText(body, "plain", "utf-8")
652
- mime_msg["to"] = recipient
653
- mime_msg["subject"] = subject
654
-
655
- # Base64-encode and send
656
- raw_msg = base64.urlsafe_b64encode(mime_msg.as_bytes()).decode()
657
- try:
658
- sent = (
659
- service.users()
660
- .messages()
661
- .send(userId="me", body={"raw": raw_msg})
662
- .execute()
663
- )
664
- return json.dumps(
665
- {"success": True, "message_id": sent.get("id")}, indent=2
666
- )
667
- except HttpError as err:
668
- logger.error(f"Error sending email: {err}")
669
- # err.error_details may be None; fallback to string
670
- error_detail = getattr(err, "error_details", None) or str(err)
671
- return json.dumps(
672
- {"success": False, "error": error_detail},
673
- indent=2,
674
- )
675
-
676
-
677
- # Create Gradio interfaces
678
- search_interface = gr.Interface(
679
- fn=search_emails,
680
- inputs=[
681
- gr.Textbox(label="Sender Keyword", placeholder="apple, amazon, etc."),
682
- gr.Textbox(label="Start Date (Optional)", placeholder="01-Jan-2025 (leave empty for last 7 days)"),
683
- gr.Textbox(label="End Date (Optional)", placeholder="07-Jan-2025 (leave empty for today)")
684
- ],
685
- outputs=gr.Textbox(label="Search Results", lines=20),
686
- title="Email Search (OAuth)",
687
- description="Search your emails by sender keyword and date range with OAuth authentication"
688
- )
689
-
690
- details_interface = gr.Interface(
691
- fn=get_email_details,
692
- inputs=[
693
- gr.Textbox(label="Message ID", placeholder="Email message ID from search results")
694
- ],
695
- outputs=gr.Textbox(label="Email Details", lines=20),
696
- title="Email Details (OAuth)",
697
- description="Get full details of a specific email by message ID with OAuth authentication"
698
- )
699
-
700
- analysis_interface = gr.Interface(
701
- fn=analyze_email_patterns,
702
- inputs=[
703
- gr.Textbox(label="Sender Keyword", placeholder="amazon, google, linkedin, etc."),
704
- gr.Textbox(label="Days Back", value="30", placeholder="Number of days to analyze")
705
- ],
706
- outputs=gr.Textbox(label="Analysis Results", lines=20),
707
- title="Email Pattern Analysis (OAuth)",
708
- description="Analyze email patterns from a specific sender over time with OAuth authentication"
709
- )
710
-
711
- auth_interface = gr.Interface(
712
- fn=authenticate_user,
713
- inputs=[],
714
- outputs=gr.Textbox(label="Authentication Result", lines=10),
715
- title="Authenticate with Gmail",
716
- description="Click Submit to start OAuth authentication flow with Gmail"
717
- )
718
-
719
- status_interface = gr.Interface(
720
- fn=get_authentication_status,
721
- inputs=[],
722
- outputs=gr.Textbox(label="Authentication Status", lines=15),
723
- title="Authentication Status",
724
- description="Check current authentication status and view all accounts"
725
- )
726
-
727
- switch_interface = gr.Interface(
728
- fn=switch_account,
729
- inputs=[
730
- gr.Textbox(label="Target Email", placeholder="email@gmail.com")
731
- ],
732
- outputs=gr.Textbox(label="Switch Result", lines=10),
733
- title="Switch Account",
734
- description="Switch to a different authenticated Gmail account"
735
- )
736
-
737
- accounts_interface = gr.Interface(
738
- fn=list_accounts,
739
- inputs=[],
740
- outputs=gr.Textbox(label="Accounts List", lines=15),
741
- title="List All Accounts",
742
- description="View all authenticated Gmail accounts and their status"
743
- )
744
-
745
- remove_interface = gr.Interface(
746
- fn=remove_account,
747
- inputs=[
748
- gr.Textbox(label="Email to Remove", placeholder="email@gmail.com")
749
- ],
750
- outputs=gr.Textbox(label="Removal Result", lines=10),
751
- title="Remove Account",
752
- description="Remove an authenticated Gmail account and its credentials"
753
- )
754
-
755
- send_interface = gr.Interface(
756
- fn=send_email,
757
- inputs=[
758
- gr.Textbox(label="Recipient Email", placeholder="recipient@example.com"),
759
- gr.Textbox(label="Subject", placeholder="Email subject"),
760
- gr.Textbox(label="Body", placeholder="Email body text", lines=5)
761
- ],
762
- outputs=gr.Textbox(label="Send Result", lines=10),
763
- title="✉️ Send Email",
764
- description="Send an email via Gmail using OAuth authenticated account"
765
- )
766
-
767
- # Combine interfaces into a tabbed interface
768
- demo = gr.TabbedInterface(
769
- [auth_interface, status_interface, accounts_interface, switch_interface, remove_interface, search_interface, details_interface, analysis_interface, send_interface],
770
- ["🔐 Authenticate", "📊 Status", "👥 All Accounts", "🔄 Switch Account", "🗑️ Remove Account", "📧 Email Search", "📄 Email Details", "📈 Pattern Analysis", "✉️ Send Email"],
771
- title="📧 Gmail Assistant MCP Server (Multi-Account OAuth)"
772
- )
773
-
774
- app = FastAPI()
775
- # Add your OAuth callback route
776
- @app.get("/oauth2callback")
777
- async def google_oauth_cb(request: Request):
778
- code = request.query_params.get("code")
779
- print("code:", code)
780
- return HTMLResponse(handle_oauth_callback(code))
781
-
782
- app = gr.mount_gradio_app(app, demo, path="/")
783
-
784
- if __name__ == "__main__":
785
- import uvicorn
786
- uvicorn.run(app, host="0.0.0.0", port=7860)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
agentic_implementation/gmail_api_scraper.py DELETED
@@ -1,301 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Gmail API-based Email Scraper with OAuth Authentication
4
- """
5
-
6
- import base64
7
- import re
8
- from datetime import datetime, timedelta
9
- from typing import List, Dict, Optional
10
- from email.mime.text import MIMEText
11
- import googleapiclient.errors
12
- from oauth_manager import oauth_manager
13
- from logger import logger
14
-
15
- class GmailAPIScraper:
16
- """Gmail API-based email scraper using OAuth authentication"""
17
-
18
- def __init__(self):
19
- """Initialize the Gmail API scraper"""
20
- self.oauth_manager = oauth_manager
21
-
22
- def _parse_date_string(self, date_str: str) -> datetime:
23
- """Parse date string in DD-MMM-YYYY format to datetime object"""
24
- try:
25
- return datetime.strptime(date_str, "%d-%b-%Y")
26
- except ValueError:
27
- raise ValueError(f"Invalid date format: {date_str}. Expected DD-MMM-YYYY")
28
-
29
- def _format_date_for_query(self, date_obj: datetime) -> str:
30
- """Format datetime object for Gmail API query"""
31
- return date_obj.strftime("%Y/%m/%d")
32
-
33
- def _decode_message_part(self, part: Dict) -> str:
34
- """Decode message part content"""
35
- data = part.get('body', {}).get('data', '')
36
- if data:
37
- # Decode base64url
38
- data += '=' * (4 - len(data) % 4) # Add padding if needed
39
- decoded_bytes = base64.urlsafe_b64decode(data)
40
- try:
41
- return decoded_bytes.decode('utf-8')
42
- except UnicodeDecodeError:
43
- return decoded_bytes.decode('utf-8', errors='ignore')
44
- return ''
45
-
46
- def _extract_email_content(self, message: Dict) -> str:
47
- """Extract readable content from Gmail API message"""
48
- content = ""
49
-
50
- if 'payload' not in message:
51
- return content
52
-
53
- payload = message['payload']
54
-
55
- # Handle multipart messages
56
- if 'parts' in payload:
57
- for part in payload['parts']:
58
- mime_type = part.get('mimeType', '')
59
-
60
- if mime_type == 'text/plain':
61
- content += self._decode_message_part(part)
62
- elif mime_type == 'text/html':
63
- html_content = self._decode_message_part(part)
64
- # Simple HTML tag removal
65
- clean_text = re.sub(r'<[^>]+>', '', html_content)
66
- content += clean_text
67
- elif mime_type.startswith('multipart/'):
68
- # Handle nested multipart
69
- if 'parts' in part:
70
- for nested_part in part['parts']:
71
- nested_mime = nested_part.get('mimeType', '')
72
- if nested_mime == 'text/plain':
73
- content += self._decode_message_part(nested_part)
74
- else:
75
- # Handle single part messages
76
- mime_type = payload.get('mimeType', '')
77
- if mime_type in ['text/plain', 'text/html']:
78
- raw_content = self._decode_message_part(payload)
79
- if mime_type == 'text/html':
80
- # Simple HTML tag removal
81
- content = re.sub(r'<[^>]+>', '', raw_content)
82
- else:
83
- content = raw_content
84
-
85
- return content.strip()
86
-
87
- def _get_header_value(self, headers: List[Dict], name: str) -> str:
88
- """Get header value by name"""
89
- for header in headers:
90
- if header.get('name', '').lower() == name.lower():
91
- return header.get('value', '')
92
- return ''
93
-
94
- def _parse_email_message(self, message: Dict) -> Dict:
95
- """Parse Gmail API message into structured format"""
96
- headers = message.get('payload', {}).get('headers', [])
97
-
98
- # Extract headers
99
- subject = self._get_header_value(headers, 'Subject') or 'No Subject'
100
- from_header = self._get_header_value(headers, 'From') or 'Unknown Sender'
101
- date_header = self._get_header_value(headers, 'Date')
102
- message_id = self._get_header_value(headers, 'Message-ID') or message.get('id', '')
103
-
104
- # Parse date
105
- email_date = datetime.now().strftime("%d-%b-%Y")
106
- email_time = "00:00:00"
107
-
108
- if date_header:
109
- try:
110
- # Parse RFC 2822 date format
111
- from email.utils import parsedate_to_datetime
112
- dt_obj = parsedate_to_datetime(date_header)
113
- # Convert to IST (Indian Standard Time)
114
- from zoneinfo import ZoneInfo
115
- ist_dt = dt_obj.astimezone(ZoneInfo("Asia/Kolkata"))
116
- email_date = ist_dt.strftime("%d-%b-%Y")
117
- email_time = ist_dt.strftime("%H:%M:%S")
118
- except Exception as e:
119
- logger.warning(f"Failed to parse date {date_header}: {e}")
120
-
121
- # Extract content
122
- content = self._extract_email_content(message)
123
-
124
- return {
125
- "date": email_date,
126
- "time": email_time,
127
- "subject": subject,
128
- "from": from_header,
129
- "content": content[:2000], # Limit content length
130
- "message_id": message_id,
131
- "gmail_id": message.get('id', '')
132
- }
133
-
134
- def search_emails(self, keyword: str, start_date: str, end_date: str) -> List[Dict]:
135
- """Search emails containing keyword within date range using Gmail API
136
-
137
- Args:
138
- keyword: Keyword to search for in emails
139
- start_date: Start date in DD-MMM-YYYY format
140
- end_date: End date in DD-MMM-YYYY format
141
-
142
- Returns:
143
- List of email dictionaries
144
- """
145
- logger.info(f"Searching emails containing '{keyword}' between {start_date} and {end_date}")
146
-
147
- # Get Gmail service
148
- service = self.oauth_manager.get_gmail_service()
149
- if not service:
150
- raise Exception("Not authenticated. Please authenticate first using the setup tool.")
151
-
152
- try:
153
- # Parse dates
154
- start_dt = self._parse_date_string(start_date)
155
- end_dt = self._parse_date_string(end_date)
156
-
157
- # Format dates for Gmail API query
158
- after_date = self._format_date_for_query(start_dt)
159
- before_date = self._format_date_for_query(end_dt + timedelta(days=1)) # Add 1 day for inclusive end
160
-
161
- # Build search query
162
- # Gmail API search syntax: https://developers.google.com/gmail/api/guides/filtering
163
- query_parts = [
164
- f'after:{after_date}',
165
- f'before:{before_date}',
166
- f'({keyword})' # Search in all fields
167
- ]
168
- query = ' '.join(query_parts)
169
-
170
- logger.info(f"Gmail API query: {query}")
171
-
172
- # Search for messages
173
- results = service.users().messages().list(
174
- userId='me',
175
- q=query,
176
- maxResults=500 # Limit to 500 results
177
- ).execute()
178
-
179
- messages = results.get('messages', [])
180
- logger.info(f"Found {len(messages)} messages")
181
-
182
- if not messages:
183
- return []
184
-
185
- # Fetch full message details
186
- scraped_emails = []
187
-
188
- for i, msg_ref in enumerate(messages):
189
- try:
190
- logger.info(f"Processing email {i+1}/{len(messages)}")
191
-
192
- # Get full message
193
- message = service.users().messages().get(
194
- userId='me',
195
- id=msg_ref['id'],
196
- format='full'
197
- ).execute()
198
-
199
- # Parse message
200
- parsed_email = self._parse_email_message(message)
201
-
202
- # Verify date range (double-check since Gmail search might be inclusive)
203
- email_dt = self._parse_date_string(parsed_email['date'])
204
- if start_dt <= email_dt <= end_dt:
205
- # Verify keyword presence (case-insensitive)
206
- keyword_lower = keyword.lower()
207
- if any(keyword_lower in text.lower() for text in [
208
- parsed_email['subject'],
209
- parsed_email['from'],
210
- parsed_email['content']
211
- ]):
212
- scraped_emails.append(parsed_email)
213
-
214
- except googleapiclient.errors.HttpError as e:
215
- logger.error(f"Error fetching message {msg_ref['id']}: {e}")
216
- continue
217
- except Exception as e:
218
- logger.error(f"Error processing message {msg_ref['id']}: {e}")
219
- continue
220
-
221
- # Sort by date (newest first)
222
- scraped_emails.sort(
223
- key=lambda x: datetime.strptime(f"{x['date']} {x['time']}", "%d-%b-%Y %H:%M:%S"),
224
- reverse=True
225
- )
226
-
227
- logger.info(f"Successfully processed {len(scraped_emails)} emails containing '{keyword}'")
228
- return scraped_emails
229
-
230
- except googleapiclient.errors.HttpError as e:
231
- logger.error(f"Gmail API error: {e}")
232
- raise Exception(f"Gmail API error: {e}")
233
- except Exception as e:
234
- logger.error(f"Email search failed: {e}")
235
- raise
236
-
237
- def get_email_by_id(self, message_id: str) -> Optional[Dict]:
238
- """Get email details by message ID or Gmail ID
239
-
240
- Args:
241
- message_id: Either the Message-ID header or Gmail message ID
242
-
243
- Returns:
244
- Email dictionary or None if not found
245
- """
246
- service = self.oauth_manager.get_gmail_service()
247
- if not service:
248
- raise Exception("Not authenticated. Please authenticate first using the setup tool.")
249
-
250
- try:
251
- # Try to get message directly by Gmail ID first
252
- try:
253
- message = service.users().messages().get(
254
- userId='me',
255
- id=message_id,
256
- format='full'
257
- ).execute()
258
- return self._parse_email_message(message)
259
- except googleapiclient.errors.HttpError:
260
- # If direct ID lookup fails, search by Message-ID header
261
- pass
262
-
263
- # Search by Message-ID header
264
- query = f'rfc822msgid:{message_id}'
265
- results = service.users().messages().list(
266
- userId='me',
267
- q=query,
268
- maxResults=1
269
- ).execute()
270
-
271
- messages = results.get('messages', [])
272
- if not messages:
273
- return None
274
-
275
- # Get the message
276
- message = service.users().messages().get(
277
- userId='me',
278
- id=messages[0]['id'],
279
- format='full'
280
- ).execute()
281
-
282
- return self._parse_email_message(message)
283
-
284
- except Exception as e:
285
- logger.error(f"Failed to get email {message_id}: {e}")
286
- return None
287
-
288
- def is_authenticated(self) -> bool:
289
- """Check if user is authenticated"""
290
- return self.oauth_manager.is_authenticated()
291
-
292
- def get_user_email(self) -> Optional[str]:
293
- """Get authenticated user's email address"""
294
- return self.oauth_manager.get_user_email()
295
-
296
- def authenticate(self) -> bool:
297
- """Trigger interactive authentication"""
298
- return self.oauth_manager.authenticate_interactive()
299
-
300
- # Global scraper instance
301
- gmail_scraper = GmailAPIScraper()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
agentic_implementation/logger.py DELETED
@@ -1,29 +0,0 @@
1
- import logging
2
- import sys
3
- from datetime import datetime
4
- from loguru import logger as _logger
5
- from pathlib import Path
6
- import sys
7
-
8
- _print_level = "INFO"
9
-
10
- PROJECT_ROOT = Path(sys.path[0]) # Assuming the script is run from the project root
11
-
12
- def define_log_level(print_level="INFO", logfile_level="DEBUG", name: str = None):
13
- """Adjust the log level to above level"""
14
- global _print_level
15
- _print_level = print_level
16
-
17
- current_date = datetime.now()
18
- formatted_date = current_date.strftime("%Y%m%d%H%M%S")
19
- log_name = (
20
- f"{name}_{formatted_date}" if name else formatted_date
21
- ) # name a log with prefix name
22
-
23
- _logger.remove()
24
- _logger.add(sys.stderr, level=print_level)
25
- _logger.add(PROJECT_ROOT / f"logs/{log_name}.log", level=logfile_level)
26
- return _logger
27
-
28
- # Module-level default logger
29
- logger = define_log_level()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
agentic_implementation/oauth_manager.py DELETED
@@ -1,631 +0,0 @@
1
- import os
2
- import json
3
- import pickle
4
- import base64
5
- from pathlib import Path
6
- from typing import Optional, Dict, Any
7
- from cryptography.fernet import Fernet
8
- import google.auth.transport.requests
9
- import google_auth_oauthlib.flow
10
- import googleapiclient.discovery
11
- from google.oauth2.credentials import Credentials
12
- from google.auth.transport.requests import Request
13
- import webbrowser
14
- import threading
15
- import time
16
- from http.server import HTTPServer, BaseHTTPRequestHandler
17
- from urllib.parse import urlparse,parse_qs
18
- from logger import logger
19
- from dotenv import load_dotenv
20
- load_dotenv()
21
-
22
-
23
- redirect_uri=os.getenv("GOOGLE_REDIRECT_URI")
24
-
25
- class OAuthCallbackHandler(BaseHTTPRequestHandler):
26
- """HTTP request handler for OAuth callback"""
27
-
28
- def do_GET(self):
29
- """Handle GET request (OAuth callback)"""
30
- # Parse the callback URL to extract authorization code
31
- parsed_path = urlparse.urlparse(self.path)
32
- query_params = urlparse.parse_qs(parsed_path.query)
33
-
34
- if 'code' in query_params:
35
- # Store the authorization code
36
- self.server.auth_code = query_params['code'][0]
37
-
38
- # Send success response
39
- self.send_response(200)
40
- self.send_header('Content-type', 'text/html')
41
- self.end_headers()
42
-
43
- success_html = """
44
- <html>
45
- <head><title>Authentication Successful</title></head>
46
- <body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;">
47
- <h1 style="color: #4CAF50;">✅ Authentication Successful!</h1>
48
- <p>You have successfully authenticated with Gmail.</p>
49
- <p>You can now close this window and return to Claude Desktop.</p>
50
- <script>
51
- setTimeout(function() {
52
- window.close();
53
- }, 3000);
54
- </script>
55
- </body>
56
- </html>
57
- """
58
- self.wfile.write(success_html.encode())
59
- else:
60
- # Send error response
61
- self.send_response(400)
62
- self.send_header('Content-type', 'text/html')
63
- self.end_headers()
64
-
65
- error_html = """
66
- <html>
67
- <head><title>Authentication Error</title></head>
68
- <body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;">
69
- <h1 style="color: #f44336;">❌ Authentication Failed</h1>
70
- <p>There was an error during authentication.</p>
71
- <p>Please try again.</p>
72
- </body>
73
- </html>
74
- """
75
- self.wfile.write(error_html.encode())
76
-
77
- def log_message(self, format, *args):
78
- """Suppress server log messages"""
79
- pass
80
-
81
- class GmailOAuthManager:
82
- """Manages Gmail OAuth 2.0 authentication and token storage for multiple accounts"""
83
-
84
- # Gmail API scopes
85
- SCOPES = [
86
- 'https://www.googleapis.com/auth/gmail.readonly',
87
- 'https://www.googleapis.com/auth/gmail.modify'
88
- ]
89
-
90
- def __init__(self, credentials_dir: str = None):
91
- """Initialize OAuth manager
92
-
93
- Args:
94
- credentials_dir: Directory to store credentials (defaults to ~/.mailquery_oauth)
95
- """
96
- if credentials_dir is None:
97
- credentials_dir = os.path.expanduser("~/.mailquery_oauth")
98
-
99
- self.credentials_dir = Path(credentials_dir)
100
- self.credentials_dir.mkdir(exist_ok=True)
101
-
102
- # File paths
103
- self.client_secrets_file = self.credentials_dir / "client_secret.json"
104
- self.accounts_file = self.credentials_dir / "accounts.json"
105
- self.encryption_key_file = self.credentials_dir / "key.key"
106
- self.current_account_file = self.credentials_dir / "current_account.txt"
107
-
108
- # Initialize encryption
109
- self._init_encryption()
110
-
111
- # OAuth flow settings
112
- self.redirect_uri = redirect_uri
113
-
114
- # Current account
115
- self.current_account_email = self._load_current_account()
116
-
117
- def _init_encryption(self):
118
- """Initialize encryption for secure credential storage"""
119
- if self.encryption_key_file.exists():
120
- with open(self.encryption_key_file, 'rb') as key_file:
121
- self.encryption_key = key_file.read()
122
- else:
123
- self.encryption_key = Fernet.generate_key()
124
- with open(self.encryption_key_file, 'wb') as key_file:
125
- key_file.write(self.encryption_key)
126
- # Make key file readable only by owner
127
- os.chmod(self.encryption_key_file, 0o600)
128
-
129
- self.cipher_suite = Fernet(self.encryption_key)
130
-
131
- def _load_current_account(self) -> Optional[str]:
132
- """Load the currently selected account"""
133
- if self.current_account_file.exists():
134
- try:
135
- with open(self.current_account_file, 'r') as f:
136
- return f.read().strip()
137
- except Exception as e:
138
- logger.error(f"Failed to load current account: {e}")
139
- return None
140
-
141
- def _save_current_account(self, email: str):
142
- """Save the currently selected account"""
143
- try:
144
- with open(self.current_account_file, 'w') as f:
145
- f.write(email)
146
- self.current_account_email = email
147
- logger.info(f"Set current account to: {email}")
148
- except Exception as e:
149
- logger.error(f"Failed to save current account: {e}")
150
-
151
- def setup_client_secrets(self, client_id: str, client_secret: str):
152
- """Setup OAuth client secrets
153
-
154
- Args:
155
- client_id: Google OAuth 2.0 client ID
156
- client_secret: Google OAuth 2.0 client secret
157
- """
158
- client_config = {
159
- "web": {
160
- "client_id": client_id,
161
- "client_secret": client_secret,
162
- "auth_uri": "https://accounts.google.com/o/oauth2/auth",
163
- "token_uri": "https://oauth2.googleapis.com/token",
164
- "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
165
- "redirect_uris": [self.redirect_uri]
166
- }
167
- }
168
-
169
- with open(self.client_secrets_file, 'w') as f:
170
- json.dump(client_config, f, indent=2)
171
-
172
- logger.info("Client secrets saved successfully")
173
-
174
- def _encrypt_data(self, data: Any) -> bytes:
175
- """Encrypt data using Fernet encryption"""
176
- serialized_data = pickle.dumps(data)
177
- return self.cipher_suite.encrypt(serialized_data)
178
-
179
- def _decrypt_data(self, encrypted_data: bytes) -> Any:
180
- """Decrypt data using Fernet encryption"""
181
- decrypted_data = self.cipher_suite.decrypt(encrypted_data)
182
- return pickle.loads(decrypted_data)
183
-
184
- def get_authorization_url(self) -> str:
185
- """Get the authorization URL for OAuth flow
186
-
187
- Returns:
188
- Authorization URL that user should visit
189
- """
190
- if not self.client_secrets_file.exists():
191
- raise ValueError("Client secrets not found. Please run setup first.")
192
-
193
- flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file(
194
- str(self.client_secrets_file),
195
- scopes=self.SCOPES
196
- )
197
- flow.redirect_uri = self.redirect_uri
198
- print("👉 redirect_uri being sent to Google:", self.redirect_uri, flush=True)
199
- auth_url, _ = flow.authorization_url(
200
- access_type='offline',
201
- include_granted_scopes='true',
202
- prompt='consent' # Force consent to get refresh token
203
- )
204
-
205
- return auth_url
206
-
207
- def authenticate_interactive(self) -> bool:
208
- """Interactive authentication flow for Hugging Face Spaces
209
-
210
- Returns:
211
- True if authentication successful, False otherwise
212
- """
213
- try:
214
- # Check if already authenticated
215
- if self.is_authenticated():
216
- logger.info("Already authenticated")
217
- return True
218
-
219
-
220
- # Get authorization URL
221
- auth_url = self.get_authorization_url()
222
-
223
- logger.info("Running on Hugging Face Spaces")
224
- logger.info(f"Authentication URL generated: {auth_url}")
225
- logger.info("User must visit the URL manually to complete authentication")
226
-
227
- # Store the auth URL for the Gradio interface to use
228
- self._pending_auth_url = auth_url
229
- self._auth_completed = False
230
-
231
- # For setup_oauth.py and testing contexts, we'll print the URL
232
- # and wait briefly to see if authentication completes
233
- print(f"\n🌐 Please visit this URL to authenticate:")
234
- print(f" {auth_url}")
235
- print("\n⏳ Waiting for authentication completion...")
236
-
237
- # Wait for a reasonable time to see if auth completes
238
- # This allows the callback to potentially complete the auth
239
- timeout = 10 # 1 minute for manual completion
240
- start_time = time.time()
241
-
242
- while (time.time() - start_time) < timeout:
243
- # Check if authentication was completed via callback
244
- if getattr(self, '_auth_completed', False):
245
- logger.info("Authentication completed successfully!")
246
- return True
247
-
248
- # Check if user is now authenticated (credentials were saved)
249
- if self.is_authenticated():
250
- self._auth_completed = True
251
- logger.info("Authentication verified successful!")
252
- return True
253
-
254
- time.sleep(2) # Check every 2 seconds
255
-
256
- # Timeout reached - authentication not completed
257
- logger.info("Authentication timeout. Please complete authentication via the provided URL.")
258
- return False
259
-
260
- except Exception as e:
261
- logger.error(f"Authentication failed: {e}")
262
- return False
263
-
264
- def complete_hf_spaces_auth(self, auth_code: str) -> bool:
265
- """Complete authentication for HF Spaces with received auth code
266
-
267
- Args:
268
- auth_code: Authorization code received from OAuth callback
269
-
270
- Returns:
271
- True if authentication successful, False otherwise
272
- """
273
- try:
274
- success = self._exchange_code_for_credentials(auth_code)
275
-
276
- if success:
277
- # Mark authentication as completed
278
- self._auth_completed = True
279
- logger.info("HF Spaces authentication marked as completed")
280
-
281
- return success
282
-
283
- except Exception as e:
284
- logger.error(f"Failed to complete HF Spaces authentication: {e}")
285
- return False
286
-
287
- def _exchange_code_for_credentials(self, auth_code: str) -> bool:
288
- """Exchange authorization code for credentials
289
-
290
- Args:
291
- auth_code: Authorization code from OAuth flow
292
-
293
- Returns:
294
- True if successful, False otherwise
295
- """
296
- try:
297
- # Exchange authorization code for credentials
298
- flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file(
299
- str(self.client_secrets_file),
300
- scopes=self.SCOPES
301
- )
302
- flow.redirect_uri = self.redirect_uri
303
-
304
- flow.fetch_token(code=auth_code)
305
- credentials = flow.credentials
306
-
307
- # Get user email from credentials
308
- user_email = self._get_email_from_credentials(credentials)
309
- if not user_email:
310
- logger.error("Failed to get user email from credentials")
311
- return False
312
-
313
- # Save encrypted credentials for this account
314
- self._save_credentials(user_email, credentials)
315
-
316
- # Set as current account
317
- self._save_current_account(user_email)
318
-
319
- logger.info("Authentication successful!")
320
- return True
321
-
322
- except Exception as e:
323
- logger.error(f"Failed to exchange code for credentials: {e}")
324
- return False
325
-
326
- def get_pending_auth_url(self) -> str:
327
- """Get the pending authentication URL for manual completion
328
-
329
- Returns:
330
- Authentication URL string or None if not available
331
- """
332
- return getattr(self, '_pending_auth_url', None)
333
-
334
- def get_hf_redirect_uri(self) -> str:
335
- """Get the Hugging Face Spaces redirect URI
336
-
337
- Returns:
338
- Redirect URI string
339
- # """
340
- # space_id = os.getenv('SPACE_ID')
341
- # space_author = os.getenv('SPACE_AUTHOR', 'username')
342
- return redirect_uri
343
-
344
- # For running in a local machine, use this method instead
345
-
346
- # def authenticate_interactive(self) -> bool:
347
- # """Interactive authentication flow that opens browser
348
-
349
- # Returns:
350
- # True if authentication successful, False otherwise
351
- # """
352
- # try:
353
- # # Start local HTTP server for OAuth callback
354
- # server = HTTPServer(('localhost', 8080), OAuthCallbackHandler)
355
- # server.auth_code = None
356
-
357
- # # Get authorization URL
358
- # auth_url = self.get_authorization_url()
359
-
360
- # logger.info("Opening browser for authentication...")
361
- # logger.info(f"If browser doesn't open, visit: {auth_url}")
362
-
363
- # # Open browser
364
- # webbrowser.open(auth_url)
365
-
366
- # # Start server in background thread
367
- # server_thread = threading.Thread(target=server.handle_request)
368
- # server_thread.daemon = True
369
- # server_thread.start()
370
-
371
- # # Wait for callback (max 5 minutes)
372
- # timeout = 300 # 5 minutes
373
- # start_time = time.time()
374
-
375
- # while server.auth_code is None and (time.time() - start_time) < timeout:
376
- # time.sleep(1)
377
-
378
- # if server.auth_code is None:
379
- # logger.error("Authentication timed out")
380
- # return False
381
-
382
- # # Exchange authorization code for credentials
383
- # flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file(
384
- # str(self.client_secrets_file),
385
- # scopes=self.SCOPES
386
- # )
387
- # flow.redirect_uri = self.redirect_uri
388
-
389
- # flow.fetch_token(code=server.auth_code)
390
- # credentials = flow.credentials
391
-
392
- # # Get user email from credentials
393
- # user_email = self._get_email_from_credentials(credentials)
394
- # if not user_email:
395
- # logger.error("Failed to get user email from credentials")
396
- # return False
397
-
398
- # # Save encrypted credentials for this account
399
- # self._save_credentials(user_email, credentials)
400
-
401
- # # Set as current account
402
- # self._save_current_account(user_email)
403
-
404
- # logger.info("Authentication successful!")
405
- # return True
406
-
407
- # except Exception as e:
408
- # logger.error(f"Authentication failed: {e}")
409
- # return False
410
-
411
- def _get_email_from_credentials(self, credentials: Credentials) -> Optional[str]:
412
- """Get email address from credentials"""
413
- try:
414
- service = googleapiclient.discovery.build(
415
- 'gmail', 'v1', credentials=credentials
416
- )
417
- profile = service.users().getProfile(userId='me').execute()
418
- return profile.get('emailAddress')
419
- except Exception as e:
420
- logger.error(f"Failed to get email from credentials: {e}")
421
- return None
422
-
423
- def _save_credentials(self, email: str, credentials: Credentials):
424
- """Save encrypted credentials for a specific account"""
425
- try:
426
- # Load existing accounts
427
- accounts = self._load_accounts()
428
-
429
- # Encrypt and store credentials
430
- encrypted_credentials = self._encrypt_data(credentials)
431
- accounts[email] = base64.b64encode(encrypted_credentials).decode('utf-8')
432
-
433
- # Save accounts file
434
- with open(self.accounts_file, 'w') as f:
435
- json.dump(accounts, f, indent=2)
436
-
437
- # Make accounts file readable only by owner
438
- os.chmod(self.accounts_file, 0o600)
439
-
440
- logger.info(f"Credentials saved for account: {email}")
441
- except Exception as e:
442
- logger.error(f"Failed to save credentials for {email}: {e}")
443
- raise
444
-
445
- def _load_accounts(self) -> Dict[str, str]:
446
- """Load accounts data"""
447
- if not self.accounts_file.exists():
448
- return {}
449
-
450
- try:
451
- with open(self.accounts_file, 'r') as f:
452
- return json.load(f)
453
- except Exception as e:
454
- logger.error(f"Failed to load accounts: {e}")
455
- return {}
456
-
457
- def _load_credentials(self, email: str) -> Optional[Credentials]:
458
- """Load and decrypt credentials for a specific account"""
459
- accounts = self._load_accounts()
460
-
461
- if email not in accounts:
462
- return None
463
-
464
- try:
465
- encrypted_credentials = base64.b64decode(accounts[email])
466
- credentials = self._decrypt_data(encrypted_credentials)
467
- return credentials
468
- except Exception as e:
469
- logger.error(f"Failed to load credentials for {email}: {e}")
470
- return None
471
-
472
- def get_valid_credentials(self, email: str = None) -> Optional[Credentials]:
473
- """Get valid credentials for an account, refreshing if necessary
474
-
475
- Args:
476
- email: Email address of account (uses current account if None)
477
-
478
- Returns:
479
- Valid Credentials object or None if authentication required
480
- """
481
- if email is None:
482
- email = self.current_account_email
483
-
484
- if not email:
485
- logger.warning("No current account set")
486
- return None
487
-
488
- credentials = self._load_credentials(email)
489
-
490
- if not credentials:
491
- logger.warning(f"No stored credentials found for {email}")
492
- return None
493
-
494
- # Refresh if expired
495
- if credentials.expired and credentials.refresh_token:
496
- try:
497
- logger.info(f"Refreshing expired credentials for {email}...")
498
- credentials.refresh(Request())
499
- self._save_credentials(email, credentials)
500
- logger.info("Credentials refreshed successfully")
501
- except Exception as e:
502
- logger.error(f"Failed to refresh credentials for {email}: {e}")
503
- return None
504
-
505
- if not credentials.valid:
506
- logger.warning(f"Credentials are not valid for {email}")
507
- return None
508
-
509
- return credentials
510
-
511
- def is_authenticated(self, email: str = None) -> bool:
512
- """Check if user is authenticated
513
-
514
- Args:
515
- email: Email address to check (uses current account if None)
516
-
517
- Returns:
518
- True if valid credentials exist, False otherwise
519
- """
520
- return self.get_valid_credentials(email) is not None
521
-
522
- def switch_account(self, email: str) -> bool:
523
- """Switch to a different authenticated account
524
-
525
- Args:
526
- email: Email address to switch to
527
-
528
- Returns:
529
- True if switch successful, False if account not found or not authenticated
530
- """
531
- if self.is_authenticated(email):
532
- self._save_current_account(email)
533
- logger.info(f"Switched to account: {email}")
534
- return True
535
- else:
536
- logger.error(f"Account {email} is not authenticated")
537
- return False
538
-
539
- def list_accounts(self) -> Dict[str, bool]:
540
- """List all stored accounts and their authentication status
541
-
542
- Returns:
543
- Dictionary mapping email addresses to authentication status
544
- """
545
- accounts = self._load_accounts()
546
- result = {}
547
-
548
- for email in accounts.keys():
549
- result[email] = self.is_authenticated(email)
550
-
551
- return result
552
-
553
- def remove_account(self, email: str):
554
- """Remove an account and its credentials
555
-
556
- Args:
557
- email: Email address to remove
558
- """
559
- accounts = self._load_accounts()
560
-
561
- if email in accounts:
562
- del accounts[email]
563
-
564
- # Save updated accounts
565
- with open(self.accounts_file, 'w') as f:
566
- json.dump(accounts, f, indent=2)
567
-
568
- # If this was the current account, clear it
569
- if self.current_account_email == email:
570
- if self.current_account_file.exists():
571
- self.current_account_file.unlink()
572
- self.current_account_email = None
573
-
574
- logger.info(f"Removed account: {email}")
575
- else:
576
- logger.warning(f"Account {email} not found")
577
-
578
- def clear_credentials(self):
579
- """Clear all stored credentials"""
580
- if self.accounts_file.exists():
581
- self.accounts_file.unlink()
582
- if self.current_account_file.exists():
583
- self.current_account_file.unlink()
584
- self.current_account_email = None
585
- logger.info("All credentials cleared")
586
-
587
- def get_gmail_service(self, email: str = None):
588
- """Get authenticated Gmail service object
589
-
590
- Args:
591
- email: Email address (uses current account if None)
592
-
593
- Returns:
594
- Gmail service object or None if not authenticated
595
- """
596
- credentials = self.get_valid_credentials(email)
597
- if not credentials:
598
- return None
599
-
600
- try:
601
- service = googleapiclient.discovery.build(
602
- 'gmail', 'v1', credentials=credentials
603
- )
604
- return service
605
- except Exception as e:
606
- logger.error(f"Failed to build Gmail service: {e}")
607
- return None
608
-
609
- def get_user_email(self, email: str = None) -> Optional[str]:
610
- """Get the authenticated user's email address
611
-
612
- Args:
613
- email: Email address (uses current account if None)
614
-
615
- Returns:
616
- User's email address or None if not authenticated
617
- """
618
- if email is None:
619
- return self.current_account_email
620
- return email if self.is_authenticated(email) else None
621
-
622
- def get_current_account(self) -> Optional[str]:
623
- """Get the currently selected account
624
-
625
- Returns:
626
- Current account email or None if no account selected
627
- """
628
- return self.current_account_email
629
-
630
- # Global OAuth manager instance
631
- oauth_manager = GmailOAuthManager(credentials_dir="secure_data")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
agentic_implementation/setup_oauth.py DELETED
@@ -1,263 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- OAuth Setup Utility for Gmail MCP Server
4
-
5
- This script helps users set up OAuth authentication for the Gmail MCP server.
6
- """
7
-
8
- import sys
9
- import os
10
- import json
11
- from pathlib import Path
12
- from oauth_manager import oauth_manager
13
- from logger import logger
14
- from dotenv import load_dotenv
15
- load_dotenv()
16
- import os
17
-
18
-
19
- def print_banner():
20
- """Print setup banner"""
21
- print("=" * 60)
22
- print("📧 Gmail MCP Server - OAuth Setup")
23
- print("=" * 60)
24
- print()
25
-
26
- def print_step(step_num: int, title: str):
27
- """Print step header"""
28
- print(f"\n🔹 Step {step_num}: {title}")
29
- print("-" * 50)
30
-
31
- def check_dependencies():
32
- """Check if required dependencies are installed"""
33
- try:
34
- import google.auth
35
- import google_auth_oauthlib
36
- import googleapiclient
37
- import cryptography
38
- print("✅ All required dependencies are installed")
39
- return True
40
- except ImportError as e:
41
- print(f"❌ Missing dependency: {e}")
42
- print("\nPlease install the required dependencies:")
43
- print("pip install google-auth google-auth-oauthlib google-api-python-client cryptography")
44
- return False
45
-
46
- def setup_google_cloud_project():
47
- """Guide user through Google Cloud project setup"""
48
- print_step(1, "Google Cloud Project Setup")
49
-
50
- print("You need to create a Google Cloud project and enable the Gmail API.")
51
- print("\n📋 Follow these steps:")
52
- print("1. Go to: https://console.cloud.google.com/")
53
- print("2. Create a new project or select an existing one")
54
- print("3. Enable the Gmail API:")
55
- print(" - Go to 'APIs & Services' > 'Library'")
56
- print(" - Search for 'Gmail API'")
57
- print(" - Click 'Enable'")
58
-
59
- input("\n✅ Press Enter when you've completed these steps...")
60
-
61
- def setup_oauth_consent():
62
- """Guide user through OAuth consent screen setup"""
63
- print_step(2, "OAuth Consent Screen Setup")
64
-
65
- print("Now you need to configure the OAuth consent screen.")
66
- print("\n📋 Follow these steps:")
67
- print("1. Go to: https://console.cloud.google.com/apis/credentials/consent")
68
- print("2. Choose 'External' user type (unless using Google Workspace)")
69
- print("3. Fill in the app information:")
70
- print(" - App name: 'Gmail MCP Server' (or your preferred name)")
71
- print(" - User support email: Your email address")
72
- print(" - Developer contact: Your email address")
73
- print("4. Add these scopes:")
74
- print(" - https://www.googleapis.com/auth/gmail.readonly")
75
- print(" - https://www.googleapis.com/auth/gmail.modify")
76
- print("5. Add your email as a test user")
77
- print("6. Complete the setup")
78
-
79
- input("\n✅ Press Enter when you've completed these steps...")
80
-
81
- def setup_oauth_credentials():
82
- """Guide user through OAuth credentials setup"""
83
- print_step(3, "OAuth Client Credentials Setup")
84
-
85
- client_id = os.getenv("GOOGLE_CLIENT_ID")
86
- client_secret = os.getenv("GOOGLE_CLIENT_SECRET")
87
-
88
- if not client_id or not client_secret:
89
- print("❌ Missing GOOGLE_CLIENT_ID or GOOGLE_CLIENT_SECRET in your .env")
90
- print(" Please add:")
91
- print(" GOOGLE_CLIENT_ID=your-client-id")
92
- print(" GOOGLE_CLIENT_SECRET=your-client-secret")
93
- return False
94
-
95
- try:
96
- oauth_manager.setup_client_secrets(client_id, client_secret)
97
- print("✅ OAuth credentials saved successfully")
98
- return True
99
- except Exception as e:
100
- print(f"❌ Failed to save credentials: {e}")
101
- return False
102
-
103
- def test_authentication():
104
- """Test the OAuth authentication flow"""
105
- print_step(4, "Authentication Test")
106
-
107
- print("Now let's test the authentication flow.")
108
- print("This will open your web browser for authentication.")
109
-
110
- confirm = input("\n🌐 Ready to open browser for authentication? (y/n): ").strip().lower()
111
- if confirm != 'y':
112
- print("Authentication test skipped.")
113
- return False
114
-
115
- try:
116
- print("\n🔄 Starting authentication flow...")
117
- success = oauth_manager.authenticate_interactive()
118
-
119
- if success:
120
- print("✅ Authentication successful!")
121
-
122
- # Test getting user info
123
- user_email = oauth_manager.get_user_email()
124
- if user_email:
125
- print(f"✅ Authenticated as: {user_email}")
126
-
127
- return True
128
- else:
129
- print("❌ Authentication failed")
130
- return False
131
-
132
- except Exception as e:
133
- print(f"❌ Authentication error: {e}")
134
- return False
135
-
136
- def show_completion_info():
137
- """Show completion information and next steps"""
138
- print("\n" + "=" * 60)
139
- print("🎉 Setup Complete!")
140
- print("=" * 60)
141
-
142
- print("\n✅ Your Gmail MCP server is now configured with OAuth authentication!")
143
- print("\n📝 Next steps:")
144
- print("1. Start the MCP server:")
145
- print(" python email_mcp_server_oauth.py")
146
- print("\n2. Configure Claude Desktop:")
147
- print(' Add this to your MCP configuration:')
148
- print(' {')
149
- print(' "mcpServers": {')
150
- print(' "gmail-oauth": {')
151
- print(' "command": "npx",')
152
- print(' "args": ["mcp-remote", "http://localhost:7860/gradio_api/mcp/sse"]')
153
- print(' }')
154
- print(' }')
155
- print(' }')
156
-
157
- print("\n🔐 Security notes:")
158
- print("- Your credentials are encrypted and stored locally")
159
- print("- Tokens are automatically refreshed when needed")
160
- print("- You can revoke access anytime from Google Account settings")
161
-
162
- credentials_dir = oauth_manager.credentials_dir
163
- print(f"\n📁 Credentials stored in: {credentials_dir}")
164
-
165
- def show_help():
166
- """Show help information"""
167
- print("Gmail MCP Server OAuth Setup")
168
- print("\nUsage:")
169
- print(" python setup_oauth.py # Full interactive setup")
170
- print(" python setup_oauth.py --help # Show this help")
171
- print(" python setup_oauth.py --auth # Re-authenticate only")
172
- print(" python setup_oauth.py --status # Check authentication status")
173
- print(" python setup_oauth.py --clear # Clear stored credentials")
174
-
175
- def check_status():
176
- """Check authentication status"""
177
- print("🔍 Checking authentication status...")
178
-
179
- if oauth_manager.is_authenticated():
180
- user_email = oauth_manager.get_user_email()
181
- print(f"✅ Authenticated as: {user_email}")
182
- return True
183
- else:
184
- print("❌ Not authenticated")
185
- return False
186
-
187
- def clear_credentials():
188
- """Clear stored credentials"""
189
- confirm = input("⚠️ This will clear all stored credentials. Continue? (y/n): ").strip().lower()
190
- if confirm == 'y':
191
- oauth_manager.clear_credentials()
192
- print("✅ Credentials cleared")
193
- else:
194
- print("Operation cancelled")
195
-
196
- def main():
197
- """Main setup function"""
198
- if len(sys.argv) > 1:
199
- arg = sys.argv[1].lower()
200
-
201
- if arg in ['--help', '-h', 'help']:
202
- show_help()
203
- return
204
- elif arg == '--status':
205
- check_status()
206
- return
207
- elif arg == '--auth':
208
- print("🔄 Starting re-authentication...")
209
- if test_authentication():
210
- print("✅ Re-authentication successful")
211
- else:
212
- print("❌ Re-authentication failed")
213
- return
214
- elif arg == '--clear':
215
- clear_credentials()
216
- return
217
- else:
218
- print(f"Unknown argument: {arg}")
219
- show_help()
220
- return
221
-
222
- # Full interactive setup
223
- print_banner()
224
-
225
- # Check if already authenticated
226
- if oauth_manager.is_authenticated():
227
- user_email = oauth_manager.get_user_email()
228
- print(f"✅ Already authenticated as: {user_email}")
229
-
230
- choice = input("\n🔄 Do you want to re-authenticate? (y/n): ").strip().lower()
231
- if choice == 'y':
232
- if test_authentication():
233
- show_completion_info()
234
- else:
235
- print("Setup complete - you're already authenticated!")
236
- return
237
-
238
- # Check dependencies
239
- if not check_dependencies():
240
- return
241
-
242
- # Full setup flow
243
- try:
244
- setup_google_cloud_project()
245
- setup_oauth_consent()
246
-
247
- if not setup_oauth_credentials():
248
- print("❌ Setup failed at credentials step")
249
- return
250
-
251
- if test_authentication():
252
- show_completion_info()
253
- else:
254
- print("❌ Setup completed but authentication test failed")
255
- print("You can try authentication later with: python setup_oauth.py --auth")
256
-
257
- except KeyboardInterrupt:
258
- print("\n\n⚠️ Setup interrupted by user")
259
- except Exception as e:
260
- print(f"\n❌ Setup failed: {e}")
261
-
262
- if __name__ == "__main__":
263
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from huggingface_hub import InferenceClient
3
+
4
+ """
5
+ For more information on `huggingface_hub` Inference API support, please check the docs: https://huggingface.co/docs/huggingface_hub/v0.22.2/en/guides/inference
6
+ """
7
+ client = InferenceClient("HuggingFaceH4/zephyr-7b-beta")
8
+
9
+
10
+ def respond(
11
+ message,
12
+ history: list[tuple[str, str]],
13
+ system_message,
14
+ max_tokens,
15
+ temperature,
16
+ top_p,
17
+ ):
18
+ messages = [{"role": "system", "content": system_message}]
19
+
20
+ for val in history:
21
+ if val[0]:
22
+ messages.append({"role": "user", "content": val[0]})
23
+ if val[1]:
24
+ messages.append({"role": "assistant", "content": val[1]})
25
+
26
+ messages.append({"role": "user", "content": message})
27
+
28
+ response = ""
29
+
30
+ for message in client.chat_completion(
31
+ messages,
32
+ max_tokens=max_tokens,
33
+ stream=True,
34
+ temperature=temperature,
35
+ top_p=top_p,
36
+ ):
37
+ token = message.choices[0].delta.content
38
+
39
+ response += token
40
+ yield response
41
+
42
+
43
+ """
44
+ For information on how to customize the ChatInterface, peruse the gradio docs: https://www.gradio.app/docs/chatinterface
45
+ """
46
+ demo = gr.ChatInterface(
47
+ respond,
48
+ additional_inputs=[
49
+ gr.Textbox(value="You are a friendly Chatbot.", label="System message"),
50
+ gr.Slider(minimum=1, maximum=2048, value=512, step=1, label="Max new tokens"),
51
+ gr.Slider(minimum=0.1, maximum=4.0, value=0.7, step=0.1, label="Temperature"),
52
+ gr.Slider(
53
+ minimum=0.1,
54
+ maximum=1.0,
55
+ value=0.95,
56
+ step=0.05,
57
+ label="Top-p (nucleus sampling)",
58
+ ),
59
+ ],
60
+ )
61
+
62
+
63
+ if __name__ == "__main__":
64
+ demo.launch()
mcp/client/host.py ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ CLI host for the FastAPI-powered Swiggy server.
4
+
5
+ Flow
6
+ 1. Send natural-language query → /parse_query
7
+ 2. If is_swiggy_query == True and dates are present → /get_orders
8
+ 3. Pretty-print the result
9
+ """
10
+
11
+ import requests, sys
12
+ from dateutil import parser as dtparse
13
+
14
+ API_BASE = "http://127.0.0.1:8000"
15
+
16
+ # ----------------------------------------------------------------------
17
+ # Helpers
18
+ # ----------------------------------------------------------------------
19
+ def nice_date(dt_str: str | None) -> str:
20
+ if not dt_str:
21
+ return "??"
22
+ return dtparse.parse(dt_str).strftime("%d %b %Y")
23
+
24
+ def pretty_order(order: dict) -> str:
25
+ if "error" in order:
26
+ return f" - Email #{order['email_number']}: ❌ {order['error']}"
27
+ head = (
28
+ f"Restaurant : {order['restaurant_name']}\n"
29
+ f"Date : {nice_date(order['order_date'])} {order['order_time']}\n"
30
+ f"Total : ₹{order['total_price']:.0f}\n"
31
+ "Items:"
32
+ )
33
+ items = "\n".join(
34
+ f" • {it['quantity']} × {it['name']} – ₹{it['price']:.0f}"
35
+ for it in order["items"]
36
+ )
37
+ return head + "\n" + items
38
+
39
+ # ----------------------------------------------------------------------
40
+ # Main REPL
41
+ # ----------------------------------------------------------------------
42
+ def main() -> None:
43
+ # Quick health-check
44
+ try:
45
+ requests.get(f"{API_BASE}/docs").raise_for_status()
46
+ except Exception as e:
47
+ print("❌ Cannot reach FastAPI server:", e)
48
+ sys.exit(1)
49
+ print("✅ Connected. Type a Swiggy question (or Ctrl-C to quit).")
50
+
51
+ while True:
52
+ try:
53
+ query = input("\n🗨️ You: ").strip()
54
+ except (EOFError, KeyboardInterrupt):
55
+ break
56
+ if not query:
57
+ continue
58
+
59
+ # ── Stage 1: parse_query ────────────────────────────────────
60
+ try:
61
+ r = requests.post(f"{API_BASE}/parse_query", json={"query": query})
62
+ r.raise_for_status()
63
+ meta = r.json() # {is_swiggy_query, start_date, end_date, intent}
64
+ except Exception as e:
65
+ print("⚠️ Parse error:", e)
66
+ print("🔎 Server:", r.text if 'r' in locals() else "no response")
67
+ continue
68
+
69
+ # Handle non-Swiggy or missing dates
70
+ if not meta.get("is_swiggy_query"):
71
+ print("🤷 That doesn’t look like a Swiggy order query.")
72
+ continue
73
+ if not meta.get("start_date") or not meta.get("end_date"):
74
+ print("⚠️ Couldn’t find a full date range in your question.")
75
+ continue
76
+
77
+ print(f" ↳ Date range: {meta['start_date']} → {meta['end_date']}")
78
+ print(f" ↳ Intent : {meta['intent']}")
79
+
80
+ # ── Stage 2: get_orders ────────────────────────────────────
81
+ try:
82
+ r2 = requests.post(
83
+ f"{API_BASE}/get_orders",
84
+ json={
85
+ "start_date": meta["start_date"],
86
+ "end_date": meta["end_date"],
87
+ },
88
+ )
89
+ r2.raise_for_status()
90
+ orders = r2.json()
91
+ except Exception as e:
92
+ print("⚠️ Failed to fetch orders:", e)
93
+ print("🔎 Server:", r2.text if 'r2' in locals() else "no response")
94
+ continue
95
+
96
+ # ── Output ────────────────────────────────────────────────
97
+ if not orders:
98
+ print("😕 No orders found for that range.")
99
+ continue
100
+
101
+ print("\n📦 Orders:")
102
+ for o in orders:
103
+ print(pretty_order(o))
104
+ print("-" * 40)
105
+
106
+ print("\n👋 Bye!")
107
+
108
+ # ----------------------------------------------------------------------
109
+ if __name__ == "__main__":
110
+ main()
mcp/server/db_schema.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # File: db_schema.py
2
+ import sqlite3
3
+
4
+ DB_NAME = "swiggy_orders.db"
5
+
6
+
7
+ def init_db(db_path: str = DB_NAME) -> None:
8
+ """
9
+ Initialize the SQLite database with the necessary tables.
10
+ """
11
+ conn = sqlite3.connect(db_path)
12
+ c = conn.cursor()
13
+ # Orders metadata
14
+ c.execute(
15
+ """
16
+ CREATE TABLE IF NOT EXISTS orders (
17
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
18
+ email_number INTEGER,
19
+ order_date TEXT,
20
+ order_time TEXT,
21
+ restaurant_name TEXT,
22
+ delivery_address TEXT,
23
+ total_price REAL
24
+ )
25
+ """
26
+ )
27
+ # Individual items per order
28
+ c.execute(
29
+ """
30
+ CREATE TABLE IF NOT EXISTS order_items (
31
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
32
+ order_id INTEGER,
33
+ item_name TEXT,
34
+ quantity INTEGER,
35
+ price REAL,
36
+ FOREIGN KEY(order_id) REFERENCES orders(id)
37
+ )
38
+ """
39
+ )
40
+ conn.commit()
41
+ conn.close()
42
+
43
+
44
+ def get_db_connection(db_path: str = DB_NAME) -> sqlite3.Connection:
45
+ """Return a new connection to the database."""
46
+ return sqlite3.connect(db_path)
47
+
48
+
49
+ def get_orders_by_date_from_db(date_str: str) -> list[dict]:
50
+ """
51
+ Fetch all orders and their items for a given date from the database.
52
+ """
53
+ conn = get_db_connection()
54
+ c = conn.cursor()
55
+ c.execute(
56
+ "SELECT id, email_number, order_time, restaurant_name, delivery_address, total_price"
57
+ " FROM orders WHERE order_date = ?",
58
+ (date_str,)
59
+ )
60
+ orders = []
61
+ for order_id, email_number, order_time, restaurant_name, delivery_address, total_price in c.fetchall():
62
+ # fetch items for this order
63
+ c.execute(
64
+ "SELECT item_name, quantity, price FROM order_items WHERE order_id = ?",
65
+ (order_id,)
66
+ )
67
+ items = [
68
+ {"name": name, "quantity": qty, "price": price}
69
+ for name, qty, price in c.fetchall()
70
+ ]
71
+ orders.append({
72
+ "email_number": email_number,
73
+ "order_date": date_str,
74
+ "order_time": order_time,
75
+ "restaurant_name": restaurant_name,
76
+ "delivery_address": delivery_address,
77
+ "items": items,
78
+ "total_price": total_price
79
+ })
80
+ conn.close()
81
+ return orders
82
+
83
+
84
+ def save_orders_to_db(date_str: str, orders: list[dict]) -> None:
85
+ """
86
+ Insert scraped orders and their items for a given date into the database.
87
+ """
88
+ conn = get_db_connection()
89
+ c = conn.cursor()
90
+ for order in orders:
91
+ c.execute(
92
+ """
93
+ INSERT INTO orders
94
+ (email_number, order_date, order_time, restaurant_name, delivery_address, total_price)
95
+ VALUES (?, ?, ?, ?, ?, ?)
96
+ """,
97
+ (
98
+ order["email_number"],
99
+ date_str,
100
+ order["order_time"],
101
+ order["restaurant_name"],
102
+ order["delivery_address"],
103
+ order["total_price"]
104
+ )
105
+ )
106
+ order_id = c.lastrowid
107
+ for item in order.get("items", []):
108
+ c.execute(
109
+ """
110
+ INSERT INTO order_items
111
+ (order_id, item_name, quantity, price)
112
+ VALUES (?, ?, ?, ?)
113
+ """,
114
+ (
115
+ order_id,
116
+ item["name"],
117
+ item["quantity"],
118
+ item["price"]
119
+ )
120
+ )
121
+ conn.commit()
122
+ conn.close()
123
+
124
+
mcp/server/eval.txt ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ EVALUATIONS
2
+
3
+ able to differentiate between swiggy based queries or not
4
+ able to break prompts into 2 instruction. First instruction will be extracting dates.
5
+ second instructions will be analysing the data scrapped.
6
+ able to extract dates
7
+ able to extract the analysis:
8
+
9
+ -How much non-veg did I order last week / between date X and date Y?
10
+
11
+ -Counts and/or total rupees spent on non-veg dishes.
12
+
13
+ -What was my total expense between date X and date Y?
14
+
15
+ -What is my average daily spend on Swiggy (overall or for a given period)?
16
+
17
+ -Which restaurant got the most orders (or the most money) in that period?
18
+
19
+ -What single order cost me the most during that window?
20
+
21
+ -Show a day-by-day spend chart between date X and date Y.
22
+
23
+ -List every item I’ve ordered only once—my “one-hit wonders.”
24
+
25
+ -How many unique cuisines did I try in the last month?
26
+
27
+ -Compare veg vs. non-veg spend for the current calendar year.
28
+
29
+ -When was my longest streak of days without ordering anything?
30
+
31
+ -Identify any “late-night” orders placed after 11 p.m. this week.
32
+
33
+ -Tell me my top three most-ordered dishes in the past six months.
34
+
35
+ -Which new restaurant did I try most recently, and what did I order?
36
+
37
+ -Forecast next month’s spend based on my last three-month trend.
mcp/server/main.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # File: main.py
2
+ from fastapi import FastAPI
3
+ from contextlib import asynccontextmanager
4
+ from routes import router
5
+ from db_schema import init_db
6
+
7
+ @asynccontextmanager
8
+ async def lifespan(app: FastAPI):
9
+ print("🚀 Server is starting up...")
10
+ # Ensure SQLite tables exist before handling requests
11
+ init_db()
12
+ yield
13
+ print("🧹 Server is shutting down... Cleaned up!")
14
+
15
+ app = FastAPI(
16
+ title="Swiggy Email API",
17
+ description="Extract Swiggy orders from Gmail",
18
+ version="1.0.0",
19
+ lifespan=lifespan
20
+ )
21
+
22
+ app.include_router(router)
23
+
24
+ # Run with: uvicorn main:app --reload
mcp/server/routes.py ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException
2
+ from pydantic import BaseModel
3
+ from swiggy_scraper import fetch_swiggy_orders
4
+ from datetime import datetime
5
+ from openai import OpenAI
6
+ import os, json
7
+
8
+ router = APIRouter()
9
+ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
10
+
11
+ class QueryInput(BaseModel):
12
+ query: str
13
+
14
+ class DateRange(BaseModel):
15
+ start_date: str
16
+ end_date: str
17
+
18
+ # @router.post("/parse_query")
19
+ # def parse_query_llm(input: QueryInput):
20
+
21
+ # print("\ninput query:")
22
+ # print(input)
23
+
24
+ # today_str = datetime.today().strftime("%d-%b-%Y")
25
+ # system = (
26
+ # f"You are a date range extractor.\n"
27
+ # f"Today is {today_str}.\n"
28
+ # "Extract start_date and end_date in 'DD-MMM-YYYY' format.\n"
29
+ # "Respond with:\n"
30
+ # "Output ONLY a valid JSON object like:\n"
31
+ # '{ "start_date": "17-May-2025", "end_date": "18-May-2025" }\n'
32
+ # 'no extra commentry needed.'
33
+ # )
34
+ # try:
35
+ # rsp = client.chat.completions.create(
36
+ # model="gpt-4o-mini",
37
+ # temperature=0,
38
+ # messages=[
39
+ # {"role": "system", "content": system},
40
+ # {"role": "user", "content": input.query},
41
+ # ]
42
+ # )
43
+ # result = json.loads(rsp.choices[0].message.content.strip())
44
+ # if "start_date" not in result or "end_date" not in result:
45
+ # raise ValueError("Invalid response format")
46
+ # print("results:", result)
47
+ # return result
48
+ # except Exception as e:
49
+ # raise HTTPException(status_code=400, detail=str(e))
50
+
51
+
52
+
53
+
54
+
55
+
56
+
57
+ def _llm(messages, model="gpt-4o-mini", temperature=0):
58
+ rsp = client.chat.completions.create(
59
+ model=model,
60
+ temperature=temperature,
61
+ messages=messages,
62
+ )
63
+ return rsp.choices[0].message.content.strip()
64
+
65
+ # ---------- Stage 1: classify + extract dates --------------------------
66
+
67
+ def _extract_scope(user_query: str):
68
+ today_str = datetime.today().strftime("%d-%b-%Y")
69
+
70
+ sys_prompt = f"""
71
+ Today is {today_str}.
72
+ You are a SCOPING assistant: decide if the user's text is about Swiggy food orders,
73
+ extract ONE date range, and keep the leftover words.
74
+
75
+ Return ONLY valid JSON like:
76
+ {{
77
+ "is_swiggy_query": true,
78
+ "start_date": "15-May-2025",
79
+ "end_date": "20-May-2025",
80
+ "remainder": "non veg expense"
81
+ }}
82
+
83
+ Rules:
84
+ • Accept natural phrases (“last week”, “since 1 May”).
85
+ • If no dates → start_date & end_date = null.
86
+ • If not Swiggy related → is_swiggy_query=false and remainder is full original text.
87
+ • Do NOT invent a remainder; it is literally whatever words follow the date phrase(s).
88
+ """
89
+ raw = _llm(
90
+ [
91
+ {"role": "system", "content": sys_prompt},
92
+ {"role": "user", "content": user_query}
93
+ ]
94
+ )
95
+ return json.loads(raw)
96
+
97
+ # ---------- Stage 2: shrink “remainder” into an intent -----------------
98
+
99
+ def _extract_intent(remainder: str):
100
+ sys_prompt = """
101
+ You are an INTENT classifier for Swiggy-order analytics.
102
+ Map the sentence into one concise snake_case intent.
103
+ Allowed intents (extendable):
104
+
105
+ • calculate_expense
106
+ • list_orders
107
+ • list_items
108
+ • list_nonveg_items
109
+ • list_veg_items
110
+ • count_orders
111
+ • unknown
112
+
113
+ Return JSON: { "intent": "calculate_expense" }
114
+ If unsure choose "unknown".
115
+ """
116
+ raw = _llm(
117
+ [
118
+ {"role": "system", "content": sys_prompt},
119
+ {"role": "user", "content": remainder.strip()}
120
+ ]
121
+ )
122
+ return json.loads(raw)["intent"]
123
+
124
+ # ---------- FastAPI route ----------------------------------------------
125
+
126
+ @router.post("/parse_query")
127
+ def parse_query_llm(input: QueryInput):
128
+ try:
129
+ scope = _extract_scope(input.query)
130
+ print("scope")
131
+ print(scope)
132
+ # If it is a Swiggy query, classify intent; else, intent = "unrelated"
133
+ if scope.get("is_swiggy_query", False):
134
+ intent = _extract_intent(scope.get("remainder", ""))
135
+ else:
136
+ intent = "unrelated"
137
+
138
+ result = {
139
+ "is_swiggy_query": scope["is_swiggy_query"],
140
+ "start_date": scope["start_date"],
141
+ "end_date": scope["end_date"],
142
+ "intent": intent
143
+ }
144
+
145
+
146
+ print("result")
147
+ print(result)
148
+
149
+ return result
150
+
151
+ except Exception as e:
152
+ raise HTTPException(status_code=400, detail=str(e))
153
+
154
+
155
+
156
+
157
+
158
+
159
+
160
+
161
+ @router.post("/get_orders")
162
+ def get_orders(range: DateRange):
163
+ try:
164
+ orders = fetch_swiggy_orders(range.start_date, range.end_date)
165
+ return orders
166
+ except Exception as e:
167
+ raise HTTPException(status_code=500, detail=str(e))
mcp/server/swiggy_cache.json ADDED
@@ -0,0 +1,1379 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "2025-05-17": [
3
+ {
4
+ "email_number": 1,
5
+ "order_date": "17-May-2025",
6
+ "order_time": "11:00:08",
7
+ "restaurant_name": "IDC Kitchen",
8
+ "delivery_address": "2C, Orchard Green Apartment, 2nd Main Rd, Domlur, Bangalore, Karnataka 560071, India. (Orchard Green)",
9
+ "items": [
10
+ {
11
+ "name": "Thatte Idli",
12
+ "quantity": 1,
13
+ "price": 59
14
+ },
15
+ {
16
+ "name": "Idly 2 Pc",
17
+ "quantity": 1,
18
+ "price": 59
19
+ },
20
+ {
21
+ "name": "Vada (1 Pc)",
22
+ "quantity": 3,
23
+ "price": 174
24
+ }
25
+ ],
26
+ "total_price": 330
27
+ },
28
+ {
29
+ "email_number": 2,
30
+ "order_date": "17-May-2025",
31
+ "order_time": "17:02:59",
32
+ "restaurant_name": "Suryawanshi",
33
+ "delivery_address": "Shobhit\n2C, Orchard Green Apartment\n2nd Main Rd, Domlur, Bangalore, Karnataka 560071, India. (Orchard Green)",
34
+ "items": [
35
+ {
36
+ "name": "Vada Pav (twin)",
37
+ "quantity": 1,
38
+ "price": 95
39
+ },
40
+ {
41
+ "name": "Kande Bhaji",
42
+ "quantity": 1,
43
+ "price": 160
44
+ }
45
+ ],
46
+ "total_price": 300
47
+ },
48
+ {
49
+ "email_number": 3,
50
+ "order_date": "17-May-2025",
51
+ "order_time": "20:25:39",
52
+ "restaurant_name": "Meghana Foods",
53
+ "delivery_address": "2C, Orchard Green Apartment, 2nd Main Rd, Domlur, Bangalore, Karnataka 560071, India. (Orchard Green)",
54
+ "items": [
55
+ {
56
+ "name": "Extra Aloo 4 Pcs",
57
+ "quantity": 1,
58
+ "price": 45
59
+ },
60
+ {
61
+ "name": "Chicken Boneless Biryani",
62
+ "quantity": 1,
63
+ "price": 360
64
+ },
65
+ {
66
+ "name": "Lemon Chicken",
67
+ "quantity": 1,
68
+ "price": 360
69
+ },
70
+ {
71
+ "name": "Veg Manchurian Biryani",
72
+ "quantity": 1,
73
+ "price": 360
74
+ }
75
+ ],
76
+ "total_price": 1240
77
+ }
78
+ ],
79
+ "2025-05-19": [
80
+ {
81
+ "email_number": 1,
82
+ "order_date": "19-May-2025",
83
+ "order_time": "22:28:08",
84
+ "restaurant_name": "Swiggy Instamart",
85
+ "delivery_address": "2C, Orchard Green Apartment 2nd Main Rd, Domlur, Bangalore, Karnataka 560071, India. (Orchard Green)",
86
+ "items": [
87
+ {
88
+ "name": "Safal Frozen Green Peas (Matar)",
89
+ "quantity": 1,
90
+ "price": 110
91
+ },
92
+ {
93
+ "name": "NOTO Strawberry Raspberry Sugar Free Popsicle Ice Cream",
94
+ "quantity": 4,
95
+ "price": 500
96
+ },
97
+ {
98
+ "name": "Baby Lady's Finger (Bendekaayi)",
99
+ "quantity": 2,
100
+ "price": 32
101
+ },
102
+ {
103
+ "name": "Whisper Ultra Skin Love Soft 30 Xl+ Sanitary Pads, Cottony Soft",
104
+ "quantity": 1,
105
+ "price": 323
106
+ },
107
+ {
108
+ "name": "Akshayakalpa Artisanal Organic Set Curd",
109
+ "quantity": 1,
110
+ "price": 145
111
+ },
112
+ {
113
+ "name": "Akshayakalpa Organic Malai Paneer",
114
+ "quantity": 1,
115
+ "price": 119
116
+ },
117
+ {
118
+ "name": "Aashirvaad Superior MP Atta",
119
+ "quantity": 2,
120
+ "price": 146
121
+ },
122
+ {
123
+ "name": "NOTO Kala Jamun Sugar Free Popsicle Ice Cream",
124
+ "quantity": 2,
125
+ "price": 250
126
+ },
127
+ {
128
+ "name": "Royal Gala Apple (Sebu)",
129
+ "quantity": 2,
130
+ "price": 356
131
+ }
132
+ ],
133
+ "total_price": 1564
134
+ }
135
+ ],
136
+ "2025-05-01": [
137
+ {
138
+ "email_number": 1,
139
+ "order_date": "01-May-2025",
140
+ "order_time": "11:36:15",
141
+ "restaurant_name": "Starbucks Coffee",
142
+ "delivery_address": "2C, Orchard Green Apartment, 2nd Main Rd, Domlur, Bangalore, Karnataka 560071, India. (Orchard Green)",
143
+ "items": [
144
+ {
145
+ "name": "Caffe Americano",
146
+ "quantity": 2,
147
+ "price": 710
148
+ }
149
+ ],
150
+ "total_price": 647
151
+ },
152
+ {
153
+ "email_number": 2,
154
+ "order_date": "01-May-2025",
155
+ "order_time": "18:34:08",
156
+ "restaurant_name": "Chaayos Chai+Snacks=Relax",
157
+ "delivery_address": "Shobhit\n2C, Orchard Green Apartment\n2nd Main Rd, Domlur, Bangalore, Karnataka 560071, India. (Orchard Green)",
158
+ "items": [
159
+ {
160
+ "name": "Vada Pav",
161
+ "quantity": 2,
162
+ "price": 238
163
+ },
164
+ {
165
+ "name": "Desi Chai",
166
+ "quantity": 1,
167
+ "price": 219
168
+ },
169
+ {
170
+ "name": "Samosa(2 pc)",
171
+ "quantity": 1,
172
+ "price": 90
173
+ }
174
+ ],
175
+ "total_price": 528
176
+ },
177
+ {
178
+ "email_number": 3,
179
+ "order_date": "01-May-2025",
180
+ "order_time": "21:28:12",
181
+ "restaurant_name": "Lavonne Cafe",
182
+ "delivery_address": "2C, Orchard Green Apartment, 2nd Main Rd, Domlur, Bangalore, Karnataka 560071, India. (Orchard Green)",
183
+ "items": [
184
+ {
185
+ "name": "Baguette",
186
+ "quantity": 1,
187
+ "price": 190.48
188
+ }
189
+ ],
190
+ "total_price": 278
191
+ }
192
+ ],
193
+ "2025-05-02": [
194
+ {
195
+ "email_number": 1,
196
+ "order_date": "02-May-2025",
197
+ "order_time": "11:13:38",
198
+ "restaurant_name": "Starbucks Coffee",
199
+ "delivery_address": "2C, Orchard Green Apartment, 2nd Main Rd, Domlur, Bangalore, Karnataka 560071, India. (Orchard Green)",
200
+ "items": [
201
+ {
202
+ "name": "Caffe Americano",
203
+ "quantity": 1,
204
+ "price": 315
205
+ },
206
+ {
207
+ "name": "Caffe Americano",
208
+ "quantity": 1,
209
+ "price": 355
210
+ }
211
+ ],
212
+ "total_price": 605
213
+ },
214
+ {
215
+ "email_number": 2,
216
+ "order_date": "02-May-2025",
217
+ "order_time": "14:43:19",
218
+ "restaurant_name": "Harvest Salad Co",
219
+ "delivery_address": "2C, Orchard Green Apartment, 2nd Main Rd, Domlur, Bangalore, Karnataka 560071, India. (Orchard Green)",
220
+ "items": [
221
+ {
222
+ "name": "Hummus And Chipotle Chicken Wrap",
223
+ "quantity": 2,
224
+ "price": 498
225
+ },
226
+ {
227
+ "name": "Mediterranean Bowl (Veg)",
228
+ "quantity": 1,
229
+ "price": 299
230
+ }
231
+ ],
232
+ "total_price": 899
233
+ },
234
+ {
235
+ "email_number": 3,
236
+ "order_date": "02-May-2025",
237
+ "order_time": "21:10:20",
238
+ "restaurant_name": "Swiggy Instamart",
239
+ "delivery_address": "2C, Orchard Green Apartment 2nd Main Rd, Domlur, Bangalore, Karnataka 560071, India. (Orchard Green)",
240
+ "items": [
241
+ {
242
+ "name": "Classic Ice Burst",
243
+ "quantity": 1,
244
+ "price": 170
245
+ }
246
+ ],
247
+ "total_price": 222
248
+ },
249
+ {
250
+ "email_number": 4,
251
+ "order_date": "02-May-2025",
252
+ "order_time": "22:01:35",
253
+ "restaurant_name": "Dhaba Estd 1986 Delhi",
254
+ "delivery_address": "2C, Orchard Green Apartment, 2nd Main Rd, Domlur, Bangalore, Karnataka 560071, India. (Orchard Green)",
255
+ "items": [
256
+ {
257
+ "name": "Non Vegetarian Platter ( 12 Pcs)",
258
+ "quantity": 1,
259
+ "price": 1249
260
+ },
261
+ {
262
+ "name": "Butter Naan",
263
+ "quantity": 2,
264
+ "price": 198
265
+ },
266
+ {
267
+ "name": "Dhabe Di Roti",
268
+ "quantity": 2,
269
+ "price": 238
270
+ },
271
+ {
272
+ "name": "Laal Mirchi Parantha",
273
+ "quantity": 1,
274
+ "price": 109
275
+ },
276
+ {
277
+ "name": "Handi Murgh",
278
+ "quantity": 1,
279
+ "price": 629
280
+ }
281
+ ],
282
+ "total_price": 2006
283
+ }
284
+ ],
285
+ "2025-05-03": [
286
+ {
287
+ "email_number": 1,
288
+ "order_date": "03-May-2025",
289
+ "order_time": "09:50:51",
290
+ "restaurant_name": "Starbucks Coffee",
291
+ "delivery_address": "2C, Orchard Green Apartment, 2nd Main Rd, Domlur, Bangalore, Karnataka 560071, India. (Orchard Green)",
292
+ "items": [
293
+ {
294
+ "name": "Cold Brew Black",
295
+ "quantity": 1,
296
+ "price": 410
297
+ },
298
+ {
299
+ "name": "Caffe Americano.",
300
+ "quantity": 1,
301
+ "price": 315
302
+ }
303
+ ],
304
+ "total_price": 691
305
+ },
306
+ {
307
+ "email_number": 2,
308
+ "order_date": "03-May-2025",
309
+ "order_time": "15:06:18",
310
+ "restaurant_name": "Copper + Cloves",
311
+ "delivery_address": "2C, Orchard Green Apartment, 2nd Main Rd, Domlur, Bangalore, Karnataka 560071, India. (Orchard Green)",
312
+ "items": [
313
+ {
314
+ "name": "Asian Peanut Salad",
315
+ "quantity": 1,
316
+ "price": 500
317
+ },
318
+ {
319
+ "name": "Tofu",
320
+ "quantity": 1,
321
+ "price": 100
322
+ }
323
+ ],
324
+ "total_price": 432
325
+ },
326
+ {
327
+ "email_number": 3,
328
+ "order_date": "03-May-2025",
329
+ "order_time": "21:32:47",
330
+ "restaurant_name": "Shiro",
331
+ "delivery_address": "2C, Orchard Green Apartment, 2nd Main Rd, Domlur, Bangalore, Karnataka 560071, India. (Orchard Green)",
332
+ "items": [
333
+ {
334
+ "name": "Super Crunch (8 pcs)",
335
+ "quantity": 1,
336
+ "price": 695
337
+ },
338
+ {
339
+ "name": "Bulgogi Chicken Spring Rolls (6 pcs)",
340
+ "quantity": 1,
341
+ "price": 595
342
+ },
343
+ {
344
+ "name": "Cantonese Chicken Wonton (6 pcs)",
345
+ "quantity": 1,
346
+ "price": 410
347
+ },
348
+ {
349
+ "name": "Smoky Pork Gyoza (4 pcs)",
350
+ "quantity": 1,
351
+ "price": 495
352
+ },
353
+ {
354
+ "name": "Spicy Salmon Negi (8 pcs)",
355
+ "quantity": 1,
356
+ "price": 670
357
+ }
358
+ ],
359
+ "total_price": 2600
360
+ }
361
+ ],
362
+ "2025-05-04": [
363
+ {
364
+ "email_number": 1,
365
+ "order_date": "04-May-2025",
366
+ "order_time": "09:54:22",
367
+ "restaurant_name": "IDC Kitchen",
368
+ "delivery_address": "2C, Orchard Green Apartment, 2nd Main Rd, Domlur, Bangalore, Karnataka 560071, India. (Orchard Green)",
369
+ "items": [
370
+ {
371
+ "name": "Masala Dosa",
372
+ "quantity": 1,
373
+ "price": 140
374
+ },
375
+ {
376
+ "name": "Idly 2 Pc",
377
+ "quantity": 1,
378
+ "price": 59
379
+ },
380
+ {
381
+ "name": "Thatte Idli",
382
+ "quantity": 1,
383
+ "price": 59
384
+ },
385
+ {
386
+ "name": "Vada (1 Pc)",
387
+ "quantity": 1,
388
+ "price": 58
389
+ }
390
+ ],
391
+ "total_price": 356
392
+ },
393
+ {
394
+ "email_number": 2,
395
+ "order_date": "04-May-2025",
396
+ "order_time": "10:21:33",
397
+ "restaurant_name": "Starbucks Coffee",
398
+ "delivery_address": "2C, Orchard Green Apartment, 2nd Main Rd, Domlur, Bangalore, Karnataka 560071, India. (Orchard Green)",
399
+ "items": [
400
+ {
401
+ "name": "Caffe Americano",
402
+ "quantity": 1,
403
+ "price": 315
404
+ },
405
+ {
406
+ "name": "Caffe Americano",
407
+ "quantity": 1,
408
+ "price": 355
409
+ }
410
+ ],
411
+ "total_price": 694
412
+ }
413
+ ],
414
+ "2025-05-05": [
415
+ {
416
+ "email_number": 1,
417
+ "order_date": "05-May-2025",
418
+ "order_time": "09:23:24",
419
+ "restaurant_name": "Starbucks Coffee",
420
+ "delivery_address": "2C, Orchard Green Apartment, 2nd Main Rd, Domlur, Bangalore, Karnataka 560071, India. (Orchard Green)",
421
+ "items": [
422
+ {
423
+ "name": "Caffe Americano",
424
+ "quantity": 1,
425
+ "price": 315
426
+ },
427
+ {
428
+ "name": "Caffe Americano",
429
+ "quantity": 1,
430
+ "price": 355
431
+ }
432
+ ],
433
+ "total_price": 694
434
+ },
435
+ {
436
+ "email_number": 2,
437
+ "order_date": "05-May-2025",
438
+ "order_time": "13:58:29",
439
+ "restaurant_name": "Maverick & Farmer Coffee",
440
+ "delivery_address": "2C, Orchard Green Apartment, 2nd Main Rd, Domlur, Bangalore, Karnataka 560071, India. (Orchard Green)",
441
+ "items": [
442
+ {
443
+ "name": "Cold Brew Latte",
444
+ "quantity": 1,
445
+ "price": 300
446
+ },
447
+ {
448
+ "name": "Smoked Tandoori Chicken",
449
+ "quantity": 1,
450
+ "price": 450
451
+ }
452
+ ],
453
+ "total_price": 730
454
+ },
455
+ {
456
+ "email_number": 3,
457
+ "order_date": "05-May-2025",
458
+ "order_time": "14:42:09",
459
+ "restaurant_name": "Magnolia Bakery",
460
+ "delivery_address": "2C, Orchard Green Apartment, 2nd Main Rd, Domlur, Bangalore, Karnataka 560071, India. (Orchard Green)",
461
+ "items": [
462
+ {
463
+ "name": "CLASSIC TRES LECHES",
464
+ "quantity": 1,
465
+ "price": 410
466
+ }
467
+ ],
468
+ "total_price": 538
469
+ },
470
+ {
471
+ "email_number": 4,
472
+ "order_date": "05-May-2025",
473
+ "order_time": "21:09:39",
474
+ "restaurant_name": "Hyderabad Biryaani House",
475
+ "delivery_address": "2C, Orchard Green Apartment, 2nd Main Rd, Domlur, Bangalore, Karnataka 560071, India. (Orchard Green)",
476
+ "items": [
477
+ {
478
+ "name": "Mutton Biryani Family Pack",
479
+ "quantity": 1,
480
+ "price": 749
481
+ }
482
+ ],
483
+ "total_price": 806
484
+ }
485
+ ],
486
+ "2025-05-06": [
487
+ {
488
+ "email_number": 1,
489
+ "order_date": "06-May-2025",
490
+ "order_time": "09:58:07",
491
+ "restaurant_name": "Swiggy Instamart",
492
+ "delivery_address": "2C, Orchard Green Apartment 2nd Main Rd, Domlur, Bangalore, Karnataka 560071, India. (Orchard Green)",
493
+ "items": [
494
+ {
495
+ "name": "Yellaki Banana (Baalehannu)",
496
+ "quantity": 1,
497
+ "price": 59
498
+ },
499
+ {
500
+ "name": "Robusta Banana (Pachha Baalehannu)",
501
+ "quantity": 1,
502
+ "price": 30
503
+ },
504
+ {
505
+ "name": "Eggoz Farm Fresh High Protein White Eggs Box",
506
+ "quantity": 1,
507
+ "price": 325
508
+ },
509
+ {
510
+ "name": "Amul Processed Cheese Slices",
511
+ "quantity": 1,
512
+ "price": 145
513
+ },
514
+ {
515
+ "name": "NOTO Orange Sugar Free Popsicle Ice Cream",
516
+ "quantity": 2,
517
+ "price": 157.5
518
+ },
519
+ {
520
+ "name": "Rocket Leaves (Arugula)",
521
+ "quantity": 1,
522
+ "price": 54
523
+ },
524
+ {
525
+ "name": "Royal Gala Apple (Sebu)",
526
+ "quantity": 2,
527
+ "price": 254.6
528
+ },
529
+ {
530
+ "name": "NOTO Kala Jamun Sugar Free Popsicle Ice Cream",
531
+ "quantity": 2,
532
+ "price": 132.5
533
+ },
534
+ {
535
+ "name": "Imported Avocado - Tanzania",
536
+ "quantity": 1,
537
+ "price": 75
538
+ },
539
+ {
540
+ "name": "Ratnagiri Alphonso Mango (Hapus)",
541
+ "quantity": 1,
542
+ "price": 415
543
+ },
544
+ {
545
+ "name": "Indian Blueberries",
546
+ "quantity": 1,
547
+ "price": 199
548
+ },
549
+ {
550
+ "name": "Flyer Dil Foods",
551
+ "quantity": 1,
552
+ "price": 0
553
+ },
554
+ {
555
+ "name": "Premium Guava (Thai)",
556
+ "quantity": 1,
557
+ "price": 71
558
+ },
559
+ {
560
+ "name": "Lettuce Mix Salad",
561
+ "quantity": 2,
562
+ "price": 120
563
+ }
564
+ ],
565
+ "total_price": 2049
566
+ },
567
+ {
568
+ "email_number": 2,
569
+ "order_date": "06-May-2025",
570
+ "order_time": "10:51:28",
571
+ "restaurant_name": "Starbucks Coffee",
572
+ "delivery_address": "2C, Orchard Green Apartment, 2nd Main Rd, Domlur, Bangalore, Karnataka 560071, India. (Orchard Green)",
573
+ "items": [
574
+ {
575
+ "name": "Caffe Americano",
576
+ "quantity": 1,
577
+ "price": 355
578
+ },
579
+ {
580
+ "name": "Caffe Americano",
581
+ "quantity": 1,
582
+ "price": 315
583
+ }
584
+ ],
585
+ "total_price": 694
586
+ },
587
+ {
588
+ "email_number": 3,
589
+ "order_date": "06-May-2025",
590
+ "order_time": "11:06:59",
591
+ "restaurant_name": "Yogisthaan",
592
+ "delivery_address": "2C, Orchard Green Apartment, 2nd Main Rd, Domlur, Bangalore, Karnataka 560071, India. (Orchard Green)",
593
+ "items": [
594
+ {
595
+ "name": "Tapioca (sabu Dana) Khichadi",
596
+ "quantity": 1,
597
+ "price": 252
598
+ },
599
+ {
600
+ "name": "Sattu Paratha",
601
+ "quantity": 1,
602
+ "price": 288
603
+ }
604
+ ],
605
+ "total_price": 537
606
+ },
607
+ {
608
+ "email_number": 4,
609
+ "order_date": "06-May-2025",
610
+ "order_time": "15:05:22",
611
+ "restaurant_name": "Sweet Karam Coffee",
612
+ "delivery_address": "2C, Orchard Green Apartment 2nd Main Rd, Domlur, Bangalore, Karnataka 560071, India. (Orchard Green)",
613
+ "items": [
614
+ {
615
+ "name": "Sweet Karam Coffee Tapioca Chips (Kappa) - No Palm Oil",
616
+ "quantity": 1,
617
+ "price": 65
618
+ },
619
+ {
620
+ "name": "Sweet Karam Coffee Special Madras Mixture (No Palm Oil)",
621
+ "quantity": 1,
622
+ "price": 90
623
+ },
624
+ {
625
+ "name": "Sweet Karam Coffee Peanut Chikki Bites (No White Sugar, No Liquid Glucose)",
626
+ "quantity": 1,
627
+ "price": 87
628
+ },
629
+ {
630
+ "name": "Mother's Recipe Potato Masala Papad",
631
+ "quantity": 4,
632
+ "price": 126.64
633
+ },
634
+ {
635
+ "name": "Sweet Karam Coffee Kerala Nendran Banana Chips",
636
+ "quantity": 1,
637
+ "price": 87.12
638
+ },
639
+ {
640
+ "name": "Iyer's Rice Papad Buy 1 Get 1 (inside one pack)",
641
+ "quantity": 1,
642
+ "price": 64
643
+ },
644
+ {
645
+ "name": "Ginger kombucha (Low cal, Low sugar)",
646
+ "quantity": 4,
647
+ "price": 368.28
648
+ }
649
+ ],
650
+ "total_price": 899
651
+ },
652
+ {
653
+ "email_number": 5,
654
+ "order_date": "06-May-2025",
655
+ "order_time": "22:36:46",
656
+ "restaurant_name": "Misu",
657
+ "delivery_address": "2C, Orchard Green Apartment, 2nd Main Rd, Domlur, Bangalore, Karnataka 560071, India.",
658
+ "items": [
659
+ {
660
+ "name": "Spicy Chilli Basil Noodles Non-veg",
661
+ "quantity": 1,
662
+ "price": 465
663
+ },
664
+ {
665
+ "name": "Vietnamese Pho Chicken",
666
+ "quantity": 1,
667
+ "price": 305
668
+ },
669
+ {
670
+ "name": "Sour & Spicy Dumplings Chicken (6 Pcs)",
671
+ "quantity": 1,
672
+ "price": 405
673
+ }
674
+ ],
675
+ "total_price": 1117
676
+ }
677
+ ],
678
+ "2025-05-07": [
679
+ {
680
+ "email_number": 1,
681
+ "order_date": "07-May-2025",
682
+ "order_time": "13:26:32",
683
+ "restaurant_name": "Kale - A Salad Symphony",
684
+ "delivery_address": "Shobhit\n2nd Floor\nKariyammana Agrahara Road, Marathahalli, Bengaluru, Karnataka 560037, India. (Divyasree Technopolis)",
685
+ "items": [
686
+ {
687
+ "name": "Thai Chicken Bowl (nutrients & Vitamin B12)",
688
+ "quantity": 1,
689
+ "price": 425
690
+ },
691
+ {
692
+ "name": "Green Protein Smoothie",
693
+ "quantity": 1,
694
+ "price": 250
695
+ }
696
+ ],
697
+ "total_price": 693
698
+ },
699
+ {
700
+ "email_number": 2,
701
+ "order_date": "07-May-2025",
702
+ "order_time": "13:53:43",
703
+ "restaurant_name": "Chakum Chukum",
704
+ "delivery_address": "2C, Orchard Green Apartment, 2nd Main Rd, Domlur, Bangalore, Karnataka 560071, India. (Orchard Green)",
705
+ "items": [
706
+ {
707
+ "name": "Chicken Tikka Roll",
708
+ "quantity": 1,
709
+ "price": 237.15
710
+ },
711
+ {
712
+ "name": "Kasundi Paneer Roll",
713
+ "quantity": 1,
714
+ "price": 275.24
715
+ }
716
+ ],
717
+ "total_price": 518
718
+ },
719
+ {
720
+ "email_number": 3,
721
+ "order_date": "07-May-2025",
722
+ "order_time": "20:50:27",
723
+ "restaurant_name": "Lavonne Cafe",
724
+ "delivery_address": "2C, Orchard Green Apartment, 2nd Main Rd, Domlur, Bangalore, Karnataka 560071, India. (Orchard Green)",
725
+ "items": [
726
+ {
727
+ "name": "Sourdough Bread Plain",
728
+ "quantity": 1,
729
+ "price": 219.05
730
+ }
731
+ ],
732
+ "total_price": 41
733
+ },
734
+ {
735
+ "email_number": 4,
736
+ "order_date": "07-May-2025",
737
+ "order_time": "21:51:42",
738
+ "restaurant_name": "Basco And Fry",
739
+ "delivery_address": "2C, Orchard Green Apartment, 2nd Main Rd, Domlur, Bangalore, Karnataka 560071, India. (Orchard Green)",
740
+ "items": [
741
+ {
742
+ "name": "Truffle Shroom Burger",
743
+ "quantity": 1,
744
+ "price": 365
745
+ },
746
+ {
747
+ "name": "Peri Peri Wings",
748
+ "quantity": 1,
749
+ "price": 425
750
+ },
751
+ {
752
+ "name": "Chicken Katsu Burger",
753
+ "quantity": 1,
754
+ "price": 365
755
+ }
756
+ ],
757
+ "total_price": 1100
758
+ }
759
+ ],
760
+ "2025-05-08": [
761
+ {
762
+ "email_number": 1,
763
+ "order_date": "08-May-2025",
764
+ "order_time": "13:04:12",
765
+ "restaurant_name": "NATRAJ Chole bhature",
766
+ "delivery_address": "2C, Orchard Green Apartment, 2nd Main Rd, Domlur, Bangalore, Karnataka 560071, India. (Orchard Green)",
767
+ "items": [
768
+ {
769
+ "name": "Chole Bhature(full Plate)",
770
+ "quantity": 1,
771
+ "price": 225
772
+ },
773
+ {
774
+ "name": "Samosa(1pc)",
775
+ "quantity": 1,
776
+ "price": 45
777
+ }
778
+ ],
779
+ "total_price": 310
780
+ },
781
+ {
782
+ "email_number": 2,
783
+ "order_date": "08-May-2025",
784
+ "order_time": "14:27:04",
785
+ "restaurant_name": "Salad Days",
786
+ "delivery_address": "2nd Floor, Kariyammana Agrahara Road, Marathahalli, Bengaluru, Karnataka 560037, India. (Divyasree Technopolis)",
787
+ "items": [
788
+ {
789
+ "name": "Asian Chicken, Egg & Soba Noodle Salad",
790
+ "quantity": 1,
791
+ "price": 379
792
+ },
793
+ {
794
+ "name": "Green Pressed Juice",
795
+ "quantity": 1,
796
+ "price": 199
797
+ }
798
+ ],
799
+ "total_price": 655
800
+ },
801
+ {
802
+ "email_number": 3,
803
+ "order_date": "08-May-2025",
804
+ "order_time": "22:47:20",
805
+ "restaurant_name": "Mahesh Lunch Home",
806
+ "delivery_address": "2C, Orchard Green Apartment, 2nd Main Rd, Domlur, Bangalore, Karnataka 560071, India. (Orchard Green)",
807
+ "items": [
808
+ {
809
+ "name": "Appam",
810
+ "quantity": 2,
811
+ "price": 60
812
+ },
813
+ {
814
+ "name": "Chicken Ghee Roast",
815
+ "quantity": 1,
816
+ "price": 460
817
+ },
818
+ {
819
+ "name": "Butter Roti",
820
+ "quantity": 2,
821
+ "price": 80
822
+ },
823
+ {
824
+ "name": "Chicken Manglorean",
825
+ "quantity": 1,
826
+ "price": 405
827
+ },
828
+ {
829
+ "name": "Neer Dosa (4 Pcs)",
830
+ "quantity": 1,
831
+ "price": 80
832
+ },
833
+ {
834
+ "name": "Dal Fry",
835
+ "quantity": 1,
836
+ "price": 210
837
+ }
838
+ ],
839
+ "total_price": 1267
840
+ },
841
+ {
842
+ "email_number": 4,
843
+ "order_date": "08-May-2025",
844
+ "order_time": "23:41:54",
845
+ "restaurant_name": "Swiggy Instamart",
846
+ "delivery_address": "2C, Orchard Green Apartment 2nd Main Rd, Domlur, Bangalore, Karnataka 560071, India. (Orchard Green)",
847
+ "items": [
848
+ {
849
+ "name": "NOTO Strawberry Raspberry Sugar Free Popsicle Ice Cream",
850
+ "quantity": 4,
851
+ "price": 325
852
+ },
853
+ {
854
+ "name": "NOTO Orange Sugar Free Popsicle Ice Cream",
855
+ "quantity": 2,
856
+ "price": 162.5
857
+ }
858
+ ],
859
+ "total_price": 498
860
+ }
861
+ ],
862
+ "2025-05-09": [
863
+ {
864
+ "email_number": 1,
865
+ "order_date": "09-May-2025",
866
+ "order_time": "15:22:57",
867
+ "restaurant_name": "Kale - A Salad Symphony",
868
+ "delivery_address": "Shobhit\n2nd Floor\nKariyammana Agrahara Road, Marathahalli, Bengaluru, Karnataka 560037, India. (Divyasree Technopolis)",
869
+ "items": [
870
+ {
871
+ "name": "Thai Chicken Bowl (nutrients & Vitamin B12)",
872
+ "quantity": 1,
873
+ "price": 425
874
+ },
875
+ {
876
+ "name": "Green Protein Smoothie",
877
+ "quantity": 1,
878
+ "price": 250
879
+ }
880
+ ],
881
+ "total_price": 693
882
+ },
883
+ {
884
+ "email_number": 2,
885
+ "order_date": "09-May-2025",
886
+ "order_time": "16:25:50",
887
+ "restaurant_name": "Swiggy Instamart",
888
+ "delivery_address": "2C, Orchard Green Apartment 2nd Main Rd, Domlur, Bangalore, Karnataka 560071, India. (Orchard Green)",
889
+ "items": [
890
+ {
891
+ "name": "Kodai Parmesan Cheese",
892
+ "quantity": 1,
893
+ "price": 475
894
+ }
895
+ ],
896
+ "total_price": 487
897
+ }
898
+ ],
899
+ "2025-05-10": [
900
+ {
901
+ "email_number": 1,
902
+ "order_date": "10-May-2025",
903
+ "order_time": "13:34:39",
904
+ "restaurant_name": "Swiggy Instamart",
905
+ "delivery_address": "2C, Orchard Green Apartment 2nd Main Rd, Domlur, Bangalore, Karnataka 560071, India. (Orchard Green)",
906
+ "items": [
907
+ {
908
+ "name": "Gooseberry (Nellikaayi)",
909
+ "quantity": 1,
910
+ "price": 39
911
+ },
912
+ {
913
+ "name": "Fresh Turmeric",
914
+ "quantity": 2,
915
+ "price": 42
916
+ },
917
+ {
918
+ "name": "Licious Chicken Curry Cut (Large Pieces) - Skinless",
919
+ "quantity": 1,
920
+ "price": 165
921
+ },
922
+ {
923
+ "name": "White Radish (Moolangi)",
924
+ "quantity": 2,
925
+ "price": 30.6
926
+ },
927
+ {
928
+ "name": "English Cucumber (Sowthekaayi)",
929
+ "quantity": 2,
930
+ "price": 65.8
931
+ },
932
+ {
933
+ "name": "Robusta Banana (Pachha Baalehannu)",
934
+ "quantity": 1,
935
+ "price": 44
936
+ },
937
+ {
938
+ "name": "Curry Leaves (Karibevu)",
939
+ "quantity": 1,
940
+ "price": 11
941
+ },
942
+ {
943
+ "name": "Green Chilli (Hasiru Menasinakaayi)",
944
+ "quantity": 1,
945
+ "price": 12
946
+ },
947
+ {
948
+ "name": "Coriander - Without Roots (Kotthambari)",
949
+ "quantity": 1,
950
+ "price": 18
951
+ },
952
+ {
953
+ "name": "Ginger (Shunti)",
954
+ "quantity": 1,
955
+ "price": 16
956
+ },
957
+ {
958
+ "name": "Premium Guava (Thai)",
959
+ "quantity": 2,
960
+ "price": 131.4
961
+ },
962
+ {
963
+ "name": "Royal Gala Apple (Sebu)",
964
+ "quantity": 2,
965
+ "price": 275
966
+ }
967
+ ],
968
+ "total_price": 862
969
+ },
970
+ {
971
+ "email_number": 2,
972
+ "order_date": "10-May-2025",
973
+ "order_time": "17:23:45",
974
+ "restaurant_name": "Irani Std. Tea",
975
+ "delivery_address": "Swiggy,Tower D, 9th Floor, IBC Knowledge Park, Bannerghatta Road, Bangalore - 560029",
976
+ "items": [],
977
+ "total_price": 291
978
+ },
979
+ {
980
+ "email_number": 3,
981
+ "order_date": "10-May-2025",
982
+ "order_time": "18:05:00",
983
+ "restaurant_name": "Irani Std. Tea",
984
+ "delivery_address": "2C, Orchard Green Apartment, 2nd Main Rd, Domlur, Bangalore, Karnataka 560071, India",
985
+ "items": [
986
+ {
987
+ "name": "Irani Ginger Chai",
988
+ "quantity": 1,
989
+ "price": 219
990
+ },
991
+ {
992
+ "name": "Osmania Biscuits 100 Grams",
993
+ "quantity": 1,
994
+ "price": 100
995
+ },
996
+ {
997
+ "name": "Bun Maska",
998
+ "quantity": 2,
999
+ "price": 158
1000
+ }
1001
+ ],
1002
+ "total_price": 399
1003
+ }
1004
+ ],
1005
+ "2025-05-11": [
1006
+ {
1007
+ "email_number": 1,
1008
+ "order_date": "11-May-2025",
1009
+ "order_time": "12:36:17",
1010
+ "restaurant_name": "Anand Sweets & Savouries",
1011
+ "delivery_address": "2C, Orchard Green Apartment, 2nd Main Rd, Domlur, Bangalore, Karnataka 560071, India.",
1012
+ "items": [
1013
+ {
1014
+ "name": "Shahi Tohfa Dry Fruits 750 Gms",
1015
+ "quantity": 1,
1016
+ "price": 1022.32
1017
+ }
1018
+ ],
1019
+ "total_price": 1167
1020
+ },
1021
+ {
1022
+ "email_number": 2,
1023
+ "order_date": "11-May-2025",
1024
+ "order_time": "21:17:18",
1025
+ "restaurant_name": "Hotel Empire",
1026
+ "delivery_address": "2C, Orchard Green Apartment, 2nd Main Rd, Domlur, Bangalore, Karnataka 560071, India. (Orchard Green)",
1027
+ "items": [
1028
+ {
1029
+ "name": "Paneer Butter Masala",
1030
+ "quantity": 1,
1031
+ "price": 252
1032
+ },
1033
+ {
1034
+ "name": "Kerala Parotta",
1035
+ "quantity": 1,
1036
+ "price": 39
1037
+ },
1038
+ {
1039
+ "name": "Coin Parotta",
1040
+ "quantity": 2,
1041
+ "price": 64
1042
+ },
1043
+ {
1044
+ "name": "Dal Fry",
1045
+ "quantity": 1,
1046
+ "price": 137
1047
+ },
1048
+ {
1049
+ "name": "Ghee Rice",
1050
+ "quantity": 1,
1051
+ "price": 110
1052
+ },
1053
+ {
1054
+ "name": "Malabar Parotta",
1055
+ "quantity": 2,
1056
+ "price": 70
1057
+ }
1058
+ ],
1059
+ "total_price": 749
1060
+ }
1061
+ ],
1062
+ "2025-05-12": [
1063
+ {
1064
+ "email_number": 1,
1065
+ "order_date": "12-May-2025",
1066
+ "order_time": "22:12:42",
1067
+ "restaurant_name": "Swiggy Instamart",
1068
+ "delivery_address": "2C, Orchard Green Apartment 2nd Main Rd, Domlur, Bangalore, Karnataka 560071, India. (Orchard Green)",
1069
+ "items": [
1070
+ {
1071
+ "name": "NOTO Strawberry Raspberry Sugar Free Popsicle Ice Cream",
1072
+ "quantity": 4,
1073
+ "price": 275
1074
+ },
1075
+ {
1076
+ "name": "NOTO Orange Sugar Free Popsicle Ice Cream",
1077
+ "quantity": 2,
1078
+ "price": 137.5
1079
+ }
1080
+ ],
1081
+ "total_price": 423
1082
+ }
1083
+ ],
1084
+ "2025-05-13": [],
1085
+ "2025-05-14": [
1086
+ {
1087
+ "email_number": 1,
1088
+ "order_date": "14-May-2025",
1089
+ "order_time": "14:14:14",
1090
+ "restaurant_name": "Swiggy Instamart",
1091
+ "delivery_address": "2C, Orchard Green Apartment 2nd Main Rd, Domlur, Bangalore, Karnataka 560071, India. (Orchard Green)",
1092
+ "items": [
1093
+ {
1094
+ "name": "Robusta Banana (Pachha Baalehannu)",
1095
+ "quantity": 1,
1096
+ "price": 42
1097
+ },
1098
+ {
1099
+ "name": "Coriander - Without Roots (Kotthambari)",
1100
+ "quantity": 1,
1101
+ "price": 18
1102
+ },
1103
+ {
1104
+ "name": "Tata Sampann Turmeric Powder Masala",
1105
+ "quantity": 2,
1106
+ "price": 85.04
1107
+ },
1108
+ {
1109
+ "name": "Organic Certified Onion (Eerulli)",
1110
+ "quantity": 1,
1111
+ "price": 66
1112
+ },
1113
+ {
1114
+ "name": "NOTO Strawberry Raspberry Sugar Free Popsicle Ice Cream",
1115
+ "quantity": 4,
1116
+ "price": 275
1117
+ },
1118
+ {
1119
+ "name": "Premium Guava (Thai)",
1120
+ "quantity": 2,
1121
+ "price": 154
1122
+ },
1123
+ {
1124
+ "name": "Garlic (Bellulli)",
1125
+ "quantity": 1,
1126
+ "price": 47
1127
+ },
1128
+ {
1129
+ "name": "Royal Gala Apple (Sebu)",
1130
+ "quantity": 1,
1131
+ "price": 142
1132
+ }
1133
+ ],
1134
+ "total_price": 841
1135
+ }
1136
+ ],
1137
+ "2025-05-15": [],
1138
+ "2025-05-16": [
1139
+ {
1140
+ "email_number": 1,
1141
+ "order_date": "16-May-2025",
1142
+ "order_time": "15:43:43",
1143
+ "restaurant_name": "Glen's Bakehouse",
1144
+ "delivery_address": "2C, Orchard Green Apartment, 2nd Main Rd, Domlur, Bangalore, Karnataka 560071, India. (Orchard Green)",
1145
+ "items": [
1146
+ {
1147
+ "name": "Mushroom Puff",
1148
+ "quantity": 1,
1149
+ "price": 62.86
1150
+ },
1151
+ {
1152
+ "name": "Chicken Puff",
1153
+ "quantity": 1,
1154
+ "price": 84.23
1155
+ }
1156
+ ],
1157
+ "total_price": 235
1158
+ },
1159
+ {
1160
+ "email_number": 2,
1161
+ "order_date": "16-May-2025",
1162
+ "order_time": "15:48:57",
1163
+ "restaurant_name": "The Himalayan Momo Company",
1164
+ "delivery_address": "2C, Orchard Green Apartment, 2nd Main Rd, Domlur, Bangalore, Karnataka 560071, India. (Orchard Green)",
1165
+ "items": [
1166
+ {
1167
+ "name": "Woked Maggi",
1168
+ "quantity": 1,
1169
+ "price": 209
1170
+ },
1171
+ {
1172
+ "name": "Himalayan Steamed Chicken Momos",
1173
+ "quantity": 2,
1174
+ "price": 318
1175
+ }
1176
+ ],
1177
+ "total_price": 547
1178
+ }
1179
+ ],
1180
+ "2025-05-18": [
1181
+ {
1182
+ "email_number": 1,
1183
+ "order_date": "18-May-2025",
1184
+ "order_time": "12:57:21",
1185
+ "restaurant_name": "Starbucks Coffee",
1186
+ "delivery_address": "2C, Orchard Green Apartment, 2nd Main Rd, Domlur, Bangalore, Karnataka 560071, India. (Orchard Green)",
1187
+ "items": [
1188
+ {
1189
+ "name": "Caffe Americano",
1190
+ "quantity": 1,
1191
+ "price": 315
1192
+ }
1193
+ ],
1194
+ "total_price": 363
1195
+ },
1196
+ {
1197
+ "email_number": 2,
1198
+ "order_date": "18-May-2025",
1199
+ "order_time": "13:02:51",
1200
+ "restaurant_name": "Maiz Mexican Kitchen",
1201
+ "delivery_address": "2C, Orchard Green Apartment, 2nd Main Rd, Domlur, Bangalore, Karnataka 560071, India. (Orchard Green)",
1202
+ "items": [
1203
+ {
1204
+ "name": "Chips With Guacamole",
1205
+ "quantity": 1,
1206
+ "price": 249
1207
+ },
1208
+ {
1209
+ "name": "Fresh Tomato Salsa (50ml)",
1210
+ "quantity": 1,
1211
+ "price": 69
1212
+ },
1213
+ {
1214
+ "name": "Chipotle Chicken Burrito",
1215
+ "quantity": 1,
1216
+ "price": 299
1217
+ }
1218
+ ],
1219
+ "total_price": 491
1220
+ }
1221
+ ],
1222
+ "2025-05-20": [
1223
+ {
1224
+ "email_number": 1,
1225
+ "order_date": "20-May-2025",
1226
+ "order_time": "11:24:05",
1227
+ "restaurant_name": "IDC Kitchen",
1228
+ "delivery_address": "2C, Orchard Green Apartment, 2nd Main Rd, Domlur, Bangalore, Karnataka 560071, India. (Orchard Green)",
1229
+ "items": [
1230
+ {
1231
+ "name": "Vada (1 Pc)",
1232
+ "quantity": 1,
1233
+ "price": 58
1234
+ },
1235
+ {
1236
+ "name": "Masala Dosa",
1237
+ "quantity": 1,
1238
+ "price": 140
1239
+ }
1240
+ ],
1241
+ "total_price": 167
1242
+ },
1243
+ {
1244
+ "email_number": 2,
1245
+ "order_date": "20-May-2025",
1246
+ "order_time": "16:15:50",
1247
+ "restaurant_name": "Irani Std. Tea",
1248
+ "delivery_address": "2C, Orchard Green Apartment, 2nd Main Rd, Domlur, Bangalore, Karnataka 560071, India. (Orchard Green)",
1249
+ "items": [
1250
+ {
1251
+ "name": "Bun Omelette",
1252
+ "quantity": 1,
1253
+ "price": 139
1254
+ },
1255
+ {
1256
+ "name": "Osmania Biscuits 100 Grams",
1257
+ "quantity": 1,
1258
+ "price": 100
1259
+ },
1260
+ {
1261
+ "name": "Plain Maggie",
1262
+ "quantity": 1,
1263
+ "price": 119
1264
+ },
1265
+ {
1266
+ "name": "Bun Maska",
1267
+ "quantity": 1,
1268
+ "price": 79
1269
+ },
1270
+ {
1271
+ "name": "Irani Ginger Chai",
1272
+ "quantity": 1,
1273
+ "price": 219
1274
+ },
1275
+ {
1276
+ "name": "Aloo Samosa",
1277
+ "quantity": 1,
1278
+ "price": 49
1279
+ }
1280
+ ],
1281
+ "total_price": 627
1282
+ },
1283
+ {
1284
+ "email_number": 3,
1285
+ "order_date": "20-May-2025",
1286
+ "order_time": "20:13:33",
1287
+ "restaurant_name": "Gayatri Sandwich(Mithibai College)",
1288
+ "delivery_address": "Shobhit, ground floor, Vile Parle East, Vile Parle, Mumbai, Maharashtra, India. (T1)",
1289
+ "items": [
1290
+ {
1291
+ "name": "Sada Sandwich",
1292
+ "quantity": 1,
1293
+ "price": 50
1294
+ },
1295
+ {
1296
+ "name": "Vada Pav",
1297
+ "quantity": 1,
1298
+ "price": 25
1299
+ }
1300
+ ],
1301
+ "total_price": 178
1302
+ }
1303
+ ],
1304
+ "2025-05-21": [
1305
+ {
1306
+ "email_number": 1,
1307
+ "order_date": "21-May-2025",
1308
+ "order_time": "12:56:56",
1309
+ "restaurant_name": "Starbucks Coffee",
1310
+ "delivery_address": "Shobhit\n2C, Orchard Green Apartment\n2nd Main Rd, Domlur, Bangalore, Karnataka 560071, India. (Orchard Green)",
1311
+ "items": [
1312
+ {
1313
+ "name": "Cold Brew Black (Cold Brew)",
1314
+ "quantity": 1,
1315
+ "price": 390
1316
+ }
1317
+ ],
1318
+ "total_price": 401
1319
+ },
1320
+ {
1321
+ "email_number": 2,
1322
+ "order_date": "21-May-2025",
1323
+ "order_time": "12:58:49",
1324
+ "restaurant_name": "Starbucks Coffee",
1325
+ "delivery_address": "2C, Orchard Green Apartment, 2nd Main Rd, Domlur, Bangalore, Karnataka 560071, India. (Orchard Green)",
1326
+ "items": [
1327
+ {
1328
+ "name": "Caffe Americano",
1329
+ "quantity": 1,
1330
+ "price": 315
1331
+ }
1332
+ ],
1333
+ "total_price": 363
1334
+ },
1335
+ {
1336
+ "email_number": 3,
1337
+ "order_date": "21-May-2025",
1338
+ "order_time": "22:07:56",
1339
+ "restaurant_name": "Magnolia Bakery",
1340
+ "delivery_address": "2C, Orchard Green Apartment, 2nd Main Rd, Domlur, Bangalore, Karnataka 560071, India. (Orchard Green)",
1341
+ "items": [
1342
+ {
1343
+ "name": "Chocolate Cake with Chocolate Buttercream Cake Slice",
1344
+ "quantity": 1,
1345
+ "price": 260
1346
+ },
1347
+ {
1348
+ "name": "CLASSIC TRES LECHES",
1349
+ "quantity": 1,
1350
+ "price": 410
1351
+ }
1352
+ ],
1353
+ "total_price": 850
1354
+ }
1355
+ ],
1356
+ "2025-05-22": [
1357
+ {
1358
+ "email_number": 1,
1359
+ "order_date": "22-May-2025",
1360
+ "order_time": "12:00:34",
1361
+ "restaurant_name": "Yogisthaan",
1362
+ "delivery_address": "2C, Orchard Green Apartment, 2nd Main Rd, Domlur, Bangalore, Karnataka 560071, India. (Orchard Green)",
1363
+ "items": [
1364
+ {
1365
+ "name": "Poha (quick Of Breakfast)",
1366
+ "quantity": 1,
1367
+ "price": 288
1368
+ },
1369
+ {
1370
+ "name": "Tapioca (sabu Dana) Khichadi",
1371
+ "quantity": 1,
1372
+ "price": 252
1373
+ }
1374
+ ],
1375
+ "total_price": 547
1376
+ }
1377
+ ],
1378
+ "2025-05-23": []
1379
+ }
mcp/server/swiggy_scraper.py ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Reusable helper to fetch Swiggy order e-mails and return a list[dict].
3
+
4
+ Usage:
5
+ from swiggy_scraper import fetch_swiggy_orders
6
+ orders = fetch_swiggy_orders("17-May-2025", "20-May-2025")
7
+ """
8
+
9
+ import os, imaplib, json
10
+ from email import message_from_bytes
11
+ from bs4 import BeautifulSoup
12
+ from openai import OpenAI
13
+ from dotenv import load_dotenv
14
+ from datetime import datetime, timedelta
15
+ from email.utils import parsedate_to_datetime
16
+ from zoneinfo import ZoneInfo
17
+
18
+ from db_schema import init_db, get_orders_by_date_from_db, save_orders_to_db
19
+
20
+
21
+ load_dotenv()
22
+
23
+ APP_PASSWORD = os.getenv("APP_PASSWORD")
24
+ EMAIL_ID = os.getenv("EMAIL_ID")
25
+ OPENAI_KEY = os.getenv("OPENAI_API_KEY")
26
+
27
+ client = OpenAI(api_key=OPENAI_KEY)
28
+
29
+ def _imap_connect():
30
+ m = imaplib.IMAP4_SSL("imap.gmail.com")
31
+ m.login(EMAIL_ID, APP_PASSWORD)
32
+ m.select('"[Gmail]/All Mail"')
33
+ return m
34
+
35
+ def _email_to_clean_text(msg):
36
+ html = next(
37
+ (part.get_payload(decode=True).decode(errors="ignore")
38
+ for part in msg.walk()
39
+ if part.get_content_type() == "text/html"),
40
+ None,
41
+ )
42
+ if not html:
43
+ return ""
44
+ soup = BeautifulSoup(html, "html.parser")
45
+ for t in soup(["script", "style", "head", "meta", "link"]):
46
+ t.decompose()
47
+ return "\n".join(
48
+ line.strip() for line in soup.get_text("\n").splitlines() if line.strip()
49
+ )
50
+
51
+ def _get_all_dates(start_date: str, end_date: str):
52
+ start = datetime.strptime(start_date, "%d-%b-%Y")
53
+ end = datetime.strptime(end_date, "%d-%b-%Y")
54
+ delta = (end - start).days + 1
55
+ return [(start + timedelta(days=i)).strftime("%Y-%m-%d") for i in range(delta)]
56
+
57
+
58
+
59
+
60
+ def _extract_with_llm(email_number, subject, body, email_date, email_time):
61
+ current_email = {
62
+ "subject": subject,
63
+ "body": body
64
+ }
65
+
66
+ prompt = f"""
67
+ You are given a Swiggy order confirmation email with a subject and body.
68
+
69
+ Extract and return only the following:
70
+ - "restaurant_name": name of the restaurant
71
+ - "delivery_address": the delivery address
72
+ - "items": a list of ordered items, each with "name", "quantity", and "price" (number)
73
+ - "total_price": the total bill paid including taxes, charges, etc.
74
+
75
+ Example output format:
76
+ {{
77
+ "restaurant_name": "Dominos Pizza",
78
+ "delivery_address": "123 Main St, City",
79
+ "total_price": 567,
80
+ "items": [
81
+ {{ "name": "Veg Pizza", "quantity": 2, "price": 199 }},
82
+ {{ "name": "Coke", "quantity": 1, "price": 45 }}
83
+ ]
84
+ }}
85
+
86
+ Return only valid JSON. No extra text or comments.
87
+
88
+ {json.dumps(current_email, indent=2)}
89
+ """
90
+
91
+
92
+ try:
93
+ rsp = client.chat.completions.create(
94
+ model="gpt-4o-mini",
95
+ temperature=0,
96
+ messages=[
97
+ {"role": "system", "content": "You are a precise JSON extractor."},
98
+ {"role": "user", "content": prompt},
99
+ ],
100
+ )
101
+
102
+ # Attempt to parse the returned content
103
+ parsed_data = json.loads(rsp.choices[0].message.content)
104
+
105
+ # Wrap into final structure
106
+ final_output = {
107
+ "email_number": email_number,
108
+ "order_date": email_date,
109
+ "order_time": email_time,
110
+ "restaurant_name": parsed_data.get("restaurant_name", ""),
111
+ "delivery_address": parsed_data.get("delivery_address", ""),
112
+ "items": parsed_data.get("items", []),
113
+ "total_price": parsed_data.get("total_price", 0)
114
+ }
115
+
116
+
117
+ return final_output
118
+
119
+ except json.JSONDecodeError as json_err:
120
+ return {
121
+ "email_number": email_number,
122
+ "error": f"JSON decoding failed: {str(json_err)}",
123
+ "raw_response": rsp.choices[0].message.content if 'rsp' in locals() else None
124
+ }
125
+
126
+ except Exception as e:
127
+ return {
128
+ "email_number": email_number,
129
+ "error": f"Unexpected error: {str(e)}"
130
+ }
131
+
132
+
133
+
134
+ def fetch_swiggy_orders(start_date: str, end_date: str) -> list[dict]:
135
+ mail = _imap_connect()
136
+ all_dates = _get_all_dates(start_date, end_date)
137
+ orders = []
138
+
139
+ for date_str in all_dates:
140
+ # 1) Try loading from DB
141
+ day_orders = get_orders_by_date_from_db(date_str)
142
+ if day_orders:
143
+ print(f"{date_str} loaded from DB")
144
+ orders.extend(day_orders)
145
+ continue
146
+
147
+ # 2) Otherwise scrape emails for that date
148
+ print(f"Fetching Swiggy emails for {date_str}")
149
+ dt_obj = datetime.strptime(date_str, "%Y-%m-%d")
150
+ next_day = (dt_obj + timedelta(days=1)).strftime("%d-%b-%Y")
151
+ this_day = dt_obj.strftime("%d-%b-%Y")
152
+
153
+ crit = f'(FROM "noreply@swiggy.in") SINCE "{this_day}" BEFORE "{next_day}"'
154
+ _, data = mail.search(None, crit)
155
+ ids = data[0].split()
156
+
157
+ scraped_orders = []
158
+ for idx, eid in enumerate(ids, 1):
159
+ _, msg_data = mail.fetch(eid, "(RFC822)")
160
+ msg = message_from_bytes(msg_data[0][1])
161
+ subject = msg.get("Subject", "")
162
+ body_text = _email_to_clean_text(msg)
163
+
164
+ try:
165
+ dt_obj = parsedate_to_datetime(msg["Date"]).astimezone(ZoneInfo("Asia/Kolkata"))
166
+ email_date = dt_obj.strftime("%d-%b-%Y")
167
+ email_time = dt_obj.strftime("%H:%M:%S")
168
+
169
+ order = _extract_with_llm(idx, subject, body_text, email_date, email_time)
170
+ scraped_orders.append(order)
171
+ except Exception as exc:
172
+ scraped_orders.append({"email_number": idx, "error": str(exc)})
173
+
174
+ # 3) Save newly scraped data to DB
175
+ save_orders_to_db(date_str, scraped_orders)
176
+ orders.extend(scraped_orders)
177
+
178
+ mail.logout()
179
+ return orders
requirements.txt CHANGED
@@ -1,24 +1 @@
1
- # Core OAuth Gmail MCP Server Dependencies
2
- gradio[mcp]
3
- google-auth
4
- google-auth-oauthlib
5
- google-auth-httplib2
6
- google-api-python-client
7
- cryptography
8
- requests
9
- loguru
10
- python-dateutil
11
-
12
- uvicorn
13
-
14
- # MCP server support
15
- mcp
16
-
17
- # Email processing
18
- email-validator
19
- beautifulsoup4
20
- html2text
21
-
22
- # Development (optional)
23
- pytest
24
- black
 
1
+ huggingface_hub==0.25.2