Spaces:
Running
Running
timchen0618 commited on
Commit ·
b03f016
1
Parent(s): 3adde9f
Deploy research dashboard
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .dockerignore +8 -0
- .gitattributes +0 -35
- .gitignore +6 -0
- .hfignore +8 -0
- CLAUDE.md +91 -0
- Dockerfile +25 -0
- README.md +31 -5
- backend/__init__.py +0 -0
- backend/api/__init__.py +0 -0
- backend/api/experiments.py +730 -0
- backend/api/manifest.py +104 -0
- backend/api/model_datasets.py +409 -0
- backend/api/plan_revisions.py +60 -0
- backend/api/presets.py +188 -0
- backend/app.py +36 -0
- backend/data/activity_logs.json +53 -0
- backend/data/artifacts.json +1 -0
- backend/data/experiment_notes.json +42 -0
- backend/data/experiments.json +39 -0
- backend/data/runs.json +1 -0
- backend/data/sub_experiments.json +1 -0
- backend/data/summary_findings.json +1 -0
- backend/requirements.txt +5 -0
- docs/managing_presets.md +194 -0
- docs/plans/2026-03-07-research-dashboard-design.md +86 -0
- frontend/dist/assets/ExperimentsApp-B57tv21O.js +0 -0
- frontend/dist/assets/ExperimentsApp-DnJR2-55.css +1 -0
- frontend/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- frontend/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- frontend/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- frontend/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- frontend/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- frontend/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- frontend/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- frontend/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- frontend/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- frontend/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- frontend/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- frontend/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- frontend/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- frontend/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- frontend/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- frontend/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- frontend/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- frontend/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- frontend/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- frontend/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- frontend/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- frontend/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- frontend/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
.dockerignore
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
node_modules
|
| 2 |
+
frontend/node_modules
|
| 3 |
+
frontend/src
|
| 4 |
+
.git
|
| 5 |
+
**/__pycache__
|
| 6 |
+
**/*.pyc
|
| 7 |
+
.env
|
| 8 |
+
backend/data
|
.gitattributes
DELETED
|
@@ -1,35 +0,0 @@
|
|
| 1 |
-
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
-
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
-
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
-
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.gitignore
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
node_modules/
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.pyc
|
| 4 |
+
.DS_Store
|
| 5 |
+
.raca/
|
| 6 |
+
frontend/node_modules/
|
.hfignore
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
node_modules/
|
| 2 |
+
__pycache__/
|
| 3 |
+
.venv/
|
| 4 |
+
*.pyc
|
| 5 |
+
.DS_Store
|
| 6 |
+
.raca/
|
| 7 |
+
.git/
|
| 8 |
+
dist/
|
CLAUDE.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Experiment Dashboard Visualizer
|
| 2 |
+
|
| 3 |
+
Research dashboard with experiment tracking and trace visualization.
|
| 4 |
+
|
| 5 |
+
- **Live**: https://huggingface.co/spaces/{HF_ORG}/dashboard
|
| 6 |
+
- **GitHub**: `{github-org}/dashboard-repo`
|
| 7 |
+
- **Remotes**: `origin` (GitHub), `space` (HF Space)
|
| 8 |
+
|
| 9 |
+
## Updating the Website
|
| 10 |
+
|
| 11 |
+
When the user says "update the website", "sync the dashboard", or "push to the website":
|
| 12 |
+
|
| 13 |
+
1. **Build the frontend**:
|
| 14 |
+
```bash
|
| 15 |
+
cd tools/visualizer/frontend && npm run build
|
| 16 |
+
```
|
| 17 |
+
|
| 18 |
+
2. **Import experiment data** (reads local files, uploads to HF dataset):
|
| 19 |
+
```bash
|
| 20 |
+
cd tools/visualizer && python3 scripts/import_experiments.py
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
3. **Sync the live Space** (tells the running app to re-download data from HF):
|
| 24 |
+
```bash
|
| 25 |
+
curl -s -X POST https://{HF_ORG}-dashboard.hf.space/api/experiments/sync
|
| 26 |
+
```
|
| 27 |
+
|
| 28 |
+
4. **Push code to HF Space** (deploys new frontend/backend code):
|
| 29 |
+
```bash
|
| 30 |
+
cd tools/visualizer
|
| 31 |
+
git add -A && git commit -m "update dashboard"
|
| 32 |
+
git push origin main
|
| 33 |
+
git push space main
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
Steps 1-3 sync **data** (experiments, summary findings, presets). Step 4 deploys **code** changes.
|
| 37 |
+
|
| 38 |
+
If only `summary_findings.md` changed (no code changes), steps 2-3 are sufficient.
|
| 39 |
+
|
| 40 |
+
## Summary Findings
|
| 41 |
+
|
| 42 |
+
- Source file: `{WORKSPACE}/notes/experiments/summary_findings.md`
|
| 43 |
+
- Only the user writes this file — never edit it
|
| 44 |
+
- Synced to HF via `import_experiments.py` → served at `GET /api/experiments/summary`
|
| 45 |
+
- Shown on the Experiments page via the "Findings / Summary" button
|
| 46 |
+
|
| 47 |
+
## Experiment Discovery
|
| 48 |
+
|
| 49 |
+
`scripts/import_experiments.py` auto-discovers all experiments in `notes/experiments/`, skipping `old/`, `_templates/`, and hidden directories. To hide specific experiments, add them to `EXCLUDED_EXPERIMENTS` in the script.
|
| 50 |
+
|
| 51 |
+
## Stack
|
| 52 |
+
|
| 53 |
+
- **Backend**: Flask (Python), blueprints in `backend/api/`
|
| 54 |
+
- **Frontend**: React + TypeScript + Tailwind, built with Vite into `frontend/dist/`
|
| 55 |
+
- **Data**: HF datasets (`RACA_DASHBOARD` for experiments, `RACA-VIS-PRESETS` for visualizer presets)
|
| 56 |
+
|
| 57 |
+
## AdaEvolve Traces
|
| 58 |
+
|
| 59 |
+
### Required HuggingFace Dataset Columns
|
| 60 |
+
|
| 61 |
+
| Column | Type | Purpose |
|
| 62 |
+
|--------|------|---------|
|
| 63 |
+
| iteration | int | Iteration number |
|
| 64 |
+
| island_id | int | Which island produced this solution |
|
| 65 |
+
| score | float | Current iteration score |
|
| 66 |
+
| best_score | float | Best score so far |
|
| 67 |
+
| delta | float | Change from previous iteration |
|
| 68 |
+
| adaptation_type | string | "L1_explore", "L1_exploit", "L2_migrate", "L3_meta" |
|
| 69 |
+
| exploration_intensity | float | How exploratory this iteration was |
|
| 70 |
+
| is_valid | bool | Whether solution is valid |
|
| 71 |
+
| task_id | string | Task identifier |
|
| 72 |
+
| prompt_text | string | (optional) Input prompt to model |
|
| 73 |
+
| reasoning_trace | string | (optional) Thinking/reasoning output |
|
| 74 |
+
| program_code | string | (optional) Generated/evolved code |
|
| 75 |
+
|
| 76 |
+
### Color Mapping
|
| 77 |
+
|
| 78 |
+
L1_explore=blue, L1_exploit=green, L2_migrate=amber, L3_meta=red, other=gray.
|
| 79 |
+
|
| 80 |
+
### Adding a Preset
|
| 81 |
+
|
| 82 |
+
1. Upload dataset to HuggingFace (org: `your-org`)
|
| 83 |
+
2. Add entry to `your-org/RACA-VIS-PRESETS` file `adaevolve_presets.json`:
|
| 84 |
+
```json
|
| 85 |
+
{"id": "8-char-hex", "name": "Descriptive Name", "repo": "org/dataset-name", "split": "train"}
|
| 86 |
+
```
|
| 87 |
+
3. Sync: `curl -X POST "https://your-org-agg-trace-visualizer.hf.space/api/presets/sync"`
|
| 88 |
+
|
| 89 |
+
### Naming Convention
|
| 90 |
+
|
| 91 |
+
`{Task} {Model}: {Description} ({N} iter)`
|
Dockerfile
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
# Install nginx
|
| 4 |
+
RUN apt-get update && apt-get install -y --no-install-recommends nginx && rm -rf /var/lib/apt/lists/*
|
| 5 |
+
|
| 6 |
+
WORKDIR /app
|
| 7 |
+
|
| 8 |
+
# Install Python deps (cached unless requirements.txt changes)
|
| 9 |
+
COPY backend/requirements.txt ./backend/
|
| 10 |
+
RUN pip install --no-cache-dir -r backend/requirements.txt
|
| 11 |
+
|
| 12 |
+
# Copy backend + pre-built frontend (built locally by install.sh, no Node needed)
|
| 13 |
+
COPY backend/ ./backend/
|
| 14 |
+
COPY frontend/dist/ ./frontend/dist/
|
| 15 |
+
|
| 16 |
+
# Writable dirs for runtime
|
| 17 |
+
RUN mkdir -p /app/backend/data /app/backend/presets && \
|
| 18 |
+
chmod -R 777 /app/backend/data /app/backend/presets
|
| 19 |
+
|
| 20 |
+
COPY nginx.conf /etc/nginx/nginx.conf
|
| 21 |
+
COPY start.sh ./
|
| 22 |
+
RUN chmod +x start.sh
|
| 23 |
+
|
| 24 |
+
EXPOSE 7860
|
| 25 |
+
CMD ["./start.sh"]
|
README.md
CHANGED
|
@@ -1,10 +1,36 @@
|
|
| 1 |
---
|
| 2 |
-
title: Dashboard
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Research Dashboard
|
| 3 |
+
emoji: 📊
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
| 10 |
+
# Research Dashboard
|
| 11 |
+
|
| 12 |
+
A unified research control pane with two main sections:
|
| 13 |
+
|
| 14 |
+
## Experiments
|
| 15 |
+
|
| 16 |
+
Track research experiments, hypotheses, runs, and artifacts:
|
| 17 |
+
|
| 18 |
+
- **Experiment tracking** - Create/manage experiments with hypothesis statements, status, and completeness scoring
|
| 19 |
+
- **Run history** - Record runs with conditions, models, clusters, metrics, and HF dataset links
|
| 20 |
+
- **Sub-experiments** - Drill into focused sub-studies with markdown reports
|
| 21 |
+
- **HF dataset catalog** - Link and browse all HuggingFace datasets per experiment
|
| 22 |
+
|
| 23 |
+
Data stored in `your-org/RACA_DASHBOARD`. Supports programmatic import via `/api/experiments/import`.
|
| 24 |
+
|
| 25 |
+
## Visualizer
|
| 26 |
+
|
| 27 |
+
Six trace visualization tools:
|
| 28 |
+
|
| 29 |
+
- **Model Trace** - Analyze reasoning traces from model responses (think tags, backtracks, restarts)
|
| 30 |
+
- **Arena** - Explore multi-agent game episodes and transcripts
|
| 31 |
+
- **RLM** - Navigate hierarchical RLM call traces (GEPA iterations, RLM calls)
|
| 32 |
+
- **RLM Eval** - RLM evaluation trace viewer
|
| 33 |
+
- **Harbor** - View SWE-bench agent trajectories (ATIF + raw message formats)
|
| 34 |
+
- **AdaEvolve** - Explore AdaEvolve optimization traces
|
| 35 |
+
|
| 36 |
+
Each visualizer loads datasets from HuggingFace and supports preset configurations stored in `your-org/RACA-VIS-PRESETS`.
|
backend/__init__.py
ADDED
|
File without changes
|
backend/api/__init__.py
ADDED
|
File without changes
|
backend/api/experiments.py
ADDED
|
@@ -0,0 +1,730 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
import uuid
|
| 4 |
+
import tempfile
|
| 5 |
+
import threading
|
| 6 |
+
from datetime import datetime, timezone
|
| 7 |
+
from flask import Blueprint, request, jsonify
|
| 8 |
+
|
| 9 |
+
bp = Blueprint("experiments", __name__, url_prefix="/api/experiments")
|
| 10 |
+
|
| 11 |
+
def _resolve_hf_org() -> str:
|
| 12 |
+
"""Resolve HF org from env > .raca/config.yaml > fallback."""
|
| 13 |
+
org = os.environ.get("HF_ORG")
|
| 14 |
+
if org and org != "your-org":
|
| 15 |
+
return org
|
| 16 |
+
# Walk up from this file looking for .raca/config.yaml
|
| 17 |
+
from pathlib import Path
|
| 18 |
+
current = Path(__file__).resolve().parent
|
| 19 |
+
for _ in range(10):
|
| 20 |
+
current = current.parent
|
| 21 |
+
config = current / ".raca" / "config.yaml"
|
| 22 |
+
if config.exists():
|
| 23 |
+
try:
|
| 24 |
+
import yaml
|
| 25 |
+
with open(config) as f:
|
| 26 |
+
cfg = yaml.safe_load(f) or {}
|
| 27 |
+
org = cfg.get("hf_org", "")
|
| 28 |
+
if org:
|
| 29 |
+
return org
|
| 30 |
+
except Exception:
|
| 31 |
+
pass
|
| 32 |
+
return "your-org"
|
| 33 |
+
|
| 34 |
+
HF_ORG = _resolve_hf_org()
|
| 35 |
+
DASHBOARD_REPO = f"{HF_ORG}/RACA_DASHBOARD"
|
| 36 |
+
LOCAL_DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data")
|
| 37 |
+
|
| 38 |
+
_cache: dict[str, list[dict]] = {}
|
| 39 |
+
_cache_loaded: set[str] = set()
|
| 40 |
+
_dashboard_cache: dict[str, dict] = {}
|
| 41 |
+
_dashboard_cache_loaded: bool = False
|
| 42 |
+
_lock = threading.Lock()
|
| 43 |
+
|
| 44 |
+
FILES = ["experiments", "runs", "sub_experiments", "experiment_notes", "summary_findings", "activity_logs", "artifacts"]
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def _ensure_local_dir():
|
| 48 |
+
os.makedirs(LOCAL_DATA_DIR, exist_ok=True)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def _local_path(name: str) -> str:
|
| 52 |
+
_ensure_local_dir()
|
| 53 |
+
return os.path.join(LOCAL_DATA_DIR, f"{name}.json")
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def _download_file(name: str) -> list[dict]:
|
| 57 |
+
try:
|
| 58 |
+
from huggingface_hub import hf_hub_download
|
| 59 |
+
path = hf_hub_download(
|
| 60 |
+
DASHBOARD_REPO,
|
| 61 |
+
f"{name}.json",
|
| 62 |
+
repo_type="dataset",
|
| 63 |
+
)
|
| 64 |
+
with open(path) as f:
|
| 65 |
+
data = json.load(f)
|
| 66 |
+
with open(_local_path(name), "w") as f:
|
| 67 |
+
json.dump(data, f, indent=2)
|
| 68 |
+
return data
|
| 69 |
+
except Exception:
|
| 70 |
+
local = _local_path(name)
|
| 71 |
+
if os.path.exists(local):
|
| 72 |
+
with open(local) as f:
|
| 73 |
+
return json.load(f)
|
| 74 |
+
return []
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def _upload_file(name: str, data: list[dict]):
|
| 78 |
+
with open(_local_path(name), "w") as f:
|
| 79 |
+
json.dump(data, f, indent=2)
|
| 80 |
+
|
| 81 |
+
def _do_upload():
|
| 82 |
+
try:
|
| 83 |
+
from huggingface_hub import HfApi
|
| 84 |
+
api = HfApi()
|
| 85 |
+
try:
|
| 86 |
+
api.create_repo(DASHBOARD_REPO, repo_type="dataset", exist_ok=True)
|
| 87 |
+
except Exception:
|
| 88 |
+
pass
|
| 89 |
+
with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False) as f:
|
| 90 |
+
json.dump(data, f, indent=2)
|
| 91 |
+
tmp = f.name
|
| 92 |
+
api.upload_file(
|
| 93 |
+
path_or_fileobj=tmp,
|
| 94 |
+
path_in_repo=f"{name}.json",
|
| 95 |
+
repo_id=DASHBOARD_REPO,
|
| 96 |
+
repo_type="dataset",
|
| 97 |
+
)
|
| 98 |
+
os.unlink(tmp)
|
| 99 |
+
except Exception as e:
|
| 100 |
+
print(f"[experiments] HF upload failed for {name}: {e}")
|
| 101 |
+
|
| 102 |
+
threading.Thread(target=_do_upload, daemon=True).start()
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def _get(name: str):
|
| 106 |
+
"""Get cached data, downloading from HF if needed.
|
| 107 |
+
Returns list[dict] for most files, but dict for activity_logs (keyed by experiment_id).
|
| 108 |
+
"""
|
| 109 |
+
# Check cache without holding lock during download
|
| 110 |
+
with _lock:
|
| 111 |
+
if name in _cache_loaded:
|
| 112 |
+
data = _cache.get(name, [])
|
| 113 |
+
return dict(data) if isinstance(data, dict) else list(data)
|
| 114 |
+
need_download = True
|
| 115 |
+
|
| 116 |
+
if need_download:
|
| 117 |
+
# Download outside the lock so one slow download doesn't block all requests
|
| 118 |
+
downloaded = _download_file(name)
|
| 119 |
+
with _lock:
|
| 120 |
+
if name not in _cache_loaded: # Double-check after reacquiring
|
| 121 |
+
_cache[name] = downloaded
|
| 122 |
+
_cache_loaded.add(name)
|
| 123 |
+
data = _cache.get(name, [])
|
| 124 |
+
return dict(data) if isinstance(data, dict) else list(data)
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def _set(name: str, data: list[dict]):
|
| 128 |
+
with _lock:
|
| 129 |
+
_cache[name] = data
|
| 130 |
+
_cache_loaded.add(name)
|
| 131 |
+
_upload_file(name, data)
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def _now() -> str:
|
| 135 |
+
return datetime.now(timezone.utc).isoformat()
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
def _load_dashboard_state() -> dict:
|
| 139 |
+
"""Load dashboard_state.json (a single dict, not an array) from HF or local fallback."""
|
| 140 |
+
global _dashboard_cache_loaded
|
| 141 |
+
with _lock:
|
| 142 |
+
if _dashboard_cache_loaded:
|
| 143 |
+
return dict(_dashboard_cache)
|
| 144 |
+
try:
|
| 145 |
+
from huggingface_hub import hf_hub_download
|
| 146 |
+
path = hf_hub_download(
|
| 147 |
+
DASHBOARD_REPO,
|
| 148 |
+
"dashboard_state.json",
|
| 149 |
+
repo_type="dataset",
|
| 150 |
+
)
|
| 151 |
+
with open(path) as f:
|
| 152 |
+
data = json.load(f)
|
| 153 |
+
# Cache locally
|
| 154 |
+
local = os.path.join(LOCAL_DATA_DIR, "dashboard_state.json")
|
| 155 |
+
_ensure_local_dir()
|
| 156 |
+
with open(local, "w") as f:
|
| 157 |
+
json.dump(data, f, indent=2)
|
| 158 |
+
with _lock:
|
| 159 |
+
_dashboard_cache.clear()
|
| 160 |
+
_dashboard_cache.update(data if isinstance(data, dict) else {})
|
| 161 |
+
_dashboard_cache_loaded = True
|
| 162 |
+
return dict(_dashboard_cache)
|
| 163 |
+
except Exception:
|
| 164 |
+
local = os.path.join(LOCAL_DATA_DIR, "dashboard_state.json")
|
| 165 |
+
if os.path.exists(local):
|
| 166 |
+
try:
|
| 167 |
+
with open(local) as f:
|
| 168 |
+
data = json.load(f)
|
| 169 |
+
with _lock:
|
| 170 |
+
_dashboard_cache.clear()
|
| 171 |
+
_dashboard_cache.update(data if isinstance(data, dict) else {})
|
| 172 |
+
_dashboard_cache_loaded = True
|
| 173 |
+
return dict(_dashboard_cache)
|
| 174 |
+
except Exception:
|
| 175 |
+
pass
|
| 176 |
+
with _lock:
|
| 177 |
+
_dashboard_cache_loaded = True
|
| 178 |
+
return {}
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
def _merge_dashboard_state(experiments: list[dict]) -> list[dict]:
|
| 182 |
+
"""Enrich experiment list with live dashboard state fields."""
|
| 183 |
+
state = _load_dashboard_state()
|
| 184 |
+
if not state:
|
| 185 |
+
return experiments
|
| 186 |
+
|
| 187 |
+
# Build lookup: dashboard state keyed by experiment id and name
|
| 188 |
+
exp_states: dict[str, dict] = {}
|
| 189 |
+
for exp_id, exp_state in state.items():
|
| 190 |
+
if isinstance(exp_state, dict):
|
| 191 |
+
exp_states[exp_id] = exp_state
|
| 192 |
+
# Also index by name if present
|
| 193 |
+
name = exp_state.get("name", "")
|
| 194 |
+
if name:
|
| 195 |
+
exp_states[name] = exp_state
|
| 196 |
+
|
| 197 |
+
# Enrich existing experiments
|
| 198 |
+
seen_ids = set()
|
| 199 |
+
result = []
|
| 200 |
+
for exp in experiments:
|
| 201 |
+
seen_ids.add(exp["id"])
|
| 202 |
+
ds = exp_states.get(exp["id"]) or exp_states.get(exp.get("name", ""))
|
| 203 |
+
if ds:
|
| 204 |
+
exp = {
|
| 205 |
+
**exp,
|
| 206 |
+
"live_status": ds.get("status"),
|
| 207 |
+
"live_message": ds.get("message", ""),
|
| 208 |
+
"live_jobs": ds.get("jobs", {}),
|
| 209 |
+
"unreachable_clusters": ds.get("unreachable_clusters", {}),
|
| 210 |
+
"live_history": ds.get("history", []),
|
| 211 |
+
"live_started_at": ds.get("started_at"),
|
| 212 |
+
"live_updated_at": ds.get("updated_at"),
|
| 213 |
+
}
|
| 214 |
+
result.append(exp)
|
| 215 |
+
|
| 216 |
+
# Add experiments that exist ONLY in dashboard state (not in experiments.json)
|
| 217 |
+
for exp_id, ds in state.items():
|
| 218 |
+
if exp_id not in seen_ids and isinstance(ds, dict):
|
| 219 |
+
seen_ids.add(exp_id)
|
| 220 |
+
result.append({
|
| 221 |
+
"id": exp_id,
|
| 222 |
+
"name": ds.get("name", exp_id),
|
| 223 |
+
"research_project": ds.get("research_project", ""),
|
| 224 |
+
"hypothesis": {
|
| 225 |
+
"statement": "",
|
| 226 |
+
"type": "exploration",
|
| 227 |
+
"status": "pending",
|
| 228 |
+
"success_criteria": "",
|
| 229 |
+
},
|
| 230 |
+
"stage": "active",
|
| 231 |
+
"completeness": 0,
|
| 232 |
+
"models": [],
|
| 233 |
+
"tasks": [],
|
| 234 |
+
"tags": [],
|
| 235 |
+
"hf_repos": [],
|
| 236 |
+
"wandb_url": "",
|
| 237 |
+
"notes": "",
|
| 238 |
+
"created": ds.get("started_at", _now()),
|
| 239 |
+
"updated": ds.get("updated_at", _now()),
|
| 240 |
+
"run_count": 0,
|
| 241 |
+
"sub_count": 0,
|
| 242 |
+
"note_count": 0,
|
| 243 |
+
"live_status": ds.get("status"),
|
| 244 |
+
"live_message": ds.get("message", ""),
|
| 245 |
+
"live_jobs": ds.get("jobs", {}),
|
| 246 |
+
"unreachable_clusters": ds.get("unreachable_clusters", {}),
|
| 247 |
+
"live_history": ds.get("history", []),
|
| 248 |
+
"live_started_at": ds.get("started_at"),
|
| 249 |
+
"live_updated_at": ds.get("updated_at"),
|
| 250 |
+
})
|
| 251 |
+
|
| 252 |
+
return result
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
# --- Experiments CRUD ---
|
| 256 |
+
|
| 257 |
+
@bp.route("/", methods=["GET"])
|
| 258 |
+
def list_experiments():
|
| 259 |
+
experiments = _get("experiments")
|
| 260 |
+
runs = _get("runs")
|
| 261 |
+
subs = _get("sub_experiments")
|
| 262 |
+
notes = _get("experiment_notes")
|
| 263 |
+
|
| 264 |
+
# Enrich with counts
|
| 265 |
+
result = []
|
| 266 |
+
for exp in experiments:
|
| 267 |
+
exp_runs = [r for r in runs if r.get("experiment_id") == exp["id"]]
|
| 268 |
+
exp_subs = [s for s in subs if s.get("experiment_id") == exp["id"]]
|
| 269 |
+
exp_notes = [n for n in notes if n.get("experiment_id") == exp["id"]]
|
| 270 |
+
result.append({
|
| 271 |
+
**exp,
|
| 272 |
+
"run_count": len(exp_runs),
|
| 273 |
+
"sub_count": len(exp_subs),
|
| 274 |
+
"note_count": len(exp_notes),
|
| 275 |
+
})
|
| 276 |
+
|
| 277 |
+
# Merge live dashboard state
|
| 278 |
+
result = _merge_dashboard_state(result)
|
| 279 |
+
|
| 280 |
+
return jsonify(result)
|
| 281 |
+
|
| 282 |
+
|
| 283 |
+
@bp.route("/", methods=["POST"])
|
| 284 |
+
def create_experiment():
|
| 285 |
+
data = request.get_json()
|
| 286 |
+
name = data.get("name", "").strip()
|
| 287 |
+
if not name:
|
| 288 |
+
return jsonify({"error": "name is required"}), 400
|
| 289 |
+
|
| 290 |
+
exp_id = data.get("id", name.lower().replace(" ", "_"))
|
| 291 |
+
|
| 292 |
+
experiments = _get("experiments")
|
| 293 |
+
if any(e["id"] == exp_id for e in experiments):
|
| 294 |
+
return jsonify({"error": f"Experiment '{exp_id}' already exists"}), 409
|
| 295 |
+
|
| 296 |
+
experiment = {
|
| 297 |
+
"id": exp_id,
|
| 298 |
+
"name": name,
|
| 299 |
+
"research_project": data.get("research_project", ""),
|
| 300 |
+
"hypothesis": data.get("hypothesis", {
|
| 301 |
+
"statement": "",
|
| 302 |
+
"type": "exploration",
|
| 303 |
+
"status": "pending",
|
| 304 |
+
"success_criteria": "",
|
| 305 |
+
}),
|
| 306 |
+
"stage": data.get("stage", "idea"),
|
| 307 |
+
"completeness": data.get("completeness", 0),
|
| 308 |
+
"models": data.get("models", []),
|
| 309 |
+
"tasks": data.get("tasks", []),
|
| 310 |
+
"tags": data.get("tags", []),
|
| 311 |
+
"hf_repos": data.get("hf_repos", []),
|
| 312 |
+
"wandb_url": data.get("wandb_url", ""),
|
| 313 |
+
"notes": data.get("notes", ""),
|
| 314 |
+
"created": _now(),
|
| 315 |
+
"updated": _now(),
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
experiments.append(experiment)
|
| 319 |
+
_set("experiments", experiments)
|
| 320 |
+
return jsonify(experiment), 201
|
| 321 |
+
|
| 322 |
+
|
| 323 |
+
@bp.route("/<exp_id>", methods=["GET"])
|
| 324 |
+
def get_experiment(exp_id):
|
| 325 |
+
experiments = _get("experiments")
|
| 326 |
+
exp = next((e for e in experiments if e["id"] == exp_id), None)
|
| 327 |
+
if not exp:
|
| 328 |
+
return jsonify({"error": "not found"}), 404
|
| 329 |
+
|
| 330 |
+
runs = [r for r in _get("runs") if r.get("experiment_id") == exp_id]
|
| 331 |
+
subs = [s for s in _get("sub_experiments") if s.get("experiment_id") == exp_id]
|
| 332 |
+
notes = [n for n in _get("experiment_notes") if n.get("experiment_id") == exp_id]
|
| 333 |
+
|
| 334 |
+
# Activity log
|
| 335 |
+
all_logs = _get("activity_logs")
|
| 336 |
+
if isinstance(all_logs, dict):
|
| 337 |
+
activity_log = all_logs.get(exp_id, [])
|
| 338 |
+
elif isinstance(all_logs, list) and len(all_logs) == 1 and isinstance(all_logs[0], dict):
|
| 339 |
+
activity_log = all_logs[0].get(exp_id, [])
|
| 340 |
+
else:
|
| 341 |
+
activity_log = []
|
| 342 |
+
|
| 343 |
+
# Artifacts from manifest
|
| 344 |
+
all_artifacts = _get("artifacts")
|
| 345 |
+
artifacts = [a for a in all_artifacts if a.get("experiment_id") == exp_id]
|
| 346 |
+
|
| 347 |
+
return jsonify({
|
| 348 |
+
**exp,
|
| 349 |
+
"runs": runs,
|
| 350 |
+
"sub_experiments": subs,
|
| 351 |
+
"experiment_notes": notes,
|
| 352 |
+
"activity_log": activity_log,
|
| 353 |
+
"artifacts": artifacts,
|
| 354 |
+
})
|
| 355 |
+
|
| 356 |
+
|
| 357 |
+
@bp.route("/<exp_id>", methods=["PUT"])
|
| 358 |
+
def update_experiment(exp_id):
|
| 359 |
+
data = request.get_json()
|
| 360 |
+
experiments = _get("experiments")
|
| 361 |
+
|
| 362 |
+
for exp in experiments:
|
| 363 |
+
if exp["id"] == exp_id:
|
| 364 |
+
for key in ["name", "research_project", "hypothesis", "stage",
|
| 365 |
+
"completeness", "models", "tasks", "tags", "hf_repos",
|
| 366 |
+
"wandb_url", "notes"]:
|
| 367 |
+
if key in data:
|
| 368 |
+
exp[key] = data[key]
|
| 369 |
+
exp["updated"] = _now()
|
| 370 |
+
_set("experiments", experiments)
|
| 371 |
+
return jsonify(exp)
|
| 372 |
+
|
| 373 |
+
return jsonify({"error": "not found"}), 404
|
| 374 |
+
|
| 375 |
+
|
| 376 |
+
@bp.route("/<exp_id>", methods=["DELETE"])
|
| 377 |
+
def delete_experiment(exp_id):
|
| 378 |
+
experiments = _get("experiments")
|
| 379 |
+
experiments = [e for e in experiments if e["id"] != exp_id]
|
| 380 |
+
_set("experiments", experiments)
|
| 381 |
+
|
| 382 |
+
# Also delete associated runs, subs, and notes
|
| 383 |
+
runs = [r for r in _get("runs") if r.get("experiment_id") != exp_id]
|
| 384 |
+
_set("runs", runs)
|
| 385 |
+
subs = [s for s in _get("sub_experiments") if s.get("experiment_id") != exp_id]
|
| 386 |
+
_set("sub_experiments", subs)
|
| 387 |
+
notes = [n for n in _get("experiment_notes") if n.get("experiment_id") != exp_id]
|
| 388 |
+
_set("experiment_notes", notes)
|
| 389 |
+
|
| 390 |
+
return jsonify({"status": "ok"})
|
| 391 |
+
|
| 392 |
+
|
| 393 |
+
# --- Run records ---
|
| 394 |
+
|
| 395 |
+
@bp.route("/<exp_id>/runs", methods=["POST"])
|
| 396 |
+
def create_run(exp_id):
|
| 397 |
+
experiments = _get("experiments")
|
| 398 |
+
if not any(e["id"] == exp_id for e in experiments):
|
| 399 |
+
return jsonify({"error": "experiment not found"}), 404
|
| 400 |
+
|
| 401 |
+
data = request.get_json()
|
| 402 |
+
run = {
|
| 403 |
+
"id": data.get("id", f"run_{uuid.uuid4().hex[:8]}"),
|
| 404 |
+
"experiment_id": exp_id,
|
| 405 |
+
"condition": data.get("condition", ""),
|
| 406 |
+
"model": data.get("model", ""),
|
| 407 |
+
"cluster": data.get("cluster", ""),
|
| 408 |
+
"status": data.get("status", "completed"),
|
| 409 |
+
"hf_dataset": data.get("hf_dataset", ""),
|
| 410 |
+
"metrics": data.get("metrics", {}),
|
| 411 |
+
"timestamp": data.get("timestamp", _now()),
|
| 412 |
+
"notes": data.get("notes", ""),
|
| 413 |
+
}
|
| 414 |
+
|
| 415 |
+
runs = _get("runs")
|
| 416 |
+
runs.append(run)
|
| 417 |
+
_set("runs", runs)
|
| 418 |
+
|
| 419 |
+
# Touch experiment updated timestamp
|
| 420 |
+
for exp in experiments:
|
| 421 |
+
if exp["id"] == exp_id:
|
| 422 |
+
exp["updated"] = _now()
|
| 423 |
+
_set("experiments", experiments)
|
| 424 |
+
|
| 425 |
+
return jsonify(run), 201
|
| 426 |
+
|
| 427 |
+
|
| 428 |
+
@bp.route("/<exp_id>/runs/<run_id>", methods=["PUT"])
|
| 429 |
+
def update_run(exp_id, run_id):
|
| 430 |
+
data = request.get_json()
|
| 431 |
+
runs = _get("runs")
|
| 432 |
+
|
| 433 |
+
for run in runs:
|
| 434 |
+
if run["id"] == run_id and run["experiment_id"] == exp_id:
|
| 435 |
+
for key in ["condition", "model", "cluster", "status",
|
| 436 |
+
"hf_dataset", "metrics", "notes"]:
|
| 437 |
+
if key in data:
|
| 438 |
+
run[key] = data[key]
|
| 439 |
+
_set("runs", runs)
|
| 440 |
+
return jsonify(run)
|
| 441 |
+
|
| 442 |
+
return jsonify({"error": "not found"}), 404
|
| 443 |
+
|
| 444 |
+
|
| 445 |
+
@bp.route("/<exp_id>/runs/<run_id>", methods=["DELETE"])
|
| 446 |
+
def delete_run(exp_id, run_id):
|
| 447 |
+
runs = _get("runs")
|
| 448 |
+
runs = [r for r in runs if not (r["id"] == run_id and r["experiment_id"] == exp_id)]
|
| 449 |
+
_set("runs", runs)
|
| 450 |
+
return jsonify({"status": "ok"})
|
| 451 |
+
|
| 452 |
+
|
| 453 |
+
# --- Sub-experiments ---
|
| 454 |
+
|
| 455 |
+
@bp.route("/<exp_id>/subs", methods=["POST"])
|
| 456 |
+
def create_sub(exp_id):
|
| 457 |
+
experiments = _get("experiments")
|
| 458 |
+
if not any(e["id"] == exp_id for e in experiments):
|
| 459 |
+
return jsonify({"error": "experiment not found"}), 404
|
| 460 |
+
|
| 461 |
+
data = request.get_json()
|
| 462 |
+
name = data.get("name", "").strip()
|
| 463 |
+
if not name:
|
| 464 |
+
return jsonify({"error": "name is required"}), 400
|
| 465 |
+
|
| 466 |
+
sub_id = data.get("id", f"{exp_id}__{name.lower().replace(' ', '_')}")
|
| 467 |
+
|
| 468 |
+
sub = {
|
| 469 |
+
"id": sub_id,
|
| 470 |
+
"experiment_id": exp_id,
|
| 471 |
+
"name": name,
|
| 472 |
+
"hypothesis": data.get("hypothesis", ""),
|
| 473 |
+
"status": data.get("status", "active"),
|
| 474 |
+
"content_md": data.get("content_md", ""),
|
| 475 |
+
"hf_repos": data.get("hf_repos", []),
|
| 476 |
+
"created": _now(),
|
| 477 |
+
"updated": _now(),
|
| 478 |
+
}
|
| 479 |
+
|
| 480 |
+
subs = _get("sub_experiments")
|
| 481 |
+
subs.append(sub)
|
| 482 |
+
_set("sub_experiments", subs)
|
| 483 |
+
|
| 484 |
+
# Touch experiment updated timestamp
|
| 485 |
+
for exp in experiments:
|
| 486 |
+
if exp["id"] == exp_id:
|
| 487 |
+
exp["updated"] = _now()
|
| 488 |
+
_set("experiments", experiments)
|
| 489 |
+
|
| 490 |
+
return jsonify(sub), 201
|
| 491 |
+
|
| 492 |
+
|
| 493 |
+
@bp.route("/<exp_id>/subs/<sub_id>", methods=["PUT"])
|
| 494 |
+
def update_sub(exp_id, sub_id):
|
| 495 |
+
data = request.get_json()
|
| 496 |
+
subs = _get("sub_experiments")
|
| 497 |
+
|
| 498 |
+
for sub in subs:
|
| 499 |
+
if sub["id"] == sub_id and sub["experiment_id"] == exp_id:
|
| 500 |
+
for key in ["name", "hypothesis", "status", "content_md", "hf_repos"]:
|
| 501 |
+
if key in data:
|
| 502 |
+
sub[key] = data[key]
|
| 503 |
+
sub["updated"] = _now()
|
| 504 |
+
_set("sub_experiments", subs)
|
| 505 |
+
return jsonify(sub)
|
| 506 |
+
|
| 507 |
+
return jsonify({"error": "not found"}), 404
|
| 508 |
+
|
| 509 |
+
|
| 510 |
+
@bp.route("/<exp_id>/subs/<sub_id>", methods=["DELETE"])
|
| 511 |
+
def delete_sub(exp_id, sub_id):
|
| 512 |
+
subs = _get("sub_experiments")
|
| 513 |
+
subs = [s for s in subs if not (s["id"] == sub_id and s["experiment_id"] == exp_id)]
|
| 514 |
+
_set("sub_experiments", subs)
|
| 515 |
+
return jsonify({"status": "ok"})
|
| 516 |
+
|
| 517 |
+
|
| 518 |
+
# --- Experiment Notes ---
|
| 519 |
+
|
| 520 |
+
@bp.route("/<exp_id>/notes", methods=["POST"])
|
| 521 |
+
def create_note(exp_id):
|
| 522 |
+
experiments = _get("experiments")
|
| 523 |
+
if not any(e["id"] == exp_id for e in experiments):
|
| 524 |
+
return jsonify({"error": "experiment not found"}), 404
|
| 525 |
+
|
| 526 |
+
data = request.get_json()
|
| 527 |
+
title = data.get("title", "").strip()
|
| 528 |
+
if not title:
|
| 529 |
+
return jsonify({"error": "title is required"}), 400
|
| 530 |
+
|
| 531 |
+
note_id = data.get("id", f"{exp_id}__note_{uuid.uuid4().hex[:8]}")
|
| 532 |
+
|
| 533 |
+
note = {
|
| 534 |
+
"id": note_id,
|
| 535 |
+
"experiment_id": exp_id,
|
| 536 |
+
"title": title,
|
| 537 |
+
"filename": data.get("filename", ""),
|
| 538 |
+
"content_md": data.get("content_md", ""),
|
| 539 |
+
"created": _now(),
|
| 540 |
+
"updated": _now(),
|
| 541 |
+
}
|
| 542 |
+
|
| 543 |
+
notes = _get("experiment_notes")
|
| 544 |
+
notes.append(note)
|
| 545 |
+
_set("experiment_notes", notes)
|
| 546 |
+
return jsonify(note), 201
|
| 547 |
+
|
| 548 |
+
|
| 549 |
+
@bp.route("/<exp_id>/notes/<note_id>", methods=["GET"])
|
| 550 |
+
def get_note(exp_id, note_id):
|
| 551 |
+
notes = _get("experiment_notes")
|
| 552 |
+
note = next((n for n in notes if n["id"] == note_id and n["experiment_id"] == exp_id), None)
|
| 553 |
+
if not note:
|
| 554 |
+
return jsonify({"error": "not found"}), 404
|
| 555 |
+
return jsonify(note)
|
| 556 |
+
|
| 557 |
+
|
| 558 |
+
@bp.route("/<exp_id>/notes/<note_id>", methods=["PUT"])
|
| 559 |
+
def update_note(exp_id, note_id):
|
| 560 |
+
data = request.get_json()
|
| 561 |
+
notes = _get("experiment_notes")
|
| 562 |
+
|
| 563 |
+
for note in notes:
|
| 564 |
+
if note["id"] == note_id and note["experiment_id"] == exp_id:
|
| 565 |
+
for key in ["title", "content_md"]:
|
| 566 |
+
if key in data:
|
| 567 |
+
note[key] = data[key]
|
| 568 |
+
note["updated"] = _now()
|
| 569 |
+
_set("experiment_notes", notes)
|
| 570 |
+
return jsonify(note)
|
| 571 |
+
|
| 572 |
+
return jsonify({"error": "not found"}), 404
|
| 573 |
+
|
| 574 |
+
|
| 575 |
+
@bp.route("/<exp_id>/notes/<note_id>", methods=["DELETE"])
|
| 576 |
+
def delete_note(exp_id, note_id):
|
| 577 |
+
notes = _get("experiment_notes")
|
| 578 |
+
notes = [n for n in notes if not (n["id"] == note_id and n["experiment_id"] == exp_id)]
|
| 579 |
+
_set("experiment_notes", notes)
|
| 580 |
+
return jsonify({"status": "ok"})
|
| 581 |
+
|
| 582 |
+
|
| 583 |
+
# --- Activity Log ---
|
| 584 |
+
|
| 585 |
+
@bp.route("/<exp_id>/activity-log", methods=["GET"])
|
| 586 |
+
def get_activity_log(exp_id):
|
| 587 |
+
"""Get activity log entries for an experiment."""
|
| 588 |
+
all_logs = _get("activity_logs")
|
| 589 |
+
# activity_logs is a dict keyed by experiment_id (or empty list on first load)
|
| 590 |
+
if isinstance(all_logs, dict):
|
| 591 |
+
entries = all_logs.get(exp_id, [])
|
| 592 |
+
elif isinstance(all_logs, list) and len(all_logs) == 1 and isinstance(all_logs[0], dict):
|
| 593 |
+
# HF download may wrap dict in a list
|
| 594 |
+
entries = all_logs[0].get(exp_id, [])
|
| 595 |
+
else:
|
| 596 |
+
entries = []
|
| 597 |
+
|
| 598 |
+
# Sort by timestamp descending (most recent first)
|
| 599 |
+
entries.sort(key=lambda e: e.get("timestamp", ""), reverse=True)
|
| 600 |
+
|
| 601 |
+
# Optional filters
|
| 602 |
+
scope = request.args.get("scope")
|
| 603 |
+
entry_type = request.args.get("type")
|
| 604 |
+
if scope:
|
| 605 |
+
entries = [e for e in entries if e.get("scope") == scope]
|
| 606 |
+
if entry_type:
|
| 607 |
+
entries = [e for e in entries if e.get("type") == entry_type]
|
| 608 |
+
|
| 609 |
+
return jsonify(entries)
|
| 610 |
+
|
| 611 |
+
|
| 612 |
+
# --- Artifacts ---
|
| 613 |
+
|
| 614 |
+
@bp.route("/<exp_id>/artifacts", methods=["GET"])
|
| 615 |
+
def get_artifacts(exp_id):
|
| 616 |
+
"""Get artifact entries for an experiment from manifest data."""
|
| 617 |
+
all_artifacts = _get("artifacts")
|
| 618 |
+
artifacts = [a for a in all_artifacts if a.get("experiment_id") == exp_id]
|
| 619 |
+
return jsonify(artifacts)
|
| 620 |
+
|
| 621 |
+
|
| 622 |
+
# --- Summary Findings ---
|
| 623 |
+
|
| 624 |
+
@bp.route("/summary", methods=["GET"])
|
| 625 |
+
def get_summary():
|
| 626 |
+
data = _get("summary_findings")
|
| 627 |
+
if data and len(data) > 0:
|
| 628 |
+
return jsonify(data[0])
|
| 629 |
+
return jsonify({"content_md": "", "updated": ""})
|
| 630 |
+
|
| 631 |
+
|
| 632 |
+
# --- Sync & Import ---
|
| 633 |
+
|
| 634 |
+
@bp.route("/sync", methods=["POST"])
|
| 635 |
+
def sync():
|
| 636 |
+
global _dashboard_cache_loaded
|
| 637 |
+
with _lock:
|
| 638 |
+
_cache.clear()
|
| 639 |
+
_cache_loaded.clear()
|
| 640 |
+
_dashboard_cache.clear()
|
| 641 |
+
_dashboard_cache_loaded = False
|
| 642 |
+
for name in FILES:
|
| 643 |
+
_get(name)
|
| 644 |
+
return jsonify({"status": "ok"})
|
| 645 |
+
|
| 646 |
+
|
| 647 |
+
@bp.route("/import", methods=["POST"])
|
| 648 |
+
def import_experiments():
|
| 649 |
+
"""Bulk import from experiment.yaml format (as produced by exp-runner)."""
|
| 650 |
+
data = request.get_json()
|
| 651 |
+
items = data if isinstance(data, list) else [data]
|
| 652 |
+
imported = []
|
| 653 |
+
|
| 654 |
+
experiments = _get("experiments")
|
| 655 |
+
runs = _get("runs")
|
| 656 |
+
subs = _get("sub_experiments")
|
| 657 |
+
existing_ids = {e["id"] for e in experiments}
|
| 658 |
+
|
| 659 |
+
for item in items:
|
| 660 |
+
exp_id = item.get("name", "").lower().replace(" ", "_").replace("-", "_")
|
| 661 |
+
if not exp_id:
|
| 662 |
+
continue
|
| 663 |
+
|
| 664 |
+
hypothesis = item.get("hypothesis", {})
|
| 665 |
+
models = item.get("models", [])
|
| 666 |
+
model_names = [m.get("id", "") if isinstance(m, dict) else str(m) for m in models]
|
| 667 |
+
|
| 668 |
+
if exp_id not in existing_ids:
|
| 669 |
+
experiment = {
|
| 670 |
+
"id": exp_id,
|
| 671 |
+
"name": item.get("name", exp_id),
|
| 672 |
+
"research_project": item.get("research_project", ""),
|
| 673 |
+
"hypothesis": {
|
| 674 |
+
"statement": hypothesis.get("statement", "") if isinstance(hypothesis, dict) else str(hypothesis),
|
| 675 |
+
"type": hypothesis.get("type", "exploration") if isinstance(hypothesis, dict) else "exploration",
|
| 676 |
+
"status": hypothesis.get("status", "pending") if isinstance(hypothesis, dict) else "pending",
|
| 677 |
+
"success_criteria": hypothesis.get("success_criteria", "") if isinstance(hypothesis, dict) else "",
|
| 678 |
+
},
|
| 679 |
+
"stage": "active",
|
| 680 |
+
"completeness": 0,
|
| 681 |
+
"models": model_names,
|
| 682 |
+
"tasks": [],
|
| 683 |
+
"tags": item.get("observability", {}).get("tags", []) if isinstance(item.get("observability"), dict) else [],
|
| 684 |
+
"hf_repos": [],
|
| 685 |
+
"wandb_url": "",
|
| 686 |
+
"notes": "",
|
| 687 |
+
"created": item.get("created", _now()),
|
| 688 |
+
"updated": _now(),
|
| 689 |
+
}
|
| 690 |
+
experiments.append(experiment)
|
| 691 |
+
existing_ids.add(exp_id)
|
| 692 |
+
|
| 693 |
+
# Import runs
|
| 694 |
+
for run_data in item.get("runs", []):
|
| 695 |
+
run_id = run_data.get("run_id", f"run_{uuid.uuid4().hex[:8]}")
|
| 696 |
+
if any(r["id"] == run_id and r["experiment_id"] == exp_id for r in runs):
|
| 697 |
+
continue
|
| 698 |
+
run = {
|
| 699 |
+
"id": run_id,
|
| 700 |
+
"experiment_id": exp_id,
|
| 701 |
+
"condition": run_data.get("condition", ""),
|
| 702 |
+
"model": run_data.get("model", ""),
|
| 703 |
+
"cluster": run_data.get("cluster", ""),
|
| 704 |
+
"status": run_data.get("status", "completed"),
|
| 705 |
+
"hf_dataset": run_data.get("hf_dataset", ""),
|
| 706 |
+
"metrics": run_data.get("metrics", {}),
|
| 707 |
+
"timestamp": run_data.get("timestamp", _now()),
|
| 708 |
+
"notes": run_data.get("notes", ""),
|
| 709 |
+
}
|
| 710 |
+
runs.append(run)
|
| 711 |
+
|
| 712 |
+
# Add HF repo to experiment if present
|
| 713 |
+
if run.get("hf_dataset"):
|
| 714 |
+
for exp in experiments:
|
| 715 |
+
if exp["id"] == exp_id:
|
| 716 |
+
existing_repos = {r["repo"] for r in exp.get("hf_repos", [])}
|
| 717 |
+
if run["hf_dataset"] not in existing_repos:
|
| 718 |
+
exp.setdefault("hf_repos", []).append({
|
| 719 |
+
"repo": run["hf_dataset"],
|
| 720 |
+
"description": f"{run['condition']} - {run['model']}",
|
| 721 |
+
"date": run["timestamp"][:10] if run["timestamp"] else "",
|
| 722 |
+
})
|
| 723 |
+
|
| 724 |
+
imported.append(exp_id)
|
| 725 |
+
|
| 726 |
+
_set("experiments", experiments)
|
| 727 |
+
_set("runs", runs)
|
| 728 |
+
_set("sub_experiments", subs)
|
| 729 |
+
|
| 730 |
+
return jsonify({"imported": imported, "count": len(imported)})
|
backend/api/manifest.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Shared manifest query API — loads RACA-PROJECT-MANIFEST from HuggingFace.
|
| 2 |
+
|
| 3 |
+
Provides both a utility function (for use by other blueprints) and a
|
| 4 |
+
shared /api/manifest endpoint so any frontend tab can browse runs.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import json
|
| 8 |
+
import logging
|
| 9 |
+
import os
|
| 10 |
+
|
| 11 |
+
from datasets import load_dataset
|
| 12 |
+
from flask import Blueprint, jsonify, request
|
| 13 |
+
from huggingface_hub import HfApi
|
| 14 |
+
|
| 15 |
+
ORG_NAME = os.environ.get("HF_ORG", "your-org")
|
| 16 |
+
MANIFEST_REPO = f"{ORG_NAME}/RACA-PROJECT-MANIFEST"
|
| 17 |
+
|
| 18 |
+
log = logging.getLogger(__name__)
|
| 19 |
+
|
| 20 |
+
bp = Blueprint("manifest", __name__, url_prefix="/api/manifest")
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def get_manifest():
|
| 24 |
+
"""Load RACA-PROJECT-MANIFEST dataset. Returns list of dicts, or None on failure."""
|
| 25 |
+
try:
|
| 26 |
+
ds = load_dataset(MANIFEST_REPO, split="train")
|
| 27 |
+
return [row for row in ds]
|
| 28 |
+
except Exception as e:
|
| 29 |
+
error_str = str(e).lower()
|
| 30 |
+
if any(phrase in error_str for phrase in [
|
| 31 |
+
"doesn't exist", "404", "no data", "corresponds to no data",
|
| 32 |
+
]):
|
| 33 |
+
return None
|
| 34 |
+
raise
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def _get_live_repos() -> set[str]:
|
| 38 |
+
"""Return set of dataset repo IDs that actually exist in the org."""
|
| 39 |
+
try:
|
| 40 |
+
api = HfApi()
|
| 41 |
+
return {
|
| 42 |
+
ds.id for ds in api.list_datasets(author=ORG_NAME)
|
| 43 |
+
}
|
| 44 |
+
except Exception as e:
|
| 45 |
+
log.warning("Failed to list org datasets for liveness check: %s", e)
|
| 46 |
+
return set()
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def query_runs(prefix: str, validate: bool = True):
|
| 50 |
+
"""Query manifest for datasets matching a name prefix. Returns list of run dicts.
|
| 51 |
+
|
| 52 |
+
Args:
|
| 53 |
+
prefix: filter by dataset_name prefix (empty string = all).
|
| 54 |
+
validate: if True, filter out datasets that no longer exist on HF.
|
| 55 |
+
"""
|
| 56 |
+
manifest = get_manifest()
|
| 57 |
+
if manifest is None:
|
| 58 |
+
return []
|
| 59 |
+
|
| 60 |
+
live_repos = _get_live_repos() if validate else set()
|
| 61 |
+
|
| 62 |
+
runs = []
|
| 63 |
+
for row in manifest:
|
| 64 |
+
name = row.get("dataset_name", "")
|
| 65 |
+
if prefix and not name.startswith(prefix):
|
| 66 |
+
continue
|
| 67 |
+
repo = f"{ORG_NAME}/{name}"
|
| 68 |
+
if validate and live_repos and repo not in live_repos:
|
| 69 |
+
continue
|
| 70 |
+
tags = row.get("tags", [])
|
| 71 |
+
if isinstance(tags, str):
|
| 72 |
+
try:
|
| 73 |
+
tags = json.loads(tags)
|
| 74 |
+
except (json.JSONDecodeError, TypeError):
|
| 75 |
+
tags = []
|
| 76 |
+
metadata = row.get("custom_metadata", "{}")
|
| 77 |
+
if isinstance(metadata, str):
|
| 78 |
+
try:
|
| 79 |
+
metadata = json.loads(metadata)
|
| 80 |
+
except (json.JSONDecodeError, TypeError):
|
| 81 |
+
metadata = {}
|
| 82 |
+
runs.append({
|
| 83 |
+
"dataset_name": name,
|
| 84 |
+
"repo": repo,
|
| 85 |
+
"tags": tags,
|
| 86 |
+
"metadata": metadata,
|
| 87 |
+
"created_at": row.get("created", ""),
|
| 88 |
+
})
|
| 89 |
+
return runs
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
@bp.route("/query", methods=["GET"])
|
| 93 |
+
def query_endpoint():
|
| 94 |
+
"""Generic manifest query — any tab can use this.
|
| 95 |
+
|
| 96 |
+
Query params:
|
| 97 |
+
prefix: filter datasets by name prefix (e.g. 'my-experiment-')
|
| 98 |
+
"""
|
| 99 |
+
prefix = request.args.get("prefix", "")
|
| 100 |
+
try:
|
| 101 |
+
runs = query_runs(prefix)
|
| 102 |
+
return jsonify(runs)
|
| 103 |
+
except Exception as e:
|
| 104 |
+
return jsonify({"error": f"Failed to query manifest: {e}"}), 500
|
backend/api/model_datasets.py
ADDED
|
@@ -0,0 +1,409 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
import hashlib
|
| 4 |
+
from flask import Blueprint, request, jsonify
|
| 5 |
+
from datasets import load_dataset, Dataset
|
| 6 |
+
|
| 7 |
+
bp = Blueprint("model_datasets", __name__, url_prefix="/api/model/datasets")
|
| 8 |
+
|
| 9 |
+
# In-memory cache: id -> {dataset, repo, column, split, n_rows, n_samples}
|
| 10 |
+
_cache: dict[str, dict] = {}
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def _make_id(repo: str, column: str, split: str) -> str:
|
| 14 |
+
key = f"{repo}:{column}:{split}"
|
| 15 |
+
return hashlib.md5(key.encode()).hexdigest()[:12]
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def _load_hf_dataset(repo: str, split: str) -> Dataset:
|
| 19 |
+
if os.path.exists(repo):
|
| 20 |
+
return Dataset.from_parquet(repo)
|
| 21 |
+
return load_dataset(repo, split=split)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def _detect_response_column(columns: list[str], preferred: str) -> str:
|
| 25 |
+
if preferred and preferred in columns:
|
| 26 |
+
return preferred
|
| 27 |
+
for fallback in ["model_responses", "model_response", "response", "responses", "output", "outputs", "completion", "messages"]:
|
| 28 |
+
if fallback in columns:
|
| 29 |
+
return fallback
|
| 30 |
+
# Last resort: return preferred if set, otherwise first column
|
| 31 |
+
return preferred if preferred else (columns[0] if columns else "")
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _detect_prompt_column(columns: list[str], preferred: str) -> str | None:
|
| 35 |
+
if preferred and preferred in columns:
|
| 36 |
+
return preferred
|
| 37 |
+
for fallback in ["formatted_prompt", "prompt", "question", "input", "instruction"]:
|
| 38 |
+
if fallback in columns:
|
| 39 |
+
return fallback
|
| 40 |
+
return None
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def _compute_question_fingerprint(ds: Dataset, n: int = 5) -> str:
|
| 44 |
+
"""Hash first N question texts to fingerprint the question set."""
|
| 45 |
+
questions = []
|
| 46 |
+
for i in range(min(n, len(ds))):
|
| 47 |
+
row = ds[i]
|
| 48 |
+
for qcol in ["question", "prompt", "input", "formatted_prompt"]:
|
| 49 |
+
if qcol in row:
|
| 50 |
+
questions.append(str(row[qcol] or "")[:200])
|
| 51 |
+
break
|
| 52 |
+
return hashlib.md5("||".join(questions).encode()).hexdigest()[:8]
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def _compute_chat_fingerprint(ds: Dataset, column: str, n: int = 5) -> str:
|
| 56 |
+
"""Hash first N user messages from chat-format data to fingerprint the question set."""
|
| 57 |
+
prompts = []
|
| 58 |
+
for i in range(min(n, len(ds))):
|
| 59 |
+
row = ds[i]
|
| 60 |
+
messages = row[column]
|
| 61 |
+
if _is_chat_messages(messages):
|
| 62 |
+
_, user_prompt, _ = _extract_chat_messages(messages)
|
| 63 |
+
prompts.append(user_prompt[:200])
|
| 64 |
+
else:
|
| 65 |
+
prompts.append("")
|
| 66 |
+
return hashlib.md5("||".join(prompts).encode()).hexdigest()[:8]
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def _count_samples(ds: Dataset, column: str) -> int:
|
| 70 |
+
if len(ds) == 0:
|
| 71 |
+
return 0
|
| 72 |
+
first = ds[0][column]
|
| 73 |
+
if isinstance(first, list):
|
| 74 |
+
return len(first)
|
| 75 |
+
return 1
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def _flatten_evals(evals) -> list[bool]:
|
| 79 |
+
if not isinstance(evals, list):
|
| 80 |
+
return [bool(evals)]
|
| 81 |
+
return [
|
| 82 |
+
bool(e[-1]) if isinstance(e, list) and len(e) > 0
|
| 83 |
+
else (bool(e) if not isinstance(e, list) else False)
|
| 84 |
+
for e in evals
|
| 85 |
+
]
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def _is_chat_messages(value) -> bool:
|
| 89 |
+
"""Check if a column value is in chat format (list of role/content dicts)."""
|
| 90 |
+
if not isinstance(value, list) or len(value) == 0:
|
| 91 |
+
return False
|
| 92 |
+
first = value[0]
|
| 93 |
+
return isinstance(first, dict) and "role" in first and "content" in first
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def _extract_chat_messages(messages: list[dict]) -> tuple[str, str, str]:
|
| 97 |
+
"""Extract (system, user_prompt, assistant_response) from chat messages."""
|
| 98 |
+
system = ""
|
| 99 |
+
user_prompt = ""
|
| 100 |
+
assistant_response = ""
|
| 101 |
+
for msg in messages:
|
| 102 |
+
role = msg.get("role", "")
|
| 103 |
+
content = msg.get("content", "") or ""
|
| 104 |
+
if role == "system":
|
| 105 |
+
system = content
|
| 106 |
+
elif role == "user":
|
| 107 |
+
user_prompt = content
|
| 108 |
+
elif role == "assistant":
|
| 109 |
+
assistant_response = content
|
| 110 |
+
return system, user_prompt, assistant_response
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def _extract_reasoning(meta: dict | None) -> str | None:
|
| 114 |
+
"""Extract reasoning/thinking content from response metadata's raw_response."""
|
| 115 |
+
if not meta or not isinstance(meta, dict):
|
| 116 |
+
return None
|
| 117 |
+
raw = meta.get("raw_response")
|
| 118 |
+
if not raw or not isinstance(raw, dict):
|
| 119 |
+
return None
|
| 120 |
+
try:
|
| 121 |
+
msg = raw["choices"][0]["message"]
|
| 122 |
+
return (
|
| 123 |
+
msg.get("reasoning_content")
|
| 124 |
+
or msg.get("thinking")
|
| 125 |
+
or msg.get("reasoning")
|
| 126 |
+
)
|
| 127 |
+
except (KeyError, IndexError, TypeError):
|
| 128 |
+
return None
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def _merge_reasoning_into_response(response: str, reasoning: str | None) -> str:
|
| 132 |
+
"""Prepend <think>{reasoning}</think> to response if reasoning exists
|
| 133 |
+
and isn't already present in the response."""
|
| 134 |
+
if not reasoning:
|
| 135 |
+
return response or ""
|
| 136 |
+
response = response or ""
|
| 137 |
+
# Don't double-add if response already contains the thinking
|
| 138 |
+
if "<think>" in response:
|
| 139 |
+
return response
|
| 140 |
+
return f"<think>{reasoning}</think>\n{response}"
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
def _analyze_trace(text: str) -> dict:
|
| 144 |
+
if not text:
|
| 145 |
+
return dict(total_len=0, think_len=0, answer_len=0,
|
| 146 |
+
backtracks=0, restarts=0, think_text="", answer_text="")
|
| 147 |
+
think_end = text.find("</think>")
|
| 148 |
+
if think_end > 0:
|
| 149 |
+
# Keep raw tags so display is 1:1 with HuggingFace data
|
| 150 |
+
think_text = text[:think_end + 8] # include </think>
|
| 151 |
+
answer_text = text[think_end + 8:].strip()
|
| 152 |
+
else:
|
| 153 |
+
think_text = text
|
| 154 |
+
answer_text = ""
|
| 155 |
+
t = text.lower()
|
| 156 |
+
backtracks = sum(t.count(w) for w in
|
| 157 |
+
["wait,", "wait ", "hmm", "let me try", "try again",
|
| 158 |
+
"another approach", "let me reconsider"])
|
| 159 |
+
restarts = sum(t.count(w) for w in
|
| 160 |
+
["start over", "fresh approach", "different approach", "from scratch"])
|
| 161 |
+
return dict(total_len=len(text), think_len=len(think_text),
|
| 162 |
+
answer_len=len(answer_text), backtracks=backtracks,
|
| 163 |
+
restarts=restarts, think_text=think_text, answer_text=answer_text)
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
@bp.route("/load", methods=["POST"])
|
| 167 |
+
def load_dataset_endpoint():
|
| 168 |
+
data = request.get_json()
|
| 169 |
+
repo = data.get("repo", "").strip()
|
| 170 |
+
if not repo:
|
| 171 |
+
return jsonify({"error": "repo is required"}), 400
|
| 172 |
+
|
| 173 |
+
split = data.get("split", "train")
|
| 174 |
+
preferred_column = data.get("column") or ""
|
| 175 |
+
preferred_prompt_column = data.get("prompt_column") or ""
|
| 176 |
+
|
| 177 |
+
try:
|
| 178 |
+
ds = _load_hf_dataset(repo, split)
|
| 179 |
+
except Exception as e:
|
| 180 |
+
return jsonify({"error": f"Failed to load dataset: {e}"}), 400
|
| 181 |
+
|
| 182 |
+
columns = ds.column_names
|
| 183 |
+
column = _detect_response_column(columns, preferred_column)
|
| 184 |
+
prompt_column = _detect_prompt_column(columns, preferred_prompt_column)
|
| 185 |
+
|
| 186 |
+
if column not in columns:
|
| 187 |
+
return jsonify({
|
| 188 |
+
"error": f"Column '{column}' not found. Available: {columns}"
|
| 189 |
+
}), 400
|
| 190 |
+
|
| 191 |
+
# Detect chat messages format (list of role/content dicts)
|
| 192 |
+
is_chat = False
|
| 193 |
+
if len(ds) > 0 and column in ds.column_names:
|
| 194 |
+
first_val = ds[0][column]
|
| 195 |
+
is_chat = _is_chat_messages(first_val)
|
| 196 |
+
|
| 197 |
+
n_samples = 1 if is_chat else _count_samples(ds, column)
|
| 198 |
+
ds_id = _make_id(repo, column, split)
|
| 199 |
+
|
| 200 |
+
if is_chat:
|
| 201 |
+
# For chat format, fingerprint based on the user message content
|
| 202 |
+
fingerprint = _compute_chat_fingerprint(ds, column)
|
| 203 |
+
else:
|
| 204 |
+
fingerprint = _compute_question_fingerprint(ds)
|
| 205 |
+
|
| 206 |
+
_cache[ds_id] = {
|
| 207 |
+
"dataset": ds,
|
| 208 |
+
"repo": repo,
|
| 209 |
+
"column": column,
|
| 210 |
+
"prompt_column": prompt_column,
|
| 211 |
+
"split": split,
|
| 212 |
+
"n_rows": len(ds),
|
| 213 |
+
"n_samples": n_samples,
|
| 214 |
+
"question_fingerprint": fingerprint,
|
| 215 |
+
"is_chat": is_chat,
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
short_name = repo.rsplit("/", 1)[-1] if "/" in repo else repo
|
| 219 |
+
|
| 220 |
+
return jsonify({
|
| 221 |
+
"id": ds_id,
|
| 222 |
+
"repo": repo,
|
| 223 |
+
"name": short_name,
|
| 224 |
+
"column": column,
|
| 225 |
+
"prompt_column": prompt_column,
|
| 226 |
+
"columns": columns,
|
| 227 |
+
"split": split,
|
| 228 |
+
"n_rows": len(ds),
|
| 229 |
+
"n_samples": n_samples,
|
| 230 |
+
"question_fingerprint": fingerprint,
|
| 231 |
+
})
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
@bp.route("/", methods=["GET"])
|
| 235 |
+
def list_datasets():
|
| 236 |
+
result = []
|
| 237 |
+
for ds_id, info in _cache.items():
|
| 238 |
+
result.append({
|
| 239 |
+
"id": ds_id,
|
| 240 |
+
"repo": info["repo"],
|
| 241 |
+
"name": info["repo"].rsplit("/", 1)[-1] if "/" in info["repo"] else info["repo"],
|
| 242 |
+
"column": info["column"],
|
| 243 |
+
"split": info["split"],
|
| 244 |
+
"n_rows": info["n_rows"],
|
| 245 |
+
"n_samples": info["n_samples"],
|
| 246 |
+
"question_fingerprint": info.get("question_fingerprint", ""),
|
| 247 |
+
})
|
| 248 |
+
return jsonify(result)
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
@bp.route("/<ds_id>/question/<int:idx>", methods=["GET"])
|
| 252 |
+
def get_question(ds_id, idx):
|
| 253 |
+
if ds_id not in _cache:
|
| 254 |
+
return jsonify({"error": "Dataset not loaded"}), 404
|
| 255 |
+
|
| 256 |
+
info = _cache[ds_id]
|
| 257 |
+
ds = info["dataset"]
|
| 258 |
+
column = info["column"]
|
| 259 |
+
|
| 260 |
+
if idx < 0 or idx >= len(ds):
|
| 261 |
+
return jsonify({"error": f"Index {idx} out of range (0-{len(ds)-1})"}), 400
|
| 262 |
+
|
| 263 |
+
row = ds[idx]
|
| 264 |
+
is_chat = info.get("is_chat", False)
|
| 265 |
+
|
| 266 |
+
if is_chat:
|
| 267 |
+
# Chat messages format: extract assistant response, user prompt, system
|
| 268 |
+
messages = row[column]
|
| 269 |
+
system_msg, user_prompt, assistant_response = _extract_chat_messages(messages)
|
| 270 |
+
responses_raw = [assistant_response]
|
| 271 |
+
prompt_text = user_prompt
|
| 272 |
+
question = user_prompt
|
| 273 |
+
else:
|
| 274 |
+
responses_raw = row[column]
|
| 275 |
+
if not isinstance(responses_raw, list):
|
| 276 |
+
responses_raw = [responses_raw]
|
| 277 |
+
|
| 278 |
+
# Check for {column}__metadata to recover reasoning/thinking content
|
| 279 |
+
meta_column = f"{column}__metadata"
|
| 280 |
+
response_metas = None
|
| 281 |
+
if meta_column in row:
|
| 282 |
+
response_metas = row[meta_column]
|
| 283 |
+
if not isinstance(response_metas, list):
|
| 284 |
+
response_metas = [response_metas]
|
| 285 |
+
|
| 286 |
+
# Merge reasoning from metadata into responses
|
| 287 |
+
merged_responses = []
|
| 288 |
+
for i, resp in enumerate(responses_raw):
|
| 289 |
+
meta = response_metas[i] if response_metas and i < len(response_metas) else None
|
| 290 |
+
reasoning = _extract_reasoning(meta)
|
| 291 |
+
merged_responses.append(_merge_reasoning_into_response(resp, reasoning))
|
| 292 |
+
responses_raw = merged_responses
|
| 293 |
+
|
| 294 |
+
# Prompt text from configured prompt column
|
| 295 |
+
prompt_text = ""
|
| 296 |
+
prompt_col = info.get("prompt_column")
|
| 297 |
+
if prompt_col and prompt_col in row:
|
| 298 |
+
val = row[prompt_col]
|
| 299 |
+
if isinstance(val, str):
|
| 300 |
+
prompt_text = val
|
| 301 |
+
elif isinstance(val, list):
|
| 302 |
+
prompt_text = json.dumps(val)
|
| 303 |
+
elif val is not None:
|
| 304 |
+
prompt_text = str(val)
|
| 305 |
+
|
| 306 |
+
question = ""
|
| 307 |
+
for qcol in ["question", "prompt", "input", "problem", "formatted_prompt"]:
|
| 308 |
+
if qcol in row:
|
| 309 |
+
val = row[qcol] or ""
|
| 310 |
+
if isinstance(val, str):
|
| 311 |
+
question = val
|
| 312 |
+
elif isinstance(val, list):
|
| 313 |
+
question = json.dumps(val)
|
| 314 |
+
else:
|
| 315 |
+
question = str(val)
|
| 316 |
+
break
|
| 317 |
+
|
| 318 |
+
eval_correct = []
|
| 319 |
+
if "eval_correct" in row:
|
| 320 |
+
eval_correct = _flatten_evals(row["eval_correct"])
|
| 321 |
+
elif "correct" in row:
|
| 322 |
+
eval_correct = _flatten_evals(row["correct"])
|
| 323 |
+
|
| 324 |
+
# Check extractions with column-aware name
|
| 325 |
+
extractions = []
|
| 326 |
+
extractions_col = f"{column}__extractions"
|
| 327 |
+
for ecol in [extractions_col, "response__extractions"]:
|
| 328 |
+
if ecol in row:
|
| 329 |
+
ext = row[ecol]
|
| 330 |
+
if isinstance(ext, list):
|
| 331 |
+
extractions = [str(e) for e in ext]
|
| 332 |
+
break
|
| 333 |
+
|
| 334 |
+
metadata = {}
|
| 335 |
+
if "metadata" in row:
|
| 336 |
+
metadata = row["metadata"] if isinstance(row["metadata"], dict) else {}
|
| 337 |
+
|
| 338 |
+
analyses = [_analyze_trace(r or "") for r in responses_raw]
|
| 339 |
+
|
| 340 |
+
return jsonify({
|
| 341 |
+
"question": question,
|
| 342 |
+
"prompt_text": prompt_text,
|
| 343 |
+
"responses": [r or "" for r in responses_raw],
|
| 344 |
+
"eval_correct": eval_correct,
|
| 345 |
+
"extractions": extractions,
|
| 346 |
+
"metadata": metadata,
|
| 347 |
+
"analyses": analyses,
|
| 348 |
+
"n_samples": len(responses_raw),
|
| 349 |
+
"index": idx,
|
| 350 |
+
})
|
| 351 |
+
|
| 352 |
+
|
| 353 |
+
@bp.route("/<ds_id>/summary", methods=["GET"])
|
| 354 |
+
def get_summary(ds_id):
|
| 355 |
+
if ds_id not in _cache:
|
| 356 |
+
return jsonify({"error": "Dataset not loaded"}), 404
|
| 357 |
+
|
| 358 |
+
info = _cache[ds_id]
|
| 359 |
+
ds = info["dataset"]
|
| 360 |
+
n_rows = info["n_rows"]
|
| 361 |
+
n_samples = info["n_samples"]
|
| 362 |
+
|
| 363 |
+
# Support both "eval_correct" and "correct" column names
|
| 364 |
+
eval_col = None
|
| 365 |
+
if "eval_correct" in ds.column_names:
|
| 366 |
+
eval_col = "eval_correct"
|
| 367 |
+
elif "correct" in ds.column_names:
|
| 368 |
+
eval_col = "correct"
|
| 369 |
+
|
| 370 |
+
if eval_col is None:
|
| 371 |
+
return jsonify({
|
| 372 |
+
"n_rows": n_rows,
|
| 373 |
+
"n_samples": n_samples,
|
| 374 |
+
"has_eval": False,
|
| 375 |
+
})
|
| 376 |
+
|
| 377 |
+
pass_at = {}
|
| 378 |
+
for k in [1, 2, 4, 8]:
|
| 379 |
+
if k > n_samples:
|
| 380 |
+
break
|
| 381 |
+
correct = sum(1 for i in range(n_rows)
|
| 382 |
+
if any(_flatten_evals(ds[i][eval_col])[:k]))
|
| 383 |
+
pass_at[k] = {"correct": correct, "total": n_rows,
|
| 384 |
+
"rate": correct / n_rows if n_rows > 0 else 0}
|
| 385 |
+
|
| 386 |
+
total_samples = n_rows * n_samples
|
| 387 |
+
total_correct = sum(
|
| 388 |
+
sum(_flatten_evals(ds[i][eval_col]))
|
| 389 |
+
for i in range(n_rows)
|
| 390 |
+
)
|
| 391 |
+
|
| 392 |
+
return jsonify({
|
| 393 |
+
"n_rows": n_rows,
|
| 394 |
+
"n_samples": n_samples,
|
| 395 |
+
"has_eval": True,
|
| 396 |
+
"sample_accuracy": {
|
| 397 |
+
"correct": total_correct,
|
| 398 |
+
"total": total_samples,
|
| 399 |
+
"rate": total_correct / total_samples if total_samples > 0 else 0,
|
| 400 |
+
},
|
| 401 |
+
"pass_at": pass_at,
|
| 402 |
+
})
|
| 403 |
+
|
| 404 |
+
|
| 405 |
+
@bp.route("/<ds_id>", methods=["DELETE"])
|
| 406 |
+
def unload_dataset(ds_id):
|
| 407 |
+
if ds_id in _cache:
|
| 408 |
+
del _cache[ds_id]
|
| 409 |
+
return jsonify({"status": "ok"})
|
backend/api/plan_revisions.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from flask import Blueprint, jsonify
|
| 3 |
+
from datasets import load_dataset
|
| 4 |
+
|
| 5 |
+
bp = Blueprint("plan_revisions", __name__, url_prefix="/api/plan-revisions")
|
| 6 |
+
|
| 7 |
+
HF_REPO = "timchen0618/bcp-plan-revisions-v1"
|
| 8 |
+
|
| 9 |
+
_cache: dict | None = None
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def _load():
|
| 13 |
+
global _cache
|
| 14 |
+
if _cache is not None:
|
| 15 |
+
return _cache
|
| 16 |
+
|
| 17 |
+
ds = load_dataset(HF_REPO, split="train")
|
| 18 |
+
|
| 19 |
+
# Group: condition -> query_id -> sorted list of revision entries
|
| 20 |
+
grouped: dict[str, dict[str, list]] = {}
|
| 21 |
+
for row in ds:
|
| 22 |
+
cond = row["condition"]
|
| 23 |
+
qid = str(row["query_id"])
|
| 24 |
+
grouped.setdefault(cond, {}).setdefault(qid, [])
|
| 25 |
+
grouped[cond][qid].append({
|
| 26 |
+
"revision_index": row["revision_index"],
|
| 27 |
+
"source": row["source"],
|
| 28 |
+
"plan_text": row["plan_text"],
|
| 29 |
+
"total_revisions": row["total_revisions"],
|
| 30 |
+
"status": row["status"],
|
| 31 |
+
})
|
| 32 |
+
|
| 33 |
+
# Sort revisions within each group
|
| 34 |
+
for cond in grouped:
|
| 35 |
+
for qid in grouped[cond]:
|
| 36 |
+
grouped[cond][qid].sort(key=lambda r: r["revision_index"])
|
| 37 |
+
|
| 38 |
+
conditions = sorted(grouped.keys())
|
| 39 |
+
_cache = {"conditions": conditions, "data": grouped}
|
| 40 |
+
return _cache
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
@bp.get("/")
|
| 44 |
+
def get_data():
|
| 45 |
+
try:
|
| 46 |
+
result = _load()
|
| 47 |
+
return jsonify(result)
|
| 48 |
+
except Exception as e:
|
| 49 |
+
return jsonify({"error": str(e)}), 500
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
@bp.post("/reload")
|
| 53 |
+
def reload_data():
|
| 54 |
+
global _cache
|
| 55 |
+
_cache = None
|
| 56 |
+
try:
|
| 57 |
+
result = _load()
|
| 58 |
+
return jsonify({"status": "ok", "conditions": result["conditions"]})
|
| 59 |
+
except Exception as e:
|
| 60 |
+
return jsonify({"error": str(e)}), 500
|
backend/api/presets.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
import uuid
|
| 4 |
+
import tempfile
|
| 5 |
+
import threading
|
| 6 |
+
from flask import Blueprint, request, jsonify
|
| 7 |
+
|
| 8 |
+
bp = Blueprint("presets", __name__, url_prefix="/api/presets")
|
| 9 |
+
|
| 10 |
+
HF_ORG = os.environ.get("HF_ORG", "your-org")
|
| 11 |
+
PRESETS_REPO = f"{HF_ORG}/RACA-VIS-PRESETS"
|
| 12 |
+
VALID_TYPES = {"model"}
|
| 13 |
+
LOCAL_PRESETS_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "presets")
|
| 14 |
+
|
| 15 |
+
# In-memory cache: vis_type -> list[dict]
|
| 16 |
+
_cache: dict[str, list[dict]] = {}
|
| 17 |
+
_cache_loaded: set[str] = set()
|
| 18 |
+
_lock = threading.Lock()
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def _ensure_local_dir():
|
| 22 |
+
os.makedirs(LOCAL_PRESETS_DIR, exist_ok=True)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def _local_path(vis_type: str) -> str:
|
| 26 |
+
_ensure_local_dir()
|
| 27 |
+
return os.path.join(LOCAL_PRESETS_DIR, f"{vis_type}_presets.json")
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def _download_presets(vis_type: str) -> list[dict]:
|
| 31 |
+
"""Download presets from HuggingFace, falling back to local file."""
|
| 32 |
+
try:
|
| 33 |
+
from huggingface_hub import hf_hub_download
|
| 34 |
+
path = hf_hub_download(
|
| 35 |
+
PRESETS_REPO,
|
| 36 |
+
f"{vis_type}_presets.json",
|
| 37 |
+
repo_type="dataset",
|
| 38 |
+
)
|
| 39 |
+
with open(path) as f:
|
| 40 |
+
presets = json.load(f)
|
| 41 |
+
# Cache locally for offline fallback
|
| 42 |
+
with open(_local_path(vis_type), "w") as f:
|
| 43 |
+
json.dump(presets, f, indent=2)
|
| 44 |
+
return presets
|
| 45 |
+
except Exception:
|
| 46 |
+
# Fall back to local cache
|
| 47 |
+
local = _local_path(vis_type)
|
| 48 |
+
if os.path.exists(local):
|
| 49 |
+
with open(local) as f:
|
| 50 |
+
return json.load(f)
|
| 51 |
+
return []
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def _upload_presets(vis_type: str, presets: list[dict]):
|
| 55 |
+
"""Upload presets to HuggingFace (best-effort, non-blocking)."""
|
| 56 |
+
# Always save locally first
|
| 57 |
+
with open(_local_path(vis_type), "w") as f:
|
| 58 |
+
json.dump(presets, f, indent=2)
|
| 59 |
+
|
| 60 |
+
def _do_upload():
|
| 61 |
+
try:
|
| 62 |
+
from huggingface_hub import HfApi
|
| 63 |
+
api = HfApi()
|
| 64 |
+
# Ensure repo exists
|
| 65 |
+
try:
|
| 66 |
+
api.create_repo(
|
| 67 |
+
PRESETS_REPO,
|
| 68 |
+
repo_type="dataset",
|
| 69 |
+
exist_ok=True,
|
| 70 |
+
)
|
| 71 |
+
except Exception:
|
| 72 |
+
pass
|
| 73 |
+
with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False) as f:
|
| 74 |
+
json.dump(presets, f, indent=2)
|
| 75 |
+
tmp = f.name
|
| 76 |
+
api.upload_file(
|
| 77 |
+
path_or_fileobj=tmp,
|
| 78 |
+
path_in_repo=f"{vis_type}_presets.json",
|
| 79 |
+
repo_id=PRESETS_REPO,
|
| 80 |
+
repo_type="dataset",
|
| 81 |
+
)
|
| 82 |
+
os.unlink(tmp)
|
| 83 |
+
except Exception as e:
|
| 84 |
+
print(f"[presets] HF upload failed for {vis_type}: {e}")
|
| 85 |
+
|
| 86 |
+
threading.Thread(target=_do_upload, daemon=True).start()
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def _get_presets(vis_type: str) -> list[dict]:
|
| 90 |
+
"""Get presets for a visualizer type, downloading if needed."""
|
| 91 |
+
with _lock:
|
| 92 |
+
if vis_type not in _cache_loaded:
|
| 93 |
+
_cache[vis_type] = _download_presets(vis_type)
|
| 94 |
+
_cache_loaded.add(vis_type)
|
| 95 |
+
return list(_cache.get(vis_type, []))
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def _set_presets(vis_type: str, presets: list[dict]):
|
| 99 |
+
"""Update presets in cache and sync to HF."""
|
| 100 |
+
with _lock:
|
| 101 |
+
_cache[vis_type] = presets
|
| 102 |
+
_cache_loaded.add(vis_type)
|
| 103 |
+
_upload_presets(vis_type, presets)
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
@bp.route("/<vis_type>", methods=["GET"])
|
| 107 |
+
def list_presets(vis_type):
|
| 108 |
+
if vis_type not in VALID_TYPES:
|
| 109 |
+
return jsonify({"error": f"Invalid type. Must be one of: {VALID_TYPES}"}), 400
|
| 110 |
+
return jsonify(_get_presets(vis_type))
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
@bp.route("/<vis_type>", methods=["POST"])
|
| 114 |
+
def create_preset(vis_type):
|
| 115 |
+
if vis_type not in VALID_TYPES:
|
| 116 |
+
return jsonify({"error": f"Invalid type. Must be one of: {VALID_TYPES}"}), 400
|
| 117 |
+
|
| 118 |
+
data = request.get_json()
|
| 119 |
+
name = data.get("name", "").strip()
|
| 120 |
+
|
| 121 |
+
if not name:
|
| 122 |
+
return jsonify({"error": "name is required"}), 400
|
| 123 |
+
|
| 124 |
+
preset = {
|
| 125 |
+
"id": uuid.uuid4().hex[:8],
|
| 126 |
+
"name": name,
|
| 127 |
+
}
|
| 128 |
+
# Include type-specific fields
|
| 129 |
+
repo = data.get("repo", "").strip()
|
| 130 |
+
if not repo:
|
| 131 |
+
return jsonify({"error": "repo is required"}), 400
|
| 132 |
+
preset["repo"] = repo
|
| 133 |
+
preset["split"] = data.get("split", "train")
|
| 134 |
+
|
| 135 |
+
if vis_type == "model":
|
| 136 |
+
preset["column"] = data.get("column", "model_responses")
|
| 137 |
+
|
| 138 |
+
presets = _get_presets(vis_type)
|
| 139 |
+
presets.append(preset)
|
| 140 |
+
_set_presets(vis_type, presets)
|
| 141 |
+
|
| 142 |
+
return jsonify(preset), 201
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
@bp.route("/<vis_type>/<preset_id>", methods=["PUT"])
|
| 146 |
+
def update_preset(vis_type, preset_id):
|
| 147 |
+
if vis_type not in VALID_TYPES:
|
| 148 |
+
return jsonify({"error": f"Invalid type. Must be one of: {VALID_TYPES}"}), 400
|
| 149 |
+
|
| 150 |
+
data = request.get_json()
|
| 151 |
+
presets = _get_presets(vis_type)
|
| 152 |
+
|
| 153 |
+
for p in presets:
|
| 154 |
+
if p["id"] == preset_id:
|
| 155 |
+
if "name" in data:
|
| 156 |
+
p["name"] = data["name"].strip()
|
| 157 |
+
if "column" in data:
|
| 158 |
+
p["column"] = data["column"]
|
| 159 |
+
if "split" in data:
|
| 160 |
+
p["split"] = data["split"]
|
| 161 |
+
if "config" in data:
|
| 162 |
+
p["config"] = data["config"]
|
| 163 |
+
_set_presets(vis_type, presets)
|
| 164 |
+
return jsonify(p)
|
| 165 |
+
|
| 166 |
+
return jsonify({"error": "not found"}), 404
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
@bp.route("/<vis_type>/<preset_id>", methods=["DELETE"])
|
| 170 |
+
def delete_preset(vis_type, preset_id):
|
| 171 |
+
if vis_type not in VALID_TYPES:
|
| 172 |
+
return jsonify({"error": f"Invalid type. Must be one of: {VALID_TYPES}"}), 400
|
| 173 |
+
|
| 174 |
+
presets = _get_presets(vis_type)
|
| 175 |
+
presets = [p for p in presets if p["id"] != preset_id]
|
| 176 |
+
_set_presets(vis_type, presets)
|
| 177 |
+
return jsonify({"status": "ok"})
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
@bp.route("/sync", methods=["POST"])
|
| 181 |
+
def sync_presets():
|
| 182 |
+
"""Force re-download presets from HF."""
|
| 183 |
+
with _lock:
|
| 184 |
+
_cache.clear()
|
| 185 |
+
_cache_loaded.clear()
|
| 186 |
+
for vt in VALID_TYPES:
|
| 187 |
+
_get_presets(vt)
|
| 188 |
+
return jsonify({"status": "ok"})
|
backend/app.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Flask
|
| 2 |
+
from flask_cors import CORS
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def create_app():
|
| 6 |
+
app = Flask(__name__, static_folder="../frontend/dist", static_url_path="/")
|
| 7 |
+
CORS(app)
|
| 8 |
+
|
| 9 |
+
from backend.api import model_datasets, presets, experiments, manifest, plan_revisions
|
| 10 |
+
app.register_blueprint(model_datasets.bp)
|
| 11 |
+
app.register_blueprint(presets.bp)
|
| 12 |
+
app.register_blueprint(experiments.bp)
|
| 13 |
+
app.register_blueprint(manifest.bp)
|
| 14 |
+
app.register_blueprint(plan_revisions.bp)
|
| 15 |
+
|
| 16 |
+
@app.route("/api/health")
|
| 17 |
+
def health():
|
| 18 |
+
return {"status": "ok"}
|
| 19 |
+
|
| 20 |
+
@app.route("/", defaults={"path": ""})
|
| 21 |
+
@app.route("/<path:path>")
|
| 22 |
+
def serve_frontend(path):
|
| 23 |
+
return app.send_static_file("index.html")
|
| 24 |
+
|
| 25 |
+
return app
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
app = create_app()
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def main():
|
| 32 |
+
app.run(debug=True, port=8080)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
if __name__ == "__main__":
|
| 36 |
+
main()
|
backend/data/activity_logs.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"onboarding": [
|
| 3 |
+
{
|
| 4 |
+
"timestamp": "2026-04-06T00:00:00Z",
|
| 5 |
+
"type": "milestone",
|
| 6 |
+
"scope": "experiment",
|
| 7 |
+
"author": "agent",
|
| 8 |
+
"message": "Experiment design complete: Qwen3-1.7B on Countdown, 10-sample canary, torch cluster"
|
| 9 |
+
},
|
| 10 |
+
{
|
| 11 |
+
"timestamp": "2026-04-06T00:01:00Z",
|
| 12 |
+
"type": "milestone",
|
| 13 |
+
"scope": "experiment",
|
| 14 |
+
"author": "agent",
|
| 15 |
+
"message": "Red-team review passed. Key checks: max_tokens=4096 (marginal, monitor for truncation), prompt format verified against reference, evaluation must use CountdownJudge AST-based eval (not string match). Output schema defined with 8 columns including finish_reason."
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
"timestamp": "2026-04-06T00:05:00Z",
|
| 19 |
+
"type": "milestone",
|
| 20 |
+
"scope": "job",
|
| 21 |
+
"author": "agent",
|
| 22 |
+
"message": "Canary job 5550586 submitted to torch (h200_courant, 1x H200, 30min). Qwen3-1.7B on 10 Countdown problems. Results \u2192 timchen0618/onboarding-countdown-qwen3-1.7b"
|
| 23 |
+
},
|
| 24 |
+
{
|
| 25 |
+
"timestamp": "2026-04-06T14:22:00Z",
|
| 26 |
+
"type": "note",
|
| 27 |
+
"scope": "job",
|
| 28 |
+
"author": "agent",
|
| 29 |
+
"message": "Job 5550712 failed: OOM during vLLM CUDA graph compilation. Fixed: added enforce_eager=True to skip compilation. Resubmitted as job 5552202."
|
| 30 |
+
},
|
| 31 |
+
{
|
| 32 |
+
"timestamp": "2026-04-06T14:22:00Z",
|
| 33 |
+
"type": "milestone",
|
| 34 |
+
"scope": "job",
|
| 35 |
+
"author": "agent",
|
| 36 |
+
"message": "Job 5552202 submitted to torch (enforce_eager=True fix applied)"
|
| 37 |
+
},
|
| 38 |
+
{
|
| 39 |
+
"timestamp": "2026-04-06T14:36:00Z",
|
| 40 |
+
"type": "note",
|
| 41 |
+
"scope": "job",
|
| 42 |
+
"author": "agent",
|
| 43 |
+
"message": "Job 5552202 failed: OOM (CPU RAM) from vLLM spawn subprocess. Switched to transformers AutoModelForCausalLM \u2014 no subprocess needed. Resubmitted as job 5552573."
|
| 44 |
+
},
|
| 45 |
+
{
|
| 46 |
+
"timestamp": "2026-04-06T15:05:00Z",
|
| 47 |
+
"type": "result",
|
| 48 |
+
"scope": "artifact",
|
| 49 |
+
"author": "agent",
|
| 50 |
+
"message": "Job 5553179 completed: 10/10 samples generated. Evaluator bugs fixed (last <answer> block extraction + cascaded step validation). Re-scored: 10/10 correct (100%). Uploaded to timchen0618/onboarding-countdown-qwen3-1.7b."
|
| 51 |
+
}
|
| 52 |
+
]
|
| 53 |
+
}
|
backend/data/artifacts.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
[]
|
backend/data/experiment_notes.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"id": "onboarding__note__Users_hungting_Desktop_Lab_Projects_notes_experiments_onboarding_EXPERIMENT_README_md",
|
| 4 |
+
"experiment_id": "onboarding",
|
| 5 |
+
"title": "EXPERIMENT_README.md",
|
| 6 |
+
"filename": "EXPERIMENT_README.md",
|
| 7 |
+
"relative_path": "/Users/hungting/Desktop/Lab/Projects/notes/experiments/onboarding/EXPERIMENT_README.md",
|
| 8 |
+
"content_md": "# Welcome to RACA\n\nThis is a sample experiment to show you how the dashboard works. You're looking at the **Overview** tab right now \u2014 it displays the experiment's README (this file).\n\nEverything you see here is generated from plain files in `notes/experiments/onboarding/`. You can browse them in your editor anytime.\n\n## How This Dashboard Works\n\nEach experiment has several tabs at the top. Here's what they do:\n\n### Overview (you are here)\n\nDisplays the experiment's README and any notes you've written in the `user/` folder. This is the main landing page for each experiment \u2014 a summary of what the experiment is, what you're investigating, and what you found.\n\n### Red Team Brief\n\nBefore any experiment runs, RACA reviews the design for potential problems \u2014 wrong evaluation metrics, truncated outputs, missing baselines, wasted compute. The brief lives at `red_team_brief.md`. This tab will be empty until you run your first real experiment.\n\n### Timeline\n\nA chronological log of everything that happened: when jobs were submitted, when artifacts were uploaded, when bugs were found and fixed. This is auto-generated from `activity_log.jsonl` \u2014 RACA writes to it as events happen.\n\n### Runs\n\nTracks each job submission \u2014 which model, which cluster, what status (pending, running, completed, failed), and links to the HuggingFace dataset with the results. Empty until you run something.\n\n### Artifacts\n\nLinks to all HuggingFace datasets produced by this experiment \u2014 canary runs, partial results, final data. Each artifact has metadata about what generated it. Empty until artifacts are uploaded.\n\n### Files\n\nAll the markdown and YAML files in the experiment folder. Click any file to read it. This is a quick way to browse the experiment's configuration and notes without leaving the dashboard.\n\n## Folder Structure\n\n```\nnotes/experiments/onboarding/\n EXPERIMENT_README.md \u2190 this file (shows in Overview tab)\n experiment.yaml \u2190 config: hypothesis, models, tasks\n flow_state.json \u2190 current phase (design/running/complete)\n HUGGINGFACE_REPOS.md \u2190 links to all uploaded datasets\n questions.md \u2190 research questions (read-only)\n red_team_brief.md \u2190 created during preflight review\n activity_log.jsonl \u2190 timeline entries (auto-generated)\n user/ \u2190 YOUR notes \u2014 RACA doesn't touch these\n README.md \u2190 your interpretation and observations\n FINDINGS.md \u2190 key results and surprises\n DECISIONS.md \u2190 design decisions and rationale\n summary.md \u2190 one-paragraph summary when done\n```\n\n**Most of this is automated.** RACA creates and updates the experiment files, uploads artifacts, and keeps the timeline current. The only files you write are in `user/` \u2014 that's your space for notes, findings, and decisions.\n\n## What's Next\n\nThis sample experiment hasn't been run yet \u2014 it's just here to show you the structure. When you're ready to run a real experiment, just tell RACA:\n\n> *I want to test whether Qwen3-8B follows complex instructions better than Llama-3.1-8B*\n\nOr try the full guided tutorial:\n\n> */raca:experiment-tutorial*\n",
|
| 9 |
+
"created": "",
|
| 10 |
+
"updated": ""
|
| 11 |
+
},
|
| 12 |
+
{
|
| 13 |
+
"id": "onboarding__note__Users_hungting_Desktop_Lab_Projects_notes_experiments_onboarding_HUGGINGFACE_REPOS_md",
|
| 14 |
+
"experiment_id": "onboarding",
|
| 15 |
+
"title": "HUGGINGFACE_REPOS.md",
|
| 16 |
+
"filename": "HUGGINGFACE_REPOS.md",
|
| 17 |
+
"relative_path": "/Users/hungting/Desktop/Lab/Projects/notes/experiments/onboarding/HUGGINGFACE_REPOS.md",
|
| 18 |
+
"content_md": "# HuggingFace Repositories\n\n| Dataset | Date | Rows | Purpose |\n|---------|------|------|---------|\n| [onboarding-countdown-qwen3-1.7b \u2014 10 rows, 10/10 correct (2026-04-06)](https://huggingface.co/datasets/timchen0618/onboarding-countdown-qwen3-1.7b) | 2026-04-06 | 10 | Qwen3-1.7B on Countdown (3-4 operands, targets 1-99, seed=42) |\n",
|
| 19 |
+
"created": "",
|
| 20 |
+
"updated": ""
|
| 21 |
+
},
|
| 22 |
+
{
|
| 23 |
+
"id": "onboarding__note__Users_hungting_Desktop_Lab_Projects_notes_experiments_onboarding_questions_md",
|
| 24 |
+
"experiment_id": "onboarding",
|
| 25 |
+
"title": "questions.md",
|
| 26 |
+
"filename": "questions.md",
|
| 27 |
+
"relative_path": "/Users/hungting/Desktop/Lab/Projects/notes/experiments/onboarding/questions.md",
|
| 28 |
+
"content_md": "# Research Questions\n\n1. Can Qwen3-1.7B solve basic Countdown problems (4 numbers, targets < 100)?\n2. What reasoning strategies does the model use (trial-and-error, systematic search, pattern matching)?\n3. Where does the model fail \u2014 wrong arithmetic, giving up, or invalid expressions?\n",
|
| 29 |
+
"created": "",
|
| 30 |
+
"updated": ""
|
| 31 |
+
},
|
| 32 |
+
{
|
| 33 |
+
"id": "onboarding__note__Users_hungting_Desktop_Lab_Projects_notes_experiments_onboarding_red_team_brief_md",
|
| 34 |
+
"experiment_id": "onboarding",
|
| 35 |
+
"title": "red_team_brief.md",
|
| 36 |
+
"filename": "red_team_brief.md",
|
| 37 |
+
"relative_path": "/Users/hungting/Desktop/Lab/Projects/notes/experiments/onboarding/red_team_brief.md",
|
| 38 |
+
"content_md": "# Red Team Brief \u2014 onboarding\n\n**Experiment:** Qwen3-1.7B on Countdown (tutorial/canary)\n**Reviewer:** agent\n**Date:** 2026-04-06\n**Status:** PASS\n\n---\n\n## Hypothesis\n\n> Qwen3-1.7B can solve basic Countdown arithmetic problems (>50% accuracy on 3-4 operand problems)\n\nThis is an exploratory baseline \u2014 a reasonable first question. No prior published result exists for Qwen3-1.7B specifically on this exact config, so the hypothesis is testable and non-trivial.\n\n---\n\n## Config Review\n\n### Model\n- **Qwen/Qwen3-1.7B** \u2014 small instruct model, appropriate for a tutorial canary\n- Known to handle arithmetic reasoning; 1.7B is at the edge of competence for Countdown (expect 20-50% accuracy)\n\n### max_tokens: 4096\n- **Status: PASS (marginal)**\n- Reference minimum is 2048-4096. We are at the ceiling of \"minimum\". For Qwen3-1.7B reasoning traces, 4096 should be sufficient for 3-4 operand problems.\n- **Watch for truncation in outputs** \u2014 if `finish_reason == \"length\"` appears in any response, flag immediately.\n\n### Prompt Format\n- **Must use the CoT + in-context examples prompt from the reference** (not the TinyZero `<think>` variant, since we are not RL-training)\n- Template:\n ```\n Answer the following problem. Explain your reasoning step by step. When you are finished, give your answer in this format: <answer>(your answer)</answer>.\n \n # Problem\n Using the numbers in the list [{numbers}], create an equation that equals {target}. ...\n ```\n- Answer extraction: regex on `<answer>...</answer>` tags\n\n### Evaluation Method\n- **Must use `CountdownJudge.validate_countdown_solution()` \u2014 NOT string matching**\n- Located at: `packages/custom_evaluations/custom_evaluations/sources/countdown/countdown_judge.py`\n- Handles: AST-based evaluation, Unicode operator normalization, multiset number validation\n- **Failure mode if string match is used:** \"3 + 5\" and \"5 + 3\" would score differently. This is wrong.\n\n### Dataset Generation\n- 10 samples (canary size \u2014 fine)\n- Use forward generation (inline, no dependencies): 3-4 operands, targets 1-99\n- Do NOT require all numbers used \u2014 subset is sufficient (this is the easier, more standard setting)\n\n---\n\n## Failure Modes & Mitigations\n\n| Risk | Severity | Mitigation |\n|------|----------|------------|\n| Truncated outputs (`finish_reason=length`) | HIGH | Check every row \u2014 flag if any truncation occurs |\n| String matching instead of equation eval | HIGH | Use `CountdownJudge` \u2014 verified AST-based |\n| Model outputs `\u00d7` instead of `*` | MEDIUM | `CountdownJudge` handles Unicode normalization |\n| `<answer>` tag missing in output | MEDIUM | Log separately as \"format failures\" vs \"wrong answer\" |\n| Duplicate problems in 10 samples | LOW | Use `random.seed(42)` for reproducibility |\n\n---\n\n## Expected Results\n\nBased on typical results from the reference file:\n- Qwen3-1.7B is smaller than Qwen2.5-3B (which achieves 70-80% after GRPO training)\n- For a **pre-GRPO instruct model** at this size, expect roughly **20-50% accuracy**\n- If accuracy is <10%, suspect prompt format issue or evaluation bug \u2014 investigate before scaling\n\n---\n\n## Output Schema\n\nExpected columns in the HuggingFace dataset:\n\n| Column | Type | Description |\n|--------|------|-------------|\n| `prompt` | str | Full prompt sent to the model |\n| `model_response` | str | Full untruncated model output |\n| `model` | str | `Qwen/Qwen3-1.7B` |\n| `numbers` | list[int] | Available operands |\n| `target` | int | Target value |\n| `correct` | bool | Whether the answer evaluates correctly |\n| `extracted_answer` | str | Parsed content from `<answer>` tags (null if missing) |\n| `finish_reason` | str | `stop` or `length` \u2014 flag any `length` rows |\n\n---\n\n## Validation Criteria (for data-validator)\n\nA healthy artifact must satisfy ALL of the following:\n1. All rows have non-empty `model_response`\n2. Zero rows with `finish_reason == \"length\"` (no truncation)\n3. `correct` column is boolean, computed via equation evaluation (not string match)\n4. `extracted_answer` is null only when model didn't produce `<answer>` tags\n5. Row count matches n_samples (10)\n6. No degenerate outputs (empty strings, repeated tokens, only whitespace)\n",
|
| 39 |
+
"created": "",
|
| 40 |
+
"updated": ""
|
| 41 |
+
}
|
| 42 |
+
]
|
backend/data/experiments.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"id": "onboarding",
|
| 4 |
+
"name": "Onboarding",
|
| 5 |
+
"research_project": "",
|
| 6 |
+
"hypothesis": {
|
| 7 |
+
"statement": "Qwen3-1.7B can solve basic Countdown arithmetic problems",
|
| 8 |
+
"type": "exploratory",
|
| 9 |
+
"status": "active",
|
| 10 |
+
"success_criteria": "Model produces valid arithmetic expressions that reach the target number on >50% of problems"
|
| 11 |
+
},
|
| 12 |
+
"stage": "active",
|
| 13 |
+
"completeness": 4,
|
| 14 |
+
"models": [],
|
| 15 |
+
"tasks": [],
|
| 16 |
+
"tags": [
|
| 17 |
+
"countdown",
|
| 18 |
+
"reasoning",
|
| 19 |
+
"onboarding",
|
| 20 |
+
"tutorial"
|
| 21 |
+
],
|
| 22 |
+
"hf_repos": [
|
| 23 |
+
{
|
| 24 |
+
"repo": "timchen0618/onboarding-countdown-qwen3-1.7b",
|
| 25 |
+
"description": "onboarding-countdown-qwen3-1.7b \u2014 10 rows, 10/10 correct (2026-04-06)",
|
| 26 |
+
"date": ""
|
| 27 |
+
}
|
| 28 |
+
],
|
| 29 |
+
"wandb_url": "",
|
| 30 |
+
"notes": "# Welcome to RACA\n\nThis is a sample experiment to show you how the dashboard works. You're looking at the **Overview** tab right now \u2014 it displays the experiment's README (this file).\n\nEverything you see here is generated from plain files in `notes/experiments/onboarding/`. You can browse them in your editor anytime.\n\n## How This Dashboard Works\n\nEach experiment has several tabs at the top. Here's what they do:\n\n### Overview (you are here)\n\nDisplays the experiment's README and any notes you've written in the `user/` folder. This is the main landing page for each experiment \u2014 a summary of what the experiment is, what you're investigating, and what you found.\n\n### Red Team Brief\n\nBefore any experiment runs, RACA reviews the design for potential problems \u2014 wrong evaluation metrics, truncated outputs, missing baselines, wasted compute. The brief lives at `red_team_brief.md`. This tab will be empty until you run your first real experiment.\n\n### Timeline\n\nA chronological log of everything that happened: when jobs were submitted, when artifacts were uploaded, when bugs were found and fixed. This is auto-generated from `activity_log.jsonl` \u2014 RACA writes to it as events happen.\n\n### Runs\n\nTracks each job submission \u2014 which model, which cluster, what status (pending, running, completed, failed), and links to the HuggingFace dataset with the results. Empty until you run something.\n\n### Artifacts\n\nLinks to all HuggingFace datasets produced by this experiment \u2014 canary runs, partial results, final data. Each artifact has metadata about what generated it. Empty until artifacts are uploaded.\n\n### Files\n\nAll the markdown and YAML files in the experiment folder. Click any file to read it. This is a quick way to browse the experiment's configuration and notes without leaving the dashboard.\n\n## Folder Structure\n\n```\nnotes/experiments/onboarding/\n EXPERIMENT_README.md \u2190 this file (shows in Overview tab)\n experiment.yaml \u2190 config: hypothesis, models, tasks\n flow_state.json \u2190 current phase (design/running/complete)\n HUGGINGFACE_REPOS.md \u2190 links to all uploaded datasets\n questions.md \u2190 research questions (read-only)\n red_team_brief.md \u2190 created during preflight review\n activity_log.jsonl \u2190 timeline entries (auto-generated)\n user/ \u2190 YOUR notes \u2014 RACA doesn't touch these\n README.md \u2190 your interpretation and observations\n FINDINGS.md \u2190 key results and surprises\n DECISIONS.md \u2190 design decisions and rationale\n summary.md \u2190 one-paragraph summary when done\n```\n\n**Most of this is automated.** RACA creates and updates the experiment files, uploads artifacts, and keeps the timeline current. The only files you write are in `user/` \u2014 that's your space for notes, findings, and decisions.\n\n## What's Next\n\nThis sample experiment hasn't been run yet \u2014 it's just here to show you the structure. When you're ready to run a real experiment, just tell RACA:\n\n> *I want to test whether Qwen3-8B follows complex instructions better than Llama-3.1-8B*\n\nOr try the full guided tutorial:\n\n> */raca:experiment-tutorial*\n",
|
| 31 |
+
"zayne_summary": "# Summary\n\n_Write a one-paragraph summary of the experiment and its outcome when you're done._\n\n## Status: active\n\n## Next Steps\n\n_What to do next based on findings._",
|
| 32 |
+
"zayne_readme": "# Onboarding Experiment \u2014 Your Notes\n\n## What I'm investigating\n\nThis is the tutorial experiment \u2014 testing Qwen3-1.7B on Countdown to learn the RACA pipeline.\n\n## Key observations\n\n_Fill this in as you review the results._\n\n## Open questions\n\n_Anything you want to follow up on._",
|
| 33 |
+
"zayne_findings": "# Welcome to Your Dashboard\n\nThis is a sample experiment to show you how the dashboard works. Everything you see here is generated from plain files in `notes/experiments/onboarding/`.\n\n## Dashboard Tabs\n\nEach experiment has tabs at the top:\n\n- **Overview** \u2014 the experiment's README and your notes (you're reading this now)\n- **Red Team Brief** \u2014 RACA reviews experiment designs for problems before running. Empty until your first real experiment.\n- **Timeline** \u2014 chronological log of everything that happened (auto-generated from `activity_log.jsonl`)\n- **Runs** \u2014 tracks each job submission: model, cluster, status, HuggingFace dataset links\n- **Artifacts** \u2014 links to all HuggingFace datasets produced by this experiment\n- **Files** \u2014 browse all experiment files without leaving the dashboard\n\n## What's Automated vs What You Write\n\nMost of this is automated. RACA creates and updates experiment files, uploads artifacts, and keeps the timeline current.\n\nThe `user/` folder is yours \u2014 RACA doesn't touch it:\n- `user/FINDINGS.md` \u2014 key results and surprises (this file)\n- `user/README.md` \u2014 your interpretation and observations\n- `user/DECISIONS.md` \u2014 design decisions and rationale\n- `user/summary.md` \u2014 one-paragraph summary when done\n\n## What's Next\n\nThis sample experiment hasn't been run yet \u2014 it's here to show you the structure. When you're ready:\n\n> *I want to test whether Qwen3-8B follows complex instructions better than Llama-3.1-8B*\n\nOr try the full guided tutorial: `/raca:experiment-tutorial`",
|
| 34 |
+
"zayne_decisions": "# Decisions\n\n| Date | Decision | Rationale |\n|------|----------|-----------|",
|
| 35 |
+
"red_team_brief": "# Red Team Brief \u2014 onboarding\n\n**Experiment:** Qwen3-1.7B on Countdown (tutorial/canary)\n**Reviewer:** agent\n**Date:** 2026-04-06\n**Status:** PASS\n\n---\n\n## Hypothesis\n\n> Qwen3-1.7B can solve basic Countdown arithmetic problems (>50% accuracy on 3-4 operand problems)\n\nThis is an exploratory baseline \u2014 a reasonable first question. No prior published result exists for Qwen3-1.7B specifically on this exact config, so the hypothesis is testable and non-trivial.\n\n---\n\n## Config Review\n\n### Model\n- **Qwen/Qwen3-1.7B** \u2014 small instruct model, appropriate for a tutorial canary\n- Known to handle arithmetic reasoning; 1.7B is at the edge of competence for Countdown (expect 20-50% accuracy)\n\n### max_tokens: 4096\n- **Status: PASS (marginal)**\n- Reference minimum is 2048-4096. We are at the ceiling of \"minimum\". For Qwen3-1.7B reasoning traces, 4096 should be sufficient for 3-4 operand problems.\n- **Watch for truncation in outputs** \u2014 if `finish_reason == \"length\"` appears in any response, flag immediately.\n\n### Prompt Format\n- **Must use the CoT + in-context examples prompt from the reference** (not the TinyZero `<think>` variant, since we are not RL-training)\n- Template:\n ```\n Answer the following problem. Explain your reasoning step by step. When you are finished, give your answer in this format: <answer>(your answer)</answer>.\n \n # Problem\n Using the numbers in the list [{numbers}], create an equation that equals {target}. ...\n ```\n- Answer extraction: regex on `<answer>...</answer>` tags\n\n### Evaluation Method\n- **Must use `CountdownJudge.validate_countdown_solution()` \u2014 NOT string matching**\n- Located at: `packages/custom_evaluations/custom_evaluations/sources/countdown/countdown_judge.py`\n- Handles: AST-based evaluation, Unicode operator normalization, multiset number validation\n- **Failure mode if string match is used:** \"3 + 5\" and \"5 + 3\" would score differently. This is wrong.\n\n### Dataset Generation\n- 10 samples (canary size \u2014 fine)\n- Use forward generation (inline, no dependencies): 3-4 operands, targets 1-99\n- Do NOT require all numbers used \u2014 subset is sufficient (this is the easier, more standard setting)\n\n---\n\n## Failure Modes & Mitigations\n\n| Risk | Severity | Mitigation |\n|------|----------|------------|\n| Truncated outputs (`finish_reason=length`) | HIGH | Check every row \u2014 flag if any truncation occurs |\n| String matching instead of equation eval | HIGH | Use `CountdownJudge` \u2014 verified AST-based |\n| Model outputs `\u00d7` instead of `*` | MEDIUM | `CountdownJudge` handles Unicode normalization |\n| `<answer>` tag missing in output | MEDIUM | Log separately as \"format failures\" vs \"wrong answer\" |\n| Duplicate problems in 10 samples | LOW | Use `random.seed(42)` for reproducibility |\n\n---\n\n## Expected Results\n\nBased on typical results from the reference file:\n- Qwen3-1.7B is smaller than Qwen2.5-3B (which achieves 70-80% after GRPO training)\n- For a **pre-GRPO instruct model** at this size, expect roughly **20-50% accuracy**\n- If accuracy is <10%, suspect prompt format issue or evaluation bug \u2014 investigate before scaling\n\n---\n\n## Output Schema\n\nExpected columns in the HuggingFace dataset:\n\n| Column | Type | Description |\n|--------|------|-------------|\n| `prompt` | str | Full prompt sent to the model |\n| `model_response` | str | Full untruncated model output |\n| `model` | str | `Qwen/Qwen3-1.7B` |\n| `numbers` | list[int] | Available operands |\n| `target` | int | Target value |\n| `correct` | bool | Whether the answer evaluates correctly |\n| `extracted_answer` | str | Parsed content from `<answer>` tags (null if missing) |\n| `finish_reason` | str | `stop` or `length` \u2014 flag any `length` rows |\n\n---\n\n## Validation Criteria (for data-validator)\n\nA healthy artifact must satisfy ALL of the following:\n1. All rows have non-empty `model_response`\n2. Zero rows with `finish_reason == \"length\"` (no truncation)\n3. `correct` column is boolean, computed via equation evaluation (not string match)\n4. `extracted_answer` is null only when model didn't produce `<answer>` tags\n5. Row count matches n_samples (10)\n6. No degenerate outputs (empty strings, repeated tokens, only whitespace)\n",
|
| 36 |
+
"created": "",
|
| 37 |
+
"updated": ""
|
| 38 |
+
}
|
| 39 |
+
]
|
backend/data/runs.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
[]
|
backend/data/sub_experiments.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
[]
|
backend/data/summary_findings.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
[]
|
backend/requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
flask>=3.0.0
|
| 2 |
+
flask-cors>=4.0.0
|
| 3 |
+
datasets>=2.14.0
|
| 4 |
+
gunicorn>=21.0.0
|
| 5 |
+
huggingface_hub>=0.20.0
|
docs/managing_presets.md
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Managing RACA-VIS-PRESETS Programmatically
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
The agg_visualizer stores presets in the HuggingFace dataset repo `your-org/RACA-VIS-PRESETS`. Each visualizer type has its own JSON file:
|
| 6 |
+
|
| 7 |
+
| Type | File | Extra Fields |
|
| 8 |
+
|------|------|-------------|
|
| 9 |
+
| `model` | `model_presets.json` | `column` (default: `"model_responses"`) |
|
| 10 |
+
| `arena` | `arena_presets.json` | none |
|
| 11 |
+
| `rlm` | `rlm_presets.json` | `config` (default: `"rlm_call_traces"`) |
|
| 12 |
+
| `harbor` | `harbor_presets.json` | none |
|
| 13 |
+
|
| 14 |
+
## Preset Schema
|
| 15 |
+
|
| 16 |
+
Every preset has these base fields:
|
| 17 |
+
|
| 18 |
+
```json
|
| 19 |
+
{
|
| 20 |
+
"id": "8-char hex",
|
| 21 |
+
"name": "Human-readable name",
|
| 22 |
+
"repo": "org/dataset-name",
|
| 23 |
+
"split": "train"
|
| 24 |
+
}
|
| 25 |
+
```
|
| 26 |
+
|
| 27 |
+
Plus type-specific fields listed above.
|
| 28 |
+
|
| 29 |
+
## How to Add Presets from Experiment Markdown Files
|
| 30 |
+
|
| 31 |
+
### Step 1: Identify repos and their visualizer type
|
| 32 |
+
|
| 33 |
+
Read the experiment markdown file(s) and extract all HuggingFace repo links. Categorize each:
|
| 34 |
+
|
| 35 |
+
- **Countdown / MuSR datasets** (model response traces) → `model` type, set `column: "response"`
|
| 36 |
+
- **FrozenLake / arena datasets** (game episodes) → `arena` type
|
| 37 |
+
- **Harbor / SWE-bench datasets** → `harbor` type
|
| 38 |
+
- **RLM call traces** → `rlm` type, set `config: "rlm_call_traces"`
|
| 39 |
+
|
| 40 |
+
### Step 2: Download existing presets from HF
|
| 41 |
+
|
| 42 |
+
```python
|
| 43 |
+
from huggingface_hub import hf_hub_download
|
| 44 |
+
import json
|
| 45 |
+
|
| 46 |
+
PRESETS_REPO = "your-org/RACA-VIS-PRESETS"
|
| 47 |
+
|
| 48 |
+
def load_hf_presets(vis_type):
|
| 49 |
+
try:
|
| 50 |
+
path = hf_hub_download(PRESETS_REPO, f"{vis_type}_presets.json", repo_type="dataset")
|
| 51 |
+
with open(path) as f:
|
| 52 |
+
return json.load(f)
|
| 53 |
+
except Exception:
|
| 54 |
+
return []
|
| 55 |
+
|
| 56 |
+
existing_model = load_hf_presets("model")
|
| 57 |
+
existing_arena = load_hf_presets("arena")
|
| 58 |
+
# ... etc for rlm, harbor
|
| 59 |
+
|
| 60 |
+
# Build set of repos already present
|
| 61 |
+
existing_repos = set()
|
| 62 |
+
for presets_list in [existing_model, existing_arena]:
|
| 63 |
+
for p in presets_list:
|
| 64 |
+
existing_repos.add(p["repo"])
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
### Step 3: Build new presets, skipping duplicates
|
| 68 |
+
|
| 69 |
+
```python
|
| 70 |
+
import uuid
|
| 71 |
+
|
| 72 |
+
new_presets = [] # list of (vis_type, name, repo)
|
| 73 |
+
|
| 74 |
+
# Example: adding strategy compliance countdown presets
|
| 75 |
+
new_presets.append(("model", "SC Countdown K2-Inst TreeSearch",
|
| 76 |
+
"your-org/t1-strategy-countdown-treesearch-kimi-k2-instruct-kimi-inst"))
|
| 77 |
+
|
| 78 |
+
# ... add all repos from the markdown ...
|
| 79 |
+
|
| 80 |
+
# Filter out existing
|
| 81 |
+
to_add = {"model": [], "arena": [], "rlm": [], "harbor": []}
|
| 82 |
+
for vis_type, name, repo in new_presets:
|
| 83 |
+
if repo in existing_repos:
|
| 84 |
+
continue # skip duplicates
|
| 85 |
+
preset = {
|
| 86 |
+
"id": uuid.uuid4().hex[:8],
|
| 87 |
+
"name": name,
|
| 88 |
+
"repo": repo,
|
| 89 |
+
"split": "train",
|
| 90 |
+
}
|
| 91 |
+
if vis_type == "model":
|
| 92 |
+
preset["column"] = "response"
|
| 93 |
+
elif vis_type == "rlm":
|
| 94 |
+
preset["config"] = "rlm_call_traces"
|
| 95 |
+
to_add[vis_type].append(preset)
|
| 96 |
+
```
|
| 97 |
+
|
| 98 |
+
### Step 4: Merge and upload to HF
|
| 99 |
+
|
| 100 |
+
```python
|
| 101 |
+
import tempfile, os
|
| 102 |
+
from huggingface_hub import HfApi
|
| 103 |
+
|
| 104 |
+
api = HfApi()
|
| 105 |
+
|
| 106 |
+
# Merge new presets with existing
|
| 107 |
+
final_model = existing_model + to_add["model"]
|
| 108 |
+
final_arena = existing_arena + to_add["arena"]
|
| 109 |
+
|
| 110 |
+
for vis_type, presets in [("model", final_model), ("arena", final_arena)]:
|
| 111 |
+
if not presets:
|
| 112 |
+
continue
|
| 113 |
+
with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False) as f:
|
| 114 |
+
json.dump(presets, f, indent=2)
|
| 115 |
+
tmp = f.name
|
| 116 |
+
api.upload_file(
|
| 117 |
+
path_or_fileobj=tmp,
|
| 118 |
+
path_in_repo=f"{vis_type}_presets.json",
|
| 119 |
+
repo_id=PRESETS_REPO,
|
| 120 |
+
repo_type="dataset",
|
| 121 |
+
)
|
| 122 |
+
os.unlink(tmp)
|
| 123 |
+
```
|
| 124 |
+
|
| 125 |
+
### Step 5: Sync the deployed HF Space
|
| 126 |
+
|
| 127 |
+
After uploading to the HF dataset, tell the running Space to re-download presets:
|
| 128 |
+
|
| 129 |
+
```bash
|
| 130 |
+
curl -X POST "https://your-org-agg-trace-visualizer.hf.space/api/presets/sync"
|
| 131 |
+
```
|
| 132 |
+
|
| 133 |
+
This forces the Space to re-download all preset files from `RACA-VIS-PRESETS` without needing a restart or redeployment.
|
| 134 |
+
|
| 135 |
+
### Step 6: Sync local preset files
|
| 136 |
+
|
| 137 |
+
```python
|
| 138 |
+
import shutil
|
| 139 |
+
from huggingface_hub import hf_hub_download
|
| 140 |
+
|
| 141 |
+
local_dir = Path(__file__).parent.parent / "backend" / "presets"
|
| 142 |
+
for vis_type in ["model", "arena", "rlm", "harbor"]:
|
| 143 |
+
try:
|
| 144 |
+
path = hf_hub_download(PRESETS_REPO, f"{vis_type}_presets.json", repo_type="dataset")
|
| 145 |
+
shutil.copy2(path, f"{local_dir}/{vis_type}_presets.json")
|
| 146 |
+
except Exception:
|
| 147 |
+
pass
|
| 148 |
+
```
|
| 149 |
+
|
| 150 |
+
## Naming Convention
|
| 151 |
+
|
| 152 |
+
Preset names follow this pattern to be descriptive and avoid future conflicts:
|
| 153 |
+
|
| 154 |
+
```
|
| 155 |
+
{Experiment} {Task} {Model} {Variant}
|
| 156 |
+
```
|
| 157 |
+
|
| 158 |
+
### Experiment prefixes
|
| 159 |
+
- `SC` — Strategy Compliance
|
| 160 |
+
- `Wing` — Wingdings Compliance
|
| 161 |
+
|
| 162 |
+
### Model abbreviations
|
| 163 |
+
- `K2-Inst` — Kimi-K2-Instruct (RLHF)
|
| 164 |
+
- `K2-Think` — Kimi-K2-Thinking (RLVR)
|
| 165 |
+
- `Q3-Inst` — Qwen3-Next-80B Instruct (RLHF)
|
| 166 |
+
- `Q3-Think` — Qwen3-Next-80B Thinking (RLVR)
|
| 167 |
+
|
| 168 |
+
### Task names
|
| 169 |
+
- `Countdown` — 8-arg arithmetic countdown
|
| 170 |
+
- `MuSR` — MuSR murder mysteries
|
| 171 |
+
- `FrozenLake` — FrozenLake grid navigation
|
| 172 |
+
|
| 173 |
+
### Variant names (strategy compliance only)
|
| 174 |
+
- `TreeSearch` / `Baseline` / `Anti` — countdown tree search experiment
|
| 175 |
+
- `CritFirst` / `Anti-CritFirst` — criterion-first cross-cutting analysis
|
| 176 |
+
- `Counterfactual` / `Anti-Counterfactual` — counterfactual hypothesis testing
|
| 177 |
+
- `BackChain` — backward chaining (FrozenLake)
|
| 178 |
+
|
| 179 |
+
### Examples
|
| 180 |
+
|
| 181 |
+
```
|
| 182 |
+
SC Countdown K2-Inst TreeSearch # Strategy compliance, countdown, Kimi instruct, tree search variant
|
| 183 |
+
SC MuSR Q3-Think Counterfactual # Strategy compliance, MuSR, Qwen thinking, counterfactual variant
|
| 184 |
+
SC FrozenLake K2-Think BackChain # Strategy compliance, FrozenLake, Kimi thinking, backward chaining
|
| 185 |
+
Wing Countdown Q3-Inst # Wingdings, countdown, Qwen instruct (no variant — wingdings has one condition)
|
| 186 |
+
Wing MuSR K2-Think # Wingdings, MuSR, Kimi thinking
|
| 187 |
+
```
|
| 188 |
+
|
| 189 |
+
## Important Notes
|
| 190 |
+
|
| 191 |
+
- **Always check for existing repos** before adding. The script above uses `existing_repos` set to skip duplicates.
|
| 192 |
+
- **The `column` field matters for model presets.** Strategy compliance and wingdings datasets use `"response"` as the response column, not the default `"model_responses"`.
|
| 193 |
+
- **Local files are fallback cache.** The agg_visualizer downloads from HF on startup and caches locally. After uploading to HF, sync the local files so the running app picks up changes without restart (or hit the `/api/presets/sync` endpoint).
|
| 194 |
+
- **Don't modify rlm or harbor presets** unless adding datasets of those types. The script above only touches model and arena.
|
docs/plans/2026-03-07-research-dashboard-design.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Research Dashboard Design
|
| 2 |
+
|
| 3 |
+
**Date:** 2026-03-07
|
| 4 |
+
**Status:** Approved
|
| 5 |
+
|
| 6 |
+
## Overview
|
| 7 |
+
|
| 8 |
+
Extend the existing agg_visualizer into a parent "Research Dashboard" website with a top-level navigation bar. The current visualizer becomes one page; a new Experiments page provides a control pane for tracking experiments, hypotheses, runs, and artifacts.
|
| 9 |
+
|
| 10 |
+
Deployed on HuggingFace Spaces (same Space as the current visualizer).
|
| 11 |
+
|
| 12 |
+
## Audience
|
| 13 |
+
|
| 14 |
+
Primarily the researcher + advisor. May expand to a small team later.
|
| 15 |
+
|
| 16 |
+
## Architecture
|
| 17 |
+
|
| 18 |
+
### Navigation
|
| 19 |
+
- Top-level nav bar: `Experiments | Visualizer` (future: Research Map, Knowledge Base)
|
| 20 |
+
- State-driven view switching (useState), not URL routing (HF Spaces doesn't support deep-linking)
|
| 21 |
+
- Current visualizer tabs (Model Trace, Arena, RLM, etc.) nest inside the Visualizer page unchanged
|
| 22 |
+
|
| 23 |
+
### Data Storage
|
| 24 |
+
- JSON files in HF dataset repo `your-org/RACA_DASHBOARD`
|
| 25 |
+
- Three files: `experiments.json`, `runs.json`, `sub_experiments.json`
|
| 26 |
+
- In-memory cache with async HF upload (same pattern as presets.py)
|
| 27 |
+
- Local JSON fallback in `backend/data/`
|
| 28 |
+
|
| 29 |
+
### Backend API
|
| 30 |
+
Blueprint at `/api/experiments/`:
|
| 31 |
+
|
| 32 |
+
| Method | Path | Purpose |
|
| 33 |
+
|--------|------|---------|
|
| 34 |
+
| GET | `/` | List all experiments |
|
| 35 |
+
| POST | `/` | Create experiment |
|
| 36 |
+
| GET | `/:id` | Full detail (includes runs + subs) |
|
| 37 |
+
| PUT | `/:id` | Update experiment |
|
| 38 |
+
| DELETE | `/:id` | Delete experiment |
|
| 39 |
+
| POST | `/:id/runs` | Add run record |
|
| 40 |
+
| PUT | `/:id/runs/:run_id` | Update run |
|
| 41 |
+
| DELETE | `/:id/runs/:run_id` | Delete run |
|
| 42 |
+
| POST | `/:id/subs` | Add sub-experiment |
|
| 43 |
+
| PUT | `/:id/subs/:sub_id` | Update sub-experiment |
|
| 44 |
+
| DELETE | `/:id/subs/:sub_id` | Delete sub-experiment |
|
| 45 |
+
| POST | `/sync` | Force re-download from HF |
|
| 46 |
+
| POST | `/import` | Bulk import (experiment.yaml format) |
|
| 47 |
+
|
| 48 |
+
### Data Model
|
| 49 |
+
|
| 50 |
+
**Experiment:**
|
| 51 |
+
- id, name, research_project, hypothesis (statement, type, status, success_criteria)
|
| 52 |
+
- stage, completeness (0-5), models[], tasks[], tags[]
|
| 53 |
+
- hf_repos[] (repo, description, date), wandb_url, notes (markdown)
|
| 54 |
+
- created, updated timestamps
|
| 55 |
+
|
| 56 |
+
**Run Record:**
|
| 57 |
+
- id, experiment_id, condition, model, cluster, status
|
| 58 |
+
- hf_dataset, metrics (dict), timestamp, notes
|
| 59 |
+
|
| 60 |
+
**Sub-experiment:**
|
| 61 |
+
- id, experiment_id, name, hypothesis, status
|
| 62 |
+
- content_md (full markdown report), hf_repos[]
|
| 63 |
+
- created, updated timestamps
|
| 64 |
+
|
| 65 |
+
### Frontend
|
| 66 |
+
|
| 67 |
+
Three drill-down levels:
|
| 68 |
+
|
| 69 |
+
1. **Experiment List** — Cards with name, hypothesis, status badge, completeness, tags, last updated. Sort/filter controls.
|
| 70 |
+
2. **Experiment Detail** — Hypothesis header, tabbed views (Overview, Runs, Datasets, Sub-experiments). Inline editing.
|
| 71 |
+
3. **Sub-experiment View** — Breadcrumb, header, markdown-rendered body, HF repos, edit toggle.
|
| 72 |
+
|
| 73 |
+
### Integration Points
|
| 74 |
+
- exp-runner v2 pushes data via `/api/experiments/import`
|
| 75 |
+
- Flexible ingestion — API accepts data from any source
|
| 76 |
+
- No local filesystem dependency at runtime
|
| 77 |
+
|
| 78 |
+
## Future Pages (Phase 2+)
|
| 79 |
+
- **Research Map** — Graph/board view of research directions and experiment relationships
|
| 80 |
+
- **Knowledge Base** — Searchable wiki of findings, notes, HF repos
|
| 81 |
+
|
| 82 |
+
## Tech Stack
|
| 83 |
+
- Backend: Flask (existing)
|
| 84 |
+
- Frontend: React + Vite + Tailwind + Zustand (existing)
|
| 85 |
+
- Deployment: Docker on HuggingFace Spaces (existing)
|
| 86 |
+
- Storage: HF dataset repo as JSON store
|
frontend/dist/assets/ExperimentsApp-B57tv21O.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
frontend/dist/assets/ExperimentsApp-DnJR2-55.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
@font-face{font-display:block;font-family:KaTeX_AMS;font-style:normal;font-weight:400;src:url(/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2) format("woff2"),url(/assets/KaTeX_AMS-Regular-DMm9YOAa.woff) format("woff"),url(/assets/KaTeX_AMS-Regular-DRggAlZN.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Caligraphic;font-style:normal;font-weight:700;src:url(/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2) format("woff2"),url(/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff) format("woff"),url(/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Caligraphic;font-style:normal;font-weight:400;src:url(/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2) format("woff2"),url(/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff) format("woff"),url(/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Fraktur;font-style:normal;font-weight:700;src:url(/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2) format("woff2"),url(/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff) format("woff"),url(/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Fraktur;font-style:normal;font-weight:400;src:url(/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2) format("woff2"),url(/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff) format("woff"),url(/assets/KaTeX_Fraktur-Regular-CB_wures.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Main;font-style:normal;font-weight:700;src:url(/assets/KaTeX_Main-Bold-Cx986IdX.woff2) format("woff2"),url(/assets/KaTeX_Main-Bold-Jm3AIy58.woff) format("woff"),url(/assets/KaTeX_Main-Bold-waoOVXN0.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Main;font-style:italic;font-weight:700;src:url(/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2) format("woff2"),url(/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff) format("woff"),url(/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Main;font-style:italic;font-weight:400;src:url(/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2) format("woff2"),url(/assets/KaTeX_Main-Italic-BMLOBm91.woff) format("woff"),url(/assets/KaTeX_Main-Italic-3WenGoN9.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Main;font-style:normal;font-weight:400;src:url(/assets/KaTeX_Main-Regular-B22Nviop.woff2) format("woff2"),url(/assets/KaTeX_Main-Regular-Dr94JaBh.woff) format("woff"),url(/assets/KaTeX_Main-Regular-ypZvNtVU.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Math;font-style:italic;font-weight:700;src:url(/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2) format("woff2"),url(/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff) format("woff"),url(/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Math;font-style:italic;font-weight:400;src:url(/assets/KaTeX_Math-Italic-t53AETM-.woff2) format("woff2"),url(/assets/KaTeX_Math-Italic-DA0__PXp.woff) format("woff"),url(/assets/KaTeX_Math-Italic-flOr_0UB.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_SansSerif;font-style:normal;font-weight:700;src:url(/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2) format("woff2"),url(/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff) format("woff"),url(/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_SansSerif;font-style:italic;font-weight:400;src:url(/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2) format("woff2"),url(/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff) format("woff"),url(/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_SansSerif;font-style:normal;font-weight:400;src:url(/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2) format("woff2"),url(/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff) format("woff"),url(/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Script;font-style:normal;font-weight:400;src:url(/assets/KaTeX_Script-Regular-D3wIWfF6.woff2) format("woff2"),url(/assets/KaTeX_Script-Regular-D5yQViql.woff) format("woff"),url(/assets/KaTeX_Script-Regular-C5JkGWo-.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Size1;font-style:normal;font-weight:400;src:url(/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2) format("woff2"),url(/assets/KaTeX_Size1-Regular-C195tn64.woff) format("woff"),url(/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Size2;font-style:normal;font-weight:400;src:url(/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2) format("woff2"),url(/assets/KaTeX_Size2-Regular-oD1tc_U0.woff) format("woff"),url(/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Size3;font-style:normal;font-weight:400;src:url(data:font/woff2;base64,d09GMgABAAAAAA4oAA4AAAAAHbQAAA3TAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAABmAAgRQIDgmcDBEICo1oijYBNgIkA14LMgAEIAWJAAeBHAyBHBvbGiMRdnO0IkRRkiYDgr9KsJ1NUAf2kILNxgUmgqIgq1P89vcbIcmsQbRps3vCcXdYOKSWEPEKgZgQkprQQsxIXUgq0DqpGKmIvrgkeVGtEQD9DzAO29fM9jYhxZEsL2FeURH2JN4MIcTdO049NCVdxQ/w9NrSYFEBKTDKpLKfNkCGDc1RwjZLQcm3vqJ2UW9Xfa3tgAHz6ivp6vgC2yD4/6352ndnN0X0TL7seypkjZlMsjmZnf0Mm5Q+JykRWQBKCVCVPbARPXWyQtb5VgLB6Biq7/Uixcj2WGqdI8tGSgkuRG+t910GKP2D7AQH0DB9FMDW/obJZ8giFI3Wg8Cvevz0M+5m0rTh7XDBlvo9Y4vm13EXmfttwI4mBo1EG15fxJhUiCLbiiyCf/ZA6MFAhg3pGIZGdGIVjtPn6UcMk9A/UUr9PhoNsCENw1APAq0gpH73e+M+0ueyHbabc3vkbcdtzcf/fiy+NxQEjf9ud/ELBHAXJ0nk4z+MXH2Ev/kWyV4k7SkvpPc9Qr38F6RPWnM9cN6DJ0AdD1BhtgABtmoRoFCvPsBAumNm6soZG2Gk5GyVTo2sJncSyp0jQTYoR6WDvTwaaEcHsxHfvuWhHA3a6bN7twRKtcGok6NsCi7jYRrM2jExsUFMxMQYuJbMhuWNOumEJy9hi29Dmg5zMp/A5+hhPG19j1vBrq8JTLr8ki5VLPmG/PynJHVul440bxg5xuymHUFPBshC+nA9I1FmwbRBTNHAcik3Oae0cxKoI3MOriM42UrPe51nsaGxJ+WfXubAsP84aabUlQSJ1IiE0iPETLUU4CATgfXSCSpuRFRmCGbO+wSpAnzaeaCYW1VNEysRtuXCEL1kUFUbbtMv3Tilt/1c11jt3Q5bbMa84cpWipp8Elw3MZhOHsOlwwVUQM3lAR35JiFQbaYCRnMF2lxAWoOg2gyoIV4PouX8HytNIfLhqpJtXB4vjiViUI8IJ7bkC4ikkQvKksnOTKICwnqWSZ9YS5f0WCxmpgjbIq7EJcM4aI2nmhLNY2JIUgOjXZFWBHb+x5oh6cwb0Tv1ackHdKi0I9OO2wE9aogIOn540CCCziyhN+IaejtgAONKznHlHyutPrHGwCx9S6B8kfS4Mfi4Eyv7OU730bT1SCBjt834cXsf43zVjPUqqJjgrjeGnBxSG4aYAKFuVbeCfkDIjAqMb6yLNIbCuvXhMH2/+k2vkNpkORhR59N1CkzoOENvneIosjYmuTxlhUzaGEJQ/iWqx4dmwpmKjrwTiTGTCVozNAYqk/zXOndWxuWSmJkQpJw3pK5KX6QrLt5LATMqpmPAQhkhK6PUjzHUn7E0gHE0kPE0iKkolgkUx9SZmVAdDgpffdyJKg3k7VmzYGCwVXGz/tXmkOIp+vcWs+EMuhhvN0h9uhfzWJziBQmCREGSIFmQIkgVpAnSBRmC//6hkLZwaVhwxlrJSOdqlFtOYxlau9F2QN5Y98xmIAsiM1HVp2VFX+DHHGg6Ecjh3vmqtidX3qHI2qycTk/iwxSt5UzTmEP92ZBnEWTk4Mx8Mpl78ZDokxg/KWb+Q0QkvdKVmq3TMW+RXEgrsziSAfNXFMhDc60N5N9jQzjfO0kBKpUZl0ZmwJ41j/B9Hz6wmRaJB84niNmQrzp9eSlQCDDzazGDdVi3P36VZQ+Jy4f9UBNp+3zTjqI4abaFAm+GShVaXlsGdF3FYzZcDI6cori4kMxUECl9IjJZpzkvitAoxKue+90pDMvcKRxLl53TmOKCmV/xRolNKSqqUxc6LStOETmFOiLZZptlZepcKiAzteG8PEdpnQpbOMNcMsR4RR2Bs0cKFEvSmIjAFcnarqwUL4lDhHmnVkwu1IwshbiCcgvOheZuYyOteufZZwlcTlLgnZ3o/WcYdzZHW/WGaqaVfmTZ1aWCceJjkbZqsfbkOtcFlUZM/jy+hXHDbaUobWqqXaeWobbLO99yG5N3U4wxco0rQGGcOLASFMXeJoham8M+/x6O2WywK2l4HGbq1CoUyC/IZikQhdq3SiuNrvAEj0AVu9x2x3lp/xWzahaxidezFVtdcb5uEnzyl0ZmYiuKI0exvCd4Xc9CV1KB0db00z92wDPde0kukbvZIWN6jUWFTmPIC/Y4UPCm8UfDTFZpZNon1qLFTkBhxzB+FjQRA2Q/YRJT8pQigslMaUpFyAG8TMlXigiqmAZX4xgijKjRlGpLE0GdplRfCaJo0JQaSxNBk6ZmMzcya0FmrcisDdn0Q3HI2sWSppYigmlM1XT/kLQZSNpMJG0WkjYbSZuDpM1F0uYhFc1HxU4m1QJjDK6iL0S5uSj5rgXc3RejEigtcRBtqYPQsiTskmO5vosV+q4VGIKbOkDg0jtRrq+Em1YloaTFar3EGr1EUC8R0kus1Uus00usL97ABr2BjXoDm/QGNhuWtMVBKOwg/i78lT7hBsAvDmwHc/ao3vmUbBmhjeYySZNWvGkfZAgISDSaDo1SVpzGDsAEkF8B+gEapViUoZgUWXcRIGFZNm6gWbAKk0bp0k1MHG9fLYtV4iS2SmLEQFARzRcnf9PUS0LVn05/J9MiRRBU3v2IrvW974v4N00L7ZMk0wXP1409CHo/an8zTRHD3eSJ6m8D4YMkZNl3M79sqeuAsr/m3f+8/yl7A50aiAEJgeBeMWzu7ui9UfUBCe2TIqZIoOd/3/udRBOQidQZUERzb2/VwZN1H/Sju82ew2H2Wfr6qvfVf3hqwDvAIpkQVFy4B9Pe9e4/XvPeceu7h3dvO56iJPf0+A6cqA2ip18ER+iFgggiuOkvj24bby0N9j2UHIkgqIt+sVgfodC4YghLSMjSZbH0VR/6dMDrYJeKHilKTemt6v6kvzvn3/RrdWtr0GoN/xL+Sex/cPYLUpepx9cz/D46UPU5KXgAQa+NDps1v6J3xP1i2HtaDB0M9aX2deA7SYff//+gUCovMmIK/qfsFcOk+4Y5ZN97XlG6zebqtMbKgeRFi51vnxTQYBUik2rS/Cn6PC8ADR8FGxsRPB82dzfND90gIcshOcYUkfjherBz53odpm6TP8txlwOZ71xmfHHOvq053qFF/MRlS3jP0ELudrf2OeN8DHvp6ZceLe8qKYvWz/7yp0u4dKPfli3CYq0O13Ih71mylJ80tOi10On8wi+F4+LWgDPeJ30msSQt9/vkmHq9/Lvo2b461mP801v3W4xTcs6CbvF9UDdrSt+A8OUbpSh55qAUFXWznBBfdeJ8a4d7ugT5tvxUza3h9m4H7ptTqiG4z0g5dc0X29OcGlhpGFMpQo9ytTS+NViZpNdvU4kWx+LKxNY10kQ1yqGXrhe4/1nvP7E+nd5A92TtaRplbHSqoIdOqtRWti+fkB5/n1+/VvCmz12pG1kpQWsfi1ftlBobm0bpngs16CHkbIwdLnParxtTV3QYRlfJ0KFskH7pdN/YDn+yRuSd7sNH3aO0DYPggk6uWuXrfOc+fa3VTxFVvKaNxHsiHmsXyCLIE5yuOeN3/Jdf8HBL/5M6shjyhxHx9BjB1O0+4NLOnjLLSxwO7ukN4jMbOIcD879KLSi6Pk61Oqm2377n8079PXEEQ7cy7OKEC9nbpet118fxweTafpt69x/Bt8UqGzNQt7aelpc44dn5cqhwf71+qKp/Zf/+a0zcizOUWpl/iBcSXip0pplkatCchoH5c5aUM8I7/dWxAej8WicPL1URFZ9BDJelUwEwTkGqUhgSlydVes95YdXvhh9Gfz/aeFWvgVb4tuLbcv4+wLdutVZv/cUonwBD/6eDlE0aSiKK/uoH3+J1wDE/jMVqY2ysGufN84oIXB0sPzy8ollX/LegY74DgJXJR57sn+VGza0x3DnuIgABFM15LmajjjsNlYj+JEZGbuRYcAMOWxFkPN2w6Wd46xo4gVWQR/X4lyI/R6K/YK0110GzudPRW7Y+UOBGTfNNzHeYT0fiH0taunBpq9HEW8OKSaBGj21L0MqenEmNRWBAWDWAk4CpNoEZJ2tTaPFgbQYj8HxtFilErs3BTRwT8uO1NXQaWfIotchmPkAF5mMBAliEmZiOGVgCG9LgRzpscMAOOwowlT3JhusdazXGSC/hxR3UlmWVwWHpOIKheqONvjyhSiTHIkVUco5bnji8m//zL7PKaT1Vl5I6UE609f+gkr6MZKVyKc7zJRmCahLsdlyA5fdQkRSan9LgnnLEyGSkaKJCJog0wAgvepWBt80+1yKln1bMVtCljfNWDueKLsWwaEbBSfSPTEmVRsUcYYMnEjcjeyCZzBXK9E9BYBXLKjOSpUDR+nEV3TFSUdQaz+ot98QxgXwx0GQ+EEUAKB2qZPkQQ0GqFD8UPFMqyaCHM24BZmSGic9EYMagKizOw9Hz50DMrDLrqqLkTAhplMictiCAx5S3BIUQdeJeLnBy2CNtMfz6cV4u8XKoFZQesbf9YZiIERiHjaNodDW6LgcirX/mPnJIkBGDUpTBhSa0EIr38D5hCIszhCM8URGBqImoWjpvpt1ebu/v3Gl3qJfMnNM+9V+kiRFyROTPHQWOcs1dNW94/ukKMPZBvDi55i5CttdeJz84DLngLqjcdwEZ87bFFR8CIG35OAkDVN6VRDZ7aq67NteYqZ2lpT8oYB2CytoBd6VuAx4WgiAsnuj3WohG+LugzXiQRDeM3XYXlULv4dp5VFYC) format("woff2"),url(/assets/KaTeX_Size3-Regular-CTq5MqoE.woff) format("woff"),url(/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Size4;font-style:normal;font-weight:400;src:url(/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2) format("woff2"),url(/assets/KaTeX_Size4-Regular-BF-4gkZK.woff) format("woff"),url(/assets/KaTeX_Size4-Regular-DWFBv043.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Typewriter;font-style:normal;font-weight:400;src:url(/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2) format("woff2"),url(/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff) format("woff"),url(/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf) format("truetype")}.katex{font: 1.21em KaTeX_Main,Times New Roman,serif;line-height:1.2;position:relative;text-indent:0;text-rendering:auto}.katex *{-ms-high-contrast-adjust:none!important;border-color:currentColor}.katex .katex-version:after{content:"0.16.40"}.katex .katex-mathml{clip:rect(1px,1px,1px,1px);border:0;height:1px;overflow:hidden;padding:0;position:absolute;width:1px}.katex .katex-html>.newline{display:block}.katex .base{position:relative;white-space:nowrap;width:-moz-min-content;width:min-content}.katex .base,.katex .strut{display:inline-block}.katex .textbf{font-weight:700}.katex .textit{font-style:italic}.katex .textrm{font-family:KaTeX_Main}.katex .textsf{font-family:KaTeX_SansSerif}.katex .texttt{font-family:KaTeX_Typewriter}.katex .mathnormal{font-family:KaTeX_Math;font-style:italic}.katex .mathit{font-family:KaTeX_Main;font-style:italic}.katex .mathrm{font-style:normal}.katex .mathbf{font-family:KaTeX_Main;font-weight:700}.katex .boldsymbol{font-family:KaTeX_Math;font-style:italic;font-weight:700}.katex .amsrm,.katex .mathbb,.katex .textbb{font-family:KaTeX_AMS}.katex .mathcal{font-family:KaTeX_Caligraphic}.katex .mathfrak,.katex .textfrak{font-family:KaTeX_Fraktur}.katex .mathboldfrak,.katex .textboldfrak{font-family:KaTeX_Fraktur;font-weight:700}.katex .mathtt{font-family:KaTeX_Typewriter}.katex .mathscr,.katex .textscr{font-family:KaTeX_Script}.katex .mathsf,.katex .textsf{font-family:KaTeX_SansSerif}.katex .mathboldsf,.katex .textboldsf{font-family:KaTeX_SansSerif;font-weight:700}.katex .mathitsf,.katex .mathsfit,.katex .textitsf{font-family:KaTeX_SansSerif;font-style:italic}.katex .mainrm{font-family:KaTeX_Main;font-style:normal}.katex .vlist-t{border-collapse:collapse;display:inline-table;table-layout:fixed}.katex .vlist-r{display:table-row}.katex .vlist{display:table-cell;position:relative;vertical-align:bottom}.katex .vlist>span{display:block;height:0;position:relative}.katex .vlist>span>span{display:inline-block}.katex .vlist>span>.pstrut{overflow:hidden;width:0}.katex .vlist-t2{margin-right:-2px}.katex .vlist-s{display:table-cell;font-size:1px;min-width:2px;vertical-align:bottom;width:2px}.katex .vbox{align-items:baseline;display:inline-flex;flex-direction:column}.katex .hbox{width:100%}.katex .hbox,.katex .thinbox{display:inline-flex;flex-direction:row}.katex .thinbox{max-width:0;width:0}.katex .msupsub{text-align:left}.katex .mfrac>span>span{text-align:center}.katex .mfrac .frac-line{border-bottom-style:solid;display:inline-block;width:100%}.katex .hdashline,.katex .hline,.katex .mfrac .frac-line,.katex .overline .overline-line,.katex .rule,.katex .underline .underline-line{min-height:1px}.katex .mspace{display:inline-block}.katex .smash{display:inline;line-height:0}.katex .clap,.katex .llap,.katex .rlap{position:relative;width:0}.katex .clap>.inner,.katex .llap>.inner,.katex .rlap>.inner{position:absolute}.katex .clap>.fix,.katex .llap>.fix,.katex .rlap>.fix{display:inline-block}.katex .llap>.inner{right:0}.katex .clap>.inner,.katex .rlap>.inner{left:0}.katex .clap>.inner>span{margin-left:-50%;margin-right:50%}.katex .rule{border:0 solid;display:inline-block;position:relative}.katex .hline,.katex .overline .overline-line,.katex .underline .underline-line{border-bottom-style:solid;display:inline-block;width:100%}.katex .hdashline{border-bottom-style:dashed;display:inline-block;width:100%}.katex .sqrt>.root{margin-left:.2777777778em;margin-right:-.5555555556em}.katex .fontsize-ensurer.reset-size1.size1,.katex .sizing.reset-size1.size1{font-size:1em}.katex .fontsize-ensurer.reset-size1.size2,.katex .sizing.reset-size1.size2{font-size:1.2em}.katex .fontsize-ensurer.reset-size1.size3,.katex .sizing.reset-size1.size3{font-size:1.4em}.katex .fontsize-ensurer.reset-size1.size4,.katex .sizing.reset-size1.size4{font-size:1.6em}.katex .fontsize-ensurer.reset-size1.size5,.katex .sizing.reset-size1.size5{font-size:1.8em}.katex .fontsize-ensurer.reset-size1.size6,.katex .sizing.reset-size1.size6{font-size:2em}.katex .fontsize-ensurer.reset-size1.size7,.katex .sizing.reset-size1.size7{font-size:2.4em}.katex .fontsize-ensurer.reset-size1.size8,.katex .sizing.reset-size1.size8{font-size:2.88em}.katex .fontsize-ensurer.reset-size1.size9,.katex .sizing.reset-size1.size9{font-size:3.456em}.katex .fontsize-ensurer.reset-size1.size10,.katex .sizing.reset-size1.size10{font-size:4.148em}.katex .fontsize-ensurer.reset-size1.size11,.katex .sizing.reset-size1.size11{font-size:4.976em}.katex .fontsize-ensurer.reset-size2.size1,.katex .sizing.reset-size2.size1{font-size:.8333333333em}.katex .fontsize-ensurer.reset-size2.size2,.katex .sizing.reset-size2.size2{font-size:1em}.katex .fontsize-ensurer.reset-size2.size3,.katex .sizing.reset-size2.size3{font-size:1.1666666667em}.katex .fontsize-ensurer.reset-size2.size4,.katex .sizing.reset-size2.size4{font-size:1.3333333333em}.katex .fontsize-ensurer.reset-size2.size5,.katex .sizing.reset-size2.size5{font-size:1.5em}.katex .fontsize-ensurer.reset-size2.size6,.katex .sizing.reset-size2.size6{font-size:1.6666666667em}.katex .fontsize-ensurer.reset-size2.size7,.katex .sizing.reset-size2.size7{font-size:2em}.katex .fontsize-ensurer.reset-size2.size8,.katex .sizing.reset-size2.size8{font-size:2.4em}.katex .fontsize-ensurer.reset-size2.size9,.katex .sizing.reset-size2.size9{font-size:2.88em}.katex .fontsize-ensurer.reset-size2.size10,.katex .sizing.reset-size2.size10{font-size:3.4566666667em}.katex .fontsize-ensurer.reset-size2.size11,.katex .sizing.reset-size2.size11{font-size:4.1466666667em}.katex .fontsize-ensurer.reset-size3.size1,.katex .sizing.reset-size3.size1{font-size:.7142857143em}.katex .fontsize-ensurer.reset-size3.size2,.katex .sizing.reset-size3.size2{font-size:.8571428571em}.katex .fontsize-ensurer.reset-size3.size3,.katex .sizing.reset-size3.size3{font-size:1em}.katex .fontsize-ensurer.reset-size3.size4,.katex .sizing.reset-size3.size4{font-size:1.1428571429em}.katex .fontsize-ensurer.reset-size3.size5,.katex .sizing.reset-size3.size5{font-size:1.2857142857em}.katex .fontsize-ensurer.reset-size3.size6,.katex .sizing.reset-size3.size6{font-size:1.4285714286em}.katex .fontsize-ensurer.reset-size3.size7,.katex .sizing.reset-size3.size7{font-size:1.7142857143em}.katex .fontsize-ensurer.reset-size3.size8,.katex .sizing.reset-size3.size8{font-size:2.0571428571em}.katex .fontsize-ensurer.reset-size3.size9,.katex .sizing.reset-size3.size9{font-size:2.4685714286em}.katex .fontsize-ensurer.reset-size3.size10,.katex .sizing.reset-size3.size10{font-size:2.9628571429em}.katex .fontsize-ensurer.reset-size3.size11,.katex .sizing.reset-size3.size11{font-size:3.5542857143em}.katex .fontsize-ensurer.reset-size4.size1,.katex .sizing.reset-size4.size1{font-size:.625em}.katex .fontsize-ensurer.reset-size4.size2,.katex .sizing.reset-size4.size2{font-size:.75em}.katex .fontsize-ensurer.reset-size4.size3,.katex .sizing.reset-size4.size3{font-size:.875em}.katex .fontsize-ensurer.reset-size4.size4,.katex .sizing.reset-size4.size4{font-size:1em}.katex .fontsize-ensurer.reset-size4.size5,.katex .sizing.reset-size4.size5{font-size:1.125em}.katex .fontsize-ensurer.reset-size4.size6,.katex .sizing.reset-size4.size6{font-size:1.25em}.katex .fontsize-ensurer.reset-size4.size7,.katex .sizing.reset-size4.size7{font-size:1.5em}.katex .fontsize-ensurer.reset-size4.size8,.katex .sizing.reset-size4.size8{font-size:1.8em}.katex .fontsize-ensurer.reset-size4.size9,.katex .sizing.reset-size4.size9{font-size:2.16em}.katex .fontsize-ensurer.reset-size4.size10,.katex .sizing.reset-size4.size10{font-size:2.5925em}.katex .fontsize-ensurer.reset-size4.size11,.katex .sizing.reset-size4.size11{font-size:3.11em}.katex .fontsize-ensurer.reset-size5.size1,.katex .sizing.reset-size5.size1{font-size:.5555555556em}.katex .fontsize-ensurer.reset-size5.size2,.katex .sizing.reset-size5.size2{font-size:.6666666667em}.katex .fontsize-ensurer.reset-size5.size3,.katex .sizing.reset-size5.size3{font-size:.7777777778em}.katex .fontsize-ensurer.reset-size5.size4,.katex .sizing.reset-size5.size4{font-size:.8888888889em}.katex .fontsize-ensurer.reset-size5.size5,.katex .sizing.reset-size5.size5{font-size:1em}.katex .fontsize-ensurer.reset-size5.size6,.katex .sizing.reset-size5.size6{font-size:1.1111111111em}.katex .fontsize-ensurer.reset-size5.size7,.katex .sizing.reset-size5.size7{font-size:1.3333333333em}.katex .fontsize-ensurer.reset-size5.size8,.katex .sizing.reset-size5.size8{font-size:1.6em}.katex .fontsize-ensurer.reset-size5.size9,.katex .sizing.reset-size5.size9{font-size:1.92em}.katex .fontsize-ensurer.reset-size5.size10,.katex .sizing.reset-size5.size10{font-size:2.3044444444em}.katex .fontsize-ensurer.reset-size5.size11,.katex .sizing.reset-size5.size11{font-size:2.7644444444em}.katex .fontsize-ensurer.reset-size6.size1,.katex .sizing.reset-size6.size1{font-size:.5em}.katex .fontsize-ensurer.reset-size6.size2,.katex .sizing.reset-size6.size2{font-size:.6em}.katex .fontsize-ensurer.reset-size6.size3,.katex .sizing.reset-size6.size3{font-size:.7em}.katex .fontsize-ensurer.reset-size6.size4,.katex .sizing.reset-size6.size4{font-size:.8em}.katex .fontsize-ensurer.reset-size6.size5,.katex .sizing.reset-size6.size5{font-size:.9em}.katex .fontsize-ensurer.reset-size6.size6,.katex .sizing.reset-size6.size6{font-size:1em}.katex .fontsize-ensurer.reset-size6.size7,.katex .sizing.reset-size6.size7{font-size:1.2em}.katex .fontsize-ensurer.reset-size6.size8,.katex .sizing.reset-size6.size8{font-size:1.44em}.katex .fontsize-ensurer.reset-size6.size9,.katex .sizing.reset-size6.size9{font-size:1.728em}.katex .fontsize-ensurer.reset-size6.size10,.katex .sizing.reset-size6.size10{font-size:2.074em}.katex .fontsize-ensurer.reset-size6.size11,.katex .sizing.reset-size6.size11{font-size:2.488em}.katex .fontsize-ensurer.reset-size7.size1,.katex .sizing.reset-size7.size1{font-size:.4166666667em}.katex .fontsize-ensurer.reset-size7.size2,.katex .sizing.reset-size7.size2{font-size:.5em}.katex .fontsize-ensurer.reset-size7.size3,.katex .sizing.reset-size7.size3{font-size:.5833333333em}.katex .fontsize-ensurer.reset-size7.size4,.katex .sizing.reset-size7.size4{font-size:.6666666667em}.katex .fontsize-ensurer.reset-size7.size5,.katex .sizing.reset-size7.size5{font-size:.75em}.katex .fontsize-ensurer.reset-size7.size6,.katex .sizing.reset-size7.size6{font-size:.8333333333em}.katex .fontsize-ensurer.reset-size7.size7,.katex .sizing.reset-size7.size7{font-size:1em}.katex .fontsize-ensurer.reset-size7.size8,.katex .sizing.reset-size7.size8{font-size:1.2em}.katex .fontsize-ensurer.reset-size7.size9,.katex .sizing.reset-size7.size9{font-size:1.44em}.katex .fontsize-ensurer.reset-size7.size10,.katex .sizing.reset-size7.size10{font-size:1.7283333333em}.katex .fontsize-ensurer.reset-size7.size11,.katex .sizing.reset-size7.size11{font-size:2.0733333333em}.katex .fontsize-ensurer.reset-size8.size1,.katex .sizing.reset-size8.size1{font-size:.3472222222em}.katex .fontsize-ensurer.reset-size8.size2,.katex .sizing.reset-size8.size2{font-size:.4166666667em}.katex .fontsize-ensurer.reset-size8.size3,.katex .sizing.reset-size8.size3{font-size:.4861111111em}.katex .fontsize-ensurer.reset-size8.size4,.katex .sizing.reset-size8.size4{font-size:.5555555556em}.katex .fontsize-ensurer.reset-size8.size5,.katex .sizing.reset-size8.size5{font-size:.625em}.katex .fontsize-ensurer.reset-size8.size6,.katex .sizing.reset-size8.size6{font-size:.6944444444em}.katex .fontsize-ensurer.reset-size8.size7,.katex .sizing.reset-size8.size7{font-size:.8333333333em}.katex .fontsize-ensurer.reset-size8.size8,.katex .sizing.reset-size8.size8{font-size:1em}.katex .fontsize-ensurer.reset-size8.size9,.katex .sizing.reset-size8.size9{font-size:1.2em}.katex .fontsize-ensurer.reset-size8.size10,.katex .sizing.reset-size8.size10{font-size:1.4402777778em}.katex .fontsize-ensurer.reset-size8.size11,.katex .sizing.reset-size8.size11{font-size:1.7277777778em}.katex .fontsize-ensurer.reset-size9.size1,.katex .sizing.reset-size9.size1{font-size:.2893518519em}.katex .fontsize-ensurer.reset-size9.size2,.katex .sizing.reset-size9.size2{font-size:.3472222222em}.katex .fontsize-ensurer.reset-size9.size3,.katex .sizing.reset-size9.size3{font-size:.4050925926em}.katex .fontsize-ensurer.reset-size9.size4,.katex .sizing.reset-size9.size4{font-size:.462962963em}.katex .fontsize-ensurer.reset-size9.size5,.katex .sizing.reset-size9.size5{font-size:.5208333333em}.katex .fontsize-ensurer.reset-size9.size6,.katex .sizing.reset-size9.size6{font-size:.5787037037em}.katex .fontsize-ensurer.reset-size9.size7,.katex .sizing.reset-size9.size7{font-size:.6944444444em}.katex .fontsize-ensurer.reset-size9.size8,.katex .sizing.reset-size9.size8{font-size:.8333333333em}.katex .fontsize-ensurer.reset-size9.size9,.katex .sizing.reset-size9.size9{font-size:1em}.katex .fontsize-ensurer.reset-size9.size10,.katex .sizing.reset-size9.size10{font-size:1.2002314815em}.katex .fontsize-ensurer.reset-size9.size11,.katex .sizing.reset-size9.size11{font-size:1.4398148148em}.katex .fontsize-ensurer.reset-size10.size1,.katex .sizing.reset-size10.size1{font-size:.2410800386em}.katex .fontsize-ensurer.reset-size10.size2,.katex .sizing.reset-size10.size2{font-size:.2892960463em}.katex .fontsize-ensurer.reset-size10.size3,.katex .sizing.reset-size10.size3{font-size:.337512054em}.katex .fontsize-ensurer.reset-size10.size4,.katex .sizing.reset-size10.size4{font-size:.3857280617em}.katex .fontsize-ensurer.reset-size10.size5,.katex .sizing.reset-size10.size5{font-size:.4339440694em}.katex .fontsize-ensurer.reset-size10.size6,.katex .sizing.reset-size10.size6{font-size:.4821600771em}.katex .fontsize-ensurer.reset-size10.size7,.katex .sizing.reset-size10.size7{font-size:.5785920926em}.katex .fontsize-ensurer.reset-size10.size8,.katex .sizing.reset-size10.size8{font-size:.6943105111em}.katex .fontsize-ensurer.reset-size10.size9,.katex .sizing.reset-size10.size9{font-size:.8331726133em}.katex .fontsize-ensurer.reset-size10.size10,.katex .sizing.reset-size10.size10{font-size:1em}.katex .fontsize-ensurer.reset-size10.size11,.katex .sizing.reset-size10.size11{font-size:1.1996142719em}.katex .fontsize-ensurer.reset-size11.size1,.katex .sizing.reset-size11.size1{font-size:.2009646302em}.katex .fontsize-ensurer.reset-size11.size2,.katex .sizing.reset-size11.size2{font-size:.2411575563em}.katex .fontsize-ensurer.reset-size11.size3,.katex .sizing.reset-size11.size3{font-size:.2813504823em}.katex .fontsize-ensurer.reset-size11.size4,.katex .sizing.reset-size11.size4{font-size:.3215434084em}.katex .fontsize-ensurer.reset-size11.size5,.katex .sizing.reset-size11.size5{font-size:.3617363344em}.katex .fontsize-ensurer.reset-size11.size6,.katex .sizing.reset-size11.size6{font-size:.4019292605em}.katex .fontsize-ensurer.reset-size11.size7,.katex .sizing.reset-size11.size7{font-size:.4823151125em}.katex .fontsize-ensurer.reset-size11.size8,.katex .sizing.reset-size11.size8{font-size:.578778135em}.katex .fontsize-ensurer.reset-size11.size9,.katex .sizing.reset-size11.size9{font-size:.6945337621em}.katex .fontsize-ensurer.reset-size11.size10,.katex .sizing.reset-size11.size10{font-size:.8336012862em}.katex .fontsize-ensurer.reset-size11.size11,.katex .sizing.reset-size11.size11{font-size:1em}.katex .delimsizing.size1{font-family:KaTeX_Size1}.katex .delimsizing.size2{font-family:KaTeX_Size2}.katex .delimsizing.size3{font-family:KaTeX_Size3}.katex .delimsizing.size4{font-family:KaTeX_Size4}.katex .delimsizing.mult .delim-size1>span{font-family:KaTeX_Size1}.katex .delimsizing.mult .delim-size4>span{font-family:KaTeX_Size4}.katex .nulldelimiter{display:inline-block;width:.12em}.katex .delimcenter,.katex .op-symbol{position:relative}.katex .op-symbol.small-op{font-family:KaTeX_Size1}.katex .op-symbol.large-op{font-family:KaTeX_Size2}.katex .accent>.vlist-t,.katex .op-limits>.vlist-t{text-align:center}.katex .accent .accent-body{position:relative}.katex .accent .accent-body:not(.accent-full){width:0}.katex .overlay{display:block}.katex .mtable .vertical-separator{display:inline-block;min-width:1px}.katex .mtable .arraycolsep{display:inline-block}.katex .mtable .col-align-c>.vlist-t{text-align:center}.katex .mtable .col-align-l>.vlist-t{text-align:left}.katex .mtable .col-align-r>.vlist-t{text-align:right}.katex .svg-align{text-align:left}.katex svg{fill:currentColor;stroke:currentColor;display:block;height:inherit;position:absolute;width:100%}.katex svg path{stroke:none}.katex svg{fill-rule:nonzero;fill-opacity:1;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1}.katex img{border-style:none;max-height:none;max-width:none;min-height:0;min-width:0}.katex .stretchy{display:block;overflow:hidden;position:relative;width:100%}.katex .stretchy:after,.katex .stretchy:before{content:""}.katex .hide-tail{overflow:hidden;position:relative;width:100%}.katex .halfarrow-left{left:0;overflow:hidden;position:absolute;width:50.2%}.katex .halfarrow-right{overflow:hidden;position:absolute;right:0;width:50.2%}.katex .brace-left{left:0;overflow:hidden;position:absolute;width:25.1%}.katex .brace-center{left:25%;overflow:hidden;position:absolute;width:50%}.katex .brace-right{overflow:hidden;position:absolute;right:0;width:25.1%}.katex .x-arrow-pad{padding:0 .5em}.katex .cd-arrow-pad{padding:0 .55556em 0 .27778em}.katex .mover,.katex .munder,.katex .x-arrow{text-align:center}.katex .boxpad{padding:0 .3em}.katex .fbox,.katex .fcolorbox{border:.04em solid;box-sizing:border-box}.katex .cancel-pad{padding:0 .2em}.katex .cancel-lap{margin-left:-.2em;margin-right:-.2em}.katex .sout{border-bottom-style:solid;border-bottom-width:.08em}.katex .angl{border-right:.049em solid;border-top:.049em solid;box-sizing:border-box;margin-right:.03889em}.katex .anglpad{padding:0 .03889em}.katex .eqn-num:before{content:"(" counter(katexEqnNo) ")";counter-increment:katexEqnNo}.katex .mml-eqn-num:before{content:"(" counter(mmlEqnNo) ")";counter-increment:mmlEqnNo}.katex .mtr-glue{width:50%}.katex .cd-vert-arrow{display:inline-block;position:relative}.katex .cd-label-left{display:inline-block;position:absolute;right:calc(50% + .3em);text-align:left}.katex .cd-label-right{display:inline-block;left:calc(50% + .3em);position:absolute;text-align:right}.katex-display{display:block;margin:1em 0;text-align:center}.katex-display>.katex{display:block;text-align:center;white-space:nowrap}.katex-display>.katex>.katex-html{display:block;position:relative}.katex-display>.katex>.katex-html>.tag{position:absolute;right:0}.katex-display.leqno>.katex>.katex-html>.tag{left:0;right:auto}.katex-display.fleqn>.katex{padding-left:2em;text-align:left}body{counter-reset:katexEqnNo mmlEqnNo}
|
frontend/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2
ADDED
|
Binary file (28.1 kB). View file
|
|
|
frontend/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff
ADDED
|
Binary file (33.5 kB). View file
|
|
|
frontend/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf
ADDED
|
Binary file (63.6 kB). View file
|
|
|
frontend/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf
ADDED
|
Binary file (12.4 kB). View file
|
|
|
frontend/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff
ADDED
|
Binary file (7.72 kB). View file
|
|
|
frontend/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2
ADDED
|
Binary file (6.91 kB). View file
|
|
|
frontend/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff
ADDED
|
Binary file (7.66 kB). View file
|
|
|
frontend/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2
ADDED
|
Binary file (6.91 kB). View file
|
|
|
frontend/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf
ADDED
|
Binary file (12.3 kB). View file
|
|
|
frontend/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf
ADDED
|
Binary file (19.6 kB). View file
|
|
|
frontend/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff
ADDED
|
Binary file (13.3 kB). View file
|
|
|
frontend/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2
ADDED
|
Binary file (11.3 kB). View file
|
|
|
frontend/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf
ADDED
|
Binary file (19.6 kB). View file
|
|
|
frontend/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2
ADDED
|
Binary file (11.3 kB). View file
|
|
|
frontend/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff
ADDED
|
Binary file (13.2 kB). View file
|
|
|
frontend/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2
ADDED
|
Binary file (25.3 kB). View file
|
|
|
frontend/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff
ADDED
|
Binary file (29.9 kB). View file
|
|
|
frontend/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf
ADDED
|
Binary file (51.3 kB). View file
|
|
|
frontend/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2
ADDED
|
Binary file (16.8 kB). View file
|
|
|
frontend/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf
ADDED
|
Binary file (33 kB). View file
|
|
|
frontend/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff
ADDED
|
Binary file (19.4 kB). View file
|
|
|
frontend/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf
ADDED
|
Binary file (33.6 kB). View file
|
|
|
frontend/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff
ADDED
|
Binary file (19.7 kB). View file
|
|
|