# VMK 360 Data Collector

## Описание

Сервис приёма, нормализации и ИИ-обогащения данных об объектах недвижимости.

- **Приём**: Принимает полусырые данные от парсеров через REST API
- **Валидация**: AI определяет, является ли payload объявлением о недвижимости
- **Нормализация**: Приводит неструктурированные данные к единому формату
- **Обогащение**: Анализ изображений (vision) + текстовый анализ (NER, summary, оценка цены)
- **Хранение**: Сохраняет в PostgreSQL с полной историей изменений (snapshots)

---

## Быстрый старт (Docker)

```bash
# 1. Клонировать и перейти в директорию
cd vmk_data_collector

# 2. Скопировать конфиг
#   Отредактируй .env под себя (OLLAMA_BASE_URL, порты и т.д.)
cp .env.example .env

# 3. Запуск всего стека (PostgreSQL + FastAPI)
docker compose up -d --build

# 4. Проверка
#   Документация API: http://localhost:8020/docs
#   Health check:      http://localhost:8020/api/v1/health
```

### Стоп/очистка

```bash
docker compose down            # остановить
docker compose down -v        # остановить + удалить данные БД
```

---

## Ручной старт (для разработки)

```bash
# 1. PostgreSQL должен быть доступен (локально или в Docker)
#    Порт по умолчанию: 5432 (хост) / 5433 (если через docker-compose)

# 2. Установка зависимостей
pip install -e ".[dev]"

# 3. Применение миграций
alembic upgrade head

# 4. Запуск приложения
uvicorn vmk_data_collector.main:app --reload --port 8020
```

---

## API для парсеров

### `POST /api/v1/ingest`

Принимает сырые данные от парсеров, валидирует payload и ставит задачу в очередь на обработку.

#### Заголовки

| Заголовок | Обязательный | Значение |
|---|---|---|
| `Content-Type` | Да | `application/json` |

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

```json
{
  "source_slug": "avito",
  "external_id": "avito-12345678",
  "payload": {
    "title": "2-комнатная квартира, 65 м², 5/25 этаж",
    "description": "Продается просторная двухкомнатная квартира в новостройке. Рядом метро.",
    "price": 8500000,
    "url": "https://avito.ru/item/12345678",
    "images": [
      "https://avito.ru/img1.jpg",
      "https://avito.ru/img2.jpg"
    ],
    "contact_phone": "+7 (999) 123-45-67",
    "address": "Москва, Тверская ул., 1",
    "area": 65.5,
    "rooms": 2,
    "floor": 5
  }
}
```

#### Поля `payload`

| Поле | Тип | Обязательное | Описание |
|---|---|---|---|
| `title` | `string` | Нет | Заголовок объявления |
| `description` | `string` | Нет | Описание объявления |
| `price` | `number / string` | Нет | Цена (руб.) |
| `url` | `string` | Нет | Ссылка на источник |
| `images` | `string[]` | Нет | Массив URL изображений |
| `contact_phone` | `string` | Нет | Телефон продавца |
| `address` | `string` | Нет | Адрес объекта |
| `area` | `number / string` | Нет | Площадь (м²) |
| `rooms` | `integer / string` | Нет | Количество комнат |
| `floor` | `integer / string` | Нет | Этаж |

**Важно:** хотя бы одно из полей `title` или `description` должно присутствовать.

#### Ответ `202 Accepted` (успех)

```json
{
  "job_id": 42,
  "property_id": null,
  "status": "pending",
  "reason": null,
  "message": "Queued for processing",
  "snapshot_id": null
}
```

#### Ответ `422 Unprocessable Entity` (ошибка валидации)

```json
{
  "detail": "Invalid payload: 1 validation error for PayloadSchema..."
}
```

#### Статусы обработки

После приёма задача (`job_id`) проходит через pipeline:

| Статус | Значение |
|---|---|
| `pending` | Задача поставлена в очередь |
| `processing` | AI нормализует данные |
| `completed` | Объект сохранён в БД |
| `invalid` | AI определил, что это не недвижимость |
| `failed` | Ошибка на стадии обработки (Ollama недоступен и т.д.) |

