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:
2026-06-10 16:37:02 +03:00
parent 143f14d516
commit 1fd5e55db7
6 changed files with 56 additions and 15 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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