|
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 |
|
) |
|
|
|
|
|
import logging |
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') |
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
|
|
USE_IMPROVED_SEARCH = False |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try: |
|
from smart_warehouse_with_price import get_warehouse_stock_smart_with_price |
|
USE_GPT5_SEARCH = True |
|
logger.info("✅ GPT-5 complete smart warehouse with price (BF algorithm) loaded") |
|
except ImportError: |
|
USE_GPT5_SEARCH = False |
|
logger.info("❌ GPT-5 search not available") |
|
|
|
warnings.simplefilter('ignore') |
|
|
|
|
|
try: |
|
from media_queue_v2 import media_queue |
|
USE_MEDIA_QUEUE = True |
|
logger.info("✅ Media Queue V2 loaded successfully") |
|
except ImportError: |
|
USE_MEDIA_QUEUE = False |
|
logger.info("❌ Media Queue V2 not available") |
|
|
|
|
|
try: |
|
from store_notification import ( |
|
notify_product_reservation, |
|
notify_price_inquiry, |
|
notify_stock_inquiry, |
|
send_test_notification, |
|
send_store_notification, |
|
should_notify_mehmet_bey |
|
) |
|
USE_STORE_NOTIFICATION = True |
|
logger.info("✅ Store Notification System loaded") |
|
except ImportError: |
|
USE_STORE_NOTIFICATION = False |
|
logger.info("❌ Store Notification System not available") |
|
|
|
|
|
try: |
|
from intent_analyzer import ( |
|
analyze_customer_intent, |
|
should_notify_store, |
|
get_smart_notification_message |
|
) |
|
USE_INTENT_ANALYZER = True |
|
logger.info("✅ GPT-5 Intent Analyzer loaded") |
|
except ImportError: |
|
USE_INTENT_ANALYZER = False |
|
logger.info("❌ Intent Analyzer not available") |
|
|
|
|
|
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 - GPT-5 enhanced""" |
|
|
|
if USE_GPT5_SEARCH: |
|
try: |
|
gpt5_result = get_warehouse_stock_smart_with_price(product_name) |
|
if gpt5_result and isinstance(gpt5_result, list): |
|
|
|
if all(isinstance(item, str) for item in gpt5_result): |
|
return gpt5_result |
|
|
|
warehouse_info = [] |
|
for item in gpt5_result: |
|
if isinstance(item, dict): |
|
info = f"📦 {item.get('name', '')}" |
|
if item.get('variant'): |
|
info += f" ({item['variant']})" |
|
if item.get('warehouses'): |
|
info += f"\n📍 Mevcut: {', '.join(item['warehouses'])}" |
|
if item.get('price'): |
|
info += f"\n💰 {item['price']}" |
|
warehouse_info.append(info) |
|
else: |
|
warehouse_info.append(str(item)) |
|
return warehouse_info if warehouse_info else None |
|
except Exception as e: |
|
logger.error(f"GPT-5 warehouse search error: {e}") |
|
|
|
|
|
|
|
try: |
|
import re |
|
warehouse_url = 'https://video.trek-turkey.com/bizimhesap-warehouse-xml-b2b-api-v2.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 = [] |
|
|
|
|
|
size_color_words = ['s', 'm', 'l', 'xl', 'xs', 'small', 'medium', 'large', |
|
'turuncu', 'siyah', 'beyaz', 'mavi', 'kirmizi', 'yesil', |
|
'orange', 'black', 'white', 'blue', 'red', 'green'] |
|
|
|
variant_words = [word for word in search_words if word in size_color_words] |
|
product_words = [word for word in search_words if word not in size_color_words] |
|
|
|
|
|
is_size_color_query = len(variant_words) > 0 and len(search_words) <= 4 |
|
|
|
|
|
if is_size_color_query: |
|
for product in root.findall('Product'): |
|
product_name_elem = product.find('ProductName') |
|
variant_elem = product.find('ProductVariant') |
|
|
|
if product_name_elem is not None and product_name_elem.text: |
|
xml_product_name = product_name_elem.text.strip() |
|
normalized_product_name = normalize_turkish(xml_product_name.lower()) |
|
|
|
|
|
product_name_matches = True |
|
if product_words: |
|
product_name_matches = all(word in normalized_product_name for word in product_words) |
|
|
|
|
|
if product_name_matches: |
|
|
|
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 variant_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: |
|
|
|
for warehouse in product.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}: Stokta var") |
|
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: |
|
|
|
url = 'https://www.trekbisiklet.com.tr/output/8582384479' |
|
response = requests.get(url, verify=False, timeout=10) |
|
|
|
|
|
content_preview = response.content[:500].decode('utf-8', errors='ignore') |
|
root = ET.fromstring(response.content) |
|
all_items = root.findall('item') |
|
|
|
|
|
|
|
|
|
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 |
|
pass |
|
|
|
|
|
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)) |
|
|
|
|
|
|
|
|
|
global improved_whatsapp_bot |
|
improved_whatsapp_bot = None |
|
if USE_IMPROVED_SEARCH: |
|
try: |
|
improved_whatsapp_bot = WhatsAppImprovedChatbot(products) |
|
|
|
except Exception as e: |
|
logger.error(f"BF Space: Failed to initialize improved WhatsApp search: {e}") |
|
USE_IMPROVED_SEARCH = False |
|
improved_whatsapp_bot = None |
|
|
|
|
|
|
|
|
|
|
|
if marlin_count == 0: |
|
pass |
|
else: |
|
|
|
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"] |
|
|
|
|
|
pass |
|
|
|
except Exception as e: |
|
logger.error(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 |
|
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_media(user_message, phone_number, media_urls, media_types): |
|
"""Medya içeriği olan WhatsApp mesajı işleme - GPT-5 Vision ile""" |
|
try: |
|
logger.info(f"🖼️ Medya analizi başlıyor: {len(media_urls)} medya") |
|
|
|
|
|
profile_analysis = analyze_user_message(phone_number, user_message) |
|
logger.info(f"📊 Profil analizi: {phone_number} -> {profile_analysis}") |
|
|
|
|
|
messages = build_context_messages(phone_number, user_message if user_message else "Gönderilen görseli analiz et") |
|
|
|
|
|
vision_message = { |
|
"role": "user", |
|
"content": [] |
|
} |
|
|
|
|
|
if user_message and user_message.strip(): |
|
vision_message["content"].append({ |
|
"type": "text", |
|
"text": user_message |
|
}) |
|
else: |
|
vision_message["content"].append({ |
|
"type": "text", |
|
"text": "Bu görselde ne var? Detaylı açıkla." |
|
}) |
|
|
|
|
|
for i, media_url in enumerate(media_urls): |
|
media_type = media_types[i] if i < len(media_types) else "image/jpeg" |
|
|
|
|
|
if media_type and media_type.startswith('image/'): |
|
|
|
if 'api.twilio.com' in media_url: |
|
|
|
import re |
|
match = re.search(r'/Messages/([^/]+)/Media/([^/]+)', media_url) |
|
if match: |
|
message_sid = match.group(1) |
|
media_sid = match.group(2) |
|
|
|
proxy_url = f"https://video.trek-turkey.com/twilio-media-proxy.php?action=media&message={message_sid}&media={media_sid}" |
|
logger.info(f"🔄 Proxy URL: {proxy_url}") |
|
|
|
vision_message["content"].append({ |
|
"type": "image_url", |
|
"image_url": { |
|
"url": proxy_url |
|
} |
|
}) |
|
else: |
|
|
|
vision_message["content"].append({ |
|
"type": "image_url", |
|
"image_url": { |
|
"url": media_url |
|
} |
|
}) |
|
|
|
|
|
messages = [msg for msg in messages if msg.get("role") != "user" or msg != messages[-1]] |
|
messages.append(vision_message) |
|
|
|
|
|
messages.insert(0, { |
|
"role": "system", |
|
"content": "Gönderilen görsellerde bisiklet veya bisiklet parçaları varsa, bunları detaylıca tanımla. Marka, model, renk, özellikler gibi detayları belirt. Eğer Trek bisiklet ise modeli tahmin etmeye çalış. Stok durumu sorulursa, görseldeki bisikletin özelliklerini belirterek stok kontrolü yapılması gerektiğini söyle." |
|
}) |
|
|
|
if not OPENAI_API_KEY: |
|
return "OpenAI API anahtarı eksik. Lütfen environment variables'ları kontrol edin." |
|
|
|
logger.info(f"📤 GPT-5 Vision'a gönderiliyor: {len(messages)} mesaj") |
|
|
|
payload = { |
|
"model": "gpt-5-chat-latest", |
|
"messages": messages, |
|
"temperature": 0, |
|
"max_tokens": 800, |
|
"stream": False, |
|
"top_p": 0.1, |
|
"frequency_penalty": 0.1, |
|
"presence_penalty": 0 |
|
} |
|
|
|
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, f"[Görsel gönderildi] {user_message if user_message else ''}", formatted_response) |
|
|
|
return formatted_response |
|
else: |
|
logger.error(f"OpenAI API Error: {response.status_code} - {response.text}") |
|
return f"Görsel analizi başarısız oldu. Lütfen tekrar deneyin." |
|
|
|
except Exception as e: |
|
logger.error(f"❌ Medya işleme hatası: {e}") |
|
import traceback |
|
traceback.print_exc() |
|
return "Görsel işlenirken bir hata oluştu. Lütfen tekrar deneyin." |
|
|
|
def process_whatsapp_message_with_memory(user_message, phone_number): |
|
"""Hafızalı WhatsApp mesaj işleme""" |
|
try: |
|
|
|
if USE_STORE_NOTIFICATION: |
|
|
|
should_notify_mehmet, notification_reason, urgency = should_notify_mehmet_bey(user_message) |
|
|
|
|
|
if not should_notify_mehmet and USE_INTENT_ANALYZER: |
|
context = get_conversation_context(phone_number) |
|
intent_analysis = analyze_customer_intent(user_message, context) |
|
should_notify_mehmet, notification_reason, urgency = should_notify_mehmet_bey(user_message, intent_analysis) |
|
else: |
|
intent_analysis = None |
|
|
|
if should_notify_mehmet: |
|
|
|
if intent_analysis: |
|
product = intent_analysis.get("product") or "Belirtilmemiş" |
|
else: |
|
|
|
context = get_conversation_context(phone_number) |
|
product = context.get("current_category") or "Ürün belirtilmemiş" |
|
|
|
|
|
if "rezervasyon" in notification_reason.lower() or urgency == "high": |
|
action = "reserve" |
|
elif "mağaza" in notification_reason.lower() or "lokasyon" in notification_reason.lower(): |
|
action = "info" |
|
elif "fiyat" in notification_reason.lower() or "ödeme" in notification_reason.lower(): |
|
action = "price" |
|
else: |
|
action = "info" |
|
|
|
|
|
additional_info = f"{notification_reason}\n\nMüşteri Mesajı: '{user_message}'" |
|
if urgency == "high": |
|
additional_info = "⚠️ YÜKSEK ÖNCELİK ⚠️\n" + additional_info |
|
|
|
|
|
result = send_store_notification( |
|
customer_phone=phone_number, |
|
customer_name=None, |
|
product_name=product, |
|
action=action, |
|
store_name=None, |
|
additional_info=additional_info |
|
) |
|
|
|
if result: |
|
logger.info(f"✅ Mehmet Bey'e bildirim gönderildi!") |
|
logger.info(f" 📍 Sebep: {notification_reason}") |
|
logger.info(f" ⚡ Öncelik: {urgency}") |
|
logger.info(f" 📦 Ürün: {product}") |
|
else: |
|
logger.error("❌ Mehmet Bey'e bildirim gönderilemedi") |
|
|
|
|
|
|
|
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: |
|
|
|
product_keywords = ['fiyat', 'kaç', 'stok', 'var mı', 'mevcut', 'bisiklet', 'bike', |
|
'trek', 'model', 'beden', 'renk', 'mağaza', 'nerede', 'hangi'] |
|
|
|
|
|
non_product_responses = ['süper', 'harika', 'güzel', 'teşekkür', 'tamam', 'olur', |
|
'evet', 'hayır', 'peki', 'anladım', 'tamamdır'] |
|
|
|
is_product_query = False |
|
lower_message = user_message.lower() |
|
|
|
|
|
if any(keyword in lower_message for keyword in product_keywords): |
|
is_product_query = True |
|
|
|
elif lower_message not in non_product_responses and len(lower_message.split()) > 1: |
|
|
|
is_product_query = True |
|
|
|
elif len(lower_message.split()) == 1 and len(lower_message) < 6: |
|
is_product_query = False |
|
|
|
if is_product_query: |
|
|
|
logger.info("📦 Using GPT-5 warehouse search") |
|
warehouse_result = get_warehouse_stock(user_message) |
|
if warehouse_result and warehouse_result != ['Ürün bulunamadı']: |
|
warehouse_response = "\n".join(warehouse_result) |
|
messages.append({ |
|
"role": "system", |
|
"content": f"MAĞAZA STOK BİLGİSİ:\n{warehouse_response}\n\nBu bilgileri kullanarak kullanıcıya yardımcı ol. Fiyat ve stok bilgilerini AYNEN kullan." |
|
}) |
|
logger.info(f"✅ Warehouse stock info added: {warehouse_response[:200]}...") |
|
else: |
|
logger.info(f"🚫 Skipping product search for: '{user_message}'") |
|
|
|
if not OPENAI_API_KEY: |
|
return "OpenAI API anahtarı eksik. Lütfen environment variables'ları kontrol edin." |
|
|
|
|
|
logger.info(f"📤 Sending to GPT-5: {len(messages)} messages") |
|
for i, msg in enumerate(messages): |
|
if msg.get('role') == 'system': |
|
content_preview = msg.get('content', '')[:500] |
|
if 'Fiyat:' in content_preview or 'TL' in content_preview: |
|
logger.info(f"💰 System message with price info: {content_preview}") |
|
|
|
payload = { |
|
"model": "gpt-5-chat-latest", |
|
"messages": messages, |
|
"temperature": 0, |
|
"max_tokens": 800, |
|
"stream": False, |
|
"top_p": 0.1, |
|
"frequency_penalty": 0.1, |
|
"presence_penalty": 0 |
|
} |
|
|
|
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}") |
|
import traceback |
|
traceback.print_exc() |
|
logger.error(f"Detailed error: {str(e)}") |
|
logger.error(f"Error type: {type(e).__name__}") |
|
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: |
|
logger.error(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, |
|
"top_p": 0.1, |
|
"frequency_penalty": 0.1, |
|
"presence_penalty": 0 |
|
} |
|
|
|
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}") |
|
import traceback |
|
traceback.print_exc() |
|
logger.error(f"Detailed error: {str(e)}") |
|
logger.error(f"Error type: {type(e).__name__}") |
|
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') |
|
|
|
|
|
num_media = form_data.get('NumMedia', '0') |
|
media_urls = [] |
|
media_types = [] |
|
|
|
|
|
if num_media and int(num_media) > 0: |
|
for i in range(int(num_media)): |
|
media_url = form_data.get(f'MediaUrl{i}') |
|
media_type = form_data.get(f'MediaContentType{i}') |
|
if media_url: |
|
media_urls.append(media_url) |
|
media_types.append(media_type) |
|
logger.info(f"📸 Medya alındı: {media_type} - {media_url[:100]}...") |
|
|
|
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 USE_MEDIA_QUEUE: |
|
if media_urls: |
|
|
|
logger.info(f"📸 Media Queue V2: Medya alındı - {from_number}") |
|
|
|
|
|
wait_message = media_queue.handle_media( |
|
from_number, |
|
media_urls, |
|
media_types, |
|
message_body or "" |
|
) |
|
|
|
|
|
if twilio_client: |
|
twilio_client.messages.create( |
|
messaging_service_sid=TWILIO_MESSAGING_SERVICE_SID, |
|
body=wait_message, |
|
to=from_number |
|
) |
|
logger.info(f"📤 Bekleme mesajı gönderildi: {wait_message}") |
|
|
|
return {"status": "media_queued", "message": wait_message} |
|
|
|
else: |
|
|
|
combined_text, cached_media_urls, cached_media_types = media_queue.handle_text(from_number, message_body) |
|
|
|
if combined_text and cached_media_urls: |
|
|
|
logger.info(f"✅ Media Queue V2: Birleştirildi - {from_number}") |
|
logger.info(f" Birleşik mesaj: {combined_text[:100]}...") |
|
logger.info(f" Medya sayısı: {len(cached_media_urls)}") |
|
|
|
|
|
message_body = combined_text |
|
media_urls = cached_media_urls |
|
media_types = cached_media_types |
|
|
|
else: |
|
|
|
logger.info(f"💬 Media Queue V2: Normal metin - {from_number}") |
|
|
|
|
|
|
|
if not message_body or message_body.strip() == "": |
|
if not media_urls: |
|
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"} |
|
|
|
|
|
if media_urls: |
|
|
|
ai_response = process_whatsapp_message_with_media(message_body, from_number, media_urls, media_types) |
|
else: |
|
|
|
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" |
|
} |
|
|
|
@app.get("/test-madone") |
|
async def test_madone(): |
|
"""Test MADONE search directly""" |
|
from smart_warehouse_with_price import get_warehouse_stock_smart_with_price |
|
|
|
|
|
import os |
|
if not os.getenv("OPENAI_API_KEY"): |
|
return {"error": "No OPENAI_API_KEY set"} |
|
|
|
try: |
|
result = get_warehouse_stock_smart_with_price("madone sl 6") |
|
return { |
|
"query": "madone sl 6", |
|
"result": result if result else "No result", |
|
"api_key_set": bool(os.getenv("OPENAI_API_KEY")) |
|
} |
|
except Exception as e: |
|
return {"error": str(e), "type": type(e).__name__} |
|
|
|
@app.post("/test-vision") |
|
async def test_vision(request: Request): |
|
"""Test vision capabilities with a sample image URL""" |
|
try: |
|
data = await request.json() |
|
image_url = data.get("image_url") |
|
text = data.get("text", "Bu görselde ne var?") |
|
|
|
if not image_url: |
|
return {"error": "image_url is required"} |
|
|
|
|
|
messages = [ |
|
{ |
|
"role": "system", |
|
"content": "Sen bir bisiklet uzmanısın. Görselleri analiz et ve detaylı bilgi ver." |
|
}, |
|
{ |
|
"role": "user", |
|
"content": [ |
|
{"type": "text", "text": text}, |
|
{"type": "image_url", "image_url": {"url": image_url}} |
|
] |
|
} |
|
] |
|
|
|
payload = { |
|
"model": "gpt-5-chat-latest", |
|
"messages": messages, |
|
"temperature": 0, |
|
"max_tokens": 500, |
|
"stream": False, |
|
"top_p": 0.1, |
|
"frequency_penalty": 0.1, |
|
"presence_penalty": 0 |
|
} |
|
|
|
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 { |
|
"success": True, |
|
"response": result['choices'][0]['message']['content'], |
|
"model": result.get('model', 'unknown') |
|
} |
|
else: |
|
return { |
|
"success": False, |
|
"error": response.text, |
|
"status_code": response.status_code |
|
} |
|
|
|
except Exception as e: |
|
return {"error": str(e), "type": type(e).__name__} |
|
|
|
if __name__ == "__main__": |
|
import uvicorn |
|
print("🚀 Trek WhatsApp Bot başlatılıyor...") |
|
uvicorn.run(app, host="0.0.0.0", port=7860) |