Chat / app.py
HeavensHackDev's picture
Update app.py
2857a5a verified
raw
history blame contribute delete
19 kB
# 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 для отладки