diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,4 +1,3 @@ - import os import io import base64 @@ -6,13 +5,13 @@ import json import logging import threading import time +import random from datetime import datetime from uuid import uuid4 -from flask import Flask, render_template_string, request, redirect, url_for, flash, jsonify, Response +from flask import Flask, render_template_string, request, redirect, url_for, flash, jsonify, abort from PIL import Image import google.generativeai as genai -import numpy as np from huggingface_hub import HfApi, hf_hub_download from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError from werkzeug.utils import secure_filename @@ -21,2816 +20,736 @@ import requests load_dotenv() -app = Flask(__name__) +app = Flask(name) app.secret_key = 'your_unique_secret_key_gippo_312_shop_54321_no_login' -DATA_FILE = 'data.json' - -SYNC_FILES = [DATA_FILE] +ENVIRONMENTS_FILE = 'environments.json' REPO_ID = "Kgshop/metastorebase1" HF_TOKEN_WRITE = os.getenv("HF_TOKEN") HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY") -STORE_ADDRESS = "Рынок Кербент, 6 ряд , 43 контейнер " -WHATSAPP_NUMBER = "+996701202013" - CURRENCY_CODE = 'KGS' -CURRENCY_NAME = 'Кыргызский сом' - DOWNLOAD_RETRIES = 3 DOWNLOAD_DELAY = 5 logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)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.") - - 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 - - for file_name in files_to_download: - success = False - for attempt in range(retries + 1): - try: - local_path = hf_hub_download( - repo_id=REPO_ID, - filename=file_name, - repo_type="dataset", - token=token_to_use, - local_dir=".", - local_dir_use_symlinks=False, - force_download=True, - resume_download=False - ) - success = True - break - except RepositoryNotFoundError: - return False - except HfHubHTTPError as e: - if e.response.status_code == 404: - if attempt == 0 and not os.path.exists(file_name): - try: - if file_name == DATA_FILE: - with open(file_name, 'w', encoding='utf-8') as f: - json.dump({'products': [], 'categories': [], 'orders': {}, 'organization_info': {}, 'chats': {}}, f) - except Exception as create_e: - pass - success = False - break - else: - pass - except requests.exceptions.RequestException as e: - pass - except Exception as e: - pass - - if attempt < retries: - time.sleep(delay) - - if not success: - all_successful = False - - return all_successful - -def upload_db_to_hf(specific_file=None): - if not HF_TOKEN_WRITE: - return - 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: - api.upload_file( - path_or_fileobj=file_name, - path_in_repo=file_name, - repo_id=REPO_ID, - repo_type="dataset", - token=HF_TOKEN_WRITE, - commit_message=f"Sync {file_name} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" - ) - except Exception as e: - pass - else: - pass - except Exception as e: - pass - -def periodic_backup(): - backup_interval = 1800 - while True: - time.sleep(backup_interval) - 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." - } - default_data = {'products': [], 'categories': [], 'orders': {}, 'organization_info': default_organization_info, 'chats': {}} - data = default_data - try: - with open(DATA_FILE, 'r', encoding='utf-8') as file: - data = json.load(file) - if not isinstance(data, dict): - raise FileNotFoundError +def get_env_filename(env_id): +return f"data_{env_id}.json" + +def download_file_from_hf(filename, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY): +token_to_use = HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE +for attempt in range(retries + 1): +try: +hf_hub_download( +repo_id=REPO_ID, +filename=filename, +repo_type="dataset", +token=token_to_use, +local_dir=".", +local_dir_use_symlinks=False, +force_download=True, +resume_download=False +) +return True +except (RepositoryNotFoundError, HfHubHTTPError, requests.exceptions.RequestException): +if attempt < retries: +time.sleep(delay) +return False + +def upload_file_to_hf(filename): +if not HF_TOKEN_WRITE: +return +if os.path.exists(filename): +try: +api = HfApi() +api.upload_file( +path_or_fileobj=filename, +path_in_repo=filename, +repo_id=REPO_ID, +repo_type="dataset", +token=HF_TOKEN_WRITE, +commit_message=f"Sync {filename} {datetime.now()}" +) +except Exception: +pass + +def delete_file_from_hf(filename): +if not HF_TOKEN_WRITE: +return +try: +api = HfApi() +api.delete_files( +repo_id=REPO_ID, +paths_in_repo=[filename], +repo_type="dataset", +token=HF_TOKEN_WRITE, +commit_message=f"Delete {filename}" +) +except Exception: +pass + +def load_environments(): +if not os.path.exists(ENVIRONMENTS_FILE): +if not download_file_from_hf(ENVIRONMENTS_FILE): +return {} +try: +with open(ENVIRONMENTS_FILE, 'r', encoding='utf-8') as f: +return json.load(f) +except Exception: +return {} + +def save_environments(envs): +with open(ENVIRONMENTS_FILE, 'w', encoding='utf-8') as f: +json.dump(envs, f, indent=4) +upload_file_to_hf(ENVIRONMENTS_FILE) + +def load_tenant_data(env_id): +filename = get_env_filename(env_id) +default_org_info = { +"about_us": "Мы — Gippo312...", +"shipping": "Доставка по всему Кыргызстану...", +"returns": "Возврат 14 дней...", +"contact": "Свяжитесь с нами..." +} +default_data = {'products': [], 'categories': [], 'orders': {}, 'organization_info': default_org_info, 'chats': {}} + +code +Code +download +content_copy +expand_less +if not os.path.exists(filename): + if not download_file_from_hf(filename): + return default_data + +try: + with open(filename, 'r', encoding='utf-8') as file: + data = json.load(file) + if not isinstance(data, dict): return 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 + if 'organization_info' not in data: data['organization_info'] = default_org_info if 'chats' not in data: data['chats'] = {} - except FileNotFoundError: - 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 - if 'chats' not in data: data['chats'] = {} - except (FileNotFoundError, json.JSONDecodeError, Exception) as e: - 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 - if 'chats' not in data: data['chats'] = {} - except (FileNotFoundError, json.JSONDecodeError, Exception) as e: - data = default_data - else: - data = default_data - except Exception as e: - data = default_data - - 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 - 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 'chats' not in data: data['chats'] = {} - - 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: - pass + return data +except Exception: + return default_data + +def save_tenant_data(env_id, data): +filename = get_env_filename(env_id) +try: +with open(filename, 'w', encoding='utf-8') as file: +json.dump(data, file, ensure_ascii=False, indent=4) +upload_file_to_hf(filename) +except Exception: +pass def configure_gemini(): - if not GOOGLE_API_KEY: - return False - try: - genai.configure(api_key=GOOGLE_API_KEY) - return True - except Exception as e: - return False +if not GOOGLE_API_KEY: return False +try: +genai.configure(api_key=GOOGLE_API_KEY) +return True +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}" - - try: - 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 - - 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}") - -def generate_chat_response(message, chat_history_from_client): - if not configure_gemini(): - return "Извините, сервис чата временно недоступен. Пожалуйста, попробуйте позже." - - data = load_data() - products = data.get('products', []) - categories = data.get('categories', []) - organization_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." - ) - - generated_text = "" - response = None - - try: - model = genai.GenerativeModel('gemma-3-27b-it') - - 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 - - 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}" +if not configure_gemini(): raise ValueError("API error") +try: +image = Image.open(io.BytesIO(image_data)).convert('RGB') +prompt = f"Напиши рекламный пост для товара на фото на языке: {language}. Минимум 1000 символов, смайлики, хэштеги." +model = genai.GenerativeModel('gemma-2-9b-it') +response = model.generate_content([prompt, image]) +return response.text if hasattr(response, 'text') else "" +except Exception as e: +raise ValueError(str(e)) + +def generate_chat_response(message, chat_history, env_id): +if not configure_gemini(): return "Сервис недоступен." +data = load_tenant_data(env_id) +products = [p for p in data.get('products', []) if p.get('in_stock', True)] +categories = data.get('categories', []) +org_info = data.get('organization_info', {}) + +code +Code +download +content_copy +expand_less +prod_str = "\n".join([f"[ID:{p['product_id']} Name:{p['name']}] Price:{p['price']} {CURRENCY_CODE}" for p in products]) +cat_str = ", ".join(categories) + +system_prompt = ( + f"Ты помощник магазина (Environment {env_id}). " + f"Категории: {cat_str}. Товары:\n{prod_str}\n" + f"Инфо: {json.dumps(org_info, ensure_ascii=False)}. " + "Отвечай вежливо, предлагай товары из списка." +) + +try: + model = genai.GenerativeModel('gemma-2-9b-it') + gemini_hist = [{'role': 'user', 'parts': [{'text': system_prompt}]}] + for msg in chat_history: + role = 'model' if msg['role'] == 'ai' else 'user' + gemini_hist.append({'role': role, 'parts': [{'text': msg['text']}]}) + + chat = model.start_chat(history=gemini_hist) + response = chat.send_message(message) + return response.text +except Exception as e: + return f"Ошибка AI: {e}" + +MASTER_ADMIN_TEMPLATE = ''' -CATALOG_TEMPLATE = ''' - + + - - - Gippo312 - Каталог - - - - +Master Admin - Gippo312 Host + + + -
-
- -
- - -
-
+
+

Управление средами (Master Admin)

+ +code +Code +download +content_copy +expand_less +
+

Создать новую среду

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

{{ category_name }}

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

Активные среды

+ + + + + + + + + + + + {% for eid, info in envs.items() %} + + + + + + + {% endfor %} + +
ID СредыНазваниеДата созданияСсылкиДействия
{{ eid }}{{ info.name }}{{ info.created_at }} +
+ + +
+
+
+ + +''' - {% if not has_products %} -

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

- {% endif %} - -
-
- +CATALOG_TEMPLATE = ''' - + -