alma-memory 0.5.0__py3-none-any.whl → 0.7.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.
Files changed (111) hide show
  1. alma/__init__.py +296 -194
  2. alma/compression/__init__.py +33 -0
  3. alma/compression/pipeline.py +980 -0
  4. alma/confidence/__init__.py +47 -47
  5. alma/confidence/engine.py +540 -540
  6. alma/confidence/types.py +351 -351
  7. alma/config/loader.py +157 -157
  8. alma/consolidation/__init__.py +23 -23
  9. alma/consolidation/engine.py +678 -678
  10. alma/consolidation/prompts.py +84 -84
  11. alma/core.py +1189 -322
  12. alma/domains/__init__.py +30 -30
  13. alma/domains/factory.py +359 -359
  14. alma/domains/schemas.py +448 -448
  15. alma/domains/types.py +272 -272
  16. alma/events/__init__.py +75 -75
  17. alma/events/emitter.py +285 -284
  18. alma/events/storage_mixin.py +246 -246
  19. alma/events/types.py +126 -126
  20. alma/events/webhook.py +425 -425
  21. alma/exceptions.py +49 -49
  22. alma/extraction/__init__.py +31 -31
  23. alma/extraction/auto_learner.py +265 -264
  24. alma/extraction/extractor.py +420 -420
  25. alma/graph/__init__.py +106 -81
  26. alma/graph/backends/__init__.py +32 -18
  27. alma/graph/backends/kuzu.py +624 -0
  28. alma/graph/backends/memgraph.py +432 -0
  29. alma/graph/backends/memory.py +236 -236
  30. alma/graph/backends/neo4j.py +417 -417
  31. alma/graph/base.py +159 -159
  32. alma/graph/extraction.py +198 -198
  33. alma/graph/store.py +860 -860
  34. alma/harness/__init__.py +35 -35
  35. alma/harness/base.py +386 -386
  36. alma/harness/domains.py +705 -705
  37. alma/initializer/__init__.py +37 -37
  38. alma/initializer/initializer.py +418 -418
  39. alma/initializer/types.py +250 -250
  40. alma/integration/__init__.py +62 -62
  41. alma/integration/claude_agents.py +444 -432
  42. alma/integration/helena.py +423 -423
  43. alma/integration/victor.py +471 -471
  44. alma/learning/__init__.py +101 -86
  45. alma/learning/decay.py +878 -0
  46. alma/learning/forgetting.py +1446 -1446
  47. alma/learning/heuristic_extractor.py +390 -390
  48. alma/learning/protocols.py +374 -374
  49. alma/learning/validation.py +346 -346
  50. alma/mcp/__init__.py +123 -45
  51. alma/mcp/__main__.py +156 -156
  52. alma/mcp/resources.py +122 -122
  53. alma/mcp/server.py +955 -591
  54. alma/mcp/tools.py +3254 -511
  55. alma/observability/__init__.py +91 -0
  56. alma/observability/config.py +302 -0
  57. alma/observability/guidelines.py +170 -0
  58. alma/observability/logging.py +424 -0
  59. alma/observability/metrics.py +583 -0
  60. alma/observability/tracing.py +440 -0
  61. alma/progress/__init__.py +21 -21
  62. alma/progress/tracker.py +607 -607
  63. alma/progress/types.py +250 -250
  64. alma/retrieval/__init__.py +134 -53
  65. alma/retrieval/budget.py +525 -0
  66. alma/retrieval/cache.py +1304 -1061
  67. alma/retrieval/embeddings.py +202 -202
  68. alma/retrieval/engine.py +850 -366
  69. alma/retrieval/modes.py +365 -0
  70. alma/retrieval/progressive.py +560 -0
  71. alma/retrieval/scoring.py +344 -344
  72. alma/retrieval/trust_scoring.py +637 -0
  73. alma/retrieval/verification.py +797 -0
  74. alma/session/__init__.py +19 -19
  75. alma/session/manager.py +442 -399
  76. alma/session/types.py +288 -288
  77. alma/storage/__init__.py +101 -61
  78. alma/storage/archive.py +233 -0
  79. alma/storage/azure_cosmos.py +1259 -1048
  80. alma/storage/base.py +1083 -525
  81. alma/storage/chroma.py +1443 -1443
  82. alma/storage/constants.py +103 -0
  83. alma/storage/file_based.py +614 -619
  84. alma/storage/migrations/__init__.py +21 -0
  85. alma/storage/migrations/base.py +321 -0
  86. alma/storage/migrations/runner.py +323 -0
  87. alma/storage/migrations/version_stores.py +337 -0
  88. alma/storage/migrations/versions/__init__.py +11 -0
  89. alma/storage/migrations/versions/v1_0_0.py +373 -0
  90. alma/storage/migrations/versions/v1_1_0_workflow_context.py +551 -0
  91. alma/storage/pinecone.py +1080 -1080
  92. alma/storage/postgresql.py +1948 -1452
  93. alma/storage/qdrant.py +1306 -1306
  94. alma/storage/sqlite_local.py +3041 -1358
  95. alma/testing/__init__.py +46 -0
  96. alma/testing/factories.py +301 -0
  97. alma/testing/mocks.py +389 -0
  98. alma/types.py +292 -264
  99. alma/utils/__init__.py +19 -0
  100. alma/utils/tokenizer.py +521 -0
  101. alma/workflow/__init__.py +83 -0
  102. alma/workflow/artifacts.py +170 -0
  103. alma/workflow/checkpoint.py +311 -0
  104. alma/workflow/context.py +228 -0
  105. alma/workflow/outcomes.py +189 -0
  106. alma/workflow/reducers.py +393 -0
  107. {alma_memory-0.5.0.dist-info → alma_memory-0.7.0.dist-info}/METADATA +244 -72
  108. alma_memory-0.7.0.dist-info/RECORD +112 -0
  109. alma_memory-0.5.0.dist-info/RECORD +0 -76
  110. {alma_memory-0.5.0.dist-info → alma_memory-0.7.0.dist-info}/WHEEL +0 -0
  111. {alma_memory-0.5.0.dist-info → alma_memory-0.7.0.dist-info}/top_level.txt +0 -0
@@ -1,264 +1,265 @@
1
- """
2
- ALMA Auto-Learning Module.
3
-
4
- Bridges LLM-powered fact extraction with ALMA's learning protocols.
5
- Enables Mem0-style automatic learning from conversations.
6
- """
7
-
8
- import logging
9
- from typing import Any, Dict, List, Optional
10
-
11
- from alma.extraction import (
12
- ExtractedFact,
13
- FactExtractor,
14
- FactType,
15
- create_extractor,
16
- )
17
-
18
- logger = logging.getLogger(__name__)
19
-
20
-
21
- class AutoLearner:
22
- """
23
- Automatic learning from conversations.
24
-
25
- This class bridges the gap between Mem0's automatic extraction
26
- and ALMA's explicit learning protocols. It:
27
-
28
- 1. Extracts facts from conversations using LLM or rules
29
- 2. Validates facts against agent scopes
30
- 3. Deduplicates against existing memories
31
- 4. Commits valid facts to ALMA storage
32
-
33
- Usage:
34
- alma = ALMA.from_config(".alma/config.yaml")
35
- auto_learner = AutoLearner(alma)
36
-
37
- # After a conversation
38
- results = auto_learner.learn_from_conversation(
39
- messages=[
40
- {"role": "user", "content": "Test the login form"},
41
- {"role": "assistant", "content": "I tested using incremental validation..."},
42
- ],
43
- agent="helena",
44
- )
45
- """
46
-
47
- def __init__(
48
- self,
49
- alma, # ALMA instance - avoid circular import
50
- extractor: Optional[FactExtractor] = None,
51
- auto_commit: bool = True,
52
- min_confidence: float = 0.5,
53
- ):
54
- """
55
- Initialize AutoLearner.
56
-
57
- Args:
58
- alma: ALMA instance for storage and retrieval
59
- extractor: Custom extractor, or None for auto-detection
60
- auto_commit: Whether to automatically commit extracted facts
61
- min_confidence: Minimum confidence threshold for facts
62
- """
63
- self.alma = alma
64
- self.extractor = extractor or create_extractor()
65
- self.auto_commit = auto_commit
66
- self.min_confidence = min_confidence
67
-
68
- def learn_from_conversation(
69
- self,
70
- messages: List[Dict[str, str]],
71
- agent: str,
72
- user_id: Optional[str] = None,
73
- commit: Optional[bool] = None,
74
- ) -> Dict[str, Any]:
75
- """
76
- Extract and learn from a conversation.
77
-
78
- Args:
79
- messages: Conversation messages
80
- agent: Agent that had the conversation
81
- user_id: Optional user ID for preferences
82
- commit: Override auto_commit setting
83
-
84
- Returns:
85
- Dict with extraction results and commit status
86
- """
87
- should_commit = commit if commit is not None else self.auto_commit
88
-
89
- # Get agent scope for context
90
- scope = self.alma.scopes.get(agent)
91
- agent_context = None
92
- if scope:
93
- agent_context = f"Agent '{agent}' can learn: {scope.can_learn}. Cannot learn: {scope.cannot_learn}"
94
-
95
- # Get existing facts to avoid duplicates
96
- existing_memories = self.alma.retrieve(
97
- task=" ".join(m["content"] for m in messages[-3:]), # Recent context
98
- agent=agent,
99
- top_k=20,
100
- )
101
- existing_facts = []
102
- for h in existing_memories.heuristics:
103
- existing_facts.append(f"{h.condition}: {h.strategy}")
104
- for ap in existing_memories.anti_patterns:
105
- existing_facts.append(f"AVOID: {ap.pattern}")
106
- for dk in existing_memories.domain_knowledge:
107
- existing_facts.append(dk.fact)
108
-
109
- # Extract facts
110
- extraction_result = self.extractor.extract(
111
- messages=messages,
112
- agent_context=agent_context,
113
- existing_facts=existing_facts if existing_facts else None,
114
- )
115
-
116
- # Filter by confidence and scope
117
- valid_facts = []
118
- rejected_facts = []
119
-
120
- for fact in extraction_result.facts:
121
- # Check confidence
122
- if fact.confidence < self.min_confidence:
123
- rejected_facts.append(
124
- {
125
- "fact": fact,
126
- "reason": f"Low confidence: {fact.confidence} < {self.min_confidence}",
127
- }
128
- )
129
- continue
130
-
131
- # Check scope for heuristics and anti-patterns
132
- if scope and fact.fact_type in (FactType.HEURISTIC, FactType.ANTI_PATTERN):
133
- # Infer domain from content
134
- inferred_domain = self._infer_domain(fact.content)
135
- if inferred_domain and not scope.is_allowed(inferred_domain):
136
- rejected_facts.append(
137
- {
138
- "fact": fact,
139
- "reason": f"Outside agent scope: {inferred_domain}",
140
- }
141
- )
142
- continue
143
-
144
- valid_facts.append(fact)
145
-
146
- # Commit if enabled
147
- committed = []
148
- if should_commit:
149
- for fact in valid_facts:
150
- try:
151
- result = self._commit_fact(fact, agent, user_id)
152
- if result:
153
- committed.append({"fact": fact, "id": result})
154
- except Exception as e:
155
- logger.error(f"Failed to commit fact: {e}")
156
- rejected_facts.append(
157
- {
158
- "fact": fact,
159
- "reason": f"Commit failed: {str(e)}",
160
- }
161
- )
162
-
163
- return {
164
- "extracted_count": len(extraction_result.facts),
165
- "valid_count": len(valid_facts),
166
- "committed_count": len(committed),
167
- "rejected_count": len(rejected_facts),
168
- "extraction_time_ms": extraction_result.extraction_time_ms,
169
- "tokens_used": extraction_result.tokens_used,
170
- "committed": committed,
171
- "rejected": rejected_facts,
172
- "valid_facts": valid_facts,
173
- }
174
-
175
- def _commit_fact(
176
- self,
177
- fact: ExtractedFact,
178
- agent: str,
179
- user_id: Optional[str],
180
- ) -> Optional[str]:
181
- """Commit a single fact to ALMA storage."""
182
-
183
- if fact.fact_type == FactType.HEURISTIC:
184
- # Use learning protocol for heuristics
185
- return self.alma.learning.add_heuristic_direct(
186
- agent=agent,
187
- project_id=self.alma.project_id,
188
- condition=fact.condition or fact.content,
189
- strategy=fact.strategy or fact.content,
190
- confidence=fact.confidence,
191
- metadata={"source": "auto_extraction"},
192
- )
193
-
194
- elif fact.fact_type == FactType.ANTI_PATTERN:
195
- return self.alma.learning.add_anti_pattern(
196
- agent=agent,
197
- project_id=self.alma.project_id,
198
- pattern=fact.content,
199
- why_bad=fact.condition,
200
- better_alternative=fact.strategy,
201
- )
202
-
203
- elif fact.fact_type == FactType.PREFERENCE:
204
- if user_id:
205
- pref = self.alma.add_user_preference(
206
- user_id=user_id,
207
- category=fact.category or "general",
208
- preference=fact.content,
209
- source="auto_extraction",
210
- )
211
- return pref.id if pref else None
212
-
213
- elif fact.fact_type == FactType.DOMAIN_KNOWLEDGE:
214
- knowledge = self.alma.add_domain_knowledge(
215
- agent=agent,
216
- domain=fact.domain or "general",
217
- fact=fact.content,
218
- source="auto_extraction",
219
- )
220
- return knowledge.id if knowledge else None
221
-
222
- elif fact.fact_type == FactType.OUTCOME:
223
- # Outcomes need success/failure info we don't have
224
- # Store as domain knowledge instead
225
- knowledge = self.alma.add_domain_knowledge(
226
- agent=agent,
227
- domain="outcomes",
228
- fact=fact.content,
229
- source="auto_extraction",
230
- )
231
- return knowledge.id if knowledge else None
232
-
233
- return None
234
-
235
- def _infer_domain(self, content: str) -> Optional[str]:
236
- """Infer domain from fact content using keywords."""
237
- content_lower = content.lower()
238
-
239
- domain_keywords = {
240
- "testing": ["test", "assert", "selenium", "playwright", "cypress"],
241
- "frontend": ["css", "html", "react", "vue", "ui", "button", "form"],
242
- "backend": ["api", "database", "sql", "server", "endpoint"],
243
- "security": ["auth", "token", "password", "encrypt", "csrf"],
244
- "performance": ["latency", "cache", "optimize", "slow", "fast"],
245
- }
246
-
247
- for domain, keywords in domain_keywords.items():
248
- if any(kw in content_lower for kw in keywords):
249
- return domain
250
-
251
- return None
252
-
253
-
254
- def add_auto_learning_to_alma(alma) -> AutoLearner:
255
- """
256
- Convenience function to add auto-learning to an ALMA instance.
257
-
258
- Usage:
259
- alma = ALMA.from_config(".alma/config.yaml")
260
- auto_learner = add_auto_learning_to_alma(alma)
261
-
262
- # Now use auto_learner.learn_from_conversation()
263
- """
264
- return AutoLearner(alma)
1
+ """
2
+ ALMA Auto-Learning Module.
3
+
4
+ Bridges LLM-powered fact extraction with ALMA's learning protocols.
5
+ Enables Mem0-style automatic learning from conversations.
6
+ """
7
+
8
+ import logging
9
+ from typing import Any, Dict, List, Optional
10
+
11
+ from alma.extraction.extractor import (
12
+ ExtractedFact,
13
+ FactExtractor,
14
+ FactType,
15
+ create_extractor,
16
+ )
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class AutoLearner:
22
+ """
23
+ Automatic learning from conversations.
24
+
25
+ This class bridges the gap between Mem0's automatic extraction
26
+ and ALMA's explicit learning protocols. It:
27
+
28
+ 1. Extracts facts from conversations using LLM or rules
29
+ 2. Validates facts against agent scopes
30
+ 3. Deduplicates against existing memories
31
+ 4. Commits valid facts to ALMA storage
32
+
33
+ Usage:
34
+ alma = ALMA.from_config(".alma/config.yaml")
35
+ auto_learner = AutoLearner(alma)
36
+
37
+ # After a conversation
38
+ results = auto_learner.learn_from_conversation(
39
+ messages=[
40
+ {"role": "user", "content": "Test the login form"},
41
+ {"role": "assistant", "content": "I tested using incremental validation..."},
42
+ ],
43
+ agent="helena",
44
+ )
45
+ """
46
+
47
+ def __init__(
48
+ self,
49
+ alma, # ALMA instance - avoid circular import
50
+ extractor: Optional[FactExtractor] = None,
51
+ auto_commit: bool = True,
52
+ min_confidence: float = 0.5,
53
+ ):
54
+ """
55
+ Initialize AutoLearner.
56
+
57
+ Args:
58
+ alma: ALMA instance for storage and retrieval
59
+ extractor: Custom extractor, or None for auto-detection
60
+ auto_commit: Whether to automatically commit extracted facts
61
+ min_confidence: Minimum confidence threshold for facts
62
+ """
63
+ self.alma = alma
64
+ self.extractor = extractor or create_extractor()
65
+ self.auto_commit = auto_commit
66
+ self.min_confidence = min_confidence
67
+
68
+ def learn_from_conversation(
69
+ self,
70
+ messages: List[Dict[str, str]],
71
+ agent: str,
72
+ user_id: Optional[str] = None,
73
+ commit: Optional[bool] = None,
74
+ ) -> Dict[str, Any]:
75
+ """
76
+ Extract and learn from a conversation.
77
+
78
+ Args:
79
+ messages: Conversation messages
80
+ agent: Agent that had the conversation
81
+ user_id: Optional user ID for preferences
82
+ commit: Override auto_commit setting
83
+
84
+ Returns:
85
+ Dict with extraction results and commit status
86
+ """
87
+ should_commit = commit if commit is not None else self.auto_commit
88
+
89
+ # Get agent scope for context
90
+ scope = self.alma.scopes.get(agent)
91
+ agent_context = None
92
+ if scope:
93
+ agent_context = f"Agent '{agent}' can learn: {scope.can_learn}. Cannot learn: {scope.cannot_learn}"
94
+
95
+ # Get existing facts to avoid duplicates
96
+ existing_memories = self.alma.retrieve(
97
+ task=" ".join(m["content"] for m in messages[-3:]), # Recent context
98
+ agent=agent,
99
+ top_k=20,
100
+ )
101
+ existing_facts = []
102
+ for h in existing_memories.heuristics:
103
+ existing_facts.append(f"{h.condition}: {h.strategy}")
104
+ for ap in existing_memories.anti_patterns:
105
+ existing_facts.append(f"AVOID: {ap.pattern}")
106
+ for dk in existing_memories.domain_knowledge:
107
+ existing_facts.append(dk.fact)
108
+
109
+ # Extract facts
110
+ extraction_result = self.extractor.extract(
111
+ messages=messages,
112
+ agent_context=agent_context,
113
+ existing_facts=existing_facts if existing_facts else None,
114
+ )
115
+
116
+ # Filter by confidence and scope
117
+ valid_facts = []
118
+ rejected_facts = []
119
+
120
+ for fact in extraction_result.facts:
121
+ # Check confidence
122
+ if fact.confidence < self.min_confidence:
123
+ rejected_facts.append(
124
+ {
125
+ "fact": fact,
126
+ "reason": f"Low confidence: {fact.confidence} < {self.min_confidence}",
127
+ }
128
+ )
129
+ continue
130
+
131
+ # Check scope for heuristics and anti-patterns
132
+ if scope and fact.fact_type in (FactType.HEURISTIC, FactType.ANTI_PATTERN):
133
+ # Infer domain from content
134
+ inferred_domain = self._infer_domain(fact.content)
135
+ if inferred_domain and not scope.is_allowed(inferred_domain):
136
+ rejected_facts.append(
137
+ {
138
+ "fact": fact,
139
+ "reason": f"Outside agent scope: {inferred_domain}",
140
+ }
141
+ )
142
+ continue
143
+
144
+ valid_facts.append(fact)
145
+
146
+ # Commit if enabled
147
+ committed = []
148
+ if should_commit:
149
+ for fact in valid_facts:
150
+ try:
151
+ result = self._commit_fact(fact, agent, user_id)
152
+ if result:
153
+ committed.append({"fact": fact, "id": result})
154
+ except Exception as e:
155
+ logger.error(f"Failed to commit fact: {e}")
156
+ rejected_facts.append(
157
+ {
158
+ "fact": fact,
159
+ "reason": f"Commit failed: {str(e)}",
160
+ }
161
+ )
162
+
163
+ return {
164
+ "extracted_count": len(extraction_result.facts),
165
+ "valid_count": len(valid_facts),
166
+ "committed_count": len(committed),
167
+ "rejected_count": len(rejected_facts),
168
+ "extraction_time_ms": extraction_result.extraction_time_ms,
169
+ "tokens_used": extraction_result.tokens_used,
170
+ "committed": committed,
171
+ "rejected": rejected_facts,
172
+ "valid_facts": valid_facts,
173
+ }
174
+
175
+ def _commit_fact(
176
+ self,
177
+ fact: ExtractedFact,
178
+ agent: str,
179
+ user_id: Optional[str],
180
+ ) -> Optional[str]:
181
+ """Commit a single fact to ALMA storage."""
182
+
183
+ if fact.fact_type == FactType.HEURISTIC:
184
+ # Use learning protocol for heuristics
185
+ return self.alma.learning.add_heuristic_direct(
186
+ agent=agent,
187
+ project_id=self.alma.project_id,
188
+ condition=fact.condition or fact.content,
189
+ strategy=fact.strategy or fact.content,
190
+ confidence=fact.confidence,
191
+ metadata={"source": "auto_extraction"},
192
+ )
193
+
194
+ elif fact.fact_type == FactType.ANTI_PATTERN:
195
+ return self.alma.learning.add_anti_pattern(
196
+ agent=agent,
197
+ project_id=self.alma.project_id,
198
+ pattern=fact.content,
199
+ why_bad=fact.condition,
200
+ better_alternative=fact.strategy,
201
+ )
202
+
203
+ elif fact.fact_type == FactType.PREFERENCE:
204
+ if user_id:
205
+ pref = self.alma.add_user_preference(
206
+ user_id=user_id,
207
+ category=fact.category or "general",
208
+ preference=fact.content,
209
+ source="auto_extraction",
210
+ )
211
+ return pref.id
212
+
213
+ elif fact.fact_type == FactType.DOMAIN_KNOWLEDGE:
214
+ # add_domain_knowledge now raises ScopeViolationError instead of returning None
215
+ knowledge = self.alma.add_domain_knowledge(
216
+ agent=agent,
217
+ domain=fact.domain or "general",
218
+ fact=fact.content,
219
+ source="auto_extraction",
220
+ )
221
+ return knowledge.id
222
+
223
+ elif fact.fact_type == FactType.OUTCOME:
224
+ # Outcomes need success/failure info we don't have
225
+ # Store as domain knowledge instead
226
+ knowledge = self.alma.add_domain_knowledge(
227
+ agent=agent,
228
+ domain="outcomes",
229
+ fact=fact.content,
230
+ source="auto_extraction",
231
+ )
232
+ return knowledge.id
233
+
234
+ return None
235
+
236
+ def _infer_domain(self, content: str) -> Optional[str]:
237
+ """Infer domain from fact content using keywords."""
238
+ content_lower = content.lower()
239
+
240
+ domain_keywords = {
241
+ "testing": ["test", "assert", "selenium", "playwright", "cypress"],
242
+ "frontend": ["css", "html", "react", "vue", "ui", "button", "form"],
243
+ "backend": ["api", "database", "sql", "server", "endpoint"],
244
+ "security": ["auth", "token", "password", "encrypt", "csrf"],
245
+ "performance": ["latency", "cache", "optimize", "slow", "fast"],
246
+ }
247
+
248
+ for domain, keywords in domain_keywords.items():
249
+ if any(kw in content_lower for kw in keywords):
250
+ return domain
251
+
252
+ return None
253
+
254
+
255
+ def add_auto_learning_to_alma(alma) -> AutoLearner:
256
+ """
257
+ Convenience function to add auto-learning to an ALMA instance.
258
+
259
+ Usage:
260
+ alma = ALMA.from_config(".alma/config.yaml")
261
+ auto_learner = add_auto_learning_to_alma(alma)
262
+
263
+ # Now use auto_learner.learn_from_conversation()
264
+ """
265
+ return AutoLearner(alma)