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.
Files changed (49) hide show
  1. {empathy_framework-5.2.1.dist-info → empathy_framework-5.3.0.dist-info}/METADATA +28 -4
  2. {empathy_framework-5.2.1.dist-info → empathy_framework-5.3.0.dist-info}/RECORD +27 -49
  3. empathy_os/__init__.py +1 -1
  4. empathy_os/cache/hybrid.py +5 -1
  5. empathy_os/cli/commands/batch.py +8 -0
  6. empathy_os/cli/commands/profiling.py +4 -0
  7. empathy_os/cli/commands/workflow.py +8 -4
  8. empathy_os/config.py +15 -2
  9. empathy_os/dashboard/simple_server.py +62 -30
  10. empathy_os/memory/long_term.py +5 -5
  11. empathy_os/memory/mixins/backend_init_mixin.py +6 -1
  12. empathy_os/memory/mixins/capabilities_mixin.py +12 -3
  13. empathy_os/memory/short_term.py +54 -12
  14. empathy_os/memory/simple_storage.py +3 -3
  15. empathy_os/memory/types.py +8 -3
  16. empathy_os/telemetry/agent_coordination.py +2 -3
  17. empathy_os/telemetry/agent_tracking.py +26 -7
  18. empathy_os/telemetry/approval_gates.py +18 -24
  19. empathy_os/telemetry/event_streaming.py +7 -3
  20. empathy_os/telemetry/feedback_loop.py +28 -15
  21. empathy_os/workflows/output.py +4 -1
  22. empathy_os/workflows/progress.py +8 -2
  23. empathy_os/cli/parsers/cache 2.py +0 -65
  24. empathy_os/cli_router 2.py +0 -416
  25. empathy_os/dashboard/app 2.py +0 -512
  26. empathy_os/dashboard/simple_server 2.py +0 -403
  27. empathy_os/dashboard/standalone_server 2.py +0 -536
  28. empathy_os/models/adaptive_routing 2.py +0 -437
  29. empathy_os/project_index/scanner_parallel 2.py +0 -291
  30. empathy_os/telemetry/agent_coordination 2.py +0 -478
  31. empathy_os/telemetry/agent_tracking 2.py +0 -350
  32. empathy_os/telemetry/approval_gates 2.py +0 -563
  33. empathy_os/telemetry/event_streaming 2.py +0 -405
  34. empathy_os/telemetry/feedback_loop 2.py +0 -557
  35. empathy_os/vscode_bridge 2.py +0 -173
  36. empathy_os/workflows/document_gen.py +0 -29
  37. empathy_os/workflows/progressive/__init__ 2.py +0 -92
  38. empathy_os/workflows/progressive/cli 2.py +0 -242
  39. empathy_os/workflows/progressive/core 2.py +0 -488
  40. empathy_os/workflows/progressive/orchestrator 2.py +0 -701
  41. empathy_os/workflows/progressive/reports 2.py +0 -528
  42. empathy_os/workflows/progressive/telemetry 2.py +0 -280
  43. empathy_os/workflows/progressive/test_gen 2.py +0 -514
  44. empathy_os/workflows/progressive/workflow 2.py +0 -628
  45. {empathy_framework-5.2.1.dist-info → empathy_framework-5.3.0.dist-info}/WHEEL +0 -0
  46. {empathy_framework-5.2.1.dist-info → empathy_framework-5.3.0.dist-info}/entry_points.txt +0 -0
  47. {empathy_framework-5.2.1.dist-info → empathy_framework-5.3.0.dist-info}/licenses/LICENSE +0 -0
  48. {empathy_framework-5.2.1.dist-info → empathy_framework-5.3.0.dist-info}/licenses/LICENSE_CHANGE_ANNOUNCEMENT.md +0 -0
  49. {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("Redis is required but not available")
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("Short-term memory not initialized")
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("Long-term memory not initialized")
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("File session memory not initialized")
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:
@@ -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=host,
143
- port=port,
144
- db=db,
145
- password=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"
@@ -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
- if hasattr(self.memory, "retrieve"):
451
- return self.memory.retrieve(key, credentials=None)
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(self, agent_id: str, metadata: dict[str, Any] | None = None) -> None:
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", progress=0.0, current_task="initializing", metadata=metadata or {}
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(status=status, progress=progress, current_task=current_task, metadata={})
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
- if hasattr(self.memory, "retrieve"):
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
- raw_data = self.memory._client.get(key)
468
- if raw_data:
469
- if isinstance(raw_data, bytes):
470
- raw_data = raw_data.decode("utf-8")
471
- data = json.loads(raw_data)
472
- else:
473
- data = None
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
- if hasattr(self.memory, "retrieve"):
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
- raw_data = self.memory._client.get(key)
523
- if raw_data:
524
- if isinstance(raw_data, bytes):
525
- raw_data = raw_data.decode("utf-8")
526
- data = json.loads(raw_data)
527
- else:
528
- data = None
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 = {k.decode("utf-8"): v.decode("utf-8") for k, v in entry_data.items()}
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=data["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
- entries.append(FeedbackEntry.from_dict(data))
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
- if hasattr(self.memory, "retrieve"):
307
- return self.memory.retrieve(key, credentials=None)
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 stages
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 stages
503
- stages = set()
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
- stages.add(parts[2])
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 stages:
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
- underperforming.append((stage_name, stats))
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)
@@ -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("Rich is not available")
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)
@@ -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("Rich library required for RichProgressReporter")
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("Rich not available")
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)