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