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.
@@ -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'; // Available in recent Node versions
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 = dbPath || path.join(homedir(), '.morpheus', 'memory', 'santi-memory.db');
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
- try {
21
- // Ensure directory exists
22
- fs.ensureDirSync(path.dirname(this.dbPath));
23
- // Connect to database
24
- this.db = new Database(this.dbPath, { timeout: 5000 });
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("DB not initialized");
33
+ throw new Error('DB not initialized');
37
34
  this.db.exec(`
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 ON long_term_memory(category);
55
- CREATE INDEX IF NOT EXISTS idx_memory_importance ON long_term_memory(importance);
56
- CREATE INDEX IF NOT EXISTS idx_memory_archived ON long_term_memory(archived);
57
-
58
- -- FTS5 Virtual Table for semantic-like keyword search
59
- CREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5(summary, content='long_term_memory', content_rowid='rowid');
60
-
61
- -- Triggers to sync FTS
62
- CREATE TRIGGER IF NOT EXISTS memory_ai AFTER INSERT ON long_term_memory BEGIN
63
- INSERT INTO memory_fts(rowid, summary) VALUES (new.rowid, new.summary);
64
- END;
65
- CREATE TRIGGER IF NOT EXISTS memory_ad AFTER DELETE ON long_term_memory BEGIN
66
- INSERT INTO memory_fts(memory_fts, rowid, summary) VALUES('delete', old.rowid, old.summary);
67
- END;
68
- CREATE TRIGGER IF NOT EXISTS memory_au AFTER UPDATE ON long_term_memory BEGIN
69
- INSERT INTO memory_fts(memory_fts, rowid, summary) VALUES('delete', old.rowid, old.summary);
70
- INSERT INTO memory_fts(rowid, summary) VALUES (new.rowid, new.summary);
71
- END;
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
- // Sanitize query for FTS5: remove characters that break FTS5 syntax
125
- // Keep only alphanumeric, spaces, and safe punctuation (comma, period, hyphen)
126
- // const safeQuery = query.replace(/[^a-zA-Z0-9\s,.\-]/g, "").trim();
127
- // if (!safeQuery) {
128
- // console.warn('[SatiRepository] Empty query after sanitization');
129
- // return this.getFallbackMemories(limit);
130
- // }
131
- // try {
132
- // // Try FTS5 search first
133
- // const stmt = this.db!.prepare(`
134
- // SELECT m.*
135
- // FROM long_term_memory m
136
- // JOIN memory_fts f ON m.rowid = f.rowid
137
- // WHERE memory_fts MATCH ? AND m.archived = 0
138
- // ORDER BY rank
139
- // LIMIT ?
140
- // `);
141
- // const rows = stmt.all(safeQuery, limit) as any[];
142
- // if (rows.length > 0) {
143
- // console.log(`[SatiRepository] FTS5 found ${rows.length} memories for: "${safeQuery}"`);
144
- // return rows.map(this.mapRowToRecord);
145
- // }
146
- // // Fallback: try LIKE search
147
- // console.log(`[SatiRepository] FTS5 returned no results, trying LIKE search for: "${safeQuery}"`);
148
- // const likeStmt = this.db!.prepare(`
149
- // SELECT * FROM long_term_memory
150
- // WHERE (summary LIKE ? OR details LIKE ?)
151
- // AND archived = 0
152
- // ORDER BY importance DESC, access_count DESC
153
- // LIMIT ?
154
- // `);
155
- // const likePattern = `%${safeQuery}%`;
156
- // const likeRows = likeStmt.all(likePattern, likePattern, limit) as any[];
157
- // if (likeRows.length > 0) {
158
- // console.log(`[SatiRepository] LIKE search found ${likeRows.length} memories`);
159
- // return likeRows.map(this.mapRowToRecord);
160
- // }
161
- // // Final fallback: return most important/accessed memories
162
- // console.log('[SatiRepository] No search results, returning most important memories');
163
- // return this.getFallbackMemories(limit);
164
- // } catch (e) {
165
- // console.warn(`[SatiRepository] Search failed for query "${query}": ${e}`);
166
- // return this.getFallbackMemories(limit);
167
- // }
168
- return this.getFallbackMemories(limit);
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 stmt = this.db.prepare(`
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
- const rows = stmt.all(limit);
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 ? new Date(row.last_accessed_at) : undefined,
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 || 1000;
28
- // Use the current message as the primary search query
29
- // We could enhance this by extracting keywords from the last few messages
30
- // but for FR-004 we start with user input.
31
- const memories = this.repository.search(currentMessage, memoryLimit);
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
- if (result.should_store && result.summary && result.category && result.importance) {
112
- display.log(`Persisting new memory: [${result.category.toUpperCase()}] ${result.summary}`, { source: 'Sati' });
113
- try {
114
- await this.repository.save({
115
- summary: result.summary,
116
- category: result.category,
117
- importance: result.importance,
118
- details: result.reason,
119
- hash: this.generateHash(result.summary),
120
- source: 'conversation' // Could track actual session ID here if available
121
- });
122
- // Quiet success - logging handled by repository/middleware if needed, or verbose debug
123
- }
124
- catch (saveError) {
125
- if (saveError.message && saveError.message.includes('UNIQUE constraint failed')) {
126
- // Duplicate detected by DB (Hash collision)
127
- // This is expected given T012 logic
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
- else {
130
- throw saveError;
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 \`ISatiEvaluationOutput\` interface:
35
- {
36
- "should_store": boolean,
37
- "category": "category_name" | null,
38
- "importance": "low" | "medium" | "high" | null,
39
- "summary": "Concise factual statement" | null,
40
- "reason": "Why you decided to store or not store"
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
  `;