Claude Code Claude Opus 4.6 commited on
Commit
34e2e40
·
1 Parent(s): d664b0a

Claude Code: refactor app.py with AgentRouter communication manager

Browse files

- Add AgentRouter class for centralized message routing between agents
- Implement asyncio.Queue for each agent to prevent state corruption
- Add AgentState model with last_heartbeat timestamp tracking
- Add /health/agents endpoint returning status of all agents
- Add /agents/send, /agents/{agent_id}/receive, /agents/{agent_id}/state endpoints for inter-agent communication
- Integrate agent router with FastAPI lifespan management

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Files changed (1) hide show
  1. app.py +202 -1
app.py CHANGED
@@ -9,6 +9,8 @@ from fastapi.staticfiles import StaticFiles
9
  from fastapi.responses import FileResponse
10
  from fastapi import WebSocket
11
  from pydantic import BaseModel
 
 
12
  import os
13
  import sys
14
  from datetime import datetime
@@ -30,19 +32,132 @@ from error_handlers import handle_status_file_read, handle_brain_response, handl
30
  START_TIME = time.time()
31
 
32
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  @asynccontextmanager
34
  async def lifespan(app: FastAPI):
35
  """Lifespan context manager for startup/shutdown events."""
36
  # Startup: log system startup
37
  log_startup("Cain", "1.0.0")
38
 
 
 
 
39
  # Start heartbeat task
40
  heartbeat_task = asyncio.create_task(heartbeat_loop())
41
 
42
  yield
43
 
44
- # Shutdown: cancel heartbeat
45
  heartbeat_task.cancel()
 
46
 
47
 
48
  # Background heartbeat loop
@@ -149,6 +264,55 @@ async def chat(msg: ChatMessage):
149
  }
150
 
151
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  @app.websocket("/ws")
153
  async def websocket_endpoint(websocket: WebSocket):
154
  """WebSocket endpoint for real-time dashboard updates."""
@@ -219,6 +383,43 @@ async def debug_health():
219
  }
220
 
221
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
  if __name__ == "__main__":
223
  import uvicorn
224
  uvicorn.run(app, host="0.0.0.0", port=7860)
 
9
  from fastapi.responses import FileResponse
10
  from fastapi import WebSocket
11
  from pydantic import BaseModel
12
+ from typing import Optional, Dict, List
13
+ from enum import Enum
14
  import os
15
  import sys
16
  from datetime import datetime
 
32
  START_TIME = time.time()
33
 
34
 
35
+ # ============================================================================
36
+ # Agent Communication Manager
37
+ # ============================================================================
38
+
39
+ class AgentRole(str, Enum):
40
+ """Agent roles in the HuggingClaw World family."""
41
+ ADAM = "adam"
42
+ EVE = "eve"
43
+ CAIN = "cain"
44
+
45
+
46
+ class AgentMessage(BaseModel):
47
+ """Message structure for inter-agent communication."""
48
+ sender: AgentRole
49
+ recipient: AgentRole
50
+ content: str
51
+ timestamp: str
52
+ message_id: Optional[str] = None
53
+
54
+
55
+ class AgentState(BaseModel):
56
+ """State tracking for each agent including heartbeat."""
57
+ agent_id: AgentRole
58
+ current_state: str = "idle"
59
+ last_heartbeat: float = 0.0
60
+ message_queue_size: int = 0
61
+ is_active: bool = False
62
+
63
+
64
+ class AgentRouter:
65
+ """
66
+ Centralized message router for inter-agent communication.
67
+ Uses asyncio.Queue to prevent state corruption.
68
+ """
69
+
70
+ def __init__(self):
71
+ self._queues: Dict[AgentRole, asyncio.Queue] = {
72
+ AgentRole.ADAM: asyncio.Queue(),
73
+ AgentRole.EVE: asyncio.Queue(),
74
+ AgentRole.CAIN: asyncio.Queue(),
75
+ }
76
+ self._states: Dict[AgentRole, AgentState] = {
77
+ role: AgentState(agent_id=role, last_heartbeat=time.time())
78
+ for role in AgentRole
79
+ }
80
+ self._heartbeat_task: Optional[asyncio.Task] = None
81
+
82
+ async def start(self):
83
+ """Start the agent router background tasks."""
84
+ self._heartbeat_task = asyncio.create_task(self._heartbeat_loop())
85
+
86
+ async def stop(self):
87
+ """Stop the agent router background tasks."""
88
+ if self._heartbeat_task:
89
+ self._heartbeat_task.cancel()
90
+ try:
91
+ await self._heartbeat_task
92
+ except asyncio.CancelledError:
93
+ pass
94
+
95
+ async def _heartbeat_loop(self):
96
+ """Update heartbeat timestamps every 10 seconds."""
97
+ while True:
98
+ await asyncio.sleep(10)
99
+ now = time.time()
100
+ for state in self._states.values():
101
+ state.last_heartbeat = now
102
+ state.message_queue_size = self._queues[state.agent_id].qsize()
103
+ # Mark inactive if no heartbeat for 30 seconds
104
+ state.is_active = (now - state.last_heartbeat) < 30
105
+
106
+ async def send_message(self, message: AgentMessage) -> bool:
107
+ """Send a message to a specific agent's queue."""
108
+ recipient = message.recipient
109
+ if recipient not in self._queues:
110
+ return False
111
+ await self._queues[recipient].put(message)
112
+ return True
113
+
114
+ async def receive_message(self, agent: AgentRole, timeout: float = 1.0) -> Optional[AgentMessage]:
115
+ """Receive a message from an agent's queue."""
116
+ if agent not in self._queues:
117
+ return None
118
+ try:
119
+ return await asyncio.wait_for(self._queues[agent].get(), timeout=timeout)
120
+ except asyncio.TimeoutError:
121
+ return None
122
+
123
+ def get_all_states(self) -> List[AgentState]:
124
+ """Get current state of all agents."""
125
+ return list(self._states.values())
126
+
127
+ def get_state(self, agent: AgentRole) -> Optional[AgentState]:
128
+ """Get state of a specific agent."""
129
+ return self._states.get(agent)
130
+
131
+ def update_state(self, agent: AgentRole, state: str) -> bool:
132
+ """Update the state of a specific agent."""
133
+ if agent not in self._states:
134
+ return False
135
+ self._states[agent].current_state = state
136
+ self._states[agent].last_heartbeat = time.time()
137
+ return True
138
+
139
+
140
+ # Global agent router instance
141
+ agent_router = AgentRouter()
142
+
143
+
144
  @asynccontextmanager
