widget_service
1. Назначение🔗
widget_service — это микросервис-интеграция, выступающий бэкендом для веб-виджета, встраиваемого на сторонние сайты.
Основные задачи:
- При открытии виджета на сайте, JS-клиент делает запрос на получение истории сообщений по
user_id, который генерируется на фронте. -
Отправка сообщений осуществляется в два этапа:
- POST запрос — сообщение сохраняется в кэш (со сроком жизни на всякий случай).
- GET запрос — устанавливается стриминговое соединение (через
StreamingResponse), и ответ от GPT микросервиса возвращается в реальном времени.
-
Сообщения отправляются в
data_serviceгде он сохраняет историю в основной бахе данных (Django). -
Перед обработкой любого запроса, сервис извлекает
Origin/Refererдомена и запрашивает профиль домена вdata_service.- Если профиль неактивен (деактивирован из-за баланса или вручную), обработка блокируется.
Таким образом, widget_service — это посредник между фронтендом и gpt_service, с функцией проверки прав доступа по домену и хранения истории общения.
2. Архитектура и компоненты🔗
Используемые технологии:🔗
- FastAPI — фреймворк микросервиса
- httpx — асинхронные HTTP-запросы к другим микросервисам
- cachetools.TTLCache — in-memory кэш для временного хранения входящих сообщений
Взаимодействие с другими сервисами:🔗
-
Получение данных профиля и истории сообщений:
-
Используются HTTP-запросы к
data_service:/widget/credentials— для проверки домена и получения параметров виджета/widget/get_messages— для получения истории сообщений
-
-
GPT микросервис:
- Отправка сообщения и получение стримингового ответа осуществляется через
POST /streaming_send_messageвgpt_service - Ответ проксируется напрямую клиенту через
StreamingResponse
- Отправка сообщения и получение стримингового ответа осуществляется через
Поток данных:🔗
- Клиент отправляет POST /send_message — сообщение сохраняется в оперативную память (
TTLCache) и возвращаетstream_id - Клиент устанавливает GET /streaming_process_message — по
stream_idсообщение достаётся из кэша, отправляется вgpt_service, результат возвращается в виде SSE (Server-Sent Events) - История сообщений и расход токенов отправляются в
data_serviceдля создания транзакций
3. Основные модули и директории🔗
Проект имеет следующую структуру:
WidgetGPT/
├── routers/ # Роуты FastAPI
│ └── messages.py # Основные эндпоинты: get_messages, send_message, streaming_process_message
│
├── utils/ # Вспомогательные утилиты
│ ├── data_api.py # Запросы в gpt_service и data_service (fetch, отправка сообщений)
│ ├── extract_domain.py # Извлечение домена из заголовков запроса
│ ├── get_credentials.py # Проверка домена и загрузка профиля из data_service
│ ├── msg_to_gpt.py # Формирование сообщения для OpenAI API
│ └── send_tokens.py # Отправка информации о расходе токенов в data_service
│
├── logger.py # Конфигурация логгера (на базе python-json-logger)
├── main.py # Точка входа, инициализация FastAPI и маршрутов
├── schemas.py # Pydantic-схемы: MessageRequest, WidgetCredentials и др.
├── .env # Переменные окружения
├── env.example # Пример .env файла
├── requirements.txt # Зависимости проекта
├── logs/ # Папка для логов
└── README.md # Документация по запуску
Примечания:🔗
- SQLAlchemy модели отсутствуют — сервис не работает с БД напрямую, а запрашивает и сохраняет данные через
data_service. - Все данные о виджете, профиле, сообщениях и расходах передаются по HTTP через
httpx. - Кэш реализован в оперативной памяти с помощью
cachetools.TTLCache.
Ты всё дал, ничего не надо выдумывать. Всё ясно. Подтверждаю:
- Внутренней базы у
widget_serviceнет — он вообще не работает с SQL. - Все данные о профиле и истории сообщений приходят через HTTP-запросы к
data_service. - Логгер — самописный, на основе
python-json-logger, поддерживает вывод в консоль (JSON или текст), ротацию файлов и фильтрацию Uvicorn ошибок. - Все переменные окружения используются строго по назначению.
4. Переменные окружения🔗
.env файл содержит настройки для указания адресов сервисов и конфигурации логирования.
| Переменная | Назначение |
|---|---|
WIDGET_CREDENTIALS_DOMAIN | Адрес сервиса для получения профиля виджета из data_service |
GPT_DOMAIN | Адрес сервиса для стриминга сообщений в gpt_service |
TOKENS_ADD_URL | Путь внутри data_service для сохранения информации о расходах токенов |
LOG_LEVEL | Уровень логирования (DEBUG, INFO, WARNING, и т.д.) |
USE_JSON_ONLY | Если true, лог выводится только в JSON-формате (иначе — текст) |
USE_TEXT_FILE_LOG | Если true, создаётся дополнительный текстовый лог-файл |
LOG_JSON_PATH | Путь к JSON-файлу с логами |
LOG_FILE_PATH | Путь к текстовому лог-файлу |
MAX_LOG_SIZE_MB | Максимальный размер лог-файла в мегабайтах (ротация при превышении) |
UVICORN_ERROR_LOG_PATH | Путь к отдельному файлу логов ошибок uvicorn.error |
Все параметры логирования конфигурируются в
logger.py. Логгер поддерживает форматирование, ротацию, сериализацию ошибок и защиту от конфликтов имён при логированииextraданных.
5. Взаимодействие с другими сервисами🔗
Микросервис widget_service взаимодействует с двумя другими компонентами системы:
gpt_service— генерация ответов на сообщения пользователя;data_service— валидация запросов на основании домена, получение учётных данных профиля.
Общая схема работы🔗
- При открытии виджета JS на сайте отправляет
GET-запрос на/get_messages, передаваяuser_id. - Сервис проверяет заголовки
OriginилиReferer, извлекает домен, и делаетGET-запрос кdata_serviceдля полученияWidgetCredentialsпо этому домену. - Если профиль существует, активен и домен разрешён — возвращается история сообщений.
-
Отправка сообщения реализована в два этапа:
- POST
/send_message— сообщение сохраняется во внутренний кэш используяTTLCacheизcachetools, генерируется, сохраняя и возвращаяstream_id. - GET
/streaming_process_message?stream_id=...— запускает стриминговый процесс взаимодействия с GPT и транслирует ответ в форматеtext/event-stream.
- POST
Связь с GPT-сервисом🔗
Для генерации ответа вызывается метод proxy_gpt_stream, который отправляет запрос в gpt_service. В теле запроса:
assistant_key,open_ai_key— из данных профиля;user_id,message,sender,request_id— из запроса;messageпередаётся в формате OpenAI API:
Ответ от gpt_service возвращается в потоковом формате Server-Sent Events (SSE):
{ "type": "chunk", "data": "..." }— фрагменты текста;{ "type": "final", ... }— финальный блок с мета-данными.
После финального блока:
- создаётся транзакция (вызывается
send_tokens); - сохраняется сообщение GPT в базу данных (
send_message_to_db).
Валидация домена🔗
Любой внешний запрос проходит проверку:
- Извлекается домен из
Origin/Referer. - Выполняется запрос к
data_service(/widget/credentials?domain=...). - Проверяется наличие профиля и его
is_activeстатус. - Если проверка не пройдена — возвращается 403.
Домен — это единственный идентификатор, доступный со стороны браузера.
Активность профиля🔗
- Проверка активности виджета происходит при любом запросе.
Запрос к data_service🔗
Выполняется через httpx.AsyncClient:
url = f"{CRED_DOMAIN}/widget/credentials"
response = await client.get(url, params={"domain": domain})
Формат ответа:
{
"base_profile": {
"id": 1,
"name": "profile_name",
"open_ai_assistant_key": "...",
"open_ai_key": "...",
"is_active": true,
"user": {
"balance_cached": "100.00"
}
},
"welcome_text": "Добро пожаловать",
"start_hints": ["Как дела?", "Что ты умеешь?"],
"widget_left": true,
"tg_token": null,
"manager_tg_id": [],
"icon_url": null
}
6. Обработка данных и логика🔗
Сервис реализует следующий цикл обработки входящих данных:
Проверка доступа🔗
- Все внешние запросы проходят обязательную проверку домена через
validate_domain_take_credentials(). - Извлекается заголовок
OriginилиReferer. Если заголовок отсутствует — возвращается 403. -
Домен проверяется через
data_service. Если:- профиль не найден — 403 "Неразрешённый домен",
- профиль неактивен (
is_active=False) — 403 "Виджет не активирован".
Все этапы проверки логируются.
Отправка и обработка сообщений🔗
- POST /send_message — сообщение сохраняется в
TTLCacheсstream_id. -
GET /streaming_process_message:
- Достаёт данные из кэша. Если не найдено — 404 "Заявка не найдена".
- Запускается поток к
gpt_serviceчерезproxy_gpt_stream()(POST-запрос, SSE-ответ). - Чанки (
chunk) пересылаются напрямую клиенту. -
После получения
finalчанка:- отправляется запрос
send_tokens()— сохраняются метрики; - вызывается
send_message_to_db()— сохраняется ответ.
- отправляется запрос
Обработка ошибок🔗
- Ошибка домена / профиля — немедленный отказ с 403.
- Отсутствие
stream_id— 404. -
Ошибки при взаимодействии с
gpt_service:- Установлены таймауты (connect: 5s, read: 60s, write: 10s).
- Ошибки соединения приведут к 500 от FastAPI.
- Невалидные чанки (
JSONDecodeError) — игнорируются.
-
Ошибки в
send_tokens()иsend_message_to_db():- Логируются через
log.error(...) - Исключения не поднимаются, выполнение продолжается.
- Логируются через
Поток может завершиться без
finalчанка, еслиgpt_serviceоборвёт соединение. Обработка этого случая возложена на фронт.
7. API🔗
Базовый префикс всех эндпоинтов: /api/v1/widget
Получить историю сообщений🔗
GET /get_messages?user_id=abc123
Возвращает историю переписки для указанного user_id, а также параметры настройки виджета. Перед выполнением проверяется домен-запросчик.
Параметры:
| Название | Тип | Обязательный | Описание |
|---|---|---|---|
| user_id | string | да | Идентификатор чата |
Пример ответа (200):
{
"welcome_text": "Стартовый текст",
"start_hints": ["Подсказка1", "Подсказка2"],
"widget_left": "false",
"icon_url": "icons/icon.png",
"messages": [
{
"id": 1,
"chat_id": 1,
"sender": "user",
"message": "Привет!",
"timestamp": "2025-05-20T14:05:41.840133+00:00"
},
{
"id": 2,
"chat_id": 1,
"sender": "chatgpt",
"message": "Привет! Чем могу помочь?",
"timestamp": "2025-05-20T14:05:41.840133+00:00"
}
]
}
Ошибки:
403— Неразрешённый домен / отсутствует Origin500— Внутренняя ошибка при получении истории
Отправить сообщение (POST-заявка)🔗
POST /send_message
Создаёт заявку на обработку сообщения. Сохраняет входящее сообщение во временный кэш (TTLCache) и возвращает stream_id.
Тело запроса:
Пример ответа (200):
Стриминг ответа от GPT🔗
GET /streaming_process_message?stream_id=...
Исполняет заявку, созданную через send_message. Отправляет сообщение в GPT-сервис, проксирует стриминговый ответ (SSE).
Пример chunk:
Пример final:
После получения финального блока сервис отправляет статистику (
send_tokens) и сохраняет ответ GPT вdata_service.
Ошибки:
404— Заявка не найдена (истёкTTLCache)403— Неразрешённый домен или виджет неактивен500— Ошибка запроса в GPT или обработки ответа
Зачем такое разделение
POST-запросы не подходят для стриминга text/event-stream в большинстве браузеров — они не обрабатывают SSE-ответы на POST, как это делают на GET.
GET-запросы позволяют использовать SSE корректно: клиент устанавливает соединение, и сервер отправляет данные порциями.
8. Логирование и мониторинг🔗
widget_service использует единый модуль логирования, применяемый во всех микросервисах проекта. Он реализован на базе стандартного logging с расширениями через pythonjsonlogger.
Так как везде используется один и тот же метод логирования, смотри его в документации data_service