feat(mimari): ANLAMA LLM'DE, HESAP KODDA — yapilandirilmis stok sorgusu
Browse filesKullanicinin tezi dogrulandi: her Turkce cekim icin kural yazmak surdurulemez.
Stopword listesi 100 kelimeyi asti; 'elektrikli' kurali varken 'Elektrik
Bisiklet' bir harf farkiyla deldi, bot Verve 3'u yine elektrikli diye satti.
YENI AKIS:
musteri mesaji -> intent analyzer (gpt-4o-mini, ZATEN her mesajda calisiyor)
-> yapilandirilmis sorgu {urun, kategori, beden, renk, magaza, fiyat_max,
siralama, adet_sorusu, malzeme, onceki_urun}
-> lookup_stock_structured(): KOD deterministik calistirir (filtre/sayma/
siralama — olcum: sayma sorusunda Luna=19, Terra=19, kod=25 DOGRU)
-> ayni hukum/seal formatiyla doner.
- 'sarjli/akulu/pilli/elektrik' icin kural GEREKMEZ — model normalize eder.
- 'urun' alani SADECE mevcut mesajtan (onceki sohbetten Madone tasima
zehirlenmesi bitti); takip sorusu 'onceki_urun: true' ile ayri.
- Eski metin ayristirici FALLBACK olarak durur -> regresyon 65/65 yesil.
- Ek maliyet ~SIFIR: analyzer zaten calisiyordu, sema genisledi.
- /debug-intent endpoint'i: soru -> LLM anlama -> kod hesap zinciri testi.
Ilk denemede yakalanan bug da duzeltildi: siralama stok kontrolunden ONCE
ilk 5'i kesiyordu, en ucuz katalog kayitlari stoksuz cikinca bos donuyordu.
- app.py +54 -7
- intent_analyzer.py +40 -2
- warehouse_lookup.py +226 -0
|
@@ -1523,7 +1523,30 @@ def process_whatsapp_message_with_memory(user_message, phone_number):
|
|
| 1523 |
|
| 1524 |
# Stok bilgisi ekle - detected_product kullan
|
| 1525 |
# ONCELIK: XML sonucu (smart_warehouse_with_price.py) daha guvenilir
|
| 1526 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1527 |
# TAKIP SORUSU DESTEGI (11 Agu): "medium ne var" gibi urunsuz sorgular
|
| 1528 |
# tek basina eslesmez; sohbet baglamindaki son urunle birlestirip dene.
|
| 1529 |
# ("small elektrikli bisiklet" -> "medium ne var" => "medium elektrikli bisiklet")
|
|
@@ -1621,12 +1644,13 @@ def process_whatsapp_message_with_memory(user_message, phone_number):
|
|
| 1621 |
# arama adinda 'karbon' gecen AKSESUARLARI buluyor ve kompakt stok
|
| 1622 |
# listesi hic gonderilmiyordu -> bot 'stokta karbon bisiklet yok'
|
| 1623 |
# dedi (11 Agu). Malzeme sorusunda o yolu ATLA, dogrudan listeye git.
|
| 1624 |
-
_is_material_q =
|
| 1625 |
-
|
| 1626 |
-
|
| 1627 |
-
|
| 1628 |
-
|
| 1629 |
-
|
|
|
|
| 1630 |
|
| 1631 |
if should_query_stock and stock_query_product and not xml_has_valid_stock:
|
| 1632 |
realtime_stock = None if _is_material_q else get_realtime_stock_parallel(stock_query_product)
|
|
@@ -2085,6 +2109,29 @@ async def debug_flow(q: str):
|
|
| 2085 |
})
|
| 2086 |
|
| 2087 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2088 |
@app.get("/model-compare")
|
| 2089 |
async def model_compare(q: str, models: str = "gpt-5.6-luna,gpt-5.6-terra"):
|
| 2090 |
"""LUNA vs TERRA — ayni soru, ayni baglam, farkli model.
|
|
|
|
| 1523 |
|
| 1524 |
# Stok bilgisi ekle - detected_product kullan
|
| 1525 |
# ONCELIK: XML sonucu (smart_warehouse_with_price.py) daha guvenilir
|
| 1526 |
+
# YAPILANDIRILMIS SORGU — ANLAMA LLM'DE, HESAP KODDA (11 Agu gece).
|
| 1527 |
+
# Intent analyzer serbest Turkce'yi yapiya cevirir ({kategori:
|
| 1528 |
+
# elektrikli, siralama: ucuz...}), kod deterministik calistirir.
|
| 1529 |
+
# 'Elektrik/sarjli/akulu' icin kural GEREKMEZ — model Turkce'yi bilir.
|
| 1530 |
+
# Eski metin ayristirici FALLBACK olarak durur.
|
| 1531 |
+
warehouse_info = None
|
| 1532 |
+
_struct = intent_analysis.get("stok_sorgusu") if isinstance(intent_analysis, dict) else None
|
| 1533 |
+
if isinstance(_struct, dict) and any(v for v in _struct.values()):
|
| 1534 |
+
if _struct.get("onceki_urun") and not _struct.get("urun"):
|
| 1535 |
+
_prev_p = get_conversation_context(phone_number).get("current_product")
|
| 1536 |
+
if _prev_p:
|
| 1537 |
+
_struct["urun"] = _prev_p
|
| 1538 |
+
try:
|
| 1539 |
+
from warehouse_lookup import lookup_stock_structured
|
| 1540 |
+
warehouse_info = lookup_stock_structured(_struct)
|
| 1541 |
+
if warehouse_info:
|
| 1542 |
+
logger.info(f"🎯 [wh_struct] HIT: {_struct}")
|
| 1543 |
+
else:
|
| 1544 |
+
logger.info(f"[wh_struct] None, metin yoluna dusuluyor: {_struct}")
|
| 1545 |
+
except Exception as e:
|
| 1546 |
+
logger.warning(f"[wh_struct] hata, metin yolu devrede: {e}")
|
| 1547 |
+
|
| 1548 |
+
if not warehouse_info:
|
| 1549 |
+
warehouse_info = get_warehouse_stock(stock_query_product)
|
| 1550 |
# TAKIP SORUSU DESTEGI (11 Agu): "medium ne var" gibi urunsuz sorgular
|
| 1551 |
# tek basina eslesmez; sohbet baglamindaki son urunle birlestirip dene.
|
| 1552 |
# ("small elektrikli bisiklet" -> "medium ne var" => "medium elektrikli bisiklet")
|
|
|
|
| 1644 |
# arama adinda 'karbon' gecen AKSESUARLARI buluyor ve kompakt stok
|
| 1645 |
# listesi hic gonderilmiyordu -> bot 'stokta karbon bisiklet yok'
|
| 1646 |
# dedi (11 Agu). Malzeme sorusunda o yolu ATLA, dogrudan listeye git.
|
| 1647 |
+
_is_material_q = bool(isinstance(_struct, dict) and _struct.get("malzeme"))
|
| 1648 |
+
if not _is_material_q:
|
| 1649 |
+
try:
|
| 1650 |
+
from warehouse_lookup import _detect_material
|
| 1651 |
+
_is_material_q = _detect_material(user_message) is not None
|
| 1652 |
+
except Exception:
|
| 1653 |
+
pass
|
| 1654 |
|
| 1655 |
if should_query_stock and stock_query_product and not xml_has_valid_stock:
|
| 1656 |
realtime_stock = None if _is_material_q else get_realtime_stock_parallel(stock_query_product)
|
|
|
|
| 2109 |
})
|
| 2110 |
|
| 2111 |
|
| 2112 |
+
@app.get("/debug-intent")
|
| 2113 |
+
async def debug_intent(q: str):
|
| 2114 |
+
"""UCTAN UCA yapilandirilmis sorgu testi:
|
| 2115 |
+
soru -> intent analyzer (LLM anlama) -> lookup_stock_structured (kod hesap).
|
| 2116 |
+
'Elektrik Bisiklet' gibi kural-disi ifadelerin cozuldugunu dogrulamak icin."""
|
| 2117 |
+
import asyncio as _a
|
| 2118 |
+
from intent_analyzer import analyze_customer_intent
|
| 2119 |
+
from warehouse_lookup import lookup_stock_structured
|
| 2120 |
+
|
| 2121 |
+
analysis = await _a.to_thread(analyze_customer_intent, q, None)
|
| 2122 |
+
struct = analysis.get("stok_sorgusu") if isinstance(analysis, dict) else None
|
| 2123 |
+
result = None
|
| 2124 |
+
if isinstance(struct, dict) and any(v for v in struct.values()):
|
| 2125 |
+
result = await _a.to_thread(lookup_stock_structured, struct)
|
| 2126 |
+
return JSONResponse({
|
| 2127 |
+
"soru": q,
|
| 2128 |
+
"1_llm_anlama": struct,
|
| 2129 |
+
"2_kod_hesap": {"hit": result is not None,
|
| 2130 |
+
"hukum": (result[0][:160] if result else None),
|
| 2131 |
+
"satirlar": (result[1:12] if result else None)},
|
| 2132 |
+
})
|
| 2133 |
+
|
| 2134 |
+
|
| 2135 |
@app.get("/model-compare")
|
| 2136 |
async def model_compare(q: str, models: str = "gpt-5.6-luna,gpt-5.6-terra"):
|
| 2137 |
"""LUNA vs TERRA — ayni soru, ayni baglam, farkli model.
|
|
@@ -88,8 +88,46 @@ JSON FORMATI:
|
|
| 88 |
"customer_mood": "positive",
|
| 89 |
"suggested_action": "notify_store", // notify_store, answer_directly, both, none
|
| 90 |
"notification_required": true,
|
| 91 |
-
"notification_reason": "Müşteri FX 2'yi ayırtmak istiyor"
|
| 92 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
|
| 94 |
# Bağlam varsa ekle
|
| 95 |
context_info = ""
|
|
|
|
| 88 |
"customer_mood": "positive",
|
| 89 |
"suggested_action": "notify_store", // notify_store, answer_directly, both, none
|
| 90 |
"notification_required": true,
|
| 91 |
+
"notification_reason": "Müşteri FX 2'yi ayırtmak istiyor",
|
| 92 |
+
"stok_sorgusu": { ... } // Aşağıya bak — stok/fiyat sorusuysa DOLDUR, değilse null
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
STOK_SORGUSU (EN ÖNEMLİ ALAN — stok/fiyat/model sorularında doldur):
|
| 96 |
+
Müşterinin serbest cümlesini şu YAPIYA çevir. Sistem bu yapıyı deterministik
|
| 97 |
+
çalıştırır; senin görevin Türkçe'yi ANLAYIP normalize etmek:
|
| 98 |
+
{
|
| 99 |
+
"urun": "marlin 4", // SADECE BU MESAJTA geçen model adı. Önceki
|
| 100 |
+
// konuşmadaki ürünü BURAYA YAZMA! Kategori
|
| 101 |
+
// sorusunda ("elektrikli bisikletler") null bırak.
|
| 102 |
+
"onceki_urun": false, // "bunun başka rengi", "medium'u var mı" gibi
|
| 103 |
+
// önceki ürüne atıf varsa true
|
| 104 |
+
"kategori": null, // elektrikli|sehir|dag|yol|gravel|cocuk|giyim|kask|lastik|pompa
|
| 105 |
+
// NORMALIZE ET: "elektrik/şarjlı/akülü/pilli/e-bike" → elektrikli
|
| 106 |
+
// "MTB/dağ/arazi" → dag, "hibrit/city" → sehir
|
| 107 |
+
"beden": null, // XXS|XS|S|M|ML|L|XL|XXL veya 47-62 kadro ölçüsü
|
| 108 |
+
// "small→S, medium→M, large→L, küçük→S, orta→M"
|
| 109 |
+
"renk": null, // "su yeşili", "mat siyah", "kırmızı"...
|
| 110 |
+
"magaza": null, // caddebostan|alsancak|bahcekoy
|
| 111 |
+
// "Kadıköy→caddebostan, İzmir→alsancak, Sarıyer→bahcekoy"
|
| 112 |
+
"fiyat_max": null, // "100 bin altında/bütçem 150 bin" → 100000/150000
|
| 113 |
+
"fiyat_min": null, // "300 bin üstü" → 300000
|
| 114 |
+
"siralama": null, // "en ucuz/uygun/ekonomik" → ucuz, "en pahalı/iyi" → pahali
|
| 115 |
+
"adet_sorusu": false, // "kaç tane/kaç adet var" → true
|
| 116 |
+
"malzeme": null // "karbon" | "aluminyum" (kadro malzemesi sorusuysa)
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
STOK_SORGUSU ÖRNEKLERİ:
|
| 120 |
+
- "Elektrik Bisiklet modellerini soruyorum" → {"kategori": "elektrikli"}
|
| 121 |
+
- "şarjlı bisiklet var mı" → {"kategori": "elektrikli"}
|
| 122 |
+
- "En ucuz elektrikli bisiklet modeli hangisi" → {"kategori": "elektrikli", "siralama": "ucuz"}
|
| 123 |
+
- "su yeşili xs marlin hangi mağazada" → {"urun": "marlin", "beden": "XS", "renk": "su yeşili"}
|
| 124 |
+
- "caddebostanda kaç tane marlin var" → {"urun": "marlin", "magaza": "caddebostan", "adet_sorusu": true}
|
| 125 |
+
- "100 bin altında dağ bisikleti" → {"kategori": "dag", "fiyat_max": 100000}
|
| 126 |
+
- "oğlum için 80 bin bütçeyle mtb arıyoruz" → {"kategori": "dag", "fiyat_max": 80000}
|
| 127 |
+
- "madone kaç para" → {"urun": "madone"}
|
| 128 |
+
- "bunun mavisi var mı" → {"onceki_urun": true, "renk": "mavi"}
|
| 129 |
+
- "karbonda hangi modeller var" → {"malzeme": "karbon"}
|
| 130 |
+
- "merhaba nasılsınız" → stok_sorgusu: null"""
|
| 131 |
|
| 132 |
# Bağlam varsa ekle
|
| 133 |
context_info = ""
|
|
@@ -578,6 +578,232 @@ def _detect_material(query: str) -> str | None:
|
|
| 578 |
return None
|
| 579 |
|
| 580 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 581 |
def _seal(lines: list[str]) -> list[str]:
|
| 582 |
"""Hukum listesini KAPAT: modele 'listede olmayani ekleme' de.
|
| 583 |
|
|
|
|
| 578 |
return None
|
| 579 |
|
| 580 |
|
| 581 |
+
# ---------- YAPILANDIRILMIS SORGU (11 Agu gece mimari degisikligi) ----------
|
| 582 |
+
# ANLAMA LLM'DE, HESAP KODDA. Eski yol: kod, Turkce'yi stopword/regex'le
|
| 583 |
+
# ayristirmaya calisiyordu — her cekim icin kural gerekiyordu ('elektrikli'
|
| 584 |
+
# kurali vardi, 'Elektrik Bisiklet' bir harf farkiyla deldi; kullanici hakli:
|
| 585 |
+
# binlerce kural yazilamaz). Yeni yol: intent analyzer (LLM, zaten her
|
| 586 |
+
# mesajda calisiyor) serbest metni SU YAPIYA cevirir, kod calistirir:
|
| 587 |
+
# {urun, kategori, beden, renk, magaza, fiyat_max, fiyat_min,
|
| 588 |
+
# siralama, adet_sorusu, malzeme, onceki_urun}
|
| 589 |
+
# Boylece 'sarjli bisiklet', 'akulu', 'pilli' icin kural GEREKMEZ — model
|
| 590 |
+
# Turkce'yi zaten biliyor. Sayma/filtreleme/siralama kodda kalir (olcum:
|
| 591 |
+
# Luna ve Terra 19 dedi, kod 25 — dogru).
|
| 592 |
+
|
| 593 |
+
_STRUCT_CATEGORY_MAP = {
|
| 594 |
+
"elektrikli": "ELEKTRİKLİ", "sehir": "ŞEHİR/HİBRİT", "hibrit": "ŞEHİR/HİBRİT",
|
| 595 |
+
"dag": "DAĞ", "yol": "YOL", "gravel": "GRAVEL", "cocuk": "ÇOCUK",
|
| 596 |
+
"giyim": "GİYİM", "lastik": "LASTİK", "kask": "KASK", "pompa": "POMPA",
|
| 597 |
+
}
|
| 598 |
+
|
| 599 |
+
|
| 600 |
+
def _rule_for_label(label: str):
|
| 601 |
+
for lb, _qw, cat_pat, name_pat in _CATEGORY_RULES:
|
| 602 |
+
if lb == label:
|
| 603 |
+
return cat_pat, name_pat
|
| 604 |
+
return None, None
|
| 605 |
+
|
| 606 |
+
|
| 607 |
+
def lookup_stock_structured(p: dict) -> list[str] | None:
|
| 608 |
+
"""Intent analyzer'in cikardigi yapilandirilmis sorguyu calistirir.
|
| 609 |
+
Donus: lookup_stock_for_query ile ayni format (hukumlu satir listesi).
|
| 610 |
+
None = bu sorgu stokla cozulemez -> cagiran taraf eski yola/listeye duser."""
|
| 611 |
+
if not isinstance(p, dict):
|
| 612 |
+
return None
|
| 613 |
+
|
| 614 |
+
urun = (p.get("urun") or "").strip()
|
| 615 |
+
kategori_raw = normalize((p.get("kategori") or "").strip())
|
| 616 |
+
beden = (p.get("beden") or "").strip().upper() or None
|
| 617 |
+
renk = normalize((p.get("renk") or "").strip()) or None
|
| 618 |
+
magaza = (p.get("magaza") or "").strip().upper() or None
|
| 619 |
+
if magaza:
|
| 620 |
+
magaza = normalize(magaza).upper().replace("İ", "I")
|
| 621 |
+
magaza = {"CADDEBOSTAN": "CADDEBOSTAN", "ALSANCAK": "ALSANCAK",
|
| 622 |
+
"BAHCEKOY": "BAHCEKOY"}.get(magaza, None)
|
| 623 |
+
try:
|
| 624 |
+
fiyat_max = float(p["fiyat_max"]) if p.get("fiyat_max") else None
|
| 625 |
+
fiyat_min = float(p["fiyat_min"]) if p.get("fiyat_min") else None
|
| 626 |
+
except (TypeError, ValueError):
|
| 627 |
+
fiyat_max = fiyat_min = None
|
| 628 |
+
siralama = (p.get("siralama") or "").strip().lower() or None
|
| 629 |
+
adet_sorusu = bool(p.get("adet_sorusu"))
|
| 630 |
+
malzeme = (p.get("malzeme") or "").strip().lower() or None
|
| 631 |
+
|
| 632 |
+
# Hicbir kriter yoksa yapacak is yok
|
| 633 |
+
if not any([urun, kategori_raw, beden, renk, fiyat_max, fiyat_min,
|
| 634 |
+
siralama, adet_sorusu]):
|
| 635 |
+
return None
|
| 636 |
+
|
| 637 |
+
# Malzeme sorusu -> None: app.py kompakt liste + malzeme talimati gonderir
|
| 638 |
+
# (A/B olcumu: siniflandirmayi LLM listeden koddan IYI yapiyor)
|
| 639 |
+
if malzeme and not any([beden, renk, fiyat_max, adet_sorusu]):
|
| 640 |
+
logger.info("[wh_struct] malzeme sorusu -> kompakt listeye birakildi")
|
| 641 |
+
return None
|
| 642 |
+
|
| 643 |
+
# Rakip marka koruma
|
| 644 |
+
if urun and set(re.findall(r"[a-z0-9]+", normalize(urun))) & _COMPETITOR_TOKENS:
|
| 645 |
+
logger.info(f"[wh_struct] rakip marka: {urun!r}")
|
| 646 |
+
return None
|
| 647 |
+
|
| 648 |
+
idx = get_index()
|
| 649 |
+
sku_map = _get_sku_map()
|
| 650 |
+
if not sku_map or not idx.ensure():
|
| 651 |
+
return None
|
| 652 |
+
|
| 653 |
+
# --- AILE SECIMI ---
|
| 654 |
+
label = _STRUCT_CATEGORY_MAP.get(kategori_raw) if kategori_raw else None
|
| 655 |
+
cat_pat, name_pat = _rule_for_label(label) if label else (None, None)
|
| 656 |
+
urun_tokens = [t for t in re.findall(r"[a-z0-9]+", normalize(urun))
|
| 657 |
+
if t not in ("bisiklet", "bisikleti", "bike", "model", "modeli")] if urun else []
|
| 658 |
+
bike_search = bool(label and label not in ("GİYİM", "LASTİK", "KASK", "POMPA")) or \
|
| 659 |
+
any(t in _BIKE_FAMILY_TOKENS for t in urun_tokens)
|
| 660 |
+
|
| 661 |
+
family: list[dict] = []
|
| 662 |
+
for prod in idx.snapshot():
|
| 663 |
+
if prod["is_variant"]:
|
| 664 |
+
continue
|
| 665 |
+
nn = prod.get("_label_norm", "")
|
| 666 |
+
if not nn:
|
| 667 |
+
continue
|
| 668 |
+
if urun_tokens:
|
| 669 |
+
words = set(re.findall(r"[a-z0-9]+", nn))
|
| 670 |
+
if not all(t in words for t in urun_tokens):
|
| 671 |
+
continue
|
| 672 |
+
if label:
|
| 673 |
+
cn = prod.get("_category_norm", "")
|
| 674 |
+
if cn:
|
| 675 |
+
if cat_pat and not cat_pat.search(cn):
|
| 676 |
+
continue
|
| 677 |
+
elif name_pat and not name_pat.search(nn):
|
| 678 |
+
continue
|
| 679 |
+
if bike_search and any(t in nn for t in _ACCESSORY_TOKENS):
|
| 680 |
+
continue
|
| 681 |
+
family.append(prod)
|
| 682 |
+
|
| 683 |
+
# Kriter var ama urun/kategori yok (sadece renk/beden/fiyat/siralama):
|
| 684 |
+
# tum bisiklet havuzunda ara
|
| 685 |
+
if not family and not urun_tokens and not label:
|
| 686 |
+
family = _all_bikes()
|
| 687 |
+
|
| 688 |
+
if not family:
|
| 689 |
+
if label or urun_tokens:
|
| 690 |
+
hedef = urun or (label or "")
|
| 691 |
+
return _seal([f"[STOK HÜKMÜ — KESİN VERİ] '{hedef}' için stokta ürün YOK ❌. "
|
| 692 |
+
f"Model/fiyat UYDURMA, müşteriyi mağazaya yönlendir."])
|
| 693 |
+
return None
|
| 694 |
+
|
| 695 |
+
cat_suffix = f" ({label})" if label else ""
|
| 696 |
+
|
| 697 |
+
# --- ADET SAYMA ---
|
| 698 |
+
if adet_sorusu:
|
| 699 |
+
toplam = 0
|
| 700 |
+
dagilim: dict[str, int] = {}
|
| 701 |
+
satirlar: list[str] = []
|
| 702 |
+
for m in family:
|
| 703 |
+
link = m.get("link") or ""
|
| 704 |
+
for v in (idx.variants_of(link) if link else [m]):
|
| 705 |
+
sku = (v.get("sku") or "").strip()
|
| 706 |
+
if not sku:
|
| 707 |
+
continue
|
| 708 |
+
for wh, qty in sku_map.get(sku, []):
|
| 709 |
+
if magaza and wh.upper() != magaza:
|
| 710 |
+
continue
|
| 711 |
+
if beden and not _variant_matches_size(v, beden):
|
| 712 |
+
continue
|
| 713 |
+
if renk and renk not in normalize(_format_variant_label(v)):
|
| 714 |
+
continue
|
| 715 |
+
toplam += qty
|
| 716 |
+
dagilim[wh] = dagilim.get(wh, 0) + qty
|
| 717 |
+
satirlar.append(f"• {m['name']} {_format_variant_label(v)} — {wh}: {qty} adet")
|
| 718 |
+
if toplam:
|
| 719 |
+
nerede = f"{magaza} mağazasında" if magaza else "tüm mağazalarda"
|
| 720 |
+
dag = ", ".join(f"{k}: {v}" for k, v in sorted(dagilim.items(), key=lambda x: -x[1]))
|
| 721 |
+
out = [f"[ADET HÜKMÜ — KESİN VERİ] {nerede} TOPLAM {toplam} ADET var. "
|
| 722 |
+
f"Dağılım: {dag}. Bu sayı sistem hesabı — KENDİN SAYMA:"]
|
| 723 |
+
out.extend(satirlar[:30])
|
| 724 |
+
return _seal(out)
|
| 725 |
+
return _seal([f"[ADET HÜKMÜ — KESİN VERİ] {'bu kriterlerle' if not magaza else magaza + ' mağazasında'} "
|
| 726 |
+
f"stok YOK (0 adet). Sayı UYDURMA."])
|
| 727 |
+
|
| 728 |
+
# --- FIYAT FILTRESI ---
|
| 729 |
+
if fiyat_min is not None or fiyat_max is not None:
|
| 730 |
+
family = [m for m in family if m.get("_category_norm") or not label]
|
| 731 |
+
family = [m for m in family if _price_in_range(m.get("price"), fiyat_min, fiyat_max)]
|
| 732 |
+
family.sort(key=lambda m: m.get("price") or 0)
|
| 733 |
+
|
| 734 |
+
# --- SIRALAMA (en ucuz / en pahali) ---
|
| 735 |
+
# STOKLU olanlar arasindan sec: fiyat sirali katalog listesinin basi
|
| 736 |
+
# stoksuz eski modellerle dolu olabiliyor (ilk denemede 'en ucuz
|
| 737 |
+
# elektrikli' bos donmustu — en ucuz 5 kayit depoda yoktu).
|
| 738 |
+
if siralama in ("ucuz", "pahali", "cheap", "expensive"):
|
| 739 |
+
pool = [m for m in family if m.get("price")]
|
| 740 |
+
if label:
|
| 741 |
+
cat_ok = [m for m in pool if m.get("_category_norm")]
|
| 742 |
+
if cat_ok:
|
| 743 |
+
pool = cat_ok # kategori dogrulanmis olanlar (aksesuar sizmasin)
|
| 744 |
+
ucuz = siralama in ("ucuz", "cheap")
|
| 745 |
+
pool.sort(key=lambda m: m["price"], reverse=not ucuz)
|
| 746 |
+
picked: list[list[str]] = []
|
| 747 |
+
for m in pool:
|
| 748 |
+
lines, found, _ = _stock_lines_for_main(
|
| 749 |
+
m, sku_map, idx, size=beden, name_suffix=cat_suffix, color=renk)
|
| 750 |
+
if found:
|
| 751 |
+
picked.append(lines)
|
| 752 |
+
if len(picked) >= 3:
|
| 753 |
+
break
|
| 754 |
+
if picked:
|
| 755 |
+
out = [f"[STOK HÜKMÜ — KESİN VERİ] Stoktaki EN "
|
| 756 |
+
f"{'UYGUN FİYATLI' if ucuz else 'ÜST SEGMENT'} {kind if (kind := (label + ' ') if label else '') else ''}"
|
| 757 |
+
f"modeller ({'ucuzdan' if ucuz else 'pahalıdan'} sıralı). "
|
| 758 |
+
f"Fiyat hesabı YAPMA, sırayı DEĞİŞTİRME:"]
|
| 759 |
+
for s in picked:
|
| 760 |
+
out.extend(s)
|
| 761 |
+
return _seal(out)
|
| 762 |
+
return _seal([f"[STOK HÜKMÜ — KESİN VERİ] Bu kriterlerle stokta fiyatlı ürün "
|
| 763 |
+
f"bulunamadı ❌. Fiyat/model UYDURMA, mağazaya yönlendir."])
|
| 764 |
+
|
| 765 |
+
# --- SONUC LISTESI ---
|
| 766 |
+
sections: list[list[str]] = []
|
| 767 |
+
all_sizes: set[str] = set()
|
| 768 |
+
for m in family:
|
| 769 |
+
lines, found, sizes = _stock_lines_for_main(
|
| 770 |
+
m, sku_map, idx, size=beden, name_suffix=cat_suffix, color=renk)
|
| 771 |
+
all_sizes |= sizes
|
| 772 |
+
if found:
|
| 773 |
+
sections.append(lines)
|
| 774 |
+
if len(sections) >= 8:
|
| 775 |
+
break
|
| 776 |
+
|
| 777 |
+
kind = f"{label} " if label else ""
|
| 778 |
+
if sections:
|
| 779 |
+
parts = []
|
| 780 |
+
if beden:
|
| 781 |
+
parts.append(f"{beden} beden")
|
| 782 |
+
if renk:
|
| 783 |
+
parts.append(f"'{renk.upper()}' renk")
|
| 784 |
+
if fiyat_max:
|
| 785 |
+
parts.append(f"{int(fiyat_max):,} TL altı".replace(",", "."))
|
| 786 |
+
kriter = " + ".join(parts) if parts else "aranan kriterlerde"
|
| 787 |
+
out = [f"[STOK HÜKMÜ — KESİN VERİ] {kriter} {kind}ürün STOKTA VAR ✅. "
|
| 788 |
+
f"Aşağıdakileri eksiksiz aktar, 'stok yok' YAZMA:"]
|
| 789 |
+
for s in sections:
|
| 790 |
+
out.extend(s)
|
| 791 |
+
return _seal(out)
|
| 792 |
+
|
| 793 |
+
# Aile var ama filtre bos birakti -> kesin YOK + alternatifler
|
| 794 |
+
if beden or renk or fiyat_max:
|
| 795 |
+
sizes_str = ", ".join(sorted(all_sizes)) if all_sizes else "-"
|
| 796 |
+
out = [f"[STOK HÜKMÜ — KESİN VERİ] Bu kriterlerle STOKTA YOK ❌. "
|
| 797 |
+
f"Stokta olan bedenler: {sizes_str}. Alternatif sun, UYDURMA:"]
|
| 798 |
+
for m in family[:5]:
|
| 799 |
+
lines, found, _ = _stock_lines_for_main(m, sku_map, idx, name_suffix=cat_suffix)
|
| 800 |
+
if found:
|
| 801 |
+
out.extend(lines)
|
| 802 |
+
return _seal(out)
|
| 803 |
+
|
| 804 |
+
return None
|
| 805 |
+
|
| 806 |
+
|
| 807 |
def _seal(lines: list[str]) -> list[str]:
|
| 808 |
"""Hukum listesini KAPAT: modele 'listede olmayani ekleme' de.
|
| 809 |
|