From 79a926e4d8b3872394a9968f14d890cd182ed0ca Mon Sep 17 00:00:00 2001 From: Roberto Date: Thu, 11 Jun 2026 00:27:04 +0200 Subject: [PATCH] feat(scouts): debug scripts + deliver_pending diagnostic logs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - scripts/trigger_gmail_scout.py: manually fire ScoutEngine.trigger_scout - scripts/inspect_gmail_scout_token.py: decrypt + show stored OAuth scopes - scripts/show_gmail_scout_state.py: print scout config + queue/log counts - scripts/reset_triage_queue_to_queued.py: revert delivered → queued for re-delivery - engine.py: info logs around deliver_pending (rows found, send_json roundtrip) Co-Authored-By: Claude Opus 4.7 --- app/scouts/engine.py | 3 + scripts/inspect_gmail_scout_token.py | 56 +++++++++++++++++++ scripts/reset_triage_queue_to_queued.py | 35 ++++++++++++ scripts/show_gmail_scout_state.py | 59 ++++++++++++++++++++ scripts/trigger_gmail_scout.py | 74 +++++++++++++++++++++++++ 5 files changed, 227 insertions(+) create mode 100644 scripts/inspect_gmail_scout_token.py create mode 100644 scripts/reset_triage_queue_to_queued.py create mode 100644 scripts/show_gmail_scout_state.py create mode 100644 scripts/trigger_gmail_scout.py diff --git a/app/scouts/engine.py b/app/scouts/engine.py index e1932c0..34999bf 100644 --- a/app/scouts/engine.py +++ b/app/scouts/engine.py @@ -144,6 +144,7 @@ class ScoutEngine: ScoutTriageQueue.status == "queued", ) )).scalars().all() + logger.info("deliver_pending: user=%s found %d queued rows", user_id, len(rows)) for row in rows: try: @@ -173,7 +174,9 @@ class ScoutEngine: "payload": None, }, } + logger.info("deliver_pending: sending proposal id=%s subject=%r", row.id, meta.subject) await ws.send_json(payload) + logger.info("deliver_pending: send_json returned for proposal id=%s", row.id) row.status = "delivered" row.delivered_at = datetime.now(tz=timezone.utc) diff --git a/scripts/inspect_gmail_scout_token.py b/scripts/inspect_gmail_scout_token.py new file mode 100644 index 0000000..e6ae583 --- /dev/null +++ b/scripts/inspect_gmail_scout_token.py @@ -0,0 +1,56 @@ +"""Decrypt and inspect the Gmail scout's stored OAuth token. + +Shows what scopes were granted at consent time. If gmail.readonly / gmail.modify +are missing, the consent screen didn't actually grant them. + +Usage: + python scripts/inspect_gmail_scout_token.py +""" + +from __future__ import annotations + +import asyncio +import sys +from pathlib import Path + +_API_ROOT = Path(__file__).resolve().parent.parent +if str(_API_ROOT) not in sys.path: + sys.path.insert(0, str(_API_ROOT)) + +from sqlalchemy import select + +from app.db import async_session +from app.integrations import decrypt_token +from app.models import CloudScoutConfig + + +async def main() -> None: + async with async_session() as session: + scouts = ( + await session.execute( + select(CloudScoutConfig).where(CloudScoutConfig.provider == "gmail") + ) + ).scalars().all() + + if not scouts: + print("No Gmail scouts found.") + return + + for scout in scouts: + print(f"\nScout: {scout.name} (id={scout.id})") + if not scout.oauth_token_encrypted: + print(" (no token stored)") + continue + try: + creds = decrypt_token(scout.oauth_token_encrypted) + except Exception as exc: + print(f" decrypt failed: {exc}") + continue + print(f" has refresh_token: {bool(creds.get('refresh_token'))}") + print(f" stored scopes: {creds.get('scopes')}") + print(f" token_uri: {creds.get('token_uri')}") + print(f" client_id (last 8): ...{(creds.get('client_id') or '')[-8:]}") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/scripts/reset_triage_queue_to_queued.py b/scripts/reset_triage_queue_to_queued.py new file mode 100644 index 0000000..37cc550 --- /dev/null +++ b/scripts/reset_triage_queue_to_queued.py @@ -0,0 +1,35 @@ +"""Re-queue all delivered (but not acked) triage rows so deliver_pending sends them again. + +Usage: + python scripts/reset_triage_queue_to_queued.py +""" + +from __future__ import annotations + +import asyncio +import sys +from pathlib import Path + +_API_ROOT = Path(__file__).resolve().parent.parent +if str(_API_ROOT) not in sys.path: + sys.path.insert(0, str(_API_ROOT)) + +from sqlalchemy import update + +from app.db import async_session +from app.models import ScoutTriageQueue + + +async def main() -> None: + async with async_session() as session: + result = await session.execute( + update(ScoutTriageQueue) + .where(ScoutTriageQueue.status == "delivered") + .values(status="queued", delivered_at=None) + ) + await session.commit() + print(f"Reset {result.rowcount} rows from delivered → queued") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/scripts/show_gmail_scout_state.py b/scripts/show_gmail_scout_state.py new file mode 100644 index 0000000..a60be05 --- /dev/null +++ b/scripts/show_gmail_scout_state.py @@ -0,0 +1,59 @@ +"""Print Gmail scout state for debugging. + +Usage: + python scripts/show_gmail_scout_state.py +""" + +from __future__ import annotations + +import asyncio +import sys +from pathlib import Path + +_API_ROOT = Path(__file__).resolve().parent.parent +if str(_API_ROOT) not in sys.path: + sys.path.insert(0, str(_API_ROOT)) + +from sqlalchemy import select, func + +from app.db import async_session +from app.models import CloudScoutConfig, ScoutTriageQueue, ScoutRunLog + + +async def main() -> None: + async with async_session() as session: + scouts = ( + await session.execute( + select(CloudScoutConfig).where(CloudScoutConfig.provider == "gmail") + ) + ).scalars().all() + + for scout in scouts: + print(f"\nScout: {scout.name} (id={scout.id})") + print(f" enabled: {scout.enabled}") + print(f" gmail_history_id: {scout.gmail_history_id}") + print(f" gmail_watch_expires_at: {scout.gmail_watch_expires_at}") + print(f" auto_trash_spam: {scout.auto_trash_spam}") + print(f" last_run_at: {scout.last_run_at}") + + queued_count = ( + await session.execute( + select(func.count()) + .select_from(ScoutTriageQueue) + .where(ScoutTriageQueue.scout_id == scout.id) + ) + ).scalar() + print(f" triage_queue rows: {queued_count}") + + run_count = ( + await session.execute( + select(func.count()) + .select_from(ScoutRunLog) + .where(ScoutRunLog.scout_id == scout.id) + ) + ).scalar() + print(f" scout_run_logs: {run_count}") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/scripts/trigger_gmail_scout.py b/scripts/trigger_gmail_scout.py new file mode 100644 index 0000000..cd3ca58 --- /dev/null +++ b/scripts/trigger_gmail_scout.py @@ -0,0 +1,74 @@ +"""Manually trigger the user's Gmail scout for testing. + +Usage: + python scripts/trigger_gmail_scout.py [user_email] + +If user_email omitted, picks the first user with a Gmail scout. +Runs ScoutEngine.trigger_scout — which calls Gmail history.list since last +gmail_history_id, fetches each new message, runs LLM triage, inserts queue rows +for relevant items. + +After running, check the queue: + psql -d adiuvai -c "select source_msg_ref, triage_verdict, status from scout_triage_queue order by triaged_at desc limit 10" + +Then restart the Electron app to trigger deliver_pending → frames → local +scout_suggestions rows. +""" + +from __future__ import annotations + +import asyncio +import sys +import uuid +from pathlib import Path + +# Ensure api/ root is importable when running from scripts/ subdir +_API_ROOT = Path(__file__).resolve().parent.parent +if str(_API_ROOT) not in sys.path: + sys.path.insert(0, str(_API_ROOT)) + +from sqlalchemy import select + +from app.db import async_session +from app.models import CloudScoutConfig, User +from app.scouts.connectors.gmail import GmailConnector +from app.scouts.connectors.registry import register_connector +from app.scouts.engine import ScoutEngine + + +async def main() -> None: + register_connector(GmailConnector()) + + target_email = sys.argv[1] if len(sys.argv) > 1 else None + + async with async_session() as session: + q = select(CloudScoutConfig).where( + CloudScoutConfig.provider == "gmail", + CloudScoutConfig.enabled.is_(True), + ) + if target_email: + user = ( + await session.execute(select(User).where(User.email == target_email)) + ).scalar_one_or_none() + if user is None: + print(f"No user with email {target_email}") + return + q = q.where(CloudScoutConfig.user_id == user.id) + + scouts = (await session.execute(q)).scalars().all() + + if not scouts: + print("No enabled Gmail scouts found. Create one in Settings → Scouts first.") + return + + for scout in scouts: + print(f"Triggering scout id={scout.id} name={scout.name!r} user={scout.user_id}") + try: + await ScoutEngine().trigger_scout(uuid.UUID(scout.id)) + print(" → done") + except Exception as exc: + print(f" → failed: {exc}") + + +if __name__ == "__main__": + asyncio.run(main())