Skip to content

Commit a1b6a04

Browse files
authored
Decode LangCache attribute values on retrieval (#437)
In #429, we found and fixed an issue with LangCache attribute values, which is that some characters are not allowed (commas) and some are accepted but silently fail during retrieval (slashes). Our solution was to encode these characters when we persist them and at query time. This PR decodes the values in retrieved data as well.
1 parent 2dfbf45 commit a1b6a04

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)