Update 2 files

- /core/services/qdrant_service.py
- /bots/rag_client.py
This commit is contained in:
Markov Andrey
2026-06-30 12:06:46 +00:00
parent dc9d4b5197
commit 9c8e377b02
2 changed files with 114 additions and 17 deletions

View File

@@ -8,6 +8,8 @@ HTTP-клиент для взаимодействия с RAG-сервером.
Все методы повторяют сигнатуру RAGAPI для бесшовной замены. Все методы повторяют сигнатуру RAGAPI для бесшовной замены.
ИСТОРИЯ НЕ ПЕРЕДАЁТСЯ сервер получает её из БД. ИСТОРИЯ НЕ ПЕРЕДАЁТСЯ сервер получает её из БД.
Улучшена обработка сетевых ошибок с повторными попытками.
""" """
import logging import logging
@@ -20,7 +22,7 @@ logger = logging.getLogger(__name__)
class RAGClient: class RAGClient:
""" """
HTTP-клиент для RAG-сервера. HTTP-клиент для RAG-сервера с поддержкой повторных попыток при сетевых ошибках.
""" """
def __init__( def __init__(
@@ -37,14 +39,14 @@ class RAGClient:
server_url (str): базовый URL RAG-сервера (например, http://localhost:8080) server_url (str): базовый URL RAG-сервера (например, http://localhost:8080)
timeout (int): таймаут на запрос в секундах timeout (int): таймаут на запрос в секундах
max_retries (int): максимальное количество повторных попыток при ошибке max_retries (int): максимальное количество повторных попыток при ошибке
retry_delay (float): задержка между попытками (секунды) retry_delay (float): базовая задержка между попытками (секунды)
""" """
self.server_url = server_url.rstrip('/') self.server_url = server_url.rstrip('/')
self.timeout = timeout self.timeout = timeout
self.max_retries = max_retries self.max_retries = max_retries
self.retry_delay = retry_delay self.retry_delay = retry_delay
self._session: Optional[aiohttp.ClientSession] = None self._session: Optional[aiohttp.ClientSession] = None
logger.info(f"RAGClient инициализирован с сервером {server_url}") logger.info(f"RAGClient инициализирован с сервером {server_url}, таймаут={timeout}с, макс. попыток={max_retries}")
async def _get_session(self) -> aiohttp.ClientSession: async def _get_session(self) -> aiohttp.ClientSession:
""" """
@@ -52,9 +54,10 @@ class RAGClient:
Сессия переиспользуется для всех запросов для эффективности. Сессия переиспользуется для всех запросов для эффективности.
""" """
if self._session is None: if self._session is None:
self._session = aiohttp.ClientSession( # Создаём сессию с таймаутом, который применяется ко всем запросам
timeout=aiohttp.ClientTimeout(total=self.timeout) timeout_obj = aiohttp.ClientTimeout(total=self.timeout)
) self._session = aiohttp.ClientSession(timeout=timeout_obj)
logger.debug("HTTP-сессия создана")
return self._session return self._session
async def close(self): async def close(self):
@@ -77,33 +80,126 @@ class RAGClient:
Исключения: Исключения:
aiohttp.ClientError: если все попытки не удались aiohttp.ClientError: если все попытки не удались
asyncio.TimeoutError: если все попытки истекли по таймауту
""" """
url = f"{self.server_url}{endpoint}" url = f"{self.server_url}{endpoint}"
session = await self._get_session() session = await self._get_session()
last_exception = None
for attempt in range(self.max_retries): for attempt in range(self.max_retries):
try: try:
logger.debug(f"Попытка {attempt + 1}/{self.max_retries} к {endpoint}")
async with session.post(url, json=payload) as resp: async with session.post(url, json=payload) as resp:
if resp.status == 200: if resp.status == 200:
return await resp.json() return await resp.json()
else: else:
# Обрабатываем HTTP ошибки (4xx, 5xx)
error_text = await resp.text() error_text = await resp.text()
logger.error( error_msg = f"HTTP {resp.status}: {error_text[:200]}"
f"RAG-сервер вернул {resp.status}: {error_text[:200]}" logger.error(error_msg)
) # Для 5xx ошибок делаем повтор, для 4xx нет (это ошибка клиента)
raise aiohttp.ClientError( if 500 <= resp.status < 600:
f"HTTP {resp.status}: {error_text[:200]}" raise aiohttp.ClientResponseError(
) status=resp.status,
except (aiohttp.ClientError, asyncio.TimeoutError) as e: message=error_msg,
headers=resp.headers
)
else:
# 4xx не повторяем, сразу выбрасываем
raise aiohttp.ClientResponseError(
status=resp.status,
message=error_msg,
headers=resp.headers
)
except (aiohttp.ClientConnectorError, aiohttp.ServerDisconnectedError) as e:
# Ошибки подключения или разрыва соединения повторяем с увеличенной задержкой
logger.warning( logger.warning(
f"Попытка {attempt + 1}/{self.max_retries} к {endpoint} не удалась: {e}" f"Сетевая ошибка при попытке {attempt + 1}/{self.max_retries} к {endpoint}: {e}"
) )
last_exception = e
if attempt < self.max_retries - 1: if attempt < self.max_retries - 1:
# Экспоненциальная задержка: 1, 2, 4, ... секунд # Для сетевых ошибок задержка больше: 2, 4, 8 секунд
await asyncio.sleep(self.retry_delay * (2 ** attempt)) wait = (self.retry_delay * 2) * (2 ** attempt)
logger.info(f"Повтор через {wait:.1f} секунд")
await asyncio.sleep(wait)
else:
logger.error(f"Все попытки к {endpoint} не удались из-за сетевых проблем")
except (aiohttp.ClientPayloadError, aiohttp.ClientResponseError) as e:
# Ошибки ответа (например, повреждённые данные)
logger.warning(
f"Ошибка ответа при попытке {attempt + 1}/{self.max_retries} к {endpoint}: {e}"
)
last_exception = e
if attempt < self.max_retries - 1:
wait = self.retry_delay * (2 ** attempt)
logger.info(f"Повтор через {wait:.1f} секунд")
await asyncio.sleep(wait)
else:
logger.error(f"Все попытки к {endpoint} не удались из-за ошибок ответа")
except asyncio.TimeoutError as e:
# Таймаут
logger.warning(
f"Таймаут при попытке {attempt + 1}/{self.max_retries} к {endpoint}: {e}"
)
last_exception = e
if attempt < self.max_retries - 1:
wait = self.retry_delay * (2 ** attempt)
logger.info(f"Повтор через {wait:.1f} секунд")
await asyncio.sleep(wait)
else:
logger.error(f"Все попытки к {endpoint} не удались из-за таймаута")
except aiohttp.ClientError as e:
# Все остальные ошибки клиента (общий класс)
logger.warning(
f"Ошибка клиента при попытке {attempt + 1}/{self.max_retries} к {endpoint}: {e}"
)
last_exception = e
if attempt < self.max_retries - 1:
wait = self.retry_delay * (2 ** attempt)
logger.info(f"Повтор через {wait:.1f} секунд")
await asyncio.sleep(wait)
else: else:
logger.error(f"Все попытки к {endpoint} не удались") logger.error(f"Все попытки к {endpoint} не удались")
raise
except Exception as e:
# Неожиданная ошибка не повторяем, логируем и выбрасываем
logger.error(f"Неожиданная ошибка при запросе к {endpoint}: {e}", exc_info=True)
raise
# Если мы вышли из цикла, значит, все попытки провалились
error_msg = f"Не удалось выполнить запрос к {endpoint} после {self.max_retries} попыток"
if last_exception:
error_msg += f": {last_exception}"
logger.error(error_msg)
raise aiohttp.ClientError(error_msg)
async def health_check(self) -> bool:
"""
Проверяет доступность RAG-сервера через эндпоинт /health.
Возвращает True, если сервер отвечает со статусом 200.
"""
try:
session = await self._get_session()
async with session.get(f"{self.server_url}/health", timeout=10) as resp:
if resp.status == 200:
data = await resp.json()
if data.get('status') == 'healthy':
logger.debug("RAG-сервер доступен (health check пройден)")
return True
else:
logger.warning(f"RAG-сервер вернул нездоровый статус: {data}")
return False
else:
logger.warning(f"Health check вернул HTTP {resp.status}")
return False
except Exception as e:
logger.error(f"Ошибка при health check: {e}")
return False
async def query( async def query(
self, self,
@@ -139,6 +235,7 @@ class RAGClient:
return result return result
except Exception as e: except Exception as e:
logger.error(f"Ошибка в RAGClient.query: {e}", exc_info=True) logger.error(f"Ошибка в RAGClient.query: {e}", exc_info=True)
# Возвращаем структурированный ответ с ошибкой
return { return {
"answer": f"⚠️ Ошибка связи с RAG-сервером: {str(e)}", "answer": f"⚠️ Ошибка связи с RAG-сервером: {str(e)}",
"intent": "ERROR", "intent": "ERROR",

View File