@graphorin/reranker-transformersjs 0.6.0 → 0.7.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 +26 -0
- package/README.md +3 -3
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/package.js +6 -0
- package/dist/package.js.map +1 -0
- package/package.json +6 -5
- package/src/cross-encoder.ts +148 -0
- package/src/index.ts +65 -0
- package/src/model-selection.ts +40 -0
- package/src/reranker.ts +318 -0
- package/src/text-extraction.ts +49 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# @graphorin/reranker-transformersjs
|
|
2
2
|
|
|
3
|
+
## 0.7.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#154](https://github.com/o-stepper/graphorin/pull/154) [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04) Thanks [@o-stepper](https://github.com/o-stepper)! - W-072: every export map's `import` condition becomes `default`, and the Node floor rises to `>=22.12.0`.
|
|
8
|
+
|
|
9
|
+
CJS consumers previously hit a bewildering `ERR_PACKAGE_PATH_NOT_EXPORTED` instead of a clear ESM-only signal. With the `default` condition, plain `require('@graphorin/core')` works via Node's stable `require(esm)` - which shipped in 22.12, hence the engines bump across every workspace manifest (packages, examples, benchmarks, docs; enforced by the widened mvp-readiness sweep). No dual-instance hazard: there is no CJS build, `require()` returns the same ESM module instance. ESM consumers are unaffected (`default` serves both paths; `types` stays first). The pack gate now runs attw under the full `node16` profile (was `esm-only`) and adds a runtime `require(esm)` smoke against the packed tarballs. Installs on Node 22.0-22.11 with `engine-strict` will refuse - upgrade Node (see the migration guide).
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- [#164](https://github.com/o-stepper/graphorin/pull/164) [`764239b`](https://github.com/o-stepper/graphorin/commit/764239b97e0e0202442e91272583f13adeb12d00) Thanks [@o-stepper](https://github.com/o-stepper)! - Tarballs now ship `src/` so the published `dist/**/*.d.ts.map` files actually work (W-136): the maps referenced `../src/*.ts` that the `files` whitelist excluded, so go-to-definition fell back into `.d.ts` and the shipped maps were dead weight. The pack gate gains a `map-integrity` leg: every source referenced by a shipped map must resolve inside the tarball (or be embedded via `sourcesContent`), with an anti-vacuous guard - a package whose tsdown config emits declaration maps must contain a non-zero number of `.d.ts.map` files, so a cache-restored dist that silently dropped maps fails the gate instead of passing vacuously. `mvp-readiness` now requires `src` in every publishable `files` array.
|
|
14
|
+
|
|
15
|
+
- Updated dependencies [[`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04), [`c1af9c7`](https://github.com/o-stepper/graphorin/commit/c1af9c790757fbe82da6dd2b6c1fdc497b5c605e), [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04), [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04), [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04), [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04), [`832f22e`](https://github.com/o-stepper/graphorin/commit/832f22e570b8c3175c1adeb4c150070cbd131534), [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04), [`832f22e`](https://github.com/o-stepper/graphorin/commit/832f22e570b8c3175c1adeb4c150070cbd131534), [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04), [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04), [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04), [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04), [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04), [`832f22e`](https://github.com/o-stepper/graphorin/commit/832f22e570b8c3175c1adeb4c150070cbd131534), [`32bbd03`](https://github.com/o-stepper/graphorin/commit/32bbd03b588136a355e4b5ad6ac5e19b36b4d8ab), [`32bbd03`](https://github.com/o-stepper/graphorin/commit/32bbd03b588136a355e4b5ad6ac5e19b36b4d8ab), [`32bbd03`](https://github.com/o-stepper/graphorin/commit/32bbd03b588136a355e4b5ad6ac5e19b36b4d8ab), [`4ee256e`](https://github.com/o-stepper/graphorin/commit/4ee256e30fe9190cef6c48dc6785464757707156), [`4ee256e`](https://github.com/o-stepper/graphorin/commit/4ee256e30fe9190cef6c48dc6785464757707156), [`4ee256e`](https://github.com/o-stepper/graphorin/commit/4ee256e30fe9190cef6c48dc6785464757707156), [`73b19ca`](https://github.com/o-stepper/graphorin/commit/73b19caeda388bda628a48138cb7d70b1db839a3), [`4ee256e`](https://github.com/o-stepper/graphorin/commit/4ee256e30fe9190cef6c48dc6785464757707156), [`4ee256e`](https://github.com/o-stepper/graphorin/commit/4ee256e30fe9190cef6c48dc6785464757707156), [`4ee256e`](https://github.com/o-stepper/graphorin/commit/4ee256e30fe9190cef6c48dc6785464757707156), [`4ee256e`](https://github.com/o-stepper/graphorin/commit/4ee256e30fe9190cef6c48dc6785464757707156), [`4ee256e`](https://github.com/o-stepper/graphorin/commit/4ee256e30fe9190cef6c48dc6785464757707156), [`4ee256e`](https://github.com/o-stepper/graphorin/commit/4ee256e30fe9190cef6c48dc6785464757707156), [`4ee256e`](https://github.com/o-stepper/graphorin/commit/4ee256e30fe9190cef6c48dc6785464757707156), [`4ee256e`](https://github.com/o-stepper/graphorin/commit/4ee256e30fe9190cef6c48dc6785464757707156), [`32bbd03`](https://github.com/o-stepper/graphorin/commit/32bbd03b588136a355e4b5ad6ac5e19b36b4d8ab), [`32bbd03`](https://github.com/o-stepper/graphorin/commit/32bbd03b588136a355e4b5ad6ac5e19b36b4d8ab), [`764239b`](https://github.com/o-stepper/graphorin/commit/764239b97e0e0202442e91272583f13adeb12d00), [`764239b`](https://github.com/o-stepper/graphorin/commit/764239b97e0e0202442e91272583f13adeb12d00), [`764239b`](https://github.com/o-stepper/graphorin/commit/764239b97e0e0202442e91272583f13adeb12d00), [`764239b`](https://github.com/o-stepper/graphorin/commit/764239b97e0e0202442e91272583f13adeb12d00), [`4ee256e`](https://github.com/o-stepper/graphorin/commit/4ee256e30fe9190cef6c48dc6785464757707156), [`764239b`](https://github.com/o-stepper/graphorin/commit/764239b97e0e0202442e91272583f13adeb12d00), [`4ee256e`](https://github.com/o-stepper/graphorin/commit/4ee256e30fe9190cef6c48dc6785464757707156), [`764239b`](https://github.com/o-stepper/graphorin/commit/764239b97e0e0202442e91272583f13adeb12d00), [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04), [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04), [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04)]:
|
|
16
|
+
- @graphorin/core@0.7.0
|
|
17
|
+
- @graphorin/memory@0.7.0
|
|
18
|
+
|
|
19
|
+
## 0.6.1
|
|
20
|
+
|
|
21
|
+
### Patch Changes
|
|
22
|
+
|
|
23
|
+
- [#142](https://github.com/o-stepper/graphorin/pull/142) [`436d6ca`](https://github.com/o-stepper/graphorin/commit/436d6ca5ebbd16df094e915682d3915c279a8430) Thanks [@o-stepper](https://github.com/o-stepper)! - Version constants and version-bearing strings now derive from each package's manifest at build time (`VERSION = pkg.version`; writer ids, client/server info, OTLP framework attributes, build-info metrics interpolate it). No behavioral change at the current version: the rendered strings are byte-identical. A release bump no longer edits source; the new `check-version-consistency` gate fails any reintroduced hardcoded framework version.
|
|
24
|
+
|
|
25
|
+
- Updated dependencies [[`436d6ca`](https://github.com/o-stepper/graphorin/commit/436d6ca5ebbd16df094e915682d3915c279a8430)]:
|
|
26
|
+
- @graphorin/core@0.6.1
|
|
27
|
+
- @graphorin/memory@0.6.1
|
|
28
|
+
|
|
3
29
|
## 0.6.0
|
|
4
30
|
|
|
5
31
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -7,14 +7,14 @@
|
|
|
7
7
|
> entirely in-process. Implements the `ReRanker` contract from
|
|
8
8
|
> `@graphorin/memory/search`.
|
|
9
9
|
>
|
|
10
|
-
> Project Graphorin · v0.
|
|
10
|
+
> Project Graphorin · v0.7.0 · MIT License · © 2026 Oleksiy Stepurenko ·
|
|
11
11
|
> <https://github.com/o-stepper/graphorin>
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
15
15
|
## Status
|
|
16
16
|
|
|
17
|
-
- **Published:** v0.
|
|
17
|
+
- **Published:** v0.7.0 (optional sub-pack)
|
|
18
18
|
- **Default model:** locale-aware (`Xenova/bge-reranker-base` for
|
|
19
19
|
English, `BAAI/bge-reranker-v2-m3` for everything else).
|
|
20
20
|
- **Default precision:** FP16 (`'fp16'` dtype).
|
|
@@ -120,4 +120,4 @@ MIT © 2026 Oleksiy Stepurenko
|
|
|
120
120
|
|
|
121
121
|
---
|
|
122
122
|
|
|
123
|
-
**Project Graphorin** · v0.
|
|
123
|
+
**Project Graphorin** · v0.7.0 · MIT License · © 2026 Oleksiy Stepurenko · <https://github.com/o-stepper/graphorin>
|
package/dist/index.d.ts
CHANGED
|
@@ -37,8 +37,7 @@ import { CrossEncoderRerankerOptions, RERANKER_ID, TransformersJsReRanker, creat
|
|
|
37
37
|
*
|
|
38
38
|
* @packageDocumentation
|
|
39
39
|
*/
|
|
40
|
-
|
|
41
|
-
declare const VERSION = "0.6.0";
|
|
40
|
+
declare const VERSION: string;
|
|
42
41
|
//#endregion
|
|
43
42
|
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
43
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;AAoCA;;;;;;;;;;;;;;;;;;;;;;;;;;cAAa"}
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { version } from "./package.js";
|
|
1
2
|
import { CrossEncoderLoadError, _resetPipelineFactoryCacheForTesting, extractPairScores, loadDefaultPipelineFactory } from "./cross-encoder.js";
|
|
2
3
|
import { DEFAULT_ENGLISH_MODEL, DEFAULT_MULTILINGUAL_MODEL, pickRerankerModel } from "./model-selection.js";
|
|
3
4
|
import { defaultPassageExtractor } from "./text-extraction.js";
|
|
@@ -36,8 +37,8 @@ import { RERANKER_ID, TransformersJsReRanker, createCrossEncoderReranker, mergeA
|
|
|
36
37
|
*
|
|
37
38
|
* @packageDocumentation
|
|
38
39
|
*/
|
|
39
|
-
/** Canonical version constant
|
|
40
|
-
const VERSION =
|
|
40
|
+
/** Canonical version constant, derived from `package.json` at build time. */
|
|
41
|
+
const VERSION = version;
|
|
41
42
|
|
|
42
43
|
//#endregion
|
|
43
44
|
export { CrossEncoderLoadError, DEFAULT_ENGLISH_MODEL, DEFAULT_MULTILINGUAL_MODEL, RERANKER_ID, TransformersJsReRanker, VERSION, _resetPipelineFactoryCacheForTesting, createCrossEncoderReranker, defaultPassageExtractor, extractPairScores, loadDefaultPipelineFactory, mergeAndDedupe, pickRerankerModel };
|
package/dist/index.js.map
CHANGED
|
@@ -1 +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
|
|
1
|
+
{"version":3,"file":"index.js","names":["VERSION: string","pkg.version"],"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, derived from `package.json` at build time. */\nimport pkg from '../package.json' with { type: 'json' };\n\nexport const VERSION: string = pkg.version;\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCA,MAAaA,UAAkBC"}
|
package/dist/package.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"package.js","names":[],"sources":["../package.json"],"sourcesContent":["{\n \"name\": \"@graphorin/reranker-transformersjs\",\n \"version\": \"0.7.0\",\n \"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.\",\n \"license\": \"MIT\",\n \"author\": \"Oleksiy Stepurenko\",\n \"homepage\": \"https://github.com/o-stepper/graphorin/tree/main/packages/reranker-transformersjs\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/o-stepper/graphorin.git\",\n \"directory\": \"packages/reranker-transformersjs\"\n },\n \"bugs\": {\n \"url\": \"https://github.com/o-stepper/graphorin/issues\"\n },\n \"keywords\": [\n \"graphorin\",\n \"ai\",\n \"agents\",\n \"framework\",\n \"rerank\",\n \"reranker\",\n \"cross-encoder\",\n \"huggingface\",\n \"transformersjs\",\n \"bge\",\n \"bge-reranker\"\n ],\n \"type\": \"module\",\n \"engines\": {\n \"node\": \">=22.12.0\"\n },\n \"main\": \"./dist/index.js\",\n \"module\": \"./dist/index.js\",\n \"types\": \"./dist/index.d.ts\",\n \"sideEffects\": false,\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"default\": \"./dist/index.js\"\n },\n \"./package.json\": \"./package.json\"\n },\n \"files\": [\n \"dist\",\n \"src\",\n \"README.md\",\n \"CHANGELOG.md\",\n \"LICENSE\"\n ],\n \"scripts\": {\n \"build\": \"tsdown\",\n \"typecheck\": \"tsc --noEmit && tsc -p tsconfig.tests.json\",\n \"test\": \"vitest run\",\n \"lint\": \"biome check .\",\n \"clean\": \"rimraf dist .turbo *.tsbuildinfo\"\n },\n \"dependencies\": {\n \"@graphorin/core\": \"workspace:*\",\n \"@graphorin/memory\": \"workspace:*\"\n },\n \"peerDependencies\": {\n \"@huggingface/transformers\": \"^4.1.0\"\n },\n \"peerDependenciesMeta\": {\n \"@huggingface/transformers\": {\n \"optional\": false\n }\n },\n \"publishConfig\": {\n \"access\": \"public\",\n \"provenance\": true\n }\n}\n"],"mappings":";cAEa"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@graphorin/reranker-transformersjs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
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
5
|
"license": "MIT",
|
|
6
6
|
"author": "Oleksiy Stepurenko",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
],
|
|
29
29
|
"type": "module",
|
|
30
30
|
"engines": {
|
|
31
|
-
"node": ">=22.
|
|
31
|
+
"node": ">=22.12.0"
|
|
32
32
|
},
|
|
33
33
|
"main": "./dist/index.js",
|
|
34
34
|
"module": "./dist/index.js",
|
|
@@ -37,19 +37,20 @@
|
|
|
37
37
|
"exports": {
|
|
38
38
|
".": {
|
|
39
39
|
"types": "./dist/index.d.ts",
|
|
40
|
-
"
|
|
40
|
+
"default": "./dist/index.js"
|
|
41
41
|
},
|
|
42
42
|
"./package.json": "./package.json"
|
|
43
43
|
},
|
|
44
44
|
"files": [
|
|
45
45
|
"dist",
|
|
46
|
+
"src",
|
|
46
47
|
"README.md",
|
|
47
48
|
"CHANGELOG.md",
|
|
48
49
|
"LICENSE"
|
|
49
50
|
],
|
|
50
51
|
"dependencies": {
|
|
51
|
-
"@graphorin/core": "0.
|
|
52
|
-
"@graphorin/memory": "0.
|
|
52
|
+
"@graphorin/core": "0.7.0",
|
|
53
|
+
"@graphorin/memory": "0.7.0"
|
|
53
54
|
},
|
|
54
55
|
"peerDependencies": {
|
|
55
56
|
"@huggingface/transformers": "^4.1.0"
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy `@huggingface/transformers` text-classification pipeline used to
|
|
3
|
+
* score query / passage pairs. The runner caches the first-loaded
|
|
4
|
+
* pipeline per `(model, dtype, device, revision, cacheDir)` tuple so
|
|
5
|
+
* repeated `rerank(...)` calls inside the same process do not pay the
|
|
6
|
+
* model-load cost twice.
|
|
7
|
+
*
|
|
8
|
+
* Tests inject a stub `pipelineFactory`; the production path lazily
|
|
9
|
+
* imports `@huggingface/transformers` so plain inspection of the
|
|
10
|
+
* package (e.g. for documentation generation) does not pull in the
|
|
11
|
+
* native ONNX runtime.
|
|
12
|
+
*
|
|
13
|
+
* @packageDocumentation
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Numeric dtype hint. Default `'fp16'` per Phase 16 §
|
|
18
|
+
* `@graphorin/reranker-transformersjs`.
|
|
19
|
+
*
|
|
20
|
+
* @stable
|
|
21
|
+
*/
|
|
22
|
+
export type RerankerDtype = 'fp32' | 'fp16' | 'q8' | 'q4';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Output shape returned by `@huggingface/transformers`'
|
|
26
|
+
* text-classification pipeline. Each pair returns either a single
|
|
27
|
+
* `{ label, score }` object (top-k = 1) or an array of them. We
|
|
28
|
+
* normalise on the array form upstream so the cross-encoder always
|
|
29
|
+
* sees a consistent shape.
|
|
30
|
+
*
|
|
31
|
+
* @internal
|
|
32
|
+
*/
|
|
33
|
+
export interface ClassifierResult {
|
|
34
|
+
readonly label: string;
|
|
35
|
+
readonly score: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** @internal */
|
|
39
|
+
export type CrossEncoderPipeline = (
|
|
40
|
+
pairs: ReadonlyArray<{ text: string; text_pair: string }>,
|
|
41
|
+
options?: { topk?: number; signal?: AbortSignal },
|
|
42
|
+
) => Promise<ReadonlyArray<ClassifierResult> | ReadonlyArray<ReadonlyArray<ClassifierResult>>>;
|
|
43
|
+
|
|
44
|
+
/** @internal */
|
|
45
|
+
export type CrossEncoderPipelineFactory = (
|
|
46
|
+
task: 'text-classification',
|
|
47
|
+
model: string,
|
|
48
|
+
options: {
|
|
49
|
+
revision?: string;
|
|
50
|
+
cache_dir?: string;
|
|
51
|
+
dtype?: RerankerDtype;
|
|
52
|
+
device?: string;
|
|
53
|
+
},
|
|
54
|
+
) => Promise<CrossEncoderPipeline>;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Raised when the `@huggingface/transformers` peer is missing or the
|
|
58
|
+
* configured cross-encoder model fails to load.
|
|
59
|
+
*
|
|
60
|
+
* @stable
|
|
61
|
+
*/
|
|
62
|
+
export class CrossEncoderLoadError extends Error {
|
|
63
|
+
override readonly name = 'CrossEncoderLoadError';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** @internal */
|
|
67
|
+
let CACHED_PIPELINE_FACTORY: CrossEncoderPipelineFactory | null = null;
|
|
68
|
+
|
|
69
|
+
/** @internal */
|
|
70
|
+
export async function loadDefaultPipelineFactory(): Promise<CrossEncoderPipelineFactory> {
|
|
71
|
+
if (CACHED_PIPELINE_FACTORY !== null) return CACHED_PIPELINE_FACTORY;
|
|
72
|
+
try {
|
|
73
|
+
const mod = (await import('@huggingface/transformers')) as unknown as {
|
|
74
|
+
pipeline: CrossEncoderPipelineFactory;
|
|
75
|
+
};
|
|
76
|
+
CACHED_PIPELINE_FACTORY = mod.pipeline;
|
|
77
|
+
return CACHED_PIPELINE_FACTORY;
|
|
78
|
+
} catch (err) {
|
|
79
|
+
throw new CrossEncoderLoadError(
|
|
80
|
+
"[graphorin/reranker-transformersjs] required peer '@huggingface/transformers' is not installed.",
|
|
81
|
+
{ cause: err },
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Test-only helper. Drops the cached pipeline factory so the next
|
|
88
|
+
* loader call re-imports the peer.
|
|
89
|
+
*
|
|
90
|
+
* @internal
|
|
91
|
+
*/
|
|
92
|
+
export function _resetPipelineFactoryCacheForTesting(): void {
|
|
93
|
+
CACHED_PIPELINE_FACTORY = null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* True when a classifier label names the *positive* (relevant) class. Matches
|
|
98
|
+
* the conventional binary forms - `LABEL_1`, `positive`, `relevant`,
|
|
99
|
+
* `entailment`, `true`, `yes` - using exact words so it does not mis-fire on a
|
|
100
|
+
* negative label that merely contains one (`irrelevant` ⊃ `relevant`).
|
|
101
|
+
*/
|
|
102
|
+
function isPositiveLabel(label: string): boolean {
|
|
103
|
+
const l = label.toLowerCase().trim();
|
|
104
|
+
if (l === '1' || /(?:^|[_-])1$/.test(l)) return true; // '1', 'label_1', 'class-1'
|
|
105
|
+
return /^(?:positive|relevant|entailment|true|yes|pos)$/.test(l);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Normalises the raw pipeline output to a flat `score[]` aligned with the input
|
|
110
|
+
* pair order. Cross-encoder classifiers return either a single-best
|
|
111
|
+
* `{label, score}` per pair (the default single-logit bge exports) or an array
|
|
112
|
+
* of `topk` entries. For the array shape we read the POSITIVE label's
|
|
113
|
+
* confidence - NOT the max of any label (PS-16): an irrelevant pair's most
|
|
114
|
+
* confident class is the *negative* one, so taking the max would invert the
|
|
115
|
+
* ranking for any 2-label classifier. When no label looks positive (single-logit
|
|
116
|
+
* or unrecognised labels) we fall back to the top score.
|
|
117
|
+
*
|
|
118
|
+
* @internal
|
|
119
|
+
*/
|
|
120
|
+
export function extractPairScores(
|
|
121
|
+
raw: ReadonlyArray<ClassifierResult> | ReadonlyArray<ReadonlyArray<ClassifierResult>>,
|
|
122
|
+
pairCount: number,
|
|
123
|
+
): number[] {
|
|
124
|
+
const out: number[] = new Array(pairCount).fill(0) as number[];
|
|
125
|
+
for (let i = 0; i < pairCount; i++) {
|
|
126
|
+
const cell = raw[i];
|
|
127
|
+
if (cell === undefined) continue;
|
|
128
|
+
if (Array.isArray(cell)) {
|
|
129
|
+
let best = Number.NEGATIVE_INFINITY;
|
|
130
|
+
let positive = Number.NEGATIVE_INFINITY;
|
|
131
|
+
let sawPositive = false;
|
|
132
|
+
for (const entry of cell) {
|
|
133
|
+
if (entry === undefined) continue;
|
|
134
|
+
if (entry.score > best) best = entry.score;
|
|
135
|
+
if (isPositiveLabel(entry.label)) {
|
|
136
|
+
sawPositive = true;
|
|
137
|
+
if (entry.score > positive) positive = entry.score;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const chosen = sawPositive ? positive : best;
|
|
141
|
+
out[i] = Number.isFinite(chosen) ? chosen : 0;
|
|
142
|
+
} else {
|
|
143
|
+
const single = cell as ClassifierResult;
|
|
144
|
+
out[i] = single.score;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @graphorin/reranker-transformersjs - cross-encoder reranker adapter
|
|
3
|
+
* for the Graphorin framework.
|
|
4
|
+
*
|
|
5
|
+
* Wraps `@huggingface/transformers@^4.1.0` to score `(query, passage)`
|
|
6
|
+
* pairs in-process. Plug into the memory hybrid-search pipeline as a
|
|
7
|
+
* drop-in replacement for the built-in `RRFReranker`:
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { createMemory } from '@graphorin/memory';
|
|
11
|
+
* import { createCrossEncoderReranker } from '@graphorin/reranker-transformersjs';
|
|
12
|
+
*
|
|
13
|
+
* const memory = createMemory({
|
|
14
|
+
* store,
|
|
15
|
+
* embedder,
|
|
16
|
+
* reranker: createCrossEncoderReranker({ locale: 'en' }),
|
|
17
|
+
* });
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* Locale-aware default model:
|
|
21
|
+
*
|
|
22
|
+
* - `'en'` / `'en-*'` → `Xenova/bge-reranker-base` (278M parameters,
|
|
23
|
+
* FP16 quantized).
|
|
24
|
+
* - Every other locale → `BAAI/bge-reranker-v2-m3` (568M parameters,
|
|
25
|
+
* multilingual baseline).
|
|
26
|
+
*
|
|
27
|
+
* Operators that want a narrower / language-specific cross-encoder
|
|
28
|
+
* pass an explicit `model` option - the package's defaults
|
|
29
|
+
* deliberately avoid privileging any single language pair.
|
|
30
|
+
*
|
|
31
|
+
* @packageDocumentation
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/** Canonical version constant, derived from `package.json` at build time. */
|
|
35
|
+
import pkg from '../package.json' with { type: 'json' };
|
|
36
|
+
|
|
37
|
+
export const VERSION: string = pkg.version;
|
|
38
|
+
|
|
39
|
+
export {
|
|
40
|
+
_resetPipelineFactoryCacheForTesting,
|
|
41
|
+
type ClassifierResult,
|
|
42
|
+
CrossEncoderLoadError,
|
|
43
|
+
type CrossEncoderPipeline,
|
|
44
|
+
type CrossEncoderPipelineFactory,
|
|
45
|
+
extractPairScores,
|
|
46
|
+
loadDefaultPipelineFactory,
|
|
47
|
+
type RerankerDtype,
|
|
48
|
+
} from './cross-encoder.js';
|
|
49
|
+
export {
|
|
50
|
+
DEFAULT_ENGLISH_MODEL,
|
|
51
|
+
DEFAULT_MULTILINGUAL_MODEL,
|
|
52
|
+
type LocaleTag,
|
|
53
|
+
pickRerankerModel,
|
|
54
|
+
} from './model-selection.js';
|
|
55
|
+
export {
|
|
56
|
+
type CrossEncoderRerankerOptions,
|
|
57
|
+
createCrossEncoderReranker,
|
|
58
|
+
mergeAndDedupe,
|
|
59
|
+
RERANKER_ID,
|
|
60
|
+
TransformersJsReRanker,
|
|
61
|
+
} from './reranker.js';
|
|
62
|
+
export {
|
|
63
|
+
defaultPassageExtractor,
|
|
64
|
+
type PassageExtractor,
|
|
65
|
+
} from './text-extraction.js';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locale-aware default model selector. Mirrors ADR-024 / DEC-120 § BGE
|
|
3
|
+
* model family:
|
|
4
|
+
*
|
|
5
|
+
* - English-only deployments → `Xenova/bge-reranker-base` (278M, FP16).
|
|
6
|
+
* - Every other locale → `BAAI/bge-reranker-v2-m3` (568M, multilingual).
|
|
7
|
+
*
|
|
8
|
+
* The selector deliberately stays language-pair-agnostic outside the
|
|
9
|
+
* English fast path; operators that want a narrower / language-specific
|
|
10
|
+
* cross-encoder pass an explicit `model` to {@link
|
|
11
|
+
* createCrossEncoderReranker}.
|
|
12
|
+
*
|
|
13
|
+
* @packageDocumentation
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* BCP 47 locale tag (e.g. `'en'`, `'en-GB'`, `'pt-BR'`, `'zh-Hans-CN'`).
|
|
18
|
+
*
|
|
19
|
+
* @stable
|
|
20
|
+
*/
|
|
21
|
+
export type LocaleTag = string;
|
|
22
|
+
|
|
23
|
+
/** @stable */
|
|
24
|
+
export const DEFAULT_ENGLISH_MODEL = 'Xenova/bge-reranker-base';
|
|
25
|
+
/** @stable */
|
|
26
|
+
export const DEFAULT_MULTILINGUAL_MODEL = 'BAAI/bge-reranker-v2-m3';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Pick a reranker model from the agent locale. Pure function so callers
|
|
30
|
+
* (and tests) can pre-resolve the choice without constructing the
|
|
31
|
+
* reranker.
|
|
32
|
+
*
|
|
33
|
+
* @stable
|
|
34
|
+
*/
|
|
35
|
+
export function pickRerankerModel(locale: LocaleTag | undefined): string {
|
|
36
|
+
if (locale === undefined || locale.length === 0) return DEFAULT_MULTILINGUAL_MODEL;
|
|
37
|
+
const normalized = locale.toLowerCase();
|
|
38
|
+
if (normalized === 'en' || normalized.startsWith('en-')) return DEFAULT_ENGLISH_MODEL;
|
|
39
|
+
return DEFAULT_MULTILINGUAL_MODEL;
|
|
40
|
+
}
|
package/src/reranker.ts
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-encoder `ReRanker` implementation backed by
|
|
3
|
+
* `@huggingface/transformers`. Lazy-loaded, optional idle eviction,
|
|
4
|
+
* locale-aware default model.
|
|
5
|
+
*
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { MemoryHit, MemoryRecord } from '@graphorin/core';
|
|
10
|
+
import type { ReRanker, ReRankOptions } from '@graphorin/memory/search';
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
CrossEncoderLoadError,
|
|
14
|
+
type CrossEncoderPipeline,
|
|
15
|
+
type CrossEncoderPipelineFactory,
|
|
16
|
+
extractPairScores,
|
|
17
|
+
loadDefaultPipelineFactory,
|
|
18
|
+
type RerankerDtype,
|
|
19
|
+
} from './cross-encoder.js';
|
|
20
|
+
import {
|
|
21
|
+
DEFAULT_ENGLISH_MODEL,
|
|
22
|
+
DEFAULT_MULTILINGUAL_MODEL,
|
|
23
|
+
type LocaleTag,
|
|
24
|
+
pickRerankerModel,
|
|
25
|
+
} from './model-selection.js';
|
|
26
|
+
import { defaultPassageExtractor, type PassageExtractor } from './text-extraction.js';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Options accepted by {@link createCrossEncoderReranker}.
|
|
30
|
+
*
|
|
31
|
+
* @stable
|
|
32
|
+
*/
|
|
33
|
+
export interface CrossEncoderRerankerOptions<TRecord extends MemoryRecord = MemoryRecord> {
|
|
34
|
+
/** Override the auto-picked model. Default: derived from `locale`. */
|
|
35
|
+
readonly model?: string;
|
|
36
|
+
/** BCP 47 locale tag used to select the default model. Default `'en'`. */
|
|
37
|
+
readonly locale?: LocaleTag;
|
|
38
|
+
/** Default `'fp16'`. */
|
|
39
|
+
readonly dtype?: RerankerDtype;
|
|
40
|
+
/** Optional revision pin (`'main'` if unset). */
|
|
41
|
+
readonly revision?: string;
|
|
42
|
+
/** Optional cache directory. Honours `GRAPHORIN_CACHE_DIR` when unset. */
|
|
43
|
+
readonly cacheDir?: string;
|
|
44
|
+
/** Override device (`'cpu'`, `'webgpu'`, …). Default `'cpu'`. */
|
|
45
|
+
readonly device?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Maximum batch size sent to the cross-encoder per inference call.
|
|
48
|
+
* Default `32`. Larger batches improve throughput at the cost of
|
|
49
|
+
* resident memory.
|
|
50
|
+
*/
|
|
51
|
+
readonly batchSize?: number;
|
|
52
|
+
/**
|
|
53
|
+
* Optional idle-eviction timeout in milliseconds. When the reranker
|
|
54
|
+
* does not score a pair within this window, the loaded pipeline is
|
|
55
|
+
* dropped so the OS can reclaim the underlying ONNX session. Default
|
|
56
|
+
* `undefined` (eviction disabled).
|
|
57
|
+
*/
|
|
58
|
+
readonly idleEvictionMs?: number;
|
|
59
|
+
/**
|
|
60
|
+
* Optional passage extractor - replaces the default heuristic that
|
|
61
|
+
* walks `text → summary → value → label → id`. Useful when a custom
|
|
62
|
+
* `MemoryRecord` schema attaches the canonical text elsewhere.
|
|
63
|
+
*/
|
|
64
|
+
readonly passageExtractor?: PassageExtractor<TRecord>;
|
|
65
|
+
/**
|
|
66
|
+
* Inject a `pipelineFactory`. Used by tests to stub the underlying
|
|
67
|
+
* `@huggingface/transformers` pipeline. Production callers leave
|
|
68
|
+
* this unset so the package lazily loads the peer.
|
|
69
|
+
*/
|
|
70
|
+
readonly pipelineFactory?: CrossEncoderPipelineFactory;
|
|
71
|
+
/**
|
|
72
|
+
* Override the wall-clock provider. Used by tests so the
|
|
73
|
+
* idle-eviction timer can be exercised deterministically.
|
|
74
|
+
*
|
|
75
|
+
* @internal
|
|
76
|
+
*/
|
|
77
|
+
readonly now?: () => number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** @stable */
|
|
81
|
+
export const RERANKER_ID = 'transformersjs-cross-encoder' as const;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Build a cross-encoder reranker. Lazy: the pipeline is constructed on
|
|
85
|
+
* the first `rerank()` call so packaging the reranker pays no
|
|
86
|
+
* model-load cost.
|
|
87
|
+
*
|
|
88
|
+
* @stable
|
|
89
|
+
*/
|
|
90
|
+
export function createCrossEncoderReranker<TRecord extends MemoryRecord = MemoryRecord>(
|
|
91
|
+
options: CrossEncoderRerankerOptions<TRecord> = {},
|
|
92
|
+
): TransformersJsReRanker<TRecord> {
|
|
93
|
+
return new TransformersJsReRanker<TRecord>(options);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* `ReRanker` implementation. Matches the contract from
|
|
98
|
+
* `@graphorin/memory/search`.
|
|
99
|
+
*
|
|
100
|
+
* @stable
|
|
101
|
+
*/
|
|
102
|
+
export class TransformersJsReRanker<TRecord extends MemoryRecord = MemoryRecord>
|
|
103
|
+
implements ReRanker
|
|
104
|
+
{
|
|
105
|
+
readonly id = RERANKER_ID;
|
|
106
|
+
readonly model: string;
|
|
107
|
+
readonly locale: LocaleTag;
|
|
108
|
+
readonly dtype: RerankerDtype;
|
|
109
|
+
readonly batchSize: number;
|
|
110
|
+
readonly idleEvictionMs: number | undefined;
|
|
111
|
+
|
|
112
|
+
readonly #revision: string | undefined;
|
|
113
|
+
readonly #cacheDir: string | undefined;
|
|
114
|
+
readonly #device: string | undefined;
|
|
115
|
+
readonly #pipelineFactory: CrossEncoderPipelineFactory | undefined;
|
|
116
|
+
readonly #passageExtractor: PassageExtractor<TRecord>;
|
|
117
|
+
readonly #now: () => number;
|
|
118
|
+
#pipeline: CrossEncoderPipeline | null = null;
|
|
119
|
+
#loading: Promise<CrossEncoderPipeline> | null = null;
|
|
120
|
+
#lastUsedAt: number = 0;
|
|
121
|
+
#idleTimer: ReturnType<typeof setTimeout> | null = null;
|
|
122
|
+
#invocationCount = 0;
|
|
123
|
+
|
|
124
|
+
constructor(options: CrossEncoderRerankerOptions<TRecord>) {
|
|
125
|
+
this.locale = options.locale ?? 'en';
|
|
126
|
+
this.model = options.model ?? pickRerankerModel(this.locale);
|
|
127
|
+
this.dtype = options.dtype ?? 'fp16';
|
|
128
|
+
this.batchSize = options.batchSize ?? 32;
|
|
129
|
+
if (!Number.isInteger(this.batchSize) || this.batchSize <= 0) {
|
|
130
|
+
throw new RangeError(
|
|
131
|
+
`[graphorin/reranker-transformersjs] batchSize must be a positive integer; got ${String(
|
|
132
|
+
options.batchSize,
|
|
133
|
+
)}.`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
this.idleEvictionMs = options.idleEvictionMs;
|
|
137
|
+
this.#revision = options.revision;
|
|
138
|
+
this.#cacheDir = options.cacheDir ?? process.env.GRAPHORIN_CACHE_DIR;
|
|
139
|
+
this.#device = options.device;
|
|
140
|
+
this.#pipelineFactory = options.pipelineFactory;
|
|
141
|
+
this.#passageExtractor =
|
|
142
|
+
options.passageExtractor ?? ((record: TRecord) => defaultPassageExtractor(record));
|
|
143
|
+
this.#now = options.now ?? Date.now;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Number of `rerank(...)` invocations since construction. Surfaced
|
|
148
|
+
* for observability + the test suite.
|
|
149
|
+
*
|
|
150
|
+
* @stable
|
|
151
|
+
*/
|
|
152
|
+
get invocationCount(): number {
|
|
153
|
+
return this.#invocationCount;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Whether the underlying ONNX pipeline is currently loaded in
|
|
158
|
+
* memory. Surfaced for the idle-eviction integration test.
|
|
159
|
+
*
|
|
160
|
+
* @stable
|
|
161
|
+
*/
|
|
162
|
+
get pipelineLoaded(): boolean {
|
|
163
|
+
return this.#pipeline !== null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Drop the loaded pipeline. Equivalent to letting the idle-eviction
|
|
168
|
+
* timer fire. Idempotent.
|
|
169
|
+
*
|
|
170
|
+
* @stable
|
|
171
|
+
*/
|
|
172
|
+
unload(): void {
|
|
173
|
+
this.#pipeline = null;
|
|
174
|
+
this.#loading = null;
|
|
175
|
+
if (this.#idleTimer !== null) {
|
|
176
|
+
clearTimeout(this.#idleTimer);
|
|
177
|
+
this.#idleTimer = null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async rerank<TInputRecord extends MemoryRecord>(
|
|
182
|
+
query: string,
|
|
183
|
+
lists: ReadonlyArray<ReadonlyArray<MemoryHit<TInputRecord>>>,
|
|
184
|
+
options: ReRankOptions = {},
|
|
185
|
+
): Promise<ReadonlyArray<MemoryHit<TInputRecord>>> {
|
|
186
|
+
if (options.signal?.aborted === true) {
|
|
187
|
+
throw new DOMException('TransformersJsReRanker aborted', 'AbortError');
|
|
188
|
+
}
|
|
189
|
+
this.#invocationCount += 1;
|
|
190
|
+
const merged = mergeAndDedupe(lists);
|
|
191
|
+
if (merged.length === 0) return [];
|
|
192
|
+
const pipeline = await this.#getPipeline();
|
|
193
|
+
const passages = merged.map((entry) =>
|
|
194
|
+
this.#passageExtractor(entry.hit.record as unknown as TRecord),
|
|
195
|
+
);
|
|
196
|
+
const scores = await this.#scoreInBatches(pipeline, query, passages, options.signal);
|
|
197
|
+
const fused: MemoryHit<TInputRecord>[] = merged.map((entry, idx) => {
|
|
198
|
+
const score = scores[idx] ?? 0;
|
|
199
|
+
const baseSignals = entry.hit.signals ?? {};
|
|
200
|
+
const signals: Record<string, number> = {
|
|
201
|
+
...baseSignals,
|
|
202
|
+
cross_encoder: score,
|
|
203
|
+
};
|
|
204
|
+
return Object.freeze({
|
|
205
|
+
record: entry.hit.record,
|
|
206
|
+
score,
|
|
207
|
+
signals: Object.freeze(signals),
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
fused.sort((a, b) => b.score - a.score);
|
|
211
|
+
const topK = options.topK ?? 10;
|
|
212
|
+
return fused.slice(0, Math.max(0, topK));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async #scoreInBatches(
|
|
216
|
+
pipeline: CrossEncoderPipeline,
|
|
217
|
+
query: string,
|
|
218
|
+
passages: ReadonlyArray<string>,
|
|
219
|
+
signal: AbortSignal | undefined,
|
|
220
|
+
): Promise<number[]> {
|
|
221
|
+
const out: number[] = [];
|
|
222
|
+
for (let i = 0; i < passages.length; i += this.batchSize) {
|
|
223
|
+
if (signal?.aborted === true) {
|
|
224
|
+
throw new DOMException('TransformersJsReRanker aborted', 'AbortError');
|
|
225
|
+
}
|
|
226
|
+
const batch = passages.slice(i, i + this.batchSize);
|
|
227
|
+
const pairs = batch.map((text_pair) => ({ text: query, text_pair }));
|
|
228
|
+
const raw = await pipeline(pairs, {
|
|
229
|
+
topk: 1,
|
|
230
|
+
...(signal !== undefined ? { signal } : {}),
|
|
231
|
+
});
|
|
232
|
+
const scores = extractPairScores(raw, batch.length);
|
|
233
|
+
for (const s of scores) out.push(s);
|
|
234
|
+
}
|
|
235
|
+
return out;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async #getPipeline(): Promise<CrossEncoderPipeline> {
|
|
239
|
+
this.#lastUsedAt = this.#now();
|
|
240
|
+
this.#scheduleIdleEviction();
|
|
241
|
+
if (this.#pipeline !== null) return this.#pipeline;
|
|
242
|
+
if (this.#loading !== null) return this.#loading;
|
|
243
|
+
const factory = this.#pipelineFactory ?? (await loadDefaultPipelineFactory());
|
|
244
|
+
this.#loading = factory('text-classification', this.model, {
|
|
245
|
+
...(this.#revision !== undefined ? { revision: this.#revision } : {}),
|
|
246
|
+
...(this.#cacheDir !== undefined ? { cache_dir: this.#cacheDir } : {}),
|
|
247
|
+
...(this.dtype !== undefined ? { dtype: this.dtype } : {}),
|
|
248
|
+
...(this.#device !== undefined ? { device: this.#device } : {}),
|
|
249
|
+
})
|
|
250
|
+
.then((pipe) => {
|
|
251
|
+
this.#pipeline = pipe;
|
|
252
|
+
this.#loading = null;
|
|
253
|
+
return pipe;
|
|
254
|
+
})
|
|
255
|
+
.catch((err) => {
|
|
256
|
+
this.#loading = null;
|
|
257
|
+
throw new CrossEncoderLoadError(
|
|
258
|
+
`[graphorin/reranker-transformersjs] failed to load model '${this.model}'.`,
|
|
259
|
+
{ cause: err },
|
|
260
|
+
);
|
|
261
|
+
});
|
|
262
|
+
return this.#loading;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
#scheduleIdleEviction(): void {
|
|
266
|
+
if (this.idleEvictionMs === undefined) return;
|
|
267
|
+
if (this.#idleTimer !== null) {
|
|
268
|
+
clearTimeout(this.#idleTimer);
|
|
269
|
+
}
|
|
270
|
+
this.#idleTimer = setTimeout(() => {
|
|
271
|
+
const elapsed = this.#now() - this.#lastUsedAt;
|
|
272
|
+
if (elapsed >= (this.idleEvictionMs ?? 0)) {
|
|
273
|
+
this.#pipeline = null;
|
|
274
|
+
this.#loading = null;
|
|
275
|
+
}
|
|
276
|
+
this.#idleTimer = null;
|
|
277
|
+
}, this.idleEvictionMs);
|
|
278
|
+
this.#idleTimer.unref?.();
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
interface MergedEntry<TRecord extends MemoryRecord> {
|
|
283
|
+
readonly hit: MemoryHit<TRecord>;
|
|
284
|
+
readonly firstSeenOrder: number;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Merge the per-source lists into a single deduplicated array,
|
|
289
|
+
* preserving the **highest** initial score per record id and the
|
|
290
|
+
* **first-seen order** for stable tie-breaking. Pure function;
|
|
291
|
+
* exported for the unit test fixture.
|
|
292
|
+
*
|
|
293
|
+
* @stable
|
|
294
|
+
*/
|
|
295
|
+
export function mergeAndDedupe<TRecord extends MemoryRecord>(
|
|
296
|
+
lists: ReadonlyArray<ReadonlyArray<MemoryHit<TRecord>>>,
|
|
297
|
+
): ReadonlyArray<MergedEntry<TRecord>> {
|
|
298
|
+
const out = new Map<string, MergedEntry<TRecord>>();
|
|
299
|
+
let order = 0;
|
|
300
|
+
for (const list of lists) {
|
|
301
|
+
for (const hit of list) {
|
|
302
|
+
const existing = out.get(hit.record.id);
|
|
303
|
+
if (existing === undefined) {
|
|
304
|
+
out.set(hit.record.id, { hit, firstSeenOrder: order++ });
|
|
305
|
+
} else if (hit.score > existing.hit.score) {
|
|
306
|
+
out.set(hit.record.id, {
|
|
307
|
+
hit,
|
|
308
|
+
firstSeenOrder: existing.firstSeenOrder,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
const arr = Array.from(out.values());
|
|
314
|
+
arr.sort((a, b) => a.firstSeenOrder - b.firstSeenOrder);
|
|
315
|
+
return arr;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export { DEFAULT_ENGLISH_MODEL, DEFAULT_MULTILINGUAL_MODEL };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heuristic text-extraction helpers. The reranker scores `(query,
|
|
3
|
+
* passage)` pairs; this module turns a `MemoryRecord` of any concrete
|
|
4
|
+
* tier into the best single-passage approximation.
|
|
5
|
+
*
|
|
6
|
+
* The defaults are conservative - text-bearing fields take precedence
|
|
7
|
+
* over IDs / labels - and operators may inject a custom
|
|
8
|
+
* `passageExtractor` when their schema attaches the canonical text
|
|
9
|
+
* elsewhere.
|
|
10
|
+
*
|
|
11
|
+
* @packageDocumentation
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { MemoryRecord } from '@graphorin/core';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Returns the best-effort passage text for a {@link MemoryRecord}. The
|
|
18
|
+
* order of preference, top-down:
|
|
19
|
+
*
|
|
20
|
+
* 1. `text` - facts, rules, generic text-bearing tiers.
|
|
21
|
+
* 2. `summary` - episodes.
|
|
22
|
+
* 3. `value` - working-memory blocks.
|
|
23
|
+
* 4. `id` fallback so the reranker never sees an empty passage.
|
|
24
|
+
*
|
|
25
|
+
* @stable
|
|
26
|
+
*/
|
|
27
|
+
export function defaultPassageExtractor(record: MemoryRecord): string {
|
|
28
|
+
const obj = record as unknown as Record<string, unknown>;
|
|
29
|
+
const text = obj.text;
|
|
30
|
+
if (typeof text === 'string' && text.length > 0) return text;
|
|
31
|
+
const summary = obj.summary;
|
|
32
|
+
if (typeof summary === 'string' && summary.length > 0) return summary;
|
|
33
|
+
const value = obj.value;
|
|
34
|
+
if (typeof value === 'string' && value.length > 0) return value;
|
|
35
|
+
const label = obj.label;
|
|
36
|
+
if (typeof label === 'string' && label.length > 0) return label;
|
|
37
|
+
return record.id;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Caller-supplied passage extractor. Receives the record + the
|
|
42
|
+
* surrounding metadata (kind, sensitivity, tags) and returns the
|
|
43
|
+
* passage to feed into the cross-encoder.
|
|
44
|
+
*
|
|
45
|
+
* @stable
|
|
46
|
+
*/
|
|
47
|
+
export type PassageExtractor<TRecord extends MemoryRecord = MemoryRecord> = (
|
|
48
|
+
record: TRecord,
|
|
49
|
+
) => string;
|