iris-vector-rag 0.2.1__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.
- adapters/rag_templates_bridge.py +578 -0
- common/__init__.py +1 -0
- common/batch_utils.py +294 -0
- common/config.py +58 -0
- common/connection_manager.py +242 -0
- common/connection_pool.py +445 -0
- common/database.py +141 -0
- common/database_schema_manager.py +289 -0
- common/db_init_with_indexes.py +181 -0
- common/db_vector_utils.py +181 -0
- common/environment_manager.py +220 -0
- common/huggingface_utils.py +206 -0
- common/iris_client.py +92 -0
- common/iris_connection_manager.py +411 -0
- common/iris_connection_pool.py +360 -0
- common/iris_connector.py +71 -0
- common/iris_dbapi_connector.py +323 -0
- common/iris_index_utils.py +195 -0
- common/iris_sql_utils.py +704 -0
- common/iris_stream_reader.py +166 -0
- common/llm_cache_config.py +308 -0
- common/llm_cache_iris.py +671 -0
- common/llm_cache_manager.py +911 -0
- common/utils.py +559 -0
- common/vector_sql_utils.py +584 -0
- evaluation_framework/__init__.py +66 -0
- evaluation_framework/biomedical_question_generator.py +826 -0
- evaluation_framework/comparative_analysis_system.py +1173 -0
- evaluation_framework/empirical_reporting.py +1298 -0
- evaluation_framework/evaluation_orchestrator.py +1123 -0
- evaluation_framework/execute_production_evaluation.py +382 -0
- evaluation_framework/pmc_data_pipeline.py +1332 -0
- evaluation_framework/production_evaluation_config.py +353 -0
- evaluation_framework/ragas_metrics_framework.py +1013 -0
- evaluation_framework/real_production_evaluation.py +544 -0
- evaluation_framework/run_10k_evaluation.py +175 -0
- evaluation_framework/simple_test.py +364 -0
- evaluation_framework/statistical_evaluation_methodology.py +1016 -0
- evaluation_framework/test_framework.py +281 -0
- evaluation_framework/test_iris_connectivity.py +244 -0
- evaluation_framework/test_small_scale_evaluation.py +432 -0
- evaluation_framework/true_e2e_evaluation.py +639 -0
- evaluation_framework/visualization_engine.py +585 -0
- iris_rag/__init__.py +284 -0
- iris_rag/config/default_config.yaml +295 -0
- iris_vector_rag-0.2.1.dist-info/METADATA +499 -0
- iris_vector_rag-0.2.1.dist-info/RECORD +50 -0
- iris_vector_rag-0.2.1.dist-info/WHEEL +5 -0
- iris_vector_rag-0.2.1.dist-info/entry_points.txt +2 -0
- iris_vector_rag-0.2.1.dist-info/top_level.txt +4 -0
|
@@ -0,0 +1,911 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LLM Cache Manager
|
|
3
|
+
|
|
4
|
+
This module provides the main cache management functionality, integrating
|
|
5
|
+
with Langchain's caching system and coordinating between different cache backends.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import hashlib
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
import time
|
|
13
|
+
from typing import Any, Dict, Optional, Union, List
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from langchain_core.outputs import Generation
|
|
16
|
+
|
|
17
|
+
from common.llm_cache_config import CacheConfig, load_cache_config
|
|
18
|
+
from common.llm_cache_iris import IRISCacheBackend, create_iris_cache_backend
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class CacheMetrics:
|
|
25
|
+
"""Cache performance metrics."""
|
|
26
|
+
|
|
27
|
+
hits: int = 0
|
|
28
|
+
misses: int = 0
|
|
29
|
+
total_requests: int = 0
|
|
30
|
+
hit_rate: float = 0.0
|
|
31
|
+
avg_response_time_cached: float = 0.0
|
|
32
|
+
avg_response_time_uncached: float = 0.0
|
|
33
|
+
|
|
34
|
+
def record_hit(self, response_time: float = 0.0) -> None:
|
|
35
|
+
"""Record a cache hit."""
|
|
36
|
+
self.hits += 1
|
|
37
|
+
self.total_requests += 1
|
|
38
|
+
self._update_hit_rate()
|
|
39
|
+
if response_time > 0:
|
|
40
|
+
self._update_cached_response_time(response_time)
|
|
41
|
+
|
|
42
|
+
def record_miss(self, response_time: float = 0.0) -> None:
|
|
43
|
+
"""Record a cache miss."""
|
|
44
|
+
self.misses += 1
|
|
45
|
+
self.total_requests += 1
|
|
46
|
+
self._update_hit_rate()
|
|
47
|
+
if response_time > 0:
|
|
48
|
+
self._update_uncached_response_time(response_time)
|
|
49
|
+
|
|
50
|
+
def _update_hit_rate(self) -> None:
|
|
51
|
+
"""Update hit rate calculation."""
|
|
52
|
+
if self.total_requests > 0:
|
|
53
|
+
self.hit_rate = self.hits / self.total_requests
|
|
54
|
+
|
|
55
|
+
def _update_cached_response_time(self, response_time: float) -> None:
|
|
56
|
+
"""Update average cached response time."""
|
|
57
|
+
if self.hits > 1:
|
|
58
|
+
self.avg_response_time_cached = (
|
|
59
|
+
self.avg_response_time_cached * (self.hits - 1) + response_time
|
|
60
|
+
) / self.hits
|
|
61
|
+
else:
|
|
62
|
+
self.avg_response_time_cached = response_time
|
|
63
|
+
|
|
64
|
+
def _update_uncached_response_time(self, response_time: float) -> None:
|
|
65
|
+
"""Update average uncached response time."""
|
|
66
|
+
if self.misses > 1:
|
|
67
|
+
self.avg_response_time_uncached = (
|
|
68
|
+
self.avg_response_time_uncached * (self.misses - 1) + response_time
|
|
69
|
+
) / self.misses
|
|
70
|
+
else:
|
|
71
|
+
self.avg_response_time_uncached = response_time
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class LangchainCacheManager:
|
|
75
|
+
"""Manages Langchain cache configuration and lifecycle."""
|
|
76
|
+
|
|
77
|
+
def __init__(self, config: CacheConfig):
|
|
78
|
+
self.config = config
|
|
79
|
+
self.metrics = CacheMetrics()
|
|
80
|
+
self.cache_backend = None
|
|
81
|
+
self._langchain_cache_configured = False
|
|
82
|
+
|
|
83
|
+
def setup_cache(self) -> Optional[Any]:
|
|
84
|
+
"""Configure Langchain's global cache."""
|
|
85
|
+
if not self.config.enabled:
|
|
86
|
+
logger.info("LLM caching is disabled")
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
import langchain
|
|
91
|
+
from langchain_community.cache import InMemoryCache
|
|
92
|
+
|
|
93
|
+
if self.config.backend == "memory":
|
|
94
|
+
cache = InMemoryCache()
|
|
95
|
+
langchain.llm_cache = cache
|
|
96
|
+
logger.info("Langchain memory cache configured")
|
|
97
|
+
|
|
98
|
+
elif self.config.backend == "iris":
|
|
99
|
+
# Try to reuse existing IRIS connection first, fallback to URL-based connection
|
|
100
|
+
try:
|
|
101
|
+
iris_connector = self._get_iris_connection_for_cache()
|
|
102
|
+
|
|
103
|
+
# Validate connection before creating cache backend
|
|
104
|
+
if iris_connector is None:
|
|
105
|
+
raise ConnectionError(
|
|
106
|
+
"Failed to setup IRIS cache table: _handle is NULL"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
self.cache_backend = create_iris_cache_backend(
|
|
110
|
+
self.config, iris_connector
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Create Langchain-compatible cache wrapper
|
|
114
|
+
cache = LangchainIRISCacheWrapper(self.cache_backend)
|
|
115
|
+
langchain.llm_cache = cache
|
|
116
|
+
logger.info("Langchain IRIS cache configured")
|
|
117
|
+
|
|
118
|
+
except Exception as e:
|
|
119
|
+
logger.error(f"Failed to setup IRIS cache table: {e}")
|
|
120
|
+
if self.config.graceful_fallback:
|
|
121
|
+
logger.info(
|
|
122
|
+
"Falling back to memory cache due to IRIS connection failure"
|
|
123
|
+
)
|
|
124
|
+
cache = InMemoryCache()
|
|
125
|
+
langchain.llm_cache = cache
|
|
126
|
+
logger.info("Langchain memory cache configured as fallback")
|
|
127
|
+
else:
|
|
128
|
+
raise
|
|
129
|
+
|
|
130
|
+
else:
|
|
131
|
+
logger.error(
|
|
132
|
+
f"Unsupported cache backend: {self.config.backend}. Supported backends: memory, iris"
|
|
133
|
+
)
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
self._langchain_cache_configured = True
|
|
137
|
+
return cache
|
|
138
|
+
|
|
139
|
+
except Exception as e:
|
|
140
|
+
logger.error(f"Failed to setup Langchain cache: {e}")
|
|
141
|
+
if self.config.graceful_fallback:
|
|
142
|
+
logger.info("Continuing without cache due to graceful fallback")
|
|
143
|
+
return None
|
|
144
|
+
raise
|
|
145
|
+
|
|
146
|
+
def _get_iris_connection_for_cache(self):
|
|
147
|
+
"""
|
|
148
|
+
Get IRIS connection for cache using DBAPI interface.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
IRIS connection object
|
|
152
|
+
"""
|
|
153
|
+
# Always use DBAPI for cache to avoid JDBC compatibility issues
|
|
154
|
+
try:
|
|
155
|
+
from common.iris_dbapi_connector import get_iris_dbapi_connection
|
|
156
|
+
|
|
157
|
+
iris_connector = get_iris_dbapi_connection()
|
|
158
|
+
|
|
159
|
+
# Validate the connection handle
|
|
160
|
+
if iris_connector is None:
|
|
161
|
+
raise ConnectionError("DBAPI connection returned NULL handle")
|
|
162
|
+
|
|
163
|
+
logger.info("Using DBAPI IRIS connection for cache")
|
|
164
|
+
return iris_connector
|
|
165
|
+
except Exception as e:
|
|
166
|
+
logger.debug(f"Could not get DBAPI connection: {e}")
|
|
167
|
+
|
|
168
|
+
# Fallback to URL-based connection (original behavior)
|
|
169
|
+
try:
|
|
170
|
+
from common.utils import get_iris_connector
|
|
171
|
+
|
|
172
|
+
iris_connector = get_iris_connector()
|
|
173
|
+
|
|
174
|
+
# Validate the connection handle
|
|
175
|
+
if iris_connector is None:
|
|
176
|
+
raise ConnectionError("URL-based connection returned NULL handle")
|
|
177
|
+
|
|
178
|
+
logger.info("Using URL-based IRIS connection for cache")
|
|
179
|
+
return iris_connector
|
|
180
|
+
except Exception as e:
|
|
181
|
+
logger.error(f"Failed to establish IRIS connection for cache: {e}")
|
|
182
|
+
raise
|
|
183
|
+
|
|
184
|
+
def _setup_iris_fallback(self) -> Optional[Any]:
|
|
185
|
+
"""Setup IRIS cache as fallback."""
|
|
186
|
+
try:
|
|
187
|
+
iris_connector = self._get_iris_connection_for_cache()
|
|
188
|
+
|
|
189
|
+
self.cache_backend = create_iris_cache_backend(self.config, iris_connector)
|
|
190
|
+
cache = LangchainIRISCacheWrapper(self.cache_backend)
|
|
191
|
+
|
|
192
|
+
import langchain
|
|
193
|
+
|
|
194
|
+
langchain.llm_cache = cache
|
|
195
|
+
|
|
196
|
+
logger.info("IRIS cache configured as fallback")
|
|
197
|
+
return cache
|
|
198
|
+
|
|
199
|
+
except Exception as e:
|
|
200
|
+
logger.error(f"IRIS fallback cache setup failed: {e}")
|
|
201
|
+
return None
|
|
202
|
+
|
|
203
|
+
def get_cache_stats(self) -> Dict[str, Any]:
|
|
204
|
+
"""Get cache performance statistics."""
|
|
205
|
+
stats = {
|
|
206
|
+
"enabled": self.config.enabled,
|
|
207
|
+
"backend": self.config.backend,
|
|
208
|
+
"configured": self._langchain_cache_configured,
|
|
209
|
+
"metrics": {
|
|
210
|
+
"hits": self.metrics.hits,
|
|
211
|
+
"misses": self.metrics.misses,
|
|
212
|
+
"total_requests": self.metrics.total_requests,
|
|
213
|
+
"hit_rate": self.metrics.hit_rate,
|
|
214
|
+
"avg_response_time_cached": self.metrics.avg_response_time_cached,
|
|
215
|
+
"avg_response_time_uncached": self.metrics.avg_response_time_uncached,
|
|
216
|
+
},
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if self.cache_backend:
|
|
220
|
+
stats["backend_stats"] = self.cache_backend.get_stats()
|
|
221
|
+
|
|
222
|
+
return stats
|
|
223
|
+
|
|
224
|
+
def clear_cache(self) -> None:
|
|
225
|
+
"""Clear all cached entries."""
|
|
226
|
+
if self.cache_backend:
|
|
227
|
+
self.cache_backend.clear()
|
|
228
|
+
|
|
229
|
+
# Also clear Langchain cache if configured
|
|
230
|
+
try:
|
|
231
|
+
import langchain
|
|
232
|
+
|
|
233
|
+
if hasattr(langchain, "llm_cache") and langchain.llm_cache:
|
|
234
|
+
if hasattr(langchain.llm_cache, "clear"):
|
|
235
|
+
langchain.llm_cache.clear()
|
|
236
|
+
except Exception as e:
|
|
237
|
+
logger.warning(f"Failed to clear Langchain cache: {e}")
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class LangchainIRISCacheWrapper:
|
|
241
|
+
"""Wrapper to make IRIS cache backend compatible with Langchain."""
|
|
242
|
+
|
|
243
|
+
def __init__(self, iris_cache_backend: IRISCacheBackend):
|
|
244
|
+
self.backend = iris_cache_backend
|
|
245
|
+
|
|
246
|
+
def lookup(self, prompt: str, llm_string: str) -> Optional[str]:
|
|
247
|
+
"""Langchain cache lookup interface."""
|
|
248
|
+
cache_key = self._generate_langchain_key(prompt, llm_string)
|
|
249
|
+
result = self.backend.get(cache_key)
|
|
250
|
+
|
|
251
|
+
if result:
|
|
252
|
+
# Extract the response from cached data
|
|
253
|
+
if isinstance(result, dict) and "response" in result:
|
|
254
|
+
return result["response"]
|
|
255
|
+
elif isinstance(result, str):
|
|
256
|
+
return result
|
|
257
|
+
|
|
258
|
+
return None
|
|
259
|
+
|
|
260
|
+
async def alookup(
|
|
261
|
+
self, prompt: str, llm_string: str
|
|
262
|
+
) -> Optional[List["Generation"]]:
|
|
263
|
+
"""
|
|
264
|
+
Asynchronous Langchain cache lookup interface.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
prompt: The input prompt
|
|
268
|
+
llm_string: The LLM string identifier
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
Optional[List[Generation]]: List of Generation objects if found, None otherwise
|
|
272
|
+
"""
|
|
273
|
+
try:
|
|
274
|
+
# Run the synchronous lookup logic in a separate thread
|
|
275
|
+
result = await asyncio.to_thread(self._lookup_sync, prompt, llm_string)
|
|
276
|
+
return result
|
|
277
|
+
except Exception as e:
|
|
278
|
+
logger.error(f"Error during async cache lookup: {e}")
|
|
279
|
+
return None
|
|
280
|
+
|
|
281
|
+
def _lookup_sync(
|
|
282
|
+
self, prompt: str, llm_string: str
|
|
283
|
+
) -> Optional[List["Generation"]]:
|
|
284
|
+
"""
|
|
285
|
+
Synchronous helper method for database lookup logic.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
prompt: The input prompt
|
|
289
|
+
llm_string: The LLM string identifier
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
Optional[List[Generation]]: List of Generation objects if found, None otherwise
|
|
293
|
+
"""
|
|
294
|
+
try:
|
|
295
|
+
# Import Generation and load here to avoid circular imports
|
|
296
|
+
from langchain_core.outputs import Generation
|
|
297
|
+
from langchain_core.load import load
|
|
298
|
+
|
|
299
|
+
cache_key = self._generate_langchain_key(prompt, llm_string)
|
|
300
|
+
result = self.backend.get(cache_key)
|
|
301
|
+
|
|
302
|
+
if result:
|
|
303
|
+
# Extract the response from cached data
|
|
304
|
+
if isinstance(result, dict) and "response" in result:
|
|
305
|
+
response_data = result["response"]
|
|
306
|
+
elif isinstance(result, str):
|
|
307
|
+
response_data = result
|
|
308
|
+
else:
|
|
309
|
+
return None
|
|
310
|
+
|
|
311
|
+
# Parse the JSON string containing the list of generations
|
|
312
|
+
try:
|
|
313
|
+
generations_data = json.loads(response_data)
|
|
314
|
+
|
|
315
|
+
# Ensure it's a list
|
|
316
|
+
if not isinstance(generations_data, list):
|
|
317
|
+
raise TypeError(
|
|
318
|
+
f"Expected list of generations, got {type(generations_data)}"
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
# Convert each generation dict back to Generation object
|
|
322
|
+
generations = []
|
|
323
|
+
for gen_data in generations_data:
|
|
324
|
+
if isinstance(gen_data, dict):
|
|
325
|
+
try:
|
|
326
|
+
# Check if it's a langchain serialized format (has 'lc' and 'kwargs')
|
|
327
|
+
if "lc" in gen_data and "kwargs" in gen_data:
|
|
328
|
+
# Use langchain's load function to reconstruct
|
|
329
|
+
generation = load(gen_data)
|
|
330
|
+
generations.append(generation)
|
|
331
|
+
elif "text" in gen_data and "type" in gen_data:
|
|
332
|
+
# Handle fallback string representation
|
|
333
|
+
logger.warning(
|
|
334
|
+
f"Loading generation from string fallback: {gen_data['type']}"
|
|
335
|
+
)
|
|
336
|
+
generation = Generation(text=gen_data["text"])
|
|
337
|
+
generations.append(generation)
|
|
338
|
+
else:
|
|
339
|
+
# Direct construction from kwargs
|
|
340
|
+
generation = Generation(**gen_data)
|
|
341
|
+
generations.append(generation)
|
|
342
|
+
except Exception as load_error:
|
|
343
|
+
logger.warning(
|
|
344
|
+
f"Failed to load generation from cache data, skipping: {load_error}"
|
|
345
|
+
)
|
|
346
|
+
# Try to create a basic Generation object with available text
|
|
347
|
+
if "text" in gen_data:
|
|
348
|
+
generation = Generation(text=gen_data["text"])
|
|
349
|
+
generations.append(generation)
|
|
350
|
+
elif isinstance(gen_data, str):
|
|
351
|
+
generation = Generation(text=gen_data)
|
|
352
|
+
generations.append(generation)
|
|
353
|
+
else:
|
|
354
|
+
logger.warning(
|
|
355
|
+
f"Expected dict for generation data, got {type(gen_data)}, attempting string conversion"
|
|
356
|
+
)
|
|
357
|
+
generation = Generation(text=str(gen_data))
|
|
358
|
+
generations.append(generation)
|
|
359
|
+
|
|
360
|
+
return generations
|
|
361
|
+
|
|
362
|
+
except json.JSONDecodeError as e:
|
|
363
|
+
logger.error("Error during cache lookup JSON decode: %s", e)
|
|
364
|
+
return None
|
|
365
|
+
except (TypeError, AttributeError, ValueError) as e:
|
|
366
|
+
logger.error("Error during cache lookup data processing: %s", e)
|
|
367
|
+
return None
|
|
368
|
+
except Exception as e:
|
|
369
|
+
# Catch ValidationError and other pydantic errors
|
|
370
|
+
logger.error("Error during cache lookup: %s", e)
|
|
371
|
+
return None
|
|
372
|
+
|
|
373
|
+
return None
|
|
374
|
+
|
|
375
|
+
except Exception as e:
|
|
376
|
+
logger.error("Error during async cache lookup: %s", e)
|
|
377
|
+
return None
|
|
378
|
+
|
|
379
|
+
async def aupdate(
|
|
380
|
+
self, prompt: str, llm_string: str, return_val: List["Generation"]
|
|
381
|
+
) -> None:
|
|
382
|
+
"""
|
|
383
|
+
Asynchronous Langchain cache update interface.
|
|
384
|
+
|
|
385
|
+
Args:
|
|
386
|
+
prompt: The input prompt
|
|
387
|
+
llm_string: The LLM string identifier
|
|
388
|
+
return_val: List of Generation objects to cache
|
|
389
|
+
"""
|
|
390
|
+
try:
|
|
391
|
+
# Run the synchronous update logic in a separate thread
|
|
392
|
+
await asyncio.to_thread(self._update_sync, prompt, llm_string, return_val)
|
|
393
|
+
except Exception as e:
|
|
394
|
+
logger.error(f"Error during async cache update: {e}")
|
|
395
|
+
|
|
396
|
+
def _update_sync(
|
|
397
|
+
self, prompt: str, llm_string: str, return_val: List["Generation"]
|
|
398
|
+
) -> None:
|
|
399
|
+
"""
|
|
400
|
+
Synchronous helper method for database update logic.
|
|
401
|
+
|
|
402
|
+
Args:
|
|
403
|
+
prompt: The input prompt
|
|
404
|
+
llm_string: The LLM string identifier
|
|
405
|
+
return_val: List of Generation objects to cache
|
|
406
|
+
"""
|
|
407
|
+
try:
|
|
408
|
+
import json
|
|
409
|
+
from langchain_core.load import dumpd
|
|
410
|
+
from langchain_core.messages import BaseMessage
|
|
411
|
+
|
|
412
|
+
# Convert Generation objects to serializable format with robust error handling
|
|
413
|
+
generations_data = []
|
|
414
|
+
|
|
415
|
+
for i, generation in enumerate(return_val):
|
|
416
|
+
try:
|
|
417
|
+
# Log generation details for debugging
|
|
418
|
+
logger.debug(
|
|
419
|
+
f"Serializing generation {i}: {type(generation).__name__}"
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
# Use Langchain's dumpd function for proper serialization
|
|
423
|
+
gen_data = dumpd(generation)
|
|
424
|
+
|
|
425
|
+
# Additional validation: ensure the result is JSON serializable
|
|
426
|
+
# This catches cases where dumpd succeeds but produces non-serializable objects
|
|
427
|
+
json.dumps(gen_data)
|
|
428
|
+
|
|
429
|
+
# Special handling for ChatGeneration with BaseMessage that might not be fully serialized
|
|
430
|
+
if hasattr(generation, "message") and isinstance(
|
|
431
|
+
generation.message, BaseMessage
|
|
432
|
+
):
|
|
433
|
+
# Double-check that the message in the serialized data is also serializable
|
|
434
|
+
if "message" in gen_data and isinstance(
|
|
435
|
+
gen_data["message"], BaseMessage
|
|
436
|
+
):
|
|
437
|
+
logger.debug(
|
|
438
|
+
f"Re-serializing BaseMessage in generation {i}"
|
|
439
|
+
)
|
|
440
|
+
gen_data["message"] = dumpd(generation.message)
|
|
441
|
+
# Validate again
|
|
442
|
+
json.dumps(gen_data)
|
|
443
|
+
|
|
444
|
+
generations_data.append(gen_data)
|
|
445
|
+
logger.debug(f"Successfully serialized generation {i}")
|
|
446
|
+
|
|
447
|
+
except Exception as e:
|
|
448
|
+
logger.warning(
|
|
449
|
+
f"Failed to serialize generation {i} with dumpd: {e}"
|
|
450
|
+
)
|
|
451
|
+
logger.debug(
|
|
452
|
+
f"Generation {i} type: {type(generation)}, attributes: {dir(generation)}"
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
# Fallback 1: Try dict method if available
|
|
456
|
+
try:
|
|
457
|
+
if hasattr(generation, "dict"):
|
|
458
|
+
gen_data = generation.dict()
|
|
459
|
+
|
|
460
|
+
# Handle BaseMessage in dict output
|
|
461
|
+
if "message" in gen_data and hasattr(
|
|
462
|
+
gen_data["message"], "dict"
|
|
463
|
+
):
|
|
464
|
+
gen_data["message"] = gen_data["message"].dict()
|
|
465
|
+
|
|
466
|
+
# Validate JSON serialization
|
|
467
|
+
json.dumps(gen_data)
|
|
468
|
+
generations_data.append(gen_data)
|
|
469
|
+
logger.debug(
|
|
470
|
+
f"Successfully serialized generation {i} using dict fallback"
|
|
471
|
+
)
|
|
472
|
+
continue
|
|
473
|
+
|
|
474
|
+
except Exception as dict_error:
|
|
475
|
+
logger.warning(
|
|
476
|
+
f"Dict fallback failed for generation {i}: {dict_error}"
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
# Fallback 2: Manual extraction of key attributes
|
|
480
|
+
try:
|
|
481
|
+
gen_data = {
|
|
482
|
+
"text": getattr(generation, "text", str(generation)),
|
|
483
|
+
"type": type(generation).__name__,
|
|
484
|
+
"generation_info": getattr(
|
|
485
|
+
generation, "generation_info", None
|
|
486
|
+
),
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
# Handle message attribute for ChatGeneration
|
|
490
|
+
if hasattr(generation, "message"):
|
|
491
|
+
message = generation.message
|
|
492
|
+
if hasattr(message, "content"):
|
|
493
|
+
gen_data["message"] = {
|
|
494
|
+
"content": getattr(message, "content", ""),
|
|
495
|
+
"type": type(message).__name__,
|
|
496
|
+
}
|
|
497
|
+
else:
|
|
498
|
+
gen_data["message"] = str(message)
|
|
499
|
+
|
|
500
|
+
# Validate JSON serialization
|
|
501
|
+
json.dumps(gen_data)
|
|
502
|
+
generations_data.append(gen_data)
|
|
503
|
+
logger.debug(
|
|
504
|
+
f"Successfully serialized generation {i} using manual extraction"
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
except Exception as manual_error:
|
|
508
|
+
logger.error(
|
|
509
|
+
f"Manual extraction failed for generation {i}: {manual_error}"
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
# Fallback 3: Minimal safe representation
|
|
513
|
+
gen_data = {
|
|
514
|
+
"text": str(generation) if generation is not None else "",
|
|
515
|
+
"type": type(generation).__name__,
|
|
516
|
+
"error": "serialization_failed",
|
|
517
|
+
"error_details": str(e),
|
|
518
|
+
}
|
|
519
|
+
generations_data.append(gen_data)
|
|
520
|
+
logger.warning(
|
|
521
|
+
f"Using minimal safe representation for generation {i}"
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
# Final validation: ensure the entire list is JSON serializable
|
|
525
|
+
try:
|
|
526
|
+
serialized_generations = json.dumps(generations_data)
|
|
527
|
+
logger.debug(
|
|
528
|
+
f"Successfully serialized {len(generations_data)} generations to JSON ({len(serialized_generations)} chars)"
|
|
529
|
+
)
|
|
530
|
+
except Exception as final_error:
|
|
531
|
+
logger.error(f"Final JSON serialization failed: {final_error}")
|
|
532
|
+
# Create a safe fallback representation
|
|
533
|
+
safe_generations = []
|
|
534
|
+
for i, gen_data in enumerate(generations_data):
|
|
535
|
+
try:
|
|
536
|
+
json.dumps(gen_data)
|
|
537
|
+
safe_generations.append(gen_data)
|
|
538
|
+
except:
|
|
539
|
+
safe_generations.append(
|
|
540
|
+
{
|
|
541
|
+
"text": f"Generation {i} serialization failed",
|
|
542
|
+
"type": "unknown",
|
|
543
|
+
"error": "json_serialization_failed",
|
|
544
|
+
}
|
|
545
|
+
)
|
|
546
|
+
serialized_generations = json.dumps(safe_generations)
|
|
547
|
+
logger.warning(
|
|
548
|
+
f"Used safe fallback serialization for {len(safe_generations)} generations"
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
# Use the existing update method with serialized data
|
|
552
|
+
self.update(prompt, llm_string, serialized_generations)
|
|
553
|
+
logger.debug("Cache update completed successfully")
|
|
554
|
+
|
|
555
|
+
except Exception as e:
|
|
556
|
+
logger.error(f"Error during sync cache update: {e}")
|
|
557
|
+
# Don't re-raise the exception to avoid breaking the LLM call
|
|
558
|
+
|
|
559
|
+
def update(
|
|
560
|
+
self, prompt: str, llm_string: str, return_val: Union[str, List["Generation"]]
|
|
561
|
+
) -> None:
|
|
562
|
+
"""Langchain cache update interface."""
|
|
563
|
+
cache_key = self._generate_langchain_key(prompt, llm_string)
|
|
564
|
+
|
|
565
|
+
# Handle both string and List[Generation] inputs
|
|
566
|
+
if isinstance(return_val, list):
|
|
567
|
+
# This is a List[Generation] - serialize it properly
|
|
568
|
+
try:
|
|
569
|
+
serialized_value = self._serialize_generations_for_sync_update(
|
|
570
|
+
return_val
|
|
571
|
+
)
|
|
572
|
+
except Exception as e:
|
|
573
|
+
logger.error(f"Error serializing generations for sync update: {e}")
|
|
574
|
+
# Fallback to string representation
|
|
575
|
+
serialized_value = str(return_val)
|
|
576
|
+
else:
|
|
577
|
+
# Already a string
|
|
578
|
+
serialized_value = return_val
|
|
579
|
+
|
|
580
|
+
# Store with metadata
|
|
581
|
+
cache_data = {
|
|
582
|
+
"response": serialized_value,
|
|
583
|
+
"llm_string": llm_string,
|
|
584
|
+
"timestamp": time.time(),
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
# Extract model name from llm_string if possible
|
|
588
|
+
model_name = self._extract_model_name(llm_string)
|
|
589
|
+
prompt_hash = hashlib.sha256(prompt.encode()).hexdigest()[:16]
|
|
590
|
+
|
|
591
|
+
self.backend.set(
|
|
592
|
+
cache_key=cache_key,
|
|
593
|
+
value=cache_data,
|
|
594
|
+
model_name=model_name,
|
|
595
|
+
prompt_hash=prompt_hash,
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
def _serialize_generations_for_sync_update(
|
|
599
|
+
self, return_val: List["Generation"]
|
|
600
|
+
) -> str:
|
|
601
|
+
"""
|
|
602
|
+
Serialize List[Generation] for synchronous cache update.
|
|
603
|
+
|
|
604
|
+
Args:
|
|
605
|
+
return_val: List of Generation objects to serialize
|
|
606
|
+
|
|
607
|
+
Returns:
|
|
608
|
+
JSON string representation of the generations
|
|
609
|
+
"""
|
|
610
|
+
try:
|
|
611
|
+
import json
|
|
612
|
+
from langchain_core.load import dumpd
|
|
613
|
+
from langchain_core.messages import BaseMessage
|
|
614
|
+
|
|
615
|
+
# Convert Generation objects to serializable format with robust error handling
|
|
616
|
+
generations_data = []
|
|
617
|
+
|
|
618
|
+
for i, generation in enumerate(return_val):
|
|
619
|
+
try:
|
|
620
|
+
# Log generation details for debugging
|
|
621
|
+
logger.debug(
|
|
622
|
+
f"Serializing generation {i}: {type(generation).__name__}"
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
# Use Langchain's dumpd function for proper serialization
|
|
626
|
+
gen_data = dumpd(generation)
|
|
627
|
+
|
|
628
|
+
# Additional validation: ensure the result is JSON serializable
|
|
629
|
+
# This catches cases where dumpd succeeds but produces non-serializable objects
|
|
630
|
+
json.dumps(gen_data)
|
|
631
|
+
|
|
632
|
+
# Special handling for ChatGeneration with BaseMessage that might not be fully serialized
|
|
633
|
+
if hasattr(generation, "message") and isinstance(
|
|
634
|
+
generation.message, BaseMessage
|
|
635
|
+
):
|
|
636
|
+
# Double-check that the message in the serialized data is also serializable
|
|
637
|
+
if "message" in gen_data and isinstance(
|
|
638
|
+
gen_data["message"], BaseMessage
|
|
639
|
+
):
|
|
640
|
+
logger.debug(
|
|
641
|
+
f"Re-serializing BaseMessage in generation {i}"
|
|
642
|
+
)
|
|
643
|
+
gen_data["message"] = dumpd(generation.message)
|
|
644
|
+
# Validate again
|
|
645
|
+
json.dumps(gen_data)
|
|
646
|
+
|
|
647
|
+
generations_data.append(gen_data)
|
|
648
|
+
logger.debug(f"Successfully serialized generation {i}")
|
|
649
|
+
|
|
650
|
+
except Exception as e:
|
|
651
|
+
logger.warning(
|
|
652
|
+
f"Failed to serialize generation {i} with dumpd: {e}"
|
|
653
|
+
)
|
|
654
|
+
logger.debug(
|
|
655
|
+
f"Generation {i} type: {type(generation)}, attributes: {dir(generation)}"
|
|
656
|
+
)
|
|
657
|
+
|
|
658
|
+
# Fallback 1: Try dict method if available
|
|
659
|
+
try:
|
|
660
|
+
if hasattr(generation, "dict"):
|
|
661
|
+
gen_data = generation.dict()
|
|
662
|
+
|
|
663
|
+
# Handle BaseMessage in dict output
|
|
664
|
+
if "message" in gen_data and hasattr(
|
|
665
|
+
gen_data["message"], "dict"
|
|
666
|
+
):
|
|
667
|
+
gen_data["message"] = gen_data["message"].dict()
|
|
668
|
+
|
|
669
|
+
# Validate JSON serialization
|
|
670
|
+
json.dumps(gen_data)
|
|
671
|
+
generations_data.append(gen_data)
|
|
672
|
+
logger.debug(
|
|
673
|
+
f"Successfully serialized generation {i} using dict fallback"
|
|
674
|
+
)
|
|
675
|
+
continue
|
|
676
|
+
|
|
677
|
+
except Exception as dict_error:
|
|
678
|
+
logger.warning(
|
|
679
|
+
f"Dict fallback failed for generation {i}: {dict_error}"
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
# Fallback 2: Manual extraction of key attributes
|
|
683
|
+
try:
|
|
684
|
+
gen_data = {
|
|
685
|
+
"text": getattr(generation, "text", str(generation)),
|
|
686
|
+
"type": type(generation).__name__,
|
|
687
|
+
"generation_info": getattr(
|
|
688
|
+
generation, "generation_info", None
|
|
689
|
+
),
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
# Handle message attribute for ChatGeneration
|
|
693
|
+
if hasattr(generation, "message"):
|
|
694
|
+
message = generation.message
|
|
695
|
+
if hasattr(message, "content"):
|
|
696
|
+
gen_data["message"] = {
|
|
697
|
+
"content": getattr(message, "content", ""),
|
|
698
|
+
"type": type(message).__name__,
|
|
699
|
+
}
|
|
700
|
+
else:
|
|
701
|
+
gen_data["message"] = str(message)
|
|
702
|
+
|
|
703
|
+
# Validate JSON serialization
|
|
704
|
+
json.dumps(gen_data)
|
|
705
|
+
generations_data.append(gen_data)
|
|
706
|
+
logger.debug(
|
|
707
|
+
f"Successfully serialized generation {i} using manual extraction"
|
|
708
|
+
)
|
|
709
|
+
|
|
710
|
+
except Exception as manual_error:
|
|
711
|
+
logger.error(
|
|
712
|
+
f"Manual extraction failed for generation {i}: {manual_error}"
|
|
713
|
+
)
|
|
714
|
+
|
|
715
|
+
# Fallback 3: Minimal safe representation
|
|
716
|
+
gen_data = {
|
|
717
|
+
"text": str(generation) if generation is not None else "",
|
|
718
|
+
"type": type(generation).__name__,
|
|
719
|
+
"error": "serialization_failed",
|
|
720
|
+
"error_details": str(e),
|
|
721
|
+
}
|
|
722
|
+
generations_data.append(gen_data)
|
|
723
|
+
logger.warning(
|
|
724
|
+
f"Using minimal safe representation for generation {i}"
|
|
725
|
+
)
|
|
726
|
+
|
|
727
|
+
# Final validation: ensure the entire list is JSON serializable
|
|
728
|
+
try:
|
|
729
|
+
serialized_generations = json.dumps(generations_data)
|
|
730
|
+
logger.debug(
|
|
731
|
+
f"Successfully serialized {len(generations_data)} generations to JSON ({len(serialized_generations)} chars)"
|
|
732
|
+
)
|
|
733
|
+
return serialized_generations
|
|
734
|
+
except Exception as final_error:
|
|
735
|
+
logger.error(f"Final JSON serialization failed: {final_error}")
|
|
736
|
+
# Create a safe fallback representation
|
|
737
|
+
safe_generations = []
|
|
738
|
+
for i, gen_data in enumerate(generations_data):
|
|
739
|
+
try:
|
|
740
|
+
json.dumps(gen_data)
|
|
741
|
+
safe_generations.append(gen_data)
|
|
742
|
+
except:
|
|
743
|
+
safe_generations.append(
|
|
744
|
+
{
|
|
745
|
+
"text": f"Generation {i} serialization failed",
|
|
746
|
+
"type": "unknown",
|
|
747
|
+
"error": "json_serialization_failed",
|
|
748
|
+
}
|
|
749
|
+
)
|
|
750
|
+
serialized_generations = json.dumps(safe_generations)
|
|
751
|
+
logger.warning(
|
|
752
|
+
f"Used safe fallback serialization for {len(safe_generations)} generations"
|
|
753
|
+
)
|
|
754
|
+
return serialized_generations
|
|
755
|
+
|
|
756
|
+
except Exception as e:
|
|
757
|
+
logger.error(f"Error during sync cache update serialization: {e}")
|
|
758
|
+
# Ultimate fallback - return a safe JSON string
|
|
759
|
+
return json.dumps(
|
|
760
|
+
[
|
|
761
|
+
{
|
|
762
|
+
"text": "Serialization completely failed",
|
|
763
|
+
"type": "unknown",
|
|
764
|
+
"error": str(e),
|
|
765
|
+
}
|
|
766
|
+
]
|
|
767
|
+
)
|
|
768
|
+
|
|
769
|
+
def clear(self) -> None:
|
|
770
|
+
"""Clear all cache entries."""
|
|
771
|
+
self.backend.clear()
|
|
772
|
+
|
|
773
|
+
def _generate_langchain_key(self, prompt: str, llm_string: str) -> str:
|
|
774
|
+
"""Generate cache key compatible with Langchain format."""
|
|
775
|
+
combined = f"{prompt}|{llm_string}"
|
|
776
|
+
return hashlib.sha256(combined.encode()).hexdigest()
|
|
777
|
+
|
|
778
|
+
def _extract_model_name(self, llm_string: str) -> str:
|
|
779
|
+
"""Extract model name from Langchain LLM string."""
|
|
780
|
+
# Try to parse model name from llm_string
|
|
781
|
+
# Format is usually like "OpenAI\nmodel_name=gpt-3.5-turbo\n..."
|
|
782
|
+
try:
|
|
783
|
+
for line in llm_string.split("\n"):
|
|
784
|
+
if "model_name=" in line:
|
|
785
|
+
return line.split("model_name=")[1].split("\n")[0].strip()
|
|
786
|
+
elif "model=" in line:
|
|
787
|
+
return line.split("model=")[1].split("\n")[0].strip()
|
|
788
|
+
except Exception:
|
|
789
|
+
pass
|
|
790
|
+
|
|
791
|
+
return "unknown"
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
def generate_cache_key(prompt: str, model_name: str = "default", **kwargs) -> str:
|
|
795
|
+
"""
|
|
796
|
+
Generate a cache key from prompt and parameters.
|
|
797
|
+
|
|
798
|
+
Args:
|
|
799
|
+
prompt: The input prompt
|
|
800
|
+
model_name: LLM model name
|
|
801
|
+
**kwargs: Additional parameters (temperature, max_tokens, etc.)
|
|
802
|
+
|
|
803
|
+
Returns:
|
|
804
|
+
SHA256 hash as cache key
|
|
805
|
+
"""
|
|
806
|
+
# Load config to determine what to include in key
|
|
807
|
+
config = load_cache_config()
|
|
808
|
+
|
|
809
|
+
cache_data = {"prompt": prompt.strip()}
|
|
810
|
+
|
|
811
|
+
if config.include_model_name:
|
|
812
|
+
cache_data["model"] = model_name
|
|
813
|
+
|
|
814
|
+
if config.include_temperature and "temperature" in kwargs:
|
|
815
|
+
cache_data["temperature"] = kwargs["temperature"]
|
|
816
|
+
|
|
817
|
+
if config.include_max_tokens and "max_tokens" in kwargs:
|
|
818
|
+
cache_data["max_tokens"] = kwargs["max_tokens"]
|
|
819
|
+
|
|
820
|
+
# Add other specified parameters
|
|
821
|
+
for key, value in kwargs.items():
|
|
822
|
+
if key not in ["temperature", "max_tokens"]: # Already handled above
|
|
823
|
+
cache_data[key] = value
|
|
824
|
+
|
|
825
|
+
# Apply normalization if configured
|
|
826
|
+
if config.normalize_whitespace:
|
|
827
|
+
cache_data["prompt"] = " ".join(cache_data["prompt"].split())
|
|
828
|
+
|
|
829
|
+
if config.normalize_case:
|
|
830
|
+
cache_data["prompt"] = cache_data["prompt"].lower()
|
|
831
|
+
|
|
832
|
+
# Sort keys for deterministic hashing
|
|
833
|
+
cache_str = json.dumps(cache_data, sort_keys=True)
|
|
834
|
+
|
|
835
|
+
if config.hash_algorithm == "sha256":
|
|
836
|
+
return hashlib.sha256(cache_str.encode()).hexdigest()
|
|
837
|
+
elif config.hash_algorithm == "md5":
|
|
838
|
+
return hashlib.md5(cache_str.encode()).hexdigest()
|
|
839
|
+
elif config.hash_algorithm == "sha1":
|
|
840
|
+
return hashlib.sha1(cache_str.encode()).hexdigest()
|
|
841
|
+
else:
|
|
842
|
+
# Default to sha256
|
|
843
|
+
return hashlib.sha256(cache_str.encode()).hexdigest()
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
def setup_langchain_cache(config: Optional[CacheConfig] = None) -> Optional[Any]:
|
|
847
|
+
"""
|
|
848
|
+
Setup Langchain cache based on configuration.
|
|
849
|
+
|
|
850
|
+
Args:
|
|
851
|
+
config: Cache configuration. If None, loads from default location.
|
|
852
|
+
|
|
853
|
+
Returns:
|
|
854
|
+
Cache instance or None if disabled/failed
|
|
855
|
+
"""
|
|
856
|
+
if config is None:
|
|
857
|
+
config = load_cache_config()
|
|
858
|
+
|
|
859
|
+
if not config.enabled:
|
|
860
|
+
return None
|
|
861
|
+
|
|
862
|
+
manager = LangchainCacheManager(config)
|
|
863
|
+
return manager.setup_cache()
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
def is_langchain_cache_configured() -> bool:
|
|
867
|
+
"""Check if Langchain cache is already configured."""
|
|
868
|
+
try:
|
|
869
|
+
import langchain
|
|
870
|
+
|
|
871
|
+
return hasattr(langchain, "llm_cache") and langchain.llm_cache is not None
|
|
872
|
+
except ImportError:
|
|
873
|
+
return False
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
# Global cache manager instance
|
|
877
|
+
_global_cache_manager: Optional[LangchainCacheManager] = None
|
|
878
|
+
|
|
879
|
+
|
|
880
|
+
def get_global_cache_manager() -> Optional[LangchainCacheManager]:
|
|
881
|
+
"""Get or create global cache manager instance."""
|
|
882
|
+
global _global_cache_manager
|
|
883
|
+
|
|
884
|
+
if _global_cache_manager is None:
|
|
885
|
+
config = load_cache_config()
|
|
886
|
+
if config.enabled:
|
|
887
|
+
_global_cache_manager = LangchainCacheManager(config)
|
|
888
|
+
_global_cache_manager.setup_cache()
|
|
889
|
+
|
|
890
|
+
return _global_cache_manager
|
|
891
|
+
|
|
892
|
+
|
|
893
|
+
def clear_global_cache() -> None:
|
|
894
|
+
"""Clear the global cache."""
|
|
895
|
+
manager = get_global_cache_manager()
|
|
896
|
+
if manager:
|
|
897
|
+
manager.clear_cache()
|
|
898
|
+
|
|
899
|
+
|
|
900
|
+
def get_cache_stats() -> Dict[str, Any]:
|
|
901
|
+
"""Get global cache statistics."""
|
|
902
|
+
manager = get_global_cache_manager()
|
|
903
|
+
if manager:
|
|
904
|
+
return manager.get_cache_stats()
|
|
905
|
+
else:
|
|
906
|
+
return {
|
|
907
|
+
"enabled": False,
|
|
908
|
+
"backend": "none",
|
|
909
|
+
"configured": False,
|
|
910
|
+
"metrics": {"hits": 0, "misses": 0, "total_requests": 0, "hit_rate": 0.0},
|
|
911
|
+
}
|