создание агента для написния кода

Добавлен блок LOGGING
main
pilot 2 months ago
parent 21bb6f2a8a
commit bb166bbae3

@ -0,0 +1,414 @@
import os
import json
import re
import shutil
import subprocess
from collections import defaultdict
from datetime import datetime
from pathlib import Path
# ---------- Функции сбора проекта ----------
def collect_project_files(root_dir, extensions=('.py', '.html'),
exclude_dirs=('venv', '__pycache__', '.git', 'env', 'node_modules')):
files_data = []
root_dir = os.path.abspath(root_dir)
for dirpath, dirnames, filenames in os.walk(root_dir):
dirnames[:] = [d for d in dirnames if d not in exclude_dirs]
for filename in filenames:
if filename.endswith(extensions):
file_path = os.path.join(dirpath, filename)
rel_path = os.path.relpath(file_path, root_dir)
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
except UnicodeDecodeError:
try:
with open(file_path, 'r', encoding='cp1251') as f:
content = f.read()
except Exception:
content = f"[Не удалось прочитать файл {rel_path} — возможно, бинарный]"
except Exception as e:
content = f"[Ошибка чтения: {e}]"
files_data.append({'path': rel_path, 'content': content})
return files_data
def build_tree(files_paths):
tree = defaultdict(dict)
for path in sorted(files_paths):
parts = path.split(os.sep)
current = tree
for part in parts:
if part not in current:
current[part] = {}
current = current[part]
current['__is_file__'] = True
def render(node, prefix=''):
lines = []
items = sorted([k for k in node.keys() if not k.startswith('__')])
for i, key in enumerate(items):
is_last = (i == len(items) - 1)
is_file = node[key].get('__is_file__', False)
connector = '└── ' if is_last else '├── '
lines.append(prefix + connector + key + (' (файл)' if is_file else '/'))
if not is_file:
extension = ' ' if is_last else '│ '
lines.extend(render(node[key], prefix + extension))
return lines
return '\n'.join(render(tree))
def build_prompt(main_prompt, files_data):
parts = []
parts.append("=" * 80)
parts.append("ОСНОВНОЙ ЗАПРОС (ПРОМТ)")
parts.append("=" * 80)
parts.append(main_prompt)
parts.append("")
parts.append("=" * 80)
parts.append("СТРУКТУРА ПРОЕКТА (ДЕРЕВО ПАПОК И ФАЙЛОВ)")
parts.append("=" * 80)
if files_data:
paths = [item['path'] for item in files_data]
tree_str = build_tree(paths)
parts.append(tree_str)
else:
parts.append("Файлы с расширениями .py или .html не найдены.")
parts.append("")
parts.append("=" * 80)
parts.append("СОДЕРЖИМОЕ ФАЙЛОВ (ПО ОТНОСИТЕЛЬНЫМ ПУТЯМ)")
parts.append("=" * 80)
if files_data:
for item in files_data:
parts.append(f"\n--- Файл: {item['path']} ---")
parts.append(item['content'])
parts.append("")
else:
parts.append("Нет файлов для отображения.")
parts.append("")
parts.append("=" * 80)
parts.append("ИНСТРУКЦИЯ ПО ФОРМАТУ ОТВЕТА (ОБЯЗАТЕЛЬНО К ВЫПОЛНЕНИЮ)")
parts.append("=" * 80)
parts.append(
"Ты — ассистент по разработке. На основе структуры проекта и запроса пользователя предложи изменения в коде.\n"
"Твой ответ должен состоять из двух частей:\n"
"1. Краткое описание того, какие изменения ты предлагаешь (на русском, 1–3 предложения).\n"
"2. Блок кода с JSON-структурой, который будет использован для автоматического применения изменений.\n"
" Блок должен быть оформлен как ```json ... ``` (это позволит скопировать его одной кнопкой).\n"
"\n"
"Структура JSON:\n"
"{\n"
" \"actions\": [\n"
" {\n"
" \"action\": \"create\" | \"update\" | \"delete\",\n"
" \"file_path\": \"относительный/путь/к/файлу\",\n"
" \"content\": \"полное новое содержимое файла (для create/update)\",\n"
" \"description\": \"краткое описание этого изменения\"\n"
" }\n"
" ]\n"
"}\n"
"\n"
"Важно:\n"
"- Все пути должны быть относительно корня проекта (как в дереве выше).\n"
"- Для update передавай ПОЛНОЕ новое содержимое файла (заменяй целиком).\n"
"- Для delete можно не указывать content (или оставить пустую строку).\n"
"- Если изменений не требуется, верни пустой массив actions.\n"
"- Весь JSON должен быть внутри блока ```json ... ```. Никакого другого кода вне блока.\n"
"- Текст описания должен быть перед блоком, а не после.\n"
"- Используй только двойные кавычки для строк в JSON.\n"
)
parts.append("")
return "\n".join(parts)
# ---------- Извлечение JSON ----------
def extract_json_from_text(text):
json_block_pattern = r"```json\s*(\{.*?\})\s*```"
match = re.search(json_block_pattern, text, re.DOTALL | re.IGNORECASE)
if match:
json_str = match.group(1)
try:
return json.loads(json_str)
except json.JSONDecodeError:
pass
start = text.find('{')
if start == -1:
return None
end = text.rfind('}')
if end == -1:
return None
json_candidate = text[start:end+1]
try:
return json.loads(json_candidate)
except json.JSONDecodeError:
return None
# ---------- Применение изменений ----------
def apply_changes(project_root, actions):
reports = []
errors = False
project_root = os.path.abspath(project_root)
for action in actions:
action_type = action.get('action')
file_path = action.get('file_path')
content = action.get('content', '')
description = action.get('description', '')
if not action_type or not file_path:
reports.append("❌ Пропущено действие: отсутствует 'action' или 'file_path'")
errors = True
continue
norm_path = os.path.normpath(file_path.replace('/', os.sep))
abs_path = os.path.join(project_root, norm_path)
if not os.path.abspath(abs_path).startswith(project_root):
reports.append(f"❌ Опасный путь: {file_path} — выход за пределы проекта")
errors = True
continue
try:
if action_type == 'create':
os.makedirs(os.path.dirname(abs_path), exist_ok=True)
with open(abs_path, 'w', encoding='utf-8') as f:
f.write(content)
reports.append(f"✅ Создан: {file_path} — {description}")
elif action_type == 'update':
if not os.path.exists(abs_path):
reports.append(f"⚠️ Файл {file_path} не существует, создаём новый (как create)")
os.makedirs(os.path.dirname(abs_path), exist_ok=True)
with open(abs_path, 'w', encoding='utf-8') as f:
f.write(content)
reports.append(f"✅ Обновлён: {file_path} — {description}")
elif action_type == 'delete':
if os.path.exists(abs_path):
os.remove(abs_path)
reports.append(f"✅ Удалён: {file_path} — {description}")
else:
reports.append(f"⚠️ Файл {file_path} не найден, пропускаем удаление")
else:
reports.append(f"❌ Неизвестное действие '{action_type}' для {file_path}")
errors = True
except Exception as e:
reports.append(f"❌ Ошибка при обработке {file_path}: {e}")
errors = True
return reports, errors
# ---------- Вспомогательные функции ----------
def get_downloads_folder():
home = Path.home()
downloads = home / 'Downloads'
if downloads.exists():
return str(downloads)
alt = home / 'Загрузки'
if alt.exists():
return str(alt)
return os.getcwd()
def find_latest_json_file(directory, prefix='deepseek_json_'):
files = []
for f in os.listdir(directory):
if f.startswith(prefix) and f.endswith('.json'):
full_path = os.path.join(directory, f)
if os.path.isfile(full_path):
files.append((full_path, os.path.getmtime(full_path)))
if not files:
return None
files.sort(key=lambda x: x[1], reverse=True)
return files[0][0]
def run_tests(project_root, test_path=None):
reports = []
manage_path = os.path.join(project_root, 'manage.py')
cmd = []
if os.path.isfile(manage_path):
cmd = ['python', 'manage.py', 'test']
if test_path:
cmd.append(test_path)
cmd.append('--verbosity=2')
reports.append(f"🔍 Запуск тестов Django: {' '.join(cmd)}")
else:
cmd = ['pytest', '-v']
if test_path:
cmd.append(test_path)
reports.append(f"🔍 Запуск тестов pytest: {' '.join(cmd)}")
try:
result = subprocess.run(cmd, cwd=project_root, capture_output=True, text=True)
output = result.stdout + result.stderr
success = result.returncode == 0
if success:
reports.append(f"✅ Тесты прошли успешно (код {result.returncode})")
else:
reports.append(f"❌ Тесты завершились с ошибкой (код {result.returncode})")
return success, reports, output
except Exception as e:
reports.append(f"❌ Ошибка при запуске тестов: {e}")
return False, reports, str(e)
# ---------- Логирование в суточный файл (только для APPLY и кратких записей о тестах) ----------
def write_log_entry(log_file_path, entry_type, lines, extra_info=''):
with open(log_file_path, 'a', encoding='utf-8') as f:
f.write("=" * 80 + "\n")
f.write(f"ТИП: {entry_type}\n")
f.write(f"ВРЕМЯ: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
if extra_info:
f.write(f"ДОП. ИНФО: {extra_info}\n")
f.write("-" * 80 + "\n")
for line in lines:
f.write(line + "\n")
f.write("=" * 80 + "\n\n")
# ---------- ОСНОВНАЯ ФУНКЦИЯ ----------
def main():
print("=" * 60)
print(" Инструмент для работы с DeepSeek")
print("=" * 60)
mode = input(
"Выберите режим:\n"
" 1 — Создать промт (собрать проект и инструкцию)\n"
" 2 — Применить ответ DeepSeek (автоматически найти JSON в Downloads)\n"
" 3 — Запустить тесты (все или указать папку/файл)\n"
"По умолчанию (Enter) — режим 1\n"
"Ваш выбор: "
).strip()
project_root = os.getcwd()
ai_response_dir = os.path.join(project_root, '_ai_response')
os.makedirs(ai_response_dir, exist_ok=True)
# Суточный лог-файл (только для изменений и кратких записей о тестах)
today = datetime.now().strftime('%Y-%m-%d')
log_file = os.path.join(ai_response_dir, f'ai_log_{today}.txt')
if mode == '2':
# ---- РЕЖИМ 2: Применение изменений (пишем в суточный лог) ----
downloads_dir = get_downloads_folder()
json_file = find_latest_json_file(downloads_dir)
if json_file is None:
print(f"❌ Не найден JSON-файл с префиксом 'deepseek_json_' в папке {downloads_dir}")
return
print(f"📂 Найден файл: {json_file}")
try:
with open(json_file, 'r', encoding='utf-8') as f:
text = f.read()
except Exception as e:
print(f"❌ Ошибка чтения файла: {e}")
return
data = extract_json_from_text(text)
if data is None:
print("❌ Не удалось извлечь JSON из ответа. Убедитесь, что в ответе есть блок ```json ... ``` или валидный JSON.")
return
actions = data.get('actions')
if actions is None:
print("❌ В JSON отсутствует поле 'actions'.")
return
if not isinstance(actions, list):
print("❌ Поле 'actions' должно быть массивом.")
return
if len(actions) == 0:
print("ℹ️ Нет действий для применения.")
return
print(f"\n📁 Применяем изменения в: {project_root}")
reports, errors = apply_changes(project_root, actions)
# Перемещаем JSON-файл в _ai_response
dest_filename = f"applied_{os.path.basename(json_file)}"
dest_path = os.path.join(ai_response_dir, dest_filename)
shutil.move(json_file, dest_path)
move_info = f"JSON перемещён в {dest_path}"
# Пишем в суточный лог
write_log_entry(log_file, 'APPLY', reports, extra_info=move_info)
# Вывод в консоль
print("\n" + "=" * 60)
print("ОТЧЁТ О ВЫПОЛНЕННЫХ ДЕЙСТВИЯХ")
print("=" * 60)
for line in reports:
print(line)
if errors:
print("\n❌ Были ошибки при применении изменений.")
else:
print("\n✅ Все изменения применены успешно.")
print(f"\n📄 Лог изменений записан в: {log_file}")
elif mode == '3':
# ---- РЕЖИМ 3: Тестирование (отдельный файл + краткая запись в суточный лог) ----
test_path = input("Введите относительный путь к папке или файлу с тестами (оставьте пустым для всех тестов): ").strip()
if test_path == '':
test_path = None
print("\n🔍 Запуск тестов...")
success, reports, output = run_tests(project_root, test_path)
# Сохраняем полный вывод в отдельный файл
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
test_report_file = os.path.join(ai_response_dir, f"test_report_{timestamp}.txt")
with open(test_report_file, 'w', encoding='utf-8') as f:
f.write("=" * 60 + "\n")
f.write("ОТЧЁТ О ТЕСТИРОВАНИИ\n")
f.write(f"Дата: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
f.write(f"Путь тестов: {test_path if test_path else 'все тесты'}\n")
f.write("=" * 60 + "\n")
f.write(output)
# Пишем краткую запись в суточный лог
brief_lines = [
f"Тесты запущены для пути: {test_path if test_path else 'все тесты'}",
f"Результат: {'✅ УСПЕШНО' if success else '❌ ОШИБКА'}",
f"Полный отчёт сохранён в: {test_report_file}"
]
write_log_entry(log_file, 'TEST', brief_lines, extra_info=f"Путь: {test_path if test_path else 'все тесты'}")
# Вывод в консоль
print("\n" + "=" * 60)
print("РЕЗУЛЬТАТ ТЕСТИРОВАНИЯ")
print("=" * 60)
for line in reports:
print(line)
if success:
print("\n✅ Тесты прошли успешно.")
else:
print("\n❌ Тесты завершились с ошибками. Проверьте полный отчёт.")
print(f"\n📄 Полный отчёт сохранён в: {test_report_file}")
print(f"📄 Краткая запись добавлена в суточный лог: {log_file}")
else:
# ---- РЕЖИМ 1: Создание промта (сохраняем в _ai_response) ----
print("\nСбор информации о проекте для отправки в DeepSeek.")
main_prompt = input("\nВведите ваш промт (одна строка, Enter для завершения): ").strip()
if not main_prompt:
print("Промт не может быть пустым. Завершение.")
return
print(f"\nСканируем папку: {project_root}")
files_data = collect_project_files(project_root)
full_prompt = build_prompt(main_prompt, files_data)
output_file = os.path.join(ai_response_dir, "ai_promt.txt")
try:
with open(output_file, 'w', encoding='utf-8') as f:
f.write(full_prompt)
print(f"\n✅ Готово! Промт сохранён в файл: {output_file}")
print(f"Всего собрано файлов: {len(files_data)}")
print("\nТеперь скопируйте содержимое этого файла и отправьте в DeepSeek.")
print("DeepSeek вернёт ответ с описанием и JSON-блоком.")
print("Скопируйте весь ответ и сохраните в файл с именем deepseek_json_YYYYMMDD_HHMMSS.json в папке Downloads.")
print("Затем запустите этот скрипт в режиме 2 для автоматического применения.")
except Exception as e:
print(f"❌ Ошибка при сохранении: {e}")
if __name__ == "__main__":
main()

@ -0,0 +1,74 @@
-----BEGIN CERTIFICATE-----
MIIFwjCCA6qgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwcDELMAkGA1UEBhMCUlUx
PzA9BgNVBAoMNlRoZSBNaW5pc3RyeSBvZiBEaWdpdGFsIERldmVsb3BtZW50IGFu
ZCBDb21tdW5pY2F0aW9uczEgMB4GA1UEAwwXUnVzc2lhbiBUcnVzdGVkIFJvb3Qg
Q0EwHhcNMjIwMzAxMjEwNDE1WhcNMzIwMjI3MjEwNDE1WjBwMQswCQYDVQQGEwJS
VTE/MD0GA1UECgw2VGhlIE1pbmlzdHJ5IG9mIERpZ2l0YWwgRGV2ZWxvcG1lbnQg
YW5kIENvbW11bmljYXRpb25zMSAwHgYDVQQDDBdSdXNzaWFuIFRydXN0ZWQgUm9v
dCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMfFOZ8pUAL3+r2n
qqE0Zp52selXsKGFYoG0GM5bwz1bSFtCt+AZQMhkWQheI3poZAToYJu69pHLKS6Q
XBiwBC1cvzYmUYKMYZC7jE5YhEU2bSL0mX7NaMxMDmH2/NwuOVRj8OImVa5s1F4U
zn4Kv3PFlDBjjSjXKVY9kmjUBsXQrIHeaqmUIsPIlNWUnimXS0I0abExqkbdrXbX
YwCOXhOO2pDUx3ckmJlCMUGacUTnylyQW2VsJIyIGA8V0xzdaeUXg0VZ6ZmNUr5Y
Ber/EAOLPb8NYpsAhJe2mXjMB/J9HNsoFMBFJ0lLOT/+dQvjbdRZoOT8eqJpWnVD
U+QL/qEZnz57N88OWM3rabJkRNdU/Z7x5SFIM9FrqtN8xewsiBWBI0K6XFuOBOTD
4V08o4TzJ8+Ccq5XlCUW2L48pZNCYuBDfBh7FxkB7qDgGDiaftEkZZfApRg2E+M9
G8wkNKTPLDc4wH0FDTijhgxR3Y4PiS1HL2Zhw7bD3CbslmEGgfnnZojNkJtcLeBH
BLa52/dSwNU4WWLubaYSiAmA9IUMX1/RpfpxOxd4Ykmhz97oFbUaDJFipIggx5sX
ePAlkTdWnv+RWBxlJwMQ25oEHmRguNYf4Zr/Rxr9cS93Y+mdXIZaBEE0KS2iLRqa
OiWBki9IMQU4phqPOBAaG7A+eP8PAgMBAAGjZjBkMB0GA1UdDgQWBBTh0YHlzlpf
BKrS6badZrHF+qwshzAfBgNVHSMEGDAWgBTh0YHlzlpfBKrS6badZrHF+qwshzAS
BgNVHRMBAf8ECDAGAQH/AgEEMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsF
AAOCAgEAALIY1wkilt/urfEVM5vKzr6utOeDWCUczmWX/RX4ljpRdgF+5fAIS4vH
tmXkqpSCOVeWUrJV9QvZn6L227ZwuE15cWi8DCDal3Ue90WgAJJZMfTshN4OI8cq
W9E4EG9wglbEtMnObHlms8F3CHmrw3k6KmUkWGoa+/ENmcVl68u/cMRl1JbW2bM+
/3A+SAg2c6iPDlehczKx2oa95QW0SkPPWGuNA/CE8CpyANIhu9XFrj3RQ3EqeRcS
AQQod1RNuHpfETLU/A2gMmvn/w/sx7TB3W5BPs6rprOA37tutPq9u6FTZOcG1Oqj
C/B7yTqgI7rbyvox7DEXoX7rIiEqyNNUguTk/u3SZ4VXE2kmxdmSh3TQvybfbnXV
4JbCZVaqiZraqc7oZMnRoWrXRG3ztbnbes/9qhRGI7PqXqeKJBztxRTEVj8ONs1d
WN5szTwaPIvhkhO3CO5ErU2rVdUr89wKpNXbBODFKRtgxUT70YpmJ46VVaqdAhOZ
D9EUUn4YaeLaS8AjSF/h7UkjOibNc4qVDiPP+rkehFWM66PVnP1Msh93tc+taIfC
EYVMxjh8zNbFuoc7fzvvrFILLe7ifvEIUqSVIC/AzplM/Jxw7buXFeGP1qVCBEHq
391d/9RAfaZ12zkwFsl+IKwE/OZxW8AHa9i1p4GO0YSNuczzEm4=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIHQjCCBSqgAwIBAgICEAIwDQYJKoZIhvcNAQELBQAwcDELMAkGA1UEBhMCUlUx
PzA9BgNVBAoMNlRoZSBNaW5pc3RyeSBvZiBEaWdpdGFsIERldmVsb3BtZW50IGFu
ZCBDb21tdW5pY2F0aW9uczEgMB4GA1UEAwwXUnVzc2lhbiBUcnVzdGVkIFJvb3Qg
Q0EwHhcNMjIwMzAyMTEyNTE5WhcNMjcwMzA2MTEyNTE5WjBvMQswCQYDVQQGEwJS
VTE/MD0GA1UECgw2VGhlIE1pbmlzdHJ5IG9mIERpZ2l0YWwgRGV2ZWxvcG1lbnQg
YW5kIENvbW11bmljYXRpb25zMR8wHQYDVQQDDBZSdXNzaWFuIFRydXN0ZWQgU3Vi
IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA9YPqBKOk19NFymrE
wehzrhBEgT2atLezpduB24mQ7CiOa/HVpFCDRZzdxqlh8drku408/tTmWzlNH/br
HuQhZ/miWKOf35lpKzjyBd6TPM23uAfJvEOQ2/dnKGGJbsUo1/udKSvxQwVHpVv3
S80OlluKfhWPDEXQpgyFqIzPoxIQTLZ0deirZwMVHarZ5u8HqHetRuAtmO2ZDGQn
vVOJYAjls+Hiueq7Lj7Oce7CQsTwVZeP+XQx28PAaEZ3y6sQEt6rL06ddpSdoTMp
BnCqTbxW+eWMyjkIn6t9GBtUV45yB1EkHNnj2Ex4GwCiN9T84QQjKSr+8f0psGrZ
vPbCbQAwNFJjisLixnjlGPLKa5vOmNwIh/LAyUW5DjpkCx004LPDuqPpFsKXNKpa
L2Dm6uc0x4Jo5m+gUTVORB6hOSzWnWDj2GWfomLzzyjG81DRGFBpco/O93zecsIN
3SL2Ysjpq1zdoS01CMYxie//9zWvYwzI25/OZigtnpCIrcd2j1Y6dMUFQAzAtHE+
qsXflSL8HIS+IJEFIQobLlYhHkoE3avgNx5jlu+OLYe0dF0Ykx1PGNjbwqvTX37R
Cn32NMjlotW2QcGEZhDKj+3urZizp5xdTPZitA+aEjZM/Ni71VOdiOP0igbw6asZ
2fxdozZ1TnSSYNYvNATwthNmZysCAwEAAaOCAeUwggHhMBIGA1UdEwEB/wQIMAYB
Af8CAQAwDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTR4XENCy2BTm6KSo9MI7NM
XqtpCzAfBgNVHSMEGDAWgBTh0YHlzlpfBKrS6badZrHF+qwshzCBxwYIKwYBBQUH
AQEEgbowgbcwOwYIKwYBBQUHMAKGL2h0dHA6Ly9yb3N0ZWxlY29tLnJ1L2NkcC9y
b290Y2Ffc3NsX3JzYTIwMjIuY3J0MDsGCCsGAQUFBzAChi9odHRwOi8vY29tcGFu
eS5ydC5ydS9jZHAvcm9vdGNhX3NzbF9yc2EyMDIyLmNydDA7BggrBgEFBQcwAoYv
aHR0cDovL3JlZXN0ci1wa2kucnUvY2RwL3Jvb3RjYV9zc2xfcnNhMjAyMi5jcnQw
gbAGA1UdHwSBqDCBpTA1oDOgMYYvaHR0cDovL3Jvc3RlbGVjb20ucnUvY2RwL3Jv
b3RjYV9zc2xfcnNhMjAyMi5jcmwwNaAzoDGGL2h0dHA6Ly9jb21wYW55LnJ0LnJ1
L2NkcC9yb290Y2Ffc3NsX3JzYTIwMjIuY3JsMDWgM6Axhi9odHRwOi8vcmVlc3Ry
LXBraS5ydS9jZHAvcm9vdGNhX3NzbF9yc2EyMDIyLmNybDANBgkqhkiG9w0BAQsF
AAOCAgEARBVzZls79AdiSCpar15dA5Hr/rrT4WbrOfzlpI+xrLeRPrUG6eUWIW4v
Sui1yx3iqGLCjPcKb+HOTwoRMbI6ytP/ndp3TlYua2advYBEhSvjs+4vDZNwXr/D
anbwIWdurZmViQRBDFebpkvnIvru/RpWud/5r624Wp8voZMRtj/cm6aI9LtvBfT9
cfzhOaexI/99c14dyiuk1+6QhdwKaCRTc1mdfNQmnfWNRbfWhWBlK3h4GGE9JK33
Gk8ZS8DMrkdAh0xby4xAQ/mSWAfWrBmfzlOqGyoB1U47WTOeqNbWkkoAP2ys94+s
Jg4NTkiDVtXRF6nr6fYi0bSOvOFg0IQrMXO2Y8gyg9ARdPJwKtvWX8VPADCYMiWH
h4n8bZokIrImVKLDQKHY4jCsND2HHdJfnrdL2YJw1qFskNO4cSNmZydw0Wkgjv9k
F+KxqrDKlB8MZu2Hclph6v/CZ0fQ9YuE8/lsHZ0Qc2HyiSMnvjgK5fDc3TD4fa8F
E8gMNurM+kV8PT8LNIM+4Zs+LKEV8nqRWBaxkIVJGekkVKO8xDBOG/aN62AZKHOe
GcyIdu7yNMMRihGVZCYr8rYiJoKiOzDqOkPkLOPdhtVlgnhowzHDxMHND/E2WA5p
ZHuNM/m0TXt2wTTPL7JH2YC0gPz/BvvSzjksgzU5rLbRyUKQkgU=
-----END CERTIFICATE-----

@ -4,5 +4,5 @@ from content_bot.views import api_default_load
from max_bot.views import api_start_max_v1, iiko_webhook, iiko_send_message
urlpatterns = [
path('default-load/<str:token>', api_default_load),
path('default-load', api_default_load),
]

@ -6,29 +6,6 @@ from max_bot.models import Client, ProductCategory, Product
# Create your views here.
@csrf_exempt
def api_default_load(request: WSGIRequest, token):
# получаем информацию по токену клиента и делаем проверку
client = Client.objects.filter(token=token).first()
if not client:
rt = {'success': False, 'error': 'Unauthorized token', 'data': {}}
return JsonResponse(rt, status=401)
# делаем на базе клиента Тэнгис ID 50
categorys = ProductCategory.objects.filter(client=50, src=1, status=1)
for category in categorys:
if not ProductCategory.objects.filter(client=client, src=1, status=1, title=category.title):
pk = category.pk
new_category = category
new_category.pk = None
new_category.client = client
new_category.save()
products = Product.objects.filter(client=50, status=1, up=pk)
for product in products:
new_product = product
new_product.pk = None
new_product.client = client
new_product.up = new_category.pk
new_product.save()
rt = {'success': False, 'data': ''}
def api_default_load(request: WSGIRequest):
rt = {'success': True, 'data': ''}
return JsonResponse(rt, status=200)

@ -6,4 +6,5 @@ urlpatterns = [
path('iiko/webhook/<str:token>', iiko_webhook),
path('iiko/send-message/<str:token>', iiko_send_message),
]

@ -0,0 +1,19 @@
# ai_handler.py
from ai_agent.api_common import get_ai_agent, call_ai_agent
from max_bot.models import Client
def process_with_ai(client: Client, chat_id: str, user_query: str, contact) -> dict:
"""Вызывает AI-агента и возвращает структурированный ответ."""
ai_agent = get_ai_agent(client)
if not ai_agent.get('ai_agent'):
return None
real_name = contact.name.strip() if contact else None
response = call_ai_agent(
client=client,
session_id=str(chat_id),
user_query=user_query,
contact_name=real_name
)
return response

@ -1,305 +1,128 @@
# bot_message.py
import re
import json
import qrcode
from ai_agent.api_common import get_ai_agent, call_ai_agent, get_contact_meta
from max_bot.max_api import *
from max_bot.models import PromoCode
from max_bot.models import Client, Contact, PromoCode
from restoran_max_bot.bot_started import check_registration
from restoran_max_bot.common import *
from restoran_max_bot.iiko_api import customer_info, customer_create, customer_wallet
from restoran_max_bot.rkiper_api import rkiper_customer_info, rkiper_customer_create
from restoran_max_bot.common import common_get_data, common_set_contact, common_check_registration
from restoran_max_bot.message_sender import send_text, send_image, send_menu, send_feedback_buttons, send_booking_people_buttons, send_link_button
from restoran_max_bot.command_handlers import handle_standard_command, handle_ai_response
from restoran_max_bot.ai_handler import process_with_ai
from restoran_max_bot.settings import STATICFILES_DIRS, BASE_URL
from restoran_max_bot.utils import has_key, is_json
from restoran_max_bot.utils import 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)
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)
return True
# ---------- получение бонусов ----------
def get_bonus(client: Client, phone: str, default_bonus: float, contact: Contact):
integration = common_get_integration(client=client)
bonus = 0
date = contact.field1
if len(date) < 2:
date = '0' + date
month = contact.field2
if len(month) < 2:
month = '0' + month
if integration:
if integration.partner.slug == 'iiko':
config = json.loads(integration.setting)
customerinfo = customer_info(config['token'], phone, config['id_org'])
if not customerinfo:
birthday = f"1990-{month}-{date} 00:00:00.000"
dt = customer_create(api_key=config['token'], phone=phone, name=contact.name,
organization_id=config['id_org'], birthday=birthday)
dt = customer_info(config['token'], phone, config['id_org'])
customer_wallet(api_key=config['token'], iiko_wallet_id=dt['walletBalances'][0]['id'],
customer_id=dt['id'], organization_id=config['id_org'],
bonus=default_bonus)
bonus = default_bonus
else:
bonus = customerinfo['walletBalances'][0]['balance']
if integration.partner.slug == 'rkeeper':
config = json.loads(integration.setting)
rs = rkiper_customer_info(rkiper_server=config['url'], rkiper_token=config['token'], phone=phone)
if len(rs):
rs = rs[0]
if has_key(rs, 'card_use'):
bonus = int(rs['card_use']['sum1']) / 100
else:
birthday = f"1990-{month}-{date}"
rs = rkiper_customer_create(rkiper_server=config['url'], rkiper_token=config['token'], phone=phone,
name=contact.name, birthday=birthday, bonus=config['bonus'],
discount=config['discount'])
bonus = 0
return bonus
# ---------- получение вложений ----------
def get_attachments(message: dict):
if 'attachments' in message['message']['body']:
if 'vcf_info' in message['message']['body']['attachments'][0]['payload']:
match = re.search(r'TEL;TYPE=cell:(\d+)',
message['message']['body']['attachments'][0]['payload']['vcf_info'])
if match:
phone_number = match.group(1)
return {'attachments_type': 'vcf_info', 'data': phone_number}
return {'attachments_type': False, 'data': ''}
# ---------- основной обработчик ----------
def bot_message(client: Client, message: dict, settings_max: dict, message_type):
def bot_message(client: Client, message: dict, settings_max: dict, message_type: str) -> bool:
"""
Основной обработчик входящих сообщений от Max бота.
"""
chat_id = message['message']['recipient']['chat_id']
attachments = get_attachments(message=message)
token = settings_max['token']
# регистрация по vcf
if attachments['attachments_type'] == 'vcf_info':
common_set_contact(client=client, uid_client=chat_id, name=message['message']['sender']['name'],
phone=attachments['data'])
# 1. Обработка вложений (контакт из vcf)
attachments = _get_attachments(message)
if attachments['type'] == 'vcf_info':
common_set_contact(
client=client,
uid_client=chat_id,
name=message['message']['sender']['name'],
phone=attachments['data']
)
# Подменяем текст на системную команду регистрации
message['message']['body']['text'] = 'system_registration'
# 2. Проверка регистрации
contact = check_registration(client=client, chat_id=chat_id, message=message, settings=settings_max)
if not contact:
return False
# ---------- проверка и запрос имени ----------
meta = get_contact_meta(contact)
# ---------- обработка стандартных команд (меню, бонусы и т.п.) ----------
data = None
if message_type == 'message_created':
data = common_get_data(client=client, key=str(message['message']['body']['text']).lower())
elif message_type == 'message_callback':
data_payload = json.loads(message['callback']['payload'])
data = common_get_data(client=client, key=str(data_payload['data']).lower())
if data:
for dt in data:
if dt.title == '##bonus##':
img = qrcode.make(contact.phone)
img.save(f"{STATICFILES_DIRS[0]}/client_qr/{contact.phone}.png")
url = f"https://maxbot.telefon-ip.ru/static/client_qr/{contact.phone}.png"
maxbot_send_img_message(chat_id=chat_id, max_token=settings_max['token'], img=url,
message='QR-код для начисления, списания бонусов.')
bonus = get_bonus(client=client, phone=contact.phone, default_bonus=float(dt.price), contact=contact)
maxbot_send_text_message(chat_id=chat_id, max_token=settings_max['token'],
message=f"💰 Ваш бонусный баланс: {bonus} руб")
elif dt.title == '##feedback##':
maxbot_send_feedback_button(client=client, message='👇', chat_id=chat_id,
max_token=settings_max['token'])
# 3. Получение текста запроса
user_query = _extract_query(message, message_type)
if not user_query:
send_menu(chat_id, token, client)
return True
elif dt.title == '##booking##':
maxbot_send_booking_count_people_button(client=client, message='👇',
chat_id=chat_id, max_token=settings_max['token'])
# 4. Обработка стандартных команд (меню, бонусы и т.п.)
data = _get_command_data(client, message, message_type)
if data:
handled = handle_standard_command(client, chat_id, token, data, contact)
if handled:
return True
# сложные кнопки
if is_json(dt.title):
data_json = json.loads(dt.title)
if 'type' in dt.title and data_json.get('type') == 'linkbutton':
maxbot_send_link_button(text=dt.descr, title_button=data_json['button'], link=dt.url,
chat_id=chat_id, max_token=settings_max['token'])
if dt.img:
url = f"https://cdn.telefon-ip.ru/{dt.img}?thumb=600"
maxbot_send_img_message(chat_id=chat_id, max_token=settings_max['token'], img=url, message=dt.descr)
if '##' not in dt.title and not dt.img and not is_json(dt.title):
maxbot_send_text_message(chat_id=chat_id, max_token=settings_max['token'], message=dt.descr)
if dt.url and not is_json(dt.title):
maxbot_send_text_message(chat_id=chat_id, max_token=settings_max['token'], message=dt.url)
maxbot_send_menu_button(chat_id=chat_id, max_token=settings_max['token'], message='👇 Выберите раздел',
client=client)
# 5. Если не стандартная команда — пробуем AI
response = process_with_ai(client, chat_id, user_query, contact)
if response:
handle_ai_response(client, chat_id, token, response, contact)
send_menu(chat_id, token, client)
return True
else:
# если при нажатии на кнопку небыл найден контент, тогда для AI передаем имя кнопки
if message_type == 'message_callback':
message['message']['body']['text'] = str(data_payload['data']).lower()
# ---------- обработка через AI ----------
# Проверяем, есть ли AI-агент у клиента
ai_agent = get_ai_agent(client)
if not ai_agent.get('ai_agent'):
maxbot_send_menu_button(chat_id=chat_id, max_token=settings_max['token'],
message='👇 Выберите раздел', client=client)
# 6. Если AI недоступен — просто меню
send_menu(chat_id, token, client)
return True
real_name = contact.name.strip()
# Вызываем AI-агента через универсальную функцию, передавая реальное имя
response = call_ai_agent(
client=client,
session_id=str(chat_id),
user_query=message['message']['body']['text'],
contact_name=real_name # передаём имя для персонализации
)
# ---------- обработка ответа AI ----------
intent = response.get('intent')
message_text = response.get('message', '')
entities = response.get('entities', {})
status = response.get('status', 'complete')
if intent == 'general':
maxbot_send_text_message(chat_id, settings_max['token'], message_text)
def _get_attachments(message: dict) -> dict:
"""Извлекает информацию о вложениях (например, vCard)."""
attachments = message.get('message', {}).get('body', {}).get('attachments', [])
for att in attachments:
payload = att.get('payload', {})
if 'vcf_info' in payload:
match = re.search(r'TEL;TYPE=cell:(\d+)', payload['vcf_info'])
if match:
return {'type': 'vcf_info', 'data': match.group(1)}
return {'type': None, 'data': ''}
elif intent == 'booking':
if status == 'need_more_info':
maxbot_send_text_message(chat_id, settings_max['token'], message_text)
elif status == 'complete':
date = entities.get('date')
time = entities.get('time')
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)
if success:
confirm_msg = (f"✅ Бронирование на {date} в {time} на {people} чел. принято! "
f"Администратор свяжется с вами.")
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)
elif intent == 'contact_admin':
if status == 'need_more_info':
maxbot_send_text_message(chat_id, settings_max['token'], message_text)
elif status == 'complete':
phone = entities.get('phone')
contact_method = entities.get('contact_method')
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)
if success:
confirm_msg = f"✅ Ваш запрос передан администратору. Способ связи: {contact_method}. Скоро с вами свяжутся."
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)
def _extract_query(message: dict, message_type: str) -> str:
"""Извлекает текст запроса из сообщения или callback'а."""
if message_type == 'message_created':
return message.get('message', {}).get('body', {}).get('text', '')
elif message_type == 'message_callback':
payload = message.get('callback', {}).get('payload', '{}')
try:
data = json.loads(payload)
return data.get('data', '')
except json.JSONDecodeError:
return ''
return ''
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)
def _get_command_data(client: Client, message: dict, message_type: str):
"""Получает данные команды (контент) по ключу."""
key = _extract_query(message, message_type)
if not key:
return None
return common_get_data(client=client, key=str(key).lower())
# всегда отправляем меню после ответа
maxbot_send_menu_button(chat_id=chat_id, max_token=settings_max['token'],
message='👇 Выберите раздел', client=client)
return True
# ==================== bot_promo ====================
def bot_promo(client: Client, message: dict, settings_max: dict):
def bot_promo(client: Client, message: dict, settings_max: dict) -> bool:
"""Обработчик промокодов (QR-коды)."""
chat_id = message['chat_id']
promo = message['payload'].lower()
messenger = 'max-bot'
client_id = client.id
# Проверяем, существует ли промокод в системе (через common_get_data)
data = common_get_data(client=client, key=promo)
if not data:
# Если промокод не заведён – ничего не отправляем (скрываем факт)
return True
# Пытаемся найти или создать запись в БД
promo_obj, created = PromoCode.objects.get_or_create(
promo=promo,
chat_id=chat_id,
client=client,
defaults={
'messenger': messenger,
'used': False,
}
defaults={'messenger': messenger, 'used': False}
)
# Если запись уже существовала
if not created:
if promo_obj.used:
# Уже использован – уведомляем
maxbot_send_text_message(
chat_id=chat_id,
max_token=settings_max['token'],
message="⚠️ Этот промокод уже был использован."
)
send_text(chat_id, settings_max['token'], "⚠️ Этот промокод уже был использован.")
else:
# QR-код уже был отправлен ранее – напоминаем
maxbot_send_text_message(
chat_id=chat_id,
max_token=settings_max['token'],
message="ℹ️ QR-код уже был отправлен."
)
send_text(chat_id, settings_max['token'], "ℹ️ QR-код уже был отправлен.")
return True
# Если запись создана впервые – генерируем QR-код
# Генерируем QR
base_url = f"{BASE_URL}/promo/status/"
url_promo = f"{base_url}{promo}/{chat_id}/{client_id}"
img = qrcode.make(url_promo)
@ -307,21 +130,16 @@ def bot_promo(client: Client, message: dict, settings_max: dict):
img.save(f"{STATICFILES_DIRS[0]}/client_qr/{file_name}")
qr_url = f"{BASE_URL}/static/client_qr/{file_name}"
maxbot_send_img_message(
chat_id=chat_id,
max_token=settings_max['token'],
img=qr_url,
message='Ваш QR-код.'
)
# Отправляем дополнительный контент, если есть
for dt in data:
if dt.img:
img_url = f"https://cdn.telefon-ip.ru/{dt.img}?thumb=600"
maxbot_send_img_message(chat_id=chat_id, max_token=settings_max['token'], img=img_url, message=dt.descr)
if dt.title:
maxbot_send_text_message(chat_id=chat_id, max_token=settings_max['token'], message=dt.descr)
if dt.url:
maxbot_send_text_message(chat_id=chat_id, max_token=settings_max['token'], message=dt.url)
send_image(chat_id, settings_max['token'], qr_url, 'Ваш QR-код.')
# Отправляем дополнительный контент
for item in data:
if item.img:
img_url = f"https://cdn.telefon-ip.ru/{item.img}?thumb=600"
send_image(chat_id, settings_max['token'], img_url, item.descr)
if item.title and not item.img:
send_text(chat_id, settings_max['token'], item.descr)
if item.url:
send_text(chat_id, settings_max['token'], item.url)
return True

