Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions redisvl/extensions/router/semantic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +556 to +558

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


def remove_route(self, route_name: str) -> None:
"""Remove a route and all references from the semantic router.

Expand All @@ -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."""
Expand Down
115 changes: 115 additions & 0 deletions tests/integration/test_semantic_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading