alma-memory 0.2.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.
alma/__init__.py ADDED
@@ -0,0 +1,75 @@
1
+ """
2
+ ALMA - Agent Learning Memory Architecture
3
+
4
+ Persistent memory system for AI agents that learn and improve over time
5
+ through structured memory layers - without model weight updates.
6
+
7
+ The Harness Pattern:
8
+ 1. Setting - Fixed environment (tools, constraints)
9
+ 2. Context - Ephemeral per-run inputs
10
+ 3. Agent - The executor with scoped intelligence
11
+ 4. Memory Schema - Domain-specific learning structure
12
+
13
+ This makes any tool-using agent appear to "learn" by injecting relevant
14
+ memory slices before each run and updating memory after.
15
+ """
16
+
17
+ __version__ = "0.2.0"
18
+
19
+ # Core
20
+ from alma.core import ALMA
21
+ from alma.types import (
22
+ Heuristic,
23
+ Outcome,
24
+ UserPreference,
25
+ DomainKnowledge,
26
+ AntiPattern,
27
+ MemorySlice,
28
+ MemoryScope,
29
+ )
30
+
31
+ # Harness Pattern
32
+ from alma.harness.base import (
33
+ Setting,
34
+ Context,
35
+ Agent,
36
+ MemorySchema,
37
+ Harness,
38
+ Tool,
39
+ ToolType,
40
+ RunResult,
41
+ )
42
+ from alma.harness.domains import (
43
+ CodingDomain,
44
+ ResearchDomain,
45
+ ContentDomain,
46
+ OperationsDomain,
47
+ create_harness,
48
+ )
49
+
50
+ __all__ = [
51
+ # Core
52
+ "ALMA",
53
+ "Heuristic",
54
+ "Outcome",
55
+ "UserPreference",
56
+ "DomainKnowledge",
57
+ "AntiPattern",
58
+ "MemorySlice",
59
+ "MemoryScope",
60
+ # Harness Pattern
61
+ "Setting",
62
+ "Context",
63
+ "Agent",
64
+ "MemorySchema",
65
+ "Harness",
66
+ "Tool",
67
+ "ToolType",
68
+ "RunResult",
69
+ # Domain Configurations
70
+ "CodingDomain",
71
+ "ResearchDomain",
72
+ "ContentDomain",
73
+ "OperationsDomain",
74
+ "create_harness",
75
+ ]
@@ -0,0 +1,5 @@
1
+ """ALMA Configuration."""
2
+
3
+ from alma.config.loader import ConfigLoader
4
+
5
+ __all__ = ["ConfigLoader"]
alma/config/loader.py ADDED
@@ -0,0 +1,156 @@
1
+ """
2
+ ALMA Configuration Loader.
3
+
4
+ Handles loading configuration from files and environment variables,
5
+ with support for Azure Key Vault secret resolution.
6
+ """
7
+
8
+ import os
9
+ import logging
10
+ from pathlib import Path
11
+ from typing import Dict, Any, Optional
12
+
13
+ import yaml
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class ConfigLoader:
19
+ """
20
+ Loads ALMA configuration from YAML files with environment variable expansion.
21
+
22
+ Supports:
23
+ - ${ENV_VAR} syntax for environment variables
24
+ - ${KEYVAULT:secret-name} syntax for Azure Key Vault (when configured)
25
+ """
26
+
27
+ _keyvault_client = None
28
+
29
+ @classmethod
30
+ def load(cls, config_path: str) -> Dict[str, Any]:
31
+ """
32
+ Load configuration from YAML file.
33
+
34
+ Args:
35
+ config_path: Path to config.yaml
36
+
37
+ Returns:
38
+ Parsed and expanded configuration dict
39
+ """
40
+ path = Path(config_path)
41
+ if not path.exists():
42
+ logger.warning(f"Config not found at {config_path}, using defaults")
43
+ return cls._get_defaults()
44
+
45
+ with open(path, "r") as f:
46
+ raw_config = yaml.safe_load(f)
47
+
48
+ # Get the 'alma' section or use whole file
49
+ config = raw_config.get("alma", raw_config)
50
+
51
+ # Expand environment variables and secrets
52
+ config = cls._expand_config(config)
53
+
54
+ return config
55
+
56
+ @classmethod
57
+ def _expand_config(cls, config: Any) -> Any:
58
+ """Recursively expand environment variables and secrets in config."""
59
+ if isinstance(config, dict):
60
+ return {k: cls._expand_config(v) for k, v in config.items()}
61
+ elif isinstance(config, list):
62
+ return [cls._expand_config(item) for item in config]
63
+ elif isinstance(config, str):
64
+ return cls._expand_value(config)
65
+ return config
66
+
67
+ @classmethod
68
+ def _expand_value(cls, value: str) -> str:
69
+ """
70
+ Expand a single config value.
71
+
72
+ Handles:
73
+ - ${ENV_VAR} -> os.environ["ENV_VAR"]
74
+ - ${KEYVAULT:secret-name} -> Azure Key Vault lookup
75
+ """
76
+ if not isinstance(value, str) or "${" not in value:
77
+ return value
78
+
79
+ # Handle ${VAR} patterns
80
+ import re
81
+ pattern = r"\$\{([^}]+)\}"
82
+
83
+ def replace(match):
84
+ ref = match.group(1)
85
+
86
+ if ref.startswith("KEYVAULT:"):
87
+ secret_name = ref[9:] # Remove "KEYVAULT:" prefix
88
+ return cls._get_keyvault_secret(secret_name)
89
+ else:
90
+ # Environment variable
91
+ env_value = os.environ.get(ref)
92
+ if env_value is None:
93
+ logger.warning(f"Environment variable {ref} not set")
94
+ return match.group(0) # Keep original if not found
95
+ return env_value
96
+
97
+ return re.sub(pattern, replace, value)
98
+
99
+ @classmethod
100
+ def _get_keyvault_secret(cls, secret_name: str) -> str:
101
+ """
102
+ Retrieve secret from Azure Key Vault.
103
+
104
+ Requires AZURE_KEYVAULT_URL environment variable.
105
+ """
106
+ if cls._keyvault_client is None:
107
+ vault_url = os.environ.get("AZURE_KEYVAULT_URL")
108
+ if not vault_url:
109
+ logger.error("AZURE_KEYVAULT_URL not set, cannot retrieve secrets")
110
+ return f"${{KEYVAULT:{secret_name}}}"
111
+
112
+ try:
113
+ from azure.identity import DefaultAzureCredential
114
+ from azure.keyvault.secrets import SecretClient
115
+
116
+ credential = DefaultAzureCredential()
117
+ cls._keyvault_client = SecretClient(
118
+ vault_url=vault_url,
119
+ credential=credential,
120
+ )
121
+ except ImportError:
122
+ logger.error(
123
+ "azure-identity and azure-keyvault-secrets packages required "
124
+ "for Key Vault integration"
125
+ )
126
+ return f"${{KEYVAULT:{secret_name}}}"
127
+
128
+ try:
129
+ secret = cls._keyvault_client.get_secret(secret_name)
130
+ return secret.value
131
+ except Exception as e:
132
+ logger.error(f"Failed to retrieve secret {secret_name}: {e}")
133
+ return f"${{KEYVAULT:{secret_name}}}"
134
+
135
+ @staticmethod
136
+ def _get_defaults() -> Dict[str, Any]:
137
+ """Return default configuration."""
138
+ return {
139
+ "project_id": "default",
140
+ "storage": "file",
141
+ "embedding_provider": "local",
142
+ "agents": {},
143
+ }
144
+
145
+ @classmethod
146
+ def save(cls, config: Dict[str, Any], config_path: str):
147
+ """
148
+ Save configuration to YAML file.
149
+
150
+ Note: Does NOT save secrets - those should remain as ${} references.
151
+ """
152
+ path = Path(config_path)
153
+ path.parent.mkdir(parents=True, exist_ok=True)
154
+
155
+ with open(path, "w") as f:
156
+ yaml.dump({"alma": config}, f, default_flow_style=False)
alma/core.py ADDED
@@ -0,0 +1,322 @@
1
+ """
2
+ ALMA Core - Main interface for the Agent Learning Memory Architecture.
3
+ """
4
+
5
+ from typing import Optional, Dict, Any, List
6
+ from pathlib import Path
7
+ import yaml
8
+ import logging
9
+
10
+ from alma.types import (
11
+ MemorySlice,
12
+ MemoryScope,
13
+ Heuristic,
14
+ Outcome,
15
+ UserPreference,
16
+ DomainKnowledge,
17
+ AntiPattern,
18
+ )
19
+ from alma.storage.base import StorageBackend
20
+ from alma.retrieval.engine import RetrievalEngine
21
+ from alma.learning.protocols import LearningProtocol
22
+ from alma.config.loader import ConfigLoader
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ class ALMA:
28
+ """
29
+ Agent Learning Memory Architecture - Main Interface.
30
+
31
+ Provides methods for:
32
+ - Retrieving relevant memories for a task
33
+ - Learning from task outcomes
34
+ - Managing agent memory scopes
35
+ """
36
+
37
+ def __init__(
38
+ self,
39
+ storage: StorageBackend,
40
+ retrieval_engine: RetrievalEngine,
41
+ learning_protocol: LearningProtocol,
42
+ scopes: Dict[str, MemoryScope],
43
+ project_id: str,
44
+ ):
45
+ self.storage = storage
46
+ self.retrieval = retrieval_engine
47
+ self.learning = learning_protocol
48
+ self.scopes = scopes
49
+ self.project_id = project_id
50
+
51
+ @classmethod
52
+ def from_config(cls, config_path: str) -> "ALMA":
53
+ """
54
+ Initialize ALMA from a configuration file.
55
+
56
+ Args:
57
+ config_path: Path to .alma/config.yaml
58
+
59
+ Returns:
60
+ Configured ALMA instance
61
+ """
62
+ config = ConfigLoader.load(config_path)
63
+
64
+ # Initialize storage backend based on config
65
+ storage = cls._create_storage(config)
66
+
67
+ # Initialize retrieval engine
68
+ retrieval = RetrievalEngine(
69
+ storage=storage,
70
+ embedding_provider=config.get("embedding_provider", "local"),
71
+ )
72
+
73
+ # Initialize learning protocol
74
+ learning = LearningProtocol(
75
+ storage=storage,
76
+ scopes={
77
+ name: MemoryScope(
78
+ agent_name=name,
79
+ can_learn=scope.get("can_learn", []),
80
+ cannot_learn=scope.get("cannot_learn", []),
81
+ min_occurrences_for_heuristic=scope.get(
82
+ "min_occurrences_for_heuristic", 3
83
+ ),
84
+ )
85
+ for name, scope in config.get("agents", {}).items()
86
+ },
87
+ )
88
+
89
+ # Build scopes dict
90
+ scopes = {
91
+ name: MemoryScope(
92
+ agent_name=name,
93
+ can_learn=scope.get("can_learn", []),
94
+ cannot_learn=scope.get("cannot_learn", []),
95
+ min_occurrences_for_heuristic=scope.get(
96
+ "min_occurrences_for_heuristic", 3
97
+ ),
98
+ )
99
+ for name, scope in config.get("agents", {}).items()
100
+ }
101
+
102
+ return cls(
103
+ storage=storage,
104
+ retrieval_engine=retrieval,
105
+ learning_protocol=learning,
106
+ scopes=scopes,
107
+ project_id=config.get("project_id", "default"),
108
+ )
109
+
110
+ @staticmethod
111
+ def _create_storage(config: Dict[str, Any]) -> StorageBackend:
112
+ """Create appropriate storage backend based on config."""
113
+ storage_type = config.get("storage", "file")
114
+
115
+ if storage_type == "azure":
116
+ from alma.storage.azure_cosmos import AzureCosmosStorage
117
+ return AzureCosmosStorage.from_config(config)
118
+ elif storage_type == "sqlite":
119
+ from alma.storage.sqlite_local import SQLiteStorage
120
+ return SQLiteStorage.from_config(config)
121
+ else:
122
+ from alma.storage.file_based import FileBasedStorage
123
+ return FileBasedStorage.from_config(config)
124
+
125
+ def retrieve(
126
+ self,
127
+ task: str,
128
+ agent: str,
129
+ user_id: Optional[str] = None,
130
+ top_k: int = 5,
131
+ ) -> MemorySlice:
132
+ """
133
+ Retrieve relevant memories for a task.
134
+
135
+ Args:
136
+ task: Description of the task to perform
137
+ agent: Name of the agent requesting memories
138
+ user_id: Optional user ID for preference retrieval
139
+ top_k: Maximum items per memory type
140
+
141
+ Returns:
142
+ MemorySlice with relevant memories for context injection
143
+ """
144
+ # Validate agent has a defined scope
145
+ if agent not in self.scopes:
146
+ logger.warning(f"Agent '{agent}' has no defined scope, using defaults")
147
+
148
+ return self.retrieval.retrieve(
149
+ query=task,
150
+ agent=agent,
151
+ project_id=self.project_id,
152
+ user_id=user_id,
153
+ top_k=top_k,
154
+ scope=self.scopes.get(agent),
155
+ )
156
+
157
+ def learn(
158
+ self,
159
+ agent: str,
160
+ task: str,
161
+ outcome: str, # "success" or "failure"
162
+ strategy_used: str,
163
+ task_type: Optional[str] = None,
164
+ duration_ms: Optional[int] = None,
165
+ error_message: Optional[str] = None,
166
+ feedback: Optional[str] = None,
167
+ ) -> bool:
168
+ """
169
+ Learn from a task outcome.
170
+
171
+ Validates that learning is within agent's scope before committing.
172
+ Invalidates cache after learning to ensure fresh retrieval results.
173
+
174
+ Args:
175
+ agent: Name of the agent that executed the task
176
+ task: Description of the task
177
+ outcome: "success" or "failure"
178
+ strategy_used: What approach was taken
179
+ task_type: Category of task (for grouping)
180
+ duration_ms: How long the task took
181
+ error_message: Error details if failed
182
+ feedback: User feedback if provided
183
+
184
+ Returns:
185
+ True if learning was accepted, False if rejected (scope violation)
186
+ """
187
+ result = self.learning.learn(
188
+ agent=agent,
189
+ project_id=self.project_id,
190
+ task=task,
191
+ outcome=outcome == "success",
192
+ strategy_used=strategy_used,
193
+ task_type=task_type,
194
+ duration_ms=duration_ms,
195
+ error_message=error_message,
196
+ feedback=feedback,
197
+ )
198
+
199
+ # Invalidate cache for this agent/project after learning
200
+ if result:
201
+ self.retrieval.invalidate_cache(agent=agent, project_id=self.project_id)
202
+
203
+ return result
204
+
205
+ def add_user_preference(
206
+ self,
207
+ user_id: str,
208
+ category: str,
209
+ preference: str,
210
+ source: str = "explicit_instruction",
211
+ ) -> UserPreference:
212
+ """
213
+ Add a user preference to memory.
214
+
215
+ Args:
216
+ user_id: User identifier
217
+ category: Category (communication, code_style, workflow)
218
+ preference: The preference text
219
+ source: How this was learned
220
+
221
+ Returns:
222
+ The created UserPreference
223
+ """
224
+ result = self.learning.add_preference(
225
+ user_id=user_id,
226
+ category=category,
227
+ preference=preference,
228
+ source=source,
229
+ )
230
+
231
+ # Invalidate cache for project (user preferences affect all agents)
232
+ self.retrieval.invalidate_cache(project_id=self.project_id)
233
+
234
+ return result
235
+
236
+ def add_domain_knowledge(
237
+ self,
238
+ agent: str,
239
+ domain: str,
240
+ fact: str,
241
+ source: str = "user_stated",
242
+ ) -> Optional[DomainKnowledge]:
243
+ """
244
+ Add domain knowledge within agent's scope.
245
+
246
+ Args:
247
+ agent: Agent this knowledge belongs to
248
+ domain: Knowledge domain
249
+ fact: The fact to remember
250
+ source: How this was learned
251
+
252
+ Returns:
253
+ The created DomainKnowledge or None if scope violation
254
+ """
255
+ # Check scope
256
+ scope = self.scopes.get(agent)
257
+ if scope and not scope.is_allowed(domain):
258
+ logger.warning(
259
+ f"Agent '{agent}' not allowed to learn in domain '{domain}'"
260
+ )
261
+ return None
262
+
263
+ result = self.learning.add_domain_knowledge(
264
+ agent=agent,
265
+ project_id=self.project_id,
266
+ domain=domain,
267
+ fact=fact,
268
+ source=source,
269
+ )
270
+
271
+ # Invalidate cache for this agent/project after adding knowledge
272
+ if result:
273
+ self.retrieval.invalidate_cache(agent=agent, project_id=self.project_id)
274
+
275
+ return result
276
+
277
+ def forget(
278
+ self,
279
+ agent: Optional[str] = None,
280
+ older_than_days: int = 90,
281
+ below_confidence: float = 0.3,
282
+ ) -> int:
283
+ """
284
+ Prune stale or low-confidence memories.
285
+
286
+ Invalidates cache after pruning to ensure fresh retrieval results.
287
+
288
+ Args:
289
+ agent: Specific agent to prune, or None for all
290
+ older_than_days: Remove outcomes older than this
291
+ below_confidence: Remove heuristics below this confidence
292
+
293
+ Returns:
294
+ Number of items pruned
295
+ """
296
+ count = self.learning.forget(
297
+ project_id=self.project_id,
298
+ agent=agent,
299
+ older_than_days=older_than_days,
300
+ below_confidence=below_confidence,
301
+ )
302
+
303
+ # Invalidate cache after forgetting (memories were removed)
304
+ if count > 0:
305
+ self.retrieval.invalidate_cache(agent=agent, project_id=self.project_id)
306
+
307
+ return count
308
+
309
+ def get_stats(self, agent: Optional[str] = None) -> Dict[str, Any]:
310
+ """
311
+ Get memory statistics.
312
+
313
+ Args:
314
+ agent: Specific agent or None for all
315
+
316
+ Returns:
317
+ Dict with counts and metadata
318
+ """
319
+ return self.storage.get_stats(
320
+ project_id=self.project_id,
321
+ agent=agent,
322
+ )
@@ -0,0 +1,35 @@
1
+ """
2
+ ALMA Harness Pattern.
3
+
4
+ A structured framework for creating learning agents across any domain:
5
+ - Setting: Environment, tools, constraints
6
+ - Context: Task-specific inputs per run
7
+ - Agent: The executor with scoped intelligence
8
+ - Memory Schema: Domain-specific persistent learning
9
+ """
10
+
11
+ from alma.harness.base import (
12
+ Setting,
13
+ Context,
14
+ Agent,
15
+ MemorySchema,
16
+ Harness,
17
+ )
18
+ from alma.harness.domains import (
19
+ CodingDomain,
20
+ ResearchDomain,
21
+ ContentDomain,
22
+ OperationsDomain,
23
+ )
24
+
25
+ __all__ = [
26
+ "Setting",
27
+ "Context",
28
+ "Agent",
29
+ "MemorySchema",
30
+ "Harness",
31
+ "CodingDomain",
32
+ "ResearchDomain",
33
+ "ContentDomain",
34
+ "OperationsDomain",
35
+ ]