Spaces:
Running on Zero
Running on Zero
| title: Agenda Parser | |
| emoji: π | |
| colorFrom: indigo | |
| colorTo: blue | |
| sdk: gradio | |
| sdk_version: 6.18.0 | |
| app_file: app.py | |
| pinned: false | |
| short_description: Upload an agenda packet PDF β parse, summarize, report | |
| tags: | |
| - track:backyard | |
| - sponsor:modal | |
| - achievement:offgrid | |
| - achievement:welltuned | |
| - achievement:offbrand | |
| - achievement:llama | |
| - achievement:sharing | |
| - achievement:fieldnotes | |
| # Introducing Agenda Parser: Making Sense of Local Government | |
| Social media post can be found [here](https://bsky.app/profile/rdubwiley.bsky.social/post/3mobuvtk3js2u) | |
| Demo video can be found [here](https://youtu.be/-XbfHguXQxs) | |
| ## The Problem | |
| Local governments in the United States (municipality or county) publish an agenda before every public meeting, along with all of the supporting materials β staff reports, contracts, studies, maps, budget tables. Bundled together these are called agenda packets, and they routinely run to hundreds of pages. The agenda itself is a few pages of table-of-contents at the front, and everything you'd actually want to know about an item is buried somewhere in the back. | |
| Different people hit this same wall in different ways: | |
| - **The council or board member** prepping for Tuesday's meeting has to be ready to vote on every item, which in theory means reading the whole packet every time. | |
| - **Clerks and staff** field a steady stream of "where in the packet is that?" and "what does this item actually do?" questions, from both officials and the public. | |
| - **The resident** showing up for public comment usually cares about exactly one item but still has to dig through a 300-page PDF to find the ten pages that matter. | |
| - **The local journalist or civic watchdog** is scanning many meetings across many bodies, looking for the items worth a closer read. | |
| None of these people need a chatbot that has vaguely read the internet. They need the packet in front of them made navigable and answerable. | |
| ## How we solve it | |
| [Agenda Parser](https://huggingface.co/spaces/build-small-hackathon/agenda-parser) is a Hugging Face Space where you upload an agenda-packet PDF, mark which pages are the agenda (the table of contents), and the app parses it into agenda items mapped to their backup pages in the packet. When the PDF has bookmarks the bookmarks drive the mapping. When it doesn't, the app falls back to matching the agenda text against the packet. The item titles and page numbers are all editable in case pages don't line up exactly. | |
| That parsing step alone is a simple programming win that needs no AI: a clean list of items, each one a click away from its actual pages. On top of that structure sit three AI resources, roughly one per persona: | |
| 1. **Ask the packet** β open any item's pages and ask a question of just those pages. You pick the reading method (semantic search for speed, or a full read of every section) and a depth-of-research slider. This is the official and resident's tool: "What is being approved, and what does it cost?" answered from the ten pages that matter. | |
| 2. **The Agent tab** β a small tool-calling agent with the run of the whole packet (list the items, pull an item's text, search, summarize, build a report). This is for the official or staffer who wants "search the packet for anything about the budget" or a synthesized report over the entire meeting. | |
| 3. **The Cornell LII tab** β a second research agent pointed at [Cornell LII](https://www.law.cornell.edu/) and the [Michigan Compiled Laws](https://legislature.mi.gov/Laws/ChapterIndex), for the low-stakes legal questions that come up constantly in local government: what notice does the Open Meetings Act require, what's exempt from public-records law, what ADA requirements apply to our meetings. | |
| The models behind all of this are small on purpose. Everything runs in-process on the Space's ZeroGPU hardware via llama.cpp β no external API. I started from Google's Gemma 4 models (the E4B and the 26B-A4B), then teacher-distilled tool-calling traces from larger open-weight models into them with an SFT pass (plus a GRPO step for the top tier). All of the heavy computation behind those models β the fine-tuning runs, the evaluations, and local model testing β ran on [Modal](https://modal.com/). The result is three selectable tiers in the app β [agenda-parser-lite](https://huggingface.co/build-small-hackathon/agenda-parser-lite), [agenda-parser-medium](https://huggingface.co/build-small-hackathon/agenda-parser-medium), and [agenda-parser-high](https://huggingface.co/build-small-hackathon/agenda-parser-high) so you can trade speed for quality with a dropdown. With the data constrained to one packet and a handful of well-shaped tools, small models are genuinely enough for this job. | |
| One note on persistence: your parsed agendas, edited page ranges, answers, and agent runs are saved in your browser (localStorage), but the packet PDF itself lives in the Space's ephemeral storage. If the Space restarts, a saved agenda will ask you to re-upload the PDF β your items, edits, and answers are all kept and rebind to the fresh upload. | |
| ## Demo | |
| Try it live: [build-small-hackathon/agenda-parser](https://huggingface.co/spaces/build-small-hackathon/agenda-parser). You don't need to bring your own PDF β a few sample packets (public Michigan meeting agendas) are bundled right on the upload screen. | |
| Here's the parsed agenda after upload, with each item's page range inline and editable: | |
| Clicking an item's **Pages** button opens the packet pages alongside the "Ask the packet" inquiry desk: | |
| An agent run over the whole packet: | |
| And a Cornell LII run on a local-government law question: | |
| ## Usage Guide | |
| 1. **Upload a packet.** Drag a PDF in (or pick a sample), enter the agenda's table-of-contents pages (e.g. `1-3`), and hit **Upload & parse**. | |
| 2. **Check the items.** Each parsed item shows its mapped page range (e.g. `pp. 24β35`). Click the range to adjust it, or the pencil to rename an item β your edits are flagged and preserved. | |
| 3. **Ask about one item.** Hit **Pages** on an item to open the page viewer, then use **Ask the packet** on the right: type a question (or pick a suggested one), choose semantic search vs. a full read, set the depth slider, and ask. | |
| 4. **Ask about the whole packet.** Switch to the **Agent** tab for plain-language questions over everything ("What's on this agenda? List the main items."), with each tool step shown as it runs. The **Report** sub-tab builds a synthesized report over the full packet. | |
| 5. **Ask about the law.** The **Cornell LII** tab handles state and federal questions β open-meetings notice requirements, public-records exemptions, ADA obligations, procurement rules. | |
| 6. **Pick your model.** The dropdown in the sidebar switches between lite (fast), medium (detailed), and high (best). Saved agendas live in the sidebar too; if the Space has restarted since your upload, a banner offers **Restore packet** to re-upload and keep everything you've done. | |
| As the app itself reminds you under every answer: this is generated text β confirm against the official record before relying on it. | |
| ## Merit Badges | |
| We believe this entry has earned all six bonus merit badges: | |
| - **Off the Grid** (local-first) β no cloud APIs. The Gemma 4 GGUFs run in-process on the Space's GPU; even the pivot away from an external data API means the whole app is the model and the PDF in front of you. | |
| - **Well-Tuned** (fine-tuned) β the app's three model tiers are our own fine-tunes published on the Hub: [lite](https://huggingface.co/build-small-hackathon/agenda-parser-lite), [medium](https://huggingface.co/build-small-hackathon/agenda-parser-medium), and [high](https://huggingface.co/build-small-hackathon/agenda-parser-high). | |
| - **Off-Brand** (custom UI) β a custom React + Tailwind frontend served through `gr.Server`, well past the default Gradio look. | |
| - **Llama Champion** (llama.cpp) β every model tier runs through the llama.cpp runtime as a GGUF. | |
| - **Sharing is Caring** (open trace) β our agent traces are public on the Hub: the [training traces](https://huggingface.co/datasets/build-small-hackathon/agenda-parser-tool-traces) and [example runs from the fine-tuned models](https://huggingface.co/datasets/build-small-hackathon/agenda-parser-models-example-agent-traces). | |
| - **Field Notes** β we wrote up what we built and learned in [Agenda Parser: Field Notes](https://huggingface.co/blog/build-small-hackathon/agenda-parser-field-notes). | |
| ## Built on Modal | |
| The app ships as a tiny GGUF that runs on the Space's ZeroGPU, but *getting* to that GGUF took a lot of GPU time on hardware we don't own. [Modal](https://modal.com/) was the backbone of that work and was the entire development engine behind the models. Every step that needed a real GPU was a single `modal run` away, billed per-second, with the weights cached on a shared volume so nothing re-downloaded between jobs. The $250 in credits went a remarkably long way. | |
| Here is essentially everything that happened off-Space, and where it ran: | |
| **Training.** The three model tiers were fine-tuned as separate Modal jobs (LoRA SFT of Gemma 4 E4B on an **A100-80GB** (lite) and Gemma 4 26B-A4B on an **H100** (medium and high)), all on the tool-calling trace dataset. The top tier then got a second pass: per-step **GRPO** on an **H200**, scoring each JSON action against a verifiable reward (valid single action, real tool name, schema-valid args, pure JSON, correct tool match) and updating the policy with a KL penalty back to the SFT reference. Adapter-merge steps ran on an A100. The whole `download β train β merge` chain for a tier is one command. | |
| **Packaging.** Converting the merged bf16 weights to GGUF and quantizing them (Q8_0 for lite/high, Q4_K_M for medium) ran in **CPU-only Modal containers** that build llama.cpp from source, then push the finished GGUFs and their model cards straight to the Hub. The stock Gemma 4 base models were converted the same way so the eval could compare against them. | |
| **Serving for evaluation.** To A/B the fine-tunes against their base models, Modal stood up **six llama.cpp endpoints at once**, a base and a fine-tuned server for each of lite (A10G), medium, and high (A100-80GB), all scale-to-zero, so they cost nothing between eval runs. The stronger teacher and judge models that generated and scored training traces were served the same way through parameterized **vLLM endpoints** (OpenAI-compatible, tool-calling enabled). | |
| **Evaluation.** The held-out A/B harness (the Oakland-1570 packet plus the LII law tasks) drove those six endpoints in parallel β collecting valid-action rates, tool-error rates, and pass@k robustness from temperature-sampled completions, then running an LLM-as-judge for faithfulness and a position-swapped pairwise vote for the base-vs-fine-tuned win-rate. The whole scoreboard in the [field notes](https://huggingface.co/blog/build-small-hackathon/agenda-parser-field-notes) came out of this one Modal-backed run. | |
| **Development and testing.** Day-to-day model poking lived on Modal too an on-demand **A100 JupyterLab** for exploring Gemma 4's tool-calling, and a headless debug harness that executes the model notebook end-to-end on a GPU to catch breakage before a real training run. | |
| | What | Hardware | Job | | |
| |---|---|---| | |
| | SFT β lite | A100-80GB | LoRA fine-tune of Gemma 4 E4B | | |
| | SFT β medium / high | H100 | LoRA fine-tune of Gemma 4 26B-A4B | | |
| | GRPO β high | H200 | Reward-scored RL pass on the SFT'd high model | | |
| | Adapter merge | A100-80GB | Merge LoRA β full bf16 weights | | |
| | GGUF convert + quantize | CPU (8-core / 64 GB) | bf16 β Q8_0 / Q4_K_M, push to the Hub | | |
| | Eval serving | A10G / A100-80GB | 6 base-vs-fine-tuned llama.cpp endpoints, scale-to-zero | | |
| | Teacher / judge serving | A100-80GB | vLLM endpoints for trace generation + scoring | | |
| | A/B eval harness | CPU (drives the endpoints) | Held-out metrics, LLM-as-judge, pairwise win-rate | | |
| | Dev + testing | A100-80GB | GPU JupyterLab + headless notebook debug harness | | |
| Scale-to-zero is the quiet hero here: every GPU endpoint sleeps when idle, so a six-model evaluation costs only the minutes it actually runs, and the same volume-cached weights make cold starts cheap. That economic model is what made it reasonable to retrain, requantize, and re-evaluate three models repeatedly over a hackathon weekend. | |
| ## Resources | |
| - The Space: [build-small-hackathon/agenda-parser](https://huggingface.co/spaces/build-small-hackathon/agenda-parser) | |
| - The models: [agenda-parser-lite](https://huggingface.co/build-small-hackathon/agenda-parser-lite) Β· [agenda-parser-medium](https://huggingface.co/build-small-hackathon/agenda-parser-medium) Β· [agenda-parser-high](https://huggingface.co/build-small-hackathon/agenda-parser-high) | |
| - Training traces: [agenda-parser-tool-traces](https://huggingface.co/datasets/build-small-hackathon/agenda-parser-tool-traces) | |
| - Example agent runs from the fine-tuned models: [agenda-parser-models-example-agent-traces](https://huggingface.co/datasets/build-small-hackathon/agenda-parser-models-example-agent-traces) | |
| - Legal sources used by the LII agent: [Cornell LII](https://www.law.cornell.edu/) and the [Michigan Compiled Laws](https://legislature.mi.gov/Laws/ChapterIndex) | |
| - The full technical write-up β model choice, fine-tuning pipeline, GRPO rubric, and evals: [Agenda Parser: Field Notes](https://huggingface.co/blog/build-small-hackathon/agenda-parser-field-notes) | |
| - Our collection: [Agenda Parser](https://huggingface.co/collections/build-small-hackathon/agenda-parser) |