Router Group
On this page
Provides the "module" abstraction: RouteGroup is a self-contained namespace of routes with its own intent description, usable as a first-class routing target by the top-level router. Also provides three built-in group templates.
RouteGroup
class RouteGroup:
def __init__(
self,
name: str,
description: str,
intent_examples: Optional[List[str]] = None,
intent_keywords: Optional[RouteKeywords] = None,
priority: int = 0,
enabled: bool = True,
metadata: Optional[Dict[str, Any]] = None,
): ...
A named collection of Route objects sharing a common intent. The parent HierarchicalRouter first identifies the best group, then delegates route selection within it.
Constructor
Parameters:
Raises: ValueError — if name is empty or whitespace-only.
Attributes
(_routes: Dict[str, Route] is a private implementation detail backing route storage — accessed only through the methods below.)
Route management methods
add_route(self, route: Route) -> RouteGroup
Registers a route under this group. Sets route.group = self.name as a side effect.
Raises: ValueError — if a route with the same name already exists in this group ("Route '{name}' already exists in group '{group}'. Use update_route() to replace it.").
Returns: self — supports fluent chaining: group.add_route(r1).add_route(r2).
update_route(self, route: Route) -> RouteGroup
Adds a route, or replaces an existing one with the same name (no error on collision, unlike add_route). Sets route.group = self.name.
Returns: self.
remove_route(self, name: str) -> bool
Removes a route by name.
Returns: True if removed, False if no route with that name existed.
get_route(self, name: str) -> Optional[Route]
Direct lookup by name; None if not found.
get_routes(self, enabled_only: bool = True) -> List[Route]
Returns all routes (optionally filtered to enabled=True), sorted by priority descending.
get_by_tag(self, tag: str) -> List[Route]
Returns all routes (regardless of enabled state) whose tags set contains tag. Not priority-sorted.
Fluent builder / decorator
route(self, name, description, examples=None, keywords=None, priority=0, tags=None, tools=None)
Returns a decorator that registers the decorated class or function as a handler for a new Route, and returns the original decorated object unchanged.
Behavior by decorated type:
Example:
@group.route(
name="greet",
description="Greet the user",
examples=["hello", "hi"],
)
class GreetHandler(BaseHandler):
def handle(self, request):
return HandlerResponse.ok("Hi there!")
[!WARNING] No-argument instantiation only Because a decorated
BaseHandlersubclass is instantiated ashandler_or_cls(), this decorator pattern only works for handlers whose__init__takes no required arguments. Handlers needing configuration (API keys, DB connections) should be instantiated manually and passed toadd_route(Route(..., handler=my_instance))instead.
Lifecycle methods
enable(self) / disable(self)
Toggle self.enabled; refresh updated_at. Note this does not cascade to the group's individual routes — a disabled group's routes retain their own enabled state, they're simply not reachable because RoutingPipeline.run filters active_groups = [g for g in groups if g.enabled] before ever considering routes.
invalidate_embeddings(self)
Clears self.embeddings and calls route.invalidate_embeddings() on every contained route — a cascading invalidation, unlike enable/disable. Refreshes updated_at.
Introspection
summary(self) -> Dict[str, Any]
Returns {"name", "description", "enabled", "priority", "route_count", "routes" (list of names), "metadata"}.
Magic methods
__len__— number of routes in the group.__iter__— iterates overRouteobjects (viaself._routes.values()) — order is Python dict insertion order, not priority-sorted (unlikeget_routes()).__contains__(name)—name in groupchecks route names.__repr__— e.g.RouteGroup(name='rag', routes=3, status=enabled, priority=10).
Examples
1. Basic:
from zerda.router import RouteGroup, Route
support = RouteGroup("support", "Handle customer support requests")
support.add_route(Route(name="refunds", description="Process refunds", handler=RefundHandler()))
2. Intermediate — fluent chaining + decorator style mixed:
support = (
RouteGroup("support", "Handle customer support requests")
.add_route(Route(name="refunds", description="Process refunds", handler=RefundHandler()))
)
@support.route(name="shipping", description="Track shipments", examples=["where's my order"])
def shipping_handler(query: str):
return {"status": "in transit"}
3. Advanced — tag-based filtering:
group.add_route(Route(name="a", description="...", handler=h1, tags={"beta"}))
group.add_route(Route(name="b", description="...", handler=h2, tags={"beta", "internal"}))
beta_routes = group.get_by_tag("beta") # [route a, route b]
4. Error handling — duplicate route name:
try:
group.add_route(Route(name="refunds", description="dup", handler=h))
except ValueError as e:
print(e) # Route 'refunds' already exists in group 'support'. Use update_route() to replace it.
5. Production-ready — bulk registration with router:
support = RouteGroup("support", "Customer support")
for name, desc, handler, examples in load_route_definitions():
support.add_route(Route(name=name, description=desc, handler=handler, examples=examples))
router.register_group(support) # encodes all embeddings in one call
Best practices
- Prefer
get_routes()over iterating the group directly (for r in group) when route order matters — onlyget_routes()guarantees priority ordering. - Call
invalidate_embeddings()(not individualroute.invalidate_embeddings()calls) after bulk-editing many routes' examples at once, then re-encode viarouter.re_encode_group(name).
Common mistakes
- Relying on
for route in group:to yield routes in priority order — it yields insertion order. - Calling
group.disable()and expecting individual routes to reportenabled=False— they don't; only the group-level gate changes.
Related APIs: Route, HierarchicalRouter.register_group
Built-in group templates
Three factory functions return pre-configured RouteGroup instances covering the most common LLM application intent domains. Each returns an empty group (no routes) — you still need to add_route(...) your own routes; the templates only provide the group-level description, intent_examples, intent_keywords, and priority.
make_rag_group
def make_rag_group(priority: int = 10) -> RouteGroup
Template for a Retrieval-Augmented Generation intent group — "answer questions by retrieving and synthesising from documents, knowledge bases, or vector stores."
make_tools_group
def make_tools_group(priority: int = 20) -> RouteGroup
Template for a tool/function-calling intent group — "execute actions using tools: call APIs, run code, query databases, send emails, manage calendars."
make_chat_group
def make_chat_group(priority: int = 0) -> RouteGroup
Template for a general conversational intent group — "handle general conversation, creative writing, coding help, and open-ended questions." Intended as the lowest-priority catch-all.
[!TIP] Why
toolsoutranksragoutrankschatby default The default priorities (tools=20 > rag=10 > chat=0) reflect that action-taking intents are usually more specific/urgent signals than document lookups, which are in turn more specific than general chat — this ordering only matters as a scoring tie-breaker (viaRoute.priority/group priority), not as a hard override of the semantic/keyword score.
Example: assembling the three templates
from zerda.router import HierarchicalRouter, make_rag_group, make_tools_group, make_chat_group, Route
router = HierarchicalRouter()
rag = make_rag_group()
rag.add_route(Route(name="docs_qa", description="...", handler=DocsQAHandler(), examples=[...]))
tools = make_tools_group()
tools.add_route(Route(name="weather", description="...", handler=WeatherHandler(), examples=[...]))
chat = make_chat_group()
chat.add_route(Route(name="general", description="...", handler=ChatHandler(), examples=[...]))
for g in (rag, tools, chat):
router.register_group(g)
Related APIs: RouteGroup, RouteKeywords, Quickstart