File size: 18,298 Bytes
4ad5efa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
"""
Модуль утиліт для роботи з FAISS векторними індексами.
Цей файл повинен бути розміщений у modules/ai_analysis/faiss_utils.py
"""

import logging
import os
from pathlib import Path
import json
import hashlib
from datetime import datetime
import shutil
import tempfile
import sys

logger = logging.getLogger(__name__)

# Перевірка наявності змінних середовища Hugging Face
IS_HUGGINGFACE = os.environ.get("SPACE_ID") is not None
if IS_HUGGINGFACE:
    logger.info("Виявлено середовище Hugging Face Spaces")

try:
    import faiss
    import numpy as np
    from llama_index.vector_stores.faiss import FaissVectorStore
    from llama_index.core import load_index_from_storage
    from llama_index.core import StorageContext
    
    FAISS_AVAILABLE = True
    logger.info("FAISS успішно імпортовано")
except ImportError as e:
    logger.warning(f"FAISS або llama-index-vector-stores-faiss не встановлено: {e}. Використання FAISS буде вимкнено.")
    FAISS_AVAILABLE = False

def check_faiss_available():
    """
    Перевірка доступності FAISS.
    
    Returns:
        bool: True, якщо FAISS доступний, False інакше
    """
    return FAISS_AVAILABLE

def generate_file_hash(file_path):
    """
    Генерує хеш для файлу на основі його вмісту.
    
    Args:
        file_path (str): Шлях до файлу
        
    Returns:
        str: Хеш файлу або None у випадку помилки
    """
    try:
        if not os.path.exists(file_path):
            logger.error(f"Файл не знайдено: {file_path}")
            return None
            
        # Отримуємо базову інформацію про файл для додавання в хеш
        file_stat = os.stat(file_path)
        file_size = file_stat.st_size
        file_mtime = file_stat.st_mtime
        
        # Створюємо хеш на основі вмісту файлу
        sha256 = hashlib.sha256()
        
        # Додаємо базову інформацію про файл
        sha256.update(f"{file_size}_{file_mtime}".encode())
        
        # Додаємо вміст файлу
        with open(file_path, "rb") as f:
            for byte_block in iter(lambda: f.read(4096), b""):
                sha256.update(byte_block)
        
        return sha256.hexdigest()
        
    except Exception as e:
        logger.error(f"Помилка при генерації хешу файлу: {e}")
        return None

def save_indices_metadata(directory_path, metadata):
    """
    Зберігає метадані індексів у JSON файл.
    
    Args:
        directory_path (str): Шлях до директорії з індексами
        metadata (dict): Метадані для збереження
        
    Returns:
        bool: True, якщо збереження успішне, False інакше
    """
    try:
        # Перевіряємо наявність директорії
        if not os.path.exists(directory_path):
            logger.warning(f"Директорія {directory_path} не існує. Створюємо...")
            os.makedirs(directory_path, exist_ok=True)
            
        metadata_path = Path(directory_path) / "metadata.json"
        
        # Додаємо додаткову інформацію про оточення
        metadata["environment"] = {
            "is_huggingface": IS_HUGGINGFACE,
            "python_version": sys.version,
            "platform": sys.platform
        }
        
        # Додаємо логування для діагностики
        logger.info(f"Збереження метаданих у {metadata_path}")
        logger.info(f"Розмір метаданих: {len(str(metadata))} символів")
        
        with open(metadata_path, "w", encoding="utf-8") as f:
            json.dump(metadata, f, ensure_ascii=False, indent=2)
        
        # Перевіряємо, що файл було створено
        if os.path.exists(metadata_path):
            logger.info(f"Метадані успішно збережено у {metadata_path}")
            return True
        else:
            logger.error(f"Файл {metadata_path} не було створено")
            return False
        
    except Exception as e:
        logger.error(f"Помилка при збереженні метаданих: {e}")
        return False

def load_indices_metadata(directory_path):
    """
    Завантажує метадані індексів з JSON файлу.
    
    Args:
        directory_path (str): Шлях до директорії з індексами
        
    Returns:
        dict: Метадані або пустий словник у випадку помилки
    """
    try:
        metadata_path = Path(directory_path) / "metadata.json"
        
        if not metadata_path.exists():
            logger.warning(f"Файл метаданих не знайдено: {metadata_path}")
            return {}
        
        with open(metadata_path, "r", encoding="utf-8") as f:
            metadata = json.load(f)
        
        logger.info(f"Метадані успішно завантажено з {metadata_path}")
        return metadata
        
    except Exception as e:
        logger.error(f"Помилка при завантаженні метаданих: {e}")
        return {}

