Skip to content

Routing

Bob edited this page Jun 21, 2026 · 5 revisions

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.

The decision flow

                              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

Two ways a source gets chosen

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.

The discourse-framing bias

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.

Fallback — when a source comes back empty

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", "no recent articles", "could not fetch", and a few 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.

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.

Where this connects to everything else

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

Clone this wiki locally