File size: 11,899 Bytes
51073dd
 
 
 
 
 
 
94248da
51073dd
c5f052a
ced20ea
4b8e764
c5f052a
 
ced20ea
94248da
ced20ea
 
4b8e764
 
ced20ea
 
c5f052a
 
 
4b8e764
 
 
 
 
 
 
c5f052a
 
4b8e764
c5f052a
ced20ea
c5f052a
4b8e764
 
 
c5f052a
4b8e764
ced20ea
 
1a49277
51073dd
ced20ea
 
 
 
 
 
c5f052a
4b8e764
 
 
 
c5f052a
 
4b8e764
 
94248da
 
 
 
 
ced20ea
4b8e764
 
c5f052a
 
 
 
 
4b8e764
c5f052a
 
 
 
51073dd
94248da
4b8e764
c5f052a
4b8e764
 
 
c5f052a
 
ced20ea
4b8e764
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51073dd
 
4b8e764
 
c5f052a
 
 
4b8e764
c5f052a
4b8e764
 
 
ced20ea
c5f052a
ced20ea
 
 
 
4b8e764
 
 
c5f052a
ced20ea
 
51073dd
 
 
4b8e764
51073dd
4b8e764
c5f052a
51073dd
ced20ea
 
c5f052a
51073dd
c5f052a
4b8e764
51073dd
 
4b8e764
 
51073dd
c5f052a
51073dd
 
c5f052a
4b8e764
 
c5f052a
51073dd
4b8e764
 
 
c5f052a
4b8e764
c5f052a
4b8e764
 
c5f052a
 
4b8e764
 
 
c5f052a
 
4b8e764
 
51073dd
c5f052a
51073dd
4b8e764
c5f052a
 
 
 
 
 
 
94248da
4b8e764
 
51073dd
4b8e764
c5f052a
4b8e764
51073dd
c5f052a
51073dd
 
4b8e764
 
 
51073dd
 
c5f052a
4b8e764
 
c5f052a
 
 
 
 
51073dd
4b8e764
 
 
c5f052a
1a49277
c5f052a
4b8e764
 
c5f052a
 
 
 
 
51073dd
4b8e764
 
 
c5f052a
1a49277
94248da
 
1a49277
 
 
94248da
 
 
1a49277
 
 
 
 
 
 
 
 
 
 
 
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
# core/error_handler.py
"""
Обработчик ошибок с понятными сообщениями.
Адаптировано для Streamlit-приложений.
"""

from __future__ import annotations
import logging
import json
import re
import traceback
from datetime import datetime
from pathlib import Path
from typing import Any, Callable, Awaitable

logger = logging.getLogger(__name__)
class ErrorHandler:
    """
    Обработчик ошибок с понятными сообщениями.
    Адаптировано для Streamlit-приложений.
    """

    _VALID_LOG_LEVELS = {"debug", "info", "error"}

    def __init__(self, log_level: str = "info", log_file: str | None = None):
        """
        Инициализация обработчика ошибок.

        Args:
            log_level: Уровень логирования ("info", "debug", "error")
            log_file: Путь к файлу для записи логов (опционально)
        """
        normalized_level = (log_level or "info").strip().lower()
        self.log_level = normalized_level if normalized_level in self._VALID_LOG_LEVELS else "info"
        self.log_file = log_file
        self.errors: list[dict[str, Any]] = []

    def handle(self, error: Exception, context: dict[str, Any] | None = None) -> dict[str, Any]:
        """
        Обрабатывает ошибку и возвращает понятный ответ для пользователя.
        """
        error_info: dict[str, Any] = {
            "timestamp": datetime.now().isoformat(),
            "type": type(error).__name__,
            "message": str(error),
            "traceback": "".join(traceback.format_exception(type(error), error, error.__traceback__)),
            "context": context or {},
        }

        self.errors.append(error_info)
        self._log_error(error_info)
        return self._format_error(error_info)

    def _log_error(self, error_info: dict[str, Any]) -> None:
        """
        Логирует ошибку в консоль и/или файл.
        """
        message = (
            f"[ERROR] {error_info.get('timestamp', '')} "
            f"{error_info.get('type', 'Unknown')}: {error_info.get('message', '')}"
        )

        trace = str(error_info.get("traceback", "") or "")
        if self.log_level == "debug" and trace.strip() and trace.strip() != "NoneType: None":
            logger.error("%s\n%s", message, trace)
        else:
            logger.error("%s", message)

        if self.log_file:
            try:
                log_path = Path(self.log_file)
                log_path.parent.mkdir(parents=True, exist_ok=True)

                with log_path.open("a", encoding="utf-8") as file:
                    file.write(message + "\n")
                    if self.log_level == "debug":
                        trace = str(error_info.get("traceback", "") or "")
                        if trace.strip():
                            file.write(trace + "\n")
                    file.write("-" * 80 + "\n")
            except OSError:
                logger.exception("Не удалось записать лог ошибок в %s", self.log_file)

    def _format_error(self, error_info: dict[str, Any]) -> dict[str, Any]:
        """
        Форматирует ошибку для пользователя.
        """
        error_type = str(error_info.get("type", "Exception"))
        message = str(error_info.get("message", ""))

        error_messages = {
            "ModuleNotFoundError": (
                f"⚠️ Отсутствует необходимая библиотека. "
                f"Установите: pip install {self._extract_module_name(message)}"
            ),
            "FileNotFoundError": f"📁 Файл не найден: {message}",
            "KeyError": f"🔑 Отсутствует ключ в данных: {message}",
            "ValueError": f"📊 Ошибка в данных: {message}",
            "ConnectionError": "🌐 Ошибка подключения. Проверьте интернет-соединение.",
            "TimeoutError": "⏰ Превышено время ожидания. Попробуйте позже.",
            "PermissionError": f"🔒 Нет доступа к файлу: {message}",
            "JSONDecodeError": "📄 Ошибка парсинга JSON. Проверьте формат данных.",
            "ZeroDivisionError": "⚠️ Деление на ноль. Проверьте исходные данные.",
            "TypeError": f"⚠️ Неверный тип данных: {message}",
            "AttributeError": f"⚠️ Ошибка доступа к атрибуту: {message}",
            "ImportError": f"⚠️ Ошибка импорта: {message}",
            "RuntimeError": f"⚠️ Ошибка выполнения: {message}",
            "MemoryError": "⚠️ Недостаточно памяти. Попробуйте уменьшить объём данных.",
            "IndexError": "⚠️ Выход за границы списка. Проверьте индексы.",
            "NotImplementedError": "⚠️ Функция ещё не реализована.",
            "StopIteration": "⚠️ Итерация завершена.",
            "OverflowError": "⚠️ Число слишком велико для обработки.",
            "RecursionError": "⚠️ Превышена глубина рекурсии.",
            "KeyboardInterrupt": "⏹️ Операция прервана пользователем.",
            "OSError": f"⚠️ Системная ошибка: {message}",
            "IOError": f"⚠️ Ошибка ввода/вывода: {message}",
            "UnicodeDecodeError": "⚠️ Ошибка декодирования. Проверьте кодировку файла.",
            "UnicodeEncodeError": "⚠️ Ошибка кодирования. Проверьте текстовые данные.",
        }

        message_lower = message.lower()

        if "FormulaEngine" in error_type or "calculation" in message_lower:
            user_message = f"📐 Ошибка расчёта: {message}"
        elif "QASystem" in error_type or "search" in message_lower:
            user_message = f"🔍 Ошибка поиска: {message}"
        elif "AgentLoop" in error_type:
            user_message = f"🤖 Ошибка агента: {message}"
        else:
            user_message = error_messages.get(error_type, f"❌ Ошибка: {message or 'Неизвестная ошибка'}")

        return {
            "user_message": user_message,
            "debug_info": error_info if self.log_level == "debug" else None,
            "is_error": True,
            "type": error_type,
            "message": message,
            "timestamp": str(error_info.get("timestamp", "") or ""),
        }

    @staticmethod
    def _extract_module_name(message: str) -> str:
        """Извлекает имя модуля из сообщения об ошибке."""
        match = re.search(r"'([^']+)'", message)
        return match.group(1) if match else "название-библиотеки"

    def get_last_error(self) -> dict[str, Any] | None:
        """Возвращает последнюю ошибку."""
        return self.errors[-1] if self.errors else None

    def get_all_errors(self) -> list[dict[str, Any]]:
        """Возвращает все ошибки."""
        return list(self.errors)

    def clear_errors(self) -> None:
        """Очищает список ошибок."""
        self.errors.clear()

    @staticmethod
    def format_for_ui(error_info: dict[str, Any]) -> str:
        """Форматирует ошибку для отображения в UI."""
        if error_info.get("is_error"):
            return str(error_info.get("user_message", "❌ Неизвестная ошибка"))
        return f"ℹ️ {error_info.get('message', 'Неизвестная ошибка')}"

    def get_error_summary(self) -> dict[str, Any]:
        """Возвращает сводку по ошибкам."""
        if not self.errors:
            return {"total": 0, "types": {}}

        types: dict[str, int] = {}
        for error in self.errors:
            error_type = str(error.get("type", "Unknown"))
            types[error_type] = types.get(error_type, 0) + 1

        last_error = self.errors[-1] if self.errors else None

        return {
            "total": len(self.errors),
            "types": types,
            "last_error": last_error,
            "last_timestamp": last_error.get("timestamp") if last_error else None,
        }

    @staticmethod
    def log_to_file(error_info: dict[str, Any], file_path: str) -> None:
        """Записывает ошибку в отдельный файл лога в формате JSON."""
        try:
            path = Path(file_path)
            path.parent.mkdir(parents=True, exist_ok=True)

            with path.open("a", encoding="utf-8") as file:
                json.dump(error_info, file, ensure_ascii=False)
                file.write("\n" + "-" * 80 + "\n")
        except OSError as exc:
            logger.warning("Не удалось записать лог %s: %s", file_path, exc)

    def get_user_friendly_message(self, error: Exception) -> str:
        """Быстрый метод для получения понятного сообщения об ошибке."""
        result = self.handle(error)
        return str(result["user_message"])

    @staticmethod
    def is_critical(error_info: dict[str, Any]) -> bool:
        """Определяет, является ли ошибка критической."""
        critical_types = {
            "MemoryError",
            "RecursionError",
            "KeyboardInterrupt",
            "SystemError",
        }
        return str(error_info.get("type")) in critical_types