---

## Примеры (curl)

### Успешный ingest

```bash
curl -X POST http://localhost:8020/api/v1/ingest \
  -H "Content-Type: application/json" \
  -d '{
    "source_slug": "avito",
    "external_id": "avito-99999",
    "payload": {
      "title": "3-комнатная квартира, 120 м²",
      "description": "Элитная квартира в центре",
      "price": 25000000,
      "url": "https://avito.ru/item/99999",
      "address": "Москва, Сити"
    }
  }'
```

### Проверка архивации (404/410 = объявление снято)

```bash
curl -X POST http://localhost:8020/api/v1/listings/1/archive-check
```

---

## Конфигурация

Все настройки задаются через переменные окружения (`.env` или `docker-compose.yml`):

| Переменная | Дефолт | Описание |
|---|---|---|
| `APP_PORT` | `8020` | Порт приложения (хост) |
| `DATABASE_URL` | `postgresql+asyncpg://postgres:postgres@postgres:5432/vmk_data` | PostgreSQL (async) |
| `OLLAMA_BASE_URL` | `http://192.168.1.75:11434` | Ollama API |
| `OLLAMA_TEXT_MODEL` | `gemma4:e2b-it-q4_K_M` | Модель для текстовых задач |
| `OLLAMA_VISION_MODEL` | `gemma4:e2b-it-q4_K_M` | Модель для анализа изображений |
| `OLLAMA_TIMEOUT` | `120` | Таймаут запроса к Ollama (сек) |
| `LOG_LEVEL` | `INFO` | Уровень логирования |

---

## Архитектура

```
┌──────────────┐     ┌──────────────┐     ┌──────────────────┐
│   Парсеры    │────▶│  FastAPI     │────▶│  PostgreSQL      │
│  (curl/HTTP) │     │  /api/v1/    │     │  (raw_data +     │
└──────────────┘     │  ingest      │     │   property_...)   │
                     └──────┬───────┘     └──────────────────┘
                            │
                            ▼
                     ┌──────────────┐
                     │  QueueWorker │
                     │  (async bg)  │
                     └──────┬───────┘
                            │
              ┌─────────────┼─────────────┐
              ▼             ▼             ▼
        ┌─────────┐   ┌─────────┐   ┌─────────┐
        │AI Норма-│   │AI Анализ│   │AI Обога-│
        │лизатор  │   │изображ. │   │щение    │
        └────┬────┘   └────┬────┘   └────┬────┘
             │             │             │
             └─────────────┴─────────────┘
                           │
                           ▼
                     ┌──────────────┐
                     │   Ollama     │
                     │ 192.168.1.75 │
                     └──────────────┘
```

- **API Layer** — FastAPI, Pydantic валидация, structured logging (structlog)
- **Service Layer** — `PropertyPipeline`, AI-нормализация, обогащение, `QueueWorker`
- **Repository Layer** — абстракция доступа к PostgreSQL (async SQLAlchemy)
- **Domain Layer** — чистые сущности недвижимости
- **Infrastructure Layer** — Ollama client, image downloader, rate limiter

---

## Документация

- [Техническое задание](docs/SPECIFICATION.md) — полное описание модели данных, API, AI-слоя
- [Архитектура](docs/ARCHITECTURE.md) — диаграммы, потоки данных, компоненты
- **OpenAPI (Swagger)**: `http://localhost:8020/docs`
- **ReDoc**: `http://localhost:8020/redoc`

---

## Логирование

Все логи выводятся в stdout в формате JSON (structlog), что удобно для сбора через `docker compose logs`:

```bash
# Следить за логами в реальном времени
docker compose logs -f app

# Посмотреть последние 50 строк
docker compose logs --tail=50 app
```

Ключевые события для мониторинга:
- `ingest_request` — новый запрос от парсера
- `ingest_accepted` — задача поставлена в очередь
- `ingest_validation_failed` — ошибка валидации payload
- `pipeline_start` — начало обработки задачи
- `pipeline_completed` — задача успешно завершена
- `pipeline_not_real_estate` — AI отбросил объект
- `ollama_chat_request/response` — запросы к Ollama
