Update 2 files
- /core/services/qdrant_service.py - /bots/rag_client.py
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
0
core/services/qdrant_service.py
Normal file
0
core/services/qdrant_service.py
Normal file
Reference in New Issue
Block a user