cortext-memory 0.1.0__py3-none-any.whl

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.
cortext/__init__.py ADDED
@@ -0,0 +1,72 @@
1
+ """Cortext — Memory system for AI agents.
2
+
3
+ 5-element detector compliant, internationalized, efficient.
4
+ """
5
+
6
+ from cortext.cortex import CortexV5
7
+ from cortext.core.memory import Memory
8
+ from cortext.core.entity import Entity
9
+ from cortext.core.relation import Relation, RelationType
10
+ from cortext.core.graph import MemoryGraph, RecallResult
11
+ from cortext.core.validation import (
12
+ CanonicalValidator,
13
+ ValidationResult,
14
+ ValidationStatus,
15
+ ValidationPolicy,
16
+ create_default_validator,
17
+ create_strict_validator,
18
+ )
19
+ from cortext.core.recall import (
20
+ StructuralQueryParser,
21
+ QueryIntent,
22
+ pack_for_context,
23
+ RegexExtractor,
24
+ LLMExtractor,
25
+ HybridExtractor,
26
+ )
27
+ from cortext.core.decay import (
28
+ DecayConfig,
29
+ retrievability,
30
+ effective_stability,
31
+ decay_status,
32
+ ForgetGate,
33
+ ForgetGateConfig,
34
+ )
35
+ from cortext.workers import DreamAgent
36
+
37
+ __version__ = "0.1.0"
38
+
39
+ __all__ = [
40
+ # Main entry point
41
+ "CortexV5",
42
+ # Core data structures
43
+ "Memory",
44
+ "Entity",
45
+ "Relation",
46
+ "RelationType",
47
+ "MemoryGraph",
48
+ "RecallResult",
49
+ # Validation
50
+ "CanonicalValidator",
51
+ "ValidationResult",
52
+ "ValidationStatus",
53
+ "ValidationPolicy",
54
+ "create_default_validator",
55
+ "create_strict_validator",
56
+ # Recall
57
+ "StructuralQueryParser",
58
+ "QueryIntent",
59
+ "pack_for_context",
60
+ "RegexExtractor",
61
+ "LLMExtractor",
62
+ "HybridExtractor",
63
+ # Decay
64
+ "DecayConfig",
65
+ "retrievability",
66
+ "effective_stability",
67
+ "decay_status",
68
+ "ForgetGate",
69
+ "ForgetGateConfig",
70
+ # Workers
71
+ "DreamAgent",
72
+ ]
@@ -0,0 +1 @@
1
+ """Core data structures: Memory, Entity, Relation, MemoryGraph."""
@@ -0,0 +1,23 @@
1
+ """
2
+ Decay subsystem: Ebbinghaus R = e^(-t/S) + Forget Gate.
3
+ """
4
+
5
+ from cortext.core.decay.ebbinghaus import (
6
+ DecayConfig,
7
+ retrievability,
8
+ effective_stability,
9
+ decay_status,
10
+ )
11
+ from cortext.core.decay.forget_gate import (
12
+ ForgetGate,
13
+ ForgetGateConfig,
14
+ )
15
+
16
+ __all__ = [
17
+ "DecayConfig",
18
+ "retrievability",
19
+ "effective_stability",
20
+ "decay_status",
21
+ "ForgetGate",
22
+ "ForgetGateConfig",
23
+ ]
@@ -0,0 +1,126 @@
1
+ """
2
+ Ebbinghaus decay — R = e^(-t/S).
3
+
4
+ The classic forgetting curve, applied to memory retrieval. Simple,
5
+ universal, and validated empirically (Ebbinghaus, 1885).
6
+
7
+ Formula: R = e^(-t/S)
8
+ - R: retrievability (0.0-1.0)
9
+ - t: time since last access (days)
10
+ - S: stability (days) — base × modifiers
11
+
12
+ Stability modifiers (extensible):
13
+ - access_count: more accesses = more stable (log scale)
14
+ - importance: high importance = more stable
15
+ - consolidation: consolidated memories = more stable
16
+ - (custom) user-defined via metadata
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import math
22
+ from dataclasses import dataclass
23
+ from datetime import datetime
24
+ from typing import TYPE_CHECKING
25
+
26
+ if TYPE_CHECKING:
27
+ from cortext.core.memory import Memory
28
+
29
+
30
+ @dataclass
31
+ class DecayConfig:
32
+ """Tunables for the decay function."""
33
+
34
+ # Base stability (days for 63% decay without reinforcement)
35
+ base_stability_days: float = 7.0
36
+
37
+ # Stability modifiers
38
+ access_log_multiplier: float = 1.0 # log(access_count + 1) factor
39
+ importance_bonus: float = 1.3 # bonus for high importance (>0.7)
40
+ consolidation_bonus: float = 2.0 # bonus for consolidated memories
41
+
42
+ # Min/max stability (prevent degenerate values)
43
+ min_stability: float = 0.1
44
+ max_stability: float = 365.0
45
+
46
+ # Thresholds for status
47
+ active_threshold: float = 0.7
48
+ fading_threshold: float = 0.3
49
+ forgotten_threshold: float = 0.1
50
+
51
+
52
+ def retrievability(
53
+ memory: "Memory",
54
+ now: datetime | None = None,
55
+ config: DecayConfig | None = None,
56
+ ) -> float:
57
+ """
58
+ Compute retrievability of a memory using the Ebbinghaus curve.
59
+
60
+ R = e^(-t/S), where S = base_stability × modifiers.
61
+
62
+ Returns:
63
+ float between 0.0 (forgotten) and 1.0 (fresh)
64
+ """
65
+ if config is None:
66
+ config = DecayConfig()
67
+ now = now or datetime.now()
68
+
69
+ # Reference time = last access, fallback to creation
70
+ reference_time = memory.last_accessed or memory.when
71
+ days_since = max(0.0, (now - reference_time).total_seconds() / 86400)
72
+
73
+ # Compute effective stability
74
+ s = effective_stability(memory, config)
75
+
76
+ # Ebbinghaus formula
77
+ return math.exp(-days_since / s)
78
+
79
+
80
+ def effective_stability(
81
+ memory: "Memory",
82
+ config: DecayConfig | None = None,
83
+ ) -> float:
84
+ """
85
+ Compute the effective stability S for a memory.
86
+
87
+ S = base × access_modifier × importance_modifier × consolidation_modifier
88
+ """
89
+ if config is None:
90
+ config = DecayConfig()
91
+
92
+ s = config.base_stability_days
93
+
94
+ # Access count modifier (logarithmic)
95
+ access_factor = 1.0 + config.access_log_multiplier * math.log(memory.access_count + 1)
96
+ s *= access_factor
97
+
98
+ # Importance bonus
99
+ if memory.importance > 0.7:
100
+ s *= config.importance_bonus
101
+
102
+ # Consolidation bonus
103
+ if memory.is_consolidated:
104
+ s *= config.consolidation_bonus
105
+
106
+ return max(config.min_stability, min(config.max_stability, s))
107
+
108
+
109
+ def decay_status(
110
+ memory: "Memory",
111
+ now: datetime | None = None,
112
+ config: DecayConfig | None = None,
113
+ ) -> str:
114
+ """
115
+ Get a categorical status: "active" | "fading" | "weak" | "forgotten".
116
+ """
117
+ if config is None:
118
+ config = DecayConfig()
119
+ r = retrievability(memory, now, config)
120
+ if r >= config.active_threshold:
121
+ return "active"
122
+ elif r >= config.fading_threshold:
123
+ return "fading"
124
+ elif r >= config.forgotten_threshold:
125
+ return "weak"
126
+ return "forgotten"
@@ -0,0 +1,169 @@
1
+ """
2
+ Forget Gate — active forgetting based on 3 signals.
3
+
4
+ Inspired by LSTM forget gates and active forgetting in human cognition.
5
+ 3 signals combine into a forget score (0-1, higher = more forgettable):
6
+
7
+ 1. Noise (0.4 weight): how likely the memory is noise vs signal
8
+ - Low importance, no participants, empty/short content
9
+ 2. Redundancy (0.35): is this memory redundant with others?
10
+ 3. Obsolescence (0.25): is this memory outdated?
11
+
12
+ Usage: gate = ForgetGate()
13
+ score = gate.compute_forget_signal(memory, graph)
14
+ if score >= threshold: accelerate_decay(memory)
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from dataclasses import dataclass
20
+ from datetime import datetime
21
+ from typing import TYPE_CHECKING
22
+
23
+ if TYPE_CHECKING:
24
+ from cortext.core.memory import Memory
25
+ from cortext.core.graph import MemoryGraph
26
+
27
+
28
+ @dataclass
29
+ class ForgetGateConfig:
30
+ noise_weight: float = 0.4
31
+ redundancy_weight: float = 0.35
32
+ obsolescence_weight: float = 0.25
33
+ forget_threshold: float = 0.3 # lowered from 0.6 — typical "noisy" memories cross this
34
+
35
+
36
+ class ForgetGate:
37
+ """
38
+ Active forgetting: score memories for deletion.
39
+
40
+ Combines 3 signals into a single forget score.
41
+ """
42
+
43
+ def __init__(self, config: ForgetGateConfig | None = None) -> None:
44
+ self.config = config or ForgetGateConfig()
45
+
46
+ def compute_forget_signal(
47
+ self, memory: "Memory", graph: "MemoryGraph"
48
+ ) -> float:
49
+ """Compute forget signal (0-1, higher = more forgettable)."""
50
+ noise = self._noise_score(memory)
51
+ redundancy = self._redundancy_score(memory, graph)
52
+ obsolescence = self._obsolescence_score(memory)
53
+
54
+ return min(1.0, max(0.0, (
55
+ self.config.noise_weight * noise +
56
+ self.config.redundancy_weight * redundancy +
57
+ self.config.obsolescence_weight * obsolescence
58
+ )))
59
+
60
+ def filter_forgettable(
61
+ self, memories: list["Memory"], graph: "MemoryGraph"
62
+ ) -> tuple[list["Memory"], list["Memory"]]:
63
+ """
64
+ Split memories into (keep, forget) based on forget signal.
65
+
66
+ Returns:
67
+ (memories_to_keep, memories_to_forget)
68
+ """
69
+ keep: list["Memory"] = []
70
+ forget: list["Memory"] = []
71
+ for mem in memories:
72
+ signal = self.compute_forget_signal(mem, graph)
73
+ if signal >= self.config.forget_threshold:
74
+ forget.append(mem)
75
+ else:
76
+ keep.append(mem)
77
+ return keep, forget
78
+
79
+ def accelerate_decay(
80
+ self, memory: "Memory", multiplier: float = 0.5
81
+ ) -> float:
82
+ """Reduce importance to accelerate decay. Returns new importance."""
83
+ memory.importance = max(0.0, memory.importance * multiplier)
84
+ return memory.importance
85
+
86
+ def _noise_score(self, memory: "Memory") -> float:
87
+ """Estimate if memory is noise vs signal."""
88
+ score = 0.0
89
+
90
+ # Low importance
91
+ if memory.importance < 0.3:
92
+ score += 0.3
93
+ elif memory.importance < 0.5:
94
+ score += 0.1
95
+
96
+ # No participants
97
+ if not memory.who:
98
+ score += 0.2
99
+
100
+ # Empty/short content
101
+ content_len = len(memory.what or "")
102
+ if content_len < 10:
103
+ score += 0.2
104
+ elif content_len < 30:
105
+ score += 0.1
106
+
107
+ # Generic values
108
+ generic_terms = {"undefined", "none", "null", "unknown", "n/a", ""}
109
+ if (memory.what or "").lower().strip() in generic_terms:
110
+ score += 0.2
111
+
112
+ # Not consolidated
113
+ if not memory.is_consolidated:
114
+ score += 0.1
115
+
116
+ return min(1.0, score)
117
+
118
+ def _redundancy_score(
119
+ self, memory: "Memory", graph: "MemoryGraph"
120
+ ) -> float:
121
+ """Estimate redundancy with other memories."""
122
+ score = 0.0
123
+ similar_count = 0
124
+ if hasattr(graph, "iter_memories"):
125
+ for other in graph.iter_memories():
126
+ if other.id == memory.id:
127
+ continue
128
+ # Simple overlap check on what-field
129
+ a = set((memory.what or "").lower().split())
130
+ b = set((other.what or "").lower().split())
131
+ if a and b:
132
+ overlap = len(a & b) / len(a | b)
133
+ if overlap > 0.7:
134
+ similar_count += 1
135
+
136
+ if similar_count >= 3:
137
+ score += 0.5
138
+ elif similar_count >= 1:
139
+ score += 0.2
140
+
141
+ return min(1.0, score)
142
+
143
+ def _obsolescence_score(self, memory: "Memory") -> float:
144
+ """Estimate if memory is obsolete."""
145
+ score = 0.0
146
+ now = datetime.now()
147
+
148
+ # Old with no recent access
149
+ if memory.when:
150
+ days_old = (now - memory.when).days
151
+ last_access = memory.last_accessed or memory.when
152
+ days_since_access = (now - last_access).days
153
+ if days_old > 90 and days_since_access > 60:
154
+ score += 0.4
155
+ elif days_old > 30 and days_since_access > 30:
156
+ score += 0.2
157
+
158
+ # Low retrievability (uses ebbinghaus)
159
+ try:
160
+ from cortext.core.decay.ebbinghaus import retrievability
161
+ r = retrievability(memory)
162
+ if r < 0.2:
163
+ score += 0.4
164
+ elif r < 0.4:
165
+ score += 0.2
166
+ except Exception:
167
+ pass
168
+
169
+ return min(1.0, score)
cortext/core/entity.py ADDED
@@ -0,0 +1,85 @@
1
+ """
2
+ Entity — represents a "thing" that can be referenced in memories.
3
+
4
+ Entities are language-agnostic. A person named "Maria" in PT and
5
+ "Maria" in EN is the same entity. The name is just a string.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+ from datetime import datetime
12
+ from typing import Any
13
+ from uuid import uuid4
14
+
15
+
16
+ @dataclass
17
+ class Entity:
18
+ """
19
+ A referenceable thing in the memory system.
20
+
21
+ Attributes:
22
+ type: type tag (e.g., "person", "place", "concept", "file")
23
+ name: display name (in any language)
24
+ identifiers: list of ways to identify this entity (emails, IDs, paths)
25
+ attributes: free-form metadata
26
+ """
27
+
28
+ type: str
29
+ name: str
30
+ identifiers: list[str] = field(default_factory=list)
31
+ attributes: dict[str, Any] = field(default_factory=dict)
32
+ id: str = field(default_factory=lambda: str(uuid4()))
33
+ created_at: datetime = field(default_factory=datetime.now)
34
+ updated_at: datetime = field(default_factory=datetime.now)
35
+
36
+ def __post_init__(self) -> None:
37
+ if not self.type or not self.type.strip():
38
+ raise ValueError("Entity 'type' is required")
39
+ if not self.name or not self.name.strip():
40
+ raise ValueError("Entity 'name' is required")
41
+ self.type = self.type.strip()
42
+ self.name = self.name.strip()
43
+
44
+ def matches(self, query: str) -> bool:
45
+ """Check if this entity matches a query (name, ID, or identifier)."""
46
+ if not query:
47
+ return False
48
+ q = query.lower()
49
+ if q == self.id or q == self.name.lower():
50
+ return True
51
+ if any(q in ident.lower() for ident in self.identifiers):
52
+ return True
53
+ return False
54
+
55
+ def to_dict(self) -> dict[str, Any]:
56
+ return {
57
+ "id": self.id,
58
+ "type": self.type,
59
+ "name": self.name,
60
+ "identifiers": list(self.identifiers),
61
+ "attributes": dict(self.attributes),
62
+ "created_at": self.created_at.isoformat(),
63
+ "updated_at": self.updated_at.isoformat(),
64
+ }
65
+
66
+ @classmethod
67
+ def from_dict(cls, data: dict[str, Any]) -> "Entity":
68
+ return cls(
69
+ id=data.get("id", str(uuid4())),
70
+ type=data.get("type", ""),
71
+ name=data.get("name", ""),
72
+ identifiers=data.get("identifiers", []),
73
+ attributes=data.get("attributes", {}),
74
+ )
75
+
76
+ def __repr__(self) -> str:
77
+ return f"Entity(type={self.type!r}, name={self.name!r})"
78
+
79
+ def __eq__(self, other: object) -> bool:
80
+ if not isinstance(other, Entity):
81
+ return False
82
+ return self.id == other.id
83
+
84
+ def __hash__(self) -> int:
85
+ return hash(self.id)
cortext/core/graph.py ADDED
@@ -0,0 +1,226 @@
1
+ """
2
+ MemoryGraph — collection of memories, entities, and relations.
3
+
4
+ Enxuto, sem feature flags, sem BFS/Louvain/PageRank. Operations are
5
+ O(1) for direct lookup, O(n) for scans. The Dream Agent (background
6
+ worker) handles consolidation separately.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass, field
12
+ from typing import Any, Iterator, Optional
13
+
14
+ from cortext.core.memory import Memory
15
+ from cortext.core.entity import Entity
16
+ from cortext.core.relation import Relation
17
+
18
+
19
+ @dataclass
20
+ class RecallResult:
21
+ """Result of a memory recall operation."""
22
+
23
+ memories: list[Memory] = field(default_factory=list)
24
+ entities: list[Entity] = field(default_factory=list)
25
+ relations: list[Relation] = field(default_factory=list)
26
+ metrics: dict[str, Any] = field(default_factory=dict)
27
+
28
+ def __len__(self) -> int:
29
+ return len(self.memories)
30
+
31
+ def is_empty(self) -> bool:
32
+ return len(self.memories) == 0 and len(self.entities) == 0 and len(self.relations) == 0
33
+
34
+ def to_dict(self) -> dict[str, Any]:
35
+ return {
36
+ "memories": [m.to_dict() for m in self.memories],
37
+ "entities": [e.to_dict() for e in self.entities],
38
+ "relations": [r.to_dict() for r in self.relations],
39
+ "metrics": self.metrics,
40
+ }
41
+
42
+
43
+ class MemoryGraph:
44
+ """
45
+ In-memory graph of memories, entities, and relations.
46
+
47
+ API enxuto: 5 métodos públicos (add_memory, add_entity, add_relation,
48
+ get_*, iter_*) + 1 busca livre (find). Sem indexação sofisticada
49
+ (sem inverted index, sem BFS expansion). Dream Agent pode adicionar
50
+ indexação opcional se necessário.
51
+ """
52
+
53
+ def __init__(self, namespace: str = "default") -> None:
54
+ """Initialize an in-memory graph (no persistence in core)."""
55
+ self.namespace = namespace
56
+ self._memories: dict[str, Memory] = {}
57
+ self._entities: dict[str, Entity] = {}
58
+ self._relations: dict[str, Relation] = {}
59
+
60
+ # === Add operations ===
61
+
62
+ def add_memory(self, memory: Memory) -> Memory:
63
+ """Add a memory. Returns the memory (with id)."""
64
+ if not isinstance(memory, Memory):
65
+ raise TypeError(f"expected Memory, got {type(memory).__name__}")
66
+ self._memories[memory.id] = memory
67
+ return memory
68
+
69
+ def add_entity(self, entity: Entity) -> Entity:
70
+ """Add an entity. Returns the entity (with id)."""
71
+ if not isinstance(entity, Entity):
72
+ raise TypeError(f"expected Entity, got {type(entity).__name__}")
73
+ self._entities[entity.id] = entity
74
+ return entity
75
+
76
+ def add_relation(self, relation: Relation) -> Relation:
77
+ """Add a relation. Returns the relation (with id)."""
78
+ if not isinstance(relation, Relation):
79
+ raise TypeError(f"expected Relation, got {type(relation).__name__}")
80
+ self._relations[relation.id] = relation
81
+ return relation
82
+
83
+ # === Get operations ===
84
+
85
+ def get_memory(self, memory_id: str) -> Optional[Memory]:
86
+ return self._memories.get(memory_id)
87
+
88
+ def get_entity(self, entity_id: str) -> Optional[Entity]:
89
+ return self._entities.get(entity_id)
90
+
91
+ def get_relation(self, relation_id: str) -> Optional[Relation]:
92
+ return self._relations.get(relation_id)
93
+
94
+ # === Iteration ===
95
+
96
+ def iter_memories(self) -> Iterator[Memory]:
97
+ return iter(self._memories.values())
98
+
99
+ def iter_entities(self) -> Iterator[Entity]:
100
+ return iter(self._entities.values())
101
+
102
+ def iter_relations(self) -> Iterator[Relation]:
103
+ return iter(self._relations.values())
104
+
105
+ def all_memories(self) -> list[Memory]:
106
+ return list(self._memories.values())
107
+
108
+ def all_entities(self) -> list[Entity]:
109
+ return list(self._entities.values())
110
+
111
+ def all_relations(self) -> list[Relation]:
112
+ return list(self._relations.values())
113
+
114
+ # === Find operations ===
115
+
116
+ def find_memories(
117
+ self,
118
+ who: Optional[str] = None,
119
+ where: Optional[str] = None,
120
+ what_contains: Optional[str] = None,
121
+ ) -> list[Memory]:
122
+ """
123
+ Find memories matching filters. O(n) scan.
124
+
125
+ Args:
126
+ who: filter by participant (case-insensitive substring)
127
+ where: filter by namespace/exact match
128
+ what_contains: filter by substring in 'what' field
129
+ """
130
+ results: list[Memory] = []
131
+ for mem in self._memories.values():
132
+ if who is not None and not mem.is_about(who):
133
+ continue
134
+ if where is not None and mem.where != where:
135
+ continue
136
+ if what_contains is not None and what_contains.lower() not in mem.what.lower():
137
+ continue
138
+ results.append(mem)
139
+ return results
140
+
141
+ def find_entities_by_name(self, name: str) -> list[Entity]:
142
+ """Find entities by name (case-insensitive exact match)."""
143
+ name_lower = name.lower()
144
+ return [e for e in self._entities.values() if e.name.lower() == name_lower]
145
+
146
+ def find_relations(
147
+ self,
148
+ from_id: Optional[str] = None,
149
+ to_id: Optional[str] = None,
150
+ relation_type: Optional[str] = None,
151
+ ) -> list[Relation]:
152
+ """Find relations matching filters. O(n) scan."""
153
+ results: list[Relation] = []
154
+ for rel in self._relations.values():
155
+ if from_id is not None and rel.from_id != from_id:
156
+ continue
157
+ if to_id is not None and rel.to_id != to_id:
158
+ continue
159
+ if relation_type is not None and rel.relation_type != relation_type.lower():
160
+ continue
161
+ results.append(rel)
162
+ return results
163
+
164
+ # === Persistence ===
165
+
166
+ def to_dict(self) -> dict[str, Any]:
167
+ """Serialize the whole graph to a plain dict (JSON-ready)."""
168
+ return {
169
+ "namespace": self.namespace,
170
+ "memories": [m.to_dict() for m in self._memories.values()],
171
+ "entities": [e.to_dict() for e in self._entities.values()],
172
+ "relations": [r.to_dict() for r in self._relations.values()],
173
+ }
174
+
175
+ @classmethod
176
+ def from_dict(cls, data: dict[str, Any]) -> "MemoryGraph":
177
+ """Reconstruct a graph from a dict produced by ``to_dict``."""
178
+ graph = cls(namespace=data.get("namespace", "default"))
179
+ for m in data.get("memories", []):
180
+ graph.add_memory(Memory.from_dict(m))
181
+ for e in data.get("entities", []):
182
+ graph.add_entity(Entity.from_dict(e))
183
+ for r in data.get("relations", []):
184
+ graph.add_relation(Relation.from_dict(r))
185
+ return graph
186
+
187
+ def save(self, path: Any) -> None:
188
+ """Persist the graph to a JSON file at ``path`` (atomic write)."""
189
+ import json
190
+ import os
191
+ from pathlib import Path
192
+
193
+ path = Path(path)
194
+ path.parent.mkdir(parents=True, exist_ok=True)
195
+ tmp = path.with_suffix(path.suffix + ".tmp")
196
+ with open(tmp, "w", encoding="utf-8") as f:
197
+ json.dump(self.to_dict(), f, ensure_ascii=False, default=str)
198
+ os.replace(tmp, path)
199
+
200
+ @classmethod
201
+ def load(cls, path: Any, namespace: str = "default") -> "MemoryGraph":
202
+ """Load a graph from a JSON file. Returns an empty graph if missing."""
203
+ import json
204
+ from pathlib import Path
205
+
206
+ path = Path(path)
207
+ if not path.exists():
208
+ return cls(namespace=namespace)
209
+ with open(path, encoding="utf-8") as f:
210
+ return cls.from_dict(json.load(f))
211
+
212
+ # === Stats ===
213
+
214
+ def stats(self) -> dict[str, Any]:
215
+ return {
216
+ "namespace": self.namespace,
217
+ "total_memories": len(self._memories),
218
+ "total_entities": len(self._entities),
219
+ "total_relations": len(self._relations),
220
+ }
221
+
222
+ def __len__(self) -> int:
223
+ return len(self._memories)
224
+
225
+ def __repr__(self) -> str:
226
+ return f"MemoryGraph(ns={self.namespace!r}, M={len(self._memories)}, E={len(self._entities)}, R={len(self._relations)})"