morpheus-cli 0.2.6 → 0.2.8
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 +428 -2
- package/dist/channels/telegram.js +169 -19
- package/dist/cli/commands/doctor.js +1 -1
- package/dist/cli/commands/session.js +79 -0
- package/dist/cli/commands/start.js +3 -0
- package/dist/cli/index.js +3 -1
- package/dist/http/api.js +25 -0
- package/dist/runtime/memory/backfill-embeddings.js +54 -0
- package/dist/runtime/memory/embedding.service.js +21 -0
- package/dist/runtime/memory/sati/index.js +5 -5
- package/dist/runtime/memory/sati/repository.js +320 -116
- package/dist/runtime/memory/sati/service.js +48 -25
- package/dist/runtime/memory/sati/system-prompts.js +19 -8
- package/dist/runtime/memory/session-embedding-worker.js +99 -0
- package/dist/runtime/memory/sqlite-vec.js +6 -0
- package/dist/runtime/memory/sqlite.js +415 -3
- package/dist/runtime/oracle.js +13 -1
- package/dist/runtime/session-embedding-scheduler.js +21 -0
- package/dist/ui/assets/{index-BrbyUtJ5.js → index-Dx1lwaMu.js} +2 -2
- package/dist/ui/assets/index-QHZ08tDL.css +1 -0
- package/dist/ui/index.html +2 -2
- package/package.json +10 -3
- package/dist/ui/assets/index-BiXkm8Yr.css +0 -1
|
@@ -2,13 +2,18 @@ import Database from 'better-sqlite3';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
4
|
import fs from 'fs-extra';
|
|
5
|
-
import { randomUUID } from 'crypto';
|
|
5
|
+
import { randomUUID } from 'crypto';
|
|
6
|
+
import loadVecExtension from '../sqlite-vec.js';
|
|
7
|
+
import { DisplayManager } from '../../display.js';
|
|
8
|
+
const EMBEDDING_DIM = 384;
|
|
6
9
|
export class SatiRepository {
|
|
7
10
|
db = null;
|
|
8
11
|
dbPath;
|
|
9
12
|
static instance;
|
|
13
|
+
display = DisplayManager.getInstance();
|
|
10
14
|
constructor(dbPath) {
|
|
11
|
-
this.dbPath =
|
|
15
|
+
this.dbPath =
|
|
16
|
+
dbPath || path.join(homedir(), '.morpheus', 'memory', 'sati-memory.db');
|
|
12
17
|
}
|
|
13
18
|
static getInstance(dbPath) {
|
|
14
19
|
if (!SatiRepository.instance) {
|
|
@@ -17,59 +22,248 @@ export class SatiRepository {
|
|
|
17
22
|
return SatiRepository.instance;
|
|
18
23
|
}
|
|
19
24
|
initialize() {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
this.db.pragma('journal_mode = WAL');
|
|
26
|
-
// Create schema
|
|
27
|
-
this.createSchema();
|
|
28
|
-
}
|
|
29
|
-
catch (error) {
|
|
30
|
-
console.error(`[SatiRepository] Failed to initialize database: ${error}`);
|
|
31
|
-
throw error;
|
|
32
|
-
}
|
|
25
|
+
fs.ensureDirSync(path.dirname(this.dbPath));
|
|
26
|
+
this.db = new Database(this.dbPath, { timeout: 5000 });
|
|
27
|
+
this.db.pragma('journal_mode = WAL');
|
|
28
|
+
loadVecExtension(this.db);
|
|
29
|
+
this.createSchema();
|
|
33
30
|
}
|
|
34
31
|
createSchema() {
|
|
35
32
|
if (!this.db)
|
|
36
|
-
throw new Error(
|
|
33
|
+
throw new Error('DB not initialized');
|
|
37
34
|
this.db.exec(`
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
35
|
+
-- ===============================
|
|
36
|
+
-- 1️⃣ TABELA PRINCIPAL
|
|
37
|
+
-- ===============================
|
|
38
|
+
CREATE TABLE IF NOT EXISTS long_term_memory (
|
|
39
|
+
id TEXT PRIMARY KEY,
|
|
40
|
+
category TEXT NOT NULL,
|
|
41
|
+
importance TEXT NOT NULL,
|
|
42
|
+
summary TEXT NOT NULL,
|
|
43
|
+
details TEXT,
|
|
44
|
+
hash TEXT NOT NULL UNIQUE,
|
|
45
|
+
source TEXT,
|
|
46
|
+
created_at TEXT NOT NULL,
|
|
47
|
+
updated_at TEXT NOT NULL,
|
|
48
|
+
last_accessed_at TEXT,
|
|
49
|
+
access_count INTEGER DEFAULT 0,
|
|
50
|
+
version INTEGER DEFAULT 1,
|
|
51
|
+
archived INTEGER DEFAULT 0
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
CREATE INDEX IF NOT EXISTS idx_memory_category
|
|
55
|
+
ON long_term_memory(category);
|
|
56
|
+
|
|
57
|
+
CREATE INDEX IF NOT EXISTS idx_memory_importance
|
|
58
|
+
ON long_term_memory(importance);
|
|
59
|
+
|
|
60
|
+
CREATE INDEX IF NOT EXISTS idx_memory_archived
|
|
61
|
+
ON long_term_memory(archived);
|
|
62
|
+
|
|
63
|
+
-- ===============================
|
|
64
|
+
-- 2️⃣ FTS5
|
|
65
|
+
-- ===============================
|
|
66
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5(
|
|
67
|
+
summary,
|
|
68
|
+
details,
|
|
69
|
+
content='long_term_memory',
|
|
70
|
+
content_rowid='rowid',
|
|
71
|
+
tokenize = 'unicode61 remove_diacritics 2'
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
-- ===============================
|
|
75
|
+
-- 3️⃣ VECTOR TABLE (vec0)
|
|
76
|
+
-- ===============================
|
|
77
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memory_vec USING vec0(
|
|
78
|
+
embedding float[${EMBEDDING_DIM}]
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
CREATE TABLE IF NOT EXISTS memory_embedding_map (
|
|
82
|
+
memory_id TEXT PRIMARY KEY,
|
|
83
|
+
vec_rowid INTEGER NOT NULL
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
-- ===============================
|
|
87
|
+
-- 4️⃣ TRIGGERS FTS
|
|
88
|
+
-- ===============================
|
|
89
|
+
CREATE TRIGGER IF NOT EXISTS memory_ai
|
|
90
|
+
AFTER INSERT ON long_term_memory BEGIN
|
|
91
|
+
INSERT INTO memory_fts(rowid, summary, details)
|
|
92
|
+
VALUES (new.rowid, new.summary, new.details);
|
|
93
|
+
END;
|
|
94
|
+
|
|
95
|
+
CREATE TRIGGER IF NOT EXISTS memory_ad
|
|
96
|
+
AFTER DELETE ON long_term_memory BEGIN
|
|
97
|
+
INSERT INTO memory_fts(memory_fts, rowid, summary, details)
|
|
98
|
+
VALUES('delete', old.rowid, old.summary, old.details);
|
|
99
|
+
END;
|
|
100
|
+
|
|
101
|
+
CREATE TRIGGER IF NOT EXISTS memory_au
|
|
102
|
+
AFTER UPDATE ON long_term_memory BEGIN
|
|
103
|
+
INSERT INTO memory_fts(memory_fts, rowid, summary, details)
|
|
104
|
+
VALUES('delete', old.rowid, old.summary, old.details);
|
|
105
|
+
|
|
106
|
+
INSERT INTO memory_fts(rowid, summary, details)
|
|
107
|
+
VALUES (new.rowid, new.summary, new.details);
|
|
108
|
+
END;
|
|
109
|
+
|
|
110
|
+
-- ===============================
|
|
111
|
+
-- 3️⃣ VECTOR TABLE SESSIONS (vec0)
|
|
112
|
+
-- ===============================
|
|
113
|
+
|
|
114
|
+
CREATE TABLE IF NOT EXISTS session_chunks (
|
|
115
|
+
id TEXT PRIMARY KEY,
|
|
116
|
+
session_id TEXT NOT NULL,
|
|
117
|
+
chunk_index INTEGER NOT NULL,
|
|
118
|
+
content TEXT NOT NULL,
|
|
119
|
+
created_at TEXT NOT NULL
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS session_vec USING vec0(
|
|
124
|
+
embedding float[384]
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
CREATE TABLE IF NOT EXISTS session_embedding_map (
|
|
128
|
+
session_chunk_id TEXT PRIMARY KEY,
|
|
129
|
+
vec_rowid INTEGER NOT NULL
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
`);
|
|
133
|
+
}
|
|
134
|
+
// 🔥 NOVO — Salvar embedding
|
|
135
|
+
upsertEmbedding(memoryId, embedding) {
|
|
136
|
+
if (!this.db)
|
|
137
|
+
this.initialize();
|
|
138
|
+
const getExisting = this.db.prepare(`
|
|
139
|
+
SELECT vec_rowid FROM memory_embedding_map
|
|
140
|
+
WHERE memory_id = ?
|
|
141
|
+
`);
|
|
142
|
+
const insertVec = this.db.prepare(`
|
|
143
|
+
INSERT INTO memory_vec (embedding)
|
|
144
|
+
VALUES (?)
|
|
145
|
+
`);
|
|
146
|
+
const deleteVec = this.db.prepare(`
|
|
147
|
+
DELETE FROM memory_vec WHERE rowid = ?
|
|
148
|
+
`);
|
|
149
|
+
const upsertMap = this.db.prepare(`
|
|
150
|
+
INSERT OR REPLACE INTO memory_embedding_map (memory_id, vec_rowid)
|
|
151
|
+
VALUES (?, ?)
|
|
152
|
+
`);
|
|
153
|
+
const transaction = this.db.transaction(() => {
|
|
154
|
+
const existing = getExisting.get(memoryId);
|
|
155
|
+
if (existing?.vec_rowid) {
|
|
156
|
+
deleteVec.run(existing.vec_rowid);
|
|
157
|
+
}
|
|
158
|
+
const result = insertVec.run(new Float32Array(embedding));
|
|
159
|
+
const newVecRowId = result.lastInsertRowid;
|
|
160
|
+
upsertMap.run(memoryId, newVecRowId);
|
|
161
|
+
});
|
|
162
|
+
transaction();
|
|
163
|
+
}
|
|
164
|
+
// 🔥 NOVO — Busca vetorial
|
|
165
|
+
searchByVector(embedding, limit) {
|
|
166
|
+
if (!this.db)
|
|
167
|
+
return [];
|
|
168
|
+
const SIMILARITY_THRESHOLD = 0.5; // ajuste fino depois
|
|
169
|
+
const stmt = this.db.prepare(`
|
|
170
|
+
SELECT
|
|
171
|
+
m.*,
|
|
172
|
+
vec_distance_cosine(v.embedding, ?) as distance
|
|
173
|
+
FROM memory_vec v
|
|
174
|
+
JOIN memory_embedding_map map ON map.vec_rowid = v.rowid
|
|
175
|
+
JOIN long_term_memory m ON m.id = map.memory_id
|
|
176
|
+
WHERE m.archived = 0
|
|
177
|
+
ORDER BY distance ASC
|
|
178
|
+
LIMIT ?
|
|
179
|
+
`);
|
|
180
|
+
const rows = stmt.all(new Float32Array(embedding), limit);
|
|
181
|
+
// 🔥 Filtrar por similaridade real
|
|
182
|
+
const ranked = rows
|
|
183
|
+
.map(r => ({
|
|
184
|
+
...r,
|
|
185
|
+
similarity: 1 - r.distance
|
|
186
|
+
}))
|
|
187
|
+
.sort((a, b) => b.distance - a.distance);
|
|
188
|
+
const filtered = ranked
|
|
189
|
+
.filter(r => r.distance >= SIMILARITY_THRESHOLD)
|
|
190
|
+
.sort((a, b) => b.distance - a.distance);
|
|
191
|
+
if (filtered.length > 0) {
|
|
192
|
+
console.log(`[SatiRepository] Vector hit (${filtered.length})`);
|
|
193
|
+
}
|
|
194
|
+
return filtered.map(this.mapRowToRecord);
|
|
195
|
+
}
|
|
196
|
+
searchUnifiedVector(embedding, limit) {
|
|
197
|
+
if (!this.db)
|
|
198
|
+
return [];
|
|
199
|
+
const SIMILARITY_THRESHOLD = 0.75;
|
|
200
|
+
const stmt = this.db.prepare(`
|
|
201
|
+
SELECT *
|
|
202
|
+
FROM (
|
|
203
|
+
-- LONG TERM MEMORY
|
|
204
|
+
SELECT
|
|
205
|
+
m.id as id,
|
|
206
|
+
m.summary as summary,
|
|
207
|
+
m.details as details,
|
|
208
|
+
m.category as category,
|
|
209
|
+
m.importance as importance,
|
|
210
|
+
'long_term' as source_type,
|
|
211
|
+
(1 - vec_distance_cosine(v.embedding, ?)) * 0.7 as distance
|
|
212
|
+
FROM memory_vec v
|
|
213
|
+
JOIN memory_embedding_map map ON map.vec_rowid = v.rowid
|
|
214
|
+
JOIN long_term_memory m ON m.id = map.memory_id
|
|
215
|
+
WHERE m.archived = 0
|
|
216
|
+
|
|
217
|
+
UNION ALL
|
|
218
|
+
|
|
219
|
+
-- SESSION CHUNKS
|
|
220
|
+
SELECT
|
|
221
|
+
sc.id as id,
|
|
222
|
+
sc.content as summary,
|
|
223
|
+
sc.content as details,
|
|
224
|
+
'session' as category,
|
|
225
|
+
'medium' as importance,
|
|
226
|
+
'session_chunk' as source_type,
|
|
227
|
+
(1 - vec_distance_cosine(v.embedding, ?)) * 0.3 as distance
|
|
228
|
+
FROM session_vec v
|
|
229
|
+
JOIN session_embedding_map map ON map.vec_rowid = v.rowid
|
|
230
|
+
JOIN session_chunks sc ON sc.id = map.session_chunk_id
|
|
231
|
+
)
|
|
232
|
+
ORDER BY distance ASC
|
|
233
|
+
LIMIT ?
|
|
234
|
+
`);
|
|
235
|
+
const rows = stmt.all(new Float32Array(embedding), new Float32Array(embedding), limit);
|
|
236
|
+
// console.log(
|
|
237
|
+
// `[SatiRepository] Unified vector search returned ${rows.length} raw results`
|
|
238
|
+
// );
|
|
239
|
+
// console each row
|
|
240
|
+
// rows.forEach((row, index) => {
|
|
241
|
+
// console.log(`[SatiRepository] Row ${index + 1}:`, row);
|
|
242
|
+
// });
|
|
243
|
+
const ranked = rows
|
|
244
|
+
.map(r => ({
|
|
245
|
+
...r,
|
|
246
|
+
similarity: 1 - r.distance
|
|
247
|
+
}))
|
|
248
|
+
.sort((a, b) => b.similarity - a.similarity);
|
|
249
|
+
const filtered = ranked
|
|
250
|
+
.filter(r => r.similarity >= SIMILARITY_THRESHOLD)
|
|
251
|
+
.sort((a, b) => b.similarity - a.similarity);
|
|
252
|
+
this.display.log(`🧠 Unified vector search retornou ${filtered.length} resultados`, { source: 'Sati', level: 'debug' });
|
|
253
|
+
return filtered.map(r => ({
|
|
254
|
+
id: r.id,
|
|
255
|
+
summary: r.summary,
|
|
256
|
+
details: r.details,
|
|
257
|
+
category: r.category,
|
|
258
|
+
importance: r.importance,
|
|
259
|
+
hash: '',
|
|
260
|
+
source: r.source_type,
|
|
261
|
+
created_at: new Date(),
|
|
262
|
+
updated_at: new Date(),
|
|
263
|
+
access_count: 0,
|
|
264
|
+
version: 1,
|
|
265
|
+
archived: false
|
|
266
|
+
}));
|
|
73
267
|
}
|
|
74
268
|
async save(record) {
|
|
75
269
|
if (!this.db)
|
|
@@ -118,73 +312,81 @@ export class SatiRepository {
|
|
|
118
312
|
const row = this.db.prepare('SELECT * FROM long_term_memory WHERE hash = ?').get(hash);
|
|
119
313
|
return row ? this.mapRowToRecord(row) : null;
|
|
120
314
|
}
|
|
121
|
-
search(query, limit = 5) {
|
|
315
|
+
search(query, limit = 5, embedding) {
|
|
122
316
|
if (!this.db)
|
|
123
317
|
this.initialize();
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
318
|
+
try {
|
|
319
|
+
this.display.log(`🔍 Iniciando busca de memória | Query: "${query}"`, { source: 'Sati', level: 'debug' });
|
|
320
|
+
// 1️⃣ Vetorial
|
|
321
|
+
if (embedding) {
|
|
322
|
+
this.display.log('🧠 Tentando busca vetorial...', { source: 'Sati', level: 'debug' });
|
|
323
|
+
const vectorResults = this.searchUnifiedVector(embedding, limit);
|
|
324
|
+
if (vectorResults.length > 0) {
|
|
325
|
+
this.display.log(`✅ Vetorial retornou ${vectorResults.length} resultado(s)`, { source: 'Sati', level: 'success' });
|
|
326
|
+
return vectorResults.slice(0, limit);
|
|
327
|
+
}
|
|
328
|
+
this.display.log('⚠️ Vetorial não encontrou resultados relevantes', { source: 'Sati', level: 'debug' });
|
|
329
|
+
}
|
|
330
|
+
// 2️⃣ BM25 (FTS)
|
|
331
|
+
// Sanitize query: remove characters that could break FTS5 syntax (like ?, *, OR, etc)
|
|
332
|
+
// keeping only letters, numbers and spaces.
|
|
333
|
+
const safeQuery = query
|
|
334
|
+
.replace(/[^\p{L}\p{N}\s]/gu, ' ')
|
|
335
|
+
.replace(/\s+/g, ' ')
|
|
336
|
+
.trim();
|
|
337
|
+
if (safeQuery) {
|
|
338
|
+
this.display.log('📚 Tentando busca BM25 (FTS5)...', { source: 'Sati', level: 'debug' });
|
|
339
|
+
const stmt = this.db.prepare(`
|
|
340
|
+
SELECT m.*, bm25(memory_fts) as rank
|
|
341
|
+
FROM long_term_memory m
|
|
342
|
+
JOIN memory_fts ON m.rowid = memory_fts.rowid
|
|
343
|
+
WHERE memory_fts MATCH ?
|
|
344
|
+
AND m.archived = 0
|
|
345
|
+
ORDER BY rank
|
|
346
|
+
LIMIT ?
|
|
347
|
+
`);
|
|
348
|
+
const rows = stmt.all(safeQuery, limit);
|
|
349
|
+
if (rows.length > 0) {
|
|
350
|
+
this.display.log(`✅ BM25 retornou ${rows.length} resultado(s)`, { source: 'Sati', level: 'success' });
|
|
351
|
+
return rows.map(this.mapRowToRecord);
|
|
352
|
+
}
|
|
353
|
+
this.display.log('⚠️ BM25 não encontrou resultados', { source: 'Sati', level: 'debug' });
|
|
354
|
+
}
|
|
355
|
+
// 3️⃣ LIKE fallback
|
|
356
|
+
this.display.log('🧵 Tentando fallback LIKE...', { source: 'Sati', level: 'debug' });
|
|
357
|
+
const likeStmt = this.db.prepare(`
|
|
358
|
+
SELECT * FROM long_term_memory
|
|
359
|
+
WHERE (summary LIKE ? OR details LIKE ?)
|
|
360
|
+
AND archived = 0
|
|
361
|
+
ORDER BY importance DESC, access_count DESC
|
|
362
|
+
LIMIT ?
|
|
363
|
+
`);
|
|
364
|
+
const pattern = `%${query}%`;
|
|
365
|
+
const likeRows = likeStmt.all(pattern, pattern, limit);
|
|
366
|
+
if (likeRows.length > 0) {
|
|
367
|
+
this.display.log(`✅ LIKE retornou ${likeRows.length} resultado(s)`, { source: 'Sati', level: 'success' });
|
|
368
|
+
return likeRows.map(this.mapRowToRecord);
|
|
369
|
+
}
|
|
370
|
+
// 4️⃣ Final fallback
|
|
371
|
+
this.display.log('🛟 Nenhum mecanismo encontrou resultados. Usando fallback estratégico.', { source: 'Sati', level: 'warning' });
|
|
372
|
+
return this.getFallbackMemories(limit);
|
|
373
|
+
}
|
|
374
|
+
catch (e) {
|
|
375
|
+
this.display.log(`❌ Erro durante busca: ${e}`, { source: 'Sati', level: 'error' });
|
|
376
|
+
return this.getFallbackMemories(limit);
|
|
377
|
+
}
|
|
169
378
|
}
|
|
170
379
|
getFallbackMemories(limit) {
|
|
171
380
|
if (!this.db)
|
|
172
381
|
return [];
|
|
173
|
-
const
|
|
382
|
+
const rows = this.db
|
|
383
|
+
.prepare(`
|
|
174
384
|
SELECT * FROM long_term_memory
|
|
175
385
|
WHERE archived = 0
|
|
176
|
-
ORDER BY
|
|
177
|
-
CASE importance
|
|
178
|
-
WHEN 'critical' THEN 1
|
|
179
|
-
WHEN 'high' THEN 2
|
|
180
|
-
WHEN 'medium' THEN 3
|
|
181
|
-
WHEN 'low' THEN 4
|
|
182
|
-
END,
|
|
183
|
-
access_count DESC,
|
|
184
|
-
created_at DESC
|
|
386
|
+
ORDER BY access_count DESC, created_at DESC
|
|
185
387
|
LIMIT ?
|
|
186
|
-
|
|
187
|
-
|
|
388
|
+
`)
|
|
389
|
+
.all(limit);
|
|
188
390
|
return rows.map(this.mapRowToRecord);
|
|
189
391
|
}
|
|
190
392
|
getAllMemories() {
|
|
@@ -204,18 +406,14 @@ export class SatiRepository {
|
|
|
204
406
|
source: row.source,
|
|
205
407
|
created_at: new Date(row.created_at),
|
|
206
408
|
updated_at: new Date(row.updated_at),
|
|
207
|
-
last_accessed_at: row.last_accessed_at
|
|
409
|
+
last_accessed_at: row.last_accessed_at
|
|
410
|
+
? new Date(row.last_accessed_at)
|
|
411
|
+
: undefined,
|
|
208
412
|
access_count: row.access_count,
|
|
209
413
|
version: row.version,
|
|
210
414
|
archived: Boolean(row.archived)
|
|
211
415
|
};
|
|
212
416
|
}
|
|
213
|
-
close() {
|
|
214
|
-
if (this.db) {
|
|
215
|
-
this.db.close();
|
|
216
|
-
this.db = null;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
417
|
archiveMemory(id) {
|
|
220
418
|
if (!this.db)
|
|
221
419
|
this.initialize();
|
|
@@ -223,4 +421,10 @@ export class SatiRepository {
|
|
|
223
421
|
const result = stmt.run(id);
|
|
224
422
|
return result.changes > 0;
|
|
225
423
|
}
|
|
424
|
+
close() {
|
|
425
|
+
if (this.db) {
|
|
426
|
+
this.db.close();
|
|
427
|
+
this.db = null;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
226
430
|
}
|
|
@@ -6,6 +6,7 @@ import { SATI_EVALUATION_PROMPT } from './system-prompts.js';
|
|
|
6
6
|
import { createHash } from 'crypto';
|
|
7
7
|
import { DisplayManager } from '../../display.js';
|
|
8
8
|
import { SQLiteChatMessageHistory } from '../sqlite.js';
|
|
9
|
+
import { EmbeddingService } from '../embedding.service.js';
|
|
9
10
|
const display = DisplayManager.getInstance();
|
|
10
11
|
export class SatiService {
|
|
11
12
|
repository;
|
|
@@ -24,11 +25,21 @@ export class SatiService {
|
|
|
24
25
|
}
|
|
25
26
|
async recover(currentMessage, recentMessages) {
|
|
26
27
|
const santiConfig = ConfigManager.getInstance().getSatiConfig();
|
|
27
|
-
const memoryLimit = santiConfig.memory_limit ||
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
const memoryLimit = santiConfig.memory_limit || 10;
|
|
29
|
+
let queryEmbedding;
|
|
30
|
+
try {
|
|
31
|
+
const embeddingService = await EmbeddingService.getInstance();
|
|
32
|
+
const queryText = [
|
|
33
|
+
...recentMessages.slice(-3),
|
|
34
|
+
currentMessage
|
|
35
|
+
].join(' ');
|
|
36
|
+
queryEmbedding = await embeddingService.generate(queryText);
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
console.warn('[Sati] Failed to generate embedding:', err);
|
|
40
|
+
}
|
|
41
|
+
const memories = this.repository.search(currentMessage, memoryLimit, queryEmbedding // 🔥 agora vai usar vector search
|
|
42
|
+
);
|
|
32
43
|
return {
|
|
33
44
|
relevant_memories: memories.map(m => ({
|
|
34
45
|
summary: m.summary,
|
|
@@ -100,7 +111,7 @@ export class SatiService {
|
|
|
100
111
|
}
|
|
101
112
|
// Safe JSON parsing (handle markdown blocks if LLM wraps output)
|
|
102
113
|
content = content.replace(/```json/g, '').replace(/```/g, '').trim();
|
|
103
|
-
let result;
|
|
114
|
+
let result = [];
|
|
104
115
|
try {
|
|
105
116
|
result = JSON.parse(content);
|
|
106
117
|
}
|
|
@@ -108,26 +119,38 @@ export class SatiService {
|
|
|
108
119
|
console.warn('[SatiService] Failed to parse JSON response:', content);
|
|
109
120
|
return;
|
|
110
121
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
122
|
+
for (const item of result) {
|
|
123
|
+
if (item.should_store && item.summary && item.category && item.importance) {
|
|
124
|
+
display.log(`Persisting new memory: [${item.category.toUpperCase()}] ${item.summary}`, { source: 'Sati' });
|
|
125
|
+
try {
|
|
126
|
+
const savedMemory = await this.repository.save({
|
|
127
|
+
summary: item.summary,
|
|
128
|
+
category: item.category,
|
|
129
|
+
importance: item.importance,
|
|
130
|
+
details: item.reason,
|
|
131
|
+
hash: this.generateHash(item.summary),
|
|
132
|
+
source: 'conversation'
|
|
133
|
+
});
|
|
134
|
+
// 🔥 GERAR EMBEDDING
|
|
135
|
+
const embeddingService = await EmbeddingService.getInstance();
|
|
136
|
+
const textForEmbedding = [
|
|
137
|
+
savedMemory.summary,
|
|
138
|
+
savedMemory.details ?? ''
|
|
139
|
+
].join(' ');
|
|
140
|
+
const embedding = await embeddingService.generate(textForEmbedding);
|
|
141
|
+
display.log(`Generated embedding for memory ID ${savedMemory.id}`, { source: 'Sati', level: 'debug' });
|
|
142
|
+
// 🔥 SALVAR EMBEDDING NO SQLITE_VEC
|
|
143
|
+
this.repository.upsertEmbedding(savedMemory.id, embedding);
|
|
144
|
+
// Quiet success - logging handled by repository/middleware if needed, or verbose debug
|
|
128
145
|
}
|
|
129
|
-
|
|
130
|
-
|
|
146
|
+
catch (saveError) {
|
|
147
|
+
if (saveError.message && saveError.message.includes('UNIQUE constraint failed')) {
|
|
148
|
+
// Duplicate detected by DB (Hash collision)
|
|
149
|
+
// This is expected given T012 logic
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
throw saveError;
|
|
153
|
+
}
|
|
131
154
|
}
|
|
132
155
|
}
|
|
133
156
|
}
|
|
@@ -22,6 +22,7 @@ Classify any new memory into one of these types:
|
|
|
22
22
|
- **professional_profile**: Job title, industry, skills.
|
|
23
23
|
|
|
24
24
|
### CRITICAL RULES
|
|
25
|
+
0. **SAVE ON SUMMARY and REASONING IN ENGLISH AND NATIVE LANGUAGE**: Always generate a concise summary in English and, if the original information is in another language, also provide a summary in the original language. This ensures the memory is accessible and useful for future interactions, regardless of the language used.
|
|
25
26
|
1. **NO SECRETS**: NEVER store API keys, passwords, credit cards, or private tokens. If found, ignore them explicitly.
|
|
26
27
|
2. **NO DUPLICATES**: If the information is already covered by the \`existing_memory_summaries\`, DO NOT store it again.
|
|
27
28
|
3. **NO CHIT-CHAT**: Do not store trivial conversation like "Hello", "Thanks", "How are you?".
|
|
@@ -31,12 +32,22 @@ Classify any new memory into one of these types:
|
|
|
31
32
|
5. **OBEY THE USER**: If the user explicitly states something should be remembered, it must be stored with at least 'medium' importance.
|
|
32
33
|
|
|
33
34
|
### OUTPUT FORMAT
|
|
34
|
-
You MUST respond with a valid JSON object matching the \`
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
35
|
+
You MUST respond with a valid JSON object ARRAY matching the \`ISatiEvaluationOutputArray\` interface:
|
|
36
|
+
[
|
|
37
|
+
{
|
|
38
|
+
"should_store": boolean,
|
|
39
|
+
"category": "category_name" | null,
|
|
40
|
+
"importance": "low" | "medium" | "high" | null,
|
|
41
|
+
"summary": "Concise factual statement | Summary in native language" | null,
|
|
42
|
+
"reason": "Why you decided to store or not store | Reason in native language"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"should_store": boolean,
|
|
46
|
+
"category": "category_name" | null,
|
|
47
|
+
"importance": "low" | "medium" | "high" | null,
|
|
48
|
+
"summary": "Concise factual statement | Summary in native language" | null,
|
|
49
|
+
"reason": "Why you decided to store or not store | Reason in native language"
|
|
50
|
+
},
|
|
51
|
+
]
|
|
52
|
+
|
|
42
53
|
`;
|