chromerag 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.
- chromerag/__init__.py +37 -0
- chromerag/batch.py +111 -0
- chromerag/cli.py +296 -0
- chromerag/config.py +181 -0
- chromerag/density.py +293 -0
- chromerag/dvdf.py +129 -0
- chromerag/extractor.py +226 -0
- chromerag/input_quality.py +124 -0
- chromerag/markdown_out.py +164 -0
- chromerag/models.py +35 -0
- chromerag/schema_fusion.py +183 -0
- chromerag/site_chrome.py +362 -0
- chromerag/tables.py +71 -0
- chromerag-0.1.0.dist-info/METADATA +241 -0
- chromerag-0.1.0.dist-info/RECORD +20 -0
- chromerag-0.1.0.dist-info/WHEEL +5 -0
- chromerag-0.1.0.dist-info/entry_points.txt +2 -0
- chromerag-0.1.0.dist-info/licenses/LICENSE +21 -0
- chromerag-0.1.0.dist-info/licenses/LICENSE.txt +21 -0
- chromerag-0.1.0.dist-info/top_level.txt +1 -0
chromerag/__init__.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""ChromeRAG — Ingest-time elimination of site template noise for enterprise web RAG.
|
|
2
|
+
|
|
3
|
+
Core contract: HTML string in → RAG-ready Markdown out.
|
|
4
|
+
Fetching/crawling is intentionally out of scope (use your crawler / poc harness).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from chromerag.batch import learn_then_extract
|
|
8
|
+
from chromerag.config import ContentPriority, PageType, PipelineConfig, Strictness
|
|
9
|
+
from chromerag.extractor import ChromeRAG
|
|
10
|
+
from chromerag.input_quality import InputQualityReport, assess_input_html
|
|
11
|
+
from chromerag.models import ExtractResult
|
|
12
|
+
from chromerag.site_chrome import (
|
|
13
|
+
SiteChromeModel,
|
|
14
|
+
load_chrome_models,
|
|
15
|
+
mine_site_chrome,
|
|
16
|
+
save_chrome_models,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"ChromeRAG",
|
|
21
|
+
"ExtractResult",
|
|
22
|
+
"PipelineConfig",
|
|
23
|
+
"ContentPriority",
|
|
24
|
+
"Strictness",
|
|
25
|
+
"PageType",
|
|
26
|
+
"SiteChromeModel",
|
|
27
|
+
"mine_site_chrome",
|
|
28
|
+
"save_chrome_models",
|
|
29
|
+
"load_chrome_models",
|
|
30
|
+
"learn_then_extract",
|
|
31
|
+
"assess_input_html",
|
|
32
|
+
"InputQualityReport",
|
|
33
|
+
]
|
|
34
|
+
__version__ = "0.1.0"
|
|
35
|
+
__paper__ = (
|
|
36
|
+
"ChromeRAG: Ingest-Time Elimination of Site Template Noise for Enterprise Web RAG"
|
|
37
|
+
)
|
chromerag/batch.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Learn-then-extract batch helpers.
|
|
2
|
+
|
|
3
|
+
Critical order when STCE is enabled:
|
|
4
|
+
1. Mine chrome patterns across the batch (or load a saved model)
|
|
5
|
+
2. Only then extract each page with the learned model
|
|
6
|
+
|
|
7
|
+
Never extract-first then learn later — early chunks would keep template noise.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Iterable
|
|
14
|
+
|
|
15
|
+
from chromerag.config import PipelineConfig
|
|
16
|
+
from chromerag.extractor import ChromeRAG
|
|
17
|
+
from chromerag.models import ExtractResult
|
|
18
|
+
from chromerag.site_chrome import (
|
|
19
|
+
SiteChromeModel,
|
|
20
|
+
mine_site_chrome,
|
|
21
|
+
select_model_for_url,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class BatchPage:
|
|
27
|
+
html: str
|
|
28
|
+
url: str | None = None
|
|
29
|
+
page_id: str | None = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class BatchResult:
|
|
34
|
+
page_id: str | None
|
|
35
|
+
url: str | None
|
|
36
|
+
result: ExtractResult
|
|
37
|
+
chrome_group: str | None
|
|
38
|
+
stce_applied: bool
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def learn_chrome(
|
|
42
|
+
pages: Iterable[BatchPage | tuple[str | None, str]],
|
|
43
|
+
*,
|
|
44
|
+
min_pages: int | None = None,
|
|
45
|
+
frequency: float | None = None,
|
|
46
|
+
max_block_chars: int | None = None,
|
|
47
|
+
require_chrome_features: bool = True,
|
|
48
|
+
path_prefix_depth: int = 1,
|
|
49
|
+
config: PipelineConfig | None = None,
|
|
50
|
+
) -> dict[str, SiteChromeModel]:
|
|
51
|
+
"""Mine site chrome from a multi-page corpus. Does not extract."""
|
|
52
|
+
cfg = config or PipelineConfig()
|
|
53
|
+
pairs: list[tuple[str | None, str]] = []
|
|
54
|
+
for p in pages:
|
|
55
|
+
if isinstance(p, BatchPage):
|
|
56
|
+
pairs.append((p.url, p.html))
|
|
57
|
+
else:
|
|
58
|
+
pairs.append((p[0], p[1]))
|
|
59
|
+
return mine_site_chrome(
|
|
60
|
+
pairs,
|
|
61
|
+
min_pages=min_pages if min_pages is not None else cfg.stce_min_pages,
|
|
62
|
+
frequency=frequency if frequency is not None else cfg.stce_frequency,
|
|
63
|
+
max_block_chars=max_block_chars
|
|
64
|
+
if max_block_chars is not None
|
|
65
|
+
else cfg.stce_max_block_chars,
|
|
66
|
+
require_chrome_features=require_chrome_features,
|
|
67
|
+
path_prefix_depth=path_prefix_depth,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def learn_then_extract(
|
|
72
|
+
pages: Iterable[BatchPage],
|
|
73
|
+
*,
|
|
74
|
+
config: PipelineConfig | None = None,
|
|
75
|
+
chrome_models: dict[str, SiteChromeModel] | None = None,
|
|
76
|
+
learn: bool = True,
|
|
77
|
+
path_prefix_depth: int = 1,
|
|
78
|
+
) -> tuple[list[BatchResult], dict[str, SiteChromeModel]]:
|
|
79
|
+
"""
|
|
80
|
+
If learn=True and chrome_models is None: mine from `pages` first, then extract.
|
|
81
|
+
If chrome_models provided: use them (skip mining).
|
|
82
|
+
If learn=False and no models: extract with heuristics/DVDF only (STCE off).
|
|
83
|
+
"""
|
|
84
|
+
page_list = list(pages)
|
|
85
|
+
cfg = config or PipelineConfig()
|
|
86
|
+
|
|
87
|
+
models: dict[str, SiteChromeModel] = {}
|
|
88
|
+
if chrome_models is not None:
|
|
89
|
+
models = chrome_models
|
|
90
|
+
elif learn and cfg.enable_stce:
|
|
91
|
+
models = learn_chrome(page_list, config=cfg, path_prefix_depth=path_prefix_depth)
|
|
92
|
+
|
|
93
|
+
results: list[BatchResult] = []
|
|
94
|
+
for page in page_list:
|
|
95
|
+
model = (
|
|
96
|
+
select_model_for_url(models, page.url, path_prefix_depth=path_prefix_depth)
|
|
97
|
+
if models and cfg.enable_stce
|
|
98
|
+
else None
|
|
99
|
+
)
|
|
100
|
+
extractor = ChromeRAG(config=cfg, site_chrome=model)
|
|
101
|
+
result = extractor.extract(page.html, url=page.url)
|
|
102
|
+
results.append(
|
|
103
|
+
BatchResult(
|
|
104
|
+
page_id=page.page_id,
|
|
105
|
+
url=page.url,
|
|
106
|
+
result=result,
|
|
107
|
+
chrome_group=model.group_key if model else None,
|
|
108
|
+
stce_applied=bool(model and model.signatures),
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
return results, models
|
chromerag/cli.py
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""ChromeRAG CLI — HTML → RAG Markdown (fetch is not a core concern).
|
|
2
|
+
|
|
3
|
+
Subcommands:
|
|
4
|
+
extract — single HTML file → markdown
|
|
5
|
+
learn — mine site-chrome model from a directory of HTML fixtures
|
|
6
|
+
batch — learn-then-extract (or extract with a saved chrome model)
|
|
7
|
+
|
|
8
|
+
URL fetching is intentionally omitted from the default path. Use `poc.fetch_pages`
|
|
9
|
+
or your own crawler, then point ChromeRAG at the saved HTML.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import json
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from chromerag.batch import BatchPage, learn_chrome, learn_then_extract
|
|
20
|
+
from chromerag.config import ContentPriority, PipelineConfig, Strictness
|
|
21
|
+
from chromerag.extractor import ChromeRAG
|
|
22
|
+
from chromerag.site_chrome import load_chrome_models, save_chrome_models
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _build_config(args: argparse.Namespace) -> PipelineConfig:
|
|
26
|
+
overrides = dict(
|
|
27
|
+
enable_dvdf=not getattr(args, "no_dvdf", False),
|
|
28
|
+
enable_schema=not getattr(args, "no_schema", False),
|
|
29
|
+
enable_tables=not getattr(args, "no_tables", False),
|
|
30
|
+
enable_stce=not getattr(args, "no_stce", False),
|
|
31
|
+
inject_heading_paths=getattr(args, "heading_paths", False),
|
|
32
|
+
)
|
|
33
|
+
if getattr(args, "priority", None):
|
|
34
|
+
return PipelineConfig.from_priority(args.priority, **overrides)
|
|
35
|
+
if getattr(args, "strictness", None):
|
|
36
|
+
return PipelineConfig.from_strictness(args.strictness, **overrides)
|
|
37
|
+
return PipelineConfig.from_priority(ContentPriority.BALANCED, **overrides)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _add_shared_flags(p: argparse.ArgumentParser) -> None:
|
|
41
|
+
p.add_argument(
|
|
42
|
+
"--priority",
|
|
43
|
+
choices=[c.value for c in ContentPriority],
|
|
44
|
+
default=None,
|
|
45
|
+
help="precision | coverage | balanced (default)",
|
|
46
|
+
)
|
|
47
|
+
p.add_argument(
|
|
48
|
+
"--strictness",
|
|
49
|
+
choices=[s.value for s in Strictness],
|
|
50
|
+
default=None,
|
|
51
|
+
help="Legacy alias for priority",
|
|
52
|
+
)
|
|
53
|
+
p.add_argument("--no-dvdf", action="store_true")
|
|
54
|
+
p.add_argument("--no-schema", action="store_true")
|
|
55
|
+
p.add_argument("--no-tables", action="store_true")
|
|
56
|
+
p.add_argument("--no-stce", action="store_true")
|
|
57
|
+
p.add_argument("--heading-paths", action="store_true")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _load_fixture_dir(raw_dir: Path) -> list[BatchPage]:
|
|
61
|
+
pages: list[BatchPage] = []
|
|
62
|
+
for meta_path in sorted(raw_dir.glob("*.meta.json")):
|
|
63
|
+
if meta_path.name.startswith("_"):
|
|
64
|
+
continue
|
|
65
|
+
meta = json.loads(meta_path.read_text(encoding="utf-8"))
|
|
66
|
+
html_path = raw_dir / f"{meta['id']}.html"
|
|
67
|
+
if not html_path.exists():
|
|
68
|
+
continue
|
|
69
|
+
pages.append(
|
|
70
|
+
BatchPage(
|
|
71
|
+
html=html_path.read_text(encoding="utf-8", errors="ignore"),
|
|
72
|
+
url=meta.get("url"),
|
|
73
|
+
page_id=meta["id"],
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
if not pages:
|
|
77
|
+
# Fallback: bare *.html without meta
|
|
78
|
+
for html_path in sorted(raw_dir.glob("*.html")):
|
|
79
|
+
pages.append(
|
|
80
|
+
BatchPage(
|
|
81
|
+
html=html_path.read_text(encoding="utf-8", errors="ignore"),
|
|
82
|
+
url=None,
|
|
83
|
+
page_id=html_path.stem,
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
return pages
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def cmd_extract(args: argparse.Namespace) -> int:
|
|
90
|
+
path = Path(args.html)
|
|
91
|
+
if not path.exists():
|
|
92
|
+
sys.stderr.write(
|
|
93
|
+
f"File not found: {path}\n"
|
|
94
|
+
"ChromeRAG extracts HTML files only. Fetch pages with your crawler "
|
|
95
|
+
"or: python -m poc.fetch_pages\n"
|
|
96
|
+
)
|
|
97
|
+
return 2
|
|
98
|
+
html = path.read_text(encoding="utf-8", errors="ignore")
|
|
99
|
+
cfg = _build_config(args)
|
|
100
|
+
model = None
|
|
101
|
+
if args.chrome_model and not args.no_stce:
|
|
102
|
+
models = load_chrome_models(args.chrome_model)
|
|
103
|
+
from chromerag.site_chrome import select_model_for_url
|
|
104
|
+
|
|
105
|
+
model = select_model_for_url(models, args.url)
|
|
106
|
+
extractor = ChromeRAG(config=cfg, site_chrome=model)
|
|
107
|
+
result = extractor.extract(html, url=args.url)
|
|
108
|
+
|
|
109
|
+
for warning in result.warnings:
|
|
110
|
+
sys.stderr.write(f"WARNING: {warning}\n")
|
|
111
|
+
|
|
112
|
+
if args.output:
|
|
113
|
+
Path(args.output).write_text(result.markdown, encoding="utf-8")
|
|
114
|
+
else:
|
|
115
|
+
sys.stdout.write(result.markdown)
|
|
116
|
+
|
|
117
|
+
if args.json_meta:
|
|
118
|
+
meta = {
|
|
119
|
+
"url": result.url,
|
|
120
|
+
"tokens_estimate": result.tokens_estimate,
|
|
121
|
+
"n_blocks_in": result.n_blocks_in,
|
|
122
|
+
"n_blocks_kept": result.n_blocks_kept,
|
|
123
|
+
"n_tables": result.n_tables,
|
|
124
|
+
"front_matter": result.front_matter,
|
|
125
|
+
"warnings": result.warnings,
|
|
126
|
+
"input_quality": result.input_quality,
|
|
127
|
+
"diagnostics": result.diagnostics,
|
|
128
|
+
}
|
|
129
|
+
sys.stderr.write(json.dumps(meta, indent=2) + "\n")
|
|
130
|
+
# Non-zero only on hard failures; thin JS shells warn but still exit 0 so
|
|
131
|
+
# pipelines can log and decide. Use --fail-on-thin to hard-fail.
|
|
132
|
+
if getattr(args, "fail_on_thin", False) and result.input_quality.get("is_thin"):
|
|
133
|
+
return 3
|
|
134
|
+
return 0
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def cmd_learn(args: argparse.Namespace) -> int:
|
|
138
|
+
pages = _load_fixture_dir(Path(args.html_dir))
|
|
139
|
+
if len(pages) < 2:
|
|
140
|
+
sys.stderr.write("Need at least 2 HTML pages to learn chrome patterns.\n")
|
|
141
|
+
return 2
|
|
142
|
+
cfg = _build_config(args)
|
|
143
|
+
models = learn_chrome(
|
|
144
|
+
pages,
|
|
145
|
+
config=cfg,
|
|
146
|
+
min_pages=args.min_pages,
|
|
147
|
+
path_prefix_depth=args.path_prefix_depth,
|
|
148
|
+
)
|
|
149
|
+
out = Path(args.output)
|
|
150
|
+
save_chrome_models(models, out)
|
|
151
|
+
active = {k: len(m.signatures) for k, m in models.items() if m.signatures}
|
|
152
|
+
sys.stderr.write(
|
|
153
|
+
json.dumps(
|
|
154
|
+
{
|
|
155
|
+
"pages": len(pages),
|
|
156
|
+
"groups": {k: m.n_pages for k, m in models.items()},
|
|
157
|
+
"chrome_signatures": active,
|
|
158
|
+
"wrote": str(out),
|
|
159
|
+
},
|
|
160
|
+
indent=2,
|
|
161
|
+
)
|
|
162
|
+
+ "\n"
|
|
163
|
+
)
|
|
164
|
+
return 0
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def cmd_batch(args: argparse.Namespace) -> int:
|
|
168
|
+
"""Learn-then-extract (default) or extract with a pre-learned chrome model."""
|
|
169
|
+
pages = _load_fixture_dir(Path(args.html_dir))
|
|
170
|
+
if not pages:
|
|
171
|
+
sys.stderr.write(f"No HTML fixtures in {args.html_dir}\n")
|
|
172
|
+
return 2
|
|
173
|
+
cfg = _build_config(args)
|
|
174
|
+
out_dir = Path(args.output_dir)
|
|
175
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
176
|
+
|
|
177
|
+
chrome_models = None
|
|
178
|
+
learn = True
|
|
179
|
+
if args.chrome_model:
|
|
180
|
+
chrome_models = load_chrome_models(args.chrome_model)
|
|
181
|
+
learn = False
|
|
182
|
+
if args.no_learn:
|
|
183
|
+
learn = False
|
|
184
|
+
|
|
185
|
+
results, models = learn_then_extract(
|
|
186
|
+
pages,
|
|
187
|
+
config=cfg,
|
|
188
|
+
chrome_models=chrome_models,
|
|
189
|
+
learn=learn,
|
|
190
|
+
path_prefix_depth=args.path_prefix_depth,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
if args.save_chrome:
|
|
194
|
+
save_chrome_models(models, args.save_chrome)
|
|
195
|
+
|
|
196
|
+
summary = []
|
|
197
|
+
for br in results:
|
|
198
|
+
pid = br.page_id or "page"
|
|
199
|
+
page_out = out_dir / pid
|
|
200
|
+
page_out.mkdir(parents=True, exist_ok=True)
|
|
201
|
+
(page_out / "chromerag.md").write_text(br.result.markdown, encoding="utf-8")
|
|
202
|
+
# UI compat alias
|
|
203
|
+
(page_out / "smart_rag_extractor.md").write_text(br.result.markdown, encoding="utf-8")
|
|
204
|
+
iq = br.result.input_quality or {}
|
|
205
|
+
row = {
|
|
206
|
+
"id": pid,
|
|
207
|
+
"url": br.url,
|
|
208
|
+
"tokens": br.result.tokens_estimate,
|
|
209
|
+
"stce_applied": br.stce_applied,
|
|
210
|
+
"chrome_group": br.chrome_group,
|
|
211
|
+
"stce_removed": br.result.diagnostics.get("stce_removed", 0),
|
|
212
|
+
"priority": br.result.diagnostics.get("content_priority"),
|
|
213
|
+
"json_ld": br.result.diagnostics.get("schema_sources", {}).get("json_ld_count", 0),
|
|
214
|
+
"input_thin": bool(iq.get("is_thin")),
|
|
215
|
+
"likely_js_shell": bool(iq.get("likely_js_shell")),
|
|
216
|
+
"warnings": list(br.result.warnings or []),
|
|
217
|
+
}
|
|
218
|
+
summary.append(row)
|
|
219
|
+
flag = " THIN" if row["input_thin"] else ""
|
|
220
|
+
if row["likely_js_shell"]:
|
|
221
|
+
flag = " JS_SHELL"
|
|
222
|
+
print(
|
|
223
|
+
f"{pid:28s} tok={row['tokens']:6d} stce={row['stce_applied']} "
|
|
224
|
+
f"rm={row['stce_removed']} jsonld={row['json_ld']}{flag}"
|
|
225
|
+
)
|
|
226
|
+
for warning in br.result.warnings or []:
|
|
227
|
+
sys.stderr.write(f"WARNING [{pid}]: {warning}\n")
|
|
228
|
+
|
|
229
|
+
(out_dir / "batch_summary.json").write_text(json.dumps(summary, indent=2), encoding="utf-8")
|
|
230
|
+
return 0
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def main(argv: list[str] | None = None) -> int:
|
|
234
|
+
parser = argparse.ArgumentParser(
|
|
235
|
+
prog="chromerag",
|
|
236
|
+
description=(
|
|
237
|
+
"ChromeRAG: ingest-time elimination of site template noise "
|
|
238
|
+
"for enterprise web RAG. Pass HTML files — do not use as a crawler."
|
|
239
|
+
),
|
|
240
|
+
)
|
|
241
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
242
|
+
|
|
243
|
+
p_ex = sub.add_parser("extract", help="Extract one HTML file to Markdown")
|
|
244
|
+
p_ex.add_argument("html", help="Path to .html file")
|
|
245
|
+
p_ex.add_argument("-o", "--output", help="Write markdown to path")
|
|
246
|
+
p_ex.add_argument("--url", help="Canonical URL (for page-type + STCE group)")
|
|
247
|
+
p_ex.add_argument("--chrome-model", help="Path to learned chrome JSON from `chromerag learn`")
|
|
248
|
+
p_ex.add_argument("--json-meta", action="store_true")
|
|
249
|
+
p_ex.add_argument(
|
|
250
|
+
"--fail-on-thin",
|
|
251
|
+
action="store_true",
|
|
252
|
+
help="Exit 3 when input HTML looks thin / like a JS shell (still prints WARNINGs by default)",
|
|
253
|
+
)
|
|
254
|
+
_add_shared_flags(p_ex)
|
|
255
|
+
p_ex.set_defaults(func=cmd_extract)
|
|
256
|
+
|
|
257
|
+
p_learn = sub.add_parser(
|
|
258
|
+
"learn",
|
|
259
|
+
help="Mine site-chrome patterns from a directory of HTML (run BEFORE batch extract)",
|
|
260
|
+
)
|
|
261
|
+
p_learn.add_argument("html_dir", help="Directory with *.html (+ optional *.meta.json)")
|
|
262
|
+
p_learn.add_argument("-o", "--output", required=True, help="Write chrome model JSON here")
|
|
263
|
+
p_learn.add_argument("--min-pages", type=int, default=3)
|
|
264
|
+
p_learn.add_argument("--path-prefix-depth", type=int, default=1)
|
|
265
|
+
_add_shared_flags(p_learn)
|
|
266
|
+
p_learn.set_defaults(func=cmd_learn)
|
|
267
|
+
|
|
268
|
+
p_batch = sub.add_parser(
|
|
269
|
+
"batch",
|
|
270
|
+
help="Learn-then-extract a directory (or extract with --chrome-model / --no-learn)",
|
|
271
|
+
)
|
|
272
|
+
p_batch.add_argument("html_dir", help="Directory with HTML fixtures")
|
|
273
|
+
p_batch.add_argument("-o", "--output-dir", required=True, help="Output directory")
|
|
274
|
+
p_batch.add_argument(
|
|
275
|
+
"--chrome-model",
|
|
276
|
+
help="Use a previously learned chrome model (skips mining)",
|
|
277
|
+
)
|
|
278
|
+
p_batch.add_argument(
|
|
279
|
+
"--save-chrome",
|
|
280
|
+
help="Also write the mined chrome model to this path",
|
|
281
|
+
)
|
|
282
|
+
p_batch.add_argument(
|
|
283
|
+
"--no-learn",
|
|
284
|
+
action="store_true",
|
|
285
|
+
help="Skip STCE mining; heuristics + DVDF only",
|
|
286
|
+
)
|
|
287
|
+
p_batch.add_argument("--path-prefix-depth", type=int, default=1)
|
|
288
|
+
_add_shared_flags(p_batch)
|
|
289
|
+
p_batch.set_defaults(func=cmd_batch)
|
|
290
|
+
|
|
291
|
+
args = parser.parse_args(argv)
|
|
292
|
+
return int(args.func(args))
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
if __name__ == "__main__":
|
|
296
|
+
raise SystemExit(main())
|
chromerag/config.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Pipeline configuration — manageable strictness knobs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import Enum
|
|
7
|
+
|
|
8
|
+
from chromerag.density import DensityConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ContentPriority(str, Enum):
|
|
12
|
+
"""User-facing tradeoff: clean vectors vs keeping more relevant text.
|
|
13
|
+
|
|
14
|
+
PRECISION — Prefer correctness of extracted text. Aggressively drop chrome;
|
|
15
|
+
may lose some relevant chunks (lower content recall).
|
|
16
|
+
COVERAGE — Prefer not losing relevant chunks. Tolerates more residual noise
|
|
17
|
+
(higher content recall, higher noise retention).
|
|
18
|
+
BALANCED — Default enterprise RAG compromise.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
PRECISION = "precision"
|
|
22
|
+
COVERAGE = "coverage"
|
|
23
|
+
BALANCED = "balanced"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Strictness(str, Enum):
|
|
27
|
+
"""Internal aggressiveness presets (mapped from ContentPriority)."""
|
|
28
|
+
|
|
29
|
+
LENIENT = "lenient" # Prefer recall — keep more text
|
|
30
|
+
BALANCED = "balanced" # Default for enterprise RAG
|
|
31
|
+
AGGRESSIVE = "aggressive" # Prefer precision — more chrome gone
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class PageType(str, Enum):
|
|
35
|
+
DOCS = "docs"
|
|
36
|
+
PRICING = "pricing"
|
|
37
|
+
MARKETING = "marketing"
|
|
38
|
+
HUB = "hub"
|
|
39
|
+
UNKNOWN = "unknown"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
_PRIORITY_TO_STRICTNESS = {
|
|
43
|
+
ContentPriority.PRECISION: Strictness.AGGRESSIVE,
|
|
44
|
+
ContentPriority.COVERAGE: Strictness.LENIENT,
|
|
45
|
+
ContentPriority.BALANCED: Strictness.BALANCED,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def priority_to_strictness(priority: ContentPriority | str) -> Strictness:
|
|
50
|
+
if isinstance(priority, str):
|
|
51
|
+
priority = ContentPriority(priority)
|
|
52
|
+
return _PRIORITY_TO_STRICTNESS[priority]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class PipelineConfig:
|
|
57
|
+
"""All tunables in one place for CLI / UI / experiments."""
|
|
58
|
+
|
|
59
|
+
strictness: Strictness = Strictness.BALANCED
|
|
60
|
+
content_priority: ContentPriority = ContentPriority.BALANCED
|
|
61
|
+
page_type: PageType = PageType.UNKNOWN
|
|
62
|
+
|
|
63
|
+
enable_schema: bool = True
|
|
64
|
+
enable_tables: bool = True
|
|
65
|
+
enable_dvdf: bool = True
|
|
66
|
+
enable_stce: bool = True # applied only when a SiteChromeModel is provided
|
|
67
|
+
inject_heading_paths: bool = False
|
|
68
|
+
|
|
69
|
+
# Density
|
|
70
|
+
min_chars: int = 40
|
|
71
|
+
max_link_density: float = 0.55
|
|
72
|
+
min_text_density: float = 0.12
|
|
73
|
+
|
|
74
|
+
# Chrome / size guards
|
|
75
|
+
max_noise_block_chars: int = 4000 # do not strip wrappers larger than this via class heuristics
|
|
76
|
+
max_rejected_drop_chars: int = 400 # density/DVDF rejected nodes dropped only if shorter
|
|
77
|
+
|
|
78
|
+
# DVDF
|
|
79
|
+
dvdf_threshold: float = 0.42
|
|
80
|
+
|
|
81
|
+
# STCE (site-template chrome elimination)
|
|
82
|
+
stce_min_pages: int = 5
|
|
83
|
+
stce_frequency: float = 0.80 # appear in ≥80% of site pages
|
|
84
|
+
stce_max_block_chars: int = 1500 # never STCE-drop long shared content blocks
|
|
85
|
+
stce_require_chrome_features: bool = True # frequency alone is not enough
|
|
86
|
+
|
|
87
|
+
def density_config(self) -> DensityConfig:
|
|
88
|
+
return DensityConfig(
|
|
89
|
+
min_chars=self.min_chars,
|
|
90
|
+
max_link_density=self.max_link_density,
|
|
91
|
+
min_text_density=self.min_text_density,
|
|
92
|
+
prefer_main=True,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
@classmethod
|
|
96
|
+
def from_priority(
|
|
97
|
+
cls,
|
|
98
|
+
priority: ContentPriority | str = ContentPriority.BALANCED,
|
|
99
|
+
page_type: PageType | str = PageType.UNKNOWN,
|
|
100
|
+
**overrides: object,
|
|
101
|
+
) -> PipelineConfig:
|
|
102
|
+
"""Build config from the user-facing precision/coverage flag."""
|
|
103
|
+
if isinstance(priority, str):
|
|
104
|
+
priority = ContentPriority(priority)
|
|
105
|
+
cfg = cls.from_strictness(
|
|
106
|
+
priority_to_strictness(priority),
|
|
107
|
+
page_type,
|
|
108
|
+
**overrides,
|
|
109
|
+
)
|
|
110
|
+
cfg.content_priority = priority
|
|
111
|
+
return cfg
|
|
112
|
+
|
|
113
|
+
@classmethod
|
|
114
|
+
def from_strictness(
|
|
115
|
+
cls,
|
|
116
|
+
strictness: Strictness | str = Strictness.BALANCED,
|
|
117
|
+
page_type: PageType | str = PageType.UNKNOWN,
|
|
118
|
+
**overrides: object,
|
|
119
|
+
) -> PipelineConfig:
|
|
120
|
+
if isinstance(strictness, str):
|
|
121
|
+
strictness = Strictness(strictness)
|
|
122
|
+
if isinstance(page_type, str):
|
|
123
|
+
page_type = PageType(page_type)
|
|
124
|
+
|
|
125
|
+
# Keep content_priority aligned with strictness when callers use from_strictness
|
|
126
|
+
priority = ContentPriority.BALANCED
|
|
127
|
+
if strictness == Strictness.AGGRESSIVE:
|
|
128
|
+
priority = ContentPriority.PRECISION
|
|
129
|
+
elif strictness == Strictness.LENIENT:
|
|
130
|
+
priority = ContentPriority.COVERAGE
|
|
131
|
+
|
|
132
|
+
base = cls(strictness=strictness, content_priority=priority, page_type=page_type)
|
|
133
|
+
|
|
134
|
+
if strictness == Strictness.LENIENT:
|
|
135
|
+
base.max_link_density = 0.70
|
|
136
|
+
base.min_text_density = 0.08
|
|
137
|
+
base.min_chars = 25
|
|
138
|
+
base.dvdf_threshold = 0.55
|
|
139
|
+
base.max_rejected_drop_chars = 250
|
|
140
|
+
base.stce_frequency = 0.85
|
|
141
|
+
base.enable_dvdf = True
|
|
142
|
+
elif strictness == Strictness.AGGRESSIVE:
|
|
143
|
+
base.max_link_density = 0.40
|
|
144
|
+
base.min_text_density = 0.18
|
|
145
|
+
base.min_chars = 50
|
|
146
|
+
base.dvdf_threshold = 0.32
|
|
147
|
+
base.max_rejected_drop_chars = 600
|
|
148
|
+
base.stce_frequency = 0.55
|
|
149
|
+
base.stce_max_block_chars = 3500
|
|
150
|
+
else: # BALANCED
|
|
151
|
+
pass
|
|
152
|
+
|
|
153
|
+
# Page-type nudges (on top of strictness)
|
|
154
|
+
if page_type == PageType.MARKETING:
|
|
155
|
+
base.max_link_density = min(base.max_link_density, 0.50)
|
|
156
|
+
base.dvdf_threshold = min(base.dvdf_threshold, 0.38)
|
|
157
|
+
elif page_type == PageType.DOCS:
|
|
158
|
+
base.max_link_density = max(base.max_link_density, 0.60) # docs side nav is linky but keep body
|
|
159
|
+
base.enable_dvdf = True
|
|
160
|
+
elif page_type == PageType.PRICING:
|
|
161
|
+
base.enable_tables = True
|
|
162
|
+
base.max_rejected_drop_chars = min(base.max_rejected_drop_chars, 300)
|
|
163
|
+
|
|
164
|
+
for key, value in overrides.items():
|
|
165
|
+
if not hasattr(base, key):
|
|
166
|
+
raise ValueError(f"Unknown PipelineConfig field: {key}")
|
|
167
|
+
setattr(base, key, value)
|
|
168
|
+
return base
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def infer_page_type(url: str | None, html: str = "") -> PageType:
|
|
172
|
+
blob = f"{url or ''} {html[:2000]}".lower()
|
|
173
|
+
if any(k in blob for k in ("/pricing", "pricing", "on-demand", "price-list")):
|
|
174
|
+
return PageType.PRICING
|
|
175
|
+
if any(k in blob for k in ("/docs", "documentation", "userguide", "developer", "/learn/")):
|
|
176
|
+
return PageType.DOCS
|
|
177
|
+
if any(k in blob for k in ("/products", "/product/", "solutions", "platform")):
|
|
178
|
+
return PageType.MARKETING
|
|
179
|
+
if any(k in blob for k in ("/blog", "resources", "customers")):
|
|
180
|
+
return PageType.HUB
|
|
181
|
+
return PageType.UNKNOWN
|