-
Notifications
You must be signed in to change notification settings - Fork 62
Add LangCache integration tests #429
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
72a6cbc
Add LangCache integration tests
abrookins 5c9abcd
Apply suggestions from code review
abrookins 47a4473
Move import to top of module
abrookins 9bdcb46
Resolving review feedback
abrookins cde0f4a
Encode LangCache attributes for safe filtering
abrookins fc0b285
Fix LangCache integration test attributes
abrookins 11fe095
Document pytest --run-api-tests usage
abrookins 6ada90a
remove unused symbol
abrookins File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
303 changes: 303 additions & 0 deletions
303
tests/integration/test_langcache_semantic_cache_integration.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,303 @@ | ||
| """Integration tests for LangCacheSemanticCache against the LangCache managed service. | ||
|
|
||
| These tests exercise the real LangCache API using two configured caches: | ||
| - One with attributes configured | ||
| - One without attributes configured | ||
|
|
||
| Env vars (loaded from .env locally, injected via CI): | ||
| - LANGCACHE_WITH_ATTRIBUTES_API_KEY | ||
| - LANGCACHE_WITH_ATTRIBUTES_CACHE_ID | ||
| - LANGCACHE_WITH_ATTRIBUTES_URL | ||
| - LANGCACHE_NO_ATTRIBUTES_API_KEY | ||
| - LANGCACHE_NO_ATTRIBUTES_CACHE_ID | ||
| - LANGCACHE_NO_ATTRIBUTES_URL | ||
| """ | ||
|
|
||
| import os | ||
| from typing import Dict | ||
|
|
||
| import pytest | ||
|
|
||
| from redisvl.extensions.cache.llm.langcache import LangCacheSemanticCache | ||
|
|
||
| REQUIRED_WITH_ATTRS_VARS = ( | ||
| "LANGCACHE_WITH_ATTRIBUTES_API_KEY", | ||
| "LANGCACHE_WITH_ATTRIBUTES_CACHE_ID", | ||
| "LANGCACHE_WITH_ATTRIBUTES_URL", | ||
| ) | ||
|
|
||
| REQUIRED_NO_ATTRS_VARS = ( | ||
| "LANGCACHE_NO_ATTRIBUTES_API_KEY", | ||
| "LANGCACHE_NO_ATTRIBUTES_CACHE_ID", | ||
| "LANGCACHE_NO_ATTRIBUTES_URL", | ||
| ) | ||
|
|
||
|
|
||
| def _require_env_vars(var_names: tuple[str, ...]) -> Dict[str, str]: | ||
| missing = [name for name in var_names if not os.getenv(name)] | ||
| if missing: | ||
| pytest.skip( | ||
| f"Missing required LangCache env vars: {', '.join(missing)}. " | ||
| "Set them locally (e.g., via .env) or in CI secrets to run these tests." | ||
| ) | ||
|
|
||
| return {name: os.environ[name] for name in var_names} | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def langcache_with_attrs() -> LangCacheSemanticCache: | ||
| """LangCacheSemanticCache instance bound to a cache with attributes configured.""" | ||
|
|
||
| env = _require_env_vars(REQUIRED_WITH_ATTRS_VARS) | ||
|
|
||
| return LangCacheSemanticCache( | ||
| name="langcache_with_attributes", | ||
| server_url=env["LANGCACHE_WITH_ATTRIBUTES_URL"], | ||
| cache_id=env["LANGCACHE_WITH_ATTRIBUTES_CACHE_ID"], | ||
| api_key=env["LANGCACHE_WITH_ATTRIBUTES_API_KEY"], | ||
| ) | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def langcache_no_attrs() -> LangCacheSemanticCache: | ||
| """LangCacheSemanticCache instance bound to a cache with NO attributes configured.""" | ||
|
|
||
| env = _require_env_vars(REQUIRED_NO_ATTRS_VARS) | ||
|
|
||
| return LangCacheSemanticCache( | ||
| name="langcache_no_attributes", | ||
| server_url=env["LANGCACHE_NO_ATTRIBUTES_URL"], | ||
| cache_id=env["LANGCACHE_NO_ATTRIBUTES_CACHE_ID"], | ||
| api_key=env["LANGCACHE_NO_ATTRIBUTES_API_KEY"], | ||
| ) | ||
|
|
||
|
|
||
| @pytest.mark.requires_api_keys | ||
| class TestLangCacheSemanticCacheIntegrationWithAttributes: | ||
| def test_store_and_check_sync( | ||
| self, langcache_with_attrs: LangCacheSemanticCache | ||
| ) -> None: | ||
| prompt = "What is Redis?" | ||
| response = "Redis is an in-memory data store." | ||
|
|
||
| entry_id = langcache_with_attrs.store(prompt=prompt, response=response) | ||
| assert entry_id | ||
|
|
||
| hits = langcache_with_attrs.check(prompt=prompt, num_results=1) | ||
| assert hits | ||
| assert hits[0]["response"] == response | ||
| assert hits[0]["prompt"] == prompt | ||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_store_and_check_async( | ||
| self, langcache_with_attrs: LangCacheSemanticCache | ||
| ) -> None: | ||
| prompt = "What is Redis async?" | ||
| response = "Redis is an in-memory data store (async)." | ||
|
|
||
| entry_id = await langcache_with_attrs.astore(prompt=prompt, response=response) | ||
| assert entry_id | ||
|
|
||
| hits = await langcache_with_attrs.acheck(prompt=prompt, num_results=1) | ||
| assert hits | ||
| assert hits[0]["response"] == response | ||
| assert hits[0]["prompt"] == prompt | ||
|
|
||
| def test_store_with_metadata_and_check_with_attributes( | ||
| self, langcache_with_attrs: LangCacheSemanticCache | ||
| ) -> None: | ||
| prompt = "Explain Redis search." | ||
| response = "Redis provides full-text search via RediSearch." | ||
| # Use attribute names that are actually configured on this cache. | ||
| metadata = {"user_id": "tenant_a"} | ||
|
|
||
| entry_id = langcache_with_attrs.store( | ||
| prompt=prompt, | ||
| response=response, | ||
| metadata=metadata, | ||
| ) | ||
| assert entry_id | ||
|
|
||
| hits = langcache_with_attrs.check( | ||
| prompt=prompt, | ||
| attributes={"user_id": "tenant_a"}, | ||
| num_results=3, | ||
| ) | ||
| assert hits | ||
| assert any(hit["response"] == response for hit in hits) | ||
|
|
||
| def test_delete_and_clear_alias( | ||
| self, langcache_with_attrs: LangCacheSemanticCache | ||
| ) -> None: | ||
| """delete() and clear() should flush the whole cache.""" | ||
|
|
||
| prompt = "Delete me" | ||
| response = "You won't see me again." | ||
|
|
||
| langcache_with_attrs.store(prompt=prompt, response=response) | ||
| hits_before = langcache_with_attrs.check(prompt=prompt, num_results=5) | ||
| assert hits_before | ||
|
|
||
| # delete() and clear() both flush the whole cache | ||
| langcache_with_attrs.delete() | ||
| hits_after_delete = langcache_with_attrs.check(prompt=prompt, num_results=5) | ||
|
|
||
| # It is possible for other tests or data to exist; we only assert that | ||
| # the original response is no longer present if any hits are returned. | ||
| assert not any(hit["response"] == response for hit in hits_after_delete) | ||
|
|
||
| langcache_with_attrs.store(prompt=prompt, response=response) | ||
| langcache_with_attrs.clear() | ||
| hits_after_clear = langcache_with_attrs.check(prompt=prompt, num_results=5) | ||
| assert not any(hit["response"] == response for hit in hits_after_clear) | ||
|
|
||
| def test_delete_by_id_and_by_attributes( | ||
| self, langcache_with_attrs: LangCacheSemanticCache | ||
| ) -> None: | ||
| prompt = "Delete by id" | ||
| response = "Entry to delete by id." | ||
| metadata = {"user_id": "tenant_delete"} | ||
|
|
||
| entry_id = langcache_with_attrs.store( | ||
| prompt=prompt, | ||
| response=response, | ||
| metadata=metadata, | ||
| ) | ||
| assert entry_id | ||
|
|
||
| hits = langcache_with_attrs.check( | ||
| prompt=prompt, attributes=metadata, num_results=1 | ||
| ) | ||
| assert hits | ||
| assert hits[0]["entry_id"] == entry_id | ||
|
|
||
| # delete by id | ||
| langcache_with_attrs.delete_by_id(entry_id) | ||
| hits_after_id_delete = langcache_with_attrs.check( | ||
| prompt=prompt, attributes=metadata, num_results=3 | ||
| ) | ||
| assert not any(hit["entry_id"] == entry_id for hit in hits_after_id_delete) | ||
|
|
||
| # store multiple entries and delete by attributes | ||
| for i in range(3): | ||
| langcache_with_attrs.store( | ||
| prompt=f"{prompt} {i}", | ||
| response=f"{response} {i}", | ||
| metadata=metadata, | ||
| ) | ||
|
|
||
| delete_result = langcache_with_attrs.delete_by_attributes(attributes=metadata) | ||
| assert isinstance(delete_result, dict) | ||
| assert delete_result.get("deleted_entries_count", 0) >= 1 | ||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_async_delete_variants( | ||
| self, langcache_with_attrs: LangCacheSemanticCache | ||
| ) -> None: | ||
| prompt = "Async delete by attributes" | ||
| response = "Async delete candidate" | ||
| metadata = {"user_id": "tenant_async"} | ||
|
|
||
| entry_id = await langcache_with_attrs.astore( | ||
| prompt=prompt, | ||
| response=response, | ||
| metadata=metadata, | ||
| ) | ||
| assert entry_id | ||
|
|
||
| hits = await langcache_with_attrs.acheck(prompt=prompt, attributes=metadata) | ||
| assert hits | ||
|
|
||
| await langcache_with_attrs.adelete_by_id(entry_id) | ||
| hits_after_id_delete = await langcache_with_attrs.acheck( | ||
| prompt=prompt, attributes=metadata | ||
| ) | ||
| assert not any(hit["entry_id"] == entry_id for hit in hits_after_id_delete) | ||
|
|
||
| for i in range(2): | ||
| await langcache_with_attrs.astore( | ||
| prompt=f"{prompt} {i}", | ||
| response=f"{response} {i}", | ||
| metadata=metadata, | ||
| ) | ||
|
|
||
| delete_result = await langcache_with_attrs.adelete_by_attributes( | ||
| attributes=metadata | ||
| ) | ||
| assert isinstance(delete_result, dict) | ||
| assert delete_result.get("deleted_entries_count", 0) >= 1 | ||
|
|
||
| # Finally, aclear() should flush the cache. | ||
| await langcache_with_attrs.aclear() | ||
| hits_after_clear = await langcache_with_attrs.acheck( | ||
| prompt=prompt, num_results=5 | ||
| ) | ||
| assert not hits_after_clear | ||
abrookins marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
|
|
||
| @pytest.mark.requires_api_keys | ||
| class TestLangCacheSemanticCacheIntegrationWithoutAttributes: | ||
| def test_error_on_store_with_metadata_when_no_attributes_configured( | ||
| self, langcache_no_attrs: LangCacheSemanticCache | ||
| ) -> None: | ||
| from langcache.errors import BadRequestErrorResponseContent | ||
abrookins marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
abrookins marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| prompt = "Attributes not configured" | ||
| response = "This should fail due to missing attributes configuration." | ||
|
|
||
| with pytest.raises(RuntimeError) as exc: | ||
| langcache_no_attrs.store( | ||
| prompt=prompt, | ||
| response=response, | ||
| metadata={"tenant": "tenant_without_attrs"}, | ||
| ) | ||
|
|
||
| assert "attributes are not configured for this cache" in str(exc.value).lower() | ||
|
|
||
| def test_error_on_check_with_attributes_when_no_attributes_configured( | ||
| self, langcache_no_attrs: LangCacheSemanticCache | ||
| ) -> None: | ||
| prompt = "Attributes not configured on check" | ||
|
|
||
| with pytest.raises(RuntimeError) as exc: | ||
| langcache_no_attrs.check( | ||
| prompt=prompt, | ||
| attributes={"tenant": "tenant_without_attrs"}, | ||
| ) | ||
|
|
||
| assert "attributes are not configured for this cache" in str(exc.value).lower() | ||
|
|
||
| def test_basic_store_and_check_works_without_attributes( | ||
| self, langcache_no_attrs: LangCacheSemanticCache | ||
| ) -> None: | ||
| prompt = "Plain cache without attributes" | ||
| response = "This should be cached successfully." | ||
|
|
||
| entry_id = langcache_no_attrs.store(prompt=prompt, response=response) | ||
| assert entry_id | ||
|
|
||
| hits = langcache_no_attrs.check(prompt=prompt) | ||
| assert hits | ||
| assert any(hit["response"] == response for hit in hits) | ||
|
|
||
| def test_attribute_value_with_comma_passes_through_to_api( | ||
| self, langcache_with_attrs: LangCacheSemanticCache | ||
| ) -> None: | ||
abrookins marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| """We currently rely on the LangCache API to validate commas in attribute values. | ||
|
|
||
| This test verifies we do not perform client-side validation and that the | ||
| error is raised by the backend. If this behavior changes, this test will | ||
| need to be updated. | ||
| """ | ||
|
|
||
| from langcache.errors import BadRequestErrorResponseContent | ||
abrookins marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| prompt = "Comma attribute value" | ||
| response = "This may fail depending on the remote validation rules." | ||
|
|
||
| with pytest.raises(BadRequestErrorResponseContent): | ||
| langcache_with_attrs.store( | ||
| prompt=prompt, | ||
| response=response, | ||
| metadata={"llm_string": "tenant,with,comma"}, | ||
abrookins marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| ) | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
BadRequestErrorResponseContentimport is unused in this test file. The actual error handling in the implementation file (redisvl/extensions/cache/llm/langcache.py) imports this conditionally within exception handlers, and the tests only verify the wrappedRuntimeErrorexceptions. Consider removing this unused import.