Sla7f-EL-Ninja/Judge-Assistant

Python

1

210 commits

updated Jun 14, 2026

See the code

README

Judge Assistant (حكيم)

An AI-powered legal assistant built for Egyptian judicial workflows. It combines Arabic OCR, multi-corpus civil law retrieval, case document reasoning, multi-document summarization, and an adaptive chat reasoner into a unified REST API — orchestrated by a 15-node LangGraph multi-agent supervisor with persistent long-term memory.

Language: All agent outputs, prompts, and responses are in Arabic. The system is purpose-built for Egyptian civil law (القانون المدني المصري).


Table of Contents


Features

FeatureDescription
Arabic OCRQARI OCR (NAMAA-Space/Qari-OCR-v0.3-VL-2B-Instruct — Qwen2VL-based VLM) with two preprocessing stages: (1) CLAHE contrast normalization on LAB L-channel; (2) perspective correction via Canny edge-contour detection with adaptive-threshold fallback and safety guards. Confidence derived from token softmax probabilities (page-level + per-word). Arabic-Indic digit normalization applied post-inference.
Civil Law RAGUnified multi-corpus retrieval across Egyptian Civil Law, Evidence Law, and Civil Procedure Law. Corpus is auto-selected per query via LLM scoring. Optional HyDE expansion. TEI-powered BAAI/bge-m3 embeddings + reranker
Case Document RAGPer-case document retrieval with parallel sub-question fan-out, three document selection modes (retrieve_specific / restrict_to / search_all), and automatic rephrase-retry loop
Case ReasonerPer-issue legal analysis pipeline: issue extraction → law + fact retrieval → evidence classification → legal application → counterarguments → validation → global consistency check → confidence scoring
Chat ReasonerAdaptive planner-executor: generates a multi-step tool-use plan, validates it, executes steps in parallel (respecting dependency ordering), and synthesizes a final answer with replan support
Document Summarization7-node sequential pipeline producing a 7-section judge-facing Arabic brief: parallel document intake, role classification, bullet extraction, aggregation (متفق عليه / محل النزاع), thematic clustering, synthesis, and brief generation
Multi-Agent Supervisor15-node LangGraph state machine: intent classification → context enrichment → agent dispatch → response merging → citation verification → output validation with retry loop → long-term memory read/write → history summarization → audit logging
Async Report GenerationBackground pipeline that runs Summarization + Case Reasoning in sequence and returns results via a polling endpoint
REST API + SSEFastAPI with JWT authentication, SSE streaming (progress / result / error / done events), file uploads, case management, conversation history
Long-Term MemoryPer-case semantic facts and per-judge procedural preferences stored in MongoDB, loaded at turn start and updated at turn end

Architecture Overview

Judge (HTTP Client)
        │
        │  POST /api/v1/query  (JWT Bearer)
        ▼
┌───────────────────┐
│  FastAPI  api/    │  SSE stream ──────────────────────────► Judge
│  app.py           │
└────────┬──────────┘
         │
         ▼
┌────────────────────────────────────────────────────────────┐
│  Supervisor  (LangGraph 15-node StateGraph)                │
│                                                            │
│  validate_input → load_long_term_memory                    │
│    → classify_intent → enrich_context                      │
│    → dispatch_agents ──────────────────────────────────┐   │
│         │                                              │   │
│         ▼                                              │   │
│  ┌─────────────────────────────────────────────────┐  │   │
│  │  MCP Transport Layer  (JSON-RPC 2.0 / stdio)    │  │   │
│  │                                                 │  │   │
│  │  ┌──────────────────┐  ┌─────────────────────┐ │  │   │
│  │  │  LegalRAG Server │  │  CaseDoc RAG Server  │ │  │   │
│  │  │  (FastMCP)       │  │  (FastMCP)           │ │  │   │
│  │  └──────────────────┘  └─────────────────────┘ │  │   │
│  └─────────────────────────────────────────────────┘  │   │
│         │                                              │   │
│  ChatReasonerAdapter (direct invocation) ──────────────┘   │
│  SummarizeAdapter    (direct invocation)                   │
│                                                            │
│  merge_responses → verify_citations → validate_output      │
│    → [retry loop, max 3] OR fallback                       │
│    → update_memory → write_long_term_memory                │
│    → [summarize_history?] → audit_log                      │
└────────────────────────────────────────────────────────────┘
         │
         ▼
┌────────────────────────────────────────┐
│  Infrastructure                        │
│  MongoDB · Qdrant · Redis              │
│  MinIO · PostgreSQL · TEI              │
└────────────────────────────────────────┘

Supervisor Turn Lifecycle

  1. validate_input — sanitize and length-check the judge query; off-topic if injection detected
  2. load_long_term_memory — fetch per-case semantic facts and per-judge preferences from MongoDB
  3. classify_intent — medium-tier LLM → one of civil_law_rag | case_doc_rag | reason | multi | off_topic
  4. enrich_context — prefetch case summary and document titles from MongoDB
  5. dispatch_agents — run selected adapters; for multi intent, parallel Send() fan-out
  6. merge_responses — high-tier LLM synthesizes all agent outputs
  7. verify_citations — cross-check article references
  8. validate_output — low-tier LLM scores 4 criteria: hallucination · relevance · completeness · coherence
  9. prepare_retry — on failure, append validation_feedback and re-dispatch (max 3 retries)
  10. fallback_response — generic Arabic fallback after retries exhausted
  11. update_memory — append turn to MongoDB conversation history
  12. write_long_term_memory — upsert semantic facts and procedural preferences
  13. summarize_history — if messages_since_last_summary exceeds threshold, compress older turns into running_summary using high-tier LLM
  14. audit_log — write structured audit entry to MongoDB
  15. off_topic_response — direct refusal path (skips dispatch)

Agent Roster

AgentTrigger IntentTransportEntry Point
Civil Law RAGcivil_law_ragMCP (subprocess stdio)mcp_servers.legal_rag_server → RAG/legal_rag/service.py
Case Doc RAGcase_doc_ragMCP (subprocess stdio)mcp_servers.case_doc_server → RAG/case_doc_rag/graph.py
Chat ReasonerreasonDirect invocationchat_reasoner/graph.py
SummarizersummarizeDirect invocationsummarize/graph.py
OCRocrDirect invocationDocumentProcessor/OCR/ocr_pipeline.py

Civil Law RAG

  • Corpora: Civil Law (القانون المدني) · Evidence Law (قانون الإثبات) · Civil Procedure (قانون المرافعات)
  • Corpus routing: LLM scores the query against all 3 corpora at runtime — no upfront selection needed
  • Scope classification: narrows to chapter then section before vector search
  • Retrieval: BAAI/bge-m3 embeddings (1024-dim COSINE) via remote TEI service → Qdrant civil_law_docs collection
  • Reranking: TEI reranker re-scores candidates before grading
  • Grading: rule_grader (heuristic) → llm_grader (semantic) → generate_answer or refine loop
  • Caching: SemanticCache keyed by (query, corpus_version, prompt_version)

Case Doc RAG

  • Scope: per-case documents only, filtered by case_id in Qdrant case_docs collection
  • Sub-question fan-out: query is decomposed into sub-questions; each executes in a parallel branch via LangGraph Send()
  • Branch modes: retrieve_specific_doc (short-circuit fetch) · restrict_to_doc (scoped search) · no_doc_specified (full case search)
  • Rephrase loop: up to 2 rephrases per sub-question on grading failure

Chat Reasoner

  • Tools available: case_doc_rag · civil_law_rag · fetch_summary_report
  • Plan validation: unknown tools, empty queries, circular deps, >5 steps all rejected (up to 3 validator retries)
  • Parallel execution: steps with empty depends_on fan out simultaneously via Send(); dependent steps wait
  • Replan cap: 2 replans before returning best-effort answer
  • Trace: every run writes a structured trace to chat_reasoner_traces in MongoDB

Case Reasoner

  • Issue extraction: high-tier LLM identifies distinct legal issues from the case brief
  • Per-issue branch (sequential within branch, parallel across issues):
    1. Decompose issue into legal elements
    2. Generate retrieval queries for law + facts
    3. Retrieve law via civil_law_rag MCP tool
    4. Retrieve facts via case_doc_rag MCP tool
    5. Classify evidence: established | not_established | disputed | insufficient_evidence
    6. Apply law with Arabic legal reasoning citing specific articles
    7. Generate counterarguments for both parties
    8. Validate analysis (citation + consistency + completeness)
    9. Package result
  • Post-aggregation: global consistency check → reconciliation paragraphs → per-issue and case-level confidence scoring → final report generation

Summarizer

  • Parallel intake: Node 0 processes documents concurrently via ThreadPoolExecutor
  • Document types (7): صحيفة دعوى · مذكرة دفاع · مذكرة رد · حافظة مستندات · محضر جلسة · حكم تمهيدي · غير محدد
  • Party types (6): المدعي · المدعى عليه · النيابة · المحكمة · خبير · غير محدد
  • Defendant disambiguation: merges "المدعى عليه" variants into numbered parties automatically
  • Output: 7-section structured brief persisted to MongoDB summaries collection

Infrastructure Stack

