"""FastAPI backend for the embedding tool selector (Docker Space, no Gradio). Builds the tool index once at startup, then serves: POST /api/search -> {query, ms, total, results:[{rank, name, description, domain, score, params}]} GET /api/health -> readiness (tool + domain counts) GET / -> static/index.html The model repo may be private/gated, so HF_TOKEN (a Space secret) is needed to pull it. uvicorn server:app --host 0.0.0.0 --port 7860 """ import os import time import traceback from fastapi import FastAPI from fastapi.responses import FileResponse, JSONResponse from fastapi.staticfiles import StaticFiles from pydantic import BaseModel import search as S STATIC = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static") INDEX, INDEX_ERROR = None, None try: print(f"[server] building tool index with {S.MODEL_ID} ...", flush=True) INDEX = S.load_or_build_index() print(f"[server] ready: {len(INDEX['tools'])} tools across {len(INDEX['domains'])} domains", flush=True) except Exception: INDEX_ERROR = traceback.format_exc() print("INDEX BUILD FAILED:\n" + INDEX_ERROR, flush=True) app = FastAPI(title="Embedding tool selection") class SearchRequest(BaseModel): q: str k: int = 5 @app.get("/api/health") def health(): return { "status": "ok" if INDEX is not None else "error", "model": S.MODEL_ID, "tools": (len(INDEX["tools"]) if INDEX is not None else 0), "domains": (INDEX["domains"] if INDEX is not None else []), } @app.post("/api/search") def do_search(req: SearchRequest): if INDEX is None: return JSONResponse({"error": INDEX_ERROR or "index not ready"}, status_code=503) q = (req.q or "").strip() total = len(INDEX["tools"]) if not q: return {"query": "", "ms": 0, "total": total, "results": []} t0 = time.time() results = S.search(INDEX, q, k=max(1, min(int(req.k), 12))) return {"query": q, "ms": int((time.time() - t0) * 1000), "total": total, "results": results} @app.get("/") def index(): return FileResponse(os.path.join(STATIC, "index.html")) app.mount("/", StaticFiles(directory=STATIC), name="static")