@wirecat/cli-core 0.434.0 → 0.435.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -1
- package/dist/cli/messenger/context.js +1 -0
- package/dist/cli/messenger/conversation-answers.d.ts +1 -0
- package/dist/cli/messenger/conversations-command.js +2 -2
- package/dist/cli/messenger/message-answers.d.ts +1 -0
- package/dist/cli/messenger/message-writes.d.ts +2 -0
- package/dist/cli/messenger/messages-search-command.js +6 -10
- package/dist/cli/messenger/models-command.js +34 -7
- package/dist/cli/messenger/search-answers.d.ts +3 -0
- package/dist/cli/messenger/search-answers.js +40 -8
- package/dist/cli/messenger/search-command.js +3 -1
- package/dist/cli/messenger/store-maintenance-command.js +7 -4
- package/dist/cli/model-catalogue-command.js +1 -1
- package/dist/cli/settings.d.ts +4 -0
- package/dist/cli/settings.js +5 -0
- package/dist/cli/shared-command-exports.d.ts +4 -0
- package/dist/embeddings/child.js +15 -5
- package/dist/embeddings/embed.d.ts +16 -2
- package/dist/embeddings/embed.js +57 -28
- package/dist/embeddings/models.d.ts +18 -0
- package/dist/embeddings/models.js +69 -0
- package/dist/embeddings/process.d.ts +6 -1
- package/dist/embeddings/process.js +23 -7
- package/dist/embeddings/rerank.d.ts +10 -0
- package/dist/embeddings/rerank.js +31 -0
- package/dist/entities/conversation-results.d.ts +2 -0
- package/dist/entities/evidence.d.ts +8 -0
- package/dist/entities/index.d.ts +27 -1
- package/dist/entities/listener.d.ts +4 -0
- package/dist/entities/message-context.d.ts +3 -0
- package/dist/entities/message-write.d.ts +4 -0
- package/dist/entities/message.d.ts +2 -0
- package/dist/entities/message.js +2 -0
- package/dist/entities/model.d.ts +5 -2
- package/dist/entities/model.js +5 -2
- package/dist/entities/store-maintenance.d.ts +1 -0
- package/dist/entities/store-maintenance.js +8 -1
- package/dist/entities/store-records.d.ts +1 -0
- package/dist/mcp/tool.d.ts +1 -1
- package/dist/mcp/tool.js +1 -0
- package/dist/services/deps.d.ts +3 -0
- package/dist/services/embeddings-maintenance.d.ts +10 -2
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.js +1 -0
- package/dist/services/messages-archive-search.d.ts +2 -2
- package/dist/services/messages-archive-search.js +4 -4
- package/dist/services/messages-discovery.d.ts +33 -4
- package/dist/services/messages-discovery.js +88 -3
- package/dist/services/messages-meaning.d.ts +2 -1
- package/dist/services/messages-meaning.js +7 -3
- package/dist/services/messages-rerank.d.ts +7 -0
- package/dist/services/messages-rerank.js +42 -0
- package/dist/services/messages-search.d.ts +5 -0
- package/dist/services/messages.d.ts +6 -2
- package/dist/services/messages.js +4 -3
- package/dist/services/model-downloads.d.ts +3 -1
- package/dist/services/model-downloads.js +20 -1
- package/dist/services/notes-search.d.ts +7 -2
- package/dist/services/notes-search.js +7 -4
- package/dist/services/search-all-meetings.d.ts +3 -2
- package/dist/services/search-all-meetings.js +8 -8
- package/dist/services/search-all.d.ts +7 -1
- package/dist/services/search-all.js +109 -45
- package/dist/store/exports.d.ts +1 -1
- package/dist/store/exports.js +1 -1
- package/dist/store/sqlite/accounts.js +2 -2
- package/dist/store/sqlite/chats.d.ts +1 -1
- package/dist/store/sqlite/chats.js +11 -4
- package/dist/store/sqlite/document-pictures.js +2 -7
- package/dist/store/sqlite/email-index.d.ts +1 -3
- package/dist/store/sqlite/email-index.js +5 -21
- package/dist/store/sqlite/emails.js +4 -3
- package/dist/store/sqlite/image-chunks.d.ts +2 -2
- package/dist/store/sqlite/image-chunks.js +10 -13
- package/dist/store/sqlite/meeting-vectors.js +11 -9
- package/dist/store/sqlite/message-chunks.js +3 -8
- package/dist/store/sqlite/note-index.js +2 -7
- package/dist/store/sqlite/schema.d.ts +1 -1
- package/dist/store/sqlite/schema.js +1 -1
- package/dist/store/sqlite/vector-index.d.ts +9 -0
- package/dist/store/sqlite/vector-index.js +25 -0
- package/dist/store/sqlite/vectors.d.ts +2 -2
- package/dist/store/sqlite/vectors.js +11 -8
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,41 @@ Notable changes to `@wirecat/cli-core` (`@wirecat/cli-messaging` up to 0.261.0,
|
|
|
5
5
|
minor version may break callers, and says how under "Changed — may break callers". `pnpm docs:check` checks the
|
|
6
6
|
shape of this file.
|
|
7
7
|
|
|
8
|
-
##
|
|
8
|
+
## 0.435.0 — 25.10.2026
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **`search messages --discover` reranks its first 50 hits, on its own, once the reranker is downloaded.** A cross-encoder reads the question with each hit, headed by its chat, author and date and preceded by the message it replies to, and its score is mixed with discovery's own, not put in its place: both scaled to 0–1 over those 50, then 0.3 · discovery + 0.7 · reranker; the hits after keep their order. This is the winning variant of the [evidence-pipeline benchmark](https://github.com/WireCatLabs/cli-testing/tree/main/performance/search/evidence-pipeline#reranker-variants) (holdout Answer@3 67% → 81% with mMARCO). Each hit it read has `discovery.reranker_score`; `score` stays discovery's. The default is `mmarco` (136 MB, `models download mmarco`); `bge-reranker-v2-m3` (588 MB) or `off` is the profile setting `search_reranker`. Loading and scoring do not count against the word search's 2 s: scoring has 15 s of its own, and a reranking that would not finish in them is dropped, never the search. Measured on a Ryzen AI 9 HX 470 through Core's WebAssembly runtime, 50 hits take mmarco about 0.9–1.7 s and BGE about 7–10 s. Without the model, discovery keeps its order and says so: a `not reranked: …` note in the terminal, `missing`/`next` (`models.download`) in `--json` and MCP. A search never downloads a model and never sends text anywhere; `--exact`, `--newest` and strict queries are untouched. `models list` shows the rerankers as kind `reranker`.
|
|
13
|
+
- **`search all` finds messages by meaning too, on its own, once it can.** It runs the same meaning lookup as
|
|
14
|
+
`search messages --discover` (e5-small over each message's own vector, the 100 nearest, every account the search
|
|
15
|
+
reads) and fuses it with the word hits by reciprocal rank fusion (k = 60) into one messages list, before the merge
|
|
16
|
+
with mail and notes. The query's filters apply to both; a query with NOT, or with a filter under OR or in a group,
|
|
17
|
+
stays words only, since the meaning hits cannot follow it (`cat OR fish` still uses meaning). Each item's `foundBy` says which search found it, and the answer's
|
|
18
|
+
`messages` says how they were searched. Without the model or the vectors, `search all` reads words as before and
|
|
19
|
+
says so: a `messages: meaning search not used: …` note, and `missing`/`next` (`models.download` or
|
|
20
|
+
`conversations.rebuild`) in `--json` and MCP. `--exact` and `--backend server` stay words only. A search never
|
|
21
|
+
downloads a model, embeds anything or sends text anywhere. `searchAll`, `searchAllWithMeetings` and
|
|
22
|
+
`searchAllIncludingMeetings` take the meaning lane as an optional last argument; a caller that passes none
|
|
23
|
+
(memo's `searchEverywhere` today) searches messages by words alone. `meaningLane` is exported and needs only
|
|
24
|
+
`{ env, store, messenger: { app: { command } } }`, so memo can build one without the messenger's services.
|
|
25
|
+
Messages and notes share one loaded model within a `search all`, so searching both by meaning loads e5-small once:
|
|
26
|
+
on a 10,000-message store with 200 notes the median went from 1.7 s to 0.9 s.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- **A vector goes once no text in the store uses it, by one rule for every kind.** Writing messages, images, notes,
|
|
31
|
+
documents and their pictures, mail and meeting transcripts, and purging an account or a chat, each had their own
|
|
32
|
+
query; they now all share one, which drops every model's vector of a released hash that no chunk
|
|
33
|
+
holds any more, and keeps one a chunk of any kind still holds. Deleting a note, document, memory or email still
|
|
34
|
+
goes through its delete trigger, which applies the same rule. Three writes left vectors behind and no longer do: an image whose
|
|
35
|
+
picture changed, a meeting transcript whose text changed, and a purged chat (`store clear --left` and a chat
|
|
36
|
+
merged into another), which also left its messages' and images' chunks. The rule reaches each vector by its key
|
|
37
|
+
(model, hash), where most of the old queries scanned every vector once per hash.
|
|
38
|
+
- **`conversations embeddings clear --chat` drops the chat's message vectors too**, not only its conversations'; a
|
|
39
|
+
vector a chunk outside the chat shares stays, and `conversations embed --chat` makes them again.
|
|
40
|
+
- **`store rebuild` drops the vectors no stored text uses**, which older builds left behind, and says how many in
|
|
41
|
+
`unused_vectors_dropped`. `store check` counts them (`vectors.unused`) and, when there are any, notes that
|
|
42
|
+
`store rebuild` drops them.
|
|
9
43
|
|
|
10
44
|
## 0.434.0 — 25.10.2026
|
|
11
45
|
|
|
@@ -414,6 +414,7 @@ export const messengerContext = (command, messenger, resolving = {}) => {
|
|
|
414
414
|
history: base.settings.keepFailedRuns,
|
|
415
415
|
searchCatchUp: base.settings.searchCatchUp,
|
|
416
416
|
pictures: base.settings.pictureSearch,
|
|
417
|
+
reranker: base.settings.searchReranker,
|
|
417
418
|
guard,
|
|
418
419
|
...(embedders ? { embedders } : {}),
|
|
419
420
|
connection: async () => {
|
|
@@ -66,6 +66,7 @@ export declare const conversationShown: (account: AccountKey, { summary, message
|
|
|
66
66
|
missing_terms: string[];
|
|
67
67
|
parent?: string | undefined;
|
|
68
68
|
found_by?: ("meaning" | "words")[] | undefined;
|
|
69
|
+
reranker_score?: number | undefined;
|
|
69
70
|
} | undefined;
|
|
70
71
|
context?: {
|
|
71
72
|
ref: string;
|
|
@@ -125,8 +125,8 @@ const embeddingStatusCommand = (messenger) => withModelOptions(annotate(new Comm
|
|
|
125
125
|
"\n");
|
|
126
126
|
});
|
|
127
127
|
const embeddingClearCommand = (messenger) => withModelOptions(destroysNothing(annotate(new Command("clear"), { output: { entity: "conversation_change" }, effects: LOCAL_WRITE }))
|
|
128
|
-
.summary("remove stored conversation vectors")
|
|
129
|
-
.description("drop a chat's vectors, or only one model's; messages and conversations are never touched")
|
|
128
|
+
.summary("remove a chat's stored conversation and message vectors")
|
|
129
|
+
.description("drop a chat's conversation and message vectors, or only one model's; messages and conversations are never touched")
|
|
130
130
|
.option("--chat <chat>", messenger.chatArgument)).action(async function () {
|
|
131
131
|
const options = this.optsWithGlobals();
|
|
132
132
|
const chat = chatOf(options);
|
|
@@ -43,6 +43,7 @@ export declare const threadAnswer: ({ items, ...thread }: ThreadContext, reader:
|
|
|
43
43
|
missing_terms: string[];
|
|
44
44
|
parent?: string | undefined;
|
|
45
45
|
found_by?: ("meaning" | "words")[] | undefined;
|
|
46
|
+
reranker_score?: number | undefined;
|
|
46
47
|
} | undefined;
|
|
47
48
|
context?: {
|
|
48
49
|
ref: string;
|
|
@@ -47,6 +47,7 @@ export declare const writtenAnswer: ({ operationId, sendId, message, messages, s
|
|
|
47
47
|
missing_terms: string[];
|
|
48
48
|
parent?: string | undefined;
|
|
49
49
|
found_by?: ("meaning" | "words")[] | undefined;
|
|
50
|
+
reranker_score?: number | undefined;
|
|
50
51
|
} | undefined;
|
|
51
52
|
context?: {
|
|
52
53
|
ref: string;
|
|
@@ -236,6 +237,7 @@ export declare const writtenAnswer: ({ operationId, sendId, message, messages, s
|
|
|
236
237
|
missing_terms: string[];
|
|
237
238
|
parent?: string | undefined;
|
|
238
239
|
found_by?: ("meaning" | "words")[] | undefined;
|
|
240
|
+
reranker_score?: number | undefined;
|
|
239
241
|
} | undefined;
|
|
240
242
|
context?: {
|
|
241
243
|
ref: string;
|
|
@@ -12,7 +12,7 @@ import { renderAnswer } from "../envelope.js";
|
|
|
12
12
|
import { positiveCount } from "../paging.js";
|
|
13
13
|
import { archiveSummary } from "./archive-summary.js";
|
|
14
14
|
import { messengerContext } from "./context.js";
|
|
15
|
-
import { messagesAnswer, offsetOf } from "./search-answers.js";
|
|
15
|
+
import { meaningNote, messagesAnswer, offsetOf } from "./search-answers.js";
|
|
16
16
|
import { backendOptions, backendRequest, noteServer } from "./search-backend-options.js";
|
|
17
17
|
import { syncOptions, syncRequest, syncsFirst } from "./search-sync-options.js";
|
|
18
18
|
import { threadOptions, threadRequest } from "./thread-options.js";
|
|
@@ -130,15 +130,11 @@ const searchAction = (messenger) => async function (given) {
|
|
|
130
130
|
context.renderer.note(`${from} → ${to.join(", ")}`);
|
|
131
131
|
for (const { source, reason } of found.skipped ?? [])
|
|
132
132
|
context.renderer.note(`${source} not searched: ${reason}`);
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
? ` — \`${command} conversations rebuild\` gives messages their vectors, a batch a run`
|
|
139
|
-
: "";
|
|
140
|
-
context.renderer.note(`meaning search not used: ${meaning.reason}${step}`);
|
|
141
|
-
}
|
|
133
|
+
if (found.meaningSkipped)
|
|
134
|
+
context.renderer.note(meaningNote(found.meaningSkipped, command));
|
|
135
|
+
const rerank = found.rerankSkipped;
|
|
136
|
+
if (rerank)
|
|
137
|
+
context.renderer.note(`not reranked: ${rerank.reason}${rerank.step === "models.download" ? ` — \`${command} models download ${rerank.model}\`` : ""}`);
|
|
142
138
|
if (found.picturesSkipped && found.picturesModel === undefined)
|
|
143
139
|
context.renderer.note(`pictures not searched: ${found.picturesSkipped}`);
|
|
144
140
|
const stemming = found.query?.stemming;
|
|
@@ -4,10 +4,10 @@ import { annotate } from "@wirecat/cli-base/commands";
|
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import { isTextModelInstalled, textModelsDirectory } from "../../embeddings/embed.js";
|
|
6
6
|
import { embeddingKeys } from "../../embeddings/keys.js";
|
|
7
|
-
import { DEFAULT_TEXT_MODEL, TEXT_MODELS } from "../../embeddings/models.js";
|
|
7
|
+
import { DEFAULT_RERANK_MODEL, DEFAULT_TEXT_MODEL, RERANK_MODELS, TEXT_MODELS, } from "../../embeddings/models.js";
|
|
8
8
|
import { model as modelEntity } from "../../entities/model.js";
|
|
9
9
|
import { DEFAULT_IMAGE_MODEL, IMAGE_MODELS, imageModelsDirectory, isImageModelInstalled, } from "../../images/picture/models.js";
|
|
10
|
-
import { downloadImageModel, downloadTextModel } from "../../services/model-downloads.js";
|
|
10
|
+
import { downloadImageModel, downloadRerankModel, downloadTextModel } from "../../services/model-downloads.js";
|
|
11
11
|
import { install, installedBytes, isInstalled, megabytes, modelPath, modelsDirectory, vadPath, } from "../../speech/install.js";
|
|
12
12
|
import { orderedModels, speechModel, VAD } from "../../speech/models.js";
|
|
13
13
|
import { choose } from "../../speech/transcribe.js";
|
|
@@ -44,6 +44,17 @@ const textItem = (model, directory) => ({
|
|
|
44
44
|
default: model.id === DEFAULT_TEXT_MODEL,
|
|
45
45
|
path: join(directory, model.id),
|
|
46
46
|
});
|
|
47
|
+
const rerankItem = (model, directory, chosen) => ({
|
|
48
|
+
id: model.id,
|
|
49
|
+
kind: "reranker",
|
|
50
|
+
title: model.title,
|
|
51
|
+
languages: model.languages,
|
|
52
|
+
licence: model.licence,
|
|
53
|
+
size_bytes: model.files.reduce((sum, file) => sum + file.bytes, 0),
|
|
54
|
+
downloaded: isTextModelInstalled(model, directory),
|
|
55
|
+
default: model.id === chosen,
|
|
56
|
+
path: join(directory, model.id),
|
|
57
|
+
});
|
|
47
58
|
const imageItem = (model, directory) => ({
|
|
48
59
|
id: model.id,
|
|
49
60
|
kind: "image",
|
|
@@ -93,6 +104,16 @@ const downloadEmbeddingModel = async (command, messenger, id, options) => {
|
|
|
93
104
|
renderAnswer(command, context, itemOf(textItem(model, directory)), downloaded);
|
|
94
105
|
}, { unbounded: true });
|
|
95
106
|
};
|
|
107
|
+
const downloadReranker = async (command, messenger, id) => {
|
|
108
|
+
const context = baseContext(command, messenger.resolveSettings);
|
|
109
|
+
await context.run(async () => {
|
|
110
|
+
const { model, directory } = await downloadRerankModel(id, {
|
|
111
|
+
env: context.env,
|
|
112
|
+
progress: (line) => context.renderer.note(line),
|
|
113
|
+
});
|
|
114
|
+
renderAnswer(command, context, itemOf(rerankItem(model, directory, context.settings.searchReranker ?? DEFAULT_RERANK_MODEL)), downloaded);
|
|
115
|
+
}, { unbounded: true });
|
|
116
|
+
};
|
|
96
117
|
const modelKeysCommand = (messenger, tool) => {
|
|
97
118
|
const key = new Command("keys").description("API keys for embedding and analysis providers");
|
|
98
119
|
grantsAuthority(annotate(key.command("set"), {
|
|
@@ -136,7 +157,7 @@ export const modelsCommand = (messenger) => {
|
|
|
136
157
|
effects: LOCAL_READ,
|
|
137
158
|
examples: [`${tool} models list`, `${tool} models list kind:audio`],
|
|
138
159
|
}), ROOT_MODELS_QUERY)
|
|
139
|
-
.description("speech and
|
|
160
|
+
.description("speech, embedding and reranker models: downloaded or not, each kind's default")
|
|
140
161
|
.action(async function (words) {
|
|
141
162
|
const terms = nativeTerms(listQuery(typedQuery(words), ROOT_MODELS_QUERY, "models list"), "models list");
|
|
142
163
|
const context = baseContext(this, messenger.resolveSettings);
|
|
@@ -150,25 +171,29 @@ export const modelsCommand = (messenger) => {
|
|
|
150
171
|
const textModels = kinds === undefined || kinds === "text"
|
|
151
172
|
? TEXT_MODELS.filter((model) => id === undefined || model.id === id)
|
|
152
173
|
: [];
|
|
174
|
+
const rerankers = kinds === undefined || kinds === "reranker"
|
|
175
|
+
? RERANK_MODELS.filter((model) => id === undefined || model.id === id)
|
|
176
|
+
: [];
|
|
153
177
|
const chosen = audioModels.length > 0 ? choose(messenger, context.settings, {}, context.env).model.id : "";
|
|
154
178
|
const catalogue = [
|
|
155
179
|
...audioModels.map((model) => audioItem(model, modelsDirectory(context.env), chosen)),
|
|
156
180
|
...textModels.map((model) => textItem(model, textModelsDirectory(context.env))),
|
|
181
|
+
...rerankers.map((model) => rerankItem(model, textModelsDirectory(context.env), context.settings.searchReranker ?? DEFAULT_RERANK_MODEL)),
|
|
157
182
|
];
|
|
158
183
|
const items = catalogue.filter((item) => kept === undefined || item.downloaded === (kept === "true"));
|
|
159
184
|
const next = items
|
|
160
185
|
.filter((item) => item.default && !item.downloaded)
|
|
161
186
|
.map((item) => nextStep("models.download", { args: { model: item.id } }));
|
|
162
|
-
renderAnswer(this, context, everyModel("models list", items, next), () => items.map((item) => `${item.kind.padEnd(
|
|
187
|
+
renderAnswer(this, context, everyModel("models list", items, next), () => items.map((item) => `${item.kind.padEnd(8)} ${modelLine(item, 18)} ${item.languages}\n`).join(""));
|
|
163
188
|
});
|
|
164
189
|
});
|
|
165
190
|
reachesOtherService(annotate(models.command("download"), {
|
|
166
191
|
effects: FETCH,
|
|
167
192
|
output: { entity: "model" },
|
|
168
|
-
examples: [`${tool} models download ${DEFAULT_TEXT_MODEL}`],
|
|
193
|
+
examples: [`${tool} models download ${DEFAULT_TEXT_MODEL}`, `${tool} models download ${DEFAULT_RERANK_MODEL}`],
|
|
169
194
|
}))
|
|
170
|
-
.summary("download and verify one speech or
|
|
171
|
-
.description("download and verify one speech or
|
|
195
|
+
.summary("download and verify one speech, embedding or reranker model")
|
|
196
|
+
.description("download and verify one speech, embedding or reranker model: tens to hundreds of MB, each size in models list")
|
|
172
197
|
.argument("<model>", "a model id from models list")
|
|
173
198
|
.option("--accept-terms", "accept the model's licence terms, for a model that has its own")
|
|
174
199
|
.action(async function (id, options) {
|
|
@@ -176,6 +201,8 @@ export const modelsCommand = (messenger) => {
|
|
|
176
201
|
return downloadAudioModel(this, messenger, id);
|
|
177
202
|
if (TEXT_MODELS.some((model) => model.id === id))
|
|
178
203
|
return downloadEmbeddingModel(this, messenger, id, options);
|
|
204
|
+
if (RERANK_MODELS.some((model) => model.id === id))
|
|
205
|
+
return downloadReranker(this, messenger, id);
|
|
179
206
|
throw new CliError("validation_error", "unknown model; choose an id from models list");
|
|
180
207
|
});
|
|
181
208
|
return models;
|
|
@@ -2,6 +2,7 @@ import type { Missing, NextStep } from "@wirecat/cli-base/standards";
|
|
|
2
2
|
import type { InferOutput } from "valibot";
|
|
3
3
|
import type { ENTITIES } from "../../entities/index.js";
|
|
4
4
|
import type { FoundMessage, SearchFound } from "../../services/messages.js";
|
|
5
|
+
import type { MeaningSkipped } from "../../services/messages-discovery.js";
|
|
5
6
|
import type { FoundNote, NotesFoundOf } from "../../services/notes-search.js";
|
|
6
7
|
import type { SearchAllItem } from "../../services/search-all.js";
|
|
7
8
|
import type { MeetingSearchItem, SearchAllIncludingMeetingsFound } from "../../services/search-all-meetings.js";
|
|
@@ -20,6 +21,8 @@ export declare const coverageGaps: (found: Pick<SearchFound, "coverage" | "refre
|
|
|
20
21
|
next: NextStep[];
|
|
21
22
|
missing: Missing[];
|
|
22
23
|
};
|
|
24
|
+
/** The pretty note for messages searched by words only, with the command that would add meaning. */
|
|
25
|
+
export declare const meaningNote: (meaning: MeaningSkipped, command: string) => string;
|
|
23
26
|
export interface Paging {
|
|
24
27
|
limit: number;
|
|
25
28
|
page?: number;
|
|
@@ -31,6 +31,7 @@ export const messageHitItem = (hit, query = "") => {
|
|
|
31
31
|
missing_terms: hit.discovery.missingTerms,
|
|
32
32
|
parent: hit.discovery.parent,
|
|
33
33
|
found_by: hit.discovery.foundBy,
|
|
34
|
+
reranker_score: hit.discovery.rerankerScore,
|
|
34
35
|
})
|
|
35
36
|
: undefined,
|
|
36
37
|
email: hit.email
|
|
@@ -88,6 +89,31 @@ export const coverageGaps = (found, shown) => {
|
|
|
88
89
|
],
|
|
89
90
|
};
|
|
90
91
|
};
|
|
92
|
+
/** Messages searched by words only, and the step that would add meaning: `--discover` and `search all` alike. */
|
|
93
|
+
const messageMeaningGap = (meaning, field) => {
|
|
94
|
+
const next = [];
|
|
95
|
+
if (!meaning)
|
|
96
|
+
return { next, missing: [] };
|
|
97
|
+
if (meaning.step === "models.download")
|
|
98
|
+
next.push(nextStep("models.download", { args: { model: meaning.model } }));
|
|
99
|
+
if (meaning.step === "conversations.rebuild")
|
|
100
|
+
next.push(nextStep("conversations.rebuild", { args: {} }));
|
|
101
|
+
return {
|
|
102
|
+
next,
|
|
103
|
+
missing: [
|
|
104
|
+
{ field, why: `searched by words only: ${meaning.reason}`, ...(meaning.step ? { step: meaning.step } : {}) },
|
|
105
|
+
],
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
/** The pretty note for messages searched by words only, with the command that would add meaning. */
|
|
109
|
+
export const meaningNote = (meaning, command) => {
|
|
110
|
+
const step = meaning.step === "models.download"
|
|
111
|
+
? ` — \`${command} models download ${meaning.model}\`, then \`${command} conversations rebuild\` gives messages their vectors`
|
|
112
|
+
: meaning.step === "conversations.rebuild"
|
|
113
|
+
? ` — \`${command} conversations rebuild\` gives messages their vectors, a batch a run`
|
|
114
|
+
: "";
|
|
115
|
+
return `meaning search not used: ${meaning.reason}${step}`;
|
|
116
|
+
};
|
|
91
117
|
const gapsOf = (found) => {
|
|
92
118
|
const { next, missing } = coverageGaps(found, found.items.length);
|
|
93
119
|
const building = !found.wordsReady || (found.query?.stemming?.applied === false && found.query.stemming.reason === "building");
|
|
@@ -101,16 +127,19 @@ const gapsOf = (found) => {
|
|
|
101
127
|
step: "store.migrate",
|
|
102
128
|
});
|
|
103
129
|
}
|
|
104
|
-
const meaning = found.meaningSkipped;
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
130
|
+
const meaning = messageMeaningGap(found.meaningSkipped, "discovery");
|
|
131
|
+
for (const step of meaning.next)
|
|
132
|
+
next.push(step);
|
|
133
|
+
for (const gap of meaning.missing)
|
|
134
|
+
missing.push(gap);
|
|
135
|
+
const rerank = found.rerankSkipped;
|
|
136
|
+
if (rerank) {
|
|
137
|
+
if (rerank.step === "models.download")
|
|
138
|
+
next.push(nextStep("models.download", { args: { model: rerank.model } }));
|
|
110
139
|
missing.push({
|
|
111
140
|
field: "discovery",
|
|
112
|
-
why: `
|
|
113
|
-
...(
|
|
141
|
+
why: `not reranked: ${rerank.reason}`,
|
|
142
|
+
...(rerank.step ? { step: rerank.step } : {}),
|
|
114
143
|
});
|
|
115
144
|
}
|
|
116
145
|
if (found.picturesModel !== undefined) {
|
|
@@ -197,6 +226,7 @@ export const notesAnswer = (command, found, { limit, page, again }, exact = fals
|
|
|
197
226
|
export const allAnswer = (command, found, { limit, page, again }, exact = false) => {
|
|
198
227
|
const scan = found.meetings?.complete === false ? found.meetings : undefined;
|
|
199
228
|
const notes = meaningGap(found.notes?.meaningSkipped, exact);
|
|
229
|
+
const messages = messageMeaningGap(found.messages?.meaningSkipped, "found_by");
|
|
200
230
|
return listOf(command, {
|
|
201
231
|
items: found.items.map(allHitItem),
|
|
202
232
|
limit,
|
|
@@ -214,6 +244,7 @@ export const allAnswer = (command, found, { limit, page, again }, exact = false)
|
|
|
214
244
|
]
|
|
215
245
|
: []),
|
|
216
246
|
...notes.next,
|
|
247
|
+
...messages.next.filter((step) => !notes.next.some((asked) => JSON.stringify(asked) === JSON.stringify(step))),
|
|
217
248
|
],
|
|
218
249
|
missing: [
|
|
219
250
|
...(scan
|
|
@@ -226,6 +257,7 @@ export const allAnswer = (command, found, { limit, page, again }, exact = false)
|
|
|
226
257
|
]
|
|
227
258
|
: []),
|
|
228
259
|
...notes.missing,
|
|
260
|
+
...messages.missing,
|
|
229
261
|
],
|
|
230
262
|
meta: metaOf(found.accounts, found.skipped),
|
|
231
263
|
});
|
|
@@ -11,7 +11,7 @@ import { commandAt } from "../steps.js";
|
|
|
11
11
|
import { messengerContext } from "./context.js";
|
|
12
12
|
import { conversationsSearchCommand } from "./conversations-command.js";
|
|
13
13
|
import { messagesSearchCommand } from "./messages-search-command.js";
|
|
14
|
-
import { allAnswer, notesAnswer, offsetOf, SOURCE_SEARCHED } from "./search-answers.js";
|
|
14
|
+
import { allAnswer, meaningNote, notesAnswer, offsetOf, SOURCE_SEARCHED, } from "./search-answers.js";
|
|
15
15
|
import { backendOptions, backendRequest, noteServer } from "./search-backend-options.js";
|
|
16
16
|
import { topicsSearchCommand } from "./topics-command.js";
|
|
17
17
|
const onlyOf = (value) => {
|
|
@@ -79,6 +79,8 @@ const allCommand = (messenger) => {
|
|
|
79
79
|
context.renderer.note(`${resource ?? source} not searched: ${reason}`);
|
|
80
80
|
if (found.notes?.meaningSkipped)
|
|
81
81
|
context.renderer.note(`notes by words only: ${found.notes.meaningSkipped}`);
|
|
82
|
+
if (found.messages?.meaningSkipped)
|
|
83
|
+
context.renderer.note(`messages: ${meaningNote(found.messages.meaningSkipped, messenger.app.command)}`);
|
|
82
84
|
const again = {
|
|
83
85
|
query: words.join(" "),
|
|
84
86
|
...(only === undefined ? {} : { only: only.join(",") }),
|
|
@@ -12,7 +12,7 @@ import { migrate } from "../../store/migrations.js";
|
|
|
12
12
|
import { openCache } from "../../store/open.js";
|
|
13
13
|
import { storePath } from "../../store/path.js";
|
|
14
14
|
import { deleteCopy, repairStore } from "../../store/repair.js";
|
|
15
|
-
import { drainNoteIndex, fillSearchIndex, fillStems, involvementStoreOver, noteIndexState, openSqlite, openStore, orphanPointers, pendingNormalization, resetAttachmentWords, resetNoteIndex, resetSearchIndex, resetStems, searchIndexState, stemmerCache, stemsState, vectorIndexCounts, } from "../../store/store.js";
|
|
15
|
+
import { drainNoteIndex, dropAllUnusedVectors, fillSearchIndex, fillStems, involvementStoreOver, noteIndexState, openSqlite, openStore, orphanPointers, pendingNormalization, resetAttachmentWords, resetNoteIndex, resetSearchIndex, resetStems, searchIndexState, stemmerCache, stemsState, vectorIndexCounts, } from "../../store/store.js";
|
|
16
16
|
import { environmentOf, outputFor } from "../context.js";
|
|
17
17
|
import { destroysNothing, LOCAL_READ, LOCAL_WRITE } from "../effects.js";
|
|
18
18
|
import { itemOf, renderAnswer, snakeKeys } from "../envelope.js";
|
|
@@ -217,6 +217,8 @@ const checkCommand = (messenger) => annotate(new Command("check"), { output: { e
|
|
|
217
217
|
const orphans = answer.orphanPointers.reduce((sum, { count }) => sum + count, 0);
|
|
218
218
|
if (orphans > 0)
|
|
219
219
|
renderer.note(`${orphans} pointers name a row that is gone or a type the store does not know — see orphan_pointers`);
|
|
220
|
+
if (answer.vectors && answer.vectors.unused > 0)
|
|
221
|
+
renderer.note(`${answer.vectors.unused} vectors belong to no text the store still holds — \`${command} store rebuild\` drops them`);
|
|
220
222
|
const older = answer.conversations.filter((chat) => chat.provider === messenger.provider && !chat.current).length;
|
|
221
223
|
if (older > 0) {
|
|
222
224
|
renderer.note(`${older} chats' conversations were built by older rules — \`${command} conversations links rebuild --chat <chat>\``);
|
|
@@ -285,8 +287,8 @@ const indexIntegrity = (database, index, rank = 1) => {
|
|
|
285
287
|
}
|
|
286
288
|
};
|
|
287
289
|
/**
|
|
288
|
-
* Vectors per model, and how many no chunk
|
|
289
|
-
*
|
|
290
|
+
* Vectors per model, and how many no chunk points at any more. Each write that drops chunks drops those
|
|
291
|
+
* (`dropUnusedVectors`), so a count above zero was most likely left by an older build; `store rebuild` drops them.
|
|
290
292
|
*/
|
|
291
293
|
const vectorsHeld = (database) => {
|
|
292
294
|
const exists = database.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'embeddings'").get();
|
|
@@ -418,7 +420,7 @@ const buildNoteIndex = (database, note, { reset = false } = {}) => {
|
|
|
418
420
|
};
|
|
419
421
|
const reindexCommand = (messenger) => destroysNothing(annotate(new Command("rebuild"), { output: { entity: "store_reindex" }, effects: LOCAL_WRITE }))
|
|
420
422
|
.summary("rebuild the local archive's search indexes")
|
|
421
|
-
.description("rebuild the word index, its typo vocabulary, the stems, the files' word index, the notes' indexes and who took part in what, from what is stored; loses nothing")
|
|
423
|
+
.description("rebuild the word index, its typo vocabulary, the stems, the files' word index, the notes' indexes and who took part in what, from what is stored, and drop the vectors no stored text uses; loses nothing")
|
|
422
424
|
.action(async function () {
|
|
423
425
|
const { renderer } = outputFor(this);
|
|
424
426
|
const path = storePath(environmentOf(this).env ?? process.env);
|
|
@@ -446,6 +448,7 @@ const reindexCommand = (messenger) => destroysNothing(annotate(new Command("rebu
|
|
|
446
448
|
...buildStems(database, (note) => renderer.note(note), { force: true }),
|
|
447
449
|
...buildNoteIndex(database, (note) => renderer.note(note), { reset: true }),
|
|
448
450
|
involvements: involvementStoreOver({ database, now: Date.now }).rebuild(),
|
|
451
|
+
unusedVectorsDropped: dropAllUnusedVectors(database),
|
|
449
452
|
};
|
|
450
453
|
});
|
|
451
454
|
result(this, answer);
|
|
@@ -15,7 +15,7 @@ export const MODEL_CATALOGUE_QUERY = defineQuery({
|
|
|
15
15
|
entity: "model",
|
|
16
16
|
backend: "native",
|
|
17
17
|
fields: [
|
|
18
|
-
termField("kind", "enum", "kind:audio", { values: ["audio", "text"] }),
|
|
18
|
+
termField("kind", "enum", "kind:audio", { values: ["audio", "text", "reranker"] }),
|
|
19
19
|
termField("id", "kw", "id:e5-small"),
|
|
20
20
|
termField("downloaded", "bool", "downloaded:true"),
|
|
21
21
|
],
|
package/dist/cli/settings.d.ts
CHANGED
|
@@ -82,6 +82,8 @@ export interface Settings extends AISettings {
|
|
|
82
82
|
searchCatchUp?: boolean;
|
|
83
83
|
/** Search and `images embed` read the pictures themselves, with a model on this machine. Off until set. */
|
|
84
84
|
pictureSearch?: boolean;
|
|
85
|
+
/** The reranker that orders `search messages --discover`'s first hits once it is downloaded, or `off`. */
|
|
86
|
+
searchReranker?: string;
|
|
85
87
|
limit: number;
|
|
86
88
|
/** Which page, 1-based. Per invocation only — a page number in a file is a setting nobody wants twice. */
|
|
87
89
|
page: number;
|
|
@@ -271,6 +273,7 @@ export declare const settingsFor: <T extends object = Record<never, never>>(app:
|
|
|
271
273
|
speech_model: v.OptionalSchema<v.StringSchema<(issue: v.BaseIssue<unknown>) => string>, undefined>;
|
|
272
274
|
search_catch_up: v.OptionalSchema<v.BooleanSchema<(issue: v.BaseIssue<unknown>) => string>, undefined>;
|
|
273
275
|
picture_search: v.OptionalSchema<v.BooleanSchema<(issue: v.BaseIssue<unknown>) => string>, undefined>;
|
|
276
|
+
search_reranker: v.OptionalSchema<v.PicklistSchema<[...string[], "off"], (issue: v.BaseIssue<unknown>) => string>, undefined>;
|
|
274
277
|
update_check: v.OptionalSchema<v.BooleanSchema<(issue: v.BaseIssue<unknown>) => string>, undefined>;
|
|
275
278
|
skill_hint: v.OptionalSchema<v.BooleanSchema<(issue: v.BaseIssue<unknown>) => string>, undefined>;
|
|
276
279
|
}, (issue: v.StrictObjectIssue) => string>, undefined>;
|
|
@@ -343,6 +346,7 @@ export declare const settingsFor: <T extends object = Record<never, never>>(app:
|
|
|
343
346
|
speech_model: v.OptionalSchema<v.StringSchema<(issue: v.BaseIssue<unknown>) => string>, undefined>;
|
|
344
347
|
search_catch_up: v.OptionalSchema<v.BooleanSchema<(issue: v.BaseIssue<unknown>) => string>, undefined>;
|
|
345
348
|
picture_search: v.OptionalSchema<v.BooleanSchema<(issue: v.BaseIssue<unknown>) => string>, undefined>;
|
|
349
|
+
search_reranker: v.OptionalSchema<v.PicklistSchema<[...string[], "off"], (issue: v.BaseIssue<unknown>) => string>, undefined>;
|
|
346
350
|
}, (issue: v.StrictObjectIssue) => string>, "has to be an object of profiles, by name">, {}>;
|
|
347
351
|
personal: v.OptionalSchema<v.StrictObjectSchema<{
|
|
348
352
|
readonly defaults: v.OptionalSchema<v.StrictObjectSchema<v.ObjectEntries, (issue: v.StrictObjectIssue) => string>, undefined>;
|
package/dist/cli/settings.js
CHANGED
|
@@ -3,6 +3,7 @@ import { CliError, configFilePath, resolvePaths, saveConfigFile } from "@wirecat
|
|
|
3
3
|
import * as v from "valibot";
|
|
4
4
|
import { AI_ENTRIES, resolveAISettings } from "../analysis/settings.js";
|
|
5
5
|
import { parseDuration } from "../domain/duration.js";
|
|
6
|
+
import { DEFAULT_RERANK_MODEL, RERANK_MODELS } from "../embeddings/models.js";
|
|
6
7
|
import { envName, readOnlyLockOf } from "../ports/app.js";
|
|
7
8
|
import { assertOverridesTighten, CEILING_KEY, LEVELS, layerPermissions, permissionOverrides, RESOURCES, } from "../sends/permissions.js";
|
|
8
9
|
import { storePath } from "../store/path.js";
|
|
@@ -63,6 +64,7 @@ const SHARED_PROFILE_ENTRIES = {
|
|
|
63
64
|
speech_model: v.optional(v.string(plain("has to be a model id from `models list`, in quotes"))),
|
|
64
65
|
search_catch_up: v.optional(flag),
|
|
65
66
|
picture_search: v.optional(flag),
|
|
67
|
+
search_reranker: v.optional(v.picklist([...RERANK_MODELS.map((model) => model.id), "off"], plain(`has to be off or a reranker id: ${RERANK_MODELS.map((model) => model.id).join(", ")}`))),
|
|
66
68
|
...AI_ENTRIES,
|
|
67
69
|
};
|
|
68
70
|
/**
|
|
@@ -228,6 +230,7 @@ export const settingsFor = (app, extension = {}) => {
|
|
|
228
230
|
const senderColors = fromLayers("sender_colors", false);
|
|
229
231
|
const searchCatchUp = fromLayers("search_catch_up", false);
|
|
230
232
|
const pictureSearch = fromLayers("picture_search", false);
|
|
233
|
+
const searchReranker = fromLayers("search_reranker", DEFAULT_RERANK_MODEL);
|
|
231
234
|
const keepRunsForDays = fromLayers("keep_runs_for_days", DEFAULT_KEEP_RUNS_FOR_DAYS);
|
|
232
235
|
// Set where an agent runs unattended; the config file and `--permission` cannot undo it.
|
|
233
236
|
const readOnlyLock = readOnlyLockOf(app, env);
|
|
@@ -275,6 +278,7 @@ export const settingsFor = (app, extension = {}) => {
|
|
|
275
278
|
senderColors: senderColors.value,
|
|
276
279
|
searchCatchUp: searchCatchUp.value,
|
|
277
280
|
pictureSearch: pictureSearch.value,
|
|
281
|
+
searchReranker: searchReranker.value,
|
|
278
282
|
limit: limit.value,
|
|
279
283
|
page: flags.page ?? 1,
|
|
280
284
|
all: flags.all === true,
|
|
@@ -308,6 +312,7 @@ export const settingsFor = (app, extension = {}) => {
|
|
|
308
312
|
senderColors: senderColors.from,
|
|
309
313
|
searchCatchUp: searchCatchUp.from,
|
|
310
314
|
pictureSearch: pictureSearch.from,
|
|
315
|
+
searchReranker: searchReranker.from,
|
|
311
316
|
record: record.from,
|
|
312
317
|
keepRunsForDays: keepRunsForDays.from,
|
|
313
318
|
sendsPerHour: sendsPerHour.from,
|
|
@@ -87,6 +87,7 @@ export declare const configCommand: (app: import("./entry.js").AppIdentity, conf
|
|
|
87
87
|
speech_model: import("valibot").OptionalSchema<import("valibot").StringSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
88
88
|
search_catch_up: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
89
89
|
picture_search: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
90
|
+
search_reranker: import("valibot").OptionalSchema<import("valibot").PicklistSchema<[...string[], "off"], (issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
90
91
|
update_check: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
91
92
|
skill_hint: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
92
93
|
}, (issue: import("valibot").StrictObjectIssue) => string>, undefined>;
|
|
@@ -159,6 +160,7 @@ export declare const configCommand: (app: import("./entry.js").AppIdentity, conf
|
|
|
159
160
|
speech_model: import("valibot").OptionalSchema<import("valibot").StringSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
160
161
|
search_catch_up: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
161
162
|
picture_search: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
163
|
+
search_reranker: import("valibot").OptionalSchema<import("valibot").PicklistSchema<[...string[], "off"], (issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
162
164
|
}, (issue: import("valibot").StrictObjectIssue) => string>, "has to be an object of profiles, by name">, {}>;
|
|
163
165
|
personal: import("valibot").OptionalSchema<import("valibot").StrictObjectSchema<{
|
|
164
166
|
readonly defaults: import("valibot").OptionalSchema<import("valibot").StrictObjectSchema<import("valibot").ObjectEntries, (issue: import("valibot").StrictObjectIssue) => string>, undefined>;
|
|
@@ -286,6 +288,7 @@ export declare const completeCommand: (messenger: import("./index.js").Messenger
|
|
|
286
288
|
speech_model: import("valibot").OptionalSchema<import("valibot").StringSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
287
289
|
search_catch_up: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
288
290
|
picture_search: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
291
|
+
search_reranker: import("valibot").OptionalSchema<import("valibot").PicklistSchema<[...string[], "off"], (issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
289
292
|
update_check: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
290
293
|
skill_hint: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
291
294
|
}, (issue: import("valibot").StrictObjectIssue) => string>, undefined>;
|
|
@@ -358,6 +361,7 @@ export declare const completeCommand: (messenger: import("./index.js").Messenger
|
|
|
358
361
|
speech_model: import("valibot").OptionalSchema<import("valibot").StringSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
359
362
|
search_catch_up: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
360
363
|
picture_search: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
364
|
+
search_reranker: import("valibot").OptionalSchema<import("valibot").PicklistSchema<[...string[], "off"], (issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
361
365
|
}, (issue: import("valibot").StrictObjectIssue) => string>, "has to be an object of profiles, by name">, {}>;
|
|
362
366
|
personal: import("valibot").OptionalSchema<import("valibot").StrictObjectSchema<{
|
|
363
367
|
readonly defaults: import("valibot").OptionalSchema<import("valibot").StrictObjectSchema<import("valibot").ObjectEntries, (issue: import("valibot").StrictObjectIssue) => string>, undefined>;
|
package/dist/embeddings/child.js
CHANGED
|
@@ -1,18 +1,28 @@
|
|
|
1
1
|
import { createInterface } from "node:readline";
|
|
2
2
|
import { openEmbedder } from "./embed.js";
|
|
3
|
-
|
|
4
|
-
const
|
|
3
|
+
import { openReranker } from "./rerank.js";
|
|
4
|
+
const { model, directory, threads, rerank } = JSON.parse(process.argv[2] ?? "{}");
|
|
5
|
+
const session = rerank
|
|
6
|
+
? { reranker: await openReranker(model, directory, { threads }) }
|
|
7
|
+
: { embedder: await openEmbedder(model, directory, { threads }) };
|
|
5
8
|
// stdout carries the answers alone, one JSON line each: anything else there breaks the parent's reading.
|
|
6
9
|
const answer = (line) => process.stdout.write(`${JSON.stringify(line)}\n`);
|
|
7
10
|
answer({ ready: true });
|
|
8
11
|
for await (const line of createInterface({ input: process.stdin })) {
|
|
9
|
-
const
|
|
12
|
+
const request = JSON.parse(line);
|
|
13
|
+
const { id } = request;
|
|
10
14
|
try {
|
|
11
|
-
|
|
15
|
+
if (session.reranker)
|
|
16
|
+
answer({ id, scores: await session.reranker.score(request.question, request.texts) });
|
|
17
|
+
else if (session.embedder)
|
|
18
|
+
answer({
|
|
19
|
+
id,
|
|
20
|
+
vectors: (await session.embedder.embed(request.texts, request.kind)).map((vector) => Array.from(vector)),
|
|
21
|
+
});
|
|
12
22
|
}
|
|
13
23
|
catch (error) {
|
|
14
24
|
answer({ id, error: error instanceof Error ? error.message : String(error) });
|
|
15
25
|
}
|
|
16
26
|
}
|
|
17
|
-
await embedder
|
|
27
|
+
await (session.reranker ?? session.embedder)?.close();
|
|
18
28
|
process.exit(0);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { TextModel } from "./models.js";
|
|
2
|
+
import type { Reranker } from "./rerank.js";
|
|
2
3
|
export declare const textModelsDirectory: (env?: NodeJS.ProcessEnv) => string;
|
|
3
|
-
export declare const placedText: (model: TextModel, directory: string) => [import("../speech/models.js").ModelFile, string][];
|
|
4
|
-
export declare const isTextModelInstalled: (model: TextModel, directory: string) => boolean;
|
|
4
|
+
export declare const placedText: (model: Pick<TextModel, "id" | "files">, directory: string) => [import("../speech/models.js").ModelFile, string][];
|
|
5
|
+
export declare const isTextModelInstalled: (model: Pick<TextModel, "id" | "files">, directory: string) => boolean;
|
|
5
6
|
/** Past 8 threads one session gains nothing; Bun takes 1 unless told (`2026-10-02-embeddings.md`). */
|
|
6
7
|
export declare const defaultThreads: () => number;
|
|
7
8
|
export type Kind = "query" | "passage";
|
|
@@ -19,9 +20,12 @@ export interface Embedder {
|
|
|
19
20
|
*/
|
|
20
21
|
export interface WarmEmbedders {
|
|
21
22
|
get(key: string, open: () => Promise<Embedder>): Promise<Embedder>;
|
|
23
|
+
reranker(key: string, open: () => Promise<Reranker>): Promise<Reranker>;
|
|
22
24
|
close(): Promise<void>;
|
|
23
25
|
}
|
|
24
26
|
export declare const WARM_IDLE_MS: number;
|
|
27
|
+
/** One search's models, so the parts of a search that need the same model load it once; closed when it ends. */
|
|
28
|
+
export declare const runEmbedders: () => Pick<WarmEmbedders, "get" | "close">;
|
|
25
29
|
export declare const warmEmbedders: ({ idleMs }?: {
|
|
26
30
|
idleMs?: number;
|
|
27
31
|
}) => WarmEmbedders;
|
|
@@ -30,6 +34,16 @@ export declare const truncated: (ids: number[], max: number) => number[];
|
|
|
30
34
|
export declare const unit: (vector: Float32Array) => Float32Array;
|
|
31
35
|
/** The mean over a single text's tokens: one text a run, so no padding to mask. */
|
|
32
36
|
export declare const meanOf: (data: Float32Array, tokens: number, dims: number) => Float32Array;
|
|
37
|
+
/** A model's tokenizer and ONNX session, from its folder. */
|
|
38
|
+
export declare const loadModel: (model: {
|
|
39
|
+
id: string;
|
|
40
|
+
onnx: string;
|
|
41
|
+
externalData?: string;
|
|
42
|
+
}, directory: string, threads: number) => Promise<{
|
|
43
|
+
ort: typeof import("@wirecat/cli-core-onnx");
|
|
44
|
+
tokenizer: any;
|
|
45
|
+
session: import("@wirecat/cli-core-onnx").InferenceSession;
|
|
46
|
+
}>;
|
|
33
47
|
export declare const openEmbedder: (model: TextModel, directory: string, { threads }?: {
|
|
34
48
|
threads?: number;
|
|
35
49
|
}) => Promise<Embedder>;
|