samikoen commited on
Commit
6ea7409
·
0 Parent(s):

Deploy from GitHub Actions

Browse files
Dockerfile ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ RUN apt-get update && apt-get install -y build-essential && rm -rf /var/lib/apt/lists/*
6
+
7
+ COPY requirements.txt .
8
+ RUN pip install --no-cache-dir -r requirements.txt
9
+
10
+ COPY . .
11
+
12
+ EXPOSE 7860
13
+
14
+ CMD ["python", "app.py"]
README.md ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Trek WhatsApp AI
3
+ emoji: 🚴
4
+ colorFrom: red
5
+ colorTo: blue
6
+ sdk: docker
7
+ pinned: false
8
+ ---
9
+
10
+ # Trek WhatsApp AI Chatbot
11
+
12
+ Trek Bicycle Turkey için WhatsApp üzerinden çalışan AI chatbot.
13
+
14
+ ## Canlı Ortam
15
+ - **HuggingFace Space:** https://huggingface.co/spaces/SamiKoen/BF-WAB
16
+ - **Webhook:** Twilio WhatsApp entegrasyonu
17
+
18
+ ## Teknoloji
19
+ - **GPT-4o:** Görsel mesajlar için (Vision)
20
+ - **GPT-5.2:** Metin mesajları için
21
+ - **BizimHesap API:** Stok sorguları
22
+ - **IdeaSoft XML:** Ürün bilgileri
23
+
24
+ ## Ana Dosyalar
25
+ | Dosya | Açıklama |
26
+ |-------|----------|
27
+ | app.py | Ana uygulama |
28
+ | prompts.py | System promptları |
29
+ | smart_warehouse_with_price.py | Stok + fiyat sorgusu |
30
+ | intent_analyzer.py | Müşteri niyet analizi |
31
+ | store_notification.py | Mağaza bildirimleri |
32
+
33
+ ## Deploy
34
+ HuggingFace'e push etmek için:
35
+ ```bash
36
+ cd /tmp && rm -rf bf-wab-clone
37
+ git clone https://SamiKoen:TOKEN@huggingface.co/spaces/SamiKoen/BF-WAB bf-wab-clone
38
+ # Değişiklikleri yap
39
+ git add . && git commit -m "mesaj" && git push
40
+ ```
41
+ # Last deploy: Mon, Jan 19, 2026 5:36:55 PM
42
+ # Deploy test: Mon, Jan 19, 2026 5:39:13 PM
app.py ADDED
@@ -0,0 +1,1810 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Trek WhatsApp AI Asistani - Hybrid Model Version
4
+ Gorsel varsa: GPT-4o (vision)
5
+ Metin varsa: GPT-5.2 (daha akilli)
6
+ """
7
+
8
+ import os
9
+ import json
10
+ import re
11
+ import requests
12
+ import xml.etree.ElementTree as ET
13
+ import warnings
14
+ import time
15
+ import threading
16
+ import datetime
17
+ import unicodedata
18
+ from concurrent.futures import ThreadPoolExecutor, as_completed
19
+ from fastapi import FastAPI, Request
20
+ from twilio.rest import Client
21
+ from twilio.twiml.messaging_response import MessagingResponse
22
+
23
+ # Yeni moduller - Basit sistem
24
+ from prompts import get_active_prompts
25
+ from whatsapp_renderer import extract_product_info_whatsapp
26
+ from whatsapp_passive_profiler import (
27
+ analyze_user_message, get_user_profile_summary, get_personalized_recommendations
28
+ )
29
+
30
+ # LOGGING EN BASA EKLENDI
31
+ import logging
32
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
33
+ logger = logging.getLogger(__name__)
34
+
35
+ # Import improved WhatsApp search for BF space
36
+ # DISABLED - Using GPT-5 smart warehouse search instead
37
+ USE_IMPROVED_SEARCH = False
38
+
39
+ warnings.simplefilter('ignore')
40
+
41
+ # ===============================
42
+ # MODEL KONFIGURASYONU
43
+ # ===============================
44
+ MODEL_CONFIG = {
45
+ "vision": "gpt-4o", # Gorsel analizi icin (Vision destekli)
46
+ "text": "gpt-5.2-chat-latest", # Metin icin (en akilli model)
47
+ "fallback": "gpt-4o" # Yedek model (GPT-5.2 hata verirse)
48
+ }
49
+
50
+ # Model secimi icin yardimci fonksiyon
51
+ def get_model_for_request(has_media=False):
52
+ """
53
+ Istek tipine gore uygun modeli sec
54
+ has_media=True -> GPT-4o (vision destekli)
55
+ has_media=False -> GPT-5.2 (daha akilli metin isleme)
56
+ """
57
+ if has_media:
58
+ logger.info(f"🖼️ Gorsel tespit edildi -> Model: {MODEL_CONFIG['vision']}")
59
+ return MODEL_CONFIG["vision"]
60
+ else:
61
+ logger.info(f"📝 Metin mesaji -> Model: {MODEL_CONFIG['text']}")
62
+ return MODEL_CONFIG["text"]
63
+
64
+ # ===============================
65
+ # API AYARLARI
66
+ # ===============================
67
+ API_URL = "https://api.openai.com/v1/chat/completions"
68
+ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
69
+ logger.info(f"OpenAI API Key var mi: {'Evet' if OPENAI_API_KEY else 'Hayir'}")
70
+
71
+ # Twilio WhatsApp ayarlari
72
+ TWILIO_ACCOUNT_SID = os.getenv("TWILIO_ACCOUNT_SID")
73
+ TWILIO_AUTH_TOKEN = os.getenv("TWILIO_AUTH_TOKEN")
74
+ TWILIO_MESSAGING_SERVICE_SID = os.getenv("TWILIO_MESSAGING_SERVICE_SID", "MG11c1dfac28ad5f81908ec9ede0f7247f")
75
+ TWILIO_WHATSAPP_NUMBER = "whatsapp:+905332047254" # Bizim WhatsApp Business numaramiz
76
+
77
+ logger.info(f"Twilio SID var mi: {'Evet' if TWILIO_ACCOUNT_SID else 'Hayir'}")
78
+ logger.info(f"Twilio Auth Token var mi: {'Evet' if TWILIO_AUTH_TOKEN else 'Hayir'}")
79
+ logger.info(f"Messaging Service SID var mi: {'Evet' if TWILIO_MESSAGING_SERVICE_SID else 'Hayir'}")
80
+
81
+ if not TWILIO_ACCOUNT_SID or not TWILIO_AUTH_TOKEN:
82
+ logger.error("❌ Twilio bilgileri eksik!")
83
+ twilio_client = None
84
+ else:
85
+ try:
86
+ twilio_client = Client(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN)
87
+ logger.info("✅ Twilio client basariyla olusturuldu!")
88
+ except Exception as e:
89
+ logger.error(f"❌ Twilio client hatasi: {e}")
90
+ twilio_client = None
91
+
92
+ # ===============================
93
+ # GPT-5 SMART WAREHOUSE
94
+ # ===============================
95
+ try:
96
+ from smart_warehouse_with_price import get_warehouse_stock_smart_with_price
97
+ USE_GPT5_SEARCH = True
98
+ logger.info("✅ GPT-5 complete smart warehouse with price (BF algorithm) loaded")
99
+ except ImportError:
100
+ USE_GPT5_SEARCH = False
101
+ logger.info("❌ GPT-5 search not available")
102
+
103
+ # Import Media Queue V2
104
+ try:
105
+ from media_queue_v2 import media_queue
106
+ USE_MEDIA_QUEUE = True
107
+ logger.info("✅ Media Queue V2 loaded successfully")
108
+ except ImportError:
109
+ USE_MEDIA_QUEUE = False
110
+ logger.info("❌ Media Queue V2 not available")
111
+
112
+ # Import Store Notification System
113
+ try:
114
+ from store_notification import (
115
+ notify_product_reservation,
116
+ notify_price_inquiry,
117
+ notify_stock_inquiry,
118
+ send_test_notification,
119
+ send_store_notification,
120
+ should_notify_mehmet_bey
121
+ )
122
+ USE_STORE_NOTIFICATION = True
123
+ logger.info("✅ Store Notification System loaded")
124
+ except ImportError:
125
+ USE_STORE_NOTIFICATION = False
126
+ logger.info("❌ Store Notification System not available")
127
+
128
+ # Import Follow-Up System
129
+ try:
130
+ from follow_up_system import (
131
+ FollowUpManager,
132
+ analyze_message_for_follow_up,
133
+ FollowUpType
134
+ )
135
+ USE_FOLLOW_UP = True
136
+ follow_up_manager = FollowUpManager()
137
+ logger.info("✅ Follow-Up System loaded")
138
+ except ImportError:
139
+ USE_FOLLOW_UP = False
140
+ follow_up_manager = None
141
+ logger.info("❌ Follow-Up System not available")
142
+
143
+ # Import Intent Analyzer
144
+ try:
145
+ from intent_analyzer import (
146
+ analyze_customer_intent,
147
+ should_notify_store,
148
+ get_smart_notification_message
149
+ )
150
+ USE_INTENT_ANALYZER = True
151
+ logger.info("✅ GPT-5 Intent Analyzer loaded")
152
+ except ImportError:
153
+ USE_INTENT_ANALYZER = False
154
+ logger.info("❌ Intent Analyzer not available")
155
+
156
+ # ===============================
157
+ # STOK API ENTEGRASYONU
158
+ # ===============================
159
+ STOCK_API_BASE = "https://video.trek-turkey.com/bizimhesap-proxy.php"
160
+
161
+ # Stock cache (5 dakikalik cache)
162
+ stock_cache = {}
163
+ CACHE_DURATION = 300 # 5 dakika (saniye cinsinden)
164
+
165
+ # Turkish character normalization
166
+ turkish_map = {'ı': 'i', 'ğ': 'g', 'ü': 'u', 'ş': 's', 'ö': 'o', 'ç': 'c', 'İ': 'i', 'I': 'i'}
167
+
168
+ def normalize_turkish(text):
169
+ """Turkce karakterleri normalize et"""
170
+ if not text:
171
+ return ""
172
+ text = unicodedata.normalize('NFD', text)
173
+ text = ''.join(char for char in text if unicodedata.category(char) != 'Mn')
174
+ for tr_char, en_char in turkish_map.items():
175
+ text = text.replace(tr_char, en_char)
176
+ return text.lower()
177
+
178
+ def fetch_warehouse_inventory(warehouse, product_name, search_terms):
179
+ """Tek bir magazanin stok bilgisini al"""
180
+ try:
181
+ warehouse_id = warehouse['id']
182
+ warehouse_name = warehouse['title']
183
+
184
+ # DSW'yi ayri tut (gelecek stok icin)
185
+ is_dsw = 'DSW' in warehouse_name or 'ÖN SİPARİŞ' in warehouse_name.upper()
186
+
187
+ # Magaza stoklarini al
188
+ inventory_url = f"{STOCK_API_BASE}?action=inventory&warehouse={warehouse_id}&endpoint=inventory/{warehouse_id}"
189
+ inventory_response = requests.get(inventory_url, timeout=3, verify=False)
190
+
191
+ if inventory_response.status_code != 200:
192
+ return None
193
+
194
+ inventory_data = inventory_response.json()
195
+
196
+ # API yanitini kontrol et
197
+ if 'data' not in inventory_data or 'inventory' not in inventory_data['data']:
198
+ return None
199
+
200
+ products_list = inventory_data['data']['inventory']
201
+
202
+ # Beden terimleri kontrolu
203
+ size_terms = ['xs', 's', 'm', 'ml', 'l', 'xl', 'xxl', '2xl', '3xl', 'small', 'medium', 'large']
204
+ size_numbers = ['44', '46', '48', '50', '52', '54', '56', '58', '60']
205
+
206
+ # Arama terimlerinde beden var mi kontrol et
207
+ has_size_query = False
208
+ size_query = None
209
+ for term in search_terms:
210
+ if term in size_terms or term in size_numbers:
211
+ has_size_query = True
212
+ size_query = term
213
+ break
214
+
215
+ # Eger sadece beden sorgusu varsa (or: "m", "xl")
216
+ is_only_size_query = len(search_terms) == 1 and has_size_query
217
+
218
+ # Urunu ara
219
+ warehouse_variants = []
220
+ dsw_stock_count = 0
221
+
222
+ for product in products_list:
223
+ product_title = normalize_turkish(product.get('title', '')).lower()
224
+ original_title = product.get('title', '')
225
+
226
+ # Eger sadece beden sorgusu ise
227
+ if is_only_size_query:
228
+ if size_query in product_title.split() or f'({size_query})' in product_title or f' {size_query} ' in product_title or product_title.endswith(f' {size_query}'):
229
+ qty = int(product.get('qty', 0))
230
+ stock = int(product.get('stock', 0))
231
+ actual_stock = max(qty, stock)
232
+
233
+ if actual_stock > 0:
234
+ if is_dsw:
235
+ dsw_stock_count += actual_stock
236
+ continue
237
+ warehouse_variants.append(f"{original_title}: ✓ Stokta")
238
+ else:
239
+ # Normal urun aramasi
240
+ if has_size_query:
241
+ non_size_terms = [t for t in search_terms if t != size_query]
242
+ product_matches = all(term in product_title for term in non_size_terms)
243
+ size_matches = size_query in product_title.split() or f'({size_query})' in product_title or f' {size_query} ' in product_title or product_title.endswith(f' {size_query}')
244
+
245
+ if product_matches and size_matches:
246
+ qty = int(product.get('qty', 0))
247
+ stock = int(product.get('stock', 0))
248
+ actual_stock = max(qty, stock)
249
+
250
+ if actual_stock > 0:
251
+ if is_dsw:
252
+ dsw_stock_count += actual_stock
253
+ continue
254
+
255
+ variant_info = original_title
256
+ possible_names = [
257
+ product_name.upper(),
258
+ product_name.lower(),
259
+ product_name.title(),
260
+ product_name.upper().replace('I', 'İ'),
261
+ product_name.upper().replace('İ', 'I'),
262
+ ]
263
+
264
+ if 'fx sport' in product_name.lower():
265
+ possible_names.extend(['FX Sport AL 3', 'FX SPORT AL 3', 'Fx Sport Al 3'])
266
+
267
+ for possible_name in possible_names:
268
+ variant_info = variant_info.replace(possible_name, '').strip()
269
+
270
+ variant_info = ' '.join(variant_info.split())
271
+
272
+ if variant_info and variant_info != original_title:
273
+ warehouse_variants.append(f"{variant_info}: ✓ Stokta")
274
+ else:
275
+ warehouse_variants.append(f"{original_title}: ✓ Stokta")
276
+ else:
277
+ if all(term in product_title for term in search_terms):
278
+ qty = int(product.get('qty', 0))
279
+ stock = int(product.get('stock', 0))
280
+ actual_stock = max(qty, stock)
281
+
282
+ if actual_stock > 0:
283
+ if is_dsw:
284
+ dsw_stock_count += actual_stock
285
+ continue
286
+
287
+ variant_info = original_title
288
+ possible_names = [
289
+ product_name.upper(),
290
+ product_name.lower(),
291
+ product_name.title(),
292
+ product_name.upper().replace('I', 'İ'),
293
+ product_name.upper().replace('İ', 'I'),
294
+ ]
295
+
296
+ if 'fx sport' in product_name.lower():
297
+ possible_names.extend(['FX Sport AL 3', 'FX SPORT AL 3', 'Fx Sport Al 3'])
298
+
299
+ for possible_name in possible_names:
300
+ variant_info = variant_info.replace(possible_name, '').strip()
301
+
302
+ variant_info = ' '.join(variant_info.split())
303
+
304
+ if variant_info and variant_info != original_title:
305
+ warehouse_variants.append(f"{variant_info}: ✓ Stokta")
306
+ else:
307
+ warehouse_variants.append(f"{original_title}: ✓ Stokta")
308
+
309
+ # Sonuc dondur
310
+ if warehouse_variants and not is_dsw:
311
+ return {'warehouse': warehouse_name, 'variants': warehouse_variants, 'is_dsw': False}
312
+ elif dsw_stock_count > 0:
313
+ return {'dsw_stock': dsw_stock_count, 'is_dsw': True}
314
+
315
+ return None
316
+
317
+ except Exception:
318
+ return None
319
+
320
+ def get_realtime_stock_parallel(product_name):
321
+ """API'den gercek zamanli stok bilgisini cek - Paralel versiyon with cache"""
322
+ try:
323
+ # Cache kontrolu
324
+ cache_key = normalize_turkish(product_name).lower()
325
+ current_time = time.time()
326
+
327
+ if cache_key in stock_cache:
328
+ cached_data, cached_time = stock_cache[cache_key]
329
+ if current_time - cached_time < CACHE_DURATION:
330
+ logger.info(f"Cache'den donduruluyor: {product_name}")
331
+ return cached_data
332
+
333
+ # Once magaza listesini al
334
+ warehouses_url = f"{STOCK_API_BASE}?action=warehouses&endpoint=warehouses"
335
+ warehouses_response = requests.get(warehouses_url, timeout=3, verify=False)
336
+
337
+ if warehouses_response.status_code != 200:
338
+ logger.error(f"Magaza listesi alinamadi: {warehouses_response.status_code}")
339
+ return None
340
+
341
+ warehouses_data = warehouses_response.json()
342
+
343
+ if 'data' not in warehouses_data or 'warehouses' not in warehouses_data['data']:
344
+ logger.error("Magaza verisi bulunamadi")
345
+ return None
346
+
347
+ warehouses = warehouses_data['data']['warehouses']
348
+
349
+ # Urun adini normalize et
350
+ search_terms = normalize_turkish(product_name).lower().split()
351
+ logger.info(f"Aranan urun: {product_name} -> {search_terms}")
352
+
353
+ stock_info = {}
354
+ total_dsw_stock = 0
355
+ total_stock = 0
356
+
357
+ # Paralel olarak tum magazalari sorgula
358
+ with ThreadPoolExecutor(max_workers=10) as executor:
359
+ futures = {
360
+ executor.submit(fetch_warehouse_inventory, warehouse, product_name, search_terms): warehouse
361
+ for warehouse in warehouses
362
+ }
363
+
364
+ for future in as_completed(futures):
365
+ result = future.result()
366
+ if result:
367
+ if result.get('is_dsw'):
368
+ total_dsw_stock += result.get('dsw_stock', 0)
369
+ else:
370
+ warehouse_name = result['warehouse']
371
+ stock_info[warehouse_name] = result['variants']
372
+ total_stock += 1
373
+
374
+ # Sonucu olustur
375
+ if not stock_info:
376
+ if total_dsw_stock > 0:
377
+ result = f"{product_name}: Su anda magazalarda stokta yok, ancak yakinda gelecek. On siparis verebilirsiniz."
378
+ else:
379
+ result = f"{product_name}: Su anda hicbir magazada stokta bulunmuyor."
380
+ else:
381
+ prompt_lines = [f"{product_name} stok durumu:"]
382
+ for warehouse, variants in stock_info.items():
383
+ if isinstance(variants, list):
384
+ prompt_lines.append(f"- {warehouse}:")
385
+ for variant in variants:
386
+ prompt_lines.append(f" • {variant}")
387
+ else:
388
+ prompt_lines.append(f"- {warehouse}: {variants}")
389
+
390
+ if total_stock > 0:
391
+ prompt_lines.append(f"✓ Urun stokta mevcut")
392
+
393
+ result = "\n".join(prompt_lines)
394
+
395
+ # Sonucu cache'e kaydet
396
+ stock_cache[cache_key] = (result, current_time)
397
+
398
+ return result
399
+
400
+ except Exception as e:
401
+ logger.error(f"API hatasi: {e}")
402
+ return None
403
+
404
+ def is_stock_query(message):
405
+ """Mesajin stok sorgusu olup olmadigini kontrol et - Basit yedek kontrol"""
406
+ # Bu fonksiyon artik sadece yedek olarak kullaniliyor
407
+ # Ana tespit Intent Analyzer tarafindan yapiliyor
408
+ basic_keywords = ['stok', 'stock', 'var mı', 'mevcut']
409
+ message_lower = message.lower()
410
+ return any(keyword in message_lower for keyword in basic_keywords)
411
+
412
+ # ===============================
413
+ # MAGAZA STOK BILGISI CEKME
414
+ # ===============================
415
+ def get_warehouse_stock(product_name):
416
+ """B2B API'den magaza stok bilgilerini cek - GPT-5 enhanced"""
417
+ # Try GPT-5 complete smart search (BF algorithm)
418
+ if USE_GPT5_SEARCH:
419
+ try:
420
+ gpt5_result = get_warehouse_stock_smart_with_price(product_name)
421
+ if gpt5_result and isinstance(gpt5_result, list):
422
+ if all(isinstance(item, str) for item in gpt5_result):
423
+ return gpt5_result
424
+ warehouse_info = []
425
+ for item in gpt5_result:
426
+ if isinstance(item, dict):
427
+ info = f"📦 {item.get('name', '')}"
428
+ if item.get('variant'):
429
+ info += f" ({item['variant']})"
430
+ if item.get('warehouses'):
431
+ info += f"\n📍 Mevcut: {', '.join(item['warehouses'])}"
432
+ if item.get('price'):
433
+ info += f"\n💰 {item['price']}"
434
+ warehouse_info.append(info)
435
+ else:
436
+ warehouse_info.append(str(item))
437
+ return warehouse_info if warehouse_info else None
438
+ except Exception as e:
439
+ logger.error(f"GPT-5 warehouse search error: {e}")
440
+
441
+ # Fallback to original search
442
+ try:
443
+ import re
444
+ warehouse_url = 'https://video.trek-turkey.com/bizimhesap-warehouse-xml-b2b-api-v2.php'
445
+ response = requests.get(warehouse_url, verify=False, timeout=15)
446
+
447
+ if response.status_code != 200:
448
+ return None
449
+
450
+ root = ET.fromstring(response.content)
451
+
452
+ # Normalize search product name
453
+ search_name = normalize_turkish(product_name.lower().strip())
454
+ search_name = search_name.replace('(2026)', '').replace('(2025)', '').replace(' gen 3', '').replace(' gen', '').strip()
455
+ search_words = search_name.split()
456
+
457
+ best_matches = []
458
+ exact_matches = []
459
+ variant_matches = []
460
+ candidates = []
461
+
462
+ size_color_words = ['s', 'm', 'l', 'xl', 'xs', 'small', 'medium', 'large',
463
+ 'turuncu', 'siyah', 'beyaz', 'mavi', 'kirmizi', 'yesil',
464
+ 'orange', 'black', 'white', 'blue', 'red', 'green']
465
+
466
+ variant_words = [word for word in search_words if word in size_color_words]
467
+ product_words = [word for word in search_words if word not in size_color_words]
468
+
469
+ is_size_color_query = len(variant_words) > 0 and len(search_words) <= 4
470
+
471
+ if is_size_color_query:
472
+ for product in root.findall('Product'):
473
+ product_name_elem = product.find('ProductName')
474
+ variant_elem = product.find('ProductVariant')
475
+
476
+ if product_name_elem is not None and product_name_elem.text:
477
+ xml_product_name = product_name_elem.text.strip()
478
+ normalized_product_name = normalize_turkish(xml_product_name.lower())
479
+
480
+ product_name_matches = True
481
+ if product_words:
482
+ product_name_matches = all(word in normalized_product_name for word in product_words)
483
+
484
+ if product_name_matches:
485
+ if variant_elem is not None and variant_elem.text:
486
+ variant_text = normalize_turkish(variant_elem.text.lower().replace('-', ' '))
487
+
488
+ if all(word in variant_text for word in variant_words):
489
+ variant_matches.append((product, xml_product_name, variant_text))
490
+
491
+ if variant_matches:
492
+ candidates = variant_matches
493
+ else:
494
+ is_size_color_query = False
495
+
496
+ if not is_size_color_query or not candidates:
497
+ for product in root.findall('Product'):
498
+ product_name_elem = product.find('ProductName')
499
+ if product_name_elem is not None and product_name_elem.text:
500
+ xml_product_name = product_name_elem.text.strip()
501
+ normalized_xml = normalize_turkish(xml_product_name.lower())
502
+ normalized_xml = normalized_xml.replace('(2026)', '').replace('(2025)', '').replace(' gen 3', '').replace(' gen', '').strip()
503
+ xml_words = normalized_xml.split()
504
+
505
+ if len(search_words) >= 2 and len(xml_words) >= 2:
506
+ search_key = f"{search_words[0]} {search_words[1]}"
507
+ xml_key = f"{xml_words[0]} {xml_words[1]}"
508
+
509
+ if search_key == xml_key:
510
+ exact_matches.append((product, xml_product_name, normalized_xml))
511
+
512
+ if not candidates:
513
+ candidates = exact_matches if exact_matches else []
514
+
515
+ if not candidates:
516
+ for product in root.findall('Product'):
517
+ product_name_elem = product.find('ProductName')
518
+ if product_name_elem is not None and product_name_elem.text:
519
+ xml_product_name = product_name_elem.text.strip()
520
+ normalized_xml = normalize_turkish(xml_product_name.lower())
521
+ normalized_xml = normalized_xml.replace('(2026)', '').replace('(2025)', '').replace(' gen 3', '').replace(' gen', '').strip()
522
+ xml_words = normalized_xml.split()
523
+
524
+ common_words = set(search_words) & set(xml_words)
525
+
526
+ if (len(common_words) >= 2 and
527
+ len(search_words) > 0 and len(xml_words) > 0 and
528
+ search_words[0] == xml_words[0]):
529
+ best_matches.append((product, xml_product_name, normalized_xml, len(common_words)))
530
+
531
+ if best_matches:
532
+ max_common = max(match[3] for match in best_matches)
533
+ candidates = [(match[0], match[1], match[2]) for match in best_matches if match[3] == max_common]
534
+
535
+ warehouse_stock_map = {}
536
+
537
+ for product, xml_name, _ in candidates:
538
+ for warehouse in product.findall('Warehouse'):
539
+ name_elem = warehouse.find('Name')
540
+ stock_elem = warehouse.find('Stock')
541
+
542
+ if name_elem is not None and stock_elem is not None:
543
+ warehouse_name = name_elem.text if name_elem.text else "Bilinmeyen"
544
+ try:
545
+ stock_count = int(stock_elem.text) if stock_elem.text else 0
546
+ if stock_count > 0:
547
+ if warehouse_name in warehouse_stock_map:
548
+ warehouse_stock_map[warehouse_name] += stock_count
549
+ else:
550
+ warehouse_stock_map[warehouse_name] = stock_count
551
+ except (ValueError, TypeError):
552
+ pass
553
+
554
+ if warehouse_stock_map:
555
+ all_warehouse_info = []
556
+ for warehouse_name, total_stock in warehouse_stock_map.items():
557
+ all_warehouse_info.append(f"{warehouse_name}: Stokta var")
558
+ return all_warehouse_info
559
+ else:
560
+ return ["Hicbir magazada stokta bulunmuyor"]
561
+
562
+ except Exception as e:
563
+ logger.error(f"Magaza stok bilgisi cekme hatasi: {e}")
564
+ return None
565
+
566
+ # ===============================
567
+ # TREK BISIKLET URUNLERINI CEKME
568
+ # ===============================
569
+ try:
570
+ url = 'https://www.trekbisiklet.com.tr/output/8582384479'
571
+ response = requests.get(url, verify=False, timeout=10)
572
+ root = ET.fromstring(response.content)
573
+ all_items = root.findall('item')
574
+
575
+ products = []
576
+
577
+ for item in all_items:
578
+ stock_number = 0
579
+ stock_amount = "stokta degil"
580
+ price = ""
581
+ price_eft = ""
582
+ product_link = ""
583
+ picture_url = ""
584
+ category_tree = ""
585
+ category_label = ""
586
+ stock_code = ""
587
+ root_product_stock_code = ""
588
+ is_option_of_product = "0"
589
+ is_optioned_product = "0"
590
+
591
+ rootlabel = item.find('rootlabel')
592
+ if rootlabel is None or not rootlabel.text:
593
+ continue
594
+
595
+ full_name = rootlabel.text.strip()
596
+ name_words = full_name.lower().split()
597
+ name = name_words[0] if name_words else "unknown"
598
+
599
+ # STOK KONTROLU - SAYISAL KARSILASTIRMA
600
+ stock_element = item.find('stockAmount')
601
+ if stock_element is not None and stock_element.text:
602
+ try:
603
+ stock_number = int(stock_element.text.strip())
604
+ stock_amount = "stokta" if stock_number > 0 else "stokta degil"
605
+ except (ValueError, TypeError):
606
+ stock_number = 0
607
+ stock_amount = "stokta degil"
608
+
609
+ # Urun linki - HER URUN ICIN AL
610
+ link_element = item.find('productLink')
611
+ product_link = link_element.text if link_element is not None and link_element.text else ""
612
+
613
+ # Urun resmi - HER URUN ICIN AL
614
+ picture_element = item.find('picture1Path')
615
+ picture_url = picture_element.text if picture_element is not None and picture_element.text else ""
616
+
617
+ # Kategori bilgileri - HER URUN ICIN AL
618
+ category_tree_element = item.find('categoryTree')
619
+ category_tree = category_tree_element.text if category_tree_element is not None and category_tree_element.text else ""
620
+
621
+ category_label_element = item.find('productCategoryLabel')
622
+ category_label = category_label_element.text if category_label_element is not None and category_label_element.text else ""
623
+
624
+ # Stock Code (SKU) - HER URUN ICIN AL
625
+ stock_code_element = item.find('stockCode')
626
+ stock_code = stock_code_element.text if stock_code_element is not None and stock_code_element.text else ""
627
+
628
+ # Variant/main product iliski alanlari
629
+ root_product_stock_code_element = item.find('rootProductStockCode')
630
+ root_product_stock_code = root_product_stock_code_element.text if root_product_stock_code_element is not None and root_product_stock_code_element.text else ""
631
+
632
+ is_option_of_product_element = item.find('isOptionOfAProduct')
633
+ is_option_of_product = is_option_of_product_element.text if is_option_of_product_element is not None and is_option_of_product_element.text else "0"
634
+
635
+ is_optioned_product_element = item.find('isOptionedProduct')
636
+ is_optioned_product = is_optioned_product_element.text if is_optioned_product_element is not None and is_optioned_product_element.text else "0"
637
+
638
+ # Stokta olan urunler icin fiyat bilgilerini al
639
+ if stock_amount == "stokta":
640
+ # Normal fiyat
641
+ price_element = item.find('priceTaxWithCur')
642
+ price_str = price_element.text if price_element is not None and price_element.text else "0"
643
+
644
+ # Kampanya fiyati
645
+ price_rebate_element = item.find('priceRebateWithTax')
646
+ price_rebate_str = price_rebate_element.text if price_rebate_element is not None and price_rebate_element.text else ""
647
+
648
+ final_price_str = price_str
649
+ if price_rebate_str:
650
+ try:
651
+ normal_price = float(price_str)
652
+ rebate_price = float(price_rebate_str)
653
+ if rebate_price < normal_price:
654
+ final_price_str = price_rebate_str
655
+ except (ValueError, TypeError):
656
+ final_price_str = price_str
657
+
658
+ # EFT fiyati
659
+ price_eft_element = item.find('priceEft')
660
+ price_eft_str = price_eft_element.text if price_eft_element is not None and price_eft_element.text else ""
661
+
662
+ # Fiyat formatting
663
+ try:
664
+ price_float = float(final_price_str)
665
+ if price_float > 200000:
666
+ price = str(round(price_float / 5000) * 5000)
667
+ elif price_float > 30000:
668
+ price = str(round(price_float / 1000) * 1000)
669
+ elif price_float > 10000:
670
+ price = str(round(price_float / 100) * 100)
671
+ else:
672
+ price = str(round(price_float / 10) * 10)
673
+ except (ValueError, TypeError):
674
+ price = final_price_str
675
+
676
+ # EFT fiyat formatting
677
+ if price_eft_str:
678
+ try:
679
+ price_eft_float = float(price_eft_str)
680
+ if price_eft_float > 200000:
681
+ price_eft = str(round(price_eft_float / 5000) * 5000)
682
+ elif price_eft_float > 30000:
683
+ price_eft = str(round(price_eft_float / 1000) * 1000)
684
+ elif price_eft_float > 10000:
685
+ price_eft = str(round(price_eft_float / 100) * 100)
686
+ else:
687
+ price_eft = str(round(price_eft_float / 10) * 10)
688
+ except (ValueError, TypeError):
689
+ price_eft = price_eft_str
690
+ else:
691
+ try:
692
+ price_eft_float = float(price_str)
693
+ price_eft = str(round(price_eft_float * 0.975 / 10) * 10)
694
+ except:
695
+ price_eft = ""
696
+
697
+ # Urun bilgilerini tuple olarak olustur
698
+ item_info = (stock_amount, price, product_link, price_eft, str(stock_number),
699
+ picture_url, category_tree, category_label, stock_code,
700
+ root_product_stock_code, is_option_of_product, is_optioned_product)
701
+ products.append((name, item_info, full_name))
702
+
703
+ logger.info(f"✅ {len(products)} urun yuklendi")
704
+
705
+ except Exception as e:
706
+ logger.error(f"❌ Urun yukleme hatasi: {e}")
707
+ import traceback
708
+ traceback.print_exc()
709
+ products = []
710
+
711
+ # ===============================
712
+ # SISTEM MESAJLARI
713
+ # ===============================
714
+ def get_system_messages():
715
+ """Sistem mesajlarini yukle - Moduler prompts'tan"""
716
+ try:
717
+ return get_active_prompts()
718
+ except:
719
+ # Fallback sistem mesajlari
720
+ return [
721
+ {"role": "system", "content": "Sen Trek bisiklet uzmani AI asistanisin. Trek ve Electra bisikletler konusunda uzmansin. Stokta bulunan urunlerin fiyat bilgilerini verebilirsin."}
722
+ ]
723
+
724
+ # ===============================
725
+ # SOHBET HAFIZASI SISTEMI
726
+ # ===============================
727
+ conversation_memory = {}
728
+
729
+ def get_conversation_context(phone_number):
730
+ """Kullanicinin sohbet gecmisini getir"""
731
+ if phone_number not in conversation_memory:
732
+ conversation_memory[phone_number] = {
733
+ "messages": [],
734
+ "current_category": None,
735
+ "current_product": None,
736
+ "current_product_link": None,
737
+ "current_product_price": None,
738
+ "last_activity": None
739
+ }
740
+ return conversation_memory[phone_number]
741
+
742
+ def add_to_conversation(phone_number, user_message, ai_response):
743
+ """Sohbet gecmisine ekle"""
744
+ context = get_conversation_context(phone_number)
745
+ context["last_activity"] = datetime.datetime.now()
746
+
747
+ context["messages"].append({
748
+ "user": user_message,
749
+ "ai": ai_response,
750
+ "timestamp": datetime.datetime.now()
751
+ })
752
+
753
+ # Sadece son 10 mesaji tut
754
+ if len(context["messages"]) > 10:
755
+ context["messages"] = context["messages"][-10:]
756
+
757
+ detect_category(phone_number, user_message, ai_response)
758
+
759
+ def detect_category(phone_number, user_message, ai_response):
760
+ """Konusulan kategoriyi ve tam urun adini tespit et"""
761
+ context = get_conversation_context(phone_number)
762
+
763
+ categories = {
764
+ "marlin": ["marlin", "marlin+"],
765
+ "madone": ["madone"],
766
+ "emonda": ["emonda", "émonda"],
767
+ "domane": ["domane"],
768
+ "checkpoint": ["checkpoint"],
769
+ "fuel": ["fuel", "fuel ex", "fuel exe"],
770
+ "procaliber": ["procaliber"],
771
+ "supercaliber": ["supercaliber"],
772
+ "fx": ["fx"],
773
+ "ds": ["ds", "dual sport"],
774
+ "powerfly": ["powerfly"],
775
+ "rail": ["rail"],
776
+ "verve": ["verve"],
777
+ "townie": ["townie"]
778
+ }
779
+
780
+ user_lower = user_message.lower()
781
+ for category, keywords in categories.items():
782
+ for keyword in keywords:
783
+ if keyword in user_lower:
784
+ context["current_category"] = category
785
+
786
+ # TAM URUN ADINI CIKAR - ornegin "Marlin 5", "Madone SLR 9"
787
+ # Model numarasini da yakala
788
+ import re
789
+ # Keyword + sayi pattern'i ara (ornegin "marlin 5", "madone slr 9")
790
+ pattern = rf'{keyword}\s*\+?\s*(?:slr\s*)?(\d+)?'
791
+ match = re.search(pattern, user_lower)
792
+ if match:
793
+ model_num = match.group(1)
794
+ if model_num:
795
+ # Tam urun adi: "marlin 5" veya "madone slr 9"
796
+ full_product = match.group(0).strip()
797
+ context["current_product"] = full_product
798
+ else:
799
+ # Sadece kategori adi
800
+ context["current_product"] = keyword
801
+ else:
802
+ context["current_product"] = keyword
803
+
804
+ return category
805
+
806
+ return context.get("current_category")
807
+
808
+ def build_context_messages(phone_number, current_message):
809
+ """Sohbet gecmisi ile sistem mesajlarini olustur"""
810
+ context = get_conversation_context(phone_number)
811
+ system_messages = get_system_messages()
812
+
813
+ # Mevcut kategori varsa, sistem mesajina ekle - GUCLENDIRILMIS BAGLAIM
814
+ if context.get("current_category"):
815
+ cat = context['current_category'].upper()
816
+ category_msg = f"""KRITIK BAGLAIM BILGISI:
817
+ Musteri su anda {cat} modelleri hakkinda konusuyor.
818
+ Butun sorulari bu baglamda cevapla.
819
+ "Hangi model var", "stok var mi", "fiyat ne" gibi sorular {cat} icin sorulmus demektir.
820
+ DS, FX, Verve gibi BASKA kategorilerden bahsetme - sadece {cat} hakkinda konusuyoruz!"""
821
+ system_messages.append({"role": "system", "content": category_msg})
822
+
823
+ # Son konusulan urun bilgilerini ekle (link, fiyat sorulari icin)
824
+ if context.get("current_product"):
825
+ product_context = f"Son konusulan urun: {context['current_product']}"
826
+ if context.get("current_product_link"):
827
+ product_context += f"\nUrun linki: {context['current_product_link']}"
828
+ if context.get("current_product_price"):
829
+ product_context += f"\nUrun fiyati: {context['current_product_price']}"
830
+ system_messages.append({"role": "system", "content": product_context})
831
+
832
+ # Son 3 mesaj alisverisini ekle
833
+ recent_messages = context["messages"][-3:] if context["messages"] else []
834
+
835
+ all_messages = system_messages.copy()
836
+
837
+ # Gecmis mesajlari ekle
838
+ for msg in recent_messages:
839
+ all_messages.append({"role": "user", "content": msg["user"]})
840
+ all_messages.append({"role": "assistant", "content": msg["ai"]})
841
+
842
+ # Mevcut mesaji ekle
843
+ all_messages.append({"role": "user", "content": current_message})
844
+
845
+ return all_messages
846
+
847
+ # ===============================
848
+ # HYBRID MODEL MESAJ ISLEME
849
+ # ===============================
850
+
851
+
852
+ def extract_product_from_vision_response(response):
853
+ """
854
+ GPT Vision yanitindan urun adini cikarir
855
+ Ornek: "Trek Domane+ SLR 7 AXS" -> "Domane+ SLR 7 AXS"
856
+ """
857
+ import re
858
+
859
+ # Bilinen Trek model pattern'leri
860
+ patterns = [
861
+ # Trek Domane+ SLR 7 AXS gibi tam isimler
862
+ r'Trek\s+((?:Domane|Madone|Emonda|Marlin|Fuel|Rail|Powerfly|Checkpoint|FX|Verve|Dual\s*Sport|Procaliber|Supercaliber|Roscoe|Top\s*Fuel|Slash|Remedy|X-?Caliber|Allant)[+]?\s*(?:SLR|SL|Gen|EX|EXe)?\s*\d*\s*(?:AXS|Di2|eTap|Frameset)?)',
863
+ # Sadece model adi + numara
864
+ r'((?:Domane|Madone|Emonda|Marlin|Fuel|Rail|Powerfly|Checkpoint|FX|Verve|Dual\s*Sport|Procaliber|Supercaliber|Roscoe|Top\s*Fuel|Slash|Remedy|X-?Caliber|Allant)[+]?\s*(?:SLR|SL|Gen|EX|EXe)?\s*\d+\s*(?:AXS|Di2|eTap|Frameset)?)',
865
+ # Model + SLR/SL + numara
866
+ r'((?:Domane|Madone|Emonda)[+]?\s+(?:SLR|SL)\s*\d+)',
867
+ # Marlin + numara
868
+ r'(Marlin\s*\d+)',
869
+ # FX + numara
870
+ r'(FX\s*(?:Sport)?\s*\d+)',
871
+ ]
872
+
873
+ response_lower = response.lower()
874
+
875
+ for pattern in patterns:
876
+ match = re.search(pattern, response, re.IGNORECASE)
877
+ if match:
878
+ product = match.group(1).strip()
879
+ # "Trek " prefix varsa kaldir
880
+ product = re.sub(r'^Trek\s+', '', product, flags=re.IGNORECASE)
881
+ return product
882
+
883
+ return None
884
+
885
+
886
+ def process_whatsapp_message_with_media(user_message, phone_number, media_urls, media_types):
887
+ """
888
+ GORSEL MESAJ ISLEME - GPT-4o Vision kullanir
889
+ """
890
+ try:
891
+ logger.info(f"🖼️ Gorsel analizi basliyor: {len(media_urls)} medya")
892
+ logger.info(f"📎 Medya URL'leri: {media_urls}")
893
+ logger.info(f"📎 Medya tipleri: {media_types}")
894
+
895
+ # Pasif profil analizi
896
+ try:
897
+ profile_analysis = analyze_user_message(phone_number, user_message)
898
+ logger.info(f"📊 Profil analizi: {phone_number} -> {profile_analysis}")
899
+ except:
900
+ pass
901
+
902
+ # Gorsel icin GPT-4o kullan
903
+ model = get_model_for_request(has_media=True)
904
+
905
+ # Sohbet gecmisi ile sistem mesajlarini olustur
906
+ messages = build_context_messages(phone_number, user_message if user_message else "Gonderilen gorseli analiz et")
907
+
908
+ # GPT-4o Vision icin mesaj hazirla
909
+ vision_message = {
910
+ "role": "user",
911
+ "content": []
912
+ }
913
+
914
+ # Metin mesaji varsa ekle
915
+ # KRITIK: Kisa sorularda (var mi, fiyat, stok) gorseli dikkate alarak cevap vermesini sagla
916
+ if user_message and user_message.strip():
917
+ short_questions = ['var mi', 'var mı', 'stok', 'fiyat', 'kac', 'kaç', 'ne kadar', 'beden', 'renk']
918
+ is_short_question = len(user_message.strip()) < 20 and any(q in user_message.lower() for q in short_questions)
919
+
920
+ if is_short_question:
921
+ # Kisa soru - gorseli referans alarak cevap vermesini iste
922
+ enhanced_text = f"Gorseldeki BISIKLETI dikkatlice incele ve model adini tespit et. Musteri bu bisiklet icin '{user_message}' soruyor. Gorseldeki bisikletin TAM MODEL ADINI (ornegin 'Trek Domane+ SLR 7 AXS') belirle ve buna gore cevap ver."
923
+ vision_message["content"].append({
924
+ "type": "text",
925
+ "text": enhanced_text
926
+ })
927
+ else:
928
+ vision_message["content"].append({
929
+ "type": "text",
930
+ "text": user_message
931
+ })
932
+ else:
933
+ vision_message["content"].append({
934
+ "type": "text",
935
+ "text": "Bu gorselde ne var? Eger bisiklet veya bisiklet parcasi ise detayli acikla."
936
+ })
937
+ # Medya URL'lerini isle
938
+ valid_images = 0
939
+ for i, media_url in enumerate(media_urls):
940
+ media_type = media_types[i] if i < len(media_types) else "image/jpeg"
941
+
942
+ # Sadece gorsel medyalari isle
943
+ if media_type and media_type.startswith('image/'):
944
+ try:
945
+ # Twilio medya URL'sini proxy uzerinden cevir
946
+ if 'api.twilio.com' in media_url:
947
+ import re
948
+ match = re.search(r'/Messages/([^/]+)/Media/([^/]+)', media_url)
949
+ if match:
950
+ message_sid = match.group(1)
951
+ media_sid = match.group(2)
952
+ proxy_url = f"https://video.trek-turkey.com/twilio-media-proxy.php?action=media&message={message_sid}&media={media_sid}"
953
+ logger.info(f"🔄 Proxy URL olusturuldu: {proxy_url}")
954
+
955
+ # Proxy URL'sinin calisip calismadigini kontrol et
956
+ try:
957
+ test_response = requests.head(proxy_url, timeout=5, verify=False)
958
+ if test_response.status_code == 200:
959
+ vision_message["content"].append({
960
+ "type": "image_url",
961
+ "image_url": {"url": proxy_url}
962
+ })
963
+ valid_images += 1
964
+ logger.info(f"✅ Proxy URL gecerli: {proxy_url}")
965
+ else:
966
+ logger.error(f"❌ Proxy URL calismiyor: {test_response.status_code}")
967
+ vision_message["content"].append({
968
+ "type": "image_url",
969
+ "image_url": {"url": media_url}
970
+ })
971
+ valid_images += 1
972
+ except Exception as proxy_error:
973
+ logger.error(f"❌ Proxy test hatasi: {proxy_error}")
974
+ vision_message["content"].append({
975
+ "type": "image_url",
976
+ "image_url": {"url": media_url}
977
+ })
978
+ valid_images += 1
979
+ else:
980
+ logger.error(f"❌ Twilio URL parse edilemedi: {media_url}")
981
+ else:
982
+ vision_message["content"].append({
983
+ "type": "image_url",
984
+ "image_url": {"url": media_url}
985
+ })
986
+ valid_images += 1
987
+ logger.info(f"✅ Dogrudan URL eklendi: {media_url}")
988
+ except Exception as url_error:
989
+ logger.error(f"❌ URL isleme hatasi: {url_error}")
990
+ else:
991
+ logger.warning(f"⚠️ Gorsel olmayan medya atlandi: {media_type}")
992
+
993
+ # Hic gecerli gorsel yoksa
994
+ if valid_images == 0:
995
+ logger.error("❌ Hic gecerli gorsel bulunamadi")
996
+ return "Gonderdiginiz gorsel islenemedi. Lutfen farkli bir gorsel gonderin veya sorunuzu yazili olarak iletin."
997
+
998
+ logger.info(f"✅ {valid_images} gorsel islenecek")
999
+
1000
+ # Son user mesajini vision mesajiyla degistir
1001
+ messages = [msg for msg in messages if not (msg.get("role") == "user" and msg == messages[-1])]
1002
+ messages.append(vision_message)
1003
+
1004
+ # Sistem mesajina bisiklet tanima talimati ekle
1005
+ bike_recognition_prompt = {
1006
+ "role": "system",
1007
+ "content": """Gonderilen gorselleri dikkatle analiz et:
1008
+ 1. Eger bisiklet veya bisiklet parcasi goruyorsan, detaylica tanimla (marka, model, renk, beden, ozellikler)
1009
+ 2. Trek bisiklet ise modeli tahmin etmeye calis
1010
+ 3. Stok veya fiyat sorulursa, gorseldeki bisikletin ozelliklerini belirterek bilgi ver
1011
+ 4. Gorsel net degilse veya tanimlanamiyorsa, kullanicidan daha net bir gorsel istemek yerine, gorselde gorduklerini acikla
1012
+ 5. Eger gorsel bisikletle ilgili degilse, ne gordugunu kisaca acikla"""
1013
+ }
1014
+ messages.insert(0, bike_recognition_prompt)
1015
+
1016
+ if not OPENAI_API_KEY:
1017
+ logger.error("❌ OpenAI API anahtari eksik")
1018
+ return "Sistem hatasi olustu. Lutfen daha sonra tekrar deneyin."
1019
+
1020
+ logger.info(f"📤 GPT Vision API'ye gonderiliyor: {len(messages)} mesaj, {valid_images} gorsel")
1021
+
1022
+ payload = {
1023
+ "model": model, # GPT-4o
1024
+ "messages": messages,
1025
+ "max_tokens": 800,
1026
+ "temperature": 0.3
1027
+ }
1028
+
1029
+ headers = {
1030
+ "Content-Type": "application/json",
1031
+ "Authorization": f"Bearer {OPENAI_API_KEY}"
1032
+ }
1033
+
1034
+ response = requests.post(API_URL, headers=headers, json=payload, timeout=30)
1035
+
1036
+ logger.info(f"📥 API yaniti: {response.status_code}")
1037
+
1038
+ if response.status_code == 200:
1039
+ result = response.json()
1040
+ ai_response = result['choices'][0]['message']['content']
1041
+ logger.info(f"✅ Gorsel analizi basarili: {ai_response[:100]}...")
1042
+
1043
+ # KRITIK: Gorselden urun adini cikar ve GERCEK stok kontrolu yap
1044
+ try:
1045
+ # app.py'deki get_warehouse_stock fonksiyonunu kullan
1046
+
1047
+ # GPT yanitindan urun adini cikar
1048
+ product_name = extract_product_from_vision_response(ai_response)
1049
+
1050
+ if product_name:
1051
+ logger.info(f"🔍 Gorselden tespit edilen urun: {product_name}")
1052
+
1053
+ # GERCEK stok kontrolu yap
1054
+ stock_result = get_warehouse_stock(product_name)
1055
+
1056
+ # Sonuc list veya string olabilir
1057
+ stock_info = None
1058
+ if stock_result:
1059
+ if isinstance(stock_result, list):
1060
+ stock_info = chr(10).join(str(item) for item in stock_result)
1061
+ else:
1062
+ stock_info = str(stock_result)
1063
+
1064
+ if stock_info and len(stock_info) > 5:
1065
+ logger.info(f"✅ Stok bilgisi bulundu: {stock_info[:100]}...")
1066
+
1067
+ # GPT yanlis "stokta yok" dediyse duzelt
1068
+ if "stokta bulunmuyor" in ai_response.lower() or "stokta yok" in ai_response.lower():
1069
+ if "stokta bulunmuyor" not in stock_info.lower():
1070
+ # Yanlis bilgiyi kaldir ve dogru stok bilgisini ekle
1071
+ ai_response = re.sub(
1072
+ r'[^.]*stok[^.]*bulunmuyor[^.]*[.]?',
1073
+ '',
1074
+ ai_response,
1075
+ flags=re.IGNORECASE
1076
+ )
1077
+ ai_response = ai_response.strip()
1078
+
1079
+ # Stok bilgisini ekle (eger zaten yoksa)
1080
+ if "Stok:" not in ai_response and "stokta" not in stock_info.lower():
1081
+ ai_response = ai_response + "\n\n" + stock_info
1082
+
1083
+ elif "Stok:" not in ai_response:
1084
+ ai_response = ai_response + "\n\n" + stock_info
1085
+
1086
+ else:
1087
+ logger.info(f"⚠️ Urun icin stok bilgisi bulunamadi: {product_name}")
1088
+ else:
1089
+ logger.info("⚠️ Gorselden urun adi cikarilamadi")
1090
+ except Exception as stock_error:
1091
+ logger.error(f"❌ Vision stok kontrolu hatasi: {stock_error}")
1092
+
1093
+ # Context'e urun adini kaydet (sonraki sorular icin)
1094
+ if product_name:
1095
+ try:
1096
+ ctx = get_conversation_context(phone_number)
1097
+ ctx["current_product"] = product_name
1098
+ ctx["current_category"] = product_name.split()[0].lower()
1099
+ logger.info(f"📝 Vision context kaydedildi: {product_name}")
1100
+ except:
1101
+ pass
1102
+
1103
+ # WhatsApp icin formatla
1104
+ try:
1105
+ formatted_response = extract_product_info_whatsapp(ai_response)
1106
+ except:
1107
+ formatted_response = ai_response
1108
+
1109
+ # Sohbet gecmisine ekle
1110
+ add_to_conversation(phone_number, f"[Gorsel gonderildi] {user_message if user_message else ''}", formatted_response)
1111
+
1112
+ return formatted_response
1113
+ else:
1114
+ error_detail = response.text[:500] if response.text else "Detay yok"
1115
+ logger.error(f"❌ OpenAI API Error: {response.status_code} - {error_detail}")
1116
+
1117
+ if response.status_code == 400:
1118
+ return "Gorsel formati desteklenmiyor. Lutfen JPG veya PNG formatinda bir gorsel gonderin."
1119
+ elif response.status_code == 413:
1120
+ return "Gorsel boyutu cok buyuk. Lutfen daha kucuk bir gorsel gonderin."
1121
+ elif response.status_code == 429:
1122
+ return "Sistem su anda yogun. Lutfen birkac saniye sonra tekrar deneyin."
1123
+ else:
1124
+ return "Gorsel su anda analiz edilemiyor. Sorunuzu yazili olarak iletebilir misiniz?"
1125
+
1126
+ except requests.exceptions.Timeout:
1127
+ logger.error("❌ API timeout hatasi")
1128
+ return "Islem zaman asimina ugradi. Lutfen tekrar deneyin."
1129
+ except Exception as e:
1130
+ logger.error(f"❌ Medya isleme hatasi: {e}")
1131
+ import traceback
1132
+ traceback.print_exc()
1133
+ return "Gorsel islenirken bir sorun olustu. Lutfen sorunuzu yazili olarak iletin veya farkli bir gorsel deneyin."
1134
+
1135
+
1136
+ def process_whatsapp_message_with_memory(user_message, phone_number):
1137
+ """
1138
+ METIN MESAJ ISLEME - GPT-5.2 kullanir (daha akilli)
1139
+ """
1140
+ try:
1141
+ # Metin icin GPT-5.2 kullan
1142
+ model = get_model_for_request(has_media=False)
1143
+
1144
+ # Pasif profil analizi
1145
+ try:
1146
+ profile_analysis = analyze_user_message(phone_number, user_message)
1147
+ logger.info(f"📊 Profil analizi: {phone_number}")
1148
+ except:
1149
+ pass
1150
+
1151
+ # 🔔 Yeni Magaza Bildirim Sistemi - Mehmet Bey'e otomatik bildirim
1152
+ if USE_STORE_NOTIFICATION:
1153
+ try:
1154
+ should_notify_mehmet, notification_reason, urgency = should_notify_mehmet_bey(user_message)
1155
+
1156
+ if not should_notify_mehmet and USE_INTENT_ANALYZER:
1157
+ context = get_conversation_context(phone_number)
1158
+ intent_analysis = analyze_customer_intent(user_message, context)
1159
+ should_notify_mehmet, notification_reason, urgency = should_notify_mehmet_bey(user_message, intent_analysis)
1160
+ else:
1161
+ intent_analysis = None
1162
+
1163
+ if should_notify_mehmet:
1164
+ if intent_analysis:
1165
+ product = intent_analysis.get("product") or "Belirtilmemis"
1166
+ else:
1167
+ context = get_conversation_context(phone_number)
1168
+ product = context.get("current_category") or "Urun belirtilmemis"
1169
+
1170
+ if "rezervasyon" in notification_reason.lower() or urgency == "high":
1171
+ action = "reserve"
1172
+ elif "magaza" in notification_reason.lower() or "lokasyon" in notification_reason.lower():
1173
+ action = "info"
1174
+ elif "fiyat" in notification_reason.lower() or "odeme" in notification_reason.lower():
1175
+ action = "price"
1176
+ else:
1177
+ action = "info"
1178
+
1179
+ additional_info = f"{notification_reason}\n\nMusteri Mesaji: '{user_message}'"
1180
+ if urgency == "high":
1181
+ additional_info = "⚠️ YUKSEK ONCELIK ⚠️\n" + additional_info
1182
+
1183
+ result = send_store_notification(
1184
+ customer_phone=phone_number,
1185
+ customer_name=None,
1186
+ product_name=product,
1187
+ action=action,
1188
+ store_name=None,
1189
+ additional_info=additional_info
1190
+ )
1191
+
1192
+ if result:
1193
+ logger.info(f"✅ Mehmet Bey'e bildirim gonderildi!")
1194
+ logger.info(f" 📍 Sebep: {notification_reason}")
1195
+ logger.info(f" ⚡ Oncelik: {urgency}")
1196
+ logger.info(f" 📦 Urun: {product}")
1197
+
1198
+ # TAKIP SISTEMINI KONTROL ET
1199
+ if USE_FOLLOW_UP and follow_up_manager:
1200
+ try:
1201
+ follow_up_analysis = analyze_message_for_follow_up(user_message)
1202
+ if follow_up_analysis and follow_up_analysis.get("needs_follow_up"):
1203
+ follow_up = follow_up_manager.create_follow_up(
1204
+ customer_phone=phone_number,
1205
+ product_name=product,
1206
+ follow_up_type=follow_up_analysis["follow_up_type"],
1207
+ original_message=user_message,
1208
+ follow_up_hours=follow_up_analysis.get("follow_up_hours", 24),
1209
+ notes=follow_up_analysis.get("reason", "")
1210
+ )
1211
+ logger.info(f"📌 Takip olusturuldu: {follow_up_analysis.get('reason', '')}")
1212
+ except Exception as follow_up_error:
1213
+ logger.error(f"Takip sistemi hatasi: {follow_up_error}")
1214
+ except Exception as notify_error:
1215
+ logger.error(f"Bildirim hatasi: {notify_error}")
1216
+
1217
+ # Sohbet gecmisi ile mesajlari olustur
1218
+ messages = build_context_messages(phone_number, user_message)
1219
+
1220
+ # Intent Analyzer veya context'ten urun bilgisini al
1221
+ detected_product = None
1222
+ intent_analysis = None # Scope disinda da kullanilacak
1223
+ if USE_INTENT_ANALYZER:
1224
+ try:
1225
+ context = get_conversation_context(phone_number)
1226
+ intent_analysis = analyze_customer_intent(user_message, context)
1227
+ if intent_analysis and intent_analysis.get("product"):
1228
+ detected_product = intent_analysis.get("product")
1229
+ logger.info(f"🎯 Intent'ten tespit edilen urun: {detected_product}")
1230
+ except Exception as e:
1231
+ logger.error(f"Intent analiz hatasi: {e}")
1232
+ intent_analysis = None
1233
+
1234
+ # Eger Intent'ten urun bulunamadiysa, context'ten al
1235
+ # ONEMLI: current_product TAM URUN ADINI icerir (ornegin "marlin 5")
1236
+ # current_category ise sadece kategori adidir (ornegin "marlin")
1237
+ if not detected_product:
1238
+ context = get_conversation_context(phone_number)
1239
+ # Oncelikle tam urun adini dene
1240
+ if context.get("current_product"):
1241
+ detected_product = context.get("current_product")
1242
+ logger.info(f"🎯 Context'ten tespit edilen TAM URUN: {detected_product}")
1243
+ elif context.get("current_category"):
1244
+ detected_product = context.get("current_category")
1245
+ logger.info(f"🎯 Context'ten tespit edilen kategori: {detected_product}")
1246
+
1247
+ # Stok sorgusu icin kullanilacak urun adi
1248
+ stock_query_product = detected_product if detected_product else user_message
1249
+
1250
+ # Urun bilgilerini ekle
1251
+ input_words = user_message.lower().split()
1252
+ # Detected product'i da input_words'e ekle
1253
+ if detected_product:
1254
+ input_words.extend(detected_product.lower().split())
1255
+
1256
+ # Bulunan urunun link ve gorsel bilgilerini sakla
1257
+ found_product_link = None
1258
+ found_product_image = None
1259
+ found_product_name = None
1260
+ best_match_score = 0
1261
+
1262
+ # Kullanici mesaji veya detected_product'i normalize et
1263
+ search_text = (detected_product or user_message).lower()
1264
+ search_words = search_text.split()
1265
+
1266
+ # AKILLI URUN ESLESTIRME
1267
+ # 1. Kategori bazli filtreleme - Bisiklet aramasinda aksesuar gosterme
1268
+ # 2. Urun tipi benzerligi - Ayni tip urunler oncelikli
1269
+ # 3. Kelime eslesmesi - En cok eslesen urun
1270
+
1271
+ def is_bicycle_search(text):
1272
+ """Arama bisiklet mi yoksa aksesuar mi?"""
1273
+ bike_indicators = ['madone', 'domane', 'emonda', 'checkpoint', 'fuel', 'slash',
1274
+ 'marlin', 'procaliber', 'supercaliber', 'fx', 'verve', 'dual sport',
1275
+ 'powerfly', 'rail', 'allant', 'bisiklet', 'bike']
1276
+ text_lower = text.lower()
1277
+ return any(ind in text_lower for ind in bike_indicators)
1278
+
1279
+ def get_product_type(category_tree, product_name):
1280
+ """Urun tipini belirle"""
1281
+ cat_lower = (category_tree or "").lower()
1282
+ name_lower = (product_name or "").lower()
1283
+
1284
+ # Kategori agacindan tip belirle
1285
+ if 'bisiklet' in cat_lower or 'bike' in cat_lower:
1286
+ if 'yol' in cat_lower or 'road' in cat_lower:
1287
+ return 'road_bike'
1288
+ elif 'dag' in cat_lower or 'mountain' in cat_lower or 'mtb' in cat_lower:
1289
+ return 'mtb'
1290
+ elif 'elektrik' in cat_lower or 'e-bike' in cat_lower:
1291
+ return 'ebike'
1292
+ elif 'sehir' in cat_lower or 'hybrid' in cat_lower:
1293
+ return 'hybrid'
1294
+ return 'bicycle'
1295
+ elif 'aksesuar' in cat_lower or 'parça' in cat_lower or 'parca' in cat_lower or 'accessory' in cat_lower or 'yedek' in cat_lower:
1296
+ return 'accessory'
1297
+
1298
+ # Isimden tip belirle (fallback)
1299
+ # ONEMLI: Aksesuar kontrolu ONCE yapilmali cunku "Domane Kadro Kulagi" gibi
1300
+ # urunlerde model adi gecebilir ama aksesuar kelimeleri varsa aksesuar'dir
1301
+ accessory_keywords = ['sele', 'gidon', 'pedal', 'zincir', 'lastik', 'jant', 'fren', 'vites',
1302
+ 'kadro kulağı', 'kadro kulagi', 'kulak', 'kablo', 'kasnak', 'dişli',
1303
+ 'zil', 'far', 'lamba', 'pompa', 'kilit', 'çanta', 'canta', 'suluk',
1304
+ 'gözlük', 'gozluk', 'kask', 'eldiven', 'ayakkabı', 'ayakkabi',
1305
+ 'forma', 'tayt', 'şort', 'sort', 'mont', 'yağmurluk', 'yagmurluk']
1306
+ if any(x in name_lower for x in accessory_keywords):
1307
+ return 'accessory'
1308
+ if any(x in name_lower for x in ['madone', 'domane', 'emonda', 'checkpoint', 'fuel', 'marlin', 'fx']):
1309
+ return 'bicycle'
1310
+
1311
+ return 'unknown'
1312
+
1313
+ def calculate_smart_match_score(search_words, product_name, product_category, is_bike_search):
1314
+ """Akilli eslesme skoru hesapla"""
1315
+ product_name_lower = product_name.lower()
1316
+ product_words = product_name_lower.split()
1317
+ product_type = get_product_type(product_category, product_name)
1318
+
1319
+ # Temel kelime eslesmesi
1320
+ base_score = sum(1 for word in search_words if word in product_name_lower)
1321
+
1322
+ # Kategori uyumu bonusu/cezasi
1323
+ if is_bike_search:
1324
+ if product_type == 'bicycle':
1325
+ base_score += 2 # Bisiklet aramasinda bisiklet bulunca bonus
1326
+ elif product_type == 'accessory':
1327
+ base_score -= 100 # Bisiklet aramasinda aksesuar KESINLIKLE gosterilmemeli
1328
+
1329
+ # KRITIK VARYANT KONTROLU
1330
+ # AXS, Di2, eTap gibi varyantlar FARKLI URUNLERDIR
1331
+ # Kullanici "Madone SLR 9" diyorsa "Madone SLR 9 AXS" GOSTERILMEMELI
1332
+ critical_variants = ['axs', 'etap', 'di2', 'frameset']
1333
+
1334
+ # Urunde olan kritik varyantlar
1335
+ product_critical_variants = [v for v in critical_variants if v in product_name_lower]
1336
+ # Kullanicinin soyledigi kritik varyantlar
1337
+ user_critical_variants = [v for v in critical_variants if v in ' '.join(search_words)]
1338
+
1339
+ # Urunde kritik varyant var AMA kullanici soylemedi -> BUYUK CEZA
1340
+ for variant in product_critical_variants:
1341
+ if variant not in user_critical_variants:
1342
+ base_score -= 50 # Bu urun gosterilmemeli
1343
+
1344
+ # Kullanici kritik varyant soyledi AMA urunde yok -> CEZA
1345
+ for variant in user_critical_variants:
1346
+ if variant not in product_critical_variants:
1347
+ base_score -= 50 # Bu urun gosterilmemeli
1348
+
1349
+ # Tam esleme bonusu - Tum arama kelimeleri urunde varsa
1350
+ all_words_match = all(word in product_name_lower for word in search_words if len(word) > 2)
1351
+ if all_words_match and len(search_words) > 1:
1352
+ base_score += 3
1353
+
1354
+ # Model numarasi eslesmesi - "9" araniyorsa "9" olmali, "7" olmamali
1355
+ for word in search_words:
1356
+ if word.isdigit():
1357
+ if word in product_words:
1358
+ base_score += 2 # Tam numara eslesmesi
1359
+ else:
1360
+ # Farkli numara varsa ceza
1361
+ for pword in product_words:
1362
+ if pword.isdigit() and pword != word:
1363
+ base_score -= 20 # Farkli model numarasi
1364
+
1365
+ return base_score
1366
+
1367
+ # Arama bisiklet mi?
1368
+ is_bike_search = is_bicycle_search(search_text)
1369
+
1370
+ for product_info in products:
1371
+ product_full_name = product_info[2] # Tam urun adi
1372
+ product_category = product_info[1][6] if len(product_info[1]) > 6 else "" # category_tree
1373
+
1374
+ # Akilli skor hesapla
1375
+ match_score = calculate_smart_match_score(
1376
+ search_words,
1377
+ product_full_name,
1378
+ product_category,
1379
+ is_bike_search
1380
+ )
1381
+
1382
+ # Daha iyi eslesme bulduysa guncelle
1383
+ if match_score > best_match_score and product_info[1][0] == "stokta":
1384
+ best_match_score = match_score
1385
+
1386
+ normal_price = f"Fiyat: {product_info[1][1]} TL"
1387
+ if product_info[1][3]:
1388
+ eft_price = f"Havale: {product_info[1][3]} TL"
1389
+ price_info = f"{normal_price}, {eft_price}"
1390
+ else:
1391
+ price_info = normal_price
1392
+
1393
+ # Urun linki ve gorseli al - EN IYI ESLESEN URUN
1394
+ if product_info[1][2]: # product_link
1395
+ found_product_link = product_info[1][2]
1396
+ if product_info[1][5]: # picture_url
1397
+ found_product_image = product_info[1][5]
1398
+ found_product_name = product_info[2]
1399
+
1400
+ # System message'a LINK EKLEME - cunku GPT linki yanitina dahil ediyor
1401
+ # ve sonra biz de ekledigimizde 2 kere gorunuyor
1402
+ new_msg = f"{product_info[2]} {product_info[1][0]} - {price_info}"
1403
+ messages.append({"role": "system", "content": new_msg})
1404
+
1405
+ # Eger match bulunamadiysa, basit eslesme dene
1406
+ if best_match_score == 0:
1407
+ for product_info in products:
1408
+ if product_info[0] in input_words and product_info[1][0] == "stokta":
1409
+ normal_price = f"Fiyat: {product_info[1][1]} TL"
1410
+ if product_info[1][3]:
1411
+ eft_price = f"Havale: {product_info[1][3]} TL"
1412
+ price_info = f"{normal_price}, {eft_price}"
1413
+ else:
1414
+ price_info = normal_price
1415
+
1416
+ if product_info[1][2]:
1417
+ found_product_link = product_info[1][2]
1418
+ if product_info[1][5]:
1419
+ found_product_image = product_info[1][5]
1420
+ found_product_name = product_info[2]
1421
+
1422
+ new_msg = f"{product_info[2]} {product_info[1][0]} - {price_info}"
1423
+ messages.append({"role": "system", "content": new_msg})
1424
+ break # Ilk eslesen yeterli
1425
+
1426
+ # Stok bilgisi ekle - detected_product kullan
1427
+ # ONCELIK: XML sonucu (smart_warehouse_with_price.py) daha guvenilir
1428
+ warehouse_info = get_warehouse_stock(stock_query_product)
1429
+ xml_has_valid_stock = False
1430
+
1431
+ if warehouse_info:
1432
+ stock_msg = "Magaza Stok Durumu:\n" + "\n".join(warehouse_info) if isinstance(warehouse_info, list) else str(warehouse_info)
1433
+ messages.append({"role": "system", "content": stock_msg})
1434
+
1435
+ # XML sonucunda gercek stok bilgisi var mi kontrol et
1436
+ # "mevcut degil", "bulunamadi", "tukendi" gibi ifadeler yoksa stok var demektir
1437
+ stock_msg_lower = stock_msg.lower()
1438
+ negative_indicators = ["mevcut değil", "mevcut degil", "bulunamadi", "bulunmuyor", "tükendi", "tukendi", "stokta yok"]
1439
+ xml_has_valid_stock = not any(indicator in stock_msg_lower for indicator in negative_indicators)
1440
+
1441
+ if xml_has_valid_stock:
1442
+ logger.info(f"✅ XML'den stok bilgisi bulundu: {stock_query_product}")
1443
+
1444
+ # Link ve fiyat bilgisini context'e kaydet (follow-up sorular icin)
1445
+ context = get_conversation_context(phone_number)
1446
+ link_match = re.search(r'Link: (https?://[^\s]+)', stock_msg)
1447
+ if link_match:
1448
+ context["current_product_link"] = link_match.group(1)
1449
+ logger.info(f"🔗 Link context'e kaydedildi: {link_match.group(1)}")
1450
+ price_match = re.search(r'Fiyat: ([^\n]+)', stock_msg)
1451
+ if price_match:
1452
+ context["current_product_price"] = price_match.group(1)
1453
+ # Urun adini da kaydet
1454
+ if stock_query_product:
1455
+ context["current_product"] = stock_query_product
1456
+
1457
+ # Gercek zamanli stok sorgusu - SADECE XML sonucu bos veya olumsuzsa calistir
1458
+ # Bu sayede XML'de bulunan urunler icin yanlis "stokta yok" mesaji onlenir
1459
+ should_query_stock = False
1460
+
1461
+ if not xml_has_valid_stock:
1462
+ if intent_analysis:
1463
+ # Intent Analyzer'dan gelen intent'lere bak
1464
+ intents = intent_analysis.get("intents", [])
1465
+ # stock, info, price gibi intent'ler stok sorgusu gerektirir
1466
+ stock_related_intents = ["stock", "info", "price", "availability"]
1467
+ should_query_stock = any(intent in intents for intent in stock_related_intents)
1468
+
1469
+ # Urun tespit edildiyse de stok sorgula (kullanici urun hakkinda konusuyor demektir)
1470
+ if detected_product:
1471
+ should_query_stock = True
1472
+
1473
+ # Yedek: Intent Analyzer calismazsa basit keyword kontrolu
1474
+ if not intent_analysis and is_stock_query(user_message):
1475
+ should_query_stock = True
1476
+
1477
+ if should_query_stock and stock_query_product and not xml_has_valid_stock:
1478
+ realtime_stock = get_realtime_stock_parallel(stock_query_product)
1479
+ if realtime_stock:
1480
+ messages.append({"role": "system", "content": f"Gercek Zamanli Stok:\n{realtime_stock}"})
1481
+ logger.info(f"📦 API'den stok bilgisi eklendi: {stock_query_product}")
1482
+
1483
+ if not OPENAI_API_KEY:
1484
+ return "Sistem hatasi olustu."
1485
+
1486
+ # SON HATIRLATMA: Turkce dil kurallari - GPT'ye her yanit oncesi hatirlatma
1487
+ # Bu mesaj en sona ekleniyor ki GPT son gordukleri kurallari uygulasın
1488
+ turkish_reminder = """KRITIK KURALLAR (HER YANIT ICIN GECERLI):
1489
+ 1. ASLA 'sen' kullanma, HER ZAMAN 'siz' kullan (istersen -> isterseniz, sana -> size)
1490
+ 2. ASLA soru ile bitirme (ayirtayim mi?, ister misiniz?, bakar misiniz? YASAK)
1491
+ 3. Bilgiyi ver ve sus, musteri karar versin
1492
+ 4. ONEMLI: Onceki mesajlarda bahsedilen urunleri UNUTMA! "Hangi model var" gibi sorular onceki konudan devam eder.
1493
+ YANLIS: "Istersen beden ve magaza bazli stok bilgisini de netlestirebilirim."
1494
+ DOGRU: "Beden ve magaza bazli stok bilgisi icin yazabilirsiniz." """
1495
+ messages.append({"role": "system", "content": turkish_reminder})
1496
+
1497
+ # Model tipine gore payload olustur
1498
+ # GPT-5.2 ve o1/o3 modelleri: temperature ve max_tokens desteklemiyor
1499
+ if "gpt-5" in model or "o1" in model or "o3" in model:
1500
+ payload = {
1501
+ "model": model,
1502
+ "messages": messages,
1503
+ "max_completion_tokens": 1000
1504
+ }
1505
+ else:
1506
+ payload = {
1507
+ "model": model,
1508
+ "messages": messages,
1509
+ "temperature": 0.3,
1510
+ "max_tokens": 1000
1511
+ }
1512
+
1513
+ headers = {
1514
+ "Content-Type": "application/json",
1515
+ "Authorization": f"Bearer {OPENAI_API_KEY}"
1516
+ }
1517
+
1518
+ logger.info(f"📤 API istegi gonderiliyor - Model: {model}")
1519
+
1520
+ response = requests.post(API_URL, headers=headers, json=payload, timeout=30)
1521
+
1522
+ if response.status_code == 200:
1523
+ result = response.json()
1524
+ ai_response = result['choices'][0]['message']['content']
1525
+
1526
+ # Kullanilan modeli logla
1527
+ used_model = result.get('model', model)
1528
+ logger.info(f"✅ Yanit alindi - Kullanilan model: {used_model}")
1529
+
1530
+ try:
1531
+ formatted_response = extract_product_info_whatsapp(ai_response)
1532
+ except:
1533
+ formatted_response = ai_response
1534
+
1535
+ # Urun linki ve gorseli varsa ekle
1536
+ # NOT: Stok/magaza sorularinda yanlis gorsel gostermemek icin
1537
+ # sadece TEK URUN soruldugundan eminsen gorsel gonder
1538
+ is_specific_product_query = best_match_score >= 3 # En az 3 kelime eslesmeli
1539
+
1540
+ # GORSEL GONDERILMEMESI GEREKEN SORU TIPLERI
1541
+ # Beden, size, kadro, geometri gibi sorularda gorsel gereksiz
1542
+ no_image_keywords = [
1543
+ 'beden', 'size', 'kadro', 'boy', 'kac cm', 'kaç cm',
1544
+ 'hangi beden', 'xl mi', 'l mi', 'm mi', 's mi',
1545
+ 'geometri', 'stack', 'reach', 'standover',
1546
+ 'kilo', 'agirlik', 'ağırlık', 'weight',
1547
+ 'garanti', 'warranty', 'teslimat', 'kargo',
1548
+ 'taksit', 'odeme', 'ödeme', 'kredi', 'havale'
1549
+ ]
1550
+ user_msg_lower = user_message.lower()
1551
+ is_info_only_query = any(keyword in user_msg_lower for keyword in no_image_keywords)
1552
+
1553
+ if found_product_link and is_specific_product_query and not is_info_only_query:
1554
+ formatted_response += f"\n\n🔗 {found_product_link}"
1555
+
1556
+ add_to_conversation(phone_number, user_message, formatted_response)
1557
+
1558
+ # Gorsel sadece spesifik urun sorgusu ise gonder
1559
+ # "Sariyer'de hangi renk var" gibi genel sorularda gorsel gonderme
1560
+ # Beden/size sorularinda da gorsel gonderme
1561
+ if found_product_image and is_specific_product_query and not is_info_only_query:
1562
+ return (formatted_response, found_product_image)
1563
+
1564
+ return formatted_response
1565
+ else:
1566
+ error_msg = response.text[:200] if response.text else "Bilinmeyen hata"
1567
+ logger.error(f"❌ API hatasi {response.status_code}: {error_msg}")
1568
+
1569
+ # Fallback modele gec
1570
+ if model != MODEL_CONFIG["fallback"]:
1571
+ logger.info(f"🔄 Fallback modele geciliyor: {MODEL_CONFIG['fallback']}")
1572
+ fallback_model = MODEL_CONFIG["fallback"]
1573
+
1574
+ # Fallback icin payload'i yeniden olustur (max_tokens kullan)
1575
+ fallback_payload = {
1576
+ "model": fallback_model,
1577
+ "messages": messages,
1578
+ "temperature": 0.3,
1579
+ "max_tokens": 1000
1580
+ }
1581
+
1582
+ response = requests.post(API_URL, headers=headers, json=fallback_payload, timeout=30)
1583
+
1584
+ if response.status_code == 200:
1585
+ result = response.json()
1586
+ ai_response = result['choices'][0]['message']['content']
1587
+ add_to_conversation(phone_number, user_message, ai_response)
1588
+ return ai_response
1589
+
1590
+ return "Su anda bir sorun yasiyorum. Lutfen tekrar deneyin."
1591
+
1592
+ except requests.exceptions.Timeout:
1593
+ logger.error("❌ API timeout hatasi")
1594
+ return "Islem zaman asimina ugradi. Lutfen tekrar deneyin."
1595
+ except Exception as e:
1596
+ logger.error(f"❌ Mesaj isleme hatasi: {e}")
1597
+ import traceback
1598
+ traceback.print_exc()
1599
+ return "Teknik bir sorun olustu. Lutfen tekrar deneyin."
1600
+
1601
+
1602
+ # ===============================
1603
+ # FASTAPI UYGULAMASI
1604
+ # ===============================
1605
+ app = FastAPI(title="Trek WhatsApp Bot - Hybrid Model")
1606
+
1607
+ @app.post("/whatsapp-webhook")
1608
+ async def whatsapp_webhook(request: Request):
1609
+ """WhatsApp webhook - Hybrid model ile"""
1610
+ try:
1611
+ form_data = await request.form()
1612
+
1613
+ from_number = form_data.get('From')
1614
+ to_number = form_data.get('To')
1615
+ message_body = form_data.get('Body', '')
1616
+ message_status = form_data.get('MessageStatus')
1617
+
1618
+ # Medya kontrolu
1619
+ num_media = int(form_data.get('NumMedia', 0))
1620
+ media_urls = []
1621
+ media_types = []
1622
+
1623
+ for i in range(num_media):
1624
+ media_url = form_data.get(f'MediaUrl{i}')
1625
+ media_type = form_data.get(f'MediaContentType{i}')
1626
+ if media_url:
1627
+ media_urls.append(media_url)
1628
+ media_types.append(media_type)
1629
+
1630
+ logger.info(f"📱 Webhook - From: {from_number}, Body: {message_body[:50] if message_body else 'N/A'}, Media: {num_media}")
1631
+
1632
+ # Durum guncellemelerini ignore et
1633
+ if message_status in ['sent', 'delivered', 'read', 'failed']:
1634
+ return {"status": "ignored", "message": f"Status: {message_status}"}
1635
+
1636
+ # Giden mesajlari ignore et
1637
+ if to_number != TWILIO_WHATSAPP_NUMBER:
1638
+ return {"status": "ignored", "message": "Outgoing message"}
1639
+
1640
+ # Bos mesaj ve medya yoksa ignore et
1641
+ if not message_body and num_media == 0:
1642
+ return {"status": "ignored", "message": "Empty message"}
1643
+
1644
+ logger.info(f"✅ MESAJ ALINDI: {from_number} -> Metin: {bool(message_body)}, Medya: {num_media}")
1645
+
1646
+ if not twilio_client:
1647
+ return {"status": "error", "message": "Twilio yapilandirmasi eksik"}
1648
+
1649
+ # ========================================
1650
+ # HYBRID MODEL SECIMI
1651
+ # ========================================
1652
+ product_image_url = None
1653
+
1654
+ if num_media > 0 and media_urls:
1655
+ # GORSEL VAR -> GPT-4o kullan
1656
+ logger.info("🖼️ GORSEL TESPIT EDILDI -> GPT-4o Vision kullanilacak")
1657
+ ai_response = process_whatsapp_message_with_media(
1658
+ message_body,
1659
+ from_number,
1660
+ media_urls,
1661
+ media_types
1662
+ )
1663
+ else:
1664
+ # SADECE METIN -> GPT-5.2 kullan
1665
+ logger.info("📝 METIN MESAJI -> GPT-5.2 kullanilacak")
1666
+ result = process_whatsapp_message_with_memory(
1667
+ message_body,
1668
+ from_number
1669
+ )
1670
+
1671
+ # Tuple donerse (metin, gorsel_url) seklinde
1672
+ if isinstance(result, tuple):
1673
+ ai_response, product_image_url = result
1674
+ logger.info(f"🖼️ Urun gorseli bulundu: {product_image_url}")
1675
+ else:
1676
+ ai_response = result
1677
+
1678
+ # Yanit kisalt
1679
+ if len(ai_response) > 1500:
1680
+ ai_response = ai_response[:1500] + "...\n\nDetayli bilgi: trekbisiklet.com.tr"
1681
+
1682
+ # WhatsApp'a gonder
1683
+ # Once metin mesaji gonder
1684
+ message = twilio_client.messages.create(
1685
+ messaging_service_sid=TWILIO_MESSAGING_SERVICE_SID,
1686
+ body=ai_response,
1687
+ to=from_number
1688
+ )
1689
+
1690
+ logger.info(f"✅ YANIT GONDERILDI: {ai_response[:100]}...")
1691
+
1692
+ # Urun gorseli varsa ayrica gonder
1693
+ if product_image_url:
1694
+ try:
1695
+ image_message = twilio_client.messages.create(
1696
+ messaging_service_sid=TWILIO_MESSAGING_SERVICE_SID,
1697
+ media_url=[product_image_url],
1698
+ to=from_number
1699
+ )
1700
+ logger.info(f"🖼️ URUN GORSELI GONDERILDI: {product_image_url}")
1701
+ except Exception as img_error:
1702
+ logger.error(f"❌ Gorsel gonderme hatasi: {img_error}")
1703
+
1704
+ return {"status": "success", "message_sid": message.sid}
1705
+
1706
+ except Exception as e:
1707
+ logger.error(f"❌ Webhook hatasi: {str(e)}")
1708
+ import traceback
1709
+ traceback.print_exc()
1710
+ return {"status": "error", "message": str(e)}
1711
+
1712
+
1713
+ @app.get("/")
1714
+ async def root():
1715
+ return {
1716
+ "message": "Trek WhatsApp Bot - Hybrid Model calisiyor!",
1717
+ "status": "active",
1718
+ "models": {
1719
+ "vision": MODEL_CONFIG["vision"],
1720
+ "text": MODEL_CONFIG["text"],
1721
+ "fallback": MODEL_CONFIG["fallback"]
1722
+ }
1723
+ }
1724
+
1725
+
1726
+ @app.get("/health")
1727
+ async def health():
1728
+ return {
1729
+ "status": "healthy",
1730
+ "twilio_configured": twilio_client is not None,
1731
+ "openai_configured": OPENAI_API_KEY is not None,
1732
+ "models": MODEL_CONFIG,
1733
+ "products_loaded": len(products),
1734
+ "modules": {
1735
+ "gpt5_search": USE_GPT5_SEARCH,
1736
+ "media_queue": USE_MEDIA_QUEUE,
1737
+ "store_notification": USE_STORE_NOTIFICATION,
1738
+ "follow_up": USE_FOLLOW_UP,
1739
+ "intent_analyzer": USE_INTENT_ANALYZER
1740
+ }
1741
+ }
1742
+
1743
+
1744
+ @app.get("/test-models")
1745
+ async def test_models():
1746
+ """Model durumlarini test et"""
1747
+ results = {}
1748
+
1749
+ for model_type, model_name in MODEL_CONFIG.items():
1750
+ try:
1751
+ payload = {
1752
+ "model": model_name,
1753
+ "messages": [{"role": "user", "content": "Merhaba, test mesaji."}],
1754
+ "max_tokens": 10
1755
+ }
1756
+ headers = {
1757
+ "Content-Type": "application/json",
1758
+ "Authorization": f"Bearer {OPENAI_API_KEY}"
1759
+ }
1760
+ response = requests.post(API_URL, headers=headers, json=payload, timeout=10)
1761
+ results[model_type] = {
1762
+ "model": model_name,
1763
+ "status": "OK" if response.status_code == 200 else f"Error: {response.status_code}",
1764
+ "available": response.status_code == 200
1765
+ }
1766
+ except Exception as e:
1767
+ results[model_type] = {
1768
+ "model": model_name,
1769
+ "status": f"Error: {str(e)}",
1770
+ "available": False
1771
+ }
1772
+
1773
+ return results
1774
+
1775
+
1776
+ @app.get("/cache-status")
1777
+ async def cache_status():
1778
+ """Cache durumunu goster"""
1779
+ return {
1780
+ "cache_size": len(stock_cache),
1781
+ "cache_duration_seconds": CACHE_DURATION,
1782
+ "cached_products": list(stock_cache.keys())[:10] # Ilk 10
1783
+ }
1784
+
1785
+
1786
+ @app.post("/clear-cache")
1787
+ async def clear_cache():
1788
+ """Cache'i temizle"""
1789
+ global stock_cache
1790
+ old_size = len(stock_cache)
1791
+ stock_cache = {}
1792
+ return {"message": f"Cache temizlendi. {old_size} kayit silindi."}
1793
+
1794
+
1795
+ if __name__ == "__main__":
1796
+ import uvicorn
1797
+ print("=" * 60)
1798
+ print(" Trek WhatsApp Bot - HYBRID MODEL")
1799
+ print("=" * 60)
1800
+ print(f" 🖼️ Gorsel mesajlar -> {MODEL_CONFIG['vision']}")
1801
+ print(f" 📝 Metin mesajlar -> {MODEL_CONFIG['text']}")
1802
+ print(f" 🔄 Fallback -> {MODEL_CONFIG['fallback']}")
1803
+ print("=" * 60)
1804
+ print(f" 📦 Yuklenen urun sayisi: {len(products)}")
1805
+ print(f" 🔍 GPT-5 Search: {'Aktif' if USE_GPT5_SEARCH else 'Pasif'}")
1806
+ print(f" 🔔 Store Notification: {'Aktif' if USE_STORE_NOTIFICATION else 'Pasif'}")
1807
+ print(f" 📌 Follow-Up System: {'Aktif' if USE_FOLLOW_UP else 'Pasif'}")
1808
+ print(f" 🧠 Intent Analyzer: {'Aktif' if USE_INTENT_ANALYZER else 'Pasif'}")
1809
+ print("=" * 60)
1810
+ uvicorn.run(app, host="0.0.0.0", port=7860)
cached_warehouse_search.py ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Cached warehouse search to reduce API calls and token usage"""
2
+
3
+ import time
4
+ import re
5
+ import json
6
+ import requests
7
+ from typing import Dict, List, Optional, Tuple
8
+
9
+ # Cache configuration
10
+ CACHE_DURATION = 43200 # 12 hours (12 * 60 * 60)
11
+ cache = {
12
+ 'warehouse_xml': {'data': None, 'time': 0},
13
+ 'trek_xml': {'data': None, 'time': 0},
14
+ 'products_summary': {'data': None, 'time': 0},
15
+ 'simple_searches': {} # Cache for specific searches
16
+ }
17
+
18
+ def get_cached_warehouse_xml() -> str:
19
+ """Get warehouse XML with caching"""
20
+ current_time = time.time()
21
+
22
+ if cache['warehouse_xml']['data'] and (current_time - cache['warehouse_xml']['time'] < CACHE_DURATION):
23
+ print("📦 Using cached warehouse XML")
24
+ return cache['warehouse_xml']['data']
25
+
26
+ print("📡 Fetching fresh warehouse XML...")
27
+ url = 'https://video.trek-turkey.com/bizimhesap-warehouse-xml-b2b-api-v2.php'
28
+ response = requests.get(url, verify=False, timeout=15)
29
+
30
+ cache['warehouse_xml']['data'] = response.text
31
+ cache['warehouse_xml']['time'] = current_time
32
+
33
+ return response.text
34
+
35
+ def get_cached_trek_xml() -> str:
36
+ """Get Trek XML with caching"""
37
+ current_time = time.time()
38
+
39
+ if cache['trek_xml']['data'] and (current_time - cache['trek_xml']['time'] < CACHE_DURATION):
40
+ print("🚴 Using cached Trek XML")
41
+ return cache['trek_xml']['data']
42
+
43
+ print("📡 Fetching fresh Trek XML...")
44
+ url = 'https://www.trekbisiklet.com.tr/output/8582384479'
45
+ response = requests.get(url, verify=False, timeout=15)
46
+
47
+ cache['trek_xml']['data'] = response.content
48
+ cache['trek_xml']['time'] = current_time
49
+
50
+ return response.content
51
+
52
+ def simple_product_search(query: str) -> Optional[List[Dict]]:
53
+ """
54
+ Simple local search without GPT-5
55
+ Returns product info if exact/close match found
56
+ """
57
+ query_upper = query.upper()
58
+ query_parts = query_upper.split()
59
+
60
+ # Get cached products summary
61
+ if not cache['products_summary']['data'] or \
62
+ (time.time() - cache['products_summary']['time'] > CACHE_DURATION):
63
+ # Build products summary from cached XML
64
+ build_products_summary()
65
+
66
+ products_summary = cache['products_summary']['data']
67
+
68
+ # Exact product name patterns
69
+ exact_patterns = {
70
+ 'MADONE SL 6': lambda p: 'MADONE SL 6' in p['name'],
71
+ 'MADONE SL 7': lambda p: 'MADONE SL 7' in p['name'],
72
+ 'MARLIN 5': lambda p: 'MARLIN 5' in p['name'],
73
+ 'MARLIN 6': lambda p: 'MARLIN 6' in p['name'],
74
+ 'MARLIN 7': lambda p: 'MARLIN 7' in p['name'],
75
+ 'DOMANE SL 5': lambda p: 'DOMANE SL 5' in p['name'],
76
+ 'CHECKPOINT': lambda p: 'CHECKPOINT' in p['name'],
77
+ 'FX': lambda p: p['name'].startswith('FX'),
78
+ 'DUAL SPORT': lambda p: 'DUAL SPORT' in p['name'],
79
+ 'RAIL': lambda p: 'RAIL' in p['name'],
80
+ 'POWERFLY': lambda p: 'POWERFLY' in p['name'],
81
+ }
82
+
83
+ # Check for exact patterns
84
+ for pattern, matcher in exact_patterns.items():
85
+ if pattern in query_upper:
86
+ matching = [p for p in products_summary if matcher(p)]
87
+ if matching:
88
+ print(f"✅ Found {len(matching)} products via simple search (no GPT-5 needed)")
89
+ return matching
90
+
91
+ # Check for simple one-word queries
92
+ if len(query_parts) == 1:
93
+ matching = [p for p in products_summary if query_parts[0] in p['name']]
94
+ if matching and len(matching) < 20: # If reasonable number of matches
95
+ print(f"✅ Found {len(matching)} products via simple search (no GPT-5 needed)")
96
+ return matching
97
+
98
+ return None # Need GPT-5 for complex queries
99
+
100
+ def build_products_summary():
101
+ """Build products summary from cached XMLs"""
102
+ xml_text = get_cached_warehouse_xml()
103
+
104
+ # Extract products
105
+ product_pattern = r'<Product>(.*?)</Product>'
106
+ all_products = re.findall(product_pattern, xml_text, re.DOTALL)
107
+
108
+ products_summary = []
109
+ for i, product_block in enumerate(all_products):
110
+ name_match = re.search(r'<ProductName><!\[CDATA\[(.*?)\]\]></ProductName>', product_block)
111
+ variant_match = re.search(r'<ProductVariant><!\[CDATA\[(.*?)\]\]></ProductVariant>', product_block)
112
+
113
+ if name_match:
114
+ warehouses_with_stock = []
115
+ warehouse_regex = r'<Warehouse>.*?<Name><!\[CDATA\[(.*?)\]\]></Name>.*?<Stock>(.*?)</Stock>.*?</Warehouse>'
116
+ warehouses = re.findall(warehouse_regex, product_block, re.DOTALL)
117
+
118
+ for wh_name, wh_stock in warehouses:
119
+ try:
120
+ if int(wh_stock.strip()) > 0:
121
+ warehouses_with_stock.append(wh_name)
122
+ except:
123
+ pass
124
+
125
+ product_info = {
126
+ "index": i,
127
+ "name": name_match.group(1),
128
+ "variant": variant_match.group(1) if variant_match else "",
129
+ "warehouses": warehouses_with_stock
130
+ }
131
+ products_summary.append(product_info)
132
+
133
+ cache['products_summary']['data'] = products_summary
134
+ cache['products_summary']['time'] = time.time()
135
+
136
+ print(f"📊 Built products summary: {len(products_summary)} products")
137
+
138
+ def should_use_gpt5(query: str) -> bool:
139
+ """Determine if query needs GPT-5"""
140
+ query_lower = query.lower()
141
+
142
+ # Complex queries that need GPT-5
143
+ gpt5_triggers = [
144
+ 'öneri', 'tavsiye', 'bütçe', 'karşılaştır',
145
+ 'hangisi', 'ne önerirsin', 'yardım',
146
+ 'en iyi', 'en ucuz', 'en pahalı',
147
+ 'kaç tane', 'toplam', 'fark'
148
+ ]
149
+
150
+ for trigger in gpt5_triggers:
151
+ if trigger in query_lower:
152
+ return True
153
+
154
+ # If simple search found results, don't use GPT-5
155
+ if simple_product_search(query):
156
+ return False
157
+
158
+ return True # Default to GPT-5 for uncertain cases
159
+
160
+ # Usage example
161
+ def smart_warehouse_search(query: str) -> List[str]:
162
+ """
163
+ Smart search with caching and minimal GPT-5 usage
164
+ """
165
+ # Check simple search cache first
166
+ cache_key = query.lower()
167
+ if cache_key in cache['simple_searches']:
168
+ cached_result = cache['simple_searches'][cache_key]
169
+ if time.time() - cached_result['time'] < CACHE_DURATION:
170
+ print(f"✅ Using cached result for '{query}'")
171
+ return cached_result['data']
172
+
173
+ # Try simple search
174
+ simple_results = simple_product_search(query)
175
+ if simple_results:
176
+ # Format and cache the results
177
+ formatted_results = format_simple_results(simple_results)
178
+ cache['simple_searches'][cache_key] = {
179
+ 'data': formatted_results,
180
+ 'time': time.time()
181
+ }
182
+ return formatted_results
183
+
184
+ # Fall back to GPT-5 if needed
185
+ print(f"🤖 Using GPT-5 for complex query: '{query}'")
186
+ # Call existing GPT-5 function here
187
+ return None # Would call get_warehouse_stock_smart_with_price
188
+
189
+ def format_simple_results(products: List[Dict]) -> List[str]:
190
+ """Format simple search results"""
191
+ if not products:
192
+ return ["Ürün bulunamadı"]
193
+
194
+ result = ["Bulunan ürünler:"]
195
+
196
+ # Group by product name
197
+ product_groups = {}
198
+ for p in products:
199
+ if p['name'] not in product_groups:
200
+ product_groups[p['name']] = []
201
+ product_groups[p['name']].append(p)
202
+
203
+ for product_name, variants in product_groups.items():
204
+ result.append(f"\n{product_name}:")
205
+ for v in variants:
206
+ if v['variant']:
207
+ warehouses_str = ", ".join([w.replace('MAGAZA DEPO', '').strip() for w in v['warehouses']])
208
+ result.append(f"• {v['variant']}: {warehouses_str if warehouses_str else 'Stokta yok'}")
209
+
210
+ return result
combined_api_search.py ADDED
@@ -0,0 +1,581 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Combined API Search Module - Yeni tek API'yi kullanan arama sistemi"""
2
+
3
+ import requests
4
+ import xml.etree.ElementTree as ET
5
+ import os
6
+ import time
7
+ import logging
8
+
9
+ # Logger ayarla
10
+ logger = logging.getLogger(__name__)
11
+
12
+ def normalize_turkish(text):
13
+ """Türkçe karakter normalizasyonu"""
14
+ if not text:
15
+ return ""
16
+
17
+ # Önce uppercase yap
18
+ text = text.upper()
19
+
20
+ # Türkçe karakterleri normalize et
21
+ replacements = {
22
+ 'İ': 'I', 'I': 'I', 'ı': 'I', 'Ì': 'I', 'Í': 'I', 'Î': 'I', 'Ï': 'I',
23
+ 'Ş': 'S', 'ş': 'S', 'Ș': 'S', 'ș': 'S', 'Ś': 'S',
24
+ 'Ğ': 'G', 'ğ': 'G', 'Ĝ': 'G', 'ĝ': 'G',
25
+ 'Ü': 'U', 'ü': 'U', 'Û': 'U', 'û': 'U', 'Ù': 'U', 'Ú': 'U',
26
+ 'Ö': 'O', 'ö': 'O', 'Ô': 'O', 'ô': 'O', 'Ò': 'O', 'Ó': 'O',
27
+ 'Ç': 'C', 'ç': 'C', 'Ć': 'C', 'ć': 'C',
28
+ 'Â': 'A', 'â': 'A', 'À': 'A', 'Á': 'A', 'Ä': 'A',
29
+ 'Ê': 'E', 'ê': 'E', 'È': 'E', 'É': 'E', 'Ë': 'E'
30
+ }
31
+
32
+ for tr_char, en_char in replacements.items():
33
+ text = text.replace(tr_char, en_char)
34
+
35
+ # Birden fazla boşluğu tek boşluğa çevir
36
+ import re
37
+ text = re.sub(r'\s+', ' ', text)
38
+
39
+ return text.strip()
40
+
41
+ # Cache configuration
42
+ CACHE_DURATION = 3600 # 1 saat
43
+ cache = {
44
+ 'combined_api': {'data': None, 'time': 0}
45
+ }
46
+
47
+ def get_combined_api_data():
48
+ """Combined API'den veriyi çek ve cache'le"""
49
+ current_time = time.time()
50
+
51
+ # Cache kontrolü
52
+ if cache['combined_api']['data'] and (current_time - cache['combined_api']['time'] < CACHE_DURATION):
53
+ cache_age = (current_time - cache['combined_api']['time']) / 60
54
+ logger.info(f"📦 Combined API cache kullanılıyor (yaş: {cache_age:.1f} dakika)")
55
+ return cache['combined_api']['data']
56
+
57
+ try:
58
+ # Yeni combined API'yi çağır
59
+ url = 'https://video.trek-turkey.com/combined_trek_xml_api_v2.php'
60
+ response = requests.get(url, timeout=30)
61
+
62
+ if response.status_code == 200:
63
+ # XML parse et
64
+ root = ET.fromstring(response.content)
65
+
66
+ # Cache'e kaydet
67
+ cache['combined_api']['data'] = root
68
+ cache['combined_api']['time'] = current_time
69
+
70
+ # İstatistikleri logla
71
+ stats = root.find('stats')
72
+ if stats is not None:
73
+ matched = stats.find('matched_products')
74
+ match_rate = stats.find('match_rate')
75
+ if matched is not None and match_rate is not None:
76
+ logger.info(f"✅ Combined API: {matched.text} ürün eşleşti ({match_rate.text})")
77
+
78
+ return root
79
+ else:
80
+ logger.error(f"❌ Combined API HTTP Error: {response.status_code}")
81
+ return None
82
+
83
+ except Exception as e:
84
+ logger.error(f"❌ Combined API Hata: {e}")
85
+ return None
86
+
87
+ def search_products_combined_api(query):
88
+ """Yeni combined API kullanarak ürün ara"""
89
+ try:
90
+ # Combined API verisini al
91
+ root = get_combined_api_data()
92
+ if not root:
93
+ return []
94
+
95
+ # Stop words - arama skorunu etkilememesi gereken kelimeler
96
+ stop_words = ['VAR', 'MI', 'MEVCUT', 'MU', 'STOK', 'STOGU', 'DURUMU', 'BULUNUYOR']
97
+
98
+ # Query'yi normalize et ve stop words'leri filtrele
99
+ query_words = normalize_turkish(query).upper().split()
100
+ query_words = [word for word in query_words if word not in stop_words]
101
+ query_normalized = ' '.join(query_words)
102
+
103
+ results = []
104
+
105
+ # Tüm ürünlerde ara
106
+ for product in root.findall('product'):
107
+ # Ürün bilgilerini al
108
+ stock_code = product.find('stock_code')
109
+ name = product.find('name')
110
+ label = product.find('label')
111
+ url = product.find('url')
112
+ image_url = product.find('image_url')
113
+
114
+ # Fiyat bilgileri
115
+ prices = product.find('prices')
116
+ regular_price = None
117
+ discounted_price = None
118
+
119
+ if prices is not None:
120
+ regular_price_elem = prices.find('regular_price')
121
+ discounted_price_elem = prices.find('discounted_price')
122
+
123
+ if regular_price_elem is not None:
124
+ regular_price = regular_price_elem.text
125
+ if discounted_price_elem is not None:
126
+ discounted_price = discounted_price_elem.text
127
+
128
+ # Kategori bilgileri
129
+ category = product.find('category')
130
+ main_category = None
131
+ sub_category = None
132
+
133
+ if category is not None:
134
+ main_cat_elem = category.find('main_category')
135
+ sub_cat_elem = category.find('sub_category')
136
+
137
+ if main_cat_elem is not None:
138
+ main_category = main_cat_elem.text
139
+ if sub_cat_elem is not None:
140
+ sub_category = sub_cat_elem.text
141
+
142
+ # Warehouse stok bilgileri
143
+ warehouse_stock = product.find('warehouse_stock')
144
+ stock_found = False
145
+ total_stock = 0
146
+ warehouses = []
147
+
148
+ if warehouse_stock is not None:
149
+ found_attr = warehouse_stock.get('found')
150
+ stock_found = found_attr == 'true'
151
+
152
+ if stock_found:
153
+ total_stock_elem = warehouse_stock.find('total_stock')
154
+ if total_stock_elem is not None:
155
+ total_stock = int(total_stock_elem.text)
156
+
157
+ # Depo bilgileri
158
+ warehouses_elem = warehouse_stock.find('warehouses')
159
+ if warehouses_elem is not None:
160
+ for wh in warehouses_elem.findall('warehouse'):
161
+ wh_name = wh.find('name')
162
+ wh_stock = wh.find('stock')
163
+
164
+ if wh_name is not None and wh_stock is not None:
165
+ warehouses.append({
166
+ 'name': wh_name.text,
167
+ 'stock': int(wh_stock.text)
168
+ })
169
+
170
+ # Arama kriteri kontrolü - Türkçe normalizasyon ile
171
+ searchable_text = ""
172
+ if name is not None:
173
+ searchable_text += normalize_turkish(name.text) + " "
174
+ if label is not None:
175
+ searchable_text += normalize_turkish(label.text) + " "
176
+ if stock_code is not None:
177
+ searchable_text += normalize_turkish(stock_code.text) + " "
178
+ if main_category is not None:
179
+ searchable_text += normalize_turkish(main_category) + " "
180
+ if sub_category is not None:
181
+ searchable_text += normalize_turkish(sub_category) + " "
182
+
183
+ # Geliştirilmiş arama eşleştirme - stop words filtrelenmiş query_words kullan
184
+ filtered_query_words = query_words # Zaten üstte filtrelenmiş
185
+ name_normalized = normalize_turkish(name.text.upper()) if name is not None else ""
186
+
187
+ # 1. Temel kelime eşleşmesi
188
+ basic_matches = sum(1 for word in filtered_query_words if word in searchable_text)
189
+
190
+ if basic_matches > 0:
191
+ # 2. Tam eşleşme bonusu - tüm query kelimeleri ürün adında
192
+ exact_bonus = 10 if all(word in name_normalized for word in filtered_query_words) else 0
193
+
194
+ # 3. Sıra bonus - kelimeler doğru sırada mı?
195
+ sequence_bonus = 0
196
+ if len(filtered_query_words) >= 2:
197
+ query_sequence = " ".join(filtered_query_words)
198
+ if query_sequence in name_normalized:
199
+ sequence_bonus = 5
200
+
201
+ # 4. Uzunluk penaltısı - çok uzun ürün adları düşük skor alsın
202
+ length_penalty = max(0, len(name_normalized.split()) - len(filtered_query_words)) * -1
203
+
204
+ # Toplam skor
205
+ total_score = basic_matches + exact_bonus + sequence_bonus + length_penalty
206
+
207
+ result = {
208
+ 'stock_code': stock_code.text if stock_code is not None else '',
209
+ 'name': name.text if name is not None else '',
210
+ 'label': label.text if label is not None else '',
211
+ 'url': url.text if url is not None else '',
212
+ 'image_url': image_url.text if image_url is not None else '',
213
+ 'regular_price': regular_price,
214
+ 'discounted_price': discounted_price,
215
+ 'main_category': main_category,
216
+ 'sub_category': sub_category,
217
+ 'stock_found': stock_found,
218
+ 'total_stock': total_stock,
219
+ 'warehouses': warehouses,
220
+ 'match_score': total_score
221
+ }
222
+ results.append(result)
223
+
224
+ # Sonuçları match score'a göre sırala
225
+ results.sort(key=lambda x: x['match_score'], reverse=True)
226
+
227
+ logger.info(f"🔍 Combined API araması: '{query}' için {len(results)} sonuç")
228
+ return results[:10] # İlk 10 sonuç
229
+
230
+ except Exception as e:
231
+ logger.error(f"❌ Combined API arama hatası: {e}")
232
+ return []
233
+
234
+ def format_product_result_whatsapp(product_data, show_variants_info=False, all_results=None):
235
+ """Ürün bilgisini WhatsApp için formatla"""
236
+ try:
237
+ name = product_data['name']
238
+ url = product_data['url']
239
+ image_url = product_data['image_url']
240
+ regular_price = product_data['regular_price']
241
+ discounted_price = product_data['discounted_price']
242
+ stock_found = product_data['stock_found']
243
+ total_stock = product_data['total_stock']
244
+ warehouses = product_data['warehouses']
245
+
246
+ result = []
247
+
248
+ # Ürün adı
249
+ result.append(f"🚲 **{name}**")
250
+
251
+ # Fiyat bilgisi - Original Trek fiyat yuvarlama algoritması (app.py'dan)
252
+ def format_price(price_str):
253
+ if not price_str:
254
+ return ""
255
+ try:
256
+ price_float = float(price_str)
257
+ # Original Trek fiyat yuvarlama algoritması
258
+ if price_float > 200000:
259
+ rounded = round(price_float / 5000) * 5000
260
+ elif price_float > 30000:
261
+ rounded = round(price_float / 1000) * 1000
262
+ elif price_float > 10000:
263
+ rounded = round(price_float / 100) * 100
264
+ else:
265
+ rounded = round(price_float / 10) * 10
266
+
267
+ # Türk Lirası formatı: 1.234,56
268
+ return f"{rounded:,.0f}".replace(",", ".")
269
+ except:
270
+ return price_str
271
+
272
+ if regular_price:
273
+ formatted_regular = format_price(regular_price)
274
+ if discounted_price and discounted_price != regular_price:
275
+ formatted_discounted = format_price(discounted_price)
276
+ result.append(f"💰 Fiyat: ~~{formatted_regular}~~ **{formatted_discounted} TL**")
277
+ result.append("🔥 İndirimli fiyat!")
278
+ else:
279
+ result.append(f"💰 Fiyat: {formatted_regular} TL")
280
+
281
+ # Stok durumu - Varyant detayları ile
282
+ if stock_found and total_stock > 0:
283
+ # Hangi mağazalarda var + varyant bilgisi (ana ürünse)
284
+ if warehouses:
285
+ # Ana ürün ise varyant detaylarını da göster
286
+ if show_variants_info and all_results and is_main_product(name):
287
+ result.append("📦 **Mevcut varyantlar:**")
288
+
289
+ # Bu ana ürünün varyantlarını bul
290
+ main_name_base = name.upper()
291
+ variant_details = {}
292
+
293
+ for product in all_results:
294
+ if (main_name_base in product['name'].upper() and
295
+ product['stock_found'] and
296
+ product['total_stock'] > 0 and
297
+ not is_main_product(product['name'])):
298
+
299
+ # Varyant isminden beden/renk çıkar
300
+ variant_name = product['name']
301
+ variant_part = variant_name.replace(main_name_base, '').strip()
302
+ if variant_part.startswith('GEN 3 (2026)'):
303
+ variant_part = variant_part.replace('GEN 3 (2026)', '').strip()
304
+
305
+ for wh in product['warehouses']:
306
+ if wh['stock'] > 0:
307
+ store_name = wh['name']
308
+ if store_name not in variant_details:
309
+ variant_details[store_name] = []
310
+ variant_details[store_name].append(variant_part)
311
+
312
+ # Mağaza bazında varyantları listele
313
+ for store, variants in variant_details.items():
314
+ result.append(f"• **{store}:** {', '.join(variants)}")
315
+
316
+ # Genel bilgi
317
+ result.append("📞 Diğer beden/renk teyidi için mağazaları arayın")
318
+ else:
319
+ # Normal stok bilgisi
320
+ available_stores = [wh['name'] for wh in warehouses if wh['stock'] > 0]
321
+ if available_stores:
322
+ result.append("📦 **Stokta mevcut mağazalar:**")
323
+ for store in available_stores:
324
+ result.append(f"• {store}")
325
+ else:
326
+ result.append("⚠️ **Stok durumu kontrol edilemiyor**")
327
+ result.append("📞 Güncel stok için mağazalarımızı arayın:")
328
+ result.append("• Caddebostan: 0543 934 0438")
329
+ result.append("• Alsancak: 0543 936 2335")
330
+
331
+ # Ürün linki
332
+ if url:
333
+ result.append(f"🔗 [Ürün Detayları]({url})")
334
+
335
+ # Ürün resmi (WhatsApp için) - Direkt URL WhatsApp'ta resmi gösterir
336
+ if image_url and image_url.startswith('https://'):
337
+ # WhatsApp'ta resmi direkt göstermek için sadece URL'i ver (link formatı değil)
338
+ result.append(f"📷 {image_url}")
339
+
340
+ return "\n".join(result)
341
+
342
+ except Exception as e:
343
+ logger.error(f"❌ WhatsApp format hatası: {e}")
344
+ return "Ürün bilgisi formatlanamadı"
345
+
346
+ def is_main_product(product_name):
347
+ """Ürün adına bakarak ana ürün mü varyant mı kontrol et"""
348
+ name_upper = product_name.upper()
349
+
350
+ # Varyant göstergeleri - beden, renk, yıl belirticileri
351
+ variant_indicators = [
352
+ # Bedenler
353
+ ' - XS', ' - S', ' - M', ' - L', ' - XL', ' - XXL',
354
+ ' XS', ' S ', ' M ', ' L ', ' XL', ' XXL',
355
+
356
+ # Renkler
357
+ ' - SİYAH', ' - MAVİ', ' - KIRMIZI', ' - YEŞİL', ' - BEYAZ', ' - MOR',
358
+ ' SİYAH', ' MAVİ', ' KIRMIZI', ' YEŞİL', ' BEYAZ', ' MOR',
359
+
360
+ # İngilizce renkler
361
+ ' - BLACK', ' - BLUE', ' - RED', ' - GREEN', ' - WHITE', ' - PURPLE',
362
+ ' BLACK', ' BLUE', ' RED', ' GREEN', ' WHITE', ' PURPLE'
363
+ ]
364
+
365
+ # Eğer bu göstergelerden biri varsa varyant
366
+ for indicator in variant_indicators:
367
+ if indicator in name_upper:
368
+ return False
369
+
370
+ return True
371
+
372
+ def get_warehouse_stock_combined_api(query):
373
+ """Combined API kullanan warehouse search - eski fonksiyonla uyumlu"""
374
+ results = search_products_combined_api(query)
375
+
376
+ if not results:
377
+ return ["Ürün bulunamadı"]
378
+
379
+ # Akıllı model geçişi - eski model stokta yoksa yeni modeli öner
380
+ # Örn: "marlin 4" arandığında MARLIN 4 yoksa MARLIN 4 GEN 3'ü göster
381
+ query_normalized = normalize_turkish(query.upper())
382
+
383
+ # Query'yi temizle - "var mı", "stok", "fiyat" gibi ekleri çıkar
384
+ clean_query = query_normalized
385
+ for suffix in ['VAR MI', 'VAR MIYDI', 'STOK', 'STOKTA', 'FIYAT', 'FIYATI', 'KACA', 'NE KADAR']:
386
+ clean_query = clean_query.replace(' ' + suffix, '').replace(suffix + ' ', '').replace(suffix, '')
387
+ clean_query = clean_query.strip()
388
+
389
+ logger.info(f"🧹 Query temizlendi: '{query}' → '{clean_query}'")
390
+
391
+ # Genel ürün adı aranıyorsa (model numarası yok)
392
+ if not any(year in clean_query for year in ['GEN', '2024', '2025', '2026', '(20']):
393
+ # Stokta olan modelleri kontrol et
394
+ stocked_alternatives = []
395
+
396
+ for product in results:
397
+ name_normalized = normalize_turkish(product['name'].upper())
398
+ # Ana ürün adıyla eşleşiyor mu? (temizlenmiş query ile)
399
+ if clean_query in name_normalized:
400
+ # Ana ürün mü?
401
+ if is_main_product(product['name']):
402
+ # Stokta mı veya varyantları var mı?
403
+ if product['stock_found'] and product['total_stock'] > 0:
404
+ stocked_alternatives.append(product)
405
+ else:
406
+ # Ana ürün stokta yok, varyantları kontrol et
407
+ has_stocked_variants = False
408
+ for variant in results:
409
+ if (product['name'].upper() in variant['name'].upper() and
410
+ variant['stock_found'] and
411
+ variant['total_stock'] > 0 and
412
+ not is_main_product(variant['name'])):
413
+ has_stocked_variants = True
414
+ break
415
+ if has_stocked_variants:
416
+ stocked_alternatives.append(product)
417
+
418
+ # Eğer alternatifler varsa, en yenisini seç (genelde GEN 3, 2026 vs.)
419
+ if stocked_alternatives:
420
+ # Model yılı/versiyonu olanları öncelikle
421
+ def get_model_priority(product):
422
+ name = product['name'].upper()
423
+ if '2026' in name or 'GEN 3' in name:
424
+ return 3
425
+ elif '2025' in name or 'GEN 2' in name:
426
+ return 2
427
+ elif '2024' in name or 'GEN' in name:
428
+ return 1
429
+ else:
430
+ return 0
431
+
432
+ stocked_alternatives.sort(key=lambda x: get_model_priority(x), reverse=True)
433
+
434
+ # En yeni modeli öncelikle kullan
435
+ if stocked_alternatives:
436
+ results = [stocked_alternatives[0]] + [r for r in results if r != stocked_alternatives[0]]
437
+
438
+ # Önce ana ürünleri filtrele
439
+ main_products = []
440
+ variant_products = []
441
+
442
+ for product in results:
443
+ if is_main_product(product['name']):
444
+ main_products.append(product)
445
+ else:
446
+ variant_products.append(product)
447
+
448
+ # Ana ürünün stok bilgilerini varyantlarından topla
449
+ def enrich_main_product_with_variant_stock(main_product, all_results):
450
+ """Ana ürün stoku yoksa varyant stoklarını topla - TAM EŞLEŞTİRME"""
451
+ if main_product['stock_found'] and main_product['total_stock'] > 0:
452
+ return main_product # Zaten stok bilgisi var
453
+
454
+ # Bu ana ürünün varyantlarını bul - TAM EŞLEŞTIRME
455
+ main_name = main_product['name'].upper()
456
+ related_variants = []
457
+
458
+ for product in all_results:
459
+ product_name_upper = product['name'].upper()
460
+
461
+ # TAM EŞLEŞTIRME: varyant ana ürünle TAM BAŞLAMALI
462
+ # Örn: "MARLIN 4 GEN 3 (2026) MOR - XS" → "MARLIN 4 GEN 3 (2026)" ile eşleşmeli
463
+ # "MARLIN 4 SİYAH - XS" → "MARLIN 4" ile eşleşmeli
464
+
465
+ variant_matches = False
466
+ if (product['stock_found'] and
467
+ product['total_stock'] > 0 and
468
+ not is_main_product(product['name'])):
469
+
470
+ # TAM ÜRÜN EŞLEŞTIRMESI - backup mantığıyla ters
471
+ # Ana ürün "MARLIN 4" ise
472
+ # Varyant "MARLIN 4 SİYAH - XS" EVET, "MARLIN 4 GEN 3 (...) MOR - XS" HAYIR
473
+
474
+ # Ana ürün tam adıyla başlamalı, fazla kelime olmamalı
475
+ if product_name_upper.startswith(main_name):
476
+ remaining_after_main = product_name_upper[len(main_name):].strip()
477
+
478
+ # Kalan kısımda GEN/2026 gibi model ayırıcıları varsa bu farklı ürün
479
+ model_separators = ['GEN 3', 'GEN', '(2026)', '(2025)', '2026', '2025']
480
+ has_model_separator = any(sep in remaining_after_main for sep in model_separators)
481
+
482
+ # Ana ürün model ayırıcısı içeriyorsa, varyant da içermeli
483
+ main_has_separators = any(sep in main_name for sep in model_separators)
484
+
485
+ # Ana ürün model separator içeriyorsa, varyant onun devamı olabilir
486
+ # Ana ürün model separator içermiyorsa, varyant da içermemeli
487
+
488
+ if main_has_separators:
489
+ # Ana ürün GEN 3 (2026) gibi → varyant da GEN 3'ün devamı olmalı
490
+ # Kalan kısımda model separator olmamalı (çünkü ana üründe zaten var)
491
+ if not has_model_separator:
492
+ # Varyant göstergeleri var mı?
493
+ if remaining_after_main and any(indicator in remaining_after_main for indicator in [' - ', ' MOR', ' SIYAH', ' MAVI', ' XS', ' S ', ' M ', ' L ', ' XL']):
494
+ variant_matches = True
495
+ else:
496
+ # Ana ürün model separator içermiyor → varyant da içermemeli
497
+ if not has_model_separator:
498
+ # Varyant göstergeleri var mı?
499
+ if remaining_after_main and any(indicator in remaining_after_main for indicator in [' - ', ' MOR', ' SIYAH', ' MAVI', ' XS', ' S ', ' M ', ' L ', ' XL']):
500
+ variant_matches = True
501
+
502
+ if variant_matches:
503
+ related_variants.append(product)
504
+
505
+ if related_variants:
506
+ # Varyant stoklarını topla
507
+ combined_warehouses = {}
508
+ total_stock = 0
509
+
510
+ for variant in related_variants:
511
+ total_stock += variant['total_stock']
512
+ for wh in variant['warehouses']:
513
+ wh_name = wh['name']
514
+ wh_stock = wh['stock']
515
+ if wh_name in combined_warehouses:
516
+ combined_warehouses[wh_name] += wh_stock
517
+ else:
518
+ combined_warehouses[wh_name] = wh_stock
519
+
520
+ # Ana ürünü stok bilgileriyle zenginleştir
521
+ main_product['stock_found'] = True
522
+ main_product['total_stock'] = total_stock
523
+ main_product['warehouses'] = [
524
+ {'name': name, 'stock': stock}
525
+ for name, stock in combined_warehouses.items()
526
+ if stock > 0
527
+ ]
528
+
529
+ return main_product
530
+
531
+ # Öncelikle ana ürünleri kullan, Ana ürün yoksa varyantları göster
532
+ if main_products:
533
+ # Ana ürünleri varyant stoklarıyla zenginleştir
534
+ enriched_main_products = []
535
+ for main_product in main_products:
536
+ enriched = enrich_main_product_with_variant_stock(main_product, results)
537
+ enriched_main_products.append(enriched)
538
+
539
+ # Basit sorgu (örn: "marlin 4") için sadece en iyi ana ürün
540
+ query_words = normalize_turkish(query).upper().split()
541
+ product_query_words = [w for w in query_words if w not in ['FIYAT', 'STOK', 'VAR', 'MI', 'RENK', 'BEDEN']]
542
+ is_simple_product_query = len(product_query_words) <= 2
543
+
544
+ if is_simple_product_query:
545
+ selected_products = enriched_main_products[:1] # Sadece en iyi ana ürün
546
+ else:
547
+ selected_products = enriched_main_products[:3] # Birden fazla ana ürün
548
+ else:
549
+ # Ana ürün yoksa varyantları göster
550
+ selected_products = variant_products[:3]
551
+
552
+ formatted_results = []
553
+ for product in selected_products:
554
+ # Ana ürün ise varyant detaylarını göster
555
+ show_variants = is_main_product(product['name'])
556
+ formatted = format_product_result_whatsapp(product, show_variants_info=show_variants, all_results=results)
557
+ formatted_results.append(formatted)
558
+
559
+ return formatted_results
560
+
561
+ # Test fonksiyonu
562
+ if __name__ == "__main__":
563
+ # Test arama
564
+ logging.basicConfig(level=logging.INFO)
565
+
566
+ print("Combined API Test Başlıyor...")
567
+
568
+ test_queries = ["marlin", "trek tool", "bisiklet", "madone"]
569
+
570
+ for query in test_queries:
571
+ print(f"\n🔍 Test: '{query}'")
572
+ results = search_products_combined_api(query)
573
+
574
+ if results:
575
+ print(f"✅ {len(results)} sonuç bulundu")
576
+ for i, product in enumerate(results[:2]):
577
+ print(f"\n{i+1}. {product['name']}")
578
+ print(f" Fiyat: {product['regular_price']} TL")
579
+ print(f" Stok: {'✅' if product['stock_found'] else '❌'} ({product['total_stock']} adet)")
580
+ else:
581
+ print("❌ Sonuç bulunamadı")
customer_manager.py ADDED
@@ -0,0 +1,456 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ CRM (Customer Relationship Management) System
3
+ Müşteri tanıma, segmentasyon ve kişiselleştirilmiş yanıtlar
4
+ """
5
+ import json
6
+ import os
7
+ from datetime import datetime, timedelta
8
+ from typing import Dict, List, Optional, Tuple
9
+ import re
10
+
11
+ class CustomerManager:
12
+ def __init__(self, db_path: str = "customers.json"):
13
+ self.db_path = db_path
14
+ self.customers = self._load_database()
15
+
16
+ def _load_database(self) -> Dict:
17
+ """Müşteri veritabanını yükle"""
18
+ if os.path.exists(self.db_path):
19
+ try:
20
+ with open(self.db_path, 'r', encoding='utf-8') as f:
21
+ return json.load(f)
22
+ except:
23
+ return {}
24
+ return {}
25
+
26
+ def _save_database(self):
27
+ """Veritabanını kaydet"""
28
+ with open(self.db_path, 'w', encoding='utf-8') as f:
29
+ json.dump(self.customers, f, ensure_ascii=False, indent=2)
30
+
31
+ def _extract_name_from_message(self, message: str) -> Optional[str]:
32
+ """Mesajdan isim çıkarmaya çalış"""
33
+ # "Ben Ahmet", "Benim adım Mehmet", "Merhaba ben Ali" gibi kalıplar
34
+ patterns = [
35
+ r"ben[im]?\s+ad[ıi]m?\s+(\w+)",
36
+ r"ben\s+(\w+)",
37
+ r"ad[ıi]m\s+(\w+)",
38
+ r"(\w+)\s+diye",
39
+ ]
40
+
41
+ message_lower = message.lower()
42
+ for pattern in patterns:
43
+ match = re.search(pattern, message_lower)
44
+ if match:
45
+ name = match.group(1)
46
+ # İlk harfi büyük yap
47
+ return name.capitalize()
48
+ return None
49
+
50
+ def _detect_interests(self, message: str) -> List[str]:
51
+ """Mesajdan ilgi alanlarını çıkar"""
52
+ interests = []
53
+ message_lower = message.lower()
54
+
55
+ # Bisiklet kategorileri
56
+ if any(word in message_lower for word in ["dağ", "mountain", "mtb"]):
57
+ interests.append("mountain_bike")
58
+ if any(word in message_lower for word in ["yol", "road", "yarış"]):
59
+ interests.append("road_bike")
60
+ if any(word in message_lower for word in ["şehir", "city", "urban"]):
61
+ interests.append("city_bike")
62
+
63
+ # Marka ve modeller
64
+ if "trek" in message_lower:
65
+ interests.append("trek")
66
+ if "marlin" in message_lower:
67
+ interests.append("marlin_series")
68
+ if "fx" in message_lower:
69
+ interests.append("fx_series")
70
+ if "domane" in message_lower:
71
+ interests.append("domane_series")
72
+ if "madone" in message_lower:
73
+ interests.append("madone_series")
74
+
75
+ # Özellikler
76
+ if any(word in message_lower for word in ["elektrik", "e-bike", "ebike"]):
77
+ interests.append("e_bike")
78
+ if any(word in message_lower for word in ["karbon", "carbon"]):
79
+ interests.append("carbon")
80
+ if any(word in message_lower for word in ["kadın", "women", "wsd"]):
81
+ interests.append("women_specific")
82
+
83
+ # Bütçe
84
+ if any(word in message_lower for word in ["ucuz", "uygun fiyat", "ekonomik"]):
85
+ interests.append("budget_friendly")
86
+ if any(word in message_lower for word in ["premium", "high-end", "üst segment"]):
87
+ interests.append("premium")
88
+
89
+ return interests
90
+
91
+ def _calculate_segment(self, customer: Dict) -> str:
92
+ """Müşteri segmentini hesapla"""
93
+ total_purchases = len(customer.get("purchases", []))
94
+ total_spent = sum(p.get("price", 0) for p in customer.get("purchases", []))
95
+ total_queries = customer.get("total_queries", 0)
96
+
97
+ # Son aktivite kontrolü
98
+ last_interaction = datetime.fromisoformat(customer.get("last_interaction", datetime.now().isoformat()))
99
+ days_since_last = (datetime.now() - last_interaction).days
100
+
101
+ # VIP kriterler
102
+ if total_purchases >= 3 or total_spent >= 50000:
103
+ return "VIP"
104
+
105
+ # Potansiyel müşteri
106
+ if total_queries >= 5 and total_purchases == 0:
107
+ return "Potansiyel"
108
+
109
+ # Kayıp riski
110
+ if days_since_last > 30 and total_purchases > 0:
111
+ return "Kayıp Riski"
112
+
113
+ # Yeni müşteri (ilk 7 gün)
114
+ first_seen = datetime.fromisoformat(customer.get("first_seen", datetime.now().isoformat()))
115
+ if (datetime.now() - first_seen).days <= 7:
116
+ return "Yeni"
117
+
118
+ # Aktif müşteri
119
+ if days_since_last <= 7:
120
+ return "Aktif"
121
+
122
+ # Standart
123
+ return "Standart"
124
+
125
+ def get_or_create_customer(self, phone: str, name: Optional[str] = None) -> Dict:
126
+ """Müşteriyi getir veya yeni oluştur"""
127
+ if phone in self.customers:
128
+ # Var olan müşteri
129
+ customer = self.customers[phone]
130
+ customer["last_interaction"] = datetime.now().isoformat()
131
+
132
+ # İsim güncellemesi
133
+ if name and not customer.get("name"):
134
+ customer["name"] = name
135
+
136
+ # Segment güncelle
137
+ customer["segment"] = self._calculate_segment(customer)
138
+
139
+ else:
140
+ # Yeni müşteri
141
+ customer = {
142
+ "phone": phone,
143
+ "name": name,
144
+ "first_seen": datetime.now().isoformat(),
145
+ "last_interaction": datetime.now().isoformat(),
146
+ "total_queries": 0,
147
+ "purchases": [],
148
+ "interests": [],
149
+ "searched_products": [],
150
+ "segment": "Yeni",
151
+ "notes": [],
152
+ "preferred_contact_time": None # "morning", "afternoon", "evening"
153
+ }
154
+ self.customers[phone] = customer
155
+
156
+ self._save_database()
157
+ return customer
158
+
159
+ def update_interaction(self, phone: str, message: str, product_searched: Optional[str] = None) -> Dict:
160
+ """Müşteri etkileşimini güncelle"""
161
+ customer = self.get_or_create_customer(phone)
162
+
163
+ # Sorgu sayısını artır
164
+ customer["total_queries"] = customer.get("total_queries", 0) + 1
165
+
166
+ # İsim çıkarma denemesi
167
+ if not customer.get("name"):
168
+ extracted_name = self._extract_name_from_message(message)
169
+ if extracted_name:
170
+ customer["name"] = extracted_name
171
+
172
+ # İlgi alanlarını güncelle
173
+ new_interests = self._detect_interests(message)
174
+ existing_interests = set(customer.get("interests", []))
175
+ customer["interests"] = list(existing_interests.union(set(new_interests)))
176
+
177
+ # Aranan ürünü kaydet
178
+ if product_searched:
179
+ searched_products = customer.get("searched_products", [])
180
+ # Son 10 aramayı tut
181
+ searched_products.append({
182
+ "product": product_searched,
183
+ "date": datetime.now().isoformat()
184
+ })
185
+ customer["searched_products"] = searched_products[-10:]
186
+
187
+ # İletişim zamanı tercihi
188
+ hour = datetime.now().hour
189
+ if 6 <= hour < 12:
190
+ time_pref = "morning"
191
+ elif 12 <= hour < 18:
192
+ time_pref = "afternoon"
193
+ else:
194
+ time_pref = "evening"
195
+
196
+ # En çok hangi zaman diliminde yazıyor
197
+ if not customer.get("preferred_contact_time"):
198
+ customer["preferred_contact_time"] = time_pref
199
+
200
+ # Segment güncelle
201
+ customer["segment"] = self._calculate_segment(customer)
202
+
203
+ # Son etkileşim zamanı
204
+ customer["last_interaction"] = datetime.now().isoformat()
205
+
206
+ self._save_database()
207
+ return customer
208
+
209
+ def add_purchase(self, phone: str, product: str, price: float) -> Dict:
210
+ """Satın alma kaydı ekle"""
211
+ customer = self.get_or_create_customer(phone)
212
+
213
+ purchase = {
214
+ "product": product,
215
+ "price": price,
216
+ "date": datetime.now().isoformat()
217
+ }
218
+
219
+ customer.setdefault("purchases", []).append(purchase)
220
+ customer["segment"] = self._calculate_segment(customer)
221
+
222
+ self._save_database()
223
+ return customer
224
+
225
+ def get_customer_context(self, phone: str) -> Dict:
226
+ """Müşteri bağlamını getir"""
227
+ customer = self.customers.get(phone, {})
228
+
229
+ if not customer:
230
+ return {
231
+ "is_new": True,
232
+ "greeting": "Merhaba! Trek bisikletleri hakkında size nasıl yardımcı olabilirim?"
233
+ }
234
+
235
+ context = {
236
+ "is_new": False,
237
+ "name": customer.get("name"),
238
+ "segment": customer.get("segment", "Standart"),
239
+ "total_queries": customer.get("total_queries", 0),
240
+ "interests": customer.get("interests", []),
241
+ "last_product": None,
242
+ "days_since_last": 0,
243
+ "greeting": ""
244
+ }
245
+
246
+ # Son aranan ürün
247
+ searched_products = customer.get("searched_products", [])
248
+ if searched_products:
249
+ context["last_product"] = searched_products[-1]["product"]
250
+ last_search_date = datetime.fromisoformat(searched_products[-1]["date"])
251
+ context["days_since_last"] = (datetime.now() - last_search_date).days
252
+
253
+ # Kişiselleştirilmiş selamlama oluştur
254
+ context["greeting"] = self._generate_greeting(customer, context)
255
+
256
+ return context
257
+
258
+ def _generate_greeting(self, customer: Dict, context: Dict) -> str:
259
+ """Kişiselleştirilmiş selamlama oluştur"""
260
+ segment = customer.get("segment", "Standart")
261
+ name = customer.get("name", "")
262
+
263
+ # Saat bazlı selamlama
264
+ hour = datetime.now().hour
265
+ if 6 <= hour < 12:
266
+ time_greeting = "Günaydın"
267
+ elif 12 <= hour < 18:
268
+ time_greeting = "İyi günler"
269
+ else:
270
+ time_greeting = "İyi akşamlar"
271
+
272
+ # Segment bazlı selamlama
273
+ if segment == "VIP":
274
+ if name:
275
+ greeting = f"{time_greeting} {name} Bey/Hanım! Değerli müşterimiz olarak size özel VIP avantajlarımız mevcut."
276
+ else:
277
+ greeting = f"{time_greeting}! Değerli müşterimize özel VIP avantajlarımız mevcut."
278
+
279
+ elif segment == "Yeni":
280
+ greeting = f"{time_greeting}! Trek bisikletleri dünyasına hoş geldiniz! Size nasıl yardımcı olabilirim?"
281
+
282
+ elif segment == "Potansiyel":
283
+ if context.get("last_product"):
284
+ greeting = f"{time_greeting}! Daha önce sorduğunuz {context['last_product']} veya başka bir konuda size yardımcı olabilir miyim?"
285
+ else:
286
+ greeting = f"{time_greeting}! Bisiklet arayışınızda size nasıl yardımcı olabilirim?"
287
+
288
+ elif segment == "Kayıp Riski":
289
+ if name:
290
+ greeting = f"{time_greeting} {name} Bey/Hanım! Sizi özledik! Size özel geri dönüş kampanyamız var."
291
+ else:
292
+ greeting = f"{time_greeting}! Sizi tekrar aramızda görmek güzel! Size özel fırsatlarımız var."
293
+
294
+ elif segment == "Aktif":
295
+ if name:
296
+ greeting = f"{time_greeting} {name} Bey/Hanım! Tekrar hoş geldiniz!"
297
+ else:
298
+ greeting = f"{time_greeting}! Tekrar hoş geldiniz!"
299
+
300
+ # Son aranan ürün varsa ekle
301
+ if context.get("last_product") and context.get("days_since_last", 99) <= 3:
302
+ greeting += f" {context['last_product']} hakkında bilgi almak ister misiniz?"
303
+
304
+ else: # Standart
305
+ if name:
306
+ greeting = f"{time_greeting} {name} Bey/Hanım! Size nasıl yardımcı olabilirim?"
307
+ else:
308
+ greeting = f"{time_greeting}! Size nasıl yardımcı olabilirim?"
309
+
310
+ return greeting
311
+
312
+ def get_analytics(self) -> Dict:
313
+ """Analitik özet döndür"""
314
+ total_customers = len(self.customers)
315
+ segments = {}
316
+ total_purchases = 0
317
+ total_revenue = 0
318
+ active_today = 0
319
+
320
+ today = datetime.now().date()
321
+
322
+ for customer in self.customers.values():
323
+ # Segment dağılımı
324
+ segment = customer.get("segment", "Standart")
325
+ segments[segment] = segments.get(segment, 0) + 1
326
+
327
+ # Satış istatistikleri
328
+ purchases = customer.get("purchases", [])
329
+ total_purchases += len(purchases)
330
+ total_revenue += sum(p.get("price", 0) for p in purchases)
331
+
332
+ # Bugün aktif olanlar
333
+ last_interaction = datetime.fromisoformat(customer.get("last_interaction", "2020-01-01"))
334
+ if last_interaction.date() == today:
335
+ active_today += 1
336
+
337
+ return {
338
+ "total_customers": total_customers,
339
+ "segments": segments,
340
+ "total_purchases": total_purchases,
341
+ "total_revenue": total_revenue,
342
+ "active_today": active_today,
343
+ "average_purchase_value": total_revenue / total_purchases if total_purchases > 0 else 0
344
+ }
345
+
346
+ def get_customer_list(self, segment: Optional[str] = None) -> List[Dict]:
347
+ """Müşteri listesini getir"""
348
+ customers = []
349
+
350
+ for phone, customer in self.customers.items():
351
+ if segment and customer.get("segment") != segment:
352
+ continue
353
+
354
+ customers.append({
355
+ "phone": phone,
356
+ "name": customer.get("name", "Bilinmiyor"),
357
+ "segment": customer.get("segment", "Standart"),
358
+ "total_queries": customer.get("total_queries", 0),
359
+ "total_purchases": len(customer.get("purchases", [])),
360
+ "last_interaction": customer.get("last_interaction"),
361
+ "interests": customer.get("interests", [])
362
+ })
363
+
364
+ # Son etkileşime göre sırala
365
+ customers.sort(key=lambda x: x["last_interaction"], reverse=True)
366
+ return customers
367
+
368
+ def should_ask_for_name(self, phone: str, message: str) -> Tuple[bool, Optional[str]]:
369
+ """İsim sorulmalı mı kontrol et"""
370
+ customer = self.customers.get(phone, {})
371
+
372
+ # Zaten isim varsa sorma
373
+ if customer.get("name"):
374
+ return False, None
375
+
376
+ queries = customer.get("total_queries", 0)
377
+ message_lower = message.lower()
378
+
379
+ # 1. Rezervasyon/satın alma anında (EN DOĞAL)
380
+ reservation_keywords = ["rezerv", "ayırt", "satın al", "alabilir miyim", "almak istiyorum",
381
+ "sipariş", "kargola", "gönder", "paket", "teslimat"]
382
+ if any(keyword in message_lower for keyword in reservation_keywords):
383
+ return True, "📝 Rezervasyon için adınızı alabilir miyim?"
384
+
385
+ # 2. Mağaza ziyareti planı
386
+ store_visit_keywords = ["mağazaya gel", "mağazaya uğra", "görmeye gel", "bakmaya gel",
387
+ "test sürüşü", "denemek istiyorum"]
388
+ if any(keyword in message_lower for keyword in store_visit_keywords):
389
+ return True, "🏪 Mağazada size daha iyi yardımcı olabilmemiz için adınızı öğrenebilir miyim?"
390
+
391
+ # 3. Telefon görüşmesi talebi
392
+ phone_keywords = ["ara", "telefon", "görüş", "konuş", "iletişim"]
393
+ if any(keyword in message_lower for keyword in phone_keywords):
394
+ return True, "📞 Sizi arayabilmemiz için adınızı paylaşır mısınız?"
395
+
396
+ # 4. Fiyat/ödeme görüşmesi (ciddi alıcı)
397
+ payment_keywords = ["taksit", "ödeme", "kredi kartı", "havale", "eft", "nakit", "peşin"]
398
+ if any(keyword in message_lower for keyword in payment_keywords):
399
+ return True, "💳 Size özel ödeme seçenekleri sunabilmem için adınızı öğrenebilir miyim?"
400
+
401
+ # 5. 3. mesajda nazikçe sor (sadece ürün sorguluyorsa)
402
+ if queries == 2: # 3. mesaj (0'dan başlıyor)
403
+ # Sadece basit selamlaşma değilse
404
+ greeting_only = ["merhaba", "selam", "günaydın", "iyi günler", "hey", "sa"]
405
+ if not any(message_lower.strip() == greeting for greeting in greeting_only):
406
+ return True, "Bu arada, size daha iyi hizmet verebilmem için adınızı öğrenebilir miyim? 😊"
407
+
408
+ # 6. 5+ sorgudan sonra hala isim yoksa (potansiyel müşteri)
409
+ if queries >= 5:
410
+ return True, "✨ Sizin için özel tekliflerimiz olabilir. Adınızı paylaşır mısınız?"
411
+
412
+ return False, None
413
+
414
+ def extract_name_from_response(self, message: str) -> Optional[str]:
415
+ """İsim sorulduktan sonra gelen yanıttan isim çıkar"""
416
+ message_lower = message.lower().strip()
417
+
418
+ # Direkt isim yanıtları
419
+ if len(message_lower.split()) == 1:
420
+ # Yaygın olmayan tek kelime yanıtları filtrele
421
+ common_words = ["evet", "hayır", "tamam", "olur", "peki", "teşekkür", "teşekkürler",
422
+ "merhaba", "selam", "günaydın", "güle", "hoşçakal", "görüşürüz",
423
+ "sa", "as", "slm", "mrb", "ok", "okay", "yes", "no", "hi", "hello", "bye"]
424
+ if message_lower not in common_words:
425
+ # Tek kelime, muhtemelen isim
426
+ name = message.capitalize()
427
+ # Türkçe isim kontrolü (en az 2 karakter)
428
+ if len(name) >= 2 and name.isalpha():
429
+ return name
430
+
431
+ # "Benim adım X", "Ben X", "Adım X" kalıpları
432
+ patterns = [
433
+ r"(?:benim\s+)?ad[ıi]m?\s+(\w+)",
434
+ r"ben\s+(\w+)",
435
+ r"(\w+)\s+diye\s+hitap",
436
+ r"bana\s+(\w+)\s+diyebilirsiniz",
437
+ r"ismim\s+(\w+)",
438
+ r"(\w+),?\s*teşekkür", # "Ahmet, teşekkürler"
439
+ ]
440
+
441
+ for pattern in patterns:
442
+ import re
443
+ match = re.search(pattern, message_lower)
444
+ if match:
445
+ name = match.group(1).capitalize()
446
+ if len(name) >= 2:
447
+ return name
448
+
449
+ # İsim + Soyisim durumu (sadece ismi al)
450
+ two_words = message.strip().split()
451
+ if len(two_words) == 2:
452
+ # Her ikisi de büyük harfle başlıyorsa
453
+ if two_words[0][0].isupper() and two_words[1][0].isupper():
454
+ return two_words[0]
455
+
456
+ return None
follow_up_system.py ADDED
@@ -0,0 +1,407 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ Otomatik Takip ve Hatırlatma Sistemi
6
+ Müşteri taahhütlerini takip eder ve Mehmet Bey'e hatırlatır
7
+ """
8
+
9
+ import os
10
+ import json
11
+ import logging
12
+ from datetime import datetime, timedelta
13
+ from typing import Optional, List, Dict
14
+ from dataclasses import dataclass, asdict
15
+ from enum import Enum
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ # Takip tipi
20
+ class FollowUpType(Enum):
21
+ RESERVATION = "reservation" # Ayırtma takibi
22
+ VISIT_PROMISE = "visit" # Gelme sözü takibi
23
+ PRICE_INQUIRY = "price" # Fiyat sorusu takibi
24
+ DECISION_PENDING = "decision" # Karar bekleyen
25
+ TEST_RIDE = "test_ride" # Test sürüşü
26
+
27
+ # Takip durumu
28
+ class FollowUpStatus(Enum):
29
+ PENDING = "pending" # Bekliyor
30
+ REMINDED = "reminded" # Hatırlatma yapıldı
31
+ COMPLETED = "completed" # Tamamlandı
32
+ CANCELLED = "cancelled" # İptal edildi
33
+
34
+ @dataclass
35
+ class FollowUp:
36
+ """Takip kaydı"""
37
+ id: str # Unique ID
38
+ customer_phone: str # Müşteri telefonu
39
+ customer_name: Optional[str] # Müşteri adı
40
+ product_name: str # Ürün
41
+ follow_up_type: str # Takip tipi
42
+ status: str # Durum
43
+ created_at: str # Oluşturulma zamanı
44
+ follow_up_at: str # Hatırlatma zamanı
45
+ original_message: str # Orijinal mesaj
46
+ notes: Optional[str] # Notlar
47
+ store_name: Optional[str] # Mağaza
48
+ reminded_count: int = 0 # Kaç kez hatırlatıldı
49
+
50
+ class FollowUpManager:
51
+ """Takip yöneticisi"""
52
+
53
+ def __init__(self, db_file: str = "follow_ups.json"):
54
+ self.db_file = db_file
55
+ self.follow_ups = self._load_database()
56
+
57
+ def _load_database(self) -> List[FollowUp]:
58
+ """Database'i yükle"""
59
+ if os.path.exists(self.db_file):
60
+ try:
61
+ with open(self.db_file, "r") as f:
62
+ data = json.load(f)
63
+ return [FollowUp(**item) for item in data]
64
+ except Exception as e:
65
+ logger.error(f"Database yükleme hatası: {e}")
66
+ return []
67
+ return []
68
+
69
+ def _save_database(self):
70
+ """Database'i kaydet"""
71
+ try:
72
+ data = [asdict(f) for f in self.follow_ups]
73
+ with open(self.db_file, "w") as f:
74
+ json.dump(data, f, indent=2, ensure_ascii=False)
75
+ except Exception as e:
76
+ logger.error(f"Database kayıt hatası: {e}")
77
+
78
+ def create_follow_up(
79
+ self,
80
+ customer_phone: str,
81
+ product_name: str,
82
+ follow_up_type: FollowUpType,
83
+ original_message: str,
84
+ follow_up_hours: int = 24,
85
+ customer_name: Optional[str] = None,
86
+ notes: Optional[str] = None,
87
+ store_name: Optional[str] = None
88
+ ) -> FollowUp:
89
+ """Yeni takip oluştur"""
90
+
91
+ now = datetime.now()
92
+ follow_up_time = now + timedelta(hours=follow_up_hours)
93
+
94
+ # Unique ID oluştur
95
+ follow_up_id = f"{customer_phone}_{now.strftime('%Y%m%d_%H%M%S')}"
96
+
97
+ follow_up = FollowUp(
98
+ id=follow_up_id,
99
+ customer_phone=customer_phone,
100
+ customer_name=customer_name,
101
+ product_name=product_name,
102
+ follow_up_type=follow_up_type.value,
103
+ status=FollowUpStatus.PENDING.value,
104
+ created_at=now.isoformat(),
105
+ follow_up_at=follow_up_time.isoformat(),
106
+ original_message=original_message,
107
+ notes=notes,
108
+ store_name=store_name,
109
+ reminded_count=0
110
+ )
111
+
112
+ self.follow_ups.append(follow_up)
113
+ self._save_database()
114
+
115
+ logger.info(f"✅ Takip oluşturuldu: {follow_up_id}")
116
+ logger.info(f" Hatırlatma zamanı: {follow_up_time.strftime('%d.%m.%Y %H:%M')}")
117
+
118
+ return follow_up
119
+
120
+ def get_pending_reminders(self) -> List[FollowUp]:
121
+ """Bekleyen hatırlatmaları getir"""
122
+ now = datetime.now()
123
+ pending = []
124
+
125
+ for follow_up in self.follow_ups:
126
+ if follow_up.status == FollowUpStatus.PENDING.value:
127
+ follow_up_time = datetime.fromisoformat(follow_up.follow_up_at)
128
+ if follow_up_time <= now:
129
+ pending.append(follow_up)
130
+
131
+ return pending
132
+
133
+ def mark_as_reminded(self, follow_up_id: str):
134
+ """Hatırlatma yapıldı olarak işaretle"""
135
+ for follow_up in self.follow_ups:
136
+ if follow_up.id == follow_up_id:
137
+ follow_up.status = FollowUpStatus.REMINDED.value
138
+ follow_up.reminded_count += 1
139
+ self._save_database()
140
+ logger.info(f"✅ Hatırlatma yapıldı: {follow_up_id}")
141
+ break
142
+
143
+ def mark_as_completed(self, follow_up_id: str):
144
+ """Tamamlandı olarak işaretle"""
145
+ for follow_up in self.follow_ups:
146
+ if follow_up.id == follow_up_id:
147
+ follow_up.status = FollowUpStatus.COMPLETED.value
148
+ self._save_database()
149
+ logger.info(f"✅ Takip tamamlandı: {follow_up_id}")
150
+ break
151
+
152
+ def get_customer_history(self, customer_phone: str) -> List[FollowUp]:
153
+ """Müşteri geçmişini getir"""
154
+ return [f for f in self.follow_ups if f.customer_phone == customer_phone]
155
+
156
+ def get_todays_follow_ups(self) -> List[FollowUp]:
157
+ """Bugünün takiplerini getir"""
158
+ today = datetime.now().date()
159
+ todays = []
160
+
161
+ for follow_up in self.follow_ups:
162
+ follow_up_date = datetime.fromisoformat(follow_up.follow_up_at).date()
163
+ if follow_up_date == today:
164
+ todays.append(follow_up)
165
+
166
+ return todays
167
+
168
+ def analyze_message_for_follow_up(message: str) -> Optional[Dict]:
169
+ """
170
+ Mesajı analiz et ve takip gerekip gerekmediğini belirle
171
+
172
+ Returns:
173
+ {
174
+ "needs_follow_up": True/False,
175
+ "follow_up_type": FollowUpType,
176
+ "follow_up_hours": int,
177
+ "reason": str
178
+ }
179
+ """
180
+
181
+ message_lower = message.lower()
182
+
183
+ # YARIN gelme sözleri (24 saat sonra hatırlat)
184
+ tomorrow_keywords = [
185
+ 'yarın', 'yarin',
186
+ 'yarın gel', 'yarın al', 'yarın uğra',
187
+ 'yarına', 'yarina',
188
+ 'ertesi gün'
189
+ ]
190
+
191
+ # BUGÜN gelme sözleri (6 saat sonra hatırlat)
192
+ today_keywords = [
193
+ 'bugün', 'bugun',
194
+ 'bugün gel', 'bugün al', 'bugün uğra',
195
+ 'akşam gel', 'aksam gel',
196
+ 'öğleden sonra', 'ogleden sonra',
197
+ 'birazdan', 'biraz sonra',
198
+ '1 saat', 'bir saat',
199
+ '2 saat', 'iki saat',
200
+ '30 dakika', 'yarım saat'
201
+ ]
202
+
203
+ # HAFTA SONU gelme sözleri
204
+ weekend_keywords = [
205
+ 'hafta sonu', 'haftasonu',
206
+ 'cumartesi', 'pazar',
207
+ 'hafta içi', 'haftaiçi'
208
+ ]
209
+
210
+ # KARAR VERME sözleri (48 saat sonra hatırlat)
211
+ decision_keywords = [
212
+ 'düşüneyim', 'düşüncem', 'düşünelim',
213
+ 'danışayım', 'danısayım', 'danışıcam',
214
+ 'eşime sor', 'eşimle konuş', 'esime sor',
215
+ 'karar ver', 'haber ver',
216
+ 'araştırayım', 'araştırıcam',
217
+ 'bakarım', 'bakarız', 'bakıcam'
218
+ ]
219
+
220
+ # TEST SÜRÜŞÜ (4 saat sonra hatırlat)
221
+ test_keywords = [
222
+ 'test sürüş', 'test et', 'dene',
223
+ 'binebilir', 'binmek ist',
224
+ 'test ride', 'deneme sürüş'
225
+ ]
226
+
227
+ # Yarın kontrolü
228
+ for keyword in tomorrow_keywords:
229
+ if keyword in message_lower:
230
+ return {
231
+ "needs_follow_up": True,
232
+ "follow_up_type": FollowUpType.VISIT_PROMISE,
233
+ "follow_up_hours": 24,
234
+ "reason": f"Müşteri yarın geleceğini söyledi: '{keyword}'"
235
+ }
236
+
237
+ # Bugün kontrolü
238
+ for keyword in today_keywords:
239
+ if keyword in message_lower:
240
+ return {
241
+ "needs_follow_up": True,
242
+ "follow_up_type": FollowUpType.VISIT_PROMISE,
243
+ "follow_up_hours": 6,
244
+ "reason": f"Müşteri bugün geleceğini söyledi: '{keyword}'"
245
+ }
246
+
247
+ # Hafta sonu kontrolü
248
+ for keyword in weekend_keywords:
249
+ if keyword in message_lower:
250
+ # Bugün hangi gün?
251
+ today = datetime.now().weekday() # 0=Pazartesi, 6=Pazar
252
+ if today < 5: # Hafta içiyse
253
+ days_to_saturday = 5 - today
254
+ hours = days_to_saturday * 24
255
+ else: # Zaten hafta sonuysa
256
+ hours = 24
257
+
258
+ return {
259
+ "needs_follow_up": True,
260
+ "follow_up_type": FollowUpType.VISIT_PROMISE,
261
+ "follow_up_hours": hours,
262
+ "reason": f"Müşteri hafta sonu geleceğini söyledi: '{keyword}'"
263
+ }
264
+
265
+ # Karar verme kontrolü
266
+ for keyword in decision_keywords:
267
+ if keyword in message_lower:
268
+ return {
269
+ "needs_follow_up": True,
270
+ "follow_up_type": FollowUpType.DECISION_PENDING,
271
+ "follow_up_hours": 48,
272
+ "reason": f"Müşteri düşüneceğini söyledi: '{keyword}'"
273
+ }
274
+
275
+ # Test sürüşü kontrolü
276
+ for keyword in test_keywords:
277
+ if keyword in message_lower:
278
+ return {
279
+ "needs_follow_up": True,
280
+ "follow_up_type": FollowUpType.TEST_RIDE,
281
+ "follow_up_hours": 4,
282
+ "reason": f"Müşteri test sürüşü istiyor: '{keyword}'"
283
+ }
284
+
285
+ # Ayırtma varsa 24 saat sonra kontrol
286
+ reservation_keywords = ['ayırt', 'rezerve', 'tutun', 'sakla']
287
+ for keyword in reservation_keywords:
288
+ if keyword in message_lower:
289
+ return {
290
+ "needs_follow_up": True,
291
+ "follow_up_type": FollowUpType.RESERVATION,
292
+ "follow_up_hours": 24,
293
+ "reason": f"Ayırtma talebi var: '{keyword}'"
294
+ }
295
+
296
+ return None
297
+
298
+ def format_reminder_message(follow_up: FollowUp) -> str:
299
+ """Hatırlatma mesajını formatla"""
300
+
301
+ # Takip tipine göre emoji
302
+ type_emojis = {
303
+ "reservation": "📦",
304
+ "visit": "🚶",
305
+ "price": "💰",
306
+ "decision": "🤔",
307
+ "test_ride": "🚴"
308
+ }
309
+
310
+ emoji = type_emojis.get(follow_up.follow_up_type, "📌")
311
+
312
+ # Zaman hesaplama
313
+ created_time = datetime.fromisoformat(follow_up.created_at)
314
+ time_diff = datetime.now() - created_time
315
+
316
+ if time_diff.days > 0:
317
+ time_str = f"{time_diff.days} gün önce"
318
+ elif time_diff.seconds > 3600:
319
+ hours = time_diff.seconds // 3600
320
+ time_str = f"{hours} saat önce"
321
+ else:
322
+ time_str = "Az önce"
323
+
324
+ # Mesaj oluştur
325
+ message = f"""
326
+ {emoji} **TAKİP HATIRLATMASI**
327
+
328
+ 👤 Müşteri: {follow_up.customer_name or "İsimsiz"}
329
+ 📱 Tel: {follow_up.customer_phone.replace('whatsapp:', '')}
330
+
331
+ 🚲 Ürün: {follow_up.product_name}
332
+ ⏰ İlk mesaj: {time_str}
333
+
334
+ 📝 Müşteri mesajı:
335
+ "{follow_up.original_message}"
336
+ """
337
+
338
+ # Tipe göre özel mesaj
339
+ if follow_up.follow_up_type == "reservation":
340
+ message += "\n⚠️ AYIRTMA TAKİBİ: Müşteri geldi mi?"
341
+ elif follow_up.follow_up_type == "visit":
342
+ message += "\n⚠️ ZİYARET TAKİBİ: Müşteri geleceğini söylemişti"
343
+ elif follow_up.follow_up_type == "decision":
344
+ message += "\n⚠️ KARAR TAKİBİ: Müşteri düşüneceğini söylemişti"
345
+ elif follow_up.follow_up_type == "test_ride":
346
+ message += "\n⚠️ TEST SÜRÜŞÜ TAKİBİ: Test için gelecekti"
347
+
348
+ message += "\n\n📞 Müşteriyi arayıp durumu öğrenin!"
349
+
350
+ return message
351
+
352
+ # Test fonksiyonu
353
+ def test_follow_up_system():
354
+ """Test senaryoları"""
355
+
356
+ print("\n" + "="*60)
357
+ print("TAKİP SİSTEMİ TESTİ")
358
+ print("="*60)
359
+
360
+ manager = FollowUpManager("test_follow_ups.json")
361
+
362
+ # Test mesajları
363
+ test_messages = [
364
+ ("Yarın gelip FX 2'yi alacağım", "FX 2"),
365
+ ("Eşime danışayım, size dönerim", "Marlin 5"),
366
+ ("Bugün akşam uğrarım", "Checkpoint"),
367
+ ("Hafta sonu test sürüşü yapabilir miyim?", "Domane"),
368
+ ("30 dakikaya oradayım, ayırtın", "FX Sport")
369
+ ]
370
+
371
+ for message, product in test_messages:
372
+ print(f"\n📝 Mesaj: '{message}'")
373
+
374
+ analysis = analyze_message_for_follow_up(message)
375
+ if analysis and analysis["needs_follow_up"]:
376
+ print(f"✅ Takip gerekli!")
377
+ print(f" Tip: {analysis['follow_up_type'].value}")
378
+ print(f" {analysis['follow_up_hours']} saat sonra hatırlat")
379
+ print(f" Sebep: {analysis['reason']}")
380
+
381
+ # Takip oluştur
382
+ follow_up = manager.create_follow_up(
383
+ customer_phone="whatsapp:+905551234567",
384
+ product_name=product,
385
+ follow_up_type=analysis["follow_up_type"],
386
+ original_message=message,
387
+ follow_up_hours=analysis["follow_up_hours"]
388
+ )
389
+ else:
390
+ print("❌ Takip gerekmiyor")
391
+
392
+ # Bekleyen hatırlatmaları göster
393
+ print("\n" + "="*60)
394
+ print("BEKLEYEN HATIRLATMALAR")
395
+ print("="*60)
396
+
397
+ pending = manager.get_pending_reminders()
398
+ if pending:
399
+ for f in pending:
400
+ print(f"\n{format_reminder_message(f)}")
401
+ else:
402
+ print("Bekleyen hatırlatma yok")
403
+
404
+ print("\n" + "="*60)
405
+
406
+ if __name__ == "__main__":
407
+ test_follow_up_system()
get_warehouse_fast.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Ultra fast warehouse stock getter using regex"""
2
+
3
+ def get_warehouse_stock(product_name):
4
+ """Super fast warehouse stock finder using regex instead of XML parsing"""
5
+ try:
6
+ import re
7
+ import requests
8
+
9
+ # Fetch XML
10
+ warehouse_url = 'https://video.trek-turkey.com/bizimhesap-warehouse-xml-b2b-api-v2.php'
11
+ response = requests.get(warehouse_url, verify=False, timeout=7)
12
+
13
+ if response.status_code != 200:
14
+ return None
15
+
16
+ xml_text = response.text
17
+
18
+ # Turkish normalization
19
+ turkish_map = {'ı': 'i', 'ğ': 'g', 'ü': 'u', 'ş': 's', 'ö': 'o', 'ç': 'c', 'İ': 'i', 'I': 'i'}
20
+
21
+ def normalize_turkish(text):
22
+ text = text.lower()
23
+ for tr_char, en_char in turkish_map.items():
24
+ text = text.replace(tr_char, en_char)
25
+ return text
26
+
27
+ # Normalize search
28
+ search_name = normalize_turkish(product_name.strip())
29
+ search_name = search_name.replace('(2026)', '').replace('(2025)', '').strip()
30
+ search_words = search_name.split()
31
+
32
+ # Separate size from product words
33
+ size_words = ['s', 'm', 'l', 'xl', 'xs', 'xxl', 'ml']
34
+ size_indicators = ['beden', 'size', 'boy']
35
+
36
+ variant_filter = [w for w in search_words if w in size_words]
37
+ product_words = [w for w in search_words if w not in size_words and w not in size_indicators]
38
+
39
+ print(f"DEBUG - Looking for: {' '.join(product_words)}")
40
+ print(f"DEBUG - Size filter: {variant_filter}")
41
+
42
+ # Build exact pattern for common products
43
+ search_pattern = ' '.join(product_words).upper()
44
+
45
+ # Handle specific products
46
+ if 'madone' in product_words and 'sl' in product_words and '6' in product_words and 'gen' in product_words and '8' in product_words:
47
+ search_pattern = 'MADONE SL 6 GEN 8'
48
+ elif 'madone' in product_words and 'slr' in product_words and '7' in product_words:
49
+ search_pattern = 'MADONE SLR 7 GEN 8'
50
+ else:
51
+ # Generic pattern
52
+ search_pattern = search_pattern.replace('İ', 'I')
53
+
54
+ # Find all Product blocks with this name
55
+ # Using lazy quantifier .*? for efficiency
56
+ product_pattern = f'<Product>.*?<ProductName><!\\[CDATA\\[{re.escape(search_pattern)}\\]\\]></ProductName>.*?</Product>'
57
+ matches = re.findall(product_pattern, xml_text, re.DOTALL)
58
+
59
+ print(f"DEBUG - Found {len(matches)} products matching '{search_pattern}'")
60
+
61
+ # Process matches
62
+ warehouse_stock_map = {}
63
+
64
+ for match in matches:
65
+ # Extract variant if we need to filter by size
66
+ should_process = True
67
+
68
+ if variant_filter:
69
+ variant_match = re.search(r'<ProductVariant><!\\[CDATA\\[(.*?)\\]\\]></ProductVariant>', match)
70
+ if variant_match:
71
+ variant = variant_match.group(1)
72
+ size_wanted = variant_filter[0].upper()
73
+
74
+ # Check if variant starts with desired size
75
+ if variant.startswith(f'{size_wanted}-'):
76
+ print(f"DEBUG - Found matching variant: {variant}")
77
+ should_process = True
78
+ else:
79
+ should_process = False
80
+ else:
81
+ should_process = False
82
+
83
+ if should_process:
84
+ # Extract all warehouses with stock
85
+ warehouse_pattern = r'<Warehouse>.*?<Name><!\\[CDATA\\[(.*?)\\]\\]></Name>.*?<Stock>(.*?)</Stock>.*?</Warehouse>'
86
+ warehouses = re.findall(warehouse_pattern, match, re.DOTALL)
87
+
88
+ for wh_name, wh_stock in warehouses:
89
+ try:
90
+ stock = int(wh_stock.strip())
91
+ if stock > 0:
92
+ if wh_name in warehouse_stock_map:
93
+ warehouse_stock_map[wh_name] += stock
94
+ else:
95
+ warehouse_stock_map[wh_name] = stock
96
+ except:
97
+ pass
98
+
99
+ # If we found a variant match, stop looking
100
+ if variant_filter and should_process:
101
+ break
102
+
103
+ print(f"DEBUG - Warehouse stock: {warehouse_stock_map}")
104
+
105
+ # Format results
106
+ if warehouse_stock_map:
107
+ all_warehouse_info = []
108
+ for warehouse_name, total_stock in warehouse_stock_map.items():
109
+ # Make store names more readable
110
+ if "Caddebostan" in warehouse_name or "CADDEBOSTAN" in warehouse_name:
111
+ display_name = "Caddebostan mağazası"
112
+ elif "Ortaköy" in warehouse_name or "ORTAKÖY" in warehouse_name:
113
+ display_name = "Ortaköy mağazası"
114
+ elif "Sarıyer" in warehouse_name:
115
+ display_name = "Sarıyer mağazası"
116
+ elif "Alsancak" in warehouse_name or "ALSANCAK" in warehouse_name or "İzmir" in warehouse_name:
117
+ display_name = "İzmir Alsancak mağazası"
118
+ elif "BAHCEKOY" in warehouse_name or "Bahçeköy" in warehouse_name:
119
+ display_name = "Bahçeköy mağazası"
120
+ else:
121
+ display_name = warehouse_name
122
+
123
+ all_warehouse_info.append(f"{display_name}: Mevcut")
124
+ return all_warehouse_info
125
+ else:
126
+ return ["Hiçbir mağazada mevcut değil"]
127
+
128
+ except Exception as e:
129
+ print(f"Warehouse stock error: {e}")
130
+ return None
image_renderer.py ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Ürün Resimlerini Sohbette Gösterme Sistemi
4
+ """
5
+
6
+ def round_price(price_str):
7
+ """Fiyatı yuvarlama formülüne göre yuvarla"""
8
+ try:
9
+ # TL ve diğer karakterleri temizle
10
+ price_clean = price_str.replace(' TL', '').replace(',', '.')
11
+ price_float = float(price_clean)
12
+
13
+ # Fiyat 200000 üzerindeyse en yakın 5000'lik basamağa yuvarla
14
+ if price_float > 200000:
15
+ return str(round(price_float / 5000) * 5000)
16
+ # Fiyat 30000 üzerindeyse en yakın 1000'lik basamağa yuvarla
17
+ elif price_float > 30000:
18
+ return str(round(price_float / 1000) * 1000)
19
+ # Fiyat 10000 üzerindeyse en yakın 100'lük basamağa yuvarla
20
+ elif price_float > 10000:
21
+ return str(round(price_float / 100) * 100)
22
+ # Diğer durumlarda en yakın 10'luk basamağa yuvarla
23
+ else:
24
+ return str(round(price_float / 10) * 10)
25
+ except (ValueError, TypeError):
26
+ return price_str
27
+
28
+ def format_message_with_images(message):
29
+ """Mesajdaki resim URL'lerini HTML formatına çevir"""
30
+ if "Ürün resmi:" not in message:
31
+ return message
32
+
33
+ lines = message.split('\n')
34
+ formatted_lines = []
35
+
36
+ for line in lines:
37
+ if line.startswith("Ürün resmi:"):
38
+ image_url = line.replace("Ürün resmi:", "").strip()
39
+ if image_url:
40
+ # HTML img tag'i oluştur
41
+ img_html = f"""
42
+ <div style="margin: 10px 0;">
43
+ <img src="{image_url}"
44
+ alt="Ürün Resmi"
45
+ style="max-width: 300px; max-height: 200px; border-radius: 8px; border: 1px solid #ddd; box-shadow: 0 2px 4px rgba(0,0,0,0.1);"
46
+ onerror="this.style.display='none'">
47
+ </div>"""
48
+ formatted_lines.append(img_html)
49
+ else:
50
+ formatted_lines.append(line)
51
+ else:
52
+ formatted_lines.append(line)
53
+
54
+ return '\n'.join(formatted_lines)
55
+
56
+ def create_product_gallery(products_with_images):
57
+ """Birden fazla ürün için galeri oluştur"""
58
+ if not products_with_images:
59
+ return ""
60
+
61
+ gallery_html = """
62
+ <div style="display: flex; flex-wrap: wrap; gap: 15px; margin: 15px 0;">
63
+ """
64
+
65
+ for product in products_with_images:
66
+ name = product.get('name', 'Bilinmeyen')
67
+ price = product.get('price', 'Fiyat yok')
68
+ image_url = product.get('image_url', '')
69
+ product_url = product.get('product_url', '')
70
+
71
+ if image_url:
72
+ card_html = f"""
73
+ <div style="border: 1px solid #ddd; border-radius: 8px; padding: 10px; max-width: 200px; text-align: center; background: white; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
74
+ <img src="{image_url}"
75
+ alt="{name}"
76
+ style="max-width: 180px; max-height: 120px; border-radius: 4px; margin-bottom: 8px;"
77
+ onerror="this.style.display='none'">
78
+ <div style="font-size: 12px; font-weight: bold; margin-bottom: 4px;">{name}</div>
79
+ <div style="font-size: 11px; color: #666; margin-bottom: 8px;">{price}</div>
80
+ {f'<a href="{product_url}" target="_blank" style="font-size: 10px; color: #007bff; text-decoration: none;">Detaylı Bilgi</a>' if product_url else ''}
81
+ </div>"""
82
+ gallery_html += card_html
83
+
84
+ gallery_html += "</div>"
85
+ return gallery_html
86
+
87
+ def extract_product_info_for_gallery(message):
88
+ """Mesajdan ürün bilgilerini çıkarıp galeri formatına çevir"""
89
+ if "karşılaştır" in message.lower() or "öneri" in message.lower():
90
+ # Bu durumda galeri formatı kullan
91
+ lines = message.split('\n')
92
+ products = []
93
+
94
+ current_product = {}
95
+ for line in lines:
96
+ line = line.strip()
97
+ if line.startswith('•') and any(keyword in line.lower() for keyword in ['marlin', 'émonda', 'madone', 'domane', 'fuel', 'powerfly', 'fx']):
98
+ # Yeni ürün başladı
99
+ if current_product:
100
+ products.append(current_product)
101
+
102
+ # Ürün adı ve fiyatı parse et
103
+ parts = line.split(' - ')
104
+ name = parts[0].replace('•', '').strip()
105
+ price_raw = parts[1] if len(parts) > 1 else 'Fiyat yok'
106
+
107
+ # Fiyatı yuvarlama formülüne göre yuvarla
108
+ if price_raw != 'Fiyat yok':
109
+ price = round_price(price_raw) + ' TL'
110
+ else:
111
+ price = price_raw
112
+
113
+ current_product = {
114
+ 'name': name,
115
+ 'price': price,
116
+ 'image_url': '',
117
+ 'product_url': ''
118
+ }
119
+ elif "Ürün resmi:" in line and current_product:
120
+ current_product['image_url'] = line.replace("Ürün resmi:", "").strip()
121
+ elif "Ürün linki:" in line and current_product:
122
+ current_product['product_url'] = line.replace("Ür��n linki:", "").strip()
123
+
124
+ # Son ürünü ekle
125
+ if current_product:
126
+ products.append(current_product)
127
+
128
+ if products:
129
+ gallery = create_product_gallery(products)
130
+ # Orijinal mesajdaki resim linklerini temizle
131
+ cleaned_message = message
132
+ for line in message.split('\n'):
133
+ if line.startswith("Ürün resmi:") or line.startswith("Ürün linki:"):
134
+ cleaned_message = cleaned_message.replace(line, "")
135
+
136
+ return cleaned_message.strip() + "\n\n" + gallery
137
+
138
+ return format_message_with_images(message)
139
+
140
+ def should_use_gallery_format(message):
141
+ """Mesajın galeri formatı kullanması gerekip gerekmediğini kontrol et"""
142
+ gallery_keywords = ["karşılaştır", "öneri", "seçenek", "alternatif", "bütçe"]
143
+ return any(keyword in message.lower() for keyword in gallery_keywords)
intent_analyzer.py ADDED
@@ -0,0 +1,328 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ GPT-5 Powered Intent Analyzer
6
+ Müşteri mesajlarından niyeti anlar ve aksiyonları belirler
7
+ """
8
+
9
+ import os
10
+ import json
11
+ import requests
12
+ import logging
13
+ from typing import Dict, Optional, List, Tuple
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ # OpenAI API
18
+ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
19
+ API_URL = "https://api.openai.com/v1/chat/completions"
20
+
21
+ def analyze_customer_intent(
22
+ message: str,
23
+ context: Optional[Dict] = None
24
+ ) -> Dict:
25
+ """
26
+ GPT-5 ile müşteri niyetini analiz et
27
+
28
+ Args:
29
+ message: Müşteri mesajı
30
+ context: Sohbet bağlamı (varsa)
31
+
32
+ Returns:
33
+ {
34
+ "intents": ["reserve", "price", "stock", "info"],
35
+ "product": "FX 2",
36
+ "confidence": 0.95,
37
+ "store": "caddebostan", # Eğer belirtildiyse
38
+ "urgency": "high/medium/low",
39
+ "customer_mood": "positive/neutral/negative",
40
+ "suggested_action": "notify_store/answer_directly/both",
41
+ "notification_message": "Özel mesaj"
42
+ }
43
+ """
44
+
45
+ if not OPENAI_API_KEY:
46
+ logger.error("OpenAI API key missing")
47
+ return {"intents": [], "confidence": 0}
48
+
49
+ # Sistem promptu
50
+ system_prompt = """Sen bir müşteri niyet analiz uzmanısın. Trek bisiklet mağazası için müşteri mesajlarını analiz ediyorsun.
51
+
52
+ GÖREV: Müşteri mesajını analiz et ve JSON formatında döndür.
53
+
54
+ NİYET TİPLERİ:
55
+ - "reserve": Ürünü ayırtmak, rezerve etmek, tutmak istiyor
56
+ - "price": Fiyat bilgisi istiyor
57
+ - "stock": Stok durumu soruyor
58
+ - "info": Genel bilgi istiyor
59
+ - "complaint": Şikayet ediyor
60
+ - "order": Sipariş vermek istiyor
61
+ - "test_ride": Test sürüşü istiyor
62
+ - "service": Servis/tamir istiyor
63
+ - "none": Belirgin bir niyet yok
64
+
65
+ ÖRNEK CÜMLELER:
66
+ - "bu bisikleti ayırtabilir misiniz" → reserve
67
+ - "yarın gelip alabilirim" → reserve
68
+ - "benim için tutun" → reserve
69
+ - "fiyatı nedir" → price
70
+ - "kaç para" → price
71
+ - "var mı" → stock
72
+ - "stokta mı" → stock
73
+ - "özellikleri nelerdir" → info
74
+ - "test edebilir miyim" → test_ride
75
+
76
+ ACİLİYET:
77
+ - high: Hemen aksiyon gerekiyor (ayırtma, hemen alma isteği)
78
+ - medium: Normal takip yeterli
79
+ - low: Bilgi amaçlı
80
+
81
+ JSON FORMATI:
82
+ {
83
+ "intents": ["reserve", "price"], // Birden fazla olabilir
84
+ "product": "FX 2", // Eğer üründen bahsediyorsa
85
+ "confidence": 0.95, // 0-1 arası güven skoru
86
+ "store": null, // Mağaza adı varsa
87
+ "urgency": "high",
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 = ""
96
+ if context:
97
+ if context.get("current_category"):
98
+ context_info = f"\nSON KONUŞULAN ÜRÜN: {context['current_category']}"
99
+ if context.get("messages"):
100
+ last_msg = context["messages"][-1] if context["messages"] else None
101
+ if last_msg:
102
+ context_info += f"\nÖNCEKİ MESAJ: {last_msg.get('user', '')}"
103
+
104
+ # GPT-5'e gönder
105
+ try:
106
+ messages = [
107
+ {"role": "system", "content": system_prompt},
108
+ {"role": "user", "content": f"Müşteri mesajı: \"{message}\"{context_info}\n\nJSON formatında analiz et:"}
109
+ ]
110
+
111
+ # GPT-5 modelleri temperature ve max_tokens desteklemiyor
112
+ payload = {
113
+ "model": "gpt-5.2-chat-latest",
114
+ "messages": messages,
115
+ "response_format": {"type": "json_object"} # JSON garantisi
116
+ }
117
+
118
+ headers = {
119
+ "Content-Type": "application/json",
120
+ "Authorization": f"Bearer {OPENAI_API_KEY}"
121
+ }
122
+
123
+ response = requests.post(API_URL, headers=headers, json=payload, timeout=10)
124
+
125
+ if response.status_code == 200:
126
+ result = response.json()
127
+ analysis = json.loads(result['choices'][0]['message']['content'])
128
+
129
+ logger.info(f"🧠 Intent Analysis: {analysis}")
130
+ return analysis
131
+ else:
132
+ logger.error(f"GPT-5 API error: {response.status_code}")
133
+ return {"intents": [], "confidence": 0}
134
+
135
+ except Exception as e:
136
+ logger.error(f"Intent analysis error: {e}")
137
+ return {"intents": [], "confidence": 0}
138
+
139
+ def should_notify_store(analysis: Dict) -> Tuple[bool, str]:
140
+ """
141
+ Mağazaya bildirim gönderilmeli mi?
142
+
143
+ Returns:
144
+ (should_notify, reason)
145
+ """
146
+
147
+ # Yüksek güvenle rezervasyon talebi
148
+ if "reserve" in analysis.get("intents", []) and analysis.get("confidence", 0) > 0.7:
149
+ return True, "Müşteri ürünü ayırtmak istiyor"
150
+
151
+ # Acil stok sorusu
152
+ if "stock" in analysis.get("intents", []) and analysis.get("urgency") == "high":
153
+ return True, "Müşteri acil stok bilgisi istiyor"
154
+
155
+ # Test sürüşü talebi
156
+ if "test_ride" in analysis.get("intents", []):
157
+ return True, "Müşteri test sürüşü talep ediyor"
158
+
159
+ # Sipariş vermek istiyor
160
+ if "order" in analysis.get("intents", []) and analysis.get("confidence", 0) > 0.8:
161
+ return True, "Müşteri sipariş vermek istiyor"
162
+
163
+ # Şikayet varsa
164
+ if "complaint" in analysis.get("intents", []):
165
+ return True, "Müşteri şikayette bulunuyor"
166
+
167
+ # GPT öneriyorsa
168
+ if analysis.get("suggested_action") in ["notify_store", "both"]:
169
+ return True, analysis.get("notification_reason", "GPT-5 bildirim öneriyor")
170
+
171
+ return False, ""
172
+
173
+ def get_smart_notification_message(
174
+ analysis: Dict,
175
+ customer_phone: str,
176
+ original_message: str
177
+ ) -> str:
178
+ """
179
+ Analiz sonucuna göre akıllı bildirim mesajı oluştur
180
+ """
181
+
182
+ intents = analysis.get("intents", [])
183
+ product = analysis.get("product", "Belirtilmemiş")
184
+ urgency = analysis.get("urgency", "medium")
185
+ mood = analysis.get("customer_mood", "neutral")
186
+
187
+ # Emoji seçimi
188
+ if "reserve" in intents:
189
+ emoji = "🔔"
190
+ title = "AYIRTMA TALEBİ"
191
+ elif "price" in intents:
192
+ emoji = "💰"
193
+ title = "FİYAT SORUSU"
194
+ elif "stock" in intents:
195
+ emoji = "📦"
196
+ title = "STOK SORUSU"
197
+ elif "test_ride" in intents:
198
+ emoji = "🚴"
199
+ title = "TEST SÜRÜŞÜ TALEBİ"
200
+ elif "complaint" in intents:
201
+ emoji = "⚠️"
202
+ title = "ŞİKAYET"
203
+ elif "order" in intents:
204
+ emoji = "🛒"
205
+ title = "SİPARİŞ TALEBİ"
206
+ else:
207
+ emoji = "ℹ️"
208
+ title = "MÜŞTERİ TALEBİ"
209
+
210
+ # Aciliyet vurgusu
211
+ if urgency == "high":
212
+ title = f"🚨 ACİL - {title}"
213
+
214
+ # Mesaj oluştur
215
+ from datetime import datetime
216
+ now = datetime.now()
217
+ date_str = now.strftime("%d.%m.%Y %H:%M")
218
+
219
+ message_parts = [
220
+ f"{emoji} **{title}**",
221
+ f"📅 {date_str}",
222
+ "",
223
+ f"👤 **Müşteri:** {customer_phone.replace('whatsapp:', '')}",
224
+ f"🚲 **Ürün:** {product}",
225
+ f"💬 **Mesaj:** \"{original_message}\"",
226
+ "",
227
+ "📊 **AI Analizi:**"
228
+ ]
229
+
230
+ # Niyet listesi
231
+ intent_map = {
232
+ "reserve": "✓ Ayırtma isteği",
233
+ "price": "✓ Fiyat bilgisi",
234
+ "stock": "✓ Stok durumu",
235
+ "test_ride": "✓ Test sürüşü",
236
+ "order": "✓ Sipariş",
237
+ "complaint": "✓ Şikayet",
238
+ "info": "✓ Bilgi talebi"
239
+ }
240
+
241
+ for intent in intents:
242
+ if intent in intent_map:
243
+ message_parts.append(f" {intent_map[intent]}")
244
+
245
+ # Müşteri durumu
246
+ mood_map = {
247
+ "positive": "😊 Pozitif",
248
+ "neutral": "😐 Nötr",
249
+ "negative": "😟 Negatif"
250
+ }
251
+ message_parts.append(f" Müşteri Durumu: {mood_map.get(mood, mood)}")
252
+ message_parts.append(f" Aciliyet: {'🔴 Yüksek' if urgency == 'high' else '🟡 Orta' if urgency == 'medium' else '🟢 Düşük'}")
253
+
254
+ # Önerilen aksiyon
255
+ message_parts.extend([
256
+ "",
257
+ "✅ **Önerilen Aksiyon:**"
258
+ ])
259
+
260
+ if "reserve" in intents:
261
+ message_parts.extend([
262
+ "1. Stok kontrolü yapın",
263
+ "2. Müşteriyi hemen arayın",
264
+ "3. Ödeme ve teslimat planı belirleyin"
265
+ ])
266
+ elif "test_ride" in intents:
267
+ message_parts.extend([
268
+ "1. Test bisikleti hazırlığı",
269
+ "2. Randevu ayarlayın",
270
+ "3. Kimlik ve güvenlik prosedürü"
271
+ ])
272
+ elif "complaint" in intents:
273
+ message_parts.extend([
274
+ "1. Müşteriyi hemen arayın",
275
+ "2. Sorunu dinleyin",
276
+ "3. Çözüm önerisi sunun"
277
+ ])
278
+ else:
279
+ message_parts.append("Müşteri ile iletişime geçin")
280
+
281
+ message_parts.extend([
282
+ "",
283
+ "---",
284
+ "Trek AI Assistant"
285
+ ])
286
+
287
+ return "\n".join(message_parts)
288
+
289
+ # Test fonksiyonu
290
+ def test_intent_analysis():
291
+ """Test senaryoları"""
292
+
293
+ test_messages = [
294
+ "FX 2'yi ayırtabilir misiniz?",
295
+ "Marlin 5'in fiyatı ne kadar?",
296
+ "Stokta var mı?",
297
+ "Yarın gelip alabilirim",
298
+ "Test sürüşü yapabilir miyim?",
299
+ "Bisikletim bozuldu",
300
+ "Teşekkürler",
301
+ "Merhaba",
302
+ "Sipariş vermek istiyorum"
303
+ ]
304
+
305
+ print("\n" + "="*60)
306
+ print("INTENT ANALYSIS TEST")
307
+ print("="*60)
308
+
309
+ for msg in test_messages:
310
+ print(f"\n📝 Mesaj: \"{msg}\"")
311
+ analysis = analyze_customer_intent(msg)
312
+
313
+ if analysis.get("intents"):
314
+ print(f"🎯 Niyetler: {', '.join(analysis['intents'])}")
315
+ print(f"📊 Güven: {analysis.get('confidence', 0):.2%}")
316
+ print(f"🚨 Aciliyet: {analysis.get('urgency', 'unknown')}")
317
+
318
+ should_notify, reason = should_notify_store(analysis)
319
+ print(f"🔔 Bildirim: {'EVET' if should_notify else 'HAYIR'}")
320
+ if reason:
321
+ print(f" Sebep: {reason}")
322
+ else:
323
+ print("❌ Niyet tespit edilemedi")
324
+
325
+ print("\n" + "="*60)
326
+
327
+ if __name__ == "__main__":
328
+ test_intent_analysis()
media_queue_v2.py ADDED
@@ -0,0 +1,281 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ WhatsApp Media Queue V2 - Global Cache ile
6
+ Medya URL'lerini kaybetmeden, mesajları akıllıca birleştirir
7
+ """
8
+
9
+ import time
10
+ import threading
11
+ from typing import Dict, Optional, List, Tuple
12
+ import logging
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ class MediaQueueV2:
17
+ """
18
+ Media Queue V2 - Basit ve güvenilir
19
+ """
20
+
21
+ def __init__(self, wait_time: float = 3.0):
22
+ """
23
+ Args:
24
+ wait_time: Medya sonrası bekleme süresi (saniye)
25
+ """
26
+ self.wait_time = wait_time
27
+ # Global media cache - {user_id: {"media_urls": [...], "media_types": [...], "timestamp": ..., "caption": ...}}
28
+ self.media_cache: Dict[str, Dict] = {}
29
+ # Timer'lar - {user_id: threading.Timer}
30
+ self.timers: Dict[str, threading.Timer] = {}
31
+ # Lock for thread safety
32
+ self.lock = threading.Lock()
33
+
34
+ def handle_media(self, user_id: str, media_urls: List[str], media_types: List[str], caption: str = "") -> str:
35
+ """
36
+ Medya mesajı geldiğinde çağrılır
37
+
38
+ Args:
39
+ user_id: WhatsApp kullanıcı numarası
40
+ media_urls: Medya URL listesi
41
+ media_types: Medya tipi listesi
42
+ caption: Medya başlığı (varsa)
43
+
44
+ Returns:
45
+ Kullanıcıya gönderilecek bekleme mesajı
46
+ """
47
+ with self.lock:
48
+ # Önceki timer'ı iptal et (varsa)
49
+ self._cancel_timer(user_id)
50
+
51
+ # Media'yı cache'e kaydet
52
+ self.media_cache[user_id] = {
53
+ "media_urls": media_urls,
54
+ "media_types": media_types,
55
+ "caption": caption,
56
+ "timestamp": time.time()
57
+ }
58
+
59
+ logger.info(f"📸 {user_id}: Medya cache'e kaydedildi - {len(media_urls)} dosya")
60
+
61
+ # Yeni timer başlat
62
+ self._start_timer(user_id)
63
+
64
+ # Medya tipine göre bekleme mesajı
65
+ if media_types and "image" in media_types[0].lower():
66
+ return "🖼️ Görsel alındı. Hakkında sormak istediğiniz bir şey var mı?"
67
+ elif media_types and "video" in media_types[0].lower():
68
+ return "🎥 Video alındı. Hakkında sormak istediğiniz bir şey var mı?"
69
+ elif media_types and "audio" in media_types[0].lower():
70
+ return "🎵 Ses dosyası alındı. Hakkında sormak istediğiniz bir şey var mı?"
71
+ else:
72
+ return "📎 Dosya alındı. Hakkında sormak istediğiniz bir şey var mı?"
73
+
74
+ def handle_text(self, user_id: str, text: str) -> Tuple[Optional[str], Optional[List[str]], Optional[List[str]]]:
75
+ """
76
+ Metin mesajı geldiğinde çağrılır
77
+
78
+ Args:
79
+ user_id: WhatsApp kullanıcı numarası
80
+ text: Metin mesajı
81
+
82
+ Returns:
83
+ (combined_text, media_urls, media_types) veya (None, None, None)
84
+ """
85
+ with self.lock:
86
+ # Cache'de medya var mı?
87
+ if user_id in self.media_cache:
88
+ # Timer'ı iptal et
89
+ self._cancel_timer(user_id)
90
+
91
+ # Cache'den medya bilgilerini al
92
+ cached = self.media_cache[user_id]
93
+ media_urls = cached["media_urls"]
94
+ media_types = cached["media_types"]
95
+ caption = cached["caption"]
96
+
97
+ # Cache'i temizle
98
+ del self.media_cache[user_id]
99
+
100
+ # Mesajları birleştir
101
+ combined_text = self._combine_messages(caption, text)
102
+
103
+ logger.info(f"✅ {user_id}: Medya + metin birleştirildi")
104
+ logger.info(f" Birleşik mesaj: {combined_text[:100]}...")
105
+ logger.info(f" Medya URL'leri: {media_urls}")
106
+
107
+ return combined_text, media_urls, media_types
108
+
109
+ # Cache'de medya yoksa normal metin olarak dön
110
+ logger.info(f"💬 {user_id}: Normal metin mesajı (cache'de medya yok)")
111
+ return None, None, None
112
+
113
+ def _combine_messages(self, caption: str, text: str) -> str:
114
+ """
115
+ Caption ve metin mesajını birleştir
116
+
117
+ Args:
118
+ caption: Medya başlığı
119
+ text: Kullanıcının sonraki mesajı
120
+
121
+ Returns:
122
+ Birleştirilmiş mesaj
123
+ """
124
+ parts = []
125
+
126
+ if caption and caption.strip():
127
+ parts.append(caption.strip())
128
+
129
+ if text and text.strip():
130
+ parts.append(text.strip())
131
+
132
+ # Eğer hiç metin yoksa varsayılan
133
+ if not parts:
134
+ return "Bu görseli analiz et."
135
+
136
+ # Birleştir
137
+ combined = " ".join(parts)
138
+
139
+ # Eğer sadece caption varsa ve soru işareti yoksa, analiz isteği ekle
140
+ if len(parts) == 1 and "?" not in combined and len(combined) < 20:
141
+ combined += ". Bu hakkında bilgi ver."
142
+
143
+ return combined
144
+
145
+ def _start_timer(self, user_id: str):
146
+ """Timeout timer'ını başlat"""
147
+ timer = threading.Timer(self.wait_time, self._handle_timeout, args=[user_id])
148
+ timer.start()
149
+ self.timers[user_id] = timer
150
+ logger.debug(f"⏰ {user_id}: {self.wait_time}s timer başlatıldı")
151
+
152
+ def _cancel_timer(self, user_id: str):
153
+ """Timer'ı iptal et"""
154
+ if user_id in self.timers:
155
+ self.timers[user_id].cancel()
156
+ del self.timers[user_id]
157
+ logger.debug(f"⏰ {user_id}: Timer iptal edildi")
158
+
159
+ def _handle_timeout(self, user_id: str):
160
+ """
161
+ Timeout olduğunda çağrılır
162
+ Sadece medyayı işlemek için kullanılabilir
163
+ """
164
+ with self.lock:
165
+ if user_id in self.media_cache:
166
+ logger.info(f"⏱️ {user_id}: Timeout - metin beklenmedi, sadece medya işlenecek")
167
+ # Burada cache'i temizlemiyoruz, bir sonraki mesajda kullanılabilir
168
+ # Ama 5 dakikadan eski cache'leri temizleyebiliriz
169
+ self._cleanup_old_cache()
170
+
171
+ def _cleanup_old_cache(self):
172
+ """5 dakikadan eski cache'leri temizle"""
173
+ current_time = time.time()
174
+ expired_users = []
175
+
176
+ for user_id, cached in self.media_cache.items():
177
+ if current_time - cached["timestamp"] > 300: # 5 dakika
178
+ expired_users.append(user_id)
179
+
180
+ for user_id in expired_users:
181
+ del self.media_cache[user_id]
182
+ logger.debug(f"🗑️ {user_id}: Eski cache temizlendi")
183
+
184
+ def get_cache_status(self, user_id: str) -> bool:
185
+ """
186
+ Kullanıcının cache'inde medya var mı?
187
+
188
+ Args:
189
+ user_id: WhatsApp kullanıcı numarası
190
+
191
+ Returns:
192
+ True: Cache'de medya var
193
+ False: Cache boş
194
+ """
195
+ with self.lock:
196
+ return user_id in self.media_cache
197
+
198
+ def clear_user_cache(self, user_id: str):
199
+ """
200
+ Belirli bir kullanıcının cache'ini temizle
201
+
202
+ Args:
203
+ user_id: WhatsApp kullanıcı numarası
204
+ """
205
+ with self.lock:
206
+ if user_id in self.media_cache:
207
+ del self.media_cache[user_id]
208
+ logger.info(f"🗑️ {user_id}: Cache manuel olarak temizlendi")
209
+
210
+ self._cancel_timer(user_id)
211
+
212
+ # Global instance
213
+ media_queue = MediaQueueV2(wait_time=3.0)
214
+
215
+ # Test fonksiyonu
216
+ def test_media_queue():
217
+ """Test senaryoları"""
218
+
219
+ print("\n" + "="*60)
220
+ print("Media Queue V2 Test")
221
+ print("="*60)
222
+
223
+ # Test 1: Medya + Metin
224
+ print("\n📱 Test 1: Görsel + Soru")
225
+ print("-"*40)
226
+
227
+ # Görsel gönder
228
+ response = media_queue.handle_media(
229
+ "user123",
230
+ ["https://example.com/image.jpg"],
231
+ ["image/jpeg"],
232
+ "Bugünkü bisiklet"
233
+ )
234
+ print(f"Bot: {response}")
235
+
236
+ # 1 saniye bekle
237
+ time.sleep(1)
238
+
239
+ # Soru sor
240
+ combined, urls, types = media_queue.handle_text("user123", "Fiyatı ne kadar?")
241
+
242
+ if combined:
243
+ print(f"\n✅ Birleştirildi:")
244
+ print(f" Mesaj: {combined}")
245
+ print(f" URLs: {urls}")
246
+ print(f" Types: {types}")
247
+
248
+ # Test 2: Sadece metin
249
+ print("\n📱 Test 2: Sadece Metin")
250
+ print("-"*40)
251
+
252
+ combined, urls, types = media_queue.handle_text("user456", "Merhaba")
253
+
254
+ if combined:
255
+ print("Birleştirilmiş mesaj var")
256
+ else:
257
+ print("Normal metin mesajı (birleştirme yok)")
258
+
259
+ # Test 3: Medya + Timeout
260
+ print("\n📱 Test 3: Görsel + Timeout")
261
+ print("-"*40)
262
+
263
+ response = media_queue.handle_media(
264
+ "user789",
265
+ ["https://example.com/image2.jpg"],
266
+ ["image/jpeg"],
267
+ ""
268
+ )
269
+ print(f"Bot: {response}")
270
+
271
+ print("3 saniye bekleniyor...")
272
+ time.sleep(4)
273
+
274
+ # Timeout sonrası cache durumu
275
+ if media_queue.get_cache_status("user789"):
276
+ print("✅ Medya hala cache'de (bir sonraki mesajda kullanılabilir)")
277
+ else:
278
+ print("❌ Cache temizlenmiş")
279
+
280
+ if __name__ == "__main__":
281
+ test_media_queue()
product_search.py ADDED
@@ -0,0 +1,284 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Professional Product Search Engine for Trek Chatbot
3
+ Implements intelligent product matching with fuzzy search and NLP techniques
4
+ """
5
+
6
+ import re
7
+ from difflib import SequenceMatcher
8
+ from typing import List, Tuple, Dict, Optional
9
+ import unicodedata
10
+
11
+ class ProductSearchEngine:
12
+ """Advanced product search with intelligent matching"""
13
+
14
+ def __init__(self, products: List[Tuple]):
15
+ """
16
+ Initialize with products list
17
+ products: List of tuples (short_name, product_info, full_name)
18
+ """
19
+ self.products = products
20
+ self.product_index = self._build_index()
21
+
22
+ def _build_index(self) -> Dict:
23
+ """Build search index for faster lookups"""
24
+ index = {
25
+ 'by_name': {},
26
+ 'by_words': {},
27
+ 'by_category': {},
28
+ 'by_model': {},
29
+ 'normalized': {}
30
+ }
31
+
32
+ for product in self.products:
33
+ short_name = product[0]
34
+ full_name = product[2]
35
+
36
+ # Normalize and store
37
+ normalized_full = self._normalize_text(full_name)
38
+ normalized_short = self._normalize_text(short_name)
39
+
40
+ # Store by full name
41
+ index['by_name'][normalized_full] = product
42
+ index['normalized'][normalized_full] = full_name
43
+
44
+ # Extract and index words
45
+ words = normalized_full.split()
46
+ for word in words:
47
+ if len(word) > 2: # Skip very short words
48
+ if word not in index['by_words']:
49
+ index['by_words'][word] = []
50
+ index['by_words'][word].append(product)
51
+
52
+ # Extract model numbers and categories
53
+ model_match = re.search(r'\b(\d+\.?\d*)\b', full_name)
54
+ if model_match:
55
+ model_num = model_match.group(1)
56
+ if model_num not in index['by_model']:
57
+ index['by_model'][model_num] = []
58
+ index['by_model'][model_num].append(product)
59
+
60
+ # Category extraction (first word often represents category)
61
+ if words:
62
+ category = words[0]
63
+ if category not in index['by_category']:
64
+ index['by_category'][category] = []
65
+ index['by_category'][category].append(product)
66
+
67
+ return index
68
+
69
+ def _normalize_text(self, text: str) -> str:
70
+ """Normalize text for better matching"""
71
+ if not text:
72
+ return ""
73
+
74
+ # Convert to lowercase
75
+ text = text.lower()
76
+
77
+ # Remove Turkish characters
78
+ replacements = {
79
+ 'ı': 'i', 'İ': 'i', 'ş': 's', 'Ş': 's',
80
+ 'ğ': 'g', 'Ğ': 'g', 'ü': 'u', 'Ü': 'u',
81
+ 'ö': 'o', 'Ö': 'o', 'ç': 'c', 'Ç': 'c'
82
+ }
83
+ for tr_char, eng_char in replacements.items():
84
+ text = text.replace(tr_char, eng_char)
85
+
86
+ # Remove special characters but keep spaces and numbers
87
+ text = re.sub(r'[^\w\s\d\.]', ' ', text)
88
+
89
+ # Normalize whitespace
90
+ text = ' '.join(text.split())
91
+
92
+ return text
93
+
94
+ def _calculate_similarity(self, str1: str, str2: str) -> float:
95
+ """Calculate similarity between two strings"""
96
+ return SequenceMatcher(None, str1, str2).ratio()
97
+
98
+ def search(self, query: str, threshold: float = 0.6) -> List[Tuple[float, Tuple]]:
99
+ """
100
+ Search for products matching the query
101
+ Returns list of (score, product) tuples sorted by relevance
102
+ """
103
+ query_normalized = self._normalize_text(query)
104
+ query_words = query_normalized.split()
105
+
106
+ results = {}
107
+
108
+ # 1. Exact match
109
+ if query_normalized in self.product_index['by_name']:
110
+ product = self.product_index['by_name'][query_normalized]
111
+ results[id(product)] = (1.0, product)
112
+
113
+ # 2. Model number search
114
+ model_match = re.search(r'\b(\d+\.?\d*)\b', query)
115
+ if model_match:
116
+ model_num = model_match.group(1)
117
+ if model_num in self.product_index['by_model']:
118
+ for product in self.product_index['by_model'][model_num]:
119
+ if id(product) not in results:
120
+ # Check if model number is in correct context
121
+ score = 0.9 if model_num in product[2].lower() else 0.7
122
+ results[id(product)] = (score, product)
123
+
124
+ # 3. Word-based search with scoring
125
+ word_matches = {}
126
+ for word in query_words:
127
+ if len(word) > 2 and word in self.product_index['by_words']:
128
+ for product in self.product_index['by_words'][word]:
129
+ if id(product) not in word_matches:
130
+ word_matches[id(product)] = {'count': 0, 'product': product}
131
+ word_matches[id(product)]['count'] += 1
132
+
133
+ # Calculate word match scores
134
+ for product_id, match_info in word_matches.items():
135
+ product = match_info['product']
136
+ matched_count = match_info['count']
137
+ total_query_words = len([w for w in query_words if len(w) > 2])
138
+
139
+ if total_query_words > 0:
140
+ word_score = matched_count / total_query_words
141
+
142
+ # Boost score if all important words match
143
+ if matched_count == total_query_words:
144
+ word_score = min(word_score * 1.2, 0.95)
145
+
146
+ # Check word order for better scoring
147
+ product_text = self._normalize_text(product[2])
148
+ if query_normalized in product_text:
149
+ word_score = min(word_score * 1.3, 0.98)
150
+
151
+ if id(product) not in results or results[id(product)][0] < word_score:
152
+ results[id(product)] = (word_score, product)
153
+
154
+ # 4. Fuzzy matching for all products
155
+ for product in self.products:
156
+ product_normalized = self._normalize_text(product[2])
157
+ similarity = self._calculate_similarity(query_normalized, product_normalized)
158
+
159
+ # Substring matching
160
+ if query_normalized in product_normalized:
161
+ similarity = max(similarity, 0.8)
162
+
163
+ # Check if product contains all query words (in any order)
164
+ if all(word in product_normalized for word in query_words if len(word) > 2):
165
+ similarity = max(similarity, 0.75)
166
+
167
+ if similarity >= threshold:
168
+ if id(product) not in results or results[id(product)][0] < similarity:
169
+ results[id(product)] = (similarity, product)
170
+
171
+ # 5. Category-based fallback
172
+ if not results and query_words:
173
+ category = query_words[0]
174
+ if category in self.product_index['by_category']:
175
+ for product in self.product_index['by_category'][category]:
176
+ results[id(product)] = (0.5, product)
177
+
178
+ # Convert to list and sort by score
179
+ result_list = list(results.values())
180
+ result_list.sort(key=lambda x: x[0], reverse=True)
181
+
182
+ return result_list
183
+
184
+ def find_best_match(self, query: str) -> Optional[Tuple]:
185
+ """Find the single best matching product"""
186
+ results = self.search(query)
187
+ if results and results[0][0] >= 0.6:
188
+ return results[0][1]
189
+ return None
190
+
191
+ def find_similar_products(self, product_name: str, limit: int = 5) -> List[Tuple]:
192
+ """Find products similar to the given product name"""
193
+ results = self.search(product_name)
194
+ similar = []
195
+
196
+ # Skip the first result if it's an exact match
197
+ start_idx = 1 if results and results[0][0] > 0.95 else 0
198
+
199
+ for score, product in results[start_idx:start_idx + limit]:
200
+ if score >= 0.5:
201
+ similar.append(product)
202
+
203
+ return similar
204
+
205
+ def extract_product_context(self, query: str) -> Dict:
206
+ """Extract context from query (size, color, type, etc.)"""
207
+ context = {
208
+ 'sizes': [],
209
+ 'colors': [],
210
+ 'types': [],
211
+ 'features': [],
212
+ 'price_range': None
213
+ }
214
+
215
+ # Size detection
216
+ size_patterns = [
217
+ r'\b(xs|s|m|l|xl|xxl|2xl|3xl)\b',
218
+ r'\b(\d{2})\b(?=\s*beden|\s*numara|$)', # 44, 46, etc.
219
+ r'\b(small|medium|large)\b'
220
+ ]
221
+ for pattern in size_patterns:
222
+ matches = re.findall(pattern, query.lower())
223
+ context['sizes'].extend(matches)
224
+
225
+ # Color detection
226
+ colors = ['siyah', 'beyaz', 'mavi', 'kirmizi', 'yesil', 'gri', 'turuncu',
227
+ 'black', 'white', 'blue', 'red', 'green', 'grey', 'gray', 'orange']
228
+ for color in colors:
229
+ if color in query.lower():
230
+ context['colors'].append(color)
231
+
232
+ # Type detection
233
+ types = ['erkek', 'kadin', 'cocuk', 'yol', 'dag', 'sehir', 'elektrikli',
234
+ 'karbon', 'aluminyum', 'gravel', 'hybrid']
235
+ for type_word in types:
236
+ if type_word in query.lower():
237
+ context['types'].append(type_word)
238
+
239
+ # Feature detection
240
+ features = ['disk fren', 'shimano', 'sram', 'karbon', 'aluminyum',
241
+ 'hidrolik', 'mekanik', '29 jant', '27.5 jant']
242
+ for feature in features:
243
+ if feature in query.lower():
244
+ context['features'].append(feature)
245
+
246
+ # Price range detection
247
+ price_match = re.search(r'(\d+)\.?(\d*)\s*(bin|tl)', query.lower())
248
+ if price_match:
249
+ price = float(price_match.group(1) + ('.' + price_match.group(2) if price_match.group(2) else ''))
250
+ if 'bin' in price_match.group(3):
251
+ price *= 1000
252
+ context['price_range'] = price
253
+
254
+ return context
255
+
256
+ def generate_suggestions(self, failed_query: str) -> List[str]:
257
+ """Generate suggestions for failed searches"""
258
+ suggestions = []
259
+ query_normalized = self._normalize_text(failed_query)
260
+ query_words = query_normalized.split()
261
+
262
+ # Find products with partial matches
263
+ partial_matches = set()
264
+ for word in query_words:
265
+ if len(word) > 3:
266
+ for product_word in self.product_index['by_words']:
267
+ if word in product_word or product_word in word:
268
+ partial_matches.add(product_word)
269
+
270
+ # Generate suggestions from partial matches
271
+ for match in list(partial_matches)[:5]:
272
+ if match in self.product_index['by_words']:
273
+ products = self.product_index['by_words'][match]
274
+ if products:
275
+ suggestions.append(products[0][2])
276
+
277
+ # Add category suggestions
278
+ for category in list(self.product_index['by_category'].keys())[:3]:
279
+ if any(word in category for word in query_words):
280
+ category_products = self.product_index['by_category'][category]
281
+ if category_products:
282
+ suggestions.append(category_products[0][2])
283
+
284
+ return list(set(suggestions))[:5] # Return unique suggestions
prompts.py ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Trek Bisiklet Chatbot Sistem Promptları
4
+ Kategorize edilmiş ve optimize edilmiş prompt konfigürasyonu
5
+ """
6
+
7
+ from datetime import datetime
8
+
9
+ SYSTEM_PROMPTS = [
10
+ # 1. TEMEL KİMLİK VE ROL
11
+ {
12
+ "role": "system",
13
+ "category": "identity",
14
+ "content": "Sen Trek bisiklet uzmanı AI asistanısın. Trek ve Electra bisikletler konusunda uzmanısın. Stokta bulunan ürünlerin fiyat bilgilerini verebilirsin.\n\nCEVAP TARZI:\n• Kısa ve öz cevaplar ver (maksimum 3-4 cümle)\n• Gereksiz açıklama yapma, sadece sorulan bilgiyi ver\n• Stok sorularında direkt mağaza ve beden bilgisi ver\n• Fiyat sorularında sadece fiyatı söyle\n• WhatsApp için uygun, mobil okunabilir formatla yaz\n\nTÜRKÇE KONUŞMA KURALLARI:\n• Müşteriye HER ZAMAN 'siz' diye hitap et, ASLA 'sen' kullanma\n• Doğru: 'isterseniz', 'bakabilirsiniz', 'size', 'sizin için'\n• Yanlış: 'istersen', 'bakabilirsin', 'sana', 'senin için'\n• Cümleleri ASLA soru ile bitirme ('ayırtayım mı?', 'ister misiniz?', 'bakar mısınız?' gibi sorular SORMA)\n• Bilgiyi verdikten sonra sus, müşteriyi sıkıştırma\n• Müşteri karar vermek isterse kendisi sorar"
15
+ },
16
+
17
+ # 2. MAĞAZA BİLGİLERİ VE İLETİŞİM
18
+ {
19
+ "role": "system",
20
+ "category": "stores",
21
+ "content": "MAĞAZA BİLGİLERİ:\n• İstanbul - Caddebostan: Prof. Dr. Hulusi Behçet 18, Kadıköy (Göztepe Parkı karşısı) | Tel: 0543 934 0438\n• İstanbul - Ortaköy: Dereboyu Cad No:84, Beşiktaş (Toyota Plaza yanında).Öenmli: Ortaköy şubemiz 1 Aralık 2025 itibari ile kalıcı olarak kapanmıştır. BU şebeyi, şube bilgilerinde vermeyeceksin | Tel: 0543 933 9884\n• İstanbul - Sarıyer: Mareşal Fevzi Çakmak Cad. No 54, Kemer-Bahçeköy | Tel: 0542 137 1080\n• İzmir - Alsancak: Sezer Doğan Sok. The Kar Suits 14A, Konak | Tel: 0543 936 2335\n\nYETKİLİ TREK BAYİLERİ:\n• İzmir Bisiklet (Karşıyaka): Tuna Mah. Cemal Gürsel Cad. No:90/A, Karşıyaka | Tel: 0232 369 42 42 | WhatsApp: 0535 374 24 69\n• Bike Stop (Antalya): Öğretmenevleri Mah. 20. Cd. 98/B Ata Apt, Konyaaltı | Tel: 0536 975 16 98\nBu bayiler kendi mağazalarımız dışında Trek bisiklet alabileceğiniz yetkili satış noktalarıdır.\n\nÇalışma saatleri: 10:00-19:00 (Pazar kapalı)\nSarıyer mağazası elektrikli bisiklet odaklıdır.\nBike Fit: Caddebostan'da, 3500 TL, 60-90 dk, randevu gerekli."
22
+ },
23
+
24
+ # 3. ÜRÜN KATEGORİLERİ VE MODELLER
25
+ {
26
+ "role": "system",
27
+ "category": "products",
28
+ "content": "ÜRÜN KATEGORİLERİ:\n• Dağ Bisikletleri: Marlin, Roscoe, Procaliber, Supercaliber, Fuel EX\n• Yol Bisikletleri: Émonda, Domane, Speed Concept\n• Şehir Bisikletleri: FX, DS (Dual Sport - 2026 itibariyle uretilmiyor, sadece DS+ elektrikli devam ediyor), Verve\n• Gravel: Checkpoint\n• Elektrikli: Powerfly, Rail, Fuel EXe, Domane+, FX+, DS+, Verve+, Townie+\n• Boylar: XXS, XS, S, M, ML, L, XL"
29
+ },
30
+
31
+ # 4. MARKA KURALLAR VE KISITLAMALAR
32
+ {
33
+ "role": "system",
34
+ "category": "brand_rules",
35
+ "content": "MARKA KURALLARI:\n• Sadece Trek, Electra, Bontrager, Saris, Bryton, Trieye, Gobik markalarını önerebilirsin\n• Diğer markalar (Specialized, Orbea, BMC, Carraro, Scott, Giant) hakkında objektif yorum yapamayacağını belirt\n• Sadece www.trekbisiklet.com.tr ile başlayan linkleri paylaş\n• Trek kadrolar ömür boyu garantilidir"
36
+ },
37
+
38
+ # 5. ÖZEL ÜRÜNLER VE MARKALAR
39
+ {
40
+ "role": "system",
41
+ "category": "special_brands",
42
+ "content": "ÖZEL MARKALAR:\n• Bontrager: Bisiklet aksesuar ve yedek parçaları\n• Bryton Rider S800: En üst seviye GPS yol bilgisayarı\n• Trieye: Norveç menşeili geri görüş aynalı güvenlik gözlükleri (Photochromatic ve renkli lens seçenekleri)"
43
+ },
44
+
45
+ # 6. MADONE GEN 8 ÖZEL BİLGİLER
46
+ {
47
+ "role": "system",
48
+ "category": "madone_gen8",
49
+ "content": "MADONE GEN 8 (27 Haziran 2024):\nÉmonda kadar hafif, Madone kadar hızlı. Gen 7 ve Émonda'nın yerine geçen hibrit model.\n• 900 Serisi OCLV Karbon, 320g daha hafif\n• Émonda'dan 77 saniye/saat daha hızlı\n• %80 daha uyumlu IsoFlow teknolojisi\n• SL: 500 Serisi karbon, ekonomik, mekanik vites uyumlu\n• SLR: Premium model, RSL Aero gidon"
50
+ },
51
+
52
+ # 7. FİRMA GEÇMİŞİ VE SPONSORLUKLAR
53
+ {
54
+ "role": "system",
55
+ "category": "company_info",
56
+ "content": "FİRMA BİLGİLERİ:\n• 2000'den beri Alatin Bisiklet tarafından Türkiye distribütörlüğü\n• Sponsorluklar: Fatih Topçu (ASLA DURMA), TREK RMK DYNAMIS takımı\n• Bisiklet takası: https://www.bikeexchangehub.com/\n• Web sitesi: https://www.alatin.com.tr\n• Canlı destek: Sitedeki YEŞİL düğme"
57
+ },
58
+
59
+ # 8. ONLİNE SATIŞ VE SİPARİŞ BİLGİLERİ
60
+ {
61
+ "role": "system",
62
+ "category": "online_sales",
63
+ "content": "ONLİNE SATIŞ BİLGİLERİ:\n• www.trekbisiklet.com.tr üzerinden ONLINE SATIŞ YAPILMAKTADIR\n• Müşteriler web sitesi üzerinden tüm ürünleri görüntüleyebilir, sepete ekleyebilir ve online ödeme yaparak sipariş verebilir\n• Online sipariş süreci: 'Ürünü sepete ekle → Bilgilerini gir → Ödeme yöntemini seç → Siparişi tamamla'\n• Kargo: Aras Kargo ile gönderim, 24 saat içinde hazırlık, 3-5 iş günü teslimat\n• SMS ve email ile sipariş takibi yapılır\n• Belirli tutar üzeri siparişlerde kargo ücretsizdir\n• Müşteriler isterlarsa mağazadan da teslim alabilir\n• Stok ve fiyat bilgileri web sitesinde güncel olarak görüntülenebilir\n• Özel durumlar, detaylı ürün danışmanlığı veya test sürüşü için mağazalarla iletişime geçilmesi önerilir"
64
+ },
65
+
66
+ # 9. BIKE FINDER SİSTEMİ
67
+ {
68
+ "role": "system",
69
+ "category": "bike_finder",
70
+ "content": "BIKE FINDER ASİSTANI:\nKullanıcılara uygun bisiklet seçimi için adım adım sorular sor:\n1. Bisiklet kategorisi (Yol/Dağ/Şehir/Gravel/Elektrikli)\n2. Kullanım amacı (Günlük/Spor/Tur/Yarış/Offroad)\n3. Öncelikler (Performans/Konfor/Dayanıklılık)\n4. Zemin koşulları (Asfalt/Parkur/Orman/Offroad)\n5. Fiziksel ölçüler (Boy/İç bacak)\n6. Bütçe ve özel tercihler\n7. Öneri ve satış yönlendirme"
71
+ },
72
+
73
+ # 10. AĞIRLIK VERİLERİ VE TEKNİK BİLGİLER
74
+ {
75
+ "role": "system",
76
+ "category": "technical_specs",
77
+ "content": "AĞIRLIK VERİLERİ (kg):\nMadone Gen 8: SL5(8.70), SL6(8.16), SL7(7.88), SLR7(7.30), SLR9(7.00)\nÉmonda: SLR9(6.72), SLR7(7.10), SL7(7.95), SL5(9.15)\nDomane: SLR7(7.25), SL6(8.90), AL2(10.55)\nFuel: EXe 9.8(18.1), EX 8(13.77)\nMarlin: 4(14.60), 5(13.90), 7(13.77), 8(13.2)\nKullanıcı sadece ana model adı verirse, tüm varyantları listele."
78
+ },
79
+
80
+ # 11. OPERASYONEL KURALLAR
81
+ {
82
+ "role": "system",
83
+ "category": "operations",
84
+ "content": "OPERASYONEL KURALLAR:\n• Cevap verirken bilgilerin doğruluğunu kontrol et\n• Diğer markalarla ilgili sorularda kibarca Trek'in avantajlarını anlat\n• Stok ve fiyat bilgileri için web sitesini referans göster\n• Müşteri özel danışmanlık veya test sürüşü isterse en yakın mağazaya yönlendir"
85
+ },
86
+
87
+ # 12. BÜTÇE REHBERİ
88
+ {
89
+ "role": "system",
90
+ "category": "budget_guide",
91
+ "content": "BÜTÇE ARALIĞI:\n• Başlangıç: 46.000-100.000 TL (Dual Sport, FX serisi)\n• Orta seviye: 200.000-400.000 TL (Domane SL, Émonda SL)\n• Profesyonel: 500.000-750.000 TL (Madone SLR, Speed Concept)\n• Elektrikli: 200.000-600.000 TL (Powerfly, Rail serisi)"
92
+ },
93
+
94
+ # 13. BOYUT REHBERİ
95
+ {
96
+ "role": "system",
97
+ "category": "sizing_guide",
98
+ "content": "BOYUT REHBERİ:\n• XXS: 150-155cm | XS: 155-165cm | S: 165-172cm\n• M: 172-178cm | ML: 178-185cm | L: 185-192cm | XL: 192cm+\nDoğru boyut için hem boy hem iç bacak ölçümü önemlidir."
99
+ },
100
+
101
+ # 14. KAMPANYA VE SERVİS HİZMETLERİ
102
+ {
103
+ "role": "system",
104
+ "category": "services",
105
+ "content": "KAMPANYALAR: %15-20 mevsimsel indirimler, eski model kampanyaları\nSERVİS HİZMETLERİ: Ücretsiz ilk bakım (3 ay), ömür boyu garanti, yedek parça temini\nKARGO: Belirli tutar üzeri ücretsiz, mağazadan teslim alma seçeneği. Bisiklet bakımlarını tüm markalara yapıyoruz. Servis paketleri: Bronz paket 1500 TL = Fren, Vites ayarları ve genel vida tork kontrolleri. Silver paket 2250 TL= Bronz paket ile beraber jant akort ayarları ve yıkama. Gold paket 3000 TL= Silver paket ile beraber ön, arka, orta ve furs yatak bakımları"
106
+ },
107
+
108
+ # 15. ODEME VE BANKA BILGILERI
109
+ {
110
+ "role": "system",
111
+ "category": "payment_info",
112
+ "content": "ODEME VE TAKSIT SECENEKLERI:\nCalistigimiz bankalar ve kart programlari:\n- Axess (Akbank)\n- Bonus (Garanti BBVA)\n- Maximum (Is Bankasi)\n- World (Yapi Kredi)\n- CardFinans (QNB Finansbank)\n- Paraf (Halkbank)\n- Combo (Halkbank)\n\nTum bu kartlarla taksitli alisveris yapilabilir. Taksit secenekleri kampanyalara gore degisebilir."
113
+ },
114
+
115
+ # 16. EKİM KAMPANYASI - VADE FARKSIZ 8 TAKSİT
116
+ {
117
+ "role": "system",
118
+ "category": "ekim_kampanya",
119
+ "start_date": "2025-10-01",
120
+ "end_date": "2025-10-31",
121
+ "content": "EKİM KAMPANYASI (1-31 Ekim 2025):\nTrek Bicycle Turkey, Ekim ayı boyunca seçili bisiklet modellerinde vade farksız 8 taksit imkanı sunmaktadır.\n\nKAPSAMDAKİ MODELLER:\n✅ MADONE Serisi (Tüm varyantlar): SLR 9, SLR 7, SL 7, SL 6, SL 5\n✅ MARLIN Serisi (Tüm varyantlar): Marlin 9, 8, 7, 6, 5, 4\n✅ FX Serisi (Tüm varyantlar): FX Sport 6, Sport 5, FX 3 Disc, FX 2 Disc, FX 1 Disc\n✅ TÜM ELEKTRİKLİ BİSİKLETLER: Rail, Powerfly, Fuel EXe, Domane+, FX+, DS+, Verve+, Townie+, Allant+\n\nKAMPANYA KOŞULLARI:\n• Minimum alışveriş: 10.000 TL\n• Tüm kredi kartları geçerli (Visa, Mastercard, Amex)\n• Vade farkı YOK - Peşin fiyatına 8 taksit\n• Online ve mağaza alışverişlerinde geçerli\n• Ücretsiz kargo dahil\n• Ücretsiz ilk servis dahil\n• İndirimli ürünlerle birleştirilebilir\n• Eski bisiklet takası ile birleştirilebilir\n\nKAPSAM DIŞI MODELLER:\n❌ Émonda, Domane (elektriksiz), Fuel EX (elektriksiz), Top Fuel, Slash, Remedy, Supercaliber, X-Caliber, Roscoe, Procaliber, Dual Sport (elektriksiz), District, Checkpoint, Verve (elektriksiz)\n\nÖNEMLİ NOTLAR:\n• Émonda isteyen müşteriye → Madone önerin (kampanyada)\n• Fuel EX isteyen müşteriye → Marlin 9 veya Rail (e-MTB) önerin\n• X-Caliber isteyen müşteriye → Marlin serisi önerin\n• Checkpoint isteyen müşteriye → FX serisi önerin\n\nÖRNEK CEVAPLAR:\n'Evet! Ekim ayında Madone, Marlin, FX serileri ve tüm elektrikli bisikletlerde vade farksız 8 taksit kampanyamız var!'\n'Madone SL 6 kampanyaya dahil! Vade farksız 8 taksit ile alabilirsiniz.'\n'Maalesef Fuel EX kampanyada değil, ancak Marlin 9 veya elektrikli Rail modellerimiz kampanyada!'\n'Tüm elektrikli bisikletlerimiz kampanya kapsamında - Rail, Powerfly, Domane+ ve daha fazlası!'"
122
+ },
123
+
124
+ # 17. HATA ÖNLEMLERİ
125
+ {
126
+ "role": "system",
127
+ "category": "error_prevention",
128
+ "content": "KRİTİK KURALLAR:\n• Telefon numaralarını ASLA uydurma! Sadece bu numaraları kullan:\n - Caddebostan: 0543 934 0438\n - Sarıyer: 0542 137 1080\n - Alsancak: 0543 936 2335\n - İzmir Bisiklet: 0232 369 42 42\n - Bike Stop Antalya: 0536 975 16 98\n• Bilmediğin bilgiyi uydurma, 'bu bilgiye sahip değilim' de\n• Emoji kullanma\n• 'Özetle:' gibi başlıklar kullanma, doğal konuş"
129
+ },
130
+
131
+ # 18. URETIMI DURDURULAN MODELLER
132
+ {
133
+ "role": "system",
134
+ "category": "discontinued_models",
135
+ "content": "URETIMI DURDURULAN MODELLER:\n\nDS (Dual Sport) ELEKTRIKSIZ MODELLER:\n2026 itibariyle DS elektriksiz modellerin uretimi durdurulmustur. Yeni DS elektriksiz modeller stoklara GIRMEYECEK.\n\nMusteriye oneriler:\n- DS elektriksiz arayan musteriye FX serisi onerin (DS yerine gecen model)\n- Alternatif olarak Verve serisi de onerilebilir\n- Mevcut DS stoklari bitene kadar satis devam edecek, ancak yenisi gelmeyecek\n\nOnemli: Stokta DS elektriksiz model varsa satilabilir, ancak musteri yeni model bekliyorsa FX serisi onerilmeli."
136
+ },
137
+ ]
138
+
139
+
140
+ def get_system_prompts():
141
+ """Sistem promptlarını döndür"""
142
+ return SYSTEM_PROMPTS
143
+
144
+
145
+ def get_prompts_by_category(category):
146
+ """Belirli kategorideki promptları döndür"""
147
+ return [prompt for prompt in SYSTEM_PROMPTS if prompt.get("category") == category]
148
+
149
+
150
+ def get_prompt_content_only():
151
+ """Sadece content kısmını döndür (eski format uyumluluğu için)"""
152
+ return [{"role": prompt["role"], "content": prompt["content"]} for prompt in SYSTEM_PROMPTS]
153
+
154
+
155
+ def get_active_prompts():
156
+ """Tarihe göre aktif promptları döndür - süresi geçmiş kampanyaları filtrele"""
157
+ today = datetime.now().date()
158
+ active_prompts = []
159
+
160
+ for prompt in SYSTEM_PROMPTS:
161
+ # Tarih alanları varsa kontrol et
162
+ start_date = prompt.get("start_date")
163
+ end_date = prompt.get("end_date")
164
+
165
+ if start_date and end_date:
166
+ # Tarih stringlerini date objesine çevir
167
+ start = datetime.strptime(start_date, "%Y-%m-%d").date()
168
+ end = datetime.strptime(end_date, "%Y-%m-%d").date()
169
+
170
+ # Sadece aktif tarih aralığındaysa ekle
171
+ if start <= today <= end:
172
+ active_prompts.append({"role": prompt["role"], "content": prompt["content"]})
173
+ else:
174
+ # Tarih alanı yoksa her zaman ekle
175
+ active_prompts.append({"role": prompt["role"], "content": prompt["content"]})
176
+
177
+ return active_prompts
178
+
179
+
180
+ def get_active_prompt_content_only():
181
+ """Aktif promptların sadece content kısmını döndür (API çağrıları için)"""
182
+ return get_active_prompts()
reminder_scheduler.py ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ Otomatik Hatırlatma Zamanlayıcısı
6
+ Belirli aralıklarla takipleri kontrol eder ve hatırlatma gönderir
7
+ """
8
+
9
+ import os
10
+ import time
11
+ import logging
12
+ from datetime import datetime
13
+ from typing import List
14
+
15
+ # Import our modules
16
+ from follow_up_system import FollowUpManager, format_reminder_message
17
+ from store_notification import send_store_notification
18
+
19
+ logging.basicConfig(
20
+ level=logging.INFO,
21
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
22
+ )
23
+ logger = logging.getLogger(__name__)
24
+
25
+ class ReminderScheduler:
26
+ """Hatırlatma zamanlayıcısı"""
27
+
28
+ def __init__(self, check_interval_minutes: int = 10):
29
+ """
30
+ Args:
31
+ check_interval_minutes: Kaç dakikada bir kontrol yapılacak
32
+ """
33
+ self.check_interval = check_interval_minutes * 60 # Saniyeye çevir
34
+ self.manager = FollowUpManager()
35
+ self.running = False
36
+
37
+ def send_reminder(self, follow_up) -> bool:
38
+ """Hatırlatma mesajı gönder"""
39
+
40
+ try:
41
+ # Hatırlatma mesajını hazırla
42
+ reminder_message = format_reminder_message(follow_up)
43
+
44
+ # Mehmet Bey'e bildirim gönder
45
+ result = send_store_notification(
46
+ customer_phone=follow_up.customer_phone,
47
+ customer_name=follow_up.customer_name,
48
+ product_name=follow_up.product_name,
49
+ action="reminder", # Yeni tip: hatırlatma
50
+ store_name=follow_up.store_name,
51
+ additional_info=f"⏰ TAKİP HATIRLATMASI: {follow_up.original_message}"
52
+ )
53
+
54
+ if result:
55
+ logger.info(f"✅ Hatırlatma gönderildi: {follow_up.id}")
56
+ return True
57
+ else:
58
+ logger.error(f"❌ Hatırlatma gönderilemedi: {follow_up.id}")
59
+ return False
60
+
61
+ except Exception as e:
62
+ logger.error(f"Hatırlatma gönderme hatası: {e}")
63
+ return False
64
+
65
+ def check_and_send_reminders(self):
66
+ """Bekleyen hatırlatmaları kontrol et ve gönder"""
67
+
68
+ logger.info("🔍 Hatırlatmalar kontrol ediliyor...")
69
+
70
+ # Bekleyen hatırlatmaları al
71
+ pending = self.manager.get_pending_reminders()
72
+
73
+ if not pending:
74
+ logger.info("📭 Bekleyen hatırlatma yok")
75
+ return
76
+
77
+ logger.info(f"📬 {len(pending)} hatırlatma bulundu")
78
+
79
+ # Her birini gönder
80
+ for follow_up in pending:
81
+ logger.info(f"📤 Hatırlatma gönderiliyor: {follow_up.customer_phone}")
82
+
83
+ if self.send_reminder(follow_up):
84
+ # Başarılıysa durumu güncelle
85
+ self.manager.mark_as_reminded(follow_up.id)
86
+
87
+ # Eğer çok kez hatırlatma yapıldıysa tamamlanmış say
88
+ if follow_up.reminded_count >= 2:
89
+ self.manager.mark_as_completed(follow_up.id)
90
+ logger.info(f"✅ Takip tamamlandı (2 hatırlatma yapıldı): {follow_up.id}")
91
+
92
+ # Mesajlar arası bekle (rate limit)
93
+ time.sleep(2)
94
+
95
+ def send_daily_summary(self):
96
+ """Günlük özet gönder"""
97
+
98
+ now = datetime.now()
99
+ todays = self.manager.get_todays_follow_ups()
100
+
101
+ if not todays:
102
+ return
103
+
104
+ # Özet mesajı hazırla
105
+ summary = f"""
106
+ 📊 **GÜNLÜK TAKİP ÖZETİ**
107
+ 📅 {now.strftime('%d.%m.%Y')}
108
+
109
+ Bugün takip edilmesi gereken {len(todays)} müşteri var:
110
+ """
111
+
112
+ for i, follow_up in enumerate(todays, 1):
113
+ status_emoji = "✅" if follow_up.status == "completed" else "⏳"
114
+ summary += f"""
115
+ {i}. {status_emoji} {follow_up.customer_phone.replace('whatsapp:', '')}
116
+ Ürün: {follow_up.product_name}
117
+ Durum: {follow_up.status}
118
+ """
119
+
120
+ summary += "\n📞 Takipleri tamamlamayı unutmayın!"
121
+
122
+ # Özet bildirimi gönder
123
+ send_store_notification(
124
+ customer_phone="whatsapp:+905439362335", # Direkt Mehmet Bey
125
+ customer_name="Sistem",
126
+ product_name="Günlük Özet",
127
+ action="info",
128
+ additional_info=summary
129
+ )
130
+
131
+ logger.info("📊 Günlük özet gönderildi")
132
+
133
+ def run(self):
134
+ """Zamanlayıcıyı başlat"""
135
+
136
+ self.running = True
137
+ logger.info(f"⏰ Hatırlatma zamanlayıcısı başlatıldı")
138
+ logger.info(f" Kontrol aralığı: {self.check_interval_minutes} dakika")
139
+
140
+ last_summary_date = None
141
+
142
+ while self.running:
143
+ try:
144
+ # Hatırlatmaları kontrol et
145
+ self.check_and_send_reminders()
146
+
147
+ # Günlük özet zamanı mı? (Saat 09:00 ve 18:00)
148
+ now = datetime.now()
149
+ if now.hour in [9, 18] and now.date() != last_summary_date:
150
+ self.send_daily_summary()
151
+ last_summary_date = now.date()
152
+
153
+ # Bekle
154
+ logger.info(f"⏳ {self.check_interval_minutes} dakika bekleniyor...")
155
+ time.sleep(self.check_interval)
156
+
157
+ except KeyboardInterrupt:
158
+ logger.info("⏹️ Zamanlayıcı durduruldu")
159
+ self.running = False
160
+ break
161
+ except Exception as e:
162
+ logger.error(f"Zamanlayıcı hatası: {e}")
163
+ time.sleep(60) # Hata durumunda 1 dakika bekle
164
+
165
+ def stop(self):
166
+ """Zamanlayıcıyı durdur"""
167
+ self.running = False
168
+ logger.info("⏹️ Zamanlayıcı durduruluyor...")
169
+
170
+ def run_scheduler():
171
+ """Ana fonksiyon"""
172
+
173
+ # Twilio credentials'ları ayarla
174
+ os.environ['TWILIO_ACCOUNT_SID'] = 'AC643d14a443b9fbcbc17a2828508870a6'
175
+ os.environ['TWILIO_AUTH_TOKEN'] = '9203f0328bfd737dc39bf9be5aa97ca9'
176
+
177
+ print("""
178
+ ╔════════════════════════════════════════╗
179
+ ║ TREK TAKİP HATIRLATMA SİSTEMİ ║
180
+ ║ Otomatik Müşteri Takibi ║
181
+ ╚════════════════════════════════════════╝
182
+
183
+ Ayarlar:
184
+ - Kontrol aralığı: 10 dakika
185
+ - Günlük özet: 09:00 ve 18:00
186
+ - Mehmet Bey: +905439362335
187
+
188
+ Başlatılıyor...
189
+ """)
190
+
191
+ scheduler = ReminderScheduler(check_interval_minutes=10)
192
+
193
+ try:
194
+ scheduler.run()
195
+ except KeyboardInterrupt:
196
+ print("\n👋 Sistem kapatılıyor...")
197
+ scheduler.stop()
198
+
199
+ if __name__ == "__main__":
200
+ # Test modda çalıştır (1 dakika aralıklarla)
201
+ if os.getenv("TEST_MODE"):
202
+ print("🧪 TEST MODU - 1 dakika aralıklarla kontrol")
203
+ scheduler = ReminderScheduler(check_interval_minutes=1)
204
+ else:
205
+ scheduler = ReminderScheduler(check_interval_minutes=10)
206
+
207
+ run_scheduler()
requirements.txt ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ schedule
2
+ gradio>=3.41.2
3
+ requests
4
+ pandas
5
+ python-docx
6
+ huggingface_hub
7
+ schedule
8
+ openpyxl
9
+ spaces
10
+ google-auth
11
+ google-auth-oauthlib
12
+ google-auth-httplib2
13
+ google-api-python-client
14
+ fastapi
15
+ uvicorn
16
+ twilio
17
+ python-multipart==0.0.6
18
+ pydantic>=2.10.0
search_marlin_products.py ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ import requests
3
+ import xml.etree.ElementTree as ET
4
+
5
+ def search_marlin_products():
6
+ """B2B API'den MARLIN ürünlerini listele"""
7
+ try:
8
+ warehouse_url = 'https://video.trek-turkey.com/bizimhesap-warehouse-xml.php'
9
+ response = requests.get(warehouse_url, verify=False, timeout=15)
10
+
11
+ if response.status_code != 200:
12
+ return None
13
+
14
+ root = ET.fromstring(response.content)
15
+
16
+ # Turkish character normalization function
17
+ turkish_map = {'ı': 'i', 'ğ': 'g', 'ü': 'u', 'ş': 's', 'ö': 'o', 'ç': 'c', 'İ': 'i', 'I': 'i'}
18
+
19
+ def normalize_turkish(text):
20
+ import unicodedata
21
+ text = unicodedata.normalize('NFD', text)
22
+ text = ''.join(char for char in text if unicodedata.category(char) != 'Mn')
23
+ for tr_char, en_char in turkish_map.items():
24
+ text = text.replace(tr_char, en_char)
25
+ return text
26
+
27
+ marlin_products = []
28
+
29
+ # MARLIN içeren tüm ürünleri bul
30
+ for product in root.findall('Product'):
31
+ product_name_elem = product.find('ProductName')
32
+ if product_name_elem is not None and product_name_elem.text:
33
+ xml_product_name = product_name_elem.text.strip()
34
+ normalized_xml = normalize_turkish(xml_product_name.lower())
35
+
36
+ if 'marlin' in normalized_xml:
37
+ # Stok durumunu kontrol et
38
+ has_stock = False
39
+ stock_info = []
40
+
41
+ warehouses = product.find('Warehouses')
42
+ if warehouses is not None:
43
+ for warehouse in warehouses.findall('Warehouse'):
44
+ name_elem = warehouse.find('Name')
45
+ stock_elem = warehouse.find('Stock')
46
+
47
+ if name_elem is not None and stock_elem is not None:
48
+ warehouse_name = name_elem.text if name_elem.text else "Bilinmeyen"
49
+ try:
50
+ stock_count = int(stock_elem.text) if stock_elem.text else 0
51
+ if stock_count > 0:
52
+ stock_info.append(f"{warehouse_name}: {stock_count}")
53
+ has_stock = True
54
+ except (ValueError, TypeError):
55
+ pass
56
+
57
+ marlin_products.append({
58
+ 'name': xml_product_name,
59
+ 'has_stock': has_stock,
60
+ 'stock_info': stock_info
61
+ })
62
+
63
+ return marlin_products
64
+
65
+ except Exception as e:
66
+ print(f"Arama hatası: {e}")
67
+ return None
68
+
69
+ def search_turuncu_products():
70
+ """B2B API'den TURUNCU içeren ürünleri listele"""
71
+ try:
72
+ warehouse_url = 'https://video.trek-turkey.com/bizimhesap-warehouse-xml.php'
73
+ response = requests.get(warehouse_url, verify=False, timeout=15)
74
+
75
+ if response.status_code != 200:
76
+ return None
77
+
78
+ root = ET.fromstring(response.content)
79
+
80
+ # Turkish character normalization function
81
+ turkish_map = {'ı': 'i', 'ğ': 'g', 'ü': 'u', 'ş': 's', 'ö': 'o', 'ç': 'c', 'İ': 'i', 'I': 'i'}
82
+
83
+ def normalize_turkish(text):
84
+ import unicodedata
85
+ text = unicodedata.normalize('NFD', text)
86
+ text = ''.join(char for char in text if unicodedata.category(char) != 'Mn')
87
+ for tr_char, en_char in turkish_map.items():
88
+ text = text.replace(tr_char, en_char)
89
+ return text
90
+
91
+ turuncu_products = []
92
+
93
+ # TURUNCU içeren tüm ürünleri bul
94
+ for product in root.findall('Product'):
95
+ product_name_elem = product.find('ProductName')
96
+ if product_name_elem is not None and product_name_elem.text:
97
+ xml_product_name = product_name_elem.text.strip()
98
+ normalized_xml = normalize_turkish(xml_product_name.lower())
99
+
100
+ if 'turuncu' in normalized_xml:
101
+ # Stok durumunu kontrol et
102
+ has_stock = False
103
+ stock_info = []
104
+
105
+ warehouses = product.find('Warehouses')
106
+ if warehouses is not None:
107
+ for warehouse in warehouses.findall('Warehouse'):
108
+ name_elem = warehouse.find('Name')
109
+ stock_elem = warehouse.find('Stock')
110
+
111
+ if name_elem is not None and stock_elem is not None:
112
+ warehouse_name = name_elem.text if name_elem.text else "Bilinmeyen"
113
+ try:
114
+ stock_count = int(stock_elem.text) if stock_elem.text else 0
115
+ if stock_count > 0:
116
+ stock_info.append(f"{warehouse_name}: {stock_count}")
117
+ has_stock = True
118
+ except (ValueError, TypeError):
119
+ pass
120
+
121
+ turuncu_products.append({
122
+ 'name': xml_product_name,
123
+ 'has_stock': has_stock,
124
+ 'stock_info': stock_info
125
+ })
126
+
127
+ return turuncu_products
128
+
129
+ except Exception as e:
130
+ print(f"Arama hatası: {e}")
131
+ return None
132
+
133
+ if __name__ == "__main__":
134
+ print("=== MARLIN ÜRÜNLERİ ===")
135
+ marlin_products = search_marlin_products()
136
+ if marlin_products:
137
+ print(f"Toplam {len(marlin_products)} MARLIN ürünü bulundu:")
138
+ for i, product in enumerate(marlin_products, 1):
139
+ print(f"{i:2}. {product['name']}")
140
+ if product['has_stock']:
141
+ print(f" STOKTA: {', '.join(product['stock_info'])}")
142
+ else:
143
+ print(f" STOKTA DEĞİL")
144
+ print()
145
+
146
+ print("\n" + "="*50)
147
+ print("=== TURUNCU ÜRÜNLERİ ===")
148
+ turuncu_products = search_turuncu_products()
149
+ if turuncu_products:
150
+ print(f"Toplam {len(turuncu_products)} TURUNCU ürünü bulundu:")
151
+ for i, product in enumerate(turuncu_products, 1):
152
+ print(f"{i:2}. {product['name']}")
153
+ if product['has_stock']:
154
+ print(f" STOKTA: {', '.join(product['stock_info'])}")
155
+ else:
156
+ print(f" STOKTA DEĞİL")
157
+ print()
smart_warehouse.py ADDED
@@ -0,0 +1,262 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Smart warehouse stock finder using GPT-5's intelligence"""
2
+
3
+ import requests
4
+ import re
5
+ import os
6
+ import json
7
+
8
+ def get_warehouse_stock_smart(user_message, previous_result=None):
9
+ """Let GPT-5 intelligently find products or filter by warehouse"""
10
+
11
+ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
12
+
13
+ # Check if user is asking about specific warehouse
14
+ warehouse_keywords = {
15
+ 'caddebostan': 'Caddebostan',
16
+ 'ortaköy': 'Ortaköy',
17
+ 'ortakoy': 'Ortaköy',
18
+ 'alsancak': 'Alsancak',
19
+ 'izmir': 'Alsancak',
20
+ 'bahçeköy': 'Bahçeköy',
21
+ 'bahcekoy': 'Bahçeköy'
22
+ }
23
+
24
+ user_lower = user_message.lower()
25
+ asked_warehouse = None
26
+ for keyword, warehouse in warehouse_keywords.items():
27
+ if keyword in user_lower:
28
+ asked_warehouse = warehouse
29
+ break
30
+
31
+ # Get XML data with retry
32
+ xml_text = None
33
+ for attempt in range(3): # Try 3 times
34
+ try:
35
+ url = 'https://video.trek-turkey.com/bizimhesap-warehouse-xml-b2b-api-v2.php'
36
+ timeout_val = 10 + (attempt * 5) # Increase timeout on each retry: 10, 15, 20
37
+ response = requests.get(url, verify=False, timeout=timeout_val)
38
+ xml_text = response.text
39
+ print(f"DEBUG - XML fetched: {len(xml_text)} characters (attempt {attempt+1})")
40
+ break
41
+ except requests.exceptions.Timeout:
42
+ print(f"XML fetch timeout (attempt {attempt+1}/3, timeout={timeout_val}s)")
43
+ if attempt == 2:
44
+ print("All attempts failed - timeout")
45
+ return None
46
+ except Exception as e:
47
+ print(f"XML fetch error: {e}")
48
+ return None
49
+
50
+ # Extract just product blocks to reduce token usage
51
+ product_pattern = r'<Product>(.*?)</Product>'
52
+ all_products = re.findall(product_pattern, xml_text, re.DOTALL)
53
+
54
+ # Create a simplified product list for GPT
55
+ products_summary = []
56
+ for i, product_block in enumerate(all_products):
57
+ name_match = re.search(r'<ProductName><!\[CDATA\[(.*?)\]\]></ProductName>', product_block)
58
+ variant_match = re.search(r'<ProductVariant><!\[CDATA\[(.*?)\]\]></ProductVariant>', product_block)
59
+
60
+ if name_match:
61
+ # Check warehouse stock for this product
62
+ warehouses_with_stock = []
63
+ warehouse_regex = r'<Warehouse>.*?<Name><!\[CDATA\[(.*?)\]\]></Name>.*?<Stock>(.*?)</Stock>.*?</Warehouse>'
64
+ warehouses = re.findall(warehouse_regex, product_block, re.DOTALL)
65
+
66
+ for wh_name, wh_stock in warehouses:
67
+ try:
68
+ if int(wh_stock.strip()) > 0:
69
+ warehouses_with_stock.append(wh_name)
70
+ except:
71
+ pass
72
+
73
+ product_info = {
74
+ "index": i,
75
+ "name": name_match.group(1),
76
+ "variant": variant_match.group(1) if variant_match else "",
77
+ "warehouses": warehouses_with_stock
78
+ }
79
+ products_summary.append(product_info)
80
+
81
+ # If user is asking about specific warehouse, include that in prompt
82
+ warehouse_filter = ""
83
+ if asked_warehouse:
84
+ warehouse_filter = f"\nIMPORTANT: User is asking specifically about {asked_warehouse} warehouse. Only return products available in that warehouse."
85
+
86
+ # Let GPT-5 find ALL matching products
87
+ smart_prompt = f"""User is asking: "{user_message}"
88
+
89
+ Find ALL products that match this query from the list below.
90
+ If user asks about specific size (S, M, L, XL, XXL, SMALL, MEDIUM, LARGE, X-LARGE), return only that size.
91
+ If user asks generally (without size), return ALL variants of the product.
92
+ {warehouse_filter}
93
+
94
+ IMPORTANT BRAND AND PRODUCT TYPE RULES:
95
+ - GOBIK: Spanish textile brand we import. When user asks about "gobik", return ALL products with "GOBIK" in the name.
96
+ - Product names contain type information: FORMA (jersey/cycling shirt), TAYT (tights), İÇLİK (base layer), YAĞMURLUK (raincoat), etc.
97
+ - Understand Turkish/English terms:
98
+ * "erkek forma" / "men's jersey" -> Find products with FORMA in name
99
+ * "tayt" / "tights" -> Find products with TAYT in name
100
+ * "içlik" / "base layer" -> Find products with İÇLİK in name
101
+ * "yağmurluk" / "raincoat" -> Find products with YAĞMURLUK in name
102
+ - Gender: UNISEX means for both men and women. If no gender specified, it's typically men's.
103
+ - Be smart: "erkek forma" should find all FORMA products (excluding women-specific if any)
104
+
105
+ Products list (with warehouse availability):
106
+ {json.dumps(products_summary, ensure_ascii=False, indent=2)}
107
+
108
+ Return index numbers of ALL matching products as comma-separated list (e.g., "5,8,12,15").
109
+ If no products found, return: -1
110
+
111
+ Examples:
112
+ - "madone sl 6 var mı" -> Return ALL Madone SL 6 variants
113
+ - "erkek forma" -> Return all products with FORMA in name
114
+ - "gobik tayt" -> Return all GOBIK products with TAYT in name
115
+ - "içlik var mı" -> Return all products with İÇLİK in name
116
+ - "gobik erkek forma" -> Return all GOBIK products with FORMA in name
117
+ - "yağmurluk medium" -> Return all YAĞMURLUK products in MEDIUM size"""
118
+
119
+ headers = {
120
+ "Content-Type": "application/json",
121
+ "Authorization": f"Bearer {OPENAI_API_KEY}"
122
+ }
123
+
124
+ payload = {
125
+ "model": "gpt-5-chat-latest",
126
+ "messages": [
127
+ {"role": "system", "content": "You are a product matcher. Find ALL matching products. Return only index numbers."},
128
+ {"role": "user", "content": smart_prompt}
129
+ ],
130
+ "temperature": 0,
131
+ "max_tokens": 100
132
+ }
133
+
134
+ try:
135
+ response = requests.post(
136
+ "https://api.openai.com/v1/chat/completions",
137
+ headers=headers,
138
+ json=payload,
139
+ timeout=10
140
+ )
141
+
142
+ if response.status_code == 200:
143
+ result = response.json()
144
+ indices_str = result['choices'][0]['message']['content'].strip()
145
+
146
+ if indices_str == "-1":
147
+ return ["Ürün bulunamadı"]
148
+
149
+ try:
150
+ # Parse multiple indices
151
+ indices = [int(idx.strip()) for idx in indices_str.split(',')]
152
+
153
+ # Collect all matching products
154
+ all_variants = []
155
+ warehouse_stock = {}
156
+
157
+ for idx in indices:
158
+ if 0 <= idx < len(all_products):
159
+ product_block = all_products[idx]
160
+
161
+ # Get product name and variant
162
+ name_match = re.search(r'<ProductName><!\[CDATA\[(.*?)\]\]></ProductName>', product_block)
163
+ variant_match = re.search(r'<ProductVariant><!\[CDATA\[(.*?)\]\]></ProductVariant>', product_block)
164
+
165
+ if name_match:
166
+ product_name = name_match.group(1)
167
+ variant = variant_match.group(1) if variant_match else ""
168
+
169
+ # Track this variant
170
+ variant_info = {
171
+ 'name': product_name,
172
+ 'variant': variant,
173
+ 'warehouses': []
174
+ }
175
+
176
+ # Get warehouse stock
177
+ warehouse_regex = r'<Warehouse>.*?<Name><!\[CDATA\[(.*?)\]\]></Name>.*?<Stock>(.*?)</Stock>.*?</Warehouse>'
178
+ warehouses = re.findall(warehouse_regex, product_block, re.DOTALL)
179
+
180
+ for wh_name, wh_stock in warehouses:
181
+ try:
182
+ stock = int(wh_stock.strip())
183
+ if stock > 0:
184
+ display_name = format_warehouse_name(wh_name)
185
+ variant_info['warehouses'].append({
186
+ 'name': display_name,
187
+ 'stock': stock
188
+ })
189
+
190
+ # Track total stock per warehouse
191
+ if display_name not in warehouse_stock:
192
+ warehouse_stock[display_name] = 0
193
+ warehouse_stock[display_name] += stock
194
+ except:
195
+ pass
196
+
197
+ if variant_info['warehouses']: # Only add if has stock
198
+ all_variants.append(variant_info)
199
+
200
+ # Format result
201
+ result = []
202
+
203
+ if asked_warehouse:
204
+ # Filter for specific warehouse
205
+ warehouse_variants = []
206
+ for variant in all_variants:
207
+ for wh in variant['warehouses']:
208
+ if asked_warehouse in wh['name']:
209
+ warehouse_variants.append({
210
+ 'name': variant['name'],
211
+ 'variant': variant['variant'],
212
+ 'stock': wh['stock']
213
+ })
214
+
215
+ if warehouse_variants:
216
+ result.append(f"{format_warehouse_name(asked_warehouse)} mağazasında mevcut:")
217
+ for v in warehouse_variants:
218
+ result.append(f"• {v['name']} ({v['variant']})")
219
+ else:
220
+ result.append(f"{format_warehouse_name(asked_warehouse)} mağazasında bu ürün mevcut değil")
221
+ else:
222
+ # Show all variants and warehouses
223
+ if all_variants:
224
+ result.append(f"Bulunan {len(all_variants)} varyant:")
225
+
226
+ # Show ALL variants (not just first 5)
227
+ for variant in all_variants:
228
+ variant_text = f" ({variant['variant']})" if variant['variant'] else ""
229
+ result.append(f"• {variant['name']}{variant_text}")
230
+
231
+ result.append("")
232
+ result.append("Mağaza stok durumu:")
233
+ for warehouse, total_stock in sorted(warehouse_stock.items()):
234
+ result.append(f"• {warehouse}: Mevcut")
235
+ else:
236
+ result.append("Hiçbir mağazada stok yok")
237
+
238
+ return result
239
+
240
+ except (ValueError, IndexError) as e:
241
+ print(f"DEBUG - Error parsing indices: {e}")
242
+ return None
243
+ else:
244
+ print(f"GPT API error: {response.status_code}")
245
+ return None
246
+
247
+ except Exception as e:
248
+ print(f"Error calling GPT: {e}")
249
+ return None
250
+
251
+ def format_warehouse_name(wh_name):
252
+ """Format warehouse name nicely"""
253
+ if "CADDEBOSTAN" in wh_name:
254
+ return "Caddebostan mağazası"
255
+ elif "ORTAKÖY" in wh_name:
256
+ return "Ortaköy mağazası"
257
+ elif "ALSANCAK" in wh_name:
258
+ return "İzmir Alsancak mağazası"
259
+ elif "BAHCEKOY" in wh_name or "BAHÇEKÖY" in wh_name:
260
+ return "Bahçeköy mağazası"
261
+ else:
262
+ return wh_name.replace("MAGAZA DEPO", "").strip()
smart_warehouse_complete.py ADDED
@@ -0,0 +1,393 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Smart warehouse stock finder with price and link information"""
2
+
3
+ import requests
4
+ import re
5
+ import os
6
+ import json
7
+ import xml.etree.ElementTree as ET
8
+
9
+ def get_product_price_and_link(product_name, variant=None):
10
+ """Get price and link from Trek website XML"""
11
+ try:
12
+ # Get Trek product catalog
13
+ url = 'https://www.trekbisiklet.com.tr/output/8582384479'
14
+ response = requests.get(url, verify=False, timeout=10)
15
+
16
+ if response.status_code != 200:
17
+ return None, None
18
+
19
+ root = ET.fromstring(response.content)
20
+
21
+ # Turkish character normalization FIRST (before lower)
22
+ tr_map = {'İ': 'i', 'I': 'i', 'ı': 'i', 'Ğ': 'g', 'ğ': 'g', 'Ü': 'u', 'ü': 'u', 'Ş': 's', 'ş': 's', 'Ö': 'o', 'ö': 'o', 'Ç': 'c', 'ç': 'c'}
23
+
24
+ # Apply normalization to original
25
+ search_name_norm = product_name
26
+ search_variant_norm = variant if variant else ""
27
+ for tr, en in tr_map.items():
28
+ search_name_norm = search_name_norm.replace(tr, en)
29
+ search_variant_norm = search_variant_norm.replace(tr, en)
30
+
31
+ # Now lowercase
32
+ search_name = search_name_norm.lower()
33
+ search_variant = search_variant_norm.lower()
34
+
35
+ best_match = None
36
+ best_score = 0
37
+
38
+ for item in root.findall('item'):
39
+ # Get product name
40
+ rootlabel_elem = item.find('rootlabel')
41
+ if rootlabel_elem is None or not rootlabel_elem.text:
42
+ continue
43
+
44
+ item_name = rootlabel_elem.text.lower()
45
+ for tr, en in tr_map.items():
46
+ item_name = item_name.replace(tr, en)
47
+
48
+ # Calculate match score
49
+ score = 0
50
+ name_parts = search_name.split()
51
+ for part in name_parts:
52
+ if part in item_name:
53
+ score += 1
54
+
55
+ # Check variant if specified
56
+ if variant and search_variant in item_name:
57
+ score += 2 # Variant match is important
58
+
59
+ if score > best_score:
60
+ best_score = score
61
+ best_match = item
62
+
63
+ if best_match and best_score > 0:
64
+ # Extract price
65
+ price_elem = best_match.find('priceTaxWithCur')
66
+ price = price_elem.text if price_elem is not None and price_elem.text else None
67
+
68
+ # Round price
69
+ if price:
70
+ try:
71
+ price_float = float(price)
72
+ if price_float > 200000:
73
+ rounded = round(price_float / 5000) * 5000
74
+ price = f"{int(rounded):,}".replace(',', '.') + " TL"
75
+ elif price_float > 30000:
76
+ rounded = round(price_float / 1000) * 1000
77
+ price = f"{int(rounded):,}".replace(',', '.') + " TL"
78
+ elif price_float > 10000:
79
+ rounded = round(price_float / 100) * 100
80
+ price = f"{int(rounded):,}".replace(',', '.') + " TL"
81
+ else:
82
+ rounded = round(price_float / 10) * 10
83
+ price = f"{int(rounded):,}".replace(',', '.') + " TL"
84
+ except:
85
+ price = f"{price} TL"
86
+
87
+ # Extract link (field name is productLink, not productUrl!)
88
+ link_elem = best_match.find('productLink')
89
+ link = link_elem.text if link_elem is not None and link_elem.text else None
90
+
91
+ return price, link
92
+
93
+ return None, None
94
+
95
+ except Exception as e:
96
+ print(f"Error getting price/link: {e}")
97
+ return None, None
98
+
99
+ def get_warehouse_stock_smart_complete(user_message):
100
+ """Complete smart warehouse search for WhatsApp - BF algorithm"""
101
+
102
+ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
103
+
104
+ # Check if user is asking about specific warehouse
105
+ warehouse_keywords = {
106
+ 'caddebostan': 'Caddebostan',
107
+ 'ortaköy': 'Ortaköy',
108
+ 'ortakoy': 'Ortaköy',
109
+ 'alsancak': 'Alsancak',
110
+ 'izmir': 'Alsancak',
111
+ 'bahçeköy': 'Bahçeköy',
112
+ 'bahcekoy': 'Bahçeköy'
113
+ }
114
+
115
+ user_lower = user_message.lower()
116
+ asked_warehouse = None
117
+ for keyword, warehouse in warehouse_keywords.items():
118
+ if keyword in user_lower:
119
+ asked_warehouse = warehouse
120
+ break
121
+
122
+ # Get XML data with retry
123
+ xml_text = None
124
+ for attempt in range(3):
125
+ try:
126
+ url = 'https://video.trek-turkey.com/bizimhesap-warehouse-xml-b2b-api-v2.php'
127
+ timeout_val = 10 + (attempt * 5)
128
+ response = requests.get(url, verify=False, timeout=timeout_val)
129
+ xml_text = response.text
130
+ print(f"DEBUG - XML fetched: {len(xml_text)} characters (attempt {attempt+1})")
131
+ break
132
+ except requests.exceptions.Timeout:
133
+ print(f"XML fetch timeout (attempt {attempt+1}/3, timeout={timeout_val}s)")
134
+ if attempt == 2:
135
+ print("All attempts failed - timeout")
136
+ return None
137
+ except Exception as e:
138
+ print(f"XML fetch error: {e}")
139
+ return None
140
+
141
+ # Extract product blocks
142
+ product_pattern = r'<Product>(.*?)</Product>'
143
+ all_products = re.findall(product_pattern, xml_text, re.DOTALL)
144
+
145
+ # Create simplified product list for GPT
146
+ products_summary = []
147
+ for i, product_block in enumerate(all_products):
148
+ name_match = re.search(r'<ProductName><!\[CDATA\[(.*?)\]\]></ProductName>', product_block)
149
+ variant_match = re.search(r'<ProductVariant><!\[CDATA\[(.*?)\]\]></ProductVariant>', product_block)
150
+
151
+ if name_match:
152
+ warehouses_with_stock = []
153
+ warehouse_regex = r'<Warehouse>.*?<Name><!\[CDATA\[(.*?)\]\]></Name>.*?<Stock>(.*?)</Stock>.*?</Warehouse>'
154
+ warehouses = re.findall(warehouse_regex, product_block, re.DOTALL)
155
+
156
+ for wh_name, wh_stock in warehouses:
157
+ try:
158
+ if int(wh_stock.strip()) > 0:
159
+ warehouses_with_stock.append(wh_name)
160
+ except:
161
+ pass
162
+
163
+ product_info = {
164
+ "index": i,
165
+ "name": name_match.group(1),
166
+ "variant": variant_match.group(1) if variant_match else "",
167
+ "warehouses": warehouses_with_stock
168
+ }
169
+ products_summary.append(product_info)
170
+
171
+ # Prepare warehouse filter if needed
172
+ warehouse_filter = ""
173
+ if asked_warehouse:
174
+ warehouse_filter = f"\nIMPORTANT: User is asking specifically about {asked_warehouse} warehouse. Only return products available in that warehouse."
175
+
176
+ # Log what we're searching for
177
+ print(f"DEBUG - Searching for: '{user_message}'")
178
+ print(f"DEBUG - Total products to search: {len(products_summary)}")
179
+
180
+ # Check if the target product exists
181
+ search_term = user_message.upper()
182
+ matching_products = []
183
+ for p in products_summary:
184
+ if search_term in p['name'].upper():
185
+ matching_products.append(p)
186
+
187
+ if matching_products:
188
+ print(f"DEBUG - Found {len(matching_products)} products containing '{user_message}':")
189
+ for p in matching_products[:3]:
190
+ print(f" - Index {p['index']}: {p['name']} ({p['variant']})")
191
+
192
+ # GPT-5 prompt with enhanced instructions
193
+ smart_prompt = f"""User is asking: "{user_message}"
194
+
195
+ Find ALL products that match this query from the list below.
196
+ If user asks about specific size (S, M, L, XL, XXL, SMALL, MEDIUM, LARGE, X-LARGE), return only that size.
197
+ If user asks generally (without size), return ALL variants of the product.
198
+ {warehouse_filter}
199
+
200
+ IMPORTANT BRAND AND PRODUCT TYPE RULES:
201
+ - GOBIK: Spanish textile brand we import. When user asks about "gobik", return ALL products with "GOBIK" in the name.
202
+ - Product names contain type information: FORMA (jersey/cycling shirt), TAYT (tights), İÇLİK (base layer), YAĞMURLUK (raincoat), etc.
203
+ - Understand Turkish/English terms:
204
+ * "erkek forma" / "men's jersey" -> Find products with FORMA in name
205
+ * "tayt" / "tights" -> Find products with TAYT in name
206
+ * "içlik" / "base layer" -> Find products with İÇLİK in name
207
+ * "yağmurluk" / "raincoat" -> Find products with YAĞMURLUK in name
208
+ - Gender: UNISEX means for both men and women. If no gender specified, it's typically men's.
209
+
210
+ Products list (with warehouse availability):
211
+ {json.dumps(products_summary, ensure_ascii=False, indent=2)}
212
+
213
+ Return ONLY index numbers of ALL matching products as comma-separated list (e.g., "5,8,12,15").
214
+ If no products found, return ONLY: -1
215
+ DO NOT return empty string or any explanation, ONLY numbers or -1
216
+
217
+ Examples of correct responses:
218
+ - "2,5,8,12,15,20" (multiple products found)
219
+ - "45" (single product found)
220
+ - "-1" (no products found)"""
221
+
222
+ headers = {
223
+ "Content-Type": "application/json",
224
+ "Authorization": f"Bearer {OPENAI_API_KEY}"
225
+ }
226
+
227
+ payload = {
228
+ "model": "gpt-5-chat-latest",
229
+ "messages": [
230
+ {"role": "system", "content": "You are a product matcher. Find ALL matching products. Return only index numbers."},
231
+ {"role": "user", "content": smart_prompt}
232
+ ],
233
+ "temperature": 0,
234
+ "max_tokens": 100
235
+ }
236
+
237
+ try:
238
+ response = requests.post(
239
+ "https://api.openai.com/v1/chat/completions",
240
+ headers=headers,
241
+ json=payload,
242
+ timeout=10
243
+ )
244
+
245
+ if response.status_code == 200:
246
+ result = response.json()
247
+ indices_str = result['choices'][0]['message']['content'].strip()
248
+
249
+ print(f"DEBUG - GPT-5 response: '{indices_str}'")
250
+ print(f"DEBUG - Query was: '{user_message}'")
251
+ print(f"DEBUG - Products sent to GPT: {len(products_summary)} items")
252
+
253
+ # Handle empty response
254
+ if not indices_str or indices_str == "-1":
255
+ return ["Ürün bulunamadı"]
256
+
257
+ try:
258
+ # Filter out empty strings and parse indices
259
+ indices = []
260
+ for idx in indices_str.split(','):
261
+ idx = idx.strip()
262
+ if idx and idx.isdigit():
263
+ indices.append(int(idx))
264
+
265
+ # Collect all matching products with price/link
266
+ all_variants = []
267
+ warehouse_stock = {}
268
+
269
+ for idx in indices:
270
+ if 0 <= idx < len(all_products):
271
+ product_block = all_products[idx]
272
+
273
+ # Get product details
274
+ name_match = re.search(r'<ProductName><!\[CDATA\[(.*?)\]\]></ProductName>', product_block)
275
+ variant_match = re.search(r'<ProductVariant><!\[CDATA\[(.*?)\]\]></ProductVariant>', product_block)
276
+
277
+ if name_match:
278
+ product_name = name_match.group(1)
279
+ variant = variant_match.group(1) if variant_match else ""
280
+
281
+ # Get price and link from Trek website
282
+ price, link = get_product_price_and_link(product_name, variant)
283
+
284
+ variant_info = {
285
+ 'name': product_name,
286
+ 'variant': variant,
287
+ 'price': price,
288
+ 'link': link,
289
+ 'warehouses': []
290
+ }
291
+
292
+ # Get warehouse stock
293
+ warehouse_regex = r'<Warehouse>.*?<Name><!\[CDATA\[(.*?)\]\]></Name>.*?<Stock>(.*?)</Stock>.*?</Warehouse>'
294
+ warehouses = re.findall(warehouse_regex, product_block, re.DOTALL)
295
+
296
+ for wh_name, wh_stock in warehouses:
297
+ try:
298
+ stock = int(wh_stock.strip())
299
+ if stock > 0:
300
+ display_name = format_warehouse_name(wh_name)
301
+ variant_info['warehouses'].append({
302
+ 'name': display_name,
303
+ 'stock': stock
304
+ })
305
+
306
+ if display_name not in warehouse_stock:
307
+ warehouse_stock[display_name] = 0
308
+ warehouse_stock[display_name] += stock
309
+ except:
310
+ pass
311
+
312
+ if variant_info['warehouses']:
313
+ all_variants.append(variant_info)
314
+
315
+ # Format result
316
+ result = []
317
+
318
+ if asked_warehouse:
319
+ # Filter for specific warehouse
320
+ warehouse_variants = []
321
+ for variant in all_variants:
322
+ for wh in variant['warehouses']:
323
+ if asked_warehouse in wh['name']:
324
+ warehouse_variants.append(variant)
325
+ break
326
+
327
+ if warehouse_variants:
328
+ result.append(f"{format_warehouse_name(asked_warehouse)} mağazasında mevcut:")
329
+ for v in warehouse_variants:
330
+ variant_text = f" ({v['variant']})" if v['variant'] else ""
331
+ result.append(f"• {v['name']}{variant_text}")
332
+ if v['price']:
333
+ result.append(f" Fiyat: {v['price']}")
334
+ if v['link']:
335
+ result.append(f" Link: {v['link']}")
336
+ else:
337
+ result.append(f"{format_warehouse_name(asked_warehouse)} mağazasında bu ürün mevcut değil")
338
+ else:
339
+ # Show all variants
340
+ if all_variants:
341
+ # Group by product name for cleaner display
342
+ product_groups = {}
343
+ for variant in all_variants:
344
+ if variant['name'] not in product_groups:
345
+ product_groups[variant['name']] = []
346
+ product_groups[variant['name']].append(variant)
347
+
348
+ result.append(f"Bulunan ürünler:")
349
+
350
+ for product_name, variants in product_groups.items():
351
+ result.append(f"\n{product_name}:")
352
+
353
+ # Show first variant's price and link (usually same for all variants)
354
+ if variants[0]['price']:
355
+ result.append(f"Fiyat: {variants[0]['price']}")
356
+ if variants[0]['link']:
357
+ result.append(f"Link: {variants[0]['link']}")
358
+
359
+ # Show variants and their availability
360
+ for v in variants:
361
+ if v['variant']:
362
+ warehouses_str = ", ".join([w['name'].replace(' mağazası', '') for w in v['warehouses']])
363
+ result.append(f"• {v['variant']}: {warehouses_str}")
364
+
365
+ else:
366
+ result.append("Hiçbir mağazada stok yok")
367
+
368
+ return result
369
+
370
+ except (ValueError, IndexError) as e:
371
+ print(f"DEBUG - Error parsing indices: {e}")
372
+ return None
373
+ else:
374
+ print(f"GPT API error: {response.status_code}")
375
+ print(f"Response: {response.text[:500]}")
376
+ return None
377
+
378
+ except Exception as e:
379
+ print(f"Error calling GPT: {e}")
380
+ return None
381
+
382
+ def format_warehouse_name(wh_name):
383
+ """Format warehouse name nicely"""
384
+ if "CADDEBOSTAN" in wh_name:
385
+ return "Caddebostan mağazası"
386
+ elif "ORTAKÖY" in wh_name:
387
+ return "Ortaköy mağazası"
388
+ elif "ALSANCAK" in wh_name:
389
+ return "İzmir Alsancak mağazası"
390
+ elif "BAHCEKOY" in wh_name or "BAHÇEKÖY" in wh_name:
391
+ return "Bahçeköy mağazası"
392
+ else:
393
+ return wh_name.replace("MAGAZA DEPO", "").strip()
smart_warehouse_whatsapp.py ADDED
@@ -0,0 +1,284 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Smart warehouse stock finder for WhatsApp - GPT-5 powered"""
2
+
3
+ import requests
4
+ import re
5
+ import os
6
+ import json
7
+ import xml.etree.ElementTree as ET
8
+
9
+ def get_product_price_and_link(product_name, variant=None):
10
+ """Get price and link from Trek website XML"""
11
+ try:
12
+ url = 'https://www.trekbisiklet.com.tr/output/8582384479'
13
+ response = requests.get(url, verify=False, timeout=10)
14
+
15
+ if response.status_code != 200:
16
+ return None, None
17
+
18
+ root = ET.fromstring(response.content)
19
+
20
+ # Turkish character normalization FIRST (before lower)
21
+ tr_map = {'İ': 'i', 'I': 'i', 'ı': 'i', 'Ğ': 'g', 'ğ': 'g', 'Ü': 'u', 'ü': 'u', 'Ş': 's', 'ş': 's', 'Ö': 'o', 'ö': 'o', 'Ç': 'c', 'ç': 'c'}
22
+
23
+ # Apply normalization to original
24
+ search_name_norm = product_name
25
+ search_variant_norm = variant if variant else ""
26
+ for tr, en in tr_map.items():
27
+ search_name_norm = search_name_norm.replace(tr, en)
28
+ search_variant_norm = search_variant_norm.replace(tr, en)
29
+
30
+ # Now lowercase
31
+ search_name = search_name_norm.lower()
32
+ search_variant = search_variant_norm.lower()
33
+
34
+ best_match = None
35
+ best_score = 0
36
+
37
+ for item in root.findall('item'):
38
+ rootlabel_elem = item.find('rootlabel')
39
+ if rootlabel_elem is None or not rootlabel_elem.text:
40
+ continue
41
+
42
+ item_name = rootlabel_elem.text.lower()
43
+ for tr, en in tr_map.items():
44
+ item_name = item_name.replace(tr, en)
45
+
46
+ # Calculate match score
47
+ score = 0
48
+ name_parts = search_name.split()
49
+ for part in name_parts:
50
+ if part in item_name:
51
+ score += 1
52
+
53
+ if variant and search_variant in item_name:
54
+ score += 2
55
+
56
+ if score > best_score:
57
+ best_score = score
58
+ best_match = item
59
+
60
+ if best_match and best_score > 0:
61
+ # Extract price
62
+ price_elem = best_match.find('priceTaxWithCur')
63
+ price = price_elem.text if price_elem is not None and price_elem.text else None
64
+
65
+ # Round price for WhatsApp display
66
+ if price:
67
+ try:
68
+ price_float = float(price)
69
+ if price_float > 200000:
70
+ rounded = round(price_float / 5000) * 5000
71
+ price = f"{int(rounded):,}".replace(',', '.') + " TL"
72
+ elif price_float > 30000:
73
+ rounded = round(price_float / 1000) * 1000
74
+ price = f"{int(rounded):,}".replace(',', '.') + " TL"
75
+ elif price_float > 10000:
76
+ rounded = round(price_float / 100) * 100
77
+ price = f"{int(rounded):,}".replace(',', '.') + " TL"
78
+ else:
79
+ rounded = round(price_float / 10) * 10
80
+ price = f"{int(rounded):,}".replace(',', '.') + " TL"
81
+ except:
82
+ price = None
83
+
84
+ # Extract link
85
+ link_elem = best_match.find('productLink')
86
+ link = link_elem.text if link_elem is not None and link_elem.text else None
87
+
88
+ return price, link
89
+
90
+ return None, None
91
+
92
+ except Exception as e:
93
+ print(f"Error getting price/link: {e}")
94
+ return None, None
95
+
96
+ def get_warehouse_stock_gpt5(user_message):
97
+ """GPT-5 powered smart warehouse stock search for WhatsApp"""
98
+
99
+ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
100
+ if not OPENAI_API_KEY:
101
+ return None
102
+
103
+ # Check for specific warehouse in query
104
+ warehouse_keywords = {
105
+ 'caddebostan': 'Caddebostan',
106
+ 'ortaköy': 'Ortaköy',
107
+ 'ortakoy': 'Ortaköy',
108
+ 'alsancak': 'Alsancak',
109
+ 'izmir': 'Alsancak',
110
+ 'bahçeköy': 'Bahçeköy',
111
+ 'bahcekoy': 'Bahçeköy'
112
+ }
113
+
114
+ user_lower = user_message.lower()
115
+ asked_warehouse = None
116
+ for keyword, warehouse in warehouse_keywords.items():
117
+ if keyword in user_lower:
118
+ asked_warehouse = warehouse
119
+ break
120
+
121
+ # Get warehouse XML with retry
122
+ xml_text = None
123
+ for attempt in range(3):
124
+ try:
125
+ url = 'https://video.trek-turkey.com/bizimhesap-warehouse-xml-b2b-api-v2.php'
126
+ timeout_val = 10 + (attempt * 5)
127
+ response = requests.get(url, verify=False, timeout=timeout_val)
128
+ xml_text = response.text
129
+ break
130
+ except:
131
+ if attempt == 2:
132
+ return None
133
+
134
+ if not xml_text:
135
+ return None
136
+
137
+ # Extract product blocks
138
+ product_pattern = r'<Product>(.*?)</Product>'
139
+ all_products = re.findall(product_pattern, xml_text, re.DOTALL)
140
+
141
+ # Create product summary for GPT
142
+ products_summary = []
143
+ for i, product_block in enumerate(all_products):
144
+ name_match = re.search(r'<ProductName><!\[CDATA\[(.*?)\]\]></ProductName>', product_block)
145
+ variant_match = re.search(r'<ProductVariant><!\[CDATA\[(.*?)\]\]></ProductVariant>', product_block)
146
+
147
+ if name_match:
148
+ warehouses_with_stock = []
149
+ warehouse_regex = r'<Warehouse>.*?<Name><!\[CDATA\[(.*?)\]\]></Name>.*?<Stock>(.*?)</Stock>.*?</Warehouse>'
150
+ warehouses = re.findall(warehouse_regex, product_block, re.DOTALL)
151
+
152
+ for wh_name, wh_stock in warehouses:
153
+ try:
154
+ if int(wh_stock.strip()) > 0:
155
+ warehouses_with_stock.append(wh_name)
156
+ except:
157
+ pass
158
+
159
+ if warehouses_with_stock: # Only add if has stock
160
+ product_info = {
161
+ "index": i,
162
+ "name": name_match.group(1),
163
+ "variant": variant_match.group(1) if variant_match else "",
164
+ "warehouses": warehouses_with_stock
165
+ }
166
+ products_summary.append(product_info)
167
+
168
+ # Prepare GPT-5 prompt
169
+ warehouse_filter = ""
170
+ if asked_warehouse:
171
+ warehouse_filter = f"\nIMPORTANT: User is asking about {asked_warehouse} warehouse. Only return products available there."
172
+
173
+ smart_prompt = f"""User WhatsApp message: "{user_message}"
174
+
175
+ Find EXACT products matching this query. Be very precise with product names.
176
+ IMPORTANT: If user asks for "madone sl 6", find products with "MADONE SL 6" in the name.
177
+ DO NOT return similar products (like Marlin) if the exact product is not found.
178
+
179
+ Turkish/English terms:
180
+ - FORMA = jersey, TAYT = tights, İÇLİK = base layer, YAĞMURLUK = raincoat
181
+ - GOBIK = Spanish textile brand
182
+ - Sizes: S, M, L, XL, XXL, SMALL, MEDIUM, LARGE
183
+ {warehouse_filter}
184
+
185
+ Products with stock:
186
+ {json.dumps(products_summary, ensure_ascii=False)}
187
+
188
+ Return ONLY index numbers of EXACT matches as comma-separated list.
189
+ If NO exact match found, return: -1
190
+ Examples:
191
+ - "madone sl 6" -> Find only products with "MADONE SL 6" in name, NOT Marlin or other models
192
+ - "gobik forma" -> Find only products with "GOBIK" AND "FORMA" in name"""
193
+
194
+ headers = {
195
+ "Content-Type": "application/json",
196
+ "Authorization": f"Bearer {OPENAI_API_KEY}"
197
+ }
198
+
199
+ payload = {
200
+ "model": "gpt-5-chat-latest",
201
+ "messages": [
202
+ {"role": "system", "content": "You are a product matcher. Return only numbers."},
203
+ {"role": "user", "content": smart_prompt}
204
+ ],
205
+ "temperature": 0,
206
+ "max_tokens": 100
207
+ }
208
+
209
+ try:
210
+ response = requests.post(
211
+ "https://api.openai.com/v1/chat/completions",
212
+ headers=headers,
213
+ json=payload,
214
+ timeout=10
215
+ )
216
+
217
+ if response.status_code != 200:
218
+ return None
219
+
220
+ result = response.json()
221
+ indices_str = result['choices'][0]['message']['content'].strip()
222
+
223
+ if not indices_str or indices_str == "-1":
224
+ return None
225
+
226
+ # Parse indices safely
227
+ indices = []
228
+ for idx in indices_str.split(','):
229
+ idx = idx.strip()
230
+ if idx and idx.isdigit():
231
+ indices.append(int(idx))
232
+
233
+ if not indices:
234
+ return None
235
+
236
+ # Collect matched products
237
+ matched_products = []
238
+ for idx in indices: # No limit, get all matches
239
+ if 0 <= idx < len(all_products):
240
+ product_block = all_products[idx]
241
+
242
+ name_match = re.search(r'<ProductName><!\[CDATA\[(.*?)\]\]></ProductName>', product_block)
243
+ variant_match = re.search(r'<ProductVariant><!\[CDATA\[(.*?)\]\]></ProductVariant>', product_block)
244
+
245
+ if name_match:
246
+ product_name = name_match.group(1)
247
+ variant = variant_match.group(1) if variant_match else ""
248
+
249
+ # Get price and link
250
+ price, link = get_product_price_and_link(product_name, variant)
251
+
252
+ # Get warehouse info
253
+ warehouses = []
254
+ warehouse_regex = r'<Warehouse>.*?<Name><!\[CDATA\[(.*?)\]\]></Name>.*?<Stock>(.*?)</Stock>.*?</Warehouse>'
255
+ wh_matches = re.findall(warehouse_regex, product_block, re.DOTALL)
256
+
257
+ for wh_name, wh_stock in wh_matches:
258
+ try:
259
+ if int(wh_stock.strip()) > 0:
260
+ if "CADDEBOSTAN" in wh_name:
261
+ warehouses.append("Caddebostan")
262
+ elif "ORTAKÖY" in wh_name:
263
+ warehouses.append("Ortaköy")
264
+ elif "ALSANCAK" in wh_name:
265
+ warehouses.append("Alsancak")
266
+ elif "BAHCEKOY" in wh_name or "BAHÇEKÖY" in wh_name:
267
+ warehouses.append("Bahçeköy")
268
+ except:
269
+ pass
270
+
271
+ if warehouses:
272
+ matched_products.append({
273
+ 'name': product_name,
274
+ 'variant': variant,
275
+ 'price': price,
276
+ 'link': link,
277
+ 'warehouses': warehouses
278
+ })
279
+
280
+ return matched_products if matched_products else None
281
+
282
+ except Exception as e:
283
+ print(f"GPT-5 search error: {e}")
284
+ return None
smart_warehouse_with_price.py ADDED
@@ -0,0 +1,769 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Smart warehouse stock finder with price and link information"""
2
+
3
+ import requests
4
+ import re
5
+ import os
6
+ import json
7
+ import xml.etree.ElementTree as ET
8
+ import time
9
+
10
+ # Cache configuration - 2 hours (reduced from 12 hours for more accurate results)
11
+ CACHE_DURATION = 7200 # 2 hours
12
+ cache = {
13
+ 'warehouse_xml': {'data': None, 'time': 0},
14
+ 'trek_xml': {'data': None, 'time': 0},
15
+ 'products_summary': {'data': None, 'time': 0},
16
+ 'search_results': {} # Cache for specific searches
17
+ }
18
+
19
+ def get_cached_trek_xml():
20
+ """Get Trek XML with 12-hour caching"""
21
+ current_time = time.time()
22
+
23
+ if cache['trek_xml']['data'] and (current_time - cache['trek_xml']['time'] < CACHE_DURATION):
24
+ cache_age = (current_time - cache['trek_xml']['time']) / 60 # in minutes
25
+ return cache['trek_xml']['data']
26
+
27
+ try:
28
+ url = 'https://www.trekbisiklet.com.tr/output/8582384479'
29
+ response = requests.get(url, verify=False, timeout=10)
30
+
31
+ if response.status_code == 200:
32
+ cache['trek_xml']['data'] = response.content
33
+ cache['trek_xml']['time'] = current_time
34
+ return response.content
35
+ else:
36
+ return None
37
+ except Exception as e:
38
+ return None
39
+
40
+ def apply_price_rounding(price_str):
41
+ """Apply the same price rounding formula used in app.py"""
42
+ if not price_str:
43
+ return price_str
44
+
45
+ try:
46
+ price_float = float(price_str)
47
+ if price_float > 200000:
48
+ return str(round(price_float / 5000) * 5000)
49
+ elif price_float > 30000:
50
+ return str(round(price_float / 1000) * 1000)
51
+ elif price_float > 10000:
52
+ return str(round(price_float / 100) * 100)
53
+ else:
54
+ return str(round(price_float / 10) * 10)
55
+ except:
56
+ return price_str
57
+
58
+ def get_product_price_and_link_by_sku(product_code):
59
+ """Get price and link from Trek XML using improved SKU matching with new XML fields
60
+ Uses: stockCode, rootProductStockCode, isOptionOfAProduct, isOptionedProduct
61
+ Level 1: Search variants by stockCode where isOptionOfAProduct=1
62
+ Level 2: Search main products by stockCode where isOptionOfAProduct=0
63
+ Level 3: Search by rootProductStockCode for variant-to-main mapping
64
+ """
65
+ try:
66
+ # Import XML parsing for cleaner approach
67
+ import xml.etree.ElementTree as ET
68
+
69
+ # Get cached Trek XML
70
+ xml_content = get_cached_trek_xml()
71
+ if not xml_content:
72
+ return None, None
73
+
74
+ # Convert bytes to string if needed
75
+ if isinstance(xml_content, bytes):
76
+ xml_content = xml_content.decode('utf-8')
77
+
78
+ # Parse XML properly instead of regex
79
+ try:
80
+ root = ET.fromstring(xml_content)
81
+ except:
82
+ # Fallback to regex if XML parsing fails
83
+ return get_product_price_and_link_by_sku_regex(product_code)
84
+
85
+ # Level 1: Search variants first (isOptionOfAProduct=1)
86
+ for item in root.findall('.//item'):
87
+ is_option_element = item.find('isOptionOfAProduct')
88
+ stock_code_element = item.find('stockCode')
89
+
90
+ if (is_option_element is not None and is_option_element.text == '1' and
91
+ stock_code_element is not None and stock_code_element.text and stock_code_element.text.strip() == product_code):
92
+
93
+ price_element = item.find('priceTaxWithCur')
94
+ link_element = item.find('productLink')
95
+
96
+ if price_element is not None and link_element is not None:
97
+ rounded_price = apply_price_rounding(price_element.text)
98
+ return rounded_price, link_element.text
99
+
100
+ # Level 2: Search main products (isOptionOfAProduct=0)
101
+ for item in root.findall('.//item'):
102
+ is_option_element = item.find('isOptionOfAProduct')
103
+ stock_code_element = item.find('stockCode')
104
+
105
+ if (is_option_element is not None and is_option_element.text == '0' and
106
+ stock_code_element is not None and stock_code_element.text and stock_code_element.text.strip() == product_code):
107
+
108
+ price_element = item.find('priceTaxWithCur')
109
+ link_element = item.find('productLink')
110
+
111
+ if price_element is not None and link_element is not None:
112
+ rounded_price = apply_price_rounding(price_element.text)
113
+ return rounded_price, link_element.text
114
+
115
+ # Level 3: Search by rootProductStockCode (variant parent lookup)
116
+ for item in root.findall('.//item'):
117
+ root_stock_element = item.find('rootProductStockCode')
118
+
119
+ if (root_stock_element is not None and root_stock_element.text and root_stock_element.text.strip() == product_code):
120
+ price_element = item.find('priceTaxWithCur')
121
+ link_element = item.find('productLink')
122
+
123
+ if price_element is not None and link_element is not None:
124
+ rounded_price = apply_price_rounding(price_element.text)
125
+ return rounded_price, link_element.text
126
+
127
+ # Not found
128
+ return None, None
129
+
130
+ except Exception as e:
131
+ return None, None
132
+
133
+ def get_product_price_and_link_by_sku_regex(product_code):
134
+ """Fallback regex method for SKU lookup if XML parsing fails"""
135
+ try:
136
+ xml_content = get_cached_trek_xml()
137
+ if isinstance(xml_content, bytes):
138
+ xml_content = xml_content.decode('utf-8')
139
+
140
+ # Level 1: Search in variants first (isOptionOfAProduct=1)
141
+ variant_pattern = rf'<isOptionOfAProduct>1</isOptionOfAProduct>.*?<stockCode><!\[CDATA\[{re.escape(product_code)}\]\]></stockCode>.*?(?=<item>|$)'
142
+ variant_match = re.search(variant_pattern, xml_content, re.DOTALL)
143
+
144
+ if variant_match:
145
+ section = variant_match.group(0)
146
+ price_match = re.search(r'<price><!\[CDATA\[(.*?)\]\]></price>', section)
147
+ link_match = re.search(r'<producturl><!\[CDATA\[(.*?)\]\]></producturl>', section)
148
+
149
+ if price_match and link_match:
150
+ rounded_price = apply_price_rounding(price_match.group(1))
151
+ return rounded_price, link_match.group(1)
152
+
153
+ # Level 2: Search in main products (isOptionOfAProduct=0)
154
+ main_pattern = rf'<isOptionOfAProduct>0</isOptionOfAProduct>.*?<stockCode><!\[CDATA\[{re.escape(product_code)}\]\]></stockCode>.*?(?=<item>|$)'
155
+ main_match = re.search(main_pattern, xml_content, re.DOTALL)
156
+
157
+ if main_match:
158
+ section = main_match.group(0)
159
+ price_match = re.search(r'<price><!\[CDATA\[(.*?)\]\]></price>', section)
160
+ link_match = re.search(r'<producturl><!\[CDATA\[(.*?)\]\]></producturl>', section)
161
+
162
+ if price_match and link_match:
163
+ rounded_price = apply_price_rounding(price_match.group(1))
164
+ return rounded_price, link_match.group(1)
165
+
166
+ return None, None
167
+
168
+ except Exception as e:
169
+ return None, None
170
+
171
+ def get_product_price_and_link(product_name, variant=None):
172
+ """Get price and link from Trek website XML"""
173
+ try:
174
+ # Get cached Trek XML
175
+ xml_content = get_cached_trek_xml()
176
+ if not xml_content:
177
+ return None, None
178
+
179
+ root = ET.fromstring(xml_content)
180
+
181
+ # Turkish character normalization FIRST (before lower())
182
+ tr_map = {
183
+ 'İ': 'i', 'I': 'i', 'ı': 'i', # All I variations to i
184
+ 'Ğ': 'g', 'ğ': 'g',
185
+ 'Ü': 'u', 'ü': 'u',
186
+ 'Ş': 's', 'ş': 's',
187
+ 'Ö': 'o', 'ö': 'o',
188
+ 'Ç': 'c', 'ç': 'c'
189
+ }
190
+
191
+ # Apply normalization to original (before lower)
192
+ search_name_normalized = product_name
193
+ search_variant_normalized = variant if variant else ""
194
+ for tr, en in tr_map.items():
195
+ search_name_normalized = search_name_normalized.replace(tr, en)
196
+ search_variant_normalized = search_variant_normalized.replace(tr, en)
197
+
198
+ # Now lowercase
199
+ search_name = search_name_normalized.lower()
200
+ search_variant = search_variant_normalized.lower()
201
+
202
+ best_match = None
203
+ best_score = 0
204
+
205
+ # Clean search name - remove year and parentheses
206
+ clean_search = re.sub(r'\s*\(\d{4}\)\s*', '', search_name).strip()
207
+
208
+ for item in root.findall('item'):
209
+ # Get product name
210
+ rootlabel_elem = item.find('rootlabel')
211
+ if rootlabel_elem is None or not rootlabel_elem.text:
212
+ continue
213
+
214
+ item_name = rootlabel_elem.text.lower()
215
+ for tr, en in tr_map.items():
216
+ item_name = item_name.replace(tr, en)
217
+
218
+ # Clean item name too
219
+ clean_item = re.sub(r'\s*\(\d{4}\)\s*', '', item_name).strip()
220
+
221
+ # Calculate match score with priority for exact matches
222
+ score = 0
223
+
224
+ # Exact match gets highest priority
225
+ if clean_search == clean_item:
226
+ score += 100
227
+ # Check if starts with exact product name (e.g., "fx 2" in "fx 2 kirmizi")
228
+ elif clean_item.startswith(clean_search + " ") or clean_item == clean_search:
229
+ score += 50
230
+ else:
231
+ # Partial matching
232
+ name_parts = clean_search.split()
233
+ for part in name_parts:
234
+ if part in clean_item:
235
+ score += 1
236
+
237
+ # Check variant if specified
238
+ if variant and search_variant in item_name:
239
+ score += 2 # Variant match is important
240
+
241
+ if score > best_score:
242
+ best_score = score
243
+ best_match = item
244
+
245
+ if best_match and best_score > 0:
246
+ # Extract price
247
+ price_elem = best_match.find('priceTaxWithCur')
248
+ price = price_elem.text if price_elem is not None and price_elem.text else None
249
+
250
+ # Round price
251
+ if price:
252
+ try:
253
+ price_float = float(price)
254
+ if price_float > 200000:
255
+ rounded = round(price_float / 5000) * 5000
256
+ price = f"{int(rounded):,}".replace(',', '.') + " TL"
257
+ elif price_float > 30000:
258
+ rounded = round(price_float / 1000) * 1000
259
+ price = f"{int(rounded):,}".replace(',', '.') + " TL"
260
+ elif price_float > 10000:
261
+ rounded = round(price_float / 100) * 100
262
+ price = f"{int(rounded):,}".replace(',', '.') + " TL"
263
+ else:
264
+ rounded = round(price_float / 10) * 10
265
+ price = f"{int(rounded):,}".replace(',', '.') + " TL"
266
+ except:
267
+ price = f"{price} TL"
268
+
269
+ # Extract link (field name is productLink, not productUrl!)
270
+ link_elem = best_match.find('productLink')
271
+ link = link_elem.text if link_elem is not None and link_elem.text else None
272
+
273
+ return price, link
274
+
275
+ return None, None
276
+
277
+ except Exception as e:
278
+ return None, None
279
+
280
+ def get_cached_warehouse_xml():
281
+ """Get warehouse XML with 12-hour caching"""
282
+ current_time = time.time()
283
+
284
+ if cache['warehouse_xml']['data'] and (current_time - cache['warehouse_xml']['time'] < CACHE_DURATION):
285
+ cache_age = (current_time - cache['warehouse_xml']['time']) / 60 # in minutes
286
+ return cache['warehouse_xml']['data']
287
+
288
+ for attempt in range(3):
289
+ try:
290
+ url = 'https://video.trek-turkey.com/bizimhesap-warehouse-xml-b2b-api-v2.php'
291
+ timeout_val = 10 + (attempt * 5)
292
+ response = requests.get(url, verify=False, timeout=timeout_val)
293
+ xml_text = response.text
294
+ cache['warehouse_xml']['data'] = xml_text
295
+ cache['warehouse_xml']['time'] = current_time
296
+
297
+ return xml_text
298
+ except requests.exceptions.Timeout:
299
+ if attempt == 2:
300
+ return None
301
+ except Exception as e:
302
+ return None
303
+
304
+ return None
305
+
306
+ def get_warehouse_stock_smart_with_price(user_message, previous_result=None):
307
+ """Enhanced smart warehouse search with price and link info"""
308
+
309
+ # Canlı destek / müşteri temsilcisi talepleri - ÜRÜN ARAMASI YAPMA
310
+ live_support_phrases = [
311
+ 'müşteri bağla', 'canlı bağla', 'temsilci', 'yetkili', 'gerçek kişi',
312
+ 'insan ile', 'operatör', 'canlı destek', 'bağlayın', 'bağlar mısın',
313
+ 'görüşmek istiyorum', 'konuşmak istiyorum', 'yetkiliye bağla',
314
+ 'müşteri hizmetleri', 'çağrı merkezi', 'santral', 'bağla'
315
+ ]
316
+
317
+ clean_message = user_message.lower().strip()
318
+
319
+ for phrase in live_support_phrases:
320
+ if phrase in clean_message:
321
+ return None # Ürün araması yapma, GPT'ye bırak
322
+
323
+ # Filter out common non-product words and responses
324
+ non_product_words = [
325
+ 'süper', 'harika', 'güzel', 'teşekkürler', 'teşekkür', 'tamam', 'olur',
326
+ 'evet', 'hayır', 'merhaba', 'selam', 'iyi', 'kötü', 'fena', 'muhteşem',
327
+ 'mükemmel', 'berbat', 'idare eder', 'olabilir', 'değil', 'var', 'yok',
328
+ 'anladım', 'anlaşıldı', 'peki', 'tamamdır', 'ok', 'okay', 'aynen',
329
+ 'kesinlikle', 'elbette', 'tabii', 'tabiki', 'doğru', 'yanlış'
330
+ ]
331
+
332
+ # Check if message is just a simple response
333
+ if clean_message in non_product_words:
334
+ return None
335
+
336
+ # Brand keywords that should ALWAYS trigger product search regardless of length
337
+ brand_keywords = ['gobik', 'trek', 'bontrager', 'kask', 'shimano', 'sram', 'garmin', 'wahoo']
338
+
339
+ # Check if message contains a brand keyword
340
+ contains_brand = any(brand in clean_message for brand in brand_keywords)
341
+
342
+ # Check if it's a single word that's likely not a product
343
+ # BUT allow if it contains a known brand
344
+ if not contains_brand and len(clean_message.split()) == 1 and len(clean_message) < 5:
345
+ # Short single words are usually not product names
346
+ return None
347
+
348
+ # Check if this is a question rather than a product search
349
+ # BUT skip this check if message contains a known brand
350
+ question_indicators = [
351
+ 'musun', 'müsün', 'misin', 'mısın', 'miyim', 'mıyım',
352
+ 'musunuz', 'müsünüz', 'misiniz', 'mısınız',
353
+ 'neden', 'nasıl', 'ne zaman', 'kim', 'nerede', 'nereye',
354
+ 'ulaşamıyor', 'yapamıyor', 'gönderemiyor', 'edemiyor',
355
+ '?'
356
+ ]
357
+
358
+ # If message contains question indicators, it's likely not a product search
359
+ # EXCEPTION: If message contains a brand keyword, still search for products
360
+ if not contains_brand:
361
+ for indicator in question_indicators:
362
+ if indicator in clean_message:
363
+ return None
364
+
365
+ # Normalize cache key for consistent caching (Turkish chars + lowercase)
366
+ def normalize_for_cache(text):
367
+ """Normalize text for cache key"""
368
+ tr_map = {'İ': 'i', 'I': 'i', 'ı': 'i', 'Ğ': 'g', 'ğ': 'g', 'Ü': 'u', 'ü': 'u',
369
+ 'Ş': 's', 'ş': 's', 'Ö': 'o', 'ö': 'o', 'Ç': 'c', 'ç': 'c'}
370
+ for tr, en in tr_map.items():
371
+ text = text.replace(tr, en)
372
+ return text.lower().strip()
373
+
374
+ # Check search cache first
375
+ cache_key = normalize_for_cache(user_message)
376
+ current_time = time.time()
377
+
378
+ if cache_key in cache['search_results']:
379
+ cached = cache['search_results'][cache_key]
380
+ if current_time - cached['time'] < CACHE_DURATION:
381
+ cache_age = (current_time - cached['time']) / 60 # in minutes
382
+ return cached['data']
383
+ else:
384
+ pass
385
+
386
+ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
387
+
388
+ # Check if user is asking about specific warehouse
389
+ warehouse_keywords = {
390
+ 'caddebostan': 'Caddebostan',
391
+ 'ortaköy': 'Ortaköy',
392
+ 'ortakoy': 'Ortaköy',
393
+ 'alsancak': 'Alsancak',
394
+ 'izmir': 'Alsancak',
395
+ 'bahçeköy': 'Bahçeköy',
396
+ 'bahcekoy': 'Bahçeköy',
397
+ 'sarıyer': 'Bahçeköy',
398
+ 'sariyer': 'Bahçeköy'
399
+ }
400
+
401
+ user_lower = user_message.lower()
402
+ asked_warehouse = None
403
+ for keyword, warehouse in warehouse_keywords.items():
404
+ if keyword in user_lower:
405
+ asked_warehouse = warehouse
406
+ break
407
+
408
+ # Get cached XML data
409
+ xml_text = get_cached_warehouse_xml()
410
+ if not xml_text:
411
+ return None
412
+
413
+ # Extract product blocks
414
+ product_pattern = r'<Product>(.*?)</Product>'
415
+ all_products = re.findall(product_pattern, xml_text, re.DOTALL)
416
+
417
+ # Create simplified product list for GPT
418
+ products_summary = []
419
+ for i, product_block in enumerate(all_products):
420
+ name_match = re.search(r'<ProductName><!\[CDATA\[(.*?)\]\]></ProductName>', product_block)
421
+ variant_match = re.search(r'<ProductVariant><!\[CDATA\[(.*?)\]\]></ProductVariant>', product_block)
422
+
423
+ if name_match:
424
+ warehouses_with_stock = []
425
+ warehouse_regex = r'<Warehouse>.*?<Name><!\[CDATA\[(.*?)\]\]></Name>.*?<Stock>(.*?)</Stock>.*?</Warehouse>'
426
+ warehouses = re.findall(warehouse_regex, product_block, re.DOTALL)
427
+
428
+ for wh_name, wh_stock in warehouses:
429
+ try:
430
+ if int(wh_stock.strip()) > 0:
431
+ warehouses_with_stock.append(wh_name)
432
+ except:
433
+ pass
434
+
435
+ product_info = {
436
+ "index": i,
437
+ "name": name_match.group(1),
438
+ "variant": variant_match.group(1) if variant_match else "",
439
+ "warehouses": warehouses_with_stock
440
+ }
441
+ products_summary.append(product_info)
442
+
443
+ # Prepare warehouse filter if needed
444
+ warehouse_filter = ""
445
+ if asked_warehouse:
446
+ warehouse_filter = f"\nIMPORTANT: User is asking specifically about {asked_warehouse} warehouse. Only return products available in that warehouse."
447
+
448
+ # Debug logging
449
+ # Check if the target product exists
450
+ # Normalize Turkish characters for comparison
451
+ def normalize_turkish(text):
452
+ text = text.upper()
453
+ replacements = {'I': 'İ', 'Ç': 'C', 'Ş': 'S', 'Ğ': 'G', 'Ü': 'U', 'Ö': 'O'}
454
+ # Also try with İ -> I conversion
455
+ text2 = text.replace('İ', 'I')
456
+ return text, text2
457
+
458
+ search_term = user_message.upper()
459
+ search_norm1, search_norm2 = normalize_turkish(search_term)
460
+
461
+ matching_products = []
462
+ for p in products_summary:
463
+ p_name = p['name'].upper()
464
+ # Check both original and normalized versions
465
+ if (search_term in p_name or
466
+ search_norm1 in p_name or
467
+ search_norm2 in p_name or
468
+ search_term.replace('I', 'İ') in p_name):
469
+ matching_products.append(p)
470
+
471
+ if matching_products:
472
+ pass
473
+ else:
474
+ pass
475
+
476
+ # GPT-5 prompt with enhanced instructions
477
+ smart_prompt = f"""User is asking: "{user_message}"
478
+
479
+ FIRST CHECK: Is this actually a product search?
480
+ - If the message is a question about the system, service, or a general inquiry, return: -1
481
+ - If the message contains "musun", "misin", "neden", "nasıl", etc. it's likely NOT a product search
482
+ - Only proceed if this looks like a genuine product name or model
483
+
484
+ Find ALL products that match this query from the list below.
485
+ If user asks about specific size (S, M, L, XL, XXL, SMALL, MEDIUM, LARGE, X-LARGE), return only that size.
486
+ If user asks generally (without size), return ALL variants of the product.
487
+ {warehouse_filter}
488
+
489
+ CRITICAL TURKISH CHARACTER RULES:
490
+ - "MARLIN" and "MARLİN" are the SAME product (Turkish İ vs I)
491
+ - Treat these as equivalent: I/İ/ı, Ö/ö, Ü/ü, Ş/ş, Ğ/ğ, Ç/ç
492
+ - If user writes "Marlin", also match "MARLİN" in the list
493
+
494
+ IMPORTANT BRAND AND PRODUCT TYPE RULES:
495
+ - GOBIK: Spanish textile brand we import. When user asks about "gobik", return ALL products with "GOBIK" in the name.
496
+ - Product names contain type information: FORMA (jersey/cycling shirt), TAYT (tights), İÇLİK (base layer), YAĞMURLUK (raincoat), etc.
497
+ - Understand Turkish/English terms:
498
+ * "erkek forma" / "men's jersey" -> Find products with FORMA in name
499
+ * "tayt" / "tights" -> Find products with TAYT in name
500
+ * "içlik" / "base layer" -> Find products with İÇLİK in name
501
+ * "yağmurluk" / "raincoat" -> Find products with YAĞMURLUK in name
502
+ - Gender: UNISEX means for both men and women. If no gender specified, it's typically men's.
503
+
504
+ Products list (with warehouse availability):
505
+ {json.dumps(products_summary, ensure_ascii=False, indent=2)}
506
+
507
+ Return ONLY index numbers of ALL matching products as comma-separated list (e.g., "5,8,12,15").
508
+ If no products found, return ONLY: -1
509
+ DO NOT return empty string or any explanation, ONLY numbers or -1
510
+
511
+ Examples of correct responses:
512
+ - "2,5,8,12,15,20" (multiple products found)
513
+ - "45" (single product found)
514
+ - "-1" (no products found)"""
515
+
516
+ # Check if we have API key before making the request
517
+ if not OPENAI_API_KEY:
518
+ # Try to find in Trek XML directly as fallback, but avoid tool products
519
+ user_message_normalized = user_message.upper()
520
+ tool_indicators = ['SUPER B', 'ANAHTAR', 'TAKIMI', 'PENSE', 'TOOL', 'ADAPTÖR', 'CONVERTER']
521
+ should_skip_trek_lookup = any(indicator in user_message_normalized for indicator in tool_indicators)
522
+
523
+ price, link = None, None
524
+ if not should_skip_trek_lookup:
525
+ price, link = get_product_price_and_link(user_message)
526
+
527
+ if price and link:
528
+ return [
529
+ f"🚲 **{user_message.title()}**",
530
+ f"💰 Fiyat: {price}",
531
+ f"🔗 Link: {link}",
532
+ "",
533
+ "⚠️ **Stok durumu kontrol edilemiyor**",
534
+ "📞 Güncel stok için mağazalarımızı arayın:",
535
+ "• Caddebostan: 0543 934 0438",
536
+ "• Alsancak: 0543 936 2335"
537
+ ]
538
+ return None
539
+
540
+ headers = {
541
+ "Content-Type": "application/json",
542
+ "Authorization": f"Bearer {OPENAI_API_KEY}"
543
+ }
544
+
545
+ # GPT-5.2 modeli temperature ve max_tokens desteklemiyor
546
+ payload = {
547
+ "model": "gpt-5.2-chat-latest",
548
+ "messages": [
549
+ {"role": "system", "content": "You are a product matcher. Find ALL matching products. Return only index numbers."},
550
+ {"role": "user", "content": smart_prompt}
551
+ ]
552
+ }
553
+
554
+ try:
555
+ response = requests.post(
556
+ "https://api.openai.com/v1/chat/completions",
557
+ headers=headers,
558
+ json=payload,
559
+ timeout=10
560
+ )
561
+
562
+ if response.status_code == 200:
563
+ result = response.json()
564
+ indices_str = result['choices'][0]['message']['content'].strip()
565
+
566
+ # Handle empty response - try Trek XML as fallback, but avoid tool products
567
+ if not indices_str or indices_str == "-1":
568
+ # Try to find in Trek XML directly, but skip tools
569
+ user_message_normalized = user_message.upper()
570
+ tool_indicators = ['SUPER B', 'ANAHTAR', 'TAKIMI', 'PENSE', 'TOOL', 'ADAPTÖR', 'CONVERTER']
571
+ should_skip_trek_lookup = any(indicator in user_message_normalized for indicator in tool_indicators)
572
+
573
+ price, link = None, None
574
+ if not should_skip_trek_lookup:
575
+ price, link = get_product_price_and_link(user_message)
576
+
577
+ if price and link:
578
+ # Found in Trek XML but not in warehouse stock!
579
+ return [
580
+ f"🚲 **{user_message.title()}**",
581
+ f"💰 Fiyat: {price}",
582
+ f"🔗 Link: {link}",
583
+ "",
584
+ "❌ **Stok Durumu: TÜKENDİ**",
585
+ "",
586
+ "📞 Stok güncellemesi veya ön sipariş için mağazalarımızı arayabilirsiniz:",
587
+ "• Caddebostan: 0543 934 0438",
588
+ "• Alsancak: 0543 936 2335"
589
+ ]
590
+ return None
591
+
592
+ try:
593
+ # Filter out empty strings and parse indices
594
+ indices = []
595
+ for idx in indices_str.split(','):
596
+ idx = idx.strip()
597
+ if idx and idx.isdigit():
598
+ indices.append(int(idx))
599
+
600
+ # Collect all matching products with price/link
601
+ all_variants = []
602
+ warehouse_stock = {}
603
+
604
+ for idx in indices:
605
+ if 0 <= idx < len(all_products):
606
+ product_block = all_products[idx]
607
+
608
+ # Get product details
609
+ name_match = re.search(r'<ProductName><!\[CDATA\[(.*?)\]\]></ProductName>', product_block)
610
+ variant_match = re.search(r'<ProductVariant><!\[CDATA\[(.*?)\]\]></ProductVariant>', product_block)
611
+ productcode_match = re.search(r'<ProductCode><!\[CDATA\[(.*?)\]\]></ProductCode>', product_block)
612
+
613
+ if name_match:
614
+ product_name = name_match.group(1)
615
+ variant = variant_match.group(1) if variant_match else ""
616
+
617
+ # Get price and link from Trek website - TRY SKU FIRST (NEW METHOD)
618
+ price, link = None, None
619
+
620
+ # Try SKU-based lookup first if ProductCode exists
621
+ product_code = productcode_match.group(1) if productcode_match else None
622
+ if product_code and product_code.strip():
623
+ price, link = get_product_price_and_link_by_sku(product_code.strip())
624
+
625
+ # Fallback to name-based if SKU didn't work, but be more careful about matching
626
+ if not price or not link:
627
+ # Only do name-based fallback if the product might reasonably be sold by Trek
628
+ # Avoid tools/accessories that clearly don't belong to Trek's bicycle catalog
629
+ product_name_normalized = product_name.upper()
630
+
631
+ # Skip name-based fallback for obvious tools/non-bike products
632
+ tool_indicators = ['SUPER B', 'ANAHTAR', 'TAKIMI', 'PENSE', 'TOOL', 'ADAPTÖR', 'CONVERTER']
633
+
634
+ should_skip_fallback = any(indicator in product_name_normalized for indicator in tool_indicators)
635
+
636
+ if not should_skip_fallback:
637
+ price, link = get_product_price_and_link(product_name, variant)
638
+
639
+ variant_info = {
640
+ 'name': product_name,
641
+ 'variant': variant,
642
+ 'price': price,
643
+ 'link': link,
644
+ 'warehouses': []
645
+ }
646
+
647
+ # Get warehouse stock
648
+ warehouse_regex = r'<Warehouse>.*?<Name><!\[CDATA\[(.*?)\]\]></Name>.*?<Stock>(.*?)</Stock>.*?</Warehouse>'
649
+ warehouses = re.findall(warehouse_regex, product_block, re.DOTALL)
650
+
651
+ for wh_name, wh_stock in warehouses:
652
+ try:
653
+ stock = int(wh_stock.strip())
654
+ if stock > 0:
655
+ display_name = format_warehouse_name(wh_name)
656
+ variant_info['warehouses'].append({
657
+ 'name': display_name,
658
+ 'stock': stock
659
+ })
660
+
661
+ if display_name not in warehouse_stock:
662
+ warehouse_stock[display_name] = 0
663
+ warehouse_stock[display_name] += stock
664
+ except:
665
+ pass
666
+
667
+ if variant_info['warehouses']:
668
+ all_variants.append(variant_info)
669
+
670
+ # Format result
671
+ result = []
672
+
673
+ if asked_warehouse:
674
+ # Filter for specific warehouse
675
+ warehouse_variants = []
676
+ for variant in all_variants:
677
+ for wh in variant['warehouses']:
678
+ if asked_warehouse in wh['name']:
679
+ warehouse_variants.append(variant)
680
+ break
681
+
682
+ if warehouse_variants:
683
+ result.append(f"{format_warehouse_name(asked_warehouse)} mağazasında mevcut:")
684
+ for v in warehouse_variants:
685
+ variant_text = f" ({v['variant']})" if v['variant'] else ""
686
+ result.append(f"• {v['name']}{variant_text}")
687
+ if v['price']:
688
+ result.append(f" Fiyat: {v['price']}")
689
+ if v['link']:
690
+ result.append(f" Link: {v['link']}")
691
+ else:
692
+ result.append(f"{format_warehouse_name(asked_warehouse)} mağazasında bu ürün mevcut değil")
693
+ else:
694
+ # Show all variants
695
+ if all_variants:
696
+ # Group by product name for cleaner display
697
+ product_groups = {}
698
+ for variant in all_variants:
699
+ if variant['name'] not in product_groups:
700
+ product_groups[variant['name']] = []
701
+ product_groups[variant['name']].append(variant)
702
+
703
+ result.append(f"Bulunan ürünler:")
704
+
705
+ for product_name, variants in product_groups.items():
706
+ result.append(f"\n{product_name}:")
707
+
708
+ # Show first variant's price and link (usually same for all variants)
709
+ if variants[0]['price']:
710
+ result.append(f"Fiyat: {variants[0]['price']}")
711
+ if variants[0]['link']:
712
+ result.append(f"Link: {variants[0]['link']}")
713
+
714
+ # Show variants and their availability
715
+ for v in variants:
716
+ if v['variant']:
717
+ warehouses_str = ", ".join([w['name'].replace(' mağazası', '') for w in v['warehouses']])
718
+ result.append(f"• {v['variant']}: {warehouses_str}")
719
+
720
+ else:
721
+ # No warehouse stock found - check if product exists in Trek
722
+ # But be careful not to match tools/accessories with bikes
723
+ user_message_normalized = user_message.upper()
724
+ tool_indicators = ['SUPER B', 'ANAHTAR', 'TAKIMI', 'PENSE', 'TOOL', 'ADAPTÖR', 'CONVERTER']
725
+ should_skip_trek_lookup = any(indicator in user_message_normalized for indicator in tool_indicators)
726
+
727
+ price, link = None, None
728
+ if not should_skip_trek_lookup:
729
+ price, link = get_product_price_and_link(user_message)
730
+
731
+ if price and link:
732
+ result.append(f"❌ **Stok Durumu: TÜM MAĞAZALARDA TÜKENDİ**")
733
+ result.append("")
734
+ result.append(f"💰 Web Fiyatı: {price}")
735
+ result.append(f"🔗 Ürün Detayları: {link}")
736
+ result.append("")
737
+ result.append("📞 Stok güncellemesi veya ön sipariş için:")
738
+ result.append("• Caddebostan: 0543 934 0438")
739
+ result.append("• Alsancak: 0543 936 2335")
740
+ else:
741
+ return None
742
+
743
+ # Cache the result before returning
744
+ cache['search_results'][cache_key] = {
745
+ 'data': result,
746
+ 'time': current_time
747
+ }
748
+ return result
749
+
750
+ except (ValueError, IndexError) as e:
751
+ return None
752
+ else:
753
+ return None
754
+
755
+ except Exception as e:
756
+ return None
757
+
758
+ def format_warehouse_name(wh_name):
759
+ """Format warehouse name nicely"""
760
+ if "CADDEBOSTAN" in wh_name:
761
+ return "Caddebostan mağazası"
762
+ elif "ORTAKÖY" in wh_name:
763
+ return "Ortaköy mağazası"
764
+ elif "ALSANCAK" in wh_name:
765
+ return "İzmir Alsancak mağazası"
766
+ elif "BAHCEKOY" in wh_name or "BAHÇEKÖY" in wh_name:
767
+ return "Bahçeköy mağazası"
768
+ else:
769
+ return wh_name.replace("MAGAZA DEPO", "").strip()
store_notification.py ADDED
@@ -0,0 +1,397 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ WhatsApp Mağaza Bildirim Sistemi
6
+ Müşteri taleplerini mağazalara WhatsApp ile bildirir
7
+ """
8
+
9
+ import os
10
+ import logging
11
+ from datetime import datetime
12
+ from twilio.rest import Client
13
+ from typing import Optional, Dict
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ # Mağaza WhatsApp numaraları
18
+ STORE_NUMBERS = {
19
+ "caddebostan": "+905439340438", # Alsancak - Mehmet Bey
20
+ "sariyer": "+905421371080", # Alsancak - Mehmet Bey
21
+ "alsancak": "+905439362335", # İzmir Alsancak - Mehmet Bey
22
+ "merkez": "+905439362335" # Mehmet Bey - TÜM bildirimler buraya
23
+ }
24
+
25
+ # Twilio ayarları
26
+ TWILIO_ACCOUNT_SID = os.getenv("TWILIO_ACCOUNT_SID")
27
+ TWILIO_AUTH_TOKEN = os.getenv("TWILIO_AUTH_TOKEN")
28
+ TWILIO_MESSAGING_SERVICE_SID = os.getenv("TWILIO_MESSAGING_SERVICE_SID")
29
+
30
+ def send_store_notification(
31
+ customer_phone: str,
32
+ customer_name: Optional[str],
33
+ product_name: str,
34
+ action: str, # "reserve", "info", "price", "stock"
35
+ store_name: Optional[str] = None,
36
+ additional_info: Optional[str] = None
37
+ ) -> bool:
38
+ """
39
+ Mağazaya WhatsApp bildirimi gönder
40
+
41
+ Args:
42
+ customer_phone: Müşteri telefon numarası
43
+ customer_name: Müşteri adı (opsiyonel)
44
+ product_name: Ürün adı
45
+ action: İşlem tipi (ayırtma, bilgi, fiyat, stok)
46
+ store_name: Hangi mağazaya bildirim gidecek
47
+ additional_info: Ek bilgi
48
+
49
+ Returns:
50
+ Başarılı ise True
51
+ """
52
+
53
+ try:
54
+ # Mesaj içeriğini hazırla (her durumda log için gerekli)
55
+ message = format_notification_message(
56
+ customer_phone,
57
+ customer_name,
58
+ product_name,
59
+ action,
60
+ store_name,
61
+ additional_info
62
+ )
63
+
64
+ # FALLBACK: Bildirimi dosyaya kaydet
65
+ import json
66
+ from datetime import datetime
67
+ notification_log = {
68
+ "timestamp": datetime.now().isoformat(),
69
+ "customer_phone": customer_phone,
70
+ "customer_name": customer_name,
71
+ "product_name": product_name,
72
+ "action": action,
73
+ "store_name": store_name,
74
+ "additional_info": additional_info,
75
+ "formatted_message": message
76
+ }
77
+
78
+ # Log dosyasına ekle
79
+ log_file = "mehmet_bey_notifications.json"
80
+ try:
81
+ with open(log_file, "r") as f:
82
+ logs = json.load(f)
83
+ except:
84
+ logs = []
85
+
86
+ logs.append(notification_log)
87
+
88
+ # Son 100 bildirimi tut
89
+ if len(logs) > 100:
90
+ logs = logs[-100:]
91
+
92
+ with open(log_file, "w") as f:
93
+ json.dump(logs, f, indent=2, ensure_ascii=False)
94
+
95
+ logger.info(f"📝 Bildirim kaydedildi: {log_file}")
96
+ logger.info(f" Action: {action}, Product: {product_name}")
97
+
98
+ # Twilio client oluştur (varsa)
99
+ if not TWILIO_ACCOUNT_SID or not TWILIO_AUTH_TOKEN:
100
+ logger.warning("⚠️ Twilio credentials missing - notification saved to file only")
101
+ return True # Dosyaya kaydettik, başarılı say
102
+
103
+ client = Client(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN)
104
+
105
+ # Hedef mağaza numarasını belirle
106
+ if store_name and store_name.lower() in STORE_NUMBERS:
107
+ target_number = STORE_NUMBERS[store_name.lower()]
108
+ else:
109
+ target_number = STORE_NUMBERS["merkez"]
110
+
111
+ # Mesaj içeriğini hazırla
112
+ message = format_notification_message(
113
+ customer_phone,
114
+ customer_name,
115
+ product_name,
116
+ action,
117
+ store_name,
118
+ additional_info
119
+ )
120
+
121
+ # WhatsApp mesajı gönder
122
+ whatsapp_number = f"whatsapp:{target_number}"
123
+
124
+ # WhatsApp Business numaramız
125
+ from_number = "whatsapp:+905332047254" # Bizim WhatsApp Business numaramız
126
+
127
+ try:
128
+ # WhatsApp Business numaramızdan gönder
129
+ msg = client.messages.create(
130
+ from_=from_number, # whatsapp:+905332047254
131
+ body=message,
132
+ to=whatsapp_number,
133
+ # StatusCallback parametresini vermeyelim
134
+ )
135
+ except Exception as twilio_error:
136
+ # Eğer Twilio hatası varsa, log'a yaz ama yine de başarılı say
137
+ logger.error(f"Twilio API hatası: {twilio_error}")
138
+ if "21609" in str(twilio_error):
139
+ logger.info("Not: Hedef numara henüz Sandbox'a katılmamış olabilir")
140
+ return True # Log'a kaydettik, başarılı sayıyoruz
141
+
142
+ logger.info(f"✅ Mağaza bildirimi gönderildi: {msg.sid}")
143
+ logger.info(f" Hedef: {whatsapp_number}")
144
+ logger.info(f" İşlem: {action}")
145
+
146
+ return True
147
+
148
+ except Exception as e:
149
+ logger.error(f"❌ Mağaza bildirim hatası: {e}")
150
+ return False
151
+
152
+ def format_notification_message(
153
+ customer_phone: str,
154
+ customer_name: Optional[str],
155
+ product_name: str,
156
+ action: str,
157
+ store_name: Optional[str],
158
+ additional_info: Optional[str]
159
+ ) -> str:
160
+ """
161
+ Bildirim mesajını formatla
162
+ """
163
+
164
+ # Tarih ve saat
165
+ now = datetime.now()
166
+ date_str = now.strftime("%d.%m.%Y %H:%M")
167
+
168
+ # İşlem tiplerine göre emoji ve başlık
169
+ action_config = {
170
+ "reserve": {
171
+ "emoji": "🔔",
172
+ "title": "YENİ AYIRTMA TALEBİ"
173
+ },
174
+ "info": {
175
+ "emoji": "ℹ️",
176
+ "title": "BİLGİ TALEBİ"
177
+ },
178
+ "price": {
179
+ "emoji": "💰",
180
+ "title": "FİYAT SORUSU"
181
+ },
182
+ "stock": {
183
+ "emoji": "📦",
184
+ "title": "STOK SORUSU"
185
+ },
186
+ "test": {
187
+ "emoji": "🧪",
188
+ "title": "TEST DENEMESİ"
189
+ }
190
+ }
191
+
192
+ config = action_config.get(action, action_config["info"])
193
+
194
+ # Mesaj oluştur
195
+ message_parts = [
196
+ f"{config['emoji']} *{config['title']}*",
197
+ f"📅 {date_str}",
198
+ "",
199
+ f"👤 *Müşteri:*"
200
+ ]
201
+
202
+ if customer_name:
203
+ message_parts.append(f" İsim: {customer_name}")
204
+
205
+ # Telefon numarasını formatla
206
+ phone_display = customer_phone.replace("whatsapp:", "")
207
+ message_parts.append(f" Tel: {phone_display}")
208
+
209
+ message_parts.extend([
210
+ "",
211
+ f"🚲 *Ürün:* {product_name}"
212
+ ])
213
+
214
+ if store_name:
215
+ message_parts.append(f"🏪 *Mağaza:* {store_name.title()}")
216
+
217
+ if additional_info:
218
+ message_parts.extend([
219
+ "",
220
+ f"📝 *Not:*",
221
+ additional_info
222
+ ])
223
+
224
+ # Hızlı aksiyon önerileri
225
+ if action == "reserve":
226
+ message_parts.extend([
227
+ "",
228
+ "✅ *Yapılacaklar:*",
229
+ "1. Ürün stok kontrolü",
230
+ "2. Müşteriyi arayın",
231
+ "3. Ödeme/teslimat planı"
232
+ ])
233
+ elif action == "price":
234
+ message_parts.extend([
235
+ "",
236
+ "💡 *Müşteriye güncel fiyat bildirin*"
237
+ ])
238
+ elif action == "stock":
239
+ message_parts.extend([
240
+ "",
241
+ "💡 *Stok durumunu kontrol edip bildirin*"
242
+ ])
243
+
244
+ message_parts.extend([
245
+ "",
246
+ "---",
247
+ "Trek WhatsApp Bot",
248
+ "📍 Alsancak Mağaza - Mehmet Bey"
249
+ ])
250
+
251
+ return "\n".join(message_parts)
252
+
253
+ def send_test_notification() -> bool:
254
+ """
255
+ Test bildirimi gönder
256
+ """
257
+ return send_store_notification(
258
+ customer_phone="whatsapp:+905551234567",
259
+ customer_name="Test Müşteri",
260
+ product_name="FX 2 (Kırmızı, L Beden)",
261
+ action="test",
262
+ store_name="merkez",
263
+ additional_info="Bu bir test bildirimidir. Sistem çalışıyor."
264
+ )
265
+
266
+ # Kullanım örnekleri
267
+ def notify_product_reservation(customer_phone: str, product_name: str, store: Optional[str] = None):
268
+ """Ürün ayırtma bildirimi"""
269
+ return send_store_notification(
270
+ customer_phone=customer_phone,
271
+ customer_name=None,
272
+ product_name=product_name,
273
+ action="reserve",
274
+ store_name=store,
275
+ additional_info="Müşteri bu ürünü ayırtmak istiyor. Lütfen iletişime geçin."
276
+ )
277
+
278
+ def notify_price_inquiry(customer_phone: str, product_name: str):
279
+ """Fiyat sorusu bildirimi"""
280
+ return send_store_notification(
281
+ customer_phone=customer_phone,
282
+ customer_name=None,
283
+ product_name=product_name,
284
+ action="price",
285
+ additional_info="Müşteri güncel fiyat bilgisi istiyor."
286
+ )
287
+
288
+ def notify_stock_inquiry(customer_phone: str, product_name: str, store: Optional[str] = None):
289
+ """Stok sorusu bildirimi"""
290
+ return send_store_notification(
291
+ customer_phone=customer_phone,
292
+ customer_name=None,
293
+ product_name=product_name,
294
+ action="stock",
295
+ store_name=store,
296
+ additional_info="Müşteri stok durumunu soruyor."
297
+ )
298
+
299
+ def should_notify_mehmet_bey(user_message: str, intent_analysis: Optional[Dict] = None) -> tuple[bool, str, str]:
300
+ """
301
+ Mehmet Bey'e bildirim gönderilmeli mi kontrol et
302
+
303
+ Args:
304
+ user_message: Müşteri mesajı
305
+ intent_analysis: GPT-5 intent analiz sonucu (opsiyonel)
306
+
307
+ Returns:
308
+ (should_notify: bool, reason: str, urgency: "high"/"medium"/"low")
309
+ """
310
+
311
+ message_lower = user_message.lower()
312
+
313
+ # 1. REZERVASYON/AYIRTMA TALEPLERI (Yüksek öncelik)
314
+ reservation_keywords = [
315
+ # Direkt ayırtma istekleri
316
+ 'ayırt', 'ayırın', 'ayırtın', 'ayırtabilir', 'ayırır mısınız',
317
+ 'rezerve', 'rezervasyon', 'rezarvasyon',
318
+ 'tutun', 'tutar mısınız', 'tutabilir', 'tutarsanız', 'tuttun mu',
319
+ 'sakla', 'saklar mısınız', 'saklayın', 'saklayabilir',
320
+ 'kenara koy', 'kenara ayır', 'benim için koy', 'bana ayır',
321
+
322
+ # Kesin alım niyeti
323
+ 'alacağım', 'alıcam', 'alıyorum', 'alcam', 'alırım',
324
+ 'geliyorum', 'gelcem', 'gelicem', 'geliyom', 'geleceğim',
325
+ 'yola çıktım', 'yoldayım', 'yola çıkıyorum', 'yola çıkıcam',
326
+ 'birazdan oradayım', 'yarım saate', '1 saate', 'bir saate',
327
+ '30 dakika', '45 dakika', 'akşama kadar',
328
+
329
+ # Alım planı belirtenler
330
+ 'bugün alabilir', 'yarın alabilir', 'hafta sonu', 'haftasonu',
331
+ 'akşam uğra', 'öğleden sonra gel', 'sabah gel', 'öğlen gel',
332
+ 'eşime danış', 'eşimle konuş', 'eşimi getir', 'eşimle gel',
333
+ 'kesin alıcıyım', 'kesin istiyorum', 'bunu istiyorum', 'bunu alıyorum',
334
+ 'kartımı unuttum', 'nakit getir', 'para çek', 'atm',
335
+ 'maaş yattı', 'maaşım yatınca', 'ay başı', 'aybaşı'
336
+ ]
337
+
338
+ # 2. MAĞAZA LOKASYON SORULARI (Orta öncelik)
339
+ store_keywords = [
340
+ 'hangi mağaza', 'nerede var', 'nereden alabilirim', 'nerden',
341
+ 'caddebostan', 'ortaköy', 'ortakoy', 'alsancak', 'bahçeköy', 'bahcekoy',
342
+ 'en yakın', 'bana yakın', 'yakın mağaza', 'yakınımda',
343
+ 'mağazada görebilir', 'mağazaya gel', 'mağazanıza', 'mağazanızda',
344
+ 'test edebilir', 'deneyebilir', 'binebilir', 'test sürüş',
345
+ 'yerinde gör', 'canlı gör', 'fiziksel', 'gerçeğini gör',
346
+ 'adres', 'konum', 'lokasyon', 'neredesiniz', 'harita',
347
+ 'uzak mı', 'yakın mı', 'kaç km', 'nasıl gidilir'
348
+ ]
349
+
350
+ # 3. FİYAT/ÖDEME PAZARLIĞI (Orta öncelik)
351
+ price_keywords = [
352
+ 'indirim', 'kampanya', 'promosyon', 'fırsat', 'özel fiyat',
353
+ 'taksit', 'kredi kartı', 'banka', 'ödeme seçenek', 'vadeli',
354
+ 'peşin öde', 'nakit indirim', 'peşinat', 'kaparo', 'depozito',
355
+ 'pazarlık', 'son fiyat', 'en iyi fiyat', 'net fiyat', 'kdv dahil',
356
+ 'öğrenci indirimi', 'personel indirimi', 'kurumsal', 'toplu alım',
357
+ 'takas', 'eski bisiklet', 'değer', '2.el', 'ikinci el',
358
+ 'daha ucuz', 'daha uygun', 'bütçe', 'pahalı', 'ucuzlat',
359
+ 'fiyat düş', 'fiyat kır', 'esnet', 'yardımcı ol'
360
+ ]
361
+
362
+ # Önce rezervasyon kontrolü (en yüksek öncelik)
363
+ for keyword in reservation_keywords:
364
+ if keyword in message_lower:
365
+ return True, f"🔴 Rezervasyon talebi: '{keyword}' kelimesi tespit edildi", "high"
366
+
367
+ # Sonra mağaza soruları
368
+ for keyword in store_keywords:
369
+ if keyword in message_lower:
370
+ return True, f"📍 Mağaza/lokasyon sorusu: '{keyword}' kelimesi tespit edildi", "medium"
371
+
372
+ # Fiyat/ödeme konuları
373
+ for keyword in price_keywords:
374
+ if keyword in message_lower:
375
+ return True, f"💰 Fiyat/ödeme görüşmesi: '{keyword}' kelimesi tespit edildi", "medium"
376
+
377
+ # Intent analysis'ten ek kontrol (eğer varsa)
378
+ if intent_analysis:
379
+ # GPT-5 yüksek aciliyet tespit ettiyse
380
+ if intent_analysis.get('urgency') == 'high':
381
+ return True, "🚨 GPT-5 yüksek aciliyet tespit etti", "high"
382
+
383
+ # GPT-5 rezervasyon niyeti tespit ettiyse
384
+ if 'reserve' in intent_analysis.get('intents', []):
385
+ return True, "📌 GPT-5 rezervasyon niyeti tespit etti", "high"
386
+
387
+ # Hiçbir koşul sağlanmadıysa bildirim gönderme
388
+ return False, "", "low"
389
+
390
+ if __name__ == "__main__":
391
+ # Test
392
+ print("Mağaza bildirim sistemi test ediliyor...")
393
+ result = send_test_notification()
394
+ if result:
395
+ print("✅ Test bildirimi başarıyla gönderildi!")
396
+ else:
397
+ print("❌ Test bildirimi gönderilemedi!")
test_actual_function.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Test actual get_product_price_and_link function"""
3
+
4
+ from smart_warehouse_with_price import get_product_price_and_link
5
+
6
+ # Test with debug prints
7
+ def test_with_debug():
8
+ print("Testing get_product_price_and_link('Marlin 5')...")
9
+
10
+ # Temporarily modify the function to add debug
11
+ import smart_warehouse_with_price
12
+ import xml.etree.ElementTree as ET
13
+ import re
14
+
15
+ # Get the XML
16
+ xml_content = smart_warehouse_with_price.get_cached_trek_xml()
17
+ if not xml_content:
18
+ print("No XML content")
19
+ return
20
+
21
+ root = ET.fromstring(xml_content)
22
+
23
+ search_name = "marlin 5"
24
+ clean_search = re.sub(r'\s*\(\d{4}\)\s*', '', search_name).strip()
25
+
26
+ print(f"Clean search: '{clean_search}'")
27
+
28
+ best_match = None
29
+ best_score = 0
30
+ best_name = None
31
+
32
+ count = 0
33
+ for item in root.findall('item'):
34
+ rootlabel_elem = item.find('rootlabel')
35
+ if rootlabel_elem is None or not rootlabel_elem.text:
36
+ continue
37
+
38
+ item_name = rootlabel_elem.text.lower()
39
+
40
+ # Turkish char normalization
41
+ tr_map = {'ı': 'i', 'ğ': 'g', 'ü': 'u', 'ş': 's', 'ö': 'o', 'ç': 'c'}
42
+ for tr, en in tr_map.items():
43
+ item_name = item_name.replace(tr, en)
44
+
45
+ clean_item = re.sub(r'\s*\(\d{4}\)\s*', '', item_name).strip()
46
+
47
+ score = 0
48
+
49
+ # Exact match
50
+ if clean_search == clean_item:
51
+ score += 100
52
+ elif clean_item.startswith(clean_search + " ") or clean_item == clean_search:
53
+ score += 50
54
+ else:
55
+ name_parts = clean_search.split()
56
+ for part in name_parts:
57
+ if part in clean_item:
58
+ score += 1
59
+
60
+ if score > 0:
61
+ count += 1
62
+ if count <= 10: # Show first 10 matches
63
+ print(f" {count}. {rootlabel_elem.text[:50]}: score={score}")
64
+
65
+ if score > best_score:
66
+ best_score = score
67
+ best_match = item
68
+ best_name = rootlabel_elem.text
69
+ print(f" >>> NEW BEST: {best_name} with score {best_score}")
70
+
71
+ print(f"\nFinal best match: {best_name}")
72
+ print(f"Best score: {best_score}")
73
+
74
+ if best_match is not None:
75
+ price_elem = best_match.find('priceTaxWithCur')
76
+ link_elem = best_match.find('productLink')
77
+
78
+ if price_elem is not None:
79
+ print(f"Price: {price_elem.text}")
80
+ if link_elem is not None:
81
+ print(f"Link: {link_elem.text}")
82
+
83
+ # Now test actual function
84
+ print("\n" + "="*60)
85
+ print("Actual function result:")
86
+ price, link = get_product_price_and_link("Marlin 5")
87
+ print(f"Price: {price}")
88
+ print(f"Link: {link}")
89
+
90
+ if __name__ == "__main__":
91
+ test_with_debug()
test_context_aware.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # Test context-aware variant search
3
+
4
+ import sys
5
+ import os
6
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
7
+
8
+ # Import the updated function from app.py
9
+ from app import get_warehouse_stock
10
+
11
+ if __name__ == "__main__":
12
+ test_cases = [
13
+ "M Turuncu", # Should find all M Turuncu variants
14
+ "Marlin 6 M Turuncu", # Should find only Marlin 6 M Turuncu variants
15
+ "Marlin M Turuncu", # Should find only Marlin M Turuncu variants
16
+ "L Siyah", # Should find all L Siyah variants
17
+ "Marlin 6 L Siyah" # Should find only Marlin 6 L Siyah variants
18
+ ]
19
+
20
+ for test_case in test_cases:
21
+ print(f"\n=== Testing: {test_case} ===")
22
+ try:
23
+ result = get_warehouse_stock(test_case)
24
+ if result:
25
+ print("Sonuç:")
26
+ total_stock = 0
27
+ for item in result:
28
+ print(f" • {item}")
29
+ # Extract stock count for total
30
+ if ": " in item and " adet" in item:
31
+ stock_part = item.split(": ")[1].replace(" adet", "")
32
+ try:
33
+ total_stock += int(stock_part)
34
+ except:
35
+ pass
36
+ print(f"TOPLAM: {total_stock} adet")
37
+ else:
38
+ print("Sonuç bulunamadı")
39
+ except Exception as e:
40
+ print(f"Hata: {e}")
41
+ print("-" * 50)
test_crm.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ CRM System Test Script
4
+ """
5
+ from customer_manager import CustomerManager
6
+
7
+ # Test CRM
8
+ print("🧪 CRM Test Başlatılıyor...")
9
+ print("="*50)
10
+
11
+ # Initialize CRM
12
+ crm = CustomerManager()
13
+
14
+ # Test 1: Yeni müşteri oluşturma
15
+ print("\n1️⃣ Yeni müşteri test ediliyor...")
16
+ customer1 = crm.get_or_create_customer("+905551234567", "Ahmet")
17
+ print(f" ✅ Müşteri oluşturuldu: {customer1['name']} - {customer1['segment']}")
18
+
19
+ # Test 2: İkinci mesaj - selamlama
20
+ print("\n2️⃣ Selamlama mesajı testi...")
21
+ context = crm.get_customer_context("+905551234567")
22
+ print(f" Selamlama: {context['greeting']}")
23
+
24
+ # Test 3: Ürün araması güncelleme
25
+ print("\n3️⃣ Ürün araması güncelleme testi...")
26
+ crm.update_interaction("+905551234567", "Marlin 5 var mı?", "Marlin 5")
27
+ customer1 = crm.customers["+905551234567"]
28
+ print(f" İlgi alanları: {customer1['interests']}")
29
+ print(f" Toplam sorgu: {customer1['total_queries']}")
30
+
31
+ # Test 4: Başka bir müşteri - VIP simülasyonu
32
+ print("\n4️⃣ VIP müşteri testi...")
33
+ customer2 = crm.get_or_create_customer("+905559876543", "Mehmet")
34
+ # Birkaç satın alma ekle
35
+ crm.add_purchase("+905559876543", "Trek Madone SLR 9", 180000)
36
+ crm.add_purchase("+905559876543", "Trek Domane SL 7", 95000)
37
+ crm.add_purchase("+905559876543", "Trek FX Sport 6", 45000)
38
+ customer2 = crm.customers["+905559876543"]
39
+ print(f" ✅ VIP Müşteri: {customer2['name']} - {customer2['segment']}")
40
+ context2 = crm.get_customer_context("+905559876543")
41
+ print(f" VIP Selamlama: {context2['greeting']}")
42
+
43
+ # Test 5: Analitik
44
+ print("\n5️⃣ Analitik raporu...")
45
+ analytics = crm.get_analytics()
46
+ print(f" Toplam müşteri: {analytics['total_customers']}")
47
+ print(f" Segment dağılımı: {analytics['segments']}")
48
+ print(f" Toplam satış: {analytics['total_purchases']}")
49
+ print(f" Toplam gelir: ₺{analytics['total_revenue']:,.0f}")
50
+
51
+ # Test 6: Müşteri listesi
52
+ print("\n6️⃣ Müşteri listesi...")
53
+ customers = crm.get_customer_list()
54
+ for customer in customers:
55
+ print(f" - {customer['name']} ({customer['phone']}) - {customer['segment']} - {customer['total_queries']} sorgu")
56
+
57
+ print("\n" + "="*50)
58
+ print("✅ CRM Testleri Tamamlandı!")
59
+ print("\n📊 Dashboard'u görmek için:")
60
+ print(" http://localhost:8000/crm-analytics")
test_direct_madone.py ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Direct test without GPT-5 to see what's in the data"""
3
+
4
+ import sys
5
+ import os
6
+ sys.path.insert(0, '/home/samikoen/bf_wab_space')
7
+
8
+ import requests
9
+ import re
10
+ import json
11
+
12
+ # Get warehouse XML
13
+ print("Fetching warehouse XML...")
14
+ url = 'https://video.trek-turkey.com/bizimhesap-warehouse-xml-b2b-api-v2.php'
15
+ response = requests.get(url, verify=False, timeout=15)
16
+ xml_text = response.text
17
+
18
+ # Extract products
19
+ product_pattern = r'<Product>(.*?)</Product>'
20
+ all_products = re.findall(product_pattern, xml_text, re.DOTALL)
21
+
22
+ print(f"Total products: {len(all_products)}")
23
+
24
+ # Create products_summary EXACTLY like the function
25
+ products_summary = []
26
+ for i, product_block in enumerate(all_products):
27
+ name_match = re.search(r'<ProductName><!\[CDATA\[(.*?)\]\]></ProductName>', product_block)
28
+ variant_match = re.search(r'<ProductVariant><!\[CDATA\[(.*?)\]\]></ProductVariant>', product_block)
29
+
30
+ if name_match:
31
+ warehouses_with_stock = []
32
+ warehouse_regex = r'<Warehouse>.*?<Name><!\[CDATA\[(.*?)\]\]></Name>.*?<Stock>(.*?)</Stock>.*?</Warehouse>'
33
+ warehouses = re.findall(warehouse_regex, product_block, re.DOTALL)
34
+
35
+ for wh_name, wh_stock in warehouses:
36
+ try:
37
+ if int(wh_stock.strip()) > 0:
38
+ warehouses_with_stock.append(wh_name)
39
+ except:
40
+ pass
41
+
42
+ product_info = {
43
+ "index": i,
44
+ "name": name_match.group(1),
45
+ "variant": variant_match.group(1) if variant_match else "",
46
+ "warehouses": warehouses_with_stock
47
+ }
48
+ products_summary.append(product_info)
49
+
50
+ print(f"Products in summary: {len(products_summary)}")
51
+
52
+ # Search for MADONE and MARLIN
53
+ print("\n" + "="*60)
54
+ print("SEARCHING FOR MADONE SL 6:")
55
+ print("="*60)
56
+
57
+ madone_found = []
58
+ for p in products_summary:
59
+ if "MADONE SL 6" in p['name'].upper():
60
+ madone_found.append(p)
61
+
62
+ if madone_found:
63
+ print(f"✅ Found {len(madone_found)} MADONE SL 6 products:")
64
+ for p in madone_found:
65
+ if p['warehouses']: # Only show products with stock
66
+ print(f" Index {p['index']}: {p['name']} - {p['variant']}")
67
+ print(f" Stock in: {', '.join(p['warehouses'])}")
68
+ else:
69
+ print("❌ No MADONE SL 6 found!")
70
+
71
+ print("\n" + "="*60)
72
+ print("SEARCHING FOR MARLIN:")
73
+ print("="*60)
74
+
75
+ marlin_found = []
76
+ for p in products_summary:
77
+ if "MARLIN" in p['name'].upper():
78
+ marlin_found.append(p)
79
+
80
+ if marlin_found:
81
+ print(f"Found {len(marlin_found)} MARLIN products")
82
+ print("First 3 MARLIN products with stock:")
83
+ count = 0
84
+ for p in marlin_found:
85
+ if p['warehouses'] and count < 3:
86
+ print(f" Index {p['index']}: {p['name']} - {p['variant']}")
87
+ print(f" Stock in: {', '.join(p['warehouses'])}")
88
+ count += 1
89
+
90
+ # Check what GPT-5 would receive
91
+ print("\n" + "="*60)
92
+ print("WHAT GPT-5 RECEIVES:")
93
+ print("="*60)
94
+ print(f"Total products sent to GPT-5: {len(products_summary)}")
95
+
96
+ # Check indices around MADONE
97
+ print("\nProducts at indices 460-475:")
98
+ for idx in range(460, 476):
99
+ for p in products_summary:
100
+ if p['index'] == idx:
101
+ has_stock = "✅" if p['warehouses'] else "❌"
102
+ print(f" {idx} {has_stock}: {p['name'][:30]}...")
103
+ break
104
+
105
+ # Save the exact data GPT-5 receives
106
+ output_file = '/home/samikoen/gpt5_exact_input.json'
107
+ with open(output_file, 'w', encoding='utf-8') as f:
108
+ json.dump(products_summary, f, ensure_ascii=False, indent=2)
109
+ print(f"\n💾 Saved exact GPT-5 input to: {output_file}")
110
+
111
+ # Show what indices GPT-5 should return for "madone sl 6"
112
+ madone_indices = [str(p['index']) for p in madone_found if p['warehouses']]
113
+ if madone_indices:
114
+ print(f"\n🎯 GPT-5 SHOULD RETURN: {','.join(madone_indices)}")
115
+ else:
116
+ print("\n⚠️ No MADONE SL 6 with stock found!")
test_final.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # Final test with updated get_warehouse_stock function from app.py
3
+
4
+ import sys
5
+ import os
6
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
7
+
8
+ # Import the updated function from app.py
9
+ from app import get_warehouse_stock
10
+
11
+ if __name__ == "__main__":
12
+ test_cases = [
13
+ "M Turuncu",
14
+ "Marlin 6 M Turuncu",
15
+ "L Siyah",
16
+ "S Beyaz"
17
+ ]
18
+
19
+ for test_case in test_cases:
20
+ print(f"\n=== Testing: {test_case} ===")
21
+ try:
22
+ result = get_warehouse_stock(test_case)
23
+ if result:
24
+ print("Sonuç:")
25
+ for item in result:
26
+ print(f" • {item}")
27
+ else:
28
+ print("Sonuç bulunamadı")
29
+ except Exception as e:
30
+ print(f"Hata: {e}")
31
+ print("-" * 50)
test_gpt5_mock.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Mock GPT-5 to see what it should return"""
3
+
4
+ import json
5
+
6
+ # Load the exact data GPT-5 receives
7
+ with open('/home/samikoen/gpt5_exact_input.json', 'r', encoding='utf-8') as f:
8
+ products_summary = json.load(f)
9
+
10
+ print(f"Total products GPT-5 sees: {len(products_summary)}")
11
+
12
+ # Simulate GPT-5 logic for "madone sl 6"
13
+ query = "madone sl 6"
14
+ query_upper = query.upper()
15
+
16
+ matching_indices = []
17
+
18
+ for product in products_summary:
19
+ product_name = product['name'].upper()
20
+
21
+ # GPT-5 should match "MADONE SL 6" in product name
22
+ if "MADONE SL 6" in product_name:
23
+ # Only return products with stock
24
+ if product['warehouses']:
25
+ matching_indices.append(str(product['index']))
26
+
27
+ print(f"\nQuery: '{query}'")
28
+ print(f"GPT-5 should return: {','.join(matching_indices)}")
29
+
30
+ # Check what's actually there
31
+ print("\nMatching products details:")
32
+ for product in products_summary:
33
+ if "MADONE SL 6" in product['name'].upper():
34
+ if product['warehouses']:
35
+ print(f" Index {product['index']}: {product['name']} ({product['variant']})")
36
+ print(f" Warehouses: {product['warehouses']}")
37
+
38
+ # Check if MARLIN is in the data
39
+ print("\n" + "="*60)
40
+ print("CHECKING FOR MARLIN:")
41
+ marlin_count = 0
42
+ for product in products_summary:
43
+ if "MARLIN" in product['name'].upper():
44
+ marlin_count += 1
45
+ if marlin_count <= 3 and product['warehouses']:
46
+ print(f" Index {product['index']}: {product['name']} ({product['variant']})")
47
+
48
+ if marlin_count == 0:
49
+ print(" ❌ NO MARLIN PRODUCTS IN WAREHOUSE DATA!")
50
+ else:
51
+ print(f" Total MARLIN products: {marlin_count}")
test_m_turuncu.py ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ import requests
3
+ import xml.etree.ElementTree as ET
4
+
5
+ def get_warehouse_stock(product_name):
6
+ """B2B API'den mağaza stok bilgilerini çek - İyileştirilmiş versiyon"""
7
+ try:
8
+ import re
9
+ warehouse_url = 'https://video.trek-turkey.com/bizimhesap-warehouse-xml.php'
10
+ response = requests.get(warehouse_url, verify=False, timeout=15)
11
+
12
+ if response.status_code != 200:
13
+ return None
14
+
15
+ root = ET.fromstring(response.content)
16
+
17
+ # Turkish character normalization function
18
+ turkish_map = {'ı': 'i', 'ğ': 'g', 'ü': 'u', 'ş': 's', 'ö': 'o', 'ç': 'c', 'İ': 'i', 'I': 'i'}
19
+
20
+ def normalize_turkish(text):
21
+ import unicodedata
22
+ text = unicodedata.normalize('NFD', text)
23
+ text = ''.join(char for char in text if unicodedata.category(char) != 'Mn')
24
+ for tr_char, en_char in turkish_map.items():
25
+ text = text.replace(tr_char, en_char)
26
+ return text
27
+
28
+ # Normalize search product name
29
+ search_name = normalize_turkish(product_name.lower().strip())
30
+ search_name = search_name.replace('(2026)', '').replace('(2025)', '').replace(' gen 3', '').replace(' gen', '').strip()
31
+ search_words = search_name.split()
32
+
33
+ print(f"DEBUG: Aranan ürün: '{product_name}' -> normalize: '{search_name}' -> kelimeler: {search_words}")
34
+
35
+ best_matches = []
36
+ exact_matches = []
37
+ color_size_matches = [] # For size/color specific searches
38
+
39
+ # Check if this is a size/color query (like "M Turuncu")
40
+ is_size_color_query = len(search_words) <= 2 and any(word in ['s', 'm', 'l', 'xl', 'xs', 'small', 'medium', 'large', 'turuncu', 'siyah', 'beyaz', 'mavi', 'kirmizi', 'yesil'] for word in search_words)
41
+
42
+ if is_size_color_query:
43
+ print(f"DEBUG: Beden/renk araması algılandı: {search_words}")
44
+ # For size/color queries, look for products containing these terms
45
+ for product in root.findall('Product'):
46
+ product_name_elem = product.find('ProductName')
47
+ if product_name_elem is not None and product_name_elem.text:
48
+ xml_product_name = product_name_elem.text.strip()
49
+ normalized_xml = normalize_turkish(xml_product_name.lower())
50
+
51
+ # Check if all search words appear in the product name
52
+ if all(word in normalized_xml for word in search_words):
53
+ color_size_matches.append((product, xml_product_name, normalized_xml))
54
+ print(f"DEBUG: BEDEN/RENK EŞLEŞME: '{xml_product_name}'")
55
+
56
+ candidates = color_size_matches
57
+ else:
58
+ # Normal product search logic
59
+ # İlk geçiş: Tam eşleşmeleri bul
60
+ for product in root.findall('Product'):
61
+ product_name_elem = product.find('ProductName')
62
+ if product_name_elem is not None and product_name_elem.text:
63
+ xml_product_name = product_name_elem.text.strip()
64
+ normalized_xml = normalize_turkish(xml_product_name.lower())
65
+ normalized_xml = normalized_xml.replace('(2026)', '').replace('(2025)', '').replace(' gen 3', '').replace(' gen', '').strip()
66
+ xml_words = normalized_xml.split()
67
+
68
+ # Tam eşleşme kontrolü - ilk iki kelime tam aynı olmalı
69
+ if len(search_words) >= 2 and len(xml_words) >= 2:
70
+ search_key = f"{search_words[0]} {search_words[1]}"
71
+ xml_key = f"{xml_words[0]} {xml_words[1]}"
72
+
73
+ if search_key == xml_key:
74
+ exact_matches.append((product, xml_product_name, normalized_xml))
75
+ print(f"DEBUG: TAM EŞLEŞME: '{xml_product_name}'")
76
+
77
+ # Eğer tam eşleşme varsa, sadece onları kullan
78
+ candidates = exact_matches if exact_matches else []
79
+
80
+ # Eğer tam eşleşme yoksa, fuzzy matching yap
81
+ if not candidates:
82
+ print("DEBUG: Tam eşleşme yok, fuzzy matching yapılıyor...")
83
+ for product in root.findall('Product'):
84
+ product_name_elem = product.find('ProductName')
85
+ if product_name_elem is not None and product_name_elem.text:
86
+ xml_product_name = product_name_elem.text.strip()
87
+ normalized_xml = normalize_turkish(xml_product_name.lower())
88
+ normalized_xml = normalized_xml.replace('(2026)', '').replace('(2025)', '').replace(' gen 3', '').replace(' gen', '').strip()
89
+ xml_words = normalized_xml.split()
90
+
91
+ # Ortak kelime sayısını hesapla
92
+ common_words = set(search_words) & set(xml_words)
93
+
94
+ # En az 2 ortak kelime olmalı VE ilk kelime aynı olmalı (marka kontrolü)
95
+ if (len(common_words) >= 2 and
96
+ len(search_words) > 0 and len(xml_words) > 0 and
97
+ search_words[0] == xml_words[0]):
98
+ best_matches.append((product, xml_product_name, normalized_xml, len(common_words)))
99
+ print(f"DEBUG: FUZZY EŞLEŞME: '{xml_product_name}' (ortak: {len(common_words)})")
100
+
101
+ # En çok ortak kelimeye sahip olanları seç
102
+ if best_matches:
103
+ max_common = max(match[3] for match in best_matches)
104
+ candidates = [(match[0], match[1], match[2]) for match in best_matches if match[3] == max_common]
105
+
106
+ print(f"DEBUG: Toplam {len(candidates)} aday ürün bulundu")
107
+
108
+ # Stok bilgilerini topla ve tekrarları önle
109
+ warehouse_stock_map = {} # warehouse_name -> total_stock
110
+
111
+ for product, xml_name, _ in candidates:
112
+ warehouses = product.find('Warehouses')
113
+ if warehouses is not None:
114
+ for warehouse in warehouses.findall('Warehouse'):
115
+ name_elem = warehouse.find('Name')
116
+ stock_elem = warehouse.find('Stock')
117
+
118
+ if name_elem is not None and stock_elem is not None:
119
+ warehouse_name = name_elem.text if name_elem.text else "Bilinmeyen"
120
+ try:
121
+ stock_count = int(stock_elem.text) if stock_elem.text else 0
122
+ if stock_count > 0:
123
+ # Aynı mağaza için stokları topla
124
+ if warehouse_name in warehouse_stock_map:
125
+ warehouse_stock_map[warehouse_name] += stock_count
126
+ else:
127
+ warehouse_stock_map[warehouse_name] = stock_count
128
+ print(f"DEBUG: STOK BULUNDU - {warehouse_name}: {stock_count} adet ({xml_name})")
129
+ except (ValueError, TypeError):
130
+ pass
131
+
132
+ if warehouse_stock_map:
133
+ # Mağaza stoklarını liste halinde döndür
134
+ all_warehouse_info = []
135
+ for warehouse_name, total_stock in warehouse_stock_map.items():
136
+ all_warehouse_info.append(f"{warehouse_name}: {total_stock} adet")
137
+ return all_warehouse_info
138
+ else:
139
+ print("DEBUG: Hiçbir mağazada stok bulunamadı")
140
+ return ["Hiçbir mağazada stokta bulunmuyor"]
141
+
142
+ except Exception as e:
143
+ print(f"Mağaza stok bilgisi çekme hatası: {e}")
144
+ return None
145
+
146
+ if __name__ == "__main__":
147
+ # Test the function with different M Turuncu variations
148
+ test_cases = [
149
+ "M Turuncu",
150
+ "m turuncu",
151
+ "M TURUNCU",
152
+ "Medium Turuncu",
153
+ "Marlin 6 M Turuncu"
154
+ ]
155
+
156
+ for test_case in test_cases:
157
+ print(f"=== Testing: {test_case} ===")
158
+ result = get_warehouse_stock(test_case)
159
+ print(f"Result: {result}")
160
+ print()
test_marlin_fix.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Test Marlin 5 after Turkish character fix"""
3
+
4
+ import os
5
+ os.environ['OPENAI_API_KEY'] = os.getenv('OPENAI_API_KEY', '')
6
+
7
+ from smart_warehouse_with_price import get_warehouse_stock_smart_with_price
8
+
9
+ def test_marlin():
10
+ print("Testing Marlin 5 search...")
11
+ print("="*60)
12
+
13
+ result = get_warehouse_stock_smart_with_price("Marlin 5")
14
+
15
+ if result:
16
+ print("\n✅ Results found:")
17
+ for item in result[:3]: # Show first 3
18
+ print(f"\n{item}")
19
+ else:
20
+ print("\n❌ No results")
21
+
22
+ if __name__ == "__main__":
23
+ test_marlin()
test_name_request.py ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Test Name Request Functionality
4
+ """
5
+ from customer_manager import CustomerManager
6
+ import os
7
+
8
+ # Clean test - yeni database
9
+ if os.path.exists("test_customers.json"):
10
+ os.remove("test_customers.json")
11
+
12
+ crm = CustomerManager("test_customers.json")
13
+
14
+ print("🧪 İsim Sorma Test Senaryoları")
15
+ print("="*50)
16
+
17
+ # Test 1: Rezervasyon anında isim sorma
18
+ print("\n1️⃣ REZERVASYON ANINDA İSİM SORMA")
19
+ phone = "+905551111111"
20
+ message = "Marlin 5'i rezerve edebilir miyim?"
21
+
22
+ customer = crm.get_or_create_customer(phone)
23
+ should_ask, question = crm.should_ask_for_name(phone, message)
24
+
25
+ print(f"Müşteri: {message}")
26
+ print(f"İsim sorulmalı mı: {should_ask}")
27
+ if question:
28
+ print(f"Soru: {question}")
29
+
30
+ # Müşteri cevap veriyor
31
+ print("\nMüşteri yanıtı: 'Ahmet'")
32
+ extracted = crm.extract_name_from_response("Ahmet")
33
+ print(f"Çıkarılan isim: {extracted}")
34
+
35
+ # Test 2: 3. mesajda nazikçe sorma
36
+ print("\n2️⃣ 3. MESAJDA NAZİKÇE SORMA")
37
+ phone2 = "+905552222222"
38
+
39
+ # 1. mesaj
40
+ crm.update_interaction(phone2, "Merhaba", None)
41
+ should_ask, question = crm.should_ask_for_name(phone2, "Merhaba")
42
+ print(f"1. mesaj - İsim sorulmalı: {should_ask}")
43
+
44
+ # 2. mesaj
45
+ crm.update_interaction(phone2, "FX 3 var mı?", "FX 3")
46
+ should_ask, question = crm.should_ask_for_name(phone2, "FX 3 var mı?")
47
+ print(f"2. mesaj - İsim sorulmalı: {should_ask}")
48
+
49
+ # 3. mesaj
50
+ crm.update_interaction(phone2, "Fiyatı nedir?", None)
51
+ should_ask, question = crm.should_ask_for_name(phone2, "Fiyatı nedir?")
52
+ print(f"3. mesaj - İsim sorulmalı: {should_ask}")
53
+ if question:
54
+ print(f"Soru: {question}")
55
+
56
+ # Test 3: Farklı isim formatları
57
+ print("\n3️⃣ İSİM ÇIKARMA TESTLERİ")
58
+ test_responses = [
59
+ "Mehmet",
60
+ "benim adım Ali",
61
+ "Ben Ayşe",
62
+ "Adım Fatma",
63
+ "ismim Hasan",
64
+ "Veli, teşekkürler",
65
+ "Ahmet Yılmaz", # İsim + Soyisim
66
+ "selam" # İsim değil
67
+ ]
68
+
69
+ for response in test_responses:
70
+ extracted = crm.extract_name_from_response(response)
71
+ print(f"'{response}' -> {extracted if extracted else 'İsim bulunamadı'}")
72
+
73
+ # Test 4: Mağaza ziyareti
74
+ print("\n4️⃣ MAĞAZA ZİYARETİ ANINDA")
75
+ phone3 = "+905553333333"
76
+ message = "Yarın mağazaya gelerek test sürüşü yapabilir miyim?"
77
+ should_ask, question = crm.should_ask_for_name(phone3, message)
78
+ print(f"Müşteri: {message}")
79
+ print(f"İsim sorulmalı: {should_ask}")
80
+ if question:
81
+ print(f"Soru: {question}")
82
+
83
+ # Test 5: Ödeme görüşmesi
84
+ print("\n5️⃣ ÖDEME GÖRÜŞMESİ ANINDA")
85
+ phone4 = "+905554444444"
86
+ message = "12 taksit yapıyor musunuz?"
87
+ should_ask, question = crm.should_ask_for_name(phone4, message)
88
+ print(f"Müşteri: {message}")
89
+ print(f"İsim sorulmalı: {should_ask}")
90
+ if question:
91
+ print(f"Soru: {question}")
92
+
93
+ # Test 6: 5+ sorgudan sonra
94
+ print("\n6️⃣ POTANSİYEL MÜŞTERİ (5+ SORGU)")
95
+ phone5 = "+905555555555"
96
+ for i in range(5):
97
+ crm.update_interaction(phone5, f"Soru {i+1}", None)
98
+
99
+ should_ask, question = crm.should_ask_for_name(phone5, "Başka model var mı?")
100
+ print(f"5+ sorgudan sonra")
101
+ print(f"İsim sorulmalı: {should_ask}")
102
+ if question:
103
+ print(f"Soru: {question}")
104
+
105
+ print("\n" + "="*50)
106
+ print("✅ Testler Tamamlandı!")
107
+
108
+ # Cleanup
109
+ if os.path.exists("test_customers.json"):
110
+ os.remove("test_customers.json")
test_simulate_whatsapp.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Simulate a WhatsApp message for MADONE SL 6"""
3
+
4
+ import sys
5
+ import os
6
+ sys.path.insert(0, '/home/samikoen/bf_wab_space')
7
+
8
+ # Set API key if exists
9
+ api_key = "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"
10
+ os.environ['OPENAI_API_KEY'] = api_key
11
+
12
+ print("="*60)
13
+ print("SIMULATING WHATSAPP MESSAGE: 'madone sl 6'")
14
+ print("="*60)
15
+
16
+ from smart_warehouse_complete import get_warehouse_stock_smart_complete
17
+
18
+ try:
19
+ print("\nCalling get_warehouse_stock_smart_complete('madone sl 6')...")
20
+ result = get_warehouse_stock_smart_complete("madone sl 6")
21
+
22
+ if result:
23
+ print("\n✅ RESULT:")
24
+ for line in result:
25
+ print(f" {line}")
26
+ else:
27
+ print("\n❌ No result (None returned)")
28
+
29
+ except Exception as e:
30
+ print(f"\n❌ ERROR: {e}")
31
+ import traceback
32
+ traceback.print_exc()
test_stock_consistency.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Test Stock Consistency Algorithm
4
+ Simulates conversation flow to ensure stock information remains consistent
5
+ """
6
+
7
+ # Test conversation memory functions
8
+ import sys
9
+ sys.path.insert(0, '/home/samikoen/bf_wab_space')
10
+
11
+ def test_stock_consistency():
12
+ print("🧪 STOK TUTARLILİK TESTİ")
13
+ print("="*50)
14
+
15
+ # Simulate phone number
16
+ test_phone = "+905551111111"
17
+
18
+ # Import the conversation functions
19
+ try:
20
+ from app import get_conversation_context, cache_product_info, get_cached_product_info
21
+
22
+ # Test 1: İlk ürün araması
23
+ print("\n1️⃣ İLK ÜRÜN ARAMASI")
24
+ search_term = "FX 3 var mı"
25
+ product_info = """📦 FX 3 (2024) - Şehir Bisikleti
26
+ 💰 Fiyat: 73.000 TL
27
+ 🏪 Stok: İzmir Alsancak, İstanbul Ortaköy
28
+ 📏 L beden mevcut"""
29
+
30
+ # Cache product info
31
+ cache_product_info(test_phone, search_term, product_info)
32
+ print(f"✅ Cached: {search_term}")
33
+ print(f" Info: {product_info[:100]}...")
34
+
35
+ # Test 2: İkinci mesaj - benzer sorgu
36
+ print("\n2️⃣ İKINCİ MESAJ - BENZERİ SORGU")
37
+ follow_up = "boyum 190 cm L boy olur mu"
38
+
39
+ cached_result = get_cached_product_info(test_phone, follow_up)
40
+ if cached_result:
41
+ print(f"✅ CACHED bulundu: {follow_up}")
42
+ print(f" Cached data: {cached_result[:100]}...")
43
+ else:
44
+ print(f"❌ Cache BULUNAMADI: {follow_up}")
45
+
46
+ # Test 3: Üçüncü mesaj - aynı ürün hakkında
47
+ print("\n3️⃣ ÜÇÜNCÜ MESAJ - AYNI ÜRÜN")
48
+ third_msg = "ayırtmak istiyorum"
49
+
50
+ cached_result = get_cached_product_info(test_phone, third_msg)
51
+ if cached_result:
52
+ print(f"✅ CACHED bulundu: {third_msg}")
53
+ print(f" Cached data: {cached_result[:100]}...")
54
+ else:
55
+ print(f"❌ Cache BULUNAMADI: {third_msg}")
56
+
57
+ # Test 4: Conversation context
58
+ print("\n4️⃣ CONVERSATION CONTEXT")
59
+ context = get_conversation_context(test_phone)
60
+ print(f"Context keys: {list(context.keys())}")
61
+ print(f"Last search term: {context.get('last_search_term')}")
62
+ print(f"Has product info: {'Yes' if context.get('last_product_info') else 'No'}")
63
+
64
+ # Test 5: Farklı ürün araması
65
+ print("\n5️⃣ FARKLI ÜRÜN ARAMASI")
66
+ new_search = "Marlin 5 var mı"
67
+
68
+ cached_result = get_cached_product_info(test_phone, new_search)
69
+ if cached_result:
70
+ print(f"⚠️ Yanlış cache bulundu (beklenmeyen): {new_search}")
71
+ else:
72
+ print(f"✅ Doğru: Farklı ürün için cache yok: {new_search}")
73
+
74
+ # Test 6: Cache expiration (30 dakika sonrası simülasyonu)
75
+ print("\n6️⃣ CACHE EXPIRATION TEST")
76
+ import datetime
77
+ context = get_conversation_context(test_phone)
78
+
79
+ # Zamanı geriye al (31 dakika önce)
80
+ old_time = datetime.datetime.now() - datetime.timedelta(minutes=31)
81
+ context["last_search_time"] = old_time
82
+
83
+ cached_result = get_cached_product_info(test_phone, "FX 3 fiyat")
84
+ if cached_result:
85
+ print(f"❌ Cache expire olmadı (sorun)")
86
+ else:
87
+ print(f"✅ Cache doğru şekilde expire oldu")
88
+
89
+ print("\n" + "="*50)
90
+ print("✅ STOK TUTARLILİK TESTİ TAMAMLANDI!")
91
+
92
+ except ImportError as e:
93
+ print(f"❌ Import hatası: {e}")
94
+ print("Bu test app.py ile aynı dizinde çalışmalı")
95
+
96
+ if __name__ == "__main__":
97
+ test_stock_consistency()
test_updated_warehouse.py ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ import requests
3
+ import xml.etree.ElementTree as ET
4
+
5
+ def get_warehouse_stock(product_name):
6
+ """B2B API'den mağaza stok bilgilerini çek - Final Updated Version"""
7
+ try:
8
+ import re
9
+ warehouse_url = 'https://video.trek-turkey.com/bizimhesap-warehouse-xml.php'
10
+ response = requests.get(warehouse_url, verify=False, timeout=15)
11
+
12
+ if response.status_code != 200:
13
+ return None
14
+
15
+ root = ET.fromstring(response.content)
16
+
17
+ # Turkish character normalization function
18
+ turkish_map = {'ı': 'i', 'ğ': 'g', 'ü': 'u', 'ş': 's', 'ö': 'o', 'ç': 'c', 'İ': 'i', 'I': 'i'}
19
+
20
+ def normalize_turkish(text):
21
+ import unicodedata
22
+ text = unicodedata.normalize('NFD', text)
23
+ text = ''.join(char for char in text if unicodedata.category(char) != 'Mn')
24
+ for tr_char, en_char in turkish_map.items():
25
+ text = text.replace(tr_char, en_char)
26
+ return text
27
+
28
+ # Normalize search product name
29
+ search_name = normalize_turkish(product_name.lower().strip())
30
+ search_name = search_name.replace('(2026)', '').replace('(2025)', '').replace(' gen 3', '').replace(' gen', '').strip()
31
+ search_words = search_name.split()
32
+
33
+ best_matches = []
34
+ exact_matches = []
35
+ variant_matches = []
36
+ candidates = []
37
+
38
+ # Check if this is a size/color specific query (like "M Turuncu")
39
+ is_size_color_query = (len(search_words) <= 3 and
40
+ any(word in ['s', 'm', 'l', 'xl', 'xs', 'small', 'medium', 'large',
41
+ 'turuncu', 'siyah', 'beyaz', 'mavi', 'kirmizi', 'yesil',
42
+ 'orange', 'black', 'white', 'blue', 'red', 'green']
43
+ for word in search_words))
44
+
45
+ # İlk geçiş: Variant alanında beden/renk araması
46
+ if is_size_color_query:
47
+ for product in root.findall('Product'):
48
+ product_name_elem = product.find('ProductName')
49
+ variant_elem = product.find('Variant')
50
+
51
+ if product_name_elem is not None and product_name_elem.text:
52
+ xml_product_name = product_name_elem.text.strip()
53
+
54
+ # Variant field check
55
+ if variant_elem is not None and variant_elem.text:
56
+ variant_text = normalize_turkish(variant_elem.text.lower().replace('-', ' '))
57
+
58
+ # Check if all search words are in variant field
59
+ if all(word in variant_text for word in search_words):
60
+ variant_matches.append((product, xml_product_name, variant_text))
61
+
62
+ if variant_matches:
63
+ candidates = variant_matches
64
+ else:
65
+ # Fallback to normal product name search
66
+ is_size_color_query = False
67
+
68
+ # İkinci geçiş: Normal ürün adı tam eşleşmeleri (variant match yoksa)
69
+ if not is_size_color_query or not candidates:
70
+ for product in root.findall('Product'):
71
+ product_name_elem = product.find('ProductName')
72
+ if product_name_elem is not None and product_name_elem.text:
73
+ xml_product_name = product_name_elem.text.strip()
74
+ normalized_xml = normalize_turkish(xml_product_name.lower())
75
+ normalized_xml = normalized_xml.replace('(2026)', '').replace('(2025)', '').replace(' gen 3', '').replace(' gen', '').strip()
76
+ xml_words = normalized_xml.split()
77
+
78
+ # Tam eşleşme kontrolü - ilk iki kelime tam aynı olmalı
79
+ if len(search_words) >= 2 and len(xml_words) >= 2:
80
+ search_key = f"{search_words[0]} {search_words[1]}"
81
+ xml_key = f"{xml_words[0]} {xml_words[1]}"
82
+
83
+ if search_key == xml_key:
84
+ exact_matches.append((product, xml_product_name, normalized_xml))
85
+
86
+ # Eğer variant match varsa onu kullan, yoksa exact matches kullan
87
+ if not candidates:
88
+ candidates = exact_matches if exact_matches else []
89
+
90
+ # Eğer hala bir match yoksa, fuzzy matching yap
91
+ if not candidates:
92
+ for product in root.findall('Product'):
93
+ product_name_elem = product.find('ProductName')
94
+ if product_name_elem is not None and product_name_elem.text:
95
+ xml_product_name = product_name_elem.text.strip()
96
+ normalized_xml = normalize_turkish(xml_product_name.lower())
97
+ normalized_xml = normalized_xml.replace('(2026)', '').replace('(2025)', '').replace(' gen 3', '').replace(' gen', '').strip()
98
+ xml_words = normalized_xml.split()
99
+
100
+ # Ortak kelime sayısını hesapla
101
+ common_words = set(search_words) & set(xml_words)
102
+
103
+ # En az 2 ortak kelime olmalı VE ilk kelime aynı olmalı (marka kontrolü)
104
+ if (len(common_words) >= 2 and
105
+ len(search_words) > 0 and len(xml_words) > 0 and
106
+ search_words[0] == xml_words[0]):
107
+ best_matches.append((product, xml_product_name, normalized_xml, len(common_words)))
108
+
109
+ # En çok ortak kelimeye sahip olanları seç
110
+ if best_matches:
111
+ max_common = max(match[3] for match in best_matches)
112
+ candidates = [(match[0], match[1], match[2]) for match in best_matches if match[3] == max_common]
113
+
114
+ # Stok bilgilerini topla ve tekrarları önle
115
+ warehouse_stock_map = {} # warehouse_name -> total_stock
116
+
117
+ for product, xml_name, _ in candidates:
118
+ warehouses = product.find('Warehouses')
119
+ if warehouses is not None:
120
+ for warehouse in warehouses.findall('Warehouse'):
121
+ name_elem = warehouse.find('Name')
122
+ stock_elem = warehouse.find('Stock')
123
+
124
+ if name_elem is not None and stock_elem is not None:
125
+ warehouse_name = name_elem.text if name_elem.text else "Bilinmeyen"
126
+ try:
127
+ stock_count = int(stock_elem.text) if stock_elem.text else 0
128
+ if stock_count > 0:
129
+ # Aynı mağaza için stokları topla
130
+ if warehouse_name in warehouse_stock_map:
131
+ warehouse_stock_map[warehouse_name] += stock_count
132
+ else:
133
+ warehouse_stock_map[warehouse_name] = stock_count
134
+ except (ValueError, TypeError):
135
+ pass
136
+
137
+ if warehouse_stock_map:
138
+ # Mağaza stoklarını liste halinde döndür
139
+ all_warehouse_info = []
140
+ for warehouse_name, total_stock in warehouse_stock_map.items():
141
+ all_warehouse_info.append(f"{warehouse_name}: {total_stock} adet")
142
+ return all_warehouse_info
143
+ else:
144
+ return ["Hiçbir mağazada stokta bulunmuyor"]
145
+
146
+ except Exception as e:
147
+ print(f"Mağaza stok bilgisi çekme hatası: {e}")
148
+ return None
149
+
150
+ if __name__ == "__main__":
151
+ test_cases = [
152
+ "M Turuncu",
153
+ "Marlin 6 M Turuncu",
154
+ "L Siyah"
155
+ ]
156
+
157
+ for test_case in test_cases:
158
+ print(f"\n=== Testing: {test_case} ===")
159
+ result = get_warehouse_stock(test_case)
160
+ if result:
161
+ print("Sonuç:")
162
+ for item in result:
163
+ print(f" • {item}")
164
+ else:
165
+ print("Sonuç bulunamadı")
166
+ print("-" * 50)
test_variant.py ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ import requests
3
+ import xml.etree.ElementTree as ET
4
+
5
+ def test_variant_search(product_name):
6
+ """B2B API'den variant field'ı kontrol et"""
7
+ try:
8
+ warehouse_url = 'https://video.trek-turkey.com/bizimhesap-warehouse-xml.php'
9
+ response = requests.get(warehouse_url, verify=False, timeout=15)
10
+
11
+ if response.status_code != 200:
12
+ return None
13
+
14
+ root = ET.fromstring(response.content)
15
+
16
+ # Turkish character normalization function
17
+ turkish_map = {'ı': 'i', 'ğ': 'g', 'ü': 'u', 'ş': 's', 'ö': 'o', 'ç': 'c', 'İ': 'i', 'I': 'i'}
18
+
19
+ def normalize_turkish(text):
20
+ import unicodedata
21
+ text = unicodedata.normalize('NFD', text)
22
+ text = ''.join(char for char in text if unicodedata.category(char) != 'Mn')
23
+ for tr_char, en_char in turkish_map.items():
24
+ text = text.replace(tr_char, en_char)
25
+ return text
26
+
27
+ # Normalize search product name
28
+ search_name = normalize_turkish(product_name.lower().strip())
29
+ search_words = search_name.split()
30
+
31
+ print(f"DEBUG: Aranan: '{product_name}' -> normalize: '{search_name}' -> kelimeler: {search_words}")
32
+
33
+ variant_matches = []
34
+
35
+ # Check variant field for M-TURUNCU like patterns
36
+ for product in root.findall('Product'):
37
+ product_name_elem = product.find('ProductName')
38
+ variant_elem = product.find('Variant')
39
+
40
+ if product_name_elem is not None and product_name_elem.text:
41
+ xml_product_name = product_name_elem.text.strip()
42
+
43
+ # Check variant field
44
+ if variant_elem is not None and variant_elem.text:
45
+ variant_text = variant_elem.text.strip()
46
+ variant_normalized = normalize_turkish(variant_text.lower().replace('-', ' '))
47
+
48
+ # Check if all search words are in variant field
49
+ if all(word in variant_normalized for word in search_words):
50
+ # Check for stock
51
+ warehouses = product.find('Warehouses')
52
+ stock_info = []
53
+ if warehouses is not None:
54
+ for warehouse in warehouses.findall('Warehouse'):
55
+ name_elem = warehouse.find('Name')
56
+ stock_elem = warehouse.find('Stock')
57
+
58
+ if name_elem is not None and stock_elem is not None:
59
+ warehouse_name = name_elem.text if name_elem.text else "Bilinmeyen"
60
+ try:
61
+ stock_count = int(stock_elem.text) if stock_elem.text else 0
62
+ if stock_count > 0:
63
+ stock_info.append(f"{warehouse_name}: {stock_count}")
64
+ except (ValueError, TypeError):
65
+ pass
66
+
67
+ variant_matches.append({
68
+ 'product_name': xml_product_name,
69
+ 'variant': variant_text,
70
+ 'variant_normalized': variant_normalized,
71
+ 'stock_info': stock_info
72
+ })
73
+
74
+ print(f"DEBUG: VARIANT MATCH - {xml_product_name}")
75
+ print(f" Variant: {variant_text} -> {variant_normalized}")
76
+ if stock_info:
77
+ print(f" Stok: {', '.join(stock_info)}")
78
+ else:
79
+ print(f" Stokta yok")
80
+
81
+ return variant_matches
82
+
83
+ except Exception as e:
84
+ print(f"Hata: {e}")
85
+ return None
86
+
87
+ if __name__ == "__main__":
88
+ # Test different cases
89
+ test_cases = [
90
+ "M Turuncu",
91
+ "m turuncu",
92
+ "L Siyah",
93
+ "S Beyaz"
94
+ ]
95
+
96
+ for test_case in test_cases:
97
+ print(f"\n=== Testing: {test_case} ===")
98
+ result = test_variant_search(test_case)
99
+
100
+ if result:
101
+ print(f"Toplam {len(result)} variant match bulundu:")
102
+ for i, match in enumerate(result, 1):
103
+ print(f"{i}. {match['product_name']} - {match['variant']}")
104
+ if match['stock_info']:
105
+ print(f" Stokta: {', '.join(match['stock_info'])}")
106
+ else:
107
+ print(f" Stokta değil")
108
+ else:
109
+ print("Variant match bulunamadı")
110
+ print("-" * 50)
test_warehouse.py ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ import requests
3
+ import xml.etree.ElementTree as ET
4
+
5
+ def get_warehouse_stock(product_name):
6
+ """B2B API'den mağaza stok bilgilerini çek - İyileştirilmiş versiyon"""
7
+ try:
8
+ import re
9
+ warehouse_url = 'https://video.trek-turkey.com/bizimhesap-warehouse-xml.php'
10
+ response = requests.get(warehouse_url, verify=False, timeout=15)
11
+
12
+ if response.status_code != 200:
13
+ return None
14
+
15
+ root = ET.fromstring(response.content)
16
+
17
+ # Turkish character normalization function
18
+ turkish_map = {'ı': 'i', 'ğ': 'g', 'ü': 'u', 'ş': 's', 'ö': 'o', 'ç': 'c', 'İ': 'i', 'I': 'i'}
19
+
20
+ def normalize_turkish(text):
21
+ import unicodedata
22
+ text = unicodedata.normalize('NFD', text)
23
+ text = ''.join(char for char in text if unicodedata.category(char) != 'Mn')
24
+ for tr_char, en_char in turkish_map.items():
25
+ text = text.replace(tr_char, en_char)
26
+ return text
27
+
28
+ # Normalize search product name
29
+ search_name = normalize_turkish(product_name.lower().strip())
30
+ search_name = search_name.replace('(2026)', '').replace('(2025)', '').replace(' gen 3', '').replace(' gen', '').strip()
31
+ search_words = search_name.split()
32
+
33
+ print(f"DEBUG: Aranan ürün: '{product_name}' -> normalize: '{search_name}' -> kelimeler: {search_words}")
34
+
35
+ best_matches = []
36
+ exact_matches = []
37
+
38
+ # İlk geçiş: Tam eşleşmeleri bul
39
+ for product in root.findall('Product'):
40
+ product_name_elem = product.find('ProductName')
41
+ if product_name_elem is not None and product_name_elem.text:
42
+ xml_product_name = product_name_elem.text.strip()
43
+ normalized_xml = normalize_turkish(xml_product_name.lower())
44
+ normalized_xml = normalized_xml.replace('(2026)', '').replace('(2025)', '').replace(' gen 3', '').replace(' gen', '').strip()
45
+ xml_words = normalized_xml.split()
46
+
47
+ # Tam eşleşme kontrolü - ilk iki kelime tam aynı olmalı
48
+ if len(search_words) >= 2 and len(xml_words) >= 2:
49
+ search_key = f"{search_words[0]} {search_words[1]}"
50
+ xml_key = f"{xml_words[0]} {xml_words[1]}"
51
+
52
+ if search_key == xml_key:
53
+ exact_matches.append((product, xml_product_name, normalized_xml))
54
+ print(f"DEBUG: TAM EŞLEŞME bulundu: '{xml_product_name}'")
55
+
56
+ # Eğer tam eşleşme varsa, sadece onları kullan
57
+ candidates = exact_matches if exact_matches else []
58
+
59
+ # Eğer tam eşleşme yoksa, fuzzy matching yap
60
+ if not candidates:
61
+ print("DEBUG: Tam eşleşme yok, fuzzy matching yapılıyor...")
62
+ for product in root.findall('Product'):
63
+ product_name_elem = product.find('ProductName')
64
+ if product_name_elem is not None and product_name_elem.text:
65
+ xml_product_name = product_name_elem.text.strip()
66
+ normalized_xml = normalize_turkish(xml_product_name.lower())
67
+ normalized_xml = normalized_xml.replace('(2026)', '').replace('(2025)', '').replace(' gen 3', '').replace(' gen', '').strip()
68
+ xml_words = normalized_xml.split()
69
+
70
+ # Ortak kelime sayısını hesapla
71
+ common_words = set(search_words) & set(xml_words)
72
+
73
+ # En az 2 ortak kelime olmalı VE ilk kelime aynı olmalı (marka kontrolü)
74
+ if (len(common_words) >= 2 and
75
+ len(search_words) > 0 and len(xml_words) > 0 and
76
+ search_words[0] == xml_words[0]):
77
+ best_matches.append((product, xml_product_name, normalized_xml, len(common_words)))
78
+ print(f"DEBUG: FUZZY EŞLEŞME: '{xml_product_name}' (ortak: {len(common_words)})")
79
+
80
+ # En çok ortak kelimeye sahip olanları seç
81
+ if best_matches:
82
+ max_common = max(match[3] for match in best_matches)
83
+ candidates = [(match[0], match[1], match[2]) for match in best_matches if match[3] == max_common]
84
+
85
+ print(f"DEBUG: Toplam {len(candidates)} aday ürün bulundu")
86
+
87
+ # Stok bilgilerini topla ve tekrarları önle
88
+ warehouse_stock_map = {} # warehouse_name -> total_stock
89
+
90
+ for product, xml_name, _ in candidates:
91
+ warehouses = product.find('Warehouses')
92
+ if warehouses is not None:
93
+ for warehouse in warehouses.findall('Warehouse'):
94
+ name_elem = warehouse.find('Name')
95
+ stock_elem = warehouse.find('Stock')
96
+
97
+ if name_elem is not None and stock_elem is not None:
98
+ warehouse_name = name_elem.text if name_elem.text else "Bilinmeyen"
99
+ try:
100
+ stock_count = int(stock_elem.text) if stock_elem.text else 0
101
+ if stock_count > 0:
102
+ # Aynı mağaza için stokları topla
103
+ if warehouse_name in warehouse_stock_map:
104
+ warehouse_stock_map[warehouse_name] += stock_count
105
+ else:
106
+ warehouse_stock_map[warehouse_name] = stock_count
107
+ print(f"DEBUG: STOK BULUNDU - {warehouse_name}: {stock_count} adet ({xml_name})")
108
+ except (ValueError, TypeError):
109
+ pass
110
+
111
+ if warehouse_stock_map:
112
+ # Mağaza stoklarını liste halinde döndür
113
+ all_warehouse_info = []
114
+ for warehouse_name, total_stock in warehouse_stock_map.items():
115
+ all_warehouse_info.append(f"{warehouse_name}: {total_stock} adet")
116
+ return all_warehouse_info
117
+ else:
118
+ print("DEBUG: Hiçbir mağazada stok bulunamadı")
119
+ return ["Hiçbir mağazada stokta bulunmuyor"]
120
+
121
+ except Exception as e:
122
+ print(f"Mağaza stok bilgisi çekme hatası: {e}")
123
+ return None
124
+
125
+ if __name__ == "__main__":
126
+ # Test the function with different inputs
127
+ test_cases = [
128
+ "MARLIN 6 (2026)",
129
+ "Marlin 6"
130
+ ]
131
+
132
+ for test_case in test_cases:
133
+ print(f"=== Testing: {test_case} ===")
134
+ result = get_warehouse_stock(test_case)
135
+ print(f"Result: {result}")
136
+ print()
warehouse_stock_finder.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Ultra simple and fast warehouse stock finder"""
2
+
3
+ def get_warehouse_stock(product_name):
4
+ """Find warehouse stock FAST - no XML parsing, just regex"""
5
+ try:
6
+ import re
7
+ import requests
8
+
9
+ # Get XML
10
+ url = 'https://video.trek-turkey.com/bizimhesap-warehouse-xml-b2b-api-v2.php'
11
+ response = requests.get(url, verify=False, timeout=7)
12
+ xml_text = response.text
13
+
14
+ # Turkish normalize
15
+ def normalize(text):
16
+ tr_map = {'İ': 'i', 'I': 'i', 'ı': 'i', 'Ğ': 'g', 'ğ': 'g', 'Ü': 'u', 'ü': 'u', 'Ş': 's', 'ş': 's', 'Ö': 'o', 'ö': 'o', 'Ç': 'c', 'ç': 'c'}
17
+ # FIRST normalize Turkish chars
18
+ for tr, en in tr_map.items():
19
+ text = text.replace(tr, en)
20
+ # THEN lowercase
21
+ text = text.lower()
22
+ return text
23
+
24
+ # Parse query
25
+ query = normalize(product_name.strip()).replace('(2026)', '').replace('(2025)', '').strip()
26
+ words = query.split()
27
+
28
+ # Find size
29
+ sizes = ['s', 'm', 'l', 'xl', 'xs', 'xxl', 'ml']
30
+ size = next((w for w in words if w in sizes), None)
31
+ product_words = [w for w in words if w not in sizes and w not in ['beden', 'size', 'boy']]
32
+
33
+ # Build search pattern
34
+ if 'madone' in product_words and 'sl' in product_words and '6' in product_words:
35
+ pattern = 'MADONE SL 6 GEN 8'
36
+ else:
37
+ pattern = ' '.join(product_words).upper()
38
+
39
+ print(f"DEBUG - Searching: {pattern}, Size: {size}")
40
+
41
+ # Search for product + variant combo
42
+ if size:
43
+ # Direct search for product with specific size
44
+ size_pattern = f'{size.upper()}-'
45
+
46
+ # Find all occurrences of the product name
47
+ import re
48
+ product_regex = f'<Product>.*?<ProductName><!\\[CDATA\\[{re.escape(pattern)}\\]\\]></ProductName>.*?<ProductVariant><!\\[CDATA\\[{size_pattern}.*?\\]\\]></ProductVariant>.*?</Product>'
49
+
50
+ match = re.search(product_regex, xml_text, re.DOTALL)
51
+
52
+ if match:
53
+ product_block = match.group(0)
54
+ print(f"DEBUG - Found product with {size_pattern} variant")
55
+
56
+ # Extract warehouses
57
+ warehouse_info = []
58
+ warehouse_regex = r'<Warehouse>.*?<Name><!\\[CDATA\\[(.*?)\\]\\]></Name>.*?<Stock>(.*?)</Stock>.*?</Warehouse>'
59
+ warehouses = re.findall(warehouse_regex, product_block, re.DOTALL)
60
+
61
+ for wh_name, wh_stock in warehouses:
62
+ try:
63
+ stock = int(wh_stock.strip())
64
+ if stock > 0:
65
+ # Format name
66
+ if "CADDEBOSTAN" in wh_name:
67
+ display = "Caddebostan mağazası"
68
+ elif "ORTAKÖY" in wh_name:
69
+ display = "Ortaköy mağazası"
70
+ elif "ALSANCAK" in wh_name:
71
+ display = "İzmir Alsancak mağazası"
72
+ elif "BAHCEKOY" in wh_name or "BAHÇEKÖY" in wh_name:
73
+ display = "Bahçeköy mağazası"
74
+ else:
75
+ display = wh_name
76
+
77
+ warehouse_info.append(f"{display}: Mevcut")
78
+ except:
79
+ pass
80
+
81
+ return warehouse_info if warehouse_info else ["Hiçbir mağazada mevcut değil"]
82
+ else:
83
+ print(f"DEBUG - No {size_pattern} variant found for {pattern}")
84
+ return ["Hiçbir mağazada mevcut değil"]
85
+ else:
86
+ # No size filter - get all stock
87
+ return ["Beden bilgisi belirtilmedi"]
88
+
89
+ except Exception as e:
90
+ print(f"Error: {e}")
91
+ return None
whatsapp_features.py ADDED
@@ -0,0 +1,432 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ WhatsApp Trek Chatbot Enhanced Features
4
+ 1. Ürün Karşılaştırma
5
+ 2. Bütçe Önerileri
6
+ 3. Fiyat Hesaplamaları
7
+ WhatsApp için basitleştirilmiş versiyon - Görsel AI ve profil sistemi yok
8
+ """
9
+
10
+ import re
11
+ from datetime import datetime
12
+
13
+ class WhatsAppProductComparison:
14
+ """WhatsApp için ürün karşılaştırma sistemi"""
15
+
16
+ def __init__(self, products_data):
17
+ self.products = products_data
18
+
19
+ def round_price(self, price_str):
20
+ """Fiyatı yuvarlama formülüne göre yuvarla"""
21
+ try:
22
+ price_float = float(price_str)
23
+ # Fiyat 200000 üzerindeyse en yakın 5000'lik basamağa yuvarla
24
+ if price_float > 200000:
25
+ return str(round(price_float / 5000) * 5000)
26
+ # Fiyat 30000 üzerindeyse en yakın 1000'lik basamağa yuvarla
27
+ elif price_float > 30000:
28
+ return str(round(price_float / 1000) * 1000)
29
+ # Fiyat 10000 üzerindeyse en yakın 100'lük basamağa yuvarla
30
+ elif price_float > 10000:
31
+ return str(round(price_float / 100) * 100)
32
+ # Diğer durumlarda en yakın 10'luk basamağa yuvarla
33
+ else:
34
+ return str(round(price_float / 10) * 10)
35
+ except (ValueError, TypeError):
36
+ return price_str
37
+
38
+ def find_products_by_name(self, product_names):
39
+ """İsimlere göre ürünleri bul"""
40
+ found_products = []
41
+ used_products = set() # Aynı ürünü iki kez eklememek için
42
+
43
+ for name in product_names:
44
+ for product in self.products:
45
+ product_id = product[2] # full_name'i unique id olarak kullan
46
+ if product_id not in used_products and name.lower() in product[2].lower():
47
+ found_products.append(product)
48
+ used_products.add(product_id)
49
+ break
50
+ return found_products
51
+
52
+ def create_comparison_whatsapp(self, product_names):
53
+ """WhatsApp için karşılaştırma metni oluştur"""
54
+ products = self.find_products_by_name(product_names)
55
+
56
+ if len(products) < 2:
57
+ return "❌ Karşılaştırma için en az 2 ürün gerekli."
58
+
59
+ # Karşılaştırma metnini hazırla
60
+ comparison_text = "📊 **ÜRÜN KARŞILAŞTIRMASI**\n\n"
61
+
62
+ for i, product in enumerate(products, 1):
63
+ name, item_info, full_name = product
64
+
65
+ # Ürün bilgilerini parse et
66
+ stock_status = item_info[0] if len(item_info) > 0 else "Bilgi yok"
67
+ price_raw = item_info[1] if len(item_info) > 1 and item_info[1] else "Fiyat yok"
68
+ product_link = item_info[2] if len(item_info) > 2 else ""
69
+
70
+ # Fiyatı yuvarlama formülüne göre yuvarla
71
+ if price_raw != "Fiyat yok":
72
+ price = self.round_price(price_raw)
73
+ price_display = f"{price} TL"
74
+ else:
75
+ price_display = price_raw
76
+
77
+ comparison_text += f"**{i}. {full_name}**\n"
78
+ comparison_text += f"📦 Stok: {stock_status}\n"
79
+ comparison_text += f"💰 Fiyat: {price_display}\n"
80
+
81
+ # Resim URL'si varsa ekle (6. index)
82
+ if len(item_info) > 6 and item_info[6]:
83
+ comparison_text += f"🖼️ Resim: {item_info[6]}\n"
84
+
85
+ if product_link:
86
+ comparison_text += f"🔗 Link: {product_link}\n"
87
+
88
+ comparison_text += "\n" + "─" * 25 + "\n\n"
89
+
90
+ return comparison_text
91
+
92
+ def get_similar_products(self, product_name, category_filter=None):
93
+ """Benzer ürünleri bul"""
94
+ similar_products = []
95
+ base_name = product_name.lower().split()[0] # İlk kelimeyi al
96
+
97
+ for product in self.products:
98
+ product_full_name = product[2].lower()
99
+ if base_name in product_full_name and product_name.lower() != product_full_name:
100
+ if category_filter:
101
+ if category_filter.lower() in product_full_name:
102
+ similar_products.append(product)
103
+ else:
104
+ similar_products.append(product)
105
+
106
+ return similar_products[:3] # İlk 3 benzer ürün
107
+
108
+ class WhatsAppBudgetRecommendations:
109
+ """WhatsApp için bütçe önerileri"""
110
+
111
+ def __init__(self, products_data):
112
+ self.products = products_data
113
+
114
+ def get_budget_recommendations(self, budget_min, budget_max):
115
+ """Bütçeye uygun öneriler"""
116
+ suitable_products = []
117
+
118
+ for product in self.products:
119
+ if product[1][0] == "stokta" and product[1][1]: # Stokta ve fiyatı var
120
+ try:
121
+ price = float(product[1][1])
122
+ if budget_min <= price <= budget_max:
123
+ suitable_products.append(product)
124
+ except (ValueError, TypeError):
125
+ continue
126
+
127
+ return suitable_products[:5] # İlk 5 öneri
128
+
129
+ def format_budget_recommendations_whatsapp(self, products, budget_min, budget_max):
130
+ """WhatsApp için bütçe önerilerini formatla"""
131
+ if not products:
132
+ if budget_min == budget_max:
133
+ return f"❌ {budget_min:,.0f} TL bütçenize uygun stokta ürün bulunamadı."
134
+ else:
135
+ return f"❌ {budget_min:,.0f}-{budget_max:,.0f} TL bütçenize uygun stokta ürün bulunamadı."
136
+
137
+ if budget_min == budget_max:
138
+ text = f"💰 **{budget_min:,.0f} TL BÜTÇENİZE EN YAKIN ÖNERİLER**\n\n"
139
+ else:
140
+ text = f"💰 **{budget_min:,.0f}-{budget_max:,.0f} TL BÜTÇENİZE UYGUN ÖNERİLER**\n\n"
141
+
142
+ for i, product in enumerate(products, 1):
143
+ name, item_info, full_name = product
144
+ price = item_info[1] if len(item_info) > 1 else "Fiyat yok"
145
+ product_link = item_info[2] if len(item_info) > 2 else ""
146
+
147
+ text += f"**{i}. {full_name}**\n"
148
+ # Fiyatı formatlı göster
149
+ try:
150
+ price_float = float(price)
151
+ price_formatted = f"{price_float:,.0f}"
152
+ except:
153
+ price_formatted = price
154
+ text += f"💰 Fiyat: {price_formatted} TL\n"
155
+
156
+ # Resim URL'si varsa ekle
157
+ if len(item_info) > 6 and item_info[6]:
158
+ text += f"🖼️ Resim: {item_info[6]}\n"
159
+
160
+ if product_link:
161
+ text += f"🔗 Link: {product_link}\n"
162
+
163
+ text += "\n"
164
+
165
+ return text
166
+
167
+ class WhatsAppCategoryRecommendations:
168
+ """WhatsApp için kategori bazlı öneriler"""
169
+
170
+ def __init__(self, products_data):
171
+ self.products = products_data
172
+
173
+ def get_category_products(self, category_keywords):
174
+ """Kategoriye göre ürünleri bul"""
175
+ category_products = []
176
+
177
+ for product in self.products:
178
+ if product[1][0] == "stokta": # Sadece stokta olanlar
179
+ product_name = product[2].lower()
180
+ for keyword in category_keywords:
181
+ if keyword.lower() in product_name:
182
+ category_products.append(product)
183
+ break
184
+
185
+ return category_products[:100] # İlk 100 ürün - bütçe filtresi için daha fazla ürün
186
+
187
+ def format_category_recommendations(self, category_name, products):
188
+ """Kategori önerilerini formatla"""
189
+ if not products:
190
+ return f"❌ {category_name} kategorisinde stokta ürün bulunamadı."
191
+
192
+ text = f"🚲 **{category_name.upper()} KATEGORİSİ ÖNERİLERİ**\n\n"
193
+
194
+ for i, product in enumerate(products, 1):
195
+ name, item_info, full_name = product
196
+ price = item_info[1] if len(item_info) > 1 else "Fiyat yok"
197
+
198
+ # Fiyatı formatlı göster
199
+ try:
200
+ price_float = float(price)
201
+ price_formatted = f"{price_float:,.0f}"
202
+ except:
203
+ price_formatted = price
204
+
205
+ text += f"**{i}. {full_name}**\n"
206
+ text += f"💰 Fiyat: {price_formatted} TL\n\n"
207
+
208
+ return text
209
+
210
+ # Global instance'lar
211
+ whatsapp_product_comparison = None
212
+ whatsapp_budget_recommendations = None
213
+ whatsapp_category_recommendations = None
214
+
215
+ def initialize_whatsapp_features(products_data):
216
+ """WhatsApp enhanced özellikleri başlat"""
217
+ global whatsapp_product_comparison, whatsapp_budget_recommendations, whatsapp_category_recommendations
218
+
219
+ whatsapp_product_comparison = WhatsAppProductComparison(products_data)
220
+ whatsapp_budget_recommendations = WhatsAppBudgetRecommendations(products_data)
221
+ whatsapp_category_recommendations = WhatsAppCategoryRecommendations(products_data)
222
+
223
+ def handle_whatsapp_comparison_request(user_message):
224
+ """WhatsApp karşılaştırma talebini işle"""
225
+ try:
226
+ if "karşılaştır" in user_message.lower() or "compare" in user_message.lower():
227
+ # Ürün isimlerini çıkarmaya çalış
228
+ words = user_message.lower().split()
229
+ potential_products = []
230
+
231
+ # Bilinen model isimlerini ara - daha spesifik
232
+ known_models = ["émonda", "madone", "domane", "marlin", "fuel", "powerfly", "fx", "checkpoint", "procaliber", "supercaliber"]
233
+
234
+ # Marlin 6, Marlin 7 gibi spesifik modelleri ara
235
+ message_lower = user_message.lower()
236
+ if "marlin" in message_lower:
237
+ import re
238
+ marlin_numbers = re.findall(r'marlin\s*(\d+)', message_lower)
239
+ if len(marlin_numbers) >= 2:
240
+ # Marlin 6, Marlin 7 gibi spesifik modeller bulundu
241
+ for num in marlin_numbers:
242
+ potential_products.append(f"marlin {num}")
243
+ elif len(marlin_numbers) == 1:
244
+ potential_products.append(f"marlin {marlin_numbers[0]}")
245
+ else:
246
+ potential_products.append("marlin")
247
+
248
+ # Diğer modelleri ara
249
+ for word in words:
250
+ for model in known_models:
251
+ if model != "marlin" and model in word: # marlin'i ayrı işliyoruz
252
+ potential_products.append(model)
253
+
254
+ # Duplicate'leri kaldır
255
+ potential_products = list(dict.fromkeys(potential_products))
256
+
257
+ if len(potential_products) >= 2 and whatsapp_product_comparison:
258
+ comparison_result = whatsapp_product_comparison.create_comparison_whatsapp(potential_products)
259
+ return comparison_result
260
+
261
+ return None
262
+ except Exception as e:
263
+ print(f"WhatsApp Comparison error: {e}")
264
+ return None
265
+
266
+ def handle_whatsapp_price_query(user_message):
267
+ """WhatsApp fiyat sorgusu - 'ne kadar', 'fiyat', 'kaç para' gibi"""
268
+ try:
269
+ user_lower = user_message.lower()
270
+
271
+ # Fiyat sorgusu anahtar kelimeleri
272
+ price_query_keywords = ["ne kadar", "fiyat", "kaç para", "fiyatı", "kaça", "para"]
273
+ if not any(keyword in user_lower for keyword in price_query_keywords):
274
+ return None
275
+
276
+ # Ürün adı tespit et
277
+ product_keywords = ["madone", "émonda", "domane", "marlin", "fuel", "powerfly", "fx", "checkpoint", "procaliber", "supercaliber", "ds", "verve", "rail"]
278
+ found_product = None
279
+
280
+ for keyword in product_keywords:
281
+ if keyword in user_lower:
282
+ found_product = keyword
283
+ break
284
+
285
+ if found_product and whatsapp_budget_recommendations:
286
+ # Ürünü products listesinde ara
287
+ matching_products = []
288
+ for product in whatsapp_budget_recommendations.products:
289
+ if product[1][0] == "stokta" and found_product in product[2].lower():
290
+ matching_products.append(product)
291
+
292
+ if matching_products:
293
+ # İlk ürünü al (en temel model)
294
+ main_product = matching_products[0]
295
+ name, item_info, full_name = main_product
296
+ price = item_info[1] if len(item_info) > 1 else "Fiyat yok"
297
+
298
+ try:
299
+ price_float = float(price)
300
+ price_formatted = f"{price_float:,.0f}"
301
+ except:
302
+ price_formatted = price
303
+
304
+ # Aynı serinin diğer modellerini bul
305
+ series_products = matching_products[:3] # İlk 3 model
306
+
307
+ response = f"🚲 **{found_product.upper()} SERİSİ FİYATLARI**\\n\\n"
308
+
309
+ for i, product in enumerate(series_products, 1):
310
+ name, item_info, full_name = product
311
+ price = item_info[1] if len(item_info) > 1 else "Fiyat yok"
312
+
313
+ try:
314
+ price_float = float(price)
315
+ price_formatted = f"{price_float:,.0f}"
316
+ except:
317
+ price_formatted = price
318
+
319
+ response += f"**{i}. {full_name}**\\n"
320
+ response += f"💰 Fiyat: {price_formatted} TL\\n\\n"
321
+
322
+ return response
323
+
324
+ return None
325
+ except Exception as e:
326
+ print(f"WhatsApp Price Query error: {e}")
327
+ return None
328
+
329
+ def handle_whatsapp_budget_request(user_message):
330
+ """WhatsApp bütçe talebini işle"""
331
+ try:
332
+ message_lower = user_message.lower()
333
+
334
+ # Kategori sorgusu değilse sadece bütçe sorgusuna bak
335
+ category_keywords = ["yol", "dağ", "şehir", "elektrikli", "gravel", "marlin", "madone", "émonda", "domane", "fuel", "powerfly", "fx", "ds", "checkpoint"]
336
+ has_category = any(keyword in message_lower for keyword in category_keywords)
337
+
338
+ if has_category:
339
+ return None # Kategori varsa category function'a bırak
340
+
341
+ # Sadece sayı varsa bütçe olarak algıla
342
+ import re
343
+ numbers = re.findall(r'\d+', user_message)
344
+
345
+ if numbers and whatsapp_budget_recommendations:
346
+ budget_value = int(numbers[0]) * 1000 # 350 -> 350000
347
+
348
+ # Tüm stokta olan ürünleri al
349
+ all_products = []
350
+ for product in whatsapp_budget_recommendations.products:
351
+ if product[1][0] == "stokta" and product[1][1]:
352
+ try:
353
+ price = float(product[1][1])
354
+ price_diff = abs(price - budget_value)
355
+ all_products.append((product, price_diff))
356
+ except:
357
+ continue
358
+
359
+ # Fiyat farkına göre sırala
360
+ all_products.sort(key=lambda x: x[1])
361
+ closest_products = [product[0] for product in all_products[:5]] # En yakın 5 ürün
362
+
363
+ if closest_products:
364
+ return f"💰 {budget_value:,} TL bütçenize en yakın öneriler:\n\n" + whatsapp_budget_recommendations.format_budget_recommendations_whatsapp(
365
+ closest_products, budget_value, budget_value
366
+ )
367
+
368
+ return None
369
+ except Exception as e:
370
+ print(f"WhatsApp Budget error: {e}")
371
+ return None
372
+
373
+ def handle_whatsapp_category_request(user_message, phone_number=None):
374
+ """WhatsApp kategori önerisi talebini işle"""
375
+ try:
376
+ user_lower = user_message.lower()
377
+
378
+ # Bütçe bilgisini çıkar (sadece sayı varsa)
379
+ budget_value = None
380
+ import re
381
+ numbers = re.findall(r'\d+', user_message)
382
+ if numbers:
383
+ budget_value = int(numbers[0]) * 1000 # 350 -> 350000
384
+
385
+ # Kategori tespiti - basit keyword matching
386
+ categories = {
387
+ "dağ bisikleti": ["dağ", "dag", "offroad", "mountain", "marlin", "fuel", "procaliber", "supercaliber"],
388
+ "yol bisikleti": ["yol", "road", "hız", "yarış", "émonda", "madone", "domane", "speed"],
389
+ "şehir bisikleti": ["şehir", "sehir", "city", "urban", "fx", "ds", "dual sport", "verve"],
390
+ "elektrikli bisiklet": ["elektrikli", "electric", "e-bike", "ebike", "powerfly", "rail", "fuel exe", "domane+", "fx+", "ds+", "verve+", "townie"],
391
+ "gravel bisiklet": ["gravel", "çakıl", "checkpoint"]
392
+ }
393
+
394
+ for category_name, keywords in categories.items():
395
+ if any(keyword in user_lower for keyword in keywords):
396
+ if whatsapp_category_recommendations:
397
+ # Kategori ürünlerini al
398
+ products = whatsapp_category_recommendations.get_category_products(keywords)
399
+
400
+ # Bütçe varsa fiyat farkına göre sırala
401
+ if budget_value is not None:
402
+ products_with_price_diff = []
403
+ for product in products:
404
+ if product[1][0] == "stokta" and product[1][1]:
405
+ try:
406
+ price = float(product[1][1])
407
+ price_diff = abs(price - budget_value)
408
+ products_with_price_diff.append((product, price_diff))
409
+ except:
410
+ continue
411
+
412
+ # Fiyat farkına göre sırala (en yakın fiyat önce)
413
+ products_with_price_diff.sort(key=lambda x: x[1])
414
+ products = [product[0] for product in products_with_price_diff[:5]] # En yakın 5 ürün
415
+ else:
416
+ products = products[:5] # Bütçe yoksa ilk 5 ürün
417
+
418
+ if products:
419
+ response = whatsapp_category_recommendations.format_category_recommendations(
420
+ category_name, products
421
+ )
422
+
423
+ # Bütçe bilgisi varsa ekle
424
+ if budget_value is not None:
425
+ response = f"💰 {budget_value:,} TL bütçenize en yakın öneriler:\n\n" + response
426
+
427
+ return response
428
+
429
+ return None
430
+ except Exception as e:
431
+ print(f"WhatsApp Category error: {e}")
432
+ return None
whatsapp_improved_chatbot.py ADDED
@@ -0,0 +1,298 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ WhatsApp Improved Chatbot with Professional Product Search
3
+ Optimized for mobile WhatsApp formatting
4
+ """
5
+
6
+ from product_search import ProductSearchEngine
7
+ import re
8
+ from typing import List, Dict, Optional, Tuple
9
+
10
+ class WhatsAppImprovedChatbot:
11
+ """Enhanced chatbot with intelligent product handling for WhatsApp"""
12
+
13
+ def __init__(self, products: List[Tuple]):
14
+ """Initialize with product list"""
15
+ self.search_engine = ProductSearchEngine(products)
16
+ self.products = products
17
+
18
+ def detect_product_query(self, message: str) -> bool:
19
+ """Detect if message is asking about products"""
20
+ product_keywords = [
21
+ 'bisiklet', 'bike', 'model', 'fiyat', 'price', 'stok', 'stock',
22
+ 'özellikleri', 'features', 'hangi', 'which', 'var mı', 'mevcut',
23
+ 'kaç', 'how much', 'ne kadar', 'kampanya', 'indirim', 'discount',
24
+ 'karşılaştır', 'compare', 'benzer', 'similar', 'alternatif',
25
+ 'öneri', 'recommend', 'tavsiye', 'suggest'
26
+ ]
27
+
28
+ message_lower = message.lower()
29
+
30
+ # Check for product keywords
31
+ if any(keyword in message_lower for keyword in product_keywords):
32
+ return True
33
+
34
+ # Check for model numbers
35
+ if re.search(r'\b\d+\.?\d*\b', message):
36
+ return True
37
+
38
+ # Check for known product categories
39
+ categories = ['trek', 'marlin', 'fuel', 'slash', 'remedy', 'checkpoint',
40
+ 'domane', 'madone', 'emonda', 'fx', 'dual', 'wahoo', 'powerfly']
41
+ if any(cat in message_lower for cat in categories):
42
+ return True
43
+
44
+ return False
45
+
46
+ def extract_query_intent(self, message: str) -> Dict:
47
+ """Extract the intent from user query"""
48
+ intent = {
49
+ 'type': 'general',
50
+ 'action': None,
51
+ 'filters': {}
52
+ }
53
+
54
+ message_lower = message.lower()
55
+
56
+ # Price query
57
+ if any(word in message_lower for word in ['fiyat', 'price', 'kaç', 'ne kadar', 'how much']):
58
+ intent['type'] = 'price'
59
+ intent['action'] = 'get_price'
60
+
61
+ # Stock query
62
+ elif any(word in message_lower for word in ['stok', 'stock', 'var mı', 'mevcut', 'available']):
63
+ intent['type'] = 'stock'
64
+ intent['action'] = 'check_stock'
65
+
66
+ # Comparison query
67
+ elif any(word in message_lower for word in ['karşılaştır', 'compare', 'fark', 'difference']):
68
+ intent['type'] = 'comparison'
69
+ intent['action'] = 'compare_products'
70
+
71
+ # Recommendation query
72
+ elif any(word in message_lower for word in ['öneri', 'recommend', 'tavsiye', 'suggest', 'hangi']):
73
+ intent['type'] = 'recommendation'
74
+ intent['action'] = 'recommend'
75
+
76
+ # Feature query
77
+ elif any(word in message_lower for word in ['özellik', 'feature', 'spec', 'detay', 'detail']):
78
+ intent['type'] = 'features'
79
+ intent['action'] = 'get_features'
80
+
81
+ # Extract context (size, color, type, etc.)
82
+ context = self.search_engine.extract_product_context(message)
83
+ intent['filters'] = context
84
+
85
+ return intent
86
+
87
+ def format_product_info_whatsapp(self, product: Tuple, intent: Dict) -> str:
88
+ """Format product information for WhatsApp"""
89
+ name = product[2] # Full name
90
+ info = product[1] # Product info array
91
+
92
+ # Basic info
93
+ stock_status = info[0] if len(info) > 0 else "bilinmiyor"
94
+
95
+ # Build response for WhatsApp (more compact format)
96
+ response_parts = []
97
+
98
+ # Product name with status emoji
99
+ if stock_status == "stokta":
100
+ response_parts.append(f"🚴‍♂️ *{name}*")
101
+ response_parts.append("✅ *Stokta mevcut*")
102
+ else:
103
+ response_parts.append(f"🚴‍♂️ *{name}*")
104
+ response_parts.append("❌ *Stokta yok*")
105
+
106
+ # Price information (only if in stock)
107
+ if intent['type'] in ['price', 'general'] and stock_status == "stokta" and len(info) > 1:
108
+ price = info[1]
109
+ response_parts.append(f"💰 *Fiyat:* {price} TL")
110
+
111
+ # Campaign price if available
112
+ if len(info) > 4 and info[4]:
113
+ response_parts.append(f"🎯 *Kampanya:* {info[4]} TL")
114
+
115
+ # Calculate discount
116
+ try:
117
+ normal_price = float(info[1])
118
+ campaign_price = float(info[4])
119
+ discount = normal_price - campaign_price
120
+ if discount > 0:
121
+ response_parts.append(f"💸 *İndirim:* {discount:.0f} TL")
122
+ except:
123
+ pass
124
+
125
+ # Wire transfer price if no campaign
126
+ elif len(info) > 3 and info[3]:
127
+ response_parts.append(f"🏦 *Havale:* {info[3]} TL")
128
+
129
+ # Product link (shortened for WhatsApp)
130
+ if len(info) > 2 and info[2]:
131
+ response_parts.append(f"🔗 Ürün sayfası: {info[2]}")
132
+
133
+ return "\n".join(response_parts)
134
+
135
+ def generate_product_response(self, message: str) -> str:
136
+ """Generate response for product queries optimized for WhatsApp"""
137
+ intent = self.extract_query_intent(message)
138
+
139
+ # Search for products
140
+ search_results = self.search_engine.search(message)
141
+
142
+ if not search_results:
143
+ # No products found - provide helpful response
144
+ response = self._handle_no_results_whatsapp(message)
145
+ elif len(search_results) == 1:
146
+ # Single product found
147
+ product = search_results[0][1]
148
+ response = self.format_product_info_whatsapp(product, intent)
149
+ elif search_results[0][0] > 0.9:
150
+ # Very high confidence match
151
+ product = search_results[0][1]
152
+ response = self.format_product_info_whatsapp(product, intent)
153
+ else:
154
+ # Multiple products found
155
+ response = self._handle_multiple_results_whatsapp(search_results[:3], intent) # Only 3 for WhatsApp
156
+
157
+ return response
158
+
159
+ def _handle_no_results_whatsapp(self, query: str) -> str:
160
+ """Handle case when no products are found - WhatsApp format"""
161
+ response_parts = ["🤔 *Aradığınız ürünü bulamadım.*"]
162
+
163
+ # Get suggestions
164
+ suggestions = self.search_engine.generate_suggestions(query)
165
+
166
+ if suggestions:
167
+ response_parts.append("\n💡 *Belki şunları arıyor olabilirsiniz:*")
168
+ for i, suggestion in enumerate(suggestions[:2], 1): # Only 2 suggestions for WhatsApp
169
+ response_parts.append(f"{i}. {suggestion}")
170
+ else:
171
+ response_parts.append("\n📝 *Örnek aramalar:*")
172
+ response_parts.append("• Marlin 5")
173
+ response_parts.append("• FX 3 Disc")
174
+ response_parts.append("• Checkpoint ALR 5")
175
+
176
+ return "\n".join(response_parts)
177
+
178
+ def _handle_multiple_results_whatsapp(self, results: List[Tuple[float, Tuple]], intent: Dict) -> str:
179
+ """Handle multiple product results - WhatsApp format"""
180
+ response_parts = ["📋 *Birden fazla ürün buldum:*\n"]
181
+
182
+ for i, (score, product) in enumerate(results, 1):
183
+ name = product[2]
184
+ stock = product[1][0] if len(product[1]) > 0 else "bilinmiyor"
185
+
186
+ # Compact info line for WhatsApp
187
+ if stock == "stokta":
188
+ status_emoji = "✅"
189
+ response_parts.append(f"*{i}. {name}* {status_emoji}")
190
+
191
+ # Add price if intent is price-related
192
+ if intent['type'] in ['price', 'general'] and len(product[1]) > 1:
193
+ price = product[1][1]
194
+ response_parts.append(f" 💰 {price} TL")
195
+
196
+ # Show campaign price if available
197
+ if len(product[1]) > 4 and product[1][4]:
198
+ response_parts.append(f" 🎯 Kampanya: {product[1][4]} TL")
199
+
200
+ # Add product link if available
201
+ if len(product[1]) > 2 and product[1][2]:
202
+ response_parts.append(f" 🔗 {product[1][2]}")
203
+ else:
204
+ status_emoji = "❌"
205
+ response_parts.append(f"*{i}. {name}* {status_emoji}")
206
+
207
+ response_parts.append("") # Empty line between products
208
+
209
+ response_parts.append("💡 _Daha detaylı bilgi için ürün adını yazın_")
210
+
211
+ return "\n".join(response_parts)
212
+
213
+ def handle_comparison_whatsapp(self, message: str) -> Optional[str]:
214
+ """Handle product comparison requests - WhatsApp format"""
215
+ # Extract product names for comparison
216
+ comparison_words = ['ile', 've', 'vs', 'versus', 'karşılaştır', 'arasında']
217
+
218
+ products_to_compare = []
219
+
220
+ # Try to extract two product names
221
+ for word in comparison_words:
222
+ if word in message.lower():
223
+ parts = message.lower().split(word)
224
+ if len(parts) >= 2:
225
+ # Search for each part
226
+ for part in parts[:2]:
227
+ result = self.search_engine.find_best_match(part.strip())
228
+ if result:
229
+ products_to_compare.append(result)
230
+
231
+ if len(products_to_compare) < 2:
232
+ # Try to find products mentioned in the message
233
+ search_results = self.search_engine.search(message)
234
+ products_to_compare = [r[1] for r in search_results[:2] if r[0] > 0.6]
235
+
236
+ if len(products_to_compare) >= 2:
237
+ return self._format_comparison_whatsapp(products_to_compare[:2])
238
+
239
+ return None
240
+
241
+ def _format_comparison_whatsapp(self, products: List[Tuple]) -> str:
242
+ """Format product comparison for WhatsApp"""
243
+ response_parts = ["📊 *Ürün Karşılaştırması*\n"]
244
+
245
+ for i, product in enumerate(products, 1):
246
+ name = product[2]
247
+ info = product[1]
248
+
249
+ response_parts.append(f"*{i}. {name}*")
250
+
251
+ # Stock status
252
+ stock = info[0] if len(info) > 0 else "bilinmiyor"
253
+ if stock == "stokta":
254
+ response_parts.append("✅ Stokta mevcut")
255
+ else:
256
+ response_parts.append("❌ Stokta yok")
257
+
258
+ # Price (compact for WhatsApp)
259
+ if stock == "stokta" and len(info) > 1:
260
+ price = info[1]
261
+ response_parts.append(f"💰 {price} TL")
262
+
263
+ if len(info) > 4 and info[4]:
264
+ response_parts.append(f"🎯 Kampanya: {info[4]} TL")
265
+
266
+ response_parts.append("") # Empty line
267
+
268
+ return "\n".join(response_parts)
269
+
270
+ def process_message(self, message: str) -> Dict:
271
+ """Process user message and return structured response optimized for WhatsApp"""
272
+ result = {
273
+ 'is_product_query': False,
274
+ 'response': None,
275
+ 'products_found': [],
276
+ 'intent': None
277
+ }
278
+
279
+ # Check if it's a product query
280
+ if self.detect_product_query(message):
281
+ result['is_product_query'] = True
282
+ result['intent'] = self.extract_query_intent(message)
283
+
284
+ # Check for comparison request
285
+ if result['intent']['type'] == 'comparison':
286
+ comparison_result = self.handle_comparison_whatsapp(message)
287
+ if comparison_result:
288
+ result['response'] = comparison_result
289
+ return result
290
+
291
+ # Generate product response
292
+ result['response'] = self.generate_product_response(message)
293
+
294
+ # Get found products
295
+ search_results = self.search_engine.search(message)
296
+ result['products_found'] = [r[1] for r in search_results[:2] if r[0] > 0.6] # Limit to 2 for WhatsApp
297
+
298
+ return result
whatsapp_passive_profiler.py ADDED
@@ -0,0 +1,368 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ WhatsApp Pasif Profil Sistemi
4
+ Kullanıcıya soru sormadan sohbet analizi ile profil oluşturur
5
+ """
6
+
7
+ import re
8
+ import json
9
+ import os
10
+ from datetime import datetime, timedelta
11
+ from typing import Dict, List, Optional, Any
12
+
13
+ class WhatsAppPassiveProfiler:
14
+ """Sohbet analizi ile kullanıcı profili çıkarır"""
15
+
16
+ def __init__(self):
17
+ self.profiles_file = "user_profiles.json"
18
+ self.profiles = self.load_profiles()
19
+
20
+ # Bütçe ifadeleri
21
+ self.budget_patterns = [
22
+ r'bütçem?\s*(\d+)[\s-]*(\d+)?\s*k?\s*(bin|bin tl|tl)?',
23
+ r'(\d+)[\s-]*(\d+)?\s*k?\s*(bin|bin tl|tl)?\s*bütçe',
24
+ r'(\d+)[\s-]*(\d+)?\s*k?\s*(bin|bin tl|tl)?\s*arasında',
25
+ r'maksimum\s*(\d+)\s*k?\s*(bin|bin tl|tl)?',
26
+ r'en fazla\s*(\d+)\s*k?\s*(bin|bin tl|tl)?'
27
+ ]
28
+
29
+ # Kategori tercihleri
30
+ self.category_keywords = {
31
+ "dağ_bisikleti": ["dağ", "dag", "offroad", "patika", "doğa", "orman", "marlin", "fuel", "procaliber"],
32
+ "yol_bisikleti": ["yol", "asfalt", "hız", "yarış", "triathlon", "émonda", "madone", "domane"],
33
+ "şehir_bisikleti": ["şehir", "kent", "günlük", "işe gidip gelme", "fx", "ds", "verve"],
34
+ "elektrikli": ["elektrikli", "electric", "e-bike", "ebike", "batarya", "powerfly", "rail"],
35
+ "gravel": ["gravel", "çakıl", "macera", "touring", "checkpoint"]
36
+ }
37
+
38
+ # Kullanım amaçları
39
+ self.usage_patterns = {
40
+ "spor": ["spor", "antrenman", "kondisyon", "fitness", "egzersiz", "yarış"],
41
+ "günlük": ["işe gitmek", "günlük", "şehir içi", "ulaşım", "market", "alışveriş"],
42
+ "hobi": ["hobi", "eğlence", "gezinti", "keyif", "hafta sonu", "macera"],
43
+ "profesyonel": ["profesyonel", "yarış", "müsabaka", "antrenör", "ciddi"]
44
+ }
45
+
46
+ # Boy/beden işaretçileri
47
+ self.size_patterns = {
48
+ "boy": r'boyum\s*(\d+)\s*cm?|(\d+)\s*cm?\s*boy',
49
+ "kilo": r'kilom\s*(\d+)\s*kg?|(\d+)\s*kg?\s*kilo',
50
+ "beden": r'beden[im]?\s*([xsl]+)|([xsl]+)\s*beden'
51
+ }
52
+
53
+ def load_profiles(self) -> Dict:
54
+ """Mevcut profilleri yükle"""
55
+ if os.path.exists(self.profiles_file):
56
+ try:
57
+ with open(self.profiles_file, 'r', encoding='utf-8') as f:
58
+ return json.load(f)
59
+ except Exception as e:
60
+ print(f"Profil yükleme hatası: {e}")
61
+ return {}
62
+
63
+ def save_profiles(self):
64
+ """Profilleri kaydet"""
65
+ try:
66
+ with open(self.profiles_file, 'w', encoding='utf-8') as f:
67
+ json.dump(self.profiles, f, ensure_ascii=False, indent=2, default=str)
68
+ except Exception as e:
69
+ print(f"Profil kaydetme hatası: {e}")
70
+
71
+ def get_or_create_profile(self, phone_number: str) -> Dict:
72
+ """Profil getir veya oluştur"""
73
+ if phone_number not in self.profiles:
74
+ self.profiles[phone_number] = {
75
+ "created_at": datetime.now(),
76
+ "last_updated": datetime.now(),
77
+ "total_messages": 0,
78
+ "preferences": {
79
+ "budget_min": None,
80
+ "budget_max": None,
81
+ "categories": [], # İlgilendiği kategoriler
82
+ "usage_purpose": [], # Kullanım amaçları
83
+ "size_info": {}, # Boy, kilo, beden bilgileri
84
+ "brand_preferences": [], # Marka tercihleri
85
+ "color_preferences": [] # Renk tercihleri
86
+ },
87
+ "behavior": {
88
+ "price_sensitive": False, # Fiyata duyarlı mı
89
+ "tech_interested": False, # Teknik detaylarla ilgilenir mi
90
+ "comparison_lover": False, # Karşılaştırma sever mi
91
+ "quick_decider": False, # Hızlı karar verir mi
92
+ "research_oriented": False # Araştırmacı mı
93
+ },
94
+ "interests": {
95
+ "viewed_products": [], # Baktığı ürünler
96
+ "compared_products": [], # Karşılaştırdığı ürünler
97
+ "favorite_features": [], # İlgilendiği özellikler
98
+ "mentioned_brands": [] # Bahsettiği markalar
99
+ },
100
+ "statistics": {
101
+ "comparison_requests": 0,
102
+ "budget_queries": 0,
103
+ "technical_questions": 0,
104
+ "price_questions": 0,
105
+ "availability_questions": 0
106
+ }
107
+ }
108
+ return self.profiles[phone_number]
109
+
110
+ def analyze_message(self, phone_number: str, message: str) -> Dict:
111
+ """Mesajı analiz et ve profili güncelle"""
112
+ profile = self.get_or_create_profile(phone_number)
113
+ message_lower = message.lower()
114
+
115
+ # Mesaj sayısını artır
116
+ profile["total_messages"] += 1
117
+ profile["last_updated"] = datetime.now()
118
+
119
+ analysis_results = {
120
+ "budget_detected": False,
121
+ "category_detected": False,
122
+ "usage_detected": False,
123
+ "size_detected": False,
124
+ "behavior_indicators": []
125
+ }
126
+
127
+ # Bütçe analizi
128
+ budget_info = self.extract_budget(message_lower)
129
+ if budget_info:
130
+ profile["preferences"]["budget_min"] = budget_info["min"]
131
+ profile["preferences"]["budget_max"] = budget_info["max"]
132
+ profile["statistics"]["budget_queries"] += 1
133
+ analysis_results["budget_detected"] = True
134
+
135
+ # Kategori tercihi analizi
136
+ detected_categories = self.detect_categories(message_lower)
137
+ if detected_categories:
138
+ for category in detected_categories:
139
+ if category not in profile["preferences"]["categories"]:
140
+ profile["preferences"]["categories"].append(category)
141
+ analysis_results["category_detected"] = True
142
+
143
+ # Kullanım amacı analizi
144
+ usage_purposes = self.detect_usage_purpose(message_lower)
145
+ if usage_purposes:
146
+ for purpose in usage_purposes:
147
+ if purpose not in profile["preferences"]["usage_purpose"]:
148
+ profile["preferences"]["usage_purpose"].append(purpose)
149
+ analysis_results["usage_detected"] = True
150
+
151
+ # Boy/beden bilgisi analizi
152
+ size_info = self.extract_size_info(message_lower)
153
+ if size_info:
154
+ profile["preferences"]["size_info"].update(size_info)
155
+ analysis_results["size_detected"] = True
156
+
157
+ # Davranış analizi
158
+ behavior_indicators = self.analyze_behavior(message_lower)
159
+ for behavior, detected in behavior_indicators.items():
160
+ if detected:
161
+ profile["behavior"][behavior] = True
162
+ analysis_results["behavior_indicators"].append(behavior)
163
+
164
+ # İstatistik güncelleme
165
+ self.update_statistics(profile, message_lower)
166
+
167
+ # Profili kaydet
168
+ self.save_profiles()
169
+
170
+ return analysis_results
171
+
172
+ def extract_budget(self, message: str) -> Optional[Dict]:
173
+ """Bütçe bilgisini çıkar"""
174
+ for pattern in self.budget_patterns:
175
+ match = re.search(pattern, message)
176
+ if match:
177
+ numbers = [g for g in match.groups() if g and g.isdigit()]
178
+ if numbers:
179
+ if len(numbers) == 1:
180
+ # Tek sayı - maksimum bütçe
181
+ budget = int(numbers[0])
182
+ if budget < 1000: # K formatında (50k = 50000)
183
+ budget *= 1000
184
+ return {"min": int(budget * 0.7), "max": budget}
185
+ elif len(numbers) >= 2:
186
+ # Aralık - min ve max
187
+ min_budget = int(numbers[0])
188
+ max_budget = int(numbers[1])
189
+ if min_budget < 1000:
190
+ min_budget *= 1000
191
+ if max_budget < 1000:
192
+ max_budget *= 1000
193
+ return {"min": min_budget, "max": max_budget}
194
+ return None
195
+
196
+ def detect_categories(self, message: str) -> List[str]:
197
+ """Kategori tercihlerini tespit et"""
198
+ detected = []
199
+ for category, keywords in self.category_keywords.items():
200
+ if any(keyword in message for keyword in keywords):
201
+ detected.append(category)
202
+ return detected
203
+
204
+ def detect_usage_purpose(self, message: str) -> List[str]:
205
+ """Kullanım amacını tespit et"""
206
+ detected = []
207
+ for purpose, keywords in self.usage_patterns.items():
208
+ if any(keyword in message for keyword in keywords):
209
+ detected.append(purpose)
210
+ return detected
211
+
212
+ def extract_size_info(self, message: str) -> Dict:
213
+ """Boy/beden bilgisini çıkar"""
214
+ size_info = {}
215
+
216
+ # Boy bilgisi
217
+ boy_match = re.search(self.size_patterns["boy"], message)
218
+ if boy_match:
219
+ boy = boy_match.group(1) or boy_match.group(2)
220
+ if boy:
221
+ size_info["height"] = int(boy)
222
+
223
+ # Kilo bilgisi
224
+ kilo_match = re.search(self.size_patterns["kilo"], message)
225
+ if kilo_match:
226
+ kilo = kilo_match.group(1) or kilo_match.group(2)
227
+ if kilo:
228
+ size_info["weight"] = int(kilo)
229
+
230
+ # Beden bilgisi
231
+ beden_match = re.search(self.size_patterns["beden"], message)
232
+ if beden_match:
233
+ beden = beden_match.group(1) or beden_match.group(2)
234
+ if beden:
235
+ size_info["size"] = beden.upper()
236
+
237
+ return size_info
238
+
239
+ def analyze_behavior(self, message: str) -> Dict[str, bool]:
240
+ """Davranış kalıplarını analiz et"""
241
+ behavior = {}
242
+
243
+ # Fiyata duyarlılık
244
+ price_keywords = ["ucuz", "fiyat", "indirim", "kampanya", "ekonomik", "bütçe"]
245
+ behavior["price_sensitive"] = any(keyword in message for keyword in price_keywords)
246
+
247
+ # Teknik ilgi
248
+ tech_keywords = ["teknik", "özellik", "ağırlık", "malzeme", "karbon", "alüminyum", "vites"]
249
+ behavior["tech_interested"] = any(keyword in message for keyword in tech_keywords)
250
+
251
+ # Karşılaştırma sevgisi
252
+ comparison_keywords = ["karşılaştır", "fark", "hangisi", "arası", "seçim"]
253
+ behavior["comparison_lover"] = any(keyword in message for keyword in comparison_keywords)
254
+
255
+ # Araştırmacı yapı
256
+ research_keywords = ["detay", "bilgi", "araştır", "inceleme", "test", "deneyim"]
257
+ behavior["research_oriented"] = any(keyword in message for keyword in research_keywords)
258
+
259
+ return behavior
260
+
261
+ def update_statistics(self, profile: Dict, message: str):
262
+ """İstatistikleri güncelle"""
263
+ if "karşılaştır" in message:
264
+ profile["statistics"]["comparison_requests"] += 1
265
+
266
+ if any(word in message for word in ["fiyat", "kaç para", "ne kadar"]):
267
+ profile["statistics"]["price_questions"] += 1
268
+
269
+ if any(word in message for word in ["stok", "var mı", "mevcut"]):
270
+ profile["statistics"]["availability_questions"] += 1
271
+
272
+ if any(word in message for word in ["teknik", "özellik", "detay"]):
273
+ profile["statistics"]["technical_questions"] += 1
274
+
275
+ def get_profile_summary(self, phone_number: str) -> Dict:
276
+ """Profil özetini döndür"""
277
+ if phone_number not in self.profiles:
278
+ return {"exists": False}
279
+
280
+ profile = self.profiles[phone_number]
281
+
282
+ # Profil güvenilirlik skoru (mesaj sayısına göre)
283
+ confidence = min(profile["total_messages"] / 10.0, 1.0) # 10 mesajda %100
284
+
285
+ summary = {
286
+ "exists": True,
287
+ "confidence": confidence,
288
+ "total_messages": profile["total_messages"],
289
+ "preferences": profile["preferences"],
290
+ "behavior": profile["behavior"],
291
+ "top_categories": profile["preferences"]["categories"][:3],
292
+ "is_budget_defined": profile["preferences"]["budget_min"] is not None,
293
+ "is_tech_savvy": profile["behavior"]["tech_interested"],
294
+ "is_price_conscious": profile["behavior"]["price_sensitive"],
295
+ "interaction_style": self.get_interaction_style(profile)
296
+ }
297
+
298
+ return summary
299
+
300
+ def get_interaction_style(self, profile: Dict) -> str:
301
+ """Etkileşim stilini belirle"""
302
+ stats = profile["statistics"]
303
+ behavior = profile["behavior"]
304
+
305
+ if stats["comparison_requests"] > 2 and behavior["research_oriented"]:
306
+ return "analytical" # Analitik - detaylı bilgi sever
307
+ elif behavior["price_sensitive"] and stats["budget_queries"] > 0:
308
+ return "budget_conscious" # Bütçe odaklı
309
+ elif behavior["tech_interested"] and stats["technical_questions"] > 1:
310
+ return "technical" # Teknik odaklı
311
+ elif stats["comparison_requests"] == 0 and profile["total_messages"] < 5:
312
+ return "decisive" # Hızlı karar verici
313
+ else:
314
+ return "balanced" # Dengeli
315
+
316
+ def get_personalized_suggestions(self, phone_number: str, products_data: List) -> Dict:
317
+ """Kişiselleştirilmiş öneriler"""
318
+ profile_summary = self.get_profile_summary(phone_number)
319
+
320
+ if not profile_summary["exists"] or profile_summary["confidence"] < 0.3:
321
+ return {"personalized": False, "reason": "insufficient_data"}
322
+
323
+ suggestions = {
324
+ "personalized": True,
325
+ "user_style": profile_summary["interaction_style"],
326
+ "budget_aware": profile_summary["is_budget_defined"],
327
+ "recommendations": []
328
+ }
329
+
330
+ # Bütçe filtresi
331
+ filtered_products = products_data
332
+ if profile_summary["preferences"]["budget_min"]:
333
+ budget_min = profile_summary["preferences"]["budget_min"]
334
+ budget_max = profile_summary["preferences"]["budget_max"]
335
+ filtered_products = [
336
+ p for p in products_data
337
+ if p[1][0] == "stokta" and p[1][1] and
338
+ budget_min <= float(p[1][1]) <= budget_max
339
+ ]
340
+
341
+ # Kategori filtresi
342
+ if profile_summary["top_categories"]:
343
+ category_products = []
344
+ for category in profile_summary["top_categories"]:
345
+ category_products.extend([
346
+ p for p in filtered_products
347
+ if any(keyword in p[2].lower() for keyword in self.category_keywords.get(category, []))
348
+ ])
349
+ if category_products:
350
+ filtered_products = category_products
351
+
352
+ suggestions["recommendations"] = filtered_products[:5]
353
+ return suggestions
354
+
355
+ # Global instance
356
+ passive_profiler = WhatsAppPassiveProfiler()
357
+
358
+ def analyze_user_message(phone_number: str, message: str) -> Dict:
359
+ """Kullanıcı mesajını analiz et ve profili güncelle"""
360
+ return passive_profiler.analyze_message(phone_number, message)
361
+
362
+ def get_user_profile_summary(phone_number: str) -> Dict:
363
+ """Kullanıcı profil özetini getir"""
364
+ return passive_profiler.get_profile_summary(phone_number)
365
+
366
+ def get_personalized_recommendations(phone_number: str, products_data: List) -> Dict:
367
+ """Kişiselleştirilmiş öneriler getir"""
368
+ return passive_profiler.get_personalized_suggestions(phone_number, products_data)
whatsapp_renderer.py ADDED
@@ -0,0 +1,222 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ WhatsApp Ürün Resimlerini ve Karşılaştırmalarını Gösterme Sistemi
4
+ WhatsApp için basitleştirilmiş versiyon - Sadece URL paylaşımı
5
+ """
6
+
7
+ def round_price(price_str):
8
+ """Fiyatı yuvarlama formülüne göre yuvarla"""
9
+ try:
10
+ # TL ve diğer karakterleri temizle
11
+ price_clean = price_str.replace(' TL', '').replace(',', '.')
12
+ price_float = float(price_clean)
13
+
14
+ # Fiyat 200000 üzerindeyse en yakın 5000'lik basamağa yuvarla
15
+ if price_float > 200000:
16
+ return str(round(price_float / 5000) * 5000)
17
+ # Fiyat 30000 üzerindeyse en yakın 1000'lik basamağa yuvarla
18
+ elif price_float > 30000:
19
+ return str(round(price_float / 1000) * 1000)
20
+ # Fiyat 10000 üzerindeyse en yakın 100'lük basamağa yuvarla
21
+ elif price_float > 10000:
22
+ return str(round(price_float / 100) * 100)
23
+ # Diğer durumlarda en yakın 10'luk basamağa yuvarla
24
+ else:
25
+ return str(round(price_float / 10) * 10)
26
+ except (ValueError, TypeError):
27
+ return price_str
28
+
29
+ def format_message_with_images_whatsapp(message):
30
+ """WhatsApp için mesajdaki resim URL'lerini formatla"""
31
+ if "Ürün resmi:" not in message:
32
+ return message
33
+
34
+ lines = message.split('\n')
35
+ formatted_lines = []
36
+
37
+ for line in lines:
38
+ if line.startswith("Ürün resmi:"):
39
+ image_url = line.replace("Ürün resmi:", "").strip()
40
+ if image_url:
41
+ # WhatsApp için sadece URL'yi ekle - WhatsApp otomatik önizleme yapar
42
+ formatted_lines.append(f"🖼️ Ürün Resmi: {image_url}")
43
+ else:
44
+ formatted_lines.append(line)
45
+ else:
46
+ formatted_lines.append(line)
47
+
48
+ return '\n'.join(formatted_lines)
49
+
50
+ def create_product_comparison_whatsapp(products_with_info):
51
+ """WhatsApp için ürün karşılaştırması oluştur"""
52
+ if not products_with_info:
53
+ return ""
54
+
55
+ comparison_text = "📊 **ÜRÜN KARŞILAŞTIRMASI**\n\n"
56
+
57
+ for i, product in enumerate(products_with_info, 1):
58
+ name = product.get('name', 'Bilinmeyen')
59
+ price = product.get('price', 'Fiyat yok')
60
+ stock = product.get('stock', 'Stok bilgisi yok')
61
+ product_url = product.get('product_url', '')
62
+ image_url = product.get('image_url', '')
63
+
64
+ comparison_text += f"**{i}. {name}**\n"
65
+ comparison_text += f"💰 Fiyat: {price}\n"
66
+ comparison_text += f"📦 Stok: {stock}\n"
67
+
68
+ if image_url:
69
+ comparison_text += f"🖼️ Resim: {image_url}\n"
70
+
71
+ if product_url:
72
+ comparison_text += f"🔗 Link: {product_url}\n"
73
+
74
+ comparison_text += "\n" + "─" * 30 + "\n\n"
75
+
76
+ return comparison_text
77
+
78
+ def extract_product_info_whatsapp(message):
79
+ """WhatsApp mesajından ürün bilgilerini çıkar ve formatla"""
80
+ # Önce resim URL'lerini formatla
81
+ formatted_message = format_message_with_images_whatsapp(message)
82
+
83
+ # Karşılaştırma veya öneri mesajı ise özel formatla
84
+ if any(keyword in message.lower() for keyword in ["karşılaştır", "öneri", "seçenek", "alternatif", "bütçe"]):
85
+ # Ürün listesi var mı kontrol et
86
+ lines = message.split('\n')
87
+ products = []
88
+ current_product = {}
89
+
90
+ for line in lines:
91
+ line = line.strip()
92
+ if line.startswith('•') and any(keyword in line.lower() for keyword in ['marlin', 'émonda', 'madone', 'domane', 'fuel', 'powerfly', 'fx']):
93
+ # Yeni ürün başladı
94
+ if current_product:
95
+ products.append(current_product)
96
+
97
+ # Ürün adı ve fiyatı parse et
98
+ parts = line.split(' - ')
99
+ name = parts[0].replace('•', '').strip()
100
+ price_raw = parts[1] if len(parts) > 1 else 'Fiyat yok'
101
+
102
+ # Fiyatı yuvarlama formülüne göre yuvarla
103
+ if price_raw != 'Fiyat yok':
104
+ price = round_price(price_raw) + ' TL'
105
+ else:
106
+ price = price_raw
107
+
108
+ current_product = {
109
+ 'name': name,
110
+ 'price': price,
111
+ 'stock': 'stokta',
112
+ 'image_url': '',
113
+ 'product_url': ''
114
+ }
115
+ elif "Ürün resmi:" in line and current_product:
116
+ current_product['image_url'] = line.replace("Ürün resmi:", "").strip()
117
+ elif "Ürün linki:" in line and current_product:
118
+ current_product['product_url'] = line.replace("Ürün linki:", "").strip()
119
+
120
+ # Son ürünü ekle
121
+ if current_product:
122
+ products.append(current_product)
123
+
124
+ if len(products) > 1:
125
+ # Çoklu ürün karşılaştırması
126
+ comparison = create_product_comparison_whatsapp(products)
127
+ # Orijinal mesajdaki resim linklerini temizle
128
+ cleaned_message = message
129
+ for line in message.split('\n'):
130
+ if line.startswith("Ürün resmi:") or line.startswith("Ürün linki:"):
131
+ cleaned_message = cleaned_message.replace(line, "")
132
+
133
+ return cleaned_message.strip() + "\n\n" + comparison
134
+
135
+ return formatted_message
136
+
137
+ def should_use_comparison_format_whatsapp(message):
138
+ """WhatsApp mesajının karşılaştırma formatı kullanması gerekip gerekmediğini kontrol et"""
139
+ comparison_keywords = ["karşılaştır", "öneri", "seçenek", "alternatif", "bütçe", "compare"]
140
+ return any(keyword in message.lower() for keyword in comparison_keywords)
141
+
142
+ def format_whatsapp_product_list(products, title="Ürün Listesi"):
143
+ """WhatsApp için ürün listesini formatla"""
144
+ if not products:
145
+ return f"❌ {title} - Uygun ürün bulunamadı."
146
+
147
+ text = f"🚲 **{title.upper()}**\n\n"
148
+
149
+ for i, product in enumerate(products, 1):
150
+ name, item_info, full_name = product
151
+ price = item_info[1] if len(item_info) > 1 else "Fiyat yok"
152
+ product_link = item_info[2] if len(item_info) > 2 else ""
153
+
154
+ text += f"**{i}. {full_name}**\n"
155
+ text += f"💰 {price} TL\n"
156
+
157
+ # Resim URL'si varsa ekle
158
+ if len(item_info) > 6 and item_info[6]:
159
+ text += f"🖼️ {item_info[6]}\n"
160
+
161
+ if product_link:
162
+ text += f"🔗 {product_link}\n"
163
+
164
+ text += "\n"
165
+
166
+ return text
167
+
168
+ def format_whatsapp_single_product(product):
169
+ """WhatsApp için tek ürün formatla"""
170
+ if not product:
171
+ return "❌ Ürün bulunamadı."
172
+
173
+ name, item_info, full_name = product
174
+
175
+ text = f"🚲 **{full_name}**\n\n"
176
+
177
+ # Stok durumu
178
+ stock_status = item_info[0] if len(item_info) > 0 else "Bilgi yok"
179
+ text += f"📦 Stok: {stock_status}\n"
180
+
181
+ if stock_status == "stokta":
182
+ # Fiyat bilgileri
183
+ if len(item_info) > 1 and item_info[1]:
184
+ text += f"💰 Fiyat: {item_info[1]} TL\n"
185
+
186
+ # EFT fiyatı
187
+ if len(item_info) > 3 and item_info[3]:
188
+ text += f"💳 Havale Fiyatı: {item_info[3]} TL\n"
189
+
190
+ # Kampanyalı fiyat
191
+ if len(item_info) > 4 and item_info[4]:
192
+ text += f"🎯 Kampanyalı Fiyat: {item_info[4]} TL\n"
193
+
194
+ # Resim
195
+ if len(item_info) > 6 and item_info[6]:
196
+ text += f"🖼️ {item_info[6]}\n"
197
+
198
+ # Ürün linki
199
+ if len(item_info) > 2 and item_info[2]:
200
+ text += f"🔗 {item_info[2]}\n"
201
+
202
+ return text
203
+
204
+ def clean_whatsapp_message(message):
205
+ """WhatsApp mesajını temizle ve optimize et"""
206
+ # Gereksiz boşlukları temizle
207
+ lines = [line.strip() for line in message.split('\n') if line.strip()]
208
+
209
+ # Aynı URL'leri tekrar eden satırları kaldır
210
+ seen_urls = set()
211
+ clean_lines = []
212
+
213
+ for line in lines:
214
+ if line.startswith('🖼️') or line.startswith('🔗'):
215
+ url = line.split(' ', 1)[1] if ' ' in line else line
216
+ if url not in seen_urls:
217
+ seen_urls.add(url)
218
+ clean_lines.append(line)
219
+ else:
220
+ clean_lines.append(line)
221
+
222
+ return '\n'.join(clean_lines)