j-mwendwa/ARECCA---REAL-ESTATE-COMPLIANCE-AGENT

Python

1

21 commits

updated Aug 4, 2026

See the code

README

ARECCA — Automated Real Estate Contract Compliance Auditor

Python 3.11+ FastAPI LangGraph LlamaIndex Qdrant License

ARECCA ingests commercial real estate lease PDFs, extracts structured terms via a legal-fine-tuned LLM, validates rent math deterministically (never trusts the LLM with numbers), flags compliance violations against state laws, and indexes everything for hybrid semantic search.


Architecture

                        ┌──────────────────────────────┐
                        │   Chainlit Chat UI (8501)     │
                        │   or HTTP Client (8000)       │
                        └──────────┬───────────────────┘
                                   │
                        ┌──────────▼───────────────────┐
                        │   FastAPI + LangGraph App     │
                        │   src/api/main.py             │
                        └──────────┬───────────────────┘
                                   │
              ┌────────────────────┼────────────────────┐
              │                    │                     │
     ┌────────▼────────┐  ┌───────▼────────┐  ┌────────▼────────┐
     │  Input Guardrail │  │  Local Storage │  │  PostgreSQL     │
     │  (injection,     │  │  (data/uploads)│  │  (results)      │
     │   malware scan)  │  └────────────────┘  └─────────────────┘
     └────────┬────────┘
              │
     ┌────────▼─────────────────────────────────────────────┐
     │           LangGraph StateGraph Pipeline               │
     │                                                       │
     │  ┌──────────┐   ┌─────────┐   ┌────────────────┐     │
     │  │  Ingest   │──►│  Chunk  │──►│  Extract       │     │
     │  │(PDFReader)│   │(Semantic│   │(HF Legal LLM + │     │
     │  │          │   │ Splitter)│   │ ContextAssembly)│     │
     │  └──────────┘   └─────────┘   └────────┬───────┘     │
     │                                         │             │
     │  ┌──────────┐   ┌─────────┐   ┌────────▼───────┐     │
     │  │  Report  │◄──│ Output  │◄──│  Math Validate │     │
     │  │          │   │ Guardrail│   │  (NumPy, no LLM)│     │
     │  └──────────┘   └─────────┘   └────────┬───────┘     │
     │                                         │             │
     │  ┌──────────┐   ┌─────────┐            │             │
     │  │  Index   │◄──│Compliance│◄───────────┘             │
     │  │ (Qdrant) │   │(Rule Eng)│                         │
     │  └──────────┘   └─────────┘                          │
     └──────────────────────────────────────────────────────┘
                                   │
                    ┌──────────────┴──────────────┐
                    │                              │
           ┌────────▼────────┐          ┌──────────▼───────┐
           │ Qdrant Cloud    │          │  Entity Memory   │
           │ (Hybrid Search) │          │  + Conversation  │
           │ dense + BM25    │          │  Summary (Gemini)│
           │ alpha=0.3       │          │                  │
           └─────────────────┘          └──────────────────┘

5-Phase Pipeline

PhaseWhatNever trusts LLM with
1. Document IngestionLlamaIndex PDFReader parses the PDF; raw file stored locally in data/uploads/—
2. Semantic ChunkingSemanticSplitterNodeParser splits by embedding-similarity breakpoints (buffer=3, percentile=85)—
3. Structured ExtractionAdaptLLM/law-llm-7b via HuggingFace pipeline, forced JSON output via Pydantic LeaseTerms schema, context assembled from entity memory + conversation summaryfreeform generation — output is Pydantic-validated
4. Math ValidationNumPy-vectorized computation of projected rent schedule; compares every year against stated values with configurable decimal toleranceall financial math — pure Python @tool, never called on LLM
5. Compliance & FlaggingRule engine compares against CA state laws (late fee cap, grace period, deposit limit, escalation disclosure)—

Tech Stack

