At a glance
What it is
The capstone team's web app tracked congressional bills, lobbyist spending and campaign contributions. I was in charge of the backend, and I built PolicyAI, the assistant that lets anyone ask about that data in plain English instead of writing SQL or building a report. It lives on every page of the app as a chat window.
The journey
From a chat box to a grounded analyst
-
September 2025
The problem: the data was there, the answers weren't
Policy teams need quick answers from congressional data: what a bill does, who is lobbying on it, how much a campaign has raised. The app already collected all of it, but getting an answer out meant knowing exactly where to look, or writing SQL.
The goal for my part was simple to say: ask a question in plain English, get a correct answer drawn from the data.
-
The foundation
Curated data to stand on
The app's data follows a Medallion design. Bronze holds raw pulls from public APIs: Congress.gov for bills, members and laws, the Senate's lobbying disclosures, and the FEC for campaign finance. Silver cleans and types them. Gold joins them into tables an analyst can trust. For example, each bill appears once with its summary, whether it became law, its latest action and a readable ID like “H.R. 10-119”.
That Gold layer is what the assistant answers from, so the model only ever sees clean, curated records.
-
Late September
A fluent model that knew nothing about us
The first version was a plain chat model. It could talk about Congress in general, but it had never seen our database, so it had nothing to ground an answer in. That's the gap retrieval closes: find the right rows first, then answer only from them.
-
October
Retrieval, and a bug that looked like a bad model
I built a first retrieval prototype over exported tables, then moved to a stronger embedding model. Search still came back wrong: a query for bill H.R. 3746 returned S. 3949.
The model wasn't the problem. The stored vectors and the question's vector came from differently sized embeddings. Once both sides matched, H.R. 3746 ranked first out of 715 bills.
What I foundWhen search results look random, check that both sides were embedded the same way before blaming the model.
-
November
Search inside the Gold tables
I moved search into the database itself. Every row in seven Gold tables got a 768-number embedding stored next to it in Postgres with pgvector. A question is embedded the same way, each table returns its closest rows, and the model receives them as numbered sources with instructions to cite them. The old CSV path was deleted.
Queries are read-only, search is limited to the curated Gold tables, and a user's question is never written to the database. One practical fix along the way: some bill summaries were long enough to crash the embedder, so row text is capped at 12,000 characters.
-
November
Everything on device
I set one rule for the stack: every model runs locally, and nothing is pulled from the network when someone asks a question. The language model (a 20-billion-parameter open model) and the embedding model run through Ollama, in Docker next to Postgres, the API and the web app.
The whole system runs on one machine. Questions, data and answers never leave it.
What I foundKeeping it local came down to architecture, not sacrifice. Public data comes in through ingestion, and at question time nothing goes out.
-
December 2025
The result
A working assistant, built into every page of the app, that answers questions about bills, lobbying and campaign finance in plain English from the Gold tables, running entirely on device. We demoed it to the client as part of the capstone. Here's how it got there:
- v1 · ChatA plain chat model. Fluent, but it had never seen our data.
- v2 · CSV searchFirst retrieval prototype over exported tables.
- v3 · Better embeddingsA stronger embedding model, and a bug that made search look random.
- v4 · Gold searchSearch moved into the Gold tables in Postgres, one row per source.
- v5 · On deviceModel, search, data and app in Docker on one machine.
If I picked it up again
What I'd build next
- An evaluation set of real analyst questions with known answers, so accuracy is measured rather than spot-checked.
- A vector index on each Gold table, so search stays fast as the tables grow.
- More of the Gold layer made searchable, so every table the app tracks can be asked about.