@tpsdev-ai/flair 0.17.0 → 0.19.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/dist/resources/Admin.js +12 -0
- package/dist/resources/AdminConnectors.js +6 -0
- package/dist/resources/AdminDashboard.js +9 -0
- package/dist/resources/AdminIdp.js +6 -0
- package/dist/resources/AdminInstance.js +6 -0
- package/dist/resources/AdminMemory.js +8 -0
- package/dist/resources/AdminPrincipals.js +6 -0
- package/dist/resources/Integration.js +52 -2
- package/dist/resources/Memory.js +404 -45
- package/dist/resources/MemoryGrant.js +55 -2
- package/dist/resources/Relationship.js +49 -1
- package/dist/resources/SemanticSearch.js +41 -3
- package/dist/resources/Soul.js +12 -1
- package/dist/resources/WorkspaceState.js +56 -2
- package/dist/resources/dedup.js +62 -0
- package/dist/resources/health.js +17 -0
- package/dist/resources/mcp-tools.js +75 -5
- package/dist/resources/rerank-provider.js +361 -0
- package/package.json +2 -1
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rerank-provider.ts
|
|
3
|
+
*
|
|
4
|
+
* In-process cross-encoder reranker for Flair's recall path. Mirrors the
|
|
5
|
+
* embeddings-provider.ts shape: dynamic import of node-llama-cpp (deferred to
|
|
6
|
+
* first use to dodge Harper 5.x's VM-sandbox linker race), lazy singleton init,
|
|
7
|
+
* and graceful passthrough on ANY failure — the reranker is best-effort and must
|
|
8
|
+
* NEVER block or break recall.
|
|
9
|
+
*
|
|
10
|
+
* Two inference modes (selected by FLAIR_RERANK_MODEL):
|
|
11
|
+
*
|
|
12
|
+
* 1. "qwen3-reranker-0.6b-q8" (DEFAULT, quality) — Qwen3-Reranker-0.6B is a
|
|
13
|
+
* causal-LM reranker, NOT a rank-pooling cross-encoder. Its GGUF reports
|
|
14
|
+
* supportsRanking=false, so node-llama-cpp's createRankingContext() rejects
|
|
15
|
+
* it. The correct path is generative: format query+doc into the official
|
|
16
|
+
* instruction prompt ending at the assistant turn, evaluate, and read the
|
|
17
|
+
* next-token probability of "yes" vs "no":
|
|
18
|
+
* score = P(yes) / (P(yes) + P(no))
|
|
19
|
+
* Implemented via seq.controlledEvaluate with generateNext probabilities.
|
|
20
|
+
* (Validated offline in RERANK-PILOT-RESULTS.md: 4/4 target cases flipped
|
|
21
|
+
* positive, p@1 6/8→8/8, mean margin 0.028→0.688, ~1.2s / 16 docs on rockit.)
|
|
22
|
+
*
|
|
23
|
+
* 2. "jina-reranker-v2" (latency fallback) — jina-reranker-v2 IS a rank-pooling
|
|
24
|
+
* cross-encoder; its gpustack GGUF loads via createRankingContext()/rankAll
|
|
25
|
+
* (supportsRanking=true). ~145ms / 16 docs but weaker (7/8, leaves the
|
|
26
|
+
* hardest consensus case negative). Selectable where latency is tight.
|
|
27
|
+
*
|
|
28
|
+
* Serving path = the same in-process node-llama-cpp the embedding engine ships.
|
|
29
|
+
* No Ollama (no logprobs, no rerank endpoint — verified), no network hop, no
|
|
30
|
+
* new auth boundary.
|
|
31
|
+
*
|
|
32
|
+
* GGUF files are NOT committed; they are provisioned into models/ alongside the
|
|
33
|
+
* embedding GGUF. See docs/rerank-provisioning.md for download sources.
|
|
34
|
+
*/
|
|
35
|
+
import { join } from "node:path";
|
|
36
|
+
// Known reranker models → GGUF filename + inference mode. The default is the
|
|
37
|
+
// quality model (Qwen3 generative). jina is the latency model (rank API).
|
|
38
|
+
const MODELS = {
|
|
39
|
+
"qwen3-reranker-0.6b-q8": { file: "Qwen3-Reranker-0.6B-q8_0.gguf", mode: "generative" },
|
|
40
|
+
"jina-reranker-v2": { file: "jina-reranker-v2-base.Q8_0.gguf", mode: "rank" },
|
|
41
|
+
};
|
|
42
|
+
const DEFAULT_MODEL = "qwen3-reranker-0.6b-q8";
|
|
43
|
+
// Official Qwen3-Reranker prompt scaffold (generative yes/no judgement).
|
|
44
|
+
const QWEN_PREFIX = '<|im_start|>system\nJudge whether the Document meets the requirements based on the Query and the Instruct provided. Note that the answer can only be "yes" or "no".<|im_end|>\n<|im_start|>user\n';
|
|
45
|
+
const QWEN_SUFFIX = "<|im_end|>\n<|im_start|>assistant\n<think>\n\n</think>\n\n";
|
|
46
|
+
const QWEN_INSTRUCT = "Given a web search query, retrieve relevant passages that answer the query";
|
|
47
|
+
function buildQwenPrompt(q, doc) {
|
|
48
|
+
return `${QWEN_PREFIX}<Instruct>: ${QWEN_INSTRUCT}\n<Query>: ${q}\n<Document>: ${doc}${QWEN_SUFFIX}`;
|
|
49
|
+
}
|
|
50
|
+
// Cap per-document content fed to the reranker. Short atomic notes are the norm;
|
|
51
|
+
// this bounds context blow-up + latency on the occasional huge memory.
|
|
52
|
+
const MAX_DOC_CHARS = 2000;
|
|
53
|
+
let _state = "uninitialized";
|
|
54
|
+
let _initError;
|
|
55
|
+
let _warnedOnce = false;
|
|
56
|
+
let _modelKey = "";
|
|
57
|
+
let _mode;
|
|
58
|
+
// node-llama-cpp handles (kept alive as a singleton like the embedding engine).
|
|
59
|
+
let _nlc = null;
|
|
60
|
+
let _llama = null;
|
|
61
|
+
let _model = null;
|
|
62
|
+
// generative mode handles
|
|
63
|
+
let _ctx = null;
|
|
64
|
+
let _seq = null;
|
|
65
|
+
let _yesToks = [];
|
|
66
|
+
let _noToks = [];
|
|
67
|
+
// rank mode handle
|
|
68
|
+
let _rankCtx = null;
|
|
69
|
+
// Diagnostics surfaced via getRerankStatus() / health.ts.
|
|
70
|
+
let _lastLatencyMs = null;
|
|
71
|
+
let _fallbackCount = 0;
|
|
72
|
+
let _rerankCount = 0;
|
|
73
|
+
function resolveModelKey() {
|
|
74
|
+
const requested = process.env.FLAIR_RERANK_MODEL?.trim();
|
|
75
|
+
if (requested && MODELS[requested])
|
|
76
|
+
return requested;
|
|
77
|
+
return DEFAULT_MODEL;
|
|
78
|
+
}
|
|
79
|
+
/** Discover the platform addon binary the same way embeddings-provider.ts does. */
|
|
80
|
+
async function findAddonPath() {
|
|
81
|
+
const { existsSync } = await import("node:fs");
|
|
82
|
+
const platforms = ["linux-x64", "mac-arm64-metal", "mac-arm64", "win-x64", "linux-arm64", "mac-x64"];
|
|
83
|
+
for (const platform of platforms) {
|
|
84
|
+
const candidate = join(process.cwd(), "node_modules", "@node-llama-cpp", platform, "bins", platform, "llama-addon.node");
|
|
85
|
+
if (existsSync(candidate))
|
|
86
|
+
return candidate;
|
|
87
|
+
}
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
async function ensureInit() {
|
|
91
|
+
if (_state === "ready")
|
|
92
|
+
return;
|
|
93
|
+
if (_state === "failed")
|
|
94
|
+
return; // already logged once — don't thrash
|
|
95
|
+
try {
|
|
96
|
+
_modelKey = resolveModelKey();
|
|
97
|
+
const spec = MODELS[_modelKey];
|
|
98
|
+
_mode = spec.mode;
|
|
99
|
+
const { existsSync } = await import("node:fs");
|
|
100
|
+
const modelPath = join(process.cwd(), "models", spec.file);
|
|
101
|
+
if (!existsSync(modelPath)) {
|
|
102
|
+
throw new Error(`reranker GGUF not found: models/${spec.file} (provision per docs/rerank-provisioning.md)`);
|
|
103
|
+
}
|
|
104
|
+
// Dynamic import — deferred to avoid Harper 5.x VM linker race (same reason
|
|
105
|
+
// embeddings-provider.ts defers harper-fabric-embeddings).
|
|
106
|
+
if (!_nlc)
|
|
107
|
+
_nlc = await import("node-llama-cpp");
|
|
108
|
+
// node-llama-cpp finds its prebuilt binary from process.cwd()/node_modules
|
|
109
|
+
// when run inside the Flair app dir (Harper's runtime cwd). We don't need to
|
|
110
|
+
// pass the addon path explicitly to getLlama() — but we surface it for
|
|
111
|
+
// diagnostics and as a guard that a binary exists at all.
|
|
112
|
+
const addonPath = await findAddonPath();
|
|
113
|
+
if (!addonPath) {
|
|
114
|
+
throw new Error("no @node-llama-cpp platform addon found under node_modules");
|
|
115
|
+
}
|
|
116
|
+
_llama = await _nlc.getLlama();
|
|
117
|
+
_model = await _llama.loadModel({ modelPath });
|
|
118
|
+
if (_mode === "generative") {
|
|
119
|
+
_ctx = await _model.createContext({ contextSize: 1024 });
|
|
120
|
+
_seq = _ctx.getSequence();
|
|
121
|
+
// Resolve yes/no token ids (both case variants — the post-</think>
|
|
122
|
+
// position puts mass on "yes"/"Yes").
|
|
123
|
+
_yesToks = [_model.tokenize("yes", false)[0], _model.tokenize("Yes", false)[0]].filter((t) => t != null);
|
|
124
|
+
_noToks = [_model.tokenize("no", false)[0], _model.tokenize("No", false)[0]].filter((t) => t != null);
|
|
125
|
+
if (_yesToks.length === 0 || _noToks.length === 0) {
|
|
126
|
+
throw new Error("failed to resolve yes/no token ids for generative reranker");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
// rank-pooling cross-encoder (jina). Reject if the GGUF isn't a ranking model.
|
|
131
|
+
if (!_model.fileInsights?.supportsRanking) {
|
|
132
|
+
throw new Error(`model ${spec.file} does not support ranking (not a rank-pooling cross-encoder)`);
|
|
133
|
+
}
|
|
134
|
+
// Context must fit query + the longest document (jina concatenates them).
|
|
135
|
+
// 512 is too small for real memories; 2048 covers MAX_DOC_CHARS + query.
|
|
136
|
+
_rankCtx = await _model.createRankingContext({ contextSize: 2048 });
|
|
137
|
+
}
|
|
138
|
+
_state = "ready";
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
_state = "failed";
|
|
142
|
+
_initError = err?.message || String(err);
|
|
143
|
+
if (!_warnedOnce) {
|
|
144
|
+
console.warn(`[rerank] WARN: reranker unavailable, recall falls back to vector order. Error: ${_initError}`);
|
|
145
|
+
_warnedOnce = true;
|
|
146
|
+
}
|
|
147
|
+
// Best-effort cleanup of any partial handles.
|
|
148
|
+
await disposeHandles().catch(() => { });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async function disposeHandles() {
|
|
152
|
+
try {
|
|
153
|
+
if (_rankCtx) {
|
|
154
|
+
await _rankCtx.dispose();
|
|
155
|
+
_rankCtx = null;
|
|
156
|
+
}
|
|
157
|
+
if (_ctx) {
|
|
158
|
+
await _ctx.dispose();
|
|
159
|
+
_ctx = null;
|
|
160
|
+
_seq = null;
|
|
161
|
+
}
|
|
162
|
+
if (_model) {
|
|
163
|
+
await _model.dispose();
|
|
164
|
+
_model = null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch { /* ignore */ }
|
|
168
|
+
}
|
|
169
|
+
/** Reset (or recreate) the shared generative sequence to a clean KV state. */
|
|
170
|
+
async function resetSequence() {
|
|
171
|
+
try {
|
|
172
|
+
if (_seq && typeof _seq.clearHistory === "function") {
|
|
173
|
+
await _seq.clearHistory();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
catch { /* fall through to recreate */ }
|
|
178
|
+
// clearHistory unavailable or threw — recreate the sequence from the context.
|
|
179
|
+
try {
|
|
180
|
+
_seq?.dispose?.();
|
|
181
|
+
}
|
|
182
|
+
catch { /* ignore */ }
|
|
183
|
+
_seq = _ctx.getSequence();
|
|
184
|
+
}
|
|
185
|
+
/** Generative yes/no score for one (query, doc) pair. */
|
|
186
|
+
async function scoreGenerative(q, doc) {
|
|
187
|
+
const tokens = _model.tokenize(buildQwenPrompt(q, doc.slice(0, MAX_DOC_CHARS)), true);
|
|
188
|
+
// Reset the sequence so each pair is scored independently (deterministic) and
|
|
189
|
+
// a prior eval can't poison this one ("Eval has failed" after a bad state).
|
|
190
|
+
await resetSequence();
|
|
191
|
+
const input = tokens.map((t, i) => i === tokens.length - 1 ? [t, { generateNext: { probabilities: true } }] : t);
|
|
192
|
+
const out = await _seq.controlledEvaluate(input);
|
|
193
|
+
const lastDefined = [...out].reverse().find((x) => x !== undefined);
|
|
194
|
+
const probs = lastDefined?.next?.probabilities;
|
|
195
|
+
// Defensive: under some runtimes (observed in Harper's resource worker with a
|
|
196
|
+
// second native llama backend already resident — HFE's raw-addon embedding
|
|
197
|
+
// engine) controlledEvaluate returns an empty result array (no decoded
|
|
198
|
+
// logits). Treat that as "can't score" and signal the caller to fall open,
|
|
199
|
+
// rather than silently returning 0 (which would corrupt the ranking).
|
|
200
|
+
if (out.length === 0 || !probs) {
|
|
201
|
+
throw new Error("generative reranker produced no logits (empty controlledEvaluate result)");
|
|
202
|
+
}
|
|
203
|
+
const pYes = _yesToks.reduce((s, t) => s + (probs.get(t) ?? 0), 0);
|
|
204
|
+
const pNo = _noToks.reduce((s, t) => s + (probs.get(t) ?? 0), 0);
|
|
205
|
+
return pYes + pNo > 0 ? pYes / (pYes + pNo) : 0;
|
|
206
|
+
}
|
|
207
|
+
// Serialize all engine work. node-llama-cpp's context/sequence is single-use:
|
|
208
|
+
// concurrent controlledEvaluate calls on the shared sequence corrupt the KV
|
|
209
|
+
// cache ("Eval has failed"). Harper calls SemanticSearch.post() concurrently,
|
|
210
|
+
// so we funnel every rerank through one in-process queue. Each search still
|
|
211
|
+
// gets the full engine; they just don't overlap on the hardware. The latency
|
|
212
|
+
// budget in rerankCandidates bounds how long any one search waits.
|
|
213
|
+
let _engineChain = Promise.resolve();
|
|
214
|
+
function runExclusive(fn) {
|
|
215
|
+
const next = _engineChain.then(fn, fn);
|
|
216
|
+
// Keep the chain alive regardless of this task's outcome.
|
|
217
|
+
_engineChain = next.then(() => undefined, () => undefined);
|
|
218
|
+
return next;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Score query against a batch of documents. Returns an array of scores in the
|
|
222
|
+
* same order as `docs`, in [0,1]. Higher = more relevant. Throws on engine
|
|
223
|
+
* error (caller catches and falls back to vector order). Engine access is
|
|
224
|
+
* serialized — see runExclusive.
|
|
225
|
+
*/
|
|
226
|
+
export async function rerankScores(query, docs) {
|
|
227
|
+
await ensureInit();
|
|
228
|
+
if (_state !== "ready")
|
|
229
|
+
throw new Error("reranker not ready");
|
|
230
|
+
if (_mode === "rank") {
|
|
231
|
+
const trimmed = docs.map((d) => String(d ?? "").slice(0, MAX_DOC_CHARS));
|
|
232
|
+
return runExclusive(() => _rankCtx.rankAll(query, trimmed));
|
|
233
|
+
}
|
|
234
|
+
// generative — score sequentially under the engine lock (single shared
|
|
235
|
+
// sequence; deterministic). The whole batch holds the lock for one search so
|
|
236
|
+
// its sequence resets aren't interleaved with another search's.
|
|
237
|
+
return runExclusive(async () => {
|
|
238
|
+
const scores = [];
|
|
239
|
+
for (const d of docs) {
|
|
240
|
+
scores.push(await scoreGenerative(query, String(d ?? "")));
|
|
241
|
+
}
|
|
242
|
+
return scores;
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Rerank `candidates` (each must carry `content`) against `query`. Reorders in
|
|
247
|
+
* place by rerank score and overwrites `_score` with it (so downstream margin
|
|
248
|
+
* measurement reads the rerank score); preserves the original semantic score as
|
|
249
|
+
* `_semScore`. `_rawScore` is intentionally NOT touched (recall-bench's raw mode
|
|
250
|
+
* must stay reproducible).
|
|
251
|
+
*
|
|
252
|
+
* FAIL-OPEN: on init failure, timeout, or any throw, returns the input array
|
|
253
|
+
* UNCHANGED (caller then applies the existing vector-order sort). Never throws.
|
|
254
|
+
*/
|
|
255
|
+
export async function rerankCandidates(query, candidates, opts) {
|
|
256
|
+
const top = candidates.slice(0, Math.max(0, opts.topN));
|
|
257
|
+
if (top.length < 2)
|
|
258
|
+
return candidates;
|
|
259
|
+
const t0 = Date.now();
|
|
260
|
+
let timer;
|
|
261
|
+
try {
|
|
262
|
+
const docs = top.map((c) => String(c.content ?? ""));
|
|
263
|
+
const timeout = new Promise((resolve) => {
|
|
264
|
+
timer = setTimeout(() => resolve(null), Math.max(1, opts.budgetMs));
|
|
265
|
+
});
|
|
266
|
+
// Swallow a LATE rejection: if the budget wins the race, the scoring promise
|
|
267
|
+
// is still pending and may reject afterwards — attach a no-op catch so it
|
|
268
|
+
// doesn't surface as an unhandled rejection.
|
|
269
|
+
const scoring = rerankScores(query, docs);
|
|
270
|
+
scoring.catch(() => { });
|
|
271
|
+
const scores = await Promise.race([scoring, timeout]);
|
|
272
|
+
if (timer)
|
|
273
|
+
clearTimeout(timer);
|
|
274
|
+
if (scores === null) {
|
|
275
|
+
// Budget exceeded — abandon rerank, keep vector order. No partial reorder.
|
|
276
|
+
_fallbackCount++;
|
|
277
|
+
return candidates;
|
|
278
|
+
}
|
|
279
|
+
// Attach rerank score, preserve the semantic score for diagnostics.
|
|
280
|
+
const reranked = top.map((c, i) => {
|
|
281
|
+
const sem = c._score;
|
|
282
|
+
c._semScore = sem;
|
|
283
|
+
c._score = Math.round((scores[i] ?? 0) * 1000) / 1000;
|
|
284
|
+
return c;
|
|
285
|
+
});
|
|
286
|
+
reranked.sort((a, b) => (b._score ?? 0) - (a._score ?? 0));
|
|
287
|
+
// Candidates beyond topN (not reranked) keep their vector position AFTER the
|
|
288
|
+
// reranked block — they were already the tail of the vector-ordered pool.
|
|
289
|
+
const tail = candidates.slice(top.length);
|
|
290
|
+
_lastLatencyMs = Date.now() - t0;
|
|
291
|
+
_rerankCount++;
|
|
292
|
+
return [...reranked, ...tail];
|
|
293
|
+
}
|
|
294
|
+
catch (err) {
|
|
295
|
+
if (timer)
|
|
296
|
+
clearTimeout(timer);
|
|
297
|
+
_fallbackCount++;
|
|
298
|
+
if (!_warnedOnce) {
|
|
299
|
+
console.warn(`[rerank] WARN: rerank threw, falling back to vector order. Error: ${err?.message || err}`);
|
|
300
|
+
_warnedOnce = true;
|
|
301
|
+
}
|
|
302
|
+
return candidates;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
/** Is the master flag on? (Default OFF.) */
|
|
306
|
+
export function isRerankEnabled() {
|
|
307
|
+
return process.env.FLAIR_RERANK_ENABLED === "true";
|
|
308
|
+
}
|
|
309
|
+
/** Candidate count fed to the reranker. Caps the HNSW fetch. */
|
|
310
|
+
export function getRerankTopN() {
|
|
311
|
+
const v = Number(process.env.FLAIR_RERANK_TOPN);
|
|
312
|
+
return Number.isFinite(v) && v > 0 ? Math.floor(v) : 50;
|
|
313
|
+
}
|
|
314
|
+
/** Hard latency budget for the whole rerank stage (ms). */
|
|
315
|
+
export function getRerankBudgetMs() {
|
|
316
|
+
const v = Number(process.env.FLAIR_RERANK_BUDGET_MS);
|
|
317
|
+
return Number.isFinite(v) && v > 0 ? Math.floor(v) : 2500;
|
|
318
|
+
}
|
|
319
|
+
/** Skip rerank below this many candidates (nothing to reorder). */
|
|
320
|
+
export function getRerankMinCandidates() {
|
|
321
|
+
const v = Number(process.env.FLAIR_RERANK_MIN_CANDIDATES);
|
|
322
|
+
return Number.isFinite(v) && v >= 2 ? Math.floor(v) : 2;
|
|
323
|
+
}
|
|
324
|
+
/** Status surface for health.ts. */
|
|
325
|
+
export function getRerankStatus() {
|
|
326
|
+
return {
|
|
327
|
+
enabled: isRerankEnabled(),
|
|
328
|
+
model: _modelKey || resolveModelKey(),
|
|
329
|
+
mode: _mode ?? "uninitialized",
|
|
330
|
+
state: _state,
|
|
331
|
+
topN: getRerankTopN(),
|
|
332
|
+
budgetMs: getRerankBudgetMs(),
|
|
333
|
+
lastLatencyMs: _lastLatencyMs,
|
|
334
|
+
rerankCount: _rerankCount,
|
|
335
|
+
fallbackCount: _fallbackCount,
|
|
336
|
+
error: _initError,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
// ── Test seam ────────────────────────────────────────────────────────────────
|
|
340
|
+
// Exported pure helpers so the deterministic scoring path can be unit-tested
|
|
341
|
+
// without loading a 600MB GGUF (mirrors the pilot's deterministic approach:
|
|
342
|
+
// given fixed yes/no probabilities, the score + reorder math is exact).
|
|
343
|
+
/** P(yes)/(P(yes)+P(no)) — the generative reranker's scoring function. */
|
|
344
|
+
export function yesNoScore(pYes, pNo) {
|
|
345
|
+
return pYes + pNo > 0 ? pYes / (pYes + pNo) : 0;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Pure reorder used by rerankCandidates: attach scores, overwrite `_score`,
|
|
349
|
+
* preserve `_semScore`, leave `_rawScore` untouched, sort desc, append the
|
|
350
|
+
* non-reranked tail. Exported for deterministic unit tests.
|
|
351
|
+
*/
|
|
352
|
+
export function applyRerank(candidates, scores, topN) {
|
|
353
|
+
const top = candidates.slice(0, Math.max(0, topN));
|
|
354
|
+
const reranked = top.map((c, i) => {
|
|
355
|
+
c._semScore = c._score;
|
|
356
|
+
c._score = Math.round((scores[i] ?? 0) * 1000) / 1000;
|
|
357
|
+
return c;
|
|
358
|
+
});
|
|
359
|
+
reranked.sort((a, b) => (b._score ?? 0) - (a._score ?? 0));
|
|
360
|
+
return [...reranked, ...candidates.slice(top.length)];
|
|
361
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tpsdev-ai/flair",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "Identity, memory, and soul for AI agents. Cryptographic identity (Ed25519), semantic memory with local embeddings, and persistent personality — all in a single process.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -62,6 +62,7 @@
|
|
|
62
62
|
"harper-fabric-embeddings": "0.2.3",
|
|
63
63
|
"jose": "6.2.2",
|
|
64
64
|
"js-yaml": "4.1.1",
|
|
65
|
+
"node-llama-cpp": "3.18.1",
|
|
65
66
|
"tar": "7.5.13",
|
|
66
67
|
"tweetnacl": "1.0.3"
|
|
67
68
|
},
|