LayerTechnology
Chat UIChainlit (ChatGPT-style interface on port 8501)
APIFastAPI 0.111, Uvicorn
PipelineLangGraph 1.2 (StateGraph, 10 nodes)
LLM (Primary)AdaptLLM/law-llm-7b (Mistral-7B fine-tuned on legal corpus) via HuggingFace transformers
LLM (Summarization)Gemini 2.0 Flash (optional, via google-generativeai)
Embeddingsnlpaueb/legal-bert-base-uncased (768-dim legal-domain BERT)
Vector StoreQdrant Cloud with hybrid search (dense cosine + BM25 sparse, alpha=0.3)
IngestionLlamaIndex PDFReader + SemanticSplitterNodeParser
DatabasePostgreSQL 15+ (async via asyncpg + SQLAlchemy 2.0)
File StorageLocal filesystem (data/uploads/)
MathNumPy vectorized validation
PromptsMarkdown files in prompts/, versioned, hot-reloadable
MemoryEntity memory (JSON + optional Fernet encryption), conversation summary (Gemini rolling summary)
GuardrailsInjection detection (single alternation regex), malicious content scan (bytes), LRU-cached results
ObservabilityLangSmith tracing (opt-in), structlog, token accounting
ContainerMulti-stage Docker, non-root arecca user, persistent volumes
DeploymentRailway (auto-deploy from GitHub)
Testingpytest (~44 tests), pytest-asyncio

Quick Start

Prerequisites

  • Python 3.11+
  • PostgreSQL 15+
  • Qdrant Cloud account (or local Qdrant: docker run -p 6333:6333 qdrant/qdrant)
  • (Optional) Gemini API key for conversation summaries
  • (Optional) GPU for local LLM inference; falls back to CPU

Local Development

# 1. Clone & enter
cd arecca

# 2. Environment
cp .env.example .env
# Edit .env with your keys (DATABASE_URL, QDRANT_URL, QDRANT_API_KEY, etc.)

# 3. Install
make install

# 4. Run the API
make dev
# → http://localhost:8000
# → health: http://localhost:8000/health

# 5. Run the Chainlit UI (separate terminal)
chainlit run src/ui/app.py --port 8501
# → http://localhost:8501

Docker

# Build
docker build -t arecca:latest .

# Run
docker run -p 8000:8000 \
  -v $(pwd)/.env:/app/.env \
  -v $(pwd)/data:/app/data \
  arecca:latest

Railway Deployment

Deploy on Railway

  1. Push to GitHub
  2. Connect repo to Railway
  3. Add PostgreSQL plugin (Railway injects DATABASE_URL automatically)
  4. Set env vars: GEMINI_API_KEY, QDRANT_URL, QDRANT_API_KEY, STORAGE_PATH
  5. Deploy — Railway auto-detects the Dockerfile

Chainlit UI

The Chainlit UI provides a ChatGPT-style interface on port 8501.

chainlit run src/ui/app.py --port 8501

Features:

  • Upload & Audit — Click the upload button, select a PDF, the full pipeline runs automatically
  • Document History — Browse previously audited documents with status indicators
  • Search — Natural language search across indexed lease clauses
  • Audit Results — Formatted extraction, math validation, and compliance report with risk level icons

API Reference

POST /api/v1/upload

Upload a lease PDF for auditing.

curl -X POST http://localhost:8000/api/v1/upload \
  -F "[email protected]"
{
  "document_id": "uuid-...",
  "filename": "lease.pdf",
  "status": "processing",
  "message": "Document uploaded and queued for audit"
}

POST /api/v1/audit/{document_id}

Run the full 5-phase LangGraph audit pipeline.

