# backend/src/agent/run.py # #region AgentChat.Run [C:3] [TYPE Module] [SEMANTICS agent-chat,entrypoint,startup] # @ingroup AgentChat # @BRIEF Entrypoint for Gradio agent backend. Fetches LLM config from FastAPI on startup. # @PRE FastAPI backend reachable at FASTAPI_URL. Service JWT available for auth. # @POST Gradio agent running on configured port (auto-fallback to next free port if busy). # @SIDE_EFFECT Binds to a TCP port via Gradio launch. # @RATIONALE _find_free_port() prevents port conflicts when a previous agent instance is still running # without requiring manual cleanup or port-range environment variables. # @REJECTED Failing hard on port-in-use was rejected — multiple restarts during development # should not require manual port cleanup. import os import socket import httpx import logging logger = logging.getLogger("cot") FASTAPI_URL = os.getenv("FASTAPI_URL", "http://localhost:8000") def _find_free_port(start_port: int, max_attempts: int = 100) -> int: """Find a free TCP port starting from start_port, scanning up to max_attempts ports.""" for port in range(start_port, start_port + max_attempts): with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: try: s.bind(("", port)) return port except OSError: continue raise OSError(f"No free port found in range {start_port}-{start_port + max_attempts - 1}") def _fetch_llm_config() -> dict | None: """Fetch active LLM provider config from FastAPI with retry. Retries up to 30s (6 × 5s) to wait for FastAPI to be ready. Falls back to env vars if FastAPI is unreachable or returns no active provider. """ import time service_token = os.getenv("SERVICE_JWT", "") headers = {"Authorization": f"Bearer {service_token}"} if service_token else {} for attempt in range(6): try: resp = httpx.get(f"{FASTAPI_URL}/api/agent/llm-config", headers=headers, timeout=5) resp.raise_for_status() config = resp.json() if config.get("configured"): logger.info("LLM config fetched from FastAPI: %s (%s)", config.get("provider_type"), config.get("default_model")) return config logger.warning("FastAPI returned no active LLM provider: %s", config.get("reason")) except Exception as e: if attempt < 5: logger.info("Waiting for FastAPI (attempt %d/6): %s", attempt + 1, e) time.sleep(5) else: logger.warning("Failed to fetch LLM config from FastAPI after 6 attempts: %s", e) logger.info("Falling back to env vars for LLM config") return None if __name__ == "__main__": from src.agent.app import create_chat_interface from src.agent.context import set_service_jwt from src.agent.langgraph_setup import configure_from_api # Propagate SERVICE_JWT to ContextVar for tool calls service_token = os.getenv("SERVICE_JWT", "") if service_token: set_service_jwt(service_token) # Fetch LLM config from FastAPI at startup llm_config = _fetch_llm_config() if llm_config: configure_from_api(llm_config) # Find a free port — fallback if the configured port is already in use configured_port = int(os.getenv("GRADIO_SERVER_PORT", "7860")) try: port = _find_free_port(configured_port) if port != configured_port: logger.warning("Port %d is in use, falling back to port %d", configured_port, port) except OSError as e: logger.error("Failed to find a free port: %s", e) raise demo = create_chat_interface() demo.launch( server_name=os.getenv("GRADIO_SERVER_NAME", "0.0.0.0"), server_port=port, ) # #endregion AgentChat.Run