Commit 4e045633 authored by Vũ Hoàng Anh's avatar Vũ Hoàng Anh

Update : display products using api of webapp

parent 540e3576
...@@ -91,23 +91,11 @@ def _neutralize_generic_print(name: str) -> str: ...@@ -91,23 +91,11 @@ def _neutralize_generic_print(name: str) -> str:
def format_product_results(products: list[dict]) -> list[dict]: def format_product_results(products: list[dict]) -> list[dict]:
""" """
Format products - GROUP by base SKU (magento_ref_code), with multiple colors. Format products - flat list, sku luôn là magento_ref_code, sku_color là mã biến thể màu.
Output format:
{
"sku": "1DS25S008",
"name": "Váy liền bé gái",
"colors": [
{"color": "Cam/ Orange", "color_code": "SO123", "url": "...", "thumbnail": "..."},
{"color": "Xanh/ Blue", "color_code": "SB456", "url": "...", "thumbnail": "..."}
],
"price": 349000,
"sale_price": 244000,
"description": "..."
}
""" """
max_products = 15 max_products = 15
grouped: dict[str, dict] = {} # {magento_ref_code: {product_info}} formatted: list[dict] = []
seen_skus: set[str] = set()
for p in products: for p in products:
# Extract product info # Extract product info
...@@ -135,60 +123,35 @@ def format_product_results(products: list[dict]) -> list[dict]: ...@@ -135,60 +123,35 @@ def format_product_results(products: list[dict]) -> list[dict]:
magento_ref = p.get("magento_ref_code", "") magento_ref = p.get("magento_ref_code", "")
product_color_code = p.get("product_color_code", "") product_color_code = p.get("product_color_code", "")
# Extract color code from product_color_code (VD: 1DS25S008-SO123 → SO123) sku = magento_ref
color_code_only = "" sku_color = product_color_code or magento_ref
if product_color_code and "-" in product_color_code: dedup_key = sku_color or sku
parts = product_color_code.split("-", 1) if not sku or dedup_key in seen_skus:
color_code_only = parts[1] if len(parts) > 1 else ""
internal_ref = p.get("internal_ref_code", "")
# Dùng internal_ref (Mã tổng) làm rễ để gom toàn bộ màu của mã đó vào 1 card duy nhất
base_sku = internal_ref or magento_ref or product_color_code
if not base_sku:
continue continue
seen_skus.add(dedup_key)
# Color variant info product_entry = {
color_variant = { "sku": sku,
"sku_color": sku_color,
"name": _neutralize_generic_print(name),
"color": color_name, "color": color_name,
"color_code": color_code_only, "price": int(original_price),
"sale_price": int(sale_price) if sale_price else int(original_price),
"url": web_url, "url": web_url,
"thumbnail": thumb_url, "thumbnail_image_url": thumb_url,
"description": _neutralize_generic_print(p.get("description_text") or ""),
} }
size_scale = p.get("size_scale")
if size_scale:
product_entry["sizes"] = [s.strip() for s in size_scale.split("|") if s.strip()]
qty_sold = p.get("quantity_sold")
if qty_sold is not None:
product_entry["quantity_sold"] = int(qty_sold)
if base_sku in grouped: formatted.append(product_entry)
# Add color to existing product
existing_colors = [c["color"] for c in grouped[base_sku]["colors"]] formatted = formatted[:max_products]
if color_name and color_name not in existing_colors: logger.info(f"📦 Formatted {len(formatted)} products (flat SKU list)")
grouped[base_sku]["colors"].append(color_variant)
# Update price range if different
if sale_price and sale_price < grouped[base_sku].get("sale_price", float("inf")):
grouped[base_sku]["sale_price"] = int(sale_price)
else:
# New product - use first color's URL/thumbnail as default
product_entry = {
"sku": base_sku,
"sku_color": product_color_code,
"name": _neutralize_generic_print(name),
"color": color_name, # First color as default
"colors": [color_variant] if color_name else [],
"price": int(original_price),
"sale_price": int(sale_price) if sale_price else int(original_price),
"url": web_url, # First color's URL
"thumbnail_image_url": thumb_url, # First color's thumbnail
"description": _neutralize_generic_print(p.get("description_text") or ""),
}
# Include sizes if available (pipe-separated → list)
size_scale = p.get("size_scale")
if size_scale:
product_entry["sizes"] = [s.strip() for s in size_scale.split("|") if s.strip()]
# Include quantity_sold if available (for best seller)
qty_sold = p.get("quantity_sold")
if qty_sold is not None:
product_entry["quantity_sold"] = int(qty_sold)
grouped[base_sku] = product_entry
formatted = list(grouped.values())[:max_products]
logger.info(f"📦 Formatted {len(formatted)} products (grouped by SKU)")
return formatted return formatted
...@@ -294,6 +257,14 @@ def extract_product_ids(messages: list) -> list[dict]: ...@@ -294,6 +257,14 @@ def extract_product_ids(messages: list) -> list[dict]:
return products return products
def _extract_skus_from_text(text: str) -> list[str]:
"""Extract SKU-like codes from free text, including codes not wrapped in brackets."""
if not text:
return []
matches = re.findall(r"\b[A-Z0-9]+(?:-[A-Z0-9]+)+\b", text.upper())
return list(dict.fromkeys(matches))
def parse_ai_response_fast(ai_raw_content: str) -> tuple[str, list[str], str | None]: def parse_ai_response_fast(ai_raw_content: str) -> tuple[str, list[str], str | None]:
""" """
FAST parse - Chỉ extract ai_response + product_ids từ JSON, KHÔNG query DB. FAST parse - Chỉ extract ai_response + product_ids từ JSON, KHÔNG query DB.
...@@ -317,8 +288,9 @@ def parse_ai_response_fast(ai_raw_content: str) -> tuple[str, list[str], str | N ...@@ -317,8 +288,9 @@ def parse_ai_response_fast(ai_raw_content: str) -> tuple[str, list[str], str | N
explicit_skus = ai_json.get("product_ids", []) explicit_skus = ai_json.get("product_ids", [])
raw_insight = ai_json.get("user_insight") raw_insight = ai_json.get("user_insight")
# Extract SKUs mentioned in text # Extract SKUs mentioned in text, including raw SKU text not wrapped in [].
mentioned_skus_in_text = set(re.findall(r"\[([A-Z0-9]+)\]", ai_text_response)) mentioned_skus_in_text = set(re.findall(r"\[([A-Z0-9-]+)\]", ai_text_response))
mentioned_skus_in_text.update(_extract_skus_from_text(ai_text_response))
# Determine target SKUs # Determine target SKUs
if explicit_skus and isinstance(explicit_skus, list): if explicit_skus and isinstance(explicit_skus, list):
...@@ -395,11 +367,13 @@ async def parse_ai_response_async(ai_raw_content: str, all_products: list) -> tu ...@@ -395,11 +367,13 @@ async def parse_ai_response_async(ai_raw_content: str, all_products: list) -> tu
user_insight = raw_insight user_insight = raw_insight
# === CRITICAL: Filter/Fetch products === # === CRITICAL: Filter/Fetch products ===
# Extract SKUs mentioned in ai_response text using regex pattern [SKU] # Extract SKUs mentioned in ai_response text, including raw SKU text not wrapped in [].
mentioned_skus_in_text = set(re.findall(r"\[([A-Z0-9]+)\]", ai_text_response)) mentioned_skus_in_text = set(re.findall(r"\[([A-Z0-9-]+)\]", ai_text_response))
mentioned_skus_in_text.update(_extract_skus_from_text(ai_text_response))
logger.info(f"📝 SKUs mentioned in ai_response: {mentioned_skus_in_text}") logger.info(f"📝 SKUs mentioned in ai_response: {mentioned_skus_in_text}")
target_skus = set() target_skus = set()
product_lookup = {str(p.get("sku") or "").strip(): p for p in all_products if p.get("sku")}
# 1. Use explicit SKUs if available and confirmed by text, OR just explicit # 1. Use explicit SKUs if available and confirmed by text, OR just explicit
if explicit_skus and isinstance(explicit_skus, list): if explicit_skus and isinstance(explicit_skus, list):
...@@ -413,13 +387,13 @@ async def parse_ai_response_async(ai_raw_content: str, all_products: list) -> tu ...@@ -413,13 +387,13 @@ async def parse_ai_response_async(ai_raw_content: str, all_products: list) -> tu
elif mentioned_skus_in_text: elif mentioned_skus_in_text:
# 2. If no explicit SKUs, use text mentions # 2. If no explicit SKUs, use text mentions
target_skus = mentioned_skus_in_text target_skus = mentioned_skus_in_text
elif len(product_lookup) == 1:
# 3. If model forgot product_ids and context has exactly one SKU, fill it automatically.
target_skus = set(product_lookup.keys())
logger.info(f"🎯 Target SKUs to return: {target_skus}") logger.info(f"🎯 Target SKUs to return: {target_skus}")
if target_skus: if target_skus:
# Build lookup from current context
product_lookup = {p["sku"]: p for p in all_products if p.get("sku")}
found_products = [] found_products = []
for sku in target_skus: for sku in target_skus:
......
...@@ -39,7 +39,7 @@ Bạn PHẢI trả về JSON thuần túy, KHÔNG ĐƯỢC wrap trong markdown b ...@@ -39,7 +39,7 @@ Bạn PHẢI trả về JSON thuần túy, KHÔNG ĐƯỢC wrap trong markdown b
```json ```json
{{ {{
"ai_response": "Câu trả lời ngắn gọn, KHÔNG chứa mã SKU — frontend tự render product card từ product_ids", "ai_response": "Câu trả lời ngắn gọn, KHÔNG chứa mã SKU — frontend tự render product card từ product_ids",
"product_ids": ["8TS24W001", "8TS26S008"], "product_ids": ["8TS24W001", "8TS26S008-SA718"],
"user_insight": {{ "user_insight": {{
"USER": "...", "USER": "...",
"TARGET": "...", "TARGET": "...",
...@@ -53,7 +53,9 @@ Bạn PHẢI trả về JSON thuần túy, KHÔNG ĐƯỢC wrap trong markdown b ...@@ -53,7 +53,9 @@ Bạn PHẢI trả về JSON thuần túy, KHÔNG ĐƯỢC wrap trong markdown b
``` ```
**LƯU Ý:** **LƯU Ý:**
- `product_ids`: **BẮT BUỘC LẤY ĐÚNG NGUYÊN GIÁ TRỊ trường `sku` từ data tool trả về** (thường là mã `internal_ref` 9 ký tự VD: `8TS26S008`). KHÔNG TỰ Ý THÊM MÃ MÀU VÀO ĐUÔI (ví dụ data là `8TS26S008`, tuyệt đối không chế thành `8TS26S008-SA718`). AI KHÔNG ĐƯỢC BỊA MÃ. - `product_ids`: **BẮT BUỘC LẤY ĐÚNG NGUYÊN GIÁ TRỊ trường `sku` từ data tool trả về** (ví dụ: data trả về `8TS26S008-SA718` thì array phải chứa đúng chuỗi đó). KHÔNG tự ý cắt phần mã màu (cấm cắt thành `8TS26S008`). Giữ nguyên định dạng của `sku` do hệ thống cung cấp!
- Nếu tool trả nhiều SKU variant đầy đủ như `8TS26S008-SA718`, `8TS26S008-SB179` thì `product_ids` phải dùng đúng các mã đầy đủ đó. `8TS26S008` không được coi là SKU hợp lệ trừ khi tool thực sự trả đúng chuỗi đó ở trường `sku`.
- Khi user hỏi đúng một mã variant cụ thể như `8TS26S008-SB179`, nếu tool xác nhận có sản phẩm này thì `product_ids` phải là `["8TS26S008-SB179"]`. Tuyệt đối cấm rút gọn thành `["8TS26S008"]`.
- `user_insight` theo đúng format 6 tầng như mục 8 - `user_insight` theo đúng format 6 tầng như mục 8
- **LUÔN DÙNG NGOẶC KÉP `{{` và `}}` CHO JSON** - **LUÔN DÙNG NGOẶC KÉP `{{` và `}}` CHO JSON**
...@@ -128,6 +130,28 @@ Bạn PHẢI trả về JSON thuần túy, KHÔNG ĐƯỢC wrap trong markdown b ...@@ -128,6 +130,28 @@ Bạn PHẢI trả về JSON thuần túy, KHÔNG ĐƯỢC wrap trong markdown b
--- ---
### Example 2b: Hỏi một mã variant cụ thể
**Input:** "mã này có màu gì 8TS26S008-SB179"
**Tool trả về:** đúng 1 sản phẩm có `sku = "8TS26S008-SB179"`
{{
"ai_response": "Mẫu này hiện có màu Xanh da trời, bạn xem bên dưới nhé!",
"product_ids": ["8TS26S008-SB179"],
"user_insight": {{
"USER": "Chưa rõ.",
"TARGET": "Chính mình.",
"GOAL": "Kiểm tra thông tin của SKU variant cụ thể.",
"CONSTRAINS": "SKU: 8TS26S008-SB179 (HARD).",
"LATEST_PRODUCT_INTEREST": "8TS26S008-SB179.",
"LAST_ACTION": "Xác nhận màu của SKU variant cụ thể và show product card.",
"SUMMARY_HISTORY": "Turn 1: User hỏi SKU variant 8TS26S008-SB179 → Bot xác nhận đúng variant và show card."
}}
}}
---
### Example 3: Sản phẩm KHÔNG CÓ / SAI LOẠI ### Example 3: Sản phẩm KHÔNG CÓ / SAI LOẠI
**Input:** "Shop có bikini không?" — Tool trả về: 0 sản phẩm hoặc SP sai loại **Input:** "Shop có bikini không?" — Tool trả về: 0 sản phẩm hoặc SP sai loại
......
...@@ -335,7 +335,13 @@ CASE 7: "Set đồ công sở cho nữ" → 2 Queries: ...@@ -335,7 +335,13 @@ CASE 7: "Set đồ công sở cho nữ" → 2 Queries:
CASE 8: "Tìm mã 6KS25S005" CASE 8: "Tìm mã 6KS25S005"
→ magento_ref_code: "6KS25S005", description: "product_name: Sản phẩm. description_text: Tìm sản phẩm theo mã" → magento_ref_code: "6KS25S005", description: "product_name: Sản phẩm. description_text: Tìm sản phẩm theo mã"
⚠️ RIÊNG TÌM THEO MÃ SKU: TUYỆT ĐỐI KHÔNG ĐƯỢC tự suy diễn màu sắc (master_color), loại sản phẩm (product_line_vn) từ các chữ cái trong mã SKU. ĐỂ NULL HẾT! (Ví dụ không được đoán TS là áo trắng). ⚠️ TÌM THEO MÃ SKU:
- KHÔNG ĐƯỢC tự suy diễn loại sản phẩm (product_line_vn) từ các chữ cái trong mã. ĐỂ NULL!
- KHÔNG ĐƯỢC đoán màu từ ký tự trong mã (VD: TS ≠ trắng, SB ≠ xanh). ĐỂ NULL!
- NHƯNG NẾU KHÁCH NÓI RÕ MÀU → BẮT BUỘC PHẢI SINH master_color!
VD: "Tìm mã 6KS25S005 màu đen" → magento_ref_code: "6KS25S005", master_color: "đen"
VD: "Mã 1DS26S001 có màu hồng không?" → magento_ref_code: "1DS26S001", master_color: "hồng"
VD: "Tìm mã 6KS25S005" (không nói màu) → master_color: null
CASE 9: "Áo cá sấu polo đi chơi" CASE 9: "Áo cá sấu polo đi chơi"
→ description: "product_name: Áo cá sấu polo. description_text: Áo cá sấu polo đi chơi năng động trẻ trung. style: Dynamic" → description: "product_name: Áo cá sấu polo. description_text: Áo cá sấu polo đi chơi năng động trẻ trung. style: Dynamic"
......
...@@ -110,7 +110,7 @@ def _attach_variant_skus(formatted_products: list[dict], raw_products: list[dict ...@@ -110,7 +110,7 @@ def _attach_variant_skus(formatted_products: list[dict], raw_products: list[dict
by_base_sku: dict[str, list[dict]] = {} by_base_sku: dict[str, list[dict]] = {}
for row in raw_products: for row in raw_products:
base_sku = str(row.get("magento_ref_code") or row.get("internal_ref_code") or "").strip() base_sku = str(row.get("magento_ref_code"))
variant_sku = str(row.get("product_color_code") or "").strip() variant_sku = str(row.get("product_color_code") or "").strip()
if not base_sku or not variant_sku: if not base_sku or not variant_sku:
continue continue
...@@ -150,7 +150,7 @@ def _attach_variant_skus(formatted_products: list[dict], raw_products: list[dict ...@@ -150,7 +150,7 @@ def _attach_variant_skus(formatted_products: list[dict], raw_products: list[dict
def _resolve_stock_skus(searches: list[SearchItem], products: list[dict]) -> list[str]: def _resolve_stock_skus(searches: list[SearchItem], products: list[dict]) -> list[str]:
"""Resolve sku_color list from base SKU + optional color, ready for stock tool.""" """Resolve sku_color list from returned flat products, ready for stock tool."""
resolved: list[str] = [] resolved: list[str] = []
seen: set[str] = set() seen: set[str] = set()
...@@ -160,34 +160,25 @@ def _resolve_stock_skus(searches: list[SearchItem], products: list[dict]) -> lis ...@@ -160,34 +160,25 @@ def _resolve_stock_skus(searches: list[SearchItem], products: list[dict]) -> lis
continue continue
target_color = _normalize_text(item.master_color) target_color = _normalize_text(item.master_color)
candidates = [p for p in products if str(p.get("sku") or "").strip().upper() == base_code] candidates = []
for product in products:
sku = str(product.get("sku") or "").strip().upper()
sku_color = str(product.get("sku_color") or "").strip().upper()
if not sku and not sku_color:
continue
if base_code and (sku == base_code or sku_color == base_code or sku.startswith(f"{base_code}-")):
candidates.append(product)
for product in candidates: for product in candidates:
variants = product.get("variants") or [] sku_color = str(product.get("sku_color") or "").strip()
product_color = _normalize_text(product.get("color"))
# If user specified color, prefer matching sku_color by that color first.
if target_color: if target_color:
for variant in variants: if sku_color and (target_color in product_color or product_color in target_color) and sku_color not in seen:
sku_color = str(variant.get("sku_color") or "").strip() seen.add(sku_color)
variant_color = _normalize_text(variant.get("color")) resolved.append(sku_color)
if not sku_color: continue
continue
if (target_color in variant_color or variant_color in target_color) and sku_color not in seen:
seen.add(sku_color)
resolved.append(sku_color)
# Fallback: still return at least one variant SKU for the base code.
if not target_color or all(
s not in seen for s in [v.get("sku_color") for v in variants if v.get("sku_color")]
):
for variant in variants:
sku_color = str(variant.get("sku_color") or "").strip()
if sku_color and sku_color not in seen:
seen.add(sku_color)
resolved.append(sku_color)
# Final fallback when no variants attached.
sku_color = str(product.get("sku_color") or "").strip()
if sku_color and sku_color not in seen: if sku_color and sku_color not in seen:
seen.add(sku_color) seen.add(sku_color)
resolved.append(sku_color) resolved.append(sku_color)
...@@ -293,7 +284,6 @@ async def _execute_single_search( ...@@ -293,7 +284,6 @@ async def _execute_single_search(
logger.warning("⚠️ Langfuse db-query-search span failed: %s", trace_err, exc_info=True) logger.warning("⚠️ Langfuse db-query-search span failed: %s", trace_err, exc_info=True)
formatted_products = format_product_results(products) formatted_products = format_product_results(products)
_attach_variant_skus(formatted_products, products)
return formatted_products, {"fallback_used": False} return formatted_products, {"fallback_used": False}
except Exception as e: except Exception as e:
logger.exception("Single search error for item %r: %s", item, e) logger.exception("Single search error for item %r: %s", item, e)
...@@ -356,11 +346,20 @@ async def data_retrieval_tool(searches: list[SearchItem]) -> str: ...@@ -356,11 +346,20 @@ async def data_retrieval_tool(searches: list[SearchItem]) -> str:
for item in searches for item in searches
] ]
allowed_skus = sorted({str(p.get("sku") or "").strip() for p in combined_results if p.get("sku")})
stock_skus = _resolve_stock_skus(searches, combined_results)
output = { output = {
"status": "success", "status": "success",
"message_for_ai": (
f"Quy tắc bắt buộc: `product_ids` CHỈ được lấy từ trường `sku`. "
f"Các giá trị `sku` hợp lệ trong lần search này là: {allowed_skus}. "
f"Tuyệt đối cấm dùng `sku_color` cho `product_ids`. "
f"`sku_color` chỉ là mã biến thể màu để tham chiếu nội bộ."
),
"search_input": search_inputs, "search_input": search_inputs,
"results": combined_results, "results": combined_results,
"stock_skus": _resolve_stock_skus(searches, combined_results), "stock_skus": stock_skus,
"filter_info": final_info, "filter_info": final_info,
} }
......
import logging import logging
import os
import time
from common.embedding_service import create_embedding_async from common.embedding_service import create_embedding_async
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def _parse_code_search_input(raw_code: str) -> tuple[str, str | None]:
"""Chuẩn hóa mã user gửi và tách phần internal_ref_code / suffix nếu có."""
normalized = str(raw_code or "").strip().upper().replace(" ", "")
if "-" not in normalized:
return normalized, None
internal_ref_code, suffix_code = normalized.split("-", 1)
return internal_ref_code, suffix_code or None
def _get_price_clauses(params, sql_params: list) -> list[str]: def _get_price_clauses(params, sql_params: list) -> list[str]:
"""Lọc theo giá (Parameterized).""" """Lọc theo giá (Parameterized)."""
clauses = [] clauses = []
...@@ -76,12 +82,12 @@ def _get_metadata_clauses(params, sql_params: list) -> list[str]: ...@@ -76,12 +82,12 @@ def _get_metadata_clauses(params, sql_params: list) -> list[str]:
sql_params.extend([f"%{color_lower}%", f"%{color_lower}%"]) sql_params.extend([f"%{color_lower}%", f"%{color_lower}%"])
logger.info(f"🎨 [SQL FILTER] Color: {color_val}") logger.info(f"🎨 [SQL FILTER] Color: {color_val}")
from agent.tools.product_mapping import PRODUCT_LINE_MAP from agent.tools.product_mapping import PRODUCT_LINE_MAP
GENERIC_WORDS = {key.split()[0].lower() for key in PRODUCT_LINE_MAP.keys()} GENERIC_WORDS = {key.split()[0].lower() for key in PRODUCT_LINE_MAP.keys()}
name_val = getattr(params, "product_name", None) name_val = getattr(params, "product_name", None)
if name_val: if name_val:
from agent.tools.product_mapping import resolve_product_name, get_related_lines from agent.tools.product_mapping import get_related_lines, resolve_product_name
# Support '/' separator: "Áo lót/Áo bra active" → ["Áo lót", "Áo bra active"] # Support '/' separator: "Áo lót/Áo bra active" → ["Áo lót", "Áo bra active"]
name_parts = [p.strip() for p in name_val.split("/") if p.strip()] name_parts = [p.strip() for p in name_val.split("/") if p.strip()]
...@@ -115,60 +121,95 @@ async def build_starrocks_query(params, query_vector: list[float] | None = None) ...@@ -115,60 +121,95 @@ async def build_starrocks_query(params, query_vector: list[float] | None = None)
Returns: (sql_string, params_list) Returns: (sql_string, params_list)
""" """
# ============================================================ # =========================================================================================================================
# CASE 1: CODE SEARCH # CASE 1: CODE SEARCH
# ============================================================ # ==========================================================================================================================
magento_code = getattr(params, "magento_ref_code", None) magento_code = getattr(params, "magento_ref_code", None)
if magento_code: if magento_code:
sql_params = [magento_code, magento_code, magento_code] # Chuẩn hóa code user gửi và quy input về internal_ref_code trước khi lấy variants.
extra_where = "" normalized_magento_code = str(magento_code).strip().upper().replace(" ", "")
internal_ref_hint, suffix_code = _parse_code_search_input(normalized_magento_code)
# GHI CHÚ:
# Subquery dò gốc internal_ref_code từ bất kỳ mã nào (tổng/màu/size). extra_filters = []
# Nếu khách có kèm màu → filter thêm ở vòng ngoài (master_color LIKE). sql_params = [
# Nếu không kèm màu → trả hết tất cả biến thể màu cho AI tự chọn. internal_ref_hint,
normalized_magento_code,
normalized_magento_code,
f"{internal_ref_hint}-%",
f"{internal_ref_hint[:-1]}%" if len(internal_ref_hint) > 1 else internal_ref_hint,
f"{internal_ref_hint[:-1]}%-%" if len(internal_ref_hint) > 1 else f"{internal_ref_hint}-%",
f"{normalized_magento_code[:-1]}%" if len(normalized_magento_code) > 1 else normalized_magento_code,
f"{normalized_magento_code[:-1]}%" if len(normalized_magento_code) > 1 else normalized_magento_code,
]
# Ưu tiên màu user nói trong message; đây là filter mạnh hơn suffix trong mã.
color_val = getattr(params, "master_color", None) color_val = getattr(params, "master_color", None)
if color_val: if color_val:
extra_where = " AND LOWER(master_color) LIKE %s" extra_filters.append("(LOWER(master_color) LIKE %s OR LOWER(product_color_name) LIKE %s)")
sql_params.append(f"%{color_val.lower()}%") color_like = f"%{color_val.lower()}%"
logger.info(f"🎨 [CODE SEARCH + COLOR FILTER] Code: {magento_code}, Color: {color_val}") sql_params.extend([color_like, color_like])
logger.info(
"🎨 [CODE SEARCH] Code=%s, internal_ref=%s, explicit_color=%s",
normalized_magento_code,
internal_ref_hint,
color_val,
)
# Nếu user không nói màu nhưng có suffix, dùng suffix để ưu tiên đúng variant đã copy.
elif suffix_code:
extra_filters.append("UPPER(product_color_code) LIKE %s")
sql_params.append(f"%{suffix_code}")
logger.info(
"🏷️ [CODE SEARCH] Code=%s, internal_ref=%s, suffix_fallback=%s",
normalized_magento_code,
internal_ref_hint,
suffix_code,
)
else:
logger.info("🏷️ [CODE SEARCH] Code=%s, internal_ref=%s", normalized_magento_code, internal_ref_hint)
extra_where = ""
if extra_filters:
extra_where = " AND " + " AND ".join(extra_filters)
sql = f""" sql = f"""
WITH resolved_family AS (
SELECT DISTINCT internal_ref_code
FROM shared_source.magento_product_dimension_with_text_embedding
WHERE UPPER(internal_ref_code) = %s
OR UPPER(magento_ref_code) = %s
OR UPPER(product_color_code) = %s
OR UPPER(product_color_code) LIKE %s
OR UPPER(internal_ref_code) LIKE %s
OR UPPER(product_color_code) LIKE %s
OR UPPER(magento_ref_code) LIKE %s
OR UPPER(product_color_code) LIKE %s
)
SELECT SELECT
internal_ref_code, magento_ref_code,
MAX(magento_ref_code) as magento_ref_code,
product_color_code, product_color_code,
MAX(product_name) as product_name, product_name,
MAX(master_color) as master_color, master_color,
MAX(product_image_url_thumbnail) as product_image_url_thumbnail, product_image_url_thumbnail,
MAX(product_web_url) as product_web_url, product_web_url,
MAX(description_text) as description_text, description_text,
MAX(sale_price) as sale_price, sale_price,
MAX(original_price) as original_price, original_price,
MAX(discount_amount) as discount_amount, discount_amount,
MAX(ROUND(((original_price - sale_price) / original_price * 100), 0)) as discount_percent, ROUND(((original_price - sale_price) / original_price * 100), 0) as discount_percent,
MAX(age_by_product) as age_by_product, age_by_product,
MAX(gender_by_product) as gender_by_product, gender_by_product,
MAX(product_line_vn) as product_line_vn, product_line_vn,
MAX(quantity_sold) as quantity_sold, quantity_sold,
MAX(size_scale) as size_scale, size_scale,
1.0 as similarity_score 1.0 as similarity_score
FROM shared_source.magento_product_dimension_with_text_embedding FROM shared_source.magento_product_dimension_with_text_embedding
WHERE internal_ref_code IN ( WHERE internal_ref_code IN (SELECT internal_ref_code FROM resolved_family){extra_where}
SELECT internal_ref_code
FROM shared_source.magento_product_dimension_with_text_embedding
WHERE internal_ref_code = %s OR magento_ref_code = %s OR product_color_code = %s
){extra_where}
GROUP BY internal_ref_code, product_color_code
""" """
return sql, sql_params return sql, sql_params
# ==================================================================================================================
# ============================================================
# CASE 2: DISCOVERY — Hàng mới / Bán chạy (Direct SQL, no embedding) # CASE 2: DISCOVERY — Hàng mới / Bán chạy (Direct SQL, no embedding)
# Khác với price/gender filter: discovery cần scan TOÀN BỘ bảng # ===============================================================================================================
# vì top 100 vector results gần như không chứa new/best_seller
# ============================================================
discovery_mode = getattr(params, "discovery_mode", None) discovery_mode = getattr(params, "discovery_mode", None)
if discovery_mode: if discovery_mode:
discovery_mode = discovery_mode.lower().strip() discovery_mode = discovery_mode.lower().strip()
...@@ -220,7 +261,6 @@ async def build_starrocks_query(params, query_vector: list[float] | None = None) ...@@ -220,7 +261,6 @@ async def build_starrocks_query(params, query_vector: list[float] | None = None)
logger.info("⚡ [DISCOVERY] Direct SQL — no embedding") logger.info("⚡ [DISCOVERY] Direct SQL — no embedding")
return sql, sql_params return sql, sql_params
# ============================================================ # ============================================================
# CASE 3: SEMANTIC VECTOR SEARCH # CASE 3: SEMANTIC VECTOR SEARCH
# ============================================================ # ============================================================
...@@ -364,5 +404,4 @@ async def build_starrocks_query(params, query_vector: list[float] | None = None) ...@@ -364,5 +404,4 @@ async def build_starrocks_query(params, query_vector: list[float] | None = None)
# except Exception as e: # except Exception as e:
# logger.error(f"Error writing to query.txt: {e}") # logger.error(f"Error writing to query.txt: {e}")
return sql, sql_params return sql, sql_params
...@@ -20,3 +20,4 @@ Get-NetTCPConnection -LocalPort 5000 | ForEach-Object { Stop-Process -Id $_.Owni ...@@ -20,3 +20,4 @@ Get-NetTCPConnection -LocalPort 5000 | ForEach-Object { Stop-Process -Id $_.Owni
taskkill /F /IM python.exe taskkill /F /IM python.exe
netstat -ano | findstr :5000 | ForEach-Object { $_.Split()[-1] } | Sort-Object -Unique | ForEach-Object { taskkill /F /PID $_ } netstat -ano | findstr :5000 | ForEach-Object { $_.Split()[-1] } | Sort-Object -Unique | ForEach-Object { taskkill /F /PID $_ }
\ No newline at end of file
This diff is collapsed.
import os
import sys
import asyncio
from datetime import datetime
from zoneinfo import ZoneInfo
# Add parent dir to path so we can import from backend
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from common.cache import redis_cache
async def main():
device_id = "b9ae861478939edad0dc3c77db8f0499"
print(f"🔄 Bắt đầu reset cho device_id: {device_id}")
await redis_cache.initialize()
client = redis_cache.get_client()
if not client:
print("❌ Lỗi: Không thể kết nối Redis")
return
try:
# 1. Clear conversation history
history_key = f"identity_key_history:{device_id}"
deleted_history = await client.delete(history_key)
print(f"✅ Đã xóa lịch sử chat: {history_key} (Keys deleted: {deleted_history})")
# 2. Clear user insight
insight_key = f"identity_key_insight:{device_id}"
deleted_insight = await client.delete(insight_key)
print(f"✅ Đã xóa user insight: {insight_key} (Keys deleted: {deleted_insight})")
# 3. Add 100 limit chat for guest
# Format msg_limit:YYYY-MM-DD:device_id
tz = ZoneInfo("Asia/Ho_Chi_Minh")
today = datetime.now(tz).strftime("%Y-%m-%d")
limit_key = f"msg_limit:{today}:{device_id}"
# Bằng cách set 'guest' trong Hash về số âm (-90), user sẽ có tổng 100 lượt chat
# (vì guest_limit mặc định là 10, guest_used = -90 -> remaining = 10 - (-90) = 100)
await client.hset(limit_key, "guest", -90)
# Set expire until midnight so it cleans up naturally
now = datetime.now(tz)
reset_time = now.replace(hour=0, minute=0, second=0, microsecond=0)
from datetime import timedelta
if now >= reset_time:
reset_time = reset_time + timedelta(days=1)
seconds_until_reset = int((reset_time - now).total_seconds())
await client.expire(limit_key, seconds_until_reset)
print(f"✅ Đã reset và thêm 100 lượt chat vào limit_key: {limit_key}")
print("🎉 Hoàn thành!")
except Exception as e:
print(f"❌ Lỗi: {e}")
finally:
# Close redis connection
await client.close()
if __name__ == "__main__":
asyncio.run(main())
import json, urllib.request
req = urllib.request.Request(
'https://canifa.com/v1/middleware/search_product',
data=json.dumps({"sku":["8TS26S008-SA718"]}).encode('utf-8'),
headers={'Content-Type': 'application/json'}
)
try:
with urllib.request.urlopen(req) as f:
print(f.read().decode('utf-8'))
except Exception as e:
print(e)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment