PatrickRedStar commited on
Commit
4f1eb8e
·
1 Parent(s): 23752ab

Add: Streaming processing toggle and speed optimization guide - Add streaming processing option in Gradio UI - Auto-enable for files >500 lines - Add chunk size slider - Create SPEED_OPTIMIZATION.md with model suggestions

Browse files
SPEED_OPTIMIZATION.md ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Оптимизация скорости обработки
2
+
3
+ ## Текущая ситуация
4
+
5
+ - **Модель:** DeepSeek-V3.1-Terminus (через smolagents InferenceClientModel)
6
+ - **Время обработки:** ~2.5 минуты для 13 строк логов
7
+ - **Проблема:** Модель медленная из-за размера и API задержек
8
+
9
+ ## Варианты ускорения
10
+
11
+ ### 1. Использование более быстрых моделей DeepSeek
12
+
13
+ #### Вариант A: DeepSeek-R1 (более быстрая версия)
14
+ ```python
15
+ model = InferenceClientModel(
16
+ model_id="deepseek-ai/DeepSeek-R1",
17
+ token=hf_token,
18
+ max_tokens=4096
19
+ )
20
+ ```
21
+ - **Плюсы:** Быстрее, меньше задержка
22
+ - **Минусы:** Может быть менее точной для сложных задач
23
+
24
+ #### Вариант B: DeepSeek-Coder (специализированная модель для кода/структурированных данных)
25
+ ```python
26
+ model = InferenceClientModel(
27
+ model_id="deepseek-ai/deepseek-coder-6.7b-instruct",
28
+ token=hf_token,
29
+ max_tokens=2048
30
+ )
31
+ ```
32
+ - **Плюсы:** Оптимизирована для структурированных данных (JSON, логи)
33
+ - **Минусы:** Меньший контекст, может быть менее точной для анализа
34
+
35
+ ### 2. Использование других быстрых моделей через HF Inference API
36
+
37
+ #### Qwen/Qwen2.5 (быстрая и качественная)
38
+ ```python
39
+ model = InferenceClientModel(
40
+ model_id="Qwen/Qwen2.5-7B-Instruct",
41
+ token=hf_token,
42
+ max_tokens=2048
43
+ )
44
+ ```
45
+
46
+ #### Mistral (баланс скорости и качества)
47
+ ```python
48
+ model = InferenceClientModel(
49
+ model_id="mistralai/Mistral-7B-Instruct-v0.2",
50
+ token=hf_token,
51
+ max_tokens=2048
52
+ )
53
+ ```
54
+
55
+ ### 3. Оптимизация промптов и параметров
56
+
57
+ #### Уменьшение max_tokens для более коротких ответов
58
+ ```python
59
+ model = InferenceClientModel(
60
+ model_id="deepseek-ai/DeepSeek-V3.1-Terminus",
61
+ token=hf_token,
62
+ max_tokens=2048 # Вместо 4096 - быстрее генерация
63
+ )
64
+ ```
65
+
66
+ #### Уменьшение max_steps для агентов
67
+ ```python
68
+ parser_agent = ToolCallingAgent(
69
+ model=model,
70
+ tools=[final_tool],
71
+ instructions="...",
72
+ name="LogParserAgent",
73
+ max_steps=5, # Вместо 10 - меньше итераций
74
+ )
75
+ ```
76
+
77
+ ### 4. Параллельная обработка чанков
78
+
79
+ В `streaming_processor.py` можно добавить:
80
+ ```python
81
+ from concurrent.futures import ThreadPoolExecutor
82
+
83
+ def process_chunks_parallel(chunks, max_workers=3):
84
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
85
+ results = executor.map(process_single_chunk, chunks)
86
+ return list(results)
87
+ ```
88
+
89
+ ### 5. Кэширование результатов
90
+
91
+ Кэшировать результаты парсинга одинаковых строк:
92
+ ```python
93
+ from functools import lru_cache
94
+ import hashlib
95
+
96
+ @lru_cache(maxsize=1000)
97
+ def cached_parse(log_hash: str, logs: str):
98
+ return run_parser_agent(logs)
99
+ ```
100
+
101
+ ### 6. Использование локальной модели (если доступно)
102
+
103
+ Если есть GPU, можно использовать локальную модель вместо API:
104
+ ```python
105
+ from transformers import AutoModelForCausalLM, AutoTokenizer
106
+ from smolagents import LocalModel
107
+
108
+ model = LocalModel(
109
+ model_id="deepseek-ai/DeepSeek-V3.1-Terminus",
110
+ device="cuda" # или "cpu"
111
+ )
112
+ ```
113
+
114
+ ## Рекомендации
115
+
116
+ ### Для максимальной скорости (уровень 1):
117
+ 1. Использовать более быструю модель (Qwen2.5 или DeepSeek-R1)
118
+ 2. Уменьшить max_tokens до 2048
119
+ 3. Уменьшить max_steps до 5
120
+ 4. Включить потоковую обработку для больших файлов
121
+
122
+ ### Для баланса скорости и качества (уровень 2):
123
+ 1. Оставить DeepSeek-V3.1-Terminus
124
+ 2. Уменьшить max_tokens до 2048
125
+ 3. Оптимизировать промпты (делать их короче и конкретнее)
126
+ 4. Использовать потоковую обработку
127
+
128
+ ### Для максимального качества (текущий уровень):
129
+ 1. Оставить текущую настройку
130
+ 2. Использовать retry механизм
131
+ 3. Использовать потоковую обработку для больших файлов
132
+
133
+ ## Пример реализации быстрой модели
134
+
135
+ Создать `agents/fast_agents.py`:
136
+ ```python
137
+ import os
138
+ from smolagents import ToolCallingAgent, InferenceClientModel, FinalAnswerTool
139
+
140
+ hf_token = os.getenv("HF_TOKEN")
141
+
142
+ # Быстрая модель для быстрой обр��ботки
143
+ fast_model = InferenceClientModel(
144
+ model_id="Qwen/Qwen2.5-7B-Instruct", # или "deepseek-ai/DeepSeek-R1"
145
+ token=hf_token,
146
+ max_tokens=2048,
147
+ temperature=0.3 # Меньше творчества = быстрее
148
+ )
149
+
150
+ # Использовать fast_model вместо model для быстрых задач
151
+ ```
152
+
153
+ ## Тестирование скорости
154
+
155
+ Создать бенчмарк:
156
+ ```python
157
+ import time
158
+ from test_logs.good_example_1_web_server import test_logs
159
+
160
+ start = time.time()
161
+ result = analyze_logs(test_logs)
162
+ end = time.time()
163
+
164
+ print(f"Время обработки: {end - start:.2f} секунд")
165
+ ```
166
+
167
+ ## Вывод
168
+
169
+ **Рекомендуемый подход:**
170
+ 1. Добавить выбор модели в интерфейс (Fast/Standard)
171
+ 2. Использовать Qwen2.5 для быстрого режима
172
+ 3. Оставить DeepSeek-V3.1 для режима максимального качества
173
+ 4. Включить потоковую обработку по умолчанию для файлов >500 строк
__pycache__/app.cpython-314.pyc CHANGED
Binary files a/__pycache__/app.cpython-314.pyc and b/__pycache__/app.cpython-314.pyc differ
 
app.py CHANGED
@@ -9,6 +9,7 @@ import os
9
  from typing import Tuple
10
 
11
  from agents import run_parser_agent, run_anomaly_agent, run_rca_agent, run_gpt_prompt_agent
 
12
 
13
 
14
  def format_rca_as_markdown(rca_result: dict) -> str:
@@ -76,12 +77,14 @@ def format_rca_as_markdown(rca_result: dict) -> str:
76
  return "".join(markdown_parts)
77
 
78
 
79
- def analyze_logs(raw_logs: str) -> Tuple[str, str, str, str]:
80
  """
81
  Обёртка для Gradio интерфейса.
82
 
83
  Args:
84
  raw_logs: Сырые логи из интерфейса
 
 
85
 
86
  Returns:
87
  Кортеж результатов для отображения:
@@ -97,6 +100,17 @@ def analyze_logs(raw_logs: str) -> Tuple[str, str, str, str]:
97
  error_prompt = "# Ошибка\n\nЛоги не предоставлены для анализа."
98
  return error_json, error_json, "# Ошибка\n\nЛоги не предоставлены для анализа.", error_prompt
99
 
 
 
 
 
 
 
 
 
 
 
 
100
  # Agent 1: Парсинг логов
101
  try:
102
  structured_data = run_parser_agent(raw_logs)
@@ -134,13 +148,48 @@ def analyze_logs(raw_logs: str) -> Tuple[str, str, str, str]:
134
  gpt_prompt = f"# Ошибка генерации промпта\n\nПроизошла ошибка при генерации промпта для GPT: {str(e)}\n\nПопробуйте использовать информацию из других вкладок."
135
 
136
  return parsed_json, anomalies_json, recommendations_md, gpt_prompt
137
-
138
  except Exception as e:
139
  error_json = json.dumps({"error": f"Критическая ошибка: {str(e)}"}, ensure_ascii=False, indent=2)
140
  error_prompt = f"# Критическая ошибка\n\n{str(e)}"
141
  return error_json, error_json, f"# Критическая ошибка\n\n{str(e)}", error_prompt
142
 
143
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
  def create_interface():
145
  """Создаёт и настраивает Gradio интерфейс."""
146
 
@@ -176,6 +225,22 @@ def create_interface():
176
  file_count="single"
177
  )
178
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  analyze_btn = gr.Button("🔍 Анализировать", variant="primary", size="lg")
180
 
181
  # Обработчик загрузки файла
@@ -266,7 +331,7 @@ def create_interface():
266
  # Связывание кнопки анализа с обработчиком
267
  analyze_btn.click(
268
  fn=analyze_logs,
269
- inputs=log_input,
270
  outputs=[parsed_output, anomalies_output, recommendations_output, gpt_prompt_output]
271
  )
272
 
 
9
  from typing import Tuple
10
 
11
  from agents import run_parser_agent, run_anomaly_agent, run_rca_agent, run_gpt_prompt_agent
12
+ from utils.streaming_processor import process_logs_streaming
13
 
14
 
15
  def format_rca_as_markdown(rca_result: dict) -> str:
 
77
  return "".join(markdown_parts)
78
 
79
 
80
+ def analyze_logs(raw_logs: str, use_streaming: bool = False, chunk_size: int = 100) -> Tuple[str, str, str, str]:
81
  """
