diff --git a/redisvl/extensions/cache/llm/semantic.py b/redisvl/extensions/cache/llm/semantic.py index 8824b3be..508da3de 100644 --- a/redisvl/extensions/cache/llm/semantic.py +++ b/redisvl/extensions/cache/llm/semantic.py @@ -174,6 +174,12 @@ def __init__( # Create the search index in Redis self._index.create(overwrite=self.overwrite, drop=False) + def __repr__(self) -> str: + return ( + f"SemanticCache(name={self.name!r}, " + f"distance_threshold={self.distance_threshold}, ttl={self.ttl})" + ) + def _modify_schema( self, schema: SemanticCacheIndexSchema, diff --git a/redisvl/extensions/message_history/message_history.py b/redisvl/extensions/message_history/message_history.py index d890fea3..4d2fa7d7 100644 --- a/redisvl/extensions/message_history/message_history.py +++ b/redisvl/extensions/message_history/message_history.py @@ -68,6 +68,9 @@ def __init__( self._default_session_filter = Tag(SESSION_FIELD_NAME) == self._session_tag + def __repr__(self) -> str: + return f"MessageHistory(name={self._name!r}, session_tag={self._session_tag!r})" + def clear(self) -> None: """Clears the conversation message history.""" self._index.clear() diff --git a/redisvl/extensions/message_history/semantic_history.py b/redisvl/extensions/message_history/semantic_history.py index 35bfb0ed..3def5ef7 100644 --- a/redisvl/extensions/message_history/semantic_history.py +++ b/redisvl/extensions/message_history/semantic_history.py @@ -120,6 +120,12 @@ def __init__( self._default_session_filter = Tag(SESSION_FIELD_NAME) == self._session_tag + def __repr__(self) -> str: + return ( + f"SemanticMessageHistory(name={self._name!r}, " + f"session_tag={self._session_tag!r}, distance_threshold={self.distance_threshold})" + ) + def clear(self) -> None: """Clears the message history.""" self._index.clear() diff --git a/redisvl/extensions/router/semantic.py b/redisvl/extensions/router/semantic.py index 66a00d20..a7656c2d 100644 --- a/redisvl/extensions/router/semantic.py +++ b/redisvl/extensions/router/semantic.py @@ -178,6 +178,9 @@ def _initialize_index( # write the routes to Redis self._add_routes(self.routes) + def __repr__(self) -> str: + return f"SemanticRouter(name={self.name!r}, routes={len(self.routes)})" + @property def route_names(self) -> List[str]: """Get the list of route names. diff --git a/redisvl/index/index.py b/redisvl/index/index.py index c1abd011..5f7c947b 100644 --- a/redisvl/index/index.py +++ b/redisvl/index/index.py @@ -400,6 +400,12 @@ def key(self, id: str) -> str: key_separator=self.schema.index.key_separator, ) + def __repr__(self) -> str: + return ( + f"{type(self).__name__}(name={self.name!r}, prefixes={self.prefixes!r}, " + f"storage_type={self.storage_type.value!r})" + ) + class SearchIndex(BaseSearchIndex): """A search index class for interacting with Redis as a vector database. diff --git a/tests/unit/test_repr.py b/tests/unit/test_repr.py new file mode 100644 index 00000000..079d3a87 --- /dev/null +++ b/tests/unit/test_repr.py @@ -0,0 +1,206 @@ +"""Unit tests for __repr__ implementations on core RedisVL classes.""" + +from unittest.mock import MagicMock, patch + +import pytest + +from redisvl.index import AsyncSearchIndex, SearchIndex +from redisvl.schema import IndexSchema + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _make_schema(name="my-index", prefix="rvl", storage_type="hash"): + return IndexSchema.from_dict( + {"index": {"name": name, "prefix": prefix, "storage_type": storage_type}} + ) + + +def _make_mock_vectorizer(dims=768, dtype="float32"): + mock_vec = MagicMock() + mock_vec.dims = dims + mock_vec.dtype = dtype + return mock_vec + + +# --------------------------------------------------------------------------- +# SearchIndex / AsyncSearchIndex +# --------------------------------------------------------------------------- + + +def test_search_index_repr_hash(): + schema = _make_schema(name="test-index", prefix="rvl", storage_type="hash") + index = SearchIndex(schema=schema) + assert ( + repr(index) + == "SearchIndex(name='test-index', prefixes=['rvl'], storage_type='hash')" + ) + + +def test_search_index_repr_json(): + schema = _make_schema(name="docs", prefix="doc", storage_type="json") + index = SearchIndex(schema=schema) + assert ( + repr(index) == "SearchIndex(name='docs', prefixes=['doc'], storage_type='json')" + ) + + +def test_async_search_index_repr(): + schema = _make_schema(name="async-idx", prefix="data", storage_type="json") + index = AsyncSearchIndex(schema=schema) + assert ( + repr(index) + == "AsyncSearchIndex(name='async-idx', prefixes=['data'], storage_type='json')" + ) + + +# --------------------------------------------------------------------------- +# SemanticCache +# --------------------------------------------------------------------------- + + +@pytest.fixture() +def patched_semantic_cache(): + """Patch out Redis and HFTextVectorizer so SemanticCache can be instantiated + without a running Redis server or ML model download.""" + mock_vec = _make_mock_vectorizer() + mock_idx = MagicMock() + mock_idx.exists.return_value = False + + with patch( + "redisvl.extensions.cache.llm.semantic.HFTextVectorizer", + return_value=mock_vec, + ): + with patch( + "redisvl.extensions.cache.llm.semantic.SearchIndex", + return_value=mock_idx, + ): + yield + + +def test_semantic_cache_repr_defaults(patched_semantic_cache): + from redisvl.extensions.cache.llm.semantic import SemanticCache + + cache = SemanticCache(name="llmcache", distance_threshold=0.1) + assert ( + repr(cache) + == "SemanticCache(name='llmcache', distance_threshold=0.1, ttl=None)" + ) + + +def test_semantic_cache_repr_with_ttl(patched_semantic_cache): + from redisvl.extensions.cache.llm.semantic import SemanticCache + + cache = SemanticCache(name="my-cache", distance_threshold=0.2, ttl=300) + assert ( + repr(cache) == "SemanticCache(name='my-cache', distance_threshold=0.2, ttl=300)" + ) + + +# --------------------------------------------------------------------------- +# SemanticRouter +# --------------------------------------------------------------------------- + + +def test_semantic_router_repr(): + from redisvl.extensions.router.schema import Route + from redisvl.extensions.router.semantic import SemanticRouter + + routes = [ + Route(name="greeting", references=["hello", "hi"]), + Route(name="farewell", references=["bye", "goodbye"]), + ] + # model_construct bypasses __init__ (and therefore Redis/vectorizer setup) + # while still setting the Pydantic fields that __repr__ reads. + router = SemanticRouter.model_construct(name="my-router", routes=routes) + assert repr(router) == "SemanticRouter(name='my-router', routes=2)" + + +def test_semantic_router_repr_single_route(): + from redisvl.extensions.router.schema import Route + from redisvl.extensions.router.semantic import SemanticRouter + + routes = [Route(name="support", references=["help", "issue"])] + router = SemanticRouter.model_construct(name="support-router", routes=routes) + assert repr(router) == "SemanticRouter(name='support-router', routes=1)" + + +# --------------------------------------------------------------------------- +# MessageHistory +# --------------------------------------------------------------------------- + + +@pytest.fixture() +def patched_message_history(): + """Patch SearchIndex.create so MessageHistory init doesn't need Redis.""" + with patch("redisvl.extensions.message_history.message_history.SearchIndex.create"): + yield + + +def test_message_history_repr(patched_message_history): + from redisvl.extensions.message_history.message_history import MessageHistory + + mh = MessageHistory(name="chat", session_tag="abc123") + assert repr(mh) == "MessageHistory(name='chat', session_tag='abc123')" + + +def test_message_history_repr_custom_name(patched_message_history): + from redisvl.extensions.message_history.message_history import MessageHistory + + mh = MessageHistory(name="support-chat", session_tag="sess-001") + assert repr(mh) == "MessageHistory(name='support-chat', session_tag='sess-001')" + + +# --------------------------------------------------------------------------- +# SemanticMessageHistory +# --------------------------------------------------------------------------- + + +@pytest.fixture() +def patched_semantic_message_history(): + """Patch out Redis and HFTextVectorizer for SemanticMessageHistory.""" + mock_vec = _make_mock_vectorizer(dims=384) + mock_idx = MagicMock() + mock_idx.exists.return_value = False + + with patch( + "redisvl.extensions.message_history.semantic_history.HFTextVectorizer", + return_value=mock_vec, + ): + with patch( + "redisvl.extensions.message_history.semantic_history.SearchIndex", + return_value=mock_idx, + ): + yield + + +def test_semantic_message_history_repr(patched_semantic_message_history): + from redisvl.extensions.message_history.semantic_history import ( + SemanticMessageHistory, + ) + + smh = SemanticMessageHistory( + name="sem-chat", session_tag="sess-42", distance_threshold=0.3 + ) + assert ( + repr(smh) + == "SemanticMessageHistory(name='sem-chat', session_tag='sess-42', distance_threshold=0.3)" + ) + + +def test_semantic_message_history_repr_custom_threshold( + patched_semantic_message_history, +): + from redisvl.extensions.message_history.semantic_history import ( + SemanticMessageHistory, + ) + + smh = SemanticMessageHistory( + name="history", session_tag="s1", distance_threshold=0.5 + ) + assert ( + repr(smh) + == "SemanticMessageHistory(name='history', session_tag='s1', distance_threshold=0.5)" + )