Spaces:
Runtime error
Runtime error
Commit ·
3dc597f
0
Parent(s):
vapi-sathi embed space: bge-m3 FastAPI /embed
Browse files- Dockerfile +20 -0
- README.md +51 -0
- app.py +55 -0
- requirements.txt +3 -0
Dockerfile
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Hugging Face Docker Space — runs as user 1000, app must listen on 7860.
|
| 2 |
+
FROM python:3.11-slim
|
| 3 |
+
|
| 4 |
+
RUN useradd -m -u 1000 user
|
| 5 |
+
USER user
|
| 6 |
+
ENV HOME=/home/user \
|
| 7 |
+
PATH=/home/user/.local/bin:$PATH \
|
| 8 |
+
HF_HOME=/home/user/.cache/huggingface
|
| 9 |
+
WORKDIR /home/user/app
|
| 10 |
+
|
| 11 |
+
COPY --chown=user requirements.txt .
|
| 12 |
+
RUN pip install --user --no-cache-dir -r requirements.txt
|
| 13 |
+
|
| 14 |
+
# Bake the model into the image so the first request is not a ~2 GB cold pull.
|
| 15 |
+
RUN python -c "from sentence_transformers import SentenceTransformer; SentenceTransformer('BAAI/bge-m3')"
|
| 16 |
+
|
| 17 |
+
COPY --chown=user app.py .
|
| 18 |
+
|
| 19 |
+
EXPOSE 7860
|
| 20 |
+
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Vapi Sathi Embeddings
|
| 3 |
+
emoji: 🔎
|
| 4 |
+
colorFrom: indigo
|
| 5 |
+
colorTo: blue
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
pinned: false
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# vapi-sathi embeddings
|
| 12 |
+
|
| 13 |
+
FastAPI `/embed` service hosting **BAAI/bge-m3** (1024-dim, multilingual incl.
|
| 14 |
+
Hindi/Gujarati). Offloads embedding off the NestJS API box (see
|
| 15 |
+
`../INFRA-DEPLOYMENT-PLAN.md`). Deployed on a **free CPU Basic** Hugging Face
|
| 16 |
+
Space (2 vCPU / 16 GB).
|
| 17 |
+
|
| 18 |
+
## API
|
| 19 |
+
|
| 20 |
+
`GET /` → health: `{ "status": "ok", "model": "...", "ready": true }`
|
| 21 |
+
|
| 22 |
+
`POST /embed`
|
| 23 |
+
```json
|
| 24 |
+
{ "texts": ["latest mandi prices", "ભાવ સમાચાર"] }
|
| 25 |
+
```
|
| 26 |
+
→
|
| 27 |
+
```json
|
| 28 |
+
{ "embeddings": [[...1024 floats...], [...]], "dim": 1024, "model": "BAAI/bge-m3" }
|
| 29 |
+
```
|
| 30 |
+
|
| 31 |
+
Vectors are L2-normalized → use cosine / dot directly. bge-m3 needs **no
|
| 32 |
+
query/passage prefix**.
|
| 33 |
+
|
| 34 |
+
## Auth
|
| 35 |
+
|
| 36 |
+
If `EMBED_TOKEN` is set (Space secret), every `/embed` call must send
|
| 37 |
+
`Authorization: Bearer <EMBED_TOKEN>`. Leave unset only for private testing.
|
| 38 |
+
The NestJS client reads `EMBEDDING_SERVICE_URL` + `EMBEDDING_SERVICE_TOKEN`.
|
| 39 |
+
|
| 40 |
+
## Keep-warm
|
| 41 |
+
|
| 42 |
+
Free Spaces sleep after ~48 h idle. Worker traffic keeps it warm; otherwise
|
| 43 |
+
ping `GET /` on a cron (e.g. a NestJS `@Cron`).
|
| 44 |
+
|
| 45 |
+
## Local test
|
| 46 |
+
|
| 47 |
+
```bash
|
| 48 |
+
docker build -t vapi-embed . && docker run -p 7860:7860 vapi-embed
|
| 49 |
+
curl -s localhost:7860/embed -H 'content-type: application/json' \
|
| 50 |
+
-d '{"texts":["hello","नमस्ते"]}' | head -c 200
|
| 51 |
+
```
|
app.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from contextlib import asynccontextmanager
|
| 3 |
+
|
| 4 |
+
from fastapi import FastAPI, Header, HTTPException
|
| 5 |
+
from pydantic import BaseModel
|
| 6 |
+
from sentence_transformers import SentenceTransformer
|
| 7 |
+
|
| 8 |
+
MODEL_NAME = os.getenv("EMBED_MODEL", "BAAI/bge-m3")
|
| 9 |
+
EMBED_TOKEN = os.getenv("EMBED_TOKEN") # optional shared secret
|
| 10 |
+
|
| 11 |
+
_model: SentenceTransformer | None = None
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@asynccontextmanager
|
| 15 |
+
async def lifespan(_: FastAPI):
|
| 16 |
+
global _model
|
| 17 |
+
_model = SentenceTransformer(MODEL_NAME, device="cpu")
|
| 18 |
+
yield
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
app = FastAPI(lifespan=lifespan)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class EmbedRequest(BaseModel):
|
| 25 |
+
texts: list[str]
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class EmbedResponse(BaseModel):
|
| 29 |
+
embeddings: list[list[float]]
|
| 30 |
+
dim: int
|
| 31 |
+
model: str
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
@app.get("/")
|
| 35 |
+
def health():
|
| 36 |
+
return {"status": "ok", "model": MODEL_NAME, "ready": _model is not None}
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
# sync `def` → FastAPI runs it in a threadpool, so the blocking encode() does
|
| 40 |
+
# not stall the event loop.
|
| 41 |
+
@app.post("/embed", response_model=EmbedResponse)
|
| 42 |
+
def embed(req: EmbedRequest, authorization: str | None = Header(default=None)):
|
| 43 |
+
if EMBED_TOKEN and authorization != f"Bearer {EMBED_TOKEN}":
|
| 44 |
+
raise HTTPException(status_code=401, detail="unauthorized")
|
| 45 |
+
if _model is None:
|
| 46 |
+
raise HTTPException(status_code=503, detail="model loading")
|
| 47 |
+
if not req.texts:
|
| 48 |
+
raise HTTPException(status_code=400, detail="texts required")
|
| 49 |
+
|
| 50 |
+
vecs = _model.encode(req.texts, normalize_embeddings=True, batch_size=16)
|
| 51 |
+
return EmbedResponse(
|
| 52 |
+
embeddings=[v.tolist() for v in vecs],
|
| 53 |
+
dim=len(vecs[0]),
|
| 54 |
+
model=MODEL_NAME,
|
| 55 |
+
)
|
requirements.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.115.*
|
| 2 |
+
uvicorn[standard]==0.32.*
|
| 3 |
+
sentence-transformers==3.*
|