82
  Обёртка для Gradio интерфейса.
83
 
84
  Args:
85
  raw_logs: Сырые логи из интерфейса
86
+ use_streaming: Использовать потоковую обработку для больших логов
87
+ chunk_size: Размер чанка для потоковой обработки (в строках)
88
 
89
  Returns:
90
  Кортеж результатов для отображения:
 
100
  error_prompt = "# Ошибка\n\nЛоги не предоставлены для анализа."
101
  return error_json, error_json, "# Ошибка\n\nЛоги не предоставлены для анализа.", error_prompt
102
 
103
+ # Определяем количество строк
104
+ log_lines = len(raw_logs.strip().split('\n'))
105
+
106
+ # Автоматически включаем потоковую обработку для больших файлов
107
+ # или если пользователь явно включил её
108
+ should_stream = use_streaming or log_lines > 500
109
+
110
+ if should_stream:
111
+ print(f"[Streaming Mode] Processing {log_lines} lines in chunks of {chunk_size}...")
112
+ return analyze_logs_streaming(raw_logs, chunk_size)
113
+
114
  # Agent 1: Парсинг логов
115
  try:
116
  structured_data = run_parser_agent(raw_logs)
 
148
  gpt_prompt = f"# Ошибка генерации промпта\n\nПроизошла ошибка при генерации промпта для GPT: {str(e)}\n\nПопробуйте использовать информацию из других вкладок."
149
 
150
  return parsed_json, anomalies_json, recommendations_md, gpt_prompt
151
+
152
  except Exception as e:
153
  error_json = json.dumps({"error": f"Критическая ошибка: {str(e)}"}, ensure_ascii=False, indent=2)
154
  error_prompt = f"# Критическая ошибка\n\n{str(e)}"