ServicePurposeDefault
MongoDBCases, files, conversations, summaries, case_reasonings, chat_reasoner_traces, audit logs, long-term memorylocalhost:27017
QdrantVector store — civil_law_docs (1024-dim COSINE) and case_docs (per-case filtered)localhost:6333 (HTTP) / 6334 (gRPC)
RedisCaching, rate limiting (100 req/60s), session datalocalhost:6379
MinIOS3-compatible binary file storage (PDF, images)localhost:9000
PostgreSQLUsers, roles, audit logginglocalhost:5432
TEI (Embeddings)Remote BAAI/bge-m3 embedding servicelocalhost:8080
TEI (Reranker)Remote reranker servicelocalhost:8081

Prerequisites

  • Python 3.11+
  • Running services: MongoDB · Qdrant · Redis · MinIO · PostgreSQL
  • TEI services for embeddings and reranking (or configure fallback in-process)
  • Google API key for Gemini 2.5 Flash (all LLM tiers default to Google)
# Minimum required env vars
GOOGLE_API_KEY=your_google_api_key
JA_API_JWT_SECRET=a-strong-random-secret

Quick Start (Local)

1. Clone and install

git clone <repo-url>
cd Code
python3.11 -m venv .venv
source .venv/bin/activate       # Windows: .venv\Scripts\activate
pip install -r requirements.txt

2. Configure environment

# Create .env from template
cp .env.example .env

Edit .env:

GOOGLE_API_KEY=your_actual_google_api_key
JA_API_JWT_SECRET=your-strong-jwt-secret-here

# Optional overrides
JA_MONGODB_URI=mongodb://localhost:27017/
JA_MONGODB_DATABASE=judge_assistant
JA_QDRANT_HOST=localhost

3. Start infrastructure services

# MongoDB
mongod --dbpath ./data/db

# Qdrant
docker run -p 6333:6333 -p 6334:6334 qdrant/qdrant

# Redis
redis-server

# MinIO
docker run -p 9000:9000 -p 9001:9001 \
  -e MINIO_ROOT_USER=minioadmin \
  -e MINIO_ROOT_PASSWORD=minioadmin \
  minio/minio server /data --console-address ":9001"

4. Start the API

uvicorn api.app:create_app --factory --host 0.0.0.0 --port 8000 --reload

5. Verify

curl http://localhost:8000/api/v1/health

Expected:

{"status": "healthy", "mongo": "connected", "qdrant": "connected"}

6. Open interactive API docs

  • Swagger UI: http://localhost:8000/docs
  • ReDoc: http://localhost:8000/redoc

Configuration

Configuration uses a 3-layer precedence system (highest wins):

JA_* environment variables  >  config/settings.local.yaml  >  config/settings.yaml

config/settings.local.yaml is gitignored and deep-merged over settings.yaml. Use it for local overrides without modifying committed files.

Key Configuration Sections (config/settings.yaml)

llm:
  high:
    provider: google
    model: gemini-2.5-flash        # complex reasoning, synthesis, summarization
    temperature: 0.0
  medium:
    provider: google
    model: gemini-2.5-flash        # intent classification, extraction
    temperature: 0.0
  low:
    provider: google
    model: gemini-2.5-flash-lite   # output validation, simple routing
    temperature: 0.0

embedding:
  model: BAAI/bge-m3

qdrant:
  host: localhost
  port: 6333
  collection: civil_law_docs
  case_collection: case_docs
  vector_size: 1024

tei:
  embedding_url: http://localhost:8080
  reranker_url: http://localhost:8081
  timeout_seconds: 30

rag:
  civil_law:
    hyde_enabled: false
    scope_chapter_threshold: 0.5
    scope_section_threshold: 0.5

supervisor:
  max_retries: 3
  max_conversation_turns: 20

Environment Variable Convention

Nested YAML keys are flattened with _ and prefixed with JA_:

YAML pathEnvironment variableExample
llm.high.modelJA_LLM_HIGH_MODELgemini-2.5-pro
llm.high.providerJA_LLM_HIGH_PROVIDERgoogle
mongodb.uriJA_MONGODB_URImongodb://user:pass@host:27017/
mongodb.databaseJA_MONGODB_DATABASEproduction_db
qdrant.hostJA_QDRANT_HOSTqdrant.internal
api.jwt_secretJA_API_JWT_SECRETstrong-random-value
api.cors_originsJA_API_CORS_ORIGINShttps://app.example.com
api.debugJA_API_DEBUGtrue
ocr.use_gpuJA_OCR_USE_GPUtrue
tei.embedding_urlJA_TEI_EMBEDDING_URLhttp://tei-host:8080

API Reference

All routes are prefixed with /api/v1/. Full interactive docs at /docs when the server is running.

Endpoints

MethodPathDescriptionAuth
GET/healthService + DB health checkNo
POST/casesCreate a new caseYes
GET/casesList cases (paginated)Yes
GET/cases/{case_id}Get case detailsYes
PATCH/cases/{case_id}Update case title / statusYes
DELETE/cases/{case_id}Soft-delete a caseYes
POST/files/uploadUpload file (PDF / image, ≤20MB)Yes
DELETE/files/{file_id}Delete uploaded fileYes
GET/cases/{case_id}/documentsList documents for a caseYes
GET/cases/{case_id}/documents/{doc_id}Get document metadataYes
GET/cases/{case_id}/documents/{doc_id}/ocrGet OCR text + confidenceYes
PATCH/cases/{case_id}/documents/{doc_id}/ocrSubmit corrected OCR text + re-indexYes
POST/queryRun supervisor query — SSE streamYes
GET/cases/{case_id}/conversationsList conversationsYes
GET/conversations/{conversation_id}Get full conversation historyYes
DELETE/conversations/{conversation_id}Delete conversationYes
GET/cases/{case_id}/summaryGet generated case summaryYes
POST/cases/{case_id}/reports/generateKick off async report pipelineYes
GET/cases/{case_id}/reports/{report_id}Poll report job status + fetch resultYes
GET/cases/{case_id}/reportsList historical report jobsYes

File Upload Constraints

ConstraintValue
Max file size20 MB (max_upload_bytes: 20971520)
Allowed MIME typesapplication/pdf, image/png, image/jpeg, image/tiff, image/bmp, image/webp, image/gif, image/heic, image/heif
Upload fieldfile (multipart/form-data)

Standard Error Envelope

All error responses use this shape:

{
  "error": {
    "code": "CASE_NOT_FOUND",
    "message": "Case not found",
    "status": 404
  }
}
CodeHTTPMeaning
UNAUTHORIZED401Missing, expired, or invalid JWT
VALIDATION_ERROR422Request schema validation failed
CASE_NOT_FOUND404Case doesn't exist or belongs to another user
DOCUMENT_NOT_FOUND404Document not found
FILE_NOT_FOUND404Uploaded file not found
REPORT_NOT_FOUND404Report job not found
NO_DOCUMENTS_FOR_CASE404No documents uploaded for this case
SUMMARY_NOT_FOUND404No summary generated yet
INVALID_MIME_TYPE400File type not in allowed list
FILE_TOO_LARGE400File exceeds 20 MB
INTERNAL_ERROR500Unexpected server error

Authentication

JWT Bearer tokens, HS256 signed.

Required claims: user_id (string), exp (expiration timestamp)
Header: Authorization: Bearer <token>

Generating a Test Token

import jwt
from datetime import datetime, timedelta, timezone

token = jwt.encode(
    {
        "user_id": "judge_001",
        "exp": datetime.now(timezone.utc) + timedelta(hours=24),
    },
    "your-jwt-secret",   # must match JA_API_JWT_SECRET
    algorithm="HS256",
)
print(token)

Example: End-to-End curl Workflow

export BASE=http://localhost:8000/api/v1
export TOKEN="your-jwt-token"
export H="Authorization: Bearer $TOKEN"

# 1. Create a case
curl -s -X POST $BASE/cases \
  -H "$H" -H "Content-Type: application/json" \
  -d '{"title": "قضية مدنية رقم 2024/1234"}' | jq .

# 2. Upload a scanned document
CASE_ID="<case_id from step 1>"
FILE_ID=$(curl -s -X POST $BASE/files/upload \
  -H "$H" -F "file=@/path/to/document.pdf" | jq -r .file_id)

# 3. Query the case (SSE stream)
curl -N -X POST $BASE/query \
  -H "$H" -H "Content-Type: application/json" \
  -d "{\"case_id\": \"$CASE_ID\", \"query\": \"ما هي المواد القانونية المنطبقة على هذه القضية؟\"}"

# 4. Generate full report (async)
JOB_ID=$(curl -s -X POST $BASE/cases/$CASE_ID/reports/generate \
  -H "$H" | jq -r .job_id)

# 5. Poll report status
curl -s $BASE/cases/$CASE_ID/reports/$JOB_ID -H "$H" | jq .status

Streaming Queries (SSE)

POST /api/v1/query returns text/event-stream. The client receives a sequence of typed events.

Event Types

progress — emitted as each supervisor node completes:

event: progress
data: {"step": "classify_intent", "status": "done", "intent": "civil_law_rag"}

result — the final validated Arabic answer (emitted once):

event: result
data: {
  "final_response": "وفقاً للمادة 163 من القانون المدني ...",
  "sources": [{"article": "163", "title": "المادة 163", "book": "الالتزامات"}],
  "intent": "civil_law_rag",
  "agents_used": ["civil_law_rag"],
  "conversation_id": "conv_abc123",
  "turn_count": 1
}

error — if something goes wrong:

event: error
data: {"detail": "An internal error occurred while processing the query"}

done — always the last event:

event: done
data: {}

Multi-Turn Conversations

Pass conversation_id from a previous result event to continue a thread. The supervisor loads prior history from MongoDB and uses running_summary to keep context within token limits.

JavaScript Integration

async function queryCase(caseId, question, conversationId = null) {
  const response = await fetch('/api/v1/query', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      case_id: caseId,
      query: question,
      conversation_id: conversationId,
    }),
  });

  const reader = response.body.getReader();
  const decoder = new TextDecoder();

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    const chunk = decoder.decode(value);
    for (const line of chunk.split('\n')) {
      if (line.startsWith('event: result')) continue;
      if (line.startsWith('data: ')) {
        const data = JSON.parse(line.slice(6));
        if (data.final_response) {
          displayAnswer(data.final_response);
          return data.conversation_id;   // pass to next turn
        }
      }
    }
  }
}

Report Generation

The report pipeline runs summarization and case reasoning asynchronously as a background job.

POST /api/v1/cases/{case_id}/reports/generate
  → HTTP 202 + { "job_id": "..." }

GET  /api/v1/cases/{case_id}/reports/{job_id}
  → { "status": "pending|running|completed|failed", "summary": {...}, "case_reasoning": {...} }

Pipeline sequence (background):

  1. Summarization pipeline (7-node LangGraph) → upsert to summaries
  2. Case Reasoner pipeline → upsert to case_reasonings
  3. Job status updated to completed

Polling recommendation: poll every 5–10 seconds; typical completion time is 60–120 seconds depending on document count and issue complexity.


MCP Transport Layer

The Civil Law RAG and Case Doc RAG agents run as isolated FastMCP subprocess servers, communicating over stdin/stdout using newline-delimited JSON-RPC 2.0.

Supervisor process
  │
  ├─── MCPClient("mcp_servers.legal_rag_server")
  │       │  threading.Lock (serializes calls)
  │       │  auto-respawn on transport failure (max 1 respawn)
  │       └─► subprocess: python -m mcp_servers.legal_rag_server
  │               JSON-RPC: tools/call → search_legal_corpus(query, corpus)
  │
  └─── MCPClient("mcp_servers.case_doc_server")
          │  threading.Lock
          └─► subprocess: python -m mcp_servers.case_doc_server
                  JSON-RPC: tools/call → search_case_docs(query, case_id, ...)

Timeouts: call timeout 120s · handshake timeout 600s (covers first-run model download)
Warmup: both servers warm up their LangGraph graph + Qdrant vectorstore + reranker at subprocess boot, before accepting any requests.


LLM Tier System

Three tiers map to task complexity:

TierModelUsed For
highgemini-2.5-flashLegal reasoning, response synthesis, summarization, case briefing, chat reasoner planning/synthesis
mediumgemini-2.5-flashIntent classification, document classification, query rewriting, relevance grading
lowgemini-2.5-flash-liteOutput validation, off-topic routing, thematic clustering
from config import get_llm

llm_high   = get_llm("high")
llm_medium = get_llm("medium")
llm_low    = get_llm("low")

All tiers use temperature: 0.0 by default for deterministic legal outputs.


Testing

The test suite has 100+ tests across 6 categories.

Test Markers

MarkerDescription
unitPure function tests, no external I/O
integrationTests against real MongoDB + Qdrant + LLMs
behavioralE2E multi-turn conversation and agent dispatch scenarios
performanceMemory profiling, throughput, cache speedup
regressionBoundary cases and known failure modes
llm_evalLLM-as-judge quality evaluations (RAGAS, custom rubrics)
expensiveLong-running tests (E2E multi-turn, full pipelines)

Running Tests

# Install test dependencies
pip install -r requirements.txt

# Run unit tests only (fast, no LLM calls)
pytest -m unit -v

# Run integration tests (requires all services running)
pytest -m integration -v

# Run the full suite
pytest -v

# Skip expensive LLM-eval tests
pytest -m "not expensive and not llm_eval" -v

# Run a specific subsystem
pytest tests/supervisor/ -v
pytest tests/rag/ -v
pytest tests/summarizer/ -v

# Run with parallel workers
pytest -n auto -m unit

Key Quality Thresholds

MetricThresholdTest
Supervisor routing accuracy≥ 85%test_routing_accuracy.py
RAGAS faithfulness≥ 0.80test_rag_quality.py
RAGAS context recall≥ 0.75test_rag_quality.py
Answer consistency (multi-turn)≥ 0.80test_answer_consistency.py
Cache speedup≥ 2×test_cache_speedup.py
Memory peak (summarizer)≤ 500 MBtest_memory_leak.py
Memory growth ratio≤ 1.2×test_memory_leak.py
Ingestion pipeline runtime≤ 120stest_ingestion_pipeline.py

Test Structure

tests/
  supervisor/
    unit/           — state reducers, router functions, node logic
    integration/    — full supervisor turns with real LLM + MongoDB
    behavioral/     — multi-turn conversation, intent routing accuracy
    performance/    — memory profiling, throughput
    e2e/            — 3-turn conversation, history trimming, retry count reset
  rag/
    unit/           — corpus config, cache, validation logic
    integration/    — retrieval quality, filter application
    behavioral/     — routing accuracy, boundary cases
    performance/    — cache speedup, vectorstore load
  summarizer/
    unit/           — bullet extraction, aggregation, disambiguation
    integration/    — full 7-node pipeline
    behavioral/     — party classification, theme coverage
    performance/    — memory under load
    llm_eval/       — LLM-as-judge quality scoring (EV-01 to EV-08)
  case_reasoner/
    unit/           — issue extraction, evidence classification
    integration/    — per-issue branch, consistency check
    llm_eval/       — CR-EV-01 to CR-EV-12 quality dimensions

Project Structure

Code/
├── api/                          # FastAPI application
│   ├── app.py                    #   create_app() factory, lifespan, DB init
│   ├── dependencies.py           #   JWT auth, DB, settings injection
│   ├── errors.py                 #   error code constants
│   ├── routers/                  #   route handlers
│   │   ├── cases.py
│   │   ├── files.py
│   │   ├── documents.py
│   │   ├── query.py              #   SSE streaming endpoint
│   │   ├── conversations.py
│   │   ├── summaries.py
│   │   ├── reports.py            #   async report generation
│   │   └── health.py
│   ├── schemas/                  #   Pydantic request/response models
│   ├── services/                 #   business logic
│   │   ├── query_service.py      #   supervisor graph invocation
│   │   ├── report_service.py     #   background report job
│   │   └── ...
│   └── db/                       #   Motor/Qdrant/Redis/MinIO/PostgreSQL clients
│
├── Supervisor/                   # Multi-agent supervisor
│   ├── graph.py                  #   15-node LangGraph StateGraph
│   ├── state.py                  #   SupervisorState TypedDict + Pydantic schemas
│   ├── prompts.py                #   LLM prompt templates
│   ├── agents/                   #   adapter registry + all adapters
│   │   ├── civil_law_rag_adapter.py
│   │   ├── case_doc_rag_adapter.py
│   │   ├── chat_reasoner_adapter.py
│   │   ├── summarize_adapter.py
│   │   └── ocr_adapter.py
│   └── nodes/                    #   one file per graph node
│       ├── classify_intent.py
│       ├── dispatch_agents.py
│       ├── merge_responses.py
│       ├── validate_output.py
│       ├── update_memory.py
│       ├── load_long_term_memory.py
│       ├── write_long_term_memory.py
│       ├── summarize_history.py
│       ├── audit_log.py
│       ├── enrich_context.py
│       ├── verify_citations.py
│       ├── prepare_retry.py
│       ├── fallback.py
│       ├── off_topic.py
│       └── validate_input.py
│
├── mcp_servers/                  # MCP subprocess servers
│   ├── lifecycle.py              #   start_mcp_servers(), get_client()
│   ├── client.py                 #   MCPClient (JSON-RPC 2.0 / stdio)
│   ├── legal_rag_server.py       #   FastMCP: search_legal_corpus
│   ├── case_doc_server.py        #   FastMCP: search_case_docs
│   └── errors.py                 #   ErrorCode enum, ToolError
│
├── RAG/
│   ├── legal_rag/                # Unified multi-corpus civil law RAG
│   │   ├── graph.py              #   11-node LangGraph (corpus-agnostic)
│   │   ├── service.py            #   ask_question() public entry point
│   │   ├── corpus_config.py      #   CorpusConfig frozen dataclass
│   │   ├── cache.py              #   SemanticCache
│   │   ├── state.py
│   │   ├── nodes/                #   corpus_router, preprocessor, scope_classifier, etc.
│   │   ├── retrieval/            #   embeddings.py, vectorstore.py, reranker.py
│   │   ├── civil_law_rag/        #   CIVIL_LAW_CORPUS config + docs
│   │   ├── evidence_rag/         #   EVIDENCE_CORPUS config + docs
│   │   └── procedures_rag/       #   PROCEDURES_CORPUS config + docs
│   └── case_doc_rag/             # Per-case document RAG
│       ├── graph.py              #   main graph + branch sub-graph
│       ├── routers.py            #   branchDocSelectorRouter, proceedRouter
│       ├── state.py              #   AgentState, SubQuestionState
│       └── nodes/                #   query, selection, retrieval, generation nodes
│
├── summarize/                    # 7-node summarization pipeline
│   ├── graph.py                  #   create_pipeline(), sequential + ThreadPoolExecutor
│   ├── state.py                  #   SummarizationState, disambiguate_defendants()
│   └── nodes/                    #   intake, classifier, extractor, aggregator,
│                                 #   clustering, synthesis, brief
│
├── chat_reasoner/                # Adaptive planner-executor
│   ├── graph.py                  #   build_chat_reasoner_graph()
│   ├── state.py                  #   ChatReasonerState, ALLOWED_TOOLS, Pydantic schemas
│   ├── tools.py                  #   tool implementations
│   ├── prompts.py
│   └── nodes/                    #   planner, plan_validator, executor, step_worker,
│                                 #   collector, synthesizer, replanner, trace_writer
│
├── CR/                           # Case Reasoner
│   ├── graph.py                  #   build_case_reasoner_graph(), build_issue_branch()
│   ├── pipeline.py               #   run_case_reasoning() public entry, CaseReasoningResult
│   ├── state.py                  #   CaseReasonerState, IssueAnalysisState
│   ├── routers.py
│   └── nodes/                    #   extraction, decomposition, retrieval, evidence,
│                                 #   application, counterargument, validation, package,
│                                 #   aggregation, consistency, confidence, report
│
├── DocumentProcessor/            # Document processing subsystem
│   ├── pipeline.py               #   top-level orchestration
│   ├── classifier.py             #   document type classifier
│   └── OCR/                      #   QARI OCR pipeline
│       ├── ocr_pipeline.py       #     run_ocr() — main orchestrator
│       ├── ocr_engine.py         #     QARIEngine (Qwen2VL, 8-bit quantized, singleton)
│       ├── ingestion.py          #     PDF→PIL pages (400 DPI) + image loading + validation
│       ├── restoration.py        #     CLAHE contrast normalization on LAB L-channel
│       ├── perspective_correction.py  # Canny edge-contour + adaptive-threshold dewarp
│       ├── text_reconstruction.py     # normalize_numerals() — Arabic-Indic digit conversion
│       ├── confidence.py         #     page-level + per-word token probability scoring
│       └── models.py             #     OCRPageResult, OCRDocumentResult (Pydantic)
│
├── config/                       # Centralized configuration
│   ├── settings.yaml             #   committed defaults
│   ├── __init__.py               #   AppConfig singleton, get_llm() factory
│   ├── api.py                    #   FastAPI Settings (Pydantic)
│   ├── supervisor.py             #   supervisor constants
│   └── legal_rag.py              #   RAG constants
│
├── tests/                        # Full test suite
│   ├── supervisor/
│   ├── rag/
│   ├── summarizer/
│   └── case_reasoner/
│
├── docs/                         # Documentation
│   ├── ARCHITECTURE.md
│   ├── AGENTS.md
│   ├── API.md
│   ├── DATABASE.md
│   ├── SETUP.md
│   ├── TESTING.md
│   ├── DECISIONS.md
│   ├── TROUBLESHOOTING.md
│   └── diagrams/                 # Mermaid diagrams
│       ├── architecture.mmd
│       ├── supervisor.mmd
│       ├── rag.mmd
│       ├── summarizer.mmd
│       ├── chat_reasoner.mmd
│       ├── case_reasoner.mmd
│       ├── sequences/            # 9 sequence diagrams
│       └── activity/             # 11 activity/flow diagrams
│
├── Methodology.md                # Academic methodology document
├── requirements.txt
└── config/settings.yaml          # Default configuration

