adeyemi001 commited on
Commit
a6ebcd7
·
verified ·
1 Parent(s): 768c56e

Update backend/app/utils/cache.py

Browse files
Files changed (1) hide show
  1. backend/app/utils/cache.py +446 -392
backend/app/utils/cache.py CHANGED
@@ -1,393 +1,447 @@
1
- """Caching system for embeddings, queries, and responses."""
2
- import hashlib
3
- import time
4
- from typing import Optional, Dict, Any, List
5
- from datetime import datetime, timedelta
6
- import json
7
-
8
-
9
- class CacheEntry:
10
- """Represents a cached item with expiration."""
11
-
12
- def __init__(self, value: Any, ttl: int = 3600):
13
- """
14
- Initialize cache entry.
15
-
16
- Args:
17
- value: Value to cache
18
- ttl: Time to live in seconds (default: 1 hour)
19
- """
20
- self.value = value
21
- self.created_at = time.time()
22
- self.ttl = ttl
23
- self.hit_count = 0
24
-
25
- def is_expired(self) -> bool:
26
- """Check if entry has expired."""
27
- return time.time() - self.created_at > self.ttl
28
-
29
- def increment_hits(self):
30
- """Increment hit counter."""
31
- self.hit_count += 1
32
-
33
-
34
- class EmbeddingCache:
35
- """Cache for query embeddings to avoid re-computing."""
36
-
37
- def __init__(self, max_size: int = 1000, ttl: int = 86400):
38
- """
39
- Initialize embedding cache.
40
-
41
- Args:
42
- max_size: Maximum number of entries (default: 1000)
43
- ttl: Time to live in seconds (default: 24 hours)
44
- """
45
- self.cache: Dict[str, CacheEntry] = {}
46
- self.max_size = max_size
47
- self.ttl = ttl
48
- self.hits = 0
49
- self.misses = 0
50
-
51
- def _generate_key(self, text: str) -> str:
52
- """Generate cache key from text."""
53
- return hashlib.md5(text.lower().strip().encode()).hexdigest()
54
-
55
- def get(self, text: str) -> Optional[List[float]]:
56
- """
57
- Get cached embedding.
58
-
59
- Args:
60
- text: Query text
61
-
62
- Returns:
63
- Cached embedding vector or None
64
- """
65
- key = self._generate_key(text)
66
-
67
- if key in self.cache:
68
- entry = self.cache[key]
69
- if not entry.is_expired():
70
- entry.increment_hits()
71
- self.hits += 1
72
- return entry.value
73
- else:
74
- # Remove expired entry
75
- del self.cache[key]
76
-
77
- self.misses += 1
78
- return None
79
-
80
- def set(self, text: str, embedding: List[float]):
81
- """
82
- Cache an embedding.
83
-
84
- Args:
85
- text: Query text
86
- embedding: Embedding vector
87
- """
88
- key = self._generate_key(text)
89
-
90
- # If cache is full, remove oldest entries
91
- if len(self.cache) >= self.max_size:
92
- self._evict_oldest()
93
-
94
- self.cache[key] = CacheEntry(embedding, ttl=self.ttl)
95
-
96
- def _evict_oldest(self):
97
- """Remove oldest 10% of entries."""
98
- num_to_remove = max(1, self.max_size // 10)
99
-
100
- # Sort by creation time and remove oldest
101
- sorted_keys = sorted(
102
- self.cache.keys(),
103
- key=lambda k: self.cache[k].created_at
104
- )
105
-
106
- for key in sorted_keys[:num_to_remove]:
107
- del self.cache[key]
108
-
109
- def clear(self):
110
- """Clear all cached embeddings."""
111
- self.cache.clear()
112
- self.hits = 0
113
- self.misses = 0
114
-
115
- def get_stats(self) -> Dict[str, Any]:
116
- """Get cache statistics."""
117
- total_requests = self.hits + self.misses
118
- hit_rate = (self.hits / total_requests * 100) if total_requests > 0 else 0
119
-
120
- return {
121
- "size": len(self.cache),
122
- "max_size": self.max_size,
123
- "hits": self.hits,
124
- "misses": self.misses,
125
- "hit_rate": round(hit_rate, 2),
126
- "total_requests": total_requests
127
- }
128
-
129
-
130
- class QueryResponseCache:
131
- """Cache for complete query responses."""
132
-
133
- def __init__(self, max_size: int = 500, ttl: int = 3600):
134
- """
135
- Initialize response cache.
136
-
137
- Args:
138
- max_size: Maximum number of entries (default: 500)
139
- ttl: Time to live in seconds (default: 1 hour)
140
- """
141
- self.cache: Dict[str, CacheEntry] = {}
142
- self.max_size = max_size
143
- self.ttl = ttl
144
- self.hits = 0
145
- self.misses = 0
146
-
147
- def _generate_key(
148
- self,
149
- query: str,
150
- ticker: Optional[str] = None,
151
- doc_types: Optional[List[str]] = None,
152
- top_k: int = 10
153
- ) -> str:
154
- """Generate cache key from query parameters."""
155
- # Normalize inputs
156
- query_normalized = query.lower().strip()
157
- ticker_normalized = ticker.lower() if ticker else ""
158
- doc_types_normalized = sorted(doc_types) if doc_types else []
159
-
160
- # Create key string
161
- key_parts = [
162
- query_normalized,
163
- ticker_normalized,
164
- ",".join(doc_types_normalized),
165
- str(top_k)
166
- ]
167
- key_string = "|".join(key_parts)
168
-
169
- return hashlib.md5(key_string.encode()).hexdigest()
170
-
171
- def get(
172
- self,
173
- query: str,
174
- ticker: Optional[str] = None,
175
- doc_types: Optional[List[str]] = None,
176
- top_k: int = 10
177
- ) -> Optional[Dict[str, Any]]:
178
- """
179
- Get cached response.
180
-
181
- Args:
182
- query: Query text
183
- ticker: Ticker filter
184
- doc_types: Document type filters
185
- top_k: Number of results
186
-
187
- Returns:
188
- Cached response or None
189
- """
190
- key = self._generate_key(query, ticker, doc_types, top_k)
191
-
192
- if key in self.cache:
193
- entry = self.cache[key]
194
- if not entry.is_expired():
195
- entry.increment_hits()
196
- self.hits += 1
197
- return entry.value
198
- else:
199
- del self.cache[key]
200
-
201
- self.misses += 1
202
- return None
203
-
204
- def set(
205
- self,
206
- query: str,
207
- response: Dict[str, Any],
208
- ticker: Optional[str] = None,
209
- doc_types: Optional[List[str]] = None,
210
- top_k: int = 10
211
- ):
212
- """
213
- Cache a response.
214
-
215
- Args:
216
- query: Query text
217
- response: Response to cache
218
- ticker: Ticker filter
219
- doc_types: Document type filters
220
- top_k: Number of results
221
- """
222
- key = self._generate_key(query, ticker, doc_types, top_k)
223
-
224
- if len(self.cache) >= self.max_size:
225
- self._evict_lru()
226
-
227
- self.cache[key] = CacheEntry(response, ttl=self.ttl)
228
-
229
- def _evict_lru(self):
230
- """Remove least recently used 10% of entries."""
231
- num_to_remove = max(1, self.max_size // 10)
232
-
233
- # Sort by last access time (hit count and creation time)
234
- sorted_keys = sorted(
235
- self.cache.keys(),
236
- key=lambda k: (self.cache[k].hit_count, self.cache[k].created_at)
237
- )
238
-
239
- for key in sorted_keys[:num_to_remove]:
240
- del self.cache[key]
241
-
242
- def clear(self):
243
- """Clear all cached responses."""
244
- self.cache.clear()
245
- self.hits = 0
246
- self.misses = 0
247
-
248
- def get_stats(self) -> Dict[str, Any]:
249
- """Get cache statistics."""
250
- total_requests = self.hits + self.misses
251
- hit_rate = (self.hits / total_requests * 100) if total_requests > 0 else 0
252
-
253
- # Calculate cost savings (assuming $0.0001 per query)
254
- cost_per_query = 0.0001 # Approximate cost per LLM call
255
- estimated_savings = self.hits * cost_per_query
256
-
257
- return {
258
- "size": len(self.cache),
259
- "max_size": self.max_size,
260
- "hits": self.hits,
261
- "misses": self.misses,
262
- "hit_rate": round(hit_rate, 2),
263
- "total_requests": total_requests,
264
- "estimated_savings_usd": round(estimated_savings, 4)
265
- }
266
-
267
-
268
- class DocumentCache:
269
- """Cache for retrieved documents to avoid vector searches."""
270
-
271
- def __init__(self, max_size: int = 200, ttl: int = 7200):
272
- """
273
- Initialize document cache.
274
-
275
- Args:
276
- max_size: Maximum number of entries (default: 200)
277
- ttl: Time to live in seconds (default: 2 hours)
278
- """
279
- self.cache: Dict[str, CacheEntry] = {}
280
- self.max_size = max_size
281
- self.ttl = ttl
282
- self.hits = 0
283
- self.misses = 0
284
-
285
- def _generate_key(
286
- self,
287
- query: str,
288
- ticker: Optional[str] = None,
289
- doc_types: Optional[List[str]] = None
290
- ) -> str:
291
- """Generate cache key from search parameters."""
292
- query_normalized = query.lower().strip()
293
- ticker_normalized = ticker.lower() if ticker else ""
294
- doc_types_normalized = sorted(doc_types) if doc_types else []
295
-
296
- key_string = f"{query_normalized}|{ticker_normalized}|{','.join(doc_types_normalized)}"
297
- return hashlib.md5(key_string.encode()).hexdigest()
298
-
299
- def get(
300
- self,
301
- query: str,
302
- ticker: Optional[str] = None,
303
- doc_types: Optional[List[str]] = None
304
- ) -> Optional[List[Any]]:
305
- """Get cached documents."""
306
- key = self._generate_key(query, ticker, doc_types)
307
-
308
- if key in self.cache:
309
- entry = self.cache[key]
310
- if not entry.is_expired():
311
- entry.increment_hits()
312
- self.hits += 1
313
- return entry.value
314
- else:
315
- del self.cache[key]
316
-
317
- self.misses += 1
318
- return None
319
-
320
- def set(
321
- self,
322
- query: str,
323
- documents: List[Any],
324
- ticker: Optional[str] = None,
325
- doc_types: Optional[List[str]] = None
326
- ):
327
- """Cache retrieved documents."""
328
- key = self._generate_key(query, ticker, doc_types)
329
-
330
- if len(self.cache) >= self.max_size:
331
- self._evict_oldest()
332
-
333
- self.cache[key] = CacheEntry(documents, ttl=self.ttl)
334
-
335
- def _evict_oldest(self):
336
- """Remove oldest 10% of entries."""
337
- num_to_remove = max(1, self.max_size // 10)
338
- sorted_keys = sorted(
339
- self.cache.keys(),
340
- key=lambda k: self.cache[k].created_at
341
- )
342
-
343
- for key in sorted_keys[:num_to_remove]:
344
- del self.cache[key]
345
-
346
- def clear(self):
347
- """Clear all cached documents."""
348
- self.cache.clear()
349
- self.hits = 0
350
- self.misses = 0
351
-
352
- def get_stats(self) -> Dict[str, Any]:
353
- """Get cache statistics."""
354
- total_requests = self.hits + self.misses
355
- hit_rate = (self.hits / total_requests * 100) if total_requests > 0 else 0
356
-
357
- return {
358
- "size": len(self.cache),
359
- "max_size": self.max_size,
360
- "hits": self.hits,
361
- "misses": self.misses,
362
- "hit_rate": round(hit_rate, 2),
363
- "total_requests": total_requests
364
- }
365
-
366
-
367
- class CacheManager:
368
- """Centralized cache management."""
369
-
370
- def __init__(self):
371
- """Initialize all caches."""
372
- self.embedding_cache = EmbeddingCache(max_size=1000, ttl=86400) # 24h
373
- self.response_cache = QueryResponseCache(max_size=500, ttl=3600) # 1h
374
- self.document_cache = DocumentCache(max_size=200, ttl=7200) # 2h
375
-
376
- def clear_all(self):
377
- """Clear all caches."""
378
- self.embedding_cache.clear()
379
- self.response_cache.clear()
380
- self.document_cache.clear()
381
-
382
- def get_all_stats(self) -> Dict[str, Any]:
383
- """Get statistics for all caches."""
384
- return {
385
- "embedding_cache": self.embedding_cache.get_stats(),
386
- "response_cache": self.response_cache.get_stats(),
387
- "document_cache": self.document_cache.get_stats(),
388
- "timestamp": datetime.now().isoformat()
389
- }
390
-
391
-
392
- # Global cache manager instance
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
393
  cache_manager = CacheManager()
 
1
+ """Caching system for embeddings, queries, and responses."""
2
+ import hashlib
3
+ import time
4
+ from typing import Optional, Dict, Any, List
5
+ from datetime import datetime, timedelta
6
+ import json
7
+
8
+
9
+ class CacheEntry:
10
+ """Represents a cached item with expiration."""
11
+
12
+ def __init__(self, value: Any, ttl: int = 3600):
13
+ """
14
+ Initialize cache entry.
15
+
16
+ Args:
17
+ value: Value to cache
18
+ ttl: Time to live in seconds (default: 1 hour)
19
+ """
20
+ self.value = value
21
+ self.created_at = time.time()
22
+ self.ttl = ttl
23
+ self.hit_count = 0
24
+
25
+ def is_expired(self) -> bool:
26
+ """Check if entry has expired."""
27
+ return time.time() - self.created_at > self.ttl
28
+
29
+ def increment_hits(self):
30
+ """Increment hit counter."""
31
+ self.hit_count += 1
32
+
33
+
34
+ class EmbeddingCache:
35
+ """Cache for query embeddings to avoid re-computing."""
36
+
37
+ def __init__(self, max_size: int = 1000, ttl: int = 86400):
38
+ """
39
+ Initialize embedding cache.
40
+
41
+ Args:
42
+ max_size: Maximum number of entries (default: 1000)
43
+ ttl: Time to live in seconds (default: 24 hours)
44
+ """
45
+ self.cache: Dict[str, CacheEntry] = {}
46
+ self.max_size = max_size
47
+ self.ttl = ttl
48
+ self.hits = 0
49
+ self.misses = 0
50
+
51
+ def _generate_key(self, text: str) -> str:
52
+ """Generate cache key from text."""
53
+ return hashlib.md5(text.lower().strip().encode()).hexdigest()
54
+
55
+ def get(self, text: str) -> Optional[List[float]]:
56
+ """
57
+ Get cached embedding.
58
+
59
+ Args:
60
+ text: Query text
61
+
62
+ Returns:
63
+ Cached embedding vector or None
64
+ """
65
+ key = self._generate_key(text)
66
+
67
+ if key in self.cache:
68
+ entry = self.cache[key]
69
+ if not entry.is_expired():
70
+ entry.increment_hits()
71
+ self.hits += 1
72
+ return entry.value
73
+ else:
74
+ # Remove expired entry
75
+ del self.cache[key]
76
+
77
+ self.misses += 1
78
+ return None
79
+
80
+ def set(self, text: str, embedding: List[float]):
81
+ """
82
+ Cache an embedding.
83
+
84
+ Args:
85
+ text: Query text
86
+ embedding: Embedding vector
87
+ """
88
+ key = self._generate_key(text)
89
+
90
+ # If cache is full, remove oldest entries
91
+ if len(self.cache) >= self.max_size:
92
+ self._evict_oldest()
93
+
94
+ self.cache[key] = CacheEntry(embedding, ttl=self.ttl)
95
+
96
+ def _evict_oldest(self):
97
+ """Remove oldest 10% of entries."""
98
+ num_to_remove = max(1, self.max_size // 10)
99
+
100
+ # Sort by creation time and remove oldest
101
+ sorted_keys = sorted(
102
+ self.cache.keys(),
103
+ key=lambda k: self.cache[k].created_at
104
+ )
105
+
106
+ for key in sorted_keys[:num_to_remove]:
107
+ del self.cache[key]
108
+
109
+ def clear(self):
110
+ """Clear all cached embeddings."""
111
+ self.cache.clear()
112
+ self.hits = 0
113
+ self.misses = 0
114
+
115
+ def get_stats(self) -> Dict[str, Any]:
116
+ """Get cache statistics."""
117
+ total_requests = self.hits + self.misses
118
+ hit_rate = (self.hits / total_requests * 100) if total_requests > 0 else 0
119
+
120
+ return {
121
+ "size": len(self.cache),
122
+ "max_size": self.max_size,
123
+ "hits": self.hits,
124
+ "misses": self.misses,
125
+ "hit_rate": round(hit_rate, 2),
126
+ "total_requests": total_requests
127
+ }
128
+
129
+
130
+ class QueryResponseCache:
131
+ """Cache for complete query responses."""
132
+
133
+ def __init__(self, max_size: int = 500, ttl: int = 3600):
134
+ """
135
+ Initialize response cache.
136
+
137
+ Args:
138
+ max_size: Maximum number of entries (default: 500)
139
+ ttl: Time to live in seconds (default: 1 hour)
140
+ """
141
+ self.cache: Dict[str, CacheEntry] = {}
142
+ self.max_size = max_size
143
+ self.ttl = ttl
144
+ self.hits = 0
145
+ self.misses = 0
146
+
147
+ def _generate_key(
148
+ self,
149
+ query: str,
150
+ ticker: Optional[str] = None,
151
+ doc_types: Optional[List[str]] = None,
152
+ top_k: int = 10
153
+ ) -> str:
154
+ """Generate cache key from query parameters."""
155
+ # Normalize inputs
156
+ query_normalized = query.lower().strip()
157
+ ticker_normalized = ticker.lower() if ticker else ""
158
+ doc_types_normalized = sorted(doc_types) if doc_types else []
159
+
160
+ # Create key string
161
+ key_parts = [
162
+ query_normalized,
163
+ ticker_normalized,
164
+ ",".join(doc_types_normalized),
165
+ str(top_k)
166
+ ]
167
+ key_string = "|".join(key_parts)
168
+
169
+ return hashlib.md5(key_string.encode()).hexdigest()
170
+
171
+ def get(
172
+ self,
173
+ query: str,
174
+ ticker: Optional[str] = None,
175
+ doc_types: Optional[List[str]] = None,
176
+ top_k: int = 10
177
+ ) -> Optional[Dict[str, Any]]:
178
+ """
179
+ Get cached response.
180
+
181
+ Args:
182
+ query: Query text
183
+ ticker: Ticker filter
184
+ doc_types: Document type filters
185
+ top_k: Number of results
186
+
187
+ Returns:
188
+ Cached response or None
189
+ """
190
+ key = self._generate_key(query, ticker, doc_types, top_k)
191
+
192
+ if key in self.cache:
193
+ entry = self.cache[key]
194
+ if not entry.is_expired():
195
+ entry.increment_hits()
196
+ self.hits += 1
197
+ return entry.value
198
+ else:
199
+ del self.cache[key]
200
+
201
+ self.misses += 1
202
+ return None
203
+
204
+ def set(
205
+ self,
206
+ query: str,
207
+ response: Dict[str, Any],
208
+ ticker: Optional[str] = None,
209
+ doc_types: Optional[List[str]] = None,
210
+ top_k: int = 10
211
+ ):
212
+ """
213
+ Cache a response.
214
+
215
+ Args:
216
+ query: Query text
217
+ response: Response to cache
218
+ ticker: Ticker filter
219
+ doc_types: Document type filters
220
+ top_k: Number of results
221
+ """
222
+ key = self._generate_key(query, ticker, doc_types, top_k)
223
+
224
+ if len(self.cache) >= self.max_size:
225
+ self._evict_lru()
226
+
227
+ self.cache[key] = CacheEntry(response, ttl=self.ttl)
228
+
229
+ def _evict_lru(self):
230
+ """Remove least recently used 10% of entries."""
231
+ num_to_remove = max(1, self.max_size // 10)
232
+
233
+ # Sort by last access time (hit count and creation time)
234
+ sorted_keys = sorted(
235
+ self.cache.keys(),
236
+ key=lambda k: (self.cache[k].hit_count, self.cache[k].created_at)
237
+ )
238
+
239
+ for key in sorted_keys[:num_to_remove]:
240
+ del self.cache[key]
241
+
242
+ def clear(self):
243
+ """Clear all cached responses."""
244
+ self.cache.clear()
245
+ self.hits = 0
246
+ self.misses = 0
247
+
248
+ def get_stats(self) -> Dict[str, Any]:
249
+ """Get cache statistics."""
250
+ total_requests = self.hits + self.misses
251
+ hit_rate = (self.hits / total_requests * 100) if total_requests > 0 else 0
252
+
253
+ # Calculate cost savings (assuming $0.0001 per query)
254
+ cost_per_query = 0.0001 # Approximate cost per LLM call
255
+ estimated_savings = self.hits * cost_per_query
256
+
257
+ return {
258
+ "size": len(self.cache),
259
+ "max_size": self.max_size,
260
+ "hits": self.hits,
261
+ "misses": self.misses,
262
+ "hit_rate": round(hit_rate, 2),
263
+ "total_requests": total_requests,
264
+ "estimated_savings_usd": round(estimated_savings, 4)
265
+ }
266
+
267
+
268
+ class DocumentCache:
269
+ """Cache for retrieved documents to avoid vector searches."""
270
+
271
+ def __init__(self, max_size: int = 200, ttl: int = 7200):
272
+ """
273
+ Initialize document cache.
274
+
275
+ Args:
276
+ max_size: Maximum number of entries (default: 200)
277
+ ttl: Time to live in seconds (default: 2 hours)
278
+ """
279
+ self.cache: Dict[str, CacheEntry] = {}
280
+ self.max_size = max_size
281
+ self.ttl = ttl
282
+ self.hits = 0
283
+ self.misses = 0
284
+
285
+ def _generate_key(
286
+ self,
287
+ query: str,
288
+ ticker: Optional[str] = None,
289
+ doc_types: Optional[List[str]] = None
290
+ ) -> str:
291
+ """Generate cache key from search parameters."""
292
+ query_normalized = query.lower().strip()
293
+ ticker_normalized = ticker.lower() if ticker else ""
294
+ doc_types_normalized = sorted(doc_types) if doc_types else []
295
+
296
+ key_string = f"{query_normalized}|{ticker_normalized}|{','.join(doc_types_normalized)}"
297
+ return hashlib.md5(key_string.encode()).hexdigest()
298
+
299
+ def get(
300
+ self,
301
+ query: str,
302
+ ticker: Optional[str] = None,
303
+ doc_types: Optional[List[str]] = None
304
+ ) -> Optional[List[Any]]:
305
+ """Get cached documents."""
306
+ key = self._generate_key(query, ticker, doc_types)
307
+
308
+ if key in self.cache:
309
+ entry = self.cache[key]
310
+ if not entry.is_expired():
311
+ entry.increment_hits()
312
+ self.hits += 1
313
+ return entry.value
314
+ else:
315
+ del self.cache[key]
316
+
317
+ self.misses += 1
318
+ return None
319
+
320
+ def set(
321
+ self,
322
+ query: str,
323
+ documents: List[Any],
324
+ ticker: Optional[str] = None,
325
+ doc_types: Optional[List[str]] = None
326
+ ):
327
+ """Cache retrieved documents."""
328
+ key = self._generate_key(query, ticker, doc_types)
329
+
330
+ if len(self.cache) >= self.max_size:
331
+ self._evict_oldest()
332
+
333
+ self.cache[key] = CacheEntry(documents, ttl=self.ttl)
334
+
335
+ def _evict_oldest(self):
336
+ """Remove oldest 10% of entries."""
337
+ num_to_remove = max(1, self.max_size // 10)
338
+ sorted_keys = sorted(
339
+ self.cache.keys(),
340
+ key=lambda k: self.cache[k].created_at
341
+ )
342
+
343
+ for key in sorted_keys[:num_to_remove]:
344
+ del self.cache[key]
345
+
346
+ def clear(self):
347
+ """Clear all cached documents."""
348
+ self.cache.clear()
349
+ self.hits = 0
350
+ self.misses = 0
351
+
352
+ def get_stats(self) -> Dict[str, Any]:
353
+ """Get cache statistics."""
354
+ total_requests = self.hits + self.misses
355
+ hit_rate = (self.hits / total_requests * 100) if total_requests > 0 else 0
356
+
357
+ return {
358
+ "size": len(self.cache),
359
+ "max_size": self.max_size,
360
+ "hits": self.hits,
361
+ "misses": self.misses,
362
+ "hit_rate": round(hit_rate, 2),
363
+ "total_requests": total_requests
364
+ }
365
+
366
+
367
+ class CacheManager:
368
+ """Centralized cache management."""
369
+
370
+ def __init__(self):
371
+ """Initialize all caches."""
372
+ self.embedding_cache = EmbeddingCache(max_size=1000, ttl=86400) # 24h
373
+ self.response_cache = QueryResponseCache(max_size=500, ttl=3600) # 1h
374
+ self.document_cache = DocumentCache(max_size=200, ttl=7200) # 2h
375
+
376
+ def get_response(self, cache_key: str) -> Optional[Dict[str, Any]]:
377
+ """
378
+ Get cached response by pre-computed key.
379
+
380
+ Args:
381
+ cache_key: Pre-computed cache key from rag_chain
382
+
383
+ Returns:
384
+ Cached response data or None
385
+ """
386
+ if cache_key in self.response_cache.cache:
387
+ entry = self.response_cache.cache[cache_key]
388
+ if not entry.is_expired():
389
+ entry.increment_hits()
390
+ self.response_cache.hits += 1
391
+ return entry.value
392
+ else:
393
+ # Remove expired entry
394
+ del self.response_cache.cache[cache_key]
395
+
396
+ self.response_cache.misses += 1
397
+ return None
398
+
399
+ def set_response(self, cache_key: str, response_data: Dict[str, Any]):
400
+ """
401
+ Cache a response with pre-computed key.
402
+
403
+ Args:
404
+ cache_key: Pre-computed cache key from rag_chain
405
+ response_data: Response data to cache
406
+ """
407
+ # Check if cache is full and evict if needed
408
+ if len(self.response_cache.cache) >= self.response_cache.max_size:
409
+ self.response_cache._evict_lru()
410
+
411
+ # Store with the pre-computed key
412
+ self.response_cache.cache[cache_key] = CacheEntry(
413
+ response_data,
414
+ ttl=self.response_cache.ttl
415
+ )
416
+
417
+ def get_stats(self) -> Dict[str, Any]:
418
+ """
419
+ Get cache statistics (alias for get_all_stats).
420
+
421
+ Returns:
422
+ Dictionary with stats for all caches
423
+ """
424
+ return self.get_all_stats()
425
+
426
+ def clear(self):
427
+ """Clear all caches (alias for clear_all)."""
428
+ self.clear_all()
429
+
430
+ def clear_all(self):
431
+ """Clear all caches."""
432
+ self.embedding_cache.clear()
433
+ self.response_cache.clear()
434
+ self.document_cache.clear()
435
+
436
+ def get_all_stats(self) -> Dict[str, Any]:
437
+ """Get statistics for all caches."""
438
+ return {
439
+ "embedding_cache": self.embedding_cache.get_stats(),
440
+ "response_cache": self.response_cache.get_stats(),
441
+ "document_cache": self.document_cache.get_stats(),
442
+ "timestamp": datetime.now().isoformat()
443
+ }
444
+
445
+
446
+ # Global cache manager instance
447
  cache_manager = CacheManager()