### Bugfixes — Agent Chat 'Думаю' State Leak - fix(agent-chat): loadHistory() now resets streamingState/idle + cancels stale submission — prevents 'Думаю' state leak across conversation switches - fix(agent-chat): onDisconnected/onDisconnectedPermanent cascade to streamingState — prevents permanent hang on connection loss during stream - fix(agent-chat): guard on isLoadingHistory — prevents false commit of 'agent unavailable' fallback when switching conversations - fix(agent-chat): remove race in _sendNow empty-response check vs Svelte microtask (duplicate logic removed, handles correctly) - fix(stream-processor): confirm_resolved now appends msg.text to partialText instead of dropping it ### Bugfixes — Backend PDF Upload - fix(document-parser): _detect_format_by_magic() — reads file header magic bytes as fallback when Gradio loses filename - fix(document-parser): improved name extraction — tries orig_name, path stem - fix(document-parser): @RELATION AgentChatTypes -> AgentChat.Types ### HITL Flow & Agent Chat Improvements - feat(agent): HITL resume confirm/deny with userId/userJwt/envId propagation - feat(agent): confirm_required metadata fallback via aget_state() after 'Event loop is closed' error during interrupt - feat(agent): interrupt_before re-enabled via AGENT_CONFIRM_TOOLS env var - feat(frontend): debug panel with connection/stream state monitoring - feat(frontend): AgentChatModel constructor options + onBeforeSend callback - feat(frontend): crypto.randomUUID() for local conversation ID on first send ### Backend Agent Refactoring - refactor(agent): langgraph_setup — monkey-patch for PydanticSerializationError - refactor(agent): tools.py — dual identity headers, expanded tool set - refactor(agent): run.py — _find_free_port, Gradio server port fallback - refactor(agent): app.py — file size validation, message truncation, HITL path ### Frontend - feat(dashboard-hub): DashboardHubModel with filters, pagination, git actions - feat(ui): DateRangeFilter component - feat(i18n): new dashboard keys; cache tooltips fix - fix(i18n): full run tooltips — cache is NOT ignored ### Semantic Protocol - chore(agents): update all agents with canonical format - chore(skills): sync semantics-core, semantics-contracts, molecular-cot-logging ### Housekeeping - chore: remove stale semantic reports (10 files, Jan 2026) - chore: update 033-gradio-agent-chat specs, contracts, UX, tasks, tests - chore: add .agents/ directory (mirrors .opencode/ agent layouts) - chore: update run.sh with DEV_MODE, port management
101 lines
4.0 KiB
Python
101 lines
4.0 KiB
Python
# 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.
|
||
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__":
|
||
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
|
||
|
||
# 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)
|
||
|
||
# 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 = int(os.getenv("GRADIO_SERVER_PORT", "7860"))
|
||
allow_port_fallback = os.getenv("GRADIO_ALLOW_PORT_FALLBACK", "").strip().lower() in {"1", "true", "yes"}
|
||
if allow_port_fallback:
|
||
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
|
||
else:
|
||
port = configured_port
|
||
|
||
demo = create_chat_interface()
|
||
|
||
demo.launch(
|
||
server_name=os.getenv("GRADIO_SERVER_NAME", "0.0.0.0"),
|
||
server_port=port,
|
||
)
|
||
# #endregion AgentChat.Run
|