Inspiration

Argus was built to make it easy to keep track of what's going on locally. There are dozens of sources of information and data and it's difficult to keep track of it all on a regular basis. If I'm interested in a new topic it's a whole new maze to get information.

What it does

Argus watches San Francisco's civic apparatus continuously and takes action rather than describing it. It polls 12 sources that share no common format — Legistar, SFGovTV/Granicus, five departmental Drupal sites, two iCal feeds, three news RSS feeds — notices new or amended agendas, parses the packets, and uses Gemini to judge whether any item genuinely touches an issue you track, storing why it matched. Then it writes the hearing to your Google Calendar, keeps it correct when the meeting moves, emails a weekly digest, and follows up afterwards using the meeting transcript to report what was decided.

There's also a voice interface (Gemini Live) with 11 tools, so you can ask "what's coming up on bike lanes?" and it queries the corpus mid-conversation.

How we built it

Gemini 3.7 Flash for synthesis and outcome extraction, Gemini 3.6 Flash for match judgement, gemini-embedding-001 for retrieval, and Gemini Live for voice — all through the Google GenAI SDK. PostgreSQL 17 + pgvector, React Router 7, and the whole Google Cloud deployment defined in Terraform (Cloud Run, Cloud SQL, Scheduler, Secret Manager, Monitoring).

The LLM writes content; code decides actions. No model call creates, moves, or cancels a calendar event. The model can raise a proposal card; the write happens when a person presses a button. A model that decides to "just do it" has nothing to call.

The model names a thing; the server supplies the facts. A calendar proposal passes a meeting_id and nothing else — time, place and title are read from the database, so the model cannot invent a hearing that doesn't exist.

Parse deterministically wherever the markup allows. Agendas go through four PDF parsers and an HTML parser, not a model. A layout change surfaces as "no items found", never as confidently wrong data. Gemini is reserved for judgement, which is the thing it's actually better at than a regex.

Challenges we ran into

  1. The plan was built on SF's Legistar API. It turned out to be a frozen 2018 archive. /events returns HTTP 400 from a server-side misconfiguration, and zero legislation exists after December 2018. Everything had to be rebuilt around Granicus, which sends no ETag, so a synthetic page-hash validator was added to avoid re-parsing 814 unchanged meetings every cycle.

  2. BoardDocs returns 403 from CloudFront, SFCTA sits behind Cloudflare. We could have spoofed a browser user-agent, but we chose not to. Those sources are supplied by hand through the same deterministic parsers, tagged kind='manual' so their provenance is visible.

  3. The bugs that mattered were silent ones, and every one was found by running the thing, not by reading the code:

  4. "Who is the mayor of San Francisco" returned nothing useful — the answer was in the database the whole time. Full-text search ANDs its terms, so a 20,000-character department catalogue outranked the mayor's own profile page and the excerpt shown was about an unrelated "Mayor's Office of…". Fixed with hybrid retrieval: keyword and vector search run in parallel and are fused by reciprocal rank.

  5. Cost tracking under-reported spend by 3.6× because thinking tokens bill as output but are reported in a separate field.

  6. A link-scraping regex excluded a trailing slash and matched zero of 237 links, then reported "layout may have changed" — blaming the publisher for our own bug.

Accomplishments that we're proud of

Following a single issue through San Francisco's government is a multi-step chore: watch a dozen publishers that share no format or schedule, notice that an agenda quietly changed, open the PDF packet, work out whether any of the 104 items actually touches what you care about, add the hearing into your calendar with the right room number, find out afterwards what was decided, and repeat every week.

Argus actually acts to make all of this easier: real events on a real calendar, a real weekly digest through the Gmail API, on a schedule, whether or not anyone is watching. Additionally, failures are contained and visible: every cycle step is isolated and rolls back independently, so one refusing publisher can't take down the run. Lastly, the costs are measured: every model call records tokens, latency and dollars. Total spend across the development of the project: $0.91.

What we learned

  • Verify, don't predict. Nearly every serious bug was invisible to code review and to the type checker. terraform validate and terraform plan both pass on a Cloud Run job that reads a secret it hasn't been granted; only running it fails. We ended up writing checks for the things the standard tooling structurally cannot see.

  • Deterministic beats clever. Regex parsing is free, testable offline, and fails loudly. Using a model to extract structured data that's already structured buys unreliability at a cost.

  • "Unknown" and "zero" must stay distinguishable. An unpriced model returns NULL, never 0.00 — because a zero silently sums into a spend total and reports something that didn't happen.

What's next for Argus

The plan is to add more cities. Every adapter is config plus a parser, and the four dedup gates are city-agnostic. Oakland and San José publish through the same Granicus and Legistar platforms.

Built With

Share this project:

Updates

Submission history