@ -0,0 +1,240 @@
# command_handlers.py
import json
import qrcode
from max_bot.models import Client, Contact
from restoran_max_bot.common import common_get_data, common_get_integration
from restoran_max_bot.iiko_api import customer_info, customer_create, customer_wallet
from restoran_max_bot.rkiper_api import rkiper_customer_info, rkiper_customer_create
from restoran_max_bot.message_sender import send_text, send_image, send_menu, send_feedback_buttons, send_booking_people_buttons, send_link_button
from restoran_max_bot.settings import STATICFILES_DIRS, BASE_URL
from restoran_max_bot.utils import is_json
from restoran_max_bot.bot_started import check_registration
def handle_standard_command(client: Client, chat_id: str, token: str, data, contact: Contact) -> bool:
"""Обрабатывает стандартные команды (меню, бонусы, отзывы, бронирование).
Возвращает True, если команда была обработана, иначе False."""
if not data:
return False
for item in data:
# Бонусы
if item.title == '##bonus##':
_handle_bonus(client, chat_id, token, contact)
continue
# Отзыв
if item.title == '##feedback##':
send_feedback_buttons(chat_id, token, client)
return True
# Бронирование
if item.title == '##booking##':
send_booking_people_buttons(chat_id, token, client)
return True
# Кнопка-ссылка
if is_json(item.title):
data_json = json.loads(item.title)
if data_json.get('type') == 'linkbutton':
send_link_button(chat_id, token, item.descr, data_json['button'], item.url)
continue
# Изображение
if item.img:
url = f"https://cdn.telefon-ip.ru/{item.img}?thumb=600"
send_image(chat_id, token, url, item.descr)
# Текст (если не специальный тег и нет картинки)
if '##' not in item.title and not item.img and not is_json(item.title):
send_text(chat_id, token, item.descr)
# URL
if item.url and not is_json(item.title):
send_text(chat_id, token, item.url)
send_menu(chat_id, token, client)
return True
def _handle_bonus(client: Client, chat_id: str, token: str, contact: Contact) -> None:
"""Обработка команды бонусов: генерация QR и запрос баланса."""
# Генерируем QR-код
img = qrcode.make(contact.phone)
img.save(f"{STATICFILES_DIRS[0]}/client_qr/{contact.phone}.png")
qr_url = f"https://maxbot.telefon-ip.ru/static/client_qr/{contact.phone}.png"
send_image(chat_id, token, qr_url, 'QR-код для начисления, списания бонусов.')
# Получаем баланс
bonus = _get_bonus(client, contact.phone, 0.0, contact)
send_text(chat_id, token, f"💰 Ваш бонусный баланс: {bonus} руб")
def _get_bonus(client: Client, phone: str, default_bonus: float, contact: Contact) -> float:
"""Получает бонусный баланс из интеграции (IIKO или R-Keeper)."""
integration = common_get_integration(client=client)
if not integration:
return 0.0
date = contact.field1 or '01'
month = contact.field2 or '01'
if len(date) < 2:
date = '0' + date
if len(month) < 2:
month = '0' + month
config = json.loads(integration.setting)
if integration.partner.slug == 'iiko':
customerinfo = customer_info(config['token'], phone, config['id_org'])
if not customerinfo:
birthday = f"1990-{month}-{date} 00:00:00.000"
customer_create(api_key=config['token'], phone=phone, name=contact.name,
organization_id=config['id_org'], birthday=birthday)
customerinfo = customer_info(config['token'], phone, config['id_org'])
if customerinfo:
customer_wallet(api_key=config['token'],
iiko_wallet_id=customerinfo['walletBalances'][0]['id'],
customer_id=customerinfo['id'],
organization_id=config['id_org'],
bonus=default_bonus)
return default_bonus
else:
return customerinfo['walletBalances'][0]['balance']
elif integration.partner.slug == 'rkeeper':
rs = rkiper_customer_info(config['url'], config['token'], phone)
if rs:
rs = rs[0]
if 'card_use' in rs:
return int(rs['card_use']['sum1']) / 100
else:
birthday = f"1990-{month}-{date}"
rkiper_customer_create(config['url'], config['token'], phone, contact.name,
birthday, config['discount'], config['bonus'])
return 0.0
def handle_ai_response(client: Client, chat_id: str, token: str, response: dict, contact: Contact) -> None:
"""Обрабатывает ответ от AI-агента и выполняет соответствующие действия."""
intent = response.get('intent')
message_text = response.get('message', '')
entities = response.get('entities', {})
status = response.get('status', 'complete')
if intent == 'general':
send_text(chat_id, token, message_text)
elif intent == 'booking':
_handle_booking_intent(chat_id, token, client, response, contact)
elif intent == 'contact_admin':
_handle_contact_admin_intent(chat_id, token, client, response, contact)
elif intent == 'feedback':
_handle_feedback_intent(chat_id, token, client, response, contact)
else:
send_text(chat_id, token, message_text)
def _handle_booking_intent(chat_id: str, token: str, client: Client, response: dict, contact: Contact) -> None:
"""Обрабатывает намерение 'booking'."""
message_text = response.get('message', '')
status = response.get('status', 'complete')
entities = response.get('entities', {})
if status == 'need_more_info':
send_text(chat_id, token, message_text)
return
if status == 'complete':
date = entities.get('date')
time = entities.get('time')
people = entities.get('people')
phone = entities.get('phone')
if date and time and people:
success = _send_booking_to_crm(client, date, time, people, phone, contact.name)
if success:
send_text(chat_id, token, f"✅ Бронирование на {date} в {time} на {people} чел. принято! Администратор свяжется с вами.")
else:
send_text(chat_id, token, "❌ Не удалось забронировать. Попробуйте позже.")
else:
send_text(chat_id, token, "⚠️ Не хватает данных для бронирования.")
else:
send_text(chat_id, token, message_text)
def _handle_contact_admin_intent(chat_id: str, token: str, client: Client, response: dict, contact: Contact) -> None:
"""Обрабатывает намерение 'contact_admin'."""
message_text = response.get('message', '')
status = response.get('status', 'complete')
entities = response.get('entities', {})
if status == 'need_more_info':
send_text(chat_id, token, message_text)
return
if status == 'complete':
phone = entities.get('phone')
contact_method = entities.get('contact_method')
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, contact.name)
if success:
send_text(chat_id, token, f"✅ Ваш запрос передан администратору. Способ связи: {contact_method}. Скоро с вами свяжутся.")
else:
send_text(chat_id, token, "❌ Не удалось отправить запрос. Попробуйте позже.")
else:
send_text(chat_id, token, "⚠️ Не хватает данных для связи с администратором.")
else:
send_text(chat_id, token, message_text)
def _handle_feedback_intent(chat_id: str, token: str, client: Client, response: dict, contact: Contact) -> None:
"""Обрабатывает намерение 'feedback'."""
message_text = response.get('message', '')
status = response.get('status', 'complete')
entities = response.get('entities', {})
if status == 'need_more_info':
send_text(chat_id, token, message_text)
return
if 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, contact.name, contact.phone)
if success:
send_text(chat_id, token, "✅ Спасибо за ваш отзыв! Мы учтём его.")
else:
send_text(chat_id, token, "❌ Не удалось отправить отзыв. Попробуйте позже.")
else:
send_text(chat_id, token, "⚠️ Не хватает данных для отзыва.")
else:
send_text(chat_id, token, message_text)
def _send_booking_to_crm(client: Client, date, time, people, phone, name) -> bool:
"""Отправляет бронирование в CRM/менеджерам."""
from restoran_max_bot.common import send_message_all_manager
msg = f'Бронирование столика:\nНа дату: {date}-{time} \nКоличество: {people}\nКонтакт: {phone}\nИмя: {name}'
return send_message_all_manager(client, msg)
def _send_contact_to_admin(client: Client, phone, contact_method, question, preferred_time, name) -> bool:
"""Отправляет запрос на связь с администратором."""
from restoran_max_bot.common import send_message_all_manager
msg = (f'Гость попросил связаться:\n {contact_method}\nВопрос: {question}\n'
f'Удобное время: {preferred_time}\nКонтакт: {phone}\nИмя: {name}')
return send_message_all_manager(client, msg)
def _send_feedback_to_crm(client: Client, rating, feedback_text, name, phone) -> bool:
"""Отправляет отзыв в CRM/менеджерам."""
from restoran_max_bot.common import send_message_all_manager
msg = f'Отзыв от {name} (тел. {phone}):\nОценка: {rating}/5\nТекст: {feedback_text}'
return send_message_all_manager(client, msg)

