cacheverifier 0.4.1__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. {cacheverifier-0.4.1 → cacheverifier-0.5.0}/CHANGELOG.md +17 -0
  2. {cacheverifier-0.4.1 → cacheverifier-0.5.0}/PKG-INFO +41 -1
  3. {cacheverifier-0.4.1 → cacheverifier-0.5.0}/README.md +37 -0
  4. {cacheverifier-0.4.1 → cacheverifier-0.5.0}/cacheverifier/__init__.py +3 -3
  5. {cacheverifier-0.4.1 → cacheverifier-0.5.0}/cacheverifier/client.py +36 -0
  6. cacheverifier-0.5.0/cacheverifier/integrations/langchain.py +140 -0
  7. {cacheverifier-0.4.1 → cacheverifier-0.5.0}/pyproject.toml +3 -2
  8. {cacheverifier-0.4.1 → cacheverifier-0.5.0}/tests/test_client.py +27 -1
  9. cacheverifier-0.5.0/tests/test_langchain_integration.py +90 -0
  10. {cacheverifier-0.4.1 → cacheverifier-0.5.0}/.github/workflows/ci.yml +0 -0
  11. {cacheverifier-0.4.1 → cacheverifier-0.5.0}/.github/workflows/publish.yml +0 -0
  12. {cacheverifier-0.4.1 → cacheverifier-0.5.0}/.gitignore +0 -0
  13. {cacheverifier-0.4.1 → cacheverifier-0.5.0}/LICENSE +0 -0
  14. {cacheverifier-0.4.1 → cacheverifier-0.5.0}/cacheverifier/__main__.py +0 -0
  15. {cacheverifier-0.4.1 → cacheverifier-0.5.0}/cacheverifier/_healthcheck/__init__.py +0 -0
  16. {cacheverifier-0.4.1 → cacheverifier-0.5.0}/cacheverifier/_healthcheck/_base_models.py +0 -0
  17. {cacheverifier-0.4.1 → cacheverifier-0.5.0}/cacheverifier/_healthcheck/_finetune.py +0 -0
  18. {cacheverifier-0.4.1 → cacheverifier-0.5.0}/cacheverifier/_healthcheck/cli.py +0 -0
  19. {cacheverifier-0.4.1 → cacheverifier-0.5.0}/cacheverifier/integrations/__init__.py +0 -0
  20. {cacheverifier-0.4.1 → cacheverifier-0.5.0}/cacheverifier/integrations/gptcache.py +0 -0
  21. {cacheverifier-0.4.1 → cacheverifier-0.5.0}/cacheverifier/py.typed +0 -0
  22. {cacheverifier-0.4.1 → cacheverifier-0.5.0}/examples/gptcache_example.py +0 -0
  23. {cacheverifier-0.4.1 → cacheverifier-0.5.0}/examples/quickstart.py +0 -0
  24. {cacheverifier-0.4.1 → cacheverifier-0.5.0}/tests/test_gptcache_integration.py +0 -0
  25. {cacheverifier-0.4.1 → cacheverifier-0.5.0}/tests/test_healthcheck.py +0 -0
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.0
4
+
5
+ New LangChain integration: `cacheverifier.integrations.langchain.VerifiedCache`
6
+ wraps any LangChain cache (e.g. `RedisSemanticCache`) and verifies each hit
7
+ before it is served; a rejected hit becomes a miss. Install with
8
+ `pip install "cacheverifier[langchain]"`.
9
+
10
+ `VerifyResult` gains two optional fields, both None unless the server sends
11
+ them:
12
+
13
+ - `notice`: the plain-language reason a call wasn't decided by a model (cold
14
+ start), which the API already returned but the client dropped.
15
+ - `shadow`: on a cold-start call, a `ShadowVerdict` with what the shared stock
16
+ model would have decided (`approved`, `score`, `threshold`, `latency_ms`).
17
+ The call itself is still rejected, so this is a preview: your first calls
18
+ on a new account show what the model thinks before anything is served.
19
+
3
20
  ## 0.4.1
