ocerebro 0.1.0

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.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +288 -0
  3. package/bin/ocerebro.js +98 -0
  4. package/cerebro/__init__.py +7 -0
  5. package/cerebro/__main__.py +19 -0
  6. package/cerebro/cerebro_setup.py +459 -0
  7. package/hooks/__init__.py +1 -0
  8. package/hooks/cost_hook.py +45 -0
  9. package/hooks/coverage_hook.py +39 -0
  10. package/hooks/error_hook.py +48 -0
  11. package/hooks/expensive_hook.py +41 -0
  12. package/hooks/global_logger.py +32 -0
  13. package/package.json +49 -0
  14. package/pyproject.toml +77 -0
  15. package/src/__init__.py +2 -0
  16. package/src/cli/__init__.py +2 -0
  17. package/src/cli/dream.py +91 -0
  18. package/src/cli/gc.py +93 -0
  19. package/src/cli/main.py +583 -0
  20. package/src/cli/remember.py +74 -0
  21. package/src/consolidation/__init__.py +8 -0
  22. package/src/consolidation/checkpoints.py +96 -0
  23. package/src/consolidation/dream.py +465 -0
  24. package/src/consolidation/extractor.py +313 -0
  25. package/src/consolidation/promoter.py +435 -0
  26. package/src/consolidation/remember.py +544 -0
  27. package/src/consolidation/scorer.py +191 -0
  28. package/src/core/__init__.py +6 -0
  29. package/src/core/event_schema.py +55 -0
  30. package/src/core/jsonl_storage.py +238 -0
  31. package/src/core/paths.py +254 -0
  32. package/src/core/session_manager.py +76 -0
  33. package/src/diff/__init__.py +5 -0
  34. package/src/diff/memory_diff.py +571 -0
  35. package/src/forgetting/__init__.py +6 -0
  36. package/src/forgetting/decay.py +86 -0
  37. package/src/forgetting/gc.py +296 -0
  38. package/src/forgetting/guard_rails.py +126 -0
  39. package/src/hooks/__init__.py +11 -0
  40. package/src/hooks/core_captures.py +170 -0
  41. package/src/hooks/custom_loader.py +389 -0
  42. package/src/index/__init__.py +7 -0
  43. package/src/index/embeddings_db.py +419 -0
  44. package/src/index/metadata_db.py +230 -0
  45. package/src/index/queries.py +357 -0
  46. package/src/mcp/__init__.py +2 -0
  47. package/src/mcp/server.py +640 -0
  48. package/src/memdir/__init__.py +19 -0
  49. package/src/memdir/scanner.py +260 -0
  50. package/src/official/__init__.py +5 -0
  51. package/src/official/markdown_storage.py +173 -0
  52. package/src/official/templates.py +128 -0
  53. package/src/working/__init__.py +5 -0
  54. package/src/working/memory_view.py +150 -0
  55. package/src/working/yaml_storage.py +234 -0
