@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.
Files changed (62) hide show
  1. package/README.md +195 -10
  2. package/dist/command.js +2 -1
  3. package/dist/common/cache.d.ts +66 -0
  4. package/dist/common/cache.js +172 -0
  5. package/dist/common/embedder.d.ts +11 -1
  6. package/dist/common/embedder.js +6 -17
  7. package/dist/common/manifest.d.ts +7 -1
  8. package/dist/common/progress.d.ts +16 -1
  9. package/dist/common/progress.js +35 -3
  10. package/dist/common/prose.d.ts +7 -0
  11. package/dist/common/prose.js +13 -0
  12. package/dist/docs/assemble.d.ts +52 -0
  13. package/dist/docs/assemble.js +127 -0
  14. package/dist/docs/build.d.ts +54 -0
  15. package/dist/docs/build.js +134 -0
  16. package/dist/docs/chunk.d.ts +73 -0
  17. package/dist/docs/chunk.js +586 -0
  18. package/dist/docs/command.d.ts +3 -0
  19. package/dist/docs/command.js +575 -0
  20. package/dist/docs/files.d.ts +94 -0
  21. package/dist/docs/files.js +80 -0
  22. package/dist/docs/index.d.ts +13 -0
  23. package/dist/docs/index.js +13 -0
  24. package/dist/docs/load.d.ts +42 -0
  25. package/dist/docs/load.js +203 -0
  26. package/dist/docs/lookup.d.ts +80 -0
  27. package/dist/docs/lookup.js +147 -0
  28. package/dist/docs/outline.d.ts +11 -0
  29. package/dist/docs/outline.js +55 -0
  30. package/dist/docs/parse-cache.d.ts +20 -0
  31. package/dist/docs/parse-cache.js +60 -0
  32. package/dist/docs/parse-worker.d.ts +13 -0
  33. package/dist/docs/parse-worker.js +21 -0
  34. package/dist/docs/parse.d.ts +95 -0
  35. package/dist/docs/parse.js +372 -0
  36. package/dist/docs/pool.d.ts +27 -0
  37. package/dist/docs/pool.js +133 -0
  38. package/dist/docs/readme.d.ts +6 -0
  39. package/dist/docs/readme.js +142 -0
  40. package/dist/docs/render.d.ts +13 -0
  41. package/dist/docs/render.js +46 -0
  42. package/dist/docs/repl.d.ts +7 -0
  43. package/dist/docs/repl.js +130 -0
  44. package/dist/docs/search.d.ts +92 -0
  45. package/dist/docs/search.js +251 -0
  46. package/dist/docs/store.d.ts +79 -0
  47. package/dist/docs/store.js +214 -0
  48. package/dist/docs/tools.d.ts +10 -0
  49. package/dist/docs/tools.js +300 -0
  50. package/dist/index.d.ts +1 -0
  51. package/dist/index.js +3 -0
  52. package/dist/schema/build.d.ts +16 -2
  53. package/dist/schema/build.js +37 -25
  54. package/dist/schema/command.js +39 -10
  55. package/dist/schema/query.js +1 -0
  56. package/dist/schema/readme.js +6 -2
  57. package/dist/schema/search.d.ts +2 -0
  58. package/dist/schema/search.js +18 -2
  59. package/dist/schema/store.d.ts +13 -3
  60. package/dist/schema/store.js +73 -24
  61. package/dist/schema/tools.js +21 -2
  62. package/package.json +17 -4
@@ -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
- return new SchemaIndex(index, await EntityStore.open(dir), embedder);
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 ?? [])
@@ -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
- export declare function writeStore(dir: string, rows: readonly EntityRecord[], vectors: readonly Float32Array[]): Promise<WriteResult>;
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
- static open(dir: string): Promise<EntityStore>;
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.
@@ -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 enums are the *only* thing that reaches the SQL predicate. Exclusion
12
- // lists which arrive from a model, or from a shell are applied afterwards
13
- // in JavaScript. Escaping them into `where()` would work right up until it did
14
- // not, and there is nothing here that a filter string buys.
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 writeStore(dir, rows, vectors) {
23
- if (rows.length === 0) {
24
- throw new CliError('the documents describe nothing to index', EXIT.invalid, 'they have no operations and no component schemas');
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
- const db = await connect(lancePath(dir));
27
- // Every column is always populated never null so the Arrow schema is
28
- // inferred from the first row without a declaration to keep in step.
29
- const table = await db.createTable(TABLE, rows.map((row, i) => ({ ...row, vector: vectors[i] })), { mode: 'overwrite' });
30
- await table.createIndex('text', { config: Index.fts() });
31
- for (const column of ['kind', 'direction', 'methodType']) {
32
- await table.createIndex(column, { config: Index.bitmap() });
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
- const vector = rows.length >= VECTOR_INDEX_MIN_ROWS;
35
- if (vector) {
36
- await table.createIndex('vector');
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
- constructor(db, table) {
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
- static async open(dir) {
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
  }
@@ -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.9",
4
- "description": "Retrieval over API descriptions: openapi/swagger documents as a searchable graph.",
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
- "@zenera/cli": "^1.1.9",
57
- "@zenera/neo": "^1.1.9"
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
  }