@aitytech/agentkits-memory 1.0.1 → 2.0.1
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 +54 -5
- package/dist/better-sqlite3-backend.d.ts +192 -0
- package/dist/better-sqlite3-backend.d.ts.map +1 -0
- package/dist/better-sqlite3-backend.js +801 -0
- package/dist/better-sqlite3-backend.js.map +1 -0
- package/dist/cli/save.js +0 -0
- package/dist/cli/setup.d.ts +6 -2
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +289 -42
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/viewer.js +25 -56
- package/dist/cli/viewer.js.map +1 -1
- package/dist/cli/web-viewer.d.ts +2 -1
- package/dist/cli/web-viewer.d.ts.map +1 -1
- package/dist/cli/web-viewer.js +791 -141
- package/dist/cli/web-viewer.js.map +1 -1
- package/dist/embeddings/embedding-cache.d.ts +131 -0
- package/dist/embeddings/embedding-cache.d.ts.map +1 -0
- package/dist/embeddings/embedding-cache.js +217 -0
- package/dist/embeddings/embedding-cache.js.map +1 -0
- package/dist/embeddings/index.d.ts +11 -0
- package/dist/embeddings/index.d.ts.map +1 -0
- package/dist/embeddings/index.js +11 -0
- package/dist/embeddings/index.js.map +1 -0
- package/dist/embeddings/local-embeddings.d.ts +140 -0
- package/dist/embeddings/local-embeddings.d.ts.map +1 -0
- package/dist/embeddings/local-embeddings.js +293 -0
- package/dist/embeddings/local-embeddings.js.map +1 -0
- package/dist/hooks/context.d.ts +6 -1
- package/dist/hooks/context.d.ts.map +1 -1
- package/dist/hooks/context.js +12 -2
- package/dist/hooks/context.js.map +1 -1
- package/dist/hooks/observation.d.ts +6 -1
- package/dist/hooks/observation.d.ts.map +1 -1
- package/dist/hooks/observation.js +12 -2
- package/dist/hooks/observation.js.map +1 -1
- package/dist/hooks/service.d.ts +1 -6
- package/dist/hooks/service.d.ts.map +1 -1
- package/dist/hooks/service.js +33 -85
- package/dist/hooks/service.js.map +1 -1
- package/dist/hooks/session-init.d.ts +6 -1
- package/dist/hooks/session-init.d.ts.map +1 -1
- package/dist/hooks/session-init.js +12 -2
- package/dist/hooks/session-init.js.map +1 -1
- package/dist/hooks/summarize.d.ts +6 -1
- package/dist/hooks/summarize.d.ts.map +1 -1
- package/dist/hooks/summarize.js +12 -2
- package/dist/hooks/summarize.js.map +1 -1
- package/dist/index.d.ts +10 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +172 -94
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +17 -3
- package/dist/mcp/server.js.map +1 -1
- package/dist/migration.js +3 -3
- package/dist/migration.js.map +1 -1
- package/dist/search/hybrid-search.d.ts +262 -0
- package/dist/search/hybrid-search.d.ts.map +1 -0
- package/dist/search/hybrid-search.js +688 -0
- package/dist/search/hybrid-search.js.map +1 -0
- package/dist/search/index.d.ts +13 -0
- package/dist/search/index.d.ts.map +1 -0
- package/dist/search/index.js +13 -0
- package/dist/search/index.js.map +1 -0
- package/dist/search/token-economics.d.ts +161 -0
- package/dist/search/token-economics.d.ts.map +1 -0
- package/dist/search/token-economics.js +239 -0
- package/dist/search/token-economics.js.map +1 -0
- package/dist/types.d.ts +0 -68
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +6 -4
- package/src/__tests__/better-sqlite3-backend.test.ts +1466 -0
- package/src/__tests__/cache-manager.test.ts +499 -0
- package/src/__tests__/embedding-integration.test.ts +481 -0
- package/src/__tests__/hnsw-index.test.ts +727 -0
- package/src/__tests__/index.test.ts +432 -0
- package/src/better-sqlite3-backend.ts +1000 -0
- package/src/cli/setup.ts +358 -47
- package/src/cli/viewer.ts +28 -63
- package/src/cli/web-viewer.ts +936 -182
- package/src/embeddings/__tests__/embedding-cache.test.ts +269 -0
- package/src/embeddings/__tests__/local-embeddings.test.ts +495 -0
- package/src/embeddings/embedding-cache.ts +318 -0
- package/src/embeddings/index.ts +20 -0
- package/src/embeddings/local-embeddings.ts +419 -0
- package/src/hooks/__tests__/handlers.test.ts +58 -17
- package/src/hooks/__tests__/integration.test.ts +77 -26
- package/src/hooks/context.ts +13 -2
- package/src/hooks/observation.ts +13 -2
- package/src/hooks/service.ts +39 -100
- package/src/hooks/session-init.ts +13 -2
- package/src/hooks/summarize.ts +13 -2
- package/src/index.ts +210 -116
- package/src/mcp/server.ts +20 -3
- package/src/search/__tests__/hybrid-search.test.ts +669 -0
- package/src/search/__tests__/token-economics.test.ts +276 -0
- package/src/search/hybrid-search.ts +968 -0
- package/src/search/index.ts +29 -0
- package/src/search/token-economics.ts +367 -0
- package/src/types.ts +0 -96
- package/src/__tests__/sqljs-backend.test.ts +0 -410
- package/src/migration.ts +0 -574
- package/src/sql.js.d.ts +0 -70
- package/src/sqljs-backend.ts +0 -789
package/src/cli/web-viewer.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* AgentKits Memory Web Viewer
|
|
4
4
|
*
|
|
5
|
-
* Web-based viewer for memory database with
|
|
5
|
+
* Web-based viewer for memory database with hybrid search support.
|
|
6
|
+
* Uses ProjectMemoryService for vector + text search.
|
|
6
7
|
*
|
|
7
8
|
* Usage:
|
|
8
9
|
* npx agentkits-memory-web [--port=1905]
|
|
@@ -11,14 +12,17 @@
|
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
14
|
import * as http from 'node:http';
|
|
14
|
-
import * as fs from 'node:fs';
|
|
15
15
|
import * as path from 'node:path';
|
|
16
|
-
import
|
|
17
|
-
import
|
|
16
|
+
import Database from 'better-sqlite3';
|
|
17
|
+
import type { Database as BetterDatabase } from 'better-sqlite3';
|
|
18
|
+
import { HybridSearchEngine, LocalEmbeddingsService } from '../index.js';
|
|
18
19
|
|
|
19
20
|
const args = process.argv.slice(2);
|
|
20
21
|
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
21
22
|
|
|
23
|
+
// Embeddings service singleton
|
|
24
|
+
let _embeddingsService: LocalEmbeddingsService | null = null;
|
|
25
|
+
|
|
22
26
|
function parseArgs(): Record<string, string | boolean> {
|
|
23
27
|
const parsed: Record<string, string | boolean> = {};
|
|
24
28
|
for (const arg of args) {
|
|
@@ -36,106 +40,112 @@ const PORT = parseInt(options.port as string, 10) || 1905;
|
|
|
36
40
|
const dbDir = path.join(projectDir, '.claude/memory');
|
|
37
41
|
const dbPath = path.join(dbDir, 'memory.db');
|
|
38
42
|
|
|
39
|
-
|
|
43
|
+
// Singleton database and search engine
|
|
44
|
+
let _searchEngine: HybridSearchEngine | null = null;
|
|
45
|
+
let _db: BetterDatabase | null = null;
|
|
40
46
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Get direct database access
|
|
49
|
+
*/
|
|
50
|
+
function getDatabase(): BetterDatabase {
|
|
51
|
+
if (_db) return _db;
|
|
52
|
+
_db = new Database(dbPath);
|
|
53
|
+
_db.pragma('journal_mode = WAL');
|
|
54
|
+
return _db;
|
|
49
55
|
}
|
|
50
56
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (
|
|
56
|
-
fs.mkdirSync(dbDir, { recursive: true });
|
|
57
|
-
}
|
|
57
|
+
/**
|
|
58
|
+
* Get or initialize embeddings service
|
|
59
|
+
*/
|
|
60
|
+
async function getEmbeddingsService(): Promise<LocalEmbeddingsService> {
|
|
61
|
+
if (_embeddingsService) return _embeddingsService;
|
|
58
62
|
|
|
59
|
-
|
|
63
|
+
_embeddingsService = new LocalEmbeddingsService({
|
|
64
|
+
cacheDir: path.join(dbDir, 'embeddings-cache'),
|
|
65
|
+
});
|
|
66
|
+
await _embeddingsService.initialize();
|
|
67
|
+
return _embeddingsService;
|
|
68
|
+
}
|
|
60
69
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
70
|
+
/**
|
|
71
|
+
* Get or initialize the HybridSearchEngine with embeddings
|
|
72
|
+
*/
|
|
73
|
+
async function getSearchEngine(): Promise<HybridSearchEngine> {
|
|
74
|
+
if (_searchEngine) return _searchEngine;
|
|
67
75
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
CREATE TABLE IF NOT EXISTS memory_entries (
|
|
71
|
-
id TEXT PRIMARY KEY,
|
|
72
|
-
key TEXT NOT NULL,
|
|
73
|
-
content TEXT NOT NULL,
|
|
74
|
-
type TEXT DEFAULT 'semantic',
|
|
75
|
-
namespace TEXT DEFAULT 'general',
|
|
76
|
-
tags TEXT DEFAULT '[]',
|
|
77
|
-
metadata TEXT DEFAULT '{}',
|
|
78
|
-
embedding BLOB,
|
|
79
|
-
created_at INTEGER NOT NULL,
|
|
80
|
-
updated_at INTEGER NOT NULL,
|
|
81
|
-
accessed_at INTEGER,
|
|
82
|
-
access_count INTEGER DEFAULT 0,
|
|
83
|
-
importance REAL DEFAULT 0.5,
|
|
84
|
-
decay_rate REAL DEFAULT 0.1
|
|
85
|
-
)
|
|
86
|
-
`);
|
|
87
|
-
|
|
88
|
-
db.run(`CREATE INDEX IF NOT EXISTS idx_namespace ON memory_entries(namespace)`);
|
|
89
|
-
db.run(`CREATE INDEX IF NOT EXISTS idx_key ON memory_entries(key)`);
|
|
90
|
-
db.run(`CREATE INDEX IF NOT EXISTS idx_created ON memory_entries(created_at)`);
|
|
91
|
-
|
|
92
|
-
return db;
|
|
93
|
-
}
|
|
76
|
+
const db = getDatabase();
|
|
77
|
+
const embeddings = await getEmbeddingsService();
|
|
94
78
|
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
79
|
+
// Create embedding generator function
|
|
80
|
+
const embeddingGenerator = async (text: string): Promise<Float32Array> => {
|
|
81
|
+
const result = await embeddings.embed(text);
|
|
82
|
+
return result.embedding;
|
|
83
|
+
};
|
|
100
84
|
|
|
101
|
-
|
|
102
|
-
|
|
85
|
+
_searchEngine = new HybridSearchEngine(db, {}, embeddingGenerator);
|
|
86
|
+
await _searchEngine.initialize();
|
|
87
|
+
return _searchEngine;
|
|
103
88
|
}
|
|
104
89
|
|
|
105
|
-
|
|
90
|
+
/**
|
|
91
|
+
* Get database statistics using direct SQL (faster for stats queries)
|
|
92
|
+
*/
|
|
93
|
+
function getStats(db: BetterDatabase): {
|
|
106
94
|
total: number;
|
|
107
95
|
byNamespace: Record<string, number>;
|
|
108
96
|
byType: Record<string, number>;
|
|
97
|
+
tokenEconomics: {
|
|
98
|
+
totalTokens: number;
|
|
99
|
+
avgTokensPerEntry: number;
|
|
100
|
+
totalCharacters: number;
|
|
101
|
+
estimatedSavings: number;
|
|
102
|
+
};
|
|
109
103
|
} {
|
|
110
|
-
const
|
|
111
|
-
const total =
|
|
104
|
+
const totalRow = db.prepare('SELECT COUNT(*) as count FROM memory_entries').get() as { count: number };
|
|
105
|
+
const total = totalRow?.count || 0;
|
|
112
106
|
|
|
113
|
-
const
|
|
107
|
+
const nsRows = db.prepare('SELECT namespace, COUNT(*) as count FROM memory_entries GROUP BY namespace').all() as { namespace: string; count: number }[];
|
|
114
108
|
const byNamespace: Record<string, number> = {};
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
byNamespace[row[0] as string] = row[1] as number;
|
|
118
|
-
}
|
|
109
|
+
for (const row of nsRows) {
|
|
110
|
+
byNamespace[row.namespace] = row.count;
|
|
119
111
|
}
|
|
120
112
|
|
|
121
|
-
const
|
|
113
|
+
const typeRows = db.prepare('SELECT type, COUNT(*) as count FROM memory_entries GROUP BY type').all() as { type: string; count: number }[];
|
|
122
114
|
const byType: Record<string, number> = {};
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
byType[row[0] as string] = row[1] as number;
|
|
126
|
-
}
|
|
115
|
+
for (const row of typeRows) {
|
|
116
|
+
byType[row.type] = row.count;
|
|
127
117
|
}
|
|
128
118
|
|
|
129
|
-
|
|
119
|
+
// Calculate token economics
|
|
120
|
+
const contentRow = db.prepare('SELECT SUM(LENGTH(content)) as total_chars, COUNT(*) as count FROM memory_entries').get() as { total_chars: number; count: number };
|
|
121
|
+
const totalCharacters = contentRow?.total_chars || 0;
|
|
122
|
+
const entryCount = contentRow?.count || 0;
|
|
123
|
+
|
|
124
|
+
// Estimate tokens (~4 chars per token)
|
|
125
|
+
const totalTokens = Math.ceil(totalCharacters / 4);
|
|
126
|
+
const avgTokensPerEntry = entryCount > 0 ? Math.ceil(totalTokens / entryCount) : 0;
|
|
127
|
+
|
|
128
|
+
// Estimated savings: if you had to rediscover this info each time
|
|
129
|
+
// Assume 5x overhead for discovery vs recall
|
|
130
|
+
const estimatedSavings = totalTokens * 5;
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
total,
|
|
134
|
+
byNamespace,
|
|
135
|
+
byType,
|
|
136
|
+
tokenEconomics: {
|
|
137
|
+
totalTokens,
|
|
138
|
+
avgTokensPerEntry,
|
|
139
|
+
totalCharacters,
|
|
140
|
+
estimatedSavings,
|
|
141
|
+
},
|
|
142
|
+
};
|
|
130
143
|
}
|
|
131
144
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
offset = 0,
|
|
137
|
-
search?: string
|
|
138
|
-
): Array<{
|
|
145
|
+
/**
|
|
146
|
+
* Result type for getEntries with optional score and embedding info
|
|
147
|
+
*/
|
|
148
|
+
interface EntryResult {
|
|
139
149
|
id: string;
|
|
140
150
|
key: string;
|
|
141
151
|
content: string;
|
|
@@ -144,57 +154,208 @@ function getEntries(
|
|
|
144
154
|
tags: string[];
|
|
145
155
|
created_at: number;
|
|
146
156
|
updated_at: number;
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
157
|
+
score?: number;
|
|
158
|
+
hasEmbedding?: boolean;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Get entries with optional search (standard listing)
|
|
163
|
+
*/
|
|
164
|
+
function getEntries(
|
|
165
|
+
db: BetterDatabase,
|
|
166
|
+
namespace?: string,
|
|
167
|
+
limit = 50,
|
|
168
|
+
offset = 0,
|
|
169
|
+
search?: string
|
|
170
|
+
): EntryResult[] {
|
|
171
|
+
// Standard query without search
|
|
172
|
+
if (!search || !search.trim()) {
|
|
173
|
+
let query = 'SELECT id, key, content, type, namespace, tags, embedding, created_at, updated_at FROM memory_entries';
|
|
174
|
+
const conditions: string[] = [];
|
|
175
|
+
const params: (string | number)[] = [];
|
|
176
|
+
|
|
177
|
+
if (namespace) {
|
|
178
|
+
conditions.push('namespace = ?');
|
|
179
|
+
params.push(namespace);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (conditions.length > 0) {
|
|
183
|
+
query += ' WHERE ' + conditions.join(' AND ');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
query += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
|
|
187
|
+
params.push(limit, offset);
|
|
188
|
+
|
|
189
|
+
const rows = db.prepare(query).all(...params) as {
|
|
190
|
+
id: string;
|
|
191
|
+
key: string;
|
|
192
|
+
content: string;
|
|
193
|
+
type: string;
|
|
194
|
+
namespace: string;
|
|
195
|
+
tags: string;
|
|
196
|
+
embedding: Buffer | null;
|
|
197
|
+
created_at: number;
|
|
198
|
+
updated_at: number;
|
|
199
|
+
}[];
|
|
200
|
+
|
|
201
|
+
return rows.map((row) => ({
|
|
202
|
+
id: row.id,
|
|
203
|
+
key: row.key,
|
|
204
|
+
content: row.content,
|
|
205
|
+
type: row.type,
|
|
206
|
+
namespace: row.namespace,
|
|
207
|
+
tags: JSON.parse(row.tags || '[]'),
|
|
208
|
+
created_at: row.created_at,
|
|
209
|
+
updated_at: row.updated_at,
|
|
210
|
+
hasEmbedding: !!(row.embedding && row.embedding.length > 0),
|
|
211
|
+
}));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Use FTS5 search for better CJK support
|
|
215
|
+
const sanitizedSearch = search.trim().replace(/"/g, '""');
|
|
216
|
+
let ftsQuery = `
|
|
217
|
+
SELECT m.id, m.key, m.content, m.type, m.namespace, m.tags, m.embedding, m.created_at, m.updated_at
|
|
218
|
+
FROM memory_entries m
|
|
219
|
+
INNER JOIN memory_fts f ON m.id = f.id
|
|
220
|
+
WHERE memory_fts MATCH '"${sanitizedSearch}"'
|
|
221
|
+
`;
|
|
151
222
|
|
|
152
223
|
if (namespace) {
|
|
153
|
-
|
|
154
|
-
params.push(namespace);
|
|
224
|
+
ftsQuery += ` AND m.namespace = ?`;
|
|
155
225
|
}
|
|
156
226
|
|
|
157
|
-
|
|
227
|
+
ftsQuery += ` ORDER BY m.created_at DESC LIMIT ? OFFSET ?`;
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
const params = namespace ? [namespace, limit, offset] : [limit, offset];
|
|
231
|
+
const rows = db.prepare(ftsQuery).all(...params) as {
|
|
232
|
+
id: string;
|
|
233
|
+
key: string;
|
|
234
|
+
content: string;
|
|
235
|
+
type: string;
|
|
236
|
+
namespace: string;
|
|
237
|
+
tags: string;
|
|
238
|
+
embedding: Buffer | null;
|
|
239
|
+
created_at: number;
|
|
240
|
+
updated_at: number;
|
|
241
|
+
}[];
|
|
242
|
+
|
|
243
|
+
return rows.map((row) => ({
|
|
244
|
+
id: row.id,
|
|
245
|
+
key: row.key,
|
|
246
|
+
content: row.content,
|
|
247
|
+
type: row.type,
|
|
248
|
+
namespace: row.namespace,
|
|
249
|
+
tags: JSON.parse(row.tags || '[]'),
|
|
250
|
+
created_at: row.created_at,
|
|
251
|
+
updated_at: row.updated_at,
|
|
252
|
+
hasEmbedding: !!(row.embedding && row.embedding.length > 0),
|
|
253
|
+
}));
|
|
254
|
+
} catch {
|
|
255
|
+
// Fallback to LIKE if FTS fails
|
|
256
|
+
console.warn('[WebViewer] FTS search failed, falling back to LIKE');
|
|
257
|
+
|
|
258
|
+
let query = 'SELECT id, key, content, type, namespace, tags, embedding, created_at, updated_at FROM memory_entries';
|
|
259
|
+
const conditions: string[] = [];
|
|
260
|
+
const params: (string | number)[] = [];
|
|
261
|
+
|
|
262
|
+
if (namespace) {
|
|
263
|
+
conditions.push('namespace = ?');
|
|
264
|
+
params.push(namespace);
|
|
265
|
+
}
|
|
266
|
+
|
|
158
267
|
conditions.push('(content LIKE ? OR key LIKE ? OR tags LIKE ?)');
|
|
159
268
|
const searchPattern = `%${search}%`;
|
|
160
269
|
params.push(searchPattern, searchPattern, searchPattern);
|
|
161
|
-
}
|
|
162
270
|
|
|
163
|
-
if (conditions.length > 0) {
|
|
164
271
|
query += ' WHERE ' + conditions.join(' AND ');
|
|
272
|
+
query += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
|
|
273
|
+
params.push(limit, offset);
|
|
274
|
+
|
|
275
|
+
const rows = db.prepare(query).all(...params) as {
|
|
276
|
+
id: string;
|
|
277
|
+
key: string;
|
|
278
|
+
content: string;
|
|
279
|
+
type: string;
|
|
280
|
+
namespace: string;
|
|
281
|
+
tags: string;
|
|
282
|
+
embedding: Buffer | null;
|
|
283
|
+
created_at: number;
|
|
284
|
+
updated_at: number;
|
|
285
|
+
}[];
|
|
286
|
+
|
|
287
|
+
return rows.map((row) => ({
|
|
288
|
+
id: row.id,
|
|
289
|
+
key: row.key,
|
|
290
|
+
content: row.content,
|
|
291
|
+
type: row.type,
|
|
292
|
+
namespace: row.namespace,
|
|
293
|
+
tags: JSON.parse(row.tags || '[]'),
|
|
294
|
+
created_at: row.created_at,
|
|
295
|
+
updated_at: row.updated_at,
|
|
296
|
+
hasEmbedding: !!(row.embedding && row.embedding.length > 0),
|
|
297
|
+
}));
|
|
165
298
|
}
|
|
299
|
+
}
|
|
166
300
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
301
|
+
/**
|
|
302
|
+
* Search entries using HybridSearchEngine
|
|
303
|
+
* Supports hybrid (text + vector), text-only, or vector-only search
|
|
304
|
+
*/
|
|
305
|
+
async function searchEntries(
|
|
306
|
+
searchEngine: HybridSearchEngine,
|
|
307
|
+
query: string,
|
|
308
|
+
options: {
|
|
309
|
+
type?: 'hybrid' | 'text' | 'vector';
|
|
310
|
+
namespace?: string;
|
|
311
|
+
limit?: number;
|
|
312
|
+
} = {}
|
|
313
|
+
): Promise<EntryResult[]> {
|
|
314
|
+
const { type = 'hybrid', namespace, limit = 20 } = options;
|
|
315
|
+
|
|
316
|
+
// Use searchCompact for efficient search with scores
|
|
317
|
+
const results = await searchEngine.searchCompact(query, {
|
|
318
|
+
limit,
|
|
319
|
+
namespace,
|
|
320
|
+
includeKeyword: type === 'hybrid' || type === 'text',
|
|
321
|
+
includeSemantic: type === 'hybrid' || type === 'vector',
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// Fetch full entries for the results
|
|
325
|
+
const db = getDatabase();
|
|
326
|
+
const entries: EntryResult[] = [];
|
|
327
|
+
|
|
328
|
+
for (const result of results) {
|
|
329
|
+
const row = db.prepare(`
|
|
330
|
+
SELECT id, key, content, type, namespace, tags, embedding, created_at, updated_at
|
|
331
|
+
FROM memory_entries WHERE id = ?
|
|
332
|
+
`).get(result.id) as {
|
|
333
|
+
id: string;
|
|
334
|
+
key: string;
|
|
335
|
+
content: string;
|
|
336
|
+
type: string;
|
|
337
|
+
namespace: string;
|
|
338
|
+
tags: string;
|
|
339
|
+
embedding: Buffer | null;
|
|
340
|
+
created_at: number;
|
|
341
|
+
updated_at: number;
|
|
342
|
+
} | undefined;
|
|
343
|
+
|
|
344
|
+
if (row) {
|
|
345
|
+
entries.push({
|
|
346
|
+
id: row.id,
|
|
347
|
+
key: row.key,
|
|
348
|
+
content: row.content,
|
|
349
|
+
type: row.type,
|
|
350
|
+
namespace: row.namespace,
|
|
351
|
+
tags: JSON.parse(row.tags || '[]'),
|
|
352
|
+
created_at: row.created_at,
|
|
353
|
+
updated_at: row.updated_at,
|
|
354
|
+
score: result.score,
|
|
355
|
+
hasEmbedding: !!(row.embedding && row.embedding.length > 0),
|
|
356
|
+
});
|
|
357
|
+
}
|
|
196
358
|
}
|
|
197
|
-
stmt.free();
|
|
198
359
|
|
|
199
360
|
return entries;
|
|
200
361
|
}
|
|
@@ -206,6 +367,7 @@ function getHTML(): string {
|
|
|
206
367
|
<meta charset="UTF-8">
|
|
207
368
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
208
369
|
<title>AgentKits Memory Viewer</title>
|
|
370
|
+
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' stop-color='%233B82F6'/%3E%3Cstop offset='100%25' stop-color='%238B5CF6'/%3E%3C/linearGradient%3E%3C/defs%3E%3Ccircle cx='12' cy='12' r='10' fill='url(%23g)'/%3E%3Cpath d='M10 14.17l-3.17-3.17-1.42 1.41L10 17l8-8-1.41-1.41z' fill='white'/%3E%3C/svg%3E">
|
|
209
371
|
<style>
|
|
210
372
|
:root {
|
|
211
373
|
--bg-primary: #0F172A;
|
|
@@ -316,6 +478,202 @@ function getHTML(): string {
|
|
|
316
478
|
fill: var(--text-muted);
|
|
317
479
|
}
|
|
318
480
|
|
|
481
|
+
.search-type-select {
|
|
482
|
+
padding: 12px 16px;
|
|
483
|
+
background: var(--bg-secondary);
|
|
484
|
+
border: 1px solid var(--border);
|
|
485
|
+
border-radius: 8px;
|
|
486
|
+
color: var(--text-primary);
|
|
487
|
+
font-size: 14px;
|
|
488
|
+
cursor: pointer;
|
|
489
|
+
min-width: 180px;
|
|
490
|
+
transition: border-color 0.2s;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
.search-type-select:focus {
|
|
494
|
+
outline: none;
|
|
495
|
+
border-color: var(--accent);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
.search-type-select:hover { border-color: var(--accent); }
|
|
499
|
+
|
|
500
|
+
.score-badge {
|
|
501
|
+
font-size: 11px;
|
|
502
|
+
padding: 3px 8px;
|
|
503
|
+
background: linear-gradient(135deg, rgba(59, 130, 246, 0.2), rgba(139, 92, 246, 0.2));
|
|
504
|
+
color: var(--accent);
|
|
505
|
+
border-radius: 4px;
|
|
506
|
+
font-weight: 600;
|
|
507
|
+
white-space: nowrap;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
.vector-badge {
|
|
511
|
+
display: inline-flex;
|
|
512
|
+
align-items: center;
|
|
513
|
+
font-size: 10px;
|
|
514
|
+
font-weight: 600;
|
|
515
|
+
padding: 2px 6px;
|
|
516
|
+
border-radius: 4px;
|
|
517
|
+
text-transform: uppercase;
|
|
518
|
+
letter-spacing: 0.5px;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
.vector-badge.has-vector {
|
|
522
|
+
background: rgba(34, 197, 94, 0.15);
|
|
523
|
+
color: #22C55E;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
.vector-badge.no-vector {
|
|
527
|
+
background: rgba(100, 116, 139, 0.1);
|
|
528
|
+
color: var(--text-muted);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
.embedding-stats {
|
|
532
|
+
display: flex;
|
|
533
|
+
gap: 16px;
|
|
534
|
+
margin-bottom: 20px;
|
|
535
|
+
padding: 16px;
|
|
536
|
+
background: var(--bg-card);
|
|
537
|
+
border-radius: 8px;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
.embedding-stat {
|
|
541
|
+
text-align: center;
|
|
542
|
+
flex: 1;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
.embedding-stat-value {
|
|
546
|
+
font-size: 24px;
|
|
547
|
+
font-weight: 700;
|
|
548
|
+
color: var(--text-primary);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
.embedding-stat-label {
|
|
552
|
+
font-size: 12px;
|
|
553
|
+
color: var(--text-muted);
|
|
554
|
+
text-transform: uppercase;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
.embedding-stat-value.success { color: var(--success); }
|
|
558
|
+
.embedding-stat-value.warning { color: var(--warning); }
|
|
559
|
+
|
|
560
|
+
.progress-bar {
|
|
561
|
+
height: 8px;
|
|
562
|
+
background: var(--bg-card);
|
|
563
|
+
border-radius: 4px;
|
|
564
|
+
overflow: hidden;
|
|
565
|
+
margin: 16px 0;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
.progress-bar-fill {
|
|
569
|
+
height: 100%;
|
|
570
|
+
background: linear-gradient(90deg, var(--accent), #8B5CF6);
|
|
571
|
+
border-radius: 4px;
|
|
572
|
+
transition: width 0.3s ease;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
.progress-text {
|
|
576
|
+
text-align: center;
|
|
577
|
+
font-size: 14px;
|
|
578
|
+
color: var(--text-secondary);
|
|
579
|
+
margin-top: 8px;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
.btn-icon {
|
|
583
|
+
padding: 10px;
|
|
584
|
+
min-width: auto;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
.embedding-section {
|
|
588
|
+
margin-top: 20px;
|
|
589
|
+
padding: 16px;
|
|
590
|
+
background: var(--bg-card);
|
|
591
|
+
border-radius: 8px;
|
|
592
|
+
border: 1px solid var(--border);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
.embedding-header {
|
|
596
|
+
display: flex;
|
|
597
|
+
align-items: center;
|
|
598
|
+
gap: 12px;
|
|
599
|
+
margin-bottom: 12px;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
.embedding-status {
|
|
603
|
+
display: inline-flex;
|
|
604
|
+
align-items: center;
|
|
605
|
+
gap: 6px;
|
|
606
|
+
font-size: 13px;
|
|
607
|
+
padding: 4px 10px;
|
|
608
|
+
border-radius: 6px;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
.embedding-status.has-embedding {
|
|
612
|
+
background: rgba(34, 197, 94, 0.15);
|
|
613
|
+
color: #22C55E;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
.embedding-status.no-embedding {
|
|
617
|
+
background: rgba(239, 68, 68, 0.15);
|
|
618
|
+
color: #EF4444;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
.embedding-status svg {
|
|
622
|
+
width: 14px;
|
|
623
|
+
height: 14px;
|
|
624
|
+
fill: currentColor;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
.embedding-dims {
|
|
628
|
+
font-size: 12px;
|
|
629
|
+
color: var(--text-muted);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
.embedding-viz {
|
|
633
|
+
display: flex;
|
|
634
|
+
align-items: flex-end;
|
|
635
|
+
gap: 2px;
|
|
636
|
+
height: 40px;
|
|
637
|
+
margin-top: 12px;
|
|
638
|
+
padding: 8px;
|
|
639
|
+
background: var(--bg-primary);
|
|
640
|
+
border-radius: 6px;
|
|
641
|
+
overflow: hidden;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
.embedding-bar {
|
|
645
|
+
flex: 1;
|
|
646
|
+
min-width: 4px;
|
|
647
|
+
border-radius: 2px 2px 0 0;
|
|
648
|
+
transition: height 0.3s ease;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
.embedding-bar.positive { background: linear-gradient(to top, #3B82F6, #60A5FA); }
|
|
652
|
+
.embedding-bar.negative { background: linear-gradient(to top, #8B5CF6, #A78BFA); }
|
|
653
|
+
|
|
654
|
+
.embedding-legend {
|
|
655
|
+
display: flex;
|
|
656
|
+
justify-content: space-between;
|
|
657
|
+
margin-top: 8px;
|
|
658
|
+
font-size: 11px;
|
|
659
|
+
color: var(--text-muted);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
.embedding-legend-item {
|
|
663
|
+
display: flex;
|
|
664
|
+
align-items: center;
|
|
665
|
+
gap: 4px;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
.legend-dot {
|
|
669
|
+
width: 8px;
|
|
670
|
+
height: 8px;
|
|
671
|
+
border-radius: 50%;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
.legend-dot.positive { background: #3B82F6; }
|
|
675
|
+
.legend-dot.negative { background: #8B5CF6; }
|
|
676
|
+
|
|
319
677
|
.entries-list { display: flex; flex-direction: column; gap: 12px; }
|
|
320
678
|
|
|
321
679
|
.entry-card {
|
|
@@ -341,7 +699,14 @@ function getHTML(): string {
|
|
|
341
699
|
gap: 12px;
|
|
342
700
|
}
|
|
343
701
|
|
|
344
|
-
.entry-key { font-weight: 600; font-size: 15px; color: var(--text-primary); word-break: break-word; }
|
|
702
|
+
.entry-key { font-weight: 600; font-size: 15px; color: var(--text-primary); word-break: break-word; flex: 1; }
|
|
703
|
+
|
|
704
|
+
.entry-badges {
|
|
705
|
+
display: flex;
|
|
706
|
+
align-items: center;
|
|
707
|
+
gap: 8px;
|
|
708
|
+
flex-shrink: 0;
|
|
709
|
+
}
|
|
345
710
|
|
|
346
711
|
.entry-namespace {
|
|
347
712
|
font-size: 12px;
|
|
@@ -624,6 +989,10 @@ function getHTML(): string {
|
|
|
624
989
|
<svg viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
|
|
625
990
|
Add Memory
|
|
626
991
|
</button>
|
|
992
|
+
<button class="btn" onclick="openEmbeddingModal()">
|
|
993
|
+
<svg viewBox="0 0 24 24"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>
|
|
994
|
+
Embeddings
|
|
995
|
+
</button>
|
|
627
996
|
<button class="btn" onclick="loadData()">
|
|
628
997
|
<svg viewBox="0 0 24 24"><path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
|
|
629
998
|
Refresh
|
|
@@ -639,6 +1008,11 @@ function getHTML(): string {
|
|
|
639
1008
|
<svg viewBox="0 0 24 24"><path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>
|
|
640
1009
|
<input type="text" id="search-input" placeholder="Search memories..." oninput="debounceSearch()">
|
|
641
1010
|
</div>
|
|
1011
|
+
<select id="search-type" class="search-type-select" onchange="debounceSearch()">
|
|
1012
|
+
<option value="hybrid">Hybrid (Text + Semantic)</option>
|
|
1013
|
+
<option value="text">Text Only (FTS5)</option>
|
|
1014
|
+
<option value="vector">Semantic Only (Vector)</option>
|
|
1015
|
+
</select>
|
|
642
1016
|
</div>
|
|
643
1017
|
|
|
644
1018
|
<div id="entries-container" class="entries-list">
|
|
@@ -733,6 +1107,57 @@ function getHTML(): string {
|
|
|
733
1107
|
</div>
|
|
734
1108
|
</div>
|
|
735
1109
|
|
|
1110
|
+
<!-- Embedding Management Modal -->
|
|
1111
|
+
<div id="embedding-modal" class="modal-overlay" onclick="closeEmbeddingModal(event)">
|
|
1112
|
+
<div class="modal" style="max-width: 500px;" onclick="event.stopPropagation()">
|
|
1113
|
+
<div class="modal-header">
|
|
1114
|
+
<span class="modal-title">Manage Embeddings</span>
|
|
1115
|
+
<button class="modal-close" onclick="closeEmbeddingModal()">
|
|
1116
|
+
<svg viewBox="0 0 24 24"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
|
|
1117
|
+
</button>
|
|
1118
|
+
</div>
|
|
1119
|
+
<div class="modal-body" id="embedding-modal-body">
|
|
1120
|
+
<div class="embedding-stats" id="embedding-stats">
|
|
1121
|
+
<div class="embedding-stat">
|
|
1122
|
+
<div class="embedding-stat-value" id="stat-total">-</div>
|
|
1123
|
+
<div class="embedding-stat-label">Total</div>
|
|
1124
|
+
</div>
|
|
1125
|
+
<div class="embedding-stat">
|
|
1126
|
+
<div class="embedding-stat-value success" id="stat-with">-</div>
|
|
1127
|
+
<div class="embedding-stat-label">With Embedding</div>
|
|
1128
|
+
</div>
|
|
1129
|
+
<div class="embedding-stat">
|
|
1130
|
+
<div class="embedding-stat-value warning" id="stat-without">-</div>
|
|
1131
|
+
<div class="embedding-stat-label">Without</div>
|
|
1132
|
+
</div>
|
|
1133
|
+
</div>
|
|
1134
|
+
<p style="font-size: 14px; color: var(--text-secondary); margin-bottom: 12px;">
|
|
1135
|
+
Vector embeddings enable semantic search - finding memories by meaning, not just keywords.
|
|
1136
|
+
</p>
|
|
1137
|
+
<p style="font-size: 12px; color: var(--text-muted); margin-bottom: 20px; padding: 10px; background: var(--bg-card); border-radius: 6px;">
|
|
1138
|
+
<strong>Model:</strong> multilingual-e5-small (100+ languages, optimized for retrieval)
|
|
1139
|
+
</p>
|
|
1140
|
+
<div id="embedding-progress" style="display: none;">
|
|
1141
|
+
<div class="progress-bar">
|
|
1142
|
+
<div class="progress-bar-fill" id="progress-fill" style="width: 0%"></div>
|
|
1143
|
+
</div>
|
|
1144
|
+
<div class="progress-text" id="progress-text">Processing...</div>
|
|
1145
|
+
</div>
|
|
1146
|
+
</div>
|
|
1147
|
+
<div class="modal-footer" id="embedding-modal-footer">
|
|
1148
|
+
<button class="btn" onclick="closeEmbeddingModal()">Close</button>
|
|
1149
|
+
<button class="btn btn-primary" id="btn-generate-missing" onclick="generateEmbeddings('missing')">
|
|
1150
|
+
<svg viewBox="0 0 24 24" style="width:16px;height:16px;fill:currentColor"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
|
|
1151
|
+
Generate Missing
|
|
1152
|
+
</button>
|
|
1153
|
+
<button class="btn btn-success" id="btn-regenerate-all" onclick="generateEmbeddings('all')">
|
|
1154
|
+
<svg viewBox="0 0 24 24" style="width:16px;height:16px;fill:currentColor"><path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
|
|
1155
|
+
Re-generate All
|
|
1156
|
+
</button>
|
|
1157
|
+
</div>
|
|
1158
|
+
</div>
|
|
1159
|
+
</div>
|
|
1160
|
+
|
|
736
1161
|
<script>
|
|
737
1162
|
let currentNamespace = '';
|
|
738
1163
|
let currentSearch = '';
|
|
@@ -765,10 +1190,6 @@ function getHTML(): string {
|
|
|
765
1190
|
<div class="stat-label">Namespaces</div>
|
|
766
1191
|
<div class="stat-value">\${Object.keys(stats.byNamespace || {}).length}</div>
|
|
767
1192
|
</div>
|
|
768
|
-
<div class="stat-card">
|
|
769
|
-
<div class="stat-label">Types</div>
|
|
770
|
-
<div class="stat-value">\${Object.keys(stats.byType || {}).length}</div>
|
|
771
|
-
</div>
|
|
772
1193
|
\`;
|
|
773
1194
|
}
|
|
774
1195
|
|
|
@@ -787,13 +1208,32 @@ function getHTML(): string {
|
|
|
787
1208
|
const container = document.getElementById('entries-container');
|
|
788
1209
|
container.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
|
|
789
1210
|
|
|
790
|
-
const params = new URLSearchParams({ limit: pageSize, offset: currentPage * pageSize });
|
|
791
|
-
if (currentNamespace) params.set('namespace', currentNamespace);
|
|
792
|
-
if (currentSearch) params.set('search', currentSearch);
|
|
793
|
-
|
|
794
1211
|
try {
|
|
795
|
-
|
|
796
|
-
|
|
1212
|
+
let entries;
|
|
1213
|
+
|
|
1214
|
+
if (currentSearch && currentSearch.trim()) {
|
|
1215
|
+
// Use hybrid search endpoint when searching
|
|
1216
|
+
const searchType = document.getElementById('search-type').value;
|
|
1217
|
+
const params = new URLSearchParams({
|
|
1218
|
+
q: currentSearch,
|
|
1219
|
+
type: searchType,
|
|
1220
|
+
limit: String(pageSize)
|
|
1221
|
+
});
|
|
1222
|
+
if (currentNamespace) params.set('namespace', currentNamespace);
|
|
1223
|
+
|
|
1224
|
+
const res = await fetch('/api/search?' + params);
|
|
1225
|
+
entries = await res.json();
|
|
1226
|
+
} else {
|
|
1227
|
+
// Use standard entries endpoint for listing
|
|
1228
|
+
const params = new URLSearchParams({
|
|
1229
|
+
limit: String(pageSize),
|
|
1230
|
+
offset: String(currentPage * pageSize)
|
|
1231
|
+
});
|
|
1232
|
+
if (currentNamespace) params.set('namespace', currentNamespace);
|
|
1233
|
+
|
|
1234
|
+
const res = await fetch('/api/entries?' + params);
|
|
1235
|
+
entries = await res.json();
|
|
1236
|
+
}
|
|
797
1237
|
|
|
798
1238
|
if (!Array.isArray(entries) || entries.length === 0) {
|
|
799
1239
|
container.innerHTML = \`
|
|
@@ -808,7 +1248,14 @@ function getHTML(): string {
|
|
|
808
1248
|
<div class="entry-card" onclick="showDetail('\${entry.id}')">
|
|
809
1249
|
<div class="entry-header">
|
|
810
1250
|
<span class="entry-key">\${escapeHtml(entry.key)}</span>
|
|
811
|
-
<
|
|
1251
|
+
<div class="entry-badges">
|
|
1252
|
+
\${entry.score !== undefined ? \`<span class="score-badge">\${(entry.score * 100).toFixed(1)}%</span>\` : ''}
|
|
1253
|
+
\${entry.hasEmbedding ?
|
|
1254
|
+
\`<span class="vector-badge has-vector" title="Vector embedding enabled">Vec</span>\` :
|
|
1255
|
+
\`<span class="vector-badge no-vector" title="No vector embedding">--</span>\`
|
|
1256
|
+
}
|
|
1257
|
+
<span class="entry-namespace">\${entry.namespace}</span>
|
|
1258
|
+
</div>
|
|
812
1259
|
</div>
|
|
813
1260
|
<div class="entry-content truncated">\${escapeHtml(entry.content)}</div>
|
|
814
1261
|
<div class="entry-footer">
|
|
@@ -857,6 +1304,59 @@ function getHTML(): string {
|
|
|
857
1304
|
function prevPage() { if (currentPage > 0) { currentPage--; loadEntries(); } }
|
|
858
1305
|
function nextPage() { currentPage++; loadEntries(); }
|
|
859
1306
|
|
|
1307
|
+
function renderEmbeddingViz(embedding) {
|
|
1308
|
+
if (!embedding || !embedding.hasEmbedding) {
|
|
1309
|
+
return \`
|
|
1310
|
+
<div class="embedding-section">
|
|
1311
|
+
<div class="embedding-header">
|
|
1312
|
+
<span class="embedding-status no-embedding">
|
|
1313
|
+
<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>
|
|
1314
|
+
No Embedding
|
|
1315
|
+
</span>
|
|
1316
|
+
</div>
|
|
1317
|
+
<p style="font-size: 12px; color: var(--text-muted); margin: 0;">
|
|
1318
|
+
This entry doesn't have a vector embedding. Edit and save to generate one.
|
|
1319
|
+
</p>
|
|
1320
|
+
</div>
|
|
1321
|
+
\`;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
const preview = embedding.preview || [];
|
|
1325
|
+
const maxVal = Math.max(...preview.map(Math.abs), 0.001);
|
|
1326
|
+
|
|
1327
|
+
const bars = preview.map(val => {
|
|
1328
|
+
const height = Math.abs(val) / maxVal * 100;
|
|
1329
|
+
const isPositive = val >= 0;
|
|
1330
|
+
return \`<div class="embedding-bar \${isPositive ? 'positive' : 'negative'}" style="height: \${height}%" title="\${val.toFixed(4)}"></div>\`;
|
|
1331
|
+
}).join('');
|
|
1332
|
+
|
|
1333
|
+
return \`
|
|
1334
|
+
<div class="embedding-section">
|
|
1335
|
+
<div class="embedding-header">
|
|
1336
|
+
<span class="embedding-status has-embedding">
|
|
1337
|
+
<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>
|
|
1338
|
+
Vector Enabled
|
|
1339
|
+
</span>
|
|
1340
|
+
<span class="embedding-dims">\${embedding.dimensions}D</span>
|
|
1341
|
+
</div>
|
|
1342
|
+
<div class="embedding-viz">
|
|
1343
|
+
\${bars}
|
|
1344
|
+
</div>
|
|
1345
|
+
<div class="embedding-legend">
|
|
1346
|
+
<div class="embedding-legend-item">
|
|
1347
|
+
<span class="legend-dot positive"></span>
|
|
1348
|
+
Positive values
|
|
1349
|
+
</div>
|
|
1350
|
+
<div class="embedding-legend-item">
|
|
1351
|
+
<span class="legend-dot negative"></span>
|
|
1352
|
+
Negative values
|
|
1353
|
+
</div>
|
|
1354
|
+
<span>First 20 dimensions</span>
|
|
1355
|
+
</div>
|
|
1356
|
+
</div>
|
|
1357
|
+
\`;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
860
1360
|
async function showDetail(id) {
|
|
861
1361
|
try {
|
|
862
1362
|
const res = await fetch('/api/entry/' + id);
|
|
@@ -887,6 +1387,7 @@ function getHTML(): string {
|
|
|
887
1387
|
<div class="detail-label">Created</div>
|
|
888
1388
|
<div class="detail-value">\${new Date(entry.created_at).toLocaleString()}</div>
|
|
889
1389
|
</div>
|
|
1390
|
+
\${renderEmbeddingViz(entry.embedding)}
|
|
890
1391
|
<div class="detail-actions">
|
|
891
1392
|
<button class="btn btn-primary" onclick="openEditModal('\${entry.id}')">
|
|
892
1393
|
<svg viewBox="0 0 24 24"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg>
|
|
@@ -1008,6 +1509,85 @@ function getHTML(): string {
|
|
|
1008
1509
|
}
|
|
1009
1510
|
}
|
|
1010
1511
|
|
|
1512
|
+
async function openEmbeddingModal() {
|
|
1513
|
+
document.getElementById('embedding-modal').classList.add('active');
|
|
1514
|
+
document.getElementById('embedding-progress').style.display = 'none';
|
|
1515
|
+
document.getElementById('embedding-modal-footer').style.display = 'flex';
|
|
1516
|
+
|
|
1517
|
+
// Load embedding stats
|
|
1518
|
+
try {
|
|
1519
|
+
const res = await fetch('/api/embeddings/stats');
|
|
1520
|
+
const embeddingStats = await res.json();
|
|
1521
|
+
document.getElementById('stat-total').textContent = embeddingStats.total;
|
|
1522
|
+
document.getElementById('stat-with').textContent = embeddingStats.withEmbedding;
|
|
1523
|
+
document.getElementById('stat-without').textContent = embeddingStats.withoutEmbedding;
|
|
1524
|
+
|
|
1525
|
+
// Disable buttons if nothing to do
|
|
1526
|
+
document.getElementById('btn-generate-missing').disabled = embeddingStats.withoutEmbedding === 0;
|
|
1527
|
+
document.getElementById('btn-regenerate-all').disabled = embeddingStats.total === 0;
|
|
1528
|
+
} catch (error) {
|
|
1529
|
+
console.error('Failed to load embedding stats:', error);
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
function closeEmbeddingModal(event) {
|
|
1534
|
+
if (!event || event.target.id === 'embedding-modal') {
|
|
1535
|
+
document.getElementById('embedding-modal').classList.remove('active');
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
async function generateEmbeddings(mode) {
|
|
1540
|
+
const progressEl = document.getElementById('embedding-progress');
|
|
1541
|
+
const progressFill = document.getElementById('progress-fill');
|
|
1542
|
+
const progressText = document.getElementById('progress-text');
|
|
1543
|
+
const footerEl = document.getElementById('embedding-modal-footer');
|
|
1544
|
+
|
|
1545
|
+
// Show progress, hide buttons
|
|
1546
|
+
progressEl.style.display = 'block';
|
|
1547
|
+
footerEl.style.display = 'none';
|
|
1548
|
+
progressFill.style.width = '0%';
|
|
1549
|
+
progressText.textContent = mode === 'missing' ? 'Generating missing embeddings...' : 'Re-generating all embeddings...';
|
|
1550
|
+
|
|
1551
|
+
// Animate progress bar while waiting
|
|
1552
|
+
let progress = 0;
|
|
1553
|
+
const interval = setInterval(() => {
|
|
1554
|
+
progress += Math.random() * 10;
|
|
1555
|
+
if (progress > 90) progress = 90;
|
|
1556
|
+
progressFill.style.width = progress + '%';
|
|
1557
|
+
}, 200);
|
|
1558
|
+
|
|
1559
|
+
try {
|
|
1560
|
+
const res = await fetch('/api/embeddings/generate', {
|
|
1561
|
+
method: 'POST',
|
|
1562
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1563
|
+
body: JSON.stringify({ mode }),
|
|
1564
|
+
});
|
|
1565
|
+
|
|
1566
|
+
clearInterval(interval);
|
|
1567
|
+
progressFill.style.width = '100%';
|
|
1568
|
+
|
|
1569
|
+
const result = await res.json();
|
|
1570
|
+
progressText.textContent = result.message || 'Done!';
|
|
1571
|
+
|
|
1572
|
+
setTimeout(() => {
|
|
1573
|
+
showToast(result.message || 'Embeddings generated', 'success');
|
|
1574
|
+
closeEmbeddingModal();
|
|
1575
|
+
loadData();
|
|
1576
|
+
}, 1000);
|
|
1577
|
+
} catch (error) {
|
|
1578
|
+
clearInterval(interval);
|
|
1579
|
+
progressFill.style.width = '0%';
|
|
1580
|
+
progressText.textContent = 'Failed to generate embeddings';
|
|
1581
|
+
showToast('Failed to generate embeddings', 'error');
|
|
1582
|
+
|
|
1583
|
+
// Re-show buttons after error
|
|
1584
|
+
setTimeout(() => {
|
|
1585
|
+
progressEl.style.display = 'none';
|
|
1586
|
+
footerEl.style.display = 'flex';
|
|
1587
|
+
}, 2000);
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1011
1591
|
function showToast(message, type = 'success') {
|
|
1012
1592
|
const toast = document.createElement('div');
|
|
1013
1593
|
toast.className = 'toast ' + type;
|
|
@@ -1042,6 +1622,7 @@ function getHTML(): string {
|
|
|
1042
1622
|
closeDetailModal();
|
|
1043
1623
|
closeFormModal();
|
|
1044
1624
|
closeDeleteModal();
|
|
1625
|
+
closeEmbeddingModal();
|
|
1045
1626
|
}
|
|
1046
1627
|
});
|
|
1047
1628
|
|
|
@@ -1060,24 +1641,23 @@ async function readBody(req: http.IncomingMessage): Promise<string> {
|
|
|
1060
1641
|
});
|
|
1061
1642
|
}
|
|
1062
1643
|
|
|
1063
|
-
|
|
1644
|
+
function handleRequest(
|
|
1064
1645
|
req: http.IncomingMessage,
|
|
1065
1646
|
res: http.ServerResponse
|
|
1066
|
-
):
|
|
1647
|
+
): void {
|
|
1067
1648
|
const url = new URL(req.url || '/', `http://localhost:${PORT}`);
|
|
1068
1649
|
const method = req.method || 'GET';
|
|
1069
1650
|
|
|
1070
1651
|
res.setHeader('Content-Type', 'application/json');
|
|
1071
1652
|
|
|
1072
1653
|
try {
|
|
1073
|
-
const db =
|
|
1654
|
+
const db = getDatabase();
|
|
1074
1655
|
|
|
1075
1656
|
// Serve HTML
|
|
1076
1657
|
if (url.pathname === '/' && method === 'GET') {
|
|
1077
1658
|
res.setHeader('Content-Type', 'text/html');
|
|
1078
1659
|
res.writeHead(200);
|
|
1079
1660
|
res.end(getHTML());
|
|
1080
|
-
db.close();
|
|
1081
1661
|
return;
|
|
1082
1662
|
}
|
|
1083
1663
|
|
|
@@ -1086,11 +1666,10 @@ async function handleRequest(
|
|
|
1086
1666
|
const stats = getStats(db);
|
|
1087
1667
|
res.writeHead(200);
|
|
1088
1668
|
res.end(JSON.stringify(stats));
|
|
1089
|
-
db.close();
|
|
1090
1669
|
return;
|
|
1091
1670
|
}
|
|
1092
1671
|
|
|
1093
|
-
// GET entries
|
|
1672
|
+
// GET entries (standard listing with optional FTS search)
|
|
1094
1673
|
if (url.pathname === '/api/entries' && method === 'GET') {
|
|
1095
1674
|
const namespace = url.searchParams.get('namespace') || undefined;
|
|
1096
1675
|
const limit = parseInt(url.searchParams.get('limit') || '50', 10);
|
|
@@ -1100,38 +1679,107 @@ async function handleRequest(
|
|
|
1100
1679
|
const entries = getEntries(db, namespace, limit, offset, search);
|
|
1101
1680
|
res.writeHead(200);
|
|
1102
1681
|
res.end(JSON.stringify(entries));
|
|
1103
|
-
db.close();
|
|
1104
1682
|
return;
|
|
1105
1683
|
}
|
|
1106
1684
|
|
|
1107
|
-
//
|
|
1685
|
+
// GET hybrid search (new endpoint with vector support)
|
|
1686
|
+
if (url.pathname === '/api/search' && method === 'GET') {
|
|
1687
|
+
const query = url.searchParams.get('q') || '';
|
|
1688
|
+
const searchType = (url.searchParams.get('type') || 'hybrid') as 'hybrid' | 'text' | 'vector';
|
|
1689
|
+
const limit = parseInt(url.searchParams.get('limit') || '20', 10);
|
|
1690
|
+
const namespace = url.searchParams.get('namespace') || undefined;
|
|
1691
|
+
|
|
1692
|
+
getSearchEngine()
|
|
1693
|
+
.then((searchEngine) => searchEntries(searchEngine, query, { type: searchType, namespace, limit }))
|
|
1694
|
+
.then((results) => {
|
|
1695
|
+
res.writeHead(200);
|
|
1696
|
+
res.end(JSON.stringify(results));
|
|
1697
|
+
})
|
|
1698
|
+
.catch((error) => {
|
|
1699
|
+
res.writeHead(500);
|
|
1700
|
+
res.end(JSON.stringify({ error: error instanceof Error ? error.message : 'Search failed' }));
|
|
1701
|
+
});
|
|
1702
|
+
return;
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
// POST create entry (direct DB for compatibility with existing schema)
|
|
1108
1706
|
if (url.pathname === '/api/entries' && method === 'POST') {
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1707
|
+
readBody(req)
|
|
1708
|
+
.then(async (body) => {
|
|
1709
|
+
const data = JSON.parse(body) as {
|
|
1710
|
+
key: string;
|
|
1711
|
+
content: string;
|
|
1712
|
+
type?: string;
|
|
1713
|
+
namespace?: string;
|
|
1714
|
+
tags?: string[];
|
|
1715
|
+
};
|
|
1716
|
+
|
|
1717
|
+
const now = Date.now();
|
|
1718
|
+
const id = `mem_${now}_${Math.random().toString(36).slice(2, 10)}`;
|
|
1719
|
+
const tags = JSON.stringify(data.tags || []);
|
|
1720
|
+
|
|
1721
|
+
// Generate embedding for the content
|
|
1722
|
+
let embeddingBuffer: Buffer | null = null;
|
|
1723
|
+
try {
|
|
1724
|
+
const embeddingsService = await getEmbeddingsService();
|
|
1725
|
+
const result = await embeddingsService.embed(data.content);
|
|
1726
|
+
embeddingBuffer = Buffer.from(result.embedding.buffer);
|
|
1727
|
+
} catch (e) {
|
|
1728
|
+
console.warn('[WebViewer] Failed to generate embedding:', e);
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
db.prepare(
|
|
1732
|
+
`INSERT INTO memory_entries (id, key, content, type, namespace, tags, embedding, created_at, updated_at)
|
|
1733
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
1734
|
+
).run(id, data.key, data.content, data.type || 'semantic', data.namespace || 'general', tags, embeddingBuffer, now, now);
|
|
1735
|
+
|
|
1736
|
+
res.writeHead(201);
|
|
1737
|
+
res.end(JSON.stringify({ id, success: true }));
|
|
1738
|
+
})
|
|
1739
|
+
.catch((error) => {
|
|
1740
|
+
res.writeHead(500);
|
|
1741
|
+
res.end(JSON.stringify({ error: error instanceof Error ? error.message : 'Internal error' }));
|
|
1742
|
+
});
|
|
1124
1743
|
return;
|
|
1125
1744
|
}
|
|
1126
1745
|
|
|
1127
1746
|
// GET single entry
|
|
1128
1747
|
if (url.pathname.startsWith('/api/entry/') && method === 'GET') {
|
|
1129
1748
|
const id = url.pathname.split('/').pop();
|
|
1130
|
-
const
|
|
1131
|
-
|
|
1749
|
+
const row = db.prepare('SELECT * FROM memory_entries WHERE id = ?').get(id) as {
|
|
1750
|
+
id: string;
|
|
1751
|
+
key: string;
|
|
1752
|
+
content: string;
|
|
1753
|
+
type: string;
|
|
1754
|
+
namespace: string;
|
|
1755
|
+
tags: string;
|
|
1756
|
+
embedding: Buffer | null;
|
|
1757
|
+
created_at: number;
|
|
1758
|
+
updated_at: number;
|
|
1759
|
+
} | undefined;
|
|
1760
|
+
|
|
1761
|
+
if (row) {
|
|
1762
|
+
// Extract embedding info for visualization
|
|
1763
|
+
let embeddingInfo: { hasEmbedding: boolean; dimensions?: number; preview?: number[] } = {
|
|
1764
|
+
hasEmbedding: false,
|
|
1765
|
+
};
|
|
1766
|
+
|
|
1767
|
+
if (row.embedding && row.embedding.length > 0) {
|
|
1768
|
+
const embedding = new Float32Array(
|
|
1769
|
+
row.embedding.buffer.slice(
|
|
1770
|
+
row.embedding.byteOffset,
|
|
1771
|
+
row.embedding.byteOffset + row.embedding.byteLength
|
|
1772
|
+
)
|
|
1773
|
+
);
|
|
1774
|
+
// Get first 20 values for preview visualization
|
|
1775
|
+
const preview = Array.from(embedding.slice(0, 20));
|
|
1776
|
+
embeddingInfo = {
|
|
1777
|
+
hasEmbedding: true,
|
|
1778
|
+
dimensions: embedding.length,
|
|
1779
|
+
preview,
|
|
1780
|
+
};
|
|
1781
|
+
}
|
|
1132
1782
|
|
|
1133
|
-
if (stmt.step()) {
|
|
1134
|
-
const row = stmt.getAsObject();
|
|
1135
1783
|
res.writeHead(200);
|
|
1136
1784
|
res.end(JSON.stringify({
|
|
1137
1785
|
id: row.id,
|
|
@@ -1139,53 +1787,159 @@ async function handleRequest(
|
|
|
1139
1787
|
content: row.content,
|
|
1140
1788
|
type: row.type,
|
|
1141
1789
|
namespace: row.namespace,
|
|
1142
|
-
tags: JSON.parse(
|
|
1790
|
+
tags: JSON.parse(row.tags || '[]'),
|
|
1143
1791
|
created_at: row.created_at,
|
|
1144
1792
|
updated_at: row.updated_at,
|
|
1793
|
+
embedding: embeddingInfo,
|
|
1145
1794
|
}));
|
|
1146
1795
|
} else {
|
|
1147
1796
|
res.writeHead(404);
|
|
1148
1797
|
res.end(JSON.stringify({ error: 'Entry not found' }));
|
|
1149
1798
|
}
|
|
1150
|
-
stmt.free();
|
|
1151
|
-
db.close();
|
|
1152
1799
|
return;
|
|
1153
1800
|
}
|
|
1154
1801
|
|
|
1155
|
-
// PUT update entry
|
|
1802
|
+
// PUT update entry (direct DB for full field updates)
|
|
1156
1803
|
if (url.pathname.startsWith('/api/entry/') && method === 'PUT') {
|
|
1157
1804
|
const id = url.pathname.split('/').pop();
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
`UPDATE memory_entries SET key = ?, content = ?, type = ?, namespace = ?, tags = ?, updated_at = ?
|
|
1164
|
-
WHERE id = ?`,
|
|
1165
|
-
[data.key, data.content, data.type, data.namespace, JSON.stringify(data.tags || []), now, id]
|
|
1166
|
-
);
|
|
1805
|
+
if (!id) {
|
|
1806
|
+
res.writeHead(400);
|
|
1807
|
+
res.end(JSON.stringify({ error: 'Missing entry ID' }));
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1167
1810
|
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1811
|
+
readBody(req)
|
|
1812
|
+
.then(async (body) => {
|
|
1813
|
+
const data = JSON.parse(body) as {
|
|
1814
|
+
key: string;
|
|
1815
|
+
content: string;
|
|
1816
|
+
type: string;
|
|
1817
|
+
namespace: string;
|
|
1818
|
+
tags?: string[];
|
|
1819
|
+
};
|
|
1820
|
+
const now = Date.now();
|
|
1821
|
+
const tags = JSON.stringify(data.tags || []);
|
|
1822
|
+
|
|
1823
|
+
// Generate embedding for the updated content
|
|
1824
|
+
let embeddingBuffer: Buffer | null = null;
|
|
1825
|
+
try {
|
|
1826
|
+
const embeddingsService = await getEmbeddingsService();
|
|
1827
|
+
const result = await embeddingsService.embed(data.content);
|
|
1828
|
+
embeddingBuffer = Buffer.from(result.embedding.buffer);
|
|
1829
|
+
} catch (e) {
|
|
1830
|
+
console.warn('[WebViewer] Failed to generate embedding:', e);
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
// Update with embedding
|
|
1834
|
+
const result = db.prepare(
|
|
1835
|
+
`UPDATE memory_entries SET key = ?, content = ?, type = ?, namespace = ?, tags = ?, embedding = ?, updated_at = ?
|
|
1836
|
+
WHERE id = ?`
|
|
1837
|
+
).run(data.key, data.content, data.type, data.namespace, tags, embeddingBuffer, now, id);
|
|
1838
|
+
|
|
1839
|
+
if (result.changes > 0) {
|
|
1840
|
+
res.writeHead(200);
|
|
1841
|
+
res.end(JSON.stringify({ success: true }));
|
|
1842
|
+
} else {
|
|
1843
|
+
res.writeHead(404);
|
|
1844
|
+
res.end(JSON.stringify({ error: 'Entry not found' }));
|
|
1845
|
+
}
|
|
1846
|
+
})
|
|
1847
|
+
.catch((error) => {
|
|
1848
|
+
res.writeHead(500);
|
|
1849
|
+
res.end(JSON.stringify({ error: error instanceof Error ? error.message : 'Internal error' }));
|
|
1850
|
+
});
|
|
1172
1851
|
return;
|
|
1173
1852
|
}
|
|
1174
1853
|
|
|
1175
|
-
// DELETE entry
|
|
1854
|
+
// DELETE entry (direct DB for compatibility)
|
|
1176
1855
|
if (url.pathname.startsWith('/api/entry/') && method === 'DELETE') {
|
|
1177
1856
|
const id = url.pathname.split('/').pop();
|
|
1178
|
-
|
|
1179
|
-
|
|
1857
|
+
if (!id) {
|
|
1858
|
+
res.writeHead(400);
|
|
1859
|
+
res.end(JSON.stringify({ error: 'Missing entry ID' }));
|
|
1860
|
+
return;
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
const result = db.prepare('DELETE FROM memory_entries WHERE id = ?').run(id);
|
|
1864
|
+
if (result.changes > 0) {
|
|
1865
|
+
res.writeHead(200);
|
|
1866
|
+
res.end(JSON.stringify({ success: true }));
|
|
1867
|
+
} else {
|
|
1868
|
+
res.writeHead(404);
|
|
1869
|
+
res.end(JSON.stringify({ error: 'Entry not found' }));
|
|
1870
|
+
}
|
|
1871
|
+
return;
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
// GET embedding stats
|
|
1875
|
+
if (url.pathname === '/api/embeddings/stats' && method === 'GET') {
|
|
1876
|
+
const totalRow = db.prepare('SELECT COUNT(*) as count FROM memory_entries').get() as { count: number };
|
|
1877
|
+
const withEmbeddingRow = db.prepare('SELECT COUNT(*) as count FROM memory_entries WHERE embedding IS NOT NULL AND LENGTH(embedding) > 0').get() as { count: number };
|
|
1878
|
+
|
|
1180
1879
|
res.writeHead(200);
|
|
1181
|
-
res.end(JSON.stringify({
|
|
1182
|
-
|
|
1880
|
+
res.end(JSON.stringify({
|
|
1881
|
+
total: totalRow?.count || 0,
|
|
1882
|
+
withEmbedding: withEmbeddingRow?.count || 0,
|
|
1883
|
+
withoutEmbedding: (totalRow?.count || 0) - (withEmbeddingRow?.count || 0),
|
|
1884
|
+
}));
|
|
1885
|
+
return;
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
// POST batch generate embeddings
|
|
1889
|
+
if (url.pathname === '/api/embeddings/generate' && method === 'POST') {
|
|
1890
|
+
readBody(req)
|
|
1891
|
+
.then(async (body) => {
|
|
1892
|
+
const options = JSON.parse(body || '{}') as { mode?: 'missing' | 'all' };
|
|
1893
|
+
const mode = options.mode || 'missing';
|
|
1894
|
+
|
|
1895
|
+
// Get entries to process
|
|
1896
|
+
const query = mode === 'missing'
|
|
1897
|
+
? 'SELECT id, content FROM memory_entries WHERE embedding IS NULL OR LENGTH(embedding) = 0'
|
|
1898
|
+
: 'SELECT id, content FROM memory_entries';
|
|
1899
|
+
|
|
1900
|
+
const entries = db.prepare(query).all() as { id: string; content: string }[];
|
|
1901
|
+
|
|
1902
|
+
if (entries.length === 0) {
|
|
1903
|
+
res.writeHead(200);
|
|
1904
|
+
res.end(JSON.stringify({ processed: 0, success: 0, failed: 0, message: 'No entries to process' }));
|
|
1905
|
+
return;
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
const embeddingsService = await getEmbeddingsService();
|
|
1909
|
+
let success = 0;
|
|
1910
|
+
let failed = 0;
|
|
1911
|
+
|
|
1912
|
+
const updateStmt = db.prepare('UPDATE memory_entries SET embedding = ?, updated_at = ? WHERE id = ?');
|
|
1913
|
+
|
|
1914
|
+
for (const entry of entries) {
|
|
1915
|
+
try {
|
|
1916
|
+
const result = await embeddingsService.embed(entry.content);
|
|
1917
|
+
const embeddingBuffer = Buffer.from(result.embedding.buffer);
|
|
1918
|
+
updateStmt.run(embeddingBuffer, Date.now(), entry.id);
|
|
1919
|
+
success++;
|
|
1920
|
+
} catch (e) {
|
|
1921
|
+
console.warn(`[WebViewer] Failed to generate embedding for ${entry.id}:`, e);
|
|
1922
|
+
failed++;
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
res.writeHead(200);
|
|
1927
|
+
res.end(JSON.stringify({
|
|
1928
|
+
processed: entries.length,
|
|
1929
|
+
success,
|
|
1930
|
+
failed,
|
|
1931
|
+
message: `Generated embeddings for ${success} entries${failed > 0 ? `, ${failed} failed` : ''}`,
|
|
1932
|
+
}));
|
|
1933
|
+
})
|
|
1934
|
+
.catch((error) => {
|
|
1935
|
+
res.writeHead(500);
|
|
1936
|
+
res.end(JSON.stringify({ error: error instanceof Error ? error.message : 'Internal error' }));
|
|
1937
|
+
});
|
|
1183
1938
|
return;
|
|
1184
1939
|
}
|
|
1185
1940
|
|
|
1186
1941
|
res.writeHead(404);
|
|
1187
1942
|
res.end(JSON.stringify({ error: 'Not found' }));
|
|
1188
|
-
db.close();
|
|
1189
1943
|
} catch (error) {
|
|
1190
1944
|
res.writeHead(500);
|
|
1191
1945
|
res.end(JSON.stringify({ error: error instanceof Error ? error.message : 'Internal error' }));
|