@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
package/dist/schema/search.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CliError, EXIT } from '@zenera/cli/lib';
|
|
1
2
|
import { assertSameEmbedding } from "../common/manifest.js";
|
|
2
3
|
import { openIndex } from "./files.js";
|
|
3
4
|
import { EntityStore } from "./store.js";
|
|
@@ -21,7 +22,8 @@ export class SchemaIndex {
|
|
|
21
22
|
static async open(dir, embedder) {
|
|
22
23
|
const index = await openIndex(dir);
|
|
23
24
|
assertSameEmbedding(index.manifest, embedder.id);
|
|
24
|
-
|
|
25
|
+
const names = index.manifest.sources.map((s) => s.name);
|
|
26
|
+
return new SchemaIndex(index, await EntityStore.open(dir, names), embedder);
|
|
25
27
|
}
|
|
26
28
|
schemas() {
|
|
27
29
|
return this.#index.schemas();
|
|
@@ -33,6 +35,7 @@ export class SchemaIndex {
|
|
|
33
35
|
this.#store.close();
|
|
34
36
|
}
|
|
35
37
|
async search(query, signal) {
|
|
38
|
+
this.#assertSources(query.sources);
|
|
36
39
|
const terms = termsOf(query);
|
|
37
40
|
if (terms.length === 0) {
|
|
38
41
|
return { seeds: [], subgraphs: [], empty: [] };
|
|
@@ -70,11 +73,24 @@ export class SchemaIndex {
|
|
|
70
73
|
});
|
|
71
74
|
return { seeds, subgraphs, empty };
|
|
72
75
|
}
|
|
76
|
+
/** Settled before the embedder is called, so a typo costs no credential. */
|
|
77
|
+
#assertSources(wanted) {
|
|
78
|
+
if (!wanted || wanted.length === 0) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const known = this.manifest.sources.map((s) => s.name);
|
|
82
|
+
const missing = wanted.filter((name) => !known.includes(name));
|
|
83
|
+
if (missing.length > 0) {
|
|
84
|
+
throw new CliError(`this index holds no document called ${missing.join(', ')}`, EXIT.failed, `it has: ${known.join(', ')}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
73
87
|
}
|
|
74
88
|
// ---------------------------------------------------------------------------
|
|
75
89
|
function termsOf(query) {
|
|
76
90
|
const method = methodTypes(query.method_type);
|
|
77
91
|
const loose = query.direction ?? 'any';
|
|
92
|
+
// A document is a constraint on the whole question, not on one field of it.
|
|
93
|
+
const sources = query.sources?.length ? query.sources : undefined;
|
|
78
94
|
return [
|
|
79
95
|
...group(query.all, 'all', { methodTypes: method.mixed }),
|
|
80
96
|
...group(query.methods, 'methods', { kinds: ['method'], methodTypes: method.only }),
|
|
@@ -96,7 +112,7 @@ function termsOf(query) {
|
|
|
96
112
|
kinds: ['property'],
|
|
97
113
|
directions: sides('output'),
|
|
98
114
|
}),
|
|
99
|
-
];
|
|
115
|
+
].map((term) => (sources ? { ...term, filter: { ...term.filter, sources } } : term));
|
|
100
116
|
}
|
|
101
117
|
function group(texts, field, filter) {
|
|
102
118
|
return (texts ?? [])
|
package/dist/schema/store.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export interface StoreFilter {
|
|
|
4
4
|
kinds?: readonly string[];
|
|
5
5
|
directions?: readonly string[];
|
|
6
6
|
methodTypes?: readonly string[];
|
|
7
|
+
sources?: readonly string[];
|
|
7
8
|
}
|
|
8
9
|
export interface Hit {
|
|
9
10
|
record: EntityRecord;
|
|
@@ -17,11 +18,20 @@ export interface WriteResult {
|
|
|
17
18
|
fts: boolean;
|
|
18
19
|
vector: boolean;
|
|
19
20
|
}
|
|
20
|
-
|
|
21
|
+
/** A table built a window at a time, so peak memory is not a function of size. */
|
|
22
|
+
export interface EntityWriter {
|
|
23
|
+
add(rows: readonly EntityRecord[], vectors: readonly Float32Array[]): Promise<void>;
|
|
24
|
+
/** Builds the indexes and closes. Throws if nothing was ever added. */
|
|
25
|
+
finish(): Promise<WriteResult>;
|
|
26
|
+
/** For a build that failed, so the connection does not outlive it. */
|
|
27
|
+
close(): void;
|
|
28
|
+
}
|
|
29
|
+
export declare function openStore(dir: string): Promise<EntityWriter>;
|
|
21
30
|
export declare class EntityStore {
|
|
22
31
|
#private;
|
|
23
|
-
constructor(db: Connection, table: Table);
|
|
24
|
-
|
|
32
|
+
constructor(db: Connection, table: Table, sources?: readonly string[]);
|
|
33
|
+
/** `sources` is the document vocabulary a `sources` filter is checked against. */
|
|
34
|
+
static open(dir: string, sources?: readonly string[]): Promise<EntityStore>;
|
|
25
35
|
/**
|
|
26
36
|
* One hybrid query: the same string goes to the full-text side and, as a
|
|
27
37
|
* vector, to the nearest-neighbour side, and LanceDB fuses the two.
|
package/dist/schema/store.js
CHANGED
|
@@ -8,47 +8,95 @@ import { lancePath } from "./files.js";
|
|
|
8
8
|
// carrying both the embedding and the full-text index, and the handful of
|
|
9
9
|
// enum columns a query filters on.
|
|
10
10
|
//
|
|
11
|
-
// Those
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
11
|
+
// Those closed vocabularies are the *only* thing that reaches the SQL
|
|
12
|
+
// predicate. `source` is one of them: its values are the document names the
|
|
13
|
+
// index itself wrote, so the store is opened with that list and checks against
|
|
14
|
+
// it. Exclusion lists — which arrive from a model, or from a shell — are
|
|
15
|
+
// applied afterwards in JavaScript. Escaping those into `where()` would work
|
|
16
|
+
// right up until it did not, and there is nothing here that a filter string
|
|
17
|
+
// buys.
|
|
15
18
|
// ---------------------------------------------------------------------------
|
|
16
19
|
const TABLE = 'entities';
|
|
17
20
|
/** Below this an IVF index has nothing to train on, and a flat scan is faster. */
|
|
18
21
|
const VECTOR_INDEX_MIN_ROWS = 2000;
|
|
22
|
+
/**
|
|
23
|
+
* Rows per write.
|
|
24
|
+
*
|
|
25
|
+
* Arrow addresses a batch's buffers with 32-bit offsets, so one batch carrying
|
|
26
|
+
* more than 2 GiB does not error — it panics inside the reader, on a thread
|
|
27
|
+
* whose panic never reaches this one.
|
|
28
|
+
*/
|
|
29
|
+
const WRITE_BATCH = 8192;
|
|
19
30
|
const KINDS = new Set(['method', 'type', 'property']);
|
|
20
31
|
const DIRECTIONS = new Set(['input', 'output', 'both', 'none']);
|
|
21
32
|
const METHOD_TYPES = new Set(['read_only', 'read_write', 'n/a']);
|
|
22
|
-
export async function
|
|
23
|
-
|
|
24
|
-
|
|
33
|
+
export async function openStore(dir) {
|
|
34
|
+
return new Writer(await connect(lancePath(dir)));
|
|
35
|
+
}
|
|
36
|
+
class Writer {
|
|
37
|
+
#db;
|
|
38
|
+
#table;
|
|
39
|
+
#rows = 0;
|
|
40
|
+
constructor(db) {
|
|
41
|
+
this.#db = db;
|
|
25
42
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
43
|
+
async add(rows, vectors) {
|
|
44
|
+
for (let from = 0; from < rows.length; from += WRITE_BATCH) {
|
|
45
|
+
const batch = rows.slice(from, from + WRITE_BATCH).map((row, i) => ({
|
|
46
|
+
...row,
|
|
47
|
+
vector: vectors[from + i],
|
|
48
|
+
}));
|
|
49
|
+
if (this.#table) {
|
|
50
|
+
await this.#table.add(batch);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
// Every column is always populated — never null — so the Arrow
|
|
54
|
+
// schema is inferred from the first row without a declaration.
|
|
55
|
+
this.#table = await this.#db.createTable(TABLE, batch, { mode: 'overwrite' });
|
|
56
|
+
}
|
|
57
|
+
this.#rows += batch.length;
|
|
58
|
+
}
|
|
33
59
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
60
|
+
async finish() {
|
|
61
|
+
const table = this.#table;
|
|
62
|
+
if (!table) {
|
|
63
|
+
throw new CliError('the documents describe nothing to index', EXIT.invalid, 'they have no operations and no component schemas');
|
|
64
|
+
}
|
|
65
|
+
// A writer that panicked did not reject, so a short table is the only
|
|
66
|
+
// evidence left that the rows never landed.
|
|
67
|
+
const stored = await table.countRows();
|
|
68
|
+
if (stored !== this.#rows) {
|
|
69
|
+
throw new CliError(`the table holds ${stored} of ${this.#rows} entities`, EXIT.failed, 'the write did not finish, and a partial index answers as if it were whole');
|
|
70
|
+
}
|
|
71
|
+
await table.createIndex('text', { config: Index.fts() });
|
|
72
|
+
for (const column of ['kind', 'direction', 'methodType']) {
|
|
73
|
+
await table.createIndex(column, { config: Index.bitmap() });
|
|
74
|
+
}
|
|
75
|
+
const vector = this.#rows >= VECTOR_INDEX_MIN_ROWS;
|
|
76
|
+
if (vector) {
|
|
77
|
+
await table.createIndex('vector');
|
|
78
|
+
}
|
|
79
|
+
this.#db.close();
|
|
80
|
+
return { rows: this.#rows, fts: true, vector };
|
|
81
|
+
}
|
|
82
|
+
close() {
|
|
83
|
+
this.#db.close();
|
|
37
84
|
}
|
|
38
|
-
db.close();
|
|
39
|
-
return { rows: rows.length, fts: true, vector };
|
|
40
85
|
}
|
|
41
86
|
export class EntityStore {
|
|
42
87
|
#db;
|
|
43
88
|
#table;
|
|
44
|
-
|
|
89
|
+
#sources;
|
|
90
|
+
constructor(db, table, sources = []) {
|
|
45
91
|
this.#db = db;
|
|
46
92
|
this.#table = table;
|
|
93
|
+
this.#sources = new Set(sources);
|
|
47
94
|
}
|
|
48
|
-
|
|
95
|
+
/** `sources` is the document vocabulary a `sources` filter is checked against. */
|
|
96
|
+
static async open(dir, sources = []) {
|
|
49
97
|
const db = await connect(lancePath(dir));
|
|
50
98
|
try {
|
|
51
|
-
return new EntityStore(db, await db.openTable(TABLE));
|
|
99
|
+
return new EntityStore(db, await db.openTable(TABLE), sources);
|
|
52
100
|
}
|
|
53
101
|
catch {
|
|
54
102
|
db.close();
|
|
@@ -60,7 +108,7 @@ export class EntityStore {
|
|
|
60
108
|
* vector, to the nearest-neighbour side, and LanceDB fuses the two.
|
|
61
109
|
*/
|
|
62
110
|
async search(text, vector, filter, limit) {
|
|
63
|
-
const predicate = where(filter);
|
|
111
|
+
const predicate = where(filter, this.#sources);
|
|
64
112
|
let query = this.#table.query().nearestToText(text).nearestTo(vector).limit(limit);
|
|
65
113
|
if (predicate) {
|
|
66
114
|
query = query.where(predicate);
|
|
@@ -78,11 +126,12 @@ export class EntityStore {
|
|
|
78
126
|
}
|
|
79
127
|
// ---------------------------------------------------------------------------
|
|
80
128
|
/** Closed vocabularies only. Anything else is a bug, and is treated as one. */
|
|
81
|
-
function where(filter) {
|
|
129
|
+
function where(filter, sources) {
|
|
82
130
|
const clauses = [
|
|
83
131
|
clause('kind', filter.kinds, KINDS),
|
|
84
132
|
clause('direction', filter.directions, DIRECTIONS),
|
|
85
133
|
clause('methodType', filter.methodTypes, METHOD_TYPES),
|
|
134
|
+
clause('source', filter.sources, sources),
|
|
86
135
|
].filter(Boolean);
|
|
87
136
|
return clauses.join(' AND ');
|
|
88
137
|
}
|
package/dist/schema/tools.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { tool } from '@zenera/neo';
|
|
2
2
|
import { loose, matcher, PatternError } from "../common/match.js";
|
|
3
|
-
import { FORMATS, isFormat, present } from "./present.js";
|
|
4
|
-
import { isEmpty, parseQuery, QueryError } from "./query.js";
|
|
5
3
|
import { toTypeScript } from "./hydrate.js";
|
|
6
4
|
import { fields, grepNodes, listNodes, propertyCount } from "./lookup.js";
|
|
5
|
+
import { FORMATS, isFormat, present } from "./present.js";
|
|
6
|
+
import { isEmpty, parseQuery, QueryError } from "./query.js";
|
|
7
7
|
import { sourceTag } from "./render.js";
|
|
8
8
|
import { stitch } from "./subgraph.js";
|
|
9
9
|
import { chainOf, traceNodes } from "./trace.js";
|
|
@@ -47,6 +47,7 @@ export function schemaTools(index, options = {}) {
|
|
|
47
47
|
// line is prompt spent saying the same word; with several it is the only
|
|
48
48
|
// way to tell two revisions of one API apart.
|
|
49
49
|
const source = options.source ?? index.manifest.sources.length > 1;
|
|
50
|
+
const names = index.manifest.sources.map((s) => s.name);
|
|
50
51
|
const searchApi = tool({
|
|
51
52
|
name: 'search_api',
|
|
52
53
|
group: GROUP,
|
|
@@ -81,6 +82,17 @@ export function schemaTools(index, options = {}) {
|
|
|
81
82
|
exclude_methods: list('Operation names to leave out.'),
|
|
82
83
|
exclude_types: list('Schema names to leave out.'),
|
|
83
84
|
exclude_properties: list('Field names to leave out.'),
|
|
85
|
+
// Naming the documents is only a choice when there is more than
|
|
86
|
+
// one, and an enum is what stops a model inventing a third.
|
|
87
|
+
...(names.length > 1
|
|
88
|
+
? {
|
|
89
|
+
sources: {
|
|
90
|
+
type: 'array',
|
|
91
|
+
items: { type: 'string', enum: names },
|
|
92
|
+
description: 'Search only these documents. Omit it to search all of them.',
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
: {}),
|
|
84
96
|
limit: {
|
|
85
97
|
type: 'integer',
|
|
86
98
|
description: `Results per phrase. Default ${DEFAULT_LIMIT}.`,
|
|
@@ -111,6 +123,13 @@ export function schemaTools(index, options = {}) {
|
|
|
111
123
|
if (isEmpty(query)) {
|
|
112
124
|
return { error: 'nothing was asked for', hint: 'fill at least one search field' };
|
|
113
125
|
}
|
|
126
|
+
const absent = (query.sources ?? []).filter((name) => !names.includes(name));
|
|
127
|
+
if (absent.length > 0) {
|
|
128
|
+
return {
|
|
129
|
+
error: `no document called ${absent.join(', ')}`,
|
|
130
|
+
hint: `it has: ${names.join(', ')}`,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
114
133
|
const result = await index.search({
|
|
115
134
|
limit: DEFAULT_LIMIT,
|
|
116
135
|
max_nodes: DEFAULT_MAX_NODES,
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zenera/rag",
|
|
3
|
-
"version": "1.1.
|
|
4
|
-
"description": "Retrieval over
|
|
3
|
+
"version": "1.1.11",
|
|
4
|
+
"description": "Retrieval over a corpus: openapi/swagger documents as a searchable graph, markdown and text as searchable passages.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
7
7
|
"ai-agents",
|
|
8
8
|
"llm",
|
|
9
|
+
"markdown",
|
|
9
10
|
"openapi",
|
|
10
11
|
"rag",
|
|
11
12
|
"vector-search"
|
|
@@ -35,6 +36,14 @@
|
|
|
35
36
|
"./tools": {
|
|
36
37
|
"types": "./dist/schema/tools.d.ts",
|
|
37
38
|
"default": "./dist/schema/tools.js"
|
|
39
|
+
},
|
|
40
|
+
"./docs": {
|
|
41
|
+
"types": "./dist/docs/index.d.ts",
|
|
42
|
+
"default": "./dist/docs/index.js"
|
|
43
|
+
},
|
|
44
|
+
"./docs/tools": {
|
|
45
|
+
"types": "./dist/docs/tools.d.ts",
|
|
46
|
+
"default": "./dist/docs/tools.js"
|
|
38
47
|
}
|
|
39
48
|
},
|
|
40
49
|
"files": [
|
|
@@ -52,8 +61,12 @@
|
|
|
52
61
|
"dependencies": {
|
|
53
62
|
"@apidevtools/swagger-parser": "^12.0.0",
|
|
54
63
|
"@lancedb/lancedb": "^0.38.0",
|
|
64
|
+
"@zenera/cli": "^1.1.11",
|
|
65
|
+
"@zenera/neo": "^1.1.11",
|
|
55
66
|
"graphology": "^0.26.0",
|
|
56
|
-
"
|
|
57
|
-
"
|
|
67
|
+
"remark-frontmatter": "^5.0.0",
|
|
68
|
+
"remark-gfm": "^4.0.1",
|
|
69
|
+
"remark-parse": "^11.0.0",
|
|
70
|
+
"unified": "^11.0.5"
|
|
58
71
|
}
|
|
59
72
|
}
|