SamiKoen commited on
Commit ·
179b8e3
1
Parent(s): 607111a
Update smart_warehouse_with_price.py - sync with WhatsApp version for better product matching
Browse files- smart_warehouse_with_price.py +333 -51
smart_warehouse_with_price.py
CHANGED
|
@@ -7,8 +7,8 @@ import json
|
|
| 7 |
import xml.etree.ElementTree as ET
|
| 8 |
import time
|
| 9 |
|
| 10 |
-
# Cache configuration - 12 hours
|
| 11 |
-
CACHE_DURATION =
|
| 12 |
cache = {
|
| 13 |
'warehouse_xml': {'data': None, 'time': 0},
|
| 14 |
'trek_xml': {'data': None, 'time': 0},
|
|
@@ -21,10 +21,9 @@ def get_cached_trek_xml():
|
|
| 21 |
current_time = time.time()
|
| 22 |
|
| 23 |
if cache['trek_xml']['data'] and (current_time - cache['trek_xml']['time'] < CACHE_DURATION):
|
| 24 |
-
|
| 25 |
return cache['trek_xml']['data']
|
| 26 |
|
| 27 |
-
print("📡 Fetching fresh Trek XML...")
|
| 28 |
try:
|
| 29 |
url = 'https://www.trekbisiklet.com.tr/output/8582384479'
|
| 30 |
response = requests.get(url, verify=False, timeout=10)
|
|
@@ -36,9 +35,139 @@ def get_cached_trek_xml():
|
|
| 36 |
else:
|
| 37 |
return None
|
| 38 |
except Exception as e:
|
| 39 |
-
print(f"Trek XML fetch error: {e}")
|
| 40 |
return None
|
| 41 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
def get_product_price_and_link(product_name, variant=None):
|
| 43 |
"""Get price and link from Trek website XML"""
|
| 44 |
try:
|
|
@@ -49,19 +178,33 @@ def get_product_price_and_link(product_name, variant=None):
|
|
| 49 |
|
| 50 |
root = ET.fromstring(xml_content)
|
| 51 |
|
| 52 |
-
#
|
| 53 |
-
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
|
| 56 |
-
#
|
| 57 |
-
|
|
|
|
| 58 |
for tr, en in tr_map.items():
|
| 59 |
-
|
| 60 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
|
| 62 |
best_match = None
|
| 63 |
best_score = 0
|
| 64 |
|
|
|
|
|
|
|
|
|
|
| 65 |
for item in root.findall('item'):
|
| 66 |
# Get product name
|
| 67 |
rootlabel_elem = item.find('rootlabel')
|
|
@@ -72,12 +215,24 @@ def get_product_price_and_link(product_name, variant=None):
|
|
| 72 |
for tr, en in tr_map.items():
|
| 73 |
item_name = item_name.replace(tr, en)
|
| 74 |
|
| 75 |
-
#
|
|
|
|
|
|
|
|
|
|
| 76 |
score = 0
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
|
| 82 |
# Check variant if specified
|
| 83 |
if variant and search_variant in item_name:
|
|
@@ -120,7 +275,6 @@ def get_product_price_and_link(product_name, variant=None):
|
|
| 120 |
return None, None
|
| 121 |
|
| 122 |
except Exception as e:
|
| 123 |
-
print(f"Error getting price/link: {e}")
|
| 124 |
return None, None
|
| 125 |
|
| 126 |
def get_cached_warehouse_xml():
|
|
@@ -128,28 +282,23 @@ def get_cached_warehouse_xml():
|
|
| 128 |
current_time = time.time()
|
| 129 |
|
| 130 |
if cache['warehouse_xml']['data'] and (current_time - cache['warehouse_xml']['time'] < CACHE_DURATION):
|
| 131 |
-
|
| 132 |
return cache['warehouse_xml']['data']
|
| 133 |
|
| 134 |
-
print("📡 Fetching fresh warehouse XML...")
|
| 135 |
for attempt in range(3):
|
| 136 |
try:
|
| 137 |
url = 'https://video.trek-turkey.com/bizimhesap-warehouse-xml-b2b-api-v2.php'
|
| 138 |
timeout_val = 10 + (attempt * 5)
|
| 139 |
response = requests.get(url, verify=False, timeout=timeout_val)
|
| 140 |
xml_text = response.text
|
| 141 |
-
print(f"DEBUG - XML fetched: {len(xml_text)} characters (attempt {attempt+1})")
|
| 142 |
-
|
| 143 |
cache['warehouse_xml']['data'] = xml_text
|
| 144 |
cache['warehouse_xml']['time'] = current_time
|
| 145 |
|
| 146 |
return xml_text
|
| 147 |
except requests.exceptions.Timeout:
|
| 148 |
-
print(f"XML fetch timeout (attempt {attempt+1}/3, timeout={timeout_val}s)")
|
| 149 |
if attempt == 2:
|
| 150 |
return None
|
| 151 |
except Exception as e:
|
| 152 |
-
print(f"XML fetch error: {e}")
|
| 153 |
return None
|
| 154 |
|
| 155 |
return None
|
|
@@ -180,14 +329,24 @@ def get_warehouse_stock_smart_with_price(user_message, previous_result=None):
|
|
| 180 |
'kesinlikle', 'elbette', 'tabii', 'tabiki', 'doğru', 'yanlış'
|
| 181 |
]
|
| 182 |
|
|
|
|
| 183 |
if clean_message in non_product_words:
|
| 184 |
return None
|
| 185 |
-
|
| 186 |
-
#
|
| 187 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
return None
|
| 189 |
|
| 190 |
-
#
|
|
|
|
| 191 |
question_indicators = [
|
| 192 |
'musun', 'müsün', 'misin', 'mısın', 'miyim', 'mıyım',
|
| 193 |
'musunuz', 'müsünüz', 'misiniz', 'mısınız',
|
|
@@ -195,31 +354,48 @@ def get_warehouse_stock_smart_with_price(user_message, previous_result=None):
|
|
| 195 |
'ulaşamıyor', 'yapamıyor', 'gönderemiyor', 'edemiyor',
|
| 196 |
'?'
|
| 197 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
|
| 199 |
-
for
|
| 200 |
-
|
| 201 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 202 |
|
| 203 |
# Check search cache first
|
| 204 |
-
cache_key =
|
| 205 |
current_time = time.time()
|
|
|
|
| 206 |
if cache_key in cache['search_results']:
|
| 207 |
cached = cache['search_results'][cache_key]
|
| 208 |
if current_time - cached['time'] < CACHE_DURATION:
|
| 209 |
-
|
| 210 |
return cached['data']
|
|
|
|
|
|
|
| 211 |
|
| 212 |
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
| 213 |
|
| 214 |
# Check if user is asking about specific warehouse
|
| 215 |
warehouse_keywords = {
|
| 216 |
'caddebostan': 'Caddebostan',
|
| 217 |
-
'ortaköy': 'Ortaköy',
|
| 218 |
'ortakoy': 'Ortaköy',
|
| 219 |
'alsancak': 'Alsancak',
|
| 220 |
'izmir': 'Alsancak',
|
| 221 |
'bahçeköy': 'Bahçeköy',
|
| 222 |
-
'bahcekoy': 'Bahçeköy'
|
|
|
|
|
|
|
| 223 |
}
|
| 224 |
|
| 225 |
user_lower = user_message.lower()
|
|
@@ -269,7 +445,35 @@ def get_warehouse_stock_smart_with_price(user_message, previous_result=None):
|
|
| 269 |
if asked_warehouse:
|
| 270 |
warehouse_filter = f"\nIMPORTANT: User is asking specifically about {asked_warehouse} warehouse. Only return products available in that warehouse."
|
| 271 |
|
| 272 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 273 |
smart_prompt = f"""User is asking: "{user_message}"
|
| 274 |
|
| 275 |
FIRST CHECK: Is this actually a product search?
|
|
@@ -309,19 +513,42 @@ Examples of correct responses:
|
|
| 309 |
- "45" (single product found)
|
| 310 |
- "-1" (no products found)"""
|
| 311 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 312 |
headers = {
|
| 313 |
"Content-Type": "application/json",
|
| 314 |
"Authorization": f"Bearer {OPENAI_API_KEY}"
|
| 315 |
}
|
| 316 |
|
|
|
|
| 317 |
payload = {
|
| 318 |
"model": "gpt-5.2-chat-latest",
|
| 319 |
"messages": [
|
| 320 |
{"role": "system", "content": "You are a product matcher. Find ALL matching products. Return only index numbers."},
|
| 321 |
{"role": "user", "content": smart_prompt}
|
| 322 |
-
]
|
| 323 |
-
|
| 324 |
-
"max_tokens": 100
|
| 325 |
}
|
| 326 |
|
| 327 |
try:
|
|
@@ -336,11 +563,31 @@ Examples of correct responses:
|
|
| 336 |
result = response.json()
|
| 337 |
indices_str = result['choices'][0]['message']['content'].strip()
|
| 338 |
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
# Handle empty response
|
| 342 |
if not indices_str or indices_str == "-1":
|
| 343 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 344 |
|
| 345 |
try:
|
| 346 |
# Filter out empty strings and parse indices
|
|
@@ -361,13 +608,33 @@ Examples of correct responses:
|
|
| 361 |
# Get product details
|
| 362 |
name_match = re.search(r'<ProductName><!\[CDATA\[(.*?)\]\]></ProductName>', product_block)
|
| 363 |
variant_match = re.search(r'<ProductVariant><!\[CDATA\[(.*?)\]\]></ProductVariant>', product_block)
|
|
|
|
| 364 |
|
| 365 |
if name_match:
|
| 366 |
product_name = name_match.group(1)
|
| 367 |
variant = variant_match.group(1) if variant_match else ""
|
| 368 |
|
| 369 |
-
# Get price and link from Trek website
|
| 370 |
-
price, link =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 371 |
|
| 372 |
variant_info = {
|
| 373 |
'name': product_name,
|
|
@@ -451,26 +718,41 @@ Examples of correct responses:
|
|
| 451 |
result.append(f"• {v['variant']}: {warehouses_str}")
|
| 452 |
|
| 453 |
else:
|
| 454 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 455 |
|
| 456 |
# Cache the result before returning
|
| 457 |
cache['search_results'][cache_key] = {
|
| 458 |
'data': result,
|
| 459 |
'time': current_time
|
| 460 |
}
|
| 461 |
-
print(f"💾 Cached result for '{user_message}' (12-hour cache)")
|
| 462 |
-
|
| 463 |
return result
|
| 464 |
|
| 465 |
except (ValueError, IndexError) as e:
|
| 466 |
-
print(f"DEBUG - Error parsing indices: {e}")
|
| 467 |
return None
|
| 468 |
else:
|
| 469 |
-
print(f"GPT API error: {response.status_code}")
|
| 470 |
return None
|
| 471 |
|
| 472 |
except Exception as e:
|
| 473 |
-
print(f"Error calling GPT: {e}")
|
| 474 |
return None
|
| 475 |
|
| 476 |
def format_warehouse_name(wh_name):
|
|
|
|
| 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},
|
|
|
|
| 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)
|
|
|
|
| 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:
|
|
|
|
| 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')
|
|
|
|
| 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:
|
|
|
|
| 275 |
return None, None
|
| 276 |
|
| 277 |
except Exception as e:
|
|
|
|
| 278 |
return None, None
|
| 279 |
|
| 280 |
def get_cached_warehouse_xml():
|
|
|
|
| 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
|
|
|
|
| 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',
|
|
|
|
| 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()
|
|
|
|
| 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?
|
|
|
|
| 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:
|
|
|
|
| 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
|
|
|
|
| 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,
|
|
|
|
| 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):
|