electblake commited on
Commit
ca5bf81
·
verified ·
1 Parent(s): 9f79f58

Add workbook-native XLSX table workflow

Browse files

Add XLSX upload, workbook summary, sheet explorer, formula-preserving table preview, and table inference output. Fix unsized read-only worksheets by forcing dimension calculation.

Files changed (1) hide show
  1. app.py +263 -33
app.py CHANGED
@@ -1,13 +1,22 @@
 
1
  from pathlib import Path
 
2
 
3
  import gradio as gr
4
  import pandas as pd
5
  import spaces
6
  from huggingface_hub import hf_hub_download
7
  from llama_cpp import Llama
 
 
8
 
9
 
 
 
 
10
  MODEL_REPO = "mradermacher/Spreadsheet-RL-4B-GGUF"
 
 
11
  QUANT_FILES = {
12
  "Q2_K · 1.9 GB": "Spreadsheet-RL-4B.Q2_K.gguf",
13
  "Q3_K_S · 2.2 GB": "Spreadsheet-RL-4B.Q3_K_S.gguf",
@@ -27,6 +36,58 @@ model = None
27
  active_quant = None
28
 
29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  def download_quant(quantization: str) -> None:
31
  hf_hub_download(repo_id=MODEL_REPO, filename=QUANT_FILES[quantization])
32
 
@@ -101,16 +162,72 @@ def generate(
101
  return completion["choices"][0]["message"]["content"].rsplit("</think>", 1)[-1].strip()
102
 
103
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  CSS = """
105
- .gradio-container { max-width: 1180px !important; }
 
 
 
 
106
  .agent-panel { border: 2px dashed #79b5ce; border-radius: 18px; padding: 8px; }
107
  .output-panel { border: 2px dashed #f0aeb7; border-radius: 18px; padding: 8px; }
108
  """
109
 
110
  with gr.Blocks(css=CSS, title="Spreadsheet Data Agent") as demo:
111
  gr.Markdown(
112
- """
113
- # Spreadsheet Data Agent
114
 
115
  [Code](https://github.com/electblake/Spreadsheet-RL-Data-Agent) | [Demo](https://huggingface.co/spaces/electblake/spreadsheet-data-agent) | [Paper](https://arxiv.org/abs/2605.22642) | [Spreadsheet-RL Model](https://huggingface.co/Spreadsheet-RL/Spreadsheet-RL-4B)
116
 
@@ -119,43 +236,125 @@ with gr.Blocks(css=CSS, title="Spreadsheet Data Agent") as demo:
119
  """
120
  )
121
 
122
- with gr.Row():
123
- with gr.Column(scale=1, elem_classes="agent-panel"):
124
- gr.Markdown("### RL data input")
125
- system_prompt = gr.Textbox(
126
- label="System prompt",
127
- value=(
128
- "You are a spreadsheet reasoning assistant. Inspect the supplied "
129
- "spreadsheet or text context and answer the user's request precisely."
130
- ),
131
- lines=5,
132
  )
133
- user_prompt = gr.Textbox(
134
- label="User prompt",
135
- placeholder="Describe the spreadsheet task or ask a question…",
136
- lines=8,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  )
138
- attachment = gr.File(
139
- label="Optional file context",
140
- file_types=[".txt", ".md", ".json", ".csv", ".tsv", ".xlsx", ".xls"],
 
141
  type="filepath",
142
  )
143
- quantization = gr.Dropdown(
144
- choices=list(QUANT_FILES),
145
- value="Q4_K_M · 2.8 GB · recommended",
146
- label="Spreadsheet-RL-4B quantization",
147
- info="Static GGUF quants published by mradermacher; Q4_K_M is the reference recommendation.",
 
 
 
 
 
 
148
  )
149
- run = gr.Button("Run inference", variant="primary")
150
-
151
- with gr.Column(scale=1, elem_classes="output-panel"):
152
- gr.Markdown("### Agent response")
153
- response = gr.Textbox(
154
- label="Generated text",
155
- lines=28,
156
- buttons=["copy"],
 
 
157
  )
158
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  gr.Markdown(
160
  """
161
  ---
@@ -194,4 +393,35 @@ with gr.Blocks(css=CSS, title="Spreadsheet Data Agent") as demo:
194
  show_progress="full",
195
  )
196
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  demo.queue().launch(mcp_server=True)
 
1
+ from io import StringIO
2
  from pathlib import Path
3
+ import tomllib
4
 
5
  import gradio as gr
6
  import pandas as pd
7
  import spaces
8
  from huggingface_hub import hf_hub_download
9
  from llama_cpp import Llama
10
+ from openpyxl import load_workbook
11
+ from openpyxl.utils import get_column_letter
12
 
13
 
14
+ PROJECT_VERSION = tomllib.loads(
15
+ Path(__file__).with_name("pyproject.toml").read_text(encoding="utf-8")
16
+ )["project"]["version"]
17
  MODEL_REPO = "mradermacher/Spreadsheet-RL-4B-GGUF"
18
+ XLSX_PREVIEW_ROWS = 100
19
+ XLSX_PREVIEW_COLUMNS = 50
20
  QUANT_FILES = {
21
  "Q2_K · 1.9 GB": "Spreadsheet-RL-4B.Q2_K.gguf",
22
  "Q3_K_S · 2.2 GB": "Spreadsheet-RL-4B.Q3_K_S.gguf",
 
36
  active_quant = None
37
 
38
 
39
+ def build_workbook_overview(file_path: str | Path) -> pd.DataFrame:
40
+ workbook = load_workbook(file_path, read_only=True, data_only=False)
41
+ active_sheet = workbook.active.title
42
+ overview = pd.DataFrame(
43
+ [
44
+ {
45
+ "Sheet": worksheet.title,
46
+ "Active": worksheet.title == active_sheet,
47
+ "Used range": worksheet.calculate_dimension(force=True),
48
+ "Rows": worksheet.max_row,
49
+ "Columns": worksheet.max_column,
50
+ }
51
+ for worksheet in workbook.worksheets
52
+ ]
53
+ )
54
+ workbook.close()
55
+ return overview
56
+
57
+
58
+ def preview_xlsx_sheet(file_path: str | Path, sheet_name: str) -> pd.DataFrame:
59
+ workbook = load_workbook(file_path, read_only=True, data_only=False)
60
+ worksheet = workbook[sheet_name]
61
+ worksheet.calculate_dimension(force=True)
62
+ column_count = min(worksheet.max_column, XLSX_PREVIEW_COLUMNS)
63
+ row_count = min(worksheet.max_row, XLSX_PREVIEW_ROWS)
64
+ preview = pd.DataFrame(
65
+ worksheet.iter_rows(
66
+ min_row=1,
67
+ max_row=row_count,
68
+ min_col=1,
69
+ max_col=column_count,
70
+ values_only=True,
71
+ ),
72
+ columns=[get_column_letter(index) for index in range(1, column_count + 1)],
73
+ )
74
+ workbook.close()
75
+ return preview
76
+
77
+
78
+ def load_xlsx_workflow(
79
+ file_path: str | Path,
80
+ ) -> tuple[pd.DataFrame, gr.Dropdown, pd.DataFrame]:
81
+ overview = build_workbook_overview(file_path)
82
+ sheet_names = overview["Sheet"].tolist()
83
+ selected_sheet = sheet_names[0]
84
+ return (
85
+ overview,
86
+ gr.Dropdown(choices=sheet_names, value=selected_sheet),
87
+ preview_xlsx_sheet(file_path, selected_sheet),
88
+ )
89
+
90
+
91
  def download_quant(quantization: str) -> None:
92
  hf_hub_download(repo_id=MODEL_REPO, filename=QUANT_FILES[quantization])
93
 
 
162
  return completion["choices"][0]["message"]["content"].rsplit("</think>", 1)[-1].strip()
163
 
164
 
165
+ @spaces.GPU(duration=120)
166
+ def generate_xlsx_table(
167
+ system_prompt: str,
168
+ user_prompt: str,
169
+ attachment: str,
170
+ sheet_name: str,
171
+ quantization: str,
172
+ ) -> pd.DataFrame:
173
+ global active_quant, model
174
+
175
+ quant_file = QUANT_FILES[quantization]
176
+ if active_quant != quantization:
177
+ model = None
178
+ active_quant = None
179
+ model = Llama(
180
+ model_path=hf_hub_download(repo_id=MODEL_REPO, filename=quant_file),
181
+ n_ctx=4096,
182
+ n_gpu_layers=-1,
183
+ verbose=True,
184
+ )
185
+ active_quant = quantization
186
+
187
+ overview = build_workbook_overview(attachment).to_csv(index=False)
188
+ preview = preview_xlsx_sheet(attachment, sheet_name).to_csv(index=False)
189
+ messages = [
190
+ {
191
+ "role": "system",
192
+ "content": (
193
+ f"{system_prompt}\n\nReturn only valid CSV with one header row. "
194
+ "Do not wrap the CSV in a code fence or add prose."
195
+ ),
196
+ },
197
+ {
198
+ "role": "user",
199
+ "content": (
200
+ f"Task:\n{user_prompt}\n\nWorkbook overview:\n{overview}\n"
201
+ f"Selected sheet preview ({sheet_name}, first {XLSX_PREVIEW_ROWS} rows "
202
+ f"and {XLSX_PREVIEW_COLUMNS} columns):\n{preview}"
203
+ ),
204
+ },
205
+ ]
206
+ completion = model.create_chat_completion(
207
+ messages=messages,
208
+ max_tokens=1024,
209
+ temperature=0.6,
210
+ top_p=0.95,
211
+ top_k=20,
212
+ )
213
+ response = completion["choices"][0]["message"]["content"].rsplit("</think>", 1)[-1].strip()
214
+ return pd.read_csv(StringIO(response))
215
+
216
+
217
  CSS = """
218
+ .gradio-container {
219
+ width: min(calc(100% - 32px), 1600px) !important;
220
+ max-width: 1600px !important;
221
+ margin-inline: auto !important;
222
+ }
223
  .agent-panel { border: 2px dashed #79b5ce; border-radius: 18px; padding: 8px; }
224
  .output-panel { border: 2px dashed #f0aeb7; border-radius: 18px; padding: 8px; }
225
  """
226
 
227
  with gr.Blocks(css=CSS, title="Spreadsheet Data Agent") as demo:
228
  gr.Markdown(
229
+ f"""
230
+ # Spreadsheet Data Agent · v{PROJECT_VERSION}
231
 
232
  [Code](https://github.com/electblake/Spreadsheet-RL-Data-Agent) | [Demo](https://huggingface.co/spaces/electblake/spreadsheet-data-agent) | [Paper](https://arxiv.org/abs/2605.22642) | [Spreadsheet-RL Model](https://huggingface.co/Spreadsheet-RL/Spreadsheet-RL-4B)
233
 
 
236
  """
237
  )
238
 
239
+ with gr.Tabs(selected="basic-data"):
240
+ with gr.Tab("Basic data only", id="basic-data"):
241
+ gr.Markdown(
242
+ "Uses the original inference workflow: uploaded files are converted to "
243
+ "plain data context and sent to the model with the prompt."
 
 
 
 
 
244
  )
245
+
246
+ with gr.Row():
247
+ with gr.Column(scale=1, elem_classes="agent-panel"):
248
+ gr.Markdown("### RL data input")
249
+ system_prompt = gr.Textbox(
250
+ label="System prompt",
251
+ value=(
252
+ "You are a spreadsheet reasoning assistant. Inspect the supplied "
253
+ "spreadsheet or text context and answer the user's request precisely."
254
+ ),
255
+ lines=5,
256
+ )
257
+ user_prompt = gr.Textbox(
258
+ label="User prompt",
259
+ placeholder="Describe the spreadsheet task or ask a question…",
260
+ lines=8,
261
+ )
262
+ attachment = gr.File(
263
+ label="Optional file context",
264
+ file_types=[".txt", ".md", ".json", ".csv", ".tsv", ".xlsx", ".xls"],
265
+ type="filepath",
266
+ )
267
+ quantization = gr.Dropdown(
268
+ choices=list(QUANT_FILES),
269
+ value="Q4_K_M · 2.8 GB · recommended",
270
+ label="Spreadsheet-RL-4B quantization",
271
+ info="Static GGUF quants published by mradermacher; Q4_K_M is the reference recommendation.",
272
+ )
273
+ run = gr.Button("Run inference", variant="primary")
274
+
275
+ with gr.Column(scale=1, elem_classes="output-panel"):
276
+ gr.Markdown("### Agent response")
277
+ response = gr.Textbox(
278
+ label="Generated text",
279
+ lines=28,
280
+ buttons=["copy"],
281
+ )
282
+
283
+ with gr.Tab("XLSX workflow", id="xlsx-workflow"):
284
+ gr.Markdown(
285
+ """
286
+ ## XLSX workbook workflow
287
+
288
+ Upload an Excel workbook to inspect its sheets and preview its table data
289
+ before running table-focused inference.
290
+ """
291
  )
292
+
293
+ xlsx_attachment = gr.File(
294
+ label="XLSX workbook",
295
+ file_types=[".xlsx"],
296
  type="filepath",
297
  )
298
+ workbook_overview = gr.Dataframe(
299
+ headers=["Sheet", "Active", "Used range", "Rows", "Columns"],
300
+ datatype=["str", "bool", "str", "number", "number"],
301
+ label="Workbook summary",
302
+ interactive=False,
303
+ buttons=["fullscreen", "copy"],
304
+ show_search="filter",
305
+ )
306
+ xlsx_sheet = gr.Dropdown(
307
+ label="Preview sheet",
308
+ choices=[],
309
  )
310
+ xlsx_preview = gr.Dataframe(
311
+ label=(
312
+ f"Selected sheet preview (first {XLSX_PREVIEW_ROWS} rows and "
313
+ f"{XLSX_PREVIEW_COLUMNS} columns)"
314
+ ),
315
+ interactive=False,
316
+ max_height=520,
317
+ buttons=["fullscreen", "copy"],
318
+ show_row_numbers=True,
319
+ show_search="filter",
320
  )
321
 
322
+ with gr.Row():
323
+ with gr.Column(scale=1, elem_classes="agent-panel"):
324
+ gr.Markdown("### Table inference input")
325
+ xlsx_system_prompt = gr.Textbox(
326
+ label="System prompt",
327
+ value=(
328
+ "You are a spreadsheet data assistant. Analyze the workbook "
329
+ "summary and selected sheet preview, then return the requested "
330
+ "result as a table."
331
+ ),
332
+ lines=5,
333
+ )
334
+ xlsx_user_prompt = gr.Textbox(
335
+ label="User prompt",
336
+ placeholder="Describe the table to derive from this workbook…",
337
+ lines=8,
338
+ )
339
+ xlsx_quantization = gr.Dropdown(
340
+ choices=list(QUANT_FILES),
341
+ value="Q4_K_M · 2.8 GB · recommended",
342
+ label="Spreadsheet-RL-4B quantization",
343
+ info="Static GGUF quants published by mradermacher; Q4_K_M is the reference recommendation.",
344
+ )
345
+ xlsx_run = gr.Button("Run table inference", variant="primary")
346
+
347
+ with gr.Column(scale=1, elem_classes="output-panel"):
348
+ gr.Markdown("### Table response")
349
+ xlsx_response = gr.Dataframe(
350
+ label="Generated table",
351
+ interactive=False,
352
+ max_height=720,
353
+ buttons=["fullscreen", "copy"],
354
+ show_row_numbers=True,
355
+ show_search="filter",
356
+ )
357
+
358
  gr.Markdown(
359
  """
360
  ---
 
393
  show_progress="full",
394
  )
395
 
396
+ xlsx_attachment.upload(
397
+ fn=load_xlsx_workflow,
398
+ inputs=xlsx_attachment,
399
+ outputs=[workbook_overview, xlsx_sheet, xlsx_preview],
400
+ show_progress="full",
401
+ )
402
+ xlsx_sheet.change(
403
+ fn=preview_xlsx_sheet,
404
+ inputs=[xlsx_attachment, xlsx_sheet],
405
+ outputs=xlsx_preview,
406
+ show_progress="full",
407
+ )
408
+ xlsx_run.click(
409
+ fn=download_quant,
410
+ inputs=xlsx_quantization,
411
+ outputs=None,
412
+ show_progress="full",
413
+ ).then(
414
+ fn=generate_xlsx_table,
415
+ inputs=[
416
+ xlsx_system_prompt,
417
+ xlsx_user_prompt,
418
+ xlsx_attachment,
419
+ xlsx_sheet,
420
+ xlsx_quantization,
421
+ ],
422
+ outputs=xlsx_response,
423
+ api_name="generate_xlsx_table",
424
+ show_progress="full",
425
+ )
426
+
427
  demo.queue().launch(mcp_server=True)