ChatWB / api /logistics.py
Levin-Aleksey's picture
Initial commit
da9b704
import os
from typing import Any, Optional
import httpx
from langchain_core.tools import tool
API_URL = os.getenv("API_URL", "https://levinaleksey-wb.hf.space").rstrip("/")
HTTP_TIMEOUT = float(os.getenv("HTTP_TIMEOUT", "30.0"))
ALLOWED_STOCKS_STATUSES = {"out_of_stock", "critical", "low", "ok"}
ALLOWED_STOCKS_SORT = {"days_of_stock", "total_stock", "avg_daily_sales_7d", "restock_needed_30d"}
ALLOWED_LOGISTICS_STATUSES = {"high_logistics", "has_penalties", "high_returns", "ok"}
ALLOWED_LOGISTICS_SORT = {"logistics_total", "delivery_total", "storage_total", "penalty_total", "return_rate_pct"}
async def _api_get(path: str, params: dict[str, Any] | None = None) -> Any:
"""Единый helper для GET-запросов к внешнему API."""
async with httpx.AsyncClient(timeout=HTTP_TIMEOUT) as client:
if params:
params = {k: v for k, v in params.items() if v is not None}
response = await client.get(f"{API_URL}{path}", params=params)
response.raise_for_status()
return response.json()
async def _api_post(path: str, body: dict[str, Any]) -> Any:
"""Единый helper для POST-запросов к внешнему API."""
async with httpx.AsyncClient(timeout=HTTP_TIMEOUT) as client:
response = await client.post(f"{API_URL}{path}", json=body)
response.raise_for_status()
return response.json()
# ==================== ОСТАТКИ (STOCKS) ====================
@tool
async def get_stocks_summary() -> Any:
"""Общая сводка по остаткам.
Возвращает: всего товаров, остатки FBO/FBS, количество товаров по статусам
(out_of_stock, critical, low, ok) и общее необходимое пополнение.
"""
return await _api_get("/stocks/summary")
@tool
async def get_stocks_products(
limit: int = 50,
status: Optional[str] = None,
sort_by: str = "days_of_stock"
) -> Any:
"""Список товаров с остатками, фильтрацией и сортировкой.
Args:
limit: Количество товаров.
status: Статус остатков. Допустимые: 'out_of_stock', 'critical', 'low', 'ok'.
sort_by: Поле сортировки. Допустимые: 'days_of_stock', 'total_stock',
'avg_daily_sales_7d', 'restock_needed_30d'.
"""
if status is not None and status not in ALLOWED_STOCKS_STATUSES:
raise ValueError(f"Некорректный status='{status}'. Допустимо: {sorted(ALLOWED_STOCKS_STATUSES)}")
if sort_by not in ALLOWED_STOCKS_SORT:
raise ValueError(f"Некорректный sort_by='{sort_by}'. Допустимо: {sorted(ALLOWED_STOCKS_SORT)}")
return await _api_get("/stocks/products", {"limit": limit, "status": status, "sort_by": sort_by})
@tool
async def get_stock_product(nm_id: int) -> Any:
"""Полная информация по остаткам конкретного товара.
Args:
nm_id: Артикул Wildberries.
"""
return await _api_get(f"/stocks/product/{nm_id}")
@tool
async def get_out_of_stock_products(limit: int = 50) -> Any:
"""Товары с нулевыми остатками (упущенная выручка)."""
return await _api_get("/stocks/out-of-stock", {"limit": limit})
@tool
async def get_critical_stock_products(limit: int = 50) -> Any:
"""Критически низкие остатки (хватит менее чем на 7 дней)."""
return await _api_get("/stocks/critical", {"limit": limit})
@tool
async def get_restock_plan(days: int = 30, limit: int = 50) -> Any:
"""План пополнения остатков.
Расчёт необходимого количества для закупки на N дней вперёд
на основе средних продаж.
Args:
days: На сколько дней планируем запас.
limit: Количество товаров.
"""
return await _api_get("/stocks/restock-plan", {"days": days, "limit": limit})
@tool
async def get_warehouse_distribution() -> Any:
"""Общая статистика распределения остатков по складам FBO."""
return await _api_get("/stocks/warehouses")
@tool
async def get_product_warehouse_distribution(nm_id: int) -> Any:
"""Распределение конкретного товара по складам (где именно он лежит)."""
return await _api_get(f"/stocks/product/{nm_id}/warehouses")
# ==================== ЛОГИСТИКА (LOGISTICS) ====================
@tool
async def get_logistics_summary() -> Any:
"""Общая сводка по логистике за 30 дней.
Возвращает суммы за доставку, хранение, штрафы, общий процент возвратов
и долю расходов на логистику в GMV.
"""
return await _api_get("/logistics/summary")
@tool
async def get_logistics_products(
limit: int = 50,
status: Optional[str] = None,
sort_by: str = "logistics_total"
) -> Any:
"""Список товаров с затратами на логистику.
Args:
limit: Количество товаров.
status: Фильтр. Допустимые: 'high_logistics', 'has_penalties', 'high_returns', 'ok'.
sort_by: Сортировка. Допустимые: 'logistics_total', 'delivery_total',
'storage_total', 'penalty_total', 'return_rate_pct'.
"""
if status is not None and status not in ALLOWED_LOGISTICS_STATUSES:
raise ValueError(f"Некорректный status='{status}'. Допустимо: {sorted(ALLOWED_LOGISTICS_STATUSES)}")
if sort_by not in ALLOWED_LOGISTICS_SORT:
raise ValueError(f"Некорректный sort_by='{sort_by}'. Допустимо: {sorted(ALLOWED_LOGISTICS_SORT)}")
return await _api_get("/logistics/products", {"limit": limit, "status": status, "sort_by": sort_by})
@tool
async def get_logistics_product(nm_id: int) -> Any:
"""Детализация всех затрат на логистику, возвратов и штрафов по конкретному товару."""
return await _api_get(f"/logistics/product/{nm_id}")
@tool
async def get_high_logistics_cost(limit: int = 50) -> Any:
"""Товары, где логистика съедает более 25% от GMV."""
return await _api_get("/logistics/high-cost", {"limit": limit})
@tool
async def get_logistics_penalties(limit: int = 50) -> Any:
"""Список товаров, по которым есть начисленные штрафы от WB."""
return await _api_get("/logistics/penalties", {"limit": limit})
@tool
async def get_high_returns(limit: int = 50) -> Any:
"""Товары с аномально высоким процентом возвратов (> 20%)."""
return await _api_get("/logistics/returns", {"limit": limit})
@tool
async def get_logistics_profitability(limit: int = 50) -> Any:
"""Анализ прибыльности товаров строго после вычета затрат на логистику."""
return await _api_get("/logistics/profitability", {"limit": limit})
@tool
async def get_logistics_trend(days: int = 30) -> Any:
"""Динамика (тренд) затрат на доставку, хранение и штрафы по дням."""
return await _api_get("/logistics/trend", {"days": days})
@tool
async def get_commissions_by_category() -> Any:
"""Справочник тарифов и комиссий WB (FBO/FBS) по категориям товаров."""
return await _api_get("/logistics/commissions")
# ==================== ИНСАЙТЫ ====================
@tool
async def add_logistics_insight(nm_id: int, tag: str, recommendation: str, score: float) -> Any:
"""Сохранить сгенерированную рекомендацию по остаткам/логистике в базу.
Args:
nm_id: Артикул Wildberries.
tag: Тег (например 'restock_fbo', 'reduce_returns', 'investigate_penalty').
recommendation: Текст конкретной рекомендации.
score: Уверенность (от 0.5 до 1.0).
"""
return await _api_post("/insights/add", {
"nm_id": nm_id,
"tag": tag,
"recommendation": recommendation,
"score": score
})
# Экспортируем все 18 инструментов
logistics_tools = [
get_stocks_summary,
get_stocks_products,
get_stock_product,
get_out_of_stock_products,
get_critical_stock_products,
get_restock_plan,
get_warehouse_distribution,
get_product_warehouse_distribution,
get_logistics_summary,
get_logistics_products,
get_logistics_product,
get_high_logistics_cost,
get_logistics_penalties,
get_high_returns,
get_logistics_profitability,
get_logistics_trend,
get_commissions_by_category,
add_logistics_insight,
]