Upload 21 files
Browse files- LICENSE +21 -0
- MIGRATION_NOTES.md +3 -0
- README.md +111 -1
- VALIDATION.md +12 -0
- artifacts/framework_rag_benchmark_results.json +299 -0
- artifacts/rag_benchmark_results.json +784 -0
- evaluation/__init__.py +1 -0
- evaluation/framework_rag_benchmark.py +58 -0
- evaluation/rag_benchmark.py +206 -0
- examples/quickstart.py +7 -0
- pyproject.toml +31 -0
- requirements.txt +4 -0
- src/bayesian_langgraph/__init__.py +21 -0
- src/bayesian_langgraph/core.py +255 -0
- src/bayesian_langgraph/graph.py +96 -0
- src/bayesian_langgraph/smc.py +129 -0
- tests/test_core.py +16 -0
- tests/test_framework_rag_e2e.py +8 -0
- tests/test_langgraph_integration.py +31 -0
- tests/test_rag_e2e.py +9 -0
- tests/test_smc_math.py +52 -0
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2026
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
MIGRATION_NOTES.md
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Migration notes
|
| 2 |
+
|
| 3 |
+
The two supplied LangGraph archives were duplicates. Their custom runtime did not instantiate LangGraph. This repository replaces that loop with a real `StateGraph`; the old Colab script and simulated web/official tools are not carried forward as production components. The SMC utility remains available separately, with safe log-weight normalization, importance correction, and no final-step resampling.
|
README.md
CHANGED
|
@@ -1,3 +1,113 @@
|
|
| 1 |
---
|
| 2 |
-
license:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
license: mit
|
| 3 |
+
tags:
|
| 4 |
+
- langgraph
|
| 5 |
+
- rag
|
| 6 |
+
- bayesian
|
| 7 |
+
- probabilistic
|
| 8 |
+
- uncertainty
|
| 9 |
+
- retrieval
|
| 10 |
---
|
| 11 |
+
|
| 12 |
+
# Bayesian LangGraph RAG
|
| 13 |
+
|
| 14 |
+
A corrected, Hugging Face-ready Bayesian RAG implementation using a **real LangGraph `StateGraph`**.
|
| 15 |
+
|
| 16 |
+
The original repository used a custom Python loop under the LangGraph name and could erase its final SMC posterior through final-step resampling. This version separates retrieval inference from orchestration and uses the graph:
|
| 17 |
+
|
| 18 |
+
`START -> retrieve -> assess -> (generate | abstain) -> END`
|
| 19 |
+
|
| 20 |
+
The retriever uses the generalized-Bayesian posterior
|
| 21 |
+
|
| 22 |
+
`p(d | q) ∝ p(d) (similarity(q,d) + eps)^tau`
|
| 23 |
+
|
| 24 |
+
with a Beta reliability prior plus authority and freshness metadata.
|
| 25 |
+
|
| 26 |
+
## What was corrected
|
| 27 |
+
|
| 28 |
+
- Replaced the custom pseudo-LangGraph runtime with an actual `StateGraph`.
|
| 29 |
+
- Removed Colab-only setup, Drive mounts, repository cloning, and import-time execution.
|
| 30 |
+
- Fixed all-`-inf` log-weight normalization.
|
| 31 |
+
- Fixed final-step resampling so the posterior cannot be reset to uniform.
|
| 32 |
+
- Added proposal-density correction to the reusable SMC implementation.
|
| 33 |
+
- Removed deterministic identical-particle demos as evidence of inference quality.
|
| 34 |
+
- Replaced hard-coded "Bayes wins" benchmark flags with end-to-end document retrieval and grounded answering.
|
| 35 |
+
- Added uncertainty-aware abstention for missing evidence and unresolved source conflicts.
|
| 36 |
+
- Added reproducible RAG evaluation with a plain TF-IDF baseline.
|
| 37 |
+
|
| 38 |
+
## Install
|
| 39 |
+
|
| 40 |
+
```bash
|
| 41 |
+
pip install -e .
|
| 42 |
+
```
|
| 43 |
+
|
| 44 |
+
No GPU is required. The default generator is extractive and citation-grounded; a model-backed generator can be supplied if desired.
|
| 45 |
+
|
| 46 |
+
## Quick start
|
| 47 |
+
|
| 48 |
+
```python
|
| 49 |
+
from bayesian_langgraph import DocumentRecord, BayesianIndex, BayesianRAGGraph
|
| 50 |
+
|
| 51 |
+
docs = [
|
| 52 |
+
DocumentRecord(
|
| 53 |
+
id="official",
|
| 54 |
+
title="Current portal policy",
|
| 55 |
+
text="The current HR portal is PeopleHub.",
|
| 56 |
+
source="official",
|
| 57 |
+
metadata={"reliability_alpha": 18, "reliability_beta": 2, "authority": 0.98, "freshness": 1.0},
|
| 58 |
+
),
|
| 59 |
+
DocumentRecord(
|
| 60 |
+
id="legacy",
|
| 61 |
+
title="HR portal guide",
|
| 62 |
+
text="The HR portal is WorkSphere.",
|
| 63 |
+
source="legacy-cache",
|
| 64 |
+
metadata={"reliability_alpha": 6, "reliability_beta": 6, "authority": 0.45, "freshness": 0.18},
|
| 65 |
+
),
|
| 66 |
+
]
|
| 67 |
+
|
| 68 |
+
graph_rag = BayesianRAGGraph(BayesianIndex(docs))
|
| 69 |
+
print(graph_rag.invoke("What is the current HR portal?"))
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
## RAG validation
|
| 73 |
+
|
| 74 |
+
Run:
|
| 75 |
+
|
| 76 |
+
```bash
|
| 77 |
+
PYTHONPATH=src:. python evaluation/rag_benchmark.py
|
| 78 |
+
```
|
| 79 |
+
|
| 80 |
+
The committed result is in `artifacts/rag_benchmark_results.json`.
|
| 81 |
+
|
| 82 |
+
| Suite | System | Recall@1 | MRR | Answer accuracy | Abstention accuracy | End-to-end accuracy |
|
| 83 |
+
|---|---|---:|---:|---:|---:|---:|
|
| 84 |
+
| Clean RAG (8 QA) | Bayesian | 1.000 | 1.000 | 1.000 | n/a | 1.000 |
|
| 85 |
+
| Clean RAG (8 QA) | Plain TF-IDF | 1.000 | 1.000 | 1.000 | n/a | 1.000 |
|
| 86 |
+
| Conflict/missing RAG (18 QA) | Bayesian | 1.000 | 1.000 | 1.000 | 1.000 | 1.000 |
|
| 87 |
+
| Conflict/missing RAG (18 QA) | Plain TF-IDF | 0.250 | 0.611 | 0.250 | 0.333 | 0.278 |
|
| 88 |
+
|
| 89 |
+
These are controlled end-to-end repository tests, not a claim of universal RAG accuracy.
|
| 90 |
+
|
| 91 |
+
### Framework-level LangGraph RAG test
|
| 92 |
+
|
| 93 |
+
`evaluation/framework_rag_benchmark.py` executes every case through the compiled LangGraph graph itself. The validated topology is:
|
| 94 |
+
|
| 95 |
+
`START -> retrieve -> assess -> {generate | abstain} -> END`.
|
| 96 |
+
|
| 97 |
+
It was validated against **langgraph 1.2.16** with **langchain-core 1.6.11**. Results: **8/8 clean cases** and **18/18 conflict/missing-evidence cases** passed end-to-end. The committed output is `artifacts/framework_rag_benchmark_results.json`.
|
| 98 |
+
|
| 99 |
+
Run it with:
|
| 100 |
+
|
| 101 |
+
```bash
|
| 102 |
+
PYTHONPATH=src:. python evaluation/framework_rag_benchmark.py
|
| 103 |
+
```
|
| 104 |
+
|
| 105 |
+
## Layout
|
| 106 |
+
|
| 107 |
+
- `src/bayesian_langgraph/core.py` — posterior retrieval, abstention, grounded answerer
|
| 108 |
+
- `src/bayesian_langgraph/graph.py` — actual LangGraph `StateGraph`
|
| 109 |
+
- `src/bayesian_langgraph/smc.py` — corrected importance-weighted SMC utility
|
| 110 |
+
- `evaluation/rag_benchmark.py` — controlled retrieval/grounding benchmark
|
| 111 |
+
- `evaluation/framework_rag_benchmark.py` — compiled `StateGraph` RAG benchmark
|
| 112 |
+
- `tests/` — regression and graph-integration tests
|
| 113 |
+
- `VALIDATION.md` — exact framework/version validation record
|
VALIDATION.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Validation
|
| 2 |
+
|
| 3 |
+
Validated on Python 3.13.5 against the supplied `langgraph 1.2.16` and `langchain-core 1.6.11` source archives.
|
| 4 |
+
|
| 5 |
+
- Repository tests: 8 passed.
|
| 6 |
+
- Framework RAG suite: 8/8 clean cases passed.
|
| 7 |
+
- Framework RAG suite: 18/18 stale/conflicting/missing-evidence cases passed.
|
| 8 |
+
- Package wheel build: passed.
|
| 9 |
+
- The orchestration is a real compiled `langgraph.graph.StateGraph`.
|
| 10 |
+
- Validated graph nodes: `START`, `retrieve`, `assess`, `generate`, `abstain`, `END`.
|
| 11 |
+
|
| 12 |
+
The sandbox cannot resolve PyPI. For framework-source validation only, a test harness supplied minimal compatibility modules for unavailable peripheral tracing/checkpoint/SDK dependencies. Those test shims are not part of this repository; the graph builder/executor under test is the supplied LangGraph source.
|
artifacts/framework_rag_benchmark_results.json
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"framework": "langgraph",
|
| 3 |
+
"stategraph_nodes": [
|
| 4 |
+
"__end__",
|
| 5 |
+
"__start__",
|
| 6 |
+
"abstain",
|
| 7 |
+
"assess",
|
| 8 |
+
"generate",
|
| 9 |
+
"retrieve"
|
| 10 |
+
],
|
| 11 |
+
"stategraph_edges": [
|
| 12 |
+
[
|
| 13 |
+
"__start__",
|
| 14 |
+
"retrieve"
|
| 15 |
+
],
|
| 16 |
+
[
|
| 17 |
+
"abstain",
|
| 18 |
+
"__end__"
|
| 19 |
+
],
|
| 20 |
+
[
|
| 21 |
+
"assess",
|
| 22 |
+
"abstain"
|
| 23 |
+
],
|
| 24 |
+
[
|
| 25 |
+
"assess",
|
| 26 |
+
"generate"
|
| 27 |
+
],
|
| 28 |
+
[
|
| 29 |
+
"generate",
|
| 30 |
+
"__end__"
|
| 31 |
+
],
|
| 32 |
+
[
|
| 33 |
+
"retrieve",
|
| 34 |
+
"assess"
|
| 35 |
+
]
|
| 36 |
+
],
|
| 37 |
+
"clean": {
|
| 38 |
+
"n_cases": 8,
|
| 39 |
+
"passed": 8,
|
| 40 |
+
"accuracy": 1.0,
|
| 41 |
+
"rows": [
|
| 42 |
+
{
|
| 43 |
+
"id": "c1",
|
| 44 |
+
"ok": true,
|
| 45 |
+
"answer": "The observatory opens to visitors at 9 AM every Saturday. [C1]",
|
| 46 |
+
"citations": [
|
| 47 |
+
"C1"
|
| 48 |
+
],
|
| 49 |
+
"abstained": false,
|
| 50 |
+
"top_doc": "C1"
|
| 51 |
+
},
|
| 52 |
+
{
|
| 53 |
+
"id": "c2",
|
| 54 |
+
"ok": true,
|
| 55 |
+
"answer": "The chemistry safety briefing is held in Hall 7. [C2]",
|
| 56 |
+
"citations": [
|
| 57 |
+
"C2"
|
| 58 |
+
],
|
| 59 |
+
"abstained": false,
|
| 60 |
+
"top_doc": "C2"
|
| 61 |
+
},
|
| 62 |
+
{
|
| 63 |
+
"id": "c3",
|
| 64 |
+
"ok": true,
|
| 65 |
+
"answer": "Library books may be borrowed for 21 days. [C3]",
|
| 66 |
+
"citations": [
|
| 67 |
+
"C3"
|
| 68 |
+
],
|
| 69 |
+
"abstained": false,
|
| 70 |
+
"top_doc": "C3"
|
| 71 |
+
},
|
| 72 |
+
{
|
| 73 |
+
"id": "c4",
|
| 74 |
+
"ok": true,
|
| 75 |
+
"answer": "The robotics club meets on Thursday evenings. [C4]",
|
| 76 |
+
"citations": [
|
| 77 |
+
"C4"
|
| 78 |
+
],
|
| 79 |
+
"abstained": false,
|
| 80 |
+
"top_doc": "C4"
|
| 81 |
+
},
|
| 82 |
+
{
|
| 83 |
+
"id": "c5",
|
| 84 |
+
"ok": true,
|
| 85 |
+
"answer": "Visitor parking is located beside Gate 3. [C5]",
|
| 86 |
+
"citations": [
|
| 87 |
+
"C5"
|
| 88 |
+
],
|
| 89 |
+
"abstained": false,
|
| 90 |
+
"top_doc": "C5"
|
| 91 |
+
},
|
| 92 |
+
{
|
| 93 |
+
"id": "c6",
|
| 94 |
+
"ok": true,
|
| 95 |
+
"answer": "The campus shuttle begins service at 6:30 AM. [C6]",
|
| 96 |
+
"citations": [
|
| 97 |
+
"C6"
|
| 98 |
+
],
|
| 99 |
+
"abstained": false,
|
| 100 |
+
"top_doc": "C6"
|
| 101 |
+
},
|
| 102 |
+
{
|
| 103 |
+
"id": "c7",
|
| 104 |
+
"ok": true,
|
| 105 |
+
"answer": "The data science seminar carries two academic credits. [C7]",
|
| 106 |
+
"citations": [
|
| 107 |
+
"C7"
|
| 108 |
+
],
|
| 109 |
+
"abstained": false,
|
| 110 |
+
"top_doc": "C7"
|
| 111 |
+
},
|
| 112 |
+
{
|
| 113 |
+
"id": "c8",
|
| 114 |
+
"ok": true,
|
| 115 |
+
"answer": "The emergency assembly point is the North Lawn. [C8]",
|
| 116 |
+
"citations": [
|
| 117 |
+
"C8"
|
| 118 |
+
],
|
| 119 |
+
"abstained": false,
|
| 120 |
+
"top_doc": "C8"
|
| 121 |
+
}
|
| 122 |
+
]
|
| 123 |
+
},
|
| 124 |
+
"conflict": {
|
| 125 |
+
"n_cases": 18,
|
| 126 |
+
"passed": 18,
|
| 127 |
+
"accuracy": 1.0,
|
| 128 |
+
"rows": [
|
| 129 |
+
{
|
| 130 |
+
"id": "q1",
|
| 131 |
+
"ok": true,
|
| 132 |
+
"answer": "All employee self-service tasks, including leave requests and payslips, are now handled in PeopleHub. [OFF-HR-PORTAL]",
|
| 133 |
+
"citations": [
|
| 134 |
+
"OFF-HR-PORTAL"
|
| 135 |
+
],
|
| 136 |
+
"abstained": false,
|
| 137 |
+
"top_doc": "OFF-HR-PORTAL"
|
| 138 |
+
},
|
| 139 |
+
{
|
| 140 |
+
"id": "q2",
|
| 141 |
+
"ok": true,
|
| 142 |
+
"answer": "The 2026 employee benefits policy grants 18 paid leave days per calendar year. [OFF-LEAVE]",
|
| 143 |
+
"citations": [
|
| 144 |
+
"OFF-LEAVE"
|
| 145 |
+
],
|
| 146 |
+
"abstained": false,
|
| 147 |
+
"top_doc": "OFF-LEAVE"
|
| 148 |
+
},
|
| 149 |
+
{
|
| 150 |
+
"id": "q3",
|
| 151 |
+
"ok": true,
|
| 152 |
+
"answer": "Under the active flexible-work policy, employees may work remotely up to two days per week with manager approval. [OFF-REMOTE]",
|
| 153 |
+
"citations": [
|
| 154 |
+
"OFF-REMOTE"
|
| 155 |
+
],
|
| 156 |
+
"abstained": false,
|
| 157 |
+
"top_doc": "OFF-REMOTE"
|
| 158 |
+
},
|
| 159 |
+
{
|
| 160 |
+
"id": "q4",
|
| 161 |
+
"ok": true,
|
| 162 |
+
"answer": "Payroll publishes salary slips by the fifth working day of each month. [OFF-PAYSLIP]",
|
| 163 |
+
"citations": [
|
| 164 |
+
"OFF-PAYSLIP"
|
| 165 |
+
],
|
| 166 |
+
"abstained": false,
|
| 167 |
+
"top_doc": "OFF-PAYSLIP"
|
| 168 |
+
},
|
| 169 |
+
{
|
| 170 |
+
"id": "q5",
|
| 171 |
+
"ok": true,
|
| 172 |
+
"answer": "The security baseline now requires WireGuard for the standard corporate VPN profile. [OFF-VPN]",
|
| 173 |
+
"citations": [
|
| 174 |
+
"OFF-VPN"
|
| 175 |
+
],
|
| 176 |
+
"abstained": false,
|
| 177 |
+
"top_doc": "OFF-VPN"
|
| 178 |
+
},
|
| 179 |
+
{
|
| 180 |
+
"id": "q6",
|
| 181 |
+
"ok": true,
|
| 182 |
+
"answer": "Production database backups have a retention period of 30 days. [OFF-BACKUP]",
|
| 183 |
+
"citations": [
|
| 184 |
+
"OFF-BACKUP"
|
| 185 |
+
],
|
| 186 |
+
"abstained": false,
|
| 187 |
+
"top_doc": "OFF-BACKUP"
|
| 188 |
+
},
|
| 189 |
+
{
|
| 190 |
+
"id": "q7",
|
| 191 |
+
"ok": true,
|
| 192 |
+
"answer": "The current gateway quota permits 120 requests per minute per token. [OFF-RATE]",
|
| 193 |
+
"citations": [
|
| 194 |
+
"OFF-RATE"
|
| 195 |
+
],
|
| 196 |
+
"abstained": false,
|
| 197 |
+
"top_doc": "OFF-RATE"
|
| 198 |
+
},
|
| 199 |
+
{
|
| 200 |
+
"id": "q8",
|
| 201 |
+
"ok": true,
|
| 202 |
+
"answer": "The production primary region is ap-south-1; disaster recovery remains in a separate region. [OFF-REGION]",
|
| 203 |
+
"citations": [
|
| 204 |
+
"OFF-REGION"
|
| 205 |
+
],
|
| 206 |
+
"abstained": false,
|
| 207 |
+
"top_doc": "OFF-REGION"
|
| 208 |
+
},
|
| 209 |
+
{
|
| 210 |
+
"id": "q9",
|
| 211 |
+
"ok": true,
|
| 212 |
+
"answer": "For domestic business travel, reimbursable meals are capped at INR 4,500 per day. [OFF-EXPENSE]",
|
| 213 |
+
"citations": [
|
| 214 |
+
"OFF-EXPENSE"
|
| 215 |
+
],
|
| 216 |
+
"abstained": false,
|
| 217 |
+
"top_doc": "OFF-EXPENSE"
|
| 218 |
+
},
|
| 219 |
+
{
|
| 220 |
+
"id": "q10",
|
| 221 |
+
"ok": true,
|
| 222 |
+
"answer": "Expense claims must be submitted within 15 days of the transaction date. [OFF-REIMB]",
|
| 223 |
+
"citations": [
|
| 224 |
+
"OFF-REIMB"
|
| 225 |
+
],
|
| 226 |
+
"abstained": false,
|
| 227 |
+
"top_doc": "OFF-REIMB"
|
| 228 |
+
},
|
| 229 |
+
{
|
| 230 |
+
"id": "q11",
|
| 231 |
+
"ok": true,
|
| 232 |
+
"answer": "Approved business travel must be booked through TripDesk. [OFF-TRAVEL]",
|
| 233 |
+
"citations": [
|
| 234 |
+
"OFF-TRAVEL"
|
| 235 |
+
],
|
| 236 |
+
"abstained": false,
|
| 237 |
+
"top_doc": "OFF-TRAVEL"
|
| 238 |
+
},
|
| 239 |
+
{
|
| 240 |
+
"id": "q12",
|
| 241 |
+
"ok": true,
|
| 242 |
+
"answer": "Procurement review is mandatory for purchases above INR 50,000. [OFF-PROC]",
|
| 243 |
+
"citations": [
|
| 244 |
+
"OFF-PROC"
|
| 245 |
+
],
|
| 246 |
+
"abstained": false,
|
| 247 |
+
"top_doc": "OFF-PROC"
|
| 248 |
+
},
|
| 249 |
+
{
|
| 250 |
+
"id": "q13",
|
| 251 |
+
"ok": true,
|
| 252 |
+
"answer": "I do not know based on the provided documents.",
|
| 253 |
+
"citations": [],
|
| 254 |
+
"abstained": true,
|
| 255 |
+
"top_doc": "LEG-RATE"
|
| 256 |
+
},
|
| 257 |
+
{
|
| 258 |
+
"id": "q14",
|
| 259 |
+
"ok": true,
|
| 260 |
+
"answer": "I do not know based on the provided documents.",
|
| 261 |
+
"citations": [],
|
| 262 |
+
"abstained": true,
|
| 263 |
+
"top_doc": "OFF-HR-PORTAL"
|
| 264 |
+
},
|
| 265 |
+
{
|
| 266 |
+
"id": "q15",
|
| 267 |
+
"ok": true,
|
| 268 |
+
"answer": "I do not know based on the provided documents.",
|
| 269 |
+
"citations": [],
|
| 270 |
+
"abstained": true,
|
| 271 |
+
"top_doc": "OFF-HR-PORTAL"
|
| 272 |
+
},
|
| 273 |
+
{
|
| 274 |
+
"id": "q16",
|
| 275 |
+
"ok": true,
|
| 276 |
+
"answer": "I do not know based on the provided documents.",
|
| 277 |
+
"citations": [],
|
| 278 |
+
"abstained": true,
|
| 279 |
+
"top_doc": "OFF-REIMB"
|
| 280 |
+
},
|
| 281 |
+
{
|
| 282 |
+
"id": "q17",
|
| 283 |
+
"ok": true,
|
| 284 |
+
"answer": "I do not know based on the provided documents.",
|
| 285 |
+
"citations": [],
|
| 286 |
+
"abstained": true,
|
| 287 |
+
"top_doc": "OFF-LAB-A"
|
| 288 |
+
},
|
| 289 |
+
{
|
| 290 |
+
"id": "q18",
|
| 291 |
+
"ok": true,
|
| 292 |
+
"answer": "I do not know based on the provided documents.",
|
| 293 |
+
"citations": [],
|
| 294 |
+
"abstained": true,
|
| 295 |
+
"top_doc": "OFF-OFFICE-A"
|
| 296 |
+
}
|
| 297 |
+
]
|
| 298 |
+
}
|
| 299 |
+
}
|
artifacts/rag_benchmark_results.json
ADDED
|
@@ -0,0 +1,784 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"benchmark": "controlled_rag_v2",
|
| 3 |
+
"methodology": "Two end-to-end suites. CLEAN checks ordinary retrieval+grounded answer synthesis. CONFLICT adds stale authoritative conflicts, missing evidence, and unresolved equal-authority contradictions. The generator is extractive and citation-grounded so the evaluation isolates retrieval and uncertainty behavior without an external LLM confound.",
|
| 4 |
+
"clean": {
|
| 5 |
+
"bayesian": {
|
| 6 |
+
"system": "bayesian",
|
| 7 |
+
"n_cases": 8,
|
| 8 |
+
"answerable_cases": 8,
|
| 9 |
+
"abstention_cases": 0,
|
| 10 |
+
"recall_at_1": 1.0,
|
| 11 |
+
"recall_at_3": 1.0,
|
| 12 |
+
"mrr": 1.0,
|
| 13 |
+
"end_to_end_accuracy": 1.0,
|
| 14 |
+
"grounding_accuracy": 1.0,
|
| 15 |
+
"abstention_accuracy": null,
|
| 16 |
+
"answer_accuracy_on_answerable": 1.0,
|
| 17 |
+
"top_posterior_brier": 2.8128197163138293e-05
|
| 18 |
+
},
|
| 19 |
+
"baseline": {
|
| 20 |
+
"system": "plain_tfidf",
|
| 21 |
+
"n_cases": 8,
|
| 22 |
+
"answerable_cases": 8,
|
| 23 |
+
"abstention_cases": 0,
|
| 24 |
+
"recall_at_1": 1.0,
|
| 25 |
+
"recall_at_3": 1.0,
|
| 26 |
+
"mrr": 1.0,
|
| 27 |
+
"end_to_end_accuracy": 1.0,
|
| 28 |
+
"grounding_accuracy": 1.0,
|
| 29 |
+
"abstention_accuracy": null,
|
| 30 |
+
"answer_accuracy_on_answerable": 1.0,
|
| 31 |
+
"top_posterior_brier": 0.0
|
| 32 |
+
},
|
| 33 |
+
"bayesian_cases": [
|
| 34 |
+
{
|
| 35 |
+
"id": "c1",
|
| 36 |
+
"query": "When does the observatory open on Saturday?",
|
| 37 |
+
"expected_abstain": false,
|
| 38 |
+
"top_doc": "C1",
|
| 39 |
+
"retrieval_score": 0.552814,
|
| 40 |
+
"top_posterior": 0.991539,
|
| 41 |
+
"answer": "The observatory opens to visitors at 9 AM every Saturday. [C1]",
|
| 42 |
+
"citations": [
|
| 43 |
+
"C1"
|
| 44 |
+
],
|
| 45 |
+
"correct": true,
|
| 46 |
+
"grounded": true
|
| 47 |
+
},
|
| 48 |
+
{
|
| 49 |
+
"id": "c2",
|
| 50 |
+
"query": "Where is the chemistry safety briefing held?",
|
| 51 |
+
"expected_abstain": false,
|
| 52 |
+
"top_doc": "C2",
|
| 53 |
+
"retrieval_score": 0.825787,
|
| 54 |
+
"top_posterior": 0.996101,
|
| 55 |
+
"answer": "The chemistry safety briefing is held in Hall 7. [C2]",
|
| 56 |
+
"citations": [
|
| 57 |
+
"C2"
|
| 58 |
+
],
|
| 59 |
+
"correct": true,
|
| 60 |
+
"grounded": true
|
| 61 |
+
},
|
| 62 |
+
{
|
| 63 |
+
"id": "c3",
|
| 64 |
+
"query": "How long may library books be borrowed?",
|
| 65 |
+
"expected_abstain": false,
|
| 66 |
+
"top_doc": "C3",
|
| 67 |
+
"retrieval_score": 0.683723,
|
| 68 |
+
"top_posterior": 0.994378,
|
| 69 |
+
"answer": "Library books may be borrowed for 21 days. [C3]",
|
| 70 |
+
"citations": [
|
| 71 |
+
"C3"
|
| 72 |
+
],
|
| 73 |
+
"correct": true,
|
| 74 |
+
"grounded": true
|
| 75 |
+
},
|
| 76 |
+
{
|
| 77 |
+
"id": "c4",
|
| 78 |
+
"query": "When does the robotics club meet?",
|
| 79 |
+
"expected_abstain": false,
|
| 80 |
+
"top_doc": "C4",
|
| 81 |
+
"retrieval_score": 0.742488,
|
| 82 |
+
"top_posterior": 0.995207,
|
| 83 |
+
"answer": "The robotics club meets on Thursday evenings. [C4]",
|
| 84 |
+
"citations": [
|
| 85 |
+
"C4"
|
| 86 |
+
],
|
| 87 |
+
"correct": true,
|
| 88 |
+
"grounded": true
|
| 89 |
+
},
|
| 90 |
+
{
|
| 91 |
+
"id": "c5",
|
| 92 |
+
"query": "Where is visitor parking?",
|
| 93 |
+
"expected_abstain": false,
|
| 94 |
+
"top_doc": "C5",
|
| 95 |
+
"retrieval_score": 0.79521,
|
| 96 |
+
"top_posterior": 0.995804,
|
| 97 |
+
"answer": "Visitor parking is located beside Gate 3. [C5]",
|
| 98 |
+
"citations": [
|
| 99 |
+
"C5"
|
| 100 |
+
],
|
| 101 |
+
"correct": true,
|
| 102 |
+
"grounded": true
|
| 103 |
+
},
|
| 104 |
+
{
|
| 105 |
+
"id": "c6",
|
| 106 |
+
"query": "What time does the campus shuttle begin service?",
|
| 107 |
+
"expected_abstain": false,
|
| 108 |
+
"top_doc": "C6",
|
| 109 |
+
"retrieval_score": 0.769605,
|
| 110 |
+
"top_posterior": 0.995529,
|
| 111 |
+
"answer": "The campus shuttle begins service at 6:30 AM. [C6]",
|
| 112 |
+
"citations": [
|
| 113 |
+
"C6"
|
| 114 |
+
],
|
| 115 |
+
"correct": true,
|
| 116 |
+
"grounded": true
|
| 117 |
+
},
|
| 118 |
+
{
|
| 119 |
+
"id": "c7",
|
| 120 |
+
"query": "How many credits is the data science seminar worth?",
|
| 121 |
+
"expected_abstain": false,
|
| 122 |
+
"top_doc": "C7",
|
| 123 |
+
"retrieval_score": 0.836653,
|
| 124 |
+
"top_posterior": 0.996199,
|
| 125 |
+
"answer": "The data science seminar carries two academic credits. [C7]",
|
| 126 |
+
"citations": [
|
| 127 |
+
"C7"
|
| 128 |
+
],
|
| 129 |
+
"correct": true,
|
| 130 |
+
"grounded": true
|
| 131 |
+
},
|
| 132 |
+
{
|
| 133 |
+
"id": "c8",
|
| 134 |
+
"query": "Where is the emergency assembly point?",
|
| 135 |
+
"expected_abstain": false,
|
| 136 |
+
"top_doc": "C8",
|
| 137 |
+
"retrieval_score": 0.683723,
|
| 138 |
+
"top_posterior": 0.994378,
|
| 139 |
+
"answer": "The emergency assembly point is the North Lawn. [C8]",
|
| 140 |
+
"citations": [
|
| 141 |
+
"C8"
|
| 142 |
+
],
|
| 143 |
+
"correct": true,
|
| 144 |
+
"grounded": true
|
| 145 |
+
}
|
| 146 |
+
],
|
| 147 |
+
"baseline_cases": [
|
| 148 |
+
{
|
| 149 |
+
"id": "c1",
|
| 150 |
+
"query": "When does the observatory open on Saturday?",
|
| 151 |
+
"expected_abstain": false,
|
| 152 |
+
"top_doc": "C1",
|
| 153 |
+
"retrieval_score": 0.552814,
|
| 154 |
+
"top_posterior": 1.0,
|
| 155 |
+
"answer": "The observatory opens to visitors at 9 AM every Saturday. [C1]",
|
| 156 |
+
"citations": [
|
| 157 |
+
"C1"
|
| 158 |
+
],
|
| 159 |
+
"correct": true,
|
| 160 |
+
"grounded": true
|
| 161 |
+
},
|
| 162 |
+
{
|
| 163 |
+
"id": "c2",
|
| 164 |
+
"query": "Where is the chemistry safety briefing held?",
|
| 165 |
+
"expected_abstain": false,
|
| 166 |
+
"top_doc": "C2",
|
| 167 |
+
"retrieval_score": 0.825787,
|
| 168 |
+
"top_posterior": 1.0,
|
| 169 |
+
"answer": "The chemistry safety briefing is held in Hall 7. [C2]",
|
| 170 |
+
"citations": [
|
| 171 |
+
"C2"
|
| 172 |
+
],
|
| 173 |
+
"correct": true,
|
| 174 |
+
"grounded": true
|
| 175 |
+
},
|
| 176 |
+
{
|
| 177 |
+
"id": "c3",
|
| 178 |
+
"query": "How long may library books be borrowed?",
|
| 179 |
+
"expected_abstain": false,
|
| 180 |
+
"top_doc": "C3",
|
| 181 |
+
"retrieval_score": 0.683723,
|
| 182 |
+
"top_posterior": 1.0,
|
| 183 |
+
"answer": "Library books may be borrowed for 21 days. [C3]",
|
| 184 |
+
"citations": [
|
| 185 |
+
"C3"
|
| 186 |
+
],
|
| 187 |
+
"correct": true,
|
| 188 |
+
"grounded": true
|
| 189 |
+
},
|
| 190 |
+
{
|
| 191 |
+
"id": "c4",
|
| 192 |
+
"query": "When does the robotics club meet?",
|
| 193 |
+
"expected_abstain": false,
|
| 194 |
+
"top_doc": "C4",
|
| 195 |
+
"retrieval_score": 0.742488,
|
| 196 |
+
"top_posterior": 1.0,
|
| 197 |
+
"answer": "The robotics club meets on Thursday evenings. [C4]",
|
| 198 |
+
"citations": [
|
| 199 |
+
"C4"
|
| 200 |
+
],
|
| 201 |
+
"correct": true,
|
| 202 |
+
"grounded": true
|
| 203 |
+
},
|
| 204 |
+
{
|
| 205 |
+
"id": "c5",
|
| 206 |
+
"query": "Where is visitor parking?",
|
| 207 |
+
"expected_abstain": false,
|
| 208 |
+
"top_doc": "C5",
|
| 209 |
+
"retrieval_score": 0.79521,
|
| 210 |
+
"top_posterior": 1.0,
|
| 211 |
+
"answer": "Visitor parking is located beside Gate 3. [C5]",
|
| 212 |
+
"citations": [
|
| 213 |
+
"C5"
|
| 214 |
+
],
|
| 215 |
+
"correct": true,
|
| 216 |
+
"grounded": true
|
| 217 |
+
},
|
| 218 |
+
{
|
| 219 |
+
"id": "c6",
|
| 220 |
+
"query": "What time does the campus shuttle begin service?",
|
| 221 |
+
"expected_abstain": false,
|
| 222 |
+
"top_doc": "C6",
|
| 223 |
+
"retrieval_score": 0.769605,
|
| 224 |
+
"top_posterior": 1.0,
|
| 225 |
+
"answer": "The campus shuttle begins service at 6:30 AM. [C6]",
|
| 226 |
+
"citations": [
|
| 227 |
+
"C6"
|
| 228 |
+
],
|
| 229 |
+
"correct": true,
|
| 230 |
+
"grounded": true
|
| 231 |
+
},
|
| 232 |
+
{
|
| 233 |
+
"id": "c7",
|
| 234 |
+
"query": "How many credits is the data science seminar worth?",
|
| 235 |
+
"expected_abstain": false,
|
| 236 |
+
"top_doc": "C7",
|
| 237 |
+
"retrieval_score": 0.836653,
|
| 238 |
+
"top_posterior": 1.0,
|
| 239 |
+
"answer": "The data science seminar carries two academic credits. [C7]",
|
| 240 |
+
"citations": [
|
| 241 |
+
"C7"
|
| 242 |
+
],
|
| 243 |
+
"correct": true,
|
| 244 |
+
"grounded": true
|
| 245 |
+
},
|
| 246 |
+
{
|
| 247 |
+
"id": "c8",
|
| 248 |
+
"query": "Where is the emergency assembly point?",
|
| 249 |
+
"expected_abstain": false,
|
| 250 |
+
"top_doc": "C8",
|
| 251 |
+
"retrieval_score": 0.683723,
|
| 252 |
+
"top_posterior": 1.0,
|
| 253 |
+
"answer": "The emergency assembly point is the North Lawn. [C8]",
|
| 254 |
+
"citations": [
|
| 255 |
+
"C8"
|
| 256 |
+
],
|
| 257 |
+
"correct": true,
|
| 258 |
+
"grounded": true
|
| 259 |
+
}
|
| 260 |
+
]
|
| 261 |
+
},
|
| 262 |
+
"conflict": {
|
| 263 |
+
"bayesian": {
|
| 264 |
+
"system": "bayesian",
|
| 265 |
+
"n_cases": 18,
|
| 266 |
+
"answerable_cases": 12,
|
| 267 |
+
"abstention_cases": 6,
|
| 268 |
+
"recall_at_1": 1.0,
|
| 269 |
+
"recall_at_3": 1.0,
|
| 270 |
+
"mrr": 1.0,
|
| 271 |
+
"end_to_end_accuracy": 1.0,
|
| 272 |
+
"grounding_accuracy": 1.0,
|
| 273 |
+
"abstention_accuracy": 1.0,
|
| 274 |
+
"answer_accuracy_on_answerable": 1.0,
|
| 275 |
+
"top_posterior_brier": 0.09485010369519668
|
| 276 |
+
},
|
| 277 |
+
"baseline": {
|
| 278 |
+
"system": "plain_tfidf",
|
| 279 |
+
"n_cases": 18,
|
| 280 |
+
"answerable_cases": 12,
|
| 281 |
+
"abstention_cases": 6,
|
| 282 |
+
"recall_at_1": 0.25,
|
| 283 |
+
"recall_at_3": 1.0,
|
| 284 |
+
"mrr": 0.611111111111111,
|
| 285 |
+
"end_to_end_accuracy": 0.2777777777777778,
|
| 286 |
+
"grounding_accuracy": 1.0,
|
| 287 |
+
"abstention_accuracy": 0.3333333333333333,
|
| 288 |
+
"answer_accuracy_on_answerable": 0.25,
|
| 289 |
+
"top_posterior_brier": 0.26943925109630584
|
| 290 |
+
},
|
| 291 |
+
"bayesian_cases": [
|
| 292 |
+
{
|
| 293 |
+
"id": "q1",
|
| 294 |
+
"query": "What is the current HR portal for leave requests and payslips?",
|
| 295 |
+
"expected_abstain": false,
|
| 296 |
+
"top_doc": "OFF-HR-PORTAL",
|
| 297 |
+
"retrieval_score": 0.221707,
|
| 298 |
+
"top_posterior": 0.419857,
|
| 299 |
+
"answer": "All employee self-service tasks, including leave requests and payslips, are now handled in PeopleHub. [OFF-HR-PORTAL]",
|
| 300 |
+
"citations": [
|
| 301 |
+
"OFF-HR-PORTAL"
|
| 302 |
+
],
|
| 303 |
+
"correct": true,
|
| 304 |
+
"grounded": true
|
| 305 |
+
},
|
| 306 |
+
{
|
| 307 |
+
"id": "q2",
|
| 308 |
+
"query": "How many paid leave days do employees get per year under the current policy?",
|
| 309 |
+
"expected_abstain": false,
|
| 310 |
+
"top_doc": "OFF-LEAVE",
|
| 311 |
+
"retrieval_score": 0.366908,
|
| 312 |
+
"top_posterior": 0.675297,
|
| 313 |
+
"answer": "The 2026 employee benefits policy grants 18 paid leave days per calendar year. [OFF-LEAVE]",
|
| 314 |
+
"citations": [
|
| 315 |
+
"OFF-LEAVE"
|
| 316 |
+
],
|
| 317 |
+
"correct": true,
|
| 318 |
+
"grounded": true
|
| 319 |
+
},
|
| 320 |
+
{
|
| 321 |
+
"id": "q3",
|
| 322 |
+
"query": "How many remote work days per week are currently allowed with manager approval?",
|
| 323 |
+
"expected_abstain": false,
|
| 324 |
+
"top_doc": "OFF-REMOTE",
|
| 325 |
+
"retrieval_score": 0.383162,
|
| 326 |
+
"top_posterior": 0.598358,
|
| 327 |
+
"answer": "Under the active flexible-work policy, employees may work remotely up to two days per week with manager approval. [OFF-REMOTE]",
|
| 328 |
+
"citations": [
|
| 329 |
+
"OFF-REMOTE"
|
| 330 |
+
],
|
| 331 |
+
"correct": true,
|
| 332 |
+
"grounded": true
|
| 333 |
+
},
|
| 334 |
+
{
|
| 335 |
+
"id": "q4",
|
| 336 |
+
"query": "By which working day are salary slips published each month?",
|
| 337 |
+
"expected_abstain": false,
|
| 338 |
+
"top_doc": "OFF-PAYSLIP",
|
| 339 |
+
"retrieval_score": 0.457118,
|
| 340 |
+
"top_posterior": 0.8609,
|
| 341 |
+
"answer": "Payroll publishes salary slips by the fifth working day of each month. [OFF-PAYSLIP]",
|
| 342 |
+
"citations": [
|
| 343 |
+
"OFF-PAYSLIP"
|
| 344 |
+
],
|
| 345 |
+
"correct": true,
|
| 346 |
+
"grounded": true
|
| 347 |
+
},
|
| 348 |
+
{
|
| 349 |
+
"id": "q5",
|
| 350 |
+
"query": "Which VPN protocol is required by the current corporate security baseline?",
|
| 351 |
+
"expected_abstain": false,
|
| 352 |
+
"top_doc": "OFF-VPN",
|
| 353 |
+
"retrieval_score": 0.514507,
|
| 354 |
+
"top_posterior": 0.904463,
|
| 355 |
+
"answer": "The security baseline now requires WireGuard for the standard corporate VPN profile. [OFF-VPN]",
|
| 356 |
+
"citations": [
|
| 357 |
+
"OFF-VPN"
|
| 358 |
+
],
|
| 359 |
+
"correct": true,
|
| 360 |
+
"grounded": true
|
| 361 |
+
},
|
| 362 |
+
{
|
| 363 |
+
"id": "q6",
|
| 364 |
+
"query": "What is the production database backup retention period?",
|
| 365 |
+
"expected_abstain": false,
|
| 366 |
+
"top_doc": "OFF-BACKUP",
|
| 367 |
+
"retrieval_score": 0.424384,
|
| 368 |
+
"top_posterior": 0.835659,
|
| 369 |
+
"answer": "Production database backups have a retention period of 30 days. [OFF-BACKUP]",
|
| 370 |
+
"citations": [
|
| 371 |
+
"OFF-BACKUP"
|
| 372 |
+
],
|
| 373 |
+
"correct": true,
|
| 374 |
+
"grounded": true
|
| 375 |
+
},
|
| 376 |
+
{
|
| 377 |
+
"id": "q7",
|
| 378 |
+
"query": "What is the current public API rate limit per minute per token?",
|
| 379 |
+
"expected_abstain": false,
|
| 380 |
+
"top_doc": "OFF-RATE",
|
| 381 |
+
"retrieval_score": 0.260415,
|
| 382 |
+
"top_posterior": 0.600144,
|
| 383 |
+
"answer": "The current gateway quota permits 120 requests per minute per token. [OFF-RATE]",
|
| 384 |
+
"citations": [
|
| 385 |
+
"OFF-RATE"
|
| 386 |
+
],
|
| 387 |
+
"correct": true,
|
| 388 |
+
"grounded": true
|
| 389 |
+
},
|
| 390 |
+
{
|
| 391 |
+
"id": "q8",
|
| 392 |
+
"query": "Which cloud region is primary for production?",
|
| 393 |
+
"expected_abstain": false,
|
| 394 |
+
"top_doc": "OFF-REGION",
|
| 395 |
+
"retrieval_score": 0.400734,
|
| 396 |
+
"top_posterior": 0.819994,
|
| 397 |
+
"answer": "The production primary region is ap-south-1; disaster recovery remains in a separate region. [OFF-REGION]",
|
| 398 |
+
"citations": [
|
| 399 |
+
"OFF-REGION"
|
| 400 |
+
],
|
| 401 |
+
"correct": true,
|
| 402 |
+
"grounded": true
|
| 403 |
+
},
|
| 404 |
+
{
|
| 405 |
+
"id": "q9",
|
| 406 |
+
"query": "What is the domestic business travel meal expense cap per day?",
|
| 407 |
+
"expected_abstain": false,
|
| 408 |
+
"top_doc": "OFF-EXPENSE",
|
| 409 |
+
"retrieval_score": 0.391575,
|
| 410 |
+
"top_posterior": 0.520438,
|
| 411 |
+
"answer": "For domestic business travel, reimbursable meals are capped at INR 4,500 per day. [OFF-EXPENSE]",
|
| 412 |
+
"citations": [
|
| 413 |
+
"OFF-EXPENSE"
|
| 414 |
+
],
|
| 415 |
+
"correct": true,
|
| 416 |
+
"grounded": true
|
| 417 |
+
},
|
| 418 |
+
{
|
| 419 |
+
"id": "q10",
|
| 420 |
+
"query": "Within how many days must expense reimbursement claims be submitted?",
|
| 421 |
+
"expected_abstain": false,
|
| 422 |
+
"top_doc": "OFF-REIMB",
|
| 423 |
+
"retrieval_score": 0.513156,
|
| 424 |
+
"top_posterior": 0.907517,
|
| 425 |
+
"answer": "Expense claims must be submitted within 15 days of the transaction date. [OFF-REIMB]",
|
| 426 |
+
"citations": [
|
| 427 |
+
"OFF-REIMB"
|
| 428 |
+
],
|
| 429 |
+
"correct": true,
|
| 430 |
+
"grounded": true
|
| 431 |
+
},
|
| 432 |
+
{
|
| 433 |
+
"id": "q11",
|
| 434 |
+
"query": "Which portal must be used to book approved business travel?",
|
| 435 |
+
"expected_abstain": false,
|
| 436 |
+
"top_doc": "OFF-TRAVEL",
|
| 437 |
+
"retrieval_score": 0.580663,
|
| 438 |
+
"top_posterior": 0.832603,
|
| 439 |
+
"answer": "Approved business travel must be booked through TripDesk. [OFF-TRAVEL]",
|
| 440 |
+
"citations": [
|
| 441 |
+
"OFF-TRAVEL"
|
| 442 |
+
],
|
| 443 |
+
"correct": true,
|
| 444 |
+
"grounded": true
|
| 445 |
+
},
|
| 446 |
+
{
|
| 447 |
+
"id": "q12",
|
| 448 |
+
"query": "Above what amount is procurement review mandatory?",
|
| 449 |
+
"expected_abstain": false,
|
| 450 |
+
"top_doc": "OFF-PROC",
|
| 451 |
+
"retrieval_score": 0.512226,
|
| 452 |
+
"top_posterior": 0.856608,
|
| 453 |
+
"answer": "Procurement review is mandatory for purchases above INR 50,000. [OFF-PROC]",
|
| 454 |
+
"citations": [
|
| 455 |
+
"OFF-PROC"
|
| 456 |
+
],
|
| 457 |
+
"correct": true,
|
| 458 |
+
"grounded": true
|
| 459 |
+
},
|
| 460 |
+
{
|
| 461 |
+
"id": "q13",
|
| 462 |
+
"query": "What is the company dental insurance annual limit?",
|
| 463 |
+
"expected_abstain": true,
|
| 464 |
+
"top_doc": "LEG-RATE",
|
| 465 |
+
"retrieval_score": 0.325883,
|
| 466 |
+
"top_posterior": 0.829873,
|
| 467 |
+
"answer": "I do not know based on the provided documents.",
|
| 468 |
+
"citations": [],
|
| 469 |
+
"correct": true,
|
| 470 |
+
"grounded": true
|
| 471 |
+
},
|
| 472 |
+
{
|
| 473 |
+
"id": "q14",
|
| 474 |
+
"query": "What is the cafeteria breakfast price?",
|
| 475 |
+
"expected_abstain": true,
|
| 476 |
+
"top_doc": "OFF-HR-PORTAL",
|
| 477 |
+
"retrieval_score": 0.0,
|
| 478 |
+
"top_posterior": 0.248018,
|
| 479 |
+
"answer": "I do not know based on the provided documents.",
|
| 480 |
+
"citations": [],
|
| 481 |
+
"correct": true,
|
| 482 |
+
"grounded": true
|
| 483 |
+
},
|
| 484 |
+
{
|
| 485 |
+
"id": "q15",
|
| 486 |
+
"query": "What is the emergency satellite phone number?",
|
| 487 |
+
"expected_abstain": true,
|
| 488 |
+
"top_doc": "OFF-HR-PORTAL",
|
| 489 |
+
"retrieval_score": 0.0,
|
| 490 |
+
"top_posterior": 0.248018,
|
| 491 |
+
"answer": "I do not know based on the provided documents.",
|
| 492 |
+
"citations": [],
|
| 493 |
+
"correct": true,
|
| 494 |
+
"grounded": true
|
| 495 |
+
},
|
| 496 |
+
{
|
| 497 |
+
"id": "q16",
|
| 498 |
+
"query": "What is the annual gym membership reimbursement?",
|
| 499 |
+
"expected_abstain": true,
|
| 500 |
+
"top_doc": "OFF-REIMB",
|
| 501 |
+
"retrieval_score": 0.178672,
|
| 502 |
+
"top_posterior": 0.385158,
|
| 503 |
+
"answer": "I do not know based on the provided documents.",
|
| 504 |
+
"citations": [],
|
| 505 |
+
"correct": true,
|
| 506 |
+
"grounded": true
|
| 507 |
+
},
|
| 508 |
+
{
|
| 509 |
+
"id": "q17",
|
| 510 |
+
"query": "Which room is the graduate AI lab currently in?",
|
| 511 |
+
"expected_abstain": true,
|
| 512 |
+
"top_doc": "OFF-LAB-A",
|
| 513 |
+
"retrieval_score": 0.752053,
|
| 514 |
+
"top_posterior": 0.499378,
|
| 515 |
+
"answer": "I do not know based on the provided documents.",
|
| 516 |
+
"citations": [],
|
| 517 |
+
"correct": true,
|
| 518 |
+
"grounded": true
|
| 519 |
+
},
|
| 520 |
+
{
|
| 521 |
+
"id": "q18",
|
| 522 |
+
"query": "When are Professor Sen's office hours?",
|
| 523 |
+
"expected_abstain": true,
|
| 524 |
+
"top_doc": "OFF-OFFICE-A",
|
| 525 |
+
"retrieval_score": 0.702454,
|
| 526 |
+
"top_posterior": 0.49929,
|
| 527 |
+
"answer": "I do not know based on the provided documents.",
|
| 528 |
+
"citations": [],
|
| 529 |
+
"correct": true,
|
| 530 |
+
"grounded": true
|
| 531 |
+
}
|
| 532 |
+
],
|
| 533 |
+
"baseline_cases": [
|
| 534 |
+
{
|
| 535 |
+
"id": "q1",
|
| 536 |
+
"query": "What is the current HR portal for leave requests and payslips?",
|
| 537 |
+
"expected_abstain": false,
|
| 538 |
+
"top_doc": "LEG-HR-PORTAL",
|
| 539 |
+
"retrieval_score": 0.807312,
|
| 540 |
+
"top_posterior": 0.592469,
|
| 541 |
+
"answer": "The current HR portal for leave requests and payslips is WorkSphere. [LEG-HR-PORTAL]",
|
| 542 |
+
"citations": [
|
| 543 |
+
"LEG-HR-PORTAL"
|
| 544 |
+
],
|
| 545 |
+
"correct": false,
|
| 546 |
+
"grounded": true
|
| 547 |
+
},
|
| 548 |
+
{
|
| 549 |
+
"id": "q2",
|
| 550 |
+
"query": "How many paid leave days do employees get per year under the current policy?",
|
| 551 |
+
"expected_abstain": false,
|
| 552 |
+
"top_doc": "LEG-LEAVE",
|
| 553 |
+
"retrieval_score": 0.602126,
|
| 554 |
+
"top_posterior": 0.462903,
|
| 555 |
+
"answer": "Employees receive 15 paid leave days per year under the leave policy. [LEG-LEAVE]",
|
| 556 |
+
"citations": [
|
| 557 |
+
"LEG-LEAVE"
|
| 558 |
+
],
|
| 559 |
+
"correct": false,
|
| 560 |
+
"grounded": true
|
| 561 |
+
},
|
| 562 |
+
{
|
| 563 |
+
"id": "q3",
|
| 564 |
+
"query": "How many remote work days per week are currently allowed with manager approval?",
|
| 565 |
+
"expected_abstain": false,
|
| 566 |
+
"top_doc": "LEG-REMOTE",
|
| 567 |
+
"retrieval_score": 0.699429,
|
| 568 |
+
"top_posterior": 0.435096,
|
| 569 |
+
"answer": "The current remote work policy permits three remote days per week with manager approval. [LEG-REMOTE]",
|
| 570 |
+
"citations": [
|
| 571 |
+
"LEG-REMOTE"
|
| 572 |
+
],
|
| 573 |
+
"correct": false,
|
| 574 |
+
"grounded": true
|
| 575 |
+
},
|
| 576 |
+
{
|
| 577 |
+
"id": "q4",
|
| 578 |
+
"query": "By which working day are salary slips published each month?",
|
| 579 |
+
"expected_abstain": false,
|
| 580 |
+
"top_doc": "LEG-PAYSLIP",
|
| 581 |
+
"retrieval_score": 0.672029,
|
| 582 |
+
"top_posterior": 0.550648,
|
| 583 |
+
"answer": "Salary slips are published on the tenth working day of each month. [LEG-PAYSLIP]",
|
| 584 |
+
"citations": [
|
| 585 |
+
"LEG-PAYSLIP"
|
| 586 |
+
],
|
| 587 |
+
"correct": false,
|
| 588 |
+
"grounded": true
|
| 589 |
+
},
|
| 590 |
+
{
|
| 591 |
+
"id": "q5",
|
| 592 |
+
"query": "Which VPN protocol is required by the current corporate security baseline?",
|
| 593 |
+
"expected_abstain": false,
|
| 594 |
+
"top_doc": "LEG-VPN",
|
| 595 |
+
"retrieval_score": 0.524301,
|
| 596 |
+
"top_posterior": 0.420244,
|
| 597 |
+
"answer": "The supported corporate VPN protocol is OpenVPN. [LEG-VPN]",
|
| 598 |
+
"citations": [
|
| 599 |
+
"LEG-VPN"
|
| 600 |
+
],
|
| 601 |
+
"correct": false,
|
| 602 |
+
"grounded": true
|
| 603 |
+
},
|
| 604 |
+
{
|
| 605 |
+
"id": "q6",
|
| 606 |
+
"query": "What is the production database backup retention period?",
|
| 607 |
+
"expected_abstain": false,
|
| 608 |
+
"top_doc": "LEG-BACKUP",
|
| 609 |
+
"retrieval_score": 0.556814,
|
| 610 |
+
"top_posterior": 0.467937,
|
| 611 |
+
"answer": "Production database backups are retained for 14 days. [LEG-BACKUP]",
|
| 612 |
+
"citations": [
|
| 613 |
+
"LEG-BACKUP"
|
| 614 |
+
],
|
| 615 |
+
"correct": false,
|
| 616 |
+
"grounded": true
|
| 617 |
+
},
|
| 618 |
+
{
|
| 619 |
+
"id": "q7",
|
| 620 |
+
"query": "What is the current public API rate limit per minute per token?",
|
| 621 |
+
"expected_abstain": false,
|
| 622 |
+
"top_doc": "LEG-RATE",
|
| 623 |
+
"retrieval_score": 0.845222,
|
| 624 |
+
"top_posterior": 0.672178,
|
| 625 |
+
"answer": "The public API rate limit is 60 requests per minute per token. [LEG-RATE]",
|
| 626 |
+
"citations": [
|
| 627 |
+
"LEG-RATE"
|
| 628 |
+
],
|
| 629 |
+
"correct": false,
|
| 630 |
+
"grounded": true
|
| 631 |
+
},
|
| 632 |
+
{
|
| 633 |
+
"id": "q8",
|
| 634 |
+
"query": "Which cloud region is primary for production?",
|
| 635 |
+
"expected_abstain": false,
|
| 636 |
+
"top_doc": "LEG-REGION",
|
| 637 |
+
"retrieval_score": 0.542439,
|
| 638 |
+
"top_posterior": 0.460259,
|
| 639 |
+
"answer": "The primary deployment region is us-east-1. [LEG-REGION]",
|
| 640 |
+
"citations": [
|
| 641 |
+
"LEG-REGION"
|
| 642 |
+
],
|
| 643 |
+
"correct": false,
|
| 644 |
+
"grounded": true
|
| 645 |
+
},
|
| 646 |
+
{
|
| 647 |
+
"id": "q9",
|
| 648 |
+
"query": "What is the domestic business travel meal expense cap per day?",
|
| 649 |
+
"expected_abstain": false,
|
| 650 |
+
"top_doc": "LEG-EXPENSE",
|
| 651 |
+
"retrieval_score": 0.671336,
|
| 652 |
+
"top_posterior": 0.451066,
|
| 653 |
+
"answer": "The meal expense cap is INR 3,000 per day on domestic travel. [LEG-EXPENSE]",
|
| 654 |
+
"citations": [
|
| 655 |
+
"LEG-EXPENSE"
|
| 656 |
+
],
|
| 657 |
+
"correct": false,
|
| 658 |
+
"grounded": true
|
| 659 |
+
},
|
| 660 |
+
{
|
| 661 |
+
"id": "q10",
|
| 662 |
+
"query": "Within how many days must expense reimbursement claims be submitted?",
|
| 663 |
+
"expected_abstain": false,
|
| 664 |
+
"top_doc": "OFF-REIMB",
|
| 665 |
+
"retrieval_score": 0.513156,
|
| 666 |
+
"top_posterior": 0.45338,
|
| 667 |
+
"answer": "Expense claims must be submitted within 15 days of the transaction date. [OFF-REIMB]",
|
| 668 |
+
"citations": [
|
| 669 |
+
"OFF-REIMB"
|
| 670 |
+
],
|
| 671 |
+
"correct": true,
|
| 672 |
+
"grounded": true
|
| 673 |
+
},
|
| 674 |
+
{
|
| 675 |
+
"id": "q11",
|
| 676 |
+
"query": "Which portal must be used to book approved business travel?",
|
| 677 |
+
"expected_abstain": false,
|
| 678 |
+
"top_doc": "OFF-TRAVEL",
|
| 679 |
+
"retrieval_score": 0.580663,
|
| 680 |
+
"top_posterior": 0.514293,
|
| 681 |
+
"answer": "Approved business travel must be booked through TripDesk. [OFF-TRAVEL]",
|
| 682 |
+
"citations": [
|
| 683 |
+
"OFF-TRAVEL"
|
| 684 |
+
],
|
| 685 |
+
"correct": true,
|
| 686 |
+
"grounded": true
|
| 687 |
+
},
|
| 688 |
+
{
|
| 689 |
+
"id": "q12",
|
| 690 |
+
"query": "Above what amount is procurement review mandatory?",
|
| 691 |
+
"expected_abstain": false,
|
| 692 |
+
"top_doc": "OFF-PROC",
|
| 693 |
+
"retrieval_score": 0.512226,
|
| 694 |
+
"top_posterior": 0.38514,
|
| 695 |
+
"answer": "Procurement review is mandatory for purchases above INR 50,000. [OFF-PROC]",
|
| 696 |
+
"citations": [
|
| 697 |
+
"OFF-PROC"
|
| 698 |
+
],
|
| 699 |
+
"correct": true,
|
| 700 |
+
"grounded": true
|
| 701 |
+
},
|
| 702 |
+
{
|
| 703 |
+
"id": "q13",
|
| 704 |
+
"query": "What is the company dental insurance annual limit?",
|
| 705 |
+
"expected_abstain": true,
|
| 706 |
+
"top_doc": "LEG-RATE",
|
| 707 |
+
"retrieval_score": 0.325883,
|
| 708 |
+
"top_posterior": 1.0,
|
| 709 |
+
"answer": "The public API rate limit is 60 requests per minute per token. [LEG-RATE]",
|
| 710 |
+
"citations": [
|
| 711 |
+
"LEG-RATE"
|
| 712 |
+
],
|
| 713 |
+
"correct": false,
|
| 714 |
+
"grounded": true
|
| 715 |
+
},
|
| 716 |
+
{
|
| 717 |
+
"id": "q14",
|
| 718 |
+
"query": "What is the cafeteria breakfast price?",
|
| 719 |
+
"expected_abstain": true,
|
| 720 |
+
"top_doc": "LEG-HR-PORTAL",
|
| 721 |
+
"retrieval_score": 0.0,
|
| 722 |
+
"top_posterior": 0.0,
|
| 723 |
+
"answer": "I do not know based on the provided documents.",
|
| 724 |
+
"citations": [],
|
| 725 |
+
"correct": true,
|
| 726 |
+
"grounded": true
|
| 727 |
+
},
|
| 728 |
+
{
|
| 729 |
+
"id": "q15",
|
| 730 |
+
"query": "What is the emergency satellite phone number?",
|
| 731 |
+
"expected_abstain": true,
|
| 732 |
+
"top_doc": "LEG-HR-PORTAL",
|
| 733 |
+
"retrieval_score": 0.0,
|
| 734 |
+
"top_posterior": 0.0,
|
| 735 |
+
"answer": "I do not know based on the provided documents.",
|
| 736 |
+
"citations": [],
|
| 737 |
+
"correct": true,
|
| 738 |
+
"grounded": true
|
| 739 |
+
},
|
| 740 |
+
{
|
| 741 |
+
"id": "q16",
|
| 742 |
+
"query": "What is the annual gym membership reimbursement?",
|
| 743 |
+
"expected_abstain": true,
|
| 744 |
+
"top_doc": "COM-REIMB",
|
| 745 |
+
"retrieval_score": 0.333617,
|
| 746 |
+
"top_posterior": 0.327295,
|
| 747 |
+
"answer": "Finance may request additional receipts during reimbursement review. [COM-REIMB]",
|
| 748 |
+
"citations": [
|
| 749 |
+
"COM-REIMB"
|
| 750 |
+
],
|
| 751 |
+
"correct": false,
|
| 752 |
+
"grounded": true
|
| 753 |
+
},
|
| 754 |
+
{
|
| 755 |
+
"id": "q17",
|
| 756 |
+
"query": "Which room is the graduate AI lab currently in?",
|
| 757 |
+
"expected_abstain": true,
|
| 758 |
+
"top_doc": "OFF-LAB-A",
|
| 759 |
+
"retrieval_score": 0.752053,
|
| 760 |
+
"top_posterior": 0.5,
|
| 761 |
+
"answer": "The graduate AI lab has moved to room B-204. [OFF-LAB-A]",
|
| 762 |
+
"citations": [
|
| 763 |
+
"OFF-LAB-A"
|
| 764 |
+
],
|
| 765 |
+
"correct": false,
|
| 766 |
+
"grounded": true
|
| 767 |
+
},
|
| 768 |
+
{
|
| 769 |
+
"id": "q18",
|
| 770 |
+
"query": "When are Professor Sen's office hours?",
|
| 771 |
+
"expected_abstain": true,
|
| 772 |
+
"top_doc": "OFF-OFFICE-A",
|
| 773 |
+
"retrieval_score": 0.702454,
|
| 774 |
+
"top_posterior": 0.5,
|
| 775 |
+
"answer": "Professor Sen holds office hours on Tuesday at 3 PM. [OFF-OFFICE-A]",
|
| 776 |
+
"citations": [
|
| 777 |
+
"OFF-OFFICE-A"
|
| 778 |
+
],
|
| 779 |
+
"correct": false,
|
| 780 |
+
"grounded": true
|
| 781 |
+
}
|
| 782 |
+
]
|
| 783 |
+
}
|
| 784 |
+
}
|
evaluation/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Benchmark package.
|
evaluation/framework_rag_benchmark.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from typing import Any
|
| 6 |
+
|
| 7 |
+
from bayesian_langgraph import BayesianIndex, BayesianRAGGraph
|
| 8 |
+
from evaluation.rag_benchmark import CLEAN_CASES, CLEAN_DOCS, CONFLICT_CASES, CONFLICT_DOCS
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def _case_ok(out: dict[str, Any], case: dict[str, Any]) -> bool:
|
| 12 |
+
if case["abstain"]:
|
| 13 |
+
return bool(out.get("abstained")) and not out.get("citations")
|
| 14 |
+
return (
|
| 15 |
+
not out.get("abstained")
|
| 16 |
+
and case["doc"] in out.get("citations", [])
|
| 17 |
+
and str(case["gold"]).lower() in str(out.get("answer", "")).lower()
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def _run_suite(docs, cases) -> dict[str, Any]:
|
| 22 |
+
rag = BayesianRAGGraph(BayesianIndex(docs), k=4)
|
| 23 |
+
rows = []
|
| 24 |
+
for case in cases:
|
| 25 |
+
out = rag.invoke(case["q"])
|
| 26 |
+
rows.append({
|
| 27 |
+
"id": case["id"],
|
| 28 |
+
"ok": _case_ok(out, case),
|
| 29 |
+
"answer": out["answer"],
|
| 30 |
+
"citations": out["citations"],
|
| 31 |
+
"abstained": out["abstained"],
|
| 32 |
+
"top_doc": out["hits"][0]["id"] if out["hits"] else None,
|
| 33 |
+
})
|
| 34 |
+
return {
|
| 35 |
+
"n_cases": len(rows),
|
| 36 |
+
"passed": sum(r["ok"] for r in rows),
|
| 37 |
+
"accuracy": sum(r["ok"] for r in rows) / len(rows),
|
| 38 |
+
"rows": rows,
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def run_framework_benchmark(output_path: str | Path | None = None) -> dict[str, Any]:
|
| 43 |
+
rag = BayesianRAGGraph(BayesianIndex(CLEAN_DOCS), k=4)
|
| 44 |
+
graph = rag.graph.get_graph()
|
| 45 |
+
result = {
|
| 46 |
+
"framework": "langgraph",
|
| 47 |
+
"stategraph_nodes": sorted(graph.nodes.keys()),
|
| 48 |
+
"stategraph_edges": sorted([e.source, e.target] for e in graph.edges),
|
| 49 |
+
"clean": _run_suite(CLEAN_DOCS, CLEAN_CASES),
|
| 50 |
+
"conflict": _run_suite(CONFLICT_DOCS, CONFLICT_CASES),
|
| 51 |
+
}
|
| 52 |
+
if output_path is not None:
|
| 53 |
+
Path(output_path).write_text(json.dumps(result, indent=2), encoding="utf-8")
|
| 54 |
+
return result
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
if __name__ == "__main__":
|
| 58 |
+
print(json.dumps(run_framework_benchmark(), indent=2))
|
evaluation/rag_benchmark.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from typing import Any
|
| 6 |
+
|
| 7 |
+
from bayesian_langgraph.core import (
|
| 8 |
+
BayesianIndex,
|
| 9 |
+
BayesianRetrievalConfig,
|
| 10 |
+
DocumentRecord,
|
| 11 |
+
PlainTfidfIndex,
|
| 12 |
+
ExtractiveGroundedGenerator,
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def _md(kind: str) -> dict[str, Any]:
|
| 17 |
+
if kind == "official":
|
| 18 |
+
return {"reliability_alpha": 18, "reliability_beta": 2, "authority": 0.98, "freshness": 1.0, "tier": kind}
|
| 19 |
+
if kind == "stale":
|
| 20 |
+
return {"reliability_alpha": 6, "reliability_beta": 6, "authority": 0.45, "freshness": 0.18, "tier": kind}
|
| 21 |
+
if kind == "community":
|
| 22 |
+
return {"reliability_alpha": 7, "reliability_beta": 5, "authority": 0.55, "freshness": 0.65, "tier": kind}
|
| 23 |
+
if kind == "official_equal":
|
| 24 |
+
return {"reliability_alpha": 18, "reliability_beta": 2, "authority": 0.98, "freshness": 1.0, "tier": "official"}
|
| 25 |
+
return {"reliability_alpha": 1, "reliability_beta": 1, "authority": 0.5, "freshness": 0.5, "tier": kind}
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
CONFLICT_DOCS = [
|
| 29 |
+
DocumentRecord("LEG-HR-PORTAL", "The current HR portal for leave requests and payslips is WorkSphere.", "Current HR portal login guide", "legacy-cache", _md("stale")),
|
| 30 |
+
DocumentRecord("OFF-HR-PORTAL", "All employee self-service tasks, including leave requests and payslips, are now handled in PeopleHub.", "Employee self-service migration completed", "official-handbook", _md("official")),
|
| 31 |
+
DocumentRecord("COM-HR-PORTAL", "Some teams used WorkSphere during migration; ask HR if your bookmark no longer works.", "HR portal discussion", "community-wiki", _md("community")),
|
| 32 |
+
DocumentRecord("LEG-LEAVE", "Employees receive 15 paid leave days per year under the leave policy.", "Paid leave entitlement FAQ", "legacy-cache", _md("stale")),
|
| 33 |
+
DocumentRecord("OFF-LEAVE", "The 2026 employee benefits policy grants 18 paid leave days per calendar year.", "2026 employee benefits policy", "official-handbook", _md("official")),
|
| 34 |
+
DocumentRecord("COM-LEAVE", "Leave balances vary by category and should be checked in the HR system.", "Leave balance help", "community-wiki", _md("community")),
|
| 35 |
+
DocumentRecord("LEG-REMOTE", "The current remote work policy permits three remote days per week with manager approval.", "Current remote work days", "legacy-cache", _md("stale")),
|
| 36 |
+
DocumentRecord("OFF-REMOTE", "Under the active flexible-work policy, employees may work remotely up to two days per week with manager approval.", "Flexible-work policy revision", "official-handbook", _md("official")),
|
| 37 |
+
DocumentRecord("COM-REMOTE", "Remote work requires manager approval and may vary during special projects.", "Remote work tips", "community-wiki", _md("community")),
|
| 38 |
+
DocumentRecord("LEG-PAYSLIP", "Salary slips are published on the tenth working day of each month.", "Payslip publication date", "legacy-cache", _md("stale")),
|
| 39 |
+
DocumentRecord("OFF-PAYSLIP", "Payroll publishes salary slips by the fifth working day of each month.", "Payroll service standard", "official-handbook", _md("official")),
|
| 40 |
+
DocumentRecord("COM-PAYSLIP", "Payslip publication can be delayed around holidays.", "Payroll discussion", "community-wiki", _md("community")),
|
| 41 |
+
DocumentRecord("LEG-VPN", "The supported corporate VPN protocol is OpenVPN.", "Corporate VPN protocol", "legacy-cache", _md("stale")),
|
| 42 |
+
DocumentRecord("OFF-VPN", "The security baseline now requires WireGuard for the standard corporate VPN profile.", "Network security baseline", "official-it", _md("official")),
|
| 43 |
+
DocumentRecord("COM-VPN", "OpenVPN profiles may still exist on older laptops.", "VPN migration notes", "community-wiki", _md("community")),
|
| 44 |
+
DocumentRecord("LEG-BACKUP", "Production database backups are retained for 14 days.", "Database backup retention policy", "legacy-cache", _md("stale")),
|
| 45 |
+
DocumentRecord("OFF-BACKUP", "Production database backups have a retention period of 30 days.", "Disaster recovery control DR-7", "official-it", _md("official")),
|
| 46 |
+
DocumentRecord("COM-BACKUP", "Teams should not rely on local snapshots as their only backup.", "Backup practices", "community-wiki", _md("community")),
|
| 47 |
+
DocumentRecord("LEG-RATE", "The public API rate limit is 60 requests per minute per token.", "Public API rate limit", "legacy-cache", _md("stale")),
|
| 48 |
+
DocumentRecord("OFF-RATE", "The current gateway quota permits 120 requests per minute per token.", "API gateway operating limits", "official-it", _md("official")),
|
| 49 |
+
DocumentRecord("COM-RATE", "Burst traffic may still be throttled even below the nominal quota.", "API troubleshooting", "community-wiki", _md("community")),
|
| 50 |
+
DocumentRecord("LEG-REGION", "The primary deployment region is us-east-1.", "Primary cloud deployment region", "legacy-cache", _md("stale")),
|
| 51 |
+
DocumentRecord("OFF-REGION", "The production primary region is ap-south-1; disaster recovery remains in a separate region.", "Cloud production topology", "official-it", _md("official")),
|
| 52 |
+
DocumentRecord("COM-REGION", "Developers may use several regions for non-production experiments.", "Cloud developer notes", "community-wiki", _md("community")),
|
| 53 |
+
DocumentRecord("LEG-EXPENSE", "The meal expense cap is INR 3,000 per day on domestic travel.", "Domestic meal expense cap", "legacy-cache", _md("stale")),
|
| 54 |
+
DocumentRecord("OFF-EXPENSE", "For domestic business travel, reimbursable meals are capped at INR 4,500 per day.", "Travel and expense policy 2026", "official-finance", _md("official")),
|
| 55 |
+
DocumentRecord("COM-EXPENSE", "Itemized receipts are recommended for meal reimbursement.", "Expense claim tips", "community-wiki", _md("community")),
|
| 56 |
+
DocumentRecord("LEG-REIMB", "Employees must submit reimbursement claims within 30 days of the expense.", "Reimbursement submission deadline", "legacy-cache", _md("stale")),
|
| 57 |
+
DocumentRecord("OFF-REIMB", "Expense claims must be submitted within 15 days of the transaction date.", "Expense reimbursement standard", "official-finance", _md("official")),
|
| 58 |
+
DocumentRecord("COM-REIMB", "Finance may request additional receipts during reimbursement review.", "Reimbursement FAQ", "community-wiki", _md("community")),
|
| 59 |
+
DocumentRecord("LEG-TRAVEL", "All corporate travel should be booked through TravelPro.", "Corporate travel booking portal", "legacy-cache", _md("stale")),
|
| 60 |
+
DocumentRecord("OFF-TRAVEL", "Approved business travel must be booked through TripDesk.", "Business travel booking procedure", "official-finance", _md("official")),
|
| 61 |
+
DocumentRecord("COM-TRAVEL", "Direct airline booking can cause reimbursement delays.", "Travel booking discussion", "community-wiki", _md("community")),
|
| 62 |
+
DocumentRecord("LEG-PROC", "Purchases above INR 25,000 require procurement review.", "Procurement review threshold", "legacy-cache", _md("stale")),
|
| 63 |
+
DocumentRecord("OFF-PROC", "Procurement review is mandatory for purchases above INR 50,000.", "Procurement control PC-12", "official-finance", _md("official")),
|
| 64 |
+
DocumentRecord("COM-PROC", "Some categories require procurement review regardless of value.", "Procurement FAQ", "community-wiki", _md("community")),
|
| 65 |
+
DocumentRecord("OFF-LAB-A", "The graduate AI lab has moved to room B-204.", "AI lab room notice A", "official-campus", _md("official_equal")),
|
| 66 |
+
DocumentRecord("OFF-LAB-B", "The graduate AI lab has moved to room C-101.", "AI lab room notice B", "official-campus", _md("official_equal")),
|
| 67 |
+
DocumentRecord("OFF-OFFICE-A", "Professor Sen holds office hours on Tuesday at 3 PM.", "Office hours notice A", "official-campus", _md("official_equal")),
|
| 68 |
+
DocumentRecord("OFF-OFFICE-B", "Professor Sen holds office hours on Wednesday at 4 PM.", "Office hours notice B", "official-campus", _md("official_equal")),
|
| 69 |
+
]
|
| 70 |
+
|
| 71 |
+
CONFLICT_CASES = [
|
| 72 |
+
{"id":"q1","q":"What is the current HR portal for leave requests and payslips?","gold":"PeopleHub","doc":"OFF-HR-PORTAL","abstain":False},
|
| 73 |
+
{"id":"q2","q":"How many paid leave days do employees get per year under the current policy?","gold":"18 paid leave days","doc":"OFF-LEAVE","abstain":False},
|
| 74 |
+
{"id":"q3","q":"How many remote work days per week are currently allowed with manager approval?","gold":"two days per week","doc":"OFF-REMOTE","abstain":False},
|
| 75 |
+
{"id":"q4","q":"By which working day are salary slips published each month?","gold":"fifth working day","doc":"OFF-PAYSLIP","abstain":False},
|
| 76 |
+
{"id":"q5","q":"Which VPN protocol is required by the current corporate security baseline?","gold":"WireGuard","doc":"OFF-VPN","abstain":False},
|
| 77 |
+
{"id":"q6","q":"What is the production database backup retention period?","gold":"30 days","doc":"OFF-BACKUP","abstain":False},
|
| 78 |
+
{"id":"q7","q":"What is the current public API rate limit per minute per token?","gold":"120 requests per minute","doc":"OFF-RATE","abstain":False},
|
| 79 |
+
{"id":"q8","q":"Which cloud region is primary for production?","gold":"ap-south-1","doc":"OFF-REGION","abstain":False},
|
| 80 |
+
{"id":"q9","q":"What is the domestic business travel meal expense cap per day?","gold":"INR 4,500 per day","doc":"OFF-EXPENSE","abstain":False},
|
| 81 |
+
{"id":"q10","q":"Within how many days must expense reimbursement claims be submitted?","gold":"15 days","doc":"OFF-REIMB","abstain":False},
|
| 82 |
+
{"id":"q11","q":"Which portal must be used to book approved business travel?","gold":"TripDesk","doc":"OFF-TRAVEL","abstain":False},
|
| 83 |
+
{"id":"q12","q":"Above what amount is procurement review mandatory?","gold":"INR 50,000","doc":"OFF-PROC","abstain":False},
|
| 84 |
+
{"id":"q13","q":"What is the company dental insurance annual limit?","gold":None,"doc":None,"abstain":True},
|
| 85 |
+
{"id":"q14","q":"What is the cafeteria breakfast price?","gold":None,"doc":None,"abstain":True},
|
| 86 |
+
{"id":"q15","q":"What is the emergency satellite phone number?","gold":None,"doc":None,"abstain":True},
|
| 87 |
+
{"id":"q16","q":"What is the annual gym membership reimbursement?","gold":None,"doc":None,"abstain":True},
|
| 88 |
+
{"id":"q17","q":"Which room is the graduate AI lab currently in?","gold":None,"doc":None,"abstain":True},
|
| 89 |
+
{"id":"q18","q":"When are Professor Sen's office hours?","gold":None,"doc":None,"abstain":True},
|
| 90 |
+
]
|
| 91 |
+
|
| 92 |
+
CLEAN_DOCS = [
|
| 93 |
+
DocumentRecord("C1", "The observatory opens to visitors at 9 AM every Saturday.", "Observatory hours", "handbook", _md("official")),
|
| 94 |
+
DocumentRecord("C2", "The chemistry safety briefing is held in Hall 7.", "Chemistry briefing", "handbook", _md("official")),
|
| 95 |
+
DocumentRecord("C3", "Library books may be borrowed for 21 days.", "Library borrowing", "handbook", _md("official")),
|
| 96 |
+
DocumentRecord("C4", "The robotics club meets on Thursday evenings.", "Robotics club", "handbook", _md("official")),
|
| 97 |
+
DocumentRecord("C5", "Visitor parking is located beside Gate 3.", "Visitor parking", "handbook", _md("official")),
|
| 98 |
+
DocumentRecord("C6", "The campus shuttle begins service at 6:30 AM.", "Campus shuttle", "handbook", _md("official")),
|
| 99 |
+
DocumentRecord("C7", "The data science seminar carries two academic credits.", "Data science seminar", "handbook", _md("official")),
|
| 100 |
+
DocumentRecord("C8", "The emergency assembly point is the North Lawn.", "Emergency procedure", "handbook", _md("official")),
|
| 101 |
+
]
|
| 102 |
+
|
| 103 |
+
CLEAN_CASES = [
|
| 104 |
+
{"id":"c1","q":"When does the observatory open on Saturday?","gold":"9 AM","doc":"C1","abstain":False},
|
| 105 |
+
{"id":"c2","q":"Where is the chemistry safety briefing held?","gold":"Hall 7","doc":"C2","abstain":False},
|
| 106 |
+
{"id":"c3","q":"How long may library books be borrowed?","gold":"21 days","doc":"C3","abstain":False},
|
| 107 |
+
{"id":"c4","q":"When does the robotics club meet?","gold":"Thursday evenings","doc":"C4","abstain":False},
|
| 108 |
+
{"id":"c5","q":"Where is visitor parking?","gold":"Gate 3","doc":"C5","abstain":False},
|
| 109 |
+
{"id":"c6","q":"What time does the campus shuttle begin service?","gold":"6:30 AM","doc":"C6","abstain":False},
|
| 110 |
+
{"id":"c7","q":"How many credits is the data science seminar worth?","gold":"two academic credits","doc":"C7","abstain":False},
|
| 111 |
+
{"id":"c8","q":"Where is the emergency assembly point?","gold":"North Lawn","doc":"C8","abstain":False},
|
| 112 |
+
]
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def _eval(name: str, docs: list[DocumentRecord], cases: list[dict[str, Any]], bayesian: bool) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
| 116 |
+
gen = ExtractiveGroundedGenerator()
|
| 117 |
+
if bayesian:
|
| 118 |
+
searcher = BayesianIndex(docs, BayesianRetrievalConfig(candidate_pool=10, likelihood_temperature=2.0))
|
| 119 |
+
else:
|
| 120 |
+
searcher = PlainTfidfIndex(docs)
|
| 121 |
+
rows: list[dict[str, Any]] = []
|
| 122 |
+
rr: list[float] = []
|
| 123 |
+
r1: list[float] = []
|
| 124 |
+
r3: list[float] = []
|
| 125 |
+
briers: list[float] = []
|
| 126 |
+
doc_map = {d.id: d for d in docs}
|
| 127 |
+
for c in cases:
|
| 128 |
+
hits = searcher.search(c["q"], k=4)
|
| 129 |
+
top = hits[0] if hits else None
|
| 130 |
+
if bayesian:
|
| 131 |
+
abstain = searcher.should_abstain(hits)
|
| 132 |
+
else:
|
| 133 |
+
abstain = (not hits) or top.retrieval_score < 0.06
|
| 134 |
+
if abstain:
|
| 135 |
+
answer, citations = gen.abstain_text, []
|
| 136 |
+
else:
|
| 137 |
+
answer, citations = gen.generate(c["q"], hits)
|
| 138 |
+
if c["abstain"]:
|
| 139 |
+
correct = answer == gen.abstain_text
|
| 140 |
+
else:
|
| 141 |
+
correct = str(c["gold"]).lower() in answer.lower()
|
| 142 |
+
ids = [h.document.id for h in hits]
|
| 143 |
+
rank = ids.index(c["doc"]) + 1 if c["doc"] in ids else 0
|
| 144 |
+
r1.append(1.0 if rank == 1 else 0.0)
|
| 145 |
+
r3.append(1.0 if 0 < rank <= 3 else 0.0)
|
| 146 |
+
rr.append(1.0 / rank if rank else 0.0)
|
| 147 |
+
p = top.posterior if top else 0.0
|
| 148 |
+
y = 1.0 if top and top.document.id == c["doc"] else 0.0
|
| 149 |
+
briers.append((p - y) ** 2)
|
| 150 |
+
grounded = True
|
| 151 |
+
if citations:
|
| 152 |
+
sentence = answer.rsplit(" [", 1)[0]
|
| 153 |
+
grounded = sentence in doc_map[citations[0]].text
|
| 154 |
+
rows.append({
|
| 155 |
+
"id": c["id"],
|
| 156 |
+
"query": c["q"],
|
| 157 |
+
"expected_abstain": c["abstain"],
|
| 158 |
+
"top_doc": None if top is None else top.document.id,
|
| 159 |
+
"retrieval_score": None if top is None else round(top.retrieval_score, 6),
|
| 160 |
+
"top_posterior": None if top is None else round(top.posterior, 6),
|
| 161 |
+
"answer": answer,
|
| 162 |
+
"citations": citations,
|
| 163 |
+
"correct": correct,
|
| 164 |
+
"grounded": grounded,
|
| 165 |
+
})
|
| 166 |
+
n_answerable = sum(not c["abstain"] for c in cases)
|
| 167 |
+
n_abstain = sum(c["abstain"] for c in cases)
|
| 168 |
+
summary = {
|
| 169 |
+
"system": name,
|
| 170 |
+
"n_cases": len(cases),
|
| 171 |
+
"answerable_cases": n_answerable,
|
| 172 |
+
"abstention_cases": n_abstain,
|
| 173 |
+
"recall_at_1": sum(r1) / len(r1) if r1 else None,
|
| 174 |
+
"recall_at_3": sum(r3) / len(r3) if r3 else None,
|
| 175 |
+
"mrr": sum(rr) / len(rr) if rr else None,
|
| 176 |
+
"end_to_end_accuracy": sum(r["correct"] for r in rows) / len(rows),
|
| 177 |
+
"grounding_accuracy": sum(r["grounded"] for r in rows) / len(rows),
|
| 178 |
+
"abstention_accuracy": (sum(r["correct"] for r, c in zip(rows, cases) if c["abstain"]) / n_abstain) if n_abstain else None,
|
| 179 |
+
"answer_accuracy_on_answerable": (sum(r["correct"] for r, c in zip(rows, cases) if not c["abstain"]) / n_answerable) if n_answerable else None,
|
| 180 |
+
"top_posterior_brier": sum(briers) / len(briers) if briers else None,
|
| 181 |
+
}
|
| 182 |
+
return summary, rows
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
def run_benchmark(output_path: str | Path | None = None) -> dict[str, Any]:
|
| 186 |
+
clean_b, clean_b_rows = _eval("bayesian", CLEAN_DOCS, CLEAN_CASES, True)
|
| 187 |
+
clean_t, clean_t_rows = _eval("plain_tfidf", CLEAN_DOCS, CLEAN_CASES, False)
|
| 188 |
+
conflict_b, conflict_b_rows = _eval("bayesian", CONFLICT_DOCS, CONFLICT_CASES, True)
|
| 189 |
+
conflict_t, conflict_t_rows = _eval("plain_tfidf", CONFLICT_DOCS, CONFLICT_CASES, False)
|
| 190 |
+
result = {
|
| 191 |
+
"benchmark": "controlled_rag_v2",
|
| 192 |
+
"methodology": (
|
| 193 |
+
"Two end-to-end suites. CLEAN checks ordinary retrieval+grounded answer synthesis. "
|
| 194 |
+
"CONFLICT adds stale authoritative conflicts, missing evidence, and unresolved equal-authority contradictions. "
|
| 195 |
+
"The generator is extractive and citation-grounded so the evaluation isolates retrieval and uncertainty behavior without an external LLM confound."
|
| 196 |
+
),
|
| 197 |
+
"clean": {"bayesian": clean_b, "baseline": clean_t, "bayesian_cases": clean_b_rows, "baseline_cases": clean_t_rows},
|
| 198 |
+
"conflict": {"bayesian": conflict_b, "baseline": conflict_t, "bayesian_cases": conflict_b_rows, "baseline_cases": conflict_t_rows},
|
| 199 |
+
}
|
| 200 |
+
if output_path is not None:
|
| 201 |
+
Path(output_path).write_text(json.dumps(result, indent=2), encoding="utf-8")
|
| 202 |
+
return result
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
if __name__ == "__main__":
|
| 206 |
+
print(json.dumps(run_benchmark(), indent=2))
|
examples/quickstart.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from bayesian_langgraph import BayesianIndex, BayesianRAGGraph, DocumentRecord
|
| 2 |
+
|
| 3 |
+
docs = [
|
| 4 |
+
DocumentRecord("official", "The current HR portal is PeopleHub.", "Current policy", "official", {"reliability_alpha":18,"reliability_beta":2,"authority":0.98,"freshness":1.0}),
|
| 5 |
+
DocumentRecord("legacy", "The HR portal is WorkSphere.", "Legacy guide", "cache", {"reliability_alpha":6,"reliability_beta":6,"authority":0.45,"freshness":0.18}),
|
| 6 |
+
]
|
| 7 |
+
print(BayesianRAGGraph(BayesianIndex(docs)).invoke("What is the current HR portal?"))
|
pyproject.toml
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[build-system]
|
| 2 |
+
requires = ["setuptools>=68", "wheel"]
|
| 3 |
+
build-backend = "setuptools.build_meta"
|
| 4 |
+
|
| 5 |
+
[project]
|
| 6 |
+
name = "bayesian-langgraph-rag"
|
| 7 |
+
version = "1.0.0"
|
| 8 |
+
description = "Uncertainty-aware Bayesian RAG implemented as a real LangGraph StateGraph"
|
| 9 |
+
readme = "README.md"
|
| 10 |
+
requires-python = ">=3.10"
|
| 11 |
+
license = {text = "MIT"}
|
| 12 |
+
authors = [{name = "Angshul Majumdar"}]
|
| 13 |
+
dependencies = [
|
| 14 |
+
"numpy>=1.24",
|
| 15 |
+
"scikit-learn>=1.3",
|
| 16 |
+
"langchain-core>=1.4.7,<2.0",
|
| 17 |
+
"langgraph>=1.2,<2.0",
|
| 18 |
+
]
|
| 19 |
+
|
| 20 |
+
[project.optional-dependencies]
|
| 21 |
+
dev = ["pytest>=8.0"]
|
| 22 |
+
|
| 23 |
+
[tool.setuptools]
|
| 24 |
+
package-dir = {"" = "src"}
|
| 25 |
+
|
| 26 |
+
[tool.setuptools.packages.find]
|
| 27 |
+
where = ["src"]
|
| 28 |
+
|
| 29 |
+
[tool.pytest.ini_options]
|
| 30 |
+
pythonpath = ["src"]
|
| 31 |
+
testpaths = ["tests"]
|
requirements.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
numpy>=1.24
|
| 2 |
+
scikit-learn>=1.3
|
| 3 |
+
langchain-core>=1.4.7,<2.0
|
| 4 |
+
langgraph>=1.2,<2.0
|
src/bayesian_langgraph/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .core import (
|
| 2 |
+
BayesianIndex,
|
| 3 |
+
BayesianRetrievalConfig,
|
| 4 |
+
DocumentRecord,
|
| 5 |
+
RetrievalHit,
|
| 6 |
+
ExtractiveGroundedGenerator,
|
| 7 |
+
)
|
| 8 |
+
|
| 9 |
+
__all__ = [
|
| 10 |
+
"BayesianIndex",
|
| 11 |
+
"BayesianRetrievalConfig",
|
| 12 |
+
"DocumentRecord",
|
| 13 |
+
"RetrievalHit",
|
| 14 |
+
"ExtractiveGroundedGenerator",
|
| 15 |
+
]
|
| 16 |
+
|
| 17 |
+
try:
|
| 18 |
+
from .graph import BayesianRAGGraph, RAGState
|
| 19 |
+
__all__ += ["BayesianRAGGraph", "RAGState"]
|
| 20 |
+
except ImportError:
|
| 21 |
+
pass
|
src/bayesian_langgraph/core.py
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from dataclasses import dataclass, field
|
| 4 |
+
from typing import Any, Iterable, Mapping, Sequence
|
| 5 |
+
import math
|
| 6 |
+
import re
|
| 7 |
+
|
| 8 |
+
import numpy as np
|
| 9 |
+
from sklearn.feature_extraction.text import TfidfVectorizer, ENGLISH_STOP_WORDS
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def _clip01(x: float, eps: float = 1e-6) -> float:
|
| 13 |
+
return min(max(float(x), eps), 1.0)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def _logsumexp(xs: Sequence[float]) -> float:
|
| 17 |
+
if not xs:
|
| 18 |
+
return float('-inf')
|
| 19 |
+
finite = [x for x in xs if math.isfinite(x)]
|
| 20 |
+
if not finite:
|
| 21 |
+
return float('-inf')
|
| 22 |
+
m = max(finite)
|
| 23 |
+
return m + math.log(sum(math.exp(x - m) for x in finite))
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
@dataclass(frozen=True)
|
| 27 |
+
class DocumentRecord:
|
| 28 |
+
id: str
|
| 29 |
+
text: str
|
| 30 |
+
title: str = ""
|
| 31 |
+
source: str = "unknown"
|
| 32 |
+
metadata: Mapping[str, Any] = field(default_factory=dict)
|
| 33 |
+
|
| 34 |
+
@property
|
| 35 |
+
def retrieval_text(self) -> str:
|
| 36 |
+
return (self.title + ". " + self.text).strip(". ")
|
| 37 |
+
|
| 38 |
+
def source_prior(self) -> float:
|
| 39 |
+
md = self.metadata
|
| 40 |
+
a = max(float(md.get("reliability_alpha", 1.0)), 1e-6)
|
| 41 |
+
b = max(float(md.get("reliability_beta", 1.0)), 1e-6)
|
| 42 |
+
reliability = a / (a + b)
|
| 43 |
+
authority = _clip01(md.get("authority", 0.5))
|
| 44 |
+
freshness = _clip01(md.get("freshness", 0.5))
|
| 45 |
+
return reliability * authority * freshness
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
@dataclass(frozen=True)
|
| 49 |
+
class RetrievalHit:
|
| 50 |
+
document: DocumentRecord
|
| 51 |
+
retrieval_score: float
|
| 52 |
+
log_prior: float
|
| 53 |
+
log_likelihood: float
|
| 54 |
+
log_posterior_unnormalized: float
|
| 55 |
+
posterior: float
|
| 56 |
+
query_coverage: float
|
| 57 |
+
rank: int
|
| 58 |
+
|
| 59 |
+
def as_dict(self) -> dict[str, Any]:
|
| 60 |
+
return {
|
| 61 |
+
"id": self.document.id,
|
| 62 |
+
"title": self.document.title,
|
| 63 |
+
"text": self.document.text,
|
| 64 |
+
"source": self.document.source,
|
| 65 |
+
"metadata": dict(self.document.metadata),
|
| 66 |
+
"retrieval_score": self.retrieval_score,
|
| 67 |
+
"log_prior": self.log_prior,
|
| 68 |
+
"log_likelihood": self.log_likelihood,
|
| 69 |
+
"posterior": self.posterior,
|
| 70 |
+
"query_coverage": self.query_coverage,
|
| 71 |
+
"rank": self.rank,
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
@dataclass(frozen=True)
|
| 76 |
+
class BayesianRetrievalConfig:
|
| 77 |
+
candidate_pool: int = 12
|
| 78 |
+
likelihood_temperature: float = 2.0
|
| 79 |
+
likelihood_floor: float = 0.02
|
| 80 |
+
reliability_weight: float = 1.0
|
| 81 |
+
authority_weight: float = 1.0
|
| 82 |
+
freshness_weight: float = 0.8
|
| 83 |
+
prior_floor: float = 1e-4
|
| 84 |
+
min_relevance: float = 0.06
|
| 85 |
+
min_top_posterior: float = 0.30
|
| 86 |
+
min_query_coverage: float = 0.30
|
| 87 |
+
min_dominance_ratio: float = 1.20
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
class BayesianIndex:
|
| 91 |
+
"""Generalized-Bayesian reranker over TF-IDF retrieval candidates.
|
| 92 |
+
|
| 93 |
+
Candidate document hypotheses are scored by
|
| 94 |
+
p(d|q) ∝ p(d) exp(tau * sim(q,d)).
|
| 95 |
+
p(d) is induced by a Beta source-reliability mean plus authority/freshness
|
| 96 |
+
metadata. The exponential similarity term is a Gibbs/generalized-Bayes
|
| 97 |
+
likelihood, not claimed to be a literal language-model likelihood.
|
| 98 |
+
"""
|
| 99 |
+
|
| 100 |
+
def __init__(self, documents: Sequence[DocumentRecord], config: BayesianRetrievalConfig | None = None):
|
| 101 |
+
if not documents:
|
| 102 |
+
raise ValueError("documents must be non-empty")
|
| 103 |
+
self.documents = list(documents)
|
| 104 |
+
self.config = config or BayesianRetrievalConfig()
|
| 105 |
+
self.vectorizer = TfidfVectorizer(
|
| 106 |
+
lowercase=True,
|
| 107 |
+
stop_words="english",
|
| 108 |
+
ngram_range=(1, 2),
|
| 109 |
+
sublinear_tf=True,
|
| 110 |
+
norm="l2",
|
| 111 |
+
)
|
| 112 |
+
self.matrix = self.vectorizer.fit_transform([d.retrieval_text for d in self.documents])
|
| 113 |
+
|
| 114 |
+
def _log_prior(self, d: DocumentRecord) -> float:
|
| 115 |
+
md = d.metadata
|
| 116 |
+
a = max(float(md.get("reliability_alpha", 1.0)), 1e-6)
|
| 117 |
+
b = max(float(md.get("reliability_beta", 1.0)), 1e-6)
|
| 118 |
+
rel = _clip01(a / (a + b))
|
| 119 |
+
auth = _clip01(md.get("authority", 0.5))
|
| 120 |
+
fresh = _clip01(md.get("freshness", 0.5))
|
| 121 |
+
cfg = self.config
|
| 122 |
+
prior = max(
|
| 123 |
+
cfg.prior_floor,
|
| 124 |
+
(rel ** cfg.reliability_weight)
|
| 125 |
+
* (auth ** cfg.authority_weight)
|
| 126 |
+
* (fresh ** cfg.freshness_weight),
|
| 127 |
+
)
|
| 128 |
+
return math.log(prior)
|
| 129 |
+
|
| 130 |
+
def lexical_scores(self, query: str) -> np.ndarray:
|
| 131 |
+
q = self.vectorizer.transform([str(query)])
|
| 132 |
+
# TF-IDF vectors are L2 normalized, so dot product is cosine similarity.
|
| 133 |
+
return (self.matrix @ q.T).toarray().ravel()
|
| 134 |
+
|
| 135 |
+
@staticmethod
|
| 136 |
+
def _query_coverage(query: str, text: str) -> float:
|
| 137 |
+
qterms = {t for t in re.findall(r"[A-Za-z0-9_-]+", query.lower()) if len(t) > 1 and t not in ENGLISH_STOP_WORDS}
|
| 138 |
+
if not qterms:
|
| 139 |
+
return 0.0
|
| 140 |
+
dterms = set(re.findall(r"[A-Za-z0-9_-]+", text.lower()))
|
| 141 |
+
return len(qterms & dterms) / len(qterms)
|
| 142 |
+
|
| 143 |
+
def search(self, query: str, k: int = 4) -> list[RetrievalHit]:
|
| 144 |
+
if k <= 0:
|
| 145 |
+
return []
|
| 146 |
+
scores = self.lexical_scores(query)
|
| 147 |
+
pool_n = min(max(k, self.config.candidate_pool), len(self.documents))
|
| 148 |
+
# Stable sort makes benchmark results reproducible under ties.
|
| 149 |
+
cand = np.argsort(-scores, kind="stable")[:pool_n]
|
| 150 |
+
logu: list[float] = []
|
| 151 |
+
rows: list[tuple[int, float, float, float]] = []
|
| 152 |
+
for i in cand:
|
| 153 |
+
d = self.documents[int(i)]
|
| 154 |
+
s = max(float(scores[int(i)]), 0.0)
|
| 155 |
+
lp = self._log_prior(d)
|
| 156 |
+
ll = self.config.likelihood_temperature * math.log(s + self.config.likelihood_floor)
|
| 157 |
+
u = lp + ll
|
| 158 |
+
rows.append((int(i), s, lp, ll))
|
| 159 |
+
logu.append(u)
|
| 160 |
+
lse = _logsumexp(logu)
|
| 161 |
+
probs = [math.exp(u - lse) if math.isfinite(lse) else 1.0 / len(logu) for u in logu]
|
| 162 |
+
ranked = sorted(
|
| 163 |
+
zip(rows, probs),
|
| 164 |
+
key=lambda rp: (rp[1], rp[0][1]),
|
| 165 |
+
reverse=True,
|
| 166 |
+
)
|
| 167 |
+
hits: list[RetrievalHit] = []
|
| 168 |
+
for rank, ((i, s, lp, ll), pr) in enumerate(ranked[:k], start=1):
|
| 169 |
+
hits.append(RetrievalHit(
|
| 170 |
+
document=self.documents[i],
|
| 171 |
+
retrieval_score=s,
|
| 172 |
+
log_prior=lp,
|
| 173 |
+
log_likelihood=ll,
|
| 174 |
+
log_posterior_unnormalized=lp + ll,
|
| 175 |
+
posterior=float(pr),
|
| 176 |
+
query_coverage=self._query_coverage(query, self.documents[i].retrieval_text),
|
| 177 |
+
rank=rank,
|
| 178 |
+
))
|
| 179 |
+
return hits
|
| 180 |
+
|
| 181 |
+
def should_abstain(self, hits: Sequence[RetrievalHit]) -> bool:
|
| 182 |
+
if not hits:
|
| 183 |
+
return True
|
| 184 |
+
top = hits[0]
|
| 185 |
+
if (
|
| 186 |
+
top.retrieval_score < self.config.min_relevance
|
| 187 |
+
or top.posterior < self.config.min_top_posterior
|
| 188 |
+
or top.query_coverage < self.config.min_query_coverage
|
| 189 |
+
):
|
| 190 |
+
return True
|
| 191 |
+
if len(hits) > 1:
|
| 192 |
+
second = hits[1]
|
| 193 |
+
dominance = top.posterior / max(second.posterior, 1e-12)
|
| 194 |
+
if dominance < self.config.min_dominance_ratio:
|
| 195 |
+
return True
|
| 196 |
+
return False
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
class ExtractiveGroundedGenerator:
|
| 200 |
+
"""Dependency-free grounded answer synthesizer for tests and offline use.
|
| 201 |
+
|
| 202 |
+
It selects the query-relevant sentence from the highest-posterior evidence
|
| 203 |
+
and returns that evidence sentence verbatim with its document id. It never
|
| 204 |
+
invents content not present in the retrieved context.
|
| 205 |
+
"""
|
| 206 |
+
|
| 207 |
+
abstain_text = "I do not know based on the provided documents."
|
| 208 |
+
|
| 209 |
+
@staticmethod
|
| 210 |
+
def _sentences(text: str) -> list[str]:
|
| 211 |
+
parts = re.split(r"(?<=[.!?])\s+|\n+", text.strip())
|
| 212 |
+
return [p.strip() for p in parts if p.strip()]
|
| 213 |
+
|
| 214 |
+
def generate(self, question: str, hits: Sequence[RetrievalHit]) -> tuple[str, list[str]]:
|
| 215 |
+
if not hits:
|
| 216 |
+
return self.abstain_text, []
|
| 217 |
+
# The posterior ranking has already selected the evidence hypothesis.
|
| 218 |
+
top = hits[0]
|
| 219 |
+
sents = self._sentences(top.document.text)
|
| 220 |
+
if not sents:
|
| 221 |
+
return self.abstain_text, []
|
| 222 |
+
if len(sents) == 1:
|
| 223 |
+
sentence = sents[0]
|
| 224 |
+
else:
|
| 225 |
+
v = TfidfVectorizer(lowercase=True, stop_words="english", ngram_range=(1, 2))
|
| 226 |
+
mat = v.fit_transform([question] + sents)
|
| 227 |
+
q = mat[0]
|
| 228 |
+
sims = (mat[1:] @ q.T).toarray().ravel()
|
| 229 |
+
sentence = sents[int(np.argmax(sims))]
|
| 230 |
+
return f"{sentence} [{top.document.id}]", [top.document.id]
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
class PlainTfidfIndex:
|
| 234 |
+
"""Conventional lexical baseline used by the end-to-end RAG benchmark."""
|
| 235 |
+
def __init__(self, documents: Sequence[DocumentRecord]):
|
| 236 |
+
self.documents = list(documents)
|
| 237 |
+
self.vectorizer = TfidfVectorizer(lowercase=True, stop_words="english", ngram_range=(1, 2), sublinear_tf=True, norm="l2")
|
| 238 |
+
self.matrix = self.vectorizer.fit_transform([d.retrieval_text for d in self.documents])
|
| 239 |
+
|
| 240 |
+
@staticmethod
|
| 241 |
+
def _query_coverage(query: str, text: str) -> float:
|
| 242 |
+
qterms = {t for t in re.findall(r"[A-Za-z0-9_-]+", query.lower()) if len(t) > 1 and t not in ENGLISH_STOP_WORDS}
|
| 243 |
+
if not qterms:
|
| 244 |
+
return 0.0
|
| 245 |
+
dterms = set(re.findall(r"[A-Za-z0-9_-]+", text.lower()))
|
| 246 |
+
return len(qterms & dterms) / len(qterms)
|
| 247 |
+
|
| 248 |
+
def search(self, query: str, k: int = 4) -> list[RetrievalHit]:
|
| 249 |
+
q = self.vectorizer.transform([query])
|
| 250 |
+
scores = (self.matrix @ q.T).toarray().ravel()
|
| 251 |
+
order = np.argsort(-scores, kind="stable")[:k]
|
| 252 |
+
# Baseline assigns normalized positive retrieval mass only for reporting.
|
| 253 |
+
vals = [max(float(scores[int(i)]), 0.0) for i in order]
|
| 254 |
+
z = sum(vals) or 1.0
|
| 255 |
+
return [RetrievalHit(self.documents[int(i)], float(scores[int(i)]), 0.0, 0.0, 0.0, vals[j]/z, 0.0, j+1) for j, i in enumerate(order)]
|
src/bayesian_langgraph/graph.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import Any, Literal, TypedDict
|
| 4 |
+
|
| 5 |
+
from .core import BayesianIndex, ExtractiveGroundedGenerator, RetrievalHit
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class RAGState(TypedDict, total=False):
|
| 9 |
+
query: str
|
| 10 |
+
hits: list[RetrievalHit]
|
| 11 |
+
decision: Literal["generate", "abstain"]
|
| 12 |
+
answer: str
|
| 13 |
+
citations: list[str]
|
| 14 |
+
abstained: bool
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class BayesianRAGGraph:
|
| 18 |
+
"""Actual LangGraph orchestration around the Bayesian retriever.
|
| 19 |
+
|
| 20 |
+
Graph topology:
|
| 21 |
+
START -> retrieve -> assess -> {generate | abstain} -> END
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
def __init__(self, index: BayesianIndex, generator: Any | None = None, k: int = 4):
|
| 25 |
+
self.index = index
|
| 26 |
+
self.generator = generator or ExtractiveGroundedGenerator()
|
| 27 |
+
self.k = k
|
| 28 |
+
self.graph = self._build_graph()
|
| 29 |
+
|
| 30 |
+
def _retrieve(self, state: RAGState) -> dict[str, Any]:
|
| 31 |
+
return {"hits": self.index.search(state["query"], k=self.k)}
|
| 32 |
+
|
| 33 |
+
def _assess(self, state: RAGState) -> dict[str, Any]:
|
| 34 |
+
decision: Literal["generate", "abstain"] = "abstain" if self.index.should_abstain(state.get("hits", [])) else "generate"
|
| 35 |
+
return {"decision": decision}
|
| 36 |
+
|
| 37 |
+
@staticmethod
|
| 38 |
+
def _route(state: RAGState) -> str:
|
| 39 |
+
return state.get("decision", "abstain")
|
| 40 |
+
|
| 41 |
+
def _generate(self, state: RAGState) -> dict[str, Any]:
|
| 42 |
+
hits = state.get("hits", [])
|
| 43 |
+
if hasattr(self.generator, "generate"):
|
| 44 |
+
answer, citations = self.generator.generate(state["query"], hits)
|
| 45 |
+
else:
|
| 46 |
+
context = "\n".join(f"[{h.document.id}] {h.document.text}" for h in hits)
|
| 47 |
+
prompt = (
|
| 48 |
+
"Answer only from the supplied context. Cite document ids in square brackets. "
|
| 49 |
+
"If unsupported, say you do not know.\n\n"
|
| 50 |
+
f"Context:\n{context}\n\nQuestion: {state['query']}"
|
| 51 |
+
)
|
| 52 |
+
resp = self.generator.invoke(prompt)
|
| 53 |
+
answer = getattr(resp, "content", str(resp))
|
| 54 |
+
citations = [h.document.id for h in hits if f"[{h.document.id}]" in answer]
|
| 55 |
+
return {"answer": answer, "citations": citations, "abstained": False}
|
| 56 |
+
|
| 57 |
+
@staticmethod
|
| 58 |
+
def _abstain(state: RAGState) -> dict[str, Any]:
|
| 59 |
+
return {
|
| 60 |
+
"answer": ExtractiveGroundedGenerator.abstain_text,
|
| 61 |
+
"citations": [],
|
| 62 |
+
"abstained": True,
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
def _build_graph(self):
|
| 66 |
+
try:
|
| 67 |
+
from langgraph.graph import StateGraph, START, END
|
| 68 |
+
except ImportError as exc:
|
| 69 |
+
raise ImportError("bayesian-langgraph-rag requires langgraph>=1.2,<2.0") from exc
|
| 70 |
+
|
| 71 |
+
builder = StateGraph(RAGState)
|
| 72 |
+
builder.add_node("retrieve", self._retrieve)
|
| 73 |
+
builder.add_node("assess", self._assess)
|
| 74 |
+
builder.add_node("generate", self._generate)
|
| 75 |
+
builder.add_node("abstain", self._abstain)
|
| 76 |
+
builder.add_edge(START, "retrieve")
|
| 77 |
+
builder.add_edge("retrieve", "assess")
|
| 78 |
+
builder.add_conditional_edges(
|
| 79 |
+
"assess",
|
| 80 |
+
self._route,
|
| 81 |
+
{"generate": "generate", "abstain": "abstain"},
|
| 82 |
+
)
|
| 83 |
+
builder.add_edge("generate", END)
|
| 84 |
+
builder.add_edge("abstain", END)
|
| 85 |
+
return builder.compile()
|
| 86 |
+
|
| 87 |
+
def invoke(self, query: str) -> dict[str, Any]:
|
| 88 |
+
state = self.graph.invoke({"query": query})
|
| 89 |
+
hits = state.get("hits", [])
|
| 90 |
+
return {
|
| 91 |
+
"question": query,
|
| 92 |
+
"answer": state.get("answer", ExtractiveGroundedGenerator.abstain_text),
|
| 93 |
+
"citations": state.get("citations", []),
|
| 94 |
+
"abstained": state.get("abstained", True),
|
| 95 |
+
"hits": [h.as_dict() for h in hits],
|
| 96 |
+
}
|
src/bayesian_langgraph/smc.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from dataclasses import dataclass, field
|
| 4 |
+
from typing import Any, Callable, Generic, Sequence, TypeVar
|
| 5 |
+
import math
|
| 6 |
+
import random
|
| 7 |
+
|
| 8 |
+
S = TypeVar("S")
|
| 9 |
+
A = TypeVar("A")
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def normalize_logweights(logw: Sequence[float]) -> list[float]:
|
| 13 |
+
if not logw:
|
| 14 |
+
return []
|
| 15 |
+
finite = [w for w in logw if math.isfinite(w)]
|
| 16 |
+
if not finite:
|
| 17 |
+
return [1.0 / len(logw)] * len(logw)
|
| 18 |
+
m = max(finite)
|
| 19 |
+
ws = [math.exp(w - m) if math.isfinite(w) else 0.0 for w in logw]
|
| 20 |
+
z = sum(ws)
|
| 21 |
+
if not math.isfinite(z) or z <= 0.0:
|
| 22 |
+
return [1.0 / len(logw)] * len(logw)
|
| 23 |
+
return [w / z for w in ws]
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def effective_sample_size(probs: Sequence[float]) -> float:
|
| 27 |
+
s2 = sum(float(p) ** 2 for p in probs)
|
| 28 |
+
return 0.0 if s2 <= 0.0 else 1.0 / s2
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def systematic_resample(probs: Sequence[float], rng: random.Random) -> list[int]:
|
| 32 |
+
n = len(probs)
|
| 33 |
+
if n == 0:
|
| 34 |
+
return []
|
| 35 |
+
cdf: list[float] = []
|
| 36 |
+
total = 0.0
|
| 37 |
+
for p in probs:
|
| 38 |
+
total += float(p)
|
| 39 |
+
cdf.append(total)
|
| 40 |
+
if total <= 0.0 or not math.isfinite(total):
|
| 41 |
+
return list(range(n))
|
| 42 |
+
cdf = [x / total for x in cdf]
|
| 43 |
+
cdf[-1] = 1.0
|
| 44 |
+
u0 = rng.random() / n
|
| 45 |
+
out: list[int] = []
|
| 46 |
+
j = 0
|
| 47 |
+
for i in range(n):
|
| 48 |
+
u = u0 + i / n
|
| 49 |
+
while j < n - 1 and u > cdf[j]:
|
| 50 |
+
j += 1
|
| 51 |
+
out.append(j)
|
| 52 |
+
return out
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
@dataclass(frozen=True)
|
| 56 |
+
class Proposal(Generic[A]):
|
| 57 |
+
action: A
|
| 58 |
+
probability: float
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
@dataclass
|
| 62 |
+
class Particle(Generic[S]):
|
| 63 |
+
state: S
|
| 64 |
+
logw: float = 0.0
|
| 65 |
+
history: list[Any] = field(default_factory=list)
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
@dataclass(frozen=True)
|
| 69 |
+
class SMCConfig:
|
| 70 |
+
n_particles: int = 32
|
| 71 |
+
n_steps: int = 3
|
| 72 |
+
ess_resample_fraction: float = 0.5
|
| 73 |
+
seed: int = 0
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
class ParticleFilter(Generic[S, A]):
|
| 77 |
+
"""Importance-corrected SMC runtime.
|
| 78 |
+
|
| 79 |
+
`propose(state, t)` returns a discrete proposal distribution q(a|state).
|
| 80 |
+
`transition(state, action, t)` produces the next state.
|
| 81 |
+
`log_target_increment(prev, action, nxt, t)` supplies the incremental
|
| 82 |
+
log target density. The importance update is log w += log target - log q.
|
| 83 |
+
Resampling is never performed after the final weighting step, so the final
|
| 84 |
+
posterior weights are preserved.
|
| 85 |
+
"""
|
| 86 |
+
|
| 87 |
+
def __init__(self, config: SMCConfig | None = None):
|
| 88 |
+
self.config = config or SMCConfig()
|
| 89 |
+
|
| 90 |
+
def run(
|
| 91 |
+
self,
|
| 92 |
+
initial_state: S,
|
| 93 |
+
propose: Callable[[S, int], Sequence[Proposal[A]]],
|
| 94 |
+
transition: Callable[[S, A, int], S],
|
| 95 |
+
log_target_increment: Callable[[S, A, S, int], float],
|
| 96 |
+
) -> list[Particle[S]]:
|
| 97 |
+
cfg = self.config
|
| 98 |
+
rng = random.Random(cfg.seed)
|
| 99 |
+
particles = [Particle(state=initial_state) for _ in range(cfg.n_particles)]
|
| 100 |
+
for t in range(cfg.n_steps):
|
| 101 |
+
nxt: list[Particle[S]] = []
|
| 102 |
+
for p in particles:
|
| 103 |
+
proposals = [x for x in propose(p.state, t) if x.probability > 0.0]
|
| 104 |
+
if not proposals:
|
| 105 |
+
raise ValueError("proposal distribution is empty")
|
| 106 |
+
z = sum(x.probability for x in proposals)
|
| 107 |
+
probs = [x.probability / z for x in proposals]
|
| 108 |
+
r = rng.random()
|
| 109 |
+
acc = 0.0
|
| 110 |
+
j = len(proposals) - 1
|
| 111 |
+
for i, pr in enumerate(probs):
|
| 112 |
+
acc += pr
|
| 113 |
+
if r <= acc:
|
| 114 |
+
j = i
|
| 115 |
+
break
|
| 116 |
+
selected = proposals[j]
|
| 117 |
+
q = probs[j]
|
| 118 |
+
new_state = transition(p.state, selected.action, t)
|
| 119 |
+
inc = float(log_target_increment(p.state, selected.action, new_state, t))
|
| 120 |
+
nxt.append(Particle(new_state, p.logw + inc - math.log(q), p.history + [selected.action]))
|
| 121 |
+
particles = nxt
|
| 122 |
+
probs_now = normalize_logweights([p.logw for p in particles])
|
| 123 |
+
if t < cfg.n_steps - 1 and effective_sample_size(probs_now) < cfg.ess_resample_fraction * len(particles):
|
| 124 |
+
ids = systematic_resample(probs_now, rng)
|
| 125 |
+
particles = [Particle(particles[i].state, 0.0, list(particles[i].history)) for i in ids]
|
| 126 |
+
final_probs = normalize_logweights([p.logw for p in particles])
|
| 127 |
+
for p, pr in zip(particles, final_probs):
|
| 128 |
+
setattr(p, "posterior", pr)
|
| 129 |
+
return particles
|
tests/test_core.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from bayesian_langgraph.core import BayesianIndex, DocumentRecord
|
| 2 |
+
from bayesian_langgraph.smc import normalize_logweights
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def test_all_negative_infinity_weights_become_uniform():
|
| 6 |
+
p = normalize_logweights([float('-inf'), float('-inf')])
|
| 7 |
+
assert p == [0.5, 0.5]
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def test_authoritative_current_document_beats_stale_high_overlap():
|
| 11 |
+
docs = [
|
| 12 |
+
DocumentRecord('stale','The current HR portal is WorkSphere.','Current HR portal','cache',{'reliability_alpha':6,'reliability_beta':6,'authority':0.45,'freshness':0.18}),
|
| 13 |
+
DocumentRecord('official','Leave requests and payslips are now handled in PeopleHub.','Migration complete','official',{'reliability_alpha':18,'reliability_beta':2,'authority':0.98,'freshness':1.0}),
|
| 14 |
+
]
|
| 15 |
+
hits = BayesianIndex(docs).search('What is the current HR portal for leave requests and payslips?', k=2)
|
| 16 |
+
assert hits[0].document.id == 'official'
|
tests/test_framework_rag_e2e.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from evaluation.framework_rag_benchmark import run_framework_benchmark
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
def test_real_langgraph_stategraph_rag_suite():
|
| 5 |
+
result = run_framework_benchmark()
|
| 6 |
+
assert result["clean"]["passed"] == result["clean"]["n_cases"] == 8
|
| 7 |
+
assert result["conflict"]["passed"] == result["conflict"]["n_cases"] == 18
|
| 8 |
+
assert {"retrieve", "assess", "generate", "abstain"}.issubset(result["stategraph_nodes"])
|
tests/test_langgraph_integration.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from langchain_core.runnables import RunnableLambda
|
| 2 |
+
|
| 3 |
+
from bayesian_langgraph import BayesianIndex, BayesianRAGGraph, DocumentRecord
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def _docs():
|
| 7 |
+
return [
|
| 8 |
+
DocumentRecord(
|
| 9 |
+
'd1',
|
| 10 |
+
'The access code is ORBIT-7.',
|
| 11 |
+
'Access',
|
| 12 |
+
'official',
|
| 13 |
+
{'reliability_alpha':10,'reliability_beta':1,'authority':0.99,'freshness':1.0},
|
| 14 |
+
)
|
| 15 |
+
]
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def test_actual_stategraph_executes_rag():
|
| 19 |
+
rag = BayesianRAGGraph(BayesianIndex(_docs()))
|
| 20 |
+
out = rag.invoke('What is the access code?')
|
| 21 |
+
assert 'ORBIT-7' in out['answer']
|
| 22 |
+
assert out['citations'] == ['d1']
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def test_stategraph_model_runnable_generation_branch():
|
| 26 |
+
generator = RunnableLambda(lambda prompt: 'The access code is ORBIT-7. [d1]')
|
| 27 |
+
rag = BayesianRAGGraph(BayesianIndex(_docs()), generator=generator, k=1)
|
| 28 |
+
out = rag.invoke('What is the access code?')
|
| 29 |
+
assert out['answer'] == 'The access code is ORBIT-7. [d1]'
|
| 30 |
+
assert out['citations'] == ['d1']
|
| 31 |
+
assert not out['abstained']
|
tests/test_rag_e2e.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from evaluation.rag_benchmark import run_benchmark
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
def test_end_to_end_rag_benchmark():
|
| 5 |
+
x = run_benchmark()
|
| 6 |
+
assert x['clean']['bayesian']['end_to_end_accuracy'] == 1.0
|
| 7 |
+
assert x['conflict']['bayesian']['recall_at_1'] == 1.0
|
| 8 |
+
assert x['conflict']['bayesian']['abstention_accuracy'] == 1.0
|
| 9 |
+
assert x['conflict']['bayesian']['grounding_accuracy'] == 1.0
|
tests/test_smc_math.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
def _imports():
|
| 5 |
+
try:
|
| 6 |
+
from bayesian_langchain.smc import ParticleFilter, Proposal, SMCConfig
|
| 7 |
+
except ImportError:
|
| 8 |
+
from bayesian_langgraph.smc import ParticleFilter, Proposal, SMCConfig
|
| 9 |
+
return ParticleFilter, Proposal, SMCConfig
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def test_final_step_preserves_nonuniform_posterior_weights():
|
| 13 |
+
ParticleFilter, Proposal, SMCConfig = _imports()
|
| 14 |
+
pf = ParticleFilter(SMCConfig(n_particles=64, n_steps=1, ess_resample_fraction=0.99, seed=7))
|
| 15 |
+
|
| 16 |
+
def propose(state, t):
|
| 17 |
+
return [Proposal("good", 0.5), Proposal("bad", 0.5)]
|
| 18 |
+
|
| 19 |
+
def transition(state, action, t):
|
| 20 |
+
return action
|
| 21 |
+
|
| 22 |
+
def log_target(prev, action, nxt, t):
|
| 23 |
+
return 0.0 if action == "good" else math.log(1e-5)
|
| 24 |
+
|
| 25 |
+
particles = pf.run(None, propose, transition, log_target)
|
| 26 |
+
probs = [p.posterior for p in particles]
|
| 27 |
+
assert abs(sum(probs) - 1.0) < 1e-12
|
| 28 |
+
# If the old final-resampling bug returned, these would all be 1/N.
|
| 29 |
+
assert max(probs) - min(probs) > 1e-3
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def test_importance_weight_contains_inverse_proposal_probability():
|
| 33 |
+
ParticleFilter, Proposal, SMCConfig = _imports()
|
| 34 |
+
pf = ParticleFilter(SMCConfig(n_particles=256, n_steps=1, seed=11))
|
| 35 |
+
|
| 36 |
+
def propose(state, t):
|
| 37 |
+
return [Proposal("a", 0.9), Proposal("b", 0.1)]
|
| 38 |
+
|
| 39 |
+
def transition(state, action, t):
|
| 40 |
+
return action
|
| 41 |
+
|
| 42 |
+
# Equal target mass for both actions. Importance correction should cancel
|
| 43 |
+
# the 9:1 proposal imbalance in the weighted posterior, up to MC error.
|
| 44 |
+
def log_target(prev, action, nxt, t):
|
| 45 |
+
return math.log(0.5)
|
| 46 |
+
|
| 47 |
+
particles = pf.run(None, propose, transition, log_target)
|
| 48 |
+
mass = {"a": 0.0, "b": 0.0}
|
| 49 |
+
for p in particles:
|
| 50 |
+
mass[p.state] += p.posterior
|
| 51 |
+
assert abs(mass["a"] - 0.5) < 0.15
|
| 52 |
+
assert abs(mass["b"] - 0.5) < 0.15
|