@terronex/aifbin-recall 0.1.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/CONTRIBUTING.md +65 -0
- package/LICENSE +21 -0
- package/NOTICE +36 -0
- package/README.md +250 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +182 -0
- package/dist/cli.js.map +1 -0
- package/dist/db.d.ts +29 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +252 -0
- package/dist/db.js.map +1 -0
- package/dist/embedder.d.ts +47 -0
- package/dist/embedder.d.ts.map +1 -0
- package/dist/embedder.js +152 -0
- package/dist/embedder.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +45 -0
- package/dist/index.js.map +1 -0
- package/dist/indexer.d.ts +34 -0
- package/dist/indexer.d.ts.map +1 -0
- package/dist/indexer.js +246 -0
- package/dist/indexer.js.map +1 -0
- package/dist/mcp.d.ts +7 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +207 -0
- package/dist/mcp.js.map +1 -0
- package/dist/search.d.ts +27 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +159 -0
- package/dist/search.js.map +1 -0
- package/dist/server.d.ts +13 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +250 -0
- package/dist/server.js.map +1 -0
- package/dist/types.d.ts +79 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +20 -0
- package/dist/types.js.map +1 -0
- package/package.json +64 -0
- package/src/cli.ts +195 -0
- package/src/db.ts +295 -0
- package/src/embedder.ts +175 -0
- package/src/index.ts +46 -0
- package/src/indexer.ts +272 -0
- package/src/mcp.ts +244 -0
- package/src/search.ts +201 -0
- package/src/server.ts +270 -0
- package/src/types.ts +103 -0
- package/tsconfig.json +20 -0
package/src/server.ts
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP server for AIF-BIN Recall
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import express, { Request, Response, NextFunction } from 'express';
|
|
6
|
+
import cors from 'cors';
|
|
7
|
+
import type { ServerConfig, SearchOptions } from './types.js';
|
|
8
|
+
import { DEFAULT_CONFIG } from './types.js';
|
|
9
|
+
import { EngramDB } from './db.js';
|
|
10
|
+
import { SearchEngine } from './search.js';
|
|
11
|
+
import { Indexer } from './indexer.js';
|
|
12
|
+
import { Embedder, type EmbeddingModelName } from './embedder.js';
|
|
13
|
+
|
|
14
|
+
export interface ServerOptions {
|
|
15
|
+
db: EngramDB;
|
|
16
|
+
config?: Partial<ServerConfig>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function createServer(options: ServerOptions): express.Application {
|
|
20
|
+
const { db, config } = options;
|
|
21
|
+
const serverConfig = { ...DEFAULT_CONFIG.server, ...config };
|
|
22
|
+
|
|
23
|
+
const app = express();
|
|
24
|
+
const search = new SearchEngine(db);
|
|
25
|
+
const indexer = new Indexer(db);
|
|
26
|
+
|
|
27
|
+
// Middleware
|
|
28
|
+
app.use(cors());
|
|
29
|
+
app.use(express.json({ limit: '10mb' }));
|
|
30
|
+
|
|
31
|
+
// Health check
|
|
32
|
+
app.get('/health', (_req: Request, res: Response) => {
|
|
33
|
+
res.json({ status: 'ok', version: '0.1.0' });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// List collections
|
|
37
|
+
app.get('/collections', (_req: Request, res: Response) => {
|
|
38
|
+
try {
|
|
39
|
+
const collections = db.listCollections();
|
|
40
|
+
res.json({ collections });
|
|
41
|
+
} catch (err) {
|
|
42
|
+
res.status(500).json({ error: String(err) });
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Get collection
|
|
47
|
+
app.get('/collections/:name', (req: Request, res: Response) => {
|
|
48
|
+
try {
|
|
49
|
+
const collection = db.getCollection(req.params.name);
|
|
50
|
+
if (!collection) {
|
|
51
|
+
res.status(404).json({ error: 'Collection not found' });
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
res.json(collection);
|
|
55
|
+
} catch (err) {
|
|
56
|
+
res.status(500).json({ error: String(err) });
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Create collection
|
|
61
|
+
app.post('/collections/:name', (req: Request, res: Response) => {
|
|
62
|
+
try {
|
|
63
|
+
const { description } = req.body || {};
|
|
64
|
+
const existing = db.getCollection(req.params.name);
|
|
65
|
+
if (existing) {
|
|
66
|
+
res.json(existing);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const collection = db.createCollection(req.params.name, description);
|
|
70
|
+
res.status(201).json(collection);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
res.status(500).json({ error: String(err) });
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Delete collection
|
|
77
|
+
app.delete('/collections/:name', (req: Request, res: Response) => {
|
|
78
|
+
try {
|
|
79
|
+
const deleted = db.deleteCollection(req.params.name);
|
|
80
|
+
if (!deleted) {
|
|
81
|
+
res.status(404).json({ error: 'Collection not found' });
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
res.json({ deleted: true });
|
|
85
|
+
} catch (err) {
|
|
86
|
+
res.status(500).json({ error: String(err) });
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Lazy-loaded embedder for text queries
|
|
91
|
+
let embedder: Embedder | null = null;
|
|
92
|
+
|
|
93
|
+
async function getEmbedder(model?: EmbeddingModelName): Promise<Embedder> {
|
|
94
|
+
if (!embedder) {
|
|
95
|
+
embedder = new Embedder(model || 'minilm');
|
|
96
|
+
await embedder.init();
|
|
97
|
+
}
|
|
98
|
+
return embedder;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Search - accepts either embedding array OR text query (will embed automatically)
|
|
102
|
+
app.post('/search', async (req: Request, res: Response) => {
|
|
103
|
+
try {
|
|
104
|
+
const { embedding, query, text, collection, limit, threshold, hybridWeight, model } = req.body;
|
|
105
|
+
|
|
106
|
+
// Use 'query' or 'text' for the search text
|
|
107
|
+
const queryText = query || text;
|
|
108
|
+
|
|
109
|
+
// Get or generate embedding
|
|
110
|
+
let queryEmbedding: number[];
|
|
111
|
+
if (embedding && Array.isArray(embedding)) {
|
|
112
|
+
queryEmbedding = embedding;
|
|
113
|
+
} else if (queryText) {
|
|
114
|
+
// Auto-embed the query text
|
|
115
|
+
const emb = await getEmbedder(model);
|
|
116
|
+
queryEmbedding = await emb.embed(queryText);
|
|
117
|
+
} else {
|
|
118
|
+
res.status(400).json({
|
|
119
|
+
error: 'Either "query" (text) or "embedding" (array) required',
|
|
120
|
+
hint: 'Send { "query": "your search text" } for automatic embedding',
|
|
121
|
+
});
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const options: SearchOptions = {
|
|
126
|
+
collection,
|
|
127
|
+
limit: limit || 10,
|
|
128
|
+
threshold: threshold || 0,
|
|
129
|
+
hybridWeight: hybridWeight ?? 0.7,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
let results;
|
|
133
|
+
if (queryText && (hybridWeight ?? 0.7) < 1.0) {
|
|
134
|
+
results = await search.hybridSearch(queryEmbedding, queryText, options);
|
|
135
|
+
} else {
|
|
136
|
+
results = await search.search(queryEmbedding, options);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
res.json({
|
|
140
|
+
results: results.map(r => ({
|
|
141
|
+
id: r.chunk.id,
|
|
142
|
+
text: r.chunk.text,
|
|
143
|
+
score: r.score,
|
|
144
|
+
vectorScore: r.vectorScore,
|
|
145
|
+
keywordScore: r.keywordScore,
|
|
146
|
+
sourceFile: r.chunk.sourceFile,
|
|
147
|
+
chunkIndex: r.chunk.chunkIndex,
|
|
148
|
+
metadata: r.chunk.metadata,
|
|
149
|
+
})),
|
|
150
|
+
});
|
|
151
|
+
} catch (err) {
|
|
152
|
+
res.status(500).json({ error: String(err) });
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// GET search - simple text query
|
|
157
|
+
app.get('/search', async (req: Request, res: Response) => {
|
|
158
|
+
try {
|
|
159
|
+
const { q, query, collection, limit } = req.query;
|
|
160
|
+
const queryText = (q || query) as string;
|
|
161
|
+
|
|
162
|
+
if (!queryText) {
|
|
163
|
+
res.status(400).json({
|
|
164
|
+
error: 'Query parameter "q" or "query" required',
|
|
165
|
+
example: '/search?q=your+search+text&collection=myproject',
|
|
166
|
+
});
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Embed the query
|
|
171
|
+
const emb = await getEmbedder();
|
|
172
|
+
const queryEmbedding = await emb.embed(queryText);
|
|
173
|
+
|
|
174
|
+
const options: SearchOptions = {
|
|
175
|
+
collection: collection as string,
|
|
176
|
+
limit: limit ? parseInt(limit as string, 10) : 10,
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const results = await search.hybridSearch(queryEmbedding, queryText, options);
|
|
180
|
+
|
|
181
|
+
res.json({
|
|
182
|
+
query: queryText,
|
|
183
|
+
results: results.map(r => ({
|
|
184
|
+
id: r.chunk.id,
|
|
185
|
+
text: r.chunk.text,
|
|
186
|
+
score: r.score,
|
|
187
|
+
vectorScore: r.vectorScore,
|
|
188
|
+
keywordScore: r.keywordScore,
|
|
189
|
+
sourceFile: r.chunk.sourceFile,
|
|
190
|
+
chunkIndex: r.chunk.chunkIndex,
|
|
191
|
+
metadata: r.chunk.metadata,
|
|
192
|
+
})),
|
|
193
|
+
});
|
|
194
|
+
} catch (err) {
|
|
195
|
+
res.status(500).json({ error: String(err) });
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Recall specific chunk
|
|
200
|
+
app.get('/recall/:id', (req: Request, res: Response) => {
|
|
201
|
+
try {
|
|
202
|
+
const chunk = search.recall(req.params.id);
|
|
203
|
+
if (!chunk) {
|
|
204
|
+
res.status(404).json({ error: 'Chunk not found' });
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
res.json({
|
|
208
|
+
id: chunk.id,
|
|
209
|
+
text: chunk.text,
|
|
210
|
+
sourceFile: chunk.sourceFile,
|
|
211
|
+
chunkIndex: chunk.chunkIndex,
|
|
212
|
+
metadata: chunk.metadata,
|
|
213
|
+
createdAt: chunk.createdAt,
|
|
214
|
+
updatedAt: chunk.updatedAt,
|
|
215
|
+
});
|
|
216
|
+
} catch (err) {
|
|
217
|
+
res.status(500).json({ error: String(err) });
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Index a directory
|
|
222
|
+
app.post('/index', (req: Request, res: Response) => {
|
|
223
|
+
try {
|
|
224
|
+
const { path: dirPath, collection, recursive } = req.body;
|
|
225
|
+
|
|
226
|
+
if (!dirPath || !collection) {
|
|
227
|
+
res.status(400).json({ error: 'path and collection required' });
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const result = indexer.indexDirectory(dirPath, {
|
|
232
|
+
collection,
|
|
233
|
+
recursive: recursive !== false,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
res.json({
|
|
237
|
+
indexed: true,
|
|
238
|
+
files: result.files,
|
|
239
|
+
chunks: result.chunks,
|
|
240
|
+
});
|
|
241
|
+
} catch (err) {
|
|
242
|
+
res.status(500).json({ error: String(err) });
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// Error handler
|
|
247
|
+
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
|
|
248
|
+
console.error('Server error:', err);
|
|
249
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
return app;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function startServer(options: ServerOptions): void {
|
|
256
|
+
const config = { ...DEFAULT_CONFIG.server, ...options.config };
|
|
257
|
+
const app = createServer(options);
|
|
258
|
+
|
|
259
|
+
app.listen(config.port, config.host, () => {
|
|
260
|
+
console.log(`AIF-BIN Recall server running at http://${config.host}:${config.port}`);
|
|
261
|
+
console.log('');
|
|
262
|
+
console.log('Endpoints:');
|
|
263
|
+
console.log(' GET /health - Health check');
|
|
264
|
+
console.log(' GET /collections - List collections');
|
|
265
|
+
console.log(' POST /collections/:n - Create collection');
|
|
266
|
+
console.log(' POST /search - Semantic search');
|
|
267
|
+
console.log(' GET /recall/:id - Retrieve chunk');
|
|
268
|
+
console.log(' POST /index - Index directory');
|
|
269
|
+
});
|
|
270
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core types for AIF-BIN Recall memory server
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface MemoryChunk {
|
|
6
|
+
id: string;
|
|
7
|
+
collectionId: string;
|
|
8
|
+
sourceFile: string;
|
|
9
|
+
chunkIndex: number;
|
|
10
|
+
text: string;
|
|
11
|
+
embedding: number[];
|
|
12
|
+
metadata: Record<string, unknown>;
|
|
13
|
+
createdAt: Date;
|
|
14
|
+
updatedAt: Date;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface Collection {
|
|
18
|
+
id: string;
|
|
19
|
+
name: string;
|
|
20
|
+
description?: string;
|
|
21
|
+
fileCount: number;
|
|
22
|
+
chunkCount: number;
|
|
23
|
+
createdAt: Date;
|
|
24
|
+
updatedAt: Date;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface SearchResult {
|
|
28
|
+
chunk: MemoryChunk;
|
|
29
|
+
score: number;
|
|
30
|
+
vectorScore: number;
|
|
31
|
+
keywordScore?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface SearchOptions {
|
|
35
|
+
collection?: string;
|
|
36
|
+
limit?: number;
|
|
37
|
+
threshold?: number;
|
|
38
|
+
hybridWeight?: number; // 0 = keywords only, 1 = vectors only
|
|
39
|
+
filters?: Record<string, unknown>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface IndexOptions {
|
|
43
|
+
collection: string;
|
|
44
|
+
recursive?: boolean;
|
|
45
|
+
watch?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface ServerConfig {
|
|
49
|
+
port: number;
|
|
50
|
+
host: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface IndexConfig {
|
|
54
|
+
path: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface SearchConfig {
|
|
58
|
+
defaultLimit: number;
|
|
59
|
+
hybridWeight: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface EngramConfig {
|
|
63
|
+
server: ServerConfig;
|
|
64
|
+
index: IndexConfig;
|
|
65
|
+
search: SearchConfig;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const DEFAULT_CONFIG: EngramConfig = {
|
|
69
|
+
server: {
|
|
70
|
+
port: 3847,
|
|
71
|
+
host: 'localhost',
|
|
72
|
+
},
|
|
73
|
+
index: {
|
|
74
|
+
path: '~/.aifbin-recall/index.db',
|
|
75
|
+
},
|
|
76
|
+
search: {
|
|
77
|
+
defaultLimit: 10,
|
|
78
|
+
hybridWeight: 0.7,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export interface AifBinHeader {
|
|
83
|
+
magic: Uint8Array;
|
|
84
|
+
version: number;
|
|
85
|
+
flags: number;
|
|
86
|
+
chunkCount: number;
|
|
87
|
+
embeddingDim: number;
|
|
88
|
+
createdAt: number;
|
|
89
|
+
modifiedAt: number;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface AifBinChunk {
|
|
93
|
+
id: string;
|
|
94
|
+
text: string;
|
|
95
|
+
embedding: number[];
|
|
96
|
+
metadata: Record<string, unknown>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface AifBinFile {
|
|
100
|
+
header: AifBinHeader;
|
|
101
|
+
chunks: AifBinChunk[];
|
|
102
|
+
sourcePath: string;
|
|
103
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"rootDir": "./src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"declarationMap": true,
|
|
15
|
+
"sourceMap": true,
|
|
16
|
+
"resolveJsonModule": true
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*"],
|
|
19
|
+
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
|
20
|
+
}
|