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 —
BookerHitlPollerticks 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.