v1.7.0: refactor max_bot to flat structure, add VCF+UserModel+NLP history context, portal pages and proxy fixes
- Refactored max_bot from nested packages to flat module structure - Q2: Extended BotUser model (patronymic, email, org, address, vcf_raw, contact_hash, phone_verified, email_verified, last_interaction, total_conversations, total_tickets) - Q2: VCF parser (FN, N, TEL, EMAIL, ORG, ADR), upsert on re-contact, NLP history context (_get_user_history_context -> YandexGPT) - Q1: Broadcast preview modal with 10s confirmation timer - Q3: CSS var(--white)->var(--bg-card), var(--text)->var(--text-primary) - Q4: bot_settings showNotification(), editable max_bot_id - Q5: Webhook secret passthrough via X-Max-Bot-Api-Secret - Masking sensitive keys, dialog_cleared handler, migrate via _add_column_if_not_exists() - Rate limit (asyncio.sleep 0.5 per 10), dead code removed, conv.intent context in contact.py - Portal pages: bot_consent, bot_kb (edit), bot_settings, bot_test, bot_tickets, portal_settings - Tests: 21/21 passing, added test_yandex_gpt.py, test_email_sender.py - Deploy: deploy_full.sh, schema.sql, seed_knowledge_base.sql
This commit is contained in:
@@ -1,43 +1,77 @@
|
||||
import datetime
|
||||
import re
|
||||
from app.settings_cache import SettingsCache
|
||||
from app.conversation import get_or_create_user, get_template_rendered
|
||||
from app.fsm import BotState, FSM
|
||||
import logging
|
||||
from sqlalchemy import select
|
||||
from app.database import async_session
|
||||
from app.models import BotUser, BotConversation, BotMessage
|
||||
from app.max_api import max_api
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PHONE_PATTERN = re.compile(r"^(\+7|8|7)?[\s\-]?\(?\d{3}\)?[\s\-]?\d{3}[\s\-]?\d{2}[\s\-]?\d{2}$")
|
||||
EMAIL_PATTERN = re.compile(r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$")
|
||||
|
||||
|
||||
async def handle_contact_provided(bot, user_data: dict, db, max_api):
|
||||
max_user_id = user_data["user_id"]
|
||||
user = await get_or_create_user(db, max_user_id)
|
||||
async def handle_contact_received(user_id: int, conv_id: int, contact_text: str) -> None:
|
||||
async with async_session() as db:
|
||||
result = await db.execute(select(BotUser).where(BotUser.id == user_id))
|
||||
user = result.scalar_one_or_none()
|
||||
if user:
|
||||
if PHONE_PATTERN.match(contact_text):
|
||||
user.phone = contact_text
|
||||
elif EMAIL_PATTERN.match(contact_text):
|
||||
user.phone = contact_text
|
||||
|
||||
phone = user_data.get("phone", "")
|
||||
email = user_data.get("email", "")
|
||||
result = await db.execute(
|
||||
select(BotConversation).where(BotConversation.id == conv_id)
|
||||
)
|
||||
conv = result.scalar_one_or_none()
|
||||
if conv:
|
||||
conv.state = "awaiting_inquiry"
|
||||
if conv.intent == "unknown" or not conv.intent:
|
||||
await _ask_inquiry(user_id, conv)
|
||||
else:
|
||||
await _ask_inquiry_with_context(user_id, conv)
|
||||
|
||||
if phone:
|
||||
user.phone = phone
|
||||
if email:
|
||||
user.email = email
|
||||
await db.flush()
|
||||
|
||||
text = await get_template_rendered(db, "inquiry_request")
|
||||
await max_api.send_message(max_user_id, text, format="markdown")
|
||||
FSM.set_state(max_user_id, BotState.INQUIRY_REQUEST)
|
||||
await db.commit()
|
||||
|
||||
|
||||
async def handle_contact_text(bot, user_data: dict, db, max_api):
|
||||
max_user_id = user_data["user_id"]
|
||||
text = user_data.get("text", "").strip()
|
||||
user = await get_or_create_user(db, max_user_id)
|
||||
async def handle_manual_contact(user_id: int, conv_id: int, text: str) -> None:
|
||||
text = text.strip()
|
||||
|
||||
phone_match = re.search(r'(\+?7[\s\-\(]?\d{3}[\s\-\)]?\d{3}[\s\-]?\d{2}[\s\-]?\d{2})', text)
|
||||
email_match = re.search(r'([\w.-]+@[\w.-]+\.\w+)', text)
|
||||
async with async_session() as db:
|
||||
msg = BotMessage(
|
||||
conversation_id=conv_id,
|
||||
direction="incoming",
|
||||
text=text,
|
||||
created_at=datetime.datetime.utcnow(),
|
||||
)
|
||||
db.add(msg)
|
||||
await db.commit()
|
||||
|
||||
if phone_match:
|
||||
user.phone = phone_match.group(1)
|
||||
if email_match:
|
||||
user.email = email_match.group(1)
|
||||
if PHONE_PATTERN.match(text) or EMAIL_PATTERN.match(text):
|
||||
await handle_contact_received(user_id, conv_id, text)
|
||||
else:
|
||||
await max_api.send_message(
|
||||
user_id,
|
||||
"Пожалуйста, введите корректный номер телефона (например, +7 (861) 203-33-30) "
|
||||
"или адрес электронной почты.",
|
||||
)
|
||||
|
||||
if phone_match or email_match:
|
||||
await db.flush()
|
||||
await handle_contact_provided(bot, user_data, db, max_api)
|
||||
return
|
||||
|
||||
await max_api.send_message(max_user_id, "Пожалуйста, укажите номер телефона или email, чтобы мы могли с вами связаться.")
|
||||
async def _ask_inquiry(user_id: int, conv: BotConversation) -> None:
|
||||
await max_api.send_message(
|
||||
user_id,
|
||||
"Опишите, пожалуйста, ваш вопрос или задачу. "
|
||||
"Расскажите подробнее, чем мы можем вам помочь.",
|
||||
)
|
||||
|
||||
|
||||
async def _ask_inquiry_with_context(user_id: int, conv: BotConversation) -> None:
|
||||
intent_label = "вопрос" if conv.intent == "question" else "заявку на услугу" if conv.intent == "ticket" else "обращение"
|
||||
await max_api.send_message(
|
||||
user_id,
|
||||
f"Ранее вы упомянули, что хотите оставить {intent_label}. "
|
||||
"Опишите подробнее суть вашего обращения, чтобы я могла передать "
|
||||
"информацию специалисту.",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user