|
import gradio as gr |
|
import os |
|
import json |
|
import requests |
|
import xml.etree.ElementTree as ET |
|
import schedule |
|
import time |
|
import threading |
|
from concurrent.futures import ThreadPoolExecutor, as_completed |
|
from huggingface_hub import HfApi, create_repo, hf_hub_download |
|
import warnings |
|
import pandas as pd |
|
from docx import Document |
|
import spaces |
|
from google.oauth2.service_account import Credentials |
|
from googleapiclient.discovery import build |
|
from googleapiclient.http import MediaIoBaseDownload |
|
import io |
|
import warnings |
|
from requests.packages.urllib3.exceptions import InsecureRequestWarning |
|
warnings.simplefilter('ignore', InsecureRequestWarning) |
|
|
|
|
|
from prompts import get_prompt_content_only |
|
|
|
|
|
from enhanced_features import ( |
|
initialize_enhanced_features, process_image_message, |
|
handle_comparison_request |
|
) |
|
from image_renderer import extract_product_info_for_gallery, format_message_with_images |
|
|
|
|
|
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: |
|
print(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: |
|
print(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']: |
|
print("Mağaza verisi bulunamadı") |
|
return None |
|
|
|
warehouses = warehouses_data['data']['warehouses'] |
|
|
|
|
|
search_terms = normalize_turkish(product_name).lower().split() |
|
print(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: |
|
print(f"API hatası: {e}") |
|
return None |
|
|
|
def get_realtime_stock(product_name): |
|
"""API'den gerçek zamanlı stok bilgisini çek - Minimal versiyon""" |
|
try: |
|
|
|
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: |
|
print(f"Mağaza listesi alınamadı: {warehouses_response.status_code}") |
|
return None |
|
|
|
warehouses_data = warehouses_response.json() |
|
|
|
|
|
if 'data' in warehouses_data and 'warehouses' in warehouses_data['data']: |
|
warehouses = warehouses_data['data']['warehouses'] |
|
else: |
|
print("Mağaza verisi bulunamadı") |
|
return None |
|
|
|
|
|
search_terms = normalize_turkish(product_name).lower().split() |
|
print(f"Aranan ürün: {product_name} -> {search_terms}") |
|
|
|
|
|
stock_info = {} |
|
total_stock = 0 |
|
dsw_stock = 0 |
|
|
|
for warehouse in warehouses: |
|
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: |
|
continue |
|
|
|
inventory_data = inventory_response.json() |
|
|
|
|
|
if 'data' in inventory_data and 'inventory' in inventory_data['data']: |
|
products = inventory_data['data']['inventory'] |
|
else: |
|
continue |
|
|
|
|
|
warehouse_variants = [] |
|
for product in products: |
|
product_title = normalize_turkish(product.get('title', '')).lower() |
|
original_title = product.get('title', '') |
|
|
|
|
|
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 += 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") |
|
total_stock += actual_stock |
|
|
|
|
|
if warehouse_variants and not is_dsw: |
|
stock_info[warehouse_name] = warehouse_variants |
|
|
|
if not stock_info: |
|
|
|
if dsw_stock > 0: |
|
return f"{product_name}: Şu anda mağazalarda stokta yok, ancak yakında gelecek. Ön sipariş verebilirsiniz." |
|
else: |
|
return f"{product_name}: Şu anda hiçbir mağazada stokta bulunmuyor." |
|
|
|
|
|
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} adet") |
|
|
|
if total_stock > 0: |
|
prompt_lines.append(f"✓ Ürün stokta mevcut") |
|
|
|
else: |
|
prompt_lines.append(f"✗ Stokta yok") |
|
|
|
return "\n".join(prompt_lines) |
|
|
|
except Exception as e: |
|
print(f"Stok API hatası: {e}") |
|
return None |
|
|
|
def is_stock_query(message): |
|
"""Mesajın stok sorgusu olup olmadığını kontrol et""" |
|
stock_keywords = [ |
|
'stok', 'stock', 'kaç adet', 'kaç tane', 'var mı', |
|
'mevcut mu', 'bulunuyor mu', 'hangi mağaza', 'nerede var', |
|
'stok durumu', 'stoklarda', 'stokta', 'adet', 'kaç tane var' |
|
] |
|
message_lower = message.lower() |
|
return any(keyword in message_lower for keyword in stock_keywords) |
|
|
|
|
|
|
|
warnings.filterwarnings("ignore", category=UserWarning, module="gradio.components.chatbot") |
|
|
|
|
|
LOG_FILE = '/data/chat_logs.txt' if os.path.exists('/data') else 'chat_logs.txt' |
|
print(f"Dosya yolu: {os.path.abspath(LOG_FILE)}") |
|
|
|
|
|
API_URL = "https://api.openai.com/v1/chat/completions" |
|
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") |
|
if not OPENAI_API_KEY: |
|
print("Hata: OPENAI_API_KEY çevre değişkeni ayarlanmamış!") |
|
|
|
|
|
url = 'https://www.trekbisiklet.com.tr/output/8582384479' |
|
response = requests.get(url, verify=False, timeout=60) |
|
if response.status_code == 200 and response.content: |
|
root = ET.fromstring(response.content) |
|
else: |
|
print(f"HTTP hatası: {response.status_code}") |
|
root = None |
|
|
|
products = [] |
|
if root is not None: |
|
for item in root.findall('item'): |
|
|
|
rootlabel_elem = item.find('rootlabel') |
|
stock_elem = item.find('stockAmount') |
|
|
|
if rootlabel_elem is None or stock_elem is None: |
|
continue |
|
|
|
name_words = rootlabel_elem.text.lower().split() |
|
name = name_words[0] |
|
full_name = ' '.join(name_words) |
|
|
|
stock_amount = "stokta" if stock_elem.text and stock_elem.text > '0' else "stokta değil" |
|
|
|
|
|
if stock_amount == "stokta": |
|
|
|
price_elem = item.find('priceTaxWithCur') |
|
price_str = price_elem.text if price_elem is not None and price_elem.text else "Fiyat bilgisi yok" |
|
|
|
|
|
price_eft_elem = item.find('priceEft') |
|
price_eft_str = price_eft_elem.text if price_eft_elem is not None and price_eft_elem.text else "" |
|
|
|
|
|
price_rebate_elem = item.find('priceRebateWithTax') |
|
price_rebate_str = price_rebate_elem.text if price_rebate_elem is not None and price_rebate_elem.text else "" |
|
|
|
|
|
price_rebate_money_order_elem = item.find('priceRebateWithMoneyOrderWithTax') |
|
price_rebate_money_order_str = price_rebate_money_order_elem.text if price_rebate_money_order_elem is not None and price_rebate_money_order_elem.text else "" |
|
|
|
|
|
try: |
|
price_float = float(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 = 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: |
|
price_eft = "" |
|
|
|
|
|
if price_rebate_str: |
|
try: |
|
price_rebate_float = float(price_rebate_str) |
|
|
|
if price_rebate_float > 200000: |
|
price_rebate = str(round(price_rebate_float / 5000) * 5000) |
|
|
|
elif price_rebate_float > 30000: |
|
price_rebate = str(round(price_rebate_float / 1000) * 1000) |
|
|
|
elif price_rebate_float > 10000: |
|
price_rebate = str(round(price_rebate_float / 100) * 100) |
|
|
|
else: |
|
price_rebate = str(round(price_rebate_float / 10) * 10) |
|
except (ValueError, TypeError): |
|
price_rebate = price_rebate_str |
|
else: |
|
price_rebate = "" |
|
|
|
|
|
if price_rebate_money_order_str: |
|
try: |
|
price_rebate_money_order_float = float(price_rebate_money_order_str) |
|
|
|
if price_rebate_money_order_float > 200000: |
|
price_rebate_money_order = str(round(price_rebate_money_order_float / 5000) * 5000) |
|
|
|
elif price_rebate_money_order_float > 30000: |
|
price_rebate_money_order = str(round(price_rebate_money_order_float / 1000) * 1000) |
|
|
|
elif price_rebate_money_order_float > 10000: |
|
price_rebate_money_order = str(round(price_rebate_money_order_float / 100) * 100) |
|
|
|
else: |
|
price_rebate_money_order = str(round(price_rebate_money_order_float / 10) * 10) |
|
except (ValueError, TypeError): |
|
price_rebate_money_order = price_rebate_money_order_str |
|
else: |
|
price_rebate_money_order = "" |
|
|
|
|
|
product_url_elem = item.find('productLink') |
|
product_url = product_url_elem.text if product_url_elem is not None and product_url_elem.text else "" |
|
product_info = [stock_amount, price, product_url, price_eft, price_rebate, price_rebate_money_order] |
|
|
|
|
|
image_elem = item.find('picture1Path') |
|
if image_elem is not None and image_elem.text: |
|
product_info.append(image_elem.text) |
|
else: |
|
product_info.append("") |
|
|
|
else: |
|
|
|
|
|
price_elem = item.find('priceTaxWithCur') |
|
price_str = price_elem.text if price_elem is not None and price_elem.text else "Fiyat bilgisi yok" |
|
|
|
|
|
product_url_elem = item.find('productLink') |
|
product_url = product_url_elem.text if product_url_elem is not None and product_url_elem.text else "" |
|
|
|
|
|
image_elem = item.find('picture1Path') |
|
image_url = image_elem.text if image_elem is not None and image_elem.text else "" |
|
|
|
|
|
product_info = [stock_amount, price_str, product_url, "", "", "", image_url] |
|
|
|
products.append((name, product_info, full_name)) |
|
|
|
print(f"Toplam {len(products)} ürün yüklendi.") |
|
|
|
|
|
initialize_enhanced_features(OPENAI_API_KEY, products) |
|
|
|
|
|
GOOGLE_CREDENTIALS_PATH = os.getenv("GOOGLE_CREDENTIALS_PATH") |
|
GOOGLE_FOLDER_ID = "1bE8aMj8-eFGftjMPOF8bKQJAhfHa0BN8" |
|
|
|
|
|
file_lock = threading.Lock() |
|
history_lock = threading.Lock() |
|
global_chat_history = [] |
|
document_content = "" |
|
|
|
|
|
def download_documents_from_drive(): |
|
global document_content |
|
|
|
if not GOOGLE_CREDENTIALS_PATH: |
|
print("Google credentials dosyası bulunamadı.") |
|
return |
|
|
|
try: |
|
credentials = Credentials.from_service_account_file(GOOGLE_CREDENTIALS_PATH) |
|
service = build('drive', 'v3', credentials=credentials) |
|
|
|
|
|
results = service.files().list( |
|
q=f"'{GOOGLE_FOLDER_ID}' in parents", |
|
fields="files(id, name, mimeType)" |
|
).execute() |
|
|
|
files = results.get('files', []) |
|
all_content = [] |
|
|
|
for file in files: |
|
print(f"İndiriliyor: {file['name']}") |
|
|
|
|
|
if file['mimeType'] == 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': |
|
request = service.files().get_media(fileId=file['id']) |
|
file_io = io.BytesIO() |
|
downloader = MediaIoBaseDownload(file_io, request) |
|
|
|
done = False |
|
while done is False: |
|
status, done = downloader.next_chunk() |
|
|
|
file_io.seek(0) |
|
doc = Document(file_io) |
|
|
|
content = f"\\n=== {file['name']} ===\\n" |
|
for paragraph in doc.paragraphs: |
|
if paragraph.text.strip(): |
|
content += paragraph.text + "\\n" |
|
|
|
all_content.append(content) |
|
|
|
document_content = "\\n".join(all_content) |
|
print(f"Toplam {len(files)} döküman yüklendi.") |
|
|
|
except Exception as e: |
|
print(f"Google Drive'dan döküman indirme hatası: {e}") |
|
|
|
|
|
document_thread = threading.Thread(target=download_documents_from_drive, daemon=True) |
|
document_thread.start() |
|
|
|
|
|
def clear_log_file(): |
|
try: |
|
if os.path.exists(LOG_FILE): |
|
with file_lock: |
|
with open(LOG_FILE, 'w', encoding='utf-8') as f: |
|
f.write("Log dosyası temizlendi.\\n") |
|
print("Log dosyası temizlendi.") |
|
except Exception as e: |
|
print(f"Log dosyası temizleme hatası: {e}") |
|
|
|
|
|
def run_scheduler(chat_history): |
|
schedule.every().day.at("03:00").do(clear_log_file) |
|
|
|
while True: |
|
schedule.run_pending() |
|
time.sleep(60) |
|
|
|
|
|
def chatbot_fn(user_message, history, image=None): |
|
if history is None: |
|
history = [] |
|
|
|
try: |
|
|
|
if image is not None: |
|
user_message = process_image_message(image, user_message) |
|
|
|
|
|
comparison_result = handle_comparison_request(user_message) |
|
if comparison_result: |
|
yield comparison_result |
|
return |
|
|
|
except Exception as e: |
|
print(f"Enhanced features error: {e}") |
|
|
|
|
|
try: |
|
with file_lock: |
|
with open(LOG_FILE, 'a', encoding='utf-8') as f: |
|
f.write(f"User: {user_message}\\n") |
|
except Exception as e: |
|
print(f"Dosya yazma hatası (Kullanıcı): {e}") |
|
|
|
|
|
system_messages = get_prompt_content_only() |
|
|
|
|
|
if document_content: |
|
system_messages.append({"role": "system", "content": f"Dökümanlardan gelen bilgiler: {document_content[:3000]}"}) |
|
|
|
|
|
if is_stock_query(user_message): |
|
print("Stok sorgusu algılandı, API'den veri çekiliyor...") |
|
|
|
|
|
|
|
product_words = [] |
|
skip_words = ['stok', 'stock', 'kaç', 'adet', 'tane', 'var', 'mı', 'mi', |
|
'mevcut', 'mu', 'bulunuyor', 'hangi', 'mağaza', 'nerede', |
|
'durumu', 'stoklarda', 'stokta', 'için', 've', 'ile', 'toplam', 'toplamda', |
|
'fiyat', 'fiyatı', 'ne', 'nedir', 'kadar', 'beden', 'bedeni', 'bedenli'] |
|
|
|
|
|
size_terms = ['xs', 's', 'm', 'ml', 'l', 'xl', 'xxl', '2xl', '3xl', 'small', 'medium', 'large', |
|
'44', '46', '48', '50', '52', '54', '56', '58', '60'] |
|
|
|
for word in user_message.lower().split(): |
|
|
|
if word not in skip_words: |
|
product_words.append(word) |
|
|
|
if product_words: |
|
product_name = ' '.join(product_words) |
|
|
|
|
|
size_terms = ['xs', 's', 'm', 'ml', 'l', 'xl', 'xxl', '2xl', '3xl', 'small', 'medium', 'large', |
|
'44', '46', '48', '50', '52', '54', '56', '58', '60'] |
|
is_only_size = len(product_words) == 1 and product_words[0].lower() in size_terms |
|
|
|
if is_only_size: |
|
|
|
product_name = product_words[0].lower() |
|
print(f"Sadece beden sorgusu: {product_name}") |
|
|
|
|
|
all_stock_info = [] |
|
|
|
|
|
stock_info = get_realtime_stock_parallel(product_name) |
|
if stock_info: |
|
all_stock_info.append((product_name, stock_info)) |
|
|
|
|
|
if len(product_words) <= 2 and 'fx' in product_name: |
|
|
|
if '3' in product_name: |
|
fx3_info = get_realtime_stock_parallel('fx 3') |
|
if fx3_info: |
|
all_stock_info.append(('FX 3', fx3_info)) |
|
|
|
|
|
fx_sport_info = get_realtime_stock_parallel('fx sport al 3') |
|
if fx_sport_info: |
|
all_stock_info.append(('FX Sport AL 3', fx_sport_info)) |
|
|
|
|
|
if all_stock_info: |
|
if len(all_stock_info) > 1: |
|
|
|
combined_info = "Eşleşen ürünler:\n" |
|
for prod_name, stock in all_stock_info: |
|
combined_info += f"\n{stock}\n---" |
|
system_messages.append({ |
|
"role": "system", |
|
"content": f"GÜNCEL STOK BİLGİSİ (API'den alındı):\n{combined_info}" |
|
}) |
|
else: |
|
|
|
system_messages.append({ |
|
"role": "system", |
|
"content": f"GÜNCEL STOK BİLGİSİ (API'den alındı):\n{all_stock_info[0][1]}" |
|
}) |
|
print(f"Stok bilgisi eklendi") |
|
|
|
|
|
|
|
has_stock_from_api = is_stock_query(user_message) and 'all_stock_info' in locals() and len(all_stock_info) > 0 |
|
|
|
|
|
|
|
input_words = user_message.lower().split() |
|
|
|
added_products_count = 0 |
|
|
|
|
|
if has_stock_from_api and 'product_name' in locals(): |
|
for product_info in products: |
|
product_full_name = product_info[2].lower() |
|
|
|
if product_name.lower().replace(' ', '') in product_full_name.replace(' ', ''): |
|
print(f"XML tam eşleşme bulundu: {product_info[2]}") |
|
|
|
if len(product_info[1]) > 1 and product_info[1][1]: |
|
system_messages.append({ |
|
"role": "system", |
|
"content": f"ÜRÜN BİLGİLERİ (XML):\nFiyat: {product_info[1][1]} TL\nLink: {product_info[1][2] if len(product_info[1]) > 2 else ''}\nResim: {product_info[1][6] if len(product_info[1]) > 6 else ''}" |
|
}) |
|
added_products_count = 1 |
|
break |
|
|
|
|
|
if added_products_count == 0: |
|
for word in input_words: |
|
if added_products_count >= 5: |
|
break |
|
for product_info in products: |
|
|
|
if word in product_info[0] or word in product_info[2].lower(): |
|
|
|
if has_stock_from_api or product_info[1][0] == "stokta": |
|
|
|
print(f"XML'de bulunan ürün: {product_info[2]}") |
|
|
|
|
|
if len(product_info[1]) > 1 and product_info[1][1]: |
|
normal_price = f"\\nFiyat: {product_info[1][1]} TL" |
|
print(f"Fiyat bulundu: {product_info[1][1]}") |
|
else: |
|
normal_price = "" |
|
print("Fiyat bilgisi yok") |
|
|
|
rebate_price = "" |
|
discount_info = "" |
|
eft_price = "" |
|
rebate_money_order_price = "" |
|
|
|
if len(product_info[1]) > 4 and product_info[1][4] and product_info[1][4] != "": |
|
rebate_price = f"\\nKampanyalı fiyat: {product_info[1][4]} TL" |
|
|
|
try: |
|
normal_price_float = float(product_info[1][1]) |
|
rebate_price_float = float(product_info[1][4]) |
|
discount_amount = normal_price_float - rebate_price_float |
|
|
|
if discount_amount > 0: |
|
if discount_amount > 200000: |
|
discount_amount_rounded = round(discount_amount / 5000) * 5000 |
|
elif discount_amount > 30000: |
|
discount_amount_rounded = round(discount_amount / 1000) * 1000 |
|
elif discount_amount > 10000: |
|
discount_amount_rounded = round(discount_amount / 100) * 100 |
|
else: |
|
discount_amount_rounded = round(discount_amount / 10) * 10 |
|
|
|
discount_info = f"\\nYapılan indirim: {discount_amount_rounded:.0f} TL" |
|
except (ValueError, TypeError): |
|
discount_info = "" |
|
|
|
rebate_money_order_price = "" |
|
else: |
|
if product_info[1][3] and product_info[1][3] != "": |
|
eft_price = f"\\nHavale indirimli fiyat: {product_info[1][3]} TL" |
|
|
|
product_link = f"\\nÜrün linki: {product_info[1][2]}" |
|
product_image = "" |
|
if len(product_info[1]) > 6 and product_info[1][6]: |
|
product_image = f"\\nÜrün resmi: {product_info[1][6]}" |
|
|
|
|
|
if has_stock_from_api: |
|
new_msg = f"ÜRÜN BİLGİLERİ (Fiyat/Link):\\n{normal_price}{rebate_price}{discount_info}{eft_price}{rebate_money_order_price}{product_link}{product_image}" |
|
else: |
|
|
|
new_msg = f"{product_info[2]} {product_info[1][0]}\\n{normal_price}{rebate_price}{discount_info}{eft_price}{rebate_money_order_price}{product_link}{product_image}" |
|
else: |
|
|
|
if has_stock_from_api: |
|
|
|
new_msg = None |
|
else: |
|
new_msg = f"{product_info[2]} {product_info[1][0]}" |
|
|
|
|
|
if new_msg and len(new_msg) < 5000: |
|
system_messages.append({"role": "system", "content": new_msg}) |
|
added_products_count += 1 |
|
break |
|
|
|
|
|
if len(history) > 10: |
|
history = history[-10:] |
|
|
|
messages = system_messages + history + [{"role": "user", "content": user_message}] |
|
|
|
payload = { |
|
"model": "gpt-5-chat-latest", |
|
"messages": messages, |
|
"temperature": 0.1, |
|
"top_p": 0.9, |
|
"n": 1, |
|
"stream": True, |
|
"presence_penalty": 0.1, |
|
"frequency_penalty": 0, |
|
} |
|
headers = { |
|
"Content-Type": "application/json", |
|
"Authorization": f"Bearer {OPENAI_API_KEY}" |
|
} |
|
|
|
response = requests.post(API_URL, headers=headers, json=payload, stream=True, timeout=120) |
|
if response.status_code != 200: |
|
print(f"API Hatası: {response.status_code} - {response.text}") |
|
yield f"API Hatası ({response.status_code}): {response.text[:200]}" |
|
return |
|
|
|
partial_response = "" |
|
|
|
for chunk in response.iter_lines(): |
|
if not chunk: |
|
continue |
|
chunk_str = chunk.decode('utf-8') |
|
if chunk_str.startswith("data: ") and chunk_str != "data: [DONE]": |
|
try: |
|
chunk_data = json.loads(chunk_str[6:]) |
|
delta = chunk_data['choices'][0]['delta'] |
|
if 'content' in delta: |
|
partial_response += delta['content'] |
|
|
|
formatted_response = extract_product_info_for_gallery(partial_response) |
|
yield formatted_response |
|
except json.JSONDecodeError as e: |
|
print(f"JSON parse hatası: {e} - Chunk: {chunk_str}") |
|
elif chunk_str == "data: [DONE]": |
|
break |
|
|
|
|
|
final_response = extract_product_info_for_gallery(partial_response) |
|
yield final_response |
|
|
|
|
|
try: |
|
with file_lock: |
|
with open(LOG_FILE, 'a', encoding='utf-8') as f: |
|
f.write(f"Bot: {partial_response}\\n") |
|
except Exception as e: |
|
print(f"Dosya yazma hatası (Bot): {e}") |
|
|
|
|
|
with history_lock: |
|
global_chat_history.append({"role": "user", "content": user_message}) |
|
global_chat_history.append({"role": "assistant", "content": partial_response}) |
|
|
|
|
|
def slow_echo(message, history): |
|
for i in range(len(message)): |
|
time.sleep(0.05) |
|
yield "You typed: " + message[: i + 1] |
|
|
|
|
|
USE_SLOW_ECHO = False |
|
chat_fn = slow_echo if USE_SLOW_ECHO else chatbot_fn |
|
|
|
if not USE_SLOW_ECHO: |
|
scheduler_thread = threading.Thread(target=run_scheduler, args=(global_chat_history,), daemon=True) |
|
scheduler_thread.start() |
|
|
|
|
|
trek_theme = gr.themes.Base( |
|
primary_hue="red", |
|
secondary_hue="slate", |
|
neutral_hue="slate", |
|
radius_size=gr.themes.sizes.radius_sm, |
|
spacing_size=gr.themes.sizes.spacing_md, |
|
text_size=gr.themes.sizes.text_sm |
|
) |
|
|
|
custom_css = """ |
|
/* Genel font ayarları */ |
|
.gradio-container, .gradio-container *, .message-wrap, .message-wrap p, .message-wrap div, |
|
button, input, select, textarea { |
|
font-family: 'Segoe UI', 'SF Pro Text', 'Roboto', -apple-system, BlinkMacSystemFont, |
|
'Helvetica Neue', Arial, sans-serif !important; |
|
font-size: 0.6rem !important; |
|
} |
|
|
|
/* Mobil responsive başlangıç */ |
|
@media (max-width: 768px) { |
|
.gradio-container, .gradio-container *, .message-wrap, .message-wrap p, .message-wrap div { |
|
font-size: 0.8rem !important; |
|
} |
|
|
|
h1 { |
|
font-size: 1.8rem !important; |
|
text-align: center; |
|
margin: 10px 0; |
|
} |
|
|
|
h2 { |
|
font-size: 1.4rem !important; |
|
} |
|
} |
|
|
|
/* Input alanı için de aynı boyut */ |
|
.message-textbox textarea { |
|
font-size: 0.6rem !important; |
|
} |
|
|
|
/* Başlıklar için özel boyutlar */ |
|
h1 { |
|
font-size: 1.4rem !important; |
|
font-weight: 800 !important; |
|
} |
|
|
|
h2 { |
|
font-size: 1.2rem !important; |
|
font-weight: 600 !important; |
|
} |
|
|
|
h3 { |
|
font-size: 1rem !important; |
|
font-weight: 600 !important; |
|
} |
|
|
|
/* Kart arka plan renkleri - görseldeki gibi */ |
|
/* Kullanıcı mesajları için mavi tonda arka plan */ |
|
.user-message, .user-message-highlighted { |
|
background-color: #e9f5fe !important; |
|
border-bottom-right-radius: 0 !important; |
|
box-shadow: 0 1px 2px rgba(0,0,0,0.1) !important; |
|
} |
|
|
|
/* Bot mesajları için beyaz arka plan ve hafif kenarlık */ |
|
.bot-message, .bot-message-highlighted { |
|
background-color: white !important; |
|
border: 1px solid #e0e0e0 !important; |
|
border-bottom-left-radius: 0 !important; |
|
box-shadow: 0 1px 2px rgba(0,0,0,0.05) !important; |
|
} |
|
|
|
/* Mesaj baloncuklarının köşe yuvarlatma değerleri */ |
|
.message-wrap { |
|
border-radius: 12px !important; |
|
margin: 0.5rem 0 !important; |
|
max-width: 90% !important; |
|
} |
|
|
|
/* Sohbet alanının genel arka planı */ |
|
.chat-container, .gradio-container { |
|
background-color: #f7f7f7 !important; |
|
} |
|
|
|
/* Daha net yazılar için text rendering */ |
|
* { |
|
-webkit-font-smoothing: antialiased; |
|
-moz-osx-font-smoothing: grayscale; |
|
text-rendering: optimizeLegibility; |
|
} |
|
|
|
/* Restore butonu stilleri kaldırıldı */ |
|
|
|
/* Responsive mobil tasarım - iOS benzeri */ |
|
@media (max-width: 768px) { |
|
/* Daha büyük ve dokunmatik dostu boyutlar */ |
|
.gradio-container { |
|
padding: 0 !important; |
|
margin: 0 !important; |
|
} |
|
|
|
/* Mesaj baloncukları iOS tarzı */ |
|
.message-wrap { |
|
margin: 0.3rem 0.5rem !important; |
|
max-width: 85% !important; |
|
border-radius: 18px !important; |
|
padding: 10px 15px !important; |
|
font-size: 0.9rem !important; |
|
} |
|
|
|
/* Kullanıcı mesajları */ |
|
.user-message, .user-message-highlighted { |
|
background-color: #007AFF !important; |
|
color: white !important; |
|
margin-left: auto !important; |
|
margin-right: 8px !important; |
|
} |
|
|
|
/* Bot mesajları */ |
|
.bot-message, .bot-message-highlighted { |
|
background-color: #f1f1f1 !important; |
|
color: #333 !important; |
|
margin-left: 8px !important; |
|
margin-right: auto !important; |
|
} |
|
} |
|
|
|
/* Input alanına uçan kağıt ikonu ekle */ |
|
#msg-input { |
|
position: relative; |
|
} |
|
|
|
#msg-input textarea { |
|
padding-right: 40px !important; |
|
} |
|
|
|
.input-icon { |
|
position: absolute; |
|
right: 12px; |
|
top: 50%; |
|
transform: translateY(-50%); |
|
font-size: 16px; |
|
pointer-events: none; |
|
z-index: 10; |
|
color: #666; |
|
} |
|
|
|
/* Mobil responsive - Input alanı */ |
|
@media (max-width: 768px) { |
|
#msg-input { |
|
margin: 10px 0; |
|
} |
|
|
|
#msg-input textarea { |
|
padding: 12px 45px 12px 15px !important; |
|
font-size: 1rem !important; |
|
border-radius: 20px !important; |
|
border: 1px solid #ddd !important; |
|
box-shadow: 0 1px 3px rgba(0,0,0,0.1) !important; |
|
} |
|
|
|
.input-icon { |
|
right: 15px; |
|
font-size: 18px; |
|
} |
|
} |
|
|
|
/* Genel mobil iyileştirmeler */ |
|
@media (max-width: 768px) { |
|
/* Chatbot alanı tam ekran */ |
|
.gradio-container .main { |
|
padding: 0 !important; |
|
} |
|
|
|
/* Başlık alanı küçült */ |
|
.gradio-container header { |
|
padding: 8px !important; |
|
} |
|
|
|
/* Tab alanlarını küçült */ |
|
.tab-nav { |
|
padding: 5px !important; |
|
} |
|
|
|
/* Scroll bar'ı gizle */ |
|
.scroll-hide { |
|
scrollbar-width: none; |
|
-ms-overflow-style: none; |
|
} |
|
|
|
.scroll-hide::-webkit-scrollbar { |
|
display: none; |
|
} |
|
} |
|
|
|
/* CSS Bitişi */ |
|
""" |
|
|
|
|
|
storage_js = "" |
|
|
|
|
|
def enhanced_chatbot_fn(message, history, image): |
|
return chatbot_fn(message, history, image) |
|
|
|
|
|
with gr.Blocks(css=custom_css, theme="soft", title="Trek Asistanı", head=storage_js) as demo: |
|
gr.Markdown("# 🚲 Trek Asistanı AI") |
|
gr.Markdown("**Akıllı özellikler:** Gerçek zamanlı stok bilgisi, ürün karşılaştırması ve detaylı ürün bilgileri sunuyorum.") |
|
|
|
chatbot = gr.Chatbot(height=600, elem_id="chatbot", show_label=False) |
|
|
|
msg = gr.Textbox( |
|
placeholder="Trek bisikletleri hakkında soru sorun...", |
|
show_label=False, |
|
elem_id="msg-input" |
|
) |
|
|
|
def respond(message, chat_history): |
|
if not message.strip(): |
|
return "", chat_history |
|
|
|
|
|
if chat_history is None: |
|
chat_history = [] |
|
|
|
|
|
chat_history.append((message, None)) |
|
yield "", chat_history |
|
|
|
|
|
formatted_history = [] |
|
if len(chat_history) > 1: |
|
for user_msg, bot_msg in chat_history[:-1]: |
|
if bot_msg: |
|
formatted_history.append({"role": "user", "content": user_msg}) |
|
formatted_history.append({"role": "assistant", "content": bot_msg}) |
|
|
|
try: |
|
|
|
response_generator = chatbot_fn(message, formatted_history, None) |
|
|
|
|
|
response = "" |
|
for partial in response_generator: |
|
response = partial |
|
|
|
chat_history[-1] = (message, response) |
|
yield "", chat_history |
|
|
|
except Exception as e: |
|
error_msg = f"Üzgünüm, bir hata oluştu: {str(e)}" |
|
print(f"Chat error: {e}") |
|
|
|
chat_history[-1] = (message, error_msg) |
|
yield "", chat_history |
|
|
|
msg.submit(respond, [msg, chatbot], [msg, chatbot], show_progress=True) |
|
|
|
if __name__ == "__main__": |
|
demo.launch(debug=True) |