@graphorin/reranker-transformersjs 0.5.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/CHANGELOG.md +11 -0
- package/LICENSE +21 -0
- package/README.md +123 -0
- package/dist/cross-encoder.d.ts +84 -0
- package/dist/cross-encoder.d.ts.map +1 -0
- package/dist/cross-encoder.js +81 -0
- package/dist/cross-encoder.js.map +1 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +44 -0
- package/dist/index.js.map +1 -0
- package/dist/model-selection.d.ts +36 -0
- package/dist/model-selection.d.ts.map +1 -0
- package/dist/model-selection.js +22 -0
- package/dist/model-selection.js.map +1 -0
- package/dist/reranker.d.ts +123 -0
- package/dist/reranker.d.ts.map +1 -0
- package/dist/reranker.js +190 -0
- package/dist/reranker.js.map +1 -0
- package/dist/text-extraction.d.ts +27 -0
- package/dist/text-extraction.d.ts.map +1 -0
- package/dist/text-extraction.js +28 -0
- package/dist/text-extraction.js.map +1 -0
- package/package.json +73 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# @graphorin/reranker-transformersjs
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Initial release of the cross-encoder reranker adapter for the Graphorin
|
|
8
|
+
framework. Wraps `@huggingface/transformers@^4.1.0`, automatically picks
|
|
9
|
+
the BGE reranker model from the agent locale, supports lazy load + idle
|
|
10
|
+
unload, and implements the `ReRanker` contract from
|
|
11
|
+
`@graphorin/memory/search`.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Oleksiy Stepurenko
|
|
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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# @graphorin/reranker-transformersjs
|
|
2
|
+
|
|
3
|
+
> Cross-encoder reranker adapter for the
|
|
4
|
+
> [Graphorin](https://github.com/o-stepper/graphorin) framework. Wraps
|
|
5
|
+
> [`@huggingface/transformers`](https://www.npmjs.com/package/@huggingface/transformers)
|
|
6
|
+
> to score `(query, passage)` pairs with a BGE cross-encoder running
|
|
7
|
+
> entirely in-process. Implements the `ReRanker` contract from
|
|
8
|
+
> `@graphorin/memory/search`.
|
|
9
|
+
>
|
|
10
|
+
> Project Graphorin · v0.5.0 · MIT License · © 2026 Oleksiy Stepurenko ·
|
|
11
|
+
> <https://github.com/o-stepper/graphorin>
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Status
|
|
16
|
+
|
|
17
|
+
- **Published:** v0.5.0 (optional sub-pack)
|
|
18
|
+
- **Default model:** locale-aware (`Xenova/bge-reranker-base` for
|
|
19
|
+
English, `BAAI/bge-reranker-v2-m3` for everything else).
|
|
20
|
+
- **Default precision:** FP16 (`'fp16'` dtype).
|
|
21
|
+
- **Default device:** CPU.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pnpm add @graphorin/reranker-transformersjs @huggingface/transformers
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The first call to `rerank(...)` lazily downloads the reranker weights
|
|
32
|
+
into the Hugging Face cache directory (`~/.cache/huggingface/hub` by
|
|
33
|
+
default; honours `GRAPHORIN_CACHE_DIR`). To pre-warm in CI:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
node -e "import('@graphorin/reranker-transformersjs').then(m => m.createCrossEncoderReranker({ locale: 'en' }).rerank('warmup', [[]], {}))"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
### Drop-in replacement for the built-in RRF reranker
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { createMemory } from '@graphorin/memory';
|
|
47
|
+
import { createCrossEncoderReranker } from '@graphorin/reranker-transformersjs';
|
|
48
|
+
|
|
49
|
+
const memory = createMemory({
|
|
50
|
+
store,
|
|
51
|
+
embedder,
|
|
52
|
+
reranker: createCrossEncoderReranker({ locale: 'en' }),
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Multilingual deployment
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
const reranker = createCrossEncoderReranker({
|
|
60
|
+
locale: 'pt-BR',
|
|
61
|
+
// Falls back to BAAI/bge-reranker-v2-m3 (568M, multilingual).
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Override the model explicitly
|
|
66
|
+
|
|
67
|
+
For narrow language-pair scenarios (e.g. CJK, low-resource European
|
|
68
|
+
languages) plug in a specialised cross-encoder of your choice:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
const reranker = createCrossEncoderReranker({
|
|
72
|
+
model: 'cross-encoder/ms-marco-MiniLM-L-6-v2',
|
|
73
|
+
dtype: 'q8',
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Idle eviction (bound resident memory)
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
// Drop the loaded ONNX session after 10 minutes of inactivity.
|
|
81
|
+
const reranker = createCrossEncoderReranker({
|
|
82
|
+
locale: 'en',
|
|
83
|
+
idleEvictionMs: 10 * 60 * 1000,
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Custom passage extractor
|
|
88
|
+
|
|
89
|
+
The default extractor walks `text → summary → value → label → id`.
|
|
90
|
+
Override when your custom `MemoryRecord` schema attaches the canonical
|
|
91
|
+
text elsewhere:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
const reranker = createCrossEncoderReranker<MyRecord>({
|
|
95
|
+
passageExtractor: (record) => `${record.title}. ${record.body}`,
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Acceptance criteria
|
|
102
|
+
|
|
103
|
+
- [x] Reranks fixture results.
|
|
104
|
+
- [x] Locale auto-pick verified (`'en' → bge-reranker-base`, every
|
|
105
|
+
other locale → `bge-reranker-v2-m3`).
|
|
106
|
+
- [x] Lazy load + idle unload works.
|
|
107
|
+
- [x] Implements `ReRanker` from `@graphorin/memory/search`.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Related decisions
|
|
112
|
+
|
|
113
|
+
- ADR-024 — Reciprocal Rank Fusion default + pluggable rerankers.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## License
|
|
118
|
+
|
|
119
|
+
MIT © 2026 Oleksiy Stepurenko
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
**Project Graphorin** · v0.5.0 · MIT License · © 2026 Oleksiy Stepurenko · <https://github.com/o-stepper/graphorin>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
//#region src/cross-encoder.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Lazy `@huggingface/transformers` text-classification pipeline used to
|
|
4
|
+
* score query / passage pairs. The runner caches the first-loaded
|
|
5
|
+
* pipeline per `(model, dtype, device, revision, cacheDir)` tuple so
|
|
6
|
+
* repeated `rerank(...)` calls inside the same process do not pay the
|
|
7
|
+
* model-load cost twice.
|
|
8
|
+
*
|
|
9
|
+
* Tests inject a stub `pipelineFactory`; the production path lazily
|
|
10
|
+
* imports `@huggingface/transformers` so plain inspection of the
|
|
11
|
+
* package (e.g. for documentation generation) does not pull in the
|
|
12
|
+
* native ONNX runtime.
|
|
13
|
+
*
|
|
14
|
+
* @packageDocumentation
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Numeric dtype hint. Default `'fp16'` per Phase 16 §
|
|
18
|
+
* `@graphorin/reranker-transformersjs`.
|
|
19
|
+
*
|
|
20
|
+
* @stable
|
|
21
|
+
*/
|
|
22
|
+
type RerankerDtype = 'fp32' | 'fp16' | 'q8' | 'q4';
|
|
23
|
+
/**
|
|
24
|
+
* Output shape returned by `@huggingface/transformers`'
|
|
25
|
+
* text-classification pipeline. Each pair returns either a single
|
|
26
|
+
* `{ label, score }` object (top-k = 1) or an array of them. We
|
|
27
|
+
* normalise on the array form upstream so the cross-encoder always
|
|
28
|
+
* sees a consistent shape.
|
|
29
|
+
*
|
|
30
|
+
* @internal
|
|
31
|
+
*/
|
|
32
|
+
interface ClassifierResult {
|
|
33
|
+
readonly label: string;
|
|
34
|
+
readonly score: number;
|
|
35
|
+
}
|
|
36
|
+
/** @internal */
|
|
37
|
+
type CrossEncoderPipeline = (pairs: ReadonlyArray<{
|
|
38
|
+
text: string;
|
|
39
|
+
text_pair: string;
|
|
40
|
+
}>, options?: {
|
|
41
|
+
topk?: number;
|
|
42
|
+
signal?: AbortSignal;
|
|
43
|
+
}) => Promise<ReadonlyArray<ClassifierResult> | ReadonlyArray<ReadonlyArray<ClassifierResult>>>;
|
|
44
|
+
/** @internal */
|
|
45
|
+
type CrossEncoderPipelineFactory = (task: 'text-classification', model: string, options: {
|
|
46
|
+
revision?: string;
|
|
47
|
+
cache_dir?: string;
|
|
48
|
+
dtype?: RerankerDtype;
|
|
49
|
+
device?: string;
|
|
50
|
+
}) => Promise<CrossEncoderPipeline>;
|
|
51
|
+
/**
|
|
52
|
+
* Raised when the `@huggingface/transformers` peer is missing or the
|
|
53
|
+
* configured cross-encoder model fails to load.
|
|
54
|
+
*
|
|
55
|
+
* @stable
|
|
56
|
+
*/
|
|
57
|
+
declare class CrossEncoderLoadError extends Error {
|
|
58
|
+
readonly name = "CrossEncoderLoadError";
|
|
59
|
+
}
|
|
60
|
+
/** @internal */
|
|
61
|
+
declare function loadDefaultPipelineFactory(): Promise<CrossEncoderPipelineFactory>;
|
|
62
|
+
/**
|
|
63
|
+
* Test-only helper. Drops the cached pipeline factory so the next
|
|
64
|
+
* loader call re-imports the peer.
|
|
65
|
+
*
|
|
66
|
+
* @internal
|
|
67
|
+
*/
|
|
68
|
+
declare function _resetPipelineFactoryCacheForTesting(): void;
|
|
69
|
+
/**
|
|
70
|
+
* Normalises the raw pipeline output to a flat `score[]` aligned with the input
|
|
71
|
+
* pair order. Cross-encoder classifiers return either a single-best
|
|
72
|
+
* `{label, score}` per pair (the default single-logit bge exports) or an array
|
|
73
|
+
* of `topk` entries. For the array shape we read the POSITIVE label's
|
|
74
|
+
* confidence — NOT the max of any label (PS-16): an irrelevant pair's most
|
|
75
|
+
* confident class is the *negative* one, so taking the max would invert the
|
|
76
|
+
* ranking for any 2-label classifier. When no label looks positive (single-logit
|
|
77
|
+
* or unrecognised labels) we fall back to the top score.
|
|
78
|
+
*
|
|
79
|
+
* @internal
|
|
80
|
+
*/
|
|
81
|
+
declare function extractPairScores(raw: ReadonlyArray<ClassifierResult> | ReadonlyArray<ReadonlyArray<ClassifierResult>>, pairCount: number): number[];
|
|
82
|
+
//#endregion
|
|
83
|
+
export { ClassifierResult, CrossEncoderLoadError, CrossEncoderPipeline, CrossEncoderPipelineFactory, RerankerDtype, _resetPipelineFactoryCacheForTesting, extractPairScores, loadDefaultPipelineFactory };
|
|
84
|
+
//# sourceMappingURL=cross-encoder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cross-encoder.d.ts","names":[],"sources":["../src/cross-encoder.ts"],"sourcesContent":[],"mappings":";;AAqBA;AAWA;AAMA;;;;;;;;;;AAMA;;;;;AAiBA;AAQA;AAsBgB,KAtEJ,aAAA,GAsEI,MAAA,GAAA,MAAoC,GAAA,IAAA,GAAA,IAAA;AA4BpD;;;;;;;;;UAvFiB,gBAAA;;;;;KAML,oBAAA,WACH;;;;;WAC6B;MACjC,QAAQ,cAAc,oBAAoB,cAAc,cAAc;;KAG/D,2BAAA;;;UAMA;;MAGP,QAAQ;;;;;;;cAQA,qBAAA,SAA8B,KAAA;;;;iBAQrB,0BAAA,CAAA,GAA8B,QAAQ;;;;;;;iBAsB5C,oCAAA,CAAA;;;;;;;;;;;;;iBA4BA,iBAAA,MACT,cAAc,oBAAoB,cAAc,cAAc"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
//#region src/cross-encoder.ts
|
|
2
|
+
/**
|
|
3
|
+
* Raised when the `@huggingface/transformers` peer is missing or the
|
|
4
|
+
* configured cross-encoder model fails to load.
|
|
5
|
+
*
|
|
6
|
+
* @stable
|
|
7
|
+
*/
|
|
8
|
+
var CrossEncoderLoadError = class extends Error {
|
|
9
|
+
name = "CrossEncoderLoadError";
|
|
10
|
+
};
|
|
11
|
+
/** @internal */
|
|
12
|
+
let CACHED_PIPELINE_FACTORY = null;
|
|
13
|
+
/** @internal */
|
|
14
|
+
async function loadDefaultPipelineFactory() {
|
|
15
|
+
if (CACHED_PIPELINE_FACTORY !== null) return CACHED_PIPELINE_FACTORY;
|
|
16
|
+
try {
|
|
17
|
+
CACHED_PIPELINE_FACTORY = (await import("@huggingface/transformers")).pipeline;
|
|
18
|
+
return CACHED_PIPELINE_FACTORY;
|
|
19
|
+
} catch (err) {
|
|
20
|
+
throw new CrossEncoderLoadError("[graphorin/reranker-transformersjs] required peer '@huggingface/transformers' is not installed.", { cause: err });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Test-only helper. Drops the cached pipeline factory so the next
|
|
25
|
+
* loader call re-imports the peer.
|
|
26
|
+
*
|
|
27
|
+
* @internal
|
|
28
|
+
*/
|
|
29
|
+
function _resetPipelineFactoryCacheForTesting() {
|
|
30
|
+
CACHED_PIPELINE_FACTORY = null;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* True when a classifier label names the *positive* (relevant) class. Matches
|
|
34
|
+
* the conventional binary forms — `LABEL_1`, `positive`, `relevant`,
|
|
35
|
+
* `entailment`, `true`, `yes` — using exact words so it does not mis-fire on a
|
|
36
|
+
* negative label that merely contains one (`irrelevant` ⊃ `relevant`).
|
|
37
|
+
*/
|
|
38
|
+
function isPositiveLabel(label) {
|
|
39
|
+
const l = label.toLowerCase().trim();
|
|
40
|
+
if (l === "1" || /(?:^|[_-])1$/.test(l)) return true;
|
|
41
|
+
return /^(?:positive|relevant|entailment|true|yes|pos)$/.test(l);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Normalises the raw pipeline output to a flat `score[]` aligned with the input
|
|
45
|
+
* pair order. Cross-encoder classifiers return either a single-best
|
|
46
|
+
* `{label, score}` per pair (the default single-logit bge exports) or an array
|
|
47
|
+
* of `topk` entries. For the array shape we read the POSITIVE label's
|
|
48
|
+
* confidence — NOT the max of any label (PS-16): an irrelevant pair's most
|
|
49
|
+
* confident class is the *negative* one, so taking the max would invert the
|
|
50
|
+
* ranking for any 2-label classifier. When no label looks positive (single-logit
|
|
51
|
+
* or unrecognised labels) we fall back to the top score.
|
|
52
|
+
*
|
|
53
|
+
* @internal
|
|
54
|
+
*/
|
|
55
|
+
function extractPairScores(raw, pairCount) {
|
|
56
|
+
const out = new Array(pairCount).fill(0);
|
|
57
|
+
for (let i = 0; i < pairCount; i++) {
|
|
58
|
+
const cell = raw[i];
|
|
59
|
+
if (cell === void 0) continue;
|
|
60
|
+
if (Array.isArray(cell)) {
|
|
61
|
+
let best = Number.NEGATIVE_INFINITY;
|
|
62
|
+
let positive = Number.NEGATIVE_INFINITY;
|
|
63
|
+
let sawPositive = false;
|
|
64
|
+
for (const entry of cell) {
|
|
65
|
+
if (entry === void 0) continue;
|
|
66
|
+
if (entry.score > best) best = entry.score;
|
|
67
|
+
if (isPositiveLabel(entry.label)) {
|
|
68
|
+
sawPositive = true;
|
|
69
|
+
if (entry.score > positive) positive = entry.score;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const chosen = sawPositive ? positive : best;
|
|
73
|
+
out[i] = Number.isFinite(chosen) ? chosen : 0;
|
|
74
|
+
} else out[i] = cell.score;
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
//#endregion
|
|
80
|
+
export { CrossEncoderLoadError, _resetPipelineFactoryCacheForTesting, extractPairScores, loadDefaultPipelineFactory };
|
|
81
|
+
//# sourceMappingURL=cross-encoder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cross-encoder.js","names":["CACHED_PIPELINE_FACTORY: CrossEncoderPipelineFactory | null","out: number[]"],"sources":["../src/cross-encoder.ts"],"sourcesContent":["/**\n * Lazy `@huggingface/transformers` text-classification pipeline used to\n * score query / passage pairs. The runner caches the first-loaded\n * pipeline per `(model, dtype, device, revision, cacheDir)` tuple so\n * repeated `rerank(...)` calls inside the same process do not pay the\n * model-load cost twice.\n *\n * Tests inject a stub `pipelineFactory`; the production path lazily\n * imports `@huggingface/transformers` so plain inspection of the\n * package (e.g. for documentation generation) does not pull in the\n * native ONNX runtime.\n *\n * @packageDocumentation\n */\n\n/**\n * Numeric dtype hint. Default `'fp16'` per Phase 16 §\n * `@graphorin/reranker-transformersjs`.\n *\n * @stable\n */\nexport type RerankerDtype = 'fp32' | 'fp16' | 'q8' | 'q4';\n\n/**\n * Output shape returned by `@huggingface/transformers`'\n * text-classification pipeline. Each pair returns either a single\n * `{ label, score }` object (top-k = 1) or an array of them. We\n * normalise on the array form upstream so the cross-encoder always\n * sees a consistent shape.\n *\n * @internal\n */\nexport interface ClassifierResult {\n readonly label: string;\n readonly score: number;\n}\n\n/** @internal */\nexport type CrossEncoderPipeline = (\n pairs: ReadonlyArray<{ text: string; text_pair: string }>,\n options?: { topk?: number; signal?: AbortSignal },\n) => Promise<ReadonlyArray<ClassifierResult> | ReadonlyArray<ReadonlyArray<ClassifierResult>>>;\n\n/** @internal */\nexport type CrossEncoderPipelineFactory = (\n task: 'text-classification',\n model: string,\n options: {\n revision?: string;\n cache_dir?: string;\n dtype?: RerankerDtype;\n device?: string;\n },\n) => Promise<CrossEncoderPipeline>;\n\n/**\n * Raised when the `@huggingface/transformers` peer is missing or the\n * configured cross-encoder model fails to load.\n *\n * @stable\n */\nexport class CrossEncoderLoadError extends Error {\n override readonly name = 'CrossEncoderLoadError';\n}\n\n/** @internal */\nlet CACHED_PIPELINE_FACTORY: CrossEncoderPipelineFactory | null = null;\n\n/** @internal */\nexport async function loadDefaultPipelineFactory(): Promise<CrossEncoderPipelineFactory> {\n if (CACHED_PIPELINE_FACTORY !== null) return CACHED_PIPELINE_FACTORY;\n try {\n const mod = (await import('@huggingface/transformers')) as unknown as {\n pipeline: CrossEncoderPipelineFactory;\n };\n CACHED_PIPELINE_FACTORY = mod.pipeline;\n return CACHED_PIPELINE_FACTORY;\n } catch (err) {\n throw new CrossEncoderLoadError(\n \"[graphorin/reranker-transformersjs] required peer '@huggingface/transformers' is not installed.\",\n { cause: err },\n );\n }\n}\n\n/**\n * Test-only helper. Drops the cached pipeline factory so the next\n * loader call re-imports the peer.\n *\n * @internal\n */\nexport function _resetPipelineFactoryCacheForTesting(): void {\n CACHED_PIPELINE_FACTORY = null;\n}\n\n/**\n * True when a classifier label names the *positive* (relevant) class. Matches\n * the conventional binary forms — `LABEL_1`, `positive`, `relevant`,\n * `entailment`, `true`, `yes` — using exact words so it does not mis-fire on a\n * negative label that merely contains one (`irrelevant` ⊃ `relevant`).\n */\nfunction isPositiveLabel(label: string): boolean {\n const l = label.toLowerCase().trim();\n if (l === '1' || /(?:^|[_-])1$/.test(l)) return true; // '1', 'label_1', 'class-1'\n return /^(?:positive|relevant|entailment|true|yes|pos)$/.test(l);\n}\n\n/**\n * Normalises the raw pipeline output to a flat `score[]` aligned with the input\n * pair order. Cross-encoder classifiers return either a single-best\n * `{label, score}` per pair (the default single-logit bge exports) or an array\n * of `topk` entries. For the array shape we read the POSITIVE label's\n * confidence — NOT the max of any label (PS-16): an irrelevant pair's most\n * confident class is the *negative* one, so taking the max would invert the\n * ranking for any 2-label classifier. When no label looks positive (single-logit\n * or unrecognised labels) we fall back to the top score.\n *\n * @internal\n */\nexport function extractPairScores(\n raw: ReadonlyArray<ClassifierResult> | ReadonlyArray<ReadonlyArray<ClassifierResult>>,\n pairCount: number,\n): number[] {\n const out: number[] = new Array(pairCount).fill(0) as number[];\n for (let i = 0; i < pairCount; i++) {\n const cell = raw[i];\n if (cell === undefined) continue;\n if (Array.isArray(cell)) {\n let best = Number.NEGATIVE_INFINITY;\n let positive = Number.NEGATIVE_INFINITY;\n let sawPositive = false;\n for (const entry of cell) {\n if (entry === undefined) continue;\n if (entry.score > best) best = entry.score;\n if (isPositiveLabel(entry.label)) {\n sawPositive = true;\n if (entry.score > positive) positive = entry.score;\n }\n }\n const chosen = sawPositive ? positive : best;\n out[i] = Number.isFinite(chosen) ? chosen : 0;\n } else {\n const single = cell as ClassifierResult;\n out[i] = single.score;\n }\n }\n return out;\n}\n"],"mappings":";;;;;;;AA6DA,IAAa,wBAAb,cAA2C,MAAM;CAC/C,AAAkB,OAAO;;;AAI3B,IAAIA,0BAA8D;;AAGlE,eAAsB,6BAAmE;AACvF,KAAI,4BAA4B,KAAM,QAAO;AAC7C,KAAI;AAIF,6BAHa,MAAM,OAAO,8BAGI;AAC9B,SAAO;UACA,KAAK;AACZ,QAAM,IAAI,sBACR,mGACA,EAAE,OAAO,KAAK,CACf;;;;;;;;;AAUL,SAAgB,uCAA6C;AAC3D,2BAA0B;;;;;;;;AAS5B,SAAS,gBAAgB,OAAwB;CAC/C,MAAM,IAAI,MAAM,aAAa,CAAC,MAAM;AACpC,KAAI,MAAM,OAAO,eAAe,KAAK,EAAE,CAAE,QAAO;AAChD,QAAO,kDAAkD,KAAK,EAAE;;;;;;;;;;;;;;AAelE,SAAgB,kBACd,KACA,WACU;CACV,MAAMC,MAAgB,IAAI,MAAM,UAAU,CAAC,KAAK,EAAE;AAClD,MAAK,IAAI,IAAI,GAAG,IAAI,WAAW,KAAK;EAClC,MAAM,OAAO,IAAI;AACjB,MAAI,SAAS,OAAW;AACxB,MAAI,MAAM,QAAQ,KAAK,EAAE;GACvB,IAAI,OAAO,OAAO;GAClB,IAAI,WAAW,OAAO;GACtB,IAAI,cAAc;AAClB,QAAK,MAAM,SAAS,MAAM;AACxB,QAAI,UAAU,OAAW;AACzB,QAAI,MAAM,QAAQ,KAAM,QAAO,MAAM;AACrC,QAAI,gBAAgB,MAAM,MAAM,EAAE;AAChC,mBAAc;AACd,SAAI,MAAM,QAAQ,SAAU,YAAW,MAAM;;;GAGjD,MAAM,SAAS,cAAc,WAAW;AACxC,OAAI,KAAK,OAAO,SAAS,OAAO,GAAG,SAAS;QAG5C,KAAI,KADW,KACC;;AAGpB,QAAO"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { ClassifierResult, CrossEncoderLoadError, CrossEncoderPipeline, CrossEncoderPipelineFactory, RerankerDtype, _resetPipelineFactoryCacheForTesting, extractPairScores, loadDefaultPipelineFactory } from "./cross-encoder.js";
|
|
2
|
+
import { DEFAULT_ENGLISH_MODEL, DEFAULT_MULTILINGUAL_MODEL, LocaleTag, pickRerankerModel } from "./model-selection.js";
|
|
3
|
+
import { PassageExtractor, defaultPassageExtractor } from "./text-extraction.js";
|
|
4
|
+
import { CrossEncoderRerankerOptions, RERANKER_ID, TransformersJsReRanker, createCrossEncoderReranker, mergeAndDedupe } from "./reranker.js";
|
|
5
|
+
|
|
6
|
+
//#region src/index.d.ts
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @graphorin/reranker-transformersjs — cross-encoder reranker adapter
|
|
10
|
+
* for the Graphorin framework.
|
|
11
|
+
*
|
|
12
|
+
* Wraps `@huggingface/transformers@^4.1.0` to score `(query, passage)`
|
|
13
|
+
* pairs in-process. Plug into the memory hybrid-search pipeline as a
|
|
14
|
+
* drop-in replacement for the built-in `RRFReranker`:
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* import { createMemory } from '@graphorin/memory';
|
|
18
|
+
* import { createCrossEncoderReranker } from '@graphorin/reranker-transformersjs';
|
|
19
|
+
*
|
|
20
|
+
* const memory = createMemory({
|
|
21
|
+
* store,
|
|
22
|
+
* embedder,
|
|
23
|
+
* reranker: createCrossEncoderReranker({ locale: 'en' }),
|
|
24
|
+
* });
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* Locale-aware default model:
|
|
28
|
+
*
|
|
29
|
+
* - `'en'` / `'en-*'` → `Xenova/bge-reranker-base` (278M parameters,
|
|
30
|
+
* FP16 quantized).
|
|
31
|
+
* - Every other locale → `BAAI/bge-reranker-v2-m3` (568M parameters,
|
|
32
|
+
* multilingual baseline).
|
|
33
|
+
*
|
|
34
|
+
* Operators that want a narrower / language-specific cross-encoder
|
|
35
|
+
* pass an explicit `model` option — the package's defaults
|
|
36
|
+
* deliberately avoid privileging any single language pair.
|
|
37
|
+
*
|
|
38
|
+
* @packageDocumentation
|
|
39
|
+
*/
|
|
40
|
+
/** Canonical version constant. Mirrors the `package.json` version. */
|
|
41
|
+
declare const VERSION = "0.5.0";
|
|
42
|
+
//#endregion
|
|
43
|
+
export { type ClassifierResult, CrossEncoderLoadError, type CrossEncoderPipeline, type CrossEncoderPipelineFactory, type CrossEncoderRerankerOptions, DEFAULT_ENGLISH_MODEL, DEFAULT_MULTILINGUAL_MODEL, type LocaleTag, type PassageExtractor, RERANKER_ID, type RerankerDtype, TransformersJsReRanker, VERSION, _resetPipelineFactoryCacheForTesting, createCrossEncoderReranker, defaultPassageExtractor, extractPairScores, loadDefaultPipelineFactory, mergeAndDedupe, pickRerankerModel };
|
|
44
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;AAkCA;;;;;;;;;;;;;;;;;;;;;;;;;;;cAAa,OAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { CrossEncoderLoadError, _resetPipelineFactoryCacheForTesting, extractPairScores, loadDefaultPipelineFactory } from "./cross-encoder.js";
|
|
2
|
+
import { DEFAULT_ENGLISH_MODEL, DEFAULT_MULTILINGUAL_MODEL, pickRerankerModel } from "./model-selection.js";
|
|
3
|
+
import { defaultPassageExtractor } from "./text-extraction.js";
|
|
4
|
+
import { RERANKER_ID, TransformersJsReRanker, createCrossEncoderReranker, mergeAndDedupe } from "./reranker.js";
|
|
5
|
+
|
|
6
|
+
//#region src/index.ts
|
|
7
|
+
/**
|
|
8
|
+
* @graphorin/reranker-transformersjs — cross-encoder reranker adapter
|
|
9
|
+
* for the Graphorin framework.
|
|
10
|
+
*
|
|
11
|
+
* Wraps `@huggingface/transformers@^4.1.0` to score `(query, passage)`
|
|
12
|
+
* pairs in-process. Plug into the memory hybrid-search pipeline as a
|
|
13
|
+
* drop-in replacement for the built-in `RRFReranker`:
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { createMemory } from '@graphorin/memory';
|
|
17
|
+
* import { createCrossEncoderReranker } from '@graphorin/reranker-transformersjs';
|
|
18
|
+
*
|
|
19
|
+
* const memory = createMemory({
|
|
20
|
+
* store,
|
|
21
|
+
* embedder,
|
|
22
|
+
* reranker: createCrossEncoderReranker({ locale: 'en' }),
|
|
23
|
+
* });
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* Locale-aware default model:
|
|
27
|
+
*
|
|
28
|
+
* - `'en'` / `'en-*'` → `Xenova/bge-reranker-base` (278M parameters,
|
|
29
|
+
* FP16 quantized).
|
|
30
|
+
* - Every other locale → `BAAI/bge-reranker-v2-m3` (568M parameters,
|
|
31
|
+
* multilingual baseline).
|
|
32
|
+
*
|
|
33
|
+
* Operators that want a narrower / language-specific cross-encoder
|
|
34
|
+
* pass an explicit `model` option — the package's defaults
|
|
35
|
+
* deliberately avoid privileging any single language pair.
|
|
36
|
+
*
|
|
37
|
+
* @packageDocumentation
|
|
38
|
+
*/
|
|
39
|
+
/** Canonical version constant. Mirrors the `package.json` version. */
|
|
40
|
+
const VERSION = "0.5.0";
|
|
41
|
+
|
|
42
|
+
//#endregion
|
|
43
|
+
export { CrossEncoderLoadError, DEFAULT_ENGLISH_MODEL, DEFAULT_MULTILINGUAL_MODEL, RERANKER_ID, TransformersJsReRanker, VERSION, _resetPipelineFactoryCacheForTesting, createCrossEncoderReranker, defaultPassageExtractor, extractPairScores, loadDefaultPipelineFactory, mergeAndDedupe, pickRerankerModel };
|
|
44
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * @graphorin/reranker-transformersjs — cross-encoder reranker adapter\n * for the Graphorin framework.\n *\n * Wraps `@huggingface/transformers@^4.1.0` to score `(query, passage)`\n * pairs in-process. Plug into the memory hybrid-search pipeline as a\n * drop-in replacement for the built-in `RRFReranker`:\n *\n * ```ts\n * import { createMemory } from '@graphorin/memory';\n * import { createCrossEncoderReranker } from '@graphorin/reranker-transformersjs';\n *\n * const memory = createMemory({\n * store,\n * embedder,\n * reranker: createCrossEncoderReranker({ locale: 'en' }),\n * });\n * ```\n *\n * Locale-aware default model:\n *\n * - `'en'` / `'en-*'` → `Xenova/bge-reranker-base` (278M parameters,\n * FP16 quantized).\n * - Every other locale → `BAAI/bge-reranker-v2-m3` (568M parameters,\n * multilingual baseline).\n *\n * Operators that want a narrower / language-specific cross-encoder\n * pass an explicit `model` option — the package's defaults\n * deliberately avoid privileging any single language pair.\n *\n * @packageDocumentation\n */\n\n/** Canonical version constant. Mirrors the `package.json` version. */\nexport const VERSION = '0.5.0';\n\nexport {\n _resetPipelineFactoryCacheForTesting,\n type ClassifierResult,\n CrossEncoderLoadError,\n type CrossEncoderPipeline,\n type CrossEncoderPipelineFactory,\n extractPairScores,\n loadDefaultPipelineFactory,\n type RerankerDtype,\n} from './cross-encoder.js';\nexport {\n DEFAULT_ENGLISH_MODEL,\n DEFAULT_MULTILINGUAL_MODEL,\n type LocaleTag,\n pickRerankerModel,\n} from './model-selection.js';\nexport {\n type CrossEncoderRerankerOptions,\n createCrossEncoderReranker,\n mergeAndDedupe,\n RERANKER_ID,\n TransformersJsReRanker,\n} from './reranker.js';\nexport {\n defaultPassageExtractor,\n type PassageExtractor,\n} from './text-extraction.js';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCA,MAAa,UAAU"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
//#region src/model-selection.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Locale-aware default model selector. Mirrors ADR-024 / DEC-120 § BGE
|
|
4
|
+
* model family:
|
|
5
|
+
*
|
|
6
|
+
* - English-only deployments → `Xenova/bge-reranker-base` (278M, FP16).
|
|
7
|
+
* - Every other locale → `BAAI/bge-reranker-v2-m3` (568M, multilingual).
|
|
8
|
+
*
|
|
9
|
+
* The selector deliberately stays language-pair-agnostic outside the
|
|
10
|
+
* English fast path; operators that want a narrower / language-specific
|
|
11
|
+
* cross-encoder pass an explicit `model` to {@link
|
|
12
|
+
* createCrossEncoderReranker}.
|
|
13
|
+
*
|
|
14
|
+
* @packageDocumentation
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* BCP 47 locale tag (e.g. `'en'`, `'en-GB'`, `'pt-BR'`, `'zh-Hans-CN'`).
|
|
18
|
+
*
|
|
19
|
+
* @stable
|
|
20
|
+
*/
|
|
21
|
+
type LocaleTag = string;
|
|
22
|
+
/** @stable */
|
|
23
|
+
declare const DEFAULT_ENGLISH_MODEL = "Xenova/bge-reranker-base";
|
|
24
|
+
/** @stable */
|
|
25
|
+
declare const DEFAULT_MULTILINGUAL_MODEL = "BAAI/bge-reranker-v2-m3";
|
|
26
|
+
/**
|
|
27
|
+
* Pick a reranker model from the agent locale. Pure function so callers
|
|
28
|
+
* (and tests) can pre-resolve the choice without constructing the
|
|
29
|
+
* reranker.
|
|
30
|
+
*
|
|
31
|
+
* @stable
|
|
32
|
+
*/
|
|
33
|
+
declare function pickRerankerModel(locale: LocaleTag | undefined): string;
|
|
34
|
+
//#endregion
|
|
35
|
+
export { DEFAULT_ENGLISH_MODEL, DEFAULT_MULTILINGUAL_MODEL, LocaleTag, pickRerankerModel };
|
|
36
|
+
//# sourceMappingURL=model-selection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model-selection.d.ts","names":[],"sources":["../src/model-selection.ts"],"sourcesContent":[],"mappings":";;AAoBA;AAGA;AAEA;AASA;;;;;;;;;;;;;;;KAdY,SAAA;;cAGC,qBAAA;;cAEA,0BAAA;;;;;;;;iBASG,iBAAA,SAA0B"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//#region src/model-selection.ts
|
|
2
|
+
/** @stable */
|
|
3
|
+
const DEFAULT_ENGLISH_MODEL = "Xenova/bge-reranker-base";
|
|
4
|
+
/** @stable */
|
|
5
|
+
const DEFAULT_MULTILINGUAL_MODEL = "BAAI/bge-reranker-v2-m3";
|
|
6
|
+
/**
|
|
7
|
+
* Pick a reranker model from the agent locale. Pure function so callers
|
|
8
|
+
* (and tests) can pre-resolve the choice without constructing the
|
|
9
|
+
* reranker.
|
|
10
|
+
*
|
|
11
|
+
* @stable
|
|
12
|
+
*/
|
|
13
|
+
function pickRerankerModel(locale) {
|
|
14
|
+
if (locale === void 0 || locale.length === 0) return DEFAULT_MULTILINGUAL_MODEL;
|
|
15
|
+
const normalized = locale.toLowerCase();
|
|
16
|
+
if (normalized === "en" || normalized.startsWith("en-")) return DEFAULT_ENGLISH_MODEL;
|
|
17
|
+
return DEFAULT_MULTILINGUAL_MODEL;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
//#endregion
|
|
21
|
+
export { DEFAULT_ENGLISH_MODEL, DEFAULT_MULTILINGUAL_MODEL, pickRerankerModel };
|
|
22
|
+
//# sourceMappingURL=model-selection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model-selection.js","names":[],"sources":["../src/model-selection.ts"],"sourcesContent":["/**\n * Locale-aware default model selector. Mirrors ADR-024 / DEC-120 § BGE\n * model family:\n *\n * - English-only deployments → `Xenova/bge-reranker-base` (278M, FP16).\n * - Every other locale → `BAAI/bge-reranker-v2-m3` (568M, multilingual).\n *\n * The selector deliberately stays language-pair-agnostic outside the\n * English fast path; operators that want a narrower / language-specific\n * cross-encoder pass an explicit `model` to {@link\n * createCrossEncoderReranker}.\n *\n * @packageDocumentation\n */\n\n/**\n * BCP 47 locale tag (e.g. `'en'`, `'en-GB'`, `'pt-BR'`, `'zh-Hans-CN'`).\n *\n * @stable\n */\nexport type LocaleTag = string;\n\n/** @stable */\nexport const DEFAULT_ENGLISH_MODEL = 'Xenova/bge-reranker-base';\n/** @stable */\nexport const DEFAULT_MULTILINGUAL_MODEL = 'BAAI/bge-reranker-v2-m3';\n\n/**\n * Pick a reranker model from the agent locale. Pure function so callers\n * (and tests) can pre-resolve the choice without constructing the\n * reranker.\n *\n * @stable\n */\nexport function pickRerankerModel(locale: LocaleTag | undefined): string {\n if (locale === undefined || locale.length === 0) return DEFAULT_MULTILINGUAL_MODEL;\n const normalized = locale.toLowerCase();\n if (normalized === 'en' || normalized.startsWith('en-')) return DEFAULT_ENGLISH_MODEL;\n return DEFAULT_MULTILINGUAL_MODEL;\n}\n"],"mappings":";;AAuBA,MAAa,wBAAwB;;AAErC,MAAa,6BAA6B;;;;;;;;AAS1C,SAAgB,kBAAkB,QAAuC;AACvE,KAAI,WAAW,UAAa,OAAO,WAAW,EAAG,QAAO;CACxD,MAAM,aAAa,OAAO,aAAa;AACvC,KAAI,eAAe,QAAQ,WAAW,WAAW,MAAM,CAAE,QAAO;AAChE,QAAO"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { CrossEncoderPipelineFactory, RerankerDtype } from "./cross-encoder.js";
|
|
2
|
+
import { DEFAULT_ENGLISH_MODEL, DEFAULT_MULTILINGUAL_MODEL, LocaleTag } from "./model-selection.js";
|
|
3
|
+
import { PassageExtractor } from "./text-extraction.js";
|
|
4
|
+
import { MemoryHit, MemoryRecord } from "@graphorin/core";
|
|
5
|
+
import { ReRankOptions, ReRanker } from "@graphorin/memory/search";
|
|
6
|
+
|
|
7
|
+
//#region src/reranker.d.ts
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Options accepted by {@link createCrossEncoderReranker}.
|
|
11
|
+
*
|
|
12
|
+
* @stable
|
|
13
|
+
*/
|
|
14
|
+
interface CrossEncoderRerankerOptions<TRecord extends MemoryRecord = MemoryRecord> {
|
|
15
|
+
/** Override the auto-picked model. Default: derived from `locale`. */
|
|
16
|
+
readonly model?: string;
|
|
17
|
+
/** BCP 47 locale tag used to select the default model. Default `'en'`. */
|
|
18
|
+
readonly locale?: LocaleTag;
|
|
19
|
+
/** Default `'fp16'`. */
|
|
20
|
+
readonly dtype?: RerankerDtype;
|
|
21
|
+
/** Optional revision pin (`'main'` if unset). */
|
|
22
|
+
readonly revision?: string;
|
|
23
|
+
/** Optional cache directory. Honours `GRAPHORIN_CACHE_DIR` when unset. */
|
|
24
|
+
readonly cacheDir?: string;
|
|
25
|
+
/** Override device (`'cpu'`, `'webgpu'`, …). Default `'cpu'`. */
|
|
26
|
+
readonly device?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Maximum batch size sent to the cross-encoder per inference call.
|
|
29
|
+
* Default `32`. Larger batches improve throughput at the cost of
|
|
30
|
+
* resident memory.
|
|
31
|
+
*/
|
|
32
|
+
readonly batchSize?: number;
|
|
33
|
+
/**
|
|
34
|
+
* Optional idle-eviction timeout in milliseconds. When the reranker
|
|
35
|
+
* does not score a pair within this window, the loaded pipeline is
|
|
36
|
+
* dropped so the OS can reclaim the underlying ONNX session. Default
|
|
37
|
+
* `undefined` (eviction disabled).
|
|
38
|
+
*/
|
|
39
|
+
readonly idleEvictionMs?: number;
|
|
40
|
+
/**
|
|
41
|
+
* Optional passage extractor — replaces the default heuristic that
|
|
42
|
+
* walks `text → summary → value → label → id`. Useful when a custom
|
|
43
|
+
* `MemoryRecord` schema attaches the canonical text elsewhere.
|
|
44
|
+
*/
|
|
45
|
+
readonly passageExtractor?: PassageExtractor<TRecord>;
|
|
46
|
+
/**
|
|
47
|
+
* Inject a `pipelineFactory`. Used by tests to stub the underlying
|
|
48
|
+
* `@huggingface/transformers` pipeline. Production callers leave
|
|
49
|
+
* this unset so the package lazily loads the peer.
|
|
50
|
+
*/
|
|
51
|
+
readonly pipelineFactory?: CrossEncoderPipelineFactory;
|
|
52
|
+
/**
|
|
53
|
+
* Override the wall-clock provider. Used by tests so the
|
|
54
|
+
* idle-eviction timer can be exercised deterministically.
|
|
55
|
+
*
|
|
56
|
+
* @internal
|
|
57
|
+
*/
|
|
58
|
+
readonly now?: () => number;
|
|
59
|
+
}
|
|
60
|
+
/** @stable */
|
|
61
|
+
declare const RERANKER_ID: "transformersjs-cross-encoder";
|
|
62
|
+
/**
|
|
63
|
+
* Build a cross-encoder reranker. Lazy: the pipeline is constructed on
|
|
64
|
+
* the first `rerank()` call so packaging the reranker pays no
|
|
65
|
+
* model-load cost.
|
|
66
|
+
*
|
|
67
|
+
* @stable
|
|
68
|
+
*/
|
|
69
|
+
declare function createCrossEncoderReranker<TRecord extends MemoryRecord = MemoryRecord>(options?: CrossEncoderRerankerOptions<TRecord>): TransformersJsReRanker<TRecord>;
|
|
70
|
+
/**
|
|
71
|
+
* `ReRanker` implementation. Matches the contract from
|
|
72
|
+
* `@graphorin/memory/search`.
|
|
73
|
+
*
|
|
74
|
+
* @stable
|
|
75
|
+
*/
|
|
76
|
+
declare class TransformersJsReRanker<TRecord extends MemoryRecord = MemoryRecord> implements ReRanker {
|
|
77
|
+
#private;
|
|
78
|
+
readonly id: "transformersjs-cross-encoder";
|
|
79
|
+
readonly model: string;
|
|
80
|
+
readonly locale: LocaleTag;
|
|
81
|
+
readonly dtype: RerankerDtype;
|
|
82
|
+
readonly batchSize: number;
|
|
83
|
+
readonly idleEvictionMs: number | undefined;
|
|
84
|
+
constructor(options: CrossEncoderRerankerOptions<TRecord>);
|
|
85
|
+
/**
|
|
86
|
+
* Number of `rerank(...)` invocations since construction. Surfaced
|
|
87
|
+
* for observability + the test suite.
|
|
88
|
+
*
|
|
89
|
+
* @stable
|
|
90
|
+
*/
|
|
91
|
+
get invocationCount(): number;
|
|
92
|
+
/**
|
|
93
|
+
* Whether the underlying ONNX pipeline is currently loaded in
|
|
94
|
+
* memory. Surfaced for the idle-eviction integration test.
|
|
95
|
+
*
|
|
96
|
+
* @stable
|
|
97
|
+
*/
|
|
98
|
+
get pipelineLoaded(): boolean;
|
|
99
|
+
/**
|
|
100
|
+
* Drop the loaded pipeline. Equivalent to letting the idle-eviction
|
|
101
|
+
* timer fire. Idempotent.
|
|
102
|
+
*
|
|
103
|
+
* @stable
|
|
104
|
+
*/
|
|
105
|
+
unload(): void;
|
|
106
|
+
rerank<TInputRecord extends MemoryRecord>(query: string, lists: ReadonlyArray<ReadonlyArray<MemoryHit<TInputRecord>>>, options?: ReRankOptions): Promise<ReadonlyArray<MemoryHit<TInputRecord>>>;
|
|
107
|
+
}
|
|
108
|
+
interface MergedEntry<TRecord extends MemoryRecord> {
|
|
109
|
+
readonly hit: MemoryHit<TRecord>;
|
|
110
|
+
readonly firstSeenOrder: number;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Merge the per-source lists into a single deduplicated array,
|
|
114
|
+
* preserving the **highest** initial score per record id and the
|
|
115
|
+
* **first-seen order** for stable tie-breaking. Pure function;
|
|
116
|
+
* exported for the unit test fixture.
|
|
117
|
+
*
|
|
118
|
+
* @stable
|
|
119
|
+
*/
|
|
120
|
+
declare function mergeAndDedupe<TRecord extends MemoryRecord>(lists: ReadonlyArray<ReadonlyArray<MemoryHit<TRecord>>>): ReadonlyArray<MergedEntry<TRecord>>;
|
|
121
|
+
//#endregion
|
|
122
|
+
export { CrossEncoderRerankerOptions, RERANKER_ID, TransformersJsReRanker, createCrossEncoderReranker, mergeAndDedupe };
|
|
123
|
+
//# sourceMappingURL=reranker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reranker.d.ts","names":[],"sources":["../src/reranker.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;AAgFA;AASgB,UAzDC,2BAyDyB,CAAA,gBAzDmB,YAyDnB,GAzDkC,YAyDlC,CAAA,CAAA;EAAiB;EAAe,SAAA,KAAA,CAAA,EAAA,MAAA;EACnC;EAA5B,SAAA,MAAA,CAAA,EAtDS,SAsDT;EACe;EAAvB,SAAA,KAAA,CAAA,EArDgB,aAqDhB;EAAsB;EAUZ,SAAA,QAAA,CAAA,EAAA,MAAsB;EAAiB;EAAe,SAAA,QAAA,CAAA,EAAA,MAAA;EAKhD;EACD,SAAA,MAAA,CAAA,EAAA,MAAA;EAgBiC;;;;;EA2D1B,SAAA,SAAA,CAAA,EAAA,MAAA;EAAd;;;;;;EAhFE,SAAA,cAAA,CAAA,EAAA,MAAA;EAAQ;AAiLpB;;;;EAGwB,SAAA,gBAAA,CAAA,EA3NK,gBA2NL,CA3NsB,OA2NtB,CAAA;EAYT;;;;;EACP,SAAA,eAAA,CAAA,EAlOoB,2BAkOpB;EACoB;;;;;;;;;cAxNhB;;;;;;;;iBASG,2CAA2C,eAAe,wBAC/D,4BAA4B,WACpC,uBAAuB;;;;;;;cAUb,uCAAuC,eAAe,yBACtD;;;;mBAIM;kBACD;;;uBAgBK,4BAA4B;;;;;;;;;;;;;;;;;;;;;;8BAyDf,oCAEzB,cAAc,cAAc,UAAU,2BACpC,gBACR,QAAQ,cAAc,UAAU;;UAiG3B,4BAA4B;gBACtB,UAAU;;;;;;;;;;;iBAYV,+BAA+B,qBACtC,cAAc,cAAc,UAAU,aAC5C,cAAc,YAAY"}
|
package/dist/reranker.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { CrossEncoderLoadError, extractPairScores, loadDefaultPipelineFactory } from "./cross-encoder.js";
|
|
2
|
+
import { DEFAULT_ENGLISH_MODEL, DEFAULT_MULTILINGUAL_MODEL, pickRerankerModel } from "./model-selection.js";
|
|
3
|
+
import { defaultPassageExtractor } from "./text-extraction.js";
|
|
4
|
+
|
|
5
|
+
//#region src/reranker.ts
|
|
6
|
+
/** @stable */
|
|
7
|
+
const RERANKER_ID = "transformersjs-cross-encoder";
|
|
8
|
+
/**
|
|
9
|
+
* Build a cross-encoder reranker. Lazy: the pipeline is constructed on
|
|
10
|
+
* the first `rerank()` call so packaging the reranker pays no
|
|
11
|
+
* model-load cost.
|
|
12
|
+
*
|
|
13
|
+
* @stable
|
|
14
|
+
*/
|
|
15
|
+
function createCrossEncoderReranker(options = {}) {
|
|
16
|
+
return new TransformersJsReRanker(options);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* `ReRanker` implementation. Matches the contract from
|
|
20
|
+
* `@graphorin/memory/search`.
|
|
21
|
+
*
|
|
22
|
+
* @stable
|
|
23
|
+
*/
|
|
24
|
+
var TransformersJsReRanker = class {
|
|
25
|
+
id = RERANKER_ID;
|
|
26
|
+
model;
|
|
27
|
+
locale;
|
|
28
|
+
dtype;
|
|
29
|
+
batchSize;
|
|
30
|
+
idleEvictionMs;
|
|
31
|
+
#revision;
|
|
32
|
+
#cacheDir;
|
|
33
|
+
#device;
|
|
34
|
+
#pipelineFactory;
|
|
35
|
+
#passageExtractor;
|
|
36
|
+
#now;
|
|
37
|
+
#pipeline = null;
|
|
38
|
+
#loading = null;
|
|
39
|
+
#lastUsedAt = 0;
|
|
40
|
+
#idleTimer = null;
|
|
41
|
+
#invocationCount = 0;
|
|
42
|
+
constructor(options) {
|
|
43
|
+
this.locale = options.locale ?? "en";
|
|
44
|
+
this.model = options.model ?? pickRerankerModel(this.locale);
|
|
45
|
+
this.dtype = options.dtype ?? "fp16";
|
|
46
|
+
this.batchSize = options.batchSize ?? 32;
|
|
47
|
+
if (!Number.isInteger(this.batchSize) || this.batchSize <= 0) throw new RangeError(`[graphorin/reranker-transformersjs] batchSize must be a positive integer; got ${String(options.batchSize)}.`);
|
|
48
|
+
this.idleEvictionMs = options.idleEvictionMs;
|
|
49
|
+
this.#revision = options.revision;
|
|
50
|
+
this.#cacheDir = options.cacheDir ?? process.env.GRAPHORIN_CACHE_DIR;
|
|
51
|
+
this.#device = options.device;
|
|
52
|
+
this.#pipelineFactory = options.pipelineFactory;
|
|
53
|
+
this.#passageExtractor = options.passageExtractor ?? ((record) => defaultPassageExtractor(record));
|
|
54
|
+
this.#now = options.now ?? Date.now;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Number of `rerank(...)` invocations since construction. Surfaced
|
|
58
|
+
* for observability + the test suite.
|
|
59
|
+
*
|
|
60
|
+
* @stable
|
|
61
|
+
*/
|
|
62
|
+
get invocationCount() {
|
|
63
|
+
return this.#invocationCount;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Whether the underlying ONNX pipeline is currently loaded in
|
|
67
|
+
* memory. Surfaced for the idle-eviction integration test.
|
|
68
|
+
*
|
|
69
|
+
* @stable
|
|
70
|
+
*/
|
|
71
|
+
get pipelineLoaded() {
|
|
72
|
+
return this.#pipeline !== null;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Drop the loaded pipeline. Equivalent to letting the idle-eviction
|
|
76
|
+
* timer fire. Idempotent.
|
|
77
|
+
*
|
|
78
|
+
* @stable
|
|
79
|
+
*/
|
|
80
|
+
unload() {
|
|
81
|
+
this.#pipeline = null;
|
|
82
|
+
this.#loading = null;
|
|
83
|
+
if (this.#idleTimer !== null) {
|
|
84
|
+
clearTimeout(this.#idleTimer);
|
|
85
|
+
this.#idleTimer = null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async rerank(query, lists, options = {}) {
|
|
89
|
+
if (options.signal?.aborted === true) throw new DOMException("TransformersJsReRanker aborted", "AbortError");
|
|
90
|
+
this.#invocationCount += 1;
|
|
91
|
+
const merged = mergeAndDedupe(lists);
|
|
92
|
+
if (merged.length === 0) return [];
|
|
93
|
+
const pipeline = await this.#getPipeline();
|
|
94
|
+
const passages = merged.map((entry) => this.#passageExtractor(entry.hit.record));
|
|
95
|
+
const scores = await this.#scoreInBatches(pipeline, query, passages, options.signal);
|
|
96
|
+
const fused = merged.map((entry, idx) => {
|
|
97
|
+
const score = scores[idx] ?? 0;
|
|
98
|
+
const signals = {
|
|
99
|
+
...entry.hit.signals ?? {},
|
|
100
|
+
cross_encoder: score
|
|
101
|
+
};
|
|
102
|
+
return Object.freeze({
|
|
103
|
+
record: entry.hit.record,
|
|
104
|
+
score,
|
|
105
|
+
signals: Object.freeze(signals)
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
fused.sort((a, b) => b.score - a.score);
|
|
109
|
+
const topK = options.topK ?? 10;
|
|
110
|
+
return fused.slice(0, Math.max(0, topK));
|
|
111
|
+
}
|
|
112
|
+
async #scoreInBatches(pipeline, query, passages, signal) {
|
|
113
|
+
const out = [];
|
|
114
|
+
for (let i = 0; i < passages.length; i += this.batchSize) {
|
|
115
|
+
if (signal?.aborted === true) throw new DOMException("TransformersJsReRanker aborted", "AbortError");
|
|
116
|
+
const batch = passages.slice(i, i + this.batchSize);
|
|
117
|
+
const scores = extractPairScores(await pipeline(batch.map((text_pair) => ({
|
|
118
|
+
text: query,
|
|
119
|
+
text_pair
|
|
120
|
+
})), {
|
|
121
|
+
topk: 1,
|
|
122
|
+
...signal !== void 0 ? { signal } : {}
|
|
123
|
+
}), batch.length);
|
|
124
|
+
for (const s of scores) out.push(s);
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
async #getPipeline() {
|
|
129
|
+
this.#lastUsedAt = this.#now();
|
|
130
|
+
this.#scheduleIdleEviction();
|
|
131
|
+
if (this.#pipeline !== null) return this.#pipeline;
|
|
132
|
+
if (this.#loading !== null) return this.#loading;
|
|
133
|
+
this.#loading = (this.#pipelineFactory ?? await loadDefaultPipelineFactory())("text-classification", this.model, {
|
|
134
|
+
...this.#revision !== void 0 ? { revision: this.#revision } : {},
|
|
135
|
+
...this.#cacheDir !== void 0 ? { cache_dir: this.#cacheDir } : {},
|
|
136
|
+
...this.dtype !== void 0 ? { dtype: this.dtype } : {},
|
|
137
|
+
...this.#device !== void 0 ? { device: this.#device } : {}
|
|
138
|
+
}).then((pipe) => {
|
|
139
|
+
this.#pipeline = pipe;
|
|
140
|
+
this.#loading = null;
|
|
141
|
+
return pipe;
|
|
142
|
+
}).catch((err) => {
|
|
143
|
+
this.#loading = null;
|
|
144
|
+
throw new CrossEncoderLoadError(`[graphorin/reranker-transformersjs] failed to load model '${this.model}'.`, { cause: err });
|
|
145
|
+
});
|
|
146
|
+
return this.#loading;
|
|
147
|
+
}
|
|
148
|
+
#scheduleIdleEviction() {
|
|
149
|
+
if (this.idleEvictionMs === void 0) return;
|
|
150
|
+
if (this.#idleTimer !== null) clearTimeout(this.#idleTimer);
|
|
151
|
+
this.#idleTimer = setTimeout(() => {
|
|
152
|
+
if (this.#now() - this.#lastUsedAt >= (this.idleEvictionMs ?? 0)) {
|
|
153
|
+
this.#pipeline = null;
|
|
154
|
+
this.#loading = null;
|
|
155
|
+
}
|
|
156
|
+
this.#idleTimer = null;
|
|
157
|
+
}, this.idleEvictionMs);
|
|
158
|
+
this.#idleTimer.unref?.();
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
/**
|
|
162
|
+
* Merge the per-source lists into a single deduplicated array,
|
|
163
|
+
* preserving the **highest** initial score per record id and the
|
|
164
|
+
* **first-seen order** for stable tie-breaking. Pure function;
|
|
165
|
+
* exported for the unit test fixture.
|
|
166
|
+
*
|
|
167
|
+
* @stable
|
|
168
|
+
*/
|
|
169
|
+
function mergeAndDedupe(lists) {
|
|
170
|
+
const out = /* @__PURE__ */ new Map();
|
|
171
|
+
let order = 0;
|
|
172
|
+
for (const list of lists) for (const hit of list) {
|
|
173
|
+
const existing = out.get(hit.record.id);
|
|
174
|
+
if (existing === void 0) out.set(hit.record.id, {
|
|
175
|
+
hit,
|
|
176
|
+
firstSeenOrder: order++
|
|
177
|
+
});
|
|
178
|
+
else if (hit.score > existing.hit.score) out.set(hit.record.id, {
|
|
179
|
+
hit,
|
|
180
|
+
firstSeenOrder: existing.firstSeenOrder
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
const arr = Array.from(out.values());
|
|
184
|
+
arr.sort((a, b) => a.firstSeenOrder - b.firstSeenOrder);
|
|
185
|
+
return arr;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
//#endregion
|
|
189
|
+
export { RERANKER_ID, TransformersJsReRanker, createCrossEncoderReranker, mergeAndDedupe };
|
|
190
|
+
//# sourceMappingURL=reranker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reranker.js","names":["#revision","#cacheDir","#device","#pipelineFactory","#passageExtractor","#now","#invocationCount","#pipeline","#loading","#idleTimer","#getPipeline","#scoreInBatches","fused: MemoryHit<TInputRecord>[]","signals: Record<string, number>","out: number[]","#lastUsedAt","#scheduleIdleEviction"],"sources":["../src/reranker.ts"],"sourcesContent":["/**\n * Cross-encoder `ReRanker` implementation backed by\n * `@huggingface/transformers`. Lazy-loaded, optional idle eviction,\n * locale-aware default model.\n *\n * @packageDocumentation\n */\n\nimport type { MemoryHit, MemoryRecord } from '@graphorin/core';\nimport type { ReRanker, ReRankOptions } from '@graphorin/memory/search';\n\nimport {\n CrossEncoderLoadError,\n type CrossEncoderPipeline,\n type CrossEncoderPipelineFactory,\n extractPairScores,\n loadDefaultPipelineFactory,\n type RerankerDtype,\n} from './cross-encoder.js';\nimport {\n DEFAULT_ENGLISH_MODEL,\n DEFAULT_MULTILINGUAL_MODEL,\n type LocaleTag,\n pickRerankerModel,\n} from './model-selection.js';\nimport { defaultPassageExtractor, type PassageExtractor } from './text-extraction.js';\n\n/**\n * Options accepted by {@link createCrossEncoderReranker}.\n *\n * @stable\n */\nexport interface CrossEncoderRerankerOptions<TRecord extends MemoryRecord = MemoryRecord> {\n /** Override the auto-picked model. Default: derived from `locale`. */\n readonly model?: string;\n /** BCP 47 locale tag used to select the default model. Default `'en'`. */\n readonly locale?: LocaleTag;\n /** Default `'fp16'`. */\n readonly dtype?: RerankerDtype;\n /** Optional revision pin (`'main'` if unset). */\n readonly revision?: string;\n /** Optional cache directory. Honours `GRAPHORIN_CACHE_DIR` when unset. */\n readonly cacheDir?: string;\n /** Override device (`'cpu'`, `'webgpu'`, …). Default `'cpu'`. */\n readonly device?: string;\n /**\n * Maximum batch size sent to the cross-encoder per inference call.\n * Default `32`. Larger batches improve throughput at the cost of\n * resident memory.\n */\n readonly batchSize?: number;\n /**\n * Optional idle-eviction timeout in milliseconds. When the reranker\n * does not score a pair within this window, the loaded pipeline is\n * dropped so the OS can reclaim the underlying ONNX session. Default\n * `undefined` (eviction disabled).\n */\n readonly idleEvictionMs?: number;\n /**\n * Optional passage extractor — replaces the default heuristic that\n * walks `text → summary → value → label → id`. Useful when a custom\n * `MemoryRecord` schema attaches the canonical text elsewhere.\n */\n readonly passageExtractor?: PassageExtractor<TRecord>;\n /**\n * Inject a `pipelineFactory`. Used by tests to stub the underlying\n * `@huggingface/transformers` pipeline. Production callers leave\n * this unset so the package lazily loads the peer.\n */\n readonly pipelineFactory?: CrossEncoderPipelineFactory;\n /**\n * Override the wall-clock provider. Used by tests so the\n * idle-eviction timer can be exercised deterministically.\n *\n * @internal\n */\n readonly now?: () => number;\n}\n\n/** @stable */\nexport const RERANKER_ID = 'transformersjs-cross-encoder' as const;\n\n/**\n * Build a cross-encoder reranker. Lazy: the pipeline is constructed on\n * the first `rerank()` call so packaging the reranker pays no\n * model-load cost.\n *\n * @stable\n */\nexport function createCrossEncoderReranker<TRecord extends MemoryRecord = MemoryRecord>(\n options: CrossEncoderRerankerOptions<TRecord> = {},\n): TransformersJsReRanker<TRecord> {\n return new TransformersJsReRanker<TRecord>(options);\n}\n\n/**\n * `ReRanker` implementation. Matches the contract from\n * `@graphorin/memory/search`.\n *\n * @stable\n */\nexport class TransformersJsReRanker<TRecord extends MemoryRecord = MemoryRecord>\n implements ReRanker\n{\n readonly id = RERANKER_ID;\n readonly model: string;\n readonly locale: LocaleTag;\n readonly dtype: RerankerDtype;\n readonly batchSize: number;\n readonly idleEvictionMs: number | undefined;\n\n readonly #revision: string | undefined;\n readonly #cacheDir: string | undefined;\n readonly #device: string | undefined;\n readonly #pipelineFactory: CrossEncoderPipelineFactory | undefined;\n readonly #passageExtractor: PassageExtractor<TRecord>;\n readonly #now: () => number;\n #pipeline: CrossEncoderPipeline | null = null;\n #loading: Promise<CrossEncoderPipeline> | null = null;\n #lastUsedAt: number = 0;\n #idleTimer: ReturnType<typeof setTimeout> | null = null;\n #invocationCount = 0;\n\n constructor(options: CrossEncoderRerankerOptions<TRecord>) {\n this.locale = options.locale ?? 'en';\n this.model = options.model ?? pickRerankerModel(this.locale);\n this.dtype = options.dtype ?? 'fp16';\n this.batchSize = options.batchSize ?? 32;\n if (!Number.isInteger(this.batchSize) || this.batchSize <= 0) {\n throw new RangeError(\n `[graphorin/reranker-transformersjs] batchSize must be a positive integer; got ${String(\n options.batchSize,\n )}.`,\n );\n }\n this.idleEvictionMs = options.idleEvictionMs;\n this.#revision = options.revision;\n this.#cacheDir = options.cacheDir ?? process.env.GRAPHORIN_CACHE_DIR;\n this.#device = options.device;\n this.#pipelineFactory = options.pipelineFactory;\n this.#passageExtractor =\n options.passageExtractor ?? ((record: TRecord) => defaultPassageExtractor(record));\n this.#now = options.now ?? Date.now;\n }\n\n /**\n * Number of `rerank(...)` invocations since construction. Surfaced\n * for observability + the test suite.\n *\n * @stable\n */\n get invocationCount(): number {\n return this.#invocationCount;\n }\n\n /**\n * Whether the underlying ONNX pipeline is currently loaded in\n * memory. Surfaced for the idle-eviction integration test.\n *\n * @stable\n */\n get pipelineLoaded(): boolean {\n return this.#pipeline !== null;\n }\n\n /**\n * Drop the loaded pipeline. Equivalent to letting the idle-eviction\n * timer fire. Idempotent.\n *\n * @stable\n */\n unload(): void {\n this.#pipeline = null;\n this.#loading = null;\n if (this.#idleTimer !== null) {\n clearTimeout(this.#idleTimer);\n this.#idleTimer = null;\n }\n }\n\n async rerank<TInputRecord extends MemoryRecord>(\n query: string,\n lists: ReadonlyArray<ReadonlyArray<MemoryHit<TInputRecord>>>,\n options: ReRankOptions = {},\n ): Promise<ReadonlyArray<MemoryHit<TInputRecord>>> {\n if (options.signal?.aborted === true) {\n throw new DOMException('TransformersJsReRanker aborted', 'AbortError');\n }\n this.#invocationCount += 1;\n const merged = mergeAndDedupe(lists);\n if (merged.length === 0) return [];\n const pipeline = await this.#getPipeline();\n const passages = merged.map((entry) =>\n this.#passageExtractor(entry.hit.record as unknown as TRecord),\n );\n const scores = await this.#scoreInBatches(pipeline, query, passages, options.signal);\n const fused: MemoryHit<TInputRecord>[] = merged.map((entry, idx) => {\n const score = scores[idx] ?? 0;\n const baseSignals = entry.hit.signals ?? {};\n const signals: Record<string, number> = {\n ...baseSignals,\n cross_encoder: score,\n };\n return Object.freeze({\n record: entry.hit.record,\n score,\n signals: Object.freeze(signals),\n });\n });\n fused.sort((a, b) => b.score - a.score);\n const topK = options.topK ?? 10;\n return fused.slice(0, Math.max(0, topK));\n }\n\n async #scoreInBatches(\n pipeline: CrossEncoderPipeline,\n query: string,\n passages: ReadonlyArray<string>,\n signal: AbortSignal | undefined,\n ): Promise<number[]> {\n const out: number[] = [];\n for (let i = 0; i < passages.length; i += this.batchSize) {\n if (signal?.aborted === true) {\n throw new DOMException('TransformersJsReRanker aborted', 'AbortError');\n }\n const batch = passages.slice(i, i + this.batchSize);\n const pairs = batch.map((text_pair) => ({ text: query, text_pair }));\n const raw = await pipeline(pairs, {\n topk: 1,\n ...(signal !== undefined ? { signal } : {}),\n });\n const scores = extractPairScores(raw, batch.length);\n for (const s of scores) out.push(s);\n }\n return out;\n }\n\n async #getPipeline(): Promise<CrossEncoderPipeline> {\n this.#lastUsedAt = this.#now();\n this.#scheduleIdleEviction();\n if (this.#pipeline !== null) return this.#pipeline;\n if (this.#loading !== null) return this.#loading;\n const factory = this.#pipelineFactory ?? (await loadDefaultPipelineFactory());\n this.#loading = factory('text-classification', this.model, {\n ...(this.#revision !== undefined ? { revision: this.#revision } : {}),\n ...(this.#cacheDir !== undefined ? { cache_dir: this.#cacheDir } : {}),\n ...(this.dtype !== undefined ? { dtype: this.dtype } : {}),\n ...(this.#device !== undefined ? { device: this.#device } : {}),\n })\n .then((pipe) => {\n this.#pipeline = pipe;\n this.#loading = null;\n return pipe;\n })\n .catch((err) => {\n this.#loading = null;\n throw new CrossEncoderLoadError(\n `[graphorin/reranker-transformersjs] failed to load model '${this.model}'.`,\n { cause: err },\n );\n });\n return this.#loading;\n }\n\n #scheduleIdleEviction(): void {\n if (this.idleEvictionMs === undefined) return;\n if (this.#idleTimer !== null) {\n clearTimeout(this.#idleTimer);\n }\n this.#idleTimer = setTimeout(() => {\n const elapsed = this.#now() - this.#lastUsedAt;\n if (elapsed >= (this.idleEvictionMs ?? 0)) {\n this.#pipeline = null;\n this.#loading = null;\n }\n this.#idleTimer = null;\n }, this.idleEvictionMs);\n this.#idleTimer.unref?.();\n }\n}\n\ninterface MergedEntry<TRecord extends MemoryRecord> {\n readonly hit: MemoryHit<TRecord>;\n readonly firstSeenOrder: number;\n}\n\n/**\n * Merge the per-source lists into a single deduplicated array,\n * preserving the **highest** initial score per record id and the\n * **first-seen order** for stable tie-breaking. Pure function;\n * exported for the unit test fixture.\n *\n * @stable\n */\nexport function mergeAndDedupe<TRecord extends MemoryRecord>(\n lists: ReadonlyArray<ReadonlyArray<MemoryHit<TRecord>>>,\n): ReadonlyArray<MergedEntry<TRecord>> {\n const out = new Map<string, MergedEntry<TRecord>>();\n let order = 0;\n for (const list of lists) {\n for (const hit of list) {\n const existing = out.get(hit.record.id);\n if (existing === undefined) {\n out.set(hit.record.id, { hit, firstSeenOrder: order++ });\n } else if (hit.score > existing.hit.score) {\n out.set(hit.record.id, {\n hit,\n firstSeenOrder: existing.firstSeenOrder,\n });\n }\n }\n }\n const arr = Array.from(out.values());\n arr.sort((a, b) => a.firstSeenOrder - b.firstSeenOrder);\n return arr;\n}\n\nexport { DEFAULT_ENGLISH_MODEL, DEFAULT_MULTILINGUAL_MODEL };\n"],"mappings":";;;;;;AAgFA,MAAa,cAAc;;;;;;;;AAS3B,SAAgB,2BACd,UAAgD,EAAE,EACjB;AACjC,QAAO,IAAI,uBAAgC,QAAQ;;;;;;;;AASrD,IAAa,yBAAb,MAEA;CACE,AAAS,KAAK;CACd,AAAS;CACT,AAAS;CACT,AAAS;CACT,AAAS;CACT,AAAS;CAET,CAASA;CACT,CAASC;CACT,CAASC;CACT,CAASC;CACT,CAASC;CACT,CAASC;CACT,YAAyC;CACzC,WAAiD;CACjD,cAAsB;CACtB,aAAmD;CACnD,mBAAmB;CAEnB,YAAY,SAA+C;AACzD,OAAK,SAAS,QAAQ,UAAU;AAChC,OAAK,QAAQ,QAAQ,SAAS,kBAAkB,KAAK,OAAO;AAC5D,OAAK,QAAQ,QAAQ,SAAS;AAC9B,OAAK,YAAY,QAAQ,aAAa;AACtC,MAAI,CAAC,OAAO,UAAU,KAAK,UAAU,IAAI,KAAK,aAAa,EACzD,OAAM,IAAI,WACR,iFAAiF,OAC/E,QAAQ,UACT,CAAC,GACH;AAEH,OAAK,iBAAiB,QAAQ;AAC9B,QAAKL,WAAY,QAAQ;AACzB,QAAKC,WAAY,QAAQ,YAAY,QAAQ,IAAI;AACjD,QAAKC,SAAU,QAAQ;AACvB,QAAKC,kBAAmB,QAAQ;AAChC,QAAKC,mBACH,QAAQ,sBAAsB,WAAoB,wBAAwB,OAAO;AACnF,QAAKC,MAAO,QAAQ,OAAO,KAAK;;;;;;;;CASlC,IAAI,kBAA0B;AAC5B,SAAO,MAAKC;;;;;;;;CASd,IAAI,iBAA0B;AAC5B,SAAO,MAAKC,aAAc;;;;;;;;CAS5B,SAAe;AACb,QAAKA,WAAY;AACjB,QAAKC,UAAW;AAChB,MAAI,MAAKC,cAAe,MAAM;AAC5B,gBAAa,MAAKA,UAAW;AAC7B,SAAKA,YAAa;;;CAItB,MAAM,OACJ,OACA,OACA,UAAyB,EAAE,EACsB;AACjD,MAAI,QAAQ,QAAQ,YAAY,KAC9B,OAAM,IAAI,aAAa,kCAAkC,aAAa;AAExE,QAAKH,mBAAoB;EACzB,MAAM,SAAS,eAAe,MAAM;AACpC,MAAI,OAAO,WAAW,EAAG,QAAO,EAAE;EAClC,MAAM,WAAW,MAAM,MAAKI,aAAc;EAC1C,MAAM,WAAW,OAAO,KAAK,UAC3B,MAAKN,iBAAkB,MAAM,IAAI,OAA6B,CAC/D;EACD,MAAM,SAAS,MAAM,MAAKO,eAAgB,UAAU,OAAO,UAAU,QAAQ,OAAO;EACpF,MAAMC,QAAmC,OAAO,KAAK,OAAO,QAAQ;GAClE,MAAM,QAAQ,OAAO,QAAQ;GAE7B,MAAMC,UAAkC;IACtC,GAFkB,MAAM,IAAI,WAAW,EAAE;IAGzC,eAAe;IAChB;AACD,UAAO,OAAO,OAAO;IACnB,QAAQ,MAAM,IAAI;IAClB;IACA,SAAS,OAAO,OAAO,QAAQ;IAChC,CAAC;IACF;AACF,QAAM,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,MAAM;EACvC,MAAM,OAAO,QAAQ,QAAQ;AAC7B,SAAO,MAAM,MAAM,GAAG,KAAK,IAAI,GAAG,KAAK,CAAC;;CAG1C,OAAMF,eACJ,UACA,OACA,UACA,QACmB;EACnB,MAAMG,MAAgB,EAAE;AACxB,OAAK,IAAI,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK,KAAK,WAAW;AACxD,OAAI,QAAQ,YAAY,KACtB,OAAM,IAAI,aAAa,kCAAkC,aAAa;GAExE,MAAM,QAAQ,SAAS,MAAM,GAAG,IAAI,KAAK,UAAU;GAMnD,MAAM,SAAS,kBAJH,MAAM,SADJ,MAAM,KAAK,eAAe;IAAE,MAAM;IAAO;IAAW,EAAE,EAClC;IAChC,MAAM;IACN,GAAI,WAAW,SAAY,EAAE,QAAQ,GAAG,EAAE;IAC3C,CAAC,EACoC,MAAM,OAAO;AACnD,QAAK,MAAM,KAAK,OAAQ,KAAI,KAAK,EAAE;;AAErC,SAAO;;CAGT,OAAMJ,cAA8C;AAClD,QAAKK,aAAc,MAAKV,KAAM;AAC9B,QAAKW,sBAAuB;AAC5B,MAAI,MAAKT,aAAc,KAAM,QAAO,MAAKA;AACzC,MAAI,MAAKC,YAAa,KAAM,QAAO,MAAKA;AAExC,QAAKA,WADW,MAAKL,mBAAqB,MAAM,4BAA4B,EACpD,uBAAuB,KAAK,OAAO;GACzD,GAAI,MAAKH,aAAc,SAAY,EAAE,UAAU,MAAKA,UAAW,GAAG,EAAE;GACpE,GAAI,MAAKC,aAAc,SAAY,EAAE,WAAW,MAAKA,UAAW,GAAG,EAAE;GACrE,GAAI,KAAK,UAAU,SAAY,EAAE,OAAO,KAAK,OAAO,GAAG,EAAE;GACzD,GAAI,MAAKC,WAAY,SAAY,EAAE,QAAQ,MAAKA,QAAS,GAAG,EAAE;GAC/D,CAAC,CACC,MAAM,SAAS;AACd,SAAKK,WAAY;AACjB,SAAKC,UAAW;AAChB,UAAO;IACP,CACD,OAAO,QAAQ;AACd,SAAKA,UAAW;AAChB,SAAM,IAAI,sBACR,6DAA6D,KAAK,MAAM,KACxE,EAAE,OAAO,KAAK,CACf;IACD;AACJ,SAAO,MAAKA;;CAGd,wBAA8B;AAC5B,MAAI,KAAK,mBAAmB,OAAW;AACvC,MAAI,MAAKC,cAAe,KACtB,cAAa,MAAKA,UAAW;AAE/B,QAAKA,YAAa,iBAAiB;AAEjC,OADgB,MAAKJ,KAAM,GAAG,MAAKU,eACnB,KAAK,kBAAkB,IAAI;AACzC,UAAKR,WAAY;AACjB,UAAKC,UAAW;;AAElB,SAAKC,YAAa;KACjB,KAAK,eAAe;AACvB,QAAKA,UAAW,SAAS;;;;;;;;;;;AAiB7B,SAAgB,eACd,OACqC;CACrC,MAAM,sBAAM,IAAI,KAAmC;CACnD,IAAI,QAAQ;AACZ,MAAK,MAAM,QAAQ,MACjB,MAAK,MAAM,OAAO,MAAM;EACtB,MAAM,WAAW,IAAI,IAAI,IAAI,OAAO,GAAG;AACvC,MAAI,aAAa,OACf,KAAI,IAAI,IAAI,OAAO,IAAI;GAAE;GAAK,gBAAgB;GAAS,CAAC;WAC/C,IAAI,QAAQ,SAAS,IAAI,MAClC,KAAI,IAAI,IAAI,OAAO,IAAI;GACrB;GACA,gBAAgB,SAAS;GAC1B,CAAC;;CAIR,MAAM,MAAM,MAAM,KAAK,IAAI,QAAQ,CAAC;AACpC,KAAI,MAAM,GAAG,MAAM,EAAE,iBAAiB,EAAE,eAAe;AACvD,QAAO"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { MemoryRecord } from "@graphorin/core";
|
|
2
|
+
|
|
3
|
+
//#region src/text-extraction.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Returns the best-effort passage text for a {@link MemoryRecord}. The
|
|
7
|
+
* order of preference, top-down:
|
|
8
|
+
*
|
|
9
|
+
* 1. `text` — facts, rules, generic text-bearing tiers.
|
|
10
|
+
* 2. `summary` — episodes.
|
|
11
|
+
* 3. `value` — working-memory blocks.
|
|
12
|
+
* 4. `id` fallback so the reranker never sees an empty passage.
|
|
13
|
+
*
|
|
14
|
+
* @stable
|
|
15
|
+
*/
|
|
16
|
+
declare function defaultPassageExtractor(record: MemoryRecord): string;
|
|
17
|
+
/**
|
|
18
|
+
* Caller-supplied passage extractor. Receives the record + the
|
|
19
|
+
* surrounding metadata (kind, sensitivity, tags) and returns the
|
|
20
|
+
* passage to feed into the cross-encoder.
|
|
21
|
+
*
|
|
22
|
+
* @stable
|
|
23
|
+
*/
|
|
24
|
+
type PassageExtractor<TRecord extends MemoryRecord = MemoryRecord> = (record: TRecord) => string;
|
|
25
|
+
//#endregion
|
|
26
|
+
export { PassageExtractor, defaultPassageExtractor };
|
|
27
|
+
//# sourceMappingURL=text-extraction.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text-extraction.d.ts","names":[],"sources":["../src/text-extraction.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;;;iBA0BgB,uBAAA,SAAgC;;;;;;;;KAoBpC,iCAAiC,eAAe,yBAClD"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
//#region src/text-extraction.ts
|
|
2
|
+
/**
|
|
3
|
+
* Returns the best-effort passage text for a {@link MemoryRecord}. The
|
|
4
|
+
* order of preference, top-down:
|
|
5
|
+
*
|
|
6
|
+
* 1. `text` — facts, rules, generic text-bearing tiers.
|
|
7
|
+
* 2. `summary` — episodes.
|
|
8
|
+
* 3. `value` — working-memory blocks.
|
|
9
|
+
* 4. `id` fallback so the reranker never sees an empty passage.
|
|
10
|
+
*
|
|
11
|
+
* @stable
|
|
12
|
+
*/
|
|
13
|
+
function defaultPassageExtractor(record) {
|
|
14
|
+
const obj = record;
|
|
15
|
+
const text = obj.text;
|
|
16
|
+
if (typeof text === "string" && text.length > 0) return text;
|
|
17
|
+
const summary = obj.summary;
|
|
18
|
+
if (typeof summary === "string" && summary.length > 0) return summary;
|
|
19
|
+
const value = obj.value;
|
|
20
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
21
|
+
const label = obj.label;
|
|
22
|
+
if (typeof label === "string" && label.length > 0) return label;
|
|
23
|
+
return record.id;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
//#endregion
|
|
27
|
+
export { defaultPassageExtractor };
|
|
28
|
+
//# sourceMappingURL=text-extraction.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text-extraction.js","names":[],"sources":["../src/text-extraction.ts"],"sourcesContent":["/**\n * Heuristic text-extraction helpers. The reranker scores `(query,\n * passage)` pairs; this module turns a `MemoryRecord` of any concrete\n * tier into the best single-passage approximation.\n *\n * The defaults are conservative — text-bearing fields take precedence\n * over IDs / labels — and operators may inject a custom\n * `passageExtractor` when their schema attaches the canonical text\n * elsewhere.\n *\n * @packageDocumentation\n */\n\nimport type { MemoryRecord } from '@graphorin/core';\n\n/**\n * Returns the best-effort passage text for a {@link MemoryRecord}. The\n * order of preference, top-down:\n *\n * 1. `text` — facts, rules, generic text-bearing tiers.\n * 2. `summary` — episodes.\n * 3. `value` — working-memory blocks.\n * 4. `id` fallback so the reranker never sees an empty passage.\n *\n * @stable\n */\nexport function defaultPassageExtractor(record: MemoryRecord): string {\n const obj = record as unknown as Record<string, unknown>;\n const text = obj.text;\n if (typeof text === 'string' && text.length > 0) return text;\n const summary = obj.summary;\n if (typeof summary === 'string' && summary.length > 0) return summary;\n const value = obj.value;\n if (typeof value === 'string' && value.length > 0) return value;\n const label = obj.label;\n if (typeof label === 'string' && label.length > 0) return label;\n return record.id;\n}\n\n/**\n * Caller-supplied passage extractor. Receives the record + the\n * surrounding metadata (kind, sensitivity, tags) and returns the\n * passage to feed into the cross-encoder.\n *\n * @stable\n */\nexport type PassageExtractor<TRecord extends MemoryRecord = MemoryRecord> = (\n record: TRecord,\n) => string;\n"],"mappings":";;;;;;;;;;;;AA0BA,SAAgB,wBAAwB,QAA8B;CACpE,MAAM,MAAM;CACZ,MAAM,OAAO,IAAI;AACjB,KAAI,OAAO,SAAS,YAAY,KAAK,SAAS,EAAG,QAAO;CACxD,MAAM,UAAU,IAAI;AACpB,KAAI,OAAO,YAAY,YAAY,QAAQ,SAAS,EAAG,QAAO;CAC9D,MAAM,QAAQ,IAAI;AAClB,KAAI,OAAO,UAAU,YAAY,MAAM,SAAS,EAAG,QAAO;CAC1D,MAAM,QAAQ,IAAI;AAClB,KAAI,OAAO,UAAU,YAAY,MAAM,SAAS,EAAG,QAAO;AAC1D,QAAO,OAAO"}
|
package/package.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@graphorin/reranker-transformersjs",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Cross-encoder reranker adapter for the Graphorin framework. Wraps `@huggingface/transformers@^4.1.0` to score query / passage pairs in-process via the BAAI BGE family of cross-encoder models. Automatic model selection from the agent locale: `'en'` defaults to `Xenova/bge-reranker-base` (278M); every other locale defaults to `BAAI/bge-reranker-v2-m3` (568M, multilingual). FP16 quantization default; FP32 / Q8 opt-in. Lazy-loaded with an optional idle-eviction timer to bound resident memory. Implements the `ReRanker` contract exposed by `@graphorin/memory/search`. Created and maintained by Oleksiy Stepurenko.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Oleksiy Stepurenko",
|
|
7
|
+
"homepage": "https://github.com/o-stepper/graphorin/tree/main/packages/reranker-transformersjs",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/o-stepper/graphorin.git",
|
|
11
|
+
"directory": "packages/reranker-transformersjs"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/o-stepper/graphorin/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"graphorin",
|
|
18
|
+
"ai",
|
|
19
|
+
"agents",
|
|
20
|
+
"framework",
|
|
21
|
+
"rerank",
|
|
22
|
+
"reranker",
|
|
23
|
+
"cross-encoder",
|
|
24
|
+
"huggingface",
|
|
25
|
+
"transformersjs",
|
|
26
|
+
"bge",
|
|
27
|
+
"bge-reranker"
|
|
28
|
+
],
|
|
29
|
+
"type": "module",
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=22.0.0"
|
|
32
|
+
},
|
|
33
|
+
"main": "./dist/index.js",
|
|
34
|
+
"module": "./dist/index.js",
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"sideEffects": false,
|
|
37
|
+
"exports": {
|
|
38
|
+
".": {
|
|
39
|
+
"types": "./dist/index.d.ts",
|
|
40
|
+
"import": "./dist/index.js"
|
|
41
|
+
},
|
|
42
|
+
"./package.json": "./package.json"
|
|
43
|
+
},
|
|
44
|
+
"files": [
|
|
45
|
+
"dist",
|
|
46
|
+
"README.md",
|
|
47
|
+
"CHANGELOG.md",
|
|
48
|
+
"LICENSE"
|
|
49
|
+
],
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@graphorin/core": "0.5.0",
|
|
52
|
+
"@graphorin/memory": "0.5.0"
|
|
53
|
+
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"@huggingface/transformers": "^4.1.0"
|
|
56
|
+
},
|
|
57
|
+
"peerDependenciesMeta": {
|
|
58
|
+
"@huggingface/transformers": {
|
|
59
|
+
"optional": false
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"publishConfig": {
|
|
63
|
+
"access": "public",
|
|
64
|
+
"provenance": true
|
|
65
|
+
},
|
|
66
|
+
"scripts": {
|
|
67
|
+
"build": "tsdown",
|
|
68
|
+
"typecheck": "tsc --noEmit && tsc -p tsconfig.tests.json",
|
|
69
|
+
"test": "vitest run",
|
|
70
|
+
"lint": "biome check .",
|
|
71
|
+
"clean": "rimraf dist .turbo *.tsbuildinfo"
|
|
72
|
+
}
|
|
73
|
+
}
|