Diagrams

Mermaid diagrams are in docs/diagrams/:

FileTypeShows
architecture.mmdgraph TDFull system: FastAPI → Supervisor → agents → MCP → infra
supervisor.mmdclassDiagramSupervisorState, IntentEnum, ValidationResult, adapters
rag.mmdclassDiagramLegalRAGGraph, CaseDocRAGGraph, MCPClient, both servers
summarizer.mmdclassDiagramSummarizationState, 4 enums, SummarizationGraph
chat_reasoner.mmdclassDiagramChatReasonerState, Plan/PlanStep/ToolEnum hierarchy
case_reasoner.mmdclassDiagramCaseReasonerState, IssueAnalysisState, branch + pipeline
sequences/query_lifecycle.mmdsequenceDiagramFull turn: POST /query → SSE → audit_log
sequences/chat_reasoner_flow.mmdsequenceDiagramplanner → fan-out → synthesizer/replan loop
sequences/legal_rag_routing.mmdsequenceDiagramcorpus_router → scope → embed → rerank → grade → cache
sequences/case_doc_rag_flow.mmdsequenceDiagramquestionRewriter → parallel branches → mergeAnswers
sequences/memory_lifecycle.mmdsequenceDiagramload LTM → turn → summarize_history → write LTM
sequences/report_generation.mmdsequenceDiagram202 → background task → poll
activity/intent_routing.mmdflowchart TDvalidate → classify → 5-way intent dispatch
activity/validation_retry.mmdflowchart TD4-criteria check, partial_pass caveat, retry loop
activity/case_doc_rag_branch.mmdflowchart TD3 branch modes, rephrase loop
activity/legal_rag_retrieval.mmdflowchart TDcache hit/miss, HyDE, rule/LLM grader
activity/summarization_activity.mmdflowchart TD7 nodes, ThreadPoolExecutor, disambiguate
activity/chat_reasoner_activity.mmdflowchart TDplan → validate → parallel execute → synthesize/replan
activity/case_reasoner_activity.mmdflowchart TDissue fan-out, consistency check, confidence
activity/document_ingestion_activity.mmdflowchart TDMIME/size gates, OCR tiers, embed → Qdrant
activity/report_generation_activity.mmdflowchart TDBackground task + polling loop

Troubleshooting

MCP server fails to start / handshake timeout