4
21
 
5
22
  `verify()` and `verify_batch()` now treat a **rate-limited 429** (one with a
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cacheverifier
3
- Version: 0.4.1
3
+ Version: 0.5.0
4
4
  Summary: Python client for the hosted CacheVerifier semantic-cache verification API
5
5
  Project-URL: Homepage, https://www.cacheverifier.com
6
6
  Project-URL: Documentation, https://www.cacheverifier.com/docs
@@ -19,6 +19,7 @@ Classifier: Typing :: Typed
19
19
  Requires-Python: >=3.9
20
20
  Requires-Dist: httpx>=0.24
21
21
  Provides-Extra: dev
22
+ Requires-Dist: langchain-core>=0.3; extra == 'dev'
22
23
  Requires-Dist: mypy>=1.5; extra == 'dev'
23
24
  Requires-Dist: pytest>=7; extra == 'dev'
24
25
  Requires-Dist: ruff>=0.4; extra == 'dev'
@@ -28,6 +29,8 @@ Provides-Extra: healthcheck
28
29
  Requires-Dist: numpy<2,>=1.26; extra == 'healthcheck'
29
30
  Requires-Dist: sentence-transformers<4,>=3.0; extra == 'healthcheck'
30
31
  Requires-Dist: torch>=2.2; extra == 'healthcheck'
32
+ Provides-Extra: langchain
33
+ Requires-Dist: langchain-core>=0.3; extra == 'langchain'
31
34
  Description-Content-Type: text/markdown
32
35
 
33
36
  # cacheverifier
@@ -94,6 +97,19 @@ cv.feedback(query, answer, was_correct=True, similarity_score=0.86)
94
97
  | `model_version` | `"stock"`, `"v<id>"` (fine-tuned), `"cold_start_fail_closed"` / `"cold_start_auto_pending"`, or `"verify_unavailable"` |
95
98
  | `latency_ms` | server-side inference time |
96
99
  | `degraded` | `True` when the call failed and this result was synthesized client-side |
100
+ | `notice` | why no model decided the call (cold start); `None` otherwise |
101
+ | `shadow` | cold start only: a `ShadowVerdict` with what the shared stock model would have decided. The call is still rejected, so treat it as a preview |
102
+
103
+ A new account starts in cold start: every call returns `approved=False` until a
104
+ Health Check shows the shared model is accurate enough on your data. Look at
105
+ `result.shadow` to see what it would have decided in the meantime:
106
+
107
+ ```python
108
+ result = cv.verify("when does my flight board", "Boarding starts 40 minutes before departure.")
109
+ if result.shadow:
110
+ print(result.notice)
111
+ print("stock model would approve:", result.shadow.approved, result.shadow.score)
112
+ ```
97
113
 
98
114
  ## On your request path
99
115
 
@@ -139,6 +155,30 @@ evaluator.report_feedback(query, answer, was_correct=False, similarity_score=0.9
139
155
 
140
156
  See [`examples/gptcache_example.py`](examples/gptcache_example.py).
141
157
 
158
+ ## LangChain
159
+
160
+ `VerifiedCache` wraps any LangChain cache, typically a semantic one, and only
161
+ serves a hit after CacheVerifier approves it; a rejected hit is a miss, so
162
+ your model is called as usual. For chat models it verifies against the last
163
+ user message.
164
+
165
+ ```bash
166
+ pip install "cacheverifier[langchain]"
167
+ ```
168
+
169
+ ```python
170
+ from langchain_core.globals import set_llm_cache
171
+ from langchain_redis import RedisSemanticCache
172
+ from cacheverifier.integrations.langchain import VerifiedCache
173
+
174
+ set_llm_cache(
175
+ VerifiedCache(
176
+ RedisSemanticCache(redis_url="redis://localhost:6379", embeddings=embeddings),
177
+ api_key="cv_...",
178
+ )
179
+ )
180
+ ```
181
+
142
182
  ## Fine-tuning
143
183
 
144
184
  Once you have ~20+ feedback rows (the service found fine-tuning is often a net negative
@@ -62,6 +62,19 @@ cv.feedback(query, answer, was_correct=True, similarity_score=0.86)
62
62
  | `model_version` | `"stock"`, `"v<id>"` (fine-tuned), `"cold_start_fail_closed"` / `"cold_start_auto_pending"`, or `"verify_unavailable"` |
63
63
  | `latency_ms` | server-side inference time |
64
64
  | `degraded` | `True` when the call failed and this result was synthesized client-side |
65
+ | `notice` | why no model decided the call (cold start); `None` otherwise |
66
+ | `shadow` | cold start only: a `ShadowVerdict` with what the shared stock model would have decided. The call is still rejected, so treat it as a preview |
67
+
68
+ A new account starts in cold start: every call returns `approved=False` until a
69
+ Health Check shows the shared model is accurate enough on your data. Look at
70
+ `result.shadow` to see what it would have decided in the meantime:
71
+
72
+ ```python
73
+ result = cv.verify("when does my flight board", "Boarding starts 40 minutes before departure.")
74
+ if result.shadow:
75
+ print(result.notice)
76
+ print("stock model would approve:", result.shadow.approved, result.shadow.score)
77
+ ```
65
78
 
66
79
  ## On your request path
67
80
 
@@ -107,6 +120,30 @@ evaluator.report_feedback(query, answer, was_correct=False, similarity_score=0.9
107
120
 
108
121
  See [`examples/gptcache_example.py`](examples/gptcache_example.py).
109
122
 
123
+ ## LangChain
124
+
125
+ `VerifiedCache` wraps any LangChain cache, typically a semantic one, and only
126
+ serves a hit after CacheVerifier approves it; a rejected hit is a miss, so
127
+ your model is called as usual. For chat models it verifies against the last
128
+ user message.
129
+
130
+ ```bash
131
+ pip install "cacheverifier[langchain]"
132
+ ```
133
+
134
+ ```python
135
+ from langchain_core.globals import set_llm_cache
136
+ from langchain_redis import RedisSemanticCache
137
+ from cacheverifier.integrations.langchain import VerifiedCache
138
+
139
+ set_llm_cache(
140
+ VerifiedCache(
141
+ RedisSemanticCache(redis_url="redis://localhost:6379", embeddings=embeddings),
142
+ api_key="cv_...",
143
+ )
144
+ )
145
+ ```
146
+
110
147
  ## Fine-tuning
111
148
 
112
149
  Once you have ~20+ feedback rows (the service found fine-tuning is often a net negative
@@ -16,7 +16,7 @@ Docs: https://www.cacheverifier.com/docs
16
16
  Research behind it: https://github.com/imxinchengyou/CacheVerifier
17
17
  """
18
18
 
19
- from cacheverifier.client import CacheVerifier, CacheVerifierError, VerifyResult
19
+ from cacheverifier.client import CacheVerifier, CacheVerifierError, ShadowVerdict, VerifyResult
20
20
 
21
- __version__ = "0.4.1"
22
- __all__ = ["CacheVerifier", "CacheVerifierError", "VerifyResult", "__version__"]
21
+ __version__ = "0.5.0"
22
+ __all__ = ["CacheVerifier", "CacheVerifierError", "ShadowVerdict", "VerifyResult", "__version__"]
@@ -57,6 +57,32 @@ class CacheVerifierError(RuntimeError):
57
57
  self.retry_after = retry_after
58
58
 
59
59
 
60
+ @dataclass(frozen=True)
61
+ class ShadowVerdict:
62
+ """What the shared stock model would have decided for a cold-start call.
63
+
64
+ Only a preview: during cold start the call itself is always rejected
65
+ (`VerifyResult.approved` is False). The stock model hasn't been checked
66
+ against your data, so run a Health Check before relying on it.
67
+ """
68
+
69
+ approved: bool
70
+ score: float
71
+ threshold: float
72
+ latency_ms: float
73
+ model_version: str = "stock"
74
+
75
+ @classmethod
76
+ def _from_json(cls, d: dict[str, Any]) -> ShadowVerdict:
77
+ return cls(
78
+ approved=bool(d["approved"]),
79
+ score=float(d["score"]),
80
+ threshold=float(d["threshold"]),
81
+ latency_ms=float(d["latency_ms"]),
82
+ model_version=str(d.get("model_version", "stock")),
83
+ )
84
+
85
+
60
86
  @dataclass(frozen=True)
61
87
  class VerifyResult:
62
88
  """One `/v1/verify` decision.
@@ -74,6 +100,11 @@ class VerifyResult:
74
100
  5xx / rate-limited 429) and this result was synthesized by the client
75
101
  rather than returned by the API. `approved` then reflects the client's `fail_open` setting
76
102
  (default `False` -> `approved=False`, i.e. fall through to your LLM).
103
+ - `notice`: a plain-language reason when no model decided the call (cold
104
+ start); None otherwise.
105
+ - `shadow`: on a cold-start call, what the shared stock model would have
106
+ decided (a `ShadowVerdict`); None otherwise, and on servers that don't
107
+ send it.
77
108
  """
78
109
 
79
110
  approved: bool
@@ -82,15 +113,20 @@ class VerifyResult:
82
113
  model_version: str
83
114
  latency_ms: float
84
115
  degraded: bool = False
116
+ notice: str | None = None
117
+ shadow: ShadowVerdict | None = None
85
118
 
86
119
  @classmethod
87
120
  def _from_json(cls, d: dict[str, Any]) -> VerifyResult:
121
+ shadow = d.get("shadow")
88
122
  return cls(
89
123
  approved=bool(d["approved"]),
90
124
  score=float(d["score"]),
91
125
  threshold=float(d["threshold"]),
92
126
  model_version=str(d["model_version"]),
93
127
  latency_ms=float(d["latency_ms"]),
128
+ notice=d.get("notice"),
129
+ shadow=ShadowVerdict._from_json(shadow) if shadow else None,
94
130
  )
95
131
 
96
132
 
@@ -0,0 +1,140 @@
1
+ """LangChain integration: wrap any LangChain semantic cache so a hit is only
2
+ served after CacheVerifier approves it.
3
+
4
+ A semantic cache (`RedisSemanticCache`, `MongoDBAtlasSemanticCache`, ...)
5
+ returns a stored answer whenever a past prompt is similar enough to the new
6
+ one, including when the wording differs in a way that changes the answer.
7
+ `VerifiedCache` sits in front of it: on a hit it asks `/v1/verify` whether
8
+ the cached answer fits the new question, and turns a rejected hit into a
9
+ miss, so your model is called as usual.
10
+
11
+ from langchain_core.globals import set_llm_cache
12
+ from langchain_redis import RedisSemanticCache
13
+ from cacheverifier.integrations.langchain import VerifiedCache
14
+
15
+ set_llm_cache(
16
+ VerifiedCache(
17
+ RedisSemanticCache(redis_url="redis://localhost:6379", embeddings=embeddings),
18
+ api_key="cv_...",
19
+ )
20
+ )
21
+
22
+ Writes, clears and exact-match caches pass straight through. If the verify
23
+ call times out or the service is unreachable, the hit is rejected by
24
+ default (a verifier outage becomes a normal cache miss); pass
25
+ `fail_open=True` to serve it instead.
26
+
27
+ `langchain-core` is an optional dependency -- `pip install "cacheverifier[langchain]"`.
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import asyncio
33
+ import json
34
+ from typing import Any
35
+
36
+ from cacheverifier.client import (
37
+ DEFAULT_BASE_URL,
38
+ DEFAULT_TIMEOUT,
39
+ DEFAULT_VERIFY_TIMEOUT,
40
+ CacheVerifier,
41
+ )
42
+
43
+ try:
44
+ from langchain_core.caches import RETURN_VAL_TYPE, BaseCache
45
+ except ImportError as exc: # pragma: no cover - exercised only without the extra
46
+ raise ImportError(
47
+ 'cacheverifier.integrations.langchain needs langchain-core: pip install "cacheverifier[langchain]"'
48
+ ) from exc
49
+
50
+
51
+ def _message_text(content: Any) -> str:
52
+ if isinstance(content, str):
53
+ return content
54
+ if isinstance(content, list): # multimodal content blocks
55
+ return "\n".join(
56
+ block if isinstance(block, str) else str(block.get("text", ""))
57
+ for block in content
58
+ if isinstance(block, str) or (isinstance(block, dict) and block.get("type") == "text")
59
+ )
60
+ return ""
61
+
62
+
63
+ def user_question(prompt: str) -> str:
64
+ """The text to verify against for a LangChain cache key.
65
+
66
+ Plain LLMs use the prompt string itself as the key. Chat models use a
67
+ JSON dump of the whole message list, system prompt and history
68
+ included; the question being asked is the last human message, so that
69
+ is what the verifier sees. Anything unrecognized falls back to the raw
70
+ prompt."""
71
+ if not prompt.startswith("["):
72
+ return prompt
73
+ try:
74
+ messages = json.loads(prompt)
75
+ except ValueError:
76
+ return prompt
77
+ for message in reversed(messages if isinstance(messages, list) else []):
78
+ kwargs = message.get("kwargs", {}) if isinstance(message, dict) else {}
79
+ if kwargs.get("type") == "human":
80
+ text = _message_text(kwargs.get("content"))
81
+ if text:
82
+ return text
83
+ return prompt
84
+
85
+
86
+ class VerifiedCache(BaseCache):
87
+ """A LangChain cache that verifies another cache's hits before serving them."""
88
+
89
+ def __init__(
90
+ self,
91
+ cache: BaseCache,
92
+ api_key: str | None = None,
93
+ *,
94
+ client: CacheVerifier | None = None,
95
+ base_url: str = DEFAULT_BASE_URL,
96
+ timeout: float = DEFAULT_TIMEOUT,
97
+ verify_timeout: float = DEFAULT_VERIFY_TIMEOUT,
98
+ fail_open: bool = False,
99
+ ) -> None:
100
+ if client is None:
101
+ if not api_key:
102
+ raise ValueError("pass api_key or client")
103
+ client = CacheVerifier(
104
+ api_key=api_key,
105
+ base_url=base_url,
106
+ timeout=timeout,
107
+ verify_timeout=verify_timeout,
108
+ fail_open=fail_open,
109
+ )
110
+ self.cache = cache
111
+ self.client = client
112
+
113
+ def _approved(self, prompt: str, hit: RETURN_VAL_TYPE) -> bool:
114
+ answer = "\n".join(generation.text for generation in hit)
115
+ return self.client.verify(user_question(prompt), answer).approved
116
+
117
+ def lookup(self, prompt: str, llm_string: str) -> RETURN_VAL_TYPE | None:
118
+ hit = self.cache.lookup(prompt, llm_string)
119
+ if not hit:
120
+ return hit
121
+ return hit if self._approved(prompt, hit) else None
122
+
123
+ async def alookup(self, prompt: str, llm_string: str) -> RETURN_VAL_TYPE | None:
124
+ hit = await self.cache.alookup(prompt, llm_string)
125
+ if not hit:
126
+ return hit
127
+ approved = await asyncio.get_running_loop().run_in_executor(None, self._approved, prompt, hit)
128
+ return hit if approved else None
129
+
130
+ def update(self, prompt: str, llm_string: str, return_val: RETURN_VAL_TYPE) -> None:
131
+ self.cache.update(prompt, llm_string, return_val)
132
+
133
+ async def aupdate(self, prompt: str, llm_string: str, return_val: RETURN_VAL_TYPE) -> None:
134
+ await self.cache.aupdate(prompt, llm_string, return_val)
135
+
136
+ def clear(self, **kwargs: Any) -> None:
137
+ self.cache.clear(**kwargs)
138
+
139
+ async def aclear(self, **kwargs: Any) -> None:
140
+ await self.cache.aclear(**kwargs)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "cacheverifier"
7
- version = "0.4.1"
7
+ version = "0.5.0"
8
8
  description = "Python client for the hosted CacheVerifier semantic-cache verification API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -40,6 +40,7 @@ Research = "https://github.com/imxinchengyou/CacheVerifier"
40
40
 
41
41
  [project.optional-dependencies]
42
42
  gptcache = ["gptcache>=0.1.30"]
43
+ langchain = ["langchain-core>=0.3"]
43
44
  # `cacheverifier healthcheck` -- runs the hosted Health Check Report's
44
45
  # stock-vs-fine-tuned evaluation locally. Deliberately NOT a base
45
46
  # dependency: torch + sentence-transformers dwarf the httpx-only client.
@@ -54,7 +55,7 @@ healthcheck = [
54
55
  "torch>=2.2",
55
56
  "sentence-transformers>=3.0,<4",
56
57
  ]
57
- dev = ["pytest>=7", "mypy>=1.5", "ruff>=0.4"]
58
+ dev = ["pytest>=7", "mypy>=1.5", "ruff>=0.4", "langchain-core>=0.3"]
58
59
 
59
60
  [tool.hatch.build.targets.wheel]
60
61
  packages = ["cacheverifier"]
@@ -6,7 +6,7 @@ import json
6
6
  import httpx
7
7
  import pytest
8
8
 
9
- from cacheverifier import CacheVerifier, CacheVerifierError, VerifyResult
9
+ from cacheverifier import CacheVerifier, CacheVerifierError, ShadowVerdict, VerifyResult
10
10
 
11
11
 
12
12
  def make_client(handler, **kwargs):
@@ -51,6 +51,32 @@ def test_verify_parses_result_and_sends_api_key():
51
51
  assert seen["path"] == "/v1/verify"
52
52
  assert seen["api_key"] == "cv_test"
53
53
  assert seen["body"] == {"query": "how do I cancel", "candidate_answer": "Go to Settings > Billing."}
54
+ assert result.notice is None
55
+ assert result.shadow is None
56
+
57
+
58
+ def test_verify_parses_cold_start_notice_and_shadow_verdict():
59
+ def handler(request: httpx.Request) -> httpx.Response:
60
+ return httpx.Response(
61
+ 200,
62
+ json={
63
+ "approved": False,
64
+ "score": 0.0,
65
+ "latency_ms": 0.0,
66
+ "model_version": "cold_start_auto_pending",
67
+ "threshold": 0.0,
68
+ "decision_rule": "cold_start",
69
+ "notice": "No verifier has been checked against your data yet.",
70
+ "shadow": {"approved": True, "score": 2.1, "threshold": 0.0, "latency_ms": 18.0, "model_version": "stock"},
71
+ },
72
+ )
73
+
74
+ with make_client(handler) as cv:
75
+ result = cv.verify("when does my flight board", "Boarding starts 40 minutes before departure.")
76
+
77
+ assert result.approved is False
78
+ assert result.notice.startswith("No verifier")
79
+ assert result.shadow == ShadowVerdict(approved=True, score=2.1, threshold=0.0, latency_ms=18.0)
54
80
 
55
81
 
56
82
  def test_verify_batch_returns_ordered_results():
@@ -0,0 +1,90 @@
1
+ """VerifiedCache against a real langchain-core cache and chat model, with
2
+ every verify call served by an httpx.MockTransport."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import asyncio
7
+ import json
8
+
9
+ import httpx
10
+ import pytest
11
+
12
+ pytest.importorskip("langchain_core")
13
+
14
+ from langchain_core.caches import InMemoryCache
15
+ from langchain_core.language_models.fake_chat_models import FakeListChatModel
16
+ from langchain_core.messages import HumanMessage, SystemMessage
17
+ from langchain_core.outputs import Generation
18
+
19
+ from cacheverifier import CacheVerifier
20
+ from cacheverifier.integrations.langchain import VerifiedCache, user_question
21
+
22
+
23
+ def _verified(approve: bool, seen: list | None = None, inner=None) -> VerifiedCache:
24
+ def handler(request: httpx.Request) -> httpx.Response:
25
+ if seen is not None:
26
+ seen.append(json.loads(request.content))
27
+ return httpx.Response(
28
+ 200,
29
+ json={"approved": approve, "score": 1.0, "latency_ms": 5.0, "model_version": "stock", "threshold": 0.0},
30
+ )
31
+
32
+ client = CacheVerifier(api_key="cv_test", transport=httpx.MockTransport(handler))
33
+ return VerifiedCache(inner or InMemoryCache(), client=client)
34
+
35
+
36
+ def test_approved_hit_is_served_and_rejected_hit_becomes_a_miss():
37
+ seen: list = []
38
+ cache = _verified(True, seen)
39
+ cache.update("when does my flight board", "llm", [Generation(text="40 minutes before departure.")])
40
+ assert cache.lookup("when does my flight board", "llm")[0].text == "40 minutes before departure."
41
+ assert seen == [{"query": "when does my flight board", "candidate_answer": "40 minutes before departure."}]
42
+
43
+ rejecting = _verified(False, inner=cache.cache)
44
+ assert rejecting.lookup("when does my flight board", "llm") is None
45
+
46
+
47
+ def test_a_miss_never_calls_the_verifier():
48
+ seen: list = []
49
+ cache = _verified(True, seen)
50
+ assert cache.lookup("never cached", "llm") is None
51
+ assert seen == []
52
+
53
+
54
+ def test_chat_model_verifies_against_the_last_human_message():
55
+ seen: list = []
56
+ cache = _verified(True, seen)
57
+ model = FakeListChatModel(responses=["Gate B12, boarding at 9:20.", "should not be called"], cache=cache)
58
+ messages = [SystemMessage("You are an airline assistant."), HumanMessage("which gate is my flight")]
59
+
60
+ assert model.invoke(messages).content == "Gate B12, boarding at 9:20." # miss: model called, then cached
61
+ assert model.invoke(messages).content == "Gate B12, boarding at 9:20." # hit: verified, served
62
+ assert seen == [{"query": "which gate is my flight", "candidate_answer": "Gate B12, boarding at 9:20."}]
63
+
64
+
65
+ def test_rejected_chat_hit_falls_through_to_the_model():
66
+ cache = _verified(False)
67
+ model = FakeListChatModel(responses=["first answer", "fresh answer"], cache=cache)
68
+ assert model.invoke("what is the baggage allowance").content == "first answer"
69
+ assert model.invoke("what is the baggage allowance").content == "fresh answer"
70
+
71
+
72
+ def test_async_lookup_verifies_too():
73
+ cache = _verified(False)
74
+ asyncio.run(cache.aupdate("q", "llm", [Generation(text="a")]))
75
+ assert asyncio.run(cache.alookup("q", "llm")) is None
76
+ assert cache.cache.lookup("q", "llm")[0].text == "a" # the inner cache still has it
77
+
78
+
79
+ def test_user_question_falls_back_to_the_raw_prompt():
80
+ assert user_question("plain prompt") == "plain prompt"
81
+ assert user_question("[not json") == "[not json"
82
+ multimodal = json.dumps(
83
+ [{"kwargs": {"type": "human", "content": [{"type": "text", "text": "what's in this photo"}, {"type": "image_url"}]}}]
84
+ )
85
+ assert user_question(multimodal) == "what's in this photo"
86
+
87
+
88
+ def test_needs_a_key_or_a_client():
89
+ with pytest.raises(ValueError):
90
+ VerifiedCache(InMemoryCache())
File without changes
File without changes