Skip to content

Repository files navigation

District Brain

CI License: MIT

Your district's entire knowledge, on your own hardware, answering only to your people.

A teacher asks when a confiscated phone is returned; the answer cites Board Policy 5136 with the exact passage, its tier, and its last-updated date

A real answer from a local llama.cpp model over the synthetic demo corpus. Every claim carries a citation; the sidebar shows exactly which documents this role can search.

District Brain is a locally hosted, retrieval-augmented AI assistant for K-12 school districts. Staff ask questions in plain language — "What's our device-confiscation policy?", "What time does early release end on Wednesdays?" — and get answers grounded in the district's own documents, with a citation to the exact passage on every claim.

It is built for the small district that has no vendor path to this: one spare box in a closet, a consumer GPU or none, a tech coordinator rather than a platform team, and a FERPA obligation that makes "send it all to a cloud API" a non-starter.

Status: Tier 1 (policies, handbooks, schedules, board minutes — no student data) is complete and running in daily staff use against a local llama.cpp model. Tier 2 (curriculum maps and lesson plans) is under construction: the data model and tier guard have shipped; ingestion, standards resolution, and relationship-based access are queued. See Roadmap.

Why it's different

  • Access control lives in retrieval, not in the prompt. A user's question only ever searches documents their role is allowed to see. Documents outside their scope never reach the language model, so they cannot leak — not by jailbreak, not by prompt injection hidden inside a document.
  • Local-first. Documents, embeddings, the vector index, and the audit log live on a server the district owns. The default answer engine requires no external service.
  • Every question is logged. Who asked, what was asked, exactly which passages were retrieved, and the answer as delivered — admin-viewable and CSV-exportable for the board.
  • Citations on every answer. Each claim links to the source passage and shows the document's last-updated date. When retrieval finds nothing relevant, the assistant says "I don't have that in my sources" instead of guessing.
  • Tiered corpus, enforced physically. Student records are a separate, board-approved project stage; until then the database itself refuses to store a Tier 3 document.
  • Boring, cheap stack. Django, PostgreSQL + pgvector, a small local embedding model, server-rendered pages that are fast on a Chromebook. Runs on one modest box.

How a question is answered

sequenceDiagram
    participant U as Staff member
    participant W as Django view
    participant R as retrieval.py
    participant DB as Postgres + pgvector
    participant L as LLM backend
    participant A as Audit log

    U->>W: question (session or Google SSO)
    W->>R: retrieve(user, question)
    R->>DB: visible_documents(user) ∩ nearest chunks
    Note over R,DB: role filter is part of the SQL —<br/>there is no unscoped search path
    DB-->>R: top-k chunks within distance cutoff
    alt nothing relevant
        R-->>W: [] → "I don't have that in my sources"
    else
        W->>L: system prompt + only the retrieved passages
        L-->>W: answer with [n] markers
        W->>W: drop any [n] that points at no retrieved passage
    end
    W->>A: user, role, question, every passage, answer — in one transaction with the reply
    W-->>U: answer + citations (title, last-updated date, download link)
Loading

The security boundary is one function, visible_documents(user) in chat/retrieval.py. Retrieval, the scope card in the UI, and the authenticated document-download view all go through it; nothing else touches chunks.

Quick start (development)

Prerequisites: Python 3.12+, uv, Docker with Compose.

git clone https://github.com/ZachRouan/district-brain.git && cd district-brain
cp .env.example .env            # then set SECRET_KEY (command in the file's comment)
docker compose up -d            # PostgreSQL + pgvector on localhost:54320
uv sync                         # install Python dependencies
uv run python manage.py migrate
uv run python manage.py seed_demo   # roles, demo users, synthetic sample corpus
uv run python manage.py runserver 8800

Open http://localhost:8800 and log in as one of the demo users printed by seed_demo. All sample documents are synthetic — a fictional district — so the demo works before you load a single real document. The default answer engine is a deterministic mock; swapping in a local llama.cpp model is one .env setting (runbook §8).

Full setup, operations, and hand-off documentation: docs/runbook.md.

Project layout

Path What it is
accounts/ Roles and users; role assignment drives everything retrieval may see
corpus/ Documents, chunks, embeddings, ingestion pipeline (manage.py ingest), Tier 2 curriculum metadata
chat/ Retrieval (the security boundary), LLM backend abstraction, conversations, citations
audit/ Append-only log of every prompt, retrieval, and response
districtbrain/ Django project: settings (all from environment), URLs
templates/, static/ Server-rendered UI — no build step, no JS framework
sample_corpus/ Synthetic Tier 1 documents for the demo seed
tests/ pytest suite; the scoping and audit invariants are tested end-to-end
docs/runbook.md Setup → operations → hand-off runbook for the next coordinator
docs/security-hardening-tier1.md What a security review found and how each finding was closed
docs/design/, docs/plans/ Tier 2 design spec and implementation plans

Security model (short version)

  1. Role scoping is enforced in the retrieval query itself (chat/retrieval.py); there is no user-facing code path that searches unscoped chunks. retrieve() also clamps its own top_k and distance arguments, so no caller can widen a search. The operator console (Django admin) is not role-scoped, so it opens only for superusers.
  2. Corpus documents are treated as untrusted input. Instructions embedded in a document can, at worst, influence the wording of an answer for users already entitled to that document — they cannot widen retrieval scope. Each passage reaches the model inside its own <source> element, so a document cannot forge a passage header and put words under another document's name. The test suite proves both with poisoned documents.
  3. Original files are served only through an authenticated, per-user-scoped download view; MEDIA_ROOT is never mounted as a URL, even in DEBUG.
  4. Every exchange is audited atomically with the answer, including refusals and engine failures.
  5. Citation markers the model invents (pointing at a passage that was not retrieved) are stripped before the answer is stored or shown.
  6. A database CheckConstraint refuses Tier 3 (student-record) documents by any code path, and every embedded document records which embedder produced it, so a model swap can't silently corrupt similarity search.
  7. Retrieval is narrower than storage: Tier 2 documents are searchable only when tagged as district-consensus curriculum. Teacher-authored plans stay invisible until the relationship-based access engine exists.
  8. Questions are length-capped and rate-limited per user before anything is stored, so one account cannot monopolise the single answer engine.

The full record of the Tier 1 security review and its fixes is in docs/security-hardening-tier1.md. Found a hole? See SECURITY.md.

Development

uv run pytest                 # full suite; needs the compose database up
uv run ruff check . && uv run ruff format --check .
uv run python manage.py makemigrations --check --dry-run

CI runs the same three steps on every push. Tests use a deterministic hash embedder and the mock LLM backend, so they need no model download and no network.

Roadmap

Tier Corpus Access model Status
1 Policies, handbooks, schedules, procedures, board minutes Role-based Shipped
2 Curriculum maps, pacing guides, unit and lesson plans Role-based, then relationship-based (a teacher sees the prior-year plans of her current students' feeder classrooms) via a pluggable SIS adapter In progress — data model shipped
3 Student records, grades, IEPs Relationship-based; student sees only their own Not started; requires Tiers 1–2 in clean operation and board approval

Design for Tier 2: docs/design/2026-07-03-tier2-lesson-plan-schema.md.

License

MIT.

About

Locally hosted, role-scoped RAG assistant for K-12 districts — policies, handbooks, and schedules with chunk-level citations and a full audit trail

Topics

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages