ocerebro 0.3.1 → 0.3.3

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.
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ocerebro",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "OCerebro - Sistema de Memoria para Agentes (Claude Code/MCP)",
5
5
  "main": "bin/ocerebro.js",
6
6
  "bin": {
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ocerebro"
7
- version = "0.3.1"
7
+ version = "0.3.3"
8
8
  description = "OCerebro - Sistema de Memoria para Agentes (Claude Code/MCP)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -50,6 +50,7 @@ class EntitiesDB:
50
50
  memory_id TEXT,
51
51
  entity_name TEXT,
52
52
  entity_type TEXT,
53
+ source TEXT DEFAULT 'content',
53
54
  confidence REAL DEFAULT 1.0,
54
55
  span_start INTEGER,
55
56
  span_end INTEGER,
@@ -75,6 +76,11 @@ class EntitiesDB:
75
76
  ON entities(memory_id)
76
77
  """)
77
78
 
79
+ conn.execute("""
80
+ CREATE INDEX IF NOT EXISTS idx_entities_source
81
+ ON entities(source)
82
+ """)
83
+
78
84
  # Tabela de cache de hash (para evitar reprocessamento)
79
85
  conn.execute("""
80
86
  CREATE TABLE IF NOT EXISTS entity_cache (
@@ -128,7 +134,8 @@ class EntitiesDB:
128
134
  confidence: float = 1.0,
129
135
  span_start: int = 0,
130
136
  span_end: int = 0,
131
- context_snippet: str = ""
137
+ context_snippet: str = "",
138
+ source: str = "content"
132
139
  ) -> str:
133
140
  """
134
141
  Insere uma entidade.
@@ -141,6 +148,7 @@ class EntitiesDB:
141
148
  span_start: Posição inicial no texto
142
149
  span_end: Posição final no texto
143
150
  context_snippet: Contexto ao redor da entidade
151
+ source: Origem da entidade ('frontmatter' ou 'content')
144
152
 
145
153
  Returns:
146
154
  ID da entidade
@@ -150,13 +158,14 @@ class EntitiesDB:
150
158
  conn = self._connect()
151
159
  conn.execute("""
152
160
  INSERT OR REPLACE INTO entities
153
- (id, memory_id, entity_name, entity_type, confidence, span_start, span_end, context_snippet)
154
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
161
+ (id, memory_id, entity_name, entity_type, source, confidence, span_start, span_end, context_snippet)
162
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
155
163
  """, (
156
164
  entity_id,
157
165
  memory_id,
158
166
  entity_name,
159
167
  entity_type,
168
+ source,
160
169
  confidence,
161
170
  span_start,
162
171
  span_end,
@@ -207,7 +216,7 @@ class EntitiesDB:
207
216
 
208
217
  def delete_entities_by_memory(self, memory_id: str) -> int:
209
218
  """
210
- Remove entidades de uma memória.
219
+ Remove todas as entidades de uma memória.
211
220
 
212
221
  Args:
213
222
  memory_id: ID da memória
@@ -232,6 +241,27 @@ class EntitiesDB:
232
241
  conn.close()
233
242
  return deleted
234
243
 
244
+ def delete_entities_by_source(self, memory_id: str, source: str) -> int:
245
+ """
246
+ Remove entidades de uma memória por fonte (frontmatter ou content).
247
+
248
+ Args:
249
+ memory_id: ID da memória
250
+ source: Fonte das entidades ('frontmatter' ou 'content')
251
+
252
+ Returns:
253
+ Número de entidades removidas
254
+ """
255
+ conn = self._connect()
256
+ cursor = conn.execute(
257
+ "DELETE FROM entities WHERE memory_id = ? AND source = ?",
258
+ (memory_id, source)
259
+ )
260
+ deleted = cursor.rowcount
261
+ conn.commit()
262
+ conn.close()
263
+ return deleted
264
+
235
265
  # ========================================================================
236
266
  # OPERAÇÕES DE RELACIONAMENTOS
237
267
  # ========================================================================
@@ -521,7 +551,8 @@ class EntitiesDB:
521
551
  memory_id,
522
552
  f"TYPE:{frontmatter['type']}",
523
553
  "META",
524
- confidence=1.0
554
+ confidence=1.0,
555
+ source="frontmatter"
525
556
  )
526
557
  entity_ids.append(eid)
527
558
 
@@ -531,7 +562,8 @@ class EntitiesDB:
531
562
  memory_id,
532
563
  project,
533
564
  "PROJECT",
534
- confidence=1.0
565
+ confidence=1.0,
566
+ source="frontmatter"
535
567
  )
536
568
  entity_ids.append(eid)
537
569
 
@@ -544,7 +576,8 @@ class EntitiesDB:
544
576
  memory_id,
545
577
  f"TAG:{tag}",
546
578
  "TAG",
547
- confidence=1.0
579
+ confidence=1.0,
580
+ source="frontmatter"
548
581
  )
549
582
  entity_ids.append(eid)
550
583
  elif isinstance(tags, list):
@@ -553,7 +586,8 @@ class EntitiesDB:
553
586
  memory_id,
554
587
  f"TAG:{tag}",
555
588
  "TAG",
556
- confidence=1.0
589
+ confidence=1.0,
590
+ source="frontmatter"
557
591
  )
558
592
  entity_ids.append(eid)
559
593
 
@@ -580,14 +614,18 @@ class EntitiesDB:
580
614
  cached_hash = self.get_cached_hash(memory_id)
581
615
  current_hash = self._compute_hash(content)
582
616
 
617
+ skip_delete = False
583
618
  if cached_hash == current_hash:
584
619
  # Conteúdo igual, verifica se já tem entidades
585
620
  existing = self.get_entities_by_memory(memory_id)
586
621
  if existing:
587
622
  return [] # Já processado, sem mudanças
623
+ # Cache bate mas entidades vazias (ex: banco limpo) - reprocessa sem delete
624
+ skip_delete = True
588
625
 
589
- # Conteúdo novo ou mudou - remove entidades antigas e reprocessa
590
- self.delete_entities_by_memory(memory_id)
626
+ # Conteúdo novo ou mudou - remove apenas entidades de conteúdo e reprocessa
627
+ if not skip_delete:
628
+ self.delete_entities_by_source(memory_id, "content")
591
629
 
592
630
  entity_ids = []
593
631
 
package/src/mcp/server.py CHANGED
@@ -693,6 +693,7 @@ Uma chamada por memória. O sistema salva e indexa automaticamente.
693
693
  def _capture_memory(self, args: Dict[str, Any]) -> str:
694
694
  """Salva uma memória no diretório nativo do Claude Code."""
695
695
  import re
696
+ import yaml
696
697
  from datetime import datetime
697
698
  from src.core.paths import get_memory_index
698
699
 
@@ -711,15 +712,36 @@ Uma chamada por memória. O sistema salva e indexa automaticamente.
711
712
  file_path = mem_dir / f"{mem_name}.md"
712
713
  file_path.write_text(content, encoding="utf-8")
713
714
 