145
  async def lifespan(app: FastAPI):
146
  """Lifespan context manager for startup/shutdown events."""
147
  # Startup: log system startup
148
  log_startup("Cain", "1.0.0")
149
 
150
+ # Start agent router
151
+ await agent_router.start()
152
+
153
  # Start heartbeat task
154
  heartbeat_task = asyncio.create_task(heartbeat_loop())
155
 
156
  yield
157
 
158
+ # Shutdown: cancel heartbeat and stop router
159
  heartbeat_task.cancel()
160
+ await agent_router.stop()
161
 
162
 
163
  # Background heartbeat loop
 
264
  }
265
 
266
 
267
+ @app.post("/agents/send")
268
+ async def agent_send(msg: AgentMessage):
269
+ """Send a message to another agent through the router."""
270
+ success = await agent_router.send_message(msg)
271
+ return {
272
+ "success": success,
273
+ "message": "Message queued" if success else "Failed to queue message",
274
+ "timestamp": datetime.utcnow().isoformat() + "+00:00"
275
+ }
276
+
277
+
278
+ @app.get("/agents/{agent_id}/receive")
279
+ async def agent_receive(agent_id: str, timeout: float = 1.0):
280
+ """Receive a message from the agent's queue."""
281
+ try:
282
+ role = AgentRole(agent_id)
283
+ except ValueError:
284
+ return {"error": "Invalid agent ID", "valid_agents": [r.value for r in AgentRole]}
285
+
286
+ message = await agent_router.receive_message(role, timeout=timeout)
287
+ if message:
288
+ return {
289
+ "message": message.dict(),
290
+ "timestamp": datetime.utcnow().isoformat() + "+00:00"
291
+ }
292
+ return {
293
+ "message": None,
294
+ "queue_size": agent_router.get_state(role).message_queue_size,
295
+ "timestamp": datetime.utcnow().isoformat() + "+00:00"
296
+ }
297
+
298
+
299
+ @app.post("/agents/{agent_id}/state")
300
+ async def agent_update_state(agent_id: str, state: str):
301
+ """Update the state of an agent."""
302
+ try:
303
+ role = AgentRole(agent_id)
304
+ except ValueError:
305
+ return {"error": "Invalid agent ID", "valid_agents": [r.value for r in AgentRole]}
306
+
307
+ success = agent_router.update_state(role, state)
308
+ return {
309
+ "success": success,
310
+ "agent_id": agent_id,
311
+ "new_state": state if success else None,
312
+ "timestamp": datetime.utcnow().isoformat() + "+00:00"
313
+ }
314
+
315
+
316
  @app.websocket("/ws")
317
  async def websocket_endpoint(websocket: WebSocket):
318
  """WebSocket endpoint for real-time dashboard updates."""
 
383
  }
384
 
385
 
386
+ @app.get("/health/agents")
387
+ async def health_agents():
388
+ """Health check endpoint returning status of all agents."""
389
+ agents_status = []
390
+ now = time.time()
391
+
392
+ for state in agent_router.get_all_states():
393
+ # Calculate time since last heartbeat
394
+ heartbeat_age = now - state.last_heartbeat
395
+
396
+ # Determine health status
397
+ if heartbeat_age < 15:
398
+ health = "healthy"
399
+ elif heartbeat_age < 30:
400
+ health = "degraded"
401
+ else:
402
+ health = "unhealthy"
403
+
404
+ agents_status.append({
405
+ "agent_id": state.agent_id,
406
+ "current_state": state.current_state,
407
+ "is_active": state.is_active,
408
+ "health": health,
409
+ "last_heartbeat": state.last_heartbeat,
410
+ "heartbeat_age_seconds": round(heartbeat_age, 2),
411
+ "message_queue_size": state.message_queue_size,
412
+ "last_heartbeat_iso": datetime.fromtimestamp(state.last_heartbeat).isoformat() + "+00:00"
413
+ })
414
+
415
+ return {
416
+ "agents": agents_status,
417
+ "total_agents": len(agents_status),
418
+ "active_agents": sum(1 for a in agents_status if a["is_active"]),
419
+ "timestamp": datetime.utcnow().isoformat() + "+00:00"
420
+ }
421
+
422
+
423
  if __name__ == "__main__":
424
  import uvicorn
425
  uvicorn.run(app, host="0.0.0.0", port=7860)