rag-verifier 0.1.0__py3-none-any.whl

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 (50) hide show
  1. rag_verifier/__init__.py +27 -0
  2. rag_verifier/chunking/__init__.py +3 -0
  3. rag_verifier/chunking/detector.py +72 -0
  4. rag_verifier/chunking/rules.py +90 -0
  5. rag_verifier/chunking/semantic_verifier.py +644 -0
  6. rag_verifier/chunking/splitter.py +96 -0
  7. rag_verifier/chunking/splitters/__init__.py +143 -0
  8. rag_verifier/chunking/splitters/heading_ast.py +84 -0
  9. rag_verifier/chunking/splitters/hierarchical_chinese.py +78 -0
  10. rag_verifier/chunking/splitters/hierarchical_english.py +117 -0
  11. rag_verifier/chunking/splitters/hierarchical_numeric.py +110 -0
  12. rag_verifier/chunking/splitters/paragraph_fallback.py +91 -0
  13. rag_verifier/chunking/template_verifier.py +155 -0
  14. rag_verifier/chunking/verifier.py +638 -0
  15. rag_verifier/citation/__init__.py +0 -0
  16. rag_verifier/citation/verifier.py +250 -0
  17. rag_verifier/cli.py +107 -0
  18. rag_verifier/integrations/__init__.py +4 -0
  19. rag_verifier/integrations/langchain.py +105 -0
  20. rag_verifier/models.py +26 -0
  21. rag_verifier/parsing/__init__.py +18 -0
  22. rag_verifier/parsing/adapter.py +117 -0
  23. rag_verifier/parsing/base.py +185 -0
  24. rag_verifier/parsing/docling_parser.py +223 -0
  25. rag_verifier/parsing/html_parser.py +171 -0
  26. rag_verifier/parsing/level_inference.py +120 -0
  27. rag_verifier/parsing/level_redetection.py +88 -0
  28. rag_verifier/parsing/markdown_parser.py +140 -0
  29. rag_verifier/parsing/models.py +75 -0
  30. rag_verifier/parsing/pymupdf_parser.py +208 -0
  31. rag_verifier/parsing/quality.py +178 -0
  32. rag_verifier/parsing/text_clean.py +37 -0
  33. rag_verifier/parsing/txt_parser.py +44 -0
  34. rag_verifier/retrieval/__init__.py +0 -0
  35. rag_verifier/retrieval/bm25.py +61 -0
  36. rag_verifier/retrieval/corpus.py +125 -0
  37. rag_verifier/retrieval/generate.py +52 -0
  38. rag_verifier/retrieval/index.py +204 -0
  39. rag_verifier/retrieval/ingest.py +208 -0
  40. rag_verifier/retrieval/llm_rerank.py +61 -0
  41. rag_verifier/retrieval/query_rewrite.py +139 -0
  42. rag_verifier/retrieval/recall.py +136 -0
  43. rag_verifier/retrieval/rerank.py +42 -0
  44. rag_verifier/verify_api.py +110 -0
  45. rag_verifier-0.1.0.dist-info/METADATA +273 -0
  46. rag_verifier-0.1.0.dist-info/RECORD +50 -0
  47. rag_verifier-0.1.0.dist-info/WHEEL +5 -0
  48. rag_verifier-0.1.0.dist-info/entry_points.txt +2 -0
  49. rag_verifier-0.1.0.dist-info/licenses/LICENSE +21 -0
  50. rag_verifier-0.1.0.dist-info/top_level.txt +1 -0
