#!/usr/bin/env python """End-to-end check on a real document: the NIST AI Risk Management Framework 1.0. Downloads NIST AI 100-1 (January 2023, 48 pages, a US-government work in the public domain), indexes it, and asks ten questions with known outcomes: six the document answers, three near misses (on-topic, but the fact is not in it) and one off-topic. Each question runs under three setups so the effect of each abstention gate shows: no gates, the retrieval-similarity gate, and the similarity gate plus the reader. This is a spot check on one document, not a benchmark; see scripts/benchmark.py for calibrated numbers. ``--retrieval`` instead measures retrieval inside the document with and without contextual chunk headers, on 22 questions graded by verbatim evidence spans (data/nist_rmf/eval.jsonl). Usage (needs the embed, reader and pdf extras; downloads ~200 MB of models once): python scripts/real_pdf_check.py python scripts/real_pdf_check.py --retrieval # embed and pdf extras only """ from __future__ import annotations import argparse import sys import tempfile import time from pathlib import Path import httpx ROOT = Path(__file__).parent.parent sys.path.insert(0, str(ROOT)) from lumen_rag import config from lumen_rag.embeddings import FastEmbedEmbedder, HashingEmbedder from lumen_rag.engine import RagEngine from lumen_rag.eval.evidence import evaluate_evidence, load_evidence_cases from lumen_rag.ingestion.loaders import load_file from lumen_rag.ingestion.pipeline import ingest_documents from lumen_rag.retrieval import Retriever from lumen_rag.store import VectorStore PDF_URL = "https://nvlpubs.nist.gov/nistpubs/ai/NIST.AI.100-1.pdf" READER = "onnx-community/roberta-base-squad2-ONNX" # (kind, question): "answerable", "near-miss" or "off-topic". QUESTIONS = [ ("answerable", "Who was the Secretary of Commerce when the AI RMF was published?"), ("answerable", "Which function is designed to be cross-cutting across the other three?"), ("answerable", "What does TEVV stand for?"), ("answerable", "Where can comments on the AI RMF Playbook be sent?"), ("answerable", "When is a formal review of the framework expected to take place?"), ("answerable", "What are the characteristics of trustworthy AI systems?"), ("near-miss", "When will version 2.0 of the AI RMF be released?"), ("near-miss", "How much does AI RMF certification cost?"), ("near-miss", "What fine does an organization pay for not following the AI RMF?"), ("off-topic", "What is the boiling point of water at sea level?"), ] SETUPS = [ ("no gates", None, ""), ("similarity >= 0.66", 0.66, ""), ("similarity >= 0.66, then reader >= -3.5", 0.66, READER), ] def download(tmp: str) -> Path: pdf = Path(tmp) / "NIST.AI.100-1.pdf" print(f"Downloading {PDF_URL}") # nvlpubs.nist.gov refuses Python's default user agent. resp = httpx.get(PDF_URL, follow_redirects=True, timeout=60, headers={"User-Agent": "lumen-rag/0.1"}) resp.raise_for_status() pdf.write_bytes(resp.content) return pdf def retrieval(pdf: Path) -> None: """hit@5 and MRR inside the document, with and without contextual chunk headers.""" cases = load_evidence_cases(ROOT / "data" / "nist_rmf" / "eval.jsonl") doc = load_file(pdf) print(f"{len(cases)} questions graded by evidence spans, k=5, 120-word chunks\n") print("| Embedder | Mode | hit@5 | MRR | hit@5 + headers | MRR + headers |") print("|---|---|---|---|---|---|") for name, embedder in (("hashing", HashingEmbedder()), ("bge-small", FastEmbedEmbedder())): stores = {} for ctx in (False, True): stores[ctx] = VectorStore(embedder.dim) ingest_documents([doc], store=stores[ctx], embedder=embedder, contextual=ctx) for mode in ("vector", "bm25", "hybrid"): plain, headed = ( evaluate_evidence(Retriever(stores[ctx], embedder, reranker=None), cases, k=5, mode=mode) for ctx in (False, True) ) print( f"| {name} | {mode} | {plain['hit@k']:.2f} | {plain['mrr']:.2f} " f"| {headed['hit@k']:.2f} | {headed['mrr']:.2f} |" ) def main() -> None: parser = argparse.ArgumentParser(description=__doc__.split("\n")[0]) parser.add_argument("--retrieval", action="store_true", help="compare retrieval with and without headers") args = parser.parse_args() config.settings.embedder = "fastembed" config.settings.reader_threshold = -3.5 with tempfile.TemporaryDirectory() as tmp: pdf = download(tmp) if args.retrieval: retrieval(pdf) return engine = RagEngine() doc = load_file(pdf) total = engine.add_documents([doc]) print(f"Indexed {pdf.name}: {total} chunks\n") summary = [] for name, threshold, reader in SETUPS: config.settings.abstain_threshold = threshold config.settings.reader_model = reader right = 0 print(f"=== {name}") for kind, question in QUESTIONS: start = time.perf_counter() result = engine.query(question, k=5, mode="vector") ms = (time.perf_counter() - start) * 1000 ok = result.abstained == (kind != "answerable") right += ok sim = "-" if result.confidence is None else f"{result.confidence:.3f}" read = "-" if result.answerability is None else f"{result.answerability:+.1f}" verdict = "refused " if result.abstained else "answered" print(f" [{'ok' if ok else 'XX'}] {kind:10} {verdict} sim={sim} reader={read} {ms:5.0f} ms {question}") summary.append((name, right)) print() print("| Setup | Right outcome |") print("|---|---|") for name, right in summary: print(f"| {name} | {right}/{len(QUESTIONS)} |") if __name__ == "__main__": main()