The first spawn of legal_rag_server or case_doc_server can take 2–10 minutes on a cold start because it downloads and warms up the BAAI/bge-m3 model and the reranker. The handshake timeout is 600s by default. Check subprocess stderr (it inherits the parent's stderr) for progress logs.

# Confirm TEI services are reachable
curl http://localhost:8080/health
curl http://localhost:8081/health

Qdrant connection refused

Qdrant must be running before the API starts. The API's lifespan hooks call ensure_indexed() at startup.

docker run -d -p 6333:6333 -p 6334:6334 qdrant/qdrant

MongoDB ObjectId serialization errors

Ensure Motor is version 3.x and that bson is installed from the pymongo package, not the standalone bson package (they conflict).

OCR confidence always low or CUDA OOM

The OCR engine is QARI (NAMAA-Space/Qari-OCR-v0.3-VL-2B-Instruct), a Qwen2VL vision-language model loaded in 8-bit quantization. It requires a CUDA GPU — CPU inference works but is extremely slow.

  • CUDA OOM: the engine catches torch.cuda.OutOfMemoryError, flushes the cache, and returns an error entry for that page. Reduce max_new_tokens (default 4000) or switch to 4-bit quantization by passing config={"quantization": "4bit"} to run_ocr().
  • Low confidence: confidence is computed from token softmax probabilities. Blurry or low-contrast images produce lower scores. The pipeline applies CLAHE contrast normalization automatically, but very degraded scans may still score low.
  • Perspective correction skipped: if the Canny edge-contour detector and the adaptive-threshold fallback both fail to find a valid page quad — or the safety guards reject the detected quad (output too small, crop ratio < 0.65, area < 35% of original) — the original image is passed through unchanged. Check logs for "Safety guard triggered" or "No page boundary detected".
  • PDF DPI: PDFs are rendered at 400 DPI by default. If images are too large for GPU VRAM, lower pdf_dpi in config.
  • No dictionary correction: the OCR prompt instructs the model to transcribe exactly as written. Spelling normalization is intentionally absent — do not expect it.

validator_error in every response

This usually means the low-tier LLM (gemini-2.5-flash-lite) is rate-limited or the GOOGLE_API_KEY is missing. Check logs for the specific LLM exception message.

History grows without bound

The summarize_history node fires only when messages_since_last_summary > threshold. Check config/supervisor.py for the MAX_CONVERSATION_TURNS constant. If conversations use the non-persistent graph (get_app()), history is not checkpointed between API calls — pass conversation_id on every request to use the persistent graph.

Redis connection errors (non-fatal)

Redis is used for optional caching and rate limiting. If Redis is unavailable, the API continues to function without caching. Errors are logged at WARNING level.


Documentation Index

DocumentLocationContents
Architecturedocs/ARCHITECTURE.mdFull graph diagrams, ADR pointers, component design
Agentsdocs/AGENTS.mdAll 5 agents: triggers, schemas, retrieval strategies
API Referencedocs/API.mdAll endpoints, request/response schemas, SSE guide
Databasedocs/DATABASE.mdMongoDB collections, Qdrant indexes, Redis, MinIO, PostgreSQL
Setupdocs/SETUP.mdFull env var reference, Docker Compose, local dev
Testingdocs/TESTING.mdTest suite structure, markers, writing new tests, CI/CD
Decisionsdocs/DECISIONS.md7 Architecture Decision Records with rationale
Troubleshootingdocs/TROUBLESHOOTING.md10 known issues with fixes and debugging guide
MethodologyMethodology.mdAcademic system methodology (Arabic)
Configurationconfig/settings.yamlAnnotated default configuration

Sla7f-EL-Ninja/Judge-Assistant

Python

1

210 commits

updated Jun 14, 2026

See the code

README

Judge Assistant (حكيم)

An AI-powered legal assistant built for Egyptian judicial workflows. It combines Arabic OCR, multi-corpus civil law retrieval, case document reasoning, multi-document summarization, and an adaptive chat reasoner into a unified REST API — orchestrated by a 15-node LangGraph multi-agent supervisor with persistent long-term memory.

Language: All agent outputs, prompts, and responses are in Arabic. The system is purpose-built for Egyptian civil law (القانون المدني المصري).


Table of Contents


Features

FeatureDescription
Arabic OCRQARI OCR (NAMAA-Space/Qari-OCR-v0.3-VL-2B-Instruct — Qwen2VL-based VLM) with two preprocessing stages: (1) CLAHE contrast normalization on LAB L-channel; (2) perspective correction via Canny edge-contour detection with adaptive-threshold fallback and safety guards. Confidence derived from token softmax probabilities (page-level + per-word). Arabic-Indic digit normalization applied post-inference.
Civil Law RAGUnified multi-corpus retrieval across Egyptian Civil Law, Evidence Law, and Civil Procedure Law. Corpus is auto-selected per query via LLM scoring. Optional HyDE expansion. TEI-powered BAAI/bge-m3 embeddings + reranker
Case Document RAGPer-case document retrieval with parallel sub-question fan-out, three document selection modes (retrieve_specific / restrict_to / search_all), and automatic rephrase-retry loop
Case ReasonerPer-issue legal analysis pipeline: issue extraction → law + fact retrieval → evidence classification → legal application → counterarguments → validation → global consistency check → confidence scoring
Chat ReasonerAdaptive planner-executor: generates a multi-step tool-use plan, validates it, executes steps in parallel (respecting dependency ordering), and synthesizes a final answer with replan support
Document Summarization7-node sequential pipeline producing a 7-section judge-facing Arabic brief: parallel document intake, role classification, bullet extraction, aggregation (متفق عليه / محل النزاع), thematic clustering, synthesis, and brief generation
Multi-Agent Supervisor15-node LangGraph state machine: intent classification → context enrichment → agent dispatch → response merging → citation verification → output validation with retry loop → long-term memory read/write → history summarization → audit logging
Async Report GenerationBackground pipeline that runs Summarization + Case Reasoning in sequence and returns results via a polling endpoint
REST API + SSEFastAPI with JWT authentication, SSE streaming (progress / result / error / done events), file uploads, case management, conversation history
Long-Term MemoryPer-case semantic facts and per-judge procedural preferences stored in MongoDB, loaded at turn start and updated at turn end

Architecture Overview

Judge (HTTP Client)
        │
        │  POST /api/v1/query  (JWT Bearer)
        ▼
┌───────────────────┐
│  FastAPI  api/    │  SSE stream ──────────────────────────► Judge
│  app.py           │
└────────┬──────────┘
         │
         ▼
┌────────────────────────────────────────────────────────────┐
│  Supervisor  (LangGraph 15-node StateGraph)                │
│                                                            │
│  validate_input → load_long_term_memory                    │
│    → classify_intent → enrich_context                      │
│    → dispatch_agents ──────────────────────────────────┐   │
│         │                                              │   │
│         ▼                                              │   │
│  ┌─────────────────────────────────────────────────┐  │   │
│  │  MCP Transport Layer  (JSON-RPC 2.0 / stdio)    │  │   │
│  │                                                 │  │   │
│  │  ┌──────────────────┐  ┌─────────────────────┐ │  │   │
│  │  │  LegalRAG Server │  │  CaseDoc RAG Server  │ │  │   │
│  │  │  (FastMCP)       │  │  (FastMCP)           │ │  │   │
│  │  └──────────────────┘  └─────────────────────┘ │  │   │
│  └─────────────────────────────────────────────────┘  │   │
│         │                                              │   │
│  ChatReasonerAdapter (direct invocation) ──────────────┘   │
│  SummarizeAdapter    (direct invocation)                   │
│                                                            │
│  merge_responses → verify_citations → validate_output      │
│    → [retry loop, max 3] OR fallback                       │
│    → update_memory → write_long_term_memory                │
│    → [summarize_history?] → audit_log                      │
└────────────────────────────────────────────────────────────┘
         │
         ▼
┌────────────────────────────────────────┐
│  Infrastructure                        │
│  MongoDB · Qdrant · Redis              │
│  MinIO · PostgreSQL · TEI              │
└────────────────────────────────────────┘

Supervisor Turn Lifecycle

  1. validate_input — sanitize and length-check the judge query; off-topic if injection detected
  2. load_long_term_memory — fetch per-case semantic facts and per-judge preferences from MongoDB
  3. classify_intent — medium-tier LLM → one of civil_law_rag | case_doc_rag | reason | multi | off_topic
  4. enrich_context — prefetch case summary and document titles from MongoDB
  5. dispatch_agents — run selected adapters; for multi intent, parallel Send() fan-out
  6. merge_responses — high-tier LLM synthesizes all agent outputs
  7. verify_citations — cross-check article references
  8. validate_output — low-tier LLM scores 4 criteria: hallucination · relevance · completeness · coherence
  9. prepare_retry — on failure, append validation_feedback and re-dispatch (max 3 retries)
  10. fallback_response — generic Arabic fallback after retries exhausted
  11. update_memory — append turn to MongoDB conversation history
  12. write_long_term_memory — upsert semantic facts and procedural preferences
  13. summarize_history — if messages_since_last_summary exceeds threshold, compress older turns into running_summary using high-tier LLM
  14. audit_log — write structured audit entry to MongoDB
  15. off_topic_response — direct refusal path (skips dispatch)

Agent Roster

AgentTrigger IntentTransportEntry Point
Civil Law RAGcivil_law_ragMCP (subprocess stdio)mcp_servers.legal_rag_server → RAG/legal_rag/service.py
Case Doc RAGcase_doc_ragMCP (subprocess stdio)mcp_servers.case_doc_server → RAG/case_doc_rag/graph.py
Chat ReasonerreasonDirect invocationchat_reasoner/graph.py
SummarizersummarizeDirect invocationsummarize/graph.py
OCRocrDirect invocationDocumentProcessor/OCR/ocr_pipeline.py

Civil Law RAG

  • Corpora: Civil Law (القانون المدني) · Evidence Law (قانون الإثبات) · Civil Procedure (قانون المرافعات)
  • Corpus routing: LLM scores the query against all 3 corpora at runtime — no upfront selection needed
  • Scope classification: narrows to chapter then section before vector search
  • Retrieval: BAAI/bge-m3 embeddings (1024-dim COSINE) via remote TEI service → Qdrant civil_law_docs collection
  • Reranking: TEI reranker re-scores candidates before grading
  • Grading: rule_grader (heuristic) → llm_grader (semantic) → generate_answer or refine loop
  • Caching: SemanticCache keyed by (query, corpus_version, prompt_version)

Case Doc RAG

  • Scope: per-case documents only, filtered by case_id in Qdrant case_docs collection
  • Sub-question fan-out: query is decomposed into sub-questions; each executes in a parallel branch via LangGraph Send()
  • Branch modes: retrieve_specific_doc (short-circuit fetch) · restrict_to_doc (scoped search) · no_doc_specified (full case search)
  • Rephrase loop: up to 2 rephrases per sub-question on grading failure

Chat Reasoner

  • Tools available: case_doc_rag · civil_law_rag · fetch_summary_report
  • Plan validation: unknown tools, empty queries, circular deps, >5 steps all rejected (up to 3 validator retries)
  • Parallel execution: steps with empty depends_on fan out simultaneously via Send(); dependent steps wait
  • Replan cap: 2 replans before returning best-effort answer
  • Trace: every run writes a structured trace to chat_reasoner_traces in MongoDB

Case Reasoner

  • Issue extraction: high-tier LLM identifies distinct legal issues from the case brief
  • Per-issue branch (sequential within branch, parallel across issues):
    1. Decompose issue into legal elements
    2. Generate retrieval queries for law + facts
    3. Retrieve law via civil_law_rag MCP tool
    4. Retrieve facts via case_doc_rag MCP tool
    5. Classify evidence: established | not_established | disputed | insufficient_evidence
    6. Apply law with Arabic legal reasoning citing specific articles
    7. Generate counterarguments for both parties
    8. Validate analysis (citation + consistency + completeness)
    9. Package result
  • Post-aggregation: global consistency check → reconciliation paragraphs → per-issue and case-level confidence scoring → final report generation

Summarizer

  • Parallel intake: Node 0 processes documents concurrently via ThreadPoolExecutor
  • Document types (7): صحيفة دعوى · مذكرة دفاع · مذكرة رد · حافظة مستندات · محضر جلسة · حكم تمهيدي · غير محدد
  • Party types (6): المدعي · المدعى عليه · النيابة · المحكمة · خبير · غير محدد
  • Defendant disambiguation: merges "المدعى عليه" variants into numbered parties automatically
  • Output: 7-section structured brief persisted to MongoDB summaries collection

Infrastructure Stack

ServicePurposeDefault
MongoDBCases, files, conversations, summaries, case_reasonings, chat_reasoner_traces, audit logs, long-term memorylocalhost:27017
QdrantVector store — civil_law_docs (1024-dim COSINE) and case_docs (per-case filtered)localhost:6333 (HTTP) / 6334 (gRPC)
RedisCaching, rate limiting (100 req/60s), session datalocalhost:6379
MinIOS3-compatible binary file storage (PDF, images)localhost:9000
PostgreSQLUsers, roles, audit logginglocalhost:5432
TEI (Embeddings)Remote BAAI/bge-m3 embedding servicelocalhost:8080
TEI (Reranker)Remote reranker servicelocalhost:8081

Prerequisites

  • Python 3.11+
  • Running services: MongoDB · Qdrant · Redis · MinIO · PostgreSQL
  • TEI services for embeddings and reranking (or configure fallback in-process)
  • Google API key for Gemini 2.5 Flash (all LLM tiers default to Google)
# Minimum required env vars
GOOGLE_API_KEY=your_google_api_key
JA_API_JWT_SECRET=a-strong-random-secret

Quick Start (Local)

1. Clone and install

git clone <repo-url>
cd Code
python3.11 -m venv .venv
source .venv/bin/activate       # Windows: .venv\Scripts\activate
pip install -r requirements.txt

2. Configure environment

# Create .env from template
cp .env.example .env

Edit .env:

GOOGLE_API_KEY=your_actual_google_api_key
JA_API_JWT_SECRET=your-strong-jwt-secret-here

# Optional overrides
JA_MONGODB_URI=mongodb://localhost:27017/
JA_MONGODB_DATABASE=judge_assistant
JA_QDRANT_HOST=localhost

3. Start infrastructure services

# MongoDB
mongod --dbpath ./data/db

# Qdrant
docker run -p 6333:6333 -p 6334:6334 qdrant/qdrant

# Redis
redis-server

# MinIO
docker run -p 9000:9000 -p 9001:9001 \
  -e MINIO_ROOT_USER=minioadmin \
  -e MINIO_ROOT_PASSWORD=minioadmin \
  minio/minio server /data --console-address ":9001"

4. Start the API

uvicorn api.app:create_app --factory --host 0.0.0.0 --port 8000 --reload

5. Verify

curl http://localhost:8000/api/v1/health

Expected:

{"status": "healthy", "mongo": "connected", "qdrant": "connected"}

6. Open interactive API docs

  • Swagger UI: http://localhost:8000/docs
  • ReDoc: http://localhost:8000/redoc

Configuration

Configuration uses a 3-layer precedence system (highest wins):

JA_* environment variables  >  config/settings.local.yaml  >  config/settings.yaml

config/settings.local.yaml is gitignored and deep-merged over settings.yaml. Use it for local overrides without modifying committed files.

Key Configuration Sections (config/settings.yaml)

llm:
  high:
    provider: google
    model: gemini-2.5-flash        # complex reasoning, synthesis, summarization
    temperature: 0.0
  medium:
    provider: google
    model: gemini-2.5-flash        # intent classification, extraction
    temperature: 0.0
  low:
    provider: google
    model: gemini-2.5-flash-lite   # output validation, simple routing
    temperature: 0.0

embedding:
  model: BAAI/bge-m3

qdrant:
  host: localhost
  port: 6333
  collection: civil_law_docs
  case_collection: case_docs
  vector_size: 1024

tei:
  embedding_url: http://localhost:8080
  reranker_url: http://localhost:8081
  timeout_seconds: 30

rag:
  civil_law:
    hyde_enabled: false
    scope_chapter_threshold: 0.5
    scope_section_threshold: 0.5

supervisor:
  max_retries: 3
  max_conversation_turns: 20

Environment Variable Convention

Nested YAML keys are flattened with _ and prefixed with JA_:

YAML pathEnvironment variableExample
llm.high.modelJA_LLM_HIGH_MODELgemini-2.5-pro
llm.high.providerJA_LLM_HIGH_PROVIDERgoogle
mongodb.uriJA_MONGODB_URImongodb://user:pass@host:27017/
mongodb.databaseJA_MONGODB_DATABASEproduction_db
qdrant.hostJA_QDRANT_HOSTqdrant.internal
api.jwt_secretJA_API_JWT_SECRETstrong-random-value
api.cors_originsJA_API_CORS_ORIGINShttps://app.example.com
api.debugJA_API_DEBUGtrue
ocr.use_gpuJA_OCR_USE_GPUtrue
tei.embedding_urlJA_TEI_EMBEDDING_URLhttp://tei-host:8080

API Reference

All routes are prefixed with /api/v1/. Full interactive docs at /docs when the server is running.

Endpoints

MethodPathDescriptionAuth
GET/healthService + DB health checkNo
POST/casesCreate a new caseYes
GET/casesList cases (paginated)Yes
GET/cases/{case_id}Get case detailsYes
PATCH/cases/{case_id}Update case title / statusYes
DELETE/cases/{case_id}Soft-delete a caseYes
POST/files/uploadUpload file (PDF / image, ≤20MB)Yes
DELETE/files/{file_id}Delete uploaded fileYes
GET/cases/{case_id}/documentsList documents for a caseYes
GET/cases/{case_id}/documents/{doc_id}Get document metadataYes
GET/cases/{case_id}/documents/{doc_id}/ocrGet OCR text + confidenceYes
PATCH/cases/{case_id}/documents/{doc_id}/ocrSubmit corrected OCR text + re-indexYes
POST/queryRun supervisor query — SSE streamYes
GET/cases/{case_id}/conversationsList conversationsYes
GET/conversations/{conversation_id}Get full conversation historyYes
DELETE/conversations/{conversation_id}Delete conversationYes
GET/cases/{case_id}/summaryGet generated case summaryYes
POST/cases/{case_id}/reports/generateKick off async report pipelineYes
GET/cases/{case_id}/reports/{report_id}Poll report job status + fetch resultYes
GET/cases/{case_id}/reportsList historical report jobsYes

File Upload Constraints

ConstraintValue
Max file size20 MB (max_upload_bytes: 20971520)
Allowed MIME typesapplication/pdf, image/png, image/jpeg, image/tiff, image/bmp, image/webp, image/gif, image/heic, image/heif
Upload fieldfile (multipart/form-data)

Standard Error Envelope

All error responses use this shape:

{
  "error": {
    "code": "CASE_NOT_FOUND",
    "message": "Case not found",
    "status": 404
  }
}
CodeHTTPMeaning
UNAUTHORIZED401Missing, expired, or invalid JWT
VALIDATION_ERROR422Request schema validation failed
CASE_NOT_FOUND404Case doesn't exist or belongs to another user
DOCUMENT_NOT_FOUND404Document not found
FILE_NOT_FOUND404Uploaded file not found
REPORT_NOT_FOUND404Report job not found
NO_DOCUMENTS_FOR_CASE404No documents uploaded for this case
SUMMARY_NOT_FOUND404No summary generated yet
INVALID_MIME_TYPE400File type not in allowed list
FILE_TOO_LARGE400File exceeds 20 MB
INTERNAL_ERROR500Unexpected server error

Authentication

JWT Bearer tokens, HS256 signed.

Required claims: user_id (string), exp (expiration timestamp)
Header: Authorization: Bearer <token>

Generating a Test Token

import jwt
from datetime import datetime, timedelta, timezone

token = jwt.encode(
    {
        "user_id": "judge_001",
        "exp": datetime.now(timezone.utc) + timedelta(hours=24),
    },
    "your-jwt-secret",   # must match JA_API_JWT_SECRET
    algorithm="HS256",
)
print(token)

Example: End-to-End curl Workflow

export BASE=http://localhost:8000/api/v1
export TOKEN="your-jwt-token"
export H="Authorization: Bearer $TOKEN"

# 1. Create a case
curl -s -X POST $BASE/cases \
  -H "$H" -H "Content-Type: application/json" \
  -d '{"title": "قضية مدنية رقم 2024/1234"}' | jq .

# 2. Upload a scanned document
CASE_ID="<case_id from step 1>"
FILE_ID=$(curl -s -X POST $BASE/files/upload \
  -H "$H" -F "file=@/path/to/document.pdf" | jq -r .file_id)

# 3. Query the case (SSE stream)
curl -N -X POST $BASE/query \
  -H "$H" -H "Content-Type: application/json" \
  -d "{\"case_id\": \"$CASE_ID\", \"query\": \"ما هي المواد القانونية المنطبقة على هذه القضية؟\"}"

# 4. Generate full report (async)
JOB_ID=$(curl -s -X POST $BASE/cases/$CASE_ID/reports/generate \
  -H "$H" | jq -r .job_id)

# 5. Poll report status
curl -s $BASE/cases/$CASE_ID/reports/$JOB_ID -H "$H" | jq .status

Streaming Queries (SSE)

POST /api/v1/query returns text/event-stream. The client receives a sequence of typed events.

Event Types

progress — emitted as each supervisor node completes:

event: progress
data: {"step": "classify_intent", "status": "done", "intent": "civil_law_rag"}

result — the final validated Arabic answer (emitted once):

event: result
data: {
  "final_response": "وفقاً للمادة 163 من القانون المدني ...",
  "sources": [{"article": "163", "title": "المادة 163", "book": "الالتزامات"}],
  "intent": "civil_law_rag",
  "agents_used": ["civil_law_rag"],
  "conversation_id": "conv_abc123",
  "turn_count": 1
}

error — if something goes wrong:

event: error
data: {"detail": "An internal error occurred while processing the query"}

done — always the last event:

event: done
data: {}

Multi-Turn Conversations

Pass conversation_id from a previous result event to continue a thread. The supervisor loads prior history from MongoDB and uses running_summary to keep context within token limits.

JavaScript Integration

async function queryCase(caseId, question, conversationId = null) {
  const response = await fetch('/api/v1/query', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      case_id: caseId,
      query: question,
      conversation_id: conversationId,
    }),
  });

  const reader = response.body.getReader();
  const decoder = new TextDecoder();

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    const chunk = decoder.decode(value);
    for (const line of chunk.split('\n')) {
      if (line.startsWith('event: result')) continue;
      if (line.startsWith('data: ')) {
        const data = JSON.parse(line.slice(6));
        if (data.final_response) {
          displayAnswer(data.final_response);
          return data.conversation_id;   // pass to next turn
        }
      }
    }
  }
}