714
- desc_match = re.search(r'description:\s*(.*)', content)
715
- type_match = re.search(r'type:\s*(.*)', content)
716
- project_match = re.search(r'project:\s*(.*)', content)
717
- tags_match = re.search(r'tags:\s*(.*)', content)
718
-
719
- desc = desc_match.group(1).strip() if desc_match else "sem descrição"
720
- m_type = type_match.group(1).strip() if type_match else "project"
721
- project = project_match.group(1).strip() if project_match else "unknown"
722
- tags = tags_match.group(1).strip() if tags_match else ""
715
+ # Parse frontmatter uma única vez com yaml.safe_load
716
+ frontmatter_match = re.match(r'^---\n(.*?)\n---\n(.*)$', content, re.DOTALL)
717
+ frontmatter = {}
718
+ body_content = ""
719
+ if frontmatter_match:
720
+ try:
721
+ frontmatter = yaml.safe_load(frontmatter_match.group(1)) or {}
722
+ body_content = frontmatter_match.group(2)
723
+ except Exception:
724
+ pass # Fallback para regex se yaml falhar
725
+
726
+ # Extrai variáveis do frontmatter parseado (fallback para regex se necessário)
727
+ m_type = frontmatter.get('type', '')
728
+ project = frontmatter.get('project', '')
729
+ tags = frontmatter.get('tags', '')
730
+ desc = frontmatter.get('description', '')
731
+
732
+ # Fallback via regex se frontmatter parsing falhou
733
+ if not m_type:
734
+ type_match = re.search(r'type:\s*(.*)', content)
735
+ m_type = type_match.group(1).strip() if type_match else "project"
736
+ if not project:
737
+ project_match = re.search(r'project:\s*(.*)', content)
738
+ project = project_match.group(1).strip() if project_match else "unknown"
739
+ if not tags:
740
+ tags_match = re.search(r'tags:\s*(.*)', content)
741
+ tags = tags_match.group(1).strip() if tags_match else ""
742
+ if not desc:
743
+ desc_match = re.search(r'description:\s*(.*)', content)
744
+ desc = desc_match.group(1).strip() if desc_match else "sem descrição"
723
745
 
724
746
  ts = datetime.now().strftime("%Y-%m-%d")
725
747
  entry = f"- [{m_type}] {mem_name}.md ({ts}): {desc}\n"
@@ -733,28 +755,25 @@ Uma chamada por memória. O sistema salva e indexa automaticamente.
733
755
  else:
734
756
  index_path.write_text(f"# Memórias do Projeto\n\n{entry}", encoding="utf-8")
735
757
 
736
- # BUG FIX: Registrar entidades no grafo (frontmatter + conteúdo)
737
- if self.entities_db:
738
- import yaml
739
- frontmatter_match = re.match(r'^---\n(.*?)\n---\n(.*)$', content, re.DOTALL)
740
- if frontmatter_match:
741
- try:
742
- frontmatter = yaml.safe_load(frontmatter_match.group(1))
743
- # Extrai entidades do frontmatter
744
- self.entities_db.extract_from_frontmatter(
745
- memory_id=mem_name,
746
- frontmatter=frontmatter or {},
747
- project=project
748
- )
749
- # Extrai entidades do conteúdo (spaCy NER)
750
- body_content = frontmatter_match.group(2)
751
- self.entities_db.extract_from_content(
752
- memory_id=mem_name,
753
- content=body_content,
754
- use_spacy=True
755
- )
756
- except Exception as e:
757
- pass # Falha silenciosa se frontmatter inválido
758
+ # Registrar entidades no grafo (frontmatter + conteúdo)
759
+ # ORDEM IMPORTANTE: content primeiro, frontmatter depois
760
+ # extract_from_content() deleta apenas entidades 'content', preservando 'frontmatter'
761
+ if self.entities_db and frontmatter_match:
762
+ try:
763
+ # 1. Extrai entidades do conteúdo (spaCy NER)
764
+ self.entities_db.extract_from_content(
765
+ memory_id=mem_name,
766
+ content=body_content,
767
+ use_spacy=True
768
+ )
769
+ # 2. Extrai entidades do frontmatter - preservadas
770
+ self.entities_db.extract_from_frontmatter(
771
+ memory_id=mem_name,
772
+ frontmatter=frontmatter,
773
+ project=project
774
+ )
775
+ except Exception:
776
+ pass # Falha silenciosa se frontmatter inválido
758
777
 
759
778
  return f"✅ Memória '{mem_name}' salva em {file_path}"
760
779