155
  return error_json, error_json, f"# Критическая ошибка\n\n{str(e)}", error_prompt
156
 
157
 
158
+ def analyze_logs_streaming(raw_logs: str, chunk_size: int = 100) -> Tuple[str, str, str, str]:
159
+ """
160
+ Потоковая обработка логов для больших файлов.
161
+
162
+ Args:
163
+ raw_logs: Сырые логи из интерфейса
164
+ chunk_size: Размер чанка в строках
165
+
166
+ Returns:
167
+ Кортеж результатов для отображения
168
+ """
169
+ try:
170
+ result = process_logs_streaming(raw_logs, chunk_size=chunk_size)
171
+
172
+ structured_data = result['structured_data']
173
+ anomaly_report = result['anomaly_report']
174
+ rca_result = result['rca_result']
175
+ gpt_prompt = result['gpt_prompt']
176
+
177
+ parsed_json = json.dumps(structured_data, ensure_ascii=False, indent=2)
178
+ anomalies_json = json.dumps(anomaly_report, ensure_ascii=False, indent=2)
179
+ recommendations_md = format_rca_as_markdown(rca_result)
180
+
181
+ # Добавляем информацию о потоковой обработке
182
+ info_note = f"\n\n*Обработано потоковым способом: {result['chunks_processed']} чанков по {result['chunk_size']} строк*\n"
183
+ recommendations_md = recommendations_md + info_note
184
+
185
+ return parsed_json, anomalies_json, recommendations_md, gpt_prompt
186
+
187
+ except Exception as e:
188
+ error_json = json.dumps({"error": f"Ошибка потоковой обработки: {str(e)}"}, ensure_ascii=False, indent=2)
189
+ error_prompt = f"# Ошибка\n\nОшибка потоковой обработки: {str(e)}"
190
+ return error_json, error_json, error_prompt, error_prompt
191
+
192
+
193
  def create_interface():
194
  """Создаёт и настраивает Gradio интерфейс."""
195
 
 
225
  file_count="single"
226
  )
227
 
228
+ # Настройки обработки
229
+ with gr.Accordion("⚙️ Настройки обработки", open=False):
230
+ use_streaming = gr.Checkbox(
231
+ label="Потоковая обработка (для больших файлов >500 строк)",
232
+ value=False,
233
+ info="Автоматически включается для файлов >500 строк. Разбивает логи на части для более быстрой обработки."
234
+ )
235
+ chunk_size = gr.Slider(
236
+ label="Размер чанка (строк)",
237
+ minimum=50,
238
+ maximum=200,
239
+ value=100,
240
+ step=50,
241
+ info="Количество строк в одном чанке при потоковой обработке"
242
+ )
243
+
244
  analyze_btn = gr.Button("🔍 Анализировать", variant="primary", size="lg")
245
 
246
  # Обработчик загрузки файла
 
331
  # Связывание кнопки анализа с обработчиком
332
  analyze_btn.click(
333
  fn=analyze_logs,
334
+ inputs=[log_input, use_streaming, chunk_size],
335
  outputs=[parsed_output, anomalies_output, recommendations_output, gpt_prompt_output]
336
  )
337
 
