From 13825d51d63822e680f3b2591133825133bd380e Mon Sep 17 00:00:00 2001 From: pilot <657434@03b.ru> Date: Thu, 23 Jul 2026 13:45:28 +0800 Subject: [PATCH] =?UTF-8?q?=D0=B2=D0=BD=D0=B5=D0=B4=D1=80=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D1=8F=D0=BD=D0=B4=D0=B5=D0=BA=D1=81=D0=B0=20AI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | 1 + .../{deepseek_bot => ai_agent}/__init__.py | 0 .../{deepseek_bot => ai_agent}/admin.py | 0 .../api_deepseek_ai.py} | 74 +++---- restoran_max_bot/ai_agent/api_yandex_ai.py | 200 ++++++++++++++++++ .../{deepseek_bot => ai_agent}/apps.py | 0 .../system_prompt_base.txt | 0 .../{deepseek_bot => ai_agent}/tests.py | 0 .../{deepseek_bot => ai_agent}/views.py | 0 .../restoran_max_bot/bot_message.py | 7 +- 10 files changed, 234 insertions(+), 48 deletions(-) rename restoran_max_bot/{deepseek_bot => ai_agent}/__init__.py (100%) rename restoran_max_bot/{deepseek_bot => ai_agent}/admin.py (100%) rename restoran_max_bot/{deepseek_bot/api_deepseek.py => ai_agent/api_deepseek_ai.py} (66%) create mode 100644 restoran_max_bot/ai_agent/api_yandex_ai.py rename restoran_max_bot/{deepseek_bot => ai_agent}/apps.py (100%) rename restoran_max_bot/{deepseek_bot => ai_agent}/system_prompt_base.txt (100%) rename restoran_max_bot/{deepseek_bot => ai_agent}/tests.py (100%) rename restoran_max_bot/{deepseek_bot => ai_agent}/views.py (100%) diff --git a/requirements.txt b/requirements.txt index e17e915..b389ac8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ mysqlclient qrcode django-environ pillow +yandex-ai-studio-sdk diff --git a/restoran_max_bot/deepseek_bot/__init__.py b/restoran_max_bot/ai_agent/__init__.py similarity index 100% rename from restoran_max_bot/deepseek_bot/__init__.py rename to restoran_max_bot/ai_agent/__init__.py diff --git a/restoran_max_bot/deepseek_bot/admin.py b/restoran_max_bot/ai_agent/admin.py similarity index 100% rename from restoran_max_bot/deepseek_bot/admin.py rename to restoran_max_bot/ai_agent/admin.py diff --git a/restoran_max_bot/deepseek_bot/api_deepseek.py b/restoran_max_bot/ai_agent/api_deepseek_ai.py similarity index 66% rename from restoran_max_bot/deepseek_bot/api_deepseek.py rename to restoran_max_bot/ai_agent/api_deepseek_ai.py index 67cc53a..891a31a 100644 --- a/restoran_max_bot/deepseek_bot/api_deepseek.py +++ b/restoran_max_bot/ai_agent/api_deepseek_ai.py @@ -5,63 +5,38 @@ import requests from typing import Dict, Any, List from max_bot.models import Client, ProductCategory, Product, Qr -# Хранилище контекстов (можно расширить для хранения состояния бронирования) +# Хранилище контекстов _contexts: Dict[str, List[Dict[str, str]]] = {} -# Кешируем базовый промт, чтобы не читать файл при каждом запросе _BASE_PROMPT_CACHE = None def _log_dialogue(session_id: str, role: str, content: str, log_file: str = None) -> None: """ Записывает сообщение диалога в файл лога. - Аргументы: - session_id: идентификатор сессии (например, chat_id) - role: 'SYSTEM', 'USER', 'ASSISTANT' - content: текст сообщения - log_file: путь к файлу лога (если None, берётся из настроек или по умолчанию) """ if log_file is None: - # Путь к файлу лога: можно задать в settings или использовать фиксированный путь - # Здесь используем папку logs рядом с файлом api_deepseek.py base_dir = os.path.dirname(os.path.abspath(__file__)) log_dir = os.path.join(base_dir, 'logs') os.makedirs(log_dir, exist_ok=True) log_file = os.path.join(log_dir, 'deepseek_dialogs.log') - # Формируем строку лога с временной меткой timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - # Для удобства чтения обрезаем content, если он слишком длинный? Нет, оставляем полным. log_entry = f"[{timestamp}] [{role.upper()}] [{session_id}]: {content}\n" - # Записываем в файл (атомарно, с блокировкой? Для простоты используем обычное добавление) with open(log_file, 'a', encoding='utf-8') as f: f.write(log_entry) - # Если это системное сообщение или начало сессии, добавим разделитель для наглядности - if role.upper() == 'SYSTEM': - with open(log_file, 'a', encoding='utf-8') as f: - f.write('-' * 80 + '\n') - - # Записываем в файл (атомарно, с блокировкой? Для простоты используем обычное добавление) - with open(log_file, 'a', encoding='utf-8') as f: - f.write(log_entry) - - # Если это системное сообщение или начало сессии, добавим разделитель для наглядности + # Если это системное сообщение, добавляем разделитель для наглядности if role.upper() == 'SYSTEM': with open(log_file, 'a', encoding='utf-8') as f: f.write('-' * 80 + '\n') def _load_base_prompt() -> str: - """ - Загружает базовый текст системного промта из файла. - Если файл не найден, возвращает встроенный fallback. - """ global _BASE_PROMPT_CACHE if _BASE_PROMPT_CACHE is not None: return _BASE_PROMPT_CACHE - # Определяем путь к файлу (он лежит рядом с api_deepseek.py) base_dir = os.path.dirname(os.path.abspath(__file__)) file_path = os.path.join(base_dir, 'system_prompt_base.txt') @@ -69,27 +44,18 @@ def _load_base_prompt() -> str: with open(file_path, 'r', encoding='utf-8') as f: _BASE_PROMPT_CACHE = f.read() except FileNotFoundError: - # Fallback: используем встроенный текст (минимальный) _BASE_PROMPT_CACHE = ( "Ты — ассистент ресторана. Отвечай на вопросы и помогай с бронированием. " "Возвращай JSON с полями intent, message, entities, status.\n" "Информация о ресторане и меню:\n" ) - # Также можно залогировать ошибку print("WARNING: system_prompt_base.txt not found, using fallback.") - return _BASE_PROMPT_CACHE def get_system_prompt(client: Client) -> str: - """ - Генерирует системный промпт на основе данных клиента. - Базовый текст загружается из файла, затем добавляются ресторан и меню. - """ - # 1. Загружаем базовый промт prompt = _load_base_prompt() - # 2. Добавляем информацию о ресторане qr = Qr.objects.filter(client=client).first() if qr: restaurant_info = f""" @@ -105,8 +71,7 @@ def get_system_prompt(client: Client) -> str: else: prompt += "\nИнформация о ресторане отсутствует.\n" - # 3. Добавляем меню - categories = ProductCategory.objects.filter(client=client, status=True).order_by('ord') + categories = ProductCategory.objects.filter(client=client, status=True, src=2).order_by('ord') if categories.exists(): prompt += "\nМеню:\n" for cat in categories: @@ -123,7 +88,6 @@ def get_system_prompt(client: Client) -> str: else: prompt += "\nМеню не загружено.\n" - # 4. Добавляем финальную инструкцию (если она не в файле) prompt += """ При ответах на вопросы о меню используй только те блюда, которые перечислены выше. Если клиент спрашивает о блюде, которого нет в списке, честно скажи об этом и предложи посмотреть другие позиции. @@ -144,16 +108,16 @@ def deepseek_chat_structured( """ Отправляет запрос к DeepSeek и возвращает структурированный ответ (JSON). """ - # Логируем системный промт только при создании новой сессии + # Инициализация сессии if session_id not in _contexts: _contexts[session_id] = [ {"role": "system", "content": system_prompt} ] + # Логируем системный промт при первом обращении _log_dialogue(session_id, 'SYSTEM', system_prompt) # Логируем запрос пользователя _log_dialogue(session_id, 'USER', user_query) - _contexts[session_id].append({"role": "user", "content": user_query}) url = "https://api.deepseek.com/v1/chat/completions" @@ -176,9 +140,7 @@ def deepseek_chat_structured( data = response.json() assistant_content = data["choices"][0]["message"]["content"] - # Логируем ответ ассистента _log_dialogue(session_id, 'ASSISTANT', assistant_content) - _contexts[session_id].append({"role": "assistant", "content": assistant_content}) # Парсим JSON @@ -202,13 +164,33 @@ def deepseek_chat_structured( "status": "complete" } + except requests.exceptions.HTTPError as e: + # Обработка ошибки 402 Payment Required + if e.response.status_code == 402: + user_message = "⚠️ К сожалению, я сейчас не могу отвечать на эти вопросы, попробуйте позже." + _log_dialogue(session_id, 'ERROR', f"Payment Required (402): {e}") + return { + "intent": "general", + "message": user_message, + "entities": {}, + "status": "complete" + } + else: + error_msg = f"⚠️ Произошла ошибка при обращении к серверу. Попробуйте позже." + _log_dialogue(session_id, 'ERROR', f"HTTP error: {e}") + return { + "intent": "general", + "message": error_msg, + "entities": {}, + "status": "complete" + } + except Exception as e: - error_msg = f"⚠️ Извините, произошла ошибка. Попробуйте позже." - # Логируем ошибку + error_msg = "⚠️ Извините, произошла ошибка. Попробуйте позже." _log_dialogue(session_id, 'ERROR', f"DeepSeek error: {e}") return { "intent": "general", "message": error_msg, "entities": {}, "status": "complete" - } + } \ No newline at end of file diff --git a/restoran_max_bot/ai_agent/api_yandex_ai.py b/restoran_max_bot/ai_agent/api_yandex_ai.py new file mode 100644 index 0000000..3c805e4 --- /dev/null +++ b/restoran_max_bot/ai_agent/api_yandex_ai.py @@ -0,0 +1,200 @@ +# import openai +# +# client = openai.OpenAI( +# api_key="AQVNxNGDP4KUYdTvjE_s2YBGugv5RFehgZKBlJRu", +# base_url="https://ai.api.cloud.yandex.net/v1", +# project="b1g8dec1sf09i9ohn7bl" +# ) +# +# response = client.responses.create( +# prompt={ +# "id": "fvtitb2k94c13nthvitn", +# }, +# input="some message", +# ) +# +# print(response.output_text) + +import os +import json +from datetime import datetime +from typing import Dict, Any, List, Optional + +from yandex_ai_studio_sdk import AIStudio +from yandex_ai_studio_sdk.auth import APIKeyAuth + +from max_bot.models import Client, ProductCategory, Product, Qr + +# Хранилище контекстов для диалогов +_contexts: Dict[str, List[Dict[str, str]]] = {} +_BASE_PROMPT_CACHE = None + + +def _log_dialogue(session_id: str, role: str, content: str, log_file: str = None) -> None: + """ + Записывает сообщение диалога в файл лога. + """ + if log_file is None: + base_dir = os.path.dirname(os.path.abspath(__file__)) + log_dir = os.path.join(base_dir, 'logs') + os.makedirs(log_dir, exist_ok=True) + log_file = os.path.join(log_dir, 'yandex_dialogs.log') + + timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + log_entry = f"[{timestamp}] [{role.upper()}] [{session_id}]: {content}\n" + + with open(log_file, 'a', encoding='utf-8') as f: + f.write(log_entry) + + if role.upper() == 'SYSTEM': + with open(log_file, 'a', encoding='utf-8') as f: + f.write('-' * 80 + '\n') + + +def _load_base_prompt() -> str: + """Загружает базовый текст системного промта из файла.""" + global _BASE_PROMPT_CACHE + if _BASE_PROMPT_CACHE is not None: + return _BASE_PROMPT_CACHE + + base_dir = os.path.dirname(os.path.abspath(__file__)) + file_path = os.path.join(base_dir, 'system_prompt_base.txt') + + try: + with open(file_path, 'r', encoding='utf-8') as f: + _BASE_PROMPT_CACHE = f.read() + except FileNotFoundError: + _BASE_PROMPT_CACHE = ( + "Ты — ассистент ресторана. Отвечай на вопросы и помогай с бронированием. " + "Возвращай JSON с полями intent, message, entities, status.\n" + "Информация о ресторане и меню:\n" + ) + print("WARNING: system_prompt_base.txt not found, using fallback.") + return _BASE_PROMPT_CACHE + + +def get_system_prompt(client: Client) -> str: + """Генерирует системный промпт на основе данных клиента.""" + prompt = _load_base_prompt() + + qr = Qr.objects.filter(client=client).first() + if qr: + restaurant_info = f""" + Название: {qr.title or 'Не указано'} + Бренд: {qr.brand or 'Не указан'} + Описание: {qr.description or 'Нет описания'} + Адрес: {qr.addr or 'Не указан'} + Телефон: {qr.phone or 'Не указан'} + Часы работы: {qr.worktime or 'Не указаны'} + Активность: {qr.activity or 'Не указана'} + """ + prompt += restaurant_info + else: + prompt += "\nИнформация о ресторане отсутствует.\n" + + categories = ProductCategory.objects.filter(client=client, status=True, src=2).order_by('ord') + if categories.exists(): + prompt += "\nМеню:\n" + for cat in categories: + prompt += f"\nКатегория: {cat.title}\n" + products = Product.objects.filter(client=client, up=cat.id, status=1) + if products.exists(): + for prod in products: + prompt += f" - {prod.title} — {prod.price} руб." + if prod.descr: + prompt += f" ({prod.descr})" + prompt += "\n" + else: + prompt += " (в этой категории пока нет блюд)\n" + else: + prompt += "\nМеню не загружено.\n" + + prompt += """ + При ответах на вопросы о меню используй только те блюда, которые перечислены выше. + Если клиент спрашивает о блюде, которого нет в списке, честно скажи об этом и предложи посмотреть другие позиции. + Всегда помни, что ты должен возвращать только JSON, без лишнего текста вне JSON. + """ + return prompt + + +def yandex_chat_structured( + session_id: str, + api_key: str, + folder_id: str, + system_prompt: str, + user_query: str, + model: str = "yandexgpt", + max_tokens: int = 1000, + temperature: float = 0.3, +) -> Dict[str, Any]: + """ + Отправляет запрос к Yandex GPT через официальный SDK и возвращает структурированный ответ (JSON). + + Аргументы: + session_id: идентификатор сессии (chat_id) + api_key: API-ключ Yandex Cloud (создаётся в AI Studio)[reference:2] + folder_id: ID папки в Yandex Cloud[reference:3] + system_prompt: системный промт + user_query: запрос пользователя + model: модель (yandexgpt, yandexgpt-lite и др.) + max_tokens: максимальная длина ответа + temperature: креативность (0.0–1.0) + """ + # Инициализация сессии + if session_id not in _contexts: + _contexts[session_id] = [ + {"role": "system", "content": system_prompt} + ] + _log_dialogue(session_id, 'SYSTEM', system_prompt) + + _log_dialogue(session_id, 'USER', user_query) + _contexts[session_id].append({"role": "user", "content": user_query}) + + try: + # Аутентификация через API-ключ[reference:4] + auth = APIKeyAuth(api_key) + ai_studio = AIStudio(folder_id=folder_id, auth=auth) + + # Отправка запроса через SDK (используем OpenAI-совместимый chat)[reference:5] + response = ai_studio.chat.completions.create( + model=model, + messages=_contexts[session_id], + max_tokens=max_tokens, + temperature=temperature, + ) + + assistant_content = response.choices[0].message.content + + _log_dialogue(session_id, 'ASSISTANT', assistant_content) + _contexts[session_id].append({"role": "assistant", "content": assistant_content}) + + # Парсим JSON из ответа + try: + parsed = json.loads(assistant_content) + required = ["intent", "message", "entities", "status"] + if all(k in parsed for k in required): + return parsed + else: + return { + "intent": "general", + "message": assistant_content, + "entities": {}, + "status": "complete" + } + except json.JSONDecodeError: + return { + "intent": "general", + "message": assistant_content, + "entities": {}, + "status": "complete" + } + + except Exception as e: + error_msg = "⚠️ Извините, произошла ошибка. Попробуйте позже." + _log_dialogue(session_id, 'ERROR', f"Yandex GPT error: {e}") + return { + "intent": "general", + "message": error_msg, + "entities": {}, + "status": "complete" + } \ No newline at end of file diff --git a/restoran_max_bot/deepseek_bot/apps.py b/restoran_max_bot/ai_agent/apps.py similarity index 100% rename from restoran_max_bot/deepseek_bot/apps.py rename to restoran_max_bot/ai_agent/apps.py diff --git a/restoran_max_bot/deepseek_bot/system_prompt_base.txt b/restoran_max_bot/ai_agent/system_prompt_base.txt similarity index 100% rename from restoran_max_bot/deepseek_bot/system_prompt_base.txt rename to restoran_max_bot/ai_agent/system_prompt_base.txt diff --git a/restoran_max_bot/deepseek_bot/tests.py b/restoran_max_bot/ai_agent/tests.py similarity index 100% rename from restoran_max_bot/deepseek_bot/tests.py rename to restoran_max_bot/ai_agent/tests.py diff --git a/restoran_max_bot/deepseek_bot/views.py b/restoran_max_bot/ai_agent/views.py similarity index 100% rename from restoran_max_bot/deepseek_bot/views.py rename to restoran_max_bot/ai_agent/views.py diff --git a/restoran_max_bot/restoran_max_bot/bot_message.py b/restoran_max_bot/restoran_max_bot/bot_message.py index 504aa38..ca020c4 100644 --- a/restoran_max_bot/restoran_max_bot/bot_message.py +++ b/restoran_max_bot/restoran_max_bot/bot_message.py @@ -152,11 +152,14 @@ def bot_message(client: Client, message: dict, settings_max: dict, settings_deep maxbot_send_menu_button(chat_id=chat_id, max_token=settings_max['token'], message='👇 Выберите раздел', client=client) else: - # обработка сообщения через ИИ =========================== + # =========================== обработка сообщения через ИИ =========================== if settings_deepseek.get('token', False): text = message['message']['body']['text'] # Получаем системный промт из настроек - system_prompt = settings_deepseek.get('deepseek_system_prompt', get_system_prompt(client)) + system_prompt = get_system_prompt(client) + if settings_deepseek.get('deepseek_system_prompt', False): + system_prompt = (system_prompt + "\n Дополнительная информация:\n" + + settings_deepseek.get('deepseek_system_prompt')) # Отправляем запрос в DeepSeek response = deepseek_chat_structured(