"""Pydantic-модели для входных параметров и выходных результатов инструментов."""
from datetime import date
from typing import Literal
from pydantic import BaseModel, Field
class PaginationParams(BaseModel):
"""Базовая пагинация."""
limit: int = Field(
default=20,
ge=1,
le=100,
description=(
"Количество результатов на странице. "
"Если total > limit, используй offset+=limit для следующей страницы."
),
)
offset: int = Field(
default=0,
ge=0,
description="Смещение для пагинации. Следующая страница: offset += limit.",
)
class MetadataFilters(BaseModel):
"""Фильтры по метаданным, общие для всех инструментов поиска.
Все фильтры объединяются через AND. Если результатов мало — попробуй убрать
1–2 фильтра (обычно district или metro_station).
"""
deal_type: Literal["sale", "rent_long", "rent_short"] | None = Field(
default=None,
description=(
"Тип сделки: sale=продажа, rent_long=долгосрочная аренда, "
"rent_short=посуточная"
),
)
city: str | None = Field(
default=None,
description=(
"Город на украинском языке. Поиск по подстроке (ILIKE) — "
"достаточно части названия, например 'Київ' или 'Львів'. Регистр не важен."
),
)
district: str | None = Field(
default=None,
description=(
"Район на украинском языке. Поиск по подстроке (ILIKE). "
"Примеры: 'Печерський', 'Шевченківський', 'Галицький'."
),
)
rooms_count: int | None = Field(
default=None,
ge=0,
description="Количество комнат. 0 = студия, 1 = 1-комнатная и т.д.",
)
min_price: float | None = Field(
default=None,
ge=0,
description=(
"Минимальная цена. Обязательно указывай currency вместе с min_price/max_price, "
"иначе фильтр применится ко всем валютам одновременно."
),
)
max_price: float | None = Field(
default=None,
ge=0,
description="Максимальная цена. Указывай вместе с currency.",
)
currency: Literal["USD", "EUR", "UAH"] | None = Field(
default=None,
description="Валюта цены. Обязательна при использовании min_price/max_price.",
)
min_total_area: float | None = Field(
default=None,
ge=0,
description="Минимальная общая площадь, м².",
)
max_total_area: float | None = Field(
default=None,
ge=0,
description="Максимальная общая площадь, м².",
)
building_type: Literal["brick", "panel", "monolith", "gas_block", "wood"] | None = Field(
default=None,
description=(
"Тип постройки: brick=кирпичный, panel=панельный, monolith=монолитный, "
"gas_block=газоблок, wood=деревянный."
),
)
floor: int | None = Field(
default=None,
ge=0,
description="Этаж квартиры. 0 обычно означает цоколь/подвал (если есть в базе).",
)
listing_status: Literal["active", "sold", "rented", "removed", "archived"] | None = Field(
default=None,
description="Статус объявления. По умолчанию сервер не фильтрует — включая все статусы.",
)
metro_station: str | None = Field(
default=None,
description=(
"Станция метро на украинском. Поиск по подстроке (ILIKE). "
"Примеры: 'Арсенальна', 'Театральна', 'Площа Ринок'. "
"Регистр не важен."
),
)
class SearchSimilarInput(BaseModel):
"""Входные параметры для векторного поиска.
Используй, когда пользователь описывает желания, атмосферу или качества
(«уютная квартира с ремонтом у метро»), а не конкретные ключевые слова.
"""
query: str = Field(
...,
description=(
"Текстовый запрос на украинском языке для поиска по смыслу. "
"Формулируй конкретно, без разговорных стоп-слов. "
"✓ Хорошо: '2-кімнатна квартира біля метро з ремонтом'. "
"✗ Плохо: 'дай квартиру недорого' (стоп-слова)."
),
)
filters: MetadataFilters = Field(default_factory=MetadataFilters)
pagination: PaginationParams = Field(default_factory=PaginationParams)
# min_similarity зашит в код всегда 0.7 (широкий поиск)
class SearchMetadataInput(BaseModel):
"""Входные параметры для поиска по метаданным/FTS.
Используй, когда пользователь называет конкретные ключевые слова:
район, станция метро, улица, особенности ('Печерський район метро Арсенальна').
"""
query: str = Field(
...,
description=(
"Текстовый запрос на украинском языке для полнотекстового поиска. "
"Подходит для конкретных названий и ключевых слов. "
"✓ Хорошо: 'Печерський район Арсенальна метро'. "
"✗ Плохо: 'хочу квартиру' (слишком общее, используй search_similar)."
),
)
filters: MetadataFilters = Field(default_factory=MetadataFilters)
pagination: PaginationParams = Field(default_factory=PaginationParams)
sort_by: Literal[
"relevance",
"price_asc",
"price_desc",
"date_desc",
"area_desc",
] = Field(
default="relevance",
description=(
"Сортировка результатов: relevance=по релевантности FTS (умолч.), "
"price_asc=цена по возрастанию, price_desc=цена по убыванию, "
"date_desc=новые сверху, area_desc=большая площадь сверху."
),
)
class GetListingInput(BaseModel):
"""Входные параметры для получения объявления по ID."""
listing_id: int = Field(..., ge=1, description="ID объявления (положительное целое число).")
class ListingResult(BaseModel):
"""Результат поиска — одно объявление."""
id: int
title: str
description: str | None = None
generated_description: str | None = None
price: float | None = None
currency: str | None = None
deal_type: str | None = None
city: str | None = None
district: str | None = None
rooms_count: int | None = None
total_area: float | None = None
living_area: float | None = None
kitchen_area: float | None = None
floor: int | None = None
floors_count: int | None = None
building_type: str | None = None
building_year: int | None = None
renovation_status: str | None = None
balcony_count: int | None = None
bathroom_type: str | None = None
parking_type: str | None = None
heating_type: str | None = None
layout_type: str | None = None
window_view: str | None = None
metro_station: str | None = None
metro_distance_type: str | None = None
metro_distance_meters: int | None = None
url_source: str | None = None
publish_date: date | None = None
images_count: int | None = None
contact_phone: str | None = None
listing_status: str | None = None
archived_at: date | None = None
created_at: date | None = None
updated_at: date | None = None
similarity_score: float | None = Field(
default=None,
description="Косинусная близость [0..1] для search_similar_listings. "
"1.0 = максимально похоже, 0.0 = не похоже.",
)
rank_score: float | None = Field(
default=None,
description="Релевантность FTS (ts_rank_cd) для search_by_metadata. "
"Чем выше, тем релевантнее.",
)
class SearchResult(BaseModel):
"""Результат поиска — список объявлений + мета.
Если total > limit и offset + limit < total — есть ещё результаты.
Для следующей страницы: offset += limit.
"""
total: int = Field(description="Общее количество найденных записей (без LIMIT).")
limit: int
offset: int
listings: list[ListingResult]