Spaces:
Sleeping
Sleeping
File size: 9,567 Bytes
914512c 7438bc0 914512c 7438bc0 914512c 7438bc0 914512c 7438bc0 914512c 7438bc0 914512c df5dcd7 914512c df5dcd7 914512c df5dcd7 914512c | 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 | """bioai.web.api -- FastAPI backend for the Biopesticide-AI web UI.
Replaces the Gradio UI with a clean REST API + static frontend architecture.
Endpoints:
GET / -> serve static index.html
GET /api/health -> {"status": "ok"}
GET /api/status -> backend status (model, LLM, device, species counts)
POST /api/design -> run the design pipeline, return JSON results
GET /static/* -> static assets (CSS, JS, images)
The frontend (static/index.html) calls these endpoints via fetch() and
renders the UI with vanilla JS. No build step required.
"""
from __future__ import annotations
import argparse
import json
import sys
import time
from pathlib import Path
# Make sure we can import bioai from anywhere
_PROJECT_ROOT = Path(__file__).resolve().parents[2]
if str(_PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(_PROJECT_ROOT))
from fastapi import FastAPI, HTTPException
from fastapi.responses import HTMLResponse, JSONResponse, FileResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
# NOTE: BiopesticideOrchestrator is imported lazily inside get_orchestrator()
# to avoid loading torch + models at module import time. This keeps the
# FastAPI app startup fast (<2 seconds) so HuggingFace doesn't kill the
# container before uvicorn binds to port 7860.
from bioai.sequence_utils import SAFETY_SPECIES, PEST_SPECIES
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Singleton orchestrator
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
_ORCHESTRATOR = None # lazily initialized in get_orchestrator()
def get_orchestrator():
"""Lazily initialize the orchestrator on first API call.
This avoids loading torch + models at import time, keeping FastAPI
startup fast enough for HuggingFace's 60-second container timeout.
"""
global _ORCHESTRATOR
if _ORCHESTRATOR is None:
print("[api] initializing orchestrator (first request)...", flush=True)
from bioai.orchestrator import BiopesticideOrchestrator
_ORCHESTRATOR = BiopesticideOrchestrator()
print(f"[api] backend = {type(_ORCHESTRATOR.ranker.sirna_model).__name__}, degraded_mode = {_ORCHESTRATOR.degraded_mode}", flush=True)
return _ORCHESTRATOR
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Pydantic models
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
class DesignRequest(BaseModel):
user_text: str
top_k: int = 10
pest_species: str | None = None # if provided, skip LLM parsing
class SimulateRequest(BaseModel):
candidates: list
pest_species: str = "unknown"
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# FastAPI app
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
STATIC_DIR = Path(__file__).parent / "static"
app = FastAPI(
title="Biopesticide-AI",
description="dsRNA biopesticide design pipeline API",
version="2.0.0",
)
# Mount static files (CSS, JS, images)
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
@app.get("/api/health")
async def health():
return {"status": "ok"}
@app.get("/api/status")
async def status():
"""Return backend status for the frontend status strip."""
orch = get_orchestrator()
model_class = type(orch.ranker.sirna_model).__name__
# Check if checkpoints actually loaded (not random init)
from bioai.paths import SIRNA_CHECKPOINT, PINN_CHECKPOINT
sirna_loaded = SIRNA_CHECKPOINT.exists()
pinn_loaded = PINN_CHECKPOINT.exists()
return {
"model_backend": "Caduceus-Ph-1" if model_class == "CaduceusAdapter" else "Dilated CNN (HyenaDNA-inspired)",
"llm_status": "Ollama Llama 3.2 3B (local)" if not orch.degraded_mode else "Degraded mode (Ollama not running)",
"llm_active": not orch.degraded_mode,
"device": str(orch.ranker.device),
"safety_species_count": len(SAFETY_SPECIES),
"pest_species_count": len(PEST_SPECIES),
"safety_species": SAFETY_SPECIES,
"pest_species": PEST_SPECIES,
"checkpoints": {
"sirna_loaded": sirna_loaded,
"pinn_loaded": pinn_loaded,
"sirna_path": str(SIRNA_CHECKPOINT),
"pinn_path": str(PINN_CHECKPOINT),
},
}
@app.post("/api/design")
async def design(req: DesignRequest):
"""Run the design pipeline and return results as JSON."""
if not req.user_text or not req.user_text.strip():
raise HTTPException(status_code=400, detail="user_text is required")
orch = get_orchestrator()
t0 = time.time()
try:
result = orch.design(req.user_text, top_k=int(req.top_k), pest_species_override=req.pest_species)
except Exception as e:
import traceback
raise HTTPException(
status_code=500,
detail=f"Pipeline error: {type(e).__name__}: {e}\n{traceback.format_exc()}"
)
elapsed = time.time() - t0
# Build response with metadata
response = {
"elapsed_seconds": round(elapsed, 2),
"pest_report": result.get("pest_report", {}),
"candidates": result.get("candidates", []),
"safety_cards": result.get("safety_cards", ""),
"regulatory_memo": result.get("regulatory_memo", ""),
"n_transcripts": result.get("n_transcripts", 0),
"n_precursors": result.get("n_precursors", 0),
"n_sirnas": result.get("n_sirnas", 0),
"total_cost_estimate": result.get("total_cost_estimate", 0.0),
"degraded_mode": result.get("degraded_mode", True),
}
return JSONResponse(content=response)
@app.get("/")
async def index():
"""Serve the main HTML page."""
index_path = STATIC_DIR / "index.html"
if not index_path.exists():
raise HTTPException(status_code=404, detail="index.html not found")
return HTMLResponse(content=index_path.read_text(encoding="utf-8"), status_code=200)
@app.post("/api/simulate")
async def simulate(req: SimulateRequest):
"""Run the virtual wet-lab simulation on the top candidates.
Each candidate should have: sirna_seq, efficacy, half_life_hours.
Returns 1000-trial Monte Carlo results per candidate.
"""
from bioai.simulation.wet_lab import WetLabSimulator, result_to_dict
if not req.candidates:
raise HTTPException(status_code=400, detail="candidates is required")
sim = WetLabSimulator(n_trials=1000, rng_seed=42)
results = sim.simulate_batch(req.candidates, pest_species=req.pest_species)
return JSONResponse(content={
"trials": [result_to_dict(r) for r in results],
"pest_species": req.pest_species,
"n_trials_per_candidate": 1000,
"stages": [
"delivery", "uptake", "dicer", "risc_loading", "cleavage", "phenotype"
],
})
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Entry point
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def main():
parser = argparse.ArgumentParser(description="Biopesticide-AI Web UI (FastAPI)")
parser.add_argument("--host", default="0.0.0.0", help="bind host (default 0.0.0.0)")
parser.add_argument("--port", type=int, default=7860, help="bind port (default 7860)")
parser.add_argument("--reload", action="store_true", help="auto-reload on file changes (dev mode)")
args = parser.parse_args()
import uvicorn
# DON'T pre-initialize the orchestrator here. On HuggingFace Spaces, the
# 14-species k-mer indexing + Ollama probe takes >60 seconds on the slow
# CPU, which causes HF to kill the container before uvicorn binds to port
# 7860. Instead, we start uvicorn immediately and initialize the
# orchestrator lazily on the first API request. The first request will
# be slow (~60s) but the server stays alive.
print("[api] launching uvicorn immediately (orchestrator initializes lazily on first request)")
print(f"[api] launching on http://{args.host}:{args.port}")
uvicorn.run(
"bioai.web.api:app",
host=args.host,
port=args.port,
reload=args.reload,
log_level="info",
)
if __name__ == "__main__":
main()
|