agentic-base (#4)
Browse files- first commit (9b406094e66833776ff788397b51f7ba733fcdc1)
- Merge branch 'pr/4' into agentic-base (9125b1add395861a3af9d6d414c4514745d0d90c)
- agentic_base made (4f321e93870d8b10efa6c4587ab58585217018e2)
- agentic_implementation/agent.py +141 -0
- agentic_implementation/email_db.json +14 -0
- agentic_implementation/email_scraper.py +267 -0
- agentic_implementation/logger.py +25 -0
- agentic_implementation/name_mapping.json +3 -0
- agentic_implementation/re_act.py +245 -0
- agentic_implementation/schemas.py +48 -0
- agentic_implementation/tools.py +193 -0
- server/email_db.json +16 -0
- server/name_mapping.json +3 -0
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 |
+
"[email protected]": {
|
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 |
+
"[email protected]",
|
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 |
+
"[email protected]": {
|
120 |
+
"emails": [],
|
121 |
+
"last_scraped": "07-Jun-2025"
|
122 |
+
},
|
123 |
+
"[email protected]": {
|
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 |
+
}
|