DocUA commited on
Commit
26e822e
·
1 Parent(s): deadd5a

Файл interface

Browse files
Files changed (2) hide show
  1. app.py +151 -557
  2. interface.py +217 -0
app.py CHANGED
@@ -1,14 +1,8 @@
1
  import os
2
- import gradio as gr
3
- from datetime import datetime
4
- import pandas as pd
5
- import numpy as np
6
- import matplotlib.pyplot as plt
7
- import seaborn as sns
8
  from pathlib import Path
9
- import tempfile
10
  import traceback
11
- import logging
12
 
13
  # Налаштування логування
14
  logging.basicConfig(
@@ -25,571 +19,171 @@ logger = logging.getLogger("jira_assistant")
25
  for directory in ["data", "reports", "temp", "logs"]:
26
  Path(directory).mkdir(exist_ok=True, parents=True)
27
 
28
- # Клас для аналізу даних Jira
29
- class JiraAnalyzer:
 
 
 
 
 
 
 
 
 
30
  def __init__(self):
31
- self.df = None
32
- self.stats = None
33
- self.inactive_issues = None
34
-
35
- def load_csv(self, file_path):
36
- """Завантаження даних з CSV-файлу"""
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  try:
38
- logger.info(f"Завантаження CSV-файлу: {file_path}")
39
-
40
- # Спробуємо різні кодування
41
- try:
42
- self.df = pd.read_csv(file_path, encoding='utf-8')
43
- except UnicodeDecodeError:
44
- logger.warning("Помилка декодування UTF-8, спроба з latin1")
45
- self.df = pd.read_csv(file_path, encoding='latin1')
46
-
47
- logger.info(f"Успішно завантажено {len(self.df)} записів")
48
- return self.df
49
- except Exception as e:
50
- logger.error(f"Помилка при завантаженні CSV-файлу: {e}")
51
- raise
52
-
53
- def process_dates(self):
54
- """Обробка дат у DataFrame"""
55
- date_columns = ['Created', 'Updated', 'Resolved', 'Due Date']
56
-
57
- for col in date_columns:
58
- if col in self.df.columns:
59
- try:
60
- self.df[col] = pd.to_datetime(self.df[col], errors='coerce')
61
- except Exception as e:
62
- logger.warning(f"Не вдалося конвертувати колонку {col} до datetime: {e}")
63
-
64
- def prepare_data(self):
65
- """Підготовка даних для аналізу"""
66
- # Обробка дат
67
- self.process_dates()
68
-
69
- # Додаткова обробка
70
- if 'Status' in self.df.columns:
71
- self.df['Status'] = self.df['Status'].fillna('Unknown')
72
-
73
- if 'Priority' in self.df.columns:
74
- self.df['Priority'] = self.df['Priority'].fillna('Not set')
75
-
76
- # Створення додаткових колонок
77
- if 'Created' in self.df.columns and pd.api.types.is_datetime64_dtype(self.df['Created']):
78
- self.df['Created_Date'] = self.df['Created'].dt.date
79
- self.df['Created_Month'] = self.df['Created'].dt.to_period('M')
80
-
81
- if 'Updated' in self.df.columns and pd.api.types.is_datetime64_dtype(self.df['Updated']):
82
- self.df['Updated_Date'] = self.df['Updated'].dt.date
83
- self.df['Days_Since_Update'] = (datetime.now() - self.df['Updated']).dt.days
84
-
85
- def generate_stats(self):
86
- """Генерація базової статистики"""
87
- self.stats = {
88
- 'total_tickets': len(self.df),
89
- 'status_counts': {},
90
- 'type_counts': {},
91
- 'priority_counts': {}
92
- }
93
-
94
- # Статистика за статусами
95
- if 'Status' in self.df.columns:
96
- self.stats['status_counts'] = self.df['Status'].value_counts().to_dict()
97
-
98
- # Статистика за типами
99
- if 'Issue Type' in self.df.columns:
100
- self.stats['type_counts'] = self.df['Issue Type'].value_counts().to_dict()
101
-
102
- # Статистика за пріоритетами
103
- if 'Priority' in self.df.columns:
104
- self.stats['priority_counts'] = self.df['Priority'].value_counts().to_dict()
105
-
106
- return self.stats
107
-
108
- def find_inactive_issues(self, days=14):
109
- """Аналіз неактивних тікетів"""
110
- self.inactive_issues = {
111
- 'total_count': 0,
112
- 'percentage': 0,
113
- 'by_status': {},
114
- 'top_inactive': []
115
- }
116
-
117
- if 'Updated' not in self.df.columns or not pd.api.types.is_datetime64_dtype(self.df['Updated']):
118
- return self.inactive_issues
119
-
120
- # Визначення неактивних тікетів
121
- cutoff_date = datetime.now() - pd.Timedelta(days=days)
122
- inactive = self.df[self.df['Updated'] < cutoff_date]
123
-
124
- if len(inactive) == 0:
125
- return self.inactive_issues
126
-
127
- self.inactive_issues['total_count'] = len(inactive)
128
- self.inactive_issues['percentage'] = round(len(inactive) / len(self.df) * 100, 2) if len(self.df) > 0 else 0
129
-
130
- # Розподіл за статусами
131
- if 'Status' in inactive.columns:
132
- self.inactive_issues['by_status'] = inactive['Status'].value_counts().to_dict()
133
-
134
- # Топ 5 неактивних тікетів
135
- top_inactive = []
136
- for _, row in inactive.sort_values('Updated', ascending=True).head(5).iterrows():
137
- issue_data = {
138
- 'key': row.get('Issue key', 'Невідомо'),
139
- 'summary': row.get('Summary', 'Невідомо'),
140
- 'status': row.get('Status', 'Невідомо'),
141
- 'days_inactive': (datetime.now() - row['Updated']).days if pd.notna(row['Updated']) else 'Невідомо'
142
- }
143
- top_inactive.append(issue_data)
144
-
145
- self.inactive_issues['top_inactive'] = top_inactive
146
- return self.inactive_issues
147
-
148
- def plot_status_counts(self):
149
- """Діаграма розподілу за статусами"""
150
- if 'Status' not in self.df.columns:
151
- return None
152
-
153
- status_counts = self.df['Status'].value_counts()
154
-
155
- fig, ax = plt.subplots(figsize=(10, 6))
156
- bars = sns.barplot(x=status_counts.index, y=status_counts.values, ax=ax)
157
-
158
- # Додаємо підписи значень над стовпцями
159
- for i, v in enumerate(status_counts.values):
160
- ax.text(i, v + 0.5, str(v), ha='center')
161
-
162
- ax.set_title('Розподіл тікетів за статусами')
163
- ax.set_xlabel('Статус')
164
- ax.set_ylabel('Кількість')
165
- plt.xticks(rotation=45)
166
- plt.tight_layout()
167
-
168
- return fig
169
-
170
- def plot_priority_counts(self):
171
- """Діаграма розподілу за пріоритетами"""
172
- if 'Priority' not in self.df.columns:
173
- return None
174
-
175
- priority_counts = self.df['Priority'].value_counts()
176
-
177
- fig, ax = plt.subplots(figsize=(10, 6))
178
- bars = sns.barplot(x=priority_counts.index, y=priority_counts.values, ax=ax)
179
-
180
- # Додаємо підписи значень над стовпцями
181
- for i, v in enumerate(priority_counts.values):
182
- ax.text(i, v + 0.5, str(v), ha='center')
183
-
184
- ax.set_title('Розподіл тікетів за пріоритетами')
185
- ax.set_xlabel('Пріоритет')
186
- ax.set_ylabel('Кількість')
187
- plt.xticks(rotation=45)
188
- plt.tight_layout()
189
-
190
- return fig
191
-
192
- def plot_created_timeline(self):
193
- """Часова діаграма створення тікетів"""
194
- if 'Created' not in self.df.columns or not pd.api.types.is_datetime64_dtype(self.df['Created']):
195
- return None
196
-
197
- # Додаємо колонку з датою створення (без часу)
198
- if 'Created_Date' not in self.df.columns:
199
- self.df['Created_Date'] = self.df['Created'].dt.date
200
-
201
- # Кількість створених тікетів за датами
202
- created_by_date = self.df['Created_Date'].value_counts().sort_index()
203
-
204
- # Створення діаграми
205
- fig, ax = plt.subplots(figsize=(12, 6))
206
- created_by_date.plot(kind='line', marker='o', ax=ax)
207
-
208
- ax.set_title('Кількість створених тікетів за датами')
209
- ax.set_xlabel('Дата')
210
- ax.set_ylabel('Кількість')
211
- ax.grid(True)
212
- plt.tight_layout()
213
-
214
- return fig
215
-
216
- def generate_report(self, inactive_days=14):
217
- """Генерація звіту"""
218
- report = []
219
-
220
- # Заголовок звіту
221
- report.append("# Звіт аналізу Jira")
222
- report.append(f"*Створено: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*")
223
-
224
- # Загальна статистика
225
- report.append("\n## Загальна статистика")
226
- report.append(f"**Загальна кількість тікетів:** {len(self.df)}")
227
-
228
- # Статистика за статусами
229
- if 'Status' in self.df.columns:
230
- status_counts = self.df['Status'].value_counts()
231
- report.append("\n### Статуси тікетів")
232
-
233
- for status, count in status_counts.items():
234
- percentage = count / len(self.df) * 100 if len(self.df) > 0 else 0
235
- report.append(f"- **{status}:** {count} ({percentage:.1f}%)")
236
-
237
- # Статистика за типами
238
- if 'Issue Type' in self.df.columns:
239
- type_counts = self.df['Issue Type'].value_counts()
240
- report.append("\n### Типи тікетів")
241
-
242
- for type_name, count in type_counts.items():
243
- percentage = count / len(self.df) * 100 if len(self.df) > 0 else 0
244
- report.append(f"- **{type_name}:** {count} ({percentage:.1f}%)")
245
-
246
- # Статистика за пріоритетами
247
- if 'Priority' in self.df.columns:
248
- priority_counts = self.df['Priority'].value_counts()
249
- report.append("\n### Пріоритети тікетів")
250
 
251
- for priority, count in priority_counts.items():
252
- percentage = count / len(self.df) * 100 if len(self.df) > 0 else 0
253
- report.append(f"- **{priority}:** {count} ({percentage:.1f}%)")
254
-
255
- # Неактивні тікети
256
- self.find_inactive_issues(inactive_days)
257
-
258
- if self.inactive_issues['total_count'] > 0:
259
- report.append(f"\n## Неактивні тікети (>{inactive_days} днів)")
260
- report.append(f"**Загальна кількість неактивних тікетів:** {self.inactive_issues['total_count']} ({self.inactive_issues['percentage']}%)")
261
 
262
- if self.inactive_issues['by_status']:
263
- report.append("\n**Неактивні тікети за статусами:**")
264
- for status, count in self.inactive_issues['by_status'].items():
265
- report.append(f"- **{status}:** {count}")
266
 
267
- if self.inactive_issues['top_inactive']:
268
- report.append("\n**Топ 5 найбільш неактивних тікетів:**")
269
- for i, ticket in enumerate(self.inactive_issues['top_inactive']):
270
- report.append(f"{i+1}. **{ticket['key']}:** {ticket['summary']}")
271
- report.append(f" - Статус: {ticket['status']}")
272
- report.append(f" - Днів неактивності: {ticket['days_inactive']}")
273
-
274
- return "\n".join(report)
275
-
276
- # Виправлена функція analyze_csv
277
- def analyze_csv(file_obj, inactive_days, include_ai):
278
- if file_obj is None:
279
- return "Помилка: файл не вибрано", None, None, None, None
280
-
281
- try:
282
- logger.info(f"Отримано файл: {file_obj.name}, тип: {type(file_obj)}")
283
-
284
- # Створення тимчасового файлу
285
- temp_file_path = os.path.join("temp", f"temp_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv")
286
-
287
- # У Gradio 5.19.0 об'єкт файлу має різну структуру
288
- # file_obj може бути шляхом до файлу або містити атрибут 'name'
289
- if hasattr(file_obj, 'name'):
290
- source_path = file_obj.name
291
 
292
- # Копіювання файлу
293
- import shutil
294
- shutil.copy2(source_path, temp_file_path)
295
- else:
296
- # Якщо це не шлях до файлу, ймовірно це вже самі дані
297
- with open(temp_file_path, "w", encoding="utf-8") as f:
298
- f.write(str(file_obj))
299
-
300
- # Аналіз даних
301
- analyzer = JiraAnalyzer()
302
- analyzer.load_csv(temp_file_path)
303
- analyzer.prepare_data()
304
- analyzer.generate_stats()
305
-
306
- # Візуалізації
307
- status_fig = analyzer.plot_status_counts()
308
- priority_fig = analyzer.plot_priority_counts()
309
- timeline_fig = analyzer.plot_created_timeline()
310
-
311
- # Генерація звіту
312
- report = analyzer.generate_report(inactive_days=inactive_days)
313
-
314
- # AI аналіз (заглушка)
315
- ai_analysis = None
316
- if include_ai:
317
- ai_analysis = "AI аналіз буде доступний у наступних версіях додатку."
318
-
319
- # Видалення тимчасового файлу
320
- try:
321
- os.remove(temp_file_path)
322
- except:
323
- pass
324
-
325
- return report, status_fig, priority_fig, timeline_fig, ai_analysis
326
-
327
- except Exception as e:
328
- error_msg = f"Помилка аналізу: {str(e)}\n\n{traceback.format_exc()}"
329
- logger.error(error_msg)
330
- return error_msg, None, None, None, None
331
-
332
- # Функція для збереження звіту
333
- def save_report(report_text, format_type, include_visualizations, status_fig, priority_fig, timeline_fig):
334
- if not report_text:
335
- return "Помилка: спочатку виконайте аналіз даних"
336
-
337
- try:
338
- # Створення імені файлу
339
- timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
340
- report_filename = f"jira_report_{timestamp}"
341
- reports_dir = Path("reports")
342
-
343
- if format_type == "markdown":
344
- filepath = reports_dir / f"{report_filename}.md"
345
- with open(filepath, 'w', encoding='utf-8') as f:
346
- f.write(report_text)
347
-
348
- if include_visualizations:
349
- # Збереження візуалізацій
350
- charts_dir = reports_dir / f"{report_filename}_charts"
351
- charts_dir.mkdir(exist_ok=True)
352
-
353
- if status_fig:
354
- status_fig.savefig(charts_dir / "status_counts.png")
355
- if priority_fig:
356
- priority_fig.savefig(charts_dir / "priority_counts.png")
357
- if timeline_fig:
358
- timeline_fig.savefig(charts_dir / "timeline.png")
359
-
360
- elif format_type == "html":
361
- from markdown import markdown
362
 
363
- filepath = reports_dir / f"{report_filename}.html"
 
364
 
365
- # Конвертація Markdown в HTML
366
- html_content = f"""<!DOCTYPE html>
367
- <html>
368
- <head>
369
- <meta charset="utf-8">
370
- <title>Звіт аналізу Jira</title>
371
- <style>
372
- body {{ font-family: Arial, sans-serif; line-height: 1.6; margin: 20px; }}
373
- h1, h2, h3 {{ color: #0052CC; }}
374
- table {{ border-collapse: collapse; width: 100%; margin-bottom: 20px; }}
375
- th, td {{ padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }}
376
- th {{ background-color: #0052CC; color: white; }}
377
- img {{ max-width: 100%; margin: 20px 0; }}
378
- </style>
379
- </head>
380
- <body>
381
- {markdown(report_text)}
382
- """
383
 
384
- if include_visualizations and (status_fig or priority_fig or timeline_fig):
385
- # Збереження візуалізацій
386
- charts_dir = reports_dir / f"{report_filename}_charts"
387
- charts_dir.mkdir(exist_ok=True)
388
-
389
- html_content += "<h2>Візуалізації</h2>"
390
-
391
- if status_fig:
392
- status_path = charts_dir / "status_counts.png"
393
- status_fig.savefig(status_path)
394
- html_content += f'<div><h3>Статуси тікетів</h3><img src="{status_path.relative_to(reports_dir)}" alt="Статуси тікетів"></div>'
395
-
396
- if priority_fig:
397
- priority_path = charts_dir / "priority_counts.png"
398
- priority_fig.savefig(priority_path)
399
- html_content += f'<div><h3>Пріоритети тікетів</h3><img src="{priority_path.relative_to(reports_dir)}" alt="Пріоритети тікетів"></div>'
400
-
401
- if timeline_fig:
402
- timeline_path = charts_dir / "timeline.png"
403
- timeline_fig.savefig(timeline_path)
404
- html_content += f'<div><h3>Часова шкала</h3><img src="{timeline_path.relative_to(reports_dir)}" alt="Часова шкала"></div>'
405
 
406
- html_content += "</body></html>"
 
 
 
 
 
407
 
408
- with open(filepath, 'w', encoding='utf-8') as f:
409
- f.write(html_content)
410
-
411
- elif format_type == "pdf":
412
- try:
413
- from weasyprint import HTML
414
-
415
- filepath = reports_dir / f"{report_filename}.pdf"
416
-
417
- # Створюємо тимчасовий HTML-файл
418
- temp_html_path = reports_dir / f"{report_filename}_temp.html"
419
-
420
- # Конвертація Markdown в HTML
421
- from markdown import markdown
422
- html_content = f"""<!DOCTYPE html>
423
- <html>
424
- <head>
425
- <meta charset="utf-8">
426
- <title>Звіт аналізу Jira</title>
427
- <style>
428
- body {{ font-family: Arial, sans-serif; line-height: 1.6; margin: 20px; }}
429
- h1, h2, h3 {{ color: #0052CC; }}
430
- table {{ border-collapse: collapse; width: 100%; margin-bottom: 20px; }}
431
- th, td {{ padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }}
432
- th {{ background-color: #0052CC; color: white; }}
433
- img {{ max-width: 100%; margin: 20px 0; }}
434
- </style>
435
- </head>
436
- <body>
437
- {markdown(report_text)}
438
- """
439
-
440
- if include_visualizations and (status_fig or priority_fig or timeline_fig):
441
- # Збереження візуалізацій
442
- charts_dir = reports_dir / f"{report_filename}_charts"
443
- charts_dir.mkdir(exist_ok=True)
444
-
445
- html_content += "<h2>Візуалізації</h2>"
446
-
447
- if status_fig:
448
- status_path = charts_dir / "status_counts.png"
449
- status_fig.savefig(status_path)
450
- html_content += f'<div><h3>Статуси тікетів</h3><img src="{status_path}" alt="Статуси тікетів"></div>'
451
-
452
- if priority_fig:
453
- priority_path = charts_dir / "priority_counts.png"
454
- priority_fig.savefig(priority_path)
455
- html_content += f'<div><h3>Пріоритети тікетів</h3><img src="{priority_path}" alt="Пріоритети тікетів"></div>'
456
-
457
- if timeline_fig:
458
- timeline_path = charts_dir / "timeline.png"
459
- timeline_fig.savefig(timeline_path)
460
- html_content += f'<div><h3>Часова шкала</h3><img src="{timeline_path}" alt="Часова шкала"></div>'
461
-
462
- html_content += "</body></html>"
463
-
464
- with open(temp_html_path, 'w', encoding='utf-8') as f:
465
- f.write(html_content)
466
-
467
- # Конвертація HTML в PDF
468
- HTML(filename=str(temp_html_path)).write_pdf(filepath)
469
-
470
- # Видалення тимчасового HTML-файлу
471
- try:
472
- os.remove(temp_html_path)
473
- except:
474
- pass
475
-
476
- except ImportError:
477
- return "Поми��ка: для генерації PDF потрібна бібліотека weasyprint"
478
-
479
- return f"Звіт успішно збережено: {filepath}"
480
-
481
- except Exception as e:
482
- error_msg = f"Помилка при збереженні звіту: {str(e)}\n\n{traceback.format_exc()}"
483
- logger.error(error_msg)
484
- return error_msg
485
-
486
- # Інтерфейс Gradio
487
- with gr.Blocks(title="Jira AI Assistant") as app:
488
- gr.Markdown("# 🔍 Jira AI Assistant")
489
 
490
- with gr.Tabs():
491
- with gr.Tab("CSV Аналіз"):
492
- with gr.Row():
493
- with gr.Column(scale=1):
494
- file_input = gr.File(label="Завантажити CSV файл Jira")
495
- inactive_days = gr.Slider(minimum=1, maximum=90, value=14, step=1,
496
- label="Кількість днів для визначення неактивних тікетів")
497
-
498
- include_ai = gr.Checkbox(label="Включити AI аналіз", value=False)
499
-
500
- analyze_btn = gr.Button("Аналізувати", variant="primary")
501
-
502
- with gr.Accordion("Збереження звіту", open=False):
503
- format_type = gr.Dropdown(
504
- choices=["markdown", "html", "pdf"],
505
- value="markdown",
506
- label="Формат звіту"
507
- )
508
- include_visualizations = gr.Checkbox(
509
- label="Включити візуалізації",
510
- value=True
511
- )
512
- save_btn = gr.Button("Зберегти звіт")
513
- save_output = gr.Textbox(label="Статус збереження")
514
-
515
- with gr.Column(scale=2):
516
- with gr.Tabs():
517
- with gr.Tab("Звіт"):
518
- report_output = gr.Markdown()
519
-
520
- with gr.Tab("Візуалізації"):
521
- with gr.Row():
522
- status_plot = gr.Plot(label="Статуси тікетів")
523
- priority_plot = gr.Plot(label="Пріоритети тікетів")
524
-
525
- timeline_plot = gr.Plot(label="Часова шкала")
526
-
527
- with gr.Tab("AI Аналіз"):
528
- ai_output = gr.Markdown()
529
-
530
- # Встановлюємо обробники подій
531
- analyze_btn.click(
532
- analyze_csv,
533
- inputs=[file_input, inactive_days, include_ai],
534
- outputs=[report_output, status_plot, priority_plot, timeline_plot, ai_output]
535
  )
536
 
537
- save_btn.click(
538
- save_report,
539
- inputs=[report_output, format_type, include_visualizations, status_plot, priority_plot, timeline_plot],
540
- outputs=[save_output]
 
 
541
  )
542
-
543
- with gr.Tab("Jira API"):
544
- gr.Markdown("## Підключення до Jira API")
545
-
546
- with gr.Row():
547
- jira_url = gr.Textbox(
548
- label="Jira URL",
549
- placeholder="https://your-company.atlassian.net"
550
- )
551
- jira_username = gr.Textbox(
552
- label="Ім'я користувача Jira",
553
- placeholder="email@example.com"
554
- )
555
- jira_api_token = gr.Textbox(
556
- label="Jira API Token",
557
- type="password"
558
- )
559
 
560
- test_connection_btn = gr.Button("Тестувати з'єднання")
561
- connection_status = gr.Textbox(label="Статус підключення")
 
 
562
 
563
- gr.Markdown("## ⚠️ Ця функція буде доступна у наступних версіях")
564
-
565
- with gr.Tab("AI Асистенти"):
566
- gr.Markdown("## AI Асистенти для Jira")
567
- gr.Markdown("⚠️ Ця функція буде доступна у наступних версіях")
568
-
569
- with gr.Accordion("Зразок інтерфейсу"):
570
- question = gr.Textbox(
571
- label="Запитання",
572
- placeholder="Наприклад: Які тікети мають найвищий пріоритет?",
573
- lines=2
574
- )
575
- answer = gr.Markdown(label="Відповідь")
576
-
577
- with gr.Tab("Інтеграції"):
578
- gr.Markdown("## Інтеграції з зовнішніми системами")
579
- gr.Markdown("⚠️ Ця функція буде доступна у наступних версіях")
580
-
581
- with gr.Accordion("Slack інтеграція"):
582
- slack_channel = gr.Textbox(
583
- label="Slack канал",
584
- placeholder="#project-updates"
585
- )
586
- slack_message = gr.Textbox(
587
- label="Повідомлення",
588
- placeholder="Тижневий звіт по проекту",
589
- lines=3
590
- )
591
- slack_send_btn = gr.Button("Надіслати у Slack", interactive=False)
592
 
593
- # Запуск додатку
594
  if __name__ == "__main__":
595
- app.launch()
 
 
1
  import os
2
+ import logging
 
 
 
 
 
3
  from pathlib import Path
4
+ from datetime import datetime
5
  import traceback
 
6
 
7
  # Налаштування логування
8
  logging.basicConfig(
 
19
  for directory in ["data", "reports", "temp", "logs"]:
20
  Path(directory).mkdir(exist_ok=True, parents=True)
21
 
22
+ # Імпорт необхідних модулів
23
+ from modules.data_import.csv_importer import JiraCsvImporter
24
+ from modules.data_analysis.statistics import JiraDataAnalyzer
25
+ from modules.data_analysis.visualizations import JiraVisualizer
26
+ from modules.reporting.report_generator import ReportGenerator
27
+ from modules.core.app_manager import AppManager
28
+
29
+ class JiraAssistantApp:
30
+ """
31
+ Головний клас додатку, який координує роботу всіх компонентів
32
+ """
33
  def __init__(self):
34
+ self.app_manager = AppManager()
35
+ self.current_data = None
36
+ self.current_analysis = None
37
+ self.visualizations = None
38
+
39
+ def analyze_csv_file(self, file_path, inactive_days=14, include_ai=False, api_key=None, model_type="openai"):
40
+ """
41
+ Аналіз CSV-файлу Jira
42
+
43
+ Args:
44
+ file_path (str): Шлях до CSV-файлу
45
+ inactive_days (int): Кількість днів для визначення неактивних тікетів
46
+ include_ai (bool): Чи використовувати AI-аналіз
47
+ api_key (str): API ключ для LLM (якщо include_ai=True)
48
+ model_type (str): Тип моделі LLM ("openai" або "gemini")
49
+
50
+ Returns:
51
+ dict: Результати аналізу
52
+ """
53
  try:
54
+ logger.info(f"Аналіз файлу: {file_path}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
+ # Завантаження даних
57
+ csv_importer = JiraCsvImporter(file_path)
58
+ self.current_data = csv_importer.load_data()
 
 
 
 
 
 
 
59
 
60
+ if self.current_data is None:
61
+ return {"error": "Не вдалося завантажити дані з CSV-файлу"}
 
 
62
 
63
+ # Аналіз даних
64
+ analyzer = JiraDataAnalyzer(self.current_data)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
 
66
+ # Базова статистика
67
+ stats = analyzer.generate_basic_statistics()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
 
69
+ # Аналіз неактивних тікетів
70
+ inactive_issues = analyzer.analyze_inactive_issues(days=inactive_days)
71
 
72
+ # Створення візуалізацій
73
+ visualizer = JiraVisualizer(self.current_data)
74
+ self.visualizations = {
75
+ "status": visualizer.plot_status_counts(),
76
+ "priority": visualizer.plot_priority_counts(),
77
+ "type": visualizer.plot_type_counts(),
78
+ "created_timeline": visualizer.plot_created_timeline(),
79
+ "inactive": visualizer.plot_inactive_issues(days=inactive_days)
80
+ }
 
 
 
 
 
 
 
 
 
81
 
82
+ # AI аналіз, якщо потрібен
83
+ ai_analysis = None
84
+ if include_ai and api_key:
85
+ from modules.ai_analysis.llm_connector import LLMConnector
86
+ llm = LLMConnector(api_key=api_key, model_type=model_type)
87
+ ai_analysis = llm.analyze_jira_data(stats, inactive_issues)
88
+
89
+ # Генерація звіту
90
+ report_generator = ReportGenerator(self.current_data, stats, inactive_issues, ai_analysis)
91
+ report = report_generator.create_markdown_report(inactive_days=inactive_days)
92
+
93
+ # Зберігаємо поточний аналіз
94
+ self.current_analysis = {
95
+ "stats": stats,
96
+ "inactive_issues": inactive_issues,
97
+ "report": report,
98
+ "ai_analysis": ai_analysis
99
+ }
 
 
 
100
 
101
+ return {
102
+ "report": report,
103
+ "visualizations": self.visualizations,
104
+ "ai_analysis": ai_analysis,
105
+ "error": None
106
+ }
107
 
108
+ except Exception as e:
109
+ error_msg = f"Помилка аналізу: {str(e)}\n\n{traceback.format_exc()}"
110
+ logger.error(error_msg)
111
+ return {"error": error_msg}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
 
113
+ def save_report(self, format_type="markdown", include_visualizations=True, filepath=None):
114
+ """
115
+ Збереження звіту у файл
116
+
117
+ Args:
118
+ format_type (str): Формат звіту ("markdown", "html", "pdf")
119
+ include_visualizations (bool): Чи включати візуалізації у звіт
120
+ filepath (str): Шлях для збереження файлу
121
+
122
+ Returns:
123
+ str: Шлях до збереженого файлу або повідомлення про помилку
124
+ """
125
+ try:
126
+ if not self.current_analysis or "report" not in self.current_analysis:
127
+ return "Помилка: спочатку виконайте аналіз даних"
128
+
129
+ # Створення імені файлу, якщо не вказано
130
+ if not filepath:
131
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
132
+ report_filename = f"jira_report_{timestamp}"
133
+ reports_dir = Path("reports")
134
+
135
+ if format_type == "markdown":
136
+ filepath = reports_dir / f"{report_filename}.md"
137
+ elif format_type == "html":
138
+ filepath = reports_dir / f"{report_filename}.html"
139
+ elif format_type == "pdf":
140
+ filepath = reports_dir / f"{report_filename}.pdf"
141
+
142
+ # Створення генератора звітів
143
+ report_generator = ReportGenerator(
144
+ self.current_data,
145
+ self.current_analysis.get("stats"),
146
+ self.current_analysis.get("inactive_issues"),
147
+ self.current_analysis.get("ai_analysis")
 
 
 
 
 
 
 
 
 
 
148
  )
149
 
150
+ # Збереження звіту
151
+ saved_path = report_generator.save_report(
152
+ filepath=filepath,
153
+ format=format_type,
154
+ include_visualizations=include_visualizations,
155
+ visualization_data=self.visualizations if include_visualizations else None
156
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
 
158
+ if saved_path:
159
+ return f"Звіт успішно збережено: {saved_path}"
160
+ else:
161
+ return "Не вдалося зберегти звіт"
162
 
163
+ except Exception as e:
164
+ error_msg = f"Помилка при збереженні звіту: {str(e)}\n\n{traceback.format_exc()}"
165
+ logger.error(error_msg)
166
+ return error_msg
167
+
168
+ def test_jira_connection(self, jira_url, username, api_token):
169
+ """
170
+ Тестування підключення до Jira
171
+
172
+ Args:
173
+ jira_url (str): URL сервера Jira
174
+ username (str): Ім'я користувача
175
+ api_token (str): API токен
176
+
177
+ Returns:
178
+ bool: True якщо підключення успішне, False інакше
179
+ """
180
+ from modules.data_import.jira_api import JiraConnector
181
+ return JiraConnector.test_connection(jira_url, username, api_token)
182
+
183
+ # Створення екземпляру додатку
184
+ app = JiraAssistantApp()
 
 
 
 
 
 
 
185
 
186
+ # Точка входу для запуску з командного рядка
187
  if __name__ == "__main__":
188
+ from interface import launch_interface
189
+ launch_interface(app)
interface.py ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import os
3
+ from pathlib import Path
4
+ from datetime import datetime
5
+ import matplotlib.pyplot as plt
6
+ import logging
7
+
8
+ logger = logging.getLogger("jira_assistant_interface")
9
+
10
+ def launch_interface(app):
11
+ """
12
+ Запуск інтерфейсу користувача Gradio
13
+
14
+ Args:
15
+ app: Екземпляр JiraAssistantApp
16
+ """
17
+ # Функція для обробки завантаження та аналізу CSV
18
+ def analyze_csv(file_obj, inactive_days, include_ai):
19
+ if file_obj is None:
20
+ return "Помилка: файл не вибрано", None, None, None, None
21
+
22
+ try:
23
+ logger.info(f"Отримано файл: {file_obj.name}, тип: {type(file_obj)}")
24
+
25
+ # Створення тимчасового файлу
26
+ temp_file_path = os.path.join("temp", f"temp_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv")
27
+
28
+ # У Gradio 5.19.0 об'єкт файлу має різну структуру
29
+ # file_obj може бути шляхом до файлу або містити атрибут 'name'
30
+ if hasattr(file_obj, 'name'):
31
+ source_path = file_obj.name
32
+
33
+ # Копіювання файлу
34
+ import shutil
35
+ shutil.copy2(source_path, temp_file_path)
36
+ else:
37
+ # Якщо це не шлях до файлу, ймовірно це вже самі дані
38
+ with open(temp_file_path, "w", encoding="utf-8") as f:
39
+ f.write(str(file_obj))
40
+
41
+ # Аналіз даних
42
+ api_key = os.getenv("OPENAI_API_KEY") if include_ai else None
43
+ result = app.analyze_csv_file(
44
+ temp_file_path,
45
+ inactive_days=inactive_days,
46
+ include_ai=include_ai,
47
+ api_key=api_key
48
+ )
49
+
50
+ # Видалення тимчасового файлу
51
+ try:
52
+ os.remove(temp_file_path)
53
+ except:
54
+ pass
55
+
56
+ if result.get("error"):
57
+ return result.get("error"), None, None, None, None
58
+
59
+ return (
60
+ result.get("report", ""),
61
+ result.get("visualizations", {}).get("status"),
62
+ result.get("visualizations", {}).get("priority"),
63
+ result.get("visualizations", {}).get("created_timeline"),
64
+ result.get("ai_analysis", "AI аналіз буде доступний у наступних версіях додатку.")
65
+ )
66
+
67
+ except Exception as e:
68
+ import traceback
69
+ error_msg = f"Помилка аналізу: {str(e)}\n\n{traceback.format_exc()}"
70
+ logger.error(error_msg)
71
+ return error_msg, None, None, None, None
72
+
73
+ # Функція для збереження звіту
74
+ def save_report_handler(report_text, format_type, include_visualizations):
75
+ if not report_text:
76
+ return "Помилка: спочатку виконайте аналіз даних"
77
+
78
+ return app.save_report(
79
+ format_type=format_type,
80
+ include_visualizations=include_visualizations
81
+ )
82
+
83
+ # Функція для тестування підключення до Jira
84
+ def test_jira_connection_handler(url, username, api_token):
85
+ if not url or not username or not api_token:
86
+ return "Помилка: необхідно заповнити всі поля (URL, користувач, API токен)"
87
+
88
+ success = app.test_jira_connection(url, username, api_token)
89
+ if success:
90
+ return "✅ Успішне підключення до Jira API"
91
+ else:
92
+ return "❌ Помилка підключення до Jira. Перевірте введені дані."
93
+
94
+ # Створення інтерфейсу Gradio
95
+ with gr.Blocks(title="Jira AI Assistant") as interface:
96
+ gr.Markdown("# 🔍 Jira AI Assistant")
97
+
98
+ with gr.Tabs():
99
+ with gr.Tab("CSV Аналіз"):
100
+ with gr.Row():
101
+ with gr.Column(scale=1):
102
+ file_input = gr.File(label="Завантажити CSV файл Jira")
103
+ inactive_days = gr.Slider(minimum=1, maximum=90, value=14, step=1,
104
+ label="Кількість днів для визначення неактивних тікетів")
105
+
106
+ include_ai = gr.Checkbox(label="Включити AI аналіз", value=False)
107
+
108
+ analyze_btn = gr.Button("Аналізувати", variant="primary")
109
+
110
+ with gr.Accordion("Збереження звіту", open=False):
111
+ format_type = gr.Dropdown(
112
+ choices=["markdown", "html", "pdf"],
113
+ value="markdown",
114
+ label="Формат звіту"
115
+ )
116
+ include_visualizations = gr.Checkbox(
117
+ label="Включити візуалізації",
118
+ value=True
119
+ )
120
+ save_btn = gr.Button("Зберегти звіт")
121
+ save_output = gr.Textbox(label="Статус збереження")
122
+
123
+ with gr.Column(scale=2):
124
+ with gr.Tabs():
125
+ with gr.Tab("Звіт"):
126
+ report_output = gr.Markdown()
127
+
128
+ with gr.Tab("Візуалізації"):
129
+ with gr.Row():
130
+ status_plot = gr.Plot(label="Статуси тікетів")
131
+ priority_plot = gr.Plot(label="Пріоритети тікетів")
132
+
133
+ timeline_plot = gr.Plot(label="Часова шкала")
134
+
135
+ with gr.Tab("AI Аналіз"):
136
+ ai_output = gr.Markdown()
137
+
138
+ # Встановлюємо обробники подій
139
+ analyze_btn.click(
140
+ analyze_csv,
141
+ inputs=[file_input, inactive_days, include_ai],
142
+ outputs=[report_output, status_plot, priority_plot, timeline_plot, ai_output]
143
+ )
144
+
145
+ save_btn.click(
146
+ save_report_handler,
147
+ inputs=[report_output, format_type, include_visualizations],
148
+ outputs=[save_output]
149
+ )
150
+
151
+ with gr.Tab("Jira API"):
152
+ gr.Markdown("## Підключення до Jira API")
153
+
154
+ with gr.Row():
155
+ jira_url = gr.Textbox(
156
+ label="Jira URL",
157
+ placeholder="https://your-company.atlassian.net"
158
+ )
159
+ jira_username = gr.Textbox(
160
+ label="Ім'я користувача Jira",
161
+ placeholder="email@example.com"
162
+ )
163
+ jira_api_token = gr.Textbox(
164
+ label="Jira API Token",
165
+ type="password"
166
+ )
167
+
168
+ test_connection_btn = gr.Button("Тестувати з'єднання")
169
+ connection_status = gr.Textbox(label="Статус підключення")
170
+
171
+ test_connection_btn.click(
172
+ test_jira_connection_handler,
173
+ inputs=[jira_url, jira_username, jira_api_token],
174
+ outputs=[connection_status]
175
+ )
176
+
177
+ gr.Markdown("## ⚠️ Ця функція буде доступна у наступних версіях")
178
+
179
+ with gr.Tab("AI Асистенти"):
180
+ gr.Markdown("## AI Асистенти для Jira")
181
+ gr.Markdown("⚠️ Ця функція буде доступна у наступних версіях")
182
+
183
+ with gr.Accordion("Зразок інтерфейсу"):
184
+ question = gr.Textbox(
185
+ label="Запитання",
186
+ placeholder="Наприклад: Які тікети мають найвищий пріоритет?",
187
+ lines=2
188
+ )
189
+ answer = gr.Markdown(label="Відповідь")
190
+
191
+ with gr.Tab("Інтеграції"):
192
+ gr.Markdown("## Інтеграції з зовнішніми системами")
193
+ gr.Markdown("⚠️ Ця функція буде доступна у наступних версіях")
194
+
195
+ with gr.Accordion("Slack інтеграція"):
196
+ slack_channel = gr.Textbox(
197
+ label="Slack канал",
198
+ placeholder="#project-updates"
199
+ )
200
+ slack_message = gr.Textbox(
201
+ label="Повідомлення",
202
+ placeholder="Тижневий звіт по проекту",
203
+ lines=3
204
+ )
205
+ slack_send_btn = gr.Button("Надіслати у Slack", interactive=False)
206
+
207
+ # Запуск інтерфейсу
208
+ interface.launch()
209
+
210
+ # Можливість запустити інтерфейс самостійно (для тестування)
211
+ if __name__ == "__main__":
212
+ import sys
213
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
214
+
215
+ from app import JiraAssistantApp
216
+ app_instance = JiraAssistantApp()
217
+ launch_interface(app_instance)