agentic-memory-ai 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.
@@ -0,0 +1,49 @@
1
+ """agentic-memory: Framework-agnostic memory layer for AI agents."""
2
+
3
+ from .memory import AgentMemory
4
+ from .decay import DecayEngine
5
+ from .types import (
6
+ MemoryEntry,
7
+ MemoryType,
8
+ ImportanceLevel,
9
+ DecayPolicy,
10
+ DecayConfig,
11
+ DecayTypeConfig,
12
+ RetrievalQuery,
13
+ RetrievalResult,
14
+ ConflictResult,
15
+ ConflictAction,
16
+ Checkpoint,
17
+ TaskNode,
18
+ TaskStatus,
19
+ )
20
+ from .store import LocalStore, FileStore
21
+ from .retrieval import BuiltinEmbedder, MultiSignalRetriever, ConflictDetector
22
+ from .utils import cosine_similarity, generate_id
23
+
24
+ __version__ = "0.1.0"
25
+
26
+ __all__ = [
27
+ "AgentMemory",
28
+ "DecayEngine",
29
+ "MemoryEntry",
30
+ "MemoryType",
31
+ "ImportanceLevel",
32
+ "DecayPolicy",
33
+ "DecayConfig",
34
+ "DecayTypeConfig",
35
+ "RetrievalQuery",
36
+ "RetrievalResult",
37
+ "ConflictResult",
38
+ "ConflictAction",
39
+ "Checkpoint",
40
+ "TaskNode",
41
+ "TaskStatus",
42
+ "LocalStore",
43
+ "FileStore",
44
+ "BuiltinEmbedder",
45
+ "MultiSignalRetriever",
46
+ "ConflictDetector",
47
+ "cosine_similarity",
48
+ "generate_id",
49
+ ]
@@ -0,0 +1,4 @@
1
+ from .openai import OpenAIEmbedder
2
+ from .voyageai import VoyageEmbedder
3
+
4
+ __all__ = ["OpenAIEmbedder", "VoyageEmbedder"]
@@ -0,0 +1,70 @@
1
+ """
2
+ OpenAI embedder adapter.
3
+ Requires: pip install httpx (or aiohttp)
4
+
5
+ Usage:
6
+ from agentic_memory import AgentMemory
7
+ from agentic_memory.adapters import OpenAIEmbedder
8
+
9
+ memory = AgentMemory(
10
+ embedder=OpenAIEmbedder(api_key="sk-...")
11
+ )
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ from urllib.request import Request, urlopen
18
+ from typing import Optional
19
+
20
+
21
+ class OpenAIEmbedder:
22
+ def __init__(
23
+ self,
24
+ api_key: str,
25
+ model: str = "text-embedding-3-small",
26
+ dim: int = 1536,
27
+ base_url: str = "https://api.openai.com/v1",
28
+ ) -> None:
29
+ self._api_key = api_key
30
+ self._model = model
31
+ self._dim = dim
32
+ self._base_url = base_url
33
+
34
+ def dimensions(self) -> int:
35
+ return self._dim
36
+
37
+ async def embed(self, text: str) -> list[float]:
38
+ result = self._call([text])
39
+ return result[0]
40
+
41
+ async def embed_batch(self, texts: list[str]) -> list[list[float]]:
42
+ if not texts:
43
+ return []
44
+ return self._call(texts)
45
+
46
+ def _call(self, input_texts: list[str]) -> list[list[float]]:
47
+ payload = json.dumps({
48
+ "model": self._model,
49
+ "input": input_texts,
50
+ "dimensions": self._dim,
51
+ }).encode("utf-8")
52
+
53
+ req = Request(
54
+ f"{self._base_url}/embeddings",
55
+ data=payload,
56
+ headers={
57
+ "Content-Type": "application/json",
58
+ "Authorization": f"Bearer {self._api_key}",
59
+ },
60
+ method="POST",
61
+ )
62
+
63
+ with urlopen(req) as resp:
64
+ if resp.status != 200:
65
+ raise RuntimeError(f"OpenAI embedding failed ({resp.status})")
66
+ data = json.loads(resp.read())
67
+
68
+ # Sort by index (OpenAI may return out of order)
69
+ items = sorted(data["data"], key=lambda d: d["index"])
70
+ return [d["embedding"] for d in items]
@@ -0,0 +1,62 @@
1
+ """
2
+ Voyage AI embedder adapter (high-quality, cheaper than OpenAI).
3
+ Requires: VOYAGE_API_KEY
4
+
5
+ Usage:
6
+ from agentic_memory import AgentMemory
7
+ from agentic_memory.adapters import VoyageEmbedder
8
+
9
+ memory = AgentMemory(
10
+ embedder=VoyageEmbedder(api_key="voyage-...")
11
+ )
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ from urllib.request import Request, urlopen
18
+
19
+
20
+ class VoyageEmbedder:
21
+ def __init__(
22
+ self,
23
+ api_key: str,
24
+ model: str = "voyage-3-lite",
25
+ ) -> None:
26
+ self._api_key = api_key
27
+ self._model = model
28
+
29
+ def dimensions(self) -> int:
30
+ return 512 if "lite" in self._model else 1024
31
+
32
+ async def embed(self, text: str) -> list[float]:
33
+ result = self._call([text])
34
+ return result[0]
35
+
36
+ async def embed_batch(self, texts: list[str]) -> list[list[float]]:
37
+ if not texts:
38
+ return []
39
+ return self._call(texts)
40
+
41
+ def _call(self, input_texts: list[str]) -> list[list[float]]:
42
+ payload = json.dumps({
43
+ "model": self._model,
44
+ "input": input_texts,
45
+ }).encode("utf-8")
46
+
47
+ req = Request(
48
+ "https://api.voyageai.com/v1/embeddings",
49
+ data=payload,
50
+ headers={
51
+ "Content-Type": "application/json",
52
+ "Authorization": f"Bearer {self._api_key}",
53
+ },
54
+ method="POST",
55
+ )
56
+
57
+ with urlopen(req) as resp:
58
+ if resp.status != 200:
59
+ raise RuntimeError(f"Voyage AI embedding failed ({resp.status})")
60
+ data = json.loads(resp.read())
61
+
62
+ return [d["embedding"] for d in data["data"]]
@@ -0,0 +1,93 @@
1
+ """Typed decay engine - different memory types decay at different rates."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from datetime import datetime, timezone
7
+
8
+ from .types import (
9
+ MemoryEntry,
10
+ MemoryType,
11
+ ImportanceLevel,
12
+ DecayPolicy,
13
+ DecayConfig,
14
+ DecayTypeConfig,
15
+ )
16
+
17
+ DAY_MS = 24 * 60 * 60 * 1000
18
+
19
+ DEFAULT_DECAY_CONFIG = DecayConfig(
20
+ policies={
21
+ MemoryType.CONSTRAINT: DecayTypeConfig(policy=DecayPolicy.NONE),
22
+ MemoryType.PREFERENCE: DecayTypeConfig(policy=DecayPolicy.EXPONENTIAL, half_life=30 * DAY_MS),
23
+ MemoryType.FACT: DecayTypeConfig(policy=DecayPolicy.EXPONENTIAL, half_life=90 * DAY_MS),
24
+ MemoryType.TASK: DecayTypeConfig(policy=DecayPolicy.STEP, max_age=7 * DAY_MS),
25
+ MemoryType.EPISODIC: DecayTypeConfig(policy=DecayPolicy.EXPONENTIAL, half_life=14 * DAY_MS),
26
+ },
27
+ default_policy=DecayPolicy.EXPONENTIAL,
28
+ default_half_life=30 * DAY_MS,
29
+ )
30
+
31
+
32
+ class DecayEngine:
33
+ """
34
+ Typed decay engine.
35
+ Different memory types decay at different rates.
36
+ Hard constraints (allergies, legal requirements) never decay.
37
+ """
38
+
39
+ def __init__(self, config: DecayConfig | None = None) -> None:
40
+ if config is None:
41
+ self._config = DEFAULT_DECAY_CONFIG
42
+ else:
43
+ # Merge with defaults
44
+ merged_policies = {**DEFAULT_DECAY_CONFIG.policies, **config.policies}
45
+ self._config = DecayConfig(
46
+ policies=merged_policies,
47
+ default_policy=config.default_policy,
48
+ default_half_life=config.default_half_life,
49
+ )
50
+
51
+ def compute_decayed_confidence(self, entry: MemoryEntry) -> float:
52
+ """Compute effective confidence after temporal decay."""
53
+ if entry.importance == ImportanceLevel.HARD:
54
+ return entry.confidence
55
+
56
+ type_config = self._config.policies.get(entry.type)
57
+ policy = type_config.policy if type_config else self._config.default_policy
58
+
59
+ dt = datetime.fromisoformat(entry.updated_at)
60
+ if dt.tzinfo is None:
61
+ dt = dt.replace(tzinfo=timezone.utc)
62
+ age_ms = (datetime.now(timezone.utc) - dt).total_seconds() * 1000
63
+
64
+ if policy == DecayPolicy.NONE:
65
+ return entry.confidence
66
+
67
+ if policy == DecayPolicy.LINEAR:
68
+ rate = type_config.rate_per_day if type_config and type_config.rate_per_day else 0.01
69
+ age_days = age_ms / DAY_MS
70
+ return max(0.0, entry.confidence - rate * age_days)
71
+
72
+ if policy == DecayPolicy.EXPONENTIAL:
73
+ half_life = (
74
+ type_config.half_life
75
+ if type_config and type_config.half_life
76
+ else self._config.default_half_life
77
+ )
78
+ decay_factor = math.exp(-0.693 * age_ms / half_life)
79
+ return entry.confidence * decay_factor
80
+
81
+ if policy == DecayPolicy.STEP:
82
+ max_age = type_config.max_age if type_config and type_config.max_age else 7 * DAY_MS
83
+ return 0.0 if age_ms > max_age else entry.confidence
84
+
85
+ return entry.confidence
86
+
87
+ def filter_decayed(self, entries: list[MemoryEntry], threshold: float = 0.05) -> list[MemoryEntry]:
88
+ """Filter out memories that have decayed below threshold."""
89
+ return [e for e in entries if self.compute_decayed_confidence(e) >= threshold]
90
+
91
+ def get_expired(self, entries: list[MemoryEntry], threshold: float = 0.01) -> list[MemoryEntry]:
92
+ """Get entries that should be cleaned up."""
93
+ return [e for e in entries if self.compute_decayed_confidence(e) < threshold]
@@ -0,0 +1,243 @@
1
+ """AgentMemory - the main entry point."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Optional
6
+
7
+ from .types import (
8
+ MemoryEntry,
9
+ MemoryType,
10
+ ImportanceLevel,
11
+ RetrievalQuery,
12
+ RetrievalResult,
13
+ ConflictResult,
14
+ Checkpoint,
15
+ TaskNode,
16
+ DecayConfig,
17
+ )
18
+ from .utils import generate_id, now
19
+ from .store.local import LocalStore
20
+ from .retrieval.embedder import BuiltinEmbedder
21
+ from .retrieval.multi_signal import MultiSignalRetriever
22
+ from .retrieval.conflict import ConflictDetector
23
+ from .decay import DecayEngine
24
+
25
+
26
+ class AgentMemory:
27
+ """
28
+ Framework-agnostic memory layer for AI agents.
29
+
30
+ Example::
31
+
32
+ memory = AgentMemory()
33
+
34
+ await memory.store(
35
+ content="User prefers dark mode",
36
+ type=MemoryType.PREFERENCE,
37
+ scope="user:123",
38
+ )
39
+
40
+ results = await memory.retrieve(RetrievalQuery(query="UI preferences"))
41
+ conflicts = await memory.check_conflicts("User prefers light mode", "user:123")
42
+ """
43
+
44
+ def __init__(
45
+ self,
46
+ *,
47
+ store: Any = None,
48
+ embedder: Any = None,
49
+ decay: DecayConfig | None = None,
50
+ default_scope: str = "default",
51
+ max_checkpoints: int = 10,
52
+ ) -> None:
53
+ self._backend = store if store is not None else LocalStore()
54
+ self._embedder = embedder if embedder is not None else BuiltinEmbedder()
55
+ self._retriever = MultiSignalRetriever(self._backend, self._embedder)
56
+ self._conflict_detector = ConflictDetector(self._backend, self._embedder)
57
+ self._decay_engine = DecayEngine(decay)
58
+ self._checkpoints: dict[str, Checkpoint] = {}
59
+ self._default_scope = default_scope
60
+ self._max_checkpoints = max_checkpoints
61
+
62
+ # ─── Store ───
63
+
64
+ async def store(
65
+ self,
66
+ content: str,
67
+ *,
68
+ type: MemoryType = MemoryType.FACT,
69
+ scope: Optional[str] = None,
70
+ importance: ImportanceLevel = ImportanceLevel.SOFT,
71
+ confidence: float = 1.0,
72
+ metadata: Optional[dict[str, Any]] = None,
73
+ ) -> MemoryEntry:
74
+ """Store a new memory entry."""
75
+ if not content or not content.strip():
76
+ raise ValueError("Memory content cannot be empty")
77
+
78
+ # Train before embed so IDF stats include this document
79
+ if isinstance(self._embedder, BuiltinEmbedder):
80
+ self._embedder.train([content])
81
+
82
+ embedding = await self._embedder.embed(content)
83
+
84
+ entry = MemoryEntry(
85
+ id=generate_id(),
86
+ content=content,
87
+ type=type,
88
+ scope=scope or self._default_scope,
89
+ importance=importance,
90
+ confidence=confidence,
91
+ embedding=embedding,
92
+ metadata=metadata or {},
93
+ created_at=now(),
94
+ updated_at=now(),
95
+ version=1,
96
+ )
97
+
98
+ await self._backend.set(entry)
99
+ return entry
100
+
101
+ async def update(
102
+ self,
103
+ id: str,
104
+ *,
105
+ content: Optional[str] = None,
106
+ type: Optional[MemoryType] = None,
107
+ importance: Optional[ImportanceLevel] = None,
108
+ confidence: Optional[float] = None,
109
+ metadata: Optional[dict[str, Any]] = None,
110
+ ) -> Optional[MemoryEntry]:
111
+ """Update an existing memory entry."""
112
+ entry = await self._backend.get(id)
113
+ if entry is None:
114
+ return None
115
+
116
+ if content is not None:
117
+ if not content.strip():
118
+ raise ValueError("Memory content cannot be empty")
119
+ entry.content = content
120
+
121
+ if type is not None:
122
+ entry.type = type
123
+ if importance is not None:
124
+ entry.importance = importance
125
+ if confidence is not None:
126
+ entry.confidence = confidence
127
+ if metadata is not None:
128
+ entry.metadata = metadata
129
+
130
+ entry.updated_at = now()
131
+ entry.version += 1
132
+
133
+ # Re-embed if content changed
134
+ if content is not None:
135
+ if isinstance(self._embedder, BuiltinEmbedder):
136
+ self._embedder.train([content])
137
+ entry.embedding = await self._embedder.embed(content)
138
+
139
+ await self._backend.set(entry)
140
+ return entry
141
+
142
+ async def get(self, id: str) -> Optional[MemoryEntry]:
143
+ """Get a memory by ID."""
144
+ return await self._backend.get(id)
145
+
146
+ async def delete(self, id: str) -> bool:
147
+ """Delete a memory by ID."""
148
+ return await self._backend.delete(id)
149
+
150
+ async def get_all(self, scope: Optional[str] = None) -> list[MemoryEntry]:
151
+ """Get all memories, optionally filtered by scope."""
152
+ return await self._backend.get_all(scope)
153
+
154
+ async def clear(self, scope: Optional[str] = None) -> None:
155
+ """Clear all memories, optionally for a specific scope."""
156
+ return await self._backend.clear(scope)
157
+
158
+ # ─── Retrieve ───
159
+
160
+ async def retrieve(self, query: RetrievalQuery) -> list[RetrievalResult]:
161
+ """Multi-signal retrieval."""
162
+ return await self._retriever.retrieve(query)
163
+
164
+ # ─── Conflict Detection ───
165
+
166
+ async def check_conflicts(
167
+ self, content: str, scope: Optional[str] = None
168
+ ) -> list[ConflictResult]:
169
+ """Check if content conflicts with stored memories."""
170
+ return await self._conflict_detector.check(
171
+ content, scope or self._default_scope
172
+ )
173
+
174
+ # ─── Decay ───
175
+
176
+ def get_decayed_confidence(self, entry: MemoryEntry) -> float:
177
+ """Get the current effective confidence after decay."""
178
+ return self._decay_engine.compute_decayed_confidence(entry)
179
+
180
+ async def cleanup(
181
+ self, scope: Optional[str] = None, threshold: float = 0.01
182
+ ) -> int:
183
+ """Clean up expired memories. Returns count deleted."""
184
+ entries = await self._backend.get_all(scope)
185
+ expired = self._decay_engine.get_expired(entries, threshold)
186
+ for entry in expired:
187
+ await self._backend.delete(entry.id)
188
+ return len(expired)
189
+
190
+ # ─── Checkpointing ───
191
+
192
+ async def checkpoint(
193
+ self,
194
+ *,
195
+ task_graph: list[TaskNode],
196
+ summary: str,
197
+ tool_outputs: Optional[dict[str, Any]] = None,
198
+ active_memory_ids: Optional[list[str]] = None,
199
+ ) -> Checkpoint:
200
+ """Create a checkpoint of current task state."""
201
+ cp = Checkpoint(
202
+ id=generate_id(),
203
+ task_graph=task_graph,
204
+ summary=summary,
205
+ tool_outputs=tool_outputs or {},
206
+ active_memory_ids=active_memory_ids or [],
207
+ created_at=now(),
208
+ )
209
+
210
+ self._checkpoints[cp.id] = cp
211
+
212
+ # Enforce max checkpoints (LRU eviction)
213
+ if len(self._checkpoints) > self._max_checkpoints:
214
+ oldest_key = next(iter(self._checkpoints))
215
+ del self._checkpoints[oldest_key]
216
+
217
+ return cp
218
+
219
+ async def rehydrate(
220
+ self, checkpoint_id: str
221
+ ) -> Optional[dict[str, Any]]:
222
+ """Rehydrate from a checkpoint. Returns checkpoint + memories."""
223
+ cp = self._checkpoints.get(checkpoint_id)
224
+ if cp is None:
225
+ return None
226
+
227
+ memories: list[MemoryEntry] = []
228
+ for mid in cp.active_memory_ids:
229
+ entry = await self._backend.get(mid)
230
+ if entry is not None:
231
+ memories.append(entry)
232
+
233
+ return {"checkpoint": cp, "memories": memories}
234
+
235
+ def get_latest_checkpoint(self) -> Optional[Checkpoint]:
236
+ """Get the latest checkpoint."""
237
+ if not self._checkpoints:
238
+ return None
239
+ return list(self._checkpoints.values())[-1]
240
+
241
+ def list_checkpoints(self) -> list[Checkpoint]:
242
+ """List all checkpoints."""
243
+ return list(self._checkpoints.values())
@@ -0,0 +1,5 @@
1
+ from .embedder import BuiltinEmbedder
2
+ from .multi_signal import MultiSignalRetriever
3
+ from .conflict import ConflictDetector
4
+
5
+ __all__ = ["BuiltinEmbedder", "MultiSignalRetriever", "ConflictDetector"]
@@ -0,0 +1,145 @@
1
+ """Conflict detector for finding contradictions in stored memories."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import Any, Optional
7
+
8
+ from ..types import MemoryEntry, ConflictResult, ConflictAction, ImportanceLevel
9
+ from ..utils import cosine_similarity
10
+
11
+ NEGATION_PATTERNS = [
12
+ re.compile(p, re.IGNORECASE) for p in [
13
+ r"\bnot\b", r"\bnever\b", r"\bno\b", r"\bdon't\b", r"\bdoesn't\b",
14
+ r"\bwon't\b", r"\bcan't\b", r"\bhate\b", r"\bdislike\b", r"\bavoid\b",
15
+ r"\bstop\b", r"\bquit\b", r"\bremove\b", r"\bdelete\b",
16
+ r"\binstead of\b", r"\brather than\b", r"\bno longer\b",
17
+ ]
18
+ ]
19
+
20
+ CHANGE_PATTERNS = [
21
+ re.compile(p, re.IGNORECASE) for p in [
22
+ r"\bnow\b", r"\bactually\b", r"\bchanged?\b", r"\bswitch",
23
+ r"\bprefer\b", r"\bwant\b", r"\bused to\b", r"\banymore\b",
24
+ ]
25
+ ]
26
+
27
+ ANTONYM_PAIRS: list[tuple[re.Pattern, re.Pattern]] = [
28
+ (re.compile(a, re.IGNORECASE), re.compile(b, re.IGNORECASE))
29
+ for a, b in [
30
+ (r"\blike\b", r"\bdislike\b"),
31
+ (r"\blove\b", r"\bhate\b"),
32
+ (r"\byes\b", r"\bno\b"),
33
+ (r"\btrue\b", r"\bfalse\b"),
34
+ (r"\bvegetarian\b", r"\bmeat\b"),
35
+ (r"\bvegan\b", r"\bmeat\b|\bdairy\b"),
36
+ (r"\bmorning\b", r"\bevening\b|\bnight\b"),
37
+ (r"\blight\b", r"\bdark\b"),
38
+ (r"\bhot\b", r"\bcold\b"),
39
+ (r"\bfast\b", r"\bslow\b"),
40
+ (r"\benable\b", r"\bdisable\b"),
41
+ (r"\ballow\b", r"\bblock\b|\bdeny\b"),
42
+ (r"\baccept\b", r"\breject\b"),
43
+ ]
44
+ ]
45
+
46
+
47
+ class ConflictDetector:
48
+ """
49
+ Conflict detector.
50
+ Compares new content against stored memories to find contradictions.
51
+ Uses semantic similarity + negation detection.
52
+ """
53
+
54
+ def __init__(
55
+ self,
56
+ store: Any,
57
+ embedder: Any,
58
+ topic_threshold: float = 0.6,
59
+ ) -> None:
60
+ self._store = store
61
+ self._embedder = embedder
62
+ self._topic_threshold = topic_threshold
63
+
64
+ async def check(self, content: str, scope: Optional[str] = None) -> list[ConflictResult]:
65
+ entries = await self._store.get_all(scope)
66
+ if not entries:
67
+ return []
68
+
69
+ content_embedding = await self._embedder.embed(content)
70
+ conflicts: list[ConflictResult] = []
71
+
72
+ for entry in entries:
73
+ entry_embedding = entry.embedding or await self._embedder.embed(entry.content)
74
+ similarity = cosine_similarity(content_embedding, entry_embedding)
75
+
76
+ if similarity < self._topic_threshold:
77
+ continue
78
+
79
+ contradiction_score = self._score_contradiction(content, entry.content)
80
+ if contradiction_score <= 0:
81
+ continue
82
+
83
+ confidence = min(1.0, similarity * 0.5 + contradiction_score * 0.5)
84
+
85
+ conflicts.append(ConflictResult(
86
+ incoming=content,
87
+ stored=entry,
88
+ confidence=confidence,
89
+ action=self._suggest_action(entry, confidence),
90
+ reason=self._generate_reason(content, entry, contradiction_score),
91
+ ))
92
+
93
+ conflicts.sort(key=lambda c: c.confidence, reverse=True)
94
+ return conflicts
95
+
96
+ def _score_contradiction(self, incoming: str, stored: str) -> float:
97
+ score = 0.0
98
+
99
+ incoming_negated = any(p.search(incoming) for p in NEGATION_PATTERNS)
100
+ stored_negated = any(p.search(stored) for p in NEGATION_PATTERNS)
101
+
102
+ if incoming_negated != stored_negated:
103
+ score += 0.5
104
+
105
+ if any(p.search(incoming) for p in CHANGE_PATTERNS):
106
+ score += 0.3
107
+
108
+ score += self._check_antonyms(incoming, stored) * 0.4
109
+
110
+ return min(1.0, score)
111
+
112
+ @staticmethod
113
+ def _check_antonyms(a: str, b: str) -> float:
114
+ matches = 0
115
+ for word1, word2 in ANTONYM_PAIRS:
116
+ if (word1.search(a) and word2.search(b)) or (word2.search(a) and word1.search(b)):
117
+ matches += 1
118
+ return min(1.0, matches * 0.5)
119
+
120
+ @staticmethod
121
+ def _suggest_action(stored: MemoryEntry, confidence: float) -> ConflictAction:
122
+ if stored.importance == ImportanceLevel.HARD:
123
+ return ConflictAction.CLARIFY
124
+ if confidence > 0.7 and stored.importance == ImportanceLevel.SOFT:
125
+ return ConflictAction.OVERRIDE
126
+ if stored.importance == ImportanceLevel.EPHEMERAL:
127
+ return ConflictAction.OVERRIDE
128
+ return ConflictAction.CLARIFY
129
+
130
+ @staticmethod
131
+ def _generate_reason(incoming: str, stored: MemoryEntry, contradiction_score: float) -> str:
132
+ if stored.importance == ImportanceLevel.HARD:
133
+ return (
134
+ f'Conflicts with a hard constraint: "{stored.content}". '
135
+ "This memory was marked as non-negotiable - please confirm the change."
136
+ )
137
+ if contradiction_score > 0.7:
138
+ return (
139
+ f'Directly contradicts stored memory: "{stored.content}". '
140
+ "The new statement appears to reverse a previous preference."
141
+ )
142
+ return (
143
+ f'Potentially conflicts with: "{stored.content}". '
144
+ "The statements may be inconsistent."
145
+ )