"""Decide which corpora a question needs to reach. Deliberately rule-based rather than model-based. Routing here is a cheap, verifiable decision on a handful of surface signals, and an LLM call per question would add latency and non-determinism to a step a substring match answers exactly. The policy is asymmetric on purpose: * Narrowing to a subset requires the question to **name** a corpus — "nach dem Rahmenvertrag", "§ 31 SGB V". * Everything else queries all corpora and lets score fusion decide. That asymmetry follows from the cost of being wrong. One corpus too many costs a few hundred milliseconds; one corpus too few means the assistant answers confidently from the wrong statute, which is the failure mode this pipeline exists to prevent. A bare "§ 16" is therefore *not* narrowed even though the Rahmenvertrag is the likelier intent — both documents have a § 16, and they say entirely different things. """ from __future__ import annotations from typing import Any, Dict, List, Sequence from corpus_registry import CORPUS_ALIASES def _normalize(text: str) -> str: return " ".join(str(text or "").lower().split()) def mentioned_corpora(question: str, corpus_ids: Sequence[str]) -> List[str]: """Corpora named in the question, in registry order. A statute reference such as "§ 31 SGB V" is covered by this same check: the statute name is itself an alias, so no separate norm-reference parsing is needed here. Parsing the § itself is the retriever's job. """ haystack = _normalize(question) hits: List[str] = [] for corpus_id in corpus_ids: candidates = (*CORPUS_ALIASES.get(corpus_id.lower(), ()), corpus_id.lower()) if any(alias and alias in haystack for alias in candidates): hits.append(corpus_id) return hits def route_question(question: str, corpus_ids: Sequence[str]) -> List[str]: """Return the corpora to query. Falls back to all of them without a signal.""" available = list(corpus_ids) if len(available) <= 1: return available return mentioned_corpora(question, available) or available def explain_routing(question: str, corpus_ids: Sequence[str]) -> Dict[str, Any]: """Routing decision with its reason, for /debug/routing and tests.""" available = list(corpus_ids) named = mentioned_corpora(question, available) selected = route_question(question, available) return { "question": question, "available": available, "named_corpora": named, "selected": selected, "reason": "explicit_mention" if named and len(available) > 1 else "no_signal_query_all", }