def find_latest_indices(base_dir="temp/indices"):
    """
    Знаходить найновіші збережені індекси.
    
    Args:
        base_dir (str): Базова директорія з індексами
        
    Returns:
        tuple: (bool, str) - (наявність індексів, шлях до найновіших індексів)
    """
    try:
        # Перевіряємо наявність базової директорії
        indices_dir = Path(base_dir)
        
        if not indices_dir.exists():
            logger.info(f"Директорія {base_dir} не існує")
            return False, None
            
        if not os.path.isdir(indices_dir):
            logger.warning(f"{base_dir} існує, але не є директорією")
            return False, None
            
        if not any(indices_dir.iterdir()):
            logger.info(f"Директорія {base_dir} порожня")
            return False, None
        
        # Отримання списку піддиректорій з індексами
        try:
            subdirs = [d for d in indices_dir.iterdir() if d.is_dir()]
        except Exception as iter_err:
            logger.error(f"Помилка при перегляді директорії {base_dir}: {iter_err}")
            return False, None
            
        if not subdirs:
            logger.info("Індекси не знайдено")
            return False, None
        
        # Знаходимо найновішу директорію
        try:
            latest_dir = max(subdirs, key=lambda x: x.stat().st_mtime)
        except Exception as sort_err:
            logger.error(f"Помилка при сортуванні директорій: {sort_err}")
            return False, None
        
        logger.info(f"Знайдено індекси у директорії {latest_dir}")
        return True, str(latest_dir)
        
    except Exception as e:
        logger.error(f"Помилка при пошуку індексів: {e}")
        return False, None

def find_indices_by_hash(csv_hash, base_dir="temp/indices"):
    """
    Знаходить індекси, що відповідають вказаному хешу CSV файлу.
    
    Args:
        csv_hash (str): Хеш CSV файлу
        base_dir (str): Базова директорія з індексами
        
    Returns:
        tuple: (bool, str) - (наявність індексів, шлях до відповідних індексів)
    """
    try:
        if not csv_hash:
            logger.warning("Не вказано хеш CSV файлу")
            return False, None
            
        # Перевіряємо наявність базової директорії
        indices_dir = Path(base_dir)
        
        if not indices_dir.exists():
            logger.info(f"Директорія {base_dir} не існує")
            return False, None
            
        if not any(indices_dir.iterdir()):
            logger.info(f"Директорія {base_dir} порожня")
            return False, None
        
        # Отримання списку піддиректорій з індексами
        subdirs = [d for d in indices_dir.iterdir() if d.is_dir()]
        if not subdirs:
            logger.info("Індекси не знайдено")
            return False, None
        
        # Перевіряємо кожну директорію на відповідність хешу
        for directory in subdirs:
            metadata_path = directory / "metadata.json"
            if metadata_path.exists():
                try:
                    with open(metadata_path, "r", encoding="utf-8") as f:
                        metadata = json.load(f)
                    
                    if "csv_hash" in metadata and metadata["csv_hash"] == csv_hash:
                        # Додатково перевіряємо наявність файлів індексів
                        if (directory / "docstore.json").exists():
                            logger.info(f"Знайдено індекси для CSV з хешем {csv_hash} у {directory}")
                            return True, str(directory)
                        else:
                            logger.warning(f"Знайдено метадані для CSV з хешем {csv_hash}, але файли індексів відсутні у {directory}")
                except Exception as md_err:
                    logger.warning(f"Помилка при читанні метаданих {metadata_path}: {md_err}")
        
        # Якщо відповідних індексів не знайдено, повертаємо найновіші
        logger.info(f"Не знайдено індексів для CSV з хешем {csv_hash}, спроба знайти найновіші")
        return find_latest_indices(base_dir)
        
    except Exception as e:
        logger.error(f"Помилка при пошуку індексів за хешем: {e}")
        return False, None

