forked from Rockachopa/Timmy-time-dashboard
Merge pull request #40 from AlexanderWhitestone/kimi/phase2-swarm-hardening-v2
Phase 2: Swarm hardening, auto-auction, WebSocket fix
This commit is contained in:
@@ -4,6 +4,7 @@ Provides REST endpoints for managing the swarm: listing agents,
|
||||
spawning sub-agents, posting tasks, and viewing auction results.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
@@ -90,8 +91,10 @@ async def list_tasks(status: Optional[str] = None):
|
||||
|
||||
@router.post("/tasks")
|
||||
async def post_task(description: str = Form(...)):
|
||||
"""Post a new task to the swarm for bidding."""
|
||||
"""Post a new task to the swarm and run auction to assign it."""
|
||||
task = coordinator.post_task(description)
|
||||
# Start auction asynchronously - don't wait for it to complete
|
||||
asyncio.create_task(coordinator.run_auction_and_assign(task.id))
|
||||
return {
|
||||
"task_id": task.id,
|
||||
"description": task.description,
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
<a href="/marketplace/ui" class="mc-test-link">MARKET</a>
|
||||
<a href="/tools" class="mc-test-link">TOOLS</a>
|
||||
<a href="/creative/ui" class="mc-test-link">CREATIVE</a>
|
||||
<a href="/mobile" class="mc-test-link" title="Mobile-optimized view">MOBILE</a>
|
||||
<button id="enable-notifications" class="mc-test-link" style="background:none;cursor:pointer;" title="Enable notifications">🔔</button>
|
||||
<span class="mc-time" id="clock"></span>
|
||||
</div>
|
||||
|
||||
@@ -367,7 +367,7 @@ class SwarmCoordinator:
|
||||
async def _broadcast_agent_joined(self, agent_id: str, name: str) -> None:
|
||||
"""Broadcast agent joined event via WebSocket."""
|
||||
try:
|
||||
from websocket.handler import ws_manager
|
||||
from ws_manager.handler import ws_manager
|
||||
await ws_manager.broadcast_agent_joined(agent_id, name)
|
||||
except Exception as exc:
|
||||
logger.debug("WebSocket broadcast failed (agent_joined): %s", exc)
|
||||
@@ -375,7 +375,7 @@ class SwarmCoordinator:
|
||||
async def _broadcast_bid(self, task_id: str, agent_id: str, bid_sats: int) -> None:
|
||||
"""Broadcast bid submitted event via WebSocket."""
|
||||
try:
|
||||
from websocket.handler import ws_manager
|
||||
from ws_manager.handler import ws_manager
|
||||
await ws_manager.broadcast_bid_submitted(task_id, agent_id, bid_sats)
|
||||
except Exception as exc:
|
||||
logger.debug("WebSocket broadcast failed (bid): %s", exc)
|
||||
@@ -383,7 +383,7 @@ class SwarmCoordinator:
|
||||
async def _broadcast_task_posted(self, task_id: str, description: str) -> None:
|
||||
"""Broadcast task posted event via WebSocket."""
|
||||
try:
|
||||
from websocket.handler import ws_manager
|
||||
from ws_manager.handler import ws_manager
|
||||
await ws_manager.broadcast_task_posted(task_id, description)
|
||||
except Exception as exc:
|
||||
logger.debug("WebSocket broadcast failed (task_posted): %s", exc)
|
||||
@@ -391,7 +391,7 @@ class SwarmCoordinator:
|
||||
async def _broadcast_task_assigned(self, task_id: str, agent_id: str) -> None:
|
||||
"""Broadcast task assigned event via WebSocket."""
|
||||
try:
|
||||
from websocket.handler import ws_manager
|
||||
from ws_manager.handler import ws_manager
|
||||
await ws_manager.broadcast_task_assigned(task_id, agent_id)
|
||||
except Exception as exc:
|
||||
logger.debug("WebSocket broadcast failed (task_assigned): %s", exc)
|
||||
@@ -401,7 +401,7 @@ class SwarmCoordinator:
|
||||
) -> None:
|
||||
"""Broadcast task completed event via WebSocket."""
|
||||
try:
|
||||
from websocket.handler import ws_manager
|
||||
from ws_manager.handler import ws_manager
|
||||
await ws_manager.broadcast_task_completed(task_id, agent_id, result)
|
||||
except Exception as exc:
|
||||
logger.debug("WebSocket broadcast failed (task_completed): %s", exc)
|
||||
|
||||
@@ -15,21 +15,8 @@ from typing import Optional
|
||||
DB_PATH = Path("data/swarm.db")
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentRecord:
|
||||
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
||||
name: str = ""
|
||||
status: str = "idle" # idle | busy | offline
|
||||
capabilities: str = "" # comma-separated tags
|
||||
registered_at: str = field(
|
||||
default_factory=lambda: datetime.now(timezone.utc).isoformat()
|
||||
)
|
||||
last_seen: str = field(
|
||||
default_factory=lambda: datetime.now(timezone.utc).isoformat()
|
||||
)
|
||||
|
||||
|
||||
def _get_conn() -> sqlite3.Connection:
|
||||
"""Get a SQLite connection."""
|
||||
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
conn = sqlite3.connect(str(DB_PATH))
|
||||
conn.row_factory = sqlite3.Row
|
||||
@@ -49,6 +36,20 @@ def _get_conn() -> sqlite3.Connection:
|
||||
return conn
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentRecord:
|
||||
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
||||
name: str = ""
|
||||
status: str = "idle" # idle | busy | offline
|
||||
capabilities: str = "" # comma-separated tags
|
||||
registered_at: str = field(
|
||||
default_factory=lambda: datetime.now(timezone.utc).isoformat()
|
||||
)
|
||||
last_seen: str = field(
|
||||
default_factory=lambda: datetime.now(timezone.utc).isoformat()
|
||||
)
|
||||
|
||||
|
||||
def _row_to_record(row: sqlite3.Row) -> AgentRecord:
|
||||
return AgentRecord(
|
||||
id=row["id"],
|
||||
@@ -67,70 +68,81 @@ def register(name: str, capabilities: str = "", agent_id: Optional[str] = None)
|
||||
capabilities=capabilities,
|
||||
)
|
||||
conn = _get_conn()
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT OR REPLACE INTO agents (id, name, status, capabilities, registered_at, last_seen)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(record.id, record.name, record.status, record.capabilities,
|
||||
record.registered_at, record.last_seen),
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
try:
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT OR REPLACE INTO agents (id, name, status, capabilities, registered_at, last_seen)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(record.id, record.name, record.status, record.capabilities,
|
||||
record.registered_at, record.last_seen),
|
||||
)
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
return record
|
||||
|
||||
|
||||
def unregister(agent_id: str) -> bool:
|
||||
conn = _get_conn()
|
||||
cursor = conn.execute("DELETE FROM agents WHERE id = ?", (agent_id,))
|
||||
conn.commit()
|
||||
deleted = cursor.rowcount > 0
|
||||
conn.close()
|
||||
return deleted
|
||||
try:
|
||||
cursor = conn.execute("DELETE FROM agents WHERE id = ?", (agent_id,))
|
||||
conn.commit()
|
||||
return cursor.rowcount > 0
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_agent(agent_id: str) -> Optional[AgentRecord]:
|
||||
conn = _get_conn()
|
||||
row = conn.execute("SELECT * FROM agents WHERE id = ?", (agent_id,)).fetchone()
|
||||
conn.close()
|
||||
return _row_to_record(row) if row else None
|
||||
try:
|
||||
row = conn.execute("SELECT * FROM agents WHERE id = ?", (agent_id,)).fetchone()
|
||||
return _row_to_record(row) if row else None
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def list_agents(status: Optional[str] = None) -> list[AgentRecord]:
|
||||
conn = _get_conn()
|
||||
if status:
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM agents WHERE status = ? ORDER BY registered_at DESC",
|
||||
(status,),
|
||||
).fetchall()
|
||||
else:
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM agents ORDER BY registered_at DESC"
|
||||
).fetchall()
|
||||
conn.close()
|
||||
return [_row_to_record(r) for r in rows]
|
||||
try:
|
||||
if status:
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM agents WHERE status = ? ORDER BY registered_at DESC",
|
||||
(status,),
|
||||
).fetchall()
|
||||
else:
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM agents ORDER BY registered_at DESC"
|
||||
).fetchall()
|
||||
return [_row_to_record(r) for r in rows]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def update_status(agent_id: str, status: str) -> Optional[AgentRecord]:
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
conn = _get_conn()
|
||||
conn.execute(
|
||||
"UPDATE agents SET status = ?, last_seen = ? WHERE id = ?",
|
||||
(status, now, agent_id),
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return get_agent(agent_id)
|
||||
try:
|
||||
conn.execute(
|
||||
"UPDATE agents SET status = ?, last_seen = ? WHERE id = ?",
|
||||
(status, now, agent_id),
|
||||
)
|
||||
conn.commit()
|
||||
return get_agent(agent_id)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def heartbeat(agent_id: str) -> Optional[AgentRecord]:
|
||||
"""Update last_seen timestamp for a registered agent."""
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
conn = _get_conn()
|
||||
conn.execute(
|
||||
"UPDATE agents SET last_seen = ? WHERE id = ?",
|
||||
(now, agent_id),
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return get_agent(agent_id)
|
||||
try:
|
||||
conn.execute(
|
||||
"UPDATE agents SET last_seen = ? WHERE id = ?",
|
||||
(now, agent_id),
|
||||
)
|
||||
conn.commit()
|
||||
return get_agent(agent_id)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
Reference in New Issue
Block a user