@lacneu/openclaw-knowledge 3.1.2 → 3.2.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/CHANGELOG.md +368 -1
- package/README.md +131 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.js +26 -0
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +61 -4
- package/dist/index.js +463 -50
- package/dist/index.js.map +1 -1
- package/dist/jina/classifier.d.ts +55 -0
- package/dist/jina/classifier.js +170 -0
- package/dist/jina/classifier.js.map +1 -0
- package/dist/jina/client.d.ts +30 -0
- package/dist/jina/client.js +131 -0
- package/dist/jina/client.js.map +1 -0
- package/dist/jina/errors.d.ts +42 -0
- package/dist/jina/errors.js +113 -0
- package/dist/jina/errors.js.map +1 -0
- package/dist/jina/reranker.d.ts +34 -0
- package/dist/jina/reranker.js +95 -0
- package/dist/jina/reranker.js.map +1 -0
- package/dist/jina/types.d.ts +78 -0
- package/dist/jina/types.js +12 -0
- package/dist/jina/types.js.map +1 -0
- package/dist/pgvector.d.ts +29 -0
- package/dist/pgvector.js +68 -0
- package/dist/pgvector.js.map +1 -1
- package/dist/router/heuristic.d.ts +29 -0
- package/dist/router/heuristic.js +104 -0
- package/dist/router/heuristic.js.map +1 -0
- package/dist/router/index.d.ts +33 -0
- package/dist/router/index.js +94 -0
- package/dist/router/index.js.map +1 -0
- package/dist/router/labels.d.ts +33 -0
- package/dist/router/labels.js +67 -0
- package/dist/router/labels.js.map +1 -0
- package/dist/router/types.d.ts +23 -0
- package/dist/router/types.js +7 -0
- package/dist/router/types.js.map +1 -0
- package/dist/tracing/events.d.ts +83 -0
- package/dist/tracing/events.js +86 -0
- package/dist/tracing/events.js.map +1 -0
- package/dist/types.d.ts +61 -1
- package/openclaw.plugin.json +97 -4
- package/package.json +3 -3
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Jina Reranker client.
|
|
2
|
+
//
|
|
3
|
+
// Wraps POST /v1/rerank for use after a pgvector cosine-similarity pass:
|
|
4
|
+
// vector search returns N coarse candidates, then a cross-encoder rerank
|
|
5
|
+
// promotes the ones that actually answer the query.
|
|
6
|
+
//
|
|
7
|
+
// Implementation choices:
|
|
8
|
+
//
|
|
9
|
+
// 1. **`return_documents: false` is hardcoded.** The caller (pgvector source)
|
|
10
|
+
// already owns the `PgvectorResult` objects and only needs the new
|
|
11
|
+
// ordering. Asking Jina to echo the documents back would multiply the
|
|
12
|
+
// response payload by ~10× for nothing.
|
|
13
|
+
//
|
|
14
|
+
// 2. **`truncate: true` by default.** Lets Jina silently shrink each document
|
|
15
|
+
// to the model's per-doc cap rather than failing the whole batch. The
|
|
16
|
+
// LightRAG-side reranker uses the same setting (`RERANK_ENABLE_CHUNKING`).
|
|
17
|
+
//
|
|
18
|
+
// 3. **Defensive response parsing.** We validate the structural shape
|
|
19
|
+
// (`results: [{index, score}]`) and silently skip malformed entries
|
|
20
|
+
// rather than crashing. An empty or unrecognized response yields an
|
|
21
|
+
// empty array — the caller falls back to the original cosine order.
|
|
22
|
+
import { postJson } from "./client.js";
|
|
23
|
+
const RERANK_URL = "https://api.jina.ai/v1/rerank";
|
|
24
|
+
/**
|
|
25
|
+
* Default reranker model. v2-base-multilingual is the best-balanced choice
|
|
26
|
+
* for French-heavy corpora (LightRAG-side tuning notes from 2026-05-14 show
|
|
27
|
+
* v3 plateauing at 0.05-0.15 on short French queries vs dense chunks).
|
|
28
|
+
*/
|
|
29
|
+
const DEFAULT_RERANKER_MODEL = "jina-reranker-v2-base-multilingual";
|
|
30
|
+
/**
|
|
31
|
+
* Rerank `documents` against `query` and return the new ordering.
|
|
32
|
+
*
|
|
33
|
+
* Each item carries the ORIGINAL index from the input array plus the new
|
|
34
|
+
* relevance score. Callers should use the `index` to look up their own
|
|
35
|
+
* data structures rather than relying on document text.
|
|
36
|
+
*
|
|
37
|
+
* @returns `[]` for an empty input (no API call) or when the response
|
|
38
|
+
* shape is unrecognized (fail-open semantics).
|
|
39
|
+
* @throws on network / auth / API errors so the cooldown breaker can react.
|
|
40
|
+
*/
|
|
41
|
+
export async function rerank({ apiKey, query, documents, model = DEFAULT_RERANKER_MODEL, topN, timeoutMs, signal, }) {
|
|
42
|
+
if (documents.length === 0)
|
|
43
|
+
return [];
|
|
44
|
+
const body = {
|
|
45
|
+
model,
|
|
46
|
+
query,
|
|
47
|
+
documents,
|
|
48
|
+
return_documents: false,
|
|
49
|
+
truncate: true,
|
|
50
|
+
};
|
|
51
|
+
if (typeof topN === "number" && topN > 0) {
|
|
52
|
+
body.top_n = topN;
|
|
53
|
+
}
|
|
54
|
+
const raw = await postJson({
|
|
55
|
+
url: RERANK_URL,
|
|
56
|
+
body,
|
|
57
|
+
apiKey,
|
|
58
|
+
timeoutMs,
|
|
59
|
+
signal,
|
|
60
|
+
});
|
|
61
|
+
return parseRerankResponse(raw, documents.length);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Defensive parser for the /v1/rerank response.
|
|
65
|
+
*
|
|
66
|
+
* @internal exported for unit testing
|
|
67
|
+
*/
|
|
68
|
+
export function parseRerankResponse(raw, inputCount) {
|
|
69
|
+
if (!isRecord(raw))
|
|
70
|
+
return [];
|
|
71
|
+
const results = raw.results;
|
|
72
|
+
if (!Array.isArray(results))
|
|
73
|
+
return [];
|
|
74
|
+
const out = [];
|
|
75
|
+
for (const item of results) {
|
|
76
|
+
if (!isRecord(item))
|
|
77
|
+
continue;
|
|
78
|
+
const index = item.index;
|
|
79
|
+
const score = item.relevance_score ?? item.score;
|
|
80
|
+
if (typeof index !== "number" ||
|
|
81
|
+
!Number.isInteger(index) ||
|
|
82
|
+
index < 0 ||
|
|
83
|
+
index >= inputCount ||
|
|
84
|
+
typeof score !== "number" ||
|
|
85
|
+
!Number.isFinite(score)) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
out.push({ index, score });
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
function isRecord(value) {
|
|
93
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=reranker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reranker.js","sourceRoot":"","sources":["../../src/jina/reranker.ts"],"names":[],"mappings":"AAAA,wBAAwB;AACxB,EAAE;AACF,yEAAyE;AACzE,yEAAyE;AACzE,oDAAoD;AACpD,EAAE;AACF,0BAA0B;AAC1B,EAAE;AACF,8EAA8E;AAC9E,sEAAsE;AACtE,yEAAyE;AACzE,2CAA2C;AAC3C,EAAE;AACF,8EAA8E;AAC9E,yEAAyE;AACzE,8EAA8E;AAC9E,EAAE;AACF,sEAAsE;AACtE,uEAAuE;AACvE,uEAAuE;AACvE,uEAAuE;AAEvE,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAOvC,MAAM,UAAU,GAAG,+BAA+B,CAAC;AAEnD;;;;GAIG;AACH,MAAM,sBAAsB,GAAkB,oCAAoC,CAAC;AAkBnF;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,MAAM,CAAC,EAC3B,MAAM,EACN,KAAK,EACL,SAAS,EACT,KAAK,GAAG,sBAAsB,EAC9B,IAAI,EACJ,SAAS,EACT,MAAM,GACO;IACb,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAEtC,MAAM,IAAI,GAAsB;QAC9B,KAAK;QACL,KAAK;QACL,SAAS;QACT,gBAAgB,EAAE,KAAK;QACvB,QAAQ,EAAE,IAAI;KACf,CAAC;IACF,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,GAAG,CAAC,EAAE,CAAC;QACzC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;IACpB,CAAC;IAED,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAoB;QAC5C,GAAG,EAAE,UAAU;QACf,IAAI;QACJ,MAAM;QACN,SAAS;QACT,MAAM;KACP,CAAC,CAAC;IAEH,OAAO,mBAAmB,CAAC,GAAG,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC;AACpD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CACjC,GAAY,EACZ,UAAkB;IAElB,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,EAAE,CAAC;IAE9B,MAAM,OAAO,GAAG,GAAG,CAAC,OAAO,CAAC;IAC5B,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC;QAAE,OAAO,EAAE,CAAC;IAEvC,MAAM,GAAG,GAAmB,EAAE,CAAC;IAC/B,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;QAC3B,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;YAAE,SAAS;QAC9B,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;QACzB,MAAM,KAAK,GAAG,IAAI,CAAC,eAAe,IAAI,IAAI,CAAC,KAAK,CAAC;QACjD,IACE,OAAO,KAAK,KAAK,QAAQ;YACzB,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC;YACxB,KAAK,GAAG,CAAC;YACT,KAAK,IAAI,UAAU;YACnB,OAAO,KAAK,KAAK,QAAQ;YACzB,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,EACvB,CAAC;YACD,SAAS;QACX,CAAC;QACD,GAAG,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;IAC7B,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Embedding model used as backbone for Classifier zero-shot requests.
|
|
3
|
+
* The Reranker endpoint uses its own model identifiers (see RerankerModel).
|
|
4
|
+
*/
|
|
5
|
+
export type ClassifierEmbeddingModel = "jina-embeddings-v3" | "jina-embeddings-v4" | "jina-clip-v2";
|
|
6
|
+
/**
|
|
7
|
+
* Supported Jina reranker models.
|
|
8
|
+
*
|
|
9
|
+
* - `jina-reranker-v2-base-multilingual` (default in this plugin) — trained on
|
|
10
|
+
* 100+ languages, ideal for French content. Context cap: 8 192 tokens.
|
|
11
|
+
* - `jina-reranker-v3` — bigger context (131 K), primarily English-trained.
|
|
12
|
+
* - `jina-reranker-m0` — multimodal.
|
|
13
|
+
* - `jina-colbert-v2` — late-interaction.
|
|
14
|
+
*/
|
|
15
|
+
export type RerankerModel = "jina-reranker-v2-base-multilingual" | "jina-reranker-v3" | "jina-reranker-m0" | "jina-colbert-v2" | (string & {});
|
|
16
|
+
/**
|
|
17
|
+
* Input element for the Classifier endpoint. Text-only is what the plugin
|
|
18
|
+
* uses; the API also supports `{image: "..."}` with `jina-clip-v2`, but the
|
|
19
|
+
* router never classifies images.
|
|
20
|
+
*/
|
|
21
|
+
export interface ClassifierTextInput {
|
|
22
|
+
text: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Zero-shot classification request body for POST /v1/classify.
|
|
26
|
+
*
|
|
27
|
+
* `labels` must contain semantic category strings (the Classifier embeds
|
|
28
|
+
* them and picks the closest one to each input). At least 2 labels.
|
|
29
|
+
*/
|
|
30
|
+
export interface JinaClassifyZeroShotRequest {
|
|
31
|
+
model: ClassifierEmbeddingModel;
|
|
32
|
+
input: ClassifierTextInput[];
|
|
33
|
+
labels: string[];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Few-shot classification request body for POST /v1/classify (with a
|
|
37
|
+
* pre-trained classifier_id obtained out-of-band — the plugin does NOT
|
|
38
|
+
* implement /v1/train; operators train via the Jina Playground or CLI and
|
|
39
|
+
* paste the ID into the plugin config).
|
|
40
|
+
*/
|
|
41
|
+
export interface JinaClassifyFewShotRequest {
|
|
42
|
+
classifier_id: string;
|
|
43
|
+
input: ClassifierTextInput[];
|
|
44
|
+
}
|
|
45
|
+
export type JinaClassifyRequest = JinaClassifyZeroShotRequest | JinaClassifyFewShotRequest;
|
|
46
|
+
/**
|
|
47
|
+
* Normalized classification outcome as seen by the rest of the plugin.
|
|
48
|
+
* `label` is the picked class. `score` is the confidence in [0, 1] when
|
|
49
|
+
* Jina returns it; `null` when the field is not in the response (the
|
|
50
|
+
* defensive parser still produces a label in that case).
|
|
51
|
+
*/
|
|
52
|
+
export interface ClassificationOutcome {
|
|
53
|
+
label: string;
|
|
54
|
+
score: number | null;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Reranker request body for POST /v1/rerank.
|
|
58
|
+
*
|
|
59
|
+
* We always send `return_documents: false`: the caller already holds the
|
|
60
|
+
* original documents (PgvectorResult[]) and only needs the new ordering. This
|
|
61
|
+
* saves a meaningful chunk of egress tokens on large payloads.
|
|
62
|
+
*/
|
|
63
|
+
export interface JinaRerankRequest {
|
|
64
|
+
model: RerankerModel;
|
|
65
|
+
query: string;
|
|
66
|
+
documents: string[];
|
|
67
|
+
top_n?: number;
|
|
68
|
+
return_documents: false;
|
|
69
|
+
truncate?: boolean;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Single reranked result item — `index` references the original `documents`
|
|
73
|
+
* array position.
|
|
74
|
+
*/
|
|
75
|
+
export interface RerankedItem {
|
|
76
|
+
index: number;
|
|
77
|
+
score: number;
|
|
78
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Jina AI API request/response types.
|
|
2
|
+
//
|
|
3
|
+
// Documented at:
|
|
4
|
+
// https://jina.ai/classifier/ (POST /v1/classify, zero-shot + few-shot)
|
|
5
|
+
// https://jina.ai/reranker/ (POST /v1/rerank)
|
|
6
|
+
//
|
|
7
|
+
// We deliberately keep the response shapes flexible (`unknown`-leaning) so the
|
|
8
|
+
// parser can stay defensive: Jina has changed field names between iterations
|
|
9
|
+
// (e.g. `predictions[]` vs `results[]`), and a brittle interface would mask
|
|
10
|
+
// silent breakage. Strong typing happens at the parser boundary.
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/jina/types.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,EAAE;AACF,iBAAiB;AACjB,4EAA4E;AAC5E,oDAAoD;AACpD,EAAE;AACF,+EAA+E;AAC/E,6EAA6E;AAC7E,4EAA4E;AAC5E,iEAAiE"}
|
package/dist/pgvector.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RerankerModel } from "./jina/types.js";
|
|
1
2
|
import type { PgPoolLike, PgvectorResult } from "./types.js";
|
|
2
3
|
/**
|
|
3
4
|
* Search a single collection in `knowledge_vectors` using cosine similarity.
|
|
@@ -19,3 +20,31 @@ export declare function searchCollection(pool: PgPoolLike, collection: string, v
|
|
|
19
20
|
* easily skip empty sections.
|
|
20
21
|
*/
|
|
21
22
|
export declare function formatPgvectorResults(results: PgvectorResult[], maxChars: number): string | null;
|
|
23
|
+
export interface RerankPgvectorParams {
|
|
24
|
+
apiKey: string;
|
|
25
|
+
query: string;
|
|
26
|
+
model?: RerankerModel;
|
|
27
|
+
/** Cap on the number of results returned post-rerank. */
|
|
28
|
+
topN?: number;
|
|
29
|
+
timeoutMs?: number;
|
|
30
|
+
signal?: AbortSignal;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Re-order `results` using the Jina cross-encoder reranker.
|
|
34
|
+
*
|
|
35
|
+
* This is the precision stage on top of pgvector's recall: the cosine pass
|
|
36
|
+
* grabs ~20 coarse candidates, the reranker promotes the ones that actually
|
|
37
|
+
* match the user's intent.
|
|
38
|
+
*
|
|
39
|
+
* Contract:
|
|
40
|
+
* - On success, returns at most `topN` items in descending relevance order.
|
|
41
|
+
* The original cosine `score` is **preserved** on each item; the reranker
|
|
42
|
+
* produces its own score we expose via the log event, not in the
|
|
43
|
+
* PgvectorResult.
|
|
44
|
+
* - On any error (including auth / rate limit), throws a `JinaError`. The
|
|
45
|
+
* caller is responsible for falling back to the original cosine order —
|
|
46
|
+
* we do NOT swallow here because the caller wants to track the failure
|
|
47
|
+
* for the cooldown breaker.
|
|
48
|
+
* - On empty input, returns `[]` without hitting the network.
|
|
49
|
+
*/
|
|
50
|
+
export declare function rerankPgvectorResults(results: PgvectorResult[], params: RerankPgvectorParams): Promise<PgvectorResult[]>;
|
package/dist/pgvector.js
CHANGED
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
// `halfvec(3072)` because pgvector's HNSW implementation caps at 2000 dims
|
|
5
5
|
// for the native `vector` type. Both the column cast and the query parameter
|
|
6
6
|
// cast must match, otherwise the planner falls back to a sequential scan.
|
|
7
|
+
//
|
|
8
|
+
// As of v3.2.0, results from `searchCollection` may optionally be re-ordered
|
|
9
|
+
// by a Jina cross-encoder reranker (see `rerankPgvectorResults`). The vector
|
|
10
|
+
// search remains the recall stage; the reranker is the precision stage.
|
|
11
|
+
import { rerank } from "./jina/reranker.js";
|
|
12
|
+
import { JinaError } from "./jina/errors.js";
|
|
7
13
|
const SEARCH_SQL = `SELECT file_name, mime_type, text, file_id, source, owner,
|
|
8
14
|
chunk_index, total_chunks, timestamp_start, timestamp_end,
|
|
9
15
|
embedded_at,
|
|
@@ -78,4 +84,66 @@ export function formatPgvectorResults(results, maxChars) {
|
|
|
78
84
|
}
|
|
79
85
|
return output;
|
|
80
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Re-order `results` using the Jina cross-encoder reranker.
|
|
89
|
+
*
|
|
90
|
+
* This is the precision stage on top of pgvector's recall: the cosine pass
|
|
91
|
+
* grabs ~20 coarse candidates, the reranker promotes the ones that actually
|
|
92
|
+
* match the user's intent.
|
|
93
|
+
*
|
|
94
|
+
* Contract:
|
|
95
|
+
* - On success, returns at most `topN` items in descending relevance order.
|
|
96
|
+
* The original cosine `score` is **preserved** on each item; the reranker
|
|
97
|
+
* produces its own score we expose via the log event, not in the
|
|
98
|
+
* PgvectorResult.
|
|
99
|
+
* - On any error (including auth / rate limit), throws a `JinaError`. The
|
|
100
|
+
* caller is responsible for falling back to the original cosine order —
|
|
101
|
+
* we do NOT swallow here because the caller wants to track the failure
|
|
102
|
+
* for the cooldown breaker.
|
|
103
|
+
* - On empty input, returns `[]` without hitting the network.
|
|
104
|
+
*/
|
|
105
|
+
export async function rerankPgvectorResults(results, params) {
|
|
106
|
+
if (results.length === 0)
|
|
107
|
+
return [];
|
|
108
|
+
// The reranker only sees the textual content. Empty/null texts cannot be
|
|
109
|
+
// ranked, so we filter them out BEFORE the API call to avoid wasting
|
|
110
|
+
// tokens on rows the cross-encoder can't score anyway.
|
|
111
|
+
const indexed = results
|
|
112
|
+
.map((r, i) => ({ row: r, originalIndex: i, text: r.text ?? "" }))
|
|
113
|
+
.filter((x) => x.text.trim().length > 0);
|
|
114
|
+
if (indexed.length === 0)
|
|
115
|
+
return results.slice(0, params.topN ?? results.length);
|
|
116
|
+
try {
|
|
117
|
+
const reranked = await rerank({
|
|
118
|
+
apiKey: params.apiKey,
|
|
119
|
+
query: params.query,
|
|
120
|
+
documents: indexed.map((x) => x.text),
|
|
121
|
+
model: params.model,
|
|
122
|
+
topN: params.topN,
|
|
123
|
+
timeoutMs: params.timeoutMs,
|
|
124
|
+
signal: params.signal,
|
|
125
|
+
});
|
|
126
|
+
if (reranked.length === 0) {
|
|
127
|
+
// Defensive: reranker returned no usable items. Surface the original
|
|
128
|
+
// cosine order rather than wiping the candidate list.
|
|
129
|
+
return results.slice(0, params.topN ?? results.length);
|
|
130
|
+
}
|
|
131
|
+
// Map back to PgvectorResult using the reranker's `index` (which points
|
|
132
|
+
// into our filtered `indexed` array, NOT the original `results` array).
|
|
133
|
+
const out = [];
|
|
134
|
+
for (const item of reranked) {
|
|
135
|
+
const found = indexed[item.index];
|
|
136
|
+
if (found)
|
|
137
|
+
out.push(found.row);
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
// Re-throw only if it's a Jina error the caller will recognize. Any
|
|
143
|
+
// other failure (programmer error) propagates as-is.
|
|
144
|
+
if (err instanceof JinaError)
|
|
145
|
+
throw err;
|
|
146
|
+
throw err;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
81
149
|
//# sourceMappingURL=pgvector.js.map
|
package/dist/pgvector.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pgvector.js","sourceRoot":"","sources":["../src/pgvector.ts"],"names":[],"mappings":"AAAA,wCAAwC;AACxC,EAAE;AACF,iEAAiE;AACjE,2EAA2E;AAC3E,6EAA6E;AAC7E,0EAA0E;
|
|
1
|
+
{"version":3,"file":"pgvector.js","sourceRoot":"","sources":["../src/pgvector.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Route, RouterReason } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Triggers from the OpenClaw SDK that mean "not a user-initiated turn".
|
|
4
|
+
*
|
|
5
|
+
* Sourced from `PluginHookAgentContext.trigger` in the OpenClaw plugin SDK:
|
|
6
|
+
* `"user" | "heartbeat" | "cron" | "memory"`.
|
|
7
|
+
*/
|
|
8
|
+
export declare const NON_USER_TRIGGERS: Set<string>;
|
|
9
|
+
export interface HeuristicInput {
|
|
10
|
+
/** The extracted user query (already trimmed and length-validated). */
|
|
11
|
+
query: string;
|
|
12
|
+
/** Trigger from `PluginHookAgentContext`. May be undefined on legacy SDKs. */
|
|
13
|
+
trigger?: string;
|
|
14
|
+
/** Whether the sender is the local CLI test harness (id: "cli"). */
|
|
15
|
+
isCli?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface HeuristicVerdict {
|
|
18
|
+
route: Route | null;
|
|
19
|
+
reason: RouterReason;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Decide a route from cheap signals only. Returns `route: null` when the
|
|
23
|
+
* input is ambiguous and a classifier (or `ALL` fallback) should take over.
|
|
24
|
+
*
|
|
25
|
+
* The returned `reason` always identifies the rule that fired (or
|
|
26
|
+
* `"classifier_fallback"` if no rule did — yes that's reused, but a `null`
|
|
27
|
+
* route forces the caller to consult the classifier or fall back).
|
|
28
|
+
*/
|
|
29
|
+
export declare function heuristicRoute(input: HeuristicInput): HeuristicVerdict;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Zero-cost router heuristics.
|
|
2
|
+
//
|
|
3
|
+
// Runs BEFORE any Jina call. Three jobs:
|
|
4
|
+
//
|
|
5
|
+
// 1. **Deterministic skip on operational triggers.** When OpenClaw fires
|
|
6
|
+
// `before_prompt_build` with `ctx.trigger ∈ {heartbeat, cron, memory}`,
|
|
7
|
+
// we know the turn is not a real user question — skip retrieval
|
|
8
|
+
// unconditionally. This is the cheapest and most important gain:
|
|
9
|
+
// heartbeats fire continuously and were eating ~95% of the previous
|
|
10
|
+
// Jina quota.
|
|
11
|
+
//
|
|
12
|
+
// 2. **Meta-agent regex matches.** Questions like "what is your session
|
|
13
|
+
// id" or "combien d'agents ici" cannot be answered by the knowledge
|
|
14
|
+
// base, so we skip them deterministically too.
|
|
15
|
+
//
|
|
16
|
+
// 3. **Keyword fast-paths for common business questions.** A small set of
|
|
17
|
+
// regex hints lets the heuristic decide on its own (PGVECTOR_ONLY vs
|
|
18
|
+
// LIGHTRAG_ONLY) without paying for a Jina call. The router falls back
|
|
19
|
+
// to the classifier — or to `ALL` — when nothing matches.
|
|
20
|
+
//
|
|
21
|
+
// Everything in this module is pure (no I/O, no side effects) so the tests
|
|
22
|
+
// can exhaustively cover the matrix.
|
|
23
|
+
/**
|
|
24
|
+
* Triggers from the OpenClaw SDK that mean "not a user-initiated turn".
|
|
25
|
+
*
|
|
26
|
+
* Sourced from `PluginHookAgentContext.trigger` in the OpenClaw plugin SDK:
|
|
27
|
+
* `"user" | "heartbeat" | "cron" | "memory"`.
|
|
28
|
+
*/
|
|
29
|
+
export const NON_USER_TRIGGERS = new Set(["heartbeat", "cron", "memory"]);
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Meta-agent questions — skip entirely
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
const META_PATTERNS = [
|
|
34
|
+
// Identifiant/Id de session, session id, runId, agent id
|
|
35
|
+
/\b(?:session\s*id|runid|run\s*id|identifiant\s+(?:de\s+)?session|sessions?\s*identifi(?:ant|cation))\b/i,
|
|
36
|
+
// Combien d'agents/subagents
|
|
37
|
+
/\bcombien\s+d['e\s]?\s*(?:sub-?)?agent/i,
|
|
38
|
+
// Self-introspection "qui es-tu", "what model are you", "who are you"
|
|
39
|
+
/\b(?:qui\s+es-tu|what\s+model\s+are\s+you|who\s+are\s+you|what\s+is\s+your\s+name|comment\s+t['e]appelles?-tu)\b/i,
|
|
40
|
+
// Trivial system pings — the WHOLE prompt must be a status/ping check.
|
|
41
|
+
// Anchored on `^` so business questions ending with the word "status"
|
|
42
|
+
// (e.g. "what is the ACME project status?") are NOT classified as meta.
|
|
43
|
+
// Same anchoring for the FR variants ("tu es là ?").
|
|
44
|
+
/^\s*(?:(?:system|the\s+system)\s+)?(?:status|ping|heartbeat)\s*[?!.]*\s*$/i,
|
|
45
|
+
/^\s*(?:are\s+you\s+(?:there|alive)|t['e]es?\s+(?:la|en\s+ligne))\s*[?!.]*\s*$/i,
|
|
46
|
+
];
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// CLI test guards — short trivial prompts coming from `id:"cli"`
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
const CLI_TRIVIAL_PATTERN = /^\s*(?:test|test\s+de\s+(?:bon\s+)?fonctionnement|ping|hello|hi|salut|coucou|ok|yes|no|oui|non)\W*\s*$/i;
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Keyword fast-paths
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
const PGVECTOR_KEYWORDS = [
|
|
55
|
+
/\bversion\b/i,
|
|
56
|
+
/\brelease\s+notes?\b/i,
|
|
57
|
+
/\bchangelog\b/i,
|
|
58
|
+
/\bsource\s+(?:document|file|pdf|markdown)\b/i,
|
|
59
|
+
/\b\w+\.(?:md|pdf|yaml|yml|json|ts|js|py|sh)\b/i, // file name with extension
|
|
60
|
+
];
|
|
61
|
+
const LIGHTRAG_KEYWORDS = [
|
|
62
|
+
/\bcompare\w*\b/i,
|
|
63
|
+
/\baudit\b/i,
|
|
64
|
+
/\bsynth[éè]se\b/i,
|
|
65
|
+
/\brelations?\s+entre\b/i,
|
|
66
|
+
/\bqui\s+(?:travaille|collabore|coach|forme)\s+(?:avec|pour|chez)\b/i,
|
|
67
|
+
/\bdifferent\w*\s+(?:entre|de)\b/i,
|
|
68
|
+
];
|
|
69
|
+
/**
|
|
70
|
+
* Decide a route from cheap signals only. Returns `route: null` when the
|
|
71
|
+
* input is ambiguous and a classifier (or `ALL` fallback) should take over.
|
|
72
|
+
*
|
|
73
|
+
* The returned `reason` always identifies the rule that fired (or
|
|
74
|
+
* `"classifier_fallback"` if no rule did — yes that's reused, but a `null`
|
|
75
|
+
* route forces the caller to consult the classifier or fall back).
|
|
76
|
+
*/
|
|
77
|
+
export function heuristicRoute(input) {
|
|
78
|
+
const { query, trigger, isCli } = input;
|
|
79
|
+
// 1. Operational trigger → NEVER call any source.
|
|
80
|
+
if (trigger && NON_USER_TRIGGERS.has(trigger)) {
|
|
81
|
+
return { route: "NONE", reason: "heuristic_trigger" };
|
|
82
|
+
}
|
|
83
|
+
// 2. Meta-agent question → NEVER call any source.
|
|
84
|
+
for (const re of META_PATTERNS) {
|
|
85
|
+
if (re.test(query)) {
|
|
86
|
+
return { route: "NONE", reason: "heuristic_meta" };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// 3. CLI trivial prompt → NEVER call any source. Restricted to CLI to
|
|
90
|
+
// avoid blocking legitimate-but-short collaborator questions.
|
|
91
|
+
if (isCli && CLI_TRIVIAL_PATTERN.test(query)) {
|
|
92
|
+
return { route: "NONE", reason: "heuristic_short" };
|
|
93
|
+
}
|
|
94
|
+
// 4. Keyword fast-paths.
|
|
95
|
+
if (PGVECTOR_KEYWORDS.some((re) => re.test(query))) {
|
|
96
|
+
return { route: "PGVECTOR_ONLY", reason: "heuristic_keyword" };
|
|
97
|
+
}
|
|
98
|
+
if (LIGHTRAG_KEYWORDS.some((re) => re.test(query))) {
|
|
99
|
+
return { route: "LIGHTRAG_ONLY", reason: "heuristic_keyword" };
|
|
100
|
+
}
|
|
101
|
+
// 5. Nothing fired — defer to the classifier (or fallback to ALL).
|
|
102
|
+
return { route: null, reason: "classifier_fallback" };
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=heuristic.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"heuristic.js","sourceRoot":"","sources":["../../src/router/heuristic.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,EAAE;AACF,yCAAyC;AACzC,EAAE;AACF,2EAA2E;AAC3E,6EAA6E;AAC7E,qEAAqE;AACrE,sEAAsE;AACtE,yEAAyE;AACzE,mBAAmB;AACnB,EAAE;AACF,0EAA0E;AAC1E,yEAAyE;AACzE,oDAAoD;AACpD,EAAE;AACF,4EAA4E;AAC5E,0EAA0E;AAC1E,4EAA4E;AAC5E,+DAA+D;AAC/D,EAAE;AACF,2EAA2E;AAC3E,qCAAqC;AAIrC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;AAE1E,8EAA8E;AAC9E,uCAAuC;AACvC,8EAA8E;AAE9E,MAAM,aAAa,GAAa;IAC9B,yDAAyD;IACzD,yGAAyG;IACzG,6BAA6B;IAC7B,yCAAyC;IACzC,sEAAsE;IACtE,mHAAmH;IACnH,uEAAuE;IACvE,sEAAsE;IACtE,wEAAwE;IACxE,qDAAqD;IACrD,4EAA4E;IAC5E,gFAAgF;CACjF,CAAC;AAEF,8EAA8E;AAC9E,iEAAiE;AACjE,8EAA8E;AAE9E,MAAM,mBAAmB,GACvB,yGAAyG,CAAC;AAE5G,8EAA8E;AAC9E,qBAAqB;AACrB,8EAA8E;AAE9E,MAAM,iBAAiB,GAAa;IAClC,cAAc;IACd,uBAAuB;IACvB,gBAAgB;IAChB,8CAA8C;IAC9C,gDAAgD,EAAE,2BAA2B;CAC9E,CAAC;AAEF,MAAM,iBAAiB,GAAa;IAClC,iBAAiB;IACjB,YAAY;IACZ,kBAAkB;IAClB,yBAAyB;IACzB,qEAAqE;IACrE,kCAAkC;CACnC,CAAC;AAoBF;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAC,KAAqB;IAClD,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,KAAK,CAAC;IAExC,kDAAkD;IAClD,IAAI,OAAO,IAAI,iBAAiB,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;QAC9C,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,mBAAmB,EAAE,CAAC;IACxD,CAAC;IAED,kDAAkD;IAClD,KAAK,MAAM,EAAE,IAAI,aAAa,EAAE,CAAC;QAC/B,IAAI,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YACnB,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC;QACrD,CAAC;IACH,CAAC;IAED,sEAAsE;IACtE,iEAAiE;IACjE,IAAI,KAAK,IAAI,mBAAmB,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QAC7C,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC;IACtD,CAAC;IAED,yBAAyB;IACzB,IAAI,iBAAiB,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;QACnD,OAAO,EAAE,KAAK,EAAE,eAAe,EAAE,MAAM,EAAE,mBAAmB,EAAE,CAAC;IACjE,CAAC;IACD,IAAI,iBAAiB,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;QACnD,OAAO,EAAE,KAAK,EAAE,eAAe,EAAE,MAAM,EAAE,mBAAmB,EAAE,CAAC;IACjE,CAAC;IAED,mEAAmE;IACnE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,qBAAqB,EAAE,CAAC;AACxD,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { RouterDecision } from "./types.js";
|
|
2
|
+
export interface RouterConfig {
|
|
3
|
+
/** Master switch. When false, every call returns `{route: "ALL"}`. */
|
|
4
|
+
enabled: boolean;
|
|
5
|
+
/** Which engine fills the gap when heuristics are ambiguous. */
|
|
6
|
+
mode: "heuristic" | "jina-classifier";
|
|
7
|
+
/** Jina API key — required when mode === "jina-classifier". */
|
|
8
|
+
jinaApiKey: string;
|
|
9
|
+
/**
|
|
10
|
+
* Optional few-shot classifier ID. When provided, the router calls
|
|
11
|
+
* `/v1/classify` with this ID instead of running zero-shot.
|
|
12
|
+
*/
|
|
13
|
+
classifierId?: string;
|
|
14
|
+
/** Labels for zero-shot classification. Defaults to {@link DEFAULT_ROUTER_LABELS}. */
|
|
15
|
+
labels?: readonly string[];
|
|
16
|
+
/** Triggers that bypass retrieval (subset of NON_USER_TRIGGERS). */
|
|
17
|
+
skipTriggers?: readonly string[];
|
|
18
|
+
}
|
|
19
|
+
export interface RouterRuntimeContext {
|
|
20
|
+
query: string;
|
|
21
|
+
trigger?: string;
|
|
22
|
+
/** Whether the sender is the local CLI test harness. */
|
|
23
|
+
isCli?: boolean;
|
|
24
|
+
/** Optional AbortSignal propagated to the classifier HTTP call. */
|
|
25
|
+
signal?: AbortSignal;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Decide the route for one user turn.
|
|
29
|
+
*
|
|
30
|
+
* Returns a {@link RouterDecision}. The plugin caller logs `reason` and
|
|
31
|
+
* uses `route` to gate source calls.
|
|
32
|
+
*/
|
|
33
|
+
export declare function decideRoute(cfg: RouterConfig, ctx: RouterRuntimeContext): Promise<RouterDecision>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Router orchestrator: heuristic → classifier (optional) → fallback.
|
|
2
|
+
//
|
|
3
|
+
// Public entry point is `decideRoute(...)`. It produces a `RouterDecision`
|
|
4
|
+
// the hook handler consumes to gate calls to pgvector / LightRAG.
|
|
5
|
+
//
|
|
6
|
+
// Design contract (fail-open):
|
|
7
|
+
// - Any error in the classifier MUST yield `ALL` so retrieval keeps
|
|
8
|
+
// working. The router never blocks the agent.
|
|
9
|
+
// - The classifier is only called when the heuristic returns `null`
|
|
10
|
+
// (ambiguous input). Heuristic hits are deterministic and free.
|
|
11
|
+
// - Classifier results that don't map to a known label fall back to
|
|
12
|
+
// `ALL` with reason `"classifier_fallback"`.
|
|
13
|
+
import { classifyFewShot, classifyZeroShot, } from "../jina/classifier.js";
|
|
14
|
+
import { JinaError } from "../jina/errors.js";
|
|
15
|
+
import { DEFAULT_ROUTER_LABELS, ROUTER_LABEL_NAMES, extractRouteFromLabel, } from "./labels.js";
|
|
16
|
+
import { heuristicRoute } from "./heuristic.js";
|
|
17
|
+
const FALLBACK = "ALL";
|
|
18
|
+
/**
|
|
19
|
+
* Decide the route for one user turn.
|
|
20
|
+
*
|
|
21
|
+
* Returns a {@link RouterDecision}. The plugin caller logs `reason` and
|
|
22
|
+
* uses `route` to gate source calls.
|
|
23
|
+
*/
|
|
24
|
+
export async function decideRoute(cfg, ctx) {
|
|
25
|
+
// 0. Disabled → preserve legacy behavior.
|
|
26
|
+
if (!cfg.enabled) {
|
|
27
|
+
return { route: "ALL", reason: "router_disabled", score: null };
|
|
28
|
+
}
|
|
29
|
+
// 1. Heuristic pass — cheap and deterministic.
|
|
30
|
+
const verdict = heuristicRoute({
|
|
31
|
+
query: ctx.query,
|
|
32
|
+
trigger: ctx.trigger,
|
|
33
|
+
isCli: ctx.isCli,
|
|
34
|
+
});
|
|
35
|
+
if (verdict.route !== null) {
|
|
36
|
+
return { route: verdict.route, reason: verdict.reason, score: null };
|
|
37
|
+
}
|
|
38
|
+
// 2. Classifier pass — only when heuristics were ambiguous.
|
|
39
|
+
if (cfg.mode === "heuristic") {
|
|
40
|
+
// Operator asked for heuristic-only routing; ambiguous → ALL.
|
|
41
|
+
return { route: FALLBACK, reason: "classifier_fallback", score: null };
|
|
42
|
+
}
|
|
43
|
+
if (!cfg.jinaApiKey) {
|
|
44
|
+
// Misconfiguration safety net — never crash, just fall back.
|
|
45
|
+
return { route: FALLBACK, reason: "classifier_fallback", score: null };
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const outcome = cfg.classifierId
|
|
49
|
+
? await classifyFewShot({
|
|
50
|
+
apiKey: cfg.jinaApiKey,
|
|
51
|
+
text: ctx.query,
|
|
52
|
+
classifierId: cfg.classifierId,
|
|
53
|
+
expectedLabels: ROUTER_LABEL_NAMES,
|
|
54
|
+
signal: ctx.signal,
|
|
55
|
+
})
|
|
56
|
+
: await classifyZeroShot({
|
|
57
|
+
apiKey: cfg.jinaApiKey,
|
|
58
|
+
text: ctx.query,
|
|
59
|
+
labels: (cfg.labels ?? DEFAULT_ROUTER_LABELS),
|
|
60
|
+
signal: ctx.signal,
|
|
61
|
+
});
|
|
62
|
+
if (!outcome) {
|
|
63
|
+
return { route: FALLBACK, reason: "classifier_fallback", score: null };
|
|
64
|
+
}
|
|
65
|
+
const routeName =
|
|
66
|
+
// Few-shot classifiers return the raw label name as trained; zero-shot
|
|
67
|
+
// returns the full descriptive label — strip the colon prefix.
|
|
68
|
+
cfg.classifierId
|
|
69
|
+
? outcome.label
|
|
70
|
+
: (extractRouteFromLabel(outcome.label) ?? outcome.label);
|
|
71
|
+
if (!isKnownRoute(routeName)) {
|
|
72
|
+
return { route: FALLBACK, reason: "classifier_fallback", score: outcome.score };
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
route: routeName,
|
|
76
|
+
reason: "classifier_hit",
|
|
77
|
+
score: outcome.score,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
// Re-throw non-Jina errors (programmer errors, type mismatches). Jina
|
|
82
|
+
// failures fail open.
|
|
83
|
+
if (!(err instanceof JinaError))
|
|
84
|
+
throw err;
|
|
85
|
+
return { route: FALLBACK, reason: "classifier_error", score: null };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function isKnownRoute(value) {
|
|
89
|
+
return (value === "NONE" ||
|
|
90
|
+
value === "PGVECTOR_ONLY" ||
|
|
91
|
+
value === "LIGHTRAG_ONLY" ||
|
|
92
|
+
value === "ALL");
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/router/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public constants — MUST stay in sync with `Route` in `types.js`.
|
|
3
|
+
*
|
|
4
|
+
* The label prefix passed to Jina (zero-shot) and the canonical name a
|
|
5
|
+
* few-shot classifier MUST be trained against share the same literal
|
|
6
|
+
* values as the `Route` union ("NONE" | "PGVECTOR_ONLY" | "LIGHTRAG_ONLY"
|
|
7
|
+
* | "ALL"). If they diverged, the classifier path would always fall back
|
|
8
|
+
* to "ALL" because `isKnownRoute` would reject the predicted label.
|
|
9
|
+
*/
|
|
10
|
+
export declare const ROUTE_NONE = "NONE";
|
|
11
|
+
export declare const ROUTE_PGVECTOR_ONLY = "PGVECTOR_ONLY";
|
|
12
|
+
export declare const ROUTE_LIGHTRAG_ONLY = "LIGHTRAG_ONLY";
|
|
13
|
+
export declare const ROUTE_ALL = "ALL";
|
|
14
|
+
/**
|
|
15
|
+
* Default labels handed to Jina `/v1/classify` in zero-shot mode when the
|
|
16
|
+
* operator does not supply a few-shot `classifierId`.
|
|
17
|
+
*
|
|
18
|
+
* Order does not matter for correctness, but we keep `NONE` first by
|
|
19
|
+
* convention so test output is stable.
|
|
20
|
+
*/
|
|
21
|
+
export declare const DEFAULT_ROUTER_LABELS: readonly string[];
|
|
22
|
+
/**
|
|
23
|
+
* The label names that the classifier may legitimately return.
|
|
24
|
+
* Used by the defensive parser to refuse hallucinated classes.
|
|
25
|
+
*/
|
|
26
|
+
export declare const ROUTER_LABEL_NAMES: readonly string[];
|
|
27
|
+
/**
|
|
28
|
+
* Extract the route name from a full label string (the part before the
|
|
29
|
+
* colon). Returns `null` when the label is malformed.
|
|
30
|
+
*
|
|
31
|
+
* @internal exported for unit testing
|
|
32
|
+
*/
|
|
33
|
+
export declare function extractRouteFromLabel(label: string): string | null;
|