Spaces:
Running
Running
Initial Docker Space: FastAPI /route endpoint, eager-load multilingual-next-v1 + Markov-2
6ed68a1 verified | """ | |
| Agent Tool Router — REST API. | |
| A minimal FastAPI wrapper around the open-source `agent_tool_router.Router`, | |
| designed to be deployed on a HuggingFace Spaces Docker template. One model | |
| is loaded into memory at startup; routing is a single sparse-times-dense | |
| matmul plus an optional Markov rerank, so request latency stays in the | |
| ~10-50ms band on free-tier CPU. | |
| Run locally: | |
| pip install agent-tool-router[encoder] | |
| pip install fastapi uvicorn | |
| uvicorn router.api.main:app --reload | |
| Environment variables: | |
| ATR_DEFAULT_MODEL short name of the model returned when `model` is | |
| omitted from the request. Defaults to | |
| `baseline-v1-desc-hybrid-multilingual-next-v1`. | |
| ATR_PRELOAD_MODELS comma-separated list of short names to eager-load | |
| at startup. Defaults to ATR_DEFAULT_MODEL only. | |
| """ | |
| from __future__ import annotations | |
| import logging | |
| import os | |
| import time | |
| from typing import Optional | |
| from fastapi import FastAPI, HTTPException | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from pydantic import BaseModel, Field | |
| from agent_tool_router.router import Router | |
| log = logging.getLogger("router.api") | |
| logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s") | |
| DEFAULT_MODEL = os.getenv("ATR_DEFAULT_MODEL", "baseline-v1-desc-hybrid-multilingual-next-v1") | |
| PRELOAD_MODELS = [ | |
| m.strip() for m in os.getenv("ATR_PRELOAD_MODELS", DEFAULT_MODEL).split(",") if m.strip() | |
| ] | |
| API_VERSION = "0.4.0" | |
| app = FastAPI( | |
| title="Agent Tool Router API", | |
| description=( | |
| "Pick the right tools for an agent task. Open-source router trained on " | |
| "14K public agent traces. Top-3 = 77.4% on next-tool retrieval (n=2094 " | |
| "held-out). github.com/dalek-ai/agent-tool-router" | |
| ), | |
| version=API_VERSION, | |
| docs_url="/docs", | |
| redoc_url=None, | |
| ) | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_methods=["GET", "POST"], | |
| allow_headers=["*"], | |
| ) | |
| ROUTERS: dict[str, Router] = {} | |
| def load_models() -> None: | |
| for name in PRELOAD_MODELS: | |
| log.info("Loading model %s ...", name) | |
| t0 = time.time() | |
| ROUTERS[name] = Router.from_pretrained(name) | |
| log.info(" %s loaded in %.1fs", name, time.time() - t0) | |
| class RouteRequest(BaseModel): | |
| task: str = Field(..., min_length=1, max_length=2000, description="The user task to route.") | |
| history: Optional[list[str]] = Field( | |
| default=None, | |
| description="Previously called tool names in this trace, oldest first. Enables Markov rerank.", | |
| ) | |
| k: int = Field(default=3, ge=1, le=50, description="Number of tools to return.") | |
| model: Optional[str] = Field( | |
| default=None, | |
| description=f"Pretrained model name. Defaults to '{DEFAULT_MODEL}'.", | |
| ) | |
| class RoutedTool(BaseModel): | |
| name: str | |
| score: float | |
| class RouteResponse(BaseModel): | |
| tools: list[RoutedTool] | |
| model: str | |
| latency_ms: float | |
| def index() -> dict: | |
| return { | |
| "service": "agent-tool-router", | |
| "version": API_VERSION, | |
| "default_model": DEFAULT_MODEL, | |
| "loaded_models": list(ROUTERS.keys()), | |
| "endpoints": { | |
| "POST /route": "Route a task to top-k tools", | |
| "GET /models": "List loaded models", | |
| "GET /health": "Liveness probe", | |
| "GET /docs": "OpenAPI / Swagger UI", | |
| }, | |
| "links": { | |
| "github": "https://github.com/dalek-ai/agent-tool-router", | |
| "huggingface": "https://huggingface.co/dalek-ai", | |
| }, | |
| "example": { | |
| "curl": ( | |
| "curl -X POST $URL/route -H 'Content-Type: application/json' " | |
| "-d '{\"task\": \"annule ma commande et rembourse-moi\", \"k\": 3}'" | |
| ) | |
| }, | |
| } | |
| def health() -> dict: | |
| return {"status": "ok", "models_loaded": len(ROUTERS)} | |
| def models() -> dict: | |
| return {"loaded": list(ROUTERS.keys()), "default": DEFAULT_MODEL} | |
| def route(req: RouteRequest) -> RouteResponse: | |
| model_name = req.model or DEFAULT_MODEL | |
| router = ROUTERS.get(model_name) | |
| if router is None: | |
| raise HTTPException( | |
| status_code=404, | |
| detail=f"Model '{model_name}' not loaded. Available: {list(ROUTERS.keys())}", | |
| ) | |
| t0 = time.time() | |
| results = router.route( | |
| req.task, | |
| k=req.k, | |
| return_scores=True, | |
| history=req.history, | |
| ) | |
| latency_ms = (time.time() - t0) * 1000.0 | |
| return RouteResponse( | |
| tools=[RoutedTool(name=r.tool, score=float(r.score)) for r in results], | |
| model=model_name, | |
| latency_ms=round(latency_ms, 2), | |
| ) | |