morpheus-cli 0.2.7 → 0.3.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 +51 -8
- package/dist/channels/telegram.js +229 -22
- package/dist/cli/commands/doctor.js +11 -11
- package/dist/cli/commands/init.js +34 -34
- package/dist/cli/commands/restart.js +1 -1
- package/dist/cli/commands/session.js +79 -0
- package/dist/cli/commands/start.js +4 -1
- package/dist/cli/index.js +3 -1
- package/dist/config/manager.js +16 -15
- package/dist/config/schemas.js +2 -1
- package/dist/http/__tests__/config_api.test.js +6 -1
- package/dist/http/api.js +160 -3
- package/dist/http/server.js +4 -2
- 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 +323 -116
- package/dist/runtime/memory/sati/service.js +58 -33
- package/dist/runtime/memory/sati/system-prompts.js +19 -8
- package/dist/runtime/memory/session-embedding-worker.js +94 -0
- package/dist/runtime/memory/sqlite-vec.js +6 -0
- package/dist/runtime/memory/sqlite.js +432 -3
- package/dist/runtime/migration.js +40 -0
- package/dist/runtime/oracle.js +69 -1
- package/dist/runtime/session-embedding-scheduler.js +21 -0
- package/dist/types/config.js +8 -0
- package/dist/ui/assets/index-DqzvLXXS.js +109 -0
- package/dist/ui/assets/index-f1sqiqOo.css +1 -0
- package/dist/ui/index.html +2 -2
- package/package.json +11 -4
- package/dist/ui/assets/index-Dx1lwaMu.js +0 -96
- package/dist/ui/assets/index-QHZ08tDL.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,84 @@ 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 && embedding.length > 0) {
|
|
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
|
+
else {
|
|
331
|
+
this.display.log('🛡️ Disabled Archived Sessions in Memory Retrieval', { source: 'Sati', level: 'info' });
|
|
332
|
+
}
|
|
333
|
+
// 2️⃣ BM25 (FTS)
|
|
334
|
+
// Sanitize query: remove characters that could break FTS5 syntax (like ?, *, OR, etc)
|
|
335
|
+
// keeping only letters, numbers and spaces.
|
|
336
|
+
const safeQuery = query
|
|
337
|
+
.replace(/[^\p{L}\p{N}\s]/gu, ' ')
|
|
338
|
+
.replace(/\s+/g, ' ')
|
|
339
|
+
.trim();
|
|
340
|
+
if (safeQuery) {
|
|
341
|
+
this.display.log('📚 Tentando busca BM25 (FTS5)...', { source: 'Sati', level: 'debug' });
|
|
342
|
+
const stmt = this.db.prepare(`
|
|
343
|
+
SELECT m.*, bm25(memory_fts) as rank
|
|
344
|
+
FROM long_term_memory m
|
|
345
|
+
JOIN memory_fts ON m.rowid = memory_fts.rowid
|
|
346
|
+
WHERE memory_fts MATCH ?
|
|
347
|
+
AND m.archived = 0
|
|
348
|
+
ORDER BY rank
|
|
349
|
+
LIMIT ?
|
|
350
|
+
`);
|
|
351
|
+
const rows = stmt.all(safeQuery, limit);
|
|
352
|
+
if (rows.length > 0) {
|
|
353
|
+
this.display.log(`✅ BM25 retornou ${rows.length} resultado(s)`, { source: 'Sati', level: 'success' });
|
|
354
|
+
return rows.map(this.mapRowToRecord);
|
|
355
|
+
}
|
|
356
|
+
this.display.log('⚠️ BM25 não encontrou resultados', { source: 'Sati', level: 'debug' });
|
|
357
|
+
}
|
|
358
|
+
// 3️⃣ LIKE fallback
|
|
359
|
+
this.display.log('🧵 Tentando fallback LIKE...', { source: 'Sati', level: 'debug' });
|
|
360
|
+
const likeStmt = this.db.prepare(`
|
|
361
|
+
SELECT * FROM long_term_memory
|
|
362
|
+
WHERE (summary LIKE ? OR details LIKE ?)
|
|
363
|
+
AND archived = 0
|
|
364
|
+
ORDER BY importance DESC, access_count DESC
|
|
365
|
+
LIMIT ?
|
|
366
|
+
`);
|
|
367
|
+
const pattern = `%${query}%`;
|
|
368
|
+
const likeRows = likeStmt.all(pattern, pattern, limit);
|
|
369
|
+
if (likeRows.length > 0) {
|
|
370
|
+
this.display.log(`✅ LIKE retornou ${likeRows.length} resultado(s)`, { source: 'Sati', level: 'success' });
|
|
371
|
+
return likeRows.map(this.mapRowToRecord);
|
|
372
|
+
}
|
|
373
|
+
// 4️⃣ Final fallback
|
|
374
|
+
this.display.log('🛟 Nenhum mecanismo encontrou resultados. Usando fallback estratégico.', { source: 'Sati', level: 'warning' });
|
|
375
|
+
return this.getFallbackMemories(limit);
|
|
376
|
+
}
|
|
377
|
+
catch (e) {
|
|
378
|
+
this.display.log(`❌ Erro durante busca: ${e}`, { source: 'Sati', level: 'error' });
|
|
379
|
+
return this.getFallbackMemories(limit);
|
|
380
|
+
}
|
|
169
381
|
}
|
|
170
382
|
getFallbackMemories(limit) {
|
|
171
383
|
if (!this.db)
|
|
172
384
|
return [];
|
|
173
|
-
const
|
|
385
|
+
const rows = this.db
|
|
386
|
+
.prepare(`
|
|
174
387
|
SELECT * FROM long_term_memory
|
|
175
388
|
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
|
|
389
|
+
ORDER BY access_count DESC, created_at DESC
|
|
185
390
|
LIMIT ?
|
|
186
|
-
|
|
187
|
-
|
|
391
|
+
`)
|
|
392
|
+
.all(limit);
|
|
188
393
|
return rows.map(this.mapRowToRecord);
|
|
189
394
|
}
|
|
190
395
|
getAllMemories() {
|
|
@@ -204,18 +409,14 @@ export class SatiRepository {
|
|
|
204
409
|
source: row.source,
|
|
205
410
|
created_at: new Date(row.created_at),
|
|
206
411
|
updated_at: new Date(row.updated_at),
|
|
207
|
-
last_accessed_at: row.last_accessed_at
|
|
412
|
+
last_accessed_at: row.last_accessed_at
|
|
413
|
+
? new Date(row.last_accessed_at)
|
|
414
|
+
: undefined,
|
|
208
415
|
access_count: row.access_count,
|
|
209
416
|
version: row.version,
|
|
210
417
|
archived: Boolean(row.archived)
|
|
211
418
|
};
|
|
212
419
|
}
|
|
213
|
-
close() {
|
|
214
|
-
if (this.db) {
|
|
215
|
-
this.db.close();
|
|
216
|
-
this.db = null;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
420
|
archiveMemory(id) {
|
|
220
421
|
if (!this.db)
|
|
221
422
|
this.initialize();
|
|
@@ -223,4 +424,10 @@ export class SatiRepository {
|
|
|
223
424
|
const result = stmt.run(id);
|
|
224
425
|
return result.changes > 0;
|
|
225
426
|
}
|
|
427
|
+
close() {
|
|
428
|
+
if (this.db) {
|
|
429
|
+
this.db.close();
|
|
430
|
+
this.db = null;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
226
433
|
}
|
|
@@ -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;
|
|
@@ -23,12 +24,24 @@ export class SatiService {
|
|
|
23
24
|
this.repository.initialize();
|
|
24
25
|
}
|
|
25
26
|
async recover(currentMessage, recentMessages) {
|
|
26
|
-
const
|
|
27
|
-
const memoryLimit =
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
const satiConfig = ConfigManager.getInstance().getSatiConfig();
|
|
28
|
+
const memoryLimit = satiConfig.memory_limit || 10;
|
|
29
|
+
const enabled_vector_search = satiConfig.enabled_archived_sessions ?? true;
|
|
30
|
+
let queryEmbedding;
|
|
31
|
+
try {
|
|
32
|
+
const embeddingService = await EmbeddingService.getInstance();
|
|
33
|
+
const queryText = [
|
|
34
|
+
...recentMessages.slice(-3),
|
|
35
|
+
currentMessage
|
|
36
|
+
].join(' ');
|
|
37
|
+
if (enabled_vector_search) {
|
|
38
|
+
queryEmbedding = await embeddingService.generate(queryText);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
console.warn('[Sati] Failed to generate embedding:', err);
|
|
43
|
+
}
|
|
44
|
+
const memories = this.repository.search(currentMessage, memoryLimit, queryEmbedding);
|
|
32
45
|
return {
|
|
33
46
|
relevant_memories: memories.map(m => ({
|
|
34
47
|
summary: m.summary,
|
|
@@ -39,12 +52,12 @@ export class SatiService {
|
|
|
39
52
|
}
|
|
40
53
|
async evaluateAndPersist(conversation) {
|
|
41
54
|
try {
|
|
42
|
-
const
|
|
43
|
-
if (!
|
|
55
|
+
const satiConfig = ConfigManager.getInstance().getSatiConfig();
|
|
56
|
+
if (!satiConfig)
|
|
44
57
|
return;
|
|
45
58
|
// Use the main provider factory to get an agent (Reusing Zion configuration)
|
|
46
59
|
// We pass empty tools as Sati is a pure reasoning agent here
|
|
47
|
-
const agent = await ProviderFactory.create(
|
|
60
|
+
const agent = await ProviderFactory.create(satiConfig, []);
|
|
48
61
|
// Get existing memories for context (Simulated "Working Memory" or full list if small)
|
|
49
62
|
const allMemories = this.repository.getAllMemories();
|
|
50
63
|
const existingSummaries = allMemories.slice(0, 50).map(m => m.summary);
|
|
@@ -69,8 +82,8 @@ export class SatiService {
|
|
|
69
82
|
name: 'sati_evaluation_input'
|
|
70
83
|
});
|
|
71
84
|
inputMsg.provider_metadata = {
|
|
72
|
-
provider:
|
|
73
|
-
model:
|
|
85
|
+
provider: satiConfig.provider,
|
|
86
|
+
model: satiConfig.model
|
|
74
87
|
};
|
|
75
88
|
await history.addMessage(inputMsg);
|
|
76
89
|
}
|
|
@@ -90,8 +103,8 @@ export class SatiService {
|
|
|
90
103
|
outputToolMsg.usage_metadata = lastMessage.usage_metadata;
|
|
91
104
|
}
|
|
92
105
|
outputToolMsg.provider_metadata = {
|
|
93
|
-
provider:
|
|
94
|
-
model:
|
|
106
|
+
provider: satiConfig.provider,
|
|
107
|
+
model: satiConfig.model
|
|
95
108
|
};
|
|
96
109
|
await history.addMessage(outputToolMsg);
|
|
97
110
|
}
|
|
@@ -100,7 +113,7 @@ export class SatiService {
|
|
|
100
113
|
}
|
|
101
114
|
// Safe JSON parsing (handle markdown blocks if LLM wraps output)
|
|
102
115
|
content = content.replace(/```json/g, '').replace(/```/g, '').trim();
|
|
103
|
-
let result;
|
|
116
|
+
let result = [];
|
|
104
117
|
try {
|
|
105
118
|
result = JSON.parse(content);
|
|
106
119
|
}
|
|
@@ -108,26 +121,38 @@ export class SatiService {
|
|
|
108
121
|
console.warn('[SatiService] Failed to parse JSON response:', content);
|
|
109
122
|
return;
|
|
110
123
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
124
|
+
for (const item of result) {
|
|
125
|
+
if (item.should_store && item.summary && item.category && item.importance) {
|
|
126
|
+
display.log(`Persisting new memory: [${item.category.toUpperCase()}] ${item.summary}`, { source: 'Sati' });
|
|
127
|
+
try {
|
|
128
|
+
const savedMemory = await this.repository.save({
|
|
129
|
+
summary: item.summary,
|
|
130
|
+
category: item.category,
|
|
131
|
+
importance: item.importance,
|
|
132
|
+
details: item.reason,
|
|
133
|
+
hash: this.generateHash(item.summary),
|
|
134
|
+
source: 'conversation'
|
|
135
|
+
});
|
|
136
|
+
// 🔥 GERAR EMBEDDING
|
|
137
|
+
const embeddingService = await EmbeddingService.getInstance();
|
|
138
|
+
const textForEmbedding = [
|
|
139
|
+
savedMemory.summary,
|
|
140
|
+
savedMemory.details ?? ''
|
|
141
|
+
].join(' ');
|
|
142
|
+
const embedding = await embeddingService.generate(textForEmbedding);
|
|
143
|
+
display.log(`Generated embedding for memory ID ${savedMemory.id}`, { source: 'Sati', level: 'debug' });
|
|
144
|
+
// 🔥 SALVAR EMBEDDING NO SQLITE_VEC
|
|
145
|
+
this.repository.upsertEmbedding(savedMemory.id, embedding);
|
|
146
|
+
// Quiet success - logging handled by repository/middleware if needed, or verbose debug
|
|
128
147
|
}
|
|
129
|
-
|
|
130
|
-
|
|
148
|
+
catch (saveError) {
|
|
149
|
+
if (saveError.message && saveError.message.includes('UNIQUE constraint failed')) {
|
|
150
|
+
// Duplicate detected by DB (Hash collision)
|
|
151
|
+
// This is expected given T012 logic
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
throw saveError;
|
|
155
|
+
}
|
|
131
156
|
}
|
|
132
157
|
}
|
|
133
158
|
}
|