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
common/llm_cache_iris.py
ADDED
|
@@ -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
|
+
)
|