@nacre.work/api 0.0.0 → 0.1.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/LICENSE +202 -0
- package/NOTICE +9 -0
- package/dist/adapters.d.ts +326 -15
- package/dist/adapters.d.ts.map +1 -1
- package/dist/adapters.js +992 -52
- package/dist/adapters.js.map +1 -1
- package/dist/audit-export.d.ts +45 -0
- package/dist/audit-export.d.ts.map +1 -0
- package/dist/audit-export.js +155 -0
- package/dist/audit-export.js.map +1 -0
- package/dist/auth.d.ts +17 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +17 -7
- package/dist/auth.js.map +1 -1
- package/dist/idempotency.d.ts +62 -0
- package/dist/idempotency.d.ts.map +1 -0
- package/dist/idempotency.js +147 -0
- package/dist/idempotency.js.map +1 -0
- package/dist/index.d.ts +11 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -1
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +9 -0
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +50 -5
- package/dist/init.js.map +1 -1
- package/dist/limits.d.ts +80 -0
- package/dist/limits.d.ts.map +1 -0
- package/dist/limits.js +63 -0
- package/dist/limits.js.map +1 -0
- package/dist/login.d.ts +94 -0
- package/dist/login.d.ts.map +1 -0
- package/dist/login.js +283 -0
- package/dist/login.js.map +1 -0
- package/dist/main.js +136 -53
- package/dist/main.js.map +1 -1
- package/dist/pagination.d.ts +72 -0
- package/dist/pagination.d.ts.map +1 -0
- package/dist/pagination.js +110 -0
- package/dist/pagination.js.map +1 -0
- package/dist/rerank.d.ts +99 -0
- package/dist/rerank.d.ts.map +1 -0
- package/dist/rerank.js +138 -0
- package/dist/rerank.js.map +1 -0
- package/dist/server.d.ts +345 -6
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1035 -24
- package/dist/server.js.map +1 -1
- package/dist/service-keys.d.ts +13 -5
- package/dist/service-keys.d.ts.map +1 -1
- package/dist/service-keys.js +54 -31
- package/dist/service-keys.js.map +1 -1
- package/dist/source.d.ts +53 -0
- package/dist/source.d.ts.map +1 -0
- package/dist/source.js +109 -0
- package/dist/source.js.map +1 -0
- package/package.json +28 -10
- package/dist/__tests__/admin-surface.test.d.ts +0 -2
- package/dist/__tests__/admin-surface.test.d.ts.map +0 -1
- package/dist/__tests__/admin-surface.test.js +0 -431
- package/dist/__tests__/admin-surface.test.js.map +0 -1
- package/dist/__tests__/init.test.d.ts +0 -2
- package/dist/__tests__/init.test.d.ts.map +0 -1
- package/dist/__tests__/init.test.js +0 -125
- package/dist/__tests__/init.test.js.map +0 -1
- package/dist/__tests__/service-keys.test.d.ts +0 -2
- package/dist/__tests__/service-keys.test.d.ts.map +0 -1
- package/dist/__tests__/service-keys.test.js +0 -204
- package/dist/__tests__/service-keys.test.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pagination.d.ts","sourceRoot":"","sources":["../src/pagination.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,OAAO,EAAE,MAAM,aAAa,CAAA;AAEtD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,eAAO,MAAM,SAAS,MAAM,CAAA;AAC5B,eAAO,MAAM,aAAa,KAAK,CAAA;AAE/B,MAAM,WAAW,QAAQ;IACvB,0DAA0D;IAC1D,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,mEAAmE;IACnE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,IAAI;IACnB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,KAAK,EAAE,QAAQ,GAAG,SAAS,CAAA;CACrC;AAED,wBAAgB,YAAY,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAEvD;AAoCD;;;;;;GAMG;AACH,wBAAgB,QAAQ,CACtB,MAAM,EAAE,eAAe,EACvB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,GAChB,IAAI,GAAG,OAAO,CAoChB;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,UAAU,CAAC,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC,EAAE,CAAA;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CACnC;AAED;;;GAGG;AACH,wBAAgB,MAAM,CAAC,CAAC,EACtB,IAAI,EAAE,SAAS,CAAC,EAAE,EAClB,IAAI,EAAE,IAAI,GAAG,SAAS,EACtB,UAAU,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,QAAQ,GAC/B,UAAU,CAAC,CAAC,CAAC,CAIf"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { badRequest } from './errors.js';
|
|
2
|
+
/**
|
|
3
|
+
* Cursor pagination, and never offset.
|
|
4
|
+
*
|
|
5
|
+
* `docs/api.md` forbids offset on two grounds and both are real. It breaks
|
|
6
|
+
* under concurrent inserts — a row added while a client pages through shifts
|
|
7
|
+
* everything after it, so the client sees an item twice or not at all. And it
|
|
8
|
+
* invites enumeration: `?offset=10000` is a question about how much exists,
|
|
9
|
+
* which on a permission-scoped collection is a question the caller has no right
|
|
10
|
+
* to ask.
|
|
11
|
+
*
|
|
12
|
+
* The cursor is a position, not an index. It carries the sort key of the last
|
|
13
|
+
* item returned, so the next page is "everything after this one" — which is
|
|
14
|
+
* stable no matter what was inserted in between, and says nothing about size.
|
|
15
|
+
*
|
|
16
|
+
* ## Opaque, and only just
|
|
17
|
+
*
|
|
18
|
+
* It is base64url of `created_at|id`, which anyone can decode, and that is
|
|
19
|
+
* fine: both values are already in the response they came from. What matters is
|
|
20
|
+
* that it is *treated* as opaque — a client that parses it and constructs its
|
|
21
|
+
* own has built a dependency on a format that is allowed to change, and the
|
|
22
|
+
* signature check below is what tells them so before it becomes their problem.
|
|
23
|
+
*
|
|
24
|
+
* It is not signed. A forged cursor selects a different page of the same
|
|
25
|
+
* caller's own collection, which they could reach by paging anyway; signing it
|
|
26
|
+
* would defend nothing and imply a guarantee that does not exist.
|
|
27
|
+
*/
|
|
28
|
+
export const MAX_LIMIT = 200;
|
|
29
|
+
export const DEFAULT_LIMIT = 50;
|
|
30
|
+
export function encodeCursor(position) {
|
|
31
|
+
return Buffer.from(`${position.createdAt}|${position.id}`, 'utf8').toString('base64url');
|
|
32
|
+
}
|
|
33
|
+
function decodeCursor(cursor) {
|
|
34
|
+
let decoded;
|
|
35
|
+
try {
|
|
36
|
+
decoded = Buffer.from(cursor, 'base64url').toString('utf8');
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
const at = decoded.lastIndexOf('|');
|
|
42
|
+
if (at === -1)
|
|
43
|
+
return undefined;
|
|
44
|
+
const createdAt = decoded.slice(0, at);
|
|
45
|
+
const id = decoded.slice(at + 1);
|
|
46
|
+
// A cursor that does not carry a timestamp and an identifier is not one this
|
|
47
|
+
// API issued, whatever it decodes to.
|
|
48
|
+
//
|
|
49
|
+
// A uuid **or** a run of digits: every paged collection here is keyed by uuid
|
|
50
|
+
// except `audit_events`, whose primary key is a bigserial — an audit log is
|
|
51
|
+
// append-only and strictly ordered, and a sequence says so in a way a random
|
|
52
|
+
// uuid does not.
|
|
53
|
+
//
|
|
54
|
+
// Widening the format does not weaken anything, and it is worth saying why
|
|
55
|
+
// rather than leaving the next reader to work it out. The cursor is not a
|
|
56
|
+
// security boundary: it is not signed, it is decodable by anyone, and a
|
|
57
|
+
// forged one selects a different page of the caller's *own* collection —
|
|
58
|
+
// which they could reach by paging. What this check is for is telling a
|
|
59
|
+
// client that built its own cursor that it has taken a dependency on a format
|
|
60
|
+
// allowed to change, before that becomes their problem.
|
|
61
|
+
if (Number.isNaN(Date.parse(createdAt)))
|
|
62
|
+
return undefined;
|
|
63
|
+
if (!/^[0-9a-f-]{36}$/i.test(id) && !/^\d{1,19}$/.test(id))
|
|
64
|
+
return undefined;
|
|
65
|
+
return { createdAt, id };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Read `?limit=` and `?cursor=`, or explain what is wrong with them.
|
|
69
|
+
*
|
|
70
|
+
* `?offset=` is refused rather than ignored. Ignoring it gives a client the
|
|
71
|
+
* first page over and over while they believe they are paging, which is the
|
|
72
|
+
* failure that takes an afternoon to see.
|
|
73
|
+
*/
|
|
74
|
+
export function readPage(params, instance, requestId) {
|
|
75
|
+
if (params.has('offset')) {
|
|
76
|
+
return badRequest(instance, requestId, "'offset' is not supported. Use '?cursor=' — offset paging skips and repeats rows " +
|
|
77
|
+
'when the collection changes underneath it.');
|
|
78
|
+
}
|
|
79
|
+
let limit = DEFAULT_LIMIT;
|
|
80
|
+
const raw = params.get('limit');
|
|
81
|
+
if (raw !== null) {
|
|
82
|
+
const value = Number(raw);
|
|
83
|
+
if (!Number.isInteger(value) || value < 1 || value > MAX_LIMIT) {
|
|
84
|
+
return badRequest(instance, requestId, `'limit' must be an integer between 1 and ${MAX_LIMIT}.`);
|
|
85
|
+
}
|
|
86
|
+
limit = value;
|
|
87
|
+
}
|
|
88
|
+
let after;
|
|
89
|
+
const cursor = params.get('cursor');
|
|
90
|
+
if (cursor !== null && cursor !== '') {
|
|
91
|
+
const position = decodeCursor(cursor);
|
|
92
|
+
if (position === undefined) {
|
|
93
|
+
return badRequest(instance, requestId, "'cursor' is not one this API issued. Cursors are opaque — pass back the " +
|
|
94
|
+
'`next_cursor` from the previous page rather than constructing one.');
|
|
95
|
+
}
|
|
96
|
+
after = position;
|
|
97
|
+
}
|
|
98
|
+
return { limit, after };
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The common case: rows come back already filtered, so the cursor is the last
|
|
102
|
+
* one returned and a short page means the collection is exhausted.
|
|
103
|
+
*/
|
|
104
|
+
export function pageOf(rows, page, positionOf) {
|
|
105
|
+
if (page === undefined || rows.length < page.limit)
|
|
106
|
+
return { items: rows, nextCursor: null };
|
|
107
|
+
const last = rows[rows.length - 1];
|
|
108
|
+
return { items: rows, nextCursor: last === undefined ? null : encodeCursor(positionOf(last)) };
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=pagination.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pagination.js","sourceRoot":"","sources":["../src/pagination.ts"],"names":[],"mappings":"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"}
|
package/dist/rerank.d.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-encoder reranking.
|
|
3
|
+
*
|
|
4
|
+
* `docs/architecture.md` has specified this since the beginning — "a
|
|
5
|
+
* cross-encoder rerank of the top 50 down to top-k. Reranking buys more quality
|
|
6
|
+
* than any amount of chunking tuning and is on by default" — and the search
|
|
7
|
+
* path has never called one.
|
|
8
|
+
*
|
|
9
|
+
* ## Why this is not the post-filter invariant I2 forbids
|
|
10
|
+
*
|
|
11
|
+
* It looks like it. I2 says `top_k` is passed through uncorrected and that
|
|
12
|
+
* asking for more and trimming is forbidden; this asks for fifty and returns
|
|
13
|
+
* ten. The difference is what the trim decides.
|
|
14
|
+
*
|
|
15
|
+
* The forbidden pattern trims on **permission**: fetch k results, drop the ones
|
|
16
|
+
* the caller may not see, return what is left. That returns fewer than k
|
|
17
|
+
* permitted results — the caller's last page silently shrinks, and how much it
|
|
18
|
+
* shrinks by is a measurement of what exists but is invisible to them. It also
|
|
19
|
+
* costs more, because the index ranked documents it then threw away.
|
|
20
|
+
*
|
|
21
|
+
* This trims on **relevance**, over a set that is already entirely permitted.
|
|
22
|
+
* The ACL filter runs inside the index traversal exactly as before, so all
|
|
23
|
+
* fifty candidates are ones this caller may read; reordering them and keeping
|
|
24
|
+
* ten changes which permitted results are returned, never how many. The test
|
|
25
|
+
* that separates the two: **can the number of results ever be smaller than the
|
|
26
|
+
* number of permitted matches, up to k?** For a post-filter, yes. Here, no.
|
|
27
|
+
*
|
|
28
|
+
* That is the whole justification, and it is worth stating in the code rather
|
|
29
|
+
* than in a commit message, because the next person to read the search path
|
|
30
|
+
* will see an over-fetch and reach for the rule.
|
|
31
|
+
*
|
|
32
|
+
* ## Failing closed, which is the other way round from the rate limiter
|
|
33
|
+
*
|
|
34
|
+
* If the reranker is unreachable the search still answers, in fusion order,
|
|
35
|
+
* with a metric recording that it degraded. This is not a contradiction of
|
|
36
|
+
* invariant 3: reranking decides *ordering*, and every candidate in the list
|
|
37
|
+
* was already permitted before the reranker saw it. Refusing the search would
|
|
38
|
+
* make an optional quality component into a hard dependency of the product's
|
|
39
|
+
* central operation.
|
|
40
|
+
*
|
|
41
|
+
* What it must never do is fail *quietly* — an operator who turned reranking on
|
|
42
|
+
* is entitled to know it is not running, so degradation is a counter and a log
|
|
43
|
+
* line rather than a silent fallback.
|
|
44
|
+
*/
|
|
45
|
+
export interface Reranker {
|
|
46
|
+
/**
|
|
47
|
+
* Score each text against the query. One score per input, **in input order**.
|
|
48
|
+
*
|
|
49
|
+
* Returning scores rather than a sorted list is deliberate: the caller holds
|
|
50
|
+
* the hits, and a reordered list of texts cannot be matched back to them
|
|
51
|
+
* without an index that a wrong implementation would get to choose.
|
|
52
|
+
*/
|
|
53
|
+
rank(query: string, texts: readonly string[]): Promise<readonly number[]>;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Text Embeddings Inference's `/rerank`, which is what the `full` and
|
|
57
|
+
* `airgapped` Compose profiles run.
|
|
58
|
+
*
|
|
59
|
+
* The response is `[{index, score}, …]` sorted by score, so the index is the
|
|
60
|
+
* only thing tying a score back to its input. Trusting the order instead would
|
|
61
|
+
* attach the wrong score to the wrong chunk and be invisible in every test that
|
|
62
|
+
* does not check ranking against known-relevant text.
|
|
63
|
+
*/
|
|
64
|
+
export declare class HttpReranker implements Reranker {
|
|
65
|
+
private readonly endpoint;
|
|
66
|
+
private readonly timeoutMs;
|
|
67
|
+
constructor(endpoint: string, timeoutMs?: number);
|
|
68
|
+
rank(query: string, texts: readonly string[]): Promise<readonly number[]>;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The reranker a configuration asks for, or none.
|
|
72
|
+
*
|
|
73
|
+
* One factory rather than the same three lines in the API process and in the
|
|
74
|
+
* MCP one. They share a search service and must not disagree about whether
|
|
75
|
+
* results are reranked — two surfaces over one index answering in different
|
|
76
|
+
* orders is the kind of difference nobody reports as a bug and everybody
|
|
77
|
+
* notices.
|
|
78
|
+
*
|
|
79
|
+
* `loadConfig` already refuses the contradictory case (enabled with no
|
|
80
|
+
* endpoint) at startup, so there is nothing to validate here.
|
|
81
|
+
*/
|
|
82
|
+
export declare function rerankerFor(config: {
|
|
83
|
+
readonly rerankerEnabled: boolean;
|
|
84
|
+
readonly rerankerEndpoint: string | undefined;
|
|
85
|
+
}): Reranker | undefined;
|
|
86
|
+
/**
|
|
87
|
+
* Reorder hits by a reranker's scores and keep the best `topK`.
|
|
88
|
+
*
|
|
89
|
+
* Separate from the transport so the ordering rule can be tested without a
|
|
90
|
+
* model server — the rule being the part that is easy to get subtly wrong.
|
|
91
|
+
*
|
|
92
|
+
* The sort is stable in the sense that matters: ties keep the fusion order they
|
|
93
|
+
* arrived in, because `Array.prototype.sort` is stable and the comparator
|
|
94
|
+
* answers 0 for equal scores. A cross-encoder returning identical scores for
|
|
95
|
+
* two chunks is saying it cannot separate them, and falling back to the index's
|
|
96
|
+
* own ranking is a better answer than an arbitrary one.
|
|
97
|
+
*/
|
|
98
|
+
export declare function applyRanking<T>(items: readonly T[], scores: readonly number[], topK: number): T[];
|
|
99
|
+
//# sourceMappingURL=rerank.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rerank.d.ts","sourceRoot":"","sources":["../src/rerank.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAEH,MAAM,WAAW,QAAQ;IACvB;;;;;;OAMG;IACH,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,CAAA;CAC1E;AAED;;;;;;;;GAQG;AACH,qBAAa,YAAa,YAAW,QAAQ;IAEzC,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,SAAS;gBADT,QAAQ,EAAE,MAAM,EAChB,SAAS,SAAO;IAG7B,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC;CA0ChF;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE;IAClC,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAA;IACjC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,GAAG,SAAS,CAAA;CAC9C,GAAG,QAAQ,GAAG,SAAS,CAGvB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,KAAK,EAAE,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,SAAS,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,EAAE,CASjG"}
|
package/dist/rerank.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-encoder reranking.
|
|
3
|
+
*
|
|
4
|
+
* `docs/architecture.md` has specified this since the beginning — "a
|
|
5
|
+
* cross-encoder rerank of the top 50 down to top-k. Reranking buys more quality
|
|
6
|
+
* than any amount of chunking tuning and is on by default" — and the search
|
|
7
|
+
* path has never called one.
|
|
8
|
+
*
|
|
9
|
+
* ## Why this is not the post-filter invariant I2 forbids
|
|
10
|
+
*
|
|
11
|
+
* It looks like it. I2 says `top_k` is passed through uncorrected and that
|
|
12
|
+
* asking for more and trimming is forbidden; this asks for fifty and returns
|
|
13
|
+
* ten. The difference is what the trim decides.
|
|
14
|
+
*
|
|
15
|
+
* The forbidden pattern trims on **permission**: fetch k results, drop the ones
|
|
16
|
+
* the caller may not see, return what is left. That returns fewer than k
|
|
17
|
+
* permitted results — the caller's last page silently shrinks, and how much it
|
|
18
|
+
* shrinks by is a measurement of what exists but is invisible to them. It also
|
|
19
|
+
* costs more, because the index ranked documents it then threw away.
|
|
20
|
+
*
|
|
21
|
+
* This trims on **relevance**, over a set that is already entirely permitted.
|
|
22
|
+
* The ACL filter runs inside the index traversal exactly as before, so all
|
|
23
|
+
* fifty candidates are ones this caller may read; reordering them and keeping
|
|
24
|
+
* ten changes which permitted results are returned, never how many. The test
|
|
25
|
+
* that separates the two: **can the number of results ever be smaller than the
|
|
26
|
+
* number of permitted matches, up to k?** For a post-filter, yes. Here, no.
|
|
27
|
+
*
|
|
28
|
+
* That is the whole justification, and it is worth stating in the code rather
|
|
29
|
+
* than in a commit message, because the next person to read the search path
|
|
30
|
+
* will see an over-fetch and reach for the rule.
|
|
31
|
+
*
|
|
32
|
+
* ## Failing closed, which is the other way round from the rate limiter
|
|
33
|
+
*
|
|
34
|
+
* If the reranker is unreachable the search still answers, in fusion order,
|
|
35
|
+
* with a metric recording that it degraded. This is not a contradiction of
|
|
36
|
+
* invariant 3: reranking decides *ordering*, and every candidate in the list
|
|
37
|
+
* was already permitted before the reranker saw it. Refusing the search would
|
|
38
|
+
* make an optional quality component into a hard dependency of the product's
|
|
39
|
+
* central operation.
|
|
40
|
+
*
|
|
41
|
+
* What it must never do is fail *quietly* — an operator who turned reranking on
|
|
42
|
+
* is entitled to know it is not running, so degradation is a counter and a log
|
|
43
|
+
* line rather than a silent fallback.
|
|
44
|
+
*/
|
|
45
|
+
/**
|
|
46
|
+
* Text Embeddings Inference's `/rerank`, which is what the `full` and
|
|
47
|
+
* `airgapped` Compose profiles run.
|
|
48
|
+
*
|
|
49
|
+
* The response is `[{index, score}, …]` sorted by score, so the index is the
|
|
50
|
+
* only thing tying a score back to its input. Trusting the order instead would
|
|
51
|
+
* attach the wrong score to the wrong chunk and be invisible in every test that
|
|
52
|
+
* does not check ranking against known-relevant text.
|
|
53
|
+
*/
|
|
54
|
+
export class HttpReranker {
|
|
55
|
+
endpoint;
|
|
56
|
+
timeoutMs;
|
|
57
|
+
constructor(endpoint, timeoutMs = 3000) {
|
|
58
|
+
this.endpoint = endpoint;
|
|
59
|
+
this.timeoutMs = timeoutMs;
|
|
60
|
+
}
|
|
61
|
+
async rank(query, texts) {
|
|
62
|
+
if (texts.length === 0)
|
|
63
|
+
return [];
|
|
64
|
+
// A search that would otherwise have answered must not hang because a model
|
|
65
|
+
// server is wedged; the caller treats a timeout as a degradation.
|
|
66
|
+
const abort = AbortSignal.timeout(this.timeoutMs);
|
|
67
|
+
const response = await fetch(new URL('/rerank', this.endpoint), {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
headers: { 'content-type': 'application/json' },
|
|
70
|
+
body: JSON.stringify({ query, texts: [...texts], raw_scores: false }),
|
|
71
|
+
signal: abort,
|
|
72
|
+
});
|
|
73
|
+
if (!response.ok)
|
|
74
|
+
throw new Error(`the reranker answered ${response.status}`);
|
|
75
|
+
const body = (await response.json());
|
|
76
|
+
if (!Array.isArray(body))
|
|
77
|
+
throw new Error('the reranker did not answer with a list');
|
|
78
|
+
const scores = new Array(texts.length).fill(Number.NEGATIVE_INFINITY);
|
|
79
|
+
let seen = 0;
|
|
80
|
+
for (const entry of body) {
|
|
81
|
+
const { index, score } = entry;
|
|
82
|
+
if (typeof index !== 'number' || typeof score !== 'number') {
|
|
83
|
+
throw new Error('a reranker result carried no index and score');
|
|
84
|
+
}
|
|
85
|
+
if (index < 0 || index >= texts.length) {
|
|
86
|
+
throw new Error(`the reranker scored index ${index} for ${texts.length} inputs`);
|
|
87
|
+
}
|
|
88
|
+
scores[index] = score;
|
|
89
|
+
seen++;
|
|
90
|
+
}
|
|
91
|
+
if (seen !== texts.length) {
|
|
92
|
+
// A short answer would leave some chunks at -Infinity and sink them to the
|
|
93
|
+
// bottom, which is a silent quality loss rather than a visible failure.
|
|
94
|
+
throw new Error(`the reranker scored ${seen} of ${texts.length} inputs`);
|
|
95
|
+
}
|
|
96
|
+
return scores;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The reranker a configuration asks for, or none.
|
|
101
|
+
*
|
|
102
|
+
* One factory rather than the same three lines in the API process and in the
|
|
103
|
+
* MCP one. They share a search service and must not disagree about whether
|
|
104
|
+
* results are reranked — two surfaces over one index answering in different
|
|
105
|
+
* orders is the kind of difference nobody reports as a bug and everybody
|
|
106
|
+
* notices.
|
|
107
|
+
*
|
|
108
|
+
* `loadConfig` already refuses the contradictory case (enabled with no
|
|
109
|
+
* endpoint) at startup, so there is nothing to validate here.
|
|
110
|
+
*/
|
|
111
|
+
export function rerankerFor(config) {
|
|
112
|
+
if (!config.rerankerEnabled || config.rerankerEndpoint === undefined)
|
|
113
|
+
return undefined;
|
|
114
|
+
return new HttpReranker(config.rerankerEndpoint);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Reorder hits by a reranker's scores and keep the best `topK`.
|
|
118
|
+
*
|
|
119
|
+
* Separate from the transport so the ordering rule can be tested without a
|
|
120
|
+
* model server — the rule being the part that is easy to get subtly wrong.
|
|
121
|
+
*
|
|
122
|
+
* The sort is stable in the sense that matters: ties keep the fusion order they
|
|
123
|
+
* arrived in, because `Array.prototype.sort` is stable and the comparator
|
|
124
|
+
* answers 0 for equal scores. A cross-encoder returning identical scores for
|
|
125
|
+
* two chunks is saying it cannot separate them, and falling back to the index's
|
|
126
|
+
* own ranking is a better answer than an arbitrary one.
|
|
127
|
+
*/
|
|
128
|
+
export function applyRanking(items, scores, topK) {
|
|
129
|
+
if (scores.length !== items.length) {
|
|
130
|
+
throw new Error(`the reranker returned ${scores.length} scores for ${items.length} items`);
|
|
131
|
+
}
|
|
132
|
+
return items
|
|
133
|
+
.map((item, i) => ({ item, score: scores[i] }))
|
|
134
|
+
.sort((a, b) => b.score - a.score)
|
|
135
|
+
.slice(0, topK)
|
|
136
|
+
.map((s) => s.item);
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=rerank.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rerank.js","sourceRoot":"","sources":["../src/rerank.ts"],"names":[],"mappings":"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"}
|