sanjaymalladi commited on
Commit
52674b8
·
verified ·
1 Parent(s): 55adc86

DataSense E2B hackathon demo - Gradio agent, story, eval assets

Browse files
.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: gray
5
- colorTo: indigo
6
  sdk: gradio
7
- sdk_version: 6.18.0
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
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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

  • SHA256: 2ebf662cb77d49bf1e205ebfa5d91a84d2751b6626aa7080d3081c5bdb408873
  • Pointer size: 132 Bytes
  • Size of remote file: 1 MB
assets/illustrations/02-fake-data-eval.png ADDED

Git LFS Details

  • SHA256: 72205a5c8a9e31e677cb0c0f1f4c89d711a089469a77cc5bdd8dc979aef62ffc
  • Pointer size: 132 Bytes
  • Size of remote file: 1.27 MB
assets/illustrations/03-agent-loop.png ADDED

Git LFS Details

  • SHA256: 3f3214b77f130662922c92ed993d2b982227ffd7d74203133d4801c94f75fecc
  • Pointer size: 132 Bytes
  • Size of remote file: 1.25 MB
assets/illustrations/04-pipeline-stages.png ADDED

Git LFS Details

  • SHA256: f7b66f1f10fdea6f8b76ace3907dd9898bd414b0dcd58e39da57556a9061eb9f
  • Pointer size: 132 Bytes
  • Size of remote file: 1.03 MB
assets/illustrations/05-evte-five-phases.png ADDED

Git LFS Details

  • SHA256: 4dc65a5d6011c1cf7edcbf263eebebde6445096dd2dcded95ba1778760b47dc9
  • Pointer size: 132 Bytes
  • Size of remote file: 1.2 MB
assets/illustrations/06-evte-self-recovery.png ADDED

Git LFS Details

  • SHA256: d48a48103d5493dbe3e60cf1ddc34771b1b34a9be0a414e98cb5ac2ccf1a359e
  • Pointer size: 132 Bytes
  • Size of remote file: 1.33 MB
assets/illustrations/07-evte-mentor-gate.png ADDED

Git LFS Details

  • SHA256: 26d125712a6da56630385354e9b8d852573cfa13d0124d7a8802d6d9ae00cc5f
  • Pointer size: 132 Bytes
  • Size of remote file: 1.19 MB
assets/illustrations/08-evte-star-online.png ADDED

Git LFS Details

  • SHA256: 4b2a593a47bb9fb753b16ce88114cce3ff1fd2228758bf4e2024e6939d570daf
  • Pointer size: 132 Bytes
  • Size of remote file: 1.21 MB
assets/illustrations/09-eval-benchmarks.png ADDED

Git LFS Details

  • SHA256: f3490ee6b2e2640fd6661c5303de47da2b3b49e96fd0526d0580d6d4b368b813
  • Pointer size: 132 Bytes
  • Size of remote file: 1.06 MB
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 &amp; motivation</a></li>
621
+ <li><a href="#evte-feedback">EVTE feedback loops (self-recovery)</a></li>
622
+ <li><a href="#evte-mentor">Mentor verify &amp; 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 &amp; 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 &amp; 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>&lt;result&gt;</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>&lt;result&gt;</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>&lt;result&gt;</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 &lt;result&gt; 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>&lt;result&gt;</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 &amp; 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 &amp; 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 &amp; 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> &nbsp;&nbsp;·&nbsp;&nbsp;
1370
+ <a href="https://huggingface.co/sanjaymalladi/DataSense-Modal-E2B-SFT">SFT v1 on HF</a> &nbsp;&nbsp;·&nbsp;&nbsp;
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>