curl -X POST http://localhost:8000/api/v1/audit/uuid-...
{
  "document_id": "uuid-...",
  "filename": "lease.pdf",
  "status": "completed",
  "extraction": { "lease_terms": { ... }, "confidence_score": 0.94 },
  "math_validation": { "is_valid": true, "details": { ... } },
  "compliance_report": {
    "overall_risk_level": "medium",
    "flag_count": 1,
    "flags": [ { "rule_id": "GRACE_PERIOD", "risk_level": "medium", ... } ]
  }
}

POST /api/v1/search

Hybrid semantic search over ingested lease clauses.

curl -X POST http://localhost:8000/api/v1/search \
  -H "Content-Type: application/json" \
  -d '{ "query": "late fee grace period", "top_k": 5, "alpha": 0.3 }'
{
  "query": "late fee grace period",
  "results": [
    { "section_title": "Section 3: Late Fee...",
      "content": "A late fee of 5% applies after 10...",
      "score": 0.87,
      "page_number": 4 }
  ]
}

GET /api/v1/documents

List all audited documents.

GET /api/v1/documents/{document_id}

Get audit result for a specific document.

GET /health

Liveness probe.


Configuration

.env (secrets)

VariableRequiredDefault
DATABASE_URLYespostgresql+asyncpg://localhost:5432/arecca
QDRANT_URLYeshttps://your-cluster.cloud.qdrant.io:6333
QDRANT_API_KEYYes (Cloud)—
GEMINI_API_KEYNo— (for conversation summaries)
STORAGE_PATHNodata/uploads
API_KEYNodev-local-key
LANGSMITH_API_KEYNo—
MEMORY_ENCRYPTION_KEYNo—

configs/config.yaml (runtime tuning)

llm:
  model: AdaptLLM/law-llm-7b           # HuggingFace model ID
  device: auto                          # cpu / cuda / auto
  load_in_8bit: true                    # memory optimization

gemini:
  model: gemini-2.0-flash               # Gemini model for summaries
  temperature: 0.0
  max_tokens: 4096

chunking:
  strategy: semantic                    # semantic | sentence
  semantic_buffer_size: 3
  semantic_breakpoint_percentile: 85

retrieval:
  hybrid_search: true
  alpha: 0.3                            # 0 = dense only, 1 = sparse only

validation:
  rounding_decimals: 2
  tolerance: 0.01                       # $0.01 discrepancy threshold

context:
  target_context_tokens: 8000           # context window budget

prompts:
  version: v1
  hot_reload: false                     # set true during prompt development

Project Structure

src/
├── ui/
│   └── app.py                Chainlit chat UI (port 8501)
├── api/
│   ├── main.py               FastAPI app + lifespan (tracing, llamaindex, DB)
│   ├── routes.py             /upload, /audit, /search, /documents
│   └── schemas.py            Pydantic request/response models
├── graph/
│   ├── graph.py              StateGraph compiler (10 nodes, conditional edges)
│   ├── state.py              AgentState TypedDict
│   ├── nodes.py              Pipeline nodes (guardrail → ingest → ... → report)
│   ├── edges.py              Conditional routing (blocked → rejection, errors → end)
│   ├── guardrails.py         Input (ext/malware/injection) + output (confidence/risk)
│   └── tools.py              NumPy @tool for rent schedule validation
├── ingestion/
│   ├── llamaindex_pipeline.py    PDFReader + SemanticSplitterNodeParser
│   └── storage.py                Local filesystem storage (save_file, copy_file, delete_file)
├── extraction/
│   ├── schemas.py            Pydantic: LeaseTerms, RentSchedule, ExtractionResult
│   └── hf_extractor.py       HuggingFace pipeline + ContextAssembler
├── validation/
│   └── math_validator.py     Decimal-based rent math (backup for tests)
├── compliance/
│   ├── rules.py              13 compliance rules (late fee, deposit, grace, etc.)
│   └── engine.py             Rule engine → ComplianceReport
├── vectordb/
│   └── qdrant_store.py       Qdrant client + collection + hybrid store
├── retrieval/
│   └── hybrid_retriever.py   LlamaIndex retriever (alpha=0.3, hybrid mode)
├── database/
│   ├── models.py             SQLAlchemy: LeaseDocument, Extraction, Validation, Flag
│   └── session.py            AsyncSession factory
├── memory/
│   ├── entity_memory.py      KV facts per document (JSON + Fernet)
│   └── conversation.py       Rolling summary via Gemini
├── core/
│   ├── config.py             Pydantic Settings + YAML merge
│   ├── context_assembler.py  Prompt + entity_memory + summary + token trim
│   ├── prompt_manager.py     Versioned prompt loader from prompts/ tree
│   ├── token_counter.py      tiktoken cl100k_base
│   ├── tracing.py            LangSmith opt-in
│   ├── llamaindex_setup.py   Centralised embed model init
│   └── logging.py            structlog JSON/console
├── configs/config.yaml        Runtime configuration
├── prompts/
│   ├── extraction/            Lease extraction prompt (v1)
│   ├── compliance/            Compliance rules reference
│   └── summarizer_system.md   Rollup summarizer system prompt
└── tests/                     ~44 tests (pytest)
    ├── test_guardrails.py
    ├── test_guardrail_efficiency.py
    ├── test_graph_tools.py
    ├── test_compliance.py
    ├── test_math_validator.py
    ├── test_chunker.py
    ├── test_storage.py
    ├── test_parser.py
    └── test_llamaindex_pipeline.py

