Skip to content

Latest commit

 

History

History
151 lines (121 loc) · 13.1 KB

File metadata and controls

151 lines (121 loc) · 13.1 KB

Архитектура приложения

1. Компоненты

  • frontend (React + TypeScript + MUI):
    • загрузка Excel,
    • выбор колонок для анализа, колонок для passthrough и опциональной группировки,
    • шаблон (промпт + поля ответа) с готовыми пресетами,
    • запуск/пауза/resume/cancel/перезапуск,
    • live-превью обработанных строк и просмотр отчётов.
  • backend (FastAPI):
    • API, auth, валидация,
    • прием upload и постановка inspect-задач в очередь,
    • выдача отчетов и файлов.
  • worker (Python process):
    • фоновая обработка строк,
    • вызовы LLM,
    • запись прогресса и результатов.
  • inspect-worker (Python process):
    • отдельная очередь на чтение/инспекцию Excel,
    • подготовка листов/колонок без нагрузки на API и analysis-worker.
  • postgres:
    • пользователи/сессии,
    • отчеты/строки отчета,
    • долговременный кэш.
  • redis:
    • очередь задач анализа,
    • очередь задач inspect,
    • служебные lease/дедуп-маркеры.

2. Поток данных

  1. Пользователь загружает .xlsx через /api/file/inspect.
  2. Backend сохраняет файл, ставит file_inspect-задачу в Redis-очередь и возвращает inspect_status=parsing.
  3. inspect-worker выполняет инспекцию и обновляет статус файла (queued -> parsing -> ready|error), UI опрашивает GET /api/file/{file_id}/inspect. Если все inspect-worker заняты, новые upload-задачи ждут в inspect-очереди (статус parsing до освобождения воркера).
  4. Пользователь запускает задачу /api/jobs только после inspect_status=ready. В UI он выбирает:
    • Колонки для анализа — попадают в {row_json} и отправляются в LLM. В итоговом xlsx отдельными столбцами не пишутся — их содержимое уже учтено в запросе к LLM.
    • Колонки из исходника в итоговый отчёт (в API — non_analysis_columns) — не отправляются в LLM и сохраняются в итоговом файле. Можно выбирать те же колонки, что и для анализа — в итоге появятся один раз. При group_by_column список подрезается до одной колонки группировки, т.к. в групповой выгрузке одна строка = одна группа.
    • group_by_column (опционально) — имя колонки, по которой строки группируются. LLM получает всю группу целиком, возвращает один ответ на группу; все строки группы получают одинаковый custom_json. Размер группы ограничен переменной GROUP_MAX_ROWS (дефолт 100000).
  5. Backend в одном переходе создаёт запись reports, стримит placeholder-строки report_rows из исходного .xlsx и кладёт payload анализа в Redis-очередь.
    • Вставка placeholder-строк использует psycopg3 COPY FROM STDIN на чистой таблице и фоллбэк INSERT ... ON CONFLICT DO NOTHING при повторной вставке.
  6. worker забирает задачу анализа, читает строки и обрабатывает их через LLM.
    • В групповом режиме все строки группы обновляются одним запросом через bulk_update_report_rows_same_result(row_numbers) (батчи по 5000, фильтр row_number = ANY(?)).
  7. Прогресс пишется в БД, UI получает обновления через SSE /api/jobs/{job_id}/events.
  8. По завершении формируются results/{job_id}_results.xlsx и опционально results/{job_id}_raw.json.
    • В групповом режиме в xlsx попадает одна строка на group_key; первая колонка называется group_number и содержит порядковый номер группы (1..N). Из passthrough-колонок сохраняется только сама колонка группировки.

3. Контракт custom-анализа

Основной режим — analysis_mode=custom.

  • В шаблоне используются:
    • {row_json} — JSON из выбранных входных колонок (подставляется сервисом автоматически, в явном виде плейсхолдер в промпте указывать не обязательно),
    • {row_number} — номер строки; в групповом режиме — порядковый номер группы (1..N).
  • EXPECTED_JSON описывает только структуру ответа: имена полей, типы, допустимые значения и ограничения. Обязательный набор полей не фиксирован — пользователь сам выбирает, что модель должна вернуть под свою задачу. Поля могут называться по-русски.
  • Backend строит схему валидации из EXPECTED_JSON и проверяет ответ модели. Если ответ не прошёл валидацию, строка отправляется на повторную попытку с текстом ошибки, чтобы модель скорректировала формат.

Поддерживаемые типы в EXPECTED_JSON:

  • string
  • number
  • integer
  • boolean
  • enum
  • array
  • object
  • date
  • datetime

Поддерживаемые ограничения:

  • string: min_length, max_length
  • number / integer: min, max
  • enum: values
  • array: items, min_items, max_items
  • object: properties, required
  • date: формат YYYY-MM-DD
  • datetime: ISO-формат даты-времени

