Spaces:
Sleeping
Sleeping
#!/usr/bin/env python | |
# -*- coding: utf-8 -*- | |
""" | |
Скрипт для сравнения результатов InjectionBuilder при использовании | |
ChunkRepository (SQLite) и InMemoryEntityRepository (предзагруженного из SQLite). | |
""" | |
import logging | |
import random | |
import sys | |
from pathlib import Path | |
from uuid import UUID | |
# --- SQLAlchemy --- | |
from sqlalchemy import and_, create_engine, select | |
from sqlalchemy.orm import sessionmaker | |
# --- Конфигурация --- | |
# !!! ЗАМЕНИ НА АКТУАЛЬНЫЙ ПУТЬ К ТВОЕЙ БД НА СЕРВЕРЕ !!! | |
DATABASE_URL = "sqlite:///../data/logs.db" # Пример пути, используй свой | |
# Имя таблицы сущностей | |
ENTITY_TABLE_NAME = "entity" # Исправь, если нужно | |
# Количество случайных чанков для теста | |
SAMPLE_SIZE = 300 | |
# --- Настройка путей для импорта --- | |
SCRIPT_DIR = Path(__file__).parent.resolve() | |
PROJECT_ROOT = SCRIPT_DIR.parent # Перейти на уровень вверх (scripts -> project root) | |
LIB_EXTRACTOR_PATH = PROJECT_ROOT / "lib" / "extractor" | |
COMPONENTS_PATH = PROJECT_ROOT / "components" # Путь к компонентам | |
sys.path.insert(0, str(PROJECT_ROOT)) | |
sys.path.insert(0, str(LIB_EXTRACTOR_PATH)) | |
sys.path.insert(0, str(COMPONENTS_PATH)) | |
# Добавляем путь к ntr_text_fragmentation внутри lib/extractor | |
sys.path.insert(0, str(LIB_EXTRACTOR_PATH / "ntr_text_fragmentation")) | |
# --- Логирование --- | |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') | |
logger = logging.getLogger(__name__) | |
# --- Импорты из проекта и библиотеки --- | |
try: | |
# Модели БД | |
from ntr_text_fragmentation.core.entity_repository import \ | |
InMemoryEntityRepository # Импортируем InMemory Repo | |
from ntr_text_fragmentation.core.injection_builder import \ | |
InjectionBuilder # Импортируем Builder | |
# Модели сущностей | |
from ntr_text_fragmentation.models import (Chunk, DocumentAsEntity, | |
LinkerEntity) | |
# Репозитории и билдер | |
from components.dbo.chunk_repository import \ | |
ChunkRepository # Импортируем ChunkRepository | |
from components.dbo.models.acronym import \ | |
Acronym # Импортируем модель из проекта | |
from components.dbo.models.dataset import \ | |
Dataset # Импортируем модель из проекта | |
from components.dbo.models.dataset_document import \ | |
DatasetDocument # Импортируем модель из проекта | |
from components.dbo.models.document import \ | |
Document # Импортируем модель из проекта | |
from components.dbo.models.entity import \ | |
EntityModel # Импортируем модель из проекта | |
# TableEntity если есть | |
# from ntr_text_fragmentation.models.table_entity import TableEntity | |
except ImportError as e: | |
logger.error(f"Ошибка импорта необходимых модулей: {e}") | |
logger.error("Убедитесь, что скрипт находится в папке scripts вашего проекта,") | |
logger.error("и структура проекта соответствует ожиданиям (наличие lib/extractor, components/dbo и т.д.).") | |
sys.exit(1) | |
# --- Вспомогательная функция для парсинга вывода --- | |
def parse_output_by_source(text: str) -> dict[str, str]: | |
"""Разбивает текст на блоки по маркерам '[Источник]'.""" | |
blocks = {} | |
# Разделяем текст по маркеру | |
parts = text.split('[Источник]') | |
# Пропускаем первую часть (текст до первого маркера или пустая строка) | |
for part in parts[1:]: | |
part = part.strip() # Убираем лишние пробелы вокруг части | |
if not part: | |
continue | |
# Ищем первый перенос строки | |
newline_index = part.find('\n') | |
if newline_index != -1: | |
# Извлекаем заголовок ( - ИмяИсточника) | |
header = part[:newline_index].strip() | |
# Извлекаем контент | |
content = part[newline_index+1:].strip() | |
# Очищаем имя источника от " - " и пробелов | |
source_name = header.removeprefix('-').strip() | |
if source_name: # Убедимся, что имя источника не пустое | |
if source_name in blocks: | |
logger.warning(f"Найден дублирующийся источник '{source_name}' при парсинге split(). Контент будет перезаписан.") | |
blocks[source_name] = content | |
else: | |
logger.warning(f"Не удалось извлечь имя источника из заголовка: '{header}'") | |
else: | |
# Если переноса строки нет, вся часть может быть заголовком без контента? | |
logger.warning(f"Часть без переноса строки после '[Источник]': '{part[:100]}...'") | |
return blocks | |
# --- Основная функция сравнения --- | |
def compare_repositories(): | |
logger.info(f"Подключение к базе данных: {DATABASE_URL}") | |
try: | |
engine = create_engine(DATABASE_URL) | |
# Определяем модель здесь, чтобы не зависеть от Base из другого места | |
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) | |
db_session = SessionLocal() | |
# 1. Инициализация ChunkRepository (нужен для доступа к _map_db_entity_to_linker_entity) | |
# Передаем фабрику сессий, чтобы он мог создавать свои сессии при необходимости | |
chunk_repo = ChunkRepository(db=SessionLocal) | |
# 2. Загрузка ВСЕХ сущностей НАПРЯМУЮ из БД | |
logger.info("Загрузка всех сущностей из БД через сессию...") | |
all_db_models = db_session.query(EntityModel).all() | |
logger.info(f"Загружено {len(all_db_models)} записей EntityModel.") | |
if not all_db_models: | |
logger.error("Не удалось загрузить сущности из базы данных. Проверьте подключение и наличие данных.") | |
db_session.close() | |
return | |
# Конвертация в LinkerEntity с использованием маппинга из ChunkRepository | |
logger.info("Конвертация EntityModel в LinkerEntity...") | |
all_linker_entities = [chunk_repo._map_db_entity_to_linker_entity(model) for model in all_db_models] | |
logger.info(f"Сконвертировано в {len(all_linker_entities)} LinkerEntity объектов.") | |
# 3. Инициализация InMemoryEntityRepository | |
logger.info("Инициализация InMemoryEntityRepository...") | |
in_memory_repo = InMemoryEntityRepository(entities=all_linker_entities) | |
logger.info(f"InMemoryEntityRepository инициализирован с {len(in_memory_repo.entities)} сущностями.") | |
# 4. Получение ID искомых чанков НАПРЯМУЮ из БД | |
logger.info("Получение ID искомых чанков из БД через сессию...") | |
query = select(EntityModel.uuid).where( | |
and_( | |
EntityModel.in_search_text.isnot(None), | |
) | |
) | |
results = db_session.execute(query).scalars().all() | |
searchable_chunk_ids = [UUID(res) for res in results] | |
logger.info(f"Найдено {len(searchable_chunk_ids)} сущностей для поиска.") | |
if not searchable_chunk_ids: | |
logger.warning("В базе данных не найдено сущностей для поиска (с in_search_text). Тест невозможен.") | |
db_session.close() | |
return | |
# 5. Выборка случайных ID чанков | |
actual_sample_size = min(SAMPLE_SIZE, len(searchable_chunk_ids)) | |
if actual_sample_size < len(searchable_chunk_ids): | |
logger.info(f"Выбираем {actual_sample_size} случайных ID сущностей для поиска из {len(searchable_chunk_ids)}...") | |
sampled_chunk_ids = random.sample(searchable_chunk_ids, actual_sample_size) | |
else: | |
logger.info(f"Используем все {len(searchable_chunk_ids)} найденные ID сущностей для поиска (т.к. их меньше или равно {SAMPLE_SIZE}).") | |
sampled_chunk_ids = searchable_chunk_ids | |
# 6. Инициализация InjectionBuilders | |
logger.info("Инициализация InjectionBuilder для ChunkRepository...") | |
# Передаем ИМЕННО ЭКЗЕМПЛЯР chunk_repo, который мы создали | |
builder_chunk_repo = InjectionBuilder(repository=chunk_repo) | |
logger.info("Инициализация InjectionBuilder для InMemoryEntityRepository...") | |
builder_in_memory = InjectionBuilder(repository=in_memory_repo) | |
# 7. Сборка текста для обоих репозиториев | |
logger.info(f"\n--- Сборка текста для ChunkRepository ({actual_sample_size} ID)... ---") | |
try: | |
# Передаем список UUID | |
text_chunk_repo = builder_chunk_repo.build(filtered_entities=sampled_chunk_ids) | |
logger.info(f"Сборка для ChunkRepository завершена. Общая длина: {len(text_chunk_repo)}") | |
# --- Добавляем вывод начала текста --- | |
print("\n--- Начало текста (ChunkRepository, первые 1000 символов): ---") | |
print(text_chunk_repo[:1000]) | |
print("--- Конец начала текста (ChunkRepository) ---") | |
# ------------------------------------- | |
except Exception as e: | |
logger.error(f"Ошибка при сборке с ChunkRepository: {e}", exc_info=True) | |
text_chunk_repo = f"ERROR_ChunkRepo: {e}" | |
logger.info(f"\n--- Сборка текста для InMemoryEntityRepository ({actual_sample_size} ID)... ---") | |
try: | |
# Передаем список UUID | |
text_in_memory = builder_in_memory.build(filtered_entities=sampled_chunk_ids) | |
logger.info(f"Сборка для InMemoryEntityRepository завершена. Общая длина: {len(text_in_memory)}") | |
# --- Добавляем вывод начала текста --- | |
print("\n--- Начало текста (InMemory, первые 1000 символов): ---") | |
print(text_in_memory[:1000]) | |
print("--- Конец начала текста (InMemory) ---") | |
# ------------------------------------- | |
except Exception as e: | |
logger.error(f"Ошибка при сборке с InMemoryEntityRepository: {e}", exc_info=True) | |
text_in_memory = f"ERROR_InMemory: {e}" | |
# 8. Парсинг результатов по блокам | |
logger.info("\n--- Парсинг результатов по источникам ---") | |
blocks_chunk_repo = parse_output_by_source(text_chunk_repo) | |
blocks_in_memory = parse_output_by_source(text_in_memory) | |
logger.info(f"ChunkRepo: Найдено {len(blocks_chunk_repo)} блоков источников.") | |
logger.info(f"InMemory: Найдено {len(blocks_in_memory)} блоков источников.") | |
# 9. Сравнение блоков | |
logger.info("\n--- Сравнение блоков по источникам ---") | |
chunk_repo_keys = set(blocks_chunk_repo.keys()) | |
in_memory_keys = set(blocks_in_memory.keys()) | |
all_keys = chunk_repo_keys | in_memory_keys | |
mismatched_blocks = [] | |
if chunk_repo_keys != in_memory_keys: | |
logger.warning("Наборы источников НЕ СОВПАДАЮТ!") | |
only_in_chunk = chunk_repo_keys - in_memory_keys | |
only_in_memory = in_memory_keys - chunk_repo_keys | |
if only_in_chunk: | |
logger.warning(f" Источники только в ChunkRepo: {sorted(list(only_in_chunk))}") | |
if only_in_memory: | |
logger.warning(f" Источники только в InMemory: {sorted(list(only_in_memory))}") | |
else: | |
logger.info("Наборы источников совпадают.") | |
logger.info("\n--- Сравнение содержимого общих источников ---") | |
common_keys = chunk_repo_keys & in_memory_keys | |
if not common_keys: | |
logger.warning("Нет общих источников для сравнения содержимого.") | |
else: | |
all_common_blocks_match = True | |
table_marker_found_in_any_chunk_repo = False | |
table_marker_found_in_any_in_memory = False | |
for key in sorted(list(common_keys)): | |
content_chunk = blocks_chunk_repo.get(key, "") # Используем .get для безопасности | |
content_memory = blocks_in_memory.get(key, "") # Используем .get для безопасности | |
# Проверка наличия маркера таблиц | |
has_tables_chunk = "###" in content_chunk | |
has_tables_memory = "###" in content_memory | |
if has_tables_chunk: | |
table_marker_found_in_any_chunk_repo = True | |
if has_tables_memory: | |
table_marker_found_in_any_in_memory = True | |
# Логируем наличие таблиц для КАЖДОГО блока (можно закомментировать, если много) | |
# logger.info(f" Источник: '{key}' - Таблицы (###) в ChunkRepo: {has_tables_chunk}, в InMemory: {has_tables_memory}") | |
if content_chunk != content_memory: | |
all_common_blocks_match = False | |
mismatched_blocks.append(key) | |
logger.warning(f" НЕСОВПАДЕНИЕ для источника: '{key}' (Таблицы в ChunkRepo: {has_tables_chunk}, в InMemory: {has_tables_memory})") | |
# Можно добавить вывод diff для конкретного блока, если нужно | |
# import difflib | |
# block_diff = difflib.unified_diff( | |
# content_chunk.splitlines(keepends=True), | |
# content_memory.splitlines(keepends=True), | |
# fromfile=f'{key}_ChunkRepo', | |
# tofile=f'{key}_InMemory', | |
# lineterm='', | |
# ) | |
# print("\nDiff для блока:") | |
# sys.stdout.writelines(list(block_diff)[:20]) # Показать начало diff блока | |
# if len(list(block_diff)) > 20: print("...") | |
# else: | |
# # Логируем совпадение только если таблицы есть хоть где-то, для краткости | |
# if has_tables_chunk or has_tables_memory: | |
# logger.info(f" Совпадение для источника: '{key}' (Таблицы в ChunkRepo: {has_tables_chunk}, в InMemory: {has_tables_memory})") | |
# Выводим общую информацию о наличии таблиц | |
logger.info("--- Итог проверки таблиц (###) в общих блоках ---") | |
logger.info(f"Маркер таблиц '###' найден хотя бы в одном блоке ChunkRepo: {table_marker_found_in_any_chunk_repo}") | |
logger.info(f"Маркер таблиц '###' найден хотя бы в одном блоке InMemory: {table_marker_found_in_any_in_memory}") | |
logger.info("-------------------------------------------------") | |
if all_common_blocks_match: | |
logger.info("Содержимое ВСЕХ общих источников СОВПАДАЕТ.") | |
else: | |
logger.warning(f"Найдено НЕСОВПАДЕНИЕ содержимого для {len(mismatched_blocks)} источников: {sorted(mismatched_blocks)}") | |
logger.info("\n--- Итоговый вердикт ---") | |
if chunk_repo_keys == in_memory_keys and not mismatched_blocks: | |
logger.info("ПОЛНОЕ СОВПАДЕНИЕ: Наборы источников и их содержимое идентичны.") | |
elif chunk_repo_keys == in_memory_keys and mismatched_blocks: | |
logger.warning("ЧАСТИЧНОЕ СОВПАДЕНИЕ: Наборы источников совпадают, но содержимое некоторых блоков различается.") | |
else: | |
logger.warning("НЕСОВПАДЕНИЕ: Наборы источников различаются (и, возможно, содержимое общих тоже).") | |
except ImportError as e: | |
# Ловим ошибки импорта, возникшие внутри функций (маловероятно после старта) | |
logger.error(f"Критическая ошибка импорта: {e}") | |
except Exception as e: | |
logger.error(f"Произошла общая ошибка: {e}", exc_info=True) | |
finally: | |
if 'db_session' in locals() and db_session: | |
db_session.close() | |
logger.info("Сессия базы данных закрыта.") | |
# --- Запуск --- | |
if __name__ == "__main__": | |
# Используем Path для более надежного определения пути | |
db_path = Path(DATABASE_URL.replace("sqlite:///", "")) | |
if not db_path.exists(): | |
print(f"!!! ОШИБКА: Файл базы данных НЕ НАЙДЕН по пути: {db_path.resolve()} !!!") | |
print(f"!!! Проверьте значение DATABASE_URL в скрипте. !!!") | |
elif "путь/к/твоей" in DATABASE_URL: # Доп. проверка на placeholder | |
print("!!! ПОЖАЛУЙСТА, УКАЖИТЕ ПРАВИЛЬНЫЙ ПУТЬ К БАЗЕ ДАННЫХ В ПЕРЕМЕННОЙ DATABASE_URL !!!") | |
else: | |
compare_repositories() | |