def create_indices_directory(csv_hash=None, base_dir="temp/indices"):
    """
    Створює директорію для зберігання індексів з часовою міткою.
    
    Args:
        csv_hash (str, optional): Хеш CSV файлу для метаданих
        base_dir (str): Базова директорія для індексів
        
    Returns:
        str: Шлях до створеної директорії
    """
    try:
        # Створення базової директорії, якщо вона не існує
        indices_dir = Path(base_dir)
        
        # Очищаємо старі індекси перед створенням нових, якщо ми на Hugging Face
        if IS_HUGGINGFACE and indices_dir.exists():
            logger.info("Очищення старих індексів перед створенням нових на Hugging Face")
            try:
                # Видаляємо тільки старі директорії, якщо їх більше 1
                subdirs = [d for d in indices_dir.iterdir() if d.is_dir()]
                if len(subdirs) > 1:
                    # Сортуємо за часом модифікації (від найстаріших до найновіших)
                    sorted_dirs = sorted(subdirs, key=lambda x: x.stat().st_mtime)
                    
                    # Залишаємо тільки найновішу директорію
                    for directory in sorted_dirs[:-1]:
                        try:
                            shutil.rmtree(directory)
                            logger.info(f"Видалено стару директорію: {directory}")
                        except Exception as del_err:
                            logger.warning(f"Не вдалося видалити директорію {directory}: {del_err}")
            except Exception as clean_err:
                logger.warning(f"Помилка при очищенні старих індексів: {clean_err}")
        
        # Створюємо базову директорію
        indices_dir.mkdir(exist_ok=True, parents=True)
        
        # Створення унікальної директорії з часовою міткою
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        index_dir = indices_dir / timestamp
        
        # Спроба створити директорію
        try:
            index_dir.mkdir(exist_ok=True)
        except Exception as mkdir_err:
            logger.error(f"Не вдалося створити директорію {index_dir}: {mkdir_err}")
            
            # Створюємо тимчасову директорію як запасний варіант
            try:
                temp_dir = tempfile.mkdtemp(prefix="faiss_indices_")
                logger.info(f"Створено тимчасову директорію: {temp_dir}")
                return temp_dir
            except Exception as temp_err:
                logger.error(f"Не вдалося створити тимчасову директорію: {temp_err}")
                return str(indices_dir / "fallback")
        
        # Зберігаємо базові метадані
        metadata = {
            "created_at": timestamp,
            "timestamp": datetime.now().timestamp(),
            "csv_hash": csv_hash
        }
        
        save_indices_metadata(str(index_dir), metadata)
        
        logger.info(f"Створено директорію для індексів: {index_dir}")
        return str(index_dir)
        
    except Exception as e:
        logger.error(f"Помилка при створенні директорії індексів: {e}")
        
        # Створюємо тимчасову директорію як запасний варіант
        try:
            temp_dir = tempfile.mkdtemp(prefix="faiss_indices_")
            logger.info(f"Створено тимчасову директорію для індексів: {temp_dir}")
            return temp_dir
        except Exception:
            # Якщо і це не вдалося, використовуємо директорію temp
            logger.error("Не вдалося створити навіть тимчасову директорію, використовуємо базову temp")
            os.makedirs("temp", exist_ok=True)
            return "temp"

def cleanup_old_indices(max_indices=3, base_dir="temp/indices"):
    """
    Видаляє старі індекси, залишаючи тільки вказану кількість найновіших.
    
    Args:
        max_indices (int): Максимальна кількість індексів для зберігання
        base_dir (str): Базова директорія з індексами
        
    Returns:
        int: Кількість видалених директорій
    """
    try:
        # На Hugging Face Space обмежуємо максимальну кількість індексів до 1
        if IS_HUGGINGFACE:
            max_indices = 1
            logger.info("На Hugging Face Space обмежуємо кількість індексів до 1")
            
        indices_dir = Path(base_dir)
        
        if not indices_dir.exists():
            logger.warning(f"Директорія {base_dir} не існує")
            return 0
        
        # Отримання списку піддиректорій з індексами
        try:
            subdirs = [d for d in indices_dir.iterdir() if d.is_dir()]
        except Exception as iter_err:
            logger.error(f"Помилка при скануванні директорії {base_dir}: {iter_err}")
            return 0
            
        if len(subdirs) <= max_indices:
            logger.info(f"Кількість індексів ({len(subdirs)}) не перевищує ліміт ({max_indices})")
            return 0
        
        # Сортуємо директорії за часом модифікації (від найновіших до найстаріших)
        try:
            sorted_dirs = sorted(subdirs, key=lambda x: x.stat().st_mtime, reverse=True)
        except Exception as sort_err:
            logger.error(f"Помилка при сортуванні директорій: {sort_err}")
            return 0
        
        # Залишаємо тільки max_indices найновіших директорій
        dirs_to_delete = sorted_dirs[max_indices:]
        
        # Видаляємо старі директорії
        deleted_count = 0
        for directory in dirs_to_delete:
            try:
                shutil.rmtree(directory)
                deleted_count += 1
                logger.info(f"Видалено стару директорію індексів: {directory}")
            except Exception as del_err:
                logger.warning(f"Не вдалося видалити директорію {directory}: {del_err}")
        
        return deleted_count
        
    except Exception as e:
        logger.error(f"Помилка при очищенні старих індексів: {e}")
        return 0