Spaces:
Running on Zero
Running on Zero
| """Standard Gradio Blocks UI — the fallback frontend for the ZeroGPU Space. | |
| The primary frontend is the React app served by ``webapp/server.py`` (gr.Server | |
| Server mode). If that doesn't drive under ZeroGPU's runtime, point the Space's | |
| ``app_file`` at this module instead: it's a plain ``gr.Blocks`` UI over the exact | |
| same ``webapp/backend.py`` functions, so parse/summarize/report still run in-process | |
| on ZeroGPU (when ``LLM_BACKEND=local``). | |
| The flow mirrors the React app: upload an agenda-packet PDF + give the agenda page | |
| range, parse it into items, then summarize / report on an item / view its pages / | |
| ask the agent. | |
| Run: ``python -m webapp.app_blocks`` (or set it as the Space app_file). | |
| """ | |
| from __future__ import annotations | |
| import os | |
| import gradio as gr | |
| from webapp import backend | |
| def _item_titles(items: list[dict]) -> list[str]: | |
| """Every item's ``"<number> <name>"`` line, in agenda order — the anchors the | |
| backend uses to locate and bound each item's section in the packet.""" | |
| return [f"{it['number']} {it['name']}".strip() for it in items] | |
| def _upload(file_path, agenda_pages): | |
| """Parse an uploaded packet into agenda items and populate the item picker.""" | |
| blank = gr.update(choices=[], value=None) | |
| if not file_path: | |
| return "_Upload an agenda-packet PDF to begin._", blank, None | |
| try: | |
| with open(file_path, "rb") as fh: | |
| data = fh.read() | |
| name = os.path.basename(file_path) | |
| res = backend.upload_packet(data, name, (agenda_pages or "").strip()) | |
| except Exception as e: # noqa: BLE001 | |
| return f"_Could not parse the packet: {type(e).__name__}: {e}_", blank, None | |
| items = res.get("items", []) | |
| if not items: | |
| return ("_No agenda items were parsed — check the agenda page range._", blank, None) | |
| lines, choices = [], [] | |
| for i, it in enumerate(items): | |
| label = f"{it['number']} {it['name']}".strip() or "(untitled item)" | |
| indent = " " * (4 * int(it.get("level", 0))) | |
| if it.get("is_section"): | |
| lines.append(f"{indent}**{label}**") | |
| else: | |
| tag = f" · pp. {it['pages']}" if it.get("has_pages") else " · _no pages_" | |
| lines.append(f"{indent}{label}{tag}") | |
| choices.append((label[:90], i)) | |
| md = (f"### Agenda items\n_{len(choices)} reportable item(s) · {res.get('page_count')} " | |
| f"packet pages · parsed from the {res.get('source')}_\n\n" + " \n".join(lines)) | |
| state = {"upload_id": res["upload_id"], "items": items} | |
| return md, gr.update(choices=choices, value=None), state | |
| def _view_pages(state, item_idx): | |
| if not state or item_idx is None: | |
| return [], "_Pick an agenda item, then view its packet pages._" | |
| items = state["items"] | |
| if item_idx < 0 or item_idx >= len(items): | |
| return [], "_That item is no longer available — re-parse the packet._" | |
| from webapp import pdf_view | |
| it = items[item_idx] | |
| try: | |
| images, caption = pdf_view.render_item_pages( | |
| state["upload_id"], _item_titles(items), item_idx, | |
| int(it.get("start") or 0), int(it.get("end") or 0), | |
| ) | |
| except Exception as e: # noqa: BLE001 | |
| return [], f"_Couldn't render the packet pages: {type(e).__name__}: {e}_" | |
| return images, caption | |
| def _summarize(state, model): | |
| if not state: | |
| yield "_Upload a packet, then click Summarize._" | |
| return | |
| head = "## Agenda summary\n\n" | |
| for frame in backend.summarize_agenda(state["upload_id"], model): | |
| if frame["stage"] == "error": | |
| yield head + f"❌ {frame['message']}" | |
| elif frame["stage"] == "working": | |
| yield head + f"⏳ {frame['message']}" | |
| else: | |
| meta = f"_Summarized by `{frame['model']}` · {frame['message']}_\n\n" | |
| yield head + meta + frame["summary"] | |
| def _item_report(state, item_idx, question, thoroughness, engine, model): | |
| """Generate a report scoped to the selected agenda item.""" | |
| if not state or item_idx is None: | |
| yield "_Pick an agenda item, enter a question, then Generate item report._" | |
| return | |
| items = state["items"] | |
| if item_idx < 0 or item_idx >= len(items): | |
| yield "_That item is no longer available — re-parse the packet._" | |
| return | |
| it = items[item_idx] | |
| item_label = f"{it['number']} {it['name']}".strip() or "(untitled item)" | |
| head = f"## {item_label}\n\n**Question:** {question}\n\n" | |
| for frame in backend.generate_agenda_item_report( | |
| state["upload_id"], _item_titles(items), item_idx, question, | |
| int(it.get("start") or 0), int(it.get("end") or 0), | |
| int(thoroughness), engine, model, | |
| ): | |
| if frame["stage"] == "error": | |
| yield head + f"❌ {frame['message']}" | |
| elif frame["stage"] == "working": | |
| pct = int(frame.get("frac", 0) * 100) | |
| yield head + f"⏳ [{pct}%] {frame['message']}" | |
| else: | |
| meta = f"_Reported by `{frame['model']}` · {frame['message']}_\n\n" | |
| yield head + meta + frame["report"] | |
| def _agent(state, question, model): | |
| """Stream one Agent Mode turn as a running markdown transcript (single-turn).""" | |
| if not state: | |
| yield "_Upload a packet first._" | |
| return | |
| q = (question or "").strip() | |
| if not q: | |
| yield "_Ask a question to begin._" | |
| return | |
| head = f"**You:** {q}\n\n---\n" | |
| body = "" | |
| yield head + "_⏳ reading the agenda…_" | |
| for fr in backend.agent_chat(state["upload_id"], [{"role": "user", "content": q}], model): | |
| st = fr["stage"] | |
| if st == "thinking": | |
| body += f"\n*§ {fr['text']}*\n" | |
| elif st == "tool_call": | |
| args = ", ".join(f"{k}={v}" for k, v in (fr["args"] or {}).items()) | |
| body += f"\n`→ {fr['tool']}({args})`\n" | |
| elif st == "tool_result": | |
| body += f"\n<sub>↳ {fr['result'][:160]}…</sub>\n" | |
| elif st == "answer": | |
| body += f"\n\n**Agent:** {fr['text']}\n\n_— {fr['model']}_" | |
| elif st == "error": | |
| body += f"\n\n❌ {fr['text']}" | |
| yield head + body | |
| def build_demo() -> gr.Blocks: | |
| model_choices = [("Gemma 4 E4B — fast", "e4b"), ("Gemma 4 26B — detailed", "26b")] | |
| with gr.Blocks(title="Agenda Parser") as demo: | |
| doc_state = gr.State(None) | |
| gr.Markdown( | |
| "# Agenda Parser\n" | |
| "Upload a meeting **agenda-packet PDF**, give the agenda (table-of-contents) " | |
| "page range, and parse it into items mapped to their backup pages — then " | |
| "summarize, report on an item, view its pages, or ask the agent. Model runs " | |
| "in-process on ZeroGPU." | |
| ) | |
| with gr.Row(): | |
| with gr.Column(scale=2): | |
| gr.Markdown("### 📄 Upload") | |
| file_in = gr.File(label="Agenda packet PDF", file_types=[".pdf"], type="filepath") | |
| pages_in = gr.Textbox(label="Agenda pages", value="1-3", placeholder="e.g. 1-3") | |
| model = gr.Dropdown(choices=model_choices, value="e4b", label="Model") | |
| upload_btn = gr.Button("Upload & parse", variant="primary") | |
| items_md = gr.Markdown("_Upload a packet to load its agenda items._") | |
| item_dd = gr.Dropdown(choices=[], label="Agenda item") | |
| with gr.Column(scale=3): | |
| with gr.Tab("Summarize"): | |
| sum_btn = gr.Button("Summarize agenda", variant="primary") | |
| sum_md = gr.Markdown("_Upload a packet, then click Summarize._") | |
| with gr.Tab("Agenda Item Report"): | |
| report_q = gr.Textbox( | |
| label="Your question", lines=2, | |
| placeholder="e.g. What is being approved, and what does it cost?", | |
| ) | |
| rep_engine = gr.Dropdown( | |
| choices=[("Semantic search — fastest", "single"), | |
| ("Full read — every section", "mapreduce")], | |
| value="single", label="Reading method", | |
| ) | |
| thoroughness = gr.Slider( | |
| 10, 120, value=50, step=10, | |
| label="Thoroughness (context fed / sections mined)", | |
| ) | |
| rep_btn = gr.Button("Generate item report", variant="primary") | |
| rep_md = gr.Markdown( | |
| "_Pick an agenda item, ask a question, then Generate item report._" | |
| ) | |
| gr.Markdown("---\n#### 📄 Packet pages for this item") | |
| pages_btn = gr.Button("View packet pages for this item") | |
| pages_info = gr.Markdown( | |
| "_Pick an agenda item, then view its packet pages._" | |
| ) | |
| pages_gallery = gr.Gallery( | |
| label="Agenda packet pages", columns=2, height=520, | |
| object_fit="contain", show_label=False, | |
| ) | |
| with gr.Tab("Agent"): | |
| gr.Markdown( | |
| "Ask in plain language — the agent reads this agenda packet " | |
| "and shows its work." | |
| ) | |
| agent_q = gr.Textbox( | |
| label="Your question", lines=2, | |
| placeholder="e.g. What's on this agenda? · " | |
| "Search the packet for the budget.", | |
| ) | |
| agent_btn = gr.Button("Ask the agent", variant="primary") | |
| agent_md = gr.Markdown("_Upload a packet, then ask._") | |
| upload_btn.click(_upload, [file_in, pages_in], [items_md, item_dd, doc_state]) | |
| sum_btn.click(_summarize, [doc_state, model], sum_md) | |
| rep_btn.click(_item_report, | |
| [doc_state, item_dd, report_q, thoroughness, rep_engine, model], | |
| rep_md) | |
| pages_btn.click(_view_pages, [doc_state, item_dd], [pages_gallery, pages_info]) | |
| agent_btn.click(_agent, [doc_state, agent_q, model], agent_md) | |
| return demo | |
| # Module-level Blocks so HF's Gradio SDK can auto-detect `demo` when this file is | |
| # the Space app_file. Cheap to build (no model load). | |
| demo = build_demo() | |
| def main() -> None: | |
| if os.getenv("LLM_BACKEND", "remote").strip().lower() == "local": | |
| from webapp import local_llm | |
| local_llm.prefetch_models() | |
| print("[startup] models ready.", flush=True) | |
| demo.launch( | |
| server_name=os.getenv("GRADIO_SERVER_NAME", "0.0.0.0"), | |
| server_port=int(os.getenv("GRADIO_SERVER_PORT", "7860")), | |
| ) | |
| if __name__ == "__main__": | |
| main() | |