pi-unsloth-webtools 0.2.3 → 0.2.4
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/engines.ts +35 -5
- package/package.json +1 -1
- package/web-fetch.ts +44 -22
package/engines.ts
CHANGED
|
@@ -472,6 +472,28 @@ async function httpPost(
|
|
|
472
472
|
return httpFetch(url, { ...options, method: "POST", body: new URLSearchParams(data).toString() });
|
|
473
473
|
}
|
|
474
474
|
|
|
475
|
+
const MAX_ENGINE_RESPONSE_BYTES = 5 * 1024 * 1024;
|
|
476
|
+
|
|
477
|
+
async function readBodyCapped(response: Response): Promise<string | null> {
|
|
478
|
+
const declared = Number(response.headers.get("content-length") ?? "0");
|
|
479
|
+
if (declared > MAX_ENGINE_RESPONSE_BYTES) return null;
|
|
480
|
+
if (!response.body) return "";
|
|
481
|
+
const reader = response.body.getReader();
|
|
482
|
+
const chunks: Uint8Array[] = [];
|
|
483
|
+
let total = 0;
|
|
484
|
+
while (true) {
|
|
485
|
+
const { done, value } = await reader.read();
|
|
486
|
+
if (done) break;
|
|
487
|
+
total += value.length;
|
|
488
|
+
if (total > MAX_ENGINE_RESPONSE_BYTES) {
|
|
489
|
+
await reader.cancel();
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
chunks.push(value);
|
|
493
|
+
}
|
|
494
|
+
return new TextDecoder("utf-8").decode(Buffer.concat(chunks));
|
|
495
|
+
}
|
|
496
|
+
|
|
475
497
|
async function httpFetch(
|
|
476
498
|
url: string,
|
|
477
499
|
options: {
|
|
@@ -505,13 +527,18 @@ async function httpFetch(
|
|
|
505
527
|
signal: AbortSignal.any(signals),
|
|
506
528
|
});
|
|
507
529
|
} catch (err) {
|
|
508
|
-
if (err instanceof DOMException && err.name === "TimeoutError")
|
|
509
|
-
|
|
510
|
-
}
|
|
530
|
+
if (err instanceof DOMException && err.name === "TimeoutError") throw new SearchTimeoutError();
|
|
531
|
+
if (err instanceof DOMException && err.name === "AbortError") throw new SearchCancelled();
|
|
511
532
|
throw err;
|
|
512
533
|
}
|
|
513
534
|
if (response.status !== 200) return null;
|
|
514
|
-
|
|
535
|
+
try {
|
|
536
|
+
return await readBodyCapped(response);
|
|
537
|
+
} catch (err) {
|
|
538
|
+
if (err instanceof DOMException && err.name === "TimeoutError") throw new SearchTimeoutError();
|
|
539
|
+
if (err instanceof DOMException && err.name === "AbortError") throw new SearchCancelled();
|
|
540
|
+
throw err;
|
|
541
|
+
}
|
|
515
542
|
}
|
|
516
543
|
|
|
517
544
|
const DUCKDUCKGO: Engine = {
|
|
@@ -792,6 +819,7 @@ export async function autoTextSearch(
|
|
|
792
819
|
const aggregator = new ResultsAggregator();
|
|
793
820
|
const ctx: EngineContext = { region: "us-en", safesearch: "moderate" };
|
|
794
821
|
let timedOut = false;
|
|
822
|
+
let cancelled = false;
|
|
795
823
|
const uniqueProviders = new Set(engines.map((e) => e.provider)).size;
|
|
796
824
|
const maxWorkers = Math.min(uniqueProviders, Math.ceil(maxResults / 10) + 1);
|
|
797
825
|
let i = 0;
|
|
@@ -804,7 +832,8 @@ export async function autoTextSearch(
|
|
|
804
832
|
seenProviders.add(engine.provider);
|
|
805
833
|
}
|
|
806
834
|
} catch (e) {
|
|
807
|
-
if (e instanceof
|
|
835
|
+
if (e instanceof SearchCancelled) cancelled = true;
|
|
836
|
+
if (e instanceof SearchTimeoutError) timedOut = true;
|
|
808
837
|
}
|
|
809
838
|
};
|
|
810
839
|
while (i < engines.length) {
|
|
@@ -818,6 +847,7 @@ export async function autoTextSearch(
|
|
|
818
847
|
}
|
|
819
848
|
}
|
|
820
849
|
await Promise.allSettled(pending);
|
|
850
|
+
if (cancelled) throw new SearchCancelled();
|
|
821
851
|
const results = rankResults(aggregator.extractDicts(), query);
|
|
822
852
|
if (results.length) return results.slice(0, maxResults);
|
|
823
853
|
if (timedOut) throw new SearchTimeoutError();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-unsloth-webtools",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Pi extension: web_search and web_fetch tools ported from the Unsloth Studio codebase (DuckDuckGo search, SSRF-safe direct fetching, HTML-to-Markdown extraction)",
|
|
6
6
|
"main": "index.ts",
|
package/web-fetch.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type WebsitePolicy,
|
|
11
11
|
} from "./web-access.ts";
|
|
12
12
|
import { htmlToMarkdown } from "./html-to-md.ts";
|
|
13
|
+
import { INVALID_CHARREFS } from "./entities.ts";
|
|
13
14
|
import { extractPdfText, PdfParseError } from "./pdf.ts";
|
|
14
15
|
import { randomUserAgent } from "./user-agents.ts";
|
|
15
16
|
|
|
@@ -97,14 +98,17 @@ const ASCII_TEXT_BYTES = new Set<number>([
|
|
|
97
98
|
0x1b,
|
|
98
99
|
]);
|
|
99
100
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
101
|
+
export class FetchCancelledError extends Error {
|
|
102
|
+
constructor() {
|
|
103
|
+
super("cancelled");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export class FetchTimeoutError extends Error {
|
|
108
|
+
constructor() {
|
|
109
|
+
super("timed out");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
108
112
|
|
|
109
113
|
export interface FetchPageOptions {
|
|
110
114
|
timeoutMs?: number;
|
|
@@ -145,6 +149,8 @@ export interface HopOptions {
|
|
|
145
149
|
maxBytes: number;
|
|
146
150
|
maxPdfBytes: number;
|
|
147
151
|
inactivityMs: number;
|
|
152
|
+
deadlineMs?: number;
|
|
153
|
+
nowMs?: () => number;
|
|
148
154
|
signal?: AbortSignal;
|
|
149
155
|
}
|
|
150
156
|
|
|
@@ -339,8 +345,8 @@ function decodeSingleByte(bytes: Buffer, cp1252: boolean): string {
|
|
|
339
345
|
for (const byte of bytes) {
|
|
340
346
|
if (byte < 0x80) {
|
|
341
347
|
out += String.fromCharCode(byte);
|
|
342
|
-
} else if (cp1252 && byte in
|
|
343
|
-
out +=
|
|
348
|
+
} else if (cp1252 && byte in INVALID_CHARREFS) {
|
|
349
|
+
out += INVALID_CHARREFS[byte];
|
|
344
350
|
} else {
|
|
345
351
|
out += String.fromCharCode(byte);
|
|
346
352
|
}
|
|
@@ -431,7 +437,7 @@ function fetchBudgetExceeded(
|
|
|
431
437
|
}
|
|
432
438
|
|
|
433
439
|
|
|
434
|
-
function requestHop(opts: HopOptions): Promise<HopResponse> {
|
|
440
|
+
export function requestHop(opts: HopOptions): Promise<HopResponse> {
|
|
435
441
|
return new Promise((resolve, reject) => {
|
|
436
442
|
const url = opts.url;
|
|
437
443
|
const transport = url.protocol === "https:" ? https : http;
|
|
@@ -446,6 +452,13 @@ function requestHop(opts: HopOptions): Promise<HopResponse> {
|
|
|
446
452
|
lookup: (_host, _opts, callback) =>
|
|
447
453
|
callback(null, [{ address: opts.pinnedIp, family: opts.family }]),
|
|
448
454
|
};
|
|
455
|
+
let settled = false;
|
|
456
|
+
const settle = (action: () => void) => {
|
|
457
|
+
if (settled) return;
|
|
458
|
+
settled = true;
|
|
459
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
460
|
+
action();
|
|
461
|
+
};
|
|
449
462
|
const request = transport.request(options, (res: IncomingMessage) => {
|
|
450
463
|
const chunks: Buffer[] = [];
|
|
451
464
|
let total = 0;
|
|
@@ -454,17 +467,27 @@ function requestHop(opts: HopOptions): Promise<HopResponse> {
|
|
|
454
467
|
let extendedForPdf = false;
|
|
455
468
|
const finish = (err: string | null, body: Buffer) => {
|
|
456
469
|
settle(() => {
|
|
457
|
-
if (err)
|
|
458
|
-
|
|
470
|
+
if (err) {
|
|
471
|
+
if (err === "cancelled") reject(new FetchCancelledError());
|
|
472
|
+
else if (err === "timed out") reject(new FetchTimeoutError());
|
|
473
|
+
else reject(new Error(err));
|
|
474
|
+
} else {
|
|
459
475
|
resolve({
|
|
460
476
|
status: res.statusCode ?? 0,
|
|
461
477
|
headers: res.headers as Record<string, string | string[] | undefined>,
|
|
462
478
|
body,
|
|
463
479
|
});
|
|
480
|
+
}
|
|
464
481
|
});
|
|
465
482
|
};
|
|
466
483
|
res.on("data", (chunk: Buffer) => {
|
|
467
484
|
if (settled) return;
|
|
485
|
+
const now = opts.nowMs ?? Date.now;
|
|
486
|
+
if (opts.deadlineMs !== undefined && now() >= opts.deadlineMs) {
|
|
487
|
+
res.destroy();
|
|
488
|
+
finish("timed out", Buffer.concat(chunks));
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
468
491
|
if (!declaredPdf && !extendedForPdf && total + chunk.length > opts.maxBytes) {
|
|
469
492
|
if (hasPdfMagic(Buffer.concat(chunks))) {
|
|
470
493
|
limit = opts.maxPdfBytes;
|
|
@@ -488,16 +511,9 @@ function requestHop(opts: HopOptions): Promise<HopResponse> {
|
|
|
488
511
|
res.on("end", () => finish(null, Buffer.concat(chunks)));
|
|
489
512
|
res.on("error", (err) => finish(err.message, Buffer.concat(chunks)));
|
|
490
513
|
});
|
|
491
|
-
const onAbort = () => request.destroy(new
|
|
514
|
+
const onAbort = () => request.destroy(new FetchCancelledError());
|
|
492
515
|
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
493
|
-
|
|
494
|
-
const settle = (action: () => void) => {
|
|
495
|
-
if (settled) return;
|
|
496
|
-
settled = true;
|
|
497
|
-
opts.signal?.removeEventListener("abort", onAbort);
|
|
498
|
-
action();
|
|
499
|
-
};
|
|
500
|
-
request.on("timeout", () => request.destroy(new Error("timed out")));
|
|
516
|
+
request.on("timeout", () => request.destroy(new FetchTimeoutError()));
|
|
501
517
|
request.on("error", (err) => settle(() => reject(err)));
|
|
502
518
|
request.end();
|
|
503
519
|
});
|
|
@@ -557,9 +573,15 @@ export async function fetchUrlRaw(
|
|
|
557
573
|
maxBytes,
|
|
558
574
|
maxPdfBytes,
|
|
559
575
|
inactivityMs: inactivity,
|
|
576
|
+
deadlineMs: deadline,
|
|
577
|
+
nowMs: now,
|
|
560
578
|
signal,
|
|
561
579
|
});
|
|
562
580
|
} catch (err) {
|
|
581
|
+
if (err instanceof FetchCancelledError)
|
|
582
|
+
return { error: "Failed to fetch URL: cancelled.", body: "", contentType: "" };
|
|
583
|
+
if (err instanceof FetchTimeoutError)
|
|
584
|
+
return { error: "Failed to fetch URL: timed out.", body: "", contentType: "" };
|
|
563
585
|
const message = err instanceof Error ? err.message : String(err);
|
|
564
586
|
if (message === "cancelled")
|
|
565
587
|
return { error: "Failed to fetch URL: cancelled.", body: "", contentType: "" };
|