the-only-ashutosh commited on
Commit
3dc597f
·
0 Parent(s):

vapi-sathi embed space: bge-m3 FastAPI /embed

Browse files
Files changed (4) hide show
  1. Dockerfile +20 -0
  2. README.md +51 -0
  3. app.py +55 -0
  4. 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.*