sanjaymalladi commited on
Commit
f520bda
·
verified ·
1 Parent(s): 00496b5

Data preview, execution trace first, styled answer card, Excel upload

Browse files
Files changed (3) hide show
  1. agent.py +22 -5
  2. app.py +128 -32
  3. requirements.txt +1 -0
agent.py CHANGED
@@ -121,23 +121,40 @@ def execute_python(code: str, working_dir: str, timeout: int = 30) -> dict:
121
  os.unlink(tmp_path)
122
 
123
 
124
- def inspect_csv(path: Path) -> dict[str, str]:
125
  import pandas as pd
126
 
127
- df = pd.read_csv(path, nrows=200)
 
 
 
 
 
 
 
128
  schema = "\n".join(f" {c}: {df[c].dtype}" for c in df.columns)
129
  sample = df.head(5).to_string(index=False)
 
130
  return {
131
- "type": "csv",
132
  "schema": schema,
133
  "sample": sample,
134
  "row_counts": f"preview_rows={len(df)} (file may be larger)",
135
  }
136
 
137
 
 
 
 
 
138
  def build_user_message(data_path: Path, task: str) -> str:
139
- info = inspect_csv(data_path)
140
  filename = data_path.name
 
 
 
 
 
141
  lines = [
142
  f"Data source: {filename}",
143
  f"Working directory contains: {filename}",
@@ -153,7 +170,7 @@ def build_user_message(data_path: Path, task: str) -> str:
153
  "",
154
  f"Task: {task}",
155
  "",
156
- f"Read the file with pandas: pd.read_csv('{filename}')",
157
  ]
158
  return "\n".join(lines)
159
 
 
121
  os.unlink(tmp_path)
122
 
123
 
124
+ def _read_tabular(path: Path, nrows: int = 200):
125
  import pandas as pd
126
 
127
+ suffix = path.suffix.lower()
128
+ if suffix in (".xlsx", ".xls"):
129
+ return pd.read_excel(path, nrows=nrows)
130
+ return pd.read_csv(path, nrows=nrows)
131
+
132
+
133
+ def inspect_data(path: Path) -> dict[str, str]:
134
+ df = _read_tabular(path, nrows=200)
135
  schema = "\n".join(f" {c}: {df[c].dtype}" for c in df.columns)
136
  sample = df.head(5).to_string(index=False)
137
+ kind = "excel" if path.suffix.lower() in (".xlsx", ".xls") else "csv"
138
  return {
139
+ "type": kind,
140
  "schema": schema,
141
  "sample": sample,
142
  "row_counts": f"preview_rows={len(df)} (file may be larger)",
143
  }
144
 
145
 
146
+ def inspect_csv(path: Path) -> dict[str, str]:
147
+ return inspect_data(path)
148
+
149
+
150
  def build_user_message(data_path: Path, task: str) -> str:
151
+ info = inspect_data(data_path)
152
  filename = data_path.name
153
+ read_hint = (
154
+ f"pd.read_excel('{filename}')"
155
+ if info["type"] == "excel"
156
+ else f"pd.read_csv('{filename}')"
157
+ )
158
  lines = [
159
  f"Data source: {filename}",
160
  f"Working directory contains: {filename}",
 
170
  "",
171
  f"Task: {task}",
172
  "",
173
+ f"Read the file with pandas: {read_hint}",
174
  ]
175
  return "\n".join(lines)
176
 
app.py CHANGED
@@ -1,15 +1,17 @@
1
  """
2
  DataSense E2B — Hugging Face Space demo
3
- Execution-grounded data agent (SFT v1) with bundled or uploaded CSVs.
4
  """
5
 
6
  from __future__ import annotations
7
 
 
8
  import spaces # must be first — before any torch/CUDA import
9
 
10
  from pathlib import Path
11
 
12
  import gradio as gr
 
13
 
14
  from agent import run_agent
15
  from config import ADAPTER_MODEL, AGENT_MAX_STEPS, DATA_DIR
