longcache 0.1.0__py3-none-any.whl
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.
- longcache/__init__.py +11 -0
- longcache/backends/__init__.py +21 -0
- longcache/backends/async_redis.py +274 -0
- longcache/backends/base.py +167 -0
- longcache/backends/memory.py +243 -0
- longcache/backends/redis.py +374 -0
- longcache/backends/sqlite.py +521 -0
- longcache/cli.py +216 -0
- longcache/config.py +127 -0
- longcache/core/__init__.py +8 -0
- longcache/core/base_embedder.py +43 -0
- longcache/core/cache.py +872 -0
- longcache/core/embedder.py +155 -0
- longcache/core/entry.py +122 -0
- longcache/core/index.py +272 -0
- longcache/integrations/__init__.py +32 -0
- longcache/integrations/langchain.py +316 -0
- longcache/integrations/langgraph.py +150 -0
- longcache/integrations/llamaindex.py +103 -0
- longcache/integrations/openai.py +146 -0
- longcache/middleware.py +366 -0
- longcache-0.1.0.dist-info/METADATA +942 -0
- longcache-0.1.0.dist-info/RECORD +26 -0
- longcache-0.1.0.dist-info/WHEEL +4 -0
- longcache-0.1.0.dist-info/entry_points.txt +2 -0
- longcache-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LangChain integration for LongCache.
|
|
3
|
+
|
|
4
|
+
Provides CachedLLM and CachedChain wrappers that add semantic caching
|
|
5
|
+
to any LangChain LLM or LCEL chain with zero code changes.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import re
|
|
12
|
+
from typing import Any, Optional, Union
|
|
13
|
+
|
|
14
|
+
from longcache import LongCache, CacheConfig
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CachedLLM:
|
|
18
|
+
"""
|
|
19
|
+
Wraps any LangChain LLM/ChatModel with semantic caching.
|
|
20
|
+
|
|
21
|
+
Drop-in replacement that preserves the full LLM interface.
|
|
22
|
+
Cache is checked before the LLM is called, and new responses
|
|
23
|
+
are cached automatically.
|
|
24
|
+
|
|
25
|
+
Usage:
|
|
26
|
+
from langchain_openai import ChatOpenAI
|
|
27
|
+
from longcache.integrations.langchain import CachedLLM
|
|
28
|
+
|
|
29
|
+
llm = CachedLLM(ChatOpenAI(model="gpt-4o"), cache=LongCache())
|
|
30
|
+
response = llm.invoke("What is the refund policy?") # same API
|
|
31
|
+
# Second call with similar query returns cached response instantly
|
|
32
|
+
response2 = llm.invoke("How do I get a refund?") # cache HIT
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
llm: Any,
|
|
38
|
+
cache: Optional[LongCache] = None,
|
|
39
|
+
config: Optional[CacheConfig] = None,
|
|
40
|
+
namespace: str = "langchain",
|
|
41
|
+
):
|
|
42
|
+
"""
|
|
43
|
+
Initialize CachedLLM.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
llm: Any LangChain BaseLLM or BaseChatModel instance.
|
|
47
|
+
cache: Existing LongCache instance. Creates a new one if not provided.
|
|
48
|
+
config: CacheConfig for the new cache (ignored if cache is provided).
|
|
49
|
+
namespace: Default namespace for cache entries.
|
|
50
|
+
"""
|
|
51
|
+
self.llm = llm
|
|
52
|
+
self.cache = cache or LongCache(config or CacheConfig(namespace=namespace))
|
|
53
|
+
self._namespace = namespace
|
|
54
|
+
|
|
55
|
+
def _extract_query(self, input: Any) -> str:
|
|
56
|
+
"""
|
|
57
|
+
Extract a query string from various LangChain input formats.
|
|
58
|
+
|
|
59
|
+
Supports:
|
|
60
|
+
- str: used directly
|
|
61
|
+
- list of BaseMessage: extracts last HumanMessage content
|
|
62
|
+
- dict with 'query', 'question', 'input', or 'messages' key
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
input: LangChain input in any supported format.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Extracted query string.
|
|
69
|
+
"""
|
|
70
|
+
if isinstance(input, str):
|
|
71
|
+
return input
|
|
72
|
+
|
|
73
|
+
if isinstance(input, list):
|
|
74
|
+
# Try to extract from message list
|
|
75
|
+
for msg in reversed(input):
|
|
76
|
+
if hasattr(msg, "content") and hasattr(msg, "type"):
|
|
77
|
+
if msg.type == "human":
|
|
78
|
+
return msg.content
|
|
79
|
+
# Fallback: use last message with content
|
|
80
|
+
if hasattr(msg, "content"):
|
|
81
|
+
return msg.content
|
|
82
|
+
# Last resort: stringify
|
|
83
|
+
return str(input[-1]) if input else ""
|
|
84
|
+
|
|
85
|
+
if isinstance(input, dict):
|
|
86
|
+
for key in ("query", "question", "input", "messages", "prompt"):
|
|
87
|
+
if key in input:
|
|
88
|
+
value = input[key]
|
|
89
|
+
if isinstance(value, str):
|
|
90
|
+
return value
|
|
91
|
+
if isinstance(value, list):
|
|
92
|
+
return self._extract_query(value)
|
|
93
|
+
return str(value)
|
|
94
|
+
|
|
95
|
+
return str(input)
|
|
96
|
+
|
|
97
|
+
def invoke(self, input: Any, *args, **kwargs) -> Any:
|
|
98
|
+
"""
|
|
99
|
+
Invoke the LLM with caching.
|
|
100
|
+
|
|
101
|
+
Checks the cache first. On a HIT, returns the cached response.
|
|
102
|
+
On a MISS, calls the LLM and caches the result.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
input: The LLM input (string, messages list, or dict).
|
|
106
|
+
*args: Additional positional arguments passed to the LLM.
|
|
107
|
+
**kwargs: Additional keyword arguments passed to the LLM.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
The LLM response (from cache or fresh invocation).
|
|
111
|
+
"""
|
|
112
|
+
query_key = self._extract_query(input)
|
|
113
|
+
cached = self.cache.get(query_key, namespace=self._namespace)
|
|
114
|
+
if cached is not None:
|
|
115
|
+
return cached
|
|
116
|
+
|
|
117
|
+
result = self.llm.invoke(input, *args, **kwargs)
|
|
118
|
+
self.cache.set(query_key, result, namespace=self._namespace)
|
|
119
|
+
return result
|
|
120
|
+
|
|
121
|
+
async def ainvoke(self, input: Any, *args, **kwargs) -> Any:
|
|
122
|
+
"""
|
|
123
|
+
Async invoke the LLM with caching.
|
|
124
|
+
|
|
125
|
+
Same as invoke() but uses the async LLM interface.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
input: The LLM input.
|
|
129
|
+
*args: Additional positional arguments.
|
|
130
|
+
**kwargs: Additional keyword arguments.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
The LLM response (from cache or fresh invocation).
|
|
134
|
+
"""
|
|
135
|
+
query_key = self._extract_query(input)
|
|
136
|
+
cached = await self.cache.aget(query_key, namespace=self._namespace)
|
|
137
|
+
if cached is not None:
|
|
138
|
+
return cached
|
|
139
|
+
|
|
140
|
+
result = await self.llm.ainvoke(input, *args, **kwargs)
|
|
141
|
+
await self.cache.aset(query_key, result, namespace=self._namespace)
|
|
142
|
+
return result
|
|
143
|
+
|
|
144
|
+
def stream(self, input: Any, *args, **kwargs):
|
|
145
|
+
"""
|
|
146
|
+
Stream the LLM response with caching.
|
|
147
|
+
|
|
148
|
+
On a cache HIT, replays the cached response token-by-token.
|
|
149
|
+
On a MISS, streams from the LLM, collects the response, and caches it.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
input: The LLM input.
|
|
153
|
+
*args: Additional positional arguments.
|
|
154
|
+
**kwargs: Additional keyword arguments.
|
|
155
|
+
|
|
156
|
+
Yields:
|
|
157
|
+
Response chunks (from cache replay or live LLM stream).
|
|
158
|
+
"""
|
|
159
|
+
query_key = self._extract_query(input)
|
|
160
|
+
cached = self.cache.get(query_key, namespace=self._namespace)
|
|
161
|
+
|
|
162
|
+
if cached is not None:
|
|
163
|
+
# Replay cached response preserving original whitespace
|
|
164
|
+
cached_str = str(cached)
|
|
165
|
+
tokens = re.findall(r"\S+\s*", cached_str)
|
|
166
|
+
for token in tokens:
|
|
167
|
+
yield token
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
# Stream from LLM and collect
|
|
171
|
+
collected_chunks = []
|
|
172
|
+
for chunk in self.llm.stream(input, *args, **kwargs):
|
|
173
|
+
collected_chunks.append(chunk)
|
|
174
|
+
yield chunk
|
|
175
|
+
|
|
176
|
+
# Cache the assembled response
|
|
177
|
+
self.cache.set(query_key, collected_chunks, namespace=self._namespace)
|
|
178
|
+
|
|
179
|
+
async def astream(self, input: Any, *args, **kwargs):
|
|
180
|
+
"""
|
|
181
|
+
Async stream the LLM response with caching.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
input: The LLM input.
|
|
185
|
+
*args: Additional positional arguments.
|
|
186
|
+
**kwargs: Additional keyword arguments.
|
|
187
|
+
|
|
188
|
+
Yields:
|
|
189
|
+
Response chunks.
|
|
190
|
+
"""
|
|
191
|
+
query_key = self._extract_query(input)
|
|
192
|
+
cached = await self.cache.aget(query_key, namespace=self._namespace)
|
|
193
|
+
|
|
194
|
+
if cached is not None:
|
|
195
|
+
cached_str = str(cached)
|
|
196
|
+
tokens = re.findall(r"\S+\s*", cached_str)
|
|
197
|
+
for token in tokens:
|
|
198
|
+
yield token
|
|
199
|
+
return
|
|
200
|
+
|
|
201
|
+
collected_chunks = []
|
|
202
|
+
async for chunk in self.llm.astream(input, *args, **kwargs):
|
|
203
|
+
collected_chunks.append(chunk)
|
|
204
|
+
yield chunk
|
|
205
|
+
|
|
206
|
+
await self.cache.aset(query_key, collected_chunks, namespace=self._namespace)
|
|
207
|
+
|
|
208
|
+
def __getattr__(self, name: str) -> Any:
|
|
209
|
+
"""Proxy all other attributes to the underlying LLM."""
|
|
210
|
+
return getattr(self.llm, name)
|
|
211
|
+
|
|
212
|
+
def __repr__(self) -> str:
|
|
213
|
+
return f"CachedLLM(llm={self.llm!r}, cache={self.cache!r})"
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class CachedChain:
|
|
217
|
+
"""
|
|
218
|
+
Wraps any LCEL Runnable chain with semantic caching.
|
|
219
|
+
|
|
220
|
+
Cache key is the final user query string extracted from the chain input.
|
|
221
|
+
Works with any LCEL chain: prompt | llm | parser, RetrievalQA, etc.
|
|
222
|
+
|
|
223
|
+
Usage:
|
|
224
|
+
from langchain_openai import ChatOpenAI
|
|
225
|
+
from longcache.integrations.langchain import CachedChain
|
|
226
|
+
|
|
227
|
+
chain = prompt | ChatOpenAI() | StrOutputParser()
|
|
228
|
+
cached_chain = CachedChain(chain, cache=LongCache())
|
|
229
|
+
result = cached_chain.invoke({"question": "What is RAG?"})
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
def __init__(
|
|
233
|
+
self,
|
|
234
|
+
chain: Any,
|
|
235
|
+
cache: Optional[LongCache] = None,
|
|
236
|
+
config: Optional[CacheConfig] = None,
|
|
237
|
+
query_key: str = "question",
|
|
238
|
+
namespace: str = "langchain_chain",
|
|
239
|
+
):
|
|
240
|
+
"""
|
|
241
|
+
Initialize CachedChain.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
chain: Any LCEL Runnable chain.
|
|
245
|
+
cache: Existing LongCache instance. Creates a new one if not provided.
|
|
246
|
+
config: CacheConfig for the new cache (ignored if cache is provided).
|
|
247
|
+
query_key: Key to extract from input dict as the cache query.
|
|
248
|
+
namespace: Default namespace for cache entries.
|
|
249
|
+
"""
|
|
250
|
+
self.chain = chain
|
|
251
|
+
self.cache = cache or LongCache(config or CacheConfig(namespace=namespace))
|
|
252
|
+
self.query_key = query_key
|
|
253
|
+
self._namespace = namespace
|
|
254
|
+
|
|
255
|
+
def _extract_query(self, input: Any) -> str:
|
|
256
|
+
"""
|
|
257
|
+
Extract the query string from chain input.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
input: Chain input (dict or string).
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
The extracted query string.
|
|
264
|
+
"""
|
|
265
|
+
if isinstance(input, dict):
|
|
266
|
+
return input.get(self.query_key, str(input))
|
|
267
|
+
return str(input)
|
|
268
|
+
|
|
269
|
+
def invoke(self, input: Any, *args, **kwargs) -> Any:
|
|
270
|
+
"""
|
|
271
|
+
Invoke the chain with caching.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
input: Chain input (typically a dict with the query key).
|
|
275
|
+
*args: Additional positional arguments.
|
|
276
|
+
**kwargs: Additional keyword arguments.
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
The chain output (from cache or fresh invocation).
|
|
280
|
+
"""
|
|
281
|
+
query = self._extract_query(input)
|
|
282
|
+
cached = self.cache.get(query, namespace=self._namespace)
|
|
283
|
+
if cached is not None:
|
|
284
|
+
return cached
|
|
285
|
+
|
|
286
|
+
result = self.chain.invoke(input, *args, **kwargs)
|
|
287
|
+
self.cache.set(query, result, namespace=self._namespace)
|
|
288
|
+
return result
|
|
289
|
+
|
|
290
|
+
async def ainvoke(self, input: Any, *args, **kwargs) -> Any:
|
|
291
|
+
"""
|
|
292
|
+
Async invoke the chain with caching.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
input: Chain input.
|
|
296
|
+
*args: Additional positional arguments.
|
|
297
|
+
**kwargs: Additional keyword arguments.
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
The chain output (from cache or fresh invocation).
|
|
301
|
+
"""
|
|
302
|
+
query = self._extract_query(input)
|
|
303
|
+
cached = await self.cache.aget(query, namespace=self._namespace)
|
|
304
|
+
if cached is not None:
|
|
305
|
+
return cached
|
|
306
|
+
|
|
307
|
+
result = await self.chain.ainvoke(input, *args, **kwargs)
|
|
308
|
+
await self.cache.aset(query, result, namespace=self._namespace)
|
|
309
|
+
return result
|
|
310
|
+
|
|
311
|
+
def __getattr__(self, name: str) -> Any:
|
|
312
|
+
"""Proxy all other attributes to the underlying chain."""
|
|
313
|
+
return getattr(self.chain, name)
|
|
314
|
+
|
|
315
|
+
def __repr__(self) -> str:
|
|
316
|
+
return f"CachedChain(chain={self.chain!r}, cache={self.cache!r})"
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LangGraph integration for LongCache.
|
|
3
|
+
|
|
4
|
+
Provides CachedState wrapper for semantic caching of agent/graph states.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
from typing import Any, Optional
|
|
11
|
+
|
|
12
|
+
from longcache import LongCache, CacheConfig
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CachedState:
|
|
16
|
+
"""
|
|
17
|
+
Wraps LangGraph state evaluation with semantic caching.
|
|
18
|
+
|
|
19
|
+
Can be used as a node in a LangGraph graph to check the semantic cache
|
|
20
|
+
before executing expensive LLM nodes.
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
from longcache.integrations.langgraph import CachedState
|
|
24
|
+
from longcache import LongCache
|
|
25
|
+
|
|
26
|
+
cache = LongCache()
|
|
27
|
+
|
|
28
|
+
def cache_node(state: dict):
|
|
29
|
+
# Check cache based on the latest message/input
|
|
30
|
+
return CachedState.check(state, cache, query_key="messages")
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def check(
|
|
35
|
+
state: dict,
|
|
36
|
+
cache: LongCache,
|
|
37
|
+
query_key: str = "messages",
|
|
38
|
+
namespace: str = "langgraph",
|
|
39
|
+
) -> dict:
|
|
40
|
+
"""
|
|
41
|
+
Check the cache for a given state.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
state: The LangGraph state dictionary.
|
|
45
|
+
cache: LongCache instance.
|
|
46
|
+
query_key: The key in the state to use as the cache query.
|
|
47
|
+
If it's a list of messages, the last human message is used.
|
|
48
|
+
namespace: The cache namespace.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
A dictionary with cache results. If a hit is found, it can be
|
|
52
|
+
merged back into the state to skip LLM execution.
|
|
53
|
+
"""
|
|
54
|
+
query_input = state.get(query_key)
|
|
55
|
+
|
|
56
|
+
# Extract string query
|
|
57
|
+
query = ""
|
|
58
|
+
if isinstance(query_input, str):
|
|
59
|
+
query = query_input
|
|
60
|
+
elif isinstance(query_input, list):
|
|
61
|
+
for msg in reversed(query_input):
|
|
62
|
+
if hasattr(msg, "content") and getattr(msg, "type", "") == "human":
|
|
63
|
+
query = msg.content
|
|
64
|
+
break
|
|
65
|
+
elif hasattr(msg, "content"):
|
|
66
|
+
query = msg.content
|
|
67
|
+
if not query and query_input:
|
|
68
|
+
query = str(query_input[-1])
|
|
69
|
+
elif isinstance(query_input, dict):
|
|
70
|
+
query = query_input.get("query", str(query_input))
|
|
71
|
+
else:
|
|
72
|
+
query = str(query_input)
|
|
73
|
+
|
|
74
|
+
if not query:
|
|
75
|
+
return {"__cache_hit": False}
|
|
76
|
+
|
|
77
|
+
cached = cache.get(query, namespace=namespace)
|
|
78
|
+
if cached is not None:
|
|
79
|
+
return {"__cache_hit": True, "cached_response": cached}
|
|
80
|
+
|
|
81
|
+
return {"__cache_hit": False, "cached_query": query}
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
async def acheck(
|
|
85
|
+
state: dict,
|
|
86
|
+
cache: LongCache,
|
|
87
|
+
query_key: str = "messages",
|
|
88
|
+
namespace: str = "langgraph",
|
|
89
|
+
) -> dict:
|
|
90
|
+
"""Async version of check()."""
|
|
91
|
+
query_input = state.get(query_key)
|
|
92
|
+
|
|
93
|
+
# Extract string query
|
|
94
|
+
query = ""
|
|
95
|
+
if isinstance(query_input, str):
|
|
96
|
+
query = query_input
|
|
97
|
+
elif isinstance(query_input, list):
|
|
98
|
+
for msg in reversed(query_input):
|
|
99
|
+
if hasattr(msg, "content") and getattr(msg, "type", "") == "human":
|
|
100
|
+
query = msg.content
|
|
101
|
+
break
|
|
102
|
+
elif hasattr(msg, "content"):
|
|
103
|
+
query = msg.content
|
|
104
|
+
if not query and query_input:
|
|
105
|
+
query = str(query_input[-1])
|
|
106
|
+
elif isinstance(query_input, dict):
|
|
107
|
+
query = query_input.get("query", str(query_input))
|
|
108
|
+
else:
|
|
109
|
+
query = str(query_input)
|
|
110
|
+
|
|
111
|
+
if not query:
|
|
112
|
+
return {"__cache_hit": False}
|
|
113
|
+
|
|
114
|
+
cached = await cache.aget(query, namespace=namespace)
|
|
115
|
+
if cached is not None:
|
|
116
|
+
return {"__cache_hit": True, "cached_response": cached}
|
|
117
|
+
|
|
118
|
+
return {"__cache_hit": False, "cached_query": query}
|
|
119
|
+
|
|
120
|
+
@staticmethod
|
|
121
|
+
def save(
|
|
122
|
+
state: dict,
|
|
123
|
+
response: Any,
|
|
124
|
+
cache: LongCache,
|
|
125
|
+
namespace: str = "langgraph",
|
|
126
|
+
) -> None:
|
|
127
|
+
"""
|
|
128
|
+
Save a new response to the cache.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
state: The state dictionary (must contain 'cached_query' from check).
|
|
132
|
+
response: The response to cache.
|
|
133
|
+
cache: LongCache instance.
|
|
134
|
+
namespace: The cache namespace.
|
|
135
|
+
"""
|
|
136
|
+
query = state.get("cached_query")
|
|
137
|
+
if query:
|
|
138
|
+
cache.set(query, response, namespace=namespace)
|
|
139
|
+
|
|
140
|
+
@staticmethod
|
|
141
|
+
async def asave(
|
|
142
|
+
state: dict,
|
|
143
|
+
response: Any,
|
|
144
|
+
cache: LongCache,
|
|
145
|
+
namespace: str = "langgraph",
|
|
146
|
+
) -> None:
|
|
147
|
+
"""Async version of save()."""
|
|
148
|
+
query = state.get("cached_query")
|
|
149
|
+
if query:
|
|
150
|
+
await cache.aset(query, response, namespace=namespace)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LlamaIndex integration for LongCache.
|
|
3
|
+
|
|
4
|
+
Provides CachedQueryEngine that wraps any LlamaIndex query engine
|
|
5
|
+
with semantic caching.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any, Optional
|
|
11
|
+
|
|
12
|
+
from longcache import LongCache, CacheConfig
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CachedQueryEngine:
|
|
16
|
+
"""
|
|
17
|
+
Wraps a LlamaIndex query engine with semantic caching.
|
|
18
|
+
|
|
19
|
+
Intercepts queries before they reach the LlamaIndex retrieval
|
|
20
|
+
and generation pipeline. On cache HIT, returns the cached
|
|
21
|
+
response instantly.
|
|
22
|
+
|
|
23
|
+
Requires the 'llamaindex' extra: uv pip install longcache[llamaindex]
|
|
24
|
+
|
|
25
|
+
Usage:
|
|
26
|
+
from llama_index.core import VectorStoreIndex
|
|
27
|
+
from longcache.integrations.llamaindex import CachedQueryEngine
|
|
28
|
+
|
|
29
|
+
index = VectorStoreIndex.from_documents(documents)
|
|
30
|
+
engine = index.as_query_engine()
|
|
31
|
+
|
|
32
|
+
cached_engine = CachedQueryEngine(engine, cache=LongCache())
|
|
33
|
+
response = cached_engine.query("What is RAG?")
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
query_engine: Any,
|
|
39
|
+
cache: Optional[LongCache] = None,
|
|
40
|
+
config: Optional[CacheConfig] = None,
|
|
41
|
+
namespace: str = "llamaindex",
|
|
42
|
+
):
|
|
43
|
+
"""
|
|
44
|
+
Initialize CachedQueryEngine.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
query_engine: A LlamaIndex query engine instance.
|
|
48
|
+
cache: Existing LongCache instance. Creates a new one if not provided.
|
|
49
|
+
config: CacheConfig for the new cache (ignored if cache is provided).
|
|
50
|
+
namespace: Default namespace for cache entries.
|
|
51
|
+
"""
|
|
52
|
+
self.query_engine = query_engine
|
|
53
|
+
self.cache = cache or LongCache(config or CacheConfig(namespace=namespace))
|
|
54
|
+
self._namespace = namespace
|
|
55
|
+
|
|
56
|
+
def query(self, query_str: str, **kwargs) -> Any:
|
|
57
|
+
"""
|
|
58
|
+
Query the engine with caching.
|
|
59
|
+
|
|
60
|
+
Checks the cache first using semantic similarity. On a HIT,
|
|
61
|
+
returns the cached response. On a MISS, runs the full query
|
|
62
|
+
pipeline and caches the result.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
query_str: The query string.
|
|
66
|
+
**kwargs: Additional keyword arguments passed to the query engine.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
The query response (from cache or fresh invocation).
|
|
70
|
+
"""
|
|
71
|
+
cached = self.cache.get(query_str, namespace=self._namespace)
|
|
72
|
+
if cached is not None:
|
|
73
|
+
return cached
|
|
74
|
+
|
|
75
|
+
result = self.query_engine.query(query_str, **kwargs)
|
|
76
|
+
self.cache.set(query_str, result, namespace=self._namespace)
|
|
77
|
+
return result
|
|
78
|
+
|
|
79
|
+
async def aquery(self, query_str: str, **kwargs) -> Any:
|
|
80
|
+
"""
|
|
81
|
+
Async query the engine with caching.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
query_str: The query string.
|
|
85
|
+
**kwargs: Additional keyword arguments.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
The query response (from cache or fresh invocation).
|
|
89
|
+
"""
|
|
90
|
+
cached = self.cache.get(query_str, namespace=self._namespace)
|
|
91
|
+
if cached is not None:
|
|
92
|
+
return cached
|
|
93
|
+
|
|
94
|
+
result = await self.query_engine.aquery(query_str, **kwargs)
|
|
95
|
+
self.cache.set(query_str, result, namespace=self._namespace)
|
|
96
|
+
return result
|
|
97
|
+
|
|
98
|
+
def __getattr__(self, name: str) -> Any:
|
|
99
|
+
"""Proxy all other attributes to the underlying query engine."""
|
|
100
|
+
return getattr(self.query_engine, name)
|
|
101
|
+
|
|
102
|
+
def __repr__(self) -> str:
|
|
103
|
+
return f"CachedQueryEngine(engine={self.query_engine!r}, cache={self.cache!r})"
|