alma-memory 0.4.0__py3-none-any.whl → 0.5.1__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.
- alma/__init__.py +121 -45
- alma/confidence/__init__.py +1 -1
- alma/confidence/engine.py +92 -58
- alma/confidence/types.py +34 -14
- alma/config/loader.py +3 -2
- alma/consolidation/__init__.py +23 -0
- alma/consolidation/engine.py +678 -0
- alma/consolidation/prompts.py +84 -0
- alma/core.py +136 -28
- alma/domains/__init__.py +6 -6
- alma/domains/factory.py +12 -9
- alma/domains/schemas.py +17 -3
- alma/domains/types.py +8 -4
- alma/events/__init__.py +75 -0
- alma/events/emitter.py +284 -0
- alma/events/storage_mixin.py +246 -0
- alma/events/types.py +126 -0
- alma/events/webhook.py +425 -0
- alma/exceptions.py +49 -0
- alma/extraction/__init__.py +31 -0
- alma/extraction/auto_learner.py +265 -0
- alma/extraction/extractor.py +420 -0
- alma/graph/__init__.py +106 -0
- alma/graph/backends/__init__.py +32 -0
- alma/graph/backends/kuzu.py +624 -0
- alma/graph/backends/memgraph.py +432 -0
- alma/graph/backends/memory.py +236 -0
- alma/graph/backends/neo4j.py +417 -0
- alma/graph/base.py +159 -0
- alma/graph/extraction.py +198 -0
- alma/graph/store.py +860 -0
- alma/harness/__init__.py +4 -4
- alma/harness/base.py +18 -9
- alma/harness/domains.py +27 -11
- alma/initializer/__init__.py +1 -1
- alma/initializer/initializer.py +51 -43
- alma/initializer/types.py +25 -17
- alma/integration/__init__.py +9 -9
- alma/integration/claude_agents.py +32 -20
- alma/integration/helena.py +32 -22
- alma/integration/victor.py +57 -33
- alma/learning/__init__.py +27 -27
- alma/learning/forgetting.py +198 -148
- alma/learning/heuristic_extractor.py +40 -24
- alma/learning/protocols.py +65 -17
- alma/learning/validation.py +7 -2
- alma/mcp/__init__.py +4 -4
- alma/mcp/__main__.py +2 -1
- alma/mcp/resources.py +17 -16
- alma/mcp/server.py +102 -44
- alma/mcp/tools.py +180 -45
- alma/observability/__init__.py +84 -0
- alma/observability/config.py +302 -0
- alma/observability/logging.py +424 -0
- alma/observability/metrics.py +583 -0
- alma/observability/tracing.py +440 -0
- alma/progress/__init__.py +3 -3
- alma/progress/tracker.py +26 -20
- alma/progress/types.py +8 -12
- alma/py.typed +0 -0
- alma/retrieval/__init__.py +11 -11
- alma/retrieval/cache.py +20 -21
- alma/retrieval/embeddings.py +4 -4
- alma/retrieval/engine.py +179 -39
- alma/retrieval/scoring.py +73 -63
- alma/session/__init__.py +2 -2
- alma/session/manager.py +5 -5
- alma/session/types.py +5 -4
- alma/storage/__init__.py +70 -0
- alma/storage/azure_cosmos.py +414 -133
- alma/storage/base.py +215 -4
- alma/storage/chroma.py +1443 -0
- alma/storage/constants.py +103 -0
- alma/storage/file_based.py +59 -28
- alma/storage/migrations/__init__.py +21 -0
- alma/storage/migrations/base.py +321 -0
- alma/storage/migrations/runner.py +323 -0
- alma/storage/migrations/version_stores.py +337 -0
- alma/storage/migrations/versions/__init__.py +11 -0
- alma/storage/migrations/versions/v1_0_0.py +373 -0
- alma/storage/pinecone.py +1080 -0
- alma/storage/postgresql.py +1559 -0
- alma/storage/qdrant.py +1306 -0
- alma/storage/sqlite_local.py +504 -60
- alma/testing/__init__.py +46 -0
- alma/testing/factories.py +301 -0
- alma/testing/mocks.py +389 -0
- alma/types.py +62 -14
- alma_memory-0.5.1.dist-info/METADATA +939 -0
- alma_memory-0.5.1.dist-info/RECORD +93 -0
- {alma_memory-0.4.0.dist-info → alma_memory-0.5.1.dist-info}/WHEEL +1 -1
- alma_memory-0.4.0.dist-info/METADATA +0 -488
- alma_memory-0.4.0.dist-info/RECORD +0 -52
- {alma_memory-0.4.0.dist-info → alma_memory-0.5.1.dist-info}/top_level.txt +0 -0
alma/testing/mocks.py
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ALMA Testing Mocks.
|
|
3
|
+
|
|
4
|
+
Provides mock implementations of ALMA interfaces for testing.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from typing import Any, Dict, List, Optional
|
|
9
|
+
|
|
10
|
+
from alma.retrieval.embeddings import MockEmbedder
|
|
11
|
+
from alma.storage.base import StorageBackend
|
|
12
|
+
from alma.types import (
|
|
13
|
+
AntiPattern,
|
|
14
|
+
DomainKnowledge,
|
|
15
|
+
Heuristic,
|
|
16
|
+
Outcome,
|
|
17
|
+
UserPreference,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# Re-export MockEmbedder for convenience
|
|
21
|
+
__all__ = ["MockStorage", "MockEmbedder"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class MockStorage(StorageBackend):
|
|
25
|
+
"""
|
|
26
|
+
In-memory mock storage backend for testing.
|
|
27
|
+
|
|
28
|
+
Stores all memory types in dictionaries for fast, isolated testing.
|
|
29
|
+
Does not persist data between test runs.
|
|
30
|
+
|
|
31
|
+
Example:
|
|
32
|
+
>>> from alma.testing import MockStorage, create_test_heuristic
|
|
33
|
+
>>> storage = MockStorage()
|
|
34
|
+
>>> heuristic = create_test_heuristic(agent="test-agent")
|
|
35
|
+
>>> storage.save_heuristic(heuristic)
|
|
36
|
+
>>> found = storage.get_heuristics("test-project", agent="test-agent")
|
|
37
|
+
>>> assert len(found) == 1
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self):
|
|
41
|
+
"""Initialize empty in-memory storage."""
|
|
42
|
+
self._heuristics: Dict[str, Heuristic] = {}
|
|
43
|
+
self._outcomes: Dict[str, Outcome] = {}
|
|
44
|
+
self._preferences: Dict[str, UserPreference] = {}
|
|
45
|
+
self._domain_knowledge: Dict[str, DomainKnowledge] = {}
|
|
46
|
+
self._anti_patterns: Dict[str, AntiPattern] = {}
|
|
47
|
+
|
|
48
|
+
# ==================== WRITE OPERATIONS ====================
|
|
49
|
+
|
|
50
|
+
def save_heuristic(self, heuristic: Heuristic) -> str:
|
|
51
|
+
"""Save a heuristic, return its ID."""
|
|
52
|
+
self._heuristics[heuristic.id] = heuristic
|
|
53
|
+
return heuristic.id
|
|
54
|
+
|
|
55
|
+
def save_outcome(self, outcome: Outcome) -> str:
|
|
56
|
+
"""Save an outcome, return its ID."""
|
|
57
|
+
self._outcomes[outcome.id] = outcome
|
|
58
|
+
return outcome.id
|
|
59
|
+
|
|
60
|
+
def save_user_preference(self, preference: UserPreference) -> str:
|
|
61
|
+
"""Save a user preference, return its ID."""
|
|
62
|
+
self._preferences[preference.id] = preference
|
|
63
|
+
return preference.id
|
|
64
|
+
|
|
65
|
+
def save_domain_knowledge(self, knowledge: DomainKnowledge) -> str:
|
|
66
|
+
"""Save domain knowledge, return its ID."""
|
|
67
|
+
self._domain_knowledge[knowledge.id] = knowledge
|
|
68
|
+
return knowledge.id
|
|
69
|
+
|
|
70
|
+
def save_anti_pattern(self, anti_pattern: AntiPattern) -> str:
|
|
71
|
+
"""Save an anti-pattern, return its ID."""
|
|
72
|
+
self._anti_patterns[anti_pattern.id] = anti_pattern
|
|
73
|
+
return anti_pattern.id
|
|
74
|
+
|
|
75
|
+
# ==================== READ OPERATIONS ====================
|
|
76
|
+
|
|
77
|
+
def get_heuristics(
|
|
78
|
+
self,
|
|
79
|
+
project_id: str,
|
|
80
|
+
agent: Optional[str] = None,
|
|
81
|
+
embedding: Optional[List[float]] = None,
|
|
82
|
+
top_k: int = 5,
|
|
83
|
+
min_confidence: float = 0.0,
|
|
84
|
+
) -> List[Heuristic]:
|
|
85
|
+
"""Get heuristics, optionally filtered by agent."""
|
|
86
|
+
results = []
|
|
87
|
+
for h in self._heuristics.values():
|
|
88
|
+
if h.project_id != project_id:
|
|
89
|
+
continue
|
|
90
|
+
if agent and h.agent != agent:
|
|
91
|
+
continue
|
|
92
|
+
if h.confidence < min_confidence:
|
|
93
|
+
continue
|
|
94
|
+
results.append(h)
|
|
95
|
+
|
|
96
|
+
# Sort by confidence descending
|
|
97
|
+
results.sort(key=lambda x: x.confidence, reverse=True)
|
|
98
|
+
return results[:top_k]
|
|
99
|
+
|
|
100
|
+
def get_outcomes(
|
|
101
|
+
self,
|
|
102
|
+
project_id: str,
|
|
103
|
+
agent: Optional[str] = None,
|
|
104
|
+
task_type: Optional[str] = None,
|
|
105
|
+
embedding: Optional[List[float]] = None,
|
|
106
|
+
top_k: int = 5,
|
|
107
|
+
success_only: bool = False,
|
|
108
|
+
) -> List[Outcome]:
|
|
109
|
+
"""Get outcomes, optionally filtered."""
|
|
110
|
+
results = []
|
|
111
|
+
for o in self._outcomes.values():
|
|
112
|
+
if o.project_id != project_id:
|
|
113
|
+
continue
|
|
114
|
+
if agent and o.agent != agent:
|
|
115
|
+
continue
|
|
116
|
+
if task_type and o.task_type != task_type:
|
|
117
|
+
continue
|
|
118
|
+
if success_only and not o.success:
|
|
119
|
+
continue
|
|
120
|
+
results.append(o)
|
|
121
|
+
|
|
122
|
+
# Sort by timestamp descending (most recent first)
|
|
123
|
+
results.sort(key=lambda x: x.timestamp, reverse=True)
|
|
124
|
+
return results[:top_k]
|
|
125
|
+
|
|
126
|
+
def get_user_preferences(
|
|
127
|
+
self,
|
|
128
|
+
user_id: str,
|
|
129
|
+
category: Optional[str] = None,
|
|
130
|
+
) -> List[UserPreference]:
|
|
131
|
+
"""Get user preferences."""
|
|
132
|
+
results = []
|
|
133
|
+
for p in self._preferences.values():
|
|
134
|
+
if p.user_id != user_id:
|
|
135
|
+
continue
|
|
136
|
+
if category and p.category != category:
|
|
137
|
+
continue
|
|
138
|
+
results.append(p)
|
|
139
|
+
return results
|
|
140
|
+
|
|
141
|
+
def get_domain_knowledge(
|
|
142
|
+
self,
|
|
143
|
+
project_id: str,
|
|
144
|
+
agent: Optional[str] = None,
|
|
145
|
+
domain: Optional[str] = None,
|
|
146
|
+
embedding: Optional[List[float]] = None,
|
|
147
|
+
top_k: int = 5,
|
|
148
|
+
) -> List[DomainKnowledge]:
|
|
149
|
+
"""Get domain knowledge."""
|
|
150
|
+
results = []
|
|
151
|
+
for dk in self._domain_knowledge.values():
|
|
152
|
+
if dk.project_id != project_id:
|
|
153
|
+
continue
|
|
154
|
+
if agent and dk.agent != agent:
|
|
155
|
+
continue
|
|
156
|
+
if domain and dk.domain != domain:
|
|
157
|
+
continue
|
|
158
|
+
results.append(dk)
|
|
159
|
+
|
|
160
|
+
# Sort by confidence descending
|
|
161
|
+
results.sort(key=lambda x: x.confidence, reverse=True)
|
|
162
|
+
return results[:top_k]
|
|
163
|
+
|
|
164
|
+
def get_anti_patterns(
|
|
165
|
+
self,
|
|
166
|
+
project_id: str,
|
|
167
|
+
agent: Optional[str] = None,
|
|
168
|
+
embedding: Optional[List[float]] = None,
|
|
169
|
+
top_k: int = 5,
|
|
170
|
+
) -> List[AntiPattern]:
|
|
171
|
+
"""Get anti-patterns."""
|
|
172
|
+
results = []
|
|
173
|
+
for ap in self._anti_patterns.values():
|
|
174
|
+
if ap.project_id != project_id:
|
|
175
|
+
continue
|
|
176
|
+
if agent and ap.agent != agent:
|
|
177
|
+
continue
|
|
178
|
+
results.append(ap)
|
|
179
|
+
|
|
180
|
+
# Sort by occurrence_count descending
|
|
181
|
+
results.sort(key=lambda x: x.occurrence_count, reverse=True)
|
|
182
|
+
return results[:top_k]
|
|
183
|
+
|
|
184
|
+
# ==================== UPDATE OPERATIONS ====================
|
|
185
|
+
|
|
186
|
+
def update_heuristic(
|
|
187
|
+
self,
|
|
188
|
+
heuristic_id: str,
|
|
189
|
+
updates: Dict[str, Any],
|
|
190
|
+
) -> bool:
|
|
191
|
+
"""Update a heuristic's fields."""
|
|
192
|
+
if heuristic_id not in self._heuristics:
|
|
193
|
+
return False
|
|
194
|
+
|
|
195
|
+
h = self._heuristics[heuristic_id]
|
|
196
|
+
for key, value in updates.items():
|
|
197
|
+
if hasattr(h, key):
|
|
198
|
+
setattr(h, key, value)
|
|
199
|
+
return True
|
|
200
|
+
|
|
201
|
+
def increment_heuristic_occurrence(
|
|
202
|
+
self,
|
|
203
|
+
heuristic_id: str,
|
|
204
|
+
success: bool,
|
|
205
|
+
) -> bool:
|
|
206
|
+
"""Increment heuristic occurrence count."""
|
|
207
|
+
if heuristic_id not in self._heuristics:
|
|
208
|
+
return False
|
|
209
|
+
|
|
210
|
+
h = self._heuristics[heuristic_id]
|
|
211
|
+
h.occurrence_count += 1
|
|
212
|
+
if success:
|
|
213
|
+
h.success_count += 1
|
|
214
|
+
h.last_validated = datetime.now(timezone.utc)
|
|
215
|
+
return True
|
|
216
|
+
|
|
217
|
+
def update_heuristic_confidence(
|
|
218
|
+
self,
|
|
219
|
+
heuristic_id: str,
|
|
220
|
+
new_confidence: float,
|
|
221
|
+
) -> bool:
|
|
222
|
+
"""Update a heuristic's confidence value."""
|
|
223
|
+
if heuristic_id not in self._heuristics:
|
|
224
|
+
return False
|
|
225
|
+
|
|
226
|
+
self._heuristics[heuristic_id].confidence = new_confidence
|
|
227
|
+
return True
|
|
228
|
+
|
|
229
|
+
def update_knowledge_confidence(
|
|
230
|
+
self,
|
|
231
|
+
knowledge_id: str,
|
|
232
|
+
new_confidence: float,
|
|
233
|
+
) -> bool:
|
|
234
|
+
"""Update domain knowledge confidence value."""
|
|
235
|
+
if knowledge_id not in self._domain_knowledge:
|
|
236
|
+
return False
|
|
237
|
+
|
|
238
|
+
self._domain_knowledge[knowledge_id].confidence = new_confidence
|
|
239
|
+
return True
|
|
240
|
+
|
|
241
|
+
# ==================== DELETE OPERATIONS ====================
|
|
242
|
+
|
|
243
|
+
def delete_heuristic(self, heuristic_id: str) -> bool:
|
|
244
|
+
"""Delete a heuristic by ID."""
|
|
245
|
+
if heuristic_id not in self._heuristics:
|
|
246
|
+
return False
|
|
247
|
+
del self._heuristics[heuristic_id]
|
|
248
|
+
return True
|
|
249
|
+
|
|
250
|
+
def delete_outcome(self, outcome_id: str) -> bool:
|
|
251
|
+
"""Delete an outcome by ID."""
|
|
252
|
+
if outcome_id not in self._outcomes:
|
|
253
|
+
return False
|
|
254
|
+
del self._outcomes[outcome_id]
|
|
255
|
+
return True
|
|
256
|
+
|
|
257
|
+
def delete_domain_knowledge(self, knowledge_id: str) -> bool:
|
|
258
|
+
"""Delete domain knowledge by ID."""
|
|
259
|
+
if knowledge_id not in self._domain_knowledge:
|
|
260
|
+
return False
|
|
261
|
+
del self._domain_knowledge[knowledge_id]
|
|
262
|
+
return True
|
|
263
|
+
|
|
264
|
+
def delete_anti_pattern(self, anti_pattern_id: str) -> bool:
|
|
265
|
+
"""Delete an anti-pattern by ID."""
|
|
266
|
+
if anti_pattern_id not in self._anti_patterns:
|
|
267
|
+
return False
|
|
268
|
+
del self._anti_patterns[anti_pattern_id]
|
|
269
|
+
return True
|
|
270
|
+
|
|
271
|
+
def delete_outcomes_older_than(
|
|
272
|
+
self,
|
|
273
|
+
project_id: str,
|
|
274
|
+
older_than: datetime,
|
|
275
|
+
agent: Optional[str] = None,
|
|
276
|
+
) -> int:
|
|
277
|
+
"""Delete old outcomes."""
|
|
278
|
+
to_delete = []
|
|
279
|
+
for oid, o in self._outcomes.items():
|
|
280
|
+
if o.project_id != project_id:
|
|
281
|
+
continue
|
|
282
|
+
if agent and o.agent != agent:
|
|
283
|
+
continue
|
|
284
|
+
if o.timestamp < older_than:
|
|
285
|
+
to_delete.append(oid)
|
|
286
|
+
|
|
287
|
+
for oid in to_delete:
|
|
288
|
+
del self._outcomes[oid]
|
|
289
|
+
return len(to_delete)
|
|
290
|
+
|
|
291
|
+
def delete_low_confidence_heuristics(
|
|
292
|
+
self,
|
|
293
|
+
project_id: str,
|
|
294
|
+
below_confidence: float,
|
|
295
|
+
agent: Optional[str] = None,
|
|
296
|
+
) -> int:
|
|
297
|
+
"""Delete low-confidence heuristics."""
|
|
298
|
+
to_delete = []
|
|
299
|
+
for hid, h in self._heuristics.items():
|
|
300
|
+
if h.project_id != project_id:
|
|
301
|
+
continue
|
|
302
|
+
if agent and h.agent != agent:
|
|
303
|
+
continue
|
|
304
|
+
if h.confidence < below_confidence:
|
|
305
|
+
to_delete.append(hid)
|
|
306
|
+
|
|
307
|
+
for hid in to_delete:
|
|
308
|
+
del self._heuristics[hid]
|
|
309
|
+
return len(to_delete)
|
|
310
|
+
|
|
311
|
+
# ==================== STATS ====================
|
|
312
|
+
|
|
313
|
+
def get_stats(
|
|
314
|
+
self,
|
|
315
|
+
project_id: str,
|
|
316
|
+
agent: Optional[str] = None,
|
|
317
|
+
) -> Dict[str, Any]:
|
|
318
|
+
"""Get memory statistics."""
|
|
319
|
+
stats = {
|
|
320
|
+
"heuristics": 0,
|
|
321
|
+
"outcomes": 0,
|
|
322
|
+
"preferences": 0,
|
|
323
|
+
"domain_knowledge": 0,
|
|
324
|
+
"anti_patterns": 0,
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
for h in self._heuristics.values():
|
|
328
|
+
if h.project_id == project_id and (not agent or h.agent == agent):
|
|
329
|
+
stats["heuristics"] += 1
|
|
330
|
+
|
|
331
|
+
for o in self._outcomes.values():
|
|
332
|
+
if o.project_id == project_id and (not agent or o.agent == agent):
|
|
333
|
+
stats["outcomes"] += 1
|
|
334
|
+
|
|
335
|
+
for dk in self._domain_knowledge.values():
|
|
336
|
+
if dk.project_id == project_id and (not agent or dk.agent == agent):
|
|
337
|
+
stats["domain_knowledge"] += 1
|
|
338
|
+
|
|
339
|
+
for ap in self._anti_patterns.values():
|
|
340
|
+
if ap.project_id == project_id and (not agent or ap.agent == agent):
|
|
341
|
+
stats["anti_patterns"] += 1
|
|
342
|
+
|
|
343
|
+
# Note: preferences are user-scoped, not project-scoped
|
|
344
|
+
stats["preferences"] = len(self._preferences)
|
|
345
|
+
|
|
346
|
+
stats["total_count"] = sum(stats.values())
|
|
347
|
+
return stats
|
|
348
|
+
|
|
349
|
+
# ==================== UTILITY ====================
|
|
350
|
+
|
|
351
|
+
@classmethod
|
|
352
|
+
def from_config(cls, config: Dict[str, Any]) -> "MockStorage":
|
|
353
|
+
"""Create instance from configuration dict (ignores config for mock)."""
|
|
354
|
+
return cls()
|
|
355
|
+
|
|
356
|
+
# ==================== MOCK-SPECIFIC METHODS ====================
|
|
357
|
+
|
|
358
|
+
def clear(self) -> None:
|
|
359
|
+
"""Clear all stored data. Useful for test cleanup."""
|
|
360
|
+
self._heuristics.clear()
|
|
361
|
+
self._outcomes.clear()
|
|
362
|
+
self._preferences.clear()
|
|
363
|
+
self._domain_knowledge.clear()
|
|
364
|
+
self._anti_patterns.clear()
|
|
365
|
+
|
|
366
|
+
@property
|
|
367
|
+
def heuristic_count(self) -> int:
|
|
368
|
+
"""Get total number of stored heuristics."""
|
|
369
|
+
return len(self._heuristics)
|
|
370
|
+
|
|
371
|
+
@property
|
|
372
|
+
def outcome_count(self) -> int:
|
|
373
|
+
"""Get total number of stored outcomes."""
|
|
374
|
+
return len(self._outcomes)
|
|
375
|
+
|
|
376
|
+
@property
|
|
377
|
+
def preference_count(self) -> int:
|
|
378
|
+
"""Get total number of stored preferences."""
|
|
379
|
+
return len(self._preferences)
|
|
380
|
+
|
|
381
|
+
@property
|
|
382
|
+
def knowledge_count(self) -> int:
|
|
383
|
+
"""Get total number of stored domain knowledge items."""
|
|
384
|
+
return len(self._domain_knowledge)
|
|
385
|
+
|
|
386
|
+
@property
|
|
387
|
+
def anti_pattern_count(self) -> int:
|
|
388
|
+
"""Get total number of stored anti-patterns."""
|
|
389
|
+
return len(self._anti_patterns)
|
alma/types.py
CHANGED
|
@@ -5,13 +5,14 @@ Defines the core data structures for all memory types.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from dataclasses import dataclass, field
|
|
8
|
-
from datetime import datetime
|
|
9
|
-
from typing import Optional, List, Dict, Any
|
|
8
|
+
from datetime import datetime, timezone
|
|
10
9
|
from enum import Enum
|
|
10
|
+
from typing import Any, Dict, List, Optional
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class MemoryType(Enum):
|
|
14
14
|
"""Categories of memory that agents can store and retrieve."""
|
|
15
|
+
|
|
15
16
|
HEURISTIC = "heuristic"
|
|
16
17
|
OUTCOME = "outcome"
|
|
17
18
|
USER_PREFERENCE = "user_preference"
|
|
@@ -22,13 +23,21 @@ class MemoryType(Enum):
|
|
|
22
23
|
@dataclass
|
|
23
24
|
class MemoryScope:
|
|
24
25
|
"""
|
|
25
|
-
Defines what an agent is allowed to learn.
|
|
26
|
+
Defines what an agent is allowed to learn and share.
|
|
26
27
|
|
|
27
28
|
Prevents scope creep by explicitly listing allowed and forbidden domains.
|
|
29
|
+
Supports multi-agent memory sharing through share_with and inherit_from.
|
|
28
30
|
"""
|
|
31
|
+
|
|
29
32
|
agent_name: str
|
|
30
33
|
can_learn: List[str]
|
|
31
34
|
cannot_learn: List[str]
|
|
35
|
+
share_with: List[str] = field(
|
|
36
|
+
default_factory=list
|
|
37
|
+
) # Agents that can read this agent's memories
|
|
38
|
+
inherit_from: List[str] = field(
|
|
39
|
+
default_factory=list
|
|
40
|
+
) # Agents whose memories this agent can read
|
|
32
41
|
min_occurrences_for_heuristic: int = 3
|
|
33
42
|
|
|
34
43
|
def is_allowed(self, domain: str) -> bool:
|
|
@@ -39,6 +48,39 @@ class MemoryScope:
|
|
|
39
48
|
return True
|
|
40
49
|
return domain in self.can_learn
|
|
41
50
|
|
|
51
|
+
def get_readable_agents(self) -> List[str]:
|
|
52
|
+
"""
|
|
53
|
+
Get list of agents whose memories this agent can read.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
List containing this agent's name plus all inherited agents.
|
|
57
|
+
"""
|
|
58
|
+
return [self.agent_name] + list(self.inherit_from)
|
|
59
|
+
|
|
60
|
+
def can_read_from(self, other_agent: str) -> bool:
|
|
61
|
+
"""
|
|
62
|
+
Check if this agent can read memories from another agent.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
other_agent: Name of the agent to check
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
True if this agent can read from other_agent
|
|
69
|
+
"""
|
|
70
|
+
return other_agent == self.agent_name or other_agent in self.inherit_from
|
|
71
|
+
|
|
72
|
+
def shares_with(self, other_agent: str) -> bool:
|
|
73
|
+
"""
|
|
74
|
+
Check if this agent shares memories with another agent.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
other_agent: Name of the agent to check
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
True if this agent shares with other_agent
|
|
81
|
+
"""
|
|
82
|
+
return other_agent in self.share_with
|
|
83
|
+
|
|
42
84
|
|
|
43
85
|
@dataclass
|
|
44
86
|
class Heuristic:
|
|
@@ -47,11 +89,12 @@ class Heuristic:
|
|
|
47
89
|
|
|
48
90
|
Heuristics are only created after min_occurrences validations.
|
|
49
91
|
"""
|
|
92
|
+
|
|
50
93
|
id: str
|
|
51
94
|
agent: str
|
|
52
95
|
project_id: str
|
|
53
96
|
condition: str # "form with multiple required fields"
|
|
54
|
-
strategy: str
|
|
97
|
+
strategy: str # "test happy path first, then individual validation"
|
|
55
98
|
confidence: float # 0.0 to 1.0
|
|
56
99
|
occurrence_count: int
|
|
57
100
|
success_count: int
|
|
@@ -75,6 +118,7 @@ class Outcome:
|
|
|
75
118
|
|
|
76
119
|
Outcomes are raw data that can be consolidated into heuristics.
|
|
77
120
|
"""
|
|
121
|
+
|
|
78
122
|
id: str
|
|
79
123
|
agent: str
|
|
80
124
|
project_id: str
|
|
@@ -85,7 +129,7 @@ class Outcome:
|
|
|
85
129
|
duration_ms: Optional[int] = None
|
|
86
130
|
error_message: Optional[str] = None
|
|
87
131
|
user_feedback: Optional[str] = None
|
|
88
|
-
timestamp: datetime = field(default_factory=datetime.
|
|
132
|
+
timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
89
133
|
embedding: Optional[List[float]] = None
|
|
90
134
|
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
91
135
|
|
|
@@ -97,13 +141,14 @@ class UserPreference:
|
|
|
97
141
|
|
|
98
142
|
Persists across sessions so users don't repeat themselves.
|
|
99
143
|
"""
|
|
144
|
+
|
|
100
145
|
id: str
|
|
101
146
|
user_id: str
|
|
102
147
|
category: str # "communication", "code_style", "workflow"
|
|
103
148
|
preference: str # "No emojis in documentation"
|
|
104
149
|
source: str # "explicit_instruction", "inferred_from_correction"
|
|
105
150
|
confidence: float = 1.0 # Lower for inferred preferences
|
|
106
|
-
timestamp: datetime = field(default_factory=datetime.
|
|
151
|
+
timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
107
152
|
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
108
153
|
|
|
109
154
|
|
|
@@ -114,6 +159,7 @@ class DomainKnowledge:
|
|
|
114
159
|
|
|
115
160
|
Different from heuristics - these are facts, not strategies.
|
|
116
161
|
"""
|
|
162
|
+
|
|
117
163
|
id: str
|
|
118
164
|
agent: str
|
|
119
165
|
project_id: str
|
|
@@ -121,7 +167,7 @@ class DomainKnowledge:
|
|
|
121
167
|
fact: str # "Login endpoint uses JWT with 24h expiry"
|
|
122
168
|
source: str # "code_analysis", "documentation", "user_stated"
|
|
123
169
|
confidence: float = 1.0
|
|
124
|
-
last_verified: datetime = field(default_factory=datetime.
|
|
170
|
+
last_verified: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
125
171
|
embedding: Optional[List[float]] = None
|
|
126
172
|
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
127
173
|
|
|
@@ -133,6 +179,7 @@ class AntiPattern:
|
|
|
133
179
|
|
|
134
180
|
Helps agents avoid repeating mistakes.
|
|
135
181
|
"""
|
|
182
|
+
|
|
136
183
|
id: str
|
|
137
184
|
agent: str
|
|
138
185
|
project_id: str
|
|
@@ -141,7 +188,7 @@ class AntiPattern:
|
|
|
141
188
|
better_alternative: str # "Use explicit waits with conditions"
|
|
142
189
|
occurrence_count: int
|
|
143
190
|
last_seen: datetime
|
|
144
|
-
created_at: datetime = field(default_factory=datetime.
|
|
191
|
+
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
145
192
|
embedding: Optional[List[float]] = None
|
|
146
193
|
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
147
194
|
|
|
@@ -153,6 +200,7 @@ class MemorySlice:
|
|
|
153
200
|
|
|
154
201
|
This is what gets injected per-call - must stay under token budget.
|
|
155
202
|
"""
|
|
203
|
+
|
|
156
204
|
heuristics: List[Heuristic] = field(default_factory=list)
|
|
157
205
|
outcomes: List[Outcome] = field(default_factory=list)
|
|
158
206
|
preferences: List[UserPreference] = field(default_factory=list)
|
|
@@ -200,7 +248,7 @@ class MemorySlice:
|
|
|
200
248
|
|
|
201
249
|
# Basic token estimation (rough: 1 token ~ 4 chars)
|
|
202
250
|
if len(result) > max_tokens * 4:
|
|
203
|
-
result = result[:max_tokens * 4] + "\n[truncated]"
|
|
251
|
+
result = result[: max_tokens * 4] + "\n[truncated]"
|
|
204
252
|
|
|
205
253
|
return result
|
|
206
254
|
|
|
@@ -208,9 +256,9 @@ class MemorySlice:
|
|
|
208
256
|
def total_items(self) -> int:
|
|
209
257
|
"""Total number of memory items in this slice."""
|
|
210
258
|
return (
|
|
211
|
-
len(self.heuristics)
|
|
212
|
-
len(self.outcomes)
|
|
213
|
-
len(self.preferences)
|
|
214
|
-
len(self.domain_knowledge)
|
|
215
|
-
len(self.anti_patterns)
|
|
259
|
+
len(self.heuristics)
|
|
260
|
+
+ len(self.outcomes)
|
|
261
|
+
+ len(self.preferences)
|
|
262
|
+
+ len(self.domain_knowledge)
|
|
263
|
+
+ len(self.anti_patterns)
|
|
216
264
|
)
|