mrme77/dfars-assistant
DFARS Context Assistant
Ask cited questions over the DFARS regulation. The 1,466-page PDF is preprocessed once, locally into a structured, searchable index of section-level records. At query time the app retrieves only the most relevant sections and sends those — not the whole document — to the model.
The original regulatory text remains the authoritative source for every answer; the enriched metadata exists to help retrieval find the right sections and help the model orient itself.
Preview
A question resolves to a cited answer plus the exact DFARS sections it drew from:
Each retrieved section shows its identifier, page range, retrieval method, key topics, and the verbatim source text:
Why this is more reliable than a generic chatbot
- Answers are grounded in indexed DFARS source text, with section + page citations.
- Retrieval is controlled: exact clause lookup plus metadata-enhanced BM25.
- Expensive work (extraction, LLM enrichment, indexing) is done offline, so the hosted app stays lightweight.
Architecture at a glance
Data/DFARS.pdf
-> src/ingestion/extract_pdf.py # PDF -> page text
-> src/ingestion/detect_sections.py # page text -> section records
-> indexes/dfars_sections.jsonl # base index (id, title, pages, text)
-> src/preprocessing/enrich_sections.py# + LLM summaries, topics, obligations...
-> indexes/dfars_sections_enriched.jsonl # deployed index
-> src/retrieval/* # exact lookup + BM25 (+ optional vectors)
-> src/generation/build_context.py # compact, budgeted context package
-> app/streamlit_app.py / app.py # UI + OpenRouter answerSee docs/ARCHITECTURE.md for the full data flow and docs/ENRICHMENT.md for the metadata schema.
The deployed index
indexes/dfars_sections_enriched.jsonl (~6.7 MB, 1,444 sections) is the only artifact the hosted app needs. Each record is a SectionRecord (src/models.py):
section_id title part / subpart
page_start page_end original_text (authoritative source)
summary key_topics applies_when required_actions
obligations exceptions cross_references contracting_officer_notessummary and key_topics are produced by a local LLM (Ollama); the remaining fields are deterministic rule-based extractions. See enrichment details below.
Hosted retrieval mode (Hugging Face Spaces)
The Space uses the same RAG shape as local, without Ollama or Chroma:
user question
-> exact section lookup (252.204-7012 -> that record, score boosted)
-> BM25 over enriched metadata + original section text
-> top SectionRecords (deduped, ranked)
-> budgeted context package (metadata + trimmed original_text)
-> OpenRouter answer with citationsHosted dependencies are the five packages in requirements.txt. Set OPENROUTER_API_KEY in the Space secrets. Entry point:
streamlit run app.pyLocal development
uv venv dfars-env
source dfars-env/bin/activate
uv pip install -r requirements-local.txt # adds pymupdf, chroma, sentence-transformers, keyring
cp .env.example .env # set OPENROUTER_API_KEY (or keychain fields)
dfars-env/bin/python -m streamlit run app/streamlit_app.pyOptional local semantic search (needs Ollama + Chroma):
DFARS_ENABLE_VECTOR_SEARCH=true dfars-env/bin/python -m streamlit run app/streamlit_app.pyRebuilding the enriched index
Summaries and key topics come from a local Ollama model (OLLAMA_LLM_MODEL, default gemma4:e4b). Build offline, then commit the resulting JSONL for hosting:
# full run (1,444 sections)
PYTHONPATH=. dfars-env/bin/python -m src.preprocessing.enrich_sections \
--input indexes/dfars_sections.jsonl \
--output indexes/dfars_sections_enriched.jsonl
# faster: run LLM calls concurrently and skip tiny sections
OLLAMA_NUM_PARALLEL=4 PYTHONPATH=. dfars-env/bin/python -m src.preprocessing.enrich_sections \
--workers 4 --llm-min-chars 200
# no LLM (deterministic truncation summaries only)
PYTHONPATH=. dfars-env/bin/python -m src.preprocessing.enrich_sections --no-llm
# pilot a handful of sections to a throwaway file
PYTHONPATH=. dfars-env/bin/python -m src.preprocessing.enrich_sections \
--limit 5 --output indexes/pilot.jsonlNotes:
- If the LLM call fails or returns invalid JSON, the section falls back to a deterministic truncation summary — the build never crashes.
- Records are always written in input order, flushed per line, so a long run is inspectable as it goes.
--workers Nshould matchOLLAMA_NUM_PARALLELon the Ollama server.
Configuration
OpenRouter (answer generation):
OPENROUTER_API_KEY=... # or leave empty to use macOS Keychain:
OPENROUTER_KEYCHAIN_SERVICE=openrouter
OPENROUTER_KEYCHAIN_ACCOUNT=OPENROUTER_API_KEYOllama (offline enrichment / optional local vectors):
OLLAMA_HOST=http://127.0.0.1:11434
OLLAMA_LLM_MODEL=gemma4:e4b
OLLAMA_EMBED_MODEL=nomic-embed-text:latestThe answer model is google/gemini-2.5-flash-lite-preview-09-2025 (src/generation/answer.py).
Authentication (optional)
A single shared login gates the app only when `DFARS_AUTH_PASSWORD_HASH` is set (a bcrypt hash). Unset → the app runs open, which is convenient locally.
Generate a hash without exposing the plaintext, then store the printed values:
dfars-env/bin/python -m app.authDFARS_AUTH_USERNAME=dfars
DFARS_AUTH_PASSWORD_HASH=$2b$12$... # bcrypt hash, never the plaintext- Local: put both lines in
.env(git-ignored);load_dotenv()reads them. - Hugging Face: set both as Space Secrets (
.envis not deployed).
Login does a constant-time username check plus bcrypt.checkpw, returns a generic error, and applies a short lockout after repeated failures. Sessions are held in st.session_state, so a hard refresh requires signing in again.
Deployment
The same code lives on two git remotes:
- GitHub —
github.com/mrme77/dfars-assistant(source of truth, clean README). - Hugging Face Space —
huggingface.co/spaces/mrme77/dfars-assistant, run as a Docker Space (HF no longer offers a native Streamlit SDK).Dockerfileserves Streamlit on port 7860;.dockerignorekeeps.env/venv/tests out of the image.
HF requires a YAML frontmatter block in README.md (sdk: docker, app_port: 7860) or the Space reports CONFIG_ERROR. To keep GitHub's README clean, that block lives only on the hf-deploy branch, which is pushed to HF main:
git push dfars-assistant main # GitHub (clean README)
git checkout hf-deploy && git merge main \
&& git push hf hf-deploy:main && git checkout main # HF (with frontmatter)Runtime secrets (OPENROUTER_API_KEY, DFARS_AUTH_USERNAME, DFARS_AUTH_PASSWORD_HASH) are set in the Space Secrets UI — never committed.
Testing
dfars-env/bin/python -m pytest -qContext engineering
The project is organized around four moves; see context/README.md:
- Write — durable section records, summaries, obligations, cross-references.
- Select — retrieve the smallest relevant set of sections per question.
- Compress — summaries for orientation, original text only where needed, under an explicit character budget.
- Isolate — keep instructions, source text, retrieval metadata, and the user question in separate, labeled blocks.
License
MIT © Pasquale Salomone
