File size: 6,028 Bytes
710fe08 12c693a 710fe08 12c693a 710fe08 12c693a 710fe08 12c693a 710fe08 12c693a 710fe08 12c693a 710fe08 12c693a 710fe08 12c693a 710fe08 12c693a 710fe08 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | #!/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()
|