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
This commit is contained in:
Markov Andrey
2026-06-30 10:33:28 +00:00
parent ea0d42de9c
commit 63900feece
45 changed files with 0 additions and 265 deletions

226
rag/utils/text_utils.py Normal file
View File

@@ -0,0 +1,226 @@
# -*- coding: utf-8 -*-
"""
Утилиты для работы с текстом:
- Разбиение на чанки (токенизация) с помощью tiktoken для индексации документов.
- Подсчёт количества токенов в тексте (для обрезки истории диалога).
- Fallback-разбиение по символам, если tiktoken не установлен.
Используется в:
- KBService при индексации документов (split_into_chunks).
- RAG-пайплайне для обрезки истории диалога по токенам (count_tokens).
Особенности:
- Разбиение на предложения использует библиотеку razdel для корректной обработки
аббревиатур (т.е., и.т.д.) и других сложных случаев русского языка.
- Если razdel недоступен, используется fallback-регулярное выражение.
- Если tiktoken недоступен, используется грубая оценка токенов (3 символа на токен).
"""
import re
import logging
from typing import List
# ---- Инициализация токенизатора tiktoken (для точного подсчёта токенов) ----
try:
import tiktoken
# Используем кодировку cl100k_base, которая используется в моделях GPT-4, GPT-3.5 и GigaChat
TOKENIZER = tiktoken.get_encoding("cl100k_base")
except ImportError:
TOKENIZER = None
logging.warning("tiktoken не установлен. Для точного подсчёта токенов установите: pip install tiktoken")
# ----------------------------------------------------------------------------
# ---- Импорт razdel для умного разбиения по предложениям (русский язык) ----
try:
from razdel import sentenize
HAS_RAZDEL = True
except ImportError:
HAS_RAZDEL = False
logging.warning("razdel не установлен. Разбиение по предложениям будет неточным. Установите: pip install razdel")
# ----------------------------------------------------------------------------
logger = logging.getLogger(__name__)
def count_tokens(text: str) -> int:
"""
Подсчитывает количество токенов в тексте, используя tiktoken (если доступен).
Если tiktoken недоступен, использует грубую оценку: 1 токен ≈ 3 символа.
Эта функция используется в RAG-пайплайне для обрезки истории диалога
до заданного лимита токенов (max_context_tokens).
Аргументы:
text (str): текст для подсчёта токенов.
Возвращает:
int: количество токенов (или оценка).
"""
if not text:
return 0
if TOKENIZER is not None:
# Точный подсчёт через tiktoken
return len(TOKENIZER.encode(text))
else:
# Грубая оценка: для русского языка в среднем 1 токен ≈ 2.53 символа.
# Используем 3 как консервативную оценку (чтобы не превысить лимит).
return len(text) // 3 + 1
def split_into_chunks(
text: str,
chunk_size_tokens: int = 200,
overlap_tokens: int = 50,
approx_chunk_chars: int = 500,
approx_overlap_chars: int = 100,
strategy: str = "recursive_split_by_sentences"
) -> List[str]:
"""
Разбивает текст на чанки (фрагменты) для последующей индексации в векторной БД.
Сначала пытается использовать tiktoken для точного разбиения по токенам.
Если tiktoken недоступен, использует приближённое разбиение по символам.
Аргументы:
text (str): исходный текст.
chunk_size_tokens (int): целевой размер чанка в токенах (по умолчанию 200).
overlap_tokens (int): перекрытие между соседними чанками в токенах (по умолчанию 50).
approx_chunk_chars (int): размер чанка в символах для fallback-режима (по умолчанию 500).
approx_overlap_chars (int): перекрытие в символах для fallback-режима (по умолчанию 100).
strategy (str): стратегия разбиения (пока только "recursive_split_by_sentences").
Возвращает:
List[str]: список чанков (текстовых фрагментов).
"""
if not text or not text.strip():
return []
# Нормализуем пробелы: заменяем множественные пробелы, табуляции и переносы на один пробел.
# Это упрощает дальнейшую обработку и улучшает качество разбиения.
text = re.sub(r'\s+', ' ', text).strip()
# Если токенизатор недоступен, используем fallback-разбиение по символам.
if TOKENIZER is None:
logger.debug("tiktoken недоступен, используем разбиение по символам")
return _split_by_chars(text, approx_chunk_chars, approx_overlap_chars)
# Основной метод: разбиение по предложениям с учётом токенов.
return _split_by_tokens(text, chunk_size_tokens, overlap_tokens)
def _split_by_tokens(text: str, chunk_size: int, overlap: int) -> List[str]:
"""
Разбивает текст на чанки, используя tiktoken для точного подсчёта токенов.
Алгоритм:
1. Разбить текст на предложения (с помощью razdel для русского языка).
2. Накопить предложения, пока сумма токенов не превысит chunk_size.
3. Когда превышает сохранить текущий чанк и создать перекрытие из последних предложений (overlap токенов).
4. Продолжить с оставшимся хвостом.
5. В конце объединить очень короткие чанки (< chunk_size/3) с предыдущим,
чтобы избежать множества мелких фрагментов.
Аргументы:
text (str): исходный текст.
chunk_size (int): максимальный размер чанка в токенах.
overlap (int): количество токенов перекрытия между соседними чанками.
Возвращает:
List[str]: список чанков.
"""
# Вспомогательная функция для подсчёта токенов в строке.
def _count_tokens(s: str) -> int:
return len(TOKENIZER.encode(s))
# ---- 1. Разбиение на предложения ----
if HAS_RAZDEL:
# razdel возвращает итератор объектов Sentence, у которых есть атрибут .text
sentences = [sent.text for sent in sentenize(text)]
else:
# Fallback: разбиение по точкам, вопросительным и восклицательным знакам.
# Это может быть неточным для аббревиатур (например, "т.е.", "и.т.д.").
sentences = re.split(r'(?<=[.!?])\s+', text)
# -------------------------------------
chunks = []
current_chunk = [] # Список предложений текущего чанка
current_len = 0 # Суммарное количество токенов в текущем чанке
for sent in sentences:
sent_tokens = _count_tokens(sent) # Токены в текущем предложении
# Проверяем, помещается ли предложение в текущий чанк
if current_len + sent_tokens <= chunk_size:
# Помещается добавляем
current_chunk.append(sent)
current_len += sent_tokens
else:
# Не помещается сохраняем текущий чанк
if current_chunk:
chunks.append(' '.join(current_chunk))
# --- Формируем перекрытие для следующего чанка ---
# Берём последние предложения текущего чанка, сумма токенов которых
# не превышает overlap (чтобы сохранить контекст между чанками).
overlap_sentences = []
overlap_len = 0
# Идём с конца текущего чанка (от последнего предложения к первому)
for prev_sent in reversed(current_chunk):
prev_len = _count_tokens(prev_sent)
if overlap_len + prev_len <= overlap:
overlap_sentences.insert(0, prev_sent) # Вставляем в начало, чтобы сохранить порядок
overlap_len += prev_len
else:
break
# -------------------------------------------------
# Новый чанк начинается с перекрытия + текущее предложение
current_chunk = overlap_sentences + [sent]
current_len = overlap_len + sent_tokens
# Добавляем последний чанк, если он не пуст
if current_chunk:
chunks.append(' '.join(current_chunk))
# ---- Склеиваем очень короткие чанки с предыдущим ----
# Это предотвращает создание большого количества мелких фрагментов,
# которые могут ухудшить качество поиска.
merged = []
for chunk in chunks:
# Если текущий чанк слишком короткий (< 1/3 от chunk_size) и есть предыдущий чанк,
# присоединяем его к предыдущему.
if merged and _count_tokens(chunk) < chunk_size // 3:
merged[-1] = merged[-1] + " " + chunk
else:
merged.append(chunk)
# -------------------------------------------------
return merged
def _split_by_chars(text: str, chunk_size: int, overlap: int) -> List[str]:
"""
Fallback-метод: разбиение по символам (при отсутствии tiktoken).
Использует простое скользящее окно с фиксированной длиной chunk_size и перекрытием overlap.
Аргументы:
text (str): исходный текст.
chunk_size (int): размер чанка в символах.
overlap (int): перекрытие в символах.
Возвращает:
List[str]: список чанков.
"""
if chunk_size <= 0:
return [text]
chunks = []
start = 0
while start < len(text):
end = min(start + chunk_size, len(text))
chunks.append(text[start:end])
start = end - overlap
if start < 0:
start = 0
if start >= len(text):
break
return chunks