внедрение yandex

main
pilot639 2 days ago
parent 13825d51d6
commit 5792993ca5

@ -1,7 +1,6 @@
Django~=5.2.13 Django
requests requests
mysqlclient mysqlclient
qrcode qrcode
django-environ django-environ
pillow pillow
yandex-ai-studio-sdk

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

@ -1,99 +1,10 @@
import os # api_deepseek_ai.py
import json
from datetime import datetime
import requests import requests
from typing import Dict, Any, List 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]]] = {} _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( def deepseek_chat_structured(
@ -108,15 +19,12 @@ def deepseek_chat_structured(
""" """
Отправляет запрос к DeepSeek и возвращает структурированный ответ (JSON). Отправляет запрос к DeepSeek и возвращает структурированный ответ (JSON).
""" """
# Инициализация сессии
if session_id not in _contexts: if session_id not in _contexts:
_contexts[session_id] = [ _contexts[session_id] = [
{"role": "system", "content": system_prompt} {"role": "system", "content": system_prompt}
] ]
# Логируем системный промт при первом обращении
_log_dialogue(session_id, 'SYSTEM', system_prompt) _log_dialogue(session_id, 'SYSTEM', system_prompt)
# Логируем запрос пользователя
_log_dialogue(session_id, 'USER', user_query) _log_dialogue(session_id, 'USER', user_query)
_contexts[session_id].append({"role": "user", "content": 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) _log_dialogue(session_id, 'ASSISTANT', assistant_content)
_contexts[session_id].append({"role": "assistant", "content": assistant_content}) _contexts[session_id].append({"role": "assistant", "content": assistant_content})
# Парсим JSON return parse_ai_response(assistant_content)
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 requests.exceptions.HTTPError as e: except requests.exceptions.HTTPError as e:
# Обработка ошибки 402 Payment Required
if e.response.status_code == 402: if e.response.status_code == 402:
user_message = "⚠️ К сожалению, я сейчас не могу отвечать на эти вопросы, попробуйте позже." user_message = "⚠️ К сожалению, я сейчас не могу отвечать на эти вопросы, попробуйте позже."
_log_dialogue(session_id, 'ERROR', f"Payment Required (402): {e}") _log_dialogue(session_id, 'ERROR', f"Payment Required (402): {e}")
@ -176,7 +64,7 @@ def deepseek_chat_structured(
"status": "complete" "status": "complete"
} }
else: else:
error_msg = f"⚠️ Произошла ошибка при обращении к серверу. Попробуйте позже." error_msg = "⚠️ Произошла ошибка при обращении к серверу. Попробуйте позже."
_log_dialogue(session_id, 'ERROR', f"HTTP error: {e}") _log_dialogue(session_id, 'ERROR', f"HTTP error: {e}")
return { return {
"intent": "general", "intent": "general",
@ -184,7 +72,6 @@ def deepseek_chat_structured(
"entities": {}, "entities": {},
"status": "complete" "status": "complete"
} }
except Exception as e: except Exception as e:
error_msg = "⚠️ Извините, произошла ошибка. Попробуйте позже." error_msg = "⚠️ Извините, произошла ошибка. Попробуйте позже."
_log_dialogue(session_id, 'ERROR', f"DeepSeek error: {e}") _log_dialogue(session_id, 'ERROR', f"DeepSeek error: {e}")

@ -1,120 +1,10 @@
# import openai # api_yandex_ai.py
# from typing import Dict, Any, List
# client = openai.OpenAI( from openai import OpenAI
# api_key="AQVNxNGDP4KUYdTvjE_s2YBGugv5RFehgZKBlJRu", from api_common import _log_dialogue, get_system_prompt, parse_ai_response
# 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 # Хранилище контекстов для Yandex
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]]] = {} _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( def yandex_chat_structured(
@ -128,19 +18,8 @@ def yandex_chat_structured(
temperature: float = 0.3, temperature: float = 0.3,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
""" """
Отправляет запрос к Yandex GPT через официальный SDK и возвращает структурированный ответ (JSON). Отправляет запрос к Yandex GPT через OpenAI-совместимый API.
Аргументы:
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.01.0)
""" """
# Инициализация сессии
if session_id not in _contexts: if session_id not in _contexts:
_contexts[session_id] = [ _contexts[session_id] = [
{"role": "system", "content": system_prompt} {"role": "system", "content": system_prompt}
@ -151,16 +30,18 @@ def yandex_chat_structured(
_contexts[session_id].append({"role": "user", "content": user_query}) _contexts[session_id].append({"role": "user", "content": user_query})
try: try:
# Аутентификация через API-ключ[reference:4] client = OpenAI(
auth = APIKeyAuth(api_key) api_key=api_key,
ai_studio = AIStudio(folder_id=folder_id, auth=auth) base_url="https://ai.api.cloud.yandex.net/v1",
project=folder_id,
)
# Отправка запроса через SDK (используем OpenAI-совместимый chat)[reference:5] response = client.chat.completions.create(
response = ai_studio.chat.completions.create(
model=model, model=model,
messages=_contexts[session_id], messages=_contexts[session_id],
max_tokens=max_tokens, max_tokens=max_tokens,
temperature=temperature, temperature=temperature,
response_format={"type": "json_object"}
) )
assistant_content = response.choices[0].message.content assistant_content = response.choices[0].message.content
@ -168,29 +49,10 @@ def yandex_chat_structured(
_log_dialogue(session_id, 'ASSISTANT', assistant_content) _log_dialogue(session_id, 'ASSISTANT', assistant_content)
_contexts[session_id].append({"role": "assistant", "content": assistant_content}) _contexts[session_id].append({"role": "assistant", "content": assistant_content})
# Парсим JSON из ответа return parse_ai_response(assistant_content)
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: except Exception as e:
error_msg = "⚠️ Извините, произошла ошибка. Попробуйте позже." error_msg = "⚠️ Извините, произошла ошибка при обращении к Yandex GPT. Попробуйте позже."
_log_dialogue(session_id, 'ERROR', f"Yandex GPT error: {e}") _log_dialogue(session_id, 'ERROR', f"Yandex GPT error: {e}")
return { return {
"intent": "general", "intent": "general",

@ -3,4 +3,4 @@ from django.apps import AppConfig
class DeepseekBotConfig(AppConfig): class DeepseekBotConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField' default_auto_field = 'django.db.models.BigAutoField'
name = 'deepseek_bot' name = 'ai_agent'

@ -8,6 +8,7 @@ from restoran_max_bot.settings import DEBUG
url_max = "https://platform-api2.max.ru" url_max = "https://platform-api2.max.ru"
max_verify = False max_verify = False
def maxbot_send_text_message(chat_id: str, max_token: str, message: str): def maxbot_send_text_message(chat_id: str, max_token: str, message: str):
url = f"{url_max}/messages?chat_id={chat_id}" url = f"{url_max}/messages?chat_id={chat_id}"

@ -1,5 +1,6 @@
import re 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 * from max_bot.max_api import *
import qrcode import qrcode
from restoran_max_bot.bot_started import check_registration from restoran_max_bot.bot_started import check_registration

@ -35,7 +35,7 @@ INSTALLED_APPS = [
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'max_bot', 'max_bot',
'deepseek_bot', 'ai_agent',
] ]
MIDDLEWARE = [ MIDDLEWARE = [

Loading…
Cancel
Save