"""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 `/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()