# 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. # @SIDE_EFFECT Binds to a TCP port via Gradio launch. # @RATIONALE Gradio port must match the frontend proxy target. Optional fallback is available only # when GRADIO_ALLOW_PORT_FALLBACK=true and an external proxy is updated separately. # @REJECTED Hardcoding the port was rejected — it must be configurable for different deployment environments. import socket import httpx from src.agent._config import FASTAPI_URL, GRADIO_ALLOW_PORT_FALLBACK, GRADIO_SERVER_NAME, GRADIO_SERVER_PORT, SERVICE_JWT from src.core.cot_logger import seed_trace_id from src.core.logger import logger 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 = 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.reason( "LLM config fetched from FastAPI", payload={"provider_type": config.get("provider_type"), "model": config.get("default_model")}, extra={"src": "AgentChat.Run.FetchLlmConfig"}, ) return config logger.explore( "FastAPI returned no active LLM provider", payload={"reason": config.get("reason")}, error="No configured LLM provider", extra={"src": "AgentChat.Run.FetchLlmConfig"}, ) except Exception as e: if attempt < 5: logger.reason( f"Waiting for FastAPI (attempt {attempt + 1}/6)", payload={"error": str(e)}, extra={"src": "AgentChat.Run.FetchLlmConfig"}, ) time.sleep(5) else: logger.explore( "Failed to fetch LLM config after 6 attempts", error=str(e), extra={"src": "AgentChat.Run.FetchLlmConfig"}, ) logger.explore( "Falling back to env vars for LLM config", error="FastAPI unreachable", extra={"src": "AgentChat.Run.FetchLlmConfig"}, ) return None if __name__ == "__main__": import asyncio 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, init_checkpointer seed_trace_id() # Seed trace for agent startup lifecycle # Propagate SERVICE_JWT to ContextVar for tool calls if SERVICE_JWT: set_service_jwt(SERVICE_JWT) # Fetch LLM config from FastAPI at startup llm_config = _fetch_llm_config() if llm_config: configure_from_api(llm_config) # Initialize PostgreSQL checkpointer (FR-004/FR-012/FR-027) asyncio.run(init_checkpointer()) # Bind the configured port. Falling back silently breaks the Vite/nginx proxy target. configured_port = GRADIO_SERVER_PORT allow_port_fallback = GRADIO_ALLOW_PORT_FALLBACK if allow_port_fallback: try: port = _find_free_port(configured_port) if port != configured_port: logger.explore( "Port in use, falling back", payload={"configured_port": configured_port, "actual_port": port}, error=f"Port {configured_port} is in use", extra={"src": "AgentChat.Run.PortBinding"}, ) except OSError as e: logger.explore( "Failed to find a free port", error=str(e), extra={"src": "AgentChat.Run.PortBinding"}, ) raise else: port = configured_port demo = create_chat_interface() demo.launch( server_name=GRADIO_SERVER_NAME, server_port=port, ) # #endregion AgentChat.Run