rag-verifier 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.
- rag_verifier-0.1.0/LICENSE +21 -0
- rag_verifier-0.1.0/PKG-INFO +273 -0
- rag_verifier-0.1.0/README.md +258 -0
- rag_verifier-0.1.0/pyproject.toml +25 -0
- rag_verifier-0.1.0/rag_verifier/__init__.py +27 -0
- rag_verifier-0.1.0/rag_verifier/chunking/__init__.py +3 -0
- rag_verifier-0.1.0/rag_verifier/chunking/detector.py +72 -0
- rag_verifier-0.1.0/rag_verifier/chunking/rules.py +90 -0
- rag_verifier-0.1.0/rag_verifier/chunking/semantic_verifier.py +644 -0
- rag_verifier-0.1.0/rag_verifier/chunking/splitter.py +96 -0
- rag_verifier-0.1.0/rag_verifier/chunking/splitters/__init__.py +143 -0
- rag_verifier-0.1.0/rag_verifier/chunking/splitters/heading_ast.py +84 -0
- rag_verifier-0.1.0/rag_verifier/chunking/splitters/hierarchical_chinese.py +78 -0
- rag_verifier-0.1.0/rag_verifier/chunking/splitters/hierarchical_english.py +117 -0
- rag_verifier-0.1.0/rag_verifier/chunking/splitters/hierarchical_numeric.py +110 -0
- rag_verifier-0.1.0/rag_verifier/chunking/splitters/paragraph_fallback.py +91 -0
- rag_verifier-0.1.0/rag_verifier/chunking/template_verifier.py +155 -0
- rag_verifier-0.1.0/rag_verifier/chunking/verifier.py +638 -0
- rag_verifier-0.1.0/rag_verifier/citation/__init__.py +0 -0
- rag_verifier-0.1.0/rag_verifier/citation/verifier.py +250 -0
- rag_verifier-0.1.0/rag_verifier/cli.py +107 -0
- rag_verifier-0.1.0/rag_verifier/integrations/__init__.py +4 -0
- rag_verifier-0.1.0/rag_verifier/integrations/langchain.py +105 -0
- rag_verifier-0.1.0/rag_verifier/models.py +26 -0
- rag_verifier-0.1.0/rag_verifier/parsing/__init__.py +18 -0
- rag_verifier-0.1.0/rag_verifier/parsing/adapter.py +117 -0
- rag_verifier-0.1.0/rag_verifier/parsing/base.py +185 -0
- rag_verifier-0.1.0/rag_verifier/parsing/docling_parser.py +223 -0
- rag_verifier-0.1.0/rag_verifier/parsing/html_parser.py +171 -0
- rag_verifier-0.1.0/rag_verifier/parsing/level_inference.py +120 -0
- rag_verifier-0.1.0/rag_verifier/parsing/level_redetection.py +88 -0
- rag_verifier-0.1.0/rag_verifier/parsing/markdown_parser.py +140 -0
- rag_verifier-0.1.0/rag_verifier/parsing/models.py +75 -0
- rag_verifier-0.1.0/rag_verifier/parsing/pymupdf_parser.py +208 -0
- rag_verifier-0.1.0/rag_verifier/parsing/quality.py +178 -0
- rag_verifier-0.1.0/rag_verifier/parsing/text_clean.py +37 -0
- rag_verifier-0.1.0/rag_verifier/parsing/txt_parser.py +44 -0
- rag_verifier-0.1.0/rag_verifier/retrieval/__init__.py +0 -0
- rag_verifier-0.1.0/rag_verifier/retrieval/bm25.py +61 -0
- rag_verifier-0.1.0/rag_verifier/retrieval/corpus.py +125 -0
- rag_verifier-0.1.0/rag_verifier/retrieval/generate.py +52 -0
- rag_verifier-0.1.0/rag_verifier/retrieval/index.py +204 -0
- rag_verifier-0.1.0/rag_verifier/retrieval/ingest.py +208 -0
- rag_verifier-0.1.0/rag_verifier/retrieval/llm_rerank.py +61 -0
- rag_verifier-0.1.0/rag_verifier/retrieval/query_rewrite.py +139 -0
- rag_verifier-0.1.0/rag_verifier/retrieval/recall.py +136 -0
- rag_verifier-0.1.0/rag_verifier/retrieval/rerank.py +42 -0
- rag_verifier-0.1.0/rag_verifier/verify_api.py +110 -0
- rag_verifier-0.1.0/rag_verifier.egg-info/PKG-INFO +273 -0
- rag_verifier-0.1.0/rag_verifier.egg-info/SOURCES.txt +53 -0
- rag_verifier-0.1.0/rag_verifier.egg-info/dependency_links.txt +1 -0
- rag_verifier-0.1.0/rag_verifier.egg-info/entry_points.txt +2 -0
- rag_verifier-0.1.0/rag_verifier.egg-info/requires.txt +8 -0
- rag_verifier-0.1.0/rag_verifier.egg-info/top_level.txt +1 -0
- rag_verifier-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 hlc0000
|
|
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.
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rag-verifier
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Chunk-level quality verification for RAG ingestion: independence, completeness, traceability, semantic independence, template completeness
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Provides-Extra: parse
|
|
9
|
+
Requires-Dist: pymupdf<2.0,>=1.23; extra == "parse"
|
|
10
|
+
Requires-Dist: marko>=2.0; extra == "parse"
|
|
11
|
+
Requires-Dist: lxml>=4.9; extra == "parse"
|
|
12
|
+
Provides-Extra: langchain
|
|
13
|
+
Requires-Dist: langchain-core>=0.2; extra == "langchain"
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# rag-verifier
|
|
17
|
+
|
|
18
|
+
[](LICENSE)
|
|
19
|
+
[](https://github.com/hlc0000/rag-verifier/actions/workflows/ci.yml)
|
|
20
|
+
[](pyproject.toml)
|
|
21
|
+
[](#installation)
|
|
22
|
+
|
|
23
|
+
🎯 **[Online Demo](https://rag-verifier.streamlit.app)** · 📦 `pip install rag-verifier`
|
|
24
|
+
|
|
25
|
+
> **PyPI publishing is in progress.** Until then, install from GitHub — see [Installation](#installation).
|
|
26
|
+
|
|
27
|
+

|
|
28
|
+
|
|
29
|
+
Most RAG failures aren't model failures — they're evidence failures. A clause gets split mid-condition: `If the Client breaches any material obligation,` goes into one vector, `the Contractor may terminate this Agreement.` into another. Both chunks look fine to your retriever; neither is answerable on its own. And when the LLM finally answers, it quotes a citation that *sounds* right — with the trigger condition quietly cut away.
|
|
30
|
+
|
|
31
|
+
rag-verifier is a quality gate for that evidence layer. It checks every chunk **before** it enters your vector store, and every citation **before** it reaches your user.
|
|
32
|
+
|
|
33
|
+
**Niche:** `chunkvet` audits chunk geometry. `ContextDoctor` does static analysis. `rag-verifier` targets **semantic integrity before ingestion** + **citation faithfulness after generation**. Rule layer is zero-dependency; LLM fallback only.
|
|
34
|
+
|
|
35
|
+
## One-line CLI
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
rag-verifier check "Therefore the Contractor may terminate this Agreement." \
|
|
39
|
+
--prev "If the Client breaches any material obligation,"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
[FAIL] overall = fail
|
|
44
|
+
- independence: fail (Starts with a dependent connective: 'Therefore')
|
|
45
|
+
- completeness: pass
|
|
46
|
+
- traceability: uncertain (No source text provided)
|
|
47
|
+
- semantic_independence: uncertain (No LLM callback; semantic layer skipped)
|
|
48
|
+
- template_completeness: pass
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Exit code `0` = pass, `1` = fail or uncertain. Add `--json` for machine-readable output, `--llm` to enable the semantic layer.
|
|
52
|
+
|
|
53
|
+
## What it checks
|
|
54
|
+
|
|
55
|
+
Five checks run sequentially on each chunk. The rule layer is **fast** (pure regex) and catches structural defects deterministically — measured **false-positive rate on our evaluation set (1,388 chunks / 20 real-world documents)**. The LLM semantic layer kicks in only when the rule layer says "looks OK" but the chunk might still be semantically broken.
|
|
56
|
+
|
|
57
|
+
| Check | Rule layer (fast, zero LLM cost) | LLM semantic layer (fallback) |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| `independence` | dependent connective at start (`However`/`Therefore`/`Accordingly`), incomplete conditional ending (`If`/`When`/`Unless`), fragments, empty chunks, table-continuation markers | trigger condition cut away, elided subject |
|
|
60
|
+
| `completeness` | conditional pairings, concessive pairings, dependent connective with word boundary | — |
|
|
61
|
+
| `traceability` | unresolved anaphora (`this`/`that`/`it` with no in-chunk referent), cross-chunk substantive references (`as shown in Figure 2`) | — |
|
|
62
|
+
| `template_completeness` | bracketed placeholders (`[yyyy]`), underscore blanks, quantity/date/signature slots | — |
|
|
63
|
+
| `semantic_independence` | — | full independent judgment with the complete document as context |
|
|
64
|
+
|
|
65
|
+
The LLM layer is **fail-open**: if the API is down, the chunk gets `uncertain`, **never** silently reclassified as `supported` or `insufficient`.
|
|
66
|
+
|
|
67
|
+
## Why this exists: pure-rules vs pure-LLM vs this
|
|
68
|
+
|
|
69
|
+
| | Pure regex rules | Pure LLM judge | rag-verifier (hybrid) |
|
|
70
|
+
|---|---|---|---|
|
|
71
|
+
| **Cost per chunk** | $0 | $0.001–$0.01 | $0 for rule-pass; LLM only on rule-ambiguous |
|
|
72
|
+
| **Latency** | <1 ms | 500–2000 ms | <1 ms for the 95% rule-pass cases |
|
|
73
|
+
| **Consistency** (same chunk, same verdict every run) | ✅ deterministic | ❌ sampling variance, prompt drift | ✅ deterministic for rule layer; LLM only where needed |
|
|
74
|
+
| **Fail-open on API failure** | N/A | ❌ often silently passes | ✅ returns `uncertain`, never fakes `pass` |
|
|
75
|
+
| **Catches subtle semantic breaks** | ❌ only structural | ✅ but expensive & inconsistent | ✅ LLM layer for the hard cases |
|
|
76
|
+
|
|
77
|
+
The rule layer does the cheap, deterministic work. The LLM layer exists only for the cases rules can't decide.
|
|
78
|
+
|
|
79
|
+
## How it fits in the RAG quality stack
|
|
80
|
+
|
|
81
|
+
This is one layer of a healthy stack, designed to compose with the others:
|
|
82
|
+
|
|
83
|
+
- **Structure-aware chunkers** (e.g. chunkvet, LangChain text splitters) decide *where to cut*. rag-verifier runs after any chunker — yours, theirs, LangChain's — and gates *what may pass*.
|
|
84
|
+
- **Citation-side checkers** (e.g. spancheck) verify that quoted spans exist. We go one step further into *faithfulness*: a verbatim quote can still be `insufficient` if its trigger condition was cut away.
|
|
85
|
+
- **Static context analyzers** (e.g. ContextDoctor) lint your context before retrieval. We add a fail-open LLM semantic layer on top of deterministic rules, in both Chinese and English.
|
|
86
|
+
|
|
87
|
+
Parse with anything, chunk with anything, gate with rag-verifier.
|
|
88
|
+
|
|
89
|
+
## Installation
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
pip install rag-verifier
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
> **Status:** PyPI upload is being finalized. The command above will work once published. Meanwhile, install from GitHub:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
pip install git+https://github.com/hlc0000/rag-verifier.git
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The core verification layer is **zero-dependency** (pure stdlib — the LLM calls go through `urllib`). Optional extras:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
pip install "rag-verifier[parse]" # PDF/DOCX/Markdown/HTML parsing
|
|
105
|
+
pip install "rag-verifier[langchain]" # VerifierTransformer for LangChain pipelines
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
<details><summary>Development install (editable)</summary>
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
git clone https://github.com/hlc0000/rag-verifier.git
|
|
112
|
+
cd rag-verifier
|
|
113
|
+
pip install -e ".[parse,langchain]"
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
</details>
|
|
117
|
+
|
|
118
|
+
## Usage
|
|
119
|
+
|
|
120
|
+
### 1. Command line
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
# Single chunk (rule layer only)
|
|
124
|
+
rag-verifier check "Therefore the Contractor may terminate." --prev "If the Client breaches,"
|
|
125
|
+
|
|
126
|
+
# Read from stdin
|
|
127
|
+
echo "However, the agreement is terminated." | rag-verifier check -
|
|
128
|
+
|
|
129
|
+
# Citation faithfulness
|
|
130
|
+
rag-verifier citation \
|
|
131
|
+
--claim "Apache License requires copyright headers." \
|
|
132
|
+
--citation "Copyright [yyyy] [name of copyright owner]" \
|
|
133
|
+
--source-text "$(cat apache2.txt)"
|
|
134
|
+
|
|
135
|
+
# JSON output for piping
|
|
136
|
+
rag-verifier check "..." --json | jq '.overall'
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### 2. Chunk quality gate (Python API)
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from rag_verifier import verify_chunk
|
|
143
|
+
|
|
144
|
+
result = verify_chunk(
|
|
145
|
+
"Therefore the Contractor has the right to claim damages from the Client.",
|
|
146
|
+
prev_chunk="If the Client breaches any material obligation under this Agreement,",
|
|
147
|
+
)
|
|
148
|
+
# result["overall"].value == "fail"
|
|
149
|
+
# result["checks"]["independence"]["reason"] == "Starts with a dependent connective: 'Therefore'"
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### 3. Citation faithfulness — does the citation actually support the claim?
|
|
153
|
+
|
|
154
|
+
People search for "RAG citation verification" more than "chunk quality gate" — this is the check that catches AI *appearing* to cite evidence while quietly omitting the premise.
|
|
155
|
+
|
|
156
|
+
**Three-way verdict** (plus `uncertain` when the LLM is unavailable):
|
|
157
|
+
|
|
158
|
+
- `supported` — citation both entails the claim **and** stands alone without misleading
|
|
159
|
+
- `unsupported` — citation contradicts the claim → **regenerate the answer**
|
|
160
|
+
- `insufficient` — citation is incomplete or off-topic (e.g. only the result clause of a conditional, trigger condition cut away) → **re-retrieve**
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
from rag_verifier import verify_citation
|
|
164
|
+
|
|
165
|
+
result = verify_citation(
|
|
166
|
+
claim="Apache License 2.0 requires copyright headers.",
|
|
167
|
+
citation="Copyright [yyyy] [name of copyright owner]",
|
|
168
|
+
source_text="... full Apache 2.0 license text ...",
|
|
169
|
+
)
|
|
170
|
+
# result["status"] -> "insufficient"
|
|
171
|
+
# result["reason"] -> why the citation doesn't support the claim
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
A **deterministic gate runs before any LLM call**: if the citation is located verbatim in the source and its preceding context shows a truncated conditional (the citation kept only the result clause), the verdict is `insufficient` immediately — no LLM variance, no API cost. This gate alone lifted truncated-conditional detection from 55% to 95% on our evaluation set.
|
|
175
|
+
|
|
176
|
+
### 4. With the LLM semantic layer
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
export LLM_API_KEY=sk-xxx
|
|
180
|
+
export LLM_BASE_URL=https://api.deepseek.com/v1 # any OpenAI-compatible endpoint
|
|
181
|
+
export LLM_MODEL=deepseek-chat
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
from rag_verifier import verify_chunk, builtin_llm
|
|
186
|
+
|
|
187
|
+
result = verify_chunk(
|
|
188
|
+
"Therefore Party A has the right to claim compensation from Party B.",
|
|
189
|
+
prev_chunk="If Party B breaches the confidentiality obligation,",
|
|
190
|
+
full_doc="If Party B breaches the confidentiality obligation, therefore Party A ...",
|
|
191
|
+
llm_callback=builtin_llm, # inject any LLM callable; without it the semantic check returns uncertain
|
|
192
|
+
)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### 5. LangChain integration (5 lines)
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
from langchain_text_splitters import RecursiveCharacterTextSplitter
|
|
199
|
+
from rag_verifier.integrations.langchain import VerifierTransformer
|
|
200
|
+
|
|
201
|
+
splitter = RecursiveCharacterTextSplitter(chunk_size=500)
|
|
202
|
+
chunks = splitter.split_documents(docs)
|
|
203
|
+
|
|
204
|
+
gate = VerifierTransformer() # rule layer, zero LLM calls
|
|
205
|
+
clean_chunks = gate.transform_documents(chunks) # drops FAIL chunks; marks UNCERTAIN in metadata
|
|
206
|
+
# clean_chunks -> vectorstore
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Enable the semantic layer by injecting an LLM callback:
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
from rag_verifier import builtin_llm
|
|
213
|
+
gate = VerifierTransformer(llm_callback=builtin_llm)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### 6. Full pipeline: parse → split → verify
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
from rag_verifier.parsing import parse_document
|
|
220
|
+
from rag_verifier.chunking import split_document
|
|
221
|
+
from rag_verifier import verify_chunk
|
|
222
|
+
|
|
223
|
+
doc = parse_document("contract.pdf") # StructuredDocument
|
|
224
|
+
chunks = split_document(doc) # list[dict]: {"id", "card", "source_text", ...}
|
|
225
|
+
|
|
226
|
+
prev = ""
|
|
227
|
+
for item in chunks:
|
|
228
|
+
content = item["card"]["content"]
|
|
229
|
+
result = verify_chunk(content, prev_chunk=prev, source_text=item.get("source_text", ""))
|
|
230
|
+
if result["overall"].value == "fail":
|
|
231
|
+
failed = [f"{k}: {v['reason']}" for k, v in result["checks"].items() if v["status"] == "fail"]
|
|
232
|
+
print(f"DROP {item['id']}: {'; '.join(failed)}")
|
|
233
|
+
prev = content
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Bilingual parity (zh / en) — enforced in CI
|
|
237
|
+
|
|
238
|
+
Every rule has a Chinese and an English copy, dispatched automatically by `detect_language()` (CJK ratio ≥ 15% → zh). The prompt templates, the `reason` strings, and the three citation verdicts all come in matching pairs. Run an English document → English output. Run a Chinese document → Chinese output. Never mixed.
|
|
239
|
+
|
|
240
|
+
This is a hard guarantee, not a promise: `scripts/rule_parity_check.py` runs the **positive and negative examples for every rule in both languages** and **fails the build** if any positive case is missed or any negative case is hit by accident. English coverage is not "good enough" — it is byte-for-byte symmetric with Chinese.
|
|
241
|
+
|
|
242
|
+
## Limitations — not a silver bullet
|
|
243
|
+
|
|
244
|
+
rag-verifier is a quality gate, not a repair tool. It tells you which chunks are broken; it does not fix them.
|
|
245
|
+
|
|
246
|
+
- **Does not repair chunks.** A failed chunk needs to be re-split (different chunker, larger overlap) or merged with its neighbor — that's your pipeline's job.
|
|
247
|
+
- **Does not evaluate retrieval quality.** It doesn't measure recall@k or whether the right chunk was retrieved. It only checks whether a chunk, once retrieved, is self-sufficient.
|
|
248
|
+
- **Does not support scanned PDFs.** No OCR; a scanned PDF yields an empty text layer. Use an OCR pipeline first.
|
|
249
|
+
- **Does not support RTF.** RTF headings rarely carry style information, so heading levels are lost during parsing.
|
|
250
|
+
- **Chinese-question-on-English-document is not supported.** The verifier detects the document's language and applies matching rules; cross-language question answering is out of scope.
|
|
251
|
+
- **Rule layer catches structural defects, not factual errors.** A chunk can be perfectly self-contained and still contain wrong information — that's the LLM / citation check's job, not the chunk gate's.
|
|
252
|
+
|
|
253
|
+
## Supported formats
|
|
254
|
+
|
|
255
|
+
Text-layer PDF, DOCX, TXT, Markdown, HTML. Not supported: RTF, scanned PDF (see Limitations above).
|
|
256
|
+
|
|
257
|
+
## Tests
|
|
258
|
+
|
|
259
|
+
```bash
|
|
260
|
+
python scripts/rule_parity_check.py # zh/en rule parity — must pass (runs in CI)
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Roadmap & Contributing
|
|
264
|
+
|
|
265
|
+
- **LlamaIndex integration** ( `VerifierTransformer` for LlamaIndex nodes)
|
|
266
|
+
- **CLI batch mode** (verify a JSONL of chunks in one command)
|
|
267
|
+
- **More document types** for the parsing extra
|
|
268
|
+
|
|
269
|
+
Found a chunk that *should* be caught but isn't? Or a false positive that broke your pipeline? [Open an issue](https://github.com/hlc0000/rag-verifier/issues) with the chunk text and the expected verdict. Rule-layer fixes are deterministic and permanent — once a pattern is covered, it stays covered.
|
|
270
|
+
|
|
271
|
+
## License
|
|
272
|
+
|
|
273
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# rag-verifier
|
|
2
|
+
|
|
3
|
+
[](LICENSE)
|
|
4
|
+
[](https://github.com/hlc0000/rag-verifier/actions/workflows/ci.yml)
|
|
5
|
+
[](pyproject.toml)
|
|
6
|
+
[](#installation)
|
|
7
|
+
|
|
8
|
+
🎯 **[Online Demo](https://rag-verifier.streamlit.app)** · 📦 `pip install rag-verifier`
|
|
9
|
+
|
|
10
|
+
> **PyPI publishing is in progress.** Until then, install from GitHub — see [Installation](#installation).
|
|
11
|
+
|
|
12
|
+

|
|
13
|
+
|
|
14
|
+
Most RAG failures aren't model failures — they're evidence failures. A clause gets split mid-condition: `If the Client breaches any material obligation,` goes into one vector, `the Contractor may terminate this Agreement.` into another. Both chunks look fine to your retriever; neither is answerable on its own. And when the LLM finally answers, it quotes a citation that *sounds* right — with the trigger condition quietly cut away.
|
|
15
|
+
|
|
16
|
+
rag-verifier is a quality gate for that evidence layer. It checks every chunk **before** it enters your vector store, and every citation **before** it reaches your user.
|
|
17
|
+
|
|
18
|
+
**Niche:** `chunkvet` audits chunk geometry. `ContextDoctor` does static analysis. `rag-verifier` targets **semantic integrity before ingestion** + **citation faithfulness after generation**. Rule layer is zero-dependency; LLM fallback only.
|
|
19
|
+
|
|
20
|
+
## One-line CLI
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
rag-verifier check "Therefore the Contractor may terminate this Agreement." \
|
|
24
|
+
--prev "If the Client breaches any material obligation,"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
[FAIL] overall = fail
|
|
29
|
+
- independence: fail (Starts with a dependent connective: 'Therefore')
|
|
30
|
+
- completeness: pass
|
|
31
|
+
- traceability: uncertain (No source text provided)
|
|
32
|
+
- semantic_independence: uncertain (No LLM callback; semantic layer skipped)
|
|
33
|
+
- template_completeness: pass
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Exit code `0` = pass, `1` = fail or uncertain. Add `--json` for machine-readable output, `--llm` to enable the semantic layer.
|
|
37
|
+
|
|
38
|
+
## What it checks
|
|
39
|
+
|
|
40
|
+
Five checks run sequentially on each chunk. The rule layer is **fast** (pure regex) and catches structural defects deterministically — measured **false-positive rate on our evaluation set (1,388 chunks / 20 real-world documents)**. The LLM semantic layer kicks in only when the rule layer says "looks OK" but the chunk might still be semantically broken.
|
|
41
|
+
|
|
42
|
+
| Check | Rule layer (fast, zero LLM cost) | LLM semantic layer (fallback) |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| `independence` | dependent connective at start (`However`/`Therefore`/`Accordingly`), incomplete conditional ending (`If`/`When`/`Unless`), fragments, empty chunks, table-continuation markers | trigger condition cut away, elided subject |
|
|
45
|
+
| `completeness` | conditional pairings, concessive pairings, dependent connective with word boundary | — |
|
|
46
|
+
| `traceability` | unresolved anaphora (`this`/`that`/`it` with no in-chunk referent), cross-chunk substantive references (`as shown in Figure 2`) | — |
|
|
47
|
+
| `template_completeness` | bracketed placeholders (`[yyyy]`), underscore blanks, quantity/date/signature slots | — |
|
|
48
|
+
| `semantic_independence` | — | full independent judgment with the complete document as context |
|
|
49
|
+
|
|
50
|
+
The LLM layer is **fail-open**: if the API is down, the chunk gets `uncertain`, **never** silently reclassified as `supported` or `insufficient`.
|
|
51
|
+
|
|
52
|
+
## Why this exists: pure-rules vs pure-LLM vs this
|
|
53
|
+
|
|
54
|
+
| | Pure regex rules | Pure LLM judge | rag-verifier (hybrid) |
|
|
55
|
+
|---|---|---|---|
|
|
56
|
+
| **Cost per chunk** | $0 | $0.001–$0.01 | $0 for rule-pass; LLM only on rule-ambiguous |
|
|
57
|
+
| **Latency** | <1 ms | 500–2000 ms | <1 ms for the 95% rule-pass cases |
|
|
58
|
+
| **Consistency** (same chunk, same verdict every run) | ✅ deterministic | ❌ sampling variance, prompt drift | ✅ deterministic for rule layer; LLM only where needed |
|
|
59
|
+
| **Fail-open on API failure** | N/A | ❌ often silently passes | ✅ returns `uncertain`, never fakes `pass` |
|
|
60
|
+
| **Catches subtle semantic breaks** | ❌ only structural | ✅ but expensive & inconsistent | ✅ LLM layer for the hard cases |
|
|
61
|
+
|
|
62
|
+
The rule layer does the cheap, deterministic work. The LLM layer exists only for the cases rules can't decide.
|
|
63
|
+
|
|
64
|
+
## How it fits in the RAG quality stack
|
|
65
|
+
|
|
66
|
+
This is one layer of a healthy stack, designed to compose with the others:
|
|
67
|
+
|
|
68
|
+
- **Structure-aware chunkers** (e.g. chunkvet, LangChain text splitters) decide *where to cut*. rag-verifier runs after any chunker — yours, theirs, LangChain's — and gates *what may pass*.
|
|
69
|
+
- **Citation-side checkers** (e.g. spancheck) verify that quoted spans exist. We go one step further into *faithfulness*: a verbatim quote can still be `insufficient` if its trigger condition was cut away.
|
|
70
|
+
- **Static context analyzers** (e.g. ContextDoctor) lint your context before retrieval. We add a fail-open LLM semantic layer on top of deterministic rules, in both Chinese and English.
|
|
71
|
+
|
|
72
|
+
Parse with anything, chunk with anything, gate with rag-verifier.
|
|
73
|
+
|
|
74
|
+
## Installation
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pip install rag-verifier
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
> **Status:** PyPI upload is being finalized. The command above will work once published. Meanwhile, install from GitHub:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
pip install git+https://github.com/hlc0000/rag-verifier.git
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The core verification layer is **zero-dependency** (pure stdlib — the LLM calls go through `urllib`). Optional extras:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
pip install "rag-verifier[parse]" # PDF/DOCX/Markdown/HTML parsing
|
|
90
|
+
pip install "rag-verifier[langchain]" # VerifierTransformer for LangChain pipelines
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
<details><summary>Development install (editable)</summary>
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
git clone https://github.com/hlc0000/rag-verifier.git
|
|
97
|
+
cd rag-verifier
|
|
98
|
+
pip install -e ".[parse,langchain]"
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
</details>
|
|
102
|
+
|
|
103
|
+
## Usage
|
|
104
|
+
|
|
105
|
+
### 1. Command line
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
# Single chunk (rule layer only)
|
|
109
|
+
rag-verifier check "Therefore the Contractor may terminate." --prev "If the Client breaches,"
|
|
110
|
+
|
|
111
|
+
# Read from stdin
|
|
112
|
+
echo "However, the agreement is terminated." | rag-verifier check -
|
|
113
|
+
|
|
114
|
+
# Citation faithfulness
|
|
115
|
+
rag-verifier citation \
|
|
116
|
+
--claim "Apache License requires copyright headers." \
|
|
117
|
+
--citation "Copyright [yyyy] [name of copyright owner]" \
|
|
118
|
+
--source-text "$(cat apache2.txt)"
|
|
119
|
+
|
|
120
|
+
# JSON output for piping
|
|
121
|
+
rag-verifier check "..." --json | jq '.overall'
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### 2. Chunk quality gate (Python API)
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from rag_verifier import verify_chunk
|
|
128
|
+
|
|
129
|
+
result = verify_chunk(
|
|
130
|
+
"Therefore the Contractor has the right to claim damages from the Client.",
|
|
131
|
+
prev_chunk="If the Client breaches any material obligation under this Agreement,",
|
|
132
|
+
)
|
|
133
|
+
# result["overall"].value == "fail"
|
|
134
|
+
# result["checks"]["independence"]["reason"] == "Starts with a dependent connective: 'Therefore'"
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### 3. Citation faithfulness — does the citation actually support the claim?
|
|
138
|
+
|
|
139
|
+
People search for "RAG citation verification" more than "chunk quality gate" — this is the check that catches AI *appearing* to cite evidence while quietly omitting the premise.
|
|
140
|
+
|
|
141
|
+
**Three-way verdict** (plus `uncertain` when the LLM is unavailable):
|
|
142
|
+
|
|
143
|
+
- `supported` — citation both entails the claim **and** stands alone without misleading
|
|
144
|
+
- `unsupported` — citation contradicts the claim → **regenerate the answer**
|
|
145
|
+
- `insufficient` — citation is incomplete or off-topic (e.g. only the result clause of a conditional, trigger condition cut away) → **re-retrieve**
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
from rag_verifier import verify_citation
|
|
149
|
+
|
|
150
|
+
result = verify_citation(
|
|
151
|
+
claim="Apache License 2.0 requires copyright headers.",
|
|
152
|
+
citation="Copyright [yyyy] [name of copyright owner]",
|
|
153
|
+
source_text="... full Apache 2.0 license text ...",
|
|
154
|
+
)
|
|
155
|
+
# result["status"] -> "insufficient"
|
|
156
|
+
# result["reason"] -> why the citation doesn't support the claim
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
A **deterministic gate runs before any LLM call**: if the citation is located verbatim in the source and its preceding context shows a truncated conditional (the citation kept only the result clause), the verdict is `insufficient` immediately — no LLM variance, no API cost. This gate alone lifted truncated-conditional detection from 55% to 95% on our evaluation set.
|
|
160
|
+
|
|
161
|
+
### 4. With the LLM semantic layer
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
export LLM_API_KEY=sk-xxx
|
|
165
|
+
export LLM_BASE_URL=https://api.deepseek.com/v1 # any OpenAI-compatible endpoint
|
|
166
|
+
export LLM_MODEL=deepseek-chat
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
from rag_verifier import verify_chunk, builtin_llm
|
|
171
|
+
|
|
172
|
+
result = verify_chunk(
|
|
173
|
+
"Therefore Party A has the right to claim compensation from Party B.",
|
|
174
|
+
prev_chunk="If Party B breaches the confidentiality obligation,",
|
|
175
|
+
full_doc="If Party B breaches the confidentiality obligation, therefore Party A ...",
|
|
176
|
+
llm_callback=builtin_llm, # inject any LLM callable; without it the semantic check returns uncertain
|
|
177
|
+
)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### 5. LangChain integration (5 lines)
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
from langchain_text_splitters import RecursiveCharacterTextSplitter
|
|
184
|
+
from rag_verifier.integrations.langchain import VerifierTransformer
|
|
185
|
+
|
|
186
|
+
splitter = RecursiveCharacterTextSplitter(chunk_size=500)
|
|
187
|
+
chunks = splitter.split_documents(docs)
|
|
188
|
+
|
|
189
|
+
gate = VerifierTransformer() # rule layer, zero LLM calls
|
|
190
|
+
clean_chunks = gate.transform_documents(chunks) # drops FAIL chunks; marks UNCERTAIN in metadata
|
|
191
|
+
# clean_chunks -> vectorstore
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Enable the semantic layer by injecting an LLM callback:
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
from rag_verifier import builtin_llm
|
|
198
|
+
gate = VerifierTransformer(llm_callback=builtin_llm)
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### 6. Full pipeline: parse → split → verify
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
from rag_verifier.parsing import parse_document
|
|
205
|
+
from rag_verifier.chunking import split_document
|
|
206
|
+
from rag_verifier import verify_chunk
|
|
207
|
+
|
|
208
|
+
doc = parse_document("contract.pdf") # StructuredDocument
|
|
209
|
+
chunks = split_document(doc) # list[dict]: {"id", "card", "source_text", ...}
|
|
210
|
+
|
|
211
|
+
prev = ""
|
|
212
|
+
for item in chunks:
|
|
213
|
+
content = item["card"]["content"]
|
|
214
|
+
result = verify_chunk(content, prev_chunk=prev, source_text=item.get("source_text", ""))
|
|
215
|
+
if result["overall"].value == "fail":
|
|
216
|
+
failed = [f"{k}: {v['reason']}" for k, v in result["checks"].items() if v["status"] == "fail"]
|
|
217
|
+
print(f"DROP {item['id']}: {'; '.join(failed)}")
|
|
218
|
+
prev = content
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Bilingual parity (zh / en) — enforced in CI
|
|
222
|
+
|
|
223
|
+
Every rule has a Chinese and an English copy, dispatched automatically by `detect_language()` (CJK ratio ≥ 15% → zh). The prompt templates, the `reason` strings, and the three citation verdicts all come in matching pairs. Run an English document → English output. Run a Chinese document → Chinese output. Never mixed.
|
|
224
|
+
|
|
225
|
+
This is a hard guarantee, not a promise: `scripts/rule_parity_check.py` runs the **positive and negative examples for every rule in both languages** and **fails the build** if any positive case is missed or any negative case is hit by accident. English coverage is not "good enough" — it is byte-for-byte symmetric with Chinese.
|
|
226
|
+
|
|
227
|
+
## Limitations — not a silver bullet
|
|
228
|
+
|
|
229
|
+
rag-verifier is a quality gate, not a repair tool. It tells you which chunks are broken; it does not fix them.
|
|
230
|
+
|
|
231
|
+
- **Does not repair chunks.** A failed chunk needs to be re-split (different chunker, larger overlap) or merged with its neighbor — that's your pipeline's job.
|
|
232
|
+
- **Does not evaluate retrieval quality.** It doesn't measure recall@k or whether the right chunk was retrieved. It only checks whether a chunk, once retrieved, is self-sufficient.
|
|
233
|
+
- **Does not support scanned PDFs.** No OCR; a scanned PDF yields an empty text layer. Use an OCR pipeline first.
|
|
234
|
+
- **Does not support RTF.** RTF headings rarely carry style information, so heading levels are lost during parsing.
|
|
235
|
+
- **Chinese-question-on-English-document is not supported.** The verifier detects the document's language and applies matching rules; cross-language question answering is out of scope.
|
|
236
|
+
- **Rule layer catches structural defects, not factual errors.** A chunk can be perfectly self-contained and still contain wrong information — that's the LLM / citation check's job, not the chunk gate's.
|
|
237
|
+
|
|
238
|
+
## Supported formats
|
|
239
|
+
|
|
240
|
+
Text-layer PDF, DOCX, TXT, Markdown, HTML. Not supported: RTF, scanned PDF (see Limitations above).
|
|
241
|
+
|
|
242
|
+
## Tests
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
python scripts/rule_parity_check.py # zh/en rule parity — must pass (runs in CI)
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## Roadmap & Contributing
|
|
249
|
+
|
|
250
|
+
- **LlamaIndex integration** ( `VerifierTransformer` for LlamaIndex nodes)
|
|
251
|
+
- **CLI batch mode** (verify a JSONL of chunks in one command)
|
|
252
|
+
- **More document types** for the parsing extra
|
|
253
|
+
|
|
254
|
+
Found a chunk that *should* be caught but isn't? Or a false positive that broke your pipeline? [Open an issue](https://github.com/hlc0000/rag-verifier/issues) with the chunk text and the expected verdict. Rule-layer fixes are deterministic and permanent — once a pattern is covered, it stays covered.
|
|
255
|
+
|
|
256
|
+
## License
|
|
257
|
+
|
|
258
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "rag-verifier"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Chunk-level quality verification for RAG ingestion: independence, completeness, traceability, semantic independence, template completeness"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
# 验证层只用标准库;解析/检索的依赖由各自模块按需引入(docling 走独立 venv)。
|
|
12
|
+
dependencies = []
|
|
13
|
+
|
|
14
|
+
[project.scripts]
|
|
15
|
+
rag-verifier = "rag_verifier.cli:main"
|
|
16
|
+
|
|
17
|
+
[project.optional-dependencies]
|
|
18
|
+
# pymupdf 2.0 起不再提供 Intel Mac wheel;<2.0 保证全平台可装,
|
|
19
|
+
# 且解析层代码是针对 1.x API 开发验证的
|
|
20
|
+
parse = ["pymupdf>=1.23,<2.0", "marko>=2.0", "lxml>=4.9"]
|
|
21
|
+
# LangChain 集成:提供 VerifierTransformer,可直接接入 LCEL 流水线
|
|
22
|
+
langchain = ["langchain-core>=0.2"]
|
|
23
|
+
|
|
24
|
+
[tool.setuptools.packages.find]
|
|
25
|
+
include = ["rag_verifier*"]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""rag-verifier 对外包入口。
|
|
2
|
+
|
|
3
|
+
from rag_verifier import verify_chunks
|
|
4
|
+
|
|
5
|
+
只依赖验证层(不导入 docling/pymupdf/检索层),可作为独立组件嵌入任意
|
|
6
|
+
Python 流水线:任意解析器产出的块 → 五关质量验证。
|
|
7
|
+
"""
|
|
8
|
+
from rag_verifier.chunking.rules import CHECKS, RuleContext, detect_language, list_rules, register_rule
|
|
9
|
+
from rag_verifier.chunking.semantic_verifier import builtin_llm
|
|
10
|
+
from rag_verifier.citation.verifier import verify_citation
|
|
11
|
+
from rag_verifier.models import EvidenceCard, VerificationResult, VerificationStatus
|
|
12
|
+
from rag_verifier.verify_api import verify_chunk, verify_chunks
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"verify_chunk",
|
|
16
|
+
"verify_chunks",
|
|
17
|
+
"verify_citation",
|
|
18
|
+
"EvidenceCard",
|
|
19
|
+
"VerificationResult",
|
|
20
|
+
"VerificationStatus",
|
|
21
|
+
"builtin_llm",
|
|
22
|
+
"register_rule",
|
|
23
|
+
"detect_language",
|
|
24
|
+
"list_rules",
|
|
25
|
+
"RuleContext",
|
|
26
|
+
"CHECKS",
|
|
27
|
+
]
|