1515logger = get_logger (__name__ )
1616
1717
18+ _LANGCACHE_ATTR_ENCODE_TRANS = str .maketrans (
19+ {
20+ "," : "," , # U+FF0C FULLWIDTH COMMA
21+ "/" : "∕" , # U+2215 DIVISION SLASH
22+ }
23+ )
24+
25+
26+ def _encode_attribute_value_for_langcache (value : str ) -> str :
27+ """Encode a string attribute value for use with the LangCache service.
28+
29+ LangCache applies validation and matching rules to attribute values. In
30+ particular, the managed service can reject values containing commas (",")
31+ and may not reliably match filters on values containing slashes ("/").
32+
33+ To keep attribute values round-trippable *and* usable for attribute
34+ filtering, we replace these characters with visually similar Unicode
35+ variants that the service accepts. A precomputed ``str.translate`` table is
36+ used so values are scanned only once.
37+ """
38+
39+ return value .translate (_LANGCACHE_ATTR_ENCODE_TRANS )
40+
41+
42+ def _encode_attributes_for_langcache (attributes : Dict [str , Any ]) -> Dict [str , Any ]:
43+ """Return a copy of *attributes* with string values safely encoded.
44+
45+ Only top-level string values are encoded; non-string values are left
46+ unchanged. If no values require encoding, the original dict is returned
47+ unchanged.
48+ """
49+
50+ if not attributes :
51+ return attributes
52+
53+ changed = False
54+ safe_attributes : Dict [str , Any ] = dict (attributes )
55+ for key , value in attributes .items ():
56+ if isinstance (value , str ):
57+ encoded = _encode_attribute_value_for_langcache (value )
58+ if encoded != value :
59+ safe_attributes [key ] = encoded
60+ changed = True
61+
62+ return safe_attributes if changed else attributes
63+
64+
1865class LangCacheSemanticCache (BaseLLMCache ):
1966 """LLM Cache implementation using the LangCache managed service.
2067
@@ -163,7 +210,9 @@ def _build_search_kwargs(
163210 "similarity_threshold" : similarity_threshold ,
164211 }
165212 if attributes :
166- kwargs ["attributes" ] = attributes
213+ # Encode all string attribute values so they are accepted by the
214+ # LangCache service and remain filterable.
215+ kwargs ["attributes" ] = _encode_attributes_for_langcache (attributes )
167216 return kwargs
168217
169218 def _hits_from_response (
@@ -403,8 +452,9 @@ def store(
403452 # Store using the LangCache client; only send attributes if provided (non-empty)
404453 try :
405454 if metadata :
455+ safe_metadata = _encode_attributes_for_langcache (metadata )
406456 result = self ._client .set (
407- prompt = prompt , response = response , attributes = metadata
457+ prompt = prompt , response = response , attributes = safe_metadata
408458 )
409459 else :
410460 result = self ._client .set (prompt = prompt , response = response )
@@ -471,8 +521,9 @@ async def astore(
471521 # Store using the LangCache client (async); only send attributes if provided (non-empty)
472522 try :
473523 if metadata :
524+ safe_metadata = _encode_attributes_for_langcache (metadata )
474525 result = await self ._client .set_async (
475- prompt = prompt , response = response , attributes = metadata
526+ prompt = prompt , response = response , attributes = safe_metadata
476527 )
477528 else :
478529 result = await self ._client .set_async (prompt = prompt , response = response )
@@ -594,7 +645,8 @@ def delete_by_attributes(self, attributes: Dict[str, Any]) -> Dict[str, Any]:
594645 raise ValueError (
595646 "Cannot delete by attributes with an empty attributes dictionary."
596647 )
597- result = self ._client .delete_query (attributes = attributes )
648+ safe_attributes = _encode_attributes_for_langcache (attributes )
649+ result = self ._client .delete_query (attributes = safe_attributes )
598650 # Convert DeleteQueryResponse to dict
599651 return result .model_dump () if hasattr (result , "model_dump" ) else {}
600652
@@ -615,6 +667,7 @@ async def adelete_by_attributes(self, attributes: Dict[str, Any]) -> Dict[str, A
615667 raise ValueError (
616668 "Cannot delete by attributes with an empty attributes dictionary."
617669 )
618- result = await self ._client .delete_query_async (attributes = attributes )
670+ safe_attributes = _encode_attributes_for_langcache (attributes )
671+ result = await self ._client .delete_query_async (attributes = safe_attributes )
619672 # Convert DeleteQueryResponse to dict
620673 return result .model_dump () if hasattr (result , "model_dump" ) else {}
0 commit comments