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/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}")
|