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
common/batch_utils.py ADDED
@@ -0,0 +1,294 @@
1
+ """
2
+ Batch processing utilities for entity extraction.
3
+
4
+ This module provides utilities for batching documents, retry logic with exponential
5
+ backoff, and batch processing metrics tracking.
6
+
7
+ Implementation:
8
+ - BatchQueue: FIFO queue with token-aware batching (FR-006)
9
+ - Retry logic: Exponential backoff with batch splitting (FR-005)
10
+ - BatchMetricsTracker: Global singleton for metrics tracking (FR-007)
11
+ """
12
+
13
+ from collections import deque
14
+ from typing import List, Optional, Callable
15
+ import time
16
+ import logging
17
+
18
+ from iris_rag.core.models import (
19
+ Document,
20
+ BatchExtractionResult,
21
+ ProcessingMetrics,
22
+ BatchStatus,
23
+ )
24
+
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ class BatchQueue:
30
+ """
31
+ FIFO queue for batching documents with token budget enforcement.
32
+
33
+ This class supports FR-006 (dynamic batch sizing based on token budget) and
34
+ provides O(1) queue operations using collections.deque.
35
+
36
+ Attributes:
37
+ token_budget: Default token budget for batches (default: 8192)
38
+ _queue: Internal deque storing (document, token_count) tuples
39
+ """
40
+
41
+ def __init__(self, token_budget: int = 8192):
42
+ """
43
+ Initialize batch queue.
44
+
45
+ Args:
46
+ token_budget: Default token budget for batches (default: 8192 per FR-006)
47
+ """
48
+ self.token_budget = token_budget
49
+ self._queue: deque = deque()
50
+
51
+ def add_document(self, document: Document, token_count: int) -> None:
52
+ """
53
+ Add a document to the queue.
54
+
55
+ Args:
56
+ document: Document to add
57
+ token_count: Token count for the document
58
+
59
+ Raises:
60
+ ValueError: If token_count is negative
61
+ """
62
+ if token_count < 0:
63
+ raise ValueError("Token count must be non-negative")
64
+
65
+ self._queue.append((document, token_count))
66
+
67
+ def get_next_batch(self, token_budget: Optional[int] = None) -> Optional[List[Document]]:
68
+ """
69
+ Get next batch of documents within token budget.
70
+
71
+ This implements first-fit batching strategy: documents are added to the batch
72
+ in FIFO order until adding the next document would exceed the token budget.
73
+
74
+ Special cases (per FR-006):
75
+ - If queue is empty, returns None
76
+ - If first document exceeds budget, returns it anyway (can't skip)
77
+ - If token_budget is 0 or negative, returns first document
78
+
79
+ Args:
80
+ token_budget: Token budget for this batch (overrides default if provided)
81
+
82
+ Returns:
83
+ List of documents for the batch, or None if queue is empty
84
+ """
85
+ if not self._queue:
86
+ return None
87
+
88
+ # Use provided budget or default
89
+ budget = token_budget if token_budget is not None else self.token_budget
90
+
91
+ batch = []
92
+ cumulative_tokens = 0
93
+
94
+ # Build batch until we exceed budget
95
+ while self._queue:
96
+ doc, tokens = self._queue[0] # Peek at first document
97
+
98
+ # First document always goes in batch (even if exceeds budget)
99
+ if not batch:
100
+ batch.append(doc)
101
+ cumulative_tokens += tokens
102
+ self._queue.popleft() # Remove from queue
103
+ continue
104
+
105
+ # Check if adding this document would exceed budget
106
+ if cumulative_tokens + tokens > budget:
107
+ break # Don't add this document
108
+
109
+ # Add document to batch
110
+ batch.append(doc)
111
+ cumulative_tokens += tokens
112
+ self._queue.popleft() # Remove from queue
113
+
114
+ return batch if batch else None
115
+
116
+
117
+ def extract_batch_with_retry(
118
+ documents: List[Document],
119
+ extract_fn: Callable[[List[Document]], BatchExtractionResult],
120
+ max_retries: int = 3,
121
+ retry_delays: List[float] = None,
122
+ ) -> BatchExtractionResult:
123
+ """
124
+ Extract batch with exponential backoff retry logic (FR-005).
125
+
126
+ This function implements:
127
+ - Exponential backoff: 2s, 4s, 8s delays (configurable)
128
+ - Batch splitting: If all retries fail, split batch in half and retry
129
+ - Retry tracking: Tracks retry attempts in result
130
+
131
+ Args:
132
+ documents: List of documents to extract entities from
133
+ extract_fn: Function to call for extraction (takes List[Document], returns BatchExtractionResult)
134
+ max_retries: Maximum retry attempts (default: 3 per FR-005)
135
+ retry_delays: Retry delays in seconds (default: [2, 4, 8] per FR-005)
136
+
137
+ Returns:
138
+ BatchExtractionResult with extraction results or error
139
+
140
+ Examples:
141
+ >>> def my_extractor(docs):
142
+ ... return BatchExtractionResult(batch_id="test", success_status=True)
143
+ >>> result = extract_batch_with_retry(documents, my_extractor)
144
+ """
145
+ if retry_delays is None:
146
+ retry_delays = [2.0, 4.0, 8.0] # FR-005 exponential backoff
147
+
148
+ retry_count = 0
149
+
150
+ for attempt in range(max_retries + 1): # +1 for initial attempt
151
+ try:
152
+ # Attempt extraction
153
+ result = extract_fn(documents)
154
+
155
+ # If successful, return result with retry count
156
+ if result.success_status:
157
+ result.retry_count = retry_count
158
+ return result
159
+
160
+ # If extraction failed, retry
161
+ logger.warning(
162
+ f"Batch extraction failed (attempt {attempt + 1}/{max_retries + 1}): {result.error_message}"
163
+ )
164
+
165
+ except Exception as e:
166
+ logger.error(
167
+ f"Batch extraction exception (attempt {attempt + 1}/{max_retries + 1}): {e}"
168
+ )
169
+
170
+ # If not last attempt, wait and retry
171
+ if attempt < max_retries:
172
+ retry_count += 1
173
+ delay = retry_delays[min(attempt, len(retry_delays) - 1)]
174
+ logger.info(f"Retrying batch extraction after {delay}s delay...")
175
+ time.sleep(delay)
176
+ else:
177
+ # All retries exhausted
178
+ logger.error(
179
+ f"All {max_retries} retry attempts exhausted for batch of {len(documents)} documents"
180
+ )
181
+
182
+ # Split batch and retry if batch has multiple documents
183
+ if len(documents) > 1:
184
+ logger.info(
185
+ f"Splitting batch of {len(documents)} documents into 2 sub-batches"
186
+ )
187
+ mid = len(documents) // 2
188
+ batch1 = documents[:mid]
189
+ batch2 = documents[mid:]
190
+
191
+ # Recursively retry each sub-batch (with retry_count tracking)
192
+ result1 = extract_batch_with_retry(
193
+ batch1, extract_fn, max_retries, retry_delays
194
+ )
195
+ result2 = extract_batch_with_retry(
196
+ batch2, extract_fn, max_retries, retry_delays
197
+ )
198
+
199
+ # Merge results
200
+ merged_result = BatchExtractionResult(
201
+ batch_id=result1.batch_id, # Use first sub-batch ID
202
+ per_document_entities={
203
+ **result1.per_document_entities,
204
+ **result2.per_document_entities,
205
+ },
206
+ per_document_relationships={
207
+ **result1.per_document_relationships,
208
+ **result2.per_document_relationships,
209
+ },
210
+ processing_time=result1.processing_time + result2.processing_time,
211
+ success_status=result1.success_status and result2.success_status,
212
+ retry_count=retry_count + result1.retry_count + result2.retry_count,
213
+ error_message=None
214
+ if (result1.success_status and result2.success_status)
215
+ else f"Sub-batch errors: {result1.error_message}; {result2.error_message}",
216
+ )
217
+
218
+ return merged_result
219
+
220
+ # Single document batch - return failure
221
+ return BatchExtractionResult(
222
+ batch_id="failed",
223
+ success_status=False,
224
+ retry_count=retry_count,
225
+ error_message=f"Failed after {max_retries} retries (single document)",
226
+ )
227
+
228
+
229
+ class BatchMetricsTracker:
230
+ """
231
+ Global singleton for tracking batch processing metrics (FR-007).
232
+
233
+ This class provides a global singleton instance for tracking metrics across
234
+ all batch processing operations. It implements FR-007 statistics requirements.
235
+
236
+ Usage:
237
+ >>> tracker = BatchMetricsTracker.get_instance()
238
+ >>> tracker.update_with_batch(batch_result, batch_size=10)
239
+ >>> metrics = tracker.get_statistics()
240
+ """
241
+
242
+ _instance: Optional["BatchMetricsTracker"] = None
243
+
244
+ def __init__(self):
245
+ """Initialize metrics tracker with default values."""
246
+ self._metrics = ProcessingMetrics()
247
+
248
+ @classmethod
249
+ def get_instance(cls) -> "BatchMetricsTracker":
250
+ """
251
+ Get global singleton instance of BatchMetricsTracker.
252
+
253
+ Returns:
254
+ Global BatchMetricsTracker instance
255
+ """
256
+ if cls._instance is None:
257
+ cls._instance = cls()
258
+ return cls._instance
259
+
260
+ @classmethod
261
+ def reset_instance(cls) -> None:
262
+ """
263
+ Reset global singleton instance (useful for testing).
264
+
265
+ This creates a fresh instance with zeroed metrics.
266
+ """
267
+ cls._instance = None
268
+
269
+ def update_with_batch(
270
+ self,
271
+ batch_result: BatchExtractionResult,
272
+ batch_size: int,
273
+ single_doc_baseline_time: float = 7.2,
274
+ ) -> None:
275
+ """
276
+ Update metrics with a new batch result.
277
+
278
+ Args:
279
+ batch_result: Result from batch processing
280
+ batch_size: Number of documents in the batch
281
+ single_doc_baseline_time: Baseline time per document (default: 7.2s from spec)
282
+ """
283
+ self._metrics.update_with_batch(
284
+ batch_result, batch_size, single_doc_baseline_time
285
+ )
286
+
287
+ def get_statistics(self) -> ProcessingMetrics:
288
+ """
289
+ Get current processing statistics (FR-007).
290
+
291
+ Returns:
292
+ ProcessingMetrics with current statistics
293
+ """
294
+ return self._metrics
common/config.py ADDED
@@ -0,0 +1,58 @@
1
+ """
2
+ Common configuration module for IRIS RAG system
3
+ Provides unified access to configuration settings
4
+ """
5
+
6
+ import os
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ # Add config path to sys.path
11
+ config_dir = Path(__file__).parent.parent / "config"
12
+ sys.path.insert(0, str(config_dir))
13
+
14
+ from loader import get_config
15
+
16
+
17
+ def get_iris_config():
18
+ """Get IRIS database configuration"""
19
+ config = get_config()
20
+ if not config:
21
+ raise ValueError("Failed to load configuration")
22
+
23
+ db_config = config.get("database", {})
24
+ return {
25
+ "host": db_config.get("db_host", "localhost"),
26
+ "port": db_config.get("db_port", 1974),
27
+ "username": db_config.get("db_user", "SuperUser"),
28
+ "password": db_config.get("db_password", "SYS"),
29
+ "namespace": db_config.get("db_namespace", "USER"),
30
+ "connection_type": "dbapi", # Default to DBAPI
31
+ }
32
+
33
+
34
+ def get_embedding_config():
35
+ """Get embedding model configuration"""
36
+ config = get_config()
37
+ if not config:
38
+ raise ValueError("Failed to load configuration")
39
+
40
+ return config.get("embedding_model", {})
41
+
42
+
43
+ def get_storage_config():
44
+ """Get storage configuration"""
45
+ config = get_config()
46
+ if not config:
47
+ raise ValueError("Failed to load configuration")
48
+
49
+ return config.get("storage", {})
50
+
51
+
52
+ def get_pipeline_config():
53
+ """Get pipeline configuration"""
54
+ config = get_config()
55
+ if not config:
56
+ raise ValueError("Failed to load configuration")
57
+
58
+ return config.get("pipelines", {})
@@ -0,0 +1,242 @@
1
+ """
2
+ Centralized connection manager for RAG pipelines
3
+ Handles JDBC, ODBC, and future dbapi connections with a unified interface
4
+ """
5
+
6
+ import logging
7
+ import os
8
+ from contextlib import contextmanager
9
+ from typing import Any, List, Optional
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class ConnectionManager:
15
+ """Manages database connections for RAG pipelines"""
16
+
17
+ def __init__(self, connection_type: str = "odbc"):
18
+ """
19
+ Initialize connection manager
20
+
21
+ Args:
22
+ connection_type: Either "jdbc", "odbc", or "dbapi" (default: "odbc" for stability)
23
+ """
24
+ self.connection_type = connection_type.lower()
25
+ self._connection = None
26
+ self._cursor = None
27
+
28
+ if self.connection_type not in ["jdbc", "odbc", "dbapi"]:
29
+ raise ValueError(
30
+ f"Invalid connection type: {connection_type}. Must be 'jdbc', 'odbc', or 'dbapi'"
31
+ )
32
+
33
+ def connect(self):
34
+ """Establish database connection based on configured type"""
35
+ if self._connection:
36
+ return self._connection
37
+
38
+ if self.connection_type == "jdbc":
39
+ try:
40
+ from common.iris_connection_manager import get_iris_jdbc_connection
41
+
42
+ self._connection = get_iris_jdbc_connection()
43
+ logger.info("Established JDBC connection")
44
+ except Exception as e:
45
+ logger.warning(f"JDBC connection failed: {e}, falling back to ODBC")
46
+ self.connection_type = "odbc"
47
+ return self.connect()
48
+
49
+ elif self.connection_type == "dbapi":
50
+ try:
51
+ from common.iris_dbapi_connector import get_iris_dbapi_connection
52
+
53
+ self._connection = get_iris_dbapi_connection()
54
+ if not self._connection: # If get_iris_dbapi_connection returns None
55
+ raise ConnectionError("DBAPI connection returned None")
56
+ logger.info("Established DBAPI connection")
57
+ except Exception as e:
58
+ logger.warning(f"DBAPI connection failed: {e}, falling back to ODBC")
59
+ self.connection_type = "odbc" # Fallback
60
+ # Clear any partial connection state before retrying
61
+ if self._connection and hasattr(self._connection, "close"):
62
+ try:
63
+ self._connection.close()
64
+ except Exception as close_err:
65
+ logger.error(
66
+ f"Error closing partial DBAPI connection during fallback: {close_err}"
67
+ )
68
+ self._connection = None
69
+ return self.connect() # Retry with ODBC
70
+
71
+ else: # odbc (or fallback from dbapi/jdbc)
72
+ from common.iris_connector import get_iris_connection
73
+
74
+ self._connection = get_iris_connection()
75
+ logger.info("Established ODBC connection")
76
+
77
+ return self._connection
78
+
79
+ def execute(self, query: str, params: Optional[List[Any]] = None) -> List[Any]:
80
+ """
81
+ Execute a query with optional parameters
82
+
83
+ Args:
84
+ query: SQL query to execute
85
+ params: Optional list of parameters for parameterized queries
86
+
87
+ Returns:
88
+ List of result rows
89
+ """
90
+ if not self._connection:
91
+ self.connect()
92
+
93
+ if self.connection_type == "jdbc":
94
+ # JDBC connector has execute method
95
+ return self._connection.execute(query, params or [])
96
+ else: # Covers ODBC and DBAPI
97
+ # ODBC and DBAPI use a similar cursor-based approach
98
+ cursor = self._connection.cursor()
99
+ try:
100
+ if params:
101
+ cursor.execute(query, params)
102
+ else:
103
+ cursor.execute(query)
104
+
105
+ # For SELECT queries, fetchall. For others (INSERT, UPDATE, DELETE),
106
+ # fetchall might not be appropriate or available, or might return empty.
107
+ # Standard DBAPI cursor.description is None for non-SELECT.
108
+ if cursor.description:
109
+ return cursor.fetchall()
110
+ else:
111
+ # For non-SELECT, perhaps return rowcount or an empty list
112
+ # For now, returning empty list to maintain consistency with existing behavior
113
+ # if fetchall() was called on a non-SELECT for ODBC.
114
+ return []
115
+ finally:
116
+ cursor.close()
117
+
118
+ def execute_many(self, query: str, params_list: List[List[Any]]) -> None:
119
+ """
120
+ Execute a query multiple times with different parameters
121
+
122
+ Args:
123
+ query: SQL query to execute
124
+ params_list: List of parameter lists
125
+ """
126
+ if not self._connection:
127
+ self.connect()
128
+
129
+ if self.connection_type == "jdbc":
130
+ # Execute in a loop for JDBC
131
+ for params in params_list:
132
+ self._connection.execute(query, params)
133
+ else: # Covers ODBC and DBAPI
134
+ # ODBC and DBAPI support executemany
135
+ cursor = self._connection.cursor()
136
+ try:
137
+ cursor.executemany(query, params_list)
138
+ self._connection.commit() # Ensure commit for DBAPI as well
139
+ finally:
140
+ cursor.close()
141
+
142
+ @contextmanager
143
+ def cursor(self):
144
+ """Get a cursor for direct database operations"""
145
+ if not self._connection:
146
+ self.connect()
147
+
148
+ if self.connection_type == "jdbc":
149
+ # For JDBC, we'll create a wrapper that mimics cursor behavior
150
+ class JDBCCursorWrapper:
151
+ def __init__(self, connection):
152
+ self.connection = connection
153
+ self._results = None
154
+
155
+ def execute(self, query, params=None):
156
+ self._results = self.connection.execute(query, params or [])
157
+
158
+ def fetchall(self):
159
+ return self._results or []
160
+
161
+ def fetchone(self):
162
+ if self._results and len(self._results) > 0:
163
+ return self._results[0]
164
+ return None
165
+
166
+ def close(self):
167
+ pass # No-op for JDBC
168
+
169
+ yield JDBCCursorWrapper(self._connection)
170
+ else: # Covers ODBC and DBAPI
171
+ # ODBC and DBAPI native cursor
172
+ cursor = self._connection.cursor()
173
+ try:
174
+ yield cursor
175
+ finally:
176
+ cursor.close()
177
+
178
+ def close(self):
179
+ """Close the database connection"""
180
+ if self._connection:
181
+ if self.connection_type == "jdbc":
182
+ # JDBC connection might not have close method
183
+ if hasattr(self._connection, "close"):
184
+ self._connection.close()
185
+ else: # Covers ODBC and DBAPI
186
+ if hasattr(
187
+ self._connection, "close"
188
+ ): # Good practice to check for DBAPI too
189
+ self._connection.close()
190
+ self._connection = None
191
+ logger.info(f"Closed {self.connection_type.upper()} connection")
192
+
193
+ def __enter__(self):
194
+ """Context manager entry"""
195
+ self.connect()
196
+ return self
197
+
198
+ def __exit__(self, exc_type, exc_val, exc_tb):
199
+ """Context manager exit"""
200
+ self.close()
201
+ return False
202
+
203
+
204
+ # Global connection manager instance (can be configured at startup)
205
+ _global_connection_manager = None
206
+
207
+
208
+ def get_connection_manager(connection_type: Optional[str] = None) -> ConnectionManager:
209
+ """
210
+ Get or create a global connection manager
211
+
212
+ Args:
213
+ connection_type: Override connection type (default uses environment or "odbc" for stability)
214
+
215
+ Returns:
216
+ ConnectionManager instance
217
+ """
218
+ global _global_connection_manager
219
+
220
+ if connection_type:
221
+ # Create new manager with specified type
222
+ return ConnectionManager(connection_type)
223
+
224
+ if _global_connection_manager is None:
225
+ # Create default manager based on environment
226
+ # Default to ODBC for stability until JDBC issues are resolved
227
+ default_type = os.environ.get("RAG_CONNECTION_TYPE", "odbc")
228
+ _global_connection_manager = ConnectionManager(default_type)
229
+
230
+ return _global_connection_manager
231
+
232
+
233
+ def set_global_connection_type(connection_type: str):
234
+ """
235
+ Set the global connection type for all pipelines
236
+
237
+ Args:
238
+ connection_type: Either "jdbc", "odbc", or "dbapi"
239
+ """
240
+ global _global_connection_manager
241
+ _global_connection_manager = ConnectionManager(connection_type)
242
+ logger.info(f"Set global connection type to: {connection_type}")