agent-coder/agent.py

211 lines
5.8 KiB
Python

#!/usr/bin/env python3
"""Team Board Agent — Coder.
Connects to Tracker via WebSocket, listens for tasks, executes them via OpenClaw.
"""
import asyncio
import json
import logging
import signal
import sys
import websockets
from config import (
TRACKER_WS_URL,
AGENT_TOKEN,
AGENT_NAME,
AGENT_SLUG,
HEARTBEAT_INTERVAL,
)
from brain import think
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
)
logger = logging.getLogger("agent")
# State
current_tasks: list[str] = []
status = "idle"
running = True
async def send_json(ws, data: dict):
await ws.send(json.dumps(data))
async def heartbeat(ws):
"""Send periodic heartbeat to Tracker."""
while running:
try:
await send_json(ws, {
"event": "agent.heartbeat",
"status": status,
"current_tasks": current_tasks,
})
except Exception:
break
await asyncio.sleep(HEARTBEAT_INTERVAL)
async def handle_task_assigned(ws, data: dict):
"""Handle an assigned task — execute it."""
global status
task_id = data.get("task_id")
task_title = data.get("title", "Без названия")
task_description = data.get("description")
project_name = data.get("project_name", "")
logger.info("Task assigned: %s%s", task_id, task_title)
status = "busy"
current_tasks.append(task_id)
try:
# Take the task
await send_json(ws, {"event": "task.take", "task_id": task_id})
# Think (send to OpenClaw)
result = await think(task_title, task_description, project_name)
# Post result as comment
await send_json(ws, {
"event": "task.comment",
"task_id": task_id,
"content": result,
"sender_type": "agent",
"sender_name": AGENT_NAME,
})
# Complete the task
await send_json(ws, {"event": "task.complete", "task_id": task_id})
logger.info("Task completed: %s", task_id)
except Exception as e:
logger.error("Error processing task %s: %s", task_id, e)
# Comment the error
await send_json(ws, {
"event": "task.comment",
"task_id": task_id,
"content": f"❌ Ошибка: {e}",
"sender_type": "agent",
"sender_name": AGENT_NAME,
})
finally:
if task_id in current_tasks:
current_tasks.remove(task_id)
status = "idle" if not current_tasks else "busy"
async def handle_chat_message(ws, data: dict):
"""Handle incoming chat message — respond if mentioned."""
content = data.get("content", "")
sender_name = data.get("sender_name", "")
chat_id = data.get("chat_id")
# Only respond if mentioned
if f"@{AGENT_SLUG}" not in content and f"@{AGENT_NAME}" not in content.lower():
return
logger.info("Mentioned in chat by %s: %s", sender_name, content[:100])
# Simple response via brain
result = await think(
f"Сообщение от {sender_name}: {content}",
None,
"чат",
)
await send_json(ws, {
"event": "chat.send",
"chat_id": chat_id,
"content": result,
"sender_type": "agent",
"sender_name": AGENT_NAME,
})
async def connect():
"""Main connection loop with auto-reconnect."""
while running:
try:
url = f"{TRACKER_WS_URL}?client_type=agent&client_id={AGENT_SLUG}"
logger.info("Connecting to Tracker: %s", url)
async with websockets.connect(url) as ws:
logger.info("Connected to Tracker ✅")
# Auth
await send_json(ws, {
"event": "auth",
"token": AGENT_TOKEN,
})
# Start heartbeat
hb_task = asyncio.create_task(heartbeat(ws))
try:
async for raw in ws:
try:
msg = json.loads(raw)
except json.JSONDecodeError:
continue
event = msg.get("event")
logger.debug("Event: %s", event)
if event == "auth.ok":
logger.info("Authenticated. Init: %s", json.dumps(msg.get("init", {}), ensure_ascii=False)[:200])
elif event == "task.assigned":
asyncio.create_task(handle_task_assigned(ws, msg))
elif event == "chat.message":
asyncio.create_task(handle_chat_message(ws, msg))
elif event == "agent.heartbeat.ack":
pass # OK
elif event == "error":
logger.warning("Error from Tracker: %s", msg.get("message"))
else:
logger.debug("Unhandled event: %s", event)
finally:
hb_task.cancel()
except (websockets.ConnectionClosed, ConnectionRefusedError, OSError) as e:
logger.warning("Connection lost: %s. Reconnecting in 5s...", e)
await asyncio.sleep(5)
except Exception as e:
logger.error("Unexpected error: %s. Reconnecting in 10s...", e)
await asyncio.sleep(10)
def main():
global running
def stop(*_):
global running
running = False
logger.info("Shutting down...")
signal.signal(signal.SIGINT, stop)
signal.signal(signal.SIGTERM, stop)
logger.info("🤖 Agent '%s' (%s) starting...", AGENT_NAME, AGENT_SLUG)
if not AGENT_TOKEN:
logger.error("AGENT_TOKEN is required!")
sys.exit(1)
asyncio.run(connect())
if __name__ == "__main__":
main()