AI-Powered ISO 14001 Environmental Compliance Assistant
VerdeAI is a multi-tenant SaaS platform that automates ISO 14001 Environmental Management System compliance gap analysis. Upload your organisation's policy documents, run an AI-powered gap analysis against ISO 14001 clauses, receive prioritised recommendations, and chat with an AI assistant that has full context of your compliance posture.
- Document ingestion — Upload PDFs/DOCX; 6-stage pipeline (parse → chunk → contextualise → embed → index) with real-time progress
- Gap analysis — LangGraph state machine iterates all ISO 14001 clauses, retrieves relevant evidence, and evaluates compliance with structured reasoning
- Recommendations — Auto-generated prioritised action items for each identified gap
- Missing requirements — Flags missing organisational data fields needed for a complete assessment
- Conversational RAG — Chat interface with hybrid vector + BM25 retrieval and streaming responses, aware of your gap analysis results; messages rendered as markdown
- ISO versioning — LLM-driven pipeline that detects structure, extracts clauses, and verifies coverage from ISO standard documents; versioned clause sets stored in MongoDB
- Admin UI — Admin pages for managing ISO standard versions, publishing new versions, and inspecting version details
- Multi-tenancy — All data isolated per tenant via Keycloak JWT claims; no cross-tenant data leakage
- Observability — Full LLM trace capture via self-hosted Langfuse with cost tracking
┌─────────────────────────────────────────┐
│ External Clients │
│ Browser (demo_ui :5173) │
└────────────┬───────────────┬────────────┘
│ REST/WS │ SSE
┌──────────▼──────┐ ┌─────▼──────────┐
│ api-gateway │ │ chat-rag │
│ FastAPI :8000 │ │ FastAPI :8001 │
└──────┬──────────┘ └────────────────┘
│ RabbitMQ events
┌───────────────────┼──────────────────────────┐
│ │ │
┌──────────▼────────┐ ┌───────▼──────────┐ ┌──────────▼──────────┐
│ document-processor│ │ gap-analyzer │ │ iso-knowledge │
│ (×2 replicas) │ │ (×2 replicas) │ │ CLI + consumer │
│ 6-stage pipeline │ │ LangGraph SM │ │ ISO seed data │
└───────────────────┘ └──────┬───────────┘ └─────────────────────┘
│ analyses.gaps.ready
┌─────────────┴──────────────┐
┌────────▼─────────┐ ┌────────────▼──────────┐
│ recommendation │ │ missing-requirements │
│ (×1) │ │ (×1) │
└──────────────────┘ └───────────────────────┘
┌──────────────┐ ┌──────────────┐ ┌──────────┐ ┌────────────────┐
│ MongoDB :27017│ │ RabbitMQ:5672│ │Redis:6379│ │ Keycloak :8080 │
│ Atlas Local │ │ + mgmt:15672│ │ │ │ │
└──────────────┘ └──────────────┘ └──────────┘ └────────────────┘
┌────────────────────────────────────────────┐
│ Langfuse :3000 (self-hosted observability)│
│ + langfuse-db (postgres) │
└────────────────────────────────────────────┘
| Service | Type | Description |
|---|---|---|
api-gateway |
FastAPI HTTP | Auth, document uploads, analysis orchestration, WebSocket job progress relay |
chat-rag |
FastAPI SSE | Conversational RAG with hybrid vector+BM25 search and streaming |
document-processor |
RabbitMQ worker ×2 | 6-stage document pipeline: dedup → parse (LlamaParse) → chunk → image summary → embed (Voyage AI) → index |
gap-analyzer |
RabbitMQ worker ×2 | LangGraph state machine — iterates ISO 14001 clauses, retrieves evidence, evaluates compliance |
recommendation |
RabbitMQ worker | Generates prioritised recommendations for identified gaps |
missing-requirements |
RabbitMQ worker | Drafts information-request messages for missing state fields |
iso-knowledge |
RabbitMQ worker + CLI | Seeds ISO 14001 clause data, org state templates, and runs the LLM-driven ISO versioning pipeline |
| Layer | Tool |
|---|---|
| Language | Python 3.12 |
| HTTP framework | FastAPI 0.115 |
| Async server | Uvicorn + uvloop |
| Database | MongoDB 8 (Atlas Local) + Motor async driver |
| Message broker | RabbitMQ 3.13 (quorum queues) + aio-pika |
| Cache / pubsub | Redis 7 |
| Identity | Keycloak 26 (JWT + JWKS validation) |
| Agent orchestration | LangGraph 0.2 + LangChain Core |
| LLM gateway | OpenRouter (via OpenAI SDK) |
| Embeddings | Voyage AI voyage-4-lite (1024 dims) |
| Reranker | Voyage AI rerank-2.5 |
| Document parsing | LlamaParse (agentic tier) |
| Validation | Pydantic v2 + pydantic-settings |
| Logging | Loguru (structured, per-service) |
| Observability | Langfuse (self-hosted) + OpenTelemetry |
| Frontend | React 18 + Vite + Tailwind CSS + React Router 6 |
| Linter / formatter | Ruff |
| Type checker | Mypy (strict) |
| Tests | pytest + pytest-asyncio + httpx |
- Docker Desktop (Windows/macOS) or Docker Engine + Compose v2 (Linux)
- WSL2 (Windows only — required for Docker and make commands)
- Git
- Node.js 18+ and npm (for the demo UI only)
- make (available in WSL2/Linux/macOS)
git clone <repo-url>
cd VerdeAIThe .env file is already present. Fill in the required API keys:
# Minimum required keys
OPENROUTER_API_KEY=sk-or-v1-... # https://openrouter.ai
VOYAGE_API_KEY=pa-... # https://www.voyageai.com
LLAMA_CLOUD_API_KEY=llx-... # https://cloud.llamaindex.aimake upThis builds all Docker images and starts all services. First build takes 3–5 minutes. Infrastructure services (MongoDB, RabbitMQ, Redis, Keycloak) must pass their healthchecks before app services start.
Wait ~60–90 seconds for Keycloak to finish its first-boot realm import.
- Open
http://localhost:8080→ log in withadmin/admin - Switch to the verdeai realm
- Go to Clients →
verdeai-admin→ Credentials → copy the secret - Set it in
.env:KEYCLOAK_ADMIN_CLIENT_SECRET=<copied-secret> - Restart app services:
make rebuild
make seed-isoThis populates the iso_clauses and iso_state_template collections. Required before running any gap analysis.
cd demo_ui
npm install
npm run devOpen http://localhost:5173 in your browser.
Use the UI to register a new user account. The Keycloak realm is pre-configured for self-registration.
| Service | URL | Purpose |
|---|---|---|
| API Gateway | http://localhost:8000 |
REST API + WebSocket |
| API Docs (Swagger) | http://localhost:8000/docs |
Interactive API documentation |
| Chat RAG | http://localhost:8001 |
SSE streaming chat |
| Demo UI | http://localhost:5173 |
React frontend (dev server) |
| Keycloak Admin | http://localhost:8080 |
Identity provider admin console |
| RabbitMQ Management | http://localhost:15672 |
Queue/exchange browser (guest/guest) |
| Langfuse Dashboard | http://localhost:3000 |
LLM observability (see Observability section) |
| MongoDB | mongodb://localhost:27017 |
Direct connection for debugging |
| Redis | redis://localhost:6379 |
Direct connection for debugging |
make up # Build images and start all services in detached mode
make down # Gracefully stop all services (volumes preserved)
make rebuild # Rebuild and restart only app services (keeps DB/cache running)
make logs s=<svc> # Stream logs for a service, e.g. s=api-gateway, s=gap-analyzer
make seed-iso # Seed ISO 14001 clause data into MongoDB
make test # Run unit + integration tests (pytest --tb=short -q)
make test-e2e # Run full end-to-end test suite
make test-auth # Register a test user and fetch a Keycloak JWT for manual API testing
make lint # Ruff lint check + Mypy strict type check
make fmt # Ruff format (auto-fix)
make clean # Remove all containers AND volumes (destructive — data loss!)Avoid
make cleanunless you want to wipe all data (MongoDB, Keycloak, RabbitMQ, Langfuse). Usemake down(stop) ormake rebuild(hot-swap app code) instead.
All config is loaded from .env → verdeai_shared.settings.Settings (pydantic-settings). Never hardcode secrets.
ENVIRONMENT=development
LOG_LEVEL=INFOKEYCLOAK_URL=http://keycloak:8080 # Internal (container-to-container)
KEYCLOAK_PUBLIC_URL=http://localhost:8080 # External (browser-facing)
KEYCLOAK_REALM=verdeai
KEYCLOAK_FRONTEND_CLIENT_ID=verdeai-frontend
KEYCLOAK_ADMIN_CLIENT_ID=verdeai-admin
KEYCLOAK_ADMIN_CLIENT_SECRET= # Set after first realm importMONGO_URI=mongodb://mongodb:27017/?directConnection=true
MONGO_DB=verdeai
RABBITMQ_URL=amqp://verdeai:verdeai@rabbitmq:5672/
REDIS_URL=redis://redis:6379/0OPENROUTER_API_KEY=sk-or-v1-...
PRIMARY_REASONING_MODEL=deepseek/deepseek-v4-flash # Gap analysis, recommendations
CHEAP_REASONING_MODEL=meta-llama/llama-3.3-70b-instruct # Chunking, contextualisation
VISION_MODEL=mistralai/mistral-small-3.1-24b-instruct # Image summariesVOYAGE_API_KEY=pa-...
VOYAGE_EMBEDDING_MODEL=voyage-4-lite
VOYAGE_RERANKER_MODEL=rerank-2.5
EMBEDDING_DIMENSIONS=1024LLAMA_CLOUD_API_KEY=llx-...
LLAMA_PARSE_TIER=agenticLANGFUSE_PUBLIC_KEY=pk-lf-local-verdeai
LANGFUSE_SECRET_KEY=sk-lf-local-verdeai
LANGFUSE_BASE_URL=http://langfuse:3000EMBED_BATCH_SIZE=128
RETRIEVAL_TOP_K=30 # Candidates before reranking
RERANK_TOP_K=8 # Final chunks passed to LLM
CHUNK_TARGET_TOKENS=512
CHUNK_OVERLAP_TOKENS=64ISO 14001 clause definitions and state templates must be seeded once before running gap analyses:
make seed-isoThis runs the iso-knowledge service CLI, which inserts clause data (titles, requirements, sub-clauses) and state template fields (org profile fields for each clause) into MongoDB.
The seed is idempotent — running it multiple times is safe.
cd demo_ui
npm installnpm run dev # Dev server at http://localhost:5173 (hot-reload)
npm run build # Production build to dist/
npm run preview # Serve the production build locallyVITE_API_URL=http://localhost:8000
VITE_CHAT_URL=http://localhost:8001
VITE_KC_URL=http://localhost:8080
VITE_KC_REALM=verdeai
VITE_KC_CLIENT_ID=verdeai-frontend- Auth — JWT stored in
localStorage.apiFetch()(insrc/api/client.js) injects the Bearer token automatically; on 401 clears storage and redirects to/login. - Chat streaming — Uses
fetch()+ReadableStreamto consume SSE. Parsestoken,citations,error,doneevent types. - Job progress — WebSocket at
/ws/jobs/{jobId}via a single-use ticket fromPOST /ws/ticket. Auto-retries up to 3 times on disconnect.
All LLM calls are automatically traced via langfuse.openai (transparent drop-in for the OpenAI SDK). Named traces include:
| Trace Name | Pipeline |
|---|---|
analyse_clause:<clause_id> |
Gap analysis per clause |
state_compare |
State vs evidence comparison (within clause) |
gap_analyse |
Gap decision generation (within clause) |
recommendation |
Recommendation generation |
draft_request |
Missing information request drafting |
chat |
Conversational RAG response |
chunk_summary |
Document summary generation |
contextualise_chunk |
Chunk contextualisation |
image_summary |
Image/diagram summarisation |
iso_detect_structure |
ISO document structure detection (versioning pipeline) |
iso_extract_clause |
ISO clause extraction (versioning pipeline) |
iso_verify_coverage |
ISO clause coverage verification (versioning pipeline) |
- Open
http://localhost:3000 - Log in:
admin@verdeai.local/admin123 - Select the verdeai project
Langfuse doesn't know the OpenRouter model prices by default. Run the migration script once after Langfuse is up:
bash migrate_models_to_langfuse.shThis registers pricing for all 4 models so cost columns show non-zero values.
| Model | Input $/M tokens | Output $/M tokens |
|---|---|---|
deepseek/deepseek-v4-flash |
$0.38 | $1.50 |
meta-llama/llama-3.3-70b-instruct |
$0.12 | $0.30 |
deepseek/deepseek-chat |
$0.14 | $0.28 |
mistralai/mistral-small-3.1-24b-instruct |
$0.10 | $0.30 |
A docker-compose.override.yml bind-mounts service source directories and enables Uvicorn --reload. Changes to Python files in services/ and shared/ are picked up automatically without rebuilding.
docker compose up -d --build gap-analyzerOr use make rebuild to rebuild all app services while keeping MongoDB, RabbitMQ, Redis, and Keycloak running.
make logs s=gap-analyzer
make logs s=api-gateway
make logs s=document-processormake test-authThis registers a test user (test@verdeai.local / TestPass!23) and prints the access token for use with Authorization: Bearer <token> in API calls.
MongoDB Atlas Local uses WiredTiger — always use make down (graceful stop) not docker compose down to avoid unclean shutdown. If MongoDB fails to start after an unclean stop, remove the mongo-data volume:
docker compose down -v # WARNING: deletes all data
make up
make seed-isomake test
# or directly:
pytest --tb=short -qTests are collected from:
shared/tests/services/*/tests/tests/integration/(uses testcontainers — real MongoDB + RabbitMQ)
make test-e2eSpins up the full Docker Compose stack, runs black-box tests, then stops.
pytest services/chat-rag/tests/test_pipeline.py -v≥ 80% coverage per service.
make lint # Ruff lint (E, F, W, I, N, UP, B, SIM, ASYNC, S) + Mypy strict
make fmt # Ruff format (auto-fixes style)Configuration lives in the root pyproject.toml:
- Line length: 100
- Target: Python 3.12
- Mypy strict mode — all public functions must be typed
VerdeAI/
├── services/
│ ├── api-gateway/ # FastAPI HTTP gateway (port 8000)
│ ├── chat-rag/ # SSE chat service (port 8001)
│ ├── document-processor/ # 6-stage document pipeline (×2 replicas)
│ ├── gap-analyzer/ # LangGraph gap analysis engine (×2 replicas)
│ ├── iso-knowledge/ # ISO clause seeding CLI + worker
│ ├── missing-requirements/ # Missing data request drafter
│ └── recommendation/ # Recommendation generator
│
├── shared/
│ └── verdeai_shared/ # Library imported by all services
│ ├── auth/ # Keycloak JWT validation + tenant ContextVar
│ ├── db/ # Motor connection + repositories (auto-inject tenant_id), incl. iso_versions
│ ├── llm/ # OpenRouter async client + Jinja2 prompt templates (incl. iso_detect_structure, iso_extract_clause, iso_verify_coverage)
│ ├── messaging/ # aio-pika consumers, publishers, event models
│ ├── retrieval/ # Voyage AI embeddings, reranker, hybrid search
│ ├── parsing/ # LlamaParse adapter, HybridChunker, dedup
│ ├── pipeline/ # Shared recommendation + missing-request pipelines
│ └── observability/ # Langfuse init, OpenTelemetry tracing
│
├── demo_ui/ # React 18 + Vite SPA (port 5173 dev)
│ └── src/
│ ├── api/ # apiFetch, auth, documents, analyses, chat, admin
│ ├── context/ # AuthContext
│ ├── hooks/ # useJobProgress (WebSocket)
│ ├── components/ # Layout, Sidebar, Badge, Spinner, ProtectedRoute
│ └── pages/ # Login, Dashboard, Documents, Analysis, Chat, AdminVersions, AdminVersionDetail
│
├── infra/
│ ├── keycloak/ # realm-export.json (realm, clients, roles)
│ ├── mongodb/init/ # 01-collections.js, 02-vector-indexes.js
│ └── rabbitmq/ # definitions.json, rabbitmq.conf
│
├── tests/
│ ├── e2e/ # Full stack black-box tests
│ └── integration/ # Testcontainers-based integration tests
│
├── docker-compose.yml # Service definitions
├── docker-compose.override.yml # Dev hot-reload overrides
├── Makefile # All build and dev commands
├── pyproject.toml # Ruff, Mypy, Pytest config
├── .env # Secrets and configuration
├── migrate_models_to_langfuse.sh # One-time Langfuse model pricing setup
└── PROJECT.md # Full build specification (authoritative)
- Single LLM gateway — All LLM calls go through
verdeai_shared.llm.openrouter_client. Never call LLM APIs directly from service code. - Tenant isolation —
tenant_idcomes exclusively from the validated Keycloak JWT via a ContextVar. Never accept it from request bodies or headers. - Crash-resumable gap analysis — LangGraph results are persisted to
result_storeafter each clause. Restarted analyses skip already-completed clauses. - Async-only I/O — Motor (MongoDB), aio-pika (RabbitMQ), httpx (HTTP), redis-py async. No blocking I/O on the event loop.
- Type safety —
mypy --strictmust pass. All public functions require type annotations. Pydantic v2 for all DTOs.