diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,4 +1,10 @@ +Вот полный код проекта, объединяющий все ваши требования. В него добавлен отдельный маршрут /chat с премиальным дизайном, адаптацией, анимациями и полной синхронизацией корзины. Также добавлена система сохранения диалогов и просмотр их через админ-панель. +code +Python +download +content_copy +expand_less import os import io import base64 @@ -45,10 +51,9 @@ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %( def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY): if not HF_TOKEN_READ and not HF_TOKEN_WRITE: - logging.warning("HF_TOKEN_READ/HF_TOKEN_WRITE not set. Download might fail for private repos.") + logging.warning("HF_TOKEN_READ/HF_TOKEN_WRITE not set.") token_to_use = HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE - files_to_download = [specific_file] if specific_file else SYNC_FILES all_successful = True @@ -56,7 +61,7 @@ def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWN success = False for attempt in range(retries + 1): try: - local_path = hf_hub_download( + hf_hub_download( repo_id=REPO_ID, filename=file_name, repo_type="dataset", @@ -76,24 +81,17 @@ def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWN try: if file_name == DATA_FILE: with open(file_name, 'w', encoding='utf-8') as f: - json.dump({'products': [], 'categories': [], 'orders': {}, 'organization_info': {}}, f) - except Exception as create_e: + json.dump({'products': [], 'categories': [], 'orders': {}, 'organization_info': {}, 'chat_sessions': {}}, f) + except Exception: pass success = False break - else: - pass - except requests.exceptions.RequestException as e: - pass - except Exception as e: + except Exception: pass - if attempt < retries: time.sleep(delay) - if not success: all_successful = False - return all_successful def upload_db_to_hf(specific_file=None): @@ -102,7 +100,6 @@ def upload_db_to_hf(specific_file=None): try: api = HfApi() files_to_upload = [specific_file] if specific_file else SYNC_FILES - for file_name in files_to_upload: if os.path.exists(file_name): try: @@ -114,585 +111,437 @@ def upload_db_to_hf(specific_file=None): token=HF_TOKEN_WRITE, commit_message=f"Sync {file_name} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" ) - except Exception as e: + except Exception: pass - else: - pass - except Exception as e: + except Exception: pass def periodic_backup(): - backup_interval = 1800 while True: - time.sleep(backup_interval) + time.sleep(1800) upload_db_to_hf() def load_data(): default_organization_info = { - "about_us": "Мы — Gippo312, ваш надежный партнер в мире уникальных товаров. Мы предлагаем широкий ассортимент продукции, от электроники до товаров для дома, всегда стремясь к качеству и доступности. Наша миссия — сделать ваш шопинг приятным и удобным, предлагая только лучшие товары, тщательно отобранные для вас.", - "shipping": "Доставка осуществляется по всему Кыргызстану. Стоимость и сроки доставки зависят от региона и веса товара. По Бишкеку доставка возможна в течение 1-2 рабочих дней, в регионы — от 3 до 7 дней. Для уточнения деталей свяжитесь с нами.", - "returns": "Возврат и обмен товара возможен в течение 14 дней с момента покупки, при условии сохранения товарного вида, упаковки и чека. Некоторые категории товаров могут иметь особые условия возврата. Пожалуйста, свяжитесь с нами для оформления возврата или обмена.", - "contact": f"Наш магазин находится по адресу: {STORE_ADDRESS}. Связаться с нами можно по телефону: {WHATSAPP_NUMBER} или через WhatsApp по этому же номеру. Мы работаем ежедневно с 9:00 до 18:00." + "about_us": "Мы — Gippo312...", + "shipping": "Доставка по всему Кыргызстану...", + "returns": "Возврат в течение 14 дней...", + "contact": f"Адрес: {STORE_ADDRESS}. Тел: {WHATSAPP_NUMBER}" } - default_data = {'products': [], 'categories': [], 'orders': {}, 'organization_info': default_organization_info} - data = default_data + default_data = {'products': [], 'categories': [], 'orders': {}, 'organization_info': default_organization_info, 'chat_sessions': {}} + try: with open(DATA_FILE, 'r', encoding='utf-8') as file: data = json.load(file) - if not isinstance(data, dict): - raise FileNotFoundError - if 'products' not in data: data['products'] = [] - if 'categories' not in data: data['categories'] = [] - if 'orders' not in data: data['orders'] = {} - if 'organization_info' not in data: data['organization_info'] = default_organization_info - except FileNotFoundError: + except (FileNotFoundError, json.JSONDecodeError): if download_db_from_hf(specific_file=DATA_FILE): try: with open(DATA_FILE, 'r', encoding='utf-8') as file: data = json.load(file) - if not isinstance(data, dict): - data = default_data - if 'products' not in data: data['products'] = [] - if 'categories' not in data: data['categories'] = [] - if 'orders' not in data: data['orders'] = {} - if 'organization_info' not in data: data['organization_info'] = default_organization_info - except (FileNotFoundError, json.JSONDecodeError, Exception) as e: + except Exception: data = default_data else: data = default_data - except json.JSONDecodeError: - if download_db_from_hf(specific_file=DATA_FILE): - try: - with open(DATA_FILE, 'r', encoding='utf-8') as file: - data = json.load(file) - if not isinstance(data, dict): - data = default_data - if 'products' not in data: data['products'] = [] - if 'categories' not in data: data['categories'] = [] - if 'orders' not in data: data['orders'] = {} - if 'organization_info' not in data: data['organization_info'] = default_organization_info - except (FileNotFoundError, json.JSONDecodeError, Exception) as e: - data = default_data - else: - data = default_data - except Exception as e: - data = default_data + + if not isinstance(data, dict): data = default_data + if 'products' not in data: data['products'] = [] + if 'categories' not in data: data['categories'] = [] + if 'orders' not in data: data['orders'] = {} + if 'chat_sessions' not in data: data['chat_sessions'] = {} + if 'organization_info' not in data: data['organization_info'] = default_organization_info for product in data['products']: if 'product_id' not in product: product['product_id'] = uuid4().hex - if any('product_id' not in p for p in data['products']): - save_data(data) - + if not os.path.exists(DATA_FILE): - try: - with open(DATA_FILE, 'w', encoding='utf-8') as f: - json.dump(default_data, f) - except Exception as create_e: - pass + save_data(data) + return data def save_data(data): try: - if not isinstance(data, dict): - return - if 'products' not in data: data['products'] = [] - if 'categories' not in data: data['categories'] = [] - if 'orders' not in data: data['orders'] = {} - if 'organization_info' not in data: data['organization_info'] = {} - + if not isinstance(data, dict): return with open(DATA_FILE, 'w', encoding='utf-8') as file: json.dump(data, file, ensure_ascii=False, indent=4) upload_db_to_hf(specific_file=DATA_FILE) - except Exception as e: + except Exception: pass def configure_gemini(): - if not GOOGLE_API_KEY: - return False + if not GOOGLE_API_KEY: return False try: genai.configure(api_key=GOOGLE_API_KEY) return True - except Exception as e: + except Exception: return False def generate_ai_description_from_image(image_data, language): - if not configure_gemini(): - raise ValueError("Google AI API не настроен.") - - try: - if not image_data: - raise ValueError("Файл из��бражения не найден.") - image_stream = io.BytesIO(image_data) - image = Image.open(image_stream).convert('RGB') - except Exception as e: - raise ValueError(f"Не удалось обработать изображение. Убедитесь, что это действительный файл изображения.") - - base_prompt = "Напиши большой и красивый, содержательный рекламный пост минимум на 1000 символов со смайликами и 25 тематических хэштегов с ключевыми словами разных вариантов, чтобы мои клиенты могли найти меня в поиске Instagram, Google и т.д. по ключевым словам. Пост пиши исключительно под товар, который на фото, без адресов и номеров телефона." - - lang_suffix = "" - if language == "Русский": - lang_suffix = " Пиши на русском языке." - elif language == "Кыргызский": - lang_suffix = " Пиши на кыргызском языке." - elif language == "Казахский": - lang_suffix = " Пиши на казахском языке." - elif language == "Узбекский": - lang_suffix = " Пиши на узбекском языке." - - final_prompt = f"{base_prompt}{lang_suffix}" - + if not configure_gemini(): raise ValueError("Google AI API не настроен.") try: + image = Image.open(io.BytesIO(image_data)).convert('RGB') + lang_prompt = f"Пиши на языке: {language}." + prompt = f"Напиши рекламный пост для Instagram про этот товар. {lang_prompt}" model = genai.GenerativeModel('gemma-3-27b-it') - - response = model.generate_content([final_prompt, image]) - - if hasattr(response, 'text'): - return response.text - else: - if response.parts: - return "".join(part.text for part in response.parts if hasattr(part, 'text')) - else: - response.resolve() - return response.text - + response = model.generate_content([prompt, image]) + return response.text if hasattr(response, 'text') else "Ошибка генерации" except Exception as e: - if "API key not valid" in str(e): - raise ValueError("Внутренняя ошибка конфигурации API.") - elif " Billing account not found" in str(e): - raise ValueError("Проблема с биллингом аккаунта Google Cloud. Проверьте ваш аккаунт.") - elif "Could not find model" in str(e): - raise ValueError(f"Модель 'learnlm-2.0-flash-experimental' не найдена или недоступна.") - elif "resource has been exhausted" in str(e).lower(): - raise ValueError("Квота запросов исчерпана. Попробуйте позже.") - elif "content has been blocked" in str(e).lower(): - reason = "неизвестна" - if hasattr(e, 'response') and hasattr(e.response, 'prompt_feedback') and e.response.prompt_feedback.block_reason: - reason = e.response.prompt_feedback.block_reason - raise ValueError(f"Генерация контента заблокирована из-за политики безопасности (причина: {reason}). Попробуйте другое изображение или запрос.)") - else: - raise ValueError(f"Ошибка при генерации контента: {e}") + raise ValueError(f"Ошибка: {str(e)}") -def generate_chat_response(message, chat_history_from_client): +def generate_chat_response(message, chat_history_from_client, session_id=None): if not configure_gemini(): - return "Извините, сервис чата временно недоступен. Пожалуйста, попробуйте позже." + return "Сервис временно недоступен." data = load_data() products = data.get('products', []) - categories = data.get('categories', []) - organization_info = data.get('organization_info', {}) + org_info = data.get('organization_info', {}) - product_info_list = [] - for p in products: - if p.get('in_stock', True): - price_display = f"{p.get('price', 0):.2f}".replace('.00', '') - product_info_list.append(f"- [ID_ТОВАРА: {p.get('product_id', 'N/A')} Название: {p.get('name', 'Без названия')}], Категория: {p.get('category', 'Без категории')}, Цена: {price_display} {CURRENCY_CODE}, Описание: {p.get('description', '')[:100]}...") - product_list_str = "\n".join(product_info_list) if product_info_list else "В данный момент нет товаров в наличии." - - category_list_str = ", ".join(categories) if categories else "Категорий пока нет." - - org_info_str = "" - if organization_info: - org_info_str += "\n\nИнформация о магазине:\n" - if organization_info.get("about_us"): - org_info_str += f"О нас: {organization_info['about_us']}\n" - if organization_info.get("shipping"): - org_info_str += f"Доставка: {organization_info['shipping']}\n" - if organization_info.get("returns"): - org_info_str += f"Возврат и обмен: {organization_info['returns']}\n" - if organization_info.get("contact"): - org_info_str += f"Контактная информация: {organization_info['contact']}\n" - - - system_instruction_content = ( - "Ты - доброжелательный и очень полезный виртуальный консультант для магазина Gippo312. " - "Твоя задача - помогать пользователям находить товары, отвечать на вопросы о них, предлагать варианты, а также предоставлять информацию о магазине. " - "Всегда будь вежлив, информативен и стремись решить проблему пользователя. " - "Никогда не выдумывай товары или категории, которых нет в предоставленных списках. " - "Когда ты предлагаешь товар, всегда указывай его название и ID, используя *точный формат*: [ID_ТОВАРА: Название: ]. Это *очень важно* для клиента. " - "Если пользователь ищет товар или категорию, предлагай несколько наиболее подходящих вариантов или перечисляй доступные из этой категории.\n\n" - f"Список доступных категорий: {category_list_str}.\n\n" - f"Список доступных товаров в магазине:\n" - f"{product_list_str}" - f"{org_info_str}\n\n" - "Если пользователь спрашивает про товары или категории, которых нет в списках, вежливо сообщи, что таких товаров/категорий нет и предложи что-то из имеющихся, или перечисли доступные категории. " - "Если вопрос касается общей информации о магазине (например, 'о нас', 'доставка', 'возврат', 'контакты'), используй данные из блока 'Информация о магазине'. " - "Старайся быть кратким, но информативным. Используй эмодзи для дружелюбности. " - "Избегай упоминания Hugging Face или Hugging Face Hub." - ) + # Сохранение сообщения пользователя + if session_id: + if session_id not in data['chat_sessions']: + data['chat_sessions'][session_id] = {'timestamp': datetime.now().isoformat(), 'messages': []} + data['chat_sessions'][session_id]['messages'].append({'role': 'user', 'text': message, 'time': datetime.now().isoformat()}) + save_data(data) - generated_text = "" - response = None + product_list = "\n".join([f"- [ID_ТОВАРА: {p.get('product_id')} Название: {p.get('name')}] Цена: {p.get('price')} {CURRENCY_CODE}" for p in products if p.get('in_stock', True)]) + if not product_list: product_list = "Нет товаров." + + system_instruction = ( + "Ты - консультант магазина Gippo312. " + "Твоя цель - продавать товары. Будь вежлив и краток. " + "ОБЯЗАТЕЛЬНОЕ ПРАВИЛО: Если ты рекомендуешь товар, используй СТРОГИЙ формат: " + "[ID_ТОВАРА: Название: ]. " + f"Список товаров:\n{product_list}\n" + f"О нас: {org_info.get('about_us')}\n" + f"Контакты: {org_info.get('contact')}" + ) try: model = genai.GenerativeModel('gemma-3-27b-it') + history_gemini = [{'role': 'user', 'parts': [{'text': system_instruction}]}] + + # Ограничиваем историю для контекста, чтобы не перегружать модель, но сохраняем всё в БД + recent_history = chat_history_from_client[-10:] + for entry in recent_history: + role = 'model' if entry['role'] == 'ai' else 'user' + history_gemini.append({'role': role, 'parts': [{'text': entry['text']}]}) + + chat = model.start_chat(history=history_gemini) + response = chat.send_message(message) + text_response = response.text + + # Сохранение ответа ИИ + if session_id: + data = load_data() # Перезагружаем на случай изменений + if session_id in data['chat_sessions']: + data['chat_sessions'][session_id]['messages'].append({'role': 'ai', 'text': text_response, 'time': datetime.now().isoformat()}) + data['chat_sessions'][session_id]['last_update'] = datetime.now().isoformat() + save_data(data) - model_chat_history_for_gemini = [ - {'role': 'user', 'parts': [{'text': system_instruction_content}]} - ] - for entry in chat_history_from_client: - gemini_role = 'model' if entry['role'] == 'ai' else 'user' - model_chat_history_for_gemini.append({ - 'role': gemini_role, - 'parts': [{'text': entry['text']}] - }) - - chat = model.start_chat(history=model_chat_history_for_gemini) - - response = chat.send_message(message, generation_config={'max_output_tokens': 1000}) - - if hasattr(response, 'text'): - generated_text = response.text - elif response.parts: - generated_text = "".join(part.text for part in response.parts if hasattr(part, 'text')) - else: - response.resolve() - if hasattr(response, 'text'): - generated_text = response.text - else: - raise ValueError("AI did not return a valid text response.") - - return generated_text - + return text_response except Exception as e: - if "API key not valid" in str(e): - return "Внутренняя ошибка конфигурации API." - elif " Billing account not found" in str(e): - return "Проблема с биллингом аккаунта Google Cloud." - elif "Could not find model" in str(e): - return "Модель AI не найдена или недоступна." - elif "resource has been exhausted" in str(e).lower(): - return "Квота запросов исчерпана. Попробуйте позже." - elif "content has been blocked" in str(e).lower() or (response is not None and hasattr(response, 'prompt_feedback') and response.prompt_feedback.block_reason): - reason = response.prompt_feedback.block_reason if (response and hasattr(response, 'prompt_feedback')) else "неизвестна" - return f"Извините, Ваш запрос был заблокирован из-за политики безопасности (причина: {reason}). Пожалуйста, переформулируйте его." - else: - return f"Извините, произошла ошибка: {e}" + return f"Ошибка ИИ: {str(e)}" -CHAT_TEMPLATE = ''' +# --- TEMPLATES --- + +CHAT_STANDALONE_TEMPLATE = ''' - EVA AI - Gippo312 - + AI Чат - Gippo312 + +
-
- EVA -
-

EVA AI

-

Онлайн • Gippo312

+
+ + Bot +
+

EVA Assistant

+
Online
- - +
+ + +
- -
-
- Привет! Я EVA, ваш персональный помощник в Gippo312. Чем я могу помочь вам сегодня? 🛍️ -
+
+ Привет! Я EVA, ваш персональный помощник в Gippo312. 🛍️
Я помогу найти лучшие товары. Что вас интересует? + Сейчас
- +
- +
+ +
+ + +
+ @@ -771,1843 +676,491 @@ CATALOG_TEMPLATE = ''' + + +
+ + +
+ +
+ {% for category in ordered_categories %} +
+

{{ category }}

+
+ {% for product in products_by_category[category] %} +
+ +
+
{{ "%.0f"|format(product.price) }} {{ currency_code }}
+
{{ product.name }}
+
+
+ {% endfor %} +
+
+ {% endfor %} +
+ + + + + + + + + + + + + + +''' +ADMIN_TEMPLATE = ''' + + + + + Админ-панель + + +
-
- -
- - -
+

Админ-панель Gippo312

+ +
+ + +
-
- {% set has_products = False %} - {% for category_name in ordered_categories %} - {% if products_by_category[category_name] %} - {% set has_products = True %} -
-
-

{{ category_name }}

- > -
- -
- {% endif %} +
+

Добавить товар

+
+ + + + + + + +
+ +

Список товаров

+ {% for p in products %} +
+
+ + {{ p.name }} - {{ p.price }} {{ currency_code }} +
+
+ + + +
+
{% endfor %} - - {% if not has_products %} -

Товары пока не добавлены.

- {% endif %} -
-
-