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 +75 -0
- alma/config/__init__.py +5 -0
- alma/config/loader.py +156 -0
- alma/core.py +322 -0
- alma/harness/__init__.py +35 -0
- alma/harness/base.py +377 -0
- alma/harness/domains.py +689 -0
- alma/integration/__init__.py +62 -0
- alma/integration/claude_agents.py +432 -0
- alma/integration/helena.py +413 -0
- alma/integration/victor.py +447 -0
- alma/learning/__init__.py +86 -0
- alma/learning/forgetting.py +1396 -0
- alma/learning/heuristic_extractor.py +374 -0
- alma/learning/protocols.py +326 -0
- alma/learning/validation.py +341 -0
- alma/mcp/__init__.py +45 -0
- alma/mcp/__main__.py +155 -0
- alma/mcp/resources.py +121 -0
- alma/mcp/server.py +533 -0
- alma/mcp/tools.py +374 -0
- alma/retrieval/__init__.py +53 -0
- alma/retrieval/cache.py +1062 -0
- alma/retrieval/embeddings.py +202 -0
- alma/retrieval/engine.py +287 -0
- alma/retrieval/scoring.py +334 -0
- alma/storage/__init__.py +20 -0
- alma/storage/azure_cosmos.py +972 -0
- alma/storage/base.py +372 -0
- alma/storage/file_based.py +583 -0
- alma/storage/sqlite_local.py +912 -0
- alma/types.py +216 -0
- alma_memory-0.2.0.dist-info/METADATA +327 -0
- alma_memory-0.2.0.dist-info/RECORD +36 -0
- alma_memory-0.2.0.dist-info/WHEEL +5 -0
- alma_memory-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ALMA Learning Validation.
|
|
3
|
+
|
|
4
|
+
Enforces scope constraints and validates learning requests.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Optional, List, Dict, Any, Set
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from enum import Enum
|
|
11
|
+
|
|
12
|
+
from alma.types import MemoryScope
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ValidationResult(Enum):
|
|
18
|
+
"""Result of a validation check."""
|
|
19
|
+
ALLOWED = "allowed"
|
|
20
|
+
DENIED_OUT_OF_SCOPE = "denied_out_of_scope"
|
|
21
|
+
DENIED_FORBIDDEN = "denied_forbidden"
|
|
22
|
+
DENIED_UNKNOWN_AGENT = "denied_unknown_agent"
|
|
23
|
+
WARNING_NO_SCOPE = "warning_no_scope"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class ValidationReport:
|
|
28
|
+
"""Detailed report of a validation check."""
|
|
29
|
+
result: ValidationResult
|
|
30
|
+
agent: str
|
|
31
|
+
domain: str
|
|
32
|
+
reason: str
|
|
33
|
+
allowed_domains: List[str] = field(default_factory=list)
|
|
34
|
+
forbidden_domains: List[str] = field(default_factory=list)
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def is_allowed(self) -> bool:
|
|
38
|
+
"""Check if the validation passed."""
|
|
39
|
+
return self.result in (ValidationResult.ALLOWED, ValidationResult.WARNING_NO_SCOPE)
|
|
40
|
+
|
|
41
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
42
|
+
"""Convert to dictionary."""
|
|
43
|
+
return {
|
|
44
|
+
"result": self.result.value,
|
|
45
|
+
"agent": self.agent,
|
|
46
|
+
"domain": self.domain,
|
|
47
|
+
"reason": self.reason,
|
|
48
|
+
"is_allowed": self.is_allowed,
|
|
49
|
+
"allowed_domains": self.allowed_domains,
|
|
50
|
+
"forbidden_domains": self.forbidden_domains,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ScopeValidator:
|
|
55
|
+
"""
|
|
56
|
+
Validates that learning requests are within agent scope.
|
|
57
|
+
|
|
58
|
+
Provides strict enforcement with detailed reporting.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def __init__(
|
|
62
|
+
self,
|
|
63
|
+
scopes: Dict[str, MemoryScope],
|
|
64
|
+
strict_mode: bool = True,
|
|
65
|
+
allow_unknown_agents: bool = False,
|
|
66
|
+
):
|
|
67
|
+
"""
|
|
68
|
+
Initialize validator.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
scopes: Dict of agent_name -> MemoryScope
|
|
72
|
+
strict_mode: If True, deny requests for unknown domains
|
|
73
|
+
allow_unknown_agents: If True, allow learning for agents without scopes
|
|
74
|
+
"""
|
|
75
|
+
self.scopes = scopes
|
|
76
|
+
self.strict_mode = strict_mode
|
|
77
|
+
self.allow_unknown_agents = allow_unknown_agents
|
|
78
|
+
|
|
79
|
+
# Track validation statistics
|
|
80
|
+
self._stats = {
|
|
81
|
+
"total_validations": 0,
|
|
82
|
+
"allowed": 0,
|
|
83
|
+
"denied": 0,
|
|
84
|
+
"warnings": 0,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
def validate(
|
|
88
|
+
self,
|
|
89
|
+
agent: str,
|
|
90
|
+
domain: str,
|
|
91
|
+
task_type: Optional[str] = None,
|
|
92
|
+
) -> ValidationReport:
|
|
93
|
+
"""
|
|
94
|
+
Validate a learning request.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
agent: Agent attempting to learn
|
|
98
|
+
domain: Knowledge domain to learn in
|
|
99
|
+
task_type: Optional task type for context
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
ValidationReport with detailed results
|
|
103
|
+
"""
|
|
104
|
+
self._stats["total_validations"] += 1
|
|
105
|
+
|
|
106
|
+
# Check if agent has a scope
|
|
107
|
+
scope = self.scopes.get(agent)
|
|
108
|
+
|
|
109
|
+
if scope is None:
|
|
110
|
+
if self.allow_unknown_agents:
|
|
111
|
+
self._stats["warnings"] += 1
|
|
112
|
+
logger.warning(f"Agent '{agent}' has no defined scope, allowing anyway")
|
|
113
|
+
return ValidationReport(
|
|
114
|
+
result=ValidationResult.WARNING_NO_SCOPE,
|
|
115
|
+
agent=agent,
|
|
116
|
+
domain=domain,
|
|
117
|
+
reason=f"Agent '{agent}' has no defined scope",
|
|
118
|
+
)
|
|
119
|
+
else:
|
|
120
|
+
self._stats["denied"] += 1
|
|
121
|
+
logger.warning(f"Agent '{agent}' denied: no scope defined")
|
|
122
|
+
return ValidationReport(
|
|
123
|
+
result=ValidationResult.DENIED_UNKNOWN_AGENT,
|
|
124
|
+
agent=agent,
|
|
125
|
+
domain=domain,
|
|
126
|
+
reason=f"Agent '{agent}' has no defined scope and unknown agents are not allowed",
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Check if domain is explicitly forbidden
|
|
130
|
+
if domain in scope.cannot_learn:
|
|
131
|
+
self._stats["denied"] += 1
|
|
132
|
+
logger.warning(
|
|
133
|
+
f"Agent '{agent}' denied learning in '{domain}': explicitly forbidden"
|
|
134
|
+
)
|
|
135
|
+
return ValidationReport(
|
|
136
|
+
result=ValidationResult.DENIED_FORBIDDEN,
|
|
137
|
+
agent=agent,
|
|
138
|
+
domain=domain,
|
|
139
|
+
reason=f"Domain '{domain}' is explicitly forbidden for agent '{agent}'",
|
|
140
|
+
allowed_domains=scope.can_learn,
|
|
141
|
+
forbidden_domains=scope.cannot_learn,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# Check if domain is in allowed list
|
|
145
|
+
if scope.can_learn: # If list is not empty, it's an allowlist
|
|
146
|
+
if domain not in scope.can_learn:
|
|
147
|
+
# Check for partial matches (e.g., "form_testing" contains "testing")
|
|
148
|
+
partial_match = any(
|
|
149
|
+
allowed in domain or domain in allowed
|
|
150
|
+
for allowed in scope.can_learn
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
if not partial_match and self.strict_mode:
|
|
154
|
+
self._stats["denied"] += 1
|
|
155
|
+
logger.warning(
|
|
156
|
+
f"Agent '{agent}' denied learning in '{domain}': not in allowed list"
|
|
157
|
+
)
|
|
158
|
+
return ValidationReport(
|
|
159
|
+
result=ValidationResult.DENIED_OUT_OF_SCOPE,
|
|
160
|
+
agent=agent,
|
|
161
|
+
domain=domain,
|
|
162
|
+
reason=f"Domain '{domain}' is not in agent '{agent}'s allowed domains",
|
|
163
|
+
allowed_domains=scope.can_learn,
|
|
164
|
+
forbidden_domains=scope.cannot_learn,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# Allowed
|
|
168
|
+
self._stats["allowed"] += 1
|
|
169
|
+
return ValidationReport(
|
|
170
|
+
result=ValidationResult.ALLOWED,
|
|
171
|
+
agent=agent,
|
|
172
|
+
domain=domain,
|
|
173
|
+
reason="Learning allowed",
|
|
174
|
+
allowed_domains=scope.can_learn,
|
|
175
|
+
forbidden_domains=scope.cannot_learn,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
def validate_batch(
|
|
179
|
+
self,
|
|
180
|
+
agent: str,
|
|
181
|
+
domains: List[str],
|
|
182
|
+
) -> Dict[str, ValidationReport]:
|
|
183
|
+
"""
|
|
184
|
+
Validate multiple domains at once.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
agent: Agent attempting to learn
|
|
188
|
+
domains: List of domains to validate
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
Dict of domain -> ValidationReport
|
|
192
|
+
"""
|
|
193
|
+
return {domain: self.validate(agent, domain) for domain in domains}
|
|
194
|
+
|
|
195
|
+
def get_allowed_domains(self, agent: str) -> Set[str]:
|
|
196
|
+
"""Get all allowed domains for an agent."""
|
|
197
|
+
scope = self.scopes.get(agent)
|
|
198
|
+
if scope is None:
|
|
199
|
+
return set()
|
|
200
|
+
return set(scope.can_learn)
|
|
201
|
+
|
|
202
|
+
def get_forbidden_domains(self, agent: str) -> Set[str]:
|
|
203
|
+
"""Get all forbidden domains for an agent."""
|
|
204
|
+
scope = self.scopes.get(agent)
|
|
205
|
+
if scope is None:
|
|
206
|
+
return set()
|
|
207
|
+
return set(scope.cannot_learn)
|
|
208
|
+
|
|
209
|
+
def is_allowed(self, agent: str, domain: str) -> bool:
|
|
210
|
+
"""Quick check if learning is allowed (no detailed report)."""
|
|
211
|
+
return self.validate(agent, domain).is_allowed
|
|
212
|
+
|
|
213
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
214
|
+
"""Get validation statistics."""
|
|
215
|
+
total = self._stats["total_validations"]
|
|
216
|
+
return {
|
|
217
|
+
**self._stats,
|
|
218
|
+
"allow_rate": self._stats["allowed"] / total if total > 0 else 0,
|
|
219
|
+
"deny_rate": self._stats["denied"] / total if total > 0 else 0,
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
def reset_stats(self):
|
|
223
|
+
"""Reset validation statistics."""
|
|
224
|
+
self._stats = {
|
|
225
|
+
"total_validations": 0,
|
|
226
|
+
"allowed": 0,
|
|
227
|
+
"denied": 0,
|
|
228
|
+
"warnings": 0,
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class TaskTypeValidator:
|
|
233
|
+
"""
|
|
234
|
+
Validates and normalizes task types.
|
|
235
|
+
|
|
236
|
+
Ensures consistent categorization across learning events.
|
|
237
|
+
"""
|
|
238
|
+
|
|
239
|
+
# Standard task type categories
|
|
240
|
+
STANDARD_TYPES = {
|
|
241
|
+
"testing": ["test", "validate", "verify", "check", "qa"],
|
|
242
|
+
"form_testing": ["form", "input", "field", "validation"],
|
|
243
|
+
"api_testing": ["api", "endpoint", "rest", "graphql", "request"],
|
|
244
|
+
"database_validation": ["database", "query", "sql", "schema"],
|
|
245
|
+
"ui_testing": ["ui", "component", "button", "click", "element"],
|
|
246
|
+
"performance_testing": ["performance", "load", "stress", "speed"],
|
|
247
|
+
"security_testing": ["security", "auth", "permission", "xss", "injection"],
|
|
248
|
+
"accessibility_testing": ["accessibility", "a11y", "aria", "screen reader"],
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
def __init__(self, custom_types: Optional[Dict[str, List[str]]] = None):
|
|
252
|
+
"""
|
|
253
|
+
Initialize validator.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
custom_types: Additional custom type mappings
|
|
257
|
+
"""
|
|
258
|
+
self.type_mappings = {**self.STANDARD_TYPES}
|
|
259
|
+
if custom_types:
|
|
260
|
+
self.type_mappings.update(custom_types)
|
|
261
|
+
|
|
262
|
+
# Build reverse lookup
|
|
263
|
+
self._keyword_to_type: Dict[str, str] = {}
|
|
264
|
+
for task_type, keywords in self.type_mappings.items():
|
|
265
|
+
for keyword in keywords:
|
|
266
|
+
self._keyword_to_type[keyword.lower()] = task_type
|
|
267
|
+
|
|
268
|
+
def infer_type(self, task_description: str) -> str:
|
|
269
|
+
"""
|
|
270
|
+
Infer task type from description.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
task_description: Description of the task
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
Inferred task type or "general"
|
|
277
|
+
"""
|
|
278
|
+
task_lower = task_description.lower()
|
|
279
|
+
|
|
280
|
+
# Check for keyword matches
|
|
281
|
+
scores: Dict[str, int] = {}
|
|
282
|
+
for task_type, keywords in self.type_mappings.items():
|
|
283
|
+
score = sum(1 for kw in keywords if kw in task_lower)
|
|
284
|
+
if score > 0:
|
|
285
|
+
scores[task_type] = score
|
|
286
|
+
|
|
287
|
+
if scores:
|
|
288
|
+
# Return the type with highest score
|
|
289
|
+
return max(scores.keys(), key=lambda k: scores[k])
|
|
290
|
+
|
|
291
|
+
return "general"
|
|
292
|
+
|
|
293
|
+
def normalize_type(self, task_type: str) -> str:
|
|
294
|
+
"""
|
|
295
|
+
Normalize a task type to standard category.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
task_type: Raw task type
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
Normalized task type
|
|
302
|
+
"""
|
|
303
|
+
type_lower = task_type.lower().replace("_", " ").replace("-", " ")
|
|
304
|
+
|
|
305
|
+
# Check direct match
|
|
306
|
+
if type_lower in self.type_mappings:
|
|
307
|
+
return type_lower
|
|
308
|
+
|
|
309
|
+
# Check keyword lookup
|
|
310
|
+
for word in type_lower.split():
|
|
311
|
+
if word in self._keyword_to_type:
|
|
312
|
+
return self._keyword_to_type[word]
|
|
313
|
+
|
|
314
|
+
return task_type # Return original if no match
|
|
315
|
+
|
|
316
|
+
def validate_type(self, task_type: str) -> bool:
|
|
317
|
+
"""Check if task type is recognized."""
|
|
318
|
+
normalized = self.normalize_type(task_type)
|
|
319
|
+
return normalized in self.type_mappings or task_type == "general"
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def validate_learning_request(
|
|
323
|
+
agent: str,
|
|
324
|
+
domain: str,
|
|
325
|
+
scopes: Dict[str, MemoryScope],
|
|
326
|
+
strict: bool = True,
|
|
327
|
+
) -> ValidationReport:
|
|
328
|
+
"""
|
|
329
|
+
Convenience function for one-off validation.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
agent: Agent attempting to learn
|
|
333
|
+
domain: Knowledge domain
|
|
334
|
+
scopes: Dict of agent scopes
|
|
335
|
+
strict: Use strict mode
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
ValidationReport
|
|
339
|
+
"""
|
|
340
|
+
validator = ScopeValidator(scopes, strict_mode=strict)
|
|
341
|
+
return validator.validate(agent, domain)
|
alma/mcp/__init__.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ALMA MCP Server Module.
|
|
3
|
+
|
|
4
|
+
Exposes ALMA functionality to any Claude Code instance via the
|
|
5
|
+
Model Context Protocol (MCP).
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
# stdio mode (for Claude Code integration)
|
|
9
|
+
python -m alma.mcp --config .alma/config.yaml
|
|
10
|
+
|
|
11
|
+
# HTTP mode (for remote access)
|
|
12
|
+
python -m alma.mcp --http --port 8765
|
|
13
|
+
|
|
14
|
+
Integration with Claude Code (.mcp.json):
|
|
15
|
+
{
|
|
16
|
+
"mcpServers": {
|
|
17
|
+
"alma-memory": {
|
|
18
|
+
"command": "python",
|
|
19
|
+
"args": ["-m", "alma.mcp", "--config", ".alma/config.yaml"]
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from alma.mcp.server import ALMAMCPServer
|
|
26
|
+
from alma.mcp.tools import (
|
|
27
|
+
alma_retrieve,
|
|
28
|
+
alma_learn,
|
|
29
|
+
alma_add_preference,
|
|
30
|
+
alma_add_knowledge,
|
|
31
|
+
alma_forget,
|
|
32
|
+
alma_stats,
|
|
33
|
+
alma_health,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"ALMAMCPServer",
|
|
38
|
+
"alma_retrieve",
|
|
39
|
+
"alma_learn",
|
|
40
|
+
"alma_add_preference",
|
|
41
|
+
"alma_add_knowledge",
|
|
42
|
+
"alma_forget",
|
|
43
|
+
"alma_stats",
|
|
44
|
+
"alma_health",
|
|
45
|
+
]
|
alma/mcp/__main__.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ALMA MCP Server CLI Entry Point.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
# stdio mode (for Claude Code)
|
|
6
|
+
python -m alma.mcp --config .alma/config.yaml
|
|
7
|
+
|
|
8
|
+
# HTTP mode (for remote access)
|
|
9
|
+
python -m alma.mcp --http --port 8765
|
|
10
|
+
|
|
11
|
+
# With verbose logging
|
|
12
|
+
python -m alma.mcp --config .alma/config.yaml --verbose
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import asyncio
|
|
17
|
+
import logging
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from alma import ALMA
|
|
22
|
+
from alma.mcp.server import ALMAMCPServer
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def setup_logging(verbose: bool = False):
|
|
26
|
+
"""Configure logging."""
|
|
27
|
+
level = logging.DEBUG if verbose else logging.INFO
|
|
28
|
+
logging.basicConfig(
|
|
29
|
+
level=level,
|
|
30
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
31
|
+
stream=sys.stderr, # Log to stderr to avoid interfering with stdio
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def main():
|
|
36
|
+
"""Main entry point."""
|
|
37
|
+
parser = argparse.ArgumentParser(
|
|
38
|
+
description="ALMA MCP Server - Memory for AI Agents",
|
|
39
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
40
|
+
epilog="""
|
|
41
|
+
Examples:
|
|
42
|
+
# Start in stdio mode for Claude Code
|
|
43
|
+
python -m alma.mcp --config .alma/config.yaml
|
|
44
|
+
|
|
45
|
+
# Start in HTTP mode on port 8765
|
|
46
|
+
python -m alma.mcp --http --port 8765
|
|
47
|
+
|
|
48
|
+
# Use with verbose logging
|
|
49
|
+
python -m alma.mcp --config .alma/config.yaml --verbose
|
|
50
|
+
""",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
parser.add_argument(
|
|
54
|
+
"--config",
|
|
55
|
+
type=str,
|
|
56
|
+
default=".alma/config.yaml",
|
|
57
|
+
help="Path to ALMA config file (default: .alma/config.yaml)",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
parser.add_argument(
|
|
61
|
+
"--http",
|
|
62
|
+
action="store_true",
|
|
63
|
+
help="Run in HTTP mode instead of stdio",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
parser.add_argument(
|
|
67
|
+
"--host",
|
|
68
|
+
type=str,
|
|
69
|
+
default="0.0.0.0",
|
|
70
|
+
help="HTTP server host (default: 0.0.0.0)",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
parser.add_argument(
|
|
74
|
+
"--port",
|
|
75
|
+
type=int,
|
|
76
|
+
default=8765,
|
|
77
|
+
help="HTTP server port (default: 8765)",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
parser.add_argument(
|
|
81
|
+
"--verbose", "-v",
|
|
82
|
+
action="store_true",
|
|
83
|
+
help="Enable verbose logging",
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
args = parser.parse_args()
|
|
87
|
+
|
|
88
|
+
# Setup logging
|
|
89
|
+
setup_logging(args.verbose)
|
|
90
|
+
logger = logging.getLogger(__name__)
|
|
91
|
+
|
|
92
|
+
# Load ALMA from config
|
|
93
|
+
config_path = Path(args.config)
|
|
94
|
+
if not config_path.exists():
|
|
95
|
+
logger.error(f"Config file not found: {config_path}")
|
|
96
|
+
logger.info("Creating default config...")
|
|
97
|
+
|
|
98
|
+
# Create minimal default config
|
|
99
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
100
|
+
with open(config_path, "w") as f:
|
|
101
|
+
f.write("""# ALMA Configuration
|
|
102
|
+
alma:
|
|
103
|
+
project_id: "default"
|
|
104
|
+
storage: "file"
|
|
105
|
+
storage_dir: ".alma"
|
|
106
|
+
embedding_provider: "local"
|
|
107
|
+
|
|
108
|
+
agents:
|
|
109
|
+
helena:
|
|
110
|
+
can_learn:
|
|
111
|
+
- testing_strategies
|
|
112
|
+
- selector_patterns
|
|
113
|
+
- form_testing
|
|
114
|
+
- accessibility_testing
|
|
115
|
+
cannot_learn:
|
|
116
|
+
- backend_logic
|
|
117
|
+
- database_queries
|
|
118
|
+
min_occurrences_for_heuristic: 3
|
|
119
|
+
|
|
120
|
+
victor:
|
|
121
|
+
can_learn:
|
|
122
|
+
- api_design_patterns
|
|
123
|
+
- authentication_patterns
|
|
124
|
+
- error_handling
|
|
125
|
+
- database_query_patterns
|
|
126
|
+
cannot_learn:
|
|
127
|
+
- frontend_styling
|
|
128
|
+
- ui_testing
|
|
129
|
+
min_occurrences_for_heuristic: 3
|
|
130
|
+
""")
|
|
131
|
+
logger.info(f"Created default config at {config_path}")
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
alma = ALMA.from_config(str(config_path))
|
|
135
|
+
logger.info(f"Loaded ALMA from {config_path}")
|
|
136
|
+
logger.info(f"Project: {alma.project_id}")
|
|
137
|
+
logger.info(f"Agents: {list(alma.scopes.keys())}")
|
|
138
|
+
|
|
139
|
+
except Exception as e:
|
|
140
|
+
logger.exception(f"Failed to load ALMA: {e}")
|
|
141
|
+
sys.exit(1)
|
|
142
|
+
|
|
143
|
+
# Create and run server
|
|
144
|
+
server = ALMAMCPServer(alma=alma)
|
|
145
|
+
|
|
146
|
+
if args.http:
|
|
147
|
+
logger.info(f"Starting HTTP server on {args.host}:{args.port}")
|
|
148
|
+
asyncio.run(server.run_http(host=args.host, port=args.port))
|
|
149
|
+
else:
|
|
150
|
+
logger.info("Starting stdio server for Claude Code integration")
|
|
151
|
+
asyncio.run(server.run_stdio())
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
if __name__ == "__main__":
|
|
155
|
+
main()
|
alma/mcp/resources.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ALMA MCP Resource Definitions.
|
|
3
|
+
|
|
4
|
+
Provides read-only resources that can be accessed via MCP protocol.
|
|
5
|
+
Resources represent configuration and metadata about the ALMA instance.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Dict, Any, List
|
|
10
|
+
|
|
11
|
+
from alma import ALMA
|
|
12
|
+
from alma.types import MemoryScope
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_config_resource(alma: ALMA) -> Dict[str, Any]:
|
|
18
|
+
"""
|
|
19
|
+
Get current ALMA configuration as a resource.
|
|
20
|
+
|
|
21
|
+
This exposes non-sensitive configuration information about
|
|
22
|
+
the ALMA instance.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
alma: ALMA instance
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Dict with configuration details
|
|
29
|
+
"""
|
|
30
|
+
return {
|
|
31
|
+
"uri": "alma://config",
|
|
32
|
+
"name": "ALMA Configuration",
|
|
33
|
+
"description": "Current configuration of the ALMA memory system",
|
|
34
|
+
"mimeType": "application/json",
|
|
35
|
+
"content": {
|
|
36
|
+
"project_id": alma.project_id,
|
|
37
|
+
"storage_type": type(alma.storage).__name__,
|
|
38
|
+
"registered_agents": list(alma.scopes.keys()),
|
|
39
|
+
"scopes": {
|
|
40
|
+
name: {
|
|
41
|
+
"can_learn": scope.can_learn,
|
|
42
|
+
"cannot_learn": scope.cannot_learn,
|
|
43
|
+
"min_occurrences_for_heuristic": scope.min_occurrences_for_heuristic,
|
|
44
|
+
}
|
|
45
|
+
for name, scope in alma.scopes.items()
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def get_agents_resource(alma: ALMA) -> Dict[str, Any]:
|
|
52
|
+
"""
|
|
53
|
+
Get registered agents and their scopes as a resource.
|
|
54
|
+
|
|
55
|
+
This provides detailed information about each registered agent
|
|
56
|
+
and their learning permissions.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
alma: ALMA instance
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Dict with agent details
|
|
63
|
+
"""
|
|
64
|
+
agents = []
|
|
65
|
+
|
|
66
|
+
for name, scope in alma.scopes.items():
|
|
67
|
+
# Get stats for this agent
|
|
68
|
+
try:
|
|
69
|
+
stats = alma.get_stats(agent=name)
|
|
70
|
+
except Exception:
|
|
71
|
+
stats = {}
|
|
72
|
+
|
|
73
|
+
agents.append({
|
|
74
|
+
"name": name,
|
|
75
|
+
"scope": {
|
|
76
|
+
"can_learn": scope.can_learn,
|
|
77
|
+
"cannot_learn": scope.cannot_learn,
|
|
78
|
+
"min_occurrences_for_heuristic": scope.min_occurrences_for_heuristic,
|
|
79
|
+
},
|
|
80
|
+
"stats": {
|
|
81
|
+
"heuristics_count": stats.get("heuristics_count", 0),
|
|
82
|
+
"outcomes_count": stats.get("outcomes_count", 0),
|
|
83
|
+
"domain_knowledge_count": stats.get("domain_knowledge_count", 0),
|
|
84
|
+
"anti_patterns_count": stats.get("anti_patterns_count", 0),
|
|
85
|
+
},
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
"uri": "alma://agents",
|
|
90
|
+
"name": "ALMA Registered Agents",
|
|
91
|
+
"description": "All registered agents and their memory scopes",
|
|
92
|
+
"mimeType": "application/json",
|
|
93
|
+
"content": {
|
|
94
|
+
"project_id": alma.project_id,
|
|
95
|
+
"agent_count": len(agents),
|
|
96
|
+
"agents": agents,
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def list_resources() -> List[Dict[str, Any]]:
|
|
102
|
+
"""
|
|
103
|
+
List all available resources.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
List of resource descriptors
|
|
107
|
+
"""
|
|
108
|
+
return [
|
|
109
|
+
{
|
|
110
|
+
"uri": "alma://config",
|
|
111
|
+
"name": "ALMA Configuration",
|
|
112
|
+
"description": "Current configuration of the ALMA memory system",
|
|
113
|
+
"mimeType": "application/json",
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"uri": "alma://agents",
|
|
117
|
+
"name": "ALMA Registered Agents",
|
|
118
|
+
"description": "All registered agents and their memory scopes",
|
|
119
|
+
"mimeType": "application/json",
|
|
120
|
+
},
|
|
121
|
+
]
|