Skip to content

Repository files navigation

NC Pollbook

tests docker-publish

NC Pollbook is an exploratory Django web app for importing and analyzing North Carolina State Board of Elections (NCSBE) voter registration and history data with LLMs.

It combines a Django/PostgreSQL ETL pipeline and materialized views with a Pydantic AI SQL agent that answers analytical questions over the voter dataset in CLI and web chat interfaces.

Built with Django 6.x, PostgreSQL 18, and django-pgviews-redux for materialized views.

Setup

Prerequisites: Python 3.14+, PostgreSQL 18, uv

# Install dependencies
uv sync

# Configure database (defaults to postgresql://postgres@localhost:5432/ncpollbook)
export DATABASE_URL=postgresql://user:password@localhost:5432/yourdb

# Apply migrations
uv run manage.py migrate
uv run manage.py sync_pgviews

# Create superuser (optional)
uv run manage.py createsuperuser

Loading Data

# Download NCSBE files and load into PostgreSQL, then refresh materialized views
uv run manage.py ncsbe etl

# Only refresh materialized views (skip download/load)
uv run manage.py ncsbe etl --refresh-only

# Inspect the first 100 rows of each source file
uv run manage.py ncsbe peek

Data is cached in scratch/data/ after the first download.

Model Providers

Models are configured via the Django admin under Agent > Tool Models. Load the default fixture to get started:

uv run manage.py loaddata agent_models

This configures lmstudio:mistralai/ministral-3-3b for the voter_agent tool and bedrock:us.anthropic.claude-haiku-4-5-20251001-v1:0 for sql_gen.

LM Studio (local, default)

  1. Download and install LM Studio.
  2. Search for and download mistralai/ministral-3-3b (the default model).
  3. Start the local inference server (listens on http://localhost:1234/v1):
lms server start

No API key is required — LM Studio is accessed with the placeholder key lm-studio.

AWS Bedrock

Set the bearer token before starting the server:

export AWS_BEARER_TOKEN_BEDROCK=<your-token>

Model names use the bedrock: prefix (e.g. bedrock:us.anthropic.claude-sonnet-4-6).

Anthropic API

Set the API key before starting the server:

export ANTHROPIC_API_KEY=<your-key>

Model names use the anthropic: prefix (e.g. anthropic:claude-sonnet-4-6).

SQL Agent (OpenAI-Compatible API)

An OpenAI-compatible API (/v1/chat/completions, /v1/models) served by django-ninja and Daphne (ASGI). Point LibreChat or any OpenAI-compatible client at http://<host>:8000/v1 with model voter-agent.

Optionally protect the API with an API key by setting AGENT_API_KEY in the environment. Clients send it as Authorization: Bearer <key>.

# Start the async API server (serves API, Django admin, and OpenAPI docs on port 8000)
uv run manage.py runserver

# Health endpoints (no auth required)
# GET /health  — liveness probe
# GET /ready   — readiness probe (checks database connectivity)
# Django admin available at http://127.0.0.1:8000/admin/
# OpenAPI docs available at http://127.0.0.1:8000/api/docs

LibreChat

To run LibreChat locally, follow the Local Installation - Docker guide to clone the repo.

Then we need to configure LibreChat to use our local API. First we create a custom config YAML file:

# file: librechat.yaml
version: 1.3.7
cache: true

endpoints:
  custom:
    - name: "Django-Backend"
      # The URL where your Django app serves the OpenAI-compatible API
      baseURL: "http://host.docker.internal:8000/v1" 
      # Reference a variable in your .env file
      apiKey: "apikey"
      models:
        # List the models your Django app supports
        default: ["voter-agent"] 
        # Set to true if your Django app has a /models endpoint
        fetch: true 
      titleConvo: true
      modelDisplayLabel: "NC Voter Data Agent"

Then we create a compose override file to mount the config and set the environment variable:

# file: docker-compose.override.yml

services:
  api:
    volumes:
    - type: bind
      source: ./librechat.yaml
      target: /app/librechat.yaml

Start the server with the override:

docker compose up -d

Now open the LibreChat UI at http://localhost:3080/login.

As noted in the docs, the first account you register becomes the admin account. There are no default credentials.

SQL Agent (CLI)

A terminal alternative to the web UI with step-by-step output and a per-run summary of model response time, tool execution time, and tokens/second.

# Interactive mode — type questions at the prompt, quit to exit
uv run manage.py agent cli

# Single question mode
uv run manage.py agent cli -q "how many active voters are in Durham County?"

# Inspect agent system prompts
uv run manage.py agent prompts
uv run manage.py agent prompts --name sql_gen
uv run manage.py agent prompts --name voter

Each run prints:

  • Thinking panels — the model's internal reasoning (when supported by the model)
  • → tool_name(args) — each tool call as it is issued
  • ↩ tool result — a truncated preview of each tool response
  • Answer — the final markdown answer
  • Run summary table — step name, elapsed time, input/output tokens, and tokens/second

Sample questions:

  • How many people are registered to vote in Durham County?
  • What is the breakdown of party affiliation among voters aged 18–25 vs 65+?
  • What percentage of voters who voted in the 2020 General also voted in the 2022 Primary?

The agent has two tools:

  • run_sql_query — generates and executes a SQL query, returns a markdown table
  • run_python_code — executes LLM-written Python in a secure Monty sandbox, with run_sql_query available for chaining multiple queries

Testing and Linting

# Run tests (LLM evals skipped by default)
uv run pytest

# Run only LLM-judge evals (requires ollama or another local model running)
uv run pytest -m llm

# Run all tests including LLM evals
uv run pytest -m ''

# Run linters
uv run pre-commit run --all-files

Releasing

  1. Bump the version in pyproject.toml:

    uv version --bump patch  # or --bump minor/major
  2. Create a new release on GitHub:

    • Tag the version (e.g., v0.2.1)
    • Add the release notes
    • Publish the release

    When the release is published, CI automatically builds and publishes a new Docker image.

Deployment

See DEPLOYMENT.md for Kubernetes/Ansible deployment instructions.

About

An exploratory Django web app for importing and analyzing North Carolina State Board of Elections (NCSBE) voter registration and history data with LLMs.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages