import gradio as gr import time import threading import os from .analyzer import NPAAnalyzer # Кольори сайту НАЗК NAZK_BLUE = "#0056b3" NAZK_LIGHT_BLUE = "#006ec7" NAZK_YELLOW = "#ffd700" NAZK_WHITE = "#ffffff" NAZK_GRAY = "#f5f7f9" NAZK_DARK_TEXT = "#333333" def create_interface(): try: # Ініціалізація аналізатора з OpenRouter за замовчуванням analyzer = NPAAnalyzer(provider_name="openrouter") # Отримання доступних провайдерів та моделей available_providers = analyzer.get_available_providers() available_models = analyzer.get_available_models() or ["Немає доступних моделей"] # Перевірка наявності API ключів has_anthropic_key = bool(os.getenv('ANTHROPIC_API_KEY')) has_openrouter_key = bool(os.getenv('OPENROUTER_API_KEY')) api_keys_warning = "" if not has_anthropic_key and not has_openrouter_key: api_keys_warning = "⚠️ Жоден з API ключів (ANTHROPIC_API_KEY, OPENROUTER_API_KEY) не встановлено. Аналіз буде недоступний." if not has_openrouter_key and analyzer.get_provider_name() == "OpenRouter": # Використовуємо Anthropic, якщо OpenRouter не доступний if has_anthropic_key: analyzer.change_provider("anthropic") print("Ключ OpenRouter відсутній, використовується Anthropic") else: api_keys_warning = "⚠️ Відсутній ключ OpenRouter, а Anthropic недоступний. Аналіз буде недоступний." # Функція для оновлення доступних моделей при зміні провайдера def update_models(provider_name): try: analyzer.change_provider(provider_name) new_models = analyzer.get_available_models() or ["Немає доступних моделей"] supports_thinking = analyzer.supports_thinking() # Оновлення елементів інтерфейсу return gr.Dropdown(choices=new_models, value=new_models[0]), gr.update(interactive=supports_thinking), gr.update(interactive=supports_thinking) except Exception as e: return gr.Dropdown(choices=["Помилка при завантаженні моделей"], value="Помилка при завантаженні моделей"), gr.update(interactive=False), gr.update(interactive=False) # Функція для оновлення моделі def update_model(model_name): try: analyzer.change_model(model_name) return f"Модель змінено на: {model_name}" except Exception as e: return f"Помилка при зміні моделі: {str(e)}" # Функція для форматування результату в Markdown def analyze_and_format(npa_text, selected_provider, selected_model, enable_thinking, thinking_budget, progress=gr.Progress()): if not npa_text.strip(): return "### Помилка\nПоле для тексту не може бути порожнім. Введіть текст для аналізу.", "", "Перевірте введені дані" if api_keys_warning: return f"### Помилка\n{api_keys_warning}", "", "Помилка конфігурації API ключів" # Переконуємось, що провайдер та модель встановлені правильно try: if analyzer.get_provider_name() != selected_provider: analyzer.change_provider(selected_provider) current_models = analyzer.get_available_models() or [] if selected_model in current_models: analyzer.change_model(selected_model) elif "Немає доступних моделей" in selected_model or "Помилка при завантаженні моделей" in selected_model: return "### Помилка\nНемає доступних моделей для вибраного провайдера. Перевірте налаштування API ключів.", "", "Помилка конфігурації моделей" except Exception as e: return f"### Помилка при налаштуванні провайдера/моделі\n{str(e)}", "", f"Помилка: {str(e)}" # Перевірка та конвертація бюджету токенів try: thinking_budget = int(thinking_budget) if thinking_budget < 1024: thinking_budget = 1024 # Мінімальний бюджет elif thinking_budget > 8000: thinking_budget = 8000 # Рекомендований максимум except (ValueError, TypeError): thinking_budget = 2000 # Значення за замовчуванням start_time = time.time() # Початкові кроки прогресу progress(0, desc="Ініціалізація аналізу...") time.sleep(0.3) progress(0.05, desc="Підготовка тексту НПА...") time.sleep(0.3) progress(0.1, desc=f"Підготовка запиту до {selected_provider}...") time.sleep(0.3) progress(0.15, desc=f"Відправка запиту до {selected_provider}...") # Змінні для контролю фонового потоку is_analyzing = True current_progress = 0.15 def update_progress_bar(): nonlocal current_progress while is_analyzing and current_progress < 0.95: # Поступово збільшуємо значення прогресу time.sleep(0.5) # Оновлюємо кожні 0.5 секунди # Використовуємо логарифмічну прогресію для повільнішого наростання в кінці step = max(0.005, (0.95 - current_progress) / 20) current_progress += step # Оновлюємо опис базуючись на поточному прогресі desc = "Аналіз тексту..." if current_progress > 0.7: desc = "Формування відповіді..." elif current_progress > 0.4: desc = "Опрацювання результатів..." progress(current_progress, desc=desc) # Запускаємо фоновий потік для оновлення прогресу progress_thread = threading.Thread(target=update_progress_bar) progress_thread.daemon = True progress_thread.start() # Аналіз з поточними налаштуваннями try: supports_thinking = analyzer.supports_thinking() result = analyzer.analyze_npa( npa_text, enable_thinking=(enable_thinking and supports_thinking), thinking_budget=thinking_budget ) except Exception as e: # Зупиняємо фоновий потік оновлення прогресу is_analyzing = False progress_thread.join(timeout=1.0) return f"### Помилка при аналізі\n```\n{str(e)}\n```\n\nПеревірте текст та налаштування або спробуйте ще раз.", "", f"Помилка аналізу: {str(e)}" # Зупиняємо фоновий потік оновлення прогресу is_analyzing = False progress_thread.join(timeout=1.0) progress(0.95, desc="Форматування результатів...") end_time = time.time() elapsed_time = end_time - start_time # Форматування основної відповіді formatted_response = f"## Результат аналізу:\n{result['response']}\n\n_Час аналізу: {elapsed_time:.2f} секунд._" # Форматування роздумів моделі - показуємо автоматично якщо режим роздумів увімкнено thinking_output = "" if enable_thinking and supports_thinking: if result['thinking']: thinking_output = f"## Хід роздумів моделі\n```\n{result['thinking']}\n```" else: thinking_output = "## Роздуми моделі недоступні\nРоздуми моделі відсутні або виникла помилка при їх отриманні." else: thinking_output = f"## Роздуми моделі недоступні\n{'Режим роздумів вимкнено.' if supports_thinking else f'Провайдер {selected_provider} не підтримує режим роздумів.'}" # Додавання інформації про налаштування та токени settings_info = f"\n\n_Провайдер: {selected_provider}, Модель: {selected_model}, " if supports_thinking: settings_info += f"режим роздумів {'увімкнено' if enable_thinking else 'вимкнено'}" if enable_thinking: settings_info += f", бюджет токенів: {thinking_budget}_" else: settings_info += "_" else: settings_info += "_" formatted_response += settings_info progress(1.0, desc="Аналіз завершено") status = "Аналіз завершено успішно" return formatted_response, thinking_output, status # Налаштування користувацького CSS для стилізації інтерфейсу custom_css = """ :root { --nazk-blue: #0056b3; --nazk-light-blue: #006ec7; --nazk-hover-blue: #0066cc; --nazk-yellow: #ffd700; --nazk-white: #ffffff; --nazk-gray: #f5f7f9; --nazk-dark-gray: #e0e0e0; --nazk-dark-text: #333333; --nazk-border-radius: 6px; --nazk-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); --nazk-transition: all 0.2s ease-in-out; } body, .gradio-container { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background-color: var(--nazk-white) !important; color: var(--nazk-dark-text); } h1, h2, h3 { color: var(--nazk-blue) !important; margin-top: 1.5rem; margin-bottom: 1rem; } .custom-header { background-color: var(--nazk-blue) !important; color: var(--nazk-white) !important; padding: 15px; margin-bottom: 25px; text-align: center; border-radius: var(--nazk-border-radius); box-shadow: var(--nazk-box-shadow); } .custom-footer { color: var(--nazk-dark-text) !important; background-color: var(--nazk-gray) !important; padding: 15px; margin-top: 30px; text-align: center; border-radius: var(--nazk-border-radius); border-top: 1px solid var(--nazk-dark-gray); } /* Tabs styling */ .tabs { background-color: var(--nazk-white) !important; border-radius: var(--nazk-border-radius); overflow: hidden; margin-top: 20px; } .tab-nav { background-color: var(--nazk-gray) !important; border-radius: var(--nazk-border-radius) var(--nazk-border-radius) 0 0; } .tab-selected { background-color: var(--nazk-blue) !important; color: var(--nazk-white) !important; border-radius: var(--nazk-border-radius) var(--nazk-border-radius) 0 0; font-weight: 500; } .tab-unselected { background-color: var(--nazk-gray) !important; transition: var(--nazk-transition); } .tab-unselected:hover { background-color: var(--nazk-dark-gray) !important; } /* Button styling */ button, .primary-button { background-color: var(--nazk-blue) !important; color: var(--nazk-white) !important; border-radius: var(--nazk-border-radius) !important; border: none !important; padding: 10px 16px !important; font-weight: 500 !important; transition: var(--nazk-transition) !important; box-shadow: var(--nazk-box-shadow) !important; } button:hover, .primary-button:hover { background-color: var(--nazk-hover-blue) !important; transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15) !important; } .secondary-button { border: 1px solid var(--nazk-blue) !important; color: var(--nazk-blue) !important; background-color: transparent !important; border-radius: var(--nazk-border-radius) !important; padding: 9px 15px !important; transition: var(--nazk-transition) !important; } .secondary-button:hover { background-color: rgba(0, 86, 179, 0.05) !important; border-color: var(--nazk-hover-blue) !important; } /* Input fields */ input, textarea, select { border: 1px solid var(--nazk-dark-gray) !important; border-radius: var(--nazk-border-radius) !important; padding: 8px 12px !important; transition: var(--nazk-transition) !important; } input:focus, textarea:focus, select:focus { border-color: var(--nazk-blue) !important; box-shadow: 0 0 0 2px rgba(0, 86, 179, 0.2) !important; outline: none !important; } /* Checkbox and slider */ input[type="range"] { accent-color: var(--nazk-blue) !important; } input[type="range"]::-webkit-slider-thumb { background: var(--nazk-blue) !important; border-radius: 50% !important; cursor: pointer !important; transition: var(--nazk-transition) !important; } input[type="range"]::-webkit-slider-thumb:hover { transform: scale(1.1) !important; } input[type="checkbox"] { accent-color: var(--nazk-blue) !important; width: 18px !important; height: 18px !important; border-radius: 3px !important; } input[type="checkbox"]:checked { background-color: var(--nazk-blue) !important; border-color: var(--nazk-blue) !important; } .label-wrap { display: flex !important; align-items: center !important; gap: 8px !important; } /* Links */ a { color: var(--nazk-blue) !important; text-decoration: none !important; transition: var(--nazk-transition) !important; } a:hover { color: var(--nazk-hover-blue) !important; text-decoration: underline !important; } /* Progress bar */ .progress-bar { background-color: var(--nazk-blue) !important; height: 6px !important; border-radius: 3px !important; } .progress-container { background-color: var(--nazk-gray) !important; border-radius: 3px !important; } /* Accordion */ .accordion { border: 1px solid var(--nazk-dark-gray) !important; border-radius: var(--nazk-border-radius) !important; margin: 15px 0 !important; overflow: hidden !important; } .accordion-header { background-color: var(--nazk-gray) !important; padding: 12px 15px !important; cursor: pointer !important; transition: var(--nazk-transition) !important; } .accordion-header:hover { background-color: var(--nazk-dark-gray) !important; } .accordion-content { padding: 15px !important; background-color: var(--nazk-white) !important; } /* Output markdown */ .output-markdown { background-color: var(--nazk-white) !important; border-radius: var(--nazk-border-radius) !important; padding: 20px !important; box-shadow: var(--nazk-box-shadow) !important; border: 1px solid var(--nazk-dark-gray) !important; } .output-markdown code { background-color: var(--nazk-gray) !important; padding: 2px 5px !important; border-radius: 3px !important; font-family: monospace !important; } .output-markdown pre { background-color: var(--nazk-gray) !important; padding: 15px !important; border-radius: var(--nazk-border-radius) !important; overflow-x: auto !important; margin: 15px 0 !important; border: 1px solid var(--nazk-dark-gray) !important; } .output-markdown h2, .output-markdown h3 { border-bottom: 1px solid var(--nazk-dark-gray) !important; padding-bottom: 8px !important; margin-top: 25px !important; } .output-markdown blockquote { border-left: 4px solid var(--nazk-blue) !important; padding-left: 15px !important; margin-left: 0 !important; color: #555 !important; } /* Warning text */ .warning-text { color: #d63031 !important; font-weight: 500 !important; } """ with gr.Blocks(css=custom_css) as iface: # Хедер із логотипом gr.HTML(f"""

AI Асистент для НАЗК

""") # Відображення попередження про відсутність API ключів, якщо вони відсутні if api_keys_warning: gr.Markdown(f"""
{api_keys_warning}
""") gr.Markdown(""" ## Антикорупційна експертиза НПА Асистент допомагає визначати корупціогенні фактори в проектах НПА відповідно до [офіційної методології](https://nazk.gov.ua/uk/antykoruptsijna-ekspertyza/). ### Інструкція: 1. Введіть текст проекту нормативно-правового акту в поле нижче. 2. Виберіть провайдера та модель для аналізу. 3. За бажанням, увімкніть режим "роздумів" моделі (якщо підтримується). 4. Натисніть кнопку "Аналіз" для запуску аналізу. 5. Ознайомтесь із висновком. 6. Перегляньте процес "роздумів" моделі на відповідній вкладці (якщо увімкнено режим роздумів). """) with gr.Row(): input_box = gr.Textbox( lines=12, label="Введіть текст проекту НПА", placeholder="Вставте текст проекту нормативно-правового акту для аналізу...", elem_classes="input-textbox" ) with gr.Row(): with gr.Column(scale=1, min_width=200): provider_dropdown = gr.Dropdown( choices=available_providers, value=analyzer.get_provider_name(), label="Провайдер", info="Виберіть провайдера для аналізу", elem_classes="provider-dropdown" ) with gr.Column(scale=1, min_width=200): model_dropdown = gr.Dropdown( choices=available_models, value=available_models[0] if available_models else None, label="Модель", info="Виберіть модель для аналізу", elem_classes="model-dropdown" ) with gr.Row(): with gr.Column(scale=2, min_width=200): analyze_button = gr.Button("Аналіз", variant="primary", elem_classes="analyze-button") with gr.Column(scale=1, min_width=200): with gr.Row(): enable_thinking_checkbox = gr.Checkbox( label="Увімкнути режим роздумів", value=False, info="Дозволяє моделі виконувати покрокові роздуми перед відповіддю та показує їх у відповідній вкладці", interactive=analyzer.supports_thinking(), elem_classes="thinking-checkbox" ) with gr.Column(scale=1, min_width=200): thinking_budget_slider = gr.Slider( minimum=1024, maximum=8000, value=2000, step=500, label="Бюджет токенів для роздумів", info="Рекомендований діапазон: 1024-8000", interactive=False, # Буде активовано при увімкненні режиму роздумів elem_classes="budget-slider" ) with gr.Row(): status_box = gr.Textbox( label="Статус", interactive=False, value="Готовий до аналізу", elem_classes="status-box" ) # Вкладки для відображення результатів with gr.Tabs(elem_classes="result-tabs") as tabs: with gr.TabItem("Висновок", id="conclusion_tab"): output_box = gr.Markdown(elem_classes="output-markdown") with gr.TabItem("Роздуми моделі", id="thinking_tab"): thinking_box = gr.Markdown(elem_classes="output-markdown") # Оновлення моделей при зміні провайдера provider_dropdown.change( update_models, inputs=[provider_dropdown], outputs=[model_dropdown, enable_thinking_checkbox, thinking_budget_slider] ) # Оновлення моделі model_dropdown.change( update_model, inputs=[model_dropdown], outputs=[status_box] ) # Логіка кнопки аналізу analyze_button.click( analyze_and_format, inputs=[input_box, provider_dropdown, model_dropdown, enable_thinking_checkbox, thinking_budget_slider], outputs=[output_box, thinking_box, status_box] ) # Логіка чекбоксу "Увімкнути режим роздумів" enable_thinking_checkbox.change( lambda x: gr.update(interactive=x), inputs=[enable_thinking_checkbox], outputs=[thinking_budget_slider] ) # Інформація про можливі тривалі запити with gr.Accordion("Інформація про тривалі запити", open=False, elem_classes="info-accordion"): gr.Markdown(""" **Увага**: Аналіз великих документів може тривати кілька хвилин, особливо при використанні режиму роздумів з високим бюджетом токенів. Система використовує потокову передачу даних (streaming) для зменшення ймовірності помилок тайм-ауту при тривалих запитах. Якщо виникають помилки з таймаутами: - Зменшіть бюджет токенів режиму роздумів - Скоротіть текст документа або розділіть його на частини - Вимкніть режим роздумів для пришвидшення аналізу - Спробуйте інший провайдер або модель """) # Інформація про API ключі with gr.Accordion("Налаштування API ключів", open=False, elem_classes="api-accordion"): gr.Markdown(f""" ### Статус API ключів - OpenRouter API Key: {'✅ Встановлено' if has_openrouter_key else '❌ Відсутній'} - Anthropic API Key: {'✅ Встановлено' if has_anthropic_key else '❌ Відсутній'} Для роботи з різними провайдерами необхідно встановити відповідні API ключі через змінні середовища: ```bash # Для OpenRouter export OPENROUTER_API_KEY=your-api-key # Для Anthropic export ANTHROPIC_API_KEY=your-api-key ``` """) return iface except Exception as e: print(f"Помилка при створенні інтерфейсу: {str(e)}") # Повертаємо простий інтерфейс з описом помилки with gr.Blocks() as error_iface: gr.Markdown(f""" # Помилка при ініціалізації інтерфейсу ``` {str(e)} ``` Перевірте журнал помилок додатку та переконайтесь, що: - Усі необхідні залежності встановлено - API ключі налаштовано правильно - Модуль analyzer.py працює коректно. """) return error_iface