внедрение яндекс

main
pilot 6 hours ago
parent 5b7ad6da89
commit 45b235f159

@ -3,7 +3,7 @@ import os
import json import json
import re import re
from datetime import datetime from datetime import datetime
from typing import Dict, Any, List, Optional from typing import Dict, Any, Optional
from django.core.cache import cache from django.core.cache import cache
from max_bot.models import Client, ProductCategory, Product, Qr, Partner, Integration from max_bot.models import Client, ProductCategory, Product, Qr, Partner, Integration
from restoran_max_bot import settings from restoran_max_bot import settings
@ -30,6 +30,7 @@ AI_AGENT_REGISTRY = {
def _log_dialogue(session_id: str, role: str, content: str, log_file: str = None) -> None: def _log_dialogue(session_id: str, role: str, content: str, log_file: str = None) -> None:
"""Логирует сообщение, если роль не SYSTEM или включено логирование SYSTEM."""
if role.upper() == 'SYSTEM' and not getattr(settings, 'LOG_SYSTEM_PROMPT', False): if role.upper() == 'SYSTEM' and not getattr(settings, 'LOG_SYSTEM_PROMPT', False):
return return
@ -51,6 +52,7 @@ def _log_dialogue(session_id: str, role: str, content: str, log_file: str = None
def _load_base_prompt() -> str: def _load_base_prompt() -> str:
"""Загружает базовый текст системного промта из файла."""
global _BASE_PROMPT_CACHE global _BASE_PROMPT_CACHE
if _BASE_PROMPT_CACHE is not None: if _BASE_PROMPT_CACHE is not None:
return _BASE_PROMPT_CACHE return _BASE_PROMPT_CACHE
@ -71,56 +73,65 @@ def _load_base_prompt() -> str:
return _BASE_PROMPT_CACHE return _BASE_PROMPT_CACHE
def get_system_prompt(client: Client) -> str: def get_system_prompt(client: Client, name: str = None) -> str:
cache_key = f"system_prompt_{client.id}" """
cached = cache.get(cache_key) Генерирует системный промпт с учётом имени клиента.
if cached: Базовая часть кешируется отдельно.
return cached """
# Кешируем базовую часть (без имени)
prompt = _load_base_prompt() cache_key = f"system_prompt_base_{client.pk}"
base_prompt = cache.get(cache_key)
qr = Qr.objects.filter(client=client).first() if not base_prompt:
if qr: base_prompt = _load_base_prompt()
restaurant_info = f"""
Название: {qr.title or 'Не указано'} qr = Qr.objects.filter(client=client).first()
Бренд: {qr.brand or 'Не указан'} if qr:
Описание: {qr.description or 'Нет описания'} restaurant_info = f"""
Адрес: {qr.addr or 'Не указан'} Название: {qr.title or 'Не указано'}
Телефон: {qr.phone or 'Не указан'} Бренд: {qr.brand or 'Не указан'}
Часы работы: {qr.worktime or 'Не указаны'} Описание: {qr.description or 'Нет описания'}
Активность: {qr.activity or 'Не указана'} Адрес: {qr.addr or 'Не указан'}
Телефон: {qr.phone or 'Не указан'}
Часы работы: {qr.worktime or 'Не указаны'}
Активность: {qr.activity or 'Не указана'}
"""
base_prompt += restaurant_info
else:
base_prompt += "\nИнформация о ресторане отсутствует.\n"
categories = ProductCategory.objects.filter(client=client, status=True, src=2).order_by('ord')
if categories.exists():
base_prompt += "\nМеню:\n"
for cat in categories:
base_prompt += f"\nКатегория: {cat.title}\n"
products = Product.objects.filter(client=client, up=cat.id, status=1)
if products.exists():
for prod in products:
base_prompt += f" - {prod.title}{prod.price} руб."
if prod.descr:
base_prompt += f" ({prod.descr})"
base_prompt += "\n"
else:
base_prompt += " (в этой категории пока нет блюд)\n"
else:
base_prompt += "\nМеню не загружено.\n"
base_prompt += """
При ответах на вопросы о меню используй только те блюда, которые перечислены выше.
Если клиент спрашивает о блюде, которого нет в списке, честно скажи об этом и предложи посмотреть другие позиции.
Всегда помни, что ты должен возвращать только JSON, без лишнего текста вне JSON.
""" """
prompt += restaurant_info cache.set(cache_key, base_prompt, 300) # 5 минут
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 += """ # Добавляем имя, если оно валидно
При ответах на вопросы о меню используй только те блюда, которые перечислены выше. if name and is_valid_name(name):
Если клиент спрашивает о блюде, которого нет в списке, честно скажи об этом и предложи посмотреть другие позиции. return f"Обращайся к клиенту по имени: {name}. Используй обращение в каждом ответе.\n\n{base_prompt}"
Всегда помни, что ты должен возвращать только JSON, без лишнего текста вне JSON. else:
""" return base_prompt
cache.set(cache_key, prompt, 300)
return prompt
def parse_ai_response(assistant_content: str) -> Dict[str, Any]: def parse_ai_response(assistant_content: str) -> Dict[str, Any]:
"""Универсальный парсер для ответа AI (JSON)."""
content = re.sub(r'```(?:json)?\s*', '', assistant_content) content = re.sub(r'```(?:json)?\s*', '', assistant_content)
content = re.sub(r'\s*```', '', content) content = re.sub(r'\s*```', '', content)
content = content.strip() content = content.strip()
@ -147,6 +158,7 @@ def parse_ai_response(assistant_content: str) -> Dict[str, Any]:
def get_ai_agent(client: Client) -> Dict[str, Optional[str]]: def get_ai_agent(client: Client) -> Dict[str, Optional[str]]:
"""Определяет, какой AI-агент подключен к клиенту."""
for agent_slug, config in AI_AGENT_REGISTRY.items(): for agent_slug, config in AI_AGENT_REGISTRY.items():
partner = Partner.objects.filter(slug=agent_slug).first() partner = Partner.objects.filter(slug=agent_slug).first()
if not partner: if not partner:
@ -165,12 +177,12 @@ def get_ai_agent(client: Client) -> Dict[str, Optional[str]]:
required_settings = config.get('required_settings', []) required_settings = config.get('required_settings', [])
missing = [f for f in required_settings if f not in settings] missing = [f for f in required_settings if f not in settings]
if missing: if missing:
_log_dialogue(str(client.id), 'ERROR', _log_dialogue(str(client.pk), 'ERROR',
f"AI agent {agent_slug} missing settings: {missing}") f"AI agent {agent_slug} missing settings: {missing}")
continue continue
if not integration.token: if not integration.token:
_log_dialogue(str(client.id), 'ERROR', _log_dialogue(str(client.pk), 'ERROR',
f"AI agent {agent_slug} missing token") f"AI agent {agent_slug} missing token")
continue continue
@ -193,7 +205,10 @@ def get_ai_agent(client: Client) -> Dict[str, Optional[str]]:
} }
def call_ai_agent(client: Client, session_id: str, user_query: str) -> Dict[str, Any]: def call_ai_agent(client: Client, session_id: str, user_query: str, contact_name: str = None) -> Dict[str, Any]:
"""
Универсальный вызов AI-агента с передачей имени клиента.
"""
agent_info = get_ai_agent(client) agent_info = get_ai_agent(client)
ai_agent = agent_info.get('ai_agent') ai_agent = agent_info.get('ai_agent')
if not ai_agent: if not ai_agent:
@ -226,7 +241,7 @@ def call_ai_agent(client: Client, session_id: str, user_query: str) -> Dict[str,
"status": "complete" "status": "complete"
} }
system_prompt = get_system_prompt(client) system_prompt = get_system_prompt(client, name=contact_name)
client_prompt = agent_info.get('client_prompt') client_prompt = agent_info.get('client_prompt')
if client_prompt: if client_prompt:
system_prompt += f"\n\nДополнительная информация о ресторане:\n{client_prompt}" system_prompt += f"\n\nДополнительная информация о ресторане:\n{client_prompt}"
@ -248,11 +263,9 @@ def call_ai_agent(client: Client, session_id: str, user_query: str) -> Dict[str,
"status": "complete" "status": "complete"
} }
kwargs['folder_id'] = folder_id kwargs['folder_id'] = folder_id
# Можно задать модель из настроек или использовать default
model = agent_info.get('settings', {}).get('model', config.get('default_model', 'yandexgpt')) model = agent_info.get('settings', {}).get('model', config.get('default_model', 'yandexgpt'))
kwargs['model'] = model kwargs['model'] = model
# Логируем вызов
_log_dialogue(session_id, 'DEBUG', f"Calling {ai_agent} with kwargs: {kwargs}") _log_dialogue(session_id, 'DEBUG', f"Calling {ai_agent} with kwargs: {kwargs}")
try: try:
@ -266,3 +279,36 @@ def call_ai_agent(client: Client, session_id: str, user_query: str) -> Dict[str,
"entities": {}, "entities": {},
"status": "complete" "status": "complete"
} }
# ---------- работа с мета-данными контакта ----------
def get_contact_meta(contact) -> Dict[str, Any]:
"""Извлекает JSON-данные из поля descr контакта."""
if not contact.descr:
return {}
try:
return json.loads(contact.descr)
except json.JSONDecodeError:
return {}
def set_contact_meta(contact, key: str, value: Any):
"""Устанавливает ключ в JSON-данных контакта и сохраняет."""
meta = get_contact_meta(contact)
meta[key] = value
contact.descr = json.dumps(meta, ensure_ascii=False)
contact.save(update_fields=['descr'])
def is_valid_name(name: str) -> bool:
"""Проверяет, является ли имя валидным (не никнейм, не телефон)."""
if not name:
return False
name = name.strip()
if len(name) < 2:
return False
if re.match(r'^[0-9@_\-\.]+$', name):
return False
if re.match(r'^\+?[0-9]{10,15}$', name):
return False
return True

