AgentEvalTool/backend/agenteval/channels/openclaw.py

109 lines
4.1 KiB
Python

"""OpenClaw message channel.
Connects directly to an OpenClaw instance as an evaluation target.
Uses the OpenClaw chat API to send messages and poll for replies.
Configuration keys (in channel_config, all optional — defaults come from settings):
base_url Override OpenClaw upstream URL (defaults to AGENTEVAL_OPENCLAW_UPSTREAM)
auth_token Override auth token (defaults to AGENTEVAL_OPENCLAW_AUTH_TOKEN)
model Model name for chat completions (default: "doubao-seed-2.0")
poll_interval Seconds between polls (default 1.0)
timeout Seconds before poll gives up (default 30.0)
"""
import asyncio
import uuid
from typing import Any, Optional
import httpx
from agenteval.channels.base import ChannelHealth, EvalChannel, Reply, SendResult
from agenteval.config import get_settings
class OpenClawChannel(EvalChannel):
"""Message channel backed by an OpenClaw chat API."""
def __init__(self, config: dict[str, Any]):
settings = get_settings()
self.base_url: str = config.get("base_url", settings.openclaw_upstream).rstrip("/")
self.auth_token: str = config.get("auth_token", settings.openclaw_auth_token)
self.model: str = config.get("model", "doubao-seed-2.0")
self._poll_interval: float = float(config.get("poll_interval", 1.0))
self._client = httpx.AsyncClient(
headers={
"Authorization": f"Bearer {self.auth_token}",
"Content-Type": "application/json",
},
timeout=30,
)
async def close(self) -> None:
await self._client.aclose()
async def health_check(self) -> ChannelHealth:
try:
resp = await self._client.get(f"{self.base_url}/api/health")
resp.raise_for_status()
return ChannelHealth(ok=True, message=f"OpenClaw {resp.status_code}")
except Exception as exc:
return ChannelHealth(ok=False, message=str(exc))
async def _send(self, content: str, **kwargs: Any) -> SendResult:
"""Send a chat message to OpenClaw."""
payload = {
"model": self.model,
"messages": [{"role": "user", "content": content}],
"stream": False,
}
try:
resp = await self._client.post(
f"{self.base_url}/api/v1/chat/completions",
json=payload,
)
resp.raise_for_status()
data = resp.json()
# Extract the assistant message ID from the response
msg_id = data.get("id") or str(uuid.uuid4())
return SendResult(ok=True, question_msg_id=msg_id, raw_response=data)
except Exception as exc:
return SendResult(ok=False, error=str(exc))
async def _poll_reply(
self,
question_msg_id: str,
timeout: float = 30.0,
poll_interval: float = 1.0,
) -> Optional[Reply]:
"""Poll the chat completions endpoint until a reply is available.
Uses the conversation ID from the send response to track the thread.
"""
interval = poll_interval or self._poll_interval
deadline = asyncio.get_event_loop().time() + timeout
# Re-send with the same conversation to get the latest reply
while asyncio.get_event_loop().time() < deadline:
try:
# Get the thread/messages from the conversation
resp = await self._client.get(
f"{self.base_url}/api/v1/chat/completions/{question_msg_id}",
)
if resp.status_code == 200:
data = resp.json()
choice = (data.get("choices") or [{}])[0]
reply_content = choice.get("message", {}).get("content", "")
if reply_content:
return Reply(
question_msg_id=question_msg_id,
content=reply_content,
raw_message={"text": reply_content, "_raw": data},
)
except Exception:
pass
await asyncio.sleep(interval)
return None