@@ -32,7 +34,7 @@ CUSTOM_CSS = """
32
  --ds-accent-dim: #1f6f5c;
33
  --ds-text: #e8eef5;
34
  --ds-muted: #8b9cb3;
35
- max-width: 1180px !important;
36
  font-family: 'Newsreader', Georgia, serif !important;
37
  }
38
  #ds-header {
@@ -61,18 +63,68 @@ CUSTOM_CSS = """
61
  padding: 0.2rem 0.65rem;
62
  margin-bottom: 0.75rem;
63
  }
64
- #ds-panel {
65
  background: var(--ds-surface);
66
  border: 1px solid var(--ds-border);
67
  border-radius: 14px;
68
  padding: 1.1rem;
 
69
  }
 
 
 
 
 
 
 
70
  #run-btn {
71
  background: linear-gradient(90deg, #1f6f5c, #3ecfae) !important;
72
  border: none !important;
73
  font-weight: 600 !important;
74
  letter-spacing: 0.02em;
75
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  footer { visibility: hidden; }
77
  """
78
 
@@ -112,7 +164,7 @@ def _load_model():
112
 
113
 
114
  def _resolve_data_path(data_mode: str, dataset_name: str, upload_file) -> Path | None:
115
- if data_mode == "Upload your CSV":
116
  if upload_file is None:
117
  return None
118
  path_str = upload_file[0] if isinstance(upload_file, list) else upload_file
@@ -123,8 +175,41 @@ def _resolve_data_path(data_mode: str, dataset_name: str, upload_file) -> Path |
123
  return DEMO_DATASETS.get(dataset_name)
124
 
125
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  def _toggle_data_inputs(data_mode: str):
127
- is_upload = data_mode == "Upload your CSV"
128
  return (
129
  gr.update(visible=not is_upload),
130
  gr.update(visible=is_upload),
@@ -141,13 +226,13 @@ def run_task(
141
  progress=gr.Progress(),
142
  ):
143
  if not task.strip():
144
- return "⚠️ Enter a task question.", ""
145
 
146
  progress(0.05, desc="Resolving dataset…")
147
  data_path = _resolve_data_path(data_mode, dataset_name, upload_file)
148
  if data_path is None:
149
- msg = "⚠️ Upload a `.csv` file first." if data_mode == "Upload your CSV" else f"⚠️ Dataset not found: {dataset_name}"
150
- return msg, ""
151
 
152
  try:
153
  progress(0.15, desc="Loading Gemma-4 + SFT LoRA…")
@@ -162,13 +247,11 @@ def run_task(
162
  progress=progress,
163
  )
164
  except Exception as exc:
165
- return f"**Error:** {exc}", ""
166
 
167
- answer_block = f"## {result['answer']}" if result["answer"] else "_Could not parse an answer — check the execution trace._"
168
- if result.get("summary"):
169
- answer_block += f"\n\n{result['summary']}"
170
-
171
- return answer_block, result["steps_markdown"]
172
 
173
 
174
  @spaces.GPU(duration=300)
@@ -178,6 +261,7 @@ def preload_model():
178
 
179
  def build_ui() -> gr.Blocks:
180
  dataset_choices = list(DEMO_DATASETS.keys())
 
181
 
182
  with gr.Blocks(title="DataSense E2B") as demo:
183
  with gr.Column(elem_id="ds-header"):
@@ -185,7 +269,7 @@ def build_ui() -> gr.Blocks:
185
  gr.Markdown(
186
  """
187
  # DataSense E2B
188
- **Live inference** — Gemma-4 2B + SFT v1 writes Python, runs it on your CSV, reads real stdout/errors.
189
  """
190
  )
191
  gr.Markdown(
@@ -195,10 +279,10 @@ def build_ui() -> gr.Blocks:
195
  )
196
 
197
  with gr.Row(equal_height=False):
198
- with gr.Column(scale=4, elem_id="ds-panel"):
199
  gr.Markdown("### Configure")
200
  data_mode = gr.Radio(
201
- choices=["Bundled examples", "Upload your CSV"],
202
  value="Bundled examples",
203
  label="Data source",
204
  )
@@ -208,11 +292,22 @@ def build_ui() -> gr.Blocks:
208
  label="Demo dataset",
209
  )
210
  upload = gr.File(
211
- label="Your CSV file",
212
- file_types=[".csv"],
213
  type="filepath",
214
  visible=False,
215
  )
 
 
 
 
 
 
 
 
 
 
 
216
  task = gr.Textbox(
217
  label="Question / task",
218
  placeholder="e.g. Which product had the highest total revenue?",
@@ -233,29 +328,30 @@ def build_ui() -> gr.Blocks:
233
  label="Quick examples (bundled data)",
234
  )
235
 
236
- gr.Markdown(
237
- f"""
238
- ---
239
- <small>Base `unsloth/gemma-4-E2B-it` · bundled CSVs in `{DATA_DIR.name}/`</small>
240
- """
241
- )
242
-
243
- with gr.Column(scale=6):
244
  with gr.Tabs():
245
- with gr.Tab(" Answer"):
246
- answer_out = gr.Markdown()
247
- with gr.Tab("🔍 Execution trace"):
248
  steps_out = gr.Markdown()
 
 
249
 
250
- data_mode.change(_toggle_data_inputs, data_mode, [dataset, upload])
 
 
 
 
 
251
 
252
  run_btn.click(
253
  fn=run_task,
254
  inputs=[data_mode, dataset, upload, task, max_steps],
255
- outputs=[answer_out, steps_out],
256
  show_progress="full",
257
  )
258
 
 
 
259
  return demo
260
 
261
 
 
1
  """
2
  DataSense E2B — Hugging Face Space demo
3
+ Execution-grounded data agent (SFT v1) with bundled or uploaded CSVs/Excel.
4
  """
5
 
6
  from __future__ import annotations
7
 
8
+ import html
9
  import spaces # must be first — before any torch/CUDA import
10
 
11
  from pathlib import Path
12
 
13
  import gradio as gr
14
+ import pandas as pd
15
 
16
  from agent import run_agent
17
  from config import ADAPTER_MODEL, AGENT_MAX_STEPS, DATA_DIR
 
34
  --ds-accent-dim: #1f6f5c;
35
  --ds-text: #e8eef5;
36
  --ds-muted: #8b9cb3;
37
+ max-width: 1280px !important;
38
  font-family: 'Newsreader', Georgia, serif !important;
39
  }
40
  #ds-header {
 
63
  padding: 0.2rem 0.65rem;
64
  margin-bottom: 0.75rem;
65
  }
66
+ #ds-panel, #ds-results {
67
  background: var(--ds-surface);
68
  border: 1px solid var(--ds-border);
69
  border-radius: 14px;
70
  padding: 1.1rem;
71
+ min-height: 520px;
72
  }
73
+ #ds-preview-box {
74
+ margin-top: 0.5rem;
75
+ border: 1px solid var(--ds-border);
76
+ border-radius: 10px;
77
+ overflow: hidden;
78
+ }
79
+ #ds-preview-box .label-wrap { padding: 0.5rem 0.75rem !important; }
80
  #run-btn {
81
  background: linear-gradient(90deg, #1f6f5c, #3ecfae) !important;
82
  border: none !important;
83
  font-weight: 600 !important;
84
  letter-spacing: 0.02em;
85
  }
86
+ #ds-results .tabs { margin-top: 0 !important; }
87
+ #ds-results .tabitem { padding-top: 0.75rem !important; }
88
+ .ds-answer-card {
89
+ background: linear-gradient(145deg, #122a2a 0%, #151c26 100%);
90
+ border: 1px solid #2a3544;
91
+ border-left: 4px solid #3ecfae;
92
+ border-radius: 12px;
93
+ padding: 1.5rem 1.75rem;
94
+ margin: 0;
95
+ min-height: 200px;
96
+ }
97
+ .ds-answer-label {
98
+ font-family: 'IBM Plex Mono', monospace;
99
+ font-size: 0.72rem;
100
+ letter-spacing: 0.1em;
101
+ text-transform: uppercase;
102
+ color: #3ecfae;
103
+ margin-bottom: 0.75rem;
104
+ }
105
+ .ds-answer-value {
106
+ font-family: 'Newsreader', Georgia, serif;
107
+ font-size: 2rem;
108
+ font-weight: 600;
109
+ color: #e8eef5;
110
+ line-height: 1.3;
111
+ word-break: break-word;
112
+ }
113
+ .ds-summary {
114
+ margin-top: 1.25rem;
115
+ padding-top: 1rem;
116
+ border-top: 1px solid #2a3544;
117
+ font-size: 1.05rem;
118
+ color: #8b9cb3;
119
+ line-height: 1.55;
120
+ }
121
+ .ds-answer-empty {
122
+ color: #8b9cb3;
123
+ font-style: italic;
124
+ padding: 1rem 0;
125
+ }
126
+ .ds-trace-wrap { margin: 0; padding: 0; }
127
+ .ds-trace-wrap h3 { margin-top: 1rem; color: #3ecfae; font-size: 1rem; }
128
  footer { visibility: hidden; }
129
  """
130
 
 
164
 
165
 
166
  def _resolve_data_path(data_mode: str, dataset_name: str, upload_file) -> Path | None:
167
+ if data_mode == "Upload your file":
168
  if upload_file is None:
169
  return None
170
  path_str = upload_file[0] if isinstance(upload_file, list) else upload_file
 
175
  return DEMO_DATASETS.get(dataset_name)
176
 
177
 
178
+ def _load_preview(data_mode: str, dataset_name: str, upload_file):
179
+ path = _resolve_data_path(data_mode, dataset_name, upload_file)
180
+ if path is None:
181
+ return pd.DataFrame(), "_Select or upload a file to preview._"
182
+ try:
183
+ suffix = path.suffix.lower()
184
+ if suffix in (".xlsx", ".xls"):
185
+ df = pd.read_excel(path, nrows=100)
186
+ else:
187
+ df = pd.read_csv(path, nrows=100)
188
+ meta = f"**{path.name}** · {len(df)} rows · {len(df.columns)} columns"
189
+ return df, meta
190
+ except Exception as exc:
191
+ return pd.DataFrame(), f"_Could not preview file: {exc}_"
192
+
193
+
194
+ def _format_answer_html(answer: str, summary: str = "") -> str:
195
+ if not answer:
196
+ return '<div class="ds-answer-empty">Could not parse an answer — check the execution trace tab.</div>'
197
+ safe_answer = html.escape(answer)
198
+ summary_block = ""
199
+ if summary:
200
+ safe_summary = html.escape(summary)
201
+ summary_block = f'<p class="ds-summary">{safe_summary}</p>'
202
+ return (
203
+ f'<div class="ds-answer-card">'
204
+ f'<div class="ds-answer-label">Verified answer</div>'
205
+ f'<div class="ds-answer-value">{safe_answer}</div>'
206
+ f"{summary_block}"
207
+ f"</div>"
208
+ )
209
+
210
+
211
  def _toggle_data_inputs(data_mode: str):
212
+ is_upload = data_mode == "Upload your file"
213
  return (
214
  gr.update(visible=not is_upload),
215
  gr.update(visible=is_upload),
 
226
  progress=gr.Progress(),
227
  ):
228
  if not task.strip():
229
+ return "", "⚠️ Enter a task question."
230
 
231
  progress(0.05, desc="Resolving dataset…")
232
  data_path = _resolve_data_path(data_mode, dataset_name, upload_file)
233
  if data_path is None:
234
+ msg = "⚠️ Upload a `.csv` or `.xlsx` file first." if data_mode == "Upload your file" else f"⚠️ Dataset not found: {dataset_name}"
235
+ return "", msg
236
 
237
  try:
238
  progress(0.15, desc="Loading Gemma-4 + SFT LoRA…")
 
247
  progress=progress,
248
  )
