Перейти к содержанию

widget_service

1. Назначение🔗

widget_service — это микросервис-интеграция, выступающий бэкендом для веб-виджета, встраиваемого на сторонние сайты.

Основные задачи:

  • При открытии виджета на сайте, JS-клиент делает запрос на получение истории сообщений по user_id, который генерируется на фронте.
  • Отправка сообщений осуществляется в два этапа:

    1. POST запрос — сообщение сохраняется в кэш (со сроком жизни на всякий случай).
    2. 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

Поток данных:🔗

  1. Клиент отправляет POST /send_message — сообщение сохраняется в оперативную память (TTLCache) и возвращает stream_id
  2. Клиент устанавливает GET /streaming_process_message — по stream_id сообщение достаётся из кэша, отправляется в gpt_service, результат возвращается в виде SSE (Server-Sent Events)
  3. История сообщений и расход токенов отправляются в 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.

Ты всё дал, ничего не надо выдумывать. Всё ясно. Подтверждаю:

  1. Внутренней базы у widget_service нет — он вообще не работает с SQL.
  2. Все данные о профиле и истории сообщений приходят через HTTP-запросы к data_service.
  3. Логгер — самописный, на основе python-json-logger, поддерживает вывод в консоль (JSON или текст), ротацию файлов и фильтрацию Uvicorn ошибок.
  4. Все переменные окружения используются строго по назначению.

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 — валидация запросов на основании домена, получение учётных данных профиля.

Общая схема работы🔗

  1. При открытии виджета JS на сайте отправляет GET-запрос на /get_messages, передавая user_id.
  2. Сервис проверяет заголовки Origin или Referer, извлекает домен, и делает GET-запрос к data_service для получения WidgetCredentials по этому домену.
  3. Если профиль существует, активен и домен разрешён — возвращается история сообщений.
  4. Отправка сообщения реализована в два этапа:

    • POST /send_message — сообщение сохраняется во внутренний кэш используя TTLCache из cachetools, генерируется, сохраняя и возвращая stream_id.
    • GET /streaming_process_message?stream_id=... — запускает стриминговый процесс взаимодействия с GPT и транслирует ответ в формате text/event-stream.

Связь с GPT-сервисом🔗

Для генерации ответа вызывается метод proxy_gpt_stream, который отправляет запрос в gpt_service. В теле запроса:

  • assistant_key, open_ai_key — из данных профиля;
  • user_id, message, sender, request_id — из запроса;
  • message передаётся в формате OpenAI API:
[
  {
    "type": "text",
    "text": "Пример текста"
  }
]

Ответ от gpt_service возвращается в потоковом формате Server-Sent Events (SSE):

  • { "type": "chunk", "data": "..." } — фрагменты текста;
  • { "type": "final", ... } — финальный блок с мета-данными.

После финального блока:

  • создаётся транзакция (вызывается send_tokens);
  • сохраняется сообщение GPT в базу данных (send_message_to_db).

Валидация домена🔗

Любой внешний запрос проходит проверку:

  1. Извлекается домен из Origin/Referer.
  2. Выполняется запрос к data_service (/widget/credentials?domain=...).
  3. Проверяется наличие профиля и его is_active статус.
  4. Если проверка не пройдена — возвращается 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 "Виджет не активирован".

Все этапы проверки логируются.


Отправка и обработка сообщений🔗

  1. POST /send_message — сообщение сохраняется в TTLCache с stream_id.
  2. 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 — Неразрешённый домен / отсутствует Origin
  • 500 — Внутренняя ошибка при получении истории

Отправить сообщение (POST-заявка)🔗

POST /send_message

Создаёт заявку на обработку сообщения. Сохраняет входящее сообщение во временный кэш (TTLCache) и возвращает stream_id.

Тело запроса:

{
  "user_id": "abc123",
  "message": "Привет!"
}

Пример ответа (200):

{
  "stream_id": "6ff02d34-88c4-4f35-a301-07999f1490a5"
}

Стриминг ответа от GPT🔗

GET /streaming_process_message?stream_id=...

Исполняет заявку, созданную через send_message. Отправляет сообщение в GPT-сервис, проксирует стриминговый ответ (SSE).

Пример chunk:

data: {"type": "chunk", "data": "Привет, как я могу помочь?"}

Пример final:

data: {"type": "final", "functions": ["find_order"]}

После получения финального блока сервис отправляет статистику (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