@ -0,0 +1,34 @@
# message_sender.py
from max_bot.max_api import maxbot_send_text_message, maxbot_send_img_message, maxbot_send_menu_button, maxbot_send_feedback_button, maxbot_send_booking_count_people_button, maxbot_send_link_button
from max_bot.models import Client
def send_text(chat_id: str, token: str, text: str) -> None:
"""Отправить текстовое сообщение."""
if text:
maxbot_send_text_message(chat_id=chat_id, max_token=token, message=text)
def send_image(chat_id: str, token: str, image_url: str, caption: str = '') -> None:
"""Отправить сообщение с изображением."""
maxbot_send_img_message(chat_id=chat_id, max_token=token, img=image_url, message=caption)
def send_menu(chat_id: str, token: str, client: Client, text: str = '👇 Выберите раздел') -> None:
"""Отправить меню с кнопками."""
maxbot_send_menu_button(chat_id=chat_id, max_token=token, message=text, client=client)
def send_feedback_buttons(chat_id: str, token: str, client: Client, text: str = '👇') -> None:
"""Отправить кнопки для оценки."""
maxbot_send_feedback_button(client=client, message=text, chat_id=chat_id, max_token=token)
def send_booking_people_buttons(chat_id: str, token: str, client: Client, text: str = '👇') -> None:
"""Отправить кнопки выбора количества гостей."""
maxbot_send_booking_count_people_button(client=client, message=text, chat_id=chat_id, max_token=token)
def send_link_button(chat_id: str, token: str, text: str, button_title: str, url: str) -> None:
"""Отправить сообщение с кнопкой-ссылкой."""
maxbot_send_link_button(text=text, title_button=button_title, link=url, chat_id=chat_id, max_token=token)

