Skip to content

Commit c7c6c6d

Browse files
committed
LangCache: avoid sending empty attributes/metadata; add tests; ignore .ai
1 parent c9357c3 commit c7c6c6d

File tree

2 files changed

+154
-29
lines changed

2 files changed

+154
-29
lines changed

redisvl/extensions/cache/llm/langcache.py

Lines changed: 92 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class LangCacheSemanticCache(BaseLLMCache):
4747
def __init__(
4848
self,
4949
name: str = "langcache",
50-
server_url: str = "https://api.langcache.com",
50+
server_url: str = "https://aws-us-east-1.langcache.redis.io",
5151
cache_id: str = "",
5252
api_key: str = "",
5353
ttl: Optional[int] = None,
@@ -56,7 +56,7 @@ def __init__(
5656
distance_scale: Literal["normalized", "redis"] = "normalized",
5757
**kwargs,
5858
):
59-
"""Initialize a LangCache wrapper.
59+
"""Initialize a LangCache semantic cache.
6060
6161
Args:
6262
name (str): The name of the cache. Defaults to "langcache".
@@ -108,20 +108,20 @@ def _create_client(self):
108108
Initialize the LangCache client.
109109
110110
Returns:
111-
LangCacheClient: The LangCache client.
111+
LangCache: The LangCache client.
112112
113113
Raises:
114114
ImportError: If the langcache package is not installed.
115115
"""
116116
try:
117-
from langcache import LangCacheClient
117+
from langcache import LangCache
118118
except ImportError as e:
119119
raise ImportError(
120120
"The langcache package is required to use LangCacheSemanticCache. "
121121
"Install it with: pip install langcache"
122122
) from e
123123

124-
return LangCacheClient(
124+
return LangCache(
125125
server_url=self._server_url,
126126
cache_id=self._cache_id,
127127
api_key=self._api_key,
@@ -155,6 +155,8 @@ def _build_search_kwargs(
155155
SearchStrategy.EXACT if "exact" in self._search_strategies else None,
156156
SearchStrategy.SEMANTIC if "semantic" in self._search_strategies else None,
157157
]
158+
# Filter out Nones to avoid sending invalid enum values
159+
search_strategies = [s for s in search_strategies if s is not None]
158160
kwargs: Dict[str, Any] = {
159161
"prompt": prompt,
160162
"search_strategies": search_strategies,
@@ -253,15 +255,31 @@ def check(
253255
if distance_threshold is not None:
254256
similarity_threshold = self._similarity_threshold(distance_threshold)
255257

256-
# Search using the LangCache client
257-
# The client itself is the context manager
258+
# Build kwargs
258259
search_kwargs = self._build_search_kwargs(
259260
prompt=prompt,
260261
similarity_threshold=similarity_threshold,
261262
attributes=attributes,
262263
)
263264

264-
response = self._client.search(**search_kwargs)
265+
try:
266+
response = self._client.search(**search_kwargs)
267+
except Exception as e:
268+
try:
269+
from langcache.errors import BadRequestErrorResponseContent
270+
except Exception:
271+
raise
272+
if (
273+
isinstance(e, BadRequestErrorResponseContent)
274+
and "no attributes are configured" in str(e).lower()
275+
and attributes
276+
):
277+
raise RuntimeError(
278+
"LangCache reported attributes are not configured for this cache, "
279+
"but attributes were provided to check(). Remove attributes or configure them on the cache."
280+
) from e
281+
else:
282+
raise
265283

266284
# Convert results to cache hits
267285
return self._hits_from_response(response, num_results)
@@ -321,7 +339,24 @@ async def acheck(
321339

322340
# Add attributes if provided (already handled by builder)
323341

324-
response = await self._client.search_async(**search_kwargs)
342+
try:
343+
response = await self._client.search_async(**search_kwargs)
344+
except Exception as e:
345+
try:
346+
from langcache.errors import BadRequestErrorResponseContent
347+
except Exception:
348+
raise
349+
if (
350+
isinstance(e, BadRequestErrorResponseContent)
351+
and "no attributes are configured" in str(e).lower()
352+
and attributes
353+
):
354+
raise RuntimeError(
355+
"LangCache reported attributes are not configured for this cache, "
356+
"but attributes were provided to acheck(). Remove attributes or configure them on the cache."
357+
) from e
358+
else:
359+
raise
325360

326361
# Convert results to cache hits
327362
return self._hits_from_response(response, num_results)
@@ -365,16 +400,30 @@ def store(
365400
if ttl is not None:
366401
logger.warning("LangCache does not support per-entry TTL")
367402

368-
# Store using the LangCache client
369-
# The client itself is the context manager
370-
# Only pass attributes if metadata is provided
371-
# Some caches may not have attributes configured
372-
if metadata:
373-
result = self._client.set(
374-
prompt=prompt, response=response, attributes=metadata
375-
)
376-
else:
377-
result = self._client.set(prompt=prompt, response=response)
403+
# Store using the LangCache client; only send attributes if provided (non-empty)
404+
try:
405+
if metadata:
406+
result = self._client.set(
407+
prompt=prompt, response=response, attributes=metadata
408+
)
409+
else:
410+
result = self._client.set(prompt=prompt, response=response)
411+
except Exception as e: # narrow for known SDK error when possible
412+
try:
413+
from langcache.errors import BadRequestErrorResponseContent
414+
except Exception:
415+
raise
416+
if (
417+
isinstance(e, BadRequestErrorResponseContent)
418+
and "no attributes are configured" in str(e).lower()
419+
and metadata
420+
):
421+
raise RuntimeError(
422+
"LangCache reported attributes are not configured for this cache, "
423+
"but metadata was provided to store(). Remove metadata or configure attributes on the cache."
424+
) from e
425+
else:
426+
raise
378427

379428
# Return the entry ID
380429
# Result is a SetResponse Pydantic model with entry_id attribute
@@ -419,16 +468,30 @@ async def astore(
419468
if ttl is not None:
420469
logger.warning("LangCache does not support per-entry TTL")
421470

422-
# Store using the LangCache client (async)
423-
# The client itself is the context manager
424-
# Only pass attributes if metadata is provided
425-
# Some caches may not have attributes configured
426-
if metadata:
427-
result = await self._client.set_async(
428-
prompt=prompt, response=response, attributes=metadata
429-
)
430-
else:
431-
result = await self._client.set_async(prompt=prompt, response=response)
471+
# Store using the LangCache client (async); only send attributes if provided (non-empty)
472+
try:
473+
if metadata:
474+
result = await self._client.set_async(
475+
prompt=prompt, response=response, attributes=metadata
476+
)
477+
else:
478+
result = await self._client.set_async(prompt=prompt, response=response)
479+
except Exception as e:
480+
try:
481+
from langcache.errors import BadRequestErrorResponseContent
482+
except Exception:
483+
raise
484+
if (
485+
isinstance(e, BadRequestErrorResponseContent)
486+
and "no attributes are configured" in str(e).lower()
487+
and metadata
488+
):
489+
raise RuntimeError(
490+
"LangCache reported attributes are not configured for this cache, "
491+
"but metadata was provided to astore(). Remove metadata or configure attributes on the cache."
492+
) from e
493+
else:
494+
raise
432495

433496
# Return the entry ID
434497
# Result is a SetResponse Pydantic model with entry_id attribute

tests/unit/test_langcache_semantic_cache.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,68 @@ def test_check_with_attributes(self, mock_langcache_client):
292292
"topic": "programming",
293293
}
294294

295+
def test_store_with_empty_metadata_does_not_send_attributes(
296+
self, mock_langcache_client
297+
):
298+
"""Empty metadata {} should not be forwarded as attributes to the SDK."""
299+
_, mock_client = mock_langcache_client
300+
301+
mock_response = MagicMock()
302+
mock_response.entry_id = "entry-empty"
303+
mock_client.set.return_value = mock_response
304+
305+
cache = LangCacheSemanticCache(
306+
name="test",
307+
server_url="https://api.example.com",
308+
cache_id="test-cache",
309+
api_key="test-key",
310+
)
311+
312+
entry_id = cache.store(
313+
prompt="Q?",
314+
response="A",
315+
metadata={}, # should be ignored
316+
)
317+
318+
assert entry_id == "entry-empty"
319+
# Ensure attributes kwarg was NOT sent when metadata is {}
320+
_, call_kwargs = mock_client.set.call_args
321+
assert "attributes" not in call_kwargs
322+
323+
def test_check_with_empty_attributes_does_not_send_attributes(
324+
self, mock_langcache_client
325+
):
326+
"""Empty attributes {} should not be forwarded to the SDK search call."""
327+
_, mock_client = mock_langcache_client
328+
329+
mock_entry = MagicMock()
330+
mock_entry.model_dump.return_value = {
331+
"id": "e1",
332+
"prompt": "Q?",
333+
"response": "A",
334+
"similarity": 1.0,
335+
"created_at": 0.0,
336+
"updated_at": 0.0,
337+
"attributes": {},
338+
}
339+
mock_response = MagicMock()
340+
mock_response.data = [mock_entry]
341+
mock_client.search.return_value = mock_response
342+
343+
cache = LangCacheSemanticCache(
344+
name="test",
345+
server_url="https://api.example.com",
346+
cache_id="test-cache",
347+
api_key="test-key",
348+
)
349+
350+
results = cache.check(prompt="Q?", attributes={}) # should be ignored
351+
assert results and results[0]["entry_id"] == "e1"
352+
353+
# Ensure attributes kwarg was NOT sent when attributes is {}
354+
_, call_kwargs = mock_client.search.call_args
355+
assert "attributes" not in call_kwargs
356+
295357
def test_delete(self, mock_langcache_client):
296358
"""Test deleting the entire cache."""
297359
_, mock_client = mock_langcache_client

0 commit comments

Comments
 (0)