CPSC Compliance Assistant

Python FastAPI LangChain pgvector React AWS

Overview

A web app that answers US consumer product safety compliance questions in plain English, with every claim cited to the exact regulation, statute or CPSC guidance page it came from. Ask "Do I need a Children's Product Certificate for a wooden toy I import?" and you get a short answer with numbered citations. Click one and the source opens, scrolled to the highlighted passage.

The rules are hard to navigate because they come from three kinds of source that don't link to each other: binding regulations (16 CFR), the statutes Congress passed (CPSA and FHSA), and CPSC's plain-language guidance, which isn't binding. General-purpose chatbots answer from memory and cite nothing. This assistant answers only from the sources it retrieves, and labels each one as a Rule, a Law or Guidance.

Demo of the assistant: a compliance question streams back a cited answer, and clicking a citation opens the highlighted source passage
701 chunks from 30 source documents
16/17 eval questions where a binding rule or law reached the model (up from 10)
<3s to the first word of the answer
70 automated tests in CI

How it works

  1. Ingest. 16 CFR parts are pulled from the eCFR API, the CPSA and FHSA from govinfo, and 17 CPSC guidance pages are parsed from saved HTML. Each legal section becomes one chunk, and each chunk starts with a header naming its source, so citations point to exact sections.
  2. Embed. Chunks are embedded with Amazon Titan V2 and stored in Postgres with pgvector, behind an HNSW index.
  3. Retrieve. A question runs through vector search and BM25 keyword search at the same time. The two rankings are merged with reciprocal rank fusion, and the top six passages are balanced so FAQ-style guidance can't crowd out the binding legal text.
  4. Answer. Claude Haiku 4.5 on Bedrock writes the answer only from those six passages, and the answer streams to the browser. Code then checks every [n] citation, and an answer with missing or invalid citations becomes "not covered" rather than a guess.
Animated LangChain pipeline: ingest and chunk, Titan embeddings, PGVector store, hybrid retriever, classifier and answer LCEL chains, citation check and streaming, with fakes swapped in for tests

Guardrails

  • A Haiku classifier turns away off-topic questions and prompt-injection attempts before retrieval, and a distance threshold catches anything the sources don't cover.
  • Rate limits (per visitor, per IP and a global daily cap) are stored in Postgres, which keeps the model bill bounded.
  • A 22-question golden set measures retrieval. It showed that balancing sources raised binding-source recall from 10 to 16 of 17.

Tools

  • Backend: Python, FastAPI and Pydantic for the API and streaming (Server-Sent Events). LangChain for the vector store and model interfaces, with a custom retriever for hybrid search.
  • Data: Postgres 16 with pgvector for vectors, documents, usage counters and feedback. rank-bm25 for keyword search. httpx and lxml for parsing XML and HTML.
  • Frontend: React, Vite, TypeScript and Tailwind, with a sources library, a streaming answer view and a document viewer that highlights the exact passage.
  • Tooling: Docker Compose, Caddy as the reverse proxy, uv, ruff and pytest (including tests against a real pgvector instance), and GitHub Actions for CI/CD.

Cloud setup (AWS)

  • Amazon Bedrock: Titan Embeddings V2 for vectors and Claude Haiku 4.5 for answers and scope checks. No API key is involved, only IAM permissions.
  • EC2: a t4g.small (ARM) instance runs the Docker Compose stack: Caddy, the FastAPI container and Postgres. Postgres is never exposed to the internet.
  • S3 + CloudFront + ACM: the React site lives in a private bucket served through CloudFront, with a free TLS certificate. CloudFront also forwards /api/* to EC2, so the site and the API share one domain.
  • CloudFormation: two stacks describe the whole setup as code, one for the server and one for the public edge.
  • IAM, OIDC and SSM: no stored AWS keys anywhere. The server uses an instance role, and a push to main runs the tests, then GitHub Actions assumes a short-lived role via OIDC and uses SSM to deploy on the server, then smoke-tests the live site.
Animated request flow: browser, DNS, CloudFront with ACM, S3 for the site, then /api/ask through the firewall, Caddy and FastAPI to the Bedrock classifier, Titan, pgvector and BM25, and back as a streamed, citation-checked Claude answer

All of it runs for roughly $17 a month for the server, plus about $7 in model usage per thousand questions.

Why I built it

I built this to learn how to take an LLM application all the way from idea to production, not just a notebook demo. I wanted to understand retrieval-augmented generation properly: how chunking choices shape citations, why hybrid search beats vector search alone, and how to measure retrieval with an eval set instead of guessing.

It was also my first time owning a real cloud deployment end to end: infrastructure as code, least-privilege IAM, keyless CI/CD, TLS, CDN caching, rate limiting and a budget. Several lessons only showed up in production. My eval tuned the off-topic threshold to its own phrasing, but real users write shorter, vaguer questions, so the classifier became the main guard. Next on the list is hardening: locking the origin to CloudFront, replacing SSH with SSM Session Manager, and adding dependency and image scanning.

← Back to all projects