empathy-framework 5.2.1__py3-none-any.whl → 5.3.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.
- {empathy_framework-5.2.1.dist-info → empathy_framework-5.3.0.dist-info}/METADATA +28 -4
- {empathy_framework-5.2.1.dist-info → empathy_framework-5.3.0.dist-info}/RECORD +27 -49
- empathy_os/__init__.py +1 -1
- empathy_os/cache/hybrid.py +5 -1
- empathy_os/cli/commands/batch.py +8 -0
- empathy_os/cli/commands/profiling.py +4 -0
- empathy_os/cli/commands/workflow.py +8 -4
- empathy_os/config.py +15 -2
- empathy_os/dashboard/simple_server.py +62 -30
- empathy_os/memory/long_term.py +5 -5
- empathy_os/memory/mixins/backend_init_mixin.py +6 -1
- empathy_os/memory/mixins/capabilities_mixin.py +12 -3
- empathy_os/memory/short_term.py +54 -12
- empathy_os/memory/simple_storage.py +3 -3
- empathy_os/memory/types.py +8 -3
- empathy_os/telemetry/agent_coordination.py +2 -3
- empathy_os/telemetry/agent_tracking.py +26 -7
- empathy_os/telemetry/approval_gates.py +18 -24
- empathy_os/telemetry/event_streaming.py +7 -3
- empathy_os/telemetry/feedback_loop.py +28 -15
- empathy_os/workflows/output.py +4 -1
- empathy_os/workflows/progress.py +8 -2
- empathy_os/cli/parsers/cache 2.py +0 -65
- empathy_os/cli_router 2.py +0 -416
- empathy_os/dashboard/app 2.py +0 -512
- empathy_os/dashboard/simple_server 2.py +0 -403
- empathy_os/dashboard/standalone_server 2.py +0 -536
- empathy_os/models/adaptive_routing 2.py +0 -437
- empathy_os/project_index/scanner_parallel 2.py +0 -291
- empathy_os/telemetry/agent_coordination 2.py +0 -478
- empathy_os/telemetry/agent_tracking 2.py +0 -350
- empathy_os/telemetry/approval_gates 2.py +0 -563
- empathy_os/telemetry/event_streaming 2.py +0 -405
- empathy_os/telemetry/feedback_loop 2.py +0 -557
- empathy_os/vscode_bridge 2.py +0 -173
- empathy_os/workflows/document_gen.py +0 -29
- empathy_os/workflows/progressive/__init__ 2.py +0 -92
- empathy_os/workflows/progressive/cli 2.py +0 -242
- empathy_os/workflows/progressive/core 2.py +0 -488
- empathy_os/workflows/progressive/orchestrator 2.py +0 -701
- empathy_os/workflows/progressive/reports 2.py +0 -528
- empathy_os/workflows/progressive/telemetry 2.py +0 -280
- empathy_os/workflows/progressive/test_gen 2.py +0 -514
- empathy_os/workflows/progressive/workflow 2.py +0 -628
- {empathy_framework-5.2.1.dist-info → empathy_framework-5.3.0.dist-info}/WHEEL +0 -0
- {empathy_framework-5.2.1.dist-info → empathy_framework-5.3.0.dist-info}/entry_points.txt +0 -0
- {empathy_framework-5.2.1.dist-info → empathy_framework-5.3.0.dist-info}/licenses/LICENSE +0 -0
- {empathy_framework-5.2.1.dist-info → empathy_framework-5.3.0.dist-info}/licenses/LICENSE_CHANGE_ANNOUNCEMENT.md +0 -0
- {empathy_framework-5.2.1.dist-info → empathy_framework-5.3.0.dist-info}/top_level.txt +0 -0
|
@@ -146,7 +146,12 @@ class BackendInitMixin:
|
|
|
146
146
|
if self.config.redis_required and not (
|
|
147
147
|
self._redis_status and self._redis_status.available
|
|
148
148
|
):
|
|
149
|
-
raise RuntimeError(
|
|
149
|
+
raise RuntimeError(
|
|
150
|
+
"Redis is required but not available. "
|
|
151
|
+
f"Config requires Redis (redis_required=True, environment={self.config.environment.value}). "
|
|
152
|
+
"Either: (1) Start Redis server, (2) Set REDIS_URL environment variable, "
|
|
153
|
+
"or (3) Set redis_required=False in MemoryConfig."
|
|
154
|
+
)
|
|
150
155
|
|
|
151
156
|
except RuntimeError:
|
|
152
157
|
raise # Re-raise required Redis error
|
|
@@ -68,7 +68,10 @@ class CapabilitiesMixin:
|
|
|
68
68
|
|
|
69
69
|
"""
|
|
70
70
|
if self._short_term is None:
|
|
71
|
-
raise RuntimeError(
|
|
71
|
+
raise RuntimeError(
|
|
72
|
+
"Short-term memory not initialized. "
|
|
73
|
+
"Ensure Redis is running and UnifiedMemory was initialized with Redis enabled."
|
|
74
|
+
)
|
|
72
75
|
return self._short_term
|
|
73
76
|
|
|
74
77
|
@property
|
|
@@ -87,7 +90,10 @@ class CapabilitiesMixin:
|
|
|
87
90
|
|
|
88
91
|
"""
|
|
89
92
|
if self._simple_long_term is None:
|
|
90
|
-
raise RuntimeError(
|
|
93
|
+
raise RuntimeError(
|
|
94
|
+
"Long-term memory not initialized. "
|
|
95
|
+
"Ensure UnifiedMemory was initialized with long_term_enabled=True."
|
|
96
|
+
)
|
|
91
97
|
return self._simple_long_term
|
|
92
98
|
|
|
93
99
|
# =========================================================================
|
|
@@ -145,7 +151,10 @@ class CapabilitiesMixin:
|
|
|
145
151
|
RuntimeError: If file session memory is not initialized
|
|
146
152
|
"""
|
|
147
153
|
if self._file_session is None:
|
|
148
|
-
raise RuntimeError(
|
|
154
|
+
raise RuntimeError(
|
|
155
|
+
"File session memory not initialized. "
|
|
156
|
+
"File session tracking is automatically enabled when UnifiedMemory is initialized."
|
|
157
|
+
)
|
|
149
158
|
return self._file_session
|
|
150
159
|
|
|
151
160
|
def supports_realtime(self) -> bool:
|
empathy_os/memory/short_term.py
CHANGED
|
@@ -23,6 +23,7 @@ Licensed under Fair Source 0.9
|
|
|
23
23
|
"""
|
|
24
24
|
|
|
25
25
|
import json
|
|
26
|
+
import os
|
|
26
27
|
import threading
|
|
27
28
|
import time
|
|
28
29
|
from collections.abc import Callable
|
|
@@ -138,11 +139,25 @@ class RedisShortTermMemory:
|
|
|
138
139
|
if config is not None:
|
|
139
140
|
self._config = config
|
|
140
141
|
else:
|
|
142
|
+
# Check environment variable for Redis enablement (default: disabled)
|
|
143
|
+
redis_enabled = os.getenv("REDIS_ENABLED", "false").lower() in ("true", "1", "yes")
|
|
144
|
+
|
|
145
|
+
# Use environment variables for configuration if available
|
|
146
|
+
env_host = os.getenv("REDIS_HOST", host)
|
|
147
|
+
env_port = int(os.getenv("REDIS_PORT", str(port)))
|
|
148
|
+
env_db = int(os.getenv("REDIS_DB", str(db)))
|
|
149
|
+
env_password = os.getenv("REDIS_PASSWORD", password)
|
|
150
|
+
|
|
151
|
+
# If Redis is not enabled via env var, force mock mode
|
|
152
|
+
if not redis_enabled and not use_mock:
|
|
153
|
+
use_mock = True
|
|
154
|
+
logger.info("redis_disabled_via_env", message="Redis not enabled in environment, using mock mode")
|
|
155
|
+
|
|
141
156
|
self._config = RedisConfig(
|
|
142
|
-
host=
|
|
143
|
-
port=
|
|
144
|
-
db=
|
|
145
|
-
password=
|
|
157
|
+
host=env_host,
|
|
158
|
+
port=env_port,
|
|
159
|
+
db=env_db,
|
|
160
|
+
password=env_password if env_password else None,
|
|
146
161
|
use_mock=use_mock,
|
|
147
162
|
)
|
|
148
163
|
|
|
@@ -193,6 +208,33 @@ class RedisShortTermMemory:
|
|
|
193
208
|
else:
|
|
194
209
|
self._client = self._create_client_with_retry()
|
|
195
210
|
|
|
211
|
+
@property
|
|
212
|
+
def client(self) -> Any:
|
|
213
|
+
"""Get the Redis client instance.
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
Redis client instance or None if using mock mode
|
|
217
|
+
|
|
218
|
+
Example:
|
|
219
|
+
>>> memory = RedisShortTermMemory()
|
|
220
|
+
>>> if memory.client:
|
|
221
|
+
... print("Redis connected")
|
|
222
|
+
"""
|
|
223
|
+
return self._client
|
|
224
|
+
|
|
225
|
+
@property
|
|
226
|
+
def metrics(self) -> "RedisMetrics":
|
|
227
|
+
"""Get Redis metrics instance.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
RedisMetrics instance with connection and operation statistics
|
|
231
|
+
|
|
232
|
+
Example:
|
|
233
|
+
>>> memory = RedisShortTermMemory()
|
|
234
|
+
>>> print(f"Retries: {memory.metrics.retries_total}")
|
|
235
|
+
"""
|
|
236
|
+
return self._metrics
|
|
237
|
+
|
|
196
238
|
def _create_client_with_retry(self) -> Any:
|
|
197
239
|
"""Create Redis client with retry logic."""
|
|
198
240
|
max_attempts = self._config.retry_max_attempts
|
|
@@ -560,7 +602,7 @@ class RedisShortTermMemory:
|
|
|
560
602
|
"""
|
|
561
603
|
# Pattern 1: String ID validation
|
|
562
604
|
if not key or not key.strip():
|
|
563
|
-
raise ValueError("key cannot be empty")
|
|
605
|
+
raise ValueError(f"key cannot be empty. Got: {key!r}")
|
|
564
606
|
|
|
565
607
|
if not credentials.can_stage():
|
|
566
608
|
raise PermissionError(
|
|
@@ -612,7 +654,7 @@ class RedisShortTermMemory:
|
|
|
612
654
|
"""
|
|
613
655
|
# Pattern 1: String ID validation
|
|
614
656
|
if not key or not key.strip():
|
|
615
|
-
raise ValueError("key cannot be empty")
|
|
657
|
+
raise ValueError(f"key cannot be empty. Got: {key!r}")
|
|
616
658
|
|
|
617
659
|
owner = agent_id or credentials.agent_id
|
|
618
660
|
full_key = f"{self.PREFIX_WORKING}{owner}:{key}"
|
|
@@ -703,7 +745,7 @@ class RedisShortTermMemory:
|
|
|
703
745
|
"""
|
|
704
746
|
# Pattern 1: String ID validation
|
|
705
747
|
if not pattern_id or not pattern_id.strip():
|
|
706
|
-
raise ValueError("pattern_id cannot be empty")
|
|
748
|
+
raise ValueError(f"pattern_id cannot be empty. Got: {pattern_id!r}")
|
|
707
749
|
|
|
708
750
|
key = f"{self.PREFIX_STAGED}{pattern_id}"
|
|
709
751
|
raw = self._get(key)
|
|
@@ -824,7 +866,7 @@ class RedisShortTermMemory:
|
|
|
824
866
|
"""
|
|
825
867
|
# Pattern 1: String ID validation
|
|
826
868
|
if not conflict_id or not conflict_id.strip():
|
|
827
|
-
raise ValueError("conflict_id cannot be empty")
|
|
869
|
+
raise ValueError(f"conflict_id cannot be empty. Got: {conflict_id!r}")
|
|
828
870
|
|
|
829
871
|
# Pattern 5: Type validation
|
|
830
872
|
if not isinstance(positions, dict):
|
|
@@ -874,7 +916,7 @@ class RedisShortTermMemory:
|
|
|
874
916
|
"""
|
|
875
917
|
# Pattern 1: String ID validation
|
|
876
918
|
if not conflict_id or not conflict_id.strip():
|
|
877
|
-
raise ValueError("conflict_id cannot be empty")
|
|
919
|
+
raise ValueError(f"conflict_id cannot be empty. Got: {conflict_id!r}")
|
|
878
920
|
|
|
879
921
|
key = f"{self.PREFIX_CONFLICT}{conflict_id}"
|
|
880
922
|
raw = self._get(key)
|
|
@@ -949,7 +991,7 @@ class RedisShortTermMemory:
|
|
|
949
991
|
"""
|
|
950
992
|
# Pattern 1: String ID validation
|
|
951
993
|
if not session_id or not session_id.strip():
|
|
952
|
-
raise ValueError("session_id cannot be empty")
|
|
994
|
+
raise ValueError(f"session_id cannot be empty. Got: {session_id!r}")
|
|
953
995
|
|
|
954
996
|
# Pattern 5: Type validation
|
|
955
997
|
if metadata is not None and not isinstance(metadata, dict):
|
|
@@ -985,7 +1027,7 @@ class RedisShortTermMemory:
|
|
|
985
1027
|
"""
|
|
986
1028
|
# Pattern 1: String ID validation
|
|
987
1029
|
if not session_id or not session_id.strip():
|
|
988
|
-
raise ValueError("session_id cannot be empty")
|
|
1030
|
+
raise ValueError(f"session_id cannot be empty. Got: {session_id!r}")
|
|
989
1031
|
|
|
990
1032
|
key = f"{self.PREFIX_SESSION}{session_id}"
|
|
991
1033
|
raw = self._get(key)
|
|
@@ -2009,7 +2051,7 @@ class RedisShortTermMemory:
|
|
|
2009
2051
|
"""
|
|
2010
2052
|
# Pattern 1: String ID validation
|
|
2011
2053
|
if not pattern_id or not pattern_id.strip():
|
|
2012
|
-
raise ValueError("pattern_id cannot be empty")
|
|
2054
|
+
raise ValueError(f"pattern_id cannot be empty. Got: {pattern_id!r}")
|
|
2013
2055
|
|
|
2014
2056
|
# Pattern 4: Range validation
|
|
2015
2057
|
if not 0.0 <= min_confidence <= 1.0:
|
|
@@ -94,7 +94,7 @@ class LongTermMemory:
|
|
|
94
94
|
|
|
95
95
|
"""
|
|
96
96
|
if not key or not key.strip():
|
|
97
|
-
raise ValueError("key cannot be empty")
|
|
97
|
+
raise ValueError(f"key cannot be empty. Got: {key!r}")
|
|
98
98
|
|
|
99
99
|
# Validate key for path traversal attacks
|
|
100
100
|
if ".." in key or key.startswith("/") or "\x00" in key:
|
|
@@ -165,7 +165,7 @@ class LongTermMemory:
|
|
|
165
165
|
|
|
166
166
|
"""
|
|
167
167
|
if not key or not key.strip():
|
|
168
|
-
raise ValueError("key cannot be empty")
|
|
168
|
+
raise ValueError(f"key cannot be empty. Got: {key!r}")
|
|
169
169
|
|
|
170
170
|
try:
|
|
171
171
|
file_path = self.storage_path / f"{key}.json"
|
|
@@ -204,7 +204,7 @@ class LongTermMemory:
|
|
|
204
204
|
|
|
205
205
|
"""
|
|
206
206
|
if not key or not key.strip():
|
|
207
|
-
raise ValueError("key cannot be empty")
|
|
207
|
+
raise ValueError(f"key cannot be empty. Got: {key!r}")
|
|
208
208
|
|
|
209
209
|
try:
|
|
210
210
|
file_path = self.storage_path / f"{key}.json"
|
empathy_os/memory/types.py
CHANGED
|
@@ -188,6 +188,11 @@ class RedisMetrics:
|
|
|
188
188
|
return 100.0
|
|
189
189
|
return (self.operations_success / self.operations_total) * 100
|
|
190
190
|
|
|
191
|
+
@property
|
|
192
|
+
def total_requests(self) -> int:
|
|
193
|
+
"""Total requests (alias for operations_total for backward compatibility)."""
|
|
194
|
+
return self.operations_total
|
|
195
|
+
|
|
191
196
|
def to_dict(self) -> dict:
|
|
192
197
|
"""Convert metrics to dictionary for reporting and serialization.
|
|
193
198
|
|
|
@@ -297,11 +302,11 @@ class StagedPattern:
|
|
|
297
302
|
"""Validate fields after initialization"""
|
|
298
303
|
# Pattern 1: String ID validation
|
|
299
304
|
if not self.pattern_id or not self.pattern_id.strip():
|
|
300
|
-
raise ValueError("pattern_id cannot be empty")
|
|
305
|
+
raise ValueError(f"pattern_id cannot be empty. Got: {self.pattern_id!r}")
|
|
301
306
|
if not self.agent_id or not self.agent_id.strip():
|
|
302
|
-
raise ValueError("agent_id cannot be empty")
|
|
307
|
+
raise ValueError(f"agent_id cannot be empty. Got: {self.agent_id!r}")
|
|
303
308
|
if not self.pattern_type or not self.pattern_type.strip():
|
|
304
|
-
raise ValueError("pattern_type cannot be empty")
|
|
309
|
+
raise ValueError(f"pattern_type cannot be empty. Got: {self.pattern_type!r}")
|
|
305
310
|
|
|
306
311
|
# Pattern 4: Range validation for confidence
|
|
307
312
|
if not 0.0 <= self.confidence <= 1.0:
|
|
@@ -447,9 +447,8 @@ class CoordinationSignals:
|
|
|
447
447
|
return None
|
|
448
448
|
|
|
449
449
|
try:
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
elif hasattr(self.memory, "_client"):
|
|
450
|
+
# Use direct Redis access (signal keys are stored without prefix)
|
|
451
|
+
if hasattr(self.memory, "_client"):
|
|
453
452
|
import json
|
|
454
453
|
|
|
455
454
|
data = self.memory._client.get(key)
|
|
@@ -53,6 +53,7 @@ class AgentHeartbeat:
|
|
|
53
53
|
current_task: str
|
|
54
54
|
last_beat: datetime
|
|
55
55
|
metadata: dict[str, Any] = field(default_factory=dict)
|
|
56
|
+
display_name: str | None = None # Optional human-readable name for dashboard
|
|
56
57
|
|
|
57
58
|
def to_dict(self) -> dict[str, Any]:
|
|
58
59
|
"""Convert to dictionary for serialization."""
|
|
@@ -63,6 +64,7 @@ class AgentHeartbeat:
|
|
|
63
64
|
"current_task": self.current_task,
|
|
64
65
|
"last_beat": self.last_beat.isoformat() if isinstance(self.last_beat, datetime) else self.last_beat,
|
|
65
66
|
"metadata": self.metadata,
|
|
67
|
+
"display_name": self.display_name,
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
@classmethod
|
|
@@ -82,6 +84,7 @@ class AgentHeartbeat:
|
|
|
82
84
|
current_task=data["current_task"],
|
|
83
85
|
last_beat=last_beat,
|
|
84
86
|
metadata=data.get("metadata", {}),
|
|
87
|
+
display_name=data.get("display_name"),
|
|
85
88
|
)
|
|
86
89
|
|
|
87
90
|
|
|
@@ -110,6 +113,7 @@ class HeartbeatCoordinator:
|
|
|
110
113
|
"""
|
|
111
114
|
self.memory = memory
|
|
112
115
|
self.agent_id: str | None = None
|
|
116
|
+
self.display_name: str | None = None
|
|
113
117
|
self._enable_streaming = enable_streaming
|
|
114
118
|
self._event_streamer = None
|
|
115
119
|
|
|
@@ -142,20 +146,28 @@ class HeartbeatCoordinator:
|
|
|
142
146
|
|
|
143
147
|
return self._event_streamer
|
|
144
148
|
|
|
145
|
-
def start_heartbeat(
|
|
149
|
+
def start_heartbeat(
|
|
150
|
+
self, agent_id: str, metadata: dict[str, Any] | None = None, display_name: str | None = None
|
|
151
|
+
) -> None:
|
|
146
152
|
"""Start heartbeat for an agent.
|
|
147
153
|
|
|
148
154
|
Args:
|
|
149
155
|
agent_id: Unique agent identifier
|
|
150
156
|
metadata: Initial metadata (workflow, run_id, etc.)
|
|
157
|
+
display_name: Optional human-readable name for dashboard display
|
|
151
158
|
"""
|
|
152
159
|
if not self.memory:
|
|
153
160
|
logger.debug("Heartbeat tracking disabled (no memory backend)")
|
|
154
161
|
return
|
|
155
162
|
|
|
156
163
|
self.agent_id = agent_id
|
|
164
|
+
self.display_name = display_name
|
|
157
165
|
self._publish_heartbeat(
|
|
158
|
-
status="starting",
|
|
166
|
+
status="starting",
|
|
167
|
+
progress=0.0,
|
|
168
|
+
current_task="initializing",
|
|
169
|
+
metadata=metadata or {},
|
|
170
|
+
display_name=display_name,
|
|
159
171
|
)
|
|
160
172
|
|
|
161
173
|
def beat(self, status: str = "running", progress: float = 0.0, current_task: str = "") -> None:
|
|
@@ -169,7 +181,13 @@ class HeartbeatCoordinator:
|
|
|
169
181
|
if not self.agent_id or not self.memory:
|
|
170
182
|
return
|
|
171
183
|
|
|
172
|
-
self._publish_heartbeat(
|
|
184
|
+
self._publish_heartbeat(
|
|
185
|
+
status=status,
|
|
186
|
+
progress=progress,
|
|
187
|
+
current_task=current_task,
|
|
188
|
+
metadata={},
|
|
189
|
+
display_name=self.display_name,
|
|
190
|
+
)
|
|
173
191
|
|
|
174
192
|
def stop_heartbeat(self, final_status: str = "completed") -> None:
|
|
175
193
|
"""Stop heartbeat (agent finished).
|
|
@@ -192,7 +210,7 @@ class HeartbeatCoordinator:
|
|
|
192
210
|
self.agent_id = None
|
|
193
211
|
|
|
194
212
|
def _publish_heartbeat(
|
|
195
|
-
self, status: str, progress: float, current_task: str, metadata: dict[str, Any]
|
|
213
|
+
self, status: str, progress: float, current_task: str, metadata: dict[str, Any], display_name: str | None = None
|
|
196
214
|
) -> None:
|
|
197
215
|
"""Publish heartbeat to Redis with TTL and optionally to event stream."""
|
|
198
216
|
if not self.memory or not self.agent_id:
|
|
@@ -205,10 +223,11 @@ class HeartbeatCoordinator:
|
|
|
205
223
|
current_task=current_task,
|
|
206
224
|
last_beat=datetime.utcnow(),
|
|
207
225
|
metadata=metadata,
|
|
226
|
+
display_name=display_name,
|
|
208
227
|
)
|
|
209
228
|
|
|
210
229
|
# Store in Redis with TTL (Pattern 1)
|
|
211
|
-
key = f"heartbeat:{self.agent_id}"
|
|
230
|
+
key = f"empathy:heartbeat:{self.agent_id}"
|
|
212
231
|
try:
|
|
213
232
|
# Use direct Redis access for heartbeats (need custom 30s TTL)
|
|
214
233
|
if hasattr(self.memory, "_client") and self.memory._client:
|
|
@@ -243,9 +262,9 @@ class HeartbeatCoordinator:
|
|
|
243
262
|
return []
|
|
244
263
|
|
|
245
264
|
try:
|
|
246
|
-
# Scan for heartbeat:* keys
|
|
265
|
+
# Scan for empathy:heartbeat:* keys
|
|
247
266
|
if hasattr(self.memory, "_client") and self.memory._client:
|
|
248
|
-
keys = self.memory._client.keys("heartbeat:*")
|
|
267
|
+
keys = self.memory._client.keys("empathy:heartbeat:*")
|
|
249
268
|
else:
|
|
250
269
|
logger.warning("Cannot scan for heartbeats: no Redis access")
|
|
251
270
|
return []
|
|
@@ -458,19 +458,16 @@ class ApprovalGate:
|
|
|
458
458
|
if isinstance(key, bytes):
|
|
459
459
|
key = key.decode("utf-8")
|
|
460
460
|
|
|
461
|
-
# Retrieve request
|
|
462
|
-
|
|
463
|
-
data = self.memory.retrieve(key, credentials=None)
|
|
464
|
-
else:
|
|
465
|
-
import json
|
|
461
|
+
# Retrieve request - use direct Redis access (approval keys are stored without prefix)
|
|
462
|
+
import json
|
|
466
463
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
464
|
+
raw_data = self.memory._client.get(key)
|
|
465
|
+
if raw_data:
|
|
466
|
+
if isinstance(raw_data, bytes):
|
|
467
|
+
raw_data = raw_data.decode("utf-8")
|
|
468
|
+
data = json.loads(raw_data)
|
|
469
|
+
else:
|
|
470
|
+
data = None
|
|
474
471
|
|
|
475
472
|
if not data:
|
|
476
473
|
continue
|
|
@@ -513,19 +510,16 @@ class ApprovalGate:
|
|
|
513
510
|
if isinstance(key, bytes):
|
|
514
511
|
key = key.decode("utf-8")
|
|
515
512
|
|
|
516
|
-
# Retrieve request
|
|
517
|
-
|
|
518
|
-
data = self.memory.retrieve(key, credentials=None)
|
|
519
|
-
else:
|
|
520
|
-
import json
|
|
513
|
+
# Retrieve request - use direct Redis access (approval keys are stored without prefix)
|
|
514
|
+
import json
|
|
521
515
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
516
|
+
raw_data = self.memory._client.get(key)
|
|
517
|
+
if raw_data:
|
|
518
|
+
if isinstance(raw_data, bytes):
|
|
519
|
+
raw_data = raw_data.decode("utf-8")
|
|
520
|
+
data = json.loads(raw_data)
|
|
521
|
+
else:
|
|
522
|
+
data = None
|
|
529
523
|
|
|
530
524
|
if not data:
|
|
531
525
|
continue
|
|
@@ -66,13 +66,17 @@ class StreamEvent:
|
|
|
66
66
|
|
|
67
67
|
Args:
|
|
68
68
|
event_id: Redis stream entry ID
|
|
69
|
-
entry_data: Raw entry data from Redis (bytes dict)
|
|
69
|
+
entry_data: Raw entry data from Redis (bytes dict or str dict)
|
|
70
70
|
|
|
71
71
|
Returns:
|
|
72
72
|
StreamEvent instance
|
|
73
73
|
"""
|
|
74
|
-
# Decode bytes to strings
|
|
75
|
-
decoded = {
|
|
74
|
+
# Decode bytes to strings (handle both bytes and str)
|
|
75
|
+
decoded = {}
|
|
76
|
+
for k, v in entry_data.items():
|
|
77
|
+
key = k.decode("utf-8") if isinstance(k, bytes) else k
|
|
78
|
+
value = v.decode("utf-8") if isinstance(v, bytes) else v
|
|
79
|
+
decoded[key] = value
|
|
76
80
|
|
|
77
81
|
# Parse timestamp
|
|
78
82
|
timestamp_str = decoded.get("timestamp", "")
|
|
@@ -93,8 +93,13 @@ class FeedbackEntry:
|
|
|
93
93
|
elif not isinstance(timestamp, datetime):
|
|
94
94
|
timestamp = datetime.utcnow()
|
|
95
95
|
|
|
96
|
+
# Handle missing feedback_id (legacy entries)
|
|
97
|
+
feedback_id = data.get("feedback_id")
|
|
98
|
+
if not feedback_id:
|
|
99
|
+
feedback_id = f"fb-{int(timestamp.timestamp()*1000)}"
|
|
100
|
+
|
|
96
101
|
return cls(
|
|
97
|
-
feedback_id=
|
|
102
|
+
feedback_id=feedback_id,
|
|
98
103
|
workflow_name=data["workflow_name"],
|
|
99
104
|
stage_name=data["stage_name"],
|
|
100
105
|
tier=data["tier"],
|
|
@@ -284,7 +289,11 @@ class FeedbackLoop:
|
|
|
284
289
|
# Retrieve entry
|
|
285
290
|
data = self._retrieve_feedback(key)
|
|
286
291
|
if data:
|
|
287
|
-
|
|
292
|
+
try:
|
|
293
|
+
entries.append(FeedbackEntry.from_dict(data))
|
|
294
|
+
except Exception as e:
|
|
295
|
+
logger.error(f"Failed to parse feedback entry {key}: {e}, data={data}")
|
|
296
|
+
continue
|
|
288
297
|
|
|
289
298
|
if len(entries) >= limit:
|
|
290
299
|
break
|
|
@@ -303,9 +312,8 @@ class FeedbackLoop:
|
|
|
303
312
|
return None
|
|
304
313
|
|
|
305
314
|
try:
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
elif hasattr(self.memory, "_client"):
|
|
315
|
+
# Use direct Redis access (feedback keys are stored without prefix)
|
|
316
|
+
if hasattr(self.memory, "_client"):
|
|
309
317
|
import json
|
|
310
318
|
|
|
311
319
|
data = self.memory._client.get(key)
|
|
@@ -482,14 +490,15 @@ class FeedbackLoop:
|
|
|
482
490
|
def get_underperforming_stages(
|
|
483
491
|
self, workflow_name: str, quality_threshold: float = 0.7
|
|
484
492
|
) -> list[tuple[str, QualityStats]]:
|
|
485
|
-
"""Get workflow stages with poor quality scores.
|
|
493
|
+
"""Get workflow stages/tiers with poor quality scores.
|
|
486
494
|
|
|
487
495
|
Args:
|
|
488
496
|
workflow_name: Name of workflow
|
|
489
|
-
quality_threshold: Threshold below which stage is considered underperforming
|
|
497
|
+
quality_threshold: Threshold below which stage/tier is considered underperforming
|
|
490
498
|
|
|
491
499
|
Returns:
|
|
492
|
-
List of (stage_name, stats) tuples for underperforming
|
|
500
|
+
List of (stage_name, stats) tuples for underperforming stage/tier combinations
|
|
501
|
+
The stage_name includes the tier for clarity (e.g., "analysis/cheap")
|
|
493
502
|
"""
|
|
494
503
|
if not self.memory or not hasattr(self.memory, "_client"):
|
|
495
504
|
return []
|
|
@@ -499,22 +508,26 @@ class FeedbackLoop:
|
|
|
499
508
|
pattern = f"feedback:{workflow_name}:*"
|
|
500
509
|
keys = self.memory._client.keys(pattern)
|
|
501
510
|
|
|
502
|
-
# Extract unique
|
|
503
|
-
|
|
511
|
+
# Extract unique stage/tier combinations
|
|
512
|
+
stage_tier_combos = set()
|
|
504
513
|
for key in keys:
|
|
505
514
|
if isinstance(key, bytes):
|
|
506
515
|
key = key.decode("utf-8")
|
|
507
516
|
# Parse key: feedback:{workflow}:{stage}:{tier}:{id}
|
|
508
517
|
parts = key.split(":")
|
|
509
518
|
if len(parts) >= 4:
|
|
510
|
-
|
|
519
|
+
stage_name = parts[2]
|
|
520
|
+
tier = parts[3]
|
|
521
|
+
stage_tier_combos.add((stage_name, tier))
|
|
511
522
|
|
|
512
|
-
# Get stats for each stage
|
|
523
|
+
# Get stats for each stage/tier combination
|
|
513
524
|
underperforming = []
|
|
514
|
-
for stage_name in
|
|
515
|
-
stats = self.get_quality_stats(workflow_name, stage_name)
|
|
525
|
+
for stage_name, tier in stage_tier_combos:
|
|
526
|
+
stats = self.get_quality_stats(workflow_name, stage_name, tier=tier)
|
|
516
527
|
if stats and stats.avg_quality < quality_threshold:
|
|
517
|
-
|
|
528
|
+
# Include tier in the stage name for clarity
|
|
529
|
+
stage_label = f"{stage_name}/{tier}"
|
|
530
|
+
underperforming.append((stage_label, stats))
|
|
518
531
|
|
|
519
532
|
# Sort by quality (worst first)
|
|
520
533
|
underperforming.sort(key=lambda x: x[1].avg_quality)
|
empathy_os/workflows/output.py
CHANGED
|
@@ -326,7 +326,10 @@ class MetricsPanel:
|
|
|
326
326
|
Rich Panel with formatted score
|
|
327
327
|
"""
|
|
328
328
|
if not RICH_AVAILABLE or Panel is None:
|
|
329
|
-
raise RuntimeError(
|
|
329
|
+
raise RuntimeError(
|
|
330
|
+
"Rich library not available. "
|
|
331
|
+
"Install with: pip install rich"
|
|
332
|
+
)
|
|
330
333
|
|
|
331
334
|
style = cls.get_style(score)
|
|
332
335
|
icon = cls.get_icon(score)
|
empathy_os/workflows/progress.py
CHANGED
|
@@ -590,7 +590,10 @@ class RichProgressReporter:
|
|
|
590
590
|
stage_names: List of stage names for progress tracking
|
|
591
591
|
"""
|
|
592
592
|
if not RICH_AVAILABLE:
|
|
593
|
-
raise RuntimeError(
|
|
593
|
+
raise RuntimeError(
|
|
594
|
+
"Rich library required for RichProgressReporter. "
|
|
595
|
+
"Install with: pip install rich"
|
|
596
|
+
)
|
|
594
597
|
|
|
595
598
|
self.workflow_name = workflow_name
|
|
596
599
|
self.stage_names = stage_names
|
|
@@ -674,7 +677,10 @@ class RichProgressReporter:
|
|
|
674
677
|
Rich Panel containing progress information
|
|
675
678
|
"""
|
|
676
679
|
if not RICH_AVAILABLE or Panel is None or Table is None:
|
|
677
|
-
raise RuntimeError(
|
|
680
|
+
raise RuntimeError(
|
|
681
|
+
"Rich library not available. "
|
|
682
|
+
"Install with: pip install rich"
|
|
683
|
+
)
|
|
678
684
|
|
|
679
685
|
# Build metrics table
|
|
680
686
|
metrics = Table(show_header=False, box=None, padding=(0, 2))
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
"""Argument parser for cache commands.
|
|
2
|
-
|
|
3
|
-
Copyright 2025 Smart-AI-Memory
|
|
4
|
-
Licensed under Fair Source License 0.9
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def register_parsers(subparsers):
|
|
9
|
-
"""Register cache command parsers.
|
|
10
|
-
|
|
11
|
-
Args:
|
|
12
|
-
subparsers: Subparser object from main argument parser
|
|
13
|
-
|
|
14
|
-
Returns:
|
|
15
|
-
None: Adds cache subparser with stats and clear subcommands
|
|
16
|
-
"""
|
|
17
|
-
from ..commands.cache import cmd_cache_clear, cmd_cache_stats
|
|
18
|
-
# Main cache command
|
|
19
|
-
cache_parser = subparsers.add_parser(
|
|
20
|
-
"cache",
|
|
21
|
-
help="Cache monitoring and management",
|
|
22
|
-
description="Monitor prompt caching performance and cost savings",
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
# Cache subcommands
|
|
26
|
-
cache_subparsers = cache_parser.add_subparsers(dest="cache_command", required=True)
|
|
27
|
-
|
|
28
|
-
# cache stats command
|
|
29
|
-
stats_parser = cache_subparsers.add_parser(
|
|
30
|
-
"stats",
|
|
31
|
-
help="Show cache performance statistics",
|
|
32
|
-
description="Display prompt caching metrics including hit rate and cost savings",
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
stats_parser.add_argument(
|
|
36
|
-
"--days",
|
|
37
|
-
type=int,
|
|
38
|
-
default=7,
|
|
39
|
-
help="Number of days to analyze (default: 7)",
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
stats_parser.add_argument(
|
|
43
|
-
"--format",
|
|
44
|
-
choices=["table", "json"],
|
|
45
|
-
default="table",
|
|
46
|
-
help="Output format (default: table)",
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
stats_parser.add_argument(
|
|
50
|
-
"--verbose",
|
|
51
|
-
"-v",
|
|
52
|
-
action="store_true",
|
|
53
|
-
help="Show detailed token metrics",
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
stats_parser.set_defaults(func=cmd_cache_stats)
|
|
57
|
-
|
|
58
|
-
# cache clear command (placeholder)
|
|
59
|
-
clear_parser = cache_subparsers.add_parser(
|
|
60
|
-
"clear",
|
|
61
|
-
help="Clear cache (note: Anthropic cache is server-side with 5min TTL)",
|
|
62
|
-
description="Information about cache clearing",
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
clear_parser.set_defaults(func=cmd_cache_clear)
|