Files
site_aegisone/max_bot/app/bot_engine.py
T

269 lines
11 KiB
Python

import json
import logging
from typing import Optional
from app.max_api import MaxAPIClient
from app.settings_cache import SettingsCache
from app.fsm import BotState, FSM
from app.conversation import (
get_or_create_user, add_message,
get_template_rendered, find_in_knowledge_base
)
from app.utils.emotion_detector import detect_emotion
logger = logging.getLogger(__name__)
async def handle_update(bot, update: dict, db, max_api: MaxAPIClient):
update_type = update.get("update_type", "")
user_data = update.get("user", {})
max_user_id = user_data.get("user_id")
if not max_user_id:
return
await SettingsCache.refresh(db)
if update_type == "bot_started":
await _handle_bot_started(bot, user_data, db, max_api)
return
if update_type == "message_created":
await _handle_message_created(bot, user_data, update, db, max_api)
return
if update_type == "message_callback":
await _handle_callback(bot, user_data, update, db, max_api)
return
async def _handle_bot_started(bot, user_data, db, max_api):
await SettingsCache.refresh(db)
if not SettingsCache.is_work_hours():
from app.handlers.out_of_hours import handle_out_of_hours
await handle_out_of_hours(bot, user_data, db, max_api)
return
from app.handlers.greeting import handle_start
await handle_start(bot, user_data, db, max_api)
async def _handle_message_created(bot, user_data, update, db, max_api):
max_user_id = user_data["user_id"]
message = update.get("message", {})
body = message.get("body", {})
text = body.get("text", "").strip()
attachments = body.get("attachments", [])
user_data["text"] = text
user_data["ip"] = update.get("ip", "")
has_attachment = len(attachments) > 0
attachment_type = ""
attachment_path = ""
if has_attachment:
att = attachments[0]
attachment_type = att.get("type", "")
if attachment_type == "contact":
payload = att.get("payload", {})
vcf_info = payload.get("vcf_info", "")
import re
phone_match = re.search(r'TEL[^:]*:(\+?\d+)', vcf_info)
name_match = re.search(r'FN:(.+)', vcf_info)
if phone_match:
user_data["phone"] = phone_match.group(1)
if name_match:
user_data["first_name"] = name_match.group(1).strip()
await _handle_contact_shared(bot, user_data, db, max_api)
return
attachment_path = att.get("url", "")
user_data["has_attachment"] = has_attachment
user_data["attachment_type"] = attachment_type
user_data["attachment_path"] = attachment_path
state = FSM.get_state(max_user_id)
if state == BotState.IDLE:
if text.lower() in ("/start", "/help"):
if not SettingsCache.is_work_hours():
from app.handlers.out_of_hours import handle_out_of_hours
await handle_out_of_hours(bot, user_data, db, max_api)
return
from app.handlers.greeting import handle_start
await handle_start(bot, user_data, db, max_api)
return
if not SettingsCache.is_work_hours():
from app.handlers.out_of_hours import handle_out_of_hours
await handle_out_of_hours(bot, user_data, db, max_api)
return
emotion = await detect_emotion(text)
if emotion == "negative":
tmpl = await get_template_rendered(db, "emotion_escalate")
await max_api.send_message(max_user_id, tmpl)
from app.handlers.handoff import handle_handoff_request
await handle_handoff_request(bot, user_data, db, max_api)
from app.max_notifications import notify_handoff_escalation
user = await get_or_create_user(db, max_user_id)
conv = None
try:
from app.conversation import get_open_conversation
conv = await get_open_conversation(db, user.id)
except Exception:
pass
await notify_handoff_escalation(
db, max_api, user,
conv.id if conv else 0, text
)
return
kb_entry = await find_in_knowledge_base(db, text)
if kb_entry:
await max_api.send_message(max_user_id, kb_entry.answer, format="markdown")
return
from app.conversation import get_or_create_user
user = await get_or_create_user(db, max_user_id)
if not user.consent_given:
from app.handlers.greeting import handle_start
await handle_start(bot, user_data, db, max_api)
return
from app.handlers.main_menu import handle_main_menu
user_data["payload"] = "new_inquiry"
await handle_main_menu(bot, user_data, db, max_api)
return
if state == BotState.NAME_REQUEST:
if not text:
await max_api.send_message(max_user_id, "Как к вам обращаться?")
return
user = await get_or_create_user(db, max_user_id)
user.first_name = text
await db.flush()
from app.handlers.consent import handle_consent_request
await handle_consent_request(bot, user_data, db, max_api)
return
if state == BotState.CONSENT_REQUEST:
if text.lower() in ("да", "даю согласие", "согласен", "yes"):
from app.handlers.consent import handle_consent_given
await handle_consent_given(bot, user_data, db, max_api)
elif text.lower() in ("нет", "не даю", "не согласен", "no"):
from app.handlers.consent import handle_consent_refused
await handle_consent_refused(bot, user_data, db, max_api)
else:
from app.handlers.main_menu import handle_main_menu
user_data["payload"] = text.lower()
await handle_main_menu(bot, user_data, db, max_api)
return
if state == BotState.CONTACT_REQUEST:
from app.handlers.contact import handle_contact_text
await handle_contact_text(bot, user_data, db, max_api)
return
if state == BotState.INQUIRY_REQUEST:
from app.handlers.inquiry import handle_inquiry
await handle_inquiry(bot, user_data, db, max_api)
return
if state == BotState.CATEGORY_SELECT:
from app.handlers.inquiry import handle_inquiry
await handle_inquiry(bot, user_data, db, max_api)
return
if state == BotState.OUT_OF_HOURS:
from app.handlers.out_of_hours import handle_out_of_hours_callback
await handle_out_of_hours_callback(bot, user_data, db, max_api, text.lower())
return
_feature_handlers = {
BotState.FEATURE_RISK_SCORE: ("app.handlers.features.risk_score", "handle_risk_score"),
BotState.FEATURE_SLA_STATUS: ("app.handlers.features.sla_status", "handle_sla_status"),
BotState.FEATURE_PHOTO_REPORT: ("app.handlers.features.photo_report", "handle_photo_report"),
BotState.FEATURE_APPOINTMENT: ("app.handlers.features.appointment", "handle_appointment"),
BotState.FEATURE_COMMERCIAL: ("app.handlers.features.commercial", "handle_commercial"),
BotState.FEATURE_MY_OBJECTS: ("app.handlers.features.my_objects", "handle_my_objects"),
BotState.FEATURE_REMINDERS: ("app.handlers.features.reminders", "handle_reminders"),
BotState.FEATURE_QUALITY_RATE: ("app.handlers.features.quality_rate", "handle_quality_rate"),
BotState.FEATURE_NOTIFICATIONS: ("app.handlers.features.notifications", "handle_notifications"),
BotState.FEATURE_BROADCAST: ("app.handlers.features.broadcasts", "handle_broadcast_send"),
BotState.SLA_TICKET_CREATE: ("app.handlers.features.sla_ticket", "handle_sla_ticket"),
}
if state in _feature_handlers:
mod_path, func_name = _feature_handlers[state]
import importlib
mod = importlib.import_module(mod_path)
func = getattr(mod, func_name)
await func(bot, user_data, db, max_api)
return
if state == BotState.KB_SEARCH:
kb_entry = await find_in_knowledge_base(db, text)
if kb_entry:
await max_api.send_message(max_user_id, kb_entry.answer, format="markdown")
FSM.transition(max_user_id, "found")
else:
await max_api.send_message(max_user_id,
"К сожалению, не нашёл ответа на ваш вопрос. "
"Опишите подробнее или нажмите «Оставить заявку» для создания обращения.")
FSM.transition(max_user_id, "not_found")
from app.keyboards.inline import build_main_menu_buttons
buttons = build_main_menu_buttons()
menu_text = await get_template_rendered(db, "main_menu")
await max_api.send_message_with_keyboard(max_user_id, menu_text, buttons)
return
from app.handlers.main_menu import handle_main_menu
user_data["payload"] = text.lower()
await handle_main_menu(bot, user_data, db, max_api)
async def _handle_callback(bot, user_data, update, db, max_api):
max_user_id = user_data["user_id"]
callback = update.get("callback", {})
payload = callback.get("payload", "")
callback_id = callback.get("id", "")
user_data["payload"] = payload
if payload == "cancel":
FSM.reset(max_user_id)
from app.keyboards.inline import build_main_menu_buttons
buttons = build_main_menu_buttons()
menu_text = await get_template_rendered(db, "main_menu")
await max_api.send_message_with_keyboard(max_user_id, menu_text, buttons)
await max_api.answer_callback(callback_id)
return
if payload.startswith("rate_"):
from app.handlers.features.quality_rate import handle_quality_rate
await handle_quality_rate(bot, user_data, db, max_api)
await max_api.answer_callback(callback_id)
return
from app.handlers.main_menu import handle_main_menu
await handle_main_menu(bot, user_data, db, max_api)
await max_api.answer_callback(callback_id)
async def _handle_contact_shared(bot, user_data, db, max_api):
max_user_id = user_data["user_id"]
user = await get_or_create_user(db, max_user_id)
if user_data.get("phone"):
user.phone = user_data["phone"]
if user_data.get("first_name"):
user.first_name = user_data["first_name"]
await db.flush()
state = FSM.get_state(max_user_id)
if state == BotState.CONTACT_REQUEST:
from app.handlers.contact import handle_contact_provided
await handle_contact_provided(bot, user_data, db, max_api)
else:
from app.handlers.main_menu import handle_main_menu
user_data["payload"] = "new_inquiry"
await handle_main_menu(bot, user_data, db, max_api)