@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/LICENSE +676 -0
- package/README.md +151 -0
- package/dist/chunker.d.ts +21 -0
- package/dist/chunker.d.ts.map +1 -0
- package/dist/chunker.js +103 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +26 -0
- package/dist/embedder.d.ts +20 -0
- package/dist/embedder.d.ts.map +1 -0
- package/dist/embedder.js +106 -0
- package/dist/errors.d.ts +19 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +33 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +54 -0
- package/dist/keyword.d.ts +43 -0
- package/dist/keyword.d.ts.map +1 -0
- package/dist/keyword.js +114 -0
- package/dist/pipeline.d.ts +4 -0
- package/dist/pipeline.d.ts.map +1 -0
- package/dist/pipeline.js +224 -0
- package/dist/reranker.d.ts +17 -0
- package/dist/reranker.d.ts.map +1 -0
- package/dist/reranker.js +48 -0
- package/dist/scoring.d.ts +16 -0
- package/dist/scoring.d.ts.map +1 -0
- package/dist/scoring.js +64 -0
- package/dist/stores/memory.d.ts +16 -0
- package/dist/stores/memory.d.ts.map +1 -0
- package/dist/stores/memory.js +43 -0
- package/dist/stores/pgvector.d.ts +31 -0
- package/dist/stores/pgvector.d.ts.map +1 -0
- package/dist/stores/pgvector.js +97 -0
- package/dist/tool.d.ts +30 -0
- package/dist/tool.d.ts.map +1 -0
- package/dist/tool.js +48 -0
- package/dist/types.d.ts +101 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +27 -0
- package/package.json +61 -0
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"}
|
package/dist/chunker.js
ADDED
|
@@ -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"}
|
package/dist/embedder.js
ADDED
|
@@ -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
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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"}
|