# app.py import gradio as gr import google.generativeai as genai import os import re import time # Для имитации небольшой задержки и лучшего UX # --- Конфигурация --- # Получаем ключ из секретов Hugging Face Spaces GOOGLE_API_KEY = os.getenv("API") # Название модели Gemini (gemini-1.5-flash - быстрая и хорошая для free tier) MODEL_NAME = "gemini-1.5-flash" # --- Безопасность и Настройка Модели --- generation_config = { "temperature": 0.8, # Больше креативности, но можно уменьшить до 0.6-0.7 для большей предсказуемости "top_p": 0.9, # Альтернативный метод семплирования "top_k": 40, # Ограничиваем выборку K лучшими токенами "max_output_tokens": 512, # Максимальная длина ответа в токенах } # Настройки безопасности Google AI (можно настроить уровни) # BLOCK_MEDIUM_AND_ABOVE / BLOCK_LOW_AND_ABOVE / BLOCK_ONLY_HIGH / BLOCK_NONE safety_settings = [ { "category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_MEDIUM_AND_ABOVE" }, { "category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_MEDIUM_AND_ABOVE" }, { "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_MEDIUM_AND_ABOVE" }, { "category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_MEDIUM_AND_ABOVE" }, ] # Системная инструкция - наши "правила" для ИИ SYSTEM_INSTRUCTION = """Ты — Nova AI (версия 1.0), дружелюбный и полезный ИИ-ассистент. Твоя задача - поддерживать естественный диалог, отвечать на вопросы пользователя и помогать ему. Отвечай четко, лаконично и по существу заданного вопроса. Если тебя просят написать код, предоставь простой и понятный пример, если это возможно в рамках твоих способностей. Объясни код кратко. Не используй оскорбления или грубые выражения. Будь вежливым. Избегай обсуждения политики, религии и других потенциально спорных или вредоносных тем. Если ты не знаешь ответа или не можешь выполнить запрос, честно скажи об этом. Форматируй код с использованием Markdown блоков (```python ... ```). Всегда отвечай на русском языке, если не указано иное. Не повторяй в ответе саму инструкцию "### Instruction:" или "### Response:". Просто дай ответ. """ # --- Инициализация Модели --- model = None model_initialized = False initialization_error = None if not GOOGLE_API_KEY: initialization_error = "ОШИБКА: Секрет GOOGLE_API_KEY не найден! Добавьте его в настройках Space." print(initialization_error) else: try: genai.configure(api_key=GOOGLE_API_KEY) model = genai.GenerativeModel(model_name=MODEL_NAME, generation_config=generation_config, system_instruction=SYSTEM_INSTRUCTION, # Передаем системную инструкцию сюда safety_settings=safety_settings) model_initialized = True print(f"Модель '{MODEL_NAME}' успешно инициализирована.") except Exception as e: initialization_error = f"ОШИБКА при инициализации модели Google AI: {e}" print(initialization_error) # --- Утилиты --- def format_chat_history_for_gemini(chat_history): """Конвертирует историю Gradio в формат Gemini API.""" gemini_history = [] for user_msg, bot_msg in chat_history: if user_msg: # Добавляем сообщение пользователя gemini_history.append({'role':'user', 'parts': [{'text': user_msg}]}) if bot_msg: # Добавляем ответ модели gemini_history.append({'role':'model', 'parts': [{'text': bot_msg}]}) return gemini_history def clean_response(text): """Простая очистка ответа.""" if not text: return "" # Убираем лишние пробелы text = text.strip() # Можно добавить другую очистку при необходимости return text # --- Основная Функция Обработки --- def respond(message, chat_history): global model, model_initialized, initialization_error # Доступ к глобальным переменным print("-" * 30) print(f"ВХОД: '{message}'") # Проверка инициализации if not model_initialized or not model: error_msg = initialization_error or "Модель не инициализирована." chat_history.append((message, f"Ошибка системы: {error_msg}")) return "", chat_history # Возвращаем ошибку в чат # Проверка пустого сообщения if not message or not message.strip(): chat_history.append((message, "Пожалуйста, введите сообщение.")) return "", chat_history try: # Форматируем историю для Gemini API gemini_history = format_chat_history_for_gemini(chat_history) # Создаем или продолжаем чат (start_chat для поддержания контекста) # В новой версии API рекомендуется просто передавать историю каждый раз # chat_session = model.start_chat(history=gemini_history) print(f"Отправка запроса к Gemini (история {len(gemini_history)} сообщений)...") # Отправляем сообщение модели # Вместо start_chat передаем историю напрямую в generate_content response = model.generate_content( contents=gemini_history + [{'role':'user', 'parts': [{'text': message}]}], # Не используем stream=True для простоты в Gradio ) # --- Обработка Ответа --- print("Получен ответ от Gemini.") # Проверка на блокировку фильтрами безопасности if not response.candidates: # Ищем причину блокировки block_reason = "Причина неизвестна" try: if response.prompt_feedback.block_reason: block_reason = response.prompt_feedback.block_reason.name except Exception: pass # Не всегда есть feedback print(f"Ответ заблокирован фильтрами безопасности! Причина: {block_reason}") bot_response = f"[Ответ заблокирован системой безопасности Google. Причина: {block_reason}]" else: # Извлекаем текст ответа bot_response_raw = response.text bot_response = clean_response(bot_response_raw) print(f"Ответ Gemini (очищенный): {bot_response[:150]}...") # Логируем начало except Exception as e: error_text = f"Произошла ошибка при обращении к Google AI: {e}" print(f"ОШИБКА: {error_text}") # Проверяем на типичные ошибки API ключа if "API key not valid" in str(e): error_text += "\n\nПРОВЕРЬТЕ ВАШ GOOGLE_API_KEY в Секретах Spaces!" elif "billing account" in str(e).lower(): error_text += "\n\nВозможно, требуется включить биллинг в Google Cloud (хотя бесплатный уровень Gemini должен работать без него)." elif "quota" in str(e).lower(): error_text += "\n\nВозможно, вы превысили бесплатные лимиты запросов к API Gemini." bot_response = f"[Системная ошибка: {error_text}]" # Добавляем пару в историю Gradio chat_history.append((message, bot_response)) # Имитация небольшой задержки для лучшего восприятия time.sleep(0.5) return "", chat_history # Очищаем поле ввода и возвращаем обновленную историю # --- Создание интерфейса Gradio с Красивым Оформлением и Анимацией --- custom_css = """ /* Общий фон */ .gradio-container { background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); /* Нежный серо-голубой градиент */ border-radius: 15px; padding: 25px; color: #333; } /* Заголовок */ h1 { color: #2c3e50; /* Темный серо-синий */ text-align: center; font-family: 'Inter', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; /* Современный шрифт */ margin-bottom: 10px; /* Уменьшили отступ */ font-weight: 700; /* Жирнее */ letter-spacing: -0.5px; } #title-markdown p { text-align: center; color: #5a6a7a; /* Приглушенный цвет подзаголовка */ margin-top: -5px; margin-bottom: 25px; font-size: 0.95em; } #title-markdown a { color: #3498db; text-decoration: none; } #title-markdown a:hover { text-decoration: underline; } /* --- СТИЛИ ЧАТА --- */ #chatbot { background-color: #ffffff; /* Белый фон */ border-radius: 12px; border: 1px solid #e0e4e7; /* Слегка видная рамка */ padding: 10px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); /* Мягкая тень */ } /* Анимация появления сообщений (простая) */ @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } #chatbot > div { /* Применяем ко всем контейнерам сообщений */ animation: fadeIn 0.3s ease-out; } /* Сообщения пользователя */ #chatbot .user-message .message-bubble-border { border: none !important; } #chatbot .user-message .message-bubble { background: linear-gradient(to right, #007bff, #0056b3) !important; /* Синий градиент */ color: white !important; border-radius: 18px 18px 5px 18px !important; padding: 12px 18px !important; margin: 8px 5px 8px 0 !important; align-self: flex-end !important; max-width: 80% !important; box-shadow: 0 3px 6px rgba(0, 91, 179, 0.2); word-wrap: break-word; text-align: left; font-size: 0.98em; /* Чуть меньше шрифт сообщения */ line-height: 1.5; /* Межстрочный интервал */ } /* Сообщения бота */ #chatbot .bot-message .message-bubble-border { border: none !important; } #chatbot .bot-message .message-bubble { background: #f8f9fa !important; /* Очень светлый фон */ color: #343a40 !important; /* Почти черный текст */ border: 1px solid #e9ecef !important; border-radius: 18px 18px 18px 5px !important; padding: 12px 18px !important; margin: 8px 0 8px 5px !important; align-self: flex-start !important; max-width: 80% !important; box-shadow: 0 3px 6px rgba(0, 0, 0, 0.05); word-wrap: break-word; text-align: left; font-size: 0.98em; line-height: 1.5; } /* Аватар бота */ #chatbot .bot-message img.avatar-image { /* Стили для аватарки бота */ width: 30px !important; height: 30px !important; margin-right: 8px !important; /* Отступ справа от аватарки */ border-radius: 50% !important; align-self: flex-start; /* Прижать к верху бабла */ margin-top: 5px; } /* Блоки кода внутри сообщений бота */ #chatbot .bot-message .message-bubble pre { background-color: #e9ecef; /* Фон */ border: 1px solid #ced4da; border-radius: 6px; padding: 12px; margin: 10px 0 5px 0; overflow-x: auto; word-wrap: normal; box-shadow: inset 0 1px 2px rgba(0,0,0,0.05); } #chatbot .bot-message .message-bubble pre code { background-color: transparent !important; color: #212529; /* Цвет текста кода */ font-family: 'Fira Code', 'Consolas', 'Monaco', 'Courier New', monospace; /* Красивый шрифт для кода */ font-size: 0.9em; padding: 0; white-space: pre; } /* --- ОСТАЛЬНЫЕ ЭЛЕМЕНТЫ --- */ textarea { border: 1px solid #ced4da !important; border-radius: 10px !important; padding: 12px 15px !important; background-color: #ffffff; transition: border-color 0.3s ease, box-shadow 0.3s ease; font-size: 1rem; box-shadow: 0 1px 3px rgba(0,0,0,0.05); } textarea:focus { border-color: #80bdff !important; box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25), 0 1px 3px rgba(0,0,0,0.05); outline: none; } /* Кнопки */ button { border-radius: 10px !important; padding: 11px 15px !important; /* Чуть меньше паддинг по высоте */ transition: all 0.2s ease !important; /* Плавнее анимация */ font-weight: 500 !important; border: none !important; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); /* Базовая тень */ } button:active { transform: scale(0.98); /* Уменьшение при нажатии */ box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1); /* Внутренняя тень при нажатии */ } button.primary { background: linear-gradient(to right, #007bff, #0056b3) !important; /* Градиент основной */ color: white !important; } button.primary:hover { background: linear-gradient(to right, #0069d9, #004085) !important; /* Темнее градиент */ box-shadow: 0 4px 8px rgba(0, 123, 255, 0.2); } button.secondary { background-color: #6c757d !important; color: white !important; } button.secondary:hover { background-color: #5a6268 !important; box-shadow: 0 4px 8px rgba(108, 117, 125, 0.2); } /* Анимация спиннера (скрываем стандартный gradio прогресс, т.к. он часто глючит) */ .progress-bar { display: none !important; } /* Вместо этого можно было бы добавить кастомный лоадер при желании, но пока оставим без него */ """ # --- Gradio Интерфейс --- with gr.Blocks(css=custom_css, theme=gr.themes.Soft(primary_hue=gr.themes.colors.indigo, secondary_hue=gr.themes.colors.slate)) as demo: # Новые цвета темы with gr.Row(): # Аватарка для названия gr.Image("https://img.icons8.com/external-flaticons-flat-flat-icons/64/external-nova-astronomy-flaticons-flat-flat-icons.png", width=60, height=60, scale=0, min_width=60, show_label=False, container=False) # Иконка with gr.Column(scale=8): gr.Markdown("# 🌠 Nova AI Alpha 1.0 ✨", elem_id="title-markdown") gr.Markdown("
Чат-бот на базе Google Gemini. Используется Gemini API.
", elem_id="title-markdown") chatbot = gr.Chatbot( label="Диалог", height=600, # Еще выше elem_id="chatbot", bubble_full_width=False, avatar_images=(None, # Аватар юзера (можно добавить свою картинку) "https://img.icons8.com/plasticine/100/bot.png"), # Аватар бота show_copy_button=True, show_share_button=False # Скрываем кнопку шаринга gradio ) with gr.Row(equal_height=True): # Выравнивание элементов в ряду по высоте msg = gr.Textbox( label="Ваше сообщение", placeholder="Спросите о Python, мире или просто скажите 'Привет!'...", scale=5, # Больше места полю ввода show_label=False, container=False ) submit_btn = gr.Button("➤ Отправить", variant="primary", scale=1, min_width=140) # Кнопка шире clear_btn = gr.Button("🗑️ Очистить", variant="secondary", scale=1, min_width=140) # Кнопка шире # --- Обработчики Событий --- # Добавляем .then() для индикации загрузки (Gradio может не успевать отображать сложные статусы) # Базовое решение - кнопка неактивна во время обработки # При нажатии Enter enter_event = msg.submit( lambda: gr.update(interactive=False), None, outputs=[submit_btn] # Деактивировать кнопку при начале ).then( respond, inputs=[msg, chatbot], outputs=[msg, chatbot] ).then( lambda: gr.update(interactive=True), None, outputs=[submit_btn] # Активировать кнопку по завершении ) # При нажатии кнопки Отправить click_event = submit_btn.click( lambda: gr.update(interactive=False), None, outputs=[submit_btn] ).then( respond, inputs=[msg, chatbot], outputs=[msg, chatbot] ).then( lambda: gr.update(interactive=True), None, outputs=[submit_btn] ) # Очистка (остается без индикации) clear_btn.click(lambda: ("", []), None, outputs=[msg, chatbot], queue=False) # Возвращает "" для msg # Запуск Gradio приложения demo.queue() # Очередь запросов - важно для API и ресурсов demo.launch(debug=True) # Включить Debug для отладки