File size: 7,119 Bytes
ba4ad33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
import json
import os
from functools import lru_cache

from .engine import ChemicalSearchEngine
from .utils import smiles_to_image_url
from .generation import generate_explanations_batch

# Global variables
engine = None
dataset = None
index_path = None
data_path = None


def get_data_paths():
    """Get paths for data and index files."""
    global data_path, index_path
    
    base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    data_path = os.path.join(base_dir, "data", "compounds.json")
    index_path = os.path.join(base_dir, "data", "compounds_index.pkl")
    
    return data_path, index_path


def data_exists():
    """Check if compounds.json exists and has data."""
    data_path, _ = get_data_paths()
    
    if not os.path.exists(data_path):
        return False
    
    try:
        with open(data_path) as f:
            data = json.load(f)
        return len(data) > 0
    except:
        return False


def index_exists():
    """Check if FAISS index exists."""
    _, index_path = get_data_paths()
    return os.path.exists(index_path)


def initialize_engine():
    """
    Centralized initialization:
    1. Check if compounds.json exists → use it
    2. If not → run ingest.py
    3. Check if FAISS index exists → load it
    4. If not → build it
    """
    global engine, dataset
    
    if engine is not None and dataset is not None:
        return engine
    
    data_path, index_path = get_data_paths()
    
    print("\n" + "="*60)
    print("[STARTUP] Initializing Chemical RAG System (Centralized)")
    print("="*60 + "\n")
    
    # Step 1: Check for compounds.json
    if not data_exists():
        print("[WARNING] compounds.json not found or empty")
        print("[INFO] Running ingestion pipeline...")
        from . import ingest_handler
        ingest_handler.run_ingestion()
    
    # Step 2: Load compounds.json
    print(f"[LOAD] Loading compounds from {data_path}...")
    try:
        with open(data_path) as f:
            dataset = json.load(f)
        print(f"[SUCCESS] Loaded {len(dataset)} compounds")
    except Exception as e:
        raise RuntimeError(f"Failed to load compounds: {e}")
    
    # Step 3: Initialize engine
    smiles_list = [d["smiles"] for d in dataset]
    engine = ChemicalSearchEngine(bit_size=2048)
    
    # Step 4: Check for FAISS index
    if index_exists():
        print(f"[LOAD] FAISS index found at {index_path}")
        if engine.load_index(index_path):
            print("[SUCCESS] FAISS-IVF index loaded successfully")
            print(f"[SUCCESS] Generation layer enabled with Llama-3.1-8B")
            print("="*60 + "\n")
            return engine
    
    # Step 5: Build and save FAISS index
    print("[BUILD] Building FAISS-IVF index (this may take a few minutes)...")
    engine.add_compounds(smiles_list, metadata_list=dataset)
    engine.save_index(index_path)
    
    print(f"[SUCCESS] Generation layer enabled with Llama-3.1-8B")
    print(f"[SUCCESS] FAISS index saved to {index_path}")
    print("="*60 + "\n")
    return engine


def _search_internal(smiles: str, top_k: int, base_url: str = None, include_explanation: bool = True):
    """
    Internal search using FAISS-IVF with optional LLM explanation.
    
    Args:
        smiles: Query SMILES string
        top_k: Number of results (1-100)
        base_url: Base URL for image URLs (e.g. https://example.com)
        include_explanation: Whether to generate LLM explanations
    
    Returns:
        List of enriched results with metadata and optional explanations
    """
    global engine, dataset
    
    if engine is None:
        raise RuntimeError("Engine not initialized. Call initialize_engine() first.")
    
    if dataset is None:
        raise RuntimeError("Dataset not loaded. Call initialize_engine() first.")
    
    # FAISS-IVF search
    results = engine.search(smiles, top_k)
    
    if not results:
        return []  # Empty results is valid - just no similar compounds found
    
    enriched = []
    for r in results:
        cid = r["metadata"].get("cid")
        enriched.append({
            "smiles": r["smiles"],
            "similarity_score": r["similarity_score"],
            "image": smiles_to_image_url(r["smiles"], base_url=base_url),
            "cid": str(cid) if cid is not None else None,  # Convert to string for schema
            "name": r["metadata"].get("name"),
            "mw": r["metadata"].get("mw"),
            "explanation": None
        })
    
    # Generate LLM explanations if requested
    if include_explanation:
        enriched = generate_explanations_batch(smiles, enriched)
    
    return enriched


@lru_cache(maxsize=1000)
def cached_search(smiles: str, top_k: int, explain: bool = True):
    """
    Cached search results using FAISS-IVF with optional explanations.
    """
    results = _search_internal(smiles, top_k, include_explanation=explain)
    return tuple([tuple(sorted(r.items())) for r in results])


def get_search_results(smiles: str, top_k: int = 3, explain: bool = True, base_url: str = None):
    """
    Main search function: FAISS-IVF retrieval with optional LLM generation.
    
    Args:
        smiles: Query SMILES string
        top_k: Number of results to return (default 3, max 100)
        explain: Whether to generate LLM explanations (default True)
        base_url: Base URL for image URLs (e.g. https://example.com)
    
    Returns:
        Tuple of (results_list, query_smiles)
    """
    results = _search_internal(smiles, top_k, base_url=base_url, include_explanation=explain)
    return results, smiles


def get_search_results_retrieval_only(smiles: str, top_k: int = 3, base_url: str = None):
    """
    Fast retrieval-only search (no LLM generation).
    
    Args:
        smiles: Query SMILES string
        top_k: Number of results to return (default 3, max 100)
        base_url: Base URL for image URLs (e.g. https://example.com)
    
    Returns:
        Tuple of (results_list, query_smiles)
    """
    results = _search_internal(smiles, top_k, base_url=base_url, include_explanation=False)
    return results, smiles


def get_system_stats():
    """Get system statistics and status."""
    global dataset, engine
    
    if engine is None or dataset is None:
        return {
            "status": "uninitialized",
            "compounds": 0,
            "index_type": "FAISS-BinaryFlat",
            "index_exists": False
        }
    
    return {
        "status": "ready",
        "compounds": len(dataset),
        "index_type": "FAISS-BinaryFlat (Binary Flat Index)",
        "index_built": engine.index_built,
        "total_indexed": engine.total_compounds,
        "fingerprint_bits": engine.bit_size,
        "similarity_metric": "Tanimoto (exact)",
        "generation_model": "Llama-3.1-8B-Instruct",
        "generation_enabled": True,
        "endpoints": [
            "/search/retrieval-only - Fast retrieval only",
            "/search/full-rag - Retrieval + LLM explanation",
            "/stats - System statistics",
            "/health - Health check"
        ]
    }