File size: 8,449 Bytes
1063f06
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
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_FINANCE_STATUSES = {"loss", "low_margin", "has_penalties", "profit_falling", "ok"}
ALLOWED_FINANCE_SORT = {"profit", "gmv", "margin_net_pct", "roi_pct", "unit_profit_net"}
ALLOWED_UNIT_ECONOMICS_SORT = {"unit_profit_net", "unit_revenue", "unit_cost"}

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:
        # Убираем параметры со значением None, чтобы не передавать их в URL
        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()


@tool
async def get_finance_summary() -> Any:
    """Общая финансовая сводка за 30 дней.
    
    Возвращает: общую статистику (GMV, прибыль WB, чистую прибыль, 
    рентабельность/маржу, сумму комиссий, логистики, хранения и кол-во убыточных товаров).
    """
    return await _api_get("/finance/summary")


@tool
async def get_finance_products(
    limit: int = 50,
    status: Optional[str] = None,
    sort_by: str = "profit"
) -> Any:
    """Список товаров с основной финансовой информацией (маржа, прибыль, ROI).
    
    Args:
        limit: Сколько товаров вернуть (по умолчанию 50).
        status: Опциональный фильтр статуса. Допустимые:
            'loss', 'low_margin', 'has_penalties', 'profit_falling', 'ok'.
        sort_by: Поле сортировки (по умолчанию 'profit'). Допустимые:
            'profit', 'gmv', 'margin_net_pct', 'roi_pct', 'unit_profit_net'.
    """
    if status is not None and status not in ALLOWED_FINANCE_STATUSES:
        raise ValueError(
            f"Некорректный status='{status}'. Допустимо: {sorted(ALLOWED_FINANCE_STATUSES)}"
        )
    if sort_by not in ALLOWED_FINANCE_SORT:
        raise ValueError(
            f"Некорректный sort_by='{sort_by}'. Допустимо: {sorted(ALLOWED_FINANCE_SORT)}"
        )
    return await _api_get("/finance/products", {"limit": limit, "status": status, "sort_by": sort_by})


@tool
async def get_finance_product(nm_id: int) -> Any:
    """Полная финансовая карточка конкретного товара.
    
    Возвращает выручку, все виды расходов (комиссия, логистика, штрафы),
    юнит-экономику, динамику и ROI.
    
    Args:
        nm_id: Артикул Wildberries.
    """
    return await _api_get(f"/finance/product/{nm_id}")


@tool
async def get_loss_products(limit: int = 50) -> Any:
    """Убыточные товары.
    
    Возвращает товары с отрицательной чистой прибылью (profit_net < 0).
    
    Args:
        limit: Сколько товаров вернуть.
    """
    return await _api_get("/finance/loss", {"limit": limit})


@tool
async def get_low_margin_products(threshold: float = 5.0, limit: int = 50) -> Any:
    """Товары с низкой маржой.
    
    Возвращает товары, у которых маржа (margin_net_pct) ниже заданного порога.
    
    Args:
        threshold: Порог маржи в процентах (по умолчанию 5.0%).
        limit: Сколько товаров вернуть.
    """
    return await _api_get("/finance/low-margin", {"threshold": threshold, "limit": limit})


@tool
async def get_top_profit_products(limit: int = 50) -> Any:
    """Самые прибыльные товары.
    
    Возвращает ТОП товаров по абсолютной чистой прибыли (profit_net > 0).
    
    Args:
        limit: Сколько товаров вернуть.
    """
    return await _api_get("/finance/top-profit", {"limit": limit})


@tool
async def get_top_roi_products(limit: int = 50) -> Any:
    """Товары с лучшим возвратом на инвестиции (ROI).
    
    Args:
        limit: Сколько товаров вернуть.
    """
    return await _api_get("/finance/top-roi", {"limit": limit})


@tool
async def get_unit_economics(limit: int = 50, sort_by: str = "unit_profit_net") -> Any:
    """Юнит-экономика товаров (в пересчете на 1 штуку).
    
    Разбивка доходов и расходов на 1 единицу проданного товара.
    
    Args:
        limit: Сколько товаров вернуть.
        sort_by: Поле сортировки (по умолчанию 'unit_profit_net'). Допустимые:
            'unit_profit_net', 'unit_revenue', 'unit_cost'.
    """
    if sort_by not in ALLOWED_UNIT_ECONOMICS_SORT:
        raise ValueError(
            f"Некорректный sort_by='{sort_by}'. Допустимо: {sorted(ALLOWED_UNIT_ECONOMICS_SORT)}"
        )
    return await _api_get("/finance/unit-economics", {"limit": limit, "sort_by": sort_by})


@tool
async def get_expenses_breakdown() -> Any:
    """Структура расходов продавца на WB.
    
    Разбивка в абсолютных значениях и процентах: комиссия, эквайринг,
    логистика, хранение, штрафы, платная приемка.
    """
    return await _api_get("/finance/expenses-breakdown")


@tool
async def get_finance_trend(days: int = 30) -> Any:
    """Динамика финансов по дням (тренд).
    
    Возвращает: продажи, GMV, прибыль, комиссию и расходы по дням
    для построения графиков.
    
    Args:
        days: Период в днях (по умолчанию 30).
    """
    return await _api_get("/finance/trend", {"days": days})


@tool
async def get_category_report() -> Any:
    """Финансы агрегированные по категориям товаров.
    
    Аналитика по продажам, выручке, прибыли, средней марже и ROI 
    в разрезе каждой категории (предмета) магазина.
    """
    return await _api_get("/finance/category-report")


@tool
async def add_finance_insight(nm_id: int, tag: str, recommendation: str, score: float) -> Any:
    """Сохранить AI-рекомендацию по финансам для товара в базу.
    
    Args:
        nm_id: Артикул Wildberries.
        tag: Тег рекомендации (например 'reduce_cost', 'increase_price').
        recommendation: Конкретное действие по улучшению юнит-экономики с цифрами.
        score: Уверенность от 0.5 до 1.0.
    """
    return await _api_post("/insights/add", {
        "nm_id": nm_id,
        "tag": tag,
        "recommendation": recommendation,
        "score": score
    })


# Экспортируем все 12 инструментов одним списком
finance_tools = [
    get_finance_summary,
    get_finance_products,
    get_finance_product,
    get_loss_products,
    get_low_margin_products,
    get_top_profit_products,
    get_top_roi_products,
    get_unit_economics,
    get_expenses_breakdown,
    get_finance_trend,
    get_category_report,
    add_finance_insight,
]