Spaces:
Runtime error
Runtime error
| # 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("<p>Чат-бот на базе Google Gemini. <a href='https://aistudio.google.com/' target='_blank'>Используется Gemini API</a>.</p>", 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 для отладки |