@hanhnd/agent-kit 1.0.29 → 1.0.30
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/dist/kit-server.js +14910 -13
- package/dist/memory/__tests__/indexer.test.js +60 -0
- package/dist/memory/__tests__/store.test.js +19 -0
- package/dist/memory/indexer.js +4 -0
- package/dist/memory/store.js +21 -16
- package/package.json +2 -1
|
@@ -268,6 +268,66 @@ describe('MemoryIndexer', () => {
|
|
|
268
268
|
fs.rmSync(testDir, { recursive: true, force: true });
|
|
269
269
|
}
|
|
270
270
|
});
|
|
271
|
+
test('search does not include dense-only results when BM25 has matches', async () => {
|
|
272
|
+
const testCfg = makeConfig('/tmp/search-dense-filter');
|
|
273
|
+
const chunks = [
|
|
274
|
+
{
|
|
275
|
+
id: 'preference-1',
|
|
276
|
+
source: 'compiled/preferences.md',
|
|
277
|
+
heading: '',
|
|
278
|
+
headingLevel: 0,
|
|
279
|
+
content: 'I like fish',
|
|
280
|
+
lineStart: 1,
|
|
281
|
+
lineEnd: 1,
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
id: 'dense-only-1',
|
|
285
|
+
source: 'compiled/entities/worktree.md',
|
|
286
|
+
heading: 'Worktree',
|
|
287
|
+
headingLevel: 1,
|
|
288
|
+
content: 'Unrelated worktree lifecycle content',
|
|
289
|
+
lineStart: 1,
|
|
290
|
+
lineEnd: 2,
|
|
291
|
+
},
|
|
292
|
+
];
|
|
293
|
+
const fakeStore = {
|
|
294
|
+
vecAvailable: true,
|
|
295
|
+
searchDense: () => [
|
|
296
|
+
{ id: 'dense-only-1', score: 0.99 },
|
|
297
|
+
{ id: 'preference-1', score: 0.98 },
|
|
298
|
+
],
|
|
299
|
+
searchBm25: () => [{ id: 'preference-1', score: 1 }],
|
|
300
|
+
getChunksByIds: (ids) => chunks.filter((chunk) => ids.includes(chunk.id)),
|
|
301
|
+
};
|
|
302
|
+
const testIndexer = new MemoryIndexer(fakeStore, new StubEmbedder(), testCfg);
|
|
303
|
+
const results = await testIndexer.search('personal likes and preferences of the user', 5);
|
|
304
|
+
assert.deepEqual(results.map((result) => result.chunk.id), ['preference-1']);
|
|
305
|
+
assert.equal(results[0].retriever, 'both');
|
|
306
|
+
});
|
|
307
|
+
test('search still returns dense-only results when BM25 has no matches', async () => {
|
|
308
|
+
const testCfg = makeConfig('/tmp/search-dense-fallback');
|
|
309
|
+
const chunks = [
|
|
310
|
+
{
|
|
311
|
+
id: 'semantic-1',
|
|
312
|
+
source: 'compiled/preferences.md',
|
|
313
|
+
heading: '',
|
|
314
|
+
headingLevel: 0,
|
|
315
|
+
content: 'I like fish',
|
|
316
|
+
lineStart: 1,
|
|
317
|
+
lineEnd: 1,
|
|
318
|
+
},
|
|
319
|
+
];
|
|
320
|
+
const fakeStore = {
|
|
321
|
+
vecAvailable: true,
|
|
322
|
+
searchDense: () => [{ id: 'semantic-1', score: 0.99 }],
|
|
323
|
+
searchBm25: () => [],
|
|
324
|
+
getChunksByIds: (ids) => chunks.filter((chunk) => ids.includes(chunk.id)),
|
|
325
|
+
};
|
|
326
|
+
const testIndexer = new MemoryIndexer(fakeStore, new StubEmbedder(), testCfg);
|
|
327
|
+
const results = await testIndexer.search('favorite dish', 5);
|
|
328
|
+
assert.deepEqual(results.map((result) => result.chunk.id), ['semantic-1']);
|
|
329
|
+
assert.equal(results[0].retriever, 'dense');
|
|
330
|
+
});
|
|
271
331
|
test('search falls back to stored chunk content when source file is missing', async () => {
|
|
272
332
|
const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'search-fallback-'));
|
|
273
333
|
const testCfg = makeConfig(path.join(testDir, 'wiki'));
|
|
@@ -72,6 +72,25 @@ describe('MemoryStore', () => {
|
|
|
72
72
|
assert.ok(results.length > 0, 'Expected at least one BM25 result');
|
|
73
73
|
assert.ok(results.some((r) => r.id === 'bm25-search-0001'), `Expected chunk id in results, got: ${results.map((r) => r.id).join(', ')}`);
|
|
74
74
|
});
|
|
75
|
+
test('searchBm25 ignores filler words and matches preference source/content terms', () => {
|
|
76
|
+
const preference = makeChunk({
|
|
77
|
+
id: 'preference-search-0001',
|
|
78
|
+
source: 'compiled/preferences.md',
|
|
79
|
+
heading: '',
|
|
80
|
+
headingLevel: 0,
|
|
81
|
+
content: 'I like fish',
|
|
82
|
+
});
|
|
83
|
+
const unrelated = makeChunk({
|
|
84
|
+
id: 'preference-search-0002',
|
|
85
|
+
source: 'compiled/entities/worktree.md',
|
|
86
|
+
heading: 'Open Questions',
|
|
87
|
+
content: 'How should git manage worktree lifecycle decisions?',
|
|
88
|
+
});
|
|
89
|
+
store.upsert([preference, unrelated], [new Float32Array(384), new Float32Array(384)]);
|
|
90
|
+
const results = store.searchBm25('personal likes and preferences of the user', 5);
|
|
91
|
+
assert.ok(results.length > 0, 'Expected preference query to return results');
|
|
92
|
+
assert.equal(results[0].id, 'preference-search-0001');
|
|
93
|
+
});
|
|
75
94
|
test('searchBm25 returns empty array for empty query', () => {
|
|
76
95
|
const results = store.searchBm25(' ', 5);
|
|
77
96
|
assert.deepEqual(results, []);
|
package/dist/memory/indexer.js
CHANGED
|
@@ -131,6 +131,10 @@ export class MemoryIndexer {
|
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
133
|
const bm25Results = this.store.searchBm25(query, fetchLimit);
|
|
134
|
+
const bm25Ids = new Set(bm25Results.map((result) => result.id));
|
|
135
|
+
if (bm25Ids.size > 0) {
|
|
136
|
+
denseResults = denseResults.filter((result) => bm25Ids.has(result.id));
|
|
137
|
+
}
|
|
134
138
|
// RRF fusion (k=60)
|
|
135
139
|
const k = 60;
|
|
136
140
|
const scoreMap = new Map();
|
package/dist/memory/store.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Database from 'better-sqlite3';
|
|
2
2
|
import * as fs from 'fs';
|
|
3
3
|
import * as path from 'path';
|
|
4
|
+
import { eng, removeStopwords } from 'stopword';
|
|
4
5
|
export class StoreError extends Error {
|
|
5
6
|
constructor(message, cause) {
|
|
6
7
|
super(message);
|
|
@@ -58,18 +59,20 @@ export class MemoryStore {
|
|
|
58
59
|
createSchema(db) {
|
|
59
60
|
db.exec(`
|
|
60
61
|
CREATE TABLE IF NOT EXISTS memory_chunks (
|
|
61
|
-
rowid
|
|
62
|
-
id
|
|
63
|
-
source
|
|
64
|
-
heading
|
|
62
|
+
rowid INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
63
|
+
id TEXT NOT NULL UNIQUE,
|
|
64
|
+
source TEXT NOT NULL,
|
|
65
|
+
heading TEXT NOT NULL DEFAULT '',
|
|
65
66
|
heading_level INTEGER NOT NULL DEFAULT 0,
|
|
66
|
-
content
|
|
67
|
-
line_start
|
|
68
|
-
line_end
|
|
69
|
-
indexed_at
|
|
67
|
+
content TEXT NOT NULL,
|
|
68
|
+
line_start INTEGER NOT NULL,
|
|
69
|
+
line_end INTEGER NOT NULL,
|
|
70
|
+
indexed_at INTEGER NOT NULL
|
|
70
71
|
);
|
|
71
72
|
|
|
72
73
|
CREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5(
|
|
74
|
+
source,
|
|
75
|
+
heading,
|
|
73
76
|
content,
|
|
74
77
|
content='memory_chunks',
|
|
75
78
|
content_rowid='rowid'
|
|
@@ -77,13 +80,14 @@ export class MemoryStore {
|
|
|
77
80
|
|
|
78
81
|
CREATE TRIGGER IF NOT EXISTS memory_chunks_ai
|
|
79
82
|
AFTER INSERT ON memory_chunks BEGIN
|
|
80
|
-
INSERT INTO memory_fts(rowid,
|
|
83
|
+
INSERT INTO memory_fts(rowid, source, heading, content)
|
|
84
|
+
VALUES (new.rowid, new.source, new.heading, new.content);
|
|
81
85
|
END;
|
|
82
86
|
|
|
83
87
|
CREATE TRIGGER IF NOT EXISTS memory_chunks_ad
|
|
84
88
|
AFTER DELETE ON memory_chunks BEGIN
|
|
85
|
-
INSERT INTO memory_fts(memory_fts, rowid, content)
|
|
86
|
-
VALUES ('delete', old.rowid, old.content);
|
|
89
|
+
INSERT INTO memory_fts(memory_fts, rowid, source, heading, content)
|
|
90
|
+
VALUES ('delete', old.rowid, old.source, old.heading, old.content);
|
|
87
91
|
END;
|
|
88
92
|
`);
|
|
89
93
|
if (this._vecAvailable) {
|
|
@@ -206,18 +210,19 @@ export class MemoryStore {
|
|
|
206
210
|
searchBm25(query, limit) {
|
|
207
211
|
if (!query.trim())
|
|
208
212
|
return [];
|
|
209
|
-
|
|
210
|
-
const ftsQuery = query
|
|
213
|
+
const tokens = query
|
|
211
214
|
.trim()
|
|
212
215
|
.split(/\s+/)
|
|
213
|
-
.map((t) => t.replace(/[
|
|
214
|
-
.filter(
|
|
216
|
+
.map((t) => t.replace(/[^a-zA-Z0-9_]/g, '').toLowerCase())
|
|
217
|
+
.filter((t) => t.length > 1);
|
|
218
|
+
const ftsQuery = removeStopwords(tokens, eng)
|
|
219
|
+
.map((t) => `${t.replace(/s$/, '')}*`)
|
|
215
220
|
.join(' OR ');
|
|
216
221
|
if (!ftsQuery)
|
|
217
222
|
return [];
|
|
218
223
|
try {
|
|
219
224
|
const rows = this.db
|
|
220
|
-
.prepare(`SELECT mc.id, rank
|
|
225
|
+
.prepare(`SELECT mc.id, bm25(memory_fts, 0.2, 2.0, 5.0) AS rank
|
|
221
226
|
FROM memory_fts
|
|
222
227
|
JOIN memory_chunks mc ON mc.rowid = memory_fts.rowid
|
|
223
228
|
WHERE memory_fts MATCH ?
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanhnd/agent-kit",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.30",
|
|
4
4
|
"description": "Agent Kit MCP server for software development agent workflows",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/kit-server.js",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"dedent": "^1.5.3",
|
|
33
33
|
"fastembed": "^1.14.0",
|
|
34
34
|
"sqlite-vec": "^0.1.0",
|
|
35
|
+
"stopword": "^3.1.5",
|
|
35
36
|
"zod": "^3.24.4"
|
|
36
37
|
},
|
|
37
38
|
"devDependencies": {
|