249
  except Exception as exc:
250
+ return "", f"**Error:** {exc}"
251
 
252
+ answer_html = _format_answer_html(result.get("answer", ""), result.get("summary", ""))
253
+ trace = f'<div class="ds-trace-wrap">\n\n{result["steps_markdown"]}\n\n</div>'
254
+ return trace, answer_html
 
 
255
 
256
 
257
  @spaces.GPU(duration=300)
 
261
 
262
  def build_ui() -> gr.Blocks:
263
  dataset_choices = list(DEMO_DATASETS.keys())
264
+ default_df, default_meta = _load_preview("Bundled examples", dataset_choices[0], None)
265
 
266
  with gr.Blocks(title="DataSense E2B") as demo:
267
  with gr.Column(elem_id="ds-header"):
 
269
  gr.Markdown(
270
  """
271
  # DataSense E2B
272
+ **Live inference** — Gemma-4 2B + SFT v1 writes Python, runs it on your data, reads real stdout/errors.
273
  """
274
  )
275
  gr.Markdown(
 
279
  )
280
 
281
  with gr.Row(equal_height=False):
282
+ with gr.Column(scale=5, elem_id="ds-panel"):
283
  gr.Markdown("### Configure")
284
  data_mode = gr.Radio(
285
+ choices=["Bundled examples", "Upload your file"],
286
  value="Bundled examples",
287
  label="Data source",
288
  )
 
