Skip to content

Discord bot

The bot is the only surface the operator interacts with. Source: agent/src/accounting_agent/discord_bot/. Entry point: python -m accounting_agent.discord_bot.

What it owns

  • Discord gateway connection (persistent WebSocket, ~24/7 uptime)
  • The ingest pipeline — when a PDF/image arrives in the allowlisted channel, the bot OCRs it, indexes it, runs the document-side matcher
  • HITL prompt surfaces — BookerHitlPoller ticks every 300s, scans for new gated rows / pending checkpoints, posts Discord messages with buttons
  • The chat agent for /ask — Pydantic AI agent with tools: search_docs, recent_bank_transactions, vat_decision, get_doc, list_recent
  • Every slash command: /pending, /book, /skip, /unmatched, /rules, /addrule, /delrule, /ruleq, /setkind, /bank-link, /bank-status, /bank-revoke, /setmodel, /recompile, etc.

How HITL works

Three resolver kinds are registered in hitl_setup.py:

Kind Resolver action
agent_ask Resumes the paused chat agent with the operator's reply
doc_match Confirms a doc↔bank pairing → sets recon booked
booker_approve Books a gated row → calls book_one(recon_id) with gate=None
client_create_approval Creates the missing e-Fin client + re-runs book_one

The bot's CheckpointRouter dispatches each button click / message reply to the right resolver. All four resolvers are self-contained — they never block the Discord event loop on long operations (each resolve creates its own short-lived clients).

Why slash commands AND buttons

Buttons are stateful (each button carries a custom_id with the checkpoint id), but Discord button views don't survive a bot restart. Slash commands cover the restart case: /pending lists proposed rows so the operator can manually approve them even when no button view is alive.

Allowlist

The bot only responds in the channel from DISCORD_CHANNEL_ID and only to user ids in DISCORD_ALLOWED_USERS. This is a hard gate; no fallback. A single-operator system.

What the bot does NOT do

  • It does not write to e-Financials directly. Every e-Fin write goes through the booker daemon or a resolver-spawned booker instance.
  • It does not run the EB poll loop. That's the webhooks container.
  • It does not hold long-running state in memory. The agent's conversation history, the operator's pending checkpoints, every reconciliation row — all in Turso.