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
45 changes: 44 additions & 1 deletion redisvl/extensions/cache/llm/langcache.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@
)


_LANGCACHE_ATTR_DECODE_TRANS = str.maketrans(
{v: k for k, v in _LANGCACHE_ATTR_ENCODE_TRANS.items()}
)


def _encode_attribute_value_for_langcache(value: str) -> str:
"""Encode a string attribute value for use with the LangCache service.

Expand Down Expand Up @@ -62,6 +67,40 @@ def _encode_attributes_for_langcache(attributes: Dict[str, Any]) -> Dict[str, An
return safe_attributes if changed else attributes


def _decode_attribute_value_from_langcache(value: str) -> str:
"""Decode a string attribute value returned from the LangCache service.

This reverses :func:`_encode_attribute_value_for_langcache`, translating the
fullwidth comma and division slash characters back to their ASCII
counterparts so callers see the original values they stored.
"""

return value.translate(_LANGCACHE_ATTR_DECODE_TRANS)


def _decode_attributes_from_langcache(attributes: Dict[str, Any]) -> Dict[str, Any]:
"""Return a copy of *attributes* with string values safely decoded.

This is the inverse of :func:`_encode_attributes_for_langcache`. Only
top-level string values are decoded; non-string values are left unchanged.
If no values require decoding, the original dict is returned unchanged.
"""

if not attributes:
return attributes

changed = False
decoded_attributes: Dict[str, Any] = dict(attributes)
for key, value in attributes.items():
if isinstance(value, str):
decoded = _decode_attribute_value_from_langcache(value)
if decoded != value:
decoded_attributes[key] = decoded
changed = True

return decoded_attributes if changed else attributes


class LangCacheSemanticCache(BaseLLMCache):
"""LLM Cache implementation using the LangCache managed service.

Expand Down Expand Up @@ -239,7 +278,11 @@ def _convert_to_cache_hit(self, result: Dict[str, Any]) -> CacheHit:
CacheHit: The converted cache hit.
"""
# Extract attributes (metadata) from the result
attributes = result.get("attributes", {})
attributes = result.get("attributes", {}) or {}
if attributes:
# Decode attribute values that were encoded for LangCache so callers
# see the original metadata values they stored.
attributes = _decode_attributes_from_langcache(attributes)

# LangCache returns similarity in [0,1] (higher is better)
similarity = result.get("similarity", 0.0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,12 @@ def test_attribute_value_with_comma_and_slash_is_encoded_for_llm_string(
num_results=3,
)
assert hits
# Response must match, and metadata should contain the original value
# (the client handles encoding/decoding around the LangCache API).
assert any(hit["response"] == response for hit in hits)
assert any(
hit.get("metadata", {}).get("llm_string") == raw_llm_string for hit in hits
)


@pytest.mark.requires_api_keys
Expand Down
29 changes: 24 additions & 5 deletions tests/unit/test_langcache_semantic_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,9 @@ def test_check(self, mock_langcache_client):
abs(results[0]["vector_distance"] - 0.05) < 0.001
) # 1.0 - 0.95 similarity

# Attributes should round-trip decoded in metadata
assert results[0]["metadata"] == {"topic": "programming"}

mock_client.search.assert_called_once()

@pytest.mark.asyncio
Expand Down Expand Up @@ -261,7 +264,12 @@ def test_check_with_attributes(self, mock_langcache_client):
"similarity": 0.95,
"created_at": 1234567890.0,
"updated_at": 1234567890.0,
"attributes": {"language": "python", "topic": "programming"},
# Attributes come back from LangCache already encoded; the client
# should decode them before exposing them to callers.
"attributes": {
"language": "python",
"topic": "programming,with∕encoding",
},
}

mock_response = MagicMock()
Expand All @@ -275,21 +283,32 @@ def test_check_with_attributes(self, mock_langcache_client):
api_key="test-key",
)

# Search with attributes filter
# Search with attributes filter – we pass raw, unencoded values and
# expect to see those same values in the returned metadata.
results = cache.check(
prompt="What is Python?",
attributes={"language": "python", "topic": "programming"},
attributes={
"language": "python",
"topic": "programming,with/encoding",
},
)

assert len(results) == 1
assert results[0]["entry_id"] == "entry-123"

# Verify attributes were passed to search
# Verify attributes were passed to search (encoded by the client)
mock_client.search.assert_called_once()
call_kwargs = mock_client.search.call_args.kwargs
assert call_kwargs["attributes"] == {
"language": "python",
"topic": "programming",
# The comma and slash should be encoded for LangCache.
"topic": "programming,with∕encoding",
}

# And the decoded, original values should appear in metadata
assert results[0]["metadata"] == {
"language": "python",
"topic": "programming,with/encoding",
}

def test_store_with_empty_metadata_does_not_send_attributes(
Expand Down
Loading