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,445 @@
1
+ """
2
+ IRIS Database Connection Pool Manager.
3
+
4
+ Provides connection pooling for concurrent REST API requests to prevent
5
+ connection pool exhaustion and ensure efficient resource management.
6
+
7
+ Based on research.md findings:
8
+ - 20 base connections, 10 overflow (adaptive under load)
9
+ - 1-hour connection recycle to prevent stale connections
10
+ - Pre-ping validation before each use
11
+ - Thread-safe pool management
12
+ """
13
+
14
+ import logging
15
+ import os
16
+ from contextlib import contextmanager
17
+ from typing import Any, Dict, Generator, Optional
18
+
19
+ import intersystems_iris.dbapi._DBAPI as iris_dbapi
20
+ from intersystems_iris.dbapi._DBAPI import Connection
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ class IRISConnectionPool:
26
+ """
27
+ Thread-safe connection pool for IRIS database.
28
+
29
+ This class implements connection pooling to support concurrent RAG API requests
30
+ without exhausting the IRIS connection limit.
31
+
32
+ Features:
33
+ - Configurable pool size with overflow capacity
34
+ - Connection recycling to prevent stale connections
35
+ - Pre-ping validation for connection health
36
+ - Thread-safe acquire/release operations
37
+ """
38
+
39
+ def __init__(
40
+ self,
41
+ host: str,
42
+ port: int,
43
+ namespace: str,
44
+ username: str,
45
+ password: str,
46
+ pool_size: int = 20,
47
+ max_overflow: int = 10,
48
+ pool_recycle: int = 3600,
49
+ pool_pre_ping: bool = True,
50
+ pool_timeout: int = 30,
51
+ ):
52
+ """
53
+ Initialize IRIS connection pool.
54
+
55
+ Args:
56
+ host: IRIS server hostname
57
+ port: IRIS server port
58
+ namespace: IRIS namespace
59
+ username: Database username
60
+ password: Database password
61
+ pool_size: Number of base connections (default: 20)
62
+ max_overflow: Additional connections under load (default: 10)
63
+ pool_recycle: Seconds before recycling connection (default: 3600)
64
+ pool_pre_ping: Validate connection before use (default: True)
65
+ pool_timeout: Seconds to wait for connection (default: 30)
66
+ """
67
+ self.host = host
68
+ self.port = port
69
+ self.namespace = namespace
70
+ self.username = username
71
+ self.password = password
72
+ self.pool_size = pool_size
73
+ self.max_overflow = max_overflow
74
+ self.pool_recycle = pool_recycle
75
+ self.pool_pre_ping = pool_pre_ping
76
+ self.pool_timeout = pool_timeout
77
+
78
+ # Connection tracking
79
+ self._available_connections: list[Connection] = []
80
+ self._in_use_connections: set[Connection] = set()
81
+ self._connection_timestamps: Dict[Connection, float] = {}
82
+
83
+ # Thread safety
84
+ import threading
85
+
86
+ self._lock = threading.Lock()
87
+
88
+ # Metrics
89
+ self._total_connections_created = 0
90
+ self._total_connections_recycled = 0
91
+ self._total_acquisitions = 0
92
+
93
+ logger.info(
94
+ f"Initialized IRIS connection pool: "
95
+ f"pool_size={pool_size}, max_overflow={max_overflow}, "
96
+ f"recycle={pool_recycle}s, pre_ping={pool_pre_ping}"
97
+ )
98
+
99
+ def _create_connection(self) -> Connection:
100
+ """
101
+ Create a new IRIS database connection.
102
+
103
+ Returns:
104
+ IRIS connection object
105
+
106
+ Raises:
107
+ Exception: If connection creation fails
108
+ """
109
+ try:
110
+ connection = iris_dbapi.connect(
111
+ hostname=self.host,
112
+ port=self.port,
113
+ namespace=self.namespace,
114
+ username=self.username,
115
+ password=self.password,
116
+ )
117
+
118
+ import time
119
+
120
+ self._connection_timestamps[connection] = time.time()
121
+ self._total_connections_created += 1
122
+
123
+ logger.debug(
124
+ f"Created new IRIS connection "
125
+ f"(total created: {self._total_connections_created})"
126
+ )
127
+
128
+ return connection
129
+
130
+ except Exception as e:
131
+ logger.error(f"Failed to create IRIS connection: {e}")
132
+ raise
133
+
134
+ def _validate_connection(self, connection: Connection) -> bool:
135
+ """
136
+ Validate that a connection is still alive.
137
+
138
+ Args:
139
+ connection: Connection to validate
140
+
141
+ Returns:
142
+ True if connection is valid, False otherwise
143
+ """
144
+ if not self.pool_pre_ping:
145
+ return True
146
+
147
+ try:
148
+ cursor = connection.cursor()
149
+ cursor.execute("SELECT 1")
150
+ cursor.close()
151
+ return True
152
+
153
+ except Exception as e:
154
+ logger.warning(f"Connection validation failed: {e}")
155
+ return False
156
+
157
+ def _should_recycle_connection(self, connection: Connection) -> bool:
158
+ """
159
+ Check if connection should be recycled based on age.
160
+
161
+ Args:
162
+ connection: Connection to check
163
+
164
+ Returns:
165
+ True if connection should be recycled, False otherwise
166
+ """
167
+ if connection not in self._connection_timestamps:
168
+ return False
169
+
170
+ import time
171
+
172
+ age = time.time() - self._connection_timestamps[connection]
173
+ return age >= self.pool_recycle
174
+
175
+ def _recycle_connection(self, connection: Connection) -> None:
176
+ """
177
+ Close and remove a connection from the pool.
178
+
179
+ Args:
180
+ connection: Connection to recycle
181
+ """
182
+ try:
183
+ connection.close()
184
+ self._total_connections_recycled += 1
185
+
186
+ if connection in self._connection_timestamps:
187
+ del self._connection_timestamps[connection]
188
+
189
+ logger.debug(
190
+ f"Recycled IRIS connection "
191
+ f"(total recycled: {self._total_connections_recycled})"
192
+ )
193
+
194
+ except Exception as e:
195
+ logger.warning(f"Error recycling connection: {e}")
196
+
197
+ def acquire(self) -> Connection:
198
+ """
199
+ Acquire a connection from the pool.
200
+
201
+ This method will:
202
+ 1. Try to get an available connection from the pool
203
+ 2. Validate the connection if pre_ping is enabled
204
+ 3. Recycle connections that are too old
205
+ 4. Create new connections if pool is not full
206
+
207
+ Returns:
208
+ Active IRIS connection
209
+
210
+ Raises:
211
+ Exception: If unable to acquire connection within timeout
212
+ """
213
+ import time
214
+
215
+ start_time = time.time()
216
+
217
+ while (time.time() - start_time) < self.pool_timeout:
218
+ with self._lock:
219
+ # Try to get existing connection
220
+ if self._available_connections:
221
+ connection = self._available_connections.pop()
222
+
223
+ # Check if connection should be recycled
224
+ if self._should_recycle_connection(connection):
225
+ self._recycle_connection(connection)
226
+ continue
227
+
228
+ # Validate connection health
229
+ if not self._validate_connection(connection):
230
+ self._recycle_connection(connection)
231
+ continue
232
+
233
+ # Connection is good - mark as in use
234
+ self._in_use_connections.add(connection)
235
+ self._total_acquisitions += 1
236
+
237
+ logger.debug(
238
+ f"Acquired connection from pool "
239
+ f"(available: {len(self._available_connections)}, "
240
+ f"in_use: {len(self._in_use_connections)})"
241
+ )
242
+
243
+ return connection
244
+
245
+ # No available connections - check if we can create new one
246
+ total_connections = (
247
+ len(self._available_connections) + len(self._in_use_connections)
248
+ )
249
+
250
+ if total_connections < (self.pool_size + self.max_overflow):
251
+ # Create new connection
252
+ connection = self._create_connection()
253
+ self._in_use_connections.add(connection)
254
+ self._total_acquisitions += 1
255
+
256
+ logger.debug(
257
+ f"Created new connection "
258
+ f"(total: {total_connections + 1}/{self.pool_size + self.max_overflow})"
259
+ )
260
+
261
+ return connection
262
+
263
+ # Pool is full - wait briefly and retry
264
+ time.sleep(0.1)
265
+
266
+ # Timeout exceeded
267
+ raise Exception(
268
+ f"Timeout acquiring connection from pool after {self.pool_timeout}s "
269
+ f"(pool_size: {self.pool_size}, max_overflow: {self.max_overflow}, "
270
+ f"in_use: {len(self._in_use_connections)})"
271
+ )
272
+
273
+ def release(self, connection: Connection) -> None:
274
+ """
275
+ Release a connection back to the pool.
276
+
277
+ Args:
278
+ connection: Connection to release
279
+ """
280
+ with self._lock:
281
+ if connection in self._in_use_connections:
282
+ self._in_use_connections.remove(connection)
283
+
284
+ # Check if we should recycle instead of returning to pool
285
+ if self._should_recycle_connection(connection):
286
+ self._recycle_connection(connection)
287
+ else:
288
+ self._available_connections.append(connection)
289
+
290
+ logger.debug(
291
+ f"Released connection to pool "
292
+ f"(available: {len(self._available_connections)}, "
293
+ f"in_use: {len(self._in_use_connections)})"
294
+ )
295
+
296
+ @contextmanager
297
+ def get_connection(self) -> Generator[Connection, None, None]:
298
+ """
299
+ Context manager for acquiring and releasing connections.
300
+
301
+ Usage:
302
+ >>> pool = IRISConnectionPool(...)
303
+ >>> with pool.get_connection() as conn:
304
+ ... cursor = conn.cursor()
305
+ ... cursor.execute("SELECT * FROM table")
306
+
307
+ Yields:
308
+ IRIS connection
309
+ """
310
+ connection = self.acquire()
311
+ try:
312
+ yield connection
313
+ finally:
314
+ self.release(connection)
315
+
316
+ def close_all(self) -> None:
317
+ """
318
+ Close all connections in the pool.
319
+
320
+ This should be called on application shutdown.
321
+ """
322
+ with self._lock:
323
+ # Close available connections
324
+ for connection in self._available_connections:
325
+ self._recycle_connection(connection)
326
+ self._available_connections.clear()
327
+
328
+ # Close in-use connections (should be empty at shutdown)
329
+ for connection in self._in_use_connections:
330
+ self._recycle_connection(connection)
331
+ self._in_use_connections.clear()
332
+
333
+ logger.info(
334
+ f"Closed all connections in pool "
335
+ f"(total created: {self._total_connections_created}, "
336
+ f"recycled: {self._total_connections_recycled}, "
337
+ f"acquisitions: {self._total_acquisitions})"
338
+ )
339
+
340
+ def get_stats(self) -> Dict[str, Any]:
341
+ """
342
+ Get connection pool statistics.
343
+
344
+ Returns:
345
+ Dictionary with pool metrics
346
+ """
347
+ with self._lock:
348
+ return {
349
+ "pool_size": self.pool_size,
350
+ "max_overflow": self.max_overflow,
351
+ "available_connections": len(self._available_connections),
352
+ "in_use_connections": len(self._in_use_connections),
353
+ "total_connections": len(self._available_connections)
354
+ + len(self._in_use_connections),
355
+ "total_created": self._total_connections_created,
356
+ "total_recycled": self._total_connections_recycled,
357
+ "total_acquisitions": self._total_acquisitions,
358
+ }
359
+
360
+
361
+ # Global connection pool instance (initialized by FastAPI app)
362
+ _global_pool: Optional[IRISConnectionPool] = None
363
+
364
+
365
+ def get_pool() -> IRISConnectionPool:
366
+ """
367
+ Get the global connection pool instance.
368
+
369
+ Returns:
370
+ Global IRISConnectionPool instance
371
+
372
+ Raises:
373
+ RuntimeError: If pool has not been initialized
374
+ """
375
+ if _global_pool is None:
376
+ raise RuntimeError(
377
+ "Connection pool not initialized. "
378
+ "Call initialize_pool() during application startup."
379
+ )
380
+ return _global_pool
381
+
382
+
383
+ def initialize_pool(
384
+ host: Optional[str] = None,
385
+ port: Optional[int] = None,
386
+ namespace: Optional[str] = None,
387
+ username: Optional[str] = None,
388
+ password: Optional[str] = None,
389
+ **kwargs: Any,
390
+ ) -> IRISConnectionPool:
391
+ """
392
+ Initialize the global connection pool.
393
+
394
+ This should be called during FastAPI application startup.
395
+
396
+ Args:
397
+ host: IRIS server hostname (default: from IRIS_HOST env)
398
+ port: IRIS server port (default: from IRIS_PORT env)
399
+ namespace: IRIS namespace (default: from IRIS_NAMESPACE env)
400
+ username: Database username (default: from IRIS_USERNAME env)
401
+ password: Database password (default: from IRIS_PASSWORD env)
402
+ **kwargs: Additional IRISConnectionPool arguments
403
+
404
+ Returns:
405
+ Initialized global connection pool
406
+ """
407
+ global _global_pool
408
+
409
+ # Get connection parameters from environment if not provided
410
+ host = host or os.getenv("IRIS_HOST", "localhost")
411
+ port = port or int(os.getenv("IRIS_PORT", "1972"))
412
+ namespace = namespace or os.getenv("IRIS_NAMESPACE", "USER")
413
+ username = username or os.getenv("IRIS_USERNAME", "_SYSTEM")
414
+ password = password or os.getenv("IRIS_PASSWORD", "SYS")
415
+
416
+ _global_pool = IRISConnectionPool(
417
+ host=host,
418
+ port=port,
419
+ namespace=namespace,
420
+ username=username,
421
+ password=password,
422
+ **kwargs,
423
+ )
424
+
425
+ logger.info(
426
+ f"Initialized global connection pool: {host}:{port}/{namespace} "
427
+ f"(pool_size={_global_pool.pool_size}, "
428
+ f"max_overflow={_global_pool.max_overflow})"
429
+ )
430
+
431
+ return _global_pool
432
+
433
+
434
+ def close_pool() -> None:
435
+ """
436
+ Close the global connection pool.
437
+
438
+ This should be called during FastAPI application shutdown.
439
+ """
440
+ global _global_pool
441
+
442
+ if _global_pool is not None:
443
+ _global_pool.close_all()
444
+ _global_pool = None
445
+ logger.info("Closed global connection pool")
common/database.py ADDED
@@ -0,0 +1,141 @@
1
+ """
2
+ Database connectivity module for RAG Templates.
3
+ Provides simple database connection checking functionality.
4
+ """
5
+
6
+ import logging
7
+ import os
8
+ import time
9
+ from typing import Optional
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ def wait_for_database(
15
+ max_attempts: int = 30, wait_seconds: int = 2, database_url: Optional[str] = None
16
+ ) -> bool:
17
+ """
18
+ Wait for database to become available.
19
+
20
+ Args:
21
+ max_attempts: Maximum number of connection attempts
22
+ wait_seconds: Seconds to wait between attempts
23
+ database_url: Database URL to connect to (optional)
24
+
25
+ Returns:
26
+ True if database is available, False otherwise
27
+ """
28
+ # Get database URL from environment if not provided
29
+ if not database_url:
30
+ database_url = os.getenv("DATABASE_URL", "iris://localhost:1974/USER")
31
+
32
+ logger.info(f"Waiting for database at {database_url}")
33
+
34
+ for attempt in range(max_attempts):
35
+ try:
36
+ # Try to import and use IRIS connection
37
+ # Parse the database URL to get connection parameters
38
+ # Format: iris://username:password@host:port/namespace
39
+ import urllib.parse
40
+
41
+ from .iris_client import IRISClient
42
+
43
+ parsed = urllib.parse.urlparse(database_url)
44
+
45
+ host = parsed.hostname or "localhost"
46
+ port = parsed.port or 1974
47
+ username = parsed.username or "demo"
48
+ password = parsed.password or "demo"
49
+ namespace = parsed.path.lstrip("/") or "USER"
50
+
51
+ # Create IRIS client and test connection
52
+ client = IRISClient(
53
+ host=host,
54
+ port=port,
55
+ username=username,
56
+ password=password,
57
+ namespace=namespace,
58
+ )
59
+
60
+ # Test the connection with a simple query
61
+ result = client.execute_query("SELECT 1")
62
+ if result:
63
+ logger.info("Database connection successful")
64
+ return True
65
+
66
+ except ImportError:
67
+ # Fallback to basic socket check if IRIS client is not available
68
+ try:
69
+ import socket
70
+ import urllib.parse
71
+
72
+ parsed = urllib.parse.urlparse(database_url)
73
+ host = parsed.hostname or "localhost"
74
+ port = parsed.port or 1974
75
+
76
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
77
+ sock.settimeout(5)
78
+ result = sock.connect_ex((host, port))
79
+ sock.close()
80
+
81
+ if result == 0:
82
+ logger.info(f"Database port {port} is accessible on {host}")
83
+ return True
84
+
85
+ except Exception as e:
86
+ logger.warning(f"Socket connection check failed: {e}")
87
+
88
+ except Exception as e:
89
+ logger.warning(f"Database connection attempt {attempt + 1} failed: {e}")
90
+
91
+ if attempt < max_attempts - 1:
92
+ logger.info(f"Retrying in {wait_seconds} seconds...")
93
+ time.sleep(wait_seconds)
94
+
95
+ logger.error(f"Failed to connect to database after {max_attempts} attempts")
96
+ return False
97
+
98
+
99
+ def check_database_health() -> dict:
100
+ """
101
+ Check database health and return status information.
102
+
103
+ Returns:
104
+ Dictionary with health status information
105
+ """
106
+ try:
107
+ from .iris_client import IRISClient
108
+
109
+ # Get connection parameters from environment
110
+ database_url = os.getenv("DATABASE_URL", "iris://localhost:1974/USER")
111
+
112
+ import urllib.parse
113
+
114
+ parsed = urllib.parse.urlparse(database_url)
115
+
116
+ host = parsed.hostname or "localhost"
117
+ port = parsed.port or 1972
118
+ username = parsed.username or "demo"
119
+ password = parsed.password or "demo"
120
+ namespace = parsed.path.lstrip("/") or "USER"
121
+
122
+ # Create IRIS client and test connection
123
+ client = IRISClient(
124
+ host=host,
125
+ port=port,
126
+ username=username,
127
+ password=password,
128
+ namespace=namespace,
129
+ )
130
+
131
+ # Test with a simple query
132
+ result = client.execute_query("SELECT 1")
133
+
134
+ return {
135
+ "status": "healthy" if result else "unhealthy",
136
+ "database_url": f"iris://{host}:{port}/{namespace}",
137
+ "connection_test": "passed" if result else "failed",
138
+ }
139
+
140
+ except Exception as e:
141
+ return {"status": "unhealthy", "error": str(e), "connection_test": "failed"}