JARVIS / task_manager_agent /main_agent.py
viraj.kothari
fix: rename agent folders to remove spaces
58b74a0
Raw
History Blame Contribute Delete
7.93 kB
"""
Task Manager Agent
==================
Turns emails + meeting notes into tasks automatically.
Prioritizes by deadline and impact.
Syncs with Notion or Todoist.
Triggers: new email arrival + daily 9 AM sync.
"""
import os
import time
import logging
import schedule
import threading
from datetime import datetime
import os as _os
from dotenv import load_dotenv
from data_fetcher import (
fetch_recent_emails,
fetch_calendar_meetings,
fetch_existing_tasks,
fetch_notion_tasks,
)
from llm import extract_tasks_from_emails, extract_tasks_from_meeting_notes, prioritize_tasks
from task_store import TaskStore
import sys, os
sys.path.insert(0, os.path.dirname(__file__))
from delivery import (
send_task_digest_email,
send_task_digest_whatsapp,
sync_to_notion,
sync_to_todoist,
)
load_dotenv(_os.path.join(_os.path.dirname(_os.path.abspath(__file__)), "..", ".env"))
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s β€” %(message)s",
handlers=[
logging.FileHandler("task_manager.log"),
logging.StreamHandler(),
],
)
logger = logging.getLogger("TaskManagerAgent")
# ── config ────────────────────────────────────────────────────────────────────
EMAIL_POLL_INTERVAL_MINUTES = int(os.getenv("EMAIL_POLL_INTERVAL_MINUTES", "15"))
SYNC_BACKEND = os.getenv("TASK_SYNC_BACKEND", "notion") # "notion" | "todoist" | "both"
DELIVER_EMAIL = os.getenv("DELIVER_TASK_EMAIL", "true").lower() == "true"
DELIVER_WHATSAPP = os.getenv("DELIVER_TASK_WHATSAPP", "false").lower() == "true"
MAX_EMAILS_TO_SCAN = int(os.getenv("MAX_EMAILS_TO_SCAN", "20"))
USER_EMAIL = os.getenv("USER_EMAIL", "")
# ── core pipeline ─────────────────────────────────────────────────────────────
def run_task_extraction_pipeline(trigger: str = "scheduled") -> dict:
"""Full pipeline: fetch β†’ extract β†’ prioritize β†’ sync β†’ deliver."""
logger.info(f"πŸš€ Task Manager pipeline started (trigger={trigger})")
start = datetime.now()
results = {"new_tasks": [], "updated_tasks": [], "errors": []}
try:
# 1. Fetch inputs
logger.info("πŸ“₯ Fetching recent emails …")
emails = fetch_recent_emails(max_results=MAX_EMAILS_TO_SCAN)
logger.info(f" Found {len(emails)} emails to analyse")
logger.info("πŸ“… Fetching today's meetings for context …")
meetings = fetch_calendar_meetings(days_back=1, days_ahead=0)
logger.info(f" Found {len(meetings)} recent meetings")
logger.info("πŸ“‹ Fetching existing tasks to avoid duplicates …")
existing_tasks = fetch_existing_tasks()
# 2. Extract tasks from emails
if emails:
logger.info("πŸ€– Extracting tasks from emails via Groq …")
email_tasks = extract_tasks_from_emails(emails, existing_tasks)
logger.info(f" Extracted {len(email_tasks)} tasks from emails")
results["new_tasks"].extend(email_tasks)
# 3. Extract tasks from meeting notes / descriptions
if meetings:
logger.info("πŸ€– Extracting tasks from meeting context …")
meeting_tasks = extract_tasks_from_meeting_notes(meetings, existing_tasks)
logger.info(f" Extracted {len(meeting_tasks)} tasks from meetings")
results["new_tasks"].extend(meeting_tasks)
if not results["new_tasks"]:
logger.info("βœ… No new tasks found β€” nothing to sync")
return results
# 4. Prioritize all new tasks together
logger.info("🎯 Prioritizing tasks …")
all_existing = existing_tasks + results["new_tasks"]
prioritized = prioritize_tasks(results["new_tasks"], all_existing)
results["new_tasks"] = prioritized
# 5. Persist locally
store = TaskStore()
saved_count = store.save_tasks(prioritized)
logger.info(f"πŸ’Ύ Saved {saved_count} tasks to local store")
# 6. Sync to external backend(s)
if SYNC_BACKEND in ("notion", "both"):
logger.info("πŸ”„ Syncing to Notion …")
synced = sync_to_notion(prioritized)
logger.info(f" Synced {synced} tasks to Notion")
if SYNC_BACKEND in ("todoist", "both"):
logger.info("πŸ”„ Syncing to Todoist …")
synced = sync_to_todoist(prioritized)
logger.info(f" Synced {synced} tasks to Todoist")
# 7. Deliver digest
if DELIVER_EMAIL and USER_EMAIL:
logger.info("πŸ“§ Sending task digest email …")
send_task_digest_email(prioritized, trigger=trigger, recipient=USER_EMAIL)
if DELIVER_WHATSAPP:
logger.info("πŸ“± Sending WhatsApp digest …")
send_task_digest_whatsapp(prioritized, trigger=trigger)
elapsed = (datetime.now() - start).seconds
logger.info(f"βœ… Pipeline complete in {elapsed}s β€” {len(prioritized)} tasks processed")
except Exception as e:
logger.error(f"❌ Pipeline error: {e}", exc_info=True)
results["errors"].append(str(e))
return results
# ── email-trigger watcher ─────────────────────────────────────────────────────
class EmailWatcher(threading.Thread):
"""Polls Gmail every N minutes; fires pipeline when new actionable emails arrive."""
def __init__(self):
super().__init__(daemon=True)
self._stop_event = threading.Event()
self._last_email_id: str | None = None
def run(self):
logger.info(f"πŸ‘οΈ EmailWatcher started (polling every {EMAIL_POLL_INTERVAL_MINUTES} min)")
while not self._stop_event.is_set():
try:
self._check_for_new_emails()
except Exception as e:
logger.warning(f"EmailWatcher error: {e}")
self._stop_event.wait(EMAIL_POLL_INTERVAL_MINUTES * 60)
def _check_for_new_emails(self):
emails = fetch_recent_emails(max_results=5, since_id=self._last_email_id)
if emails:
newest_id = emails[0].get("id")
if newest_id != self._last_email_id:
logger.info(f"πŸ“¬ {len(emails)} new email(s) detected β€” triggering extraction")
self._last_email_id = newest_id
run_task_extraction_pipeline(trigger="email_trigger")
def stop(self):
self._stop_event.set()
# ── scheduler ─────────────────────────────────────────────────────────────────
def schedule_daily_sync():
schedule.every().day.at("09:00").do(
lambda: run_task_extraction_pipeline(trigger="daily_9am")
)
logger.info("⏰ Daily sync scheduled at 09:00")
def run_scheduler():
logger.info("πŸ—“οΈ Scheduler running …")
while True:
schedule.run_pending()
time.sleep(30)
# ── entry point ───────────────────────────────────────────────────────────────
def main():
logger.info("=" * 60)
logger.info(" TASK MANAGER AGENT β€” Personal AI OS")
logger.info("=" * 60)
# Immediate run on startup
run_task_extraction_pipeline(trigger="startup")
# Continuous email watcher
watcher = EmailWatcher()
watcher.start()
# Daily 9 AM scheduled sync
schedule_daily_sync()
run_scheduler() # blocks
if __name__ == "__main__":
main()