292
  label="Demo dataset",
293
  )
294
  upload = gr.File(
295
+ label="Your CSV or Excel file",
296
+ file_types=[".csv", ".xlsx", ".xls"],
297
  type="filepath",
298
  visible=False,
299
  )
300
+
301
+ gr.Markdown("### Data preview")
302
+ preview_meta = gr.Markdown(default_meta)
303
+ with gr.Group(elem_id="ds-preview-box"):
304
+ preview_df = gr.Dataframe(
305
+ value=default_df,
306
+ interactive=False,
307
+ wrap=True,
308
+ max_height=280,
309
+ )
310
+
311
  task = gr.Textbox(
312
  label="Question / task",
313
  placeholder="e.g. Which product had the highest total revenue?",
 
328
  label="Quick examples (bundled data)",
329
  )
330
 
331
+ with gr.Column(scale=7, elem_id="ds-results"):
332
+ gr.Markdown("### Results")
 
 
 
 
 
 
333
  with gr.Tabs():
334
+ with gr.Tab("🔍 Execution trace", id="trace_tab"):
 
 
335
  steps_out = gr.Markdown()
336
+ with gr.Tab("✅ Answer", id="answer_tab"):
337
+ answer_out = gr.HTML()
338
 
339
+ preview_inputs = [data_mode, dataset, upload]
340
+ data_mode.change(_toggle_data_inputs, data_mode, [dataset, upload]).then(
341
+ _load_preview, preview_inputs, [preview_df, preview_meta]
342
+ )
343
+ dataset.change(_load_preview, preview_inputs, [preview_df, preview_meta])
344
+ upload.change(_load_preview, preview_inputs, [preview_df, preview_meta])
345
 
346
  run_btn.click(
347
  fn=run_task,
348
  inputs=[data_mode, dataset, upload, task, max_steps],
349
+ outputs=[steps_out, answer_out],
350
  show_progress="full",
351
  )
352
 
353
+ demo.load(_load_preview, preview_inputs, [preview_df, preview_meta])
354
+
355
  return demo
356
 
357
 
requirements.txt CHANGED
@@ -7,3 +7,4 @@ accelerate>=0.30.0
7
  bitsandbytes>=0.43.0
8
  matplotlib>=3.7.0
9
  sentencepiece>=0.2.0
 
 
7
  bitsandbytes>=0.43.0
8
  matplotlib>=3.7.0
9
  sentencepiece>=0.2.0
10
+ openpyxl>=3.1.0