utils/streaming_processor.py ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Потоковая обработка больших логов.
3
+ Разбивает логи на чанки и обрабатывает их параллельно или последовательно.
4
+ """
5
+
6
+ from typing import List, Dict, Any, Callable, Generator
7
+ from agents import run_parser_agent, run_anomaly_agent, run_rca_agent, run_gpt_prompt_agent
8
+
9
+
10
+ def split_logs_into_chunks(logs: str, chunk_size: int = 100) -> Generator[str, None, None]:
11
+ """
12
+ Разбивает логи на чанки по количеству строк.
13
+
14
+ Args:
15
+ logs: Полный текст логов
16
+ chunk_size: Количество строк в одном чанке
17
+
18
+ Yields:
19
+ str: Чанк логов
20
+ """
21
+ lines = logs.strip().split('\n')
22
+ total_lines = len(lines)
23
+
24
+ for i in range(0, total_lines, chunk_size):
25
+ chunk_lines = lines[i:i + chunk_size]
26
+ yield '\n'.join(chunk_lines)
27
+
28
+
29
+ def process_logs_streaming(
30
+ raw_logs: str,
31
+ chunk_size: int = 100,
32
+ parallel: bool = False
33
+ ) -> Dict[str, Any]:
34
+ """
35
+ Обрабатывает логи потоковым способом - разбивает на чанки и обрабатывает каждый.
36
+
37
+ Args:
38
+ raw_logs: Сырые логи для обработки
39
+ chunk_size: Размер чанка в строках
40
+ parallel: Если True, обрабатывает чанки параллельно (требует threading/multiprocessing)
41
+
42
+ Returns:
43
+ dict: Объединенные результаты всех чанков
44
+ """
45
+ chunks = list(split_logs_into_chunks(raw_logs, chunk_size))
46
+ total_chunks = len(chunks)
47
+
48
+ all_events = []
49
+ all_errors = []
50
+ all_warnings = []
51
+ all_anomalies = []
52
+
53
+ statistics_accumulator = {
54
+ 'total_lines': 0,
55
+ 'parsed_events': 0,
56
+ 'errors': 0,
57
+ 'warnings': 0,
58
+ 'info_messages': 0,
59
+ 'event_types': {},
60
+ 'time_range': None
61
+ }
62
+
63
+ # Обрабатываем каждый чанк
64
+ for chunk_idx, chunk in enumerate(chunks, 1):
65
+ print(f"[Streaming] Processing chunk {chunk_idx}/{total_chunks} ({len(chunk.split(chr(10)))} lines)...")
66
+
67
+ # Шаг 1: Парсинг чанка
68
+ try:
69
+ structured_data = run_parser_agent(chunk)
70
+
71
+ # Объединяем результаты
72
+ all_events.extend(structured_data.get('events', []))
73
+ all_errors.extend(structured_data.get('errors', []))
74
+ all_warnings.extend(structured_data.get('warnings', []))
75
+
76
+ # Обновляем статистику
77
+ stats = structured_data.get('statistics', {})
78
+ statistics_accumulator['total_lines'] += stats.get('total_lines', 0)
79
+ statistics_accumulator['parsed_events'] += stats.get('parsed_events', 0)
80
+ statistics_accumulator['errors'] += stats.get('errors', 0)
81
+ statistics_accumulator['warnings'] += stats.get('warnings', 0)
82
+ statistics_accumulator['info_messages'] += stats.get('info_messages', 0)
83
+
84
+ # Объединяем типы событий
85
+ for event_type, count in stats.get('event_types', {}).items():
86
+ statistics_accumulator['event_types'][event_type] = \
87
+ statistics_accumulator['event_types'].get(event_type, 0) + count
88
+
89
+ # Обновляем временной диапазон (берем самый ранний start и самый поздний end)
90
+ chunk_time_range = stats.get('time_range')
91
+ if chunk_time_range:
92
+ if statistics_accumulator['time_range'] is None:
93
+ statistics_accumulator['time_range'] = chunk_time_range.copy()
94
+ else:
95
+ if chunk_time_range.get('start'):
96
+ if (statistics_accumulator['time_range'].get('start') is None or
97
+ chunk_time_range['start'] < statistics_accumulator['time_range']['start']):
98
+ statistics_accumulator['time_range']['start'] = chunk_time_range['start']
99
+
100
+ if chunk_time_range.get('end'):
101
+ if (statistics_accumulator['time_range'].get('end') is None or
102
+ chunk_time_range['end'] > statistics_accumulator['time_range']['end']):
103
+ statistics_accumulator['time_range']['end'] = chunk_time_range['end']
104
+
105
+ except Exception as e:
106
+ print(f"[Streaming] Error processing chunk {chunk_idx}: {e}")
107
+ continue
108
+
109
+ # Обновляем номера строк в объединенных событиях
110
+ for idx, event in enumerate(all_events, 1):
111
+ event['line_number'] = idx
112
+
113
+ # Шаг 2: Обнаружение аномалий на объединенных данных
114
+ merged_structured_data = {
115
+ 'events': all_events,
116
+ 'errors': all_errors,
117
+ 'warnings': all_warnings,
118
+ 'statistics': statistics_accumulator
119
+ }
120
+
121
+ print(f"[Streaming] Analyzing {len(all_events)} total events for anomalies...")
122
+ anomaly_report = run_anomaly_agent(merged_structured_data)
123
+ all_anomalies = anomaly_report.get('anomalies', [])
124
+
125
+ # Шаг 3: Анализ первопричин
126
+ print(f"[Streaming] Running root cause analysis...")
127
+ rca_result = run_rca_agent(anomaly_report)
128
+
129
+ # Шаг 4: Генерация промпта для GPT
130
+ print(f"[Streaming] Generating GPT prompt...")
131
+ recommendations_md = format_rca_as_markdown_streaming(rca_result)
132
+ gpt_prompt = run_gpt_prompt_agent(merged_structured_data, anomaly_report, recommendations_md)
133
+
134
+ return {
135
+ 'structured_data': merged_structured_data,
136
+ 'anomaly_report': anomaly_report,
137
+ 'rca_result': rca_result,
138
+ 'gpt_prompt': gpt_prompt,
139
+ 'chunks_processed': total_chunks,
140
+ 'chunk_size': chunk_size
141
+ }
142
+
143
+
144
+ def format_rca_as_markdown_streaming(rca_result: dict) -> str:
145
+ """Форматирует результат RCA в Markdown (копия из app.py для избежания циклических зависимостей)."""
146
+ markdown_parts = []
147
+
148
+ # Возможные первопричины
149
+ analysis = rca_result.get("analysis", {})
150
+ root_causes = analysis.get("root_causes", [])
151
+ if root_causes:
152
+ markdown_parts.append("## Возможные первопричины\n\n")
153
+ for i, cause in enumerate(root_causes, 1):
154
+ markdown_parts.append(f"{i}. {cause}\n")
155
+ markdown_parts.append("\n")
156
+
157
+ # Детальный анализ аномалий
158
+ details = analysis.get("details", [])
159
+ if details:
160
+ markdown_parts.append("## Детальный анализ аномалий\n\n")
161
+ for detail in details:
162
+ anomaly_type = detail.get("anomaly_type", "UNKNOWN")
163
+ severity = detail.get("severity", "MEDIUM")
164
+ description = detail.get("description", "")
165
+ possible_causes = detail.get("possible_causes", [])
166
+
167
+ emoji = "🔴" if severity == "CRITICAL" else "🟡" if severity == "HIGH" else "🟢" if severity == "MEDIUM" else "⚪"
168
+ markdown_parts.append(f"### {emoji} {anomaly_type} ({severity})\n\n{description}\n\n")
169
+
170
+ if possible_causes:
171
+ markdown_parts.append("#### Возможные первопричины:\n\n")
172
+ for i, cause in enumerate(possible_causes, 1):
173
+ markdown_parts.append(f"{i}. {cause}\n")
174
+ markdown_parts.append("\n")
175
+
176
+ # Рекомендации по устранению
177
+ recommendations = rca_result.get("recommendations", [])
178
+ if recommendations:
179
+ markdown_parts.append("## Рекомендации по устранению\n\n")
180
+ for rec in recommendations:
181
+ priority = rec.get("priority", "MEDIUM")
182
+ text = rec.get("text", "")
183
+ actions = rec.get("actions", [])
184
+
185
+ emoji = "🔴" if priority == "CRITICAL" else "🟡" if priority == "HIGH" else "🟢" if priority == "MEDIUM" else "⚪"
186
+ markdown_parts.append(f"### {emoji} Рекомендация (Приоритет: {priority})\n\n{text}\n\n")
187
+
188
+ if actions:
189
+ markdown_parts.append("**Конкретные действия:**\n\n")
190
+ for action in actions:
191
+ markdown_parts.append(f"- {action}\n")
192
+ markdown_parts.append("\n")
193
+
194
+ # Общие рекомендации
195
+ general = rca_result.get("general_recommendations", [])
196
+ if general:
197
+ markdown_parts.append("## Общие рекомендации\n\n")
198
+ for rec in general:
199
+ markdown_parts.append(f"- {rec}\n")
200
+
201
+ return "".join(markdown_parts)