| """ |
| 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") |
|
|
| |
| EMAIL_POLL_INTERVAL_MINUTES = int(os.getenv("EMAIL_POLL_INTERVAL_MINUTES", "15")) |
| SYNC_BACKEND = os.getenv("TASK_SYNC_BACKEND", "notion") |
| 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", "") |
|
|
|
|
| |
|
|
| 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: |
| |
| 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() |
|
|
| |
| 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) |
|
|
| |
| 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 |
|
|
| |
| logger.info("π― Prioritizing tasks β¦") |
| all_existing = existing_tasks + results["new_tasks"] |
| prioritized = prioritize_tasks(results["new_tasks"], all_existing) |
| results["new_tasks"] = prioritized |
|
|
| |
| store = TaskStore() |
| saved_count = store.save_tasks(prioritized) |
| logger.info(f"πΎ Saved {saved_count} tasks to local store") |
|
|
| |
| 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") |
|
|
| |
| 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 |
|
|
|
|
| |
|
|
| 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() |
|
|
|
|
| |
|
|
| 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) |
|
|
|
|
| |
|
|
| def main(): |
| logger.info("=" * 60) |
| logger.info(" TASK MANAGER AGENT β Personal AI OS") |
| logger.info("=" * 60) |
|
|
| |
| run_task_extraction_pipeline(trigger="startup") |
|
|
| |
| watcher = EmailWatcher() |
| watcher.start() |
|
|
| |
| schedule_daily_sync() |
| run_scheduler() |
|
|
|
|
| if __name__ == "__main__": |
| main() |