ADOLF SCOUT — Раздел 4: Open WebUI
Проект: ÐŸÑ€ÐµÐ´Ð¸ÐºÑ‚Ð¸Ð²Ð½Ð°Ñ Ð°Ð½Ð°Ð»Ð¸Ñ‚Ð¸ÐºÐ° товарных ниш
Модуль: Scout / Open WebUI
ВерÑиÑ: 1.0
Дата: Январь 2026
4.1 Обзор интеграции
Примечание (Март 2026): Ниже описана архитектура интеграции через chat-агента. Фактическая реализация использует standalone-страницу
/scout. Структура страницы:
Вкладка Описание Новый анализ Запуск нового анализа ниши Результат Просмотр результатов анализа История История предыдущих анализов Документация агента ниже сохранена как спецификация backend API.
Ðрхитектура взаимодейÑтвиÑ
Компоненты интеграции
| Компонент | Тип | Ðазначение |
|---|---|---|
@Adolf_Scout | Pipeline | ОÑновной обработчик запроÑов |
scout_analyze_niche | Tool | Ðнализ ниши |
scout_get_history | Tool | Получение иÑтории |
scout_compare | Tool | Сравнение анализов |
scout_export | Tool | ÐкÑпорт отчёта |
scout_update_rates | Tool | Обновление Ñтавок МП |
4.2 Pipeline @Adolf_Scout
4.2.1 КонфигурациÑ
# pipelines/adolf_scout.py
"""
title: Adolf Scout Pipeline
description: ÐŸÑ€ÐµÐ´Ð¸ÐºÑ‚Ð¸Ð²Ð½Ð°Ñ Ð°Ð½Ð°Ð»Ð¸Ñ‚Ð¸ÐºÐ° товарных ниш Ð´Ð»Ñ e-commerce
author: ADOLF Team
version: 1.0.0
license: MIT
requirements:
- httpx>=0.25.0
- pydantic>=2.0.0
"""
from typing import Optional, Dict, Any, List, Generator
from pydantic import BaseModel, Field
import httpx
import json
class Pipeline:
"""Pipeline Ð´Ð»Ñ Ð°Ð½Ð°Ð»Ð¸Ð·Ð° товарных ниш."""
class Valves(BaseModel):
"""ÐаÑтройки Pipeline."""
MIDDLEWARE_URL: str = Field(
default="http://middleware:8000",
description="URL ADOLF Middleware"
)
REQUEST_TIMEOUT: int = Field(
default=120,
description="Таймаут запроÑа в Ñекундах"
)
ENABLE_STREAMING: bool = Field(
default=True,
description="Включить Ñтриминг ответа"
)
DEBUG_MODE: bool = Field(
default=False,
description="Режим отладки"
)
def __init__(self):
self.name = "Adolf Scout"
self.valves = self.Valves()
self.client = None
async def on_startup(self):
"""Ð˜Ð½Ð¸Ñ†Ð¸Ð°Ð»Ð¸Ð·Ð°Ñ†Ð¸Ñ Ð¿Ñ€Ð¸ запуÑке."""
self.client = httpx.AsyncClient(
base_url=self.valves.MIDDLEWARE_URL,
timeout=self.valves.REQUEST_TIMEOUT
)
print(f"[Scout] Pipeline started, middleware: {self.valves.MIDDLEWARE_URL}")
async def on_shutdown(self):
"""ОчиÑтка при оÑтановке."""
if self.client:
await self.client.aclose()
print("[Scout] Pipeline stopped")
def pipe(
self,
body: Dict[str, Any],
__user__: Dict[str, Any],
__event_emitter__=None
) -> Generator[str, None, None]:
"""
ОÑновной метод обработки запроÑа.
Args:
body: Тело запроÑа от Open WebUI
__user__: Данные пользователÑ
__event_emitter__: Ðмиттер Ñобытий Ð´Ð»Ñ UI
"""
import asyncio
# ЗапуÑк async обработки
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
async_gen = self._process_request(body, __user__, __event_emitter__)
while True:
try:
chunk = loop.run_until_complete(async_gen.__anext__())
yield chunk
except StopAsyncIteration:
break
finally:
loop.close()
async def _process_request(
self,
body: Dict[str, Any],
user: Dict[str, Any],
event_emitter
):
"""ÐÑÐ¸Ð½Ñ…Ñ€Ð¾Ð½Ð½Ð°Ñ Ð¾Ð±Ñ€Ð°Ð±Ð¾Ñ‚ÐºÐ° запроÑа."""
# Извлечение ÑообщениÑ
messages = body.get("messages", [])
if not messages:
yield "ПожалуйÑта, укажите Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð´Ð»Ñ Ð°Ð½Ð°Ð»Ð¸Ð·Ð° ниши."
return
user_message = messages[-1].get("content", "")
user_id = user.get("id")
user_role = user.get("role", "user")
auth_token = user.get("token", "")
# Проверка доÑтупа (Senior+)
if not await self._check_access(user_role):
yield "â›” ДоÑтуп к модулю Scout ограничен. ТребуетÑÑ Ñ€Ð¾Ð»ÑŒ Senior или выше."
return
# Определение типа запроÑа
request_type = self._detect_request_type(user_message)
# Обработка по типу
if request_type == "analyze":
async for chunk in self._handle_analyze(user_message, user_id, auth_token, event_emitter):
yield chunk
elif request_type == "history":
async for chunk in self._handle_history(user_message, user_id, auth_token):
yield chunk
elif request_type == "compare":
async for chunk in self._handle_compare(user_message, user_id, auth_token):
yield chunk
elif request_type == "export":
async for chunk in self._handle_export(user_message, user_id, auth_token, event_emitter):
yield chunk
elif request_type == "rates":
async for chunk in self._handle_rates(user_message, user_id, user_role, auth_token):
yield chunk
else:
yield self._get_help_message()
def _detect_request_type(self, message: str) -> str:
"""Определение типа запроÑа."""
message_lower = message.lower()
# ИÑториÑ
if any(kw in message_lower for kw in ["иÑториÑ", "history", "прошлые", "предыдущие"]):
return "history"
# Сравнение
if any(kw in message_lower for kw in ["Ñравни", "compare", "Ñравнение"]):
return "compare"
# ÐкÑпорт
if any(kw in message_lower for kw in ["ÑкÑпорт", "export", "Ñкачать", "pdf", "excel"]):
return "export"
# Ставки
if any(kw in message_lower for kw in ["Ñтавк", "комиÑÑи", "rates", "overhead"]):
return "rates"
# Ðнализ (по умолчанию)
if any(kw in message_lower for kw in [
"анализ", "analyze", "оцени", "проанализируй", "ниш", "категори",
"wildberries", "ozon", "ÑндекÑ", "маркет", "wb", "вб", "cogs", "закупк"
]):
return "analyze"
# Справка
return "help"
async def _check_access(self, role: str) -> bool:
"""Проверка доÑтупа по роли."""
allowed_roles = ["senior", "senior_manager", "director", "admin", "administrator"]
return role.lower() in allowed_roles
async def _handle_analyze(
self,
message: str,
user_id: str,
token: str,
event_emitter
):
"""Обработка запроÑа на анализ."""
# ПрогреÑÑ: начало
if event_emitter:
await event_emitter({
"type": "status",
"data": {"description": "🔠Ðачинаю анализ ниши...", "done": False}
})
yield "🔠**Ðнализирую нишу...**\n\n"
try:
# Ð—Ð°Ð¿Ñ€Ð¾Ñ Ðº API
response = await self.client.post(
"/api/v1/scout/analyze",
json={"query": message, "user_id": user_id},
headers={"Authorization": f"Bearer {token}"}
)
if response.status_code == 401:
yield "â›” Ошибка авторизации. ПожалуйÑта, войдите в ÑиÑтему заново."
return
if response.status_code == 400:
error = response.json().get("detail", "Ðеверный запроÑ")
yield f"âš ï¸ {error}\n\n"
yield self._get_analyze_help()
return
response.raise_for_status()
result = response.json()
# ПрогреÑÑ: завершение
if event_emitter:
await event_emitter({
"type": "status",
"data": {"description": "✅ Ðнализ завершён", "done": True}
})
# Форматирование результата
yield self._format_verdict_result(result)
except httpx.TimeoutException:
yield "â±ï¸ Превышено Ð²Ñ€ÐµÐ¼Ñ Ð¾Ð¶Ð¸Ð´Ð°Ð½Ð¸Ñ. Попробуйте повторить запроÑ."
except Exception as e:
if self.valves.DEBUG_MODE:
yield f"⌠Ошибка: {str(e)}"
else:
yield "⌠Произошла ошибка при анализе. Попробуйте позже."
async def _handle_history(
self,
message: str,
user_id: str,
token: str
):
"""Обработка запроÑа иÑтории."""
# ПарÑинг параметров
limit = 10
if "поÑледн" in message.lower():
import re
match = re.search(r"(\d+)", message)
if match:
limit = min(int(match.group(1)), 50)
try:
response = await self.client.get(
"/api/v1/scout/history",
params={"user_id": user_id, "limit": limit},
headers={"Authorization": f"Bearer {token}"}
)
response.raise_for_status()
history = response.json()
yield self._format_history(history)
except Exception as e:
yield f"⌠Ошибка загрузки иÑтории: {str(e)}"
async def _handle_compare(
self,
message: str,
user_id: str,
token: str
):
"""Обработка запроÑа ÑравнениÑ."""
yield "🔄 Ð¤ÑƒÐ½ÐºÑ†Ð¸Ñ ÑÑ€Ð°Ð²Ð½ÐµÐ½Ð¸Ñ Ð°Ð½Ð°Ð»Ð¸Ð·Ð¾Ð² будет доÑтупна в Ñледующей верÑии."
async def _handle_export(
self,
message: str,
user_id: str,
token: str,
event_emitter
):
"""Обработка запроÑа ÑкÑпорта."""
# Определение формата
export_format = "pdf"
if "excel" in message.lower() or "xlsx" in message.lower():
export_format = "xlsx"
# ПоиÑк ID анализа
import re
id_match = re.search(r"([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})", message)
if not id_match:
# ÐкÑпорт поÑледнего анализа
yield "📤 ÐкÑпортирую поÑледний анализ...\n\n"
analysis_id = "latest"
else:
analysis_id = id_match.group(1)
yield f"📤 ÐкÑпортирую анализ `{analysis_id[:8]}...`\n\n"
try:
response = await self.client.post(
"/api/v1/scout/export",
json={
"analysis_id": analysis_id,
"format": export_format,
"user_id": user_id
},
headers={"Authorization": f"Bearer {token}"}
)
response.raise_for_status()
result = response.json()
download_url = result.get("download_url")
yield f"✅ Отчёт готов!\n\n"
yield f"📥 [Скачать {export_format.upper()}]({download_url})\n\n"
yield f"_СÑылка дейÑтвительна 24 чаÑа._"
except Exception as e:
yield f"⌠Ошибка ÑкÑпорта: {str(e)}"
async def _handle_rates(
self,
message: str,
user_id: str,
user_role: str,
token: str
):
"""Обработка запроÑа по Ñтавкам."""
message_lower = message.lower()
# ПроÑмотр Ñтавок
if any(kw in message_lower for kw in ["покажи", "текущи", "show", "view"]):
try:
response = await self.client.get(
"/api/v1/scout/rates",
headers={"Authorization": f"Bearer {token}"}
)
response.raise_for_status()
rates = response.json()
yield self._format_rates(rates)
except Exception as e:
yield f"⌠Ошибка загрузки Ñтавок: {str(e)}"
# Изменение Ñтавок
elif any(kw in message_lower for kw in ["измени", "уÑтанови", "update", "set"]):
# Проверка прав (Senior+)
if user_role.lower() not in ["senior", "senior_manager", "director", "admin", "administrator"]:
yield "â›” Изменение Ñтавок доÑтупно только Ð´Ð»Ñ Senior и выше."
return
yield "âœï¸ Ð”Ð»Ñ Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ñтавок иÑпользуйте кнопку **«ÐаÑтройки Ñтавок»** ниже."
yield "\n\n"
yield self._get_rates_form()
else:
yield self._format_rates_help()
def _format_verdict_result(self, result: Dict) -> str:
"""Форматирование результата анализа."""
verdict = result.get("verdict", "UNKNOWN")
color = result.get("color", "gray")
# Ðмодзи Ñветофора
verdict_emoji = {
"GO": "🟢",
"CONSIDER": "🟡",
"RISKY": "🔴"
}.get(verdict, "⚪")
# Заголовок
output = f"## {verdict_emoji} Вердикт: **{verdict}**\n\n"
# Summary
summary = result.get("summary", "")
if summary:
output += f"_{summary}_\n\n"
# Ключевые метрики
metrics = result.get("metrics", {})
output += "### 📊 Ключевые метрики\n\n"
output += "| Метрика | Значение | Ð¡Ñ‚Ð°Ñ‚ÑƒÑ |\n"
output += "|---------|----------|--------|\n"
# Trend
trend_slope = metrics.get("trend_slope", 0)
trend_status = metrics.get("trend_status", "unknown")
trend_emoji = {"green": "🟢", "yellow": "🟡", "red": "🔴"}.get(trend_status, "⚪")
output += f"| Тренд ÑпроÑа | {trend_slope:+.2f} | {trend_emoji} |\n"
# Monopoly
monopoly_rate = metrics.get("monopoly_rate", 0)
monopoly_status = metrics.get("monopoly_status", "unknown")
monopoly_emoji = {"green": "🟢", "yellow": "🟡", "red": "🔴"}.get(monopoly_status, "⚪")
output += f"| ÐœÐ¾Ð½Ð¾Ð¿Ð¾Ð»Ð¸Ð·Ð°Ñ†Ð¸Ñ | {monopoly_rate*100:.0f}% | {monopoly_emoji} |\n"
# Margin
margin = metrics.get("expected_margin", 0)
margin_status = metrics.get("margin_status", "unknown")
margin_emoji = {"green": "🟢", "yellow": "🟡", "red": "🔴"}.get(margin_status, "⚪")
output += f"| Ожид. маржа | {margin:.1f}% | {margin_emoji} |\n"
output += "\n"
# Unit-Ñкономика
unit_economics = result.get("unit_economics", {})
if unit_economics:
output += "### 💰 Unit-Ñкономика\n\n"
for mp, econ in unit_economics.items():
mp_name = {"wildberries": "Wildberries", "ozon": "Ozon", "yandex_market": "ЯндекÑ.Маркет"}.get(mp, mp)
output += f"**{mp_name}**\n"
output += f"- Цена продажи: {econ.get('selling_price', 0):.0f} ₽\n"
output += f"- СебеÑтоимоÑть: {econ.get('cogs', 0):.0f} ₽\n"
output += f"- РаÑходы МП: {econ.get('total_overhead_pct', 0):.1f}%\n"
output += f"- ЧиÑÑ‚Ð°Ñ Ð¿Ñ€Ð¸Ð±Ñ‹Ð»ÑŒ: {econ.get('net_profit', 0):.0f} ₽\n"
output += f"- ЧиÑÑ‚Ð°Ñ Ð¼Ð°Ñ€Ð¶Ð°: **{econ.get('net_margin_pct', 0):.1f}%**\n"
output += f"- Цена Ð´Ð»Ñ 25% маржи: {econ.get('target_price_25', 0):.0f} ₽\n\n"
# Рекомендации
recommendations = result.get("recommendations", [])
if recommendations:
output += "### 💡 Рекомендации\n\n"
for i, rec in enumerate(recommendations, 1):
output += f"{i}. {rec}\n"
output += "\n"
# РиÑки
risks = result.get("risks", [])
if risks:
output += "### âš ï¸ Ð Ð¸Ñки\n\n"
for risk in risks[:5]:
if isinstance(risk, dict):
output += f"- **{risk.get('risk', '')}** ({risk.get('probability', '')})\n"
mitigation = risk.get('mitigation', '')
if mitigation:
output += f" _МитигациÑ: {mitigation}_\n"
else:
output += f"- {risk}\n"
output += "\n"
# ВозможноÑти
opportunities = result.get("opportunities", [])
if opportunities:
output += "### 🚀 ВозможноÑти\n\n"
for opp in opportunities[:5]:
output += f"- {opp}\n"
output += "\n"
# Метаданные
analysis_id = result.get("analysis_id", "")
analyzed_at = result.get("analyzed_at", "")
output += "---\n"
output += f"_ID анализа: `{analysis_id[:8]}...` | {analyzed_at[:10]}_\n\n"
# Кнопки дейÑтвий
output += self._get_action_buttons(analysis_id)
return output
def _format_history(self, history: Dict) -> str:
"""Форматирование иÑтории анализов."""
analyses = history.get("analyses", [])
if not analyses:
return "📋 ИÑÑ‚Ð¾Ñ€Ð¸Ñ Ð°Ð½Ð°Ð»Ð¸Ð·Ð¾Ð² пуÑта.\n\nПопробуйте: «Проанализируй нишу летних платьев на WB, закупка 500₽»"
output = "## 📋 ИÑÑ‚Ð¾Ñ€Ð¸Ñ Ð°Ð½Ð°Ð»Ð¸Ð·Ð¾Ð²\n\n"
output += "| Дата | Ð—Ð°Ð¿Ñ€Ð¾Ñ | Вердикт | Маржа |\n"
output += "|------|--------|---------|-------|\n"
for item in analyses:
date = item.get("analyzed_at", "")[:10]
query = item.get("query", "")[:30]
if len(item.get("query", "")) > 30:
query += "..."
verdict = item.get("verdict", "")
verdict_emoji = {"GO": "🟢", "CONSIDER": "🟡", "RISKY": "🔴"}.get(verdict, "⚪")
margin = item.get("metrics", {}).get("expected_margin", 0)
output += f"| {date} | {query} | {verdict_emoji} {verdict} | {margin:.1f}% |\n"
output += "\n"
output += "_Ð”Ð»Ñ Ð´ÐµÑ‚Ð°Ð»ÑŒÐ½Ð¾Ð³Ð¾ проÑмотра укажите ID анализа._"
return output
def _format_rates(self, rates: Dict) -> str:
"""Форматирование Ñтавок МП."""
output = "## 📊 Текущие Ñтавки маркетплейÑов\n\n"
output += "| Ð¡Ñ‚Ð°Ñ‚ÑŒÑ | Wildberries | Ozon | ЯндекÑ.Маркет |\n"
output += "|--------|:-----------:|:----:|:-------------:|\n"
wb = rates.get("wildberries", {})
ozon = rates.get("ozon", {})
ym = rates.get("yandex_market", {})
output += f"| КомиÑÑÐ¸Ñ | {wb.get('commission_pct', 0)}% | {ozon.get('commission_pct', 0)}% | {ym.get('commission_pct', 0)}% |\n"
output += f"| ЛогиÑтика | {wb.get('logistics_pct', 0)}% | {ozon.get('logistics_pct', 0)}% | {ym.get('logistics_pct', 0)}% |\n"
output += f"| Возвраты | {wb.get('return_logistics_pct', 0)}% | {ozon.get('return_logistics_pct', 0)}% | {ym.get('return_logistics_pct', 0)}% |\n"
output += f"| Хранение | {wb.get('storage_pct', 0)}% | {ozon.get('storage_pct', 0)}% | {ym.get('storage_pct', 0)}% |\n"
output += f"| Ðквайринг | {wb.get('acquiring_pct', 0)}% | {ozon.get('acquiring_pct', 0)}% | {ym.get('acquiring_pct', 0)}% |\n"
output += f"| **Итого** | **{wb.get('total_overhead_pct', 0)}%** | **{ozon.get('total_overhead_pct', 0)}%** | **{ym.get('total_overhead_pct', 0)}%** |\n"
output += "\n_Ставки можно изменить (Senior+): «Измени Ñтавки»_"
return output
def _format_rates_help(self) -> str:
"""Справка по Ñтавкам."""
return """## âš™ï¸ Ð£Ð¿Ñ€Ð°Ð²Ð»ÐµÐ½Ð¸Ðµ Ñтавками
**ПроÑмотр Ñтавок:**
- «Покажи текущие Ñтавки»
- «Какие комиÑÑии маркетплейÑов?»
**Изменение Ñтавок (Senior+):**
- «Измени Ñтавки»
- «УÑтанови комиÑÑию WB 16%»
"""
def _get_action_buttons(self, analysis_id: str) -> str:
"""Кнопки дейÑтвий поÑле анализа."""
return f"""<div class="scout-actions">
<button onclick="sendMessage('ÐкÑпорт в PDF {analysis_id}')">📥 ÐкÑпорт PDF</button>
<button onclick="sendMessage('ÐкÑпорт в Excel {analysis_id}')">📊 ÐкÑпорт Excel</button>
<button onclick="sendMessage('ИÑÑ‚Ð¾Ñ€Ð¸Ñ Ð°Ð½Ð°Ð»Ð¸Ð·Ð¾Ð²')">📋 ИÑториÑ</button>
</div>
"""
def _get_rates_form(self) -> str:
"""Форма наÑтройки Ñтавок."""
return """<div class="scout-rates-form">
<p>Ð”Ð»Ñ Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ñтавок отправьте Ñообщение в формате:</p>
<code>УÑтанови Ñтавки WB: комиÑÑÐ¸Ñ 16%, логиÑтика 5%, возвраты 3%, хранение 1%</code>
</div>
"""
def _get_help_message(self) -> str:
"""Справочное Ñообщение."""
return """## 🔠ADOLF Scout — Ðнализ товарных ниш
### Как иÑпользовать
**Ðнализ ниши:**Проанализируй нишу летних платьев на Wildberries, закупка 500 рублей
Оцени https://www.ozon.ru/category/platya-zhenskie-7502/, COGS от 400 до 600₽
**ИÑториÑ:**Покажи иÑторию анализов Покажи поÑледние 5 анализов
**ÐкÑпорт:**ÐкÑпорт в PDF ÐкÑпорт в Excel
**Ставки:**Покажи текущие Ñтавки Измени Ñтавки
### Пороги «Светофора»
| Метрика | 🟢 GO | 🟡 CONSIDER | 🔴 RISKY |
|---------|-------|-------------|----------|
| Тренд | > +0.15 | 0 — 0.15 | < 0 |
| ÐœÐ¾Ð½Ð¾Ð¿Ð¾Ð»Ð¸Ð·Ð°Ñ†Ð¸Ñ | < 50% | 50-70% | > 70% |
| Маржа | > 25% | 15-25% | < 15% |
---
_Модуль доÑтупен Ð´Ð»Ñ Ñ€Ð¾Ð»ÐµÐ¹: Senior, Director, Administrator_
"""
def _get_analyze_help(self) -> str:
"""Справка по анализу."""
return """**Формат запроÑа:**
1. Укажите нишу/категорию (текÑÑ‚ или URL)
2. Укажите закупочную цену (COGS)
**Примеры:**
- `Ðнализ ниши детÑких комбинезонов, закупка 800₽`
- `Оцени https://www.wildberries.ru/catalog/..., COGS 500`
- `Летние Ð¿Ð»Ð°Ñ‚ÑŒÑ Ð½Ð° Ozon, от 400 до 600 рублей`
"""4.3 Tools
4.3.1 Tool: scout_analyze_niche
# tools/scout_analyze_niche.py
"""
title: Scout Analyze Niche
description: Ðнализ товарной ниши Ð´Ð»Ñ Ð¾Ñ†ÐµÐ½ÐºÐ¸ целеÑообразноÑти входа
author: ADOLF Team
version: 1.0.0
"""
from typing import Optional
from pydantic import BaseModel, Field
class Tools:
"""Tools Ð´Ð»Ñ Ð°Ð½Ð°Ð»Ð¸Ð·Ð° ниши."""
class Valves(BaseModel):
MIDDLEWARE_URL: str = Field(default="http://middleware:8000")
class UserValves(BaseModel):
pass
def __init__(self):
self.valves = self.Valves()
async def analyze_niche(
self,
query: str,
cogs: float,
marketplace: Optional[str] = None,
__user__: dict = {}
) -> str:
"""
Ðнализ товарной ниши.
Args:
query: ПоиÑковый Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð¸Ð»Ð¸ URL категории
cogs: Ð—Ð°ÐºÑƒÐ¿Ð¾Ñ‡Ð½Ð°Ñ Ñ†ÐµÐ½Ð° в рублÑÑ…
marketplace: ÐœÐ°Ñ€ÐºÐµÑ‚Ð¿Ð»ÐµÐ¹Ñ (wildberries/ozon/yandex_market), опционально
Returns:
Результат анализа Ñ Ð²ÐµÑ€Ð´Ð¸ÐºÑ‚Ð¾Ð¼
"""
import httpx
user_id = __user__.get("id")
token = __user__.get("token", "")
# Формирование запроÑа
request_body = {
"query": query,
"cogs": cogs,
"user_id": user_id
}
if marketplace:
request_body["marketplaces"] = [marketplace]
async with httpx.AsyncClient(timeout=120) as client:
response = await client.post(
f"{self.valves.MIDDLEWARE_URL}/api/v1/scout/analyze",
json=request_body,
headers={"Authorization": f"Bearer {token}"}
)
if response.status_code == 403:
return "â›” ДоÑтуп запрещён. ТребуетÑÑ Ñ€Ð¾Ð»ÑŒ Senior или выше."
response.raise_for_status()
result = response.json()
# Форматирование результата
verdict = result.get("verdict", "UNKNOWN")
emoji = {"GO": "🟢", "CONSIDER": "🟡", "RISKY": "🔴"}.get(verdict, "⚪")
summary = result.get("summary", "")
metrics = result.get("metrics", {})
output = f"{emoji} **{verdict}**\n\n"
output += f"{summary}\n\n"
output += f"- Тренд: {metrics.get('trend_slope', 0):+.2f}\n"
output += f"- МонополизациÑ: {metrics.get('monopoly_rate', 0)*100:.0f}%\n"
output += f"- Ожид. маржа: {metrics.get('expected_margin', 0):.1f}%\n"
return output4.3.2 Tool: scout_get_history
# tools/scout_get_history.py
"""
title: Scout Get History
description: Получение иÑтории анализов ниш
author: ADOLF Team
version: 1.0.0
"""
from typing import Optional
from pydantic import BaseModel, Field
class Tools:
"""Tools Ð´Ð»Ñ Ð¸Ñтории анализов."""
class Valves(BaseModel):
MIDDLEWARE_URL: str = Field(default="http://middleware:8000")
def __init__(self):
self.valves = self.Valves()
async def get_history(
self,
limit: int = 10,
query_filter: Optional[str] = None,
__user__: dict = {}
) -> str:
"""
Получение иÑтории анализов.
Args:
limit: КоличеÑтво запиÑей (макÑ. 50)
query_filter: Фильтр по запроÑу (опционально)
Returns:
СпиÑок поÑледних анализов
"""
import httpx
user_id = __user__.get("id")
token = __user__.get("token", "")
params = {
"user_id": user_id,
"limit": min(limit, 50)
}
if query_filter:
params["query"] = query_filter
async with httpx.AsyncClient(timeout=30) as client:
response = await client.get(
f"{self.valves.MIDDLEWARE_URL}/api/v1/scout/history",
params=params,
headers={"Authorization": f"Bearer {token}"}
)
response.raise_for_status()
data = response.json()
analyses = data.get("analyses", [])
if not analyses:
return "📋 ИÑÑ‚Ð¾Ñ€Ð¸Ñ Ð°Ð½Ð°Ð»Ð¸Ð·Ð¾Ð² пуÑта."
output = "📋 **ИÑÑ‚Ð¾Ñ€Ð¸Ñ Ð°Ð½Ð°Ð»Ð¸Ð·Ð¾Ð²:**\n\n"
for item in analyses:
date = item.get("analyzed_at", "")[:10]
query = item.get("query", "")[:25]
verdict = item.get("verdict", "")
emoji = {"GO": "🟢", "CONSIDER": "🟡", "RISKY": "🔴"}.get(verdict, "⚪")
output += f"- {date} | {emoji} {verdict} | {query}\n"
return output4.3.3 Tool: scout_export
# tools/scout_export.py
"""
title: Scout Export
description: ÐкÑпорт отчёта по анализу ниши
author: ADOLF Team
version: 1.0.0
"""
from typing import Optional, Literal
from pydantic import BaseModel, Field
class Tools:
"""Tools Ð´Ð»Ñ ÑкÑпорта отчётов."""
class Valves(BaseModel):
MIDDLEWARE_URL: str = Field(default="http://middleware:8000")
def __init__(self):
self.valves = self.Valves()
async def export_report(
self,
analysis_id: Optional[str] = None,
format: Literal["pdf", "xlsx"] = "pdf",
__user__: dict = {}
) -> str:
"""
ÐкÑпорт отчёта по анализу.
Args:
analysis_id: ID анализа (еÑли не указан — поÑледний)
format: Формат отчёта (pdf или xlsx)
Returns:
СÑылка Ð´Ð»Ñ ÑкачиваниÑ
"""
import httpx
user_id = __user__.get("id")
token = __user__.get("token", "")
request_body = {
"analysis_id": analysis_id or "latest",
"format": format,
"user_id": user_id
}
async with httpx.AsyncClient(timeout=60) as client:
response = await client.post(
f"{self.valves.MIDDLEWARE_URL}/api/v1/scout/export",
json=request_body,
headers={"Authorization": f"Bearer {token}"}
)
response.raise_for_status()
result = response.json()
download_url = result.get("download_url", "")
return f"📥 [Скачать отчёт ({format.upper()})]({download_url})\n\n_СÑылка дейÑтвительна 24 чаÑа._"4.3.4 Tool: scout_update_rates
# tools/scout_update_rates.py
"""
title: Scout Update Rates
description: Обновление Ñтавок маркетплейÑов
author: ADOLF Team
version: 1.0.0
"""
from typing import Optional
from pydantic import BaseModel, Field
class Tools:
"""Tools Ð´Ð»Ñ ÑƒÐ¿Ñ€Ð°Ð²Ð»ÐµÐ½Ð¸Ñ Ñтавками."""
class Valves(BaseModel):
MIDDLEWARE_URL: str = Field(default="http://middleware:8000")
def __init__(self):
self.valves = self.Valves()
async def get_rates(
self,
marketplace: Optional[str] = None,
__user__: dict = {}
) -> str:
"""
Получение текущих Ñтавок.
Args:
marketplace: Фильтр по маркетплейÑу (опционально)
Returns:
Таблица Ñтавок
"""
import httpx
token = __user__.get("token", "")
params = {}
if marketplace:
params["marketplace"] = marketplace
async with httpx.AsyncClient(timeout=30) as client:
response = await client.get(
f"{self.valves.MIDDLEWARE_URL}/api/v1/scout/rates",
params=params,
headers={"Authorization": f"Bearer {token}"}
)
response.raise_for_status()
rates = response.json()
output = "📊 **Ставки маркетплейÑов:**\n\n"
for mp, data in rates.items():
mp_name = {"wildberries": "WB", "ozon": "Ozon", "yandex_market": "YM"}.get(mp, mp)
output += f"**{mp_name}:** комиÑÑÐ¸Ñ {data.get('commission_pct')}%, "
output += f"логиÑтика {data.get('logistics_pct')}%, "
output += f"итого {data.get('total_overhead_pct')}%\n"
return output
async def update_rates(
self,
marketplace: str,
commission_pct: Optional[float] = None,
logistics_pct: Optional[float] = None,
return_logistics_pct: Optional[float] = None,
storage_pct: Optional[float] = None,
acquiring_pct: Optional[float] = None,
__user__: dict = {}
) -> str:
"""
Обновление Ñтавок маркетплейÑа.
Args:
marketplace: Код маркетплейÑа (wildberries/ozon/yandex_market)
commission_pct: КомиÑÑÐ¸Ñ (%)
logistics_pct: ЛогиÑтика (%)
return_logistics_pct: Возвраты (%)
storage_pct: Хранение (%)
acquiring_pct: Ðквайринг (%)
Returns:
Результат обновлениÑ
"""
import httpx
user_id = __user__.get("id")
user_role = __user__.get("role", "")
token = __user__.get("token", "")
# Проверка прав
allowed = ["senior", "senior_manager", "director", "admin", "administrator"]
if user_role.lower() not in allowed:
return "â›” Изменение Ñтавок доÑтупно только Ð´Ð»Ñ Senior и выше."
# Формирование запроÑа
updates = {"marketplace": marketplace}
if commission_pct is not None:
updates["commission_pct"] = commission_pct
if logistics_pct is not None:
updates["logistics_pct"] = logistics_pct
if return_logistics_pct is not None:
updates["return_logistics_pct"] = return_logistics_pct
if storage_pct is not None:
updates["storage_pct"] = storage_pct
if acquiring_pct is not None:
updates["acquiring_pct"] = acquiring_pct
async with httpx.AsyncClient(timeout=30) as client:
response = await client.put(
f"{self.valves.MIDDLEWARE_URL}/api/v1/scout/rates",
json=updates,
headers={"Authorization": f"Bearer {token}"}
)
response.raise_for_status()
return f"✅ Ставки Ð´Ð»Ñ {marketplace} обновлены."4.4 REST API Endpoints
4.4.1 Обзор endpoints
| Endpoint | Метод | ОпиÑание | Роли |
|---|---|---|---|
/api/v1/scout/analyze | POST | Ðнализ ниши | Senior+ |
/api/v1/scout/history | GET | ИÑÑ‚Ð¾Ñ€Ð¸Ñ Ð°Ð½Ð°Ð»Ð¸Ð·Ð¾Ð² | Senior+ |
/api/v1/scout/history/{id} | GET | Детали анализа | Senior+ |
/api/v1/scout/export | POST | ÐкÑпорт отчёта | Senior+ |
/api/v1/scout/rates | GET | Получение Ñтавок | Senior+ |
/api/v1/scout/rates | PUT | Обновление Ñтавок | Senior+ |
/api/v1/scout/settings | GET | ÐаÑтройки Ð¼Ð¾Ð´ÑƒÐ»Ñ | Admin |
/api/v1/scout/settings | PUT | Изменение наÑтроек | Admin |
4.4.2 POST /api/v1/scout/analyze
ЗапроÑ:
{
"query": "летние платьÑ",
"cogs": 500,
"cogs_min": null,
"cogs_max": null,
"marketplaces": ["wildberries", "ozon"],
"user_id": "user_123"
}Ответ (200 OK):
{
"analysis_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"query": "летние платьÑ",
"marketplaces": ["wildberries", "ozon"],
"verdict": "CONSIDER",
"color": "yellow",
"confidence": 0.78,
"summary": "Ðиша показывает Ñтабильный ÑпроÑ, но выÑÐ¾ÐºÐ°Ñ ÐºÐ¾Ð½ÐºÑƒÑ€ÐµÐ½Ñ†Ð¸Ñ Ð¸ ÑƒÐ¼ÐµÑ€ÐµÐ½Ð½Ð°Ñ Ð¼Ð°Ñ€Ð¶Ð° требуют тщательного анализа позиционированиÑ.",
"metrics": {
"trend_slope": 0.08,
"trend_status": "yellow",
"monopoly_rate": 0.52,
"monopoly_status": "yellow",
"expected_margin": 18.5,
"margin_status": "yellow"
},
"detailed_analysis": {
"trend_assessment": "Ð¡Ð¿Ñ€Ð¾Ñ Ñтабилен Ñ Ð½ÐµÐ±Ð¾Ð»ÑŒÑˆÐ¸Ð¼ роÑтом...",
"competition_assessment": "Рынок умеренно монополизирован...",
"economics_assessment": "МаржинальноÑть на нижней границе..."
},
"unit_economics": {
"wildberries": {
"selling_price": 2450,
"cogs": 500,
"total_overhead_pct": 24.0,
"net_profit": 362,
"net_margin_pct": 14.8,
"margin_status": "red",
"break_even_price": 658,
"target_price_25": 980
},
"ozon": {
"selling_price": 2600,
"cogs": 500,
"total_overhead_pct": 29.5,
"net_profit": 333,
"net_margin_pct": 12.8,
"margin_status": "red",
"break_even_price": 709,
"target_price_25": 1099
}
},
"recommendations": [
"РаÑÑмотрите нишевые подкатегории Ñ Ð¼ÐµÐ½ÑŒÑˆÐµÐ¹ конкуренцией",
"Ð”Ð»Ñ Ð´Ð¾ÑÑ‚Ð¸Ð¶ÐµÐ½Ð¸Ñ Ð¼Ð°Ñ€Ð¶Ð¸ 25% необходима Ð·Ð°ÐºÑƒÐ¿Ð¾Ñ‡Ð½Ð°Ñ Ñ†ÐµÐ½Ð° до 350₽",
"СфокуÑируйтеÑÑŒ на WB — overhead ниже на 5.5%"
],
"risks": [
{
"risk": "Ð’Ñ‹ÑÐ¾ÐºÐ°Ñ ÐºÐ¾Ð½ÐºÑƒÑ€ÐµÐ½Ñ†Ð¸Ñ Ð¾Ñ‚ крупных брендов",
"probability": "high",
"mitigation": "Уникальное позиционирование и нишевой дизайн"
},
{
"risk": "СезонноÑть ÑпроÑа",
"probability": "medium",
"mitigation": "Планирование закупок Ñ ÑƒÑ‡Ñ‘Ñ‚Ð¾Ð¼ пика в апреле-июне"
}
],
"opportunities": [
"РоÑÑ‚ ÑпроÑа на Ñкологичные материалы",
"ÐедоÑтаток Ð¿Ñ€ÐµÐ´Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð² Ñегменте plus-size"
],
"action_plan": {
"if_go": [
"ПровеÑти теÑтовую партию 50-100 единиц",
"ЗапуÑтить Ñначала на WB",
"Мониторить маржу первые 2 недели"
],
"if_not": [
"РаÑÑмотреть Ñмежные категории: Ñарафаны, туники",
"ИÑкать поÑтавщиков Ñ COGS < 400₽"
]
},
"price_recommendations": {
"optimal_price": 2800,
"min_viable_price": 2200,
"premium_price": 3500,
"reasoning": "ÐžÐ¿Ñ‚Ð¸Ð¼Ð°Ð»ÑŒÐ½Ð°Ñ Ñ†ÐµÐ½Ð° 2800₽ обеÑпечивает маржу ~22% при текущем COGS"
},
"data_sources": ["wordstat", "watcher", "ozon_analytics"],
"analyzed_at": "2026-01-21T10:30:00Z",
"processing_time_ms": 45200
}Ошибки:
| Код | ОпиÑание |
|---|---|
| 400 | Ðе указана Ð·Ð°ÐºÑƒÐ¿Ð¾Ñ‡Ð½Ð°Ñ Ñ†ÐµÐ½Ð° или невалидный Ð·Ð°Ð¿Ñ€Ð¾Ñ |
| 401 | Ðе авторизован |
| 403 | ÐедоÑтаточно прав (требуетÑÑ Senior+) |
| 500 | ВнутреннÑÑ Ð¾ÑˆÐ¸Ð±ÐºÐ° |
| 504 | Таймаут анализа |
4.4.3 GET /api/v1/scout/history
Параметры запроÑа:
| Параметр | Тип | ОпиÑание |
|---|---|---|
user_id | string | ID Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ |
limit | int | КоличеÑтво (default: 10, max: 50) |
offset | int | Смещение Ð´Ð»Ñ Ð¿Ð°Ð³Ð¸Ð½Ð°Ñ†Ð¸Ð¸ |
query | string | Фильтр по запроÑу |
verdict | string | Фильтр по вердикту (GO/CONSIDER/RISKY) |
date_from | date | Ðачало периода |
date_to | date | Конец периода |
Ответ:
{
"total": 42,
"limit": 10,
"offset": 0,
"analyses": [
{
"analysis_id": "a1b2c3d4-...",
"query": "летние платьÑ",
"marketplaces": ["wildberries", "ozon"],
"verdict": "CONSIDER",
"metrics": {
"trend_slope": 0.08,
"monopoly_rate": 0.52,
"expected_margin": 18.5
},
"analyzed_at": "2026-01-21T10:30:00Z"
}
]
}4.4.4 POST /api/v1/scout/export
ЗапроÑ:
{
"analysis_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"format": "pdf",
"user_id": "user_123"
}Ответ:
{
"export_id": "exp_123",
"format": "pdf",
"status": "ready",
"download_url": "https://storage.adolf.local/exports/scout_a1b2c3d4_20260121.pdf",
"expires_at": "2026-01-22T10:30:00Z",
"file_size_bytes": 245760
}4.4.5 GET/PUT /api/v1/scout/rates
GET Response:
{
"wildberries": {
"marketplace": "wildberries",
"category": "default",
"commission_pct": 15.0,
"logistics_pct": 5.0,
"return_logistics_pct": 3.0,
"storage_pct": 1.0,
"acquiring_pct": 0.0,
"total_overhead_pct": 24.0,
"updated_at": "2026-01-15T12:00:00Z",
"updated_by": "user_456"
},
"ozon": { ... },
"yandex_market": { ... }
}PUT Request:
{
"marketplace": "wildberries",
"commission_pct": 16.0,
"logistics_pct": 5.5
}4.5 Ð˜Ð½Ñ‚ÐµÑ€Ñ„ÐµÐ¹Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ
4.5.1 Компоненты UI
4.5.2 CSS Ñтили Ð´Ð»Ñ Ñ€ÐµÐ·ÑƒÐ»ÑŒÑ‚Ð°Ñ‚Ð°
/* styles/scout.css */
.scout-verdict {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
border-radius: 8px;
margin-bottom: 16px;
}
.scout-verdict.go {
background: linear-gradient(135deg, #d4edda 0%, #c3e6cb 100%);
border-left: 4px solid #28a745;
}
.scout-verdict.consider {
background: linear-gradient(135deg, #fff3cd 0%, #ffeeba 100%);
border-left: 4px solid #ffc107;
}
.scout-verdict.risky {
background: linear-gradient(135deg, #f8d7da 0%, #f5c6cb 100%);
border-left: 4px solid #dc3545;
}
.scout-verdict-icon {
font-size: 32px;
}
.scout-verdict-text {
font-size: 24px;
font-weight: 600;
}
.scout-metrics-table {
width: 100%;
border-collapse: collapse;
margin: 16px 0;
}
.scout-metrics-table th,
.scout-metrics-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #e0e0e0;
}
.scout-metrics-table .status-green { color: #28a745; }
.scout-metrics-table .status-yellow { color: #ffc107; }
.scout-metrics-table .status-red { color: #dc3545; }
.scout-unit-economics {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
margin: 16px 0;
}
.scout-mp-card {
background: #f8f9fa;
border-radius: 8px;
padding: 16px;
}
.scout-mp-card h4 {
margin: 0 0 12px 0;
color: #333;
}
.scout-mp-card .metric {
display: flex;
justify-content: space-between;
padding: 4px 0;
}
.scout-actions {
display: flex;
gap: 8px;
margin-top: 16px;
flex-wrap: wrap;
}
.scout-actions button {
padding: 8px 16px;
border-radius: 6px;
border: 1px solid #ddd;
background: white;
cursor: pointer;
transition: all 0.2s;
}
.scout-actions button:hover {
background: #f0f0f0;
border-color: #bbb;
}
.scout-recommendations {
background: #e8f4fd;
border-radius: 8px;
padding: 16px;
margin: 16px 0;
}
.scout-risks {
background: #fff5f5;
border-radius: 8px;
padding: 16px;
margin: 16px 0;
}
.scout-opportunities {
background: #f0fff4;
border-radius: 8px;
padding: 16px;
margin: 16px 0;
}4.5.3 Пример Ð¾Ñ‚Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð¸Ñ Ñ€ÐµÐ·ÑƒÐ»ÑŒÑ‚Ð°Ñ‚Ð°
<!-- Пример HTML-Ñтруктуры результата -->
<div class="scout-result">
<!-- Вердикт -->
<div class="scout-verdict consider">
<span class="scout-verdict-icon">🟡</span>
<span class="scout-verdict-text">CONSIDER</span>
</div>
<p class="scout-summary">
<em>Ðиша показывает Ñтабильный ÑпроÑ, но выÑÐ¾ÐºÐ°Ñ ÐºÐ¾Ð½ÐºÑƒÑ€ÐµÐ½Ñ†Ð¸Ñ Ð¸ ÑƒÐ¼ÐµÑ€ÐµÐ½Ð½Ð°Ñ Ð¼Ð°Ñ€Ð¶Ð°
требуют тщательного анализа позиционированиÑ.</em>
</p>
<!-- Метрики -->
<h3>📊 Ключевые метрики</h3>
<table class="scout-metrics-table">
<tr>
<th>Метрика</th>
<th>Значение</th>
<th>СтатуÑ</th>
</tr>
<tr>
<td>Тренд ÑпроÑа</td>
<td>+0.08</td>
<td class="status-yellow">🟡</td>
</tr>
<tr>
<td>МонополизациÑ</td>
<td>52%</td>
<td class="status-yellow">🟡</td>
</tr>
<tr>
<td>Ожид. маржа</td>
<td>18.5%</td>
<td class="status-yellow">🟡</td>
</tr>
</table>
<!-- Unit-Ñкономика -->
<h3>💰 Unit-Ñкономика</h3>
<div class="scout-unit-economics">
<div class="scout-mp-card">
<h4>Wildberries</h4>
<div class="metric"><span>Цена продажи:</span><span>2 450 ₽</span></div>
<div class="metric"><span>СебеÑтоимоÑть:</span><span>500 ₽</span></div>
<div class="metric"><span>РаÑходы МП:</span><span>24.0%</span></div>
<div class="metric"><span>ЧиÑÑ‚Ð°Ñ Ð¿Ñ€Ð¸Ð±Ñ‹Ð»ÑŒ:</span><span>362 ₽</span></div>
<div class="metric"><span>ЧиÑÑ‚Ð°Ñ Ð¼Ð°Ñ€Ð¶Ð°:</span><strong>14.8%</strong></div>
</div>
<div class="scout-mp-card">
<h4>Ozon</h4>
<div class="metric"><span>Цена продажи:</span><span>2 600 ₽</span></div>
<div class="metric"><span>СебеÑтоимоÑть:</span><span>500 ₽</span></div>
<div class="metric"><span>РаÑходы МП:</span><span>29.5%</span></div>
<div class="metric"><span>ЧиÑÑ‚Ð°Ñ Ð¿Ñ€Ð¸Ð±Ñ‹Ð»ÑŒ:</span><span>333 ₽</span></div>
<div class="metric"><span>ЧиÑÑ‚Ð°Ñ Ð¼Ð°Ñ€Ð¶Ð°:</span><strong>12.8%</strong></div>
</div>
</div>
<!-- Рекомендации -->
<div class="scout-recommendations">
<h3>💡 Рекомендации</h3>
<ol>
<li>РаÑÑмотрите нишевые подкатегории Ñ Ð¼ÐµÐ½ÑŒÑˆÐµÐ¹ конкуренцией</li>
<li>Ð”Ð»Ñ Ð´Ð¾ÑÑ‚Ð¸Ð¶ÐµÐ½Ð¸Ñ Ð¼Ð°Ñ€Ð¶Ð¸ 25% необходима Ð·Ð°ÐºÑƒÐ¿Ð¾Ñ‡Ð½Ð°Ñ Ñ†ÐµÐ½Ð° до 350₽</li>
<li>СфокуÑируйтеÑÑŒ на WB — overhead ниже на 5.5%</li>
</ol>
</div>
<!-- РиÑки -->
<div class="scout-risks">
<h3>âš ï¸ Ð Ð¸Ñки</h3>
<ul>
<li>
<strong>Ð’Ñ‹ÑÐ¾ÐºÐ°Ñ ÐºÐ¾Ð½ÐºÑƒÑ€ÐµÐ½Ñ†Ð¸Ñ Ð¾Ñ‚ крупных брендов</strong> (high)
<br><em>МитигациÑ: Уникальное позиционирование и нишевой дизайн</em>
</li>
<li>
<strong>СезонноÑть ÑпроÑа</strong> (medium)
<br><em>МитигациÑ: Планирование закупок Ñ ÑƒÑ‡Ñ‘Ñ‚Ð¾Ð¼ пика в апреле-июне</em>
</li>
</ul>
</div>
<!-- Кнопки -->
<div class="scout-actions">
<button onclick="exportPDF()">📥 ÐкÑпорт PDF</button>
<button onclick="exportExcel()">📊 ÐкÑпорт Excel</button>
<button onclick="viewHistory()">📋 ИÑториÑ</button>
</div>
<hr>
<small>ID анализа: a1b2c3d4... | 2026-01-21</small>
</div>4.6 Сценарии взаимодейÑтвиÑ
4.6.1 Базовый анализ ниши
Пользователь: Проанализируй нишу летних платьев на Wildberries, закупка 500 рублей
СиÑтема: 🔠**Ðнализирую нишу...**
[прогреÑÑ-индикатор]
СиÑтема: ## 🟡 Вердикт: **CONSIDER**
_Ðиша показывает Ñтабильный ÑпроÑ, но выÑÐ¾ÐºÐ°Ñ ÐºÐ¾Ð½ÐºÑƒÑ€ÐµÐ½Ñ†Ð¸Ñ..._
### 📊 Ключевые метрики
| Метрика | Значение | Ð¡Ñ‚Ð°Ñ‚ÑƒÑ |
|---------|----------|--------|
| Тренд ÑпроÑа | +0.08 | 🟡 |
| ÐœÐ¾Ð½Ð¾Ð¿Ð¾Ð»Ð¸Ð·Ð°Ñ†Ð¸Ñ | 52% | 🟡 |
| Ожид. маржа | 18.5% | 🟡 |
...4.6.2 Ðнализ Ñ Ð´Ð¸Ð°Ð¿Ð°Ð·Ð¾Ð½Ð¾Ð¼ COGS
Пользователь: Оцени нишу детÑких комбинезонов на Ozon, закупка от 800 до 1200 рублей
СиÑтема: 🔠**Ðнализирую нишу...**
СиÑтема: ## 🟢 Вердикт: **GO**
_При оптимальной закупочной цене 800₽ ниша показывает хорошие показатели..._
### 💰 Unit-Ñкономика (диапазон COGS)
| COGS | Маржа | Прибыль |
|------|-------|---------|
| 800 ₽ (min) | 28.5% | 855 ₽ |
| 1000 ₽ (avg) | 22.1% | 663 ₽ |
| 1200 ₽ (max) | 15.7% | 471 ₽ |
**РекомендациÑ:** Держите COGS ниже 1000₽ Ð´Ð»Ñ Ð·ÐµÐ»Ñ‘Ð½Ð¾Ð¹ маржи.4.6.3 ПроÑмотр иÑтории
Пользователь: Покажи поÑледние 5 анализов
СиÑтема: ## 📋 ИÑÑ‚Ð¾Ñ€Ð¸Ñ Ð°Ð½Ð°Ð»Ð¸Ð·Ð¾Ð²
| Дата | Ð—Ð°Ð¿Ñ€Ð¾Ñ | Вердикт | Маржа |
|------|--------|---------|-------|
| 2026-01-21 | летние Ð¿Ð»Ð°Ñ‚ÑŒÑ | 🟡 CONSIDER | 18.5% |
| 2026-01-20 | детÑкие комбинезоны | 🟢 GO | 28.5% |
| 2026-01-18 | женÑкие Ñумки | 🔴 RISKY | 12.1% |
| 2026-01-15 | мужÑкие футболки | 🟢 GO | 31.2% |
| 2026-01-12 | кроÑÑовки Ñпортивные | 🟡 CONSIDER | 19.8% |
_Ð”Ð»Ñ Ð´ÐµÑ‚Ð°Ð»ÑŒÐ½Ð¾Ð³Ð¾ проÑмотра укажите ID анализа._4.6.4 Изменение Ñтавок
Пользователь: Покажи текущие Ñтавки
СиÑтема: ## 📊 Текущие Ñтавки маркетплейÑов
| Ð¡Ñ‚Ð°Ñ‚ÑŒÑ | Wildberries | Ozon | ЯндекÑ.Маркет |
|--------|:-----------:|:----:|:-------------:|
| КомиÑÑÐ¸Ñ | 15% | 18% | 15% |
| ЛогиÑтика | 5% | 6% | 7% |
| Возвраты | 3% | 4% | 4% |
| Хранение | 1% | 1.5% | 1% |
| Ðквайринг | 0% | 0% | 1.5% |
| **Итого** | **24%** | **29.5%** | **28.5%** |
_Ставки можно изменить (Senior+): «Измени Ñтавки»_
Пользователь: УÑтанови комиÑÑию WB 16%
СиÑтема: ✅ Ставки Ð´Ð»Ñ wildberries обновлены.
Ðовые значениÑ:
- КомиÑÑиÑ: 16% (было 15%)
- Итого overhead: 25%4.7 Обработка ошибок
4.7.1 Типичные ошибки
| Ð¡Ð¸Ñ‚ÑƒÐ°Ñ†Ð¸Ñ | Сообщение пользователю |
|---|---|
| Ðе указан COGS | Â«âš ï¸ ÐŸÐ¾Ð¶Ð°Ð»ÑƒÐ¹Ñта, укажите закупочную цену. Пример: ...» |
| Ðет доÑтупа | «⛔ ДоÑтуп к модулю Scout ограничен. ТребуетÑÑ Ñ€Ð¾Ð»ÑŒ Senior или выше.» |
| Таймаут | «â±ï¸ Превышено Ð²Ñ€ÐµÐ¼Ñ Ð¾Ð¶Ð¸Ð´Ð°Ð½Ð¸Ñ. Попробуйте повторить запроÑ.» |
| ИÑточник недоÑтупен | Â«âš ï¸ Ð§Ð°Ñть данных временно недоÑтупна. Результат может быть неполным.» |
| Ðевалидный URL | Â«âš ï¸ Ðе удалоÑÑŒ раÑпознать URL. ПоддерживаютÑÑ: WB, Ozon, ЯндекÑ.Маркет» |
4.7.2 Graceful Degradation в UI
async def _handle_analyze_with_fallback(self, ...):
"""Ðнализ Ñ Ð¾Ð±Ñ€Ð°Ð±Ð¾Ñ‚ÐºÐ¾Ð¹ чаÑтичных ошибок."""
try:
result = await self._request_analyze(...)
# Проверка чаÑтичных ошибок
if result.get("sources_failed"):
yield f"âš ï¸ _Ðекоторые иÑточники недоÑтупны: {', '.join(result['sources_failed'])}_\n\n"
yield self._format_verdict_result(result)
except PartialDataError as e:
yield f"âš ï¸ Ðнализ выполнен Ñ Ð¾Ð³Ñ€Ð°Ð½Ð¸Ñ‡ÐµÐ½Ð¸Ñми:\n{e.message}\n\n"
yield self._format_verdict_result(e.partial_result)
except CriticalError as e:
yield f"⌠{e.user_message}"
yield "\n\nПопробуйте:\n"
yield "- Повторить Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð¿Ð¾Ð·Ð¶Ðµ\n"
yield "- УпроÑтить запроÑ\n"
yield "- ОбратитьÑÑ Ðº админиÑтратору"Документ подготовлен: Январь 2026
ВерÑиÑ: 1.0
СтатуÑ: Черновик