Testing

# Run all tests
make test

# Or directly
python -m pytest tests/ -v

# Run a specific module
python -m pytest tests/test_math_validator.py -v
python -m pytest tests/test_guardrail_efficiency.py -v

Test coverage:

ModuleTestsWhat it proves
test_math_validator6Deterministic rent math (fixed %, fixed $, CPI, caps, discrepancies)
test_compliance5Rule engine flags (late fee, grace, deposit, clean)
test_chunker4Section header detection, multi-page chunking
test_graph_tools5NumPy vectorized validate_rent_schedule
test_guardrails9Input/output security decisions
test_guardrail_efficiency10Sub-millisecond guard checks, cache hits, early exits
test_storage4Local filesystem save, copy, delete, error handling
test_llamaindex_pipeline3Ingestion pipeline
test_parser2PDF parsing

Key Design Decisions

DecisionRationale
LLM never touches mathvalidate_rent_schedule is a pure NumPy @tool; the graph node calls it directly, never the LLM
Prompts in markdown filesprompts/extraction/lease_extraction_v1.md — versioned, hot-reloadable, auditable in git
Semantic over regex chunkingSemanticSplitterNodeParser adapts to document structure; no brittle regex maintenance
Context assembly with token budgetEntity memory + conversation summary injected into prompt, trimmed from the back if over 8K tokens
LRU-cached guardrailsSame content_hash skips re-scanning; single alternation regex for O(1) pattern matching
HuggingFace legal modelAdaptLLM/law-llm-7b fine-tuned on legal corpus; no API calls, private by default
Hybrid search alpha=0.3Biased toward dense semantic search with 30% sparse BM25 signal for exact clause matching
Per-document entity memoryExtracted facts persisted as JSON; optional Fernet encryption at rest
Gemini for summariesOptional Gemini 2.0 Flash generates rolling conversation summaries; falls back to template if no key set
Local file storagePDFs stored in data/uploads/ — no cloud dependency; just mount a volume for persistence

GitHub Actions CI / Deploy

The project includes a CI/CD pipeline in .github/workflows/deploy.yml:

  • CI — Runs on every PR/push to main: lint (ruff), type check (mypy), tests (pytest)
  • Docker — Builds the Docker image with BuildKit caching
  • Deploy — Deploys to Railway via the official GitHub Action

Required secrets:

  • RAILWAY_TOKEN — Railway API token
  • RAILWAY_PROJECT_ID — Railway project ID
  • RAILWAY_SERVICE_ID — Railway service ID

