Files
fckbot/rag/services/embedding_service.py
Markov Andrey 63900feece Update 86 files
- /rag/commands/expert.py
- /core/services/__init__.py
- /core/services/embedding_service.py
- /core/services/file_service.py
- /core/services/giga_client.py
- /core/services/kb_service.py
- /core/services/postgres_service.py
- /core/services/qdrant_service.py
- /core/services/reranker_service.py
- /core/functions/__init__.py
- /core/functions/check_consistency.py
- /core/functions/check_spelling.py
- /core/functions/critique_answer.py
- /core/functions/expand_query.py
- /core/functions/extract_metrics.py
- /core/functions/file_processor.py
- /core/functions/generate_document.py
- /core/functions/intent_classify.py
- /core/functions/rerank_context.py
- /core/functions/summarize_document.py
- /core/utils/__init__.py
- /core/utils/arg_parser.py
- /core/utils/config_loader.py
- /core/utils/layout_converter.py
- /core/utils/logger.py
- /core/utils/text_utils.py
- /core/utils/web_utils.py
- /bots/commands/__init__.py
- /bots/commands/base.py
- /bots/commands/create.py
- /core/commands/global_remove.py
- /core/commands/help.py
- /core/commands/info.py
- /core/commands/kb.py
- /core/commands/learn.py
- /core/commands/other.py
- /core/commands/registry.py
- /core/commands/stats.py
- /core/commands/template.py
- /core/handlers/__init__.py
- /core/handlers/message_handler.py
- /core/workers/__init__.py
- /core/workers/indexing_worker.py
- /core/xmpp/__init__.py
- /core/xmpp/client.py
- /bots/commands/global_remove.py
- /bots/commands/help.py
- /bots/commands/info.py
- /bots/commands/kb.py
- /bots/commands/registry.py
- /bots/commands/other.py
- /bots/commands/template.py
- /bots/commands/learn.py
- /bots/commands/stats.py
- /bots/handlers/message_handler.py
- /bots/handlers/__init__.py
- /bots/workers/__init__.py
- /bots/workers/indexing_worker.py
- /bots/xmpp/__init__.py
- /bots/xmpp/client.py
- /rag/services/qdrant_service.py
- /rag/services/giga_client.py
- /rag/services/embedding_service.py
- /rag/services/reranker_service.py
- /rag/services/__init__.py
- /rag/services/file_service.py
- /rag/services/postgres_service.py
- /rag/services/kb_service.py
- /rag/functions/check_spelling.py
- /rag/functions/__init__.py
- /rag/functions/extract_metrics.py
- /rag/functions/generate_document.py
- /rag/functions/expand_query.py
- /rag/functions/critique_answer.py
- /rag/functions/file_processor.py
- /rag/functions/check_consistency.py
- /rag/functions/summarize_document.py
- /rag/functions/rerank_context.py
- /rag/functions/intent_classify.py
- /rag/utils/__init__.py
- /rag/utils/layout_converter.py
- /rag/utils/text_utils.py
- /rag/utils/arg_parser.py
- /rag/utils/config_loader.py
- /rag/utils/logger.py
- /rag/utils/web_utils.py
2026-06-30 10:33:28 +00:00

