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,357 @@
|
|
|
1
|
+
"""QueryEngine: consultas híbridas para Cerebro"""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from .metadata_db import MetadataDB
|
|
8
|
+
from .embeddings_db import EmbeddingsDB
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class QueryResult:
|
|
13
|
+
"""Resultado de consulta"""
|
|
14
|
+
memory_id: str
|
|
15
|
+
type: str
|
|
16
|
+
project: str
|
|
17
|
+
title: str
|
|
18
|
+
score: float
|
|
19
|
+
source: str # 'fts', 'semantic', 'metadata'
|
|
20
|
+
metadata: Dict[str, Any] = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class QueryEngine:
|
|
24
|
+
"""
|
|
25
|
+
Engine de consultas híbridas.
|
|
26
|
+
|
|
27
|
+
Combina três tipos de busca:
|
|
28
|
+
- Metadata: filtros estruturados (projeto, tipo, tags)
|
|
29
|
+
- FTS: busca full-text no conteúdo
|
|
30
|
+
- Semantic: busca por similaridade de embeddings
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
metadata_db: MetadataDB,
|
|
36
|
+
embeddings_db: EmbeddingsDB
|
|
37
|
+
):
|
|
38
|
+
"""
|
|
39
|
+
Inicializa o QueryEngine.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
metadata_db: Instância do MetadataDB
|
|
43
|
+
embeddings_db: Instância do EmbeddingsDB
|
|
44
|
+
"""
|
|
45
|
+
self.metadata_db = metadata_db
|
|
46
|
+
self.embeddings_db = embeddings_db
|
|
47
|
+
|
|
48
|
+
def search(
|
|
49
|
+
self,
|
|
50
|
+
query: str,
|
|
51
|
+
project: Optional[str] = None,
|
|
52
|
+
mem_type: Optional[str] = None,
|
|
53
|
+
limit: int = 10,
|
|
54
|
+
use_fts: bool = True,
|
|
55
|
+
use_semantic: bool = True,
|
|
56
|
+
fts_weight: float = 0.4,
|
|
57
|
+
semantic_weight: float = 0.6
|
|
58
|
+
) -> List[QueryResult]:
|
|
59
|
+
"""
|
|
60
|
+
Busca híbrida combinando FTS e semantic.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
query: Texto de busca
|
|
64
|
+
project: Filtrar por projeto
|
|
65
|
+
mem_type: Filtrar por tipo
|
|
66
|
+
limit: Limite de resultados
|
|
67
|
+
use_fts: Usar busca FTS
|
|
68
|
+
use_semantic: Usar busca semantic
|
|
69
|
+
fts_weight: Peso da busca FTS
|
|
70
|
+
semantic_weight: Peso da busca semantic
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Lista de resultados ordenados por relevância
|
|
74
|
+
"""
|
|
75
|
+
results: Dict[str, QueryResult] = {}
|
|
76
|
+
|
|
77
|
+
# Busca FTS
|
|
78
|
+
if use_fts:
|
|
79
|
+
fts_results = self._search_fts(query, project, mem_type, limit)
|
|
80
|
+
for r in fts_results:
|
|
81
|
+
results[r.memory_id] = r
|
|
82
|
+
|
|
83
|
+
# Busca Semantic
|
|
84
|
+
if use_semantic:
|
|
85
|
+
semantic_results = self._search_semantic(query, project, limit)
|
|
86
|
+
for r in semantic_results:
|
|
87
|
+
if r.memory_id in results:
|
|
88
|
+
# Combina scores
|
|
89
|
+
existing = results[r.memory_id]
|
|
90
|
+
combined_score = (
|
|
91
|
+
existing.score * fts_weight +
|
|
92
|
+
r.score * semantic_weight
|
|
93
|
+
)
|
|
94
|
+
results[r.memory_id] = QueryResult(
|
|
95
|
+
memory_id=r.memory_id,
|
|
96
|
+
type=r.type,
|
|
97
|
+
project=r.project,
|
|
98
|
+
title=r.title,
|
|
99
|
+
score=combined_score,
|
|
100
|
+
source="hybrid",
|
|
101
|
+
metadata=r.metadata
|
|
102
|
+
)
|
|
103
|
+
else:
|
|
104
|
+
r.score *= semantic_weight
|
|
105
|
+
results[r.memory_id] = r
|
|
106
|
+
|
|
107
|
+
# Filtra por tipo se especificado
|
|
108
|
+
if mem_type:
|
|
109
|
+
results = {
|
|
110
|
+
k: v for k, v in results.items()
|
|
111
|
+
if v.type == mem_type
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# Ordena por score e limita
|
|
115
|
+
sorted_results = sorted(
|
|
116
|
+
results.values(),
|
|
117
|
+
key=lambda x: x.score,
|
|
118
|
+
reverse=True
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
return sorted_results[:limit]
|
|
122
|
+
|
|
123
|
+
def _search_fts(
|
|
124
|
+
self,
|
|
125
|
+
query: str,
|
|
126
|
+
project: Optional[str],
|
|
127
|
+
mem_type: Optional[str],
|
|
128
|
+
limit: int
|
|
129
|
+
) -> List[QueryResult]:
|
|
130
|
+
"""
|
|
131
|
+
Busca full-text.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
query: Texto de busca
|
|
135
|
+
project: Filtrar por projeto
|
|
136
|
+
mem_type: Filtrar por tipo
|
|
137
|
+
limit: Limite de resultados
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Lista de resultados
|
|
141
|
+
"""
|
|
142
|
+
fts_results = self.metadata_db.search_fts(query, project)
|
|
143
|
+
|
|
144
|
+
results = []
|
|
145
|
+
for row in fts_results[:limit]:
|
|
146
|
+
results.append(QueryResult(
|
|
147
|
+
memory_id=row["id"],
|
|
148
|
+
type=row["type"],
|
|
149
|
+
project=row["project"],
|
|
150
|
+
title=row.get("title", row["id"]),
|
|
151
|
+
score=1.0, # FTS não retorna score normalizado
|
|
152
|
+
source="fts",
|
|
153
|
+
metadata={
|
|
154
|
+
"tags": row.get("tags"),
|
|
155
|
+
"severity": row.get("severity"),
|
|
156
|
+
"access_count": row.get("access_count", 0)
|
|
157
|
+
}
|
|
158
|
+
))
|
|
159
|
+
|
|
160
|
+
return results
|
|
161
|
+
|
|
162
|
+
def _search_semantic(
|
|
163
|
+
self,
|
|
164
|
+
query: str,
|
|
165
|
+
project: Optional[str],
|
|
166
|
+
limit: int
|
|
167
|
+
) -> List[QueryResult]:
|
|
168
|
+
"""
|
|
169
|
+
Busca semântica por similaridade.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
query: Texto de busca
|
|
173
|
+
project: Filtrar por projeto
|
|
174
|
+
limit: Limite de resultados
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
Lista de resultados
|
|
178
|
+
"""
|
|
179
|
+
similar = self.embeddings_db.search_similar(query, project, limit * 2)
|
|
180
|
+
|
|
181
|
+
results = []
|
|
182
|
+
for item in similar:
|
|
183
|
+
# Busca metadados adicionais
|
|
184
|
+
memory = self.metadata_db.get_by_id(item["memory_id"])
|
|
185
|
+
|
|
186
|
+
results.append(QueryResult(
|
|
187
|
+
memory_id=item["memory_id"],
|
|
188
|
+
type=item["type"],
|
|
189
|
+
project=item["project"],
|
|
190
|
+
title=memory.get("title", item["memory_id"]) if memory else item["memory_id"],
|
|
191
|
+
score=item["similarity"],
|
|
192
|
+
source="semantic",
|
|
193
|
+
metadata={
|
|
194
|
+
"similarity": item["similarity"],
|
|
195
|
+
"content_hash": item.get("content_hash")
|
|
196
|
+
}
|
|
197
|
+
))
|
|
198
|
+
|
|
199
|
+
return results
|
|
200
|
+
|
|
201
|
+
def search_by_metadata(
|
|
202
|
+
self,
|
|
203
|
+
project: Optional[str] = None,
|
|
204
|
+
mem_type: Optional[str] = None,
|
|
205
|
+
tags: Optional[List[str]] = None,
|
|
206
|
+
min_score: Optional[float] = None,
|
|
207
|
+
limit: int = 50
|
|
208
|
+
) -> List[QueryResult]:
|
|
209
|
+
"""
|
|
210
|
+
Busca apenas por metadados.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
project: Filtrar por projeto
|
|
214
|
+
mem_type: Filtrar por tipo
|
|
215
|
+
tags: Filtrar por tags
|
|
216
|
+
min_score: Score mínimo
|
|
217
|
+
limit: Limite de resultados
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
Lista de resultados
|
|
221
|
+
"""
|
|
222
|
+
memories = self.metadata_db.search(project, mem_type)
|
|
223
|
+
|
|
224
|
+
results = []
|
|
225
|
+
for m in memories:
|
|
226
|
+
# Filtra por tags
|
|
227
|
+
if tags:
|
|
228
|
+
memory_tags = (m.get("tags") or "").split(",")
|
|
229
|
+
if not any(t in memory_tags for t in tags):
|
|
230
|
+
continue
|
|
231
|
+
|
|
232
|
+
# Filtra por score
|
|
233
|
+
if min_score and m.get("total_score", 0) < min_score:
|
|
234
|
+
continue
|
|
235
|
+
|
|
236
|
+
results.append(QueryResult(
|
|
237
|
+
memory_id=m["id"],
|
|
238
|
+
type=m["type"],
|
|
239
|
+
project=m["project"],
|
|
240
|
+
title=m.get("title", m["id"]),
|
|
241
|
+
score=m.get("total_score", 0),
|
|
242
|
+
source="metadata",
|
|
243
|
+
metadata={
|
|
244
|
+
"tags": m.get("tags"),
|
|
245
|
+
"severity": m.get("severity"),
|
|
246
|
+
"access_count": m.get("access_count", 0),
|
|
247
|
+
"created_at": m.get("created_at"),
|
|
248
|
+
"updated_at": m.get("updated_at")
|
|
249
|
+
}
|
|
250
|
+
))
|
|
251
|
+
|
|
252
|
+
if len(results) >= limit:
|
|
253
|
+
break
|
|
254
|
+
|
|
255
|
+
return results
|
|
256
|
+
|
|
257
|
+
def find_similar_to_memory(
|
|
258
|
+
self,
|
|
259
|
+
memory_id: str,
|
|
260
|
+
limit: int = 5,
|
|
261
|
+
threshold: float = 0.7
|
|
262
|
+
) -> List[QueryResult]:
|
|
263
|
+
"""
|
|
264
|
+
Encontra memórias similares a uma memória específica.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
memory_id: ID da memória de referência
|
|
268
|
+
limit: Limite de resultados
|
|
269
|
+
threshold: Threshold de similaridade
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
Lista de memórias similares
|
|
273
|
+
"""
|
|
274
|
+
# Obtém memória de referência
|
|
275
|
+
memory = self.metadata_db.get_by_id(memory_id)
|
|
276
|
+
if not memory:
|
|
277
|
+
return []
|
|
278
|
+
|
|
279
|
+
# Busca similares
|
|
280
|
+
query = memory.get("title", "") + " " + (memory.get("content", "") or "")[:200]
|
|
281
|
+
similar = self.embeddings_db.search_similar(
|
|
282
|
+
query,
|
|
283
|
+
memory.get("project"),
|
|
284
|
+
limit + 1,
|
|
285
|
+
threshold
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
# Remove a própria memória dos resultados
|
|
289
|
+
similar = [s for s in similar if s["memory_id"] != memory_id]
|
|
290
|
+
|
|
291
|
+
results = []
|
|
292
|
+
for item in similar[:limit]:
|
|
293
|
+
mem = self.metadata_db.get_by_id(item["memory_id"])
|
|
294
|
+
results.append(QueryResult(
|
|
295
|
+
memory_id=item["memory_id"],
|
|
296
|
+
type=item["type"],
|
|
297
|
+
project=item["project"],
|
|
298
|
+
title=mem.get("title", item["memory_id"]) if mem else item["memory_id"],
|
|
299
|
+
score=item["similarity"],
|
|
300
|
+
source="semantic",
|
|
301
|
+
metadata={"similarity": item["similarity"]}
|
|
302
|
+
))
|
|
303
|
+
|
|
304
|
+
return results
|
|
305
|
+
|
|
306
|
+
def get_related(
|
|
307
|
+
self,
|
|
308
|
+
memory_id: str,
|
|
309
|
+
by_tags: bool = True,
|
|
310
|
+
by_semantic: bool = True,
|
|
311
|
+
limit: int = 10
|
|
312
|
+
) -> List[QueryResult]:
|
|
313
|
+
"""
|
|
314
|
+
Obtém memórias relacionadas por múltiplos critérios.
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
memory_id: ID da memória
|
|
318
|
+
by_tags: Buscar por tags similares
|
|
319
|
+
by_semantic: Buscar por similaridade semântica
|
|
320
|
+
limit: Limite de resultados
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
Lista de memórias relacionadas
|
|
324
|
+
"""
|
|
325
|
+
memory = self.metadata_db.get_by_id(memory_id)
|
|
326
|
+
if not memory:
|
|
327
|
+
return []
|
|
328
|
+
|
|
329
|
+
related: Dict[str, QueryResult] = {}
|
|
330
|
+
|
|
331
|
+
# Por tags
|
|
332
|
+
if by_tags and memory.get("tags"):
|
|
333
|
+
tags = memory["tags"].split(",")
|
|
334
|
+
for tag in tags:
|
|
335
|
+
tag_results = self.search_by_metadata(tags=[tag.strip()], limit=limit)
|
|
336
|
+
for r in tag_results:
|
|
337
|
+
if r.memory_id != memory_id:
|
|
338
|
+
if r.memory_id not in related:
|
|
339
|
+
related[r.memory_id] = r
|
|
340
|
+
else:
|
|
341
|
+
related[r.memory_id].score += 0.1
|
|
342
|
+
|
|
343
|
+
# Por similaridade semântica
|
|
344
|
+
if by_semantic:
|
|
345
|
+
semantic_results = self.find_similar_to_memory(memory_id, limit)
|
|
346
|
+
for r in semantic_results:
|
|
347
|
+
if r.memory_id not in related:
|
|
348
|
+
related[r.memory_id] = r
|
|
349
|
+
|
|
350
|
+
# Ordena e limita
|
|
351
|
+
sorted_related = sorted(
|
|
352
|
+
related.values(),
|
|
353
|
+
key=lambda x: x.score,
|
|
354
|
+
reverse=True
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
return sorted_related[:limit]
|