agent-working-memory 0.9.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/migrate.js +29 -29
- package/dist/cli.js +6 -2
- package/dist/cli.js.map +1 -1
- package/dist/coordination/circuit-breaker.js +23 -23
- package/dist/storage/pglite-schema.js +143 -143
- package/dist/storage/pglite.js +138 -138
- package/package.json +1 -1
- package/src/api/index.ts +3 -3
- package/src/cli/migrate.ts +307 -307
- package/src/cli.ts +6 -2
- package/src/coordination/circuit-breaker.ts +83 -83
- package/src/coordination/failure-modes.ts +50 -50
- package/src/core/decay.ts +63 -63
- package/src/core/embeddings.ts +110 -110
- package/src/core/index.ts +5 -5
- package/src/core/logger.ts +36 -36
- package/src/core/ml-worker-entry.ts +194 -194
- package/src/core/ml-worker.ts +281 -281
- package/src/core/query-expander.ts +122 -122
- package/src/core/reranker.ts +119 -119
- package/src/engine/confidence.ts +120 -120
- package/src/engine/consolidation-scheduler.ts +242 -242
- package/src/engine/eval.ts +102 -102
- package/src/engine/eviction.ts +101 -101
- package/src/engine/index.ts +8 -8
- package/src/engine/retraction.ts +366 -366
- package/src/engine/staging.ts +74 -74
- package/src/storage/factory.ts +147 -147
- package/src/storage/index.ts +3 -3
- package/src/storage/pglite-schema.ts +166 -166
- package/src/storage/pglite.ts +1363 -1363
- package/src/storage/store.ts +80 -80
- package/src/types/agent.ts +67 -67
- package/src/types/checkpoint.ts +46 -46
- package/src/types/eval.ts +100 -100
- package/src/types/index.ts +6 -6
|
@@ -1,122 +1,122 @@
|
|
|
1
|
-
// Copyright 2026 Robert Winter / Complete Ideas
|
|
2
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
/**
|
|
4
|
-
* Query Expander - rewrites queries with synonyms and related terms.
|
|
5
|
-
*
|
|
6
|
-
* Uses Xenova/flan-t5-small (~80MB ONNX) to expand search queries with
|
|
7
|
-
* related terms that improve BM25 recall.
|
|
8
|
-
*
|
|
9
|
-
* AWM 0.8.x: inference dispatches through ml-worker.ts (currently in-process
|
|
10
|
-
* — worker_threads reverted because onnxruntime-node bindings cross isolate
|
|
11
|
-
* boundaries unsafely; see ml-worker.ts). The dispatch abstraction is
|
|
12
|
-
* preserved for a future child_process / HTTP sidecar pool.
|
|
13
|
-
*
|
|
14
|
-
* The LRU cache + skip heuristic stay on the main thread — they're pure
|
|
15
|
-
* filter/lookup logic that shouldn't pay IPC cost.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { pipeline, type Text2TextGenerationPipeline } from '@huggingface/transformers';
|
|
19
|
-
import { dispatchExpand, registerInProcessHandlers } from './ml-worker.js';
|
|
20
|
-
|
|
21
|
-
const MODEL_ID = 'Xenova/flan-t5-small';
|
|
22
|
-
|
|
23
|
-
// --- In-process fallback ---
|
|
24
|
-
|
|
25
|
-
let inProcessInstance: Text2TextGenerationPipeline | null = null;
|
|
26
|
-
let inProcessInitPromise: Promise<Text2TextGenerationPipeline> | null = null;
|
|
27
|
-
|
|
28
|
-
async function loadInProcess(): Promise<Text2TextGenerationPipeline> {
|
|
29
|
-
if (inProcessInstance) return inProcessInstance;
|
|
30
|
-
if (inProcessInitPromise) return inProcessInitPromise;
|
|
31
|
-
inProcessInitPromise = pipeline('text2text-generation', MODEL_ID, { dtype: 'fp32' }).then(pipe => {
|
|
32
|
-
inProcessInstance = pipe as Text2TextGenerationPipeline;
|
|
33
|
-
console.log(`Query expander loaded in-process: ${MODEL_ID}`);
|
|
34
|
-
return inProcessInstance;
|
|
35
|
-
});
|
|
36
|
-
return inProcessInitPromise;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async function inProcessExpand(args: { prompt: string; maxNewTokens: number; noRepeatNgramSize: number }): Promise<string> {
|
|
40
|
-
const expander = await loadInProcess();
|
|
41
|
-
const result = await expander(args.prompt, {
|
|
42
|
-
max_new_tokens: args.maxNewTokens,
|
|
43
|
-
no_repeat_ngram_size: args.noRepeatNgramSize,
|
|
44
|
-
});
|
|
45
|
-
const text = Array.isArray(result) ? (result[0] as any)?.generated_text ?? '' : '';
|
|
46
|
-
return String(text).trim();
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
registerInProcessHandlers({ expand: inProcessExpand });
|
|
50
|
-
|
|
51
|
-
// --- Public API ---
|
|
52
|
-
|
|
53
|
-
/** Kept for backwards compat. */
|
|
54
|
-
export async function getExpander(): Promise<Text2TextGenerationPipeline> {
|
|
55
|
-
return loadInProcess();
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* LRU cache of normalized-query → expanded-query mappings.
|
|
60
|
-
* Lives on the main thread — cache hits skip the worker IPC entirely.
|
|
61
|
-
*/
|
|
62
|
-
const expansionCache = new Map<string, string>();
|
|
63
|
-
const EXPANSION_CACHE_LIMIT = 500;
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Skip expansion when the query is already specific (long or many tokens).
|
|
67
|
-
*/
|
|
68
|
-
function shouldSkipExpansion(normalized: string): boolean {
|
|
69
|
-
if (normalized.length === 0) return true;
|
|
70
|
-
if (normalized.length > 50) return true;
|
|
71
|
-
const tokens = new Set(normalized.split(/\s+/).filter(t => t.length > 2));
|
|
72
|
-
return tokens.size >= 5;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Expand a query with related terms and synonyms.
|
|
77
|
-
* Returns the original query + generated expansion terms.
|
|
78
|
-
* Falls back to the original query on any error.
|
|
79
|
-
*
|
|
80
|
-
* Dispatches inference to the worker pool. Cache + skip heuristic stay
|
|
81
|
-
* on the main thread.
|
|
82
|
-
*/
|
|
83
|
-
export async function expandQuery(originalQuery: string): Promise<string> {
|
|
84
|
-
const normalized = originalQuery.toLowerCase().trim();
|
|
85
|
-
const optimizationsEnabled = process.env.AWM_DISABLE_EXPANSION_CACHE !== '1';
|
|
86
|
-
|
|
87
|
-
if (optimizationsEnabled) {
|
|
88
|
-
if (shouldSkipExpansion(normalized)) return originalQuery;
|
|
89
|
-
const cached = expansionCache.get(normalized);
|
|
90
|
-
if (cached !== undefined) {
|
|
91
|
-
// LRU touch
|
|
92
|
-
expansionCache.delete(normalized);
|
|
93
|
-
expansionCache.set(normalized, cached);
|
|
94
|
-
return cached;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
try {
|
|
99
|
-
const prompt = `Expand this search query with synonyms and related terms. Only output the additional terms, not the original query. Query: ${originalQuery}. Additional terms:`;
|
|
100
|
-
const expansion = await dispatchExpand({ prompt, maxNewTokens: 25, noRepeatNgramSize: 2 });
|
|
101
|
-
const finalQuery = expansion && expansion.length > 2
|
|
102
|
-
? `${originalQuery} ${expansion}`
|
|
103
|
-
: originalQuery;
|
|
104
|
-
|
|
105
|
-
if (optimizationsEnabled) {
|
|
106
|
-
if (expansionCache.size >= EXPANSION_CACHE_LIMIT) {
|
|
107
|
-
const oldestKey = expansionCache.keys().next().value;
|
|
108
|
-
if (oldestKey !== undefined) expansionCache.delete(oldestKey);
|
|
109
|
-
}
|
|
110
|
-
expansionCache.set(normalized, finalQuery);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return finalQuery;
|
|
114
|
-
} catch {
|
|
115
|
-
return originalQuery;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/** Clear the expansion cache (used by tests + cache invalidation). */
|
|
120
|
-
export function clearExpansionCache(): void {
|
|
121
|
-
expansionCache.clear();
|
|
122
|
-
}
|
|
1
|
+
// Copyright 2026 Robert Winter / Complete Ideas
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* Query Expander - rewrites queries with synonyms and related terms.
|
|
5
|
+
*
|
|
6
|
+
* Uses Xenova/flan-t5-small (~80MB ONNX) to expand search queries with
|
|
7
|
+
* related terms that improve BM25 recall.
|
|
8
|
+
*
|
|
9
|
+
* AWM 0.8.x: inference dispatches through ml-worker.ts (currently in-process
|
|
10
|
+
* — worker_threads reverted because onnxruntime-node bindings cross isolate
|
|
11
|
+
* boundaries unsafely; see ml-worker.ts). The dispatch abstraction is
|
|
12
|
+
* preserved for a future child_process / HTTP sidecar pool.
|
|
13
|
+
*
|
|
14
|
+
* The LRU cache + skip heuristic stay on the main thread — they're pure
|
|
15
|
+
* filter/lookup logic that shouldn't pay IPC cost.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { pipeline, type Text2TextGenerationPipeline } from '@huggingface/transformers';
|
|
19
|
+
import { dispatchExpand, registerInProcessHandlers } from './ml-worker.js';
|
|
20
|
+
|
|
21
|
+
const MODEL_ID = 'Xenova/flan-t5-small';
|
|
22
|
+
|
|
23
|
+
// --- In-process fallback ---
|
|
24
|
+
|
|
25
|
+
let inProcessInstance: Text2TextGenerationPipeline | null = null;
|
|
26
|
+
let inProcessInitPromise: Promise<Text2TextGenerationPipeline> | null = null;
|
|
27
|
+
|
|
28
|
+
async function loadInProcess(): Promise<Text2TextGenerationPipeline> {
|
|
29
|
+
if (inProcessInstance) return inProcessInstance;
|
|
30
|
+
if (inProcessInitPromise) return inProcessInitPromise;
|
|
31
|
+
inProcessInitPromise = pipeline('text2text-generation', MODEL_ID, { dtype: 'fp32' }).then(pipe => {
|
|
32
|
+
inProcessInstance = pipe as Text2TextGenerationPipeline;
|
|
33
|
+
console.log(`Query expander loaded in-process: ${MODEL_ID}`);
|
|
34
|
+
return inProcessInstance;
|
|
35
|
+
});
|
|
36
|
+
return inProcessInitPromise;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function inProcessExpand(args: { prompt: string; maxNewTokens: number; noRepeatNgramSize: number }): Promise<string> {
|
|
40
|
+
const expander = await loadInProcess();
|
|
41
|
+
const result = await expander(args.prompt, {
|
|
42
|
+
max_new_tokens: args.maxNewTokens,
|
|
43
|
+
no_repeat_ngram_size: args.noRepeatNgramSize,
|
|
44
|
+
});
|
|
45
|
+
const text = Array.isArray(result) ? (result[0] as any)?.generated_text ?? '' : '';
|
|
46
|
+
return String(text).trim();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
registerInProcessHandlers({ expand: inProcessExpand });
|
|
50
|
+
|
|
51
|
+
// --- Public API ---
|
|
52
|
+
|
|
53
|
+
/** Kept for backwards compat. */
|
|
54
|
+
export async function getExpander(): Promise<Text2TextGenerationPipeline> {
|
|
55
|
+
return loadInProcess();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* LRU cache of normalized-query → expanded-query mappings.
|
|
60
|
+
* Lives on the main thread — cache hits skip the worker IPC entirely.
|
|
61
|
+
*/
|
|
62
|
+
const expansionCache = new Map<string, string>();
|
|
63
|
+
const EXPANSION_CACHE_LIMIT = 500;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Skip expansion when the query is already specific (long or many tokens).
|
|
67
|
+
*/
|
|
68
|
+
function shouldSkipExpansion(normalized: string): boolean {
|
|
69
|
+
if (normalized.length === 0) return true;
|
|
70
|
+
if (normalized.length > 50) return true;
|
|
71
|
+
const tokens = new Set(normalized.split(/\s+/).filter(t => t.length > 2));
|
|
72
|
+
return tokens.size >= 5;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Expand a query with related terms and synonyms.
|
|
77
|
+
* Returns the original query + generated expansion terms.
|
|
78
|
+
* Falls back to the original query on any error.
|
|
79
|
+
*
|
|
80
|
+
* Dispatches inference to the worker pool. Cache + skip heuristic stay
|
|
81
|
+
* on the main thread.
|
|
82
|
+
*/
|
|
83
|
+
export async function expandQuery(originalQuery: string): Promise<string> {
|
|
84
|
+
const normalized = originalQuery.toLowerCase().trim();
|
|
85
|
+
const optimizationsEnabled = process.env.AWM_DISABLE_EXPANSION_CACHE !== '1';
|
|
86
|
+
|
|
87
|
+
if (optimizationsEnabled) {
|
|
88
|
+
if (shouldSkipExpansion(normalized)) return originalQuery;
|
|
89
|
+
const cached = expansionCache.get(normalized);
|
|
90
|
+
if (cached !== undefined) {
|
|
91
|
+
// LRU touch
|
|
92
|
+
expansionCache.delete(normalized);
|
|
93
|
+
expansionCache.set(normalized, cached);
|
|
94
|
+
return cached;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const prompt = `Expand this search query with synonyms and related terms. Only output the additional terms, not the original query. Query: ${originalQuery}. Additional terms:`;
|
|
100
|
+
const expansion = await dispatchExpand({ prompt, maxNewTokens: 25, noRepeatNgramSize: 2 });
|
|
101
|
+
const finalQuery = expansion && expansion.length > 2
|
|
102
|
+
? `${originalQuery} ${expansion}`
|
|
103
|
+
: originalQuery;
|
|
104
|
+
|
|
105
|
+
if (optimizationsEnabled) {
|
|
106
|
+
if (expansionCache.size >= EXPANSION_CACHE_LIMIT) {
|
|
107
|
+
const oldestKey = expansionCache.keys().next().value;
|
|
108
|
+
if (oldestKey !== undefined) expansionCache.delete(oldestKey);
|
|
109
|
+
}
|
|
110
|
+
expansionCache.set(normalized, finalQuery);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return finalQuery;
|
|
114
|
+
} catch {
|
|
115
|
+
return originalQuery;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Clear the expansion cache (used by tests + cache invalidation). */
|
|
120
|
+
export function clearExpansionCache(): void {
|
|
121
|
+
expansionCache.clear();
|
|
122
|
+
}
|
package/src/core/reranker.ts
CHANGED
|
@@ -1,119 +1,119 @@
|
|
|
1
|
-
// Copyright 2026 Robert Winter / Complete Ideas
|
|
2
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
/**
|
|
4
|
-
* Cross-Encoder Re-Ranker - scores (query, passage) pairs for relevance.
|
|
5
|
-
*
|
|
6
|
-
* Uses Xenova/ms-marco-MiniLM-L-6-v2 (~22MB ONNX) trained on MS-MARCO
|
|
7
|
-
* passage ranking. Unlike bi-encoders, cross-encoders see both query and
|
|
8
|
-
* passage together via full attention - much better at judging if a
|
|
9
|
-
* passage actually answers a question.
|
|
10
|
-
*
|
|
11
|
-
* AWM 0.8.x: inference dispatches through ml-worker.ts (currently in-process
|
|
12
|
-
* — see ml-worker.ts for the worker_threads → in-process revert rationale).
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import {
|
|
16
|
-
AutoTokenizer,
|
|
17
|
-
AutoModelForSequenceClassification,
|
|
18
|
-
type PreTrainedTokenizer,
|
|
19
|
-
type PreTrainedModel,
|
|
20
|
-
} from '@huggingface/transformers';
|
|
21
|
-
import { dispatchRerank, registerInProcessHandlers } from './ml-worker.js';
|
|
22
|
-
|
|
23
|
-
const DEFAULT_MODEL = 'Xenova/ms-marco-MiniLM-L-6-v2';
|
|
24
|
-
const MODEL_ID = process.env.AWM_RERANKER_MODEL || DEFAULT_MODEL;
|
|
25
|
-
|
|
26
|
-
// --- In-process fallback ---
|
|
27
|
-
|
|
28
|
-
let tokenizer: PreTrainedTokenizer | null = null;
|
|
29
|
-
let model: PreTrainedModel | null = null;
|
|
30
|
-
let initPromise: Promise<void> | null = null;
|
|
31
|
-
|
|
32
|
-
async function ensureLoaded(): Promise<void> {
|
|
33
|
-
if (tokenizer && model) return;
|
|
34
|
-
if (initPromise) return initPromise;
|
|
35
|
-
initPromise = (async () => {
|
|
36
|
-
tokenizer = await AutoTokenizer.from_pretrained(MODEL_ID);
|
|
37
|
-
model = await AutoModelForSequenceClassification.from_pretrained(MODEL_ID, { dtype: 'fp32' });
|
|
38
|
-
console.log(`Re-ranker model loaded in-process: ${MODEL_ID}`);
|
|
39
|
-
})();
|
|
40
|
-
return initPromise;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function sigmoid(x: number): number {
|
|
44
|
-
return 1 / (1 + Math.exp(-x));
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async function inProcessRerank(args: { query: string; passages: string[] }): Promise<Array<{ index: number; score: number }>> {
|
|
48
|
-
const { query, passages } = args;
|
|
49
|
-
if (passages.length === 0) return [];
|
|
50
|
-
await ensureLoaded();
|
|
51
|
-
|
|
52
|
-
// Batch path
|
|
53
|
-
try {
|
|
54
|
-
const queries = passages.map(() => query);
|
|
55
|
-
const inputs = tokenizer!(queries, {
|
|
56
|
-
text_pair: passages,
|
|
57
|
-
padding: true,
|
|
58
|
-
truncation: true,
|
|
59
|
-
return_tensors: 'pt',
|
|
60
|
-
});
|
|
61
|
-
const output = await model!(inputs);
|
|
62
|
-
const logits = output.logits ?? output.last_hidden_state;
|
|
63
|
-
const data = logits.data as Float32Array | number[];
|
|
64
|
-
const results: Array<{ index: number; score: number }> = [];
|
|
65
|
-
for (let i = 0; i < passages.length; i++) {
|
|
66
|
-
const rawLogit = Number(data[i] ?? 0);
|
|
67
|
-
results.push({ index: i, score: sigmoid(rawLogit) });
|
|
68
|
-
}
|
|
69
|
-
results.sort((a, b) => b.score - a.score);
|
|
70
|
-
return results;
|
|
71
|
-
} catch {
|
|
72
|
-
// Per-passage fallback (the original 0.7.13 path)
|
|
73
|
-
const results: Array<{ index: number; score: number }> = [];
|
|
74
|
-
for (let i = 0; i < passages.length; i++) {
|
|
75
|
-
try {
|
|
76
|
-
const inputs = tokenizer!(query, {
|
|
77
|
-
text_pair: passages[i],
|
|
78
|
-
padding: true,
|
|
79
|
-
truncation: true,
|
|
80
|
-
return_tensors: 'pt',
|
|
81
|
-
});
|
|
82
|
-
const output = await model!(inputs);
|
|
83
|
-
const logits = output.logits ?? output.last_hidden_state;
|
|
84
|
-
const rawLogit = logits.data[0] as number;
|
|
85
|
-
results.push({ index: i, score: sigmoid(rawLogit) });
|
|
86
|
-
} catch {
|
|
87
|
-
results.push({ index: i, score: 0 });
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
results.sort((a, b) => b.score - a.score);
|
|
91
|
-
return results;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Register the in-process handler with the pool
|
|
96
|
-
registerInProcessHandlers({ rerank: inProcessRerank });
|
|
97
|
-
|
|
98
|
-
// --- Public API ---
|
|
99
|
-
|
|
100
|
-
/** Kept for backwards compat. */
|
|
101
|
-
export async function getReranker(): Promise<any> {
|
|
102
|
-
await ensureLoaded();
|
|
103
|
-
return model;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export interface RerankResult {
|
|
107
|
-
index: number;
|
|
108
|
-
score: number; // sigmoid-normalized relevance (0-1)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Re-rank candidate passages against a query using the cross-encoder.
|
|
113
|
-
* Returns results sorted by relevance score (descending).
|
|
114
|
-
* Dispatches to the worker pool (or in-process fallback).
|
|
115
|
-
*/
|
|
116
|
-
export async function rerank(query: string, passages: string[]): Promise<RerankResult[]> {
|
|
117
|
-
if (passages.length === 0) return [];
|
|
118
|
-
return dispatchRerank({ query, passages });
|
|
119
|
-
}
|
|
1
|
+
// Copyright 2026 Robert Winter / Complete Ideas
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* Cross-Encoder Re-Ranker - scores (query, passage) pairs for relevance.
|
|
5
|
+
*
|
|
6
|
+
* Uses Xenova/ms-marco-MiniLM-L-6-v2 (~22MB ONNX) trained on MS-MARCO
|
|
7
|
+
* passage ranking. Unlike bi-encoders, cross-encoders see both query and
|
|
8
|
+
* passage together via full attention - much better at judging if a
|
|
9
|
+
* passage actually answers a question.
|
|
10
|
+
*
|
|
11
|
+
* AWM 0.8.x: inference dispatches through ml-worker.ts (currently in-process
|
|
12
|
+
* — see ml-worker.ts for the worker_threads → in-process revert rationale).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
AutoTokenizer,
|
|
17
|
+
AutoModelForSequenceClassification,
|
|
18
|
+
type PreTrainedTokenizer,
|
|
19
|
+
type PreTrainedModel,
|
|
20
|
+
} from '@huggingface/transformers';
|
|
21
|
+
import { dispatchRerank, registerInProcessHandlers } from './ml-worker.js';
|
|
22
|
+
|
|
23
|
+
const DEFAULT_MODEL = 'Xenova/ms-marco-MiniLM-L-6-v2';
|
|
24
|
+
const MODEL_ID = process.env.AWM_RERANKER_MODEL || DEFAULT_MODEL;
|
|
25
|
+
|
|
26
|
+
// --- In-process fallback ---
|
|
27
|
+
|
|
28
|
+
let tokenizer: PreTrainedTokenizer | null = null;
|
|
29
|
+
let model: PreTrainedModel | null = null;
|
|
30
|
+
let initPromise: Promise<void> | null = null;
|
|
31
|
+
|
|
32
|
+
async function ensureLoaded(): Promise<void> {
|
|
33
|
+
if (tokenizer && model) return;
|
|
34
|
+
if (initPromise) return initPromise;
|
|
35
|
+
initPromise = (async () => {
|
|
36
|
+
tokenizer = await AutoTokenizer.from_pretrained(MODEL_ID);
|
|
37
|
+
model = await AutoModelForSequenceClassification.from_pretrained(MODEL_ID, { dtype: 'fp32' });
|
|
38
|
+
console.log(`Re-ranker model loaded in-process: ${MODEL_ID}`);
|
|
39
|
+
})();
|
|
40
|
+
return initPromise;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function sigmoid(x: number): number {
|
|
44
|
+
return 1 / (1 + Math.exp(-x));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function inProcessRerank(args: { query: string; passages: string[] }): Promise<Array<{ index: number; score: number }>> {
|
|
48
|
+
const { query, passages } = args;
|
|
49
|
+
if (passages.length === 0) return [];
|
|
50
|
+
await ensureLoaded();
|
|
51
|
+
|
|
52
|
+
// Batch path
|
|
53
|
+
try {
|
|
54
|
+
const queries = passages.map(() => query);
|
|
55
|
+
const inputs = tokenizer!(queries, {
|
|
56
|
+
text_pair: passages,
|
|
57
|
+
padding: true,
|
|
58
|
+
truncation: true,
|
|
59
|
+
return_tensors: 'pt',
|
|
60
|
+
});
|
|
61
|
+
const output = await model!(inputs);
|
|
62
|
+
const logits = output.logits ?? output.last_hidden_state;
|
|
63
|
+
const data = logits.data as Float32Array | number[];
|
|
64
|
+
const results: Array<{ index: number; score: number }> = [];
|
|
65
|
+
for (let i = 0; i < passages.length; i++) {
|
|
66
|
+
const rawLogit = Number(data[i] ?? 0);
|
|
67
|
+
results.push({ index: i, score: sigmoid(rawLogit) });
|
|
68
|
+
}
|
|
69
|
+
results.sort((a, b) => b.score - a.score);
|
|
70
|
+
return results;
|
|
71
|
+
} catch {
|
|
72
|
+
// Per-passage fallback (the original 0.7.13 path)
|
|
73
|
+
const results: Array<{ index: number; score: number }> = [];
|
|
74
|
+
for (let i = 0; i < passages.length; i++) {
|
|
75
|
+
try {
|
|
76
|
+
const inputs = tokenizer!(query, {
|
|
77
|
+
text_pair: passages[i],
|
|
78
|
+
padding: true,
|
|
79
|
+
truncation: true,
|
|
80
|
+
return_tensors: 'pt',
|
|
81
|
+
});
|
|
82
|
+
const output = await model!(inputs);
|
|
83
|
+
const logits = output.logits ?? output.last_hidden_state;
|
|
84
|
+
const rawLogit = logits.data[0] as number;
|
|
85
|
+
results.push({ index: i, score: sigmoid(rawLogit) });
|
|
86
|
+
} catch {
|
|
87
|
+
results.push({ index: i, score: 0 });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
results.sort((a, b) => b.score - a.score);
|
|
91
|
+
return results;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Register the in-process handler with the pool
|
|
96
|
+
registerInProcessHandlers({ rerank: inProcessRerank });
|
|
97
|
+
|
|
98
|
+
// --- Public API ---
|
|
99
|
+
|
|
100
|
+
/** Kept for backwards compat. */
|
|
101
|
+
export async function getReranker(): Promise<any> {
|
|
102
|
+
await ensureLoaded();
|
|
103
|
+
return model;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface RerankResult {
|
|
107
|
+
index: number;
|
|
108
|
+
score: number; // sigmoid-normalized relevance (0-1)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Re-rank candidate passages against a query using the cross-encoder.
|
|
113
|
+
* Returns results sorted by relevance score (descending).
|
|
114
|
+
* Dispatches to the worker pool (or in-process fallback).
|
|
115
|
+
*/
|
|
116
|
+
export async function rerank(query: string, passages: string[]): Promise<RerankResult[]> {
|
|
117
|
+
if (passages.length === 0) return [];
|
|
118
|
+
return dispatchRerank({ query, passages });
|
|
119
|
+
}
|