from typing import Any from fastapi import HTTPException, status from app.core.config import Settings from app.modules.ai_agent.adapters import httpx from app.modules.ai_agent.adapters.base import AIAdapter from app.modules.ai_agent.adapters.common import ( _response_payload, ) from app.modules.ai_agent.constants import ( AUTHORIZATION_BEARER_TEMPLATE, OPENCLAW_ACTION_NOT_ALLOWED, OPENCLAW_CHAT_PROVIDER_REQUIRED, OPENCLAW_TOOL_COMPLETED_ANSWER, OPENCLAW_TOOL_NOT_ALLOWED, AIDefault, AIContextKey, AIErrorKey, AIHttpHeader, AIHttpPath, AIHttpPayloadKey, AIProviderName, AIResponseKey, ) class OpenClawAdapter(AIAdapter): """Adapter for the OpenClaw Gateway control-plane API.""" provider_name = AIProviderName.OPENCLAW def __init__(self, settings: Settings): self.settings = settings def ask(self, prompt: str, context: dict[str, Any] | None = None) -> dict[str, Any]: context = context or {} tool = context.get(AIContextKey.OPENCLAW_TOOL) if not tool: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=OPENCLAW_CHAT_PROVIDER_REQUIRED, ) result = self.invoke_tool( tool=str(tool), action=str(context.get(AIContextKey.OPENCLAW_ACTION) or AIDefault.ACTION_JSON), args=context.get(AIContextKey.OPENCLAW_ARGS) or {}, session_key=str( context.get(AIContextKey.OPENCLAW_SESSION_KEY) or AIDefault.SESSION_KEY_MAIN ), ) return { AIResponseKey.ANSWER: OPENCLAW_TOOL_COMPLETED_ANSWER, AIResponseKey.RAW: result, } def health(self) -> dict[str, Any]: """Check OpenClaw Gateway health endpoints.""" headers = self._headers() base_url = self._base_url() with httpx.Client(timeout=5, trust_env=False) as client: healthz = client.get(f"{base_url}{AIHttpPath.HEALTHZ}", headers=headers) readyz = client.get(f"{base_url}{AIHttpPath.READYZ}", headers=headers) return { AIResponseKey.OK: ( healthz.status_code < status.HTTP_400_BAD_REQUEST and readyz.status_code < status.HTTP_400_BAD_REQUEST ), AIResponseKey.BASE_URL: base_url, AIResponseKey.HEALTHZ: _response_payload(healthz), AIResponseKey.READYZ: _response_payload(readyz), } def invoke_tool( self, tool: str, action: str = AIDefault.ACTION_JSON, args: dict[str, Any] | None = None, session_key: str = AIDefault.SESSION_KEY_MAIN, ) -> dict[str, Any]: """Invoke one OpenClaw Gateway tool through `/tools/invoke`.""" self._ensure_tool_allowed(tool, action) payload = { AIHttpPayloadKey.TOOL: tool, AIHttpPayloadKey.ACTION: action, AIHttpPayloadKey.ARGS: args or {}, AIHttpPayloadKey.SESSION_KEY: session_key, } url = f"{self._base_url()}{AIHttpPath.TOOLS_INVOKE}" with httpx.Client(timeout=120, trust_env=False) as client: response = client.post(url, json=payload, headers=self._headers()) if response.status_code >= status.HTTP_400_BAD_REQUEST: raise HTTPException( status_code=status.HTTP_502_BAD_GATEWAY, detail={AIErrorKey.OPENCLAW: response.text}, ) return _response_payload(response) def _base_url(self) -> str: return (self.settings.openclaw_http_url or self.settings.openclaw_base_url).rstrip("/") def _headers(self) -> dict[str, str]: token = self.settings.openclaw_gateway_token or self.settings.openclaw_api_key if not token: return {} return { AIHttpHeader.AUTHORIZATION: AUTHORIZATION_BEARER_TEMPLATE.format(token=token) } def _ensure_tool_allowed(self, tool: str, action: str) -> None: if tool not in set(self.settings.openclaw_allowed_tools): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=OPENCLAW_TOOL_NOT_ALLOWED, ) if action not in set(self.settings.openclaw_allowed_actions): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=OPENCLAW_ACTION_NOT_ALLOWED, )