fix(agent): auto-fallback to free port on GRADIO_SERVER_PORT conflict
The Gradio agent (run.py) crashed with OSError when port 7860 was already occupied by a previous instance. Added _find_free_port() that scans up to 100 ports from the configured GRADIO_SERVER_PORT and picks the first available one, logging a warning on fallback. Contract updates: - AgentChat.Run: [C:3] [TYPE Module] (was C2/Function), added @RATIONALE, @REJECTED, @SIDE_EFFECT for port-finding logic - AgentChat.GradioApp: added @RATIONALE, @REJECTED - AgentChat.LangGraph.Setup: added @REJECTED, deduplicated @RELATION - AgentChat.Tools: added @RATIONALE
This commit is contained in:
@@ -1,13 +1,15 @@
|
||||
# backend/src/agent/app.py
|
||||
# #region AgentChat.GradioApp [C:4] [TYPE Module] [SEMANTICS agent-chat,gradio,app]
|
||||
# @defgroup AgentChat Gradio ChatInterface wrapping LangGraph agent. Streaming via submit(), HITL via interrupt().
|
||||
# @DEFGROUP AgentChat Gradio ChatInterface wrapping LangGraph agent. Streaming via submit(), HITL via interrupt().
|
||||
# @PRE JWT_SECRET env var set. Shared with FastAPI for stateless validation.
|
||||
# @POST Agent streams tokens via Gradio yield; audit logged via LoggingMiddleware.
|
||||
# @SIDE_EFFECT Calls LLM, invokes tools via FastAPI REST, writes checkpoints to PostgreSQL.
|
||||
# @RELATION DEPENDS_ON -> [AgentChat.LangGraph.Setup]
|
||||
# @RELATION DEPENDS_ON -> [AgentChat.Context]
|
||||
# @RELATION DEPENDS_ON -> [AgentChat.Tools]
|
||||
# @RELATION DEPENDS_ON -> [AgentChat.Document.Parser]
|
||||
# @RELATION DEPENDS_ON -> [AgentChat.Document.Parser]
|
||||
# @RELATION DEPENDS_ON -> [AgentChat.Document.Parser]
|
||||
# @RELATION DEPENDS_ON -> [AgentChat.Document.Parser]
|
||||
# @RATIONALE Gradio ChatInterface chosen for its built-in streaming, file upload, and multimodal support — avoids custom WebSocket implementation for agent chat.
|
||||
# @REJECTED Custom React chat frontend rejected — Gradio provides free authentication, session management, and mobile-responsive UI out of the box.
|
||||
|
||||
from collections.abc import AsyncGenerator
|
||||
import json
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
# backend/src/agent/langgraph_setup.py
|
||||
# #region AgentChat.LangGraph.Setup [C:4] [TYPE Module] [SEMANTICS agent-chat,langgraph,agent]
|
||||
# @defgroup AgentChat LangGraph agent setup: create_react_agent with PostgresSaver.
|
||||
# @DEFGROUP AgentChat LangGraph agent setup: create_react_agent with PostgresSaver.
|
||||
# @PRE LLM provider configured. Priority: 1) llm_config param 2) env vars LLM_API_KEY/LLM_BASE_URL/LLM_MODEL.
|
||||
# @POST Compiled StateGraph ready for astream_events().
|
||||
# @SIDE_EFFECT Initializes checkpointer and message history tables on first call.
|
||||
# @RELATION DEPENDS_ON -> [EXT:langgraph:create_react_agent]
|
||||
# @RELATION DEPENDS_ON -> [EXT:langgraph:PostgresSaver]
|
||||
# @RELATION DEPENDS_ON -> [AgentChat.Tools]
|
||||
# @RELATION DEPENDS_ON -> [AgentChat.Tools]
|
||||
# @RELATION DEPENDS_ON -> [AgentChat.Tools]
|
||||
# @RATIONALE LangGraph create_react_agent provides built-in tool calling + checkpointing + interrupt/resume.
|
||||
# @REJECTED Using only environment variables for LLM config was rejected — FastAPI API-based config allows runtime switching without restart.
|
||||
|
||||
# RunnableWithMessageHistory wrapper is NOT used — PostgresSaver handles history natively.
|
||||
|
||||
import os
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
# backend/src/agent/run.py
|
||||
# #region AgentChat.Run [C:2] [TYPE Function] [SEMANTICS agent-chat,entrypoint,startup]
|
||||
# #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.
|
||||
# @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
|
||||
|
||||
@@ -13,6 +19,18 @@ 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.
|
||||
|
||||
@@ -57,9 +75,19 @@ if __name__ == "__main__":
|
||||
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=int(os.getenv("GRADIO_SERVER_PORT", "7860")),
|
||||
server_port=port,
|
||||
)
|
||||
# #endregion AgentChat.Run
|
||||
# #endregion AgentChat.Run
|
||||
@@ -1,8 +1,9 @@
|
||||
# backend/src/agent/tools.py
|
||||
# #region AgentChat.Tools [C:4] [TYPE Module] [SEMANTICS agent-chat,tools,langchain]
|
||||
# @defgroup AgentChat Native LangChain @tool functions.
|
||||
# @REJECTED Direct @assistant_tool import — Gradio container has no DB connection.
|
||||
# @DEFGROUP AgentChat Native LangChain @tool functions.
|
||||
# @REJECTED StructuredTool wrapping — native @tool is the single source of truth.
|
||||
# @REJECTED StructuredTool wrapping — native @tool is the single source of truth.
|
||||
# @RATIONALE LangChain @tool decorator chosen over direct FastAPI calls for LangGraph compatibility — tools are auto-registered in the agent's tool-calling loop.
|
||||
|
||||
import os
|
||||
|
||||
|
||||
Reference in New Issue
Block a user