@ -1,10 +1,16 @@
import os.path
import sys
from pathlib import Path
from environ import environ
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Создаём папку для логов, если её нет
LOG_DIR = os.path.join(BASE_DIR, 'log')
if not os.path.exists(LOG_DIR):
os.makedirs(LOG_DIR)
env = environ.Env()
env_file = f'{BASE_DIR}/.env'
env.read_env(env_file=env_file)
@ -76,6 +82,19 @@ WSGI_APPLICATION = 'restoran_max_bot.wsgi.application'
# Database
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
# Если запускаются тесты, используем SQLite in-memory для изоляции
# и отключаем сериализацию данных, чтобы не пытаться копировать данные из существующей БД
if 'test' in sys.argv:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ':memory:',
'TEST': {
'SERIALIZE': False, # не копировать данные из основной БД
}
}
}
else:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
@ -133,3 +152,58 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
LOG_SYSTEM_PROMPT = False # False - не логировать системный промт
PROMO_ADMIN_PIN = os.environ.get('PROMO_ADMIN_PIN', '1111')
# ---------- Настройка логирования ошибок ----------
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}',
'style': '{',
},
'simple': {
'format': '{levelname} {asctime} {module} {message}',
'style': '{',
},
},
'handlers': {
'file': {
'level': 'ERROR',
'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': os.path.join(BASE_DIR, 'log', 'error.log'),
'when': 'midnight',
'interval': 1,
'backupCount': 30,
'formatter': 'verbose',
},
},
'loggers': {
'django': {
'handlers': ['file'],
'level': 'ERROR',
'propagate': True,
},
'django.request': {
'handlers': ['file'],
'level': 'ERROR',
'propagate': False,
},
'django.server': {
'handlers': ['file'],
'level': 'ERROR',
'propagate': False,
},
# Можно добавить логгеры для своих приложений, если нужно
# 'max_bot': {
# 'handlers': ['file'],
# 'level': 'ERROR',
# 'propagate': False,
# },
# 'ai_agent': {
# 'handlers': ['file'],
# 'level': 'ERROR',
# 'propagate': False,
# },
},
}

