# frontend/api_client.py import os import httpx import json import logging from typing import AsyncGenerator, Generator, List, Dict # --- Setup --- logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # Get API URL from environment variables, with a default for local development API_BASE_URL = os.getenv("API_BASE_URL", "http://127.0.0.1:8000") CHAT_ENDPOINT = f"{API_BASE_URL}/chat" # Use a shared client for connection pooling and better performance async_client = httpx.AsyncClient(timeout=300.0) async def stream_chat_from_api(history: List[Dict[str, str]], message: str, user_id: str = "voicebot_user") -> AsyncGenerator[str, None]: """ Sends a chat request to the FastAPI backend and streams the response. This function handles both streaming (text/plain) and JSON (application/json) responses from the backend. Args: history: The conversation history. message: The latest user message. user_id: An identifier for the user session. Yields: str: Chunks of the response text. """ request_payload = { "user_id": user_id, "history": history, "message": message } logger.info(f"Sending request to API: {CHAT_ENDPOINT}") try: async with async_client.stream("POST", CHAT_ENDPOINT, json=request_payload) as response: # Check for HTTP errors response.raise_for_status() content_type = response.headers.get("content-type", "") # --- Handle Streaming Response (RAG Pipeline) --- if "text/plain" in content_type: logger.info("API returned a streaming response.") async for chunk in response.aiter_text(): if chunk: yield chunk # --- Handle JSON Response (Non-RAG or Error) --- elif "application/json" in content_type: logger.info("API returned a JSON response.") full_body = await response.aread() try: json_response = json.loads(full_body) reply = json_response.get("reply") if reply: yield reply else: error_message = f"API JSON response missing 'reply' key: {json_response}" logger.error(error_message) yield f"[Error: {error_message}]" except json.JSONDecodeError: error_message = "Failed to decode JSON from API." logger.error(error_message) yield f"[Error: {error_message}]" # --- Handle Unexpected Content Types --- else: error_message = f"Unexpected content type from API: {content_type}" logger.error(error_message) yield f"[Error: {error_message}]" except httpx.RequestError as e: error_message = f"API request failed: Could not connect to {e.request.url}." logger.critical(error_message, exc_info=True) yield f"[Error: {error_message}]" except httpx.HTTPStatusError as e: error_message = f"API returned an error: {e.response.status_code} - {e.response.text}" logger.error(error_message, exc_info=True) yield f"[Error: {error_message}]" except Exception as e: error_message = f"An unexpected error occurred in the API client: {e}" logger.error(error_message, exc_info=True) yield f"[Error: {error_message}]" def stream_chat_from_api_sync(history: List[Dict[str, str]], message: str, user_id: str = "voicebot_user") -> Generator[str, None, None]: """Synchronous wrapper for sending a chat request to the FastAPI backend.""" request_payload = { "user_id": user_id, "history": history, "message": message, } logger.info(f"Sending request to API (sync): {CHAT_ENDPOINT}") try: with httpx.Client(timeout=300.0) as client: with client.stream("POST", CHAT_ENDPOINT, json=request_payload) as response: response.raise_for_status() content_type = response.headers.get("content-type", "") if "text/plain" in content_type: logger.info("API returned a streaming response (sync).") for chunk in response.iter_text(): if chunk: yield chunk elif "application/json" in content_type: logger.info("API returned a JSON response (sync).") try: json_response = response.json() reply = json_response.get("reply") if reply: yield reply else: error_message = f"API JSON response missing 'reply' key: {json_response}" logger.error(error_message) yield f"[Error: {error_message}]" except json.JSONDecodeError: error_message = "Failed to decode JSON from API." logger.error(error_message) yield f"[Error: {error_message}]" else: error_message = f"Unexpected content type from API: {content_type}" logger.error(error_message) yield f"[Error: {error_message}]" except httpx.RequestError as e: error_message = f"API request failed: Could not connect to {e.request.url}." logger.critical(error_message, exc_info=True) yield f"[Error: {error_message}]" except httpx.HTTPStatusError as e: error_message = f"API returned an error: {e.response.status_code} - {e.response.text}" logger.error(error_message, exc_info=True) yield f"[Error: {error_message}]" except Exception as e: error_message = f"An unexpected error occurred in the synchronous API client: {e}" logger.error(error_message, exc_info=True) yield f"[Error: {error_message}]"