Spaces:
Sleeping
Sleeping
SamiKoen commited on
Commit ·
dbd1c5c
1
Parent(s): 829ccbf
Restore gpt-5.5 as primary fuzzy matcher (V1 quality), local + nano as fallback
Browse files- config.py +5 -1
- product_matcher.py +73 -1
- tools.py +20 -7
config.py
CHANGED
|
@@ -6,7 +6,11 @@ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
|
| 6 |
REALTIME_MODEL = "gpt-realtime-2"
|
| 7 |
REALTIME_URL = f"wss://api.openai.com/v1/realtime?model={REALTIME_MODEL}"
|
| 8 |
|
| 9 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
NANO_MODEL = "gpt-5-nano"
|
| 11 |
NANO_TIMEOUT = 6
|
| 12 |
|
|
|
|
| 6 |
REALTIME_MODEL = "gpt-realtime-2"
|
| 7 |
REALTIME_URL = f"wss://api.openai.com/v1/realtime?model={REALTIME_MODEL}"
|
| 8 |
|
| 9 |
+
# Tool icindeki fuzzy matching modeli (primary)
|
| 10 |
+
MATCHER_MODEL = "gpt-5.5"
|
| 11 |
+
MATCHER_TIMEOUT = 12
|
| 12 |
+
|
| 13 |
+
# Fallback model (matcher fail ederse)
|
| 14 |
NANO_MODEL = "gpt-5-nano"
|
| 15 |
NANO_TIMEOUT = 6
|
| 16 |
|
product_matcher.py
CHANGED
|
@@ -15,7 +15,7 @@ import logging
|
|
| 15 |
|
| 16 |
import requests
|
| 17 |
|
| 18 |
-
from config import OPENAI_API_KEY, NANO_MODEL, NANO_TIMEOUT
|
| 19 |
from product_index import get_index, normalize, public_view
|
| 20 |
|
| 21 |
logger = logging.getLogger(__name__)
|
|
@@ -144,6 +144,78 @@ def extract_product_link_from_text(text: str) -> str | None:
|
|
| 144 |
return m.group(0) if m else None
|
| 145 |
|
| 146 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
def nano_fallback_match(query: str, products_summary: list[dict]) -> str:
|
| 148 |
"""Local matcher hicbir sey bulamazsa, gpt-5-nano'ya kucuk bir liste gonder.
|
| 149 |
products_summary: [{i: int, n: name, v: variant}, ...]
|
|
|
|
| 15 |
|
| 16 |
import requests
|
| 17 |
|
| 18 |
+
from config import OPENAI_API_KEY, MATCHER_MODEL, MATCHER_TIMEOUT, NANO_MODEL, NANO_TIMEOUT
|
| 19 |
from product_index import get_index, normalize, public_view
|
| 20 |
|
| 21 |
logger = logging.getLogger(__name__)
|
|
|
|
| 144 |
return m.group(0) if m else None
|
| 145 |
|
| 146 |
|
| 147 |
+
def gpt_match(query: str, products_summary: list[dict], asked_warehouse: str | None = None) -> str:
|
| 148 |
+
"""V1'deki gpt-5.5 fuzzy matcher (primary). Yazim hatasi, sira farki, semantik
|
| 149 |
+
eslesme yapar. Donus: virgulle ayrilmis index string'i ('5,12') veya '-1'."""
|
| 150 |
+
import json as _json
|
| 151 |
+
if not OPENAI_API_KEY or not products_summary:
|
| 152 |
+
return "-1"
|
| 153 |
+
|
| 154 |
+
warehouse_filter = ""
|
| 155 |
+
if asked_warehouse:
|
| 156 |
+
warehouse_filter = (
|
| 157 |
+
f"\nIMPORTANT: User is asking specifically about {asked_warehouse} warehouse. "
|
| 158 |
+
f"Only return products available in that warehouse."
|
| 159 |
+
)
|
| 160 |
+
|
| 161 |
+
smart_prompt = f"""User is asking: "{query}"
|
| 162 |
+
|
| 163 |
+
FIRST CHECK: Is this actually a product search?
|
| 164 |
+
- If the message is a question about the system, service, or a general inquiry, return: -1
|
| 165 |
+
- If the message contains "musun", "misin", "neden", "nasıl", etc. it's likely NOT a product search
|
| 166 |
+
- Only proceed if this looks like a genuine product name or model
|
| 167 |
+
|
| 168 |
+
Find ALL products that match this query from the list below.
|
| 169 |
+
If user asks about specific size (S, M, L, XL, XXL, SMALL, MEDIUM, LARGE, X-LARGE), return only that size.
|
| 170 |
+
If user asks generally (without size), return ALL variants of the product.
|
| 171 |
+
{warehouse_filter}
|
| 172 |
+
|
| 173 |
+
CRITICAL TURKISH CHARACTER RULES:
|
| 174 |
+
- "MARLIN" and "MARLİN" are the SAME product (Turkish İ vs I)
|
| 175 |
+
- Treat these as equivalent: I/İ/ı, Ö/ö, Ü/ü, Ş/ş, Ğ/ğ, Ç/ç
|
| 176 |
+
|
| 177 |
+
IMPORTANT BRAND AND PRODUCT TYPE RULES:
|
| 178 |
+
- GOBIK: Spanish textile brand we import. When user asks about "gobik", return ALL products with "GOBIK" in the name.
|
| 179 |
+
- Product names contain type information: FORMA (jersey/cycling shirt), TAYT (tights), İÇLİK (base layer), YAĞMURLUK (raincoat), etc.
|
| 180 |
+
- Understand Turkish/English terms:
|
| 181 |
+
* "erkek forma" / "men's jersey" -> Find products with FORMA in name
|
| 182 |
+
* "tayt" / "tights" -> Find products with TAYT in name
|
| 183 |
+
* "içlik" / "base layer" -> Find products with İÇLİK in name
|
| 184 |
+
* "yağmurluk" / "raincoat" -> Find products with YAĞMURLUK in name
|
| 185 |
+
- Gender: UNISEX means for both men and women.
|
| 186 |
+
|
| 187 |
+
Products list (with warehouse availability):
|
| 188 |
+
{_json.dumps(products_summary, ensure_ascii=False, indent=2)}
|
| 189 |
+
|
| 190 |
+
Return ONLY index numbers of ALL matching products as comma-separated list (e.g., "5,8,12,15").
|
| 191 |
+
If no products found, return ONLY: -1
|
| 192 |
+
DO NOT return empty string or any explanation, ONLY numbers or -1
|
| 193 |
+
"""
|
| 194 |
+
|
| 195 |
+
try:
|
| 196 |
+
r = requests.post(
|
| 197 |
+
"https://api.openai.com/v1/chat/completions",
|
| 198 |
+
headers={
|
| 199 |
+
"Content-Type": "application/json",
|
| 200 |
+
"Authorization": f"Bearer {OPENAI_API_KEY}",
|
| 201 |
+
},
|
| 202 |
+
json={
|
| 203 |
+
"model": MATCHER_MODEL,
|
| 204 |
+
"messages": [
|
| 205 |
+
{"role": "system", "content": "You are a product matcher. Find ALL matching products. Return only index numbers."},
|
| 206 |
+
{"role": "user", "content": smart_prompt},
|
| 207 |
+
],
|
| 208 |
+
},
|
| 209 |
+
timeout=MATCHER_TIMEOUT,
|
| 210 |
+
)
|
| 211 |
+
if r.status_code == 200:
|
| 212 |
+
return r.json()["choices"][0]["message"]["content"].strip()
|
| 213 |
+
logger.warning(f"[matcher/gpt] HTTP {r.status_code}: {r.text[:200]}")
|
| 214 |
+
except Exception as e:
|
| 215 |
+
logger.warning(f"[matcher/gpt] fail: {e}")
|
| 216 |
+
return "-1"
|
| 217 |
+
|
| 218 |
+
|
| 219 |
def nano_fallback_match(query: str, products_summary: list[dict]) -> str:
|
| 220 |
"""Local matcher hicbir sey bulamazsa, gpt-5-nano'ya kucuk bir liste gonder.
|
| 221 |
products_summary: [{i: int, n: name, v: variant}, ...]
|
tools.py
CHANGED
|
@@ -14,7 +14,7 @@ import time
|
|
| 14 |
|
| 15 |
from config import CACHE_TTL_SEARCH
|
| 16 |
from product_index import get_index, normalize
|
| 17 |
-
from product_matcher import nano_fallback_match
|
| 18 |
from stock_service import get_cached_warehouse_xml, search_cache_get, search_cache_set
|
| 19 |
|
| 20 |
logger = logging.getLogger(__name__)
|
|
@@ -208,15 +208,28 @@ def get_warehouse_stock_smart(user_message: str) -> list[str] | None:
|
|
| 208 |
"warehouses": whs_with_stock,
|
| 209 |
})
|
| 210 |
|
| 211 |
-
# 1.
|
| 212 |
-
|
| 213 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
|
| 215 |
-
# 2.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 216 |
if indices_str == "-1":
|
| 217 |
mini = [{"i": s["index"], "n": s["name"], "v": s["variant"]} for s in summary]
|
| 218 |
-
|
| 219 |
-
|
|
|
|
|
|
|
| 220 |
|
| 221 |
if not indices_str or indices_str == "-1":
|
| 222 |
search_cache_set(cache_key, None)
|
|
|
|
| 14 |
|
| 15 |
from config import CACHE_TTL_SEARCH
|
| 16 |
from product_index import get_index, normalize
|
| 17 |
+
from product_matcher import gpt_match, nano_fallback_match
|
| 18 |
from stock_service import get_cached_warehouse_xml, search_cache_get, search_cache_set
|
| 19 |
|
| 20 |
logger = logging.getLogger(__name__)
|
|
|
|
| 208 |
"warehouses": whs_with_stock,
|
| 209 |
})
|
| 210 |
|
| 211 |
+
# 1. PRIMARY: gpt-5.5 fuzzy matcher (yazim hatasi, sira, semantik esnek)
|
| 212 |
+
gpt_summary = [
|
| 213 |
+
{"index": s["index"], "name": s["name"], "variant": s["variant"], "warehouses": s["warehouses"]}
|
| 214 |
+
for s in summary
|
| 215 |
+
]
|
| 216 |
+
indices_str = gpt_match(user_message, gpt_summary, asked_wh)
|
| 217 |
+
logger.info(f"[stock/gpt] indices: {indices_str!r}")
|
| 218 |
|
| 219 |
+
# 2. Local fallback (gpt cagrisi fail ederse)
|
| 220 |
+
if not indices_str or indices_str == "-1":
|
| 221 |
+
local_str = _local_match_indices(user_message, summary, asked_wh)
|
| 222 |
+
if local_str != "-1":
|
| 223 |
+
indices_str = local_str
|
| 224 |
+
logger.info(f"[stock/local-fallback] indices: {indices_str!r}")
|
| 225 |
+
|
| 226 |
+
# 3. Nano son fallback
|
| 227 |
if indices_str == "-1":
|
| 228 |
mini = [{"i": s["index"], "n": s["name"], "v": s["variant"]} for s in summary]
|
| 229 |
+
nano_str = nano_fallback_match(user_message, mini)
|
| 230 |
+
if nano_str != "-1":
|
| 231 |
+
indices_str = nano_str
|
| 232 |
+
logger.info(f"[stock/nano-fallback] indices: {indices_str!r}")
|
| 233 |
|
| 234 |
if not indices_str or indices_str == "-1":
|
| 235 |
search_cache_set(cache_key, None)
|