From ce368429f7e31f2f7ac75dbb3ef5811e202d4fe3 Mon Sep 17 00:00:00 2001 From: busya Date: Tue, 7 Jul 2026 13:34:07 +0300 Subject: [PATCH] fix(agent): rewrite _llm_health to use openai+httpx instead of langchain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previous lazy-import fix still required langchain at function call time. Root cause: langchain_openai/langchain_core are only in requirements-agent.txt, not requirements-backend.txt. The /api/agent/llm-status endpoint runs in the backend container which has 'openai' but not 'langchain'. Rewrite _check_llm_provider_health() to use: - AsyncOpenAI from 'openai' package (in both containers) - httpx to call /api/agent/llm-config on localhost (same as agent does) - system_ssl_context() for SSL (same as LLM test endpoint) - openai exceptions (APIConnectionError, APITimeoutError, etc.) No langchain dependency at all — works in both backend and agent containers. Verified: 977 tests passed, import without langchain succeeds. --- backend/src/agent/_llm_health.py | 91 +++++++++++++++++++------------- 1 file changed, 54 insertions(+), 37 deletions(-) diff --git a/backend/src/agent/_llm_health.py b/backend/src/agent/_llm_health.py index fd5dc94a..67993df8 100644 --- a/backend/src/agent/_llm_health.py +++ b/backend/src/agent/_llm_health.py @@ -2,21 +2,30 @@ # #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. +# Uses openai+httpx only (no langchain) — works in both backend and agent containers. # @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. +# @RATIONALE agent/app.py imports gradio at module level. The backend container +# does not have gradio installed. The /api/agent/llm-status endpoint needs +# _check_llm_provider_health but must not trigger gradio import. +# Additionally, langchain_openai/langchain_core are only in the agent container +# (requirements-agent.txt), not the backend (requirements-backend.txt). +# This module uses only openai+httpx (available in both containers). # @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 os import time from typing import Any import httpx +from openai import ( + APIConnectionError, + APITimeoutError, + AuthenticationError, + RateLimitError, +) from src.core.logger import logger @@ -37,51 +46,54 @@ _LLM_LAST_ERROR_TS_KEY = "last_llm_error_ts" # @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. +# @REJECTED langchain_openai.ChatOpenAI was rejected — not installed in backend +# container. AsyncOpenAI from the openai package is available in both containers. 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. + # Fetch LLM config from backend's own API (same as agent container does) 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: + fastapi_url = os.getenv("FASTAPI_URL", "http://localhost:8000") + async with httpx.AsyncClient(timeout=5) as client: + resp = await client.get(f"{fastapi_url}/api/agent/llm-config") + if resp.status_code != 200: + _llm_status["status"] = "unavailable" + _llm_status["last_error"] = f"LLM config endpoint returned {resp.status_code}" + _llm_status["last_check_ts"] = time.time() + return "unavailable" + config = resp.json() + if not config or not config.get("configured"): + _llm_status["status"] = "unavailable" + _llm_status["last_error"] = "No LLM provider configured" + _llm_status["last_check_ts"] = time.time() + return "unavailable" + except Exception as exc: _llm_status["status"] = "unavailable" - _llm_status["last_error"] = f"LLM dependencies not installed: {exc}" + _llm_status["last_error"] = f"Failed to fetch LLM config: {exc}" _llm_status["last_check_ts"] = time.time() return "unavailable" + # Probe LLM API using AsyncOpenAI (available in both backend and agent containers) try: - from src.agent.langgraph_setup import _fetch_llm_config as _get_config + from openai import AsyncOpenAI + from src.core.ssl import system_ssl_context - 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, - ) + api = AsyncOpenAI( + api_key=config.get("api_key", ""), + base_url=config.get("base_url", "https://api.openai.com/v1"), + http_client=httpx.AsyncClient( + verify=system_ssl_context(), + timeout=10, + ), + ) + await api.chat.completions.create( + model=config.get("default_model", "gpt-4o-mini"), + max_tokens=1, + messages=[{"role": "user", "content": "ping"}], ) - await llm.ainvoke([HumanMessage(content="ping")]) _llm_status["status"] = "ok" _llm_status["last_error"] = "" _llm_status["last_check_ts"] = time.time() @@ -101,8 +113,13 @@ async def _check_llm_provider_health() -> str: _llm_status["last_error"] = "Invalid API key" _llm_status["last_check_ts"] = time.time() return "auth_error" + except RateLimitError: + _llm_status["status"] = "unavailable" + _llm_status["last_error"] = "Rate limited" + _llm_status["last_check_ts"] = time.time() + return "unavailable" except Exception as exc: - if "usage_metadata.total_tokens" in str(exc): + if "usage_metadata" in str(exc) or "total_tokens" in str(exc): _llm_status["status"] = "ok" _llm_status["last_error"] = "" _llm_status["last_check_ts"] = time.time()