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.
- package/LICENSE +21 -0
- package/README.md +288 -0
- package/bin/ocerebro.js +98 -0
- package/cerebro/__init__.py +7 -0
- package/cerebro/__main__.py +19 -0
- package/cerebro/cerebro_setup.py +459 -0
- package/hooks/__init__.py +1 -0
- package/hooks/cost_hook.py +45 -0
- package/hooks/coverage_hook.py +39 -0
- package/hooks/error_hook.py +48 -0
- package/hooks/expensive_hook.py +41 -0
- package/hooks/global_logger.py +32 -0
- package/package.json +49 -0
- package/pyproject.toml +77 -0
- package/src/__init__.py +2 -0
- package/src/cli/__init__.py +2 -0
- package/src/cli/dream.py +91 -0
- package/src/cli/gc.py +93 -0
- package/src/cli/main.py +583 -0
- package/src/cli/remember.py +74 -0
- package/src/consolidation/__init__.py +8 -0
- package/src/consolidation/checkpoints.py +96 -0
- package/src/consolidation/dream.py +465 -0
- package/src/consolidation/extractor.py +313 -0
- package/src/consolidation/promoter.py +435 -0
- package/src/consolidation/remember.py +544 -0
- package/src/consolidation/scorer.py +191 -0
- package/src/core/__init__.py +6 -0
- package/src/core/event_schema.py +55 -0
- package/src/core/jsonl_storage.py +238 -0
- package/src/core/paths.py +254 -0
- package/src/core/session_manager.py +76 -0
- package/src/diff/__init__.py +5 -0
- package/src/diff/memory_diff.py +571 -0
- package/src/forgetting/__init__.py +6 -0
- package/src/forgetting/decay.py +86 -0
- package/src/forgetting/gc.py +296 -0
- package/src/forgetting/guard_rails.py +126 -0
- package/src/hooks/__init__.py +11 -0
- package/src/hooks/core_captures.py +170 -0
- package/src/hooks/custom_loader.py +389 -0
- package/src/index/__init__.py +7 -0
- package/src/index/embeddings_db.py +419 -0
- package/src/index/metadata_db.py +230 -0
- package/src/index/queries.py +357 -0
- package/src/mcp/__init__.py +2 -0
- package/src/mcp/server.py +640 -0
- package/src/memdir/__init__.py +19 -0
- package/src/memdir/scanner.py +260 -0
- package/src/official/__init__.py +5 -0
- package/src/official/markdown_storage.py +173 -0
- package/src/official/templates.py +128 -0
- package/src/working/__init__.py +5 -0
- package/src/working/memory_view.py +150 -0
- 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()
|