From 5792993ca5a53d6216be3a1cb210bf2bc4260fd7 Mon Sep 17 00:00:00 2001 From: pilot639 <60057546+pilot639@users.noreply.github.com> Date: Sat, 25 Jul 2026 01:24:07 +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=20yandex?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | 3 +- restoran_max_bot/ai_agent/api_common.py | 122 +++++++++++++ restoran_max_bot/ai_agent/api_deepseek_ai.py | 123 +------------ restoran_max_bot/ai_agent/api_yandex_ai.py | 168 ++---------------- restoran_max_bot/ai_agent/apps.py | 2 +- restoran_max_bot/max_bot/max_api.py | 1 + .../restoran_max_bot/bot_message.py | 3 +- restoran_max_bot/restoran_max_bot/settings.py | 2 +- 8 files changed, 148 insertions(+), 276 deletions(-) create mode 100644 restoran_max_bot/ai_agent/api_common.py diff --git a/requirements.txt b/requirements.txt index b389ac8..9380374 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,6 @@ -Django~=5.2.13 +Django requests mysqlclient qrcode django-environ pillow -yandex-ai-studio-sdk diff --git a/restoran_max_bot/ai_agent/api_common.py b/restoran_max_bot/ai_agent/api_common.py new file mode 100644 index 0000000..3c92509 --- /dev/null +++ b/restoran_max_bot/ai_agent/api_common.py @@ -0,0 +1,122 @@ +# api_common.py +import os +import json +from datetime import datetime +from typing import Dict, Any, List +from max_bot.models import Client, ProductCategory, Product, Qr + +# Кеш для базового промта +_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, 'ai_dialogs.log') # общий лог для всех AI + + 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 parse_ai_response(assistant_content: str) -> Dict[str, Any]: + """ + Универсальный парсер для ответа AI (ожидается JSON с полями intent, message, entities, status). + Возвращает словарь с гарантированными ключами. + """ + 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" + } \ No newline at end of file diff --git a/restoran_max_bot/ai_agent/api_deepseek_ai.py b/restoran_max_bot/ai_agent/api_deepseek_ai.py index 891a31a..3c7d1bb 100644 --- a/restoran_max_bot/ai_agent/api_deepseek_ai.py +++ b/restoran_max_bot/ai_agent/api_deepseek_ai.py @@ -1,99 +1,10 @@ -import os -import json -from datetime import datetime +# api_deepseek_ai.py import requests from typing import Dict, Any, List -from max_bot.models import Client, ProductCategory, Product, Qr +from api_common import _log_dialogue, get_system_prompt, parse_ai_response -# Хранилище контекстов +# Хранилище контекстов для DeepSeek _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, 'deepseek_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 deepseek_chat_structured( @@ -108,15 +19,12 @@ 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}) @@ -143,29 +51,9 @@ def deepseek_chat_structured( _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" - } + return parse_ai_response(assistant_content) 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}") @@ -176,7 +64,7 @@ def deepseek_chat_structured( "status": "complete" } else: - error_msg = f"⚠️ Произошла ошибка при обращении к серверу. Попробуйте позже." + error_msg = "⚠️ Произошла ошибка при обращении к серверу. Попробуйте позже." _log_dialogue(session_id, 'ERROR', f"HTTP error: {e}") return { "intent": "general", @@ -184,7 +72,6 @@ def deepseek_chat_structured( "entities": {}, "status": "complete" } - except Exception as e: error_msg = "⚠️ Извините, произошла ошибка. Попробуйте позже." _log_dialogue(session_id, 'ERROR', f"DeepSeek error: {e}") diff --git a/restoran_max_bot/ai_agent/api_yandex_ai.py b/restoran_max_bot/ai_agent/api_yandex_ai.py index 3c805e4..692794d 100644 --- a/restoran_max_bot/ai_agent/api_yandex_ai.py +++ b/restoran_max_bot/ai_agent/api_yandex_ai.py @@ -1,120 +1,10 @@ -# 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) +# api_yandex_ai.py +from typing import Dict, Any, List +from openai import OpenAI +from api_common import _log_dialogue, get_system_prompt, parse_ai_response -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 - -# Хранилище контекстов для диалогов +# Хранилище контекстов для Yandex _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( @@ -128,19 +18,8 @@ def yandex_chat_structured( 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) + Отправляет запрос к Yandex GPT через OpenAI-совместимый API. """ - # Инициализация сессии if session_id not in _contexts: _contexts[session_id] = [ {"role": "system", "content": system_prompt} @@ -151,16 +30,18 @@ def yandex_chat_structured( _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) + client = OpenAI( + api_key=api_key, + base_url="https://ai.api.cloud.yandex.net/v1", + project=folder_id, + ) - # Отправка запроса через SDK (используем OpenAI-совместимый chat)[reference:5] - response = ai_studio.chat.completions.create( + response = client.chat.completions.create( model=model, messages=_contexts[session_id], max_tokens=max_tokens, temperature=temperature, + response_format={"type": "json_object"} ) assistant_content = response.choices[0].message.content @@ -168,29 +49,10 @@ def yandex_chat_structured( _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" - } + return parse_ai_response(assistant_content) except Exception as e: - error_msg = "⚠️ Извините, произошла ошибка. Попробуйте позже." + error_msg = "⚠️ Извините, произошла ошибка при обращении к Yandex GPT. Попробуйте позже." _log_dialogue(session_id, 'ERROR', f"Yandex GPT error: {e}") return { "intent": "general", diff --git a/restoran_max_bot/ai_agent/apps.py b/restoran_max_bot/ai_agent/apps.py index 9dd6504..64bc7ac 100644 --- a/restoran_max_bot/ai_agent/apps.py +++ b/restoran_max_bot/ai_agent/apps.py @@ -3,4 +3,4 @@ from django.apps import AppConfig class DeepseekBotConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' - name = 'deepseek_bot' + name = 'ai_agent' diff --git a/restoran_max_bot/max_bot/max_api.py b/restoran_max_bot/max_bot/max_api.py index af59063..ef6a3ae 100644 --- a/restoran_max_bot/max_bot/max_api.py +++ b/restoran_max_bot/max_bot/max_api.py @@ -8,6 +8,7 @@ from restoran_max_bot.settings import DEBUG url_max = "https://platform-api2.max.ru" max_verify = False + def maxbot_send_text_message(chat_id: str, max_token: str, message: str): url = f"{url_max}/messages?chat_id={chat_id}" diff --git a/restoran_max_bot/restoran_max_bot/bot_message.py b/restoran_max_bot/restoran_max_bot/bot_message.py index ca020c4..3236128 100644 --- a/restoran_max_bot/restoran_max_bot/bot_message.py +++ b/restoran_max_bot/restoran_max_bot/bot_message.py @@ -1,5 +1,6 @@ import re -from deepseek_bot.api_deepseek import * + +from ai_agent.api_deepseek_ai import get_system_prompt, deepseek_chat_structured from max_bot.max_api import * import qrcode from restoran_max_bot.bot_started import check_registration diff --git a/restoran_max_bot/restoran_max_bot/settings.py b/restoran_max_bot/restoran_max_bot/settings.py index 5ff9dcc..cfd6dcb 100644 --- a/restoran_max_bot/restoran_max_bot/settings.py +++ b/restoran_max_bot/restoran_max_bot/settings.py @@ -35,7 +35,7 @@ INSTALLED_APPS = [ 'django.contrib.messages', 'django.contrib.staticfiles', 'max_bot', - 'deepseek_bot', + 'ai_agent', ] MIDDLEWARE = [