v1.8.3: добавлены docstrings и комментарии по AGENTS.md ко всем ключевым файлам

This commit is contained in:
2026-06-02 18:53:21 +03:00
parent 4c3026a80f
commit a7a39bdd5d
19 changed files with 212 additions and 0 deletions
+35
View File
@@ -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: