_llm_health.py imported langchain_core, langchain_openai, and openai at module level. These packages are only installed in the agent container (requirements-agent.txt), not the backend container (requirements-backend.txt). Moved all langchain/openai imports inside _check_llm_provider_health() with ImportError handled gracefully — returns 'unavailable' status instead of ModuleNotFoundError 500 error. Root cause: the /api/agent/llm-status endpoint runs in the backend container, which has httpx but not langchain. The agent container has all LLM deps. Verified: import without langchain succeeds, health check returns 'unavailable'.
116 lines
5.0 KiB
Python
116 lines
5.0 KiB
Python
# backend/src/agent/_llm_health.py
|
|
# #region AgentChat.LlmHealth [C:3] [TYPE Module] [SEMANTICS agent-chat,llm,health,status]
|
|
# @ingroup AgentChat
|
|
# @BRIEF LLM provider health check with in-memory cache (30s TTL).
|
|
# Extracted from agent/app.py to avoid importing gradio in backend container.
|
|
# @LAYER Infrastructure
|
|
# @RELATION CALLED_BY -> [api/routes/agent_status.py]
|
|
# @RELATION CALLED_BY -> [agent/app.py]
|
|
# @RATIONALE agent/app.py imports gradio at module level (line 28). The backend
|
|
# container does not have gradio installed (only the agent container does).
|
|
# The /api/agent/llm-status endpoint needs _check_llm_provider_health but must
|
|
# not trigger gradio import. This module isolates the health-check logic.
|
|
# @POST Returns status string: 'ok' | 'unavailable' | 'timeout' | 'auth_error'.
|
|
# @SIDE_EFFECT Makes a probe request to the LLM provider; caches result in module memory.
|
|
|
|
import time
|
|
from typing import Any
|
|
|
|
import httpx
|
|
|
|
from src.core.logger import logger
|
|
|
|
# ── LLM provider health cache ─────────────────────────────────────────
|
|
_llm_status: dict[str, Any] = {
|
|
"status": "ok",
|
|
"last_error": "",
|
|
"last_check_ts": 0.0,
|
|
}
|
|
_LLM_CHECK_CACHE_TTL = 30 # seconds between health checks
|
|
_LLM_LAST_ERROR_KEY = "last_llm_error"
|
|
_LLM_LAST_ERROR_TS_KEY = "last_llm_error_ts"
|
|
|
|
|
|
# #region AgentChat.LlmHealth.Check [C:2] [TYPE Function] [SEMANTICS agent-chat,llm,health,check]
|
|
# @ingroup AgentChat
|
|
# @BRIEF Check LLM provider connectivity with in-memory cache (30s TTL).
|
|
# @POST Returns status string: 'ok' | 'unavailable' | 'timeout' | 'auth_error'.
|
|
# @SIDE_EFFECT Makes a probe request to the LLM provider; caches result in module memory.
|
|
# @RATIONALE Prevents sending every user request into a dead LLM backend.
|
|
# @REJECTED Module-level imports of langchain_core/openai were rejected — these
|
|
# packages are only installed in the agent container, not the backend container.
|
|
# The /api/agent/llm-status endpoint runs in the backend container and must not
|
|
# fail with ModuleNotFoundError. All langchain/openai imports are lazy (inside
|
|
# the function body) with ImportError handled gracefully.
|
|
async def _check_llm_provider_health() -> str:
|
|
"""Check LLM provider connectivity. Cached for _LLM_CHECK_CACHE_TTL seconds."""
|
|
now = time.time()
|
|
if now - _llm_status["last_check_ts"] < _LLM_CHECK_CACHE_TTL:
|
|
return _llm_status["status"]
|
|
|
|
# Lazy imports: langchain_core, langchain_openai, and openai are only
|
|
# installed in the agent container, not the backend container.
|
|
try:
|
|
from langchain_core.messages import HumanMessage
|
|
from langchain_openai import ChatOpenAI
|
|
from openai import (
|
|
APIConnectionError,
|
|
APITimeoutError,
|
|
AuthenticationError,
|
|
RateLimitError,
|
|
)
|
|
from src.agent._llm_params import chat_openai_kwargs
|
|
except ImportError as exc:
|
|
_llm_status["status"] = "unavailable"
|
|
_llm_status["last_error"] = f"LLM dependencies not installed: {exc}"
|
|
_llm_status["last_check_ts"] = time.time()
|
|
return "unavailable"
|
|
|
|
try:
|
|
from src.agent.langgraph_setup import _fetch_llm_config as _get_config
|
|
|
|
config = await _get_config()
|
|
if not config or not config.get("configured"):
|
|
return _llm_status["status"]
|
|
|
|
llm = ChatOpenAI(
|
|
**chat_openai_kwargs(
|
|
model=config.get("default_model", "gpt-4o-mini"),
|
|
base_url=config.get("base_url", "https://api.openai.com/v1"),
|
|
api_key=config.get("api_key", ""),
|
|
max_tokens=1,
|
|
)
|
|
)
|
|
await llm.ainvoke([HumanMessage(content="ping")])
|
|
_llm_status["status"] = "ok"
|
|
_llm_status["last_error"] = ""
|
|
_llm_status["last_check_ts"] = time.time()
|
|
return "ok"
|
|
except (APIConnectionError, httpx.ConnectError):
|
|
_llm_status["status"] = "unavailable"
|
|
_llm_status["last_error"] = "Connection refused"
|
|
_llm_status["last_check_ts"] = time.time()
|
|
return "unavailable"
|
|
except (APITimeoutError, httpx.ReadTimeout):
|
|
_llm_status["status"] = "timeout"
|
|
_llm_status["last_error"] = "Request timed out"
|
|
_llm_status["last_check_ts"] = time.time()
|
|
return "timeout"
|
|
except AuthenticationError:
|
|
_llm_status["status"] = "auth_error"
|
|
_llm_status["last_error"] = "Invalid API key"
|
|
_llm_status["last_check_ts"] = time.time()
|
|
return "auth_error"
|
|
except Exception as exc:
|
|
if "usage_metadata.total_tokens" in str(exc):
|
|
_llm_status["status"] = "ok"
|
|
_llm_status["last_error"] = ""
|
|
_llm_status["last_check_ts"] = time.time()
|
|
return "ok"
|
|
logger.explore("LLM health check failed", error=str(exc), extra={"src": "AgentChat.LlmHealth.Check"})
|
|
return _llm_status["status"] # return cached status on unexpected errors
|
|
|
|
|
|
# #endregion AgentChat.LlmHealth.Check
|
|
# #endregion AgentChat.LlmHealth
|