j-mwendwa/ARECCA---REAL-ESTATE-COMPLIANCE-AGENT

Python

1

21 commits

updated Aug 4, 2026

See the code

README

ARECCA — Automated Real Estate Contract Compliance Auditor

Python 3.11+ FastAPI LangGraph LlamaIndex Qdrant License

ARECCA ingests commercial real estate lease PDFs, extracts structured terms via a legal-fine-tuned LLM, validates rent math deterministically (never trusts the LLM with numbers), flags compliance violations against state laws, and indexes everything for hybrid semantic search.


Architecture

                        ┌──────────────────────────────┐
                        │   Chainlit Chat UI (8501)     │
                        │   or HTTP Client (8000)       │
                        └──────────┬───────────────────┘
                                   │
                        ┌──────────▼───────────────────┐
                        │   FastAPI + LangGraph App     │
                        │   src/api/main.py             │
                        └──────────┬───────────────────┘
                                   │
              ┌────────────────────┼────────────────────┐
              │                    │                     │
     ┌────────▼────────┐  ┌───────▼────────┐  ┌────────▼────────┐
     │  Input Guardrail │  │  Local Storage │  │  PostgreSQL     │
     │  (injection,     │  │  (data/uploads)│  │  (results)      │
     │   malware scan)  │  └────────────────┘  └─────────────────┘
     └────────┬────────┘
              │
     ┌────────▼─────────────────────────────────────────────┐
     │           LangGraph StateGraph Pipeline               │
     │                                                       │
     │  ┌──────────┐   ┌─────────┐   ┌────────────────┐     │
     │  │  Ingest   │──►│  Chunk  │──►│  Extract       │     │
     │  │(PDFReader)│   │(Semantic│   │(HF Legal LLM + │     │
     │  │          │   │ Splitter)│   │ ContextAssembly)│     │
     │  └──────────┘   └─────────┘   └────────┬───────┘     │
     │                                         │             │
     │  ┌──────────┐   ┌─────────┐   ┌────────▼───────┐     │
     │  │  Report  │◄──│ Output  │◄──│  Math Validate │     │
     │  │          │   │ Guardrail│   │  (NumPy, no LLM)│     │
     │  └──────────┘   └─────────┘   └────────┬───────┘     │
     │                                         │             │
     │  ┌──────────┐   ┌─────────┐            │             │
     │  │  Index   │◄──│Compliance│◄───────────┘             │
     │  │ (Qdrant) │   │(Rule Eng)│                         │
     │  └──────────┘   └─────────┘                          │
     └──────────────────────────────────────────────────────┘
                                   │
                    ┌──────────────┴──────────────┐
                    │                              │
           ┌────────▼────────┐          ┌──────────▼───────┐
           │ Qdrant Cloud    │          │  Entity Memory   │
           │ (Hybrid Search) │          │  + Conversation  │
           │ dense + BM25    │          │  Summary (Gemini)│
           │ alpha=0.3       │          │                  │
           └─────────────────┘          └──────────────────┘

5-Phase Pipeline

PhaseWhatNever trusts LLM with
1. Document IngestionLlamaIndex PDFReader parses the PDF; raw file stored locally in data/uploads/—
2. Semantic ChunkingSemanticSplitterNodeParser splits by embedding-similarity breakpoints (buffer=3, percentile=85)—
3. Structured ExtractionAdaptLLM/law-llm-7b via HuggingFace pipeline, forced JSON output via Pydantic LeaseTerms schema, context assembled from entity memory + conversation summaryfreeform generation — output is Pydantic-validated
4. Math ValidationNumPy-vectorized computation of projected rent schedule; compares every year against stated values with configurable decimal toleranceall financial math — pure Python @tool, never called on LLM
5. Compliance & FlaggingRule engine compares against CA state laws (late fee cap, grace period, deposit limit, escalation disclosure)—

Tech Stack

