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 для отладки |