Compare commits

..

No commits in common. '0b5a5d0286e7c686d983b8221c04c3a1478c144d' and '5b7ad6da89320df80fb7a82681e52a16eef5457b' have entirely different histories.

@ -3,7 +3,7 @@ import os
import json
import re
from datetime import datetime
from typing import Dict, Any, Optional
from typing import Dict, Any, List, Optional
from django.core.cache import cache
from max_bot.models import Client, ProductCategory, Product, Qr, Partner, Integration
from restoran_max_bot import settings
@ -30,7 +30,6 @@ AI_AGENT_REGISTRY = {
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):
return
@ -52,7 +51,6 @@ def _log_dialogue(session_id: str, role: str, content: str, log_file: str = None
def _load_base_prompt() -> str:
"""Загружает базовый текст системного промта из файла."""
global _BASE_PROMPT_CACHE
if _BASE_PROMPT_CACHE is not None:
return _BASE_PROMPT_CACHE
@ -73,16 +71,13 @@ def _load_base_prompt() -> str:
return _BASE_PROMPT_CACHE
def get_system_prompt(client: Client, name: str = None) -> str:
"""
Генерирует системный промпт с учётом имени клиента.
Базовая часть кешируется отдельно.
"""
# Кешируем базовую часть (без имени)
cache_key = f"system_prompt_base_{client.pk}"
base_prompt = cache.get(cache_key)
if not base_prompt:
base_prompt = _load_base_prompt()
def get_system_prompt(client: Client) -> str:
cache_key = f"system_prompt_{client.id}"
cached = cache.get(cache_key)
if cached:
return cached
prompt = _load_base_prompt()
qr = Qr.objects.filter(client=client).first()
if qr:
@ -95,43 +90,37 @@ def get_system_prompt(client: Client, name: str = None) -> str:
Часы работы: {qr.worktime or 'Не указаны'}
Активность: {qr.activity or 'Не указана'}
"""
base_prompt += restaurant_info
prompt += restaurant_info
else:
base_prompt += "\nИнформация о ресторане отсутствует.\n"
prompt += "\nИнформация о ресторане отсутствует.\n"
categories = ProductCategory.objects.filter(client=client, status=True, src=2).order_by('ord')
if categories.exists():
base_prompt += "\nМеню:\n"
prompt += "\nМеню:\n"
for cat in categories:
base_prompt += f"\nКатегория: {cat.title}\n"
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} руб."
prompt += f" - {prod.title}{prod.price} руб."
if prod.descr:
base_prompt += f" ({prod.descr})"
base_prompt += "\n"
prompt += f" ({prod.descr})"
prompt += "\n"
else:
base_prompt += " (в этой категории пока нет блюд)\n"
prompt += " (в этой категории пока нет блюд)\n"
else:
base_prompt += "\nМеню не загружено.\n"
prompt += "\nМеню не загружено.\n"
base_prompt += """
prompt += """
При ответах на вопросы о меню используй только те блюда, которые перечислены выше.
Если клиент спрашивает о блюде, которого нет в списке, честно скажи об этом и предложи посмотреть другие позиции.
Всегда помни, что ты должен возвращать только JSON, без лишнего текста вне JSON.
"""
cache.set(cache_key, base_prompt, 300) # 5 минут
# Добавляем имя, если оно валидно
if name and is_valid_name(name):
return f"Обращайся к клиенту по имени: {name}. Используй обращение в каждом ответе.\n\n{base_prompt}"
else:
return base_prompt
cache.set(cache_key, prompt, 300)
return prompt
def parse_ai_response(assistant_content: str) -> Dict[str, Any]:
"""Универсальный парсер для ответа AI (JSON)."""
content = re.sub(r'```(?:json)?\s*', '', assistant_content)
content = re.sub(r'\s*```', '', content)
content = content.strip()
@ -158,7 +147,6 @@ def parse_ai_response(assistant_content: str) -> Dict[str, Any]:
def get_ai_agent(client: Client) -> Dict[str, Optional[str]]:
"""Определяет, какой AI-агент подключен к клиенту."""
for agent_slug, config in AI_AGENT_REGISTRY.items():
partner = Partner.objects.filter(slug=agent_slug).first()
if not partner:
@ -177,12 +165,12 @@ def get_ai_agent(client: Client) -> Dict[str, Optional[str]]:
required_settings = config.get('required_settings', [])
missing = [f for f in required_settings if f not in settings]
if missing:
_log_dialogue(str(client.pk), 'ERROR',
_log_dialogue(str(client.id), 'ERROR',
f"AI agent {agent_slug} missing settings: {missing}")
continue
if not integration.token:
_log_dialogue(str(client.pk), 'ERROR',
_log_dialogue(str(client.id), 'ERROR',
f"AI agent {agent_slug} missing token")
continue
@ -205,10 +193,7 @@ def get_ai_agent(client: Client) -> Dict[str, Optional[str]]:
}
def call_ai_agent(client: Client, session_id: str, user_query: str, contact_name: str = None) -> Dict[str, Any]:
"""
Универсальный вызов AI-агента с передачей имени клиента.
"""
def call_ai_agent(client: Client, session_id: str, user_query: str) -> Dict[str, Any]:
agent_info = get_ai_agent(client)
ai_agent = agent_info.get('ai_agent')
if not ai_agent:
@ -241,7 +226,7 @@ def call_ai_agent(client: Client, session_id: str, user_query: str, contact_name
"status": "complete"
}
system_prompt = get_system_prompt(client, name=contact_name)
system_prompt = get_system_prompt(client)
client_prompt = agent_info.get('client_prompt')
if client_prompt:
system_prompt += f"\n\nДополнительная информация о ресторане:\n{client_prompt}"
@ -263,9 +248,11 @@ def call_ai_agent(client: Client, session_id: str, user_query: str, contact_name
"status": "complete"
}
kwargs['folder_id'] = folder_id
# Можно задать модель из настроек или использовать default
model = agent_info.get('settings', {}).get('model', config.get('default_model', 'yandexgpt'))
kwargs['model'] = model
# Логируем вызов
_log_dialogue(session_id, 'DEBUG', f"Calling {ai_agent} with kwargs: {kwargs}")
try:
@ -279,36 +266,3 @@ def call_ai_agent(client: Client, session_id: str, user_query: str, contact_name
"entities": {},
"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,
system_prompt: str,
user_query: str,
model: str = "yandexgpt", # имя модели (yandexgpt или yandexgpt-lite)
model: str = "yandexgpt", # можно "yandexgpt-lite"
max_tokens: int = 500,
temperature: float = 0.3,
) -> Dict[str, Any]:
@ -32,21 +32,18 @@ def yandex_chat_structured(
if not folder_id:
raise ValueError("folder_id is empty")
# Формируем URI модели в формате, который ожидает Yandex
model_uri = f"gpt://{folder_id}/{model}/latest"
client = OpenAI(
api_key=api_token,
base_url="https://ai.api.cloud.yandex.net/v1",
project=folder_id, # folder_id для аутентификации
project=folder_id,
)
# Убираем response_format он может вызывать ошибку парсинга URI модели
response = client.chat.completions.create(
model=model_uri, # <-- используем полный URI
model=model,
messages=_contexts[session_id],
max_tokens=max_tokens,
temperature=temperature,
# response_format не используем
)
assistant_content = response.choices[0].message.content

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

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

@ -1,8 +1,6 @@
# функция поиска контента по ключевому слову
import json
import requests
from max_bot.models import Client, ProductCategory, Product, Contact, Integration, Partner
@ -71,20 +69,3 @@ def common_get_integration(client: Client) -> Integration:
partner = Partner.objects.filter(status=1, slug__in=['iiko', 'rkeeper'])
integration = Integration.objects.filter(client=client, status=1, partner__in=partner).first()
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