Skip to content
nerva docs v0.2.1

Router

The Router takes a raw user message and returns a structured IntentResult with a confidence score and ranked handler candidates.

Protocol

class IntentRouter(Protocol):
async def classify(self, message: str, ctx: ExecContext) -> IntentResult:
...

IntentResult

@dataclass(frozen=True)
class IntentResult:
intent: str # classified intent label
confidence: float # 0.0 - 1.0
handlers: list[HandlerCandidate] # ranked, best first
raw_scores: dict[str, float] # per-handler scores (debugging)
@dataclass(frozen=True)
class HandlerCandidate:
name: str # must match a registry entry
score: float # 0.0 - 1.0
reason: str # why this handler was selected

Strategies

RuleRouter

Deterministic regex matching. First match wins. Zero LLM cost.

from nerva.router.rule import RuleRouter, Rule
router = RuleRouter(
rules=[
Rule(pattern=r"weather", handler="weather_agent", intent="weather"),
Rule(pattern=r"calendar|schedule", handler="calendar_agent", intent="calendar"),
],
default_handler="general_agent",
)
result = await router.classify("What's the weather?", ctx)
# intent="weather", confidence=1.0, handler="weather_agent"

When to use: < 10 agents, keywords are unambiguous, you want deterministic routing.

EmbeddingRouter

Cosine similarity against handler descriptions. No LLM call needed — just an embedding model.

from nerva.router.embedding import EmbeddingRouter
router = EmbeddingRouter(embed=my_embed_func, threshold=0.3, top_k=5)
await router.register("weather_agent", "Answer weather and forecast questions")
await router.register("calendar_agent", "Manage calendar events and scheduling")
result = await router.classify("Will it rain tomorrow?", ctx)
# intent="semantic", confidence=0.87, handler="weather_agent"

When to use: 10+ agents, natural language overlap between domains, you want fast sub-50ms routing without LLM costs.

LLMRouter

Sends the handler catalog to an LLM and asks for structured output.

from nerva.router.llm import LLMRouter
router = LLMRouter(llm=my_llm_func)
await router.register("flight_agent", "Book airline flights and manage reservations")
await router.register("hotel_agent", "Reserve hotel rooms")
result = await router.classify("Book me a flight to Berlin next Tuesday", ctx)
# intent="llm", confidence=0.95, handler="flight_agent"

When to use: Complex queries that need reasoning, multi-intent messages, high accuracy requirements.

HybridRouter

Embedding pre-filter then LLM re-rank. Best of both worlds.

from nerva.router.hybrid import HybridRouter
router = HybridRouter(
embed=my_embed_func,
rerank=my_rerank_func,
embedding_threshold=0.2,
pre_filter_k=10, # pre-filter to top 10
final_k=5, # return top 5 after re-ranking
)

When to use: Large handler catalogs (50+) where LLM routing over the full catalog is too slow or expensive.

Choosing a strategy

CriteriaRuleEmbeddingLLMHybrid
Latency~0ms~10ms~500ms~50ms (cache hit) / ~550ms
CostFreeEmbedding modelLLM tokensBoth
AccuracyExact match onlyGoodBestBest
Setup effortManual rulesHandler descriptionsHandler descriptionsBoth routers
Scales to~20 handlers~200 handlers~50 handlers~500 handlers

Start with RuleRouter. Switch to EmbeddingRouter when keyword overlap causes misroutes. Add HybridRouter when you need both speed and accuracy at scale.