empathy-framework 5.0.1__py3-none-any.whl → 5.1.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 (61) hide show
  1. {empathy_framework-5.0.1.dist-info → empathy_framework-5.1.0.dist-info}/METADATA +311 -150
  2. {empathy_framework-5.0.1.dist-info → empathy_framework-5.1.0.dist-info}/RECORD +60 -33
  3. empathy_framework-5.1.0.dist-info/licenses/LICENSE +201 -0
  4. empathy_framework-5.1.0.dist-info/licenses/LICENSE_CHANGE_ANNOUNCEMENT.md +101 -0
  5. empathy_llm_toolkit/providers.py +175 -35
  6. empathy_llm_toolkit/utils/tokens.py +150 -30
  7. empathy_os/__init__.py +1 -1
  8. empathy_os/cli/commands/batch.py +256 -0
  9. empathy_os/cli/commands/cache.py +248 -0
  10. empathy_os/cli/commands/inspect.py +1 -2
  11. empathy_os/cli/commands/metrics.py +1 -1
  12. empathy_os/cli/commands/routing.py +285 -0
  13. empathy_os/cli/commands/workflow.py +2 -1
  14. empathy_os/cli/parsers/__init__.py +6 -0
  15. empathy_os/cli/parsers/batch.py +118 -0
  16. empathy_os/cli/parsers/cache 2.py +65 -0
  17. empathy_os/cli/parsers/cache.py +65 -0
  18. empathy_os/cli/parsers/routing.py +110 -0
  19. empathy_os/cli_minimal.py +3 -3
  20. empathy_os/cli_router 2.py +416 -0
  21. empathy_os/dashboard/__init__.py +1 -2
  22. empathy_os/dashboard/app 2.py +512 -0
  23. empathy_os/dashboard/app.py +1 -1
  24. empathy_os/dashboard/simple_server 2.py +403 -0
  25. empathy_os/dashboard/standalone_server 2.py +536 -0
  26. empathy_os/dashboard/standalone_server.py +22 -11
  27. empathy_os/memory/types 2.py +441 -0
  28. empathy_os/metrics/collector.py +31 -0
  29. empathy_os/models/__init__.py +19 -0
  30. empathy_os/models/adaptive_routing 2.py +437 -0
  31. empathy_os/models/auth_cli.py +444 -0
  32. empathy_os/models/auth_strategy.py +450 -0
  33. empathy_os/models/token_estimator.py +21 -13
  34. empathy_os/project_index/scanner_parallel 2.py +291 -0
  35. empathy_os/telemetry/agent_coordination 2.py +478 -0
  36. empathy_os/telemetry/agent_coordination.py +14 -16
  37. empathy_os/telemetry/agent_tracking 2.py +350 -0
  38. empathy_os/telemetry/agent_tracking.py +18 -20
  39. empathy_os/telemetry/approval_gates 2.py +563 -0
  40. empathy_os/telemetry/approval_gates.py +27 -39
  41. empathy_os/telemetry/event_streaming 2.py +405 -0
  42. empathy_os/telemetry/event_streaming.py +22 -22
  43. empathy_os/telemetry/feedback_loop 2.py +557 -0
  44. empathy_os/telemetry/feedback_loop.py +14 -17
  45. empathy_os/workflows/__init__.py +8 -0
  46. empathy_os/workflows/autonomous_test_gen.py +569 -0
  47. empathy_os/workflows/batch_processing.py +56 -10
  48. empathy_os/workflows/bug_predict.py +45 -0
  49. empathy_os/workflows/code_review.py +92 -22
  50. empathy_os/workflows/document_gen.py +594 -62
  51. empathy_os/workflows/llm_base.py +363 -0
  52. empathy_os/workflows/perf_audit.py +69 -0
  53. empathy_os/workflows/release_prep.py +54 -0
  54. empathy_os/workflows/security_audit.py +154 -79
  55. empathy_os/workflows/test_gen.py +60 -0
  56. empathy_os/workflows/test_gen_behavioral.py +477 -0
  57. empathy_os/workflows/test_gen_parallel.py +341 -0
  58. empathy_framework-5.0.1.dist-info/licenses/LICENSE +0 -139
  59. {empathy_framework-5.0.1.dist-info → empathy_framework-5.1.0.dist-info}/WHEEL +0 -0
  60. {empathy_framework-5.0.1.dist-info → empathy_framework-5.1.0.dist-info}/entry_points.txt +0 -0
  61. {empathy_framework-5.0.1.dist-info → empathy_framework-5.1.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,350 @@
1
+ """Agent Heartbeat Tracking System.
2
+
3
+ Pattern 1 from Agent Coordination Architecture - TTL-based heartbeat monitoring
4
+ for tracking agent execution status and detecting stale/failed agents.
5
+
6
+ Usage:
7
+ # Start tracking an agent
8
+ coordinator = HeartbeatCoordinator()
9
+ coordinator.start_heartbeat(
10
+ agent_id="code-review-abc123",
11
+ metadata={"workflow": "code-review", "run_id": "xyz"}
12
+ )
13
+
14
+ # Update progress
15
+ coordinator.beat(
16
+ status="running",
17
+ progress=0.5,
18
+ current_task="Analyzing functions"
19
+ )
20
+
21
+ # Stop tracking
22
+ coordinator.stop_heartbeat(final_status="completed")
23
+
24
+ # Monitor all active agents
25
+ active_agents = coordinator.get_active_agents()
26
+ for agent in active_agents:
27
+ print(f"{agent.agent_id}: {agent.status} - {agent.current_task}")
28
+
29
+ Copyright 2025 Smart-AI-Memory
30
+ Licensed under Fair Source License 0.9
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import logging
36
+ import time
37
+ from dataclasses import dataclass, field
38
+ from datetime import datetime
39
+ from typing import Any
40
+
41
+ logger = logging.getLogger(__name__)
42
+
43
+
44
+ @dataclass
45
+ class AgentHeartbeat:
46
+ """Agent heartbeat data structure.
47
+
48
+ Represents the current state of a running agent, stored in Redis with TTL.
49
+ """
50
+
51
+ agent_id: str
52
+ status: str # "starting", "running", "completed", "failed", "cancelled"
53
+ progress: float # 0.0 to 1.0
54
+ current_task: str
55
+ last_beat: datetime
56
+ metadata: dict[str, Any] = field(default_factory=dict)
57
+
58
+ def to_dict(self) -> dict[str, Any]:
59
+ """Convert to dictionary for serialization."""
60
+ return {
61
+ "agent_id": self.agent_id,
62
+ "status": self.status,
63
+ "progress": self.progress,
64
+ "current_task": self.current_task,
65
+ "last_beat": self.last_beat.isoformat() if isinstance(self.last_beat, datetime) else self.last_beat,
66
+ "metadata": self.metadata,
67
+ }
68
+
69
+ @classmethod
70
+ def from_dict(cls, data: dict[str, Any]) -> AgentHeartbeat:
71
+ """Create from dictionary."""
72
+ # Convert ISO string back to datetime
73
+ last_beat = data.get("last_beat")
74
+ if isinstance(last_beat, str):
75
+ last_beat = datetime.fromisoformat(last_beat)
76
+ elif not isinstance(last_beat, datetime):
77
+ last_beat = datetime.utcnow()
78
+
79
+ return cls(
80
+ agent_id=data["agent_id"],
81
+ status=data["status"],
82
+ progress=data["progress"],
83
+ current_task=data["current_task"],
84
+ last_beat=last_beat,
85
+ metadata=data.get("metadata", {}),
86
+ )
87
+
88
+
89
+ class HeartbeatCoordinator:
90
+ """Coordinates agent heartbeats using Redis TTL keys.
91
+
92
+ Agents publish heartbeats with a TTL. If an agent stops responding,
93
+ its heartbeat key expires automatically, indicating failure/crash.
94
+
95
+ Attributes:
96
+ HEARTBEAT_TTL: Default heartbeat TTL in seconds (30s)
97
+ HEARTBEAT_INTERVAL: Recommended update interval (10s)
98
+ """
99
+
100
+ HEARTBEAT_TTL = 30 # Heartbeat expires after 30s of no updates
101
+ HEARTBEAT_INTERVAL = 10 # Agents should update every 10s
102
+
103
+ def __init__(self, memory=None, enable_streaming: bool = False):
104
+ """Initialize heartbeat coordinator.
105
+
106
+ Args:
107
+ memory: Memory instance (UnifiedMemory or ShortTermMemory).
108
+ If None, attempts to get from UsageTracker.
109
+ enable_streaming: If True, publish heartbeat events to Redis Streams
110
+ for real-time monitoring (Pattern 4).
111
+ """
112
+ self.memory = memory
113
+ self.agent_id: str | None = None
114
+ self._enable_streaming = enable_streaming
115
+ self._event_streamer = None
116
+
117
+ if self.memory is None:
118
+ try:
119
+ from empathy_os.telemetry import UsageTracker
120
+
121
+ tracker = UsageTracker.get_instance()
122
+ if hasattr(tracker, "_memory"):
123
+ self.memory = tracker._memory
124
+ except (ImportError, AttributeError):
125
+ pass
126
+
127
+ if self.memory is None:
128
+ logger.warning("No memory backend available for heartbeat tracking")
129
+
130
+ def _get_event_streamer(self):
131
+ """Get or create EventStreamer instance (lazy initialization)."""
132
+ if not self._enable_streaming:
133
+ return None
134
+
135
+ if self._event_streamer is None:
136
+ try:
137
+ from empathy_os.telemetry.event_streaming import EventStreamer
138
+
139
+ self._event_streamer = EventStreamer(memory=self.memory)
140
+ except Exception as e:
141
+ logger.warning(f"Failed to initialize EventStreamer: {e}")
142
+ self._enable_streaming = False
143
+
144
+ return self._event_streamer
145
+
146
+ def start_heartbeat(self, agent_id: str, metadata: dict[str, Any] | None = None) -> None:
147
+ """Start heartbeat for an agent.
148
+
149
+ Args:
150
+ agent_id: Unique agent identifier
151
+ metadata: Initial metadata (workflow, run_id, etc.)
152
+ """
153
+ if not self.memory:
154
+ logger.debug("Heartbeat tracking disabled (no memory backend)")
155
+ return
156
+
157
+ self.agent_id = agent_id
158
+ self._publish_heartbeat(
159
+ status="starting", progress=0.0, current_task="initializing", metadata=metadata or {}
160
+ )
161
+
162
+ def beat(self, status: str = "running", progress: float = 0.0, current_task: str = "") -> None:
163
+ """Publish heartbeat update.
164
+
165
+ Args:
166
+ status: Current agent status
167
+ progress: Progress percentage (0.0 - 1.0)
168
+ current_task: Human-readable current task description
169
+ """
170
+ if not self.agent_id or not self.memory:
171
+ return
172
+
173
+ self._publish_heartbeat(status=status, progress=progress, current_task=current_task, metadata={})
174
+
175
+ def stop_heartbeat(self, final_status: str = "completed") -> None:
176
+ """Stop heartbeat (agent finished).
177
+
178
+ Args:
179
+ final_status: Final status ("completed", "failed", "cancelled")
180
+ """
181
+ if not self.agent_id or not self.memory:
182
+ return
183
+
184
+ # Publish final heartbeat with short TTL
185
+ self._publish_heartbeat(
186
+ status=final_status,
187
+ progress=1.0,
188
+ current_task="finished",
189
+ metadata={"final": True},
190
+ )
191
+
192
+ # Clear agent ID
193
+ self.agent_id = None
194
+
195
+ def _publish_heartbeat(
196
+ self, status: str, progress: float, current_task: str, metadata: dict[str, Any]
197
+ ) -> None:
198
+ """Publish heartbeat to Redis with TTL and optionally to event stream."""
199
+ if not self.memory or not self.agent_id:
200
+ return
201
+
202
+ heartbeat = AgentHeartbeat(
203
+ agent_id=self.agent_id,
204
+ status=status,
205
+ progress=progress,
206
+ current_task=current_task,
207
+ last_beat=datetime.utcnow(),
208
+ metadata=metadata,
209
+ )
210
+
211
+ # Store in Redis with TTL (Pattern 1)
212
+ key = f"heartbeat:{self.agent_id}"
213
+ try:
214
+ # Use stash if available (UnifiedMemory), otherwise try Redis directly
215
+ if hasattr(self.memory, "stash"):
216
+ self.memory.stash(
217
+ key=key, data=heartbeat.to_dict(), credentials=None, ttl_seconds=self.HEARTBEAT_TTL
218
+ )
219
+ elif hasattr(self.memory, "_redis"):
220
+ # Direct Redis access for ShortTermMemory
221
+ import json
222
+
223
+ self.memory._redis.setex(key, self.HEARTBEAT_TTL, json.dumps(heartbeat.to_dict()))
224
+ else:
225
+ logger.warning(f"Cannot publish heartbeat: unsupported memory type {type(self.memory)}")
226
+ except Exception as e:
227
+ logger.warning(f"Failed to publish heartbeat for {self.agent_id}: {e}")
228
+
229
+ # Publish to event stream (Pattern 4 - optional)
230
+ streamer = self._get_event_streamer()
231
+ if streamer:
232
+ try:
233
+ streamer.publish_event(
234
+ event_type="agent_heartbeat",
235
+ data=heartbeat.to_dict(),
236
+ source="empathy_os",
237
+ )
238
+ except Exception as e:
239
+ logger.debug(f"Failed to publish heartbeat event to stream: {e}")
240
+
241
+ def get_active_agents(self) -> list[AgentHeartbeat]:
242
+ """Get all currently active agents.
243
+
244
+ Returns:
245
+ List of active agent heartbeats
246
+ """
247
+ if not self.memory:
248
+ return []
249
+
250
+ try:
251
+ # Scan for heartbeat:* keys
252
+ if hasattr(self.memory, "_redis"):
253
+ keys = self.memory._redis.keys("heartbeat:*")
254
+ else:
255
+ logger.warning("Cannot scan for heartbeats: no Redis access")
256
+ return []
257
+
258
+ heartbeats = []
259
+ for key in keys:
260
+ if isinstance(key, bytes):
261
+ key = key.decode("utf-8")
262
+
263
+ data = self._retrieve_heartbeat(key)
264
+ if data:
265
+ heartbeats.append(AgentHeartbeat.from_dict(data))
266
+
267
+ return heartbeats
268
+ except Exception as e:
269
+ logger.error(f"Failed to get active agents: {e}")
270
+ return []
271
+
272
+ def is_agent_alive(self, agent_id: str) -> bool:
273
+ """Check if agent is still alive.
274
+
275
+ Args:
276
+ agent_id: Agent to check
277
+
278
+ Returns:
279
+ True if heartbeat key exists (agent is alive)
280
+ """
281
+ if not self.memory:
282
+ return False
283
+
284
+ key = f"heartbeat:{agent_id}"
285
+ data = self._retrieve_heartbeat(key)
286
+ return data is not None
287
+
288
+ def get_agent_status(self, agent_id: str) -> AgentHeartbeat | None:
289
+ """Get current status of an agent.
290
+
291
+ Args:
292
+ agent_id: Agent to query
293
+
294
+ Returns:
295
+ AgentHeartbeat or None if agent not active
296
+ """
297
+ if not self.memory:
298
+ return None
299
+
300
+ key = f"heartbeat:{agent_id}"
301
+ data = self._retrieve_heartbeat(key)
302
+
303
+ if data:
304
+ return AgentHeartbeat.from_dict(data)
305
+ return None
306
+
307
+ def _retrieve_heartbeat(self, key: str) -> dict[str, Any] | None:
308
+ """Retrieve heartbeat data from memory."""
309
+ if not self.memory:
310
+ return None
311
+
312
+ try:
313
+ # Try retrieve method first (UnifiedMemory)
314
+ if hasattr(self.memory, "retrieve"):
315
+ return self.memory.retrieve(key, credentials=None)
316
+ # Try direct Redis access
317
+ elif hasattr(self.memory, "_redis"):
318
+ import json
319
+
320
+ data = self.memory._redis.get(key)
321
+ if data:
322
+ if isinstance(data, bytes):
323
+ data = data.decode("utf-8")
324
+ return json.loads(data)
325
+ return None
326
+ except Exception as e:
327
+ logger.debug(f"Failed to retrieve heartbeat {key}: {e}")
328
+ return None
329
+
330
+ def get_stale_agents(self, threshold_seconds: float = 60.0) -> list[AgentHeartbeat]:
331
+ """Get agents that haven't updated in a while (but key still exists).
332
+
333
+ This detects agents that are stuck or slow, not crashed (TTL would expire).
334
+
335
+ Args:
336
+ threshold_seconds: Time without update to consider stale
337
+
338
+ Returns:
339
+ List of stale agent heartbeats
340
+ """
341
+ active = self.get_active_agents()
342
+ now = datetime.utcnow()
343
+ stale = []
344
+
345
+ for agent in active:
346
+ time_since_beat = (now - agent.last_beat).total_seconds()
347
+ if time_since_beat > threshold_seconds and agent.status not in ("completed", "failed", "cancelled"):
348
+ stale.append(agent)
349
+
350
+ return stale
@@ -33,7 +33,6 @@ Licensed under Fair Source License 0.9
33
33
  from __future__ import annotations
34
34
 
35
35
  import logging
36
- import time
37
36
  from dataclasses import dataclass, field
38
37
  from datetime import datetime
39
38
  from typing import Any
@@ -211,18 +210,14 @@ class HeartbeatCoordinator:
211
210
  # Store in Redis with TTL (Pattern 1)
212
211
  key = f"heartbeat:{self.agent_id}"
213
212
  try:
214
- # Use stash if available (UnifiedMemory), otherwise try Redis directly
215
- if hasattr(self.memory, "stash"):
216
- self.memory.stash(
217
- key=key, data=heartbeat.to_dict(), credentials=None, ttl_seconds=self.HEARTBEAT_TTL
218
- )
219
- elif hasattr(self.memory, "_redis"):
220
- # Direct Redis access for ShortTermMemory
213
+ # Use direct Redis access for heartbeats (need custom 30s TTL)
214
+ if hasattr(self.memory, "_client") and self.memory._client:
215
+ # Direct Redis access with setex for custom TTL
221
216
  import json
222
217
 
223
- self.memory._redis.setex(key, self.HEARTBEAT_TTL, json.dumps(heartbeat.to_dict()))
218
+ self.memory._client.setex(key, self.HEARTBEAT_TTL, json.dumps(heartbeat.to_dict()))
224
219
  else:
225
- logger.warning(f"Cannot publish heartbeat: unsupported memory type {type(self.memory)}")
220
+ logger.warning("Cannot publish heartbeat: no Redis backend available")
226
221
  except Exception as e:
227
222
  logger.warning(f"Failed to publish heartbeat for {self.agent_id}: {e}")
228
223
 
@@ -249,8 +244,8 @@ class HeartbeatCoordinator:
249
244
 
250
245
  try:
251
246
  # Scan for heartbeat:* keys
252
- if hasattr(self.memory, "_redis"):
253
- keys = self.memory._redis.keys("heartbeat:*")
247
+ if hasattr(self.memory, "_client") and self.memory._client:
248
+ keys = self.memory._client.keys("heartbeat:*")
254
249
  else:
255
250
  logger.warning("Cannot scan for heartbeats: no Redis access")
256
251
  return []
@@ -305,23 +300,26 @@ class HeartbeatCoordinator:
305
300
  return None
306
301
 
307
302
  def _retrieve_heartbeat(self, key: str) -> dict[str, Any] | None:
308
- """Retrieve heartbeat data from memory."""
303
+ """Retrieve heartbeat data from memory.
304
+
305
+ Heartbeat keys are stored directly as 'heartbeat:{agent_id}' and must be
306
+ retrieved via direct Redis access, not through the standard retrieve() method
307
+ which expects keys with 'working:{agent_id}:{key}' format.
308
+ """
309
309
  if not self.memory:
310
310
  return None
311
311
 
312
312
  try:
313
- # Try retrieve method first (UnifiedMemory)
314
- if hasattr(self.memory, "retrieve"):
315
- return self.memory.retrieve(key, credentials=None)
316
- # Try direct Redis access
317
- elif hasattr(self.memory, "_redis"):
313
+ # Use direct Redis access for heartbeat keys
314
+ if hasattr(self.memory, "_client") and self.memory._client:
318
315
  import json
319
316
 
320
- data = self.memory._redis.get(key)
317
+ data = self.memory._client.get(key)
321
318
  if data:
322
319
  if isinstance(data, bytes):
323
320
  data = data.decode("utf-8")
324
- return json.loads(data)
321
+ result = json.loads(data)
322
+ return result if isinstance(result, dict) else None
325
323
  return None
326
324
  except Exception as e:
327
325
  logger.debug(f"Failed to retrieve heartbeat {key}: {e}")