File size: 19,016 Bytes
e54a9ba
26471fd
f35bde9
2857a5a
 
e54a9ba
2857a5a
e54a9ba
2857a5a
 
 
a3dfced
2857a5a
 
e54a9ba
2857a5a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e54a9ba
2857a5a
 
 
 
a3dfced
2857a5a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e54a9ba
2857a5a
f35bde9
2857a5a
 
a3dfced
 
20ffed9
2857a5a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20ffed9
2857a5a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a3dfced
20ffed9
2857a5a
 
 
 
20ffed9
2857a5a
 
e54a9ba
2857a5a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e54a9ba
2857a5a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e54a9ba
2857a5a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e54a9ba
 
2857a5a
 
 
 
 
 
 
 
 
20ffed9
a3dfced
2857a5a
 
 
 
 
 
 
 
a3dfced
2857a5a
 
26471fd
2857a5a
 
 
 
 
26471fd
2857a5a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
# 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 для отладки