alma-memory 0.5.1__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 -226
  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 -430
  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 -265
  24. alma/extraction/extractor.py +420 -420
  25. alma/graph/__init__.py +106 -106
  26. alma/graph/backends/__init__.py +32 -32
  27. alma/graph/backends/kuzu.py +624 -624
  28. alma/graph/backends/memgraph.py +432 -432
  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 -444
  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 -509
  55. alma/observability/__init__.py +91 -84
  56. alma/observability/config.py +302 -302
  57. alma/observability/guidelines.py +170 -0
  58. alma/observability/logging.py +424 -424
  59. alma/observability/metrics.py +583 -583
  60. alma/observability/tracing.py +440 -440
  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 -427
  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 -90
  78. alma/storage/archive.py +233 -0
  79. alma/storage/azure_cosmos.py +1259 -1259
  80. alma/storage/base.py +1083 -583
  81. alma/storage/chroma.py +1443 -1443
  82. alma/storage/constants.py +103 -103
  83. alma/storage/file_based.py +614 -614
  84. alma/storage/migrations/__init__.py +21 -21
  85. alma/storage/migrations/base.py +321 -321
  86. alma/storage/migrations/runner.py +323 -323
  87. alma/storage/migrations/version_stores.py +337 -337
  88. alma/storage/migrations/versions/__init__.py +11 -11
  89. alma/storage/migrations/versions/v1_0_0.py +373 -373
  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 -1559
  93. alma/storage/qdrant.py +1306 -1306
  94. alma/storage/sqlite_local.py +3041 -1457
  95. alma/testing/__init__.py +46 -46
  96. alma/testing/factories.py +301 -301
  97. alma/testing/mocks.py +389 -389
  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.1.dist-info → alma_memory-0.7.0.dist-info}/METADATA +210 -72
  108. alma_memory-0.7.0.dist-info/RECORD +112 -0
  109. alma_memory-0.5.1.dist-info/RECORD +0 -93
  110. {alma_memory-0.5.1.dist-info → alma_memory-0.7.0.dist-info}/WHEEL +0 -0
  111. {alma_memory-0.5.1.dist-info → alma_memory-0.7.0.dist-info}/top_level.txt +0 -0
