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.
@@ -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})"