|
|
|
|
@ -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"
|
|
|
|
|
}
|
|
|
|
|
}
|