Spaces:
Runtime error
Runtime error
import os | |
import gradio as gr | |
from datetime import datetime | |
import pandas as pd | |
import numpy as np | |
import matplotlib.pyplot as plt | |
import seaborn as sns | |
from pathlib import Path | |
import tempfile | |
import traceback | |
import logging | |
# Налаштування логування | |
logging.basicConfig( | |
level=logging.INFO, | |
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', | |
handlers=[ | |
logging.FileHandler("jira_assistant.log"), | |
logging.StreamHandler() | |
] | |
) | |
logger = logging.getLogger("jira_assistant") | |
# Створення необхідних директорій | |
for directory in ["data", "reports", "temp", "logs"]: | |
Path(directory).mkdir(exist_ok=True, parents=True) | |
# Клас для аналізу даних Jira | |
class JiraAnalyzer: | |
def __init__(self): | |
self.df = None | |
self.stats = None | |
self.inactive_issues = None | |
def load_csv(self, file_path): | |
"""Завантаження даних з CSV-файлу""" | |
try: | |
logger.info(f"Завантаження CSV-файлу: {file_path}") | |
# Спробуємо різні кодування | |
try: | |
self.df = pd.read_csv(file_path, encoding='utf-8') | |
except UnicodeDecodeError: | |
logger.warning("Помилка декодування UTF-8, спроба з latin1") | |
self.df = pd.read_csv(file_path, encoding='latin1') | |
logger.info(f"Успішно завантажено {len(self.df)} записів") | |
return self.df | |
except Exception as e: | |
logger.error(f"Помилка при завантаженні CSV-файлу: {e}") | |
raise | |
def process_dates(self): | |
"""Обробка дат у DataFrame""" | |
date_columns = ['Created', 'Updated', 'Resolved', 'Due Date'] | |
for col in date_columns: | |
if col in self.df.columns: | |
try: | |
self.df[col] = pd.to_datetime(self.df[col], errors='coerce') | |
except Exception as e: | |
logger.warning(f"Не вдалося конвертувати колонку {col} до datetime: {e}") | |
def prepare_data(self): | |
"""Підготовка даних для аналізу""" | |
# Обробка дат | |
self.process_dates() | |
# Додаткова обробка | |
if 'Status' in self.df.columns: | |
self.df['Status'] = self.df['Status'].fillna('Unknown') | |
if 'Priority' in self.df.columns: | |
self.df['Priority'] = self.df['Priority'].fillna('Not set') | |
# Створення додаткових колонок | |
if 'Created' in self.df.columns and pd.api.types.is_datetime64_dtype(self.df['Created']): | |
self.df['Created_Date'] = self.df['Created'].dt.date | |
self.df['Created_Month'] = self.df['Created'].dt.to_period('M') | |
if 'Updated' in self.df.columns and pd.api.types.is_datetime64_dtype(self.df['Updated']): | |
self.df['Updated_Date'] = self.df['Updated'].dt.date | |
self.df['Days_Since_Update'] = (datetime.now() - self.df['Updated']).dt.days | |
def generate_stats(self): | |
"""Генерація базової статистики""" | |
self.stats = { | |
'total_tickets': len(self.df), | |
'status_counts': {}, | |
'type_counts': {}, | |
'priority_counts': {} | |
} | |
# Статистика за статусами | |
if 'Status' in self.df.columns: | |
self.stats['status_counts'] = self.df['Status'].value_counts().to_dict() | |
# Статистика за типами | |
if 'Issue Type' in self.df.columns: | |
self.stats['type_counts'] = self.df['Issue Type'].value_counts().to_dict() | |
# Статистика за пріоритетами | |
if 'Priority' in self.df.columns: | |
self.stats['priority_counts'] = self.df['Priority'].value_counts().to_dict() | |
return self.stats | |
def find_inactive_issues(self, days=14): | |
"""Аналіз неактивних тікетів""" | |
self.inactive_issues = { | |
'total_count': 0, | |
'percentage': 0, | |
'by_status': {}, | |
'top_inactive': [] | |
} | |
if 'Updated' not in self.df.columns or not pd.api.types.is_datetime64_dtype(self.df['Updated']): | |
return self.inactive_issues | |
# Визначення неактивних тікетів | |
cutoff_date = datetime.now() - pd.Timedelta(days=days) | |
inactive = self.df[self.df['Updated'] < cutoff_date] | |
if len(inactive) == 0: | |
return self.inactive_issues | |
self.inactive_issues['total_count'] = len(inactive) | |
self.inactive_issues['percentage'] = round(len(inactive) / len(self.df) * 100, 2) if len(self.df) > 0 else 0 | |
# Розподіл за статусами | |
if 'Status' in inactive.columns: | |
self.inactive_issues['by_status'] = inactive['Status'].value_counts().to_dict() | |
# Топ 5 неактивних тікетів | |
top_inactive = [] | |
for _, row in inactive.sort_values('Updated', ascending=True).head(5).iterrows(): | |
issue_data = { | |
'key': row.get('Issue key', 'Невідомо'), | |
'summary': row.get('Summary', 'Невідомо'), | |
'status': row.get('Status', 'Невідомо'), | |
'days_inactive': (datetime.now() - row['Updated']).days if pd.notna(row['Updated']) else 'Невідомо' | |
} | |
top_inactive.append(issue_data) | |
self.inactive_issues['top_inactive'] = top_inactive | |
return self.inactive_issues | |
def plot_status_counts(self): | |
"""Діаграма розподілу за статусами""" | |
if 'Status' not in self.df.columns: | |
return None | |
status_counts = self.df['Status'].value_counts() | |
fig, ax = plt.subplots(figsize=(10, 6)) | |
bars = sns.barplot(x=status_counts.index, y=status_counts.values, ax=ax) | |
# Додаємо підписи значень над стовпцями | |
for i, v in enumerate(status_counts.values): | |
ax.text(i, v + 0.5, str(v), ha='center') | |
ax.set_title('Розподіл тікетів за статусами') | |
ax.set_xlabel('Статус') | |
ax.set_ylabel('Кількість') | |
plt.xticks(rotation=45) | |
plt.tight_layout() | |
return fig | |
def plot_priority_counts(self): | |
"""Діаграма розподілу за пріоритетами""" | |
if 'Priority' not in self.df.columns: | |
return None | |
priority_counts = self.df['Priority'].value_counts() | |
fig, ax = plt.subplots(figsize=(10, 6)) | |
bars = sns.barplot(x=priority_counts.index, y=priority_counts.values, ax=ax) | |
# Додаємо підписи значень над стовпцями | |
for i, v in enumerate(priority_counts.values): | |
ax.text(i, v + 0.5, str(v), ha='center') | |
ax.set_title('Розподіл тікетів за пріоритетами') | |
ax.set_xlabel('Пріоритет') | |
ax.set_ylabel('Кількість') | |
plt.xticks(rotation=45) | |
plt.tight_layout() | |
return fig | |
def plot_created_timeline(self): | |
"""Часова діаграма створення тікетів""" | |
if 'Created' not in self.df.columns or not pd.api.types.is_datetime64_dtype(self.df['Created']): | |
return None | |
# Додаємо колонку з датою створення (без часу) | |
if 'Created_Date' not in self.df.columns: | |
self.df['Created_Date'] = self.df['Created'].dt.date | |
# Кількість створених тікетів за датами | |
created_by_date = self.df['Created_Date'].value_counts().sort_index() | |
# Створення діаграми | |
fig, ax = plt.subplots(figsize=(12, 6)) | |
created_by_date.plot(kind='line', marker='o', ax=ax) | |
ax.set_title('Кількість створених тікетів за датами') | |
ax.set_xlabel('Дата') | |
ax.set_ylabel('Кількість') | |
ax.grid(True) | |
plt.tight_layout() | |
return fig | |
def generate_report(self, inactive_days=14): | |
"""Генерація звіту""" | |
report = [] | |
# Заголовок звіту | |
report.append("# Звіт аналізу Jira") | |
report.append(f"*Створено: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*") | |
# Загальна статистика | |
report.append("\n## Загальна статистика") | |
report.append(f"**Загальна кількість тікетів:** {len(self.df)}") | |
# Статистика за статусами | |
if 'Status' in self.df.columns: | |
status_counts = self.df['Status'].value_counts() | |
report.append("\n### Статуси тікетів") | |
for status, count in status_counts.items(): | |
percentage = count / len(self.df) * 100 if len(self.df) > 0 else 0 | |
report.append(f"- **{status}:** {count} ({percentage:.1f}%)") | |
# Статистика за типами | |
if 'Issue Type' in self.df.columns: | |
type_counts = self.df['Issue Type'].value_counts() | |
report.append("\n### Типи тікетів") | |
for type_name, count in type_counts.items(): | |
percentage = count / len(self.df) * 100 if len(self.df) > 0 else 0 | |
report.append(f"- **{type_name}:** {count} ({percentage:.1f}%)") | |
# Статистика за пріоритетами | |
if 'Priority' in self.df.columns: | |
priority_counts = self.df['Priority'].value_counts() | |
report.append("\n### Пріоритети тікетів") | |
for priority, count in priority_counts.items(): | |
percentage = count / len(self.df) * 100 if len(self.df) > 0 else 0 | |
report.append(f"- **{priority}:** {count} ({percentage:.1f}%)") | |
# Неактивні тікети | |
self.find_inactive_issues(inactive_days) | |
if self.inactive_issues['total_count'] > 0: | |
report.append(f"\n## Неактивні тікети (>{inactive_days} днів)") | |
report.append(f"**Загальна кількість неактивних тікетів:** {self.inactive_issues['total_count']} ({self.inactive_issues['percentage']}%)") | |
if self.inactive_issues['by_status']: | |
report.append("\n**Неактивні тікети за статусами:**") | |
for status, count in self.inactive_issues['by_status'].items(): | |
report.append(f"- **{status}:** {count}") | |
if self.inactive_issues['top_inactive']: | |
report.append("\n**Топ 5 найбільш неактивних тікетів:**") | |
for i, ticket in enumerate(self.inactive_issues['top_inactive']): | |
report.append(f"{i+1}. **{ticket['key']}:** {ticket['summary']}") | |
report.append(f" - Статус: {ticket['status']}") | |
report.append(f" - Днів неактивності: {ticket['days_inactive']}") | |
return "\n".join(report) | |
# Виправлена функція analyze_csv | |
def analyze_csv(file_obj, inactive_days, include_ai): | |
if file_obj is None: | |
return "Помилка: файл не вибрано", None, None, None, None | |
try: | |
logger.info(f"Отримано файл: {file_obj.name}, тип: {type(file_obj)}") | |
# Створення тимчасового файлу | |
temp_file_path = os.path.join("temp", f"temp_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv") | |
# У Gradio 5.19.0 об'єкт файлу має різну структуру | |
# file_obj може бути шляхом до файлу або містити атрибут 'name' | |
if hasattr(file_obj, 'name'): | |
source_path = file_obj.name | |
# Копіювання файлу | |
import shutil | |
shutil.copy2(source_path, temp_file_path) | |
else: | |
# Якщо це не шлях до файлу, ймовірно це вже самі дані | |
with open(temp_file_path, "w", encoding="utf-8") as f: | |
f.write(str(file_obj)) | |
# Аналіз даних | |
analyzer = JiraAnalyzer() | |
analyzer.load_csv(temp_file_path) | |
analyzer.prepare_data() | |
analyzer.generate_stats() | |
# Візуалізації | |
status_fig = analyzer.plot_status_counts() | |
priority_fig = analyzer.plot_priority_counts() | |
timeline_fig = analyzer.plot_created_timeline() | |
# Генерація звіту | |
report = analyzer.generate_report(inactive_days=inactive_days) | |
# AI аналіз (заглушка) | |
ai_analysis = None | |
if include_ai: | |
ai_analysis = "AI аналіз буде доступний у наступних версіях додатку." | |
# Видалення тимчасового файлу | |
try: | |
os.remove(temp_file_path) | |
except: | |
pass | |
return report, status_fig, priority_fig, timeline_fig, ai_analysis | |
except Exception as e: | |
error_msg = f"Помилка аналізу: {str(e)}\n\n{traceback.format_exc()}" | |
logger.error(error_msg) | |
return error_msg, None, None, None, None | |
# Функція для збереження звіту | |
def save_report(report_text, format_type, include_visualizations, status_fig, priority_fig, timeline_fig): | |
if not report_text: | |
return "Помилка: спочатку виконайте аналіз даних" | |
try: | |
# Створення імені файлу | |
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') | |
report_filename = f"jira_report_{timestamp}" | |
reports_dir = Path("reports") | |
if format_type == "markdown": | |
filepath = reports_dir / f"{report_filename}.md" | |
with open(filepath, 'w', encoding='utf-8') as f: | |
f.write(report_text) | |
if include_visualizations: | |
# Збереження візуалізацій | |
charts_dir = reports_dir / f"{report_filename}_charts" | |
charts_dir.mkdir(exist_ok=True) | |
if status_fig: | |
status_fig.savefig(charts_dir / "status_counts.png") | |
if priority_fig: | |
priority_fig.savefig(charts_dir / "priority_counts.png") | |
if timeline_fig: | |
timeline_fig.savefig(charts_dir / "timeline.png") | |
elif format_type == "html": | |
from markdown import markdown | |
filepath = reports_dir / f"{report_filename}.html" | |
# Конвертація Markdown в HTML | |
html_content = f"""<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="utf-8"> | |
<title>Звіт аналізу Jira</title> | |
<style> | |
body {{ font-family: Arial, sans-serif; line-height: 1.6; margin: 20px; }} | |
h1, h2, h3 {{ color: #0052CC; }} | |
table {{ border-collapse: collapse; width: 100%; margin-bottom: 20px; }} | |
th, td {{ padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }} | |
th {{ background-color: #0052CC; color: white; }} | |
img {{ max-width: 100%; margin: 20px 0; }} | |
</style> | |
</head> | |
<body> | |
{markdown(report_text)} | |
""" | |
if include_visualizations and (status_fig or priority_fig or timeline_fig): | |
# Збереження візуалізацій | |
charts_dir = reports_dir / f"{report_filename}_charts" | |
charts_dir.mkdir(exist_ok=True) | |
html_content += "<h2>Візуалізації</h2>" | |
if status_fig: | |
status_path = charts_dir / "status_counts.png" | |
status_fig.savefig(status_path) | |
html_content += f'<div><h3>Статуси тікетів</h3><img src="{status_path.relative_to(reports_dir)}" alt="Статуси тікетів"></div>' | |
if priority_fig: | |
priority_path = charts_dir / "priority_counts.png" | |
priority_fig.savefig(priority_path) | |
html_content += f'<div><h3>Пріоритети тікетів</h3><img src="{priority_path.relative_to(reports_dir)}" alt="Пріоритети тікетів"></div>' | |
if timeline_fig: | |
timeline_path = charts_dir / "timeline.png" | |
timeline_fig.savefig(timeline_path) | |
html_content += f'<div><h3>Часова шкала</h3><img src="{timeline_path.relative_to(reports_dir)}" alt="Часова шкала"></div>' | |
html_content += "</body></html>" | |
with open(filepath, 'w', encoding='utf-8') as f: | |
f.write(html_content) | |
elif format_type == "pdf": | |
try: | |
from weasyprint import HTML | |
filepath = reports_dir / f"{report_filename}.pdf" | |
# Створюємо тимчасовий HTML-файл | |
temp_html_path = reports_dir / f"{report_filename}_temp.html" | |
# Конвертація Markdown в HTML | |
from markdown import markdown | |
html_content = f"""<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="utf-8"> | |
<title>Звіт аналізу Jira</title> | |
<style> | |
body {{ font-family: Arial, sans-serif; line-height: 1.6; margin: 20px; }} | |
h1, h2, h3 {{ color: #0052CC; }} | |
table {{ border-collapse: collapse; width: 100%; margin-bottom: 20px; }} | |
th, td {{ padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }} | |
th {{ background-color: #0052CC; color: white; }} | |
img {{ max-width: 100%; margin: 20px 0; }} | |
</style> | |
</head> | |
<body> | |
{markdown(report_text)} | |
""" | |
if include_visualizations and (status_fig or priority_fig or timeline_fig): | |
# Збереження візуалізацій | |
charts_dir = reports_dir / f"{report_filename}_charts" | |
charts_dir.mkdir(exist_ok=True) | |
html_content += "<h2>Візуалізації</h2>" | |
if status_fig: | |
status_path = charts_dir / "status_counts.png" | |
status_fig.savefig(status_path) | |
html_content += f'<div><h3>Статуси тікетів</h3><img src="{status_path}" alt="Статуси тікетів"></div>' | |
if priority_fig: | |
priority_path = charts_dir / "priority_counts.png" | |
priority_fig.savefig(priority_path) | |
html_content += f'<div><h3>Пріоритети тікетів</h3><img src="{priority_path}" alt="Пріоритети тікетів"></div>' | |
if timeline_fig: | |
timeline_path = charts_dir / "timeline.png" | |
timeline_fig.savefig(timeline_path) | |
html_content += f'<div><h3>Часова шкала</h3><img src="{timeline_path}" alt="Часова шкала"></div>' | |
html_content += "</body></html>" | |
with open(temp_html_path, 'w', encoding='utf-8') as f: | |
f.write(html_content) | |
# Конвертація HTML в PDF | |
HTML(filename=str(temp_html_path)).write_pdf(filepath) | |
# Видалення тимчасового HTML-файлу | |
try: | |
os.remove(temp_html_path) | |
except: | |
pass | |
except ImportError: | |
return "Помилка: для генерації PDF потрібна бібліотека weasyprint" | |
return f"Звіт успішно збережено: {filepath}" | |
except Exception as e: | |
error_msg = f"Помилка при збереженні звіту: {str(e)}\n\n{traceback.format_exc()}" | |
logger.error(error_msg) | |
return error_msg | |
# Інтерфейс Gradio | |
with gr.Blocks(title="Jira AI Assistant") as app: | |
gr.Markdown("# 🔍 Jira AI Assistant") | |
with gr.Tabs(): | |
with gr.Tab("CSV Аналіз"): | |
with gr.Row(): | |
with gr.Column(scale=1): | |
file_input = gr.File(label="Завантажити CSV файл Jira") | |
inactive_days = gr.Slider(minimum=1, maximum=90, value=14, step=1, | |
label="Кількість днів для визначення неактивних тікетів") | |
include_ai = gr.Checkbox(label="Включити AI аналіз", value=False) | |
analyze_btn = gr.Button("Аналізувати", variant="primary") | |
with gr.Accordion("Збереження звіту", open=False): | |
format_type = gr.Dropdown( | |
choices=["markdown", "html", "pdf"], | |
value="markdown", | |
label="Формат звіту" | |
) | |
include_visualizations = gr.Checkbox( | |
label="Включити візуалізації", | |
value=True | |
) | |
save_btn = gr.Button("Зберегти звіт") | |
save_output = gr.Textbox(label="Статус збереження") | |
with gr.Column(scale=2): | |
with gr.Tabs(): | |
with gr.Tab("Звіт"): | |
report_output = gr.Markdown() | |
with gr.Tab("Візуалізації"): | |
with gr.Row(): | |
status_plot = gr.Plot(label="Статуси тікетів") | |
priority_plot = gr.Plot(label="Пріоритети тікетів") | |
timeline_plot = gr.Plot(label="Часова шкала") | |
with gr.Tab("AI Аналіз"): | |
ai_output = gr.Markdown() | |
# Встановлюємо обробники подій | |
analyze_btn.click( | |
analyze_csv, | |
inputs=[file_input, inactive_days, include_ai], | |
outputs=[report_output, status_plot, priority_plot, timeline_plot, ai_output] | |
) | |
save_btn.click( | |
save_report, | |
inputs=[report_output, format_type, include_visualizations, status_plot, priority_plot, timeline_plot], | |
outputs=[save_output] | |
) | |
with gr.Tab("Jira API"): | |
gr.Markdown("## Підключення до Jira API") | |
with gr.Row(): | |
jira_url = gr.Textbox( | |
label="Jira URL", | |
placeholder="https://your-company.atlassian.net" | |
) | |
jira_username = gr.Textbox( | |
label="Ім'я користувача Jira", | |
placeholder="email@example.com" | |
) | |
jira_api_token = gr.Textbox( | |
label="Jira API Token", | |
type="password" | |
) | |
test_connection_btn = gr.Button("Тестувати з'єднання") | |
connection_status = gr.Textbox(label="Статус підключення") | |
gr.Markdown("## ⚠️ Ця функція буде доступна у наступних версіях") | |
with gr.Tab("AI Асистенти"): | |
gr.Markdown("## AI Асистенти для Jira") | |
gr.Markdown("⚠️ Ця функція буде доступна у наступних версіях") | |
with gr.Accordion("Зразок інтерфейсу"): | |
question = gr.Textbox( | |
label="Запитання", | |
placeholder="Наприклад: Які тікети мають найвищий пріоритет?", | |
lines=2 | |
) | |
answer = gr.Markdown(label="Відповідь") | |
with gr.Tab("Інтеграції"): | |
gr.Markdown("## Інтеграції з зовнішніми системами") | |
gr.Markdown("⚠️ Ця функція буде доступна у наступних версіях") | |
with gr.Accordion("Slack інтеграція"): | |
slack_channel = gr.Textbox( | |
label="Slack канал", | |
placeholder="#project-updates" | |
) | |
slack_message = gr.Textbox( | |
label="Повідомлення", | |
placeholder="Тижневий звіт по проекту", | |
lines=3 | |
) | |
slack_send_btn = gr.Button("Надіслати у Slack", interactive=False) | |
# Запуск додатку | |
if __name__ == "__main__": | |
app.launch() |