Da-123 commited on
Commit
b0ee7e5
·
verified ·
1 Parent(s): e7ad868

agentic-base (#4)

Browse files

- first commit (9b406094e66833776ff788397b51f7ba733fcdc1)
- Merge branch 'pr/4' into agentic-base (9125b1add395861a3af9d6d414c4514745d0d90c)
- agentic_base made (4f321e93870d8b10efa6c4587ab58585217018e2)

agentic_implementation/agent.py ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # agent.py
2
+
3
+ import json
4
+ from typing import Dict, Any
5
+
6
+ from re_act import (
7
+ get_plan_from_llm,
8
+ think,
9
+ act,
10
+ store_name_email_mapping,
11
+ extract_sender_info,
12
+ client
13
+ )
14
+ from schemas import PlanStep
15
+ from logger import logger # from logger.py
16
+
17
+
18
+ def run_agent():
19
+ """
20
+ Main REPL loop for the email agent.
21
+ """
22
+ logger.info("Starting Email Agent REPL...")
23
+ print("🤖 Email Agent ready. Type 'exit' to quit.\n")
24
+
25
+ while True:
26
+ try:
27
+ user_query = input("🗨 You: ").strip()
28
+ logger.info("Received user input: %s", user_query)
29
+
30
+ if user_query.lower() in ("exit", "quit"):
31
+ logger.info("Exit command detected, shutting down agent.")
32
+ print("👋 Goodbye!")
33
+ break
34
+
35
+ # 1) Generate plan
36
+ try:
37
+ plan = get_plan_from_llm(user_query)
38
+ logger.debug("Generated plan: %s", plan)
39
+ except Exception as e:
40
+ logger.error("Failed to generate plan: %s", e)
41
+ print(f"❌ Could not generate a plan: {e}")
42
+ continue
43
+
44
+ # print plan for user transparency
45
+ print("\n\n plan:")
46
+ print(plan)
47
+ print("\n\n")
48
+
49
+ results: Dict[str, Any] = {}
50
+
51
+ # 2) Execute each plan step
52
+ for step in plan.plan:
53
+ logger.info("Processing step: %s", step.action)
54
+
55
+ if step.action == "done":
56
+ logger.info("Encountered 'done' action. Plan complete.")
57
+ print("✅ Plan complete.")
58
+ break
59
+
60
+ try:
61
+ should_run, updated_step, user_prompt = think(step, results, user_query)
62
+ logger.debug(
63
+ "Think outcome for '%s': should_run=%s, updated_step=%s, user_prompt=%s",
64
+ step.action, should_run, updated_step, user_prompt
65
+ )
66
+ except Exception as e:
67
+ logger.error("Error in think() for step '%s': %s", step.action, e)
68
+ print(f"❌ Error in planning step '{step.action}': {e}")
69
+ break
70
+
71
+ # Handle user prompt (e.g., missing email mapping)
72
+ if user_prompt:
73
+ logger.info("User prompt required: %s", user_prompt)
74
+ print(f"❓ {user_prompt}")
75
+ user_input = input("📧 Email: ").strip()
76
+
77
+ try:
78
+ sender_info = extract_sender_info(user_query)
79
+ sender_intent = sender_info.get("sender_intent", "")
80
+ store_name_email_mapping(sender_intent, user_input)
81
+ logger.info("Stored mapping: %s -> %s", sender_intent, user_input)
82
+ print(f"✅ Stored mapping: {sender_intent} → {user_input}")
83
+
84
+ # Retry current step
85
+ should_run, updated_step, _ = think(step, results, user_query)
86
+ logger.debug(
87
+ "Post-mapping think outcome: should_run=%s, updated_step=%s",
88
+ should_run, updated_step
89
+ )
90
+ except Exception as e:
91
+ logger.error("Error storing mapping or retrying step '%s': %s", step.action, e)
92
+ print(f"❌ Error storing mapping or retrying step: {e}")
93
+ break
94
+
95
+ if not should_run:
96
+ logger.info("Skipping step: %s", step.action)
97
+ print(f"⏭️ Skipping `{step.action}`")
98
+ continue
99
+
100
+ # Execute the action
101
+ try:
102
+ output = act(updated_step)
103
+ results[updated_step.action] = output
104
+ logger.info("Action '%s' executed successfully.", updated_step.action)
105
+ print(f"🔧 Ran `{updated_step.action}` → {output}")
106
+ except Exception as e:
107
+ logger.error("Error executing action '%s': %s", updated_step.action, e)
108
+ print(f"❌ Error running `{updated_step.action}`: {e}")
109
+ break
110
+
111
+ # 3) Summarize results via LLM
112
+ try:
113
+ summary_rsp = client.chat.completions.create(
114
+ model="gpt-4o-mini",
115
+ temperature=0.0,
116
+ messages=[
117
+ {"role": "system", "content": "Summarize these results for the user in a friendly way."},
118
+ {"role": "assistant", "content": json.dumps(results)}
119
+ ],
120
+ )
121
+ summary = summary_rsp.choices[0].message.content
122
+ logger.info("Summary generated successfully.")
123
+ print("\n📋 Summary:\n" + summary)
124
+ except Exception as e:
125
+ logger.error("Failed to generate summary: %s", e)
126
+ print("\n❌ Failed to generate summary.")
127
+
128
+ print("\nAnything else I can help you with?\n")
129
+
130
+ except KeyboardInterrupt:
131
+ logger.info("KeyboardInterrupt received, shutting down.")
132
+ print("\n👋 Goodbye!")
133
+ break
134
+ except Exception as e:
135
+ logger.exception("Unexpected error in REPL loop: %s", e)
136
+ print(f"❌ Unexpected error: {e}")
137
+ continue
138
+
139
+
140
+ if __name__ == "__main__":
141
+ run_agent()
agentic_implementation/email_db.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
3
+ "emails": [
4
+ {
5
+ "date": "07-Jun-2025",
6
+ "time": "16:42:51",
7
+ "subject": "testing",
8
+ "content": "hi bro",
9
+ "message_id": "<CAPziNCaSuVqpqNNfsRjhVbx22jN_vos3EGK_Odt-8WiD0HRKKQ@mail.gmail.com>"
10
+ }
11
+ ],
12
+ "last_scraped": "08-Jun-2025"
13
+ }
14
+ }
agentic_implementation/email_scraper.py ADDED
@@ -0,0 +1,267 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Enhanced Email Scraper with Intelligent Caching
4
+ """
5
+
6
+ import os
7
+ import imaplib
8
+ import json
9
+ from email import message_from_bytes
10
+ from bs4 import BeautifulSoup
11
+ from datetime import datetime, timedelta
12
+ from dotenv import load_dotenv
13
+ from zoneinfo import ZoneInfo
14
+ from email.utils import parsedate_to_datetime
15
+ from typing import List, Dict
16
+
17
+ load_dotenv()
18
+
19
+ # Email credentials
20
+ APP_PASSWORD = os.getenv("APP_PASSWORD")
21
+ EMAIL_ID = os.getenv("EMAIL_ID")
22
+ EMAIL_DB_FILE = "email_db.json"
23
+
24
+ def _imap_connect():
25
+ """Connect to Gmail IMAP server"""
26
+ try:
27
+ mail = imaplib.IMAP4_SSL("imap.gmail.com")
28
+ mail.login(EMAIL_ID, APP_PASSWORD)
29
+ mail.select('"[Gmail]/All Mail"')
30
+ return mail
31
+ except Exception as e:
32
+ print(f"IMAP connection failed: {e}")
33
+ raise
34
+
35
+ def _email_to_clean_text(msg):
36
+ """Extract clean text from email message"""
37
+ # Try HTML first
38
+ html_content = None
39
+ text_content = None
40
+
41
+ if msg.is_multipart():
42
+ for part in msg.walk():
43
+ content_type = part.get_content_type()
44
+ if content_type == "text/html":
45
+ try:
46
+ html_content = part.get_payload(decode=True).decode(errors="ignore")
47
+ except:
48
+ continue
49
+ elif content_type == "text/plain":
50
+ try:
51
+ text_content = part.get_payload(decode=True).decode(errors="ignore")
52
+ except:
53
+ continue
54
+ else:
55
+ # Non-multipart message
56
+ content_type = msg.get_content_type()
57
+ try:
58
+ content = msg.get_payload(decode=True).decode(errors="ignore")
59
+ if content_type == "text/html":
60
+ html_content = content
61
+ else:
62
+ text_content = content
63
+ except:
64
+ pass
65
+
66
+ # Clean HTML content
67
+ if html_content:
68
+ soup = BeautifulSoup(html_content, "html.parser")
69
+ # Remove script and style elements
70
+ for script in soup(["script", "style"]):
71
+ script.decompose()
72
+ return soup.get_text(separator=' ', strip=True)
73
+ elif text_content:
74
+ return text_content.strip()
75
+ else:
76
+ return ""
77
+
78
+ def _load_email_db() -> Dict:
79
+ """Load email database from file"""
80
+ if not os.path.exists(EMAIL_DB_FILE):
81
+ return {}
82
+ try:
83
+ with open(EMAIL_DB_FILE, "r") as f:
84
+ return json.load(f)
85
+ except (json.JSONDecodeError, IOError):
86
+ print(f"Warning: Could not load {EMAIL_DB_FILE}, starting with empty database")
87
+ return {}
88
+
89
+ def _save_email_db(db: Dict):
90
+ """Save email database to file"""
91
+ try:
92
+ with open(EMAIL_DB_FILE, "w") as f:
93
+ json.dump(db, f, indent=2)
94
+ except IOError as e:
95
+ print(f"Error saving database: {e}")
96
+ raise
97
+
98
+ def _date_to_imap_format(date_str: str) -> str:
99
+ """Convert DD-MMM-YYYY to IMAP date format"""
100
+ try:
101
+ dt = datetime.strptime(date_str, "%d-%b-%Y")
102
+ return dt.strftime("%d-%b-%Y")
103
+ except ValueError:
104
+ raise ValueError(f"Invalid date format: {date_str}. Expected DD-MMM-YYYY")
105
+
106
+ def _is_date_in_range(email_date: str, start_date: str, end_date: str) -> bool:
107
+ """Check if email date is within the specified range"""
108
+ try:
109
+ email_dt = datetime.strptime(email_date, "%d-%b-%Y")
110
+ start_dt = datetime.strptime(start_date, "%d-%b-%Y")
111
+ end_dt = datetime.strptime(end_date, "%d-%b-%Y")
112
+ return start_dt <= email_dt <= end_dt
113
+ except ValueError:
114
+ return False
115
+
116
+ def scrape_emails_from_sender(sender_email: str, start_date: str, end_date: str) -> List[Dict]:
117
+ """
118
+ Scrape emails from specific sender within date range
119
+ Uses intelligent caching to avoid re-scraping
120
+ """
121
+ print(f"Scraping emails from {sender_email} between {start_date} and {end_date}")
122
+
123
+ # Load existing database
124
+ db = _load_email_db()
125
+ sender_email = sender_email.lower().strip()
126
+
127
+ # Check if we have cached emails for this sender
128
+ if sender_email in db:
129
+ cached_emails = db[sender_email].get("emails", [])
130
+
131
+ # Filter cached emails by date range
132
+ filtered_emails = [
133
+ email for email in cached_emails
134
+ if _is_date_in_range(email["date"], start_date, end_date)
135
+ ]
136
+
137
+ # Check if we need to scrape more recent emails
138
+ last_scraped = db[sender_email].get("last_scraped", "01-Jan-2020")
139
+ today = datetime.today().strftime("%d-%b-%Y")
140
+
141
+ if last_scraped == today and filtered_emails:
142
+ print(f"Using cached emails (last scraped: {last_scraped})")
143
+ return filtered_emails
144
+
145
+ # Need to scrape emails
146
+ try:
147
+ mail = _imap_connect()
148
+
149
+ # Prepare IMAP search criteria
150
+ start_imap = _date_to_imap_format(start_date)
151
+ # Add one day to end_date for BEFORE criteria (IMAP BEFORE is exclusive)
152
+ end_dt = datetime.strptime(end_date, "%d-%b-%Y") + timedelta(days=1)
153
+ end_imap = end_dt.strftime("%d-%b-%Y")
154
+
155
+ search_criteria = f'(FROM "{sender_email}") SINCE "{start_imap}" BEFORE "{end_imap}"'
156
+ print(f"IMAP search: {search_criteria}")
157
+
158
+ # Search for emails
159
+ status, data = mail.search(None, search_criteria)
160
+ if status != 'OK':
161
+ raise Exception(f"IMAP search failed: {status}")
162
+
163
+ email_ids = data[0].split()
164
+ print(f"Found {len(email_ids)} emails")
165
+
166
+ scraped_emails = []
167
+
168
+ # Process each email
169
+ for i, email_id in enumerate(email_ids):
170
+ try:
171
+ print(f"Processing email {i+1}/{len(email_ids)}")
172
+
173
+ # Fetch email
174
+ status, msg_data = mail.fetch(email_id, "(RFC822)")
175
+ if status != 'OK':
176
+ continue
177
+
178
+ # Parse email
179
+ msg = message_from_bytes(msg_data[0][1])
180
+
181
+ # Extract information
182
+ subject = msg.get("Subject", "No Subject")
183
+ content = _email_to_clean_text(msg)
184
+
185
+ # Parse date
186
+ date_header = msg.get("Date", "")
187
+ if date_header:
188
+ try:
189
+ dt_obj = parsedate_to_datetime(date_header)
190
+ # Convert to IST
191
+ ist_dt = dt_obj.astimezone(ZoneInfo("Asia/Kolkata"))
192
+ email_date = ist_dt.strftime("%d-%b-%Y")
193
+ email_time = ist_dt.strftime("%H:%M:%S")
194
+ except:
195
+ email_date = datetime.today().strftime("%d-%b-%Y")
196
+ email_time = "00:00:00"
197
+ else:
198
+ email_date = datetime.today().strftime("%d-%b-%Y")
199
+ email_time = "00:00:00"
200
+
201
+ # Get message ID for deduplication
202
+ message_id = msg.get("Message-ID", f"missing-{email_id.decode()}")
203
+
204
+ scraped_emails.append({
205
+ "date": email_date,
206
+ "time": email_time,
207
+ "subject": subject,
208
+ "content": content[:2000], # Limit content length
209
+ "message_id": message_id
210
+ })
211
+
212
+ except Exception as e:
213
+ print(f"Error processing email {email_id}: {e}")
214
+ continue
215
+
216
+ mail.logout()
217
+
218
+ # Update database
219
+ if sender_email not in db:
220
+ db[sender_email] = {"emails": [], "last_scraped": ""}
221
+
222
+ # Merge with existing emails (avoid duplicates)
223
+ existing_emails = db[sender_email].get("emails", [])
224
+ existing_ids = {email.get("message_id") for email in existing_emails}
225
+
226
+ new_emails = [
227
+ email for email in scraped_emails
228
+ if email["message_id"] not in existing_ids
229
+ ]
230
+
231
+ # Update database
232
+ db[sender_email]["emails"] = existing_emails + new_emails
233
+ db[sender_email]["last_scraped"] = datetime.today().strftime("%d-%b-%Y")
234
+
235
+ # Save database
236
+ _save_email_db(db)
237
+
238
+ # Return filtered results
239
+ all_emails = db[sender_email]["emails"]
240
+ filtered_emails = [
241
+ email for email in all_emails
242
+ if _is_date_in_range(email["date"], start_date, end_date)
243
+ ]
244
+
245
+ print(f"Scraped {len(new_emails)} new emails, returning {len(filtered_emails)} in date range")
246
+ return filtered_emails
247
+
248
+ except Exception as e:
249
+ print(f"Email scraping failed: {e}")
250
+ raise
251
+
252
+ # Test the scraper
253
+ if __name__ == "__main__":
254
+ # Test scraping
255
+ try:
256
+ emails = scrape_emails_from_sender(
257
258
+ "01-Jun-2025",
259
+ "07-Jun-2025"
260
+ )
261
+
262
+ print(f"\nFound {len(emails)} emails:")
263
+ for email in emails[:3]: # Show first 3
264
+ print(f"- {email['date']} {email['time']}: {email['subject']}")
265
+
266
+ except Exception as e:
267
+ print(f"Test failed: {e}")
agentic_implementation/logger.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import sys
3
+
4
+ def configure_logger(name: str = None, level: int = logging.INFO) -> logging.Logger:
5
+ """
6
+ Configure and return a logger with a StreamHandler to stdout.
7
+ Use this function in any module to get a consistent logger.
8
+ """
9
+ logger = logging.getLogger(name)
10
+ logger.setLevel(level)
11
+
12
+ # Avoid adding multiple handlers to the same logger
13
+ if not logger.handlers:
14
+ handler = logging.StreamHandler(sys.stdout)
15
+ handler.setLevel(level)
16
+ formatter = logging.Formatter(
17
+ '%(asctime)s %(levelname)s %(name)s - %(message)s'
18
+ )
19
+ handler.setFormatter(formatter)
20
+ logger.addHandler(handler)
21
+
22
+ return logger
23
+
24
+ # Module-level default logger
25
+ logger = configure_logger()
agentic_implementation/name_mapping.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ {
2
+ "dev agarwal": "[email protected]"
3
+ }
agentic_implementation/re_act.py ADDED
@@ -0,0 +1,245 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # orchestrator.py
2
+
3
+ import os
4
+ import json
5
+ import re
6
+ from typing import Any, Dict, Tuple, Optional
7
+ from datetime import datetime
8
+
9
+ from dotenv import load_dotenv
10
+ from openai import OpenAI
11
+
12
+ from schemas import Plan, PlanStep, FetchEmailsParams
13
+ from tools import TOOL_MAPPING
14
+
15
+ # Load .env and initialize OpenAI client
16
+ load_dotenv()
17
+ api_key = os.getenv("OPENAI_API_KEY")
18
+ if not api_key:
19
+ raise RuntimeError("Missing OPENAI_API_KEY in environment")
20
+ client = OpenAI(api_key=api_key)
21
+
22
+ # File paths for name mapping
23
+ NAME_MAPPING_FILE = "name_mapping.json"
24
+
25
+ # === Hard-coded list of available actions ===
26
+ SYSTEM_PLAN_PROMPT = """
27
+ You are an email assistant agent. You have access to the following actions:
28
+
29
+ • fetch_emails - fetch emails based on sender and date criteria (includes date extraction)
30
+ • show_email - display specific email content
31
+ • analyze_emails - analyze email patterns or content
32
+ • draft_reply - create a reply to an email
33
+ • send_reply - send a drafted reply
34
+ • done - complete the task
35
+
36
+ When the user gives you a query, output _only_ valid JSON of this form:
37
+
38
+ {
39
+ "plan": [
40
+ "fetch_emails",
41
+ ...,
42
+ "done"
43
+ ]
44
+ }
45
+
46
+ Rules:
47
+ - Use "fetch_emails" when you need to retrieve emails (it automatically handles date extraction)
48
+ - The final entry _must_ be "done"
49
+ - If no tool is needed, return `{"plan":["done"]}`
50
+
51
+ Example: For "show me emails from dev today" → ["fetch_emails", "done"]
52
+ """
53
+
54
+ SYSTEM_VALIDATOR_TEMPLATE = """
55
+ You are a plan validator.
56
+ Context (results so far):
57
+ {context}
58
+
59
+ Next action:
60
+ {action}
61
+
62
+ Reply _only_ with JSON:
63
+ {{
64
+ "should_execute": <true|false>,
65
+ "parameters": <null or a JSON object with this action's parameters>
66
+ }}
67
+ """
68
+
69
+
70
+ def _load_name_mapping() -> Dict[str, str]:
71
+ """Load name to email mapping from JSON file"""
72
+ if not os.path.exists(NAME_MAPPING_FILE):
73
+ return {}
74
+ try:
75
+ with open(NAME_MAPPING_FILE, "r") as f:
76
+ return json.load(f)
77
+ except (json.JSONDecodeError, IOError):
78
+ return {}
79
+
80
+
81
+ def _save_name_mapping(mapping: Dict[str, str]):
82
+ """Save name to email mapping to JSON file"""
83
+ with open(NAME_MAPPING_FILE, "w") as f:
84
+ json.dump(mapping, f, indent=2)
85
+
86
+
87
+ def store_name_email_mapping(name: str, email: str):
88
+ """Store new name to email mapping"""
89
+ name_mapping = _load_name_mapping()
90
+ name_mapping[name.lower().strip()] = email.lower().strip()
91
+ _save_name_mapping(name_mapping)
92
+
93
+
94
+ def extract_sender_info(query: str) -> Dict:
95
+ """
96
+ Extract sender information from user query using LLM
97
+ """
98
+ system_prompt = """
99
+ You are an email query parser that extracts sender information.
100
+
101
+ Given a user query, extract the sender intent - the person/entity they want emails from.
102
+ This could be:
103
+ - A person's name (e.g., "dev", "john smith", "dev agarwal")
104
+ - A company/service (e.g., "amazon", "google", "linkedin")
105
+ - An email address (e.g., "[email protected]")
106
+
107
+ Examples:
108
+ - "emails from dev agarwal last week" → "dev agarwal"
109
+ - "show amazon emails from last month" → "amazon"
110
+ - "emails from [email protected] yesterday" → "[email protected]"
111
+ - "get messages from sarah" → "sarah"
112
+
113
+ Return ONLY valid JSON:
114
+ {
115
+ "sender_intent": "extracted name, company, or email"
116
+ }
117
+ """
118
+
119
+ response = client.chat.completions.create(
120
+ model="gpt-4o-mini",
121
+ temperature=0.0,
122
+ messages=[
123
+ {"role": "system", "content": system_prompt},
124
+ {"role": "user", "content": query}
125
+ ],
126
+ )
127
+
128
+ result = json.loads(response.choices[0].message.content)
129
+ return result
130
+
131
+
132
+ def resolve_sender_email(sender_intent: str) -> Tuple[Optional[str], bool]:
133
+ """
134
+ Resolve sender intent to actual email address
135
+ Returns: (email_address, needs_user_input)
136
+ """
137
+ # Check if it's already an email address
138
+ if "@" in sender_intent:
139
+ return sender_intent.lower(), False
140
+
141
+ # Load name mapping
142
+ name_mapping = _load_name_mapping()
143
+
144
+ # Normalize the intent (lowercase for comparison)
145
+ normalized_intent = sender_intent.lower().strip()
146
+
147
+ # Check direct match
148
+ if normalized_intent in name_mapping:
149
+ return name_mapping[normalized_intent], False
150
+
151
+ # Check partial matches (fuzzy matching)
152
+ for name, email in name_mapping.items():
153
+ if normalized_intent in name.lower() or name.lower() in normalized_intent:
154
+ return email, False
155
+
156
+ # No match found
157
+ return None, True
158
+
159
+
160
+ def get_plan_from_llm(user_query: str) -> Plan:
161
+ """
162
+ Ask the LLM which actions to run, in order. No parameters here.
163
+ """
164
+ response = client.chat.completions.create(
165
+ model="gpt-4o-mini",
166
+ temperature=0.0,
167
+ messages=[
168
+ {"role": "system", "content": SYSTEM_PLAN_PROMPT},
169
+ {"role": "user", "content": user_query},
170
+ ],
171
+ )
172
+
173
+ plan_json = json.loads(response.choices[0].message.content)
174
+ steps = [PlanStep(action=a) for a in plan_json["plan"]]
175
+ return Plan(plan=steps)
176
+
177
+
178
+ def think(
179
+ step: PlanStep,
180
+ context: Dict[str, Any],
181
+ user_query: str
182
+ ) -> Tuple[bool, Optional[PlanStep], Optional[str]]:
183
+ """
184
+ Fill in parameters or skip based on the action:
185
+ - fetch_emails: extract sender and pass the raw query for date extraction
186
+ - others: ask the LLM validator for params
187
+
188
+ Returns: (should_execute, updated_step, user_prompt_if_needed)
189
+ """
190
+ # 1) fetch_emails → extract sender and pass query for internal date extraction
191
+ if step.action == "fetch_emails":
192
+ # Extract sender using LLM
193
+ sender_info = extract_sender_info(user_query)
194
+ sender_intent = sender_info.get("sender_intent", "")
195
+
196
+ if not sender_intent:
197
+ return False, None, None
198
+
199
+ # Resolve sender to email address
200
+ email_address, needs_input = resolve_sender_email(sender_intent)
201
+
202
+ if needs_input:
203
+ # Need user input for email address
204
+ prompt_msg = f"I don't have an email address for '{sender_intent}'. Please provide the email address:"
205
+ return False, None, prompt_msg
206
+
207
+ params = FetchEmailsParams(
208
+ email=email_address,
209
+ query=user_query # Pass the full query for date extraction
210
+ )
211
+ return True, PlanStep(action="fetch_emails", parameters=params), None
212
+
213
+ # 2) everything else → validate & supply params via LLM
214
+ prompt = SYSTEM_VALIDATOR_TEMPLATE.format(
215
+ context=json.dumps(context, indent=2),
216
+ action=step.action,
217
+ )
218
+ response = client.chat.completions.create(
219
+ model="gpt-4o-mini",
220
+ temperature=0.0,
221
+ messages=[
222
+ {"role": "system", "content": "Validate or supply parameters for this action."},
223
+ {"role": "user", "content": prompt},
224
+ ],
225
+ )
226
+ verdict = json.loads(response.choices[0].message.content)
227
+ if not verdict.get("should_execute", False):
228
+ return False, None, None
229
+
230
+ return True, PlanStep(
231
+ action=step.action,
232
+ parameters=verdict.get("parameters")
233
+ ), None
234
+
235
+
236
+ def act(step: PlanStep) -> Any:
237
+ """
238
+ Dispatch to the actual implementation in tools.py.
239
+ """
240
+ fn = TOOL_MAPPING.get(step.action)
241
+ if fn is None:
242
+ raise ValueError(f"Unknown action '{step.action}'")
243
+
244
+ kwargs = step.parameters.model_dump() if step.parameters else {}
245
+ return fn(**kwargs)
agentic_implementation/schemas.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # schemas.py
2
+
3
+ from pydantic import BaseModel, EmailStr
4
+ from typing import List, Literal, Optional, Union
5
+
6
+
7
+
8
+ class FetchEmailsParams(BaseModel):
9
+ email: str
10
+ query: str # Changed from start_date/end_date to query for internal date extraction
11
+
12
+
13
+ class ShowEmailParams(BaseModel):
14
+ message_id: str
15
+
16
+ class AnalyzeEmailsParams(BaseModel):
17
+ emails: List[dict]
18
+
19
+ class DraftReplyParams(BaseModel):
20
+ email: dict
21
+ tone: Optional[Literal["formal", "informal"]] = "formal"
22
+
23
+ class SendReplyParams(BaseModel):
24
+ message_id: str
25
+ reply_body: str
26
+
27
+
28
+ ToolParams = Union[
29
+ FetchEmailsParams,
30
+ ShowEmailParams,
31
+ AnalyzeEmailsParams,
32
+ DraftReplyParams,
33
+ SendReplyParams
34
+ ]
35
+
36
+ class PlanStep(BaseModel):
37
+ action: Literal[
38
+ "fetch_emails",
39
+ "show_email",
40
+ "analyze_emails",
41
+ "draft_reply",
42
+ "send_reply",
43
+ "done",
44
+ ]
45
+ parameters: Optional[ToolParams] = None
46
+
47
+ class Plan(BaseModel):
48
+ plan: List[PlanStep]
agentic_implementation/tools.py ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from schemas import (
2
+ FetchEmailsParams,
3
+ ShowEmailParams,
4
+ AnalyzeEmailsParams,
5
+ DraftReplyParams,
6
+ SendReplyParams,
7
+ )
8
+ from typing import Any, Dict
9
+ from email_scraper import scrape_emails_from_sender, _load_email_db, _save_email_db, _is_date_in_range
10
+ from datetime import datetime
11
+ from typing import List
12
+ from openai import OpenAI
13
+ import json
14
+ from dotenv import load_dotenv
15
+ import os
16
+
17
+ # Load environment variables from .env file
18
+ load_dotenv()
19
+
20
+ # Initialize OpenAI client
21
+ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
22
+ client = OpenAI(api_key=OPENAI_API_KEY)
23
+
24
+
25
+ def extract_date_range(query: str) -> Dict[str, str]:
26
+ """
27
+ Use an LLM to extract a date range from a user query.
28
+ Returns {"start_date":"DD-MMM-YYYY","end_date":"DD-MMM-YYYY"}.
29
+ """
30
+ today_str = datetime.today().strftime("%d-%b-%Y")
31
+ system_prompt = f"""
32
+ You are a date‐range extractor. Today is {today_str}.
33
+
34
+ Given a user query (in natural language), return _only_ valid JSON with:
35
+ {{
36
+ "start_date": "DD-MMM-YYYY",
37
+ "end_date": "DD-MMM-YYYY"
38
+ }}
39
+
40
+ Interpret relative dates as:
41
+ - "today" → {today_str} to {today_str}
42
+ - "yesterday" → 1 day ago to 1 day ago
43
+ - "last week" → 7 days ago to {today_str}
44
+ - "last month" → 30 days ago to {today_str}
45
+ - "last N days" → N days ago to {today_str}
46
+
47
+ Examples:
48
+ - "emails from dev agarwal last week"
49
+ → {{ "start_date": "01-Jun-2025", "end_date": "{today_str}" }}
50
+ - "show me emails yesterday"
51
+ → {{ "start_date": "06-Jun-2025", "end_date": "06-Jun-2025" }}
52
+
53
+ Return _only_ the JSON object—no extra text.
54
+ """
55
+
56
+ messages = [
57
+ {"role": "system", "content": system_prompt},
58
+ {"role": "user", "content": query}
59
+ ]
60
+ resp = client.chat.completions.create(
61
+ model="gpt-4o-mini",
62
+ temperature=0.0,
63
+ messages=messages
64
+ )
65
+ content = resp.choices[0].message.content.strip()
66
+
67
+ # Try direct parse; if the model added fluff, strip to the JSON block.
68
+ try:
69
+ return json.loads(content)
70
+ except json.JSONDecodeError:
71
+ start = content.find("{")
72
+ end = content.rfind("}") + 1
73
+ return json.loads(content[start:end])
74
+
75
+
76
+ def fetch_emails(email: str, query: str) -> Dict:
77
+ """
78
+ Fetch emails from a sender within a date range extracted from the query.
79
+ Now returns both date info and emails.
80
+
81
+ Args:
82
+ email: The sender's email address
83
+ query: The original user query (for date extraction)
84
+
85
+ Returns:
86
+ Dict with date_info and emails
87
+ """
88
+ # Extract date range from query
89
+ date_info = extract_date_range(query)
90
+ start_date = date_info.get("start_date")
91
+ end_date = date_info.get("end_date")
92
+
93
+ # Fetch emails using the existing scraper
94
+ emails = scrape_emails_from_sender(email, start_date, end_date)
95
+
96
+ # Return both date info and emails
97
+ return {
98
+ "date_info": date_info,
99
+ "emails": emails,
100
+ "email_count": len(emails)
101
+ }
102
+
103
+
104
+ def show_email(message_id: str) -> Dict:
105
+ """
106
+ Retrieve the full email record (date, time, subject, content, etc.)
107
+ from the local cache by message_id.
108
+ """
109
+ db = _load_email_db() # returns { sender_email: { "emails": [...], "last_scraped": ... }, ... }
110
+
111
+ # Search each sender's email list
112
+ for sender_data in db.values():
113
+ for email in sender_data.get("emails", []):
114
+ if email.get("message_id") == message_id:
115
+ return email
116
+
117
+ # If we didn't find it, raise or return an error structure
118
+ raise ValueError(f"No email found with message_id '{message_id}'")
119
+
120
+
121
+ def draft_reply(email: Dict, tone: str) -> str:
122
+ # call LLM to generate reply
123
+ # return a dummy reply for now
124
+ print(f"Drafting reply for email {email['id']} with tone: {tone}")
125
+ return f"Drafted reply for email {email['id']} with tone {tone}."
126
+ ...
127
+
128
+
129
+ def send_reply(message_id: str, reply_body: str) -> Dict:
130
+ # SMTP / Gmail API send
131
+ print(f"Sending reply to message {message_id} with body: {reply_body}")
132
+ ...
133
+
134
+
135
+ def analyze_emails(emails: List[Dict]) -> Dict:
136
+ """
137
+ Summarize and extract insights from a list of emails.
138
+ Returns a dict with this schema:
139
+ {
140
+ "summary": str, # a concise overview of all emails
141
+ "insights": [str, ...] # list of key observations or stats
142
+ }
143
+ """
144
+ # 1) Prepare the email payload
145
+ emails_payload = json.dumps(emails, ensure_ascii=False)
146
+
147
+ # 2) Build the LLM prompt
148
+ system_prompt = """
149
+ You are an expert email analyst. You will be given a JSON array of email objects,
150
+ each with keys: date, time, subject, content, message_id.
151
+
152
+ Your job is to produce _only_ valid JSON with two fields:
153
+ 1. summary: a 1–2 sentence high-level overview of these emails.
154
+ 2. insights: a list of 3–5 bullet-style observations or statistics
155
+ (e.g. "2 job offers found", "overall positive tone", "next action: reply").
156
+
157
+ Output exactly:
158
+
159
+ {
160
+ "summary": "...",
161
+ "insights": ["...", "...", ...]
162
+ }
163
+ """
164
+ messages = [
165
+ {"role": "system", "content": system_prompt},
166
+ {"role": "user", "content": f"Here are the emails:\n{emails_payload}"}
167
+ ]
168
+
169
+ # 3) Call the LLM
170
+ response = client.chat.completions.create(
171
+ model="gpt-4o-mini",
172
+ temperature=0.0,
173
+ messages=messages
174
+ )
175
+
176
+ # 4) Parse and return
177
+ content = response.choices[0].message.content.strip()
178
+ try:
179
+ return json.loads(content)
180
+ except json.JSONDecodeError:
181
+ # In case the model outputs extra text, extract the JSON block
182
+ start = content.find('{')
183
+ end = content.rfind('}') + 1
184
+ return json.loads(content[start:end])
185
+
186
+
187
+ TOOL_MAPPING = {
188
+ "fetch_emails": fetch_emails,
189
+ "show_email": show_email,
190
+ "analyze_emails": analyze_emails,
191
+ "draft_reply": draft_reply,
192
+ "send_reply": send_reply,
193
+ }
server/email_db.json CHANGED
@@ -115,5 +115,21 @@
115
  }
116
  ],
117
  "last_scraped": "07-Jun-2025"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  }
119
  }
 
115
  }
116
  ],
117
  "last_scraped": "07-Jun-2025"
118
+ },
119
120
+ "emails": [],
121
+ "last_scraped": "07-Jun-2025"
122
+ },
123
124
+ "emails": [
125
+ {
126
+ "date": "07-Jun-2025",
127
+ "time": "16:42:51",
128
+ "subject": "testing",
129
+ "content": "hi bro",
130
+ "message_id": "<CAPziNCaSuVqpqNNfsRjhVbx22jN_vos3EGK_Odt-8WiD0HRKKQ@mail.gmail.com>"
131
+ }
132
+ ],
133
+ "last_scraped": "07-Jun-2025"
134
  }
135
  }
server/name_mapping.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ {
2
+ "dev": "[email protected]"
3
+ }