Spaces:
Sleeping
Sleeping
devangshrivastava
commited on
Commit
·
4f321e9
1
Parent(s):
9125b1a
agentic_base made
Browse files- 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 |
+
}
|