Intent classifier
Source: agent/src/accounting_agent/intent_classifier/.
Used by the chat agent (/ask) to route the operator's question
to the right tools without making the main agent do
multi-part-query decomposition. Built on DSPy because the
problem is exactly what DSPy is for: a small classifier with
labelled examples + a metric.
What it does
Given a user message, return:
IntentClassification(
intent: "ask" | "decompose" | "choices" | "general",
parts: list[str] | None, # populated for decompose
options: list[str] | None, # populated for choices
)
- ask — one-shot question with a clear answer the main agent can produce: "what did I spend in May?"
- decompose — multi-part query that needs to be split: "show me my expenses last quarter AND tell me the VAT" → two sub-questions answered separately, then merged.
- choices — the user is picking between known options:
"should I book recon #44 or #45?" → spawns an
agent_askHITL with the options. - general — small talk / off-topic. The main agent answers directly without tools.
Where the dataset lives
intent_classifier_examples Turso table. ~25 seed examples
shipped with the agent (intent_classifier/examples.py).
Operator can add more via /teach <message> <intent> or by
correcting the classifier when it's wrong (each correction lands
as a new dataset row).
When the seed count + corrections cross 25 uncompiled examples,
the classifier emits a "consider /recompile" log line. /recompile
runs DSPy's compiler against the dataset and writes the new
prompt + few-shot demos to the DB.
Why DSPy
- Classification metrics are clear (precision/recall on labelled examples)
- Small enough that a 25-example training set is meaningful
- DSPy's optimizer beats hand-tuning prompts at the margin
- Provider-agnostic via OpenRouter (default model:
openai/gpt-4o-minifor cheap classification)
What it does NOT do
- It does not answer the user's question. Just labels it.
- It does not pause the main agent — classification is a fast, blocking call inside the orchestrator.
- It does not run on every Discord message. Only on
/askand on operator-DMs to the bot.