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,671 @@
1
+ """
2
+ IRIS Cache Backend for LLM Caching
3
+
4
+ This module provides an IRIS database backend for the LLM caching layer,
5
+ leveraging the existing IRIS infrastructure for persistent cache storage.
6
+ """
7
+
8
+ import json
9
+ import hashlib
10
+ import logging
11
+ from typing import Any, Dict, Optional
12
+ from datetime import datetime, timedelta
13
+
14
+ from common.llm_cache_config import CacheConfig
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class IRISCacheBackend:
20
+ """IRIS database backend for LLM response caching."""
21
+
22
+ def __init__(
23
+ self,
24
+ iris_connector,
25
+ table_name: str = "llm_cache",
26
+ ttl_seconds: int = 3600,
27
+ schema: str = "USER",
28
+ ):
29
+ """
30
+ Initialize IRIS cache backend.
31
+
32
+ Args:
33
+ iris_connector: IRIS database connector instance (DBAPI, JDBC, or SQLAlchemy)
34
+ table_name: Name of the cache table
35
+ ttl_seconds: Default time-to-live for cache entries
36
+ schema: Database schema name
37
+ """
38
+ self.iris_connector = iris_connector
39
+ self.table_name = table_name
40
+ self.ttl_seconds = ttl_seconds
41
+ self.schema = schema
42
+ self.full_table_name = f"{schema}.{table_name}" if schema else table_name
43
+
44
+ # Detect connection type and set up appropriate interface
45
+ self._setup_connection_interface()
46
+
47
+ # Cache statistics
48
+ self.stats = {"hits": 0, "misses": 0, "sets": 0, "deletes": 0, "errors": 0}
49
+
50
+ # Ensure table exists
51
+ self.setup_table()
52
+
53
+ def _setup_connection_interface(self):
54
+ """Setup the appropriate interface based on connection type."""
55
+ # Check if it's a SQLAlchemy connection
56
+ if hasattr(self.iris_connector, "execute") and hasattr(
57
+ self.iris_connector, "commit"
58
+ ):
59
+ self.connection_type = "sqlalchemy"
60
+ logger.debug("Using SQLAlchemy connection interface for cache")
61
+ # Check if it's a DBAPI/JDBC connection
62
+ elif hasattr(self.iris_connector, "cursor"):
63
+ self.connection_type = "dbapi"
64
+ logger.debug("Using DBAPI/JDBC connection interface for cache")
65
+ else:
66
+ # Try to detect other connection types
67
+ logger.warning(
68
+ f"Unknown connection type: {type(self.iris_connector)}. Attempting DBAPI interface."
69
+ )
70
+ self.connection_type = "dbapi"
71
+
72
+ def _get_cursor(self):
73
+ """Get a cursor appropriate for the connection type."""
74
+ # Validate connection handle before proceeding
75
+ if self.iris_connector is None:
76
+ raise ConnectionError("Cannot get cursor: _handle is NULL")
77
+
78
+ if self.connection_type == "sqlalchemy":
79
+ # For SQLAlchemy connections, we use the connection directly
80
+ return self.iris_connector
81
+ else:
82
+ # For DBAPI/JDBC connections
83
+ try:
84
+ cursor = self.iris_connector.cursor()
85
+ if cursor is None:
86
+ raise ConnectionError("Failed to create cursor: cursor is NULL")
87
+ return cursor
88
+ except AttributeError as e:
89
+ raise ConnectionError(
90
+ f"Connection object does not support cursor(): {e}"
91
+ )
92
+
93
+ def _execute_sql(self, cursor, sql, params=None):
94
+ """Execute SQL with appropriate method based on connection type."""
95
+ if self.connection_type == "sqlalchemy":
96
+ if params:
97
+ return cursor.execute(sql, params)
98
+ else:
99
+ return cursor.execute(sql)
100
+ else:
101
+ if params:
102
+ return cursor.execute(sql, params)
103
+ else:
104
+ return cursor.execute(sql)
105
+
106
+ def _commit_transaction(self):
107
+ """Commit transaction based on connection type."""
108
+ if self.connection_type == "sqlalchemy":
109
+ # SQLAlchemy connections handle commits differently
110
+ pass # SQLAlchemy autocommits by default
111
+ else:
112
+ self.iris_connector.commit()
113
+
114
+ def _close_cursor(self, cursor):
115
+ """Close cursor if needed based on connection type."""
116
+ if self.connection_type == "sqlalchemy":
117
+ # SQLAlchemy connections don't need cursor closing
118
+ pass
119
+ else:
120
+ cursor.close()
121
+
122
+ def setup_table(self) -> None:
123
+ """Create the cache table if it doesn't exist."""
124
+ # Validate connection handle before proceeding
125
+ if self.iris_connector is None:
126
+ error_msg = "Failed to setup IRIS cache table: _handle is NULL"
127
+ logger.error(error_msg)
128
+ self.stats["errors"] += 1
129
+ raise ConnectionError(error_msg)
130
+
131
+ try:
132
+ cursor = self._get_cursor()
133
+
134
+ # Validate cursor was created successfully
135
+ if cursor is None:
136
+ error_msg = "Failed to setup IRIS cache table: cursor is NULL"
137
+ logger.error(error_msg)
138
+ self.stats["errors"] += 1
139
+ raise ConnectionError(error_msg)
140
+
141
+ # Create table with proper IRIS SQL syntax
142
+ create_table_sql = f"""
143
+ CREATE TABLE IF NOT EXISTS {self.full_table_name} (
144
+ cache_key VARCHAR(255) PRIMARY KEY,
145
+ cache_value LONGVARCHAR,
146
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
147
+ expires_at TIMESTAMP,
148
+ model_name VARCHAR(100),
149
+ prompt_hash VARCHAR(64)
150
+ )
151
+ """
152
+
153
+ self._execute_sql(cursor, create_table_sql)
154
+
155
+ # Create indexes with existence check
156
+ self._create_index_if_not_exists(
157
+ cursor, f"idx_{self.table_name}_expires", "expires_at"
158
+ )
159
+ self._create_index_if_not_exists(
160
+ cursor, f"idx_{self.table_name}_model", "model_name"
161
+ )
162
+
163
+ self._commit_transaction()
164
+ self._close_cursor(cursor)
165
+
166
+ logger.info(
167
+ f"IRIS cache table {self.full_table_name} initialized successfully"
168
+ )
169
+
170
+ except Exception as e:
171
+ logger.error(f"Failed to setup IRIS cache table: {e}")
172
+ self.stats["errors"] += 1
173
+ raise
174
+
175
+ def _create_index_if_not_exists(
176
+ self, cursor, index_name: str, column_name: str
177
+ ) -> None:
178
+ """
179
+ Create an index only if it doesn't already exist.
180
+
181
+ Args:
182
+ cursor: Database cursor
183
+ index_name: Name of the index to create
184
+ column_name: Column to index
185
+ """
186
+ try:
187
+ # Check if index exists using IRIS system catalog
188
+ # Try multiple approaches as IRIS may have different system table configurations
189
+ index_exists = False
190
+
191
+ # Method 1: Try %Dictionary.CompiledIndex (IRIS-specific)
192
+ try:
193
+ check_index_sql = """
194
+ SELECT COUNT(*) FROM %Dictionary.CompiledIndex
195
+ WHERE ParentClass = ? AND Name = ?
196
+ """
197
+ self._execute_sql(
198
+ cursor, check_index_sql, (self.full_table_name, index_name)
199
+ )
200
+ result = cursor.fetchone()
201
+ if result and result[0] > 0:
202
+ index_exists = True
203
+ except Exception:
204
+ # Method 1 failed, try Method 2
205
+ pass
206
+
207
+ # Method 2: Try INFORMATION_SCHEMA.INDEXES if Method 1 failed
208
+ if not index_exists:
209
+ try:
210
+ check_index_sql = """
211
+ SELECT COUNT(*) FROM INFORMATION_SCHEMA.INDEXES
212
+ WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND INDEX_NAME = ?
213
+ """
214
+ self._execute_sql(
215
+ cursor,
216
+ check_index_sql,
217
+ (self.schema, self.table_name, index_name),
218
+ )
219
+ result = cursor.fetchone()
220
+ if result and result[0] > 0:
221
+ index_exists = True
222
+ except Exception:
223
+ # Method 2 failed, try Method 3
224
+ pass
225
+
226
+ # Method 3: Try a simple CREATE INDEX and catch the error if it exists
227
+ if not index_exists:
228
+ try:
229
+ create_index_sql = f"""
230
+ CREATE INDEX {index_name} ON {self.full_table_name} ({column_name})
231
+ """
232
+ self._execute_sql(cursor, create_index_sql)
233
+ logger.debug(
234
+ f"Created index {index_name} on {self.full_table_name}({column_name})"
235
+ )
236
+ except Exception as e:
237
+ # Check if error is about index already existing
238
+ error_msg = str(e).lower()
239
+ if any(
240
+ keyword in error_msg
241
+ for keyword in ["already exists", "duplicate", "exists"]
242
+ ):
243
+ logger.debug(f"Index {index_name} already exists")
244
+ else:
245
+ # Re-raise if it's a different error
246
+ raise
247
+ else:
248
+ logger.debug(f"Index {index_name} already exists")
249
+
250
+ except Exception as e:
251
+ logger.error(f"Error creating index {index_name}: {e}")
252
+ # Don't raise - index creation is not critical for basic functionality
253
+
254
+ def _serialize_cache_value(self, value: Any) -> str:
255
+ """
256
+ Robustly serialize cache value to JSON string, handling ChatGeneration objects.
257
+
258
+ Args:
259
+ value: Value to serialize
260
+
261
+ Returns:
262
+ JSON string representation of the value
263
+ """
264
+ try:
265
+ # If it's already a string (pre-serialized), return as-is
266
+ if isinstance(value, str):
267
+ # Test if it's valid JSON
268
+ try:
269
+ json.loads(value)
270
+ return value
271
+ except json.JSONDecodeError:
272
+ # Not JSON, treat as plain string
273
+ return json.dumps(value)
274
+
275
+ # Handle basic types
276
+ if isinstance(value, (dict, list, int, float, bool)) or value is None:
277
+ return json.dumps(value)
278
+
279
+ # Handle LangChain Generation objects that might slip through
280
+ try:
281
+ from langchain_core.outputs import Generation, ChatGeneration
282
+ from langchain_core.messages import BaseMessage
283
+ from langchain_core.load import dumpd
284
+
285
+ if isinstance(value, (Generation, ChatGeneration)):
286
+ logger.warning(
287
+ f"Received unserialized {type(value).__name__} object in IRIS cache backend"
288
+ )
289
+
290
+ # Use the same robust serialization as in _update_sync
291
+ try:
292
+ serialized = dumpd(value)
293
+
294
+ # Handle nested BaseMessage objects
295
+ if hasattr(value, "message") and isinstance(
296
+ value.message, BaseMessage
297
+ ):
298
+ if "message" in serialized and isinstance(
299
+ serialized["message"], BaseMessage
300
+ ):
301
+ serialized["message"] = dumpd(value.message)
302
+
303
+ return json.dumps(serialized)
304
+
305
+ except Exception as dumpd_error:
306
+ logger.warning(
307
+ f"dumpd failed for {type(value).__name__}: {dumpd_error}"
308
+ )
309
+
310
+ # Fallback to manual extraction
311
+ fallback_data = {
312
+ "text": getattr(value, "text", str(value)),
313
+ "type": type(value).__name__,
314
+ "generation_info": getattr(value, "generation_info", None),
315
+ }
316
+
317
+ if hasattr(value, "message"):
318
+ message = value.message
319
+ if hasattr(message, "content"):
320
+ fallback_data["message"] = {
321
+ "content": getattr(message, "content", ""),
322
+ "type": type(message).__name__,
323
+ }
324
+ else:
325
+ fallback_data["message"] = str(message)
326
+
327
+ return json.dumps(fallback_data)
328
+
329
+ except ImportError:
330
+ # LangChain not available, continue with other methods
331
+ pass
332
+
333
+ # Try to serialize as-is
334
+ try:
335
+ return json.dumps(value)
336
+ except (TypeError, ValueError) as e:
337
+ logger.warning(
338
+ f"Direct JSON serialization failed for {type(value)}: {e}"
339
+ )
340
+
341
+ # Try to convert to dict if possible
342
+ if hasattr(value, "dict"):
343
+ try:
344
+ return json.dumps(value.dict())
345
+ except Exception as dict_error:
346
+ logger.warning(f"Dict serialization failed: {dict_error}")
347
+
348
+ # Last resort: string representation
349
+ logger.warning(f"Using string fallback for {type(value)}")
350
+ return json.dumps(
351
+ {
352
+ "text": str(value),
353
+ "type": type(value).__name__,
354
+ "error": "serialization_fallback",
355
+ }
356
+ )
357
+
358
+ except Exception as e:
359
+ logger.error(f"Cache value serialization failed completely: {e}")
360
+ # Ultimate fallback
361
+ return json.dumps(
362
+ {"text": "Serialization failed", "type": "unknown", "error": str(e)}
363
+ )
364
+
365
+ def _generate_cache_key(
366
+ self, prompt: str, model_name: str = "default", **kwargs
367
+ ) -> str:
368
+ """
369
+ Generate a cache key from prompt and parameters.
370
+
371
+ Args:
372
+ prompt: The input prompt
373
+ model_name: LLM model name
374
+ **kwargs: Additional parameters (temperature, max_tokens, etc.)
375
+
376
+ Returns:
377
+ SHA256 hash as cache key
378
+ """
379
+ # Create deterministic key from prompt and parameters
380
+ cache_data = {
381
+ "prompt": prompt.strip() if isinstance(prompt, str) else str(prompt),
382
+ "model": model_name,
383
+ **kwargs,
384
+ }
385
+
386
+ # Sort keys for deterministic hashing
387
+ cache_str = json.dumps(cache_data, sort_keys=True)
388
+ return hashlib.sha256(cache_str.encode()).hexdigest()
389
+
390
+ def get(self, cache_key: str) -> Optional[Any]:
391
+ """
392
+ Retrieve value from cache.
393
+
394
+ Args:
395
+ cache_key: Cache key to retrieve
396
+
397
+ Returns:
398
+ Cached value or None if not found/expired
399
+ """
400
+ # Validate connection handle before proceeding
401
+ if self.iris_connector is None:
402
+ logger.error("Cannot retrieve from cache: _handle is NULL")
403
+ self.stats["errors"] += 1
404
+ return None
405
+
406
+ try:
407
+ cursor = self._get_cursor()
408
+
409
+ # Query with expiration check using IRIS SQL syntax
410
+ select_sql = f"""
411
+ SELECT cache_value
412
+ FROM {self.full_table_name}
413
+ WHERE cache_key = ?
414
+ AND (expires_at IS NULL OR expires_at > CURRENT_TIMESTAMP)
415
+ """
416
+
417
+ self._execute_sql(cursor, select_sql, (cache_key,))
418
+
419
+ if self.connection_type == "sqlalchemy":
420
+ result = cursor.fetchone()
421
+ else:
422
+ result = cursor.fetchone()
423
+
424
+ self._close_cursor(cursor)
425
+
426
+ if result:
427
+ self.stats["hits"] += 1
428
+ try:
429
+ # Parse JSON response
430
+ return json.loads(result[0])
431
+ except json.JSONDecodeError:
432
+ # Return as string if not JSON
433
+ return result[0]
434
+ else:
435
+ self.stats["misses"] += 1
436
+ return None
437
+
438
+ except Exception as e:
439
+ logger.error(f"Error retrieving from IRIS cache: {e}")
440
+ self.stats["errors"] += 1
441
+ return None
442
+
443
+ def set(
444
+ self,
445
+ cache_key: str,
446
+ value: Any,
447
+ ttl: Optional[int] = None,
448
+ model_name: str = "default",
449
+ prompt_hash: Optional[str] = None,
450
+ ) -> None:
451
+ """
452
+ Store value in cache.
453
+
454
+ Args:
455
+ cache_key: Cache key
456
+ value: Value to cache
457
+ ttl: Time-to-live in seconds (uses default if None)
458
+ model_name: LLM model name for analytics
459
+ prompt_hash: Hash of the original prompt
460
+ """
461
+ # Validate connection handle before proceeding
462
+ if self.iris_connector is None:
463
+ logger.error("Cannot store to cache: _handle is NULL")
464
+ self.stats["errors"] += 1
465
+ return
466
+
467
+ try:
468
+ cursor = self._get_cursor()
469
+
470
+ # Calculate expiration time
471
+ ttl_to_use = ttl or self.ttl_seconds
472
+ expires_at = datetime.now() + timedelta(seconds=ttl_to_use)
473
+
474
+ # Convert datetime to string format for IRIS compatibility
475
+ expires_at_str = expires_at.strftime("%Y-%m-%d %H:%M:%S")
476
+
477
+ # Robust serialization with ChatGeneration handling
478
+ cache_value = self._serialize_cache_value(value)
479
+
480
+ # Add logging to debug the SQL statement
481
+ logger.debug(
482
+ f"Attempting to store cache entry for key: {cache_key[:16]}..."
483
+ )
484
+
485
+ # Use simpler INSERT OR REPLACE pattern for IRIS compatibility
486
+ # First try to delete existing entry, then insert new one
487
+ delete_sql = f"DELETE FROM {self.full_table_name} WHERE cache_key = ?"
488
+ self._execute_sql(cursor, delete_sql, (cache_key,))
489
+
490
+ # Now insert the new entry
491
+ insert_sql = f"""
492
+ INSERT INTO {self.full_table_name}
493
+ (cache_key, cache_value, created_at, expires_at, model_name, prompt_hash)
494
+ VALUES (?, ?, CURRENT_TIMESTAMP, ?, ?, ?)
495
+ """
496
+
497
+ logger.debug(f"Executing INSERT SQL: {insert_sql}")
498
+ logger.debug(
499
+ f"With parameters: cache_key={cache_key[:16]}..., cache_value_len={len(cache_value)}, expires_at={expires_at_str}, model_name={model_name}, prompt_hash={prompt_hash}"
500
+ )
501
+
502
+ self._execute_sql(
503
+ cursor,
504
+ insert_sql,
505
+ (cache_key, cache_value, expires_at_str, model_name, prompt_hash),
506
+ )
507
+
508
+ self._commit_transaction()
509
+ self._close_cursor(cursor)
510
+
511
+ self.stats["sets"] += 1
512
+ logger.debug(f"Cached value for key: {cache_key[:16]}...")
513
+
514
+ except Exception as e:
515
+ logger.error(f"Error storing to IRIS cache: {e}")
516
+ self.stats["errors"] += 1
517
+ # Don't raise - graceful fallback
518
+
519
+ def delete(self, cache_key: str) -> None:
520
+ """
521
+ Delete value from cache.
522
+
523
+ Args:
524
+ cache_key: Cache key to delete
525
+ """
526
+ try:
527
+ cursor = self._get_cursor()
528
+
529
+ delete_sql = f"DELETE FROM {self.full_table_name} WHERE cache_key = ?"
530
+ self._execute_sql(cursor, delete_sql, (cache_key,))
531
+
532
+ self._commit_transaction()
533
+ self._close_cursor(cursor)
534
+
535
+ self.stats["deletes"] += 1
536
+
537
+ except Exception as e:
538
+ logger.error(f"Error deleting from IRIS cache: {e}")
539
+ self.stats["errors"] += 1
540
+
541
+ def clear(self) -> None:
542
+ """Clear all cache entries."""
543
+ try:
544
+ cursor = self._get_cursor()
545
+
546
+ delete_all_sql = f"DELETE FROM {self.full_table_name}"
547
+ self._execute_sql(cursor, delete_all_sql)
548
+
549
+ self._commit_transaction()
550
+ self._close_cursor(cursor)
551
+
552
+ logger.info("IRIS cache cleared")
553
+
554
+ except Exception as e:
555
+ logger.error(f"Error clearing IRIS cache: {e}")
556
+ self.stats["errors"] += 1
557
+
558
+ def cleanup_expired(self, batch_size: int = 1000) -> int:
559
+ """
560
+ Remove expired cache entries.
561
+
562
+ Args:
563
+ batch_size: Number of entries to delete in each batch
564
+
565
+ Returns:
566
+ Number of entries deleted
567
+ """
568
+ try:
569
+ cursor = self._get_cursor()
570
+
571
+ # Delete expired entries using IRIS SQL syntax
572
+ delete_expired_sql = f"""
573
+ DELETE FROM {self.full_table_name}
574
+ WHERE expires_at IS NOT NULL
575
+ AND expires_at < CURRENT_TIMESTAMP
576
+ """
577
+
578
+ result = self._execute_sql(cursor, delete_expired_sql)
579
+
580
+ # Get row count based on connection type
581
+ if self.connection_type == "sqlalchemy":
582
+ deleted_count = result.rowcount if hasattr(result, "rowcount") else 0
583
+ else:
584
+ deleted_count = cursor.rowcount if hasattr(cursor, "rowcount") else 0
585
+
586
+ self._commit_transaction()
587
+ self._close_cursor(cursor)
588
+
589
+ if deleted_count > 0:
590
+ logger.info(f"Cleaned up {deleted_count} expired cache entries")
591
+
592
+ return deleted_count
593
+
594
+ except Exception as e:
595
+ logger.error(f"Error cleaning up expired cache entries: {e}")
596
+ self.stats["errors"] += 1
597
+ return 0
598
+
599
+ def get_stats(self) -> Dict[str, Any]:
600
+ """Get cache statistics."""
601
+ total_requests = self.stats["hits"] + self.stats["misses"]
602
+ hit_rate = self.stats["hits"] / total_requests if total_requests > 0 else 0.0
603
+
604
+ return {
605
+ **self.stats,
606
+ "total_requests": total_requests,
607
+ "hit_rate": hit_rate,
608
+ "table_name": self.full_table_name,
609
+ }
610
+
611
+ def get_cache_info(self) -> Dict[str, Any]:
612
+ """Get detailed cache information from database."""
613
+ try:
614
+ cursor = self._get_cursor()
615
+
616
+ # Get cache statistics using IRIS SQL
617
+ stats_sql = f"""
618
+ SELECT
619
+ COUNT(*) as total_entries,
620
+ COUNT(CASE WHEN expires_at > CURRENT_TIMESTAMP OR expires_at IS NULL THEN 1 END) as active_entries,
621
+ COUNT(CASE WHEN expires_at <= CURRENT_TIMESTAMP THEN 1 END) as expired_entries,
622
+ MIN(created_at) as oldest_entry,
623
+ MAX(created_at) as newest_entry
624
+ FROM {self.full_table_name}
625
+ """
626
+
627
+ self._execute_sql(cursor, stats_sql)
628
+ result = cursor.fetchone()
629
+
630
+ if result:
631
+ info = {
632
+ "total_entries": result[0],
633
+ "active_entries": result[1],
634
+ "expired_entries": result[2],
635
+ "oldest_entry": result[3],
636
+ "newest_entry": result[4],
637
+ }
638
+ else:
639
+ info = {
640
+ "total_entries": 0,
641
+ "active_entries": 0,
642
+ "expired_entries": 0,
643
+ "oldest_entry": None,
644
+ "newest_entry": None,
645
+ }
646
+
647
+ self._close_cursor(cursor)
648
+ return info
649
+
650
+ except Exception as e:
651
+ logger.error(f"Error getting cache info: {e}")
652
+ return {"error": str(e)}
653
+
654
+
655
+ def create_iris_cache_backend(config: CacheConfig, iris_connector) -> IRISCacheBackend:
656
+ """
657
+ Factory function to create IRIS cache backend.
658
+
659
+ Args:
660
+ config: Cache configuration
661
+ iris_connector: IRIS database connector
662
+
663
+ Returns:
664
+ IRISCacheBackend instance
665
+ """
666
+ return IRISCacheBackend(
667
+ iris_connector=iris_connector,
668
+ table_name=config.table_name,
669
+ ttl_seconds=config.ttl_seconds,
670
+ schema=config.iris_schema,
671
+ )