Skip to content

Commit 72a6cbc

Browse files
committed
Add LangCache integration tests
1 parent 62dc045 commit 72a6cbc

File tree

4 files changed

+314
-5
lines changed

4 files changed

+314
-5
lines changed

.github/workflows/test.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,12 @@ jobs:
7676
OPENAI_API_VERSION: ${{ secrets.OPENAI_API_VERSION }}
7777
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
7878
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
79+
LANGCACHE_WITH_ATTRIBUTES_API_KEY: ${{ secrets.LANGCACHE_WITH_ATTRIBUTES_API_KEY }}
80+
LANGCACHE_WITH_ATTRIBUTES_CACHE_ID: ${{ secrets.LANGCACHE_WITH_ATTRIBUTES_CACHE_ID }}
81+
LANGCACHE_WITH_ATTRIBUTES_URL: ${{ secrets.LANGCACHE_WITH_ATTRIBUTES_URL }}
82+
LANGCACHE_NO_ATTRIBUTES_API_KEY: ${{ secrets.LANGCACHE_NO_ATTRIBUTES_API_KEY }}
83+
LANGCACHE_NO_ATTRIBUTES_CACHE_ID: ${{ secrets.LANGCACHE_NO_ATTRIBUTES_CACHE_ID }}
84+
LANGCACHE_NO_ATTRIBUTES_URL: ${{ secrets.LANGCACHE_NO_ATTRIBUTES_URL }}
7985
run: |
8086
make test-all
8187

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ nltk = ["nltk>=3.8.1,<4"]
3939
cohere = ["cohere>=4.44"]
4040
voyageai = ["voyageai>=0.2.2"]
4141
sentence-transformers = ["sentence-transformers>=3.4.0,<4"]
42-
langcache = ["langcache>=0.9.0"]
42+
langcache = ["langcache>=0.11.0"]
4343
vertexai = [
4444
"google-cloud-aiplatform>=1.26,<2.0.0",
4545
"protobuf>=5.28.0,<6.0.0",
Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
"""Integration tests for LangCacheSemanticCache against the LangCache managed service.
2+
3+
These tests exercise the real LangCache API using two configured caches:
4+
- One with attributes configured
5+
- One without attributes configured
6+
7+
Env vars (loaded from .env locally, injected via CI):
8+
- LANGCACHE_WITH_ATTRIBUTES_API_KEY
9+
- LANGCACHE_WITH_ATTRIBUTES_CACHE_ID
10+
- LANGCACHE_WITH_ATTRIBUTES_URL
11+
- LANGCACHE_NO_ATTRIBUTES_API_KEY
12+
- LANGCACHE_NO_ATTRIBUTES_CACHE_ID
13+
- LANGCACHE_NO_ATTRIBUTES_URL
14+
"""
15+
16+
import os
17+
from typing import Dict
18+
19+
import pytest
20+
21+
from redisvl.extensions.cache.llm.langcache import LangCacheSemanticCache
22+
23+
REQUIRED_WITH_ATTRS_VARS = (
24+
"LANGCACHE_WITH_ATTRIBUTES_API_KEY",
25+
"LANGCACHE_WITH_ATTRIBUTES_CACHE_ID",
26+
"LANGCACHE_WITH_ATTRIBUTES_URL",
27+
)
28+
29+
REQUIRED_NO_ATTRS_VARS = (
30+
"LANGCACHE_NO_ATTRIBUTES_API_KEY",
31+
"LANGCACHE_NO_ATTRIBUTES_CACHE_ID",
32+
"LANGCACHE_NO_ATTRIBUTES_URL",
33+
)
34+
35+
36+
def _require_env_vars(var_names: tuple[str, ...]) -> Dict[str, str]:
37+
missing = [name for name in var_names if not os.getenv(name)]
38+
if missing:
39+
pytest.skip(
40+
f"Missing required LangCache env vars: {', '.join(missing)}. "
41+
"Set them locally (e.g., via .env) or in CI secrets to run these tests."
42+
)
43+
44+
return {name: os.environ[name] for name in var_names}
45+
46+
47+
@pytest.fixture
48+
def langcache_with_attrs() -> LangCacheSemanticCache:
49+
"""LangCacheSemanticCache instance bound to a cache with attributes configured."""
50+
51+
env = _require_env_vars(REQUIRED_WITH_ATTRS_VARS)
52+
53+
return LangCacheSemanticCache(
54+
name="langcache_with_attributes",
55+
server_url=env["LANGCACHE_WITH_ATTRIBUTES_URL"],
56+
cache_id=env["LANGCACHE_WITH_ATTRIBUTES_CACHE_ID"],
57+
api_key=env["LANGCACHE_WITH_ATTRIBUTES_API_KEY"],
58+
)
59+
60+
61+
@pytest.fixture
62+
def langcache_no_attrs() -> LangCacheSemanticCache:
63+
"""LangCacheSemanticCache instance bound to a cache with NO attributes configured."""
64+
65+
env = _require_env_vars(REQUIRED_NO_ATTRS_VARS)
66+
67+
return LangCacheSemanticCache(
68+
name="langcache_no_attributes",
69+
server_url=env["LANGCACHE_NO_ATTRIBUTES_URL"],
70+
cache_id=env["LANGCACHE_NO_ATTRIBUTES_CACHE_ID"],
71+
api_key=env["LANGCACHE_NO_ATTRIBUTES_API_KEY"],
72+
)
73+
74+
75+
@pytest.mark.requires_api_keys
76+
class TestLangCacheSemanticCacheIntegrationWithAttributes:
77+
def test_store_and_check_sync(
78+
self, langcache_with_attrs: LangCacheSemanticCache
79+
) -> None:
80+
prompt = "What is Redis?"
81+
response = "Redis is an in-memory data store."
82+
83+
entry_id = langcache_with_attrs.store(prompt=prompt, response=response)
84+
assert entry_id
85+
86+
hits = langcache_with_attrs.check(prompt=prompt, num_results=1)
87+
assert hits
88+
assert hits[0]["response"] == response
89+
assert hits[0]["prompt"] == prompt
90+
91+
@pytest.mark.asyncio
92+
async def test_store_and_check_async(
93+
self, langcache_with_attrs: LangCacheSemanticCache
94+
) -> None:
95+
prompt = "What is Redis async?"
96+
response = "Redis is an in-memory data store (async)."
97+
98+
entry_id = await langcache_with_attrs.astore(prompt=prompt, response=response)
99+
assert entry_id
100+
101+
hits = await langcache_with_attrs.acheck(prompt=prompt, num_results=1)
102+
assert hits
103+
assert hits[0]["response"] == response
104+
assert hits[0]["prompt"] == prompt
105+
106+
def test_store_with_metadata_and_check_with_attributes(
107+
self, langcache_with_attrs: LangCacheSemanticCache
108+
) -> None:
109+
prompt = "Explain Redis search."
110+
response = "Redis provides full-text search via RediSearch."
111+
# Use attribute names that are actually configured on this cache.
112+
metadata = {"user_id": "tenant_a"}
113+
114+
entry_id = langcache_with_attrs.store(
115+
prompt=prompt,
116+
response=response,
117+
metadata=metadata,
118+
)
119+
assert entry_id
120+
121+
hits = langcache_with_attrs.check(
122+
prompt=prompt,
123+
attributes={"user_id": "tenant_a"},
124+
num_results=3,
125+
)
126+
assert hits
127+
assert any(hit["response"] == response for hit in hits)
128+
129+
def test_delete_and_clear_alias(
130+
self, langcache_with_attrs: LangCacheSemanticCache
131+
) -> None:
132+
"""delete() and clear() should flush the whole cache."""
133+
134+
prompt = "Delete me"
135+
response = "You won't see me again."
136+
137+
langcache_with_attrs.store(prompt=prompt, response=response)
138+
hits_before = langcache_with_attrs.check(prompt=prompt, num_results=5)
139+
assert hits_before
140+
141+
# delete() and clear() both flush the whole cache
142+
langcache_with_attrs.delete()
143+
hits_after_delete = langcache_with_attrs.check(prompt=prompt, num_results=5)
144+
145+
# It is possible for other tests or data to exist; we only assert that
146+
# the original response is no longer present if any hits are returned.
147+
assert not any(hit["response"] == response for hit in hits_after_delete)
148+
149+
langcache_with_attrs.store(prompt=prompt, response=response)
150+
langcache_with_attrs.clear()
151+
hits_after_clear = langcache_with_attrs.check(prompt=prompt, num_results=5)
152+
assert not any(hit["response"] == response for hit in hits_after_clear)
153+
154+
def test_delete_by_id_and_by_attributes(
155+
self, langcache_with_attrs: LangCacheSemanticCache
156+
) -> None:
157+
prompt = "Delete by id"
158+
response = "Entry to delete by id."
159+
metadata = {"user_id": "tenant_delete"}
160+
161+
entry_id = langcache_with_attrs.store(
162+
prompt=prompt,
163+
response=response,
164+
metadata=metadata,
165+
)
166+
assert entry_id
167+
168+
hits = langcache_with_attrs.check(
169+
prompt=prompt, attributes=metadata, num_results=1
170+
)
171+
assert hits
172+
assert hits[0]["entry_id"] == entry_id
173+
174+
# delete by id
175+
langcache_with_attrs.delete_by_id(entry_id)
176+
hits_after_id_delete = langcache_with_attrs.check(
177+
prompt=prompt, attributes=metadata, num_results=3
178+
)
179+
assert not any(hit["entry_id"] == entry_id for hit in hits_after_id_delete)
180+
181+
# store multiple entries and delete by attributes
182+
for i in range(3):
183+
langcache_with_attrs.store(
184+
prompt=f"{prompt} {i}",
185+
response=f"{response} {i}",
186+
metadata=metadata,
187+
)
188+
189+
delete_result = langcache_with_attrs.delete_by_attributes(attributes=metadata)
190+
assert isinstance(delete_result, dict)
191+
assert delete_result.get("deleted_entries_count", 0) >= 1
192+
193+
@pytest.mark.asyncio
194+
async def test_async_delete_variants(
195+
self, langcache_with_attrs: LangCacheSemanticCache
196+
) -> None:
197+
prompt = "Async delete by attributes"
198+
response = "Async delete candidate"
199+
metadata = {"user_id": "tenant_async"}
200+
201+
entry_id = await langcache_with_attrs.astore(
202+
prompt=prompt,
203+
response=response,
204+
metadata=metadata,
205+
)
206+
assert entry_id
207+
208+
hits = await langcache_with_attrs.acheck(prompt=prompt, attributes=metadata)
209+
assert hits
210+
211+
await langcache_with_attrs.adelete_by_id(entry_id)
212+
hits_after_id_delete = await langcache_with_attrs.acheck(
213+
prompt=prompt, attributes=metadata
214+
)
215+
assert not any(hit["entry_id"] == entry_id for hit in hits_after_id_delete)
216+
217+
for i in range(2):
218+
await langcache_with_attrs.astore(
219+
prompt=f"{prompt} {i}",
220+
response=f"{response} {i}",
221+
metadata=metadata,
222+
)
223+
224+
delete_result = await langcache_with_attrs.adelete_by_attributes(
225+
attributes=metadata
226+
)
227+
assert isinstance(delete_result, dict)
228+
assert delete_result.get("deleted_entries_count", 0) >= 1
229+
230+
# Finally, aclear() should flush the cache.
231+
await langcache_with_attrs.aclear()
232+
hits_after_clear = await langcache_with_attrs.acheck(
233+
prompt=prompt, num_results=5
234+
)
235+
assert not hits_after_clear
236+
237+
238+
@pytest.mark.requires_api_keys
239+
class TestLangCacheSemanticCacheIntegrationWithoutAttributes:
240+
def test_error_on_store_with_metadata_when_no_attributes_configured(
241+
self, langcache_no_attrs: LangCacheSemanticCache
242+
) -> None:
243+
from langcache.errors import BadRequestErrorResponseContent
244+
245+
prompt = "Attributes not configured"
246+
response = "This should fail due to missing attributes configuration."
247+
248+
with pytest.raises(RuntimeError) as exc:
249+
langcache_no_attrs.store(
250+
prompt=prompt,
251+
response=response,
252+
metadata={"tenant": "tenant_without_attrs"},
253+
)
254+
255+
assert "attributes are not configured for this cache" in str(exc.value).lower()
256+
257+
def test_error_on_check_with_attributes_when_no_attributes_configured(
258+
self, langcache_no_attrs: LangCacheSemanticCache
259+
) -> None:
260+
prompt = "Attributes not configured on check"
261+
262+
with pytest.raises(RuntimeError) as exc:
263+
langcache_no_attrs.check(
264+
prompt=prompt,
265+
attributes={"tenant": "tenant_without_attrs"},
266+
)
267+
268+
assert "attributes are not configured for this cache" in str(exc.value).lower()
269+
270+
def test_basic_store_and_check_works_without_attributes(
271+
self, langcache_no_attrs: LangCacheSemanticCache
272+
) -> None:
273+
prompt = "Plain cache without attributes"
274+
response = "This should be cached successfully."
275+
276+
entry_id = langcache_no_attrs.store(prompt=prompt, response=response)
277+
assert entry_id
278+
279+
hits = langcache_no_attrs.check(prompt=prompt)
280+
assert hits
281+
assert any(hit["response"] == response for hit in hits)
282+
283+
def test_attribute_value_with_comma_passes_through_to_api(
284+
self, langcache_with_attrs: LangCacheSemanticCache
285+
) -> None:
286+
"""We currently rely on the LangCache API to validate commas in attribute values.
287+
288+
This test verifies we do not perform client-side validation and that the
289+
error is raised by the backend. If this behavior changes, this test will
290+
need to be updated.
291+
"""
292+
293+
from langcache.errors import BadRequestErrorResponseContent
294+
295+
prompt = "Comma attribute value"
296+
response = "This may fail depending on the remote validation rules."
297+
298+
with pytest.raises(BadRequestErrorResponseContent):
299+
langcache_with_attrs.store(
300+
prompt=prompt,
301+
response=response,
302+
metadata={"llm_string": "tenant,with,comma"},
303+
)

uv.lock

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)