Query routing¶
The query classifier routes each retrieve() call to a per-type weight profile,
applying different retrieval strategies depending on whether the query targets an
emotion state, an affect-conditioned topic, or a temporal narrative trajectory.
This tutorial covers:
- Enabling the heuristic classifier (zero config, fast)
- Plugging in the Addendum L routing table (
LOCOMO_ROUTING) - Writing a custom classifier
- (Advanced) LLM-backed classification
1. Enable heuristic routing¶
from emotional_memory import EmotionalMemory, InMemoryStore, EmotionalMemoryConfig
from emotional_memory.retrieval import RetrievalConfig, QueryClassifierConfig
from emotional_memory.query_classifier import HeuristicQueryClassifier, LOCOMO_ROUTING
config = EmotionalMemoryConfig(
retrieval=RetrievalConfig(
query_classifier=QueryClassifierConfig(
routed_weights=LOCOMO_ROUTING,
)
)
)
em = EmotionalMemory(
store=InMemoryStore(),
embedder=my_embedder,
config=config,
query_classifier=HeuristicQueryClassifier(),
)
The engine now classifies every query and applies the matching weight vector.
Unrecognised types fall back to default_type (default: "default").
2. The LOCOMO routing table¶
LOCOMO_ROUTING maps each LoCoMo question type to a 6-element weight vector
[semantic, mood_congruence, affect_proximity, momentum, recency, resonance_boost]:
| Type | Weights | Strategy |
|---|---|---|
single_hop |
[0.70, 0.10, 0.05, 0.05, 0.05, 0.05] |
Suppress affect, maximise semantic for literal-fact questions |
multi_hop |
[0.50, 0.30, 0.10, 0.05, 0.05, 0.00] |
Moderate mood, no resonance |
open_domain |
[0.40, 0.20, 0.15, 0.10, 0.10, 0.05] |
Balanced semantic + mood |
temporal |
[0.50, 0.30, 0.10, 0.05, 0.05, 0.00] |
Same as multi_hop |
default |
[0.35, 0.25, 0.15, 0.10, 0.10, 0.05] |
Standard AFT baseline |
Empirical note: These weights are derived from Addendum J closure (oracle Pareto sweep). Addendum L (confirmatory, N=1540) tested whether routing improves over W2 on LoCoMo — result pending. The routing table is pre-registered; use
LOCOMO_ROUTINGas-is for reproducibility.
3. Custom classifier¶
Implement the QueryClassifier protocol (a single classify(query: str) -> str method):
from emotional_memory.query_classifier import QueryClassifier, QUERY_TYPES
class EmotionKeywordClassifier:
"""Routes queries mentioning an emotion word to open_domain."""
_EMOTION_WORDS = frozenset(["happy", "sad", "angry", "afraid", "surprised"])
def classify(self, query: str) -> str:
if any(w in query.lower() for w in self._EMOTION_WORDS):
return "open_domain"
return "default"
Pass it to EmotionalMemory:
em = EmotionalMemory(
store=InMemoryStore(),
embedder=my_embedder,
config=config,
query_classifier=EmotionKeywordClassifier(),
)
Unknown labels are silently mapped to default_type — no exception.
4. LLM-backed classifier (advanced)¶
LLMQueryClassifier uses an OpenAI-compatible endpoint to classify queries.
It mirrors the LLMAppraisalEngine pattern (thread-safe, LRU cache):
from emotional_memory.query_classifier import LLMQueryClassifier
from emotional_memory.llm_http import make_httpx_llm_from_env
classifier = LLMQueryClassifier(
llm=make_httpx_llm_from_env(),
valid_types=frozenset(LOCOMO_ROUTING.keys()),
default_type="default",
cache_size=512,
)
Requires EMOTIONAL_MEMORY_LLM_API_KEY in the environment. See
LLM configuration for environment variables.
Performance warning:
LLMQueryClassifierissues one LLM call perretrieve(). On benchmarks this makes runs impractically slow (~10 h for 1 540 QA). The defaultrouting_runnertherefore excludesaft_routed_llm— add--with-llm-classifierif you explicitly want to measure it.
5. Benchmark execution notes¶
Run Addendum L (LoCoMo routing benchmark) via the Makefile target:
By default five systems are evaluated (aft_routed_heuristic, aft_W0,
aft_W2, naive_rag, aft_oracle_routed). The runner supports:
- Checkpoint/resume —
--checkpoint(default) writes a JSONL file; restart after a crash resumes from the last completed conversation. - Incremental JSON —
routing_results.jsonis rewritten after each system so partial results are never lost. - Progress bars —
tqdmon conversations, QA pairs, and judging. - Smoke test —
--subset 200qaruns a stratified 200-question subset (~20 min total) for quick validation before a full run.
# Smoke test
PYTHONUNBUFFERED=1 uv run python -m benchmarks.locomo.routing_runner \
--subset 200qa --seed 42 --verbose
# With LLM classifier (slow)
PYTHONUNBUFFERED=1 uv run python -m benchmarks.locomo.routing_runner \
--with-llm-classifier --verbose