docs: add TRD for SheetDoc AI MVP
Derive technical requirements from PRD_SheetDoc_AI.md and the telecom.quest deploy constraints in GITEA.md: stack (React/Vite + FastAPI), stateless backend, four API endpoints, Traefik-labelled docker-compose deploy, MVP limits (1000 rows x 30 cols), 6-day plan. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
333ccca8a2
commit
efd2323d05
380
TRD.md
380
TRD.md
@ -0,0 +1,380 @@
|
||||
# TRD: SheetDoc AI
|
||||
|
||||
Технические требования к реализации MVP сервиса генерации документов из Google Таблиц. Источники: `PRD_SheetDoc_AI.md`, `GITEA.md`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Цель документа
|
||||
|
||||
Описать техническую реализацию MVP: стек, архитектуру, модули, API, обработку данных, AI-логику, безопасность и порядок развёртывания на инфраструктуре `telecom.quest` (Gitea + Traefik).
|
||||
|
||||
Документ покрывает только то, что нужно для MVP-сценария:
|
||||
вставка ссылки → выбор листа → анализ → уточняющие вопросы → текстовая справка → копирование.
|
||||
|
||||
---
|
||||
|
||||
## 2. Технологический стек
|
||||
|
||||
### 2.1. Frontend
|
||||
|
||||
- Язык: TypeScript.
|
||||
- Фреймворк: React 18 + Vite.
|
||||
- UI: Tailwind CSS, минимум компонентов (shadcn/ui по необходимости).
|
||||
- Состояние: React Query (серверное), Zustand (локальное состояние мастера шагов).
|
||||
- Сборка: статический бандл (`dist/`).
|
||||
|
||||
### 2.2. Backend
|
||||
|
||||
- Язык: Python 3.11.
|
||||
- Фреймворк: FastAPI + Uvicorn.
|
||||
- HTTP-клиент к Google: `httpx` (без OAuth, только публичные CSV-экспорты Sheets).
|
||||
- Валидация: Pydantic v2.
|
||||
- LLM SDK: `anthropic` (Claude). Модель по умолчанию: `claude-sonnet-4-6`.
|
||||
- Менеджер пакетов: `uv` или `pip` + `requirements.txt`.
|
||||
|
||||
### 2.3. Хранилище
|
||||
|
||||
- MVP: stateless. Никакой БД.
|
||||
- Сессия мастера хранится на клиенте (Zustand + sessionStorage).
|
||||
- Кэш ответа Sheets и анализа: in-memory LRU на backend (TTL 10 минут, ключ — `sheet_id:gid`).
|
||||
|
||||
### 2.4. Инфраструктура
|
||||
|
||||
- Docker, docker-compose.
|
||||
- Reverse proxy: Traefik (уже поднят на сервере).
|
||||
- Хост: `sheetdoc.telecom.quest` (или `<login>.telecom.quest/sheetdoc/` через Gitea Pages — только если фронт развязан от бэка).
|
||||
|
||||
---
|
||||
|
||||
## 3. Архитектура
|
||||
|
||||
### 3.1. Общая схема
|
||||
|
||||
```
|
||||
[Browser SPA] ──HTTPS──> [Traefik] ──> [FastAPI backend] ──HTTPS──> [Google Sheets CSV export]
|
||||
│
|
||||
└──HTTPS──> [Anthropic API]
|
||||
```
|
||||
|
||||
- SPA — статический бандл, отдаётся либо backend-ом (через FastAPI StaticFiles), либо отдельно с Gitea Pages.
|
||||
- Backend — единый сервис, без отдельной БД.
|
||||
- Внешние зависимости: Google Sheets публичный экспорт, Anthropic API.
|
||||
|
||||
### 3.2. Поток данных MVP
|
||||
|
||||
1. SPA шлёт `POST /api/sheets/resolve` со ссылкой.
|
||||
2. Backend парсит `spreadsheetId`, проверяет доступность, возвращает список листов.
|
||||
3. SPA шлёт `POST /api/sheets/load` с `spreadsheetId` + `gid`.
|
||||
4. Backend качает CSV, парсит, определяет активный диапазон, типы колонок, возвращает превью.
|
||||
5. SPA шлёт `POST /api/analyze/questions` — backend через LLM генерирует 3–7 уточняющих вопросов.
|
||||
6. SPA шлёт `POST /api/generate` с ответами пользователя — backend через LLM формирует текстовую справку.
|
||||
7. SPA рендерит результат, даёт кнопку «Копировать».
|
||||
|
||||
---
|
||||
|
||||
## 4. Модули backend
|
||||
|
||||
### 4.1. `sheets_client`
|
||||
|
||||
Задача: получить данные публичной Google Таблицы без OAuth.
|
||||
|
||||
- URL парсер: извлекает `spreadsheetId` из ссылок вида
|
||||
`https://docs.google.com/spreadsheets/d/<id>/edit#gid=<gid>`.
|
||||
- Список листов: HTML-страница `https://docs.google.com/spreadsheets/d/<id>/htmlview`, парсинг названий + gid (fallback). Альтернатива — `gviz/tq?tqx=out:json&sheet=<name>`.
|
||||
- Загрузка данных: `https://docs.google.com/spreadsheets/d/<id>/export?format=csv&gid=<gid>`.
|
||||
- Парсер CSV: `csv` stdlib, защита от больших файлов (лимит 5 MB на ответ).
|
||||
- Ошибки: 401/403/404 от Google → доменные исключения (`SheetForbidden`, `SheetNotFound`).
|
||||
|
||||
### 4.2. `range_detector`
|
||||
|
||||
Задача: определить активный диапазон.
|
||||
|
||||
- Срезать полностью пустые строки сверху/снизу.
|
||||
- Срезать полностью пустые столбцы слева/справа.
|
||||
- Определить строку заголовков:
|
||||
- кандидат — первая непустая строка, где все ячейки строковые и не выглядят как числа/даты;
|
||||
- fallback — первая непустая строка.
|
||||
- Учитывать внутренние пропуски (не обрывать диапазон на одной пустой строке).
|
||||
|
||||
### 4.3. `column_profiler`
|
||||
|
||||
Задача: профилировать колонки.
|
||||
|
||||
- Тип значения: `text | number | date | bool | empty`.
|
||||
- Доля заполненности, число уникальных значений, top-3 значения.
|
||||
- Эвристики для категориальных полей (статус, регион, тариф): доля уникальных < 20% и тип text.
|
||||
- Эвристики для дат: pandas-стиль парсинг (`dateutil`), валидные >70%.
|
||||
|
||||
### 4.4. `question_generator`
|
||||
|
||||
Задача: сформировать 3–7 уточняющих вопросов через LLM.
|
||||
|
||||
- Вход: профиль таблицы (название листа, колонки, типы, top-значения, число строк).
|
||||
- Промпт требует JSON-ответ: `{ questions: [{ id, text, why, optional, kind: "single|multi|text" , options? }] }`.
|
||||
- Жёсткое ограничение: 3 ≤ N ≤ 7.
|
||||
- Валидация Pydantic.
|
||||
|
||||
### 4.5. `document_generator`
|
||||
|
||||
Задача: сгенерировать текстовую справку.
|
||||
|
||||
- Вход: профиль таблицы + ответы пользователя + первые 50 строк данных (truncate, чтобы уложиться в контекст).
|
||||
- Промпт фиксирует структуру: заголовок, описание источника, показатели, наблюдения, выводы, рекомендации (если запрошены), дата генерации.
|
||||
- Опционально (необязательная функция MVP): структура презентации тем же endpoint-ом, отдельный режим.
|
||||
|
||||
### 4.6. `llm_client`
|
||||
|
||||
Обёртка над Anthropic SDK.
|
||||
|
||||
- Поддержка повторов (3 попытки, экспоненциальный backoff).
|
||||
- Включён prompt caching для системных промптов.
|
||||
- Логирование: токены, latency, без содержимого данных пользователя.
|
||||
|
||||
---
|
||||
|
||||
## 5. API
|
||||
|
||||
Базовый префикс: `/api`. Все ответы — JSON. Ошибки — единый формат:
|
||||
|
||||
```json
|
||||
{ "error": { "code": "SHEET_FORBIDDEN", "what": "...", "why": "...", "next": "..." } }
|
||||
```
|
||||
|
||||
Коды: `INVALID_URL`, `NOT_A_SHEET`, `SHEET_FORBIDDEN`, `SHEET_NOT_FOUND`, `SHEET_EMPTY`, `SHEET_TOO_LARGE`, `STRUCTURE_UNCLEAR`, `LLM_FAILED`, `VALIDATION_ERROR`.
|
||||
|
||||
### 5.1. `POST /api/sheets/resolve`
|
||||
|
||||
Вход: `{ url: string }`.
|
||||
Выход: `{ spreadsheetId, title, sheets: [{ gid, name, isEmpty }] }`.
|
||||
|
||||
### 5.2. `POST /api/sheets/load`
|
||||
|
||||
Вход: `{ spreadsheetId, gid }`.
|
||||
Выход:
|
||||
```json
|
||||
{
|
||||
"rows": 240,
|
||||
"cols": 8,
|
||||
"headers": ["..."],
|
||||
"columns": [{ "name": "...", "type": "text|number|date|bool", "fill": 0.97, "top": ["..."] }],
|
||||
"preview": [["..."]]
|
||||
}
|
||||
```
|
||||
Лимиты: 1 000 строк × 30 колонок. Превышение → `SHEET_TOO_LARGE` с предложением сузить диапазон.
|
||||
|
||||
### 5.3. `POST /api/analyze/questions`
|
||||
|
||||
Вход: `{ profile: <выход load> }`.
|
||||
Выход: `{ questions: [...] }` (3–7 шт).
|
||||
|
||||
### 5.4. `POST /api/generate`
|
||||
|
||||
Вход:
|
||||
```json
|
||||
{
|
||||
"profile": {...},
|
||||
"answers": [{ "id": "q1", "value": "..." }],
|
||||
"mode": "report" | "presentation_outline",
|
||||
"dataSample": [[...]]
|
||||
}
|
||||
```
|
||||
Выход: `{ document: "<markdown>", generatedAt: "ISO-8601", mode: "report" }`.
|
||||
|
||||
### 5.5. `GET /api/health`
|
||||
|
||||
Простой liveness-чек для Traefik.
|
||||
|
||||
---
|
||||
|
||||
## 6. Frontend: экраны и состояния
|
||||
|
||||
### 6.1. Шаги мастера
|
||||
|
||||
1. **Ссылка** — поле ввода + подсказка «откройте доступ по ссылке».
|
||||
2. **Лист** — список листов, отметка пустых, автoselect если один.
|
||||
3. **Превью** — карточка анализа: число строк/колонок, заголовки, типы, топ-значения.
|
||||
4. **Вопросы** — динамическая форма по схеме из API. Опциональные помечены, есть «Пропустить».
|
||||
5. **Результат** — markdown → отрендеренный текст, кнопки «Копировать», «Изменить ответы», «Сгенерировать заново».
|
||||
|
||||
### 6.2. Состояния загрузки и ошибок
|
||||
|
||||
- Глобальный индикатор для шагов > 1 секунды.
|
||||
- Ошибки по схеме «что/почему/что делать» (см. п. 9.5 PRD).
|
||||
- Кнопка повторного запроса на ошибках сети/LLM.
|
||||
|
||||
### 6.3. Копирование
|
||||
|
||||
- `navigator.clipboard.writeText` для markdown.
|
||||
- Дополнительно — копирование plain text (без markdown-разметки) для вставки в Word/Docs.
|
||||
|
||||
---
|
||||
|
||||
## 7. Нефункциональные требования
|
||||
|
||||
### 7.1. Производительность
|
||||
|
||||
- Парсинг 1 000 × 30 — ≤ 2 секунд (без LLM).
|
||||
- Генерация вопросов — ≤ 8 секунд p95.
|
||||
- Генерация справки — ≤ 25 секунд p95.
|
||||
- Стриминг LLM-ответа на фронт через `text/event-stream` (опционально для MVP).
|
||||
|
||||
### 7.2. Лимиты
|
||||
|
||||
- Размер CSV-ответа от Google: 5 MB.
|
||||
- Строк после обрезки: 1 000.
|
||||
- Активных колонок: 30.
|
||||
- Размер ответа LLM: до 4 000 токенов на справку.
|
||||
|
||||
### 7.3. Безопасность
|
||||
|
||||
- CORS: разрешён только домен фронта.
|
||||
- Никакой записи содержимого таблиц в логи или БД.
|
||||
- Anthropic API ключ — через env var `ANTHROPIC_API_KEY`, не в репозитории.
|
||||
- Rate limit: 30 запросов / минута / IP на `/api/generate` и `/api/analyze/questions`.
|
||||
- HTTPS принудительно — терминируется на Traefik.
|
||||
|
||||
### 7.4. Приватность
|
||||
|
||||
- Данные таблицы существуют только в памяти процесса на время запроса.
|
||||
- Кэш — только агрегированный профиль (без сырых строк) на 10 минут.
|
||||
- На фронте — баннер «данные обрабатываются в памяти и не сохраняются».
|
||||
|
||||
### 7.5. Логирование и метрики
|
||||
|
||||
- Логи: structlog в JSON, поля `request_id`, `endpoint`, `latency_ms`, `status`.
|
||||
- Без содержимого пользовательских данных.
|
||||
- Метрики (опционально, после MVP): Prometheus `/metrics`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Развёртывание
|
||||
|
||||
Согласно `GITEA.md`: статика — Gitea Pages, динамика — docker-compose за Traefik.
|
||||
|
||||
### 8.1. Вариант MVP (рекомендуемый)
|
||||
|
||||
Один сервис `sheetdoc-api`, который отдаёт и API, и собранный SPA как статику.
|
||||
|
||||
`docker-compose.yml` в корне репозитория:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
sheetdoc:
|
||||
build: .
|
||||
image: sheetdoc:latest
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY}
|
||||
ALLOWED_ORIGINS: "https://sheetdoc.telecom.quest"
|
||||
networks:
|
||||
- traefik
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.sheetdoc.rule=Host(`sheetdoc.telecom.quest`)"
|
||||
- "traefik.http.routers.sheetdoc.entrypoints=websecure"
|
||||
- "traefik.http.routers.sheetdoc.tls=true"
|
||||
- "traefik.http.services.sheetdoc.loadbalancer.server.port=8000"
|
||||
|
||||
networks:
|
||||
traefik:
|
||||
external: true
|
||||
```
|
||||
|
||||
Правила из `GITEA.md`:
|
||||
|
||||
- Никаких `ports:` наружу — только лейблы Traefik.
|
||||
- Сеть `traefik` подключается как `external: true`.
|
||||
- Секреты — через `.env` файл рядом с compose (не коммитить).
|
||||
|
||||
### 8.2. Вариант с Gitea Pages (только если фронт живёт отдельно)
|
||||
|
||||
- Ветка `gitea-pages`, в корне — `index.html` собранного бандла.
|
||||
- Топик репозитория — `gitea-pages-allowall`.
|
||||
- Все пути ассетов — относительные (`./assets/...`).
|
||||
- Backend — отдельный compose-стек с Traefik по адресу `api.sheetdoc.telecom.quest`.
|
||||
- На фронте — env-переменная `VITE_API_BASE=https://api.sheetdoc.telecom.quest`.
|
||||
|
||||
Этот вариант сложнее (CORS, два домена) и для MVP не рекомендуется.
|
||||
|
||||
### 8.3. Dockerfile (контур)
|
||||
|
||||
Двухстейджевый:
|
||||
|
||||
1. `node:20-alpine` — `npm ci && npm run build` → `/app/dist`.
|
||||
2. `python:3.11-slim` — установка backend, копирование `dist` в `app/static`, запуск `uvicorn`.
|
||||
|
||||
### 8.4. CI / автодеплой
|
||||
|
||||
- На push в `main`: Gitea Actions собирает образ, кладёт на сервер, `docker compose up -d --build`.
|
||||
- Pages-ветка `gitea-pages` — только если выбран вариант 8.2.
|
||||
|
||||
---
|
||||
|
||||
## 9. Обработка ошибок (mapping PRD → реализация)
|
||||
|
||||
| PRD пункт | Backend код | UX-сообщение |
|
||||
|---|---|---|
|
||||
| 7.1 Неверная ссылка | `INVALID_URL` / `NOT_A_SHEET` | «Ссылка не похожа на Google Sheets» |
|
||||
| 7.2 Закрытая таблица | `SHEET_FORBIDDEN` | «Откройте доступ по ссылке» |
|
||||
| 7.3 Несколько листов | — | UI: выбор листа |
|
||||
| 7.4 Пустой лист | `SHEET_EMPTY` | «Лист пуст, выберите другой» |
|
||||
| 7.5 Плохая структура | `STRUCTURE_UNCLEAR` | Доп. вопросы о заголовках |
|
||||
| 9.5.7 Слишком много данных | `SHEET_TOO_LARGE` | «Сузьте диапазон до 1 000 × 30» |
|
||||
| 9.5.9 LLM упал | `LLM_FAILED` | «Повторить генерацию» |
|
||||
|
||||
---
|
||||
|
||||
## 10. Тестирование
|
||||
|
||||
### 10.1. Backend
|
||||
|
||||
- Unit: `range_detector`, `column_profiler`, парсер URL — pytest, без сети.
|
||||
- Контрактные: моки Anthropic SDK, проверка валидности JSON-ответов.
|
||||
- Integration: запись фикстур реальных публичных таблиц (CSV-снимки в `tests/fixtures/`).
|
||||
|
||||
### 10.2. Frontend
|
||||
|
||||
- Vitest + React Testing Library: рендер шагов мастера, ошибки, валидация формы.
|
||||
- Playwright (опционально): e2e на staging домене с тестовой таблицей.
|
||||
|
||||
### 10.3. Smoke-чек MVP
|
||||
|
||||
Скрипт `scripts/smoke.sh`: дергает все четыре endpoint-а на одной зафиксированной публичной таблице, печатает результаты.
|
||||
|
||||
---
|
||||
|
||||
## 11. Этапы разработки
|
||||
|
||||
1. **День 1** — каркас FastAPI, парсер URL, CSV-загрузка, `range_detector`, `column_profiler`. Тесты на фикстурах.
|
||||
2. **День 2** — LLM-клиент, `question_generator`, `document_generator`. Промпты + валидация JSON.
|
||||
3. **День 3** — SPA: шаги 1–3 (ссылка, лист, превью), интеграция API.
|
||||
4. **День 4** — SPA: шаги 4–5 (вопросы, результат, копирование). Обработка ошибок.
|
||||
5. **День 5** — Dockerfile, compose, Traefik labels, деплой на `sheetdoc.telecom.quest`. Smoke-тесты.
|
||||
6. **День 6** — полировка UX, тексты ошибок, README, демонстрация end-to-end сценария.
|
||||
|
||||
---
|
||||
|
||||
## 12. Что НЕ реализуется в MVP (фиксация из PRD)
|
||||
|
||||
- OAuth и приватные таблицы.
|
||||
- Экспорт в `.docx`, `.pdf`, `.pptx`.
|
||||
- Личный кабинет, история, БД.
|
||||
- Графики, диаграммы.
|
||||
- Excel-файлы, несколько листов одновременно.
|
||||
- Корпоративные шаблоны и стили.
|
||||
|
||||
---
|
||||
|
||||
## 13. Открытые технические вопросы
|
||||
|
||||
1. Достаточно ли публичного CSV-экспорта, или нужен `gviz/tq` для получения форматирования?
|
||||
2. Стримить ли ответ LLM на фронт (улучшает UX, усложняет CORS/Traefik)?
|
||||
3. Хранить ли кратко (10 минут) сгенерированный документ, чтобы пользователь мог обновить страницу без потери результата?
|
||||
4. Нужен ли rate limit per-IP уже в MVP или достаточно лимита от Anthropic?
|
||||
5. Использовать ли Gitea Actions или ручной `docker compose up` для первой выкатки?
|
||||
|
||||
---
|
||||
|
||||
## 14. Следующий шаг
|
||||
|
||||
После согласования TRD — реализация по этапам из п. 11. Промпты для `question_generator` и `document_generator` оформить отдельным файлом `prompts/` в репозитории, чтобы версионировать независимо от кода.
|
||||
Reference in New Issue
Block a user