-
Notifications
You must be signed in to change notification settings - Fork 0
Routing
Routing is the decision layer that sits between "what you typed" and "which Source actually answers it." Every query that comes in as source="auto" passes through this logic; explicit source requests (source="kiwix", source="forecast", etc.) skip most of it deliberately, on the assumption that if you already know which source you want, the router shouldn't second-guess you.
Query arrives, source="auto"
│
▼
Leading "if X, Y" structure?
(see Conditional Query Detection)
│
┌───────────┴───────────┐
▼ yes ▼ no
Search only the condition Contains a conjunction?
Frame response around (see Query Decomposition)
the real answer │
┌─────────────┴─────────────┐
▼ yes ▼ no
Split into sub-queries, Single intent —
route each one independently route directly
(recursive conditional │
re-check applies here too) │
│ │
└─────────────┬─────────────┘
▼
Keyword match found?
(INTENT_MAP lookup)
│
┌────────────┴────────────┐
▼ yes ▼ no
Use matched source(s) Ask the LLM to pick
(2+ matches = fusion) 1 source, or 2-3 for
│ fusion if complex
│ │
│ Discourse-framing
│ phrase detected?
│ │
│ ┌─────────┴─────────┐
│ ▼ yes ▼ no
│ Add kiwix if Use LLM's
│ not already chosen, choice as-is
│ escalate to fusion │
└────────────────┬───────────────────┘
▼
Query the chosen source(s)
│
▼
Result looks like "no results"?
(kiwix/news only — see below)
│
┌─────────┴─────────┐
▼ yes ▼ no
Fall back to web Return result,
Report source_used source_used =
= "web", log what was
fallback_occurred actually used
Keyword matching runs first, always, because it's free — no LLM call needed. INTENT_MAP holds a list of trigger phrases per source ("battery status" → ha, "will it rain" → forecast, "is everything up" → uptime, and so on). If a query matches triggers from more than one source, that's treated as a genuine multi-topic question and escalated straight to fusion without ever asking the LLM.
LLM-assisted selection only runs when no keyword matched at all. The LLM is shown the query plus a one-line description of every source, and asked to return either one source name, or 2–3 comma-separated names if the question seems complex enough to benefit from combining sources. This is also where Kiwix Disambiguation's alternate phrasings and book selection happen, and where Query Expansion kicks in for web.
Every LLM routing decision is cached for an hour (ROUTING_CACHE_TTL) — a query's source assignment is stable in practice, so paying the LLM cost twice for the same question is wasted work. See Caching for how that cache is bounded.
This exists because of a real, repeatedly-reproduced bug: queries phrased as current public discourse — "what's the deal with that whole mercury retrograde thing everyone keeps talking about" — kept routing past kiwix straight to news/web, even when the underlying topic was genuinely encyclopedic. The cause: news and web's own descriptions ("current events," "recent information") matched this kind of phrasing almost word-for-word, while kiwix's description gave the LLM no reason to think it covers evergreen topics that happen to be phrased as current chatter.
Rather than trying to nudge the LLM's judgment by rewording kiwix's description — an indirect, hard-to-verify lever — this is detected explicitly with a small set of literal phrase matches ("everyone keeps talking about", "everyone's obsessed with", and a few variants). If one of these phrases is present and kiwix wasn't already part of the chosen source(s), kiwix gets added and the result escalates to fusion. The full investigation, including a second bug found after the routing fix alone turned out not to be enough, is in The Discourse-Framing Investigation.
Two sources have a configured fallback target: kiwix and news both fall back to web if their own result looks empty. "Looks empty" is a literal phrase match against a known list — "no results found", "not configured", "could not connect", and several others — not a judgment call or a confidence score. If kiwix's response contains one of those phrases, web gets queried instead, transparently, and source_used in the response correctly reports "web" — never the originally-intended source that actually failed.
If a source you forgot to configure ever returned its own raw error message instead of automatically falling back to web, that's fixed now. router.py and fusion.py used to carry separate copies of the "looks empty" phrase list that had quietly drifted apart — confirmed directly: with FreshRSS unconfigured, a "give me the news" query returned the literal string "FreshRSS is not configured. Set FRESHRSS_URL and FRESHRSS_USER" as the actual result, with source_used: "news", because the routing module's own copy had never been taught to recognize "not configured" as a failure worth falling back from. Both now share one canonical list, living in fusion.py.
This matters because it used to not work that way: an earlier version of source_used reported the originally intended source regardless of whether a fallback happened, meaning a query that silently fell back from kiwix to web claimed source_used: "kiwix" while the real content came from somewhere else entirely. Fixed properly, and now also fully observable — see Health & Observability for how fallback_occurred gets tracked and surfaced in /logs/stats.
Routing decides which source(s) answer a query, but it's not the only layer that touches a query before that decision gets made:
- Query Decomposition runs first for any compound question, splitting it into independent sub-queries that each get routed separately
-
Conditional Query Detection runs before decomposition for leading
"if X, Y"phrasing, and is re-applied to each decomposed sub-query too - Fusion is what actually happens when routing decides more than one source is needed