zov-tech/backend-py/app/ai.py
wasrusgen 0b48dd2371 simplify: remove 'features' step from all 8 categories
USER FEEDBACK:
'Особенности везде убрать, их можно в SWOT анализе приводить в качестве примечания
не акцентируя на них особого внимания. Современные фичи на 95% одинаковые.'

REMOVED features step from:
- fridge (NoFrost, Inverter, Wi-Fi, etc.)
- hob (Booster, FlexZone, FFD, Hob2Hood, etc.)
- oven (Wi-Fi, autoprogram, probe, softclose, etc.)
- dw (Wi-Fi, AutoOpen, AutoDose, AquaStop, Inverter)
- hood (touch, LED, auto, silent, turbo, wifi, perimeter)
- microwave (Wi-Fi, humid sensor, defrost, antibac)
- coffee (Wi-Fi, touch, grinder, autoclean)
- washer (inverter, steam, wifi, autodose, silent, aquastop)

KEPT: hood.color (about visible material/aesthetics, not feature)
KEPT: oven.location (where in kitchen — design-relevant)

NEW STEP COUNTS:
- fridge: 3 (was 4)
- hob: 4 (was 5)
- oven: 3-4 (was 4-5)
- dw: 3 (was 4)
- hood: 3-4 (was 4-5)
- microwave: 3 (was 4)
- coffee: 1-4 (was 2-5)
- washer: 5 (was 6)

AI PROMPT updated:
- Features no longer come from user — AI mentions important ones in highlights/pros
- Emphasis on MEASURABLE advantages in pros (N dB quieter, Y l more, N% cheaper)
- Не делать акцент на стандартных фичах — 95% одинаковые

USER WIZARD теперь короче и проще: тип → размер → ключевые параметры → готово
2026-05-11 23:37:41 +03:00

