rayuga2503 commited on
Commit
6f1d51c
·
verified ·
1 Parent(s): cc05d8d

Upload 2 files

Browse files
multi_tool_agent/.env ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ # Google Generative AI (Gemini) API Key
2
+ GOOGLE_API_KEY="AIzaSyDJK5_ZQ_b72ayti4z96k4kU6t6loGMoWs"
3
+
4
+ # Google Cloud OAuth Credentials JSON content
5
+ # This should be the entire content of your credentials.json file, enclosed in single quotes.
6
+ GMAIL_CREDENTIALS_JSON='{"installed":{"client_id":"1091473629975-ho2j2a2qhkppb6b2n385c18ci5f6hpft.apps.googleusercontent.com","project_id":"multi-agent-inbox","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"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","redirect_uris":["http://localhost"]}}'
multi_tool_agent/gmail_agent_logic.py ADDED
@@ -0,0 +1,597 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os.path
2
+ import base64
3
+ import os
4
+ import google.generativeai as genai
5
+ from email.mime.multipart import MIMEMultipart
6
+ from email.mime.text import MIMEText
7
+ import secrets
8
+
9
+ # --- Load .env variables ---
10
+ from dotenv import load_dotenv
11
+ load_dotenv()
12
+ # --- End Load .env variables ---
13
+
14
+ from google.adk.tools import FunctionTool
15
+
16
+ from google.auth.transport.requests import Request
17
+ from google.oauth2.credentials import Credentials
18
+ from google_auth_oauthlib.flow import Flow
19
+ from googleapiclient.discovery import build
20
+ from googleapiclient.errors import HttpError
21
+
22
+ # If modifying these scopes, delete the file token.json.
23
+ # --- Modified Scopes ---
24
+ SCOPES = ["https://www.googleapis.com/auth/gmail.modify"] # Changed to allow sending/replyingE"
25
+ # --- End Modified Scopes ---
26
+ print("IMPORTANT: If you changed SCOPES, delete token.json and re-authenticate.")
27
+
28
+ # Global storage for user sessions and credentials
29
+ user_sessions = {}
30
+
31
+ # --- Gemini Configuration ---
32
+ # NOTE: In a real agent, manage API keys and model initialization securely
33
+ # within the agent's setup or context.
34
+ try:
35
+ # Configure the Gemini API key (Example: Load from environment or secure config)
36
+ gemini_api_key = os.environ.get("GOOGLE_API_KEY") # Prioritize env var, remove default fallback for clarity
37
+
38
+ if not gemini_api_key:
39
+ # Make the error message clearer if key is not found
40
+ raise ValueError("GOOGLE_API_KEY not found in environment variables. Please set it in the .env file.")
41
+ genai.configure(api_key=gemini_api_key)
42
+
43
+ gemini_model = genai.GenerativeModel('gemini-2.5-flash-lite')
44
+ except ValueError as e:
45
+ print(f"Error configuring Gemini: {e}")
46
+ gemini_model = None
47
+ except Exception as e:
48
+ print(f"An unexpected error occurred during Gemini configuration: {e}")
49
+ gemini_model = None
50
+ # --- End Gemini Configuration ---
51
+
52
+ # --- New Web OAuth Functions ---
53
+ def create_oauth_flow():
54
+ """Creates and returns an OAuth flow for web authentication."""
55
+ if not os.path.exists("credentials.json"):
56
+ raise FileNotFoundError("credentials.json not found. Please ensure it's available.")
57
+
58
+ flow = Flow.from_client_secrets_file(
59
+ "credentials.json",
60
+ scopes=SCOPES,
61
+ redirect_uri="urn:ietf:wg:oauth:2.0:oob" # For manual code entry
62
+ )
63
+ return flow
64
+
65
+ def get_auth_url():
66
+ """Gets the authorization URL for OAuth flow."""
67
+ flow = create_oauth_flow()
68
+ auth_url, _ = flow.authorization_url(prompt='consent')
69
+ return auth_url, flow
70
+
71
+ def exchange_code_for_credentials(auth_code, flow):
72
+ """Exchanges authorization code for credentials."""
73
+ try:
74
+ flow.fetch_token(code=auth_code)
75
+ return flow.credentials
76
+ except Exception as e:
77
+ print(f"Error exchanging code for credentials: {e}")
78
+ return None
79
+
80
+ def store_user_credentials(session_id, credentials):
81
+ """Stores user credentials for a session."""
82
+ if credentials:
83
+ user_sessions[session_id] = {
84
+ 'credentials': credentials,
85
+ 'token_data': credentials.to_json()
86
+ }
87
+ return True
88
+ return False
89
+
90
+ def get_user_credentials(session_id):
91
+ """Gets user credentials for a session."""
92
+ session_data = user_sessions.get(session_id)
93
+ if session_data:
94
+ creds = session_data['credentials']
95
+ # Check if credentials need refresh
96
+ if creds.expired and creds.refresh_token:
97
+ try:
98
+ creds.refresh(Request())
99
+ # Update stored credentials
100
+ user_sessions[session_id]['credentials'] = creds
101
+ user_sessions[session_id]['token_data'] = creds.to_json()
102
+ return creds
103
+ except Exception as e:
104
+ print(f"Error refreshing credentials: {e}")
105
+ return None
106
+ return creds
107
+ return None
108
+
109
+ def create_session_id():
110
+ """Creates a unique session ID."""
111
+ return secrets.token_urlsafe(32)
112
+
113
+ def is_user_authenticated(session_id):
114
+ """Checks if user is authenticated for this session."""
115
+ creds = get_user_credentials(session_id)
116
+ return creds is not None and creds.valid
117
+
118
+ # --- Modified Gmail Service Function ---
119
+ def get_gmail_service(session_id=None):
120
+ """Authenticates and builds the Gmail API service for a specific user session."""
121
+ # Fallback for backward compatibility - try to use existing token.json
122
+ if session_id is None:
123
+ creds = None
124
+ if os.path.exists("token.json"):
125
+ creds = Credentials.from_authorized_user_file("token.json", SCOPES)
126
+ if creds and creds.valid:
127
+ try:
128
+ service = build("gmail", "v1", credentials=creds)
129
+ print("Gmail service built successfully using token.json")
130
+ return service
131
+ except Exception as e:
132
+ print(f"Failed to build Gmail service with token.json: {e}")
133
+ return None
134
+
135
+ # Use session-based credentials
136
+ creds = get_user_credentials(session_id)
137
+ if not creds:
138
+ return None
139
+
140
+ try:
141
+ service = build("gmail", "v1", credentials=creds)
142
+ print(f"Gmail service built successfully for session {session_id[:8]}...")
143
+ return service
144
+ except Exception as e:
145
+ print(f"Failed to build Gmail service for session: {e}")
146
+ return None
147
+
148
+ # --- Helper Function to Get Email Body ---
149
+ def get_email_body(payload):
150
+ """Parses the email payload to find the text body."""
151
+ body = ""
152
+ if "parts" in payload:
153
+ for part in payload["parts"]:
154
+ mime_type = part.get("mimeType", "")
155
+ if mime_type == "text/plain":
156
+ data = part.get("body", {}).get("data")
157
+ if data:
158
+ body = base64.urlsafe_b64decode(data).decode("utf-8")
159
+ break # Prefer plain text
160
+ elif mime_type == "text/html":
161
+ # Fallback to HTML if plain text not found yet
162
+ if not body:
163
+ data = part.get("body", {}).get("data")
164
+ if data:
165
+ body = base64.urlsafe_b64decode(data).decode("utf-8")
166
+ elif "parts" in part:
167
+ # Recursively check nested parts
168
+ nested_body = get_email_body(part)
169
+ if nested_body:
170
+ body = nested_body
171
+ if mime_type == "text/plain": # Prioritize plain text from nested parts
172
+ break
173
+ elif payload.get("mimeType") == "text/plain":
174
+ data = payload.get("body", {}).get("data")
175
+ if data:
176
+ body = base64.urlsafe_b64decode(data).decode("utf-8")
177
+
178
+ # If body is still empty, check the top-level body (for non-multipart emails)
179
+ if not body and payload.get("mimeType", "").startswith("text/"):
180
+ data = payload.get("body", {}).get("data")
181
+ if data:
182
+ body = base64.urlsafe_b64decode(data).decode("utf-8")
183
+
184
+ return body
185
+ # --- End Helper Function ---
186
+
187
+
188
+ # --- Added Function to List Recent Emails ---
189
+ def list_recent_emails(user_id: str, max_results: int, session_id: str = None) -> dict:
190
+ """Lists the most recent emails from the user's inbox.
191
+
192
+ Args:
193
+ user_id: The user's email address or 'me'.
194
+ max_results: The maximum number of emails to retrieve.
195
+ session_id: User session ID for authentication.
196
+
197
+ Returns:
198
+ A dictionary containing the 'status' ('success' or 'error'),
199
+ and either 'emails' (a list of email details) on success,
200
+ or 'error_message' on failure. Each email detail includes
201
+ 'id', 'threadId', 'subject', 'from', and 'date'.
202
+ """
203
+ service = get_gmail_service(session_id)
204
+ if not service:
205
+ return {"status": "error", "error_message": "Failed to get Gmail service."}
206
+
207
+ try:
208
+ # List messages
209
+ results = service.users().messages().list(
210
+ userId=user_id, labelIds=['INBOX'], maxResults=max_results
211
+ ).execute()
212
+ messages = results.get('messages', [])
213
+
214
+ if not messages:
215
+ return {"status": "success", "emails": []} # Return success with empty list
216
+
217
+ email_list = []
218
+ for msg_stub in messages:
219
+ msg_id = msg_stub['id']
220
+ # Fetch metadata for each message
221
+ msg = service.users().messages().get(
222
+ userId=user_id, id=msg_id, format='metadata',
223
+ metadataHeaders=['Subject', 'From', 'Date']
224
+ ).execute()
225
+
226
+ payload = msg.get('payload', {})
227
+ headers = payload.get('headers', [])
228
+ subject = 'No Subject'
229
+ sender = 'Unknown Sender'
230
+ date = 'No Date'
231
+
232
+ for header in headers:
233
+ name = header['name'].lower()
234
+ if name == 'subject':
235
+ subject = header['value']
236
+ elif name == 'from':
237
+ sender = header['value']
238
+ elif name == 'date':
239
+ date = header['value']
240
+
241
+ email_list.append({
242
+ 'id': msg_id,
243
+ 'threadId': msg.get('threadId'),
244
+ 'subject': subject,
245
+ 'from': sender,
246
+ 'date': date
247
+ })
248
+
249
+ return {"status": "success", "emails": email_list}
250
+
251
+ except HttpError as error:
252
+ return {"status": "error", "error_message": f"An API error occurred listing emails: {error}"}
253
+ except Exception as e:
254
+ return {"status": "error", "error_message": f"An unexpected error occurred listing emails: {e}"}
255
+ # --- End Added Function to List Recent Emails ---
256
+
257
+
258
+ # --- Summarization Function ---
259
+ def summarize_email_with_gemini(user_id: str, email_id: str, session_id: str = None) -> dict:
260
+ """Fetches a specific email by its ID and summarizes its content using an LLM.
261
+ Requires Gmail service to be available via get_gmail_service().
262
+
263
+ Args:
264
+ user_id: The user's email address or 'me'.
265
+ email_id: The ID of the email message to summarize.
266
+
267
+ Returns:
268
+ A dictionary containing the 'status' ('success' or 'error'),
269
+ and either 'summary', 'subject', 'original_body', 'sender_email',
270
+ 'thread_id', 'original_message_id', 'references' on success,
271
+ or 'error_message' on failure.
272
+ """
273
+ service = get_gmail_service(session_id) # Get service when function is called
274
+ if not service:
275
+ return {"status": "error", "error_message": "Failed to get Gmail service."}
276
+ if not gemini_model:
277
+ return {"status": "error", "error_message": "Gemini model not initialized."}
278
+
279
+ try:
280
+ # Get the full email content
281
+ message = service.users().messages().get(userId=user_id, id=email_id, format='full').execute()
282
+ payload = message.get('payload', {})
283
+ thread_id = message.get('threadId') # Get thread ID
284
+
285
+ # Extract headers
286
+ headers = payload.get('headers', [])
287
+ subject = 'No Subject'
288
+ sender_email = ''
289
+ original_message_id = ''
290
+ references = ''
291
+ for header in headers:
292
+ name = header['name'].lower()
293
+ if name == 'subject':
294
+ subject = header['value']
295
+ elif name == 'from':
296
+ if '<' in header['value'] and '>' in header['value']:
297
+ sender_email = header['value'][header['value'].find('<')+1:header['value'].find('>')]
298
+ else:
299
+ sender_email = header['value'] # Handle cases without <>
300
+ elif name == 'message-id':
301
+ original_message_id = header['value']
302
+ elif name == 'references':
303
+ references = header['value']
304
+
305
+
306
+ # Extract body
307
+ email_body = get_email_body(payload)
308
+
309
+ if not email_body:
310
+ return {"status": "error", "error_message": "Could not extract email body."}
311
+
312
+ # Summarize using Gemini
313
+ prompt = f"Summarize the following email concisely:\\n\\nSubject: {subject}\\n\\nBody:\\n{email_body[:3000]}\\n\\nSummary:" # Limit body length
314
+ # --- ADDED DEBUG ---
315
+ # Print the first 500 chars of the prompt to check its content
316
+ # --- END ADDED DEBUG ---
317
+ response = gemini_model.generate_content(prompt)
318
+
319
+ return {
320
+ "status": "success",
321
+ "summary": response.text,
322
+ "subject": subject,
323
+ "original_body": email_body,
324
+ "sender_email": sender_email, # Add sender
325
+ "thread_id": thread_id, # Add thread ID
326
+ "original_message_id": original_message_id, # Add message ID
327
+ "references": references # Add references
328
+ }
329
+
330
+ except HttpError as error:
331
+ return {"status": "error", "error_message": f"An API error occurred fetching email {email_id}: {error}"}
332
+ except Exception as e:
333
+ # Include the specific exception type and message in the error
334
+ error_type = type(e).__name__
335
+ return {"status": "error", "error_message": f"An unexpected error occurred during summarization: {error_type}: {e}"}
336
+ # --- End Summarization Function ---
337
+
338
+
339
+ # --- Added Function to Generate Reply ---
340
+ def generate_reply_with_gemini(original_subject: str, original_body: str) -> dict:
341
+ """Generates a draft reply email body using an LLM based on the original email.
342
+
343
+ Args:
344
+ original_subject: The subject line of the email being replied to.
345
+ original_body: The body content of the email being replied to.
346
+
347
+ Returns:
348
+ A dictionary containing the 'status' ('success' or 'error'),
349
+ and either 'reply_body' on success or 'error_message' on failure.
350
+ """
351
+ if not gemini_model:
352
+ return {"status": "error", "error_message": "Gemini model not initialized."}
353
+ if not original_body:
354
+ return {"status": "error", "error_message": "Cannot generate reply without original email body."}
355
+
356
+ try:
357
+ prompt = f"""Generate a helpful and concise reply draft for the following email.
358
+ Keep the reply professional and address the main points. Do not include greetings or closings like "Hi" or "Best regards".
359
+
360
+ Original Email Subject: {original_subject}
361
+ Original Email Body:
362
+ ---
363
+ {original_body[:2000]}
364
+ ---
365
+
366
+ Generated Reply Draft:""" # Limit body length
367
+
368
+ response = gemini_model.generate_content(prompt)
369
+ return {"status": "success", "reply_body": response.text}
370
+
371
+ except Exception as e:
372
+ # Add print statement for debugging
373
+ error_type = type(e).__name__
374
+ return {"status": "error", "error_message": f"An error occurred during reply generation: {error_type}: {e}"}
375
+ # --- End Added Function to Generate Reply ---
376
+
377
+
378
+ # --- Added Function to Create Reply Message ---
379
+ def create_reply_message(sender, to, subject, reply_body, thread_id, original_message_id, references):
380
+ """Create a MIME message for replying to an email thread."""
381
+ message = MIMEMultipart('related')
382
+ message['to'] = to
383
+ message['from'] = sender
384
+ message['subject'] = subject
385
+
386
+ # Set threading headers
387
+ message['In-Reply-To'] = original_message_id
388
+ message['References'] = references if references else original_message_id
389
+
390
+ # Attach the reply body as plain text
391
+ msg_text = MIMEText(reply_body, 'plain')
392
+ message.attach(msg_text)
393
+
394
+ raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode('utf-8')
395
+ return {'raw': raw_message, 'threadId': thread_id}
396
+ # --- End Added Function to Create Reply Message ---
397
+
398
+
399
+ # --- Added Function to Send Reply ---
400
+ def send_reply(user_id: str, to: str, sender: str, subject: str, reply_body: str, thread_id: str, original_message_id: str, references: str, session_id: str = None) -> dict:
401
+ """Creates and sends a reply email within a specific thread.
402
+ Requires Gmail service to be available via get_gmail_service().
403
+
404
+ Args:
405
+ user_id: The user's email address or 'me'.
406
+ to: The recipient's email address.
407
+ sender: The sender's email address (should be the authenticated user).
408
+ subject: The subject line for the reply email.
409
+ reply_body: The plain text content of the reply.
410
+ thread_id: The ID of the thread to reply within.
411
+ original_message_id: The Message-ID header of the message being replied to.
412
+ references: The References header content for threading.
413
+
414
+ Returns:
415
+ A dictionary containing the 'status' ('success' or 'error'),
416
+ and either 'message_id' on success or 'error_message' on failure.
417
+ """
418
+ service = get_gmail_service(session_id) # Get service when function is called
419
+ if not service:
420
+ return {"status": "error", "error_message": "Failed to get Gmail service."}
421
+ try:
422
+ # Determine sender's actual email if 'me' is used
423
+ if sender.lower() == 'me':
424
+ profile = service.users().getProfile(userId='me').execute()
425
+ actual_sender = profile.get('emailAddress')
426
+ if not actual_sender:
427
+ return {"status": "error", "error_message": "Could not determine sender email address from profile."}
428
+ else:
429
+ actual_sender = sender
430
+
431
+ # Ensure subject starts with Re: if it's a reply
432
+ reply_subject = subject
433
+ if not subject.lower().startswith("re:"):
434
+ reply_subject = f"Re: {subject}"
435
+
436
+ # Construct references header
437
+ new_references = f"{references} {original_message_id}".strip() if references else original_message_id
438
+
439
+ reply_message_dict = create_reply_message(
440
+ sender=actual_sender, # Use actual sender email
441
+ to=to,
442
+ subject=reply_subject, # Use adjusted subject
443
+ reply_body=reply_body,
444
+ thread_id=thread_id,
445
+ original_message_id=original_message_id,
446
+ references=new_references # Use constructed references
447
+ )
448
+ message = service.users().messages().send(userId=user_id, body=reply_message_dict).execute()
449
+ print(f"Reply sent successfully. Message ID: {message['id']}") # Keep console log for now
450
+ return {"status": "success", "message_id": message['id']}
451
+ except HttpError as error:
452
+ print(f"An error occurred sending the reply: {error}") # Keep console log
453
+ return {"status": "error", "error_message": f"An API error occurred sending the reply: {error}"}
454
+ except Exception as e:
455
+ print(f"An unexpected error occurred sending the reply: {e}") # Keep console log
456
+ return {"status": "error", "error_message": f"An unexpected error occurred sending the reply: {e}"}
457
+ # --- End Added Function to Send Reply ---
458
+
459
+
460
+ # --- Added Function to Search Emails ---
461
+ def search_emails(query: str, user_id: str, session_id: str = None) -> dict:
462
+ """Searches for emails matching the given query.
463
+
464
+ Args:
465
+ query: The search query string (e.g., 'from:someone subject:report').
466
+ user_id: The user's email address or 'me'.
467
+ max_results: The maximum number of emails to retrieve.
468
+
469
+ Returns:
470
+ A dictionary containing the 'status' ('success' or 'error'),
471
+ and either 'emails' (a list of matching email details) on success,
472
+ or 'error_message' on failure. Each email detail includes
473
+ 'id', 'threadId', 'subject', 'from', and 'date'.
474
+ """
475
+ service = get_gmail_service(session_id)
476
+ if not service:
477
+ return {"status": "error", "error_message": "Failed to get Gmail service."}
478
+
479
+ try:
480
+ # Search messages using the query
481
+ results = service.users().messages().list(
482
+ userId=user_id, q=query, maxResults=5
483
+ ).execute()
484
+ messages = results.get('messages', [])
485
+
486
+ if not messages:
487
+ return {"status": "success", "emails": []} # Return success with empty list if no matches
488
+
489
+ email_list = []
490
+ for msg_stub in messages:
491
+ msg_id = msg_stub['id']
492
+ # Fetch metadata for each message
493
+ msg = service.users().messages().get(
494
+ userId=user_id, id=msg_id, format='metadata',
495
+ metadataHeaders=['Subject', 'From', 'Date']
496
+ ).execute()
497
+
498
+ payload = msg.get('payload', {})
499
+ headers = payload.get('headers', [])
500
+ subject = 'No Subject'
501
+ sender = 'Unknown Sender'
502
+ date = 'No Date'
503
+
504
+ for header in headers:
505
+ name = header['name'].lower()
506
+ if name == 'subject':
507
+ subject = header['value']
508
+ elif name == 'from':
509
+ sender = header['value']
510
+ elif name == 'date':
511
+ date = header['value']
512
+
513
+ email_list.append({
514
+ 'id': msg_id,
515
+ 'threadId': msg.get('threadId'),
516
+ 'subject': subject,
517
+ 'from': sender,
518
+ 'date': date
519
+ })
520
+
521
+ return {"status": "success", "emails": email_list}
522
+
523
+ except HttpError as error:
524
+ return {"status": "error", "error_message": f"An API error occurred searching emails: {error}"}
525
+ except Exception as e:
526
+ return {"status": "error", "error_message": f"An unexpected error occurred searching emails: {e}"}
527
+ # --- End Added Function to Search Emails ---
528
+
529
+
530
+ # --- Added Function to Get Unread Count ---
531
+ def get_total_unread_count(user_id: str, session_id: str = None) -> dict:
532
+ """Gets the total number of unread messages in the inbox.
533
+
534
+ Args:
535
+ user_id: The user's email address or 'me'.
536
+
537
+ Returns:
538
+ A dictionary containing the 'status' ('success' or 'error'),
539
+ and either 'unread_count' on success or 'error_message' on failure.
540
+ """
541
+ service = get_gmail_service(session_id)
542
+ if not service:
543
+ return {"status": "error", "error_message": "Failed to get Gmail service."}
544
+ try:
545
+ # Get the INBOX label details
546
+ label_info = service.users().labels().get(userId=user_id, id='INBOX').execute()
547
+ unread_count = label_info.get('messagesUnread', 0)
548
+ return {"status": "success", "unread_count": unread_count}
549
+ except HttpError as error:
550
+ return {"status": "error", "error_message": f"An API error occurred getting unread count: {error}"}
551
+ except Exception as e:
552
+ return {"status": "error", "error_message": f"An unexpected error occurred getting unread count: {e}"}
553
+ # --- End Added Function ---
554
+
555
+ # --- Added Function to Get Today's Email Count ---
556
+ def get_emails_received_today_count(user_id: str, session_id: str = None) -> dict:
557
+ """Gets the count of emails received in the inbox within the last 24 hours (approximates 'today').
558
+
559
+ Args:
560
+ user_id: The user's email address or 'me'.
561
+
562
+ Returns:
563
+ A dictionary containing the 'status' ('success' or 'error'),
564
+ and either 'today_count' on success or 'error_message' on failure.
565
+ """
566
+ service = get_gmail_service(session_id)
567
+ if not service:
568
+ return {"status": "error", "error_message": "Failed to get Gmail service."}
569
+ try:
570
+ # Use a query to find messages newer than 1 day in the inbox
571
+ # Note: 'newer_than:1d' typically covers the last 24 hours.
572
+ query = "label:inbox newer_than:1d"
573
+ results = service.users().messages().list(userId=user_id, q=query).execute()
574
+ messages = results.get('messages', [])
575
+ # The result only contains message stubs, count them.
576
+ today_count = len(messages)
577
+ # For very large counts, results might be paginated, estimate might be needed.
578
+ # estimated_count = results.get('resultSizeEstimate', 0) # Alternative if needed
579
+ return {"status": "success", "today_count": today_count}
580
+ except HttpError as error:
581
+ return {"status": "error", "error_message": f"An API error occurred counting today's emails: {error}"}
582
+ except Exception as e:
583
+ return {"status": "error", "error_message": f"An unexpected error occurred counting today's emails: {e}"}
584
+ # --- End Added Function ---
585
+
586
+ # --- REMOVE OLD TOOL BINDINGS ---
587
+ # list_emails_tool = list_recent_emails
588
+ # summarize_email_tool = summarize_email_with_gemini
589
+ # generate_reply_tool = generate_reply_with_gemini
590
+ # send_reply_tool = send_reply
591
+ # search_emails_tool = search_emails
592
+ # get_gmail_service = get_gmail_service
593
+ #
594
+ # print("ADK Function Tools created")
595
+ # --- END REMOVED BINDINGS ---
596
+
597
+