Skip to content

Commit e285fed

Browse files
tuecodertylerhutchersonvishal-balaCopilot
authored
Task/483 add __repr__ methods (#515)
## Summary - Add `__repr__` to `SearchIndex` / `AsyncSearchIndex` (via `BaseSearchIndex`) showing `name`, `prefix`, and `storage_type` - Add `__repr__` to `SemanticCache` showing `name`, `distance_threshold`, and `ttl` - Add `__repr__` to `SemanticRouter` showing `name` and route count - Add `__repr__` to `MessageHistory` showing `name` and `session_tag` - Add `__repr__` to `SemanticMessageHistory` showing `name`, `session_tag`, and `distance_threshold` - Add `tests/unit/test_repr.py` with 11 unit tests covering all new reprs (no Redis required) ## Motivation Core objects previously fell back to the default `<ClassName object at 0x...>` repr, making notebook debugging, log inspection, and agent-generated diagnostics hard to read. All new reprs are: - **Human-readable** – field names and values are immediately obvious - **Deterministic** – same inputs always produce the same string - **Non-sensitive** – no passwords, URLs, or raw vectors are included ## Example output ```python >>> SearchIndex(schema=schema) SearchIndex(name='my-index', prefix='rvl', storage_type='hash') >>> SemanticCache(name='llmcache', distance_threshold=0.1, ttl=300) SemanticCache(name='llmcache', distance_threshold=0.1, ttl=300) >>> SemanticRouter(name='router', routes=[...]) SemanticRouter(name='router', routes=3) >>> MessageHistory(name='chat', session_tag='abc') MessageHistory(name='chat', session_tag='abc') >>> SemanticMessageHistory(name='chat', session_tag='abc', distance_threshold=0.3) SemanticMessageHistory(name='chat', session_tag='abc', distance_threshold=0.3) ``` For #483 <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk: adds string representations and unit tests only, with no changes to Redis interactions, query logic, or data handling. > > **Overview** > Adds deterministic, human-readable `__repr__` implementations to core RedisVL objects to improve debugging/logging output: `BaseSearchIndex` (covering `SearchIndex`/`AsyncSearchIndex`), `SemanticCache`, `SemanticRouter`, `MessageHistory`, and `SemanticMessageHistory`. > > Introduces `tests/unit/test_repr.py`, which validates the exact repr strings and uses mocking/`model_construct` to avoid requiring a running Redis instance or downloading embedding models. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 8bbb90b. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Tyler Hutcherson <tyler.hutcherson@redis.com> Co-authored-by: Vishal Bala <vishal-bala@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 7d7f51e commit e285fed

6 files changed

Lines changed: 230 additions & 0 deletions

File tree

redisvl/extensions/cache/llm/semantic.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,12 @@ def __init__(
174174
# Create the search index in Redis
175175
self._index.create(overwrite=self.overwrite, drop=False)
176176

177+
def __repr__(self) -> str:
178+
return (
179+
f"SemanticCache(name={self.name!r}, "
180+
f"distance_threshold={self.distance_threshold}, ttl={self.ttl})"
181+
)
182+
177183
def _modify_schema(
178184
self,
179185
schema: SemanticCacheIndexSchema,

redisvl/extensions/message_history/message_history.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ def __init__(
6868

6969
self._default_session_filter = Tag(SESSION_FIELD_NAME) == self._session_tag
7070

71+
def __repr__(self) -> str:
72+
return f"MessageHistory(name={self._name!r}, session_tag={self._session_tag!r})"
73+
7174
def clear(self) -> None:
7275
"""Clears the conversation message history."""
7376
self._index.clear()

redisvl/extensions/message_history/semantic_history.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,12 @@ def __init__(
120120

121121
self._default_session_filter = Tag(SESSION_FIELD_NAME) == self._session_tag
122122

123+
def __repr__(self) -> str:
124+
return (
125+
f"SemanticMessageHistory(name={self._name!r}, "
126+
f"session_tag={self._session_tag!r}, distance_threshold={self.distance_threshold})"
127+
)
128+
123129
def clear(self) -> None:
124130
"""Clears the message history."""
125131
self._index.clear()

redisvl/extensions/router/semantic.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,9 @@ def _initialize_index(
178178
# write the routes to Redis
179179
self._add_routes(self.routes)
180180

181+
def __repr__(self) -> str:
182+
return f"SemanticRouter(name={self.name!r}, routes={len(self.routes)})"
183+
181184
@property
182185
def route_names(self) -> List[str]:
183186
"""Get the list of route names.

redisvl/index/index.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,12 @@ def key(self, id: str) -> str:
400400
key_separator=self.schema.index.key_separator,
401401
)
402402

403+
def __repr__(self) -> str:
404+
return (
405+
f"{type(self).__name__}(name={self.name!r}, prefixes={self.prefixes!r}, "
406+
f"storage_type={self.storage_type.value!r})"
407+
)
408+
403409

404410
class SearchIndex(BaseSearchIndex):
405411
"""A search index class for interacting with Redis as a vector database.

tests/unit/test_repr.py

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
"""Unit tests for __repr__ implementations on core RedisVL classes."""
2+
3+
from unittest.mock import MagicMock, patch
4+
5+
import pytest
6+
7+
from redisvl.index import AsyncSearchIndex, SearchIndex
8+
from redisvl.schema import IndexSchema
9+
10+
# ---------------------------------------------------------------------------
11+
# Helpers
12+
# ---------------------------------------------------------------------------
13+
14+
15+
def _make_schema(name="my-index", prefix="rvl", storage_type="hash"):
16+
return IndexSchema.from_dict(
17+
{"index": {"name": name, "prefix": prefix, "storage_type": storage_type}}
18+
)
19+
20+
21+
def _make_mock_vectorizer(dims=768, dtype="float32"):
22+
mock_vec = MagicMock()
23+
mock_vec.dims = dims
24+
mock_vec.dtype = dtype
25+
return mock_vec
26+
27+
28+
# ---------------------------------------------------------------------------
29+
# SearchIndex / AsyncSearchIndex
30+
# ---------------------------------------------------------------------------
31+
32+
33+
def test_search_index_repr_hash():
34+
schema = _make_schema(name="test-index", prefix="rvl", storage_type="hash")
35+
index = SearchIndex(schema=schema)
36+
assert (
37+
repr(index)
38+
== "SearchIndex(name='test-index', prefixes=['rvl'], storage_type='hash')"
39+
)
40+
41+
42+
def test_search_index_repr_json():
43+
schema = _make_schema(name="docs", prefix="doc", storage_type="json")
44+
index = SearchIndex(schema=schema)
45+
assert (
46+
repr(index) == "SearchIndex(name='docs', prefixes=['doc'], storage_type='json')"
47+
)
48+
49+
50+
def test_async_search_index_repr():
51+
schema = _make_schema(name="async-idx", prefix="data", storage_type="json")
52+
index = AsyncSearchIndex(schema=schema)
53+
assert (
54+
repr(index)
55+
== "AsyncSearchIndex(name='async-idx', prefixes=['data'], storage_type='json')"
56+
)
57+
58+
59+
# ---------------------------------------------------------------------------
60+
# SemanticCache
61+
# ---------------------------------------------------------------------------
62+
63+
64+
@pytest.fixture()
65+
def patched_semantic_cache():
66+
"""Patch out Redis and HFTextVectorizer so SemanticCache can be instantiated
67+
without a running Redis server or ML model download."""
68+
mock_vec = _make_mock_vectorizer()
69+
mock_idx = MagicMock()
70+
mock_idx.exists.return_value = False
71+
72+
with patch(
73+
"redisvl.extensions.cache.llm.semantic.HFTextVectorizer",
74+
return_value=mock_vec,
75+
):
76+
with patch(
77+
"redisvl.extensions.cache.llm.semantic.SearchIndex",
78+
return_value=mock_idx,
79+
):
80+
yield
81+
82+
83+
def test_semantic_cache_repr_defaults(patched_semantic_cache):
84+
from redisvl.extensions.cache.llm.semantic import SemanticCache
85+
86+
cache = SemanticCache(name="llmcache", distance_threshold=0.1)
87+
assert (
88+
repr(cache)
89+
== "SemanticCache(name='llmcache', distance_threshold=0.1, ttl=None)"
90+
)
91+
92+
93+
def test_semantic_cache_repr_with_ttl(patched_semantic_cache):
94+
from redisvl.extensions.cache.llm.semantic import SemanticCache
95+
96+
cache = SemanticCache(name="my-cache", distance_threshold=0.2, ttl=300)
97+
assert (
98+
repr(cache) == "SemanticCache(name='my-cache', distance_threshold=0.2, ttl=300)"
99+
)
100+
101+
102+
# ---------------------------------------------------------------------------
103+
# SemanticRouter
104+
# ---------------------------------------------------------------------------
105+
106+
107+
def test_semantic_router_repr():
108+
from redisvl.extensions.router.schema import Route
109+
from redisvl.extensions.router.semantic import SemanticRouter
110+
111+
routes = [
112+
Route(name="greeting", references=["hello", "hi"]),
113+
Route(name="farewell", references=["bye", "goodbye"]),
114+
]
115+
# model_construct bypasses __init__ (and therefore Redis/vectorizer setup)
116+
# while still setting the Pydantic fields that __repr__ reads.
117+
router = SemanticRouter.model_construct(name="my-router", routes=routes)
118+
assert repr(router) == "SemanticRouter(name='my-router', routes=2)"
119+
120+
121+
def test_semantic_router_repr_single_route():
122+
from redisvl.extensions.router.schema import Route
123+
from redisvl.extensions.router.semantic import SemanticRouter
124+
125+
routes = [Route(name="support", references=["help", "issue"])]
126+
router = SemanticRouter.model_construct(name="support-router", routes=routes)
127+
assert repr(router) == "SemanticRouter(name='support-router', routes=1)"
128+
129+
130+
# ---------------------------------------------------------------------------
131+
# MessageHistory
132+
# ---------------------------------------------------------------------------
133+
134+
135+
@pytest.fixture()
136+
def patched_message_history():
137+
"""Patch SearchIndex.create so MessageHistory init doesn't need Redis."""
138+
with patch("redisvl.extensions.message_history.message_history.SearchIndex.create"):
139+
yield
140+
141+
142+
def test_message_history_repr(patched_message_history):
143+
from redisvl.extensions.message_history.message_history import MessageHistory
144+
145+
mh = MessageHistory(name="chat", session_tag="abc123")
146+
assert repr(mh) == "MessageHistory(name='chat', session_tag='abc123')"
147+
148+
149+
def test_message_history_repr_custom_name(patched_message_history):
150+
from redisvl.extensions.message_history.message_history import MessageHistory
151+
152+
mh = MessageHistory(name="support-chat", session_tag="sess-001")
153+
assert repr(mh) == "MessageHistory(name='support-chat', session_tag='sess-001')"
154+
155+
156+
# ---------------------------------------------------------------------------
157+
# SemanticMessageHistory
158+
# ---------------------------------------------------------------------------
159+
160+
161+
@pytest.fixture()
162+
def patched_semantic_message_history():
163+
"""Patch out Redis and HFTextVectorizer for SemanticMessageHistory."""
164+
mock_vec = _make_mock_vectorizer(dims=384)
165+
mock_idx = MagicMock()
166+
mock_idx.exists.return_value = False
167+
168+
with patch(
169+
"redisvl.extensions.message_history.semantic_history.HFTextVectorizer",
170+
return_value=mock_vec,
171+
):
172+
with patch(
173+
"redisvl.extensions.message_history.semantic_history.SearchIndex",
174+
return_value=mock_idx,
175+
):
176+
yield
177+
178+
179+
def test_semantic_message_history_repr(patched_semantic_message_history):
180+
from redisvl.extensions.message_history.semantic_history import (
181+
SemanticMessageHistory,
182+
)
183+
184+
smh = SemanticMessageHistory(
185+
name="sem-chat", session_tag="sess-42", distance_threshold=0.3
186+
)
187+
assert (
188+
repr(smh)
189+
== "SemanticMessageHistory(name='sem-chat', session_tag='sess-42', distance_threshold=0.3)"
190+
)
191+
192+
193+
def test_semantic_message_history_repr_custom_threshold(
194+
patched_semantic_message_history,
195+
):
196+
from redisvl.extensions.message_history.semantic_history import (
197+
SemanticMessageHistory,
198+
)
199+
200+
smh = SemanticMessageHistory(
201+
name="history", session_tag="s1", distance_threshold=0.5
202+
)
203+
assert (
204+
repr(smh)
205+
== "SemanticMessageHistory(name='history', session_tag='s1', distance_threshold=0.5)"
206+
)

0 commit comments

Comments
 (0)