| |
| """ |
| main.py β FollowUp Agent CLI |
| ============================== |
| Usage: |
| # Pick ONE new (unprocessed) inbox email and route it |
| /usr/bin/python3 main.py --run |
| |
| # Ingest + process an email from a .txt file |
| /usr/bin/python3 main.py --email email_body.txt --from "client@company.com" --subject "Invoice #102" |
| |
| # Simulate with raw text (testing) |
| /usr/bin/python3 main.py --simulate "Best Threads asking about payment status for invoice #102" |
| |
| # List inbox emails (NEW emails highlighted) |
| /usr/bin/python3 main.py --list |
| |
| # Show reply draft for a specific inbox ID |
| /usr/bin/python3 main.py --draft 1 |
| |
| # Initialise databases only |
| /usr/bin/python3 main.py --init |
| """ |
|
|
| from __future__ import annotations |
| import argparse |
| import os |
| import sys |
|
|
| sys.path.insert(0, os.path.dirname(__file__)) |
| from db_setup import init_db, get_conn |
| from router import route_email |
|
|
|
|
| def cmd_list() -> None: |
| """Print inbox emails β [NEW] marks unprocessed (processed=0) rows.""" |
| conn = get_conn() |
| rows = conn.execute( |
| "SELECT id, sender, subject, status, label, processed, received_at FROM inbox ORDER BY id DESC LIMIT 30" |
| ).fetchall() |
| conn.close() |
| if not rows: |
| print("Inbox is empty.") |
| return |
| new_count = sum(1 for r in rows if r['label'] == 'new') |
| print(f"\n Inbox ({len(rows)} emails, {new_count} new)") |
| print(f"\n{'ID':<5} {'LABEL':<8} {'STATUS':<10} {'FROM':<30} {'SUBJECT'}") |
| print("β"*85) |
| for r in rows: |
| label = f"[{r['label'].upper()}]" if r['label'] == 'new' else r['label'] or '' |
| print(f"{r['id']:<5} {label:<8} {r['status']:<10} {(r['sender'] or '')[:28]:<30} {r['subject']}") |
|
|
|
|
| def cmd_run_next() -> None: |
| """ |
| Fetch the OLDEST unprocessed email (processed=0) from inbox |
| and run the full routing pipeline on it. |
| """ |
| conn = get_conn() |
| row = conn.execute( |
| "SELECT * FROM inbox WHERE processed = 0 ORDER BY id ASC LIMIT 1" |
| ).fetchone() |
| conn.close() |
|
|
| if not row: |
| print("\n β
No new emails to process. Inbox is up to date.") |
| return |
|
|
| email_id = row['id'] |
| print(f"\n{'β'*65}") |
| print(f" FollowUp Agent β Processing Queued Email") |
| print(f" Inbox ID : {email_id}") |
| print(f" From : {row['sender']}") |
| print(f" Subject : {row['subject']}") |
| print(f" Received : {row['received_at']}") |
| print(f"{'β'*65}") |
|
|
| |
| from router import step1_routing_decision, step1b_generate_sql, step2_execute_and_draft |
| import json |
|
|
| |
| conn2 = get_conn() |
| thread_history = [dict(r) for r in conn2.execute( |
| "SELECT sender, subject, body, received_at FROM inbox " |
| "WHERE thread_id=? AND id!=? ORDER BY received_at DESC LIMIT 5", |
| (row['thread_id'], email_id), |
| ).fetchall()] |
|
|
| |
| routing_plan = step1_routing_decision( |
| row['body'], row['sender'], row['subject'], thread_history |
| ) |
|
|
| reply_draft = "" |
| if routing_plan: |
| |
| routing_plan = step1b_generate_sql(row['body'], row['sender'], row['subject'], routing_plan) |
|
|
| |
| reply_draft = step2_execute_and_draft( |
| row['body'], row['sender'], row['subject'], routing_plan |
| ) |
| |
| |
| conn2.execute( |
| "UPDATE inbox SET processed=1, routing_plan=?, reply_draft=?, status='under review', label='under review' WHERE id=?", |
| (json.dumps(routing_plan), reply_draft, email_id), |
| ) |
| conn2.commit() |
|
|
| conn2.close() |
|
|
| print(f"\n{'β'*65}") |
| if reply_draft: |
| print(f" β
Done. inbox_id={email_id} β status: under review") |
| print(f"\n DRAFT REPLY:") |
| print(f" {'β'*60}") |
| print(" " + "\n ".join(reply_draft.splitlines())) |
| print(f" {'β'*60}") |
| print(f"\n View again: python3 main.py --draft {email_id}") |
| else: |
| print(f" β οΈ Routed but draft could not be generated (inbox_id={email_id}).") |
| print(f"{'β'*65}") |
|
|
|
|
| def cmd_show_draft(inbox_id: int) -> None: |
| """Print the reply draft and original email for a specific inbox ID.""" |
| conn = get_conn() |
| row = conn.execute("SELECT body, sender, subject, reply_draft FROM inbox WHERE id=?", |
| (inbox_id,)).fetchone() |
| conn.close() |
| if not row or not row['reply_draft']: |
| print(f"No drafted reply found for inbox id={inbox_id}") |
| return |
| print(f"\n{'β'*65}") |
| print(f" ORIGINAL EMAIL (inbox_id={inbox_id})") |
| print(f"{'β'*65}") |
| print(f"\n{row['body']}") |
| |
| print(f"\n{'β'*65}") |
| print(f" DRAFTED REPLY (Status: Under Review)") |
| print(f"{'β'*65}") |
| print(f" To : {row['sender']}") |
| print(f" Subject: Re: {row['subject']}") |
| print(f"\n{row['reply_draft']}") |
| print(f"{'β'*65}") |
|
|
|
|
|
|
| def main() -> None: |
| parser = argparse.ArgumentParser( |
| prog="FollowUp Agent", |
| description="LLM-powered email routing and reply drafting", |
| ) |
| group = parser.add_mutually_exclusive_group(required=True) |
| group.add_argument("--run", action="store_true", help="Process next NEW (unprocessed) inbox email") |
| group.add_argument("--email", metavar="FILE", help="Ingest + process email from .txt file") |
| group.add_argument("--simulate", metavar="TEXT", help="Raw email text for simulation/testing") |
| group.add_argument("--list", action="store_true", help="List inbox emails ([NEW] = unprocessed)") |
| group.add_argument("--draft", metavar="ID", type=int, help="Show draft for inbox ID") |
| group.add_argument("--init", action="store_true", help="Initialise DB only") |
|
|
| parser.add_argument("--from", dest="sender", default="unknown@email.com", help="Sender email address") |
| parser.add_argument("--subject", default="No Subject", help="Email subject line") |
|
|
| args = parser.parse_args() |
|
|
| |
| init_db() |
|
|
| if args.init: |
| print("β
Databases initialised.") |
| return |
|
|
| if args.run: |
| cmd_run_next() |
| return |
|
|
| if args.list: |
| cmd_list() |
| return |
|
|
| if args.draft is not None: |
| cmd_show_draft(args.draft) |
| return |
|
|
| |
| if args.email: |
| path = os.path.abspath(args.email) |
| if not os.path.isfile(path): |
| print(f"β File not found: {path}") |
| sys.exit(1) |
| with open(path, encoding="utf-8") as f: |
| email_text = f.read() |
| source_label = f"file:{os.path.basename(path)}" |
| else: |
| email_text = args.simulate |
| source_label = "simulate" |
|
|
| print(f"\n{'β'*65}") |
| print(f" FollowUp Agent β Processing Email") |
| print(f" From : {args.sender}") |
| print(f" Subject: {args.subject}") |
| print(f"{'β'*65}") |
|
|
| result = route_email( |
| email_text = email_text, |
| sender = args.sender, |
| subject = args.subject, |
| ) |
|
|
| print(f"\n{'β'*65}") |
| if result.get("reply_draft"): |
| print(f" β
Done. Inbox id={result['inbox_id']} | Status: under review") |
| print(f"\n DRAFT REPLY:") |
| print(f" {'β'*60}") |
| print(" " + "\n ".join(result["reply_draft"].splitlines())) |
| print(f" {'β'*60}") |
| print(f"\n View again anytime: python3 main.py --draft {result['inbox_id']}") |
| else: |
| print(f" β οΈ Email stored (id={result.get('inbox_id')}) but draft could not be generated.") |
| print(f"{'β'*65}") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|