LLM-powered export-control screening for chemical trade — deterministic screen, LLM explanation, human decision.
ExportGuard screens outbound chemical sales orders against real regulatory control lists (EU dual-use, embargo, drug-precursor and PIC lists). It is a portfolio project demonstrating production-grade LLM application engineering on the Anthropic API: structured document ingestion, citation-backed explanations, local RAG, a tool-using copilot, and AI regulation-change summaries — all wrapped around a screening core that deliberately contains no model at all.
The architectural thesis: in compliance, decisions must be reproducible and auditable. So the LLM extracts, explains and summarizes — exact matching flags, and a human decides every outcome.
| Feature | What it does | API technique |
|---|---|---|
| Smart ingestion | Parses regulatory PDFs/XLSX/CSV (mostly German) into structured control entries — CAS numbers, HS codes, embargoed countries | messages.parse() + Pydantic schema; PDF document blocks; every proposed entry re-verified by deterministic validators (CAS checksum, HS format, ISO country) before it can enter the database |
| Hit explanations | Streams why a transaction matched, quoting the exact regulatory passage with page numbers | Citations API (citations: {enabled: true} on document blocks) + SSE streaming + prompt caching on the document; persisted, never billed twice |
| RAG Q&A | "Is thiodiglycol controlled for export?" answered from the document library with source chips deep-linking into the PDFs | Local embeddings (fastembed / bge-small, ONNX) + sqlite-vec in the same SQLite file — no vector DB service, no second paid API |
| Compliance copilot | Chat over the app's own data: "show open hits to Russia" | SDK tool runner (@beta_tool) with five read-only tools, streamed tool activity, capped iterations |
| Change summaries | When a list's source URL yields changed content (SHA-256), explains what changed | Deterministic set-diff of validated entries + messages.parse() narration with screening-impact notes — demoed against real published revisions of the seeded lists |
The pipeline is ingest → screen → review, and the model's authority shrinks at every step:
flowchart LR
subgraph AI["LLM (proposes & explains)"]
A[Document ingestion<br/>Claude extracts entries]
E[Hit explanations<br/>with citations]
end
subgraph DET["Deterministic core"]
V[Validators<br/>CAS checksum · HS format · ISO]
S[Screening engine<br/>exact matching]
end
subgraph HUMAN["Human"]
R[Review queue<br/>confirm / ignore]
T[Audit trail]
end
A -->|proposed entries| V -->|accepted entries| S -->|hits| R --> T
E -.explains.-> R
- The LLM proposes entries; deterministic validators accept them (a CAS number with a bad checksum never enters the database, whoever wrote it).
- Screening is exact matching against those validated entries — reproducible, auditable, no model in the loop,
matched_entry_idsrecorded for provenance. - Every hit is decided by a person in the review queue; decisions append to an audit trail and roll up the transaction status (open hit → awaiting review, confirmed hit → withheld, all clear/ignored → cleared).
- The explanation prompt explicitly forbids verdicts; the copilot is read-only and points to the review queue.
The review queue — a hit explained by Claude with citations anchored into the German regulation (chips deep-link to the PDF page):
Regulation change summaries, generated against real published revisions (this one caught Guatemala being added to the EU embargo overview):
The compliance copilot — visible tool calls, and it knows it doesn't decide:
Transactions after a screening run:
uv sync # install everything
cp .env.example .env # add your ANTHROPIC_API_KEY
uv run python scripts/seed.py # demo dataset + 6 real regulatory lists (11 versions)
uv run python scripts/ingest.py # LLM-extract entries from the current lists
uv run python scripts/generate_diffs.py # optional: pre-generate change summaries
uv run uvicorn exportguard.main:app --reloadThen open http://127.0.0.1:8000 — press Screen all pending, open the review queue, and hit ✨ Explain this hit.
Without an API key the app still runs: screening, review, versioning and deterministic diffs all work; the LLM features show a friendly hint.
Docker: docker compose up --build (uses the same .env).
- Vantiro Specialty Chemicals GmbH — a fictional trading company. The seed generates ~60 orders of real generic chemicals (public CAS/CN numbers) to a believable destination mix, several of them genuinely listed: thiodiglycol (1C350), triethanolamine, acetic anhydride, potassium permanganate…
- Real regulatory documents (all public: BAFA, BfArM, EUR-Lex/ECHA) are committed under
fixtures/control_lists/including their genuine published revisions — the embargo overview (v1→v2), the precursor list (v1→v2→v3) and the PIC annex (v1→v2→v3) — so change summaries demo against real regulation changes. Bonus: the documents are German, so ingestion demonstrates multilingual structured extraction.
- Stack: FastAPI · SQLAlchemy 2.0 · Alembic · Pydantic v2 · HTMX + SSE · Tailwind (standalone CLI, no Node) · SQLite (+ sqlite-vec) · uv · ruff · pytest · GitHub Actions.
- LLM boundary: every feature depends on a small
LlmServicesurface (src/exportguard/llm/client.py); tests inject fakes — the whole suite (80+ tests) runs hermetically with zero network. One live smoke test is gated behindEXPORTGUARD_LIVE_TESTS=1. - Failure handling:
stop_reason == "refusal"checked before reading content; rate-limit/API errors surface as friendly HTMX fragments; ingestion failures mark the versionfailedwith the error, never half-imported. - Cost awareness: regulation documents carry
cache_controlso repeated explanations hit the prompt cache; explanations and diffs are persisted (never re-billed); embeddings are local; the model is configurable viaEXPORTGUARD_MODEL(defaultclaude-opus-4-8). - Why SQLite (+sqlite-vec): single-file, zero-ops demo; vectors live next to the relational data with SQL-visible provenance. A Postgres swap is a documented non-goal.
This project is a from-scratch public rebuild of an internal compliance tool I built professionally (Flask/Jinja/raw-SQL at the time). No code, data or configuration was carried over — only the domain knowledge, plus the public regulatory documents listed above. The rebuild adds everything the original never had: the LLM layer, structured extraction, RAG, and the copilot.
MIT — see LICENSE.