def safe_execute(
    func: Callable[..., Any],
    *args: Any,
    **kwargs: Any,
) -> tuple[Any | None, Exception | None]:
    """Безопасное выполнение функции с обработкой ошибок."""
    try:
        result = func(*args, **kwargs)
        return result, None
    except Exception as exc:
        logger.exception("Ошибка безопасного синхронного вызова")
        return None, exc


async def safe_execute_async(
    async_func: Callable[..., Awaitable[Any]],
    *args: Any,
    **kwargs: Any,
) -> tuple[Any | None, Exception | None]:
    """Безопасное выполнение асинхронной функции с обработкой ошибок."""
    try:
        result = await async_func(*args, **kwargs)
        return result, None
    except Exception as exc:
        logger.exception("Ошибка безопасного асинхронного вызова")
        return None, exc


def _run_self_tests() -> None:
    from unittest.mock import patch
    handler = ErrorHandler(log_level="debug")
    info = handler.handle(ValueError("проверка журнала"), {"stage": "self-test"})
    assert info["is_error"] is True
    assert handler.get_error_summary()["total"] == 1
    assert "ValueError: проверка журнала" in handler.errors[0]["traceback"]
    with patch.object(logger, "exception") as log:
        result, error = safe_execute(lambda: 1 / 0)
        assert result is None and isinstance(error, ZeroDivisionError)
        assert log.called


if __name__ == "__main__":
    _run_self_tests()

# ИСПРАВЛЕНО: traceback сохраняется и вне except; safe_execute/async записывают причину ошибки в журнал.