@@ -0,0 +1,419 @@
1
+ """EmbeddingsDB: armazenamento e busca vetorial com sqlite-vec para Cerebro"""
2
+
3
+ import sqlite3
4
+ import json
5
+ from pathlib import Path
6
+ from typing import Any, Dict, List, Optional, Tuple
7
+ import hashlib
8
+
9
+
10
+ class EmbeddingsDB:
11
+ """
12
+ Banco de dados para embeddings vetoriais usando sqlite-vec.
13
+
14
+ Armazena vetores de embeddings gerados por modelos como
15
+ sentence-transformers e oferece busca por similaridade com
16
+ ANN (Approximate Nearest Neighbor) via sqlite-vec.
17
+
18
+ sqlite-vec fornece:
19
+ - Busca vetorial eficiente dentro do SQLite
20
+ - Zero dependências externas
21
+ - Ideal para centenas/milhares de vetores
22
+ """
23
+
24
+ def __init__(self, db_path: Path, model_name: str = "all-MiniLM-L6-v2"):
25
+ """
26
+ Inicializa o EmbeddingsDB.
27
+
28
+ Args:
29
+ db_path: Path para o arquivo do banco
30
+ model_name: Nome do modelo sentence-transformers
31
+ """
32
+ self.db_path = db_path
33
+ self.model_name = model_name
34
+ self._model = None
35
+ self._init_sqlite_vec()
36
+ self._init_schema()
37
+
38
+ @property
39
+ def model(self):
40
+ """Lazy load do modelo de embeddings"""
41
+ if self._model is None:
42
+ try:
43
+ from sentence_transformers import SentenceTransformer
44
+ self._model = SentenceTransformer(self.model_name)
45
+ except ImportError:
46
+ raise ImportError(
47
+ "sentence-transformers não instalado. "
48
+ "Instale com: pip install sentence-transformers"
49
+ )
50
+ return self._model
51
+
52
+ def _init_sqlite_vec(self):
53
+ """Inicializa extensão sqlite-vec"""
54
+ try:
55
+ import sqlite_vec
56
+ self._sqlite_vec_available = True
57
+ except ImportError:
58
+ self._sqlite_vec_available = False
59
+
60
+ def _connect(self) -> sqlite3.Connection:
61
+ """Cria conexão com o banco e carrega sqlite-vec"""
62
+ conn = sqlite3.connect(self.db_path)
63
+ conn.row_factory = sqlite3.Row
64
+ conn.execute("PRAGMA journal_mode=WAL")
65
+
66
+ # Carrega extensão sqlite-vec se disponível
67
+ if self._sqlite_vec_available:
68
+ import sqlite_vec
69
+ conn.enable_load_extension(True)
70
+ sqlite_vec.load(conn)
71
+ conn.enable_load_extension(False)
72
+
73
+ return conn
74
+
75
+ def _init_schema(self):
76
+ """Cria schema do banco"""
77
+ conn = self._connect()
78
+
79
+ # Tabela de embeddings com suporte a vetor
80
+ conn.execute("""
81
+ CREATE TABLE IF NOT EXISTS embeddings (
82
+ id TEXT PRIMARY KEY,
83
+ memory_id TEXT UNIQUE,
84
+ type TEXT,
85
+ project TEXT,
86
+ embedding BLOB, -- sqlite-vec armazena como BLOB
87
+ content_hash TEXT,
88
+ model_name TEXT,
89
+ created_at TEXT DEFAULT (datetime('now')),
90
+ FOREIGN KEY (memory_id) REFERENCES memories(id)
91
+ )
92
+ """)
93
+
94
+ conn.execute("""
95
+ CREATE INDEX IF NOT EXISTS idx_embeddings_memory
96
+ ON embeddings(memory_id)
97
+ """)
98
+
99
+ conn.execute("""
100
+ CREATE INDEX IF NOT EXISTS idx_embeddings_project
101
+ ON embeddings(project)
102
+ """)
103
+
104
+ conn.commit()
105
+ conn.close()
106
+
107
+ def _compute_embedding(self, text: str) -> List[float]:
108
+ """
109
+ Computa embedding para texto.
110
+
111
+ Args:
112
+ text: Texto para embedar
113
+
114
+ Returns:
115
+ Lista de floats (vetor de embedding)
116
+ """
117
+ embedding = self.model.encode(text, convert_to_numpy=True)
118
+ return embedding.tolist()
119
+
120
+ def _compute_hash(self, text: str) -> str:
121
+ """
122
+ Computa hash do conteúdo para cache.
123
+
124
+ Args:
125
+ text: Texto para hashear
126
+
127
+ Returns:
128
+ Hash SHA256 do texto
129
+ """
130
+ return hashlib.sha256(text.encode()).hexdigest()[:16]
131
+
132
+ def _cosine_similarity(self, a: List[float], b: List[float]) -> float:
133
+ """
134
+ Calcula similaridade cosseno entre dois vetores.
135
+
136
+ Args:
137
+ a: Primeiro vetor
138
+ b: Segundo vetor
139
+
140
+ Returns:
141
+ Similaridade cosseno (0-1)
142
+ """
143
+ import math
144
+
145
+ dot_product = sum(x * y for x, y in zip(a, b))
146
+ norm_a = math.sqrt(sum(x * x for x in a))
147
+ norm_b = math.sqrt(sum(x * x for x in b))
148
+
149
+ if norm_a == 0 or norm_b == 0:
150
+ return 0.0
151
+
152
+ return dot_product / (norm_a * norm_b)
153
+
154
+ def upsert(
155
+ self,
156
+ memory_id: str,
157
+ text: str,
158
+ memory_type: str,
159
+ project: str,
160
+ force_recompute: bool = False
161
+ ) -> str:
162
+ """
163
+ Insere ou atualiza embedding.
164
+
165
+ Args:
166
+ memory_id: ID da memória
167
+ text: Texto para embedar
168
+ memory_type: Tipo de memória (decision, error, etc)
169
+ project: Nome do projeto
170
+ force_recompute: Forçar recálculo mesmo com hash igual
171
+
172
+ Returns:
173
+ ID do embedding
174
+ """
175
+ content_hash = self._compute_hash(text)
176
+
177
+ # Verifica se já existe embedding com mesmo hash
178
+ if not force_recompute:
179
+ existing = self.get_by_memory_id(memory_id)
180
+ if existing and existing["content_hash"] == content_hash:
181
+ return existing["id"]
182
+
183
+ # Computa novo embedding
184
+ embedding = self._compute_embedding(text)
185
+ embedding_id = f"emb_{memory_id}"
186
+
187
+ conn = self._connect()
188
+
189
+ if self._sqlite_vec_available:
190
+ # Usa sqlite-vec para armazenamento otimizado
191
+ import sqlite_vec
192
+ embedding_blob = sqlite_vec.serialize_float32(embedding)
193
+ else:
194
+ # Fallback para JSON
195
+ embedding_blob = json.dumps(embedding)
196
+
197
+ conn.execute("""
198
+ INSERT OR REPLACE INTO embeddings
199
+ (id, memory_id, type, project, embedding, content_hash, model_name)
200
+ VALUES (?, ?, ?, ?, ?, ?, ?)
201
+ """, (
202
+ embedding_id,
203
+ memory_id,
204
+ memory_type,
205
+ project,
206
+ embedding_blob,
207
+ content_hash,
208
+ self.model_name
209
+ ))
210
+ conn.commit()
211
+ conn.close()
212
+
213
+ return embedding_id
214
+
215
+ def get_by_memory_id(self, memory_id: str) -> Optional[Dict[str, Any]]:
216
+ """
217
+ Obtém embedding por ID de memória.
218
+
219
+ Args:
220
+ memory_id: ID da memória
221
+
222
+ Returns:
223
+ Dados do embedding ou None
224
+ """
225
+ conn = self._connect()
226
+ cursor = conn.execute(
227
+ "SELECT * FROM embeddings WHERE memory_id = ?",
228
+ (memory_id,)
229
+ )
230
+ row = cursor.fetchone()
231
+ conn.close()
232
+
233
+ if row:
234
+ # Desserializa embedding
235
+ if self._sqlite_vec_available:
236
+ import numpy as np
237
+ embedding = np.frombuffer(row["embedding"], dtype=np.float32).tolist()
238
+ else:
239
+ embedding = json.loads(row["embedding"])
240
+
241
+ return {
242
+ "id": row["id"],
243
+ "memory_id": row["memory_id"],
244
+ "type": row["type"],
245
+ "project": row["project"],
246
+ "embedding": embedding,
247
+ "content_hash": row["content_hash"],
248
+ "model_name": row["model_name"],
249
+ "created_at": row["created_at"]
250
+ }
251
+ return None
252
+
253
+ def search_similar(
254
+ self,
255
+ query: str,
256
+ project: Optional[str] = None,
257
+ limit: int = 10,
258
+ threshold: float = 0.5
259
+ ) -> List[Dict[str, Any]]:
260
+ """
261
+ Busca memórias similares usando sqlite-vec ANN.
262
+
263
+ Args:
264
+ query: Texto de busca
265
+ project: Filtrar por projeto (opcional)
266
+ limit: Limite de resultados
267
+ threshold: Threshold mínimo de similaridade
268
+
269
+ Returns:
270
+ Lista de memórias similares com score
271
+ """
272
+ # Computa embedding da query
273
+ query_embedding = self._compute_embedding(query)
274
+
275
+ conn = self._connect()
276
+
277
+ if self._sqlite_vec_available:
278
+ # Usa busca vetorial do sqlite-vec (ANN)
279
+ import sqlite_vec
280
+ query_blob = sqlite_vec.serialize_float32(query_embedding)
281
+
282
+ # sqlite-vec usa similaridade de cosseno via operador MATCH
283
+ if project:
284
+ cursor = conn.execute("""
285
+ SELECT id, memory_id, type, project, content_hash,
286
+ vec_distance_cosine(embedding, ?) as distance
287
+ FROM embeddings
288
+ WHERE project = ?
289
+ ORDER BY distance ASC
290
+ LIMIT ?
291
+ """, (query_blob, project, limit))
292
+ else:
293
+ cursor = conn.execute("""
294
+ SELECT id, memory_id, type, project, content_hash,
295
+ vec_distance_cosine(embedding, ?) as distance
296
+ FROM embeddings
297
+ ORDER BY distance ASC
298
+ LIMIT ?
299
+ """, (query_blob, limit))
300
+
301
+ results = []
302
+ for row in cursor.fetchall():
303
+ # sqlite-vec retorna distância (1 - similaridade)
304
+ similarity = 1.0 - row["distance"]
305
+ if similarity >= threshold:
306
+ results.append({
307
+ "memory_id": row["memory_id"],
308
+ "type": row["type"],
309
+ "project": row["project"],
310
+ "similarity": similarity,
311
+ "content_hash": row["content_hash"]
312
+ })
313
+ else:
314
+ # Fallback: carrega todos e calcula em Python
315
+ import math
316
+
317
+ if project:
318
+ cursor = conn.execute(
319
+ "SELECT * FROM embeddings WHERE project = ?",
320
+ (project,)
321
+ )
322
+ else:
323
+ cursor = conn.execute("SELECT * FROM embeddings")
324
+
325
+ results = []
326
+ for row in cursor.fetchall():
327
+ stored_embedding = json.loads(row["embedding"])
328
+
329
+ # Calcula similaridade cosseno manual
330
+ dot_product = sum(x * y for x, y in zip(query_embedding, stored_embedding))
331
+ norm_query = math.sqrt(sum(x * x for x in query_embedding))
332
+ norm_stored = math.sqrt(sum(x * x for x in stored_embedding))
333
+
334
+ if norm_query > 0 and norm_stored > 0:
335
+ similarity = dot_product / (norm_query * norm_stored)
336
+ else:
337
+ similarity = 0.0
338
+
339
+ if similarity >= threshold:
340
+ results.append({
341
+ "memory_id": row["memory_id"],
342
+ "type": row["type"],
343
+ "project": row["project"],
344
+ "similarity": similarity,
345
+ "content_hash": row["content_hash"]
346
+ })
347
+
348
+ conn.close()
349
+ return results
350
+
351
+ def delete(self, memory_id: str) -> None:
352
+ """
353
+ Remove embedding.
354
+
355
+ Args:
356
+ memory_id: ID da memória
357
+ """
358
+ conn = self._connect()
359
+ conn.execute(
360
+ "DELETE FROM embeddings WHERE memory_id = ?",
361
+ (memory_id,)
362
+ )
363
+ conn.commit()
364
+ conn.close()
365
+
366
+ def list_embeddings(self, project: Optional[str] = None) -> List[Dict[str, Any]]:
367
+ """
368
+ Lista embeddings.
369
+
370
+ Args:
371
+ project: Filtrar por projeto (opcional)
372
+
373
+ Returns:
374
+ Lista de embeddings (sem vetores)
375
+ """
376
+ conn = self._connect()
377
+
378
+ if project:
379
+ cursor = conn.execute(
380
+ "SELECT id, memory_id, type, project, content_hash, model_name, created_at "
381
+ "FROM embeddings WHERE project = ?",
382
+ (project,)
383
+ )
384
+ else:
385
+ cursor = conn.execute(
386
+ "SELECT id, memory_id, type, project, content_hash, model_name, created_at "
387
+ "FROM embeddings"
388
+ )
389
+
390
+ results = [dict(row) for row in cursor.fetchall()]
391
+ conn.close()
392
+ return results
393
+
394
+ def get_stats(self) -> Dict[str, Any]:
395
+ """
396
+ Obtém estatísticas do banco.
397
+
398
+ Returns:
399
+ Dicionário com estatísticas
400
+ """
401
+ conn = self._connect()
402
+
403
+ total = conn.execute("SELECT COUNT(*) FROM embeddings").fetchone()[0]
404
+ by_type = conn.execute(
405
+ "SELECT type, COUNT(*) FROM embeddings GROUP BY type"
406
+ ).fetchall()
407
+ by_project = conn.execute(
408
+ "SELECT project, COUNT(*) FROM embeddings GROUP BY project"
409
+ ).fetchall()
410
+
411
+ conn.close()
412
+
413
+ return {
414
+ "total_embeddings": total,
415
+ "by_type": dict(by_type),
416
+ "by_project": dict(by_project),
417
+ "model_name": self.model_name,
418
+ "sqlite_vec_available": self._sqlite_vec_available
419
+ }
@@ -0,0 +1,230 @@
1
+ """SQLite + FTS para metadados do Cerebro"""
2
+
3
+ import sqlite3
4
+ from pathlib import Path
5
+ from typing import Any, Dict, List, Optional
6
+
7
+
8
+ class MetadataDB:
9
+ """
10
+ Banco de dados SQLite com FTS5 para índice de memórias.
11
+
12
+ Armazena metadados estruturados e oferece busca full-text
13
+ via FTS5 virtual table.
14
+ """
15
+
16
+ def __init__(self, db_path: Path):
17
+ """
18
+ Inicializa o MetadataDB.
19
+
20
+ Args:
21
+ db_path: Path para o arquivo do banco de dados
22
+ """
23
+ self.db_path = db_path
24
+ self._init_schema()
25
+
26
+ def _connect(self) -> sqlite3.Connection:
27
+ """
28
+ Cria conexão com o banco.
29
+
30
+ Returns:
31
+ Conexão SQLite configurada
32
+ """
33
+ conn = sqlite3.connect(self.db_path)
34
+ conn.row_factory = sqlite3.Row
35
+ conn.execute("PRAGMA journal_mode=WAL")
36
+ return conn
37
+
38
+ def _init_schema(self):
39
+ """Cria schema do banco"""
40
+ conn = self._connect()
41
+
42
+ conn.execute("""
43
+ CREATE TABLE IF NOT EXISTS memories (
44
+ id TEXT PRIMARY KEY,
45
+ type TEXT,
46
+ project TEXT,
47
+ title TEXT,
48
+ content TEXT,
49
+ tags TEXT,
50
+ severity TEXT,
51
+ impact TEXT,
52
+ importance_score REAL,
53
+ recency_score REAL,
54
+ frequency_score REAL,
55
+ links_score REAL,
56
+ total_score REAL,
57
+ created_at TEXT,
58
+ updated_at TEXT,
59
+ last_accessed TEXT,
60
+ access_count INTEGER DEFAULT 0,
61
+ path TEXT,
62
+ layer TEXT,
63
+ content_hash TEXT
64
+ )
65
+ """)
66
+
67
+ conn.execute("""
68
+ CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
69
+ id UNINDEXED,
70
+ title,
71
+ content,
72
+ tags,
73
+ project
74
+ )
75
+ """)
76
+
77
+ conn.commit()
78
+ conn.close()
79
+
80
+ def list_tables(self) -> List[str]:
81
+ """
82
+ Lista tabelas do banco.
83
+
84
+ Returns:
85
+ Lista de nomes de tabelas
86
+ """
87
+ conn = self._connect()
88
+ cursor = conn.execute(
89
+ "SELECT name FROM sqlite_master WHERE type='table'"
90
+ )
91
+ tables = [row[0] for row in cursor.fetchall()]
92
+ conn.close()
93
+ return tables
94
+
95
+ def insert(self, data: Dict[str, Any]) -> None:
96
+ """
97
+ Insere memória no índice.
98
+
99
+ Args:
100
+ data: Dados da memória
101
+ """
102
+ conn = self._connect()
103
+
104
+ columns = ", ".join(data.keys())
105
+ placeholders = ", ".join(["?" for _ in data])
106
+
107
+ conn.execute(
108
+ f"INSERT OR REPLACE INTO memories ({columns}) VALUES ({placeholders})",
109
+ list(data.values())
110
+ )
111
+
112
+ # Atualiza FTS
113
+ if "content" in data:
114
+ conn.execute(
115
+ "INSERT OR REPLACE INTO memories_fts (id, title, content, tags, project) VALUES (?, ?, ?, ?, ?)",
116
+ (data.get("id"), data.get("title", ""), data.get("content", ""), data.get("tags", ""), data.get("project", ""))
117
+ )
118
+
119
+ conn.commit()
120
+ conn.close()
121
+
122
+ def search(self, project: Optional[str] = None, type: Optional[str] = None) -> List[Dict]:
123
+ """
124
+ Busca por metadados.
125
+
126
+ Args:
127
+ project: Filtrar por projeto (opcional)
128
+ type: Filtrar por tipo (opcional)
129
+
130
+ Returns:
131
+ Lista de memórias encontradas
132
+ """
133
+ conn = self._connect()
134
+
135
+ query = "SELECT * FROM memories WHERE 1=1"
136
+ params = []
137
+
138
+ if project:
139
+ query += " AND project = ?"
140
+ params.append(project)
141
+
142
+ if type:
143
+ query += " AND type = ?"
144
+ params.append(type)
145
+
146
+ cursor = conn.execute(query, params)
147
+ results = [dict(row) for row in cursor.fetchall()]
148
+ conn.close()
149
+ return results
150
+
151
+ def search_fts(self, query: str, project: Optional[str] = None) -> List[Dict]:
152
+ """
153
+ Busca full-text.
154
+
155
+ Args:
156
+ query: Termo de busca
157
+ project: Filtrar por projeto (opcional)
158
+
159
+ Returns:
160
+ Lista de memórias encontradas
161
+ """
162
+ conn = self._connect()
163
+
164
+ if project:
165
+ sql = """
166
+ SELECT m.* FROM memories m
167
+ JOIN memories_fts fts ON fts.id = m.id
168
+ WHERE memories_fts MATCH ? AND m.project = ?
169
+ """
170
+ cursor = conn.execute(sql, (query, project))
171
+ else:
172
+ sql = """
173
+ SELECT m.* FROM memories m
174
+ JOIN memories_fts fts ON fts.id = m.id
175
+ WHERE memories_fts MATCH ?
176
+ """
177
+ cursor = conn.execute(sql, (query,))
178
+
179
+ results = [dict(row) for row in cursor.fetchall()]
180
+ conn.close()
181
+ return results
182
+
183
+ def get_by_id(self, memory_id: str) -> Optional[Dict]:
184
+ """
185
+ Obtém memória por ID.
186
+
187
+ Args:
188
+ memory_id: ID da memória
189
+
190
+ Returns:
191
+ Dados da memória ou None
192
+ """
193
+ conn = self._connect()
194
+ cursor = conn.execute(
195
+ "SELECT * FROM memories WHERE id = ?",
196
+ (memory_id,)
197
+ )
198
+ row = cursor.fetchone()
199
+ conn.close()
200
+ return dict(row) if row else None
201
+
202
+ def update_access(self, memory_id: str) -> None:
203
+ """
204
+ Atualiza contagem de acessos.
205
+
206
+ Args:
207
+ memory_id: ID da memória
208
+ """
209
+ conn = self._connect()
210
+ conn.execute("""
211
+ UPDATE memories
212
+ SET access_count = access_count + 1,
213
+ last_accessed = datetime('now')
214
+ WHERE id = ?
215
+ """, (memory_id,))
216
+ conn.commit()
217
+ conn.close()
218
+
219
+ def delete(self, memory_id: str) -> None:
220
+ """
221
+ Remove memória do índice.
222
+
223
+ Args:
224
+ memory_id: ID da memória
225
+ """
226
+ conn = self._connect()
227
+ conn.execute("DELETE FROM memories WHERE id = ?", (memory_id,))
228
+ conn.execute("DELETE FROM memories_fts WHERE id = ?", (memory_id,))
229
+ conn.commit()
230
+ conn.close()