ragdiag 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. ragdiag-0.1.0/.github/workflows/publish.yml +166 -0
  2. ragdiag-0.1.0/.github/workflows/test.yml +42 -0
  3. ragdiag-0.1.0/.gitignore +51 -0
  4. ragdiag-0.1.0/LICENSE +21 -0
  5. ragdiag-0.1.0/PKG-INFO +407 -0
  6. ragdiag-0.1.0/README.md +378 -0
  7. ragdiag-0.1.0/examples/README.md +107 -0
  8. ragdiag-0.1.0/examples/basic_dataset.json +41 -0
  9. ragdiag-0.1.0/examples/basic_pipeline.py +89 -0
  10. ragdiag-0.1.0/examples/demo_dataset.json +55 -0
  11. ragdiag-0.1.0/examples/dense_pipeline.py +83 -0
  12. ragdiag-0.1.0/examples/hybrid_pipeline.py +93 -0
  13. ragdiag-0.1.0/pyproject.toml +72 -0
  14. ragdiag-0.1.0/src/ragdiag/__init__.py +48 -0
  15. ragdiag-0.1.0/src/ragdiag/cli/__init__.py +7 -0
  16. ragdiag-0.1.0/src/ragdiag/cli/main.py +342 -0
  17. ragdiag-0.1.0/src/ragdiag/comparison/__init__.py +20 -0
  18. ragdiag-0.1.0/src/ragdiag/comparison/comparator.py +548 -0
  19. ragdiag-0.1.0/src/ragdiag/comparison/models.py +124 -0
  20. ragdiag-0.1.0/src/ragdiag/comparison/terminal.py +217 -0
  21. ragdiag-0.1.0/src/ragdiag/dataset/__init__.py +23 -0
  22. ragdiag-0.1.0/src/ragdiag/dataset/exceptions.py +17 -0
  23. ragdiag-0.1.0/src/ragdiag/dataset/loader.py +72 -0
  24. ragdiag-0.1.0/src/ragdiag/dataset/validator.py +80 -0
  25. ragdiag-0.1.0/src/ragdiag/diagnosis/__init__.py +26 -0
  26. ragdiag-0.1.0/src/ragdiag/diagnosis/classifier.py +169 -0
  27. ragdiag-0.1.0/src/ragdiag/diagnosis/models.py +48 -0
  28. ragdiag-0.1.0/src/ragdiag/diagnosis/rules.py +208 -0
  29. ragdiag-0.1.0/src/ragdiag/judges/__init__.py +22 -0
  30. ragdiag-0.1.0/src/ragdiag/judges/base.py +61 -0
  31. ragdiag-0.1.0/src/ragdiag/judges/exceptions.py +17 -0
  32. ragdiag-0.1.0/src/ragdiag/judges/models.py +35 -0
  33. ragdiag-0.1.0/src/ragdiag/judges/openai.py +157 -0
  34. ragdiag-0.1.0/src/ragdiag/metrics/__init__.py +29 -0
  35. ragdiag-0.1.0/src/ragdiag/metrics/aggregation.py +175 -0
  36. ragdiag-0.1.0/src/ragdiag/metrics/latency.py +83 -0
  37. ragdiag-0.1.0/src/ragdiag/metrics/models.py +74 -0
  38. ragdiag-0.1.0/src/ragdiag/metrics/retrieval.py +132 -0
  39. ragdiag-0.1.0/src/ragdiag/models/__init__.py +14 -0
  40. ragdiag-0.1.0/src/ragdiag/models/chunk.py +19 -0
  41. ragdiag-0.1.0/src/ragdiag/models/dataset.py +41 -0
  42. ragdiag-0.1.0/src/ragdiag/models/result.py +37 -0
  43. ragdiag-0.1.0/src/ragdiag/models/sample.py +59 -0
  44. ragdiag-0.1.0/src/ragdiag/pipeline/__init__.py +12 -0
  45. ragdiag-0.1.0/src/ragdiag/pipeline/base.py +57 -0
  46. ragdiag-0.1.0/src/ragdiag/pipeline/exceptions.py +11 -0
  47. ragdiag-0.1.0/src/ragdiag/pipeline/loader.py +65 -0
  48. ragdiag-0.1.0/src/ragdiag/reporting/__init__.py +23 -0
  49. ragdiag-0.1.0/src/ragdiag/reporting/aggregator.py +335 -0
  50. ragdiag-0.1.0/src/ragdiag/reporting/insights.py +149 -0
  51. ragdiag-0.1.0/src/ragdiag/reporting/models.py +131 -0
  52. ragdiag-0.1.0/src/ragdiag/reporting/terminal.py +142 -0
  53. ragdiag-0.1.0/src/ragdiag/runner/__init__.py +7 -0
  54. ragdiag-0.1.0/src/ragdiag/runner/evaluator.py +234 -0
  55. ragdiag-0.1.0/tests/__init__.py +1 -0
  56. ragdiag-0.1.0/tests/test_comparison.py +723 -0
  57. ragdiag-0.1.0/tests/test_dataset.py +456 -0
  58. ragdiag-0.1.0/tests/test_diagnosis.py +644 -0
  59. ragdiag-0.1.0/tests/test_judges.py +509 -0
  60. ragdiag-0.1.0/tests/test_metrics.py +413 -0
  61. ragdiag-0.1.0/tests/test_models.py +261 -0
  62. ragdiag-0.1.0/tests/test_pipeline.py +69 -0
  63. ragdiag-0.1.0/tests/test_pipeline_loader.py +90 -0
  64. ragdiag-0.1.0/tests/test_reporting.py +601 -0
  65. ragdiag-0.1.0/tests/test_runner.py +305 -0
  66. ragdiag-0.1.0/uv.lock +489 -0
