Решение
Собрали backend на FastAPI с двумя входными точками: новым /api/chat (лимит 10 запросов в минуту на IP через slowapi) и прежним /webhook — его оставили, чтобы не трогать уже работающий виджет на сайте. CORS ограничен доменом сайта клиента.
Логика ответа одинакова для обоих эндпоинтов. Сообщение пользователя проверяется на тематическую релевантность — с учётом истории диалога: если хотя бы одно из прошлых сообщений пользователя было по теме, текущий запрос тоже считается релевантным. Если вопрос точно не по теме, бот вежливо отвечает, что помогает только с волонтёрством, не обращаясь к LLM. Если по теме — сервис базы знаний ищет наиболее похожий вопрос в загруженной таблице; при уверенном совпадении найденная запись (категория, заголовок, вопрос, ответ, ссылка) передаётся в GigaChat как контекст, и модель формулирует живой ответ на её основе. Если совпадения нет, GigaChat отвечает из общих знаний, но всё ещё в рамках темы, заданной системным промптом. В ответе может быть ссылка на материал-источник.
Бот поддерживает историю диалога, которую присылает фронтенд (роль и текст предыдущих сообщений), — это позволяет учитывать контекст при ответе на уточняющие вопросы.
База знаний загружается при старте приложения и обновляется фоновой задачей каждые 10 минут — изменения в таблице на Яндекс.Диске подхватываются без перезапуска и без деплоя. Токен GigaChat (OAuth2) кешируется и обновляется автоматически по истечении срока действия. Ошибки валидации, превышения лимита запросов и сбоя обращения к GigaChat перехватываются глобальными хендлерами и возвращаются пользователю понятным сообщением на русском языке вместо технической ошибки.
Приложение упаковано в Docker и разворачивается через единый шаблон CI/CD компании — отдельные пайплайны для веток dev и master, выкладка на сервер через SSH и docker-compose.