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,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)