@@ -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"]
@@ -0,0 +1,72 @@
1
+ """结构探测:扫描全文,识别文档的编号体系,据此选择切分策略。
2
+
3
+ 设计原则:切分器不预设文档类型。先统计「哪种编号模式在本文档中占主导」,
4
+ 再据此选切分器;探测不到任何结构时诚实降级到段落切分,并标注探测失败。
5
+
6
+ 注意:本模块的编号模式全部是行首锚定(配合 re.MULTILINE),
7
+ 因此探测必须跑在「保留换行」的清洗文本上——若先把换行拼掉,
8
+ `^` 就只剩全文首行能匹配,所有计数都会失真。
9
+ """
10
+ import re
11
+
12
+ # 编号模式表:name -> 行首锚定正则
13
+ PATTERNS = {
14
+ "clause_cn": r"^第[一二三四五六七八九十百零〇\d]+条", # 合同:第九条
15
+ "item_cn": r"^([一二三四五六七八九十]+)", # 合同子条款:(一)
16
+ "chapter_cn": r"^第[一二三四五六七八九十]+章", # 章节:第三章
17
+ "clause_num": r"^\d+(?:\.\d+)+\.?(\s|$)", # GB/T:5.1 / 5.1.1;RFC:5.6.(尾点)
18
+ "chapter_num": r"^\d{1,2}(\s+\S.*)?$", # GB/T 顶级章:1 范围 / 5 显式标识
19
+ "appendix_num": r"^[A-Z]\.\d+(?:\.\d+)*(\s|$)", # 附录编号:A.1 / C.1.1 / D.2.1
20
+ "appendix_head": r"^附\s*录\s*[A-Z]", # 附录标题:附 录 A
21
+ "section_num": r"^\d+\.\s", # 论文:1. 标题
22
+ "section_alpha": r"^[a-z]\.\s", # 论文:a. 标题
23
+ "article_en": r"^Article\s+\d+", # 英文合同:Article 5
24
+ "section_en": r"^Section\s+\d+", # 英文文件:Section 5
25
+ }
26
+
27
+ MIN_HITS = 3 # 某模式命中 ≥3 次,才认为它是本文档的编号体系
28
+
29
+ # 切分器选择优先级(互斥判定,避免互相踩):
30
+ # 1) 「数字点层级」或附录编号 → 层级数字编号文档(GB/T 这类)
31
+ # 2) 中文条款(第X条 / (一) / 第X章)→ 合同类
32
+ # 3) 英文条款(Article / Section)
33
+ # 4) 只有顶级章、没有子条款 → 仍是层级数字编号文档
34
+ # 5) 都没有 → 段落兜底
35
+ # clause_num 与 section_num 互斥:前者要求「数字点数字」(5.1),后者要求「数字点 + 空格」(1. 标题)。
36
+ # chapter_num 单独不足以判定:页码、"ICS 35.030"、表格序号这类裸数字也会命中它,
37
+ # 所以它只在「已排除其他体系」之后作为兜底信号,不作为首选依据。
38
+ NUMERIC_STRONG = ("clause_num", "appendix_num", "appendix_head")
39
+ CHINESE_KEYS = ("clause_cn", "item_cn", "chapter_cn")
40
+ ENGLISH_KEYS = ("article_en", "section_en")
41
+
42
+
43
+ def is_active(active: dict, keys) -> bool:
44
+ return any(k in active for k in keys)
45
+
46
+
47
+ def detect_structure(text: str) -> dict:
48
+ """统计各编号模式的命中数,返回命中数以及「主导模式」(命中 ≥ MIN_HITS)。"""
49
+ counts = {name: len(re.findall(pattern, text, re.MULTILINE)) for name, pattern in PATTERNS.items()}
50
+ active = {name: n for name, n in counts.items() if n >= MIN_HITS}
51
+ return {"counts": counts, "active": active}
52
+
53
+
54
+ def select_splitter(active: dict) -> str:
55
+ """按优先级从「主导模式」里选切分器;一个都没有则段落兜底。
56
+
57
+ 选中 ≠ 可用:hierarchical_chinese 的切块锚点只有「第X条」——(X)只是条内
58
+ 二级切分、「第X章」不是它的锚点。主导信号若只有 item_cn/chapter_cn 而无
59
+ clause_cn,选中后一个锚点都打不到 → 静默 0 块(盲测白皮书实测:「(一)」
60
+ 顿号体系文档 0 块)。锚点存在性校验:clause_cn 不在 active 就诚实降级。
61
+ """
62
+ if is_active(active, NUMERIC_STRONG):
63
+ return "hierarchical_numeric"
64
+ if is_active(active, CHINESE_KEYS):
65
+ if "clause_cn" in active:
66
+ return "hierarchical_chinese"
67
+ return "paragraph_fallback"
68
+ if is_active(active, ENGLISH_KEYS):
69
+ return "hierarchical_english"
70
+ if "chapter_num" in active:
71
+ return "hierarchical_numeric"
72
+ return "paragraph_fallback"
@@ -0,0 +1,90 @@
1
+ """规则注册表:按检查项 + 语种分组,内置规则与第三方规则走同一接口。
2
+
3
+ 语种由 detect_language 按 CJK 字符占比自动判定(zh / en);
4
+ lang="any" 的规则对所有语种生效。规则按注册顺序执行,先命中先返回。
5
+
6
+ 第三方扩展示例(不改本仓库任何文件):
7
+
8
+ from rag_verifier.chunking.rules import register_rule
9
+
10
+ @register_rule("completeness", lang="en", id="no_currency_unit")
11
+ def my_rule(card, ctx):
12
+ # 返回 VerificationResult 表示命中,返回 None 表示放行
13
+ ...
14
+ return None
15
+ """
16
+ import re
17
+ from dataclasses import dataclass
18
+ from typing import Callable
19
+
20
+ from rag_verifier.models import EvidenceCard
21
+
22
+ CJK_RE = re.compile(r"[\u3400-\u4dbf\u4e00-\u9fff]")
23
+
24
+ CHECKS = (
25
+ "independence",
26
+ "completeness",
27
+ "traceability",
28
+ "semantic_independence",
29
+ "template_completeness",
30
+ )
31
+
32
+
33
+ def detect_language(text: str) -> str:
34
+ """按 CJK 字符占比判定语种:非空白字符中 CJK 占比 ≥15% 判 zh,否则 en。
35
+
36
+ 纯符号/数字块(如 "3.1")判为 en——中英规则对这类块都不命中,
37
+ 不影响判定结果。
38
+ """
39
+ if not text:
40
+ return "en"
41
+ cjk = len(CJK_RE.findall(text))
42
+ nonspace = len(re.sub(r"\s", "", text))
43
+ if nonspace and cjk / nonspace >= 0.15:
44
+ return "zh"
45
+ return "en"
46
+
47
+
48
+ @dataclass
49
+ class RuleContext:
50
+ """规则执行上下文:语种 + 前一块 + 源文本 + 全文。"""
51
+
52
+ lang: str
53
+ prev_chunk: str = ""
54
+ source_text: str = ""
55
+ full_doc: str = ""
56
+
57
+
58
+ @dataclass
59
+ class Rule:
60
+ id: str
61
+ check: str
62
+ lang: str # zh / en / any
63
+ fn: Callable[[EvidenceCard, RuleContext], object]
64
+
65
+
66
+ _RULES: list[Rule] = []
67
+
68
+
69
+ def register_rule(check: str, lang: str = "any", id: str | None = None):
70
+ """注册规则。check 必须是 CHECKS 之一;lang 为 zh / en / any。"""
71
+ if check not in CHECKS:
72
+ raise ValueError(f"未知检查项: {check}(可选: {', '.join(CHECKS)})")
73
+ if lang not in ("zh", "en", "any"):
74
+ raise ValueError(f"未知语种: {lang}(可选: zh / en / any)")
75
+
76
+ def deco(fn):
77
+ _RULES.append(Rule(id=id or fn.__name__, check=check, lang=lang, fn=fn))
78
+ return fn
79
+
80
+ return deco
81
+
82
+
83
+ def rules_for(check: str, lang: str) -> list[Rule]:
84
+ """取出某检查项在当前语种下生效的规则(保持注册顺序)。"""
85
+ return [r for r in _RULES if r.check == check and r.lang in ("any", lang)]
86
+
87
+
88
+ def list_rules() -> list[dict]:
89
+ """规则清单(调试/文档用途)。"""
90
+ return [{"id": r.id, "check": r.check, "lang": r.lang} for r in _RULES]