@@ -1,374 +1,374 @@
1
- """
2
- ALMA Learning Protocols.
3
-
4
- Defines how agents learn from outcomes while respecting scope constraints.
5
- """
6
-
7
- import logging
8
- import uuid
9
- from datetime import datetime, timedelta, timezone
10
- from typing import TYPE_CHECKING, Dict, Optional
11
-
12
- from alma.storage.base import StorageBackend
13
- from alma.types import (
14
- AntiPattern,
15
- DomainKnowledge,
16
- Heuristic,
17
- MemoryScope,
18
- Outcome,
19
- UserPreference,
20
- )
21
-
22
- if TYPE_CHECKING:
23
- from alma.retrieval.embeddings import EmbeddingProvider
24
-
25
- logger = logging.getLogger(__name__)
26
-
27
-
28
- class LearningProtocol:
29
- """
30
- Manages how agents learn from task outcomes.
31
-
32
- Key principles:
33
- - Validate scope before any learning
34
- - Require minimum occurrences before creating heuristics
35
- - Support forgetting to prevent memory bloat
36
- """
37
-
38
- def __init__(
39
- self,
40
- storage: StorageBackend,
41
- scopes: Dict[str, MemoryScope],
42
- embedder: Optional["EmbeddingProvider"] = None,
43
- similarity_threshold: float = 0.75,
44
- ):
45
- """
46
- Initialize learning protocol.
47
-
48
- Args:
49
- storage: Storage backend for persistence
50
- scopes: Dict of agent_name -> MemoryScope
51
- embedder: Optional embedding provider for semantic similarity
52
- similarity_threshold: Cosine similarity threshold for strategy matching (default 0.75)
53
- """
54
- self.storage = storage
55
- self.scopes = scopes
56
- self.embedder = embedder
57
- self.similarity_threshold = similarity_threshold
58
-
59
- def learn(
60
- self,
61
- agent: str,
62
- project_id: str,
63
- task: str,
64
- outcome: bool,
65
- strategy_used: str,
66
- task_type: Optional[str] = None,
67
- duration_ms: Optional[int] = None,
68
- error_message: Optional[str] = None,
69
- feedback: Optional[str] = None,
70
- ) -> Outcome:
71
- """
72
- Learn from a task outcome.
73
-
74
- Creates an Outcome record and potentially updates/creates heuristics.
75
-
76
- Args:
77
- agent: Agent that executed the task
78
- project_id: Project context
79
- task: Task description
80
- outcome: True if successful, False if failed
81
- strategy_used: The approach taken
82
- task_type: Category for grouping
83
- duration_ms: Execution time
84
- error_message: Error details if failed
85
- feedback: User feedback
86
-
87
- Returns:
88
- The created Outcome record
89
- """
90
- # Validate agent has a scope (warn but don't block)
91
- scope = self.scopes.get(agent)
92
- if scope is None:
93
- logger.warning(f"Agent '{agent}' has no defined scope")
94
-
95
- # Create outcome record
96
- outcome_record = Outcome(
97
- id=f"out_{uuid.uuid4().hex[:12]}",
98
- agent=agent,
99
- project_id=project_id,
100
- task_type=task_type or self._infer_task_type(task),
101
- task_description=task,
102
- success=outcome,
103
- strategy_used=strategy_used,
104
- duration_ms=duration_ms,
105
- error_message=error_message,
106
- user_feedback=feedback,
107
- timestamp=datetime.now(timezone.utc),
108
- )
109
-
110
- # Save outcome
111
- self.storage.save_outcome(outcome_record)
112
- logger.info(
113
- f"Recorded outcome for {agent}: {'success' if outcome else 'failure'}"
114
- )
115
-
116
- # Check if we should create/update a heuristic
117
- self._maybe_create_heuristic(
118
- agent=agent,
119
- project_id=project_id,
120
- task_type=outcome_record.task_type,
121
- strategy=strategy_used,
122
- success=outcome,
123
- scope=scope,
124
- )
125
-
126
- # If failure with clear pattern, consider anti-pattern
127
- if not outcome and error_message:
128
- self._maybe_create_anti_pattern(
129
- agent=agent,
130
- project_id=project_id,
131
- task=task,
132
- strategy=strategy_used,
133
- error=error_message,
134
- )
135
-
136
- return outcome_record
137
-
138
- def _maybe_create_heuristic(
139
- self,
140
- agent: str,
141
- project_id: str,
142
- task_type: str,
143
- strategy: str,
144
- success: bool,
145
- scope: Optional[MemoryScope],
146
- ):
147
- """
148
- Create or update a heuristic if we have enough occurrences.
149
-
150
- Only creates heuristic after min_occurrences similar outcomes.
151
- """
152
- min_occurrences = 3
153
- if scope:
154
- min_occurrences = scope.min_occurrences_for_heuristic
155
-
156
- # Get similar outcomes to check occurrence count
157
- similar_outcomes = self.storage.get_outcomes(
158
- project_id=project_id,
159
- agent=agent,
160
- task_type=task_type,
161
- top_k=min_occurrences + 1,
162
- success_only=False,
163
- )
164
-
165
- # Filter to same strategy
166
- same_strategy = [
167
- o
168
- for o in similar_outcomes
169
- if self._strategies_similar(o.strategy_used, strategy)
170
- ]
171
-
172
- if len(same_strategy) >= min_occurrences:
173
- success_count = sum(1 for o in same_strategy if o.success)
174
- confidence = success_count / len(same_strategy)
175
-
176
- # Only create heuristic if confidence is meaningful
177
- if confidence > 0.5:
178
- heuristic = Heuristic(
179
- id=f"heur_{uuid.uuid4().hex[:12]}",
180
- agent=agent,
181
- project_id=project_id,
182
- condition=f"task type: {task_type}",
183
- strategy=strategy,
184
- confidence=confidence,
185
- occurrence_count=len(same_strategy),
186
- success_count=success_count,
187
- last_validated=datetime.now(timezone.utc),
188
- created_at=datetime.now(timezone.utc),
189
- )
190
- self.storage.save_heuristic(heuristic)
191
- logger.info(
192
- f"Created heuristic for {agent}: {strategy[:50]}... "
193
- f"(confidence: {confidence:.0%})"
194
- )
195
-
196
- def _maybe_create_anti_pattern(
197
- self,
198
- agent: str,
199
- project_id: str,
200
- task: str,
201
- strategy: str,
202
- error: str,
203
- ):
204
- """Create anti-pattern if we see repeated failures with same pattern."""
205
- # Check for similar failures
206
- similar_failures = self.storage.get_outcomes(
207
- project_id=project_id,
208
- agent=agent,
209
- success_only=False,
210
- top_k=10,
211
- )
212
-
213
- # Filter to failures with similar error
214
- similar = [
215
- o
216
- for o in similar_failures
217
- if not o.success
218
- and o.error_message
219
- and self._errors_similar(o.error_message, error)
220
- ]
221
-
222
- if len(similar) >= 2: # At least 2 similar failures
223
- anti_pattern = AntiPattern(
224
- id=f"anti_{uuid.uuid4().hex[:12]}",
225
- agent=agent,
226
- project_id=project_id,
227
- pattern=strategy,
228
- why_bad=error,
229
- better_alternative="[To be determined from successful outcomes]",
230
- occurrence_count=len(similar),
231
- last_seen=datetime.now(timezone.utc),
232
- )
233
- self.storage.save_anti_pattern(anti_pattern)
234
- logger.info(f"Created anti-pattern for {agent}: {strategy[:50]}...")
235
-
236
- def add_preference(
237
- self,
238
- user_id: str,
239
- category: str,
240
- preference: str,
241
- source: str,
242
- ) -> UserPreference:
243
- """Add a user preference."""
244
- pref = UserPreference(
245
- id=f"pref_{uuid.uuid4().hex[:12]}",
246
- user_id=user_id,
247
- category=category,
248
- preference=preference,
249
- source=source,
250
- confidence=1.0 if source == "explicit_instruction" else 0.7,
251
- timestamp=datetime.now(timezone.utc),
252
- )
253
- self.storage.save_user_preference(pref)
254
- return pref
255
-
256
- def add_domain_knowledge(
257
- self,
258
- agent: str,
259
- project_id: str,
260
- domain: str,
261
- fact: str,
262
- source: str,
263
- ) -> DomainKnowledge:
264
- """Add domain knowledge."""
265
- knowledge = DomainKnowledge(
266
- id=f"dk_{uuid.uuid4().hex[:12]}",
267
- agent=agent,
268
- project_id=project_id,
269
- domain=domain,
270
- fact=fact,
271
- source=source,
272
- confidence=1.0 if source == "user_stated" else 0.8,
273
- last_verified=datetime.now(timezone.utc),
274
- )
275
- self.storage.save_domain_knowledge(knowledge)
276
- return knowledge
277
-
278
- def forget(
279
- self,
280
- project_id: str,
281
- agent: Optional[str] = None,
282
- older_than_days: int = 90,
283
- below_confidence: float = 0.3,
284
- ) -> int:
285
- """
286
- Prune stale and low-confidence memories.
287
-
288
- Returns:
289
- Total number of items pruned
290
- """
291
- cutoff = datetime.now(timezone.utc) - timedelta(days=older_than_days)
292
-
293
- # Delete old outcomes
294
- outcomes_deleted = self.storage.delete_outcomes_older_than(
295
- project_id=project_id,
296
- older_than=cutoff,
297
- agent=agent,
298
- )
299
-
300
- # Delete low-confidence heuristics
301
- heuristics_deleted = self.storage.delete_low_confidence_heuristics(
302
- project_id=project_id,
303
- below_confidence=below_confidence,
304
- agent=agent,
305
- )
306
-
307
- total = outcomes_deleted + heuristics_deleted
308
- logger.info(
309
- f"Forgot {total} items: {outcomes_deleted} outcomes, "
310
- f"{heuristics_deleted} heuristics"
311
- )
312
- return total
313
-
314
- def _infer_task_type(self, task: str) -> str:
315
- """Infer task type from description."""
316
- task_lower = task.lower()
317
- if "test" in task_lower or "validate" in task_lower:
318
- return "testing"
319
- elif "api" in task_lower or "endpoint" in task_lower:
320
- return "api_testing"
321
- elif "form" in task_lower or "input" in task_lower:
322
- return "form_testing"
323
- elif "database" in task_lower or "query" in task_lower:
324
- return "database_validation"
325
- return "general"
326
-
327
- def _strategies_similar(self, s1: str, s2: str) -> bool:
328
- """
329
- Check if two strategies are similar enough to count together.
330
-
331
- Uses embedding-based cosine similarity when an embedder is available,
332
- otherwise falls back to simple word overlap.
333
- """
334
- if self.embedder is not None:
335
- return self._strategies_similar_embedding(s1, s2)
336
- return self._strategies_similar_word_overlap(s1, s2)
337
-
338
- def _strategies_similar_embedding(self, s1: str, s2: str) -> bool:
339
- """Check strategy similarity using embedding cosine similarity."""
340
- try:
341
- emb1 = self.embedder.encode(s1)
342
- emb2 = self.embedder.encode(s2)
343
- similarity = self._cosine_similarity(emb1, emb2)
344
- return similarity >= self.similarity_threshold
345
- except Exception as e:
346
- logger.warning(
347
- f"Embedding similarity failed, falling back to word overlap: {e}"
348
- )
349
- return self._strategies_similar_word_overlap(s1, s2)
350
-
351
- def _strategies_similar_word_overlap(self, s1: str, s2: str) -> bool:
352
- """Check strategy similarity using simple word overlap."""
353
- words1 = set(s1.lower().split())
354
- words2 = set(s2.lower().split())
355
- overlap = len(words1 & words2)
356
- return overlap >= min(3, len(words1) // 2)
357
-
358
- def _cosine_similarity(self, v1: list, v2: list) -> float:
359
- """Compute cosine similarity between two vectors."""
360
- import math
361
-
362
- dot_product = sum(a * b for a, b in zip(v1, v2, strict=False))
363
- norm1 = math.sqrt(sum(a * a for a in v1))
364
- norm2 = math.sqrt(sum(b * b for b in v2))
365
- if norm1 == 0 or norm2 == 0:
366
- return 0.0
367
- return dot_product / (norm1 * norm2)
368
-
369
- def _errors_similar(self, e1: str, e2: str) -> bool:
370
- """Check if two errors are similar."""
371
- # Simple substring check
372
- e1_lower = e1.lower()
373
- e2_lower = e2.lower()
374
- return e1_lower in e2_lower or e2_lower in e1_lower
1
+ """
2
+ ALMA Learning Protocols.
3
+
4
+ Defines how agents learn from outcomes while respecting scope constraints.
5
+ """
6
+
7
+ import logging
8
+ import uuid
9
+ from datetime import datetime, timedelta, timezone
10
+ from typing import TYPE_CHECKING, Dict, Optional
11
+
12
+ from alma.storage.base import StorageBackend
13
+ from alma.types import (
14
+ AntiPattern,
15
+ DomainKnowledge,
16
+ Heuristic,
17
+ MemoryScope,
18
+ Outcome,
19
+ UserPreference,
20
+ )
21
+
22
+ if TYPE_CHECKING:
23
+ from alma.retrieval.embeddings import EmbeddingProvider
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ class LearningProtocol:
29
+ """
30
+ Manages how agents learn from task outcomes.
31
+
32
+ Key principles:
33
+ - Validate scope before any learning
34
+ - Require minimum occurrences before creating heuristics
35
+ - Support forgetting to prevent memory bloat
36
+ """
37
+
38
+ def __init__(
39
+ self,
40
+ storage: StorageBackend,
41
+ scopes: Dict[str, MemoryScope],
42
+ embedder: Optional["EmbeddingProvider"] = None,
43
+ similarity_threshold: float = 0.75,
44
+ ):
45
+ """
46
+ Initialize learning protocol.
47
+
48
+ Args:
49
+ storage: Storage backend for persistence
50
+ scopes: Dict of agent_name -> MemoryScope
51
+ embedder: Optional embedding provider for semantic similarity
52
+ similarity_threshold: Cosine similarity threshold for strategy matching (default 0.75)
53
+ """
54
+ self.storage = storage
55
+ self.scopes = scopes
56
+ self.embedder = embedder
57
+ self.similarity_threshold = similarity_threshold
58
+
59
+ def learn(
60
+ self,
61
+ agent: str,
62
+ project_id: str,
63
+ task: str,
64
+ outcome: bool,
65
+ strategy_used: str,
66
+ task_type: Optional[str] = None,
67
+ duration_ms: Optional[int] = None,
68
+ error_message: Optional[str] = None,
69
+ feedback: Optional[str] = None,
70
+ ) -> Outcome:
71
+ """
72
+ Learn from a task outcome.
73
+
74
+ Creates an Outcome record and potentially updates/creates heuristics.
75
+
76
+ Args:
77
+ agent: Agent that executed the task
78
+ project_id: Project context
79
+ task: Task description
80
+ outcome: True if successful, False if failed
81
+ strategy_used: The approach taken
82
+ task_type: Category for grouping
83
+ duration_ms: Execution time
84
+ error_message: Error details if failed
85
+ feedback: User feedback
86
+
87
+ Returns:
88
+ The created Outcome record
89
+ """
90
+ # Validate agent has a scope (warn but don't block)
91
+ scope = self.scopes.get(agent)
92
+ if scope is None:
93
+ logger.warning(f"Agent '{agent}' has no defined scope")
94
+
95
+ # Create outcome record
96
+ outcome_record = Outcome(
97
+ id=f"out_{uuid.uuid4().hex[:12]}",
98
+ agent=agent,
99
+ project_id=project_id,
100
+ task_type=task_type or self._infer_task_type(task),
101
+ task_description=task,
102
+ success=outcome,
103
+ strategy_used=strategy_used,
104
+ duration_ms=duration_ms,
105
+ error_message=error_message,
106
+ user_feedback=feedback,
107
+ timestamp=datetime.now(timezone.utc),
108
+ )
109
+
110
+ # Save outcome
111
+ self.storage.save_outcome(outcome_record)
112
+ logger.info(
113
+ f"Recorded outcome for {agent}: {'success' if outcome else 'failure'}"
114
+ )
115
+
116
+ # Check if we should create/update a heuristic
117
+ self._maybe_create_heuristic(
118
+ agent=agent,
119
+ project_id=project_id,
120
+ task_type=outcome_record.task_type,
121
+ strategy=strategy_used,
122
+ success=outcome,
123
+ scope=scope,
124
+ )
125
+
126
+ # If failure with clear pattern, consider anti-pattern
127
+ if not outcome and error_message:
128
+ self._maybe_create_anti_pattern(
129
+ agent=agent,
130
+ project_id=project_id,
131
+ task=task,
132
+ strategy=strategy_used,
133
+ error=error_message,
134
+ )
135
+
136
+ return outcome_record
137
+
138
+ def _maybe_create_heuristic(
139
+ self,
140
+ agent: str,
141
+ project_id: str,
142
+ task_type: str,
143
+ strategy: str,
144
+ success: bool,
145
+ scope: Optional[MemoryScope],
146
+ ):
147
+ """
148
+ Create or update a heuristic if we have enough occurrences.
149
+
150
+ Only creates heuristic after min_occurrences similar outcomes.
151
+ """
152
+ min_occurrences = 3
153
+ if scope:
154
+ min_occurrences = scope.min_occurrences_for_heuristic
155
+
156
+ # Get similar outcomes to check occurrence count
157
+ similar_outcomes = self.storage.get_outcomes(
158
+ project_id=project_id,
159
+ agent=agent,
160
+ task_type=task_type,
161
+ top_k=min_occurrences + 1,
162
+ success_only=False,
163
+ )
164
+
165
+ # Filter to same strategy
166
+ same_strategy = [
167
+ o
168
+ for o in similar_outcomes
169
+ if self._strategies_similar(o.strategy_used, strategy)
170
+ ]
171
+
172
+ if len(same_strategy) >= min_occurrences:
173
+ success_count = sum(1 for o in same_strategy if o.success)
174
+ confidence = success_count / len(same_strategy)
175
+
176
+ # Only create heuristic if confidence is meaningful
177
+ if confidence > 0.5:
178
+ heuristic = Heuristic(
179
+ id=f"heur_{uuid.uuid4().hex[:12]}",
180
+ agent=agent,
181
+ project_id=project_id,
182
+ condition=f"task type: {task_type}",
183
+ strategy=strategy,
184
+ confidence=confidence,
185
+ occurrence_count=len(same_strategy),
186
+ success_count=success_count,
187
+ last_validated=datetime.now(timezone.utc),
188
+ created_at=datetime.now(timezone.utc),
189
+ )
190
+ self.storage.save_heuristic(heuristic)
191
+ logger.info(
192
+ f"Created heuristic for {agent}: {strategy[:50]}... "
193
+ f"(confidence: {confidence:.0%})"
194
+ )
195
+
196
+ def _maybe_create_anti_pattern(
197
+ self,
198
+ agent: str,
199
+ project_id: str,
200
+ task: str,
201
+ strategy: str,
202
+ error: str,
203
+ ):
204
+ """Create anti-pattern if we see repeated failures with same pattern."""
205
+ # Check for similar failures
206
+ similar_failures = self.storage.get_outcomes(
207
+ project_id=project_id,
208
+ agent=agent,
209
+ success_only=False,
210
+ top_k=10,
211
+ )
212
+
213
+ # Filter to failures with similar error
214
+ similar = [
215
+ o
216
+ for o in similar_failures
217
+ if not o.success
218
+ and o.error_message
219
+ and self._errors_similar(o.error_message, error)
220
+ ]
221
+
222
+ if len(similar) >= 2: # At least 2 similar failures
223
+ anti_pattern = AntiPattern(
224
+ id=f"anti_{uuid.uuid4().hex[:12]}",
225
+ agent=agent,
226
+ project_id=project_id,
227
+ pattern=strategy,
228
+ why_bad=error,
229
+ better_alternative="[To be determined from successful outcomes]",
230
+ occurrence_count=len(similar),
231
+ last_seen=datetime.now(timezone.utc),
232
+ )
233
+ self.storage.save_anti_pattern(anti_pattern)
234
+ logger.info(f"Created anti-pattern for {agent}: {strategy[:50]}...")
235
+
236
+ def add_preference(
237
+ self,
238
+ user_id: str,
239
+ category: str,
240
+ preference: str,
241
+ source: str,
242
+ ) -> UserPreference:
243
+ """Add a user preference."""
244
+ pref = UserPreference(
245
+ id=f"pref_{uuid.uuid4().hex[:12]}",
246
+ user_id=user_id,
247
+ category=category,
248
+ preference=preference,
249
+ source=source,
250
+ confidence=1.0 if source == "explicit_instruction" else 0.7,
251
+ timestamp=datetime.now(timezone.utc),
252
+ )
253
+ self.storage.save_user_preference(pref)
254
+ return pref
255
+
256
+ def add_domain_knowledge(
257
+ self,
258
+ agent: str,
259
+ project_id: str,
260
+ domain: str,
261
+ fact: str,
262
+ source: str,
263
+ ) -> DomainKnowledge:
264
+ """Add domain knowledge."""
265
+ knowledge = DomainKnowledge(
266
+ id=f"dk_{uuid.uuid4().hex[:12]}",
267
+ agent=agent,
268
+ project_id=project_id,
269
+ domain=domain,
270
+ fact=fact,
271
+ source=source,
272
+ confidence=1.0 if source == "user_stated" else 0.8,
273
+ last_verified=datetime.now(timezone.utc),
274
+ )
275
+ self.storage.save_domain_knowledge(knowledge)
276
+ return knowledge
277
+
278
+ def forget(
279
+ self,
280
+ project_id: str,
281
+ agent: Optional[str] = None,
282
+ older_than_days: int = 90,
283
+ below_confidence: float = 0.3,
284
+ ) -> int:
285
+ """
286
+ Prune stale and low-confidence memories.
287
+
288
+ Returns:
289
+ Total number of items pruned
290
+ """
291
+ cutoff = datetime.now(timezone.utc) - timedelta(days=older_than_days)
292
+
293
+ # Delete old outcomes
294
+ outcomes_deleted = self.storage.delete_outcomes_older_than(
295
+ project_id=project_id,
296
+ older_than=cutoff,
297
+ agent=agent,
298
+ )
299
+
300
+ # Delete low-confidence heuristics
301
+ heuristics_deleted = self.storage.delete_low_confidence_heuristics(
302
+ project_id=project_id,
303
+ below_confidence=below_confidence,
304
+ agent=agent,
305
+ )
306
+
307
+ total = outcomes_deleted + heuristics_deleted
308
+ logger.info(
309
+ f"Forgot {total} items: {outcomes_deleted} outcomes, "
310
+ f"{heuristics_deleted} heuristics"
311
+ )
312
+ return total
313
+
314
+ def _infer_task_type(self, task: str) -> str:
315
+ """Infer task type from description."""
316
+ task_lower = task.lower()
317
+ if "test" in task_lower or "validate" in task_lower:
318
+ return "testing"
319
+ elif "api" in task_lower or "endpoint" in task_lower:
320
+ return "api_testing"
321
+ elif "form" in task_lower or "input" in task_lower:
322
+ return "form_testing"
323
+ elif "database" in task_lower or "query" in task_lower:
324
+ return "database_validation"
325
+ return "general"
326
+
327
+ def _strategies_similar(self, s1: str, s2: str) -> bool:
328
+ """
329
+ Check if two strategies are similar enough to count together.
330
+
331
+ Uses embedding-based cosine similarity when an embedder is available,
332
+ otherwise falls back to simple word overlap.
333
+ """
334
+ if self.embedder is not None:
335
+ return self._strategies_similar_embedding(s1, s2)
336
+ return self._strategies_similar_word_overlap(s1, s2)
337
+
338
+ def _strategies_similar_embedding(self, s1: str, s2: str) -> bool:
339
+ """Check strategy similarity using embedding cosine similarity."""
340
+ try:
341
+ emb1 = self.embedder.encode(s1)
342
+ emb2 = self.embedder.encode(s2)
343
+ similarity = self._cosine_similarity(emb1, emb2)
344
+ return similarity >= self.similarity_threshold
345
+ except Exception as e:
346
+ logger.warning(
347
+ f"Embedding similarity failed, falling back to word overlap: {e}"
348
+ )
349
+ return self._strategies_similar_word_overlap(s1, s2)
350
+
351
+ def _strategies_similar_word_overlap(self, s1: str, s2: str) -> bool:
352
+ """Check strategy similarity using simple word overlap."""
353
+ words1 = set(s1.lower().split())
354
+ words2 = set(s2.lower().split())
355
+ overlap = len(words1 & words2)
356
+ return overlap >= min(3, len(words1) // 2)
357
+
358
+ def _cosine_similarity(self, v1: list, v2: list) -> float:
359
+ """Compute cosine similarity between two vectors."""
360
+ import math
361
+
362
+ dot_product = sum(a * b for a, b in zip(v1, v2, strict=False))
363
+ norm1 = math.sqrt(sum(a * a for a in v1))
364
+ norm2 = math.sqrt(sum(b * b for b in v2))
365
+ if norm1 == 0 or norm2 == 0:
366
+ return 0.0
367
+ return dot_product / (norm1 * norm2)
368
+
369
+ def _errors_similar(self, e1: str, e2: str) -> bool:
370
+ """Check if two errors are similar."""
371
+ # Simple substring check
372
+ e1_lower = e1.lower()
373
+ e2_lower = e2.lower()
374
+ return e1_lower in e2_lower or e2_lower in e1_lower