empathy-framework 4.9.0__py3-none-any.whl → 5.0.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 (47) hide show
  1. {empathy_framework-4.9.0.dist-info → empathy_framework-5.0.0.dist-info}/METADATA +64 -25
  2. {empathy_framework-4.9.0.dist-info → empathy_framework-5.0.0.dist-info}/RECORD +47 -26
  3. empathy_os/__init__.py +2 -2
  4. empathy_os/cache/hash_only.py +6 -3
  5. empathy_os/cache/hybrid.py +6 -3
  6. empathy_os/cli_legacy.py +27 -1
  7. empathy_os/cli_minimal.py +512 -15
  8. empathy_os/cli_router.py +145 -113
  9. empathy_os/cli_unified.py +25 -0
  10. empathy_os/dashboard/__init__.py +42 -0
  11. empathy_os/dashboard/app.py +512 -0
  12. empathy_os/dashboard/simple_server.py +403 -0
  13. empathy_os/dashboard/standalone_server.py +536 -0
  14. empathy_os/memory/__init__.py +19 -5
  15. empathy_os/memory/short_term.py +4 -70
  16. empathy_os/memory/types.py +2 -2
  17. empathy_os/models/__init__.py +3 -0
  18. empathy_os/models/adaptive_routing.py +437 -0
  19. empathy_os/models/registry.py +4 -4
  20. empathy_os/socratic/ab_testing.py +1 -1
  21. empathy_os/telemetry/__init__.py +29 -1
  22. empathy_os/telemetry/agent_coordination.py +478 -0
  23. empathy_os/telemetry/agent_tracking.py +350 -0
  24. empathy_os/telemetry/approval_gates.py +563 -0
  25. empathy_os/telemetry/event_streaming.py +405 -0
  26. empathy_os/telemetry/feedback_loop.py +557 -0
  27. empathy_os/vscode_bridge 2.py +173 -0
  28. empathy_os/workflows/__init__.py +4 -4
  29. empathy_os/workflows/base.py +495 -43
  30. empathy_os/workflows/history.py +3 -5
  31. empathy_os/workflows/output.py +410 -0
  32. empathy_os/workflows/progress.py +324 -22
  33. empathy_os/workflows/progressive/README 2.md +454 -0
  34. empathy_os/workflows/progressive/__init__ 2.py +92 -0
  35. empathy_os/workflows/progressive/cli 2.py +242 -0
  36. empathy_os/workflows/progressive/core 2.py +488 -0
  37. empathy_os/workflows/progressive/orchestrator 2.py +701 -0
  38. empathy_os/workflows/progressive/reports 2.py +528 -0
  39. empathy_os/workflows/progressive/telemetry 2.py +280 -0
  40. empathy_os/workflows/progressive/test_gen 2.py +514 -0
  41. empathy_os/workflows/progressive/workflow 2.py +628 -0
  42. empathy_os/workflows/routing.py +5 -0
  43. empathy_os/workflows/security_audit.py +189 -0
  44. {empathy_framework-4.9.0.dist-info → empathy_framework-5.0.0.dist-info}/WHEEL +0 -0
  45. {empathy_framework-4.9.0.dist-info → empathy_framework-5.0.0.dist-info}/entry_points.txt +0 -0
  46. {empathy_framework-4.9.0.dist-info → empathy_framework-5.0.0.dist-info}/licenses/LICENSE +0 -0
  47. {empathy_framework-4.9.0.dist-info → empathy_framework-5.0.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