File size: 5,570 Bytes
3ca1d38
 
 
 
 
 
 
 
 
 
9659593
3ca1d38
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9659593
3ca1d38
 
9659593
3ca1d38
 
 
 
 
 
 
9659593
3ca1d38
 
 
 
 
 
 
 
696f787
3ca1d38
 
 
 
 
696f787
3ca1d38
 
 
 
 
 
 
 
 
696f787
3ca1d38
 
 
 
 
 
 
 
 
 
696f787
3ca1d38
 
696f787
3ca1d38
 
 
 
 
9659593
3ca1d38
 
 
 
 
 
9659593
3ca1d38
 
9659593
3ca1d38
 
 
 
 
696f787
3ca1d38
696f787
3ca1d38
 
696f787
3ca1d38
 
9659593
3ca1d38
696f787
3ca1d38
696f787
3ca1d38
 
 
 
 
 
696f787
3ca1d38
 
 
 
 
696f787
3ca1d38
696f787
3ca1d38
 
696f787
3ca1d38
 
9659593
3ca1d38
696f787
3ca1d38
 
 
 
696f787
3ca1d38
 
 
 
 
 
 
 
9659593
3ca1d38
 
9659593
3ca1d38
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
696f787
3ca1d38
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
"""
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()