DataSense E2B hackathon demo - Gradio agent, story, eval assets
Browse files- .gitattributes +9 -0
- DEPLOY.md +41 -0
- README.md +98 -5
- agent.py +254 -0
- app.py +130 -0
- assets/illustrations/01-goal-agent-vs-formatter.png +3 -0
- assets/illustrations/02-fake-data-eval.png +3 -0
- assets/illustrations/03-agent-loop.png +3 -0
- assets/illustrations/04-pipeline-stages.png +3 -0
- assets/illustrations/05-evte-five-phases.png +3 -0
- assets/illustrations/06-evte-self-recovery.png +3 -0
- assets/illustrations/07-evte-mentor-gate.png +3 -0
- assets/illustrations/08-evte-star-online.png +3 -0
- assets/illustrations/09-eval-benchmarks.png +3 -0
- config.py +23 -0
- data/employees.csv +9 -0
- data/sales.csv +11 -0
- data/students.csv +9 -0
- eval_hackathon_benchmarks.json +51 -0
- examples.py +38 -0
- model_loader.py +53 -0
- prompts.py +28 -0
- requirements.txt +12 -0
- story.html +1376 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,12 @@ saved_model/**/* 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 36 |
+
assets/illustrations/01-goal-agent-vs-formatter.png filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
assets/illustrations/02-fake-data-eval.png filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
assets/illustrations/03-agent-loop.png filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
assets/illustrations/04-pipeline-stages.png filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
assets/illustrations/05-evte-five-phases.png filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
assets/illustrations/06-evte-self-recovery.png filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
assets/illustrations/07-evte-mentor-gate.png filter=lfs diff=lfs merge=lfs -text
|
| 43 |
+
assets/illustrations/08-evte-star-online.png filter=lfs diff=lfs merge=lfs -text
|
| 44 |
+
assets/illustrations/09-eval-benchmarks.png filter=lfs diff=lfs merge=lfs -text
|
DEPLOY.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# DataSense HF Demo — layout
|
| 2 |
+
|
| 3 |
+
```
|
| 4 |
+
hf_demo/
|
| 5 |
+
├── app.py # Gradio UI + examples
|
| 6 |
+
├── agent.py # generate → execute loop (sandbox)
|
| 7 |
+
├── model_loader.py # base + Peft adapter
|
| 8 |
+
├── config.py # model IDs & env overrides
|
| 9 |
+
├── prompts.py # SYSTEM_PROMPT
|
| 10 |
+
├── examples.py # 6 built-in demo tasks
|
| 11 |
+
├── requirements.txt # Space dependencies
|
| 12 |
+
├── README.md # HF Space card (YAML frontmatter)
|
| 13 |
+
├── story.html # Full project narrative + eval tables
|
| 14 |
+
├── eval_hackathon_benchmarks.json
|
| 15 |
+
├── data/
|
| 16 |
+
│ ├── sales.csv
|
| 17 |
+
│ ├── employees.csv
|
| 18 |
+
│ └── students.csv
|
| 19 |
+
└── assets/illustrations/ # story.html figures (9 PNGs)
|
| 20 |
+
```
|
| 21 |
+
|
| 22 |
+
## Deploy checklist
|
| 23 |
+
|
| 24 |
+
1. Create Space (Gradio, **GPU T4** minimum)
|
| 25 |
+
2. Upload this folder or `git push`
|
| 26 |
+
3. Add secret `HF_TOKEN` if the adapter repo is private
|
| 27 |
+
4. Set hardware to `gpu-t4` in README frontmatter
|
| 28 |
+
5. Smoke-test all 6 examples after build
|
| 29 |
+
|
| 30 |
+
## Model swap path
|
| 31 |
+
|
| 32 |
+
| Checkpoint | `DATASENSE_ADAPTER_MODEL` |
|
| 33 |
+
|------------|---------------------------|
|
| 34 |
+
| **SFT v1 (demo default)** | `sanjaymalladi/DataSense-Modal-E2B-SFT` |
|
| 35 |
+
| EVTE-STaR Micro-1 | `sanjaymalladi/DataSense-Modal-E2B-EVTE-Star-Micro1` |
|
| 36 |
+
| EVTE-STaR (final) | `sanjaymalladi/DataSense-Modal-E2B-EVTE-Star` |
|
| 37 |
+
| SFT v2 | `sanjaymalladi/DataSense-Modal-E2B-SFT-v2` |
|
| 38 |
+
|
| 39 |
+
## Project story
|
| 40 |
+
|
| 41 |
+
Open [`story.html`](story.html) for the full narrative, hackathon eval tables, and why we ship **SFT v1** on the live demo.
|
README.md
CHANGED
|
@@ -1,15 +1,108 @@
|
|
| 1 |
---
|
| 2 |
title: DataSense E2B
|
| 3 |
emoji: 📊
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
-
python_version: '3.12'
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
| 11 |
license: apache-2.0
|
| 12 |
short_description: Execution-grounded data agent — Gemma-4 2B + SFT v1
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
---
|
| 14 |
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
title: DataSense E2B
|
| 3 |
emoji: 📊
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: green
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 4.44.1
|
|
|
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
license: apache-2.0
|
| 11 |
short_description: Execution-grounded data agent — Gemma-4 2B + SFT v1
|
| 12 |
+
models:
|
| 13 |
+
- sanjaymalladi/DataSense-Modal-E2B-SFT
|
| 14 |
+
- unsloth/gemma-4-E2B-it
|
| 15 |
+
hardware: gpu-t4
|
| 16 |
---
|
| 17 |
|
| 18 |
+
# DataSense E2B
|
| 19 |
+
|
| 20 |
+
A **personal data-science agent** built for the Gemma / DataBench hackathon — not a chatbot that *pretends* to run code, but a model that **writes Python, executes it, reads real errors, and verifies answers** before claiming a result.
|
| 21 |
+
|
| 22 |
+
This Space runs **SFT v1** on [`unsloth/gemma-4-E2B-it`](https://huggingface.co/unsloth/gemma-4-E2B-it) with LoRA adapter [`sanjaymalladi/DataSense-Modal-E2B-SFT`](https://huggingface.co/sanjaymalladi/DataSense-Modal-E2B-SFT). Pick a bundled CSV example or ask your own question — the agent inspects schema, runs code in a sandbox, debugs from tracebacks, and returns **Answer** + **Summary** tags.
|
| 23 |
+
|
| 24 |
+
📖 **[Read the full project story →](file/story.html)**
|
| 25 |
+
|
| 26 |
+
---
|
| 27 |
+
|
| 28 |
+
## The problem we set out to solve
|
| 29 |
+
|
| 30 |
+
Small instruction models can look competent on data questions by printing plausible `**Answer:**` tags **without executing anything**. Our first eval even reported **0% accuracy for everyone** — not because training failed, but because we were scoring synthetic sandbox data against real DataBench ground truth.
|
| 31 |
+
|
| 32 |
+
The fix was changing **what we optimize and measure**: execution-grounded rollouts, typed verifiers, and eval on **mounted real files** — not hallucinated `<result>` blocks.
|
| 33 |
+
|
| 34 |
+
## Agent loop
|
| 35 |
+
|
| 36 |
+
```mermaid
|
| 37 |
+
flowchart LR
|
| 38 |
+
User[User task + CSV] --> Agent[Gemma-4 + SFT LoRA]
|
| 39 |
+
Agent --> Code[Python code]
|
| 40 |
+
Code --> Exec[Sandbox exec]
|
| 41 |
+
Exec --> Result[stdout / errors]
|
| 42 |
+
Result --> Agent
|
| 43 |
+
Agent --> Answer[Answer + Summary]
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
Same loop in training, eval, and this demo: **THINK** → **EXECUTE** → **DEBUG** → **ANSWER**.
|
| 47 |
+
|
| 48 |
+
## Training story (Modal)
|
| 49 |
+
|
| 50 |
+
Three Kaggle notebooks (SFT → GRPO → DPO) became one Modal app (`datasense_pipeline.py`) with volume checkpoints and automatic Hub pushes.
|
| 51 |
+
|
| 52 |
+
| Stage | Status | What we learned |
|
| 53 |
+
|-------|--------|-----------------|
|
| 54 |
+
| **SFT v1** | ✅ Shipped | Real execution behavior (~100% exec on many evals); foundation everything else builds on |
|
| 55 |
+
| **GRPO** | ⏸ Deferred | ~11 min/step × execution-bound rollouts — too slow for hackathon window |
|
| 56 |
+
| **DPO** | ⏸ Deferred | Prompt drift risk; EVTE-STaR took priority for hard questions |
|
| 57 |
+
| **EVTE** | ✅ Novel | When the 2B student fails, a 31B mentor must **verify its own code** before giving a diagnostic hint |
|
| 58 |
+
| **EVTE-STaR** | ✅ Research peak | Online micro-SFT every 15 verified mentor-assisted wins → Micro-1 checkpoint |
|
| 59 |
+
|
| 60 |
+
**EVTE** = Execution-Verified Tutor Escalation. **EVTE-STaR** = Self-Taught Reasoner with online weight updates instead of one offline train at the end.
|
| 61 |
+
|
| 62 |
+
## Hackathon eval (30 problems × 3 models, T4)
|
| 63 |
+
|
| 64 |
+
Macro average = unweighted mean across DataBench (15), DSBench Excel (10), and mentor-hard (5).
|
| 65 |
+
|
| 66 |
+
| Model | DataBench | DSBench | Mentor-hard | Macro | Total |
|
| 67 |
+
|-------|-----------|---------|-------------|-------|-------|
|
| 68 |
+
| Base | 60.0% | 0.0% | 20.0% | 26.7% | 10/30 |
|
| 69 |
+
| **SFT v1** ★ | **86.7%** | 0.0% | 60.0% | **48.9%** | 16/30 |
|
| 70 |
+
| EVTE Micro-1 | 80.0% | 0.0%* | **100.0%** | 60.0% | 17/30 |
|
| 71 |
+
|
| 72 |
+
\*DSBench official scorer = 0% for all models (letter vs dollar mismatch). Micro-1 Q15 computed the correct dollar value → value-aware macro would be **63.3%**.
|
| 73 |
+
|
| 74 |
+
Always pair accuracy with **exec_ok**: base can match easy booleans via answer tags while running **0%** of its code.
|
| 75 |
+
|
| 76 |
+
## Why SFT v1 for this demo (not Micro-1)
|
| 77 |
+
|
| 78 |
+
Micro-1 wins macro average on paper (driven by 5/5 mentor-hard). We still ship **SFT v1** here:
|
| 79 |
+
|
| 80 |
+
- **Best DataBench breadth** — 86.7% vs 80% (largest held-out slice)
|
| 81 |
+
- **Stable inference** — single bulk SFT vs online micro-batch 1 (replay 100% vs saved ckpt ~60%)
|
| 82 |
+
- **Lower live-demo risk** — fewer debug ramble / dtype dumps
|
| 83 |
+
- **Held up under eval reruns** — Micro-1 mentor-hard dropped when Modal stragglers overwrote volume
|
| 84 |
+
|
| 85 |
+
**Slides show all three models.** Micro-1 is the EVTE-STaR research peak; SFT v1 is the production-shaped baseline.
|
| 86 |
+
|
| 87 |
+
## What worked / what didn't
|
| 88 |
+
|
| 89 |
+
**Worked:** SFT v1 execution behavior · EVTE episode quality filter (92 curated mentor-assisted trajectories) · honest eval harness on real files
|
| 90 |
+
|
| 91 |
+
**Didn't:** Full GRPO in hackathon time · SFT v2 (recovery-only fine-tune taught debug prose, not answers) · EVTE-STaR batch 6 overtraining (40% mentor-hard vs Micro-1's 100%)
|
| 92 |
+
|
| 93 |
+
## Models on Hugging Face
|
| 94 |
+
|
| 95 |
+
| Checkpoint | Repo | Role |
|
| 96 |
+
|------------|------|------|
|
| 97 |
+
| Base | [`unsloth/gemma-4-E2B-it`](https://huggingface.co/unsloth/gemma-4-E2B-it) | Frozen foundation |
|
| 98 |
+
| **SFT v1** ★ | [`DataSense-Modal-E2B-SFT`](https://huggingface.co/sanjaymalladi/DataSense-Modal-E2B-SFT) | **This Space** |
|
| 99 |
+
| EVTE Micro-1 | [`DataSense-Modal-E2B-EVTE-Star-Micro1`](https://huggingface.co/sanjaymalladi/DataSense-Modal-E2B-EVTE-Star-Micro1) | Best mentor-hard — research |
|
| 100 |
+
|
| 101 |
+
## Try the demo
|
| 102 |
+
|
| 103 |
+
Six one-click examples on **sales**, **employees**, and **students** CSVs — no upload required.
|
| 104 |
+
|
| 105 |
+
---
|
| 106 |
+
|
| 107 |
+
**DataSense E2B** — Execution-verified, Tutor-escalation training for personal data science agents.
|
| 108 |
+
Built June 2026 · Full narrative, illustrations, and `eval_hackathon_benchmarks.json` in [`story.html`](file/story.html).
|
agent.py
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Lightweight execution-grounded agent for the HF Space demo."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
import re
|
| 7 |
+
import subprocess
|
| 8 |
+
import sys
|
| 9 |
+
import tempfile
|
| 10 |
+
import uuid
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
from typing import Any, Optional
|
| 13 |
+
|
| 14 |
+
from config import AGENT_EXEC_TIMEOUT, AGENT_MAX_NEW_TOKENS, AGENT_MAX_STEPS, AGENT_TEMPERATURE, DONE_MARKERS
|
| 15 |
+
from prompts import SYSTEM_PROMPT
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class ContextManager:
|
| 19 |
+
def __init__(self, system_prompt: str, max_tokens: int = 6000):
|
| 20 |
+
self.system_prompt = system_prompt
|
| 21 |
+
self.max_tokens = max_tokens
|
| 22 |
+
self.messages: list[dict] = []
|
| 23 |
+
self.pinned_first_msg: Optional[dict] = None
|
| 24 |
+
|
| 25 |
+
def add_user(self, content: str) -> None:
|
| 26 |
+
msg = {"role": "user", "content": content}
|
| 27 |
+
if self.pinned_first_msg is None:
|
| 28 |
+
self.pinned_first_msg = msg
|
| 29 |
+
self.messages.append(msg)
|
| 30 |
+
|
| 31 |
+
def add_assistant(self, content: str) -> None:
|
| 32 |
+
self.messages.append({"role": "assistant", "content": content})
|
| 33 |
+
|
| 34 |
+
def add_result(self, result: str) -> None:
|
| 35 |
+
self.messages.append({
|
| 36 |
+
"role": "user",
|
| 37 |
+
"content": f"<result>\n[EXEC:real]\n{result[:2000]}\n</result>",
|
| 38 |
+
})
|
| 39 |
+
|
| 40 |
+
def get_messages(self) -> list[dict]:
|
| 41 |
+
recent = self._trim_to_budget()
|
| 42 |
+
full: list[dict] = [{"role": "system", "content": self.system_prompt}]
|
| 43 |
+
if self.pinned_first_msg:
|
| 44 |
+
full.append(self.pinned_first_msg)
|
| 45 |
+
if recent and recent[0].get("content") == self.pinned_first_msg.get("content"):
|
| 46 |
+
recent = recent[1:]
|
| 47 |
+
full.extend(recent)
|
| 48 |
+
return full
|
| 49 |
+
|
| 50 |
+
def _trim_to_budget(self) -> list[dict]:
|
| 51 |
+
budget = self.max_tokens
|
| 52 |
+
trimmed: list[dict] = []
|
| 53 |
+
for msg in reversed(self.messages):
|
| 54 |
+
tokens = len(msg["content"].split()) * 1.3
|
| 55 |
+
if budget - tokens < 0:
|
| 56 |
+
break
|
| 57 |
+
trimmed.insert(0, msg)
|
| 58 |
+
budget -= tokens
|
| 59 |
+
return trimmed
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def extract_code_blocks(text: str) -> list[str]:
|
| 63 |
+
blocks = re.findall(r"```python\n(.*?)```", text, re.DOTALL)
|
| 64 |
+
if not blocks:
|
| 65 |
+
blocks = re.findall(r"```\n(.*?)```", text, re.DOTALL)
|
| 66 |
+
return blocks
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def detect_output_files(code: str) -> list[str]:
|
| 70 |
+
files: list[str] = []
|
| 71 |
+
for pattern in (
|
| 72 |
+
r'savefig\(["\']([^"\']+)["\']\)',
|
| 73 |
+
r'write_html\(["\']([^"\']+)["\']\)',
|
| 74 |
+
r'to_csv\(["\']([^"\']+)["\']\)',
|
| 75 |
+
):
|
| 76 |
+
files.extend(re.findall(pattern, code))
|
| 77 |
+
return files
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def format_exec_result(result: dict) -> str:
|
| 81 |
+
if result["success"]:
|
| 82 |
+
out = result["stdout"] or "(no output)"
|
| 83 |
+
if result["files"]:
|
| 84 |
+
out += f"\nFiles saved: {list(result['files'].keys())}"
|
| 85 |
+
else:
|
| 86 |
+
out = result["stderr"] or result["stdout"] or "(execution failed)"
|
| 87 |
+
return out
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def execute_python(code: str, working_dir: str, timeout: int = 30) -> dict:
|
| 91 |
+
os.makedirs(working_dir, exist_ok=True)
|
| 92 |
+
safe_dir = working_dir.replace("\\", "/").replace("'", "\\'")
|
| 93 |
+
preamble = (
|
| 94 |
+
f"import os\nos.chdir('{safe_dir}')\n"
|
| 95 |
+
"import matplotlib\nmatplotlib.use('Agg')\n"
|
| 96 |
+
"import warnings\nwarnings.filterwarnings('ignore')\n"
|
| 97 |
+
)
|
| 98 |
+
with tempfile.NamedTemporaryFile(
|
| 99 |
+
mode="w", suffix=".py", dir=working_dir, delete=False, encoding="utf-8",
|
| 100 |
+
) as f:
|
| 101 |
+
f.write(preamble + code)
|
| 102 |
+
tmp_path = f.name
|
| 103 |
+
try:
|
| 104 |
+
proc = subprocess.run(
|
| 105 |
+
[sys.executable, tmp_path],
|
| 106 |
+
capture_output=True,
|
| 107 |
+
text=True,
|
| 108 |
+
timeout=timeout,
|
| 109 |
+
cwd=working_dir,
|
| 110 |
+
)
|
| 111 |
+
return {
|
| 112 |
+
"stdout": (proc.stdout or "")[:3000],
|
| 113 |
+
"stderr": (proc.stderr or "")[:1500],
|
| 114 |
+
"files": {},
|
| 115 |
+
"success": proc.returncode == 0,
|
| 116 |
+
}
|
| 117 |
+
except subprocess.TimeoutExpired:
|
| 118 |
+
return {"stdout": "", "stderr": f"TimeoutError: exceeded {timeout}s", "files": {}, "success": False}
|
| 119 |
+
finally:
|
| 120 |
+
if os.path.exists(tmp_path):
|
| 121 |
+
os.unlink(tmp_path)
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def inspect_csv(path: Path) -> dict[str, str]:
|
| 125 |
+
import pandas as pd
|
| 126 |
+
|
| 127 |
+
df = pd.read_csv(path, nrows=200)
|
| 128 |
+
schema = "\n".join(f" {c}: {df[c].dtype}" for c in df.columns)
|
| 129 |
+
sample = df.head(5).to_string(index=False)
|
| 130 |
+
return {
|
| 131 |
+
"type": "csv",
|
| 132 |
+
"schema": schema,
|
| 133 |
+
"sample": sample,
|
| 134 |
+
"row_counts": f"preview_rows={len(df)} (file may be larger)",
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
def build_user_message(data_path: Path, task: str) -> str:
|
| 139 |
+
info = inspect_csv(data_path)
|
| 140 |
+
filename = data_path.name
|
| 141 |
+
lines = [
|
| 142 |
+
f"Data source: {filename}",
|
| 143 |
+
f"Working directory contains: {filename}",
|
| 144 |
+
f"Type: {info['type']}",
|
| 145 |
+
"",
|
| 146 |
+
"Schema:",
|
| 147 |
+
info["schema"],
|
| 148 |
+
"",
|
| 149 |
+
"Sample rows:",
|
| 150 |
+
info["sample"],
|
| 151 |
+
"",
|
| 152 |
+
info["row_counts"],
|
| 153 |
+
"",
|
| 154 |
+
f"Task: {task}",
|
| 155 |
+
"",
|
| 156 |
+
f"Read the file with pandas: pd.read_csv('{filename}')",
|
| 157 |
+
]
|
| 158 |
+
return "\n".join(lines)
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
def extract_answer(text: str) -> str:
|
| 162 |
+
m = re.search(r"\*\*Answer:\*\*\s*(.+?)(?:\n|$)", text)
|
| 163 |
+
return m.group(1).strip() if m else ""
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
def generate_response(messages: list, model, tokenizer) -> str:
|
| 167 |
+
import torch
|
| 168 |
+
|
| 169 |
+
input_ids = tokenizer.apply_chat_template(
|
| 170 |
+
messages,
|
| 171 |
+
tokenize=True,
|
| 172 |
+
add_generation_prompt=True,
|
| 173 |
+
return_tensors="pt",
|
| 174 |
+
).to(model.device)
|
| 175 |
+
|
| 176 |
+
with torch.no_grad():
|
| 177 |
+
output_ids = model.generate(
|
| 178 |
+
input_ids,
|
| 179 |
+
max_new_tokens=AGENT_MAX_NEW_TOKENS,
|
| 180 |
+
temperature=AGENT_TEMPERATURE,
|
| 181 |
+
do_sample=AGENT_TEMPERATURE > 0,
|
| 182 |
+
pad_token_id=tokenizer.eos_token_id,
|
| 183 |
+
)
|
| 184 |
+
return tokenizer.decode(output_ids[0][input_ids.shape[-1] :], skip_special_tokens=False)
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
def run_agent(
|
| 188 |
+
model,
|
| 189 |
+
tokenizer,
|
| 190 |
+
data_path: Path,
|
| 191 |
+
task: str,
|
| 192 |
+
*,
|
| 193 |
+
max_steps: int = AGENT_MAX_STEPS,
|
| 194 |
+
progress: Optional[Any] = None,
|
| 195 |
+
) -> dict:
|
| 196 |
+
"""Run generate → execute loop. Returns steps log + final text."""
|
| 197 |
+
workspace = Path(tempfile.gettempdir()) / f"datasense_{uuid.uuid4().hex[:10]}"
|
| 198 |
+
workspace.mkdir(parents=True, exist_ok=True)
|
| 199 |
+
|
| 200 |
+
# Copy dataset into isolated workspace
|
| 201 |
+
dest = workspace / data_path.name
|
| 202 |
+
dest.write_bytes(data_path.read_bytes())
|
| 203 |
+
|
| 204 |
+
context = ContextManager(system_prompt=SYSTEM_PROMPT)
|
| 205 |
+
context.add_user(build_user_message(dest, task))
|
| 206 |
+
|
| 207 |
+
step_logs: list[str] = []
|
| 208 |
+
final_text = ""
|
| 209 |
+
|
| 210 |
+
for step in range(max_steps):
|
| 211 |
+
if progress is not None:
|
| 212 |
+
progress((step + 1) / max_steps, desc=f"Step {step + 1}/{max_steps}")
|
| 213 |
+
|
| 214 |
+
response = generate_response(context.get_messages(), model, tokenizer)
|
| 215 |
+
context.add_assistant(response)
|
| 216 |
+
final_text = response
|
| 217 |
+
|
| 218 |
+
preview = response.replace("\n", " ")[:180]
|
| 219 |
+
step_logs.append(f"### Step {step + 1}\n{preview}...\n")
|
| 220 |
+
|
| 221 |
+
if any(m in response for m in DONE_MARKERS):
|
| 222 |
+
step_logs.append("✅ Agent finished (summary marker).\n")
|
| 223 |
+
break
|
| 224 |
+
|
| 225 |
+
code_blocks = extract_code_blocks(response)
|
| 226 |
+
if not code_blocks:
|
| 227 |
+
step_logs.append("ℹ️ No code block — stopping.\n")
|
| 228 |
+
break
|
| 229 |
+
|
| 230 |
+
result_str = ""
|
| 231 |
+
for code_block in code_blocks:
|
| 232 |
+
out_files = detect_output_files(code_block)
|
| 233 |
+
result = execute_python(
|
| 234 |
+
code=code_block,
|
| 235 |
+
working_dir=str(workspace),
|
| 236 |
+
timeout=AGENT_EXEC_TIMEOUT,
|
| 237 |
+
)
|
| 238 |
+
result_str = format_exec_result(result)
|
| 239 |
+
status = "✅" if result["success"] else "❌"
|
| 240 |
+
step_logs.append(f"{status} **Execution**\n```\n{result_str[:1200]}\n```\n")
|
| 241 |
+
|
| 242 |
+
context.add_result(result_str)
|
| 243 |
+
|
| 244 |
+
answer = extract_answer(final_text)
|
| 245 |
+
summary_match = re.search(r"\*\*Summary:\*\*\s*(.+)", final_text, re.DOTALL)
|
| 246 |
+
summary = summary_match.group(1).strip()[:1500] if summary_match else ""
|
| 247 |
+
|
| 248 |
+
return {
|
| 249 |
+
"steps_markdown": "\n".join(step_logs),
|
| 250 |
+
"final_response": final_text,
|
| 251 |
+
"answer": answer,
|
| 252 |
+
"summary": summary,
|
| 253 |
+
"workspace": str(workspace),
|
| 254 |
+
}
|
app.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
DataSense E2B — Hugging Face Space demo
|
| 3 |
+
Execution-grounded data agent (SFT v1) with bundled CSV examples.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
import gradio as gr
|
| 9 |
+
|
| 10 |
+
from agent import run_agent
|
| 11 |
+
from config import AGENT_MAX_STEPS, DATA_DIR
|
| 12 |
+
from examples import DEMO_DATASETS, DEMO_EXAMPLES
|
| 13 |
+
from model_loader import load_model_and_tokenizer
|
| 14 |
+
|
| 15 |
+
MODEL, TOKENIZER = None, None
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def _ensure_model():
|
| 19 |
+
global MODEL, TOKENIZER
|
| 20 |
+
if MODEL is None or TOKENIZER is None:
|
| 21 |
+
MODEL, TOKENIZER = load_model_and_tokenizer()
|
| 22 |
+
return MODEL, TOKENIZER
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def run_task(dataset_name: str, task: str, max_steps: int, progress=gr.Progress()):
|
| 26 |
+
if not task.strip():
|
| 27 |
+
return "Enter a task.", "", "", ""
|
| 28 |
+
|
| 29 |
+
data_path = DEMO_DATASETS.get(dataset_name)
|
| 30 |
+
if data_path is None or not data_path.is_file():
|
| 31 |
+
return f"Dataset not found: {dataset_name}", "", "", ""
|
| 32 |
+
|
| 33 |
+
try:
|
| 34 |
+
model, tokenizer = _ensure_model()
|
| 35 |
+
result = run_agent(
|
| 36 |
+
model,
|
| 37 |
+
tokenizer,
|
| 38 |
+
data_path,
|
| 39 |
+
task.strip(),
|
| 40 |
+
max_steps=int(max_steps),
|
| 41 |
+
progress=progress,
|
| 42 |
+
)
|
| 43 |
+
except Exception as exc:
|
| 44 |
+
return f"**Error:** {exc}", "", "", ""
|
| 45 |
+
|
| 46 |
+
answer_block = f"**Answer:** {result['answer']}" if result["answer"] else "_No **Answer:** tag found_"
|
| 47 |
+
summary_block = result["summary"] or "_No summary_"
|
| 48 |
+
|
| 49 |
+
return (
|
| 50 |
+
result["steps_markdown"],
|
| 51 |
+
answer_block,
|
| 52 |
+
summary_block,
|
| 53 |
+
result["final_response"][:8000],
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def build_ui() -> gr.Blocks:
|
| 58 |
+
dataset_choices = list(DEMO_DATASETS.keys())
|
| 59 |
+
|
| 60 |
+
with gr.Blocks(title="DataSense E2B", theme=gr.themes.Soft()) as demo:
|
| 61 |
+
gr.Markdown(
|
| 62 |
+
"""
|
| 63 |
+
# DataSense E2B
|
| 64 |
+
**Execution-verified data agent** — Gemma-4 2B + SFT v1 LoRA (`sanjaymalladi/DataSense-Modal-E2B-SFT`).
|
| 65 |
+
|
| 66 |
+
The model writes Python, runs it in a sandbox, reads real stdout/errors, and answers with **Answer** + **Summary** tags.
|
| 67 |
+
Pick an example below or write your own question about the bundled CSV.
|
| 68 |
+
|
| 69 |
+
📖 **[Read the full project story →](file/story.html)**
|
| 70 |
+
"""
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
with gr.Row():
|
| 74 |
+
dataset = gr.Dropdown(
|
| 75 |
+
choices=dataset_choices,
|
| 76 |
+
value=dataset_choices[0],
|
| 77 |
+
label="Dataset",
|
| 78 |
+
)
|
| 79 |
+
max_steps = gr.Slider(
|
| 80 |
+
minimum=3,
|
| 81 |
+
maximum=12,
|
| 82 |
+
value=AGENT_MAX_STEPS,
|
| 83 |
+
step=1,
|
| 84 |
+
label="Max agent steps",
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
task = gr.Textbox(
|
| 88 |
+
label="Task",
|
| 89 |
+
placeholder="e.g. Which product had the highest total revenue?",
|
| 90 |
+
lines=2,
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
run_btn = gr.Button("Run DataSense", variant="primary")
|
| 94 |
+
|
| 95 |
+
with gr.Tabs():
|
| 96 |
+
with gr.Tab("Execution trace"):
|
| 97 |
+
steps_out = gr.Markdown()
|
| 98 |
+
with gr.Tab("Answer"):
|
| 99 |
+
answer_out = gr.Markdown()
|
| 100 |
+
with gr.Tab("Summary"):
|
| 101 |
+
summary_out = gr.Markdown()
|
| 102 |
+
with gr.Tab("Raw model output"):
|
| 103 |
+
raw_out = gr.Textbox(lines=20, max_lines=40)
|
| 104 |
+
|
| 105 |
+
gr.Examples(
|
| 106 |
+
examples=DEMO_EXAMPLES,
|
| 107 |
+
inputs=[dataset, task],
|
| 108 |
+
label="Try an example",
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
gr.Markdown(
|
| 112 |
+
f"""
|
| 113 |
+
---
|
| 114 |
+
**Model:** `unsloth/gemma-4-E2B-it` + LoRA [`DataSense-Modal-E2B-SFT`](https://huggingface.co/sanjaymalladi/DataSense-Modal-E2B-SFT)
|
| 115 |
+
**Data:** sample CSVs in `{DATA_DIR.name}/` — no upload required for the demo.
|
| 116 |
+
"""
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
run_btn.click(
|
| 120 |
+
fn=run_task,
|
| 121 |
+
inputs=[dataset, task, max_steps],
|
| 122 |
+
outputs=[steps_out, answer_out, summary_out, raw_out],
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
return demo
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
if __name__ == "__main__":
|
| 129 |
+
demo = build_ui()
|
| 130 |
+
demo.queue(max_size=8).launch()
|
assets/illustrations/01-goal-agent-vs-formatter.png
ADDED
|
Git LFS Details
|
assets/illustrations/02-fake-data-eval.png
ADDED
|
Git LFS Details
|
assets/illustrations/03-agent-loop.png
ADDED
|
Git LFS Details
|
assets/illustrations/04-pipeline-stages.png
ADDED
|
Git LFS Details
|
assets/illustrations/05-evte-five-phases.png
ADDED
|
Git LFS Details
|
assets/illustrations/06-evte-self-recovery.png
ADDED
|
Git LFS Details
|
assets/illustrations/07-evte-mentor-gate.png
ADDED
|
Git LFS Details
|
assets/illustrations/08-evte-star-online.png
ADDED
|
Git LFS Details
|
assets/illustrations/09-eval-benchmarks.png
ADDED
|
Git LFS Details
|
config.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""DataSense HF Space configuration — swap model IDs here after new checkpoints."""
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
ROOT = Path(__file__).resolve().parent
|
| 7 |
+
DATA_DIR = ROOT / "data"
|
| 8 |
+
|
| 9 |
+
# Base + LoRA adapter — SFT v1 (stable demo; see story.html § demo choice)
|
| 10 |
+
BASE_MODEL = os.environ.get("DATASENSE_BASE_MODEL", "unsloth/gemma-4-E2B-it")
|
| 11 |
+
ADAPTER_MODEL = os.environ.get(
|
| 12 |
+
"DATASENSE_ADAPTER_MODEL",
|
| 13 |
+
"sanjaymalladi/DataSense-Modal-E2B-SFT",
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
MAX_SEQ_LENGTH = int(os.environ.get("DATASENSE_MAX_SEQ_LENGTH", "2048"))
|
| 17 |
+
LOAD_IN_4BIT = os.environ.get("DATASENSE_LOAD_4BIT", "1") == "1"
|
| 18 |
+
AGENT_MAX_STEPS = int(os.environ.get("DATASENSE_MAX_STEPS", "8"))
|
| 19 |
+
AGENT_MAX_NEW_TOKENS = int(os.environ.get("DATASENSE_MAX_NEW_TOKENS", "768"))
|
| 20 |
+
AGENT_EXEC_TIMEOUT = int(os.environ.get("DATASENSE_EXEC_TIMEOUT", "30"))
|
| 21 |
+
AGENT_TEMPERATURE = float(os.environ.get("DATASENSE_TEMPERATURE", "0.2"))
|
| 22 |
+
|
| 23 |
+
DONE_MARKERS = ("**Summary:**", "**Finding:**", "**Conclusion:**", "**Results:**")
|
data/employees.csv
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name,department,salary,years_experience,city
|
| 2 |
+
Alice Chen,Engineering,92000,4,SF
|
| 3 |
+
Bob Martinez,Sales,71000,6,NYC
|
| 4 |
+
Carol Wu,Engineering,105000,8,SF
|
| 5 |
+
Dan Patel,Sales,68000,3,Austin
|
| 6 |
+
Eva Johnson,Marketing,74000,5,NYC
|
| 7 |
+
Frank Lee,Engineering,88000,2,SF
|
| 8 |
+
Grace Kim,Marketing,69000,4,Seattle
|
| 9 |
+
Henry Brown,Sales,75000,7,Chicago
|
data/sales.csv
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
product,month,units,revenue
|
| 2 |
+
Widget A,2024-01,120,4800.00
|
| 3 |
+
Widget B,2024-01,85,4250.00
|
| 4 |
+
Widget A,2024-02,140,5600.00
|
| 5 |
+
Widget C,2024-02,60,3600.00
|
| 6 |
+
Widget B,2024-03,95,4750.00
|
| 7 |
+
Widget A,2024-03,200,8000.00
|
| 8 |
+
Widget C,2024-03,110,6600.00
|
| 9 |
+
Gadget X,2024-01,45,9000.00
|
| 10 |
+
Gadget X,2024-02,52,10400.00
|
| 11 |
+
Gadget X,2024-03,48,9600.00
|
data/students.csv
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
student_id,math,reading,writing,pass_math
|
| 2 |
+
101,72,68,70,True
|
| 3 |
+
102,55,60,58,False
|
| 4 |
+
103,88,91,85,True
|
| 5 |
+
104,48,52,50,False
|
| 6 |
+
105,91,89,92,True
|
| 7 |
+
106,63,70,65,True
|
| 8 |
+
107,77,74,80,True
|
| 9 |
+
108,41,45,43,False
|
eval_hackathon_benchmarks.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"description": "Hackathon eval — first complete parallel run (Modal T4). Straggler reruns overwrote volume; these are the authoritative numbers.",
|
| 3 |
+
"benchmarks": ["databench_test_15", "dsbench_analysis_10", "mentor_hard"],
|
| 4 |
+
"n_problems_per_model": 30,
|
| 5 |
+
"demo_adapter": "sanjaymalladi/DataSense-Modal-E2B-SFT",
|
| 6 |
+
"model_summaries": [
|
| 7 |
+
{
|
| 8 |
+
"suite": "Base",
|
| 9 |
+
"per_benchmark": {
|
| 10 |
+
"databench_test_15": 0.6,
|
| 11 |
+
"dsbench_analysis_10": 0.0,
|
| 12 |
+
"mentor_hard": 0.2
|
| 13 |
+
},
|
| 14 |
+
"macro_avg_accuracy": 0.26666666666666666,
|
| 15 |
+
"micro_avg_accuracy": 0.3333333333333333,
|
| 16 |
+
"total_correct": 10,
|
| 17 |
+
"total_problems": 30
|
| 18 |
+
},
|
| 19 |
+
{
|
| 20 |
+
"suite": "SFT v1",
|
| 21 |
+
"per_benchmark": {
|
| 22 |
+
"databench_test_15": 0.8666666666666667,
|
| 23 |
+
"dsbench_analysis_10": 0.0,
|
| 24 |
+
"mentor_hard": 0.6
|
| 25 |
+
},
|
| 26 |
+
"macro_avg_accuracy": 0.48888888888888893,
|
| 27 |
+
"micro_avg_accuracy": 0.5333333333333333,
|
| 28 |
+
"total_correct": 16,
|
| 29 |
+
"total_problems": 30,
|
| 30 |
+
"exec_ok_mentor_hard": 1.0
|
| 31 |
+
},
|
| 32 |
+
{
|
| 33 |
+
"suite": "EVTE-STaR Micro-1",
|
| 34 |
+
"per_benchmark": {
|
| 35 |
+
"databench_test_15": 0.8,
|
| 36 |
+
"dsbench_analysis_10": 0.0,
|
| 37 |
+
"mentor_hard": 1.0
|
| 38 |
+
},
|
| 39 |
+
"macro_avg_accuracy": 0.6,
|
| 40 |
+
"micro_avg_accuracy": 0.5666666666666667,
|
| 41 |
+
"total_correct": 17,
|
| 42 |
+
"total_problems": 30,
|
| 43 |
+
"exec_ok_mentor_hard": 1.0,
|
| 44 |
+
"dsbench_value_adjusted": {
|
| 45 |
+
"note": "Q15 exact dollar match maps to letter A; official scorer shows 0%",
|
| 46 |
+
"adjusted_correct": 1,
|
| 47 |
+
"adjusted_macro_avg_accuracy": 0.6333333333333333
|
| 48 |
+
}
|
| 49 |
+
}
|
| 50 |
+
]
|
| 51 |
+
}
|
examples.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Built-in demo tasks — shown as Gradio examples."""
|
| 2 |
+
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
|
| 5 |
+
from config import DATA_DIR
|
| 6 |
+
|
| 7 |
+
DEMO_DATASETS: dict[str, Path] = {
|
| 8 |
+
"Sales (CSV)": DATA_DIR / "sales.csv",
|
| 9 |
+
"Employees (CSV)": DATA_DIR / "employees.csv",
|
| 10 |
+
"Students (CSV)": DATA_DIR / "students.csv",
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
DEMO_EXAMPLES: list[list[str]] = [
|
| 14 |
+
[
|
| 15 |
+
"Sales (CSV)",
|
| 16 |
+
"Which product had the highest total revenue across all months?",
|
| 17 |
+
],
|
| 18 |
+
[
|
| 19 |
+
"Sales (CSV)",
|
| 20 |
+
"How many units of Widget A were sold in total?",
|
| 21 |
+
],
|
| 22 |
+
[
|
| 23 |
+
"Employees (CSV)",
|
| 24 |
+
"What is the average salary in the Engineering department?",
|
| 25 |
+
],
|
| 26 |
+
[
|
| 27 |
+
"Employees (CSV)",
|
| 28 |
+
"Which city has the highest average salary?",
|
| 29 |
+
],
|
| 30 |
+
[
|
| 31 |
+
"Students (CSV)",
|
| 32 |
+
"What percentage of students passed math (pass_math=True)?",
|
| 33 |
+
],
|
| 34 |
+
[
|
| 35 |
+
"Students (CSV)",
|
| 36 |
+
"Who is the student with the highest average score across math, reading, and writing?",
|
| 37 |
+
],
|
| 38 |
+
]
|
model_loader.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Load Gemma-4 E2B base + DataSense SFT LoRA adapter for inference."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
from functools import lru_cache
|
| 7 |
+
from typing import Any
|
| 8 |
+
|
| 9 |
+
import torch
|
| 10 |
+
from config import ADAPTER_MODEL, BASE_MODEL, LOAD_IN_4BIT, MAX_SEQ_LENGTH
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
@lru_cache(maxsize=1)
|
| 14 |
+
def load_model_and_tokenizer() -> tuple[Any, Any]:
|
| 15 |
+
from peft import PeftModel
|
| 16 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
|
| 17 |
+
|
| 18 |
+
token = os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN")
|
| 19 |
+
|
| 20 |
+
print(f"Loading base: {BASE_MODEL}")
|
| 21 |
+
print(f"Loading adapter: {ADAPTER_MODEL}")
|
| 22 |
+
|
| 23 |
+
quant_config = None
|
| 24 |
+
if LOAD_IN_4BIT and torch.cuda.is_available():
|
| 25 |
+
quant_config = BitsAndBytesConfig(
|
| 26 |
+
load_in_4bit=True,
|
| 27 |
+
bnb_4bit_quant_type="nf4",
|
| 28 |
+
bnb_4bit_compute_dtype=torch.bfloat16,
|
| 29 |
+
bnb_4bit_use_double_quant=True,
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL, token=token, trust_remote_code=True)
|
| 33 |
+
if tokenizer.pad_token is None:
|
| 34 |
+
tokenizer.pad_token = tokenizer.eos_token
|
| 35 |
+
|
| 36 |
+
base = AutoModelForCausalLM.from_pretrained(
|
| 37 |
+
BASE_MODEL,
|
| 38 |
+
token=token,
|
| 39 |
+
trust_remote_code=True,
|
| 40 |
+
device_map="auto" if torch.cuda.is_available() else None,
|
| 41 |
+
quantization_config=quant_config,
|
| 42 |
+
torch_dtype=torch.bfloat16 if torch.cuda.is_available() else torch.float32,
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
try:
|
| 46 |
+
model = PeftModel.from_pretrained(base, ADAPTER_MODEL, token=token, is_trainable=False)
|
| 47 |
+
print("✓ Loaded LoRA adapter")
|
| 48 |
+
except Exception as exc:
|
| 49 |
+
print(f"⚠ Adapter load failed ({exc}) — using base model only")
|
| 50 |
+
model = base
|
| 51 |
+
|
| 52 |
+
model.eval()
|
| 53 |
+
return model, tokenizer
|
prompts.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""System prompt — kept in sync with datasense_utils.SYSTEM_PROMPT."""
|
| 2 |
+
|
| 3 |
+
SYSTEM_PROMPT = (
|
| 4 |
+
"You are DataSense, a personal data science and data engineering agent.\n"
|
| 5 |
+
"You explore large databases, clean messy data, analyze, model, visualize, and explain findings.\n"
|
| 6 |
+
"\n"
|
| 7 |
+
"Workflow for every task:\n"
|
| 8 |
+
"1. THINK — inspect schema, row counts, nulls, dtypes before analysis\n"
|
| 9 |
+
"2. EXPLORE — head(), describe(), value_counts(), or SQL LIMIT 5 on large tables\n"
|
| 10 |
+
"3. EXECUTE — one focused code step at a time; use the <result> to decide next step\n"
|
| 11 |
+
"4. DEBUG — read tracebacks; fix column names, dtypes, joins, and SQL syntax yourself\n"
|
| 12 |
+
"5. SCALE — for large data use SQL/DuckDB/pandas chunks; avoid loading entire tables blindly\n"
|
| 13 |
+
"\n"
|
| 14 |
+
"Data sources you may receive:\n"
|
| 15 |
+
"- CSV files (data.csv) — use pandas\n"
|
| 16 |
+
"- SQLite databases (*.db) — use sqlite3 or sqlalchemy + pandas.read_sql\n"
|
| 17 |
+
"- Multi-table warehouses — JOIN, GROUP BY, window functions; verify with small queries first\n"
|
| 18 |
+
"\n"
|
| 19 |
+
"Visualizations: matplotlib, seaborn, or plotly — always savefig('chart.png') or write_html('chart.html')\n"
|
| 20 |
+
"Dashboards: complete Streamlit apps; start the code block with # DASHBOARD:\n"
|
| 21 |
+
"Final step: print ONLY the answer value as the last line of your last code block.\n"
|
| 22 |
+
"Then write:\n"
|
| 23 |
+
"**Answer:** <raw value only — True, False, 0, 32.0, Atlanta, etc.>\n"
|
| 24 |
+
"**Summary:** <plain English explanation>\n"
|
| 25 |
+
"\n"
|
| 26 |
+
"Use only real, verified APIs. If unsure of exact syntax, use the simpler approach.\n"
|
| 27 |
+
"Do NOT hallucinate function names or parameters."
|
| 28 |
+
)
|
requirements.txt
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio>=4.44.0,<6.0.0
|
| 2 |
+
torch>=2.1.0
|
| 3 |
+
transformers>=4.51.0
|
| 4 |
+
peft>=0.11.0
|
| 5 |
+
accelerate>=0.30.0
|
| 6 |
+
bitsandbytes>=0.43.0
|
| 7 |
+
pandas>=2.0.0
|
| 8 |
+
numpy>=1.26.0
|
| 9 |
+
matplotlib>=3.7.0
|
| 10 |
+
sentencepiece>=0.2.0
|
| 11 |
+
protobuf>=4.25.0
|
| 12 |
+
huggingface_hub>=0.23.0
|
story.html
ADDED
|
@@ -0,0 +1,1376 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>DataSense E2B — The Full Story</title>
|
| 7 |
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
| 8 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
| 9 |
+
<link href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,300..900;1,9..144,300..900&family=IBM+Plex+Mono:ital,wght@0,400;0,500;0,600;1,400&family=Newsreader:ital,opsz,wght@0,6..72,200..800;1,6..72,200..800&display=swap" rel="stylesheet" />
|
| 10 |
+
<style>
|
| 11 |
+
:root {
|
| 12 |
+
/* Editorial Color Palette */
|
| 13 |
+
--bg: #F4F3ED; /* Warm newspaper cream */
|
| 14 |
+
--text: #111110; /* Deep ink */
|
| 15 |
+
--text-muted: #4A4A46;
|
| 16 |
+
--border: #111110;
|
| 17 |
+
|
| 18 |
+
/* Vibrant Print Accents */
|
| 19 |
+
--accent: #E1341E; /* Vermilion Red */
|
| 20 |
+
--accent-blue: #1843D2; /* Cobalt */
|
| 21 |
+
--accent-warm: #D46F15; /* Ochre */
|
| 22 |
+
--accent-ok: #0D733B; /* Forest Green */
|
| 23 |
+
|
| 24 |
+
--max-width: 860px;
|
| 25 |
+
--radius: 0px; /* Brutalist/Print - absolutely no rounded corners */
|
| 26 |
+
--shadow-offset: 6px;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
| 30 |
+
|
| 31 |
+
html { scroll-behavior: smooth; }
|
| 32 |
+
|
| 33 |
+
::selection {
|
| 34 |
+
background: var(--accent);
|
| 35 |
+
color: var(--bg);
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
body {
|
| 39 |
+
font-family: "Newsreader", serif;
|
| 40 |
+
background-color: var(--bg);
|
| 41 |
+
color: var(--text);
|
| 42 |
+
line-height: 1.65;
|
| 43 |
+
font-size: 1.15rem;
|
| 44 |
+
font-weight: 400;
|
| 45 |
+
-webkit-font-smoothing: antialiased;
|
| 46 |
+
/* Subtle noise texture for a paper feel */
|
| 47 |
+
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 400 400' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)' opacity='0.04'/%3E%3C/svg%3E");
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
.wrap {
|
| 51 |
+
max-width: var(--max-width);
|
| 52 |
+
margin: 0 auto;
|
| 53 |
+
padding: 4rem 2rem 8rem;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
/* -------------------------------------------
|
| 57 |
+
Header & Hero Typography
|
| 58 |
+
------------------------------------------- */
|
| 59 |
+
header {
|
| 60 |
+
margin-bottom: 4rem;
|
| 61 |
+
padding-bottom: 3rem;
|
| 62 |
+
border-bottom: 4px solid var(--border);
|
| 63 |
+
position: relative;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
header::after {
|
| 67 |
+
content: "";
|
| 68 |
+
position: absolute;
|
| 69 |
+
bottom: -10px;
|
| 70 |
+
left: 0;
|
| 71 |
+
width: 100%;
|
| 72 |
+
height: 1px;
|
| 73 |
+
background: var(--border);
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
.badge {
|
| 77 |
+
display: inline-block;
|
| 78 |
+
font-family: "IBM Plex Mono", monospace;
|
| 79 |
+
font-size: 0.75rem;
|
| 80 |
+
font-weight: 600;
|
| 81 |
+
letter-spacing: 0.1em;
|
| 82 |
+
text-transform: uppercase;
|
| 83 |
+
color: var(--bg);
|
| 84 |
+
background: var(--text);
|
| 85 |
+
padding: 0.4rem 0.8rem;
|
| 86 |
+
margin-bottom: 2rem;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
h1 {
|
| 90 |
+
font-family: "Fraunces", serif;
|
| 91 |
+
font-size: clamp(3rem, 7vw, 5.5rem);
|
| 92 |
+
font-weight: 800;
|
| 93 |
+
font-variation-settings: "SOFT" 0, "WONK" 1;
|
| 94 |
+
line-height: 0.95;
|
| 95 |
+
letter-spacing: -0.03em;
|
| 96 |
+
margin-bottom: 1.5rem;
|
| 97 |
+
text-transform: uppercase;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
.subtitle {
|
| 101 |
+
font-family: "Newsreader", serif;
|
| 102 |
+
font-size: 1.4rem;
|
| 103 |
+
font-style: italic;
|
| 104 |
+
color: var(--text-muted);
|
| 105 |
+
max-width: 36em;
|
| 106 |
+
line-height: 1.4;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
.meta {
|
| 110 |
+
margin-top: 2rem;
|
| 111 |
+
font-family: "IBM Plex Mono", monospace;
|
| 112 |
+
font-size: 0.85rem;
|
| 113 |
+
text-transform: uppercase;
|
| 114 |
+
letter-spacing: 0.05em;
|
| 115 |
+
color: var(--text-muted);
|
| 116 |
+
border-top: 1px dashed var(--border);
|
| 117 |
+
padding-top: 1rem;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
/* -------------------------------------------
|
| 121 |
+
Table of Contents
|
| 122 |
+
------------------------------------------- */
|
| 123 |
+
nav.toc {
|
| 124 |
+
background: transparent;
|
| 125 |
+
border: 2px solid var(--border);
|
| 126 |
+
padding: 2rem;
|
| 127 |
+
margin-bottom: 4rem;
|
| 128 |
+
box-shadow: var(--shadow-offset) var(--shadow-offset) 0 var(--border);
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
nav.toc h2 {
|
| 132 |
+
font-family: "IBM Plex Mono", monospace;
|
| 133 |
+
font-size: 0.9rem;
|
| 134 |
+
text-transform: uppercase;
|
| 135 |
+
letter-spacing: 0.1em;
|
| 136 |
+
border-bottom: 2px solid var(--border);
|
| 137 |
+
padding-bottom: 0.75rem;
|
| 138 |
+
margin-bottom: 1.5rem;
|
| 139 |
+
padding-top: 0;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
nav.toc ol {
|
| 143 |
+
list-style: none;
|
| 144 |
+
counter-reset: toc;
|
| 145 |
+
column-count: 2;
|
| 146 |
+
column-gap: 3rem;
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
@media (max-width: 640px) {
|
| 150 |
+
nav.toc ol { column-count: 1; }
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
nav.toc li {
|
| 154 |
+
counter-increment: toc;
|
| 155 |
+
margin-bottom: 0.75rem;
|
| 156 |
+
break-inside: avoid;
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
nav.toc a {
|
| 160 |
+
color: var(--text);
|
| 161 |
+
text-decoration: none;
|
| 162 |
+
display: flex;
|
| 163 |
+
gap: 0.5rem;
|
| 164 |
+
font-weight: 500;
|
| 165 |
+
transition: color 0.2s, transform 0.2s;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
nav.toc a::before {
|
| 169 |
+
content: counter(toc, decimal-leading-zero) ".";
|
| 170 |
+
font-family: "IBM Plex Mono", monospace;
|
| 171 |
+
font-weight: 600;
|
| 172 |
+
color: var(--accent);
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
nav.toc a:hover {
|
| 176 |
+
color: var(--accent);
|
| 177 |
+
transform: translateX(4px);
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
/* -------------------------------------------
|
| 181 |
+
Typography & Content
|
| 182 |
+
------------------------------------------- */
|
| 183 |
+
section {
|
| 184 |
+
margin-bottom: 5rem;
|
| 185 |
+
position: relative;
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
section::before {
|
| 189 |
+
content: "";
|
| 190 |
+
display: block;
|
| 191 |
+
width: 3rem;
|
| 192 |
+
height: 4px;
|
| 193 |
+
background: var(--accent);
|
| 194 |
+
margin-bottom: 1.5rem;
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
h2 {
|
| 198 |
+
font-family: "Fraunces", serif;
|
| 199 |
+
font-size: 2.5rem;
|
| 200 |
+
font-weight: 700;
|
| 201 |
+
letter-spacing: -0.02em;
|
| 202 |
+
margin-bottom: 1.5rem;
|
| 203 |
+
line-height: 1.1;
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
h3 {
|
| 207 |
+
font-family: "Fraunces", serif;
|
| 208 |
+
font-size: 1.5rem;
|
| 209 |
+
font-weight: 600;
|
| 210 |
+
font-style: italic;
|
| 211 |
+
margin: 2.5rem 0 1rem;
|
| 212 |
+
color: var(--text);
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
h4 {
|
| 216 |
+
font-family: "IBM Plex Mono", monospace;
|
| 217 |
+
font-size: 1rem;
|
| 218 |
+
font-weight: 600;
|
| 219 |
+
text-transform: uppercase;
|
| 220 |
+
letter-spacing: 0.05em;
|
| 221 |
+
margin: 2rem 0 0.75rem;
|
| 222 |
+
color: var(--text);
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
p { margin-bottom: 1.25rem; }
|
| 226 |
+
|
| 227 |
+
ul, ol {
|
| 228 |
+
margin: 0 0 1.5rem 2rem;
|
| 229 |
+
padding: 0;
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
li { margin-bottom: 0.5rem; }
|
| 233 |
+
|
| 234 |
+
li::marker {
|
| 235 |
+
color: var(--accent);
|
| 236 |
+
font-weight: bold;
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
strong { font-weight: 700; color: var(--text); }
|
| 240 |
+
em { font-style: italic; font-family: "Fraunces", serif; }
|
| 241 |
+
|
| 242 |
+
a {
|
| 243 |
+
color: var(--accent-blue);
|
| 244 |
+
text-decoration: underline;
|
| 245 |
+
text-underline-offset: 4px;
|
| 246 |
+
text-decoration-thickness: 1px;
|
| 247 |
+
transition: all 0.2s;
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
a:hover {
|
| 251 |
+
background: var(--accent-blue);
|
| 252 |
+
color: var(--bg);
|
| 253 |
+
text-decoration-color: transparent;
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
/* -------------------------------------------
|
| 257 |
+
Cards & Callouts
|
| 258 |
+
------------------------------------------- */
|
| 259 |
+
.card {
|
| 260 |
+
background: var(--bg);
|
| 261 |
+
border: 2px solid var(--border);
|
| 262 |
+
padding: 1.75rem 2rem;
|
| 263 |
+
margin: 2rem 0;
|
| 264 |
+
position: relative;
|
| 265 |
+
box-shadow: var(--shadow-offset) var(--shadow-offset) 0 var(--border);
|
| 266 |
+
transition: transform 0.2s, box-shadow 0.2s;
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
.card:hover {
|
| 270 |
+
transform: translate(-2px, -2px);
|
| 271 |
+
box-shadow: calc(var(--shadow-offset) + 2px) calc(var(--shadow-offset) + 2px) 0 var(--border);
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
.card.highlight {
|
| 275 |
+
border-color: var(--text);
|
| 276 |
+
background: #fdfcfa;
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
.card.highlight::before {
|
| 280 |
+
content: "";
|
| 281 |
+
position: absolute;
|
| 282 |
+
top: 0; left: 0; bottom: 0;
|
| 283 |
+
width: 8px;
|
| 284 |
+
background: var(--accent-blue);
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
.card.warn {
|
| 288 |
+
background: #fcf6ef;
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
.card.warn::before {
|
| 292 |
+
content: "";
|
| 293 |
+
position: absolute;
|
| 294 |
+
top: 0; left: 0; bottom: 0;
|
| 295 |
+
width: 8px;
|
| 296 |
+
background: var(--accent-warm);
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
.card.danger {
|
| 300 |
+
background: #fcefed;
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
.card.danger::before {
|
| 304 |
+
content: "";
|
| 305 |
+
position: absolute;
|
| 306 |
+
top: 0; left: 0; bottom: 0;
|
| 307 |
+
width: 8px;
|
| 308 |
+
background: var(--accent);
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
.card-title {
|
| 312 |
+
font-family: "IBM Plex Mono", monospace;
|
| 313 |
+
font-weight: 700;
|
| 314 |
+
font-size: 0.85rem;
|
| 315 |
+
text-transform: uppercase;
|
| 316 |
+
letter-spacing: 0.08em;
|
| 317 |
+
color: var(--text);
|
| 318 |
+
border-bottom: 1px solid var(--border);
|
| 319 |
+
padding-bottom: 0.5rem;
|
| 320 |
+
margin-bottom: 1rem;
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
.card h4 {
|
| 324 |
+
margin-top: 0;
|
| 325 |
+
border-bottom: 1px solid var(--border);
|
| 326 |
+
padding-bottom: 0.5rem;
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
/* -------------------------------------------
|
| 330 |
+
Data Display (Tables & Code)
|
| 331 |
+
------------------------------------------- */
|
| 332 |
+
table {
|
| 333 |
+
width: 100%;
|
| 334 |
+
border-collapse: collapse;
|
| 335 |
+
margin: 2rem 0;
|
| 336 |
+
font-family: "Newsreader", serif;
|
| 337 |
+
font-size: 1rem;
|
| 338 |
+
border-top: 3px solid var(--border);
|
| 339 |
+
border-bottom: 3px solid var(--border);
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
th, td {
|
| 343 |
+
text-align: left;
|
| 344 |
+
padding: 0.85rem 1rem;
|
| 345 |
+
border-bottom: 1px solid #d4d3cf;
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
th {
|
| 349 |
+
font-family: "IBM Plex Mono", monospace;
|
| 350 |
+
font-size: 0.75rem;
|
| 351 |
+
text-transform: uppercase;
|
| 352 |
+
letter-spacing: 0.05em;
|
| 353 |
+
color: var(--text);
|
| 354 |
+
font-weight: 600;
|
| 355 |
+
vertical-align: bottom;
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
tr:last-child td { border-bottom: none; }
|
| 359 |
+
|
| 360 |
+
tr:hover td { background: rgba(0,0,0,0.03); }
|
| 361 |
+
|
| 362 |
+
.num-good { color: var(--accent-ok); font-weight: 700; }
|
| 363 |
+
.num-mid { color: var(--accent-warm); font-weight: 700; }
|
| 364 |
+
.num-bad { color: var(--accent); font-weight: 700; }
|
| 365 |
+
.pending { color: var(--text-muted); font-style: italic; }
|
| 366 |
+
|
| 367 |
+
code, .mono {
|
| 368 |
+
font-family: "IBM Plex Mono", monospace;
|
| 369 |
+
font-size: 0.85em;
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
p code, li code {
|
| 373 |
+
background: #e8e7e1;
|
| 374 |
+
border: 1px solid #d4d3cf;
|
| 375 |
+
padding: 0.15em 0.3em;
|
| 376 |
+
color: var(--text);
|
| 377 |
+
font-weight: 500;
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
pre {
|
| 381 |
+
background: var(--text);
|
| 382 |
+
color: var(--bg);
|
| 383 |
+
padding: 1.5rem;
|
| 384 |
+
overflow-x: auto;
|
| 385 |
+
font-family: "IBM Plex Mono", monospace;
|
| 386 |
+
font-size: 0.85rem;
|
| 387 |
+
line-height: 1.5;
|
| 388 |
+
margin: 2rem 0;
|
| 389 |
+
box-shadow: var(--shadow-offset) var(--shadow-offset) 0 var(--accent);
|
| 390 |
+
}
|
| 391 |
+
|
| 392 |
+
pre code {
|
| 393 |
+
background: transparent;
|
| 394 |
+
border: none;
|
| 395 |
+
color: inherit;
|
| 396 |
+
padding: 0;
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
/* -------------------------------------------
|
| 400 |
+
UI Elements
|
| 401 |
+
------------------------------------------- */
|
| 402 |
+
.flow {
|
| 403 |
+
display: flex;
|
| 404 |
+
flex-wrap: wrap;
|
| 405 |
+
gap: 0;
|
| 406 |
+
align-items: center;
|
| 407 |
+
margin: 2rem 0;
|
| 408 |
+
font-family: "IBM Plex Mono", monospace;
|
| 409 |
+
font-size: 0.85rem;
|
| 410 |
+
font-weight: 600;
|
| 411 |
+
text-transform: uppercase;
|
| 412 |
+
border: 2px solid var(--border);
|
| 413 |
+
box-shadow: 4px 4px 0 var(--border);
|
| 414 |
+
width: fit-content;
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
.flow span {
|
| 418 |
+
padding: 0.5rem 1rem;
|
| 419 |
+
background: var(--bg);
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
.flow .arrow {
|
| 423 |
+
background: var(--text);
|
| 424 |
+
color: var(--bg);
|
| 425 |
+
padding: 0.5rem;
|
| 426 |
+
}
|
| 427 |
+
|
| 428 |
+
.pill-row {
|
| 429 |
+
display: flex;
|
| 430 |
+
flex-wrap: wrap;
|
| 431 |
+
gap: 0.5rem;
|
| 432 |
+
margin: 1rem 0;
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
.pill {
|
| 436 |
+
font-family: "IBM Plex Mono", monospace;
|
| 437 |
+
font-size: 0.75rem;
|
| 438 |
+
font-weight: 600;
|
| 439 |
+
text-transform: uppercase;
|
| 440 |
+
padding: 0.25rem 0.5rem;
|
| 441 |
+
border: 1px solid var(--border);
|
| 442 |
+
background: var(--bg);
|
| 443 |
+
}
|
| 444 |
+
|
| 445 |
+
.pill.ok { background: var(--accent-ok); color: #fff; border-color: var(--accent-ok); }
|
| 446 |
+
.pill.no { background: var(--accent); color: #fff; border-color: var(--accent); }
|
| 447 |
+
.pill.run { background: var(--accent-blue); color: #fff; border-color: var(--accent-blue); }
|
| 448 |
+
|
| 449 |
+
.two-col {
|
| 450 |
+
display: grid;
|
| 451 |
+
grid-template-columns: 1fr 1fr;
|
| 452 |
+
gap: 2rem;
|
| 453 |
+
margin: 2rem 0;
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
/* -------------------------------------------
|
| 457 |
+
Special Components
|
| 458 |
+
------------------------------------------- */
|
| 459 |
+
.status-banner {
|
| 460 |
+
background: var(--text);
|
| 461 |
+
color: var(--bg);
|
| 462 |
+
padding: 1rem 1.5rem;
|
| 463 |
+
margin-bottom: 3rem;
|
| 464 |
+
font-family: "IBM Plex Mono", monospace;
|
| 465 |
+
font-size: 0.85rem;
|
| 466 |
+
border: 2px solid var(--text);
|
| 467 |
+
position: relative;
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
.status-banner::after {
|
| 471 |
+
content: "";
|
| 472 |
+
position: absolute;
|
| 473 |
+
top: 4px; left: 4px; right: -8px; bottom: -8px;
|
| 474 |
+
border: 1px solid var(--text);
|
| 475 |
+
z-index: -1;
|
| 476 |
+
}
|
| 477 |
+
|
| 478 |
+
.status-banner strong {
|
| 479 |
+
color: #fff;
|
| 480 |
+
text-transform: uppercase;
|
| 481 |
+
letter-spacing: 0.05em;
|
| 482 |
+
margin-right: 0.5rem;
|
| 483 |
+
}
|
| 484 |
+
|
| 485 |
+
figure.figure {
|
| 486 |
+
margin: 3rem 0;
|
| 487 |
+
border: 2px solid var(--border);
|
| 488 |
+
box-shadow: var(--shadow-offset) var(--shadow-offset) 0 var(--border);
|
| 489 |
+
background: var(--bg);
|
| 490 |
+
}
|
| 491 |
+
|
| 492 |
+
figure.figure img {
|
| 493 |
+
display: block;
|
| 494 |
+
width: 100%;
|
| 495 |
+
height: auto;
|
| 496 |
+
filter: grayscale(100%) contrast(1.1); /* Editorial print feel */
|
| 497 |
+
transition: filter 0.3s;
|
| 498 |
+
}
|
| 499 |
+
|
| 500 |
+
figure.figure:hover img {
|
| 501 |
+
filter: grayscale(0%);
|
| 502 |
+
}
|
| 503 |
+
|
| 504 |
+
figure.figure figcaption {
|
| 505 |
+
padding: 1rem 1.25rem;
|
| 506 |
+
font-family: "Newsreader", serif;
|
| 507 |
+
font-size: 0.95rem;
|
| 508 |
+
color: var(--text);
|
| 509 |
+
border-top: 2px solid var(--border);
|
| 510 |
+
background: #fdfcfa;
|
| 511 |
+
}
|
| 512 |
+
|
| 513 |
+
.gate-table td:first-child {
|
| 514 |
+
font-family: "IBM Plex Mono", monospace;
|
| 515 |
+
font-size: 0.85rem;
|
| 516 |
+
font-weight: 600;
|
| 517 |
+
}
|
| 518 |
+
|
| 519 |
+
.phase-grid {
|
| 520 |
+
display: grid;
|
| 521 |
+
gap: 1.5rem;
|
| 522 |
+
margin: 2.5rem 0;
|
| 523 |
+
}
|
| 524 |
+
|
| 525 |
+
.phase-card {
|
| 526 |
+
border: 1px solid var(--border);
|
| 527 |
+
padding: 1.5rem;
|
| 528 |
+
position: relative;
|
| 529 |
+
}
|
| 530 |
+
|
| 531 |
+
.phase-card::before {
|
| 532 |
+
content: "";
|
| 533 |
+
position: absolute;
|
| 534 |
+
top: 0; left: 0;
|
| 535 |
+
width: 100%;
|
| 536 |
+
height: 4px;
|
| 537 |
+
background: var(--accent);
|
| 538 |
+
}
|
| 539 |
+
|
| 540 |
+
.phase-card h4 { margin: 0 0 0.5rem; }
|
| 541 |
+
.phase-card p { margin: 0; }
|
| 542 |
+
|
| 543 |
+
blockquote.pull {
|
| 544 |
+
font-family: "Fraunces", serif;
|
| 545 |
+
font-size: 1.5rem;
|
| 546 |
+
line-height: 1.4;
|
| 547 |
+
font-style: italic;
|
| 548 |
+
margin: 3rem 0;
|
| 549 |
+
padding: 2rem;
|
| 550 |
+
border-top: 2px solid var(--border);
|
| 551 |
+
border-bottom: 2px solid var(--border);
|
| 552 |
+
text-align: center;
|
| 553 |
+
color: var(--text);
|
| 554 |
+
background: repeating-linear-gradient(
|
| 555 |
+
45deg,
|
| 556 |
+
transparent,
|
| 557 |
+
transparent 10px,
|
| 558 |
+
rgba(0,0,0,0.02) 10px,
|
| 559 |
+
rgba(0,0,0,0.02) 20px
|
| 560 |
+
);
|
| 561 |
+
}
|
| 562 |
+
|
| 563 |
+
/* -------------------------------------------
|
| 564 |
+
Footer
|
| 565 |
+
------------------------------------------- */
|
| 566 |
+
footer {
|
| 567 |
+
margin-top: 6rem;
|
| 568 |
+
padding-top: 3rem;
|
| 569 |
+
border-top: 4px solid var(--border);
|
| 570 |
+
font-family: "IBM Plex Mono", monospace;
|
| 571 |
+
font-size: 0.85rem;
|
| 572 |
+
text-transform: uppercase;
|
| 573 |
+
letter-spacing: 0.05em;
|
| 574 |
+
color: var(--text-muted);
|
| 575 |
+
}
|
| 576 |
+
|
| 577 |
+
footer a { color: var(--text); font-weight: 600; }
|
| 578 |
+
|
| 579 |
+
@media (max-width: 640px) {
|
| 580 |
+
.two-col { grid-template-columns: 1fr; }
|
| 581 |
+
.wrap { padding: 2rem 1rem 4rem; }
|
| 582 |
+
h1 { font-size: 2.5rem; }
|
| 583 |
+
}
|
| 584 |
+
</style>
|
| 585 |
+
</head>
|
| 586 |
+
<body>
|
| 587 |
+
<div class="wrap">
|
| 588 |
+
<header>
|
| 589 |
+
<div class="badge">Vol. 1 — June 2026</div>
|
| 590 |
+
<h1>DataSense E2B<br />The Full Story</h1>
|
| 591 |
+
<p class="subtitle">
|
| 592 |
+
How we set out to build a <strong>personal data-science agent</strong> — not a chatbot that
|
| 593 |
+
<em>pretends</em> to run code, but one that <strong>writes Python, executes it, reads real errors,
|
| 594 |
+
and verifies answers</strong> — and what we learned training Gemma-4-2B on Modal with methods
|
| 595 |
+
we had to invent along the way.
|
| 596 |
+
</p>
|
| 597 |
+
<p class="meta">
|
| 598 |
+
Base: <code>unsloth/gemma-4-E2B-it</code><br />
|
| 599 |
+
Pipeline: Modal A100/T4<br />
|
| 600 |
+
Team: <strong>DataSense E2B</strong> (Execution-verified, Tutor-escalation)<br />
|
| 601 |
+
<a href="/">← Back to live Gradio demo</a>
|
| 602 |
+
</p>
|
| 603 |
+
</header>
|
| 604 |
+
|
| 605 |
+
<div class="status-banner">
|
| 606 |
+
<strong>Eval complete.</strong> Hackathon benchmarks finished on Modal (first complete parallel run, 30 problems × 3 models).
|
| 607 |
+
Modal app stopped — straggler reruns were overwriting results. Numbers below are from the authoritative first run
|
| 608 |
+
(<code>eval_hackathon_benchmarks.json</code>). This Space ships <strong>SFT v1</strong> for the live demo.
|
| 609 |
+
</div>
|
| 610 |
+
|
| 611 |
+
<nav class="toc" aria-label="Table of contents">
|
| 612 |
+
<h2>Index</h2>
|
| 613 |
+
<ol>
|
| 614 |
+
<li><a href="#goal">The goal</a></li>
|
| 615 |
+
<li><a href="#start">Where we started</a></li>
|
| 616 |
+
<li><a href="#problem">The problem with naive finetuning</a></li>
|
| 617 |
+
<li><a href="#agent">The DataSense agent loop</a></li>
|
| 618 |
+
<li><a href="#pipeline">Training pipeline: SFT → GRPO → DPO</a></li>
|
| 619 |
+
<li><a href="#methods">Supporting methods (verifiers, eval)</a></li>
|
| 620 |
+
<li><a href="#evte">EVTE — core idea & motivation</a></li>
|
| 621 |
+
<li><a href="#evte-feedback">EVTE feedback loops (self-recovery)</a></li>
|
| 622 |
+
<li><a href="#evte-mentor">Mentor verify & hint protocol</a></li>
|
| 623 |
+
<li><a href="#evte-star">EVTE-STaR — online micro-SFT</a></li>
|
| 624 |
+
<li><a href="#evte-outcomes">Episode outcomes & trainability gates</a></li>
|
| 625 |
+
<li><a href="#worked">What worked</a></li>
|
| 626 |
+
<li><a href="#didnt">What didn't work</a></li>
|
| 627 |
+
<li><a href="#evals">Evaluation results</a></li>
|
| 628 |
+
<li><a href="#demo-choice">Why SFT v1 for the demo</a></li>
|
| 629 |
+
<li><a href="#benchmarks">Benchmark suite</a></li>
|
| 630 |
+
<li><a href="#models">Model checkpoints</a></li>
|
| 631 |
+
<li><a href="#demo">This demo & what's next</a></li>
|
| 632 |
+
</ol>
|
| 633 |
+
</nav>
|
| 634 |
+
|
| 635 |
+
<!-- 01 GOAL -->
|
| 636 |
+
<section id="goal">
|
| 637 |
+
<h2>01 · The goal</h2>
|
| 638 |
+
<p>
|
| 639 |
+
The hackathon asked for something ambitious: take a small open model and make it genuinely useful
|
| 640 |
+
for <strong>data work</strong> — exploring tables, cleaning messy columns, aggregating, joining,
|
| 641 |
+
visualizing, and answering questions with <strong>verifiable correctness</strong>, not plausible prose.
|
| 642 |
+
</p>
|
| 643 |
+
<p>Our north star was simple to state and hard to achieve:</p>
|
| 644 |
+
<div class="card highlight">
|
| 645 |
+
<div class="card-title">North star</div>
|
| 646 |
+
<p style="margin:0">
|
| 647 |
+
A <strong>2B-parameter student agent</strong> that behaves like a junior data analyst:
|
| 648 |
+
inspect schema first, run focused code steps, debug from real tracebacks, and only claim an
|
| 649 |
+
answer after execution confirms it — with a training story credible enough for slides,
|
| 650 |
+
papers, and a public Hugging Face demo.
|
| 651 |
+
</p>
|
| 652 |
+
</div>
|
| 653 |
+
<p>Concretely, we targeted:</p>
|
| 654 |
+
<ul>
|
| 655 |
+
<li><strong>Execution-grounded behavior</strong> — rewards and eval tied to real <code>stdout</code> / errors, not hallucinated <code><result></code> blocks</li>
|
| 656 |
+
<li><strong>Multi-benchmark credibility</strong> — DataBench, DSBench Excel analysis, and a curated hard pool from our own training data</li>
|
| 657 |
+
<li><strong>A reproducible Modal pipeline</strong> — one app, volume checkpoints, automatic HF Hub pushes</li>
|
| 658 |
+
<li><strong>Novel training for hard questions</strong> — when the student fails, a larger mentor verifies a solution and gives diagnostic hints <em>without leaking the answer</em></li>
|
| 659 |
+
</ul>
|
| 660 |
+
|
| 661 |
+
<figure class="figure">
|
| 662 |
+
<img src="assets/illustrations/01-goal-agent-vs-formatter.png" alt="Formatter that fakes answers versus a real execution-verified agent" loading="lazy" />
|
| 663 |
+
<figcaption><strong>Fig 1 — Goal.</strong> We optimize for an agent that runs code on real data and verifies answers — not a model that prints plausible <code> Answer: </code> tags without executing anything.</figcaption>
|
| 664 |
+
</figure>
|
| 665 |
+
</section>
|
| 666 |
+
|
| 667 |
+
<!-- 02 START -->
|
| 668 |
+
<section id="start">
|
| 669 |
+
<h2>02 · Where we started</h2>
|
| 670 |
+
<h3>The base model</h3>
|
| 671 |
+
<p>
|
| 672 |
+
We built on <code>unsloth/gemma-4-E2B-it</code> — Google's Gemma 4 2B instruction model in
|
| 673 |
+
Unsloth's E2B (execution-to-build) variant. It's small enough to fine-tune on a single GPU,
|
| 674 |
+
yet designed with code and tool use in mind. We used 4-bit quantization, LoRA rank 32,
|
| 675 |
+
and a 2048-token context throughout.
|
| 676 |
+
</p>
|
| 677 |
+
|
| 678 |
+
<h3>Three Kaggle notebooks → one Modal app</h3>
|
| 679 |
+
<p>
|
| 680 |
+
The project began as three separate Kaggle notebooks covering supervised fine-tuning (SFT),
|
| 681 |
+
GRPO reinforcement learning, and DPO preference optimization. We consolidated them into
|
| 682 |
+
<code>datasense_pipeline.py</code> — a single Modal application with shared config in
|
| 683 |
+
<code>datasense_utils.py</code> — so training could run unattended on cloud GPUs with
|
| 684 |
+
checkpoints persisted to a Modal volume and pushed to Hugging Face.
|
| 685 |
+
</p>
|
| 686 |
+
|
| 687 |
+
<h3>Nine bugs we fixed before trusting any number</h3>
|
| 688 |
+
<p>Early runs were misleading because the ported notebooks had latent bugs. We fixed all nine before building the pipeline:</p>
|
| 689 |
+
<table>
|
| 690 |
+
<thead>
|
| 691 |
+
<tr><th>#</th><th>Bug</th><th>Impact</th></tr>
|
| 692 |
+
</thead>
|
| 693 |
+
<tbody>
|
| 694 |
+
<tr><td>1</td><td><code>sft_warmup</code> KeyError</td><td>SFT wouldn't start</td></tr>
|
| 695 |
+
<tr><td>2</td><td><code>lora_target_modules</code> KeyError</td><td>LoRA attach failed</td></tr>
|
| 696 |
+
<tr><td>3</td><td><code>result_str</code> UnboundLocalError</td><td>Agent loop crashed mid-rollout</td></tr>
|
| 697 |
+
<tr><td>4</td><td>DPO pairs missing chat template prefix</td><td>Preference data malformed</td></tr>
|
| 698 |
+
<tr><td>5</td><td><code>skip_special_tokens=False</code></td><td>Decode pollution in rewards</td></tr>
|
| 699 |
+
<tr><td>6</td><td>Dead <code>oci_sft_v1</code> variable</td><td>Confusing / broken cells</td></tr>
|
| 700 |
+
<tr><td>7</td><td>GRPO <code>max_steps</code> hardcoded</td><td>Config ignored</td></tr>
|
| 701 |
+
<tr><td>8</td><td>Shorter <code>SYSTEM_PROMPT</code> in DPO cell</td><td>Train/eval prompt drift</td></tr>
|
| 702 |
+
<tr><td>9</td><td><code>_PROBLEM_LOOKUP</code> naming mismatch</td><td>Dataset indexing broken</td></tr>
|
| 703 |
+
</tbody>
|
| 704 |
+
</table>
|
| 705 |
+
|
| 706 |
+
<h3>Day-one eval: 0% accuracy (and why that was informative)</h3>
|
| 707 |
+
<p>
|
| 708 |
+
Our first agent eval reported <strong>0% accuracy</strong> for everyone — including SFT — while
|
| 709 |
+
SFT already showed <strong>100% execution success</strong> and ~5.6 agent steps vs base's 2% exec /
|
| 710 |
+
1.1 steps. That gap taught us the first big lesson: <strong>the model was learning to run code,
|
| 711 |
+
but we weren't scoring against real data.</strong>
|
| 712 |
+
</p>
|
| 713 |
+
<div class="card warn">
|
| 714 |
+
<div class="card-title">Root cause</div>
|
| 715 |
+
<p style="margin:0">
|
| 716 |
+
Eval workspaces used <strong>synthetic random CSVs</strong> when DataBench parquet wasn't mounted,
|
| 717 |
+
but ground truth came from the <strong>real</strong> dataset. The agent analyzed fake data and
|
| 718 |
+
was graded against true answers — guaranteed 0%.
|
| 719 |
+
</p>
|
| 720 |
+
</div>
|
| 721 |
+
|
| 722 |
+
<figure class="figure">
|
| 723 |
+
<img src="assets/illustrations/02-fake-data-eval.png" alt="Eval bug: synthetic workspace data scored against real ground truth" loading="lazy" />
|
| 724 |
+
<figcaption><strong>Fig 2 — The 0% eval bug.</strong> Early runs used random synthetic CSVs in the sandbox while ground truth came from real DataBench files — so even a good agent could never match.</figcaption>
|
| 725 |
+
</figure>
|
| 726 |
+
</section>
|
| 727 |
+
|
| 728 |
+
<!-- 03 PROBLEM -->
|
| 729 |
+
<section id="problem">
|
| 730 |
+
<h2>03 · The problem with naive finetuning</h2>
|
| 731 |
+
<p>
|
| 732 |
+
Most "data agent" demos finetune on static (question, code, answer) triples. The model learns
|
| 733 |
+
to <em>format</em> responses that look like an agent — <code> Answer: </code> tags, pandas snippets,
|
| 734 |
+
confident summaries — without ever closing the loop on execution.
|
| 735 |
+
</p>
|
| 736 |
+
<p>We observed three failure modes immediately:</p>
|
| 737 |
+
<div class="two-col">
|
| 738 |
+
<div class="card">
|
| 739 |
+
<div class="card-title">Formatter, not agent</div>
|
| 740 |
+
<p style="margin:0;font-size:0.95rem">
|
| 741 |
+
Base Gemma-4 could score well on easy boolean questions by emitting answer tags in a single
|
| 742 |
+
turn — <strong>0% code execution</strong> — beating SFT on accuracy while doing none of the work.
|
| 743 |
+
</p>
|
| 744 |
+
</div>
|
| 745 |
+
<div class="card">
|
| 746 |
+
<div class="card-title">Hallucinated execution</div>
|
| 747 |
+
<p style="margin:0;font-size:0.95rem">
|
| 748 |
+
Models invent <code><result></code> blocks with fake stdout. RL rewards on text alone
|
| 749 |
+
reinforce the illusion of competence.
|
| 750 |
+
</p>
|
| 751 |
+
</div>
|
| 752 |
+
</div>
|
| 753 |
+
<p>
|
| 754 |
+
The fix wasn't "more SFT data." It was changing <strong>what we optimize and measure</strong>:
|
| 755 |
+
real subprocess execution, multi-turn observe→fix→retry, and verifiers that compare parsed answers
|
| 756 |
+
to typed ground truth (boolean, number, category, list types).
|
| 757 |
+
</p>
|
| 758 |
+
</section>
|
| 759 |
+
|
| 760 |
+
<!-- 04 AGENT -->
|
| 761 |
+
<section id="agent">
|
| 762 |
+
<h2>04 · The DataSense agent loop</h2>
|
| 763 |
+
<p>Every training rollout and eval episode follows the same production-shaped loop:</p>
|
| 764 |
+
<div class="flow">
|
| 765 |
+
<span>THINK</span><span class="arrow">→</span>
|
| 766 |
+
<span>EXPLORE</span><span class="arrow">→</span>
|
| 767 |
+
<span>EXECUTE</span><span class="arrow">→</span>
|
| 768 |
+
<span>DEBUG</span><span class="arrow">→</span>
|
| 769 |
+
<span>ANSWER</span>
|
| 770 |
+
</div>
|
| 771 |
+
<ol>
|
| 772 |
+
<li><strong>THINK</strong> — inspect schema, dtypes, nulls before analysis</li>
|
| 773 |
+
<li><strong>EXPLORE</strong> — <code>head()</code>, <code>describe()</code>, small SQL <code>LIMIT</code> queries</li>
|
| 774 |
+
<li><strong>EXECUTE</strong> — one focused Python step; read real <code><result></code> from sandbox</li>
|
| 775 |
+
<li><strong>DEBUG</strong> — fix column names, joins, dtypes from tracebacks</li>
|
| 776 |
+
<li><strong>ANSWER</strong> — <code> Answer: </code> + <code> Summary: </code> after verified execution</li>
|
| 777 |
+
</ol>
|
| 778 |
+
<p>
|
| 779 |
+
The system prompt (shared across train, eval, and this HF demo) explicitly forbids hallucinated APIs
|
| 780 |
+
and requires the final printed value to match the answer tag. For DataBench we mount real
|
| 781 |
+
<code>sample.parquet</code> into the workspace; for DSBench we copy <code>.xlsx</code> workbooks
|
| 782 |
+
and use <code>inspect_source</code> for Excel structure.
|
| 783 |
+
</p>
|
| 784 |
+
<pre>Reward signal (simplified):
|
| 785 |
+
+ execution actually ran
|
| 786 |
+
+ stdout parseable
|
| 787 |
+
+ answer matches ground truth (typed comparator)
|
| 788 |
+
− hallucinated inline <result> without [EXEC:real]
|
| 789 |
+
− debug rambling / column dumps as "answers"</pre>
|
| 790 |
+
|
| 791 |
+
<figure class="figure">
|
| 792 |
+
<img src="assets/illustrations/03-agent-loop.png" alt="THINK EXPLORE EXECUTE DEBUG ANSWER agent loop" loading="lazy" />
|
| 793 |
+
<figcaption><strong>Fig 3 — Agent loop.</strong> Every rollout follows the same multi-step cycle: inspect, run code, read real output, debug, then answer.</figcaption>
|
| 794 |
+
</figure>
|
| 795 |
+
</section>
|
| 796 |
+
|
| 797 |
+
<!-- 05 PIPELINE -->
|
| 798 |
+
<section id="pipeline">
|
| 799 |
+
<h2>05 · Training pipeline: SFT → GRPO → DPO</h2>
|
| 800 |
+
<p>Our planned stack mirrors modern agent training — with execution at every stage:</p>
|
| 801 |
+
<div class="flow">
|
| 802 |
+
<span>SFT</span><span class="arrow">→</span>
|
| 803 |
+
<span>GRPO</span><span class="arrow">→</span>
|
| 804 |
+
<span>DPO</span><span class="arrow">→</span>
|
| 805 |
+
<span>Eval</span>
|
| 806 |
+
</div>
|
| 807 |
+
|
| 808 |
+
<h3>Stage 1 — Supervised fine-tuning (SFT v1) ✅</h3>
|
| 809 |
+
<p>
|
| 810 |
+
Bulk SFT on DataBench-style traces plus agent supplements: multi-turn dialogs, Jupyter-agent
|
| 811 |
+
traces, dashboard examples, and code-feedback execution pairs. This produced our strongest
|
| 812 |
+
baseline — <code>sanjaymalladi/DataSense-Modal-E2B-SFT</code>.
|
| 813 |
+
</p>
|
| 814 |
+
<ul>
|
| 815 |
+
<li>LoRA r=32, α=64 on all attention + MLP projections</li>
|
| 816 |
+
<li>~600 max steps, effective batch 8</li>
|
| 817 |
+
<li>Teaches the model to <em>use</em> the agent format and run multi-step code</li>
|
| 818 |
+
</ul>
|
| 819 |
+
|
| 820 |
+
<h3>Stage 2 — GRPO (execution-grounded RL) ⚠️ partial</h3>
|
| 821 |
+
<p>
|
| 822 |
+
Group Relative Policy Optimization with <strong>real Python rollouts</strong> per prompt.
|
| 823 |
+
Each step spawns multiple agent trajectories; rewards use <code>compute_trajectory_reward()</code>
|
| 824 |
+
with <code>require_real_execution=True</code>.
|
| 825 |
+
</p>
|
| 826 |
+
<p>
|
| 827 |
+
GRPO on Gemma-4 is brutally slow (~11 min/step on A100) because most wall time is
|
| 828 |
+
<strong>CPU-bound execution</strong>, not GPU matmul — 4 rollouts × up to 5 agent steps ×
|
| 829 |
+
subprocess sandboxing. We fixed trajectory forwarding bugs, KL instability
|
| 830 |
+
(<code>final_logit_softcapping=30</code>), and added parallel rollout workers — but full
|
| 831 |
+
300-step GRPO remained impractical within hackathon time. A shortened 100-step run was targeted.
|
| 832 |
+
</p>
|
| 833 |
+
|
| 834 |
+
<h3>Stage 3 — DPO ⏸️ deferred</h3>
|
| 835 |
+
<p>
|
| 836 |
+
Preference pairs from high vs low reward rollouts (min gap 0.15) — planned but deprioritized
|
| 837 |
+
once EVTE-STaR showed more promise for hard-question gains within our compute budget.
|
| 838 |
+
</p>
|
| 839 |
+
|
| 840 |
+
<figure class="figure">
|
| 841 |
+
<img src="assets/illustrations/04-pipeline-stages.png" alt="SFT GRPO DPO training pipeline stages" loading="lazy" />
|
| 842 |
+
<figcaption><strong>Fig 4 — Training stages.</strong> SFT v1 shipped and works. Full GRPO was execution-bound and slow. DPO was deferred in favor of EVTE-STaR.</figcaption>
|
| 843 |
+
</figure>
|
| 844 |
+
</section>
|
| 845 |
+
|
| 846 |
+
<!-- 06 METHODS (supporting) -->
|
| 847 |
+
<section id="methods">
|
| 848 |
+
<h2>06 · Supporting infrastructure (not EVTE itself)</h2>
|
| 849 |
+
<p>
|
| 850 |
+
Before EVTE could work, we needed execution-grounded rollouts, typed verifiers, and honest eval.
|
| 851 |
+
These are the plumbing; the novel research contribution is EVTE + EVTE-STaR (sections 07–11 below).
|
| 852 |
+
</p>
|
| 853 |
+
|
| 854 |
+
<h3>Execution-grounded rollouts</h3>
|
| 855 |
+
<p>
|
| 856 |
+
Every GRPO/DPO/EVTE trajectory runs code in an isolated workspace. Rewards ignore fake
|
| 857 |
+
<code><result></code> tags unless tagged <code>[EXEC:real]</code>.
|
| 858 |
+
</p>
|
| 859 |
+
|
| 860 |
+
<h3>Typed answer verification (<code>databench_compare</code> + neural verifier)</h3>
|
| 861 |
+
<p>
|
| 862 |
+
Evidence-bound scoring chain: exec stdout → <code> Answer: </code> tag → LLM extract → typed compare
|
| 863 |
+
(boolean, float, category, <code>list[category]</code>, <code>list[number]</code>).
|
| 864 |
+
Without this, mentors "fail" when extraction fails, not when reasoning fails.
|
| 865 |
+
</p>
|
| 866 |
+
|
| 867 |
+
<h3>Lite eval & hackathon harness</h3>
|
| 868 |
+
<p>
|
| 869 |
+
DataBench lite scores against <code>sample_answer</code> on mounted parquet.
|
| 870 |
+
<code>run_hackathon_benchmarks_parallel</code> runs Base / SFT / Micro-1 across three benchmarks on T4.
|
| 871 |
+
</p>
|
| 872 |
+
</section>
|
| 873 |
+
|
| 874 |
+
<!-- 07 EVTE CORE -->
|
| 875 |
+
<section id="evte">
|
| 876 |
+
<h2>07 · EVTE — Execution-Verified Tutor Escalation</h2>
|
| 877 |
+
<p>
|
| 878 |
+
<strong>EVTE</strong> is the method we built when classical distillation and STaR broke down for
|
| 879 |
+
data agents. The name encodes three commitments:
|
| 880 |
+
</p>
|
| 881 |
+
<ul>
|
| 882 |
+
<li><strong>Execution</strong> — every claim of success must be backed by real code that ran on real files</li>
|
| 883 |
+
<li><strong>Verified</strong> — student <em>and</em> mentor answers pass the same typed verifier</li>
|
| 884 |
+
<li><strong>Tutor Escalation</strong> — a larger model intervenes only after student failure, and only as a <em>coach</em>, not an answer vending machine</li>
|
| 885 |
+
</ul>
|
| 886 |
+
|
| 887 |
+
<h3>Why we needed EVTE</h3>
|
| 888 |
+
<p>
|
| 889 |
+
Classical <strong>STaR</strong> (Self-Taught Reasoner) assumes a strong teacher can produce correct
|
| 890 |
+
reasoning chains, filter them, and fine-tune the student offline. That fails for DataSense because:
|
| 891 |
+
</p>
|
| 892 |
+
<ol>
|
| 893 |
+
<li>Our <strong>2B student</strong> often can't solve list/category questions at all</li>
|
| 894 |
+
<li>Our <strong>31B mentor</strong> also fails verification on the hardest 5 problems (~40% mentor-hard pool)</li>
|
| 895 |
+
<li>Even when code is <em>right</em>, <strong>answer extraction</strong> fails (no tag, wrong stdout parse)</li>
|
| 896 |
+
<li>Distilling final answers teaches <strong>memorization</strong>; we need debugging under execution constraints</li>
|
| 897 |
+
</ol>
|
| 898 |
+
|
| 899 |
+
<h3>The five-phase episode (EVTE and EVTE-STaR share this skeleton)</h3>
|
| 900 |
+
<p>Implemented in <code>datasense_evte.py</code> — <code>run_evte_episode</code> (offline collection) and <code>run_evte_star_episode</code> (online training).</p>
|
| 901 |
+
|
| 902 |
+
<div class="phase-grid">
|
| 903 |
+
<div class="phase-card">
|
| 904 |
+
<h4>Phase 1 · Student first attempt</h4>
|
| 905 |
+
<p>2B student, up to 5 agent steps, real workspace (CSV/parquet/xlsx). Scored via <code>score_rollout()</code>.</p>
|
| 906 |
+
</div>
|
| 907 |
+
<div class="phase-card">
|
| 908 |
+
<h4>Phase 2 · Self-recovery feedback</h4>
|
| 909 |
+
<p>Up to 3 rounds of <code>build_self_recovery_feedback()</code> — real tracebacks, answer withheld.</p>
|
| 910 |
+
</div>
|
| 911 |
+
<div class="phase-card">
|
| 912 |
+
<h4>Phase 3 · Mentor independent verify</h4>
|
| 913 |
+
<p>31B mentor solves in a <em>fresh</em> workspace; must pass the same verifier before any hint.</p>
|
| 914 |
+
</div>
|
| 915 |
+
<div class="phase-card">
|
| 916 |
+
<h4>Phase 4 · Diagnostic mentor hint</h4>
|
| 917 |
+
<p><code>generate_mentor_hint()</code> under <code>MENTOR_HINT_SYSTEM</code> — no final answer, no full script.</p>
|
| 918 |
+
</div>
|
| 919 |
+
<div class="phase-card">
|
| 920 |
+
<h4>Phase 5 · Post-hint student</h4>
|
| 921 |
+
<p>Up to 2 attempts × 5 steps. Episode saved only if student verifies after reading the hint.</p>
|
| 922 |
+
</div>
|
| 923 |
+
</div>
|
| 924 |
+
|
| 925 |
+
<figure class="figure">
|
| 926 |
+
<img src="assets/illustrations/05-evte-five-phases.png" alt="EVTE five phases from student attempt to mentor-assisted success" loading="lazy" />
|
| 927 |
+
<figcaption><strong>Fig 5 — EVTE in five phases.</strong> Student tries → self-recovery → mentor must verify independently → diagnostic hint → student retries. Only verified post-hint wins become training data.</figcaption>
|
| 928 |
+
</figure>
|
| 929 |
+
|
| 930 |
+
<pre>run_evte_star_episode (simplified control flow):
|
| 931 |
+
|
| 932 |
+
student_rollout = phase_1_student()
|
| 933 |
+
if clean_first_try_verified and not messy_recovery_in_trace:
|
| 934 |
+
return SKIP # already knows it — not trainable in STaR mode
|
| 935 |
+
|
| 936 |
+
if not verified:
|
| 937 |
+
for i in 1..3:
|
| 938 |
+
add_user(build_self_recovery_feedback()) # ← EVTE feedback
|
| 939 |
+
student_rollout = student_retry()
|
| 940 |
+
|
| 941 |
+
mentor_ok, mentor_rollout = mentor_verify_solution(
|
| 942 |
+
student_rollout=junior_trace # mentor sees failed code
|
| 943 |
+
)
|
| 944 |
+
if not mentor_ok:
|
| 945 |
+
return DISCARD # mentor_unverified — no training signal
|
| 946 |
+
|
| 947 |
+
hint = generate_mentor_hint(student_rollout, mentor_rollout)
|
| 948 |
+
add_user("[MENTOR] " + hint) # diagnostic only
|
| 949 |
+
|
| 950 |
+
for j in 1..2:
|
| 951 |
+
student_rollout = student_retry()
|
| 952 |
+
if verified:
|
| 953 |
+
return SAVE_TRAINABLE_EPISODE # mentor_assisted</pre>
|
| 954 |
+
|
| 955 |
+
<h3>Hard-first curriculum</h3>
|
| 956 |
+
<p>
|
| 957 |
+
<code>_prioritize_evte_problems()</code> sorts <code>list[category]</code>, <code>list[number]</code>,
|
| 958 |
+
and multi-answer types before easy booleans. EVTE compute is expensive (two models × multi-step agents);
|
| 959 |
+
we spend it where SFT v1 plateaus.
|
| 960 |
+
</p>
|
| 961 |
+
|
| 962 |
+
<h3>Mentor hardware choreography</h3>
|
| 963 |
+
<p>
|
| 964 |
+
Student (2B) and mentor (31B) don't fit comfortably together on one A100. The STaR loop uses
|
| 965 |
+
<code>on_micro_batch</code> hooks to <strong>unload mentor → micro-SFT student → reload mentor</strong>
|
| 966 |
+
every 15 episodes. Progress persists to <code>evte_star_progress.json</code> with resume support.
|
| 967 |
+
</p>
|
| 968 |
+
</section>
|
| 969 |
+
|
| 970 |
+
<!-- 08 EVTE FEEDBACK -->
|
| 971 |
+
<section id="evte-feedback">
|
| 972 |
+
<h2>08 · EVTE feedback — self-recovery without answer leakage</h2>
|
| 973 |
+
<p>
|
| 974 |
+
The most underrated piece of EVTE is not the mentor — it's <strong>what we put in the user turn
|
| 975 |
+
when the student fails</strong>. This is <code>build_self_recovery_feedback()</code> in
|
| 976 |
+
<code>datasense_evte.py</code>.
|
| 977 |
+
</p>
|
| 978 |
+
|
| 979 |
+
<figure class="figure">
|
| 980 |
+
<img src="assets/illustrations/06-evte-self-recovery.png" alt="Self-recovery feedback loop with real errors but hidden ground truth" loading="lazy" />
|
| 981 |
+
<figcaption><strong>Fig 6 — Self-recovery feedback.</strong> The student sees wrong predictions, last code, and real tracebacks — never the correct answer.</figcaption>
|
| 982 |
+
</figure>
|
| 983 |
+
|
| 984 |
+
<blockquote class="pull">
|
| 985 |
+
Messy success = verified answer but conversation contains debug/recovery language
|
| 986 |
+
(<code>trajectory_has_recovery_signal()</code>). We don't want to reinforce "stumble into correctness"
|
| 987 |
+
without tutor review in STaR mode.
|
| 988 |
+
</blockquote>
|
| 989 |
+
|
| 990 |
+
<h3>Why SFT v2 failed — feedback without balance</h3>
|
| 991 |
+
<p>
|
| 992 |
+
When we later fine-tuned <strong>only</strong> on recovery trajectories (SFT v2), the model learned
|
| 993 |
+
the <em>shape</em> of debug prose — dtype dumps, column lists — without improving verified answers.
|
| 994 |
+
Lesson: self-recovery feedback is essential <strong>during collection</strong>, but training must mix
|
| 995 |
+
clean completions with mentor-assisted wins, not recovery-only soup.
|
| 996 |
+
</p>
|
| 997 |
+
</section>
|
| 998 |
+
|
| 999 |
+
<!-- 09 EVTE MENTOR -->
|
| 1000 |
+
<section id="evte-mentor">
|
| 1001 |
+
<h2>09 · Mentor verify & hint protocol</h2>
|
| 1002 |
+
<p>
|
| 1003 |
+
The mentor is <code>google/gemma-4-31B-it</code> (4-bit via Unsloth). It is <strong>not</strong> an oracle
|
| 1004 |
+
that whispers answers. It must earn the right to hint by passing the same execution verifier as the student.
|
| 1005 |
+
</p>
|
| 1006 |
+
|
| 1007 |
+
<figure class="figure">
|
| 1008 |
+
<img src="assets/illustrations/07-evte-mentor-gate.png" alt="Mentor must pass verification gate before giving a diagnostic hint" loading="lazy" />
|
| 1009 |
+
<figcaption><strong>Fig 7 — Mentor gate.</strong> The 31B mentor must verify its own solution by running code before it may give a hint — and the hint must not leak the final answer.</figcaption>
|
| 1010 |
+
</figure>
|
| 1011 |
+
|
| 1012 |
+
<h3>Mentor retry modes</h3>
|
| 1013 |
+
<table>
|
| 1014 |
+
<thead>
|
| 1015 |
+
<tr><th>Mode</th><th>Behavior</th><th>Config</th></tr>
|
| 1016 |
+
</thead>
|
| 1017 |
+
<tbody>
|
| 1018 |
+
<tr>
|
| 1019 |
+
<td><strong>series</strong></td>
|
| 1020 |
+
<td>Same conversation; temps ramp 0.4 → 0.65 → 0.85</td>
|
| 1021 |
+
<td><code>evte_mentor_retry_mode=series</code></td>
|
| 1022 |
+
</tr>
|
| 1023 |
+
<tr>
|
| 1024 |
+
<td><strong>parallel</strong></td>
|
| 1025 |
+
<td>3 independent workspaces; first verified wins; temps [0.2, 0.5, 0.7]</td>
|
| 1026 |
+
<td><code>evte_mentor_retry_mode=parallel</code></td>
|
| 1027 |
+
</tr>
|
| 1028 |
+
</tbody>
|
| 1029 |
+
</table>
|
| 1030 |
+
|
| 1031 |
+
</section>
|
| 1032 |
+
|
| 1033 |
+
<!-- 10 EVTE-STAR -->
|
| 1034 |
+
<section id="evte-star">
|
| 1035 |
+
<h2>10 · EVTE-STaR — online Self-Taught Reasoner with micro-SFT</h2>
|
| 1036 |
+
<p>
|
| 1037 |
+
<strong>EVTE-STaR</strong> combines EVTE episode collection with <strong>online weight updates</strong>.
|
| 1038 |
+
Classical STaR: collect all successes → train offline once. EVTE-STaR:
|
| 1039 |
+
<strong>collect 15 verified mentor-assisted wins → micro-SFT 30 steps → student is slightly better → repeat.</strong>
|
| 1040 |
+
</p>
|
| 1041 |
+
|
| 1042 |
+
<figure class="figure">
|
| 1043 |
+
<img src="assets/illustrations/08-evte-star-online.png" alt="EVTE-STaR online micro-SFT every 15 verified episodes" loading="lazy" />
|
| 1044 |
+
<figcaption><strong>Fig 8 — EVTE-STaR online loop.</strong> Every 15 mentor-assisted wins → 30-step micro-SFT at low LR → student continues on harder problems with nudged weights.</figcaption>
|
| 1045 |
+
</figure>
|
| 1046 |
+
|
| 1047 |
+
<h3>The overtraining curve (batches 2–3 vs batch 6)</h3>
|
| 1048 |
+
<p>
|
| 1049 |
+
Micro-batch <strong>1</strong> replay in RAM scored <strong>100%</strong> on mentor-hard (5 problems).
|
| 1050 |
+
Saved Micro-1 checkpoint: ~<strong>60%</strong> confirmatory. Replay of batches <strong>2–3</strong>:
|
| 1051 |
+
~<strong>80%</strong>. Final batch <strong>6</strong> checkpoint: ~<strong>40%</strong> — worse than SFT v1.
|
| 1052 |
+
</p>
|
| 1053 |
+
<div class="card warn">
|
| 1054 |
+
<div class="card-title">Lesson</div>
|
| 1055 |
+
<p style="margin:0">
|
| 1056 |
+
Online micro-SFT needs <strong>early stopping on a held-out hard set</strong>, not "more batches = better."
|
| 1057 |
+
We only preserved micro-1 and final checkpoints on the volume — sweet-spot batches 2–3 were lost
|
| 1058 |
+
until <code>run_micro_replay_eval</code> reconstructed them in RAM.
|
| 1059 |
+
</p>
|
| 1060 |
+
</div>
|
| 1061 |
+
</section>
|
| 1062 |
+
|
| 1063 |
+
<!-- 11 EVTE OUTCOMES -->
|
| 1064 |
+
<section id="evte-outcomes">
|
| 1065 |
+
<h2>11 · Episode outcomes & trainability gates</h2>
|
| 1066 |
+
<p>Every episode ends in exactly one outcome. The outcome determines whether it enters training.</p>
|
| 1067 |
+
|
| 1068 |
+
<table>
|
| 1069 |
+
<thead>
|
| 1070 |
+
<tr><th>Outcome</th><th>Meaning</th><th>EVTE-STaR: train?</th></tr>
|
| 1071 |
+
</thead>
|
| 1072 |
+
<tbody>
|
| 1073 |
+
<tr>
|
| 1074 |
+
<td><code>self_solved_clean</code></td>
|
| 1075 |
+
<td>First-try verified, no recovery signals in trace</td>
|
| 1076 |
+
<td class="num-bad">Skip</td>
|
| 1077 |
+
</tr>
|
| 1078 |
+
<tr>
|
| 1079 |
+
<td><code>self_recovered</code></td>
|
| 1080 |
+
<td>Fixed via self-recovery feedback only</td>
|
| 1081 |
+
<td class="num-mid">Optional</td>
|
| 1082 |
+
</tr>
|
| 1083 |
+
<tr>
|
| 1084 |
+
<td><code>mentor_assisted</code></td>
|
| 1085 |
+
<td>Failed → mentor verified → hint → student verified</td>
|
| 1086 |
+
<td class="num-good">Yes</td>
|
| 1087 |
+
</tr>
|
| 1088 |
+
<tr>
|
| 1089 |
+
<td><code>discarded</code></td>
|
| 1090 |
+
<td>Mentor couldn't pass execution verifier</td>
|
| 1091 |
+
<td class="num-bad">No</td>
|
| 1092 |
+
</tr>
|
| 1093 |
+
</tbody>
|
| 1094 |
+
</table>
|
| 1095 |
+
</section>
|
| 1096 |
+
|
| 1097 |
+
<!-- 12 WORKED -->
|
| 1098 |
+
<section id="worked">
|
| 1099 |
+
<h2>12 · What worked</h2>
|
| 1100 |
+
|
| 1101 |
+
<div class="card">
|
| 1102 |
+
<h4>✅ SFT v1 — real execution behavior</h4>
|
| 1103 |
+
<p style="margin:0.5rem 0 0">
|
| 1104 |
+
SFT v1 consistently runs real Python (100% exec on many evals), uses ~4–5 agent steps, and
|
| 1105 |
+
beats base on hard questions where base "wins" without code. This is the behavioral foundation
|
| 1106 |
+
everything else builds on.
|
| 1107 |
+
</p>
|
| 1108 |
+
</div>
|
| 1109 |
+
|
| 1110 |
+
<div class="card">
|
| 1111 |
+
<h4>✅ EVTE episode quality filter</h4>
|
| 1112 |
+
<p style="margin:0.5rem 0 0">
|
| 1113 |
+
Saving only mentor-assisted verified trajectories produced high-signal data — multi-turn debug
|
| 1114 |
+
with real errors, not synthetic Q/A. 92 episodes is small but <em>curated</em>.
|
| 1115 |
+
</p>
|
| 1116 |
+
</div>
|
| 1117 |
+
</section>
|
| 1118 |
+
|
| 1119 |
+
<!-- 09 DIDNT -->
|
| 1120 |
+
<section id="didnt">
|
| 1121 |
+
<h2>13 · What didn't work</h2>
|
| 1122 |
+
|
| 1123 |
+
<div class="card danger">
|
| 1124 |
+
<h4>❌ Full GRPO within hackathon time</h4>
|
| 1125 |
+
<p style="margin:0.5rem 0 0">
|
| 1126 |
+
~11 min/step × hundreds of steps × execution-bound rollouts ≈ multi-day runs. Parallel rollout
|
| 1127 |
+
workers helped but couldn't change the fundamental CPU/GPU pipeline stall. vLLM isn't available
|
| 1128 |
+
for Gemma 4 E2B, so generation stays on HF generate.
|
| 1129 |
+
</p>
|
| 1130 |
+
</div>
|
| 1131 |
+
|
| 1132 |
+
<div class="card danger">
|
| 1133 |
+
<h4>❌ SFT v2 (recovery-only fine-tune)</h4>
|
| 1134 |
+
<p style="margin:0.5rem 0 0">
|
| 1135 |
+
Training only on EVTE recovery trajectories taught <strong>debug prose</strong> — column dtype
|
| 1136 |
+
dumps, rambling — without improving answers. Mentor-hard: 40% vs SFT v1's 60%.
|
| 1137 |
+
</p>
|
| 1138 |
+
</div>
|
| 1139 |
+
</section>
|
| 1140 |
+
|
| 1141 |
+
<!-- 10 EVALS -->
|
| 1142 |
+
<section id="evals">
|
| 1143 |
+
<h2>14 · Evaluation results</h2>
|
| 1144 |
+
<p>
|
| 1145 |
+
<strong>Agent accuracy</strong> on real data files (lite DataBench parquet, DSBench Excel, mentor-hard pool).
|
| 1146 |
+
Macro average = unweighted mean across three benchmarks (30 problems). Always pair accuracy with
|
| 1147 |
+
<strong>exec_ok</strong> — base can match easy booleans via answer tags without running code.
|
| 1148 |
+
</p>
|
| 1149 |
+
|
| 1150 |
+
<figure class="figure">
|
| 1151 |
+
<img src="assets/illustrations/09-eval-benchmarks.png" alt="Three hackathon benchmarks across three models" loading="lazy" />
|
| 1152 |
+
<figcaption><strong>Fig 9 — Hackathon eval suite.</strong> DataBench (15) + DSBench Excel (10) + mentor-hard (5) per model on T4.</figcaption>
|
| 1153 |
+
</figure>
|
| 1154 |
+
|
| 1155 |
+
<h3>Hackathon benchmark suite — final (first complete run)</h3>
|
| 1156 |
+
<p>Parallel eval: <code>run_hackathon_benchmarks_parallel</code> · 3× T4 · June 2026.</p>
|
| 1157 |
+
<table>
|
| 1158 |
+
<thead>
|
| 1159 |
+
<tr><th>Model</th><th>DataBench (15)</th><th>DSBench (10)</th><th>Mentor-hard (5)</th><th>Macro avg</th><th>Total</th></tr>
|
| 1160 |
+
</thead>
|
| 1161 |
+
<tbody>
|
| 1162 |
+
<tr>
|
| 1163 |
+
<td>Base</td>
|
| 1164 |
+
<td class="num-mid">60.0%</td>
|
| 1165 |
+
<td class="num-bad">0.0%</td>
|
| 1166 |
+
<td class="num-mid">20.0%</td>
|
| 1167 |
+
<td class="num-mid">26.7%</td>
|
| 1168 |
+
<td>10/30</td>
|
| 1169 |
+
</tr>
|
| 1170 |
+
<tr>
|
| 1171 |
+
<td><strong>SFT v1</strong></td>
|
| 1172 |
+
<td class="num-good">86.7%</td>
|
| 1173 |
+
<td class="num-bad">0.0%</td>
|
| 1174 |
+
<td class="num-good">60.0%</td>
|
| 1175 |
+
<td class="num-good">48.9%</td>
|
| 1176 |
+
<td>16/30</td>
|
| 1177 |
+
</tr>
|
| 1178 |
+
<tr>
|
| 1179 |
+
<td>EVTE Micro-1</td>
|
| 1180 |
+
<td class="num-good">80.0%</td>
|
| 1181 |
+
<td class="num-bad">0.0%*</td>
|
| 1182 |
+
<td class="num-good">100.0%</td>
|
| 1183 |
+
<td class="num-good">60.0%</td>
|
| 1184 |
+
<td>17/30</td>
|
| 1185 |
+
</tr>
|
| 1186 |
+
</tbody>
|
| 1187 |
+
</table>
|
| 1188 |
+
<p style="font-size:0.9rem;color:var(--text-muted)">
|
| 1189 |
+
*DSBench official scorer = 0% for all models. Micro-1 Q15 computed <code>$12,829,511</code> = option <strong>A</strong> (correct) but was graded wrong because we compare letters not dollar values → value-aware DSBench would be 1/10 (macro <strong>63.3%</strong>).
|
| 1190 |
+
</p>
|
| 1191 |
+
|
| 1192 |
+
<h3>Earlier standalone evals (sanity checks)</h3>
|
| 1193 |
+
<table>
|
| 1194 |
+
<thead>
|
| 1195 |
+
<tr><th>Eval</th><th>Base</th><th>SFT v1</th><th>Micro-1 / SFT v2</th></tr>
|
| 1196 |
+
</thead>
|
| 1197 |
+
<tbody>
|
| 1198 |
+
<tr>
|
| 1199 |
+
<td>Quick DataBench (5)</td>
|
| 1200 |
+
<td>80% acc / 0% exec</td>
|
| 1201 |
+
<td class="num-good">80% / 100% exec</td>
|
| 1202 |
+
<td>SFT v2: 40%</td>
|
| 1203 |
+
</tr>
|
| 1204 |
+
<tr>
|
| 1205 |
+
<td>Mentor-hard (5)</td>
|
| 1206 |
+
<td>40% / 0% exec</td>
|
| 1207 |
+
<td class="num-good">60% / 100% exec</td>
|
| 1208 |
+
<td>Micro-1 replay: 100% (RAM); saved ckpt ~60%</td>
|
| 1209 |
+
</tr>
|
| 1210 |
+
</tbody>
|
| 1211 |
+
</table>
|
| 1212 |
+
|
| 1213 |
+
<div class="card">
|
| 1214 |
+
<div class="card-title">How to read DSBench</div>
|
| 1215 |
+
<p style="margin:0">
|
| 1216 |
+
Models often <strong>run code</strong> (50–100% exec_ok) but return dataframe strings, <code>0.0</code>, or dollar amounts that map to the <em>wrong</em> MCQ letter. Only one case (Micro-1 Q15) was a true scoring-format bug. DSBench 0% is mostly real Excel/parsing failure, not a broken metric.
|
| 1217 |
+
</p>
|
| 1218 |
+
</div>
|
| 1219 |
+
</section>
|
| 1220 |
+
|
| 1221 |
+
<!-- DEMO MODEL CHOICE -->
|
| 1222 |
+
<section id="demo-choice">
|
| 1223 |
+
<h2>15 · Why SFT v1 for the live demo (not Micro-1)</h2>
|
| 1224 |
+
<p>
|
| 1225 |
+
Micro-1 wins <strong>macro average</strong> (60% vs 48.9%) on paper — driven by a perfect 5/5 on mentor-hard.
|
| 1226 |
+
We still ship <strong>SFT v1</strong> on this Hugging Face Space. Here's why:
|
| 1227 |
+
</p>
|
| 1228 |
+
|
| 1229 |
+
<table>
|
| 1230 |
+
<thead>
|
| 1231 |
+
<tr><th>Factor</th><th>SFT v1</th><th>EVTE Micro-1</th></tr>
|
| 1232 |
+
</thead>
|
| 1233 |
+
<tbody>
|
| 1234 |
+
<tr>
|
| 1235 |
+
<td><strong>DataBench (breadth)</strong></td>
|
| 1236 |
+
<td class="num-good"><strong>86.7%</strong> — best on the largest held-out slice</td>
|
| 1237 |
+
<td>80.0%</td>
|
| 1238 |
+
</tr>
|
| 1239 |
+
<tr>
|
| 1240 |
+
<td><strong>Mentor-hard (depth)</strong></td>
|
| 1241 |
+
<td>60% (3/5), 100% exec</td>
|
| 1242 |
+
<td class="num-good"><strong>100%</strong> (5/5) on first complete run</td>
|
| 1243 |
+
</tr>
|
| 1244 |
+
<tr>
|
| 1245 |
+
<td><strong>Stability</strong></td>
|
| 1246 |
+
<td class="num-good">Single bulk SFT — predictable at inference</td>
|
| 1247 |
+
<td>Online micro-SFT batch 1 — replay 100% vs saved ckpt ~60%</td>
|
| 1248 |
+
</tr>
|
| 1249 |
+
<tr>
|
| 1250 |
+
<td><strong>Straggler reruns</strong></td>
|
| 1251 |
+
<td class="num-good">Held up when Modal overwrote volume</td>
|
| 1252 |
+
<td>Mentor-hard dropped to 60% on duplicate run</td>
|
| 1253 |
+
</tr>
|
| 1254 |
+
<tr>
|
| 1255 |
+
<td><strong>Live demo risk</strong></td>
|
| 1256 |
+
<td class="num-good">Lower — fewer debug ramble / dtype dumps</td>
|
| 1257 |
+
<td>Higher — tuned on hard pool, can overfit quirks</td>
|
| 1258 |
+
</tr>
|
| 1259 |
+
<tr>
|
| 1260 |
+
<td><strong>Story on slides</strong></td>
|
| 1261 |
+
<td>“Execution-grounded baseline that works”</td>
|
| 1262 |
+
<td>“EVTE-STaR peak — best hard-pool result”</td>
|
| 1263 |
+
</tr>
|
| 1264 |
+
</tbody>
|
| 1265 |
+
</table>
|
| 1266 |
+
|
| 1267 |
+
<div class="card highlight">
|
| 1268 |
+
<div class="card-title">Decision</div>
|
| 1269 |
+
<p style="margin:0">
|
| 1270 |
+
<strong>Gradio Space → SFT v1</strong> (<code>sanjaymalladi/DataSense-Modal-E2B-SFT</code>) for reliable live CSV demos.<br />
|
| 1271 |
+
<strong>Slides → show all three models</strong>; cite Micro-1 as evidence EVTE-STaR helps on the hard curated pool, not as the production default yet.
|
| 1272 |
+
</p>
|
| 1273 |
+
</div>
|
| 1274 |
+
</section>
|
| 1275 |
+
|
| 1276 |
+
<!-- 11 BENCHMARKS -->
|
| 1277 |
+
<section id="benchmarks">
|
| 1278 |
+
<h2>16 · Benchmark suite</h2>
|
| 1279 |
+
<table>
|
| 1280 |
+
<thead>
|
| 1281 |
+
<tr><th>Benchmark</th><th>Problems</th><th>What it tests</th><th>Status</th></tr>
|
| 1282 |
+
</thead>
|
| 1283 |
+
<tbody>
|
| 1284 |
+
<tr>
|
| 1285 |
+
<td><strong>DataBench test (lite)</strong></td>
|
| 1286 |
+
<td>15</td>
|
| 1287 |
+
<td>SemEval-style QA on real parquet samples</td>
|
| 1288 |
+
<td><span class="pill ok">integrated</span></td>
|
| 1289 |
+
</tr>
|
| 1290 |
+
<tr>
|
| 1291 |
+
<td><strong>DSBench analysis</strong></td>
|
| 1292 |
+
<td>10</td>
|
| 1293 |
+
<td>ModelOff Excel financial modeling</td>
|
| 1294 |
+
<td><span class="pill ok">integrated</span></td>
|
| 1295 |
+
</tr>
|
| 1296 |
+
<tr>
|
| 1297 |
+
<td><strong>Mentor-hard</strong></td>
|
| 1298 |
+
<td>5</td>
|
| 1299 |
+
<td>Curated EVTE failures</td>
|
| 1300 |
+
<td><span class="pill ok">integrated</span></td>
|
| 1301 |
+
</tr>
|
| 1302 |
+
</tbody>
|
| 1303 |
+
</table>
|
| 1304 |
+
</section>
|
| 1305 |
+
|
| 1306 |
+
<!-- 12 MODELS -->
|
| 1307 |
+
<section id="models">
|
| 1308 |
+
<h2>17 · Model checkpoints on Hugging Face</h2>
|
| 1309 |
+
<table>
|
| 1310 |
+
<thead>
|
| 1311 |
+
<tr><th>Checkpoint</th><th>HF repo</th><th>Role</th></tr>
|
| 1312 |
+
</thead>
|
| 1313 |
+
<tbody>
|
| 1314 |
+
<tr>
|
| 1315 |
+
<td>Base</td>
|
| 1316 |
+
<td><a href="https://huggingface.co/unsloth/gemma-4-E2B-it">unsloth/gemma-4-E2B-it</a></td>
|
| 1317 |
+
<td>Frozen foundation</td>
|
| 1318 |
+
</tr>
|
| 1319 |
+
<tr>
|
| 1320 |
+
<td><strong>SFT v1 ★ demo</strong></td>
|
| 1321 |
+
<td><a href="https://huggingface.co/sanjaymalladi/DataSense-Modal-E2B-SFT">DataSense-Modal-E2B-SFT</a></td>
|
| 1322 |
+
<td>Live HF Space adapter — stable execution</td>
|
| 1323 |
+
</tr>
|
| 1324 |
+
<tr>
|
| 1325 |
+
<td>EVTE-STaR Micro-1</td>
|
| 1326 |
+
<td><a href="https://huggingface.co/sanjaymalladi/DataSense-Modal-E2B-EVTE-Star-Micro1">DataSense-Modal-E2B-EVTE-Star-Micro1</a></td>
|
| 1327 |
+
<td>Best mentor-hard (5/5) — research checkpoint</td>
|
| 1328 |
+
</tr>
|
| 1329 |
+
</tbody>
|
| 1330 |
+
</table>
|
| 1331 |
+
</section>
|
| 1332 |
+
|
| 1333 |
+
<!-- 13 DEMO -->
|
| 1334 |
+
<section id="demo">
|
| 1335 |
+
<h2>18 · This Hugging Face demo</h2>
|
| 1336 |
+
<p>
|
| 1337 |
+
The Gradio app runs <strong>SFT v1</strong> — same agent loop as training eval: load CSV → multi-step
|
| 1338 |
+
code generation → sandbox execution → <strong>Answer</strong> + <strong>Summary</strong>.
|
| 1339 |
+
Six built-in examples cover sales, employees, and students datasets.
|
| 1340 |
+
</p>
|
| 1341 |
+
|
| 1342 |
+
<figure class="figure">
|
| 1343 |
+
<img src="assets/illustrations/03-agent-loop.png" alt="Agent loop used in the HF Space demo" loading="lazy" />
|
| 1344 |
+
<figcaption><strong>Same loop as eval.</strong> Upload this <code>hf_demo/</code> folder to a Gradio Space (GPU T4), set <code>HF_TOKEN</code> if needed.</figcaption>
|
| 1345 |
+
</figure>
|
| 1346 |
+
|
| 1347 |
+
<h3>Deploy checklist</h3>
|
| 1348 |
+
<ol>
|
| 1349 |
+
<li>Create Space (Gradio, <strong>gpu-t4</strong>) — see <code>README.md</code> frontmatter</li>
|
| 1350 |
+
<li>Upload <code>hf_demo/</code> including <code>assets/illustrations/</code> and <code>story.html</code></li>
|
| 1351 |
+
<li>Secret <code>HF_TOKEN</code> if adapter repo is private</li>
|
| 1352 |
+
<li>Smoke-test all 6 examples</li>
|
| 1353 |
+
</ol>
|
| 1354 |
+
<h3>Future work</h3>
|
| 1355 |
+
<ul>
|
| 1356 |
+
<li>DSBench MCQ letter mapping in scorer</li>
|
| 1357 |
+
<li>Per-micro-batch checkpointing during EVTE-STaR</li>
|
| 1358 |
+
<li>Optional Space variant with Micro-1 for hard-pool showcase</li>
|
| 1359 |
+
</ul>
|
| 1360 |
+
</section>
|
| 1361 |
+
|
| 1362 |
+
<footer>
|
| 1363 |
+
<p>
|
| 1364 |
+
<strong>DataSense E2B</strong> — Execution-verified, Tutor-escalation training for personal data science agents.<br />
|
| 1365 |
+
Code: <code>datasense_pipeline.py</code> · <code>datasense_evte.py</code> · <code>datasense_agent.py</code> · <code>hf_demo/</code><br />
|
| 1366 |
+
Built for the Gemma / DataBench hackathon, June 2026.
|
| 1367 |
+
</p>
|
| 1368 |
+
<p style="margin-top:2rem">
|
| 1369 |
+
<a href="/">← Back to Gradio demo</a> ·
|
| 1370 |
+
<a href="https://huggingface.co/sanjaymalladi/DataSense-Modal-E2B-SFT">SFT v1 on HF</a> ·
|
| 1371 |
+
<a href="https://huggingface.co/sanjaymalladi/DataSense-Modal-E2B-EVTE-Star-Micro1">Micro-1 on HF</a>
|
| 1372 |
+
</p>
|
| 1373 |
+
</footer>
|
| 1374 |
+
</div>
|
| 1375 |
+
</body>
|
| 1376 |
+
</html>
|