@@ -0,0 +1,166 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ workflow_dispatch:
7
+ inputs:
8
+ target:
9
+ description: "Target registry environment"
10
+ required: true
11
+ type: choice
12
+ default: "testpypi"
13
+ options:
14
+ - "testpypi"
15
+ - "pypi"
16
+
17
+ permissions:
18
+ contents: read
19
+
20
+ jobs:
21
+ build:
22
+ name: Build distribution packages
23
+ runs-on: ubuntu-latest
24
+ permissions:
25
+ contents: read
26
+ steps:
27
+ - name: Check out repository
28
+ uses: actions/checkout@v4
29
+
30
+ - name: Set up Python 3.12
31
+ uses: actions/setup-python@v5
32
+ with:
33
+ python-version: "3.12"
34
+
35
+ - name: Install PyPA build tooling
36
+ run: |
37
+ python -m pip install --upgrade pip build twine
38
+
39
+ - name: Verify package version safety
40
+ env:
41
+ EVENT_NAME: ${{ github.event_name }}
42
+ RELEASE_TAG: ${{ github.event.release.tag_name }}
43
+ REF_NAME: ${{ github.ref_name }}
44
+ REF_TYPE: ${{ github.ref_type }}
45
+ run: |
46
+ python - << 'EOF'
47
+ import os, sys, tomllib, re
48
+
49
+ # 1. Read declared pyproject.toml version
50
+ with open("pyproject.toml", "rb") as f:
51
+ pyproject_data = tomllib.load(f)
52
+ pkg_version = pyproject_data.get("project", {}).get("version")
53
+ if not pkg_version:
54
+ sys.exit("ERROR: Could not find [project].version in pyproject.toml")
55
+
56
+ # 2. Read declared src/ragdiag/__init__.py __version__
57
+ with open("src/ragdiag/__init__.py", "r", encoding="utf-8") as f:
58
+ init_content = f.read()
59
+ init_match = re.search(r'__version__\s*=\s*["\']([^"\']+)["\']', init_content)
60
+ init_version = init_match.group(1) if init_match else None
61
+
62
+ print(f"pyproject.toml version: {pkg_version}")
63
+ print(f"src/ragdiag/__init__.py version: {init_version}")
64
+
65
+ if pkg_version != init_version:
66
+ sys.exit(
67
+ f"ERROR: Version mismatch inside repository!\n"
68
+ f" pyproject.toml: {pkg_version}\n"
69
+ f" src/ragdiag/__init__.py: {init_version}"
70
+ )
71
+
72
+ # 3. If triggered by a release or git tag, strictly verify tag match
73
+ event_name = os.environ.get("EVENT_NAME", "")
74
+ release_tag = os.environ.get("RELEASE_TAG", "").strip()
75
+ ref_name = os.environ.get("REF_NAME", "").strip()
76
+ ref_type = os.environ.get("REF_TYPE", "").strip()
77
+
78
+ tag = release_tag or (ref_name if ref_type == "tag" else "")
79
+
80
+ if tag:
81
+ normalized_tag = tag[1:] if tag.startswith("v") else tag
82
+ if normalized_tag != pkg_version:
83
+ sys.exit(
84
+ f"ERROR: Release tag does not match package version!\n"
85
+ f" Git Tag: {tag} (normalized: {normalized_tag})\n"
86
+ f" Package Version: {pkg_version}\n"
87
+ f"Publishing aborted to prevent mismatched artifact release."
88
+ )
89
+ print(f"SUCCESS: Git tag '{tag}' matches package version '{pkg_version}'.")
90
+ else:
91
+ print(f"INFO: Workflow run without git tag (event: {event_name}). Internal version consistency verified.")
92
+ EOF
93
+
94
+ - name: Build sdist and wheel
95
+ run: python -m build
96
+
97
+ - name: Verify build artifacts
98
+ run: |
99
+ python - << 'EOF'
100
+ import os, sys
101
+
102
+ dist_files = sorted(os.listdir("dist"))
103
+ print("Artifacts generated in dist/:", dist_files)
104
+
105
+ has_wheel = any(f.endswith(".whl") for f in dist_files)
106
+ has_sdist = any(f.endswith(".tar.gz") for f in dist_files)
107
+
108
+ if not has_wheel:
109
+ sys.exit("ERROR: No .whl wheel package found in dist/")
110
+ if not has_sdist:
111
+ sys.exit("ERROR: No .tar.gz source distribution found in dist/")
112
+
113
+ print("SUCCESS: Both wheel and source distribution artifacts exist.")
114
+ EOF
115
+ python -m twine check --strict dist/*
116
+ ls -la dist/
117
+
118
+ - name: Upload build artifacts
119
+ uses: actions/upload-artifact@v4
120
+ with:
121
+ name: dist-artifacts
122
+ path: dist/
123
+ retention-days: 1
124
+
125
+ publish-testpypi:
126
+ name: Publish to TestPyPI
127
+ needs: build
128
+ if: (github.event_name == 'release' && github.event.release.prerelease) || (github.event_name == 'workflow_dispatch' && inputs.target == 'testpypi')
129
+ runs-on: ubuntu-latest
130
+ environment:
131
+ name: testpypi
132
+ url: https://test.pypi.org/p/ragdiag
133
+ permissions:
134
+ id-token: write
135
+ steps:
136
+ - name: Download build artifacts
137
+ uses: actions/download-artifact@v4
138
+ with:
139
+ name: dist-artifacts
140
+ path: dist/
141
+
142
+ - name: Publish package distributions to TestPyPI
143
+ uses: pypa/gh-action-pypi-publish@release/v1
144
+ with:
145
+ repository-url: https://test.pypi.org/legacy/
146
+ skip-existing: true
147
+
148
+ publish-pypi:
149
+ name: Publish to Production PyPI
150
+ needs: build
151
+ if: (github.event_name == 'release' && !github.event.release.prerelease) || (github.event_name == 'workflow_dispatch' && inputs.target == 'pypi')
152
+ runs-on: ubuntu-latest
153
+ environment:
154
+ name: pypi
155
+ url: https://pypi.org/p/ragdiag
156
+ permissions:
157
+ id-token: write
158
+ steps:
159
+ - name: Download build artifacts
160
+ uses: actions/download-artifact@v4
161
+ with:
162
+ name: dist-artifacts
163
+ path: dist/
164
+
165
+ - name: Publish package distributions to PyPI
166
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,42 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [ main ]
6
+ pull_request:
7
+ branches: [ main ]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+
13
+ steps:
14
+ - name: Check out repository
15
+ uses: actions/checkout@v4
16
+
17
+ - name: Set up Python
18
+ uses: actions/setup-python@v5
19
+ with:
20
+ python-version: "3.12"
21
+
22
+ - name: Install uv
23
+ uses: astral-sh/setup-uv@v3
24
+ with:
25
+ version: "latest"
26
+
27
+ - name: Install dependencies
28
+ run: |
29
+ uv venv
30
+ uv pip install -e ".[dev]"
31
+
32
+ - name: Run Ruff Lint
33
+ run: |
34
+ uv run ruff check .
35
+
36
+ - name: Run Ruff Format Check
37
+ run: |
38
+ uv run ruff format --check .
39
+
40
+ - name: Run Pytest
41
+ run: |
42
+ uv run pytest
@@ -0,0 +1,51 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # Virtual environments
30
+ .venv/
31
+ env/
32
+ venv/
33
+ ENV/
34
+ env.bak/
35
+ venv.bak/
36
+
37
+ # Testing & linting
38
+ .pytest_cache/
39
+ .ruff_cache/
40
+ .coverage
41
+ htmlcov/
42
+ .mypy_cache/
43
+
44
+ # IDE & OS files
45
+ .vscode/
46
+ .idea/
47
+ *.swp
48
+ *.swo
49
+ *~
50
+ .DS_Store
51
+ Thumbs.db
ragdiag-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 RAGDiag Contributors
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.
ragdiag-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,407 @@
1
+ Metadata-Version: 2.5
2
+ Name: ragdiag
3
+ Version: 0.1.0
4
+ Summary: RAG evaluation and root-cause diagnosis SDK/CLI
5
+ Project-URL: Homepage, https://github.com/SayanBhattacharjee2006/ragdiag
6
+ Project-URL: Repository, https://github.com/SayanBhattacharjee2006/ragdiag
7
+ Project-URL: Issues, https://github.com/SayanBhattacharjee2006/ragdiag/issues
8
+ Author: RAGDiag Contributors
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: diagnosis,evaluation,hallucination,llm,metrics,rag
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
18
+ Classifier: Topic :: Software Development :: Quality Assurance
19
+ Classifier: Topic :: Software Development :: Testing
20
+ Requires-Python: >=3.12
21
+ Requires-Dist: openai>=1.40.0
22
+ Requires-Dist: pydantic>=2.7.0
23
+ Requires-Dist: rich>=13.7.0
24
+ Requires-Dist: typer>=0.12.0
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
27
+ Requires-Dist: ruff>=0.4.0; extra == 'dev'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # RAGDiag
31
+
32
+ > **Developer tool for evaluating RAG pipelines and identifying evidence-backed primary failure categories.** Built for the Razorpay Buildathon.
33
+
34
+ [![Python Version](https://img.shields.io/badge/python-3.12%2B-blue.svg)](https://www.python.org/)
35
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
36
+
37
+ ---
38
+
39
+ ## Problem
40
+
41
+ Traditional RAG evaluation frameworks calculate scalar scores (e.g. an aggregate score of 0.72) but leave developers guessing when outputs degrade:
42
+ - Did the vector retriever miss the relevant context chunks entirely?
43
+ - Were the right chunks retrieved but buried beneath distracting irrelevant chunks?
44
+ - Was only partial context retrieved for multi-part questions?
45
+ - Did the LLM hallucinate unsupported claims despite having the context?
46
+ - Or did retrieval latency spike beyond acceptable production thresholds?
47
+
48
+ When engineers test an architectural change—such as switching from dense semantic search to hybrid search—isolated numbers cannot explain whether higher recall justifies the additional latency or which specific queries improved or regressed.
49
+
50
+ ---
51
+
52
+ ## What RAGDiag Does
53
+
54
+ RAGDiag inspects the raw evidence captured during execution across retrieval and generation to classify why individual queries fail, generate system-level diagnostic intelligence, and perform evidence-based comparisons between pipeline architectures:
55
+
56
+ ```text
57
+ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
58
+ │ RAG Pipeline │ ───> │ Evaluation │ ───> │ Root-Cause │ ───> │ Multi-Pipeline │
59
+ │ (Adapter) │ │ Harness │ │ Diagnosis │ │ Comparison │
60
+ └─────────────────┘ └─────────────────┘ └─────────────────┘ └─────────────────┘
61
+ Custom retriever Precision@K, MRR, 8-Category Decision Directional deltas,
62
+ and generator Latency, LLM Judge Precedence Hierarchy Transitions, Winners
63
+ ```
64
+
65
+ ---
66
+
67
+ ## Key Capabilities
68
+
69
+ - **Framework-Agnostic Adapter**: Wrap any RAG stack (custom vector stores, LangChain, LlamaIndex, BM25) in standard Python methods.
70
+ - **Golden Dataset Schema**: Validated ground-truth dataset format with categorized query types (`factual`, `reasoning`, `multi-hop`).
71
+ - **Deterministic Retrieval Metrics**: Exact calculations for Precision@K, Recall@K, Reciprocal Rank, and MRR.
72
+ - **Latency Distribution Analysis**: Non-parametric percentile statistics (Mean, P50, P95, P99, Min, Max) for retrieval and generation stages.
73
+ - **Isolated LLM Judge**: Evaluates answer correctness against ground truth and context groundedness against retrieved chunks via schema-enforced structured outputs.
74
+ - **Evidence-Based Root-Cause Diagnosis**: Classifies failures into a deterministic 8-category taxonomy without asking an unconstrained LLM to guess.
75
+ - **Diagnostic System Reports**: Aggregates query-type breakdowns, deterministically ranked top failures, and rule-based insights.
76
+ - **Multi-Pipeline A/B Comparison**: Compares two pipeline configurations side-by-side on the same dataset, calculating metric deltas, failure shifts, query transitions (`improved`, `regressed`, `unchanged`), and deterministic winner/trade-off decisions.
77
+ - **CLI & Typed JSON Exports**: Formatted Rich terminal reports and complete Pydantic JSON serialization.
78
+
79
+ ---
80
+
81
+ ## Quickstart
82
+
83
+ ### Installation
84
+
85
+ ```bash
86
+ pip install ragdiag
87
+ ```
88
+
89
+ Or when developing with [`uv`](https://docs.astral.sh/uv/):
90
+
91
+ ```bash
92
+ git clone https://github.com/SayanBhattacharjee2006/ragdiag.git
93
+ cd ragdiag
94
+ uv venv
95
+ uv pip install -e ".[dev]"
96
+ ```
97
+
98
+ ---
99
+
100
+ ## Pipeline Adapter Interface
101
+
102
+ Developers adapt their existing RAG pipeline by subclassing `ragdiag.Pipeline` and exposing a top-level instance named `pipeline`:
103
+
104
+ ```python
105
+ # my_pipeline.py
106
+ from ragdiag import Pipeline, RetrievedChunk
107
+
108
+
109
+ class MyCustomPipeline(Pipeline):
110
+ name = "payment_faq_rag"
111
+
112
+ def retrieve(self, query: str) -> list[RetrievedChunk]:
113
+ # Connect to your vector DB, dense index, or hybrid retriever
114
+ return [
115
+ RetrievedChunk(
116
+ id="doc_refund_policy_01",
117
+ text="Standard card refunds settle within 5 to 7 business days.",
118
+ score=0.92,
119
+ )
120
+ ]
121
+
122
+ def generate(self, query: str, chunks: list[RetrievedChunk]) -> str:
123
+ # Pass context to your LLM generator
124
+ return "Card refund settlements typically take 5 to 7 business days."
125
+
126
+
127
+ pipeline = MyCustomPipeline()
128
+ ```
129
+
130
+ ---
131
+
132
+ ## Golden Dataset System
133
+
134
+ Datasets are JSON files matching the `GoldenDataset` schema with ground truth and categorized query types:
135
+
136
+ ```json
137
+ {
138
+ "name": "payment_gateway_eval",
139
+ "version": "1.0",
140
+ "samples": [
141
+ {
142
+ "id": "q001",
143
+ "query": "What is the standard turnaround time for card refund settlements?",
144
+ "expected_answer": "Standard card refund settlements are credited within 5 to 7 business days.",
145
+ "relevant_chunk_ids": ["doc_refund_policy_01"],
146
+ "query_type": "factual"
147
+ },
148
+ {
149
+ "id": "q002",
150
+ "query": "Why was the customer's recurring auto-debit declined?",
151
+ "expected_answer": "The auto-debit was declined because the e-mandate registration expired.",
152
+ "relevant_chunk_ids": ["doc_subscriptions_03", "doc_mandates_05"],
153
+ "query_type": "reasoning"
154
+ },
155
+ {
156
+ "id": "q003",
157
+ "query": "What is the effective net settlement fee considering base interchange and GST?",
158
+ "expected_answer": "The effective fee is 2.5% base fee plus 18% GST on the fee, totaling 2.95%.",
159
+ "relevant_chunk_ids": ["doc_pricing_tier_01", "doc_tax_regulations_03"],
160
+ "query_type": "multi-hop"
161
+ }
162
+ ]
163
+ }
164
+ ```
165
+
166
+ ### Validate a Dataset
167
+
168
+ ```bash
169
+ ragdiag validate --dataset examples/demo_dataset.json
170
+ ```
171
+
172
+ ---
173
+
174
+ ## Run Evaluation
175
+
176
+ Evaluate a single pipeline configuration and print the diagnostic terminal report:
177
+
178
+ ```bash
179
+ ragdiag run --pipeline examples/basic_pipeline.py --dataset examples/basic_dataset.json
180
+ ```
181
+
182
+ ### Enable Semantic LLM Judge
183
+
184
+ ```bash
185
+ export OPENAI_API_KEY="sk-..."
186
+ ragdiag run \
187
+ --pipeline examples/basic_pipeline.py \
188
+ --dataset examples/basic_dataset.json \
189
+ --judge openai \
190
+ --model gpt-4o-mini
191
+ ```
192
+
193
+ ### Export JSON Report
194
+
195
+ ```bash
196
+ ragdiag run \
197
+ --pipeline examples/basic_pipeline.py \
198
+ --dataset examples/basic_dataset.json \
199
+ --output evaluation_report.json
200
+ ```
201
+
202
+ ---
203
+
204
+ ## Compare Two Pipelines
205
+
206
+ Compare a baseline pipeline (Pipeline A) against a candidate architecture (Pipeline B) on the same dataset:
207
+
208
+ ```bash
209
+ ragdiag compare \
210
+ --pipeline-a examples/dense_pipeline.py \
211
+ --pipeline-b examples/hybrid_pipeline.py \
212
+ --dataset examples/demo_dataset.json
213
+ ```
214
+
215
+ ### Real Deterministic Comparison Output
216
+
217
+ ```text
218
+ Running multi-pipeline comparison...
219
+
220
+ RAGDiag Comparison
221
+ ==================================================
222
+ Dataset: payment_gateway_demo_eval (v1.0)
223
+ Pipeline A: dense_pipeline
224
+ Pipeline B: hybrid_pipeline
225
+
226
+ OVERALL METRICS
227
+ --------------------------------------------------
228
+ Metric dense_pipeline hybrid_pipeline Delta (B-A)
229
+ --------------------------------------------------------------
230
+ Precision@5 0.87 0.90 +0.03
231
+ Recall@5 0.80 1.00 +0.20
232
+ MRR 0.87 0.87 0.00
233
+ Mean Retrieval 5.2ms 25.5ms +20.25ms
234
+ P95 Retrieval 5.3ms 25.7ms +20.35ms
235
+
236
+ FAILURE COUNTS
237
+ --------------------------------------------------
238
+ Category dense_pipeline hybrid_pipeline Delta
239
+ ----------------------------------------------------------
240
+ PASS 3 5 +2
241
+ WRONG_CHUNK_RETRIEVED 0 0 0
242
+ WRONG_CHUNK_RANK 0 0 0
243
+ INSUFFICIENT_CONTEXT 2 0 -2
244
+ RETRIEVED_BUT_NOT_GROUNDED 0 0 0
245
+ ANSWER_INCORRECT 0 0 0
246
+ LATENCY_OUTLIER 0 0 0
247
+ UNKNOWN 0 0 0
248
+
249
+ QUERY TYPES
250
+ --------------------------------------------------
251
+ Factual
252
+ Recall@5: 1.00 -> 1.00 (0.00) MRR: 1.00 -> 1.00 (0.00)
253
+
254
+ Reasoning
255
+ Recall@5: 0.75 -> 1.00 (+0.25) MRR: 1.00 -> 1.00 (0.00)
256
+ Failure count delta: -1
257
+
258
+ Multi-hop
259
+ Recall@5: 0.50 -> 1.00 (+0.50) MRR: 0.33 -> 0.33 (0.00)
260
+ Failure count delta: -1
261
+
262
+ DECISION
263
+ --------------------------------------------------
264
+ Overall winner: hybrid_pipeline
265
+
266
+ Why:
267
+ hybrid_pipeline improves Recall@5 by 20 percentage points and MRR by 0 points,
268
+ while increasing mean retrieval latency by 20.3 ms.
269
+
270
+ Trade-off:
271
+ Higher quality <-> higher latency
272
+
273
+ QUERY OUTCOMES
274
+ --------------------------------------------------
275
+ Improved: 2
276
+ Regressed: 0
277
+ Unchanged: 3
278
+
279
+ Total comparison time: 0.16s
280
+ ```
281
+
282
+ Export comparison report to JSON:
283
+
284
+ ```bash
285
+ ragdiag compare \
286
+ --pipeline-a examples/dense_pipeline.py \
287
+ --pipeline-b examples/hybrid_pipeline.py \
288
+ --dataset examples/demo_dataset.json \
289
+ --output comparison.json
290
+ ```
291
+
292
+ ---
293
+
294
+ ## Root-Cause Failure Taxonomy
295
+
296
+ RAGDiag classifies every completed query into an explainable 8-category hierarchy:
297
+
298
+ | Category | Severity | Description |
299
+ | :--- | :--- | :--- |
300
+ | **`PASS`** | `info` | Query succeeded across retrieval, context completeness, semantic, and latency checks. |
301
+ | **`WRONG_CHUNK_RETRIEVED`** | `major` | Complete retrieval miss; none of the required context chunks were retrieved in top-$K$. |
302
+ | **`INSUFFICIENT_CONTEXT`** | `warning` | Partial context retrieval; query required multiple chunks but only a subset was retrieved. |
303
+ | **`WRONG_CHUNK_RANK`** | `warning` | All required context was retrieved, but the first relevant chunk ranked lower than threshold (rank > 3). |
304
+ | **`RETRIEVED_BUT_NOT_GROUNDED`** | `major` | Hallucination; context was retrieved, but the LLM made claims unsupported by the chunks. |
305
+ | **`ANSWER_INCORRECT`** | `major` | Context was retrieved and answer was grounded, but contradicted or failed the ground truth. |
306
+ | **`LATENCY_OUTLIER`** | `warning` | Quality passed, but retrieval latency exceeded threshold (default: 1000ms). |
307
+ | **`UNKNOWN`** | `major` | Pipeline crash or unclassifiable execution exception. |
308
+
309
+ ### Decision Precedence
310
+
311
+ 1. **Pipeline Execution Failure** (`status != 'completed'`) $\to$ `UNKNOWN`
312
+ 2. **Total Retrieval Miss** (0 overlap with expected chunks) $\to$ `WRONG_CHUNK_RETRIEVED`
313
+ 3. **Partial Context Retrieval** (subset retrieved) $\to$ `INSUFFICIENT_CONTEXT`
314
+ 4. **All Context Retrieved but Ranked Late** (rank > 3) $\to$ `WRONG_CHUNK_RANK`
315
+ 5. **Hallucination** (`grounded == False`) $\to$ `RETRIEVED_BUT_NOT_GROUNDED`
316
+ 6. **Incorrect Answer** (`answer_correct == False`) $\to$ `ANSWER_INCORRECT`
317
+ 7. **Latency Outlier** (`retrieval_ms > threshold`) $\to$ `LATENCY_OUTLIER`
318
+ 8. **Pass** (all checks passed) $\to$ `PASS`
319
+
320
+ ---
321
+
322
+ ## Comparison Methodology
323
+
324
+ ### Directional Deltas ($\Delta = \text{Pipeline B} - \text{Pipeline A}$)
325
+ - **Quality Metrics**: Positive delta means Pipeline B achieved higher quality.
326
+ - **Latency Metrics**: Positive delta means Pipeline B is slower; negative delta means Pipeline B is faster.
327
+ - **Failure Counts**: Negative delta means Pipeline B reduced failures in that category (improvement).
328
+
329
+ ### Winner Strategy & Trade-Off Detection
330
+ 1. **Quality Priority**: Primary signals are evaluated in priority order:
331
+ $$\text{Recall@}K \longrightarrow \text{MRR} \longrightarrow \text{Groundedness} \longrightarrow \text{Answer Correctness}$$
332
+ Using configurable tolerance $\epsilon = 0.02$.
333
+ 2. **Latency Trade-Off**: Evaluates latency delta against tolerance $\epsilon_{\text{lat}} = 10.0\text{ ms}$.
334
+ 3. **Synthesis**:
335
+ - Quality improves + latency increases $\to$ Declares winner with `"Higher quality <-> higher latency"`.
336
+ - Quality improves + latency improves $\to$ Declares winner with `"Higher quality with improved latency"`.
337
+ - Quality decreases + latency improves $\to$ Declares winner with `"Faster latency at the expense of lower quality"`.
338
+ - Both roughly equal $\to$ Declares `"TIE"`.
339
+
340
+ ---
341
+
342
+ ## Python SDK API
343
+
344
+ ```python
345
+ from ragdiag import Comparator, Evaluator, OpenAIJudge, build_report
346
+ from ragdiag.dataset import load_dataset
347
+ from ragdiag.pipeline import load_pipeline
348
+
349
+ # 1. Load pipeline and dataset
350
+ pipeline_a = load_pipeline("examples/dense_pipeline.py")
351
+ pipeline_b = load_pipeline("examples/hybrid_pipeline.py")
352
+ dataset = load_dataset("examples/demo_dataset.json")
353
+
354
+ # 2. Compare two pipelines
355
+ comparator = Comparator(k=5)
356
+ comparison = comparator.compare(pipeline_a, pipeline_b, dataset)
357
+
358
+ print(f"Overall Winner: {comparison.overall_winner}")
359
+ print(f"Trade-off: {comparison.trade_off}")
360
+ print(f"Recall Delta: {comparison.metric_deltas.recall_at_k:+.2f}")
361
+ print(f"Improved: {comparison.queries_improved} queries")
362
+
363
+ # 3. Export structured JSON
364
+ json_output = comparison.model_dump_json(indent=2)
365
+ ```
366
+
367
+ ---
368
+
369
+ ## Metrics Reference
370
+
371
+ - **Precision@K**: Fraction of top-$K$ retrieved chunks that are relevant:
372
+ $$\text{Precision@}K = \frac{|\text{Relevant Chunks in Top-}K|}{\min(K, |\text{Retrieved Chunks}|)}$$
373
+ - **Recall@K**: Proportion of all ground-truth relevant chunks retrieved in top-$K$:
374
+ $$\text{Recall@}K = \frac{|\text{Relevant Chunks in Top-}K|}{|\text{Total Relevant Chunks}|}$$
375
+ - **Mean Reciprocal Rank (MRR)**: Mean reciprocal rank of the first relevant chunk across queries:
376
+ $$\text{MRR} = \frac{1}{|Q|} \sum_{i=1}^{|Q|} \frac{1}{\text{rank}_i}$$
377
+ - **Answer Correctness**: Semantic equivalence of synthesized answer against expected ground truth.
378
+ - **Groundedness**: Evaluates whether synthesized claims are strictly supported by retrieved context chunks (the expected answer is never used as evidence for groundedness).
379
+ - **Latency Percentiles**: Linear interpolation percentiles (Mean, P50, P95, P99) for retrieval and generation execution.
380
+
381
+ ---
382
+
383
+ ## Limitations
384
+
385
+ - **Python-First**: Pipelines and adapters are authored in Python.
386
+ - **Two-Pipeline Comparison**: MVP currently supports comparing exactly two pipeline configurations (A vs B).
387
+ - **JSON Golden Datasets**: Evaluation datasets are currently loaded from structured JSON files.
388
+ - **LLM Judge Providers**: OpenAI structured outputs are currently supported out of the box; additional model providers can implement the extensible `Judge` interface.
389
+ - **Rule-Based Diagnosis**: The failure diagnosis taxonomy is derived deterministically from captured evidence rather than statistical model inference.
390
+
391
+ ---
392
+
393
+ ## Roadmap
394
+
395
+ Planned for future releases:
396
+ - [ ] Multi-configuration matrix comparison (>2 pipelines)
397
+ - [ ] Automated synthetic golden dataset generation
398
+ - [ ] Additional LLM judge providers (Anthropic, Gemini, local Ollama)
399
+ - [ ] Web dashboard and visual trace inspector
400
+ - [ ] CI/CD automation action for regression gating
401
+ - [ ] Docker containerized runner
402
+
403
+ ---
404
+
405
+ ## License
406
+
407
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.