195 lines
9.0 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.
# -*- coding: utf-8 -*-
"""
Сервис генерации эмбеддингов через GigaChat API.
Поддерживает одиночные запросы с кэшированием (lru_cache) и пакетную обработку.
Все параметры (модель, таймаут, размер кэша, verify_ssl) вынесены в конфиг.
"""
import asyncio
import logging
from functools import lru_cache
from typing import List, Optional, Any
from gigachat import GigaChat
from gigachat.exceptions import AuthenticationError, RateLimitError
logger = logging.getLogger(__name__)
try:
from fastembed import SparseTextEmbedding
HAS_SPARSE = True
except ImportError:
HAS_SPARSE = False
logging.warning("fastembed не установлен, sparse-эмбеддинги отключены")
class EmbeddingService:
def __init__(
self,
api_key: str,
model: str = "GigaChat-Embeddings",
timeout: int = 30,
cache_size: int = 4096,
verify_ssl: bool = False
):
self.api_key = api_key
self.model = model
self.timeout = timeout
self.cache_size = cache_size
self.verify_ssl = verify_ssl
self._giga = None
# Кэширование одиночных эмбеддингов (синхронная обёртка с lru_cache)
self.embed_sync = lru_cache(maxsize=cache_size)(self._embed_sync_impl)
self._sparse_model = None
def _get_client(self) -> GigaChat:
"""Ленивое создание клиента GigaChat (с нужными параметрами)."""
if self._giga is None:
self._giga = GigaChat(
credentials=self.api_key,
model=self.model,
verify_ssl_certs=self.verify_ssl,
timeout=self.timeout
)
logger.debug(f"Клиент GigaChat создан (модель {self.model}, timeout {self.timeout}s)")
return self._giga
def _get_sparse_model(self):
if not HAS_SPARSE:
return None
if self._sparse_model is None:
self._sparse_model = SparseTextEmbedding(model_name="Qdrant/bm25")
return self._sparse_model
# ---------- Синхронное получение эмбеддинга (с обрезкой) ----------
def _embed_sync_impl(self, text: str) -> Optional[List[float]]:
"""Синхронный вызов GigaChat API для одного текста. Результат кэшируется."""
if not text or not text.strip():
logger.warning("Попытка получить эмбеддинг пустого текста")
return None
# Обрезка до 800 символов (безопасно для лимита 514 токенов GigaChat)
MAX_CHARS = 800
if len(text) > MAX_CHARS:
text = text[:MAX_CHARS]
logger.debug(f"Текст обрезан до {MAX_CHARS} символов для эмбеддинга")
client = self._get_client()
try:
# Совместимость с разными версиями SDK: пробуем input=[text] или просто [text]
try:
response = client.embeddings(input=[text])
except TypeError:
response = client.embeddings([text])
if response.data and len(response.data) > 0:
logger.debug(f"Эмбеддинг получен, размерность {len(response.data[0].embedding)}")
return response.data[0].embedding
else:
logger.error(f"Пустой ответ эмбеддингов для текста: {text[:100]}")
return None
except (AuthenticationError, RateLimitError) as e:
logger.error(f"Ошибка авторизации/лимитов GigaChat: {e}")
raise
except Exception as e:
logger.error(f"Ошибка GigaChat при эмбеддинге: {e}")
raise
async def embed(self, text: str, retries: int = 3, delay: float = 1.0) -> Optional[List[float]]:
"""Асинхронная обёртка с повторными попытками."""
if not isinstance(text, str):
logger.error(f"embed получил не строку, а {type(text)}: {text!r}")
return None
for attempt in range(retries):
try:
loop = asyncio.get_running_loop()
result = await loop.run_in_executor(None, self.embed_sync, text)
if result is not None:
return result
else:
raise ValueError("Эмбеддинг вернул None")
except Exception as e:
wait = delay * (2 ** attempt)
logger.warning(
f"Попытка {attempt+1}/{retries} получить эмбеддинг не удалась: {e}. "
f"Повтор через {wait:.1f} сек."
)
await asyncio.sleep(wait)
logger.error(f"Не удалось получить эмбеддинг после {retries} попыток")
return None
# ---------- Пакетная обработка (для индексации) ----------
def _embed_batch_sync(self, texts: List[str]) -> Optional[List[List[float]]]:
if not texts:
return []
MAX_CHARS = 800
truncated_texts = [t[:MAX_CHARS] if t and len(t) > MAX_CHARS else (t or "") for t in texts]
client = self._get_client()
for attempt in range(5):
try:
try:
response = client.embeddings(input=truncated_texts)
except TypeError:
response = client.embeddings(truncated_texts)
if response.data and len(response.data) == len(truncated_texts):
sorted_embeddings = [None] * len(truncated_texts)
for item in response.data:
idx = item.index
if idx < len(sorted_embeddings):
sorted_embeddings[idx] = item.embedding
return sorted_embeddings
else:
logger.error(f"Неполный ответ эмбеддингов: ожидалось {len(truncated_texts)}, получено {len(response.data) if response.data else 0}")
return None
except RateLimitError as e:
if attempt < 4:
wait = 2 ** attempt # 1,2,4,8 секунд
logger.warning(f"Rate limit, повтор через {wait} секунд")
time.sleep(wait) # синхронный sleep, так как мы в синхронной функции
else:
logger.error("Превышено количество попыток при 429")
return None
except Exception as e:
logger.error(f"Ошибка пакетного эмбеддинга: {e}")
return None
return None
async def embed_batch(self, texts: List[str], retries: int = 3, delay: float = 1.0) -> Optional[List[List[float]]]:
if not texts:
return []
BATCH_SIZE = 50
results = []
for i in range(0, len(texts), BATCH_SIZE):
batch = texts[i:i+BATCH_SIZE]
loop = asyncio.get_running_loop()
vectors = await loop.run_in_executor(None, self._embed_batch_sync, batch)
if vectors is None:
for attempt in range(retries):
await asyncio.sleep(delay * (2 ** attempt))
vectors = await loop.run_in_executor(None, self._embed_batch_sync, batch)
if vectors is not None:
break
else:
return None
results.extend(vectors)
# ДОБАВИТЬ: пауза между батчами
await asyncio.sleep(0.5) # 500 мс
return results
async def embed_sparse_batch(self, texts: List[str]) -> List[Optional[Any]]:
"""Возвращает список объектов SparseEmbedding для каждого текста"""
model = self._get_sparse_model()
if model is None:
# Возвращаем список из None такой же длины, чтобы не нарушать индексацию
return [None] * len(texts)
loop = asyncio.get_running_loop()
embeddings = await loop.run_in_executor(None, list, model.embed(texts))
return embeddings
async def embed_sparse(self, text: str) -> Optional[Any]:
model = self._get_sparse_model()
if model is None:
return None
loop = asyncio.get_running_loop()
embeddings = await loop.run_in_executor(None, lambda: list(model.embed([text])))
if embeddings and len(embeddings) > 0:
return embeddings[0]
return None