Skip to content

Commit 62dc045

Browse files
authored
Fix LangCache clear() 400 error by using flush() API (#428)
Fix LangCache clear() 400 error by using flush() API ## Summary The LangCache API returns `400 Bad Request: "attributes: cannot be blank"` when `clear()` attempts to delete all entries by passing `attributes={}` to `delete_query()`. The fix uses the LangCache SDK's dedicated `flush()` method to properly delete all entries. ## Changes ### Core Fix - **`delete()` and `adelete()`** - Now call `self._client.flush()` and `self._client.flush_async()` respectively to delete all cache entries using the proper API endpoint. ### Additional Improvements - **`delete_by_attributes()` and `adelete_by_attributes()`** - Now raise `ValueError` if called with an empty attributes dictionary. This prevents misuse and provides a clear error message: "Cannot delete by attributes with an empty attributes dictionary." Users should use `delete()` or `clear()` to delete all entries, or provide specific attributes to filter deletions. - Fixes #427
1 parent c61c8c7 commit 62dc045

File tree

2 files changed

+138
-11
lines changed

2 files changed

+138
-11
lines changed

redisvl/extensions/cache/llm/langcache.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -536,18 +536,16 @@ async def aupdate(self, key: str, **kwargs) -> None:
536536
def delete(self) -> None:
537537
"""Delete the entire cache.
538538
539-
This deletes all entries in the cache by calling delete_query
540-
with no attributes.
539+
This deletes all entries in the cache by calling the flush API.
541540
"""
542-
self._client.delete_query(attributes={})
541+
self._client.flush()
543542

544543
async def adelete(self) -> None:
545544
"""Async delete the entire cache.
546545
547-
This deletes all entries in the cache by calling delete_query
548-
with no attributes.
546+
This deletes all entries in the cache by calling the flush API.
549547
"""
550-
await self._client.delete_query_async(attributes={})
548+
await self._client.flush_async()
551549

552550
def clear(self) -> None:
553551
"""Clear the cache of all entries.
@@ -584,10 +582,18 @@ def delete_by_attributes(self, attributes: Dict[str, Any]) -> Dict[str, Any]:
584582
585583
Args:
586584
attributes (Dict[str, Any]): Attributes to match for deletion.
585+
Cannot be empty.
587586
588587
Returns:
589588
Dict[str, Any]: Result of the deletion operation.
589+
590+
Raises:
591+
ValueError: If attributes is an empty dictionary.
590592
"""
593+
if not attributes:
594+
raise ValueError(
595+
"Cannot delete by attributes with an empty attributes dictionary."
596+
)
591597
result = self._client.delete_query(attributes=attributes)
592598
# Convert DeleteQueryResponse to dict
593599
return result.model_dump() if hasattr(result, "model_dump") else {}
@@ -597,10 +603,18 @@ async def adelete_by_attributes(self, attributes: Dict[str, Any]) -> Dict[str, A
597603
598604
Args:
599605
attributes (Dict[str, Any]): Attributes to match for deletion.
606+
Cannot be empty.
600607
601608
Returns:
602609
Dict[str, Any]: Result of the deletion operation.
610+
611+
Raises:
612+
ValueError: If attributes is an empty dictionary.
603613
"""
614+
if not attributes:
615+
raise ValueError(
616+
"Cannot delete by attributes with an empty attributes dictionary."
617+
)
604618
result = await self._client.delete_query_async(attributes=attributes)
605619
# Convert DeleteQueryResponse to dict
606620
return result.model_dump() if hasattr(result, "model_dump") else {}

tests/unit/test_langcache_semantic_cache.py

Lines changed: 118 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,7 @@ def test_check_with_empty_attributes_does_not_send_attributes(
355355
assert "attributes" not in call_kwargs
356356

357357
def test_delete(self, mock_langcache_client):
358-
"""Test deleting the entire cache."""
358+
"""Test deleting the entire cache using flush()."""
359359
_, mock_client = mock_langcache_client
360360

361361
cache = LangCacheSemanticCache(
@@ -367,14 +367,14 @@ def test_delete(self, mock_langcache_client):
367367

368368
cache.delete()
369369

370-
mock_client.delete_query.assert_called_once_with(attributes={})
370+
mock_client.flush.assert_called_once()
371371

372372
@pytest.mark.asyncio
373373
async def test_adelete(self, mock_langcache_client):
374-
"""Test async deleting the entire cache."""
374+
"""Test async deleting the entire cache using flush()."""
375375
_, mock_client = mock_langcache_client
376376

377-
mock_client.delete_query_async = AsyncMock()
377+
mock_client.flush_async = AsyncMock()
378378

379379
cache = LangCacheSemanticCache(
380380
name="test",
@@ -385,7 +385,40 @@ async def test_adelete(self, mock_langcache_client):
385385

386386
await cache.adelete()
387387

388-
mock_client.delete_query_async.assert_called_once_with(attributes={})
388+
mock_client.flush_async.assert_called_once()
389+
390+
def test_clear(self, mock_langcache_client):
391+
"""Test that clear() calls delete() which uses flush()."""
392+
_, mock_client = mock_langcache_client
393+
394+
cache = LangCacheSemanticCache(
395+
name="test",
396+
server_url="https://api.example.com",
397+
cache_id="test-cache",
398+
api_key="test-key",
399+
)
400+
401+
cache.clear()
402+
403+
mock_client.flush.assert_called_once()
404+
405+
@pytest.mark.asyncio
406+
async def test_aclear(self, mock_langcache_client):
407+
"""Test that async clear() calls adelete() which uses flush()."""
408+
_, mock_client = mock_langcache_client
409+
410+
mock_client.flush_async = AsyncMock()
411+
412+
cache = LangCacheSemanticCache(
413+
name="test",
414+
server_url="https://api.example.com",
415+
cache_id="test-cache",
416+
api_key="test-key",
417+
)
418+
419+
await cache.aclear()
420+
421+
mock_client.flush_async.assert_called_once()
389422

390423
def test_delete_by_id(self, mock_langcache_client):
391424
"""Test deleting a single entry by ID."""
@@ -402,6 +435,86 @@ def test_delete_by_id(self, mock_langcache_client):
402435

403436
mock_client.delete_by_id.assert_called_once_with(entry_id="entry-123")
404437

438+
def test_delete_by_attributes_with_valid_attributes(self, mock_langcache_client):
439+
"""Test deleting entries by attributes with valid attributes."""
440+
_, mock_client = mock_langcache_client
441+
442+
mock_response = MagicMock()
443+
mock_response.model_dump.return_value = {"deleted_entries_count": 5}
444+
mock_client.delete_query.return_value = mock_response
445+
446+
cache = LangCacheSemanticCache(
447+
name="test",
448+
server_url="https://api.example.com",
449+
cache_id="test-cache",
450+
api_key="test-key",
451+
)
452+
453+
result = cache.delete_by_attributes({"topic": "python"})
454+
455+
assert result == {"deleted_entries_count": 5}
456+
mock_client.delete_query.assert_called_once_with(attributes={"topic": "python"})
457+
458+
def test_delete_by_attributes_with_empty_attributes_raises_error(
459+
self, mock_langcache_client
460+
):
461+
"""Test that delete_by_attributes raises ValueError with empty attributes."""
462+
cache = LangCacheSemanticCache(
463+
name="test",
464+
server_url="https://api.example.com",
465+
cache_id="test-cache",
466+
api_key="test-key",
467+
)
468+
469+
with pytest.raises(
470+
ValueError,
471+
match="Cannot delete by attributes with an empty attributes dictionary",
472+
):
473+
cache.delete_by_attributes({})
474+
475+
@pytest.mark.asyncio
476+
async def test_adelete_by_attributes_with_valid_attributes(
477+
self, mock_langcache_client
478+
):
479+
"""Test async deleting entries by attributes with valid attributes."""
480+
_, mock_client = mock_langcache_client
481+
482+
mock_response = MagicMock()
483+
mock_response.model_dump.return_value = {"deleted_entries_count": 3}
484+
mock_client.delete_query_async = AsyncMock(return_value=mock_response)
485+
486+
cache = LangCacheSemanticCache(
487+
name="test",
488+
server_url="https://api.example.com",
489+
cache_id="test-cache",
490+
api_key="test-key",
491+
)
492+
493+
result = await cache.adelete_by_attributes({"language": "python"})
494+
495+
assert result == {"deleted_entries_count": 3}
496+
mock_client.delete_query_async.assert_called_once_with(
497+
attributes={"language": "python"}
498+
)
499+
500+
@pytest.mark.asyncio
501+
async def test_adelete_by_attributes_with_empty_attributes_raises_error(
502+
self, mock_langcache_client
503+
):
504+
"""Test that async delete_by_attributes raises ValueError with empty attributes."""
505+
cache = LangCacheSemanticCache(
506+
name="test",
507+
server_url="https://api.example.com",
508+
cache_id="test-cache",
509+
api_key="test-key",
510+
)
511+
512+
with pytest.raises(
513+
ValueError,
514+
match="Cannot delete by attributes with an empty attributes dictionary",
515+
):
516+
await cache.adelete_by_attributes({})
517+
405518
def test_update_not_supported(self, mock_langcache_client):
406519
"""Test that update raises NotImplementedError."""
407520
cache = LangCacheSemanticCache(

0 commit comments

Comments
 (0)