Skip to content

Commit 7177f4a

Browse files
authored
make add_route public (#624)
This resolves #623
1 parent cb92196 commit 7177f4a

3 files changed

Lines changed: 145 additions & 2 deletions

File tree

redisvl/extensions/router/semantic.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,33 @@ def route_many(
530530

531531
return top_route_matches
532532

533+
def add_route(self, route: Route) -> str:
534+
"""Add a new route to the SemanticRouter.
535+
536+
Embeds the route's references, writes them to the Redis index,
537+
appends the route to ``self.routes``, and persists the updated router
538+
config so the route survives :meth:`SemanticRouter.from_existing`.
539+
540+
Args:
541+
route (Route): A fully-formed Route (name, references,
542+
distance_threshold, optional metadata).
543+
544+
Returns:
545+
str: The added route's name.
546+
547+
Raises:
548+
ValueError: If a route with this name already exists on the router.
549+
Use :meth:`add_route_references` to extend an existing route.
550+
"""
551+
if self.get(route.name) is not None:
552+
raise ValueError(
553+
f"Route {route.name!r} already exists on router {self.name!r}; "
554+
f"use add_route_references() to add references to it"
555+
)
556+
self._add_routes([route])
557+
self._update_router_state()
558+
return route.name
559+
533560
def remove_route(self, route_name: str) -> None:
534561
"""Remove a route and all references from the semantic router.
535562
@@ -547,6 +574,7 @@ def remove_route(self, route_name: str) -> None:
547574
]
548575
)
549576
self.routes = [route for route in self.routes if route.name != route_name]
577+
self._update_router_state()
550578

551579
def delete(self) -> None:
552580
"""Delete the semantic router index."""

tests/integration/test_semantic_router.py

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,87 @@ def test_add_route(semantic_router):
170170
assert match.name == "politics"
171171

172172

173+
def test_add_route_public(semantic_router):
174+
new_route = Route(
175+
name="politics",
176+
references=[
177+
"are you liberal or conservative?",
178+
"who will you vote for?",
179+
"political speech",
180+
],
181+
metadata={"type": "politics"},
182+
distance_threshold=0.3,
183+
)
184+
added_name = semantic_router.add_route(new_route)
185+
assert added_name == "politics"
186+
187+
route = semantic_router.get("politics")
188+
assert route is not None
189+
assert route.name == "politics"
190+
assert "political speech" in route.references
191+
192+
redis_version = semantic_router._index.client.info()["redis_version"]
193+
if is_version_gte(redis_version, "7.0.0"):
194+
match = semantic_router("political speech")
195+
assert match is not None
196+
assert match.name == "politics"
197+
198+
199+
def test_add_route_survives_from_existing(
200+
client, redis_url, routes, redis_test_name, hf_vectorizer
201+
):
202+
skip_if_no_redis_search(client)
203+
skip_if_redis_version_below(client, "7.0.0")
204+
205+
router = None
206+
try:
207+
router = SemanticRouter(
208+
name=redis_test_name("test_add_route_persist"),
209+
routes=routes,
210+
routing_config=RoutingConfig(max_k=2),
211+
redis_client=client,
212+
overwrite=True,
213+
vectorizer=hf_vectorizer,
214+
)
215+
216+
new_route = Route(
217+
name="politics",
218+
references=["political speech", "who will you vote for?"],
219+
metadata={"type": "politics"},
220+
distance_threshold=0.3,
221+
)
222+
router.add_route(new_route)
223+
224+
reloaded = SemanticRouter.from_existing(
225+
name=router.name,
226+
redis_client=client,
227+
)
228+
reloaded_route = reloaded.get("politics")
229+
assert reloaded_route is not None
230+
assert "political speech" in reloaded_route.references
231+
assert {r.name for r in reloaded.routes} == {
232+
"greeting",
233+
"farewell",
234+
"politics",
235+
}
236+
finally:
237+
if router is not None:
238+
with suppress(Exception):
239+
router.clear()
240+
with suppress(Exception):
241+
router.delete()
242+
243+
244+
def test_add_route_duplicate_raises(semantic_router):
245+
duplicate = Route(
246+
name="greeting",
247+
references=["howdy"],
248+
distance_threshold=0.3,
249+
)
250+
with pytest.raises(ValueError):
251+
semantic_router.add_route(duplicate)
252+
253+
173254
def test_remove_routes(semantic_router):
174255
semantic_router.remove_route("greeting")
175256
assert semantic_router.get("greeting") is None
@@ -178,6 +259,40 @@ def test_remove_routes(semantic_router):
178259
assert semantic_router.get("unknown_route") is None
179260

180261

262+
def test_remove_route_survives_from_existing(
263+
client, redis_url, routes, redis_test_name, hf_vectorizer
264+
):
265+
skip_if_no_redis_search(client)
266+
skip_if_redis_version_below(client, "7.0.0")
267+
268+
router = None
269+
try:
270+
router = SemanticRouter(
271+
name=redis_test_name("test_remove_route_persist"),
272+
routes=routes,
273+
routing_config=RoutingConfig(max_k=2),
274+
redis_client=client,
275+
overwrite=True,
276+
vectorizer=hf_vectorizer,
277+
)
278+
279+
router.remove_route("greeting")
280+
assert router.get("greeting") is None
281+
282+
reloaded = SemanticRouter.from_existing(
283+
name=router.name,
284+
redis_client=client,
285+
)
286+
assert reloaded.get("greeting") is None
287+
assert {r.name for r in reloaded.routes} == {"farewell"}
288+
finally:
289+
if router is not None:
290+
with suppress(Exception):
291+
router.clear()
292+
with suppress(Exception):
293+
router.delete()
294+
295+
181296
def test_to_dict(semantic_router):
182297
router_dict = semantic_router.to_dict()
183298
assert router_dict["name"] == semantic_router.name

uv.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)