@nebutra/knowledge-rag 0.2.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 ADDED
@@ -0,0 +1,151 @@
1
+ # @nebutra/knowledge-rag
2
+
3
+ Multi-tenant **hybrid RAG** pipeline for Nebutra-Sailor.
4
+
5
+ ```
6
+ ingest → semantic chunk → embed → vector store + keyword index
7
+ → hybrid retrieval (vector ⊕ keyword) → optional rerank
8
+ ```
9
+
10
+ Every persisted record carries a **`tenantId`**. Tenant isolation is enforced
11
+ at every store/query boundary — it is never optional. A query scoped to tenant
12
+ A can never return tenant B's chunks.
13
+
14
+ ## Status
15
+
16
+ `active` — exported tool factory (`createKnowledgeRagTool`) is the real caller
17
+ (used by `@nebutra/agent-runtime` / `@nebutra/agents`). Zero-config by default.
18
+
19
+ ## Zero-config quick start
20
+
21
+ No env vars, no external services. In-memory vector store + a deterministic
22
+ local hash embedder (real signal — shared tokens raise cosine similarity, not
23
+ mocked). The keyword leg auto-activates **only** if a reachable
24
+ `@nebutra/search` backend is detected; otherwise it gracefully degrades to
25
+ vector-only.
26
+
27
+ ```ts
28
+ import { getKnowledgeRag } from "@nebutra/knowledge-rag";
29
+
30
+ const kb = await getKnowledgeRag();
31
+
32
+ await kb.ingest({ id: "doc1", tenantId: "org_a", text: "...", meta: { src: "wiki" } });
33
+
34
+ const hits = await kb.query({ query: "how does X work?", tenantId: "org_a", topK: 5 });
35
+ // → RankedChunk[]: { chunk, score, scores: { vector, keyword }, source }
36
+
37
+ await kb.deleteByDoc("doc1", "org_a"); // tenant-scoped delete
38
+ ```
39
+
40
+ ## Public API
41
+
42
+ | Function / method | Purpose |
43
+ |---|---|
44
+ | `getKnowledgeRag(config?)` | Process-wide instance (or fresh when config given). |
45
+ | `createKnowledgeRag(config?)` | Construct an isolated instance. |
46
+ | `kb.ingest({ id, text, tenantId, meta? })` | Chunk → embed → index. |
47
+ | `kb.query({ query, tenantId, topK? })` | Hybrid retrieval → ranked chunks. |
48
+ | `kb.deleteByDoc(docId, tenantId)` | Tenant-scoped removal across both legs. |
49
+ | `kb.doctor()` / `doctor(config?)` | Structured health report, **< 3s**. |
50
+ | `createKnowledgeRagTool(tenantId, config?)` | Tenant-bound agent tool. |
51
+
52
+ All thrown errors are `KnowledgeRagError` and carry an actionable
53
+ `.suggestion` (and `.code`); `.toJSON()` serialises both for logging.
54
+
55
+ ### Pluggable internals
56
+
57
+ - **Chunker** — `RecursiveCharChunker({ size, overlap, separators? })`
58
+ (paragraph → line → sentence → word → char), configurable size/overlap.
59
+ - **Embedder** — `LocalHashEmbedder` (default, zero-config, no network) or
60
+ `ProviderEmbedder` (wraps `@nebutra/agents` `embedMany()` → Vercel AI SDK
61
+ with the `LLM_EMBEDDING_FALLBACK_CHAIN`).
62
+ - **VectorStore** — `InMemoryVectorStore` (default) or `PgvectorStore`
63
+ (interface-only, see below).
64
+ - **Reranker** — `IdentityReranker` (default) or `LexicalOverlapReranker`.
65
+
66
+ ## How it wraps existing Sailor infra (no reinvention)
67
+
68
+ - **Keyword leg → `@nebutra/search`.** `SearchKeywordIndex` wraps the existing
69
+ provider-agnostic search abstraction (`getSearch()` →
70
+ Meilisearch / Typesense / Algolia / pgvector-BM25). Each chunk is indexed as
71
+ a `SearchDocument` carrying `tenantId`; every keyword query passes
72
+ `filters: { tenantId }` and results are re-filtered by tenant, so the keyword
73
+ leg has the same isolation guarantee as the vector leg. `@nebutra/search` is
74
+ imported lazily and a probe call decides reachability — an absent search
75
+ server can never break the zero-config path.
76
+ - **Embeddings → `@nebutra/agents`.** `ProviderEmbedder` lazily imports and
77
+ calls `embedMany()` (the SDK's provider/fallback chain). Vector provider
78
+ calls are reused, never reimplemented. With no AI env, the default
79
+ `LocalHashEmbedder` keeps everything running with real (non-mock) results.
80
+
81
+ ## pgvector store (production) — Prisma model + DDL
82
+
83
+ `PgvectorStore` is interface-only here (not exercised by unit tests, no
84
+ migrations run). To activate, apply the schema below and pass an executor.
85
+
86
+ ```prisma
87
+ // Add to your Prisma schema. Requires the pgvector extension:
88
+ // model: see below; SQL: CREATE EXTENSION IF NOT EXISTS vector;
89
+
90
+ /// One persisted RAG chunk. EVERY row is tenant-scoped.
91
+ model KnowledgeRagChunk {
92
+ id String @id // `${docId}::${ordinal}`
93
+ docId String @map("doc_id")
94
+ tenantId String @map("tenant_id") // REQUIRED — isolation key
95
+ text String
96
+ ordinal Int
97
+ /// Vector column — set the dimension to your embedder's output.
98
+ /// Prisma has no native vector type; declare via Unsupported + raw SQL.
99
+ embedding Unsupported("vector(1536)")
100
+ meta Json @default("{}")
101
+ createdAt DateTime @default(now()) @map("created_at")
102
+
103
+ @@index([tenantId]) // tenant-prefix filtering
104
+ @@index([docId, tenantId]) // scoped delete
105
+ @@map("knowledge_rag_chunk")
106
+ }
107
+ ```
108
+
109
+ Companion raw migration (run manually — this package does **not** migrate):
110
+
111
+ ```sql
112
+ CREATE EXTENSION IF NOT EXISTS vector;
113
+
114
+ -- After `prisma migrate` creates knowledge_rag_chunk, add the ANN index.
115
+ -- Cosine distance (<=>) matches the store's similarity = 1 - distance.
116
+ CREATE INDEX IF NOT EXISTS knowledge_rag_chunk_embedding_idx
117
+ ON knowledge_rag_chunk
118
+ USING hnsw (embedding vector_cosine_ops);
119
+ ```
120
+
121
+ ```ts
122
+ import { PgvectorStore } from "@nebutra/knowledge-rag";
123
+
124
+ const store = new PgvectorStore({
125
+ executor: { query: (sql, params) => prisma.$queryRawUnsafe(sql, ...params) },
126
+ embeddingDim: 1536,
127
+ });
128
+ const kb = await getKnowledgeRag({ vectorStore: store });
129
+ ```
130
+
131
+ Every `PgvectorStore` query includes a mandatory `WHERE tenant_id = $1`
132
+ predicate; deletes are scoped `WHERE doc_id = $1 AND tenant_id = $2`.
133
+
134
+ ## Examples
135
+
136
+ Runnable under `examples/` (`node --import tsx examples/<file>.ts`):
137
+
138
+ 1. `01-zero-config.ts` — ingest → query, no config.
139
+ 2. `02-tenant-isolation.ts` — identical text, zero cross-tenant leakage.
140
+ 3. `03-custom-pipeline-and-doctor.ts` — custom chunker/reranker + `doctor()`.
141
+ 4. `04-agent-tool.ts` — tenant-bound agent tool (the real caller).
142
+
143
+ ## Testing
144
+
145
+ ```bash
146
+ pnpm --filter @nebutra/knowledge-rag test # vitest
147
+ pnpm --filter @nebutra/knowledge-rag exec vitest run --coverage
148
+ ```
149
+
150
+ TDD-first; ≥80% coverage on core logic (chunker boundaries/overlap, hybrid
151
+ scoring math, tenant isolation, ingest→query roundtrip).
@@ -0,0 +1,21 @@
1
+ import type { Chunker } from "./types";
2
+ export interface RecursiveCharChunkerOptions {
3
+ /** Maximum chunk length in characters. */
4
+ size: number;
5
+ /** Characters of overlap carried from the end of one chunk to the next. */
6
+ overlap: number;
7
+ /** Separators tried in order, coarsest first. */
8
+ separators?: string[];
9
+ }
10
+ export declare class RecursiveCharChunker implements Chunker {
11
+ private readonly size;
12
+ private readonly overlap;
13
+ private readonly separators;
14
+ constructor(options: RecursiveCharChunkerOptions);
15
+ split(text: string): string[];
16
+ /** Break text into atomic pieces each <= size, preferring coarse separators. */
17
+ private recursiveSplit;
18
+ /** Pack atomic pieces into <= size windows, carrying `overlap` chars over. */
19
+ private mergeWithOverlap;
20
+ }
21
+ //# sourceMappingURL=chunker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunker.d.ts","sourceRoot":"","sources":["../src/chunker.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEvC,MAAM,WAAW,2BAA2B;IAC1C,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,2EAA2E;IAC3E,OAAO,EAAE,MAAM,CAAC;IAChB,iDAAiD;IACjD,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB;AAID,qBAAa,oBAAqB,YAAW,OAAO;IAClD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAW;gBAE1B,OAAO,EAAE,2BAA2B;IAsBhD,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE;IAW7B,gFAAgF;IAChF,OAAO,CAAC,cAAc;IA8BtB,8EAA8E;IAC9E,OAAO,CAAC,gBAAgB;CA2BzB"}
@@ -0,0 +1,103 @@
1
+ // =============================================================================
2
+ // @nebutra/knowledge-rag — Recursive character chunker
3
+ // =============================================================================
4
+ // Splits text by trying progressively finer separators (paragraph → line →
5
+ // sentence → word → char), packing into windows of <= `size` with `overlap`
6
+ // carried between consecutive chunks. Pure and deterministic.
7
+ // =============================================================================
8
+ import { KnowledgeRagError } from "./errors";
9
+ const DEFAULT_SEPARATORS = ["\n\n", "\n", ". ", " ", ""];
10
+ export class RecursiveCharChunker {
11
+ size;
12
+ overlap;
13
+ separators;
14
+ constructor(options) {
15
+ const { size, overlap } = options;
16
+ if (!Number.isFinite(size) || size <= 0) {
17
+ throw new KnowledgeRagError(`Invalid chunker size: ${size}`, {
18
+ code: "E_CHUNKER_CONFIG",
19
+ suggestion: "Pass a positive integer `size` (e.g. { size: 800, overlap: 100 }).",
20
+ });
21
+ }
22
+ if (overlap < 0 || overlap >= size) {
23
+ throw new KnowledgeRagError(`Invalid chunker overlap: ${overlap} (must be >= 0 and < size ${size})`, {
24
+ code: "E_CHUNKER_CONFIG",
25
+ suggestion: `Set overlap to a value in [0, ${size - 1}] — typically ~10-15% of size.`,
26
+ });
27
+ }
28
+ this.size = size;
29
+ this.overlap = overlap;
30
+ this.separators = options.separators ?? DEFAULT_SEPARATORS;
31
+ }
32
+ split(text) {
33
+ if (text.trim().length === 0) {
34
+ return [];
35
+ }
36
+ const pieces = this.recursiveSplit(text, 0);
37
+ // Pieces are each <= size. If any single piece exceeds the effective
38
+ // step (size - overlap) we still pack greedily; the overlap is applied as
39
+ // a prefix carried into the *next* chunk.
40
+ return this.mergeWithOverlap(pieces);
41
+ }
42
+ /** Break text into atomic pieces each <= size, preferring coarse separators. */
43
+ recursiveSplit(text, sepIndex) {
44
+ if (text.length <= this.size) {
45
+ return text.length > 0 ? [text] : [];
46
+ }
47
+ const sep = this.separators[sepIndex] ?? "";
48
+ if (sep === "") {
49
+ // Hard character split as a last resort. Window the step at
50
+ // (size - overlap) so an overlap prefix can always be prepended later
51
+ // without exceeding `size`.
52
+ const step = Math.max(1, this.size - this.overlap);
53
+ const out = [];
54
+ for (let i = 0; i < text.length; i += step) {
55
+ out.push(text.slice(i, i + step));
56
+ }
57
+ return out;
58
+ }
59
+ const parts = text.split(sep);
60
+ const out = [];
61
+ for (let i = 0; i < parts.length; i++) {
62
+ const withSep = i < parts.length - 1 ? parts[i] + sep : parts[i];
63
+ if (withSep.length === 0)
64
+ continue;
65
+ if (withSep.length <= this.size) {
66
+ out.push(withSep);
67
+ }
68
+ else {
69
+ out.push(...this.recursiveSplit(withSep, sepIndex + 1));
70
+ }
71
+ }
72
+ return out;
73
+ }
74
+ /** Pack atomic pieces into <= size windows, carrying `overlap` chars over. */
75
+ mergeWithOverlap(pieces) {
76
+ const chunks = [];
77
+ let current = "";
78
+ for (const piece of pieces) {
79
+ if (current.length + piece.length <= this.size) {
80
+ current += piece;
81
+ continue;
82
+ }
83
+ if (current.length > 0) {
84
+ chunks.push(current);
85
+ current = this.overlap > 0 ? current.slice(-this.overlap) : "";
86
+ }
87
+ if (piece.length <= this.size) {
88
+ current += piece;
89
+ }
90
+ else {
91
+ // Oversized atomic piece — emit fixed windows directly.
92
+ for (let i = 0; i < piece.length; i += this.size) {
93
+ chunks.push(piece.slice(i, i + this.size));
94
+ }
95
+ current = "";
96
+ }
97
+ }
98
+ if (current.trim().length > 0) {
99
+ chunks.push(current);
100
+ }
101
+ return chunks;
102
+ }
103
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ /**
2
+ * knowledge-rag CLI — `doctor` (dependency health) and `debug <query>`
3
+ * (run the real zero-config pipeline and print the ranked breakdown).
4
+ * Mirrors the Sailor capability-CLI convention (see trace-store/src/cli.ts).
5
+ */
6
+ export {};
7
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
package/dist/cli.js ADDED
@@ -0,0 +1,26 @@
1
+ /**
2
+ * knowledge-rag CLI — `doctor` (dependency health) and `debug <query>`
3
+ * (run the real zero-config pipeline and print the ranked breakdown).
4
+ * Mirrors the Sailor capability-CLI convention (see trace-store/src/cli.ts).
5
+ */
6
+ import { doctor, getKnowledgeRag } from "./index";
7
+ const command = process.argv[2] ?? "doctor";
8
+ if (command === "doctor") {
9
+ process.stdout.write(`${JSON.stringify({ capability: "knowledge-rag", ...(await doctor()) }, null, 2)}\n`);
10
+ }
11
+ else if (command === "debug") {
12
+ const query = process.argv[3] ?? "what is the canvas capability";
13
+ const tenantId = "debug-tenant";
14
+ const kb = await getKnowledgeRag();
15
+ await kb.ingest({
16
+ id: "debug-doc",
17
+ tenantId,
18
+ text: "The canvas capability adds an interactive node-graph editor over the reel model, a multi-tenant hybrid RAG pipeline, and a CRDT collaboration layer.",
19
+ });
20
+ const hits = await kb.query({ query, tenantId, topK: 3 });
21
+ process.stdout.write(`${JSON.stringify({ capability: "knowledge-rag", query, hits }, null, 2)}\n`);
22
+ }
23
+ else {
24
+ process.stderr.write(`Unknown knowledge-rag command: ${command}\n`);
25
+ process.exitCode = 1;
26
+ }
@@ -0,0 +1,20 @@
1
+ import type { Embedder } from "./types";
2
+ export declare class LocalHashEmbedder implements Embedder {
3
+ readonly name = "local-hash";
4
+ private readonly dim;
5
+ constructor(dim?: number);
6
+ embed(texts: string[]): Promise<number[][]>;
7
+ private embedOne;
8
+ }
9
+ /**
10
+ * Wraps the Sailor provider abstraction (`@nebutra/agents` → Vercel AI SDK
11
+ * `embedMany`, with the LLM_EMBEDDING_FALLBACK_CHAIN). Imported lazily so the
12
+ * zero-config path never requires the AI SDK at module load.
13
+ */
14
+ export declare class ProviderEmbedder implements Embedder {
15
+ readonly name = "nebutra-agents";
16
+ private readonly model;
17
+ constructor(model?: string);
18
+ embed(texts: string[]): Promise<number[][]>;
19
+ }
20
+ //# sourceMappingURL=embedder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"embedder.d.ts","sourceRoot":"","sources":["../src/embedder.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAmBxC,qBAAa,iBAAkB,YAAW,QAAQ;IAChD,QAAQ,CAAC,IAAI,gBAAgB;IAC7B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;gBAEjB,GAAG,SAAM;IAWf,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;IAIjD,OAAO,CAAC,QAAQ;CA0BjB;AAED;;;;GAIG;AACH,qBAAa,gBAAiB,YAAW,QAAQ;IAC/C,QAAQ,CAAC,IAAI,oBAAoB;IACjC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqB;gBAE/B,KAAK,CAAC,EAAE,MAAM;IAIpB,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;CA0BlD"}
@@ -0,0 +1,106 @@
1
+ // =============================================================================
2
+ // @nebutra/knowledge-rag — Embedders
3
+ // =============================================================================
4
+ // - LocalHashEmbedder: deterministic, zero-config, NO network. Hashed
5
+ // bag-of-words (+ char bigrams) into a fixed-dim space, L2-normalised.
6
+ // Real (non-mock) signal: shared tokens raise cosine similarity, so
7
+ // semantically-overlapping texts genuinely cluster.
8
+ // - ProviderEmbedder: wraps @nebutra/agents `embedMany()` (Vercel AI SDK
9
+ // fallback chain) when real embeddings are configured.
10
+ // =============================================================================
11
+ import { KnowledgeRagError } from "./errors";
12
+ /** FNV-1a 32-bit hash — fast, deterministic, well-distributed. */
13
+ function fnv1a(input) {
14
+ let h = 0x811c9dc5;
15
+ for (let i = 0; i < input.length; i++) {
16
+ h ^= input.charCodeAt(i);
17
+ h = Math.imul(h, 0x01000193);
18
+ }
19
+ return h >>> 0;
20
+ }
21
+ function tokenize(text) {
22
+ return text
23
+ .toLowerCase()
24
+ .split(/[^a-z0-9]+/)
25
+ .filter((t) => t.length > 0);
26
+ }
27
+ export class LocalHashEmbedder {
28
+ name = "local-hash";
29
+ dim;
30
+ constructor(dim = 256) {
31
+ if (!Number.isInteger(dim) || dim <= 0) {
32
+ throw new KnowledgeRagError(`Invalid embedding dimension: ${dim}`, {
33
+ code: "E_EMBED_DIM",
34
+ suggestion: "Pass a positive integer dimension, e.g. new LocalHashEmbedder(256).",
35
+ });
36
+ }
37
+ this.dim = dim;
38
+ }
39
+ // eslint-disable-next-line @typescript-eslint/require-await
40
+ async embed(texts) {
41
+ return texts.map((t) => this.embedOne(t));
42
+ }
43
+ embedOne(text) {
44
+ const vec = new Array(this.dim).fill(0);
45
+ const tokens = tokenize(text);
46
+ for (const tok of tokens) {
47
+ // Signed feature hashing reduces collision bias.
48
+ const h = fnv1a(tok);
49
+ const idx = h % this.dim;
50
+ const sign = (h & 1) === 0 ? 1 : -1;
51
+ vec[idx] += sign;
52
+ // Character bigrams add sub-word locality (typo / morphology robustness).
53
+ for (let i = 0; i < tok.length - 1; i++) {
54
+ const bg = fnv1a(`#${tok.slice(i, i + 2)}`);
55
+ vec[bg % this.dim] += (bg & 1) === 0 ? 0.5 : -0.5;
56
+ }
57
+ }
58
+ let norm = 0;
59
+ for (const v of vec)
60
+ norm += v * v;
61
+ norm = Math.sqrt(norm);
62
+ if (norm === 0) {
63
+ // Empty / symbol-only text — deterministic unit vector on axis 0.
64
+ vec[0] = 1;
65
+ return vec;
66
+ }
67
+ for (let i = 0; i < vec.length; i++)
68
+ vec[i] /= norm;
69
+ return vec;
70
+ }
71
+ }
72
+ /**
73
+ * Wraps the Sailor provider abstraction (`@nebutra/agents` → Vercel AI SDK
74
+ * `embedMany`, with the LLM_EMBEDDING_FALLBACK_CHAIN). Imported lazily so the
75
+ * zero-config path never requires the AI SDK at module load.
76
+ */
77
+ export class ProviderEmbedder {
78
+ name = "nebutra-agents";
79
+ model;
80
+ constructor(model) {
81
+ this.model = model;
82
+ }
83
+ async embed(texts) {
84
+ try {
85
+ const { embedMany } = await import("@nebutra/agents");
86
+ const result = await embedMany(texts, this.model ? { model: this.model } : {});
87
+ const embeddings = result.embeddings;
88
+ if (!Array.isArray(embeddings) || embeddings.length !== texts.length) {
89
+ throw new KnowledgeRagError("Provider returned an unexpected embedding shape", {
90
+ code: "E_EMBED_SHAPE",
91
+ suggestion: "Verify the embedding model in @nebutra/agents config; or omit a custom embedder to fall back to LocalHashEmbedder.",
92
+ });
93
+ }
94
+ return embeddings;
95
+ }
96
+ catch (err) {
97
+ if (err instanceof KnowledgeRagError)
98
+ throw err;
99
+ throw new KnowledgeRagError(`Provider embedding failed: ${err?.message ?? String(err)}`, {
100
+ code: "E_EMBED_PROVIDER",
101
+ cause: err,
102
+ suggestion: "Set OPENAI_API_KEY (or OPENROUTER_API_KEY) for @nebutra/agents, or use the zero-config LocalHashEmbedder (default).",
103
+ });
104
+ }
105
+ }
106
+ }
@@ -0,0 +1,19 @@
1
+ export interface KnowledgeRagErrorOptions {
2
+ /** Actionable "how to fix" hint. Defaults to a generic message if omitted. */
3
+ suggestion?: string;
4
+ /** Stable machine-readable code (e.g. "E_TENANT_MISSING"). */
5
+ code?: string;
6
+ /** Underlying cause, preserved for logging. */
7
+ cause?: unknown;
8
+ }
9
+ /**
10
+ * The single error class for this package. Always has a non-empty
11
+ * `.suggestion`; serialises cleanly for structured logging.
12
+ */
13
+ export declare class KnowledgeRagError extends Error {
14
+ readonly suggestion: string;
15
+ readonly code: string;
16
+ constructor(message: string, options?: KnowledgeRagErrorOptions);
17
+ toJSON(): Record<string, unknown>;
18
+ }
19
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,wBAAwB;IACvC,8EAA8E;IAC9E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,8DAA8D;IAC9D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,+CAA+C;IAC/C,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAKD;;;GAGG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;IAC1C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAEV,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,wBAA6B;IAWnE,MAAM,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CAQlC"}
package/dist/errors.js ADDED
@@ -0,0 +1,33 @@
1
+ // =============================================================================
2
+ // @nebutra/knowledge-rag — Error type
3
+ // =============================================================================
4
+ // Every thrown error MUST carry an actionable `.suggestion` so callers always
5
+ // know how to fix it (DX acceptance criterion).
6
+ // =============================================================================
7
+ const DEFAULT_SUGGESTION = "Check the @nebutra/knowledge-rag README, or run getKnowledgeRag().doctor() for a structured health report.";
8
+ /**
9
+ * The single error class for this package. Always has a non-empty
10
+ * `.suggestion`; serialises cleanly for structured logging.
11
+ */
12
+ export class KnowledgeRagError extends Error {
13
+ suggestion;
14
+ code;
15
+ constructor(message, options = {}) {
16
+ super(message, options.cause === undefined ? undefined : { cause: options.cause });
17
+ this.name = "KnowledgeRagError";
18
+ this.suggestion =
19
+ options.suggestion && options.suggestion.trim().length > 0
20
+ ? options.suggestion
21
+ : DEFAULT_SUGGESTION;
22
+ this.code = options.code ?? "E_KNOWLEDGE_RAG";
23
+ Object.setPrototypeOf(this, KnowledgeRagError.prototype);
24
+ }
25
+ toJSON() {
26
+ return {
27
+ name: this.name,
28
+ message: this.message,
29
+ suggestion: this.suggestion,
30
+ code: this.code,
31
+ };
32
+ }
33
+ }
@@ -0,0 +1,24 @@
1
+ import type { DoctorReport, KnowledgeRag, KnowledgeRagConfig } from "./types";
2
+ /**
3
+ * Returns a process-wide KnowledgeRag instance. Pass a config to override any
4
+ * pluggable internal; the first non-empty config wins for the singleton.
5
+ * Construct a fresh isolated instance with `createKnowledgeRag()` instead.
6
+ */
7
+ export declare function getKnowledgeRag(config?: KnowledgeRagConfig): Promise<KnowledgeRag>;
8
+ /** Standalone health probe (also available as instance method). <3s. */
9
+ export declare function doctor(config?: KnowledgeRagConfig): Promise<DoctorReport>;
10
+ /** Reset the singleton (tests / re-config). */
11
+ export declare function resetKnowledgeRag(): void;
12
+ export { RecursiveCharChunker } from "./chunker";
13
+ export { LocalHashEmbedder, ProviderEmbedder } from "./embedder";
14
+ export { KnowledgeRagError } from "./errors";
15
+ export { SearchKeywordIndex } from "./keyword";
16
+ export { createKnowledgeRag } from "./pipeline";
17
+ export { IdentityReranker, LexicalOverlapReranker } from "./reranker";
18
+ export { cosineSimilarity, hybridBlend, normalizeScores } from "./scoring";
19
+ export { InMemoryVectorStore } from "./stores/memory";
20
+ export { PgvectorStore } from "./stores/pgvector";
21
+ export { createKnowledgeRagTool } from "./tool";
22
+ export type { Chunker, DoctorReport, Embedder, IngestDocument, IngestResult, KnowledgeChunk, KnowledgeRag, KnowledgeRagConfig, QueryInput, RankedChunk, Reranker, VectorStore, } from "./types";
23
+ export { IngestDocumentSchema, QueryInputSchema } from "./types";
24
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAI9E;;;;GAIG;AAEH,wBAAsB,eAAe,CAAC,MAAM,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,YAAY,CAAC,CAIxF;AAED,wEAAwE;AACxE,wBAAsB,MAAM,CAAC,MAAM,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,YAAY,CAAC,CAG/E;AAED,+CAA+C;AAC/C,wBAAgB,iBAAiB,IAAI,IAAI,CAExC;AAED,OAAO,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC;AACjD,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAE/C,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AACtE,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAC3E,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAElD,OAAO,EAAE,sBAAsB,EAAE,MAAM,QAAQ,CAAC;AAGhD,YAAY,EACV,OAAO,EACP,YAAY,EACZ,QAAQ,EACR,cAAc,EACd,YAAY,EACZ,cAAc,EACd,YAAY,EACZ,kBAAkB,EAClB,UAAU,EACV,WAAW,EACX,QAAQ,EACR,WAAW,GACZ,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,54 @@
1
+ // =============================================================================
2
+ // @nebutra/knowledge-rag — Public API
3
+ // =============================================================================
4
+ // Multi-tenant hybrid RAG: ingest → semantic chunk → embed → vector + keyword
5
+ // index → hybrid retrieval → optional rerank.
6
+ //
7
+ // Zero-config:
8
+ //
9
+ // import { getKnowledgeRag } from "@nebutra/knowledge-rag";
10
+ // const kb = await getKnowledgeRag();
11
+ // await kb.ingest({ id: "d1", tenantId: "org_a", text: "..." });
12
+ // const hits = await kb.query({ query: "...", tenantId: "org_a" });
13
+ //
14
+ // Defaults: in-memory vector store + deterministic local embedder, keyword
15
+ // leg auto-enabled only if a @nebutra/search backend is detected. NO env
16
+ // required. Every persisted record carries a tenantId.
17
+ // =============================================================================
18
+ import { createKnowledgeRag } from "./pipeline";
19
+ let singleton = null;
20
+ /**
21
+ * Returns a process-wide KnowledgeRag instance. Pass a config to override any
22
+ * pluggable internal; the first non-empty config wins for the singleton.
23
+ * Construct a fresh isolated instance with `createKnowledgeRag()` instead.
24
+ */
25
+ // eslint-disable-next-line @typescript-eslint/require-await
26
+ export async function getKnowledgeRag(config) {
27
+ if (config)
28
+ return createKnowledgeRag(config);
29
+ if (!singleton)
30
+ singleton = createKnowledgeRag();
31
+ return singleton;
32
+ }
33
+ /** Standalone health probe (also available as instance method). <3s. */
34
+ export async function doctor(config) {
35
+ const kb = await getKnowledgeRag(config);
36
+ return kb.doctor();
37
+ }
38
+ /** Reset the singleton (tests / re-config). */
39
+ export function resetKnowledgeRag() {
40
+ singleton = null;
41
+ }
42
+ export { RecursiveCharChunker } from "./chunker";
43
+ export { LocalHashEmbedder, ProviderEmbedder } from "./embedder";
44
+ export { KnowledgeRagError } from "./errors";
45
+ export { SearchKeywordIndex } from "./keyword";
46
+ // ── Building blocks (advanced / custom wiring) ───────────────────────────────
47
+ export { createKnowledgeRag } from "./pipeline";
48
+ export { IdentityReranker, LexicalOverlapReranker } from "./reranker";
49
+ export { cosineSimilarity, hybridBlend, normalizeScores } from "./scoring";
50
+ export { InMemoryVectorStore } from "./stores/memory";
51
+ export { PgvectorStore } from "./stores/pgvector";
52
+ // ── Tool factory (for @nebutra/agent-runtime / agents) ───────────────────────
53
+ export { createKnowledgeRagTool } from "./tool";
54
+ export { IngestDocumentSchema, QueryInputSchema } from "./types";
@@ -0,0 +1,43 @@
1
+ import type { KnowledgeChunk } from "./types";
2
+ export interface KeywordHit {
3
+ chunkId: string;
4
+ docId: string;
5
+ tenantId: string;
6
+ score: number;
7
+ }
8
+ export interface KeywordIndex {
9
+ index(chunks: KnowledgeChunk[]): Promise<void>;
10
+ search(tenantId: string, query: string, topK: number): Promise<KeywordHit[]>;
11
+ deleteByDoc(docId: string, tenantId: string): Promise<void>;
12
+ health(): Promise<{
13
+ ok: boolean;
14
+ detail: string;
15
+ }>;
16
+ }
17
+ /**
18
+ * KeywordIndex backed by @nebutra/search. Construct via
19
+ * `SearchKeywordIndex.tryCreate(indexName)` which returns `null` when no
20
+ * search backend is configured (so the pipeline can degrade to vector-only).
21
+ */
22
+ export declare class SearchKeywordIndex implements KeywordIndex {
23
+ private readonly provider;
24
+ private readonly indexName;
25
+ private constructor();
26
+ /**
27
+ * Returns a live keyword index, or `null` when no @nebutra/search backend
28
+ * is genuinely reachable. The default provider auto-detects to Meilisearch
29
+ * even with no env; it only fails on the first network call. So we probe
30
+ * with a real createIndex/search call and degrade to vector-only on any
31
+ * failure — the zero-config path must NEVER break because a search server
32
+ * happens to be absent.
33
+ */
34
+ static tryCreate(indexName: string): Promise<SearchKeywordIndex | null>;
35
+ index(chunks: KnowledgeChunk[]): Promise<void>;
36
+ search(tenantId: string, query: string, topK: number): Promise<KeywordHit[]>;
37
+ deleteByDoc(docId: string, tenantId: string): Promise<void>;
38
+ health(): Promise<{
39
+ ok: boolean;
40
+ detail: string;
41
+ }>;
42
+ }
43
+ //# sourceMappingURL=keyword.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keyword.d.ts","sourceRoot":"","sources":["../src/keyword.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,CAAC,MAAM,EAAE,cAAc,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC;IAC7E,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5D,MAAM,IAAI,OAAO,CAAC;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACpD;AAED;;;;GAIG;AACH,qBAAa,kBAAmB,YAAW,YAAY;IAErD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAM;IAC/B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IAGnC,OAAO;IAKP;;;;;;;OAOG;WACU,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC;IA0BvE,KAAK,CAAC,MAAM,EAAE,cAAc,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAa9C,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IA6B5E,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI3D,MAAM,IAAI,OAAO,CAAC;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CAWzD"}