rustai 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.
- rustai-0.1.0/.gitignore +32 -0
- rustai-0.1.0/CHANGELOG.md +162 -0
- rustai-0.1.0/CONTRIBUTING.md +58 -0
- rustai-0.1.0/Cargo.lock +2286 -0
- rustai-0.1.0/Cargo.toml +78 -0
- rustai-0.1.0/LICENSE-APACHE +202 -0
- rustai-0.1.0/LICENSE-MIT +21 -0
- rustai-0.1.0/PKG-INFO +485 -0
- rustai-0.1.0/README.md +455 -0
- rustai-0.1.0/examples/bench.rs +86 -0
- rustai-0.1.0/examples/profile.rs +68 -0
- rustai-0.1.0/examples/research.py +55 -0
- rustai-0.1.0/notebooks/colab_oneshot.ipynb +286 -0
- rustai-0.1.0/notebooks/colab_oneshot.py +259 -0
- rustai-0.1.0/notebooks/colab_quickstart.ipynb +673 -0
- rustai-0.1.0/pyproject.toml +46 -0
- rustai-0.1.0/python/rustai/__init__.py +102 -0
- rustai-0.1.0/python/rustai/_rustai.pyi +256 -0
- rustai-0.1.0/python/rustai/py.typed +0 -0
- rustai-0.1.0/rustfmt.toml +6 -0
- rustai-0.1.0/src/denoise/mod.rs +549 -0
- rustai-0.1.0/src/error.rs +103 -0
- rustai-0.1.0/src/http/browser.rs +99 -0
- rustai-0.1.0/src/http/mod.rs +1110 -0
- rustai-0.1.0/src/http/robots.rs +223 -0
- rustai-0.1.0/src/lib.rs +109 -0
- rustai-0.1.0/src/parse/dom.rs +487 -0
- rustai-0.1.0/src/parse/index.rs +581 -0
- rustai-0.1.0/src/parse/markdown.rs +732 -0
- rustai-0.1.0/src/parse/meta.rs +200 -0
- rustai-0.1.0/src/parse/mod.rs +687 -0
- rustai-0.1.0/src/pipeline.rs +193 -0
- rustai-0.1.0/src/py.rs +1042 -0
- rustai-0.1.0/src/rank/bm25.rs +150 -0
- rustai-0.1.0/src/rank/density.rs +216 -0
- rustai-0.1.0/src/rank/mod.rs +9 -0
- rustai-0.1.0/src/rank/slimmer.rs +613 -0
- rustai-0.1.0/src/search/academic.rs +482 -0
- rustai-0.1.0/src/search/duckduckgo.rs +152 -0
- rustai-0.1.0/src/search/feeds.rs +460 -0
- rustai-0.1.0/src/search/mod.rs +442 -0
- rustai-0.1.0/src/search/searxng.rs +151 -0
- rustai-0.1.0/src/search/wikipedia.rs +140 -0
- rustai-0.1.0/src/text.rs +156 -0
- rustai-0.1.0/tests/conftest.py +19 -0
- rustai-0.1.0/tests/integration.rs +206 -0
- rustai-0.1.0/tests/test_extract.py +234 -0
- rustai-0.1.0/tests/test_network.py +255 -0
- rustai-0.1.0/tests/test_slim.py +110 -0
- rustai-0.1.0/tests/test_utils.py +182 -0
rustai-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Rust
|
|
2
|
+
/target/
|
|
3
|
+
**/*.rs.bk
|
|
4
|
+
*.pdb
|
|
5
|
+
|
|
6
|
+
# Python
|
|
7
|
+
__pycache__/
|
|
8
|
+
*.py[cod]
|
|
9
|
+
*.so
|
|
10
|
+
*.pyd
|
|
11
|
+
.venv/
|
|
12
|
+
venv/
|
|
13
|
+
build/
|
|
14
|
+
dist/
|
|
15
|
+
*.egg-info/
|
|
16
|
+
.pytest_cache/
|
|
17
|
+
.mypy_cache/
|
|
18
|
+
.ruff_cache/
|
|
19
|
+
|
|
20
|
+
# maturin
|
|
21
|
+
/wheels/
|
|
22
|
+
/rustai-*.tar.gz
|
|
23
|
+
/rustai-*.whl
|
|
24
|
+
|
|
25
|
+
# Editors and OS
|
|
26
|
+
.DS_Store
|
|
27
|
+
.idea/
|
|
28
|
+
.vscode/
|
|
29
|
+
*.swp
|
|
30
|
+
/rustai-report.txt
|
|
31
|
+
/rustai-logs/
|
|
32
|
+
/wheelhouse/
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project uses
|
|
5
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **Extraction stopped early on pages whose content root scores onto a
|
|
12
|
+
fragment.** The guard against a bad root asked whether it was *starved* —
|
|
13
|
+
under five percent of the document's text — which catches a root that
|
|
14
|
+
collapsed to nothing and misses every root that merely stopped early. That
|
|
15
|
+
is the common failure: a docs page split into sibling sections gives a root
|
|
16
|
+
holding one of them, comfortably above the threshold and comfortably wrong.
|
|
17
|
+
The container is now extracted too and the larger result kept, both having
|
|
18
|
+
been through the same denoiser. Measured over 34 cached pages: 20% more text
|
|
19
|
+
overall, nothing regressed, no page reclassified. `python.org`'s asyncio
|
|
20
|
+
reference goes from 19.5% of its text to 90.6%, `doc.rust-lang.org`'s `Vec`
|
|
21
|
+
from 48% to 87.5%. A gate skips the second walk when the root already covers
|
|
22
|
+
the container, which keeps throughput where it was (1,796 docs/s against
|
|
23
|
+
1,820).
|
|
24
|
+
- The chosen content root is no longer re-judged by the boilerplate rules
|
|
25
|
+
before being walked. It arrives already decided, and an article container
|
|
26
|
+
that still holds the navigation it is about to drop reads as a link farm by
|
|
27
|
+
raw link density — rejecting it discarded the document to save the part that
|
|
28
|
+
was leaving anyway.
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
- **Data tables were being dropped, five different ways.** A table of national
|
|
32
|
+
GDP figures survived none of them, and each cause hid the next:
|
|
33
|
+
- `header` is a chrome token, and it matches inside `sticky-header-multi` —
|
|
34
|
+
the class Wikipedia puts on every sortable table — exactly as it does
|
|
35
|
+
inside `site-header`. Compounds scoped to a component (`sticky-`, `row-`,
|
|
36
|
+
`column-`, `table-`) are now stripped before the vocabulary sees them;
|
|
37
|
+
page-scoped ones still read as boilerplate.
|
|
38
|
+
- Markup weight counted attribute payload in full, so one serialised JSON
|
|
39
|
+
blob per element (334 KB of markup around 7.5 KB of text) made a data
|
|
40
|
+
table look like an ad slot. An attribute value now contributes at most 128
|
|
41
|
+
bytes.
|
|
42
|
+
- Link density and text-to-markup ratio ask whether a container reads like
|
|
43
|
+
prose. A data table answers no however good it is: cells hold a word, and
|
|
44
|
+
a header row that cites its sources is mostly links. Table elements are
|
|
45
|
+
exempt from both; the vocabulary still catches an ad wherever it sits.
|
|
46
|
+
- `is_data_table` rejected anything over 4,000 nodes, and a 223-row table
|
|
47
|
+
comes to 4,522. The fallback for a rejected table is to walk it as
|
|
48
|
+
ordinary blocks, which emits nothing at all — cells are too short to
|
|
49
|
+
survive as paragraphs — so a cap set near real tables deleted the output
|
|
50
|
+
rather than degrading it.
|
|
51
|
+
- Index detection read a linked cell as a listing entry, classifying data
|
|
52
|
+
tables as front pages. Anchors inside a table with header cells are no
|
|
53
|
+
longer counted; tables *without* header cells still are, because that is
|
|
54
|
+
how Hacker News lays out its front page.
|
|
55
|
+
|
|
56
|
+
### Added
|
|
57
|
+
|
|
58
|
+
- **Linux wheel builds needed `libclang` and nobody said so.** BoringSSL's
|
|
59
|
+
binding crate build-depends on `bindgen` as well as `cmake`, so a container
|
|
60
|
+
without clang fails with "Unable to find libclang" — a message that names
|
|
61
|
+
neither the crate nor the dependency that wanted it. The release workflow now
|
|
62
|
+
installs it, and both Linux targets build on manylinux 2_28 because bindgen
|
|
63
|
+
needs libclang 9+, newer than the CentOS 7 image can offer.
|
|
64
|
+
|
|
65
|
+
### Added
|
|
66
|
+
|
|
67
|
+
- **Scholarly providers**: `arxiv`, `openalex` and `crossref`, all keyless and
|
|
68
|
+
free. OpenAlex abstracts are stored as an inverted index and are reconstructed
|
|
69
|
+
into readable text; arXiv results point at abstract pages rather than PDFs.
|
|
70
|
+
`contact_email` opts into the OpenAlex/Crossref polite pool.
|
|
71
|
+
- **Index-page extraction.** A front page, archive or aggregator defeats
|
|
72
|
+
article extraction for the same reason navigation is boilerplate everywhere
|
|
73
|
+
else — except there the link density is the point. Such pages now return an
|
|
74
|
+
inventory instead: titles, URLs, standfirsts and section headings, on
|
|
75
|
+
`Article.links`, with `Article.kind` saying which you got. Detection measures
|
|
76
|
+
how much of the page's prose belongs to a link rather than whether article
|
|
77
|
+
extraction failed, so a listing whose cards carry summaries is still
|
|
78
|
+
recognised. 12 of 12 correct across real news front pages, aggregators,
|
|
79
|
+
encyclopaedia articles, papers, READMEs and specs; ~1% of extraction time,
|
|
80
|
+
and `index_mode="never"` disables it.
|
|
81
|
+
- `index:<url>` as a search provider: a site with no feed and no sitemap is
|
|
82
|
+
still collectable.
|
|
83
|
+
- `extract_many` in the Python API: batch denoising across the rayon pool with
|
|
84
|
+
the GIL released. ~4x a loop over `extract`, ~30x `trafilatura`.
|
|
85
|
+
|
|
86
|
+
- **Proxy rotation** (`proxies`): one client per proxy, rotated round-robin.
|
|
87
|
+
A TLS fingerprint says what a client is; an address says who it is, and
|
|
88
|
+
IP-reputation blocking is what fingerprinting cannot touch.
|
|
89
|
+
- **`Retry-After` is honoured** in place of the backoff curve, capped by
|
|
90
|
+
`max_retry_after` — past which a delay is a refusal, not a wait.
|
|
91
|
+
- **Cookie persistence** (`cookie_file`, `Client.save_cookies()`): clearance
|
|
92
|
+
cookies are the expensive part of getting past a bot wall, and throwing them
|
|
93
|
+
away at process exit means paying for them again.
|
|
94
|
+
|
|
95
|
+
### Fixed
|
|
96
|
+
|
|
97
|
+
- **Extraction on pages that defeat the HTML parser.** `tl` does not insert the
|
|
98
|
+
implied end tags a browser would, so an unclosed `<p>`, `<span>`, `<li>` or
|
|
99
|
+
`<table>` can end up owning the rest of the document — and on one real
|
|
100
|
+
Wikipedia article it did, producing a single 54,000-token "paragraph"
|
|
101
|
+
containing the whole page. Four guards now hold, each of which is also just
|
|
102
|
+
correct on well-formed markup:
|
|
103
|
+
- an element is only inline if it carries no block-level content;
|
|
104
|
+
- a `<p>`, `<li>` or `<blockquote>` holding blocks is walked, not flattened;
|
|
105
|
+
- a `<table>` holding headings or sections is laid out, not tabulated;
|
|
106
|
+
- a node holding the majority of a document's text is never boilerplate,
|
|
107
|
+
whatever its class or `role` says.
|
|
108
|
+
- **Subtree size was misreported.** `text_ratio`, one of the three headline
|
|
109
|
+
denoising signals, read `tl`'s per-tag source slice — which collapses to the
|
|
110
|
+
opening tag alone when the parser cannot match a close, reporting 28 bytes for
|
|
111
|
+
an element holding 180 KB. Markup weight is now accumulated directly.
|
|
112
|
+
- `Unit.text` disagreed with `Unit.markdown` for tables — it carried the source
|
|
113
|
+
subtree's text rather than what was written — so a mis-nested table reported
|
|
114
|
+
3,870 characters of content behind 53 tokens of output, and every measurement
|
|
115
|
+
downstream inherited the error.
|
|
116
|
+
- The boilerplate vocabulary missed plurals: `reference` did not match
|
|
117
|
+
`references`, so Wikipedia citation lists survived and ate context budgets.
|
|
118
|
+
High-precision terms are now conclusive at any size, since a reference list or
|
|
119
|
+
comment thread runs far past the length guard that keeps the rest honest.
|
|
120
|
+
- JS-gate detection used an absolute text threshold, so a 350 KB front page
|
|
121
|
+
carrying 510 characters of text — plainly an app shell — was never escalated
|
|
122
|
+
to the headless renderer. The test is now the text-to-markup ratio.
|
|
123
|
+
- HTML-level redirects (`<meta http-equiv="refresh">` and `location.replace`)
|
|
124
|
+
are now followed, bounded to two hops and gated on a zero delay and a small
|
|
125
|
+
body. Sites that canonicalise URLs in the browser previously extracted to
|
|
126
|
+
nothing — they returned a valid `200` whose body was a redirect stub.
|
|
127
|
+
|
|
128
|
+
## [0.1.0] — 2026-09-04
|
|
129
|
+
|
|
130
|
+
First release.
|
|
131
|
+
|
|
132
|
+
### Added
|
|
133
|
+
|
|
134
|
+
- **Search router** over free providers — DuckDuckGo, the Wikipedia API, any
|
|
135
|
+
SearXNG instance, RSS/Atom feeds and XML sitemaps — queried concurrently and
|
|
136
|
+
combined with reciprocal rank fusion. URLs are canonicalised before fusion, so
|
|
137
|
+
the same page found by three providers counts once and ranks higher for it.
|
|
138
|
+
- **Fetcher** with Chrome TLS/JA3 and HTTP/2 fingerprint emulation via `wreq`,
|
|
139
|
+
plus `robots.txt` enforcement, `Crawl-delay`, per-host pacing, a concurrency
|
|
140
|
+
ceiling, streamed body caps, exponential backoff, and charset detection that
|
|
141
|
+
handles legacy encodings such as EUC-KR.
|
|
142
|
+
- **Denoiser** combining structural priors, link density and text-to-HTML ratio
|
|
143
|
+
with a Readability-style content score, emitting clean Markdown units that
|
|
144
|
+
each carry their heading breadcrumb and token cost.
|
|
145
|
+
- **Context slimmer** — BM25 at unit granularity, an information-density score,
|
|
146
|
+
and greedy MMR selection with an overlap-coefficient redundancy penalty, under
|
|
147
|
+
a token budget that covers the rendered output rather than just the units.
|
|
148
|
+
- **Python bindings** (PyO3, `abi3-py39`): `Client`, `research`, `extract`,
|
|
149
|
+
`slim`, and helpers, all releasing the GIL for the duration of the call.
|
|
150
|
+
- Optional headless-Chrome fallback (`browser` feature) for JS-gated pages,
|
|
151
|
+
triggered only after a static fetch has demonstrably returned an empty shell.
|
|
152
|
+
|
|
153
|
+
### Notes
|
|
154
|
+
|
|
155
|
+
- `reqwest-impersonate` and its successor `rquest` are both fully yanked on
|
|
156
|
+
crates.io; this project uses their maintained continuation, `wreq`.
|
|
157
|
+
- `tl`'s `simd` feature requires a nightly toolchain and is exposed as the
|
|
158
|
+
opt-in `nightly-simd` feature. Text scanning in this crate is SIMD-accelerated
|
|
159
|
+
on stable regardless, via `memchr`.
|
|
160
|
+
|
|
161
|
+
[Unreleased]: https://github.com/imhyensuk/rustai/compare/v0.1.0...HEAD
|
|
162
|
+
[0.1.0]: https://github.com/imhyensuk/rustai/releases/tag/v0.1.0
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thanks for taking a look. Bug reports and pull requests are both welcome.
|
|
4
|
+
|
|
5
|
+
## Getting set up
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
git clone https://github.com/imhyensuk/rustai
|
|
9
|
+
cd rustai
|
|
10
|
+
python -m venv .venv && source .venv/bin/activate
|
|
11
|
+
pip install maturin pytest
|
|
12
|
+
maturin develop --release
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## The checks CI runs
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
cargo fmt --all --check
|
|
19
|
+
cargo clippy --all-targets -- -D warnings
|
|
20
|
+
cargo test --all-targets
|
|
21
|
+
cargo test --doc
|
|
22
|
+
pytest
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Do not run `cargo clippy --all-features`: the `nightly-simd` feature enables
|
|
26
|
+
`tl`'s `portable_simd`, which does not build on stable. Lint each real
|
|
27
|
+
combination instead — that is what `.github/workflows/ci.yml` does.
|
|
28
|
+
|
|
29
|
+
The live-internet tests are off by default and are not part of CI:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pytest --network
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Notes on the code
|
|
36
|
+
|
|
37
|
+
- **The denoiser earns its keep on real pages, not synthetic ones.** If you
|
|
38
|
+
change a heuristic in `src/denoise/`, add a test with markup from a page that
|
|
39
|
+
actually broke — a fixture in `tests/integration.rs` is the right home.
|
|
40
|
+
- **Every heuristic constant should be able to explain itself.** The thresholds
|
|
41
|
+
in `DenoiseConfig`, `SlimConfig` and `density.rs` all carry a comment saying
|
|
42
|
+
what they trade off. Please keep that up when you add one.
|
|
43
|
+
- **The token budget is a guarantee, not an estimate.** `rank::slim` renders,
|
|
44
|
+
measures, and trims until the output fits. If you change the renderer, keep
|
|
45
|
+
that loop correct.
|
|
46
|
+
- **Politeness defaults stay on.** `robots.txt`, per-host pacing and body caps
|
|
47
|
+
are the reason this library is safe to hand to someone. Making them
|
|
48
|
+
configurable is fine; making them default-off is not.
|
|
49
|
+
|
|
50
|
+
## Benchmarks
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
python benches/corpus.py /tmp/rustai-corpus
|
|
54
|
+
cargo run --release --example bench -- /tmp/rustai-corpus
|
|
55
|
+
python benches/benchmark.py
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
If a change moves those numbers, say so in the pull request.
|