From db7b07bb15ce0966c835583ab626dbd2815942e7 Mon Sep 17 00:00:00 2001 From: Robert Shelton Date: Tue, 9 Jun 2026 11:50:20 -0400 Subject: [PATCH] make add_route public --- redisvl/extensions/router/semantic.py | 28 ++++++ tests/integration/test_semantic_router.py | 115 ++++++++++++++++++++++ uv.lock | 4 +- 3 files changed, 145 insertions(+), 2 deletions(-) diff --git a/redisvl/extensions/router/semantic.py b/redisvl/extensions/router/semantic.py index 53767040b..c6d336564 100644 --- a/redisvl/extensions/router/semantic.py +++ b/redisvl/extensions/router/semantic.py @@ -530,6 +530,33 @@ def route_many( return top_route_matches + def add_route(self, route: Route) -> str: + """Add a new route to the SemanticRouter. + + Embeds the route's references, writes them to the Redis index, + appends the route to ``self.routes``, and persists the updated router + config so the route survives :meth:`SemanticRouter.from_existing`. + + Args: + route (Route): A fully-formed Route (name, references, + distance_threshold, optional metadata). + + Returns: + str: The added route's name. + + Raises: + ValueError: If a route with this name already exists on the router. + Use :meth:`add_route_references` to extend an existing route. + """ + if self.get(route.name) is not None: + raise ValueError( + f"Route {route.name!r} already exists on router {self.name!r}; " + f"use add_route_references() to add references to it" + ) + self._add_routes([route]) + self._update_router_state() + return route.name + def remove_route(self, route_name: str) -> None: """Remove a route and all references from the semantic router. @@ -547,6 +574,7 @@ def remove_route(self, route_name: str) -> None: ] ) self.routes = [route for route in self.routes if route.name != route_name] + self._update_router_state() def delete(self) -> None: """Delete the semantic router index.""" diff --git a/tests/integration/test_semantic_router.py b/tests/integration/test_semantic_router.py index d1457d28c..872cb531c 100644 --- a/tests/integration/test_semantic_router.py +++ b/tests/integration/test_semantic_router.py @@ -170,6 +170,87 @@ def test_add_route(semantic_router): assert match.name == "politics" +def test_add_route_public(semantic_router): + new_route = Route( + name="politics", + references=[ + "are you liberal or conservative?", + "who will you vote for?", + "political speech", + ], + metadata={"type": "politics"}, + distance_threshold=0.3, + ) + added_name = semantic_router.add_route(new_route) + assert added_name == "politics" + + route = semantic_router.get("politics") + assert route is not None + assert route.name == "politics" + assert "political speech" in route.references + + redis_version = semantic_router._index.client.info()["redis_version"] + if is_version_gte(redis_version, "7.0.0"): + match = semantic_router("political speech") + assert match is not None + assert match.name == "politics" + + +def test_add_route_survives_from_existing( + client, redis_url, routes, redis_test_name, hf_vectorizer +): + skip_if_no_redis_search(client) + skip_if_redis_version_below(client, "7.0.0") + + router = None + try: + router = SemanticRouter( + name=redis_test_name("test_add_route_persist"), + routes=routes, + routing_config=RoutingConfig(max_k=2), + redis_client=client, + overwrite=True, + vectorizer=hf_vectorizer, + ) + + new_route = Route( + name="politics", + references=["political speech", "who will you vote for?"], + metadata={"type": "politics"}, + distance_threshold=0.3, + ) + router.add_route(new_route) + + reloaded = SemanticRouter.from_existing( + name=router.name, + redis_client=client, + ) + reloaded_route = reloaded.get("politics") + assert reloaded_route is not None + assert "political speech" in reloaded_route.references + assert {r.name for r in reloaded.routes} == { + "greeting", + "farewell", + "politics", + } + finally: + if router is not None: + with suppress(Exception): + router.clear() + with suppress(Exception): + router.delete() + + +def test_add_route_duplicate_raises(semantic_router): + duplicate = Route( + name="greeting", + references=["howdy"], + distance_threshold=0.3, + ) + with pytest.raises(ValueError): + semantic_router.add_route(duplicate) + + def test_remove_routes(semantic_router): semantic_router.remove_route("greeting") assert semantic_router.get("greeting") is None @@ -178,6 +259,40 @@ def test_remove_routes(semantic_router): assert semantic_router.get("unknown_route") is None +def test_remove_route_survives_from_existing( + client, redis_url, routes, redis_test_name, hf_vectorizer +): + skip_if_no_redis_search(client) + skip_if_redis_version_below(client, "7.0.0") + + router = None + try: + router = SemanticRouter( + name=redis_test_name("test_remove_route_persist"), + routes=routes, + routing_config=RoutingConfig(max_k=2), + redis_client=client, + overwrite=True, + vectorizer=hf_vectorizer, + ) + + router.remove_route("greeting") + assert router.get("greeting") is None + + reloaded = SemanticRouter.from_existing( + name=router.name, + redis_client=client, + ) + assert reloaded.get("greeting") is None + assert {r.name for r in reloaded.routes} == {"farewell"} + finally: + if router is not None: + with suppress(Exception): + router.clear() + with suppress(Exception): + router.delete() + + def test_to_dict(semantic_router): router_dict = semantic_router.to_dict() assert router_dict["name"] == semantic_router.name diff --git a/uv.lock b/uv.lock index f0dee76b2..0a2ea0dc5 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.10, <3.15" resolution-markers = [ "python_full_version >= '3.14'", @@ -4869,7 +4869,7 @@ wheels = [ [[package]] name = "redisvl" -version = "0.19.0" +version = "0.20.0" source = { editable = "." } dependencies = [ { name = "jsonpath-ng" },