Report Generation

The report pipeline runs summarization and case reasoning asynchronously as a background job.

POST /api/v1/cases/{case_id}/reports/generate
  → HTTP 202 + { "job_id": "..." }

GET  /api/v1/cases/{case_id}/reports/{job_id}
  → { "status": "pending|running|completed|failed", "summary": {...}, "case_reasoning": {...} }

Pipeline sequence (background):

  1. Summarization pipeline (7-node LangGraph) → upsert to summaries
  2. Case Reasoner pipeline → upsert to case_reasonings
  3. Job status updated to completed

Polling recommendation: poll every 5–10 seconds; typical completion time is 60–120 seconds depending on document count and issue complexity.


MCP Transport Layer

The Civil Law RAG and Case Doc RAG agents run as isolated FastMCP subprocess servers, communicating over stdin/stdout using newline-delimited JSON-RPC 2.0.

Supervisor process
  │
  ├─── MCPClient("mcp_servers.legal_rag_server")
  │       │  threading.Lock (serializes calls)
  │       │  auto-respawn on transport failure (max 1 respawn)
  │       └─► subprocess: python -m mcp_servers.legal_rag_server
  │               JSON-RPC: tools/call → search_legal_corpus(query, corpus)
  │
  └─── MCPClient("mcp_servers.case_doc_server")
          │  threading.Lock
          └─► subprocess: python -m mcp_servers.case_doc_server
                  JSON-RPC: tools/call → search_case_docs(query, case_id, ...)

