pi-unsloth-webtools 0.1.0

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.
package/README.md ADDED
@@ -0,0 +1,101 @@
1
+ # pi-unsloth-webtools
2
+
3
+ A [pi](https://github.com/earendil-works/pi-coding-agent) extension providing `web_search` and
4
+ `web_fetch` tools, ported from the Unsloth Studio codebase
5
+ ([`unslothai/unsloth`](https://github.com/unslothai/unsloth), `studio/backend/core/inference/`).
6
+
7
+ ## What it does
8
+
9
+ ### web_search
10
+
11
+ Mirrors Unsloth Studio's `web_search` tool:
12
+
13
+ - Searches exactly like Studio's pinned `ddgs==9.14.4` `DDGS.text()`: the same seven engines
14
+ (duckduckgo, brave, google, mojeek, yahoo, yandex, wikipedia; bing is disabled upstream),
15
+ the same provider-deduplication, href-dedupe aggregator with frequency ordering, and the
16
+ same `SimpleFilterRanker` re-ranking. Formats results identically: `Title:` / `URL:` /
17
+ `Snippet:` blocks separated by `---`, ending with the hint to pass `{"url": "<URL>"}` to
18
+ read a full page.
19
+ - Accepts an optional `url` parameter; when given, fetches that page's text instead of searching.
20
+ - Rate-limit, timeout, and empty-result messages mirror Studio's `_search_failure_message`.
21
+
22
+ ### web_fetch
23
+
24
+ Port of Studio's `_fetch_page_text` / `_fetch_url_raw` pipeline:
25
+
26
+ - URL scheme normalization (bare hosts like `google.com` become `https://google.com`).
27
+ - URL validation: http/https only, no credentials or encoded hostnames, hostname/port checks.
28
+ - DNS resolution with SSRF protection: every resolved address is validated against
29
+ private/loopback/link-local/CGNAT/documentation/multicast/reserved ranges, then the validated IP
30
+ is pinned for the connection (custom `lookup` + SNI `servername`), so DNS cannot rebind between
31
+ validation and fetch.
32
+ - GitHub repo root pages are rewritten to the unauthenticated README API
33
+ (`Accept: application/vnd.github.raw+json`), falling back to the HTML page on failure.
34
+ - Up to 5 redirect hops, each re-validated and re-resolved against the same rules.
35
+ - 512 KiB download cap (10 MiB for PDFs), overall deadline + per-hop socket timeouts, abort-aware
36
+ (`signal` cancels mid-flight).
37
+ - PDF text extraction via the official MuPDF.js engine (the same C library pymupdf wraps):
38
+ object streams, all filters, ToUnicode fonts, encryption detection, and a
39
+ pymupdf4llm-style markdown layer (headings, bold/italic, code fences, links, tables)
40
+ with Studio's corrupted/incomplete fallback to plain text.
41
+ - Content sniffing: MIME allow/deny, binary magic signatures, PDF magic + text extraction
42
+ (built-in zlib-based extractor, not pymupdf-grade), charset decoding (declared charset, BOM
43
+ sniffing for UTF-8/16/32, cp1252 rescue for mislabeled single-byte pages).
44
+ - HTML → Markdown conversion ported from Studio's dependency-free `_html_to_md.py`: headings,
45
+ links, emphasis, lists, tables, blockquotes, code fences, entity decoding; hidden-element
46
+ stripping (`hidden`, `aria-hidden`, inline styles); `<article>`/`<main>` main-content scoping
47
+ with link-density header stripping; boilerplate-line removal.
48
+ - No page-size budget: fetched pages and PDFs are returned in full (Studio's window-aware
49
+ cap is deliberately dropped; the optional `maxChars` parameter still truncates when given).
50
+ The 512 KiB / 10 MiB download caps still bound the raw fetch.
51
+ - HTML entity decoding replicates CPython's `html.unescape` (full 2,231-entry HTML5 table,
52
+ longest-prefix rule, Windows-1252 numeric mappings), matching Studio byte-for-byte.
53
+
54
+ See `ROADMAP.md` for the remaining gaps (PDF extraction, TLS fingerprinting, proxies).
55
+
56
+ ## Install
57
+
58
+ ```sh
59
+ pi install /path/to/pi-unsloth-webtools
60
+ ```
61
+
62
+ or add `npm:pi-unsloth-webtools` (once published) to the `packages` array in
63
+ `~/.pi/agent/settings.json`.
64
+
65
+ ## Development
66
+
67
+ ```sh
68
+ npm install
69
+ npm run typecheck
70
+ npm test
71
+ ```
72
+
73
+ ## Tests
74
+
75
+ The suite ports Unsloth Studio's own tests for these tools:
76
+
77
+ - `test/html-to-md.test.ts` — hidden-element stripping and main-content scoping (from
78
+ `test_web_fetch_extraction.py`)
79
+ - `test/header-strip.test.ts` — the header link-density suite plus article-vs-main selection
80
+ and boilerplate cases (from `test_web_fetch_extraction.py`)
81
+ - `test/binary-guard.test.ts` — the MIME/magic/charset/PDF matrix (from
82
+ `test_web_fetch_binary_guard.py`)
83
+ - `test/web-search-policy.test.ts` — policy filtering, overfetch, and failure messages
84
+ (from `test_web_access_policy.py`)
85
+ - `test/fetch-flow.test.ts` — GitHub README rewrite, deadline/cancellation, HTML sniffing
86
+ (from `test_web_fetch_extraction.py`; the fetch client is injected via seams)
87
+ - `test/engines.test.ts` — the ddgs engine port: normalizers, the XPath subset, the
88
+ aggregator, the ranker, and the Wikipedia engine with a stubbed fetch
89
+ - `test/pdf-parity.test.ts` — MuPDF engine capabilities: PDF 1.5 object streams,
90
+ ASCII85Decode, font `/Differences` encodings, pymupdf4llm-style headings/links/tables
91
+ - `test/smoke.test.ts` — live network checks against real hosts
92
+
93
+ The seams (`seams.resolve` / `seams.request` / `rawFetch`) replace the network stack
94
+ with fakes, mirroring how the Studio suite monkeypatches `_validate_and_resolve_host`
95
+ and `build_opener`.
96
+
97
+ ## License
98
+
99
+ The ported logic derives from Unsloth Studio
100
+ ([AGPL-3.0-only](https://github.com/unslothai/unsloth/blob/main/studio/LICENSE.AGPL-3.0)), so this
101
+ package is released under the same **AGPL-3.0-only** license.
package/ROADMAP.md ADDED
@@ -0,0 +1,77 @@
1
+ # Roadmap
2
+
3
+ Tracking the remaining gaps between this port and Unsloth Studio's web tools, and the
4
+ planned work to close them.
5
+
6
+ ## Implemented
7
+
8
+ ### PDF text extraction (MuPDF engine)
9
+
10
+ `pdf.ts` uses the official **MuPDF.js** (`mupdf` npm package) — the same C engine that
11
+ PyMuPDF wraps — replacing the earlier minimal built-in extractor:
12
+
13
+ - Full xref handling: tables, cross-reference streams, and PDF 1.5+ object streams
14
+ - All standard stream filters (FlateDecode, ASCII85Decode, LZW, RunLength, DCT, JPX, ...)
15
+ - Font encodings and ToUnicode mapping (non-Latin text extracts correctly)
16
+ - Encryption detection via `needsPassword()` (reported as unreadable, matching pymupdf's
17
+ no-password behavior)
18
+ - The markdown layer replicates pymupdf4llm's algorithm: `IdentifyHeaders` font-size
19
+ heading detection, `get_raw_lines` line reconstruction (tolerance 3, 10% span-join
20
+ delta), `write_text` styling (bold/italic/mono, code fences, bullets, link
21
+ resolution with `%0x`-escaped URIs), and Studio's corrupted/incomplete fallback to
22
+ plain MuPDF text with the exact thresholds from `backend/core/rag/parsers.py`
23
+ - Table detection and pipe-markdown rendering matching pymupdf's `Table.to_markdown`
24
+ shape (`|header|`, `|---|`, detail rows, `Col{i}` fill for empty headers)
25
+
26
+ Known deltas vs pymupdf4llm:
27
+
28
+ - Span-level styling: MuPDF.js's structured-text JSON exposes one font per line, so
29
+ mixed-style lines (one bold word inside a body line) style the whole line instead of
30
+ per-span. Line-level styling matches for homogeneous lines.
31
+ - Superscript/subscript/underline/strikeout/highlight markers are not emitted (the
32
+ JSON does not expose char-level flags).
33
+ - Table detection is a conservative text-grid detector (column-start clustering with
34
+ a 5 pt tolerance, contiguous multi-row bands) instead of PyMuPDF's
35
+ `find_tables()` vector-graphics analysis. Aligned text tables are detected; tables
36
+ defined only by drawn rules without aligned text are not.
37
+
38
+ The minimal extractor remains as an automatic fallback when the `mupdf` package cannot
39
+ be loaded (for example a stripped install).
40
+
41
+ ## Not planned (explicit decisions)
42
+
43
+ ### Proxy support
44
+
45
+ Studio routes requests through urllib's environment proxies and honors
46
+ `UNSLOTH_STUDIO_DISABLE_DNS_PINNING` for enterprise proxies. This port always connects
47
+ directly with DNS pinning. Deliberately out of scope.
48
+
49
+ ### Page size budgets
50
+
51
+ Studio's `_page_char_budget()` sizes fetched pages to the serving model's context window.
52
+ This port deliberately has **no character budget at all**: fetched pages and PDFs are
53
+ returned in full (the PDF page-count cap of 50 pages remains, matching Studio). The raw
54
+ download caps (512 KiB text / 10 MiB PDF) still bound what is fetched. A `maxChars`
55
+ parameter remains available on the tool for callers that want to truncate.
56
+
57
+ ## Known behavioral differences
58
+
59
+ ### Search engine set
60
+
61
+ The port implements ddgs 9.14.4's `DDGS.text()` exactly: the same seven engines
62
+ (duckduckgo, brave, google, mojeek, yahoo, yandex, wikipedia — bing is `disabled` in
63
+ ddgs upstream), the same provider-deduplication, href-dedupe aggregator with
64
+ frequency ordering, and the same `SimpleFilterRanker` re-ranking. Remaining deltas:
65
+
66
+ - **TLS fingerprinting**: ddgs uses `primp` with browser TLS impersonation. Node's
67
+ `fetch` has a different fingerprint, so Google/Brave/Yahoo/Yandex may block or serve
68
+ consent pages more aggressively. When an engine is blocked it simply contributes no
69
+ results, exactly as when ddgs is blocked.
70
+ - **User agents**: ddgs uses `fake_useragent`'s database; the port uses a fixed set of
71
+ browser UAs plus ddgs's own Android Google UA generator.
72
+
73
+ ### Entity decoding
74
+
75
+ `decodeHtmlEntities` replicates CPython's `html.unescape` exactly (full HTML5 table,
76
+ longest-prefix rule, Windows-1252 numeric mappings, invalid-codepoint handling), so the
77
+ HTML-to-Markdown converter and search-result normalization match Studio's output.