Supreetha15's picture
Upload 9 files
6f4e5d1 verified
Raw
History Blame Contribute Delete
8.1 kB
# api.py β€” Document Sentinel v2.0
# Routes: POST /classify, GET /download, GET /metrics, GET /feedback, POST /evaluate
# Three-layer detection (Regex + NER + LLM) with verification and explainability
import os
import uuid
import time
from pathlib import Path
from fastapi import FastAPI, UploadFile, File, HTTPException, Query
from fastapi.responses import FileResponse, JSONResponse
from fastapi.middleware.cors import CORSMiddleware
from utils import (
extract_text_from_pdf,
detect_and_mask,
redact_pdf,
get_pipeline_metrics,
get_feedback,
run_evaluation,
)
app = FastAPI(
title="Document Sentinel",
description=(
"Upload any PDF. "
"Three-layer PII detection (Regex + NER + LLM) with verification loop, "
"per-entity explainability, and performance metrics.\n\n"
"**Layer 1 β€” Regex**: SSN, Email, Phone, Credit Card, IP, Dates, Financial Amounts\n\n"
"**Layer 2 β€” NER (GLiNER)**: Names, Organizations, Addresses, Locations\n\n"
"**Layer 3 β€” LLM**: Context-sensitive PII, NDA clauses, implicit identifiers\n\n"
"**Verification**: Second-pass LLM confirms redaction completeness"
),
version="2.0.0",
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
UPLOAD_DIR = Path("temp_uploads")
REDACTED_DIR = Path("temp_redacted")
UPLOAD_DIR.mkdir(exist_ok=True)
REDACTED_DIR.mkdir(exist_ok=True)
# ── POST /classify ─────────────────────────────────────────────────────────────
@app.post("/classify", summary="Detect and redact PII/sensitive content in a PDF")
async def classify(file: UploadFile = File(..., description="PDF document to analyse")):
"""
**Upload a PDF. Returns:**
- `entities` β€” every sensitive entity found with:
- `entity`: the original value
- `label`: what type it is (EMAIL, PERSON_NAME, FINANCIAL_PENALTY, etc.)
- `method`: how it was detected (`regex`, `ner`, or `llm`)
- `confidence`: 0.0–1.0
- `reason`: context-specific AI explanation of WHY this is sensitive
- `masked_text` β€” full document text with all entities replaced by `[LABEL]`
- `redacted_pdf_url` β€” download link for the redacted PDF
- `layer_breakdown` β€” per-layer detection statistics
- `verification` β€” second-pass sanitization score and any missed entities
- `processing_time_ms` β€” end-to-end latency
"""
if not file.filename.lower().endswith(".pdf"):
raise HTTPException(status_code=400, detail="Only PDF files are accepted.")
t_start = time.time()
doc_id = str(uuid.uuid4())
in_path = UPLOAD_DIR / f"{doc_id}_original.pdf"
out_path = REDACTED_DIR / f"{doc_id}_redacted.pdf"
# Save upload
contents = await file.read()
with open(in_path, "wb") as f:
f.write(contents)
# Step 1 β€” Extract text
raw_text = extract_text_from_pdf(str(in_path))
if not raw_text.strip():
raise HTTPException(
status_code=422,
detail="Could not extract text from this PDF. It may be corrupt or fully image-based without OCR support."
)
# Step 2 β€” Detect + mask (Regex + NER + LLM + Verification)
masked_text, entities, report = detect_and_mask(raw_text)
# Step 3 β€” Redact original PDF
redact_pdf(str(in_path), str(out_path), entities)
elapsed_ms = round((time.time() - t_start) * 1000, 1)
# Step 4 β€” Build response
return JSONResponse({
"document_id": doc_id,
"original_filename": file.filename,
"processing_time_ms": elapsed_ms,
"document_type": report.get("document_type", "unknown"),
"sanitization_score": report.get("sanitization_score", 85.0),
# Entity report β€” the "responsible AI" output
"summary": {
"total_entities": len(entities),
"by_method": {
"regex": sum(1 for e in entities if e["method"] == "regex"),
"ner": sum(1 for e in entities if e["method"] == "ner"),
"llm": sum(1 for e in entities if e["method"] == "llm"),
"verification": sum(1 for e in entities if e["method"] == "verification"),
},
"by_label": _count_by_label(entities),
},
"layer_breakdown": report.get("layer_breakdown", {}),
"verification": report.get("verification"),
"entities": entities,
"masked_text": masked_text,
"redacted_pdf_url": f"/download/{doc_id}",
})
# ── GET /download/{doc_id} ────────────────────────────────────────────────────
@app.get("/download/{doc_id}", summary="Download the redacted PDF")
async def download(doc_id: str):
path = REDACTED_DIR / f"{doc_id}_redacted.pdf"
if not path.exists():
raise HTTPException(status_code=404, detail="Redacted PDF not found or expired.")
return FileResponse(
path=str(path),
media_type="application/pdf",
filename=f"redacted_{doc_id[:8]}.pdf",
)
# ── GET /metrics ──────────────────────────────────────────────────────────────
@app.get("/metrics", summary="Pipeline performance metrics")
async def metrics():
"""
Returns aggregated metrics across all processed documents:
- Total documents processed
- Average sanitization score
- Layer contribution percentages (regex / ner / llm / verification)
- Entity type distribution
- Verification pass rate
"""
return JSONResponse(get_pipeline_metrics())
# ── GET /feedback/{doc_id} ────────────────────────────────────────────────────
@app.get("/feedback/{doc_id}", summary="Per-entity feedback and explanations")
async def feedback(doc_id: str):
"""
Returns per-entity feedback for a processed document:
- Each entity with its context-specific explanation
- Detection method and confidence
- Verification status
- Layer breakdown
"""
result = get_feedback(doc_id)
if "error" in result:
raise HTTPException(status_code=404, detail=result["error"])
return JSONResponse(result)
# ── POST /evaluate ────────────────────────────────────────────────────────────
@app.post("/evaluate", summary="Run evaluation on documents")
async def evaluate():
"""
Generates synthetic documents with known PII ground truth,
runs the full pipeline, and computes precision/recall/F1:
- Per entity type (SSN, EMAIL, PERSON_NAME, etc.)
- Per detection layer (regex, ner, llm)
- Overall pipeline metrics
"""
results = run_evaluation(n_docs=20, use_llm=True)
return JSONResponse(results)
# ── GET /health ───────────────────────────────────────────────────────────────
@app.get("/health")
async def health():
return {
"status": "ok",
"service": "Document Sentinel v2.0",
"provider": os.getenv("LLM_PROVIDER", "groq"),
"layers": ["regex", "ner (GLiNER)", "llm", "verification"],
}
# ── Util ──────────────────────────────────────────────────────────────────────
def _count_by_label(entities):
counts = {}
for e in entities:
counts[e["label"]] = counts.get(e["label"], 0) + 1
return counts