Files
ss-tools/backend/src/agent/run.py
busya 2f238dee13 fix(agent): seed trace_id for agent process lifecycle
Agent logs had trace_id='no-trace' because the Gradio process
never called seed_trace_id(). The CotJsonFormatter reads trace_id
from ContextVar — without seeding, it defaults to empty string
displayed as 'no-trace'.

Fix:
- app.py: seed_trace_id() on every agent_handler invocation
- run.py: seed_trace_id() on agent startup (for LLM config fetch)

Each Gradio submit gets a fresh trace_id, making agent logs
correlatable with downstream FastAPI calls.
2026-06-30 17:33:21 +03:00

134 lines
5.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 os
import socket
import httpx
from src.core.cot_logger import seed_trace_id
from src.core.logger import logger
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.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
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.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=os.getenv("GRADIO_SERVER_NAME", "0.0.0.0"),
server_port=port,
)
# #endregion AgentChat.Run