@ -0,0 +1,370 @@
# restoran_max_bot/tests.py
import json
import unittest
from unittest.mock import Mock, patch, MagicMock
from django.test import TestCase
from max_bot.models import Client, Contact, Product, ProductCategory, PromoCode
from restoran_max_bot.bot_message import bot_message, bot_promo, _get_attachments, _extract_query, _get_command_data
from restoran_max_bot.message_sender import send_text, send_image, send_menu
from restoran_max_bot.command_handlers import handle_standard_command, handle_ai_response
from restoran_max_bot.ai_handler import process_with_ai
class BotMessageTestCase(TestCase):
"""Тесты для функции bot_message и её вспомогательных функций."""
def setUp(self):
# Создаём реального клиента в БД
self.client = Client.objects.create(
id=1,
token='test_token',
status=True,
title='Test Client',
code='001',
brand='TestBrand',
logo='',
server='test',
managment='test'
)
# Настройки Max
self.settings_max = {'token': 'max_token'}
# Создаём контакт (можно использовать Mock, но лучше реальный объект)
self.contact = Contact.objects.create(
client=self.client,
name='Test User',
phone='79001234567',
field1='01',
field2='01',
maxx='chat123',
status=1,
up=0,
group=1
)
# Базовое сообщение
self.message_created = {
'message': {
'recipient': {'chat_id': 'chat123'},
'sender': {'name': 'Test User'},
'body': {'text': 'menu'}
},
'update_type': 'message_created'
}
self.message_callback = {
'message': {
'recipient': {'chat_id': 'chat123'},
'sender': {'name': 'Test User'},
'body': {}
},
'callback': {
'payload': json.dumps({'data': 'bonus'})
},
'update_type': 'message_callback'
}
# ---------- Тесты вспомогательных функций ----------
def test_get_attachments_vcf(self):
"""Проверка извлечения номера из vCard."""
message = {
'message': {
'body': {
'attachments': [
{'payload': {'vcf_info': 'TEL;TYPE=cell:79161234567'}}
]
}
}
}
result = _get_attachments(message)
self.assertEqual(result['type'], 'vcf_info')
self.assertEqual(result['data'], '79161234567')
def test_get_attachments_no_vcf(self):
"""Нет вложений или нет vcf."""
message = {'message': {'body': {}}}
result = _get_attachments(message)
self.assertIsNone(result['type'])
self.assertEqual(result['data'], '')
def test_extract_query_message_created(self):
"""Извлечение текста из обычного сообщения."""
query = _extract_query(self.message_created, 'message_created')
self.assertEqual(query, 'menu')
def test_extract_query_message_callback(self):
"""Извлечение данных из callback."""
query = _extract_query(self.message_callback, 'message_callback')
self.assertEqual(query, 'bonus')
def test_extract_query_empty(self):
"""Пустой запрос."""
message = {'message': {'body': {}}}
query = _extract_query(message, 'message_created')
self.assertEqual(query, '')
@patch('restoran_max_bot.bot_message.common_get_data')
def test_get_command_data(self, mock_get_data):
"""Получение данных команды."""
mock_get_data.return_value = [Mock(title='menu')]
result = _get_command_data(self.client, self.message_created, 'message_created')
self.assertIsNotNone(result)
mock_get_data.assert_called_with(client=self.client, key='menu')
# ---------- Тесты основной функции bot_message ----------
@patch('restoran_max_bot.bot_message.check_registration')
@patch('restoran_max_bot.bot_message.handle_standard_command')
@patch('restoran_max_bot.bot_message.process_with_ai')
@patch('restoran_max_bot.bot_message.handle_ai_response')
@patch('restoran_max_bot.bot_message.send_menu')
@patch('restoran_max_bot.bot_message.common_set_contact')
@patch('restoran_max_bot.bot_message.common_get_data')
def test_bot_message_vcf_registration(self, mock_get_data, mock_set_contact, mock_send_menu,
mock_handle_ai, mock_process_ai,
mock_handle_std, mock_check_reg):
"""Обработка vCard -> регистрация -> вызов check_registration."""
mock_get_data.return_value = None # чтобы не искало команду
# Сообщение с vcf
message = {
'message': {
'recipient': {'chat_id': 'chat123'},
'sender': {'name': 'Vasya'},
'body': {
'attachments': [
{'payload': {'vcf_info': 'TEL;TYPE=cell:79161234567'}}
]
}
},
'update_type': 'message_created'
}
mock_check_reg.return_value = self.contact
mock_handle_std.return_value = False
mock_process_ai.return_value = None
result = bot_message(self.client, message, self.settings_max, 'message_created')
self.assertTrue(result)
mock_set_contact.assert_called_once()
mock_check_reg.assert_called_once()
mock_send_menu.assert_called_once()
@patch('restoran_max_bot.bot_message.check_registration')
@patch('restoran_max_bot.bot_message.handle_standard_command')
@patch('restoran_max_bot.bot_message.send_menu')
@patch('restoran_max_bot.bot_message.common_get_data')
def test_bot_message_standard_command(self, mock_get_data, mock_send_menu, mock_handle_std, mock_check_reg):
"""Обработка стандартной команды (меню)."""
mock_get_data.return_value = [Mock(title='menu')] # имитируем наличие данных
mock_check_reg.return_value = self.contact
mock_handle_std.return_value = True # команда обработана
result = bot_message(self.client, self.message_created, self.settings_max, 'message_created')
self.assertTrue(result)
mock_handle_std.assert_called_once()
mock_send_menu.assert_not_called()
@patch('restoran_max_bot.bot_message.check_registration')
@patch('restoran_max_bot.bot_message.handle_standard_command')
@patch('restoran_max_bot.bot_message.process_with_ai')
@patch('restoran_max_bot.bot_message.handle_ai_response')
@patch('restoran_max_bot.bot_message.send_menu')
@patch('restoran_max_bot.bot_message.common_get_data')
def test_bot_message_ai_handling(self, mock_get_data, mock_send_menu, mock_handle_ai, mock_process_ai,
mock_handle_std, mock_check_reg):
"""Обработка через AI, когда нет стандартной команды."""
mock_get_data.return_value = None # нет стандартной команды
mock_check_reg.return_value = self.contact
mock_handle_std.return_value = False
mock_process_ai.return_value = {'intent': 'general', 'message': 'Hello'}
result = bot_message(self.client, self.message_created, self.settings_max, 'message_created')
self.assertTrue(result)
mock_process_ai.assert_called_once()
mock_handle_ai.assert_called_once()
mock_send_menu.assert_called_once()
@patch('restoran_max_bot.bot_message.check_registration')
def test_bot_message_not_registered(self, mock_check_reg):
"""Пользователь не зарегистрирован -> выход."""
mock_check_reg.return_value = None
result = bot_message(self.client, self.message_created, self.settings_max, 'message_created')
self.assertFalse(result)
# ---------- Тесты bot_promo ----------
def test_bot_promo_new(self):
"""Создание нового промокода."""
with patch('restoran_max_bot.bot_message.common_get_data') as mock_get_data, \
patch('restoran_max_bot.bot_message.PromoCode.objects.get_or_create') as mock_get_or_create, \
patch('restoran_max_bot.bot_message.qrcode.make') as mock_qr, \
patch('restoran_max_bot.bot_message.send_image') as mock_send_image, \
patch('restoran_max_bot.bot_message.send_text') as mock_send_text:
mock_get_data.return_value = [Mock(img='', title='', url='')]
mock_get_or_create.return_value = (Mock(used=False), True) # created=True
mock_qr.return_value = Mock()
message = {'chat_id': 'chat123', 'payload': 'promo123'}
result = bot_promo(self.client, message, self.settings_max)
self.assertTrue(result)
mock_send_image.assert_called_once()
mock_send_text.assert_not_called()
@patch('restoran_max_bot.bot_message.common_get_data')
def test_bot_promo_no_data(self, mock_get_data):
"""Промокод не найден в системе."""
mock_get_data.return_value = None
message = {'chat_id': 'chat123', 'payload': 'unknown'}
result = bot_promo(self.client, message, self.settings_max)
self.assertTrue(result)
# Ничего не отправляем
class CommandHandlersTestCase(TestCase):
"""Тесты для command_handlers.py"""
def setUp(self):
self.client = Client.objects.create(
id=2,
token='test2',
status=True,
title='Test Client 2',
code='002',
brand='TestBrand2',
logo='',
server='test',
managment='test'
)
self.contact = Contact.objects.create(
client=self.client,
name='Test',
phone='79001234567',
maxx='chat123',
status=1,
up=0,
group=1
)
self.chat_id = 'chat123'
self.token = 'token'
@patch('restoran_max_bot.command_handlers.send_menu')
@patch('restoran_max_bot.command_handlers.send_text')
def test_handle_standard_command_bonus(self, mock_send_text, mock_send_menu):
from restoran_max_bot.command_handlers import handle_standard_command
mock_item = Mock()
mock_item.title = '##bonus##'
mock_item.img = ''
mock_item.descr = ''
mock_item.url = ''
with patch('restoran_max_bot.command_handlers._handle_bonus') as mock_handle_bonus:
result = handle_standard_command(self.client, self.chat_id, self.token,
[mock_item], self.contact)
self.assertTrue(result)
mock_handle_bonus.assert_called_once()
mock_send_menu.assert_called_once()
@patch('restoran_max_bot.command_handlers.send_feedback_buttons')
@patch('restoran_max_bot.command_handlers.send_menu')
def test_handle_standard_command_feedback(self, mock_send_menu, mock_send_feedback):
from restoran_max_bot.command_handlers import handle_standard_command
mock_item = Mock()
mock_item.title = '##feedback##'
result = handle_standard_command(self.client, self.chat_id, self.token,
[mock_item], self.contact)
self.assertTrue(result)
mock_send_feedback.assert_called_once()
mock_send_menu.assert_not_called() # feedback возвращает True сразу
@patch('restoran_max_bot.command_handlers.send_booking_people_buttons')
@patch('restoran_max_bot.command_handlers.send_menu')
def test_handle_standard_command_booking(self, mock_send_menu, mock_send_booking):
from restoran_max_bot.command_handlers import handle_standard_command
mock_item = Mock()
mock_item.title = '##booking##'
result = handle_standard_command(self.client, self.chat_id, self.token,
[mock_item], self.contact)
self.assertTrue(result)
mock_send_booking.assert_called_once()
mock_send_menu.assert_not_called()
@patch('restoran_max_bot.command_handlers.send_link_button')
@patch('restoran_max_bot.command_handlers.send_menu')
def test_handle_standard_command_linkbutton(self, mock_send_menu, mock_send_link):
from restoran_max_bot.command_handlers import handle_standard_command
mock_item = Mock()
mock_item.title = json.dumps({'type': 'linkbutton', 'button': 'Click'})
mock_item.descr = 'Text'
mock_item.url = 'http://example.com'
result = handle_standard_command(self.client, self.chat_id, self.token,
[mock_item], self.contact)
self.assertTrue(result)
mock_send_link.assert_called_once()
mock_send_menu.assert_called_once()
class AiHandlerTestCase(TestCase):
"""Тесты для ai_handler.py"""
def setUp(self):
self.client = Client.objects.create(
id=3,
token='test3',
status=True,
title='Test Client 3',
code='003',
brand='TestBrand3',
logo='',
server='test',
managment='test'
)
@patch('restoran_max_bot.ai_handler.get_ai_agent')
@patch('restoran_max_bot.ai_handler.call_ai_agent')
def test_process_with_ai_available(self, mock_call, mock_get):
from restoran_max_bot.ai_handler import process_with_ai
mock_get.return_value = {'ai_agent': 'deepseek'}
mock_call.return_value = {'intent': 'general'}
contact = Mock()
contact.name = 'Test'
result = process_with_ai(self.client, 'chat123', 'hello', contact)
self.assertEqual(result, {'intent': 'general'})
mock_call.assert_called_once()
@patch('restoran_max_bot.ai_handler.get_ai_agent')
def test_process_with_ai_not_available(self, mock_get):
from restoran_max_bot.ai_handler import process_with_ai
mock_get.return_value = {'ai_agent': None}
result = process_with_ai(self.client, 'chat123', 'hello', None)
self.assertIsNone(result)
class MessageSenderTestCase(TestCase):
"""Тесты для message_sender.py (проверка вызовов API)."""
def setUp(self):
self.client = Client.objects.create(
id=4,
token='test4',
status=True,
title='Test Client 4',
code='004',
brand='TestBrand4',
logo='',
server='test',
managment='test'
)
@patch('restoran_max_bot.message_sender.maxbot_send_text_message')
def test_send_text(self, mock_send):
from restoran_max_bot.message_sender import send_text
send_text('chat', 'token', 'text')
mock_send.assert_called_once_with(chat_id='chat', max_token='token', message='text')
@patch('restoran_max_bot.message_sender.maxbot_send_img_message')
def test_send_image(self, mock_send):
from restoran_max_bot.message_sender import send_image
send_image('chat', 'token', 'http://img', 'caption')
mock_send.assert_called_once_with(chat_id='chat', max_token='token', img='http://img', message='caption')
@patch('restoran_max_bot.message_sender.maxbot_send_menu_button')
def test_send_menu(self, mock_send):
from restoran_max_bot.message_sender import send_menu
send_menu('chat', 'token', self.client, 'text')
mock_send.assert_called_once_with(chat_id='chat', max_token='token', message='text', client=self.client)
Loading…
Cancel
Save