@ -12,7 +12,7 @@ def yandex_chat_structured(
folder_id: str, folder_id: str,
system_prompt: str, system_prompt: str,
user_query: str, user_query: str,
model: str = "yandexgpt", # можно "yandexgpt-lite" model: str = "yandexgpt", # имя модели (yandexgpt или yandexgpt-lite)
max_tokens: int = 500, max_tokens: int = 500,
temperature: float = 0.3, temperature: float = 0.3,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
@ -32,18 +32,21 @@ def yandex_chat_structured(
if not folder_id: if not folder_id:
raise ValueError("folder_id is empty") raise ValueError("folder_id is empty")
# Формируем URI модели в формате, который ожидает Yandex
model_uri = f"gpt://{folder_id}/{model}/latest"
client = OpenAI( client = OpenAI(
api_key=api_token, api_key=api_token,
base_url="https://ai.api.cloud.yandex.net/v1", base_url="https://ai.api.cloud.yandex.net/v1",
project=folder_id, project=folder_id, # folder_id для аутентификации
) )
# Убираем response_format он может вызывать ошибку парсинга URI модели
response = client.chat.completions.create( response = client.chat.completions.create(
model=model, model=model_uri, # <-- используем полный URI
messages=_contexts[session_id], messages=_contexts[session_id],
max_tokens=max_tokens, max_tokens=max_tokens,
temperature=temperature, temperature=temperature,
# response_format не используем
) )
assistant_content = response.choices[0].message.content assistant_content = response.choices[0].message.content

@ -34,6 +34,11 @@
- вопрос/проблему (question), чтобы администратор мог подготовиться. - вопрос/проблему (question), чтобы администратор мог подготовиться.
- Когда все обязательные поля (phone, contact_method, question) собраны, укажи status: "complete". - Когда все обязательные поля (phone, contact_method, question) собраны, укажи status: "complete".
4. **Сбор отзыва (intent: "feedback")**:
- Если клиент хочет оставить отзыв (фразы вроде "хочу оставить отзыв", "оценка", "как оценить"), то запроси оценку (от 1 до 5) и текст отзыва.
- Когда все данные собраны (rating и text), укажи status: "complete" и заполни entities: {"rating": число, "feedback_text": "текст"}.
- Если клиент не указал оценку или текст, задай уточняющий вопрос.
Важно: Важно:
- Если клиент не указал какую-то информацию, задавай уточняющий вопрос и возвращай status: "need_more_info". - Если клиент не указал какую-то информацию, задавай уточняющий вопрос и возвращай status: "need_more_info".
- Не выдумывай данные, которых нет. - Не выдумывай данные, которых нет.

@ -1,8 +1,7 @@
# bot_message.py # bot_message.py
import re import re
import json
import qrcode import qrcode
from ai_agent.api_common import get_system_prompt, get_ai_agent, call_ai_agent from ai_agent.api_common import get_ai_agent, call_ai_agent, get_contact_meta, set_contact_meta, is_valid_name
from max_bot.max_api import * from max_bot.max_api import *
from restoran_max_bot.bot_started import check_registration from restoran_max_bot.bot_started import check_registration
from restoran_max_bot.common import * from restoran_max_bot.common import *
@ -13,13 +12,25 @@ from restoran_max_bot.utils import has_key, is_json
# ---------- вспомогательные функции ---------- # ---------- вспомогательные функции ----------
def send_booking_to_crm(chat_id, date, time, people, phone): def send_booking_to_crm(client: Client, date, time, people, phone, name):
# TODO: реализовать отправку в CRM # TODO: реализация бронирования столика
msg = f'Бронирование столика:\nНа дату: {date}-{time} \nКоличество: {people}\nКонтакт: {phone}\nИмя: {name}'
send_message_all_manager(client, msg)
return True return True
def send_contact_to_admin(chat_id, phone, contact_method, question, preferred_time, contact): def send_contact_to_admin(client: Client, phone, contact_method, question, preferred_time, name):
# TODO: реализовать отправку администратору # TODO: реализовать отправку администратору
msg = (f'Гость попросил связаться:\n {contact_method}\nВопрос: {question}\n'
f'Удобное время: {preferred_time}\nКонтакт: {phone}\nИмя: {name}')
send_message_all_manager(client, msg)
return True
def send_feedback_to_crm(client: Client, rating, feedback_text, name, phone):
# TODO: реализовать отправку отзыва в CRM
msg = f'Отзыв от {name} (тел. {phone}):\nОценка: {rating}/5\nТекст: {feedback_text}'
send_message_all_manager(client, msg)
return True return True
@ -93,6 +104,41 @@ def bot_message(client: Client, message: dict, settings_max: dict, message_type)
if not contact: if not contact:
return False return False
# ---------- проверка и запрос имени ----------
meta = get_contact_meta(contact)
real_name = meta.get('real_name')
name_asked = meta.get('name_asked', False)
# Если имя ещё не запрашивали и нет реального имени
if not real_name and not name_asked:
# Проверяем имя, которое пришло из мессенджера (contact.name)
if is_valid_name(contact.name):
# Если валидное, сохраняем как реальное
set_contact_meta(contact, 'real_name', contact.name.strip())
real_name = contact.name.strip()
else:
# Если невалидное — запрашиваем
maxbot_send_text_message(chat_id, settings_max['token'],
"Как мне к вам обращаться? Напишите ваше имя, чтобы я мог обращаться к вам лично.")
set_contact_meta(contact, 'name_asked', True)
# Завершаем обработку, ждём ответ
return True
# Если имя уже запрашивали, но пользователь не ответил, а сейчас прислал сообщение
if name_asked and not real_name:
potential_name = message['message']['body']['text'].strip()
if is_valid_name(potential_name):
set_contact_meta(contact, 'real_name', potential_name)
real_name = potential_name
maxbot_send_text_message(chat_id, settings_max['token'],
f"Приятно познакомиться, {potential_name}! Чем могу помочь?")
# После приветствия продолжаем обработку (не завершаем)
else:
maxbot_send_text_message(chat_id, settings_max['token'],
"Пожалуйста, напишите ваше настоящее имя (от 2 символов).")
return True
# ---------- обработка стандартных команд (меню, бонусы и т.п.) ----------
data = None data = None
if message_type == 'message_created': if message_type == 'message_created':
data = common_get_data(client=client, key=str(message['message']['body']['text']).lower()) data = common_get_data(client=client, key=str(message['message']['body']['text']).lower())
@ -100,7 +146,6 @@ def bot_message(client: Client, message: dict, settings_max: dict, message_type)
payload = json.loads(message['callback']['payload']) payload = json.loads(message['callback']['payload'])
data = common_get_data(client=client, key=str(payload['data']).lower()) data = common_get_data(client=client, key=str(payload['data']).lower())
# ---------- обработка стандартных команд (меню, бонусы и т.п.) ----------
if data: if data:
for dt in data: for dt in data:
if dt.title == '##bonus##': if dt.title == '##bonus##':
@ -152,11 +197,12 @@ def bot_message(client: Client, message: dict, settings_max: dict, message_type)
message='👇 Выберите раздел', client=client) message='👇 Выберите раздел', client=client)
return True return True
# Вызываем AI-агента через универсальную функцию # Вызываем AI-агента через универсальную функцию, передавая реальное имя
response = call_ai_agent( response = call_ai_agent(
client=client, client=client,
session_id=str(chat_id), session_id=str(chat_id),
user_query=message['message']['body']['text'] user_query=message['message']['body']['text'],
contact_name=real_name # передаём имя для персонализации
) )
# ---------- обработка ответа AI ---------- # ---------- обработка ответа AI ----------
@ -177,9 +223,10 @@ def bot_message(client: Client, message: dict, settings_max: dict, message_type)
people = entities.get('people') people = entities.get('people')
phone = entities.get('phone') phone = entities.get('phone')
if date and time and people: if date and time and people:
success = send_booking_to_crm(chat_id, date, time, people, phone) success = send_booking_to_crm(client, date, time, people, phone, real_name)
if success: if success:
confirm_msg = f"✅ Бронирование на {date} в {time} на {people} чел. принято! Администратор свяжется с вами." confirm_msg = (f"✅ Бронирование на {date} в {time} на {people} чел. принято! "
f"Администратор свяжется с вами.")
else: else:
confirm_msg = "Не удалось забронировать. Попробуйте позже." confirm_msg = "Не удалось забронировать. Попробуйте позже."
maxbot_send_text_message(chat_id, settings_max['token'], confirm_msg) maxbot_send_text_message(chat_id, settings_max['token'], confirm_msg)
@ -197,7 +244,7 @@ def bot_message(client: Client, message: dict, settings_max: dict, message_type)
question = entities.get('question', '') question = entities.get('question', '')
preferred_time = entities.get('preferred_time', '') preferred_time = entities.get('preferred_time', '')
if phone and contact_method: if phone and contact_method:
success = send_contact_to_admin(chat_id, phone, contact_method, question, preferred_time, contact) success = send_contact_to_admin(client, phone, contact_method, question, preferred_time, real_name)
if success: if success:
confirm_msg = f"✅ Ваш запрос передан администратору. Способ связи: {contact_method}. Скоро с вами свяжутся." confirm_msg = f"✅ Ваш запрос передан администратору. Способ связи: {contact_method}. Скоро с вами свяжутся."
else: else:
@ -209,6 +256,24 @@ def bot_message(client: Client, message: dict, settings_max: dict, message_type)
else: else:
maxbot_send_text_message(chat_id, settings_max['token'], message_text) maxbot_send_text_message(chat_id, settings_max['token'], message_text)
elif intent == 'feedback':
if status == 'need_more_info':
maxbot_send_text_message(chat_id, settings_max['token'], message_text)
elif status == 'complete':
rating = entities.get('rating')
feedback_text = entities.get('feedback_text', '')
if rating and feedback_text:
success = send_feedback_to_crm(client, rating, feedback_text, real_name, contact.phone)
if success:
confirm_msg = "✅ Спасибо за ваш отзыв! Мы учтём его."
else:
confirm_msg = "Не удалось отправить отзыв. Попробуйте позже."
maxbot_send_text_message(chat_id, settings_max['token'], confirm_msg)
else:
maxbot_send_text_message(chat_id, settings_max['token'], "⚠️ Не хватает данных для отзыва.")
else:
maxbot_send_text_message(chat_id, settings_max['token'], message_text)
else: else:
maxbot_send_text_message(chat_id, settings_max['token'], message_text) maxbot_send_text_message(chat_id, settings_max['token'], message_text)

@ -1,6 +1,8 @@
# функция поиска контента по ключевому слову # функция поиска контента по ключевому слову
import json import json
import requests
from max_bot.models import Client, ProductCategory, Product, Contact, Integration, Partner from max_bot.models import Client, ProductCategory, Product, Contact, Integration, Partner
@ -69,3 +71,20 @@ def common_get_integration(client: Client) -> Integration:
partner = Partner.objects.filter(status=1, slug__in=['iiko', 'rkeeper']) partner = Partner.objects.filter(status=1, slug__in=['iiko', 'rkeeper'])
integration = Integration.objects.filter(client=client, status=1, partner__in=partner).first() integration = Integration.objects.filter(client=client, status=1, partner__in=partner).first()
return integration return integration
def send_message_all_manager(client: Client, message: str):
"""
Функция отправки сообщений в групу администраторам
"""
headers = {
'Content-Type': 'application/json',
}
url = f'https://api.telefon-ip.ru/webast/sendmessages/{client.token}?messages={message}'
print(url)
response = requests.request("GET", url, headers=headers).json()
return response

Loading…
Cancel
Save