"""Asynchronous HTTP client for executing flows with built-in retry logic.""" import json from collections.abc import AsyncIterator import httpx from loguru import logger from ..schema import Response class HttpClient: """Async client for flow endpoints with automated retries and error handling.""" def __init__( self, base_url: str = "http://localhost:8001", timeout: float = 3600.0, max_retries: int = 3, raise_exception: bool = True, ): """Initialize the client with base configuration.""" self.base_url = base_url.rstrip("/") self.timeout = timeout self.max_retries = max_retries self.raise_exception = raise_exception self.client = httpx.AsyncClient(timeout=timeout) async def __aenter__(self): """Enter async context manager.""" return self async def __aexit__(self, exc_type, exc_val, exc_tb): """Exit async context manager and close connection.""" await self.close() async def close(self): """Close the underlying HTTP client.""" await self.client.aclose() async def health_check(self) -> dict[str, str]: """Check the health status of the flow service.""" response = await self.client.get(f"{self.base_url}/health") response.raise_for_status() return response.json() async def execute_flow(self, flow_name: str, **kwargs) -> Response | None: """Execute a flow with automated retry logic.""" endpoint = f"{self.base_url}/{flow_name}" for attempt in range(self.max_retries): try: response = await self.client.post(endpoint, json=kwargs) response.raise_for_status() return Response(**response.json()) except (httpx.HTTPError, Exception) as e: logger.error(f"Flow {flow_name} failed (attempt {attempt + 1}/{self.max_retries}): {e}") if attempt == self.max_retries - 1 and self.raise_exception: raise e return None async def list_endpoints(self) -> dict: """Retrieve available endpoints from OpenAPI specification.""" response = await self.client.get(f"{self.base_url}/openapi.json") response.raise_for_status() return response.json() async def execute_stream_flow(self, flow_name: str, **kwargs) -> AsyncIterator[dict[str, str]]: """Execute a flow and yield parsed SSE stream chunks.""" endpoint = f"{self.base_url}/{flow_name}" async with self.client.stream("POST", endpoint, json=kwargs) as response: response.raise_for_status() async for line in response.aiter_lines(): if not line or not line.startswith("data:"): continue content = line.removeprefix("data:").strip() if content == "[DONE]": break try: data = json.loads(content) yield { "type": data.get("chunk_type", "answer"), "content": data.get("chunk", ""), } except json.JSONDecodeError: continue