@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 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"}
@@ -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"}
@@ -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":"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"}
@@ -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
+ }