Spaces:
Sleeping
Sleeping
| 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) ==================== | |
| async def get_stocks_summary() -> Any: | |
| """Общая сводка по остаткам. | |
| Возвращает: всего товаров, остатки FBO/FBS, количество товаров по статусам | |
| (out_of_stock, critical, low, ok) и общее необходимое пополнение. | |
| """ | |
| return await _api_get("/stocks/summary") | |
| 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}) | |
| async def get_stock_product(nm_id: int) -> Any: | |
| """Полная информация по остаткам конкретного товара. | |
| Args: | |
| nm_id: Артикул Wildberries. | |
| """ | |
| return await _api_get(f"/stocks/product/{nm_id}") | |
| async def get_out_of_stock_products(limit: int = 50) -> Any: | |
| """Товары с нулевыми остатками (упущенная выручка).""" | |
| return await _api_get("/stocks/out-of-stock", {"limit": limit}) | |
| async def get_critical_stock_products(limit: int = 50) -> Any: | |
| """Критически низкие остатки (хватит менее чем на 7 дней).""" | |
| return await _api_get("/stocks/critical", {"limit": limit}) | |
| 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}) | |
| async def get_warehouse_distribution() -> Any: | |
| """Общая статистика распределения остатков по складам FBO.""" | |
| return await _api_get("/stocks/warehouses") | |
| async def get_product_warehouse_distribution(nm_id: int) -> Any: | |
| """Распределение конкретного товара по складам (где именно он лежит).""" | |
| return await _api_get(f"/stocks/product/{nm_id}/warehouses") | |
| # ==================== ЛОГИСТИКА (LOGISTICS) ==================== | |
| async def get_logistics_summary() -> Any: | |
| """Общая сводка по логистике за 30 дней. | |
| Возвращает суммы за доставку, хранение, штрафы, общий процент возвратов | |
| и долю расходов на логистику в GMV. | |
| """ | |
| return await _api_get("/logistics/summary") | |
| 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}) | |
| async def get_logistics_product(nm_id: int) -> Any: | |
| """Детализация всех затрат на логистику, возвратов и штрафов по конкретному товару.""" | |
| return await _api_get(f"/logistics/product/{nm_id}") | |
| async def get_high_logistics_cost(limit: int = 50) -> Any: | |
| """Товары, где логистика съедает более 25% от GMV.""" | |
| return await _api_get("/logistics/high-cost", {"limit": limit}) | |
| async def get_logistics_penalties(limit: int = 50) -> Any: | |
| """Список товаров, по которым есть начисленные штрафы от WB.""" | |
| return await _api_get("/logistics/penalties", {"limit": limit}) | |
| async def get_high_returns(limit: int = 50) -> Any: | |
| """Товары с аномально высоким процентом возвратов (> 20%).""" | |
| return await _api_get("/logistics/returns", {"limit": limit}) | |
| async def get_logistics_profitability(limit: int = 50) -> Any: | |
| """Анализ прибыльности товаров строго после вычета затрат на логистику.""" | |
| return await _api_get("/logistics/profitability", {"limit": limit}) | |
| async def get_logistics_trend(days: int = 30) -> Any: | |
| """Динамика (тренд) затрат на доставку, хранение и штрафы по дням.""" | |
| return await _api_get("/logistics/trend", {"days": days}) | |
| async def get_commissions_by_category() -> Any: | |
| """Справочник тарифов и комиссий WB (FBO/FBS) по категориям товаров.""" | |
| return await _api_get("/logistics/commissions") | |
| # ==================== ИНСАЙТЫ ==================== | |
| 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, | |
| ] |