rag-hygiene 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_hygiene-0.1.0/LICENSE +21 -0
- rag_hygiene-0.1.0/PKG-INFO +175 -0
- rag_hygiene-0.1.0/README.md +145 -0
- rag_hygiene-0.1.0/pyproject.toml +61 -0
- rag_hygiene-0.1.0/setup.cfg +4 -0
- rag_hygiene-0.1.0/src/rag_hygiene/__init__.py +87 -0
- rag_hygiene-0.1.0/src/rag_hygiene/cli.py +233 -0
- rag_hygiene-0.1.0/src/rag_hygiene/fix.py +271 -0
- rag_hygiene-0.1.0/src/rag_hygiene/io.py +84 -0
- rag_hygiene-0.1.0/src/rag_hygiene/models.py +179 -0
- rag_hygiene-0.1.0/src/rag_hygiene/py.typed +0 -0
- rag_hygiene-0.1.0/src/rag_hygiene/rules.py +340 -0
- rag_hygiene-0.1.0/src/rag_hygiene/text.py +315 -0
- rag_hygiene-0.1.0/src/rag_hygiene.egg-info/PKG-INFO +175 -0
- rag_hygiene-0.1.0/src/rag_hygiene.egg-info/SOURCES.txt +21 -0
- rag_hygiene-0.1.0/src/rag_hygiene.egg-info/dependency_links.txt +1 -0
- rag_hygiene-0.1.0/src/rag_hygiene.egg-info/entry_points.txt +2 -0
- rag_hygiene-0.1.0/src/rag_hygiene.egg-info/requires.txt +5 -0
- rag_hygiene-0.1.0/src/rag_hygiene.egg-info/top_level.txt +1 -0
- rag_hygiene-0.1.0/tests/test_cli.py +117 -0
- rag_hygiene-0.1.0/tests/test_fix.py +178 -0
- rag_hygiene-0.1.0/tests/test_rules.py +119 -0
- rag_hygiene-0.1.0/tests/test_text.py +77 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 rag-hygiene 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.
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rag-hygiene
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Find and automatically repair broken RAG chunks before you embed them. Zero dependencies.
|
|
5
|
+
Author: OnProject
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/OnProject/rag-hygiene
|
|
8
|
+
Project-URL: Repository, https://github.com/OnProject/rag-hygiene
|
|
9
|
+
Project-URL: Issues, https://github.com/OnProject/rag-hygiene/issues
|
|
10
|
+
Keywords: rag,chunking,retrieval,llm,embeddings,vector-database,linter,cleanup
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
20
|
+
Classifier: Topic :: Text Processing :: Linguistic
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
27
|
+
Requires-Dist: build; extra == "dev"
|
|
28
|
+
Requires-Dist: twine; extra == "dev"
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# rag-hygiene
|
|
32
|
+
|
|
33
|
+
**Find and repair broken RAG chunks before you embed them.**
|
|
34
|
+
|
|
35
|
+
Most RAG failures are decided before retrieval ever runs. A chunk that opens in
|
|
36
|
+
the middle of a sentence embeds two unrelated claims. A table split from its
|
|
37
|
+
header embeds a grid of numbers with no column names. A PDF that broke a word
|
|
38
|
+
across lines embeds `docu` and `ment` as one token. None of this is visible until
|
|
39
|
+
answers start being subtly wrong months later.
|
|
40
|
+
|
|
41
|
+
`rag-hygiene` sits between your splitter and your vector database. It diagnoses
|
|
42
|
+
nine classes of chunk damage and **repairs seven of them automatically** — no
|
|
43
|
+
LLM calls, no embeddings, no eval set, no network, no dependencies.
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install rag-hygiene
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Quick start
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
rag-hygiene scan chunks.json # what is wrong?
|
|
53
|
+
rag-hygiene fix chunks.json # repair it → chunks.clean.json
|
|
54
|
+
rag-hygiene fix chunks.json --dry-run -v # preview the diff first
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
2 chunks scanned
|
|
59
|
+
|
|
60
|
+
mid-sentence-start Chunk resumes a cut sentence
|
|
61
|
+
[high] c2 opens mid-sentence ('40% year over year, beat'); its embedding mixes two unrelated claims ·fixable
|
|
62
|
+
|
|
63
|
+
2 finding(s) 1 high 1 low · 1 auto-fixable
|
|
64
|
+
run `rag-hygiene fix` to repair, or add --dry-run to preview
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## The repair that matters
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
before after
|
|
71
|
+
──────────────────────────────────── ────────────────────────────────────
|
|
72
|
+
[chunk 7] …因此公司营收同比增长 [chunk 7] …因此公司营收同比增长 40%,
|
|
73
|
+
[chunk 8] 40%,超出市场预期。 超出市场预期。
|
|
74
|
+
✓ merged ✓ heading "Q3 财报" injected
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Every repair is **lossless or a rejoin of text that was already adjacent**.
|
|
78
|
+
Nothing is summarised, paraphrased, or invented — which is what lets it run
|
|
79
|
+
unattended in a build step.
|
|
80
|
+
|
|
81
|
+
## Rules
|
|
82
|
+
|
|
83
|
+
| Rule | Severity | Auto-fix | What it catches |
|
|
84
|
+
|---|---|---|---|
|
|
85
|
+
| `empty-chunk` | high | ✅ removed | Whitespace-only chunks; wasted embedding slots |
|
|
86
|
+
| `mid-sentence-start` | high | ✅ merged | Chunk resumes a sentence the previous one cut |
|
|
87
|
+
| `tiny-chunk` | medium | ✅ merged | Under 20 chars of prose — no meaning to embed |
|
|
88
|
+
| `unclosed-fence` | medium | ✅ closed | Code fence opens, never closes — rest of prompt becomes code |
|
|
89
|
+
| `orphan-table` | medium | ✅ header restored | Markdown table rows whose header landed in another chunk |
|
|
90
|
+
| `near-duplicate` | medium | — | ≥72% word-sequence overlap; dupes split ranking mass |
|
|
91
|
+
| `oversized-chunk` | low | — | Over 4000 chars — retrieval dilutes |
|
|
92
|
+
| `pdf-noise` | low | ✅ normalised | Hyphenated line breaks, column padding, page gaps |
|
|
93
|
+
| `missing-heading` | low | ✅ injected | A heading exists in the document but the chunk can't see it |
|
|
94
|
+
|
|
95
|
+
`near-duplicate` and `oversized-chunk` are deliberately **not** auto-repaired:
|
|
96
|
+
dropping a duplicate or truncating text changes what the retriever can find, and
|
|
97
|
+
that is an editorial decision, not a mechanical one. The tool reports them and
|
|
98
|
+
stays out of the way.
|
|
99
|
+
|
|
100
|
+
## Python API
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from rag_hygiene import check, repair
|
|
104
|
+
|
|
105
|
+
chunks = [
|
|
106
|
+
{"id": "a", "text": "Revenue grew by", "source": "q3.md", "order": 0},
|
|
107
|
+
{"id": "b", "text": "40% year over year.", "source": "q3.md", "order": 1},
|
|
108
|
+
{"id": "c", "text": " ", "source": "q3.md", "order": 2},
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
report = check(chunks)
|
|
112
|
+
report.count("high") # 2
|
|
113
|
+
report.by_rule() # {'empty-chunk': 1, 'mid-sentence-start': 1, ...}
|
|
114
|
+
report.fails("medium") # True
|
|
115
|
+
|
|
116
|
+
clean, report = repair(chunks)
|
|
117
|
+
clean[0].text # 'Revenue grew by 40% year over year.'
|
|
118
|
+
clean[0].metadata['merged_ids'] # ['b'] ← provenance is never lost
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Accepts `Chunk` objects, dicts (`text` / `page_content` / `content`), or bare
|
|
122
|
+
strings. Works with LangChain and LlamaIndex document exports unchanged.
|
|
123
|
+
|
|
124
|
+
## CI gate
|
|
125
|
+
|
|
126
|
+
The exit code follows the linter convention, so it drops into any pipeline:
|
|
127
|
+
|
|
128
|
+
```yaml
|
|
129
|
+
- run: pip install rag-hygiene
|
|
130
|
+
- run: rag-hygiene scan chunks.json --fail-on high
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
| exit | meaning |
|
|
134
|
+
|---|---|
|
|
135
|
+
| 0 | clean, or the `--fail-on` gate was not tripped |
|
|
136
|
+
| 1 | a finding at or above `--fail-on` remains |
|
|
137
|
+
| 2 | bad input (missing file, unsupported format, unknown rule) |
|
|
138
|
+
| 3 | internal error |
|
|
139
|
+
|
|
140
|
+
## Formats
|
|
141
|
+
|
|
142
|
+
Reads `.json`, `.jsonl`, and `.txt` / `.md`. A JSON payload may be a bare list or
|
|
143
|
+
an object with a `chunks` / `documents` / `data` / `items` key. Output preserves
|
|
144
|
+
the input format.
|
|
145
|
+
|
|
146
|
+
## Design notes
|
|
147
|
+
|
|
148
|
+
Three decisions shape the behaviour:
|
|
149
|
+
|
|
150
|
+
**It joins only definite continuations.** A chunk opening with `40% …`, `, and
|
|
151
|
+
then`, or a lowercase word is provably a continuation, so it gets merged. A chunk
|
|
152
|
+
opening with `Revenue grew` *might* have lost its predecessor — that is reported
|
|
153
|
+
at high severity but **not** auto-merged, because guessing would rewrite evidence.
|
|
154
|
+
`rag-hygiene scan --fail-on high` still catches it; you decide.
|
|
155
|
+
|
|
156
|
+
**Repairs are idempotent.** Running `fix` twice produces the same bytes as running
|
|
157
|
+
it once. That is asserted in the test suite (`test_repair_converges`), because a
|
|
158
|
+
formatter that keeps drifting is worse than no formatter.
|
|
159
|
+
|
|
160
|
+
**`missing-heading` converges.** A document that carries no heading anywhere gets
|
|
161
|
+
one `info`-level finding, not one per chunk — there is nothing to repair, and
|
|
162
|
+
flooding the report would make the gate useless. A document with a heading but
|
|
163
|
+
unlabelled chunks gets per-chunk findings that `fix` actually resolves.
|
|
164
|
+
|
|
165
|
+
## Comparison
|
|
166
|
+
|
|
167
|
+
`chunklint` (May 2026) performs overlapping static checks and depends on
|
|
168
|
+
pydantic, rich, typer, rapidfuzz and pyyaml. `rag-hygiene` differs in three ways:
|
|
169
|
+
it **repairs** rather than only reporting, it has **zero dependencies**, and it
|
|
170
|
+
ships CJK sentence segmentation (Chinese/Japanese/Korean text has no spaces after
|
|
171
|
+
`。`, so the usual whitespace heuristics silently do nothing on it).
|
|
172
|
+
|
|
173
|
+
## License
|
|
174
|
+
|
|
175
|
+
MIT
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# rag-hygiene
|
|
2
|
+
|
|
3
|
+
**Find and repair broken RAG chunks before you embed them.**
|
|
4
|
+
|
|
5
|
+
Most RAG failures are decided before retrieval ever runs. A chunk that opens in
|
|
6
|
+
the middle of a sentence embeds two unrelated claims. A table split from its
|
|
7
|
+
header embeds a grid of numbers with no column names. A PDF that broke a word
|
|
8
|
+
across lines embeds `docu` and `ment` as one token. None of this is visible until
|
|
9
|
+
answers start being subtly wrong months later.
|
|
10
|
+
|
|
11
|
+
`rag-hygiene` sits between your splitter and your vector database. It diagnoses
|
|
12
|
+
nine classes of chunk damage and **repairs seven of them automatically** — no
|
|
13
|
+
LLM calls, no embeddings, no eval set, no network, no dependencies.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install rag-hygiene
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
rag-hygiene scan chunks.json # what is wrong?
|
|
23
|
+
rag-hygiene fix chunks.json # repair it → chunks.clean.json
|
|
24
|
+
rag-hygiene fix chunks.json --dry-run -v # preview the diff first
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
2 chunks scanned
|
|
29
|
+
|
|
30
|
+
mid-sentence-start Chunk resumes a cut sentence
|
|
31
|
+
[high] c2 opens mid-sentence ('40% year over year, beat'); its embedding mixes two unrelated claims ·fixable
|
|
32
|
+
|
|
33
|
+
2 finding(s) 1 high 1 low · 1 auto-fixable
|
|
34
|
+
run `rag-hygiene fix` to repair, or add --dry-run to preview
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## The repair that matters
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
before after
|
|
41
|
+
──────────────────────────────────── ────────────────────────────────────
|
|
42
|
+
[chunk 7] …因此公司营收同比增长 [chunk 7] …因此公司营收同比增长 40%,
|
|
43
|
+
[chunk 8] 40%,超出市场预期。 超出市场预期。
|
|
44
|
+
✓ merged ✓ heading "Q3 财报" injected
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Every repair is **lossless or a rejoin of text that was already adjacent**.
|
|
48
|
+
Nothing is summarised, paraphrased, or invented — which is what lets it run
|
|
49
|
+
unattended in a build step.
|
|
50
|
+
|
|
51
|
+
## Rules
|
|
52
|
+
|
|
53
|
+
| Rule | Severity | Auto-fix | What it catches |
|
|
54
|
+
|---|---|---|---|
|
|
55
|
+
| `empty-chunk` | high | ✅ removed | Whitespace-only chunks; wasted embedding slots |
|
|
56
|
+
| `mid-sentence-start` | high | ✅ merged | Chunk resumes a sentence the previous one cut |
|
|
57
|
+
| `tiny-chunk` | medium | ✅ merged | Under 20 chars of prose — no meaning to embed |
|
|
58
|
+
| `unclosed-fence` | medium | ✅ closed | Code fence opens, never closes — rest of prompt becomes code |
|
|
59
|
+
| `orphan-table` | medium | ✅ header restored | Markdown table rows whose header landed in another chunk |
|
|
60
|
+
| `near-duplicate` | medium | — | ≥72% word-sequence overlap; dupes split ranking mass |
|
|
61
|
+
| `oversized-chunk` | low | — | Over 4000 chars — retrieval dilutes |
|
|
62
|
+
| `pdf-noise` | low | ✅ normalised | Hyphenated line breaks, column padding, page gaps |
|
|
63
|
+
| `missing-heading` | low | ✅ injected | A heading exists in the document but the chunk can't see it |
|
|
64
|
+
|
|
65
|
+
`near-duplicate` and `oversized-chunk` are deliberately **not** auto-repaired:
|
|
66
|
+
dropping a duplicate or truncating text changes what the retriever can find, and
|
|
67
|
+
that is an editorial decision, not a mechanical one. The tool reports them and
|
|
68
|
+
stays out of the way.
|
|
69
|
+
|
|
70
|
+
## Python API
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from rag_hygiene import check, repair
|
|
74
|
+
|
|
75
|
+
chunks = [
|
|
76
|
+
{"id": "a", "text": "Revenue grew by", "source": "q3.md", "order": 0},
|
|
77
|
+
{"id": "b", "text": "40% year over year.", "source": "q3.md", "order": 1},
|
|
78
|
+
{"id": "c", "text": " ", "source": "q3.md", "order": 2},
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
report = check(chunks)
|
|
82
|
+
report.count("high") # 2
|
|
83
|
+
report.by_rule() # {'empty-chunk': 1, 'mid-sentence-start': 1, ...}
|
|
84
|
+
report.fails("medium") # True
|
|
85
|
+
|
|
86
|
+
clean, report = repair(chunks)
|
|
87
|
+
clean[0].text # 'Revenue grew by 40% year over year.'
|
|
88
|
+
clean[0].metadata['merged_ids'] # ['b'] ← provenance is never lost
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Accepts `Chunk` objects, dicts (`text` / `page_content` / `content`), or bare
|
|
92
|
+
strings. Works with LangChain and LlamaIndex document exports unchanged.
|
|
93
|
+
|
|
94
|
+
## CI gate
|
|
95
|
+
|
|
96
|
+
The exit code follows the linter convention, so it drops into any pipeline:
|
|
97
|
+
|
|
98
|
+
```yaml
|
|
99
|
+
- run: pip install rag-hygiene
|
|
100
|
+
- run: rag-hygiene scan chunks.json --fail-on high
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
| exit | meaning |
|
|
104
|
+
|---|---|
|
|
105
|
+
| 0 | clean, or the `--fail-on` gate was not tripped |
|
|
106
|
+
| 1 | a finding at or above `--fail-on` remains |
|
|
107
|
+
| 2 | bad input (missing file, unsupported format, unknown rule) |
|
|
108
|
+
| 3 | internal error |
|
|
109
|
+
|
|
110
|
+
## Formats
|
|
111
|
+
|
|
112
|
+
Reads `.json`, `.jsonl`, and `.txt` / `.md`. A JSON payload may be a bare list or
|
|
113
|
+
an object with a `chunks` / `documents` / `data` / `items` key. Output preserves
|
|
114
|
+
the input format.
|
|
115
|
+
|
|
116
|
+
## Design notes
|
|
117
|
+
|
|
118
|
+
Three decisions shape the behaviour:
|
|
119
|
+
|
|
120
|
+
**It joins only definite continuations.** A chunk opening with `40% …`, `, and
|
|
121
|
+
then`, or a lowercase word is provably a continuation, so it gets merged. A chunk
|
|
122
|
+
opening with `Revenue grew` *might* have lost its predecessor — that is reported
|
|
123
|
+
at high severity but **not** auto-merged, because guessing would rewrite evidence.
|
|
124
|
+
`rag-hygiene scan --fail-on high` still catches it; you decide.
|
|
125
|
+
|
|
126
|
+
**Repairs are idempotent.** Running `fix` twice produces the same bytes as running
|
|
127
|
+
it once. That is asserted in the test suite (`test_repair_converges`), because a
|
|
128
|
+
formatter that keeps drifting is worse than no formatter.
|
|
129
|
+
|
|
130
|
+
**`missing-heading` converges.** A document that carries no heading anywhere gets
|
|
131
|
+
one `info`-level finding, not one per chunk — there is nothing to repair, and
|
|
132
|
+
flooding the report would make the gate useless. A document with a heading but
|
|
133
|
+
unlabelled chunks gets per-chunk findings that `fix` actually resolves.
|
|
134
|
+
|
|
135
|
+
## Comparison
|
|
136
|
+
|
|
137
|
+
`chunklint` (May 2026) performs overlapping static checks and depends on
|
|
138
|
+
pydantic, rich, typer, rapidfuzz and pyyaml. `rag-hygiene` differs in three ways:
|
|
139
|
+
it **repairs** rather than only reporting, it has **zero dependencies**, and it
|
|
140
|
+
ships CJK sentence segmentation (Chinese/Japanese/Korean text has no spaces after
|
|
141
|
+
`。`, so the usual whitespace heuristics silently do nothing on it).
|
|
142
|
+
|
|
143
|
+
## License
|
|
144
|
+
|
|
145
|
+
MIT
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "rag-hygiene"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Find and automatically repair broken RAG chunks before you embed them. Zero dependencies."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "OnProject" }]
|
|
14
|
+
keywords = [
|
|
15
|
+
"rag",
|
|
16
|
+
"chunking",
|
|
17
|
+
"retrieval",
|
|
18
|
+
"llm",
|
|
19
|
+
"embeddings",
|
|
20
|
+
"vector-database",
|
|
21
|
+
"linter",
|
|
22
|
+
"cleanup",
|
|
23
|
+
]
|
|
24
|
+
classifiers = [
|
|
25
|
+
"Development Status :: 3 - Alpha",
|
|
26
|
+
"Intended Audience :: Developers",
|
|
27
|
+
"Operating System :: OS Independent",
|
|
28
|
+
"Programming Language :: Python :: 3",
|
|
29
|
+
"Programming Language :: Python :: 3.10",
|
|
30
|
+
"Programming Language :: Python :: 3.11",
|
|
31
|
+
"Programming Language :: Python :: 3.12",
|
|
32
|
+
"Programming Language :: Python :: 3.13",
|
|
33
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
34
|
+
"Topic :: Text Processing :: Linguistic",
|
|
35
|
+
"Typing :: Typed",
|
|
36
|
+
]
|
|
37
|
+
dependencies = []
|
|
38
|
+
|
|
39
|
+
[project.optional-dependencies]
|
|
40
|
+
dev = ["pytest>=7", "build", "twine"]
|
|
41
|
+
|
|
42
|
+
[project.scripts]
|
|
43
|
+
rag-hygiene = "rag_hygiene.cli:main"
|
|
44
|
+
|
|
45
|
+
[project.urls]
|
|
46
|
+
Homepage = "https://github.com/OnProject/rag-hygiene"
|
|
47
|
+
Repository = "https://github.com/OnProject/rag-hygiene"
|
|
48
|
+
Issues = "https://github.com/OnProject/rag-hygiene/issues"
|
|
49
|
+
|
|
50
|
+
[tool.setuptools]
|
|
51
|
+
package-dir = { "" = "src" }
|
|
52
|
+
|
|
53
|
+
[tool.setuptools.packages.find]
|
|
54
|
+
where = ["src"]
|
|
55
|
+
|
|
56
|
+
[tool.setuptools.package-data]
|
|
57
|
+
rag_hygiene = ["py.typed"]
|
|
58
|
+
|
|
59
|
+
[tool.pytest.ini_options]
|
|
60
|
+
testpaths = ["tests"]
|
|
61
|
+
addopts = "-q"
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""rag-hygiene — find and repair broken RAG chunks before you embed them.
|
|
2
|
+
|
|
3
|
+
>>> from rag_hygiene import check, repair
|
|
4
|
+
>>> chunks = [{"id": "a", "text": "Revenue grew by"}, {"id": "b", "text": "40% last quarter."}]
|
|
5
|
+
>>> report = check(chunks)
|
|
6
|
+
>>> report.count("high")
|
|
7
|
+
1
|
|
8
|
+
>>> clean, fixes = repair(chunks)
|
|
9
|
+
>>> clean[0].text
|
|
10
|
+
'Revenue grew by 40% last quarter.'
|
|
11
|
+
|
|
12
|
+
Zero dependencies, no network, no model calls — every rule is deterministic, so
|
|
13
|
+
this can run in a pre-commit hook or a CI job.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import Any, Iterable
|
|
19
|
+
|
|
20
|
+
from .fix import fix_chunks
|
|
21
|
+
from .models import Chunk, Finding, Fix, Report
|
|
22
|
+
from .rules import RULES
|
|
23
|
+
|
|
24
|
+
__version__ = "0.1.0"
|
|
25
|
+
__all__ = [
|
|
26
|
+
"Chunk",
|
|
27
|
+
"Finding",
|
|
28
|
+
"Fix",
|
|
29
|
+
"Report",
|
|
30
|
+
"RULES",
|
|
31
|
+
"check",
|
|
32
|
+
"repair",
|
|
33
|
+
"explain",
|
|
34
|
+
"__version__",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _coerce(chunks: Iterable[Any]) -> list[Chunk]:
|
|
39
|
+
return [Chunk.from_obj(c, i) for i, c in enumerate(chunks)]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def check(chunks: Iterable[Any], *, min_severity: str = "info") -> Report:
|
|
43
|
+
"""Diagnose ``chunks`` without changing them.
|
|
44
|
+
|
|
45
|
+
Accepts a list of :class:`Chunk`, dicts with a ``text`` (or ``page_content``)
|
|
46
|
+
key, or bare strings.
|
|
47
|
+
"""
|
|
48
|
+
from .rules import scan
|
|
49
|
+
|
|
50
|
+
items = _coerce(chunks)
|
|
51
|
+
report = Report(chunks_scanned=len(items), chunks_after=len(items))
|
|
52
|
+
report.findings = scan(items, min_severity=min_severity)
|
|
53
|
+
report.chars_before = report.chars_after = sum(len(c.text) for c in items)
|
|
54
|
+
return report
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def repair(
|
|
58
|
+
chunks: Iterable[Any],
|
|
59
|
+
*,
|
|
60
|
+
only: set[str] | None = None,
|
|
61
|
+
inject_heading: bool = True,
|
|
62
|
+
) -> tuple[list[Chunk], Report]:
|
|
63
|
+
"""Repair ``chunks`` and return ``(repaired_chunks, report)``.
|
|
64
|
+
|
|
65
|
+
The report carries both the repairs that were applied and the problems that
|
|
66
|
+
remain (duplicates, oversized chunks) because those need a human decision.
|
|
67
|
+
"""
|
|
68
|
+
from .rules import scan
|
|
69
|
+
|
|
70
|
+
items = _coerce(chunks)
|
|
71
|
+
repaired, fix_report = fix_chunks(items, only=only, inject_heading=inject_heading)
|
|
72
|
+
findings = scan(repaired)
|
|
73
|
+
fix_report.findings = findings
|
|
74
|
+
return repaired, fix_report
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def explain(rule_id: str) -> str:
|
|
78
|
+
"""Why a rule exists and whether it auto-repairs."""
|
|
79
|
+
from .rules import RULE_INDEX
|
|
80
|
+
|
|
81
|
+
if rule_id not in RULE_INDEX:
|
|
82
|
+
raise KeyError(f"unknown rule {rule_id!r}; run `rag-hygiene rules`")
|
|
83
|
+
rule = RULE_INDEX[rule_id]
|
|
84
|
+
return (
|
|
85
|
+
f"{rule.id} ({rule.severity}, {'auto-fixable' if rule.fixable else 'manual'})\n"
|
|
86
|
+
f" {rule.title}"
|
|
87
|
+
)
|