LayerTechnology
Chat UIChainlit (ChatGPT-style interface on port 8501)
APIFastAPI 0.111, Uvicorn
PipelineLangGraph 1.2 (StateGraph, 10 nodes)
LLM (Primary)AdaptLLM/law-llm-7b (Mistral-7B fine-tuned on legal corpus) via HuggingFace transformers
LLM (Summarization)Gemini 2.0 Flash (optional, via google-generativeai)
Embeddingsnlpaueb/legal-bert-base-uncased (768-dim legal-domain BERT)
Vector StoreQdrant Cloud with hybrid search (dense cosine + BM25 sparse, alpha=0.3)
IngestionLlamaIndex PDFReader + SemanticSplitterNodeParser
DatabasePostgreSQL 15+ (async via asyncpg + SQLAlchemy 2.0)
File StorageLocal filesystem (data/uploads/)
MathNumPy vectorized validation
PromptsMarkdown files in prompts/, versioned, hot-reloadable
MemoryEntity memory (JSON + optional Fernet encryption), conversation summary (Gemini rolling summary)
GuardrailsInjection detection (single alternation regex), malicious content scan (bytes), LRU-cached results
ObservabilityLangSmith tracing (opt-in), structlog, token accounting
ContainerMulti-stage Docker, non-root arecca user, persistent volumes
DeploymentRailway (auto-deploy from GitHub)
Testingpytest (~44 tests), pytest-asyncio

Quick Start

Prerequisites

  • Python 3.11+
  • PostgreSQL 15+
  • Qdrant Cloud account (or local Qdrant: docker run -p 6333:6333 qdrant/qdrant)
  • (Optional) Gemini API key for conversation summaries
  • (Optional) GPU for local LLM inference; falls back to CPU

Local Development

# 1. Clone & enter
cd arecca

# 2. Environment
cp .env.example .env
# Edit .env with your keys (DATABASE_URL, QDRANT_URL, QDRANT_API_KEY, etc.)

# 3. Install
make install

# 4. Run the API
make dev
# → http://localhost:8000
# → health: http://localhost:8000/health

# 5. Run the Chainlit UI (separate terminal)
chainlit run src/ui/app.py --port 8501
# → http://localhost:8501

Docker

# Build
docker build -t arecca:latest .

# Run
docker run -p 8000:8000 \
  -v $(pwd)/.env:/app/.env \
  -v $(pwd)/data:/app/data \
  arecca:latest

Railway Deployment

Deploy on Railway

  1. Push to GitHub
  2. Connect repo to Railway
  3. Add PostgreSQL plugin (Railway injects DATABASE_URL automatically)
  4. Set env vars: GEMINI_API_KEY, QDRANT_URL, QDRANT_API_KEY, STORAGE_PATH
  5. Deploy — Railway auto-detects the Dockerfile

Chainlit UI

The Chainlit UI provides a ChatGPT-style interface on port 8501.

chainlit run src/ui/app.py --port 8501

Features:

  • Upload & Audit — Click the upload button, select a PDF, the full pipeline runs automatically
  • Document History — Browse previously audited documents with status indicators
  • Search — Natural language search across indexed lease clauses
  • Audit Results — Formatted extraction, math validation, and compliance report with risk level icons

API Reference

POST /api/v1/upload

Upload a lease PDF for auditing.

curl -X POST http://localhost:8000/api/v1/upload \
  -F "[email protected]"
{
  "document_id": "uuid-...",
  "filename": "lease.pdf",
  "status": "processing",
  "message": "Document uploaded and queued for audit"
}

POST /api/v1/audit/{document_id}

Run the full 5-phase LangGraph audit pipeline.

