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