Coverage for /home/admin/Documents/AI/applications/lexigram-dev/lexigram/experimental/ai/lexigram-ai-rag/src/lexigram/ai/rag/di/provider.py: 45%
159 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 07:19 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 07:19 +0800
1"""DI Provider for the RAG (Retrieval Augmented Generation) subsystem."""
3from __future__ import annotations
5import importlib.util
6from typing import TYPE_CHECKING, Any
8from lexigram.ai.rag.config import RAGConfig
9from lexigram.contracts.core.health import HealthCheckResult, HealthStatus
10from lexigram.contracts.exceptions.container import UnresolvableDependencyError
11from lexigram.contracts.exceptions.provider import ModuleVisibilityError
12from lexigram.di.provider import Provider, ProviderPriority
13from lexigram.logging import (
14 get_logger,
15)
17if TYPE_CHECKING:
18 from lexigram.contracts.core.di import (
19 BootContainerProtocol,
20 ContainerRegistrarProtocol,
21 )
23logger = get_logger(__name__)
26def _llmlingua_available() -> bool:
27 """Check if llmlingua package is installed."""
28 try:
29 return importlib.util.find_spec("llmlingua") is not None
30 except (ValueError, AttributeError):
31 return False
34def _flashrank_available() -> bool:
35 """Check if flashrank package is installed."""
36 try:
37 return importlib.util.find_spec("flashrank") is not None
38 except (ValueError, AttributeError):
39 return False
42class RAGProvider(Provider):
43 """Registers RAG pipeline services and strategy registries with the DI container."""
45 name = "rag"
46 priority = ProviderPriority.DOMAIN
47 config_key: str | None = "ai_rag"
48 config_model: type | None = RAGConfig
50 def __init__(self, config: RAGConfig | None = None) -> None:
51 super().__init__()
52 self._requested_config = config
53 self._config = config or RAGConfig()
55 async def register(self, container: ContainerRegistrarProtocol) -> None:
56 self._config = self._requested_config or self._config or RAGConfig()
57 container.singleton(RAGConfig, instance=self._config)
59 from lexigram.ai.rag.context_compression.strategy_registry import (
60 CompressionStrategyRegistry,
61 )
63 compression_registry = CompressionStrategyRegistry.with_defaults()
65 # Register LLMLingua-2 handler if available
66 if _llmlingua_available():
67 from lexigram.ai.rag.context_compression.strategies.llmlingua2 import (
68 LLMLingua2CompressorStrategy,
69 LLMLingua2StrategyHandler,
70 )
72 compression_registry.register(
73 LLMLingua2StrategyHandler(LLMLingua2CompressorStrategy())
74 )
75 logger.debug("llmlingua2_compressor_registered")
76 else:
77 logger.debug("llmlingua2_compressor_skipped_not_installed")
79 container.singleton(CompressionStrategyRegistry, instance=compression_registry)
81 from lexigram.ai.rag.hyde.strategy_registry import HyDEStrategyRegistry
83 hyde_registry = HyDEStrategyRegistry.with_defaults()
84 container.singleton(HyDEStrategyRegistry, instance=hyde_registry)
86 from lexigram.ai.rag.reasoning.strategy_registry import (
87 ReasoningStrategyRegistry,
88 )
90 reasoning_registry = ReasoningStrategyRegistry.with_defaults()
91 container.singleton(ReasoningStrategyRegistry, instance=reasoning_registry)
93 from lexigram.ai.rag.pipeline.stages.synthesis_registry import (
94 SynthesisStrategyRegistry,
95 )
97 synthesis_registry = SynthesisStrategyRegistry.with_defaults()
98 container.singleton(SynthesisStrategyRegistry, instance=synthesis_registry)
100 from lexigram.ai.rag.chunking.strategy_registry import (
101 ChunkingStrategyRegistry,
102 )
104 chunking_registry = ChunkingStrategyRegistry.with_defaults()
105 container.singleton(ChunkingStrategyRegistry, instance=chunking_registry)
107 from lexigram.ai.rag.reranking.strategy_registry import (
108 RerankingStrategyRegistry,
109 )
111 reranking_registry = RerankingStrategyRegistry.with_defaults()
113 # Register FlashRank handler if available
114 if _flashrank_available():
115 from lexigram.ai.rag.reranking.strategies.flashrank import (
116 FlashRankStrategyHandler,
117 )
119 reranking_registry.register(FlashRankStrategyHandler())
120 logger.debug("flashrank_reranker_registered")
121 else:
122 logger.debug("flashrank_reranker_skipped_not_installed")
124 container.singleton(RerankingStrategyRegistry, instance=reranking_registry)
126 await self._discover_strategies(container)
128 await self._register_knowledge_graph(container)
130 logger.info("rag_provider_registered")
132 async def _register_knowledge_graph(
133 self,
134 container: ContainerRegistrarProtocol,
135 ) -> None:
136 """Register the default in-memory knowledge graph singleton."""
137 from lexigram.ai.rag.knowledge_graph.core import KnowledgeGraph
139 container.singleton(KnowledgeGraph, instance=KnowledgeGraph())
140 logger.info("rag_knowledge_graph_in_memory")
142 async def _discover_strategies(self, container: ContainerRegistrarProtocol) -> None:
143 """Auto-discover RAG strategy providers via entry-points."""
144 import importlib.metadata as _meta
146 from lexigram.di.provider import Provider as _Provider
148 for group in (
149 "lexigram.chunking.strategies",
150 "lexigram.retrieval.strategies",
151 ):
152 eps = _meta.entry_points(group=group)
153 for ep in eps:
154 try:
155 candidate = ep.load()
156 except (ImportError, AttributeError, TypeError, ValueError) as exc:
157 logger.warning(
158 "rag_strategy_ep_load_failed",
159 name=ep.name,
160 group=group,
161 error=str(exc),
162 )
163 continue
164 if isinstance(candidate, type) and issubclass(candidate, _Provider):
165 await candidate().register(container)
166 logger.info("rag_strategy_ep_loaded", name=ep.name, group=group)
167 else:
168 logger.debug("rag_strategy_ep_skipped", name=ep.name, group=group)
170 async def _maybe_wrap_with_tenancy(
171 self,
172 container: BootContainerProtocol,
173 ) -> None:
174 """Register ``TenantScopedRAGPipeline`` when tenancy is enabled."""
175 if not self._config.tenancy.enabled:
176 return
178 from lexigram.ai.rag.tenancy import TenantScopedRAGPipeline
179 from lexigram.ai.rag.tenancy.resolver import (
180 TemplatedTenantCollectionResolver,
181 )
182 from lexigram.contracts.ai.rag import RAGPipelineProtocol
183 from lexigram.primitives.context import Context
185 ctx = await container.resolve(Context)
186 resolver: Any = TemplatedTenantCollectionResolver()
187 base_config = self._config
189 async def _default_factory(config: RAGConfig) -> RAGPipelineProtocol:
190 from lexigram.ai.rag.config import PipelineConfig
191 from lexigram.ai.rag.pipeline import RAGPipeline
192 from lexigram.ai.rag.pipeline._stage_factory import (
193 build_pipeline_stages,
194 )
196 pipeline_cfg = PipelineConfig()
197 stages = build_pipeline_stages(pipeline_cfg)
198 return RAGPipeline(config=pipeline_cfg, stages=stages)
200 container.singleton(
201 RAGPipelineProtocol,
202 instance=TenantScopedRAGPipeline(
203 base_config=base_config,
204 resolver=resolver,
205 ctx=ctx,
206 pipeline_factory=_default_factory,
207 ),
208 )
209 logger.info("rag_tenancy_enabled")
211 async def boot(self, container: BootContainerProtocol) -> None:
212 """Boot RAG provider — wire optional integrations."""
213 self._booted_container = container
215 # Optional: tenancy
216 await self._maybe_wrap_with_tenancy(container)
218 # Optional: working memory (from lexigram-ai-memory)
219 working_memory = None
220 try:
221 from lexigram.contracts.ai.memory import WorkingMemoryProtocol
223 working_memory = await container.resolve(WorkingMemoryProtocol)
224 logger.debug("rag_working_memory_available")
225 except (
226 LookupError,
227 RuntimeError,
228 AttributeError,
229 ImportError,
230 TypeError,
231 ModuleVisibilityError,
232 UnresolvableDependencyError,
233 ):
234 logger.debug("rag_working_memory_not_available")
236 if working_memory is not None:
237 self._working_memory = working_memory
239 # Optional: graph store
240 graph_store_available = False
241 try:
242 from lexigram.contracts.data.graph.protocols import GraphStoreProtocol
244 graph_store = await container.resolve_optional(GraphStoreProtocol)
245 graph_store_available = graph_store is not None
246 except (
247 LookupError,
248 RuntimeError,
249 AttributeError,
250 ImportError,
251 TypeError,
252 ModuleVisibilityError,
253 UnresolvableDependencyError,
254 ):
255 pass
257 logger.info(
258 "rag_provider_booted",
259 working_memory=working_memory is not None,
260 graph_store=graph_store_available,
261 )
263 async def shutdown(self) -> None:
264 pass
266 async def health_check(self, timeout: float = 5.0) -> HealthCheckResult:
267 """Check RAG provider health — verifies embedding service and vector store.
269 Returns:
270 :class:`~lexigram.contracts.core.HealthCheckResult` with status
271 ``healthy`` when all configured dependencies are reachable, or
272 ``degraded``/``unhealthy`` otherwise.
273 """
274 details: dict = {
275 "embedding_service": "unconfigured",
276 "vector_store": "unconfigured",
277 }
278 overall = HealthStatus.HEALTHY
280 container: BootContainerProtocol | None = getattr(
281 self, "_booted_container", None
282 )
283 if container is None:
284 return HealthCheckResult(
285 component="rag",
286 status=HealthStatus.HEALTHY,
287 details={"note": "RAG provider not yet booted"},
288 )
290 # Check embedding client if available
291 try:
292 from lexigram.contracts.ai import EmbeddingClientProtocol
294 embedding_client = await container.resolve_optional(EmbeddingClientProtocol)
295 if embedding_client is not None and hasattr(
296 embedding_client, "health_check"
297 ):
298 emb_result = await embedding_client.health_check(timeout=timeout)
299 if hasattr(emb_result, "status"):
300 details["embedding_service"] = emb_result.status
301 if emb_result.status != HealthStatus.HEALTHY:
302 overall = HealthStatus.DEGRADED
303 else:
304 details["embedding_service"] = "ok"
305 else:
306 details["embedding_service"] = "not_configured"
307 except (LookupError, RuntimeError, AttributeError) as exc:
308 details["embedding_service"] = f"error: {exc}"
309 overall = HealthStatus.DEGRADED
311 # Check vector store if available
312 try:
313 from lexigram.contracts.ai import DocumentVectorStoreProtocol
315 vector_store = await container.resolve_optional(DocumentVectorStoreProtocol)
316 if vector_store is not None and hasattr(vector_store, "health_check"):
317 vs_result = await vector_store.health_check(timeout=timeout)
318 if hasattr(vs_result, "status"):
319 details["vector_store"] = vs_result.status
320 if vs_result.status != HealthStatus.HEALTHY:
321 overall = HealthStatus.DEGRADED
322 else:
323 details["vector_store"] = "ok"
324 else:
325 details["vector_store"] = "not_configured"
326 except (LookupError, RuntimeError, AttributeError) as exc:
327 details["vector_store"] = f"error: {exc}"
328 overall = HealthStatus.DEGRADED
330 return HealthCheckResult(
331 component="rag",
332 status=overall,
333 details=details,
334 )
337__all__ = ["RAGProvider"]