sanjaymalladi commited on
Commit
9edae52
·
verified ·
1 Parent(s): 2d42b2a

Softer answer parsing, simplify UI, netlify story link

Browse files
Files changed (5) hide show
  1. README.md +2 -2
  2. agent.py +94 -10
  3. app.py +16 -16
  4. index.html +1368 -0
  5. index.zip +3 -0
README.md CHANGED
@@ -22,7 +22,7 @@ A **personal data-science agent** built for the Gemma / DataBench hackathon —
22
 
23
  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.
24
 
25
- 📖 **[Read the full project story →](file/story.html)**
26
 
27
  ---
28
 
@@ -106,4 +106,4 @@ Six one-click examples on **sales**, **employees**, and **students** CSVs — no
106
  ---
107
 
108
  **DataSense E2B** — Execution-verified, Tutor-escalation training for personal data science agents.
109
- Built June 2026 · Full narrative, illustrations, and `eval_hackathon_benchmarks.json` in [`story.html`](file/story.html).
 
22
 
23
  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.
24
 
25
+ 📖 **[Read the full project story →](https://datasense-e2b.netlify.app/)**
26
 
27
  ---
28
 
 
106
  ---
107
 
108
  **DataSense E2B** — Execution-verified, Tutor-escalation training for personal data science agents.
109
+ Built June 2026 · Full narrative at [datasense-e2b.netlify.app](https://datasense-e2b.netlify.app/).
agent.py CHANGED
@@ -158,9 +158,88 @@ def build_user_message(data_path: Path, task: str) -> str:
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:
@@ -205,6 +284,7 @@ def run_agent(
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):
@@ -215,16 +295,19 @@ def run_agent(
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 = ""
@@ -236,14 +319,15 @@ def run_agent(
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),
 
158
  return "\n".join(lines)
159
 
160
 
161
+ DONE_MARKERS = ("**Summary:**", "**Finding:**", "**Conclusion:**", "**Results:**")
162
+ FINISH_MARKERS = DONE_MARKERS + (
163
+ "**Answer:**",
164
+ "**ANSWER:**",
165
+ "Final Answer:",
166
+ "final answer:",
167
+ )
168
+
169
+ _GEMMA_TOKEN_RE = re.compile(r"<(?:start_of_turn|end_of_turn|turn)[^>]*>|<\|[^|]+\|>")
170
+ _THINK_RE = re.compile(r"<think>.*?</think>", re.DOTALL | re.IGNORECASE)
171
+
172
+
173
+ def _strip_model_noise(text: str) -> str:
174
+ text = _THINK_RE.sub("", text)
175
+ text = _GEMMA_TOKEN_RE.sub("", text)
176
+ return text.strip()
177
+
178
+
179
+ def _answer_from_stdout(stdout: str) -> str:
180
+ """Best-effort answer from verified execution output."""
181
+ if not stdout:
182
+ return ""
183
+ label_patterns = [
184
+ r"(?:Product|product) with highest (?:total )?revenue:\s*(.+)",
185
+ r"(?:Top product|top product)(?:\s+by revenue)?:\s*(.+)",
186
+ r"(?:The answer is|Answer|Result|Final answer):\s*(.+)",
187
+ r"(?:Maximum|Max) revenue:\s*([\d.,]+)",
188
+ ]
189
+ for line in stdout.splitlines():
190
+ line = line.strip()
191
+ if not line or line.startswith("Name:") or "dtype:" in line:
192
+ continue
193
+ for pat in label_patterns:
194
+ m = re.search(pat, line, re.IGNORECASE)
195
+ if m:
196
+ val = m.group(1).strip().strip(".")
197
+ if val and val.lower() not in ("nan", "none"):
198
+ return val
199
+ lines = [ln.strip() for ln in stdout.splitlines() if ln.strip() and "dtype:" not in ln]
200
+ return lines[-1] if lines else ""
201
+
202
+
203
+ def extract_answer(final_text: str, exec_outputs: list[str] | None = None) -> str:
204
+ """Parse answer: **Answer:** / Final Answer: → execution stdout → last line."""
205
+ exec_outputs = exec_outputs or []
206
+ cleaned = _strip_model_noise(final_text)
207
+
208
+ tag_patterns = [
209
+ r"\*\*Answer:\*\*\s*(.+?)(?:\n|$)",
210
+ r"\*\*ANSWER:\*\*\s*(.+?)(?:\n|$)",
211
+ r"Final Answer:\s*(.+?)(?:\n|$)",
212
+ r"final answer:\s*(.+?)(?:\n|$)",
213
+ ]
214
+ for pat in tag_patterns:
215
+ m = re.search(pat, cleaned, re.IGNORECASE)
216
+ if m:
217
+ ans = m.group(1).strip().strip("*").strip()
218
+ if ans and not ans.startswith("```"):
219
+ return ans
220
+
221
+ for stdout in reversed(exec_outputs):
222
+ from_exec = _answer_from_stdout(stdout)
223
+ if from_exec:
224
+ return from_exec
225
+
226
+ lines = [ln.strip() for ln in cleaned.splitlines() if ln.strip()]
227
+ if lines:
228
+ last = lines[-1]
229
+ if len(last) < 200 and not last.startswith("```"):
230
+ return last
231
+ return ""
232
+
233
+
234
+ def extract_summary(final_text: str) -> str:
235
+ cleaned = _strip_model_noise(final_text)
236
+ for prefix in ("**Summary:**", "**Finding:**", "**Conclusion:**", "**Results:**"):
237
+ if prefix in cleaned:
238
+ tail = cleaned.split(prefix, 1)[1].strip()
239
+ line = tail.split("\n")[0].strip()
240
+ if line:
241
+ return line[:1500]
242
+ return ""
243
 
244
 
245
  def generate_response(messages: list, model, tokenizer) -> str:
 
284
  context.add_user(build_user_message(dest, task))
285
 
286
  step_logs: list[str] = []
287
+ exec_outputs: list[str] = []
288
  final_text = ""
289
 
290
  for step in range(max_steps):
 
295
  context.add_assistant(response)
296
  final_text = response
297
 
298
+ preview = _strip_model_noise(response).replace("\n", " ")[:180]
299
  step_logs.append(f"### Step {step + 1}\n{preview}...\n")
300
 
301
+ if any(m in response for m in FINISH_MARKERS):
302
+ step_logs.append("✅ Agent finished.\n")
303
  break
304
 
305
  code_blocks = extract_code_blocks(response)
306
  if not code_blocks:
307
+ if exec_outputs:
308
+ step_logs.append("ℹ️ No more code — answer from execution output.\n")
309
+ else:
310
+ step_logs.append("ℹ️ No code block — stopping.\n")
311
  break
312
 
313
  result_str = ""
 
319
  timeout=AGENT_EXEC_TIMEOUT,
320
  )
321
  result_str = format_exec_result(result)
322
+ if result["success"] and result_str:
323
+ exec_outputs.append(result_str)
324
  status = "✅" if result["success"] else "❌"
325
  step_logs.append(f"{status} **Execution**\n```\n{result_str[:1200]}\n```\n")
326
 
327
  context.add_result(result_str)
328
 
329
+ answer = extract_answer(final_text, exec_outputs)
330
+ summary = extract_summary(final_text)
 
331
 
332
  return {
333
  "steps_markdown": "\n".join(step_logs),
app.py CHANGED
@@ -17,6 +17,7 @@ from examples import DEMO_DATASETS, DEMO_EXAMPLES
17
 
18
  MODEL, TOKENIZER = None, None
19
  MODEL_STATUS = "⏳ Model not loaded yet"
 
20
 
21
  CUSTOM_CSS = """
22
  @import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,600;1,6..72,400&display=swap');
@@ -147,7 +148,7 @@ def run_task(
147
  max_steps: int,
148
  progress=gr.Progress(),
149
  ):
150
- empty = ("", "", "", "")
151
  if not task.strip():
152
  return "⚠️ Enter a task question.", *empty
153
 
@@ -172,16 +173,18 @@ def run_task(
172
  except Exception as exc:
173
  return f"**Error:** {exc}", *empty
174
 
175
- answer_block = f"**Answer:** {result['answer']}" if result["answer"] else "_No **Answer:** tag found_"
176
- summary_block = result["summary"] or "_No summary_"
177
- status = f"✅ Done — `{data_path.name}` · {int(max_steps)} max steps"
 
 
 
 
178
 
179
  return (
180
  status,
181
  result["steps_markdown"],
182
  answer_block,
183
- summary_block,
184
- result["final_response"][:8000],
185
  )
186
 
187
 
@@ -204,11 +207,12 @@ def build_ui() -> gr.Blocks:
204
  gr.Markdown(
205
  """
206
  # DataSense E2B
207
- Writes Python, runs it on **your data**, reads real errors, returns verified **Answer** + **Summary**.
 
208
  """
209
  )
210
  gr.Markdown(
211
- f"📖 [Full project story](file/story.html) · LoRA [`DataSense-Modal-E2B-SFT`](https://huggingface.co/{ADAPTER_MODEL})",
212
  )
213
 
214
  model_status = gr.Markdown(MODEL_STATUS, elem_id="ds-status")
@@ -260,23 +264,19 @@ Writes Python, runs it on **your data**, reads real errors, returns verified **A
260
  )
261
 
262
  with gr.Column(scale=6):
263
- run_status = gr.Markdown("_Ready._", elem_id="ds-status")
264
  with gr.Tabs():
265
- with gr.Tab("🔍 Execution trace"):
266
- steps_out = gr.Markdown()
267
  with gr.Tab("✅ Answer"):
268
  answer_out = gr.Markdown()
269
- with gr.Tab("📝 Summary"):
270
- summary_out = gr.Markdown()
271
- with gr.Tab("🤖 Raw output"):
272
- raw_out = gr.Textbox(lines=18, max_lines=40)
273
 
274
  data_mode.change(_toggle_data_inputs, data_mode, [dataset, upload])
275
 
276
  run_btn.click(
277
  fn=run_task,
278
  inputs=[data_mode, dataset, upload, task, max_steps],
279
- outputs=[run_status, steps_out, answer_out, summary_out, raw_out],
280
  show_progress="full",
281
  ).then(
282
  fn=lambda: MODEL_STATUS,
 
17
 
18
  MODEL, TOKENIZER = None, None
19
  MODEL_STATUS = "⏳ Model not loaded yet"
20
+ STORY_URL = "https://datasense-e2b.netlify.app/"
21
 
22
  CUSTOM_CSS = """
23
  @import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,600;1,6..72,400&display=swap');
 
148
  max_steps: int,
149
  progress=gr.Progress(),
150
  ):
151
+ empty = ("", "")
152
  if not task.strip():
153
  return "⚠️ Enter a task question.", *empty
154
 
 
173
  except Exception as exc:
174
  return f"**Error:** {exc}", *empty
175
 
176
+ answer_block = f"## {result['answer']}" if result["answer"] else "_Could not parse an answer — check the execution trace._"
177
+ if result.get("summary"):
178
+ answer_block += f"\n\n{result['summary']}"
179
+ status = (
180
+ f"✅ **Live inference complete** — `{data_path.name}` · "
181
+ f"{int(max_steps)} max steps · real model + sandbox execution (not canned)"
182
+ )
183
 
184
  return (
185
  status,
186
  result["steps_markdown"],
187
  answer_block,
 
 
188
  )
189
 
190
 
 
207
  gr.Markdown(
208
  """
209
  # DataSense E2B
210
+ **Live inference** — Gemma-4 2B + SFT v1 writes Python, runs it on your CSV, reads real stdout/errors.
211
+ Not canned responses; each run is a fresh agent loop on GPU.
212
  """
213
  )
214
  gr.Markdown(
215
+ f"📖 [Full project story]({STORY_URL}) · LoRA [`DataSense-Modal-E2B-SFT`](https://huggingface.co/{ADAPTER_MODEL})",
216
  )
217
 
218
  model_status = gr.Markdown(MODEL_STATUS, elem_id="ds-status")
 
264
  )
265
 
266
  with gr.Column(scale=6):
267
+ run_status = gr.Markdown("_Ready — click Run to start live inference._", elem_id="ds-status")
268
  with gr.Tabs():
 
 
269
  with gr.Tab("✅ Answer"):
270
  answer_out = gr.Markdown()
271
+ with gr.Tab("🔍 Execution trace"):
272
+ steps_out = gr.Markdown()
 
 
273
 
274
  data_mode.change(_toggle_data_inputs, data_mode, [dataset, upload])
275
 
276
  run_btn.click(
277
  fn=run_task,
278
  inputs=[data_mode, dataset, upload, task, max_steps],
279
+ outputs=[run_status, answer_out, steps_out],
280
  show_progress="full",
281
  ).then(
282
  fn=lambda: MODEL_STATUS,
index.html ADDED
@@ -0,0 +1,1368 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ <h1>DataSense E2B<br />The Full Story</h1>
590
+ <p class="subtitle">
591
+ How we set out to build a <strong>personal data-science agent</strong> — not a chatbot that
592
+ <em>pretends</em> to run code, but one that <strong>writes Python, executes it, reads real errors,
593
+ and verifies answers</strong> — and what we learned training Gemma-4-2B on Modal with methods
594
+ we had to invent along the way.
595
+ </p>
596
+ <p class="meta">
597
+ Base: <code>unsloth/gemma-4-E2B-it</code><br />
598
+ Pipeline: Modal A100/T4<br />
599
+ Team: <strong>DataSense E2B</strong> (Execution-verified, Tutor-escalation)<br />
600
+ </p>
601
+ </header>
602
+
603
+ <nav class="toc" aria-label="Table of contents">
604
+ <h2>Index</h2>
605
+ <ol>
606
+ <li><a href="#goal">The goal</a></li>
607
+ <li><a href="#start">Where we started</a></li>
608
+ <li><a href="#problem">The problem with naive finetuning</a></li>
609
+ <li><a href="#agent">The DataSense agent loop</a></li>
610
+ <li><a href="#pipeline">Training pipeline: SFT → GRPO → DPO</a></li>
611
+ <li><a href="#methods">Supporting methods (verifiers, eval)</a></li>
612
+ <li><a href="#evte">EVTE — core idea &amp; motivation</a></li>
613
+ <li><a href="#evte-feedback">EVTE feedback loops (self-recovery)</a></li>
614
+ <li><a href="#evte-mentor">Mentor verify &amp; hint protocol</a></li>
615
+ <li><a href="#evte-star">EVTE-STaR — online micro-SFT</a></li>
616
+ <li><a href="#evte-outcomes">Episode outcomes &amp; trainability gates</a></li>
617
+ <li><a href="#worked">What worked</a></li>
618
+ <li><a href="#didnt">What didn't work</a></li>
619
+ <li><a href="#evals">Evaluation results</a></li>
620
+ <li><a href="#demo-choice">Why SFT v1 for the demo</a></li>
621
+ <li><a href="#benchmarks">Benchmark suite</a></li>
622
+ <li><a href="#models">Model checkpoints</a></li>
623
+ <li><a href="#demo">This demo &amp; what's next</a></li>
624
+ </ol>
625
+ </nav>
626
+
627
+ <!-- 01 GOAL -->
628
+ <section id="goal">
629
+ <h2>01 · The goal</h2>
630
+ <p>
631
+ The hackathon asked for something ambitious: take a small open model and make it genuinely useful
632
+ for <strong>data work</strong> — exploring tables, cleaning messy columns, aggregating, joining,
633
+ visualizing, and answering questions with <strong>verifiable correctness</strong>, not plausible prose.
634
+ </p>
635
+ <p>Our north star was simple to state and hard to achieve:</p>
636
+ <div class="card highlight">
637
+ <div class="card-title">North star</div>
638
+ <p style="margin:0">
639
+ A <strong>2B-parameter student agent</strong> that behaves like a junior data analyst:
640
+ inspect schema first, run focused code steps, debug from real tracebacks, and only claim an
641
+ answer after execution confirms it — with a training story credible enough for slides,
642
+ papers, and a public Hugging Face demo.
643
+ </p>
644
+ </div>
645
+ <p>Concretely, we targeted:</p>
646
+ <ul>
647
+ <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>
648
+ <li><strong>Multi-benchmark credibility</strong> — DataBench, DSBench Excel analysis, and a curated hard pool from our own training data</li>
649
+ <li><strong>A reproducible Modal pipeline</strong> — one app, volume checkpoints, automatic HF Hub pushes</li>
650
+ <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>
651
+ </ul>
652
+
653
+ <figure class="figure">
654
+ <img src="https://huggingface.co/spaces/build-small-hackathon/DataSense_E2B/resolve/main/assets/illustrations/01-goal-agent-vs-formatter.png" alt="Formatter that fakes answers versus a real execution-verified agent" loading="lazy" />
655
+ <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>
656
+ </figure>
657
+ </section>
658
+
659
+ <!-- 02 START -->
660
+ <section id="start">
661
+ <h2>02 · Where we started</h2>
662
+ <h3>The base model</h3>
663
+ <p>
664
+ We built on <code>unsloth/gemma-4-E2B-it</code> — Google's Gemma 4 2B instruction model in
665
+ Unsloth's E2B (execution-to-build) variant. It's small enough to fine-tune on a single GPU,
666
+ yet designed with code and tool use in mind. We used 4-bit quantization, LoRA rank 32,
667
+ and a 2048-token context throughout.
668
+ </p>
669
+
670
+ <h3>Three Kaggle notebooks → one Modal app</h3>
671
+ <p>
672
+ The project began as three separate Kaggle notebooks covering supervised fine-tuning (SFT),
673
+ GRPO reinforcement learning, and DPO preference optimization. We consolidated them into
674
+ <code>datasense_pipeline.py</code> — a single Modal application with shared config in
675
+ <code>datasense_utils.py</code> — so training could run unattended on cloud GPUs with
676
+ checkpoints persisted to a Modal volume and pushed to Hugging Face.
677
+ </p>
678
+
679
+ <h3>Nine bugs we fixed before trusting any number</h3>
680
+ <p>Early runs were misleading because the ported notebooks had latent bugs. We fixed all nine before building the pipeline:</p>
681
+ <table>
682
+ <thead>
683
+ <tr><th>#</th><th>Bug</th><th>Impact</th></tr>
684
+ </thead>
685
+ <tbody>
686
+ <tr><td>1</td><td><code>sft_warmup</code> KeyError</td><td>SFT wouldn't start</td></tr>
687
+ <tr><td>2</td><td><code>lora_target_modules</code> KeyError</td><td>LoRA attach failed</td></tr>
688
+ <tr><td>3</td><td><code>result_str</code> UnboundLocalError</td><td>Agent loop crashed mid-rollout</td></tr>
689
+ <tr><td>4</td><td>DPO pairs missing chat template prefix</td><td>Preference data malformed</td></tr>
690
+ <tr><td>5</td><td><code>skip_special_tokens=False</code></td><td>Decode pollution in rewards</td></tr>
691
+ <tr><td>6</td><td>Dead <code>oci_sft_v1</code> variable</td><td>Confusing / broken cells</td></tr>
692
+ <tr><td>7</td><td>GRPO <code>max_steps</code> hardcoded</td><td>Config ignored</td></tr>
693
+ <tr><td>8</td><td>Shorter <code>SYSTEM_PROMPT</code> in DPO cell</td><td>Train/eval prompt drift</td></tr>
694
+ <tr><td>9</td><td><code>_PROBLEM_LOOKUP</code> naming mismatch</td><td>Dataset indexing broken</td></tr>
695
+ </tbody>
696
+ </table>
697
+
698
+ <h3>Day-one eval: 0% accuracy (and why that was informative)</h3>
699
+ <p>
700
+ Our first agent eval reported <strong>0% accuracy</strong> for everyone — including SFT — while
701
+ SFT already showed <strong>100% execution success</strong> and ~5.6 agent steps vs base's 2% exec /
702
+ 1.1 steps. That gap taught us the first big lesson: <strong>the model was learning to run code,
703
+ but we weren't scoring against real data.</strong>
704
+ </p>
705
+ <div class="card warn">
706
+ <div class="card-title">Root cause</div>
707
+ <p style="margin:0">
708
+ Eval workspaces used <strong>synthetic random CSVs</strong> when DataBench parquet wasn't mounted,
709
+ but ground truth came from the <strong>real</strong> dataset. The agent analyzed fake data and
710
+ was graded against true answers — guaranteed 0%.
711
+ </p>
712
+ </div>
713
+
714
+ <figure class="figure">
715
+ <img src="https://huggingface.co/spaces/build-small-hackathon/DataSense_E2B/resolve/main/assets/illustrations/02-fake-data-eval.png" alt="Eval bug: synthetic workspace data scored against real ground truth" loading="lazy" />
716
+ <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>
717
+ </figure>
718
+ </section>
719
+
720
+ <!-- 03 PROBLEM -->
721
+ <section id="problem">
722
+ <h2>03 · The problem with naive finetuning</h2>
723
+ <p>
724
+ Most "data agent" demos finetune on static (question, code, answer) triples. The model learns
725
+ to <em>format</em> responses that look like an agent — <code> Answer: </code> tags, pandas snippets,
726
+ confident summaries — without ever closing the loop on execution.
727
+ </p>
728
+ <p>We observed three failure modes immediately:</p>
729
+ <div class="two-col">
730
+ <div class="card">
731
+ <div class="card-title">Formatter, not agent</div>
732
+ <p style="margin:0;font-size:0.95rem">
733
+ Base Gemma-4 could score well on easy boolean questions by emitting answer tags in a single
734
+ turn — <strong>0% code execution</strong> — beating SFT on accuracy while doing none of the work.
735
+ </p>
736
+ </div>
737
+ <div class="card">
738
+ <div class="card-title">Hallucinated execution</div>
739
+ <p style="margin:0;font-size:0.95rem">
740
+ Models invent <code>&lt;result&gt;</code> blocks with fake stdout. RL rewards on text alone
741
+ reinforce the illusion of competence.
742
+ </p>
743
+ </div>
744
+ </div>
745
+ <p>
746
+ The fix wasn't "more SFT data." It was changing <strong>what we optimize and measure</strong>:
747
+ real subprocess execution, multi-turn observe→fix→retry, and verifiers that compare parsed answers
748
+ to typed ground truth (boolean, number, category, list types).
749
+ </p>
750
+ </section>
751
+
752
+ <!-- 04 AGENT -->
753
+ <section id="agent">
754
+ <h2>04 · The DataSense agent loop</h2>
755
+ <p>Every training rollout and eval episode follows the same production-shaped loop:</p>
756
+ <div class="flow">
757
+ <span>THINK</span><span class="arrow">→</span>
758
+ <span>EXPLORE</span><span class="arrow">→</span>
759
+ <span>EXECUTE</span><span class="arrow">→</span>
760
+ <span>DEBUG</span><span class="arrow">→</span>
761
+ <span>ANSWER</span>
762
+ </div>
763
+ <ol>
764
+ <li><strong>THINK</strong> — inspect schema, dtypes, nulls before analysis</li>
765
+ <li><strong>EXPLORE</strong> — <code>head()</code>, <code>describe()</code>, small SQL <code>LIMIT</code> queries</li>
766
+ <li><strong>EXECUTE</strong> — one focused Python step; read real <code>&lt;result&gt;</code> from sandbox</li>
767
+ <li><strong>DEBUG</strong> — fix column names, joins, dtypes from tracebacks</li>
768
+ <li><strong>ANSWER</strong> — <code> Answer: </code> + <code> Summary: </code> after verified execution</li>
769
+ </ol>
770
+ <p>
771
+ The system prompt (shared across train, eval, and this HF demo) explicitly forbids hallucinated APIs
772
+ and requires the final printed value to match the answer tag. For DataBench we mount real
773
+ <code>sample.parquet</code> into the workspace; for DSBench we copy <code>.xlsx</code> workbooks
774
+ and use <code>inspect_source</code> for Excel structure.
775
+ </p>
776
+ <pre>Reward signal (simplified):
777
+ + execution actually ran
778
+ + stdout parseable
779
+ + answer matches ground truth (typed comparator)
780
+ − hallucinated inline &lt;result&gt; without [EXEC:real]
781
+ − debug rambling / column dumps as "answers"</pre>
782
+
783
+ <figure class="figure">
784
+ <img src="https://huggingface.co/spaces/build-small-hackathon/DataSense_E2B/resolve/main/assets/illustrations/03-agent-loop.png" alt="THINK EXPLORE EXECUTE DEBUG ANSWER agent loop" loading="lazy" />
785
+ <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>
786
+ </figure>
787
+ </section>
788
+
789
+ <!-- 05 PIPELINE -->
790
+ <section id="pipeline">
791
+ <h2>05 · Training pipeline: SFT → GRPO → DPO</h2>
792
+ <p>Our planned stack mirrors modern agent training — with execution at every stage:</p>
793
+ <div class="flow">
794
+ <span>SFT</span><span class="arrow">→</span>
795
+ <span>GRPO</span><span class="arrow">→</span>
796
+ <span>DPO</span><span class="arrow">→</span>
797
+ <span>Eval</span>
798
+ </div>
799
+
800
+ <h3>Stage 1 — Supervised fine-tuning (SFT v1) ✅</h3>
801
+ <p>
802
+ Bulk SFT on DataBench-style traces plus agent supplements: multi-turn dialogs, Jupyter-agent
803
+ traces, dashboard examples, and code-feedback execution pairs. This produced our strongest
804
+ baseline — <code>sanjaymalladi/DataSense-Modal-E2B-SFT</code>.
805
+ </p>
806
+ <ul>
807
+ <li>LoRA r=32, α=64 on all attention + MLP projections</li>
808
+ <li>~600 max steps, effective batch 8</li>
809
+ <li>Teaches the model to <em>use</em> the agent format and run multi-step code</li>
810
+ </ul>
811
+
812
+ <h3>Stage 2 — GRPO (execution-grounded RL) ⚠️ partial</h3>
813
+ <p>
814
+ Group Relative Policy Optimization with <strong>real Python rollouts</strong> per prompt.
815
+ Each step spawns multiple agent trajectories; rewards use <code>compute_trajectory_reward()</code>
816
+ with <code>require_real_execution=True</code>.
817
+ </p>
818
+ <p>
819
+ GRPO on Gemma-4 is brutally slow (~11 min/step on A100) because most wall time is
820
+ <strong>CPU-bound execution</strong>, not GPU matmul — 4 rollouts × up to 5 agent steps ×
821
+ subprocess sandboxing. We fixed trajectory forwarding bugs, KL instability
822
+ (<code>final_logit_softcapping=30</code>), and added parallel rollout workers — but full
823
+ 300-step GRPO remained impractical within hackathon time. A shortened 100-step run was targeted.
824
+ </p>
825
+
826
+ <h3>Stage 3 — DPO ⏸️ deferred</h3>
827
+ <p>
828
+ Preference pairs from high vs low reward rollouts (min gap 0.15) — planned but deprioritized
829
+ once EVTE-STaR showed more promise for hard-question gains within our compute budget.
830
+ </p>
831
+
832
+ <figure class="figure">
833
+ <img src="https://huggingface.co/spaces/build-small-hackathon/DataSense_E2B/resolve/main/assets/illustrations/04-pipeline-stages.png" alt="SFT GRPO DPO training pipeline stages" loading="lazy" />
834
+ <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>
835
+ </figure>
836
+ </section>
837
+
838
+ <!-- 06 METHODS (supporting) -->
839
+ <section id="methods">
840
+ <h2>06 · Supporting infrastructure (not EVTE itself)</h2>
841
+ <p>
842
+ Before EVTE could work, we needed execution-grounded rollouts, typed verifiers, and honest eval.
843
+ These are the plumbing; the novel research contribution is EVTE + EVTE-STaR (sections 07–11 below).
844
+ </p>
845
+
846
+ <h3>Execution-grounded rollouts</h3>
847
+ <p>
848
+ Every GRPO/DPO/EVTE trajectory runs code in an isolated workspace. Rewards ignore fake
849
+ <code>&lt;result&gt;</code> tags unless tagged <code>[EXEC:real]</code>.
850
+ </p>
851
+
852
+ <h3>Typed answer verification (<code>databench_compare</code> + neural verifier)</h3>
853
+ <p>
854
+ Evidence-bound scoring chain: exec stdout → <code> Answer: </code> tag → LLM extract → typed compare
855
+ (boolean, float, category, <code>list[category]</code>, <code>list[number]</code>).
856
+ Without this, mentors "fail" when extraction fails, not when reasoning fails.
857
+ </p>
858
+
859
+ <h3>Lite eval &amp; hackathon harness</h3>
860
+ <p>
861
+ DataBench lite scores against <code>sample_answer</code> on mounted parquet.
862
+ <code>run_hackathon_benchmarks_parallel</code> runs Base / SFT / Micro-1 across three benchmarks on T4.
863
+ </p>
864
+ </section>
865
+
866
+ <!-- 07 EVTE CORE -->
867
+ <section id="evte">
868
+ <h2>07 · EVTE — Execution-Verified Tutor Escalation</h2>
869
+ <p>
870
+ <strong>EVTE</strong> is the method we built when classical distillation and STaR broke down for
871
+ data agents. The name encodes three commitments:
872
+ </p>
873
+ <ul>
874
+ <li><strong>Execution</strong> — every claim of success must be backed by real code that ran on real files</li>
875
+ <li><strong>Verified</strong> — student <em>and</em> mentor answers pass the same typed verifier</li>
876
+ <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>
877
+ </ul>
878
+
879
+ <h3>Why we needed EVTE</h3>
880
+ <p>
881
+ Classical <strong>STaR</strong> (Self-Taught Reasoner) assumes a strong teacher can produce correct
882
+ reasoning chains, filter them, and fine-tune the student offline. That fails for DataSense because:
883
+ </p>
884
+ <ol>
885
+ <li>Our <strong>2B student</strong> often can't solve list/category questions at all</li>
886
+ <li>Our <strong>31B mentor</strong> also fails verification on the hardest 5 problems (~40% mentor-hard pool)</li>
887
+ <li>Even when code is <em>right</em>, <strong>answer extraction</strong> fails (no tag, wrong stdout parse)</li>
888
+ <li>Distilling final answers teaches <strong>memorization</strong>; we need debugging under execution constraints</li>
889
+ </ol>
890
+
891
+ <h3>The five-phase episode (EVTE and EVTE-STaR share this skeleton)</h3>
892
+ <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>
893
+
894
+ <div class="phase-grid">
895
+ <div class="phase-card">
896
+ <h4>Phase 1 · Student first attempt</h4>
897
+ <p>2B student, up to 5 agent steps, real workspace (CSV/parquet/xlsx). Scored via <code>score_rollout()</code>.</p>
898
+ </div>
899
+ <div class="phase-card">
900
+ <h4>Phase 2 · Self-recovery feedback</h4>
901
+ <p>Up to 3 rounds of <code>build_self_recovery_feedback()</code> — real tracebacks, answer withheld.</p>
902
+ </div>
903
+ <div class="phase-card">
904
+ <h4>Phase 3 · Mentor independent verify</h4>
905
+ <p>31B mentor solves in a <em>fresh</em> workspace; must pass the same verifier before any hint.</p>
906
+ </div>
907
+ <div class="phase-card">
908
+ <h4>Phase 4 · Diagnostic mentor hint</h4>
909
+ <p><code>generate_mentor_hint()</code> under <code>MENTOR_HINT_SYSTEM</code> — no final answer, no full script.</p>
910
+ </div>
911
+ <div class="phase-card">
912
+ <h4>Phase 5 · Post-hint student</h4>
913
+ <p>Up to 2 attempts × 5 steps. Episode saved only if student verifies after reading the hint.</p>
914
+ </div>
915
+ </div>
916
+
917
+ <figure class="figure">
918
+ <img src="https://huggingface.co/spaces/build-small-hackathon/DataSense_E2B/resolve/main/assets/illustrations/05-evte-five-phases.png" alt="EVTE five phases from student attempt to mentor-assisted success" loading="lazy" />
919
+ <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>
920
+ </figure>
921
+
922
+ <pre>run_evte_star_episode (simplified control flow):
923
+
924
+ student_rollout = phase_1_student()
925
+ if clean_first_try_verified and not messy_recovery_in_trace:
926
+ return SKIP # already knows it — not trainable in STaR mode
927
+
928
+ if not verified:
929
+ for i in 1..3:
930
+ add_user(build_self_recovery_feedback()) # ← EVTE feedback
931
+ student_rollout = student_retry()
932
+
933
+ mentor_ok, mentor_rollout = mentor_verify_solution(
934
+ student_rollout=junior_trace # mentor sees failed code
935
+ )
936
+ if not mentor_ok:
937
+ return DISCARD # mentor_unverified — no training signal
938
+
939
+ hint = generate_mentor_hint(student_rollout, mentor_rollout)
940
+ add_user("[MENTOR] " + hint) # diagnostic only
941
+
942
+ for j in 1..2:
943
+ student_rollout = student_retry()
944
+ if verified:
945
+ return SAVE_TRAINABLE_EPISODE # mentor_assisted</pre>
946
+
947
+ <h3>Hard-first curriculum</h3>
948
+ <p>
949
+ <code>_prioritize_evte_problems()</code> sorts <code>list[category]</code>, <code>list[number]</code>,
950
+ and multi-answer types before easy booleans. EVTE compute is expensive (two models × multi-step agents);
951
+ we spend it where SFT v1 plateaus.
952
+ </p>
953
+
954
+ <h3>Mentor hardware choreography</h3>
955
+ <p>
956
+ Student (2B) and mentor (31B) don't fit comfortably together on one A100. The STaR loop uses
957
+ <code>on_micro_batch</code> hooks to <strong>unload mentor → micro-SFT student → reload mentor</strong>
958
+ every 15 episodes. Progress persists to <code>evte_star_progress.json</code> with resume support.
959
+ </p>
960
+ </section>
961
+
962
+ <!-- 08 EVTE FEEDBACK -->
963
+ <section id="evte-feedback">
964
+ <h2>08 · EVTE feedback — self-recovery without answer leakage</h2>
965
+ <p>
966
+ The most underrated piece of EVTE is not the mentor — it's <strong>what we put in the user turn
967
+ when the student fails</strong>. This is <code>build_self_recovery_feedback()</code> in
968
+ <code>datasense_evte.py</code>.
969
+ </p>
970
+
971
+ <figure class="figure">
972
+ <img src="https://huggingface.co/spaces/build-small-hackathon/DataSense_E2B/resolve/main/assets/illustrations/06-evte-self-recovery.png" alt="Self-recovery feedback loop with real errors but hidden ground truth" loading="lazy" />
973
+ <figcaption><strong>Fig 6 — Self-recovery feedback.</strong> The student sees wrong predictions, last code, and real tracebacks — never the correct answer.</figcaption>
974
+ </figure>
975
+
976
+ <blockquote class="pull">
977
+ Messy success = verified answer but conversation contains debug/recovery language
978
+ (<code>trajectory_has_recovery_signal()</code>). We don't want to reinforce "stumble into correctness"
979
+ without tutor review in STaR mode.
980
+ </blockquote>
981
+
982
+ <h3>Why SFT v2 failed — feedback without balance</h3>
983
+ <p>
984
+ When we later fine-tuned <strong>only</strong> on recovery trajectories (SFT v2), the model learned
985
+ the <em>shape</em> of debug prose — dtype dumps, column lists — without improving verified answers.
986
+ Lesson: self-recovery feedback is essential <strong>during collection</strong>, but training must mix
987
+ clean completions with mentor-assisted wins, not recovery-only soup.
988
+ </p>
989
+ </section>
990
+
991
+ <!-- 09 EVTE MENTOR -->
992
+ <section id="evte-mentor">
993
+ <h2>09 · Mentor verify &amp; hint protocol</h2>
994
+ <p>
995
+ The mentor is <code>google/gemma-4-31B-it</code> (4-bit via Unsloth). It is <strong>not</strong> an oracle
996
+ that whispers answers. It must earn the right to hint by passing the same execution verifier as the student.
997
+ </p>
998
+
999
+ <figure class="figure">
1000
+ <img src="https://huggingface.co/spaces/build-small-hackathon/DataSense_E2B/resolve/main/assets/illustrations/07-evte-mentor-gate.png" alt="Mentor must pass verification gate before giving a diagnostic hint" loading="lazy" />
1001
+ <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>
1002
+ </figure>
1003
+
1004
+ <h3>Mentor retry modes</h3>
1005
+ <table>
1006
+ <thead>
1007
+ <tr><th>Mode</th><th>Behavior</th><th>Config</th></tr>
1008
+ </thead>
1009
+ <tbody>
1010
+ <tr>
1011
+ <td><strong>series</strong></td>
1012
+ <td>Same conversation; temps ramp 0.4 → 0.65 → 0.85</td>
1013
+ <td><code>evte_mentor_retry_mode=series</code></td>
1014
+ </tr>
1015
+ <tr>
1016
+ <td><strong>parallel</strong></td>
1017
+ <td>3 independent workspaces; first verified wins; temps [0.2, 0.5, 0.7]</td>
1018
+ <td><code>evte_mentor_retry_mode=parallel</code></td>
1019
+ </tr>
1020
+ </tbody>
1021
+ </table>
1022
+
1023
+ </section>
1024
+
1025
+ <!-- 10 EVTE-STAR -->
1026
+ <section id="evte-star">
1027
+ <h2>10 · EVTE-STaR — online Self-Taught Reasoner with micro-SFT</h2>
1028
+ <p>
1029
+ <strong>EVTE-STaR</strong> combines EVTE episode collection with <strong>online weight updates</strong>.
1030
+ Classical STaR: collect all successes → train offline once. EVTE-STaR:
1031
+ <strong>collect 15 verified mentor-assisted wins → micro-SFT 30 steps → student is slightly better → repeat.</strong>
1032
+ </p>
1033
+
1034
+ <figure class="figure">
1035
+ <img src="https://huggingface.co/spaces/build-small-hackathon/DataSense_E2B/resolve/main/assets/illustrations/08-evte-star-online.png" alt="EVTE-STaR online micro-SFT every 15 verified episodes" loading="lazy" />
1036
+ <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>
1037
+ </figure>
1038
+
1039
+ <h3>The overtraining curve (batches 2–3 vs batch 6)</h3>
1040
+ <p>
1041
+ Micro-batch <strong>1</strong> replay in RAM scored <strong>100%</strong> on mentor-hard (5 problems).
1042
+ Saved Micro-1 checkpoint: ~<strong>60%</strong> confirmatory. Replay of batches <strong>2–3</strong>:
1043
+ ~<strong>80%</strong>. Final batch <strong>6</strong> checkpoint: ~<strong>40%</strong> — worse than SFT v1.
1044
+ </p>
1045
+ <div class="card warn">
1046
+ <div class="card-title">Lesson</div>
1047
+ <p style="margin:0">
1048
+ Online micro-SFT needs <strong>early stopping on a held-out hard set</strong>, not "more batches = better."
1049
+ We only preserved micro-1 and final checkpoints on the volume — sweet-spot batches 2–3 were lost
1050
+ until <code>run_micro_replay_eval</code> reconstructed them in RAM.
1051
+ </p>
1052
+ </div>
1053
+ </section>
1054
+
1055
+ <!-- 11 EVTE OUTCOMES -->
1056
+ <section id="evte-outcomes">
1057
+ <h2>11 · Episode outcomes &amp; trainability gates</h2>
1058
+ <p>Every episode ends in exactly one outcome. The outcome determines whether it enters training.</p>
1059
+
1060
+ <table>
1061
+ <thead>
1062
+ <tr><th>Outcome</th><th>Meaning</th><th>EVTE-STaR: train?</th></tr>
1063
+ </thead>
1064
+ <tbody>
1065
+ <tr>
1066
+ <td><code>self_solved_clean</code></td>
1067
+ <td>First-try verified, no recovery signals in trace</td>
1068
+ <td class="num-bad">Skip</td>
1069
+ </tr>
1070
+ <tr>
1071
+ <td><code>self_recovered</code></td>
1072
+ <td>Fixed via self-recovery feedback only</td>
1073
+ <td class="num-mid">Optional</td>
1074
+ </tr>
1075
+ <tr>
1076
+ <td><code>mentor_assisted</code></td>
1077
+ <td>Failed → mentor verified → hint → student verified</td>
1078
+ <td class="num-good">Yes</td>
1079
+ </tr>
1080
+ <tr>
1081
+ <td><code>discarded</code></td>
1082
+ <td>Mentor couldn't pass execution verifier</td>
1083
+ <td class="num-bad">No</td>
1084
+ </tr>
1085
+ </tbody>
1086
+ </table>
1087
+ </section>
1088
+
1089
+ <!-- 12 WORKED -->
1090
+ <section id="worked">
1091
+ <h2>12 · What worked</h2>
1092
+
1093
+ <div class="card">
1094
+ <h4>✅ SFT v1 — real execution behavior</h4>
1095
+ <p style="margin:0.5rem 0 0">
1096
+ SFT v1 consistently runs real Python (100% exec on many evals), uses ~4–5 agent steps, and
1097
+ beats base on hard questions where base "wins" without code. This is the behavioral foundation
1098
+ everything else builds on.
1099
+ </p>
1100
+ </div>
1101
+
1102
+ <div class="card">
1103
+ <h4>✅ EVTE episode quality filter</h4>
1104
+ <p style="margin:0.5rem 0 0">
1105
+ Saving only mentor-assisted verified trajectories produced high-signal data — multi-turn debug
1106
+ with real errors, not synthetic Q/A. 92 episodes is small but <em>curated</em>.
1107
+ </p>
1108
+ </div>
1109
+ </section>
1110
+
1111
+ <!-- 09 DIDNT -->
1112
+ <section id="didnt">
1113
+ <h2>13 · What didn't work</h2>
1114
+
1115
+ <div class="card danger">
1116
+ <h4>❌ Full GRPO within hackathon time</h4>
1117
+ <p style="margin:0.5rem 0 0">
1118
+ ~11 min/step × hundreds of steps × execution-bound rollouts ≈ multi-day runs. Parallel rollout
1119
+ workers helped but couldn't change the fundamental CPU/GPU pipeline stall. vLLM isn't available
1120
+ for Gemma 4 E2B, so generation stays on HF generate.
1121
+ </p>
1122
+ </div>
1123
+
1124
+ <div class="card danger">
1125
+ <h4>❌ SFT v2 (recovery-only fine-tune)</h4>
1126
+ <p style="margin:0.5rem 0 0">
1127
+ Training only on EVTE recovery trajectories taught <strong>debug prose</strong> — column dtype
1128
+ dumps, rambling — without improving answers. Mentor-hard: 40% vs SFT v1's 60%.
1129
+ </p>
1130
+ </div>
1131
+ </section>
1132
+
1133
+ <!-- 10 EVALS -->
1134
+ <section id="evals">
1135
+ <h2>14 · Evaluation results</h2>
1136
+ <p>
1137
+ <strong>Agent accuracy</strong> on real data files (lite DataBench parquet, DSBench Excel, mentor-hard pool).
1138
+ Macro average = unweighted mean across three benchmarks (30 problems). Always pair accuracy with
1139
+ <strong>exec_ok</strong> — base can match easy booleans via answer tags without running code.
1140
+ </p>
1141
+
1142
+ <figure class="figure">
1143
+ <img src="https://huggingface.co/spaces/build-small-hackathon/DataSense_E2B/resolve/main/assets/illustrations/09-eval-benchmarks.png" alt="Three hackathon benchmarks across three models" loading="lazy" />
1144
+ <figcaption><strong>Fig 9 — Hackathon eval suite.</strong> DataBench (15) + DSBench Excel (10) + mentor-hard (5) per model on T4.</figcaption>
1145
+ </figure>
1146
+
1147
+ <h3>Hackathon benchmark suite — final (first complete run)</h3>
1148
+ <p>Parallel eval: <code>run_hackathon_benchmarks_parallel</code> · 3× T4 · June 2026.</p>
1149
+ <table>
1150
+ <thead>
1151
+ <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>
1152
+ </thead>
1153
+ <tbody>
1154
+ <tr>
1155
+ <td>Base</td>
1156
+ <td class="num-mid">60.0%</td>
1157
+ <td class="num-bad">0.0%</td>
1158
+ <td class="num-mid">20.0%</td>
1159
+ <td class="num-mid">26.7%</td>
1160
+ <td>10/30</td>
1161
+ </tr>
1162
+ <tr>
1163
+ <td><strong>SFT v1</strong></td>
1164
+ <td class="num-good">86.7%</td>
1165
+ <td class="num-bad">0.0%</td>
1166
+ <td class="num-good">60.0%</td>
1167
+ <td class="num-good">48.9%</td>
1168
+ <td>16/30</td>
1169
+ </tr>
1170
+ <tr>
1171
+ <td>EVTE Micro-1</td>
1172
+ <td class="num-good">80.0%</td>
1173
+ <td class="num-bad">0.0%*</td>
1174
+ <td class="num-good">100.0%</td>
1175
+ <td class="num-good">60.0%</td>
1176
+ <td>17/30</td>
1177
+ </tr>
1178
+ </tbody>
1179
+ </table>
1180
+ <p style="font-size:0.9rem;color:var(--text-muted)">
1181
+ *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>).
1182
+ </p>
1183
+
1184
+ <h3>Earlier standalone evals (sanity checks)</h3>
1185
+ <table>
1186
+ <thead>
1187
+ <tr><th>Eval</th><th>Base</th><th>SFT v1</th><th>Micro-1 / SFT v2</th></tr>
1188
+ </thead>
1189
+ <tbody>
1190
+ <tr>
1191
+ <td>Quick DataBench (5)</td>
1192
+ <td>80% acc / 0% exec</td>
1193
+ <td class="num-good">80% / 100% exec</td>
1194
+ <td>SFT v2: 40%</td>
1195
+ </tr>
1196
+ <tr>
1197
+ <td>Mentor-hard (5)</td>
1198
+ <td>40% / 0% exec</td>
1199
+ <td class="num-good">60% / 100% exec</td>
1200
+ <td>Micro-1 replay: 100% (RAM); saved ckpt ~60%</td>
1201
+ </tr>
1202
+ </tbody>
1203
+ </table>
1204
+
1205
+ <div class="card">
1206
+ <div class="card-title">How to read DSBench</div>
1207
+ <p style="margin:0">
1208
+ 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.
1209
+ </p>
1210
+ </div>
1211
+ </section>
1212
+
1213
+ <!-- DEMO MODEL CHOICE -->
1214
+ <section id="demo-choice">
1215
+ <h2>15 · Why SFT v1 for the live demo (not Micro-1)</h2>
1216
+ <p>
1217
+ Micro-1 wins <strong>macro average</strong> (60% vs 48.9%) on paper — driven by a perfect 5/5 on mentor-hard.
1218
+ We still ship <strong>SFT v1</strong> on this Hugging Face Space. Here's why:
1219
+ </p>
1220
+
1221
+ <table>
1222
+ <thead>
1223
+ <tr><th>Factor</th><th>SFT v1</th><th>EVTE Micro-1</th></tr>
1224
+ </thead>
1225
+ <tbody>
1226
+ <tr>
1227
+ <td><strong>DataBench (breadth)</strong></td>
1228
+ <td class="num-good"><strong>86.7%</strong> — best on the largest held-out slice</td>
1229
+ <td>80.0%</td>
1230
+ </tr>
1231
+ <tr>
1232
+ <td><strong>Mentor-hard (depth)</strong></td>
1233
+ <td>60% (3/5), 100% exec</td>
1234
+ <td class="num-good"><strong>100%</strong> (5/5) on first complete run</td>
1235
+ </tr>
1236
+ <tr>
1237
+ <td><strong>Stability</strong></td>
1238
+ <td class="num-good">Single bulk SFT — predictable at inference</td>
1239
+ <td>Online micro-SFT batch 1 — replay 100% vs saved ckpt ~60%</td>
1240
+ </tr>
1241
+ <tr>
1242
+ <td><strong>Straggler reruns</strong></td>
1243
+ <td class="num-good">Held up when Modal overwrote volume</td>
1244
+ <td>Mentor-hard dropped to 60% on duplicate run</td>
1245
+ </tr>
1246
+ <tr>
1247
+ <td><strong>Live demo risk</strong></td>
1248
+ <td class="num-good">Lower — fewer debug ramble / dtype dumps</td>
1249
+ <td>Higher — tuned on hard pool, can overfit quirks</td>
1250
+ </tr>
1251
+ <tr>
1252
+ <td><strong>Story on slides</strong></td>
1253
+ <td>“Execution-grounded baseline that works”</td>
1254
+ <td>“EVTE-STaR peak — best hard-pool result”</td>
1255
+ </tr>
1256
+ </tbody>
1257
+ </table>
1258
+
1259
+ <div class="card highlight">
1260
+ <div class="card-title">Decision</div>
1261
+ <p style="margin:0">
1262
+ <strong>Gradio Space → SFT v1</strong> (<code>sanjaymalladi/DataSense-Modal-E2B-SFT</code>) for reliable live CSV demos.<br />
1263
+ <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.
1264
+ </p>
1265
+ </div>
1266
+ </section>
1267
+
1268
+ <!-- 11 BENCHMARKS -->
1269
+ <section id="benchmarks">
1270
+ <h2>16 · Benchmark suite</h2>
1271
+ <table>
1272
+ <thead>
1273
+ <tr><th>Benchmark</th><th>Problems</th><th>What it tests</th><th>Status</th></tr>
1274
+ </thead>
1275
+ <tbody>
1276
+ <tr>
1277
+ <td><strong>DataBench test (lite)</strong></td>
1278
+ <td>15</td>
1279
+ <td>SemEval-style QA on real parquet samples</td>
1280
+ <td><span class="pill ok">integrated</span></td>
1281
+ </tr>
1282
+ <tr>
1283
+ <td><strong>DSBench analysis</strong></td>
1284
+ <td>10</td>
1285
+ <td>ModelOff Excel financial modeling</td>
1286
+ <td><span class="pill ok">integrated</span></td>
1287
+ </tr>
1288
+ <tr>
1289
+ <td><strong>Mentor-hard</strong></td>
1290
+ <td>5</td>
1291
+ <td>Curated EVTE failures</td>
1292
+ <td><span class="pill ok">integrated</span></td>
1293
+ </tr>
1294
+ </tbody>
1295
+ </table>
1296
+ </section>
1297
+
1298
+ <!-- 12 MODELS -->
1299
+ <section id="models">
1300
+ <h2>17 · Model checkpoints on Hugging Face</h2>
1301
+ <table>
1302
+ <thead>
1303
+ <tr><th>Checkpoint</th><th>HF repo</th><th>Role</th></tr>
1304
+ </thead>
1305
+ <tbody>
1306
+ <tr>
1307
+ <td>Base</td>
1308
+ <td><a href="https://huggingface.co/unsloth/gemma-4-E2B-it">unsloth/gemma-4-E2B-it</a></td>
1309
+ <td>Frozen foundation</td>
1310
+ </tr>
1311
+ <tr>
1312
+ <td><strong>SFT v1 ★ demo</strong></td>
1313
+ <td><a href="https://huggingface.co/sanjaymalladi/DataSense-Modal-E2B-SFT">DataSense-Modal-E2B-SFT</a></td>
1314
+ <td>Live HF Space adapter — stable execution</td>
1315
+ </tr>
1316
+ <tr>
1317
+ <td>EVTE-STaR Micro-1</td>
1318
+ <td><a href="https://huggingface.co/sanjaymalladi/DataSense-Modal-E2B-EVTE-Star-Micro1">DataSense-Modal-E2B-EVTE-Star-Micro1</a></td>
1319
+ <td>Best mentor-hard (5/5) — research checkpoint</td>
1320
+ </tr>
1321
+ </tbody>
1322
+ </table>
1323
+ </section>
1324
+
1325
+ <!-- 13 DEMO -->
1326
+ <section id="demo">
1327
+ <h2>18 · This Hugging Face demo</h2>
1328
+ <p>
1329
+ The Gradio app runs <strong>SFT v1</strong> — same agent loop as training eval: load CSV → multi-step
1330
+ code generation → sandbox execution → <strong>Answer</strong> + <strong>Summary</strong>.
1331
+ Six built-in examples cover sales, employees, and students datasets.
1332
+ </p>
1333
+
1334
+ <figure class="figure">
1335
+ <img src="https://huggingface.co/spaces/build-small-hackathon/DataSense_E2B/resolve/main/assets/illustrations/03-agent-loop.png" alt="Agent loop used in the HF Space demo" loading="lazy" />
1336
+ <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>
1337
+ </figure>
1338
+
1339
+ <h3>Deploy checklist</h3>
1340
+ <ol>
1341
+ <li>Create Space (Gradio, <strong>gpu-t4</strong>) — see <code>README.md</code> frontmatter</li>
1342
+ <li>Upload <code>hf_demo/</code> including <code>assets/illustrations/</code> and <code>story.html</code></li>
1343
+ <li>Secret <code>HF_TOKEN</code> if adapter repo is private</li>
1344
+ <li>Smoke-test all 6 examples</li>
1345
+ </ol>
1346
+ <h3>Future work</h3>
1347
+ <ul>
1348
+ <li>DSBench MCQ letter mapping in scorer</li>
1349
+ <li>Per-micro-batch checkpointing during EVTE-STaR</li>
1350
+ <li>Optional Space variant with Micro-1 for hard-pool showcase</li>
1351
+ </ul>
1352
+ </section>
1353
+
1354
+ <footer>
1355
+ <p>
1356
+ <strong>DataSense E2B</strong> — Execution-verified, Tutor-escalation training for personal data science agents.<br />
1357
+ Code: <code>datasense_pipeline.py</code> · <code>datasense_evte.py</code> · <code>datasense_agent.py</code> · <code>hf_demo/</code><br />
1358
+ Built for the Gemma / DataBench hackathon, June 2026.
1359
+ </p>
1360
+ <p style="margin-top:2rem">
1361
+ <a href="/">← Back to Gradio demo</a> &nbsp;&nbsp;·&nbsp;&nbsp;
1362
+ <a href="https://huggingface.co/sanjaymalladi/DataSense-Modal-E2B-SFT">SFT v1 on HF</a> &nbsp;&nbsp;·&nbsp;&nbsp;
1363
+ <a href="https://huggingface.co/sanjaymalladi/DataSense-Modal-E2B-EVTE-Star-Micro1">Micro-1 on HF</a>
1364
+ </p>
1365
+ </footer>
1366
+ </div>
1367
+ </body>
1368
+ </html>
index.zip ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3ecb26cd6b0b300b8058f74c5e4a627972a388a9a54615f1cce5b818317c2c42
3
+ size 8255142