|
import os |
|
import json |
|
import requests |
|
import xml.etree.ElementTree as ET |
|
import warnings |
|
import time |
|
import threading |
|
from concurrent.futures import ThreadPoolExecutor, as_completed |
|
from fastapi import FastAPI, Request |
|
from twilio.rest import Client |
|
from twilio.twiml.messaging_response import MessagingResponse |
|
|
|
|
|
from prompts import get_prompt_content_only |
|
from whatsapp_renderer import extract_product_info_whatsapp |
|
from whatsapp_passive_profiler import ( |
|
analyze_user_message, get_user_profile_summary, get_personalized_recommendations |
|
) |
|
|
|
|
|
try: |
|
from whatsapp_improved_chatbot import WhatsAppImprovedChatbot |
|
USE_IMPROVED_SEARCH = True |
|
except ImportError: |
|
print("Improved WhatsApp chatbot not available, using basic search") |
|
USE_IMPROVED_SEARCH = False |
|
|
|
|
|
import logging |
|
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s') |
|
logger = logging.getLogger(__name__) |
|
|
|
warnings.simplefilter('ignore') |
|
|
|
|
|
API_URL = "https://api.openai.com/v1/chat/completions" |
|
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") |
|
logger.info(f"OpenAI API Key var mı: {'Evet' if OPENAI_API_KEY else 'Hayır'}") |
|
|
|
|
|
TWILIO_ACCOUNT_SID = os.getenv("TWILIO_ACCOUNT_SID") |
|
TWILIO_AUTH_TOKEN = os.getenv("TWILIO_AUTH_TOKEN") |
|
TWILIO_MESSAGING_SERVICE_SID = os.getenv("TWILIO_MESSAGING_SERVICE_SID") |
|
TWILIO_WHATSAPP_NUMBER = "whatsapp:+905332047254" |
|
|
|
logger.info(f"Twilio SID var mı: {'Evet' if TWILIO_ACCOUNT_SID else 'Hayır'}") |
|
logger.info(f"Twilio Auth Token var mı: {'Evet' if TWILIO_AUTH_TOKEN else 'Hayır'}") |
|
logger.info(f"Messaging Service SID var mı: {'Evet' if TWILIO_MESSAGING_SERVICE_SID else 'Hayır'}") |
|
|
|
if not TWILIO_ACCOUNT_SID or not TWILIO_AUTH_TOKEN: |
|
logger.error("❌ Twilio bilgileri eksik!") |
|
twilio_client = None |
|
else: |
|
try: |
|
twilio_client = Client(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN) |
|
logger.info("✅ Twilio client başarıyla oluşturuldu!") |
|
except Exception as e: |
|
logger.error(f"❌ Twilio client hatası: {e}") |
|
twilio_client = None |
|
|
|
|
|
def get_warehouse_stock(product_name): |
|
"""B2B API'den mağaza stok bilgilerini çek - İyileştirilmiş versiyon""" |
|
try: |
|
import re |
|
warehouse_url = 'https://video.trek-turkey.com/bizimhesap-warehouse-xml.php' |
|
response = requests.get(warehouse_url, verify=False, timeout=15) |
|
|
|
if response.status_code != 200: |
|
return None |
|
|
|
root = ET.fromstring(response.content) |
|
|
|
|
|
turkish_map = {'ı': 'i', 'ğ': 'g', 'ü': 'u', 'ş': 's', 'ö': 'o', 'ç': 'c', 'İ': 'i', 'I': 'i'} |
|
|
|
def normalize_turkish(text): |
|
import unicodedata |
|
text = unicodedata.normalize('NFD', text) |
|
text = ''.join(char for char in text if unicodedata.category(char) != 'Mn') |
|
for tr_char, en_char in turkish_map.items(): |
|
text = text.replace(tr_char, en_char) |
|
return text |
|
|
|
|
|
search_name = normalize_turkish(product_name.lower().strip()) |
|
search_name = search_name.replace('(2026)', '').replace('(2025)', '').replace(' gen 3', '').replace(' gen', '').strip() |
|
search_words = search_name.split() |
|
|
|
best_matches = [] |
|
exact_matches = [] |
|
variant_matches = [] |
|
candidates = [] |
|
|
|
|
|
is_size_color_query = (len(search_words) <= 3 and |
|
any(word in ['s', 'm', 'l', 'xl', 'xs', 'small', 'medium', 'large', |
|
'turuncu', 'siyah', 'beyaz', 'mavi', 'kirmizi', 'yesil', |
|
'orange', 'black', 'white', 'blue', 'red', 'green'] |
|
for word in search_words)) |
|
|
|
|
|
if is_size_color_query: |
|
for product in root.findall('Product'): |
|
product_name_elem = product.find('ProductName') |
|
variant_elem = product.find('Variant') |
|
|
|
if product_name_elem is not None and product_name_elem.text: |
|
xml_product_name = product_name_elem.text.strip() |
|
|
|
|
|
if variant_elem is not None and variant_elem.text: |
|
variant_text = normalize_turkish(variant_elem.text.lower().replace('-', ' ')) |
|
|
|
|
|
if all(word in variant_text for word in search_words): |
|
variant_matches.append((product, xml_product_name, variant_text)) |
|
|
|
if variant_matches: |
|
candidates = variant_matches |
|
else: |
|
|
|
is_size_color_query = False |
|
|
|
|
|
if not is_size_color_query or not candidates: |
|
for product in root.findall('Product'): |
|
product_name_elem = product.find('ProductName') |
|
if product_name_elem is not None and product_name_elem.text: |
|
xml_product_name = product_name_elem.text.strip() |
|
normalized_xml = normalize_turkish(xml_product_name.lower()) |
|
normalized_xml = normalized_xml.replace('(2026)', '').replace('(2025)', '').replace(' gen 3', '').replace(' gen', '').strip() |
|
xml_words = normalized_xml.split() |
|
|
|
|
|
if len(search_words) >= 2 and len(xml_words) >= 2: |
|
search_key = f"{search_words[0]} {search_words[1]}" |
|
xml_key = f"{xml_words[0]} {xml_words[1]}" |
|
|
|
if search_key == xml_key: |
|
exact_matches.append((product, xml_product_name, normalized_xml)) |
|
|
|
|
|
if not candidates: |
|
candidates = exact_matches if exact_matches else [] |
|
|
|
|
|
if not candidates: |
|
for product in root.findall('Product'): |
|
product_name_elem = product.find('ProductName') |
|
if product_name_elem is not None and product_name_elem.text: |
|
xml_product_name = product_name_elem.text.strip() |
|
normalized_xml = normalize_turkish(xml_product_name.lower()) |
|
normalized_xml = normalized_xml.replace('(2026)', '').replace('(2025)', '').replace(' gen 3', '').replace(' gen', '').strip() |
|
xml_words = normalized_xml.split() |
|
|
|
|
|
common_words = set(search_words) & set(xml_words) |
|
|
|
|
|
if (len(common_words) >= 2 and |
|
len(search_words) > 0 and len(xml_words) > 0 and |
|
search_words[0] == xml_words[0]): |
|
best_matches.append((product, xml_product_name, normalized_xml, len(common_words))) |
|
|
|
|
|
if best_matches: |
|
max_common = max(match[3] for match in best_matches) |
|
candidates = [(match[0], match[1], match[2]) for match in best_matches if match[3] == max_common] |
|
|
|
|
|
warehouse_stock_map = {} |
|
|
|
for product, xml_name, _ in candidates: |
|
warehouses = product.find('Warehouses') |
|
if warehouses is not None: |
|
for warehouse in warehouses.findall('Warehouse'): |
|
name_elem = warehouse.find('Name') |
|
stock_elem = warehouse.find('Stock') |
|
|
|
if name_elem is not None and stock_elem is not None: |
|
warehouse_name = name_elem.text if name_elem.text else "Bilinmeyen" |
|
try: |
|
stock_count = int(stock_elem.text) if stock_elem.text else 0 |
|
if stock_count > 0: |
|
|
|
if warehouse_name in warehouse_stock_map: |
|
warehouse_stock_map[warehouse_name] += stock_count |
|
else: |
|
warehouse_stock_map[warehouse_name] = stock_count |
|
except (ValueError, TypeError): |
|
pass |
|
|
|
if warehouse_stock_map: |
|
|
|
all_warehouse_info = [] |
|
for warehouse_name, total_stock in warehouse_stock_map.items(): |
|
all_warehouse_info.append(f"{warehouse_name}: {total_stock} adet") |
|
return all_warehouse_info |
|
else: |
|
return ["Hiçbir mağazada stokta bulunmuyor"] |
|
|
|
except Exception as e: |
|
print(f"Mağaza stok bilgisi çekme hatası: {e}") |
|
return None |
|
|
|
|
|
try: |
|
print("🔍 XML DEBUG TEST BAŞLADI") |
|
print("========================") |
|
|
|
url = 'https://www.trekbisiklet.com.tr/output/8582384479' |
|
print(f"📡 URL: {url}") |
|
|
|
print("📥 HTTP isteği gönderiliyor...") |
|
response = requests.get(url, verify=False, timeout=10) |
|
print(f"✅ HTTP Status: {response.status_code}") |
|
print(f"📏 Content Length: {len(response.content)} bytes") |
|
|
|
|
|
content_preview = response.content[:500].decode('utf-8', errors='ignore') |
|
print(f"📄 İlk 500 karakter:") |
|
print(content_preview) |
|
print("...") |
|
|
|
print("\n🔄 XML parsing...") |
|
root = ET.fromstring(response.content) |
|
print(f"✅ XML root tag: {root.tag}") |
|
|
|
all_items = root.findall('item') |
|
print(f"📦 Toplam item sayısı: {len(all_items)}") |
|
|
|
|
|
print(f"\n🔍 İlk 3 item analizi:") |
|
for i, item in enumerate(all_items[:3]): |
|
print(f"\n--- ITEM {i+1} ---") |
|
|
|
rootlabel = item.find('rootlabel') |
|
if rootlabel is not None and rootlabel.text: |
|
print(f"📛 Ürün: {rootlabel.text}") |
|
else: |
|
print("❌ rootlabel bulunamadı") |
|
|
|
stock = item.find('stockAmount') |
|
if stock is not None and stock.text: |
|
print(f"📦 Stok: {stock.text}") |
|
else: |
|
print("❌ stockAmount bulunamadı") |
|
|
|
price = item.find('priceTaxWithCur') |
|
if price is not None and price.text: |
|
print(f"💰 Fiyat: {price.text}") |
|
else: |
|
print("❌ priceTaxWithCur bulunamadı") |
|
|
|
|
|
print(f"\n🎯 MARLIN ARAMA TESİ:") |
|
marlin_count = 0 |
|
products = [] |
|
|
|
for item in all_items: |
|
|
|
stock_number = 0 |
|
stock_amount = "stokta değil" |
|
price = "" |
|
price_eft = "" |
|
product_link = "" |
|
|
|
rootlabel = item.find('rootlabel') |
|
if rootlabel is None or not rootlabel.text: |
|
continue |
|
|
|
full_name = rootlabel.text.strip() |
|
name_words = full_name.lower().split() |
|
name = name_words[0] if name_words else "unknown" |
|
|
|
|
|
stock_element = item.find('stockAmount') |
|
if stock_element is not None and stock_element.text: |
|
try: |
|
stock_number = int(stock_element.text.strip()) |
|
stock_amount = "stokta" if stock_number > 0 else "stokta değil" |
|
except (ValueError, TypeError): |
|
stock_number = 0 |
|
stock_amount = "stokta değil" |
|
|
|
|
|
if 'marlin' in full_name.lower(): |
|
marlin_count += 1 |
|
print(f"🎯 MARLIN BULUNDU #{marlin_count}: {full_name}") |
|
print(f" 📦 Stok: {stock_number} adet") |
|
print(f" 📊 Durum: {stock_amount}") |
|
|
|
|
|
if stock_amount == "stokta": |
|
|
|
price_element = item.find('priceTaxWithCur') |
|
price_str = price_element.text if price_element is not None and price_element.text else "0" |
|
|
|
|
|
price_rebate_element = item.find('priceRebateWithTax') |
|
price_rebate_str = price_rebate_element.text if price_rebate_element is not None and price_rebate_element.text else "" |
|
|
|
|
|
final_price_str = price_str |
|
if price_rebate_str: |
|
try: |
|
normal_price = float(price_str) |
|
rebate_price = float(price_rebate_str) |
|
|
|
if rebate_price < normal_price: |
|
final_price_str = price_rebate_str |
|
except (ValueError, TypeError): |
|
final_price_str = price_str |
|
|
|
|
|
price_eft_element = item.find('priceEft') |
|
price_eft_str = price_eft_element.text if price_eft_element is not None and price_eft_element.text else "" |
|
|
|
|
|
link_element = item.find('productLink') |
|
product_link = link_element.text if link_element is not None and link_element.text else "" |
|
|
|
|
|
try: |
|
price_float = float(final_price_str) |
|
if price_float > 200000: |
|
price = str(round(price_float / 5000) * 5000) |
|
elif price_float > 30000: |
|
price = str(round(price_float / 1000) * 1000) |
|
elif price_float > 10000: |
|
price = str(round(price_float / 100) * 100) |
|
else: |
|
price = str(round(price_float / 10) * 10) |
|
except (ValueError, TypeError): |
|
price = final_price_str |
|
|
|
|
|
if price_eft_str: |
|
try: |
|
price_eft_float = float(price_eft_str) |
|
if price_eft_float > 200000: |
|
price_eft = str(round(price_eft_float / 5000) * 5000) |
|
elif price_eft_float > 30000: |
|
price_eft = str(round(price_eft_float / 1000) * 1000) |
|
elif price_eft_float > 10000: |
|
price_eft = str(round(price_eft_float / 100) * 100) |
|
else: |
|
price_eft = str(round(price_eft_float / 10) * 10) |
|
except (ValueError, TypeError): |
|
price_eft = price_eft_str |
|
else: |
|
try: |
|
price_eft_float = float(price_str) |
|
price_eft = str(round(price_eft_float * 0.975 / 10) * 10) |
|
except: |
|
price_eft = "" |
|
|
|
|
|
item_info = (stock_amount, price, product_link, price_eft, str(stock_number)) |
|
products.append((name, item_info, full_name)) |
|
|
|
print(f"\n📊 SONUÇ:") |
|
print(f" Toplam ürün: {len(all_items)}") |
|
print(f" Marlin ürünü: {marlin_count}") |
|
print(f" Products listesi: {len(products)} ürün") |
|
|
|
|
|
global improved_whatsapp_bot |
|
improved_whatsapp_bot = None |
|
if USE_IMPROVED_SEARCH: |
|
try: |
|
improved_whatsapp_bot = WhatsAppImprovedChatbot(products) |
|
print("✅ BF Space: Improved WhatsApp product search initialized successfully") |
|
except Exception as e: |
|
print(f"❌ BF Space: Failed to initialize improved WhatsApp search: {e}") |
|
USE_IMPROVED_SEARCH = False |
|
improved_whatsapp_bot = None |
|
|
|
|
|
print("✅ Basit sistem aktif - GPT-4 doğal dil anlama") |
|
|
|
if marlin_count == 0: |
|
print("❌ XML'de hiç Marlin ürünü bulunamadı!") |
|
print(" Muhtemelen XML'de sadece aksesuar/yedek parça var.") |
|
else: |
|
print("✅ Marlin ürünleri XML'de mevcut") |
|
|
|
|
|
marlin_products = [p for p in products if 'marlin' in p[2].lower()] |
|
marlin_in_stock = [p for p in marlin_products if p[1][0] == "stokta"] |
|
marlin_out_of_stock = [p for p in marlin_products if p[1][0] == "stokta değil"] |
|
|
|
print(f"\n📊 MARLIN STOK RAPORU:") |
|
print(f" 📦 Toplam Marlin modeli: {len(marlin_products)}") |
|
print(f" ✅ Stokta olan: {len(marlin_in_stock)}") |
|
print(f" ❌ Stokta olmayan: {len(marlin_out_of_stock)}") |
|
|
|
if marlin_in_stock: |
|
print("\n✅ STOKTA OLAN MARLIN MODELLERİ:") |
|
for product in marlin_in_stock: |
|
print(f" • {product[2]} - {product[1][1]} TL ({product[1][4]} adet)") |
|
|
|
if marlin_out_of_stock: |
|
print("\n❌ STOKTA OLMAYAN MARLIN MODELLERİ:") |
|
for product in marlin_out_of_stock: |
|
print(f" • {product[2]}") |
|
|
|
except Exception as e: |
|
print(f"❌ Ürün yükleme hatası: {e}") |
|
import traceback |
|
traceback.print_exc() |
|
products = [] |
|
|
|
|
|
|
|
|
|
|
|
STOCK_API_BASE = "https://video.trek-turkey.com/bizimhesap-proxy.php" |
|
|
|
|
|
stock_cache = {} |
|
CACHE_DURATION = 300 |
|
|
|
def normalize_turkish(text): |
|
"""Türkçe karakterleri normalize et""" |
|
if not text: |
|
return "" |
|
replacements = { |
|
'ı': 'i', 'İ': 'i', 'ş': 's', 'Ş': 's', |
|
'ğ': 'g', 'Ğ': 'g', 'ü': 'u', 'Ü': 'u', |
|
'ö': 'o', 'Ö': 'o', 'ç': 'c', 'Ç': 'c' |
|
} |
|
text = text.lower() |
|
for tr_char, eng_char in replacements.items(): |
|
text = text.replace(tr_char, eng_char) |
|
return text |
|
|
|
def fetch_warehouse_inventory(warehouse, product_name, search_terms): |
|
"""Tek bir mağazanın stok bilgisini al""" |
|
try: |
|
warehouse_id = warehouse['id'] |
|
warehouse_name = warehouse['title'] |
|
|
|
|
|
is_dsw = 'DSW' in warehouse_name or 'ÖN SİPARİŞ' in warehouse_name.upper() |
|
|
|
|
|
inventory_url = f"{STOCK_API_BASE}?action=inventory&warehouse={warehouse_id}&endpoint=inventory/{warehouse_id}" |
|
inventory_response = requests.get(inventory_url, timeout=3, verify=False) |
|
|
|
if inventory_response.status_code != 200: |
|
return None |
|
|
|
inventory_data = inventory_response.json() |
|
|
|
|
|
if 'data' not in inventory_data or 'inventory' not in inventory_data['data']: |
|
return None |
|
|
|
products = inventory_data['data']['inventory'] |
|
|
|
|
|
size_terms = ['xs', 's', 'm', 'ml', 'l', 'xl', 'xxl', '2xl', '3xl', 'small', 'medium', 'large'] |
|
size_numbers = ['44', '46', '48', '50', '52', '54', '56', '58', '60'] |
|
|
|
|
|
has_size_query = False |
|
size_query = None |
|
for term in search_terms: |
|
if term in size_terms or term in size_numbers: |
|
has_size_query = True |
|
size_query = term |
|
break |
|
|
|
|
|
is_only_size_query = len(search_terms) == 1 and has_size_query |
|
|
|
|
|
warehouse_variants = [] |
|
dsw_stock_count = 0 |
|
|
|
for product in products: |
|
product_title = normalize_turkish(product.get('title', '')).lower() |
|
original_title = product.get('title', '') |
|
|
|
|
|
if is_only_size_query: |
|
|
|
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}'): |
|
qty = int(product.get('qty', 0)) |
|
stock = int(product.get('stock', 0)) |
|
actual_stock = max(qty, stock) |
|
|
|
if actual_stock > 0: |
|
if is_dsw: |
|
dsw_stock_count += actual_stock |
|
continue |
|
warehouse_variants.append(f"{original_title}: ✓ Stokta") |
|
else: |
|
|
|
|
|
if has_size_query: |
|
|
|
non_size_terms = [t for t in search_terms if t != size_query] |
|
product_matches = all(term in product_title for term in non_size_terms) |
|
|
|
|
|
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}') |
|
|
|
if product_matches and size_matches: |
|
qty = int(product.get('qty', 0)) |
|
stock = int(product.get('stock', 0)) |
|
actual_stock = max(qty, stock) |
|
|
|
if actual_stock > 0: |
|
if is_dsw: |
|
dsw_stock_count += actual_stock |
|
continue |
|
|
|
|
|
variant_info = original_title |
|
possible_names = [ |
|
product_name.upper(), |
|
product_name.lower(), |
|
product_name.title(), |
|
product_name.upper().replace('I', 'İ'), |
|
product_name.upper().replace('İ', 'I'), |
|
] |
|
|
|
if 'fx sport' in product_name.lower(): |
|
possible_names.extend(['FX Sport AL 3', 'FX SPORT AL 3', 'Fx Sport Al 3']) |
|
|
|
for possible_name in possible_names: |
|
variant_info = variant_info.replace(possible_name, '').strip() |
|
|
|
variant_info = ' '.join(variant_info.split()) |
|
|
|
if variant_info and variant_info != original_title: |
|
warehouse_variants.append(f"{variant_info}: ✓ Stokta") |
|
else: |
|
warehouse_variants.append(f"{original_title}: ✓ Stokta") |
|
else: |
|
|
|
if all(term in product_title for term in search_terms): |
|
qty = int(product.get('qty', 0)) |
|
stock = int(product.get('stock', 0)) |
|
actual_stock = max(qty, stock) |
|
|
|
if actual_stock > 0: |
|
if is_dsw: |
|
dsw_stock_count += actual_stock |
|
continue |
|
|
|
variant_info = original_title |
|
possible_names = [ |
|
product_name.upper(), |
|
product_name.lower(), |
|
product_name.title(), |
|
product_name.upper().replace('I', 'İ'), |
|
product_name.upper().replace('İ', 'I'), |
|
] |
|
|
|
if 'fx sport' in product_name.lower(): |
|
possible_names.extend(['FX Sport AL 3', 'FX SPORT AL 3', 'Fx Sport Al 3']) |
|
|
|
for possible_name in possible_names: |
|
variant_info = variant_info.replace(possible_name, '').strip() |
|
|
|
variant_info = ' '.join(variant_info.split()) |
|
|
|
if variant_info and variant_info != original_title: |
|
warehouse_variants.append(f"{variant_info}: ✓ Stokta") |
|
else: |
|
warehouse_variants.append(f"{original_title}: ✓ Stokta") |
|
|
|
|
|
if warehouse_variants and not is_dsw: |
|
return {'warehouse': warehouse_name, 'variants': warehouse_variants, 'is_dsw': False} |
|
elif dsw_stock_count > 0: |
|
return {'dsw_stock': dsw_stock_count, 'is_dsw': True} |
|
|
|
return None |
|
|
|
except Exception: |
|
return None |
|
|
|
def get_realtime_stock_parallel(product_name): |
|
"""API'den gerçek zamanlı stok bilgisini çek - Paralel versiyon with cache""" |
|
try: |
|
|
|
cache_key = normalize_turkish(product_name).lower() |
|
current_time = time.time() |
|
|
|
if cache_key in stock_cache: |
|
cached_data, cached_time = stock_cache[cache_key] |
|
|
|
if current_time - cached_time < CACHE_DURATION: |
|
logger.info(f"Cache'den döndürülüyor: {product_name}") |
|
return cached_data |
|
|
|
|
|
warehouses_url = f"{STOCK_API_BASE}?action=warehouses&endpoint=warehouses" |
|
warehouses_response = requests.get(warehouses_url, timeout=3, verify=False) |
|
|
|
if warehouses_response.status_code != 200: |
|
logger.error(f"Mağaza listesi alınamadı: {warehouses_response.status_code}") |
|
return None |
|
|
|
warehouses_data = warehouses_response.json() |
|
|
|
|
|
if 'data' not in warehouses_data or 'warehouses' not in warehouses_data['data']: |
|
logger.error("Mağaza verisi bulunamadı") |
|
return None |
|
|
|
warehouses = warehouses_data['data']['warehouses'] |
|
|
|
|
|
search_terms = normalize_turkish(product_name).lower().split() |
|
logger.info(f"Aranan ürün: {product_name} -> {search_terms}") |
|
|
|
stock_info = {} |
|
total_dsw_stock = 0 |
|
total_stock = 0 |
|
|
|
|
|
with ThreadPoolExecutor(max_workers=10) as executor: |
|
|
|
futures = { |
|
executor.submit(fetch_warehouse_inventory, warehouse, product_name, search_terms): warehouse |
|
for warehouse in warehouses |
|
} |
|
|
|
|
|
for future in as_completed(futures): |
|
result = future.result() |
|
if result: |
|
if result.get('is_dsw'): |
|
total_dsw_stock += result.get('dsw_stock', 0) |
|
else: |
|
warehouse_name = result['warehouse'] |
|
stock_info[warehouse_name] = result['variants'] |
|
total_stock += 1 |
|
|
|
|
|
if not stock_info: |
|
|
|
if total_dsw_stock > 0: |
|
result = f"{product_name}: Şu anda mağazalarda stokta yok, ancak yakında gelecek. Ön sipariş verebilirsiniz." |
|
else: |
|
result = f"{product_name}: Şu anda hiçbir mağazada stokta bulunmuyor." |
|
else: |
|
|
|
prompt_lines = [f"{product_name} stok durumu:"] |
|
for warehouse, variants in stock_info.items(): |
|
if isinstance(variants, list): |
|
prompt_lines.append(f"- {warehouse}:") |
|
for variant in variants: |
|
prompt_lines.append(f" • {variant}") |
|
else: |
|
prompt_lines.append(f"- {warehouse}: {variants}") |
|
|
|
|
|
if total_stock > 0: |
|
prompt_lines.append(f"✓ Ürün stokta mevcut") |
|
|
|
result = "\n".join(prompt_lines) |
|
|
|
|
|
stock_cache[cache_key] = (result, current_time) |
|
|
|
return result |
|
|
|
except Exception as e: |
|
logger.error(f"API hatası: {e}") |
|
return None |
|
|
|
def is_stock_query(message): |
|
"""Mesajın stok sorgusu olup olmadığını kontrol et""" |
|
stock_keywords = ['stok', 'stock', 'var mı', 'mevcut mu', 'kaç adet', |
|
'kaç tane', 'bulunuyor mu', 'hangi mağaza', |
|
'nerede var', 'beden', 'numara'] |
|
message_lower = message.lower() |
|
return any(keyword in message_lower for keyword in stock_keywords) |
|
|
|
|
|
def get_system_messages(): |
|
return get_prompt_content_only() |
|
|
|
|
|
|
|
|
|
|
|
|
|
conversation_memory = {} |
|
|
|
def get_conversation_context(phone_number): |
|
"""Kullanıcının sohbet geçmişini getir""" |
|
if phone_number not in conversation_memory: |
|
conversation_memory[phone_number] = { |
|
"messages": [], |
|
"current_category": None, |
|
"last_activity": None |
|
} |
|
return conversation_memory[phone_number] |
|
|
|
def add_to_conversation(phone_number, user_message, ai_response): |
|
"""Sohbet geçmişine ekle""" |
|
import datetime |
|
|
|
context = get_conversation_context(phone_number) |
|
context["last_activity"] = datetime.datetime.now() |
|
|
|
context["messages"].append({ |
|
"user": user_message, |
|
"ai": ai_response, |
|
"timestamp": datetime.datetime.now() |
|
}) |
|
|
|
|
|
if len(context["messages"]) > 10: |
|
context["messages"] = context["messages"][-10:] |
|
|
|
detect_category(phone_number, user_message, ai_response) |
|
|
|
def detect_category(phone_number, user_message, ai_response): |
|
"""Konuşulan kategoriyi tespit et""" |
|
context = get_conversation_context(phone_number) |
|
|
|
categories = { |
|
"marlin": ["marlin", "marlin+"], |
|
"madone": ["madone"], |
|
"emonda": ["emonda", "émonda"], |
|
"domane": ["domane"], |
|
"checkpoint": ["checkpoint"], |
|
"fuel": ["fuel", "fuel ex", "fuel exe"], |
|
"procaliber": ["procaliber"], |
|
"supercaliber": ["supercaliber"], |
|
"fx": ["fx"], |
|
"ds": ["ds", "dual sport"], |
|
"powerfly": ["powerfly"], |
|
"rail": ["rail"], |
|
"verve": ["verve"], |
|
"townie": ["townie"] |
|
} |
|
|
|
user_lower = user_message.lower() |
|
for category, keywords in categories.items(): |
|
for keyword in keywords: |
|
if keyword in user_lower: |
|
context["current_category"] = category |
|
print(f"🎯 Kategori tespit edildi: {category} ({phone_number})") |
|
return category |
|
|
|
return context.get("current_category") |
|
|
|
def build_context_messages(phone_number, current_message): |
|
"""Sohbet geçmişi ile sistem mesajlarını oluştur""" |
|
context = get_conversation_context(phone_number) |
|
system_messages = get_system_messages() |
|
|
|
|
|
if context.get("current_category"): |
|
category_msg = f"Kullanıcı şu anda {context['current_category'].upper()} kategorisi hakkında konuşuyor. Tüm cevaplarını bu kategori bağlamında ver. Kullanıcı yeni bir kategori belirtmediği sürece {context['current_category']} hakkında bilgi vermek istediğini varsay." |
|
system_messages.append({"role": "system", "content": category_msg}) |
|
|
|
|
|
recent_messages = context["messages"][-3:] if context["messages"] else [] |
|
|
|
all_messages = system_messages.copy() |
|
|
|
|
|
for msg in recent_messages: |
|
all_messages.append({"role": "user", "content": msg["user"]}) |
|
all_messages.append({"role": "assistant", "content": msg["ai"]}) |
|
|
|
|
|
all_messages.append({"role": "user", "content": current_message}) |
|
|
|
return all_messages |
|
|
|
def process_whatsapp_message_with_memory(user_message, phone_number): |
|
"""Hafızalı WhatsApp mesaj işleme""" |
|
try: |
|
|
|
profile_analysis = analyze_user_message(phone_number, user_message) |
|
logger.info(f"📊 Profil analizi: {phone_number} -> {profile_analysis}") |
|
|
|
|
|
if any(keyword in user_message.lower() for keyword in ["öneri", "öner", "tavsiye", "ne önerirsin"]): |
|
personalized = get_personalized_recommendations(phone_number, products) |
|
if personalized.get("personalized") and personalized.get("recommendations"): |
|
|
|
profile_summary = get_user_profile_summary(phone_number) |
|
custom_response = create_personalized_response(personalized, profile_summary) |
|
return extract_product_info_whatsapp(custom_response) |
|
|
|
|
|
|
|
|
|
messages = build_context_messages(phone_number, user_message) |
|
|
|
|
|
profile_summary = get_user_profile_summary(phone_number) |
|
if profile_summary.get("exists") and profile_summary.get("confidence", 0) > 0.3: |
|
profile_context = create_profile_context_message(profile_summary) |
|
messages.append({"role": "system", "content": profile_context}) |
|
|
|
|
|
product_found_improved = False |
|
if USE_IMPROVED_SEARCH and improved_whatsapp_bot: |
|
try: |
|
product_result = improved_whatsapp_bot.process_message(user_message) |
|
if product_result['is_product_query'] and product_result['response']: |
|
|
|
if any(keyword in user_message.lower() for keyword in ['mağaza', 'mağazada', 'nerede', 'hangi mağaza', 'şube']): |
|
|
|
|
|
warehouse_info_parts = [] |
|
|
|
|
|
if product_result['response']: |
|
|
|
import re |
|
|
|
product_names = re.findall(r'\*([^*]+)\*', product_result['response']) |
|
|
|
if product_names: |
|
for product_name in product_names[:3]: |
|
|
|
product_name = product_name.strip() |
|
|
|
|
|
import re |
|
product_name = re.sub(r'^\d+\.\s*', '', product_name) |
|
|
|
|
|
if product_name in ['Stokta mevcut', 'Stokta yok', 'Fiyat:', 'Kampanya:', 'İndirim:', 'Birden fazla ürün buldum:']: |
|
continue |
|
|
|
warehouse_stock = get_warehouse_stock(product_name) |
|
|
|
if warehouse_stock and warehouse_stock != ['Ürün bulunamadı'] and warehouse_stock != ['Hiçbir mağazada stokta bulunmuyor']: |
|
warehouse_info_parts.append(f"{product_name} mağaza stogu:") |
|
warehouse_info_parts.extend(warehouse_stock) |
|
warehouse_info_parts.append("") |
|
break |
|
|
|
|
|
if not warehouse_info_parts and product_result['products_found']: |
|
for product in product_result['products_found'][:2]: |
|
product_name = product[2] |
|
warehouse_stock = get_warehouse_stock(product_name) |
|
|
|
if warehouse_stock and warehouse_stock != ['Ürün bulunamadı'] and warehouse_stock != ['Hiçbir mağazada stokta bulunmuyor']: |
|
warehouse_info_parts.append(f"{product_name} mağaza stogu:") |
|
warehouse_info_parts.extend(warehouse_stock) |
|
warehouse_info_parts.append("") |
|
break |
|
|
|
if warehouse_info_parts: |
|
warehouse_response = "\n".join(warehouse_info_parts) |
|
messages.append({ |
|
"role": "system", |
|
"content": f"MAĞAZA STOK BİLGİSİ (BF Space):\n{warehouse_response}\n\nSADECE bu bilgileri kullanarak kullanıcıya yardımcı ol." |
|
}) |
|
product_found_improved = True |
|
logger.info("✅ BF Space: Warehouse stock info used") |
|
|
|
if not product_found_improved: |
|
|
|
messages.append({ |
|
"role": "system", |
|
"content": f"ÜRÜN BİLGİSİ (BF Space):\n{product_result['response']}\n\nSADECE bu bilgileri kullanarak kullanıcıya yardımcı ol. Bu bilgiler dışında ek bilgi ekleme." |
|
}) |
|
product_found_improved = True |
|
logger.info("✅ BF Space: Improved product search used") |
|
except Exception as e: |
|
logger.error(f"❌ BF Space: Improved search error: {e}") |
|
|
|
|
|
if not product_found_improved: |
|
|
|
input_words = user_message.lower().split() |
|
for product_info in products: |
|
if product_info[0] in input_words: |
|
if product_info[1][0] == "stokta": |
|
normal_price = f"Fiyat: {product_info[1][1]} TL" |
|
if product_info[1][3]: |
|
eft_price = f"Havale: {product_info[1][3]} TL" |
|
price_info = f"{normal_price}, {eft_price}" |
|
else: |
|
price_info = normal_price |
|
|
|
new_msg = f"{product_info[2]} {product_info[1][0]} - {price_info}" |
|
else: |
|
new_msg = f"{product_info[2]} {product_info[1][0]}" |
|
messages.append({"role": "system", "content": new_msg}) |
|
break |
|
|
|
if not OPENAI_API_KEY: |
|
return "OpenAI API anahtarı eksik. Lütfen environment variables'ları kontrol edin." |
|
|
|
payload = { |
|
"model": "gpt-5-chat-latest", |
|
"messages": messages, |
|
"temperature": 0, |
|
"max_tokens": 800, |
|
"stream": False, |
|
} |
|
|
|
headers = { |
|
"Content-Type": "application/json", |
|
"Authorization": f"Bearer {OPENAI_API_KEY}" |
|
} |
|
|
|
response = requests.post(API_URL, headers=headers, json=payload) |
|
if response.status_code == 200: |
|
result = response.json() |
|
ai_response = result['choices'][0]['message']['content'] |
|
|
|
|
|
formatted_response = extract_product_info_whatsapp(ai_response) |
|
|
|
|
|
add_to_conversation(phone_number, user_message, formatted_response) |
|
|
|
return formatted_response |
|
else: |
|
print(f"OpenAI API Error: {response.status_code} - {response.text}") |
|
return f"API hatası: {response.status_code}. Lütfen daha sonra tekrar deneyin." |
|
|
|
except Exception as e: |
|
print(f"❌ WhatsApp mesaj işleme hatası: {e}") |
|
return "Teknik bir sorun oluştu. Lütfen daha sonra tekrar deneyin." |
|
|
|
def create_profile_context_message(profile_summary): |
|
"""Profil bilgilerini sistem mesajına çevir""" |
|
context_parts = [] |
|
|
|
preferences = profile_summary.get("preferences", {}) |
|
behavior = profile_summary.get("behavior", {}) |
|
|
|
|
|
if preferences.get("budget_min") and preferences.get("budget_max"): |
|
budget_min = preferences["budget_min"] |
|
budget_max = preferences["budget_max"] |
|
context_parts.append(f"Kullanıcının bütçesi: {budget_min:,}-{budget_max:,} TL") |
|
|
|
|
|
if preferences.get("categories"): |
|
categories = ", ".join(preferences["categories"]) |
|
context_parts.append(f"İlgilendiği kategoriler: {categories}") |
|
|
|
|
|
if preferences.get("usage_purpose"): |
|
purposes = ", ".join(preferences["usage_purpose"]) |
|
context_parts.append(f"Kullanım amacı: {purposes}") |
|
|
|
|
|
if behavior.get("price_sensitive"): |
|
context_parts.append("Fiyata duyarlı bir kullanıcı") |
|
if behavior.get("tech_interested"): |
|
context_parts.append("Teknik detaylarla ilgilenen bir kullanıcı") |
|
if behavior.get("comparison_lover"): |
|
context_parts.append("Karşılaştırma yapmayı seven bir kullanıcı") |
|
|
|
|
|
interaction_style = profile_summary.get("interaction_style", "balanced") |
|
style_descriptions = { |
|
"analytical": "Detaylı ve analitik bilgi bekleyen", |
|
"budget_conscious": "Bütçe odaklı ve ekonomik seçenekleri arayan", |
|
"technical": "Teknik özellikler ve spesifikasyonlarla ilgilenen", |
|
"decisive": "Hızlı karar veren ve özet bilgi isteyen", |
|
"balanced": "Dengeli yaklaşım sergileyen" |
|
} |
|
context_parts.append(f"{style_descriptions.get(interaction_style, 'balanced')} bir kullanıcı") |
|
|
|
if context_parts: |
|
return f"Kullanıcı profili: {'. '.join(context_parts)}. Bu bilgileri göz önünde bulundurarak cevap ver." |
|
return "" |
|
|
|
def create_personalized_response(personalized_data, profile_summary): |
|
"""Kişiselleştirilmiş öneri cevabı oluştur""" |
|
response_parts = [] |
|
|
|
|
|
interaction_style = profile_summary.get("interaction_style", "balanced") |
|
if interaction_style == "analytical": |
|
response_parts.append("🔍 Profilinizi analiz ederek sizin için en uygun seçenekleri belirledim:") |
|
elif interaction_style == "budget_conscious": |
|
response_parts.append("💰 Bütçenize uygun en iyi seçenekleri hazırladım:") |
|
elif interaction_style == "technical": |
|
response_parts.append("⚙️ Teknik tercihlerinize göre önerilerim:") |
|
else: |
|
response_parts.append("🎯 Size özel seçtiklerim:") |
|
|
|
|
|
recommendations = personalized_data.get("recommendations", [])[:3] |
|
|
|
if recommendations: |
|
response_parts.append("\n") |
|
for i, product in enumerate(recommendations, 1): |
|
name, item_info, full_name = product |
|
price = item_info[1] if len(item_info) > 1 else "Fiyat yok" |
|
response_parts.append(f"**{i}. {full_name}**") |
|
response_parts.append(f"💰 Fiyat: {price} TL") |
|
response_parts.append("") |
|
|
|
|
|
preferences = profile_summary.get("preferences", {}) |
|
if preferences.get("categories"): |
|
category = preferences["categories"][0] |
|
response_parts.append(f"Bu öneriler {category} kategorisindeki ilginizi ve tercihlerinizi dikkate alarak hazırlandı.") |
|
|
|
return "\n".join(response_parts) |
|
|
|
def split_long_message(message, max_length=1600): |
|
"""Uzun mesajları WhatsApp için uygun parçalara böler""" |
|
if len(message) <= max_length: |
|
return [message] |
|
|
|
parts = [] |
|
remaining = message |
|
|
|
while len(remaining) > max_length: |
|
cut_point = max_length |
|
|
|
|
|
for i in range(max_length, max_length - 200, -1): |
|
if i < len(remaining) and remaining[i] in ['.', '!', '?', '\n']: |
|
cut_point = i + 1 |
|
break |
|
elif i < len(remaining) and remaining[i] in [' ', ',', ';']: |
|
cut_point = i |
|
|
|
parts.append(remaining[:cut_point].strip()) |
|
remaining = remaining[cut_point:].strip() |
|
|
|
if remaining: |
|
parts.append(remaining) |
|
|
|
return parts |
|
|
|
|
|
|
|
|
|
|
|
|
|
def process_whatsapp_message(user_message): |
|
try: |
|
system_messages = get_system_messages() |
|
|
|
|
|
product_found_improved = False |
|
if USE_IMPROVED_SEARCH and improved_whatsapp_bot: |
|
try: |
|
product_result = improved_whatsapp_bot.process_message(user_message) |
|
if product_result['is_product_query'] and product_result['response']: |
|
|
|
if any(keyword in user_message.lower() for keyword in ['mağaza', 'mağazada', 'nerede', 'hangi mağaza', 'şube']): |
|
|
|
if product_result['products_found']: |
|
warehouse_info_parts = [] |
|
for product in product_result['products_found'][:2]: |
|
product_name = product[2] |
|
warehouse_stock = get_warehouse_stock(product_name) |
|
if warehouse_stock: |
|
warehouse_info_parts.append(f"{product_name} mağaza stogu:") |
|
warehouse_info_parts.extend(warehouse_stock) |
|
warehouse_info_parts.append("") |
|
|
|
if warehouse_info_parts: |
|
warehouse_response = "\n".join(warehouse_info_parts) |
|
system_messages.append({ |
|
"role": "system", |
|
"content": f"MAĞAZA STOK BİLGİSİ (BF Space Backup):\n{warehouse_response}\n\nSADECE bu bilgileri kullanarak kullanıcıya yardımcı ol." |
|
}) |
|
product_found_improved = True |
|
|
|
if not product_found_improved: |
|
system_messages.append({ |
|
"role": "system", |
|
"content": f"ÜRÜN BİLGİSİ (BF Space Backup):\n{product_result['response']}\n\nSADECE bu bilgileri kullanarak kullanıcıya yardımcı ol. Bu bilgiler dışında ek bilgi ekleme." |
|
}) |
|
product_found_improved = True |
|
except Exception as e: |
|
print(f"BF Space backup: Improved search error: {e}") |
|
|
|
|
|
if not product_found_improved: |
|
|
|
input_words = user_message.lower().split() |
|
for product_info in products: |
|
if product_info[0] in input_words: |
|
if product_info[1][0] == "stokta": |
|
normal_price = f"Fiyat: {product_info[1][1]} TL" |
|
if product_info[1][3]: |
|
eft_price = f"Havale: {product_info[1][3]} TL" |
|
price_info = f"{normal_price}, {eft_price}" |
|
else: |
|
price_info = normal_price |
|
|
|
new_msg = f"{product_info[2]} {product_info[1][0]} - {price_info}" |
|
else: |
|
new_msg = f"{product_info[2]} {product_info[1][0]}" |
|
system_messages.append({"role": "system", "content": new_msg}) |
|
break |
|
|
|
messages = system_messages + [{"role": "user", "content": user_message}] |
|
|
|
if not OPENAI_API_KEY: |
|
return "OpenAI API anahtarı eksik. Lütfen environment variables'ları kontrol edin." |
|
|
|
payload = { |
|
"model": "gpt-5-chat-latest", |
|
"messages": messages, |
|
"temperature": 0, |
|
"max_tokens": 800, |
|
"stream": False, |
|
} |
|
|
|
headers = { |
|
"Content-Type": "application/json", |
|
"Authorization": f"Bearer {OPENAI_API_KEY}" |
|
} |
|
|
|
response = requests.post(API_URL, headers=headers, json=payload) |
|
if response.status_code == 200: |
|
result = response.json() |
|
return result['choices'][0]['message']['content'] |
|
else: |
|
print(f"OpenAI API Error: {response.status_code} - {response.text}") |
|
return f"API hatası: {response.status_code}. Lütfen daha sonra tekrar deneyin." |
|
|
|
except Exception as e: |
|
print(f"❌ WhatsApp mesaj işleme hatası: {e}") |
|
return "Teknik bir sorun oluştu. Lütfen daha sonra tekrar deneyin." |
|
|
|
|
|
app = FastAPI() |
|
|
|
@app.post("/whatsapp-webhook") |
|
async def whatsapp_webhook(request: Request): |
|
try: |
|
form_data = await request.form() |
|
|
|
from_number = form_data.get('From') |
|
to_number = form_data.get('To') |
|
message_body = form_data.get('Body') |
|
message_status = form_data.get('MessageStatus') |
|
|
|
print(f"📱 Webhook - From: {from_number}, Body: {message_body}, Status: {message_status}") |
|
|
|
|
|
if message_status in ['sent', 'delivered', 'read', 'failed']: |
|
return {"status": "ignored", "message": f"Status: {message_status}"} |
|
|
|
|
|
if to_number != TWILIO_WHATSAPP_NUMBER: |
|
return {"status": "ignored", "message": "Outgoing message"} |
|
|
|
|
|
if not message_body or message_body.strip() == "": |
|
return {"status": "ignored", "message": "Empty message"} |
|
|
|
print(f"✅ MESAJ ALINDI: {from_number} -> {message_body}") |
|
|
|
if not twilio_client: |
|
return {"status": "error", "message": "Twilio yapılandırması eksik"} |
|
|
|
|
|
ai_response = process_whatsapp_message_with_memory(message_body, from_number) |
|
|
|
|
|
message_parts = split_long_message(ai_response, max_length=1600) |
|
|
|
sent_messages = [] |
|
|
|
|
|
for i, part in enumerate(message_parts): |
|
if len(message_parts) > 1: |
|
if i == 0: |
|
part = f"{part}\n\n(1/{len(message_parts)})" |
|
elif i == len(message_parts) - 1: |
|
part = f"({i+1}/{len(message_parts)})\n\n{part}" |
|
else: |
|
part = f"({i+1}/{len(message_parts)})\n\n{part}" |
|
|
|
|
|
message = twilio_client.messages.create( |
|
messaging_service_sid=TWILIO_MESSAGING_SERVICE_SID, |
|
body=part, |
|
to=from_number |
|
) |
|
|
|
sent_messages.append(message.sid) |
|
|
|
if i < len(message_parts) - 1: |
|
import time |
|
time.sleep(0.5) |
|
|
|
print(f"✅ {len(message_parts)} PARÇA GÖNDERİLDİ") |
|
|
|
|
|
context = get_conversation_context(from_number) |
|
if context.get("current_category"): |
|
print(f"💭 Aktif kategori: {context['current_category']}") |
|
|
|
return { |
|
"status": "success", |
|
"message_parts": len(message_parts), |
|
"message_sids": sent_messages, |
|
"current_category": context.get("current_category") |
|
} |
|
|
|
except Exception as e: |
|
print(f"❌ Webhook hatası: {str(e)}") |
|
return {"status": "error", "message": str(e)} |
|
|
|
@app.get("/") |
|
async def root(): |
|
return {"message": "Trek WhatsApp Bot çalışıyor!", "status": "active"} |
|
|
|
|
|
@app.get("/clear-memory/{phone_number}") |
|
async def clear_memory(phone_number: str): |
|
"""Belirli bir telefon numarasının hafızasını temizle""" |
|
if phone_number in conversation_memory: |
|
del conversation_memory[phone_number] |
|
return {"status": "success", "message": f"{phone_number} hafızası temizlendi"} |
|
return {"status": "info", "message": "Hafıza bulunamadı"} |
|
|
|
|
|
@app.get("/debug-memory") |
|
async def debug_memory(): |
|
"""Tüm hafızayı görüntüle (debug için)""" |
|
memory_info = {} |
|
for phone, context in conversation_memory.items(): |
|
memory_info[phone] = { |
|
"current_category": context.get("current_category"), |
|
"message_count": len(context.get("messages", [])), |
|
"last_activity": str(context.get("last_activity")) |
|
} |
|
return {"conversation_memory": memory_info} |
|
|
|
|
|
@app.get("/debug-profile/{phone_number}") |
|
async def debug_profile(phone_number: str): |
|
"""Belirli kullanıcının profil bilgilerini görüntüle""" |
|
profile_summary = get_user_profile_summary(phone_number) |
|
return {"phone_number": phone_number, "profile": profile_summary} |
|
|
|
|
|
@app.get("/debug-profiles") |
|
async def debug_profiles(): |
|
"""Tüm kullanıcı profillerini görüntüle""" |
|
from whatsapp_passive_profiler import passive_profiler |
|
all_profiles = {} |
|
for phone_number in passive_profiler.profiles.keys(): |
|
all_profiles[phone_number] = get_user_profile_summary(phone_number) |
|
return {"profiles": all_profiles} |
|
|
|
@app.get("/health") |
|
async def health(): |
|
return { |
|
"status": "healthy", |
|
"twilio_configured": twilio_client is not None, |
|
"openai_configured": OPENAI_API_KEY is not None, |
|
"products_loaded": len(products), |
|
"webhook_endpoint": "/whatsapp-webhook" |
|
} |
|
|
|
if __name__ == "__main__": |
|
import uvicorn |
|
print("🚀 Trek WhatsApp Bot başlatılıyor...") |
|
uvicorn.run(app, host="0.0.0.0", port=7860) |