Softer answer parsing, simplify UI, netlify story link
Browse files- README.md +2 -2
- agent.py +94 -10
- app.py +16 -16
- index.html +1368 -0
- 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 →](
|
| 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
|
|
|
|
| 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 |
-
|
| 162 |
-
|
| 163 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 222 |
-
step_logs.append("✅ Agent finished
|
| 223 |
break
|
| 224 |
|
| 225 |
code_blocks = extract_code_blocks(response)
|
| 226 |
if not code_blocks:
|
| 227 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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"
|
| 176 |
-
|
| 177 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 208 |
"""
|
| 209 |
)
|
| 210 |
gr.Markdown(
|
| 211 |
-
f"📖 [Full project story](
|
| 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("
|
| 270 |
-
|
| 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,
|
| 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 & motivation</a></li>
|
| 613 |
+
<li><a href="#evte-feedback">EVTE feedback loops (self-recovery)</a></li>
|
| 614 |
+
<li><a href="#evte-mentor">Mentor verify & 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 & 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 & 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><result></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><result></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><result></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 <result> 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><result></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 & 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 & 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 & 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> ·
|
| 1362 |
+
<a href="https://huggingface.co/sanjaymalladi/DataSense-Modal-E2B-SFT">SFT v1 on HF</a> ·
|
| 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
|