@effect-uai/retrieval 0.12.1
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 +21 -0
- package/README.md +64 -0
- package/dist/Chunking.d.mts +48 -0
- package/dist/Chunking.d.mts.map +1 -0
- package/dist/Chunking.mjs +206 -0
- package/dist/Chunking.mjs.map +1 -0
- package/dist/HuggingFaceTokenizer.d.mts +46 -0
- package/dist/HuggingFaceTokenizer.d.mts.map +1 -0
- package/dist/HuggingFaceTokenizer.mjs +75 -0
- package/dist/HuggingFaceTokenizer.mjs.map +1 -0
- package/dist/Rank.d.mts +22 -0
- package/dist/Rank.d.mts.map +1 -0
- package/dist/Rank.mjs +29 -0
- package/dist/Rank.mjs.map +1 -0
- package/dist/index.d.mts +3 -0
- package/dist/index.mjs +3 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/package.json +75 -0
- package/src/Chunking.test.ts +233 -0
- package/src/Chunking.ts +292 -0
- package/src/HuggingFaceTokenizer.ts +119 -0
- package/src/Rank.test.ts +68 -0
- package/src/Rank.ts +38 -0
- package/src/index.ts +2 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Betalyra Sociedade Unipessoal Lda.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# @effect-uai/retrieval
|
|
2
|
+
|
|
3
|
+
Retrieval-pipeline utilities for [`@effect-uai/core`](https://www.npmjs.com/package/@effect-uai/core):
|
|
4
|
+
chunking, rank fusion, and a tokenizer layer.
|
|
5
|
+
|
|
6
|
+
The pieces of a retrieval pipeline that are plain functions rather than
|
|
7
|
+
providers. The capability tags they implement (`Chunker`, `Tokenizer`) live in
|
|
8
|
+
core, so your ingest code never names a strategy.
|
|
9
|
+
|
|
10
|
+
This package exposes:
|
|
11
|
+
|
|
12
|
+
- **`Chunking`** - four chunkers (`recursive`, `sentences`, `markdown`,
|
|
13
|
+
`fixed`), each reporting the offsets every passage came from, so a search hit
|
|
14
|
+
can be traced back to its source. `layer` serves one through the core
|
|
15
|
+
`Chunker` tag; `withTokenizer` sizes chunks by real tokens.
|
|
16
|
+
- **`Rank`** - `rrf`, reciprocal rank fusion, for merging a keyword leg and a
|
|
17
|
+
vector leg whose scores mean nothing to each other.
|
|
18
|
+
- **`HuggingFaceTokenizer`** - the core `Tokenizer` tag over any Hugging Face
|
|
19
|
+
repo with a `tokenizer.json`. Downloading and building are separate, so you
|
|
20
|
+
cache the vocabulary instead of refetching on every boot.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
pnpm add @effect-uai/retrieval @effect-uai/core effect
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The tokenizer needs one optional peer:
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
pnpm add @huggingface/tokenizers
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
ESM-only. Requires `effect@4.x` as a peer.
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import { chunk } from "@effect-uai/core/Chunker"
|
|
40
|
+
import * as Chunking from "@effect-uai/retrieval/Chunking"
|
|
41
|
+
import * as Rank from "@effect-uai/retrieval/Rank"
|
|
42
|
+
|
|
43
|
+
const ingest = Effect.gen(function* () {
|
|
44
|
+
const passages = yield* chunk(document)
|
|
45
|
+
// embed and store
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const chunker = Chunking.layer(Chunking.recursive, { targetSize: 512 })
|
|
49
|
+
|
|
50
|
+
const fused = Rank.rrf([keywordIds, vectorIds], { weights: [1, 2] })
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Docs
|
|
54
|
+
|
|
55
|
+
Full docs: <https://effect-uai.betalyra.com/retrieval/>
|
|
56
|
+
|
|
57
|
+
See [Chunking](https://effect-uai.betalyra.com/retrieval/chunking/),
|
|
58
|
+
[Tokenizers](https://effect-uai.betalyra.com/language-models/tokenizers/), and
|
|
59
|
+
[Agentic search](https://effect-uai.betalyra.com/recipes/agentic-search/) for a
|
|
60
|
+
pipeline using all of it.
|
|
61
|
+
|
|
62
|
+
## License
|
|
63
|
+
|
|
64
|
+
MIT
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Effect, Layer } from "effect";
|
|
2
|
+
import { Chunk, Chunk as Chunk$1, Chunker } from "@effect-uai/core/Chunker";
|
|
3
|
+
import { Tokenizer } from "@effect-uai/core/Tokenizer";
|
|
4
|
+
declare namespace Chunking_d_exports {
|
|
5
|
+
export { Chunk, Measure, Options, Splitter, fixed, layer, markdown, recursive, sentences, withTokenizer };
|
|
6
|
+
}
|
|
7
|
+
/** How chunk size is counted. Chunkers call this on candidate chunks. */
|
|
8
|
+
type Measure = (text: string) => number;
|
|
9
|
+
type Options = {
|
|
10
|
+
/** In units of `measure`, approximately tokens with the default. Default 512. */
|
|
11
|
+
readonly targetSize?: number;
|
|
12
|
+
/** Units of `measure` repeated from the end of the previous chunk. Default 0. */
|
|
13
|
+
readonly overlap?: number;
|
|
14
|
+
/** Default: characters / 4, a rough token estimate for prose. */
|
|
15
|
+
readonly measure?: Measure;
|
|
16
|
+
};
|
|
17
|
+
type Splitter = (text: string, options?: Options) => ReadonlyArray<Chunk$1>;
|
|
18
|
+
/**
|
|
19
|
+
* Blind character windows, sized by calibrating `measure` over the input. Cuts
|
|
20
|
+
* mid-word and mid-sentence, so reach for it when the input has no structure
|
|
21
|
+
* worth respecting or a hard size ceiling matters more than readable passages.
|
|
22
|
+
*/
|
|
23
|
+
declare const fixed: Splitter;
|
|
24
|
+
/**
|
|
25
|
+
* Split on the coarsest separator that makes the pieces fit (blank line,
|
|
26
|
+
* newline, sentence end, word), then refill up to `targetSize`. The default:
|
|
27
|
+
* it keeps related prose together without knowing anything about the format.
|
|
28
|
+
*/
|
|
29
|
+
declare const recursive: Splitter;
|
|
30
|
+
/** Pack whole sentences. Only a sentence longer than `targetSize` is cut. */
|
|
31
|
+
declare const sentences: Splitter;
|
|
32
|
+
/**
|
|
33
|
+
* One chunk per `#`..`######` section, so a heading stays with the prose under
|
|
34
|
+
* it. A section over `targetSize` is split with `recursive` inside its own
|
|
35
|
+
* bounds; a chunk never spans two sections. Headings inside fenced code are
|
|
36
|
+
* ignored; setext (`===` underline) headings are not recognized.
|
|
37
|
+
*/
|
|
38
|
+
declare const markdown: Splitter;
|
|
39
|
+
/** Serve a chunker through the core `Chunker` tag. */
|
|
40
|
+
declare const layer: (chunker: Splitter, options?: Options) => Layer.Layer<Chunker>;
|
|
41
|
+
/**
|
|
42
|
+
* Run a chunker with a real tokenizer as its measure, so `targetSize` and
|
|
43
|
+
* `overlap` count tokens rather than estimate them.
|
|
44
|
+
*/
|
|
45
|
+
declare const withTokenizer: (chunker: Splitter) => (text: string, options?: Omit<Options, "measure">) => Effect.Effect<ReadonlyArray<Chunk$1>, never, Tokenizer>;
|
|
46
|
+
//#endregion
|
|
47
|
+
export { type Chunk, Measure, Options, Splitter, fixed, layer, markdown, recursive, sentences, Chunking_d_exports as t, withTokenizer };
|
|
48
|
+
//# sourceMappingURL=Chunking.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Chunking.d.mts","names":[],"sources":["../src/Chunking.ts"],"mappings":";;;;;;;KAmBY,WAAW;KAEX;;WAED;;WAEA;;WAEA,UAAU;;KAGT,YAAY,cAAc,UAAU,YAAY,cAAc;;;;;;cAkK7D,OAAO;;;;;;cAkBP,WAAW;;cAMX,WAAW;;;;;;;cAgDX,UAAU;;cAYV,QAAK,SAAa,UAAQ,UAAY,YAAU,MAAM,MAAM;;;;;cAO5D,gBAAa,SACd,cAAQ,cAEJ,UACF,KAAK,wBACd,OAAO,OAAO,cAAc,iBAAe"}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
|
|
2
|
+
import { Array, Effect, Layer, Option, Result, pipe } from "effect";
|
|
3
|
+
import { Chunker } from "@effect-uai/core/Chunker";
|
|
4
|
+
import { Tokenizer } from "@effect-uai/core/Tokenizer";
|
|
5
|
+
//#region src/Chunking.ts
|
|
6
|
+
/**
|
|
7
|
+
* Splitting documents into retrievable passages. The chunkers are pure and
|
|
8
|
+
* synchronous; `layer` lifts one into the core `Chunker` tag so a hosted
|
|
9
|
+
* chunking service can take its place without touching callers.
|
|
10
|
+
*
|
|
11
|
+
* Every chunk carries the offsets it came from, so
|
|
12
|
+
* `input.slice(chunk.start, chunk.end) === chunk.text` always holds. Overlap
|
|
13
|
+
* makes chunks share regions, never lose them.
|
|
14
|
+
*
|
|
15
|
+
* `recursive` is the one to reach for by default. `fixed` ignores structure,
|
|
16
|
+
* `sentences` never cuts mid-sentence, `markdown` follows headers.
|
|
17
|
+
*/
|
|
18
|
+
var Chunking_exports = /* @__PURE__ */ __exportAll({
|
|
19
|
+
fixed: () => fixed,
|
|
20
|
+
layer: () => layer,
|
|
21
|
+
markdown: () => markdown,
|
|
22
|
+
recursive: () => recursive,
|
|
23
|
+
sentences: () => sentences,
|
|
24
|
+
withTokenizer: () => withTokenizer
|
|
25
|
+
});
|
|
26
|
+
const approximate = (text) => Math.ceil(text.length / 4);
|
|
27
|
+
const settings = (options) => ({
|
|
28
|
+
target: options?.targetSize ?? 512,
|
|
29
|
+
overlap: options?.overlap ?? 0,
|
|
30
|
+
measure: options?.measure ?? approximate
|
|
31
|
+
});
|
|
32
|
+
const sizeOf = (text, span, s) => s.measure(text.slice(span.start, span.end));
|
|
33
|
+
/** Narrow a span to its non-whitespace core; a blank span drops out. */
|
|
34
|
+
const trim = (text) => (span) => {
|
|
35
|
+
const region = text.slice(span.start, span.end);
|
|
36
|
+
const start = span.start + (region.length - region.trimStart().length);
|
|
37
|
+
const end = span.end - (region.length - region.trimEnd().length);
|
|
38
|
+
return start < end ? Result.succeed({
|
|
39
|
+
start,
|
|
40
|
+
end
|
|
41
|
+
}) : Result.failVoid;
|
|
42
|
+
};
|
|
43
|
+
/** Split a span on every separator match, keeping the trimmed parts between. */
|
|
44
|
+
const splitSpans = (text, span, separator) => {
|
|
45
|
+
const cuts = Array.map([...text.slice(span.start, span.end).matchAll(separator)], (m) => ({
|
|
46
|
+
start: span.start + m.index,
|
|
47
|
+
end: span.start + m.index + m[0].length
|
|
48
|
+
}));
|
|
49
|
+
return pipe(Array.zip([span.start, ...Array.map(cuts, (c) => c.end)], [...Array.map(cuts, (c) => c.start), span.end]), Array.filterMap(([start, end]) => trim(text)({
|
|
50
|
+
start,
|
|
51
|
+
end
|
|
52
|
+
})));
|
|
53
|
+
};
|
|
54
|
+
const PARAGRAPH = /\n[ \t]*(?:\n[ \t]*)+/gu;
|
|
55
|
+
const LINE = /\n/gu;
|
|
56
|
+
const SENTENCE = /(?<=[.!?…][)"'”’]*)\s+/gu;
|
|
57
|
+
const SEPARATORS = [
|
|
58
|
+
PARAGRAPH,
|
|
59
|
+
LINE,
|
|
60
|
+
SENTENCE,
|
|
61
|
+
/\s+/gu
|
|
62
|
+
];
|
|
63
|
+
/** Sentences are split by the time we get here, so oversized ones go to words. */
|
|
64
|
+
const WORD_LEVEL = 3;
|
|
65
|
+
/**
|
|
66
|
+
* `measure` is a black box, so size a character window by calibrating it once
|
|
67
|
+
* over the region instead of probing per chunk.
|
|
68
|
+
*/
|
|
69
|
+
const charBudget = (text, span, s) => {
|
|
70
|
+
const length = span.end - span.start;
|
|
71
|
+
const units = sizeOf(text, span, s);
|
|
72
|
+
return Math.max(1, Math.round(units === 0 ? s.target : s.target * length / units));
|
|
73
|
+
};
|
|
74
|
+
const windows = (span, budget, stride) => Array.unfold(span.start, (start) => start >= span.end ? Option.none() : Option.some([{
|
|
75
|
+
start,
|
|
76
|
+
end: Math.min(start + budget, span.end)
|
|
77
|
+
}, start + budget >= span.end ? span.end : start + stride]));
|
|
78
|
+
/** Break a span down until every part fits: coarsest separator first, then blind. */
|
|
79
|
+
const explode = (text, span, level, s) => {
|
|
80
|
+
if (sizeOf(text, span, s) <= s.target) return [span];
|
|
81
|
+
const separator = SEPARATORS[level];
|
|
82
|
+
if (separator === void 0) {
|
|
83
|
+
const budget = charBudget(text, span, s);
|
|
84
|
+
return windows(span, budget, budget);
|
|
85
|
+
}
|
|
86
|
+
const parts = splitSpans(text, span, separator);
|
|
87
|
+
return parts.length <= 1 ? explode(text, span, level + 1, s) : Array.flatMap(parts, (part) => explode(text, part, level + 1, s));
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* The longest suffix of a chunk worth at most `overlap`. Never the whole
|
|
91
|
+
* chunk, or packing would not advance. Relies on `measure` growing with the
|
|
92
|
+
* text it is given, which every sane measure does.
|
|
93
|
+
*/
|
|
94
|
+
const carry = (text, spans, s) => {
|
|
95
|
+
const tail = Array.drop(spans, 1);
|
|
96
|
+
return s.overlap <= 0 ? [] : pipe(Array.last(tail), Option.map((end) => Array.dropWhile(tail, (span) => sizeOf(text, {
|
|
97
|
+
start: span.start,
|
|
98
|
+
end: end.end
|
|
99
|
+
}, s) > s.overlap)), Option.getOrElse(() => []));
|
|
100
|
+
};
|
|
101
|
+
/** One chunk covering a group of spans verbatim. An empty group yields none. */
|
|
102
|
+
const toChunk = (text, spans) => pipe(Option.all([Array.head(spans), Array.last(spans)]), Option.map(([first, last]) => ({
|
|
103
|
+
text: text.slice(first.start, last.end),
|
|
104
|
+
start: first.start,
|
|
105
|
+
end: last.end
|
|
106
|
+
})), Option.match({
|
|
107
|
+
onNone: () => [],
|
|
108
|
+
onSome: (chunk) => [chunk]
|
|
109
|
+
}));
|
|
110
|
+
/** Greedily fill chunks with consecutive spans, carrying overlap forward. */
|
|
111
|
+
const pack = (text, spans, s) => {
|
|
112
|
+
const packed = Array.reduce(spans, {
|
|
113
|
+
chunks: [],
|
|
114
|
+
current: []
|
|
115
|
+
}, (acc, span) => {
|
|
116
|
+
const first = acc.current[0];
|
|
117
|
+
return first !== void 0 && sizeOf(text, {
|
|
118
|
+
start: first.start,
|
|
119
|
+
end: span.end
|
|
120
|
+
}, s) > s.target ? {
|
|
121
|
+
chunks: [...acc.chunks, ...toChunk(text, acc.current)],
|
|
122
|
+
current: [...carry(text, acc.current, s), span]
|
|
123
|
+
} : {
|
|
124
|
+
chunks: acc.chunks,
|
|
125
|
+
current: [...acc.current, span]
|
|
126
|
+
};
|
|
127
|
+
});
|
|
128
|
+
return [...packed.chunks, ...toChunk(text, packed.current)];
|
|
129
|
+
};
|
|
130
|
+
/** The input's content as a span, or nothing if it is blank. */
|
|
131
|
+
const content = (text) => Array.filterMap([{
|
|
132
|
+
start: 0,
|
|
133
|
+
end: text.length
|
|
134
|
+
}], trim(text));
|
|
135
|
+
/**
|
|
136
|
+
* Blind character windows, sized by calibrating `measure` over the input. Cuts
|
|
137
|
+
* mid-word and mid-sentence, so reach for it when the input has no structure
|
|
138
|
+
* worth respecting or a hard size ceiling matters more than readable passages.
|
|
139
|
+
*/
|
|
140
|
+
const fixed = (text, options) => {
|
|
141
|
+
const s = settings(options);
|
|
142
|
+
return Array.flatMap(content(text), (span) => {
|
|
143
|
+
const budget = charBudget(text, span, s);
|
|
144
|
+
const stride = budget - Math.min(budget - 1, Math.round(s.overlap * budget / s.target));
|
|
145
|
+
return Array.map(windows(span, budget, stride), (w) => ({
|
|
146
|
+
text: text.slice(w.start, w.end),
|
|
147
|
+
start: w.start,
|
|
148
|
+
end: w.end
|
|
149
|
+
}));
|
|
150
|
+
});
|
|
151
|
+
};
|
|
152
|
+
/**
|
|
153
|
+
* Split on the coarsest separator that makes the pieces fit (blank line,
|
|
154
|
+
* newline, sentence end, word), then refill up to `targetSize`. The default:
|
|
155
|
+
* it keeps related prose together without knowing anything about the format.
|
|
156
|
+
*/
|
|
157
|
+
const recursive = (text, options) => {
|
|
158
|
+
const s = settings(options);
|
|
159
|
+
return Array.flatMap(content(text), (span) => pack(text, explode(text, span, 0, s), s));
|
|
160
|
+
};
|
|
161
|
+
/** Pack whole sentences. Only a sentence longer than `targetSize` is cut. */
|
|
162
|
+
const sentences = (text, options) => {
|
|
163
|
+
const s = settings(options);
|
|
164
|
+
return Array.flatMap(content(text), (span) => pack(text, Array.flatMap(splitSpans(text, span, PARAGRAPH), (paragraph) => Array.flatMap(splitSpans(text, paragraph, SENTENCE), (sentence) => explode(text, sentence, WORD_LEVEL, s))), s));
|
|
165
|
+
};
|
|
166
|
+
const HEADER = /^#{1,6}[ \t]+\S.*$/gmu;
|
|
167
|
+
const FENCE = /^[ \t]{0,3}(?:```|~~~).*$/gmu;
|
|
168
|
+
/** Fenced code regions, where a `#` line is content rather than a heading. */
|
|
169
|
+
const fenced = (region) => pipe(Array.map([...region.matchAll(FENCE)], (m) => m.index), Array.chunksOf(2), Array.map(([open, close]) => ({
|
|
170
|
+
start: open,
|
|
171
|
+
end: close ?? region.length
|
|
172
|
+
})));
|
|
173
|
+
/** Spans running from each header to the next, plus any preamble. */
|
|
174
|
+
const sections = (text, span) => {
|
|
175
|
+
const region = text.slice(span.start, span.end);
|
|
176
|
+
const code = fenced(region);
|
|
177
|
+
const starts = Array.dedupe([span.start, ...pipe([...region.matchAll(HEADER)], Array.filter((m) => !Array.some(code, (f) => m.index > f.start && m.index < f.end)), Array.map((m) => span.start + m.index))]);
|
|
178
|
+
return Array.filterMap(starts, (start, i) => trim(text)({
|
|
179
|
+
start,
|
|
180
|
+
end: starts[i + 1] ?? span.end
|
|
181
|
+
}));
|
|
182
|
+
};
|
|
183
|
+
/**
|
|
184
|
+
* One chunk per `#`..`######` section, so a heading stays with the prose under
|
|
185
|
+
* it. A section over `targetSize` is split with `recursive` inside its own
|
|
186
|
+
* bounds; a chunk never spans two sections. Headings inside fenced code are
|
|
187
|
+
* ignored; setext (`===` underline) headings are not recognized.
|
|
188
|
+
*/
|
|
189
|
+
const markdown = (text, options) => {
|
|
190
|
+
const s = settings(options);
|
|
191
|
+
return Array.flatMap(content(text), (span) => Array.flatMap(sections(text, span), (section) => pack(text, explode(text, section, 0, s), s)));
|
|
192
|
+
};
|
|
193
|
+
/** Serve a chunker through the core `Chunker` tag. */
|
|
194
|
+
const layer = (chunker, options) => Layer.succeed(Chunker, { chunk: (text) => Effect.succeed(chunker(text, options)) });
|
|
195
|
+
/**
|
|
196
|
+
* Run a chunker with a real tokenizer as its measure, so `targetSize` and
|
|
197
|
+
* `overlap` count tokens rather than estimate them.
|
|
198
|
+
*/
|
|
199
|
+
const withTokenizer = (chunker) => (text, options) => Effect.map(Tokenizer, (tokenizer) => chunker(text, {
|
|
200
|
+
...options,
|
|
201
|
+
measure: (part) => tokenizer.encode(part).length
|
|
202
|
+
}));
|
|
203
|
+
//#endregion
|
|
204
|
+
export { fixed, layer, markdown, recursive, sentences, Chunking_exports as t, withTokenizer };
|
|
205
|
+
|
|
206
|
+
//# sourceMappingURL=Chunking.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Chunking.mjs","names":["Arr"],"sources":["../src/Chunking.ts"],"sourcesContent":["/**\n * Splitting documents into retrievable passages. The chunkers are pure and\n * synchronous; `layer` lifts one into the core `Chunker` tag so a hosted\n * chunking service can take its place without touching callers.\n *\n * Every chunk carries the offsets it came from, so\n * `input.slice(chunk.start, chunk.end) === chunk.text` always holds. Overlap\n * makes chunks share regions, never lose them.\n *\n * `recursive` is the one to reach for by default. `fixed` ignores structure,\n * `sentences` never cuts mid-sentence, `markdown` follows headers.\n */\nimport { Array as Arr, Effect, Layer, Option, pipe, Result } from \"effect\"\nimport { type Chunk, Chunker } from \"@effect-uai/core/Chunker\"\nimport { Tokenizer } from \"@effect-uai/core/Tokenizer\"\n\nexport type { Chunk } from \"@effect-uai/core/Chunker\"\n\n/** How chunk size is counted. Chunkers call this on candidate chunks. */\nexport type Measure = (text: string) => number\n\nexport type Options = {\n /** In units of `measure`, approximately tokens with the default. Default 512. */\n readonly targetSize?: number\n /** Units of `measure` repeated from the end of the previous chunk. Default 0. */\n readonly overlap?: number\n /** Default: characters / 4, a rough token estimate for prose. */\n readonly measure?: Measure\n}\n\nexport type Splitter = (text: string, options?: Options) => ReadonlyArray<Chunk>\n\nconst approximate: Measure = (text) => Math.ceil(text.length / 4)\n\ntype Span = { readonly start: number; readonly end: number }\n\ntype Settings = {\n readonly target: number\n readonly overlap: number\n readonly measure: Measure\n}\n\nconst settings = (options: Options | undefined): Settings => ({\n target: options?.targetSize ?? 512,\n overlap: options?.overlap ?? 0,\n measure: options?.measure ?? approximate,\n})\n\n// ---------------------------------------------------------------------------\n// Spans\n// ---------------------------------------------------------------------------\n\nconst sizeOf = (text: string, span: Span, s: Settings): number =>\n s.measure(text.slice(span.start, span.end))\n\n/** Narrow a span to its non-whitespace core; a blank span drops out. */\nconst trim =\n (text: string) =>\n (span: Span): Result.Result<Span, void> => {\n const region = text.slice(span.start, span.end)\n const start = span.start + (region.length - region.trimStart().length)\n const end = span.end - (region.length - region.trimEnd().length)\n return start < end ? Result.succeed({ start, end }) : Result.failVoid\n }\n\n/** Split a span on every separator match, keeping the trimmed parts between. */\nconst splitSpans = (text: string, span: Span, separator: RegExp): ReadonlyArray<Span> => {\n const cuts = Arr.map([...text.slice(span.start, span.end).matchAll(separator)], (m) => ({\n start: span.start + m.index,\n end: span.start + m.index + m[0].length,\n }))\n return pipe(\n Arr.zip(\n [span.start, ...Arr.map(cuts, (c) => c.end)],\n [...Arr.map(cuts, (c) => c.start), span.end],\n ),\n Arr.filterMap(([start, end]) => trim(text)({ start, end })),\n )\n}\n\n// Coarse to fine. Closing quotes may trail the sentence punctuation.\nconst PARAGRAPH = /\\n[ \\t]*(?:\\n[ \\t]*)+/gu\nconst LINE = /\\n/gu\nconst SENTENCE = /(?<=[.!?…][)\"'”’]*)\\s+/gu\nconst WORD = /\\s+/gu\nconst SEPARATORS: ReadonlyArray<RegExp> = [PARAGRAPH, LINE, SENTENCE, WORD]\n\n/** Sentences are split by the time we get here, so oversized ones go to words. */\nconst WORD_LEVEL = 3\n\n/**\n * `measure` is a black box, so size a character window by calibrating it once\n * over the region instead of probing per chunk.\n */\nconst charBudget = (text: string, span: Span, s: Settings): number => {\n const length = span.end - span.start\n const units = sizeOf(text, span, s)\n return Math.max(1, Math.round(units === 0 ? s.target : (s.target * length) / units))\n}\n\nconst windows = (span: Span, budget: number, stride: number): ReadonlyArray<Span> =>\n Arr.unfold(span.start, (start) =>\n start >= span.end\n ? Option.none()\n : Option.some([\n { start, end: Math.min(start + budget, span.end) },\n // Once a window reaches the end, stop: with overlap the strides that\n // follow would only produce windows contained in this one.\n start + budget >= span.end ? span.end : start + stride,\n ] as const),\n )\n\n/** Break a span down until every part fits: coarsest separator first, then blind. */\nconst explode = (text: string, span: Span, level: number, s: Settings): ReadonlyArray<Span> => {\n if (sizeOf(text, span, s) <= s.target) return [span]\n const separator = SEPARATORS[level]\n if (separator === undefined) {\n const budget = charBudget(text, span, s)\n return windows(span, budget, budget)\n }\n const parts = splitSpans(text, span, separator)\n // A separator that does not divide the region is no progress; try the next.\n return parts.length <= 1\n ? explode(text, span, level + 1, s)\n : Arr.flatMap(parts, (part) => explode(text, part, level + 1, s))\n}\n\n/**\n * The longest suffix of a chunk worth at most `overlap`. Never the whole\n * chunk, or packing would not advance. Relies on `measure` growing with the\n * text it is given, which every sane measure does.\n */\nconst carry = (text: string, spans: ReadonlyArray<Span>, s: Settings): ReadonlyArray<Span> => {\n const tail = Arr.drop(spans, 1)\n return s.overlap <= 0\n ? []\n : pipe(\n Arr.last(tail),\n Option.map((end) =>\n Arr.dropWhile(\n tail,\n (span) => sizeOf(text, { start: span.start, end: end.end }, s) > s.overlap,\n ),\n ),\n Option.getOrElse((): ReadonlyArray<Span> => []),\n )\n}\n\n/** One chunk covering a group of spans verbatim. An empty group yields none. */\nconst toChunk = (text: string, spans: ReadonlyArray<Span>): ReadonlyArray<Chunk> =>\n pipe(\n Option.all([Arr.head(spans), Arr.last(spans)]),\n Option.map(([first, last]): Chunk => ({\n text: text.slice(first.start, last.end),\n start: first.start,\n end: last.end,\n })),\n Option.match({ onNone: (): ReadonlyArray<Chunk> => [], onSome: (chunk) => [chunk] }),\n )\n\ntype Packing = {\n readonly chunks: ReadonlyArray<Chunk>\n readonly current: ReadonlyArray<Span>\n}\n\n/** Greedily fill chunks with consecutive spans, carrying overlap forward. */\nconst pack = (text: string, spans: ReadonlyArray<Span>, s: Settings): ReadonlyArray<Chunk> => {\n const packed = Arr.reduce(spans, { chunks: [], current: [] } as Packing, (acc, span) => {\n const first = acc.current[0]\n return first !== undefined && sizeOf(text, { start: first.start, end: span.end }, s) > s.target\n ? {\n chunks: [...acc.chunks, ...toChunk(text, acc.current)],\n current: [...carry(text, acc.current, s), span],\n }\n : { chunks: acc.chunks, current: [...acc.current, span] }\n })\n return [...packed.chunks, ...toChunk(text, packed.current)]\n}\n\n/** The input's content as a span, or nothing if it is blank. */\nconst content = (text: string): ReadonlyArray<Span> =>\n Arr.filterMap([{ start: 0, end: text.length }], trim(text))\n\n// ---------------------------------------------------------------------------\n// Chunkers\n// ---------------------------------------------------------------------------\n\n/**\n * Blind character windows, sized by calibrating `measure` over the input. Cuts\n * mid-word and mid-sentence, so reach for it when the input has no structure\n * worth respecting or a hard size ceiling matters more than readable passages.\n */\nexport const fixed: Splitter = (text, options) => {\n const s = settings(options)\n return Arr.flatMap(content(text), (span) => {\n const budget = charBudget(text, span, s)\n const stride = budget - Math.min(budget - 1, Math.round((s.overlap * budget) / s.target))\n return Arr.map(windows(span, budget, stride), (w) => ({\n text: text.slice(w.start, w.end),\n start: w.start,\n end: w.end,\n }))\n })\n}\n\n/**\n * Split on the coarsest separator that makes the pieces fit (blank line,\n * newline, sentence end, word), then refill up to `targetSize`. The default:\n * it keeps related prose together without knowing anything about the format.\n */\nexport const recursive: Splitter = (text, options) => {\n const s = settings(options)\n return Arr.flatMap(content(text), (span) => pack(text, explode(text, span, 0, s), s))\n}\n\n/** Pack whole sentences. Only a sentence longer than `targetSize` is cut. */\nexport const sentences: Splitter = (text, options) => {\n const s = settings(options)\n return Arr.flatMap(content(text), (span) =>\n pack(\n text,\n Arr.flatMap(splitSpans(text, span, PARAGRAPH), (paragraph) =>\n Arr.flatMap(splitSpans(text, paragraph, SENTENCE), (sentence) =>\n explode(text, sentence, WORD_LEVEL, s),\n ),\n ),\n s,\n ),\n )\n}\n\nconst HEADER = /^#{1,6}[ \\t]+\\S.*$/gmu\nconst FENCE = /^[ \\t]{0,3}(?:```|~~~).*$/gmu\n\n/** Fenced code regions, where a `#` line is content rather than a heading. */\nconst fenced = (region: string): ReadonlyArray<Span> =>\n pipe(\n Arr.map([...region.matchAll(FENCE)], (m) => m.index),\n Arr.chunksOf(2),\n // An unclosed fence swallows the rest of the document, as a renderer does.\n Arr.map(([open, close]) => ({ start: open, end: close ?? region.length })),\n )\n\n/** Spans running from each header to the next, plus any preamble. */\nconst sections = (text: string, span: Span): ReadonlyArray<Span> => {\n const region = text.slice(span.start, span.end)\n const code = fenced(region)\n const starts = Arr.dedupe([\n span.start,\n ...pipe(\n [...region.matchAll(HEADER)],\n Arr.filter((m) => !Arr.some(code, (f) => m.index > f.start && m.index < f.end)),\n Arr.map((m) => span.start + m.index),\n ),\n ])\n return Arr.filterMap(starts, (start, i) => trim(text)({ start, end: starts[i + 1] ?? span.end }))\n}\n\n/**\n * One chunk per `#`..`######` section, so a heading stays with the prose under\n * it. A section over `targetSize` is split with `recursive` inside its own\n * bounds; a chunk never spans two sections. Headings inside fenced code are\n * ignored; setext (`===` underline) headings are not recognized.\n */\nexport const markdown: Splitter = (text, options) => {\n const s = settings(options)\n return Arr.flatMap(content(text), (span) =>\n Arr.flatMap(sections(text, span), (section) => pack(text, explode(text, section, 0, s), s)),\n )\n}\n\n// ---------------------------------------------------------------------------\n// Wiring\n// ---------------------------------------------------------------------------\n\n/** Serve a chunker through the core `Chunker` tag. */\nexport const layer = (chunker: Splitter, options?: Options): Layer.Layer<Chunker> =>\n Layer.succeed(Chunker, { chunk: (text: string) => Effect.succeed(chunker(text, options)) })\n\n/**\n * Run a chunker with a real tokenizer as its measure, so `targetSize` and\n * `overlap` count tokens rather than estimate them.\n */\nexport const withTokenizer =\n (chunker: Splitter) =>\n (\n text: string,\n options?: Omit<Options, \"measure\">,\n ): Effect.Effect<ReadonlyArray<Chunk>, never, Tokenizer> =>\n Effect.map(Tokenizer, (tokenizer) =>\n chunker(text, { ...options, measure: (part) => tokenizer.encode(part).length }),\n )\n"],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Effect, Layer, Redacted, Schema } from "effect";
|
|
2
|
+
import { Tokenizer } from "@effect-uai/core/Tokenizer";
|
|
3
|
+
import { HttpClient } from "effect/unstable/http";
|
|
4
|
+
//#region src/HuggingFaceTokenizer.d.ts
|
|
5
|
+
declare const TokenizerLoadError_base: Schema.Class<TokenizerLoadError, Schema.TaggedStruct<"TokenizerLoadError", {
|
|
6
|
+
readonly model: Schema.optional<Schema.String>;
|
|
7
|
+
readonly reason: Schema.String;
|
|
8
|
+
readonly cause: Schema.optional<Schema.Unknown>;
|
|
9
|
+
}>, import("effect/Cause").YieldableError>;
|
|
10
|
+
/** The vocabulary could not be fetched, parsed, or loaded. */
|
|
11
|
+
declare class TokenizerLoadError extends TokenizerLoadError_base {}
|
|
12
|
+
/**
|
|
13
|
+
* Everything that defines a tokenizer's behaviour, as published. Plain JSON:
|
|
14
|
+
* write it to disk, a row, or your bundle, and decode it back with this schema.
|
|
15
|
+
*/
|
|
16
|
+
declare const Definition: Schema.Struct<{
|
|
17
|
+
/** Contents of `tokenizer.json`. */
|
|
18
|
+
readonly tokenizer: Schema.Unknown;
|
|
19
|
+
/** Contents of `tokenizer_config.json`, absent for repos that publish none. */
|
|
20
|
+
readonly config: Schema.optional<Schema.Unknown>;
|
|
21
|
+
}>;
|
|
22
|
+
type Definition = typeof Definition.Type;
|
|
23
|
+
type Options = {
|
|
24
|
+
/** A Hugging Face repo id, e.g. `"jinaai/jina-embeddings-v3"` or `"Xenova/gpt-4o"`. */
|
|
25
|
+
readonly model: string;
|
|
26
|
+
/** Branch, tag, or commit. Default `"main"`. */
|
|
27
|
+
readonly revision?: string;
|
|
28
|
+
/**
|
|
29
|
+
* A Hugging Face access token. Required for gated repos, where the Hub
|
|
30
|
+
* serves the files only to accounts that have accepted the model's terms
|
|
31
|
+
* (Google's tokenizers among them), and for private ones.
|
|
32
|
+
*/
|
|
33
|
+
readonly token?: Redacted.Redacted;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Fetch a model's definition from the Hugging Face Hub. Cache the result
|
|
37
|
+
* yourself; a repo without a `tokenizer_config.json` yields `config: undefined`.
|
|
38
|
+
*/
|
|
39
|
+
declare const download: (options: Options) => Effect.Effect<Definition, TokenizerLoadError, HttpClient.HttpClient>;
|
|
40
|
+
/** Build the tokenizer from a definition you already hold. No IO. */
|
|
41
|
+
declare const fromDefinition: (definition: Definition) => Layer.Layer<Tokenizer, TokenizerLoadError>;
|
|
42
|
+
/** Download and build in one step. For scripts; production should cache. */
|
|
43
|
+
declare const layer: (options: Options) => Layer.Layer<Tokenizer, TokenizerLoadError, HttpClient.HttpClient>;
|
|
44
|
+
//#endregion
|
|
45
|
+
export { Definition, Options, TokenizerLoadError, download, fromDefinition, layer };
|
|
46
|
+
//# sourceMappingURL=HuggingFaceTokenizer.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HuggingFaceTokenizer.d.mts","names":[],"sources":["../src/HuggingFaceTokenizer.ts"],"mappings":";;;;;;;;;;cAiBa,2BAA2B;;;;;cAY3B,YAAU,OAAA;;;;;;KAOX,oBAAoB,WAAW;KAE/B;;WAED;;WAEA;;;;;;WAMA,QAAQ,SAAS;;;;;;cAkCf,WAAQ,SACV,YACR,OAAO,OAAO,YAAY,oBAAoB,WAAW;;cAO/C,iBAAc,YACb,eACX,MAAM,MAAM,WAAW;;cAsBb,QAAK,SACP,YACR,MAAM,MAAM,WAAW,oBAAoB,WAAW"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Array, Effect, Layer, Redacted, Schema } from "effect";
|
|
2
|
+
import { Tokenizer } from "@effect-uai/core/Tokenizer";
|
|
3
|
+
import { HttpClient, HttpClientRequest } from "effect/unstable/http";
|
|
4
|
+
import { Tokenizer as Tokenizer$1 } from "@huggingface/tokenizers";
|
|
5
|
+
//#region src/HuggingFaceTokenizer.ts
|
|
6
|
+
/**
|
|
7
|
+
* The core `Tokenizer` tag over `@huggingface/tokenizers`, which reads a
|
|
8
|
+
* model's `tokenizer.json` and needs no native build. The package is an
|
|
9
|
+
* optional peer dependency: `pnpm add @huggingface/tokenizers`.
|
|
10
|
+
*
|
|
11
|
+
* Fetching and construction are separate on purpose. `download` gives you
|
|
12
|
+
* plain JSON to keep wherever your application keeps things; `fromDefinition`
|
|
13
|
+
* builds the tokenizer from what you kept. Nothing here touches the filesystem
|
|
14
|
+
* or caches behind your back. `layer` composes the two for scripts happy to
|
|
15
|
+
* download on every run.
|
|
16
|
+
*/
|
|
17
|
+
/** The vocabulary could not be fetched, parsed, or loaded. */
|
|
18
|
+
var TokenizerLoadError = class extends Schema.TaggedError("@effect-uai/retrieval/TokenizerLoadError")("TokenizerLoadError", {
|
|
19
|
+
model: Schema.optional(Schema.String),
|
|
20
|
+
reason: Schema.String,
|
|
21
|
+
cause: Schema.optional(Schema.Unknown)
|
|
22
|
+
}) {};
|
|
23
|
+
/**
|
|
24
|
+
* Everything that defines a tokenizer's behaviour, as published. Plain JSON:
|
|
25
|
+
* write it to disk, a row, or your bundle, and decode it back with this schema.
|
|
26
|
+
*/
|
|
27
|
+
const Definition = Schema.Struct({
|
|
28
|
+
/** Contents of `tokenizer.json`. */
|
|
29
|
+
tokenizer: Schema.Unknown,
|
|
30
|
+
/** Contents of `tokenizer_config.json`, absent for repos that publish none. */
|
|
31
|
+
config: Schema.optional(Schema.Unknown)
|
|
32
|
+
});
|
|
33
|
+
const fetchJson = (options, file) => Effect.gen(function* () {
|
|
34
|
+
const client = yield* HttpClient.HttpClient;
|
|
35
|
+
const url = `https://huggingface.co/${options.model}/resolve/${options.revision ?? "main"}/${file}`;
|
|
36
|
+
const failed = (reason) => (cause) => new TokenizerLoadError({
|
|
37
|
+
model: options.model,
|
|
38
|
+
reason,
|
|
39
|
+
cause
|
|
40
|
+
});
|
|
41
|
+
const request = HttpClientRequest.get(url).pipe(options.token === void 0 ? (self) => self : HttpClientRequest.bearerToken(Redacted.value(options.token)));
|
|
42
|
+
const response = yield* client.execute(request).pipe(Effect.mapError(failed(`could not fetch ${file}`)));
|
|
43
|
+
return response.status >= 400 ? yield* new TokenizerLoadError({
|
|
44
|
+
model: options.model,
|
|
45
|
+
reason: `${url} returned ${response.status}`
|
|
46
|
+
}) : yield* response.json.pipe(Effect.mapError(failed(`${file} is not valid JSON`)));
|
|
47
|
+
});
|
|
48
|
+
/**
|
|
49
|
+
* Fetch a model's definition from the Hugging Face Hub. Cache the result
|
|
50
|
+
* yourself; a repo without a `tokenizer_config.json` yields `config: undefined`.
|
|
51
|
+
*/
|
|
52
|
+
const download = (options) => Effect.all({
|
|
53
|
+
tokenizer: fetchJson(options, "tokenizer.json"),
|
|
54
|
+
config: fetchJson(options, "tokenizer_config.json").pipe(Effect.orElseSucceed(() => void 0))
|
|
55
|
+
});
|
|
56
|
+
/** Build the tokenizer from a definition you already hold. No IO. */
|
|
57
|
+
const fromDefinition = (definition) => Layer.effect(Tokenizer, Effect.try({
|
|
58
|
+
try: () => {
|
|
59
|
+
const hf = new Tokenizer$1(definition.tokenizer, definition.config ?? {});
|
|
60
|
+
return {
|
|
61
|
+
encode: (text) => hf.encode(text).ids,
|
|
62
|
+
decode: (tokens) => tokens.length === 0 ? "" : hf.decode(Array.copy(tokens))
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
catch: (cause) => new TokenizerLoadError({
|
|
66
|
+
reason: "could not load the tokenizer",
|
|
67
|
+
cause
|
|
68
|
+
})
|
|
69
|
+
}));
|
|
70
|
+
/** Download and build in one step. For scripts; production should cache. */
|
|
71
|
+
const layer = (options) => Layer.unwrap(Effect.map(download(options), fromDefinition));
|
|
72
|
+
//#endregion
|
|
73
|
+
export { Definition, TokenizerLoadError, download, fromDefinition, layer };
|
|
74
|
+
|
|
75
|
+
//# sourceMappingURL=HuggingFaceTokenizer.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HuggingFaceTokenizer.mjs","names":["HfTokenizer","Arr"],"sources":["../src/HuggingFaceTokenizer.ts"],"sourcesContent":["/**\n * The core `Tokenizer` tag over `@huggingface/tokenizers`, which reads a\n * model's `tokenizer.json` and needs no native build. The package is an\n * optional peer dependency: `pnpm add @huggingface/tokenizers`.\n *\n * Fetching and construction are separate on purpose. `download` gives you\n * plain JSON to keep wherever your application keeps things; `fromDefinition`\n * builds the tokenizer from what you kept. Nothing here touches the filesystem\n * or caches behind your back. `layer` composes the two for scripts happy to\n * download on every run.\n */\nimport { Array as Arr, Effect, Layer, Redacted, Schema } from \"effect\"\nimport { HttpClient, HttpClientRequest } from \"effect/unstable/http\"\nimport { Tokenizer, type TokenizerService } from \"@effect-uai/core/Tokenizer\"\nimport { Tokenizer as HfTokenizer } from \"@huggingface/tokenizers\"\n\n/** The vocabulary could not be fetched, parsed, or loaded. */\nexport class TokenizerLoadError extends Schema.TaggedError<TokenizerLoadError>(\n \"@effect-uai/retrieval/TokenizerLoadError\",\n)(\"TokenizerLoadError\", {\n model: Schema.optional(Schema.String),\n reason: Schema.String,\n cause: Schema.optional(Schema.Unknown),\n}) {}\n\n/**\n * Everything that defines a tokenizer's behaviour, as published. Plain JSON:\n * write it to disk, a row, or your bundle, and decode it back with this schema.\n */\nexport const Definition = Schema.Struct({\n /** Contents of `tokenizer.json`. */\n tokenizer: Schema.Unknown,\n /** Contents of `tokenizer_config.json`, absent for repos that publish none. */\n config: Schema.optional(Schema.Unknown),\n})\n\nexport type Definition = typeof Definition.Type\n\nexport type Options = {\n /** A Hugging Face repo id, e.g. `\"jinaai/jina-embeddings-v3\"` or `\"Xenova/gpt-4o\"`. */\n readonly model: string\n /** Branch, tag, or commit. Default `\"main\"`. */\n readonly revision?: string\n /**\n * A Hugging Face access token. Required for gated repos, where the Hub\n * serves the files only to accounts that have accepted the model's terms\n * (Google's tokenizers among them), and for private ones.\n */\n readonly token?: Redacted.Redacted\n}\n\nconst fetchJson = (\n options: Options,\n file: string,\n): Effect.Effect<unknown, TokenizerLoadError, HttpClient.HttpClient> =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const url = `https://huggingface.co/${options.model}/resolve/${options.revision ?? \"main\"}/${file}`\n const failed = (reason: string) => (cause: unknown) =>\n new TokenizerLoadError({ model: options.model, reason, cause })\n\n const request = HttpClientRequest.get(url).pipe(\n options.token === undefined\n ? (self) => self\n : HttpClientRequest.bearerToken(Redacted.value(options.token)),\n )\n const response = yield* client\n .execute(request)\n .pipe(Effect.mapError(failed(`could not fetch ${file}`)))\n // 401/403 on a gated repo means no token, or terms not yet accepted.\n return response.status >= 400\n ? yield* new TokenizerLoadError({\n model: options.model,\n reason: `${url} returned ${response.status}`,\n })\n : yield* response.json.pipe(Effect.mapError(failed(`${file} is not valid JSON`)))\n })\n\n/**\n * Fetch a model's definition from the Hugging Face Hub. Cache the result\n * yourself; a repo without a `tokenizer_config.json` yields `config: undefined`.\n */\nexport const download = (\n options: Options,\n): Effect.Effect<Definition, TokenizerLoadError, HttpClient.HttpClient> =>\n Effect.all({\n tokenizer: fetchJson(options, \"tokenizer.json\"),\n config: fetchJson(options, \"tokenizer_config.json\").pipe(Effect.orElseSucceed(() => undefined)),\n })\n\n/** Build the tokenizer from a definition you already hold. No IO. */\nexport const fromDefinition = (\n definition: Definition,\n): Layer.Layer<Tokenizer, TokenizerLoadError> =>\n Layer.effect(\n Tokenizer,\n Effect.try({\n try: (): TokenizerService => {\n const hf = new HfTokenizer(\n definition.tokenizer as object,\n (definition.config ?? {}) as object,\n )\n return {\n encode: (text) => hf.encode(text).ids,\n // The library rejects an empty array, but `encode(\"\")` returns one and\n // `decode` inverts `encode`. Its signature also wants a mutable array,\n // though it mutates nothing.\n decode: (tokens) => (tokens.length === 0 ? \"\" : hf.decode(Arr.copy(tokens))),\n }\n },\n catch: (cause) => new TokenizerLoadError({ reason: \"could not load the tokenizer\", cause }),\n }),\n )\n\n/** Download and build in one step. For scripts; production should cache. */\nexport const layer = (\n options: Options,\n): Layer.Layer<Tokenizer, TokenizerLoadError, HttpClient.HttpClient> =>\n Layer.unwrap(Effect.map(download(options), fromDefinition))\n"],"mappings":";;;;;;;;;;;;;;;;;AAiBA,IAAa,qBAAb,cAAwC,OAAO,YAC7C,0CACF,CAAC,CAAC,sBAAsB;CACtB,OAAO,OAAO,SAAS,OAAO,MAAM;CACpC,QAAQ,OAAO;CACf,OAAO,OAAO,SAAS,OAAO,OAAO;AACvC,CAAC,CAAC,CAAC,CAAC;;;;;AAMJ,MAAa,aAAa,OAAO,OAAO;;CAEtC,WAAW,OAAO;;CAElB,QAAQ,OAAO,SAAS,OAAO,OAAO;AACxC,CAAC;AAiBD,MAAM,aACJ,SACA,SAEA,OAAO,IAAI,aAAa;CACtB,MAAM,SAAS,OAAO,WAAW;CACjC,MAAM,MAAM,0BAA0B,QAAQ,MAAM,WAAW,QAAQ,YAAY,OAAO,GAAG;CAC7F,MAAM,UAAU,YAAoB,UAClC,IAAI,mBAAmB;EAAE,OAAO,QAAQ;EAAO;EAAQ;CAAM,CAAC;CAEhE,MAAM,UAAU,kBAAkB,IAAI,GAAG,CAAC,CAAC,KACzC,QAAQ,UAAU,KAAA,KACb,SAAS,OACV,kBAAkB,YAAY,SAAS,MAAM,QAAQ,KAAK,CAAC,CACjE;CACA,MAAM,WAAW,OAAO,OACrB,QAAQ,OAAO,CAAC,CAChB,KAAK,OAAO,SAAS,OAAO,mBAAmB,MAAM,CAAC,CAAC;CAE1D,OAAO,SAAS,UAAU,MACtB,OAAO,IAAI,mBAAmB;EAC5B,OAAO,QAAQ;EACf,QAAQ,GAAG,IAAI,YAAY,SAAS;CACtC,CAAC,IACD,OAAO,SAAS,KAAK,KAAK,OAAO,SAAS,OAAO,GAAG,KAAK,mBAAmB,CAAC,CAAC;AACpF,CAAC;;;;;AAMH,MAAa,YACX,YAEA,OAAO,IAAI;CACT,WAAW,UAAU,SAAS,gBAAgB;CAC9C,QAAQ,UAAU,SAAS,uBAAuB,CAAC,CAAC,KAAK,OAAO,oBAAoB,KAAA,CAAS,CAAC;AAChG,CAAC;;AAGH,MAAa,kBACX,eAEA,MAAM,OACJ,WACA,OAAO,IAAI;CACT,WAA6B;EAC3B,MAAM,KAAK,IAAIA,YACb,WAAW,WACV,WAAW,UAAU,CAAC,CACzB;EACA,OAAO;GACL,SAAS,SAAS,GAAG,OAAO,IAAI,CAAC,CAAC;GAIlC,SAAS,WAAY,OAAO,WAAW,IAAI,KAAK,GAAG,OAAOC,MAAI,KAAK,MAAM,CAAC;EAC5E;CACF;CACA,QAAQ,UAAU,IAAI,mBAAmB;EAAE,QAAQ;EAAgC;CAAM,CAAC;AAC5F,CAAC,CACH;;AAGF,MAAa,SACX,YAEA,MAAM,OAAO,OAAO,IAAI,SAAS,OAAO,GAAG,cAAc,CAAC"}
|
package/dist/Rank.d.mts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
declare namespace Rank_d_exports {
|
|
2
|
+
export { Fused, rrf };
|
|
3
|
+
}
|
|
4
|
+
type Fused<A> = {
|
|
5
|
+
readonly value: A;
|
|
6
|
+
readonly score: number;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Reciprocal rank fusion: `score(v) = Σ weightᵢ / (k + rankᵢ(v))` over
|
|
10
|
+
* 1-based ranks, where a list omitting `v` contributes nothing. `k`
|
|
11
|
+
* defaults to 60, `weights` to 1 per ranking.
|
|
12
|
+
*
|
|
13
|
+
* Items are keyed by `Map` semantics, so fuse ids rather than fresh
|
|
14
|
+
* objects. Sorted descending; ties keep first-seen order.
|
|
15
|
+
*/
|
|
16
|
+
declare const rrf: <A>(rankings: ReadonlyArray<ReadonlyArray<A>>, options?: {
|
|
17
|
+
readonly k?: number;
|
|
18
|
+
readonly weights?: ReadonlyArray<number>;
|
|
19
|
+
}) => Array<Fused<A>>;
|
|
20
|
+
//#endregion
|
|
21
|
+
export { Fused, rrf, Rank_d_exports as t };
|
|
22
|
+
//# sourceMappingURL=Rank.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Rank.d.mts","names":[],"sources":["../src/Rank.ts"],"mappings":";;;KAOY,MAAM;WACP,OAAO;WACP;;;;;;;;;;cAaE,MAAO,GAAC,UACT,cAAc,cAAc,KAAG;WACpB;WAAqB,UAAU;MACnD,MAAM,MAAM"}
|
package/dist/Rank.mjs
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
|
|
2
|
+
import { Array, Order, pipe } from "effect";
|
|
3
|
+
//#region src/Rank.ts
|
|
4
|
+
/**
|
|
5
|
+
* Rank fusion: merge several ranked lists of the same items into one
|
|
6
|
+
* ranking by position, so retrievers on incomparable score scales
|
|
7
|
+
* (BM25, cosine) can be combined without normalizing them.
|
|
8
|
+
*/
|
|
9
|
+
var Rank_exports = /* @__PURE__ */ __exportAll({ rrf: () => rrf });
|
|
10
|
+
const byScore = Order.mapInput(Order.flip(Order.Number), (f) => f.score);
|
|
11
|
+
/**
|
|
12
|
+
* Reciprocal rank fusion: `score(v) = Σ weightᵢ / (k + rankᵢ(v))` over
|
|
13
|
+
* 1-based ranks, where a list omitting `v` contributes nothing. `k`
|
|
14
|
+
* defaults to 60, `weights` to 1 per ranking.
|
|
15
|
+
*
|
|
16
|
+
* Items are keyed by `Map` semantics, so fuse ids rather than fresh
|
|
17
|
+
* objects. Sorted descending; ties keep first-seen order.
|
|
18
|
+
*/
|
|
19
|
+
const rrf = (rankings, options) => {
|
|
20
|
+
const k = options?.k ?? 60;
|
|
21
|
+
return pipe(Array.reduce(rankings, /* @__PURE__ */ new Map(), (scores, ranking, i) => Array.reduce(ranking, scores, (acc, value, rank) => acc.set(value, (acc.get(value) ?? 0) + (options?.weights?.[i] ?? 1) / (k + rank + 1)))), Array.fromIterable, Array.map(([value, score]) => ({
|
|
22
|
+
value,
|
|
23
|
+
score
|
|
24
|
+
})), Array.sort(byScore));
|
|
25
|
+
};
|
|
26
|
+
//#endregion
|
|
27
|
+
export { rrf, Rank_exports as t };
|
|
28
|
+
|
|
29
|
+
//# sourceMappingURL=Rank.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Rank.mjs","names":["Arr"],"sources":["../src/Rank.ts"],"sourcesContent":["/**\n * Rank fusion: merge several ranked lists of the same items into one\n * ranking by position, so retrievers on incomparable score scales\n * (BM25, cosine) can be combined without normalizing them.\n */\nimport { Array as Arr, Order, pipe } from \"effect\"\n\nexport type Fused<A> = {\n readonly value: A\n readonly score: number\n}\n\nconst byScore = Order.mapInput(Order.flip(Order.Number), (f: { readonly score: number }) => f.score)\n\n/**\n * Reciprocal rank fusion: `score(v) = Σ weightᵢ / (k + rankᵢ(v))` over\n * 1-based ranks, where a list omitting `v` contributes nothing. `k`\n * defaults to 60, `weights` to 1 per ranking.\n *\n * Items are keyed by `Map` semantics, so fuse ids rather than fresh\n * objects. Sorted descending; ties keep first-seen order.\n */\nexport const rrf = <A>(\n rankings: ReadonlyArray<ReadonlyArray<A>>,\n options?: { readonly k?: number; readonly weights?: ReadonlyArray<number> },\n): Array<Fused<A>> => {\n const k = options?.k ?? 60\n return pipe(\n Arr.reduce(rankings, new Map<A, number>(), (scores, ranking, i) =>\n Arr.reduce(ranking, scores, (acc, value, rank) =>\n acc.set(value, (acc.get(value) ?? 0) + (options?.weights?.[i] ?? 1) / (k + rank + 1)),\n ),\n ),\n Arr.fromIterable,\n Arr.map(([value, score]): Fused<A> => ({ value, score })),\n Arr.sort(byScore),\n )\n}\n"],"mappings":";;;;;;;;;AAYA,MAAM,UAAU,MAAM,SAAS,MAAM,KAAK,MAAM,MAAM,IAAI,MAAkC,EAAE,KAAK;;;;;;;;;AAUnG,MAAa,OACX,UACA,YACoB;CACpB,MAAM,IAAI,SAAS,KAAK;CACxB,OAAO,KACLA,MAAI,OAAO,0BAAU,IAAI,IAAe,IAAI,QAAQ,SAAS,MAC3DA,MAAI,OAAO,SAAS,SAAS,KAAK,OAAO,SACvC,IAAI,IAAI,QAAQ,IAAI,IAAI,KAAK,KAAK,MAAM,SAAS,UAAU,MAAM,MAAM,IAAI,OAAO,EAAE,CACtF,CACF,GACAA,MAAI,cACJA,MAAI,KAAK,CAAC,OAAO,YAAsB;EAAE;EAAO;CAAM,EAAE,GACxDA,MAAI,KAAK,OAAO,CAClB;AACF"}
|
package/dist/index.d.mts
ADDED
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region \0rolldown/runtime.js
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __exportAll = (all, no_symbols) => {
|
|
4
|
+
let target = {};
|
|
5
|
+
for (var name in all) __defProp(target, name, {
|
|
6
|
+
get: all[name],
|
|
7
|
+
enumerable: true
|
|
8
|
+
});
|
|
9
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
10
|
+
return target;
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
13
|
+
export { __exportAll as t };
|