alma-memory 0.5.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 +33 -1
- alma/core.py +124 -16
- alma/extraction/auto_learner.py +4 -3
- alma/graph/__init__.py +26 -1
- alma/graph/backends/__init__.py +14 -0
- alma/graph/backends/kuzu.py +624 -0
- alma/graph/backends/memgraph.py +432 -0
- alma/integration/claude_agents.py +22 -10
- alma/learning/protocols.py +3 -3
- alma/mcp/tools.py +9 -11
- 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/retrieval/engine.py +65 -4
- alma/storage/__init__.py +29 -0
- alma/storage/azure_cosmos.py +343 -132
- alma/storage/base.py +58 -0
- alma/storage/constants.py +103 -0
- alma/storage/file_based.py +3 -8
- 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/postgresql.py +185 -78
- alma/storage/sqlite_local.py +149 -50
- alma/testing/__init__.py +46 -0
- alma/testing/factories.py +301 -0
- alma/testing/mocks.py +389 -0
- {alma_memory-0.5.0.dist-info → alma_memory-0.5.1.dist-info}/METADATA +42 -8
- {alma_memory-0.5.0.dist-info → alma_memory-0.5.1.dist-info}/RECORD +36 -19
- {alma_memory-0.5.0.dist-info → alma_memory-0.5.1.dist-info}/WHEEL +0 -0
- {alma_memory-0.5.0.dist-info → alma_memory-0.5.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ALMA Test Factories.
|
|
3
|
+
|
|
4
|
+
Provides factory functions for creating test data with sensible defaults.
|
|
5
|
+
All factory functions accept keyword arguments to override any field.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import uuid
|
|
9
|
+
from datetime import datetime, timedelta, timezone
|
|
10
|
+
from typing import Any, Dict, List, Optional
|
|
11
|
+
|
|
12
|
+
from alma.types import (
|
|
13
|
+
AntiPattern,
|
|
14
|
+
DomainKnowledge,
|
|
15
|
+
Heuristic,
|
|
16
|
+
Outcome,
|
|
17
|
+
UserPreference,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"create_test_heuristic",
|
|
22
|
+
"create_test_outcome",
|
|
23
|
+
"create_test_preference",
|
|
24
|
+
"create_test_knowledge",
|
|
25
|
+
"create_test_anti_pattern",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def create_test_heuristic(
|
|
30
|
+
id: Optional[str] = None,
|
|
31
|
+
agent: str = "test-agent",
|
|
32
|
+
project_id: str = "test-project",
|
|
33
|
+
condition: str = "test condition",
|
|
34
|
+
strategy: str = "test strategy",
|
|
35
|
+
confidence: float = 0.85,
|
|
36
|
+
occurrence_count: int = 10,
|
|
37
|
+
success_count: int = 8,
|
|
38
|
+
last_validated: Optional[datetime] = None,
|
|
39
|
+
created_at: Optional[datetime] = None,
|
|
40
|
+
embedding: Optional[List[float]] = None,
|
|
41
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
42
|
+
) -> Heuristic:
|
|
43
|
+
"""
|
|
44
|
+
Create a test Heuristic with sensible defaults.
|
|
45
|
+
|
|
46
|
+
All parameters can be overridden to customize the test data.
|
|
47
|
+
|
|
48
|
+
Example:
|
|
49
|
+
>>> heuristic = create_test_heuristic(agent="helena", confidence=0.95)
|
|
50
|
+
>>> assert heuristic.agent == "helena"
|
|
51
|
+
>>> assert heuristic.confidence == 0.95
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
id: Unique identifier (auto-generated if not provided)
|
|
55
|
+
agent: Agent name that owns this heuristic
|
|
56
|
+
project_id: Project identifier
|
|
57
|
+
condition: When this heuristic applies
|
|
58
|
+
strategy: What strategy to use
|
|
59
|
+
confidence: Confidence score (0.0-1.0)
|
|
60
|
+
occurrence_count: Number of times this heuristic has been observed
|
|
61
|
+
success_count: Number of successful applications
|
|
62
|
+
last_validated: Last validation timestamp
|
|
63
|
+
created_at: Creation timestamp
|
|
64
|
+
embedding: Optional embedding vector
|
|
65
|
+
metadata: Additional metadata
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
A fully populated Heuristic instance
|
|
69
|
+
"""
|
|
70
|
+
now = datetime.now(timezone.utc)
|
|
71
|
+
return Heuristic(
|
|
72
|
+
id=id or str(uuid.uuid4()),
|
|
73
|
+
agent=agent,
|
|
74
|
+
project_id=project_id,
|
|
75
|
+
condition=condition,
|
|
76
|
+
strategy=strategy,
|
|
77
|
+
confidence=confidence,
|
|
78
|
+
occurrence_count=occurrence_count,
|
|
79
|
+
success_count=success_count,
|
|
80
|
+
last_validated=last_validated or now,
|
|
81
|
+
created_at=created_at or now - timedelta(days=7),
|
|
82
|
+
embedding=embedding,
|
|
83
|
+
metadata=metadata or {},
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def create_test_outcome(
|
|
88
|
+
id: Optional[str] = None,
|
|
89
|
+
agent: str = "test-agent",
|
|
90
|
+
project_id: str = "test-project",
|
|
91
|
+
task_type: str = "test_task",
|
|
92
|
+
task_description: str = "Test task description",
|
|
93
|
+
success: bool = True,
|
|
94
|
+
strategy_used: str = "test strategy",
|
|
95
|
+
duration_ms: Optional[int] = 500,
|
|
96
|
+
error_message: Optional[str] = None,
|
|
97
|
+
user_feedback: Optional[str] = None,
|
|
98
|
+
timestamp: Optional[datetime] = None,
|
|
99
|
+
embedding: Optional[List[float]] = None,
|
|
100
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
101
|
+
) -> Outcome:
|
|
102
|
+
"""
|
|
103
|
+
Create a test Outcome with sensible defaults.
|
|
104
|
+
|
|
105
|
+
All parameters can be overridden to customize the test data.
|
|
106
|
+
|
|
107
|
+
Example:
|
|
108
|
+
>>> outcome = create_test_outcome(success=False, error_message="Failed")
|
|
109
|
+
>>> assert outcome.success is False
|
|
110
|
+
>>> assert outcome.error_message == "Failed"
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
id: Unique identifier (auto-generated if not provided)
|
|
114
|
+
agent: Agent name that produced this outcome
|
|
115
|
+
project_id: Project identifier
|
|
116
|
+
task_type: Type of task (e.g., "api_validation")
|
|
117
|
+
task_description: Description of the task
|
|
118
|
+
success: Whether the task succeeded
|
|
119
|
+
strategy_used: Strategy that was applied
|
|
120
|
+
duration_ms: Task duration in milliseconds
|
|
121
|
+
error_message: Error message if failed
|
|
122
|
+
user_feedback: Optional user feedback
|
|
123
|
+
timestamp: When the outcome occurred
|
|
124
|
+
embedding: Optional embedding vector
|
|
125
|
+
metadata: Additional metadata
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
A fully populated Outcome instance
|
|
129
|
+
"""
|
|
130
|
+
return Outcome(
|
|
131
|
+
id=id or str(uuid.uuid4()),
|
|
132
|
+
agent=agent,
|
|
133
|
+
project_id=project_id,
|
|
134
|
+
task_type=task_type,
|
|
135
|
+
task_description=task_description,
|
|
136
|
+
success=success,
|
|
137
|
+
strategy_used=strategy_used,
|
|
138
|
+
duration_ms=duration_ms,
|
|
139
|
+
error_message=error_message,
|
|
140
|
+
user_feedback=user_feedback,
|
|
141
|
+
timestamp=timestamp or datetime.now(timezone.utc),
|
|
142
|
+
embedding=embedding,
|
|
143
|
+
metadata=metadata or {},
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def create_test_preference(
|
|
148
|
+
id: Optional[str] = None,
|
|
149
|
+
user_id: str = "test-user",
|
|
150
|
+
category: str = "code_style",
|
|
151
|
+
preference: str = "Test preference value",
|
|
152
|
+
source: str = "explicit_instruction",
|
|
153
|
+
confidence: float = 1.0,
|
|
154
|
+
timestamp: Optional[datetime] = None,
|
|
155
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
156
|
+
) -> UserPreference:
|
|
157
|
+
"""
|
|
158
|
+
Create a test UserPreference with sensible defaults.
|
|
159
|
+
|
|
160
|
+
All parameters can be overridden to customize the test data.
|
|
161
|
+
|
|
162
|
+
Example:
|
|
163
|
+
>>> pref = create_test_preference(
|
|
164
|
+
... category="communication",
|
|
165
|
+
... preference="No emojis"
|
|
166
|
+
... )
|
|
167
|
+
>>> assert pref.category == "communication"
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
id: Unique identifier (auto-generated if not provided)
|
|
171
|
+
user_id: User identifier
|
|
172
|
+
category: Preference category (e.g., "code_style", "communication")
|
|
173
|
+
preference: The actual preference text
|
|
174
|
+
source: How this preference was learned
|
|
175
|
+
confidence: Confidence in this preference (0.0-1.0)
|
|
176
|
+
timestamp: When the preference was recorded
|
|
177
|
+
metadata: Additional metadata
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
A fully populated UserPreference instance
|
|
181
|
+
"""
|
|
182
|
+
return UserPreference(
|
|
183
|
+
id=id or str(uuid.uuid4()),
|
|
184
|
+
user_id=user_id,
|
|
185
|
+
category=category,
|
|
186
|
+
preference=preference,
|
|
187
|
+
source=source,
|
|
188
|
+
confidence=confidence,
|
|
189
|
+
timestamp=timestamp or datetime.now(timezone.utc),
|
|
190
|
+
metadata=metadata or {},
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def create_test_knowledge(
|
|
195
|
+
id: Optional[str] = None,
|
|
196
|
+
agent: str = "test-agent",
|
|
197
|
+
project_id: str = "test-project",
|
|
198
|
+
domain: str = "test_domain",
|
|
199
|
+
fact: str = "Test domain fact",
|
|
200
|
+
source: str = "test_source",
|
|
201
|
+
confidence: float = 1.0,
|
|
202
|
+
last_verified: Optional[datetime] = None,
|
|
203
|
+
embedding: Optional[List[float]] = None,
|
|
204
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
205
|
+
) -> DomainKnowledge:
|
|
206
|
+
"""
|
|
207
|
+
Create a test DomainKnowledge with sensible defaults.
|
|
208
|
+
|
|
209
|
+
All parameters can be overridden to customize the test data.
|
|
210
|
+
|
|
211
|
+
Example:
|
|
212
|
+
>>> knowledge = create_test_knowledge(
|
|
213
|
+
... domain="authentication",
|
|
214
|
+
... fact="JWT tokens expire in 24h"
|
|
215
|
+
... )
|
|
216
|
+
>>> assert knowledge.domain == "authentication"
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
id: Unique identifier (auto-generated if not provided)
|
|
220
|
+
agent: Agent name that owns this knowledge
|
|
221
|
+
project_id: Project identifier
|
|
222
|
+
domain: Knowledge domain (e.g., "authentication", "database")
|
|
223
|
+
fact: The factual information
|
|
224
|
+
source: How this knowledge was acquired
|
|
225
|
+
confidence: Confidence in this knowledge (0.0-1.0)
|
|
226
|
+
last_verified: Last verification timestamp
|
|
227
|
+
embedding: Optional embedding vector
|
|
228
|
+
metadata: Additional metadata
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
A fully populated DomainKnowledge instance
|
|
232
|
+
"""
|
|
233
|
+
return DomainKnowledge(
|
|
234
|
+
id=id or str(uuid.uuid4()),
|
|
235
|
+
agent=agent,
|
|
236
|
+
project_id=project_id,
|
|
237
|
+
domain=domain,
|
|
238
|
+
fact=fact,
|
|
239
|
+
source=source,
|
|
240
|
+
confidence=confidence,
|
|
241
|
+
last_verified=last_verified or datetime.now(timezone.utc),
|
|
242
|
+
embedding=embedding,
|
|
243
|
+
metadata=metadata or {},
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def create_test_anti_pattern(
|
|
248
|
+
id: Optional[str] = None,
|
|
249
|
+
agent: str = "test-agent",
|
|
250
|
+
project_id: str = "test-project",
|
|
251
|
+
pattern: str = "Test anti-pattern",
|
|
252
|
+
why_bad: str = "This is why it's bad",
|
|
253
|
+
better_alternative: str = "Do this instead",
|
|
254
|
+
occurrence_count: int = 3,
|
|
255
|
+
last_seen: Optional[datetime] = None,
|
|
256
|
+
created_at: Optional[datetime] = None,
|
|
257
|
+
embedding: Optional[List[float]] = None,
|
|
258
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
259
|
+
) -> AntiPattern:
|
|
260
|
+
"""
|
|
261
|
+
Create a test AntiPattern with sensible defaults.
|
|
262
|
+
|
|
263
|
+
All parameters can be overridden to customize the test data.
|
|
264
|
+
|
|
265
|
+
Example:
|
|
266
|
+
>>> anti_pattern = create_test_anti_pattern(
|
|
267
|
+
... pattern="Using sleep() for waits",
|
|
268
|
+
... better_alternative="Use explicit waits"
|
|
269
|
+
... )
|
|
270
|
+
>>> assert "sleep" in anti_pattern.pattern
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
id: Unique identifier (auto-generated if not provided)
|
|
274
|
+
agent: Agent name that identified this anti-pattern
|
|
275
|
+
project_id: Project identifier
|
|
276
|
+
pattern: Description of the anti-pattern
|
|
277
|
+
why_bad: Explanation of why this pattern is problematic
|
|
278
|
+
better_alternative: Recommended alternative approach
|
|
279
|
+
occurrence_count: How often this pattern has been observed
|
|
280
|
+
last_seen: Last time this pattern was observed
|
|
281
|
+
created_at: When this anti-pattern was first identified
|
|
282
|
+
embedding: Optional embedding vector
|
|
283
|
+
metadata: Additional metadata
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
A fully populated AntiPattern instance
|
|
287
|
+
"""
|
|
288
|
+
now = datetime.now(timezone.utc)
|
|
289
|
+
return AntiPattern(
|
|
290
|
+
id=id or str(uuid.uuid4()),
|
|
291
|
+
agent=agent,
|
|
292
|
+
project_id=project_id,
|
|
293
|
+
pattern=pattern,
|
|
294
|
+
why_bad=why_bad,
|
|
295
|
+
better_alternative=better_alternative,
|
|
296
|
+
occurrence_count=occurrence_count,
|
|
297
|
+
last_seen=last_seen or now,
|
|
298
|
+
created_at=created_at or now - timedelta(days=3),
|
|
299
|
+
embedding=embedding,
|
|
300
|
+
metadata=metadata or {},
|
|
301
|
+
)
|
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)
|