personaflow / scripts /deploy_hf.py
mozzic's picture
Deploy PersonaFlow AI
9d1557c verified
Raw
History Blame Contribute Delete
6.81 kB
"""Deploy PersonaFlow to a Hugging Face Docker Space.
What this does:
1. Creates / updates the Space `mozzic/personaflow` with Docker SDK
2. Sets GROQ_API_KEY as an encrypted Space secret (read from local .env)
3. Writes a Space-specific README.md (with required YAML frontmatter)
4. Adjusts the Dockerfile to COPY data + models (HF has no volumes)
5. Uploads code + data + models in a single batched push
Usage (inside the project root):
python scripts/deploy_hf.py
"""
from __future__ import annotations
import os
import shutil
import sys
import tempfile
from pathlib import Path
from huggingface_hub import HfApi, create_repo
ROOT = Path(__file__).resolve().parent.parent
HF_TOKEN = os.environ["HF_TOKEN"]
SPACE_ID = "mozzic/personaflow"
api = HfApi(token=HF_TOKEN)
def ensure_space() -> str:
print(f"[deploy] ensuring Space exists: {SPACE_ID}")
try:
create_repo(
repo_id=SPACE_ID,
repo_type="space",
space_sdk="docker",
token=HF_TOKEN,
exist_ok=True,
)
except Exception as e: # noqa: BLE001
print(f" ! create_repo error (probably already exists): {e}")
return f"https://huggingface.co/spaces/{SPACE_ID}"
def set_secrets():
print("[deploy] setting GROQ_API_KEY secret")
env_path = ROOT / ".env"
groq_key = None
for line in env_path.read_text(encoding="utf-8").splitlines():
if line.startswith("GROQ_API_KEY="):
groq_key = line.split("=", 1)[1].strip()
break
if not groq_key:
raise SystemExit("[deploy] GROQ_API_KEY not found in .env")
api.add_space_secret(
repo_id=SPACE_ID,
key="GROQ_API_KEY",
value=groq_key,
)
print(" -> GROQ_API_KEY secret set")
SPACE_README = """---
title: PersonaFlow AI
emoji: 🤖
colorFrom: green
colorTo: blue
sdk: docker
app_port: 8000
pinned: false
license: mit
short_description: Behavior-aware NG-context review simulation + recommendation
---
# PersonaFlow AI (live demo)
Behavior-aware review simulation (Task A) and contextual recommendation
(Task B) for Nigerian consumers. Built for the **DSN × BCT LLM Agent
Challenge 2026**.
Source code: https://github.com/Mozzicato/persona
## API endpoints
- `GET /health` — liveness probe
- `GET /users?limit=20` — sample user IDs you can plug in
- `POST /simulate-review` — Task A: persona-aware review + rating
- `POST /recommend` — Task B: behavior-aware recommendations (supports `cross_domain: true`)
- `POST /persona` — view full persona for a user
- Swagger UI: append `/docs` to the Space URL
## Quick test (Swagger)
Open `<space-url>/docs` and try:
**Task A** — `POST /simulate-review`:
```json
{
"user_id": "A100WO06OQR8BQ",
"item": {"name": "Chowdeck", "category": "food_delivery", "item_id": "com.chowdeck.app"},
"context": {"time": "night", "weather": "rainy", "traffic_heavy": true}
}
```
**Task B** — `POST /recommend`:
```json
{
"user_id": "A100WO06OQR8BQ",
"context": {"time": "evening", "mood": "tired"},
"top_n": 3,
"cross_domain": true
}
```
**Cold-start** — `POST /simulate-review`:
```json
{
"user_id": "totally_new_user_xyz123",
"item": {"name": "Bolt Food", "category": "food_delivery"},
"context": {"time": "afternoon", "salary_week": true},
"cold_start_hints": {"budget_sensitive": true, "likes": ["affordable", "fast"]}
}
```
## Headline metrics (temporal hold-out, 1,421 users)
| Metric | Value |
|---|---|
| NDCG@10 | **0.649** |
| Hit@10 | **0.675** |
| MRR@10 | **0.642** |
| Rating RMSE (LightGBM) | **0.710** |
| Rating RMSE (baseline) | 1.090 |
See `docs/SOLUTION_PAPER.md` and `docs/ABLATION.md` in the GitHub repo for the full write-up.
"""
SPACE_DOCKERFILE = """FROM python:3.12-slim
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \\
build-essential libgomp1 curl \\
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
# Code
COPY app ./app
COPY prompts ./prompts
COPY scripts ./scripts
# Pre-built data + models — shipped via LFS in this Space
COPY data ./data
COPY models_store ./models_store
ENV PYTHONPATH=/app
ENV PYTHONUNBUFFERED=1
# Hugging Face Spaces sets PORT; honor it but default to 8000.
ENV PORT=8000
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \\
CMD curl -fsS http://localhost:${PORT}/health || exit 1
CMD ["sh", "-c", "uvicorn app.main:app --host 0.0.0.0 --port ${PORT}"]
"""
GITATTRIBUTES = """*.parquet filter=lfs diff=lfs merge=lfs -text
*.pkl filter=lfs diff=lfs merge=lfs -text
*.csv filter=lfs diff=lfs merge=lfs -text
"""
def build_staging() -> Path:
"""Copy what we want to ship into a clean staging dir."""
staging = Path(tempfile.mkdtemp(prefix="hf-deploy-"))
print(f"[deploy] staging dir: {staging}")
# Folders to ship as-is
for folder in ("app", "prompts", "scripts", "docs", "data/processed", "models_store"):
src = ROOT / folder
if not src.exists():
print(f" ! missing {folder} — skipping")
continue
dst = staging / folder
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copytree(src, dst, dirs_exist_ok=True)
# Empty placeholder dirs for runtime cache writes
for d in ("data/personas", "data/memory", "data/raw"):
(staging / d).mkdir(parents=True, exist_ok=True)
(staging / d / ".gitkeep").write_text("")
# Top-level files
for f in ("requirements.txt", ".env.example", "SUBMISSION.md"):
if (ROOT / f).exists():
shutil.copy(ROOT / f, staging / f)
# Space-specific README + Dockerfile + .gitattributes
(staging / "README.md").write_text(SPACE_README, encoding="utf-8")
(staging / "Dockerfile").write_text(SPACE_DOCKERFILE, encoding="utf-8")
(staging / ".gitattributes").write_text(GITATTRIBUTES, encoding="utf-8")
# Strip __pycache__
for cache in staging.rglob("__pycache__"):
shutil.rmtree(cache, ignore_errors=True)
# Report size
total = sum(f.stat().st_size for f in staging.rglob("*") if f.is_file())
print(f"[deploy] staging size: {total / 1024 / 1024:.1f} MB")
return staging
def main() -> None:
url = ensure_space()
set_secrets()
staging = build_staging()
print(f"[deploy] uploading staging -> {SPACE_ID}")
api.upload_folder(
folder_path=str(staging),
repo_id=SPACE_ID,
repo_type="space",
commit_message="Deploy PersonaFlow AI",
)
print(f"\n[deploy] DONE — Space URL:\n {url}")
print(f" Swagger UI: {url.replace('huggingface.co/spaces', 'huggingface.co/spaces') + '/-/'}docs")
print(f" Once build completes (~3-5 min), test:\n {url}/-/health")
if __name__ == "__main__":
main()