Spaces:
Sleeping
Sleeping
Commit Β·
5eb188c
1
Parent(s): 155573a
Fix HF Space runtime by adding persistent FastAPI server
Browse files- .gitignore +3 -1
- Dockerfile +13 -3
- backend/__init__.py +8 -0
- backend/app/__init__.py +6 -0
- backend/app/main.py +95 -0
.gitignore
CHANGED
|
@@ -1,6 +1,8 @@
|
|
| 1 |
node_modules/
|
| 2 |
.next/
|
| 3 |
-
|
|
|
|
|
|
|
| 4 |
venv/
|
| 5 |
__pycache__/
|
| 6 |
*.pyc
|
|
|
|
| 1 |
node_modules/
|
| 2 |
.next/
|
| 3 |
+
lifeline-ai/node_modules/
|
| 4 |
+
lifeline-ai/.next/
|
| 5 |
+
lifeline-ai/backend/.venv/
|
| 6 |
venv/
|
| 7 |
__pycache__/
|
| 8 |
*.pyc
|
Dockerfile
CHANGED
|
@@ -26,6 +26,12 @@ COPY requirements.txt .
|
|
| 26 |
RUN pip install --no-cache-dir --upgrade pip \
|
| 27 |
&& pip install --no-cache-dir -r requirements.txt
|
| 28 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
# ββ Copy application source safely ββββββββββββββββββββββββββββββββββββββββββββ
|
| 30 |
# Copying the full project avoids file-not-found build breaks and is HF-Spaces-friendly.
|
| 31 |
COPY . .
|
|
@@ -40,6 +46,10 @@ ENV OPENAI_API_KEY="EMPTY" \
|
|
| 40 |
MODEL_NAME="gpt-4o-mini" \
|
| 41 |
HF_TOKEN=""
|
| 42 |
|
| 43 |
-
# ββ
|
| 44 |
-
|
| 45 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
RUN pip install --no-cache-dir --upgrade pip \
|
| 27 |
&& pip install --no-cache-dir -r requirements.txt
|
| 28 |
|
| 29 |
+
# If a backend requirements file exists (lifeline-ai/backend/requirements.txt), install it too
|
| 30 |
+
COPY lifeline-ai/backend/requirements.txt ./lifeline-backend-requirements.txt
|
| 31 |
+
RUN if [ -f ./lifeline-backend-requirements.txt ]; then \
|
| 32 |
+
pip install --no-cache-dir -r ./lifeline-backend-requirements.txt; \
|
| 33 |
+
fi
|
| 34 |
+
|
| 35 |
# ββ Copy application source safely ββββββββββββββββββββββββββββββββββββββββββββ
|
| 36 |
# Copying the full project avoids file-not-found build breaks and is HF-Spaces-friendly.
|
| 37 |
COPY . .
|
|
|
|
| 46 |
MODEL_NAME="gpt-4o-mini" \
|
| 47 |
HF_TOKEN=""
|
| 48 |
|
| 49 |
+
# ββ Expose the port expected by Hugging Face Spaces and run the backend web server
|
| 50 |
+
EXPOSE 7860
|
| 51 |
+
|
| 52 |
+
# Default command: start the FastAPI backend (lifeline-ai backend) on port 7860.
|
| 53 |
+
# This keeps the container running as a web service compatible with Spaces (sdk: docker).
|
| 54 |
+
# It will cd into the backend folder and run uvicorn. Override at runtime as needed.
|
| 55 |
+
CMD ["sh", "-c", "uvicorn backend.app.main:app --host 0.0.0.0 --port 7860"]
|
backend/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Backend package for LifeLine AI HTTP API.
|
| 2 |
+
|
| 3 |
+
This package provides a small FastAPI wrapper that exposes the existing
|
| 4 |
+
benchmarking/inference logic via HTTP so the container remains running on
|
| 5 |
+
Hugging Face Spaces (sdk: docker).
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
__all__ = ["app"]
|
backend/app/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""backend.app package
|
| 2 |
+
|
| 3 |
+
Contains the FastAPI application module (main.py).
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
__all__ = ["main"]
|
backend/app/main.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
import logging
|
| 5 |
+
from typing import Any, Dict, List, Optional
|
| 6 |
+
|
| 7 |
+
from fastapi import FastAPI, HTTPException
|
| 8 |
+
from pydantic import BaseModel
|
| 9 |
+
|
| 10 |
+
# Import the existing inference runner so we can reuse run_episode
|
| 11 |
+
try:
|
| 12 |
+
# inference.py lives at project root and exports run_episode
|
| 13 |
+
import inference
|
| 14 |
+
except Exception:
|
| 15 |
+
inference = None
|
| 16 |
+
|
| 17 |
+
app = FastAPI(title="LifeLine AI API", version="1.0.0")
|
| 18 |
+
|
| 19 |
+
# Configure logging for startup visibility
|
| 20 |
+
logger = logging.getLogger("lifeline.backend")
|
| 21 |
+
logging.basicConfig(level=logging.INFO)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class BenchmarkRequest(BaseModel):
|
| 25 |
+
agent: str = "rules" # 'rules' or 'llm'
|
| 26 |
+
difficulty: str = "all" # easy|medium|hard|all
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
@app.on_event("startup")
|
| 30 |
+
def startup_event() -> None:
|
| 31 |
+
logger.info("LifeLine AI API started successfully")
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
@app.get("/health")
|
| 35 |
+
def health() -> Dict[str, str]:
|
| 36 |
+
return {"status": "ok", "project": "LifeLine AI"}
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
@app.post("/run-benchmark")
|
| 40 |
+
def run_benchmark(req: BenchmarkRequest) -> Dict[str, Any]:
|
| 41 |
+
"""Run the existing inference benchmark and return structured JSON results.
|
| 42 |
+
|
| 43 |
+
This re-uses the `run_episode` function from `inference.py` so the benchmark
|
| 44 |
+
logic remains in one place and is usable both as CLI and via the HTTP API.
|
| 45 |
+
"""
|
| 46 |
+
|
| 47 |
+
if inference is None:
|
| 48 |
+
raise HTTPException(status_code=500, detail="inference module not available")
|
| 49 |
+
|
| 50 |
+
agent = req.agent.lower()
|
| 51 |
+
if agent not in ("rules", "llm"):
|
| 52 |
+
raise HTTPException(status_code=400, detail="agent must be 'rules' or 'llm'")
|
| 53 |
+
|
| 54 |
+
difficulty = req.difficulty.lower()
|
| 55 |
+
if difficulty not in ("easy", "medium", "hard", "all"):
|
| 56 |
+
raise HTTPException(status_code=400, detail="difficulty must be easy|medium|hard|all")
|
| 57 |
+
|
| 58 |
+
# Prepare OpenAI client when requested
|
| 59 |
+
client: Optional[Any] = None
|
| 60 |
+
if agent == "llm":
|
| 61 |
+
try:
|
| 62 |
+
from openai import OpenAI as OpenAIClient # type: ignore
|
| 63 |
+
except Exception as exc: # pragma: no cover - import/runtime error
|
| 64 |
+
raise HTTPException(status_code=500, detail=f"OpenAI client not available: {exc}")
|
| 65 |
+
|
| 66 |
+
api_key = os.getenv("OPENAI_API_KEY", "EMPTY")
|
| 67 |
+
hf_token = os.getenv("HF_TOKEN", "")
|
| 68 |
+
if hf_token and api_key == "EMPTY":
|
| 69 |
+
api_key = hf_token
|
| 70 |
+
|
| 71 |
+
try:
|
| 72 |
+
client = OpenAIClient(api_key=api_key, base_url=os.getenv("API_BASE_URL", "https://api.openai.com/v1"))
|
| 73 |
+
except Exception as exc:
|
| 74 |
+
raise HTTPException(status_code=500, detail=f"Failed to initialize OpenAI client: {exc}")
|
| 75 |
+
|
| 76 |
+
# Determine difficulties list
|
| 77 |
+
difficulties: List[str]
|
| 78 |
+
if difficulty == "all":
|
| 79 |
+
difficulties = inference.ALL_DIFFICULTIES
|
| 80 |
+
else:
|
| 81 |
+
difficulties = [difficulty]
|
| 82 |
+
|
| 83 |
+
results = []
|
| 84 |
+
for diff in difficulties:
|
| 85 |
+
# Each run returns structured dicts as defined by inference.run_episode
|
| 86 |
+
try:
|
| 87 |
+
res = inference.run_episode(client, diff, agent)
|
| 88 |
+
except Exception as exc:
|
| 89 |
+
# Bubble up error details while keeping API stable
|
| 90 |
+
raise HTTPException(status_code=500, detail=f"Benchmark run failed: {exc}")
|
| 91 |
+
results.append(res)
|
| 92 |
+
|
| 93 |
+
avg_score = sum(r["score"] for r in results) / len(results) if results else 0.0
|
| 94 |
+
|
| 95 |
+
return {"average_score": avg_score, "results": results}
|