Timeouts: call timeout 120s · handshake timeout 600s (covers first-run model download)
Warmup: both servers warm up their LangGraph graph + Qdrant vectorstore + reranker at subprocess boot, before accepting any requests.


LLM Tier System

Three tiers map to task complexity:

TierModelUsed For
highgemini-2.5-flashLegal reasoning, response synthesis, summarization, case briefing, chat reasoner planning/synthesis
mediumgemini-2.5-flashIntent classification, document classification, query rewriting, relevance grading
lowgemini-2.5-flash-liteOutput validation, off-topic routing, thematic clustering
from config import get_llm

llm_high   = get_llm("high")
llm_medium = get_llm("medium")
llm_low    = get_llm("low")

All tiers use temperature: 0.0 by default for deterministic legal outputs.


Testing

The test suite has 100+ tests across 6 categories.

Test Markers

MarkerDescription
unitPure function tests, no external I/O
integrationTests against real MongoDB + Qdrant + LLMs
behavioralE2E multi-turn conversation and agent dispatch scenarios
performanceMemory profiling, throughput, cache speedup
regressionBoundary cases and known failure modes
llm_evalLLM-as-judge quality evaluations (RAGAS, custom rubrics)
expensiveLong-running tests (E2E multi-turn, full pipelines)

Running Tests

# Install test dependencies
pip install -r requirements.txt

# Run unit tests only (fast, no LLM calls)
pytest -m unit -v

# Run integration tests (requires all services running)
pytest -m integration -v

# Run the full suite
pytest -v

# Skip expensive LLM-eval tests
pytest -m "not expensive and not llm_eval" -v

# Run a specific subsystem
pytest tests/supervisor/ -v
pytest tests/rag/ -v
pytest tests/summarizer/ -v

# Run with parallel workers
pytest -n auto -m unit

Key Quality Thresholds

MetricThresholdTest
Supervisor routing accuracy≥ 85%test_routing_accuracy.py
RAGAS faithfulness≥ 0.80test_rag_quality.py
RAGAS context recall≥ 0.75test_rag_quality.py
Answer consistency (multi-turn)≥ 0.80test_answer_consistency.py
Cache speedup≥ 2×test_cache_speedup.py
Memory peak (summarizer)≤ 500 MBtest_memory_leak.py
Memory growth ratio≤ 1.2×test_memory_leak.py
Ingestion pipeline runtime≤ 120stest_ingestion_pipeline.py

Test Structure

tests/
  supervisor/
    unit/           — state reducers, router functions, node logic
    integration/    — full supervisor turns with real LLM + MongoDB
    behavioral/     — multi-turn conversation, intent routing accuracy
    performance/    — memory profiling, throughput
    e2e/            — 3-turn conversation, history trimming, retry count reset
  rag/
    unit/           — corpus config, cache, validation logic
    integration/    — retrieval quality, filter application
    behavioral/     — routing accuracy, boundary cases
    performance/    — cache speedup, vectorstore load
  summarizer/
    unit/           — bullet extraction, aggregation, disambiguation
    integration/    — full 7-node pipeline
    behavioral/     — party classification, theme coverage
    performance/    — memory under load
    llm_eval/       — LLM-as-judge quality scoring (EV-01 to EV-08)
  case_reasoner/
    unit/           — issue extraction, evidence classification
    integration/    — per-issue branch, consistency check
    llm_eval/       — CR-EV-01 to CR-EV-12 quality dimensions

Project Structure

Code/
├── api/                          # FastAPI application
│   ├── app.py                    #   create_app() factory, lifespan, DB init
│   ├── dependencies.py           #   JWT auth, DB, settings injection
│   ├── errors.py                 #   error code constants
│   ├── routers/                  #   route handlers
│   │   ├── cases.py
│   │   ├── files.py
│   │   ├── documents.py
│   │   ├── query.py              #   SSE streaming endpoint
│   │   ├── conversations.py
│   │   ├── summaries.py
│   │   ├── reports.py            #   async report generation
│   │   └── health.py
│   ├── schemas/                  #   Pydantic request/response models
│   ├── services/                 #   business logic
│   │   ├── query_service.py      #   supervisor graph invocation
│   │   ├── report_service.py     #   background report job
│   │   └── ...
│   └── db/                       #   Motor/Qdrant/Redis/MinIO/PostgreSQL clients
│
├── Supervisor/                   # Multi-agent supervisor
│   ├── graph.py                  #   15-node LangGraph StateGraph
│   ├── state.py                  #   SupervisorState TypedDict + Pydantic schemas
│   ├── prompts.py                #   LLM prompt templates
│   ├── agents/                   #   adapter registry + all adapters
│   │   ├── civil_law_rag_adapter.py
│   │   ├── case_doc_rag_adapter.py
│   │   ├── chat_reasoner_adapter.py
│   │   ├── summarize_adapter.py
│   │   └── ocr_adapter.py
│   └── nodes/                    #   one file per graph node
│       ├── classify_intent.py
│       ├── dispatch_agents.py
│       ├── merge_responses.py
│       ├── validate_output.py
│       ├── update_memory.py
│       ├── load_long_term_memory.py
│       ├── write_long_term_memory.py
│       ├── summarize_history.py
│       ├── audit_log.py
│       ├── enrich_context.py
│       ├── verify_citations.py
│       ├── prepare_retry.py
│       ├── fallback.py
│       ├── off_topic.py
│       └── validate_input.py
│
├── mcp_servers/                  # MCP subprocess servers
│   ├── lifecycle.py              #   start_mcp_servers(), get_client()
│   ├── client.py                 #   MCPClient (JSON-RPC 2.0 / stdio)
│   ├── legal_rag_server.py       #   FastMCP: search_legal_corpus
│   ├── case_doc_server.py        #   FastMCP: search_case_docs
│   └── errors.py                 #   ErrorCode enum, ToolError
│
├── RAG/
│   ├── legal_rag/                # Unified multi-corpus civil law RAG
│   │   ├── graph.py              #   11-node LangGraph (corpus-agnostic)
│   │   ├── service.py            #   ask_question() public entry point
│   │   ├── corpus_config.py      #   CorpusConfig frozen dataclass
│   │   ├── cache.py              #   SemanticCache
│   │   ├── state.py
│   │   ├── nodes/                #   corpus_router, preprocessor, scope_classifier, etc.
│   │   ├── retrieval/            #   embeddings.py, vectorstore.py, reranker.py
│   │   ├── civil_law_rag/        #   CIVIL_LAW_CORPUS config + docs
│   │   ├── evidence_rag/         #   EVIDENCE_CORPUS config + docs
│   │   └── procedures_rag/       #   PROCEDURES_CORPUS config + docs
│   └── case_doc_rag/             # Per-case document RAG
│       ├── graph.py              #   main graph + branch sub-graph
│       ├── routers.py            #   branchDocSelectorRouter, proceedRouter
│       ├── state.py              #   AgentState, SubQuestionState
│       └── nodes/                #   query, selection, retrieval, generation nodes
│
├── summarize/                    # 7-node summarization pipeline
│   ├── graph.py                  #   create_pipeline(), sequential + ThreadPoolExecutor
│   ├── state.py                  #   SummarizationState, disambiguate_defendants()
│   └── nodes/                    #   intake, classifier, extractor, aggregator,
│                                 #   clustering, synthesis, brief
│
├── chat_reasoner/                # Adaptive planner-executor
│   ├── graph.py                  #   build_chat_reasoner_graph()
│   ├── state.py                  #   ChatReasonerState, ALLOWED_TOOLS, Pydantic schemas
│   ├── tools.py                  #   tool implementations
│   ├── prompts.py
│   └── nodes/                    #   planner, plan_validator, executor, step_worker,
│                                 #   collector, synthesizer, replanner, trace_writer
│
├── CR/                           # Case Reasoner
│   ├── graph.py                  #   build_case_reasoner_graph(), build_issue_branch()
│   ├── pipeline.py               #   run_case_reasoning() public entry, CaseReasoningResult
│   ├── state.py                  #   CaseReasonerState, IssueAnalysisState
│   ├── routers.py
│   └── nodes/                    #   extraction, decomposition, retrieval, evidence,
│                                 #   application, counterargument, validation, package,
│                                 #   aggregation, consistency, confidence, report
│
├── DocumentProcessor/            # Document processing subsystem
│   ├── pipeline.py               #   top-level orchestration
│   ├── classifier.py             #   document type classifier
│   └── OCR/                      #   QARI OCR pipeline
│       ├── ocr_pipeline.py       #     run_ocr() — main orchestrator
│       ├── ocr_engine.py         #     QARIEngine (Qwen2VL, 8-bit quantized, singleton)
│       ├── ingestion.py          #     PDF→PIL pages (400 DPI) + image loading + validation
│       ├── restoration.py        #     CLAHE contrast normalization on LAB L-channel
│       ├── perspective_correction.py  # Canny edge-contour + adaptive-threshold dewarp
│       ├── text_reconstruction.py     # normalize_numerals() — Arabic-Indic digit conversion
│       ├── confidence.py         #     page-level + per-word token probability scoring
│       └── models.py             #     OCRPageResult, OCRDocumentResult (Pydantic)
│
├── config/                       # Centralized configuration
│   ├── settings.yaml             #   committed defaults
│   ├── __init__.py               #   AppConfig singleton, get_llm() factory
│   ├── api.py                    #   FastAPI Settings (Pydantic)
│   ├── supervisor.py             #   supervisor constants
│   └── legal_rag.py              #   RAG constants
│
├── tests/                        # Full test suite
│   ├── supervisor/
│   ├── rag/
│   ├── summarizer/
│   └── case_reasoner/
│
├── docs/                         # Documentation
│   ├── ARCHITECTURE.md
│   ├── AGENTS.md
│   ├── API.md
│   ├── DATABASE.md
│   ├── SETUP.md
│   ├── TESTING.md
│   ├── DECISIONS.md
│   ├── TROUBLESHOOTING.md
│   └── diagrams/                 # Mermaid diagrams
│       ├── architecture.mmd
│       ├── supervisor.mmd
│       ├── rag.mmd
│       ├── summarizer.mmd
│       ├── chat_reasoner.mmd
│       ├── case_reasoner.mmd
│       ├── sequences/            # 9 sequence diagrams
│       └── activity/             # 11 activity/flow diagrams
│
├── Methodology.md                # Academic methodology document
├── requirements.txt
└── config/settings.yaml          # Default configuration