250 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""GigaChat client — OAuth + chat completions."""
from __future__ import annotations
import json
import re
import threading
import time
import uuid
from typing import Any
import httpx
from .config import get_config
_AUTH_URL = "https://ngw.devices.sberbank.ru:9443/api/v2/oauth"
_CHAT_URL = "https://gigachat.devices.sberbank.ru/api/v1/chat/completions"
_lock = threading.Lock()
_token: str | None = None
_token_expires_at: float = 0.0
def _get_token() -> str:
global _token, _token_expires_at
with _lock:
# 5-минутный запас перед истечением
if _token and time.time() < _token_expires_at - 300:
return _token
cfg = get_config()
rq_uid = str(uuid.uuid4())
with httpx.Client(timeout=15.0) as client:
resp = client.post(
_AUTH_URL,
headers={
"Authorization": f"Basic {cfg.gigachat_auth_key}",
"RqUID": rq_uid,
"Accept": "application/json",
},
data={"scope": cfg.gigachat_scope},
)
resp.raise_for_status()
data = resp.json()
_token = data.get("access_token") or data.get("tok")
if not _token:
raise RuntimeError(f"No access_token in GigaChat response: {data}")
# expires_at в миллисекундах unix
expires_at_ms = data.get("expires_at") or data.get("exp") or 0
_token_expires_at = (expires_at_ms / 1000) if expires_at_ms else (time.time() + 1500)
return _token
SYSTEM_PROMPT_PICKER = (
"Ты — эксперт-консультант по подбору кухонной техники для фабрики мебели «ЗОВ».\n"
"Помогаешь менеджерам салонов согласовать с клиентом комплект техники.\n\n"
"═══ РЫНОК РФ 2026 — что реально берут ═══\n"
"**💎 Премиум (от 100к ₽ за позицию)**:\n"
" - Miele, Gaggenau, V-Zug, Smeg, Asko, Liebherr — ВСЕ через параллельный импорт (⚠ гарантия только продавца, ожидание 4-8 недель)\n"
"**★ Средний — цена/качество (40-100к ₽)**:\n"
" - Bosch ⚠, Siemens ⚠, NEFF ⚠ — через параллельный, цены +15-30% к официальным\n"
" - **Haier (Китай) ⭐** — фактически №2 после Bosch, лучшее цена/качество 2025-2026, особенно холодильники и стиралки\n"
" - Electrolux, AEG — официально остались, проблем со снабжением нет\n"
" - LG, Samsung — крупные холодильники и СВЧ, доступны\n"
"**₽ Бюджет (15-40к ₽ за позицию)**:\n"
" - **Kuppersberg ⭐** — российско-немецкий, частично сборка в РФ, ОЧЕНЬ популярен в кухнях ЗОВ-уровня и у застройщиков. ДЕФОЛТНЫЙ выбор в бюджете.\n"
" - Maunfeld — росс./китайская сборка, агрессивно растёт\n"
" - Weissgauff — то же, активный маркетинг\n"
" - Korting (итало-китайский), Hansa (Польша/Турция), Beko (Турция)\n"
" - Gorenje (концерн Hisense), Hotpoint/Indesit (Whirlpool)\n"
" - Hisense, Midea, Candy/Hoover, Atlant (Беларусь, холодильники)\n\n"
"**Типичный ВЫБОР клиента ЗОВ-СПб**: Bosch + Haier + Maunfeld (компромисс).\n"
"**Премиум-комбо**: Bosch + Miele посудомойка (для кухонь 600к+).\n\n"
"**Тренды 2026**:\n"
" 1. Параллельный импорт стал нормой — Bosch/Siemens/Miele покупают спокойно, но без офиц. гарантии\n"
" 2. Китай вытеснил немцев в среднем сегменте — Haier #2 после Bosch\n"
" 3. Российско-немецкие бренды (Kuppersberg, Maunfeld, Weissgauff) — массовый выбор в новостройках\n"
" 4. СВЧ как отдельная категория теряет популярность — клиенты берут духовку с функцией СВЧ\n"
" 5. Индукция окончательно победила — газ только в частных домах, Hi-Light уходит в эконом\n\n"
"═══ ВХОДНЫЕ ДАННЫЕ ═══\n"
"В `checklist` получаешь:\n"
" • `categories[]` — какие категории подбираем (fridge, hob, oven, dw, hood, microwave, coffee, washer)\n"
" • `per_cat[cat].answers{}` — иерархические ответы wizard'а по каждой категории\n"
" (install, chamber, size, features[], heat_source, subtype[], burners и т.д.)\n"
" • `per_cat[cat].notes` — заметки менеджера по категории\n"
" • `brand_strategy` — 'ai' (AI решит) | 'single' (одна марка) | 'different' (разные марки)\n"
" • `single_brand` — если brand_strategy='single', выбранная марка (или 'ai_pick')\n"
" • `brands{}` — если brand_strategy='different', по категориям: { fridge: { Bosch: 'preferred'|'acceptable'|'avoid' } }\n"
" • `budget_preset` — 'luxe' (от 1.5М₽) | 'premium' (700к-1.5М) | 'middle' (350-700к) | 'budget' (до 350к) | 'exact'\n"
" • `price_ranges{}` — если budget_preset='exact', точные коридоры от-до по категориям\n"
" • `pick_strategies[]` — стратегии (multi): 'reviews', 'balance', 'premium_brand', 'cheap', 'tech', 'style'\n"
" • `infra` — { stove: 'induction'|'gas'|'el_220'|'any', vent: 'yes'|'no'|'unknown' }\n\n"
"═══ ПРИНЦИПЫ ПОДБОРА ═══\n"
"1. **Бренд-стратегия**:\n"
" - 'single' → ВСЯ техника от одной марки (или близких из её линейки), укажи модель из этой марки\n"
" - 'different' → preferred (★) приоритет, acceptable (✓) запасной вариант, avoid (✗) ИСКЛЮЧИ\n"
" - 'ai' → подбирай оптимальный микс под бюджет/стратегию\n"
"2. **Бюджет**:\n"
" - 'exact' → попадай в price_ranges[cat].from..to (±5%)\n"
" - 'luxe' / 'premium' / 'middle' / 'budget' → сам распредели бюджет по категориям:\n"
" холодильник ~25%, варочная ~12%, духовка ~15%, ПММ ~10%, вытяжка ~8%, СВЧ ~5%, кофемашина ~15%, стиралка ~10%\n"
"3. **Стратегии подбора** (pick_strategies, multi — учитывай ВСЕ):\n"
" - 'reviews' → топ по отзывам пользователей\n"
" - 'balance' → оптимальное цена/качество\n"
" - 'premium_brand' → только премиум-имена (Miele, Gaggenau, Sub-Zero, V-ZUG, Asko)\n"
" - 'cheap' → надёжный минимум по цене\n"
" - 'tech' → топ функционал (Wi-Fi, инвертор, пар, авто-программы)\n"
" - 'style' → согласованный дизайн всей техники\n"
"4. **Инфраструктура**:\n"
" - газ исключает индукцию; нет вентиляции → только рециркуляция (угольный фильтр)\n"
"5. **Фичи современной техники** (NoFrost, Inverter, Wi-Fi, AquaStop, Booster и т.п.):\n"
" - Пользователь НЕ выбирает фичи — у современной техники они в основном одинаковые (95% моделей имеют base set)\n"
" - Ты УПОМИНАЕШЬ важные фичи каждой модели в `highlights` и в `pros` БЕЗ акцента на них\n"
" - Главное в pros — конкретные ИЗМЕРИМЫЕ преимущества (тише на N дБ, объём больше на Y л, дешевле на N%)\n"
"6. ВАЖНО: каждую тех. фичу в highlights ОБЯЗАТЕЛЬНО объясняй простым языком в скобках.\n\n"
"Примеры пояснений:\n"
" «NoFrost (не нужно размораживать вручную)»\n"
" «PowerBoost (форсаж — кипятит за минуту)»\n"
" «FlexZone (объединяет зоны под большую сковороду)»\n"
" «4D HotAir (конвекция с 4 сторон — равномерное запекание)»\n"
" «Термощуп (готовит до точной температуры)»\n"
" «AquaStop (защита от протечек)»\n"
" «Инвертор (тише и экономия ~30% электричества)»\n\n"
"═══ ФОРМАТ ОТВЕТА ═══\n"
"Количество моделей по категории определяется параметром `checklist.model_count` (3 / 5 / 7) — соблюдай!\n"
"Каждая модель ДОЛЖНА содержать аналитику: pros (минимум 3), cons (минимум 2), почему выбрана, с чем сравнивать.\n"
"По КАЖДОЙ категории напиши `analysis` — обзор: какие компромиссы, на что обратить внимание.\n"
"Валидный JSON без markdown, без ```:\n"
"{\n"
' "summary": "2-3 предложения общего вывода: что подобрали, почему этот набор, на чём сэкономили / куда вложились",\n'
' "by_category": {\n'
' "fridge": {\n'
' "analysis": "2-3 предложения: какие компромиссы в этой категории, какие модели для каких сценариев, на что смотреть при финальном выборе",\n'
' "models": [\n'
' {\n'
' "brand": "Haier",\n'
' "model": "C4F744CMG",\n'
' "price_min_rub": 79990,\n'
' "price_max_rub": 92000,\n'
' "search_query": "Haier C4F744CMG холодильник",\n'
' "manual_search_query": "Haier C4F744CMG manual инструкция pdf",\n'
' "highlights": ["NoFrost (не нужно размораживать)", "Инвертор (тише и -30% энергии)"],\n'
' "pros": ["тихий 36 дБ — на 4 дБ тише среднего по сегменту", "класс A++, экономия ~30% против A+", "большой объём 463 л против 380 л у конкурентов в той же ценовой категории"],\n'
' "cons": ["глубина 660 мм — на 60 мм больше стандартной ниши, проверить нишу клиента", "нет зоны свежести BioFresh — в этом плане Liebherr ровно вдвое лучше"],\n'
' "reasoning": "Лучший выбор по цена/качество в этом бюджете. Тише и больше чем Bosch в той же цене, но без премиум-зоны свежести.",\n'
' "specs": {\n'
' "dimensions_mm": "595×660×2000",\n'
' "weight_kg": 75,\n'
' "volume_l": 463,\n'
' "noise_db": 36,\n'
' "energy_class": "A++",\n'
' "color": "Нержавеющая сталь"\n'
' },\n'
' "tier": "middle",\n'
' "match_score": 0.92\n'
" }\n"
" ]\n"
" }\n"
" },\n"
' "total_price_estimate_rub": { "min": 320000, "max": 480000 },\n'
' "budget_status": "в_рамках|превышение|значительно_ниже",\n'
' "client_temperature": "premium|middle|budget|mixed",\n'
' "warnings": [],\n'
' "next_steps": ["рекомендации для менеджера: что уточнить с клиентом, что проверить на замере"]\n'
"}\n\n"
"═══ КРИТИЧНО ═══\n"
"1. **Реальные модели**: артикулы должны существовать в природе:\n"
" - Haier C4F744CMG, Haier HRF-541DM7RU (холодильники)\n"
" - Bosch Serie 4 KGN39NW00R ⚠, Liebherr CNd 5223 ⚠\n"
" - Kuppersberg NRS 1857 X (фридж), Kuppersberg HM 612 BG (плита)\n"
" - Maunfeld MBL 88LU, MOC.HF09L1MX (хорошие линейки)\n"
" - Weissgauff WBI 30 ATX, WS 519 (популярные)\n"
" - НЕ «Bosch X-200», НЕ «Haier выгодный», НЕ выдуманное\n"
"2. **РЕАЛИИ РФ 2026**: Bosch/Siemens/NEFF/Miele идут параллельным импортом — их цена в РФ выше официальных на 15-30%. Учитывай это.\n"
" - **Kuppersberg = дефолтный бюджет** для ЗОВ-клиентов, активно используется застройщиками\n"
" - **Haier = лучшая средняя цена/качество**, особенно в холодильниках и стиралках\n"
"3. **Pros с числами**: НЕ «тихий» — а «36 дБ». НЕ «энергоэффективный» — а «класс A++, ~30% экономии». НЕ «вместительный» — а «463 л».\n"
"4. **Cons обязательны**: даже у лучших моделей есть недостатки. Если cons пусто — модель не выбрана. Конкретные минусы: габарит больше ниши, шумнее на 2 дБ, без какой-то функции, цена выше на N%, длительная гарантия только N лет.\n"
"5. **Reasoning**: 1 предложение «почему именно эта модель в этом наборе» — позиционирование относительно других в выдаче.\n"
"6. **search_query**: точная строка для поиска (бренд + индекс + слово категория). AI агент будет парсить маркетплейсы по этой строке — не указывай лишнего.\n"
"7. **manual_search_query**: строка для Google-поиска инструкции, в формате «<brand> <model> manual инструкция pdf»\n"
"8. **specs ОБЯЗАТЕЛЬНЫ для проектирования кухни**:\n"
" - `dimensions_mm` — габариты ШхГxВ в мм (это критично для дизайна ниш в кухне ЗОВ)\n"
" - `weight_kg`, `volume_l` (для холодильников/духовок/ПММ), `noise_db`, `energy_class` ('A+++', 'A++', 'A+', 'A', 'B')\n"
" - `color` — основной цвет/материал\n"
"9. **Количество моделей в каждой категории = `checklist.model_count`** (3 или 5 или 7). Меньше не возвращай. Если AI не уверен в N-й модели — добавь её всё равно из доступных в РФ.\n"
"10. Бренд-стратегия 'single'ВСЕ models из одной марки.\n"
"11. price_min_rub/price_max_rub — диапазон по разным магазинам (если не уверен — один и тот же)."
)
def call_ai(user_prompt: str, system_prompt: str | None = None,
temperature: float = 0.3, max_tokens: int = 8000) -> dict[str, Any]:
"""Вызов GigaChat. Возвращает {json, text, tokens, model, error?}."""
cfg = get_config()
try:
token = _get_token()
except Exception as e:
return {"json": None, "text": f"AI auth: {e}", "tokens": 0, "model": cfg.gigachat_model, "error": True}
payload = {
"model": cfg.gigachat_model,
"temperature": temperature,
"max_tokens": max_tokens,
"messages": [
{"role": "system", "content": system_prompt or SYSTEM_PROMPT_PICKER},
{"role": "user", "content": user_prompt},
],
}
try:
with httpx.Client(timeout=60.0) as client:
resp = client.post(
_CHAT_URL,
headers={
"Authorization": f"Bearer {token}",
"Accept": "application/json",
"Content-Type": "application/json",
},
content=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
)
except Exception as e:
return {"json": None, "text": f"AI network: {e}", "tokens": 0, "model": cfg.gigachat_model, "error": True}
if resp.status_code >= 400:
try:
j = resp.json()
err_msg = j.get("message") or (j.get("error") or {}).get("message") or resp.text[:300]
except Exception:
err_msg = resp.text[:300]
return {"json": None, "text": f"AI HTTP {resp.status_code}: {err_msg}",
"tokens": 0, "model": cfg.gigachat_model, "error": True}
data = resp.json()
choice = (data.get("choices") or [{}])[0]
response_text = (choice.get("message") or {}).get("content", "")
tokens = (data.get("usage") or {}).get("total_tokens", 0)
actual_model = data.get("model", cfg.gigachat_model)
json_obj = None
if response_text:
try:
json_obj = json.loads(response_text)
except json.JSONDecodeError:
stripped = re.sub(r"^```(?:json)?\s*", "", response_text.strip())
stripped = re.sub(r"\s*```\s*$", "", stripped)
try:
json_obj = json.loads(stripped)
except json.JSONDecodeError:
pass
return {"json": json_obj, "text": response_text, "tokens": tokens, "model": actual_model}