Пример schema-формата (из пресета «Анализ мошенничества по одному отзыву»):

{
  "категория": { "type": "enum", "values": ["мошенничество", "плохой сервис", "брак товара", "позитивный", "нейтральный"] },
  "тип_нарушения": { "type": "enum", "values": ["нет мошенничества", "оплата мимо кассы", "вымогательство", "личные скидки", "подделка возвратов", "подмена товара", "иное мошенничество"] },
  "уверенность": { "type": "number", "min": 0, "max": 1 },
  "описание": { "type": "string", "max_length": 240 }
}

Имена полей произвольны — главное, чтобы они соответствовали тому, что просит вернуть промпт. Если в ответе окажутся стандартные поля summary, category, sentiment_label, negativity_score — UI покажет их на плашках и диаграммах; остальные поля доступны в итоговом xlsx.

4. Производительность чтения и экспорта

  • iter_report_rows использует keyset-пагинацию (WHERE row_number > :last_seen ORDER BY row_number ASC LIMIT :batch). Это удерживает чтение в O(n) — на 100k строк OFFSET/LIMIT деградировал бы до O(n²) сканов.
  • Экспорт xlsx и построение превью (build_report_analysis) идут поверх того же итератора. Для группового режима в превью и в xlsx пропускаются дубли по group_key (одна запись на группу).
  • Массовое обновление строк группы — через bulk_update_report_rows_same_result(row_numbers, ...): один UPDATE с row_number = ANY(?) на батч 5000, вместо N отдельных апдейтов при одинаковом LLM-ответе для всей группы.
  • reset_failed_and_skipped_rows(report_id) при retry сбрасывает в pending все строки со status='error' и группы, пропущенные из-за старого лимита (skipped_large_group), и очищает их custom_json/warnings_json/error_text. После этого processed_rows пересчитывается, а воркер подбирает эти строки заново.

5. Кэш

  • Точный кэш: llm_cache.
  • Семантический кэш: llm_semantic_cache (embedding similarity).
  • Инвалидация на чтении: при cache-hit ответ ещё раз прогоняется через валидатор expected_json. Если формат не сошёлся (например, пользователь поменял схему), запись в llm_cache удаляется через delete_cached_analysis(cache_key) и строка уходит в LLM заново.
  • Retry с feedback: при повторной обработке строки с retry_feedback кэш игнорируется — иначе модель получила бы тот же сломанный ответ.
  • Лимиты/обслуживание задаются через ENV:
    • MAX_LLM_CACHE_ROWS,
    • MAX_SEMANTIC_CACHE_ROWS,
    • CACHE_MAINTENANCE_INTERVAL_SEC,
    • SEMANTIC_CACHE_*.

6. Масштабирование

  • Реплики backend/worker задаются через compose/scale.
  • Для хоста 8 CPU / 16 GB RAM рекомендуемый старт:
    • backend=2, worker=4, frontend=1.

7. Наблюдаемость

  • Логи: docker compose logs -f <service>.
  • Проверка состояния: docker compose ps, docker stats.
  • Health endpoint: GET /api/health (через frontend nginx: http://localhost:8080/api/health).

8. Версионирование

Используется Semantic VersioningMAJOR.MINOR.PATCH:

  • MAJOR — несовместимые изменения. Поднимаем, если релиз ломает одно из:
    • контракт публичного API (/api/*: endpoints, форматы запросов/ответов)
    • формат итоговой выгрузки (xlsx/raw JSON), на который завязаны скрипты пользователей
    • обязательные поля конфигурации (переименование, удаление или смена семантики переменной окружения, которую пользователь уже мог задать)
    • язык контрактов expected_json_template (семантика типов/ограничений)
  • MINOR — новые возможности с обратной совместимостью: новые endpoint'ы, новые опциональные ENV с разумными дефолтами, новые возможности UI.
  • PATCH — исправление ошибок без изменений в функциональности и конфигурации.

Единый источник версии: backend/app/__init__.py (__version__). Используется в:

  • FastAPI(version=...) — для OpenAPI (/docs, /openapi.json)
  • export_raw_json(app_version=...) — в метаданных скачанного raw JSON

frontend/package.json остаётся отдельной npm-экосистемой — синхронизируется вручную при сборке.

Процесс релиза:

  1. Обновить backend/app/__init__.py::__version__
  2. При необходимости — frontend/package.json::version
  3. Добавить раздел в docs/RELEASE_NOTES.md с датой и изменениями по трём блокам: «Для пользователей», «Юнит-тестовое покрытие» (если поменялось), «Технические детали»
  4. Актуализировать README.md (раздел «Последние изменения», структура проекта, ENV-переменные)
  5. Тег коммита: vX.Y.Z