2023-09-04 03:27:02 +00:00
|
|
|
"""Test Cassandra caches. Requires a running vector-capable Cassandra cluster."""
|
2024-04-30 14:27:44 +00:00
|
|
|
import asyncio
|
2023-09-04 03:27:02 +00:00
|
|
|
import os
|
|
|
|
import time
|
|
|
|
from typing import Any, Iterator, Tuple
|
|
|
|
|
|
|
|
import pytest
|
2024-05-08 20:46:52 +00:00
|
|
|
from langchain.globals import get_llm_cache, set_llm_cache
|
2023-11-21 16:35:29 +00:00
|
|
|
from langchain_core.outputs import Generation, LLMResult
|
2023-09-04 03:27:02 +00:00
|
|
|
|
2024-05-08 20:46:52 +00:00
|
|
|
from langchain_community.cache import CassandraCache, CassandraSemanticCache
|
|
|
|
from langchain_community.utilities.cassandra import SetupMode
|
2023-12-11 21:53:30 +00:00
|
|
|
from tests.integration_tests.cache.fake_embeddings import FakeEmbeddings
|
2023-09-04 03:27:02 +00:00
|
|
|
from tests.unit_tests.llms.fake_llm import FakeLLM
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
|
|
def cassandra_connection() -> Iterator[Tuple[Any, str]]:
|
|
|
|
from cassandra.cluster import Cluster
|
|
|
|
|
|
|
|
keyspace = "langchain_cache_test_keyspace"
|
|
|
|
# get db connection
|
|
|
|
if "CASSANDRA_CONTACT_POINTS" in os.environ:
|
community: init signature revision for Cassandra LLM cache classes + small maintenance (#17765)
This PR improves on the `CassandraCache` and `CassandraSemanticCache`
classes, mainly in the constructor signature, and also introduces
several minor improvements around these classes.
### Init signature
A (sigh) breaking change is tentatively introduced to the constructor.
To me, the advantages outweigh the possible discomfort: the new syntax
places the DB-connection objects `session` and `keyspace` later in the
param list, so that they can be given a default value. This is what
enables the pattern of _not_ specifying them, provided one has
previously initialized the Cassandra connection through the versatile
utility method `cassio.init(...)`.
In this way, a much less unwieldy instantiation can be done, such as
`CassandraCache()` and `CassandraSemanticCache(embedding=xyz)`,
everything else falling back to defaults.
A downside is that, compared to the earlier signature, this might turn
out to be breaking for those doing positional instantiation. As a way to
mitigate this problem, this PR typechecks its first argument trying to
detect the legacy usage.
(And to make this point less tricky in the future, most arguments are
left to be keyword-only).
If this is considered too harsh, I'd like guidance on how to further
smoothen this transition. **Our plan is to make the pattern of optional
session/keyspace a standard across all Cassandra classes**, so that a
repeatable strategy would be ideal. A possibility would be to keep
positional arguments for legacy reasons but issue a deprecation warning
if any of them is actually used, to later remove them with 0.2 - please
advise on this point.
### Other changes
- class docstrings: enriched, completely moved to class level, added
note on `cassio.init(...)` pattern, added tiny sample usage code.
- semantic cache: revised terminology to never mention "distance" (it is
in fact a similarity!). Kept the legacy constructor param with a
deprecation warning if used.
- `llm_caching` notebook: uniform flow with the Cassandra and Astra DB
separate cases; better and Cassandra-first description; all imports made
explicit and from community where appropriate.
- cache integration tests moved to community (incl. the imported tools),
env var bugfix for `CASSANDRA_CONTACT_POINTS`.
---------
Co-authored-by: Erick Friis <erick@langchain.dev>
2024-05-16 17:22:24 +00:00
|
|
|
contact_points = os.environ["CASSANDRA_CONTACT_POINTS"].split(",")
|
2023-09-04 03:27:02 +00:00
|
|
|
cluster = Cluster(contact_points)
|
|
|
|
else:
|
|
|
|
cluster = Cluster()
|
|
|
|
#
|
|
|
|
session = cluster.connect()
|
|
|
|
# ensure keyspace exists
|
|
|
|
session.execute(
|
|
|
|
(
|
|
|
|
f"CREATE KEYSPACE IF NOT EXISTS {keyspace} "
|
|
|
|
f"WITH replication = {{'class': 'SimpleStrategy', 'replication_factor': 1}}"
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
yield (session, keyspace)
|
|
|
|
|
|
|
|
|
|
|
|
def test_cassandra_cache(cassandra_connection: Tuple[Any, str]) -> None:
|
|
|
|
session, keyspace = cassandra_connection
|
2023-09-14 15:33:06 +00:00
|
|
|
cache = CassandraCache(session=session, keyspace=keyspace)
|
2023-10-14 16:29:30 +00:00
|
|
|
set_llm_cache(cache)
|
2023-09-04 03:27:02 +00:00
|
|
|
llm = FakeLLM()
|
|
|
|
params = llm.dict()
|
|
|
|
params["stop"] = None
|
|
|
|
llm_string = str(sorted([(k, v) for k, v in params.items()]))
|
2023-10-14 16:29:30 +00:00
|
|
|
get_llm_cache().update("foo", llm_string, [Generation(text="fizz")])
|
2023-09-04 03:27:02 +00:00
|
|
|
output = llm.generate(["foo"])
|
|
|
|
expected_output = LLMResult(
|
|
|
|
generations=[[Generation(text="fizz")]],
|
|
|
|
llm_output={},
|
|
|
|
)
|
|
|
|
assert output == expected_output
|
|
|
|
cache.clear()
|
|
|
|
|
|
|
|
|
2024-04-30 14:27:44 +00:00
|
|
|
async def test_cassandra_cache_async(cassandra_connection: Tuple[Any, str]) -> None:
|
|
|
|
session, keyspace = cassandra_connection
|
|
|
|
cache = CassandraCache(
|
|
|
|
session=session, keyspace=keyspace, setup_mode=SetupMode.ASYNC
|
|
|
|
)
|
|
|
|
set_llm_cache(cache)
|
|
|
|
llm = FakeLLM()
|
|
|
|
params = llm.dict()
|
|
|
|
params["stop"] = None
|
|
|
|
llm_string = str(sorted([(k, v) for k, v in params.items()]))
|
|
|
|
await get_llm_cache().aupdate("foo", llm_string, [Generation(text="fizz")])
|
|
|
|
output = await llm.agenerate(["foo"])
|
|
|
|
expected_output = LLMResult(
|
|
|
|
generations=[[Generation(text="fizz")]],
|
|
|
|
llm_output={},
|
|
|
|
)
|
|
|
|
assert output == expected_output
|
|
|
|
await cache.aclear()
|
|
|
|
|
|
|
|
|
2023-09-04 03:27:02 +00:00
|
|
|
def test_cassandra_cache_ttl(cassandra_connection: Tuple[Any, str]) -> None:
|
|
|
|
session, keyspace = cassandra_connection
|
2023-09-14 15:33:06 +00:00
|
|
|
cache = CassandraCache(session=session, keyspace=keyspace, ttl_seconds=2)
|
2023-10-14 16:29:30 +00:00
|
|
|
set_llm_cache(cache)
|
2023-09-04 03:27:02 +00:00
|
|
|
llm = FakeLLM()
|
|
|
|
params = llm.dict()
|
|
|
|
params["stop"] = None
|
|
|
|
llm_string = str(sorted([(k, v) for k, v in params.items()]))
|
2023-10-14 16:29:30 +00:00
|
|
|
get_llm_cache().update("foo", llm_string, [Generation(text="fizz")])
|
2023-09-04 03:27:02 +00:00
|
|
|
expected_output = LLMResult(
|
|
|
|
generations=[[Generation(text="fizz")]],
|
|
|
|
llm_output={},
|
|
|
|
)
|
|
|
|
output = llm.generate(["foo"])
|
|
|
|
assert output == expected_output
|
|
|
|
time.sleep(2.5)
|
|
|
|
# entry has expired away.
|
|
|
|
output = llm.generate(["foo"])
|
|
|
|
assert output != expected_output
|
|
|
|
cache.clear()
|
|
|
|
|
|
|
|
|
2024-04-30 14:27:44 +00:00
|
|
|
async def test_cassandra_cache_ttl_async(cassandra_connection: Tuple[Any, str]) -> None:
|
|
|
|
session, keyspace = cassandra_connection
|
|
|
|
cache = CassandraCache(
|
|
|
|
session=session, keyspace=keyspace, ttl_seconds=2, setup_mode=SetupMode.ASYNC
|
|
|
|
)
|
|
|
|
set_llm_cache(cache)
|
|
|
|
llm = FakeLLM()
|
|
|
|
params = llm.dict()
|
|
|
|
params["stop"] = None
|
|
|
|
llm_string = str(sorted([(k, v) for k, v in params.items()]))
|
|
|
|
await get_llm_cache().aupdate("foo", llm_string, [Generation(text="fizz")])
|
|
|
|
expected_output = LLMResult(
|
|
|
|
generations=[[Generation(text="fizz")]],
|
|
|
|
llm_output={},
|
|
|
|
)
|
|
|
|
output = await llm.agenerate(["foo"])
|
|
|
|
assert output == expected_output
|
|
|
|
await asyncio.sleep(2.5)
|
|
|
|
# entry has expired away.
|
|
|
|
output = await llm.agenerate(["foo"])
|
|
|
|
assert output != expected_output
|
|
|
|
await cache.aclear()
|
|
|
|
|
|
|
|
|
2023-09-04 03:27:02 +00:00
|
|
|
def test_cassandra_semantic_cache(cassandra_connection: Tuple[Any, str]) -> None:
|
|
|
|
session, keyspace = cassandra_connection
|
2023-09-14 15:33:06 +00:00
|
|
|
sem_cache = CassandraSemanticCache(
|
|
|
|
session=session,
|
|
|
|
keyspace=keyspace,
|
|
|
|
embedding=FakeEmbeddings(),
|
|
|
|
)
|
2023-10-14 16:29:30 +00:00
|
|
|
set_llm_cache(sem_cache)
|
2023-09-04 03:27:02 +00:00
|
|
|
llm = FakeLLM()
|
|
|
|
params = llm.dict()
|
|
|
|
params["stop"] = None
|
|
|
|
llm_string = str(sorted([(k, v) for k, v in params.items()]))
|
2023-10-14 16:29:30 +00:00
|
|
|
get_llm_cache().update("foo", llm_string, [Generation(text="fizz")])
|
2023-09-04 03:27:02 +00:00
|
|
|
output = llm.generate(["bar"]) # same embedding as 'foo'
|
|
|
|
expected_output = LLMResult(
|
|
|
|
generations=[[Generation(text="fizz")]],
|
|
|
|
llm_output={},
|
|
|
|
)
|
|
|
|
assert output == expected_output
|
|
|
|
# clear the cache
|
|
|
|
sem_cache.clear()
|
|
|
|
output = llm.generate(["bar"]) # 'fizz' is erased away now
|
|
|
|
assert output != expected_output
|
|
|
|
sem_cache.clear()
|
2024-04-30 14:27:44 +00:00
|
|
|
|
|
|
|
|
|
|
|
async def test_cassandra_semantic_cache_async(
|
|
|
|
cassandra_connection: Tuple[Any, str],
|
|
|
|
) -> None:
|
|
|
|
session, keyspace = cassandra_connection
|
|
|
|
sem_cache = CassandraSemanticCache(
|
|
|
|
session=session,
|
|
|
|
keyspace=keyspace,
|
|
|
|
embedding=FakeEmbeddings(),
|
|
|
|
setup_mode=SetupMode.ASYNC,
|
|
|
|
)
|
|
|
|
set_llm_cache(sem_cache)
|
|
|
|
llm = FakeLLM()
|
|
|
|
params = llm.dict()
|
|
|
|
params["stop"] = None
|
|
|
|
llm_string = str(sorted([(k, v) for k, v in params.items()]))
|
|
|
|
await get_llm_cache().aupdate("foo", llm_string, [Generation(text="fizz")])
|
|
|
|
output = await llm.agenerate(["bar"]) # same embedding as 'foo'
|
|
|
|
expected_output = LLMResult(
|
|
|
|
generations=[[Generation(text="fizz")]],
|
|
|
|
llm_output={},
|
|
|
|
)
|
|
|
|
assert output == expected_output
|
|
|
|
# clear the cache
|
|
|
|
await sem_cache.aclear()
|
|
|
|
output = await llm.agenerate(["bar"]) # 'fizz' is erased away now
|
|
|
|
assert output != expected_output
|
|
|
|
await sem_cache.aclear()
|