112 lines
4.4 KiB
Python
112 lines
4.4 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 json
|
|
import uuid
|
|
from typing import Any, Optional
|
|
|
|
import httpx
|
|
|
|
from agenteval.channels.base import ChannelHealth, ChannelTransportError, 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 (httpx.HTTPError, json.JSONDecodeError) 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},
|
|
)
|
|
elif resp.status_code != 404:
|
|
raise ChannelTransportError(f"HTTP {resp.status_code}: {resp.text[:500]}")
|
|
except (httpx.HTTPError, json.JSONDecodeError) as exc:
|
|
raise ChannelTransportError(str(exc)) from exc
|
|
|
|
await asyncio.sleep(interval)
|
|
|
|
return None
|