empathy-framework 3.8.3__py3-none-any.whl → 3.9.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {empathy_framework-3.8.3.dist-info → empathy_framework-3.9.1.dist-info}/METADATA +67 -7
- {empathy_framework-3.8.3.dist-info → empathy_framework-3.9.1.dist-info}/RECORD +50 -39
- {empathy_framework-3.8.3.dist-info → empathy_framework-3.9.1.dist-info}/top_level.txt +0 -4
- empathy_os/.empathy/costs.json +60 -0
- empathy_os/.empathy/discovery_stats.json +15 -0
- empathy_os/.empathy/workflow_runs.json +45 -0
- empathy_os/cli.py +372 -13
- empathy_os/cli_unified.py +111 -0
- empathy_os/config/xml_config.py +45 -3
- empathy_os/config.py +46 -2
- empathy_os/memory/control_panel.py +128 -8
- empathy_os/memory/long_term.py +26 -4
- empathy_os/memory/short_term.py +110 -0
- empathy_os/models/token_estimator.py +25 -0
- empathy_os/pattern_library.py +81 -8
- empathy_os/patterns/debugging/all_patterns.json +81 -0
- empathy_os/patterns/debugging/workflow_20260107_1770825e.json +77 -0
- empathy_os/patterns/refactoring_memory.json +89 -0
- empathy_os/telemetry/__init__.py +11 -0
- empathy_os/telemetry/cli.py +451 -0
- empathy_os/telemetry/usage_tracker.py +475 -0
- {test_generator → empathy_os/test_generator}/generator.py +1 -0
- empathy_os/tier_recommender.py +422 -0
- empathy_os/workflows/base.py +223 -23
- empathy_os/workflows/config.py +50 -5
- empathy_os/workflows/tier_tracking.py +408 -0
- {empathy_framework-3.8.3.dist-info → empathy_framework-3.9.1.dist-info}/WHEEL +0 -0
- {empathy_framework-3.8.3.dist-info → empathy_framework-3.9.1.dist-info}/entry_points.txt +0 -0
- {empathy_framework-3.8.3.dist-info → empathy_framework-3.9.1.dist-info}/licenses/LICENSE +0 -0
- {hot_reload → empathy_os/hot_reload}/README.md +0 -0
- {hot_reload → empathy_os/hot_reload}/__init__.py +0 -0
- {hot_reload → empathy_os/hot_reload}/config.py +0 -0
- {hot_reload → empathy_os/hot_reload}/integration.py +0 -0
- {hot_reload → empathy_os/hot_reload}/reloader.py +0 -0
- {hot_reload → empathy_os/hot_reload}/watcher.py +0 -0
- {hot_reload → empathy_os/hot_reload}/websocket.py +0 -0
- {scaffolding → empathy_os/scaffolding}/README.md +0 -0
- {scaffolding → empathy_os/scaffolding}/__init__.py +0 -0
- {scaffolding → empathy_os/scaffolding}/__main__.py +0 -0
- {scaffolding → empathy_os/scaffolding}/cli.py +0 -0
- {test_generator → empathy_os/test_generator}/__init__.py +0 -0
- {test_generator → empathy_os/test_generator}/__main__.py +0 -0
- {test_generator → empathy_os/test_generator}/cli.py +0 -0
- {test_generator → empathy_os/test_generator}/risk_analyzer.py +0 -0
- {workflow_patterns → empathy_os/workflow_patterns}/__init__.py +0 -0
- {workflow_patterns → empathy_os/workflow_patterns}/behavior.py +0 -0
- {workflow_patterns → empathy_os/workflow_patterns}/core.py +0 -0
- {workflow_patterns → empathy_os/workflow_patterns}/output.py +0 -0
- {workflow_patterns → empathy_os/workflow_patterns}/registry.py +0 -0
- {workflow_patterns → empathy_os/workflow_patterns}/structural.py +0 -0
empathy_os/config.py
CHANGED
|
@@ -26,6 +26,48 @@ except ImportError:
|
|
|
26
26
|
from empathy_os.workflows.config import ModelConfig
|
|
27
27
|
|
|
28
28
|
|
|
29
|
+
def _validate_file_path(path: str, allowed_dir: str | None = None) -> Path:
|
|
30
|
+
"""Validate file path to prevent path traversal and arbitrary writes.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
path: File path to validate
|
|
34
|
+
allowed_dir: Optional directory to restrict writes to
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Validated Path object
|
|
38
|
+
|
|
39
|
+
Raises:
|
|
40
|
+
ValueError: If path is invalid or unsafe
|
|
41
|
+
"""
|
|
42
|
+
if not path or not isinstance(path, str):
|
|
43
|
+
raise ValueError("path must be a non-empty string")
|
|
44
|
+
|
|
45
|
+
# Check for null bytes
|
|
46
|
+
if "\x00" in path:
|
|
47
|
+
raise ValueError("path contains null bytes")
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
resolved = Path(path).resolve()
|
|
51
|
+
except (OSError, RuntimeError) as e:
|
|
52
|
+
raise ValueError(f"Invalid path: {e}")
|
|
53
|
+
|
|
54
|
+
# Check if within allowed directory
|
|
55
|
+
if allowed_dir:
|
|
56
|
+
try:
|
|
57
|
+
allowed = Path(allowed_dir).resolve()
|
|
58
|
+
resolved.relative_to(allowed)
|
|
59
|
+
except ValueError:
|
|
60
|
+
raise ValueError(f"path must be within {allowed_dir}")
|
|
61
|
+
|
|
62
|
+
# Check for dangerous system paths
|
|
63
|
+
dangerous_paths = ["/etc", "/sys", "/proc", "/dev"]
|
|
64
|
+
for dangerous in dangerous_paths:
|
|
65
|
+
if str(resolved).startswith(dangerous):
|
|
66
|
+
raise ValueError(f"Cannot write to system directory: {dangerous}")
|
|
67
|
+
|
|
68
|
+
return resolved
|
|
69
|
+
|
|
70
|
+
|
|
29
71
|
@dataclass
|
|
30
72
|
class EmpathyConfig:
|
|
31
73
|
"""Configuration for EmpathyOS instance
|
|
@@ -300,9 +342,10 @@ class EmpathyConfig:
|
|
|
300
342
|
"PyYAML is required for YAML export. Install with: pip install pyyaml",
|
|
301
343
|
)
|
|
302
344
|
|
|
345
|
+
validated_path = _validate_file_path(filepath)
|
|
303
346
|
data = asdict(self)
|
|
304
347
|
|
|
305
|
-
with open(
|
|
348
|
+
with open(validated_path, "w") as f:
|
|
306
349
|
yaml.dump(data, f, default_flow_style=False, sort_keys=False)
|
|
307
350
|
|
|
308
351
|
def to_json(self, filepath: str, indent: int = 2):
|
|
@@ -317,9 +360,10 @@ class EmpathyConfig:
|
|
|
317
360
|
>>> config.to_json("my-config.json")
|
|
318
361
|
|
|
319
362
|
"""
|
|
363
|
+
validated_path = _validate_file_path(filepath)
|
|
320
364
|
data = asdict(self)
|
|
321
365
|
|
|
322
|
-
with open(
|
|
366
|
+
with open(validated_path, "w") as f:
|
|
323
367
|
json.dump(data, f, indent=indent)
|
|
324
368
|
|
|
325
369
|
def to_dict(self) -> dict[str, Any]:
|
|
@@ -124,8 +124,9 @@ def _validate_agent_id(agent_id: str) -> bool:
|
|
|
124
124
|
if not agent_id or not isinstance(agent_id, str):
|
|
125
125
|
return False
|
|
126
126
|
|
|
127
|
-
# Check for dangerous characters
|
|
128
|
-
|
|
127
|
+
# Check for dangerous characters (path separators, null bytes, command injection)
|
|
128
|
+
# Note: "." and "@" are allowed for email-style user IDs
|
|
129
|
+
if any(c in agent_id for c in ["/", "\\", "\x00", ";", "|", "&"]):
|
|
129
130
|
return False
|
|
130
131
|
|
|
131
132
|
# Check length bounds
|
|
@@ -153,10 +154,70 @@ def _validate_classification(classification: str | None) -> bool:
|
|
|
153
154
|
return classification.upper() in ("PUBLIC", "INTERNAL", "SENSITIVE")
|
|
154
155
|
|
|
155
156
|
|
|
157
|
+
def _validate_file_path(path: str, allowed_dir: str | None = None) -> Path:
|
|
158
|
+
"""Validate file path to prevent path traversal and arbitrary writes.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
path: Path to validate
|
|
162
|
+
allowed_dir: Optional directory that must contain the path
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Resolved absolute Path object
|
|
166
|
+
|
|
167
|
+
Raises:
|
|
168
|
+
ValueError: If path is invalid or outside allowed directory
|
|
169
|
+
|
|
170
|
+
"""
|
|
171
|
+
if not path or not isinstance(path, str):
|
|
172
|
+
raise ValueError("path must be a non-empty string")
|
|
173
|
+
|
|
174
|
+
# Check for null bytes
|
|
175
|
+
if "\x00" in path:
|
|
176
|
+
raise ValueError("path contains null bytes")
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
# Resolve to absolute path
|
|
180
|
+
resolved = Path(path).resolve()
|
|
181
|
+
except (OSError, RuntimeError) as e:
|
|
182
|
+
raise ValueError(f"Invalid path: {e}")
|
|
183
|
+
|
|
184
|
+
# Check if within allowed directory
|
|
185
|
+
if allowed_dir:
|
|
186
|
+
try:
|
|
187
|
+
allowed = Path(allowed_dir).resolve()
|
|
188
|
+
resolved.relative_to(allowed)
|
|
189
|
+
except ValueError:
|
|
190
|
+
raise ValueError(f"path must be within {allowed_dir}")
|
|
191
|
+
|
|
192
|
+
# Check for dangerous system paths
|
|
193
|
+
dangerous_paths = ["/etc", "/sys", "/proc", "/dev"]
|
|
194
|
+
for dangerous in dangerous_paths:
|
|
195
|
+
if str(resolved).startswith(dangerous):
|
|
196
|
+
raise ValueError(f"Cannot write to system directory: {dangerous}")
|
|
197
|
+
|
|
198
|
+
return resolved
|
|
199
|
+
|
|
200
|
+
|
|
156
201
|
class RateLimiter:
|
|
157
202
|
"""Simple in-memory rate limiter by IP address."""
|
|
158
203
|
|
|
159
204
|
def __init__(self, window_seconds: int = 60, max_requests: int = 100):
|
|
205
|
+
"""Initialize rate limiter.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
window_seconds: Time window in seconds
|
|
209
|
+
max_requests: Maximum requests allowed per window
|
|
210
|
+
|
|
211
|
+
Raises:
|
|
212
|
+
ValueError: If window_seconds or max_requests is invalid
|
|
213
|
+
|
|
214
|
+
"""
|
|
215
|
+
if window_seconds < 1:
|
|
216
|
+
raise ValueError(f"window_seconds must be positive, got {window_seconds}")
|
|
217
|
+
|
|
218
|
+
if max_requests < 1:
|
|
219
|
+
raise ValueError(f"max_requests must be positive, got {max_requests}")
|
|
220
|
+
|
|
160
221
|
self.window_seconds = window_seconds
|
|
161
222
|
self.max_requests = max_requests
|
|
162
223
|
self._requests: dict[str, list[float]] = defaultdict(list)
|
|
@@ -406,8 +467,9 @@ class MemoryControlPanel:
|
|
|
406
467
|
stats.storage_bytes = sum(
|
|
407
468
|
f.stat().st_size for f in storage_path.glob("**/*") if f.is_file()
|
|
408
469
|
)
|
|
409
|
-
except Exception:
|
|
410
|
-
|
|
470
|
+
except Exception as e:
|
|
471
|
+
logger.debug("storage_size_calculation_failed", error=str(e))
|
|
472
|
+
stats.storage_bytes = 0
|
|
411
473
|
|
|
412
474
|
try:
|
|
413
475
|
long_term = self._get_long_term()
|
|
@@ -439,7 +501,24 @@ class MemoryControlPanel:
|
|
|
439
501
|
Returns:
|
|
440
502
|
List of pattern summaries
|
|
441
503
|
|
|
504
|
+
Raises:
|
|
505
|
+
ValueError: If classification is invalid or limit is out of range
|
|
506
|
+
|
|
442
507
|
"""
|
|
508
|
+
# Validate classification
|
|
509
|
+
if not _validate_classification(classification):
|
|
510
|
+
raise ValueError(
|
|
511
|
+
f"Invalid classification '{classification}'. "
|
|
512
|
+
f"Must be PUBLIC, INTERNAL, or SENSITIVE."
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
# Validate limit range
|
|
516
|
+
if limit < 1:
|
|
517
|
+
raise ValueError(f"limit must be positive, got {limit}")
|
|
518
|
+
|
|
519
|
+
if limit > 10000:
|
|
520
|
+
raise ValueError(f"limit too large (max 10000), got {limit}")
|
|
521
|
+
|
|
443
522
|
long_term = self._get_long_term()
|
|
444
523
|
|
|
445
524
|
class_filter = None
|
|
@@ -464,13 +543,24 @@ class MemoryControlPanel:
|
|
|
464
543
|
Returns:
|
|
465
544
|
True if deleted
|
|
466
545
|
|
|
546
|
+
Raises:
|
|
547
|
+
ValueError: If pattern_id or user_id format is invalid
|
|
548
|
+
|
|
467
549
|
"""
|
|
550
|
+
# Validate pattern_id
|
|
551
|
+
if not _validate_pattern_id(pattern_id):
|
|
552
|
+
raise ValueError(f"Invalid pattern_id format: {pattern_id}")
|
|
553
|
+
|
|
554
|
+
# Validate user_id (reuse agent_id validation - same format)
|
|
555
|
+
if not _validate_agent_id(user_id):
|
|
556
|
+
raise ValueError(f"Invalid user_id format: {user_id}")
|
|
557
|
+
|
|
468
558
|
long_term = self._get_long_term()
|
|
469
559
|
try:
|
|
470
560
|
return long_term.delete_pattern(pattern_id, user_id)
|
|
471
561
|
except Exception as e:
|
|
472
562
|
logger.error("delete_pattern_failed", pattern_id=pattern_id, error=str(e))
|
|
473
|
-
return False
|
|
563
|
+
return False # Graceful degradation - validation errors raise, storage errors return False
|
|
474
564
|
|
|
475
565
|
def clear_short_term(self, agent_id: str = "admin") -> int:
|
|
476
566
|
"""Clear all short-term memory for an agent.
|
|
@@ -481,7 +571,14 @@ class MemoryControlPanel:
|
|
|
481
571
|
Returns:
|
|
482
572
|
Number of keys deleted
|
|
483
573
|
|
|
574
|
+
Raises:
|
|
575
|
+
ValueError: If agent_id format is invalid
|
|
576
|
+
|
|
484
577
|
"""
|
|
578
|
+
# Validate agent_id
|
|
579
|
+
if not _validate_agent_id(agent_id):
|
|
580
|
+
raise ValueError(f"Invalid agent_id format: {agent_id}")
|
|
581
|
+
|
|
485
582
|
memory = self._get_short_term()
|
|
486
583
|
creds = AgentCredentials(agent_id=agent_id, tier=AccessTier.STEWARD)
|
|
487
584
|
return memory.clear_working_memory(creds)
|
|
@@ -496,7 +593,20 @@ class MemoryControlPanel:
|
|
|
496
593
|
Returns:
|
|
497
594
|
Number of patterns exported
|
|
498
595
|
|
|
596
|
+
Raises:
|
|
597
|
+
ValueError: If output_path is invalid, classification invalid, or path is unsafe
|
|
598
|
+
|
|
499
599
|
"""
|
|
600
|
+
# Validate file path to prevent path traversal attacks
|
|
601
|
+
validated_path = _validate_file_path(output_path)
|
|
602
|
+
|
|
603
|
+
# Validate classification (list_patterns will also validate, but do it early)
|
|
604
|
+
if not _validate_classification(classification):
|
|
605
|
+
raise ValueError(
|
|
606
|
+
f"Invalid classification '{classification}'. "
|
|
607
|
+
f"Must be PUBLIC, INTERNAL, or SENSITIVE."
|
|
608
|
+
)
|
|
609
|
+
|
|
500
610
|
patterns = self.list_patterns(classification=classification)
|
|
501
611
|
|
|
502
612
|
export_data = {
|
|
@@ -506,7 +616,7 @@ class MemoryControlPanel:
|
|
|
506
616
|
"patterns": patterns,
|
|
507
617
|
}
|
|
508
618
|
|
|
509
|
-
with open(
|
|
619
|
+
with open(validated_path, "w") as f:
|
|
510
620
|
json.dump(export_data, f, indent=2)
|
|
511
621
|
|
|
512
622
|
return len(patterns)
|
|
@@ -605,11 +715,21 @@ class MemoryControlPanel:
|
|
|
605
715
|
return self._long_term
|
|
606
716
|
|
|
607
717
|
def _count_patterns(self) -> int:
|
|
608
|
-
"""Count patterns in storage.
|
|
718
|
+
"""Count patterns in storage.
|
|
719
|
+
|
|
720
|
+
Returns:
|
|
721
|
+
Number of pattern files, or 0 if counting fails
|
|
722
|
+
|
|
723
|
+
"""
|
|
609
724
|
storage_path = Path(self.config.storage_dir)
|
|
610
725
|
if not storage_path.exists():
|
|
611
726
|
return 0
|
|
612
|
-
|
|
727
|
+
|
|
728
|
+
try:
|
|
729
|
+
return len(list(storage_path.glob("*.json")))
|
|
730
|
+
except (OSError, PermissionError) as e:
|
|
731
|
+
logger.debug("pattern_count_failed", error=str(e))
|
|
732
|
+
return 0
|
|
613
733
|
|
|
614
734
|
|
|
615
735
|
def print_status(panel: MemoryControlPanel):
|
empathy_os/memory/long_term.py
CHANGED
|
@@ -540,7 +540,8 @@ class SecureMemDocsIntegration:
|
|
|
540
540
|
|
|
541
541
|
Raises:
|
|
542
542
|
SecurityError: If secrets detected or security policy violated
|
|
543
|
-
ValueError: If invalid classification
|
|
543
|
+
ValueError: If content/pattern_type/user_id empty or invalid classification
|
|
544
|
+
TypeError: If custom_metadata is not dict
|
|
544
545
|
|
|
545
546
|
Example:
|
|
546
547
|
>>> result = integration.store_pattern(
|
|
@@ -559,9 +560,17 @@ class SecureMemDocsIntegration:
|
|
|
559
560
|
)
|
|
560
561
|
|
|
561
562
|
try:
|
|
562
|
-
#
|
|
563
|
+
# Pattern 1: String ID validation
|
|
563
564
|
if not content or not content.strip():
|
|
564
|
-
raise ValueError("
|
|
565
|
+
raise ValueError("content cannot be empty")
|
|
566
|
+
if not pattern_type or not pattern_type.strip():
|
|
567
|
+
raise ValueError("pattern_type cannot be empty")
|
|
568
|
+
if not user_id or not user_id.strip():
|
|
569
|
+
raise ValueError("user_id cannot be empty")
|
|
570
|
+
|
|
571
|
+
# Pattern 5: Type validation
|
|
572
|
+
if custom_metadata is not None and not isinstance(custom_metadata, dict):
|
|
573
|
+
raise TypeError(f"custom_metadata must be dict, got {type(custom_metadata).__name__}")
|
|
565
574
|
|
|
566
575
|
# Step 1 & 2: PII Scrubbing + Secrets Detection (PARALLEL for performance)
|
|
567
576
|
# Run both operations in parallel since they're independent
|
|
@@ -759,7 +768,7 @@ class SecureMemDocsIntegration:
|
|
|
759
768
|
|
|
760
769
|
Raises:
|
|
761
770
|
PermissionError: If access denied
|
|
762
|
-
ValueError: If pattern not found or retention expired
|
|
771
|
+
ValueError: If pattern_id/user_id empty, pattern not found, or retention expired
|
|
763
772
|
SecurityError: If decryption fails
|
|
764
773
|
|
|
765
774
|
Example:
|
|
@@ -770,6 +779,12 @@ class SecureMemDocsIntegration:
|
|
|
770
779
|
>>> print(pattern["content"])
|
|
771
780
|
|
|
772
781
|
"""
|
|
782
|
+
# Pattern 1: String ID validation
|
|
783
|
+
if not pattern_id or not pattern_id.strip():
|
|
784
|
+
raise ValueError("pattern_id cannot be empty")
|
|
785
|
+
if not user_id or not user_id.strip():
|
|
786
|
+
raise ValueError("user_id cannot be empty")
|
|
787
|
+
|
|
773
788
|
logger.info(
|
|
774
789
|
"retrieve_pattern_started",
|
|
775
790
|
pattern_id=pattern_id,
|
|
@@ -1108,8 +1123,15 @@ class SecureMemDocsIntegration:
|
|
|
1108
1123
|
|
|
1109
1124
|
Raises:
|
|
1110
1125
|
PermissionError: If user doesn't have permission to delete
|
|
1126
|
+
ValueError: If pattern_id or user_id is empty
|
|
1111
1127
|
|
|
1112
1128
|
"""
|
|
1129
|
+
# Pattern 1: String ID validation
|
|
1130
|
+
if not pattern_id or not pattern_id.strip():
|
|
1131
|
+
raise ValueError("pattern_id cannot be empty")
|
|
1132
|
+
if not user_id or not user_id.strip():
|
|
1133
|
+
raise ValueError("user_id cannot be empty")
|
|
1134
|
+
|
|
1113
1135
|
# Retrieve pattern to check permissions
|
|
1114
1136
|
pattern_data = self.storage.retrieve(pattern_id)
|
|
1115
1137
|
|
empathy_os/memory/short_term.py
CHANGED
|
@@ -295,6 +295,26 @@ class StagedPattern:
|
|
|
295
295
|
staged_at: datetime = field(default_factory=datetime.now)
|
|
296
296
|
interests: list[str] = field(default_factory=list) # For negotiation
|
|
297
297
|
|
|
298
|
+
def __post_init__(self):
|
|
299
|
+
"""Validate fields after initialization"""
|
|
300
|
+
# Pattern 1: String ID validation
|
|
301
|
+
if not self.pattern_id or not self.pattern_id.strip():
|
|
302
|
+
raise ValueError("pattern_id cannot be empty")
|
|
303
|
+
if not self.agent_id or not self.agent_id.strip():
|
|
304
|
+
raise ValueError("agent_id cannot be empty")
|
|
305
|
+
if not self.pattern_type or not self.pattern_type.strip():
|
|
306
|
+
raise ValueError("pattern_type cannot be empty")
|
|
307
|
+
|
|
308
|
+
# Pattern 4: Range validation for confidence
|
|
309
|
+
if not 0.0 <= self.confidence <= 1.0:
|
|
310
|
+
raise ValueError(f"confidence must be between 0.0 and 1.0, got {self.confidence}")
|
|
311
|
+
|
|
312
|
+
# Pattern 5: Type validation
|
|
313
|
+
if not isinstance(self.context, dict):
|
|
314
|
+
raise TypeError(f"context must be dict, got {type(self.context).__name__}")
|
|
315
|
+
if not isinstance(self.interests, list):
|
|
316
|
+
raise TypeError(f"interests must be list, got {type(self.interests).__name__}")
|
|
317
|
+
|
|
298
318
|
def to_dict(self) -> dict:
|
|
299
319
|
return {
|
|
300
320
|
"pattern_id": self.pattern_id,
|
|
@@ -619,10 +639,18 @@ class RedisShortTermMemory:
|
|
|
619
639
|
Returns:
|
|
620
640
|
True if successful
|
|
621
641
|
|
|
642
|
+
Raises:
|
|
643
|
+
ValueError: If key is empty or invalid
|
|
644
|
+
PermissionError: If credentials lack write access
|
|
645
|
+
|
|
622
646
|
Example:
|
|
623
647
|
>>> memory.stash("analysis_v1", {"findings": [...]}, creds)
|
|
624
648
|
|
|
625
649
|
"""
|
|
650
|
+
# Pattern 1: String ID validation
|
|
651
|
+
if not key or not key.strip():
|
|
652
|
+
raise ValueError("key cannot be empty")
|
|
653
|
+
|
|
626
654
|
if not credentials.can_stage():
|
|
627
655
|
raise PermissionError(
|
|
628
656
|
f"Agent {credentials.agent_id} (Tier {credentials.tier.name}) "
|
|
@@ -653,10 +681,17 @@ class RedisShortTermMemory:
|
|
|
653
681
|
Returns:
|
|
654
682
|
Retrieved data or None if not found
|
|
655
683
|
|
|
684
|
+
Raises:
|
|
685
|
+
ValueError: If key is empty or invalid
|
|
686
|
+
|
|
656
687
|
Example:
|
|
657
688
|
>>> data = memory.retrieve("analysis_v1", creds)
|
|
658
689
|
|
|
659
690
|
"""
|
|
691
|
+
# Pattern 1: String ID validation
|
|
692
|
+
if not key or not key.strip():
|
|
693
|
+
raise ValueError("key cannot be empty")
|
|
694
|
+
|
|
660
695
|
owner = agent_id or credentials.agent_id
|
|
661
696
|
full_key = f"{self.PREFIX_WORKING}{owner}:{key}"
|
|
662
697
|
raw = self._get(full_key)
|
|
@@ -704,7 +739,15 @@ class RedisShortTermMemory:
|
|
|
704
739
|
Returns:
|
|
705
740
|
True if staged successfully
|
|
706
741
|
|
|
742
|
+
Raises:
|
|
743
|
+
TypeError: If pattern is not StagedPattern
|
|
744
|
+
PermissionError: If credentials lack staging access
|
|
745
|
+
|
|
707
746
|
"""
|
|
747
|
+
# Pattern 5: Type validation
|
|
748
|
+
if not isinstance(pattern, StagedPattern):
|
|
749
|
+
raise TypeError(f"pattern must be StagedPattern, got {type(pattern).__name__}")
|
|
750
|
+
|
|
708
751
|
if not credentials.can_stage():
|
|
709
752
|
raise PermissionError(
|
|
710
753
|
f"Agent {credentials.agent_id} cannot stage patterns. "
|
|
@@ -732,7 +775,14 @@ class RedisShortTermMemory:
|
|
|
732
775
|
Returns:
|
|
733
776
|
StagedPattern or None
|
|
734
777
|
|
|
778
|
+
Raises:
|
|
779
|
+
ValueError: If pattern_id is empty
|
|
780
|
+
|
|
735
781
|
"""
|
|
782
|
+
# Pattern 1: String ID validation
|
|
783
|
+
if not pattern_id or not pattern_id.strip():
|
|
784
|
+
raise ValueError("pattern_id cannot be empty")
|
|
785
|
+
|
|
736
786
|
key = f"{self.PREFIX_STAGED}{pattern_id}"
|
|
737
787
|
raw = self._get(key)
|
|
738
788
|
|
|
@@ -844,7 +894,22 @@ class RedisShortTermMemory:
|
|
|
844
894
|
Returns:
|
|
845
895
|
ConflictContext for resolution
|
|
846
896
|
|
|
897
|
+
Raises:
|
|
898
|
+
ValueError: If conflict_id is empty
|
|
899
|
+
TypeError: If positions or interests are not dicts
|
|
900
|
+
PermissionError: If credentials lack permission
|
|
901
|
+
|
|
847
902
|
"""
|
|
903
|
+
# Pattern 1: String ID validation
|
|
904
|
+
if not conflict_id or not conflict_id.strip():
|
|
905
|
+
raise ValueError("conflict_id cannot be empty")
|
|
906
|
+
|
|
907
|
+
# Pattern 5: Type validation
|
|
908
|
+
if not isinstance(positions, dict):
|
|
909
|
+
raise TypeError(f"positions must be dict, got {type(positions).__name__}")
|
|
910
|
+
if not isinstance(interests, dict):
|
|
911
|
+
raise TypeError(f"interests must be dict, got {type(interests).__name__}")
|
|
912
|
+
|
|
848
913
|
if not credentials.can_stage():
|
|
849
914
|
raise PermissionError(
|
|
850
915
|
f"Agent {credentials.agent_id} cannot create conflict context. "
|
|
@@ -881,7 +946,14 @@ class RedisShortTermMemory:
|
|
|
881
946
|
Returns:
|
|
882
947
|
ConflictContext or None
|
|
883
948
|
|
|
949
|
+
Raises:
|
|
950
|
+
ValueError: If conflict_id is empty
|
|
951
|
+
|
|
884
952
|
"""
|
|
953
|
+
# Pattern 1: String ID validation
|
|
954
|
+
if not conflict_id or not conflict_id.strip():
|
|
955
|
+
raise ValueError("conflict_id cannot be empty")
|
|
956
|
+
|
|
885
957
|
key = f"{self.PREFIX_CONFLICT}{conflict_id}"
|
|
886
958
|
raw = self._get(key)
|
|
887
959
|
|
|
@@ -1014,7 +1086,19 @@ class RedisShortTermMemory:
|
|
|
1014
1086
|
Returns:
|
|
1015
1087
|
True if created
|
|
1016
1088
|
|
|
1089
|
+
Raises:
|
|
1090
|
+
ValueError: If session_id is empty
|
|
1091
|
+
TypeError: If metadata is not dict
|
|
1092
|
+
|
|
1017
1093
|
"""
|
|
1094
|
+
# Pattern 1: String ID validation
|
|
1095
|
+
if not session_id or not session_id.strip():
|
|
1096
|
+
raise ValueError("session_id cannot be empty")
|
|
1097
|
+
|
|
1098
|
+
# Pattern 5: Type validation
|
|
1099
|
+
if metadata is not None and not isinstance(metadata, dict):
|
|
1100
|
+
raise TypeError(f"metadata must be dict, got {type(metadata).__name__}")
|
|
1101
|
+
|
|
1018
1102
|
key = f"{self.PREFIX_SESSION}{session_id}"
|
|
1019
1103
|
payload = {
|
|
1020
1104
|
"session_id": session_id,
|
|
@@ -1039,7 +1123,14 @@ class RedisShortTermMemory:
|
|
|
1039
1123
|
Returns:
|
|
1040
1124
|
True if joined
|
|
1041
1125
|
|
|
1126
|
+
Raises:
|
|
1127
|
+
ValueError: If session_id is empty
|
|
1128
|
+
|
|
1042
1129
|
"""
|
|
1130
|
+
# Pattern 1: String ID validation
|
|
1131
|
+
if not session_id or not session_id.strip():
|
|
1132
|
+
raise ValueError("session_id cannot be empty")
|
|
1133
|
+
|
|
1043
1134
|
key = f"{self.PREFIX_SESSION}{session_id}"
|
|
1044
1135
|
raw = self._get(key)
|
|
1045
1136
|
|
|
@@ -1164,11 +1255,19 @@ class RedisShortTermMemory:
|
|
|
1164
1255
|
Returns:
|
|
1165
1256
|
Number of items successfully stashed
|
|
1166
1257
|
|
|
1258
|
+
Raises:
|
|
1259
|
+
TypeError: If items is not a list
|
|
1260
|
+
PermissionError: If credentials lack write access
|
|
1261
|
+
|
|
1167
1262
|
Example:
|
|
1168
1263
|
>>> items = [("key1", {"a": 1}), ("key2", {"b": 2})]
|
|
1169
1264
|
>>> count = memory.stash_batch(items, creds)
|
|
1170
1265
|
|
|
1171
1266
|
"""
|
|
1267
|
+
# Pattern 5: Type validation
|
|
1268
|
+
if not isinstance(items, list):
|
|
1269
|
+
raise TypeError(f"items must be list, got {type(items).__name__}")
|
|
1270
|
+
|
|
1172
1271
|
if not credentials.can_stage():
|
|
1173
1272
|
raise PermissionError(
|
|
1174
1273
|
f"Agent {credentials.agent_id} cannot write to memory. "
|
|
@@ -2042,12 +2141,23 @@ class RedisShortTermMemory:
|
|
|
2042
2141
|
Returns:
|
|
2043
2142
|
Tuple of (success, pattern, message)
|
|
2044
2143
|
|
|
2144
|
+
Raises:
|
|
2145
|
+
ValueError: If pattern_id is empty or min_confidence out of range
|
|
2146
|
+
|
|
2045
2147
|
Example:
|
|
2046
2148
|
>>> success, pattern, msg = memory.atomic_promote_pattern("pat_123", creds, min_confidence=0.7)
|
|
2047
2149
|
>>> if success:
|
|
2048
2150
|
... library.add(pattern)
|
|
2049
2151
|
|
|
2050
2152
|
"""
|
|
2153
|
+
# Pattern 1: String ID validation
|
|
2154
|
+
if not pattern_id or not pattern_id.strip():
|
|
2155
|
+
raise ValueError("pattern_id cannot be empty")
|
|
2156
|
+
|
|
2157
|
+
# Pattern 4: Range validation
|
|
2158
|
+
if not 0.0 <= min_confidence <= 1.0:
|
|
2159
|
+
raise ValueError(f"min_confidence must be between 0.0 and 1.0, got {min_confidence}")
|
|
2160
|
+
|
|
2051
2161
|
if not credentials.can_validate():
|
|
2052
2162
|
return False, None, "Requires VALIDATOR tier or higher"
|
|
2053
2163
|
|
|
@@ -53,7 +53,14 @@ def estimate_tokens(text: str, model_id: str = "claude-sonnet-4-5-20250514") ->
|
|
|
53
53
|
Returns:
|
|
54
54
|
Estimated token count
|
|
55
55
|
|
|
56
|
+
Raises:
|
|
57
|
+
ValueError: If model_id is empty
|
|
58
|
+
|
|
56
59
|
"""
|
|
60
|
+
# Pattern 1: String ID validation
|
|
61
|
+
if not model_id or not model_id.strip():
|
|
62
|
+
raise ValueError("model_id cannot be empty")
|
|
63
|
+
|
|
57
64
|
if not text:
|
|
58
65
|
return 0
|
|
59
66
|
|
|
@@ -99,9 +106,18 @@ def estimate_workflow_cost(
|
|
|
99
106
|
"risk": "low" | "medium" | "high"
|
|
100
107
|
}
|
|
101
108
|
|
|
109
|
+
Raises:
|
|
110
|
+
ValueError: If workflow_name or provider is empty
|
|
111
|
+
|
|
102
112
|
"""
|
|
103
113
|
from .registry import get_model, get_supported_providers
|
|
104
114
|
|
|
115
|
+
# Pattern 1: String ID validation
|
|
116
|
+
if not workflow_name or not workflow_name.strip():
|
|
117
|
+
raise ValueError("workflow_name cannot be empty")
|
|
118
|
+
if not provider or not provider.strip():
|
|
119
|
+
raise ValueError("provider cannot be empty")
|
|
120
|
+
|
|
105
121
|
# Validate provider
|
|
106
122
|
if provider not in get_supported_providers():
|
|
107
123
|
provider = "anthropic" # Default fallback
|
|
@@ -292,10 +308,19 @@ def estimate_single_call_cost(
|
|
|
292
308
|
Returns:
|
|
293
309
|
Cost estimate dictionary
|
|
294
310
|
|
|
311
|
+
Raises:
|
|
312
|
+
ValueError: If task_type or provider is empty
|
|
313
|
+
|
|
295
314
|
"""
|
|
296
315
|
from .registry import get_model
|
|
297
316
|
from .tasks import get_tier_for_task
|
|
298
317
|
|
|
318
|
+
# Pattern 1: String ID validation
|
|
319
|
+
if not task_type or not task_type.strip():
|
|
320
|
+
raise ValueError("task_type cannot be empty")
|
|
321
|
+
if not provider or not provider.strip():
|
|
322
|
+
raise ValueError("provider cannot be empty")
|
|
323
|
+
|
|
299
324
|
input_tokens = estimate_tokens(text)
|
|
300
325
|
|
|
301
326
|
# Get tier for task
|