@zenera/rag 1.1.9 → 1.1.11
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 +195 -10
- package/dist/command.js +2 -1
- package/dist/common/cache.d.ts +66 -0
- package/dist/common/cache.js +172 -0
- package/dist/common/embedder.d.ts +11 -1
- package/dist/common/embedder.js +6 -17
- package/dist/common/manifest.d.ts +7 -1
- package/dist/common/progress.d.ts +16 -1
- package/dist/common/progress.js +35 -3
- package/dist/common/prose.d.ts +7 -0
- package/dist/common/prose.js +13 -0
- package/dist/docs/assemble.d.ts +52 -0
- package/dist/docs/assemble.js +127 -0
- package/dist/docs/build.d.ts +54 -0
- package/dist/docs/build.js +134 -0
- package/dist/docs/chunk.d.ts +73 -0
- package/dist/docs/chunk.js +586 -0
- package/dist/docs/command.d.ts +3 -0
- package/dist/docs/command.js +575 -0
- package/dist/docs/files.d.ts +94 -0
- package/dist/docs/files.js +80 -0
- package/dist/docs/index.d.ts +13 -0
- package/dist/docs/index.js +13 -0
- package/dist/docs/load.d.ts +42 -0
- package/dist/docs/load.js +203 -0
- package/dist/docs/lookup.d.ts +80 -0
- package/dist/docs/lookup.js +147 -0
- package/dist/docs/outline.d.ts +11 -0
- package/dist/docs/outline.js +55 -0
- package/dist/docs/parse-cache.d.ts +20 -0
- package/dist/docs/parse-cache.js +60 -0
- package/dist/docs/parse-worker.d.ts +13 -0
- package/dist/docs/parse-worker.js +21 -0
- package/dist/docs/parse.d.ts +95 -0
- package/dist/docs/parse.js +372 -0
- package/dist/docs/pool.d.ts +27 -0
- package/dist/docs/pool.js +133 -0
- package/dist/docs/readme.d.ts +6 -0
- package/dist/docs/readme.js +142 -0
- package/dist/docs/render.d.ts +13 -0
- package/dist/docs/render.js +46 -0
- package/dist/docs/repl.d.ts +7 -0
- package/dist/docs/repl.js +130 -0
- package/dist/docs/search.d.ts +92 -0
- package/dist/docs/search.js +251 -0
- package/dist/docs/store.d.ts +79 -0
- package/dist/docs/store.js +214 -0
- package/dist/docs/tools.d.ts +10 -0
- package/dist/docs/tools.js +300 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -0
- package/dist/schema/build.d.ts +16 -2
- package/dist/schema/build.js +37 -25
- package/dist/schema/command.js +39 -10
- package/dist/schema/query.js +1 -0
- package/dist/schema/readme.js +6 -2
- package/dist/schema/search.d.ts +2 -0
- package/dist/schema/search.js +18 -2
- package/dist/schema/store.d.ts +13 -3
- package/dist/schema/store.js +73 -24
- package/dist/schema/tools.js +21 -2
- package/package.json +17 -4
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { tool } from '@zenera/neo';
|
|
2
|
+
import { PatternError } from "../common/match.js";
|
|
3
|
+
import { assemble } from "./assemble.js";
|
|
4
|
+
import { CHUNK_KINDS } from "./chunk.js";
|
|
5
|
+
import { grepLines, listFiles, listSections, listTables, readRange, readSection, } from "./lookup.js";
|
|
6
|
+
import { renderAssembly } from "./render.js";
|
|
7
|
+
import { SEARCH_MODES } from "./search.js";
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// The same index, given to an agent
|
|
10
|
+
//
|
|
11
|
+
// Four tools over one engine, and only one of them ranks anything. `search_docs`
|
|
12
|
+
// is the way in when the question is vague; the other three are exact, because
|
|
13
|
+
// a model told "no results" by a vector search has learned nothing — a ranking
|
|
14
|
+
// returns the top of a list, so an empty answer and an absent thing look
|
|
15
|
+
// identical.
|
|
16
|
+
//
|
|
17
|
+
// `search_docs` is shaped for the second call rather than the first. The first
|
|
18
|
+
// is always a sentence and always returns too much of the wrong tree; the
|
|
19
|
+
// second is the same sentence with `files: ["nsx_4.2*/api/**"]`, or
|
|
20
|
+
// `section: "Rate limits"`, or `kind: ["table"]` because the answer is a table
|
|
21
|
+
// and not the prose around it. Those are parameters and not separate tools, so
|
|
22
|
+
// narrowing costs one call and not three.
|
|
23
|
+
//
|
|
24
|
+
// Every answer carries line numbers, and `read_docs` takes them. That is the
|
|
25
|
+
// loop the whole thing exists for: find the passage, read around it, then edit
|
|
26
|
+
// the file the passage came from — and a passage that cannot be pointed at is a
|
|
27
|
+
// passage nothing can be done with.
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
const GROUP = 'docs';
|
|
30
|
+
/** Kept small on purpose: a tool result is prompt, and the model asked for one thing. */
|
|
31
|
+
const DEFAULT_LIMIT = 5;
|
|
32
|
+
const MAX_LIMIT = 25;
|
|
33
|
+
/** A listing is lines rather than passages, so it can afford more of them. */
|
|
34
|
+
const DEFAULT_ROWS = 50;
|
|
35
|
+
const MAX_ROWS = 200;
|
|
36
|
+
/** A ceiling on what one answer may quote, so one long section cannot eat it. */
|
|
37
|
+
const DEFAULT_MAX_LINES = 200;
|
|
38
|
+
const MAX_LINES = 800;
|
|
39
|
+
export function docsTools(index, options = {}) {
|
|
40
|
+
const limitOf = (asked) => clamp(asked ?? options.limit ?? DEFAULT_LIMIT, 1, MAX_LIMIT);
|
|
41
|
+
const linesOf = (asked) => clamp(asked ?? options.maxLines ?? DEFAULT_MAX_LINES, 20, MAX_LINES);
|
|
42
|
+
const searchDocs = tool({
|
|
43
|
+
name: 'search_docs',
|
|
44
|
+
group: GROUP,
|
|
45
|
+
description: 'Searches the documents and answers with the passages that matched, quoted ' +
|
|
46
|
+
'verbatim with their line numbers and with a marker wherever something between ' +
|
|
47
|
+
'two passages was left out. When the first answer is from the wrong part of the ' +
|
|
48
|
+
'corpus, ask again with the same query and a narrowing: `files` for a path ' +
|
|
49
|
+
'pattern, `section` for a heading, `kind` for tables or code only. Pass the ids ' +
|
|
50
|
+
'from an earlier answer in exclude_ids to be shown something new instead.',
|
|
51
|
+
parameters: {
|
|
52
|
+
type: 'object',
|
|
53
|
+
properties: {
|
|
54
|
+
query: {
|
|
55
|
+
type: 'string',
|
|
56
|
+
description: 'What is wanted, as a sentence rather than keywords.',
|
|
57
|
+
},
|
|
58
|
+
files: strings('Only documents whose name matches. A glob if it has * or ?, e.g. ' +
|
|
59
|
+
'"guides/**" or "nsx_4.2*/api/**"; otherwise a substring.'),
|
|
60
|
+
exclude_files: strings('Documents to leave out, matched the same way.'),
|
|
61
|
+
section: strings('Only under these headings, and whatever nests inside them. A heading ' +
|
|
62
|
+
'title, or a structure path from an earlier answer.'),
|
|
63
|
+
kind: {
|
|
64
|
+
type: 'array',
|
|
65
|
+
items: { type: 'string', enum: [...CHUNK_KINDS] },
|
|
66
|
+
description: 'Only these kinds of block. Use ["table"] or ["table_row"] when the ' +
|
|
67
|
+
'answer is tabular, ["code"] for examples.',
|
|
68
|
+
},
|
|
69
|
+
mode: {
|
|
70
|
+
type: 'string',
|
|
71
|
+
enum: [...SEARCH_MODES],
|
|
72
|
+
description: 'hybrid blends meaning and wording; text is exact wording only, for ' +
|
|
73
|
+
'an error string or an identifier.',
|
|
74
|
+
},
|
|
75
|
+
exclude_ids: strings('Passage ids already seen, as printed in an earlier answer.'),
|
|
76
|
+
limit: {
|
|
77
|
+
type: 'integer',
|
|
78
|
+
description: `Passages kept. Default ${DEFAULT_LIMIT}, at most ${MAX_LIMIT}.`,
|
|
79
|
+
},
|
|
80
|
+
before: { type: 'integer', description: 'Extra lines quoted before each passage.' },
|
|
81
|
+
after: { type: 'integer', description: 'Extra lines quoted after each passage.' },
|
|
82
|
+
max_lines: {
|
|
83
|
+
type: 'integer',
|
|
84
|
+
description: `A ceiling on the whole answer. Default ${DEFAULT_MAX_LINES}.`,
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
required: ['query'],
|
|
88
|
+
additionalProperties: false,
|
|
89
|
+
},
|
|
90
|
+
execute: async (args) => {
|
|
91
|
+
const query = {
|
|
92
|
+
query: args.query,
|
|
93
|
+
files: args.files,
|
|
94
|
+
exclude_files: args.exclude_files,
|
|
95
|
+
section: args.section,
|
|
96
|
+
kinds: args.kind,
|
|
97
|
+
mode: args.mode,
|
|
98
|
+
exclude_ids: args.exclude_ids,
|
|
99
|
+
limit: limitOf(args.limit),
|
|
100
|
+
};
|
|
101
|
+
const result = await guard(() => index.search(query));
|
|
102
|
+
if ('error' in result) {
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
if (result.files.length === 0) {
|
|
106
|
+
return {
|
|
107
|
+
found: 0,
|
|
108
|
+
hint: 'no document matched `files` — call list_docs to see their names',
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
if (result.matches.length === 0) {
|
|
112
|
+
return {
|
|
113
|
+
found: 0,
|
|
114
|
+
scope: { documents: result.files.length, sections: result.sections.length },
|
|
115
|
+
hint: args.section?.length
|
|
116
|
+
? 'nothing under that section — drop `section` and search the whole document'
|
|
117
|
+
: 'try fewer words, or mode "text" if it is an exact string',
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const excerpt = await assemble(index, result.matches, {
|
|
121
|
+
before: args.before,
|
|
122
|
+
after: args.after,
|
|
123
|
+
maxLines: linesOf(args.max_lines),
|
|
124
|
+
});
|
|
125
|
+
return {
|
|
126
|
+
found: result.matches.length,
|
|
127
|
+
ids: result.matches.map((m) => m.id),
|
|
128
|
+
documents: excerpt.files.map((f) => f.path),
|
|
129
|
+
truncated: excerpt.truncated,
|
|
130
|
+
passages: renderAssembly(excerpt, { colour: false }),
|
|
131
|
+
};
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
const listDocs = tool({
|
|
135
|
+
name: 'list_docs',
|
|
136
|
+
group: GROUP,
|
|
137
|
+
description: 'Lists what is in the index — the documents, their headings, or their tables — ' +
|
|
138
|
+
'without searching or ranking anything. Call it first to learn the document ' +
|
|
139
|
+
"names that `files` patterns are matched against, or to see a document's " +
|
|
140
|
+
'structure before asking about one part of it.',
|
|
141
|
+
parameters: {
|
|
142
|
+
type: 'object',
|
|
143
|
+
properties: {
|
|
144
|
+
what: {
|
|
145
|
+
type: 'string',
|
|
146
|
+
enum: ['files', 'sections', 'tables'],
|
|
147
|
+
description: 'Default files.',
|
|
148
|
+
},
|
|
149
|
+
files: strings('Only these documents, matched by glob or substring.'),
|
|
150
|
+
section: strings('Only under these headings.'),
|
|
151
|
+
depth: {
|
|
152
|
+
type: 'integer',
|
|
153
|
+
description: 'sections only: the deepest heading level to report.',
|
|
154
|
+
},
|
|
155
|
+
limit: { type: 'integer', description: `Rows kept. Default ${DEFAULT_ROWS}.` },
|
|
156
|
+
},
|
|
157
|
+
additionalProperties: false,
|
|
158
|
+
},
|
|
159
|
+
execute: async (args) => {
|
|
160
|
+
const options = {
|
|
161
|
+
files: args.files,
|
|
162
|
+
section: args.section,
|
|
163
|
+
depth: args.depth,
|
|
164
|
+
limit: clamp(args.limit ?? DEFAULT_ROWS, 1, MAX_ROWS),
|
|
165
|
+
};
|
|
166
|
+
const what = args.what ?? 'files';
|
|
167
|
+
const result = await guard(() => what === 'sections'
|
|
168
|
+
? listSections(index, options)
|
|
169
|
+
: what === 'tables'
|
|
170
|
+
? listTables(index, options)
|
|
171
|
+
: listFiles(index, options));
|
|
172
|
+
if ('error' in result) {
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
found: result.found,
|
|
177
|
+
truncated: result.truncated,
|
|
178
|
+
[what]: result.rows,
|
|
179
|
+
...(result.found === 0
|
|
180
|
+
? { hint: 'nothing matched — widen `files`, or call it with no arguments' }
|
|
181
|
+
: {}),
|
|
182
|
+
};
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
const grepDocs = tool({
|
|
186
|
+
name: 'grep_docs',
|
|
187
|
+
group: GROUP,
|
|
188
|
+
description: 'Every line matching a pattern, with the document and line number, and the ' +
|
|
189
|
+
'section it sits in. Exact, not ranked, and it reports the true total even when ' +
|
|
190
|
+
'the rows are cut — so unlike search_docs it can answer "does this string appear ' +
|
|
191
|
+
'anywhere". Reach for it with an identifier, an error message, or a flag name.',
|
|
192
|
+
parameters: {
|
|
193
|
+
type: 'object',
|
|
194
|
+
properties: {
|
|
195
|
+
pattern: {
|
|
196
|
+
type: 'string',
|
|
197
|
+
description: 'A substring, a glob if it has * or ?, or a regex with regex.',
|
|
198
|
+
},
|
|
199
|
+
files: strings('Only these documents.'),
|
|
200
|
+
section: strings('Only under these headings.'),
|
|
201
|
+
regex: { type: 'boolean', description: 'Read the pattern as a regex.' },
|
|
202
|
+
case_sensitive: { type: 'boolean', description: 'Match the capitals too.' },
|
|
203
|
+
limit: { type: 'integer', description: `Lines kept. Default ${DEFAULT_ROWS}.` },
|
|
204
|
+
},
|
|
205
|
+
required: ['pattern'],
|
|
206
|
+
additionalProperties: false,
|
|
207
|
+
},
|
|
208
|
+
execute: async (args) => {
|
|
209
|
+
const result = await guard(() => grepLines(index, args.pattern, {
|
|
210
|
+
files: args.files,
|
|
211
|
+
section: args.section,
|
|
212
|
+
regex: args.regex,
|
|
213
|
+
caseSensitive: args.case_sensitive,
|
|
214
|
+
limit: clamp(args.limit ?? DEFAULT_ROWS, 1, MAX_ROWS),
|
|
215
|
+
}));
|
|
216
|
+
if ('error' in result) {
|
|
217
|
+
return result;
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
found: result.found,
|
|
221
|
+
truncated: result.truncated,
|
|
222
|
+
lines: result.rows,
|
|
223
|
+
...(result.found === 0 ? { hint: 'nothing matched anywhere in scope' } : {}),
|
|
224
|
+
};
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
const readDocs = tool({
|
|
228
|
+
name: 'read_docs',
|
|
229
|
+
group: GROUP,
|
|
230
|
+
description: 'Reads a document verbatim: a whole named section, or a line range as printed by ' +
|
|
231
|
+
'search_docs or grep_docs. Use it when a passage was found and the lines around ' +
|
|
232
|
+
'it are needed in full, with nothing omitted and nothing summarised.',
|
|
233
|
+
parameters: {
|
|
234
|
+
type: 'object',
|
|
235
|
+
properties: {
|
|
236
|
+
file: {
|
|
237
|
+
type: 'string',
|
|
238
|
+
description: 'A document name, exactly as list_docs prints it.',
|
|
239
|
+
},
|
|
240
|
+
section: {
|
|
241
|
+
type: 'string',
|
|
242
|
+
description: 'A heading title or structure path. Overrides from/to.',
|
|
243
|
+
},
|
|
244
|
+
from: { type: 'integer', description: 'First line, 1-based. Default 1.' },
|
|
245
|
+
to: { type: 'integer', description: 'Last line. Default the end of the document.' },
|
|
246
|
+
},
|
|
247
|
+
required: ['file'],
|
|
248
|
+
additionalProperties: false,
|
|
249
|
+
},
|
|
250
|
+
execute: async (args) => {
|
|
251
|
+
const file = index.resolveFiles([args.file])[0];
|
|
252
|
+
if (!file) {
|
|
253
|
+
return {
|
|
254
|
+
error: `no document called ${args.file}`,
|
|
255
|
+
hint: 'call list_docs for the names',
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
const result = await guard(async () => args.section
|
|
259
|
+
? await readSection(index, file, args.section)
|
|
260
|
+
: await readRange(index, file, args.from ?? 1, args.to ?? Infinity));
|
|
261
|
+
if ('error' in result) {
|
|
262
|
+
return result;
|
|
263
|
+
}
|
|
264
|
+
// A whole large document is prompt spent on lines nobody asked for.
|
|
265
|
+
const kept = result.lines.slice(0, MAX_LINES);
|
|
266
|
+
return {
|
|
267
|
+
file: result.file,
|
|
268
|
+
start: result.start,
|
|
269
|
+
end: result.start + kept.length - 1,
|
|
270
|
+
total: result.total,
|
|
271
|
+
truncated: kept.length < result.lines.length,
|
|
272
|
+
text: kept.map((line, at) => `${result.start + at} | ${line}`).join('\n'),
|
|
273
|
+
};
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
return [searchDocs, listDocs, grepDocs, readDocs];
|
|
277
|
+
}
|
|
278
|
+
// ---------------------------------------------------------------------------
|
|
279
|
+
const strings = (description) => ({
|
|
280
|
+
type: 'array',
|
|
281
|
+
items: { type: 'string' },
|
|
282
|
+
description,
|
|
283
|
+
});
|
|
284
|
+
const clamp = (value, low, high) => Math.min(Math.max(Math.trunc(value), low), high);
|
|
285
|
+
/**
|
|
286
|
+
* A bad pattern is a bad argument, not a failure. A model handed a thrown
|
|
287
|
+
* exception retries the same call; one handed a sentence fixes it.
|
|
288
|
+
*/
|
|
289
|
+
async function guard(run) {
|
|
290
|
+
try {
|
|
291
|
+
return await run();
|
|
292
|
+
}
|
|
293
|
+
catch (err) {
|
|
294
|
+
if (err instanceof PatternError) {
|
|
295
|
+
return { error: err.message };
|
|
296
|
+
}
|
|
297
|
+
throw err;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
//# sourceMappingURL=tools.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from './common/locate.ts';
|
|
2
2
|
export * from './common/manifest.ts';
|
|
3
3
|
export * from './common/match.ts';
|
|
4
|
+
export * as docs from './docs/index.ts';
|
|
4
5
|
export * from './schema/build.ts';
|
|
5
6
|
export * from './schema/entities.ts';
|
|
6
7
|
export * from './schema/files.ts';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export * from "./common/locate.js";
|
|
2
2
|
export * from "./common/manifest.js";
|
|
3
3
|
export * from "./common/match.js";
|
|
4
|
+
// A namespace rather than a re-export: both subjects have a `Manifest`, a
|
|
5
|
+
// `writeIndex` and a `search`, and flattening them would collide on every one.
|
|
6
|
+
export * as docs from "./docs/index.js";
|
|
4
7
|
export * from "./schema/build.js";
|
|
5
8
|
export * from "./schema/entities.js";
|
|
6
9
|
export * from "./schema/files.js";
|
package/dist/schema/build.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Embedder } from '@zenera/neo';
|
|
2
|
+
import { type PhaseTiming } from '../common/progress.ts';
|
|
2
3
|
import { type EntityRecord } from './entities.ts';
|
|
3
4
|
import { type Counts, type Manifest, type SourceRecord } from './files.ts';
|
|
4
5
|
export interface BuildOptions {
|
|
@@ -9,10 +10,19 @@ export interface BuildOptions {
|
|
|
9
10
|
embeddingRef?: string;
|
|
10
11
|
/** told the manifest, so a store can say what wrote it */
|
|
11
12
|
indexer: string;
|
|
12
|
-
/** texts sent to the embedder at once */
|
|
13
|
-
batch?: number;
|
|
14
13
|
/** keep a bundled copy of each document in the index. On by default. */
|
|
15
14
|
sources?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* The width asked of the embedder, when one was asked for. Part of the cache
|
|
17
|
+
* key, because a truncated vector is a different vector; left undefined when
|
|
18
|
+
* nobody asked, because that is a different key again from asking for the
|
|
19
|
+
* number the model would have chosen anyway.
|
|
20
|
+
*/
|
|
21
|
+
dimensions?: number;
|
|
22
|
+
/** reuse vectors this machine already has; on by default */
|
|
23
|
+
cache?: boolean;
|
|
24
|
+
/** keep them somewhere other than the shared store */
|
|
25
|
+
cacheDir?: string;
|
|
16
26
|
signal?: AbortSignal;
|
|
17
27
|
/** what the documents turned out to hold, before a vector has been paid for */
|
|
18
28
|
onRead?: (summary: BuildSummary) => void;
|
|
@@ -25,6 +35,10 @@ export interface BuildSummary {
|
|
|
25
35
|
export interface BuildResult {
|
|
26
36
|
manifest: Manifest;
|
|
27
37
|
entities: EntityRecord[];
|
|
38
|
+
/** what each phase cost, so a slow build can say which part was slow */
|
|
39
|
+
timings: readonly PhaseTiming[];
|
|
40
|
+
/** vectors that came out of the shared cache instead of being paid for again */
|
|
41
|
+
reused: number;
|
|
28
42
|
}
|
|
29
43
|
export declare function buildIndex(options: BuildOptions): Promise<BuildResult>;
|
|
30
44
|
//# sourceMappingURL=build.d.ts.map
|
package/dist/schema/build.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
+
import { embedStream, NO_CACHE, openCache } from "../common/cache.js";
|
|
1
2
|
import { beginBuild } from "../common/progress.js";
|
|
2
3
|
import { toEntities } from "./entities.js";
|
|
3
4
|
import { INDEX_VERSION, SOURCES_DIR, writeIndex, } from "./files.js";
|
|
4
5
|
import { buildGraph } from "./graph.js";
|
|
5
6
|
import { PHASES, SCHEMA_REPORT } from "./readme.js";
|
|
6
7
|
import { loadSpecs } from "./spec.js";
|
|
7
|
-
import {
|
|
8
|
-
const DEFAULT_BATCH = 96;
|
|
8
|
+
import { openStore } from "./store.js";
|
|
9
9
|
export async function buildIndex(options) {
|
|
10
10
|
const journal = beginBuild({
|
|
11
11
|
dir: options.out,
|
|
@@ -15,6 +15,15 @@ export async function buildIndex(options) {
|
|
|
15
15
|
phases: PHASES,
|
|
16
16
|
report: SCHEMA_REPORT,
|
|
17
17
|
});
|
|
18
|
+
const ref = options.embeddingRef ?? options.embedder.id;
|
|
19
|
+
const cache = options.cache === false
|
|
20
|
+
? NO_CACHE
|
|
21
|
+
: openCache(options.embedder, {
|
|
22
|
+
ref,
|
|
23
|
+
dir: options.cacheDir,
|
|
24
|
+
dimensions: options.dimensions,
|
|
25
|
+
});
|
|
26
|
+
let writer;
|
|
18
27
|
try {
|
|
19
28
|
const corpus = await loadSpecs(options.files);
|
|
20
29
|
journal.phase('graph');
|
|
@@ -33,18 +42,20 @@ export async function buildIndex(options) {
|
|
|
33
42
|
journal.read(summary.counts, summary.counts.entities);
|
|
34
43
|
options.onRead?.(summary);
|
|
35
44
|
journal.phase('embedding');
|
|
36
|
-
|
|
45
|
+
writer = await openStore(options.out);
|
|
46
|
+
const dimensions = await embedAll(entities, options, journal, cache, writer);
|
|
37
47
|
journal.phase('writing');
|
|
38
|
-
const written = await
|
|
48
|
+
const written = await writer.finish();
|
|
39
49
|
const manifest = {
|
|
40
50
|
version: INDEX_VERSION,
|
|
41
51
|
kind: 'schema',
|
|
42
52
|
createdAt: new Date().toISOString(),
|
|
43
53
|
indexer: options.indexer,
|
|
44
54
|
embedding: {
|
|
45
|
-
ref
|
|
55
|
+
ref,
|
|
46
56
|
id: options.embedder.id,
|
|
47
|
-
dimensions
|
|
57
|
+
dimensions,
|
|
58
|
+
requested: options.dimensions,
|
|
48
59
|
},
|
|
49
60
|
sources: summary.sources,
|
|
50
61
|
counts: summary.counts,
|
|
@@ -57,32 +68,33 @@ export async function buildIndex(options) {
|
|
|
57
68
|
operations: corpus.operations,
|
|
58
69
|
documents: keep ? corpus.documents : {},
|
|
59
70
|
});
|
|
71
|
+
cache.commit();
|
|
60
72
|
journal.finish(manifest);
|
|
61
|
-
return { manifest, entities };
|
|
73
|
+
return { manifest, entities, timings: journal.timings, reused: cache.hits };
|
|
62
74
|
}
|
|
63
75
|
catch (err) {
|
|
76
|
+
writer?.close();
|
|
77
|
+
cache.abandon();
|
|
64
78
|
journal.fail(err);
|
|
65
79
|
throw err;
|
|
66
80
|
}
|
|
67
81
|
}
|
|
68
|
-
async function embedAll(entities, options, journal) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
return out;
|
|
82
|
+
async function embedAll(entities, options, journal, cache, writer) {
|
|
83
|
+
// A window at a time. How many texts fit in a request, and how many
|
|
84
|
+
// requests may be in flight, are still the embedder's to answer — it knows
|
|
85
|
+
// the model's caps and it is the one that sees the 429s.
|
|
86
|
+
return embedStream({
|
|
87
|
+
embedder: options.embedder,
|
|
88
|
+
cache,
|
|
89
|
+
records: entities,
|
|
90
|
+
textOf: (entity) => entity.text,
|
|
91
|
+
signal: options.signal,
|
|
92
|
+
onProgress: (done, total) => {
|
|
93
|
+
journal.progress(done, total);
|
|
94
|
+
options.onProgress?.(done, total);
|
|
95
|
+
},
|
|
96
|
+
onWindow: (window, vectors) => writer.add(window, vectors),
|
|
97
|
+
});
|
|
86
98
|
}
|
|
87
99
|
/**
|
|
88
100
|
* Counted off the entities rather than the corpus, so a schema the document
|
package/dist/schema/command.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { bold, CliError, cyan, dim, EXIT, isInteractive, json, note, parse, table, usageError, write, } from '@zenera/cli/lib';
|
|
1
|
+
import { bold, CliError, cyan, dim, EXIT, isInteractive, json, note, parse, paths, table, usageError, write, } from '@zenera/cli/lib';
|
|
2
2
|
import { relative, resolve } from 'node:path';
|
|
3
3
|
import { resolveEmbedder } from "../common/embedder.js";
|
|
4
4
|
import { locateIndex, outputDir } from "../common/locate.js";
|
|
5
5
|
import { assertSameEmbedding } from "../common/manifest.js";
|
|
6
6
|
import { isGlob, loose, matcher, PatternError, wildcard } from "../common/match.js";
|
|
7
|
+
import { breakdown } from "../common/prose.js";
|
|
7
8
|
import { buildIndex } from "./build.js";
|
|
8
9
|
import { openIndex, readManifest, readSource, SCHEMA_INDEX } from "./files.js";
|
|
9
10
|
import { fields, grepNodes, listNodes, propertyCount } from "./lookup.js";
|
|
@@ -68,11 +69,17 @@ export const command = {
|
|
|
68
69
|
' -o, --out <dir>',
|
|
69
70
|
dim(`Where the index goes. Default ${DEFAULT_DIR}, or ${DIR_ENV}.`),
|
|
70
71
|
],
|
|
72
|
+
[' --batch <n>', dim("Texts per embedding request. Default: the model's own cap.")],
|
|
71
73
|
[
|
|
72
|
-
' --
|
|
73
|
-
dim(
|
|
74
|
+
' --dimensions <n>',
|
|
75
|
+
dim("Narrower vectors, if the model allows it. Default: the model's own width."),
|
|
74
76
|
],
|
|
75
77
|
[' --no-sources', dim('Do not keep a copy of each document in the index.')],
|
|
78
|
+
[
|
|
79
|
+
' --no-cache',
|
|
80
|
+
dim('Embed everything again, ignoring vectors this machine already has.'),
|
|
81
|
+
],
|
|
82
|
+
[' --cache-dir <dir>', dim('Keep the vectors somewhere other than the shared cache.')],
|
|
76
83
|
]),
|
|
77
84
|
'',
|
|
78
85
|
'Search terms (repeatable)',
|
|
@@ -99,6 +106,7 @@ export const command = {
|
|
|
99
106
|
[' --exclude-method <name>', dim('Drop an operation by name.')],
|
|
100
107
|
[' --exclude-type <name>', dim('Drop a schema by name.')],
|
|
101
108
|
[' --exclude-property <name>', dim('Drop a field by name.')],
|
|
109
|
+
[' --source <name>', dim('Only this document, as `stats` names it. Repeatable.')],
|
|
102
110
|
[' --limit <n>', dim('Seeds kept per term. Default 5.')],
|
|
103
111
|
[' --max-hops <n>', dim('How far apart two hits may be. Default 3.')],
|
|
104
112
|
[' --max-nodes <n>', dim('Nodes per result. Default 200.')],
|
|
@@ -188,34 +196,48 @@ async function index(args, ctx) {
|
|
|
188
196
|
out: { type: 'string', short: 'o' },
|
|
189
197
|
embedding: { type: 'string' },
|
|
190
198
|
batch: { type: 'string' },
|
|
199
|
+
dimensions: { type: 'string' },
|
|
191
200
|
'no-sources': { type: 'boolean' },
|
|
201
|
+
'no-cache': { type: 'boolean' },
|
|
202
|
+
'cache-dir': { type: 'string' },
|
|
192
203
|
quiet: { type: 'boolean' },
|
|
193
204
|
}, INDEX_USAGE);
|
|
194
205
|
if (positionals.length === 0) {
|
|
195
206
|
throw usageError('no document given', INDEX_USAGE);
|
|
196
207
|
}
|
|
197
208
|
const out = outputDir(ctx.cwd, values.out, SCHEMA_INDEX);
|
|
209
|
+
const cacheDir = values['cache-dir'] ? resolve(ctx.cwd, values['cache-dir']) : paths.cache();
|
|
198
210
|
const loud = !values.quiet && !ctx.json;
|
|
199
|
-
|
|
211
|
+
// Undefined when unasked, all the way to the cache key: a width nobody
|
|
212
|
+
// named is not the same key as the width the model happens to default to,
|
|
213
|
+
// and resolving it here would miss every vector already paid for.
|
|
214
|
+
const dimensions = values.dimensions ? count(values.dimensions, '--dimensions') : undefined;
|
|
215
|
+
const chosen = await resolveEmbedder(values.embedding, {
|
|
216
|
+
maxBatch: values.batch ? count(values.batch, '--batch') : undefined,
|
|
217
|
+
dimensions,
|
|
218
|
+
});
|
|
200
219
|
const started = Date.now();
|
|
201
|
-
const { manifest } = await buildIndex({
|
|
220
|
+
const { manifest, timings, reused } = await buildIndex({
|
|
202
221
|
files: positionals.map((file) => resolve(ctx.cwd, file)),
|
|
203
222
|
out,
|
|
204
223
|
embedder: chosen,
|
|
205
224
|
embeddingRef: values.embedding,
|
|
206
225
|
indexer: 'zenera-rag',
|
|
207
|
-
batch: values.batch ? count(values.batch, '--batch') : undefined,
|
|
208
226
|
sources: !values['no-sources'],
|
|
227
|
+
dimensions,
|
|
228
|
+
cache: !values['no-cache'],
|
|
229
|
+
cacheDir: values['cache-dir'] ? cacheDir : undefined,
|
|
209
230
|
onRead: loud
|
|
210
231
|
? (summary) => {
|
|
211
232
|
printSources(summary.sources);
|
|
212
|
-
//
|
|
213
|
-
//
|
|
233
|
+
// Embedding is one call now, and a long one; this is the line
|
|
234
|
+
// that makes the wait before the first progress report a wait
|
|
235
|
+
// rather than a hang.
|
|
214
236
|
note(dim(` embedding ${summary.counts.entities} entities with ${chosen.id} …`));
|
|
215
237
|
}
|
|
216
238
|
: undefined,
|
|
217
239
|
onProgress: loud
|
|
218
|
-
? (done, total) => note(dim(` embedded ${done}/${total} · ${Math.
|
|
240
|
+
? (done, total) => note(dim(` embedded ${done}/${total} · ${Math.floor((done / total) * 100)}% · ${elapsed(started)}`))
|
|
219
241
|
: undefined,
|
|
220
242
|
});
|
|
221
243
|
if (ctx.json) {
|
|
@@ -228,6 +250,9 @@ async function index(args, ctx) {
|
|
|
228
250
|
write(out);
|
|
229
251
|
note(` wrote ${bold(String(manifest.counts.entities))} entities to ${bold(out)}, ` +
|
|
230
252
|
`embedded with ${manifest.embedding.ref} (${manifest.embedding.dimensions}d)`);
|
|
253
|
+
note(dim(` ${breakdown(timings)}`));
|
|
254
|
+
note(dim(` reused ${reused}/${manifest.counts.entities} vectors` +
|
|
255
|
+
`${values['no-cache'] ? ' (--no-cache)' : ` from ${cacheDir}`}`));
|
|
231
256
|
const where = out === resolve(ctx.cwd, DEFAULT_DIR) ? '' : ` --dir ${relative(ctx.cwd, out) || out}`;
|
|
232
257
|
note(dim(` search it: ${cyan(`zen rag schema search${where} --all "what you are after"`)}`));
|
|
233
258
|
}
|
|
@@ -282,6 +307,7 @@ const SEARCH_OPTIONS = {
|
|
|
282
307
|
'exclude-method': MANY,
|
|
283
308
|
'exclude-type': MANY,
|
|
284
309
|
'exclude-property': MANY,
|
|
310
|
+
source: MANY,
|
|
285
311
|
limit: { type: 'string' },
|
|
286
312
|
'max-hops': { type: 'string' },
|
|
287
313
|
'max-nodes': { type: 'string' },
|
|
@@ -313,7 +339,9 @@ async function search(args, ctx) {
|
|
|
313
339
|
const manifest = await readManifest(dir);
|
|
314
340
|
const ref = values.embedding ?? manifest.embedding.ref;
|
|
315
341
|
assertSameEmbedding(manifest, ref);
|
|
316
|
-
|
|
342
|
+
// A query has to be asked at the width the entities were written at, and
|
|
343
|
+
// the ref alone does not say what that was.
|
|
344
|
+
const index = await SchemaIndex.open(dir, await resolveEmbedder(ref, { dimensions: manifest.embedding.requested }));
|
|
317
345
|
try {
|
|
318
346
|
if (values.interactive) {
|
|
319
347
|
await repl(index, query, { format, ...options });
|
|
@@ -362,6 +390,7 @@ function fromFlags(values, positionals = []) {
|
|
|
362
390
|
put('exclude_methods', values['exclude-method']);
|
|
363
391
|
put('exclude_types', values['exclude-type']);
|
|
364
392
|
put('exclude_properties', values['exclude-property']);
|
|
393
|
+
put('sources', values.source);
|
|
365
394
|
put('direction', values.direction);
|
|
366
395
|
put('method_type', values['method-type']);
|
|
367
396
|
put('limit', values.limit && count(values.limit, '--limit'));
|
package/dist/schema/query.js
CHANGED
package/dist/schema/readme.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { basename } from 'node:path';
|
|
2
2
|
import { INTERVAL_MS } from "../common/progress.js";
|
|
3
|
-
import { duration, fields, grid, message, plural, searched } from "../common/prose.js";
|
|
3
|
+
import { breakdown, duration, fields, grid, message, plural, searched } from "../common/prose.js";
|
|
4
4
|
export const PHASES = {
|
|
5
5
|
reading: 'reading the documents',
|
|
6
6
|
graph: 'building the graph',
|
|
@@ -22,9 +22,10 @@ function building(state) {
|
|
|
22
22
|
rows.push(['found', entities(state.summary)]);
|
|
23
23
|
}
|
|
24
24
|
if (state.total > 0) {
|
|
25
|
-
const percent = Math.
|
|
25
|
+
const percent = Math.floor((state.done / state.total) * 100);
|
|
26
26
|
rows.push(['embedded', `${state.done} of ${state.total} · ${percent}%`]);
|
|
27
27
|
}
|
|
28
|
+
rows.push(['timing', breakdown(state.timings)]);
|
|
28
29
|
rows.push(['updated', new Date(state.now).toISOString()]);
|
|
29
30
|
return [
|
|
30
31
|
'# Schema index — being built',
|
|
@@ -60,6 +61,8 @@ function complete(state) {
|
|
|
60
61
|
`${entities(manifest.counts)},`,
|
|
61
62
|
`${searched(manifest.indexes)}.`,
|
|
62
63
|
'',
|
|
64
|
+
`Time: ${breakdown(state.timings)}.`,
|
|
65
|
+
'',
|
|
63
66
|
'## Files',
|
|
64
67
|
'',
|
|
65
68
|
...fields([
|
|
@@ -97,6 +100,7 @@ function failed(state) {
|
|
|
97
100
|
['step', state.step],
|
|
98
101
|
['reason', message(state.reason)],
|
|
99
102
|
['started', new Date(state.started).toISOString()],
|
|
103
|
+
['timing', breakdown(state.timings)],
|
|
100
104
|
[
|
|
101
105
|
'failed',
|
|
102
106
|
`${new Date().toISOString()} (after ${duration(Date.now() - state.started)})`,
|
package/dist/schema/search.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ export interface SchemaQuery {
|
|
|
24
24
|
exclude_methods?: readonly string[];
|
|
25
25
|
exclude_types?: readonly string[];
|
|
26
26
|
exclude_properties?: readonly string[];
|
|
27
|
+
/** document names, as `stats` prints them; any one of them is enough */
|
|
28
|
+
sources?: readonly string[];
|
|
27
29
|
/** seeds kept per query string */
|
|
28
30
|
limit?: number;
|
|
29
31
|
max_hops?: number;
|