samikoen
commited on
Commit
·
6ea7409
0
Parent(s):
Deploy from GitHub Actions
Browse files- Dockerfile +14 -0
- README.md +42 -0
- app.py +1810 -0
- cached_warehouse_search.py +210 -0
- combined_api_search.py +581 -0
- customer_manager.py +456 -0
- follow_up_system.py +407 -0
- get_warehouse_fast.py +130 -0
- image_renderer.py +143 -0
- intent_analyzer.py +328 -0
- media_queue_v2.py +281 -0
- product_search.py +284 -0
- prompts.py +182 -0
- reminder_scheduler.py +207 -0
- requirements.txt +18 -0
- search_marlin_products.py +157 -0
- smart_warehouse.py +262 -0
- smart_warehouse_complete.py +393 -0
- smart_warehouse_whatsapp.py +284 -0
- smart_warehouse_with_price.py +769 -0
- store_notification.py +397 -0
- test_actual_function.py +91 -0
- test_context_aware.py +41 -0
- test_crm.py +60 -0
- test_direct_madone.py +116 -0
- test_final.py +31 -0
- test_gpt5_mock.py +51 -0
- test_m_turuncu.py +160 -0
- test_marlin_fix.py +23 -0
- test_name_request.py +110 -0
- test_simulate_whatsapp.py +32 -0
- test_stock_consistency.py +97 -0
- test_updated_warehouse.py +166 -0
- test_variant.py +110 -0
- test_warehouse.py +136 -0
- warehouse_stock_finder.py +91 -0
- whatsapp_features.py +432 -0
- whatsapp_improved_chatbot.py +298 -0
- whatsapp_passive_profiler.py +368 -0
- whatsapp_renderer.py +222 -0
Dockerfile
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
RUN apt-get update && apt-get install -y build-essential && rm -rf /var/lib/apt/lists/*
|
| 6 |
+
|
| 7 |
+
COPY requirements.txt .
|
| 8 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 9 |
+
|
| 10 |
+
COPY . .
|
| 11 |
+
|
| 12 |
+
EXPOSE 7860
|
| 13 |
+
|
| 14 |
+
CMD ["python", "app.py"]
|
README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Trek WhatsApp AI
|
| 3 |
+
emoji: 🚴
|
| 4 |
+
colorFrom: red
|
| 5 |
+
colorTo: blue
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
# Trek WhatsApp AI Chatbot
|
| 11 |
+
|
| 12 |
+
Trek Bicycle Turkey için WhatsApp üzerinden çalışan AI chatbot.
|
| 13 |
+
|
| 14 |
+
## Canlı Ortam
|
| 15 |
+
- **HuggingFace Space:** https://huggingface.co/spaces/SamiKoen/BF-WAB
|
| 16 |
+
- **Webhook:** Twilio WhatsApp entegrasyonu
|
| 17 |
+
|
| 18 |
+
## Teknoloji
|
| 19 |
+
- **GPT-4o:** Görsel mesajlar için (Vision)
|
| 20 |
+
- **GPT-5.2:** Metin mesajları için
|
| 21 |
+
- **BizimHesap API:** Stok sorguları
|
| 22 |
+
- **IdeaSoft XML:** Ürün bilgileri
|
| 23 |
+
|
| 24 |
+
## Ana Dosyalar
|
| 25 |
+
| Dosya | Açıklama |
|
| 26 |
+
|-------|----------|
|
| 27 |
+
| app.py | Ana uygulama |
|
| 28 |
+
| prompts.py | System promptları |
|
| 29 |
+
| smart_warehouse_with_price.py | Stok + fiyat sorgusu |
|
| 30 |
+
| intent_analyzer.py | Müşteri niyet analizi |
|
| 31 |
+
| store_notification.py | Mağaza bildirimleri |
|
| 32 |
+
|
| 33 |
+
## Deploy
|
| 34 |
+
HuggingFace'e push etmek için:
|
| 35 |
+
```bash
|
| 36 |
+
cd /tmp && rm -rf bf-wab-clone
|
| 37 |
+
git clone https://SamiKoen:TOKEN@huggingface.co/spaces/SamiKoen/BF-WAB bf-wab-clone
|
| 38 |
+
# Değişiklikleri yap
|
| 39 |
+
git add . && git commit -m "mesaj" && git push
|
| 40 |
+
```
|
| 41 |
+
# Last deploy: Mon, Jan 19, 2026 5:36:55 PM
|
| 42 |
+
# Deploy test: Mon, Jan 19, 2026 5:39:13 PM
|
app.py
ADDED
|
@@ -0,0 +1,1810 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
Trek WhatsApp AI Asistani - Hybrid Model Version
|
| 4 |
+
Gorsel varsa: GPT-4o (vision)
|
| 5 |
+
Metin varsa: GPT-5.2 (daha akilli)
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import os
|
| 9 |
+
import json
|
| 10 |
+
import re
|
| 11 |
+
import requests
|
| 12 |
+
import xml.etree.ElementTree as ET
|
| 13 |
+
import warnings
|
| 14 |
+
import time
|
| 15 |
+
import threading
|
| 16 |
+
import datetime
|
| 17 |
+
import unicodedata
|
| 18 |
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 19 |
+
from fastapi import FastAPI, Request
|
| 20 |
+
from twilio.rest import Client
|
| 21 |
+
from twilio.twiml.messaging_response import MessagingResponse
|
| 22 |
+
|
| 23 |
+
# Yeni moduller - Basit sistem
|
| 24 |
+
from prompts import get_active_prompts
|
| 25 |
+
from whatsapp_renderer import extract_product_info_whatsapp
|
| 26 |
+
from whatsapp_passive_profiler import (
|
| 27 |
+
analyze_user_message, get_user_profile_summary, get_personalized_recommendations
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
# LOGGING EN BASA EKLENDI
|
| 31 |
+
import logging
|
| 32 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 33 |
+
logger = logging.getLogger(__name__)
|
| 34 |
+
|
| 35 |
+
# Import improved WhatsApp search for BF space
|
| 36 |
+
# DISABLED - Using GPT-5 smart warehouse search instead
|
| 37 |
+
USE_IMPROVED_SEARCH = False
|
| 38 |
+
|
| 39 |
+
warnings.simplefilter('ignore')
|
| 40 |
+
|
| 41 |
+
# ===============================
|
| 42 |
+
# MODEL KONFIGURASYONU
|
| 43 |
+
# ===============================
|
| 44 |
+
MODEL_CONFIG = {
|
| 45 |
+
"vision": "gpt-4o", # Gorsel analizi icin (Vision destekli)
|
| 46 |
+
"text": "gpt-5.2-chat-latest", # Metin icin (en akilli model)
|
| 47 |
+
"fallback": "gpt-4o" # Yedek model (GPT-5.2 hata verirse)
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
# Model secimi icin yardimci fonksiyon
|
| 51 |
+
def get_model_for_request(has_media=False):
|
| 52 |
+
"""
|
| 53 |
+
Istek tipine gore uygun modeli sec
|
| 54 |
+
has_media=True -> GPT-4o (vision destekli)
|
| 55 |
+
has_media=False -> GPT-5.2 (daha akilli metin isleme)
|
| 56 |
+
"""
|
| 57 |
+
if has_media:
|
| 58 |
+
logger.info(f"🖼️ Gorsel tespit edildi -> Model: {MODEL_CONFIG['vision']}")
|
| 59 |
+
return MODEL_CONFIG["vision"]
|
| 60 |
+
else:
|
| 61 |
+
logger.info(f"📝 Metin mesaji -> Model: {MODEL_CONFIG['text']}")
|
| 62 |
+
return MODEL_CONFIG["text"]
|
| 63 |
+
|
| 64 |
+
# ===============================
|
| 65 |
+
# API AYARLARI
|
| 66 |
+
# ===============================
|
| 67 |
+
API_URL = "https://api.openai.com/v1/chat/completions"
|
| 68 |
+
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
| 69 |
+
logger.info(f"OpenAI API Key var mi: {'Evet' if OPENAI_API_KEY else 'Hayir'}")
|
| 70 |
+
|
| 71 |
+
# Twilio WhatsApp ayarlari
|
| 72 |
+
TWILIO_ACCOUNT_SID = os.getenv("TWILIO_ACCOUNT_SID")
|
| 73 |
+
TWILIO_AUTH_TOKEN = os.getenv("TWILIO_AUTH_TOKEN")
|
| 74 |
+
TWILIO_MESSAGING_SERVICE_SID = os.getenv("TWILIO_MESSAGING_SERVICE_SID", "MG11c1dfac28ad5f81908ec9ede0f7247f")
|
| 75 |
+
TWILIO_WHATSAPP_NUMBER = "whatsapp:+905332047254" # Bizim WhatsApp Business numaramiz
|
| 76 |
+
|
| 77 |
+
logger.info(f"Twilio SID var mi: {'Evet' if TWILIO_ACCOUNT_SID else 'Hayir'}")
|
| 78 |
+
logger.info(f"Twilio Auth Token var mi: {'Evet' if TWILIO_AUTH_TOKEN else 'Hayir'}")
|
| 79 |
+
logger.info(f"Messaging Service SID var mi: {'Evet' if TWILIO_MESSAGING_SERVICE_SID else 'Hayir'}")
|
| 80 |
+
|
| 81 |
+
if not TWILIO_ACCOUNT_SID or not TWILIO_AUTH_TOKEN:
|
| 82 |
+
logger.error("❌ Twilio bilgileri eksik!")
|
| 83 |
+
twilio_client = None
|
| 84 |
+
else:
|
| 85 |
+
try:
|
| 86 |
+
twilio_client = Client(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN)
|
| 87 |
+
logger.info("✅ Twilio client basariyla olusturuldu!")
|
| 88 |
+
except Exception as e:
|
| 89 |
+
logger.error(f"❌ Twilio client hatasi: {e}")
|
| 90 |
+
twilio_client = None
|
| 91 |
+
|
| 92 |
+
# ===============================
|
| 93 |
+
# GPT-5 SMART WAREHOUSE
|
| 94 |
+
# ===============================
|
| 95 |
+
try:
|
| 96 |
+
from smart_warehouse_with_price import get_warehouse_stock_smart_with_price
|
| 97 |
+
USE_GPT5_SEARCH = True
|
| 98 |
+
logger.info("✅ GPT-5 complete smart warehouse with price (BF algorithm) loaded")
|
| 99 |
+
except ImportError:
|
| 100 |
+
USE_GPT5_SEARCH = False
|
| 101 |
+
logger.info("❌ GPT-5 search not available")
|
| 102 |
+
|
| 103 |
+
# Import Media Queue V2
|
| 104 |
+
try:
|
| 105 |
+
from media_queue_v2 import media_queue
|
| 106 |
+
USE_MEDIA_QUEUE = True
|
| 107 |
+
logger.info("✅ Media Queue V2 loaded successfully")
|
| 108 |
+
except ImportError:
|
| 109 |
+
USE_MEDIA_QUEUE = False
|
| 110 |
+
logger.info("❌ Media Queue V2 not available")
|
| 111 |
+
|
| 112 |
+
# Import Store Notification System
|
| 113 |
+
try:
|
| 114 |
+
from store_notification import (
|
| 115 |
+
notify_product_reservation,
|
| 116 |
+
notify_price_inquiry,
|
| 117 |
+
notify_stock_inquiry,
|
| 118 |
+
send_test_notification,
|
| 119 |
+
send_store_notification,
|
| 120 |
+
should_notify_mehmet_bey
|
| 121 |
+
)
|
| 122 |
+
USE_STORE_NOTIFICATION = True
|
| 123 |
+
logger.info("✅ Store Notification System loaded")
|
| 124 |
+
except ImportError:
|
| 125 |
+
USE_STORE_NOTIFICATION = False
|
| 126 |
+
logger.info("❌ Store Notification System not available")
|
| 127 |
+
|
| 128 |
+
# Import Follow-Up System
|
| 129 |
+
try:
|
| 130 |
+
from follow_up_system import (
|
| 131 |
+
FollowUpManager,
|
| 132 |
+
analyze_message_for_follow_up,
|
| 133 |
+
FollowUpType
|
| 134 |
+
)
|
| 135 |
+
USE_FOLLOW_UP = True
|
| 136 |
+
follow_up_manager = FollowUpManager()
|
| 137 |
+
logger.info("✅ Follow-Up System loaded")
|
| 138 |
+
except ImportError:
|
| 139 |
+
USE_FOLLOW_UP = False
|
| 140 |
+
follow_up_manager = None
|
| 141 |
+
logger.info("❌ Follow-Up System not available")
|
| 142 |
+
|
| 143 |
+
# Import Intent Analyzer
|
| 144 |
+
try:
|
| 145 |
+
from intent_analyzer import (
|
| 146 |
+
analyze_customer_intent,
|
| 147 |
+
should_notify_store,
|
| 148 |
+
get_smart_notification_message
|
| 149 |
+
)
|
| 150 |
+
USE_INTENT_ANALYZER = True
|
| 151 |
+
logger.info("✅ GPT-5 Intent Analyzer loaded")
|
| 152 |
+
except ImportError:
|
| 153 |
+
USE_INTENT_ANALYZER = False
|
| 154 |
+
logger.info("❌ Intent Analyzer not available")
|
| 155 |
+
|
| 156 |
+
# ===============================
|
| 157 |
+
# STOK API ENTEGRASYONU
|
| 158 |
+
# ===============================
|
| 159 |
+
STOCK_API_BASE = "https://video.trek-turkey.com/bizimhesap-proxy.php"
|
| 160 |
+
|
| 161 |
+
# Stock cache (5 dakikalik cache)
|
| 162 |
+
stock_cache = {}
|
| 163 |
+
CACHE_DURATION = 300 # 5 dakika (saniye cinsinden)
|
| 164 |
+
|
| 165 |
+
# Turkish character normalization
|
| 166 |
+
turkish_map = {'ı': 'i', 'ğ': 'g', 'ü': 'u', 'ş': 's', 'ö': 'o', 'ç': 'c', 'İ': 'i', 'I': 'i'}
|
| 167 |
+
|
| 168 |
+
def normalize_turkish(text):
|
| 169 |
+
"""Turkce karakterleri normalize et"""
|
| 170 |
+
if not text:
|
| 171 |
+
return ""
|
| 172 |
+
text = unicodedata.normalize('NFD', text)
|
| 173 |
+
text = ''.join(char for char in text if unicodedata.category(char) != 'Mn')
|
| 174 |
+
for tr_char, en_char in turkish_map.items():
|
| 175 |
+
text = text.replace(tr_char, en_char)
|
| 176 |
+
return text.lower()
|
| 177 |
+
|
| 178 |
+
def fetch_warehouse_inventory(warehouse, product_name, search_terms):
|
| 179 |
+
"""Tek bir magazanin stok bilgisini al"""
|
| 180 |
+
try:
|
| 181 |
+
warehouse_id = warehouse['id']
|
| 182 |
+
warehouse_name = warehouse['title']
|
| 183 |
+
|
| 184 |
+
# DSW'yi ayri tut (gelecek stok icin)
|
| 185 |
+
is_dsw = 'DSW' in warehouse_name or 'ÖN SİPARİŞ' in warehouse_name.upper()
|
| 186 |
+
|
| 187 |
+
# Magaza stoklarini al
|
| 188 |
+
inventory_url = f"{STOCK_API_BASE}?action=inventory&warehouse={warehouse_id}&endpoint=inventory/{warehouse_id}"
|
| 189 |
+
inventory_response = requests.get(inventory_url, timeout=3, verify=False)
|
| 190 |
+
|
| 191 |
+
if inventory_response.status_code != 200:
|
| 192 |
+
return None
|
| 193 |
+
|
| 194 |
+
inventory_data = inventory_response.json()
|
| 195 |
+
|
| 196 |
+
# API yanitini kontrol et
|
| 197 |
+
if 'data' not in inventory_data or 'inventory' not in inventory_data['data']:
|
| 198 |
+
return None
|
| 199 |
+
|
| 200 |
+
products_list = inventory_data['data']['inventory']
|
| 201 |
+
|
| 202 |
+
# Beden terimleri kontrolu
|
| 203 |
+
size_terms = ['xs', 's', 'm', 'ml', 'l', 'xl', 'xxl', '2xl', '3xl', 'small', 'medium', 'large']
|
| 204 |
+
size_numbers = ['44', '46', '48', '50', '52', '54', '56', '58', '60']
|
| 205 |
+
|
| 206 |
+
# Arama terimlerinde beden var mi kontrol et
|
| 207 |
+
has_size_query = False
|
| 208 |
+
size_query = None
|
| 209 |
+
for term in search_terms:
|
| 210 |
+
if term in size_terms or term in size_numbers:
|
| 211 |
+
has_size_query = True
|
| 212 |
+
size_query = term
|
| 213 |
+
break
|
| 214 |
+
|
| 215 |
+
# Eger sadece beden sorgusu varsa (or: "m", "xl")
|
| 216 |
+
is_only_size_query = len(search_terms) == 1 and has_size_query
|
| 217 |
+
|
| 218 |
+
# Urunu ara
|
| 219 |
+
warehouse_variants = []
|
| 220 |
+
dsw_stock_count = 0
|
| 221 |
+
|
| 222 |
+
for product in products_list:
|
| 223 |
+
product_title = normalize_turkish(product.get('title', '')).lower()
|
| 224 |
+
original_title = product.get('title', '')
|
| 225 |
+
|
| 226 |
+
# Eger sadece beden sorgusu ise
|
| 227 |
+
if is_only_size_query:
|
| 228 |
+
if size_query in product_title.split() or f'({size_query})' in product_title or f' {size_query} ' in product_title or product_title.endswith(f' {size_query}'):
|
| 229 |
+
qty = int(product.get('qty', 0))
|
| 230 |
+
stock = int(product.get('stock', 0))
|
| 231 |
+
actual_stock = max(qty, stock)
|
| 232 |
+
|
| 233 |
+
if actual_stock > 0:
|
| 234 |
+
if is_dsw:
|
| 235 |
+
dsw_stock_count += actual_stock
|
| 236 |
+
continue
|
| 237 |
+
warehouse_variants.append(f"{original_title}: ✓ Stokta")
|
| 238 |
+
else:
|
| 239 |
+
# Normal urun aramasi
|
| 240 |
+
if has_size_query:
|
| 241 |
+
non_size_terms = [t for t in search_terms if t != size_query]
|
| 242 |
+
product_matches = all(term in product_title for term in non_size_terms)
|
| 243 |
+
size_matches = size_query in product_title.split() or f'({size_query})' in product_title or f' {size_query} ' in product_title or product_title.endswith(f' {size_query}')
|
| 244 |
+
|
| 245 |
+
if product_matches and size_matches:
|
| 246 |
+
qty = int(product.get('qty', 0))
|
| 247 |
+
stock = int(product.get('stock', 0))
|
| 248 |
+
actual_stock = max(qty, stock)
|
| 249 |
+
|
| 250 |
+
if actual_stock > 0:
|
| 251 |
+
if is_dsw:
|
| 252 |
+
dsw_stock_count += actual_stock
|
| 253 |
+
continue
|
| 254 |
+
|
| 255 |
+
variant_info = original_title
|
| 256 |
+
possible_names = [
|
| 257 |
+
product_name.upper(),
|
| 258 |
+
product_name.lower(),
|
| 259 |
+
product_name.title(),
|
| 260 |
+
product_name.upper().replace('I', 'İ'),
|
| 261 |
+
product_name.upper().replace('İ', 'I'),
|
| 262 |
+
]
|
| 263 |
+
|
| 264 |
+
if 'fx sport' in product_name.lower():
|
| 265 |
+
possible_names.extend(['FX Sport AL 3', 'FX SPORT AL 3', 'Fx Sport Al 3'])
|
| 266 |
+
|
| 267 |
+
for possible_name in possible_names:
|
| 268 |
+
variant_info = variant_info.replace(possible_name, '').strip()
|
| 269 |
+
|
| 270 |
+
variant_info = ' '.join(variant_info.split())
|
| 271 |
+
|
| 272 |
+
if variant_info and variant_info != original_title:
|
| 273 |
+
warehouse_variants.append(f"{variant_info}: ✓ Stokta")
|
| 274 |
+
else:
|
| 275 |
+
warehouse_variants.append(f"{original_title}: ✓ Stokta")
|
| 276 |
+
else:
|
| 277 |
+
if all(term in product_title for term in search_terms):
|
| 278 |
+
qty = int(product.get('qty', 0))
|
| 279 |
+
stock = int(product.get('stock', 0))
|
| 280 |
+
actual_stock = max(qty, stock)
|
| 281 |
+
|
| 282 |
+
if actual_stock > 0:
|
| 283 |
+
if is_dsw:
|
| 284 |
+
dsw_stock_count += actual_stock
|
| 285 |
+
continue
|
| 286 |
+
|
| 287 |
+
variant_info = original_title
|
| 288 |
+
possible_names = [
|
| 289 |
+
product_name.upper(),
|
| 290 |
+
product_name.lower(),
|
| 291 |
+
product_name.title(),
|
| 292 |
+
product_name.upper().replace('I', 'İ'),
|
| 293 |
+
product_name.upper().replace('İ', 'I'),
|
| 294 |
+
]
|
| 295 |
+
|
| 296 |
+
if 'fx sport' in product_name.lower():
|
| 297 |
+
possible_names.extend(['FX Sport AL 3', 'FX SPORT AL 3', 'Fx Sport Al 3'])
|
| 298 |
+
|
| 299 |
+
for possible_name in possible_names:
|
| 300 |
+
variant_info = variant_info.replace(possible_name, '').strip()
|
| 301 |
+
|
| 302 |
+
variant_info = ' '.join(variant_info.split())
|
| 303 |
+
|
| 304 |
+
if variant_info and variant_info != original_title:
|
| 305 |
+
warehouse_variants.append(f"{variant_info}: ✓ Stokta")
|
| 306 |
+
else:
|
| 307 |
+
warehouse_variants.append(f"{original_title}: ✓ Stokta")
|
| 308 |
+
|
| 309 |
+
# Sonuc dondur
|
| 310 |
+
if warehouse_variants and not is_dsw:
|
| 311 |
+
return {'warehouse': warehouse_name, 'variants': warehouse_variants, 'is_dsw': False}
|
| 312 |
+
elif dsw_stock_count > 0:
|
| 313 |
+
return {'dsw_stock': dsw_stock_count, 'is_dsw': True}
|
| 314 |
+
|
| 315 |
+
return None
|
| 316 |
+
|
| 317 |
+
except Exception:
|
| 318 |
+
return None
|
| 319 |
+
|
| 320 |
+
def get_realtime_stock_parallel(product_name):
|
| 321 |
+
"""API'den gercek zamanli stok bilgisini cek - Paralel versiyon with cache"""
|
| 322 |
+
try:
|
| 323 |
+
# Cache kontrolu
|
| 324 |
+
cache_key = normalize_turkish(product_name).lower()
|
| 325 |
+
current_time = time.time()
|
| 326 |
+
|
| 327 |
+
if cache_key in stock_cache:
|
| 328 |
+
cached_data, cached_time = stock_cache[cache_key]
|
| 329 |
+
if current_time - cached_time < CACHE_DURATION:
|
| 330 |
+
logger.info(f"Cache'den donduruluyor: {product_name}")
|
| 331 |
+
return cached_data
|
| 332 |
+
|
| 333 |
+
# Once magaza listesini al
|
| 334 |
+
warehouses_url = f"{STOCK_API_BASE}?action=warehouses&endpoint=warehouses"
|
| 335 |
+
warehouses_response = requests.get(warehouses_url, timeout=3, verify=False)
|
| 336 |
+
|
| 337 |
+
if warehouses_response.status_code != 200:
|
| 338 |
+
logger.error(f"Magaza listesi alinamadi: {warehouses_response.status_code}")
|
| 339 |
+
return None
|
| 340 |
+
|
| 341 |
+
warehouses_data = warehouses_response.json()
|
| 342 |
+
|
| 343 |
+
if 'data' not in warehouses_data or 'warehouses' not in warehouses_data['data']:
|
| 344 |
+
logger.error("Magaza verisi bulunamadi")
|
| 345 |
+
return None
|
| 346 |
+
|
| 347 |
+
warehouses = warehouses_data['data']['warehouses']
|
| 348 |
+
|
| 349 |
+
# Urun adini normalize et
|
| 350 |
+
search_terms = normalize_turkish(product_name).lower().split()
|
| 351 |
+
logger.info(f"Aranan urun: {product_name} -> {search_terms}")
|
| 352 |
+
|
| 353 |
+
stock_info = {}
|
| 354 |
+
total_dsw_stock = 0
|
| 355 |
+
total_stock = 0
|
| 356 |
+
|
| 357 |
+
# Paralel olarak tum magazalari sorgula
|
| 358 |
+
with ThreadPoolExecutor(max_workers=10) as executor:
|
| 359 |
+
futures = {
|
| 360 |
+
executor.submit(fetch_warehouse_inventory, warehouse, product_name, search_terms): warehouse
|
| 361 |
+
for warehouse in warehouses
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
for future in as_completed(futures):
|
| 365 |
+
result = future.result()
|
| 366 |
+
if result:
|
| 367 |
+
if result.get('is_dsw'):
|
| 368 |
+
total_dsw_stock += result.get('dsw_stock', 0)
|
| 369 |
+
else:
|
| 370 |
+
warehouse_name = result['warehouse']
|
| 371 |
+
stock_info[warehouse_name] = result['variants']
|
| 372 |
+
total_stock += 1
|
| 373 |
+
|
| 374 |
+
# Sonucu olustur
|
| 375 |
+
if not stock_info:
|
| 376 |
+
if total_dsw_stock > 0:
|
| 377 |
+
result = f"{product_name}: Su anda magazalarda stokta yok, ancak yakinda gelecek. On siparis verebilirsiniz."
|
| 378 |
+
else:
|
| 379 |
+
result = f"{product_name}: Su anda hicbir magazada stokta bulunmuyor."
|
| 380 |
+
else:
|
| 381 |
+
prompt_lines = [f"{product_name} stok durumu:"]
|
| 382 |
+
for warehouse, variants in stock_info.items():
|
| 383 |
+
if isinstance(variants, list):
|
| 384 |
+
prompt_lines.append(f"- {warehouse}:")
|
| 385 |
+
for variant in variants:
|
| 386 |
+
prompt_lines.append(f" • {variant}")
|
| 387 |
+
else:
|
| 388 |
+
prompt_lines.append(f"- {warehouse}: {variants}")
|
| 389 |
+
|
| 390 |
+
if total_stock > 0:
|
| 391 |
+
prompt_lines.append(f"✓ Urun stokta mevcut")
|
| 392 |
+
|
| 393 |
+
result = "\n".join(prompt_lines)
|
| 394 |
+
|
| 395 |
+
# Sonucu cache'e kaydet
|
| 396 |
+
stock_cache[cache_key] = (result, current_time)
|
| 397 |
+
|
| 398 |
+
return result
|
| 399 |
+
|
| 400 |
+
except Exception as e:
|
| 401 |
+
logger.error(f"API hatasi: {e}")
|
| 402 |
+
return None
|
| 403 |
+
|
| 404 |
+
def is_stock_query(message):
|
| 405 |
+
"""Mesajin stok sorgusu olup olmadigini kontrol et - Basit yedek kontrol"""
|
| 406 |
+
# Bu fonksiyon artik sadece yedek olarak kullaniliyor
|
| 407 |
+
# Ana tespit Intent Analyzer tarafindan yapiliyor
|
| 408 |
+
basic_keywords = ['stok', 'stock', 'var mı', 'mevcut']
|
| 409 |
+
message_lower = message.lower()
|
| 410 |
+
return any(keyword in message_lower for keyword in basic_keywords)
|
| 411 |
+
|
| 412 |
+
# ===============================
|
| 413 |
+
# MAGAZA STOK BILGISI CEKME
|
| 414 |
+
# ===============================
|
| 415 |
+
def get_warehouse_stock(product_name):
|
| 416 |
+
"""B2B API'den magaza stok bilgilerini cek - GPT-5 enhanced"""
|
| 417 |
+
# Try GPT-5 complete smart search (BF algorithm)
|
| 418 |
+
if USE_GPT5_SEARCH:
|
| 419 |
+
try:
|
| 420 |
+
gpt5_result = get_warehouse_stock_smart_with_price(product_name)
|
| 421 |
+
if gpt5_result and isinstance(gpt5_result, list):
|
| 422 |
+
if all(isinstance(item, str) for item in gpt5_result):
|
| 423 |
+
return gpt5_result
|
| 424 |
+
warehouse_info = []
|
| 425 |
+
for item in gpt5_result:
|
| 426 |
+
if isinstance(item, dict):
|
| 427 |
+
info = f"📦 {item.get('name', '')}"
|
| 428 |
+
if item.get('variant'):
|
| 429 |
+
info += f" ({item['variant']})"
|
| 430 |
+
if item.get('warehouses'):
|
| 431 |
+
info += f"\n📍 Mevcut: {', '.join(item['warehouses'])}"
|
| 432 |
+
if item.get('price'):
|
| 433 |
+
info += f"\n💰 {item['price']}"
|
| 434 |
+
warehouse_info.append(info)
|
| 435 |
+
else:
|
| 436 |
+
warehouse_info.append(str(item))
|
| 437 |
+
return warehouse_info if warehouse_info else None
|
| 438 |
+
except Exception as e:
|
| 439 |
+
logger.error(f"GPT-5 warehouse search error: {e}")
|
| 440 |
+
|
| 441 |
+
# Fallback to original search
|
| 442 |
+
try:
|
| 443 |
+
import re
|
| 444 |
+
warehouse_url = 'https://video.trek-turkey.com/bizimhesap-warehouse-xml-b2b-api-v2.php'
|
| 445 |
+
response = requests.get(warehouse_url, verify=False, timeout=15)
|
| 446 |
+
|
| 447 |
+
if response.status_code != 200:
|
| 448 |
+
return None
|
| 449 |
+
|
| 450 |
+
root = ET.fromstring(response.content)
|
| 451 |
+
|
| 452 |
+
# Normalize search product name
|
| 453 |
+
search_name = normalize_turkish(product_name.lower().strip())
|
| 454 |
+
search_name = search_name.replace('(2026)', '').replace('(2025)', '').replace(' gen 3', '').replace(' gen', '').strip()
|
| 455 |
+
search_words = search_name.split()
|
| 456 |
+
|
| 457 |
+
best_matches = []
|
| 458 |
+
exact_matches = []
|
| 459 |
+
variant_matches = []
|
| 460 |
+
candidates = []
|
| 461 |
+
|
| 462 |
+
size_color_words = ['s', 'm', 'l', 'xl', 'xs', 'small', 'medium', 'large',
|
| 463 |
+
'turuncu', 'siyah', 'beyaz', 'mavi', 'kirmizi', 'yesil',
|
| 464 |
+
'orange', 'black', 'white', 'blue', 'red', 'green']
|
| 465 |
+
|
| 466 |
+
variant_words = [word for word in search_words if word in size_color_words]
|
| 467 |
+
product_words = [word for word in search_words if word not in size_color_words]
|
| 468 |
+
|
| 469 |
+
is_size_color_query = len(variant_words) > 0 and len(search_words) <= 4
|
| 470 |
+
|
| 471 |
+
if is_size_color_query:
|
| 472 |
+
for product in root.findall('Product'):
|
| 473 |
+
product_name_elem = product.find('ProductName')
|
| 474 |
+
variant_elem = product.find('ProductVariant')
|
| 475 |
+
|
| 476 |
+
if product_name_elem is not None and product_name_elem.text:
|
| 477 |
+
xml_product_name = product_name_elem.text.strip()
|
| 478 |
+
normalized_product_name = normalize_turkish(xml_product_name.lower())
|
| 479 |
+
|
| 480 |
+
product_name_matches = True
|
| 481 |
+
if product_words:
|
| 482 |
+
product_name_matches = all(word in normalized_product_name for word in product_words)
|
| 483 |
+
|
| 484 |
+
if product_name_matches:
|
| 485 |
+
if variant_elem is not None and variant_elem.text:
|
| 486 |
+
variant_text = normalize_turkish(variant_elem.text.lower().replace('-', ' '))
|
| 487 |
+
|
| 488 |
+
if all(word in variant_text for word in variant_words):
|
| 489 |
+
variant_matches.append((product, xml_product_name, variant_text))
|
| 490 |
+
|
| 491 |
+
if variant_matches:
|
| 492 |
+
candidates = variant_matches
|
| 493 |
+
else:
|
| 494 |
+
is_size_color_query = False
|
| 495 |
+
|
| 496 |
+
if not is_size_color_query or not candidates:
|
| 497 |
+
for product in root.findall('Product'):
|
| 498 |
+
product_name_elem = product.find('ProductName')
|
| 499 |
+
if product_name_elem is not None and product_name_elem.text:
|
| 500 |
+
xml_product_name = product_name_elem.text.strip()
|
| 501 |
+
normalized_xml = normalize_turkish(xml_product_name.lower())
|
| 502 |
+
normalized_xml = normalized_xml.replace('(2026)', '').replace('(2025)', '').replace(' gen 3', '').replace(' gen', '').strip()
|
| 503 |
+
xml_words = normalized_xml.split()
|
| 504 |
+
|
| 505 |
+
if len(search_words) >= 2 and len(xml_words) >= 2:
|
| 506 |
+
search_key = f"{search_words[0]} {search_words[1]}"
|
| 507 |
+
xml_key = f"{xml_words[0]} {xml_words[1]}"
|
| 508 |
+
|
| 509 |
+
if search_key == xml_key:
|
| 510 |
+
exact_matches.append((product, xml_product_name, normalized_xml))
|
| 511 |
+
|
| 512 |
+
if not candidates:
|
| 513 |
+
candidates = exact_matches if exact_matches else []
|
| 514 |
+
|
| 515 |
+
if not candidates:
|
| 516 |
+
for product in root.findall('Product'):
|
| 517 |
+
product_name_elem = product.find('ProductName')
|
| 518 |
+
if product_name_elem is not None and product_name_elem.text:
|
| 519 |
+
xml_product_name = product_name_elem.text.strip()
|
| 520 |
+
normalized_xml = normalize_turkish(xml_product_name.lower())
|
| 521 |
+
normalized_xml = normalized_xml.replace('(2026)', '').replace('(2025)', '').replace(' gen 3', '').replace(' gen', '').strip()
|
| 522 |
+
xml_words = normalized_xml.split()
|
| 523 |
+
|
| 524 |
+
common_words = set(search_words) & set(xml_words)
|
| 525 |
+
|
| 526 |
+
if (len(common_words) >= 2 and
|
| 527 |
+
len(search_words) > 0 and len(xml_words) > 0 and
|
| 528 |
+
search_words[0] == xml_words[0]):
|
| 529 |
+
best_matches.append((product, xml_product_name, normalized_xml, len(common_words)))
|
| 530 |
+
|
| 531 |
+
if best_matches:
|
| 532 |
+
max_common = max(match[3] for match in best_matches)
|
| 533 |
+
candidates = [(match[0], match[1], match[2]) for match in best_matches if match[3] == max_common]
|
| 534 |
+
|
| 535 |
+
warehouse_stock_map = {}
|
| 536 |
+
|
| 537 |
+
for product, xml_name, _ in candidates:
|
| 538 |
+
for warehouse in product.findall('Warehouse'):
|
| 539 |
+
name_elem = warehouse.find('Name')
|
| 540 |
+
stock_elem = warehouse.find('Stock')
|
| 541 |
+
|
| 542 |
+
if name_elem is not None and stock_elem is not None:
|
| 543 |
+
warehouse_name = name_elem.text if name_elem.text else "Bilinmeyen"
|
| 544 |
+
try:
|
| 545 |
+
stock_count = int(stock_elem.text) if stock_elem.text else 0
|
| 546 |
+
if stock_count > 0:
|
| 547 |
+
if warehouse_name in warehouse_stock_map:
|
| 548 |
+
warehouse_stock_map[warehouse_name] += stock_count
|
| 549 |
+
else:
|
| 550 |
+
warehouse_stock_map[warehouse_name] = stock_count
|
| 551 |
+
except (ValueError, TypeError):
|
| 552 |
+
pass
|
| 553 |
+
|
| 554 |
+
if warehouse_stock_map:
|
| 555 |
+
all_warehouse_info = []
|
| 556 |
+
for warehouse_name, total_stock in warehouse_stock_map.items():
|
| 557 |
+
all_warehouse_info.append(f"{warehouse_name}: Stokta var")
|
| 558 |
+
return all_warehouse_info
|
| 559 |
+
else:
|
| 560 |
+
return ["Hicbir magazada stokta bulunmuyor"]
|
| 561 |
+
|
| 562 |
+
except Exception as e:
|
| 563 |
+
logger.error(f"Magaza stok bilgisi cekme hatasi: {e}")
|
| 564 |
+
return None
|
| 565 |
+
|
| 566 |
+
# ===============================
|
| 567 |
+
# TREK BISIKLET URUNLERINI CEKME
|
| 568 |
+
# ===============================
|
| 569 |
+
try:
|
| 570 |
+
url = 'https://www.trekbisiklet.com.tr/output/8582384479'
|
| 571 |
+
response = requests.get(url, verify=False, timeout=10)
|
| 572 |
+
root = ET.fromstring(response.content)
|
| 573 |
+
all_items = root.findall('item')
|
| 574 |
+
|
| 575 |
+
products = []
|
| 576 |
+
|
| 577 |
+
for item in all_items:
|
| 578 |
+
stock_number = 0
|
| 579 |
+
stock_amount = "stokta degil"
|
| 580 |
+
price = ""
|
| 581 |
+
price_eft = ""
|
| 582 |
+
product_link = ""
|
| 583 |
+
picture_url = ""
|
| 584 |
+
category_tree = ""
|
| 585 |
+
category_label = ""
|
| 586 |
+
stock_code = ""
|
| 587 |
+
root_product_stock_code = ""
|
| 588 |
+
is_option_of_product = "0"
|
| 589 |
+
is_optioned_product = "0"
|
| 590 |
+
|
| 591 |
+
rootlabel = item.find('rootlabel')
|
| 592 |
+
if rootlabel is None or not rootlabel.text:
|
| 593 |
+
continue
|
| 594 |
+
|
| 595 |
+
full_name = rootlabel.text.strip()
|
| 596 |
+
name_words = full_name.lower().split()
|
| 597 |
+
name = name_words[0] if name_words else "unknown"
|
| 598 |
+
|
| 599 |
+
# STOK KONTROLU - SAYISAL KARSILASTIRMA
|
| 600 |
+
stock_element = item.find('stockAmount')
|
| 601 |
+
if stock_element is not None and stock_element.text:
|
| 602 |
+
try:
|
| 603 |
+
stock_number = int(stock_element.text.strip())
|
| 604 |
+
stock_amount = "stokta" if stock_number > 0 else "stokta degil"
|
| 605 |
+
except (ValueError, TypeError):
|
| 606 |
+
stock_number = 0
|
| 607 |
+
stock_amount = "stokta degil"
|
| 608 |
+
|
| 609 |
+
# Urun linki - HER URUN ICIN AL
|
| 610 |
+
link_element = item.find('productLink')
|
| 611 |
+
product_link = link_element.text if link_element is not None and link_element.text else ""
|
| 612 |
+
|
| 613 |
+
# Urun resmi - HER URUN ICIN AL
|
| 614 |
+
picture_element = item.find('picture1Path')
|
| 615 |
+
picture_url = picture_element.text if picture_element is not None and picture_element.text else ""
|
| 616 |
+
|
| 617 |
+
# Kategori bilgileri - HER URUN ICIN AL
|
| 618 |
+
category_tree_element = item.find('categoryTree')
|
| 619 |
+
category_tree = category_tree_element.text if category_tree_element is not None and category_tree_element.text else ""
|
| 620 |
+
|
| 621 |
+
category_label_element = item.find('productCategoryLabel')
|
| 622 |
+
category_label = category_label_element.text if category_label_element is not None and category_label_element.text else ""
|
| 623 |
+
|
| 624 |
+
# Stock Code (SKU) - HER URUN ICIN AL
|
| 625 |
+
stock_code_element = item.find('stockCode')
|
| 626 |
+
stock_code = stock_code_element.text if stock_code_element is not None and stock_code_element.text else ""
|
| 627 |
+
|
| 628 |
+
# Variant/main product iliski alanlari
|
| 629 |
+
root_product_stock_code_element = item.find('rootProductStockCode')
|
| 630 |
+
root_product_stock_code = root_product_stock_code_element.text if root_product_stock_code_element is not None and root_product_stock_code_element.text else ""
|
| 631 |
+
|
| 632 |
+
is_option_of_product_element = item.find('isOptionOfAProduct')
|
| 633 |
+
is_option_of_product = is_option_of_product_element.text if is_option_of_product_element is not None and is_option_of_product_element.text else "0"
|
| 634 |
+
|
| 635 |
+
is_optioned_product_element = item.find('isOptionedProduct')
|
| 636 |
+
is_optioned_product = is_optioned_product_element.text if is_optioned_product_element is not None and is_optioned_product_element.text else "0"
|
| 637 |
+
|
| 638 |
+
# Stokta olan urunler icin fiyat bilgilerini al
|
| 639 |
+
if stock_amount == "stokta":
|
| 640 |
+
# Normal fiyat
|
| 641 |
+
price_element = item.find('priceTaxWithCur')
|
| 642 |
+
price_str = price_element.text if price_element is not None and price_element.text else "0"
|
| 643 |
+
|
| 644 |
+
# Kampanya fiyati
|
| 645 |
+
price_rebate_element = item.find('priceRebateWithTax')
|
| 646 |
+
price_rebate_str = price_rebate_element.text if price_rebate_element is not None and price_rebate_element.text else ""
|
| 647 |
+
|
| 648 |
+
final_price_str = price_str
|
| 649 |
+
if price_rebate_str:
|
| 650 |
+
try:
|
| 651 |
+
normal_price = float(price_str)
|
| 652 |
+
rebate_price = float(price_rebate_str)
|
| 653 |
+
if rebate_price < normal_price:
|
| 654 |
+
final_price_str = price_rebate_str
|
| 655 |
+
except (ValueError, TypeError):
|
| 656 |
+
final_price_str = price_str
|
| 657 |
+
|
| 658 |
+
# EFT fiyati
|
| 659 |
+
price_eft_element = item.find('priceEft')
|
| 660 |
+
price_eft_str = price_eft_element.text if price_eft_element is not None and price_eft_element.text else ""
|
| 661 |
+
|
| 662 |
+
# Fiyat formatting
|
| 663 |
+
try:
|
| 664 |
+
price_float = float(final_price_str)
|
| 665 |
+
if price_float > 200000:
|
| 666 |
+
price = str(round(price_float / 5000) * 5000)
|
| 667 |
+
elif price_float > 30000:
|
| 668 |
+
price = str(round(price_float / 1000) * 1000)
|
| 669 |
+
elif price_float > 10000:
|
| 670 |
+
price = str(round(price_float / 100) * 100)
|
| 671 |
+
else:
|
| 672 |
+
price = str(round(price_float / 10) * 10)
|
| 673 |
+
except (ValueError, TypeError):
|
| 674 |
+
price = final_price_str
|
| 675 |
+
|
| 676 |
+
# EFT fiyat formatting
|
| 677 |
+
if price_eft_str:
|
| 678 |
+
try:
|
| 679 |
+
price_eft_float = float(price_eft_str)
|
| 680 |
+
if price_eft_float > 200000:
|
| 681 |
+
price_eft = str(round(price_eft_float / 5000) * 5000)
|
| 682 |
+
elif price_eft_float > 30000:
|
| 683 |
+
price_eft = str(round(price_eft_float / 1000) * 1000)
|
| 684 |
+
elif price_eft_float > 10000:
|
| 685 |
+
price_eft = str(round(price_eft_float / 100) * 100)
|
| 686 |
+
else:
|
| 687 |
+
price_eft = str(round(price_eft_float / 10) * 10)
|
| 688 |
+
except (ValueError, TypeError):
|
| 689 |
+
price_eft = price_eft_str
|
| 690 |
+
else:
|
| 691 |
+
try:
|
| 692 |
+
price_eft_float = float(price_str)
|
| 693 |
+
price_eft = str(round(price_eft_float * 0.975 / 10) * 10)
|
| 694 |
+
except:
|
| 695 |
+
price_eft = ""
|
| 696 |
+
|
| 697 |
+
# Urun bilgilerini tuple olarak olustur
|
| 698 |
+
item_info = (stock_amount, price, product_link, price_eft, str(stock_number),
|
| 699 |
+
picture_url, category_tree, category_label, stock_code,
|
| 700 |
+
root_product_stock_code, is_option_of_product, is_optioned_product)
|
| 701 |
+
products.append((name, item_info, full_name))
|
| 702 |
+
|
| 703 |
+
logger.info(f"✅ {len(products)} urun yuklendi")
|
| 704 |
+
|
| 705 |
+
except Exception as e:
|
| 706 |
+
logger.error(f"❌ Urun yukleme hatasi: {e}")
|
| 707 |
+
import traceback
|
| 708 |
+
traceback.print_exc()
|
| 709 |
+
products = []
|
| 710 |
+
|
| 711 |
+
# ===============================
|
| 712 |
+
# SISTEM MESAJLARI
|
| 713 |
+
# ===============================
|
| 714 |
+
def get_system_messages():
|
| 715 |
+
"""Sistem mesajlarini yukle - Moduler prompts'tan"""
|
| 716 |
+
try:
|
| 717 |
+
return get_active_prompts()
|
| 718 |
+
except:
|
| 719 |
+
# Fallback sistem mesajlari
|
| 720 |
+
return [
|
| 721 |
+
{"role": "system", "content": "Sen Trek bisiklet uzmani AI asistanisin. Trek ve Electra bisikletler konusunda uzmansin. Stokta bulunan urunlerin fiyat bilgilerini verebilirsin."}
|
| 722 |
+
]
|
| 723 |
+
|
| 724 |
+
# ===============================
|
| 725 |
+
# SOHBET HAFIZASI SISTEMI
|
| 726 |
+
# ===============================
|
| 727 |
+
conversation_memory = {}
|
| 728 |
+
|
| 729 |
+
def get_conversation_context(phone_number):
|
| 730 |
+
"""Kullanicinin sohbet gecmisini getir"""
|
| 731 |
+
if phone_number not in conversation_memory:
|
| 732 |
+
conversation_memory[phone_number] = {
|
| 733 |
+
"messages": [],
|
| 734 |
+
"current_category": None,
|
| 735 |
+
"current_product": None,
|
| 736 |
+
"current_product_link": None,
|
| 737 |
+
"current_product_price": None,
|
| 738 |
+
"last_activity": None
|
| 739 |
+
}
|
| 740 |
+
return conversation_memory[phone_number]
|
| 741 |
+
|
| 742 |
+
def add_to_conversation(phone_number, user_message, ai_response):
|
| 743 |
+
"""Sohbet gecmisine ekle"""
|
| 744 |
+
context = get_conversation_context(phone_number)
|
| 745 |
+
context["last_activity"] = datetime.datetime.now()
|
| 746 |
+
|
| 747 |
+
context["messages"].append({
|
| 748 |
+
"user": user_message,
|
| 749 |
+
"ai": ai_response,
|
| 750 |
+
"timestamp": datetime.datetime.now()
|
| 751 |
+
})
|
| 752 |
+
|
| 753 |
+
# Sadece son 10 mesaji tut
|
| 754 |
+
if len(context["messages"]) > 10:
|
| 755 |
+
context["messages"] = context["messages"][-10:]
|
| 756 |
+
|
| 757 |
+
detect_category(phone_number, user_message, ai_response)
|
| 758 |
+
|
| 759 |
+
def detect_category(phone_number, user_message, ai_response):
|
| 760 |
+
"""Konusulan kategoriyi ve tam urun adini tespit et"""
|
| 761 |
+
context = get_conversation_context(phone_number)
|
| 762 |
+
|
| 763 |
+
categories = {
|
| 764 |
+
"marlin": ["marlin", "marlin+"],
|
| 765 |
+
"madone": ["madone"],
|
| 766 |
+
"emonda": ["emonda", "émonda"],
|
| 767 |
+
"domane": ["domane"],
|
| 768 |
+
"checkpoint": ["checkpoint"],
|
| 769 |
+
"fuel": ["fuel", "fuel ex", "fuel exe"],
|
| 770 |
+
"procaliber": ["procaliber"],
|
| 771 |
+
"supercaliber": ["supercaliber"],
|
| 772 |
+
"fx": ["fx"],
|
| 773 |
+
"ds": ["ds", "dual sport"],
|
| 774 |
+
"powerfly": ["powerfly"],
|
| 775 |
+
"rail": ["rail"],
|
| 776 |
+
"verve": ["verve"],
|
| 777 |
+
"townie": ["townie"]
|
| 778 |
+
}
|
| 779 |
+
|
| 780 |
+
user_lower = user_message.lower()
|
| 781 |
+
for category, keywords in categories.items():
|
| 782 |
+
for keyword in keywords:
|
| 783 |
+
if keyword in user_lower:
|
| 784 |
+
context["current_category"] = category
|
| 785 |
+
|
| 786 |
+
# TAM URUN ADINI CIKAR - ornegin "Marlin 5", "Madone SLR 9"
|
| 787 |
+
# Model numarasini da yakala
|
| 788 |
+
import re
|
| 789 |
+
# Keyword + sayi pattern'i ara (ornegin "marlin 5", "madone slr 9")
|
| 790 |
+
pattern = rf'{keyword}\s*\+?\s*(?:slr\s*)?(\d+)?'
|
| 791 |
+
match = re.search(pattern, user_lower)
|
| 792 |
+
if match:
|
| 793 |
+
model_num = match.group(1)
|
| 794 |
+
if model_num:
|
| 795 |
+
# Tam urun adi: "marlin 5" veya "madone slr 9"
|
| 796 |
+
full_product = match.group(0).strip()
|
| 797 |
+
context["current_product"] = full_product
|
| 798 |
+
else:
|
| 799 |
+
# Sadece kategori adi
|
| 800 |
+
context["current_product"] = keyword
|
| 801 |
+
else:
|
| 802 |
+
context["current_product"] = keyword
|
| 803 |
+
|
| 804 |
+
return category
|
| 805 |
+
|
| 806 |
+
return context.get("current_category")
|
| 807 |
+
|
| 808 |
+
def build_context_messages(phone_number, current_message):
|
| 809 |
+
"""Sohbet gecmisi ile sistem mesajlarini olustur"""
|
| 810 |
+
context = get_conversation_context(phone_number)
|
| 811 |
+
system_messages = get_system_messages()
|
| 812 |
+
|
| 813 |
+
# Mevcut kategori varsa, sistem mesajina ekle - GUCLENDIRILMIS BAGLAIM
|
| 814 |
+
if context.get("current_category"):
|
| 815 |
+
cat = context['current_category'].upper()
|
| 816 |
+
category_msg = f"""KRITIK BAGLAIM BILGISI:
|
| 817 |
+
Musteri su anda {cat} modelleri hakkinda konusuyor.
|
| 818 |
+
Butun sorulari bu baglamda cevapla.
|
| 819 |
+
"Hangi model var", "stok var mi", "fiyat ne" gibi sorular {cat} icin sorulmus demektir.
|
| 820 |
+
DS, FX, Verve gibi BASKA kategorilerden bahsetme - sadece {cat} hakkinda konusuyoruz!"""
|
| 821 |
+
system_messages.append({"role": "system", "content": category_msg})
|
| 822 |
+
|
| 823 |
+
# Son konusulan urun bilgilerini ekle (link, fiyat sorulari icin)
|
| 824 |
+
if context.get("current_product"):
|
| 825 |
+
product_context = f"Son konusulan urun: {context['current_product']}"
|
| 826 |
+
if context.get("current_product_link"):
|
| 827 |
+
product_context += f"\nUrun linki: {context['current_product_link']}"
|
| 828 |
+
if context.get("current_product_price"):
|
| 829 |
+
product_context += f"\nUrun fiyati: {context['current_product_price']}"
|
| 830 |
+
system_messages.append({"role": "system", "content": product_context})
|
| 831 |
+
|
| 832 |
+
# Son 3 mesaj alisverisini ekle
|
| 833 |
+
recent_messages = context["messages"][-3:] if context["messages"] else []
|
| 834 |
+
|
| 835 |
+
all_messages = system_messages.copy()
|
| 836 |
+
|
| 837 |
+
# Gecmis mesajlari ekle
|
| 838 |
+
for msg in recent_messages:
|
| 839 |
+
all_messages.append({"role": "user", "content": msg["user"]})
|
| 840 |
+
all_messages.append({"role": "assistant", "content": msg["ai"]})
|
| 841 |
+
|
| 842 |
+
# Mevcut mesaji ekle
|
| 843 |
+
all_messages.append({"role": "user", "content": current_message})
|
| 844 |
+
|
| 845 |
+
return all_messages
|
| 846 |
+
|
| 847 |
+
# ===============================
|
| 848 |
+
# HYBRID MODEL MESAJ ISLEME
|
| 849 |
+
# ===============================
|
| 850 |
+
|
| 851 |
+
|
| 852 |
+
def extract_product_from_vision_response(response):
|
| 853 |
+
"""
|
| 854 |
+
GPT Vision yanitindan urun adini cikarir
|
| 855 |
+
Ornek: "Trek Domane+ SLR 7 AXS" -> "Domane+ SLR 7 AXS"
|
| 856 |
+
"""
|
| 857 |
+
import re
|
| 858 |
+
|
| 859 |
+
# Bilinen Trek model pattern'leri
|
| 860 |
+
patterns = [
|
| 861 |
+
# Trek Domane+ SLR 7 AXS gibi tam isimler
|
| 862 |
+
r'Trek\s+((?:Domane|Madone|Emonda|Marlin|Fuel|Rail|Powerfly|Checkpoint|FX|Verve|Dual\s*Sport|Procaliber|Supercaliber|Roscoe|Top\s*Fuel|Slash|Remedy|X-?Caliber|Allant)[+]?\s*(?:SLR|SL|Gen|EX|EXe)?\s*\d*\s*(?:AXS|Di2|eTap|Frameset)?)',
|
| 863 |
+
# Sadece model adi + numara
|
| 864 |
+
r'((?:Domane|Madone|Emonda|Marlin|Fuel|Rail|Powerfly|Checkpoint|FX|Verve|Dual\s*Sport|Procaliber|Supercaliber|Roscoe|Top\s*Fuel|Slash|Remedy|X-?Caliber|Allant)[+]?\s*(?:SLR|SL|Gen|EX|EXe)?\s*\d+\s*(?:AXS|Di2|eTap|Frameset)?)',
|
| 865 |
+
# Model + SLR/SL + numara
|
| 866 |
+
r'((?:Domane|Madone|Emonda)[+]?\s+(?:SLR|SL)\s*\d+)',
|
| 867 |
+
# Marlin + numara
|
| 868 |
+
r'(Marlin\s*\d+)',
|
| 869 |
+
# FX + numara
|
| 870 |
+
r'(FX\s*(?:Sport)?\s*\d+)',
|
| 871 |
+
]
|
| 872 |
+
|
| 873 |
+
response_lower = response.lower()
|
| 874 |
+
|
| 875 |
+
for pattern in patterns:
|
| 876 |
+
match = re.search(pattern, response, re.IGNORECASE)
|
| 877 |
+
if match:
|
| 878 |
+
product = match.group(1).strip()
|
| 879 |
+
# "Trek " prefix varsa kaldir
|
| 880 |
+
product = re.sub(r'^Trek\s+', '', product, flags=re.IGNORECASE)
|
| 881 |
+
return product
|
| 882 |
+
|
| 883 |
+
return None
|
| 884 |
+
|
| 885 |
+
|
| 886 |
+
def process_whatsapp_message_with_media(user_message, phone_number, media_urls, media_types):
|
| 887 |
+
"""
|
| 888 |
+
GORSEL MESAJ ISLEME - GPT-4o Vision kullanir
|
| 889 |
+
"""
|
| 890 |
+
try:
|
| 891 |
+
logger.info(f"🖼️ Gorsel analizi basliyor: {len(media_urls)} medya")
|
| 892 |
+
logger.info(f"📎 Medya URL'leri: {media_urls}")
|
| 893 |
+
logger.info(f"📎 Medya tipleri: {media_types}")
|
| 894 |
+
|
| 895 |
+
# Pasif profil analizi
|
| 896 |
+
try:
|
| 897 |
+
profile_analysis = analyze_user_message(phone_number, user_message)
|
| 898 |
+
logger.info(f"📊 Profil analizi: {phone_number} -> {profile_analysis}")
|
| 899 |
+
except:
|
| 900 |
+
pass
|
| 901 |
+
|
| 902 |
+
# Gorsel icin GPT-4o kullan
|
| 903 |
+
model = get_model_for_request(has_media=True)
|
| 904 |
+
|
| 905 |
+
# Sohbet gecmisi ile sistem mesajlarini olustur
|
| 906 |
+
messages = build_context_messages(phone_number, user_message if user_message else "Gonderilen gorseli analiz et")
|
| 907 |
+
|
| 908 |
+
# GPT-4o Vision icin mesaj hazirla
|
| 909 |
+
vision_message = {
|
| 910 |
+
"role": "user",
|
| 911 |
+
"content": []
|
| 912 |
+
}
|
| 913 |
+
|
| 914 |
+
# Metin mesaji varsa ekle
|
| 915 |
+
# KRITIK: Kisa sorularda (var mi, fiyat, stok) gorseli dikkate alarak cevap vermesini sagla
|
| 916 |
+
if user_message and user_message.strip():
|
| 917 |
+
short_questions = ['var mi', 'var mı', 'stok', 'fiyat', 'kac', 'kaç', 'ne kadar', 'beden', 'renk']
|
| 918 |
+
is_short_question = len(user_message.strip()) < 20 and any(q in user_message.lower() for q in short_questions)
|
| 919 |
+
|
| 920 |
+
if is_short_question:
|
| 921 |
+
# Kisa soru - gorseli referans alarak cevap vermesini iste
|
| 922 |
+
enhanced_text = f"Gorseldeki BISIKLETI dikkatlice incele ve model adini tespit et. Musteri bu bisiklet icin '{user_message}' soruyor. Gorseldeki bisikletin TAM MODEL ADINI (ornegin 'Trek Domane+ SLR 7 AXS') belirle ve buna gore cevap ver."
|
| 923 |
+
vision_message["content"].append({
|
| 924 |
+
"type": "text",
|
| 925 |
+
"text": enhanced_text
|
| 926 |
+
})
|
| 927 |
+
else:
|
| 928 |
+
vision_message["content"].append({
|
| 929 |
+
"type": "text",
|
| 930 |
+
"text": user_message
|
| 931 |
+
})
|
| 932 |
+
else:
|
| 933 |
+
vision_message["content"].append({
|
| 934 |
+
"type": "text",
|
| 935 |
+
"text": "Bu gorselde ne var? Eger bisiklet veya bisiklet parcasi ise detayli acikla."
|
| 936 |
+
})
|
| 937 |
+
# Medya URL'lerini isle
|
| 938 |
+
valid_images = 0
|
| 939 |
+
for i, media_url in enumerate(media_urls):
|
| 940 |
+
media_type = media_types[i] if i < len(media_types) else "image/jpeg"
|
| 941 |
+
|
| 942 |
+
# Sadece gorsel medyalari isle
|
| 943 |
+
if media_type and media_type.startswith('image/'):
|
| 944 |
+
try:
|
| 945 |
+
# Twilio medya URL'sini proxy uzerinden cevir
|
| 946 |
+
if 'api.twilio.com' in media_url:
|
| 947 |
+
import re
|
| 948 |
+
match = re.search(r'/Messages/([^/]+)/Media/([^/]+)', media_url)
|
| 949 |
+
if match:
|
| 950 |
+
message_sid = match.group(1)
|
| 951 |
+
media_sid = match.group(2)
|
| 952 |
+
proxy_url = f"https://video.trek-turkey.com/twilio-media-proxy.php?action=media&message={message_sid}&media={media_sid}"
|
| 953 |
+
logger.info(f"🔄 Proxy URL olusturuldu: {proxy_url}")
|
| 954 |
+
|
| 955 |
+
# Proxy URL'sinin calisip calismadigini kontrol et
|
| 956 |
+
try:
|
| 957 |
+
test_response = requests.head(proxy_url, timeout=5, verify=False)
|
| 958 |
+
if test_response.status_code == 200:
|
| 959 |
+
vision_message["content"].append({
|
| 960 |
+
"type": "image_url",
|
| 961 |
+
"image_url": {"url": proxy_url}
|
| 962 |
+
})
|
| 963 |
+
valid_images += 1
|
| 964 |
+
logger.info(f"✅ Proxy URL gecerli: {proxy_url}")
|
| 965 |
+
else:
|
| 966 |
+
logger.error(f"❌ Proxy URL calismiyor: {test_response.status_code}")
|
| 967 |
+
vision_message["content"].append({
|
| 968 |
+
"type": "image_url",
|
| 969 |
+
"image_url": {"url": media_url}
|
| 970 |
+
})
|
| 971 |
+
valid_images += 1
|
| 972 |
+
except Exception as proxy_error:
|
| 973 |
+
logger.error(f"❌ Proxy test hatasi: {proxy_error}")
|
| 974 |
+
vision_message["content"].append({
|
| 975 |
+
"type": "image_url",
|
| 976 |
+
"image_url": {"url": media_url}
|
| 977 |
+
})
|
| 978 |
+
valid_images += 1
|
| 979 |
+
else:
|
| 980 |
+
logger.error(f"❌ Twilio URL parse edilemedi: {media_url}")
|
| 981 |
+
else:
|
| 982 |
+
vision_message["content"].append({
|
| 983 |
+
"type": "image_url",
|
| 984 |
+
"image_url": {"url": media_url}
|
| 985 |
+
})
|
| 986 |
+
valid_images += 1
|
| 987 |
+
logger.info(f"✅ Dogrudan URL eklendi: {media_url}")
|
| 988 |
+
except Exception as url_error:
|
| 989 |
+
logger.error(f"❌ URL isleme hatasi: {url_error}")
|
| 990 |
+
else:
|
| 991 |
+
logger.warning(f"⚠️ Gorsel olmayan medya atlandi: {media_type}")
|
| 992 |
+
|
| 993 |
+
# Hic gecerli gorsel yoksa
|
| 994 |
+
if valid_images == 0:
|
| 995 |
+
logger.error("❌ Hic gecerli gorsel bulunamadi")
|
| 996 |
+
return "Gonderdiginiz gorsel islenemedi. Lutfen farkli bir gorsel gonderin veya sorunuzu yazili olarak iletin."
|
| 997 |
+
|
| 998 |
+
logger.info(f"✅ {valid_images} gorsel islenecek")
|
| 999 |
+
|
| 1000 |
+
# Son user mesajini vision mesajiyla degistir
|
| 1001 |
+
messages = [msg for msg in messages if not (msg.get("role") == "user" and msg == messages[-1])]
|
| 1002 |
+
messages.append(vision_message)
|
| 1003 |
+
|
| 1004 |
+
# Sistem mesajina bisiklet tanima talimati ekle
|
| 1005 |
+
bike_recognition_prompt = {
|
| 1006 |
+
"role": "system",
|
| 1007 |
+
"content": """Gonderilen gorselleri dikkatle analiz et:
|
| 1008 |
+
1. Eger bisiklet veya bisiklet parcasi goruyorsan, detaylica tanimla (marka, model, renk, beden, ozellikler)
|
| 1009 |
+
2. Trek bisiklet ise modeli tahmin etmeye calis
|
| 1010 |
+
3. Stok veya fiyat sorulursa, gorseldeki bisikletin ozelliklerini belirterek bilgi ver
|
| 1011 |
+
4. Gorsel net degilse veya tanimlanamiyorsa, kullanicidan daha net bir gorsel istemek yerine, gorselde gorduklerini acikla
|
| 1012 |
+
5. Eger gorsel bisikletle ilgili degilse, ne gordugunu kisaca acikla"""
|
| 1013 |
+
}
|
| 1014 |
+
messages.insert(0, bike_recognition_prompt)
|
| 1015 |
+
|
| 1016 |
+
if not OPENAI_API_KEY:
|
| 1017 |
+
logger.error("❌ OpenAI API anahtari eksik")
|
| 1018 |
+
return "Sistem hatasi olustu. Lutfen daha sonra tekrar deneyin."
|
| 1019 |
+
|
| 1020 |
+
logger.info(f"📤 GPT Vision API'ye gonderiliyor: {len(messages)} mesaj, {valid_images} gorsel")
|
| 1021 |
+
|
| 1022 |
+
payload = {
|
| 1023 |
+
"model": model, # GPT-4o
|
| 1024 |
+
"messages": messages,
|
| 1025 |
+
"max_tokens": 800,
|
| 1026 |
+
"temperature": 0.3
|
| 1027 |
+
}
|
| 1028 |
+
|
| 1029 |
+
headers = {
|
| 1030 |
+
"Content-Type": "application/json",
|
| 1031 |
+
"Authorization": f"Bearer {OPENAI_API_KEY}"
|
| 1032 |
+
}
|
| 1033 |
+
|
| 1034 |
+
response = requests.post(API_URL, headers=headers, json=payload, timeout=30)
|
| 1035 |
+
|
| 1036 |
+
logger.info(f"📥 API yaniti: {response.status_code}")
|
| 1037 |
+
|
| 1038 |
+
if response.status_code == 200:
|
| 1039 |
+
result = response.json()
|
| 1040 |
+
ai_response = result['choices'][0]['message']['content']
|
| 1041 |
+
logger.info(f"✅ Gorsel analizi basarili: {ai_response[:100]}...")
|
| 1042 |
+
|
| 1043 |
+
# KRITIK: Gorselden urun adini cikar ve GERCEK stok kontrolu yap
|
| 1044 |
+
try:
|
| 1045 |
+
# app.py'deki get_warehouse_stock fonksiyonunu kullan
|
| 1046 |
+
|
| 1047 |
+
# GPT yanitindan urun adini cikar
|
| 1048 |
+
product_name = extract_product_from_vision_response(ai_response)
|
| 1049 |
+
|
| 1050 |
+
if product_name:
|
| 1051 |
+
logger.info(f"🔍 Gorselden tespit edilen urun: {product_name}")
|
| 1052 |
+
|
| 1053 |
+
# GERCEK stok kontrolu yap
|
| 1054 |
+
stock_result = get_warehouse_stock(product_name)
|
| 1055 |
+
|
| 1056 |
+
# Sonuc list veya string olabilir
|
| 1057 |
+
stock_info = None
|
| 1058 |
+
if stock_result:
|
| 1059 |
+
if isinstance(stock_result, list):
|
| 1060 |
+
stock_info = chr(10).join(str(item) for item in stock_result)
|
| 1061 |
+
else:
|
| 1062 |
+
stock_info = str(stock_result)
|
| 1063 |
+
|
| 1064 |
+
if stock_info and len(stock_info) > 5:
|
| 1065 |
+
logger.info(f"✅ Stok bilgisi bulundu: {stock_info[:100]}...")
|
| 1066 |
+
|
| 1067 |
+
# GPT yanlis "stokta yok" dediyse duzelt
|
| 1068 |
+
if "stokta bulunmuyor" in ai_response.lower() or "stokta yok" in ai_response.lower():
|
| 1069 |
+
if "stokta bulunmuyor" not in stock_info.lower():
|
| 1070 |
+
# Yanlis bilgiyi kaldir ve dogru stok bilgisini ekle
|
| 1071 |
+
ai_response = re.sub(
|
| 1072 |
+
r'[^.]*stok[^.]*bulunmuyor[^.]*[.]?',
|
| 1073 |
+
'',
|
| 1074 |
+
ai_response,
|
| 1075 |
+
flags=re.IGNORECASE
|
| 1076 |
+
)
|
| 1077 |
+
ai_response = ai_response.strip()
|
| 1078 |
+
|
| 1079 |
+
# Stok bilgisini ekle (eger zaten yoksa)
|
| 1080 |
+
if "Stok:" not in ai_response and "stokta" not in stock_info.lower():
|
| 1081 |
+
ai_response = ai_response + "\n\n" + stock_info
|
| 1082 |
+
|
| 1083 |
+
elif "Stok:" not in ai_response:
|
| 1084 |
+
ai_response = ai_response + "\n\n" + stock_info
|
| 1085 |
+
|
| 1086 |
+
else:
|
| 1087 |
+
logger.info(f"⚠️ Urun icin stok bilgisi bulunamadi: {product_name}")
|
| 1088 |
+
else:
|
| 1089 |
+
logger.info("⚠️ Gorselden urun adi cikarilamadi")
|
| 1090 |
+
except Exception as stock_error:
|
| 1091 |
+
logger.error(f"❌ Vision stok kontrolu hatasi: {stock_error}")
|
| 1092 |
+
|
| 1093 |
+
# Context'e urun adini kaydet (sonraki sorular icin)
|
| 1094 |
+
if product_name:
|
| 1095 |
+
try:
|
| 1096 |
+
ctx = get_conversation_context(phone_number)
|
| 1097 |
+
ctx["current_product"] = product_name
|
| 1098 |
+
ctx["current_category"] = product_name.split()[0].lower()
|
| 1099 |
+
logger.info(f"📝 Vision context kaydedildi: {product_name}")
|
| 1100 |
+
except:
|
| 1101 |
+
pass
|
| 1102 |
+
|
| 1103 |
+
# WhatsApp icin formatla
|
| 1104 |
+
try:
|
| 1105 |
+
formatted_response = extract_product_info_whatsapp(ai_response)
|
| 1106 |
+
except:
|
| 1107 |
+
formatted_response = ai_response
|
| 1108 |
+
|
| 1109 |
+
# Sohbet gecmisine ekle
|
| 1110 |
+
add_to_conversation(phone_number, f"[Gorsel gonderildi] {user_message if user_message else ''}", formatted_response)
|
| 1111 |
+
|
| 1112 |
+
return formatted_response
|
| 1113 |
+
else:
|
| 1114 |
+
error_detail = response.text[:500] if response.text else "Detay yok"
|
| 1115 |
+
logger.error(f"❌ OpenAI API Error: {response.status_code} - {error_detail}")
|
| 1116 |
+
|
| 1117 |
+
if response.status_code == 400:
|
| 1118 |
+
return "Gorsel formati desteklenmiyor. Lutfen JPG veya PNG formatinda bir gorsel gonderin."
|
| 1119 |
+
elif response.status_code == 413:
|
| 1120 |
+
return "Gorsel boyutu cok buyuk. Lutfen daha kucuk bir gorsel gonderin."
|
| 1121 |
+
elif response.status_code == 429:
|
| 1122 |
+
return "Sistem su anda yogun. Lutfen birkac saniye sonra tekrar deneyin."
|
| 1123 |
+
else:
|
| 1124 |
+
return "Gorsel su anda analiz edilemiyor. Sorunuzu yazili olarak iletebilir misiniz?"
|
| 1125 |
+
|
| 1126 |
+
except requests.exceptions.Timeout:
|
| 1127 |
+
logger.error("❌ API timeout hatasi")
|
| 1128 |
+
return "Islem zaman asimina ugradi. Lutfen tekrar deneyin."
|
| 1129 |
+
except Exception as e:
|
| 1130 |
+
logger.error(f"❌ Medya isleme hatasi: {e}")
|
| 1131 |
+
import traceback
|
| 1132 |
+
traceback.print_exc()
|
| 1133 |
+
return "Gorsel islenirken bir sorun olustu. Lutfen sorunuzu yazili olarak iletin veya farkli bir gorsel deneyin."
|
| 1134 |
+
|
| 1135 |
+
|
| 1136 |
+
def process_whatsapp_message_with_memory(user_message, phone_number):
|
| 1137 |
+
"""
|
| 1138 |
+
METIN MESAJ ISLEME - GPT-5.2 kullanir (daha akilli)
|
| 1139 |
+
"""
|
| 1140 |
+
try:
|
| 1141 |
+
# Metin icin GPT-5.2 kullan
|
| 1142 |
+
model = get_model_for_request(has_media=False)
|
| 1143 |
+
|
| 1144 |
+
# Pasif profil analizi
|
| 1145 |
+
try:
|
| 1146 |
+
profile_analysis = analyze_user_message(phone_number, user_message)
|
| 1147 |
+
logger.info(f"📊 Profil analizi: {phone_number}")
|
| 1148 |
+
except:
|
| 1149 |
+
pass
|
| 1150 |
+
|
| 1151 |
+
# 🔔 Yeni Magaza Bildirim Sistemi - Mehmet Bey'e otomatik bildirim
|
| 1152 |
+
if USE_STORE_NOTIFICATION:
|
| 1153 |
+
try:
|
| 1154 |
+
should_notify_mehmet, notification_reason, urgency = should_notify_mehmet_bey(user_message)
|
| 1155 |
+
|
| 1156 |
+
if not should_notify_mehmet and USE_INTENT_ANALYZER:
|
| 1157 |
+
context = get_conversation_context(phone_number)
|
| 1158 |
+
intent_analysis = analyze_customer_intent(user_message, context)
|
| 1159 |
+
should_notify_mehmet, notification_reason, urgency = should_notify_mehmet_bey(user_message, intent_analysis)
|
| 1160 |
+
else:
|
| 1161 |
+
intent_analysis = None
|
| 1162 |
+
|
| 1163 |
+
if should_notify_mehmet:
|
| 1164 |
+
if intent_analysis:
|
| 1165 |
+
product = intent_analysis.get("product") or "Belirtilmemis"
|
| 1166 |
+
else:
|
| 1167 |
+
context = get_conversation_context(phone_number)
|
| 1168 |
+
product = context.get("current_category") or "Urun belirtilmemis"
|
| 1169 |
+
|
| 1170 |
+
if "rezervasyon" in notification_reason.lower() or urgency == "high":
|
| 1171 |
+
action = "reserve"
|
| 1172 |
+
elif "magaza" in notification_reason.lower() or "lokasyon" in notification_reason.lower():
|
| 1173 |
+
action = "info"
|
| 1174 |
+
elif "fiyat" in notification_reason.lower() or "odeme" in notification_reason.lower():
|
| 1175 |
+
action = "price"
|
| 1176 |
+
else:
|
| 1177 |
+
action = "info"
|
| 1178 |
+
|
| 1179 |
+
additional_info = f"{notification_reason}\n\nMusteri Mesaji: '{user_message}'"
|
| 1180 |
+
if urgency == "high":
|
| 1181 |
+
additional_info = "⚠️ YUKSEK ONCELIK ⚠️\n" + additional_info
|
| 1182 |
+
|
| 1183 |
+
result = send_store_notification(
|
| 1184 |
+
customer_phone=phone_number,
|
| 1185 |
+
customer_name=None,
|
| 1186 |
+
product_name=product,
|
| 1187 |
+
action=action,
|
| 1188 |
+
store_name=None,
|
| 1189 |
+
additional_info=additional_info
|
| 1190 |
+
)
|
| 1191 |
+
|
| 1192 |
+
if result:
|
| 1193 |
+
logger.info(f"✅ Mehmet Bey'e bildirim gonderildi!")
|
| 1194 |
+
logger.info(f" 📍 Sebep: {notification_reason}")
|
| 1195 |
+
logger.info(f" ⚡ Oncelik: {urgency}")
|
| 1196 |
+
logger.info(f" 📦 Urun: {product}")
|
| 1197 |
+
|
| 1198 |
+
# TAKIP SISTEMINI KONTROL ET
|
| 1199 |
+
if USE_FOLLOW_UP and follow_up_manager:
|
| 1200 |
+
try:
|
| 1201 |
+
follow_up_analysis = analyze_message_for_follow_up(user_message)
|
| 1202 |
+
if follow_up_analysis and follow_up_analysis.get("needs_follow_up"):
|
| 1203 |
+
follow_up = follow_up_manager.create_follow_up(
|
| 1204 |
+
customer_phone=phone_number,
|
| 1205 |
+
product_name=product,
|
| 1206 |
+
follow_up_type=follow_up_analysis["follow_up_type"],
|
| 1207 |
+
original_message=user_message,
|
| 1208 |
+
follow_up_hours=follow_up_analysis.get("follow_up_hours", 24),
|
| 1209 |
+
notes=follow_up_analysis.get("reason", "")
|
| 1210 |
+
)
|
| 1211 |
+
logger.info(f"📌 Takip olusturuldu: {follow_up_analysis.get('reason', '')}")
|
| 1212 |
+
except Exception as follow_up_error:
|
| 1213 |
+
logger.error(f"Takip sistemi hatasi: {follow_up_error}")
|
| 1214 |
+
except Exception as notify_error:
|
| 1215 |
+
logger.error(f"Bildirim hatasi: {notify_error}")
|
| 1216 |
+
|
| 1217 |
+
# Sohbet gecmisi ile mesajlari olustur
|
| 1218 |
+
messages = build_context_messages(phone_number, user_message)
|
| 1219 |
+
|
| 1220 |
+
# Intent Analyzer veya context'ten urun bilgisini al
|
| 1221 |
+
detected_product = None
|
| 1222 |
+
intent_analysis = None # Scope disinda da kullanilacak
|
| 1223 |
+
if USE_INTENT_ANALYZER:
|
| 1224 |
+
try:
|
| 1225 |
+
context = get_conversation_context(phone_number)
|
| 1226 |
+
intent_analysis = analyze_customer_intent(user_message, context)
|
| 1227 |
+
if intent_analysis and intent_analysis.get("product"):
|
| 1228 |
+
detected_product = intent_analysis.get("product")
|
| 1229 |
+
logger.info(f"🎯 Intent'ten tespit edilen urun: {detected_product}")
|
| 1230 |
+
except Exception as e:
|
| 1231 |
+
logger.error(f"Intent analiz hatasi: {e}")
|
| 1232 |
+
intent_analysis = None
|
| 1233 |
+
|
| 1234 |
+
# Eger Intent'ten urun bulunamadiysa, context'ten al
|
| 1235 |
+
# ONEMLI: current_product TAM URUN ADINI icerir (ornegin "marlin 5")
|
| 1236 |
+
# current_category ise sadece kategori adidir (ornegin "marlin")
|
| 1237 |
+
if not detected_product:
|
| 1238 |
+
context = get_conversation_context(phone_number)
|
| 1239 |
+
# Oncelikle tam urun adini dene
|
| 1240 |
+
if context.get("current_product"):
|
| 1241 |
+
detected_product = context.get("current_product")
|
| 1242 |
+
logger.info(f"🎯 Context'ten tespit edilen TAM URUN: {detected_product}")
|
| 1243 |
+
elif context.get("current_category"):
|
| 1244 |
+
detected_product = context.get("current_category")
|
| 1245 |
+
logger.info(f"🎯 Context'ten tespit edilen kategori: {detected_product}")
|
| 1246 |
+
|
| 1247 |
+
# Stok sorgusu icin kullanilacak urun adi
|
| 1248 |
+
stock_query_product = detected_product if detected_product else user_message
|
| 1249 |
+
|
| 1250 |
+
# Urun bilgilerini ekle
|
| 1251 |
+
input_words = user_message.lower().split()
|
| 1252 |
+
# Detected product'i da input_words'e ekle
|
| 1253 |
+
if detected_product:
|
| 1254 |
+
input_words.extend(detected_product.lower().split())
|
| 1255 |
+
|
| 1256 |
+
# Bulunan urunun link ve gorsel bilgilerini sakla
|
| 1257 |
+
found_product_link = None
|
| 1258 |
+
found_product_image = None
|
| 1259 |
+
found_product_name = None
|
| 1260 |
+
best_match_score = 0
|
| 1261 |
+
|
| 1262 |
+
# Kullanici mesaji veya detected_product'i normalize et
|
| 1263 |
+
search_text = (detected_product or user_message).lower()
|
| 1264 |
+
search_words = search_text.split()
|
| 1265 |
+
|
| 1266 |
+
# AKILLI URUN ESLESTIRME
|
| 1267 |
+
# 1. Kategori bazli filtreleme - Bisiklet aramasinda aksesuar gosterme
|
| 1268 |
+
# 2. Urun tipi benzerligi - Ayni tip urunler oncelikli
|
| 1269 |
+
# 3. Kelime eslesmesi - En cok eslesen urun
|
| 1270 |
+
|
| 1271 |
+
def is_bicycle_search(text):
|
| 1272 |
+
"""Arama bisiklet mi yoksa aksesuar mi?"""
|
| 1273 |
+
bike_indicators = ['madone', 'domane', 'emonda', 'checkpoint', 'fuel', 'slash',
|
| 1274 |
+
'marlin', 'procaliber', 'supercaliber', 'fx', 'verve', 'dual sport',
|
| 1275 |
+
'powerfly', 'rail', 'allant', 'bisiklet', 'bike']
|
| 1276 |
+
text_lower = text.lower()
|
| 1277 |
+
return any(ind in text_lower for ind in bike_indicators)
|
| 1278 |
+
|
| 1279 |
+
def get_product_type(category_tree, product_name):
|
| 1280 |
+
"""Urun tipini belirle"""
|
| 1281 |
+
cat_lower = (category_tree or "").lower()
|
| 1282 |
+
name_lower = (product_name or "").lower()
|
| 1283 |
+
|
| 1284 |
+
# Kategori agacindan tip belirle
|
| 1285 |
+
if 'bisiklet' in cat_lower or 'bike' in cat_lower:
|
| 1286 |
+
if 'yol' in cat_lower or 'road' in cat_lower:
|
| 1287 |
+
return 'road_bike'
|
| 1288 |
+
elif 'dag' in cat_lower or 'mountain' in cat_lower or 'mtb' in cat_lower:
|
| 1289 |
+
return 'mtb'
|
| 1290 |
+
elif 'elektrik' in cat_lower or 'e-bike' in cat_lower:
|
| 1291 |
+
return 'ebike'
|
| 1292 |
+
elif 'sehir' in cat_lower or 'hybrid' in cat_lower:
|
| 1293 |
+
return 'hybrid'
|
| 1294 |
+
return 'bicycle'
|
| 1295 |
+
elif 'aksesuar' in cat_lower or 'parça' in cat_lower or 'parca' in cat_lower or 'accessory' in cat_lower or 'yedek' in cat_lower:
|
| 1296 |
+
return 'accessory'
|
| 1297 |
+
|
| 1298 |
+
# Isimden tip belirle (fallback)
|
| 1299 |
+
# ONEMLI: Aksesuar kontrolu ONCE yapilmali cunku "Domane Kadro Kulagi" gibi
|
| 1300 |
+
# urunlerde model adi gecebilir ama aksesuar kelimeleri varsa aksesuar'dir
|
| 1301 |
+
accessory_keywords = ['sele', 'gidon', 'pedal', 'zincir', 'lastik', 'jant', 'fren', 'vites',
|
| 1302 |
+
'kadro kulağı', 'kadro kulagi', 'kulak', 'kablo', 'kasnak', 'dişli',
|
| 1303 |
+
'zil', 'far', 'lamba', 'pompa', 'kilit', 'çanta', 'canta', 'suluk',
|
| 1304 |
+
'gözlük', 'gozluk', 'kask', 'eldiven', 'ayakkabı', 'ayakkabi',
|
| 1305 |
+
'forma', 'tayt', 'şort', 'sort', 'mont', 'yağmurluk', 'yagmurluk']
|
| 1306 |
+
if any(x in name_lower for x in accessory_keywords):
|
| 1307 |
+
return 'accessory'
|
| 1308 |
+
if any(x in name_lower for x in ['madone', 'domane', 'emonda', 'checkpoint', 'fuel', 'marlin', 'fx']):
|
| 1309 |
+
return 'bicycle'
|
| 1310 |
+
|
| 1311 |
+
return 'unknown'
|
| 1312 |
+
|
| 1313 |
+
def calculate_smart_match_score(search_words, product_name, product_category, is_bike_search):
|
| 1314 |
+
"""Akilli eslesme skoru hesapla"""
|
| 1315 |
+
product_name_lower = product_name.lower()
|
| 1316 |
+
product_words = product_name_lower.split()
|
| 1317 |
+
product_type = get_product_type(product_category, product_name)
|
| 1318 |
+
|
| 1319 |
+
# Temel kelime eslesmesi
|
| 1320 |
+
base_score = sum(1 for word in search_words if word in product_name_lower)
|
| 1321 |
+
|
| 1322 |
+
# Kategori uyumu bonusu/cezasi
|
| 1323 |
+
if is_bike_search:
|
| 1324 |
+
if product_type == 'bicycle':
|
| 1325 |
+
base_score += 2 # Bisiklet aramasinda bisiklet bulunca bonus
|
| 1326 |
+
elif product_type == 'accessory':
|
| 1327 |
+
base_score -= 100 # Bisiklet aramasinda aksesuar KESINLIKLE gosterilmemeli
|
| 1328 |
+
|
| 1329 |
+
# KRITIK VARYANT KONTROLU
|
| 1330 |
+
# AXS, Di2, eTap gibi varyantlar FARKLI URUNLERDIR
|
| 1331 |
+
# Kullanici "Madone SLR 9" diyorsa "Madone SLR 9 AXS" GOSTERILMEMELI
|
| 1332 |
+
critical_variants = ['axs', 'etap', 'di2', 'frameset']
|
| 1333 |
+
|
| 1334 |
+
# Urunde olan kritik varyantlar
|
| 1335 |
+
product_critical_variants = [v for v in critical_variants if v in product_name_lower]
|
| 1336 |
+
# Kullanicinin soyledigi kritik varyantlar
|
| 1337 |
+
user_critical_variants = [v for v in critical_variants if v in ' '.join(search_words)]
|
| 1338 |
+
|
| 1339 |
+
# Urunde kritik varyant var AMA kullanici soylemedi -> BUYUK CEZA
|
| 1340 |
+
for variant in product_critical_variants:
|
| 1341 |
+
if variant not in user_critical_variants:
|
| 1342 |
+
base_score -= 50 # Bu urun gosterilmemeli
|
| 1343 |
+
|
| 1344 |
+
# Kullanici kritik varyant soyledi AMA urunde yok -> CEZA
|
| 1345 |
+
for variant in user_critical_variants:
|
| 1346 |
+
if variant not in product_critical_variants:
|
| 1347 |
+
base_score -= 50 # Bu urun gosterilmemeli
|
| 1348 |
+
|
| 1349 |
+
# Tam esleme bonusu - Tum arama kelimeleri urunde varsa
|
| 1350 |
+
all_words_match = all(word in product_name_lower for word in search_words if len(word) > 2)
|
| 1351 |
+
if all_words_match and len(search_words) > 1:
|
| 1352 |
+
base_score += 3
|
| 1353 |
+
|
| 1354 |
+
# Model numarasi eslesmesi - "9" araniyorsa "9" olmali, "7" olmamali
|
| 1355 |
+
for word in search_words:
|
| 1356 |
+
if word.isdigit():
|
| 1357 |
+
if word in product_words:
|
| 1358 |
+
base_score += 2 # Tam numara eslesmesi
|
| 1359 |
+
else:
|
| 1360 |
+
# Farkli numara varsa ceza
|
| 1361 |
+
for pword in product_words:
|
| 1362 |
+
if pword.isdigit() and pword != word:
|
| 1363 |
+
base_score -= 20 # Farkli model numarasi
|
| 1364 |
+
|
| 1365 |
+
return base_score
|
| 1366 |
+
|
| 1367 |
+
# Arama bisiklet mi?
|
| 1368 |
+
is_bike_search = is_bicycle_search(search_text)
|
| 1369 |
+
|
| 1370 |
+
for product_info in products:
|
| 1371 |
+
product_full_name = product_info[2] # Tam urun adi
|
| 1372 |
+
product_category = product_info[1][6] if len(product_info[1]) > 6 else "" # category_tree
|
| 1373 |
+
|
| 1374 |
+
# Akilli skor hesapla
|
| 1375 |
+
match_score = calculate_smart_match_score(
|
| 1376 |
+
search_words,
|
| 1377 |
+
product_full_name,
|
| 1378 |
+
product_category,
|
| 1379 |
+
is_bike_search
|
| 1380 |
+
)
|
| 1381 |
+
|
| 1382 |
+
# Daha iyi eslesme bulduysa guncelle
|
| 1383 |
+
if match_score > best_match_score and product_info[1][0] == "stokta":
|
| 1384 |
+
best_match_score = match_score
|
| 1385 |
+
|
| 1386 |
+
normal_price = f"Fiyat: {product_info[1][1]} TL"
|
| 1387 |
+
if product_info[1][3]:
|
| 1388 |
+
eft_price = f"Havale: {product_info[1][3]} TL"
|
| 1389 |
+
price_info = f"{normal_price}, {eft_price}"
|
| 1390 |
+
else:
|
| 1391 |
+
price_info = normal_price
|
| 1392 |
+
|
| 1393 |
+
# Urun linki ve gorseli al - EN IYI ESLESEN URUN
|
| 1394 |
+
if product_info[1][2]: # product_link
|
| 1395 |
+
found_product_link = product_info[1][2]
|
| 1396 |
+
if product_info[1][5]: # picture_url
|
| 1397 |
+
found_product_image = product_info[1][5]
|
| 1398 |
+
found_product_name = product_info[2]
|
| 1399 |
+
|
| 1400 |
+
# System message'a LINK EKLEME - cunku GPT linki yanitina dahil ediyor
|
| 1401 |
+
# ve sonra biz de ekledigimizde 2 kere gorunuyor
|
| 1402 |
+
new_msg = f"{product_info[2]} {product_info[1][0]} - {price_info}"
|
| 1403 |
+
messages.append({"role": "system", "content": new_msg})
|
| 1404 |
+
|
| 1405 |
+
# Eger match bulunamadiysa, basit eslesme dene
|
| 1406 |
+
if best_match_score == 0:
|
| 1407 |
+
for product_info in products:
|
| 1408 |
+
if product_info[0] in input_words and product_info[1][0] == "stokta":
|
| 1409 |
+
normal_price = f"Fiyat: {product_info[1][1]} TL"
|
| 1410 |
+
if product_info[1][3]:
|
| 1411 |
+
eft_price = f"Havale: {product_info[1][3]} TL"
|
| 1412 |
+
price_info = f"{normal_price}, {eft_price}"
|
| 1413 |
+
else:
|
| 1414 |
+
price_info = normal_price
|
| 1415 |
+
|
| 1416 |
+
if product_info[1][2]:
|
| 1417 |
+
found_product_link = product_info[1][2]
|
| 1418 |
+
if product_info[1][5]:
|
| 1419 |
+
found_product_image = product_info[1][5]
|
| 1420 |
+
found_product_name = product_info[2]
|
| 1421 |
+
|
| 1422 |
+
new_msg = f"{product_info[2]} {product_info[1][0]} - {price_info}"
|
| 1423 |
+
messages.append({"role": "system", "content": new_msg})
|
| 1424 |
+
break # Ilk eslesen yeterli
|
| 1425 |
+
|
| 1426 |
+
# Stok bilgisi ekle - detected_product kullan
|
| 1427 |
+
# ONCELIK: XML sonucu (smart_warehouse_with_price.py) daha guvenilir
|
| 1428 |
+
warehouse_info = get_warehouse_stock(stock_query_product)
|
| 1429 |
+
xml_has_valid_stock = False
|
| 1430 |
+
|
| 1431 |
+
if warehouse_info:
|
| 1432 |
+
stock_msg = "Magaza Stok Durumu:\n" + "\n".join(warehouse_info) if isinstance(warehouse_info, list) else str(warehouse_info)
|
| 1433 |
+
messages.append({"role": "system", "content": stock_msg})
|
| 1434 |
+
|
| 1435 |
+
# XML sonucunda gercek stok bilgisi var mi kontrol et
|
| 1436 |
+
# "mevcut degil", "bulunamadi", "tukendi" gibi ifadeler yoksa stok var demektir
|
| 1437 |
+
stock_msg_lower = stock_msg.lower()
|
| 1438 |
+
negative_indicators = ["mevcut değil", "mevcut degil", "bulunamadi", "bulunmuyor", "tükendi", "tukendi", "stokta yok"]
|
| 1439 |
+
xml_has_valid_stock = not any(indicator in stock_msg_lower for indicator in negative_indicators)
|
| 1440 |
+
|
| 1441 |
+
if xml_has_valid_stock:
|
| 1442 |
+
logger.info(f"✅ XML'den stok bilgisi bulundu: {stock_query_product}")
|
| 1443 |
+
|
| 1444 |
+
# Link ve fiyat bilgisini context'e kaydet (follow-up sorular icin)
|
| 1445 |
+
context = get_conversation_context(phone_number)
|
| 1446 |
+
link_match = re.search(r'Link: (https?://[^\s]+)', stock_msg)
|
| 1447 |
+
if link_match:
|
| 1448 |
+
context["current_product_link"] = link_match.group(1)
|
| 1449 |
+
logger.info(f"🔗 Link context'e kaydedildi: {link_match.group(1)}")
|
| 1450 |
+
price_match = re.search(r'Fiyat: ([^\n]+)', stock_msg)
|
| 1451 |
+
if price_match:
|
| 1452 |
+
context["current_product_price"] = price_match.group(1)
|
| 1453 |
+
# Urun adini da kaydet
|
| 1454 |
+
if stock_query_product:
|
| 1455 |
+
context["current_product"] = stock_query_product
|
| 1456 |
+
|
| 1457 |
+
# Gercek zamanli stok sorgusu - SADECE XML sonucu bos veya olumsuzsa calistir
|
| 1458 |
+
# Bu sayede XML'de bulunan urunler icin yanlis "stokta yok" mesaji onlenir
|
| 1459 |
+
should_query_stock = False
|
| 1460 |
+
|
| 1461 |
+
if not xml_has_valid_stock:
|
| 1462 |
+
if intent_analysis:
|
| 1463 |
+
# Intent Analyzer'dan gelen intent'lere bak
|
| 1464 |
+
intents = intent_analysis.get("intents", [])
|
| 1465 |
+
# stock, info, price gibi intent'ler stok sorgusu gerektirir
|
| 1466 |
+
stock_related_intents = ["stock", "info", "price", "availability"]
|
| 1467 |
+
should_query_stock = any(intent in intents for intent in stock_related_intents)
|
| 1468 |
+
|
| 1469 |
+
# Urun tespit edildiyse de stok sorgula (kullanici urun hakkinda konusuyor demektir)
|
| 1470 |
+
if detected_product:
|
| 1471 |
+
should_query_stock = True
|
| 1472 |
+
|
| 1473 |
+
# Yedek: Intent Analyzer calismazsa basit keyword kontrolu
|
| 1474 |
+
if not intent_analysis and is_stock_query(user_message):
|
| 1475 |
+
should_query_stock = True
|
| 1476 |
+
|
| 1477 |
+
if should_query_stock and stock_query_product and not xml_has_valid_stock:
|
| 1478 |
+
realtime_stock = get_realtime_stock_parallel(stock_query_product)
|
| 1479 |
+
if realtime_stock:
|
| 1480 |
+
messages.append({"role": "system", "content": f"Gercek Zamanli Stok:\n{realtime_stock}"})
|
| 1481 |
+
logger.info(f"📦 API'den stok bilgisi eklendi: {stock_query_product}")
|
| 1482 |
+
|
| 1483 |
+
if not OPENAI_API_KEY:
|
| 1484 |
+
return "Sistem hatasi olustu."
|
| 1485 |
+
|
| 1486 |
+
# SON HATIRLATMA: Turkce dil kurallari - GPT'ye her yanit oncesi hatirlatma
|
| 1487 |
+
# Bu mesaj en sona ekleniyor ki GPT son gordukleri kurallari uygulasın
|
| 1488 |
+
turkish_reminder = """KRITIK KURALLAR (HER YANIT ICIN GECERLI):
|
| 1489 |
+
1. ASLA 'sen' kullanma, HER ZAMAN 'siz' kullan (istersen -> isterseniz, sana -> size)
|
| 1490 |
+
2. ASLA soru ile bitirme (ayirtayim mi?, ister misiniz?, bakar misiniz? YASAK)
|
| 1491 |
+
3. Bilgiyi ver ve sus, musteri karar versin
|
| 1492 |
+
4. ONEMLI: Onceki mesajlarda bahsedilen urunleri UNUTMA! "Hangi model var" gibi sorular onceki konudan devam eder.
|
| 1493 |
+
YANLIS: "Istersen beden ve magaza bazli stok bilgisini de netlestirebilirim."
|
| 1494 |
+
DOGRU: "Beden ve magaza bazli stok bilgisi icin yazabilirsiniz." """
|
| 1495 |
+
messages.append({"role": "system", "content": turkish_reminder})
|
| 1496 |
+
|
| 1497 |
+
# Model tipine gore payload olustur
|
| 1498 |
+
# GPT-5.2 ve o1/o3 modelleri: temperature ve max_tokens desteklemiyor
|
| 1499 |
+
if "gpt-5" in model or "o1" in model or "o3" in model:
|
| 1500 |
+
payload = {
|
| 1501 |
+
"model": model,
|
| 1502 |
+
"messages": messages,
|
| 1503 |
+
"max_completion_tokens": 1000
|
| 1504 |
+
}
|
| 1505 |
+
else:
|
| 1506 |
+
payload = {
|
| 1507 |
+
"model": model,
|
| 1508 |
+
"messages": messages,
|
| 1509 |
+
"temperature": 0.3,
|
| 1510 |
+
"max_tokens": 1000
|
| 1511 |
+
}
|
| 1512 |
+
|
| 1513 |
+
headers = {
|
| 1514 |
+
"Content-Type": "application/json",
|
| 1515 |
+
"Authorization": f"Bearer {OPENAI_API_KEY}"
|
| 1516 |
+
}
|
| 1517 |
+
|
| 1518 |
+
logger.info(f"📤 API istegi gonderiliyor - Model: {model}")
|
| 1519 |
+
|
| 1520 |
+
response = requests.post(API_URL, headers=headers, json=payload, timeout=30)
|
| 1521 |
+
|
| 1522 |
+
if response.status_code == 200:
|
| 1523 |
+
result = response.json()
|
| 1524 |
+
ai_response = result['choices'][0]['message']['content']
|
| 1525 |
+
|
| 1526 |
+
# Kullanilan modeli logla
|
| 1527 |
+
used_model = result.get('model', model)
|
| 1528 |
+
logger.info(f"✅ Yanit alindi - Kullanilan model: {used_model}")
|
| 1529 |
+
|
| 1530 |
+
try:
|
| 1531 |
+
formatted_response = extract_product_info_whatsapp(ai_response)
|
| 1532 |
+
except:
|
| 1533 |
+
formatted_response = ai_response
|
| 1534 |
+
|
| 1535 |
+
# Urun linki ve gorseli varsa ekle
|
| 1536 |
+
# NOT: Stok/magaza sorularinda yanlis gorsel gostermemek icin
|
| 1537 |
+
# sadece TEK URUN soruldugundan eminsen gorsel gonder
|
| 1538 |
+
is_specific_product_query = best_match_score >= 3 # En az 3 kelime eslesmeli
|
| 1539 |
+
|
| 1540 |
+
# GORSEL GONDERILMEMESI GEREKEN SORU TIPLERI
|
| 1541 |
+
# Beden, size, kadro, geometri gibi sorularda gorsel gereksiz
|
| 1542 |
+
no_image_keywords = [
|
| 1543 |
+
'beden', 'size', 'kadro', 'boy', 'kac cm', 'kaç cm',
|
| 1544 |
+
'hangi beden', 'xl mi', 'l mi', 'm mi', 's mi',
|
| 1545 |
+
'geometri', 'stack', 'reach', 'standover',
|
| 1546 |
+
'kilo', 'agirlik', 'ağırlık', 'weight',
|
| 1547 |
+
'garanti', 'warranty', 'teslimat', 'kargo',
|
| 1548 |
+
'taksit', 'odeme', 'ödeme', 'kredi', 'havale'
|
| 1549 |
+
]
|
| 1550 |
+
user_msg_lower = user_message.lower()
|
| 1551 |
+
is_info_only_query = any(keyword in user_msg_lower for keyword in no_image_keywords)
|
| 1552 |
+
|
| 1553 |
+
if found_product_link and is_specific_product_query and not is_info_only_query:
|
| 1554 |
+
formatted_response += f"\n\n🔗 {found_product_link}"
|
| 1555 |
+
|
| 1556 |
+
add_to_conversation(phone_number, user_message, formatted_response)
|
| 1557 |
+
|
| 1558 |
+
# Gorsel sadece spesifik urun sorgusu ise gonder
|
| 1559 |
+
# "Sariyer'de hangi renk var" gibi genel sorularda gorsel gonderme
|
| 1560 |
+
# Beden/size sorularinda da gorsel gonderme
|
| 1561 |
+
if found_product_image and is_specific_product_query and not is_info_only_query:
|
| 1562 |
+
return (formatted_response, found_product_image)
|
| 1563 |
+
|
| 1564 |
+
return formatted_response
|
| 1565 |
+
else:
|
| 1566 |
+
error_msg = response.text[:200] if response.text else "Bilinmeyen hata"
|
| 1567 |
+
logger.error(f"❌ API hatasi {response.status_code}: {error_msg}")
|
| 1568 |
+
|
| 1569 |
+
# Fallback modele gec
|
| 1570 |
+
if model != MODEL_CONFIG["fallback"]:
|
| 1571 |
+
logger.info(f"🔄 Fallback modele geciliyor: {MODEL_CONFIG['fallback']}")
|
| 1572 |
+
fallback_model = MODEL_CONFIG["fallback"]
|
| 1573 |
+
|
| 1574 |
+
# Fallback icin payload'i yeniden olustur (max_tokens kullan)
|
| 1575 |
+
fallback_payload = {
|
| 1576 |
+
"model": fallback_model,
|
| 1577 |
+
"messages": messages,
|
| 1578 |
+
"temperature": 0.3,
|
| 1579 |
+
"max_tokens": 1000
|
| 1580 |
+
}
|
| 1581 |
+
|
| 1582 |
+
response = requests.post(API_URL, headers=headers, json=fallback_payload, timeout=30)
|
| 1583 |
+
|
| 1584 |
+
if response.status_code == 200:
|
| 1585 |
+
result = response.json()
|
| 1586 |
+
ai_response = result['choices'][0]['message']['content']
|
| 1587 |
+
add_to_conversation(phone_number, user_message, ai_response)
|
| 1588 |
+
return ai_response
|
| 1589 |
+
|
| 1590 |
+
return "Su anda bir sorun yasiyorum. Lutfen tekrar deneyin."
|
| 1591 |
+
|
| 1592 |
+
except requests.exceptions.Timeout:
|
| 1593 |
+
logger.error("❌ API timeout hatasi")
|
| 1594 |
+
return "Islem zaman asimina ugradi. Lutfen tekrar deneyin."
|
| 1595 |
+
except Exception as e:
|
| 1596 |
+
logger.error(f"❌ Mesaj isleme hatasi: {e}")
|
| 1597 |
+
import traceback
|
| 1598 |
+
traceback.print_exc()
|
| 1599 |
+
return "Teknik bir sorun olustu. Lutfen tekrar deneyin."
|
| 1600 |
+
|
| 1601 |
+
|
| 1602 |
+
# ===============================
|
| 1603 |
+
# FASTAPI UYGULAMASI
|
| 1604 |
+
# ===============================
|
| 1605 |
+
app = FastAPI(title="Trek WhatsApp Bot - Hybrid Model")
|
| 1606 |
+
|
| 1607 |
+
@app.post("/whatsapp-webhook")
|
| 1608 |
+
async def whatsapp_webhook(request: Request):
|
| 1609 |
+
"""WhatsApp webhook - Hybrid model ile"""
|
| 1610 |
+
try:
|
| 1611 |
+
form_data = await request.form()
|
| 1612 |
+
|
| 1613 |
+
from_number = form_data.get('From')
|
| 1614 |
+
to_number = form_data.get('To')
|
| 1615 |
+
message_body = form_data.get('Body', '')
|
| 1616 |
+
message_status = form_data.get('MessageStatus')
|
| 1617 |
+
|
| 1618 |
+
# Medya kontrolu
|
| 1619 |
+
num_media = int(form_data.get('NumMedia', 0))
|
| 1620 |
+
media_urls = []
|
| 1621 |
+
media_types = []
|
| 1622 |
+
|
| 1623 |
+
for i in range(num_media):
|
| 1624 |
+
media_url = form_data.get(f'MediaUrl{i}')
|
| 1625 |
+
media_type = form_data.get(f'MediaContentType{i}')
|
| 1626 |
+
if media_url:
|
| 1627 |
+
media_urls.append(media_url)
|
| 1628 |
+
media_types.append(media_type)
|
| 1629 |
+
|
| 1630 |
+
logger.info(f"📱 Webhook - From: {from_number}, Body: {message_body[:50] if message_body else 'N/A'}, Media: {num_media}")
|
| 1631 |
+
|
| 1632 |
+
# Durum guncellemelerini ignore et
|
| 1633 |
+
if message_status in ['sent', 'delivered', 'read', 'failed']:
|
| 1634 |
+
return {"status": "ignored", "message": f"Status: {message_status}"}
|
| 1635 |
+
|
| 1636 |
+
# Giden mesajlari ignore et
|
| 1637 |
+
if to_number != TWILIO_WHATSAPP_NUMBER:
|
| 1638 |
+
return {"status": "ignored", "message": "Outgoing message"}
|
| 1639 |
+
|
| 1640 |
+
# Bos mesaj ve medya yoksa ignore et
|
| 1641 |
+
if not message_body and num_media == 0:
|
| 1642 |
+
return {"status": "ignored", "message": "Empty message"}
|
| 1643 |
+
|
| 1644 |
+
logger.info(f"✅ MESAJ ALINDI: {from_number} -> Metin: {bool(message_body)}, Medya: {num_media}")
|
| 1645 |
+
|
| 1646 |
+
if not twilio_client:
|
| 1647 |
+
return {"status": "error", "message": "Twilio yapilandirmasi eksik"}
|
| 1648 |
+
|
| 1649 |
+
# ========================================
|
| 1650 |
+
# HYBRID MODEL SECIMI
|
| 1651 |
+
# ========================================
|
| 1652 |
+
product_image_url = None
|
| 1653 |
+
|
| 1654 |
+
if num_media > 0 and media_urls:
|
| 1655 |
+
# GORSEL VAR -> GPT-4o kullan
|
| 1656 |
+
logger.info("🖼️ GORSEL TESPIT EDILDI -> GPT-4o Vision kullanilacak")
|
| 1657 |
+
ai_response = process_whatsapp_message_with_media(
|
| 1658 |
+
message_body,
|
| 1659 |
+
from_number,
|
| 1660 |
+
media_urls,
|
| 1661 |
+
media_types
|
| 1662 |
+
)
|
| 1663 |
+
else:
|
| 1664 |
+
# SADECE METIN -> GPT-5.2 kullan
|
| 1665 |
+
logger.info("📝 METIN MESAJI -> GPT-5.2 kullanilacak")
|
| 1666 |
+
result = process_whatsapp_message_with_memory(
|
| 1667 |
+
message_body,
|
| 1668 |
+
from_number
|
| 1669 |
+
)
|
| 1670 |
+
|
| 1671 |
+
# Tuple donerse (metin, gorsel_url) seklinde
|
| 1672 |
+
if isinstance(result, tuple):
|
| 1673 |
+
ai_response, product_image_url = result
|
| 1674 |
+
logger.info(f"🖼️ Urun gorseli bulundu: {product_image_url}")
|
| 1675 |
+
else:
|
| 1676 |
+
ai_response = result
|
| 1677 |
+
|
| 1678 |
+
# Yanit kisalt
|
| 1679 |
+
if len(ai_response) > 1500:
|
| 1680 |
+
ai_response = ai_response[:1500] + "...\n\nDetayli bilgi: trekbisiklet.com.tr"
|
| 1681 |
+
|
| 1682 |
+
# WhatsApp'a gonder
|
| 1683 |
+
# Once metin mesaji gonder
|
| 1684 |
+
message = twilio_client.messages.create(
|
| 1685 |
+
messaging_service_sid=TWILIO_MESSAGING_SERVICE_SID,
|
| 1686 |
+
body=ai_response,
|
| 1687 |
+
to=from_number
|
| 1688 |
+
)
|
| 1689 |
+
|
| 1690 |
+
logger.info(f"✅ YANIT GONDERILDI: {ai_response[:100]}...")
|
| 1691 |
+
|
| 1692 |
+
# Urun gorseli varsa ayrica gonder
|
| 1693 |
+
if product_image_url:
|
| 1694 |
+
try:
|
| 1695 |
+
image_message = twilio_client.messages.create(
|
| 1696 |
+
messaging_service_sid=TWILIO_MESSAGING_SERVICE_SID,
|
| 1697 |
+
media_url=[product_image_url],
|
| 1698 |
+
to=from_number
|
| 1699 |
+
)
|
| 1700 |
+
logger.info(f"🖼️ URUN GORSELI GONDERILDI: {product_image_url}")
|
| 1701 |
+
except Exception as img_error:
|
| 1702 |
+
logger.error(f"❌ Gorsel gonderme hatasi: {img_error}")
|
| 1703 |
+
|
| 1704 |
+
return {"status": "success", "message_sid": message.sid}
|
| 1705 |
+
|
| 1706 |
+
except Exception as e:
|
| 1707 |
+
logger.error(f"❌ Webhook hatasi: {str(e)}")
|
| 1708 |
+
import traceback
|
| 1709 |
+
traceback.print_exc()
|
| 1710 |
+
return {"status": "error", "message": str(e)}
|
| 1711 |
+
|
| 1712 |
+
|
| 1713 |
+
@app.get("/")
|
| 1714 |
+
async def root():
|
| 1715 |
+
return {
|
| 1716 |
+
"message": "Trek WhatsApp Bot - Hybrid Model calisiyor!",
|
| 1717 |
+
"status": "active",
|
| 1718 |
+
"models": {
|
| 1719 |
+
"vision": MODEL_CONFIG["vision"],
|
| 1720 |
+
"text": MODEL_CONFIG["text"],
|
| 1721 |
+
"fallback": MODEL_CONFIG["fallback"]
|
| 1722 |
+
}
|
| 1723 |
+
}
|
| 1724 |
+
|
| 1725 |
+
|
| 1726 |
+
@app.get("/health")
|
| 1727 |
+
async def health():
|
| 1728 |
+
return {
|
| 1729 |
+
"status": "healthy",
|
| 1730 |
+
"twilio_configured": twilio_client is not None,
|
| 1731 |
+
"openai_configured": OPENAI_API_KEY is not None,
|
| 1732 |
+
"models": MODEL_CONFIG,
|
| 1733 |
+
"products_loaded": len(products),
|
| 1734 |
+
"modules": {
|
| 1735 |
+
"gpt5_search": USE_GPT5_SEARCH,
|
| 1736 |
+
"media_queue": USE_MEDIA_QUEUE,
|
| 1737 |
+
"store_notification": USE_STORE_NOTIFICATION,
|
| 1738 |
+
"follow_up": USE_FOLLOW_UP,
|
| 1739 |
+
"intent_analyzer": USE_INTENT_ANALYZER
|
| 1740 |
+
}
|
| 1741 |
+
}
|
| 1742 |
+
|
| 1743 |
+
|
| 1744 |
+
@app.get("/test-models")
|
| 1745 |
+
async def test_models():
|
| 1746 |
+
"""Model durumlarini test et"""
|
| 1747 |
+
results = {}
|
| 1748 |
+
|
| 1749 |
+
for model_type, model_name in MODEL_CONFIG.items():
|
| 1750 |
+
try:
|
| 1751 |
+
payload = {
|
| 1752 |
+
"model": model_name,
|
| 1753 |
+
"messages": [{"role": "user", "content": "Merhaba, test mesaji."}],
|
| 1754 |
+
"max_tokens": 10
|
| 1755 |
+
}
|
| 1756 |
+
headers = {
|
| 1757 |
+
"Content-Type": "application/json",
|
| 1758 |
+
"Authorization": f"Bearer {OPENAI_API_KEY}"
|
| 1759 |
+
}
|
| 1760 |
+
response = requests.post(API_URL, headers=headers, json=payload, timeout=10)
|
| 1761 |
+
results[model_type] = {
|
| 1762 |
+
"model": model_name,
|
| 1763 |
+
"status": "OK" if response.status_code == 200 else f"Error: {response.status_code}",
|
| 1764 |
+
"available": response.status_code == 200
|
| 1765 |
+
}
|
| 1766 |
+
except Exception as e:
|
| 1767 |
+
results[model_type] = {
|
| 1768 |
+
"model": model_name,
|
| 1769 |
+
"status": f"Error: {str(e)}",
|
| 1770 |
+
"available": False
|
| 1771 |
+
}
|
| 1772 |
+
|
| 1773 |
+
return results
|
| 1774 |
+
|
| 1775 |
+
|
| 1776 |
+
@app.get("/cache-status")
|
| 1777 |
+
async def cache_status():
|
| 1778 |
+
"""Cache durumunu goster"""
|
| 1779 |
+
return {
|
| 1780 |
+
"cache_size": len(stock_cache),
|
| 1781 |
+
"cache_duration_seconds": CACHE_DURATION,
|
| 1782 |
+
"cached_products": list(stock_cache.keys())[:10] # Ilk 10
|
| 1783 |
+
}
|
| 1784 |
+
|
| 1785 |
+
|
| 1786 |
+
@app.post("/clear-cache")
|
| 1787 |
+
async def clear_cache():
|
| 1788 |
+
"""Cache'i temizle"""
|
| 1789 |
+
global stock_cache
|
| 1790 |
+
old_size = len(stock_cache)
|
| 1791 |
+
stock_cache = {}
|
| 1792 |
+
return {"message": f"Cache temizlendi. {old_size} kayit silindi."}
|
| 1793 |
+
|
| 1794 |
+
|
| 1795 |
+
if __name__ == "__main__":
|
| 1796 |
+
import uvicorn
|
| 1797 |
+
print("=" * 60)
|
| 1798 |
+
print(" Trek WhatsApp Bot - HYBRID MODEL")
|
| 1799 |
+
print("=" * 60)
|
| 1800 |
+
print(f" 🖼️ Gorsel mesajlar -> {MODEL_CONFIG['vision']}")
|
| 1801 |
+
print(f" 📝 Metin mesajlar -> {MODEL_CONFIG['text']}")
|
| 1802 |
+
print(f" 🔄 Fallback -> {MODEL_CONFIG['fallback']}")
|
| 1803 |
+
print("=" * 60)
|
| 1804 |
+
print(f" 📦 Yuklenen urun sayisi: {len(products)}")
|
| 1805 |
+
print(f" 🔍 GPT-5 Search: {'Aktif' if USE_GPT5_SEARCH else 'Pasif'}")
|
| 1806 |
+
print(f" 🔔 Store Notification: {'Aktif' if USE_STORE_NOTIFICATION else 'Pasif'}")
|
| 1807 |
+
print(f" 📌 Follow-Up System: {'Aktif' if USE_FOLLOW_UP else 'Pasif'}")
|
| 1808 |
+
print(f" 🧠 Intent Analyzer: {'Aktif' if USE_INTENT_ANALYZER else 'Pasif'}")
|
| 1809 |
+
print("=" * 60)
|
| 1810 |
+
uvicorn.run(app, host="0.0.0.0", port=7860)
|
cached_warehouse_search.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Cached warehouse search to reduce API calls and token usage"""
|
| 2 |
+
|
| 3 |
+
import time
|
| 4 |
+
import re
|
| 5 |
+
import json
|
| 6 |
+
import requests
|
| 7 |
+
from typing import Dict, List, Optional, Tuple
|
| 8 |
+
|
| 9 |
+
# Cache configuration
|
| 10 |
+
CACHE_DURATION = 43200 # 12 hours (12 * 60 * 60)
|
| 11 |
+
cache = {
|
| 12 |
+
'warehouse_xml': {'data': None, 'time': 0},
|
| 13 |
+
'trek_xml': {'data': None, 'time': 0},
|
| 14 |
+
'products_summary': {'data': None, 'time': 0},
|
| 15 |
+
'simple_searches': {} # Cache for specific searches
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
def get_cached_warehouse_xml() -> str:
|
| 19 |
+
"""Get warehouse XML with caching"""
|
| 20 |
+
current_time = time.time()
|
| 21 |
+
|
| 22 |
+
if cache['warehouse_xml']['data'] and (current_time - cache['warehouse_xml']['time'] < CACHE_DURATION):
|
| 23 |
+
print("📦 Using cached warehouse XML")
|
| 24 |
+
return cache['warehouse_xml']['data']
|
| 25 |
+
|
| 26 |
+
print("📡 Fetching fresh warehouse XML...")
|
| 27 |
+
url = 'https://video.trek-turkey.com/bizimhesap-warehouse-xml-b2b-api-v2.php'
|
| 28 |
+
response = requests.get(url, verify=False, timeout=15)
|
| 29 |
+
|
| 30 |
+
cache['warehouse_xml']['data'] = response.text
|
| 31 |
+
cache['warehouse_xml']['time'] = current_time
|
| 32 |
+
|
| 33 |
+
return response.text
|
| 34 |
+
|
| 35 |
+
def get_cached_trek_xml() -> str:
|
| 36 |
+
"""Get Trek XML with caching"""
|
| 37 |
+
current_time = time.time()
|
| 38 |
+
|
| 39 |
+
if cache['trek_xml']['data'] and (current_time - cache['trek_xml']['time'] < CACHE_DURATION):
|
| 40 |
+
print("🚴 Using cached Trek XML")
|
| 41 |
+
return cache['trek_xml']['data']
|
| 42 |
+
|
| 43 |
+
print("📡 Fetching fresh Trek XML...")
|
| 44 |
+
url = 'https://www.trekbisiklet.com.tr/output/8582384479'
|
| 45 |
+
response = requests.get(url, verify=False, timeout=15)
|
| 46 |
+
|
| 47 |
+
cache['trek_xml']['data'] = response.content
|
| 48 |
+
cache['trek_xml']['time'] = current_time
|
| 49 |
+
|
| 50 |
+
return response.content
|
| 51 |
+
|
| 52 |
+
def simple_product_search(query: str) -> Optional[List[Dict]]:
|
| 53 |
+
"""
|
| 54 |
+
Simple local search without GPT-5
|
| 55 |
+
Returns product info if exact/close match found
|
| 56 |
+
"""
|
| 57 |
+
query_upper = query.upper()
|
| 58 |
+
query_parts = query_upper.split()
|
| 59 |
+
|
| 60 |
+
# Get cached products summary
|
| 61 |
+
if not cache['products_summary']['data'] or \
|
| 62 |
+
(time.time() - cache['products_summary']['time'] > CACHE_DURATION):
|
| 63 |
+
# Build products summary from cached XML
|
| 64 |
+
build_products_summary()
|
| 65 |
+
|
| 66 |
+
products_summary = cache['products_summary']['data']
|
| 67 |
+
|
| 68 |
+
# Exact product name patterns
|
| 69 |
+
exact_patterns = {
|
| 70 |
+
'MADONE SL 6': lambda p: 'MADONE SL 6' in p['name'],
|
| 71 |
+
'MADONE SL 7': lambda p: 'MADONE SL 7' in p['name'],
|
| 72 |
+
'MARLIN 5': lambda p: 'MARLIN 5' in p['name'],
|
| 73 |
+
'MARLIN 6': lambda p: 'MARLIN 6' in p['name'],
|
| 74 |
+
'MARLIN 7': lambda p: 'MARLIN 7' in p['name'],
|
| 75 |
+
'DOMANE SL 5': lambda p: 'DOMANE SL 5' in p['name'],
|
| 76 |
+
'CHECKPOINT': lambda p: 'CHECKPOINT' in p['name'],
|
| 77 |
+
'FX': lambda p: p['name'].startswith('FX'),
|
| 78 |
+
'DUAL SPORT': lambda p: 'DUAL SPORT' in p['name'],
|
| 79 |
+
'RAIL': lambda p: 'RAIL' in p['name'],
|
| 80 |
+
'POWERFLY': lambda p: 'POWERFLY' in p['name'],
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
# Check for exact patterns
|
| 84 |
+
for pattern, matcher in exact_patterns.items():
|
| 85 |
+
if pattern in query_upper:
|
| 86 |
+
matching = [p for p in products_summary if matcher(p)]
|
| 87 |
+
if matching:
|
| 88 |
+
print(f"✅ Found {len(matching)} products via simple search (no GPT-5 needed)")
|
| 89 |
+
return matching
|
| 90 |
+
|
| 91 |
+
# Check for simple one-word queries
|
| 92 |
+
if len(query_parts) == 1:
|
| 93 |
+
matching = [p for p in products_summary if query_parts[0] in p['name']]
|
| 94 |
+
if matching and len(matching) < 20: # If reasonable number of matches
|
| 95 |
+
print(f"✅ Found {len(matching)} products via simple search (no GPT-5 needed)")
|
| 96 |
+
return matching
|
| 97 |
+
|
| 98 |
+
return None # Need GPT-5 for complex queries
|
| 99 |
+
|
| 100 |
+
def build_products_summary():
|
| 101 |
+
"""Build products summary from cached XMLs"""
|
| 102 |
+
xml_text = get_cached_warehouse_xml()
|
| 103 |
+
|
| 104 |
+
# Extract products
|
| 105 |
+
product_pattern = r'<Product>(.*?)</Product>'
|
| 106 |
+
all_products = re.findall(product_pattern, xml_text, re.DOTALL)
|
| 107 |
+
|
| 108 |
+
products_summary = []
|
| 109 |
+
for i, product_block in enumerate(all_products):
|
| 110 |
+
name_match = re.search(r'<ProductName><!\[CDATA\[(.*?)\]\]></ProductName>', product_block)
|
| 111 |
+
variant_match = re.search(r'<ProductVariant><!\[CDATA\[(.*?)\]\]></ProductVariant>', product_block)
|
| 112 |
+
|
| 113 |
+
if name_match:
|
| 114 |
+
warehouses_with_stock = []
|
| 115 |
+
warehouse_regex = r'<Warehouse>.*?<Name><!\[CDATA\[(.*?)\]\]></Name>.*?<Stock>(.*?)</Stock>.*?</Warehouse>'
|
| 116 |
+
warehouses = re.findall(warehouse_regex, product_block, re.DOTALL)
|
| 117 |
+
|
| 118 |
+
for wh_name, wh_stock in warehouses:
|
| 119 |
+
try:
|
| 120 |
+
if int(wh_stock.strip()) > 0:
|
| 121 |
+
warehouses_with_stock.append(wh_name)
|
| 122 |
+
except:
|
| 123 |
+
pass
|
| 124 |
+
|
| 125 |
+
product_info = {
|
| 126 |
+
"index": i,
|
| 127 |
+
"name": name_match.group(1),
|
| 128 |
+
"variant": variant_match.group(1) if variant_match else "",
|
| 129 |
+
"warehouses": warehouses_with_stock
|
| 130 |
+
}
|
| 131 |
+
products_summary.append(product_info)
|
| 132 |
+
|
| 133 |
+
cache['products_summary']['data'] = products_summary
|
| 134 |
+
cache['products_summary']['time'] = time.time()
|
| 135 |
+
|
| 136 |
+
print(f"📊 Built products summary: {len(products_summary)} products")
|
| 137 |
+
|
| 138 |
+
def should_use_gpt5(query: str) -> bool:
|
| 139 |
+
"""Determine if query needs GPT-5"""
|
| 140 |
+
query_lower = query.lower()
|
| 141 |
+
|
| 142 |
+
# Complex queries that need GPT-5
|
| 143 |
+
gpt5_triggers = [
|
| 144 |
+
'öneri', 'tavsiye', 'bütçe', 'karşılaştır',
|
| 145 |
+
'hangisi', 'ne önerirsin', 'yardım',
|
| 146 |
+
'en iyi', 'en ucuz', 'en pahalı',
|
| 147 |
+
'kaç tane', 'toplam', 'fark'
|
| 148 |
+
]
|
| 149 |
+
|
| 150 |
+
for trigger in gpt5_triggers:
|
| 151 |
+
if trigger in query_lower:
|
| 152 |
+
return True
|
| 153 |
+
|
| 154 |
+
# If simple search found results, don't use GPT-5
|
| 155 |
+
if simple_product_search(query):
|
| 156 |
+
return False
|
| 157 |
+
|
| 158 |
+
return True # Default to GPT-5 for uncertain cases
|
| 159 |
+
|
| 160 |
+
# Usage example
|
| 161 |
+
def smart_warehouse_search(query: str) -> List[str]:
|
| 162 |
+
"""
|
| 163 |
+
Smart search with caching and minimal GPT-5 usage
|
| 164 |
+
"""
|
| 165 |
+
# Check simple search cache first
|
| 166 |
+
cache_key = query.lower()
|
| 167 |
+
if cache_key in cache['simple_searches']:
|
| 168 |
+
cached_result = cache['simple_searches'][cache_key]
|
| 169 |
+
if time.time() - cached_result['time'] < CACHE_DURATION:
|
| 170 |
+
print(f"✅ Using cached result for '{query}'")
|
| 171 |
+
return cached_result['data']
|
| 172 |
+
|
| 173 |
+
# Try simple search
|
| 174 |
+
simple_results = simple_product_search(query)
|
| 175 |
+
if simple_results:
|
| 176 |
+
# Format and cache the results
|
| 177 |
+
formatted_results = format_simple_results(simple_results)
|
| 178 |
+
cache['simple_searches'][cache_key] = {
|
| 179 |
+
'data': formatted_results,
|
| 180 |
+
'time': time.time()
|
| 181 |
+
}
|
| 182 |
+
return formatted_results
|
| 183 |
+
|
| 184 |
+
# Fall back to GPT-5 if needed
|
| 185 |
+
print(f"🤖 Using GPT-5 for complex query: '{query}'")
|
| 186 |
+
# Call existing GPT-5 function here
|
| 187 |
+
return None # Would call get_warehouse_stock_smart_with_price
|
| 188 |
+
|
| 189 |
+
def format_simple_results(products: List[Dict]) -> List[str]:
|
| 190 |
+
"""Format simple search results"""
|
| 191 |
+
if not products:
|
| 192 |
+
return ["Ürün bulunamadı"]
|
| 193 |
+
|
| 194 |
+
result = ["Bulunan ürünler:"]
|
| 195 |
+
|
| 196 |
+
# Group by product name
|
| 197 |
+
product_groups = {}
|
| 198 |
+
for p in products:
|
| 199 |
+
if p['name'] not in product_groups:
|
| 200 |
+
product_groups[p['name']] = []
|
| 201 |
+
product_groups[p['name']].append(p)
|
| 202 |
+
|
| 203 |
+
for product_name, variants in product_groups.items():
|
| 204 |
+
result.append(f"\n{product_name}:")
|
| 205 |
+
for v in variants:
|
| 206 |
+
if v['variant']:
|
| 207 |
+
warehouses_str = ", ".join([w.replace('MAGAZA DEPO', '').strip() for w in v['warehouses']])
|
| 208 |
+
result.append(f"• {v['variant']}: {warehouses_str if warehouses_str else 'Stokta yok'}")
|
| 209 |
+
|
| 210 |
+
return result
|
combined_api_search.py
ADDED
|
@@ -0,0 +1,581 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Combined API Search Module - Yeni tek API'yi kullanan arama sistemi"""
|
| 2 |
+
|
| 3 |
+
import requests
|
| 4 |
+
import xml.etree.ElementTree as ET
|
| 5 |
+
import os
|
| 6 |
+
import time
|
| 7 |
+
import logging
|
| 8 |
+
|
| 9 |
+
# Logger ayarla
|
| 10 |
+
logger = logging.getLogger(__name__)
|
| 11 |
+
|
| 12 |
+
def normalize_turkish(text):
|
| 13 |
+
"""Türkçe karakter normalizasyonu"""
|
| 14 |
+
if not text:
|
| 15 |
+
return ""
|
| 16 |
+
|
| 17 |
+
# Önce uppercase yap
|
| 18 |
+
text = text.upper()
|
| 19 |
+
|
| 20 |
+
# Türkçe karakterleri normalize et
|
| 21 |
+
replacements = {
|
| 22 |
+
'İ': 'I', 'I': 'I', 'ı': 'I', 'Ì': 'I', 'Í': 'I', 'Î': 'I', 'Ï': 'I',
|
| 23 |
+
'Ş': 'S', 'ş': 'S', 'Ș': 'S', 'ș': 'S', 'Ś': 'S',
|
| 24 |
+
'Ğ': 'G', 'ğ': 'G', 'Ĝ': 'G', 'ĝ': 'G',
|
| 25 |
+
'Ü': 'U', 'ü': 'U', 'Û': 'U', 'û': 'U', 'Ù': 'U', 'Ú': 'U',
|
| 26 |
+
'Ö': 'O', 'ö': 'O', 'Ô': 'O', 'ô': 'O', 'Ò': 'O', 'Ó': 'O',
|
| 27 |
+
'Ç': 'C', 'ç': 'C', 'Ć': 'C', 'ć': 'C',
|
| 28 |
+
'Â': 'A', 'â': 'A', 'À': 'A', 'Á': 'A', 'Ä': 'A',
|
| 29 |
+
'Ê': 'E', 'ê': 'E', 'È': 'E', 'É': 'E', 'Ë': 'E'
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
for tr_char, en_char in replacements.items():
|
| 33 |
+
text = text.replace(tr_char, en_char)
|
| 34 |
+
|
| 35 |
+
# Birden fazla boşluğu tek boşluğa çevir
|
| 36 |
+
import re
|
| 37 |
+
text = re.sub(r'\s+', ' ', text)
|
| 38 |
+
|
| 39 |
+
return text.strip()
|
| 40 |
+
|
| 41 |
+
# Cache configuration
|
| 42 |
+
CACHE_DURATION = 3600 # 1 saat
|
| 43 |
+
cache = {
|
| 44 |
+
'combined_api': {'data': None, 'time': 0}
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
def get_combined_api_data():
|
| 48 |
+
"""Combined API'den veriyi çek ve cache'le"""
|
| 49 |
+
current_time = time.time()
|
| 50 |
+
|
| 51 |
+
# Cache kontrolü
|
| 52 |
+
if cache['combined_api']['data'] and (current_time - cache['combined_api']['time'] < CACHE_DURATION):
|
| 53 |
+
cache_age = (current_time - cache['combined_api']['time']) / 60
|
| 54 |
+
logger.info(f"📦 Combined API cache kullanılıyor (yaş: {cache_age:.1f} dakika)")
|
| 55 |
+
return cache['combined_api']['data']
|
| 56 |
+
|
| 57 |
+
try:
|
| 58 |
+
# Yeni combined API'yi çağır
|
| 59 |
+
url = 'https://video.trek-turkey.com/combined_trek_xml_api_v2.php'
|
| 60 |
+
response = requests.get(url, timeout=30)
|
| 61 |
+
|
| 62 |
+
if response.status_code == 200:
|
| 63 |
+
# XML parse et
|
| 64 |
+
root = ET.fromstring(response.content)
|
| 65 |
+
|
| 66 |
+
# Cache'e kaydet
|
| 67 |
+
cache['combined_api']['data'] = root
|
| 68 |
+
cache['combined_api']['time'] = current_time
|
| 69 |
+
|
| 70 |
+
# İstatistikleri logla
|
| 71 |
+
stats = root.find('stats')
|
| 72 |
+
if stats is not None:
|
| 73 |
+
matched = stats.find('matched_products')
|
| 74 |
+
match_rate = stats.find('match_rate')
|
| 75 |
+
if matched is not None and match_rate is not None:
|
| 76 |
+
logger.info(f"✅ Combined API: {matched.text} ürün eşleşti ({match_rate.text})")
|
| 77 |
+
|
| 78 |
+
return root
|
| 79 |
+
else:
|
| 80 |
+
logger.error(f"❌ Combined API HTTP Error: {response.status_code}")
|
| 81 |
+
return None
|
| 82 |
+
|
| 83 |
+
except Exception as e:
|
| 84 |
+
logger.error(f"❌ Combined API Hata: {e}")
|
| 85 |
+
return None
|
| 86 |
+
|
| 87 |
+
def search_products_combined_api(query):
|
| 88 |
+
"""Yeni combined API kullanarak ürün ara"""
|
| 89 |
+
try:
|
| 90 |
+
# Combined API verisini al
|
| 91 |
+
root = get_combined_api_data()
|
| 92 |
+
if not root:
|
| 93 |
+
return []
|
| 94 |
+
|
| 95 |
+
# Stop words - arama skorunu etkilememesi gereken kelimeler
|
| 96 |
+
stop_words = ['VAR', 'MI', 'MEVCUT', 'MU', 'STOK', 'STOGU', 'DURUMU', 'BULUNUYOR']
|
| 97 |
+
|
| 98 |
+
# Query'yi normalize et ve stop words'leri filtrele
|
| 99 |
+
query_words = normalize_turkish(query).upper().split()
|
| 100 |
+
query_words = [word for word in query_words if word not in stop_words]
|
| 101 |
+
query_normalized = ' '.join(query_words)
|
| 102 |
+
|
| 103 |
+
results = []
|
| 104 |
+
|
| 105 |
+
# Tüm ürünlerde ara
|
| 106 |
+
for product in root.findall('product'):
|
| 107 |
+
# Ürün bilgilerini al
|
| 108 |
+
stock_code = product.find('stock_code')
|
| 109 |
+
name = product.find('name')
|
| 110 |
+
label = product.find('label')
|
| 111 |
+
url = product.find('url')
|
| 112 |
+
image_url = product.find('image_url')
|
| 113 |
+
|
| 114 |
+
# Fiyat bilgileri
|
| 115 |
+
prices = product.find('prices')
|
| 116 |
+
regular_price = None
|
| 117 |
+
discounted_price = None
|
| 118 |
+
|
| 119 |
+
if prices is not None:
|
| 120 |
+
regular_price_elem = prices.find('regular_price')
|
| 121 |
+
discounted_price_elem = prices.find('discounted_price')
|
| 122 |
+
|
| 123 |
+
if regular_price_elem is not None:
|
| 124 |
+
regular_price = regular_price_elem.text
|
| 125 |
+
if discounted_price_elem is not None:
|
| 126 |
+
discounted_price = discounted_price_elem.text
|
| 127 |
+
|
| 128 |
+
# Kategori bilgileri
|
| 129 |
+
category = product.find('category')
|
| 130 |
+
main_category = None
|
| 131 |
+
sub_category = None
|
| 132 |
+
|
| 133 |
+
if category is not None:
|
| 134 |
+
main_cat_elem = category.find('main_category')
|
| 135 |
+
sub_cat_elem = category.find('sub_category')
|
| 136 |
+
|
| 137 |
+
if main_cat_elem is not None:
|
| 138 |
+
main_category = main_cat_elem.text
|
| 139 |
+
if sub_cat_elem is not None:
|
| 140 |
+
sub_category = sub_cat_elem.text
|
| 141 |
+
|
| 142 |
+
# Warehouse stok bilgileri
|
| 143 |
+
warehouse_stock = product.find('warehouse_stock')
|
| 144 |
+
stock_found = False
|
| 145 |
+
total_stock = 0
|
| 146 |
+
warehouses = []
|
| 147 |
+
|
| 148 |
+
if warehouse_stock is not None:
|
| 149 |
+
found_attr = warehouse_stock.get('found')
|
| 150 |
+
stock_found = found_attr == 'true'
|
| 151 |
+
|
| 152 |
+
if stock_found:
|
| 153 |
+
total_stock_elem = warehouse_stock.find('total_stock')
|
| 154 |
+
if total_stock_elem is not None:
|
| 155 |
+
total_stock = int(total_stock_elem.text)
|
| 156 |
+
|
| 157 |
+
# Depo bilgileri
|
| 158 |
+
warehouses_elem = warehouse_stock.find('warehouses')
|
| 159 |
+
if warehouses_elem is not None:
|
| 160 |
+
for wh in warehouses_elem.findall('warehouse'):
|
| 161 |
+
wh_name = wh.find('name')
|
| 162 |
+
wh_stock = wh.find('stock')
|
| 163 |
+
|
| 164 |
+
if wh_name is not None and wh_stock is not None:
|
| 165 |
+
warehouses.append({
|
| 166 |
+
'name': wh_name.text,
|
| 167 |
+
'stock': int(wh_stock.text)
|
| 168 |
+
})
|
| 169 |
+
|
| 170 |
+
# Arama kriteri kontrolü - Türkçe normalizasyon ile
|
| 171 |
+
searchable_text = ""
|
| 172 |
+
if name is not None:
|
| 173 |
+
searchable_text += normalize_turkish(name.text) + " "
|
| 174 |
+
if label is not None:
|
| 175 |
+
searchable_text += normalize_turkish(label.text) + " "
|
| 176 |
+
if stock_code is not None:
|
| 177 |
+
searchable_text += normalize_turkish(stock_code.text) + " "
|
| 178 |
+
if main_category is not None:
|
| 179 |
+
searchable_text += normalize_turkish(main_category) + " "
|
| 180 |
+
if sub_category is not None:
|
| 181 |
+
searchable_text += normalize_turkish(sub_category) + " "
|
| 182 |
+
|
| 183 |
+
# Geliştirilmiş arama eşleştirme - stop words filtrelenmiş query_words kullan
|
| 184 |
+
filtered_query_words = query_words # Zaten üstte filtrelenmiş
|
| 185 |
+
name_normalized = normalize_turkish(name.text.upper()) if name is not None else ""
|
| 186 |
+
|
| 187 |
+
# 1. Temel kelime eşleşmesi
|
| 188 |
+
basic_matches = sum(1 for word in filtered_query_words if word in searchable_text)
|
| 189 |
+
|
| 190 |
+
if basic_matches > 0:
|
| 191 |
+
# 2. Tam eşleşme bonusu - tüm query kelimeleri ürün adında
|
| 192 |
+
exact_bonus = 10 if all(word in name_normalized for word in filtered_query_words) else 0
|
| 193 |
+
|
| 194 |
+
# 3. Sıra bonus - kelimeler doğru sırada mı?
|
| 195 |
+
sequence_bonus = 0
|
| 196 |
+
if len(filtered_query_words) >= 2:
|
| 197 |
+
query_sequence = " ".join(filtered_query_words)
|
| 198 |
+
if query_sequence in name_normalized:
|
| 199 |
+
sequence_bonus = 5
|
| 200 |
+
|
| 201 |
+
# 4. Uzunluk penaltısı - çok uzun ürün adları düşük skor alsın
|
| 202 |
+
length_penalty = max(0, len(name_normalized.split()) - len(filtered_query_words)) * -1
|
| 203 |
+
|
| 204 |
+
# Toplam skor
|
| 205 |
+
total_score = basic_matches + exact_bonus + sequence_bonus + length_penalty
|
| 206 |
+
|
| 207 |
+
result = {
|
| 208 |
+
'stock_code': stock_code.text if stock_code is not None else '',
|
| 209 |
+
'name': name.text if name is not None else '',
|
| 210 |
+
'label': label.text if label is not None else '',
|
| 211 |
+
'url': url.text if url is not None else '',
|
| 212 |
+
'image_url': image_url.text if image_url is not None else '',
|
| 213 |
+
'regular_price': regular_price,
|
| 214 |
+
'discounted_price': discounted_price,
|
| 215 |
+
'main_category': main_category,
|
| 216 |
+
'sub_category': sub_category,
|
| 217 |
+
'stock_found': stock_found,
|
| 218 |
+
'total_stock': total_stock,
|
| 219 |
+
'warehouses': warehouses,
|
| 220 |
+
'match_score': total_score
|
| 221 |
+
}
|
| 222 |
+
results.append(result)
|
| 223 |
+
|
| 224 |
+
# Sonuçları match score'a göre sırala
|
| 225 |
+
results.sort(key=lambda x: x['match_score'], reverse=True)
|
| 226 |
+
|
| 227 |
+
logger.info(f"🔍 Combined API araması: '{query}' için {len(results)} sonuç")
|
| 228 |
+
return results[:10] # İlk 10 sonuç
|
| 229 |
+
|
| 230 |
+
except Exception as e:
|
| 231 |
+
logger.error(f"❌ Combined API arama hatası: {e}")
|
| 232 |
+
return []
|
| 233 |
+
|
| 234 |
+
def format_product_result_whatsapp(product_data, show_variants_info=False, all_results=None):
|
| 235 |
+
"""Ürün bilgisini WhatsApp için formatla"""
|
| 236 |
+
try:
|
| 237 |
+
name = product_data['name']
|
| 238 |
+
url = product_data['url']
|
| 239 |
+
image_url = product_data['image_url']
|
| 240 |
+
regular_price = product_data['regular_price']
|
| 241 |
+
discounted_price = product_data['discounted_price']
|
| 242 |
+
stock_found = product_data['stock_found']
|
| 243 |
+
total_stock = product_data['total_stock']
|
| 244 |
+
warehouses = product_data['warehouses']
|
| 245 |
+
|
| 246 |
+
result = []
|
| 247 |
+
|
| 248 |
+
# Ürün adı
|
| 249 |
+
result.append(f"🚲 **{name}**")
|
| 250 |
+
|
| 251 |
+
# Fiyat bilgisi - Original Trek fiyat yuvarlama algoritması (app.py'dan)
|
| 252 |
+
def format_price(price_str):
|
| 253 |
+
if not price_str:
|
| 254 |
+
return ""
|
| 255 |
+
try:
|
| 256 |
+
price_float = float(price_str)
|
| 257 |
+
# Original Trek fiyat yuvarlama algoritması
|
| 258 |
+
if price_float > 200000:
|
| 259 |
+
rounded = round(price_float / 5000) * 5000
|
| 260 |
+
elif price_float > 30000:
|
| 261 |
+
rounded = round(price_float / 1000) * 1000
|
| 262 |
+
elif price_float > 10000:
|
| 263 |
+
rounded = round(price_float / 100) * 100
|
| 264 |
+
else:
|
| 265 |
+
rounded = round(price_float / 10) * 10
|
| 266 |
+
|
| 267 |
+
# Türk Lirası formatı: 1.234,56
|
| 268 |
+
return f"{rounded:,.0f}".replace(",", ".")
|
| 269 |
+
except:
|
| 270 |
+
return price_str
|
| 271 |
+
|
| 272 |
+
if regular_price:
|
| 273 |
+
formatted_regular = format_price(regular_price)
|
| 274 |
+
if discounted_price and discounted_price != regular_price:
|
| 275 |
+
formatted_discounted = format_price(discounted_price)
|
| 276 |
+
result.append(f"💰 Fiyat: ~~{formatted_regular}~~ **{formatted_discounted} TL**")
|
| 277 |
+
result.append("🔥 İndirimli fiyat!")
|
| 278 |
+
else:
|
| 279 |
+
result.append(f"💰 Fiyat: {formatted_regular} TL")
|
| 280 |
+
|
| 281 |
+
# Stok durumu - Varyant detayları ile
|
| 282 |
+
if stock_found and total_stock > 0:
|
| 283 |
+
# Hangi mağazalarda var + varyant bilgisi (ana ürünse)
|
| 284 |
+
if warehouses:
|
| 285 |
+
# Ana ürün ise varyant detaylarını da göster
|
| 286 |
+
if show_variants_info and all_results and is_main_product(name):
|
| 287 |
+
result.append("📦 **Mevcut varyantlar:**")
|
| 288 |
+
|
| 289 |
+
# Bu ana ürünün varyantlarını bul
|
| 290 |
+
main_name_base = name.upper()
|
| 291 |
+
variant_details = {}
|
| 292 |
+
|
| 293 |
+
for product in all_results:
|
| 294 |
+
if (main_name_base in product['name'].upper() and
|
| 295 |
+
product['stock_found'] and
|
| 296 |
+
product['total_stock'] > 0 and
|
| 297 |
+
not is_main_product(product['name'])):
|
| 298 |
+
|
| 299 |
+
# Varyant isminden beden/renk çıkar
|
| 300 |
+
variant_name = product['name']
|
| 301 |
+
variant_part = variant_name.replace(main_name_base, '').strip()
|
| 302 |
+
if variant_part.startswith('GEN 3 (2026)'):
|
| 303 |
+
variant_part = variant_part.replace('GEN 3 (2026)', '').strip()
|
| 304 |
+
|
| 305 |
+
for wh in product['warehouses']:
|
| 306 |
+
if wh['stock'] > 0:
|
| 307 |
+
store_name = wh['name']
|
| 308 |
+
if store_name not in variant_details:
|
| 309 |
+
variant_details[store_name] = []
|
| 310 |
+
variant_details[store_name].append(variant_part)
|
| 311 |
+
|
| 312 |
+
# Mağaza bazında varyantları listele
|
| 313 |
+
for store, variants in variant_details.items():
|
| 314 |
+
result.append(f"• **{store}:** {', '.join(variants)}")
|
| 315 |
+
|
| 316 |
+
# Genel bilgi
|
| 317 |
+
result.append("📞 Diğer beden/renk teyidi için mağazaları arayın")
|
| 318 |
+
else:
|
| 319 |
+
# Normal stok bilgisi
|
| 320 |
+
available_stores = [wh['name'] for wh in warehouses if wh['stock'] > 0]
|
| 321 |
+
if available_stores:
|
| 322 |
+
result.append("📦 **Stokta mevcut mağazalar:**")
|
| 323 |
+
for store in available_stores:
|
| 324 |
+
result.append(f"• {store}")
|
| 325 |
+
else:
|
| 326 |
+
result.append("⚠️ **Stok durumu kontrol edilemiyor**")
|
| 327 |
+
result.append("📞 Güncel stok için mağazalarımızı arayın:")
|
| 328 |
+
result.append("• Caddebostan: 0543 934 0438")
|
| 329 |
+
result.append("• Alsancak: 0543 936 2335")
|
| 330 |
+
|
| 331 |
+
# Ürün linki
|
| 332 |
+
if url:
|
| 333 |
+
result.append(f"🔗 [Ürün Detayları]({url})")
|
| 334 |
+
|
| 335 |
+
# Ürün resmi (WhatsApp için) - Direkt URL WhatsApp'ta resmi gösterir
|
| 336 |
+
if image_url and image_url.startswith('https://'):
|
| 337 |
+
# WhatsApp'ta resmi direkt göstermek için sadece URL'i ver (link formatı değil)
|
| 338 |
+
result.append(f"📷 {image_url}")
|
| 339 |
+
|
| 340 |
+
return "\n".join(result)
|
| 341 |
+
|
| 342 |
+
except Exception as e:
|
| 343 |
+
logger.error(f"❌ WhatsApp format hatası: {e}")
|
| 344 |
+
return "Ürün bilgisi formatlanamadı"
|
| 345 |
+
|
| 346 |
+
def is_main_product(product_name):
|
| 347 |
+
"""Ürün adına bakarak ana ürün mü varyant mı kontrol et"""
|
| 348 |
+
name_upper = product_name.upper()
|
| 349 |
+
|
| 350 |
+
# Varyant göstergeleri - beden, renk, yıl belirticileri
|
| 351 |
+
variant_indicators = [
|
| 352 |
+
# Bedenler
|
| 353 |
+
' - XS', ' - S', ' - M', ' - L', ' - XL', ' - XXL',
|
| 354 |
+
' XS', ' S ', ' M ', ' L ', ' XL', ' XXL',
|
| 355 |
+
|
| 356 |
+
# Renkler
|
| 357 |
+
' - SİYAH', ' - MAVİ', ' - KIRMIZI', ' - YEŞİL', ' - BEYAZ', ' - MOR',
|
| 358 |
+
' SİYAH', ' MAVİ', ' KIRMIZI', ' YEŞİL', ' BEYAZ', ' MOR',
|
| 359 |
+
|
| 360 |
+
# İngilizce renkler
|
| 361 |
+
' - BLACK', ' - BLUE', ' - RED', ' - GREEN', ' - WHITE', ' - PURPLE',
|
| 362 |
+
' BLACK', ' BLUE', ' RED', ' GREEN', ' WHITE', ' PURPLE'
|
| 363 |
+
]
|
| 364 |
+
|
| 365 |
+
# Eğer bu göstergelerden biri varsa varyant
|
| 366 |
+
for indicator in variant_indicators:
|
| 367 |
+
if indicator in name_upper:
|
| 368 |
+
return False
|
| 369 |
+
|
| 370 |
+
return True
|
| 371 |
+
|
| 372 |
+
def get_warehouse_stock_combined_api(query):
|
| 373 |
+
"""Combined API kullanan warehouse search - eski fonksiyonla uyumlu"""
|
| 374 |
+
results = search_products_combined_api(query)
|
| 375 |
+
|
| 376 |
+
if not results:
|
| 377 |
+
return ["Ürün bulunamadı"]
|
| 378 |
+
|
| 379 |
+
# Akıllı model geçişi - eski model stokta yoksa yeni modeli öner
|
| 380 |
+
# Örn: "marlin 4" arandığında MARLIN 4 yoksa MARLIN 4 GEN 3'ü göster
|
| 381 |
+
query_normalized = normalize_turkish(query.upper())
|
| 382 |
+
|
| 383 |
+
# Query'yi temizle - "var mı", "stok", "fiyat" gibi ekleri çıkar
|
| 384 |
+
clean_query = query_normalized
|
| 385 |
+
for suffix in ['VAR MI', 'VAR MIYDI', 'STOK', 'STOKTA', 'FIYAT', 'FIYATI', 'KACA', 'NE KADAR']:
|
| 386 |
+
clean_query = clean_query.replace(' ' + suffix, '').replace(suffix + ' ', '').replace(suffix, '')
|
| 387 |
+
clean_query = clean_query.strip()
|
| 388 |
+
|
| 389 |
+
logger.info(f"🧹 Query temizlendi: '{query}' → '{clean_query}'")
|
| 390 |
+
|
| 391 |
+
# Genel ürün adı aranıyorsa (model numarası yok)
|
| 392 |
+
if not any(year in clean_query for year in ['GEN', '2024', '2025', '2026', '(20']):
|
| 393 |
+
# Stokta olan modelleri kontrol et
|
| 394 |
+
stocked_alternatives = []
|
| 395 |
+
|
| 396 |
+
for product in results:
|
| 397 |
+
name_normalized = normalize_turkish(product['name'].upper())
|
| 398 |
+
# Ana ürün adıyla eşleşiyor mu? (temizlenmiş query ile)
|
| 399 |
+
if clean_query in name_normalized:
|
| 400 |
+
# Ana ürün mü?
|
| 401 |
+
if is_main_product(product['name']):
|
| 402 |
+
# Stokta mı veya varyantları var mı?
|
| 403 |
+
if product['stock_found'] and product['total_stock'] > 0:
|
| 404 |
+
stocked_alternatives.append(product)
|
| 405 |
+
else:
|
| 406 |
+
# Ana ürün stokta yok, varyantları kontrol et
|
| 407 |
+
has_stocked_variants = False
|
| 408 |
+
for variant in results:
|
| 409 |
+
if (product['name'].upper() in variant['name'].upper() and
|
| 410 |
+
variant['stock_found'] and
|
| 411 |
+
variant['total_stock'] > 0 and
|
| 412 |
+
not is_main_product(variant['name'])):
|
| 413 |
+
has_stocked_variants = True
|
| 414 |
+
break
|
| 415 |
+
if has_stocked_variants:
|
| 416 |
+
stocked_alternatives.append(product)
|
| 417 |
+
|
| 418 |
+
# Eğer alternatifler varsa, en yenisini seç (genelde GEN 3, 2026 vs.)
|
| 419 |
+
if stocked_alternatives:
|
| 420 |
+
# Model yılı/versiyonu olanları öncelikle
|
| 421 |
+
def get_model_priority(product):
|
| 422 |
+
name = product['name'].upper()
|
| 423 |
+
if '2026' in name or 'GEN 3' in name:
|
| 424 |
+
return 3
|
| 425 |
+
elif '2025' in name or 'GEN 2' in name:
|
| 426 |
+
return 2
|
| 427 |
+
elif '2024' in name or 'GEN' in name:
|
| 428 |
+
return 1
|
| 429 |
+
else:
|
| 430 |
+
return 0
|
| 431 |
+
|
| 432 |
+
stocked_alternatives.sort(key=lambda x: get_model_priority(x), reverse=True)
|
| 433 |
+
|
| 434 |
+
# En yeni modeli öncelikle kullan
|
| 435 |
+
if stocked_alternatives:
|
| 436 |
+
results = [stocked_alternatives[0]] + [r for r in results if r != stocked_alternatives[0]]
|
| 437 |
+
|
| 438 |
+
# Önce ana ürünleri filtrele
|
| 439 |
+
main_products = []
|
| 440 |
+
variant_products = []
|
| 441 |
+
|
| 442 |
+
for product in results:
|
| 443 |
+
if is_main_product(product['name']):
|
| 444 |
+
main_products.append(product)
|
| 445 |
+
else:
|
| 446 |
+
variant_products.append(product)
|
| 447 |
+
|
| 448 |
+
# Ana ürünün stok bilgilerini varyantlarından topla
|
| 449 |
+
def enrich_main_product_with_variant_stock(main_product, all_results):
|
| 450 |
+
"""Ana ürün stoku yoksa varyant stoklarını topla - TAM EŞLEŞTİRME"""
|
| 451 |
+
if main_product['stock_found'] and main_product['total_stock'] > 0:
|
| 452 |
+
return main_product # Zaten stok bilgisi var
|
| 453 |
+
|
| 454 |
+
# Bu ana ürünün varyantlarını bul - TAM EŞLEŞTIRME
|
| 455 |
+
main_name = main_product['name'].upper()
|
| 456 |
+
related_variants = []
|
| 457 |
+
|
| 458 |
+
for product in all_results:
|
| 459 |
+
product_name_upper = product['name'].upper()
|
| 460 |
+
|
| 461 |
+
# TAM EŞLEŞTIRME: varyant ana ürünle TAM BAŞLAMALI
|
| 462 |
+
# Örn: "MARLIN 4 GEN 3 (2026) MOR - XS" → "MARLIN 4 GEN 3 (2026)" ile eşleşmeli
|
| 463 |
+
# "MARLIN 4 SİYAH - XS" → "MARLIN 4" ile eşleşmeli
|
| 464 |
+
|
| 465 |
+
variant_matches = False
|
| 466 |
+
if (product['stock_found'] and
|
| 467 |
+
product['total_stock'] > 0 and
|
| 468 |
+
not is_main_product(product['name'])):
|
| 469 |
+
|
| 470 |
+
# TAM ÜRÜN EŞLEŞTIRMESI - backup mantığıyla ters
|
| 471 |
+
# Ana ürün "MARLIN 4" ise
|
| 472 |
+
# Varyant "MARLIN 4 SİYAH - XS" EVET, "MARLIN 4 GEN 3 (...) MOR - XS" HAYIR
|
| 473 |
+
|
| 474 |
+
# Ana ürün tam adıyla başlamalı, fazla kelime olmamalı
|
| 475 |
+
if product_name_upper.startswith(main_name):
|
| 476 |
+
remaining_after_main = product_name_upper[len(main_name):].strip()
|
| 477 |
+
|
| 478 |
+
# Kalan kısımda GEN/2026 gibi model ayırıcıları varsa bu farklı ürün
|
| 479 |
+
model_separators = ['GEN 3', 'GEN', '(2026)', '(2025)', '2026', '2025']
|
| 480 |
+
has_model_separator = any(sep in remaining_after_main for sep in model_separators)
|
| 481 |
+
|
| 482 |
+
# Ana ürün model ayırıcısı içeriyorsa, varyant da içermeli
|
| 483 |
+
main_has_separators = any(sep in main_name for sep in model_separators)
|
| 484 |
+
|
| 485 |
+
# Ana ürün model separator içeriyorsa, varyant onun devamı olabilir
|
| 486 |
+
# Ana ürün model separator içermiyorsa, varyant da içermemeli
|
| 487 |
+
|
| 488 |
+
if main_has_separators:
|
| 489 |
+
# Ana ürün GEN 3 (2026) gibi → varyant da GEN 3'ün devamı olmalı
|
| 490 |
+
# Kalan kısımda model separator olmamalı (çünkü ana üründe zaten var)
|
| 491 |
+
if not has_model_separator:
|
| 492 |
+
# Varyant göstergeleri var mı?
|
| 493 |
+
if remaining_after_main and any(indicator in remaining_after_main for indicator in [' - ', ' MOR', ' SIYAH', ' MAVI', ' XS', ' S ', ' M ', ' L ', ' XL']):
|
| 494 |
+
variant_matches = True
|
| 495 |
+
else:
|
| 496 |
+
# Ana ürün model separator içermiyor → varyant da içermemeli
|
| 497 |
+
if not has_model_separator:
|
| 498 |
+
# Varyant göstergeleri var mı?
|
| 499 |
+
if remaining_after_main and any(indicator in remaining_after_main for indicator in [' - ', ' MOR', ' SIYAH', ' MAVI', ' XS', ' S ', ' M ', ' L ', ' XL']):
|
| 500 |
+
variant_matches = True
|
| 501 |
+
|
| 502 |
+
if variant_matches:
|
| 503 |
+
related_variants.append(product)
|
| 504 |
+
|
| 505 |
+
if related_variants:
|
| 506 |
+
# Varyant stoklarını topla
|
| 507 |
+
combined_warehouses = {}
|
| 508 |
+
total_stock = 0
|
| 509 |
+
|
| 510 |
+
for variant in related_variants:
|
| 511 |
+
total_stock += variant['total_stock']
|
| 512 |
+
for wh in variant['warehouses']:
|
| 513 |
+
wh_name = wh['name']
|
| 514 |
+
wh_stock = wh['stock']
|
| 515 |
+
if wh_name in combined_warehouses:
|
| 516 |
+
combined_warehouses[wh_name] += wh_stock
|
| 517 |
+
else:
|
| 518 |
+
combined_warehouses[wh_name] = wh_stock
|
| 519 |
+
|
| 520 |
+
# Ana ürünü stok bilgileriyle zenginleştir
|
| 521 |
+
main_product['stock_found'] = True
|
| 522 |
+
main_product['total_stock'] = total_stock
|
| 523 |
+
main_product['warehouses'] = [
|
| 524 |
+
{'name': name, 'stock': stock}
|
| 525 |
+
for name, stock in combined_warehouses.items()
|
| 526 |
+
if stock > 0
|
| 527 |
+
]
|
| 528 |
+
|
| 529 |
+
return main_product
|
| 530 |
+
|
| 531 |
+
# Öncelikle ana ürünleri kullan, Ana ürün yoksa varyantları göster
|
| 532 |
+
if main_products:
|
| 533 |
+
# Ana ürünleri varyant stoklarıyla zenginleştir
|
| 534 |
+
enriched_main_products = []
|
| 535 |
+
for main_product in main_products:
|
| 536 |
+
enriched = enrich_main_product_with_variant_stock(main_product, results)
|
| 537 |
+
enriched_main_products.append(enriched)
|
| 538 |
+
|
| 539 |
+
# Basit sorgu (örn: "marlin 4") için sadece en iyi ana ürün
|
| 540 |
+
query_words = normalize_turkish(query).upper().split()
|
| 541 |
+
product_query_words = [w for w in query_words if w not in ['FIYAT', 'STOK', 'VAR', 'MI', 'RENK', 'BEDEN']]
|
| 542 |
+
is_simple_product_query = len(product_query_words) <= 2
|
| 543 |
+
|
| 544 |
+
if is_simple_product_query:
|
| 545 |
+
selected_products = enriched_main_products[:1] # Sadece en iyi ana ürün
|
| 546 |
+
else:
|
| 547 |
+
selected_products = enriched_main_products[:3] # Birden fazla ana ürün
|
| 548 |
+
else:
|
| 549 |
+
# Ana ürün yoksa varyantları göster
|
| 550 |
+
selected_products = variant_products[:3]
|
| 551 |
+
|
| 552 |
+
formatted_results = []
|
| 553 |
+
for product in selected_products:
|
| 554 |
+
# Ana ürün ise varyant detaylarını göster
|
| 555 |
+
show_variants = is_main_product(product['name'])
|
| 556 |
+
formatted = format_product_result_whatsapp(product, show_variants_info=show_variants, all_results=results)
|
| 557 |
+
formatted_results.append(formatted)
|
| 558 |
+
|
| 559 |
+
return formatted_results
|
| 560 |
+
|
| 561 |
+
# Test fonksiyonu
|
| 562 |
+
if __name__ == "__main__":
|
| 563 |
+
# Test arama
|
| 564 |
+
logging.basicConfig(level=logging.INFO)
|
| 565 |
+
|
| 566 |
+
print("Combined API Test Başlıyor...")
|
| 567 |
+
|
| 568 |
+
test_queries = ["marlin", "trek tool", "bisiklet", "madone"]
|
| 569 |
+
|
| 570 |
+
for query in test_queries:
|
| 571 |
+
print(f"\n🔍 Test: '{query}'")
|
| 572 |
+
results = search_products_combined_api(query)
|
| 573 |
+
|
| 574 |
+
if results:
|
| 575 |
+
print(f"✅ {len(results)} sonuç bulundu")
|
| 576 |
+
for i, product in enumerate(results[:2]):
|
| 577 |
+
print(f"\n{i+1}. {product['name']}")
|
| 578 |
+
print(f" Fiyat: {product['regular_price']} TL")
|
| 579 |
+
print(f" Stok: {'✅' if product['stock_found'] else '❌'} ({product['total_stock']} adet)")
|
| 580 |
+
else:
|
| 581 |
+
print("❌ Sonuç bulunamadı")
|
customer_manager.py
ADDED
|
@@ -0,0 +1,456 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
CRM (Customer Relationship Management) System
|
| 3 |
+
Müşteri tanıma, segmentasyon ve kişiselleştirilmiş yanıtlar
|
| 4 |
+
"""
|
| 5 |
+
import json
|
| 6 |
+
import os
|
| 7 |
+
from datetime import datetime, timedelta
|
| 8 |
+
from typing import Dict, List, Optional, Tuple
|
| 9 |
+
import re
|
| 10 |
+
|
| 11 |
+
class CustomerManager:
|
| 12 |
+
def __init__(self, db_path: str = "customers.json"):
|
| 13 |
+
self.db_path = db_path
|
| 14 |
+
self.customers = self._load_database()
|
| 15 |
+
|
| 16 |
+
def _load_database(self) -> Dict:
|
| 17 |
+
"""Müşteri veritabanını yükle"""
|
| 18 |
+
if os.path.exists(self.db_path):
|
| 19 |
+
try:
|
| 20 |
+
with open(self.db_path, 'r', encoding='utf-8') as f:
|
| 21 |
+
return json.load(f)
|
| 22 |
+
except:
|
| 23 |
+
return {}
|
| 24 |
+
return {}
|
| 25 |
+
|
| 26 |
+
def _save_database(self):
|
| 27 |
+
"""Veritabanını kaydet"""
|
| 28 |
+
with open(self.db_path, 'w', encoding='utf-8') as f:
|
| 29 |
+
json.dump(self.customers, f, ensure_ascii=False, indent=2)
|
| 30 |
+
|
| 31 |
+
def _extract_name_from_message(self, message: str) -> Optional[str]:
|
| 32 |
+
"""Mesajdan isim çıkarmaya çalış"""
|
| 33 |
+
# "Ben Ahmet", "Benim adım Mehmet", "Merhaba ben Ali" gibi kalıplar
|
| 34 |
+
patterns = [
|
| 35 |
+
r"ben[im]?\s+ad[ıi]m?\s+(\w+)",
|
| 36 |
+
r"ben\s+(\w+)",
|
| 37 |
+
r"ad[ıi]m\s+(\w+)",
|
| 38 |
+
r"(\w+)\s+diye",
|
| 39 |
+
]
|
| 40 |
+
|
| 41 |
+
message_lower = message.lower()
|
| 42 |
+
for pattern in patterns:
|
| 43 |
+
match = re.search(pattern, message_lower)
|
| 44 |
+
if match:
|
| 45 |
+
name = match.group(1)
|
| 46 |
+
# İlk harfi büyük yap
|
| 47 |
+
return name.capitalize()
|
| 48 |
+
return None
|
| 49 |
+
|
| 50 |
+
def _detect_interests(self, message: str) -> List[str]:
|
| 51 |
+
"""Mesajdan ilgi alanlarını çıkar"""
|
| 52 |
+
interests = []
|
| 53 |
+
message_lower = message.lower()
|
| 54 |
+
|
| 55 |
+
# Bisiklet kategorileri
|
| 56 |
+
if any(word in message_lower for word in ["dağ", "mountain", "mtb"]):
|
| 57 |
+
interests.append("mountain_bike")
|
| 58 |
+
if any(word in message_lower for word in ["yol", "road", "yarış"]):
|
| 59 |
+
interests.append("road_bike")
|
| 60 |
+
if any(word in message_lower for word in ["şehir", "city", "urban"]):
|
| 61 |
+
interests.append("city_bike")
|
| 62 |
+
|
| 63 |
+
# Marka ve modeller
|
| 64 |
+
if "trek" in message_lower:
|
| 65 |
+
interests.append("trek")
|
| 66 |
+
if "marlin" in message_lower:
|
| 67 |
+
interests.append("marlin_series")
|
| 68 |
+
if "fx" in message_lower:
|
| 69 |
+
interests.append("fx_series")
|
| 70 |
+
if "domane" in message_lower:
|
| 71 |
+
interests.append("domane_series")
|
| 72 |
+
if "madone" in message_lower:
|
| 73 |
+
interests.append("madone_series")
|
| 74 |
+
|
| 75 |
+
# Özellikler
|
| 76 |
+
if any(word in message_lower for word in ["elektrik", "e-bike", "ebike"]):
|
| 77 |
+
interests.append("e_bike")
|
| 78 |
+
if any(word in message_lower for word in ["karbon", "carbon"]):
|
| 79 |
+
interests.append("carbon")
|
| 80 |
+
if any(word in message_lower for word in ["kadın", "women", "wsd"]):
|
| 81 |
+
interests.append("women_specific")
|
| 82 |
+
|
| 83 |
+
# Bütçe
|
| 84 |
+
if any(word in message_lower for word in ["ucuz", "uygun fiyat", "ekonomik"]):
|
| 85 |
+
interests.append("budget_friendly")
|
| 86 |
+
if any(word in message_lower for word in ["premium", "high-end", "üst segment"]):
|
| 87 |
+
interests.append("premium")
|
| 88 |
+
|
| 89 |
+
return interests
|
| 90 |
+
|
| 91 |
+
def _calculate_segment(self, customer: Dict) -> str:
|
| 92 |
+
"""Müşteri segmentini hesapla"""
|
| 93 |
+
total_purchases = len(customer.get("purchases", []))
|
| 94 |
+
total_spent = sum(p.get("price", 0) for p in customer.get("purchases", []))
|
| 95 |
+
total_queries = customer.get("total_queries", 0)
|
| 96 |
+
|
| 97 |
+
# Son aktivite kontrolü
|
| 98 |
+
last_interaction = datetime.fromisoformat(customer.get("last_interaction", datetime.now().isoformat()))
|
| 99 |
+
days_since_last = (datetime.now() - last_interaction).days
|
| 100 |
+
|
| 101 |
+
# VIP kriterler
|
| 102 |
+
if total_purchases >= 3 or total_spent >= 50000:
|
| 103 |
+
return "VIP"
|
| 104 |
+
|
| 105 |
+
# Potansiyel müşteri
|
| 106 |
+
if total_queries >= 5 and total_purchases == 0:
|
| 107 |
+
return "Potansiyel"
|
| 108 |
+
|
| 109 |
+
# Kayıp riski
|
| 110 |
+
if days_since_last > 30 and total_purchases > 0:
|
| 111 |
+
return "Kayıp Riski"
|
| 112 |
+
|
| 113 |
+
# Yeni müşteri (ilk 7 gün)
|
| 114 |
+
first_seen = datetime.fromisoformat(customer.get("first_seen", datetime.now().isoformat()))
|
| 115 |
+
if (datetime.now() - first_seen).days <= 7:
|
| 116 |
+
return "Yeni"
|
| 117 |
+
|
| 118 |
+
# Aktif müşteri
|
| 119 |
+
if days_since_last <= 7:
|
| 120 |
+
return "Aktif"
|
| 121 |
+
|
| 122 |
+
# Standart
|
| 123 |
+
return "Standart"
|
| 124 |
+
|
| 125 |
+
def get_or_create_customer(self, phone: str, name: Optional[str] = None) -> Dict:
|
| 126 |
+
"""Müşteriyi getir veya yeni oluştur"""
|
| 127 |
+
if phone in self.customers:
|
| 128 |
+
# Var olan müşteri
|
| 129 |
+
customer = self.customers[phone]
|
| 130 |
+
customer["last_interaction"] = datetime.now().isoformat()
|
| 131 |
+
|
| 132 |
+
# İsim güncellemesi
|
| 133 |
+
if name and not customer.get("name"):
|
| 134 |
+
customer["name"] = name
|
| 135 |
+
|
| 136 |
+
# Segment güncelle
|
| 137 |
+
customer["segment"] = self._calculate_segment(customer)
|
| 138 |
+
|
| 139 |
+
else:
|
| 140 |
+
# Yeni müşteri
|
| 141 |
+
customer = {
|
| 142 |
+
"phone": phone,
|
| 143 |
+
"name": name,
|
| 144 |
+
"first_seen": datetime.now().isoformat(),
|
| 145 |
+
"last_interaction": datetime.now().isoformat(),
|
| 146 |
+
"total_queries": 0,
|
| 147 |
+
"purchases": [],
|
| 148 |
+
"interests": [],
|
| 149 |
+
"searched_products": [],
|
| 150 |
+
"segment": "Yeni",
|
| 151 |
+
"notes": [],
|
| 152 |
+
"preferred_contact_time": None # "morning", "afternoon", "evening"
|
| 153 |
+
}
|
| 154 |
+
self.customers[phone] = customer
|
| 155 |
+
|
| 156 |
+
self._save_database()
|
| 157 |
+
return customer
|
| 158 |
+
|
| 159 |
+
def update_interaction(self, phone: str, message: str, product_searched: Optional[str] = None) -> Dict:
|
| 160 |
+
"""Müşteri etkileşimini güncelle"""
|
| 161 |
+
customer = self.get_or_create_customer(phone)
|
| 162 |
+
|
| 163 |
+
# Sorgu sayısını artır
|
| 164 |
+
customer["total_queries"] = customer.get("total_queries", 0) + 1
|
| 165 |
+
|
| 166 |
+
# İsim çıkarma denemesi
|
| 167 |
+
if not customer.get("name"):
|
| 168 |
+
extracted_name = self._extract_name_from_message(message)
|
| 169 |
+
if extracted_name:
|
| 170 |
+
customer["name"] = extracted_name
|
| 171 |
+
|
| 172 |
+
# İlgi alanlarını güncelle
|
| 173 |
+
new_interests = self._detect_interests(message)
|
| 174 |
+
existing_interests = set(customer.get("interests", []))
|
| 175 |
+
customer["interests"] = list(existing_interests.union(set(new_interests)))
|
| 176 |
+
|
| 177 |
+
# Aranan ürünü kaydet
|
| 178 |
+
if product_searched:
|
| 179 |
+
searched_products = customer.get("searched_products", [])
|
| 180 |
+
# Son 10 aramayı tut
|
| 181 |
+
searched_products.append({
|
| 182 |
+
"product": product_searched,
|
| 183 |
+
"date": datetime.now().isoformat()
|
| 184 |
+
})
|
| 185 |
+
customer["searched_products"] = searched_products[-10:]
|
| 186 |
+
|
| 187 |
+
# İletişim zamanı tercihi
|
| 188 |
+
hour = datetime.now().hour
|
| 189 |
+
if 6 <= hour < 12:
|
| 190 |
+
time_pref = "morning"
|
| 191 |
+
elif 12 <= hour < 18:
|
| 192 |
+
time_pref = "afternoon"
|
| 193 |
+
else:
|
| 194 |
+
time_pref = "evening"
|
| 195 |
+
|
| 196 |
+
# En çok hangi zaman diliminde yazıyor
|
| 197 |
+
if not customer.get("preferred_contact_time"):
|
| 198 |
+
customer["preferred_contact_time"] = time_pref
|
| 199 |
+
|
| 200 |
+
# Segment güncelle
|
| 201 |
+
customer["segment"] = self._calculate_segment(customer)
|
| 202 |
+
|
| 203 |
+
# Son etkileşim zamanı
|
| 204 |
+
customer["last_interaction"] = datetime.now().isoformat()
|
| 205 |
+
|
| 206 |
+
self._save_database()
|
| 207 |
+
return customer
|
| 208 |
+
|
| 209 |
+
def add_purchase(self, phone: str, product: str, price: float) -> Dict:
|
| 210 |
+
"""Satın alma kaydı ekle"""
|
| 211 |
+
customer = self.get_or_create_customer(phone)
|
| 212 |
+
|
| 213 |
+
purchase = {
|
| 214 |
+
"product": product,
|
| 215 |
+
"price": price,
|
| 216 |
+
"date": datetime.now().isoformat()
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
customer.setdefault("purchases", []).append(purchase)
|
| 220 |
+
customer["segment"] = self._calculate_segment(customer)
|
| 221 |
+
|
| 222 |
+
self._save_database()
|
| 223 |
+
return customer
|
| 224 |
+
|
| 225 |
+
def get_customer_context(self, phone: str) -> Dict:
|
| 226 |
+
"""Müşteri bağlamını getir"""
|
| 227 |
+
customer = self.customers.get(phone, {})
|
| 228 |
+
|
| 229 |
+
if not customer:
|
| 230 |
+
return {
|
| 231 |
+
"is_new": True,
|
| 232 |
+
"greeting": "Merhaba! Trek bisikletleri hakkında size nasıl yardımcı olabilirim?"
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
context = {
|
| 236 |
+
"is_new": False,
|
| 237 |
+
"name": customer.get("name"),
|
| 238 |
+
"segment": customer.get("segment", "Standart"),
|
| 239 |
+
"total_queries": customer.get("total_queries", 0),
|
| 240 |
+
"interests": customer.get("interests", []),
|
| 241 |
+
"last_product": None,
|
| 242 |
+
"days_since_last": 0,
|
| 243 |
+
"greeting": ""
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
# Son aranan ürün
|
| 247 |
+
searched_products = customer.get("searched_products", [])
|
| 248 |
+
if searched_products:
|
| 249 |
+
context["last_product"] = searched_products[-1]["product"]
|
| 250 |
+
last_search_date = datetime.fromisoformat(searched_products[-1]["date"])
|
| 251 |
+
context["days_since_last"] = (datetime.now() - last_search_date).days
|
| 252 |
+
|
| 253 |
+
# Kişiselleştirilmiş selamlama oluştur
|
| 254 |
+
context["greeting"] = self._generate_greeting(customer, context)
|
| 255 |
+
|
| 256 |
+
return context
|
| 257 |
+
|
| 258 |
+
def _generate_greeting(self, customer: Dict, context: Dict) -> str:
|
| 259 |
+
"""Kişiselleştirilmiş selamlama oluştur"""
|
| 260 |
+
segment = customer.get("segment", "Standart")
|
| 261 |
+
name = customer.get("name", "")
|
| 262 |
+
|
| 263 |
+
# Saat bazlı selamlama
|
| 264 |
+
hour = datetime.now().hour
|
| 265 |
+
if 6 <= hour < 12:
|
| 266 |
+
time_greeting = "Günaydın"
|
| 267 |
+
elif 12 <= hour < 18:
|
| 268 |
+
time_greeting = "İyi günler"
|
| 269 |
+
else:
|
| 270 |
+
time_greeting = "İyi akşamlar"
|
| 271 |
+
|
| 272 |
+
# Segment bazlı selamlama
|
| 273 |
+
if segment == "VIP":
|
| 274 |
+
if name:
|
| 275 |
+
greeting = f"{time_greeting} {name} Bey/Hanım! Değerli müşterimiz olarak size özel VIP avantajlarımız mevcut."
|
| 276 |
+
else:
|
| 277 |
+
greeting = f"{time_greeting}! Değerli müşterimize özel VIP avantajlarımız mevcut."
|
| 278 |
+
|
| 279 |
+
elif segment == "Yeni":
|
| 280 |
+
greeting = f"{time_greeting}! Trek bisikletleri dünyasına hoş geldiniz! Size nasıl yardımcı olabilirim?"
|
| 281 |
+
|
| 282 |
+
elif segment == "Potansiyel":
|
| 283 |
+
if context.get("last_product"):
|
| 284 |
+
greeting = f"{time_greeting}! Daha önce sorduğunuz {context['last_product']} veya başka bir konuda size yardımcı olabilir miyim?"
|
| 285 |
+
else:
|
| 286 |
+
greeting = f"{time_greeting}! Bisiklet arayışınızda size nasıl yardımcı olabilirim?"
|
| 287 |
+
|
| 288 |
+
elif segment == "Kayıp Riski":
|
| 289 |
+
if name:
|
| 290 |
+
greeting = f"{time_greeting} {name} Bey/Hanım! Sizi özledik! Size özel geri dönüş kampanyamız var."
|
| 291 |
+
else:
|
| 292 |
+
greeting = f"{time_greeting}! Sizi tekrar aramızda görmek güzel! Size özel fırsatlarımız var."
|
| 293 |
+
|
| 294 |
+
elif segment == "Aktif":
|
| 295 |
+
if name:
|
| 296 |
+
greeting = f"{time_greeting} {name} Bey/Hanım! Tekrar hoş geldiniz!"
|
| 297 |
+
else:
|
| 298 |
+
greeting = f"{time_greeting}! Tekrar hoş geldiniz!"
|
| 299 |
+
|
| 300 |
+
# Son aranan ürün varsa ekle
|
| 301 |
+
if context.get("last_product") and context.get("days_since_last", 99) <= 3:
|
| 302 |
+
greeting += f" {context['last_product']} hakkında bilgi almak ister misiniz?"
|
| 303 |
+
|
| 304 |
+
else: # Standart
|
| 305 |
+
if name:
|
| 306 |
+
greeting = f"{time_greeting} {name} Bey/Hanım! Size nasıl yardımcı olabilirim?"
|
| 307 |
+
else:
|
| 308 |
+
greeting = f"{time_greeting}! Size nasıl yardımcı olabilirim?"
|
| 309 |
+
|
| 310 |
+
return greeting
|
| 311 |
+
|
| 312 |
+
def get_analytics(self) -> Dict:
|
| 313 |
+
"""Analitik özet döndür"""
|
| 314 |
+
total_customers = len(self.customers)
|
| 315 |
+
segments = {}
|
| 316 |
+
total_purchases = 0
|
| 317 |
+
total_revenue = 0
|
| 318 |
+
active_today = 0
|
| 319 |
+
|
| 320 |
+
today = datetime.now().date()
|
| 321 |
+
|
| 322 |
+
for customer in self.customers.values():
|
| 323 |
+
# Segment dağılımı
|
| 324 |
+
segment = customer.get("segment", "Standart")
|
| 325 |
+
segments[segment] = segments.get(segment, 0) + 1
|
| 326 |
+
|
| 327 |
+
# Satış istatistikleri
|
| 328 |
+
purchases = customer.get("purchases", [])
|
| 329 |
+
total_purchases += len(purchases)
|
| 330 |
+
total_revenue += sum(p.get("price", 0) for p in purchases)
|
| 331 |
+
|
| 332 |
+
# Bugün aktif olanlar
|
| 333 |
+
last_interaction = datetime.fromisoformat(customer.get("last_interaction", "2020-01-01"))
|
| 334 |
+
if last_interaction.date() == today:
|
| 335 |
+
active_today += 1
|
| 336 |
+
|
| 337 |
+
return {
|
| 338 |
+
"total_customers": total_customers,
|
| 339 |
+
"segments": segments,
|
| 340 |
+
"total_purchases": total_purchases,
|
| 341 |
+
"total_revenue": total_revenue,
|
| 342 |
+
"active_today": active_today,
|
| 343 |
+
"average_purchase_value": total_revenue / total_purchases if total_purchases > 0 else 0
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
def get_customer_list(self, segment: Optional[str] = None) -> List[Dict]:
|
| 347 |
+
"""Müşteri listesini getir"""
|
| 348 |
+
customers = []
|
| 349 |
+
|
| 350 |
+
for phone, customer in self.customers.items():
|
| 351 |
+
if segment and customer.get("segment") != segment:
|
| 352 |
+
continue
|
| 353 |
+
|
| 354 |
+
customers.append({
|
| 355 |
+
"phone": phone,
|
| 356 |
+
"name": customer.get("name", "Bilinmiyor"),
|
| 357 |
+
"segment": customer.get("segment", "Standart"),
|
| 358 |
+
"total_queries": customer.get("total_queries", 0),
|
| 359 |
+
"total_purchases": len(customer.get("purchases", [])),
|
| 360 |
+
"last_interaction": customer.get("last_interaction"),
|
| 361 |
+
"interests": customer.get("interests", [])
|
| 362 |
+
})
|
| 363 |
+
|
| 364 |
+
# Son etkileşime göre sırala
|
| 365 |
+
customers.sort(key=lambda x: x["last_interaction"], reverse=True)
|
| 366 |
+
return customers
|
| 367 |
+
|
| 368 |
+
def should_ask_for_name(self, phone: str, message: str) -> Tuple[bool, Optional[str]]:
|
| 369 |
+
"""İsim sorulmalı mı kontrol et"""
|
| 370 |
+
customer = self.customers.get(phone, {})
|
| 371 |
+
|
| 372 |
+
# Zaten isim varsa sorma
|
| 373 |
+
if customer.get("name"):
|
| 374 |
+
return False, None
|
| 375 |
+
|
| 376 |
+
queries = customer.get("total_queries", 0)
|
| 377 |
+
message_lower = message.lower()
|
| 378 |
+
|
| 379 |
+
# 1. Rezervasyon/satın alma anında (EN DOĞAL)
|
| 380 |
+
reservation_keywords = ["rezerv", "ayırt", "satın al", "alabilir miyim", "almak istiyorum",
|
| 381 |
+
"sipariş", "kargola", "gönder", "paket", "teslimat"]
|
| 382 |
+
if any(keyword in message_lower for keyword in reservation_keywords):
|
| 383 |
+
return True, "📝 Rezervasyon için adınızı alabilir miyim?"
|
| 384 |
+
|
| 385 |
+
# 2. Mağaza ziyareti planı
|
| 386 |
+
store_visit_keywords = ["mağazaya gel", "mağazaya uğra", "görmeye gel", "bakmaya gel",
|
| 387 |
+
"test sürüşü", "denemek istiyorum"]
|
| 388 |
+
if any(keyword in message_lower for keyword in store_visit_keywords):
|
| 389 |
+
return True, "🏪 Mağazada size daha iyi yardımcı olabilmemiz için adınızı öğrenebilir miyim?"
|
| 390 |
+
|
| 391 |
+
# 3. Telefon görüşmesi talebi
|
| 392 |
+
phone_keywords = ["ara", "telefon", "görüş", "konuş", "iletişim"]
|
| 393 |
+
if any(keyword in message_lower for keyword in phone_keywords):
|
| 394 |
+
return True, "📞 Sizi arayabilmemiz için adınızı paylaşır mısınız?"
|
| 395 |
+
|
| 396 |
+
# 4. Fiyat/ödeme görüşmesi (ciddi alıcı)
|
| 397 |
+
payment_keywords = ["taksit", "ödeme", "kredi kartı", "havale", "eft", "nakit", "peşin"]
|
| 398 |
+
if any(keyword in message_lower for keyword in payment_keywords):
|
| 399 |
+
return True, "💳 Size özel ödeme seçenekleri sunabilmem için adınızı öğrenebilir miyim?"
|
| 400 |
+
|
| 401 |
+
# 5. 3. mesajda nazikçe sor (sadece ürün sorguluyorsa)
|
| 402 |
+
if queries == 2: # 3. mesaj (0'dan başlıyor)
|
| 403 |
+
# Sadece basit selamlaşma değilse
|
| 404 |
+
greeting_only = ["merhaba", "selam", "günaydın", "iyi günler", "hey", "sa"]
|
| 405 |
+
if not any(message_lower.strip() == greeting for greeting in greeting_only):
|
| 406 |
+
return True, "Bu arada, size daha iyi hizmet verebilmem için adınızı öğrenebilir miyim? 😊"
|
| 407 |
+
|
| 408 |
+
# 6. 5+ sorgudan sonra hala isim yoksa (potansiyel müşteri)
|
| 409 |
+
if queries >= 5:
|
| 410 |
+
return True, "✨ Sizin için özel tekliflerimiz olabilir. Adınızı paylaşır mısınız?"
|
| 411 |
+
|
| 412 |
+
return False, None
|
| 413 |
+
|
| 414 |
+
def extract_name_from_response(self, message: str) -> Optional[str]:
|
| 415 |
+
"""İsim sorulduktan sonra gelen yanıttan isim çıkar"""
|
| 416 |
+
message_lower = message.lower().strip()
|
| 417 |
+
|
| 418 |
+
# Direkt isim yanıtları
|
| 419 |
+
if len(message_lower.split()) == 1:
|
| 420 |
+
# Yaygın olmayan tek kelime yanıtları filtrele
|
| 421 |
+
common_words = ["evet", "hayır", "tamam", "olur", "peki", "teşekkür", "teşekkürler",
|
| 422 |
+
"merhaba", "selam", "günaydın", "güle", "hoşçakal", "görüşürüz",
|
| 423 |
+
"sa", "as", "slm", "mrb", "ok", "okay", "yes", "no", "hi", "hello", "bye"]
|
| 424 |
+
if message_lower not in common_words:
|
| 425 |
+
# Tek kelime, muhtemelen isim
|
| 426 |
+
name = message.capitalize()
|
| 427 |
+
# Türkçe isim kontrolü (en az 2 karakter)
|
| 428 |
+
if len(name) >= 2 and name.isalpha():
|
| 429 |
+
return name
|
| 430 |
+
|
| 431 |
+
# "Benim adım X", "Ben X", "Adım X" kalıpları
|
| 432 |
+
patterns = [
|
| 433 |
+
r"(?:benim\s+)?ad[ıi]m?\s+(\w+)",
|
| 434 |
+
r"ben\s+(\w+)",
|
| 435 |
+
r"(\w+)\s+diye\s+hitap",
|
| 436 |
+
r"bana\s+(\w+)\s+diyebilirsiniz",
|
| 437 |
+
r"ismim\s+(\w+)",
|
| 438 |
+
r"(\w+),?\s*teşekkür", # "Ahmet, teşekkürler"
|
| 439 |
+
]
|
| 440 |
+
|
| 441 |
+
for pattern in patterns:
|
| 442 |
+
import re
|
| 443 |
+
match = re.search(pattern, message_lower)
|
| 444 |
+
if match:
|
| 445 |
+
name = match.group(1).capitalize()
|
| 446 |
+
if len(name) >= 2:
|
| 447 |
+
return name
|
| 448 |
+
|
| 449 |
+
# İsim + Soyisim durumu (sadece ismi al)
|
| 450 |
+
two_words = message.strip().split()
|
| 451 |
+
if len(two_words) == 2:
|
| 452 |
+
# Her ikisi de büyük harfle başlıyorsa
|
| 453 |
+
if two_words[0][0].isupper() and two_words[1][0].isupper():
|
| 454 |
+
return two_words[0]
|
| 455 |
+
|
| 456 |
+
return None
|
follow_up_system.py
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
|
| 4 |
+
"""
|
| 5 |
+
Otomatik Takip ve Hatırlatma Sistemi
|
| 6 |
+
Müşteri taahhütlerini takip eder ve Mehmet Bey'e hatırlatır
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import os
|
| 10 |
+
import json
|
| 11 |
+
import logging
|
| 12 |
+
from datetime import datetime, timedelta
|
| 13 |
+
from typing import Optional, List, Dict
|
| 14 |
+
from dataclasses import dataclass, asdict
|
| 15 |
+
from enum import Enum
|
| 16 |
+
|
| 17 |
+
logger = logging.getLogger(__name__)
|
| 18 |
+
|
| 19 |
+
# Takip tipi
|
| 20 |
+
class FollowUpType(Enum):
|
| 21 |
+
RESERVATION = "reservation" # Ayırtma takibi
|
| 22 |
+
VISIT_PROMISE = "visit" # Gelme sözü takibi
|
| 23 |
+
PRICE_INQUIRY = "price" # Fiyat sorusu takibi
|
| 24 |
+
DECISION_PENDING = "decision" # Karar bekleyen
|
| 25 |
+
TEST_RIDE = "test_ride" # Test sürüşü
|
| 26 |
+
|
| 27 |
+
# Takip durumu
|
| 28 |
+
class FollowUpStatus(Enum):
|
| 29 |
+
PENDING = "pending" # Bekliyor
|
| 30 |
+
REMINDED = "reminded" # Hatırlatma yapıldı
|
| 31 |
+
COMPLETED = "completed" # Tamamlandı
|
| 32 |
+
CANCELLED = "cancelled" # İptal edildi
|
| 33 |
+
|
| 34 |
+
@dataclass
|
| 35 |
+
class FollowUp:
|
| 36 |
+
"""Takip kaydı"""
|
| 37 |
+
id: str # Unique ID
|
| 38 |
+
customer_phone: str # Müşteri telefonu
|
| 39 |
+
customer_name: Optional[str] # Müşteri adı
|
| 40 |
+
product_name: str # Ürün
|
| 41 |
+
follow_up_type: str # Takip tipi
|
| 42 |
+
status: str # Durum
|
| 43 |
+
created_at: str # Oluşturulma zamanı
|
| 44 |
+
follow_up_at: str # Hatırlatma zamanı
|
| 45 |
+
original_message: str # Orijinal mesaj
|
| 46 |
+
notes: Optional[str] # Notlar
|
| 47 |
+
store_name: Optional[str] # Mağaza
|
| 48 |
+
reminded_count: int = 0 # Kaç kez hatırlatıldı
|
| 49 |
+
|
| 50 |
+
class FollowUpManager:
|
| 51 |
+
"""Takip yöneticisi"""
|
| 52 |
+
|
| 53 |
+
def __init__(self, db_file: str = "follow_ups.json"):
|
| 54 |
+
self.db_file = db_file
|
| 55 |
+
self.follow_ups = self._load_database()
|
| 56 |
+
|
| 57 |
+
def _load_database(self) -> List[FollowUp]:
|
| 58 |
+
"""Database'i yükle"""
|
| 59 |
+
if os.path.exists(self.db_file):
|
| 60 |
+
try:
|
| 61 |
+
with open(self.db_file, "r") as f:
|
| 62 |
+
data = json.load(f)
|
| 63 |
+
return [FollowUp(**item) for item in data]
|
| 64 |
+
except Exception as e:
|
| 65 |
+
logger.error(f"Database yükleme hatası: {e}")
|
| 66 |
+
return []
|
| 67 |
+
return []
|
| 68 |
+
|
| 69 |
+
def _save_database(self):
|
| 70 |
+
"""Database'i kaydet"""
|
| 71 |
+
try:
|
| 72 |
+
data = [asdict(f) for f in self.follow_ups]
|
| 73 |
+
with open(self.db_file, "w") as f:
|
| 74 |
+
json.dump(data, f, indent=2, ensure_ascii=False)
|
| 75 |
+
except Exception as e:
|
| 76 |
+
logger.error(f"Database kayıt hatası: {e}")
|
| 77 |
+
|
| 78 |
+
def create_follow_up(
|
| 79 |
+
self,
|
| 80 |
+
customer_phone: str,
|
| 81 |
+
product_name: str,
|
| 82 |
+
follow_up_type: FollowUpType,
|
| 83 |
+
original_message: str,
|
| 84 |
+
follow_up_hours: int = 24,
|
| 85 |
+
customer_name: Optional[str] = None,
|
| 86 |
+
notes: Optional[str] = None,
|
| 87 |
+
store_name: Optional[str] = None
|
| 88 |
+
) -> FollowUp:
|
| 89 |
+
"""Yeni takip oluştur"""
|
| 90 |
+
|
| 91 |
+
now = datetime.now()
|
| 92 |
+
follow_up_time = now + timedelta(hours=follow_up_hours)
|
| 93 |
+
|
| 94 |
+
# Unique ID oluştur
|
| 95 |
+
follow_up_id = f"{customer_phone}_{now.strftime('%Y%m%d_%H%M%S')}"
|
| 96 |
+
|
| 97 |
+
follow_up = FollowUp(
|
| 98 |
+
id=follow_up_id,
|
| 99 |
+
customer_phone=customer_phone,
|
| 100 |
+
customer_name=customer_name,
|
| 101 |
+
product_name=product_name,
|
| 102 |
+
follow_up_type=follow_up_type.value,
|
| 103 |
+
status=FollowUpStatus.PENDING.value,
|
| 104 |
+
created_at=now.isoformat(),
|
| 105 |
+
follow_up_at=follow_up_time.isoformat(),
|
| 106 |
+
original_message=original_message,
|
| 107 |
+
notes=notes,
|
| 108 |
+
store_name=store_name,
|
| 109 |
+
reminded_count=0
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
self.follow_ups.append(follow_up)
|
| 113 |
+
self._save_database()
|
| 114 |
+
|
| 115 |
+
logger.info(f"✅ Takip oluşturuldu: {follow_up_id}")
|
| 116 |
+
logger.info(f" Hatırlatma zamanı: {follow_up_time.strftime('%d.%m.%Y %H:%M')}")
|
| 117 |
+
|
| 118 |
+
return follow_up
|
| 119 |
+
|
| 120 |
+
def get_pending_reminders(self) -> List[FollowUp]:
|
| 121 |
+
"""Bekleyen hatırlatmaları getir"""
|
| 122 |
+
now = datetime.now()
|
| 123 |
+
pending = []
|
| 124 |
+
|
| 125 |
+
for follow_up in self.follow_ups:
|
| 126 |
+
if follow_up.status == FollowUpStatus.PENDING.value:
|
| 127 |
+
follow_up_time = datetime.fromisoformat(follow_up.follow_up_at)
|
| 128 |
+
if follow_up_time <= now:
|
| 129 |
+
pending.append(follow_up)
|
| 130 |
+
|
| 131 |
+
return pending
|
| 132 |
+
|
| 133 |
+
def mark_as_reminded(self, follow_up_id: str):
|
| 134 |
+
"""Hatırlatma yapıldı olarak işaretle"""
|
| 135 |
+
for follow_up in self.follow_ups:
|
| 136 |
+
if follow_up.id == follow_up_id:
|
| 137 |
+
follow_up.status = FollowUpStatus.REMINDED.value
|
| 138 |
+
follow_up.reminded_count += 1
|
| 139 |
+
self._save_database()
|
| 140 |
+
logger.info(f"✅ Hatırlatma yapıldı: {follow_up_id}")
|
| 141 |
+
break
|
| 142 |
+
|
| 143 |
+
def mark_as_completed(self, follow_up_id: str):
|
| 144 |
+
"""Tamamlandı olarak işaretle"""
|
| 145 |
+
for follow_up in self.follow_ups:
|
| 146 |
+
if follow_up.id == follow_up_id:
|
| 147 |
+
follow_up.status = FollowUpStatus.COMPLETED.value
|
| 148 |
+
self._save_database()
|
| 149 |
+
logger.info(f"✅ Takip tamamlandı: {follow_up_id}")
|
| 150 |
+
break
|
| 151 |
+
|
| 152 |
+
def get_customer_history(self, customer_phone: str) -> List[FollowUp]:
|
| 153 |
+
"""Müşteri geçmişini getir"""
|
| 154 |
+
return [f for f in self.follow_ups if f.customer_phone == customer_phone]
|
| 155 |
+
|
| 156 |
+
def get_todays_follow_ups(self) -> List[FollowUp]:
|
| 157 |
+
"""Bugünün takiplerini getir"""
|
| 158 |
+
today = datetime.now().date()
|
| 159 |
+
todays = []
|
| 160 |
+
|
| 161 |
+
for follow_up in self.follow_ups:
|
| 162 |
+
follow_up_date = datetime.fromisoformat(follow_up.follow_up_at).date()
|
| 163 |
+
if follow_up_date == today:
|
| 164 |
+
todays.append(follow_up)
|
| 165 |
+
|
| 166 |
+
return todays
|
| 167 |
+
|
| 168 |
+
def analyze_message_for_follow_up(message: str) -> Optional[Dict]:
|
| 169 |
+
"""
|
| 170 |
+
Mesajı analiz et ve takip gerekip gerekmediğini belirle
|
| 171 |
+
|
| 172 |
+
Returns:
|
| 173 |
+
{
|
| 174 |
+
"needs_follow_up": True/False,
|
| 175 |
+
"follow_up_type": FollowUpType,
|
| 176 |
+
"follow_up_hours": int,
|
| 177 |
+
"reason": str
|
| 178 |
+
}
|
| 179 |
+
"""
|
| 180 |
+
|
| 181 |
+
message_lower = message.lower()
|
| 182 |
+
|
| 183 |
+
# YARIN gelme sözleri (24 saat sonra hatırlat)
|
| 184 |
+
tomorrow_keywords = [
|
| 185 |
+
'yarın', 'yarin',
|
| 186 |
+
'yarın gel', 'yarın al', 'yarın uğra',
|
| 187 |
+
'yarına', 'yarina',
|
| 188 |
+
'ertesi gün'
|
| 189 |
+
]
|
| 190 |
+
|
| 191 |
+
# BUGÜN gelme sözleri (6 saat sonra hatırlat)
|
| 192 |
+
today_keywords = [
|
| 193 |
+
'bugün', 'bugun',
|
| 194 |
+
'bugün gel', 'bugün al', 'bugün uğra',
|
| 195 |
+
'akşam gel', 'aksam gel',
|
| 196 |
+
'öğleden sonra', 'ogleden sonra',
|
| 197 |
+
'birazdan', 'biraz sonra',
|
| 198 |
+
'1 saat', 'bir saat',
|
| 199 |
+
'2 saat', 'iki saat',
|
| 200 |
+
'30 dakika', 'yarım saat'
|
| 201 |
+
]
|
| 202 |
+
|
| 203 |
+
# HAFTA SONU gelme sözleri
|
| 204 |
+
weekend_keywords = [
|
| 205 |
+
'hafta sonu', 'haftasonu',
|
| 206 |
+
'cumartesi', 'pazar',
|
| 207 |
+
'hafta içi', 'haftaiçi'
|
| 208 |
+
]
|
| 209 |
+
|
| 210 |
+
# KARAR VERME sözleri (48 saat sonra hatırlat)
|
| 211 |
+
decision_keywords = [
|
| 212 |
+
'düşüneyim', 'düşüncem', 'düşünelim',
|
| 213 |
+
'danışayım', 'danısayım', 'danışıcam',
|
| 214 |
+
'eşime sor', 'eşimle konuş', 'esime sor',
|
| 215 |
+
'karar ver', 'haber ver',
|
| 216 |
+
'araştırayım', 'araştırıcam',
|
| 217 |
+
'bakarım', 'bakarız', 'bakıcam'
|
| 218 |
+
]
|
| 219 |
+
|
| 220 |
+
# TEST SÜRÜŞÜ (4 saat sonra hatırlat)
|
| 221 |
+
test_keywords = [
|
| 222 |
+
'test sürüş', 'test et', 'dene',
|
| 223 |
+
'binebilir', 'binmek ist',
|
| 224 |
+
'test ride', 'deneme sürüş'
|
| 225 |
+
]
|
| 226 |
+
|
| 227 |
+
# Yarın kontrolü
|
| 228 |
+
for keyword in tomorrow_keywords:
|
| 229 |
+
if keyword in message_lower:
|
| 230 |
+
return {
|
| 231 |
+
"needs_follow_up": True,
|
| 232 |
+
"follow_up_type": FollowUpType.VISIT_PROMISE,
|
| 233 |
+
"follow_up_hours": 24,
|
| 234 |
+
"reason": f"Müşteri yarın geleceğini söyledi: '{keyword}'"
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
# Bugün kontrolü
|
| 238 |
+
for keyword in today_keywords:
|
| 239 |
+
if keyword in message_lower:
|
| 240 |
+
return {
|
| 241 |
+
"needs_follow_up": True,
|
| 242 |
+
"follow_up_type": FollowUpType.VISIT_PROMISE,
|
| 243 |
+
"follow_up_hours": 6,
|
| 244 |
+
"reason": f"Müşteri bugün geleceğini söyledi: '{keyword}'"
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
# Hafta sonu kontrolü
|
| 248 |
+
for keyword in weekend_keywords:
|
| 249 |
+
if keyword in message_lower:
|
| 250 |
+
# Bugün hangi gün?
|
| 251 |
+
today = datetime.now().weekday() # 0=Pazartesi, 6=Pazar
|
| 252 |
+
if today < 5: # Hafta içiyse
|
| 253 |
+
days_to_saturday = 5 - today
|
| 254 |
+
hours = days_to_saturday * 24
|
| 255 |
+
else: # Zaten hafta sonuysa
|
| 256 |
+
hours = 24
|
| 257 |
+
|
| 258 |
+
return {
|
| 259 |
+
"needs_follow_up": True,
|
| 260 |
+
"follow_up_type": FollowUpType.VISIT_PROMISE,
|
| 261 |
+
"follow_up_hours": hours,
|
| 262 |
+
"reason": f"Müşteri hafta sonu geleceğini söyledi: '{keyword}'"
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
# Karar verme kontrolü
|
| 266 |
+
for keyword in decision_keywords:
|
| 267 |
+
if keyword in message_lower:
|
| 268 |
+
return {
|
| 269 |
+
"needs_follow_up": True,
|
| 270 |
+
"follow_up_type": FollowUpType.DECISION_PENDING,
|
| 271 |
+
"follow_up_hours": 48,
|
| 272 |
+
"reason": f"Müşteri düşüneceğini söyledi: '{keyword}'"
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
# Test sürüşü kontrolü
|
| 276 |
+
for keyword in test_keywords:
|
| 277 |
+
if keyword in message_lower:
|
| 278 |
+
return {
|
| 279 |
+
"needs_follow_up": True,
|
| 280 |
+
"follow_up_type": FollowUpType.TEST_RIDE,
|
| 281 |
+
"follow_up_hours": 4,
|
| 282 |
+
"reason": f"Müşteri test sürüşü istiyor: '{keyword}'"
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
# Ayırtma varsa 24 saat sonra kontrol
|
| 286 |
+
reservation_keywords = ['ayırt', 'rezerve', 'tutun', 'sakla']
|
| 287 |
+
for keyword in reservation_keywords:
|
| 288 |
+
if keyword in message_lower:
|
| 289 |
+
return {
|
| 290 |
+
"needs_follow_up": True,
|
| 291 |
+
"follow_up_type": FollowUpType.RESERVATION,
|
| 292 |
+
"follow_up_hours": 24,
|
| 293 |
+
"reason": f"Ayırtma talebi var: '{keyword}'"
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
return None
|
| 297 |
+
|
| 298 |
+
def format_reminder_message(follow_up: FollowUp) -> str:
|
| 299 |
+
"""Hatırlatma mesajını formatla"""
|
| 300 |
+
|
| 301 |
+
# Takip tipine göre emoji
|
| 302 |
+
type_emojis = {
|
| 303 |
+
"reservation": "📦",
|
| 304 |
+
"visit": "🚶",
|
| 305 |
+
"price": "💰",
|
| 306 |
+
"decision": "🤔",
|
| 307 |
+
"test_ride": "🚴"
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
emoji = type_emojis.get(follow_up.follow_up_type, "📌")
|
| 311 |
+
|
| 312 |
+
# Zaman hesaplama
|
| 313 |
+
created_time = datetime.fromisoformat(follow_up.created_at)
|
| 314 |
+
time_diff = datetime.now() - created_time
|
| 315 |
+
|
| 316 |
+
if time_diff.days > 0:
|
| 317 |
+
time_str = f"{time_diff.days} gün önce"
|
| 318 |
+
elif time_diff.seconds > 3600:
|
| 319 |
+
hours = time_diff.seconds // 3600
|
| 320 |
+
time_str = f"{hours} saat önce"
|
| 321 |
+
else:
|
| 322 |
+
time_str = "Az önce"
|
| 323 |
+
|
| 324 |
+
# Mesaj oluştur
|
| 325 |
+
message = f"""
|
| 326 |
+
{emoji} **TAKİP HATIRLATMASI**
|
| 327 |
+
|
| 328 |
+
👤 Müşteri: {follow_up.customer_name or "İsimsiz"}
|
| 329 |
+
📱 Tel: {follow_up.customer_phone.replace('whatsapp:', '')}
|
| 330 |
+
|
| 331 |
+
🚲 Ürün: {follow_up.product_name}
|
| 332 |
+
⏰ İlk mesaj: {time_str}
|
| 333 |
+
|
| 334 |
+
📝 Müşteri mesajı:
|
| 335 |
+
"{follow_up.original_message}"
|
| 336 |
+
"""
|
| 337 |
+
|
| 338 |
+
# Tipe göre özel mesaj
|
| 339 |
+
if follow_up.follow_up_type == "reservation":
|
| 340 |
+
message += "\n⚠️ AYIRTMA TAKİBİ: Müşteri geldi mi?"
|
| 341 |
+
elif follow_up.follow_up_type == "visit":
|
| 342 |
+
message += "\n⚠️ ZİYARET TAKİBİ: Müşteri geleceğini söylemişti"
|
| 343 |
+
elif follow_up.follow_up_type == "decision":
|
| 344 |
+
message += "\n⚠️ KARAR TAKİBİ: Müşteri düşüneceğini söylemişti"
|
| 345 |
+
elif follow_up.follow_up_type == "test_ride":
|
| 346 |
+
message += "\n⚠️ TEST SÜRÜŞÜ TAKİBİ: Test için gelecekti"
|
| 347 |
+
|
| 348 |
+
message += "\n\n📞 Müşteriyi arayıp durumu öğrenin!"
|
| 349 |
+
|
| 350 |
+
return message
|
| 351 |
+
|
| 352 |
+
# Test fonksiyonu
|
| 353 |
+
def test_follow_up_system():
|
| 354 |
+
"""Test senaryoları"""
|
| 355 |
+
|
| 356 |
+
print("\n" + "="*60)
|
| 357 |
+
print("TAKİP SİSTEMİ TESTİ")
|
| 358 |
+
print("="*60)
|
| 359 |
+
|
| 360 |
+
manager = FollowUpManager("test_follow_ups.json")
|
| 361 |
+
|
| 362 |
+
# Test mesajları
|
| 363 |
+
test_messages = [
|
| 364 |
+
("Yarın gelip FX 2'yi alacağım", "FX 2"),
|
| 365 |
+
("Eşime danışayım, size dönerim", "Marlin 5"),
|
| 366 |
+
("Bugün akşam uğrarım", "Checkpoint"),
|
| 367 |
+
("Hafta sonu test sürüşü yapabilir miyim?", "Domane"),
|
| 368 |
+
("30 dakikaya oradayım, ayırtın", "FX Sport")
|
| 369 |
+
]
|
| 370 |
+
|
| 371 |
+
for message, product in test_messages:
|
| 372 |
+
print(f"\n📝 Mesaj: '{message}'")
|
| 373 |
+
|
| 374 |
+
analysis = analyze_message_for_follow_up(message)
|
| 375 |
+
if analysis and analysis["needs_follow_up"]:
|
| 376 |
+
print(f"✅ Takip gerekli!")
|
| 377 |
+
print(f" Tip: {analysis['follow_up_type'].value}")
|
| 378 |
+
print(f" {analysis['follow_up_hours']} saat sonra hatırlat")
|
| 379 |
+
print(f" Sebep: {analysis['reason']}")
|
| 380 |
+
|
| 381 |
+
# Takip oluştur
|
| 382 |
+
follow_up = manager.create_follow_up(
|
| 383 |
+
customer_phone="whatsapp:+905551234567",
|
| 384 |
+
product_name=product,
|
| 385 |
+
follow_up_type=analysis["follow_up_type"],
|
| 386 |
+
original_message=message,
|
| 387 |
+
follow_up_hours=analysis["follow_up_hours"]
|
| 388 |
+
)
|
| 389 |
+
else:
|
| 390 |
+
print("❌ Takip gerekmiyor")
|
| 391 |
+
|
| 392 |
+
# Bekleyen hatırlatmaları göster
|
| 393 |
+
print("\n" + "="*60)
|
| 394 |
+
print("BEKLEYEN HATIRLATMALAR")
|
| 395 |
+
print("="*60)
|
| 396 |
+
|
| 397 |
+
pending = manager.get_pending_reminders()
|
| 398 |
+
if pending:
|
| 399 |
+
for f in pending:
|
| 400 |
+
print(f"\n{format_reminder_message(f)}")
|
| 401 |
+
else:
|
| 402 |
+
print("Bekleyen hatırlatma yok")
|
| 403 |
+
|
| 404 |
+
print("\n" + "="*60)
|
| 405 |
+
|
| 406 |
+
if __name__ == "__main__":
|
| 407 |
+
test_follow_up_system()
|
get_warehouse_fast.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Ultra fast warehouse stock getter using regex"""
|
| 2 |
+
|
| 3 |
+
def get_warehouse_stock(product_name):
|
| 4 |
+
"""Super fast warehouse stock finder using regex instead of XML parsing"""
|
| 5 |
+
try:
|
| 6 |
+
import re
|
| 7 |
+
import requests
|
| 8 |
+
|
| 9 |
+
# Fetch XML
|
| 10 |
+
warehouse_url = 'https://video.trek-turkey.com/bizimhesap-warehouse-xml-b2b-api-v2.php'
|
| 11 |
+
response = requests.get(warehouse_url, verify=False, timeout=7)
|
| 12 |
+
|
| 13 |
+
if response.status_code != 200:
|
| 14 |
+
return None
|
| 15 |
+
|
| 16 |
+
xml_text = response.text
|
| 17 |
+
|
| 18 |
+
# Turkish normalization
|
| 19 |
+
turkish_map = {'ı': 'i', 'ğ': 'g', 'ü': 'u', 'ş': 's', 'ö': 'o', 'ç': 'c', 'İ': 'i', 'I': 'i'}
|
| 20 |
+
|
| 21 |
+
def normalize_turkish(text):
|
| 22 |
+
text = text.lower()
|
| 23 |
+
for tr_char, en_char in turkish_map.items():
|
| 24 |
+
text = text.replace(tr_char, en_char)
|
| 25 |
+
return text
|
| 26 |
+
|
| 27 |
+
# Normalize search
|
| 28 |
+
search_name = normalize_turkish(product_name.strip())
|
| 29 |
+
search_name = search_name.replace('(2026)', '').replace('(2025)', '').strip()
|
| 30 |
+
search_words = search_name.split()
|
| 31 |
+
|
| 32 |
+
# Separate size from product words
|
| 33 |
+
size_words = ['s', 'm', 'l', 'xl', 'xs', 'xxl', 'ml']
|
| 34 |
+
size_indicators = ['beden', 'size', 'boy']
|
| 35 |
+
|
| 36 |
+
variant_filter = [w for w in search_words if w in size_words]
|
| 37 |
+
product_words = [w for w in search_words if w not in size_words and w not in size_indicators]
|
| 38 |
+
|
| 39 |
+
print(f"DEBUG - Looking for: {' '.join(product_words)}")
|
| 40 |
+
print(f"DEBUG - Size filter: {variant_filter}")
|
| 41 |
+
|
| 42 |
+
# Build exact pattern for common products
|
| 43 |
+
search_pattern = ' '.join(product_words).upper()
|
| 44 |
+
|
| 45 |
+
# Handle specific products
|
| 46 |
+
if 'madone' in product_words and 'sl' in product_words and '6' in product_words and 'gen' in product_words and '8' in product_words:
|
| 47 |
+
search_pattern = 'MADONE SL 6 GEN 8'
|
| 48 |
+
elif 'madone' in product_words and 'slr' in product_words and '7' in product_words:
|
| 49 |
+
search_pattern = 'MADONE SLR 7 GEN 8'
|
| 50 |
+
else:
|
| 51 |
+
# Generic pattern
|
| 52 |
+
search_pattern = search_pattern.replace('İ', 'I')
|
| 53 |
+
|
| 54 |
+
# Find all Product blocks with this name
|
| 55 |
+
# Using lazy quantifier .*? for efficiency
|
| 56 |
+
product_pattern = f'<Product>.*?<ProductName><!\\[CDATA\\[{re.escape(search_pattern)}\\]\\]></ProductName>.*?</Product>'
|
| 57 |
+
matches = re.findall(product_pattern, xml_text, re.DOTALL)
|
| 58 |
+
|
| 59 |
+
print(f"DEBUG - Found {len(matches)} products matching '{search_pattern}'")
|
| 60 |
+
|
| 61 |
+
# Process matches
|
| 62 |
+
warehouse_stock_map = {}
|
| 63 |
+
|
| 64 |
+
for match in matches:
|
| 65 |
+
# Extract variant if we need to filter by size
|
| 66 |
+
should_process = True
|
| 67 |
+
|
| 68 |
+
if variant_filter:
|
| 69 |
+
variant_match = re.search(r'<ProductVariant><!\\[CDATA\\[(.*?)\\]\\]></ProductVariant>', match)
|
| 70 |
+
if variant_match:
|
| 71 |
+
variant = variant_match.group(1)
|
| 72 |
+
size_wanted = variant_filter[0].upper()
|
| 73 |
+
|
| 74 |
+
# Check if variant starts with desired size
|
| 75 |
+
if variant.startswith(f'{size_wanted}-'):
|
| 76 |
+
print(f"DEBUG - Found matching variant: {variant}")
|
| 77 |
+
should_process = True
|
| 78 |
+
else:
|
| 79 |
+
should_process = False
|
| 80 |
+
else:
|
| 81 |
+
should_process = False
|
| 82 |
+
|
| 83 |
+
if should_process:
|
| 84 |
+
# Extract all warehouses with stock
|
| 85 |
+
warehouse_pattern = r'<Warehouse>.*?<Name><!\\[CDATA\\[(.*?)\\]\\]></Name>.*?<Stock>(.*?)</Stock>.*?</Warehouse>'
|
| 86 |
+
warehouses = re.findall(warehouse_pattern, match, re.DOTALL)
|
| 87 |
+
|
| 88 |
+
for wh_name, wh_stock in warehouses:
|
| 89 |
+
try:
|
| 90 |
+
stock = int(wh_stock.strip())
|
| 91 |
+
if stock > 0:
|
| 92 |
+
if wh_name in warehouse_stock_map:
|
| 93 |
+
warehouse_stock_map[wh_name] += stock
|
| 94 |
+
else:
|
| 95 |
+
warehouse_stock_map[wh_name] = stock
|
| 96 |
+
except:
|
| 97 |
+
pass
|
| 98 |
+
|
| 99 |
+
# If we found a variant match, stop looking
|
| 100 |
+
if variant_filter and should_process:
|
| 101 |
+
break
|
| 102 |
+
|
| 103 |
+
print(f"DEBUG - Warehouse stock: {warehouse_stock_map}")
|
| 104 |
+
|
| 105 |
+
# Format results
|
| 106 |
+
if warehouse_stock_map:
|
| 107 |
+
all_warehouse_info = []
|
| 108 |
+
for warehouse_name, total_stock in warehouse_stock_map.items():
|
| 109 |
+
# Make store names more readable
|
| 110 |
+
if "Caddebostan" in warehouse_name or "CADDEBOSTAN" in warehouse_name:
|
| 111 |
+
display_name = "Caddebostan mağazası"
|
| 112 |
+
elif "Ortaköy" in warehouse_name or "ORTAKÖY" in warehouse_name:
|
| 113 |
+
display_name = "Ortaköy mağazası"
|
| 114 |
+
elif "Sarıyer" in warehouse_name:
|
| 115 |
+
display_name = "Sarıyer mağazası"
|
| 116 |
+
elif "Alsancak" in warehouse_name or "ALSANCAK" in warehouse_name or "İzmir" in warehouse_name:
|
| 117 |
+
display_name = "İzmir Alsancak mağazası"
|
| 118 |
+
elif "BAHCEKOY" in warehouse_name or "Bahçeköy" in warehouse_name:
|
| 119 |
+
display_name = "Bahçeköy mağazası"
|
| 120 |
+
else:
|
| 121 |
+
display_name = warehouse_name
|
| 122 |
+
|
| 123 |
+
all_warehouse_info.append(f"{display_name}: Mevcut")
|
| 124 |
+
return all_warehouse_info
|
| 125 |
+
else:
|
| 126 |
+
return ["Hiçbir mağazada mevcut değil"]
|
| 127 |
+
|
| 128 |
+
except Exception as e:
|
| 129 |
+
print(f"Warehouse stock error: {e}")
|
| 130 |
+
return None
|
image_renderer.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
Ürün Resimlerini Sohbette Gösterme Sistemi
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
def round_price(price_str):
|
| 7 |
+
"""Fiyatı yuvarlama formülüne göre yuvarla"""
|
| 8 |
+
try:
|
| 9 |
+
# TL ve diğer karakterleri temizle
|
| 10 |
+
price_clean = price_str.replace(' TL', '').replace(',', '.')
|
| 11 |
+
price_float = float(price_clean)
|
| 12 |
+
|
| 13 |
+
# Fiyat 200000 üzerindeyse en yakın 5000'lik basamağa yuvarla
|
| 14 |
+
if price_float > 200000:
|
| 15 |
+
return str(round(price_float / 5000) * 5000)
|
| 16 |
+
# Fiyat 30000 üzerindeyse en yakın 1000'lik basamağa yuvarla
|
| 17 |
+
elif price_float > 30000:
|
| 18 |
+
return str(round(price_float / 1000) * 1000)
|
| 19 |
+
# Fiyat 10000 üzerindeyse en yakın 100'lük basamağa yuvarla
|
| 20 |
+
elif price_float > 10000:
|
| 21 |
+
return str(round(price_float / 100) * 100)
|
| 22 |
+
# Diğer durumlarda en yakın 10'luk basamağa yuvarla
|
| 23 |
+
else:
|
| 24 |
+
return str(round(price_float / 10) * 10)
|
| 25 |
+
except (ValueError, TypeError):
|
| 26 |
+
return price_str
|
| 27 |
+
|
| 28 |
+
def format_message_with_images(message):
|
| 29 |
+
"""Mesajdaki resim URL'lerini HTML formatına çevir"""
|
| 30 |
+
if "Ürün resmi:" not in message:
|
| 31 |
+
return message
|
| 32 |
+
|
| 33 |
+
lines = message.split('\n')
|
| 34 |
+
formatted_lines = []
|
| 35 |
+
|
| 36 |
+
for line in lines:
|
| 37 |
+
if line.startswith("Ürün resmi:"):
|
| 38 |
+
image_url = line.replace("Ürün resmi:", "").strip()
|
| 39 |
+
if image_url:
|
| 40 |
+
# HTML img tag'i oluştur
|
| 41 |
+
img_html = f"""
|
| 42 |
+
<div style="margin: 10px 0;">
|
| 43 |
+
<img src="{image_url}"
|
| 44 |
+
alt="Ürün Resmi"
|
| 45 |
+
style="max-width: 300px; max-height: 200px; border-radius: 8px; border: 1px solid #ddd; box-shadow: 0 2px 4px rgba(0,0,0,0.1);"
|
| 46 |
+
onerror="this.style.display='none'">
|
| 47 |
+
</div>"""
|
| 48 |
+
formatted_lines.append(img_html)
|
| 49 |
+
else:
|
| 50 |
+
formatted_lines.append(line)
|
| 51 |
+
else:
|
| 52 |
+
formatted_lines.append(line)
|
| 53 |
+
|
| 54 |
+
return '\n'.join(formatted_lines)
|
| 55 |
+
|
| 56 |
+
def create_product_gallery(products_with_images):
|
| 57 |
+
"""Birden fazla ürün için galeri oluştur"""
|
| 58 |
+
if not products_with_images:
|
| 59 |
+
return ""
|
| 60 |
+
|
| 61 |
+
gallery_html = """
|
| 62 |
+
<div style="display: flex; flex-wrap: wrap; gap: 15px; margin: 15px 0;">
|
| 63 |
+
"""
|
| 64 |
+
|
| 65 |
+
for product in products_with_images:
|
| 66 |
+
name = product.get('name', 'Bilinmeyen')
|
| 67 |
+
price = product.get('price', 'Fiyat yok')
|
| 68 |
+
image_url = product.get('image_url', '')
|
| 69 |
+
product_url = product.get('product_url', '')
|
| 70 |
+
|
| 71 |
+
if image_url:
|
| 72 |
+
card_html = f"""
|
| 73 |
+
<div style="border: 1px solid #ddd; border-radius: 8px; padding: 10px; max-width: 200px; text-align: center; background: white; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
| 74 |
+
<img src="{image_url}"
|
| 75 |
+
alt="{name}"
|
| 76 |
+
style="max-width: 180px; max-height: 120px; border-radius: 4px; margin-bottom: 8px;"
|
| 77 |
+
onerror="this.style.display='none'">
|
| 78 |
+
<div style="font-size: 12px; font-weight: bold; margin-bottom: 4px;">{name}</div>
|
| 79 |
+
<div style="font-size: 11px; color: #666; margin-bottom: 8px;">{price}</div>
|
| 80 |
+
{f'<a href="{product_url}" target="_blank" style="font-size: 10px; color: #007bff; text-decoration: none;">Detaylı Bilgi</a>' if product_url else ''}
|
| 81 |
+
</div>"""
|
| 82 |
+
gallery_html += card_html
|
| 83 |
+
|
| 84 |
+
gallery_html += "</div>"
|
| 85 |
+
return gallery_html
|
| 86 |
+
|
| 87 |
+
def extract_product_info_for_gallery(message):
|
| 88 |
+
"""Mesajdan ürün bilgilerini çıkarıp galeri formatına çevir"""
|
| 89 |
+
if "karşılaştır" in message.lower() or "öneri" in message.lower():
|
| 90 |
+
# Bu durumda galeri formatı kullan
|
| 91 |
+
lines = message.split('\n')
|
| 92 |
+
products = []
|
| 93 |
+
|
| 94 |
+
current_product = {}
|
| 95 |
+
for line in lines:
|
| 96 |
+
line = line.strip()
|
| 97 |
+
if line.startswith('•') and any(keyword in line.lower() for keyword in ['marlin', 'émonda', 'madone', 'domane', 'fuel', 'powerfly', 'fx']):
|
| 98 |
+
# Yeni ürün başladı
|
| 99 |
+
if current_product:
|
| 100 |
+
products.append(current_product)
|
| 101 |
+
|
| 102 |
+
# Ürün adı ve fiyatı parse et
|
| 103 |
+
parts = line.split(' - ')
|
| 104 |
+
name = parts[0].replace('•', '').strip()
|
| 105 |
+
price_raw = parts[1] if len(parts) > 1 else 'Fiyat yok'
|
| 106 |
+
|
| 107 |
+
# Fiyatı yuvarlama formülüne göre yuvarla
|
| 108 |
+
if price_raw != 'Fiyat yok':
|
| 109 |
+
price = round_price(price_raw) + ' TL'
|
| 110 |
+
else:
|
| 111 |
+
price = price_raw
|
| 112 |
+
|
| 113 |
+
current_product = {
|
| 114 |
+
'name': name,
|
| 115 |
+
'price': price,
|
| 116 |
+
'image_url': '',
|
| 117 |
+
'product_url': ''
|
| 118 |
+
}
|
| 119 |
+
elif "Ürün resmi:" in line and current_product:
|
| 120 |
+
current_product['image_url'] = line.replace("Ürün resmi:", "").strip()
|
| 121 |
+
elif "Ürün linki:" in line and current_product:
|
| 122 |
+
current_product['product_url'] = line.replace("Ür��n linki:", "").strip()
|
| 123 |
+
|
| 124 |
+
# Son ürünü ekle
|
| 125 |
+
if current_product:
|
| 126 |
+
products.append(current_product)
|
| 127 |
+
|
| 128 |
+
if products:
|
| 129 |
+
gallery = create_product_gallery(products)
|
| 130 |
+
# Orijinal mesajdaki resim linklerini temizle
|
| 131 |
+
cleaned_message = message
|
| 132 |
+
for line in message.split('\n'):
|
| 133 |
+
if line.startswith("Ürün resmi:") or line.startswith("Ürün linki:"):
|
| 134 |
+
cleaned_message = cleaned_message.replace(line, "")
|
| 135 |
+
|
| 136 |
+
return cleaned_message.strip() + "\n\n" + gallery
|
| 137 |
+
|
| 138 |
+
return format_message_with_images(message)
|
| 139 |
+
|
| 140 |
+
def should_use_gallery_format(message):
|
| 141 |
+
"""Mesajın galeri formatı kullanması gerekip gerekmediğini kontrol et"""
|
| 142 |
+
gallery_keywords = ["karşılaştır", "öneri", "seçenek", "alternatif", "bütçe"]
|
| 143 |
+
return any(keyword in message.lower() for keyword in gallery_keywords)
|
intent_analyzer.py
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
|
| 4 |
+
"""
|
| 5 |
+
GPT-5 Powered Intent Analyzer
|
| 6 |
+
Müşteri mesajlarından niyeti anlar ve aksiyonları belirler
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import os
|
| 10 |
+
import json
|
| 11 |
+
import requests
|
| 12 |
+
import logging
|
| 13 |
+
from typing import Dict, Optional, List, Tuple
|
| 14 |
+
|
| 15 |
+
logger = logging.getLogger(__name__)
|
| 16 |
+
|
| 17 |
+
# OpenAI API
|
| 18 |
+
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
| 19 |
+
API_URL = "https://api.openai.com/v1/chat/completions"
|
| 20 |
+
|
| 21 |
+
def analyze_customer_intent(
|
| 22 |
+
message: str,
|
| 23 |
+
context: Optional[Dict] = None
|
| 24 |
+
) -> Dict:
|
| 25 |
+
"""
|
| 26 |
+
GPT-5 ile müşteri niyetini analiz et
|
| 27 |
+
|
| 28 |
+
Args:
|
| 29 |
+
message: Müşteri mesajı
|
| 30 |
+
context: Sohbet bağlamı (varsa)
|
| 31 |
+
|
| 32 |
+
Returns:
|
| 33 |
+
{
|
| 34 |
+
"intents": ["reserve", "price", "stock", "info"],
|
| 35 |
+
"product": "FX 2",
|
| 36 |
+
"confidence": 0.95,
|
| 37 |
+
"store": "caddebostan", # Eğer belirtildiyse
|
| 38 |
+
"urgency": "high/medium/low",
|
| 39 |
+
"customer_mood": "positive/neutral/negative",
|
| 40 |
+
"suggested_action": "notify_store/answer_directly/both",
|
| 41 |
+
"notification_message": "Özel mesaj"
|
| 42 |
+
}
|
| 43 |
+
"""
|
| 44 |
+
|
| 45 |
+
if not OPENAI_API_KEY:
|
| 46 |
+
logger.error("OpenAI API key missing")
|
| 47 |
+
return {"intents": [], "confidence": 0}
|
| 48 |
+
|
| 49 |
+
# Sistem promptu
|
| 50 |
+
system_prompt = """Sen bir müşteri niyet analiz uzmanısın. Trek bisiklet mağazası için müşteri mesajlarını analiz ediyorsun.
|
| 51 |
+
|
| 52 |
+
GÖREV: Müşteri mesajını analiz et ve JSON formatında döndür.
|
| 53 |
+
|
| 54 |
+
NİYET TİPLERİ:
|
| 55 |
+
- "reserve": Ürünü ayırtmak, rezerve etmek, tutmak istiyor
|
| 56 |
+
- "price": Fiyat bilgisi istiyor
|
| 57 |
+
- "stock": Stok durumu soruyor
|
| 58 |
+
- "info": Genel bilgi istiyor
|
| 59 |
+
- "complaint": Şikayet ediyor
|
| 60 |
+
- "order": Sipariş vermek istiyor
|
| 61 |
+
- "test_ride": Test sürüşü istiyor
|
| 62 |
+
- "service": Servis/tamir istiyor
|
| 63 |
+
- "none": Belirgin bir niyet yok
|
| 64 |
+
|
| 65 |
+
ÖRNEK CÜMLELER:
|
| 66 |
+
- "bu bisikleti ayırtabilir misiniz" → reserve
|
| 67 |
+
- "yarın gelip alabilirim" → reserve
|
| 68 |
+
- "benim için tutun" → reserve
|
| 69 |
+
- "fiyatı nedir" → price
|
| 70 |
+
- "kaç para" → price
|
| 71 |
+
- "var mı" → stock
|
| 72 |
+
- "stokta mı" → stock
|
| 73 |
+
- "özellikleri nelerdir" → info
|
| 74 |
+
- "test edebilir miyim" → test_ride
|
| 75 |
+
|
| 76 |
+
ACİLİYET:
|
| 77 |
+
- high: Hemen aksiyon gerekiyor (ayırtma, hemen alma isteği)
|
| 78 |
+
- medium: Normal takip yeterli
|
| 79 |
+
- low: Bilgi amaçlı
|
| 80 |
+
|
| 81 |
+
JSON FORMATI:
|
| 82 |
+
{
|
| 83 |
+
"intents": ["reserve", "price"], // Birden fazla olabilir
|
| 84 |
+
"product": "FX 2", // Eğer üründen bahsediyorsa
|
| 85 |
+
"confidence": 0.95, // 0-1 arası güven skoru
|
| 86 |
+
"store": null, // Mağaza adı varsa
|
| 87 |
+
"urgency": "high",
|
| 88 |
+
"customer_mood": "positive",
|
| 89 |
+
"suggested_action": "notify_store", // notify_store, answer_directly, both, none
|
| 90 |
+
"notification_required": true,
|
| 91 |
+
"notification_reason": "Müşteri FX 2'yi ayırtmak istiyor"
|
| 92 |
+
}"""
|
| 93 |
+
|
| 94 |
+
# Bağlam varsa ekle
|
| 95 |
+
context_info = ""
|
| 96 |
+
if context:
|
| 97 |
+
if context.get("current_category"):
|
| 98 |
+
context_info = f"\nSON KONUŞULAN ÜRÜN: {context['current_category']}"
|
| 99 |
+
if context.get("messages"):
|
| 100 |
+
last_msg = context["messages"][-1] if context["messages"] else None
|
| 101 |
+
if last_msg:
|
| 102 |
+
context_info += f"\nÖNCEKİ MESAJ: {last_msg.get('user', '')}"
|
| 103 |
+
|
| 104 |
+
# GPT-5'e gönder
|
| 105 |
+
try:
|
| 106 |
+
messages = [
|
| 107 |
+
{"role": "system", "content": system_prompt},
|
| 108 |
+
{"role": "user", "content": f"Müşteri mesajı: \"{message}\"{context_info}\n\nJSON formatında analiz et:"}
|
| 109 |
+
]
|
| 110 |
+
|
| 111 |
+
# GPT-5 modelleri temperature ve max_tokens desteklemiyor
|
| 112 |
+
payload = {
|
| 113 |
+
"model": "gpt-5.2-chat-latest",
|
| 114 |
+
"messages": messages,
|
| 115 |
+
"response_format": {"type": "json_object"} # JSON garantisi
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
headers = {
|
| 119 |
+
"Content-Type": "application/json",
|
| 120 |
+
"Authorization": f"Bearer {OPENAI_API_KEY}"
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
response = requests.post(API_URL, headers=headers, json=payload, timeout=10)
|
| 124 |
+
|
| 125 |
+
if response.status_code == 200:
|
| 126 |
+
result = response.json()
|
| 127 |
+
analysis = json.loads(result['choices'][0]['message']['content'])
|
| 128 |
+
|
| 129 |
+
logger.info(f"🧠 Intent Analysis: {analysis}")
|
| 130 |
+
return analysis
|
| 131 |
+
else:
|
| 132 |
+
logger.error(f"GPT-5 API error: {response.status_code}")
|
| 133 |
+
return {"intents": [], "confidence": 0}
|
| 134 |
+
|
| 135 |
+
except Exception as e:
|
| 136 |
+
logger.error(f"Intent analysis error: {e}")
|
| 137 |
+
return {"intents": [], "confidence": 0}
|
| 138 |
+
|
| 139 |
+
def should_notify_store(analysis: Dict) -> Tuple[bool, str]:
|
| 140 |
+
"""
|
| 141 |
+
Mağazaya bildirim gönderilmeli mi?
|
| 142 |
+
|
| 143 |
+
Returns:
|
| 144 |
+
(should_notify, reason)
|
| 145 |
+
"""
|
| 146 |
+
|
| 147 |
+
# Yüksek güvenle rezervasyon talebi
|
| 148 |
+
if "reserve" in analysis.get("intents", []) and analysis.get("confidence", 0) > 0.7:
|
| 149 |
+
return True, "Müşteri ürünü ayırtmak istiyor"
|
| 150 |
+
|
| 151 |
+
# Acil stok sorusu
|
| 152 |
+
if "stock" in analysis.get("intents", []) and analysis.get("urgency") == "high":
|
| 153 |
+
return True, "Müşteri acil stok bilgisi istiyor"
|
| 154 |
+
|
| 155 |
+
# Test sürüşü talebi
|
| 156 |
+
if "test_ride" in analysis.get("intents", []):
|
| 157 |
+
return True, "Müşteri test sürüşü talep ediyor"
|
| 158 |
+
|
| 159 |
+
# Sipariş vermek istiyor
|
| 160 |
+
if "order" in analysis.get("intents", []) and analysis.get("confidence", 0) > 0.8:
|
| 161 |
+
return True, "Müşteri sipariş vermek istiyor"
|
| 162 |
+
|
| 163 |
+
# Şikayet varsa
|
| 164 |
+
if "complaint" in analysis.get("intents", []):
|
| 165 |
+
return True, "Müşteri şikayette bulunuyor"
|
| 166 |
+
|
| 167 |
+
# GPT öneriyorsa
|
| 168 |
+
if analysis.get("suggested_action") in ["notify_store", "both"]:
|
| 169 |
+
return True, analysis.get("notification_reason", "GPT-5 bildirim öneriyor")
|
| 170 |
+
|
| 171 |
+
return False, ""
|
| 172 |
+
|
| 173 |
+
def get_smart_notification_message(
|
| 174 |
+
analysis: Dict,
|
| 175 |
+
customer_phone: str,
|
| 176 |
+
original_message: str
|
| 177 |
+
) -> str:
|
| 178 |
+
"""
|
| 179 |
+
Analiz sonucuna göre akıllı bildirim mesajı oluştur
|
| 180 |
+
"""
|
| 181 |
+
|
| 182 |
+
intents = analysis.get("intents", [])
|
| 183 |
+
product = analysis.get("product", "Belirtilmemiş")
|
| 184 |
+
urgency = analysis.get("urgency", "medium")
|
| 185 |
+
mood = analysis.get("customer_mood", "neutral")
|
| 186 |
+
|
| 187 |
+
# Emoji seçimi
|
| 188 |
+
if "reserve" in intents:
|
| 189 |
+
emoji = "🔔"
|
| 190 |
+
title = "AYIRTMA TALEBİ"
|
| 191 |
+
elif "price" in intents:
|
| 192 |
+
emoji = "💰"
|
| 193 |
+
title = "FİYAT SORUSU"
|
| 194 |
+
elif "stock" in intents:
|
| 195 |
+
emoji = "📦"
|
| 196 |
+
title = "STOK SORUSU"
|
| 197 |
+
elif "test_ride" in intents:
|
| 198 |
+
emoji = "🚴"
|
| 199 |
+
title = "TEST SÜRÜŞÜ TALEBİ"
|
| 200 |
+
elif "complaint" in intents:
|
| 201 |
+
emoji = "⚠️"
|
| 202 |
+
title = "ŞİKAYET"
|
| 203 |
+
elif "order" in intents:
|
| 204 |
+
emoji = "🛒"
|
| 205 |
+
title = "SİPARİŞ TALEBİ"
|
| 206 |
+
else:
|
| 207 |
+
emoji = "ℹ️"
|
| 208 |
+
title = "MÜŞTERİ TALEBİ"
|
| 209 |
+
|
| 210 |
+
# Aciliyet vurgusu
|
| 211 |
+
if urgency == "high":
|
| 212 |
+
title = f"🚨 ACİL - {title}"
|
| 213 |
+
|
| 214 |
+
# Mesaj oluştur
|
| 215 |
+
from datetime import datetime
|
| 216 |
+
now = datetime.now()
|
| 217 |
+
date_str = now.strftime("%d.%m.%Y %H:%M")
|
| 218 |
+
|
| 219 |
+
message_parts = [
|
| 220 |
+
f"{emoji} **{title}**",
|
| 221 |
+
f"📅 {date_str}",
|
| 222 |
+
"",
|
| 223 |
+
f"👤 **Müşteri:** {customer_phone.replace('whatsapp:', '')}",
|
| 224 |
+
f"🚲 **Ürün:** {product}",
|
| 225 |
+
f"💬 **Mesaj:** \"{original_message}\"",
|
| 226 |
+
"",
|
| 227 |
+
"📊 **AI Analizi:**"
|
| 228 |
+
]
|
| 229 |
+
|
| 230 |
+
# Niyet listesi
|
| 231 |
+
intent_map = {
|
| 232 |
+
"reserve": "✓ Ayırtma isteği",
|
| 233 |
+
"price": "✓ Fiyat bilgisi",
|
| 234 |
+
"stock": "✓ Stok durumu",
|
| 235 |
+
"test_ride": "✓ Test sürüşü",
|
| 236 |
+
"order": "✓ Sipariş",
|
| 237 |
+
"complaint": "✓ Şikayet",
|
| 238 |
+
"info": "✓ Bilgi talebi"
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
for intent in intents:
|
| 242 |
+
if intent in intent_map:
|
| 243 |
+
message_parts.append(f" {intent_map[intent]}")
|
| 244 |
+
|
| 245 |
+
# Müşteri durumu
|
| 246 |
+
mood_map = {
|
| 247 |
+
"positive": "😊 Pozitif",
|
| 248 |
+
"neutral": "😐 Nötr",
|
| 249 |
+
"negative": "😟 Negatif"
|
| 250 |
+
}
|
| 251 |
+
message_parts.append(f" Müşteri Durumu: {mood_map.get(mood, mood)}")
|
| 252 |
+
message_parts.append(f" Aciliyet: {'🔴 Yüksek' if urgency == 'high' else '🟡 Orta' if urgency == 'medium' else '🟢 Düşük'}")
|
| 253 |
+
|
| 254 |
+
# Önerilen aksiyon
|
| 255 |
+
message_parts.extend([
|
| 256 |
+
"",
|
| 257 |
+
"✅ **Önerilen Aksiyon:**"
|
| 258 |
+
])
|
| 259 |
+
|
| 260 |
+
if "reserve" in intents:
|
| 261 |
+
message_parts.extend([
|
| 262 |
+
"1. Stok kontrolü yapın",
|
| 263 |
+
"2. Müşteriyi hemen arayın",
|
| 264 |
+
"3. Ödeme ve teslimat planı belirleyin"
|
| 265 |
+
])
|
| 266 |
+
elif "test_ride" in intents:
|
| 267 |
+
message_parts.extend([
|
| 268 |
+
"1. Test bisikleti hazırlığı",
|
| 269 |
+
"2. Randevu ayarlayın",
|
| 270 |
+
"3. Kimlik ve güvenlik prosedürü"
|
| 271 |
+
])
|
| 272 |
+
elif "complaint" in intents:
|
| 273 |
+
message_parts.extend([
|
| 274 |
+
"1. Müşteriyi hemen arayın",
|
| 275 |
+
"2. Sorunu dinleyin",
|
| 276 |
+
"3. Çözüm önerisi sunun"
|
| 277 |
+
])
|
| 278 |
+
else:
|
| 279 |
+
message_parts.append("Müşteri ile iletişime geçin")
|
| 280 |
+
|
| 281 |
+
message_parts.extend([
|
| 282 |
+
"",
|
| 283 |
+
"---",
|
| 284 |
+
"Trek AI Assistant"
|
| 285 |
+
])
|
| 286 |
+
|
| 287 |
+
return "\n".join(message_parts)
|
| 288 |
+
|
| 289 |
+
# Test fonksiyonu
|
| 290 |
+
def test_intent_analysis():
|
| 291 |
+
"""Test senaryoları"""
|
| 292 |
+
|
| 293 |
+
test_messages = [
|
| 294 |
+
"FX 2'yi ayırtabilir misiniz?",
|
| 295 |
+
"Marlin 5'in fiyatı ne kadar?",
|
| 296 |
+
"Stokta var mı?",
|
| 297 |
+
"Yarın gelip alabilirim",
|
| 298 |
+
"Test sürüşü yapabilir miyim?",
|
| 299 |
+
"Bisikletim bozuldu",
|
| 300 |
+
"Teşekkürler",
|
| 301 |
+
"Merhaba",
|
| 302 |
+
"Sipariş vermek istiyorum"
|
| 303 |
+
]
|
| 304 |
+
|
| 305 |
+
print("\n" + "="*60)
|
| 306 |
+
print("INTENT ANALYSIS TEST")
|
| 307 |
+
print("="*60)
|
| 308 |
+
|
| 309 |
+
for msg in test_messages:
|
| 310 |
+
print(f"\n📝 Mesaj: \"{msg}\"")
|
| 311 |
+
analysis = analyze_customer_intent(msg)
|
| 312 |
+
|
| 313 |
+
if analysis.get("intents"):
|
| 314 |
+
print(f"🎯 Niyetler: {', '.join(analysis['intents'])}")
|
| 315 |
+
print(f"📊 Güven: {analysis.get('confidence', 0):.2%}")
|
| 316 |
+
print(f"🚨 Aciliyet: {analysis.get('urgency', 'unknown')}")
|
| 317 |
+
|
| 318 |
+
should_notify, reason = should_notify_store(analysis)
|
| 319 |
+
print(f"🔔 Bildirim: {'EVET' if should_notify else 'HAYIR'}")
|
| 320 |
+
if reason:
|
| 321 |
+
print(f" Sebep: {reason}")
|
| 322 |
+
else:
|
| 323 |
+
print("❌ Niyet tespit edilemedi")
|
| 324 |
+
|
| 325 |
+
print("\n" + "="*60)
|
| 326 |
+
|
| 327 |
+
if __name__ == "__main__":
|
| 328 |
+
test_intent_analysis()
|
media_queue_v2.py
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
|
| 4 |
+
"""
|
| 5 |
+
WhatsApp Media Queue V2 - Global Cache ile
|
| 6 |
+
Medya URL'lerini kaybetmeden, mesajları akıllıca birleştirir
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import time
|
| 10 |
+
import threading
|
| 11 |
+
from typing import Dict, Optional, List, Tuple
|
| 12 |
+
import logging
|
| 13 |
+
|
| 14 |
+
logger = logging.getLogger(__name__)
|
| 15 |
+
|
| 16 |
+
class MediaQueueV2:
|
| 17 |
+
"""
|
| 18 |
+
Media Queue V2 - Basit ve güvenilir
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
def __init__(self, wait_time: float = 3.0):
|
| 22 |
+
"""
|
| 23 |
+
Args:
|
| 24 |
+
wait_time: Medya sonrası bekleme süresi (saniye)
|
| 25 |
+
"""
|
| 26 |
+
self.wait_time = wait_time
|
| 27 |
+
# Global media cache - {user_id: {"media_urls": [...], "media_types": [...], "timestamp": ..., "caption": ...}}
|
| 28 |
+
self.media_cache: Dict[str, Dict] = {}
|
| 29 |
+
# Timer'lar - {user_id: threading.Timer}
|
| 30 |
+
self.timers: Dict[str, threading.Timer] = {}
|
| 31 |
+
# Lock for thread safety
|
| 32 |
+
self.lock = threading.Lock()
|
| 33 |
+
|
| 34 |
+
def handle_media(self, user_id: str, media_urls: List[str], media_types: List[str], caption: str = "") -> str:
|
| 35 |
+
"""
|
| 36 |
+
Medya mesajı geldiğinde çağrılır
|
| 37 |
+
|
| 38 |
+
Args:
|
| 39 |
+
user_id: WhatsApp kullanıcı numarası
|
| 40 |
+
media_urls: Medya URL listesi
|
| 41 |
+
media_types: Medya tipi listesi
|
| 42 |
+
caption: Medya başlığı (varsa)
|
| 43 |
+
|
| 44 |
+
Returns:
|
| 45 |
+
Kullanıcıya gönderilecek bekleme mesajı
|
| 46 |
+
"""
|
| 47 |
+
with self.lock:
|
| 48 |
+
# Önceki timer'ı iptal et (varsa)
|
| 49 |
+
self._cancel_timer(user_id)
|
| 50 |
+
|
| 51 |
+
# Media'yı cache'e kaydet
|
| 52 |
+
self.media_cache[user_id] = {
|
| 53 |
+
"media_urls": media_urls,
|
| 54 |
+
"media_types": media_types,
|
| 55 |
+
"caption": caption,
|
| 56 |
+
"timestamp": time.time()
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
logger.info(f"📸 {user_id}: Medya cache'e kaydedildi - {len(media_urls)} dosya")
|
| 60 |
+
|
| 61 |
+
# Yeni timer başlat
|
| 62 |
+
self._start_timer(user_id)
|
| 63 |
+
|
| 64 |
+
# Medya tipine göre bekleme mesajı
|
| 65 |
+
if media_types and "image" in media_types[0].lower():
|
| 66 |
+
return "🖼️ Görsel alındı. Hakkında sormak istediğiniz bir şey var mı?"
|
| 67 |
+
elif media_types and "video" in media_types[0].lower():
|
| 68 |
+
return "🎥 Video alındı. Hakkında sormak istediğiniz bir şey var mı?"
|
| 69 |
+
elif media_types and "audio" in media_types[0].lower():
|
| 70 |
+
return "🎵 Ses dosyası alındı. Hakkında sormak istediğiniz bir şey var mı?"
|
| 71 |
+
else:
|
| 72 |
+
return "📎 Dosya alındı. Hakkında sormak istediğiniz bir şey var mı?"
|
| 73 |
+
|
| 74 |
+
def handle_text(self, user_id: str, text: str) -> Tuple[Optional[str], Optional[List[str]], Optional[List[str]]]:
|
| 75 |
+
"""
|
| 76 |
+
Metin mesajı geldiğinde çağrılır
|
| 77 |
+
|
| 78 |
+
Args:
|
| 79 |
+
user_id: WhatsApp kullanıcı numarası
|
| 80 |
+
text: Metin mesajı
|
| 81 |
+
|
| 82 |
+
Returns:
|
| 83 |
+
(combined_text, media_urls, media_types) veya (None, None, None)
|
| 84 |
+
"""
|
| 85 |
+
with self.lock:
|
| 86 |
+
# Cache'de medya var mı?
|
| 87 |
+
if user_id in self.media_cache:
|
| 88 |
+
# Timer'ı iptal et
|
| 89 |
+
self._cancel_timer(user_id)
|
| 90 |
+
|
| 91 |
+
# Cache'den medya bilgilerini al
|
| 92 |
+
cached = self.media_cache[user_id]
|
| 93 |
+
media_urls = cached["media_urls"]
|
| 94 |
+
media_types = cached["media_types"]
|
| 95 |
+
caption = cached["caption"]
|
| 96 |
+
|
| 97 |
+
# Cache'i temizle
|
| 98 |
+
del self.media_cache[user_id]
|
| 99 |
+
|
| 100 |
+
# Mesajları birleştir
|
| 101 |
+
combined_text = self._combine_messages(caption, text)
|
| 102 |
+
|
| 103 |
+
logger.info(f"✅ {user_id}: Medya + metin birleştirildi")
|
| 104 |
+
logger.info(f" Birleşik mesaj: {combined_text[:100]}...")
|
| 105 |
+
logger.info(f" Medya URL'leri: {media_urls}")
|
| 106 |
+
|
| 107 |
+
return combined_text, media_urls, media_types
|
| 108 |
+
|
| 109 |
+
# Cache'de medya yoksa normal metin olarak dön
|
| 110 |
+
logger.info(f"💬 {user_id}: Normal metin mesajı (cache'de medya yok)")
|
| 111 |
+
return None, None, None
|
| 112 |
+
|
| 113 |
+
def _combine_messages(self, caption: str, text: str) -> str:
|
| 114 |
+
"""
|
| 115 |
+
Caption ve metin mesajını birleştir
|
| 116 |
+
|
| 117 |
+
Args:
|
| 118 |
+
caption: Medya başlığı
|
| 119 |
+
text: Kullanıcının sonraki mesajı
|
| 120 |
+
|
| 121 |
+
Returns:
|
| 122 |
+
Birleştirilmiş mesaj
|
| 123 |
+
"""
|
| 124 |
+
parts = []
|
| 125 |
+
|
| 126 |
+
if caption and caption.strip():
|
| 127 |
+
parts.append(caption.strip())
|
| 128 |
+
|
| 129 |
+
if text and text.strip():
|
| 130 |
+
parts.append(text.strip())
|
| 131 |
+
|
| 132 |
+
# Eğer hiç metin yoksa varsayılan
|
| 133 |
+
if not parts:
|
| 134 |
+
return "Bu görseli analiz et."
|
| 135 |
+
|
| 136 |
+
# Birleştir
|
| 137 |
+
combined = " ".join(parts)
|
| 138 |
+
|
| 139 |
+
# Eğer sadece caption varsa ve soru işareti yoksa, analiz isteği ekle
|
| 140 |
+
if len(parts) == 1 and "?" not in combined and len(combined) < 20:
|
| 141 |
+
combined += ". Bu hakkında bilgi ver."
|
| 142 |
+
|
| 143 |
+
return combined
|
| 144 |
+
|
| 145 |
+
def _start_timer(self, user_id: str):
|
| 146 |
+
"""Timeout timer'ını başlat"""
|
| 147 |
+
timer = threading.Timer(self.wait_time, self._handle_timeout, args=[user_id])
|
| 148 |
+
timer.start()
|
| 149 |
+
self.timers[user_id] = timer
|
| 150 |
+
logger.debug(f"⏰ {user_id}: {self.wait_time}s timer başlatıldı")
|
| 151 |
+
|
| 152 |
+
def _cancel_timer(self, user_id: str):
|
| 153 |
+
"""Timer'ı iptal et"""
|
| 154 |
+
if user_id in self.timers:
|
| 155 |
+
self.timers[user_id].cancel()
|
| 156 |
+
del self.timers[user_id]
|
| 157 |
+
logger.debug(f"⏰ {user_id}: Timer iptal edildi")
|
| 158 |
+
|
| 159 |
+
def _handle_timeout(self, user_id: str):
|
| 160 |
+
"""
|
| 161 |
+
Timeout olduğunda çağrılır
|
| 162 |
+
Sadece medyayı işlemek için kullanılabilir
|
| 163 |
+
"""
|
| 164 |
+
with self.lock:
|
| 165 |
+
if user_id in self.media_cache:
|
| 166 |
+
logger.info(f"⏱️ {user_id}: Timeout - metin beklenmedi, sadece medya işlenecek")
|
| 167 |
+
# Burada cache'i temizlemiyoruz, bir sonraki mesajda kullanılabilir
|
| 168 |
+
# Ama 5 dakikadan eski cache'leri temizleyebiliriz
|
| 169 |
+
self._cleanup_old_cache()
|
| 170 |
+
|
| 171 |
+
def _cleanup_old_cache(self):
|
| 172 |
+
"""5 dakikadan eski cache'leri temizle"""
|
| 173 |
+
current_time = time.time()
|
| 174 |
+
expired_users = []
|
| 175 |
+
|
| 176 |
+
for user_id, cached in self.media_cache.items():
|
| 177 |
+
if current_time - cached["timestamp"] > 300: # 5 dakika
|
| 178 |
+
expired_users.append(user_id)
|
| 179 |
+
|
| 180 |
+
for user_id in expired_users:
|
| 181 |
+
del self.media_cache[user_id]
|
| 182 |
+
logger.debug(f"🗑️ {user_id}: Eski cache temizlendi")
|
| 183 |
+
|
| 184 |
+
def get_cache_status(self, user_id: str) -> bool:
|
| 185 |
+
"""
|
| 186 |
+
Kullanıcının cache'inde medya var mı?
|
| 187 |
+
|
| 188 |
+
Args:
|
| 189 |
+
user_id: WhatsApp kullanıcı numarası
|
| 190 |
+
|
| 191 |
+
Returns:
|
| 192 |
+
True: Cache'de medya var
|
| 193 |
+
False: Cache boş
|
| 194 |
+
"""
|
| 195 |
+
with self.lock:
|
| 196 |
+
return user_id in self.media_cache
|
| 197 |
+
|
| 198 |
+
def clear_user_cache(self, user_id: str):
|
| 199 |
+
"""
|
| 200 |
+
Belirli bir kullanıcının cache'ini temizle
|
| 201 |
+
|
| 202 |
+
Args:
|
| 203 |
+
user_id: WhatsApp kullanıcı numarası
|
| 204 |
+
"""
|
| 205 |
+
with self.lock:
|
| 206 |
+
if user_id in self.media_cache:
|
| 207 |
+
del self.media_cache[user_id]
|
| 208 |
+
logger.info(f"🗑️ {user_id}: Cache manuel olarak temizlendi")
|
| 209 |
+
|
| 210 |
+
self._cancel_timer(user_id)
|
| 211 |
+
|
| 212 |
+
# Global instance
|
| 213 |
+
media_queue = MediaQueueV2(wait_time=3.0)
|
| 214 |
+
|
| 215 |
+
# Test fonksiyonu
|
| 216 |
+
def test_media_queue():
|
| 217 |
+
"""Test senaryoları"""
|
| 218 |
+
|
| 219 |
+
print("\n" + "="*60)
|
| 220 |
+
print("Media Queue V2 Test")
|
| 221 |
+
print("="*60)
|
| 222 |
+
|
| 223 |
+
# Test 1: Medya + Metin
|
| 224 |
+
print("\n📱 Test 1: Görsel + Soru")
|
| 225 |
+
print("-"*40)
|
| 226 |
+
|
| 227 |
+
# Görsel gönder
|
| 228 |
+
response = media_queue.handle_media(
|
| 229 |
+
"user123",
|
| 230 |
+
["https://example.com/image.jpg"],
|
| 231 |
+
["image/jpeg"],
|
| 232 |
+
"Bugünkü bisiklet"
|
| 233 |
+
)
|
| 234 |
+
print(f"Bot: {response}")
|
| 235 |
+
|
| 236 |
+
# 1 saniye bekle
|
| 237 |
+
time.sleep(1)
|
| 238 |
+
|
| 239 |
+
# Soru sor
|
| 240 |
+
combined, urls, types = media_queue.handle_text("user123", "Fiyatı ne kadar?")
|
| 241 |
+
|
| 242 |
+
if combined:
|
| 243 |
+
print(f"\n✅ Birleştirildi:")
|
| 244 |
+
print(f" Mesaj: {combined}")
|
| 245 |
+
print(f" URLs: {urls}")
|
| 246 |
+
print(f" Types: {types}")
|
| 247 |
+
|
| 248 |
+
# Test 2: Sadece metin
|
| 249 |
+
print("\n📱 Test 2: Sadece Metin")
|
| 250 |
+
print("-"*40)
|
| 251 |
+
|
| 252 |
+
combined, urls, types = media_queue.handle_text("user456", "Merhaba")
|
| 253 |
+
|
| 254 |
+
if combined:
|
| 255 |
+
print("Birleştirilmiş mesaj var")
|
| 256 |
+
else:
|
| 257 |
+
print("Normal metin mesajı (birleştirme yok)")
|
| 258 |
+
|
| 259 |
+
# Test 3: Medya + Timeout
|
| 260 |
+
print("\n📱 Test 3: Görsel + Timeout")
|
| 261 |
+
print("-"*40)
|
| 262 |
+
|
| 263 |
+
response = media_queue.handle_media(
|
| 264 |
+
"user789",
|
| 265 |
+
["https://example.com/image2.jpg"],
|
| 266 |
+
["image/jpeg"],
|
| 267 |
+
""
|
| 268 |
+
)
|
| 269 |
+
print(f"Bot: {response}")
|
| 270 |
+
|
| 271 |
+
print("3 saniye bekleniyor...")
|
| 272 |
+
time.sleep(4)
|
| 273 |
+
|
| 274 |
+
# Timeout sonrası cache durumu
|
| 275 |
+
if media_queue.get_cache_status("user789"):
|
| 276 |
+
print("✅ Medya hala cache'de (bir sonraki mesajda kullanılabilir)")
|
| 277 |
+
else:
|
| 278 |
+
print("❌ Cache temizlenmiş")
|
| 279 |
+
|
| 280 |
+
if __name__ == "__main__":
|
| 281 |
+
test_media_queue()
|
product_search.py
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Professional Product Search Engine for Trek Chatbot
|
| 3 |
+
Implements intelligent product matching with fuzzy search and NLP techniques
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import re
|
| 7 |
+
from difflib import SequenceMatcher
|
| 8 |
+
from typing import List, Tuple, Dict, Optional
|
| 9 |
+
import unicodedata
|
| 10 |
+
|
| 11 |
+
class ProductSearchEngine:
|
| 12 |
+
"""Advanced product search with intelligent matching"""
|
| 13 |
+
|
| 14 |
+
def __init__(self, products: List[Tuple]):
|
| 15 |
+
"""
|
| 16 |
+
Initialize with products list
|
| 17 |
+
products: List of tuples (short_name, product_info, full_name)
|
| 18 |
+
"""
|
| 19 |
+
self.products = products
|
| 20 |
+
self.product_index = self._build_index()
|
| 21 |
+
|
| 22 |
+
def _build_index(self) -> Dict:
|
| 23 |
+
"""Build search index for faster lookups"""
|
| 24 |
+
index = {
|
| 25 |
+
'by_name': {},
|
| 26 |
+
'by_words': {},
|
| 27 |
+
'by_category': {},
|
| 28 |
+
'by_model': {},
|
| 29 |
+
'normalized': {}
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
for product in self.products:
|
| 33 |
+
short_name = product[0]
|
| 34 |
+
full_name = product[2]
|
| 35 |
+
|
| 36 |
+
# Normalize and store
|
| 37 |
+
normalized_full = self._normalize_text(full_name)
|
| 38 |
+
normalized_short = self._normalize_text(short_name)
|
| 39 |
+
|
| 40 |
+
# Store by full name
|
| 41 |
+
index['by_name'][normalized_full] = product
|
| 42 |
+
index['normalized'][normalized_full] = full_name
|
| 43 |
+
|
| 44 |
+
# Extract and index words
|
| 45 |
+
words = normalized_full.split()
|
| 46 |
+
for word in words:
|
| 47 |
+
if len(word) > 2: # Skip very short words
|
| 48 |
+
if word not in index['by_words']:
|
| 49 |
+
index['by_words'][word] = []
|
| 50 |
+
index['by_words'][word].append(product)
|
| 51 |
+
|
| 52 |
+
# Extract model numbers and categories
|
| 53 |
+
model_match = re.search(r'\b(\d+\.?\d*)\b', full_name)
|
| 54 |
+
if model_match:
|
| 55 |
+
model_num = model_match.group(1)
|
| 56 |
+
if model_num not in index['by_model']:
|
| 57 |
+
index['by_model'][model_num] = []
|
| 58 |
+
index['by_model'][model_num].append(product)
|
| 59 |
+
|
| 60 |
+
# Category extraction (first word often represents category)
|
| 61 |
+
if words:
|
| 62 |
+
category = words[0]
|
| 63 |
+
if category not in index['by_category']:
|
| 64 |
+
index['by_category'][category] = []
|
| 65 |
+
index['by_category'][category].append(product)
|
| 66 |
+
|
| 67 |
+
return index
|
| 68 |
+
|
| 69 |
+
def _normalize_text(self, text: str) -> str:
|
| 70 |
+
"""Normalize text for better matching"""
|
| 71 |
+
if not text:
|
| 72 |
+
return ""
|
| 73 |
+
|
| 74 |
+
# Convert to lowercase
|
| 75 |
+
text = text.lower()
|
| 76 |
+
|
| 77 |
+
# Remove Turkish characters
|
| 78 |
+
replacements = {
|
| 79 |
+
'ı': 'i', 'İ': 'i', 'ş': 's', 'Ş': 's',
|
| 80 |
+
'ğ': 'g', 'Ğ': 'g', 'ü': 'u', 'Ü': 'u',
|
| 81 |
+
'ö': 'o', 'Ö': 'o', 'ç': 'c', 'Ç': 'c'
|
| 82 |
+
}
|
| 83 |
+
for tr_char, eng_char in replacements.items():
|
| 84 |
+
text = text.replace(tr_char, eng_char)
|
| 85 |
+
|
| 86 |
+
# Remove special characters but keep spaces and numbers
|
| 87 |
+
text = re.sub(r'[^\w\s\d\.]', ' ', text)
|
| 88 |
+
|
| 89 |
+
# Normalize whitespace
|
| 90 |
+
text = ' '.join(text.split())
|
| 91 |
+
|
| 92 |
+
return text
|
| 93 |
+
|
| 94 |
+
def _calculate_similarity(self, str1: str, str2: str) -> float:
|
| 95 |
+
"""Calculate similarity between two strings"""
|
| 96 |
+
return SequenceMatcher(None, str1, str2).ratio()
|
| 97 |
+
|
| 98 |
+
def search(self, query: str, threshold: float = 0.6) -> List[Tuple[float, Tuple]]:
|
| 99 |
+
"""
|
| 100 |
+
Search for products matching the query
|
| 101 |
+
Returns list of (score, product) tuples sorted by relevance
|
| 102 |
+
"""
|
| 103 |
+
query_normalized = self._normalize_text(query)
|
| 104 |
+
query_words = query_normalized.split()
|
| 105 |
+
|
| 106 |
+
results = {}
|
| 107 |
+
|
| 108 |
+
# 1. Exact match
|
| 109 |
+
if query_normalized in self.product_index['by_name']:
|
| 110 |
+
product = self.product_index['by_name'][query_normalized]
|
| 111 |
+
results[id(product)] = (1.0, product)
|
| 112 |
+
|
| 113 |
+
# 2. Model number search
|
| 114 |
+
model_match = re.search(r'\b(\d+\.?\d*)\b', query)
|
| 115 |
+
if model_match:
|
| 116 |
+
model_num = model_match.group(1)
|
| 117 |
+
if model_num in self.product_index['by_model']:
|
| 118 |
+
for product in self.product_index['by_model'][model_num]:
|
| 119 |
+
if id(product) not in results:
|
| 120 |
+
# Check if model number is in correct context
|
| 121 |
+
score = 0.9 if model_num in product[2].lower() else 0.7
|
| 122 |
+
results[id(product)] = (score, product)
|
| 123 |
+
|
| 124 |
+
# 3. Word-based search with scoring
|
| 125 |
+
word_matches = {}
|
| 126 |
+
for word in query_words:
|
| 127 |
+
if len(word) > 2 and word in self.product_index['by_words']:
|
| 128 |
+
for product in self.product_index['by_words'][word]:
|
| 129 |
+
if id(product) not in word_matches:
|
| 130 |
+
word_matches[id(product)] = {'count': 0, 'product': product}
|
| 131 |
+
word_matches[id(product)]['count'] += 1
|
| 132 |
+
|
| 133 |
+
# Calculate word match scores
|
| 134 |
+
for product_id, match_info in word_matches.items():
|
| 135 |
+
product = match_info['product']
|
| 136 |
+
matched_count = match_info['count']
|
| 137 |
+
total_query_words = len([w for w in query_words if len(w) > 2])
|
| 138 |
+
|
| 139 |
+
if total_query_words > 0:
|
| 140 |
+
word_score = matched_count / total_query_words
|
| 141 |
+
|
| 142 |
+
# Boost score if all important words match
|
| 143 |
+
if matched_count == total_query_words:
|
| 144 |
+
word_score = min(word_score * 1.2, 0.95)
|
| 145 |
+
|
| 146 |
+
# Check word order for better scoring
|
| 147 |
+
product_text = self._normalize_text(product[2])
|
| 148 |
+
if query_normalized in product_text:
|
| 149 |
+
word_score = min(word_score * 1.3, 0.98)
|
| 150 |
+
|
| 151 |
+
if id(product) not in results or results[id(product)][0] < word_score:
|
| 152 |
+
results[id(product)] = (word_score, product)
|
| 153 |
+
|
| 154 |
+
# 4. Fuzzy matching for all products
|
| 155 |
+
for product in self.products:
|
| 156 |
+
product_normalized = self._normalize_text(product[2])
|
| 157 |
+
similarity = self._calculate_similarity(query_normalized, product_normalized)
|
| 158 |
+
|
| 159 |
+
# Substring matching
|
| 160 |
+
if query_normalized in product_normalized:
|
| 161 |
+
similarity = max(similarity, 0.8)
|
| 162 |
+
|
| 163 |
+
# Check if product contains all query words (in any order)
|
| 164 |
+
if all(word in product_normalized for word in query_words if len(word) > 2):
|
| 165 |
+
similarity = max(similarity, 0.75)
|
| 166 |
+
|
| 167 |
+
if similarity >= threshold:
|
| 168 |
+
if id(product) not in results or results[id(product)][0] < similarity:
|
| 169 |
+
results[id(product)] = (similarity, product)
|
| 170 |
+
|
| 171 |
+
# 5. Category-based fallback
|
| 172 |
+
if not results and query_words:
|
| 173 |
+
category = query_words[0]
|
| 174 |
+
if category in self.product_index['by_category']:
|
| 175 |
+
for product in self.product_index['by_category'][category]:
|
| 176 |
+
results[id(product)] = (0.5, product)
|
| 177 |
+
|
| 178 |
+
# Convert to list and sort by score
|
| 179 |
+
result_list = list(results.values())
|
| 180 |
+
result_list.sort(key=lambda x: x[0], reverse=True)
|
| 181 |
+
|
| 182 |
+
return result_list
|
| 183 |
+
|
| 184 |
+
def find_best_match(self, query: str) -> Optional[Tuple]:
|
| 185 |
+
"""Find the single best matching product"""
|
| 186 |
+
results = self.search(query)
|
| 187 |
+
if results and results[0][0] >= 0.6:
|
| 188 |
+
return results[0][1]
|
| 189 |
+
return None
|
| 190 |
+
|
| 191 |
+
def find_similar_products(self, product_name: str, limit: int = 5) -> List[Tuple]:
|
| 192 |
+
"""Find products similar to the given product name"""
|
| 193 |
+
results = self.search(product_name)
|
| 194 |
+
similar = []
|
| 195 |
+
|
| 196 |
+
# Skip the first result if it's an exact match
|
| 197 |
+
start_idx = 1 if results and results[0][0] > 0.95 else 0
|
| 198 |
+
|
| 199 |
+
for score, product in results[start_idx:start_idx + limit]:
|
| 200 |
+
if score >= 0.5:
|
| 201 |
+
similar.append(product)
|
| 202 |
+
|
| 203 |
+
return similar
|
| 204 |
+
|
| 205 |
+
def extract_product_context(self, query: str) -> Dict:
|
| 206 |
+
"""Extract context from query (size, color, type, etc.)"""
|
| 207 |
+
context = {
|
| 208 |
+
'sizes': [],
|
| 209 |
+
'colors': [],
|
| 210 |
+
'types': [],
|
| 211 |
+
'features': [],
|
| 212 |
+
'price_range': None
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
# Size detection
|
| 216 |
+
size_patterns = [
|
| 217 |
+
r'\b(xs|s|m|l|xl|xxl|2xl|3xl)\b',
|
| 218 |
+
r'\b(\d{2})\b(?=\s*beden|\s*numara|$)', # 44, 46, etc.
|
| 219 |
+
r'\b(small|medium|large)\b'
|
| 220 |
+
]
|
| 221 |
+
for pattern in size_patterns:
|
| 222 |
+
matches = re.findall(pattern, query.lower())
|
| 223 |
+
context['sizes'].extend(matches)
|
| 224 |
+
|
| 225 |
+
# Color detection
|
| 226 |
+
colors = ['siyah', 'beyaz', 'mavi', 'kirmizi', 'yesil', 'gri', 'turuncu',
|
| 227 |
+
'black', 'white', 'blue', 'red', 'green', 'grey', 'gray', 'orange']
|
| 228 |
+
for color in colors:
|
| 229 |
+
if color in query.lower():
|
| 230 |
+
context['colors'].append(color)
|
| 231 |
+
|
| 232 |
+
# Type detection
|
| 233 |
+
types = ['erkek', 'kadin', 'cocuk', 'yol', 'dag', 'sehir', 'elektrikli',
|
| 234 |
+
'karbon', 'aluminyum', 'gravel', 'hybrid']
|
| 235 |
+
for type_word in types:
|
| 236 |
+
if type_word in query.lower():
|
| 237 |
+
context['types'].append(type_word)
|
| 238 |
+
|
| 239 |
+
# Feature detection
|
| 240 |
+
features = ['disk fren', 'shimano', 'sram', 'karbon', 'aluminyum',
|
| 241 |
+
'hidrolik', 'mekanik', '29 jant', '27.5 jant']
|
| 242 |
+
for feature in features:
|
| 243 |
+
if feature in query.lower():
|
| 244 |
+
context['features'].append(feature)
|
| 245 |
+
|
| 246 |
+
# Price range detection
|
| 247 |
+
price_match = re.search(r'(\d+)\.?(\d*)\s*(bin|tl)', query.lower())
|
| 248 |
+
if price_match:
|
| 249 |
+
price = float(price_match.group(1) + ('.' + price_match.group(2) if price_match.group(2) else ''))
|
| 250 |
+
if 'bin' in price_match.group(3):
|
| 251 |
+
price *= 1000
|
| 252 |
+
context['price_range'] = price
|
| 253 |
+
|
| 254 |
+
return context
|
| 255 |
+
|
| 256 |
+
def generate_suggestions(self, failed_query: str) -> List[str]:
|
| 257 |
+
"""Generate suggestions for failed searches"""
|
| 258 |
+
suggestions = []
|
| 259 |
+
query_normalized = self._normalize_text(failed_query)
|
| 260 |
+
query_words = query_normalized.split()
|
| 261 |
+
|
| 262 |
+
# Find products with partial matches
|
| 263 |
+
partial_matches = set()
|
| 264 |
+
for word in query_words:
|
| 265 |
+
if len(word) > 3:
|
| 266 |
+
for product_word in self.product_index['by_words']:
|
| 267 |
+
if word in product_word or product_word in word:
|
| 268 |
+
partial_matches.add(product_word)
|
| 269 |
+
|
| 270 |
+
# Generate suggestions from partial matches
|
| 271 |
+
for match in list(partial_matches)[:5]:
|
| 272 |
+
if match in self.product_index['by_words']:
|
| 273 |
+
products = self.product_index['by_words'][match]
|
| 274 |
+
if products:
|
| 275 |
+
suggestions.append(products[0][2])
|
| 276 |
+
|
| 277 |
+
# Add category suggestions
|
| 278 |
+
for category in list(self.product_index['by_category'].keys())[:3]:
|
| 279 |
+
if any(word in category for word in query_words):
|
| 280 |
+
category_products = self.product_index['by_category'][category]
|
| 281 |
+
if category_products:
|
| 282 |
+
suggestions.append(category_products[0][2])
|
| 283 |
+
|
| 284 |
+
return list(set(suggestions))[:5] # Return unique suggestions
|
prompts.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
Trek Bisiklet Chatbot Sistem Promptları
|
| 4 |
+
Kategorize edilmiş ve optimize edilmiş prompt konfigürasyonu
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from datetime import datetime
|
| 8 |
+
|
| 9 |
+
SYSTEM_PROMPTS = [
|
| 10 |
+
# 1. TEMEL KİMLİK VE ROL
|
| 11 |
+
{
|
| 12 |
+
"role": "system",
|
| 13 |
+
"category": "identity",
|
| 14 |
+
"content": "Sen Trek bisiklet uzmanı AI asistanısın. Trek ve Electra bisikletler konusunda uzmanısın. Stokta bulunan ürünlerin fiyat bilgilerini verebilirsin.\n\nCEVAP TARZI:\n• Kısa ve öz cevaplar ver (maksimum 3-4 cümle)\n• Gereksiz açıklama yapma, sadece sorulan bilgiyi ver\n• Stok sorularında direkt mağaza ve beden bilgisi ver\n• Fiyat sorularında sadece fiyatı söyle\n• WhatsApp için uygun, mobil okunabilir formatla yaz\n\nTÜRKÇE KONUŞMA KURALLARI:\n• Müşteriye HER ZAMAN 'siz' diye hitap et, ASLA 'sen' kullanma\n• Doğru: 'isterseniz', 'bakabilirsiniz', 'size', 'sizin için'\n• Yanlış: 'istersen', 'bakabilirsin', 'sana', 'senin için'\n• Cümleleri ASLA soru ile bitirme ('ayırtayım mı?', 'ister misiniz?', 'bakar mısınız?' gibi sorular SORMA)\n• Bilgiyi verdikten sonra sus, müşteriyi sıkıştırma\n• Müşteri karar vermek isterse kendisi sorar"
|
| 15 |
+
},
|
| 16 |
+
|
| 17 |
+
# 2. MAĞAZA BİLGİLERİ VE İLETİŞİM
|
| 18 |
+
{
|
| 19 |
+
"role": "system",
|
| 20 |
+
"category": "stores",
|
| 21 |
+
"content": "MAĞAZA BİLGİLERİ:\n• İstanbul - Caddebostan: Prof. Dr. Hulusi Behçet 18, Kadıköy (Göztepe Parkı karşısı) | Tel: 0543 934 0438\n• İstanbul - Ortaköy: Dereboyu Cad No:84, Beşiktaş (Toyota Plaza yanında).Öenmli: Ortaköy şubemiz 1 Aralık 2025 itibari ile kalıcı olarak kapanmıştır. BU şebeyi, şube bilgilerinde vermeyeceksin | Tel: 0543 933 9884\n• İstanbul - Sarıyer: Mareşal Fevzi Çakmak Cad. No 54, Kemer-Bahçeköy | Tel: 0542 137 1080\n• İzmir - Alsancak: Sezer Doğan Sok. The Kar Suits 14A, Konak | Tel: 0543 936 2335\n\nYETKİLİ TREK BAYİLERİ:\n• İzmir Bisiklet (Karşıyaka): Tuna Mah. Cemal Gürsel Cad. No:90/A, Karşıyaka | Tel: 0232 369 42 42 | WhatsApp: 0535 374 24 69\n• Bike Stop (Antalya): Öğretmenevleri Mah. 20. Cd. 98/B Ata Apt, Konyaaltı | Tel: 0536 975 16 98\nBu bayiler kendi mağazalarımız dışında Trek bisiklet alabileceğiniz yetkili satış noktalarıdır.\n\nÇalışma saatleri: 10:00-19:00 (Pazar kapalı)\nSarıyer mağazası elektrikli bisiklet odaklıdır.\nBike Fit: Caddebostan'da, 3500 TL, 60-90 dk, randevu gerekli."
|
| 22 |
+
},
|
| 23 |
+
|
| 24 |
+
# 3. ÜRÜN KATEGORİLERİ VE MODELLER
|
| 25 |
+
{
|
| 26 |
+
"role": "system",
|
| 27 |
+
"category": "products",
|
| 28 |
+
"content": "ÜRÜN KATEGORİLERİ:\n• Dağ Bisikletleri: Marlin, Roscoe, Procaliber, Supercaliber, Fuel EX\n• Yol Bisikletleri: Émonda, Domane, Speed Concept\n• Şehir Bisikletleri: FX, DS (Dual Sport - 2026 itibariyle uretilmiyor, sadece DS+ elektrikli devam ediyor), Verve\n• Gravel: Checkpoint\n• Elektrikli: Powerfly, Rail, Fuel EXe, Domane+, FX+, DS+, Verve+, Townie+\n• Boylar: XXS, XS, S, M, ML, L, XL"
|
| 29 |
+
},
|
| 30 |
+
|
| 31 |
+
# 4. MARKA KURALLAR VE KISITLAMALAR
|
| 32 |
+
{
|
| 33 |
+
"role": "system",
|
| 34 |
+
"category": "brand_rules",
|
| 35 |
+
"content": "MARKA KURALLARI:\n• Sadece Trek, Electra, Bontrager, Saris, Bryton, Trieye, Gobik markalarını önerebilirsin\n• Diğer markalar (Specialized, Orbea, BMC, Carraro, Scott, Giant) hakkında objektif yorum yapamayacağını belirt\n• Sadece www.trekbisiklet.com.tr ile başlayan linkleri paylaş\n• Trek kadrolar ömür boyu garantilidir"
|
| 36 |
+
},
|
| 37 |
+
|
| 38 |
+
# 5. ÖZEL ÜRÜNLER VE MARKALAR
|
| 39 |
+
{
|
| 40 |
+
"role": "system",
|
| 41 |
+
"category": "special_brands",
|
| 42 |
+
"content": "ÖZEL MARKALAR:\n• Bontrager: Bisiklet aksesuar ve yedek parçaları\n• Bryton Rider S800: En üst seviye GPS yol bilgisayarı\n• Trieye: Norveç menşeili geri görüş aynalı güvenlik gözlükleri (Photochromatic ve renkli lens seçenekleri)"
|
| 43 |
+
},
|
| 44 |
+
|
| 45 |
+
# 6. MADONE GEN 8 ÖZEL BİLGİLER
|
| 46 |
+
{
|
| 47 |
+
"role": "system",
|
| 48 |
+
"category": "madone_gen8",
|
| 49 |
+
"content": "MADONE GEN 8 (27 Haziran 2024):\nÉmonda kadar hafif, Madone kadar hızlı. Gen 7 ve Émonda'nın yerine geçen hibrit model.\n• 900 Serisi OCLV Karbon, 320g daha hafif\n• Émonda'dan 77 saniye/saat daha hızlı\n• %80 daha uyumlu IsoFlow teknolojisi\n• SL: 500 Serisi karbon, ekonomik, mekanik vites uyumlu\n• SLR: Premium model, RSL Aero gidon"
|
| 50 |
+
},
|
| 51 |
+
|
| 52 |
+
# 7. FİRMA GEÇMİŞİ VE SPONSORLUKLAR
|
| 53 |
+
{
|
| 54 |
+
"role": "system",
|
| 55 |
+
"category": "company_info",
|
| 56 |
+
"content": "FİRMA BİLGİLERİ:\n• 2000'den beri Alatin Bisiklet tarafından Türkiye distribütörlüğü\n• Sponsorluklar: Fatih Topçu (ASLA DURMA), TREK RMK DYNAMIS takımı\n• Bisiklet takası: https://www.bikeexchangehub.com/\n• Web sitesi: https://www.alatin.com.tr\n• Canlı destek: Sitedeki YEŞİL düğme"
|
| 57 |
+
},
|
| 58 |
+
|
| 59 |
+
# 8. ONLİNE SATIŞ VE SİPARİŞ BİLGİLERİ
|
| 60 |
+
{
|
| 61 |
+
"role": "system",
|
| 62 |
+
"category": "online_sales",
|
| 63 |
+
"content": "ONLİNE SATIŞ BİLGİLERİ:\n• www.trekbisiklet.com.tr üzerinden ONLINE SATIŞ YAPILMAKTADIR\n• Müşteriler web sitesi üzerinden tüm ürünleri görüntüleyebilir, sepete ekleyebilir ve online ödeme yaparak sipariş verebilir\n• Online sipariş süreci: 'Ürünü sepete ekle → Bilgilerini gir → Ödeme yöntemini seç → Siparişi tamamla'\n• Kargo: Aras Kargo ile gönderim, 24 saat içinde hazırlık, 3-5 iş günü teslimat\n• SMS ve email ile sipariş takibi yapılır\n• Belirli tutar üzeri siparişlerde kargo ücretsizdir\n• Müşteriler isterlarsa mağazadan da teslim alabilir\n• Stok ve fiyat bilgileri web sitesinde güncel olarak görüntülenebilir\n• Özel durumlar, detaylı ürün danışmanlığı veya test sürüşü için mağazalarla iletişime geçilmesi önerilir"
|
| 64 |
+
},
|
| 65 |
+
|
| 66 |
+
# 9. BIKE FINDER SİSTEMİ
|
| 67 |
+
{
|
| 68 |
+
"role": "system",
|
| 69 |
+
"category": "bike_finder",
|
| 70 |
+
"content": "BIKE FINDER ASİSTANI:\nKullanıcılara uygun bisiklet seçimi için adım adım sorular sor:\n1. Bisiklet kategorisi (Yol/Dağ/Şehir/Gravel/Elektrikli)\n2. Kullanım amacı (Günlük/Spor/Tur/Yarış/Offroad)\n3. Öncelikler (Performans/Konfor/Dayanıklılık)\n4. Zemin koşulları (Asfalt/Parkur/Orman/Offroad)\n5. Fiziksel ölçüler (Boy/İç bacak)\n6. Bütçe ve özel tercihler\n7. Öneri ve satış yönlendirme"
|
| 71 |
+
},
|
| 72 |
+
|
| 73 |
+
# 10. AĞIRLIK VERİLERİ VE TEKNİK BİLGİLER
|
| 74 |
+
{
|
| 75 |
+
"role": "system",
|
| 76 |
+
"category": "technical_specs",
|
| 77 |
+
"content": "AĞIRLIK VERİLERİ (kg):\nMadone Gen 8: SL5(8.70), SL6(8.16), SL7(7.88), SLR7(7.30), SLR9(7.00)\nÉmonda: SLR9(6.72), SLR7(7.10), SL7(7.95), SL5(9.15)\nDomane: SLR7(7.25), SL6(8.90), AL2(10.55)\nFuel: EXe 9.8(18.1), EX 8(13.77)\nMarlin: 4(14.60), 5(13.90), 7(13.77), 8(13.2)\nKullanıcı sadece ana model adı verirse, tüm varyantları listele."
|
| 78 |
+
},
|
| 79 |
+
|
| 80 |
+
# 11. OPERASYONEL KURALLAR
|
| 81 |
+
{
|
| 82 |
+
"role": "system",
|
| 83 |
+
"category": "operations",
|
| 84 |
+
"content": "OPERASYONEL KURALLAR:\n• Cevap verirken bilgilerin doğruluğunu kontrol et\n• Diğer markalarla ilgili sorularda kibarca Trek'in avantajlarını anlat\n• Stok ve fiyat bilgileri için web sitesini referans göster\n• Müşteri özel danışmanlık veya test sürüşü isterse en yakın mağazaya yönlendir"
|
| 85 |
+
},
|
| 86 |
+
|
| 87 |
+
# 12. BÜTÇE REHBERİ
|
| 88 |
+
{
|
| 89 |
+
"role": "system",
|
| 90 |
+
"category": "budget_guide",
|
| 91 |
+
"content": "BÜTÇE ARALIĞI:\n• Başlangıç: 46.000-100.000 TL (Dual Sport, FX serisi)\n• Orta seviye: 200.000-400.000 TL (Domane SL, Émonda SL)\n• Profesyonel: 500.000-750.000 TL (Madone SLR, Speed Concept)\n• Elektrikli: 200.000-600.000 TL (Powerfly, Rail serisi)"
|
| 92 |
+
},
|
| 93 |
+
|
| 94 |
+
# 13. BOYUT REHBERİ
|
| 95 |
+
{
|
| 96 |
+
"role": "system",
|
| 97 |
+
"category": "sizing_guide",
|
| 98 |
+
"content": "BOYUT REHBERİ:\n• XXS: 150-155cm | XS: 155-165cm | S: 165-172cm\n• M: 172-178cm | ML: 178-185cm | L: 185-192cm | XL: 192cm+\nDoğru boyut için hem boy hem iç bacak ölçümü önemlidir."
|
| 99 |
+
},
|
| 100 |
+
|
| 101 |
+
# 14. KAMPANYA VE SERVİS HİZMETLERİ
|
| 102 |
+
{
|
| 103 |
+
"role": "system",
|
| 104 |
+
"category": "services",
|
| 105 |
+
"content": "KAMPANYALAR: %15-20 mevsimsel indirimler, eski model kampanyaları\nSERVİS HİZMETLERİ: Ücretsiz ilk bakım (3 ay), ömür boyu garanti, yedek parça temini\nKARGO: Belirli tutar üzeri ücretsiz, mağazadan teslim alma seçeneği. Bisiklet bakımlarını tüm markalara yapıyoruz. Servis paketleri: Bronz paket 1500 TL = Fren, Vites ayarları ve genel vida tork kontrolleri. Silver paket 2250 TL= Bronz paket ile beraber jant akort ayarları ve yıkama. Gold paket 3000 TL= Silver paket ile beraber ön, arka, orta ve furs yatak bakımları"
|
| 106 |
+
},
|
| 107 |
+
|
| 108 |
+
# 15. ODEME VE BANKA BILGILERI
|
| 109 |
+
{
|
| 110 |
+
"role": "system",
|
| 111 |
+
"category": "payment_info",
|
| 112 |
+
"content": "ODEME VE TAKSIT SECENEKLERI:\nCalistigimiz bankalar ve kart programlari:\n- Axess (Akbank)\n- Bonus (Garanti BBVA)\n- Maximum (Is Bankasi)\n- World (Yapi Kredi)\n- CardFinans (QNB Finansbank)\n- Paraf (Halkbank)\n- Combo (Halkbank)\n\nTum bu kartlarla taksitli alisveris yapilabilir. Taksit secenekleri kampanyalara gore degisebilir."
|
| 113 |
+
},
|
| 114 |
+
|
| 115 |
+
# 16. EKİM KAMPANYASI - VADE FARKSIZ 8 TAKSİT
|
| 116 |
+
{
|
| 117 |
+
"role": "system",
|
| 118 |
+
"category": "ekim_kampanya",
|
| 119 |
+
"start_date": "2025-10-01",
|
| 120 |
+
"end_date": "2025-10-31",
|
| 121 |
+
"content": "EKİM KAMPANYASI (1-31 Ekim 2025):\nTrek Bicycle Turkey, Ekim ayı boyunca seçili bisiklet modellerinde vade farksız 8 taksit imkanı sunmaktadır.\n\nKAPSAMDAKİ MODELLER:\n✅ MADONE Serisi (Tüm varyantlar): SLR 9, SLR 7, SL 7, SL 6, SL 5\n✅ MARLIN Serisi (Tüm varyantlar): Marlin 9, 8, 7, 6, 5, 4\n✅ FX Serisi (Tüm varyantlar): FX Sport 6, Sport 5, FX 3 Disc, FX 2 Disc, FX 1 Disc\n✅ TÜM ELEKTRİKLİ BİSİKLETLER: Rail, Powerfly, Fuel EXe, Domane+, FX+, DS+, Verve+, Townie+, Allant+\n\nKAMPANYA KOŞULLARI:\n• Minimum alışveriş: 10.000 TL\n• Tüm kredi kartları geçerli (Visa, Mastercard, Amex)\n• Vade farkı YOK - Peşin fiyatına 8 taksit\n• Online ve mağaza alışverişlerinde geçerli\n• Ücretsiz kargo dahil\n• Ücretsiz ilk servis dahil\n• İndirimli ürünlerle birleştirilebilir\n• Eski bisiklet takası ile birleştirilebilir\n\nKAPSAM DIŞI MODELLER:\n❌ Émonda, Domane (elektriksiz), Fuel EX (elektriksiz), Top Fuel, Slash, Remedy, Supercaliber, X-Caliber, Roscoe, Procaliber, Dual Sport (elektriksiz), District, Checkpoint, Verve (elektriksiz)\n\nÖNEMLİ NOTLAR:\n• Émonda isteyen müşteriye → Madone önerin (kampanyada)\n• Fuel EX isteyen müşteriye → Marlin 9 veya Rail (e-MTB) önerin\n• X-Caliber isteyen müşteriye → Marlin serisi önerin\n• Checkpoint isteyen müşteriye → FX serisi önerin\n\nÖRNEK CEVAPLAR:\n'Evet! Ekim ayında Madone, Marlin, FX serileri ve tüm elektrikli bisikletlerde vade farksız 8 taksit kampanyamız var!'\n'Madone SL 6 kampanyaya dahil! Vade farksız 8 taksit ile alabilirsiniz.'\n'Maalesef Fuel EX kampanyada değil, ancak Marlin 9 veya elektrikli Rail modellerimiz kampanyada!'\n'Tüm elektrikli bisikletlerimiz kampanya kapsamında - Rail, Powerfly, Domane+ ve daha fazlası!'"
|
| 122 |
+
},
|
| 123 |
+
|
| 124 |
+
# 17. HATA ÖNLEMLERİ
|
| 125 |
+
{
|
| 126 |
+
"role": "system",
|
| 127 |
+
"category": "error_prevention",
|
| 128 |
+
"content": "KRİTİK KURALLAR:\n• Telefon numaralarını ASLA uydurma! Sadece bu numaraları kullan:\n - Caddebostan: 0543 934 0438\n - Sarıyer: 0542 137 1080\n - Alsancak: 0543 936 2335\n - İzmir Bisiklet: 0232 369 42 42\n - Bike Stop Antalya: 0536 975 16 98\n• Bilmediğin bilgiyi uydurma, 'bu bilgiye sahip değilim' de\n• Emoji kullanma\n• 'Özetle:' gibi başlıklar kullanma, doğal konuş"
|
| 129 |
+
},
|
| 130 |
+
|
| 131 |
+
# 18. URETIMI DURDURULAN MODELLER
|
| 132 |
+
{
|
| 133 |
+
"role": "system",
|
| 134 |
+
"category": "discontinued_models",
|
| 135 |
+
"content": "URETIMI DURDURULAN MODELLER:\n\nDS (Dual Sport) ELEKTRIKSIZ MODELLER:\n2026 itibariyle DS elektriksiz modellerin uretimi durdurulmustur. Yeni DS elektriksiz modeller stoklara GIRMEYECEK.\n\nMusteriye oneriler:\n- DS elektriksiz arayan musteriye FX serisi onerin (DS yerine gecen model)\n- Alternatif olarak Verve serisi de onerilebilir\n- Mevcut DS stoklari bitene kadar satis devam edecek, ancak yenisi gelmeyecek\n\nOnemli: Stokta DS elektriksiz model varsa satilabilir, ancak musteri yeni model bekliyorsa FX serisi onerilmeli."
|
| 136 |
+
},
|
| 137 |
+
]
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def get_system_prompts():
|
| 141 |
+
"""Sistem promptlarını döndür"""
|
| 142 |
+
return SYSTEM_PROMPTS
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def get_prompts_by_category(category):
|
| 146 |
+
"""Belirli kategorideki promptları döndür"""
|
| 147 |
+
return [prompt for prompt in SYSTEM_PROMPTS if prompt.get("category") == category]
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
def get_prompt_content_only():
|
| 151 |
+
"""Sadece content kısmını döndür (eski format uyumluluğu için)"""
|
| 152 |
+
return [{"role": prompt["role"], "content": prompt["content"]} for prompt in SYSTEM_PROMPTS]
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def get_active_prompts():
|
| 156 |
+
"""Tarihe göre aktif promptları döndür - süresi geçmiş kampanyaları filtrele"""
|
| 157 |
+
today = datetime.now().date()
|
| 158 |
+
active_prompts = []
|
| 159 |
+
|
| 160 |
+
for prompt in SYSTEM_PROMPTS:
|
| 161 |
+
# Tarih alanları varsa kontrol et
|
| 162 |
+
start_date = prompt.get("start_date")
|
| 163 |
+
end_date = prompt.get("end_date")
|
| 164 |
+
|
| 165 |
+
if start_date and end_date:
|
| 166 |
+
# Tarih stringlerini date objesine çevir
|
| 167 |
+
start = datetime.strptime(start_date, "%Y-%m-%d").date()
|
| 168 |
+
end = datetime.strptime(end_date, "%Y-%m-%d").date()
|
| 169 |
+
|
| 170 |
+
# Sadece aktif tarih aralığındaysa ekle
|
| 171 |
+
if start <= today <= end:
|
| 172 |
+
active_prompts.append({"role": prompt["role"], "content": prompt["content"]})
|
| 173 |
+
else:
|
| 174 |
+
# Tarih alanı yoksa her zaman ekle
|
| 175 |
+
active_prompts.append({"role": prompt["role"], "content": prompt["content"]})
|
| 176 |
+
|
| 177 |
+
return active_prompts
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def get_active_prompt_content_only():
|
| 181 |
+
"""Aktif promptların sadece content kısmını döndür (API çağrıları için)"""
|
| 182 |
+
return get_active_prompts()
|
reminder_scheduler.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
|
| 4 |
+
"""
|
| 5 |
+
Otomatik Hatırlatma Zamanlayıcısı
|
| 6 |
+
Belirli aralıklarla takipleri kontrol eder ve hatırlatma gönderir
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import os
|
| 10 |
+
import time
|
| 11 |
+
import logging
|
| 12 |
+
from datetime import datetime
|
| 13 |
+
from typing import List
|
| 14 |
+
|
| 15 |
+
# Import our modules
|
| 16 |
+
from follow_up_system import FollowUpManager, format_reminder_message
|
| 17 |
+
from store_notification import send_store_notification
|
| 18 |
+
|
| 19 |
+
logging.basicConfig(
|
| 20 |
+
level=logging.INFO,
|
| 21 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
| 22 |
+
)
|
| 23 |
+
logger = logging.getLogger(__name__)
|
| 24 |
+
|
| 25 |
+
class ReminderScheduler:
|
| 26 |
+
"""Hatırlatma zamanlayıcısı"""
|
| 27 |
+
|
| 28 |
+
def __init__(self, check_interval_minutes: int = 10):
|
| 29 |
+
"""
|
| 30 |
+
Args:
|
| 31 |
+
check_interval_minutes: Kaç dakikada bir kontrol yapılacak
|
| 32 |
+
"""
|
| 33 |
+
self.check_interval = check_interval_minutes * 60 # Saniyeye çevir
|
| 34 |
+
self.manager = FollowUpManager()
|
| 35 |
+
self.running = False
|
| 36 |
+
|
| 37 |
+
def send_reminder(self, follow_up) -> bool:
|
| 38 |
+
"""Hatırlatma mesajı gönder"""
|
| 39 |
+
|
| 40 |
+
try:
|
| 41 |
+
# Hatırlatma mesajını hazırla
|
| 42 |
+
reminder_message = format_reminder_message(follow_up)
|
| 43 |
+
|
| 44 |
+
# Mehmet Bey'e bildirim gönder
|
| 45 |
+
result = send_store_notification(
|
| 46 |
+
customer_phone=follow_up.customer_phone,
|
| 47 |
+
customer_name=follow_up.customer_name,
|
| 48 |
+
product_name=follow_up.product_name,
|
| 49 |
+
action="reminder", # Yeni tip: hatırlatma
|
| 50 |
+
store_name=follow_up.store_name,
|
| 51 |
+
additional_info=f"⏰ TAKİP HATIRLATMASI: {follow_up.original_message}"
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
if result:
|
| 55 |
+
logger.info(f"✅ Hatırlatma gönderildi: {follow_up.id}")
|
| 56 |
+
return True
|
| 57 |
+
else:
|
| 58 |
+
logger.error(f"❌ Hatırlatma gönderilemedi: {follow_up.id}")
|
| 59 |
+
return False
|
| 60 |
+
|
| 61 |
+
except Exception as e:
|
| 62 |
+
logger.error(f"Hatırlatma gönderme hatası: {e}")
|
| 63 |
+
return False
|
| 64 |
+
|
| 65 |
+
def check_and_send_reminders(self):
|
| 66 |
+
"""Bekleyen hatırlatmaları kontrol et ve gönder"""
|
| 67 |
+
|
| 68 |
+
logger.info("🔍 Hatırlatmalar kontrol ediliyor...")
|
| 69 |
+
|
| 70 |
+
# Bekleyen hatırlatmaları al
|
| 71 |
+
pending = self.manager.get_pending_reminders()
|
| 72 |
+
|
| 73 |
+
if not pending:
|
| 74 |
+
logger.info("📭 Bekleyen hatırlatma yok")
|
| 75 |
+
return
|
| 76 |
+
|
| 77 |
+
logger.info(f"📬 {len(pending)} hatırlatma bulundu")
|
| 78 |
+
|
| 79 |
+
# Her birini gönder
|
| 80 |
+
for follow_up in pending:
|
| 81 |
+
logger.info(f"📤 Hatırlatma gönderiliyor: {follow_up.customer_phone}")
|
| 82 |
+
|
| 83 |
+
if self.send_reminder(follow_up):
|
| 84 |
+
# Başarılıysa durumu güncelle
|
| 85 |
+
self.manager.mark_as_reminded(follow_up.id)
|
| 86 |
+
|
| 87 |
+
# Eğer çok kez hatırlatma yapıldıysa tamamlanmış say
|
| 88 |
+
if follow_up.reminded_count >= 2:
|
| 89 |
+
self.manager.mark_as_completed(follow_up.id)
|
| 90 |
+
logger.info(f"✅ Takip tamamlandı (2 hatırlatma yapıldı): {follow_up.id}")
|
| 91 |
+
|
| 92 |
+
# Mesajlar arası bekle (rate limit)
|
| 93 |
+
time.sleep(2)
|
| 94 |
+
|
| 95 |
+
def send_daily_summary(self):
|
| 96 |
+
"""Günlük özet gönder"""
|
| 97 |
+
|
| 98 |
+
now = datetime.now()
|
| 99 |
+
todays = self.manager.get_todays_follow_ups()
|
| 100 |
+
|
| 101 |
+
if not todays:
|
| 102 |
+
return
|
| 103 |
+
|
| 104 |
+
# Özet mesajı hazırla
|
| 105 |
+
summary = f"""
|
| 106 |
+
📊 **GÜNLÜK TAKİP ÖZETİ**
|
| 107 |
+
📅 {now.strftime('%d.%m.%Y')}
|
| 108 |
+
|
| 109 |
+
Bugün takip edilmesi gereken {len(todays)} müşteri var:
|
| 110 |
+
"""
|
| 111 |
+
|
| 112 |
+
for i, follow_up in enumerate(todays, 1):
|
| 113 |
+
status_emoji = "✅" if follow_up.status == "completed" else "⏳"
|
| 114 |
+
summary += f"""
|
| 115 |
+
{i}. {status_emoji} {follow_up.customer_phone.replace('whatsapp:', '')}
|
| 116 |
+
Ürün: {follow_up.product_name}
|
| 117 |
+
Durum: {follow_up.status}
|
| 118 |
+
"""
|
| 119 |
+
|
| 120 |
+
summary += "\n📞 Takipleri tamamlamayı unutmayın!"
|
| 121 |
+
|
| 122 |
+
# Özet bildirimi gönder
|
| 123 |
+
send_store_notification(
|
| 124 |
+
customer_phone="whatsapp:+905439362335", # Direkt Mehmet Bey
|
| 125 |
+
customer_name="Sistem",
|
| 126 |
+
product_name="Günlük Özet",
|
| 127 |
+
action="info",
|
| 128 |
+
additional_info=summary
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
logger.info("📊 Günlük özet gönderildi")
|
| 132 |
+
|
| 133 |
+
def run(self):
|
| 134 |
+
"""Zamanlayıcıyı başlat"""
|
| 135 |
+
|
| 136 |
+
self.running = True
|
| 137 |
+
logger.info(f"⏰ Hatırlatma zamanlayıcısı başlatıldı")
|
| 138 |
+
logger.info(f" Kontrol aralığı: {self.check_interval_minutes} dakika")
|
| 139 |
+
|
| 140 |
+
last_summary_date = None
|
| 141 |
+
|
| 142 |
+
while self.running:
|
| 143 |
+
try:
|
| 144 |
+
# Hatırlatmaları kontrol et
|
| 145 |
+
self.check_and_send_reminders()
|
| 146 |
+
|
| 147 |
+
# Günlük özet zamanı mı? (Saat 09:00 ve 18:00)
|
| 148 |
+
now = datetime.now()
|
| 149 |
+
if now.hour in [9, 18] and now.date() != last_summary_date:
|
| 150 |
+
self.send_daily_summary()
|
| 151 |
+
last_summary_date = now.date()
|
| 152 |
+
|
| 153 |
+
# Bekle
|
| 154 |
+
logger.info(f"⏳ {self.check_interval_minutes} dakika bekleniyor...")
|
| 155 |
+
time.sleep(self.check_interval)
|
| 156 |
+
|
| 157 |
+
except KeyboardInterrupt:
|
| 158 |
+
logger.info("⏹️ Zamanlayıcı durduruldu")
|
| 159 |
+
self.running = False
|
| 160 |
+
break
|
| 161 |
+
except Exception as e:
|
| 162 |
+
logger.error(f"Zamanlayıcı hatası: {e}")
|
| 163 |
+
time.sleep(60) # Hata durumunda 1 dakika bekle
|
| 164 |
+
|
| 165 |
+
def stop(self):
|
| 166 |
+
"""Zamanlayıcıyı durdur"""
|
| 167 |
+
self.running = False
|
| 168 |
+
logger.info("⏹️ Zamanlayıcı durduruluyor...")
|
| 169 |
+
|
| 170 |
+
def run_scheduler():
|
| 171 |
+
"""Ana fonksiyon"""
|
| 172 |
+
|
| 173 |
+
# Twilio credentials'ları ayarla
|
| 174 |
+
os.environ['TWILIO_ACCOUNT_SID'] = 'AC643d14a443b9fbcbc17a2828508870a6'
|
| 175 |
+
os.environ['TWILIO_AUTH_TOKEN'] = '9203f0328bfd737dc39bf9be5aa97ca9'
|
| 176 |
+
|
| 177 |
+
print("""
|
| 178 |
+
╔════════════════════════════════════════╗
|
| 179 |
+
║ TREK TAKİP HATIRLATMA SİSTEMİ ║
|
| 180 |
+
║ Otomatik Müşteri Takibi ║
|
| 181 |
+
╚════════════════════════════════════════╝
|
| 182 |
+
|
| 183 |
+
Ayarlar:
|
| 184 |
+
- Kontrol aralığı: 10 dakika
|
| 185 |
+
- Günlük özet: 09:00 ve 18:00
|
| 186 |
+
- Mehmet Bey: +905439362335
|
| 187 |
+
|
| 188 |
+
Başlatılıyor...
|
| 189 |
+
""")
|
| 190 |
+
|
| 191 |
+
scheduler = ReminderScheduler(check_interval_minutes=10)
|
| 192 |
+
|
| 193 |
+
try:
|
| 194 |
+
scheduler.run()
|
| 195 |
+
except KeyboardInterrupt:
|
| 196 |
+
print("\n👋 Sistem kapatılıyor...")
|
| 197 |
+
scheduler.stop()
|
| 198 |
+
|
| 199 |
+
if __name__ == "__main__":
|
| 200 |
+
# Test modda çalıştır (1 dakika aralıklarla)
|
| 201 |
+
if os.getenv("TEST_MODE"):
|
| 202 |
+
print("🧪 TEST MODU - 1 dakika aralıklarla kontrol")
|
| 203 |
+
scheduler = ReminderScheduler(check_interval_minutes=1)
|
| 204 |
+
else:
|
| 205 |
+
scheduler = ReminderScheduler(check_interval_minutes=10)
|
| 206 |
+
|
| 207 |
+
run_scheduler()
|
requirements.txt
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
schedule
|
| 2 |
+
gradio>=3.41.2
|
| 3 |
+
requests
|
| 4 |
+
pandas
|
| 5 |
+
python-docx
|
| 6 |
+
huggingface_hub
|
| 7 |
+
schedule
|
| 8 |
+
openpyxl
|
| 9 |
+
spaces
|
| 10 |
+
google-auth
|
| 11 |
+
google-auth-oauthlib
|
| 12 |
+
google-auth-httplib2
|
| 13 |
+
google-api-python-client
|
| 14 |
+
fastapi
|
| 15 |
+
uvicorn
|
| 16 |
+
twilio
|
| 17 |
+
python-multipart==0.0.6
|
| 18 |
+
pydantic>=2.10.0
|
search_marlin_products.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
import requests
|
| 3 |
+
import xml.etree.ElementTree as ET
|
| 4 |
+
|
| 5 |
+
def search_marlin_products():
|
| 6 |
+
"""B2B API'den MARLIN ürünlerini listele"""
|
| 7 |
+
try:
|
| 8 |
+
warehouse_url = 'https://video.trek-turkey.com/bizimhesap-warehouse-xml.php'
|
| 9 |
+
response = requests.get(warehouse_url, verify=False, timeout=15)
|
| 10 |
+
|
| 11 |
+
if response.status_code != 200:
|
| 12 |
+
return None
|
| 13 |
+
|
| 14 |
+
root = ET.fromstring(response.content)
|
| 15 |
+
|
| 16 |
+
# Turkish character normalization function
|
| 17 |
+
turkish_map = {'ı': 'i', 'ğ': 'g', 'ü': 'u', 'ş': 's', 'ö': 'o', 'ç': 'c', 'İ': 'i', 'I': 'i'}
|
| 18 |
+
|
| 19 |
+
def normalize_turkish(text):
|
| 20 |
+
import unicodedata
|
| 21 |
+
text = unicodedata.normalize('NFD', text)
|
| 22 |
+
text = ''.join(char for char in text if unicodedata.category(char) != 'Mn')
|
| 23 |
+
for tr_char, en_char in turkish_map.items():
|
| 24 |
+
text = text.replace(tr_char, en_char)
|
| 25 |
+
return text
|
| 26 |
+
|
| 27 |
+
marlin_products = []
|
| 28 |
+
|
| 29 |
+
# MARLIN içeren tüm ürünleri bul
|
| 30 |
+
for product in root.findall('Product'):
|
| 31 |
+
product_name_elem = product.find('ProductName')
|
| 32 |
+
if product_name_elem is not None and product_name_elem.text:
|
| 33 |
+
xml_product_name = product_name_elem.text.strip()
|
| 34 |
+
normalized_xml = normalize_turkish(xml_product_name.lower())
|
| 35 |
+
|
| 36 |
+
if 'marlin' in normalized_xml:
|
| 37 |
+
# Stok durumunu kontrol et
|
| 38 |
+
has_stock = False
|
| 39 |
+
stock_info = []
|
| 40 |
+
|
| 41 |
+
warehouses = product.find('Warehouses')
|
| 42 |
+
if warehouses is not None:
|
| 43 |
+
for warehouse in warehouses.findall('Warehouse'):
|
| 44 |
+
name_elem = warehouse.find('Name')
|
| 45 |
+
stock_elem = warehouse.find('Stock')
|
| 46 |
+
|
| 47 |
+
if name_elem is not None and stock_elem is not None:
|
| 48 |
+
warehouse_name = name_elem.text if name_elem.text else "Bilinmeyen"
|
| 49 |
+
try:
|
| 50 |
+
stock_count = int(stock_elem.text) if stock_elem.text else 0
|
| 51 |
+
if stock_count > 0:
|
| 52 |
+
stock_info.append(f"{warehouse_name}: {stock_count}")
|
| 53 |
+
has_stock = True
|
| 54 |
+
except (ValueError, TypeError):
|
| 55 |
+
pass
|
| 56 |
+
|
| 57 |
+
marlin_products.append({
|
| 58 |
+
'name': xml_product_name,
|
| 59 |
+
'has_stock': has_stock,
|
| 60 |
+
'stock_info': stock_info
|
| 61 |
+
})
|
| 62 |
+
|
| 63 |
+
return marlin_products
|
| 64 |
+
|
| 65 |
+
except Exception as e:
|
| 66 |
+
print(f"Arama hatası: {e}")
|
| 67 |
+
return None
|
| 68 |
+
|
| 69 |
+
def search_turuncu_products():
|
| 70 |
+
"""B2B API'den TURUNCU içeren ürünleri listele"""
|
| 71 |
+
try:
|
| 72 |
+
warehouse_url = 'https://video.trek-turkey.com/bizimhesap-warehouse-xml.php'
|
| 73 |
+
response = requests.get(warehouse_url, verify=False, timeout=15)
|
| 74 |
+
|
| 75 |
+
if response.status_code != 200:
|
| 76 |
+
return None
|
| 77 |
+
|
| 78 |
+
root = ET.fromstring(response.content)
|
| 79 |
+
|
| 80 |
+
# Turkish character normalization function
|
| 81 |
+
turkish_map = {'ı': 'i', 'ğ': 'g', 'ü': 'u', 'ş': 's', 'ö': 'o', 'ç': 'c', 'İ': 'i', 'I': 'i'}
|
| 82 |
+
|
| 83 |
+
def normalize_turkish(text):
|
| 84 |
+
import unicodedata
|
| 85 |
+
text = unicodedata.normalize('NFD', text)
|
| 86 |
+
text = ''.join(char for char in text if unicodedata.category(char) != 'Mn')
|
| 87 |
+
for tr_char, en_char in turkish_map.items():
|
| 88 |
+
text = text.replace(tr_char, en_char)
|
| 89 |
+
return text
|
| 90 |
+
|
| 91 |
+
turuncu_products = []
|
| 92 |
+
|
| 93 |
+
# TURUNCU içeren tüm ürünleri bul
|
| 94 |
+
for product in root.findall('Product'):
|
| 95 |
+
product_name_elem = product.find('ProductName')
|
| 96 |
+
if product_name_elem is not None and product_name_elem.text:
|
| 97 |
+
xml_product_name = product_name_elem.text.strip()
|
| 98 |
+
normalized_xml = normalize_turkish(xml_product_name.lower())
|
| 99 |
+
|
| 100 |
+
if 'turuncu' in normalized_xml:
|
| 101 |
+
# Stok durumunu kontrol et
|
| 102 |
+
has_stock = False
|
| 103 |
+
stock_info = []
|
| 104 |
+
|
| 105 |
+
warehouses = product.find('Warehouses')
|
| 106 |
+
if warehouses is not None:
|
| 107 |
+
for warehouse in warehouses.findall('Warehouse'):
|
| 108 |
+
name_elem = warehouse.find('Name')
|
| 109 |
+
stock_elem = warehouse.find('Stock')
|
| 110 |
+
|
| 111 |
+
if name_elem is not None and stock_elem is not None:
|
| 112 |
+
warehouse_name = name_elem.text if name_elem.text else "Bilinmeyen"
|
| 113 |
+
try:
|
| 114 |
+
stock_count = int(stock_elem.text) if stock_elem.text else 0
|
| 115 |
+
if stock_count > 0:
|
| 116 |
+
stock_info.append(f"{warehouse_name}: {stock_count}")
|
| 117 |
+
has_stock = True
|
| 118 |
+
except (ValueError, TypeError):
|
| 119 |
+
pass
|
| 120 |
+
|
| 121 |
+
turuncu_products.append({
|
| 122 |
+
'name': xml_product_name,
|
| 123 |
+
'has_stock': has_stock,
|
| 124 |
+
'stock_info': stock_info
|
| 125 |
+
})
|
| 126 |
+
|
| 127 |
+
return turuncu_products
|
| 128 |
+
|
| 129 |
+
except Exception as e:
|
| 130 |
+
print(f"Arama hatası: {e}")
|
| 131 |
+
return None
|
| 132 |
+
|
| 133 |
+
if __name__ == "__main__":
|
| 134 |
+
print("=== MARLIN ÜRÜNLERİ ===")
|
| 135 |
+
marlin_products = search_marlin_products()
|
| 136 |
+
if marlin_products:
|
| 137 |
+
print(f"Toplam {len(marlin_products)} MARLIN ürünü bulundu:")
|
| 138 |
+
for i, product in enumerate(marlin_products, 1):
|
| 139 |
+
print(f"{i:2}. {product['name']}")
|
| 140 |
+
if product['has_stock']:
|
| 141 |
+
print(f" STOKTA: {', '.join(product['stock_info'])}")
|
| 142 |
+
else:
|
| 143 |
+
print(f" STOKTA DEĞİL")
|
| 144 |
+
print()
|
| 145 |
+
|
| 146 |
+
print("\n" + "="*50)
|
| 147 |
+
print("=== TURUNCU ÜRÜNLERİ ===")
|
| 148 |
+
turuncu_products = search_turuncu_products()
|
| 149 |
+
if turuncu_products:
|
| 150 |
+
print(f"Toplam {len(turuncu_products)} TURUNCU ürünü bulundu:")
|
| 151 |
+
for i, product in enumerate(turuncu_products, 1):
|
| 152 |
+
print(f"{i:2}. {product['name']}")
|
| 153 |
+
if product['has_stock']:
|
| 154 |
+
print(f" STOKTA: {', '.join(product['stock_info'])}")
|
| 155 |
+
else:
|
| 156 |
+
print(f" STOKTA DEĞİL")
|
| 157 |
+
print()
|
smart_warehouse.py
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Smart warehouse stock finder using GPT-5's intelligence"""
|
| 2 |
+
|
| 3 |
+
import requests
|
| 4 |
+
import re
|
| 5 |
+
import os
|
| 6 |
+
import json
|
| 7 |
+
|
| 8 |
+
def get_warehouse_stock_smart(user_message, previous_result=None):
|
| 9 |
+
"""Let GPT-5 intelligently find products or filter by warehouse"""
|
| 10 |
+
|
| 11 |
+
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
| 12 |
+
|
| 13 |
+
# Check if user is asking about specific warehouse
|
| 14 |
+
warehouse_keywords = {
|
| 15 |
+
'caddebostan': 'Caddebostan',
|
| 16 |
+
'ortaköy': 'Ortaköy',
|
| 17 |
+
'ortakoy': 'Ortaköy',
|
| 18 |
+
'alsancak': 'Alsancak',
|
| 19 |
+
'izmir': 'Alsancak',
|
| 20 |
+
'bahçeköy': 'Bahçeköy',
|
| 21 |
+
'bahcekoy': 'Bahçeköy'
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
user_lower = user_message.lower()
|
| 25 |
+
asked_warehouse = None
|
| 26 |
+
for keyword, warehouse in warehouse_keywords.items():
|
| 27 |
+
if keyword in user_lower:
|
| 28 |
+
asked_warehouse = warehouse
|
| 29 |
+
break
|
| 30 |
+
|
| 31 |
+
# Get XML data with retry
|
| 32 |
+
xml_text = None
|
| 33 |
+
for attempt in range(3): # Try 3 times
|
| 34 |
+
try:
|
| 35 |
+
url = 'https://video.trek-turkey.com/bizimhesap-warehouse-xml-b2b-api-v2.php'
|
| 36 |
+
timeout_val = 10 + (attempt * 5) # Increase timeout on each retry: 10, 15, 20
|
| 37 |
+
response = requests.get(url, verify=False, timeout=timeout_val)
|
| 38 |
+
xml_text = response.text
|
| 39 |
+
print(f"DEBUG - XML fetched: {len(xml_text)} characters (attempt {attempt+1})")
|
| 40 |
+
break
|
| 41 |
+
except requests.exceptions.Timeout:
|
| 42 |
+
print(f"XML fetch timeout (attempt {attempt+1}/3, timeout={timeout_val}s)")
|
| 43 |
+
if attempt == 2:
|
| 44 |
+
print("All attempts failed - timeout")
|
| 45 |
+
return None
|
| 46 |
+
except Exception as e:
|
| 47 |
+
print(f"XML fetch error: {e}")
|
| 48 |
+
return None
|
| 49 |
+
|
| 50 |
+
# Extract just product blocks to reduce token usage
|
| 51 |
+
product_pattern = r'<Product>(.*?)</Product>'
|
| 52 |
+
all_products = re.findall(product_pattern, xml_text, re.DOTALL)
|
| 53 |
+
|
| 54 |
+
# Create a simplified product list for GPT
|
| 55 |
+
products_summary = []
|
| 56 |
+
for i, product_block in enumerate(all_products):
|
| 57 |
+
name_match = re.search(r'<ProductName><!\[CDATA\[(.*?)\]\]></ProductName>', product_block)
|
| 58 |
+
variant_match = re.search(r'<ProductVariant><!\[CDATA\[(.*?)\]\]></ProductVariant>', product_block)
|
| 59 |
+
|
| 60 |
+
if name_match:
|
| 61 |
+
# Check warehouse stock for this product
|
| 62 |
+
warehouses_with_stock = []
|
| 63 |
+
warehouse_regex = r'<Warehouse>.*?<Name><!\[CDATA\[(.*?)\]\]></Name>.*?<Stock>(.*?)</Stock>.*?</Warehouse>'
|
| 64 |
+
warehouses = re.findall(warehouse_regex, product_block, re.DOTALL)
|
| 65 |
+
|
| 66 |
+
for wh_name, wh_stock in warehouses:
|
| 67 |
+
try:
|
| 68 |
+
if int(wh_stock.strip()) > 0:
|
| 69 |
+
warehouses_with_stock.append(wh_name)
|
| 70 |
+
except:
|
| 71 |
+
pass
|
| 72 |
+
|
| 73 |
+
product_info = {
|
| 74 |
+
"index": i,
|
| 75 |
+
"name": name_match.group(1),
|
| 76 |
+
"variant": variant_match.group(1) if variant_match else "",
|
| 77 |
+
"warehouses": warehouses_with_stock
|
| 78 |
+
}
|
| 79 |
+
products_summary.append(product_info)
|
| 80 |
+
|
| 81 |
+
# If user is asking about specific warehouse, include that in prompt
|
| 82 |
+
warehouse_filter = ""
|
| 83 |
+
if asked_warehouse:
|
| 84 |
+
warehouse_filter = f"\nIMPORTANT: User is asking specifically about {asked_warehouse} warehouse. Only return products available in that warehouse."
|
| 85 |
+
|
| 86 |
+
# Let GPT-5 find ALL matching products
|
| 87 |
+
smart_prompt = f"""User is asking: "{user_message}"
|
| 88 |
+
|
| 89 |
+
Find ALL products that match this query from the list below.
|
| 90 |
+
If user asks about specific size (S, M, L, XL, XXL, SMALL, MEDIUM, LARGE, X-LARGE), return only that size.
|
| 91 |
+
If user asks generally (without size), return ALL variants of the product.
|
| 92 |
+
{warehouse_filter}
|
| 93 |
+
|
| 94 |
+
IMPORTANT BRAND AND PRODUCT TYPE RULES:
|
| 95 |
+
- GOBIK: Spanish textile brand we import. When user asks about "gobik", return ALL products with "GOBIK" in the name.
|
| 96 |
+
- Product names contain type information: FORMA (jersey/cycling shirt), TAYT (tights), İÇLİK (base layer), YAĞMURLUK (raincoat), etc.
|
| 97 |
+
- Understand Turkish/English terms:
|
| 98 |
+
* "erkek forma" / "men's jersey" -> Find products with FORMA in name
|
| 99 |
+
* "tayt" / "tights" -> Find products with TAYT in name
|
| 100 |
+
* "içlik" / "base layer" -> Find products with İÇLİK in name
|
| 101 |
+
* "yağmurluk" / "raincoat" -> Find products with YAĞMURLUK in name
|
| 102 |
+
- Gender: UNISEX means for both men and women. If no gender specified, it's typically men's.
|
| 103 |
+
- Be smart: "erkek forma" should find all FORMA products (excluding women-specific if any)
|
| 104 |
+
|
| 105 |
+
Products list (with warehouse availability):
|
| 106 |
+
{json.dumps(products_summary, ensure_ascii=False, indent=2)}
|
| 107 |
+
|
| 108 |
+
Return index numbers of ALL matching products as comma-separated list (e.g., "5,8,12,15").
|
| 109 |
+
If no products found, return: -1
|
| 110 |
+
|
| 111 |
+
Examples:
|
| 112 |
+
- "madone sl 6 var mı" -> Return ALL Madone SL 6 variants
|
| 113 |
+
- "erkek forma" -> Return all products with FORMA in name
|
| 114 |
+
- "gobik tayt" -> Return all GOBIK products with TAYT in name
|
| 115 |
+
- "içlik var mı" -> Return all products with İÇLİK in name
|
| 116 |
+
- "gobik erkek forma" -> Return all GOBIK products with FORMA in name
|
| 117 |
+
- "yağmurluk medium" -> Return all YAĞMURLUK products in MEDIUM size"""
|
| 118 |
+
|
| 119 |
+
headers = {
|
| 120 |
+
"Content-Type": "application/json",
|
| 121 |
+
"Authorization": f"Bearer {OPENAI_API_KEY}"
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
payload = {
|
| 125 |
+
"model": "gpt-5-chat-latest",
|
| 126 |
+
"messages": [
|
| 127 |
+
{"role": "system", "content": "You are a product matcher. Find ALL matching products. Return only index numbers."},
|
| 128 |
+
{"role": "user", "content": smart_prompt}
|
| 129 |
+
],
|
| 130 |
+
"temperature": 0,
|
| 131 |
+
"max_tokens": 100
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
try:
|
| 135 |
+
response = requests.post(
|
| 136 |
+
"https://api.openai.com/v1/chat/completions",
|
| 137 |
+
headers=headers,
|
| 138 |
+
json=payload,
|
| 139 |
+
timeout=10
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
if response.status_code == 200:
|
| 143 |
+
result = response.json()
|
| 144 |
+
indices_str = result['choices'][0]['message']['content'].strip()
|
| 145 |
+
|
| 146 |
+
if indices_str == "-1":
|
| 147 |
+
return ["Ürün bulunamadı"]
|
| 148 |
+
|
| 149 |
+
try:
|
| 150 |
+
# Parse multiple indices
|
| 151 |
+
indices = [int(idx.strip()) for idx in indices_str.split(',')]
|
| 152 |
+
|
| 153 |
+
# Collect all matching products
|
| 154 |
+
all_variants = []
|
| 155 |
+
warehouse_stock = {}
|
| 156 |
+
|
| 157 |
+
for idx in indices:
|
| 158 |
+
if 0 <= idx < len(all_products):
|
| 159 |
+
product_block = all_products[idx]
|
| 160 |
+
|
| 161 |
+
# Get product name and variant
|
| 162 |
+
name_match = re.search(r'<ProductName><!\[CDATA\[(.*?)\]\]></ProductName>', product_block)
|
| 163 |
+
variant_match = re.search(r'<ProductVariant><!\[CDATA\[(.*?)\]\]></ProductVariant>', product_block)
|
| 164 |
+
|
| 165 |
+
if name_match:
|
| 166 |
+
product_name = name_match.group(1)
|
| 167 |
+
variant = variant_match.group(1) if variant_match else ""
|
| 168 |
+
|
| 169 |
+
# Track this variant
|
| 170 |
+
variant_info = {
|
| 171 |
+
'name': product_name,
|
| 172 |
+
'variant': variant,
|
| 173 |
+
'warehouses': []
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
# Get warehouse stock
|
| 177 |
+
warehouse_regex = r'<Warehouse>.*?<Name><!\[CDATA\[(.*?)\]\]></Name>.*?<Stock>(.*?)</Stock>.*?</Warehouse>'
|
| 178 |
+
warehouses = re.findall(warehouse_regex, product_block, re.DOTALL)
|
| 179 |
+
|
| 180 |
+
for wh_name, wh_stock in warehouses:
|
| 181 |
+
try:
|
| 182 |
+
stock = int(wh_stock.strip())
|
| 183 |
+
if stock > 0:
|
| 184 |
+
display_name = format_warehouse_name(wh_name)
|
| 185 |
+
variant_info['warehouses'].append({
|
| 186 |
+
'name': display_name,
|
| 187 |
+
'stock': stock
|
| 188 |
+
})
|
| 189 |
+
|
| 190 |
+
# Track total stock per warehouse
|
| 191 |
+
if display_name not in warehouse_stock:
|
| 192 |
+
warehouse_stock[display_name] = 0
|
| 193 |
+
warehouse_stock[display_name] += stock
|
| 194 |
+
except:
|
| 195 |
+
pass
|
| 196 |
+
|
| 197 |
+
if variant_info['warehouses']: # Only add if has stock
|
| 198 |
+
all_variants.append(variant_info)
|
| 199 |
+
|
| 200 |
+
# Format result
|
| 201 |
+
result = []
|
| 202 |
+
|
| 203 |
+
if asked_warehouse:
|
| 204 |
+
# Filter for specific warehouse
|
| 205 |
+
warehouse_variants = []
|
| 206 |
+
for variant in all_variants:
|
| 207 |
+
for wh in variant['warehouses']:
|
| 208 |
+
if asked_warehouse in wh['name']:
|
| 209 |
+
warehouse_variants.append({
|
| 210 |
+
'name': variant['name'],
|
| 211 |
+
'variant': variant['variant'],
|
| 212 |
+
'stock': wh['stock']
|
| 213 |
+
})
|
| 214 |
+
|
| 215 |
+
if warehouse_variants:
|
| 216 |
+
result.append(f"{format_warehouse_name(asked_warehouse)} mağazasında mevcut:")
|
| 217 |
+
for v in warehouse_variants:
|
| 218 |
+
result.append(f"• {v['name']} ({v['variant']})")
|
| 219 |
+
else:
|
| 220 |
+
result.append(f"{format_warehouse_name(asked_warehouse)} mağazasında bu ürün mevcut değil")
|
| 221 |
+
else:
|
| 222 |
+
# Show all variants and warehouses
|
| 223 |
+
if all_variants:
|
| 224 |
+
result.append(f"Bulunan {len(all_variants)} varyant:")
|
| 225 |
+
|
| 226 |
+
# Show ALL variants (not just first 5)
|
| 227 |
+
for variant in all_variants:
|
| 228 |
+
variant_text = f" ({variant['variant']})" if variant['variant'] else ""
|
| 229 |
+
result.append(f"• {variant['name']}{variant_text}")
|
| 230 |
+
|
| 231 |
+
result.append("")
|
| 232 |
+
result.append("Mağaza stok durumu:")
|
| 233 |
+
for warehouse, total_stock in sorted(warehouse_stock.items()):
|
| 234 |
+
result.append(f"• {warehouse}: Mevcut")
|
| 235 |
+
else:
|
| 236 |
+
result.append("Hiçbir mağazada stok yok")
|
| 237 |
+
|
| 238 |
+
return result
|
| 239 |
+
|
| 240 |
+
except (ValueError, IndexError) as e:
|
| 241 |
+
print(f"DEBUG - Error parsing indices: {e}")
|
| 242 |
+
return None
|
| 243 |
+
else:
|
| 244 |
+
print(f"GPT API error: {response.status_code}")
|
| 245 |
+
return None
|
| 246 |
+
|
| 247 |
+
except Exception as e:
|
| 248 |
+
print(f"Error calling GPT: {e}")
|
| 249 |
+
return None
|
| 250 |
+
|
| 251 |
+
def format_warehouse_name(wh_name):
|
| 252 |
+
"""Format warehouse name nicely"""
|
| 253 |
+
if "CADDEBOSTAN" in wh_name:
|
| 254 |
+
return "Caddebostan mağazası"
|
| 255 |
+
elif "ORTAKÖY" in wh_name:
|
| 256 |
+
return "Ortaköy mağazası"
|
| 257 |
+
elif "ALSANCAK" in wh_name:
|
| 258 |
+
return "İzmir Alsancak mağazası"
|
| 259 |
+
elif "BAHCEKOY" in wh_name or "BAHÇEKÖY" in wh_name:
|
| 260 |
+
return "Bahçeköy mağazası"
|
| 261 |
+
else:
|
| 262 |
+
return wh_name.replace("MAGAZA DEPO", "").strip()
|
smart_warehouse_complete.py
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Smart warehouse stock finder with price and link information"""
|
| 2 |
+
|
| 3 |
+
import requests
|
| 4 |
+
import re
|
| 5 |
+
import os
|
| 6 |
+
import json
|
| 7 |
+
import xml.etree.ElementTree as ET
|
| 8 |
+
|
| 9 |
+
def get_product_price_and_link(product_name, variant=None):
|
| 10 |
+
"""Get price and link from Trek website XML"""
|
| 11 |
+
try:
|
| 12 |
+
# Get Trek product catalog
|
| 13 |
+
url = 'https://www.trekbisiklet.com.tr/output/8582384479'
|
| 14 |
+
response = requests.get(url, verify=False, timeout=10)
|
| 15 |
+
|
| 16 |
+
if response.status_code != 200:
|
| 17 |
+
return None, None
|
| 18 |
+
|
| 19 |
+
root = ET.fromstring(response.content)
|
| 20 |
+
|
| 21 |
+
# Turkish character normalization FIRST (before lower)
|
| 22 |
+
tr_map = {'İ': 'i', 'I': 'i', 'ı': 'i', 'Ğ': 'g', 'ğ': 'g', 'Ü': 'u', 'ü': 'u', 'Ş': 's', 'ş': 's', 'Ö': 'o', 'ö': 'o', 'Ç': 'c', 'ç': 'c'}
|
| 23 |
+
|
| 24 |
+
# Apply normalization to original
|
| 25 |
+
search_name_norm = product_name
|
| 26 |
+
search_variant_norm = variant if variant else ""
|
| 27 |
+
for tr, en in tr_map.items():
|
| 28 |
+
search_name_norm = search_name_norm.replace(tr, en)
|
| 29 |
+
search_variant_norm = search_variant_norm.replace(tr, en)
|
| 30 |
+
|
| 31 |
+
# Now lowercase
|
| 32 |
+
search_name = search_name_norm.lower()
|
| 33 |
+
search_variant = search_variant_norm.lower()
|
| 34 |
+
|
| 35 |
+
best_match = None
|
| 36 |
+
best_score = 0
|
| 37 |
+
|
| 38 |
+
for item in root.findall('item'):
|
| 39 |
+
# Get product name
|
| 40 |
+
rootlabel_elem = item.find('rootlabel')
|
| 41 |
+
if rootlabel_elem is None or not rootlabel_elem.text:
|
| 42 |
+
continue
|
| 43 |
+
|
| 44 |
+
item_name = rootlabel_elem.text.lower()
|
| 45 |
+
for tr, en in tr_map.items():
|
| 46 |
+
item_name = item_name.replace(tr, en)
|
| 47 |
+
|
| 48 |
+
# Calculate match score
|
| 49 |
+
score = 0
|
| 50 |
+
name_parts = search_name.split()
|
| 51 |
+
for part in name_parts:
|
| 52 |
+
if part in item_name:
|
| 53 |
+
score += 1
|
| 54 |
+
|
| 55 |
+
# Check variant if specified
|
| 56 |
+
if variant and search_variant in item_name:
|
| 57 |
+
score += 2 # Variant match is important
|
| 58 |
+
|
| 59 |
+
if score > best_score:
|
| 60 |
+
best_score = score
|
| 61 |
+
best_match = item
|
| 62 |
+
|
| 63 |
+
if best_match and best_score > 0:
|
| 64 |
+
# Extract price
|
| 65 |
+
price_elem = best_match.find('priceTaxWithCur')
|
| 66 |
+
price = price_elem.text if price_elem is not None and price_elem.text else None
|
| 67 |
+
|
| 68 |
+
# Round price
|
| 69 |
+
if price:
|
| 70 |
+
try:
|
| 71 |
+
price_float = float(price)
|
| 72 |
+
if price_float > 200000:
|
| 73 |
+
rounded = round(price_float / 5000) * 5000
|
| 74 |
+
price = f"{int(rounded):,}".replace(',', '.') + " TL"
|
| 75 |
+
elif price_float > 30000:
|
| 76 |
+
rounded = round(price_float / 1000) * 1000
|
| 77 |
+
price = f"{int(rounded):,}".replace(',', '.') + " TL"
|
| 78 |
+
elif price_float > 10000:
|
| 79 |
+
rounded = round(price_float / 100) * 100
|
| 80 |
+
price = f"{int(rounded):,}".replace(',', '.') + " TL"
|
| 81 |
+
else:
|
| 82 |
+
rounded = round(price_float / 10) * 10
|
| 83 |
+
price = f"{int(rounded):,}".replace(',', '.') + " TL"
|
| 84 |
+
except:
|
| 85 |
+
price = f"{price} TL"
|
| 86 |
+
|
| 87 |
+
# Extract link (field name is productLink, not productUrl!)
|
| 88 |
+
link_elem = best_match.find('productLink')
|
| 89 |
+
link = link_elem.text if link_elem is not None and link_elem.text else None
|
| 90 |
+
|
| 91 |
+
return price, link
|
| 92 |
+
|
| 93 |
+
return None, None
|
| 94 |
+
|
| 95 |
+
except Exception as e:
|
| 96 |
+
print(f"Error getting price/link: {e}")
|
| 97 |
+
return None, None
|
| 98 |
+
|
| 99 |
+
def get_warehouse_stock_smart_complete(user_message):
|
| 100 |
+
"""Complete smart warehouse search for WhatsApp - BF algorithm"""
|
| 101 |
+
|
| 102 |
+
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
| 103 |
+
|
| 104 |
+
# Check if user is asking about specific warehouse
|
| 105 |
+
warehouse_keywords = {
|
| 106 |
+
'caddebostan': 'Caddebostan',
|
| 107 |
+
'ortaköy': 'Ortaköy',
|
| 108 |
+
'ortakoy': 'Ortaköy',
|
| 109 |
+
'alsancak': 'Alsancak',
|
| 110 |
+
'izmir': 'Alsancak',
|
| 111 |
+
'bahçeköy': 'Bahçeköy',
|
| 112 |
+
'bahcekoy': 'Bahçeköy'
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
user_lower = user_message.lower()
|
| 116 |
+
asked_warehouse = None
|
| 117 |
+
for keyword, warehouse in warehouse_keywords.items():
|
| 118 |
+
if keyword in user_lower:
|
| 119 |
+
asked_warehouse = warehouse
|
| 120 |
+
break
|
| 121 |
+
|
| 122 |
+
# Get XML data with retry
|
| 123 |
+
xml_text = None
|
| 124 |
+
for attempt in range(3):
|
| 125 |
+
try:
|
| 126 |
+
url = 'https://video.trek-turkey.com/bizimhesap-warehouse-xml-b2b-api-v2.php'
|
| 127 |
+
timeout_val = 10 + (attempt * 5)
|
| 128 |
+
response = requests.get(url, verify=False, timeout=timeout_val)
|
| 129 |
+
xml_text = response.text
|
| 130 |
+
print(f"DEBUG - XML fetched: {len(xml_text)} characters (attempt {attempt+1})")
|
| 131 |
+
break
|
| 132 |
+
except requests.exceptions.Timeout:
|
| 133 |
+
print(f"XML fetch timeout (attempt {attempt+1}/3, timeout={timeout_val}s)")
|
| 134 |
+
if attempt == 2:
|
| 135 |
+
print("All attempts failed - timeout")
|
| 136 |
+
return None
|
| 137 |
+
except Exception as e:
|
| 138 |
+
print(f"XML fetch error: {e}")
|
| 139 |
+
return None
|
| 140 |
+
|
| 141 |
+
# Extract product blocks
|
| 142 |
+
product_pattern = r'<Product>(.*?)</Product>'
|
| 143 |
+
all_products = re.findall(product_pattern, xml_text, re.DOTALL)
|
| 144 |
+
|
| 145 |
+
# Create simplified product list for GPT
|
| 146 |
+
products_summary = []
|
| 147 |
+
for i, product_block in enumerate(all_products):
|
| 148 |
+
name_match = re.search(r'<ProductName><!\[CDATA\[(.*?)\]\]></ProductName>', product_block)
|
| 149 |
+
variant_match = re.search(r'<ProductVariant><!\[CDATA\[(.*?)\]\]></ProductVariant>', product_block)
|
| 150 |
+
|
| 151 |
+
if name_match:
|
| 152 |
+
warehouses_with_stock = []
|
| 153 |
+
warehouse_regex = r'<Warehouse>.*?<Name><!\[CDATA\[(.*?)\]\]></Name>.*?<Stock>(.*?)</Stock>.*?</Warehouse>'
|
| 154 |
+
warehouses = re.findall(warehouse_regex, product_block, re.DOTALL)
|
| 155 |
+
|
| 156 |
+
for wh_name, wh_stock in warehouses:
|
| 157 |
+
try:
|
| 158 |
+
if int(wh_stock.strip()) > 0:
|
| 159 |
+
warehouses_with_stock.append(wh_name)
|
| 160 |
+
except:
|
| 161 |
+
pass
|
| 162 |
+
|
| 163 |
+
product_info = {
|
| 164 |
+
"index": i,
|
| 165 |
+
"name": name_match.group(1),
|
| 166 |
+
"variant": variant_match.group(1) if variant_match else "",
|
| 167 |
+
"warehouses": warehouses_with_stock
|
| 168 |
+
}
|
| 169 |
+
products_summary.append(product_info)
|
| 170 |
+
|
| 171 |
+
# Prepare warehouse filter if needed
|
| 172 |
+
warehouse_filter = ""
|
| 173 |
+
if asked_warehouse:
|
| 174 |
+
warehouse_filter = f"\nIMPORTANT: User is asking specifically about {asked_warehouse} warehouse. Only return products available in that warehouse."
|
| 175 |
+
|
| 176 |
+
# Log what we're searching for
|
| 177 |
+
print(f"DEBUG - Searching for: '{user_message}'")
|
| 178 |
+
print(f"DEBUG - Total products to search: {len(products_summary)}")
|
| 179 |
+
|
| 180 |
+
# Check if the target product exists
|
| 181 |
+
search_term = user_message.upper()
|
| 182 |
+
matching_products = []
|
| 183 |
+
for p in products_summary:
|
| 184 |
+
if search_term in p['name'].upper():
|
| 185 |
+
matching_products.append(p)
|
| 186 |
+
|
| 187 |
+
if matching_products:
|
| 188 |
+
print(f"DEBUG - Found {len(matching_products)} products containing '{user_message}':")
|
| 189 |
+
for p in matching_products[:3]:
|
| 190 |
+
print(f" - Index {p['index']}: {p['name']} ({p['variant']})")
|
| 191 |
+
|
| 192 |
+
# GPT-5 prompt with enhanced instructions
|
| 193 |
+
smart_prompt = f"""User is asking: "{user_message}"
|
| 194 |
+
|
| 195 |
+
Find ALL products that match this query from the list below.
|
| 196 |
+
If user asks about specific size (S, M, L, XL, XXL, SMALL, MEDIUM, LARGE, X-LARGE), return only that size.
|
| 197 |
+
If user asks generally (without size), return ALL variants of the product.
|
| 198 |
+
{warehouse_filter}
|
| 199 |
+
|
| 200 |
+
IMPORTANT BRAND AND PRODUCT TYPE RULES:
|
| 201 |
+
- GOBIK: Spanish textile brand we import. When user asks about "gobik", return ALL products with "GOBIK" in the name.
|
| 202 |
+
- Product names contain type information: FORMA (jersey/cycling shirt), TAYT (tights), İÇLİK (base layer), YAĞMURLUK (raincoat), etc.
|
| 203 |
+
- Understand Turkish/English terms:
|
| 204 |
+
* "erkek forma" / "men's jersey" -> Find products with FORMA in name
|
| 205 |
+
* "tayt" / "tights" -> Find products with TAYT in name
|
| 206 |
+
* "içlik" / "base layer" -> Find products with İÇLİK in name
|
| 207 |
+
* "yağmurluk" / "raincoat" -> Find products with YAĞMURLUK in name
|
| 208 |
+
- Gender: UNISEX means for both men and women. If no gender specified, it's typically men's.
|
| 209 |
+
|
| 210 |
+
Products list (with warehouse availability):
|
| 211 |
+
{json.dumps(products_summary, ensure_ascii=False, indent=2)}
|
| 212 |
+
|
| 213 |
+
Return ONLY index numbers of ALL matching products as comma-separated list (e.g., "5,8,12,15").
|
| 214 |
+
If no products found, return ONLY: -1
|
| 215 |
+
DO NOT return empty string or any explanation, ONLY numbers or -1
|
| 216 |
+
|
| 217 |
+
Examples of correct responses:
|
| 218 |
+
- "2,5,8,12,15,20" (multiple products found)
|
| 219 |
+
- "45" (single product found)
|
| 220 |
+
- "-1" (no products found)"""
|
| 221 |
+
|
| 222 |
+
headers = {
|
| 223 |
+
"Content-Type": "application/json",
|
| 224 |
+
"Authorization": f"Bearer {OPENAI_API_KEY}"
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
payload = {
|
| 228 |
+
"model": "gpt-5-chat-latest",
|
| 229 |
+
"messages": [
|
| 230 |
+
{"role": "system", "content": "You are a product matcher. Find ALL matching products. Return only index numbers."},
|
| 231 |
+
{"role": "user", "content": smart_prompt}
|
| 232 |
+
],
|
| 233 |
+
"temperature": 0,
|
| 234 |
+
"max_tokens": 100
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
try:
|
| 238 |
+
response = requests.post(
|
| 239 |
+
"https://api.openai.com/v1/chat/completions",
|
| 240 |
+
headers=headers,
|
| 241 |
+
json=payload,
|
| 242 |
+
timeout=10
|
| 243 |
+
)
|
| 244 |
+
|
| 245 |
+
if response.status_code == 200:
|
| 246 |
+
result = response.json()
|
| 247 |
+
indices_str = result['choices'][0]['message']['content'].strip()
|
| 248 |
+
|
| 249 |
+
print(f"DEBUG - GPT-5 response: '{indices_str}'")
|
| 250 |
+
print(f"DEBUG - Query was: '{user_message}'")
|
| 251 |
+
print(f"DEBUG - Products sent to GPT: {len(products_summary)} items")
|
| 252 |
+
|
| 253 |
+
# Handle empty response
|
| 254 |
+
if not indices_str or indices_str == "-1":
|
| 255 |
+
return ["Ürün bulunamadı"]
|
| 256 |
+
|
| 257 |
+
try:
|
| 258 |
+
# Filter out empty strings and parse indices
|
| 259 |
+
indices = []
|
| 260 |
+
for idx in indices_str.split(','):
|
| 261 |
+
idx = idx.strip()
|
| 262 |
+
if idx and idx.isdigit():
|
| 263 |
+
indices.append(int(idx))
|
| 264 |
+
|
| 265 |
+
# Collect all matching products with price/link
|
| 266 |
+
all_variants = []
|
| 267 |
+
warehouse_stock = {}
|
| 268 |
+
|
| 269 |
+
for idx in indices:
|
| 270 |
+
if 0 <= idx < len(all_products):
|
| 271 |
+
product_block = all_products[idx]
|
| 272 |
+
|
| 273 |
+
# Get product details
|
| 274 |
+
name_match = re.search(r'<ProductName><!\[CDATA\[(.*?)\]\]></ProductName>', product_block)
|
| 275 |
+
variant_match = re.search(r'<ProductVariant><!\[CDATA\[(.*?)\]\]></ProductVariant>', product_block)
|
| 276 |
+
|
| 277 |
+
if name_match:
|
| 278 |
+
product_name = name_match.group(1)
|
| 279 |
+
variant = variant_match.group(1) if variant_match else ""
|
| 280 |
+
|
| 281 |
+
# Get price and link from Trek website
|
| 282 |
+
price, link = get_product_price_and_link(product_name, variant)
|
| 283 |
+
|
| 284 |
+
variant_info = {
|
| 285 |
+
'name': product_name,
|
| 286 |
+
'variant': variant,
|
| 287 |
+
'price': price,
|
| 288 |
+
'link': link,
|
| 289 |
+
'warehouses': []
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
# Get warehouse stock
|
| 293 |
+
warehouse_regex = r'<Warehouse>.*?<Name><!\[CDATA\[(.*?)\]\]></Name>.*?<Stock>(.*?)</Stock>.*?</Warehouse>'
|
| 294 |
+
warehouses = re.findall(warehouse_regex, product_block, re.DOTALL)
|
| 295 |
+
|
| 296 |
+
for wh_name, wh_stock in warehouses:
|
| 297 |
+
try:
|
| 298 |
+
stock = int(wh_stock.strip())
|
| 299 |
+
if stock > 0:
|
| 300 |
+
display_name = format_warehouse_name(wh_name)
|
| 301 |
+
variant_info['warehouses'].append({
|
| 302 |
+
'name': display_name,
|
| 303 |
+
'stock': stock
|
| 304 |
+
})
|
| 305 |
+
|
| 306 |
+
if display_name not in warehouse_stock:
|
| 307 |
+
warehouse_stock[display_name] = 0
|
| 308 |
+
warehouse_stock[display_name] += stock
|
| 309 |
+
except:
|
| 310 |
+
pass
|
| 311 |
+
|
| 312 |
+
if variant_info['warehouses']:
|
| 313 |
+
all_variants.append(variant_info)
|
| 314 |
+
|
| 315 |
+
# Format result
|
| 316 |
+
result = []
|
| 317 |
+
|
| 318 |
+
if asked_warehouse:
|
| 319 |
+
# Filter for specific warehouse
|
| 320 |
+
warehouse_variants = []
|
| 321 |
+
for variant in all_variants:
|
| 322 |
+
for wh in variant['warehouses']:
|
| 323 |
+
if asked_warehouse in wh['name']:
|
| 324 |
+
warehouse_variants.append(variant)
|
| 325 |
+
break
|
| 326 |
+
|
| 327 |
+
if warehouse_variants:
|
| 328 |
+
result.append(f"{format_warehouse_name(asked_warehouse)} mağazasında mevcut:")
|
| 329 |
+
for v in warehouse_variants:
|
| 330 |
+
variant_text = f" ({v['variant']})" if v['variant'] else ""
|
| 331 |
+
result.append(f"• {v['name']}{variant_text}")
|
| 332 |
+
if v['price']:
|
| 333 |
+
result.append(f" Fiyat: {v['price']}")
|
| 334 |
+
if v['link']:
|
| 335 |
+
result.append(f" Link: {v['link']}")
|
| 336 |
+
else:
|
| 337 |
+
result.append(f"{format_warehouse_name(asked_warehouse)} mağazasında bu ürün mevcut değil")
|
| 338 |
+
else:
|
| 339 |
+
# Show all variants
|
| 340 |
+
if all_variants:
|
| 341 |
+
# Group by product name for cleaner display
|
| 342 |
+
product_groups = {}
|
| 343 |
+
for variant in all_variants:
|
| 344 |
+
if variant['name'] not in product_groups:
|
| 345 |
+
product_groups[variant['name']] = []
|
| 346 |
+
product_groups[variant['name']].append(variant)
|
| 347 |
+
|
| 348 |
+
result.append(f"Bulunan ürünler:")
|
| 349 |
+
|
| 350 |
+
for product_name, variants in product_groups.items():
|
| 351 |
+
result.append(f"\n{product_name}:")
|
| 352 |
+
|
| 353 |
+
# Show first variant's price and link (usually same for all variants)
|
| 354 |
+
if variants[0]['price']:
|
| 355 |
+
result.append(f"Fiyat: {variants[0]['price']}")
|
| 356 |
+
if variants[0]['link']:
|
| 357 |
+
result.append(f"Link: {variants[0]['link']}")
|
| 358 |
+
|
| 359 |
+
# Show variants and their availability
|
| 360 |
+
for v in variants:
|
| 361 |
+
if v['variant']:
|
| 362 |
+
warehouses_str = ", ".join([w['name'].replace(' mağazası', '') for w in v['warehouses']])
|
| 363 |
+
result.append(f"• {v['variant']}: {warehouses_str}")
|
| 364 |
+
|
| 365 |
+
else:
|
| 366 |
+
result.append("Hiçbir mağazada stok yok")
|
| 367 |
+
|
| 368 |
+
return result
|
| 369 |
+
|
| 370 |
+
except (ValueError, IndexError) as e:
|
| 371 |
+
print(f"DEBUG - Error parsing indices: {e}")
|
| 372 |
+
return None
|
| 373 |
+
else:
|
| 374 |
+
print(f"GPT API error: {response.status_code}")
|
| 375 |
+
print(f"Response: {response.text[:500]}")
|
| 376 |
+
return None
|
| 377 |
+
|
| 378 |
+
except Exception as e:
|
| 379 |
+
print(f"Error calling GPT: {e}")
|
| 380 |
+
return None
|
| 381 |
+
|
| 382 |
+
def format_warehouse_name(wh_name):
|
| 383 |
+
"""Format warehouse name nicely"""
|
| 384 |
+
if "CADDEBOSTAN" in wh_name:
|
| 385 |
+
return "Caddebostan mağazası"
|
| 386 |
+
elif "ORTAKÖY" in wh_name:
|
| 387 |
+
return "Ortaköy mağazası"
|
| 388 |
+
elif "ALSANCAK" in wh_name:
|
| 389 |
+
return "İzmir Alsancak mağazası"
|
| 390 |
+
elif "BAHCEKOY" in wh_name or "BAHÇEKÖY" in wh_name:
|
| 391 |
+
return "Bahçeköy mağazası"
|
| 392 |
+
else:
|
| 393 |
+
return wh_name.replace("MAGAZA DEPO", "").strip()
|
smart_warehouse_whatsapp.py
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Smart warehouse stock finder for WhatsApp - GPT-5 powered"""
|
| 2 |
+
|
| 3 |
+
import requests
|
| 4 |
+
import re
|
| 5 |
+
import os
|
| 6 |
+
import json
|
| 7 |
+
import xml.etree.ElementTree as ET
|
| 8 |
+
|
| 9 |
+
def get_product_price_and_link(product_name, variant=None):
|
| 10 |
+
"""Get price and link from Trek website XML"""
|
| 11 |
+
try:
|
| 12 |
+
url = 'https://www.trekbisiklet.com.tr/output/8582384479'
|
| 13 |
+
response = requests.get(url, verify=False, timeout=10)
|
| 14 |
+
|
| 15 |
+
if response.status_code != 200:
|
| 16 |
+
return None, None
|
| 17 |
+
|
| 18 |
+
root = ET.fromstring(response.content)
|
| 19 |
+
|
| 20 |
+
# Turkish character normalization FIRST (before lower)
|
| 21 |
+
tr_map = {'İ': 'i', 'I': 'i', 'ı': 'i', 'Ğ': 'g', 'ğ': 'g', 'Ü': 'u', 'ü': 'u', 'Ş': 's', 'ş': 's', 'Ö': 'o', 'ö': 'o', 'Ç': 'c', 'ç': 'c'}
|
| 22 |
+
|
| 23 |
+
# Apply normalization to original
|
| 24 |
+
search_name_norm = product_name
|
| 25 |
+
search_variant_norm = variant if variant else ""
|
| 26 |
+
for tr, en in tr_map.items():
|
| 27 |
+
search_name_norm = search_name_norm.replace(tr, en)
|
| 28 |
+
search_variant_norm = search_variant_norm.replace(tr, en)
|
| 29 |
+
|
| 30 |
+
# Now lowercase
|
| 31 |
+
search_name = search_name_norm.lower()
|
| 32 |
+
search_variant = search_variant_norm.lower()
|
| 33 |
+
|
| 34 |
+
best_match = None
|
| 35 |
+
best_score = 0
|
| 36 |
+
|
| 37 |
+
for item in root.findall('item'):
|
| 38 |
+
rootlabel_elem = item.find('rootlabel')
|
| 39 |
+
if rootlabel_elem is None or not rootlabel_elem.text:
|
| 40 |
+
continue
|
| 41 |
+
|
| 42 |
+
item_name = rootlabel_elem.text.lower()
|
| 43 |
+
for tr, en in tr_map.items():
|
| 44 |
+
item_name = item_name.replace(tr, en)
|
| 45 |
+
|
| 46 |
+
# Calculate match score
|
| 47 |
+
score = 0
|
| 48 |
+
name_parts = search_name.split()
|
| 49 |
+
for part in name_parts:
|
| 50 |
+
if part in item_name:
|
| 51 |
+
score += 1
|
| 52 |
+
|
| 53 |
+
if variant and search_variant in item_name:
|
| 54 |
+
score += 2
|
| 55 |
+
|
| 56 |
+
if score > best_score:
|
| 57 |
+
best_score = score
|
| 58 |
+
best_match = item
|
| 59 |
+
|
| 60 |
+
if best_match and best_score > 0:
|
| 61 |
+
# Extract price
|
| 62 |
+
price_elem = best_match.find('priceTaxWithCur')
|
| 63 |
+
price = price_elem.text if price_elem is not None and price_elem.text else None
|
| 64 |
+
|
| 65 |
+
# Round price for WhatsApp display
|
| 66 |
+
if price:
|
| 67 |
+
try:
|
| 68 |
+
price_float = float(price)
|
| 69 |
+
if price_float > 200000:
|
| 70 |
+
rounded = round(price_float / 5000) * 5000
|
| 71 |
+
price = f"{int(rounded):,}".replace(',', '.') + " TL"
|
| 72 |
+
elif price_float > 30000:
|
| 73 |
+
rounded = round(price_float / 1000) * 1000
|
| 74 |
+
price = f"{int(rounded):,}".replace(',', '.') + " TL"
|
| 75 |
+
elif price_float > 10000:
|
| 76 |
+
rounded = round(price_float / 100) * 100
|
| 77 |
+
price = f"{int(rounded):,}".replace(',', '.') + " TL"
|
| 78 |
+
else:
|
| 79 |
+
rounded = round(price_float / 10) * 10
|
| 80 |
+
price = f"{int(rounded):,}".replace(',', '.') + " TL"
|
| 81 |
+
except:
|
| 82 |
+
price = None
|
| 83 |
+
|
| 84 |
+
# Extract link
|
| 85 |
+
link_elem = best_match.find('productLink')
|
| 86 |
+
link = link_elem.text if link_elem is not None and link_elem.text else None
|
| 87 |
+
|
| 88 |
+
return price, link
|
| 89 |
+
|
| 90 |
+
return None, None
|
| 91 |
+
|
| 92 |
+
except Exception as e:
|
| 93 |
+
print(f"Error getting price/link: {e}")
|
| 94 |
+
return None, None
|
| 95 |
+
|
| 96 |
+
def get_warehouse_stock_gpt5(user_message):
|
| 97 |
+
"""GPT-5 powered smart warehouse stock search for WhatsApp"""
|
| 98 |
+
|
| 99 |
+
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
| 100 |
+
if not OPENAI_API_KEY:
|
| 101 |
+
return None
|
| 102 |
+
|
| 103 |
+
# Check for specific warehouse in query
|
| 104 |
+
warehouse_keywords = {
|
| 105 |
+
'caddebostan': 'Caddebostan',
|
| 106 |
+
'ortaköy': 'Ortaköy',
|
| 107 |
+
'ortakoy': 'Ortaköy',
|
| 108 |
+
'alsancak': 'Alsancak',
|
| 109 |
+
'izmir': 'Alsancak',
|
| 110 |
+
'bahçeköy': 'Bahçeköy',
|
| 111 |
+
'bahcekoy': 'Bahçeköy'
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
user_lower = user_message.lower()
|
| 115 |
+
asked_warehouse = None
|
| 116 |
+
for keyword, warehouse in warehouse_keywords.items():
|
| 117 |
+
if keyword in user_lower:
|
| 118 |
+
asked_warehouse = warehouse
|
| 119 |
+
break
|
| 120 |
+
|
| 121 |
+
# Get warehouse XML with retry
|
| 122 |
+
xml_text = None
|
| 123 |
+
for attempt in range(3):
|
| 124 |
+
try:
|
| 125 |
+
url = 'https://video.trek-turkey.com/bizimhesap-warehouse-xml-b2b-api-v2.php'
|
| 126 |
+
timeout_val = 10 + (attempt * 5)
|
| 127 |
+
response = requests.get(url, verify=False, timeout=timeout_val)
|
| 128 |
+
xml_text = response.text
|
| 129 |
+
break
|
| 130 |
+
except:
|
| 131 |
+
if attempt == 2:
|
| 132 |
+
return None
|
| 133 |
+
|
| 134 |
+
if not xml_text:
|
| 135 |
+
return None
|
| 136 |
+
|
| 137 |
+
# Extract product blocks
|
| 138 |
+
product_pattern = r'<Product>(.*?)</Product>'
|
| 139 |
+
all_products = re.findall(product_pattern, xml_text, re.DOTALL)
|
| 140 |
+
|
| 141 |
+
# Create product summary for GPT
|
| 142 |
+
products_summary = []
|
| 143 |
+
for i, product_block in enumerate(all_products):
|
| 144 |
+
name_match = re.search(r'<ProductName><!\[CDATA\[(.*?)\]\]></ProductName>', product_block)
|
| 145 |
+
variant_match = re.search(r'<ProductVariant><!\[CDATA\[(.*?)\]\]></ProductVariant>', product_block)
|
| 146 |
+
|
| 147 |
+
if name_match:
|
| 148 |
+
warehouses_with_stock = []
|
| 149 |
+
warehouse_regex = r'<Warehouse>.*?<Name><!\[CDATA\[(.*?)\]\]></Name>.*?<Stock>(.*?)</Stock>.*?</Warehouse>'
|
| 150 |
+
warehouses = re.findall(warehouse_regex, product_block, re.DOTALL)
|
| 151 |
+
|
| 152 |
+
for wh_name, wh_stock in warehouses:
|
| 153 |
+
try:
|
| 154 |
+
if int(wh_stock.strip()) > 0:
|
| 155 |
+
warehouses_with_stock.append(wh_name)
|
| 156 |
+
except:
|
| 157 |
+
pass
|
| 158 |
+
|
| 159 |
+
if warehouses_with_stock: # Only add if has stock
|
| 160 |
+
product_info = {
|
| 161 |
+
"index": i,
|
| 162 |
+
"name": name_match.group(1),
|
| 163 |
+
"variant": variant_match.group(1) if variant_match else "",
|
| 164 |
+
"warehouses": warehouses_with_stock
|
| 165 |
+
}
|
| 166 |
+
products_summary.append(product_info)
|
| 167 |
+
|
| 168 |
+
# Prepare GPT-5 prompt
|
| 169 |
+
warehouse_filter = ""
|
| 170 |
+
if asked_warehouse:
|
| 171 |
+
warehouse_filter = f"\nIMPORTANT: User is asking about {asked_warehouse} warehouse. Only return products available there."
|
| 172 |
+
|
| 173 |
+
smart_prompt = f"""User WhatsApp message: "{user_message}"
|
| 174 |
+
|
| 175 |
+
Find EXACT products matching this query. Be very precise with product names.
|
| 176 |
+
IMPORTANT: If user asks for "madone sl 6", find products with "MADONE SL 6" in the name.
|
| 177 |
+
DO NOT return similar products (like Marlin) if the exact product is not found.
|
| 178 |
+
|
| 179 |
+
Turkish/English terms:
|
| 180 |
+
- FORMA = jersey, TAYT = tights, İÇLİK = base layer, YAĞMURLUK = raincoat
|
| 181 |
+
- GOBIK = Spanish textile brand
|
| 182 |
+
- Sizes: S, M, L, XL, XXL, SMALL, MEDIUM, LARGE
|
| 183 |
+
{warehouse_filter}
|
| 184 |
+
|
| 185 |
+
Products with stock:
|
| 186 |
+
{json.dumps(products_summary, ensure_ascii=False)}
|
| 187 |
+
|
| 188 |
+
Return ONLY index numbers of EXACT matches as comma-separated list.
|
| 189 |
+
If NO exact match found, return: -1
|
| 190 |
+
Examples:
|
| 191 |
+
- "madone sl 6" -> Find only products with "MADONE SL 6" in name, NOT Marlin or other models
|
| 192 |
+
- "gobik forma" -> Find only products with "GOBIK" AND "FORMA" in name"""
|
| 193 |
+
|
| 194 |
+
headers = {
|
| 195 |
+
"Content-Type": "application/json",
|
| 196 |
+
"Authorization": f"Bearer {OPENAI_API_KEY}"
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
payload = {
|
| 200 |
+
"model": "gpt-5-chat-latest",
|
| 201 |
+
"messages": [
|
| 202 |
+
{"role": "system", "content": "You are a product matcher. Return only numbers."},
|
| 203 |
+
{"role": "user", "content": smart_prompt}
|
| 204 |
+
],
|
| 205 |
+
"temperature": 0,
|
| 206 |
+
"max_tokens": 100
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
try:
|
| 210 |
+
response = requests.post(
|
| 211 |
+
"https://api.openai.com/v1/chat/completions",
|
| 212 |
+
headers=headers,
|
| 213 |
+
json=payload,
|
| 214 |
+
timeout=10
|
| 215 |
+
)
|
| 216 |
+
|
| 217 |
+
if response.status_code != 200:
|
| 218 |
+
return None
|
| 219 |
+
|
| 220 |
+
result = response.json()
|
| 221 |
+
indices_str = result['choices'][0]['message']['content'].strip()
|
| 222 |
+
|
| 223 |
+
if not indices_str or indices_str == "-1":
|
| 224 |
+
return None
|
| 225 |
+
|
| 226 |
+
# Parse indices safely
|
| 227 |
+
indices = []
|
| 228 |
+
for idx in indices_str.split(','):
|
| 229 |
+
idx = idx.strip()
|
| 230 |
+
if idx and idx.isdigit():
|
| 231 |
+
indices.append(int(idx))
|
| 232 |
+
|
| 233 |
+
if not indices:
|
| 234 |
+
return None
|
| 235 |
+
|
| 236 |
+
# Collect matched products
|
| 237 |
+
matched_products = []
|
| 238 |
+
for idx in indices: # No limit, get all matches
|
| 239 |
+
if 0 <= idx < len(all_products):
|
| 240 |
+
product_block = all_products[idx]
|
| 241 |
+
|
| 242 |
+
name_match = re.search(r'<ProductName><!\[CDATA\[(.*?)\]\]></ProductName>', product_block)
|
| 243 |
+
variant_match = re.search(r'<ProductVariant><!\[CDATA\[(.*?)\]\]></ProductVariant>', product_block)
|
| 244 |
+
|
| 245 |
+
if name_match:
|
| 246 |
+
product_name = name_match.group(1)
|
| 247 |
+
variant = variant_match.group(1) if variant_match else ""
|
| 248 |
+
|
| 249 |
+
# Get price and link
|
| 250 |
+
price, link = get_product_price_and_link(product_name, variant)
|
| 251 |
+
|
| 252 |
+
# Get warehouse info
|
| 253 |
+
warehouses = []
|
| 254 |
+
warehouse_regex = r'<Warehouse>.*?<Name><!\[CDATA\[(.*?)\]\]></Name>.*?<Stock>(.*?)</Stock>.*?</Warehouse>'
|
| 255 |
+
wh_matches = re.findall(warehouse_regex, product_block, re.DOTALL)
|
| 256 |
+
|
| 257 |
+
for wh_name, wh_stock in wh_matches:
|
| 258 |
+
try:
|
| 259 |
+
if int(wh_stock.strip()) > 0:
|
| 260 |
+
if "CADDEBOSTAN" in wh_name:
|
| 261 |
+
warehouses.append("Caddebostan")
|
| 262 |
+
elif "ORTAKÖY" in wh_name:
|
| 263 |
+
warehouses.append("Ortaköy")
|
| 264 |
+
elif "ALSANCAK" in wh_name:
|
| 265 |
+
warehouses.append("Alsancak")
|
| 266 |
+
elif "BAHCEKOY" in wh_name or "BAHÇEKÖY" in wh_name:
|
| 267 |
+
warehouses.append("Bahçeköy")
|
| 268 |
+
except:
|
| 269 |
+
pass
|
| 270 |
+
|
| 271 |
+
if warehouses:
|
| 272 |
+
matched_products.append({
|
| 273 |
+
'name': product_name,
|
| 274 |
+
'variant': variant,
|
| 275 |
+
'price': price,
|
| 276 |
+
'link': link,
|
| 277 |
+
'warehouses': warehouses
|
| 278 |
+
})
|
| 279 |
+
|
| 280 |
+
return matched_products if matched_products else None
|
| 281 |
+
|
| 282 |
+
except Exception as e:
|
| 283 |
+
print(f"GPT-5 search error: {e}")
|
| 284 |
+
return None
|
smart_warehouse_with_price.py
ADDED
|
@@ -0,0 +1,769 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Smart warehouse stock finder with price and link information"""
|
| 2 |
+
|
| 3 |
+
import requests
|
| 4 |
+
import re
|
| 5 |
+
import os
|
| 6 |
+
import json
|
| 7 |
+
import xml.etree.ElementTree as ET
|
| 8 |
+
import time
|
| 9 |
+
|
| 10 |
+
# Cache configuration - 2 hours (reduced from 12 hours for more accurate results)
|
| 11 |
+
CACHE_DURATION = 7200 # 2 hours
|
| 12 |
+
cache = {
|
| 13 |
+
'warehouse_xml': {'data': None, 'time': 0},
|
| 14 |
+
'trek_xml': {'data': None, 'time': 0},
|
| 15 |
+
'products_summary': {'data': None, 'time': 0},
|
| 16 |
+
'search_results': {} # Cache for specific searches
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
def get_cached_trek_xml():
|
| 20 |
+
"""Get Trek XML with 12-hour caching"""
|
| 21 |
+
current_time = time.time()
|
| 22 |
+
|
| 23 |
+
if cache['trek_xml']['data'] and (current_time - cache['trek_xml']['time'] < CACHE_DURATION):
|
| 24 |
+
cache_age = (current_time - cache['trek_xml']['time']) / 60 # in minutes
|
| 25 |
+
return cache['trek_xml']['data']
|
| 26 |
+
|
| 27 |
+
try:
|
| 28 |
+
url = 'https://www.trekbisiklet.com.tr/output/8582384479'
|
| 29 |
+
response = requests.get(url, verify=False, timeout=10)
|
| 30 |
+
|
| 31 |
+
if response.status_code == 200:
|
| 32 |
+
cache['trek_xml']['data'] = response.content
|
| 33 |
+
cache['trek_xml']['time'] = current_time
|
| 34 |
+
return response.content
|
| 35 |
+
else:
|
| 36 |
+
return None
|
| 37 |
+
except Exception as e:
|
| 38 |
+
return None
|
| 39 |
+
|
| 40 |
+
def apply_price_rounding(price_str):
|
| 41 |
+
"""Apply the same price rounding formula used in app.py"""
|
| 42 |
+
if not price_str:
|
| 43 |
+
return price_str
|
| 44 |
+
|
| 45 |
+
try:
|
| 46 |
+
price_float = float(price_str)
|
| 47 |
+
if price_float > 200000:
|
| 48 |
+
return str(round(price_float / 5000) * 5000)
|
| 49 |
+
elif price_float > 30000:
|
| 50 |
+
return str(round(price_float / 1000) * 1000)
|
| 51 |
+
elif price_float > 10000:
|
| 52 |
+
return str(round(price_float / 100) * 100)
|
| 53 |
+
else:
|
| 54 |
+
return str(round(price_float / 10) * 10)
|
| 55 |
+
except:
|
| 56 |
+
return price_str
|
| 57 |
+
|
| 58 |
+
def get_product_price_and_link_by_sku(product_code):
|
| 59 |
+
"""Get price and link from Trek XML using improved SKU matching with new XML fields
|
| 60 |
+
Uses: stockCode, rootProductStockCode, isOptionOfAProduct, isOptionedProduct
|
| 61 |
+
Level 1: Search variants by stockCode where isOptionOfAProduct=1
|
| 62 |
+
Level 2: Search main products by stockCode where isOptionOfAProduct=0
|
| 63 |
+
Level 3: Search by rootProductStockCode for variant-to-main mapping
|
| 64 |
+
"""
|
| 65 |
+
try:
|
| 66 |
+
# Import XML parsing for cleaner approach
|
| 67 |
+
import xml.etree.ElementTree as ET
|
| 68 |
+
|
| 69 |
+
# Get cached Trek XML
|
| 70 |
+
xml_content = get_cached_trek_xml()
|
| 71 |
+
if not xml_content:
|
| 72 |
+
return None, None
|
| 73 |
+
|
| 74 |
+
# Convert bytes to string if needed
|
| 75 |
+
if isinstance(xml_content, bytes):
|
| 76 |
+
xml_content = xml_content.decode('utf-8')
|
| 77 |
+
|
| 78 |
+
# Parse XML properly instead of regex
|
| 79 |
+
try:
|
| 80 |
+
root = ET.fromstring(xml_content)
|
| 81 |
+
except:
|
| 82 |
+
# Fallback to regex if XML parsing fails
|
| 83 |
+
return get_product_price_and_link_by_sku_regex(product_code)
|
| 84 |
+
|
| 85 |
+
# Level 1: Search variants first (isOptionOfAProduct=1)
|
| 86 |
+
for item in root.findall('.//item'):
|
| 87 |
+
is_option_element = item.find('isOptionOfAProduct')
|
| 88 |
+
stock_code_element = item.find('stockCode')
|
| 89 |
+
|
| 90 |
+
if (is_option_element is not None and is_option_element.text == '1' and
|
| 91 |
+
stock_code_element is not None and stock_code_element.text and stock_code_element.text.strip() == product_code):
|
| 92 |
+
|
| 93 |
+
price_element = item.find('priceTaxWithCur')
|
| 94 |
+
link_element = item.find('productLink')
|
| 95 |
+
|
| 96 |
+
if price_element is not None and link_element is not None:
|
| 97 |
+
rounded_price = apply_price_rounding(price_element.text)
|
| 98 |
+
return rounded_price, link_element.text
|
| 99 |
+
|
| 100 |
+
# Level 2: Search main products (isOptionOfAProduct=0)
|
| 101 |
+
for item in root.findall('.//item'):
|
| 102 |
+
is_option_element = item.find('isOptionOfAProduct')
|
| 103 |
+
stock_code_element = item.find('stockCode')
|
| 104 |
+
|
| 105 |
+
if (is_option_element is not None and is_option_element.text == '0' and
|
| 106 |
+
stock_code_element is not None and stock_code_element.text and stock_code_element.text.strip() == product_code):
|
| 107 |
+
|
| 108 |
+
price_element = item.find('priceTaxWithCur')
|
| 109 |
+
link_element = item.find('productLink')
|
| 110 |
+
|
| 111 |
+
if price_element is not None and link_element is not None:
|
| 112 |
+
rounded_price = apply_price_rounding(price_element.text)
|
| 113 |
+
return rounded_price, link_element.text
|
| 114 |
+
|
| 115 |
+
# Level 3: Search by rootProductStockCode (variant parent lookup)
|
| 116 |
+
for item in root.findall('.//item'):
|
| 117 |
+
root_stock_element = item.find('rootProductStockCode')
|
| 118 |
+
|
| 119 |
+
if (root_stock_element is not None and root_stock_element.text and root_stock_element.text.strip() == product_code):
|
| 120 |
+
price_element = item.find('priceTaxWithCur')
|
| 121 |
+
link_element = item.find('productLink')
|
| 122 |
+
|
| 123 |
+
if price_element is not None and link_element is not None:
|
| 124 |
+
rounded_price = apply_price_rounding(price_element.text)
|
| 125 |
+
return rounded_price, link_element.text
|
| 126 |
+
|
| 127 |
+
# Not found
|
| 128 |
+
return None, None
|
| 129 |
+
|
| 130 |
+
except Exception as e:
|
| 131 |
+
return None, None
|
| 132 |
+
|
| 133 |
+
def get_product_price_and_link_by_sku_regex(product_code):
|
| 134 |
+
"""Fallback regex method for SKU lookup if XML parsing fails"""
|
| 135 |
+
try:
|
| 136 |
+
xml_content = get_cached_trek_xml()
|
| 137 |
+
if isinstance(xml_content, bytes):
|
| 138 |
+
xml_content = xml_content.decode('utf-8')
|
| 139 |
+
|
| 140 |
+
# Level 1: Search in variants first (isOptionOfAProduct=1)
|
| 141 |
+
variant_pattern = rf'<isOptionOfAProduct>1</isOptionOfAProduct>.*?<stockCode><!\[CDATA\[{re.escape(product_code)}\]\]></stockCode>.*?(?=<item>|$)'
|
| 142 |
+
variant_match = re.search(variant_pattern, xml_content, re.DOTALL)
|
| 143 |
+
|
| 144 |
+
if variant_match:
|
| 145 |
+
section = variant_match.group(0)
|
| 146 |
+
price_match = re.search(r'<price><!\[CDATA\[(.*?)\]\]></price>', section)
|
| 147 |
+
link_match = re.search(r'<producturl><!\[CDATA\[(.*?)\]\]></producturl>', section)
|
| 148 |
+
|
| 149 |
+
if price_match and link_match:
|
| 150 |
+
rounded_price = apply_price_rounding(price_match.group(1))
|
| 151 |
+
return rounded_price, link_match.group(1)
|
| 152 |
+
|
| 153 |
+
# Level 2: Search in main products (isOptionOfAProduct=0)
|
| 154 |
+
main_pattern = rf'<isOptionOfAProduct>0</isOptionOfAProduct>.*?<stockCode><!\[CDATA\[{re.escape(product_code)}\]\]></stockCode>.*?(?=<item>|$)'
|
| 155 |
+
main_match = re.search(main_pattern, xml_content, re.DOTALL)
|
| 156 |
+
|
| 157 |
+
if main_match:
|
| 158 |
+
section = main_match.group(0)
|
| 159 |
+
price_match = re.search(r'<price><!\[CDATA\[(.*?)\]\]></price>', section)
|
| 160 |
+
link_match = re.search(r'<producturl><!\[CDATA\[(.*?)\]\]></producturl>', section)
|
| 161 |
+
|
| 162 |
+
if price_match and link_match:
|
| 163 |
+
rounded_price = apply_price_rounding(price_match.group(1))
|
| 164 |
+
return rounded_price, link_match.group(1)
|
| 165 |
+
|
| 166 |
+
return None, None
|
| 167 |
+
|
| 168 |
+
except Exception as e:
|
| 169 |
+
return None, None
|
| 170 |
+
|
| 171 |
+
def get_product_price_and_link(product_name, variant=None):
|
| 172 |
+
"""Get price and link from Trek website XML"""
|
| 173 |
+
try:
|
| 174 |
+
# Get cached Trek XML
|
| 175 |
+
xml_content = get_cached_trek_xml()
|
| 176 |
+
if not xml_content:
|
| 177 |
+
return None, None
|
| 178 |
+
|
| 179 |
+
root = ET.fromstring(xml_content)
|
| 180 |
+
|
| 181 |
+
# Turkish character normalization FIRST (before lower())
|
| 182 |
+
tr_map = {
|
| 183 |
+
'İ': 'i', 'I': 'i', 'ı': 'i', # All I variations to i
|
| 184 |
+
'Ğ': 'g', 'ğ': 'g',
|
| 185 |
+
'Ü': 'u', 'ü': 'u',
|
| 186 |
+
'Ş': 's', 'ş': 's',
|
| 187 |
+
'Ö': 'o', 'ö': 'o',
|
| 188 |
+
'Ç': 'c', 'ç': 'c'
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
# Apply normalization to original (before lower)
|
| 192 |
+
search_name_normalized = product_name
|
| 193 |
+
search_variant_normalized = variant if variant else ""
|
| 194 |
+
for tr, en in tr_map.items():
|
| 195 |
+
search_name_normalized = search_name_normalized.replace(tr, en)
|
| 196 |
+
search_variant_normalized = search_variant_normalized.replace(tr, en)
|
| 197 |
+
|
| 198 |
+
# Now lowercase
|
| 199 |
+
search_name = search_name_normalized.lower()
|
| 200 |
+
search_variant = search_variant_normalized.lower()
|
| 201 |
+
|
| 202 |
+
best_match = None
|
| 203 |
+
best_score = 0
|
| 204 |
+
|
| 205 |
+
# Clean search name - remove year and parentheses
|
| 206 |
+
clean_search = re.sub(r'\s*\(\d{4}\)\s*', '', search_name).strip()
|
| 207 |
+
|
| 208 |
+
for item in root.findall('item'):
|
| 209 |
+
# Get product name
|
| 210 |
+
rootlabel_elem = item.find('rootlabel')
|
| 211 |
+
if rootlabel_elem is None or not rootlabel_elem.text:
|
| 212 |
+
continue
|
| 213 |
+
|
| 214 |
+
item_name = rootlabel_elem.text.lower()
|
| 215 |
+
for tr, en in tr_map.items():
|
| 216 |
+
item_name = item_name.replace(tr, en)
|
| 217 |
+
|
| 218 |
+
# Clean item name too
|
| 219 |
+
clean_item = re.sub(r'\s*\(\d{4}\)\s*', '', item_name).strip()
|
| 220 |
+
|
| 221 |
+
# Calculate match score with priority for exact matches
|
| 222 |
+
score = 0
|
| 223 |
+
|
| 224 |
+
# Exact match gets highest priority
|
| 225 |
+
if clean_search == clean_item:
|
| 226 |
+
score += 100
|
| 227 |
+
# Check if starts with exact product name (e.g., "fx 2" in "fx 2 kirmizi")
|
| 228 |
+
elif clean_item.startswith(clean_search + " ") or clean_item == clean_search:
|
| 229 |
+
score += 50
|
| 230 |
+
else:
|
| 231 |
+
# Partial matching
|
| 232 |
+
name_parts = clean_search.split()
|
| 233 |
+
for part in name_parts:
|
| 234 |
+
if part in clean_item:
|
| 235 |
+
score += 1
|
| 236 |
+
|
| 237 |
+
# Check variant if specified
|
| 238 |
+
if variant and search_variant in item_name:
|
| 239 |
+
score += 2 # Variant match is important
|
| 240 |
+
|
| 241 |
+
if score > best_score:
|
| 242 |
+
best_score = score
|
| 243 |
+
best_match = item
|
| 244 |
+
|
| 245 |
+
if best_match and best_score > 0:
|
| 246 |
+
# Extract price
|
| 247 |
+
price_elem = best_match.find('priceTaxWithCur')
|
| 248 |
+
price = price_elem.text if price_elem is not None and price_elem.text else None
|
| 249 |
+
|
| 250 |
+
# Round price
|
| 251 |
+
if price:
|
| 252 |
+
try:
|
| 253 |
+
price_float = float(price)
|
| 254 |
+
if price_float > 200000:
|
| 255 |
+
rounded = round(price_float / 5000) * 5000
|
| 256 |
+
price = f"{int(rounded):,}".replace(',', '.') + " TL"
|
| 257 |
+
elif price_float > 30000:
|
| 258 |
+
rounded = round(price_float / 1000) * 1000
|
| 259 |
+
price = f"{int(rounded):,}".replace(',', '.') + " TL"
|
| 260 |
+
elif price_float > 10000:
|
| 261 |
+
rounded = round(price_float / 100) * 100
|
| 262 |
+
price = f"{int(rounded):,}".replace(',', '.') + " TL"
|
| 263 |
+
else:
|
| 264 |
+
rounded = round(price_float / 10) * 10
|
| 265 |
+
price = f"{int(rounded):,}".replace(',', '.') + " TL"
|
| 266 |
+
except:
|
| 267 |
+
price = f"{price} TL"
|
| 268 |
+
|
| 269 |
+
# Extract link (field name is productLink, not productUrl!)
|
| 270 |
+
link_elem = best_match.find('productLink')
|
| 271 |
+
link = link_elem.text if link_elem is not None and link_elem.text else None
|
| 272 |
+
|
| 273 |
+
return price, link
|
| 274 |
+
|
| 275 |
+
return None, None
|
| 276 |
+
|
| 277 |
+
except Exception as e:
|
| 278 |
+
return None, None
|
| 279 |
+
|
| 280 |
+
def get_cached_warehouse_xml():
|
| 281 |
+
"""Get warehouse XML with 12-hour caching"""
|
| 282 |
+
current_time = time.time()
|
| 283 |
+
|
| 284 |
+
if cache['warehouse_xml']['data'] and (current_time - cache['warehouse_xml']['time'] < CACHE_DURATION):
|
| 285 |
+
cache_age = (current_time - cache['warehouse_xml']['time']) / 60 # in minutes
|
| 286 |
+
return cache['warehouse_xml']['data']
|
| 287 |
+
|
| 288 |
+
for attempt in range(3):
|
| 289 |
+
try:
|
| 290 |
+
url = 'https://video.trek-turkey.com/bizimhesap-warehouse-xml-b2b-api-v2.php'
|
| 291 |
+
timeout_val = 10 + (attempt * 5)
|
| 292 |
+
response = requests.get(url, verify=False, timeout=timeout_val)
|
| 293 |
+
xml_text = response.text
|
| 294 |
+
cache['warehouse_xml']['data'] = xml_text
|
| 295 |
+
cache['warehouse_xml']['time'] = current_time
|
| 296 |
+
|
| 297 |
+
return xml_text
|
| 298 |
+
except requests.exceptions.Timeout:
|
| 299 |
+
if attempt == 2:
|
| 300 |
+
return None
|
| 301 |
+
except Exception as e:
|
| 302 |
+
return None
|
| 303 |
+
|
| 304 |
+
return None
|
| 305 |
+
|
| 306 |
+
def get_warehouse_stock_smart_with_price(user_message, previous_result=None):
|
| 307 |
+
"""Enhanced smart warehouse search with price and link info"""
|
| 308 |
+
|
| 309 |
+
# Canlı destek / müşteri temsilcisi talepleri - ÜRÜN ARAMASI YAPMA
|
| 310 |
+
live_support_phrases = [
|
| 311 |
+
'müşteri bağla', 'canlı bağla', 'temsilci', 'yetkili', 'gerçek kişi',
|
| 312 |
+
'insan ile', 'operatör', 'canlı destek', 'bağlayın', 'bağlar mısın',
|
| 313 |
+
'görüşmek istiyorum', 'konuşmak istiyorum', 'yetkiliye bağla',
|
| 314 |
+
'müşteri hizmetleri', 'çağrı merkezi', 'santral', 'bağla'
|
| 315 |
+
]
|
| 316 |
+
|
| 317 |
+
clean_message = user_message.lower().strip()
|
| 318 |
+
|
| 319 |
+
for phrase in live_support_phrases:
|
| 320 |
+
if phrase in clean_message:
|
| 321 |
+
return None # Ürün araması yapma, GPT'ye bırak
|
| 322 |
+
|
| 323 |
+
# Filter out common non-product words and responses
|
| 324 |
+
non_product_words = [
|
| 325 |
+
'süper', 'harika', 'güzel', 'teşekkürler', 'teşekkür', 'tamam', 'olur',
|
| 326 |
+
'evet', 'hayır', 'merhaba', 'selam', 'iyi', 'kötü', 'fena', 'muhteşem',
|
| 327 |
+
'mükemmel', 'berbat', 'idare eder', 'olabilir', 'değil', 'var', 'yok',
|
| 328 |
+
'anladım', 'anlaşıldı', 'peki', 'tamamdır', 'ok', 'okay', 'aynen',
|
| 329 |
+
'kesinlikle', 'elbette', 'tabii', 'tabiki', 'doğru', 'yanlış'
|
| 330 |
+
]
|
| 331 |
+
|
| 332 |
+
# Check if message is just a simple response
|
| 333 |
+
if clean_message in non_product_words:
|
| 334 |
+
return None
|
| 335 |
+
|
| 336 |
+
# Brand keywords that should ALWAYS trigger product search regardless of length
|
| 337 |
+
brand_keywords = ['gobik', 'trek', 'bontrager', 'kask', 'shimano', 'sram', 'garmin', 'wahoo']
|
| 338 |
+
|
| 339 |
+
# Check if message contains a brand keyword
|
| 340 |
+
contains_brand = any(brand in clean_message for brand in brand_keywords)
|
| 341 |
+
|
| 342 |
+
# Check if it's a single word that's likely not a product
|
| 343 |
+
# BUT allow if it contains a known brand
|
| 344 |
+
if not contains_brand and len(clean_message.split()) == 1 and len(clean_message) < 5:
|
| 345 |
+
# Short single words are usually not product names
|
| 346 |
+
return None
|
| 347 |
+
|
| 348 |
+
# Check if this is a question rather than a product search
|
| 349 |
+
# BUT skip this check if message contains a known brand
|
| 350 |
+
question_indicators = [
|
| 351 |
+
'musun', 'müsün', 'misin', 'mısın', 'miyim', 'mıyım',
|
| 352 |
+
'musunuz', 'müsünüz', 'misiniz', 'mısınız',
|
| 353 |
+
'neden', 'nasıl', 'ne zaman', 'kim', 'nerede', 'nereye',
|
| 354 |
+
'ulaşamıyor', 'yapamıyor', 'gönderemiyor', 'edemiyor',
|
| 355 |
+
'?'
|
| 356 |
+
]
|
| 357 |
+
|
| 358 |
+
# If message contains question indicators, it's likely not a product search
|
| 359 |
+
# EXCEPTION: If message contains a brand keyword, still search for products
|
| 360 |
+
if not contains_brand:
|
| 361 |
+
for indicator in question_indicators:
|
| 362 |
+
if indicator in clean_message:
|
| 363 |
+
return None
|
| 364 |
+
|
| 365 |
+
# Normalize cache key for consistent caching (Turkish chars + lowercase)
|
| 366 |
+
def normalize_for_cache(text):
|
| 367 |
+
"""Normalize text for cache key"""
|
| 368 |
+
tr_map = {'İ': 'i', 'I': 'i', 'ı': 'i', 'Ğ': 'g', 'ğ': 'g', 'Ü': 'u', 'ü': 'u',
|
| 369 |
+
'Ş': 's', 'ş': 's', 'Ö': 'o', 'ö': 'o', 'Ç': 'c', 'ç': 'c'}
|
| 370 |
+
for tr, en in tr_map.items():
|
| 371 |
+
text = text.replace(tr, en)
|
| 372 |
+
return text.lower().strip()
|
| 373 |
+
|
| 374 |
+
# Check search cache first
|
| 375 |
+
cache_key = normalize_for_cache(user_message)
|
| 376 |
+
current_time = time.time()
|
| 377 |
+
|
| 378 |
+
if cache_key in cache['search_results']:
|
| 379 |
+
cached = cache['search_results'][cache_key]
|
| 380 |
+
if current_time - cached['time'] < CACHE_DURATION:
|
| 381 |
+
cache_age = (current_time - cached['time']) / 60 # in minutes
|
| 382 |
+
return cached['data']
|
| 383 |
+
else:
|
| 384 |
+
pass
|
| 385 |
+
|
| 386 |
+
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
| 387 |
+
|
| 388 |
+
# Check if user is asking about specific warehouse
|
| 389 |
+
warehouse_keywords = {
|
| 390 |
+
'caddebostan': 'Caddebostan',
|
| 391 |
+
'ortaköy': 'Ortaköy',
|
| 392 |
+
'ortakoy': 'Ortaköy',
|
| 393 |
+
'alsancak': 'Alsancak',
|
| 394 |
+
'izmir': 'Alsancak',
|
| 395 |
+
'bahçeköy': 'Bahçeköy',
|
| 396 |
+
'bahcekoy': 'Bahçeköy',
|
| 397 |
+
'sarıyer': 'Bahçeköy',
|
| 398 |
+
'sariyer': 'Bahçeköy'
|
| 399 |
+
}
|
| 400 |
+
|
| 401 |
+
user_lower = user_message.lower()
|
| 402 |
+
asked_warehouse = None
|
| 403 |
+
for keyword, warehouse in warehouse_keywords.items():
|
| 404 |
+
if keyword in user_lower:
|
| 405 |
+
asked_warehouse = warehouse
|
| 406 |
+
break
|
| 407 |
+
|
| 408 |
+
# Get cached XML data
|
| 409 |
+
xml_text = get_cached_warehouse_xml()
|
| 410 |
+
if not xml_text:
|
| 411 |
+
return None
|
| 412 |
+
|
| 413 |
+
# Extract product blocks
|
| 414 |
+
product_pattern = r'<Product>(.*?)</Product>'
|
| 415 |
+
all_products = re.findall(product_pattern, xml_text, re.DOTALL)
|
| 416 |
+
|
| 417 |
+
# Create simplified product list for GPT
|
| 418 |
+
products_summary = []
|
| 419 |
+
for i, product_block in enumerate(all_products):
|
| 420 |
+
name_match = re.search(r'<ProductName><!\[CDATA\[(.*?)\]\]></ProductName>', product_block)
|
| 421 |
+
variant_match = re.search(r'<ProductVariant><!\[CDATA\[(.*?)\]\]></ProductVariant>', product_block)
|
| 422 |
+
|
| 423 |
+
if name_match:
|
| 424 |
+
warehouses_with_stock = []
|
| 425 |
+
warehouse_regex = r'<Warehouse>.*?<Name><!\[CDATA\[(.*?)\]\]></Name>.*?<Stock>(.*?)</Stock>.*?</Warehouse>'
|
| 426 |
+
warehouses = re.findall(warehouse_regex, product_block, re.DOTALL)
|
| 427 |
+
|
| 428 |
+
for wh_name, wh_stock in warehouses:
|
| 429 |
+
try:
|
| 430 |
+
if int(wh_stock.strip()) > 0:
|
| 431 |
+
warehouses_with_stock.append(wh_name)
|
| 432 |
+
except:
|
| 433 |
+
pass
|
| 434 |
+
|
| 435 |
+
product_info = {
|
| 436 |
+
"index": i,
|
| 437 |
+
"name": name_match.group(1),
|
| 438 |
+
"variant": variant_match.group(1) if variant_match else "",
|
| 439 |
+
"warehouses": warehouses_with_stock
|
| 440 |
+
}
|
| 441 |
+
products_summary.append(product_info)
|
| 442 |
+
|
| 443 |
+
# Prepare warehouse filter if needed
|
| 444 |
+
warehouse_filter = ""
|
| 445 |
+
if asked_warehouse:
|
| 446 |
+
warehouse_filter = f"\nIMPORTANT: User is asking specifically about {asked_warehouse} warehouse. Only return products available in that warehouse."
|
| 447 |
+
|
| 448 |
+
# Debug logging
|
| 449 |
+
# Check if the target product exists
|
| 450 |
+
# Normalize Turkish characters for comparison
|
| 451 |
+
def normalize_turkish(text):
|
| 452 |
+
text = text.upper()
|
| 453 |
+
replacements = {'I': 'İ', 'Ç': 'C', 'Ş': 'S', 'Ğ': 'G', 'Ü': 'U', 'Ö': 'O'}
|
| 454 |
+
# Also try with İ -> I conversion
|
| 455 |
+
text2 = text.replace('İ', 'I')
|
| 456 |
+
return text, text2
|
| 457 |
+
|
| 458 |
+
search_term = user_message.upper()
|
| 459 |
+
search_norm1, search_norm2 = normalize_turkish(search_term)
|
| 460 |
+
|
| 461 |
+
matching_products = []
|
| 462 |
+
for p in products_summary:
|
| 463 |
+
p_name = p['name'].upper()
|
| 464 |
+
# Check both original and normalized versions
|
| 465 |
+
if (search_term in p_name or
|
| 466 |
+
search_norm1 in p_name or
|
| 467 |
+
search_norm2 in p_name or
|
| 468 |
+
search_term.replace('I', 'İ') in p_name):
|
| 469 |
+
matching_products.append(p)
|
| 470 |
+
|
| 471 |
+
if matching_products:
|
| 472 |
+
pass
|
| 473 |
+
else:
|
| 474 |
+
pass
|
| 475 |
+
|
| 476 |
+
# GPT-5 prompt with enhanced instructions
|
| 477 |
+
smart_prompt = f"""User is asking: "{user_message}"
|
| 478 |
+
|
| 479 |
+
FIRST CHECK: Is this actually a product search?
|
| 480 |
+
- If the message is a question about the system, service, or a general inquiry, return: -1
|
| 481 |
+
- If the message contains "musun", "misin", "neden", "nasıl", etc. it's likely NOT a product search
|
| 482 |
+
- Only proceed if this looks like a genuine product name or model
|
| 483 |
+
|
| 484 |
+
Find ALL products that match this query from the list below.
|
| 485 |
+
If user asks about specific size (S, M, L, XL, XXL, SMALL, MEDIUM, LARGE, X-LARGE), return only that size.
|
| 486 |
+
If user asks generally (without size), return ALL variants of the product.
|
| 487 |
+
{warehouse_filter}
|
| 488 |
+
|
| 489 |
+
CRITICAL TURKISH CHARACTER RULES:
|
| 490 |
+
- "MARLIN" and "MARLİN" are the SAME product (Turkish İ vs I)
|
| 491 |
+
- Treat these as equivalent: I/İ/ı, Ö/ö, Ü/ü, Ş/ş, Ğ/ğ, Ç/ç
|
| 492 |
+
- If user writes "Marlin", also match "MARLİN" in the list
|
| 493 |
+
|
| 494 |
+
IMPORTANT BRAND AND PRODUCT TYPE RULES:
|
| 495 |
+
- GOBIK: Spanish textile brand we import. When user asks about "gobik", return ALL products with "GOBIK" in the name.
|
| 496 |
+
- Product names contain type information: FORMA (jersey/cycling shirt), TAYT (tights), İÇLİK (base layer), YAĞMURLUK (raincoat), etc.
|
| 497 |
+
- Understand Turkish/English terms:
|
| 498 |
+
* "erkek forma" / "men's jersey" -> Find products with FORMA in name
|
| 499 |
+
* "tayt" / "tights" -> Find products with TAYT in name
|
| 500 |
+
* "içlik" / "base layer" -> Find products with İÇLİK in name
|
| 501 |
+
* "yağmurluk" / "raincoat" -> Find products with YAĞMURLUK in name
|
| 502 |
+
- Gender: UNISEX means for both men and women. If no gender specified, it's typically men's.
|
| 503 |
+
|
| 504 |
+
Products list (with warehouse availability):
|
| 505 |
+
{json.dumps(products_summary, ensure_ascii=False, indent=2)}
|
| 506 |
+
|
| 507 |
+
Return ONLY index numbers of ALL matching products as comma-separated list (e.g., "5,8,12,15").
|
| 508 |
+
If no products found, return ONLY: -1
|
| 509 |
+
DO NOT return empty string or any explanation, ONLY numbers or -1
|
| 510 |
+
|
| 511 |
+
Examples of correct responses:
|
| 512 |
+
- "2,5,8,12,15,20" (multiple products found)
|
| 513 |
+
- "45" (single product found)
|
| 514 |
+
- "-1" (no products found)"""
|
| 515 |
+
|
| 516 |
+
# Check if we have API key before making the request
|
| 517 |
+
if not OPENAI_API_KEY:
|
| 518 |
+
# Try to find in Trek XML directly as fallback, but avoid tool products
|
| 519 |
+
user_message_normalized = user_message.upper()
|
| 520 |
+
tool_indicators = ['SUPER B', 'ANAHTAR', 'TAKIMI', 'PENSE', 'TOOL', 'ADAPTÖR', 'CONVERTER']
|
| 521 |
+
should_skip_trek_lookup = any(indicator in user_message_normalized for indicator in tool_indicators)
|
| 522 |
+
|
| 523 |
+
price, link = None, None
|
| 524 |
+
if not should_skip_trek_lookup:
|
| 525 |
+
price, link = get_product_price_and_link(user_message)
|
| 526 |
+
|
| 527 |
+
if price and link:
|
| 528 |
+
return [
|
| 529 |
+
f"🚲 **{user_message.title()}**",
|
| 530 |
+
f"💰 Fiyat: {price}",
|
| 531 |
+
f"🔗 Link: {link}",
|
| 532 |
+
"",
|
| 533 |
+
"⚠️ **Stok durumu kontrol edilemiyor**",
|
| 534 |
+
"📞 Güncel stok için mağazalarımızı arayın:",
|
| 535 |
+
"• Caddebostan: 0543 934 0438",
|
| 536 |
+
"• Alsancak: 0543 936 2335"
|
| 537 |
+
]
|
| 538 |
+
return None
|
| 539 |
+
|
| 540 |
+
headers = {
|
| 541 |
+
"Content-Type": "application/json",
|
| 542 |
+
"Authorization": f"Bearer {OPENAI_API_KEY}"
|
| 543 |
+
}
|
| 544 |
+
|
| 545 |
+
# GPT-5.2 modeli temperature ve max_tokens desteklemiyor
|
| 546 |
+
payload = {
|
| 547 |
+
"model": "gpt-5.2-chat-latest",
|
| 548 |
+
"messages": [
|
| 549 |
+
{"role": "system", "content": "You are a product matcher. Find ALL matching products. Return only index numbers."},
|
| 550 |
+
{"role": "user", "content": smart_prompt}
|
| 551 |
+
]
|
| 552 |
+
}
|
| 553 |
+
|
| 554 |
+
try:
|
| 555 |
+
response = requests.post(
|
| 556 |
+
"https://api.openai.com/v1/chat/completions",
|
| 557 |
+
headers=headers,
|
| 558 |
+
json=payload,
|
| 559 |
+
timeout=10
|
| 560 |
+
)
|
| 561 |
+
|
| 562 |
+
if response.status_code == 200:
|
| 563 |
+
result = response.json()
|
| 564 |
+
indices_str = result['choices'][0]['message']['content'].strip()
|
| 565 |
+
|
| 566 |
+
# Handle empty response - try Trek XML as fallback, but avoid tool products
|
| 567 |
+
if not indices_str or indices_str == "-1":
|
| 568 |
+
# Try to find in Trek XML directly, but skip tools
|
| 569 |
+
user_message_normalized = user_message.upper()
|
| 570 |
+
tool_indicators = ['SUPER B', 'ANAHTAR', 'TAKIMI', 'PENSE', 'TOOL', 'ADAPTÖR', 'CONVERTER']
|
| 571 |
+
should_skip_trek_lookup = any(indicator in user_message_normalized for indicator in tool_indicators)
|
| 572 |
+
|
| 573 |
+
price, link = None, None
|
| 574 |
+
if not should_skip_trek_lookup:
|
| 575 |
+
price, link = get_product_price_and_link(user_message)
|
| 576 |
+
|
| 577 |
+
if price and link:
|
| 578 |
+
# Found in Trek XML but not in warehouse stock!
|
| 579 |
+
return [
|
| 580 |
+
f"🚲 **{user_message.title()}**",
|
| 581 |
+
f"💰 Fiyat: {price}",
|
| 582 |
+
f"🔗 Link: {link}",
|
| 583 |
+
"",
|
| 584 |
+
"❌ **Stok Durumu: TÜKENDİ**",
|
| 585 |
+
"",
|
| 586 |
+
"📞 Stok güncellemesi veya ön sipariş için mağazalarımızı arayabilirsiniz:",
|
| 587 |
+
"• Caddebostan: 0543 934 0438",
|
| 588 |
+
"• Alsancak: 0543 936 2335"
|
| 589 |
+
]
|
| 590 |
+
return None
|
| 591 |
+
|
| 592 |
+
try:
|
| 593 |
+
# Filter out empty strings and parse indices
|
| 594 |
+
indices = []
|
| 595 |
+
for idx in indices_str.split(','):
|
| 596 |
+
idx = idx.strip()
|
| 597 |
+
if idx and idx.isdigit():
|
| 598 |
+
indices.append(int(idx))
|
| 599 |
+
|
| 600 |
+
# Collect all matching products with price/link
|
| 601 |
+
all_variants = []
|
| 602 |
+
warehouse_stock = {}
|
| 603 |
+
|
| 604 |
+
for idx in indices:
|
| 605 |
+
if 0 <= idx < len(all_products):
|
| 606 |
+
product_block = all_products[idx]
|
| 607 |
+
|
| 608 |
+
# Get product details
|
| 609 |
+
name_match = re.search(r'<ProductName><!\[CDATA\[(.*?)\]\]></ProductName>', product_block)
|
| 610 |
+
variant_match = re.search(r'<ProductVariant><!\[CDATA\[(.*?)\]\]></ProductVariant>', product_block)
|
| 611 |
+
productcode_match = re.search(r'<ProductCode><!\[CDATA\[(.*?)\]\]></ProductCode>', product_block)
|
| 612 |
+
|
| 613 |
+
if name_match:
|
| 614 |
+
product_name = name_match.group(1)
|
| 615 |
+
variant = variant_match.group(1) if variant_match else ""
|
| 616 |
+
|
| 617 |
+
# Get price and link from Trek website - TRY SKU FIRST (NEW METHOD)
|
| 618 |
+
price, link = None, None
|
| 619 |
+
|
| 620 |
+
# Try SKU-based lookup first if ProductCode exists
|
| 621 |
+
product_code = productcode_match.group(1) if productcode_match else None
|
| 622 |
+
if product_code and product_code.strip():
|
| 623 |
+
price, link = get_product_price_and_link_by_sku(product_code.strip())
|
| 624 |
+
|
| 625 |
+
# Fallback to name-based if SKU didn't work, but be more careful about matching
|
| 626 |
+
if not price or not link:
|
| 627 |
+
# Only do name-based fallback if the product might reasonably be sold by Trek
|
| 628 |
+
# Avoid tools/accessories that clearly don't belong to Trek's bicycle catalog
|
| 629 |
+
product_name_normalized = product_name.upper()
|
| 630 |
+
|
| 631 |
+
# Skip name-based fallback for obvious tools/non-bike products
|
| 632 |
+
tool_indicators = ['SUPER B', 'ANAHTAR', 'TAKIMI', 'PENSE', 'TOOL', 'ADAPTÖR', 'CONVERTER']
|
| 633 |
+
|
| 634 |
+
should_skip_fallback = any(indicator in product_name_normalized for indicator in tool_indicators)
|
| 635 |
+
|
| 636 |
+
if not should_skip_fallback:
|
| 637 |
+
price, link = get_product_price_and_link(product_name, variant)
|
| 638 |
+
|
| 639 |
+
variant_info = {
|
| 640 |
+
'name': product_name,
|
| 641 |
+
'variant': variant,
|
| 642 |
+
'price': price,
|
| 643 |
+
'link': link,
|
| 644 |
+
'warehouses': []
|
| 645 |
+
}
|
| 646 |
+
|
| 647 |
+
# Get warehouse stock
|
| 648 |
+
warehouse_regex = r'<Warehouse>.*?<Name><!\[CDATA\[(.*?)\]\]></Name>.*?<Stock>(.*?)</Stock>.*?</Warehouse>'
|
| 649 |
+
warehouses = re.findall(warehouse_regex, product_block, re.DOTALL)
|
| 650 |
+
|
| 651 |
+
for wh_name, wh_stock in warehouses:
|
| 652 |
+
try:
|
| 653 |
+
stock = int(wh_stock.strip())
|
| 654 |
+
if stock > 0:
|
| 655 |
+
display_name = format_warehouse_name(wh_name)
|
| 656 |
+
variant_info['warehouses'].append({
|
| 657 |
+
'name': display_name,
|
| 658 |
+
'stock': stock
|
| 659 |
+
})
|
| 660 |
+
|
| 661 |
+
if display_name not in warehouse_stock:
|
| 662 |
+
warehouse_stock[display_name] = 0
|
| 663 |
+
warehouse_stock[display_name] += stock
|
| 664 |
+
except:
|
| 665 |
+
pass
|
| 666 |
+
|
| 667 |
+
if variant_info['warehouses']:
|
| 668 |
+
all_variants.append(variant_info)
|
| 669 |
+
|
| 670 |
+
# Format result
|
| 671 |
+
result = []
|
| 672 |
+
|
| 673 |
+
if asked_warehouse:
|
| 674 |
+
# Filter for specific warehouse
|
| 675 |
+
warehouse_variants = []
|
| 676 |
+
for variant in all_variants:
|
| 677 |
+
for wh in variant['warehouses']:
|
| 678 |
+
if asked_warehouse in wh['name']:
|
| 679 |
+
warehouse_variants.append(variant)
|
| 680 |
+
break
|
| 681 |
+
|
| 682 |
+
if warehouse_variants:
|
| 683 |
+
result.append(f"{format_warehouse_name(asked_warehouse)} mağazasında mevcut:")
|
| 684 |
+
for v in warehouse_variants:
|
| 685 |
+
variant_text = f" ({v['variant']})" if v['variant'] else ""
|
| 686 |
+
result.append(f"• {v['name']}{variant_text}")
|
| 687 |
+
if v['price']:
|
| 688 |
+
result.append(f" Fiyat: {v['price']}")
|
| 689 |
+
if v['link']:
|
| 690 |
+
result.append(f" Link: {v['link']}")
|
| 691 |
+
else:
|
| 692 |
+
result.append(f"{format_warehouse_name(asked_warehouse)} mağazasında bu ürün mevcut değil")
|
| 693 |
+
else:
|
| 694 |
+
# Show all variants
|
| 695 |
+
if all_variants:
|
| 696 |
+
# Group by product name for cleaner display
|
| 697 |
+
product_groups = {}
|
| 698 |
+
for variant in all_variants:
|
| 699 |
+
if variant['name'] not in product_groups:
|
| 700 |
+
product_groups[variant['name']] = []
|
| 701 |
+
product_groups[variant['name']].append(variant)
|
| 702 |
+
|
| 703 |
+
result.append(f"Bulunan ürünler:")
|
| 704 |
+
|
| 705 |
+
for product_name, variants in product_groups.items():
|
| 706 |
+
result.append(f"\n{product_name}:")
|
| 707 |
+
|
| 708 |
+
# Show first variant's price and link (usually same for all variants)
|
| 709 |
+
if variants[0]['price']:
|
| 710 |
+
result.append(f"Fiyat: {variants[0]['price']}")
|
| 711 |
+
if variants[0]['link']:
|
| 712 |
+
result.append(f"Link: {variants[0]['link']}")
|
| 713 |
+
|
| 714 |
+
# Show variants and their availability
|
| 715 |
+
for v in variants:
|
| 716 |
+
if v['variant']:
|
| 717 |
+
warehouses_str = ", ".join([w['name'].replace(' mağazası', '') for w in v['warehouses']])
|
| 718 |
+
result.append(f"• {v['variant']}: {warehouses_str}")
|
| 719 |
+
|
| 720 |
+
else:
|
| 721 |
+
# No warehouse stock found - check if product exists in Trek
|
| 722 |
+
# But be careful not to match tools/accessories with bikes
|
| 723 |
+
user_message_normalized = user_message.upper()
|
| 724 |
+
tool_indicators = ['SUPER B', 'ANAHTAR', 'TAKIMI', 'PENSE', 'TOOL', 'ADAPTÖR', 'CONVERTER']
|
| 725 |
+
should_skip_trek_lookup = any(indicator in user_message_normalized for indicator in tool_indicators)
|
| 726 |
+
|
| 727 |
+
price, link = None, None
|
| 728 |
+
if not should_skip_trek_lookup:
|
| 729 |
+
price, link = get_product_price_and_link(user_message)
|
| 730 |
+
|
| 731 |
+
if price and link:
|
| 732 |
+
result.append(f"❌ **Stok Durumu: TÜM MAĞAZALARDA TÜKENDİ**")
|
| 733 |
+
result.append("")
|
| 734 |
+
result.append(f"💰 Web Fiyatı: {price}")
|
| 735 |
+
result.append(f"🔗 Ürün Detayları: {link}")
|
| 736 |
+
result.append("")
|
| 737 |
+
result.append("📞 Stok güncellemesi veya ön sipariş için:")
|
| 738 |
+
result.append("• Caddebostan: 0543 934 0438")
|
| 739 |
+
result.append("• Alsancak: 0543 936 2335")
|
| 740 |
+
else:
|
| 741 |
+
return None
|
| 742 |
+
|
| 743 |
+
# Cache the result before returning
|
| 744 |
+
cache['search_results'][cache_key] = {
|
| 745 |
+
'data': result,
|
| 746 |
+
'time': current_time
|
| 747 |
+
}
|
| 748 |
+
return result
|
| 749 |
+
|
| 750 |
+
except (ValueError, IndexError) as e:
|
| 751 |
+
return None
|
| 752 |
+
else:
|
| 753 |
+
return None
|
| 754 |
+
|
| 755 |
+
except Exception as e:
|
| 756 |
+
return None
|
| 757 |
+
|
| 758 |
+
def format_warehouse_name(wh_name):
|
| 759 |
+
"""Format warehouse name nicely"""
|
| 760 |
+
if "CADDEBOSTAN" in wh_name:
|
| 761 |
+
return "Caddebostan mağazası"
|
| 762 |
+
elif "ORTAKÖY" in wh_name:
|
| 763 |
+
return "Ortaköy mağazası"
|
| 764 |
+
elif "ALSANCAK" in wh_name:
|
| 765 |
+
return "İzmir Alsancak mağazası"
|
| 766 |
+
elif "BAHCEKOY" in wh_name or "BAHÇEKÖY" in wh_name:
|
| 767 |
+
return "Bahçeköy mağazası"
|
| 768 |
+
else:
|
| 769 |
+
return wh_name.replace("MAGAZA DEPO", "").strip()
|
store_notification.py
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
|
| 4 |
+
"""
|
| 5 |
+
WhatsApp Mağaza Bildirim Sistemi
|
| 6 |
+
Müşteri taleplerini mağazalara WhatsApp ile bildirir
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import os
|
| 10 |
+
import logging
|
| 11 |
+
from datetime import datetime
|
| 12 |
+
from twilio.rest import Client
|
| 13 |
+
from typing import Optional, Dict
|
| 14 |
+
|
| 15 |
+
logger = logging.getLogger(__name__)
|
| 16 |
+
|
| 17 |
+
# Mağaza WhatsApp numaraları
|
| 18 |
+
STORE_NUMBERS = {
|
| 19 |
+
"caddebostan": "+905439340438", # Alsancak - Mehmet Bey
|
| 20 |
+
"sariyer": "+905421371080", # Alsancak - Mehmet Bey
|
| 21 |
+
"alsancak": "+905439362335", # İzmir Alsancak - Mehmet Bey
|
| 22 |
+
"merkez": "+905439362335" # Mehmet Bey - TÜM bildirimler buraya
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
# Twilio ayarları
|
| 26 |
+
TWILIO_ACCOUNT_SID = os.getenv("TWILIO_ACCOUNT_SID")
|
| 27 |
+
TWILIO_AUTH_TOKEN = os.getenv("TWILIO_AUTH_TOKEN")
|
| 28 |
+
TWILIO_MESSAGING_SERVICE_SID = os.getenv("TWILIO_MESSAGING_SERVICE_SID")
|
| 29 |
+
|
| 30 |
+
def send_store_notification(
|
| 31 |
+
customer_phone: str,
|
| 32 |
+
customer_name: Optional[str],
|
| 33 |
+
product_name: str,
|
| 34 |
+
action: str, # "reserve", "info", "price", "stock"
|
| 35 |
+
store_name: Optional[str] = None,
|
| 36 |
+
additional_info: Optional[str] = None
|
| 37 |
+
) -> bool:
|
| 38 |
+
"""
|
| 39 |
+
Mağazaya WhatsApp bildirimi gönder
|
| 40 |
+
|
| 41 |
+
Args:
|
| 42 |
+
customer_phone: Müşteri telefon numarası
|
| 43 |
+
customer_name: Müşteri adı (opsiyonel)
|
| 44 |
+
product_name: Ürün adı
|
| 45 |
+
action: İşlem tipi (ayırtma, bilgi, fiyat, stok)
|
| 46 |
+
store_name: Hangi mağazaya bildirim gidecek
|
| 47 |
+
additional_info: Ek bilgi
|
| 48 |
+
|
| 49 |
+
Returns:
|
| 50 |
+
Başarılı ise True
|
| 51 |
+
"""
|
| 52 |
+
|
| 53 |
+
try:
|
| 54 |
+
# Mesaj içeriğini hazırla (her durumda log için gerekli)
|
| 55 |
+
message = format_notification_message(
|
| 56 |
+
customer_phone,
|
| 57 |
+
customer_name,
|
| 58 |
+
product_name,
|
| 59 |
+
action,
|
| 60 |
+
store_name,
|
| 61 |
+
additional_info
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
# FALLBACK: Bildirimi dosyaya kaydet
|
| 65 |
+
import json
|
| 66 |
+
from datetime import datetime
|
| 67 |
+
notification_log = {
|
| 68 |
+
"timestamp": datetime.now().isoformat(),
|
| 69 |
+
"customer_phone": customer_phone,
|
| 70 |
+
"customer_name": customer_name,
|
| 71 |
+
"product_name": product_name,
|
| 72 |
+
"action": action,
|
| 73 |
+
"store_name": store_name,
|
| 74 |
+
"additional_info": additional_info,
|
| 75 |
+
"formatted_message": message
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
# Log dosyasına ekle
|
| 79 |
+
log_file = "mehmet_bey_notifications.json"
|
| 80 |
+
try:
|
| 81 |
+
with open(log_file, "r") as f:
|
| 82 |
+
logs = json.load(f)
|
| 83 |
+
except:
|
| 84 |
+
logs = []
|
| 85 |
+
|
| 86 |
+
logs.append(notification_log)
|
| 87 |
+
|
| 88 |
+
# Son 100 bildirimi tut
|
| 89 |
+
if len(logs) > 100:
|
| 90 |
+
logs = logs[-100:]
|
| 91 |
+
|
| 92 |
+
with open(log_file, "w") as f:
|
| 93 |
+
json.dump(logs, f, indent=2, ensure_ascii=False)
|
| 94 |
+
|
| 95 |
+
logger.info(f"📝 Bildirim kaydedildi: {log_file}")
|
| 96 |
+
logger.info(f" Action: {action}, Product: {product_name}")
|
| 97 |
+
|
| 98 |
+
# Twilio client oluştur (varsa)
|
| 99 |
+
if not TWILIO_ACCOUNT_SID or not TWILIO_AUTH_TOKEN:
|
| 100 |
+
logger.warning("⚠️ Twilio credentials missing - notification saved to file only")
|
| 101 |
+
return True # Dosyaya kaydettik, başarılı say
|
| 102 |
+
|
| 103 |
+
client = Client(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN)
|
| 104 |
+
|
| 105 |
+
# Hedef mağaza numarasını belirle
|
| 106 |
+
if store_name and store_name.lower() in STORE_NUMBERS:
|
| 107 |
+
target_number = STORE_NUMBERS[store_name.lower()]
|
| 108 |
+
else:
|
| 109 |
+
target_number = STORE_NUMBERS["merkez"]
|
| 110 |
+
|
| 111 |
+
# Mesaj içeriğini hazırla
|
| 112 |
+
message = format_notification_message(
|
| 113 |
+
customer_phone,
|
| 114 |
+
customer_name,
|
| 115 |
+
product_name,
|
| 116 |
+
action,
|
| 117 |
+
store_name,
|
| 118 |
+
additional_info
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
# WhatsApp mesajı gönder
|
| 122 |
+
whatsapp_number = f"whatsapp:{target_number}"
|
| 123 |
+
|
| 124 |
+
# WhatsApp Business numaramız
|
| 125 |
+
from_number = "whatsapp:+905332047254" # Bizim WhatsApp Business numaramız
|
| 126 |
+
|
| 127 |
+
try:
|
| 128 |
+
# WhatsApp Business numaramızdan gönder
|
| 129 |
+
msg = client.messages.create(
|
| 130 |
+
from_=from_number, # whatsapp:+905332047254
|
| 131 |
+
body=message,
|
| 132 |
+
to=whatsapp_number,
|
| 133 |
+
# StatusCallback parametresini vermeyelim
|
| 134 |
+
)
|
| 135 |
+
except Exception as twilio_error:
|
| 136 |
+
# Eğer Twilio hatası varsa, log'a yaz ama yine de başarılı say
|
| 137 |
+
logger.error(f"Twilio API hatası: {twilio_error}")
|
| 138 |
+
if "21609" in str(twilio_error):
|
| 139 |
+
logger.info("Not: Hedef numara henüz Sandbox'a katılmamış olabilir")
|
| 140 |
+
return True # Log'a kaydettik, başarılı sayıyoruz
|
| 141 |
+
|
| 142 |
+
logger.info(f"✅ Mağaza bildirimi gönderildi: {msg.sid}")
|
| 143 |
+
logger.info(f" Hedef: {whatsapp_number}")
|
| 144 |
+
logger.info(f" İşlem: {action}")
|
| 145 |
+
|
| 146 |
+
return True
|
| 147 |
+
|
| 148 |
+
except Exception as e:
|
| 149 |
+
logger.error(f"❌ Mağaza bildirim hatası: {e}")
|
| 150 |
+
return False
|
| 151 |
+
|
| 152 |
+
def format_notification_message(
|
| 153 |
+
customer_phone: str,
|
| 154 |
+
customer_name: Optional[str],
|
| 155 |
+
product_name: str,
|
| 156 |
+
action: str,
|
| 157 |
+
store_name: Optional[str],
|
| 158 |
+
additional_info: Optional[str]
|
| 159 |
+
) -> str:
|
| 160 |
+
"""
|
| 161 |
+
Bildirim mesajını formatla
|
| 162 |
+
"""
|
| 163 |
+
|
| 164 |
+
# Tarih ve saat
|
| 165 |
+
now = datetime.now()
|
| 166 |
+
date_str = now.strftime("%d.%m.%Y %H:%M")
|
| 167 |
+
|
| 168 |
+
# İşlem tiplerine göre emoji ve başlık
|
| 169 |
+
action_config = {
|
| 170 |
+
"reserve": {
|
| 171 |
+
"emoji": "🔔",
|
| 172 |
+
"title": "YENİ AYIRTMA TALEBİ"
|
| 173 |
+
},
|
| 174 |
+
"info": {
|
| 175 |
+
"emoji": "ℹ️",
|
| 176 |
+
"title": "BİLGİ TALEBİ"
|
| 177 |
+
},
|
| 178 |
+
"price": {
|
| 179 |
+
"emoji": "💰",
|
| 180 |
+
"title": "FİYAT SORUSU"
|
| 181 |
+
},
|
| 182 |
+
"stock": {
|
| 183 |
+
"emoji": "📦",
|
| 184 |
+
"title": "STOK SORUSU"
|
| 185 |
+
},
|
| 186 |
+
"test": {
|
| 187 |
+
"emoji": "🧪",
|
| 188 |
+
"title": "TEST DENEMESİ"
|
| 189 |
+
}
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
config = action_config.get(action, action_config["info"])
|
| 193 |
+
|
| 194 |
+
# Mesaj oluştur
|
| 195 |
+
message_parts = [
|
| 196 |
+
f"{config['emoji']} *{config['title']}*",
|
| 197 |
+
f"📅 {date_str}",
|
| 198 |
+
"",
|
| 199 |
+
f"👤 *Müşteri:*"
|
| 200 |
+
]
|
| 201 |
+
|
| 202 |
+
if customer_name:
|
| 203 |
+
message_parts.append(f" İsim: {customer_name}")
|
| 204 |
+
|
| 205 |
+
# Telefon numarasını formatla
|
| 206 |
+
phone_display = customer_phone.replace("whatsapp:", "")
|
| 207 |
+
message_parts.append(f" Tel: {phone_display}")
|
| 208 |
+
|
| 209 |
+
message_parts.extend([
|
| 210 |
+
"",
|
| 211 |
+
f"🚲 *Ürün:* {product_name}"
|
| 212 |
+
])
|
| 213 |
+
|
| 214 |
+
if store_name:
|
| 215 |
+
message_parts.append(f"🏪 *Mağaza:* {store_name.title()}")
|
| 216 |
+
|
| 217 |
+
if additional_info:
|
| 218 |
+
message_parts.extend([
|
| 219 |
+
"",
|
| 220 |
+
f"📝 *Not:*",
|
| 221 |
+
additional_info
|
| 222 |
+
])
|
| 223 |
+
|
| 224 |
+
# Hızlı aksiyon önerileri
|
| 225 |
+
if action == "reserve":
|
| 226 |
+
message_parts.extend([
|
| 227 |
+
"",
|
| 228 |
+
"✅ *Yapılacaklar:*",
|
| 229 |
+
"1. Ürün stok kontrolü",
|
| 230 |
+
"2. Müşteriyi arayın",
|
| 231 |
+
"3. Ödeme/teslimat planı"
|
| 232 |
+
])
|
| 233 |
+
elif action == "price":
|
| 234 |
+
message_parts.extend([
|
| 235 |
+
"",
|
| 236 |
+
"💡 *Müşteriye güncel fiyat bildirin*"
|
| 237 |
+
])
|
| 238 |
+
elif action == "stock":
|
| 239 |
+
message_parts.extend([
|
| 240 |
+
"",
|
| 241 |
+
"💡 *Stok durumunu kontrol edip bildirin*"
|
| 242 |
+
])
|
| 243 |
+
|
| 244 |
+
message_parts.extend([
|
| 245 |
+
"",
|
| 246 |
+
"---",
|
| 247 |
+
"Trek WhatsApp Bot",
|
| 248 |
+
"📍 Alsancak Mağaza - Mehmet Bey"
|
| 249 |
+
])
|
| 250 |
+
|
| 251 |
+
return "\n".join(message_parts)
|
| 252 |
+
|
| 253 |
+
def send_test_notification() -> bool:
|
| 254 |
+
"""
|
| 255 |
+
Test bildirimi gönder
|
| 256 |
+
"""
|
| 257 |
+
return send_store_notification(
|
| 258 |
+
customer_phone="whatsapp:+905551234567",
|
| 259 |
+
customer_name="Test Müşteri",
|
| 260 |
+
product_name="FX 2 (Kırmızı, L Beden)",
|
| 261 |
+
action="test",
|
| 262 |
+
store_name="merkez",
|
| 263 |
+
additional_info="Bu bir test bildirimidir. Sistem çalışıyor."
|
| 264 |
+
)
|
| 265 |
+
|
| 266 |
+
# Kullanım örnekleri
|
| 267 |
+
def notify_product_reservation(customer_phone: str, product_name: str, store: Optional[str] = None):
|
| 268 |
+
"""Ürün ayırtma bildirimi"""
|
| 269 |
+
return send_store_notification(
|
| 270 |
+
customer_phone=customer_phone,
|
| 271 |
+
customer_name=None,
|
| 272 |
+
product_name=product_name,
|
| 273 |
+
action="reserve",
|
| 274 |
+
store_name=store,
|
| 275 |
+
additional_info="Müşteri bu ürünü ayırtmak istiyor. Lütfen iletişime geçin."
|
| 276 |
+
)
|
| 277 |
+
|
| 278 |
+
def notify_price_inquiry(customer_phone: str, product_name: str):
|
| 279 |
+
"""Fiyat sorusu bildirimi"""
|
| 280 |
+
return send_store_notification(
|
| 281 |
+
customer_phone=customer_phone,
|
| 282 |
+
customer_name=None,
|
| 283 |
+
product_name=product_name,
|
| 284 |
+
action="price",
|
| 285 |
+
additional_info="Müşteri güncel fiyat bilgisi istiyor."
|
| 286 |
+
)
|
| 287 |
+
|
| 288 |
+
def notify_stock_inquiry(customer_phone: str, product_name: str, store: Optional[str] = None):
|
| 289 |
+
"""Stok sorusu bildirimi"""
|
| 290 |
+
return send_store_notification(
|
| 291 |
+
customer_phone=customer_phone,
|
| 292 |
+
customer_name=None,
|
| 293 |
+
product_name=product_name,
|
| 294 |
+
action="stock",
|
| 295 |
+
store_name=store,
|
| 296 |
+
additional_info="Müşteri stok durumunu soruyor."
|
| 297 |
+
)
|
| 298 |
+
|
| 299 |
+
def should_notify_mehmet_bey(user_message: str, intent_analysis: Optional[Dict] = None) -> tuple[bool, str, str]:
|
| 300 |
+
"""
|
| 301 |
+
Mehmet Bey'e bildirim gönderilmeli mi kontrol et
|
| 302 |
+
|
| 303 |
+
Args:
|
| 304 |
+
user_message: Müşteri mesajı
|
| 305 |
+
intent_analysis: GPT-5 intent analiz sonucu (opsiyonel)
|
| 306 |
+
|
| 307 |
+
Returns:
|
| 308 |
+
(should_notify: bool, reason: str, urgency: "high"/"medium"/"low")
|
| 309 |
+
"""
|
| 310 |
+
|
| 311 |
+
message_lower = user_message.lower()
|
| 312 |
+
|
| 313 |
+
# 1. REZERVASYON/AYIRTMA TALEPLERI (Yüksek öncelik)
|
| 314 |
+
reservation_keywords = [
|
| 315 |
+
# Direkt ayırtma istekleri
|
| 316 |
+
'ayırt', 'ayırın', 'ayırtın', 'ayırtabilir', 'ayırır mısınız',
|
| 317 |
+
'rezerve', 'rezervasyon', 'rezarvasyon',
|
| 318 |
+
'tutun', 'tutar mısınız', 'tutabilir', 'tutarsanız', 'tuttun mu',
|
| 319 |
+
'sakla', 'saklar mısınız', 'saklayın', 'saklayabilir',
|
| 320 |
+
'kenara koy', 'kenara ayır', 'benim için koy', 'bana ayır',
|
| 321 |
+
|
| 322 |
+
# Kesin alım niyeti
|
| 323 |
+
'alacağım', 'alıcam', 'alıyorum', 'alcam', 'alırım',
|
| 324 |
+
'geliyorum', 'gelcem', 'gelicem', 'geliyom', 'geleceğim',
|
| 325 |
+
'yola çıktım', 'yoldayım', 'yola çıkıyorum', 'yola çıkıcam',
|
| 326 |
+
'birazdan oradayım', 'yarım saate', '1 saate', 'bir saate',
|
| 327 |
+
'30 dakika', '45 dakika', 'akşama kadar',
|
| 328 |
+
|
| 329 |
+
# Alım planı belirtenler
|
| 330 |
+
'bugün alabilir', 'yarın alabilir', 'hafta sonu', 'haftasonu',
|
| 331 |
+
'akşam uğra', 'öğleden sonra gel', 'sabah gel', 'öğlen gel',
|
| 332 |
+
'eşime danış', 'eşimle konuş', 'eşimi getir', 'eşimle gel',
|
| 333 |
+
'kesin alıcıyım', 'kesin istiyorum', 'bunu istiyorum', 'bunu alıyorum',
|
| 334 |
+
'kartımı unuttum', 'nakit getir', 'para çek', 'atm',
|
| 335 |
+
'maaş yattı', 'maaşım yatınca', 'ay başı', 'aybaşı'
|
| 336 |
+
]
|
| 337 |
+
|
| 338 |
+
# 2. MAĞAZA LOKASYON SORULARI (Orta öncelik)
|
| 339 |
+
store_keywords = [
|
| 340 |
+
'hangi mağaza', 'nerede var', 'nereden alabilirim', 'nerden',
|
| 341 |
+
'caddebostan', 'ortaköy', 'ortakoy', 'alsancak', 'bahçeköy', 'bahcekoy',
|
| 342 |
+
'en yakın', 'bana yakın', 'yakın mağaza', 'yakınımda',
|
| 343 |
+
'mağazada görebilir', 'mağazaya gel', 'mağazanıza', 'mağazanızda',
|
| 344 |
+
'test edebilir', 'deneyebilir', 'binebilir', 'test sürüş',
|
| 345 |
+
'yerinde gör', 'canlı gör', 'fiziksel', 'gerçeğini gör',
|
| 346 |
+
'adres', 'konum', 'lokasyon', 'neredesiniz', 'harita',
|
| 347 |
+
'uzak mı', 'yakın mı', 'kaç km', 'nasıl gidilir'
|
| 348 |
+
]
|
| 349 |
+
|
| 350 |
+
# 3. FİYAT/ÖDEME PAZARLIĞI (Orta öncelik)
|
| 351 |
+
price_keywords = [
|
| 352 |
+
'indirim', 'kampanya', 'promosyon', 'fırsat', 'özel fiyat',
|
| 353 |
+
'taksit', 'kredi kartı', 'banka', 'ödeme seçenek', 'vadeli',
|
| 354 |
+
'peşin öde', 'nakit indirim', 'peşinat', 'kaparo', 'depozito',
|
| 355 |
+
'pazarlık', 'son fiyat', 'en iyi fiyat', 'net fiyat', 'kdv dahil',
|
| 356 |
+
'öğrenci indirimi', 'personel indirimi', 'kurumsal', 'toplu alım',
|
| 357 |
+
'takas', 'eski bisiklet', 'değer', '2.el', 'ikinci el',
|
| 358 |
+
'daha ucuz', 'daha uygun', 'bütçe', 'pahalı', 'ucuzlat',
|
| 359 |
+
'fiyat düş', 'fiyat kır', 'esnet', 'yardımcı ol'
|
| 360 |
+
]
|
| 361 |
+
|
| 362 |
+
# Önce rezervasyon kontrolü (en yüksek öncelik)
|
| 363 |
+
for keyword in reservation_keywords:
|
| 364 |
+
if keyword in message_lower:
|
| 365 |
+
return True, f"🔴 Rezervasyon talebi: '{keyword}' kelimesi tespit edildi", "high"
|
| 366 |
+
|
| 367 |
+
# Sonra mağaza soruları
|
| 368 |
+
for keyword in store_keywords:
|
| 369 |
+
if keyword in message_lower:
|
| 370 |
+
return True, f"📍 Mağaza/lokasyon sorusu: '{keyword}' kelimesi tespit edildi", "medium"
|
| 371 |
+
|
| 372 |
+
# Fiyat/ödeme konuları
|
| 373 |
+
for keyword in price_keywords:
|
| 374 |
+
if keyword in message_lower:
|
| 375 |
+
return True, f"💰 Fiyat/ödeme görüşmesi: '{keyword}' kelimesi tespit edildi", "medium"
|
| 376 |
+
|
| 377 |
+
# Intent analysis'ten ek kontrol (eğer varsa)
|
| 378 |
+
if intent_analysis:
|
| 379 |
+
# GPT-5 yüksek aciliyet tespit ettiyse
|
| 380 |
+
if intent_analysis.get('urgency') == 'high':
|
| 381 |
+
return True, "🚨 GPT-5 yüksek aciliyet tespit etti", "high"
|
| 382 |
+
|
| 383 |
+
# GPT-5 rezervasyon niyeti tespit ettiyse
|
| 384 |
+
if 'reserve' in intent_analysis.get('intents', []):
|
| 385 |
+
return True, "📌 GPT-5 rezervasyon niyeti tespit etti", "high"
|
| 386 |
+
|
| 387 |
+
# Hiçbir koşul sağlanmadıysa bildirim gönderme
|
| 388 |
+
return False, "", "low"
|
| 389 |
+
|
| 390 |
+
if __name__ == "__main__":
|
| 391 |
+
# Test
|
| 392 |
+
print("Mağaza bildirim sistemi test ediliyor...")
|
| 393 |
+
result = send_test_notification()
|
| 394 |
+
if result:
|
| 395 |
+
print("✅ Test bildirimi başarıyla gönderildi!")
|
| 396 |
+
else:
|
| 397 |
+
print("❌ Test bildirimi gönderilemedi!")
|
test_actual_function.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Test actual get_product_price_and_link function"""
|
| 3 |
+
|
| 4 |
+
from smart_warehouse_with_price import get_product_price_and_link
|
| 5 |
+
|
| 6 |
+
# Test with debug prints
|
| 7 |
+
def test_with_debug():
|
| 8 |
+
print("Testing get_product_price_and_link('Marlin 5')...")
|
| 9 |
+
|
| 10 |
+
# Temporarily modify the function to add debug
|
| 11 |
+
import smart_warehouse_with_price
|
| 12 |
+
import xml.etree.ElementTree as ET
|
| 13 |
+
import re
|
| 14 |
+
|
| 15 |
+
# Get the XML
|
| 16 |
+
xml_content = smart_warehouse_with_price.get_cached_trek_xml()
|
| 17 |
+
if not xml_content:
|
| 18 |
+
print("No XML content")
|
| 19 |
+
return
|
| 20 |
+
|
| 21 |
+
root = ET.fromstring(xml_content)
|
| 22 |
+
|
| 23 |
+
search_name = "marlin 5"
|
| 24 |
+
clean_search = re.sub(r'\s*\(\d{4}\)\s*', '', search_name).strip()
|
| 25 |
+
|
| 26 |
+
print(f"Clean search: '{clean_search}'")
|
| 27 |
+
|
| 28 |
+
best_match = None
|
| 29 |
+
best_score = 0
|
| 30 |
+
best_name = None
|
| 31 |
+
|
| 32 |
+
count = 0
|
| 33 |
+
for item in root.findall('item'):
|
| 34 |
+
rootlabel_elem = item.find('rootlabel')
|
| 35 |
+
if rootlabel_elem is None or not rootlabel_elem.text:
|
| 36 |
+
continue
|
| 37 |
+
|
| 38 |
+
item_name = rootlabel_elem.text.lower()
|
| 39 |
+
|
| 40 |
+
# Turkish char normalization
|
| 41 |
+
tr_map = {'ı': 'i', 'ğ': 'g', 'ü': 'u', 'ş': 's', 'ö': 'o', 'ç': 'c'}
|
| 42 |
+
for tr, en in tr_map.items():
|
| 43 |
+
item_name = item_name.replace(tr, en)
|
| 44 |
+
|
| 45 |
+
clean_item = re.sub(r'\s*\(\d{4}\)\s*', '', item_name).strip()
|
| 46 |
+
|
| 47 |
+
score = 0
|
| 48 |
+
|
| 49 |
+
# Exact match
|
| 50 |
+
if clean_search == clean_item:
|
| 51 |
+
score += 100
|
| 52 |
+
elif clean_item.startswith(clean_search + " ") or clean_item == clean_search:
|
| 53 |
+
score += 50
|
| 54 |
+
else:
|
| 55 |
+
name_parts = clean_search.split()
|
| 56 |
+
for part in name_parts:
|
| 57 |
+
if part in clean_item:
|
| 58 |
+
score += 1
|
| 59 |
+
|
| 60 |
+
if score > 0:
|
| 61 |
+
count += 1
|
| 62 |
+
if count <= 10: # Show first 10 matches
|
| 63 |
+
print(f" {count}. {rootlabel_elem.text[:50]}: score={score}")
|
| 64 |
+
|
| 65 |
+
if score > best_score:
|
| 66 |
+
best_score = score
|
| 67 |
+
best_match = item
|
| 68 |
+
best_name = rootlabel_elem.text
|
| 69 |
+
print(f" >>> NEW BEST: {best_name} with score {best_score}")
|
| 70 |
+
|
| 71 |
+
print(f"\nFinal best match: {best_name}")
|
| 72 |
+
print(f"Best score: {best_score}")
|
| 73 |
+
|
| 74 |
+
if best_match is not None:
|
| 75 |
+
price_elem = best_match.find('priceTaxWithCur')
|
| 76 |
+
link_elem = best_match.find('productLink')
|
| 77 |
+
|
| 78 |
+
if price_elem is not None:
|
| 79 |
+
print(f"Price: {price_elem.text}")
|
| 80 |
+
if link_elem is not None:
|
| 81 |
+
print(f"Link: {link_elem.text}")
|
| 82 |
+
|
| 83 |
+
# Now test actual function
|
| 84 |
+
print("\n" + "="*60)
|
| 85 |
+
print("Actual function result:")
|
| 86 |
+
price, link = get_product_price_and_link("Marlin 5")
|
| 87 |
+
print(f"Price: {price}")
|
| 88 |
+
print(f"Link: {link}")
|
| 89 |
+
|
| 90 |
+
if __name__ == "__main__":
|
| 91 |
+
test_with_debug()
|
test_context_aware.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# Test context-aware variant search
|
| 3 |
+
|
| 4 |
+
import sys
|
| 5 |
+
import os
|
| 6 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 7 |
+
|
| 8 |
+
# Import the updated function from app.py
|
| 9 |
+
from app import get_warehouse_stock
|
| 10 |
+
|
| 11 |
+
if __name__ == "__main__":
|
| 12 |
+
test_cases = [
|
| 13 |
+
"M Turuncu", # Should find all M Turuncu variants
|
| 14 |
+
"Marlin 6 M Turuncu", # Should find only Marlin 6 M Turuncu variants
|
| 15 |
+
"Marlin M Turuncu", # Should find only Marlin M Turuncu variants
|
| 16 |
+
"L Siyah", # Should find all L Siyah variants
|
| 17 |
+
"Marlin 6 L Siyah" # Should find only Marlin 6 L Siyah variants
|
| 18 |
+
]
|
| 19 |
+
|
| 20 |
+
for test_case in test_cases:
|
| 21 |
+
print(f"\n=== Testing: {test_case} ===")
|
| 22 |
+
try:
|
| 23 |
+
result = get_warehouse_stock(test_case)
|
| 24 |
+
if result:
|
| 25 |
+
print("Sonuç:")
|
| 26 |
+
total_stock = 0
|
| 27 |
+
for item in result:
|
| 28 |
+
print(f" • {item}")
|
| 29 |
+
# Extract stock count for total
|
| 30 |
+
if ": " in item and " adet" in item:
|
| 31 |
+
stock_part = item.split(": ")[1].replace(" adet", "")
|
| 32 |
+
try:
|
| 33 |
+
total_stock += int(stock_part)
|
| 34 |
+
except:
|
| 35 |
+
pass
|
| 36 |
+
print(f"TOPLAM: {total_stock} adet")
|
| 37 |
+
else:
|
| 38 |
+
print("Sonuç bulunamadı")
|
| 39 |
+
except Exception as e:
|
| 40 |
+
print(f"Hata: {e}")
|
| 41 |
+
print("-" * 50)
|
test_crm.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
CRM System Test Script
|
| 4 |
+
"""
|
| 5 |
+
from customer_manager import CustomerManager
|
| 6 |
+
|
| 7 |
+
# Test CRM
|
| 8 |
+
print("🧪 CRM Test Başlatılıyor...")
|
| 9 |
+
print("="*50)
|
| 10 |
+
|
| 11 |
+
# Initialize CRM
|
| 12 |
+
crm = CustomerManager()
|
| 13 |
+
|
| 14 |
+
# Test 1: Yeni müşteri oluşturma
|
| 15 |
+
print("\n1️⃣ Yeni müşteri test ediliyor...")
|
| 16 |
+
customer1 = crm.get_or_create_customer("+905551234567", "Ahmet")
|
| 17 |
+
print(f" ✅ Müşteri oluşturuldu: {customer1['name']} - {customer1['segment']}")
|
| 18 |
+
|
| 19 |
+
# Test 2: İkinci mesaj - selamlama
|
| 20 |
+
print("\n2️⃣ Selamlama mesajı testi...")
|
| 21 |
+
context = crm.get_customer_context("+905551234567")
|
| 22 |
+
print(f" Selamlama: {context['greeting']}")
|
| 23 |
+
|
| 24 |
+
# Test 3: Ürün araması güncelleme
|
| 25 |
+
print("\n3️⃣ Ürün araması güncelleme testi...")
|
| 26 |
+
crm.update_interaction("+905551234567", "Marlin 5 var mı?", "Marlin 5")
|
| 27 |
+
customer1 = crm.customers["+905551234567"]
|
| 28 |
+
print(f" İlgi alanları: {customer1['interests']}")
|
| 29 |
+
print(f" Toplam sorgu: {customer1['total_queries']}")
|
| 30 |
+
|
| 31 |
+
# Test 4: Başka bir müşteri - VIP simülasyonu
|
| 32 |
+
print("\n4️⃣ VIP müşteri testi...")
|
| 33 |
+
customer2 = crm.get_or_create_customer("+905559876543", "Mehmet")
|
| 34 |
+
# Birkaç satın alma ekle
|
| 35 |
+
crm.add_purchase("+905559876543", "Trek Madone SLR 9", 180000)
|
| 36 |
+
crm.add_purchase("+905559876543", "Trek Domane SL 7", 95000)
|
| 37 |
+
crm.add_purchase("+905559876543", "Trek FX Sport 6", 45000)
|
| 38 |
+
customer2 = crm.customers["+905559876543"]
|
| 39 |
+
print(f" ✅ VIP Müşteri: {customer2['name']} - {customer2['segment']}")
|
| 40 |
+
context2 = crm.get_customer_context("+905559876543")
|
| 41 |
+
print(f" VIP Selamlama: {context2['greeting']}")
|
| 42 |
+
|
| 43 |
+
# Test 5: Analitik
|
| 44 |
+
print("\n5️⃣ Analitik raporu...")
|
| 45 |
+
analytics = crm.get_analytics()
|
| 46 |
+
print(f" Toplam müşteri: {analytics['total_customers']}")
|
| 47 |
+
print(f" Segment dağılımı: {analytics['segments']}")
|
| 48 |
+
print(f" Toplam satış: {analytics['total_purchases']}")
|
| 49 |
+
print(f" Toplam gelir: ₺{analytics['total_revenue']:,.0f}")
|
| 50 |
+
|
| 51 |
+
# Test 6: Müşteri listesi
|
| 52 |
+
print("\n6️⃣ Müşteri listesi...")
|
| 53 |
+
customers = crm.get_customer_list()
|
| 54 |
+
for customer in customers:
|
| 55 |
+
print(f" - {customer['name']} ({customer['phone']}) - {customer['segment']} - {customer['total_queries']} sorgu")
|
| 56 |
+
|
| 57 |
+
print("\n" + "="*50)
|
| 58 |
+
print("✅ CRM Testleri Tamamlandı!")
|
| 59 |
+
print("\n📊 Dashboard'u görmek için:")
|
| 60 |
+
print(" http://localhost:8000/crm-analytics")
|
test_direct_madone.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Direct test without GPT-5 to see what's in the data"""
|
| 3 |
+
|
| 4 |
+
import sys
|
| 5 |
+
import os
|
| 6 |
+
sys.path.insert(0, '/home/samikoen/bf_wab_space')
|
| 7 |
+
|
| 8 |
+
import requests
|
| 9 |
+
import re
|
| 10 |
+
import json
|
| 11 |
+
|
| 12 |
+
# Get warehouse XML
|
| 13 |
+
print("Fetching warehouse XML...")
|
| 14 |
+
url = 'https://video.trek-turkey.com/bizimhesap-warehouse-xml-b2b-api-v2.php'
|
| 15 |
+
response = requests.get(url, verify=False, timeout=15)
|
| 16 |
+
xml_text = response.text
|
| 17 |
+
|
| 18 |
+
# Extract products
|
| 19 |
+
product_pattern = r'<Product>(.*?)</Product>'
|
| 20 |
+
all_products = re.findall(product_pattern, xml_text, re.DOTALL)
|
| 21 |
+
|
| 22 |
+
print(f"Total products: {len(all_products)}")
|
| 23 |
+
|
| 24 |
+
# Create products_summary EXACTLY like the function
|
| 25 |
+
products_summary = []
|
| 26 |
+
for i, product_block in enumerate(all_products):
|
| 27 |
+
name_match = re.search(r'<ProductName><!\[CDATA\[(.*?)\]\]></ProductName>', product_block)
|
| 28 |
+
variant_match = re.search(r'<ProductVariant><!\[CDATA\[(.*?)\]\]></ProductVariant>', product_block)
|
| 29 |
+
|
| 30 |
+
if name_match:
|
| 31 |
+
warehouses_with_stock = []
|
| 32 |
+
warehouse_regex = r'<Warehouse>.*?<Name><!\[CDATA\[(.*?)\]\]></Name>.*?<Stock>(.*?)</Stock>.*?</Warehouse>'
|
| 33 |
+
warehouses = re.findall(warehouse_regex, product_block, re.DOTALL)
|
| 34 |
+
|
| 35 |
+
for wh_name, wh_stock in warehouses:
|
| 36 |
+
try:
|
| 37 |
+
if int(wh_stock.strip()) > 0:
|
| 38 |
+
warehouses_with_stock.append(wh_name)
|
| 39 |
+
except:
|
| 40 |
+
pass
|
| 41 |
+
|
| 42 |
+
product_info = {
|
| 43 |
+
"index": i,
|
| 44 |
+
"name": name_match.group(1),
|
| 45 |
+
"variant": variant_match.group(1) if variant_match else "",
|
| 46 |
+
"warehouses": warehouses_with_stock
|
| 47 |
+
}
|
| 48 |
+
products_summary.append(product_info)
|
| 49 |
+
|
| 50 |
+
print(f"Products in summary: {len(products_summary)}")
|
| 51 |
+
|
| 52 |
+
# Search for MADONE and MARLIN
|
| 53 |
+
print("\n" + "="*60)
|
| 54 |
+
print("SEARCHING FOR MADONE SL 6:")
|
| 55 |
+
print("="*60)
|
| 56 |
+
|
| 57 |
+
madone_found = []
|
| 58 |
+
for p in products_summary:
|
| 59 |
+
if "MADONE SL 6" in p['name'].upper():
|
| 60 |
+
madone_found.append(p)
|
| 61 |
+
|
| 62 |
+
if madone_found:
|
| 63 |
+
print(f"✅ Found {len(madone_found)} MADONE SL 6 products:")
|
| 64 |
+
for p in madone_found:
|
| 65 |
+
if p['warehouses']: # Only show products with stock
|
| 66 |
+
print(f" Index {p['index']}: {p['name']} - {p['variant']}")
|
| 67 |
+
print(f" Stock in: {', '.join(p['warehouses'])}")
|
| 68 |
+
else:
|
| 69 |
+
print("❌ No MADONE SL 6 found!")
|
| 70 |
+
|
| 71 |
+
print("\n" + "="*60)
|
| 72 |
+
print("SEARCHING FOR MARLIN:")
|
| 73 |
+
print("="*60)
|
| 74 |
+
|
| 75 |
+
marlin_found = []
|
| 76 |
+
for p in products_summary:
|
| 77 |
+
if "MARLIN" in p['name'].upper():
|
| 78 |
+
marlin_found.append(p)
|
| 79 |
+
|
| 80 |
+
if marlin_found:
|
| 81 |
+
print(f"Found {len(marlin_found)} MARLIN products")
|
| 82 |
+
print("First 3 MARLIN products with stock:")
|
| 83 |
+
count = 0
|
| 84 |
+
for p in marlin_found:
|
| 85 |
+
if p['warehouses'] and count < 3:
|
| 86 |
+
print(f" Index {p['index']}: {p['name']} - {p['variant']}")
|
| 87 |
+
print(f" Stock in: {', '.join(p['warehouses'])}")
|
| 88 |
+
count += 1
|
| 89 |
+
|
| 90 |
+
# Check what GPT-5 would receive
|
| 91 |
+
print("\n" + "="*60)
|
| 92 |
+
print("WHAT GPT-5 RECEIVES:")
|
| 93 |
+
print("="*60)
|
| 94 |
+
print(f"Total products sent to GPT-5: {len(products_summary)}")
|
| 95 |
+
|
| 96 |
+
# Check indices around MADONE
|
| 97 |
+
print("\nProducts at indices 460-475:")
|
| 98 |
+
for idx in range(460, 476):
|
| 99 |
+
for p in products_summary:
|
| 100 |
+
if p['index'] == idx:
|
| 101 |
+
has_stock = "✅" if p['warehouses'] else "❌"
|
| 102 |
+
print(f" {idx} {has_stock}: {p['name'][:30]}...")
|
| 103 |
+
break
|
| 104 |
+
|
| 105 |
+
# Save the exact data GPT-5 receives
|
| 106 |
+
output_file = '/home/samikoen/gpt5_exact_input.json'
|
| 107 |
+
with open(output_file, 'w', encoding='utf-8') as f:
|
| 108 |
+
json.dump(products_summary, f, ensure_ascii=False, indent=2)
|
| 109 |
+
print(f"\n💾 Saved exact GPT-5 input to: {output_file}")
|
| 110 |
+
|
| 111 |
+
# Show what indices GPT-5 should return for "madone sl 6"
|
| 112 |
+
madone_indices = [str(p['index']) for p in madone_found if p['warehouses']]
|
| 113 |
+
if madone_indices:
|
| 114 |
+
print(f"\n🎯 GPT-5 SHOULD RETURN: {','.join(madone_indices)}")
|
| 115 |
+
else:
|
| 116 |
+
print("\n⚠️ No MADONE SL 6 with stock found!")
|
test_final.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# Final test with updated get_warehouse_stock function from app.py
|
| 3 |
+
|
| 4 |
+
import sys
|
| 5 |
+
import os
|
| 6 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 7 |
+
|
| 8 |
+
# Import the updated function from app.py
|
| 9 |
+
from app import get_warehouse_stock
|
| 10 |
+
|
| 11 |
+
if __name__ == "__main__":
|
| 12 |
+
test_cases = [
|
| 13 |
+
"M Turuncu",
|
| 14 |
+
"Marlin 6 M Turuncu",
|
| 15 |
+
"L Siyah",
|
| 16 |
+
"S Beyaz"
|
| 17 |
+
]
|
| 18 |
+
|
| 19 |
+
for test_case in test_cases:
|
| 20 |
+
print(f"\n=== Testing: {test_case} ===")
|
| 21 |
+
try:
|
| 22 |
+
result = get_warehouse_stock(test_case)
|
| 23 |
+
if result:
|
| 24 |
+
print("Sonuç:")
|
| 25 |
+
for item in result:
|
| 26 |
+
print(f" • {item}")
|
| 27 |
+
else:
|
| 28 |
+
print("Sonuç bulunamadı")
|
| 29 |
+
except Exception as e:
|
| 30 |
+
print(f"Hata: {e}")
|
| 31 |
+
print("-" * 50)
|
test_gpt5_mock.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Mock GPT-5 to see what it should return"""
|
| 3 |
+
|
| 4 |
+
import json
|
| 5 |
+
|
| 6 |
+
# Load the exact data GPT-5 receives
|
| 7 |
+
with open('/home/samikoen/gpt5_exact_input.json', 'r', encoding='utf-8') as f:
|
| 8 |
+
products_summary = json.load(f)
|
| 9 |
+
|
| 10 |
+
print(f"Total products GPT-5 sees: {len(products_summary)}")
|
| 11 |
+
|
| 12 |
+
# Simulate GPT-5 logic for "madone sl 6"
|
| 13 |
+
query = "madone sl 6"
|
| 14 |
+
query_upper = query.upper()
|
| 15 |
+
|
| 16 |
+
matching_indices = []
|
| 17 |
+
|
| 18 |
+
for product in products_summary:
|
| 19 |
+
product_name = product['name'].upper()
|
| 20 |
+
|
| 21 |
+
# GPT-5 should match "MADONE SL 6" in product name
|
| 22 |
+
if "MADONE SL 6" in product_name:
|
| 23 |
+
# Only return products with stock
|
| 24 |
+
if product['warehouses']:
|
| 25 |
+
matching_indices.append(str(product['index']))
|
| 26 |
+
|
| 27 |
+
print(f"\nQuery: '{query}'")
|
| 28 |
+
print(f"GPT-5 should return: {','.join(matching_indices)}")
|
| 29 |
+
|
| 30 |
+
# Check what's actually there
|
| 31 |
+
print("\nMatching products details:")
|
| 32 |
+
for product in products_summary:
|
| 33 |
+
if "MADONE SL 6" in product['name'].upper():
|
| 34 |
+
if product['warehouses']:
|
| 35 |
+
print(f" Index {product['index']}: {product['name']} ({product['variant']})")
|
| 36 |
+
print(f" Warehouses: {product['warehouses']}")
|
| 37 |
+
|
| 38 |
+
# Check if MARLIN is in the data
|
| 39 |
+
print("\n" + "="*60)
|
| 40 |
+
print("CHECKING FOR MARLIN:")
|
| 41 |
+
marlin_count = 0
|
| 42 |
+
for product in products_summary:
|
| 43 |
+
if "MARLIN" in product['name'].upper():
|
| 44 |
+
marlin_count += 1
|
| 45 |
+
if marlin_count <= 3 and product['warehouses']:
|
| 46 |
+
print(f" Index {product['index']}: {product['name']} ({product['variant']})")
|
| 47 |
+
|
| 48 |
+
if marlin_count == 0:
|
| 49 |
+
print(" ❌ NO MARLIN PRODUCTS IN WAREHOUSE DATA!")
|
| 50 |
+
else:
|
| 51 |
+
print(f" Total MARLIN products: {marlin_count}")
|
test_m_turuncu.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
import requests
|
| 3 |
+
import xml.etree.ElementTree as ET
|
| 4 |
+
|
| 5 |
+
def get_warehouse_stock(product_name):
|
| 6 |
+
"""B2B API'den mağaza stok bilgilerini çek - İyileştirilmiş versiyon"""
|
| 7 |
+
try:
|
| 8 |
+
import re
|
| 9 |
+
warehouse_url = 'https://video.trek-turkey.com/bizimhesap-warehouse-xml.php'
|
| 10 |
+
response = requests.get(warehouse_url, verify=False, timeout=15)
|
| 11 |
+
|
| 12 |
+
if response.status_code != 200:
|
| 13 |
+
return None
|
| 14 |
+
|
| 15 |
+
root = ET.fromstring(response.content)
|
| 16 |
+
|
| 17 |
+
# Turkish character normalization function
|
| 18 |
+
turkish_map = {'ı': 'i', 'ğ': 'g', 'ü': 'u', 'ş': 's', 'ö': 'o', 'ç': 'c', 'İ': 'i', 'I': 'i'}
|
| 19 |
+
|
| 20 |
+
def normalize_turkish(text):
|
| 21 |
+
import unicodedata
|
| 22 |
+
text = unicodedata.normalize('NFD', text)
|
| 23 |
+
text = ''.join(char for char in text if unicodedata.category(char) != 'Mn')
|
| 24 |
+
for tr_char, en_char in turkish_map.items():
|
| 25 |
+
text = text.replace(tr_char, en_char)
|
| 26 |
+
return text
|
| 27 |
+
|
| 28 |
+
# Normalize search product name
|
| 29 |
+
search_name = normalize_turkish(product_name.lower().strip())
|
| 30 |
+
search_name = search_name.replace('(2026)', '').replace('(2025)', '').replace(' gen 3', '').replace(' gen', '').strip()
|
| 31 |
+
search_words = search_name.split()
|
| 32 |
+
|
| 33 |
+
print(f"DEBUG: Aranan ürün: '{product_name}' -> normalize: '{search_name}' -> kelimeler: {search_words}")
|
| 34 |
+
|
| 35 |
+
best_matches = []
|
| 36 |
+
exact_matches = []
|
| 37 |
+
color_size_matches = [] # For size/color specific searches
|
| 38 |
+
|
| 39 |
+
# Check if this is a size/color query (like "M Turuncu")
|
| 40 |
+
is_size_color_query = len(search_words) <= 2 and any(word in ['s', 'm', 'l', 'xl', 'xs', 'small', 'medium', 'large', 'turuncu', 'siyah', 'beyaz', 'mavi', 'kirmizi', 'yesil'] for word in search_words)
|
| 41 |
+
|
| 42 |
+
if is_size_color_query:
|
| 43 |
+
print(f"DEBUG: Beden/renk araması algılandı: {search_words}")
|
| 44 |
+
# For size/color queries, look for products containing these terms
|
| 45 |
+
for product in root.findall('Product'):
|
| 46 |
+
product_name_elem = product.find('ProductName')
|
| 47 |
+
if product_name_elem is not None and product_name_elem.text:
|
| 48 |
+
xml_product_name = product_name_elem.text.strip()
|
| 49 |
+
normalized_xml = normalize_turkish(xml_product_name.lower())
|
| 50 |
+
|
| 51 |
+
# Check if all search words appear in the product name
|
| 52 |
+
if all(word in normalized_xml for word in search_words):
|
| 53 |
+
color_size_matches.append((product, xml_product_name, normalized_xml))
|
| 54 |
+
print(f"DEBUG: BEDEN/RENK EŞLEŞME: '{xml_product_name}'")
|
| 55 |
+
|
| 56 |
+
candidates = color_size_matches
|
| 57 |
+
else:
|
| 58 |
+
# Normal product search logic
|
| 59 |
+
# İlk geçiş: Tam eşleşmeleri bul
|
| 60 |
+
for product in root.findall('Product'):
|
| 61 |
+
product_name_elem = product.find('ProductName')
|
| 62 |
+
if product_name_elem is not None and product_name_elem.text:
|
| 63 |
+
xml_product_name = product_name_elem.text.strip()
|
| 64 |
+
normalized_xml = normalize_turkish(xml_product_name.lower())
|
| 65 |
+
normalized_xml = normalized_xml.replace('(2026)', '').replace('(2025)', '').replace(' gen 3', '').replace(' gen', '').strip()
|
| 66 |
+
xml_words = normalized_xml.split()
|
| 67 |
+
|
| 68 |
+
# Tam eşleşme kontrolü - ilk iki kelime tam aynı olmalı
|
| 69 |
+
if len(search_words) >= 2 and len(xml_words) >= 2:
|
| 70 |
+
search_key = f"{search_words[0]} {search_words[1]}"
|
| 71 |
+
xml_key = f"{xml_words[0]} {xml_words[1]}"
|
| 72 |
+
|
| 73 |
+
if search_key == xml_key:
|
| 74 |
+
exact_matches.append((product, xml_product_name, normalized_xml))
|
| 75 |
+
print(f"DEBUG: TAM EŞLEŞME: '{xml_product_name}'")
|
| 76 |
+
|
| 77 |
+
# Eğer tam eşleşme varsa, sadece onları kullan
|
| 78 |
+
candidates = exact_matches if exact_matches else []
|
| 79 |
+
|
| 80 |
+
# Eğer tam eşleşme yoksa, fuzzy matching yap
|
| 81 |
+
if not candidates:
|
| 82 |
+
print("DEBUG: Tam eşleşme yok, fuzzy matching yapılıyor...")
|
| 83 |
+
for product in root.findall('Product'):
|
| 84 |
+
product_name_elem = product.find('ProductName')
|
| 85 |
+
if product_name_elem is not None and product_name_elem.text:
|
| 86 |
+
xml_product_name = product_name_elem.text.strip()
|
| 87 |
+
normalized_xml = normalize_turkish(xml_product_name.lower())
|
| 88 |
+
normalized_xml = normalized_xml.replace('(2026)', '').replace('(2025)', '').replace(' gen 3', '').replace(' gen', '').strip()
|
| 89 |
+
xml_words = normalized_xml.split()
|
| 90 |
+
|
| 91 |
+
# Ortak kelime sayısını hesapla
|
| 92 |
+
common_words = set(search_words) & set(xml_words)
|
| 93 |
+
|
| 94 |
+
# En az 2 ortak kelime olmalı VE ilk kelime aynı olmalı (marka kontrolü)
|
| 95 |
+
if (len(common_words) >= 2 and
|
| 96 |
+
len(search_words) > 0 and len(xml_words) > 0 and
|
| 97 |
+
search_words[0] == xml_words[0]):
|
| 98 |
+
best_matches.append((product, xml_product_name, normalized_xml, len(common_words)))
|
| 99 |
+
print(f"DEBUG: FUZZY EŞLEŞME: '{xml_product_name}' (ortak: {len(common_words)})")
|
| 100 |
+
|
| 101 |
+
# En çok ortak kelimeye sahip olanları seç
|
| 102 |
+
if best_matches:
|
| 103 |
+
max_common = max(match[3] for match in best_matches)
|
| 104 |
+
candidates = [(match[0], match[1], match[2]) for match in best_matches if match[3] == max_common]
|
| 105 |
+
|
| 106 |
+
print(f"DEBUG: Toplam {len(candidates)} aday ürün bulundu")
|
| 107 |
+
|
| 108 |
+
# Stok bilgilerini topla ve tekrarları önle
|
| 109 |
+
warehouse_stock_map = {} # warehouse_name -> total_stock
|
| 110 |
+
|
| 111 |
+
for product, xml_name, _ in candidates:
|
| 112 |
+
warehouses = product.find('Warehouses')
|
| 113 |
+
if warehouses is not None:
|
| 114 |
+
for warehouse in warehouses.findall('Warehouse'):
|
| 115 |
+
name_elem = warehouse.find('Name')
|
| 116 |
+
stock_elem = warehouse.find('Stock')
|
| 117 |
+
|
| 118 |
+
if name_elem is not None and stock_elem is not None:
|
| 119 |
+
warehouse_name = name_elem.text if name_elem.text else "Bilinmeyen"
|
| 120 |
+
try:
|
| 121 |
+
stock_count = int(stock_elem.text) if stock_elem.text else 0
|
| 122 |
+
if stock_count > 0:
|
| 123 |
+
# Aynı mağaza için stokları topla
|
| 124 |
+
if warehouse_name in warehouse_stock_map:
|
| 125 |
+
warehouse_stock_map[warehouse_name] += stock_count
|
| 126 |
+
else:
|
| 127 |
+
warehouse_stock_map[warehouse_name] = stock_count
|
| 128 |
+
print(f"DEBUG: STOK BULUNDU - {warehouse_name}: {stock_count} adet ({xml_name})")
|
| 129 |
+
except (ValueError, TypeError):
|
| 130 |
+
pass
|
| 131 |
+
|
| 132 |
+
if warehouse_stock_map:
|
| 133 |
+
# Mağaza stoklarını liste halinde döndür
|
| 134 |
+
all_warehouse_info = []
|
| 135 |
+
for warehouse_name, total_stock in warehouse_stock_map.items():
|
| 136 |
+
all_warehouse_info.append(f"{warehouse_name}: {total_stock} adet")
|
| 137 |
+
return all_warehouse_info
|
| 138 |
+
else:
|
| 139 |
+
print("DEBUG: Hiçbir mağazada stok bulunamadı")
|
| 140 |
+
return ["Hiçbir mağazada stokta bulunmuyor"]
|
| 141 |
+
|
| 142 |
+
except Exception as e:
|
| 143 |
+
print(f"Mağaza stok bilgisi çekme hatası: {e}")
|
| 144 |
+
return None
|
| 145 |
+
|
| 146 |
+
if __name__ == "__main__":
|
| 147 |
+
# Test the function with different M Turuncu variations
|
| 148 |
+
test_cases = [
|
| 149 |
+
"M Turuncu",
|
| 150 |
+
"m turuncu",
|
| 151 |
+
"M TURUNCU",
|
| 152 |
+
"Medium Turuncu",
|
| 153 |
+
"Marlin 6 M Turuncu"
|
| 154 |
+
]
|
| 155 |
+
|
| 156 |
+
for test_case in test_cases:
|
| 157 |
+
print(f"=== Testing: {test_case} ===")
|
| 158 |
+
result = get_warehouse_stock(test_case)
|
| 159 |
+
print(f"Result: {result}")
|
| 160 |
+
print()
|
test_marlin_fix.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Test Marlin 5 after Turkish character fix"""
|
| 3 |
+
|
| 4 |
+
import os
|
| 5 |
+
os.environ['OPENAI_API_KEY'] = os.getenv('OPENAI_API_KEY', '')
|
| 6 |
+
|
| 7 |
+
from smart_warehouse_with_price import get_warehouse_stock_smart_with_price
|
| 8 |
+
|
| 9 |
+
def test_marlin():
|
| 10 |
+
print("Testing Marlin 5 search...")
|
| 11 |
+
print("="*60)
|
| 12 |
+
|
| 13 |
+
result = get_warehouse_stock_smart_with_price("Marlin 5")
|
| 14 |
+
|
| 15 |
+
if result:
|
| 16 |
+
print("\n✅ Results found:")
|
| 17 |
+
for item in result[:3]: # Show first 3
|
| 18 |
+
print(f"\n{item}")
|
| 19 |
+
else:
|
| 20 |
+
print("\n❌ No results")
|
| 21 |
+
|
| 22 |
+
if __name__ == "__main__":
|
| 23 |
+
test_marlin()
|
test_name_request.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Test Name Request Functionality
|
| 4 |
+
"""
|
| 5 |
+
from customer_manager import CustomerManager
|
| 6 |
+
import os
|
| 7 |
+
|
| 8 |
+
# Clean test - yeni database
|
| 9 |
+
if os.path.exists("test_customers.json"):
|
| 10 |
+
os.remove("test_customers.json")
|
| 11 |
+
|
| 12 |
+
crm = CustomerManager("test_customers.json")
|
| 13 |
+
|
| 14 |
+
print("🧪 İsim Sorma Test Senaryoları")
|
| 15 |
+
print("="*50)
|
| 16 |
+
|
| 17 |
+
# Test 1: Rezervasyon anında isim sorma
|
| 18 |
+
print("\n1️⃣ REZERVASYON ANINDA İSİM SORMA")
|
| 19 |
+
phone = "+905551111111"
|
| 20 |
+
message = "Marlin 5'i rezerve edebilir miyim?"
|
| 21 |
+
|
| 22 |
+
customer = crm.get_or_create_customer(phone)
|
| 23 |
+
should_ask, question = crm.should_ask_for_name(phone, message)
|
| 24 |
+
|
| 25 |
+
print(f"Müşteri: {message}")
|
| 26 |
+
print(f"İsim sorulmalı mı: {should_ask}")
|
| 27 |
+
if question:
|
| 28 |
+
print(f"Soru: {question}")
|
| 29 |
+
|
| 30 |
+
# Müşteri cevap veriyor
|
| 31 |
+
print("\nMüşteri yanıtı: 'Ahmet'")
|
| 32 |
+
extracted = crm.extract_name_from_response("Ahmet")
|
| 33 |
+
print(f"Çıkarılan isim: {extracted}")
|
| 34 |
+
|
| 35 |
+
# Test 2: 3. mesajda nazikçe sorma
|
| 36 |
+
print("\n2️⃣ 3. MESAJDA NAZİKÇE SORMA")
|
| 37 |
+
phone2 = "+905552222222"
|
| 38 |
+
|
| 39 |
+
# 1. mesaj
|
| 40 |
+
crm.update_interaction(phone2, "Merhaba", None)
|
| 41 |
+
should_ask, question = crm.should_ask_for_name(phone2, "Merhaba")
|
| 42 |
+
print(f"1. mesaj - İsim sorulmalı: {should_ask}")
|
| 43 |
+
|
| 44 |
+
# 2. mesaj
|
| 45 |
+
crm.update_interaction(phone2, "FX 3 var mı?", "FX 3")
|
| 46 |
+
should_ask, question = crm.should_ask_for_name(phone2, "FX 3 var mı?")
|
| 47 |
+
print(f"2. mesaj - İsim sorulmalı: {should_ask}")
|
| 48 |
+
|
| 49 |
+
# 3. mesaj
|
| 50 |
+
crm.update_interaction(phone2, "Fiyatı nedir?", None)
|
| 51 |
+
should_ask, question = crm.should_ask_for_name(phone2, "Fiyatı nedir?")
|
| 52 |
+
print(f"3. mesaj - İsim sorulmalı: {should_ask}")
|
| 53 |
+
if question:
|
| 54 |
+
print(f"Soru: {question}")
|
| 55 |
+
|
| 56 |
+
# Test 3: Farklı isim formatları
|
| 57 |
+
print("\n3️⃣ İSİM ÇIKARMA TESTLERİ")
|
| 58 |
+
test_responses = [
|
| 59 |
+
"Mehmet",
|
| 60 |
+
"benim adım Ali",
|
| 61 |
+
"Ben Ayşe",
|
| 62 |
+
"Adım Fatma",
|
| 63 |
+
"ismim Hasan",
|
| 64 |
+
"Veli, teşekkürler",
|
| 65 |
+
"Ahmet Yılmaz", # İsim + Soyisim
|
| 66 |
+
"selam" # İsim değil
|
| 67 |
+
]
|
| 68 |
+
|
| 69 |
+
for response in test_responses:
|
| 70 |
+
extracted = crm.extract_name_from_response(response)
|
| 71 |
+
print(f"'{response}' -> {extracted if extracted else 'İsim bulunamadı'}")
|
| 72 |
+
|
| 73 |
+
# Test 4: Mağaza ziyareti
|
| 74 |
+
print("\n4️⃣ MAĞAZA ZİYARETİ ANINDA")
|
| 75 |
+
phone3 = "+905553333333"
|
| 76 |
+
message = "Yarın mağazaya gelerek test sürüşü yapabilir miyim?"
|
| 77 |
+
should_ask, question = crm.should_ask_for_name(phone3, message)
|
| 78 |
+
print(f"Müşteri: {message}")
|
| 79 |
+
print(f"İsim sorulmalı: {should_ask}")
|
| 80 |
+
if question:
|
| 81 |
+
print(f"Soru: {question}")
|
| 82 |
+
|
| 83 |
+
# Test 5: Ödeme görüşmesi
|
| 84 |
+
print("\n5️⃣ ÖDEME GÖRÜŞMESİ ANINDA")
|
| 85 |
+
phone4 = "+905554444444"
|
| 86 |
+
message = "12 taksit yapıyor musunuz?"
|
| 87 |
+
should_ask, question = crm.should_ask_for_name(phone4, message)
|
| 88 |
+
print(f"Müşteri: {message}")
|
| 89 |
+
print(f"İsim sorulmalı: {should_ask}")
|
| 90 |
+
if question:
|
| 91 |
+
print(f"Soru: {question}")
|
| 92 |
+
|
| 93 |
+
# Test 6: 5+ sorgudan sonra
|
| 94 |
+
print("\n6️⃣ POTANSİYEL MÜŞTERİ (5+ SORGU)")
|
| 95 |
+
phone5 = "+905555555555"
|
| 96 |
+
for i in range(5):
|
| 97 |
+
crm.update_interaction(phone5, f"Soru {i+1}", None)
|
| 98 |
+
|
| 99 |
+
should_ask, question = crm.should_ask_for_name(phone5, "Başka model var mı?")
|
| 100 |
+
print(f"5+ sorgudan sonra")
|
| 101 |
+
print(f"İsim sorulmalı: {should_ask}")
|
| 102 |
+
if question:
|
| 103 |
+
print(f"Soru: {question}")
|
| 104 |
+
|
| 105 |
+
print("\n" + "="*50)
|
| 106 |
+
print("✅ Testler Tamamlandı!")
|
| 107 |
+
|
| 108 |
+
# Cleanup
|
| 109 |
+
if os.path.exists("test_customers.json"):
|
| 110 |
+
os.remove("test_customers.json")
|
test_simulate_whatsapp.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Simulate a WhatsApp message for MADONE SL 6"""
|
| 3 |
+
|
| 4 |
+
import sys
|
| 5 |
+
import os
|
| 6 |
+
sys.path.insert(0, '/home/samikoen/bf_wab_space')
|
| 7 |
+
|
| 8 |
+
# Set API key if exists
|
| 9 |
+
api_key = "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"
|
| 10 |
+
os.environ['OPENAI_API_KEY'] = api_key
|
| 11 |
+
|
| 12 |
+
print("="*60)
|
| 13 |
+
print("SIMULATING WHATSAPP MESSAGE: 'madone sl 6'")
|
| 14 |
+
print("="*60)
|
| 15 |
+
|
| 16 |
+
from smart_warehouse_complete import get_warehouse_stock_smart_complete
|
| 17 |
+
|
| 18 |
+
try:
|
| 19 |
+
print("\nCalling get_warehouse_stock_smart_complete('madone sl 6')...")
|
| 20 |
+
result = get_warehouse_stock_smart_complete("madone sl 6")
|
| 21 |
+
|
| 22 |
+
if result:
|
| 23 |
+
print("\n✅ RESULT:")
|
| 24 |
+
for line in result:
|
| 25 |
+
print(f" {line}")
|
| 26 |
+
else:
|
| 27 |
+
print("\n❌ No result (None returned)")
|
| 28 |
+
|
| 29 |
+
except Exception as e:
|
| 30 |
+
print(f"\n❌ ERROR: {e}")
|
| 31 |
+
import traceback
|
| 32 |
+
traceback.print_exc()
|
test_stock_consistency.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Test Stock Consistency Algorithm
|
| 4 |
+
Simulates conversation flow to ensure stock information remains consistent
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
# Test conversation memory functions
|
| 8 |
+
import sys
|
| 9 |
+
sys.path.insert(0, '/home/samikoen/bf_wab_space')
|
| 10 |
+
|
| 11 |
+
def test_stock_consistency():
|
| 12 |
+
print("🧪 STOK TUTARLILİK TESTİ")
|
| 13 |
+
print("="*50)
|
| 14 |
+
|
| 15 |
+
# Simulate phone number
|
| 16 |
+
test_phone = "+905551111111"
|
| 17 |
+
|
| 18 |
+
# Import the conversation functions
|
| 19 |
+
try:
|
| 20 |
+
from app import get_conversation_context, cache_product_info, get_cached_product_info
|
| 21 |
+
|
| 22 |
+
# Test 1: İlk ürün araması
|
| 23 |
+
print("\n1️⃣ İLK ÜRÜN ARAMASI")
|
| 24 |
+
search_term = "FX 3 var mı"
|
| 25 |
+
product_info = """📦 FX 3 (2024) - Şehir Bisikleti
|
| 26 |
+
💰 Fiyat: 73.000 TL
|
| 27 |
+
🏪 Stok: İzmir Alsancak, İstanbul Ortaköy
|
| 28 |
+
📏 L beden mevcut"""
|
| 29 |
+
|
| 30 |
+
# Cache product info
|
| 31 |
+
cache_product_info(test_phone, search_term, product_info)
|
| 32 |
+
print(f"✅ Cached: {search_term}")
|
| 33 |
+
print(f" Info: {product_info[:100]}...")
|
| 34 |
+
|
| 35 |
+
# Test 2: İkinci mesaj - benzer sorgu
|
| 36 |
+
print("\n2️⃣ İKINCİ MESAJ - BENZERİ SORGU")
|
| 37 |
+
follow_up = "boyum 190 cm L boy olur mu"
|
| 38 |
+
|
| 39 |
+
cached_result = get_cached_product_info(test_phone, follow_up)
|
| 40 |
+
if cached_result:
|
| 41 |
+
print(f"✅ CACHED bulundu: {follow_up}")
|
| 42 |
+
print(f" Cached data: {cached_result[:100]}...")
|
| 43 |
+
else:
|
| 44 |
+
print(f"❌ Cache BULUNAMADI: {follow_up}")
|
| 45 |
+
|
| 46 |
+
# Test 3: Üçüncü mesaj - aynı ürün hakkında
|
| 47 |
+
print("\n3️⃣ ÜÇÜNCÜ MESAJ - AYNI ÜRÜN")
|
| 48 |
+
third_msg = "ayırtmak istiyorum"
|
| 49 |
+
|
| 50 |
+
cached_result = get_cached_product_info(test_phone, third_msg)
|
| 51 |
+
if cached_result:
|
| 52 |
+
print(f"✅ CACHED bulundu: {third_msg}")
|
| 53 |
+
print(f" Cached data: {cached_result[:100]}...")
|
| 54 |
+
else:
|
| 55 |
+
print(f"❌ Cache BULUNAMADI: {third_msg}")
|
| 56 |
+
|
| 57 |
+
# Test 4: Conversation context
|
| 58 |
+
print("\n4️⃣ CONVERSATION CONTEXT")
|
| 59 |
+
context = get_conversation_context(test_phone)
|
| 60 |
+
print(f"Context keys: {list(context.keys())}")
|
| 61 |
+
print(f"Last search term: {context.get('last_search_term')}")
|
| 62 |
+
print(f"Has product info: {'Yes' if context.get('last_product_info') else 'No'}")
|
| 63 |
+
|
| 64 |
+
# Test 5: Farklı ürün araması
|
| 65 |
+
print("\n5️⃣ FARKLI ÜRÜN ARAMASI")
|
| 66 |
+
new_search = "Marlin 5 var mı"
|
| 67 |
+
|
| 68 |
+
cached_result = get_cached_product_info(test_phone, new_search)
|
| 69 |
+
if cached_result:
|
| 70 |
+
print(f"⚠️ Yanlış cache bulundu (beklenmeyen): {new_search}")
|
| 71 |
+
else:
|
| 72 |
+
print(f"✅ Doğru: Farklı ürün için cache yok: {new_search}")
|
| 73 |
+
|
| 74 |
+
# Test 6: Cache expiration (30 dakika sonrası simülasyonu)
|
| 75 |
+
print("\n6️⃣ CACHE EXPIRATION TEST")
|
| 76 |
+
import datetime
|
| 77 |
+
context = get_conversation_context(test_phone)
|
| 78 |
+
|
| 79 |
+
# Zamanı geriye al (31 dakika önce)
|
| 80 |
+
old_time = datetime.datetime.now() - datetime.timedelta(minutes=31)
|
| 81 |
+
context["last_search_time"] = old_time
|
| 82 |
+
|
| 83 |
+
cached_result = get_cached_product_info(test_phone, "FX 3 fiyat")
|
| 84 |
+
if cached_result:
|
| 85 |
+
print(f"❌ Cache expire olmadı (sorun)")
|
| 86 |
+
else:
|
| 87 |
+
print(f"✅ Cache doğru şekilde expire oldu")
|
| 88 |
+
|
| 89 |
+
print("\n" + "="*50)
|
| 90 |
+
print("✅ STOK TUTARLILİK TESTİ TAMAMLANDI!")
|
| 91 |
+
|
| 92 |
+
except ImportError as e:
|
| 93 |
+
print(f"❌ Import hatası: {e}")
|
| 94 |
+
print("Bu test app.py ile aynı dizinde çalışmalı")
|
| 95 |
+
|
| 96 |
+
if __name__ == "__main__":
|
| 97 |
+
test_stock_consistency()
|
test_updated_warehouse.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
import requests
|
| 3 |
+
import xml.etree.ElementTree as ET
|
| 4 |
+
|
| 5 |
+
def get_warehouse_stock(product_name):
|
| 6 |
+
"""B2B API'den mağaza stok bilgilerini çek - Final Updated Version"""
|
| 7 |
+
try:
|
| 8 |
+
import re
|
| 9 |
+
warehouse_url = 'https://video.trek-turkey.com/bizimhesap-warehouse-xml.php'
|
| 10 |
+
response = requests.get(warehouse_url, verify=False, timeout=15)
|
| 11 |
+
|
| 12 |
+
if response.status_code != 200:
|
| 13 |
+
return None
|
| 14 |
+
|
| 15 |
+
root = ET.fromstring(response.content)
|
| 16 |
+
|
| 17 |
+
# Turkish character normalization function
|
| 18 |
+
turkish_map = {'ı': 'i', 'ğ': 'g', 'ü': 'u', 'ş': 's', 'ö': 'o', 'ç': 'c', 'İ': 'i', 'I': 'i'}
|
| 19 |
+
|
| 20 |
+
def normalize_turkish(text):
|
| 21 |
+
import unicodedata
|
| 22 |
+
text = unicodedata.normalize('NFD', text)
|
| 23 |
+
text = ''.join(char for char in text if unicodedata.category(char) != 'Mn')
|
| 24 |
+
for tr_char, en_char in turkish_map.items():
|
| 25 |
+
text = text.replace(tr_char, en_char)
|
| 26 |
+
return text
|
| 27 |
+
|
| 28 |
+
# Normalize search product name
|
| 29 |
+
search_name = normalize_turkish(product_name.lower().strip())
|
| 30 |
+
search_name = search_name.replace('(2026)', '').replace('(2025)', '').replace(' gen 3', '').replace(' gen', '').strip()
|
| 31 |
+
search_words = search_name.split()
|
| 32 |
+
|
| 33 |
+
best_matches = []
|
| 34 |
+
exact_matches = []
|
| 35 |
+
variant_matches = []
|
| 36 |
+
candidates = []
|
| 37 |
+
|
| 38 |
+
# Check if this is a size/color specific query (like "M Turuncu")
|
| 39 |
+
is_size_color_query = (len(search_words) <= 3 and
|
| 40 |
+
any(word in ['s', 'm', 'l', 'xl', 'xs', 'small', 'medium', 'large',
|
| 41 |
+
'turuncu', 'siyah', 'beyaz', 'mavi', 'kirmizi', 'yesil',
|
| 42 |
+
'orange', 'black', 'white', 'blue', 'red', 'green']
|
| 43 |
+
for word in search_words))
|
| 44 |
+
|
| 45 |
+
# İlk geçiş: Variant alanında beden/renk araması
|
| 46 |
+
if is_size_color_query:
|
| 47 |
+
for product in root.findall('Product'):
|
| 48 |
+
product_name_elem = product.find('ProductName')
|
| 49 |
+
variant_elem = product.find('Variant')
|
| 50 |
+
|
| 51 |
+
if product_name_elem is not None and product_name_elem.text:
|
| 52 |
+
xml_product_name = product_name_elem.text.strip()
|
| 53 |
+
|
| 54 |
+
# Variant field check
|
| 55 |
+
if variant_elem is not None and variant_elem.text:
|
| 56 |
+
variant_text = normalize_turkish(variant_elem.text.lower().replace('-', ' '))
|
| 57 |
+
|
| 58 |
+
# Check if all search words are in variant field
|
| 59 |
+
if all(word in variant_text for word in search_words):
|
| 60 |
+
variant_matches.append((product, xml_product_name, variant_text))
|
| 61 |
+
|
| 62 |
+
if variant_matches:
|
| 63 |
+
candidates = variant_matches
|
| 64 |
+
else:
|
| 65 |
+
# Fallback to normal product name search
|
| 66 |
+
is_size_color_query = False
|
| 67 |
+
|
| 68 |
+
# İkinci geçiş: Normal ürün adı tam eşleşmeleri (variant match yoksa)
|
| 69 |
+
if not is_size_color_query or not candidates:
|
| 70 |
+
for product in root.findall('Product'):
|
| 71 |
+
product_name_elem = product.find('ProductName')
|
| 72 |
+
if product_name_elem is not None and product_name_elem.text:
|
| 73 |
+
xml_product_name = product_name_elem.text.strip()
|
| 74 |
+
normalized_xml = normalize_turkish(xml_product_name.lower())
|
| 75 |
+
normalized_xml = normalized_xml.replace('(2026)', '').replace('(2025)', '').replace(' gen 3', '').replace(' gen', '').strip()
|
| 76 |
+
xml_words = normalized_xml.split()
|
| 77 |
+
|
| 78 |
+
# Tam eşleşme kontrolü - ilk iki kelime tam aynı olmalı
|
| 79 |
+
if len(search_words) >= 2 and len(xml_words) >= 2:
|
| 80 |
+
search_key = f"{search_words[0]} {search_words[1]}"
|
| 81 |
+
xml_key = f"{xml_words[0]} {xml_words[1]}"
|
| 82 |
+
|
| 83 |
+
if search_key == xml_key:
|
| 84 |
+
exact_matches.append((product, xml_product_name, normalized_xml))
|
| 85 |
+
|
| 86 |
+
# Eğer variant match varsa onu kullan, yoksa exact matches kullan
|
| 87 |
+
if not candidates:
|
| 88 |
+
candidates = exact_matches if exact_matches else []
|
| 89 |
+
|
| 90 |
+
# Eğer hala bir match yoksa, fuzzy matching yap
|
| 91 |
+
if not candidates:
|
| 92 |
+
for product in root.findall('Product'):
|
| 93 |
+
product_name_elem = product.find('ProductName')
|
| 94 |
+
if product_name_elem is not None and product_name_elem.text:
|
| 95 |
+
xml_product_name = product_name_elem.text.strip()
|
| 96 |
+
normalized_xml = normalize_turkish(xml_product_name.lower())
|
| 97 |
+
normalized_xml = normalized_xml.replace('(2026)', '').replace('(2025)', '').replace(' gen 3', '').replace(' gen', '').strip()
|
| 98 |
+
xml_words = normalized_xml.split()
|
| 99 |
+
|
| 100 |
+
# Ortak kelime sayısını hesapla
|
| 101 |
+
common_words = set(search_words) & set(xml_words)
|
| 102 |
+
|
| 103 |
+
# En az 2 ortak kelime olmalı VE ilk kelime aynı olmalı (marka kontrolü)
|
| 104 |
+
if (len(common_words) >= 2 and
|
| 105 |
+
len(search_words) > 0 and len(xml_words) > 0 and
|
| 106 |
+
search_words[0] == xml_words[0]):
|
| 107 |
+
best_matches.append((product, xml_product_name, normalized_xml, len(common_words)))
|
| 108 |
+
|
| 109 |
+
# En çok ortak kelimeye sahip olanları seç
|
| 110 |
+
if best_matches:
|
| 111 |
+
max_common = max(match[3] for match in best_matches)
|
| 112 |
+
candidates = [(match[0], match[1], match[2]) for match in best_matches if match[3] == max_common]
|
| 113 |
+
|
| 114 |
+
# Stok bilgilerini topla ve tekrarları önle
|
| 115 |
+
warehouse_stock_map = {} # warehouse_name -> total_stock
|
| 116 |
+
|
| 117 |
+
for product, xml_name, _ in candidates:
|
| 118 |
+
warehouses = product.find('Warehouses')
|
| 119 |
+
if warehouses is not None:
|
| 120 |
+
for warehouse in warehouses.findall('Warehouse'):
|
| 121 |
+
name_elem = warehouse.find('Name')
|
| 122 |
+
stock_elem = warehouse.find('Stock')
|
| 123 |
+
|
| 124 |
+
if name_elem is not None and stock_elem is not None:
|
| 125 |
+
warehouse_name = name_elem.text if name_elem.text else "Bilinmeyen"
|
| 126 |
+
try:
|
| 127 |
+
stock_count = int(stock_elem.text) if stock_elem.text else 0
|
| 128 |
+
if stock_count > 0:
|
| 129 |
+
# Aynı mağaza için stokları topla
|
| 130 |
+
if warehouse_name in warehouse_stock_map:
|
| 131 |
+
warehouse_stock_map[warehouse_name] += stock_count
|
| 132 |
+
else:
|
| 133 |
+
warehouse_stock_map[warehouse_name] = stock_count
|
| 134 |
+
except (ValueError, TypeError):
|
| 135 |
+
pass
|
| 136 |
+
|
| 137 |
+
if warehouse_stock_map:
|
| 138 |
+
# Mağaza stoklarını liste halinde döndür
|
| 139 |
+
all_warehouse_info = []
|
| 140 |
+
for warehouse_name, total_stock in warehouse_stock_map.items():
|
| 141 |
+
all_warehouse_info.append(f"{warehouse_name}: {total_stock} adet")
|
| 142 |
+
return all_warehouse_info
|
| 143 |
+
else:
|
| 144 |
+
return ["Hiçbir mağazada stokta bulunmuyor"]
|
| 145 |
+
|
| 146 |
+
except Exception as e:
|
| 147 |
+
print(f"Mağaza stok bilgisi çekme hatası: {e}")
|
| 148 |
+
return None
|
| 149 |
+
|
| 150 |
+
if __name__ == "__main__":
|
| 151 |
+
test_cases = [
|
| 152 |
+
"M Turuncu",
|
| 153 |
+
"Marlin 6 M Turuncu",
|
| 154 |
+
"L Siyah"
|
| 155 |
+
]
|
| 156 |
+
|
| 157 |
+
for test_case in test_cases:
|
| 158 |
+
print(f"\n=== Testing: {test_case} ===")
|
| 159 |
+
result = get_warehouse_stock(test_case)
|
| 160 |
+
if result:
|
| 161 |
+
print("Sonuç:")
|
| 162 |
+
for item in result:
|
| 163 |
+
print(f" • {item}")
|
| 164 |
+
else:
|
| 165 |
+
print("Sonuç bulunamadı")
|
| 166 |
+
print("-" * 50)
|
test_variant.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
import requests
|
| 3 |
+
import xml.etree.ElementTree as ET
|
| 4 |
+
|
| 5 |
+
def test_variant_search(product_name):
|
| 6 |
+
"""B2B API'den variant field'ı kontrol et"""
|
| 7 |
+
try:
|
| 8 |
+
warehouse_url = 'https://video.trek-turkey.com/bizimhesap-warehouse-xml.php'
|
| 9 |
+
response = requests.get(warehouse_url, verify=False, timeout=15)
|
| 10 |
+
|
| 11 |
+
if response.status_code != 200:
|
| 12 |
+
return None
|
| 13 |
+
|
| 14 |
+
root = ET.fromstring(response.content)
|
| 15 |
+
|
| 16 |
+
# Turkish character normalization function
|
| 17 |
+
turkish_map = {'ı': 'i', 'ğ': 'g', 'ü': 'u', 'ş': 's', 'ö': 'o', 'ç': 'c', 'İ': 'i', 'I': 'i'}
|
| 18 |
+
|
| 19 |
+
def normalize_turkish(text):
|
| 20 |
+
import unicodedata
|
| 21 |
+
text = unicodedata.normalize('NFD', text)
|
| 22 |
+
text = ''.join(char for char in text if unicodedata.category(char) != 'Mn')
|
| 23 |
+
for tr_char, en_char in turkish_map.items():
|
| 24 |
+
text = text.replace(tr_char, en_char)
|
| 25 |
+
return text
|
| 26 |
+
|
| 27 |
+
# Normalize search product name
|
| 28 |
+
search_name = normalize_turkish(product_name.lower().strip())
|
| 29 |
+
search_words = search_name.split()
|
| 30 |
+
|
| 31 |
+
print(f"DEBUG: Aranan: '{product_name}' -> normalize: '{search_name}' -> kelimeler: {search_words}")
|
| 32 |
+
|
| 33 |
+
variant_matches = []
|
| 34 |
+
|
| 35 |
+
# Check variant field for M-TURUNCU like patterns
|
| 36 |
+
for product in root.findall('Product'):
|
| 37 |
+
product_name_elem = product.find('ProductName')
|
| 38 |
+
variant_elem = product.find('Variant')
|
| 39 |
+
|
| 40 |
+
if product_name_elem is not None and product_name_elem.text:
|
| 41 |
+
xml_product_name = product_name_elem.text.strip()
|
| 42 |
+
|
| 43 |
+
# Check variant field
|
| 44 |
+
if variant_elem is not None and variant_elem.text:
|
| 45 |
+
variant_text = variant_elem.text.strip()
|
| 46 |
+
variant_normalized = normalize_turkish(variant_text.lower().replace('-', ' '))
|
| 47 |
+
|
| 48 |
+
# Check if all search words are in variant field
|
| 49 |
+
if all(word in variant_normalized for word in search_words):
|
| 50 |
+
# Check for stock
|
| 51 |
+
warehouses = product.find('Warehouses')
|
| 52 |
+
stock_info = []
|
| 53 |
+
if warehouses is not None:
|
| 54 |
+
for warehouse in warehouses.findall('Warehouse'):
|
| 55 |
+
name_elem = warehouse.find('Name')
|
| 56 |
+
stock_elem = warehouse.find('Stock')
|
| 57 |
+
|
| 58 |
+
if name_elem is not None and stock_elem is not None:
|
| 59 |
+
warehouse_name = name_elem.text if name_elem.text else "Bilinmeyen"
|
| 60 |
+
try:
|
| 61 |
+
stock_count = int(stock_elem.text) if stock_elem.text else 0
|
| 62 |
+
if stock_count > 0:
|
| 63 |
+
stock_info.append(f"{warehouse_name}: {stock_count}")
|
| 64 |
+
except (ValueError, TypeError):
|
| 65 |
+
pass
|
| 66 |
+
|
| 67 |
+
variant_matches.append({
|
| 68 |
+
'product_name': xml_product_name,
|
| 69 |
+
'variant': variant_text,
|
| 70 |
+
'variant_normalized': variant_normalized,
|
| 71 |
+
'stock_info': stock_info
|
| 72 |
+
})
|
| 73 |
+
|
| 74 |
+
print(f"DEBUG: VARIANT MATCH - {xml_product_name}")
|
| 75 |
+
print(f" Variant: {variant_text} -> {variant_normalized}")
|
| 76 |
+
if stock_info:
|
| 77 |
+
print(f" Stok: {', '.join(stock_info)}")
|
| 78 |
+
else:
|
| 79 |
+
print(f" Stokta yok")
|
| 80 |
+
|
| 81 |
+
return variant_matches
|
| 82 |
+
|
| 83 |
+
except Exception as e:
|
| 84 |
+
print(f"Hata: {e}")
|
| 85 |
+
return None
|
| 86 |
+
|
| 87 |
+
if __name__ == "__main__":
|
| 88 |
+
# Test different cases
|
| 89 |
+
test_cases = [
|
| 90 |
+
"M Turuncu",
|
| 91 |
+
"m turuncu",
|
| 92 |
+
"L Siyah",
|
| 93 |
+
"S Beyaz"
|
| 94 |
+
]
|
| 95 |
+
|
| 96 |
+
for test_case in test_cases:
|
| 97 |
+
print(f"\n=== Testing: {test_case} ===")
|
| 98 |
+
result = test_variant_search(test_case)
|
| 99 |
+
|
| 100 |
+
if result:
|
| 101 |
+
print(f"Toplam {len(result)} variant match bulundu:")
|
| 102 |
+
for i, match in enumerate(result, 1):
|
| 103 |
+
print(f"{i}. {match['product_name']} - {match['variant']}")
|
| 104 |
+
if match['stock_info']:
|
| 105 |
+
print(f" Stokta: {', '.join(match['stock_info'])}")
|
| 106 |
+
else:
|
| 107 |
+
print(f" Stokta değil")
|
| 108 |
+
else:
|
| 109 |
+
print("Variant match bulunamadı")
|
| 110 |
+
print("-" * 50)
|
test_warehouse.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
import requests
|
| 3 |
+
import xml.etree.ElementTree as ET
|
| 4 |
+
|
| 5 |
+
def get_warehouse_stock(product_name):
|
| 6 |
+
"""B2B API'den mağaza stok bilgilerini çek - İyileştirilmiş versiyon"""
|
| 7 |
+
try:
|
| 8 |
+
import re
|
| 9 |
+
warehouse_url = 'https://video.trek-turkey.com/bizimhesap-warehouse-xml.php'
|
| 10 |
+
response = requests.get(warehouse_url, verify=False, timeout=15)
|
| 11 |
+
|
| 12 |
+
if response.status_code != 200:
|
| 13 |
+
return None
|
| 14 |
+
|
| 15 |
+
root = ET.fromstring(response.content)
|
| 16 |
+
|
| 17 |
+
# Turkish character normalization function
|
| 18 |
+
turkish_map = {'ı': 'i', 'ğ': 'g', 'ü': 'u', 'ş': 's', 'ö': 'o', 'ç': 'c', 'İ': 'i', 'I': 'i'}
|
| 19 |
+
|
| 20 |
+
def normalize_turkish(text):
|
| 21 |
+
import unicodedata
|
| 22 |
+
text = unicodedata.normalize('NFD', text)
|
| 23 |
+
text = ''.join(char for char in text if unicodedata.category(char) != 'Mn')
|
| 24 |
+
for tr_char, en_char in turkish_map.items():
|
| 25 |
+
text = text.replace(tr_char, en_char)
|
| 26 |
+
return text
|
| 27 |
+
|
| 28 |
+
# Normalize search product name
|
| 29 |
+
search_name = normalize_turkish(product_name.lower().strip())
|
| 30 |
+
search_name = search_name.replace('(2026)', '').replace('(2025)', '').replace(' gen 3', '').replace(' gen', '').strip()
|
| 31 |
+
search_words = search_name.split()
|
| 32 |
+
|
| 33 |
+
print(f"DEBUG: Aranan ürün: '{product_name}' -> normalize: '{search_name}' -> kelimeler: {search_words}")
|
| 34 |
+
|
| 35 |
+
best_matches = []
|
| 36 |
+
exact_matches = []
|
| 37 |
+
|
| 38 |
+
# İlk geçiş: Tam eşleşmeleri bul
|
| 39 |
+
for product in root.findall('Product'):
|
| 40 |
+
product_name_elem = product.find('ProductName')
|
| 41 |
+
if product_name_elem is not None and product_name_elem.text:
|
| 42 |
+
xml_product_name = product_name_elem.text.strip()
|
| 43 |
+
normalized_xml = normalize_turkish(xml_product_name.lower())
|
| 44 |
+
normalized_xml = normalized_xml.replace('(2026)', '').replace('(2025)', '').replace(' gen 3', '').replace(' gen', '').strip()
|
| 45 |
+
xml_words = normalized_xml.split()
|
| 46 |
+
|
| 47 |
+
# Tam eşleşme kontrolü - ilk iki kelime tam aynı olmalı
|
| 48 |
+
if len(search_words) >= 2 and len(xml_words) >= 2:
|
| 49 |
+
search_key = f"{search_words[0]} {search_words[1]}"
|
| 50 |
+
xml_key = f"{xml_words[0]} {xml_words[1]}"
|
| 51 |
+
|
| 52 |
+
if search_key == xml_key:
|
| 53 |
+
exact_matches.append((product, xml_product_name, normalized_xml))
|
| 54 |
+
print(f"DEBUG: TAM EŞLEŞME bulundu: '{xml_product_name}'")
|
| 55 |
+
|
| 56 |
+
# Eğer tam eşleşme varsa, sadece onları kullan
|
| 57 |
+
candidates = exact_matches if exact_matches else []
|
| 58 |
+
|
| 59 |
+
# Eğer tam eşleşme yoksa, fuzzy matching yap
|
| 60 |
+
if not candidates:
|
| 61 |
+
print("DEBUG: Tam eşleşme yok, fuzzy matching yapılıyor...")
|
| 62 |
+
for product in root.findall('Product'):
|
| 63 |
+
product_name_elem = product.find('ProductName')
|
| 64 |
+
if product_name_elem is not None and product_name_elem.text:
|
| 65 |
+
xml_product_name = product_name_elem.text.strip()
|
| 66 |
+
normalized_xml = normalize_turkish(xml_product_name.lower())
|
| 67 |
+
normalized_xml = normalized_xml.replace('(2026)', '').replace('(2025)', '').replace(' gen 3', '').replace(' gen', '').strip()
|
| 68 |
+
xml_words = normalized_xml.split()
|
| 69 |
+
|
| 70 |
+
# Ortak kelime sayısını hesapla
|
| 71 |
+
common_words = set(search_words) & set(xml_words)
|
| 72 |
+
|
| 73 |
+
# En az 2 ortak kelime olmalı VE ilk kelime aynı olmalı (marka kontrolü)
|
| 74 |
+
if (len(common_words) >= 2 and
|
| 75 |
+
len(search_words) > 0 and len(xml_words) > 0 and
|
| 76 |
+
search_words[0] == xml_words[0]):
|
| 77 |
+
best_matches.append((product, xml_product_name, normalized_xml, len(common_words)))
|
| 78 |
+
print(f"DEBUG: FUZZY EŞLEŞME: '{xml_product_name}' (ortak: {len(common_words)})")
|
| 79 |
+
|
| 80 |
+
# En çok ortak kelimeye sahip olanları seç
|
| 81 |
+
if best_matches:
|
| 82 |
+
max_common = max(match[3] for match in best_matches)
|
| 83 |
+
candidates = [(match[0], match[1], match[2]) for match in best_matches if match[3] == max_common]
|
| 84 |
+
|
| 85 |
+
print(f"DEBUG: Toplam {len(candidates)} aday ürün bulundu")
|
| 86 |
+
|
| 87 |
+
# Stok bilgilerini topla ve tekrarları önle
|
| 88 |
+
warehouse_stock_map = {} # warehouse_name -> total_stock
|
| 89 |
+
|
| 90 |
+
for product, xml_name, _ in candidates:
|
| 91 |
+
warehouses = product.find('Warehouses')
|
| 92 |
+
if warehouses is not None:
|
| 93 |
+
for warehouse in warehouses.findall('Warehouse'):
|
| 94 |
+
name_elem = warehouse.find('Name')
|
| 95 |
+
stock_elem = warehouse.find('Stock')
|
| 96 |
+
|
| 97 |
+
if name_elem is not None and stock_elem is not None:
|
| 98 |
+
warehouse_name = name_elem.text if name_elem.text else "Bilinmeyen"
|
| 99 |
+
try:
|
| 100 |
+
stock_count = int(stock_elem.text) if stock_elem.text else 0
|
| 101 |
+
if stock_count > 0:
|
| 102 |
+
# Aynı mağaza için stokları topla
|
| 103 |
+
if warehouse_name in warehouse_stock_map:
|
| 104 |
+
warehouse_stock_map[warehouse_name] += stock_count
|
| 105 |
+
else:
|
| 106 |
+
warehouse_stock_map[warehouse_name] = stock_count
|
| 107 |
+
print(f"DEBUG: STOK BULUNDU - {warehouse_name}: {stock_count} adet ({xml_name})")
|
| 108 |
+
except (ValueError, TypeError):
|
| 109 |
+
pass
|
| 110 |
+
|
| 111 |
+
if warehouse_stock_map:
|
| 112 |
+
# Mağaza stoklarını liste halinde döndür
|
| 113 |
+
all_warehouse_info = []
|
| 114 |
+
for warehouse_name, total_stock in warehouse_stock_map.items():
|
| 115 |
+
all_warehouse_info.append(f"{warehouse_name}: {total_stock} adet")
|
| 116 |
+
return all_warehouse_info
|
| 117 |
+
else:
|
| 118 |
+
print("DEBUG: Hiçbir mağazada stok bulunamadı")
|
| 119 |
+
return ["Hiçbir mağazada stokta bulunmuyor"]
|
| 120 |
+
|
| 121 |
+
except Exception as e:
|
| 122 |
+
print(f"Mağaza stok bilgisi çekme hatası: {e}")
|
| 123 |
+
return None
|
| 124 |
+
|
| 125 |
+
if __name__ == "__main__":
|
| 126 |
+
# Test the function with different inputs
|
| 127 |
+
test_cases = [
|
| 128 |
+
"MARLIN 6 (2026)",
|
| 129 |
+
"Marlin 6"
|
| 130 |
+
]
|
| 131 |
+
|
| 132 |
+
for test_case in test_cases:
|
| 133 |
+
print(f"=== Testing: {test_case} ===")
|
| 134 |
+
result = get_warehouse_stock(test_case)
|
| 135 |
+
print(f"Result: {result}")
|
| 136 |
+
print()
|
warehouse_stock_finder.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Ultra simple and fast warehouse stock finder"""
|
| 2 |
+
|
| 3 |
+
def get_warehouse_stock(product_name):
|
| 4 |
+
"""Find warehouse stock FAST - no XML parsing, just regex"""
|
| 5 |
+
try:
|
| 6 |
+
import re
|
| 7 |
+
import requests
|
| 8 |
+
|
| 9 |
+
# Get XML
|
| 10 |
+
url = 'https://video.trek-turkey.com/bizimhesap-warehouse-xml-b2b-api-v2.php'
|
| 11 |
+
response = requests.get(url, verify=False, timeout=7)
|
| 12 |
+
xml_text = response.text
|
| 13 |
+
|
| 14 |
+
# Turkish normalize
|
| 15 |
+
def normalize(text):
|
| 16 |
+
tr_map = {'İ': 'i', 'I': 'i', 'ı': 'i', 'Ğ': 'g', 'ğ': 'g', 'Ü': 'u', 'ü': 'u', 'Ş': 's', 'ş': 's', 'Ö': 'o', 'ö': 'o', 'Ç': 'c', 'ç': 'c'}
|
| 17 |
+
# FIRST normalize Turkish chars
|
| 18 |
+
for tr, en in tr_map.items():
|
| 19 |
+
text = text.replace(tr, en)
|
| 20 |
+
# THEN lowercase
|
| 21 |
+
text = text.lower()
|
| 22 |
+
return text
|
| 23 |
+
|
| 24 |
+
# Parse query
|
| 25 |
+
query = normalize(product_name.strip()).replace('(2026)', '').replace('(2025)', '').strip()
|
| 26 |
+
words = query.split()
|
| 27 |
+
|
| 28 |
+
# Find size
|
| 29 |
+
sizes = ['s', 'm', 'l', 'xl', 'xs', 'xxl', 'ml']
|
| 30 |
+
size = next((w for w in words if w in sizes), None)
|
| 31 |
+
product_words = [w for w in words if w not in sizes and w not in ['beden', 'size', 'boy']]
|
| 32 |
+
|
| 33 |
+
# Build search pattern
|
| 34 |
+
if 'madone' in product_words and 'sl' in product_words and '6' in product_words:
|
| 35 |
+
pattern = 'MADONE SL 6 GEN 8'
|
| 36 |
+
else:
|
| 37 |
+
pattern = ' '.join(product_words).upper()
|
| 38 |
+
|
| 39 |
+
print(f"DEBUG - Searching: {pattern}, Size: {size}")
|
| 40 |
+
|
| 41 |
+
# Search for product + variant combo
|
| 42 |
+
if size:
|
| 43 |
+
# Direct search for product with specific size
|
| 44 |
+
size_pattern = f'{size.upper()}-'
|
| 45 |
+
|
| 46 |
+
# Find all occurrences of the product name
|
| 47 |
+
import re
|
| 48 |
+
product_regex = f'<Product>.*?<ProductName><!\\[CDATA\\[{re.escape(pattern)}\\]\\]></ProductName>.*?<ProductVariant><!\\[CDATA\\[{size_pattern}.*?\\]\\]></ProductVariant>.*?</Product>'
|
| 49 |
+
|
| 50 |
+
match = re.search(product_regex, xml_text, re.DOTALL)
|
| 51 |
+
|
| 52 |
+
if match:
|
| 53 |
+
product_block = match.group(0)
|
| 54 |
+
print(f"DEBUG - Found product with {size_pattern} variant")
|
| 55 |
+
|
| 56 |
+
# Extract warehouses
|
| 57 |
+
warehouse_info = []
|
| 58 |
+
warehouse_regex = r'<Warehouse>.*?<Name><!\\[CDATA\\[(.*?)\\]\\]></Name>.*?<Stock>(.*?)</Stock>.*?</Warehouse>'
|
| 59 |
+
warehouses = re.findall(warehouse_regex, product_block, re.DOTALL)
|
| 60 |
+
|
| 61 |
+
for wh_name, wh_stock in warehouses:
|
| 62 |
+
try:
|
| 63 |
+
stock = int(wh_stock.strip())
|
| 64 |
+
if stock > 0:
|
| 65 |
+
# Format name
|
| 66 |
+
if "CADDEBOSTAN" in wh_name:
|
| 67 |
+
display = "Caddebostan mağazası"
|
| 68 |
+
elif "ORTAKÖY" in wh_name:
|
| 69 |
+
display = "Ortaköy mağazası"
|
| 70 |
+
elif "ALSANCAK" in wh_name:
|
| 71 |
+
display = "İzmir Alsancak mağazası"
|
| 72 |
+
elif "BAHCEKOY" in wh_name or "BAHÇEKÖY" in wh_name:
|
| 73 |
+
display = "Bahçeköy mağazası"
|
| 74 |
+
else:
|
| 75 |
+
display = wh_name
|
| 76 |
+
|
| 77 |
+
warehouse_info.append(f"{display}: Mevcut")
|
| 78 |
+
except:
|
| 79 |
+
pass
|
| 80 |
+
|
| 81 |
+
return warehouse_info if warehouse_info else ["Hiçbir mağazada mevcut değil"]
|
| 82 |
+
else:
|
| 83 |
+
print(f"DEBUG - No {size_pattern} variant found for {pattern}")
|
| 84 |
+
return ["Hiçbir mağazada mevcut değil"]
|
| 85 |
+
else:
|
| 86 |
+
# No size filter - get all stock
|
| 87 |
+
return ["Beden bilgisi belirtilmedi"]
|
| 88 |
+
|
| 89 |
+
except Exception as e:
|
| 90 |
+
print(f"Error: {e}")
|
| 91 |
+
return None
|
whatsapp_features.py
ADDED
|
@@ -0,0 +1,432 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
WhatsApp Trek Chatbot Enhanced Features
|
| 4 |
+
1. Ürün Karşılaştırma
|
| 5 |
+
2. Bütçe Önerileri
|
| 6 |
+
3. Fiyat Hesaplamaları
|
| 7 |
+
WhatsApp için basitleştirilmiş versiyon - Görsel AI ve profil sistemi yok
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import re
|
| 11 |
+
from datetime import datetime
|
| 12 |
+
|
| 13 |
+
class WhatsAppProductComparison:
|
| 14 |
+
"""WhatsApp için ürün karşılaştırma sistemi"""
|
| 15 |
+
|
| 16 |
+
def __init__(self, products_data):
|
| 17 |
+
self.products = products_data
|
| 18 |
+
|
| 19 |
+
def round_price(self, price_str):
|
| 20 |
+
"""Fiyatı yuvarlama formülüne göre yuvarla"""
|
| 21 |
+
try:
|
| 22 |
+
price_float = float(price_str)
|
| 23 |
+
# Fiyat 200000 üzerindeyse en yakın 5000'lik basamağa yuvarla
|
| 24 |
+
if price_float > 200000:
|
| 25 |
+
return str(round(price_float / 5000) * 5000)
|
| 26 |
+
# Fiyat 30000 üzerindeyse en yakın 1000'lik basamağa yuvarla
|
| 27 |
+
elif price_float > 30000:
|
| 28 |
+
return str(round(price_float / 1000) * 1000)
|
| 29 |
+
# Fiyat 10000 üzerindeyse en yakın 100'lük basamağa yuvarla
|
| 30 |
+
elif price_float > 10000:
|
| 31 |
+
return str(round(price_float / 100) * 100)
|
| 32 |
+
# Diğer durumlarda en yakın 10'luk basamağa yuvarla
|
| 33 |
+
else:
|
| 34 |
+
return str(round(price_float / 10) * 10)
|
| 35 |
+
except (ValueError, TypeError):
|
| 36 |
+
return price_str
|
| 37 |
+
|
| 38 |
+
def find_products_by_name(self, product_names):
|
| 39 |
+
"""İsimlere göre ürünleri bul"""
|
| 40 |
+
found_products = []
|
| 41 |
+
used_products = set() # Aynı ürünü iki kez eklememek için
|
| 42 |
+
|
| 43 |
+
for name in product_names:
|
| 44 |
+
for product in self.products:
|
| 45 |
+
product_id = product[2] # full_name'i unique id olarak kullan
|
| 46 |
+
if product_id not in used_products and name.lower() in product[2].lower():
|
| 47 |
+
found_products.append(product)
|
| 48 |
+
used_products.add(product_id)
|
| 49 |
+
break
|
| 50 |
+
return found_products
|
| 51 |
+
|
| 52 |
+
def create_comparison_whatsapp(self, product_names):
|
| 53 |
+
"""WhatsApp için karşılaştırma metni oluştur"""
|
| 54 |
+
products = self.find_products_by_name(product_names)
|
| 55 |
+
|
| 56 |
+
if len(products) < 2:
|
| 57 |
+
return "❌ Karşılaştırma için en az 2 ürün gerekli."
|
| 58 |
+
|
| 59 |
+
# Karşılaştırma metnini hazırla
|
| 60 |
+
comparison_text = "📊 **ÜRÜN KARŞILAŞTIRMASI**\n\n"
|
| 61 |
+
|
| 62 |
+
for i, product in enumerate(products, 1):
|
| 63 |
+
name, item_info, full_name = product
|
| 64 |
+
|
| 65 |
+
# Ürün bilgilerini parse et
|
| 66 |
+
stock_status = item_info[0] if len(item_info) > 0 else "Bilgi yok"
|
| 67 |
+
price_raw = item_info[1] if len(item_info) > 1 and item_info[1] else "Fiyat yok"
|
| 68 |
+
product_link = item_info[2] if len(item_info) > 2 else ""
|
| 69 |
+
|
| 70 |
+
# Fiyatı yuvarlama formülüne göre yuvarla
|
| 71 |
+
if price_raw != "Fiyat yok":
|
| 72 |
+
price = self.round_price(price_raw)
|
| 73 |
+
price_display = f"{price} TL"
|
| 74 |
+
else:
|
| 75 |
+
price_display = price_raw
|
| 76 |
+
|
| 77 |
+
comparison_text += f"**{i}. {full_name}**\n"
|
| 78 |
+
comparison_text += f"📦 Stok: {stock_status}\n"
|
| 79 |
+
comparison_text += f"💰 Fiyat: {price_display}\n"
|
| 80 |
+
|
| 81 |
+
# Resim URL'si varsa ekle (6. index)
|
| 82 |
+
if len(item_info) > 6 and item_info[6]:
|
| 83 |
+
comparison_text += f"🖼️ Resim: {item_info[6]}\n"
|
| 84 |
+
|
| 85 |
+
if product_link:
|
| 86 |
+
comparison_text += f"🔗 Link: {product_link}\n"
|
| 87 |
+
|
| 88 |
+
comparison_text += "\n" + "─" * 25 + "\n\n"
|
| 89 |
+
|
| 90 |
+
return comparison_text
|
| 91 |
+
|
| 92 |
+
def get_similar_products(self, product_name, category_filter=None):
|
| 93 |
+
"""Benzer ürünleri bul"""
|
| 94 |
+
similar_products = []
|
| 95 |
+
base_name = product_name.lower().split()[0] # İlk kelimeyi al
|
| 96 |
+
|
| 97 |
+
for product in self.products:
|
| 98 |
+
product_full_name = product[2].lower()
|
| 99 |
+
if base_name in product_full_name and product_name.lower() != product_full_name:
|
| 100 |
+
if category_filter:
|
| 101 |
+
if category_filter.lower() in product_full_name:
|
| 102 |
+
similar_products.append(product)
|
| 103 |
+
else:
|
| 104 |
+
similar_products.append(product)
|
| 105 |
+
|
| 106 |
+
return similar_products[:3] # İlk 3 benzer ürün
|
| 107 |
+
|
| 108 |
+
class WhatsAppBudgetRecommendations:
|
| 109 |
+
"""WhatsApp için bütçe önerileri"""
|
| 110 |
+
|
| 111 |
+
def __init__(self, products_data):
|
| 112 |
+
self.products = products_data
|
| 113 |
+
|
| 114 |
+
def get_budget_recommendations(self, budget_min, budget_max):
|
| 115 |
+
"""Bütçeye uygun öneriler"""
|
| 116 |
+
suitable_products = []
|
| 117 |
+
|
| 118 |
+
for product in self.products:
|
| 119 |
+
if product[1][0] == "stokta" and product[1][1]: # Stokta ve fiyatı var
|
| 120 |
+
try:
|
| 121 |
+
price = float(product[1][1])
|
| 122 |
+
if budget_min <= price <= budget_max:
|
| 123 |
+
suitable_products.append(product)
|
| 124 |
+
except (ValueError, TypeError):
|
| 125 |
+
continue
|
| 126 |
+
|
| 127 |
+
return suitable_products[:5] # İlk 5 öneri
|
| 128 |
+
|
| 129 |
+
def format_budget_recommendations_whatsapp(self, products, budget_min, budget_max):
|
| 130 |
+
"""WhatsApp için bütçe önerilerini formatla"""
|
| 131 |
+
if not products:
|
| 132 |
+
if budget_min == budget_max:
|
| 133 |
+
return f"❌ {budget_min:,.0f} TL bütçenize uygun stokta ürün bulunamadı."
|
| 134 |
+
else:
|
| 135 |
+
return f"❌ {budget_min:,.0f}-{budget_max:,.0f} TL bütçenize uygun stokta ürün bulunamadı."
|
| 136 |
+
|
| 137 |
+
if budget_min == budget_max:
|
| 138 |
+
text = f"💰 **{budget_min:,.0f} TL BÜTÇENİZE EN YAKIN ÖNERİLER**\n\n"
|
| 139 |
+
else:
|
| 140 |
+
text = f"💰 **{budget_min:,.0f}-{budget_max:,.0f} TL BÜTÇENİZE UYGUN ÖNERİLER**\n\n"
|
| 141 |
+
|
| 142 |
+
for i, product in enumerate(products, 1):
|
| 143 |
+
name, item_info, full_name = product
|
| 144 |
+
price = item_info[1] if len(item_info) > 1 else "Fiyat yok"
|
| 145 |
+
product_link = item_info[2] if len(item_info) > 2 else ""
|
| 146 |
+
|
| 147 |
+
text += f"**{i}. {full_name}**\n"
|
| 148 |
+
# Fiyatı formatlı göster
|
| 149 |
+
try:
|
| 150 |
+
price_float = float(price)
|
| 151 |
+
price_formatted = f"{price_float:,.0f}"
|
| 152 |
+
except:
|
| 153 |
+
price_formatted = price
|
| 154 |
+
text += f"💰 Fiyat: {price_formatted} TL\n"
|
| 155 |
+
|
| 156 |
+
# Resim URL'si varsa ekle
|
| 157 |
+
if len(item_info) > 6 and item_info[6]:
|
| 158 |
+
text += f"🖼️ Resim: {item_info[6]}\n"
|
| 159 |
+
|
| 160 |
+
if product_link:
|
| 161 |
+
text += f"🔗 Link: {product_link}\n"
|
| 162 |
+
|
| 163 |
+
text += "\n"
|
| 164 |
+
|
| 165 |
+
return text
|
| 166 |
+
|
| 167 |
+
class WhatsAppCategoryRecommendations:
|
| 168 |
+
"""WhatsApp için kategori bazlı öneriler"""
|
| 169 |
+
|
| 170 |
+
def __init__(self, products_data):
|
| 171 |
+
self.products = products_data
|
| 172 |
+
|
| 173 |
+
def get_category_products(self, category_keywords):
|
| 174 |
+
"""Kategoriye göre ürünleri bul"""
|
| 175 |
+
category_products = []
|
| 176 |
+
|
| 177 |
+
for product in self.products:
|
| 178 |
+
if product[1][0] == "stokta": # Sadece stokta olanlar
|
| 179 |
+
product_name = product[2].lower()
|
| 180 |
+
for keyword in category_keywords:
|
| 181 |
+
if keyword.lower() in product_name:
|
| 182 |
+
category_products.append(product)
|
| 183 |
+
break
|
| 184 |
+
|
| 185 |
+
return category_products[:100] # İlk 100 ürün - bütçe filtresi için daha fazla ürün
|
| 186 |
+
|
| 187 |
+
def format_category_recommendations(self, category_name, products):
|
| 188 |
+
"""Kategori önerilerini formatla"""
|
| 189 |
+
if not products:
|
| 190 |
+
return f"❌ {category_name} kategorisinde stokta ürün bulunamadı."
|
| 191 |
+
|
| 192 |
+
text = f"🚲 **{category_name.upper()} KATEGORİSİ ÖNERİLERİ**\n\n"
|
| 193 |
+
|
| 194 |
+
for i, product in enumerate(products, 1):
|
| 195 |
+
name, item_info, full_name = product
|
| 196 |
+
price = item_info[1] if len(item_info) > 1 else "Fiyat yok"
|
| 197 |
+
|
| 198 |
+
# Fiyatı formatlı göster
|
| 199 |
+
try:
|
| 200 |
+
price_float = float(price)
|
| 201 |
+
price_formatted = f"{price_float:,.0f}"
|
| 202 |
+
except:
|
| 203 |
+
price_formatted = price
|
| 204 |
+
|
| 205 |
+
text += f"**{i}. {full_name}**\n"
|
| 206 |
+
text += f"💰 Fiyat: {price_formatted} TL\n\n"
|
| 207 |
+
|
| 208 |
+
return text
|
| 209 |
+
|
| 210 |
+
# Global instance'lar
|
| 211 |
+
whatsapp_product_comparison = None
|
| 212 |
+
whatsapp_budget_recommendations = None
|
| 213 |
+
whatsapp_category_recommendations = None
|
| 214 |
+
|
| 215 |
+
def initialize_whatsapp_features(products_data):
|
| 216 |
+
"""WhatsApp enhanced özellikleri başlat"""
|
| 217 |
+
global whatsapp_product_comparison, whatsapp_budget_recommendations, whatsapp_category_recommendations
|
| 218 |
+
|
| 219 |
+
whatsapp_product_comparison = WhatsAppProductComparison(products_data)
|
| 220 |
+
whatsapp_budget_recommendations = WhatsAppBudgetRecommendations(products_data)
|
| 221 |
+
whatsapp_category_recommendations = WhatsAppCategoryRecommendations(products_data)
|
| 222 |
+
|
| 223 |
+
def handle_whatsapp_comparison_request(user_message):
|
| 224 |
+
"""WhatsApp karşılaştırma talebini işle"""
|
| 225 |
+
try:
|
| 226 |
+
if "karşılaştır" in user_message.lower() or "compare" in user_message.lower():
|
| 227 |
+
# Ürün isimlerini çıkarmaya çalış
|
| 228 |
+
words = user_message.lower().split()
|
| 229 |
+
potential_products = []
|
| 230 |
+
|
| 231 |
+
# Bilinen model isimlerini ara - daha spesifik
|
| 232 |
+
known_models = ["émonda", "madone", "domane", "marlin", "fuel", "powerfly", "fx", "checkpoint", "procaliber", "supercaliber"]
|
| 233 |
+
|
| 234 |
+
# Marlin 6, Marlin 7 gibi spesifik modelleri ara
|
| 235 |
+
message_lower = user_message.lower()
|
| 236 |
+
if "marlin" in message_lower:
|
| 237 |
+
import re
|
| 238 |
+
marlin_numbers = re.findall(r'marlin\s*(\d+)', message_lower)
|
| 239 |
+
if len(marlin_numbers) >= 2:
|
| 240 |
+
# Marlin 6, Marlin 7 gibi spesifik modeller bulundu
|
| 241 |
+
for num in marlin_numbers:
|
| 242 |
+
potential_products.append(f"marlin {num}")
|
| 243 |
+
elif len(marlin_numbers) == 1:
|
| 244 |
+
potential_products.append(f"marlin {marlin_numbers[0]}")
|
| 245 |
+
else:
|
| 246 |
+
potential_products.append("marlin")
|
| 247 |
+
|
| 248 |
+
# Diğer modelleri ara
|
| 249 |
+
for word in words:
|
| 250 |
+
for model in known_models:
|
| 251 |
+
if model != "marlin" and model in word: # marlin'i ayrı işliyoruz
|
| 252 |
+
potential_products.append(model)
|
| 253 |
+
|
| 254 |
+
# Duplicate'leri kaldır
|
| 255 |
+
potential_products = list(dict.fromkeys(potential_products))
|
| 256 |
+
|
| 257 |
+
if len(potential_products) >= 2 and whatsapp_product_comparison:
|
| 258 |
+
comparison_result = whatsapp_product_comparison.create_comparison_whatsapp(potential_products)
|
| 259 |
+
return comparison_result
|
| 260 |
+
|
| 261 |
+
return None
|
| 262 |
+
except Exception as e:
|
| 263 |
+
print(f"WhatsApp Comparison error: {e}")
|
| 264 |
+
return None
|
| 265 |
+
|
| 266 |
+
def handle_whatsapp_price_query(user_message):
|
| 267 |
+
"""WhatsApp fiyat sorgusu - 'ne kadar', 'fiyat', 'kaç para' gibi"""
|
| 268 |
+
try:
|
| 269 |
+
user_lower = user_message.lower()
|
| 270 |
+
|
| 271 |
+
# Fiyat sorgusu anahtar kelimeleri
|
| 272 |
+
price_query_keywords = ["ne kadar", "fiyat", "kaç para", "fiyatı", "kaça", "para"]
|
| 273 |
+
if not any(keyword in user_lower for keyword in price_query_keywords):
|
| 274 |
+
return None
|
| 275 |
+
|
| 276 |
+
# Ürün adı tespit et
|
| 277 |
+
product_keywords = ["madone", "émonda", "domane", "marlin", "fuel", "powerfly", "fx", "checkpoint", "procaliber", "supercaliber", "ds", "verve", "rail"]
|
| 278 |
+
found_product = None
|
| 279 |
+
|
| 280 |
+
for keyword in product_keywords:
|
| 281 |
+
if keyword in user_lower:
|
| 282 |
+
found_product = keyword
|
| 283 |
+
break
|
| 284 |
+
|
| 285 |
+
if found_product and whatsapp_budget_recommendations:
|
| 286 |
+
# Ürünü products listesinde ara
|
| 287 |
+
matching_products = []
|
| 288 |
+
for product in whatsapp_budget_recommendations.products:
|
| 289 |
+
if product[1][0] == "stokta" and found_product in product[2].lower():
|
| 290 |
+
matching_products.append(product)
|
| 291 |
+
|
| 292 |
+
if matching_products:
|
| 293 |
+
# İlk ürünü al (en temel model)
|
| 294 |
+
main_product = matching_products[0]
|
| 295 |
+
name, item_info, full_name = main_product
|
| 296 |
+
price = item_info[1] if len(item_info) > 1 else "Fiyat yok"
|
| 297 |
+
|
| 298 |
+
try:
|
| 299 |
+
price_float = float(price)
|
| 300 |
+
price_formatted = f"{price_float:,.0f}"
|
| 301 |
+
except:
|
| 302 |
+
price_formatted = price
|
| 303 |
+
|
| 304 |
+
# Aynı serinin diğer modellerini bul
|
| 305 |
+
series_products = matching_products[:3] # İlk 3 model
|
| 306 |
+
|
| 307 |
+
response = f"🚲 **{found_product.upper()} SERİSİ FİYATLARI**\\n\\n"
|
| 308 |
+
|
| 309 |
+
for i, product in enumerate(series_products, 1):
|
| 310 |
+
name, item_info, full_name = product
|
| 311 |
+
price = item_info[1] if len(item_info) > 1 else "Fiyat yok"
|
| 312 |
+
|
| 313 |
+
try:
|
| 314 |
+
price_float = float(price)
|
| 315 |
+
price_formatted = f"{price_float:,.0f}"
|
| 316 |
+
except:
|
| 317 |
+
price_formatted = price
|
| 318 |
+
|
| 319 |
+
response += f"**{i}. {full_name}**\\n"
|
| 320 |
+
response += f"💰 Fiyat: {price_formatted} TL\\n\\n"
|
| 321 |
+
|
| 322 |
+
return response
|
| 323 |
+
|
| 324 |
+
return None
|
| 325 |
+
except Exception as e:
|
| 326 |
+
print(f"WhatsApp Price Query error: {e}")
|
| 327 |
+
return None
|
| 328 |
+
|
| 329 |
+
def handle_whatsapp_budget_request(user_message):
|
| 330 |
+
"""WhatsApp bütçe talebini işle"""
|
| 331 |
+
try:
|
| 332 |
+
message_lower = user_message.lower()
|
| 333 |
+
|
| 334 |
+
# Kategori sorgusu değilse sadece bütçe sorgusuna bak
|
| 335 |
+
category_keywords = ["yol", "dağ", "şehir", "elektrikli", "gravel", "marlin", "madone", "émonda", "domane", "fuel", "powerfly", "fx", "ds", "checkpoint"]
|
| 336 |
+
has_category = any(keyword in message_lower for keyword in category_keywords)
|
| 337 |
+
|
| 338 |
+
if has_category:
|
| 339 |
+
return None # Kategori varsa category function'a bırak
|
| 340 |
+
|
| 341 |
+
# Sadece sayı varsa bütçe olarak algıla
|
| 342 |
+
import re
|
| 343 |
+
numbers = re.findall(r'\d+', user_message)
|
| 344 |
+
|
| 345 |
+
if numbers and whatsapp_budget_recommendations:
|
| 346 |
+
budget_value = int(numbers[0]) * 1000 # 350 -> 350000
|
| 347 |
+
|
| 348 |
+
# Tüm stokta olan ürünleri al
|
| 349 |
+
all_products = []
|
| 350 |
+
for product in whatsapp_budget_recommendations.products:
|
| 351 |
+
if product[1][0] == "stokta" and product[1][1]:
|
| 352 |
+
try:
|
| 353 |
+
price = float(product[1][1])
|
| 354 |
+
price_diff = abs(price - budget_value)
|
| 355 |
+
all_products.append((product, price_diff))
|
| 356 |
+
except:
|
| 357 |
+
continue
|
| 358 |
+
|
| 359 |
+
# Fiyat farkına göre sırala
|
| 360 |
+
all_products.sort(key=lambda x: x[1])
|
| 361 |
+
closest_products = [product[0] for product in all_products[:5]] # En yakın 5 ürün
|
| 362 |
+
|
| 363 |
+
if closest_products:
|
| 364 |
+
return f"💰 {budget_value:,} TL bütçenize en yakın öneriler:\n\n" + whatsapp_budget_recommendations.format_budget_recommendations_whatsapp(
|
| 365 |
+
closest_products, budget_value, budget_value
|
| 366 |
+
)
|
| 367 |
+
|
| 368 |
+
return None
|
| 369 |
+
except Exception as e:
|
| 370 |
+
print(f"WhatsApp Budget error: {e}")
|
| 371 |
+
return None
|
| 372 |
+
|
| 373 |
+
def handle_whatsapp_category_request(user_message, phone_number=None):
|
| 374 |
+
"""WhatsApp kategori önerisi talebini işle"""
|
| 375 |
+
try:
|
| 376 |
+
user_lower = user_message.lower()
|
| 377 |
+
|
| 378 |
+
# Bütçe bilgisini çıkar (sadece sayı varsa)
|
| 379 |
+
budget_value = None
|
| 380 |
+
import re
|
| 381 |
+
numbers = re.findall(r'\d+', user_message)
|
| 382 |
+
if numbers:
|
| 383 |
+
budget_value = int(numbers[0]) * 1000 # 350 -> 350000
|
| 384 |
+
|
| 385 |
+
# Kategori tespiti - basit keyword matching
|
| 386 |
+
categories = {
|
| 387 |
+
"dağ bisikleti": ["dağ", "dag", "offroad", "mountain", "marlin", "fuel", "procaliber", "supercaliber"],
|
| 388 |
+
"yol bisikleti": ["yol", "road", "hız", "yarış", "émonda", "madone", "domane", "speed"],
|
| 389 |
+
"şehir bisikleti": ["şehir", "sehir", "city", "urban", "fx", "ds", "dual sport", "verve"],
|
| 390 |
+
"elektrikli bisiklet": ["elektrikli", "electric", "e-bike", "ebike", "powerfly", "rail", "fuel exe", "domane+", "fx+", "ds+", "verve+", "townie"],
|
| 391 |
+
"gravel bisiklet": ["gravel", "çakıl", "checkpoint"]
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
for category_name, keywords in categories.items():
|
| 395 |
+
if any(keyword in user_lower for keyword in keywords):
|
| 396 |
+
if whatsapp_category_recommendations:
|
| 397 |
+
# Kategori ürünlerini al
|
| 398 |
+
products = whatsapp_category_recommendations.get_category_products(keywords)
|
| 399 |
+
|
| 400 |
+
# Bütçe varsa fiyat farkına göre sırala
|
| 401 |
+
if budget_value is not None:
|
| 402 |
+
products_with_price_diff = []
|
| 403 |
+
for product in products:
|
| 404 |
+
if product[1][0] == "stokta" and product[1][1]:
|
| 405 |
+
try:
|
| 406 |
+
price = float(product[1][1])
|
| 407 |
+
price_diff = abs(price - budget_value)
|
| 408 |
+
products_with_price_diff.append((product, price_diff))
|
| 409 |
+
except:
|
| 410 |
+
continue
|
| 411 |
+
|
| 412 |
+
# Fiyat farkına göre sırala (en yakın fiyat önce)
|
| 413 |
+
products_with_price_diff.sort(key=lambda x: x[1])
|
| 414 |
+
products = [product[0] for product in products_with_price_diff[:5]] # En yakın 5 ürün
|
| 415 |
+
else:
|
| 416 |
+
products = products[:5] # Bütçe yoksa ilk 5 ürün
|
| 417 |
+
|
| 418 |
+
if products:
|
| 419 |
+
response = whatsapp_category_recommendations.format_category_recommendations(
|
| 420 |
+
category_name, products
|
| 421 |
+
)
|
| 422 |
+
|
| 423 |
+
# Bütçe bilgisi varsa ekle
|
| 424 |
+
if budget_value is not None:
|
| 425 |
+
response = f"💰 {budget_value:,} TL bütçenize en yakın öneriler:\n\n" + response
|
| 426 |
+
|
| 427 |
+
return response
|
| 428 |
+
|
| 429 |
+
return None
|
| 430 |
+
except Exception as e:
|
| 431 |
+
print(f"WhatsApp Category error: {e}")
|
| 432 |
+
return None
|
whatsapp_improved_chatbot.py
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
WhatsApp Improved Chatbot with Professional Product Search
|
| 3 |
+
Optimized for mobile WhatsApp formatting
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from product_search import ProductSearchEngine
|
| 7 |
+
import re
|
| 8 |
+
from typing import List, Dict, Optional, Tuple
|
| 9 |
+
|
| 10 |
+
class WhatsAppImprovedChatbot:
|
| 11 |
+
"""Enhanced chatbot with intelligent product handling for WhatsApp"""
|
| 12 |
+
|
| 13 |
+
def __init__(self, products: List[Tuple]):
|
| 14 |
+
"""Initialize with product list"""
|
| 15 |
+
self.search_engine = ProductSearchEngine(products)
|
| 16 |
+
self.products = products
|
| 17 |
+
|
| 18 |
+
def detect_product_query(self, message: str) -> bool:
|
| 19 |
+
"""Detect if message is asking about products"""
|
| 20 |
+
product_keywords = [
|
| 21 |
+
'bisiklet', 'bike', 'model', 'fiyat', 'price', 'stok', 'stock',
|
| 22 |
+
'özellikleri', 'features', 'hangi', 'which', 'var mı', 'mevcut',
|
| 23 |
+
'kaç', 'how much', 'ne kadar', 'kampanya', 'indirim', 'discount',
|
| 24 |
+
'karşılaştır', 'compare', 'benzer', 'similar', 'alternatif',
|
| 25 |
+
'öneri', 'recommend', 'tavsiye', 'suggest'
|
| 26 |
+
]
|
| 27 |
+
|
| 28 |
+
message_lower = message.lower()
|
| 29 |
+
|
| 30 |
+
# Check for product keywords
|
| 31 |
+
if any(keyword in message_lower for keyword in product_keywords):
|
| 32 |
+
return True
|
| 33 |
+
|
| 34 |
+
# Check for model numbers
|
| 35 |
+
if re.search(r'\b\d+\.?\d*\b', message):
|
| 36 |
+
return True
|
| 37 |
+
|
| 38 |
+
# Check for known product categories
|
| 39 |
+
categories = ['trek', 'marlin', 'fuel', 'slash', 'remedy', 'checkpoint',
|
| 40 |
+
'domane', 'madone', 'emonda', 'fx', 'dual', 'wahoo', 'powerfly']
|
| 41 |
+
if any(cat in message_lower for cat in categories):
|
| 42 |
+
return True
|
| 43 |
+
|
| 44 |
+
return False
|
| 45 |
+
|
| 46 |
+
def extract_query_intent(self, message: str) -> Dict:
|
| 47 |
+
"""Extract the intent from user query"""
|
| 48 |
+
intent = {
|
| 49 |
+
'type': 'general',
|
| 50 |
+
'action': None,
|
| 51 |
+
'filters': {}
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
message_lower = message.lower()
|
| 55 |
+
|
| 56 |
+
# Price query
|
| 57 |
+
if any(word in message_lower for word in ['fiyat', 'price', 'kaç', 'ne kadar', 'how much']):
|
| 58 |
+
intent['type'] = 'price'
|
| 59 |
+
intent['action'] = 'get_price'
|
| 60 |
+
|
| 61 |
+
# Stock query
|
| 62 |
+
elif any(word in message_lower for word in ['stok', 'stock', 'var mı', 'mevcut', 'available']):
|
| 63 |
+
intent['type'] = 'stock'
|
| 64 |
+
intent['action'] = 'check_stock'
|
| 65 |
+
|
| 66 |
+
# Comparison query
|
| 67 |
+
elif any(word in message_lower for word in ['karşılaştır', 'compare', 'fark', 'difference']):
|
| 68 |
+
intent['type'] = 'comparison'
|
| 69 |
+
intent['action'] = 'compare_products'
|
| 70 |
+
|
| 71 |
+
# Recommendation query
|
| 72 |
+
elif any(word in message_lower for word in ['öneri', 'recommend', 'tavsiye', 'suggest', 'hangi']):
|
| 73 |
+
intent['type'] = 'recommendation'
|
| 74 |
+
intent['action'] = 'recommend'
|
| 75 |
+
|
| 76 |
+
# Feature query
|
| 77 |
+
elif any(word in message_lower for word in ['özellik', 'feature', 'spec', 'detay', 'detail']):
|
| 78 |
+
intent['type'] = 'features'
|
| 79 |
+
intent['action'] = 'get_features'
|
| 80 |
+
|
| 81 |
+
# Extract context (size, color, type, etc.)
|
| 82 |
+
context = self.search_engine.extract_product_context(message)
|
| 83 |
+
intent['filters'] = context
|
| 84 |
+
|
| 85 |
+
return intent
|
| 86 |
+
|
| 87 |
+
def format_product_info_whatsapp(self, product: Tuple, intent: Dict) -> str:
|
| 88 |
+
"""Format product information for WhatsApp"""
|
| 89 |
+
name = product[2] # Full name
|
| 90 |
+
info = product[1] # Product info array
|
| 91 |
+
|
| 92 |
+
# Basic info
|
| 93 |
+
stock_status = info[0] if len(info) > 0 else "bilinmiyor"
|
| 94 |
+
|
| 95 |
+
# Build response for WhatsApp (more compact format)
|
| 96 |
+
response_parts = []
|
| 97 |
+
|
| 98 |
+
# Product name with status emoji
|
| 99 |
+
if stock_status == "stokta":
|
| 100 |
+
response_parts.append(f"🚴♂️ *{name}*")
|
| 101 |
+
response_parts.append("✅ *Stokta mevcut*")
|
| 102 |
+
else:
|
| 103 |
+
response_parts.append(f"🚴♂️ *{name}*")
|
| 104 |
+
response_parts.append("❌ *Stokta yok*")
|
| 105 |
+
|
| 106 |
+
# Price information (only if in stock)
|
| 107 |
+
if intent['type'] in ['price', 'general'] and stock_status == "stokta" and len(info) > 1:
|
| 108 |
+
price = info[1]
|
| 109 |
+
response_parts.append(f"💰 *Fiyat:* {price} TL")
|
| 110 |
+
|
| 111 |
+
# Campaign price if available
|
| 112 |
+
if len(info) > 4 and info[4]:
|
| 113 |
+
response_parts.append(f"🎯 *Kampanya:* {info[4]} TL")
|
| 114 |
+
|
| 115 |
+
# Calculate discount
|
| 116 |
+
try:
|
| 117 |
+
normal_price = float(info[1])
|
| 118 |
+
campaign_price = float(info[4])
|
| 119 |
+
discount = normal_price - campaign_price
|
| 120 |
+
if discount > 0:
|
| 121 |
+
response_parts.append(f"💸 *İndirim:* {discount:.0f} TL")
|
| 122 |
+
except:
|
| 123 |
+
pass
|
| 124 |
+
|
| 125 |
+
# Wire transfer price if no campaign
|
| 126 |
+
elif len(info) > 3 and info[3]:
|
| 127 |
+
response_parts.append(f"🏦 *Havale:* {info[3]} TL")
|
| 128 |
+
|
| 129 |
+
# Product link (shortened for WhatsApp)
|
| 130 |
+
if len(info) > 2 and info[2]:
|
| 131 |
+
response_parts.append(f"🔗 Ürün sayfası: {info[2]}")
|
| 132 |
+
|
| 133 |
+
return "\n".join(response_parts)
|
| 134 |
+
|
| 135 |
+
def generate_product_response(self, message: str) -> str:
|
| 136 |
+
"""Generate response for product queries optimized for WhatsApp"""
|
| 137 |
+
intent = self.extract_query_intent(message)
|
| 138 |
+
|
| 139 |
+
# Search for products
|
| 140 |
+
search_results = self.search_engine.search(message)
|
| 141 |
+
|
| 142 |
+
if not search_results:
|
| 143 |
+
# No products found - provide helpful response
|
| 144 |
+
response = self._handle_no_results_whatsapp(message)
|
| 145 |
+
elif len(search_results) == 1:
|
| 146 |
+
# Single product found
|
| 147 |
+
product = search_results[0][1]
|
| 148 |
+
response = self.format_product_info_whatsapp(product, intent)
|
| 149 |
+
elif search_results[0][0] > 0.9:
|
| 150 |
+
# Very high confidence match
|
| 151 |
+
product = search_results[0][1]
|
| 152 |
+
response = self.format_product_info_whatsapp(product, intent)
|
| 153 |
+
else:
|
| 154 |
+
# Multiple products found
|
| 155 |
+
response = self._handle_multiple_results_whatsapp(search_results[:3], intent) # Only 3 for WhatsApp
|
| 156 |
+
|
| 157 |
+
return response
|
| 158 |
+
|
| 159 |
+
def _handle_no_results_whatsapp(self, query: str) -> str:
|
| 160 |
+
"""Handle case when no products are found - WhatsApp format"""
|
| 161 |
+
response_parts = ["🤔 *Aradığınız ürünü bulamadım.*"]
|
| 162 |
+
|
| 163 |
+
# Get suggestions
|
| 164 |
+
suggestions = self.search_engine.generate_suggestions(query)
|
| 165 |
+
|
| 166 |
+
if suggestions:
|
| 167 |
+
response_parts.append("\n💡 *Belki şunları arıyor olabilirsiniz:*")
|
| 168 |
+
for i, suggestion in enumerate(suggestions[:2], 1): # Only 2 suggestions for WhatsApp
|
| 169 |
+
response_parts.append(f"{i}. {suggestion}")
|
| 170 |
+
else:
|
| 171 |
+
response_parts.append("\n📝 *Örnek aramalar:*")
|
| 172 |
+
response_parts.append("• Marlin 5")
|
| 173 |
+
response_parts.append("• FX 3 Disc")
|
| 174 |
+
response_parts.append("• Checkpoint ALR 5")
|
| 175 |
+
|
| 176 |
+
return "\n".join(response_parts)
|
| 177 |
+
|
| 178 |
+
def _handle_multiple_results_whatsapp(self, results: List[Tuple[float, Tuple]], intent: Dict) -> str:
|
| 179 |
+
"""Handle multiple product results - WhatsApp format"""
|
| 180 |
+
response_parts = ["📋 *Birden fazla ürün buldum:*\n"]
|
| 181 |
+
|
| 182 |
+
for i, (score, product) in enumerate(results, 1):
|
| 183 |
+
name = product[2]
|
| 184 |
+
stock = product[1][0] if len(product[1]) > 0 else "bilinmiyor"
|
| 185 |
+
|
| 186 |
+
# Compact info line for WhatsApp
|
| 187 |
+
if stock == "stokta":
|
| 188 |
+
status_emoji = "✅"
|
| 189 |
+
response_parts.append(f"*{i}. {name}* {status_emoji}")
|
| 190 |
+
|
| 191 |
+
# Add price if intent is price-related
|
| 192 |
+
if intent['type'] in ['price', 'general'] and len(product[1]) > 1:
|
| 193 |
+
price = product[1][1]
|
| 194 |
+
response_parts.append(f" 💰 {price} TL")
|
| 195 |
+
|
| 196 |
+
# Show campaign price if available
|
| 197 |
+
if len(product[1]) > 4 and product[1][4]:
|
| 198 |
+
response_parts.append(f" 🎯 Kampanya: {product[1][4]} TL")
|
| 199 |
+
|
| 200 |
+
# Add product link if available
|
| 201 |
+
if len(product[1]) > 2 and product[1][2]:
|
| 202 |
+
response_parts.append(f" 🔗 {product[1][2]}")
|
| 203 |
+
else:
|
| 204 |
+
status_emoji = "❌"
|
| 205 |
+
response_parts.append(f"*{i}. {name}* {status_emoji}")
|
| 206 |
+
|
| 207 |
+
response_parts.append("") # Empty line between products
|
| 208 |
+
|
| 209 |
+
response_parts.append("💡 _Daha detaylı bilgi için ürün adını yazın_")
|
| 210 |
+
|
| 211 |
+
return "\n".join(response_parts)
|
| 212 |
+
|
| 213 |
+
def handle_comparison_whatsapp(self, message: str) -> Optional[str]:
|
| 214 |
+
"""Handle product comparison requests - WhatsApp format"""
|
| 215 |
+
# Extract product names for comparison
|
| 216 |
+
comparison_words = ['ile', 've', 'vs', 'versus', 'karşılaştır', 'arasında']
|
| 217 |
+
|
| 218 |
+
products_to_compare = []
|
| 219 |
+
|
| 220 |
+
# Try to extract two product names
|
| 221 |
+
for word in comparison_words:
|
| 222 |
+
if word in message.lower():
|
| 223 |
+
parts = message.lower().split(word)
|
| 224 |
+
if len(parts) >= 2:
|
| 225 |
+
# Search for each part
|
| 226 |
+
for part in parts[:2]:
|
| 227 |
+
result = self.search_engine.find_best_match(part.strip())
|
| 228 |
+
if result:
|
| 229 |
+
products_to_compare.append(result)
|
| 230 |
+
|
| 231 |
+
if len(products_to_compare) < 2:
|
| 232 |
+
# Try to find products mentioned in the message
|
| 233 |
+
search_results = self.search_engine.search(message)
|
| 234 |
+
products_to_compare = [r[1] for r in search_results[:2] if r[0] > 0.6]
|
| 235 |
+
|
| 236 |
+
if len(products_to_compare) >= 2:
|
| 237 |
+
return self._format_comparison_whatsapp(products_to_compare[:2])
|
| 238 |
+
|
| 239 |
+
return None
|
| 240 |
+
|
| 241 |
+
def _format_comparison_whatsapp(self, products: List[Tuple]) -> str:
|
| 242 |
+
"""Format product comparison for WhatsApp"""
|
| 243 |
+
response_parts = ["📊 *Ürün Karşılaştırması*\n"]
|
| 244 |
+
|
| 245 |
+
for i, product in enumerate(products, 1):
|
| 246 |
+
name = product[2]
|
| 247 |
+
info = product[1]
|
| 248 |
+
|
| 249 |
+
response_parts.append(f"*{i}. {name}*")
|
| 250 |
+
|
| 251 |
+
# Stock status
|
| 252 |
+
stock = info[0] if len(info) > 0 else "bilinmiyor"
|
| 253 |
+
if stock == "stokta":
|
| 254 |
+
response_parts.append("✅ Stokta mevcut")
|
| 255 |
+
else:
|
| 256 |
+
response_parts.append("❌ Stokta yok")
|
| 257 |
+
|
| 258 |
+
# Price (compact for WhatsApp)
|
| 259 |
+
if stock == "stokta" and len(info) > 1:
|
| 260 |
+
price = info[1]
|
| 261 |
+
response_parts.append(f"💰 {price} TL")
|
| 262 |
+
|
| 263 |
+
if len(info) > 4 and info[4]:
|
| 264 |
+
response_parts.append(f"🎯 Kampanya: {info[4]} TL")
|
| 265 |
+
|
| 266 |
+
response_parts.append("") # Empty line
|
| 267 |
+
|
| 268 |
+
return "\n".join(response_parts)
|
| 269 |
+
|
| 270 |
+
def process_message(self, message: str) -> Dict:
|
| 271 |
+
"""Process user message and return structured response optimized for WhatsApp"""
|
| 272 |
+
result = {
|
| 273 |
+
'is_product_query': False,
|
| 274 |
+
'response': None,
|
| 275 |
+
'products_found': [],
|
| 276 |
+
'intent': None
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
# Check if it's a product query
|
| 280 |
+
if self.detect_product_query(message):
|
| 281 |
+
result['is_product_query'] = True
|
| 282 |
+
result['intent'] = self.extract_query_intent(message)
|
| 283 |
+
|
| 284 |
+
# Check for comparison request
|
| 285 |
+
if result['intent']['type'] == 'comparison':
|
| 286 |
+
comparison_result = self.handle_comparison_whatsapp(message)
|
| 287 |
+
if comparison_result:
|
| 288 |
+
result['response'] = comparison_result
|
| 289 |
+
return result
|
| 290 |
+
|
| 291 |
+
# Generate product response
|
| 292 |
+
result['response'] = self.generate_product_response(message)
|
| 293 |
+
|
| 294 |
+
# Get found products
|
| 295 |
+
search_results = self.search_engine.search(message)
|
| 296 |
+
result['products_found'] = [r[1] for r in search_results[:2] if r[0] > 0.6] # Limit to 2 for WhatsApp
|
| 297 |
+
|
| 298 |
+
return result
|
whatsapp_passive_profiler.py
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
WhatsApp Pasif Profil Sistemi
|
| 4 |
+
Kullanıcıya soru sormadan sohbet analizi ile profil oluşturur
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import re
|
| 8 |
+
import json
|
| 9 |
+
import os
|
| 10 |
+
from datetime import datetime, timedelta
|
| 11 |
+
from typing import Dict, List, Optional, Any
|
| 12 |
+
|
| 13 |
+
class WhatsAppPassiveProfiler:
|
| 14 |
+
"""Sohbet analizi ile kullanıcı profili çıkarır"""
|
| 15 |
+
|
| 16 |
+
def __init__(self):
|
| 17 |
+
self.profiles_file = "user_profiles.json"
|
| 18 |
+
self.profiles = self.load_profiles()
|
| 19 |
+
|
| 20 |
+
# Bütçe ifadeleri
|
| 21 |
+
self.budget_patterns = [
|
| 22 |
+
r'bütçem?\s*(\d+)[\s-]*(\d+)?\s*k?\s*(bin|bin tl|tl)?',
|
| 23 |
+
r'(\d+)[\s-]*(\d+)?\s*k?\s*(bin|bin tl|tl)?\s*bütçe',
|
| 24 |
+
r'(\d+)[\s-]*(\d+)?\s*k?\s*(bin|bin tl|tl)?\s*arasında',
|
| 25 |
+
r'maksimum\s*(\d+)\s*k?\s*(bin|bin tl|tl)?',
|
| 26 |
+
r'en fazla\s*(\d+)\s*k?\s*(bin|bin tl|tl)?'
|
| 27 |
+
]
|
| 28 |
+
|
| 29 |
+
# Kategori tercihleri
|
| 30 |
+
self.category_keywords = {
|
| 31 |
+
"dağ_bisikleti": ["dağ", "dag", "offroad", "patika", "doğa", "orman", "marlin", "fuel", "procaliber"],
|
| 32 |
+
"yol_bisikleti": ["yol", "asfalt", "hız", "yarış", "triathlon", "émonda", "madone", "domane"],
|
| 33 |
+
"şehir_bisikleti": ["şehir", "kent", "günlük", "işe gidip gelme", "fx", "ds", "verve"],
|
| 34 |
+
"elektrikli": ["elektrikli", "electric", "e-bike", "ebike", "batarya", "powerfly", "rail"],
|
| 35 |
+
"gravel": ["gravel", "çakıl", "macera", "touring", "checkpoint"]
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
# Kullanım amaçları
|
| 39 |
+
self.usage_patterns = {
|
| 40 |
+
"spor": ["spor", "antrenman", "kondisyon", "fitness", "egzersiz", "yarış"],
|
| 41 |
+
"günlük": ["işe gitmek", "günlük", "şehir içi", "ulaşım", "market", "alışveriş"],
|
| 42 |
+
"hobi": ["hobi", "eğlence", "gezinti", "keyif", "hafta sonu", "macera"],
|
| 43 |
+
"profesyonel": ["profesyonel", "yarış", "müsabaka", "antrenör", "ciddi"]
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
# Boy/beden işaretçileri
|
| 47 |
+
self.size_patterns = {
|
| 48 |
+
"boy": r'boyum\s*(\d+)\s*cm?|(\d+)\s*cm?\s*boy',
|
| 49 |
+
"kilo": r'kilom\s*(\d+)\s*kg?|(\d+)\s*kg?\s*kilo',
|
| 50 |
+
"beden": r'beden[im]?\s*([xsl]+)|([xsl]+)\s*beden'
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
def load_profiles(self) -> Dict:
|
| 54 |
+
"""Mevcut profilleri yükle"""
|
| 55 |
+
if os.path.exists(self.profiles_file):
|
| 56 |
+
try:
|
| 57 |
+
with open(self.profiles_file, 'r', encoding='utf-8') as f:
|
| 58 |
+
return json.load(f)
|
| 59 |
+
except Exception as e:
|
| 60 |
+
print(f"Profil yükleme hatası: {e}")
|
| 61 |
+
return {}
|
| 62 |
+
|
| 63 |
+
def save_profiles(self):
|
| 64 |
+
"""Profilleri kaydet"""
|
| 65 |
+
try:
|
| 66 |
+
with open(self.profiles_file, 'w', encoding='utf-8') as f:
|
| 67 |
+
json.dump(self.profiles, f, ensure_ascii=False, indent=2, default=str)
|
| 68 |
+
except Exception as e:
|
| 69 |
+
print(f"Profil kaydetme hatası: {e}")
|
| 70 |
+
|
| 71 |
+
def get_or_create_profile(self, phone_number: str) -> Dict:
|
| 72 |
+
"""Profil getir veya oluştur"""
|
| 73 |
+
if phone_number not in self.profiles:
|
| 74 |
+
self.profiles[phone_number] = {
|
| 75 |
+
"created_at": datetime.now(),
|
| 76 |
+
"last_updated": datetime.now(),
|
| 77 |
+
"total_messages": 0,
|
| 78 |
+
"preferences": {
|
| 79 |
+
"budget_min": None,
|
| 80 |
+
"budget_max": None,
|
| 81 |
+
"categories": [], # İlgilendiği kategoriler
|
| 82 |
+
"usage_purpose": [], # Kullanım amaçları
|
| 83 |
+
"size_info": {}, # Boy, kilo, beden bilgileri
|
| 84 |
+
"brand_preferences": [], # Marka tercihleri
|
| 85 |
+
"color_preferences": [] # Renk tercihleri
|
| 86 |
+
},
|
| 87 |
+
"behavior": {
|
| 88 |
+
"price_sensitive": False, # Fiyata duyarlı mı
|
| 89 |
+
"tech_interested": False, # Teknik detaylarla ilgilenir mi
|
| 90 |
+
"comparison_lover": False, # Karşılaştırma sever mi
|
| 91 |
+
"quick_decider": False, # Hızlı karar verir mi
|
| 92 |
+
"research_oriented": False # Araştırmacı mı
|
| 93 |
+
},
|
| 94 |
+
"interests": {
|
| 95 |
+
"viewed_products": [], # Baktığı ürünler
|
| 96 |
+
"compared_products": [], # Karşılaştırdığı ürünler
|
| 97 |
+
"favorite_features": [], # İlgilendiği özellikler
|
| 98 |
+
"mentioned_brands": [] # Bahsettiği markalar
|
| 99 |
+
},
|
| 100 |
+
"statistics": {
|
| 101 |
+
"comparison_requests": 0,
|
| 102 |
+
"budget_queries": 0,
|
| 103 |
+
"technical_questions": 0,
|
| 104 |
+
"price_questions": 0,
|
| 105 |
+
"availability_questions": 0
|
| 106 |
+
}
|
| 107 |
+
}
|
| 108 |
+
return self.profiles[phone_number]
|
| 109 |
+
|
| 110 |
+
def analyze_message(self, phone_number: str, message: str) -> Dict:
|
| 111 |
+
"""Mesajı analiz et ve profili güncelle"""
|
| 112 |
+
profile = self.get_or_create_profile(phone_number)
|
| 113 |
+
message_lower = message.lower()
|
| 114 |
+
|
| 115 |
+
# Mesaj sayısını artır
|
| 116 |
+
profile["total_messages"] += 1
|
| 117 |
+
profile["last_updated"] = datetime.now()
|
| 118 |
+
|
| 119 |
+
analysis_results = {
|
| 120 |
+
"budget_detected": False,
|
| 121 |
+
"category_detected": False,
|
| 122 |
+
"usage_detected": False,
|
| 123 |
+
"size_detected": False,
|
| 124 |
+
"behavior_indicators": []
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
# Bütçe analizi
|
| 128 |
+
budget_info = self.extract_budget(message_lower)
|
| 129 |
+
if budget_info:
|
| 130 |
+
profile["preferences"]["budget_min"] = budget_info["min"]
|
| 131 |
+
profile["preferences"]["budget_max"] = budget_info["max"]
|
| 132 |
+
profile["statistics"]["budget_queries"] += 1
|
| 133 |
+
analysis_results["budget_detected"] = True
|
| 134 |
+
|
| 135 |
+
# Kategori tercihi analizi
|
| 136 |
+
detected_categories = self.detect_categories(message_lower)
|
| 137 |
+
if detected_categories:
|
| 138 |
+
for category in detected_categories:
|
| 139 |
+
if category not in profile["preferences"]["categories"]:
|
| 140 |
+
profile["preferences"]["categories"].append(category)
|
| 141 |
+
analysis_results["category_detected"] = True
|
| 142 |
+
|
| 143 |
+
# Kullanım amacı analizi
|
| 144 |
+
usage_purposes = self.detect_usage_purpose(message_lower)
|
| 145 |
+
if usage_purposes:
|
| 146 |
+
for purpose in usage_purposes:
|
| 147 |
+
if purpose not in profile["preferences"]["usage_purpose"]:
|
| 148 |
+
profile["preferences"]["usage_purpose"].append(purpose)
|
| 149 |
+
analysis_results["usage_detected"] = True
|
| 150 |
+
|
| 151 |
+
# Boy/beden bilgisi analizi
|
| 152 |
+
size_info = self.extract_size_info(message_lower)
|
| 153 |
+
if size_info:
|
| 154 |
+
profile["preferences"]["size_info"].update(size_info)
|
| 155 |
+
analysis_results["size_detected"] = True
|
| 156 |
+
|
| 157 |
+
# Davranış analizi
|
| 158 |
+
behavior_indicators = self.analyze_behavior(message_lower)
|
| 159 |
+
for behavior, detected in behavior_indicators.items():
|
| 160 |
+
if detected:
|
| 161 |
+
profile["behavior"][behavior] = True
|
| 162 |
+
analysis_results["behavior_indicators"].append(behavior)
|
| 163 |
+
|
| 164 |
+
# İstatistik güncelleme
|
| 165 |
+
self.update_statistics(profile, message_lower)
|
| 166 |
+
|
| 167 |
+
# Profili kaydet
|
| 168 |
+
self.save_profiles()
|
| 169 |
+
|
| 170 |
+
return analysis_results
|
| 171 |
+
|
| 172 |
+
def extract_budget(self, message: str) -> Optional[Dict]:
|
| 173 |
+
"""Bütçe bilgisini çıkar"""
|
| 174 |
+
for pattern in self.budget_patterns:
|
| 175 |
+
match = re.search(pattern, message)
|
| 176 |
+
if match:
|
| 177 |
+
numbers = [g for g in match.groups() if g and g.isdigit()]
|
| 178 |
+
if numbers:
|
| 179 |
+
if len(numbers) == 1:
|
| 180 |
+
# Tek sayı - maksimum bütçe
|
| 181 |
+
budget = int(numbers[0])
|
| 182 |
+
if budget < 1000: # K formatında (50k = 50000)
|
| 183 |
+
budget *= 1000
|
| 184 |
+
return {"min": int(budget * 0.7), "max": budget}
|
| 185 |
+
elif len(numbers) >= 2:
|
| 186 |
+
# Aralık - min ve max
|
| 187 |
+
min_budget = int(numbers[0])
|
| 188 |
+
max_budget = int(numbers[1])
|
| 189 |
+
if min_budget < 1000:
|
| 190 |
+
min_budget *= 1000
|
| 191 |
+
if max_budget < 1000:
|
| 192 |
+
max_budget *= 1000
|
| 193 |
+
return {"min": min_budget, "max": max_budget}
|
| 194 |
+
return None
|
| 195 |
+
|
| 196 |
+
def detect_categories(self, message: str) -> List[str]:
|
| 197 |
+
"""Kategori tercihlerini tespit et"""
|
| 198 |
+
detected = []
|
| 199 |
+
for category, keywords in self.category_keywords.items():
|
| 200 |
+
if any(keyword in message for keyword in keywords):
|
| 201 |
+
detected.append(category)
|
| 202 |
+
return detected
|
| 203 |
+
|
| 204 |
+
def detect_usage_purpose(self, message: str) -> List[str]:
|
| 205 |
+
"""Kullanım amacını tespit et"""
|
| 206 |
+
detected = []
|
| 207 |
+
for purpose, keywords in self.usage_patterns.items():
|
| 208 |
+
if any(keyword in message for keyword in keywords):
|
| 209 |
+
detected.append(purpose)
|
| 210 |
+
return detected
|
| 211 |
+
|
| 212 |
+
def extract_size_info(self, message: str) -> Dict:
|
| 213 |
+
"""Boy/beden bilgisini çıkar"""
|
| 214 |
+
size_info = {}
|
| 215 |
+
|
| 216 |
+
# Boy bilgisi
|
| 217 |
+
boy_match = re.search(self.size_patterns["boy"], message)
|
| 218 |
+
if boy_match:
|
| 219 |
+
boy = boy_match.group(1) or boy_match.group(2)
|
| 220 |
+
if boy:
|
| 221 |
+
size_info["height"] = int(boy)
|
| 222 |
+
|
| 223 |
+
# Kilo bilgisi
|
| 224 |
+
kilo_match = re.search(self.size_patterns["kilo"], message)
|
| 225 |
+
if kilo_match:
|
| 226 |
+
kilo = kilo_match.group(1) or kilo_match.group(2)
|
| 227 |
+
if kilo:
|
| 228 |
+
size_info["weight"] = int(kilo)
|
| 229 |
+
|
| 230 |
+
# Beden bilgisi
|
| 231 |
+
beden_match = re.search(self.size_patterns["beden"], message)
|
| 232 |
+
if beden_match:
|
| 233 |
+
beden = beden_match.group(1) or beden_match.group(2)
|
| 234 |
+
if beden:
|
| 235 |
+
size_info["size"] = beden.upper()
|
| 236 |
+
|
| 237 |
+
return size_info
|
| 238 |
+
|
| 239 |
+
def analyze_behavior(self, message: str) -> Dict[str, bool]:
|
| 240 |
+
"""Davranış kalıplarını analiz et"""
|
| 241 |
+
behavior = {}
|
| 242 |
+
|
| 243 |
+
# Fiyata duyarlılık
|
| 244 |
+
price_keywords = ["ucuz", "fiyat", "indirim", "kampanya", "ekonomik", "bütçe"]
|
| 245 |
+
behavior["price_sensitive"] = any(keyword in message for keyword in price_keywords)
|
| 246 |
+
|
| 247 |
+
# Teknik ilgi
|
| 248 |
+
tech_keywords = ["teknik", "özellik", "ağırlık", "malzeme", "karbon", "alüminyum", "vites"]
|
| 249 |
+
behavior["tech_interested"] = any(keyword in message for keyword in tech_keywords)
|
| 250 |
+
|
| 251 |
+
# Karşılaştırma sevgisi
|
| 252 |
+
comparison_keywords = ["karşılaştır", "fark", "hangisi", "arası", "seçim"]
|
| 253 |
+
behavior["comparison_lover"] = any(keyword in message for keyword in comparison_keywords)
|
| 254 |
+
|
| 255 |
+
# Araştırmacı yapı
|
| 256 |
+
research_keywords = ["detay", "bilgi", "araştır", "inceleme", "test", "deneyim"]
|
| 257 |
+
behavior["research_oriented"] = any(keyword in message for keyword in research_keywords)
|
| 258 |
+
|
| 259 |
+
return behavior
|
| 260 |
+
|
| 261 |
+
def update_statistics(self, profile: Dict, message: str):
|
| 262 |
+
"""İstatistikleri güncelle"""
|
| 263 |
+
if "karşılaştır" in message:
|
| 264 |
+
profile["statistics"]["comparison_requests"] += 1
|
| 265 |
+
|
| 266 |
+
if any(word in message for word in ["fiyat", "kaç para", "ne kadar"]):
|
| 267 |
+
profile["statistics"]["price_questions"] += 1
|
| 268 |
+
|
| 269 |
+
if any(word in message for word in ["stok", "var mı", "mevcut"]):
|
| 270 |
+
profile["statistics"]["availability_questions"] += 1
|
| 271 |
+
|
| 272 |
+
if any(word in message for word in ["teknik", "özellik", "detay"]):
|
| 273 |
+
profile["statistics"]["technical_questions"] += 1
|
| 274 |
+
|
| 275 |
+
def get_profile_summary(self, phone_number: str) -> Dict:
|
| 276 |
+
"""Profil özetini döndür"""
|
| 277 |
+
if phone_number not in self.profiles:
|
| 278 |
+
return {"exists": False}
|
| 279 |
+
|
| 280 |
+
profile = self.profiles[phone_number]
|
| 281 |
+
|
| 282 |
+
# Profil güvenilirlik skoru (mesaj sayısına göre)
|
| 283 |
+
confidence = min(profile["total_messages"] / 10.0, 1.0) # 10 mesajda %100
|
| 284 |
+
|
| 285 |
+
summary = {
|
| 286 |
+
"exists": True,
|
| 287 |
+
"confidence": confidence,
|
| 288 |
+
"total_messages": profile["total_messages"],
|
| 289 |
+
"preferences": profile["preferences"],
|
| 290 |
+
"behavior": profile["behavior"],
|
| 291 |
+
"top_categories": profile["preferences"]["categories"][:3],
|
| 292 |
+
"is_budget_defined": profile["preferences"]["budget_min"] is not None,
|
| 293 |
+
"is_tech_savvy": profile["behavior"]["tech_interested"],
|
| 294 |
+
"is_price_conscious": profile["behavior"]["price_sensitive"],
|
| 295 |
+
"interaction_style": self.get_interaction_style(profile)
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
return summary
|
| 299 |
+
|
| 300 |
+
def get_interaction_style(self, profile: Dict) -> str:
|
| 301 |
+
"""Etkileşim stilini belirle"""
|
| 302 |
+
stats = profile["statistics"]
|
| 303 |
+
behavior = profile["behavior"]
|
| 304 |
+
|
| 305 |
+
if stats["comparison_requests"] > 2 and behavior["research_oriented"]:
|
| 306 |
+
return "analytical" # Analitik - detaylı bilgi sever
|
| 307 |
+
elif behavior["price_sensitive"] and stats["budget_queries"] > 0:
|
| 308 |
+
return "budget_conscious" # Bütçe odaklı
|
| 309 |
+
elif behavior["tech_interested"] and stats["technical_questions"] > 1:
|
| 310 |
+
return "technical" # Teknik odaklı
|
| 311 |
+
elif stats["comparison_requests"] == 0 and profile["total_messages"] < 5:
|
| 312 |
+
return "decisive" # Hızlı karar verici
|
| 313 |
+
else:
|
| 314 |
+
return "balanced" # Dengeli
|
| 315 |
+
|
| 316 |
+
def get_personalized_suggestions(self, phone_number: str, products_data: List) -> Dict:
|
| 317 |
+
"""Kişiselleştirilmiş öneriler"""
|
| 318 |
+
profile_summary = self.get_profile_summary(phone_number)
|
| 319 |
+
|
| 320 |
+
if not profile_summary["exists"] or profile_summary["confidence"] < 0.3:
|
| 321 |
+
return {"personalized": False, "reason": "insufficient_data"}
|
| 322 |
+
|
| 323 |
+
suggestions = {
|
| 324 |
+
"personalized": True,
|
| 325 |
+
"user_style": profile_summary["interaction_style"],
|
| 326 |
+
"budget_aware": profile_summary["is_budget_defined"],
|
| 327 |
+
"recommendations": []
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
# Bütçe filtresi
|
| 331 |
+
filtered_products = products_data
|
| 332 |
+
if profile_summary["preferences"]["budget_min"]:
|
| 333 |
+
budget_min = profile_summary["preferences"]["budget_min"]
|
| 334 |
+
budget_max = profile_summary["preferences"]["budget_max"]
|
| 335 |
+
filtered_products = [
|
| 336 |
+
p for p in products_data
|
| 337 |
+
if p[1][0] == "stokta" and p[1][1] and
|
| 338 |
+
budget_min <= float(p[1][1]) <= budget_max
|
| 339 |
+
]
|
| 340 |
+
|
| 341 |
+
# Kategori filtresi
|
| 342 |
+
if profile_summary["top_categories"]:
|
| 343 |
+
category_products = []
|
| 344 |
+
for category in profile_summary["top_categories"]:
|
| 345 |
+
category_products.extend([
|
| 346 |
+
p for p in filtered_products
|
| 347 |
+
if any(keyword in p[2].lower() for keyword in self.category_keywords.get(category, []))
|
| 348 |
+
])
|
| 349 |
+
if category_products:
|
| 350 |
+
filtered_products = category_products
|
| 351 |
+
|
| 352 |
+
suggestions["recommendations"] = filtered_products[:5]
|
| 353 |
+
return suggestions
|
| 354 |
+
|
| 355 |
+
# Global instance
|
| 356 |
+
passive_profiler = WhatsAppPassiveProfiler()
|
| 357 |
+
|
| 358 |
+
def analyze_user_message(phone_number: str, message: str) -> Dict:
|
| 359 |
+
"""Kullanıcı mesajını analiz et ve profili güncelle"""
|
| 360 |
+
return passive_profiler.analyze_message(phone_number, message)
|
| 361 |
+
|
| 362 |
+
def get_user_profile_summary(phone_number: str) -> Dict:
|
| 363 |
+
"""Kullanıcı profil özetini getir"""
|
| 364 |
+
return passive_profiler.get_profile_summary(phone_number)
|
| 365 |
+
|
| 366 |
+
def get_personalized_recommendations(phone_number: str, products_data: List) -> Dict:
|
| 367 |
+
"""Kişiselleştirilmiş öneriler getir"""
|
| 368 |
+
return passive_profiler.get_personalized_suggestions(phone_number, products_data)
|
whatsapp_renderer.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
WhatsApp Ürün Resimlerini ve Karşılaştırmalarını Gösterme Sistemi
|
| 4 |
+
WhatsApp için basitleştirilmiş versiyon - Sadece URL paylaşımı
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
def round_price(price_str):
|
| 8 |
+
"""Fiyatı yuvarlama formülüne göre yuvarla"""
|
| 9 |
+
try:
|
| 10 |
+
# TL ve diğer karakterleri temizle
|
| 11 |
+
price_clean = price_str.replace(' TL', '').replace(',', '.')
|
| 12 |
+
price_float = float(price_clean)
|
| 13 |
+
|
| 14 |
+
# Fiyat 200000 üzerindeyse en yakın 5000'lik basamağa yuvarla
|
| 15 |
+
if price_float > 200000:
|
| 16 |
+
return str(round(price_float / 5000) * 5000)
|
| 17 |
+
# Fiyat 30000 üzerindeyse en yakın 1000'lik basamağa yuvarla
|
| 18 |
+
elif price_float > 30000:
|
| 19 |
+
return str(round(price_float / 1000) * 1000)
|
| 20 |
+
# Fiyat 10000 üzerindeyse en yakın 100'lük basamağa yuvarla
|
| 21 |
+
elif price_float > 10000:
|
| 22 |
+
return str(round(price_float / 100) * 100)
|
| 23 |
+
# Diğer durumlarda en yakın 10'luk basamağa yuvarla
|
| 24 |
+
else:
|
| 25 |
+
return str(round(price_float / 10) * 10)
|
| 26 |
+
except (ValueError, TypeError):
|
| 27 |
+
return price_str
|
| 28 |
+
|
| 29 |
+
def format_message_with_images_whatsapp(message):
|
| 30 |
+
"""WhatsApp için mesajdaki resim URL'lerini formatla"""
|
| 31 |
+
if "Ürün resmi:" not in message:
|
| 32 |
+
return message
|
| 33 |
+
|
| 34 |
+
lines = message.split('\n')
|
| 35 |
+
formatted_lines = []
|
| 36 |
+
|
| 37 |
+
for line in lines:
|
| 38 |
+
if line.startswith("Ürün resmi:"):
|
| 39 |
+
image_url = line.replace("Ürün resmi:", "").strip()
|
| 40 |
+
if image_url:
|
| 41 |
+
# WhatsApp için sadece URL'yi ekle - WhatsApp otomatik önizleme yapar
|
| 42 |
+
formatted_lines.append(f"🖼️ Ürün Resmi: {image_url}")
|
| 43 |
+
else:
|
| 44 |
+
formatted_lines.append(line)
|
| 45 |
+
else:
|
| 46 |
+
formatted_lines.append(line)
|
| 47 |
+
|
| 48 |
+
return '\n'.join(formatted_lines)
|
| 49 |
+
|
| 50 |
+
def create_product_comparison_whatsapp(products_with_info):
|
| 51 |
+
"""WhatsApp için ürün karşılaştırması oluştur"""
|
| 52 |
+
if not products_with_info:
|
| 53 |
+
return ""
|
| 54 |
+
|
| 55 |
+
comparison_text = "📊 **ÜRÜN KARŞILAŞTIRMASI**\n\n"
|
| 56 |
+
|
| 57 |
+
for i, product in enumerate(products_with_info, 1):
|
| 58 |
+
name = product.get('name', 'Bilinmeyen')
|
| 59 |
+
price = product.get('price', 'Fiyat yok')
|
| 60 |
+
stock = product.get('stock', 'Stok bilgisi yok')
|
| 61 |
+
product_url = product.get('product_url', '')
|
| 62 |
+
image_url = product.get('image_url', '')
|
| 63 |
+
|
| 64 |
+
comparison_text += f"**{i}. {name}**\n"
|
| 65 |
+
comparison_text += f"💰 Fiyat: {price}\n"
|
| 66 |
+
comparison_text += f"📦 Stok: {stock}\n"
|
| 67 |
+
|
| 68 |
+
if image_url:
|
| 69 |
+
comparison_text += f"🖼️ Resim: {image_url}\n"
|
| 70 |
+
|
| 71 |
+
if product_url:
|
| 72 |
+
comparison_text += f"🔗 Link: {product_url}\n"
|
| 73 |
+
|
| 74 |
+
comparison_text += "\n" + "─" * 30 + "\n\n"
|
| 75 |
+
|
| 76 |
+
return comparison_text
|
| 77 |
+
|
| 78 |
+
def extract_product_info_whatsapp(message):
|
| 79 |
+
"""WhatsApp mesajından ürün bilgilerini çıkar ve formatla"""
|
| 80 |
+
# Önce resim URL'lerini formatla
|
| 81 |
+
formatted_message = format_message_with_images_whatsapp(message)
|
| 82 |
+
|
| 83 |
+
# Karşılaştırma veya öneri mesajı ise özel formatla
|
| 84 |
+
if any(keyword in message.lower() for keyword in ["karşılaştır", "öneri", "seçenek", "alternatif", "bütçe"]):
|
| 85 |
+
# Ürün listesi var mı kontrol et
|
| 86 |
+
lines = message.split('\n')
|
| 87 |
+
products = []
|
| 88 |
+
current_product = {}
|
| 89 |
+
|
| 90 |
+
for line in lines:
|
| 91 |
+
line = line.strip()
|
| 92 |
+
if line.startswith('•') and any(keyword in line.lower() for keyword in ['marlin', 'émonda', 'madone', 'domane', 'fuel', 'powerfly', 'fx']):
|
| 93 |
+
# Yeni ürün başladı
|
| 94 |
+
if current_product:
|
| 95 |
+
products.append(current_product)
|
| 96 |
+
|
| 97 |
+
# Ürün adı ve fiyatı parse et
|
| 98 |
+
parts = line.split(' - ')
|
| 99 |
+
name = parts[0].replace('•', '').strip()
|
| 100 |
+
price_raw = parts[1] if len(parts) > 1 else 'Fiyat yok'
|
| 101 |
+
|
| 102 |
+
# Fiyatı yuvarlama formülüne göre yuvarla
|
| 103 |
+
if price_raw != 'Fiyat yok':
|
| 104 |
+
price = round_price(price_raw) + ' TL'
|
| 105 |
+
else:
|
| 106 |
+
price = price_raw
|
| 107 |
+
|
| 108 |
+
current_product = {
|
| 109 |
+
'name': name,
|
| 110 |
+
'price': price,
|
| 111 |
+
'stock': 'stokta',
|
| 112 |
+
'image_url': '',
|
| 113 |
+
'product_url': ''
|
| 114 |
+
}
|
| 115 |
+
elif "Ürün resmi:" in line and current_product:
|
| 116 |
+
current_product['image_url'] = line.replace("Ürün resmi:", "").strip()
|
| 117 |
+
elif "Ürün linki:" in line and current_product:
|
| 118 |
+
current_product['product_url'] = line.replace("Ürün linki:", "").strip()
|
| 119 |
+
|
| 120 |
+
# Son ürünü ekle
|
| 121 |
+
if current_product:
|
| 122 |
+
products.append(current_product)
|
| 123 |
+
|
| 124 |
+
if len(products) > 1:
|
| 125 |
+
# Çoklu ürün karşılaştırması
|
| 126 |
+
comparison = create_product_comparison_whatsapp(products)
|
| 127 |
+
# Orijinal mesajdaki resim linklerini temizle
|
| 128 |
+
cleaned_message = message
|
| 129 |
+
for line in message.split('\n'):
|
| 130 |
+
if line.startswith("Ürün resmi:") or line.startswith("Ürün linki:"):
|
| 131 |
+
cleaned_message = cleaned_message.replace(line, "")
|
| 132 |
+
|
| 133 |
+
return cleaned_message.strip() + "\n\n" + comparison
|
| 134 |
+
|
| 135 |
+
return formatted_message
|
| 136 |
+
|
| 137 |
+
def should_use_comparison_format_whatsapp(message):
|
| 138 |
+
"""WhatsApp mesajının karşılaştırma formatı kullanması gerekip gerekmediğini kontrol et"""
|
| 139 |
+
comparison_keywords = ["karşılaştır", "öneri", "seçenek", "alternatif", "bütçe", "compare"]
|
| 140 |
+
return any(keyword in message.lower() for keyword in comparison_keywords)
|
| 141 |
+
|
| 142 |
+
def format_whatsapp_product_list(products, title="Ürün Listesi"):
|
| 143 |
+
"""WhatsApp için ürün listesini formatla"""
|
| 144 |
+
if not products:
|
| 145 |
+
return f"❌ {title} - Uygun ürün bulunamadı."
|
| 146 |
+
|
| 147 |
+
text = f"🚲 **{title.upper()}**\n\n"
|
| 148 |
+
|
| 149 |
+
for i, product in enumerate(products, 1):
|
| 150 |
+
name, item_info, full_name = product
|
| 151 |
+
price = item_info[1] if len(item_info) > 1 else "Fiyat yok"
|
| 152 |
+
product_link = item_info[2] if len(item_info) > 2 else ""
|
| 153 |
+
|
| 154 |
+
text += f"**{i}. {full_name}**\n"
|
| 155 |
+
text += f"💰 {price} TL\n"
|
| 156 |
+
|
| 157 |
+
# Resim URL'si varsa ekle
|
| 158 |
+
if len(item_info) > 6 and item_info[6]:
|
| 159 |
+
text += f"🖼️ {item_info[6]}\n"
|
| 160 |
+
|
| 161 |
+
if product_link:
|
| 162 |
+
text += f"🔗 {product_link}\n"
|
| 163 |
+
|
| 164 |
+
text += "\n"
|
| 165 |
+
|
| 166 |
+
return text
|
| 167 |
+
|
| 168 |
+
def format_whatsapp_single_product(product):
|
| 169 |
+
"""WhatsApp için tek ürün formatla"""
|
| 170 |
+
if not product:
|
| 171 |
+
return "❌ Ürün bulunamadı."
|
| 172 |
+
|
| 173 |
+
name, item_info, full_name = product
|
| 174 |
+
|
| 175 |
+
text = f"🚲 **{full_name}**\n\n"
|
| 176 |
+
|
| 177 |
+
# Stok durumu
|
| 178 |
+
stock_status = item_info[0] if len(item_info) > 0 else "Bilgi yok"
|
| 179 |
+
text += f"📦 Stok: {stock_status}\n"
|
| 180 |
+
|
| 181 |
+
if stock_status == "stokta":
|
| 182 |
+
# Fiyat bilgileri
|
| 183 |
+
if len(item_info) > 1 and item_info[1]:
|
| 184 |
+
text += f"💰 Fiyat: {item_info[1]} TL\n"
|
| 185 |
+
|
| 186 |
+
# EFT fiyatı
|
| 187 |
+
if len(item_info) > 3 and item_info[3]:
|
| 188 |
+
text += f"💳 Havale Fiyatı: {item_info[3]} TL\n"
|
| 189 |
+
|
| 190 |
+
# Kampanyalı fiyat
|
| 191 |
+
if len(item_info) > 4 and item_info[4]:
|
| 192 |
+
text += f"🎯 Kampanyalı Fiyat: {item_info[4]} TL\n"
|
| 193 |
+
|
| 194 |
+
# Resim
|
| 195 |
+
if len(item_info) > 6 and item_info[6]:
|
| 196 |
+
text += f"🖼️ {item_info[6]}\n"
|
| 197 |
+
|
| 198 |
+
# Ürün linki
|
| 199 |
+
if len(item_info) > 2 and item_info[2]:
|
| 200 |
+
text += f"🔗 {item_info[2]}\n"
|
| 201 |
+
|
| 202 |
+
return text
|
| 203 |
+
|
| 204 |
+
def clean_whatsapp_message(message):
|
| 205 |
+
"""WhatsApp mesajını temizle ve optimize et"""
|
| 206 |
+
# Gereksiz boşlukları temizle
|
| 207 |
+
lines = [line.strip() for line in message.split('\n') if line.strip()]
|
| 208 |
+
|
| 209 |
+
# Aynı URL'leri tekrar eden satırları kaldır
|
| 210 |
+
seen_urls = set()
|
| 211 |
+
clean_lines = []
|
| 212 |
+
|
| 213 |
+
for line in lines:
|
| 214 |
+
if line.startswith('🖼️') or line.startswith('🔗'):
|
| 215 |
+
url = line.split(' ', 1)[1] if ' ' in line else line
|
| 216 |
+
if url not in seen_urls:
|
| 217 |
+
seen_urls.add(url)
|
| 218 |
+
clean_lines.append(line)
|
| 219 |
+
else:
|
| 220 |
+
clean_lines.append(line)
|
| 221 |
+
|
| 222 |
+
return '\n'.join(clean_lines)
|