@takara-ai/miru-code 1.4.0 → 1.5.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/README.md +2 -0
- package/docs/self-hosted-sagemaker.md +116 -0
- package/package.json +7 -2
- package/src/cli.ts +18 -19
- package/src/credentials.ts +77 -7
- package/src/embeddings/openai.ts +194 -84
- package/src/embeddings/sagemaker.ts +268 -0
- package/src/embeddings/validate.ts +2 -1
- package/src/help.ts +35 -2
- package/src/index/bm25.ts +31 -21
- package/src/index/create.ts +114 -44
- package/src/index/incremental.ts +4 -11
- package/src/index/sparse.ts +14 -0
- package/src/literal.ts +1 -1
- package/src/prompt.ts +13 -0
- package/src/setup.ts +194 -14
package/src/help.ts
CHANGED
|
@@ -67,6 +67,27 @@ export function printEnvHelp(): void {
|
|
|
67
67
|
writeStdout(" MIRU_TOKENIZER_JSON");
|
|
68
68
|
writeStdout(" Path to tokenizer.json (default: <package>/tokenizer/tokenizer.json)");
|
|
69
69
|
writeStdout("");
|
|
70
|
+
section("Self-hosted (AWS SageMaker)");
|
|
71
|
+
writeStdout(" MIRU_SAGEMAKER_ENDPOINT_ARN");
|
|
72
|
+
writeStdout(" arn:aws:sagemaker:<region>:<account-id>:endpoint/<name> — set this to");
|
|
73
|
+
writeStdout(" bypass Takara entirely and embed via your own SageMaker endpoint.");
|
|
74
|
+
writeStdout(" MIRU_SAGEMAKER_ENDPOINT_NAME / MIRU_SAGEMAKER_REGION");
|
|
75
|
+
writeStdout(" Alternative to the ARN when you'd rather name the endpoint + region");
|
|
76
|
+
writeStdout(" directly (falls back to AWS_REGION / AWS_DEFAULT_REGION).");
|
|
77
|
+
writeStdout(" MIRU_SAGEMAKER_NORMALIZE / MIRU_SAGEMAKER_TRUNCATE");
|
|
78
|
+
writeStdout(" Default: true");
|
|
79
|
+
writeStdout(" MIRU_SAGEMAKER_TRUNCATION_DIRECTION");
|
|
80
|
+
writeStdout(' "Left" | "Right" (default: Right)');
|
|
81
|
+
writeStdout(" MIRU_SAGEMAKER_PROMPT_NAME");
|
|
82
|
+
writeStdout(" Optional prompt_name passed to the endpoint");
|
|
83
|
+
writeStdout(" AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_SESSION_TOKEN / AWS_PROFILE");
|
|
84
|
+
writeStdout(" Standard AWS credential resolution — nothing Miru-specific to set");
|
|
85
|
+
writeStdout("");
|
|
86
|
+
hint("Enterprise self-hosted setup: docs/self-hosted-sagemaker.md");
|
|
87
|
+
hint("Admin runbook: bun run sagemaker:create-invoke-user -- --endpoint-arn <arn>");
|
|
88
|
+
hint("Then: miru setup --sagemaker --arn <arn> --profile <name>");
|
|
89
|
+
hint("Setup confirms auth, saves SageMaker config, and removes any stored Takara API key.");
|
|
90
|
+
writeStdout("");
|
|
70
91
|
}
|
|
71
92
|
|
|
72
93
|
export function printFullHelp(): void {
|
|
@@ -126,13 +147,25 @@ export function printCommandHelp(command: string): void {
|
|
|
126
147
|
writeStdout("");
|
|
127
148
|
return;
|
|
128
149
|
case "setup":
|
|
129
|
-
commandHeader("setup", "Store and validate
|
|
150
|
+
commandHeader("setup", "Store and validate Takara or self-hosted SageMaker credentials.");
|
|
130
151
|
section("Usage");
|
|
131
152
|
writeStdout(" miru setup [--key TOKEN] [--force] [--clear]");
|
|
132
|
-
|
|
153
|
+
writeStdout(" miru setup --sagemaker --arn ENDPOINT_ARN --profile NAME");
|
|
154
|
+
section("Options (Takara)");
|
|
133
155
|
writeStdout(" --key, -k TOKEN Non-interactive key entry");
|
|
134
156
|
writeStdout(" --force Replace an existing stored key");
|
|
135
157
|
writeStdout(" --clear Remove stored credentials");
|
|
158
|
+
section("Options (SageMaker)");
|
|
159
|
+
writeStdout(" --sagemaker Switch setup to self-hosted SageMaker mode");
|
|
160
|
+
writeStdout(" --arn ARN Endpoint ARN (implies --sagemaker)");
|
|
161
|
+
writeStdout(" --profile NAME AWS profile to inherit credentials from");
|
|
162
|
+
writeStdout("");
|
|
163
|
+
hint("Enterprise guide: docs/self-hosted-sagemaker.md (Marketplace + invoke-user runbook).");
|
|
164
|
+
hint("Takara and SageMaker are mutually exclusive — setup for one removes the other.");
|
|
165
|
+
hint("SageMaker setup invokes the endpoint once (auth + embedding check), then saves.");
|
|
166
|
+
hint("Miru only inherits AWS credentials — it never creates IAM users or writes ~/.aws.");
|
|
167
|
+
hint("Admin runbook (repo): bun run sagemaker:create-invoke-user -- --endpoint-arn <arn>");
|
|
168
|
+
hint("Then: miru setup --sagemaker --arn <arn> --profile miru");
|
|
136
169
|
writeStdout("");
|
|
137
170
|
return;
|
|
138
171
|
case "install":
|
package/src/index/bm25.ts
CHANGED
|
@@ -47,37 +47,46 @@ export class BM25Index {
|
|
|
47
47
|
private postings = new Map<string, PostingsList>();
|
|
48
48
|
private avgDocLength = 0;
|
|
49
49
|
private numDocs = 0;
|
|
50
|
+
private totalLen = 0;
|
|
51
|
+
|
|
52
|
+
/** Append one tokenized document. Safe to call incrementally while indexing. */
|
|
53
|
+
addDocument(doc: string[]): number {
|
|
54
|
+
const docIndex = this.numDocs;
|
|
55
|
+
this.numDocs += 1;
|
|
56
|
+
this.docLengths.push(doc.length);
|
|
57
|
+
this.totalLen += doc.length;
|
|
58
|
+
this.avgDocLength = this.totalLen / this.numDocs;
|
|
59
|
+
|
|
60
|
+
const tf = new Map<string, number>();
|
|
61
|
+
for (const term of doc) {
|
|
62
|
+
tf.set(term, (tf.get(term) ?? 0) + 1);
|
|
63
|
+
}
|
|
64
|
+
for (const [term, count] of tf) {
|
|
65
|
+
this.docFreq.set(term, (this.docFreq.get(term) ?? 0) + 1);
|
|
66
|
+
let list = this.postings.get(term);
|
|
67
|
+
if (!list) {
|
|
68
|
+
list = [];
|
|
69
|
+
this.postings.set(term, list);
|
|
70
|
+
}
|
|
71
|
+
list.push([docIndex, count]);
|
|
72
|
+
}
|
|
73
|
+
return docIndex;
|
|
74
|
+
}
|
|
50
75
|
|
|
51
76
|
index(tokenizedDocs: string[][]): void {
|
|
52
|
-
this.numDocs =
|
|
77
|
+
this.numDocs = 0;
|
|
53
78
|
this.docFreq.clear();
|
|
54
79
|
this.docLengths = [];
|
|
55
80
|
this.postings.clear();
|
|
81
|
+
this.totalLen = 0;
|
|
82
|
+
this.avgDocLength = 0;
|
|
56
83
|
|
|
57
|
-
|
|
58
|
-
for (let docIndex = 0; docIndex < tokenizedDocs.length; docIndex++) {
|
|
59
|
-
const doc = tokenizedDocs[docIndex];
|
|
84
|
+
for (const doc of tokenizedDocs) {
|
|
60
85
|
if (!doc) {
|
|
61
86
|
continue;
|
|
62
87
|
}
|
|
63
|
-
this.
|
|
64
|
-
totalLen += doc.length;
|
|
65
|
-
|
|
66
|
-
const tf = new Map<string, number>();
|
|
67
|
-
for (const term of doc) {
|
|
68
|
-
tf.set(term, (tf.get(term) ?? 0) + 1);
|
|
69
|
-
}
|
|
70
|
-
for (const [term, count] of tf) {
|
|
71
|
-
this.docFreq.set(term, (this.docFreq.get(term) ?? 0) + 1);
|
|
72
|
-
let list = this.postings.get(term);
|
|
73
|
-
if (!list) {
|
|
74
|
-
list = [];
|
|
75
|
-
this.postings.set(term, list);
|
|
76
|
-
}
|
|
77
|
-
list.push([docIndex, count]);
|
|
78
|
-
}
|
|
88
|
+
this.addDocument(doc);
|
|
79
89
|
}
|
|
80
|
-
this.avgDocLength = this.numDocs > 0 ? totalLen / this.numDocs : 0;
|
|
81
90
|
}
|
|
82
91
|
|
|
83
92
|
private queryTerms(queryTokens: string[]): { term: string; idf: number }[] {
|
|
@@ -222,6 +231,7 @@ export class BM25Index {
|
|
|
222
231
|
idx.docLengths = data.docLengths;
|
|
223
232
|
idx.avgDocLength = data.avgDocLength;
|
|
224
233
|
idx.numDocs = data.numDocs;
|
|
234
|
+
idx.totalLen = data.avgDocLength * data.numDocs;
|
|
225
235
|
|
|
226
236
|
for (const [term, list] of Object.entries(data.postings)) {
|
|
227
237
|
idx.postings.set(term, list);
|
package/src/index/create.ts
CHANGED
|
@@ -2,24 +2,33 @@ import { relative, resolve } from "node:path";
|
|
|
2
2
|
import { chunkSource } from "../chunking/chunking.ts";
|
|
3
3
|
import { ensureParserInit } from "../chunking/grammars.ts";
|
|
4
4
|
import { resolveWorkerConcurrency } from "../concurrency.ts";
|
|
5
|
-
import
|
|
5
|
+
import {
|
|
6
|
+
appendEmbeddingWindowJobs,
|
|
7
|
+
assignEmbeddingWindowVectors,
|
|
8
|
+
type EmbeddingBackend,
|
|
9
|
+
type EmbeddingWindowJob,
|
|
10
|
+
embedTextsWithBackend,
|
|
11
|
+
poolEmbeddingWindowBuckets,
|
|
12
|
+
resolveEmbeddingBatchSize,
|
|
13
|
+
resolveMaxEmbedChars,
|
|
14
|
+
} from "../embeddings/openai.ts";
|
|
6
15
|
import { envOptionalInt } from "../env.ts";
|
|
7
16
|
import { searchImprovementsEnabled } from "../ranking/features.ts";
|
|
8
|
-
import { tokenize } from "../tokens.ts";
|
|
9
17
|
import { type Chunk, type ContentType, defaultContentTypes } from "../types.ts";
|
|
10
18
|
import { BM25Index } from "./bm25.ts";
|
|
11
19
|
import { loadRootEntryChunks } from "./entry-chunks.ts";
|
|
12
20
|
import { walkFiles } from "./file-walker.ts";
|
|
13
21
|
import { detectLanguage, getExtensions, getFileStatus, readFileText } from "./files.ts";
|
|
14
22
|
import type { SemanticIndex } from "./semantic-index.ts";
|
|
15
|
-
import {
|
|
23
|
+
import { addChunkToBm25 } from "./sparse.ts";
|
|
16
24
|
import { buildSemanticIndex } from "./vector-storage.ts";
|
|
17
25
|
|
|
18
|
-
|
|
19
|
-
const DEFAULT_PIPELINE_EMBED_INFLIGHT =
|
|
26
|
+
/** Keep enough embed HTTP calls in flight to saturate the serverless endpoint. */
|
|
27
|
+
const DEFAULT_PIPELINE_EMBED_INFLIGHT = 16;
|
|
20
28
|
|
|
29
|
+
/** One knob: same as embedding API batch size unless explicitly overridden. */
|
|
21
30
|
function resolvePipelineEmbedBatch(): number {
|
|
22
|
-
return envOptionalInt(["MIRU_PIPELINE_EMBED_BATCH"], 1) ??
|
|
31
|
+
return envOptionalInt(["MIRU_PIPELINE_EMBED_BATCH"], 1) ?? resolveEmbeddingBatchSize();
|
|
23
32
|
}
|
|
24
33
|
|
|
25
34
|
function resolvePipelineEmbedInflight(): number {
|
|
@@ -40,6 +49,7 @@ export async function createIndexFromPath(
|
|
|
40
49
|
const started = performance.now();
|
|
41
50
|
let fileProcessMs = 0;
|
|
42
51
|
let embedBackpressureWaitMs = 0;
|
|
52
|
+
let bm25BuildMs = 0;
|
|
43
53
|
let fileEnumerated = 0;
|
|
44
54
|
let fileTasks = 0;
|
|
45
55
|
let emptyFileTasks = 0;
|
|
@@ -57,14 +67,16 @@ export async function createIndexFromPath(
|
|
|
57
67
|
const fileConcurrency = resolveWorkerConcurrency();
|
|
58
68
|
const embedBatchSize = resolvePipelineEmbedBatch();
|
|
59
69
|
const maxEmbedInflight = resolvePipelineEmbedInflight();
|
|
70
|
+
const maxEmbedChars = resolveMaxEmbedChars();
|
|
60
71
|
const maxIndexFiles = resolveMaxIndexFiles();
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
const embedInFlight = new Set<Promise<
|
|
65
|
-
const embedPromises: Promise<
|
|
66
|
-
|
|
67
|
-
let
|
|
72
|
+
const emittedChunks: Chunk[] = [];
|
|
73
|
+
const windowVectorsByChunk: Float32Array[][] = [];
|
|
74
|
+
const pendingWindows: EmbeddingWindowJob[] = [];
|
|
75
|
+
const embedInFlight = new Set<Promise<void>>();
|
|
76
|
+
const embedPromises: Promise<void>[] = [];
|
|
77
|
+
const bm25 = new BM25Index();
|
|
78
|
+
let emittedApiBatches = 0;
|
|
79
|
+
let bm25Cursor = 0;
|
|
68
80
|
|
|
69
81
|
const processFile = async (filePath: string): Promise<Chunk[]> => {
|
|
70
82
|
const t = performance.now();
|
|
@@ -86,35 +98,64 @@ export async function createIndexFromPath(
|
|
|
86
98
|
}
|
|
87
99
|
};
|
|
88
100
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
101
|
+
/** Burn idle time tokenizing + building BM25 while embeds are in flight. */
|
|
102
|
+
const progressBm25 = (budgetMs = 8): void => {
|
|
103
|
+
const t0 = performance.now();
|
|
104
|
+
while (bm25Cursor < emittedChunks.length && performance.now() - t0 < budgetMs) {
|
|
105
|
+
const chunk = emittedChunks[bm25Cursor];
|
|
106
|
+
if (!chunk) {
|
|
107
|
+
break;
|
|
95
108
|
}
|
|
96
|
-
|
|
109
|
+
const tDoc = performance.now();
|
|
110
|
+
addChunkToBm25(bm25, chunk);
|
|
111
|
+
bm25BuildMs += performance.now() - tDoc;
|
|
112
|
+
bm25Cursor++;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const enqueueChunks = (chunks: Chunk[]): void => {
|
|
117
|
+
for (const chunk of chunks) {
|
|
118
|
+
const chunkIndex = emittedChunks.length;
|
|
119
|
+
emittedChunks.push(chunk);
|
|
120
|
+
windowVectorsByChunk.push([]);
|
|
121
|
+
appendEmbeddingWindowJobs(pendingWindows, chunkIndex, chunk.content, maxEmbedChars);
|
|
97
122
|
}
|
|
98
123
|
};
|
|
99
124
|
|
|
100
125
|
const maybeScheduleEmbed = async (force = false): Promise<void> => {
|
|
101
|
-
while (
|
|
102
|
-
pendingEmbedChunks.length > 0 &&
|
|
103
|
-
(force || pendingEmbedChunks.length >= embedBatchSize)
|
|
104
|
-
) {
|
|
126
|
+
while (pendingWindows.length > 0 && (force || pendingWindows.length >= embedBatchSize)) {
|
|
105
127
|
if (embedInFlight.size >= maxEmbedInflight) {
|
|
106
128
|
const waitStart = performance.now();
|
|
107
|
-
|
|
129
|
+
// Use backpressure stalls to advance BM25 on the main thread.
|
|
130
|
+
while (embedInFlight.size >= maxEmbedInflight) {
|
|
131
|
+
progressBm25(10);
|
|
132
|
+
await Promise.race([Promise.race(embedInFlight), Bun.sleep(0)]);
|
|
133
|
+
}
|
|
108
134
|
embedBackpressureWaitMs += performance.now() - waitStart;
|
|
109
135
|
continue;
|
|
110
136
|
}
|
|
111
137
|
|
|
112
|
-
const take =
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const
|
|
138
|
+
const take = Math.min(embedBatchSize, pendingWindows.length);
|
|
139
|
+
if (!force && take < embedBatchSize) {
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const batch = pendingWindows.splice(0, take);
|
|
144
|
+
emittedApiBatches++;
|
|
145
|
+
const promise = (async () => {
|
|
146
|
+
const vectors = await embedTextsWithBackend(
|
|
147
|
+
embeddings,
|
|
148
|
+
batch.map((job) => job.text),
|
|
149
|
+
);
|
|
150
|
+
if (vectors.length !== batch.length) {
|
|
151
|
+
throw new Error(
|
|
152
|
+
`Embedding API returned ${vectors.length} vectors for ${batch.length} inputs`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
assignEmbeddingWindowVectors(batch, vectors, windowVectorsByChunk);
|
|
156
|
+
// After each batch returns, spend a little time on BM25.
|
|
157
|
+
progressBm25(5);
|
|
158
|
+
})();
|
|
118
159
|
embedPromises.push(promise);
|
|
119
160
|
embedInFlight.add(promise);
|
|
120
161
|
promise.finally(() => {
|
|
@@ -125,11 +166,13 @@ export async function createIndexFromPath(
|
|
|
125
166
|
|
|
126
167
|
const runningFiles = new Set<Promise<void>>();
|
|
127
168
|
|
|
128
|
-
const startFileTask = (filePath: string
|
|
169
|
+
const startFileTask = (filePath: string): void => {
|
|
129
170
|
const task = (async () => {
|
|
130
171
|
const chunks = await processFile(filePath);
|
|
131
|
-
|
|
132
|
-
|
|
172
|
+
// Enqueue as soon as the file is ready (no sequential barrier) so the
|
|
173
|
+
// embed pipeline fills immediately under concurrency.
|
|
174
|
+
enqueueChunks(chunks);
|
|
175
|
+
progressBm25(3);
|
|
133
176
|
await maybeScheduleEmbed(false);
|
|
134
177
|
})().finally(() => {
|
|
135
178
|
runningFiles.delete(task);
|
|
@@ -143,50 +186,62 @@ export async function createIndexFromPath(
|
|
|
143
186
|
throw new Error(`Index file budget exceeded: max ${maxIndexFiles} files per operation`);
|
|
144
187
|
}
|
|
145
188
|
while (runningFiles.size >= fileConcurrency) {
|
|
189
|
+
progressBm25(5);
|
|
146
190
|
await Promise.race(runningFiles);
|
|
147
191
|
}
|
|
148
192
|
fileTasks++;
|
|
149
|
-
startFileTask(filePath
|
|
193
|
+
startFileTask(filePath);
|
|
150
194
|
}
|
|
151
195
|
|
|
152
196
|
while (runningFiles.size > 0) {
|
|
197
|
+
progressBm25(5);
|
|
153
198
|
await Promise.race(runningFiles);
|
|
154
199
|
}
|
|
155
200
|
|
|
156
201
|
if (searchImprovementsEnabled()) {
|
|
157
202
|
const entryChunks = await loadRootEntryChunks(resolved, displayRoot ? root : undefined);
|
|
158
203
|
if (entryChunks.length > 0) {
|
|
159
|
-
|
|
160
|
-
flushReadySequentialChunks();
|
|
204
|
+
enqueueChunks(entryChunks);
|
|
161
205
|
}
|
|
162
206
|
}
|
|
163
207
|
|
|
164
|
-
|
|
208
|
+
const embedsDrainedAt = performance.now();
|
|
165
209
|
await maybeScheduleEmbed(true);
|
|
166
210
|
await Promise.all(embedPromises);
|
|
211
|
+
// Finish any BM25 docs not yet processed during idle gaps.
|
|
212
|
+
while (bm25Cursor < emittedChunks.length) {
|
|
213
|
+
progressBm25(50);
|
|
214
|
+
}
|
|
215
|
+
const embedsDoneAt = performance.now();
|
|
167
216
|
|
|
168
|
-
const chunks =
|
|
217
|
+
const chunks = emittedChunks;
|
|
169
218
|
|
|
170
219
|
if (chunks.length === 0) {
|
|
171
220
|
throw new Error(`No supported files found under ${path}.`);
|
|
172
221
|
}
|
|
173
222
|
|
|
174
|
-
const
|
|
175
|
-
const
|
|
176
|
-
|
|
223
|
+
const poolStarted = performance.now();
|
|
224
|
+
const vectors = poolEmbeddingWindowBuckets(windowVectorsByChunk);
|
|
225
|
+
const poolMs = performance.now() - poolStarted;
|
|
226
|
+
|
|
227
|
+
const semanticStarted = performance.now();
|
|
177
228
|
const semantic = buildSemanticIndex(vectors);
|
|
229
|
+
const semanticMs = performance.now() - semanticStarted;
|
|
178
230
|
|
|
179
231
|
if (profile) {
|
|
180
232
|
const finished = performance.now();
|
|
233
|
+
const wall = finished - started;
|
|
181
234
|
const embedStats =
|
|
182
235
|
"getStats" in embeddings && typeof embeddings.getStats === "function"
|
|
183
236
|
? embeddings.getStats()
|
|
184
237
|
: null;
|
|
238
|
+
const pipelineWallMs = embedsDrainedAt - started;
|
|
239
|
+
const embedDrainMs = embedsDoneAt - embedsDrainedAt;
|
|
185
240
|
console.error(
|
|
186
241
|
JSON.stringify({
|
|
187
242
|
profile: "index_build",
|
|
188
243
|
path: resolved,
|
|
189
|
-
elapsed_ms:
|
|
244
|
+
elapsed_ms: wall,
|
|
190
245
|
file_enumerated: fileEnumerated,
|
|
191
246
|
file_tasks: fileTasks,
|
|
192
247
|
empty_or_skipped_files: emptyFileTasks,
|
|
@@ -197,11 +252,26 @@ export async function createIndexFromPath(
|
|
|
197
252
|
file_concurrency: fileConcurrency,
|
|
198
253
|
embed_batch_size: embedBatchSize,
|
|
199
254
|
embed_inflight: maxEmbedInflight,
|
|
200
|
-
emitted_batches:
|
|
255
|
+
emitted_batches: emittedApiBatches,
|
|
256
|
+
max_embed_chars: maxEmbedChars,
|
|
201
257
|
},
|
|
202
258
|
stage_ms: {
|
|
203
259
|
file_process_total: fileProcessMs,
|
|
204
260
|
embed_backpressure_wait: embedBackpressureWaitMs,
|
|
261
|
+
bm25_build_cpu: bm25BuildMs,
|
|
262
|
+
wall_pipeline: pipelineWallMs,
|
|
263
|
+
wall_embed_drain: embedDrainMs,
|
|
264
|
+
wall_pool_windows: poolMs,
|
|
265
|
+
wall_bm25: 0,
|
|
266
|
+
wall_semantic: semanticMs,
|
|
267
|
+
wall_post: poolMs + semanticMs,
|
|
268
|
+
},
|
|
269
|
+
wall_pct: {
|
|
270
|
+
pipeline_overlap: +((100 * pipelineWallMs) / wall).toFixed(1),
|
|
271
|
+
embed_drain: +((100 * embedDrainMs) / wall).toFixed(1),
|
|
272
|
+
pool_windows: +((100 * poolMs) / wall).toFixed(1),
|
|
273
|
+
bm25: 0,
|
|
274
|
+
semantic: +((100 * semanticMs) / wall).toFixed(1),
|
|
205
275
|
},
|
|
206
276
|
embedding_transport: embedStats,
|
|
207
277
|
}),
|
package/src/index/incremental.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
2
2
|
import { chunkSource } from "../chunking/chunking.ts";
|
|
3
3
|
import type { EmbeddingBackend } from "../embeddings/openai.ts";
|
|
4
|
-
import { tokenize } from "../tokens.ts";
|
|
5
4
|
import type { Chunk, ContentType } from "../types.ts";
|
|
6
|
-
import { BM25Index } from "./bm25.ts";
|
|
5
|
+
import type { BM25Index } from "./bm25.ts";
|
|
7
6
|
import { detectLanguage, getExtensions, getFileStatus, readFileText } from "./files.ts";
|
|
8
7
|
import type { SemanticIndex } from "./semantic-index.ts";
|
|
9
|
-
import {
|
|
8
|
+
import { buildBm25FromChunks } from "./sparse.ts";
|
|
10
9
|
import { buildSemanticIndex } from "./vector-storage.ts";
|
|
11
10
|
import { vectorAt } from "./vectors.ts";
|
|
12
11
|
|
|
@@ -64,7 +63,7 @@ export async function applyIncrementalFileChanges(options: {
|
|
|
64
63
|
if (targets.size === 0) {
|
|
65
64
|
return {
|
|
66
65
|
chunks: options.chunks,
|
|
67
|
-
bm25:
|
|
66
|
+
bm25: buildBm25FromChunks(options.chunks),
|
|
68
67
|
semantic: options.semanticIndex,
|
|
69
68
|
};
|
|
70
69
|
}
|
|
@@ -108,17 +107,11 @@ export async function applyIncrementalFileChanges(options: {
|
|
|
108
107
|
|
|
109
108
|
return {
|
|
110
109
|
chunks,
|
|
111
|
-
bm25:
|
|
110
|
+
bm25: buildBm25FromChunks(chunks),
|
|
112
111
|
semantic: buildSemanticIndex(vectors),
|
|
113
112
|
};
|
|
114
113
|
}
|
|
115
114
|
|
|
116
|
-
function rebuildBm25(chunks: Chunk[]): BM25Index {
|
|
117
|
-
const bm25 = new BM25Index();
|
|
118
|
-
bm25.index(chunks.map((c) => tokenize(enrichForBm25(c))));
|
|
119
|
-
return bm25;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
115
|
/** Map an absolute changed path to a repo-relative path for chunk keys. */
|
|
123
116
|
export function relativePathFromRoot(root: string, absoluteOrRelative: string): string {
|
|
124
117
|
const resolvedRoot = resolve(root);
|
package/src/index/sparse.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { tokenize } from "../tokens.ts";
|
|
1
2
|
import type { Chunk } from "../types.ts";
|
|
3
|
+
import { BM25Index } from "./bm25.ts";
|
|
2
4
|
|
|
3
5
|
export function selectorToMask(
|
|
4
6
|
selector: readonly number[] | null | undefined,
|
|
@@ -23,3 +25,15 @@ export function enrichForBm25(chunk: Chunk): string {
|
|
|
23
25
|
const dirText = dirParts.slice(-3).join(" ");
|
|
24
26
|
return `${chunk.content} ${stem} ${stem} ${dirText}`;
|
|
25
27
|
}
|
|
28
|
+
|
|
29
|
+
export function addChunkToBm25(bm25: BM25Index, chunk: Chunk): void {
|
|
30
|
+
bm25.addDocument(tokenize(enrichForBm25(chunk)));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function buildBm25FromChunks(chunks: readonly Chunk[]): BM25Index {
|
|
34
|
+
const bm25 = new BM25Index();
|
|
35
|
+
for (const chunk of chunks) {
|
|
36
|
+
addChunkToBm25(bm25, chunk);
|
|
37
|
+
}
|
|
38
|
+
return bm25;
|
|
39
|
+
}
|
package/src/literal.ts
CHANGED
package/src/prompt.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { stdin as input, stdout as output } from "node:process";
|
|
2
|
+
import * as readline from "node:readline/promises";
|
|
2
3
|
|
|
3
4
|
export interface HiddenPromptState {
|
|
4
5
|
value: string;
|
|
@@ -60,6 +61,18 @@ export function applyHiddenPromptChar(
|
|
|
60
61
|
};
|
|
61
62
|
}
|
|
62
63
|
|
|
64
|
+
/** Visible-input prompt (not for secrets — use promptHidden for those). */
|
|
65
|
+
export async function promptText(message: string, defaultValue = ""): Promise<string> {
|
|
66
|
+
const rl = readline.createInterface({ input, output });
|
|
67
|
+
try {
|
|
68
|
+
const suffix = defaultValue ? ` (${defaultValue})` : "";
|
|
69
|
+
const answer = (await rl.question(`${message}${suffix}: `)).trim();
|
|
70
|
+
return answer || defaultValue;
|
|
71
|
+
} finally {
|
|
72
|
+
rl.close();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
63
76
|
export async function promptHidden(message: string): Promise<string> {
|
|
64
77
|
if (!input.isTTY) {
|
|
65
78
|
throw new Error(
|