Texbase / src_2 /FollowUp /main.py
Asad999's picture
Initial clean deployment for Hugging Face Spaces (v5 - final fix)
d712cef
Raw
History Blame Contribute Delete
7.86 kB
#!/usr/bin/python3
"""
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}")
# Import here to avoid circular import at module top
from router import step1_routing_decision, step1b_generate_sql, step2_execute_and_draft
import json
# Fetch thread history for context
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()]
# Step 1: routing decision
routing_plan = step1_routing_decision(
row['body'], row['sender'], row['subject'], thread_history
)
reply_draft = ""
if routing_plan:
# Step 1b: schema-aware SQL generation
routing_plan = step1b_generate_sql(row['body'], row['sender'], row['subject'], routing_plan)
# Step 2: execute SQL + draft reply
reply_draft = step2_execute_and_draft(
row['body'], row['sender'], row['subject'], routing_plan
)
# Save draft + mark processed
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()
# Always ensure DB exists
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
# ── Process email ─────────────────────────────────────────────────────
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()