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/learning/validation.py
CHANGED
|
@@ -1,346 +1,346 @@
|
|
|
1
|
-
"""
|
|
2
|
-
ALMA Learning Validation.
|
|
3
|
-
|
|
4
|
-
Enforces scope constraints and validates learning requests.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import logging
|
|
8
|
-
from dataclasses import dataclass, field
|
|
9
|
-
from enum import Enum
|
|
10
|
-
from typing import Any, Dict, List, Optional, Set
|
|
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
|
-
|
|
20
|
-
ALLOWED = "allowed"
|
|
21
|
-
DENIED_OUT_OF_SCOPE = "denied_out_of_scope"
|
|
22
|
-
DENIED_FORBIDDEN = "denied_forbidden"
|
|
23
|
-
DENIED_UNKNOWN_AGENT = "denied_unknown_agent"
|
|
24
|
-
WARNING_NO_SCOPE = "warning_no_scope"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
@dataclass
|
|
28
|
-
class ValidationReport:
|
|
29
|
-
"""Detailed report of a validation check."""
|
|
30
|
-
|
|
31
|
-
result: ValidationResult
|
|
32
|
-
agent: str
|
|
33
|
-
domain: str
|
|
34
|
-
reason: str
|
|
35
|
-
allowed_domains: List[str] = field(default_factory=list)
|
|
36
|
-
forbidden_domains: List[str] = field(default_factory=list)
|
|
37
|
-
|
|
38
|
-
@property
|
|
39
|
-
def is_allowed(self) -> bool:
|
|
40
|
-
"""Check if the validation passed."""
|
|
41
|
-
return self.result in (
|
|
42
|
-
ValidationResult.ALLOWED,
|
|
43
|
-
ValidationResult.WARNING_NO_SCOPE,
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
def to_dict(self) -> Dict[str, Any]:
|
|
47
|
-
"""Convert to dictionary."""
|
|
48
|
-
return {
|
|
49
|
-
"result": self.result.value,
|
|
50
|
-
"agent": self.agent,
|
|
51
|
-
"domain": self.domain,
|
|
52
|
-
"reason": self.reason,
|
|
53
|
-
"is_allowed": self.is_allowed,
|
|
54
|
-
"allowed_domains": self.allowed_domains,
|
|
55
|
-
"forbidden_domains": self.forbidden_domains,
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
class ScopeValidator:
|
|
60
|
-
"""
|
|
61
|
-
Validates that learning requests are within agent scope.
|
|
62
|
-
|
|
63
|
-
Provides strict enforcement with detailed reporting.
|
|
64
|
-
"""
|
|
65
|
-
|
|
66
|
-
def __init__(
|
|
67
|
-
self,
|
|
68
|
-
scopes: Dict[str, MemoryScope],
|
|
69
|
-
strict_mode: bool = True,
|
|
70
|
-
allow_unknown_agents: bool = False,
|
|
71
|
-
):
|
|
72
|
-
"""
|
|
73
|
-
Initialize validator.
|
|
74
|
-
|
|
75
|
-
Args:
|
|
76
|
-
scopes: Dict of agent_name -> MemoryScope
|
|
77
|
-
strict_mode: If True, deny requests for unknown domains
|
|
78
|
-
allow_unknown_agents: If True, allow learning for agents without scopes
|
|
79
|
-
"""
|
|
80
|
-
self.scopes = scopes
|
|
81
|
-
self.strict_mode = strict_mode
|
|
82
|
-
self.allow_unknown_agents = allow_unknown_agents
|
|
83
|
-
|
|
84
|
-
# Track validation statistics
|
|
85
|
-
self._stats = {
|
|
86
|
-
"total_validations": 0,
|
|
87
|
-
"allowed": 0,
|
|
88
|
-
"denied": 0,
|
|
89
|
-
"warnings": 0,
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
def validate(
|
|
93
|
-
self,
|
|
94
|
-
agent: str,
|
|
95
|
-
domain: str,
|
|
96
|
-
task_type: Optional[str] = None,
|
|
97
|
-
) -> ValidationReport:
|
|
98
|
-
"""
|
|
99
|
-
Validate a learning request.
|
|
100
|
-
|
|
101
|
-
Args:
|
|
102
|
-
agent: Agent attempting to learn
|
|
103
|
-
domain: Knowledge domain to learn in
|
|
104
|
-
task_type: Optional task type for context
|
|
105
|
-
|
|
106
|
-
Returns:
|
|
107
|
-
ValidationReport with detailed results
|
|
108
|
-
"""
|
|
109
|
-
self._stats["total_validations"] += 1
|
|
110
|
-
|
|
111
|
-
# Check if agent has a scope
|
|
112
|
-
scope = self.scopes.get(agent)
|
|
113
|
-
|
|
114
|
-
if scope is None:
|
|
115
|
-
if self.allow_unknown_agents:
|
|
116
|
-
self._stats["warnings"] += 1
|
|
117
|
-
logger.warning(f"Agent '{agent}' has no defined scope, allowing anyway")
|
|
118
|
-
return ValidationReport(
|
|
119
|
-
result=ValidationResult.WARNING_NO_SCOPE,
|
|
120
|
-
agent=agent,
|
|
121
|
-
domain=domain,
|
|
122
|
-
reason=f"Agent '{agent}' has no defined scope",
|
|
123
|
-
)
|
|
124
|
-
else:
|
|
125
|
-
self._stats["denied"] += 1
|
|
126
|
-
logger.warning(f"Agent '{agent}' denied: no scope defined")
|
|
127
|
-
return ValidationReport(
|
|
128
|
-
result=ValidationResult.DENIED_UNKNOWN_AGENT,
|
|
129
|
-
agent=agent,
|
|
130
|
-
domain=domain,
|
|
131
|
-
reason=f"Agent '{agent}' has no defined scope and unknown agents are not allowed",
|
|
132
|
-
)
|
|
133
|
-
|
|
134
|
-
# Check if domain is explicitly forbidden
|
|
135
|
-
if domain in scope.cannot_learn:
|
|
136
|
-
self._stats["denied"] += 1
|
|
137
|
-
logger.warning(
|
|
138
|
-
f"Agent '{agent}' denied learning in '{domain}': explicitly forbidden"
|
|
139
|
-
)
|
|
140
|
-
return ValidationReport(
|
|
141
|
-
result=ValidationResult.DENIED_FORBIDDEN,
|
|
142
|
-
agent=agent,
|
|
143
|
-
domain=domain,
|
|
144
|
-
reason=f"Domain '{domain}' is explicitly forbidden for agent '{agent}'",
|
|
145
|
-
allowed_domains=scope.can_learn,
|
|
146
|
-
forbidden_domains=scope.cannot_learn,
|
|
147
|
-
)
|
|
148
|
-
|
|
149
|
-
# Check if domain is in allowed list
|
|
150
|
-
if scope.can_learn: # If list is not empty, it's an allowlist
|
|
151
|
-
if domain not in scope.can_learn:
|
|
152
|
-
# Check for partial matches (e.g., "form_testing" contains "testing")
|
|
153
|
-
partial_match = any(
|
|
154
|
-
allowed in domain or domain in allowed
|
|
155
|
-
for allowed in scope.can_learn
|
|
156
|
-
)
|
|
157
|
-
|
|
158
|
-
if not partial_match and self.strict_mode:
|
|
159
|
-
self._stats["denied"] += 1
|
|
160
|
-
logger.warning(
|
|
161
|
-
f"Agent '{agent}' denied learning in '{domain}': not in allowed list"
|
|
162
|
-
)
|
|
163
|
-
return ValidationReport(
|
|
164
|
-
result=ValidationResult.DENIED_OUT_OF_SCOPE,
|
|
165
|
-
agent=agent,
|
|
166
|
-
domain=domain,
|
|
167
|
-
reason=f"Domain '{domain}' is not in agent '{agent}'s allowed domains",
|
|
168
|
-
allowed_domains=scope.can_learn,
|
|
169
|
-
forbidden_domains=scope.cannot_learn,
|
|
170
|
-
)
|
|
171
|
-
|
|
172
|
-
# Allowed
|
|
173
|
-
self._stats["allowed"] += 1
|
|
174
|
-
return ValidationReport(
|
|
175
|
-
result=ValidationResult.ALLOWED,
|
|
176
|
-
agent=agent,
|
|
177
|
-
domain=domain,
|
|
178
|
-
reason="Learning allowed",
|
|
179
|
-
allowed_domains=scope.can_learn,
|
|
180
|
-
forbidden_domains=scope.cannot_learn,
|
|
181
|
-
)
|
|
182
|
-
|
|
183
|
-
def validate_batch(
|
|
184
|
-
self,
|
|
185
|
-
agent: str,
|
|
186
|
-
domains: List[str],
|
|
187
|
-
) -> Dict[str, ValidationReport]:
|
|
188
|
-
"""
|
|
189
|
-
Validate multiple domains at once.
|
|
190
|
-
|
|
191
|
-
Args:
|
|
192
|
-
agent: Agent attempting to learn
|
|
193
|
-
domains: List of domains to validate
|
|
194
|
-
|
|
195
|
-
Returns:
|
|
196
|
-
Dict of domain -> ValidationReport
|
|
197
|
-
"""
|
|
198
|
-
return {domain: self.validate(agent, domain) for domain in domains}
|
|
199
|
-
|
|
200
|
-
def get_allowed_domains(self, agent: str) -> Set[str]:
|
|
201
|
-
"""Get all allowed domains for an agent."""
|
|
202
|
-
scope = self.scopes.get(agent)
|
|
203
|
-
if scope is None:
|
|
204
|
-
return set()
|
|
205
|
-
return set(scope.can_learn)
|
|
206
|
-
|
|
207
|
-
def get_forbidden_domains(self, agent: str) -> Set[str]:
|
|
208
|
-
"""Get all forbidden domains for an agent."""
|
|
209
|
-
scope = self.scopes.get(agent)
|
|
210
|
-
if scope is None:
|
|
211
|
-
return set()
|
|
212
|
-
return set(scope.cannot_learn)
|
|
213
|
-
|
|
214
|
-
def is_allowed(self, agent: str, domain: str) -> bool:
|
|
215
|
-
"""Quick check if learning is allowed (no detailed report)."""
|
|
216
|
-
return self.validate(agent, domain).is_allowed
|
|
217
|
-
|
|
218
|
-
def get_stats(self) -> Dict[str, Any]:
|
|
219
|
-
"""Get validation statistics."""
|
|
220
|
-
total = self._stats["total_validations"]
|
|
221
|
-
return {
|
|
222
|
-
**self._stats,
|
|
223
|
-
"allow_rate": self._stats["allowed"] / total if total > 0 else 0,
|
|
224
|
-
"deny_rate": self._stats["denied"] / total if total > 0 else 0,
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
def reset_stats(self):
|
|
228
|
-
"""Reset validation statistics."""
|
|
229
|
-
self._stats = {
|
|
230
|
-
"total_validations": 0,
|
|
231
|
-
"allowed": 0,
|
|
232
|
-
"denied": 0,
|
|
233
|
-
"warnings": 0,
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
class TaskTypeValidator:
|
|
238
|
-
"""
|
|
239
|
-
Validates and normalizes task types.
|
|
240
|
-
|
|
241
|
-
Ensures consistent categorization across learning events.
|
|
242
|
-
"""
|
|
243
|
-
|
|
244
|
-
# Standard task type categories
|
|
245
|
-
STANDARD_TYPES = {
|
|
246
|
-
"testing": ["test", "validate", "verify", "check", "qa"],
|
|
247
|
-
"form_testing": ["form", "input", "field", "validation"],
|
|
248
|
-
"api_testing": ["api", "endpoint", "rest", "graphql", "request"],
|
|
249
|
-
"database_validation": ["database", "query", "sql", "schema"],
|
|
250
|
-
"ui_testing": ["ui", "component", "button", "click", "element"],
|
|
251
|
-
"performance_testing": ["performance", "load", "stress", "speed"],
|
|
252
|
-
"security_testing": ["security", "auth", "permission", "xss", "injection"],
|
|
253
|
-
"accessibility_testing": ["accessibility", "a11y", "aria", "screen reader"],
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
def __init__(self, custom_types: Optional[Dict[str, List[str]]] = None):
|
|
257
|
-
"""
|
|
258
|
-
Initialize validator.
|
|
259
|
-
|
|
260
|
-
Args:
|
|
261
|
-
custom_types: Additional custom type mappings
|
|
262
|
-
"""
|
|
263
|
-
self.type_mappings = {**self.STANDARD_TYPES}
|
|
264
|
-
if custom_types:
|
|
265
|
-
self.type_mappings.update(custom_types)
|
|
266
|
-
|
|
267
|
-
# Build reverse lookup
|
|
268
|
-
self._keyword_to_type: Dict[str, str] = {}
|
|
269
|
-
for task_type, keywords in self.type_mappings.items():
|
|
270
|
-
for keyword in keywords:
|
|
271
|
-
self._keyword_to_type[keyword.lower()] = task_type
|
|
272
|
-
|
|
273
|
-
def infer_type(self, task_description: str) -> str:
|
|
274
|
-
"""
|
|
275
|
-
Infer task type from description.
|
|
276
|
-
|
|
277
|
-
Args:
|
|
278
|
-
task_description: Description of the task
|
|
279
|
-
|
|
280
|
-
Returns:
|
|
281
|
-
Inferred task type or "general"
|
|
282
|
-
"""
|
|
283
|
-
task_lower = task_description.lower()
|
|
284
|
-
|
|
285
|
-
# Check for keyword matches
|
|
286
|
-
scores: Dict[str, int] = {}
|
|
287
|
-
for task_type, keywords in self.type_mappings.items():
|
|
288
|
-
score = sum(1 for kw in keywords if kw in task_lower)
|
|
289
|
-
if score > 0:
|
|
290
|
-
scores[task_type] = score
|
|
291
|
-
|
|
292
|
-
if scores:
|
|
293
|
-
# Return the type with highest score
|
|
294
|
-
return max(scores.keys(), key=lambda k: scores[k])
|
|
295
|
-
|
|
296
|
-
return "general"
|
|
297
|
-
|
|
298
|
-
def normalize_type(self, task_type: str) -> str:
|
|
299
|
-
"""
|
|
300
|
-
Normalize a task type to standard category.
|
|
301
|
-
|
|
302
|
-
Args:
|
|
303
|
-
task_type: Raw task type
|
|
304
|
-
|
|
305
|
-
Returns:
|
|
306
|
-
Normalized task type
|
|
307
|
-
"""
|
|
308
|
-
type_lower = task_type.lower().replace("_", " ").replace("-", " ")
|
|
309
|
-
|
|
310
|
-
# Check direct match
|
|
311
|
-
if type_lower in self.type_mappings:
|
|
312
|
-
return type_lower
|
|
313
|
-
|
|
314
|
-
# Check keyword lookup
|
|
315
|
-
for word in type_lower.split():
|
|
316
|
-
if word in self._keyword_to_type:
|
|
317
|
-
return self._keyword_to_type[word]
|
|
318
|
-
|
|
319
|
-
return task_type # Return original if no match
|
|
320
|
-
|
|
321
|
-
def validate_type(self, task_type: str) -> bool:
|
|
322
|
-
"""Check if task type is recognized."""
|
|
323
|
-
normalized = self.normalize_type(task_type)
|
|
324
|
-
return normalized in self.type_mappings or task_type == "general"
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
def validate_learning_request(
|
|
328
|
-
agent: str,
|
|
329
|
-
domain: str,
|
|
330
|
-
scopes: Dict[str, MemoryScope],
|
|
331
|
-
strict: bool = True,
|
|
332
|
-
) -> ValidationReport:
|
|
333
|
-
"""
|
|
334
|
-
Convenience function for one-off validation.
|
|
335
|
-
|
|
336
|
-
Args:
|
|
337
|
-
agent: Agent attempting to learn
|
|
338
|
-
domain: Knowledge domain
|
|
339
|
-
scopes: Dict of agent scopes
|
|
340
|
-
strict: Use strict mode
|
|
341
|
-
|
|
342
|
-
Returns:
|
|
343
|
-
ValidationReport
|
|
344
|
-
"""
|
|
345
|
-
validator = ScopeValidator(scopes, strict_mode=strict)
|
|
346
|
-
return validator.validate(agent, domain)
|
|
1
|
+
"""
|
|
2
|
+
ALMA Learning Validation.
|
|
3
|
+
|
|
4
|
+
Enforces scope constraints and validates learning requests.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import Any, Dict, List, Optional, Set
|
|
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
|
+
|
|
20
|
+
ALLOWED = "allowed"
|
|
21
|
+
DENIED_OUT_OF_SCOPE = "denied_out_of_scope"
|
|
22
|
+
DENIED_FORBIDDEN = "denied_forbidden"
|
|
23
|
+
DENIED_UNKNOWN_AGENT = "denied_unknown_agent"
|
|
24
|
+
WARNING_NO_SCOPE = "warning_no_scope"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class ValidationReport:
|
|
29
|
+
"""Detailed report of a validation check."""
|
|
30
|
+
|
|
31
|
+
result: ValidationResult
|
|
32
|
+
agent: str
|
|
33
|
+
domain: str
|
|
34
|
+
reason: str
|
|
35
|
+
allowed_domains: List[str] = field(default_factory=list)
|
|
36
|
+
forbidden_domains: List[str] = field(default_factory=list)
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def is_allowed(self) -> bool:
|
|
40
|
+
"""Check if the validation passed."""
|
|
41
|
+
return self.result in (
|
|
42
|
+
ValidationResult.ALLOWED,
|
|
43
|
+
ValidationResult.WARNING_NO_SCOPE,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
47
|
+
"""Convert to dictionary."""
|
|
48
|
+
return {
|
|
49
|
+
"result": self.result.value,
|
|
50
|
+
"agent": self.agent,
|
|
51
|
+
"domain": self.domain,
|
|
52
|
+
"reason": self.reason,
|
|
53
|
+
"is_allowed": self.is_allowed,
|
|
54
|
+
"allowed_domains": self.allowed_domains,
|
|
55
|
+
"forbidden_domains": self.forbidden_domains,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ScopeValidator:
|
|
60
|
+
"""
|
|
61
|
+
Validates that learning requests are within agent scope.
|
|
62
|
+
|
|
63
|
+
Provides strict enforcement with detailed reporting.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
scopes: Dict[str, MemoryScope],
|
|
69
|
+
strict_mode: bool = True,
|
|
70
|
+
allow_unknown_agents: bool = False,
|
|
71
|
+
):
|
|
72
|
+
"""
|
|
73
|
+
Initialize validator.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
scopes: Dict of agent_name -> MemoryScope
|
|
77
|
+
strict_mode: If True, deny requests for unknown domains
|
|
78
|
+
allow_unknown_agents: If True, allow learning for agents without scopes
|
|
79
|
+
"""
|
|
80
|
+
self.scopes = scopes
|
|
81
|
+
self.strict_mode = strict_mode
|
|
82
|
+
self.allow_unknown_agents = allow_unknown_agents
|
|
83
|
+
|
|
84
|
+
# Track validation statistics
|
|
85
|
+
self._stats = {
|
|
86
|
+
"total_validations": 0,
|
|
87
|
+
"allowed": 0,
|
|
88
|
+
"denied": 0,
|
|
89
|
+
"warnings": 0,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
def validate(
|
|
93
|
+
self,
|
|
94
|
+
agent: str,
|
|
95
|
+
domain: str,
|
|
96
|
+
task_type: Optional[str] = None,
|
|
97
|
+
) -> ValidationReport:
|
|
98
|
+
"""
|
|
99
|
+
Validate a learning request.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
agent: Agent attempting to learn
|
|
103
|
+
domain: Knowledge domain to learn in
|
|
104
|
+
task_type: Optional task type for context
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
ValidationReport with detailed results
|
|
108
|
+
"""
|
|
109
|
+
self._stats["total_validations"] += 1
|
|
110
|
+
|
|
111
|
+
# Check if agent has a scope
|
|
112
|
+
scope = self.scopes.get(agent)
|
|
113
|
+
|
|
114
|
+
if scope is None:
|
|
115
|
+
if self.allow_unknown_agents:
|
|
116
|
+
self._stats["warnings"] += 1
|
|
117
|
+
logger.warning(f"Agent '{agent}' has no defined scope, allowing anyway")
|
|
118
|
+
return ValidationReport(
|
|
119
|
+
result=ValidationResult.WARNING_NO_SCOPE,
|
|
120
|
+
agent=agent,
|
|
121
|
+
domain=domain,
|
|
122
|
+
reason=f"Agent '{agent}' has no defined scope",
|
|
123
|
+
)
|
|
124
|
+
else:
|
|
125
|
+
self._stats["denied"] += 1
|
|
126
|
+
logger.warning(f"Agent '{agent}' denied: no scope defined")
|
|
127
|
+
return ValidationReport(
|
|
128
|
+
result=ValidationResult.DENIED_UNKNOWN_AGENT,
|
|
129
|
+
agent=agent,
|
|
130
|
+
domain=domain,
|
|
131
|
+
reason=f"Agent '{agent}' has no defined scope and unknown agents are not allowed",
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Check if domain is explicitly forbidden
|
|
135
|
+
if domain in scope.cannot_learn:
|
|
136
|
+
self._stats["denied"] += 1
|
|
137
|
+
logger.warning(
|
|
138
|
+
f"Agent '{agent}' denied learning in '{domain}': explicitly forbidden"
|
|
139
|
+
)
|
|
140
|
+
return ValidationReport(
|
|
141
|
+
result=ValidationResult.DENIED_FORBIDDEN,
|
|
142
|
+
agent=agent,
|
|
143
|
+
domain=domain,
|
|
144
|
+
reason=f"Domain '{domain}' is explicitly forbidden for agent '{agent}'",
|
|
145
|
+
allowed_domains=scope.can_learn,
|
|
146
|
+
forbidden_domains=scope.cannot_learn,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Check if domain is in allowed list
|
|
150
|
+
if scope.can_learn: # If list is not empty, it's an allowlist
|
|
151
|
+
if domain not in scope.can_learn:
|
|
152
|
+
# Check for partial matches (e.g., "form_testing" contains "testing")
|
|
153
|
+
partial_match = any(
|
|
154
|
+
allowed in domain or domain in allowed
|
|
155
|
+
for allowed in scope.can_learn
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
if not partial_match and self.strict_mode:
|
|
159
|
+
self._stats["denied"] += 1
|
|
160
|
+
logger.warning(
|
|
161
|
+
f"Agent '{agent}' denied learning in '{domain}': not in allowed list"
|
|
162
|
+
)
|
|
163
|
+
return ValidationReport(
|
|
164
|
+
result=ValidationResult.DENIED_OUT_OF_SCOPE,
|
|
165
|
+
agent=agent,
|
|
166
|
+
domain=domain,
|
|
167
|
+
reason=f"Domain '{domain}' is not in agent '{agent}'s allowed domains",
|
|
168
|
+
allowed_domains=scope.can_learn,
|
|
169
|
+
forbidden_domains=scope.cannot_learn,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# Allowed
|
|
173
|
+
self._stats["allowed"] += 1
|
|
174
|
+
return ValidationReport(
|
|
175
|
+
result=ValidationResult.ALLOWED,
|
|
176
|
+
agent=agent,
|
|
177
|
+
domain=domain,
|
|
178
|
+
reason="Learning allowed",
|
|
179
|
+
allowed_domains=scope.can_learn,
|
|
180
|
+
forbidden_domains=scope.cannot_learn,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
def validate_batch(
|
|
184
|
+
self,
|
|
185
|
+
agent: str,
|
|
186
|
+
domains: List[str],
|
|
187
|
+
) -> Dict[str, ValidationReport]:
|
|
188
|
+
"""
|
|
189
|
+
Validate multiple domains at once.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
agent: Agent attempting to learn
|
|
193
|
+
domains: List of domains to validate
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Dict of domain -> ValidationReport
|
|
197
|
+
"""
|
|
198
|
+
return {domain: self.validate(agent, domain) for domain in domains}
|
|
199
|
+
|
|
200
|
+
def get_allowed_domains(self, agent: str) -> Set[str]:
|
|
201
|
+
"""Get all allowed domains for an agent."""
|
|
202
|
+
scope = self.scopes.get(agent)
|
|
203
|
+
if scope is None:
|
|
204
|
+
return set()
|
|
205
|
+
return set(scope.can_learn)
|
|
206
|
+
|
|
207
|
+
def get_forbidden_domains(self, agent: str) -> Set[str]:
|
|
208
|
+
"""Get all forbidden domains for an agent."""
|
|
209
|
+
scope = self.scopes.get(agent)
|
|
210
|
+
if scope is None:
|
|
211
|
+
return set()
|
|
212
|
+
return set(scope.cannot_learn)
|
|
213
|
+
|
|
214
|
+
def is_allowed(self, agent: str, domain: str) -> bool:
|
|
215
|
+
"""Quick check if learning is allowed (no detailed report)."""
|
|
216
|
+
return self.validate(agent, domain).is_allowed
|
|
217
|
+
|
|
218
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
219
|
+
"""Get validation statistics."""
|
|
220
|
+
total = self._stats["total_validations"]
|
|
221
|
+
return {
|
|
222
|
+
**self._stats,
|
|
223
|
+
"allow_rate": self._stats["allowed"] / total if total > 0 else 0,
|
|
224
|
+
"deny_rate": self._stats["denied"] / total if total > 0 else 0,
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
def reset_stats(self):
|
|
228
|
+
"""Reset validation statistics."""
|
|
229
|
+
self._stats = {
|
|
230
|
+
"total_validations": 0,
|
|
231
|
+
"allowed": 0,
|
|
232
|
+
"denied": 0,
|
|
233
|
+
"warnings": 0,
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class TaskTypeValidator:
|
|
238
|
+
"""
|
|
239
|
+
Validates and normalizes task types.
|
|
240
|
+
|
|
241
|
+
Ensures consistent categorization across learning events.
|
|
242
|
+
"""
|
|
243
|
+
|
|
244
|
+
# Standard task type categories
|
|
245
|
+
STANDARD_TYPES = {
|
|
246
|
+
"testing": ["test", "validate", "verify", "check", "qa"],
|
|
247
|
+
"form_testing": ["form", "input", "field", "validation"],
|
|
248
|
+
"api_testing": ["api", "endpoint", "rest", "graphql", "request"],
|
|
249
|
+
"database_validation": ["database", "query", "sql", "schema"],
|
|
250
|
+
"ui_testing": ["ui", "component", "button", "click", "element"],
|
|
251
|
+
"performance_testing": ["performance", "load", "stress", "speed"],
|
|
252
|
+
"security_testing": ["security", "auth", "permission", "xss", "injection"],
|
|
253
|
+
"accessibility_testing": ["accessibility", "a11y", "aria", "screen reader"],
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
def __init__(self, custom_types: Optional[Dict[str, List[str]]] = None):
|
|
257
|
+
"""
|
|
258
|
+
Initialize validator.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
custom_types: Additional custom type mappings
|
|
262
|
+
"""
|
|
263
|
+
self.type_mappings = {**self.STANDARD_TYPES}
|
|
264
|
+
if custom_types:
|
|
265
|
+
self.type_mappings.update(custom_types)
|
|
266
|
+
|
|
267
|
+
# Build reverse lookup
|
|
268
|
+
self._keyword_to_type: Dict[str, str] = {}
|
|
269
|
+
for task_type, keywords in self.type_mappings.items():
|
|
270
|
+
for keyword in keywords:
|
|
271
|
+
self._keyword_to_type[keyword.lower()] = task_type
|
|
272
|
+
|
|
273
|
+
def infer_type(self, task_description: str) -> str:
|
|
274
|
+
"""
|
|
275
|
+
Infer task type from description.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
task_description: Description of the task
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
Inferred task type or "general"
|
|
282
|
+
"""
|
|
283
|
+
task_lower = task_description.lower()
|
|
284
|
+
|
|
285
|
+
# Check for keyword matches
|
|
286
|
+
scores: Dict[str, int] = {}
|
|
287
|
+
for task_type, keywords in self.type_mappings.items():
|
|
288
|
+
score = sum(1 for kw in keywords if kw in task_lower)
|
|
289
|
+
if score > 0:
|
|
290
|
+
scores[task_type] = score
|
|
291
|
+
|
|
292
|
+
if scores:
|
|
293
|
+
# Return the type with highest score
|
|
294
|
+
return max(scores.keys(), key=lambda k: scores[k])
|
|
295
|
+
|
|
296
|
+
return "general"
|
|
297
|
+
|
|
298
|
+
def normalize_type(self, task_type: str) -> str:
|
|
299
|
+
"""
|
|
300
|
+
Normalize a task type to standard category.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
task_type: Raw task type
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
Normalized task type
|
|
307
|
+
"""
|
|
308
|
+
type_lower = task_type.lower().replace("_", " ").replace("-", " ")
|
|
309
|
+
|
|
310
|
+
# Check direct match
|
|
311
|
+
if type_lower in self.type_mappings:
|
|
312
|
+
return type_lower
|
|
313
|
+
|
|
314
|
+
# Check keyword lookup
|
|
315
|
+
for word in type_lower.split():
|
|
316
|
+
if word in self._keyword_to_type:
|
|
317
|
+
return self._keyword_to_type[word]
|
|
318
|
+
|
|
319
|
+
return task_type # Return original if no match
|
|
320
|
+
|
|
321
|
+
def validate_type(self, task_type: str) -> bool:
|
|
322
|
+
"""Check if task type is recognized."""
|
|
323
|
+
normalized = self.normalize_type(task_type)
|
|
324
|
+
return normalized in self.type_mappings or task_type == "general"
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def validate_learning_request(
|
|
328
|
+
agent: str,
|
|
329
|
+
domain: str,
|
|
330
|
+
scopes: Dict[str, MemoryScope],
|
|
331
|
+
strict: bool = True,
|
|
332
|
+
) -> ValidationReport:
|
|
333
|
+
"""
|
|
334
|
+
Convenience function for one-off validation.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
agent: Agent attempting to learn
|
|
338
|
+
domain: Knowledge domain
|
|
339
|
+
scopes: Dict of agent scopes
|
|
340
|
+
strict: Use strict mode
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
ValidationReport
|
|
344
|
+
"""
|
|
345
|
+
validator = ScopeValidator(scopes, strict_mode=strict)
|
|
346
|
+
return validator.validate(agent, domain)
|