curl -X POST http://localhost:8000/api/v1/audit/uuid-...
{
  "document_id": "uuid-...",
  "filename": "lease.pdf",
  "status": "completed",
  "extraction": { "lease_terms": { ... }, "confidence_score": 0.94 },
  "math_validation": { "is_valid": true, "details": { ... } },
  "compliance_report": {
    "overall_risk_level": "medium",
    "flag_count": 1,
    "flags": [ { "rule_id": "GRACE_PERIOD", "risk_level": "medium", ... } ]
  }
}

POST /api/v1/search

Hybrid semantic search over ingested lease clauses.

curl -X POST http://localhost:8000/api/v1/search \
  -H "Content-Type: application/json" \
  -d '{ "query": "late fee grace period", "top_k": 5, "alpha": 0.3 }'
{
  "query": "late fee grace period",
  "results": [
    { "section_title": "Section 3: Late Fee...",
      "content": "A late fee of 5% applies after 10...",
      "score": 0.87,
      "page_number": 4 }
  ]
}

GET /api/v1/documents

List all audited documents.

GET /api/v1/documents/{document_id}

Get audit result for a specific document.

GET /health

Liveness probe.


Configuration

.env (secrets)

VariableRequiredDefault
DATABASE_URLYespostgresql+asyncpg://localhost:5432/arecca
QDRANT_URLYeshttps://your-cluster.cloud.qdrant.io:6333
QDRANT_API_KEYYes (Cloud)—
GEMINI_API_KEYNo— (for conversation summaries)
STORAGE_PATHNodata/uploads
API_KEYNodev-local-key
LANGSMITH_API_KEYNo—
MEMORY_ENCRYPTION_KEYNo—

configs/config.yaml (runtime tuning)

llm:
  model: AdaptLLM/law-llm-7b           # HuggingFace model ID
  device: auto                          # cpu / cuda / auto
  load_in_8bit: true                    # memory optimization

gemini:
  model: gemini-2.0-flash               # Gemini model for summaries
  temperature: 0.0
  max_tokens: 4096

chunking:
  strategy: semantic                    # semantic | sentence
  semantic_buffer_size: 3
  semantic_breakpoint_percentile: 85

retrieval:
  hybrid_search: true
  alpha: 0.3                            # 0 = dense only, 1 = sparse only

validation:
  rounding_decimals: 2
  tolerance: 0.01                       # $0.01 discrepancy threshold

context:
  target_context_tokens: 8000           # context window budget

prompts:
  version: v1
  hot_reload: false                     # set true during prompt development

Project Structure

src/
├── ui/
│   └── app.py                Chainlit chat UI (port 8501)
├── api/
│   ├── main.py               FastAPI app + lifespan (tracing, llamaindex, DB)
│   ├── routes.py             /upload, /audit, /search, /documents
│   └── schemas.py            Pydantic request/response models
├── graph/
│   ├── graph.py              StateGraph compiler (10 nodes, conditional edges)
│   ├── state.py              AgentState TypedDict
│   ├── nodes.py              Pipeline nodes (guardrail → ingest → ... → report)
│   ├── edges.py              Conditional routing (blocked → rejection, errors → end)
│   ├── guardrails.py         Input (ext/malware/injection) + output (confidence/risk)
│   └── tools.py              NumPy @tool for rent schedule validation
├── ingestion/
│   ├── llamaindex_pipeline.py    PDFReader + SemanticSplitterNodeParser
│   └── storage.py                Local filesystem storage (save_file, copy_file, delete_file)
├── extraction/
│   ├── schemas.py            Pydantic: LeaseTerms, RentSchedule, ExtractionResult
│   └── hf_extractor.py       HuggingFace pipeline + ContextAssembler
├── validation/
│   └── math_validator.py     Decimal-based rent math (backup for tests)
├── compliance/
│   ├── rules.py              13 compliance rules (late fee, deposit, grace, etc.)
│   └── engine.py             Rule engine → ComplianceReport
├── vectordb/
│   └── qdrant_store.py       Qdrant client + collection + hybrid store
├── retrieval/
│   └── hybrid_retriever.py   LlamaIndex retriever (alpha=0.3, hybrid mode)
├── database/
│   ├── models.py             SQLAlchemy: LeaseDocument, Extraction, Validation, Flag
│   └── session.py            AsyncSession factory
├── memory/
│   ├── entity_memory.py      KV facts per document (JSON + Fernet)
│   └── conversation.py       Rolling summary via Gemini
├── core/
│   ├── config.py             Pydantic Settings + YAML merge
│   ├── context_assembler.py  Prompt + entity_memory + summary + token trim
│   ├── prompt_manager.py     Versioned prompt loader from prompts/ tree
│   ├── token_counter.py      tiktoken cl100k_base
│   ├── tracing.py            LangSmith opt-in
│   ├── llamaindex_setup.py   Centralised embed model init
│   └── logging.py            structlog JSON/console
├── configs/config.yaml        Runtime configuration
├── prompts/
│   ├── extraction/            Lease extraction prompt (v1)
│   ├── compliance/            Compliance rules reference
│   └── summarizer_system.md   Rollup summarizer system prompt
└── tests/                     ~44 tests (pytest)
    ├── test_guardrails.py
    ├── test_guardrail_efficiency.py
    ├── test_graph_tools.py
    ├── test_compliance.py
    ├── test_math_validator.py
    ├── test_chunker.py
    ├── test_storage.py
    ├── test_parser.py
    └── test_llamaindex_pipeline.py

