v1.8.3: добавлены docstrings и комментарии по AGENTS.md ко всем ключевым файлам
This commit is contained in:
@@ -1,3 +1,11 @@
|
||||
"""
|
||||
Обработчик согласия на обработку персональных данных.
|
||||
|
||||
Управляет жизненным циклом согласия:
|
||||
- запрос согласия → да/нет
|
||||
- отзыв согласия (consent_revoked_date)
|
||||
- повторное согласие после отказа
|
||||
"""
|
||||
import datetime
|
||||
import re
|
||||
import logging
|
||||
@@ -12,6 +20,12 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _matches_any(text: str, words: list) -> bool:
|
||||
"""
|
||||
Проверяет, содержит ли текст хотя бы одно из указанных слов.
|
||||
|
||||
Использует split для точного совпадения коротких слов (≤3 символов)
|
||||
и подстроку для длинных (>3 символов).
|
||||
"""
|
||||
text_words = set(text.split())
|
||||
for w in words:
|
||||
if w in text_words:
|
||||
@@ -22,6 +36,7 @@ def _matches_any(text: str, words: list) -> bool:
|
||||
|
||||
|
||||
async def handle_consent_callback(user_id: int, conv_id: int, callback_id: str) -> None:
|
||||
"""Маршрутизация callback-кнопок согласия (да/нет)."""
|
||||
if callback_id == "consent_yes":
|
||||
await handle_consent_yes(user_id, conv_id)
|
||||
elif callback_id == "consent_no":
|
||||
@@ -29,6 +44,7 @@ async def handle_consent_callback(user_id: int, conv_id: int, callback_id: str)
|
||||
|
||||
|
||||
async def handle_consent_response(user_id: int, conv_id: int, text: str) -> None:
|
||||
"""Обработка текстового ответа на запрос согласия (да/нет/другое)."""
|
||||
text_lower = text.strip().lower()
|
||||
consent_words = ["да", "даю", "согласен", "согласна", "yes", "ok", "хорошо", "даю согласие"]
|
||||
refuse_words = ["нет", "не даю", "отказ", "no", "не согласен", "не согласна"]
|
||||
@@ -48,6 +64,7 @@ async def handle_consent_response(user_id: int, conv_id: int, text: str) -> None
|
||||
|
||||
|
||||
async def handle_consent_yes(user_id: int, conv_id: int) -> None:
|
||||
"""Обработка согласия: устанавливает consent_given=True, переводит в awaiting_contact."""
|
||||
async with async_session() as db:
|
||||
result = await db.execute(select(BotUser).where(BotUser.id == user_id))
|
||||
user = result.scalar_one_or_none()
|
||||
@@ -78,6 +95,7 @@ async def handle_consent_yes(user_id: int, conv_id: int) -> None:
|
||||
|
||||
|
||||
async def handle_consent_no(user_id: int, conv_id: int) -> None:
|
||||
"""Обработка отказа: устанавливает consent_refused, показывает контакты."""
|
||||
async with async_session() as db:
|
||||
result = await db.execute(
|
||||
select(BotConversation).where(BotConversation.id == conv_id)
|
||||
@@ -106,6 +124,7 @@ async def handle_consent_no(user_id: int, conv_id: int) -> None:
|
||||
|
||||
|
||||
async def handle_refused_again(user_id: int, conv_id: int, text: str) -> None:
|
||||
"""Повторная обработка после отказа: позволяет дать согласие или показывает контакты."""
|
||||
text_lower = text.strip().lower()
|
||||
consent_words = ["да", "даю", "согласен", "согласна", "yes", "ok", "хорошо", "даю согласие", "передумал", "согласен дать"]
|
||||
refuse_words = ["нет", "не даю", "не хочу", "отказ", "отмена", "no", "cancel", "я отказался"]
|
||||
@@ -145,6 +164,7 @@ async def handle_refused_again(user_id: int, conv_id: int, text: str) -> None:
|
||||
|
||||
|
||||
async def handle_revoke_consent(user_id: int, conv_id: int) -> None:
|
||||
"""Отзыв согласия: устанавливает consent_given=False, consent_revoked_date=now."""
|
||||
async with async_session() as db:
|
||||
user = await db.get(BotUser, user_id)
|
||||
if user and user.consent_given:
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
"""
|
||||
Обработчик контактов пользователя.
|
||||
|
||||
Принимает контакты (телефон, email) через:
|
||||
- sharing контакта из мессенджера (VCF)
|
||||
- ручной ввод текстом
|
||||
"""
|
||||
import datetime
|
||||
import re
|
||||
import logging
|
||||
@@ -13,6 +20,7 @@ EMAIL_PATTERN = re.compile(r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$")
|
||||
|
||||
|
||||
async def handle_contact_received(user_id: int, conv_id: int, contact_text: str) -> None:
|
||||
"""Обработка контакта из мессенджера: парсинг телефона/email, переход к заявке."""
|
||||
async with async_session() as db:
|
||||
result = await db.execute(select(BotUser).where(BotUser.id == user_id))
|
||||
user = result.scalar_one_or_none()
|
||||
@@ -37,6 +45,7 @@ async def handle_contact_received(user_id: int, conv_id: int, contact_text: str)
|
||||
|
||||
|
||||
async def handle_manual_contact(user_id: int, conv_id: int, text: str) -> None:
|
||||
"""Обработка ручного ввода контакта: валидация телефона/email, отказ → consent_refused."""
|
||||
text = text.strip()
|
||||
text_lower = text.lower()
|
||||
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
"""
|
||||
Обработчик приветствий и основных сообщений бота.
|
||||
|
||||
Управляет жизненным циклом диалога:
|
||||
- приветствие → создание пользователя и диалога
|
||||
- маршрутизация по состояниям (state machine)
|
||||
- анализ намерений через YandexGPT
|
||||
- быстрые паттерны (заявка/приветствие) до NLP
|
||||
- контекст из последних 10 минут
|
||||
"""
|
||||
import logging
|
||||
import datetime
|
||||
from typing import Optional
|
||||
@@ -7,6 +17,7 @@ from app.database import async_session
|
||||
MOSCOW_TZ = datetime.timezone(datetime.timedelta(hours=3))
|
||||
|
||||
def moscow_now():
|
||||
"""Возвращает текущее время в UTC+3 (Москва) без tzinfo."""
|
||||
return datetime.datetime.now(MOSCOW_TZ).replace(tzinfo=None)
|
||||
from app.models import BotUser, BotConversation, BotMessage, BotKnowledgeBase, BotCategory, BotUnknownQuestion
|
||||
from app.max_api import max_api
|
||||
@@ -26,6 +37,7 @@ async def handle_greeting(
|
||||
last_name: str = "",
|
||||
username: str = "",
|
||||
) -> None:
|
||||
"""Обработка приветствия: создаёт/обновляет пользователя, создаёт диалог, отправляет приветствие."""
|
||||
assistant_name = await settings_cache.get("assistant_name", "София")
|
||||
|
||||
greeting_text = (
|
||||
@@ -94,6 +106,14 @@ async def handle_greeting(
|
||||
|
||||
|
||||
async def handle_message(user_id: int, text: str) -> None:
|
||||
"""
|
||||
Основной обработчик сообщений. Маршрутизирует по состояниям:
|
||||
- greeting/analyzing/awaiting_input → analyze_and_respond
|
||||
- awaiting_consent/consent_refused → обработка согласия
|
||||
- awaiting_contact → ручной ввод контакта
|
||||
- awaiting_inquiry → обработка заявки
|
||||
- completed/escalated → повторный анализ (escape hatch)
|
||||
"""
|
||||
async with async_session() as db:
|
||||
result = await db.execute(
|
||||
select(BotConversation)
|
||||
@@ -199,6 +219,7 @@ async def handle_message(user_id: int, text: str) -> None:
|
||||
|
||||
|
||||
async def _get_user_history_context(user_id: int) -> str:
|
||||
"""Формирует контекст из последних сообщений (10 мин) и открытых заявок пользователя."""
|
||||
from app.models import BotTicket
|
||||
async with async_session() as db:
|
||||
ten_min_ago = moscow_now() - datetime.timedelta(minutes=10)
|
||||
@@ -237,12 +258,20 @@ async def _get_user_history_context(user_id: int) -> str:
|
||||
|
||||
|
||||
async def _select_ai():
|
||||
"""Выбор AI-провайдера (только YandexGPT)."""
|
||||
if await yandex_gpt.is_available():
|
||||
return yandex_gpt
|
||||
return None
|
||||
|
||||
|
||||
async def analyze_and_respond(user_id: int, conv_id: int, text: str) -> None:
|
||||
"""
|
||||
Анализ намерений и генерация ответа.
|
||||
|
||||
Быстрые паттерны (заявка/приветствие) проверяются ПЕРЕД NLP.
|
||||
Затем: analyze_intent → ticket/question/unknown → обработка.
|
||||
clarify_intent вызывается не более 1 раза за диалог.
|
||||
"""
|
||||
try:
|
||||
logger.info(f"analyze_and_respond: user={user_id} conv={conv_id} text={text[:80]}")
|
||||
|
||||
@@ -359,6 +388,7 @@ async def analyze_and_respond(user_id: int, conv_id: int, text: str) -> None:
|
||||
|
||||
|
||||
async def _limited_mode(user_id: int, conv_id: int) -> None:
|
||||
"""Режим без AI: показывает контакты компании."""
|
||||
async with async_session() as db:
|
||||
result = await db.execute(
|
||||
select(BotConversation).where(BotConversation.id == conv_id)
|
||||
@@ -372,6 +402,7 @@ async def _limited_mode(user_id: int, conv_id: int) -> None:
|
||||
|
||||
|
||||
async def _send_contacts(user_id: int, conv_id: int = None) -> None:
|
||||
"""Отправка контактных данных компании (телефон, email)."""
|
||||
phones = config_reader.get_phones()
|
||||
email = config_reader.get_site_mail()
|
||||
|
||||
@@ -387,6 +418,7 @@ async def _send_contacts(user_id: int, conv_id: int = None) -> None:
|
||||
|
||||
|
||||
async def _get_knowledge_base_context(query: str) -> str:
|
||||
"""Формирует контекст из базы знаний по ключевым словам (ранжирование по score)."""
|
||||
async with async_session() as db:
|
||||
result = await db.execute(
|
||||
select(BotKnowledgeBase)
|
||||
@@ -433,6 +465,7 @@ async def _get_knowledge_base_context(query: str) -> str:
|
||||
|
||||
|
||||
def _texts_match(text_a: str, text_b: str) -> bool:
|
||||
"""Проверка схожести двух текстов по пересечению слов (≥50%)."""
|
||||
stopwords = {"какой", "какая", "какие", "какое", "ваш", "ваша", "ваши",
|
||||
"где", "когда", "сколько", "что", "это", "или", "для", "при",
|
||||
"подскажите", "расскажите", "объясните", "пожалуйста"}
|
||||
@@ -445,6 +478,7 @@ def _texts_match(text_a: str, text_b: str) -> bool:
|
||||
|
||||
|
||||
async def _auto_generate_and_add_kb(db, question_record):
|
||||
"""Автогенерация карточки KB через YandexGPT при 3+ обращениях."""
|
||||
card_data = await yandex_gpt.generate_kb_card([question_record.question_text])
|
||||
if not card_data:
|
||||
return
|
||||
@@ -466,6 +500,7 @@ async def _auto_generate_and_add_kb(db, question_record):
|
||||
|
||||
|
||||
async def _save_unknown_question(user_id: int, question_text: str) -> None:
|
||||
"""Сохранение неизвестного вопроса с дедупликацией и автогенерацией при 3+ обращениях."""
|
||||
normalized_input = question_text.lower().strip()
|
||||
|
||||
async with async_session() as db:
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
"""
|
||||
Обработчик callback-кнопок и шаринга контактов.
|
||||
|
||||
Маршрутизирует callback-и по типу (consent_* и др.).
|
||||
"""
|
||||
import datetime
|
||||
import logging
|
||||
from sqlalchemy import select
|
||||
@@ -10,6 +15,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def handle_callback(user_id: int, callback_id: str, callback_data: dict) -> None:
|
||||
"""Маршрутизация callback-кнопок (consent_* → consent handler)."""
|
||||
async with async_session() as db:
|
||||
result = await db.execute(
|
||||
select(BotConversation)
|
||||
@@ -40,6 +46,7 @@ async def handle_contact_share(
|
||||
vcf_data: dict = None,
|
||||
contact_hash: str = "",
|
||||
) -> None:
|
||||
"""Обработка шаринга контакта из мессенджера: парсинг VCF, сохранение в BotUser."""
|
||||
if vcf_data is None:
|
||||
vcf_data = {}
|
||||
async with async_session() as db:
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
"""
|
||||
Обработчик заявок (inquiry).
|
||||
|
||||
Создаёт BotTicket с контактными данными пользователя,
|
||||
отправляет уведомление на email, подтверждает пользователю.
|
||||
"""
|
||||
import datetime
|
||||
import logging
|
||||
from sqlalchemy import select
|
||||
@@ -12,10 +18,12 @@ logger = logging.getLogger(__name__)
|
||||
MOSCOW_TZ = datetime.timezone(datetime.timedelta(hours=3))
|
||||
|
||||
def moscow_now():
|
||||
"""Возвращает текущее время в UTC+3 (Москва) без tzinfo."""
|
||||
return datetime.datetime.now(MOSCOW_TZ).replace(tzinfo=None)
|
||||
|
||||
|
||||
async def handle_inquiry(user_id: int, conv_id: int, text: str) -> None:
|
||||
"""Обработка заявки: создаёт.ticket, отправляет email, подтверждает пользователю."""
|
||||
async with async_session() as db:
|
||||
result = await db.execute(
|
||||
select(BotConversation).where(BotConversation.id == conv_id)
|
||||
|
||||
Reference in New Issue
Block a user