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/LICENSE +661 -0
- package/README.md +101 -0
- package/ROADMAP.md +77 -0
- package/engines.ts +855 -0
- package/entities.ts +2399 -0
- package/html-to-md.ts +1085 -0
- package/index.ts +86 -0
- package/package.json +64 -0
- package/pdf.ts +717 -0
- package/web-access.ts +375 -0
- package/web-fetch.ts +689 -0
- package/web-search.ts +112 -0
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.
|