Testing

# Run all tests
make test

# Or directly
python -m pytest tests/ -v

# Run a specific module
python -m pytest tests/test_math_validator.py -v
python -m pytest tests/test_guardrail_efficiency.py -v

Test coverage:

ModuleTestsWhat it proves
test_math_validator6Deterministic rent math (fixed %, fixed $, CPI, caps, discrepancies)
test_compliance5Rule engine flags (late fee, grace, deposit, clean)
test_chunker4Section header detection, multi-page chunking
test_graph_tools5NumPy vectorized validate_rent_schedule
test_guardrails9Input/output security decisions
test_guardrail_efficiency10Sub-millisecond guard checks, cache hits, early exits
test_storage4Local filesystem save, copy, delete, error handling
test_llamaindex_pipeline3Ingestion pipeline
test_parser2PDF parsing

Key Design Decisions

DecisionRationale
LLM never touches mathvalidate_rent_schedule is a pure NumPy @tool; the graph node calls it directly, never the LLM
Prompts in markdown filesprompts/extraction/lease_extraction_v1.md — versioned, hot-reloadable, auditable in git
Semantic over regex chunkingSemanticSplitterNodeParser adapts to document structure; no brittle regex maintenance
Context assembly with token budgetEntity memory + conversation summary injected into prompt, trimmed from the back if over 8K tokens
LRU-cached guardrailsSame content_hash skips re-scanning; single alternation regex for O(1) pattern matching
HuggingFace legal modelAdaptLLM/law-llm-7b fine-tuned on legal corpus; no API calls, private by default
Hybrid search alpha=0.3Biased toward dense semantic search with 30% sparse BM25 signal for exact clause matching
Per-document entity memoryExtracted facts persisted as JSON; optional Fernet encryption at rest
Gemini for summariesOptional Gemini 2.0 Flash generates rolling conversation summaries; falls back to template if no key set
Local file storagePDFs stored in data/uploads/ — no cloud dependency; just mount a volume for persistence

GitHub Actions CI / Deploy

The project includes a CI/CD pipeline in .github/workflows/deploy.yml:

  • CI — Runs on every PR/push to main: lint (ruff), type check (mypy), tests (pytest)
  • Docker — Builds the Docker image with BuildKit caching
  • Deploy — Deploys to Railway via the official GitHub Action

Required secrets:

  • RAILWAY_TOKEN — Railway API token
  • RAILWAY_PROJECT_ID — Railway project ID
  • RAILWAY_SERVICE_ID — Railway service ID