Skip to content

Commit 4ac6e8a

Browse files
committed
Decode LangCache attribute values on retrieval
1 parent 99b9d3a commit 4ac6e8a

File tree

3 files changed

+73
-6
lines changed

3 files changed

+73
-6
lines changed

redisvl/extensions/cache/llm/langcache.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@
2323
)
2424

2525

26+
_LANGCACHE_ATTR_DECODE_TRANS = str.maketrans(
27+
{v: k for k, v in _LANGCACHE_ATTR_ENCODE_TRANS.items()}
28+
)
29+
30+
2631
def _encode_attribute_value_for_langcache(value: str) -> str:
2732
"""Encode a string attribute value for use with the LangCache service.
2833
@@ -62,6 +67,40 @@ def _encode_attributes_for_langcache(attributes: Dict[str, Any]) -> Dict[str, An
6267
return safe_attributes if changed else attributes
6368

6469

70+
def _decode_attribute_value_from_langcache(value: str) -> str:
71+
"""Decode a string attribute value returned from the LangCache service.
72+
73+
This reverses :func:`_encode_attribute_value_for_langcache`, translating the
74+
fullwidth comma and division slash characters back to their ASCII
75+
counterparts so callers see the original values they stored.
76+
"""
77+
78+
return value.translate(_LANGCACHE_ATTR_DECODE_TRANS)
79+
80+
81+
def _decode_attributes_from_langcache(attributes: Dict[str, Any]) -> Dict[str, Any]:
82+
"""Return a copy of *attributes* with string values safely decoded.
83+
84+
This is the inverse of :func:`_encode_attributes_for_langcache`. Only
85+
top-level string values are decoded; non-string values are left unchanged.
86+
If no values require decoding, the original dict is returned unchanged.
87+
"""
88+
89+
if not attributes:
90+
return attributes
91+
92+
changed = False
93+
decoded_attributes: Dict[str, Any] = dict(attributes)
94+
for key, value in attributes.items():
95+
if isinstance(value, str):
96+
decoded = _decode_attribute_value_from_langcache(value)
97+
if decoded != value:
98+
decoded_attributes[key] = decoded
99+
changed = True
100+
101+
return decoded_attributes if changed else attributes
102+
103+
65104
class LangCacheSemanticCache(BaseLLMCache):
66105
"""LLM Cache implementation using the LangCache managed service.
67106
@@ -239,7 +278,11 @@ def _convert_to_cache_hit(self, result: Dict[str, Any]) -> CacheHit:
239278
CacheHit: The converted cache hit.
240279
"""
241280
# Extract attributes (metadata) from the result
242-
attributes = result.get("attributes", {})
281+
attributes = result.get("attributes", {}) or {}
282+
if attributes:
283+
# Decode attribute values that were encoded for LangCache so callers
284+
# see the original metadata values they stored.
285+
attributes = _decode_attributes_from_langcache(attributes)
243286

244287
# LangCache returns similarity in [0,1] (higher is better)
245288
similarity = result.get("similarity", 0.0)

tests/integration/test_langcache_semantic_cache_integration.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,12 @@ def test_attribute_value_with_comma_and_slash_is_encoded_for_llm_string(
261261
num_results=3,
262262
)
263263
assert hits
264+
# Response must match, and metadata should contain the original value
265+
# (the client handles encoding/decoding around the LangCache API).
264266
assert any(hit["response"] == response for hit in hits)
267+
assert any(
268+
hit.get("metadata", {}).get("llm_string") == raw_llm_string for hit in hits
269+
)
265270

266271

267272
@pytest.mark.requires_api_keys

tests/unit/test_langcache_semantic_cache.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,9 @@ def test_check(self, mock_langcache_client):
175175
abs(results[0]["vector_distance"] - 0.05) < 0.001
176176
) # 1.0 - 0.95 similarity
177177

178+
# Attributes should round-trip decoded in metadata
179+
assert results[0]["metadata"] == {"topic": "programming"}
180+
178181
mock_client.search.assert_called_once()
179182

180183
@pytest.mark.asyncio
@@ -261,7 +264,12 @@ def test_check_with_attributes(self, mock_langcache_client):
261264
"similarity": 0.95,
262265
"created_at": 1234567890.0,
263266
"updated_at": 1234567890.0,
264-
"attributes": {"language": "python", "topic": "programming"},
267+
# Attributes come back from LangCache already encoded; the client
268+
# should decode them before exposing them to callers.
269+
"attributes": {
270+
"language": "python",
271+
"topic": "programming,with∕encoding",
272+
},
265273
}
266274

267275
mock_response = MagicMock()
@@ -275,21 +283,32 @@ def test_check_with_attributes(self, mock_langcache_client):
275283
api_key="test-key",
276284
)
277285

278-
# Search with attributes filter
286+
# Search with attributes filter – we pass raw, unencoded values and
287+
# expect to see those same values in the returned metadata.
279288
results = cache.check(
280289
prompt="What is Python?",
281-
attributes={"language": "python", "topic": "programming"},
290+
attributes={
291+
"language": "python",
292+
"topic": "programming,with/encoding",
293+
},
282294
)
283295

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

287-
# Verify attributes were passed to search
299+
# Verify attributes were passed to search (encoded by the client)
288300
mock_client.search.assert_called_once()
289301
call_kwargs = mock_client.search.call_args.kwargs
290302
assert call_kwargs["attributes"] == {
291303
"language": "python",
292-
"topic": "programming",
304+
# The comma and slash should be encoded for LangCache.
305+
"topic": "programming,with∕encoding",
306+
}
307+
308+
# And the decoded, original values should appear in metadata
309+
assert results[0]["metadata"] == {
310+
"language": "python",
311+
"topic": "programming,with/encoding",
293312
}
294313

295314
def test_store_with_empty_metadata_does_not_send_attributes(

0 commit comments

Comments
 (0)