v1.8.2: AGENTS.md, consent revocation, delete bot users, dialogues fix, intent improvements, CI for max_bot
This commit is contained in:
@@ -43,6 +43,7 @@ async def handle_consent_response(user_id: int, conv_id: int, text: str) -> None
|
||||
"Пожалуйста, дайте чёткий ответ — даёте ли вы согласие на обработку "
|
||||
"персональных данных? Это необходимо для обработки вашего запроса.",
|
||||
attachments=consent_keyboard(),
|
||||
conversation_id=conv_id,
|
||||
)
|
||||
|
||||
|
||||
@@ -70,6 +71,7 @@ async def handle_consent_yes(user_id: int, conv_id: int) -> None:
|
||||
"связаться с вами.\n\n"
|
||||
"Нажмите кнопку «Поделиться контактом» или введите номер телефона / email вручную.",
|
||||
attachments=contact_keyboard(),
|
||||
conversation_id=conv_id,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send consent_yes message: {e}")
|
||||
@@ -97,6 +99,7 @@ async def handle_consent_no(user_id: int, conv_id: int) -> None:
|
||||
f"Вы можете позвонить нам:\n{phone_text}\n\n"
|
||||
f"📧 {email}\n\n"
|
||||
f"Если передумаете и захотите дать согласие — просто напишите об этом.",
|
||||
conversation_id=conv_id,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send consent_no message: {e}")
|
||||
@@ -120,6 +123,7 @@ async def handle_refused_again(user_id: int, conv_id: int, text: str) -> None:
|
||||
f"Вы можете позвонить нам:\n{phone_text}\n\n"
|
||||
f"📧 {email}\n\n"
|
||||
f"Если передумаете — напишите «Даю согласие».",
|
||||
conversation_id=conv_id,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send refused_again message: {e}")
|
||||
@@ -134,6 +138,23 @@ async def handle_refused_again(user_id: int, conv_id: int, text: str) -> None:
|
||||
f"Вы можете позвонить нам:\n{phone_text}\n\n"
|
||||
f"📧 {email}\n\n"
|
||||
f"Если передумаете — напишите «Даю согласие».",
|
||||
conversation_id=conv_id,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send refused_again message: {e}")
|
||||
|
||||
|
||||
async def handle_revoke_consent(user_id: int, conv_id: int) -> None:
|
||||
async with async_session() as db:
|
||||
user = await db.get(BotUser, user_id)
|
||||
if user and user.consent_given:
|
||||
user.consent_given = False
|
||||
from app.handlers.greeting import moscow_now
|
||||
user.consent_revoked_date = moscow_now()
|
||||
await db.commit()
|
||||
await max_api.send_message(
|
||||
user_id,
|
||||
"Понял вас. Ваше согласие на обработку персональных данных отозвано. "
|
||||
"Если передумаете — напишите «Даю согласие».",
|
||||
conversation_id=conv_id,
|
||||
)
|
||||
|
||||
@@ -58,6 +58,7 @@ async def handle_manual_contact(user_id: int, conv_id: int, text: str) -> None:
|
||||
user_id,
|
||||
"Пожалуйста, введите корректный номер телефона (например, +7 (861) 203-33-30) "
|
||||
"или адрес электронной почты. Или напишите «Отмена» чтобы вернуться назад.",
|
||||
conversation_id=conv_id,
|
||||
)
|
||||
|
||||
|
||||
@@ -66,6 +67,7 @@ async def _ask_inquiry(user_id: int, conv: BotConversation) -> None:
|
||||
user_id,
|
||||
"Опишите, пожалуйста, ваш вопрос или задачу. "
|
||||
"Расскажите подробнее, чем мы можем вам помочь.",
|
||||
conversation_id=conv.id,
|
||||
)
|
||||
|
||||
|
||||
@@ -76,4 +78,5 @@ async def _ask_inquiry_with_context(user_id: int, conv: BotConversation) -> None
|
||||
f"Ранее вы упомянули, что хотите оставить {intent_label}. "
|
||||
"Опишите подробнее суть вашего обращения, чтобы я могла передать "
|
||||
"информацию специалисту.",
|
||||
conversation_id=conv.id,
|
||||
)
|
||||
|
||||
@@ -33,8 +33,6 @@ async def handle_greeting(
|
||||
f"Чем я могу быть вам полезна?"
|
||||
)
|
||||
|
||||
await max_api.send_message(user_id, greeting_text)
|
||||
|
||||
async with async_session() as db:
|
||||
existing = await db.execute(
|
||||
select(BotUser).where(BotUser.id == user_id)
|
||||
@@ -92,6 +90,8 @@ async def handle_greeting(
|
||||
await db.commit()
|
||||
await db.refresh(conv)
|
||||
|
||||
await max_api.send_message(user_id, greeting_text, conversation_id=conv.id)
|
||||
|
||||
|
||||
async def handle_message(user_id: int, text: str) -> None:
|
||||
async with async_session() as db:
|
||||
@@ -107,11 +107,24 @@ async def handle_message(user_id: int, text: str) -> None:
|
||||
await handle_greeting(user_id, text)
|
||||
return
|
||||
|
||||
now = moscow_now()
|
||||
stale_threshold = now - datetime.timedelta(minutes=10)
|
||||
|
||||
if conv.created_at and conv.created_at < stale_threshold:
|
||||
conv = BotConversation(
|
||||
user_id=user_id,
|
||||
state="awaiting_input",
|
||||
created_at=now,
|
||||
)
|
||||
db.add(conv)
|
||||
await db.commit()
|
||||
await db.refresh(conv)
|
||||
|
||||
msg = BotMessage(
|
||||
conversation_id=conv.id,
|
||||
direction="incoming",
|
||||
text=text,
|
||||
created_at=moscow_now(),
|
||||
created_at=now,
|
||||
)
|
||||
db.add(msg)
|
||||
await db.commit()
|
||||
@@ -135,9 +148,16 @@ async def handle_message(user_id: int, text: str) -> None:
|
||||
f"{phone_text}\n"
|
||||
f"📧 {email}\n\n"
|
||||
"Мы обязательно ответим и поможем!",
|
||||
conversation_id=conv.id,
|
||||
)
|
||||
return
|
||||
|
||||
revoke_words = ["отозвать согласие", "отозвать", "отменить согласие"]
|
||||
if any(w in text_lower for w in revoke_words):
|
||||
from app.handlers.consent import handle_revoke_consent
|
||||
await handle_revoke_consent(user_id, conv.id)
|
||||
return
|
||||
|
||||
if state in ("greeting", "analyzing", "awaiting_input"):
|
||||
await analyze_and_respond(user_id, conv.id, text)
|
||||
elif state in ("awaiting_consent", "consent_refused", "awaiting_contact"):
|
||||
@@ -175,27 +195,19 @@ async def handle_message(user_id: int, text: str) -> None:
|
||||
from app.handlers.inquiry import handle_inquiry
|
||||
await handle_inquiry(user_id, conv.id, text)
|
||||
elif state in ("completed", "escalated"):
|
||||
async with async_session() as db:
|
||||
conv_new = BotConversation(
|
||||
user_id=user_id,
|
||||
state="analyzing",
|
||||
created_at=moscow_now(),
|
||||
)
|
||||
db.add(conv_new)
|
||||
await db.commit()
|
||||
await db.refresh(conv_new)
|
||||
await analyze_and_respond(user_id, conv_new.id, text)
|
||||
await analyze_and_respond(user_id, conv.id, text)
|
||||
|
||||
|
||||
async def _get_user_history_context(user_id: int) -> str:
|
||||
from app.models import BotTicket
|
||||
async with async_session() as db:
|
||||
ten_min_ago = moscow_now() - datetime.timedelta(minutes=10)
|
||||
recent_msgs = await db.execute(
|
||||
select(BotMessage)
|
||||
.join(BotConversation)
|
||||
.where(BotConversation.user_id == user_id)
|
||||
.where(BotMessage.created_at >= ten_min_ago)
|
||||
.order_by(BotMessage.id.desc())
|
||||
.limit(10)
|
||||
)
|
||||
messages = list(reversed(recent_msgs.scalars().all()))
|
||||
|
||||
@@ -233,11 +245,53 @@ async def _select_ai():
|
||||
async def analyze_and_respond(user_id: int, conv_id: int, text: str) -> None:
|
||||
try:
|
||||
logger.info(f"analyze_and_respond: user={user_id} conv={conv_id} text={text[:80]}")
|
||||
ai = await _select_ai()
|
||||
logger.info(f"analyze_and_respond: ai={ai}")
|
||||
|
||||
text_lower = text.strip().lower()
|
||||
|
||||
ticket_quick = [
|
||||
"записаться", "заявк", "заказать", "оставить заявку", "нужна помощь",
|
||||
"хочу заказать", "хочу записаться", "хочу оставить", "оформить заявку",
|
||||
"связаться", "обратный звонок", "перезвоните", "перезвонить",
|
||||
]
|
||||
if any(p in text_lower for p in ticket_quick):
|
||||
async with async_session() as db:
|
||||
conv = await db.get(BotConversation, conv_id)
|
||||
if not conv:
|
||||
return
|
||||
conv.intent = "ticket"
|
||||
conv.state = "awaiting_consent"
|
||||
await db.commit()
|
||||
consent_text = (
|
||||
"Для обработки Вашего запроса нам нужны ваши контактные данные "
|
||||
"для обратной связи. Даёте согласие на обработку "
|
||||
"персональных данных?"
|
||||
)
|
||||
await max_api.send_message(
|
||||
user_id, consent_text, attachments=consent_keyboard(), conversation_id=conv_id
|
||||
)
|
||||
return
|
||||
|
||||
greeting_words = [
|
||||
"привет", "здравствуйте", "добрый день", "доброе утро", "добрый вечер",
|
||||
"хай", "хей", "йо", "здарова", "салют",
|
||||
]
|
||||
words_only = set(text_lower.split())
|
||||
if words_only & set(greeting_words):
|
||||
async with async_session() as db:
|
||||
conv = await db.get(BotConversation, conv_id)
|
||||
if conv:
|
||||
conv.intent = "greeting"
|
||||
await db.commit()
|
||||
assistant_name = await settings_cache.get("assistant_name", "София")
|
||||
await max_api.send_message(
|
||||
user_id,
|
||||
f"Здравствуйте! Я {assistant_name}, чем могу помочь?",
|
||||
conversation_id=conv_id,
|
||||
)
|
||||
return
|
||||
|
||||
ai = await _select_ai()
|
||||
if not ai:
|
||||
logger.info("analyze_and_respond: no AI available, limited mode")
|
||||
await _limited_mode(user_id, conv_id)
|
||||
return
|
||||
|
||||
@@ -252,19 +306,22 @@ async def analyze_and_respond(user_id: int, conv_id: int, text: str) -> None:
|
||||
conv = result.scalar_one_or_none()
|
||||
if not conv:
|
||||
return
|
||||
|
||||
prev_intent = conv.intent
|
||||
conv.intent = intent
|
||||
|
||||
if intent == "question":
|
||||
kb_context = await _get_knowledge_base_context(text)
|
||||
answer = await ai.generate_answer(text, kb_context, history_context)
|
||||
if answer:
|
||||
await max_api.send_message(user_id, answer)
|
||||
await max_api.send_message(user_id, answer, conversation_id=conv_id)
|
||||
else:
|
||||
await _save_unknown_question(user_id, text)
|
||||
await max_api.send_message(user_id,
|
||||
"По данному вопросу рекомендую связаться с нашим специалистом "
|
||||
"для получения исчерпывающего ответа. "
|
||||
"Контакты: +7 (861) 203-33-30, mail@aegisone.ru"
|
||||
"Контакты: +7 (861) 203-33-30, mail@aegisone.ru",
|
||||
conversation_id=conv_id,
|
||||
)
|
||||
conv.state = "completed"
|
||||
|
||||
@@ -276,19 +333,29 @@ async def analyze_and_respond(user_id: int, conv_id: int, text: str) -> None:
|
||||
"персональных данных?"
|
||||
)
|
||||
await max_api.send_message(
|
||||
user_id, consent_text, attachments=consent_keyboard()
|
||||
user_id, consent_text, attachments=consent_keyboard(), conversation_id=conv_id
|
||||
)
|
||||
|
||||
else:
|
||||
clarification = await ai.clarify_intent(text, history_context)
|
||||
await max_api.send_message(user_id, clarification)
|
||||
conv.state = "awaiting_input"
|
||||
if prev_intent == "unknown":
|
||||
kb_context = await _get_knowledge_base_context(text)
|
||||
answer = await ai.generate_answer(text, kb_context, history_context)
|
||||
if answer:
|
||||
await max_api.send_message(user_id, answer, conversation_id=conv_id)
|
||||
conv.state = "completed"
|
||||
else:
|
||||
await _send_contacts(user_id, conv_id)
|
||||
conv.state = "completed"
|
||||
else:
|
||||
clarification = await ai.clarify_intent(text, history_context)
|
||||
await max_api.send_message(user_id, clarification, conversation_id=conv_id)
|
||||
conv.state = "awaiting_input"
|
||||
|
||||
await db.commit()
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"analyze_and_respond FAILED: {e}")
|
||||
await _send_contacts(user_id)
|
||||
await _send_contacts(user_id, conv_id)
|
||||
|
||||
|
||||
async def _limited_mode(user_id: int, conv_id: int) -> None:
|
||||
@@ -301,10 +368,10 @@ async def _limited_mode(user_id: int, conv_id: int) -> None:
|
||||
conv.state = "completed"
|
||||
await db.commit()
|
||||
|
||||
await _send_contacts(user_id)
|
||||
await _send_contacts(user_id, conv_id)
|
||||
|
||||
|
||||
async def _send_contacts(user_id: int) -> None:
|
||||
async def _send_contacts(user_id: int, conv_id: int = None) -> None:
|
||||
phones = config_reader.get_phones()
|
||||
email = config_reader.get_site_mail()
|
||||
|
||||
@@ -316,7 +383,7 @@ async def _send_contacts(user_id: int) -> None:
|
||||
f"📧 {email}"
|
||||
)
|
||||
|
||||
await max_api.send_message(user_id, message)
|
||||
await max_api.send_message(user_id, message, conversation_id=conv_id)
|
||||
|
||||
|
||||
async def _get_knowledge_base_context(query: str) -> str:
|
||||
|
||||
@@ -28,6 +28,7 @@ async def handle_callback(user_id: int, callback_id: str, callback_data: dict) -
|
||||
await max_api.send_message(
|
||||
user_id,
|
||||
"Извините, я не распознала действие. Пожалуйста, воспользуйтесь кнопками.",
|
||||
conversation_id=conv_id,
|
||||
)
|
||||
|
||||
|
||||
@@ -89,4 +90,5 @@ async def handle_contact_share(
|
||||
"Спасибо! Ваш контакт получен.\n\n"
|
||||
"Опишите, пожалуйста, суть вашего обращения — "
|
||||
"расскажите подробнее, чем мы можем вам помочь.",
|
||||
conversation_id=conv_id,
|
||||
)
|
||||
|
||||
@@ -30,6 +30,7 @@ async def handle_inquiry(user_id: int, conv_id: int, text: str) -> None:
|
||||
await max_api.send_message(
|
||||
user_id,
|
||||
"Произошла ошибка при обработке запроса. Пожалуйста, попробуйте ещё раз позже.",
|
||||
conversation_id=conv_id,
|
||||
)
|
||||
return
|
||||
|
||||
@@ -111,6 +112,7 @@ async def _finalize_ticket_in_tx(
|
||||
f"Зафиксировала: {inquiry_text}\n\n"
|
||||
f"Ваша заявка отправлена инженеру. В ближайшее время он обязательно "
|
||||
f"свяжется с вами.",
|
||||
conversation_id=conv.id,
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user