@kuralle-agents/rag 0.5.0 → 0.6.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 +1 -0
- package/dist/fs/KnowledgeFs.d.ts +58 -0
- package/dist/fs/KnowledgeFs.js +268 -0
- package/dist/fs/access.d.ts +7 -0
- package/dist/fs/access.js +12 -0
- package/dist/fs/index.d.ts +6 -0
- package/dist/fs/index.js +3 -0
- package/dist/fs/path-tree.d.ts +32 -0
- package/dist/fs/path-tree.js +157 -0
- package/dist/vectorStores/InMemoryVectorStore.d.ts +7 -0
- package/dist/vectorStores/InMemoryVectorStore.js +11 -0
- package/package.json +11 -3
package/README.md
CHANGED
|
@@ -23,6 +23,7 @@ Everything between a raw document and a grounded agent response: chunk documents
|
|
|
23
23
|
- **Retrievers** — `VectorRetriever`, `HybridRetriever`, `FusionRetriever`, `MultiHopRetriever`, `createLLMRetriever`
|
|
24
24
|
- **Rerankers** — `LLMReranker`, `CohereReranker`
|
|
25
25
|
- **Search** — `BM25Index` (keyword search for hybrid retrieval)
|
|
26
|
+
- **KnowledgeFs** — read-only `FileSystem` over a vector store (`@kuralle-agents/rag/fs`); see [guides/KNOWLEDGEFS.md](./guides/KNOWLEDGEFS.md)
|
|
26
27
|
- **Pipeline** — `RagPipeline`, `RetrievalQualityChecker`
|
|
27
28
|
- **Cache** — `RetrievalCache`, `TurnCache`, `PredictivePreFetcher`
|
|
28
29
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { CpOptions, FileSystem, FileSystemDirent, FsStat, MkdirOptions, RmOptions } from '@kuralle-agents/core';
|
|
2
|
+
import type { BM25Index } from '../search/BM25Index.js';
|
|
3
|
+
import type { VectorStoreCore } from '../types.js';
|
|
4
|
+
import type { KnowledgeAccessFilter } from './access.js';
|
|
5
|
+
export interface KnowledgeFsOptions {
|
|
6
|
+
store: VectorStoreCore;
|
|
7
|
+
indexName: string;
|
|
8
|
+
bm25?: BM25Index;
|
|
9
|
+
accessFilter?: KnowledgeAccessFilter;
|
|
10
|
+
manifestKey?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface KnowledgeSearchHit {
|
|
13
|
+
slug: string;
|
|
14
|
+
chunkIndex: number;
|
|
15
|
+
text: string;
|
|
16
|
+
}
|
|
17
|
+
export declare class KnowledgeFs implements FileSystem {
|
|
18
|
+
private readonly store;
|
|
19
|
+
private readonly indexName;
|
|
20
|
+
private readonly bm25?;
|
|
21
|
+
private readonly accessFilter?;
|
|
22
|
+
private readonly manifestKey;
|
|
23
|
+
private tree;
|
|
24
|
+
private chunksBySlug;
|
|
25
|
+
private readonly pageCache;
|
|
26
|
+
private initialized;
|
|
27
|
+
constructor(opts: KnowledgeFsOptions);
|
|
28
|
+
static open(opts: KnowledgeFsOptions): Promise<KnowledgeFs>;
|
|
29
|
+
init(): Promise<void>;
|
|
30
|
+
private assertReady;
|
|
31
|
+
private resolveCanonical;
|
|
32
|
+
private isFile;
|
|
33
|
+
private isDirectory;
|
|
34
|
+
private assertAccessible;
|
|
35
|
+
search(pattern: string, opts?: {
|
|
36
|
+
limit?: number;
|
|
37
|
+
path?: string;
|
|
38
|
+
}): Promise<KnowledgeSearchHit[]>;
|
|
39
|
+
readdir(path: string): Promise<string[]>;
|
|
40
|
+
readdirWithFileTypes(path: string): Promise<FileSystemDirent[]>;
|
|
41
|
+
exists(path: string): Promise<boolean>;
|
|
42
|
+
stat(path: string): Promise<FsStat>;
|
|
43
|
+
lstat(path: string): Promise<FsStat>;
|
|
44
|
+
readFile(path: string): Promise<string>;
|
|
45
|
+
readFileBytes(path: string): Promise<Uint8Array>;
|
|
46
|
+
writeFile(_path: string, _content: string): Promise<void>;
|
|
47
|
+
writeFileBytes(path: string, _content: Uint8Array): Promise<void>;
|
|
48
|
+
appendFile(path: string, _content: string | Uint8Array): Promise<void>;
|
|
49
|
+
mkdir(path: string, _options?: MkdirOptions): Promise<void>;
|
|
50
|
+
rm(path: string, _options?: RmOptions): Promise<void>;
|
|
51
|
+
cp(src: string, dest: string, _options?: CpOptions): Promise<void>;
|
|
52
|
+
mv(src: string, dest: string): Promise<void>;
|
|
53
|
+
symlink(_target: string, linkPath: string): Promise<void>;
|
|
54
|
+
readlink(path: string): Promise<string>;
|
|
55
|
+
resolvePath(base: string, path: string): string;
|
|
56
|
+
realpath(path: string): Promise<string>;
|
|
57
|
+
glob(pattern: string): Promise<string[]>;
|
|
58
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { slugAllowed } from './access.js';
|
|
2
|
+
import { PATH_TREE_MANIFEST_ID, allChunkRecords, buildPathTree, chunkRecordsFromEntries, createGlobMatcher, groupChunksBySlug, joinKnowledgePath, normalizeKnowledgePath, parsePathTreeManifest, pathUnderRoot, prunePathTree, resolveKnowledgePath, } from './path-tree.js';
|
|
3
|
+
function hasListEntries(store) {
|
|
4
|
+
return typeof store.listEntries === 'function';
|
|
5
|
+
}
|
|
6
|
+
function eroFs(op, path) {
|
|
7
|
+
return Object.assign(new Error(`EROFS: read-only knowledge filesystem, ${op} '${path}'`), { code: 'EROFS' });
|
|
8
|
+
}
|
|
9
|
+
function enoent(op, path) {
|
|
10
|
+
return Object.assign(new Error(`ENOENT: no such file or directory, ${op} '${path}'`), { code: 'ENOENT' });
|
|
11
|
+
}
|
|
12
|
+
function enotdir(op, path) {
|
|
13
|
+
return Object.assign(new Error(`ENOTDIR: not a directory, ${op} '${path}'`), {
|
|
14
|
+
code: 'ENOTDIR',
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
function eisdir(op, path) {
|
|
18
|
+
return Object.assign(new Error(`EISDIR: illegal operation on a directory, ${op} '${path}'`), { code: 'EISDIR' });
|
|
19
|
+
}
|
|
20
|
+
const utf8 = new TextEncoder();
|
|
21
|
+
export class KnowledgeFs {
|
|
22
|
+
store;
|
|
23
|
+
indexName;
|
|
24
|
+
bm25;
|
|
25
|
+
accessFilter;
|
|
26
|
+
manifestKey;
|
|
27
|
+
tree;
|
|
28
|
+
chunksBySlug;
|
|
29
|
+
pageCache = new Map();
|
|
30
|
+
initialized = false;
|
|
31
|
+
constructor(opts) {
|
|
32
|
+
this.store = opts.store;
|
|
33
|
+
this.indexName = opts.indexName;
|
|
34
|
+
this.bm25 = opts.bm25;
|
|
35
|
+
this.accessFilter = opts.accessFilter;
|
|
36
|
+
this.manifestKey = opts.manifestKey ?? PATH_TREE_MANIFEST_ID;
|
|
37
|
+
}
|
|
38
|
+
static async open(opts) {
|
|
39
|
+
const fs = new KnowledgeFs(opts);
|
|
40
|
+
await fs.init();
|
|
41
|
+
return fs;
|
|
42
|
+
}
|
|
43
|
+
async init() {
|
|
44
|
+
if (this.initialized)
|
|
45
|
+
return;
|
|
46
|
+
const filter = this.accessFilter?.vectorFilter;
|
|
47
|
+
const entries = await scanStoreEntries(this.store, this.indexName, filter);
|
|
48
|
+
const manifestEntry = entries.find((e) => e.id === this.manifestKey);
|
|
49
|
+
let slugs;
|
|
50
|
+
let chunkRecords;
|
|
51
|
+
if (manifestEntry?.document) {
|
|
52
|
+
slugs = parsePathTreeManifest(manifestEntry.document);
|
|
53
|
+
chunkRecords = chunkRecordsFromEntries(entries.filter((e) => e.id !== this.manifestKey));
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
chunkRecords = chunkRecordsFromEntries(entries);
|
|
57
|
+
if (chunkRecords.length === 0 && entries.length > 0) {
|
|
58
|
+
throw new Error('KnowledgeFs: store entries lack page/chunk_index metadata required for reassembly');
|
|
59
|
+
}
|
|
60
|
+
slugs = [...new Set(chunkRecords.map((c) => c.slug))];
|
|
61
|
+
}
|
|
62
|
+
const allow = (slug) => slugAllowed(slug, this.accessFilter);
|
|
63
|
+
slugs = slugs.filter(allow);
|
|
64
|
+
chunkRecords = chunkRecords.filter((c) => allow(c.slug));
|
|
65
|
+
this.tree = prunePathTree(buildPathTree(slugs), allow);
|
|
66
|
+
this.chunksBySlug = groupChunksBySlug(chunkRecords);
|
|
67
|
+
if (this.bm25) {
|
|
68
|
+
this.bm25.add(chunkRecords.map((r) => ({
|
|
69
|
+
id: `${r.slug}#${r.chunkIndex}`,
|
|
70
|
+
text: r.text,
|
|
71
|
+
})));
|
|
72
|
+
}
|
|
73
|
+
this.initialized = true;
|
|
74
|
+
}
|
|
75
|
+
assertReady() {
|
|
76
|
+
if (!this.initialized) {
|
|
77
|
+
throw new Error('KnowledgeFs: call KnowledgeFs.open() before using the filesystem');
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
resolveCanonical(path) {
|
|
81
|
+
return normalizeKnowledgePath(path);
|
|
82
|
+
}
|
|
83
|
+
isFile(path) {
|
|
84
|
+
return this.tree.files.has(path);
|
|
85
|
+
}
|
|
86
|
+
isDirectory(path) {
|
|
87
|
+
if (path === '/')
|
|
88
|
+
return true;
|
|
89
|
+
return this.tree.dirChildren.has(path);
|
|
90
|
+
}
|
|
91
|
+
assertAccessible(path, op) {
|
|
92
|
+
const canonical = this.resolveCanonical(path);
|
|
93
|
+
if (canonical.includes('\0'))
|
|
94
|
+
throw enoent(op, path);
|
|
95
|
+
if (canonical !== '/' && !this.isFile(canonical) && !this.isDirectory(canonical)) {
|
|
96
|
+
throw enoent(op, path);
|
|
97
|
+
}
|
|
98
|
+
return canonical;
|
|
99
|
+
}
|
|
100
|
+
async search(pattern, opts) {
|
|
101
|
+
this.assertReady();
|
|
102
|
+
const limit = opts?.limit ?? 50;
|
|
103
|
+
const root = opts?.path ? normalizeKnowledgePath(opts.path) : '/';
|
|
104
|
+
let records = allChunkRecords(this.chunksBySlug).filter((r) => pathUnderRoot(r.slug, root));
|
|
105
|
+
if (this.bm25) {
|
|
106
|
+
const ranked = this.bm25.search(pattern, limit);
|
|
107
|
+
const candidateIds = new Set(ranked.map((hit) => hit.id));
|
|
108
|
+
return records
|
|
109
|
+
.filter((r) => candidateIds.has(`${r.slug}#${r.chunkIndex}`))
|
|
110
|
+
.slice(0, limit)
|
|
111
|
+
.map((r) => ({
|
|
112
|
+
slug: r.slug,
|
|
113
|
+
chunkIndex: r.chunkIndex,
|
|
114
|
+
text: r.text,
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
117
|
+
let re;
|
|
118
|
+
try {
|
|
119
|
+
re = new RegExp(pattern, 'i');
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
throw new Error(`EINVAL: invalid search pattern '${pattern}'`);
|
|
123
|
+
}
|
|
124
|
+
const hits = [];
|
|
125
|
+
for (const record of records) {
|
|
126
|
+
if (re.test(record.text)) {
|
|
127
|
+
hits.push({
|
|
128
|
+
slug: record.slug,
|
|
129
|
+
chunkIndex: record.chunkIndex,
|
|
130
|
+
text: record.text,
|
|
131
|
+
});
|
|
132
|
+
if (hits.length >= limit)
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return hits;
|
|
137
|
+
}
|
|
138
|
+
async readdir(path) {
|
|
139
|
+
return (await this.readdirWithFileTypes(path)).map((d) => d.name);
|
|
140
|
+
}
|
|
141
|
+
async readdirWithFileTypes(path) {
|
|
142
|
+
this.assertReady();
|
|
143
|
+
const canonical = this.assertAccessible(path, 'scandir');
|
|
144
|
+
if (!this.isDirectory(canonical))
|
|
145
|
+
throw enotdir('scandir', path);
|
|
146
|
+
const names = this.tree.dirChildren.get(canonical) ?? [];
|
|
147
|
+
const out = [];
|
|
148
|
+
for (const name of names) {
|
|
149
|
+
const child = joinKnowledgePath(canonical, name);
|
|
150
|
+
const type = this.isDirectory(child) ? 'directory' : 'file';
|
|
151
|
+
out.push({ name, type });
|
|
152
|
+
}
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
async exists(path) {
|
|
156
|
+
this.assertReady();
|
|
157
|
+
if (path.includes('\0'))
|
|
158
|
+
return false;
|
|
159
|
+
const canonical = this.resolveCanonical(path);
|
|
160
|
+
if (canonical === '/')
|
|
161
|
+
return true;
|
|
162
|
+
return this.isFile(canonical) || this.isDirectory(canonical);
|
|
163
|
+
}
|
|
164
|
+
async stat(path) {
|
|
165
|
+
this.assertReady();
|
|
166
|
+
const canonical = this.assertAccessible(path, 'stat');
|
|
167
|
+
if (canonical === '/') {
|
|
168
|
+
return { type: 'directory', size: 0, mtime: new Date(0) };
|
|
169
|
+
}
|
|
170
|
+
if (this.isDirectory(canonical)) {
|
|
171
|
+
return { type: 'directory', size: 0, mtime: new Date(0) };
|
|
172
|
+
}
|
|
173
|
+
const cached = this.pageCache.get(canonical);
|
|
174
|
+
if (cached !== undefined) {
|
|
175
|
+
return { type: 'file', size: utf8.encode(cached).length, mtime: new Date(0) };
|
|
176
|
+
}
|
|
177
|
+
const chunks = this.chunksBySlug.get(canonical);
|
|
178
|
+
const size = chunks?.reduce((n, c) => n + utf8.encode(c.text).length, 0) ?? 0;
|
|
179
|
+
return { type: 'file', size, mtime: new Date(0) };
|
|
180
|
+
}
|
|
181
|
+
async lstat(path) {
|
|
182
|
+
return this.stat(path);
|
|
183
|
+
}
|
|
184
|
+
async readFile(path) {
|
|
185
|
+
this.assertReady();
|
|
186
|
+
const canonical = this.assertAccessible(path, 'open');
|
|
187
|
+
if (this.isDirectory(canonical))
|
|
188
|
+
throw eisdir('read', path);
|
|
189
|
+
if (!this.isFile(canonical))
|
|
190
|
+
throw enoent('open', path);
|
|
191
|
+
const cached = this.pageCache.get(canonical);
|
|
192
|
+
if (cached !== undefined)
|
|
193
|
+
return cached;
|
|
194
|
+
const chunks = this.chunksBySlug.get(canonical);
|
|
195
|
+
if (!chunks || chunks.length === 0) {
|
|
196
|
+
throw enoent('open', path);
|
|
197
|
+
}
|
|
198
|
+
const page = chunks.map((c) => c.text).join('');
|
|
199
|
+
this.pageCache.set(canonical, page);
|
|
200
|
+
return page;
|
|
201
|
+
}
|
|
202
|
+
async readFileBytes(path) {
|
|
203
|
+
return utf8.encode(await this.readFile(path));
|
|
204
|
+
}
|
|
205
|
+
async writeFile(_path, _content) {
|
|
206
|
+
throw eroFs('write', _path);
|
|
207
|
+
}
|
|
208
|
+
async writeFileBytes(path, _content) {
|
|
209
|
+
throw eroFs('write', path);
|
|
210
|
+
}
|
|
211
|
+
async appendFile(path, _content) {
|
|
212
|
+
throw eroFs('append', path);
|
|
213
|
+
}
|
|
214
|
+
async mkdir(path, _options) {
|
|
215
|
+
throw eroFs('mkdir', path);
|
|
216
|
+
}
|
|
217
|
+
async rm(path, _options) {
|
|
218
|
+
throw eroFs('rm', path);
|
|
219
|
+
}
|
|
220
|
+
async cp(src, dest, _options) {
|
|
221
|
+
throw eroFs('cp', `${src} -> ${dest}`);
|
|
222
|
+
}
|
|
223
|
+
async mv(src, dest) {
|
|
224
|
+
throw eroFs('rename', `${src} -> ${dest}`);
|
|
225
|
+
}
|
|
226
|
+
async symlink(_target, linkPath) {
|
|
227
|
+
throw eroFs('symlink', linkPath);
|
|
228
|
+
}
|
|
229
|
+
async readlink(path) {
|
|
230
|
+
throw enoent('readlink', path);
|
|
231
|
+
}
|
|
232
|
+
resolvePath(base, path) {
|
|
233
|
+
return resolveKnowledgePath(base, path);
|
|
234
|
+
}
|
|
235
|
+
async realpath(path) {
|
|
236
|
+
this.assertReady();
|
|
237
|
+
const canonical = this.assertAccessible(path, 'realpath');
|
|
238
|
+
return canonical;
|
|
239
|
+
}
|
|
240
|
+
async glob(pattern) {
|
|
241
|
+
this.assertReady();
|
|
242
|
+
const re = createGlobMatcher(pattern);
|
|
243
|
+
const hits = [];
|
|
244
|
+
for (const file of this.tree.files) {
|
|
245
|
+
if (re.test(file))
|
|
246
|
+
hits.push(file);
|
|
247
|
+
}
|
|
248
|
+
return hits.sort();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
async function scanStoreEntries(store, indexName, filter) {
|
|
252
|
+
if (hasListEntries(store)) {
|
|
253
|
+
return store.listEntries(indexName, { filter });
|
|
254
|
+
}
|
|
255
|
+
const stats = await store.describeIndex(indexName);
|
|
256
|
+
const zeros = new Array(stats.dimension).fill(0);
|
|
257
|
+
const results = await store.query(indexName, {
|
|
258
|
+
queryVector: zeros,
|
|
259
|
+
topK: Math.max(stats.count, 1),
|
|
260
|
+
filter,
|
|
261
|
+
includeDocuments: true,
|
|
262
|
+
});
|
|
263
|
+
return results.map((r) => ({
|
|
264
|
+
id: r.id,
|
|
265
|
+
metadata: r.metadata,
|
|
266
|
+
document: r.document,
|
|
267
|
+
}));
|
|
268
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { VectorFilter } from '../types.js';
|
|
2
|
+
export interface KnowledgeAccessFilter {
|
|
3
|
+
vectorFilter?: VectorFilter;
|
|
4
|
+
allowSlug?(slug: string): boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare function combineFilters(a?: VectorFilter, b?: VectorFilter): VectorFilter | undefined;
|
|
7
|
+
export declare function slugAllowed(slug: string, filter?: KnowledgeAccessFilter): boolean;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type { KnowledgeAccessFilter } from './access.js';
|
|
2
|
+
export { combineFilters, slugAllowed } from './access.js';
|
|
3
|
+
export { KnowledgeFs } from './KnowledgeFs.js';
|
|
4
|
+
export type { KnowledgeFsOptions, KnowledgeSearchHit } from './KnowledgeFs.js';
|
|
5
|
+
export { PAGE_META_KEY, CHUNK_INDEX_META_KEY, PATH_TREE_MANIFEST_ID, } from './path-tree.js';
|
|
6
|
+
export type { ChunkRecord, PathTreeData, PathTreeManifest } from './path-tree.js';
|
package/dist/fs/index.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export declare const PAGE_META_KEY = "page";
|
|
2
|
+
export declare const CHUNK_INDEX_META_KEY = "chunk_index";
|
|
3
|
+
export declare const PATH_TREE_MANIFEST_ID = "__path_tree__";
|
|
4
|
+
export type PathTreeManifest = Record<string, {
|
|
5
|
+
isPublic?: boolean;
|
|
6
|
+
groups?: string[];
|
|
7
|
+
}>;
|
|
8
|
+
export interface ChunkRecord {
|
|
9
|
+
id: string;
|
|
10
|
+
slug: string;
|
|
11
|
+
chunkIndex: number;
|
|
12
|
+
text: string;
|
|
13
|
+
}
|
|
14
|
+
export interface PathTreeData {
|
|
15
|
+
files: Set<string>;
|
|
16
|
+
dirChildren: Map<string, string[]>;
|
|
17
|
+
}
|
|
18
|
+
export declare function normalizeKnowledgePath(path: string): string;
|
|
19
|
+
export declare function joinKnowledgePath(parent: string, child: string): string;
|
|
20
|
+
export declare function resolveKnowledgePath(base: string, path: string): string;
|
|
21
|
+
export declare function pathUnderRoot(path: string, root: string): boolean;
|
|
22
|
+
export declare function buildPathTree(slugs: Iterable<string>): PathTreeData;
|
|
23
|
+
export declare function prunePathTree(tree: PathTreeData, allow: (slug: string) => boolean): PathTreeData;
|
|
24
|
+
export declare function createGlobMatcher(pattern: string): RegExp;
|
|
25
|
+
export declare function chunkRecordsFromEntries(entries: Array<{
|
|
26
|
+
id: string;
|
|
27
|
+
metadata?: Record<string, unknown>;
|
|
28
|
+
document?: string;
|
|
29
|
+
}>): ChunkRecord[];
|
|
30
|
+
export declare function groupChunksBySlug(records: ChunkRecord[]): Map<string, ChunkRecord[]>;
|
|
31
|
+
export declare function parsePathTreeManifest(raw: string): string[];
|
|
32
|
+
export declare function allChunkRecords(map: Map<string, ChunkRecord[]>): ChunkRecord[];
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
export const PAGE_META_KEY = 'page';
|
|
2
|
+
export const CHUNK_INDEX_META_KEY = 'chunk_index';
|
|
3
|
+
export const PATH_TREE_MANIFEST_ID = '__path_tree__';
|
|
4
|
+
export function normalizeKnowledgePath(path) {
|
|
5
|
+
if (!path || path === '/')
|
|
6
|
+
return '/';
|
|
7
|
+
let normalized = path.endsWith('/') && path !== '/' ? path.slice(0, -1) : path;
|
|
8
|
+
if (!normalized.startsWith('/')) {
|
|
9
|
+
normalized = `/${normalized}`;
|
|
10
|
+
}
|
|
11
|
+
const parts = normalized.split('/').filter((p) => p && p !== '.');
|
|
12
|
+
const resolved = [];
|
|
13
|
+
for (const part of parts) {
|
|
14
|
+
if (part === '..') {
|
|
15
|
+
resolved.pop();
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
resolved.push(part);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return resolved.length === 0 ? '/' : `/${resolved.join('/')}`;
|
|
22
|
+
}
|
|
23
|
+
export function joinKnowledgePath(parent, child) {
|
|
24
|
+
const base = normalizeKnowledgePath(parent);
|
|
25
|
+
if (child.startsWith('/'))
|
|
26
|
+
return normalizeKnowledgePath(child);
|
|
27
|
+
return normalizeKnowledgePath(base === '/' ? `/${child}` : `${base}/${child}`);
|
|
28
|
+
}
|
|
29
|
+
export function resolveKnowledgePath(base, path) {
|
|
30
|
+
if (path.startsWith('/'))
|
|
31
|
+
return normalizeKnowledgePath(path);
|
|
32
|
+
return joinKnowledgePath(base, path);
|
|
33
|
+
}
|
|
34
|
+
export function pathUnderRoot(path, root) {
|
|
35
|
+
const p = normalizeKnowledgePath(path);
|
|
36
|
+
const r = normalizeKnowledgePath(root);
|
|
37
|
+
if (r === '/')
|
|
38
|
+
return true;
|
|
39
|
+
return p === r || p.startsWith(`${r}/`);
|
|
40
|
+
}
|
|
41
|
+
export function buildPathTree(slugs) {
|
|
42
|
+
const files = new Set();
|
|
43
|
+
const dirChildren = new Map();
|
|
44
|
+
const addChild = (dir, name) => {
|
|
45
|
+
const normalizedDir = normalizeKnowledgePath(dir);
|
|
46
|
+
if (!dirChildren.has(normalizedDir)) {
|
|
47
|
+
dirChildren.set(normalizedDir, new Set());
|
|
48
|
+
}
|
|
49
|
+
dirChildren.get(normalizedDir).add(name);
|
|
50
|
+
};
|
|
51
|
+
for (const raw of slugs) {
|
|
52
|
+
const slug = normalizeKnowledgePath(raw);
|
|
53
|
+
if (slug === '/')
|
|
54
|
+
continue;
|
|
55
|
+
files.add(slug);
|
|
56
|
+
const parts = slug.slice(1).split('/');
|
|
57
|
+
let current = '/';
|
|
58
|
+
for (let i = 0; i < parts.length; i++) {
|
|
59
|
+
addChild(current, parts[i]);
|
|
60
|
+
if (i < parts.length - 1) {
|
|
61
|
+
current = joinKnowledgePath(current, parts[i]);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const sorted = new Map();
|
|
66
|
+
for (const [dir, children] of dirChildren) {
|
|
67
|
+
sorted.set(dir, [...children].sort());
|
|
68
|
+
}
|
|
69
|
+
return { files, dirChildren: sorted };
|
|
70
|
+
}
|
|
71
|
+
export function prunePathTree(tree, allow) {
|
|
72
|
+
const keptFiles = new Set();
|
|
73
|
+
for (const file of tree.files) {
|
|
74
|
+
if (allow(file))
|
|
75
|
+
keptFiles.add(file);
|
|
76
|
+
}
|
|
77
|
+
return buildPathTree(keptFiles);
|
|
78
|
+
}
|
|
79
|
+
export function createGlobMatcher(pattern) {
|
|
80
|
+
let i = 0;
|
|
81
|
+
let re = '^';
|
|
82
|
+
while (i < pattern.length) {
|
|
83
|
+
const ch = pattern[i];
|
|
84
|
+
if (ch === '*') {
|
|
85
|
+
if (pattern[i + 1] === '*') {
|
|
86
|
+
i += 2;
|
|
87
|
+
if (pattern[i] === '/') {
|
|
88
|
+
re += '(?:.+/)?';
|
|
89
|
+
i++;
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
re += '.*';
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
re += '[^/]*';
|
|
97
|
+
i++;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
else if (ch === '?') {
|
|
101
|
+
re += '[^/]';
|
|
102
|
+
i++;
|
|
103
|
+
}
|
|
104
|
+
else if ('.+^${}()|[]\\'.includes(ch)) {
|
|
105
|
+
re += `\\${ch}`;
|
|
106
|
+
i++;
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
re += ch;
|
|
110
|
+
i++;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
re += '$';
|
|
114
|
+
return new RegExp(re);
|
|
115
|
+
}
|
|
116
|
+
export function chunkRecordsFromEntries(entries) {
|
|
117
|
+
const records = [];
|
|
118
|
+
for (const entry of entries) {
|
|
119
|
+
const meta = entry.metadata ?? {};
|
|
120
|
+
const page = meta[PAGE_META_KEY];
|
|
121
|
+
const chunkIndex = meta[CHUNK_INDEX_META_KEY];
|
|
122
|
+
if (typeof page !== 'string' || page.length === 0)
|
|
123
|
+
continue;
|
|
124
|
+
if (typeof chunkIndex !== 'number' || !Number.isFinite(chunkIndex))
|
|
125
|
+
continue;
|
|
126
|
+
const slug = normalizeKnowledgePath(page);
|
|
127
|
+
records.push({
|
|
128
|
+
id: entry.id,
|
|
129
|
+
slug,
|
|
130
|
+
chunkIndex,
|
|
131
|
+
text: entry.document ?? '',
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return records;
|
|
135
|
+
}
|
|
136
|
+
export function groupChunksBySlug(records) {
|
|
137
|
+
const map = new Map();
|
|
138
|
+
for (const record of records) {
|
|
139
|
+
const list = map.get(record.slug) ?? [];
|
|
140
|
+
list.push(record);
|
|
141
|
+
map.set(record.slug, list);
|
|
142
|
+
}
|
|
143
|
+
for (const [, list] of map) {
|
|
144
|
+
list.sort((a, b) => a.chunkIndex - b.chunkIndex);
|
|
145
|
+
}
|
|
146
|
+
return map;
|
|
147
|
+
}
|
|
148
|
+
export function parsePathTreeManifest(raw) {
|
|
149
|
+
const parsed = JSON.parse(raw);
|
|
150
|
+
return Object.keys(parsed).map((k) => normalizeKnowledgePath(k.startsWith('/') ? k : `/${k}`));
|
|
151
|
+
}
|
|
152
|
+
export function allChunkRecords(map) {
|
|
153
|
+
const out = [];
|
|
154
|
+
for (const list of map.values())
|
|
155
|
+
out.push(...list);
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
@@ -8,6 +8,13 @@ export declare class InMemoryVectorStore implements VectorStoreCore, VectorStore
|
|
|
8
8
|
private indexes;
|
|
9
9
|
createIndex(params: CreateIndexParams): Promise<void>;
|
|
10
10
|
upsert(indexName: string, entries: VectorEntry[]): Promise<void>;
|
|
11
|
+
listEntries(indexName: string, params?: {
|
|
12
|
+
filter?: VectorFilter;
|
|
13
|
+
}): Promise<Array<{
|
|
14
|
+
id: string;
|
|
15
|
+
metadata?: Record<string, unknown>;
|
|
16
|
+
document?: string;
|
|
17
|
+
}>>;
|
|
11
18
|
query(indexName: string, params: VectorQueryParams): Promise<VectorQueryResult[]>;
|
|
12
19
|
listIndexes(): Promise<string[]>;
|
|
13
20
|
deleteIndex(indexName: string): Promise<void>;
|
|
@@ -25,6 +25,17 @@ export class InMemoryVectorStore {
|
|
|
25
25
|
index.entries.set(entry.id, { ...entry });
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
+
async listEntries(indexName, params) {
|
|
29
|
+
const index = this.getIndex(indexName);
|
|
30
|
+
const out = [];
|
|
31
|
+
for (const [id, entry] of index.entries) {
|
|
32
|
+
if (params?.filter && !matchFilter(entry.metadata ?? {}, params.filter)) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
out.push({ id, metadata: entry.metadata, document: entry.document });
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
28
39
|
async query(indexName, params) {
|
|
29
40
|
const index = this.getIndex(indexName);
|
|
30
41
|
const topK = params.topK ?? 10;
|
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"url": "git+https://github.com/kuralle/kuralle-agents.git",
|
|
7
7
|
"directory": "packages/kuralle-rag"
|
|
8
8
|
},
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.6.0",
|
|
10
10
|
"description": "RAG primitives for Kuralle",
|
|
11
11
|
"type": "module",
|
|
12
12
|
"main": "dist/index.js",
|
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
"types": "./dist/index.d.ts",
|
|
17
17
|
"default": "./dist/index.js"
|
|
18
18
|
},
|
|
19
|
+
"./fs": {
|
|
20
|
+
"types": "./dist/fs/index.d.ts",
|
|
21
|
+
"default": "./dist/fs/index.js"
|
|
22
|
+
},
|
|
19
23
|
"./filters": {
|
|
20
24
|
"types": "./dist/filters/index.d.ts",
|
|
21
25
|
"default": "./dist/filters/index.js"
|
|
@@ -25,6 +29,9 @@
|
|
|
25
29
|
"default": "./dist/vectorStores/testing.js"
|
|
26
30
|
}
|
|
27
31
|
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@kuralle-agents/core": "0.6.0"
|
|
34
|
+
},
|
|
28
35
|
"peerDependencies": {
|
|
29
36
|
"ai": "^6.0.0",
|
|
30
37
|
"zod": "^3.0.0"
|
|
@@ -34,7 +41,8 @@
|
|
|
34
41
|
"ai": "^6.0.0",
|
|
35
42
|
"bun-types": "^1.3.0",
|
|
36
43
|
"typescript": "^5.3.0",
|
|
37
|
-
"zod": "^3.23.0"
|
|
44
|
+
"zod": "^3.23.0",
|
|
45
|
+
"@kuralle-agents/fs": "0.6.0"
|
|
38
46
|
},
|
|
39
47
|
"publishConfig": {
|
|
40
48
|
"access": "public"
|
|
@@ -47,7 +55,7 @@
|
|
|
47
55
|
"prebuild": "rm -rf dist",
|
|
48
56
|
"build": "tsc -p tsconfig.json",
|
|
49
57
|
"clean": "rm -rf dist",
|
|
50
|
-
"test": "bun test test/chunkers-tokens.test.ts test/vectorStores test/filters test/e2e-loaders.test.ts",
|
|
58
|
+
"test": "bun test test/chunkers-tokens.test.ts test/vectorStores test/filters test/e2e-loaders.test.ts test/knowledgefs.test.ts test/knowledgefs-grep.test.ts test/knowledgefs-rbac.test.ts test/knowledgefs-agent.test.ts",
|
|
51
59
|
"test:live": "bun test test/e2e-tasktype.test.ts test/e2e-real.test.ts"
|
|
52
60
|
}
|
|
53
61
|
}
|