Diagrams

Mermaid diagrams are in docs/diagrams/:

FileTypeShows
architecture.mmdgraph TDFull system: FastAPI → Supervisor → agents → MCP → infra
supervisor.mmdclassDiagramSupervisorState, IntentEnum, ValidationResult, adapters
rag.mmdclassDiagramLegalRAGGraph, CaseDocRAGGraph, MCPClient, both servers
summarizer.mmdclassDiagramSummarizationState, 4 enums, SummarizationGraph
chat_reasoner.mmdclassDiagramChatReasonerState, Plan/PlanStep/ToolEnum hierarchy
case_reasoner.mmdclassDiagramCaseReasonerState, IssueAnalysisState, branch + pipeline
sequences/query_lifecycle.mmdsequenceDiagramFull turn: POST /query → SSE → audit_log
sequences/chat_reasoner_flow.mmdsequenceDiagramplanner → fan-out → synthesizer/replan loop
sequences/legal_rag_routing.mmdsequenceDiagramcorpus_router → scope → embed → rerank → grade → cache
sequences/case_doc_rag_flow.mmdsequenceDiagramquestionRewriter → parallel branches → mergeAnswers
sequences/memory_lifecycle.mmdsequenceDiagramload LTM → turn → summarize_history → write LTM
sequences/report_generation.mmdsequenceDiagram202 → background task → poll
activity/intent_routing.mmdflowchart TDvalidate → classify → 5-way intent dispatch
activity/validation_retry.mmdflowchart TD4-criteria check, partial_pass caveat, retry loop
activity/case_doc_rag_branch.mmdflowchart TD3 branch modes, rephrase loop
activity/legal_rag_retrieval.mmdflowchart TDcache hit/miss, HyDE, rule/LLM grader
activity/summarization_activity.mmdflowchart TD7 nodes, ThreadPoolExecutor, disambiguate
activity/chat_reasoner_activity.mmdflowchart TDplan → validate → parallel execute → synthesize/replan
activity/case_reasoner_activity.mmdflowchart TDissue fan-out, consistency check, confidence
activity/document_ingestion_activity.mmdflowchart TDMIME/size gates, OCR tiers, embed → Qdrant
activity/report_generation_activity.mmdflowchart TDBackground task + polling loop

Troubleshooting

MCP server fails to start / handshake timeout

The first spawn of legal_rag_server or case_doc_server can take 2–10 minutes on a cold start because it downloads and warms up the BAAI/bge-m3 model and the reranker. The handshake timeout is 600s by default. Check subprocess stderr (it inherits the parent's stderr) for progress logs.

# Confirm TEI services are reachable
curl http://localhost:8080/health
curl http://localhost:8081/health

Qdrant connection refused

Qdrant must be running before the API starts. The API's lifespan hooks call ensure_indexed() at startup.

docker run -d -p 6333:6333 -p 6334:6334 qdrant/qdrant

MongoDB ObjectId serialization errors

Ensure Motor is version 3.x and that bson is installed from the pymongo package, not the standalone bson package (they conflict).

OCR confidence always low or CUDA OOM

The OCR engine is QARI (NAMAA-Space/Qari-OCR-v0.3-VL-2B-Instruct), a Qwen2VL vision-language model loaded in 8-bit quantization. It requires a CUDA GPU — CPU inference works but is extremely slow.

  • CUDA OOM: the engine catches torch.cuda.OutOfMemoryError, flushes the cache, and returns an error entry for that page. Reduce max_new_tokens (default 4000) or switch to 4-bit quantization by passing config={"quantization": "4bit"} to run_ocr().
  • Low confidence: confidence is computed from token softmax probabilities. Blurry or low-contrast images produce lower scores. The pipeline applies CLAHE contrast normalization automatically, but very degraded scans may still score low.
  • Perspective correction skipped: if the Canny edge-contour detector and the adaptive-threshold fallback both fail to find a valid page quad — or the safety guards reject the detected quad (output too small, crop ratio < 0.65, area < 35% of original) — the original image is passed through unchanged. Check logs for "Safety guard triggered" or "No page boundary detected".
  • PDF DPI: PDFs are rendered at 400 DPI by default. If images are too large for GPU VRAM, lower pdf_dpi in config.
  • No dictionary correction: the OCR prompt instructs the model to transcribe exactly as written. Spelling normalization is intentionally absent — do not expect it.

validator_error in every response

This usually means the low-tier LLM (gemini-2.5-flash-lite) is rate-limited or the GOOGLE_API_KEY is missing. Check logs for the specific LLM exception message.

History grows without bound

The summarize_history node fires only when messages_since_last_summary > threshold. Check config/supervisor.py for the MAX_CONVERSATION_TURNS constant. If conversations use the non-persistent graph (get_app()), history is not checkpointed between API calls — pass conversation_id on every request to use the persistent graph.

Redis connection errors (non-fatal)

Redis is used for optional caching and rate limiting. If Redis is unavailable, the API continues to function without caching. Errors are logged at WARNING level.


Documentation Index

DocumentLocationContents
Architecturedocs/ARCHITECTURE.mdFull graph diagrams, ADR pointers, component design
Agentsdocs/AGENTS.mdAll 5 agents: triggers, schemas, retrieval strategies
API Referencedocs/API.mdAll endpoints, request/response schemas, SSE guide
Databasedocs/DATABASE.mdMongoDB collections, Qdrant indexes, Redis, MinIO, PostgreSQL
Setupdocs/SETUP.mdFull env var reference, Docker Compose, local dev
Testingdocs/TESTING.mdTest suite structure, markers, writing new tests, CI/CD
Decisionsdocs/DECISIONS.md7 Architecture Decision Records with rationale
Troubleshootingdocs/TROUBLESHOOTING.md10 known issues with fixes and debugging guide
MethodologyMethodology.mdAcademic system methodology (Arabic)
Configurationconfig/settings.yamlAnnotated default configuration