"""HTTP client for Loyal Companion Web API.""" from typing import Any import httpx class APIError(Exception): """API request error.""" pass class LoyalCompanionClient: """HTTP client for Loyal Companion API.""" def __init__(self, base_url: str, auth_token: str | None = None): """Initialize client. Args: base_url: API base URL auth_token: Optional authentication token """ self.base_url = base_url.rstrip("/") self.auth_token = auth_token self.client = httpx.Client(timeout=60.0) def _get_headers(self) -> dict[str, str]: """Get request headers. Returns: dict: Request headers """ headers = {"Content-Type": "application/json"} if self.auth_token: headers["Authorization"] = f"Bearer {self.auth_token}" return headers def request_token(self, email: str) -> dict[str, Any]: """Request an authentication token. Args: email: User email Returns: dict: Token response Raises: APIError: If request fails """ try: response = self.client.post( f"{self.base_url}/api/auth/token", json={"email": email}, headers={"Content-Type": "application/json"}, ) response.raise_for_status() return response.json() except httpx.HTTPError as e: raise APIError(f"Failed to request token: {e}") def send_message(self, session_id: str, message: str) -> dict[str, Any]: """Send a chat message. Args: session_id: Session identifier message: User message Returns: dict: Chat response with AI's reply and metadata Raises: APIError: If request fails """ try: response = self.client.post( f"{self.base_url}/api/chat", json={"session_id": session_id, "message": message}, headers=self._get_headers(), ) response.raise_for_status() return response.json() except httpx.HTTPError as e: if hasattr(e, "response") and e.response is not None: try: error_detail = e.response.json().get("detail", str(e)) except Exception: error_detail = str(e) raise APIError(f"Chat request failed: {error_detail}") raise APIError(f"Chat request failed: {e}") def get_history(self, session_id: str, limit: int = 50) -> dict[str, Any]: """Get conversation history. Args: session_id: Session identifier limit: Maximum number of messages Returns: dict: History response Raises: APIError: If request fails """ try: response = self.client.get( f"{self.base_url}/api/sessions/{session_id}/history", params={"limit": limit}, headers=self._get_headers(), ) response.raise_for_status() return response.json() except httpx.HTTPError as e: raise APIError(f"Failed to get history: {e}") def list_sessions(self) -> list[dict[str, Any]]: """List all user sessions. Returns: list: List of sessions Raises: APIError: If request fails """ try: response = self.client.get( f"{self.base_url}/api/sessions", headers=self._get_headers(), ) response.raise_for_status() return response.json() except httpx.HTTPError as e: raise APIError(f"Failed to list sessions: {e}") def delete_session(self, session_id: str) -> dict[str, Any]: """Delete a session. Args: session_id: Session identifier Returns: dict: Deletion response Raises: APIError: If request fails """ try: response = self.client.delete( f"{self.base_url}/api/sessions/{session_id}", headers=self._get_headers(), ) response.raise_for_status() return response.json() except httpx.HTTPError as e: raise APIError(f"Failed to delete session: {e}") def health_check(self) -> dict[str, Any]: """Check API health. Returns: dict: Health status Raises: APIError: If request fails """ try: response = self.client.get(f"{self.base_url}/api/health") response.raise_for_status() return response.json() except httpx.HTTPError as e: raise APIError(f"Health check failed: {e}") def close(self) -> None: """Close the HTTP client.""" self.client.close() def __enter__(self): """Context manager entry.""" return self def __exit__(self, exc_type, exc_val, exc_tb): """Context manager exit.""" self.close()