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.
Files changed (55) hide show
  1. rag_verifier-0.1.0/LICENSE +21 -0
  2. rag_verifier-0.1.0/PKG-INFO +273 -0
  3. rag_verifier-0.1.0/README.md +258 -0
  4. rag_verifier-0.1.0/pyproject.toml +25 -0
  5. rag_verifier-0.1.0/rag_verifier/__init__.py +27 -0
  6. rag_verifier-0.1.0/rag_verifier/chunking/__init__.py +3 -0
  7. rag_verifier-0.1.0/rag_verifier/chunking/detector.py +72 -0
  8. rag_verifier-0.1.0/rag_verifier/chunking/rules.py +90 -0
  9. rag_verifier-0.1.0/rag_verifier/chunking/semantic_verifier.py +644 -0
  10. rag_verifier-0.1.0/rag_verifier/chunking/splitter.py +96 -0
  11. rag_verifier-0.1.0/rag_verifier/chunking/splitters/__init__.py +143 -0
  12. rag_verifier-0.1.0/rag_verifier/chunking/splitters/heading_ast.py +84 -0
  13. rag_verifier-0.1.0/rag_verifier/chunking/splitters/hierarchical_chinese.py +78 -0
  14. rag_verifier-0.1.0/rag_verifier/chunking/splitters/hierarchical_english.py +117 -0
  15. rag_verifier-0.1.0/rag_verifier/chunking/splitters/hierarchical_numeric.py +110 -0
  16. rag_verifier-0.1.0/rag_verifier/chunking/splitters/paragraph_fallback.py +91 -0
  17. rag_verifier-0.1.0/rag_verifier/chunking/template_verifier.py +155 -0
  18. rag_verifier-0.1.0/rag_verifier/chunking/verifier.py +638 -0
  19. rag_verifier-0.1.0/rag_verifier/citation/__init__.py +0 -0
  20. rag_verifier-0.1.0/rag_verifier/citation/verifier.py +250 -0
  21. rag_verifier-0.1.0/rag_verifier/cli.py +107 -0
  22. rag_verifier-0.1.0/rag_verifier/integrations/__init__.py +4 -0
  23. rag_verifier-0.1.0/rag_verifier/integrations/langchain.py +105 -0
  24. rag_verifier-0.1.0/rag_verifier/models.py +26 -0
  25. rag_verifier-0.1.0/rag_verifier/parsing/__init__.py +18 -0
  26. rag_verifier-0.1.0/rag_verifier/parsing/adapter.py +117 -0
  27. rag_verifier-0.1.0/rag_verifier/parsing/base.py +185 -0
  28. rag_verifier-0.1.0/rag_verifier/parsing/docling_parser.py +223 -0
  29. rag_verifier-0.1.0/rag_verifier/parsing/html_parser.py +171 -0
  30. rag_verifier-0.1.0/rag_verifier/parsing/level_inference.py +120 -0
  31. rag_verifier-0.1.0/rag_verifier/parsing/level_redetection.py +88 -0
  32. rag_verifier-0.1.0/rag_verifier/parsing/markdown_parser.py +140 -0
  33. rag_verifier-0.1.0/rag_verifier/parsing/models.py +75 -0
  34. rag_verifier-0.1.0/rag_verifier/parsing/pymupdf_parser.py +208 -0
  35. rag_verifier-0.1.0/rag_verifier/parsing/quality.py +178 -0
  36. rag_verifier-0.1.0/rag_verifier/parsing/text_clean.py +37 -0
  37. rag_verifier-0.1.0/rag_verifier/parsing/txt_parser.py +44 -0
  38. rag_verifier-0.1.0/rag_verifier/retrieval/__init__.py +0 -0
  39. rag_verifier-0.1.0/rag_verifier/retrieval/bm25.py +61 -0
  40. rag_verifier-0.1.0/rag_verifier/retrieval/corpus.py +125 -0
  41. rag_verifier-0.1.0/rag_verifier/retrieval/generate.py +52 -0
  42. rag_verifier-0.1.0/rag_verifier/retrieval/index.py +204 -0
  43. rag_verifier-0.1.0/rag_verifier/retrieval/ingest.py +208 -0
  44. rag_verifier-0.1.0/rag_verifier/retrieval/llm_rerank.py +61 -0
  45. rag_verifier-0.1.0/rag_verifier/retrieval/query_rewrite.py +139 -0
  46. rag_verifier-0.1.0/rag_verifier/retrieval/recall.py +136 -0
  47. rag_verifier-0.1.0/rag_verifier/retrieval/rerank.py +42 -0
  48. rag_verifier-0.1.0/rag_verifier/verify_api.py +110 -0
  49. rag_verifier-0.1.0/rag_verifier.egg-info/PKG-INFO +273 -0
  50. rag_verifier-0.1.0/rag_verifier.egg-info/SOURCES.txt +53 -0
  51. rag_verifier-0.1.0/rag_verifier.egg-info/dependency_links.txt +1 -0
  52. rag_verifier-0.1.0/rag_verifier.egg-info/entry_points.txt +2 -0
  53. rag_verifier-0.1.0/rag_verifier.egg-info/requires.txt +8 -0
  54. rag_verifier-0.1.0/rag_verifier.egg-info/top_level.txt +1 -0
  55. 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: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
19
+ [![CI](https://github.com/hlc0000/rag-verifier/actions/workflows/ci.yml/badge.svg)](https://github.com/hlc0000/rag-verifier/actions/workflows/ci.yml)
20
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](pyproject.toml)
21
+ [![PyPI](https://img.shields.io/badge/PyPI-coming%20soon-orange.svg)](#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
+ ![demo](docs/demo.png)
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: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
4
+ [![CI](https://github.com/hlc0000/rag-verifier/actions/workflows/ci.yml/badge.svg)](https://github.com/hlc0000/rag-verifier/actions/workflows/ci.yml)
5
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](pyproject.toml)
6
+ [![PyPI](https://img.shields.io/badge/PyPI-coming%20soon-orange.svg)](#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
+ ![demo](docs/demo.png)
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
+ ]
@@ -0,0 +1,3 @@
1
+ from rag_verifier.chunking.splitter import split_document
2
+
3
+ __all__ = ["split_document"]