🤖 OpenAirAI-X Chat
Русскоязычный AI-ассистент
import gradio as gr from transformers import AutoModelForCausalLM, AutoTokenizer import torch import json from pathlib import Path from datetime import datetime import requests import re # === ЗАГРУЗКА МОДЕЛИ === print("🚀 Загрузка модели...") model_id = "OpenRussianAI/OpenAirAI-X" tokenizer = AutoTokenizer.from_pretrained(model_id) if tokenizer.pad_token is None: tokenizer.pad_token = tokenizer.eos_token model = AutoModelForCausalLM.from_pretrained(model_id) device = "cuda" if torch.cuda.is_available() else "cpu" model = model.to(device).eval() print(f"✅ Модель загружена на {device}") # === ХРАНИЛИЩЕ ИСТОРИИ === HISTORY_DIR = Path("chat_history") HISTORY_DIR.mkdir(exist_ok=True) def get_history_path(username): safe_name = "".join(c for c in username if c.isalnum() or c in ('-', '_')).lower() return HISTORY_DIR / f"{safe_name}.json" def load_history(username): path = get_history_path(username) if path.exists(): with open(path, "r", encoding="utf-8") as f: return json.load(f) return {} def save_history(username, history_data): path = get_history_path(username) with open(path, "w", encoding="utf-8") as f: json.dump(history_data, f, ensure_ascii=False, indent=2) def check_if_pro(token): if not token or not token.startswith("hf_"): return False try: response = requests.get( "https://huggingface.co/api/whoami-v2", headers={"Authorization": f"Bearer {token}"}, timeout=5 ) if response.status_code == 200: data = response.json() return data.get("isPro", False) or data.get("plan", {}).get("name") == "PRO" except Exception as e: print(f"Ошибка проверки PRO: {e}") return False # === ОЧИСТКА ОТ ЦИКЛОВ И СПАМА === def clean_repetitive_text(text): # Убираем явные повторы слов подряд words = text.split() if len(words) < 4: return text for i in range(len(words) - 3): if words[i] == words[i+1] == words[i+2] == words[i+3]: return " ".join(words[:i]) + "." # Если текст содержит подозрительные спам-фразы, можно их обрезать spam_triggers = ["mail@gmail.com", "O-pay", "переводить деньги"] for trigger in spam_triggers: if trigger in text: # Обрезаем текст до этого момента idx = text.find(trigger) return text[:idx].strip() + "." return text # === ГЕНЕРАЦИЯ ОТВЕТА === def generate_response(message, history, username, current_chat_id): if not username: gr.Warning("Сначала введите имя пользователя!") return history, gr.update(), current_chat_id, gr.update() if not message.strip(): return history, gr.update(), current_chat_id, gr.update() history = history + [{"role": "user", "content": message}] # ДОБАВЛЯЕМ СИСТЕМНЫЙ ПРОМПТ В НАЧАЛО КАЖДОГО ДИАЛОГА # Это заставляет модель помнить, кто она такая system_prompt = "Ты — полезный русскоязычный AI-ассистент OpenAirAI. Отвечай кратко, вежливо и по делу. Не упоминай электронную почту, платежи или сторонние организации.\n\n" prompt = system_prompt for msg in history: if msg["role"] == "user": prompt += f"Пользователь: {msg['content']}\n" elif msg["role"] == "assistant": prompt += f"AI: {msg['content']}\n" prompt += "AI:" inputs = tokenizer(prompt, return_tensors="pt").to(device) with torch.no_grad(): outputs = model.generate( **inputs, max_new_tokens=120, min_new_tokens=5, temperature=0.9, # Повышаем температуру для большей вариативности top_k=50, # Ограничиваем выборку 50 самыми вероятными токенами top_p=0.95, do_sample=True, pad_token_id=tokenizer.eos_token_id, eos_token_id=tokenizer.eos_token_id, repetition_penalty=1.3 # Штраф за повторы ) generated_ids = outputs[0][inputs.input_ids.shape[-1]:] response_text = tokenizer.decode(generated_ids, skip_special_tokens=True) # Очистка stop_words = ["Пользователь:", "User:", "\n\n"] ai_response = response_text for stop_word in stop_words: if stop_word in ai_response: ai_response = ai_response.split(stop_word)[0].strip() ai_response = clean_repetitive_text(ai_response) if not ai_response: ai_response = "Извините, я не смог сформулировать ответ." history = history + [{"role": "assistant", "content": ai_response}] if username and current_chat_id: history_data = load_history(username) if current_chat_id not in history_data: history_data[current_chat_id] = { "title": message[:40] + ("..." if len(message) > 40 else ""), "created": datetime.now().isoformat(), "messages": [] } history_data[current_chat_id]["messages"] = history save_history(username, history_data) chat_list_choices = get_chat_list(username) return history, gr.update(value=""), current_chat_id, gr.update(choices=chat_list_choices) # === УПРАВЛЕНИЕ ЧАТАМИ === def new_chat(username): if not username: return [], None, gr.update(choices=[]) chat_id = datetime.now().strftime("%Y%m%d_%H%M%S") history_data = load_history(username) history_data[chat_id] = { "title": "Новый чат", "created": datetime.now().isoformat(), "messages": [] } save_history(username, history_data) chat_list_choices = get_chat_list(username) return [], chat_id, gr.update(choices=chat_list_choices, value=None) def load_chat(chat_title, username, current_chat_id): if not username or not chat_title: return [], current_chat_id history_data = load_history(username) for cid, data in history_data.items(): if data["title"] == chat_title: return data["messages"], cid return [], current_chat_id def get_chat_list(username): if not username: return [] history_data = load_history(username) sorted_chats = sorted( history_data.items(), key=lambda x: x[1].get("created", ""), reverse=True ) return [data["title"] for _, data in sorted_chats] def delete_chat(chat_title, username): if not username or not chat_title: return [], {}, None, gr.update(choices=[]) history_data = load_history(username) for cid, data in list(history_data.items()): if data["title"] == chat_title: del history_data[cid] break save_history(username, history_data) chat_list_choices = get_chat_list(username) return [], history_data, None, gr.update(choices=chat_list_choices, value=None) # === CSS === CUSTOM_CSS = """ .main-header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 12px; margin-bottom: 20px; text-align: center; } .main-header h1 { margin: 0; font-size: 2em; } .pro-badge { background: linear-gradient(135deg, #ffd700 0%, #ffed4e 100%); color: #333; padding: 4px 12px; border-radius: 20px; font-weight: bold; display: inline-block; margin-left: 10px; box-shadow: 0 2px 8px rgba(255, 215, 0, 0.4); } .user-info { padding: 10px; background: white; border-radius: 8px; margin-bottom: 10px; text-align: center; font-weight: 600; } .privacy-banner { background: linear-gradient(135deg, #10b981 0%, #059669 100%); color: white; padding: 15px 20px; border-radius: 10px; margin: 15px 0; display: flex; align-items: center; gap: 12px; font-size: 0.95em; box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3); } .privacy-banner .icon { font-size: 1.8em; } .privacy-banner strong { display: block; font-size: 1.1em; margin-bottom: 4px; } """ PRIVACY_BANNER = """
""" # === ИНТЕРФЕЙС === with gr.Blocks(title="OpenAirAI-X Chat") as demo: username_state = gr.State("") history_state = gr.State({}) current_chat_id_state = gr.State(None) with gr.Column(visible=False) as main_interface: gr.HTML("Русскоязычный AI-ассистент
Введите данные для входа