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.
Files changed (50) hide show
  1. adapters/rag_templates_bridge.py +578 -0
  2. common/__init__.py +1 -0
  3. common/batch_utils.py +294 -0
  4. common/config.py +58 -0
  5. common/connection_manager.py +242 -0
  6. common/connection_pool.py +445 -0
  7. common/database.py +141 -0
  8. common/database_schema_manager.py +289 -0
  9. common/db_init_with_indexes.py +181 -0
  10. common/db_vector_utils.py +181 -0
  11. common/environment_manager.py +220 -0
  12. common/huggingface_utils.py +206 -0
  13. common/iris_client.py +92 -0
  14. common/iris_connection_manager.py +411 -0
  15. common/iris_connection_pool.py +360 -0
  16. common/iris_connector.py +71 -0
  17. common/iris_dbapi_connector.py +323 -0
  18. common/iris_index_utils.py +195 -0
  19. common/iris_sql_utils.py +704 -0
  20. common/iris_stream_reader.py +166 -0
  21. common/llm_cache_config.py +308 -0
  22. common/llm_cache_iris.py +671 -0
  23. common/llm_cache_manager.py +911 -0
  24. common/utils.py +559 -0
  25. common/vector_sql_utils.py +584 -0
  26. evaluation_framework/__init__.py +66 -0
  27. evaluation_framework/biomedical_question_generator.py +826 -0
  28. evaluation_framework/comparative_analysis_system.py +1173 -0
  29. evaluation_framework/empirical_reporting.py +1298 -0
  30. evaluation_framework/evaluation_orchestrator.py +1123 -0
  31. evaluation_framework/execute_production_evaluation.py +382 -0
  32. evaluation_framework/pmc_data_pipeline.py +1332 -0
  33. evaluation_framework/production_evaluation_config.py +353 -0
  34. evaluation_framework/ragas_metrics_framework.py +1013 -0
  35. evaluation_framework/real_production_evaluation.py +544 -0
  36. evaluation_framework/run_10k_evaluation.py +175 -0
  37. evaluation_framework/simple_test.py +364 -0
  38. evaluation_framework/statistical_evaluation_methodology.py +1016 -0
  39. evaluation_framework/test_framework.py +281 -0
  40. evaluation_framework/test_iris_connectivity.py +244 -0
  41. evaluation_framework/test_small_scale_evaluation.py +432 -0
  42. evaluation_framework/true_e2e_evaluation.py +639 -0
  43. evaluation_framework/visualization_engine.py +585 -0
  44. iris_rag/__init__.py +284 -0
  45. iris_rag/config/default_config.yaml +295 -0
  46. iris_vector_rag-0.2.1.dist-info/METADATA +499 -0
  47. iris_vector_rag-0.2.1.dist-info/RECORD +50 -0
  48. iris_vector_rag-0.2.1.dist-info/WHEEL +5 -0
  49. iris_vector_rag-0.2.1.dist-info/entry_points.txt +2 -0
  50. 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
+ }