agenda-parser / webapp /app_blocks.py
rdubwiley's picture
Deploy Agenda Parser
e94c043 verified
Raw
History Blame Contribute Delete
10.8 kB
"""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 = "&nbsp;" * (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()