| |
|
|
| from __future__ import annotations |
|
|
| import hashlib |
| import json |
| import os |
| import subprocess |
| import sys |
| from pathlib import Path |
| from typing import Any |
|
|
|
|
| def root() -> Path: |
| return Path(__file__).resolve().parents[1] |
|
|
|
|
| def hf_token() -> str | None: |
| t = ( |
| os.environ.get("HF_TOKEN", "").strip() |
| or os.environ.get("HUGGING_FACE_HUB_TOKEN", "").strip() |
| ) |
| return t or None |
|
|
|
|
| def hf_login() -> None: |
| tok = hf_token() |
| if not tok: |
| print( |
| "docker_build_assets: No HF_TOKEN / HUGGING_FACE_HUB_TOKEN — anonymous Hub access " |
| "(rate limits). On HF Spaces, pass token into the *Docker build* (not only runtime)." |
| ) |
| return |
| try: |
| from huggingface_hub import login |
| except ImportError: |
| print("docker_build_assets: huggingface_hub not installed; skipping login.") |
| return |
| login(token=tok, add_to_git_credential=False) |
| print("docker_build_assets: Hugging Face Hub login OK.") |
|
|
|
|
| def embedding_model_repo(hub_name: str) -> str: |
| if "/" not in hub_name.strip(): |
| return f"sentence-transformers/{hub_name.strip()}" |
| return hub_name.strip() |
|
|
|
|
| def _local_embedding_model() -> str: |
| return ( |
| os.environ.get("LOCAL_EMBEDDING_MODEL", "").strip() |
| or os.environ.get("TASK_B_LOCAL_EMBEDDING_MODEL", "").strip() |
| or os.environ.get("TASK_A_EMBEDDING_MODEL", "").strip() |
| or "all-MiniLM-L6-v2" |
| ) |
|
|
|
|
| def _skip_local_llm_hub() -> bool: |
| if os.environ.get("SKIP_LOCAL_LLM_HUB_DOWNLOAD", "").strip().lower() in ( |
| "1", |
| "true", |
| "yes", |
| ): |
| return True |
| if os.environ.get("GENERATION_BACKEND", "").strip().lower() in ("gemini", "google"): |
| return True |
| return False |
|
|
|
|
| def _unique_llm_hub_ids() -> list[str]: |
| if _skip_local_llm_hub(): |
| return [] |
| seen: list[str] = [] |
| for key in ("LOCAL_LLM_MODEL", "TASK_B_LOCAL_LLM_MODEL", "TASK_A_LOCAL_LLM_MODEL"): |
| v = os.environ.get(key, "").strip() |
| if v and v not in seen: |
| seen.append(v) |
| if not seen: |
| seen.append("Qwen/Qwen2.5-1.5B-Instruct") |
| return seen |
|
|
|
|
| _build_embedder: Any = None |
|
|
|
|
| def _get_build_embedder() -> Any: |
| global _build_embedder |
| if _build_embedder is None: |
| from sentence_transformers import SentenceTransformer |
|
|
| emb_name = _local_embedding_model() |
| print(f"docker_build_assets: loading embedder once for stub JSONL -> {emb_name}") |
| _build_embedder = SentenceTransformer(emb_name) |
| return _build_embedder |
|
|
|
|
| def prefetch_hub_files_only() -> None: |
| tok = hf_token() |
|
|
| try: |
| from huggingface_hub import snapshot_download |
| except ImportError: |
| print("docker_build_assets: huggingface_hub missing; skipping prefetch.") |
| return |
|
|
| kw: dict[str, Any] = {} |
| if tok: |
| kw["token"] = tok |
|
|
| emb_repo = embedding_model_repo(_local_embedding_model()) |
| print(f"docker_build_assets: snapshot_download (disk cache) -> {emb_repo}") |
| snapshot_download(repo_id=emb_repo, local_files_only=False, **kw) |
|
|
| llm_ids = _unique_llm_hub_ids() |
| if not llm_ids: |
| print( |
| "docker_build_assets: skipping causal LM snapshot " |
| "(GENERATION_BACKEND=gemini or SKIP_LOCAL_LLM_HUB_DOWNLOAD=1)." |
| ) |
| for llm_id in llm_ids: |
| print(f"docker_build_assets: snapshot_download (disk cache) -> {llm_id}") |
| snapshot_download(repo_id=llm_id, local_files_only=False, **kw) |
|
|
| print("docker_build_assets: Hub snapshots cached (embedder on disk; LLM via API or runtime load).") |
|
|
|
|
| def warm_runtime_models() -> None: |
| raw = os.environ.get("DOCKER_BUILD_SKIP_LLM_WARM", "1").strip().lower() |
| skip = raw not in ("0", "false", "no") |
| if skip: |
| print( |
| "docker_build_assets: skipping in-RAM LLM warm (DOCKER_BUILD_SKIP_LLM_WARM default 1). " |
| "Weights are on disk from snapshot_download + stub encodes; uvicorn prewarm loads them at runtime." |
| ) |
| return |
|
|
| print("docker_build_assets: full model warm (CPU) — DOCKER_BUILD_SKIP_LLM_WARM=0; needs several GB RAM.") |
| import gc |
|
|
| st = _get_build_embedder() |
| st.encode(["docker-build-warmup"], batch_size=1, show_progress_bar=False, convert_to_numpy=True) |
| gc.collect() |
|
|
| import torch |
| from transformers import AutoModelForCausalLM, AutoTokenizer |
|
|
| for mid in _unique_llm_hub_ids(): |
| print(f"docker_build_assets: causal LM warm — {mid}") |
| tok = AutoTokenizer.from_pretrained(mid, trust_remote_code=True) |
| mdl = AutoModelForCausalLM.from_pretrained( |
| mid, |
| torch_dtype=torch.float32, |
| trust_remote_code=True, |
| low_cpu_mem_usage=True, |
| ) |
| mdl.eval() |
| with torch.no_grad(): |
| batch = tok("warmup", return_tensors="pt") |
| mdl(**batch) |
| del mdl, tok |
| gc.collect() |
|
|
| print("docker_build_assets: model warm complete.") |
|
|
|
|
| def yelp_review_path(rt: Path) -> Path: |
| env_p = os.environ.get("YELP_REVIEW_JSON", "").strip() |
| if env_p: |
| return Path(env_p) |
| return rt / "yelp_dataset" / "extracted" / "yelp_academic_dataset_review.json" |
|
|
|
|
| def build_task_a_from_yelp(rt: Path, business_json: Path, review_json: Path) -> None: |
| py = sys.executable |
| max_rev = os.environ.get("DOCKER_TASK_A_MAX_REVIEW_ROWS", "10000") |
| subprocess.check_call( |
| [ |
| py, |
| str(rt / "scripts" / "build_task_a_review_rag.py"), |
| "--review-json", |
| str(review_json), |
| "--business-json", |
| str(business_json), |
| "--output", |
| str(rt / "data" / "task_a_reviews_embedded.jsonl"), |
| "--max-rows", |
| max_rev, |
| "--batch-size", |
| "32", |
| ] |
| ) |
|
|
|
|
| def build_stub_task_a_embedded(rt: Path, model: Any) -> None: |
| rows_raw: list[dict[str, Any]] = [] |
| for i in range(36): |
| uid = f"stub_user_{i % 9}" |
| excerpt = ( |
| f"Stub visit {i}: food was {'solid' if i % 2 == 0 else 'hit or miss'}, " |
| f"service {'friendly' if i % 3 else 'slow'}." |
| ) |
| bctx = ( |
| f"name: Stub Diner {i % 6}\ncategories: Restaurants, " |
| f"{'Italian' if i % 3 else 'Mexican'}\nlocation: Lagos, LA\n" |
| f"business_avg_stars: {3.4 + (i % 8) / 10:.1f}\n" |
| f"business_review_count: {15 + i * 3}" |
| ) |
| emb_src = f"{bctx}\nreview: {excerpt}" |
| rows_raw.append( |
| { |
| "user_id": uid, |
| "business_id": f"stub_b_{i}", |
| "stars": 1 + (i % 5), |
| "review_excerpt": excerpt, |
| "business_context": bctx, |
| "embedding_source": emb_src, |
| } |
| ) |
| texts = [r["embedding_source"] for r in rows_raw] |
| mat = model.encode(texts, batch_size=8, convert_to_numpy=True, normalize_embeddings=False) |
| out_path = rt / "data" / "task_a_reviews_embedded.jsonl" |
| with out_path.open("w", encoding="utf-8") as fout: |
| for rec, vec in zip(rows_raw, mat, strict=True): |
| row_out = {k: v for k, v in rec.items() if k != "embedding_source"} |
| row_out["embedding"] = vec.astype(float).tolist() |
| fout.write(json.dumps(row_out, ensure_ascii=False) + "\n") |
| print(f"docker_build_assets: wrote stub Task A RAG -> {out_path}") |
|
|
|
|
| def yelp_business_path(rt: Path) -> Path: |
| env_p = os.environ.get("YELP_BUSINESS_JSON", "").strip() |
| if env_p: |
| return Path(env_p) |
| return rt / "yelp_dataset" / "extracted" / "yelp_academic_dataset_business.json" |
|
|
|
|
| def build_from_yelp(rt: Path, yelp: Path) -> None: |
| max_rows = os.environ.get("DOCKER_CATALOG_MAX_ROWS", "15000") |
| out_cat = rt / "data" / "business_catalog.jsonl" |
| py = sys.executable |
| subprocess.check_call( |
| [ |
| py, |
| str(rt / "scripts" / "build_business_catalog.py"), |
| "--business-json", |
| str(yelp), |
| "--output", |
| str(out_cat), |
| "--max-rows", |
| max_rows, |
| "--only-open", |
| ] |
| ) |
| subprocess.check_call( |
| [ |
| py, |
| str(rt / "scripts" / "embed_catalog.py"), |
| "--input", |
| str(out_cat), |
| "--output", |
| str(rt / "data" / "business_catalog_embedded.jsonl"), |
| "--batch-size", |
| "32", |
| ] |
| ) |
|
|
|
|
| def stub_catalog_rows(n: int = 48) -> list[dict[str, Any]]: |
| templates = [ |
| ("Riverfront Ramen", "Restaurants, Japanese, Ramen", "Portland", "OR"), |
| ("Oak Street Bakery", "Food, Bakeries, Coffee & Tea", "Austin", "TX"), |
| ("Queen Vietnamese", "Restaurants, Vietnamese", "Philadelphia", "PA"), |
| ("Campus Espresso", "Coffee & Tea, Cafes", "Seattle", "WA"), |
| ("Park Yoga Studio", "Active Life, Yoga", "Denver", "CO"), |
| ("Midtown Books", "Shopping, Books", "Chicago", "IL"), |
| ("East Side Brewpub", "Nightlife, Breweries", "Milwaukee", "WI"), |
| ("Family Thai Kitchen", "Restaurants, Thai", "Tempe", "AZ"), |
| ("Uptown Nail Spa", "Beauty & Spas, Nail Salons", "Miami", "FL"), |
| ("Lakeside Pizza", "Restaurants, Pizza", "Minneapolis", "MN"), |
| ] |
| rows = [] |
| for i in range(n): |
| name, cats, city, state = templates[i % len(templates)] |
| suffix = i // len(templates) |
| disp = f"{name}" if suffix == 0 else f"{name} #{suffix}" |
| h = hashlib.sha256(f"{i}-{disp}".encode()).hexdigest()[:22] |
| bid = h |
| text_for_embedding = ( |
| f"name: {disp}\n" |
| f"categories: {cats}\n" |
| f"location: {city}, {state}\n" |
| f"address: {100 + i} Main St\n" |
| f"business_avg_stars: {3.5 + (i % 15) / 10:.1f}\n" |
| f"business_review_count: {20 + i * 7}\n" |
| f"is_open: 1" |
| ) |
| rows.append( |
| { |
| "business_id": bid, |
| "name": disp, |
| "categories": cats, |
| "city": city, |
| "state": state, |
| "stars": float(3.5 + (i % 15) / 10), |
| "review_count": int(20 + i * 7), |
| "is_open": 1, |
| "text_for_embedding": text_for_embedding, |
| } |
| ) |
| return rows |
|
|
|
|
| def build_stub_embedded(rt: Path, model: Any) -> None: |
| rows = stub_catalog_rows() |
| texts = [r["text_for_embedding"] for r in rows] |
| mat = model.encode(texts, batch_size=8, convert_to_numpy=True, normalize_embeddings=False) |
| out_path = rt / "data" / "business_catalog_embedded.jsonl" |
| cat_path = rt / "data" / "business_catalog.jsonl" |
| out_path.parent.mkdir(parents=True, exist_ok=True) |
| with out_path.open("w", encoding="utf-8") as fe, cat_path.open("w", encoding="utf-8") as fc: |
| for row, vec in zip(rows, mat, strict=True): |
| fc.write(json.dumps(row, ensure_ascii=False) + "\n") |
| emb_row = {**row, "embedding": vec.astype(float).tolist()} |
| fe.write(json.dumps(emb_row, ensure_ascii=False) + "\n") |
| print(f"docker_build_assets: wrote stub catalog -> {cat_path} and {out_path}") |
|
|
|
|
| def build_stub_data(rt: Path) -> None: |
| model = _get_build_embedder() |
| build_stub_embedded(rt, model) |
| build_stub_task_a_embedded(rt, model) |
|
|
|
|
| def main() -> None: |
| rt = root() |
| (rt / "data").mkdir(parents=True, exist_ok=True) |
|
|
| hf_login() |
| prefetch_hub_files_only() |
|
|
| yelp = yelp_business_path(rt) |
| yelp_rev = yelp_review_path(rt) |
| if yelp.is_file(): |
| print(f"docker_build_assets: building catalog from {yelp}") |
| build_from_yelp(rt, yelp) |
| if yelp_rev.is_file(): |
| print(f"docker_build_assets: Task A RAG from {yelp_rev}") |
| build_task_a_from_yelp(rt, yelp, yelp_rev) |
| else: |
| print( |
| "docker_build_assets: Yelp review JSON not found — stub Task A RAG " |
| "(set YELP_REVIEW_JSON or add yelp_academic_dataset_review.json)." |
| ) |
| build_stub_task_a_embedded(rt, _get_build_embedder()) |
| else: |
| print( |
| "docker_build_assets: Yelp business JSON not found; " |
| "writing stub JSONL (mount real data at runtime or bake yelp_dataset into build context)." |
| ) |
| build_stub_data(rt) |
|
|
| warm_runtime_models() |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|