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,578 @@
1
+ """
2
+ RAG Templates Bridge Adapter - Unified Interface for kg-ticket-resolver Integration
3
+
4
+ This module provides a unified interface adapter that abstracts the RAG pipeline interface
5
+ for external consumption, enabling seamless switching between RAG techniques while
6
+ maintaining consistent response format for kg-ticket-resolver.
7
+
8
+ Architecture:
9
+ - Unified interface for all RAG pipelines (BasicRAG, CRAG, GraphRAG, BasicRAGReranking)
10
+ - Circuit breaker pattern for error handling
11
+ - Performance monitoring and metrics collection
12
+ - Incremental indexing support following LightRAG patterns
13
+ - Clean service boundaries with kg-ticket-resolver
14
+ """
15
+
16
+ import asyncio
17
+ import logging
18
+ import time
19
+ from contextlib import asynccontextmanager
20
+ from dataclasses import asdict, dataclass
21
+ from enum import Enum
22
+ from typing import Any, Dict, List, Optional, Union
23
+
24
+ from iris_rag.config.manager import ConfigurationManager
25
+ from iris_rag.core.base import RAGPipeline
26
+ from iris_rag.core.connection import ConnectionManager
27
+ from iris_rag.pipelines.basic import BasicRAGPipeline
28
+ from iris_rag.pipelines.basic_rerank import BasicRAGRerankingPipeline
29
+ from iris_rag.pipelines.crag import CRAGPipeline
30
+ from iris_rag.pipelines.graphrag import GraphRAGPipeline
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+
35
+ class RAGTechnique(Enum):
36
+ """Supported RAG techniques."""
37
+
38
+ BASIC = "basic"
39
+ CRAG = "crag"
40
+ GRAPH = "graphrag"
41
+ RERANKING = "basic_reranking"
42
+
43
+
44
+ class CircuitBreakerState(Enum):
45
+ """Circuit breaker states for fault tolerance."""
46
+
47
+ CLOSED = "closed"
48
+ OPEN = "open"
49
+ HALF_OPEN = "half_open"
50
+
51
+
52
+ @dataclass
53
+ class RAGResponse:
54
+ """Standardized response format for kg-ticket-resolver."""
55
+
56
+ answer: str
57
+ sources: List[Dict[str, Any]]
58
+ confidence_score: float
59
+ technique_used: str
60
+ processing_time_ms: float
61
+ metadata: Dict[str, Any]
62
+ error: Optional[str] = None
63
+
64
+
65
+ @dataclass
66
+ class CircuitBreakerConfig:
67
+ """Configuration for circuit breaker pattern."""
68
+
69
+ failure_threshold: int = 5
70
+ recovery_timeout: int = 60
71
+ half_open_max_calls: int = 3
72
+
73
+
74
+ @dataclass
75
+ class PerformanceMetrics:
76
+ """Performance tracking metrics."""
77
+
78
+ total_queries: int = 0
79
+ successful_queries: int = 0
80
+ failed_queries: int = 0
81
+ avg_response_time_ms: float = 0.0
82
+ p95_response_time_ms: float = 0.0
83
+ technique_usage: Dict[str, int] = None
84
+
85
+ def __post_init__(self):
86
+ if self.technique_usage is None:
87
+ self.technique_usage = {}
88
+
89
+
90
+ class RAGTemplatesBridge:
91
+ """
92
+ Unified interface adapter for RAG pipeline access from kg-ticket-resolver.
93
+
94
+ Provides:
95
+ - Seamless switching between RAG techniques
96
+ - Consistent response format
97
+ - Circuit breaker pattern for resilience
98
+ - Performance monitoring and metrics
99
+ - Incremental indexing support
100
+ """
101
+
102
+ def __init__(self, config_path: Optional[str] = None):
103
+ """
104
+ Initialize the RAG Templates Bridge.
105
+
106
+ Args:
107
+ config_path: Optional path to configuration file
108
+ """
109
+ self.config_manager = ConfigurationManager(config_path)
110
+ self.connection_manager = ConnectionManager()
111
+
112
+ # Load bridge configuration
113
+ self.bridge_config = self.config_manager.get("rag_integration", {})
114
+ self.default_technique = RAGTechnique(
115
+ self.bridge_config.get("default_technique", "basic")
116
+ )
117
+ self.fallback_technique = RAGTechnique(
118
+ self.bridge_config.get("fallback_technique", "basic")
119
+ )
120
+
121
+ # Initialize pipeline registry
122
+ self._pipelines: Dict[RAGTechnique, RAGPipeline] = {}
123
+ self._circuit_breakers: Dict[RAGTechnique, Dict] = {}
124
+
125
+ # Performance tracking
126
+ self.metrics = PerformanceMetrics()
127
+ self._response_times: List[float] = []
128
+
129
+ # Circuit breaker configuration
130
+ self.cb_config = CircuitBreakerConfig(
131
+ **self.bridge_config.get("circuit_breaker", {})
132
+ )
133
+
134
+ # Initialize available techniques
135
+ self._initialize_pipelines()
136
+ self._initialize_circuit_breakers()
137
+
138
+ logger.info(
139
+ f"RAG Templates Bridge initialized with techniques: {list(self._pipelines.keys())}"
140
+ )
141
+
142
+ def _initialize_pipelines(self) -> None:
143
+ """Initialize available RAG pipelines."""
144
+ pipeline_classes = {
145
+ RAGTechnique.BASIC: BasicRAGPipeline,
146
+ RAGTechnique.CRAG: CRAGPipeline,
147
+ RAGTechnique.GRAPH: GraphRAGPipeline,
148
+ RAGTechnique.RERANKING: BasicRAGRerankingPipeline,
149
+ }
150
+
151
+ for technique, pipeline_class in pipeline_classes.items():
152
+ try:
153
+ pipeline = pipeline_class(
154
+ connection_manager=self.connection_manager,
155
+ config_manager=self.config_manager,
156
+ )
157
+ self._pipelines[technique] = pipeline
158
+ logger.info(f"Initialized {technique.value} pipeline")
159
+ except Exception as e:
160
+ logger.warning(f"Failed to initialize {technique.value} pipeline: {e}")
161
+
162
+ def _initialize_circuit_breakers(self) -> None:
163
+ """Initialize circuit breakers for each technique."""
164
+ for technique in self._pipelines.keys():
165
+ self._circuit_breakers[technique] = {
166
+ "state": CircuitBreakerState.CLOSED,
167
+ "failure_count": 0,
168
+ "last_failure_time": 0,
169
+ "success_count": 0,
170
+ }
171
+
172
+ def _check_circuit_breaker(self, technique: RAGTechnique) -> bool:
173
+ """Check if circuit breaker allows execution."""
174
+ cb = self._circuit_breakers[technique]
175
+ current_time = time.time()
176
+
177
+ if cb["state"] == CircuitBreakerState.OPEN:
178
+ if current_time - cb["last_failure_time"] > self.cb_config.recovery_timeout:
179
+ cb["state"] = CircuitBreakerState.HALF_OPEN
180
+ cb["success_count"] = 0
181
+ logger.info(f"Circuit breaker for {technique.value} moved to HALF_OPEN")
182
+ else:
183
+ return False
184
+
185
+ elif cb["state"] == CircuitBreakerState.HALF_OPEN:
186
+ if cb["success_count"] >= self.cb_config.half_open_max_calls:
187
+ cb["state"] = CircuitBreakerState.CLOSED
188
+ cb["failure_count"] = 0
189
+ logger.info(f"Circuit breaker for {technique.value} moved to CLOSED")
190
+
191
+ return True
192
+
193
+ def _record_success(self, technique: RAGTechnique) -> None:
194
+ """Record successful execution."""
195
+ cb = self._circuit_breakers[technique]
196
+ if cb["state"] == CircuitBreakerState.HALF_OPEN:
197
+ cb["success_count"] += 1
198
+ elif cb["state"] == CircuitBreakerState.CLOSED:
199
+ cb["failure_count"] = 0
200
+
201
+ def _record_failure(self, technique: RAGTechnique) -> None:
202
+ """Record failed execution."""
203
+ cb = self._circuit_breakers[technique]
204
+ cb["failure_count"] += 1
205
+ cb["last_failure_time"] = time.time()
206
+
207
+ if cb["failure_count"] >= self.cb_config.failure_threshold:
208
+ cb["state"] = CircuitBreakerState.OPEN
209
+ logger.warning(f"Circuit breaker for {technique.value} moved to OPEN")
210
+
211
+ async def query(
212
+ self,
213
+ query_text: str,
214
+ technique: Optional[Union[str, RAGTechnique]] = None,
215
+ **kwargs,
216
+ ) -> RAGResponse:
217
+ """
218
+ Execute RAG query with specified or default technique.
219
+
220
+ Args:
221
+ query_text: The query to process
222
+ technique: RAG technique to use (defaults to configured default)
223
+ **kwargs: Additional arguments for the pipeline
224
+
225
+ Returns:
226
+ RAGResponse: Standardized response object
227
+ """
228
+ start_time = time.time()
229
+
230
+ # Normalize technique parameter
231
+ if isinstance(technique, str):
232
+ try:
233
+ technique = RAGTechnique(technique)
234
+ except ValueError:
235
+ processing_time = (time.time() - start_time) * 1000
236
+ error_response = RAGResponse(
237
+ answer="",
238
+ sources=[],
239
+ confidence_score=0.0,
240
+ technique_used="invalid",
241
+ processing_time_ms=processing_time,
242
+ metadata={"error_type": "InvalidTechnique"},
243
+ error=f"Invalid RAG technique: {technique}",
244
+ )
245
+ logger.error(f"Invalid technique specified: {technique}")
246
+ return error_response
247
+ elif technique is None:
248
+ technique = self.default_technique
249
+
250
+ # Track metrics
251
+ self.metrics.total_queries += 1
252
+ self.metrics.technique_usage[technique.value] = (
253
+ self.metrics.technique_usage.get(technique.value, 0) + 1
254
+ )
255
+
256
+ try:
257
+ # Check circuit breaker
258
+ if not self._check_circuit_breaker(technique):
259
+ logger.warning(
260
+ f"Circuit breaker open for {technique.value}, falling back"
261
+ )
262
+ technique = self.fallback_technique
263
+
264
+ # Get pipeline
265
+ pipeline = self._pipelines.get(technique)
266
+ if not pipeline:
267
+ raise ValueError(f"Pipeline not available: {technique.value}")
268
+
269
+ # Execute query
270
+ result = await self._execute_pipeline_query(pipeline, query_text, **kwargs)
271
+
272
+ # Record success
273
+ self._record_success(technique)
274
+ self.metrics.successful_queries += 1
275
+
276
+ # Calculate processing time
277
+ processing_time = (time.time() - start_time) * 1000
278
+ self._response_times.append(processing_time)
279
+
280
+ # Create standardized response
281
+ response = RAGResponse(
282
+ answer=result.get("answer", ""),
283
+ sources=result.get("sources", []),
284
+ confidence_score=result.get("confidence_score", 0.0),
285
+ technique_used=technique.value,
286
+ processing_time_ms=processing_time,
287
+ metadata={
288
+ "query_id": kwargs.get("query_id"),
289
+ "user_context": kwargs.get("user_context"),
290
+ "timestamp": time.time(),
291
+ },
292
+ )
293
+
294
+ logger.info(
295
+ f"Query processed successfully with {technique.value} in {processing_time:.2f}ms"
296
+ )
297
+ return response
298
+
299
+ except Exception as e:
300
+ # Record failure
301
+ self._record_failure(technique)
302
+ self.metrics.failed_queries += 1
303
+
304
+ processing_time = (time.time() - start_time) * 1000
305
+
306
+ error_response = RAGResponse(
307
+ answer="",
308
+ sources=[],
309
+ confidence_score=0.0,
310
+ technique_used=technique.value,
311
+ processing_time_ms=processing_time,
312
+ metadata={"error_type": type(e).__name__},
313
+ error=str(e),
314
+ )
315
+
316
+ logger.error(f"Query failed with {technique.value}: {e}")
317
+ return error_response
318
+
319
+ async def _execute_pipeline_query(
320
+ self, pipeline: RAGPipeline, query_text: str, **kwargs
321
+ ) -> Dict[str, Any]:
322
+ """Execute query on specific pipeline with timeout."""
323
+ timeout = self.bridge_config.get("query_timeout", 30)
324
+
325
+ try:
326
+ # Use asyncio.wait_for for timeout handling
327
+ result = await asyncio.wait_for(
328
+ asyncio.get_event_loop().run_in_executor(
329
+ None, pipeline.query, query_text
330
+ ),
331
+ timeout=timeout,
332
+ )
333
+ return result
334
+ except asyncio.TimeoutError:
335
+ raise Exception(f"Query timeout after {timeout} seconds")
336
+
337
+ def get_metrics(self) -> Dict[str, Any]:
338
+ """Get current performance metrics."""
339
+ # Update calculated metrics
340
+ if self._response_times:
341
+ self.metrics.avg_response_time_ms = sum(self._response_times) / len(
342
+ self._response_times
343
+ )
344
+ self.metrics.p95_response_time_ms = sorted(self._response_times)[
345
+ int(len(self._response_times) * 0.95)
346
+ ]
347
+
348
+ return asdict(self.metrics)
349
+
350
+ def get_health_status(self) -> Dict[str, Any]:
351
+ """Get health status of all components."""
352
+ health = {
353
+ "bridge_status": "healthy",
354
+ "pipelines": {},
355
+ "circuit_breakers": {},
356
+ "metrics": self.get_metrics(),
357
+ }
358
+
359
+ for technique, pipeline in self._pipelines.items():
360
+ try:
361
+ # Simple health check - attempt to get pipeline config
362
+ _ = pipeline.config_manager
363
+ health["pipelines"][technique.value] = "healthy"
364
+ except Exception as e:
365
+ health["pipelines"][technique.value] = f"unhealthy: {e}"
366
+
367
+ for technique, cb in self._circuit_breakers.items():
368
+ health["circuit_breakers"][technique.value] = cb["state"].value
369
+
370
+ return health
371
+
372
+ async def index_documents(
373
+ self,
374
+ documents: List[Dict[str, Any]],
375
+ technique: Optional[Union[str, RAGTechnique]] = None,
376
+ incremental: bool = True,
377
+ ) -> Dict[str, Any]:
378
+ """
379
+ Index documents using specified technique with incremental support.
380
+
381
+ Args:
382
+ documents: List of documents to index
383
+ technique: RAG technique to use for indexing
384
+ incremental: Whether to use incremental indexing
385
+
386
+ Returns:
387
+ Dict with indexing results
388
+ """
389
+ if isinstance(technique, str):
390
+ technique = RAGTechnique(technique)
391
+ elif technique is None:
392
+ technique = self.default_technique
393
+
394
+ pipeline = self._pipelines.get(technique)
395
+ if not pipeline:
396
+ raise ValueError(f"Pipeline not available: {technique.value}")
397
+
398
+ start_time = time.time()
399
+
400
+ try:
401
+ # Convert documents to required format
402
+ from iris_rag.core.models import Document
403
+
404
+ doc_objects = [
405
+ Document(
406
+ page_content=doc.get("content", ""),
407
+ metadata=doc.get("metadata", {}),
408
+ )
409
+ for doc in documents
410
+ ]
411
+
412
+ # Use pipeline's load_documents method
413
+ await asyncio.get_event_loop().run_in_executor(
414
+ None, pipeline.load_documents, "", documents=doc_objects
415
+ )
416
+
417
+ processing_time = (time.time() - start_time) * 1000
418
+
419
+ return {
420
+ "status": "success",
421
+ "documents_indexed": len(documents),
422
+ "technique_used": technique.value,
423
+ "processing_time_ms": processing_time,
424
+ "incremental": incremental,
425
+ }
426
+
427
+ except Exception as e:
428
+ logger.error(f"Document indexing failed: {e}")
429
+ return {
430
+ "status": "error",
431
+ "error": str(e),
432
+ "technique_used": technique.value,
433
+ "processing_time_ms": (time.time() - start_time) * 1000,
434
+ }
435
+
436
+ @asynccontextmanager
437
+ async def pipeline_context(self, technique: Union[str, RAGTechnique]):
438
+ """Context manager for safe pipeline access."""
439
+ if isinstance(technique, str):
440
+ technique = RAGTechnique(technique)
441
+
442
+ pipeline = self._pipelines.get(technique)
443
+ if not pipeline:
444
+ raise ValueError(f"Pipeline not available: {technique.value}")
445
+
446
+ try:
447
+ yield pipeline
448
+ except Exception:
449
+ self._record_failure(technique)
450
+ raise
451
+ else:
452
+ self._record_success(technique)
453
+
454
+ async def health_check(self) -> Dict[str, Any]:
455
+ """
456
+ Perform comprehensive health check of all components.
457
+
458
+ Returns:
459
+ Dict containing health status of bridge, pipelines, and dependencies
460
+ """
461
+ start_time = time.time()
462
+ health_status = {
463
+ "status": "healthy",
464
+ "timestamp": time.time(),
465
+ "check_duration_ms": 0.0,
466
+ "components": {
467
+ "bridge": {"status": "healthy"},
468
+ "pipelines": {},
469
+ "circuit_breakers": {},
470
+ "dependencies": {},
471
+ },
472
+ "metrics": await self.get_performance_metrics(),
473
+ }
474
+
475
+ # Check pipeline health
476
+ for technique, pipeline in self._pipelines.items():
477
+ try:
478
+ # Perform lightweight health check
479
+ if hasattr(pipeline, "health_check"):
480
+ await asyncio.get_event_loop().run_in_executor(
481
+ None, pipeline.health_check
482
+ )
483
+ else:
484
+ # Fallback: check if pipeline can access its configuration
485
+ _ = pipeline.config_manager
486
+ health_status["components"]["pipelines"][technique.value] = {
487
+ "status": "healthy"
488
+ }
489
+ except Exception as e:
490
+ health_status["components"]["pipelines"][technique.value] = {
491
+ "status": "unhealthy",
492
+ "error": str(e),
493
+ }
494
+ health_status["status"] = "degraded"
495
+
496
+ # Check circuit breaker states
497
+ for technique, cb in self._circuit_breakers.items():
498
+ cb_status = cb["state"].value
499
+ health_status["components"]["circuit_breakers"][technique.value] = {
500
+ "state": cb_status,
501
+ "failure_count": cb["failure_count"],
502
+ }
503
+ if cb_status == "open":
504
+ health_status["status"] = "degraded"
505
+
506
+ # Check dependencies
507
+ try:
508
+ # Test database connectivity
509
+ self.connection_manager.get_connection()
510
+ health_status["components"]["dependencies"]["database"] = {
511
+ "status": "healthy"
512
+ }
513
+ except Exception as e:
514
+ health_status["components"]["dependencies"]["database"] = {
515
+ "status": "unhealthy",
516
+ "error": str(e),
517
+ }
518
+ health_status["status"] = "unhealthy"
519
+
520
+ health_status["check_duration_ms"] = (time.time() - start_time) * 1000
521
+ return health_status
522
+
523
+ async def get_available_techniques(self) -> List[str]:
524
+ """
525
+ Get list of available RAG techniques.
526
+
527
+ Returns:
528
+ List of technique names that are currently available
529
+ """
530
+ available = []
531
+ for technique in self._pipelines.keys():
532
+ # Check if technique is operational (circuit breaker not open)
533
+ cb = self._circuit_breakers.get(technique, {})
534
+ if cb.get("state", CircuitBreakerState.CLOSED) != CircuitBreakerState.OPEN:
535
+ available.append(technique.value)
536
+
537
+ # Always include fallback technique if available
538
+ if (
539
+ self.fallback_technique.value not in available
540
+ and self.fallback_technique in self._pipelines
541
+ ):
542
+ available.append(self.fallback_technique.value)
543
+
544
+ return available
545
+
546
+ async def get_performance_metrics(self) -> Dict[str, Any]:
547
+ """
548
+ Get comprehensive performance metrics.
549
+
550
+ Returns:
551
+ Dict containing current performance metrics and SLO compliance
552
+ """
553
+ current_metrics = self.get_metrics()
554
+
555
+ # Add SLO compliance checks
556
+ config_targets = self.bridge_config.get("performance", {})
557
+ p95_target = config_targets.get("target_latency_p95_ms", 500)
558
+
559
+ slo_compliance = {
560
+ "p95_latency_compliant": current_metrics["p95_response_time_ms"]
561
+ <= p95_target,
562
+ "p95_target_ms": p95_target,
563
+ "success_rate": (
564
+ current_metrics["successful_queries"]
565
+ / max(current_metrics["total_queries"], 1)
566
+ )
567
+ * 100,
568
+ }
569
+
570
+ return {
571
+ **current_metrics,
572
+ "slo_compliance": slo_compliance,
573
+ "circuit_breaker_states": {
574
+ technique.value: cb["state"].value
575
+ for technique, cb in self._circuit_breakers.items()
576
+ },
577
+ "available_techniques": await self.get_available_techniques(),
578
+ }
common/__init__.py ADDED
@@ -0,0 +1 @@
1
+ # common module