T0X1N's picture
chore: codebase audit and fixes (ruff, mypy, pytest)
9659593
"""
MediGuard AI — Retriever Factory
Auto-selects the best available retriever backend:
1. OpenSearch (production) if OPENSEARCH_* env vars are set
2. FAISS (local) if vector store exists at data/vector_stores/
3. Raises error if neither is available
Usage:
from src.services.retrieval import get_retriever
retriever = get_retriever() # Auto-selects best backend
results = retriever.retrieve("What are normal glucose levels?")
"""
from __future__ import annotations
import logging
import os
from functools import lru_cache
from pathlib import Path
from src.services.retrieval.interface import BaseRetriever
logger = logging.getLogger(__name__)
# Detection flags
_OPENSEARCH_AVAILABLE = bool(os.environ.get("OPENSEARCH__HOST") or os.environ.get("OPENSEARCH_HOST"))
_FAISS_PATH = Path(os.environ.get("FAISS_VECTOR_STORE", "data/vector_stores"))
def _detect_backend() -> str:
"""
Detect the best available retriever backend.
Returns:
"opensearch" or "faiss"
Raises:
RuntimeError: If no backend is available
"""
# Priority 1: OpenSearch (production)
if _OPENSEARCH_AVAILABLE:
try:
from src.services.opensearch.client import make_opensearch_client
client = make_opensearch_client()
if client.ping():
logger.info("Auto-detected backend: OpenSearch (cluster reachable)")
return "opensearch"
else:
logger.warning("OpenSearch configured but not reachable, checking FAISS...")
except Exception as exc:
logger.warning("OpenSearch init failed (%s), checking FAISS...", exc)
# Priority 2: FAISS (local/HuggingFace)
faiss_index = _FAISS_PATH / "medical_knowledge.faiss"
if faiss_index.exists():
logger.info("Auto-detected backend: FAISS (index found at %s)", faiss_index)
return "faiss"
# Check alternative locations
alt_paths = [
Path("huggingface/data/vector_stores/medical_knowledge.faiss"),
Path("vector_stores/medical_knowledge.faiss"),
]
for alt in alt_paths:
if alt.exists():
logger.info("Auto-detected backend: FAISS (index found at %s)", alt)
return "faiss"
# No backend found
raise RuntimeError(
"No retriever backend available. Either:\n"
" - Set OPENSEARCH__HOST for OpenSearch\n"
" - Ensure data/vector_stores/medical_knowledge.faiss exists for FAISS\n"
"Run: python -m src.pdf_processor to create the FAISS index."
)
def make_retriever(
backend: str | None = None,
*,
embedding_model=None,
vector_store_path: str | None = None,
opensearch_client=None,
embedding_service=None,
) -> BaseRetriever:
"""
Create a retriever instance.
Args:
backend: "faiss", "opensearch", or None for auto-detect
embedding_model: Embedding model for FAISS
vector_store_path: Path to FAISS index directory
opensearch_client: OpenSearch client instance
embedding_service: Embedding service for OpenSearch vector search
Returns:
Configured BaseRetriever implementation
Raises:
RuntimeError: If the requested backend is unavailable
"""
if backend is None:
backend = _detect_backend()
backend = backend.lower()
if backend == "faiss":
from src.services.retrieval.faiss_retriever import FAISSRetriever
if embedding_model is None:
from src.llm_config import get_embedding_model
embedding_model = get_embedding_model()
path = vector_store_path or str(_FAISS_PATH)
# Try multiple paths
paths_to_try = [
path,
"huggingface/data/vector_stores",
"data/vector_stores",
]
for p in paths_to_try:
try:
return FAISSRetriever.from_local(p, embedding_model)
except FileNotFoundError:
continue
raise RuntimeError(f"FAISS index not found in any of: {paths_to_try}")
elif backend == "opensearch":
from src.services.retrieval.opensearch_retriever import OpenSearchRetriever
if opensearch_client is None:
from src.services.opensearch.client import make_opensearch_client
opensearch_client = make_opensearch_client()
return OpenSearchRetriever(
opensearch_client,
embedding_service=embedding_service,
)
else:
raise ValueError(f"Unknown retriever backend: {backend}")
@lru_cache(maxsize=1)
def get_retriever() -> BaseRetriever:
"""
Get a cached retriever instance (auto-detected backend).
This is the recommended way to get a retriever in most cases.
Uses LRU cache to avoid repeated initialization.
Returns:
Cached BaseRetriever implementation
"""
return make_retriever()
# Environment hints for deployment
def print_backend_info() -> None:
"""Print information about the detected retriever backend."""
try:
backend = _detect_backend()
retriever = make_retriever(backend)
print(f"Retriever Backend: {retriever.backend_name}")
print(f" Health: {'OK' if retriever.health() else 'DEGRADED'}")
print(f" Documents: {retriever.doc_count():,}")
except Exception as exc:
print("Retriever Backend: NOT AVAILABLE")
print(f" Error: {exc}")
if __name__ == "__main__":
# Quick diagnostic
logging.basicConfig(level=logging.INFO)
print_backend_info()