KevinIsInCoding Claude Opus 4.8 commited on
Commit
de362e3
·
unverified ·
1 Parent(s): 06fbefb

chore(deploy): add upload_data.py + HF deploy runbook for runtime data (#25)

Browse files

Runtime data (ChromaDB index, graph pickle, trials.jsonl) is gitignored and served
to the Space from the candle-fire-data dataset repo at startup. There was no tooling
to push it, so the dataset could silently drift from the ingested data — that's how
trials.jsonl went missing and broke trial search on the Space.

- scripts/upload_data.py: uploads chroma/, graph/als_graph.pkl, trials/trials.jsonl to
the dataset repo (mirrors app._ensure_data). Supports --dry-run and --only.
- config.HF_DATASET_REPO: single constant for the dataset repo id, now shared by both
the uploader and app._ensure_data so upload/download targets can't drift.
- README: "Deploy to Hugging Face" section documenting the two-repo model and the
required order — upload data first, then git push hf main (the Space cold-starts with
no persistent storage and re-pulls data on every rebuild).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

Files changed (4) hide show
  1. README.md +33 -0
  2. app.py +2 -2
  3. config.py +5 -0
  4. scripts/upload_data.py +113 -0
README.md CHANGED
@@ -76,6 +76,39 @@ uv run python app.py
76
  uv run python main.py "What is the evidence for tofersen targeting SOD1?"
77
  ```
78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  ## Architecture
80
 
81
  ```
 
76
  uv run python main.py "What is the evidence for tofersen targeting SOD1?"
77
  ```
78
 
79
+ ## Deploy to Hugging Face
80
+
81
+ Deployment uses **two repos**, because runtime data is kept separate from code:
82
+
83
+ | Repo | Holds | Shipped by |
84
+ |---|---|---|
85
+ | Space `KevinIsCoding/candle-fire` (git remote `hf`) | **Code** + small committed data (`landscape.json`, seeds, tool schemas) | `git push hf main` |
86
+ | Dataset `KevinIsCoding/candle-fire-data` | **Large runtime data** — ChromaDB index, graph pickle, `trials.jsonl` (all gitignored) | `scripts/upload_data.py` |
87
+
88
+ The large data is gitignored (see `.gitignore`) and never travels with the code. On startup the Space pulls it from the dataset repo via `app._ensure_data()`. The Space has **no persistent storage**, so every restart is a clean cold start that re-downloads this data — which means the dataset repo must always hold the current files.
89
+
90
+ **Deploy in this order (data first, then code):**
91
+
92
+ ```bash
93
+ # 0. Merge the approved PR into main and sync locally
94
+ git checkout main && git pull origin main
95
+
96
+ # 1. Push runtime data to the dataset repo (REQUIRED before the code push)
97
+ # Needs a HF token with write access: `huggingface-cli login` or HF_TOKEN env var.
98
+ uv run python scripts/upload_data.py # uploads chroma/, graph, trials.jsonl
99
+ uv run python scripts/upload_data.py --dry-run # preview targets + sizes, no writes
100
+ uv run python scripts/upload_data.py --only trials # push just one target
101
+
102
+ # 2. Deploy the code (triggers the Space rebuild)
103
+ git push hf main
104
+ ```
105
+
106
+ > **Why data first:** the Space cold-starts on rebuild and immediately fetches data from the
107
+ > dataset repo. If you push code before uploading data, the Space can boot against stale or
108
+ > missing files — e.g. a missing `trials.jsonl` silently disables trial search. Always run
109
+ > `upload_data.py` before `git push hf main`. The upload/download target is a single constant
110
+ > (`config.HF_DATASET_REPO`) shared by the script and the app, so they can't drift.
111
+
112
  ## Architecture
113
 
114
  ```
app.py CHANGED
@@ -11,7 +11,7 @@ from dotenv import load_dotenv
11
  load_dotenv()
12
 
13
  from agents.research_agent import stream_research_agent
14
- from config import CHROMA_COLLECTION, CHROMA_DIR, GRAPH_PICKLE_PATH, TRIALS_PATH
15
  from logging_config import get_logger
16
  from rag.indexer import load_collection
17
 
@@ -45,7 +45,7 @@ def _load_collection():
45
  return None
46
 
47
 
48
- _HF_DATASET = "KevinIsCoding/candle-fire-data"
49
 
50
 
51
  def _ensure_data() -> None:
 
11
  load_dotenv()
12
 
13
  from agents.research_agent import stream_research_agent
14
+ from config import CHROMA_COLLECTION, CHROMA_DIR, GRAPH_PICKLE_PATH, HF_DATASET_REPO, TRIALS_PATH
15
  from logging_config import get_logger
16
  from rag.indexer import load_collection
17
 
 
45
  return None
46
 
47
 
48
+ _HF_DATASET = HF_DATASET_REPO
49
 
50
 
51
  def _ensure_data() -> None:
config.py CHANGED
@@ -23,6 +23,11 @@ GRAPH_JSON_PATH = DATA_DIR / "graph" / "als_graph.json"
23
  CHROMA_DIR = DATA_DIR / "chroma"
24
  CHROMA_COLLECTION = "als_papers"
25
 
 
 
 
 
 
26
  # Experimental therapy landscape (offline-built, committed to git — small)
27
  LANDSCAPE_PATH = DATA_DIR / "landscape" / "landscape.json"
28
  LANDSCAPE_PROGRESS_PATH = DATA_DIR / "landscape" / ".progress.json"
 
23
  CHROMA_DIR = DATA_DIR / "chroma"
24
  CHROMA_COLLECTION = "als_papers"
25
 
26
+ # Runtime data lives OUTSIDE git (see .gitignore) and is served from a HF dataset repo:
27
+ # scripts/upload_data.py pushes it there, app._ensure_data() pulls it on Space startup.
28
+ # Both sides read this one constant so the upload/download targets can never drift.
29
+ HF_DATASET_REPO = "KevinIsCoding/candle-fire-data"
30
+
31
  # Experimental therapy landscape (offline-built, committed to git — small)
32
  LANDSCAPE_PATH = DATA_DIR / "landscape" / "landscape.json"
33
  LANDSCAPE_PROGRESS_PATH = DATA_DIR / "landscape" / ".progress.json"
scripts/upload_data.py ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Upload runtime data to the Hugging Face dataset repo (deploy prerequisite).
2
+
3
+ Runtime data (ChromaDB index, the graph pickle, trials.jsonl) is intentionally kept
4
+ OUT of git — it is large and pipeline-generated (see .gitignore). The HF Space fetches
5
+ it at startup via app._ensure_data(). This script is the other half of that contract:
6
+ it pushes the current local data to the dataset repo so the Space has fresh data to pull.
7
+
8
+ Run it BEFORE `git push hf main` on every deploy — otherwise the Space serves whatever
9
+ data the dataset repo last had (or, if a file was never uploaded, none — which silently
10
+ breaks features like trial search).
11
+
12
+ What it uploads (mirrors _ensure_data exactly):
13
+ data/chroma/ → chroma/ (the ChromaDB store)
14
+ data/graph/als_graph.pkl → graph/als_graph.pkl
15
+ data/trials/trials.jsonl → trials/trials.jsonl
16
+
17
+ Usage:
18
+ uv run python scripts/upload_data.py # upload everything
19
+ uv run python scripts/upload_data.py --dry-run # list what would upload, no writes
20
+ uv run python scripts/upload_data.py --only trials # one target (chroma|graph|trials)
21
+
22
+ Auth: needs a Hugging Face token with write access to the dataset repo. Provide it via
23
+ `huggingface-cli login`, or the HF_TOKEN / HUGGING_FACE_HUB_TOKEN env var.
24
+ """
25
+ from __future__ import annotations
26
+
27
+ import argparse
28
+ import sys
29
+ from pathlib import Path
30
+
31
+ # Allow running as `python scripts/upload_data.py` (repo root on sys.path).
32
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
33
+
34
+ from config import CHROMA_DIR, GRAPH_PICKLE_PATH, HF_DATASET_REPO, TRIALS_PATH # noqa: E402
35
+
36
+
37
+ def _human_size(n: int) -> str:
38
+ for unit in ("B", "KB", "MB", "GB"):
39
+ if n < 1024 or unit == "GB":
40
+ return f"{n:.1f}{unit}" if unit != "B" else f"{n}B"
41
+ n /= 1024
42
+ return f"{n:.1f}GB"
43
+
44
+
45
+ def _dir_size(path: Path) -> int:
46
+ return sum(f.stat().st_size for f in path.rglob("*") if f.is_file())
47
+
48
+
49
+ def main() -> int:
50
+ parser = argparse.ArgumentParser(description="Upload runtime data to the HF dataset repo.")
51
+ parser.add_argument("--dry-run", action="store_true", help="Show what would upload; write nothing.")
52
+ parser.add_argument("--only", choices=["chroma", "graph", "trials"],
53
+ help="Upload just one target (default: all).")
54
+ parser.add_argument("--repo", default=HF_DATASET_REPO, help="Override the dataset repo id.")
55
+ args = parser.parse_args()
56
+
57
+ # (key, kind, local_path, path_in_repo)
58
+ targets = [
59
+ ("chroma", "folder", CHROMA_DIR, "chroma"),
60
+ ("graph", "file", GRAPH_PICKLE_PATH, "graph/als_graph.pkl"),
61
+ ("trials", "file", TRIALS_PATH, "trials/trials.jsonl"),
62
+ ]
63
+ if args.only:
64
+ targets = [t for t in targets if t[0] == args.only]
65
+
66
+ # Verify every selected target exists locally before touching the network.
67
+ missing = [str(p) for _, _, p, _ in targets if not p.exists()]
68
+ if missing:
69
+ print("ERROR — local data missing (run the offline pipeline first):", file=sys.stderr)
70
+ for m in missing:
71
+ print(f" - {m}", file=sys.stderr)
72
+ return 1
73
+
74
+ print(f"Dataset repo: {args.repo}")
75
+ for key, kind, local, in_repo in targets:
76
+ size = _dir_size(local) if kind == "folder" else local.stat().st_size
77
+ print(f" {key:7} {kind:6} {local} ({_human_size(size)}) → {in_repo}")
78
+
79
+ if args.dry_run:
80
+ print("\n--dry-run: nothing uploaded.")
81
+ return 0
82
+
83
+ from huggingface_hub import HfApi
84
+ api = HfApi()
85
+ try:
86
+ who = api.whoami().get("name")
87
+ except Exception:
88
+ print("ERROR — not authenticated. Run `huggingface-cli login` or set HF_TOKEN.", file=sys.stderr)
89
+ return 1
90
+ print(f"Authenticated as: {who}\n")
91
+
92
+ for key, kind, local, in_repo in targets:
93
+ print(f"Uploading {key} ...")
94
+ if kind == "folder":
95
+ api.upload_folder(
96
+ folder_path=str(local), path_in_repo=in_repo,
97
+ repo_id=args.repo, repo_type="dataset",
98
+ commit_message=f"Update {in_repo} (deploy)",
99
+ )
100
+ else:
101
+ api.upload_file(
102
+ path_or_fileobj=str(local), path_in_repo=in_repo,
103
+ repo_id=args.repo, repo_type="dataset",
104
+ commit_message=f"Update {in_repo} (deploy)",
105
+ )
106
+ print(f" done: {in_repo}")
107
+
108
+ print("\nAll runtime data uploaded. Now deploy the code: git push hf main")
109
+ return 0
110
+
111
+
112
+ if __name__ == "__main__":
113
+ raise SystemExit(main())