Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 49 additions & 7 deletions backend/main.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
"""OpenCLAW Bot Chat Backend"""

import subprocess
import re

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from openclaw_client import OpenCLAWClient
Expand All @@ -20,25 +24,64 @@ def root():
return {"message": "OpenCLAW Bot Chat API"}


def get_agents_from_openclaw() -> list[dict]:
"""Get agents from OpenCLAW CLI."""
try:
output = subprocess.check_output(
["openclaw", "agents", "list"], timeout=10, text=True
)
Comment on lines +27 to +32
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

subprocess.check_output(...) is called from within an async request path (/api/agents). Because it’s synchronous, it will block the event loop while the CLI runs (up to the 10s timeout), reducing throughput for all requests. Run the CLI call in a thread (asyncio.to_thread) or use an async subprocess API, and consider caching results to avoid running the CLI on every request.

Copilot uses AI. Check for mistakes.
agents = []
for line in output.split("\n"):
# Match "- agentName" or "- agentName (default)" format
match = re.match(r"^- (\w+)", line)
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parsing regex r"^- (\w+)" only matches word characters (letters/digits/underscore). If OpenCLAW agent IDs include hyphens, dots, or other valid characters, those agents will be silently dropped from the returned list. Consider matching up to whitespace/parenthesis instead (e.g., until ( or end-of-line).

Suggested change
match = re.match(r"^- (\w+)", line)
match = re.match(r"^- ([^(\s]+)", line)

Copilot uses AI. Check for mistakes.
if match:
agent_id = match.group(1)
is_default = "(default)" in line
agents.append(
{
"id": agent_id,
"name": agent_id,
"emoji": "⭐" if is_default else "🤖",
}
)
return agents
except Exception as e:
print(f"[ERROR] Failed to get agents: {e}")
return []


@app.get("/api/agents")
async def get_agents():
"""Get list of agents from OpenCLAW Gateway."""
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring says this endpoint gets agents "from OpenCLAW Gateway", but the implementation now shells out to the OpenCLAW CLI (with a fallback list). Update the docstring (and any related docs) so it reflects the real source of truth and failure behavior.

Suggested change
"""Get list of agents from OpenCLAW Gateway."""
"""Get list of agents using the OpenCLAW CLI, with a fallback to default agents.
This endpoint invokes `get_agents_from_openclaw`, which shells out to the
`openclaw agents list` CLI command to discover available agents. If the CLI
invocation fails or returns no agents, a predefined set of default agents is
returned instead. Only unexpected errors in this process result in a 503
Service Unavailable response.
"""

Copilot uses AI. Check for mistakes.
try:
agents = await openclaw_client.get_agents()
agents = get_agents_from_openclaw()
if not agents:
# Fallback to default agents
agents = [
{"id": "main", "name": "Main Agent", "emoji": "🤖"},
{"id": "default", "name": "Default Agent", "emoji": "🦊"},
]
return {"agents": agents}
except Exception as e:
raise HTTPException(status_code=503, detail=str(e))
Comment on lines 53 to 66
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/api/agents now calls get_agents_from_openclaw() which catches all exceptions and returns [], causing this endpoint to return 200 with fallback agents even when OpenCLAW is unavailable. The existing test suite expects a 503 on upstream failure (see backend/tests/test_routes.py::test_get_agents_gateway_unavailable). Either update the endpoint contract/tests, or let failures propagate so the route can return a 503 consistently.

Copilot uses AI. Check for mistakes.


@app.post("/api/chat")
async def chat(request: ChatRequest):
"""Send message to an agent."""
"""Send message to an agent and broadcast response via WebSocket."""
if not request.message:
raise HTTPException(status_code=400, detail="Message is required")

agent_id = request.agent_id or "default"
try:
result = await openclaw_client.send_message(agent_id, request.message)

# Broadcast agent response to all connected clients via WebSocket
response_content = (
result.get("response") or result.get("message") or str(result)
)
await ws_manager.broadcast({"client_id": agent_id, "message": response_content})

return result
except Exception as e:
if "not found" in str(e).lower():
Expand All @@ -54,9 +97,8 @@ async def websocket_endpoint(websocket: WebSocket, client_id: str):
while True:
data = await websocket.receive_text()
# Broadcast to all clients
await ws_manager.broadcast({
"client_id": client_id,
"message": data
}, exclude=client_id)
await ws_manager.broadcast(
{"client_id": client_id, "message": data}, exclude=client_id
)
except Exception:
ws_manager.disconnect(client_id)
9 changes: 9 additions & 0 deletions backend/websocket_manager.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""WebSocket Manager for real-time chat."""

from typing import Dict
from fastapi import WebSocket

Expand Down Expand Up @@ -28,3 +29,11 @@ async def broadcast(self, message: dict, exclude: str | None = None):
except Exception:
# Handle disconnection gracefully
self.disconnect(client_id)

async def send_to(self, client_id: str, message: dict):
"""Send a message to a specific client."""
if client_id in self.active_connections:
try:
await self.active_connections[client_id].send_json(message)
except Exception:
self.disconnect(client_id)
Comment on lines +33 to +39
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A new send_to() method was added but there’s no corresponding test coverage, even though this repo already has a backend/tests/test_websocket.py suite for WebSocketManager. Add tests that verify send_to() sends JSON to the right connection and disconnects on send failure.

Copilot uses AI. Check for mistakes.
Loading