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
|
@@ -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
|