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

Optimize Stylist payload, fix DB connection leak, update tool schema and combo logic

parent 95f6323f
# ============================================
# Langfuse v3 Self-Hosted Environment Variables
# ============================================
# --- Security Secrets (auto-generated) ---
NEXTAUTH_SECRET=fcf35cd5e20e4805871e9785cf636a81
SALT=db7e9ade13584ab2aa3ffad3c4f8d71b
ENCRYPTION_KEY=e6573ccb68eb4cd2a01869c00f4c00a4e6573ccb68eb4cd2a01869c00f4c00a4
# --- ClickHouse ---
CLICKHOUSE_PASSWORD=ch_canifa_2026
# --- MinIO ---
MINIO_ROOT_PASSWORD=minio_canifa_2026
# --- Redis ---
REDIS_AUTH=redis_langfuse_2026
# Canifa Chatbot API (Simplified)
Base URL: `http://172.16.2.207:5000`
---
## 1. Chat (Gửi tin nhắn)
**POST** `/api/agent/chat`
### Request
#### Guest (Chưa login)
```json
{
"user_query": "Tìm áo thun nam",
"device_id": "my-device-123"
}
```
#### User (Đã login)
```json
Headers: Authorization: Bearer <token>
{
"user_query": "Tìm áo thun nam",
"device_id": "my-device-123"
}
```
### Response
```json
{
"status": "success",
"ai_response": "Shop có mẫu áo thun này...",
"product_ids": [
{
"sku": "8TS24W001",
"name": "Áo thun nam Basic",
"price": 250000,
"sale_price": 199000,
"url": "https://canifa.com/...",
"thumbnail_image_url": "https://..."
}
],
"limit_info": { "limit": 10, "used": 1, "remaining": 9 }
}
```
### Error Response (500)
Trong trường hợp lỗi hệ thống (DB, LLM...), API sẽ trả về HTTP 500 kèm body:
```json
{
"status": "error",
"error_code": "SYSTEM_ERROR",
"message": "Oops 😥 Hiện Canifa-AI chưa thể xử lý yêu cầu của bạn..."
}
```
### Error Response (429) - Rate Limit Exceeded
Khi user/guest vượt quá giới hạn tin nhắn cho phép:
**Trường hợp 1: Guest hết lượt (Cần login)**
```json
{
"status": "error",
"error_code": "GUEST_LIMIT_EXCEEDED",
"message": "Bạn đã sử dụng hết tin nhắn hôm nay. Đăng nhập ngay để dùng tiếp: https://canifa.com/login",
"require_login": true,
"limit_info": {
"limit": 10,
"used": 10,
"remaining": 0,
"reset_seconds": 3600
}
}
```
**Trường hợp 2: User hết lượt (Hoặc Guest đạt Hard Limit)**
```json
{
"status": "error",
"error_code": "USER_LIMIT_EXCEEDED",
"message": "Bạn đã sử dụng hết tin nhắn hôm nay. Vui lòng quay lại vào hôm sau để dùng tiếp!",
"require_login": false,
"limit_info": { ... }
}
```
---
## 2. History (Lấy lịch sử)
**GET** `/api/history/{your_device_id}?limit=20&before_id=105`
### Query Parameters
| Param | Type | Description |
| :--- | :--- | :--- |
| `limit` | int | Số tin nhắn (Default: 50) |
| `before_id` | int | ID tin nhắn cuối của trang trước (để load thêm) |
### Request
**Guest:**
`/api/history/my-device-123?limit=20`
**User:**
`/api/history/my-device-123?limit=20` (Param URL vẫn giữ là device_id cho tiện FE)
Header: `Authorization: Bearer <token>`
*(Backend sẽ tự ưu tiên lấy User ID từ Token để truy vấn lịch sử)*
### Response
```json
{
"data": [
{
"id": 105,
"message": "...", // JSON String
"is_human": false,
"timestamp": "..."
}
],
"next_cursor": 104 // Dùng ID này cho `before_id` tiếp theo
}
```
---
## 3. Reset (Xóa và tạo mới)
**POST** `/api/history/archive`
*(Lưu ý: Chỉ dành cho User đã đăng nhập)*
### Request
Header `Authorization` (User).
Body rỗng `{}`.
### Response
```json
{
"status": "success",
"success": true,
"message": "Archived successfully",
"new_key": "my-device-123_archived_..."
}
```
...@@ -75,10 +75,7 @@ async def chat_controller( ...@@ -75,10 +75,7 @@ async def chat_controller(
session_id = f"{identity_key}-{str(uuid.uuid4())[:8]}" session_id = f"{identity_key}-{str(uuid.uuid4())[:8]}"
tags = ["stylist_pro", "user:authenticated" if is_authenticated else "user:anonymous"] tags = ["stylist_pro", "user:authenticated" if is_authenticated else "user:anonymous"]
langfuse_handler = get_callback_handler( langfuse_handler = get_callback_handler()
user_id=identity_key,
tags=tags,
)
exec_config = { exec_config = {
"configurable": { "configurable": {
"user_id": identity_key, "user_id": identity_key,
...@@ -91,7 +88,9 @@ async def chat_controller( ...@@ -91,7 +88,9 @@ async def chat_controller(
"metadata": { "metadata": {
"trace_id": trace_id, "trace_id": trace_id,
"session_id": session_id, "session_id": session_id,
"user_id": identity_key,
}, },
"tags": tags,
"run_name": "CANIFAGraph", "run_name": "CANIFAGraph",
} }
...@@ -100,8 +99,11 @@ async def chat_controller( ...@@ -100,8 +99,11 @@ async def chat_controller(
try: try:
start_time = time.time() start_time = time.time()
from langfuse import propagate_attributes
# We wrap the call to trace it manually in Langfuse if needed, # We wrap the call to trace it manually in Langfuse if needed,
# but the LangChain callbacks in graph nodes handle most of it. # but the LangChain callbacks in graph nodes handle most of it.
with propagate_attributes(session_id=session_id, user_id=identity_key, tags=tags):
result = await manager.chat( result = await manager.chat(
user_message=query, user_message=query,
user_insight=user_insight, user_insight=user_insight,
...@@ -117,13 +119,27 @@ async def chat_controller( ...@@ -117,13 +119,27 @@ async def chat_controller(
raise e raise e
# 6. Response Preparation # 6. Response Preparation
ai_response = result.get("ai_response", "") ai_response = result.get("response") or result.get("ai_response") or ""
final_products = result.get("products", []) raw_products = result.get("products", [])
updated_insight = result.get("updated_insight") updated_insight = result.get("updated_insight")
# Trim massive JSON fields (similar_items, outfit_recommendations, description_text) to save network payload
final_products = []
for p in raw_products:
final_products.append({
"sku": p.get("magento_ref_code") or p.get("sku") or p.get("internal_ref_code"),
"product_name": p.get("product_name") or p.get("name"),
"sale_price": p.get("sale_price") or p.get("price"),
"original_price": p.get("original_price") or p.get("price_vnd"),
"product_image_url_thumbnail": p.get("product_image_url_thumbnail") or p.get("image"),
"product_web_url": p.get("product_web_url") or p.get("url"),
"master_color": p.get("master_color") or p.get("color_name") or p.get("product_color_name"),
"sku_color": p.get("product_color_code") or p.get("color_code") or p.get("sku_color")
})
response_payload = { response_payload = {
"ai_response": ai_response, "ai_response": ai_response,
"product_ids": final_products, # Full objects as expected by modern UI "product_ids": final_products, # Stripped-down objects for UI
"user_insight": updated_insight, "user_insight": updated_insight,
"lead_stage": result.get("lead_stage"), "lead_stage": result.get("lead_stage"),
"elapsed_ms": result.get("elapsed_ms") "elapsed_ms": result.get("elapsed_ms")
......
...@@ -68,6 +68,29 @@ async def classifier_node(state: StylistProState, config: RunnableConfig): ...@@ -68,6 +68,29 @@ async def classifier_node(state: StylistProState, config: RunnableConfig):
if "product_line_vn" in inferred and isinstance(inferred["product_line_vn"], str): if "product_line_vn" in inferred and isinstance(inferred["product_line_vn"], str):
inferred["product_line_vn"] = [inferred["product_line_vn"]] inferred["product_line_vn"] = [inferred["product_line_vn"]]
# --- BỔ SUNG: Ép kiểu dữ liệu về Taxonomy chuẩn ---
# 1. age_group -> [adult, kid, others]
valid_ages = ["adult", "kid", "others"]
age = str(inferred.get("age_group") or "").lower()
if age:
if any(k in age for k in ["bé", "kid", "child", "tre_em", "trẻ em", "5 tuổi", "6 tuổi"]):
inferred["age_group"] = "kid"
elif any(k in age for k in ["người lớn", "adult", "lon", "gia đình", "all", "tất cả"]):
inferred["age_group"] = "adult"
elif age not in valid_ages:
inferred["age_group"] = "others"
# 2. gender_target -> [male, female, unisex]
valid_genders = ["male", "female", "unisex"]
gender = str(inferred.get("gender_target") or "").lower()
if gender:
if any(k in gender for k in ["nam", "trai", "male", "boy"]):
inferred["gender_target"] = "male"
elif any(k in gender for k in ["nữ", "gái", "female", "girl"]):
inferred["gender_target"] = "female"
elif gender not in valid_genders:
inferred["gender_target"] = "unisex"
output = ClassifierOutput(**data) output = ClassifierOutput(**data)
# Tool Registry # Tool Registry
......
...@@ -11,14 +11,25 @@ class LiteralSearchArgs(BaseModel): ...@@ -11,14 +11,25 @@ class LiteralSearchArgs(BaseModel):
raw_text: str = "" raw_text: str = ""
class InferredSearchArgs(BaseModel): class InferredSearchArgs(BaseModel):
product_line_vn: List[str] = Field(default_factory=list) product_line_vn: List[str] = Field(default_factory=list, description="Dòng sản phẩm (Vd: Áo phông, Quần short).")
gender_target: Optional[str] = None gender_target: Optional[str] = Field(default=None, description="Giới tính mục tiêu (male, female, unisex).")
age_group: Optional[str] = None age_group: Optional[str] = Field(default=None, description="Độ tuổi mục tiêu. BẮT BUỘC chỉ chọn: adult, kid, others.")
master_color: Optional[str] = None master_color: Optional[str] = Field(default=None, description="Màu sắc chủ đạo.")
tags: List[str] = Field(default_factory=list) tags: List[str] = Field(
keywords: List[str] = Field(default_factory=list) default_factory=list,
price_min: Optional[int] = None description=(
price_max: Optional[int] = None "AI suy luận ý định khách -> chọn từ 4 TRỤC CỐ ĐỊNH (BẮT BUỘC có prefix!): "
"Trục 1 (occ:): occ:di_lam, occ:di_choi, occ:di_tiec, occ:di_hoc, occ:mac_nha, occ:the_thao, occ:di_bien, occ:du_lich, occ:da_ngoai, occ:di_ngu. "
"Trục 2a (wthr:): wthr:mua_he, wthr:mua_dong, wthr:giao_mua, wthr:troi_mua, wthr:troi_nang. "
"Trục 2b (func:): func:thoang_mat, func:giu_am, func:tham_hut, func:nhanh_kho, func:chong_uv, func:can_gio. "
"Trục 3 (style:): style:thanh_lich, style:nang_dong, style:basic, style:ca_tinh, style:de_thuong, style:tre_trung, style:toi_gian, style:smart_casual. "
"Trục 4 (fit:): fit:oversize, fit:slim, fit:regular, fit:wide_leg, fit:cropped, fit:relaxed. "
"KHÔNG tự nghĩ tag mới! PHẢI giữ prefix! Tối đa 3."
)
)
keywords: List[str] = Field(default_factory=list, description="Từ khóa bổ trợ (chất liệu, kiểu dáng...).")
price_min: Optional[int] = Field(default=None, description="Giá tối thiểu (VND).")
price_max: Optional[int] = Field(default=None, description="Giá tối đa (VND).")
class ClassifierSearchArgs(BaseModel): class ClassifierSearchArgs(BaseModel):
literal: LiteralSearchArgs = Field(default_factory=LiteralSearchArgs) literal: LiteralSearchArgs = Field(default_factory=LiteralSearchArgs)
......
...@@ -12,7 +12,8 @@ ...@@ -12,7 +12,8 @@
### 💬 VĂN PHONG BÁN HÀNG (QUAN TRỌNG): ### 💬 VĂN PHONG BÁN HÀNG (QUAN TRỌNG):
**Mỗi khi giới thiệu sản phẩm, PHẢI:** **Mỗi khi giới thiệu sản phẩm, PHẢI:**
- **KHÔNG lặp lại thông tin sản phẩm** (giá, chất liệu, size...) — frontend đã hiển thị product card đầy đủ - **KHÔNG lặp lại thông tin sản phẩm** (tên, mã SKU, giá, chất liệu, size...) — frontend đã hiển thị product card đầy đủ.
- **TUYỆT ĐỐI CẤM LIỆT KÊ DANH SÁCH 1) 2) 3)** vào câu trả lời. Bạn CHỈ CẦN NÓI NGẮN GỌN 1 CÂU: "Mình có mấy mẫu dưới đây rất hợp đi [hoàn cảnh], bạn tham khảo nhé!" để tiết kiệm token.
- Dùng ngôn ngữ **gần gũi, tự nhiên** như nói chuyện với bạn - Dùng ngôn ngữ **gần gũi, tự nhiên** như nói chuyện với bạn
- **Tạo cảm xúc ngắn gọn**: "Mẫu này đang hot", "Phù hợp gu bạn lắm" (KHÔNG mô tả chi tiết SP) - **Tạo cảm xúc ngắn gọn**: "Mẫu này đang hot", "Phù hợp gu bạn lắm" (KHÔNG mô tả chi tiết SP)
- **Kết thúc bằng call-to-action**: "Bạn thấy mẫu nào ưng ý nhất?", "Bạn cần mình tư vấn thêm gì không?" - **Kết thúc bằng call-to-action**: "Bạn thấy mẫu nào ưng ý nhất?", "Bạn cần mình tư vấn thêm gì không?"
...@@ -149,6 +150,10 @@ CẢM XÚC → HIGHLIGHT → GỢI Ý → CTA ...@@ -149,6 +150,10 @@ CẢM XÚC → HIGHLIGHT → GỢI Ý → CTA
- Áo C giá 199k" - Áo C giá 199k"
→ Đã có card render, KHÔNG cần liệt kê lại! → Đã có card render, KHÔNG cần liệt kê lại!
❌ SAI (kể lể dài dòng 1, 2, 3):
"Anh/chị tham khảo 3 lựa chọn: 1) Váy liền nữ dáng A (6DS25S017)... 2) Váy liền dáng dài... 3) Váy A..."
→ TUYỆT ĐỐI CẤM liệt kê như thế này. Chỉ nói 1 câu ngắn gọn!
✅ ĐÚNG (sales stylist): ✅ ĐÚNG (sales stylist):
"Ôi bạn chọn đúng thời điểm sale luôn! 🔥 "Ôi bạn chọn đúng thời điểm sale luôn! 🔥
Mình chọn được 3 mẫu phù hợp gu bạn nè — có cả form slim Mình chọn được 3 mẫu phù hợp gu bạn nè — có cả form slim
......
...@@ -63,7 +63,8 @@ Bạn PHẢI trả về JSON thuần túy, KHÔNG ĐƯỢC wrap trong markdown b ...@@ -63,7 +63,8 @@ Bạn PHẢI trả về JSON thuần túy, KHÔNG ĐƯỢC wrap trong markdown b
### Quy tắc ai_response: ### Quy tắc ai_response:
- **KHÔNG mô tả chi tiết sản phẩm** (giá, chất liệu, size, tính năng...) vì frontend đã tự render product card - **KHÔNG mô tả chi tiết sản phẩm** (tên, giá, chất liệu, size, tính năng...) vì frontend đã tự render product card
- **TUYỆT ĐỐI CẤM LIỆT KÊ DANH SÁCH 1), 2), 3)**. CHỈ CẦN NÓI THẬT NGẮN GỌN (Ví dụ: "Mình có mấy mẫu dưới đây rất hợp để đi date, bạn xem nha!")
- **ĐIỀU KIỆN SHOW PRODUCT CARD:** KHI VÀ CHỈ KHI `product_ids` có chứa ít nhất 1 mã SKU, thì mới được nói "bạn xem bên dưới nhé". - **ĐIỀU KIỆN SHOW PRODUCT CARD:** KHI VÀ CHỈ KHI `product_ids` có chứa ít nhất 1 mã SKU, thì mới được nói "bạn xem bên dưới nhé".
- **NẾU `product_ids` rỗng (`[]`)**: **TUYỆT ĐỐI CẤM** nói "bạn xem bên dưới nhé" hoặc "mình có mấy mẫu này". Phải thành thật xin lỗi là chưa có mẫu phù hợp. - **NẾU `product_ids` rỗng (`[]`)**: **TUYỆT ĐỐI CẤM** nói "bạn xem bên dưới nhé" hoặc "mình có mấy mẫu này". Phải thành thật xin lỗi là chưa có mẫu phù hợp.
- **KHÔNG nhắc mã SKU trong `ai_response`**. Mã SKU **VẪN PHẢI CÓ** trong `product_ids` để frontend render product card! - **KHÔNG nhắc mã SKU trong `ai_response`**. Mã SKU **VẪN PHẢI CÓ** trong `product_ids` để frontend render product card!
......
...@@ -39,8 +39,20 @@ Khi dùng `data_retrieval_tool`, bạn BẮT BUỘC phân tách: ...@@ -39,8 +39,20 @@ Khi dùng `data_retrieval_tool`, bạn BẮT BUỘC phân tách:
4. Combo: Khách hỏi "đồ", "set", "phối" -> BẮT BUỘC gọi data_retrieval_tool. RESET product_line cũ từ insight. 4. Combo: Khách hỏi "đồ", "set", "phối" -> BẮT BUỘC gọi data_retrieval_tool. RESET product_line cũ từ insight.
</mapping_logic_rules> </mapping_logic_rules>
<inferred_search_taxonomy>
Khi điền `inferred`, bạn PHẢI tuân thủ bộ giá trị cố định sau:
- **age_group**: BẮT BUỘC chỉ chọn 1 trong 3: `adult` (người lớn), `kid` (trẻ em), `others` (khác).
* Nếu khách hỏi cho "gia đình" hoặc "tất cả": Ưu tiên chọn `adult`.
* Tuyệt đối KHÔNG được điền số tuổi (vd: "5 tuổi" -> `kid`).
* Tuyệt đối KHÔNG điền "all" hay "family".
- **gender_target**: BẮT BUỘC chỉ chọn 1 trong 3: `male`, `female`, `unisex`.
- **product_line_vn**: Dòng sản phẩm (Vd: "Áo phông", "Quần short"). LUÔN để trong List [].
</inferred_search_taxonomy>
<tool_routing_rules> <tool_routing_rules>
- **data_retrieval_tool**: Tìm kiếm sản phẩm chung. ĐẶC BIỆT KHI KHÁCH CHO SKU (VD: "6DS25S012", "[6TO24S010]") -> GỌI NGAY tool này bằng `magento_ref_code`. - **data_retrieval_tool**: Tìm kiếm sản phẩm chung.
+ ĐẶC BIỆT KHI KHÁCH CHO SKU (VD: "6DS25S012", "[6TO24S010]") -> GỌI NGAY tool này bằng `magento_ref_code`.
+ NẾU KHÁCH HỎI PHỐI ĐỒ CHO SẢN PHẨM CŨ (VD: "phối với váy này", "mặc cùng mẫu trên"): BẮT BUỘC trích xuất SKU từ `Latest Interest` trong `<user_memory>` và điền vào `magento_ref_code` của tool này. Việc này giúp gọi lại SP cũ để lấy mảng `outfit_recommendations`. Đừng tự ý search lung tung!
- **check_is_stock**: GỌI KHI KHÁCH HỎI TỒN KHO ONLINE (VD: "còn hàng không", "còn size M không", "mã này hết chưa"). NẾU khách hỏi MÃ SKU + "còn không" -> BẮT BUỘC gọi SONG SONG `data_retrieval_tool` và `check_is_stock`. KHÔNG dùng check_is_stock cho tồn kho offline cửa hàng. - **check_is_stock**: GỌI KHI KHÁCH HỎI TỒN KHO ONLINE (VD: "còn hàng không", "còn size M không", "mã này hết chưa"). NẾU khách hỏi MÃ SKU + "còn không" -> BẮT BUỘC gọi SONG SONG `data_retrieval_tool` và `check_is_stock`. KHÔNG dùng check_is_stock cho tồn kho offline cửa hàng.
- **canifa_store_search**: GỌI KHI HỎI ĐỊA CHỈ (VD: "cửa hàng ở đâu", "Canifa Chùa Bộc"). - **canifa_store_search**: GỌI KHI HỎI ĐỊA CHỈ (VD: "cửa hàng ở đâu", "Canifa Chùa Bộc").
- **canifa_get_promotions**: GỌI KHI HỎI SALE (VD: "có khuyến mãi gì", "đang có sale gì"). - **canifa_get_promotions**: GỌI KHI HỎI SALE (VD: "có khuyến mãi gì", "đang có sale gì").
...@@ -48,8 +60,8 @@ Khi dùng `data_retrieval_tool`, bạn BẮT BUỘC phân tách: ...@@ -48,8 +60,8 @@ Khi dùng `data_retrieval_tool`, bạn BẮT BUỘC phân tách:
</tool_routing_rules> </tool_routing_rules>
<inheritance_rules> <inheritance_rules>
- Kế thừa Giới tính/Đối tượng từ <user_insight> (VD: Target là Nam -> gender_target="men"). - Kế thừa Giới tính/Đối tượng từ `<user_memory>` (VD: Target là Nữ -> gender_target="women"). TUY NHIÊN, nếu câu hỏi mới nhất của khách CÓ NHẮC ĐẾN GIỚI TÍNH KHÁC (VD: "cho nữ", "đồ nữ", "váy") -> BẮT BUỘC GHI ĐÈ (OVERRIDE) giới tính mới, KHÔNG được bê nguyên si cái cũ!
- RESET hoàn toàn product_line_vn theo câu hỏi mới nhất. - RESET hoàn toàn product_line_vn theo câu hỏi mới nhất. Trừ phi khách yêu cầu "phối đồ với sản phẩm này" thì mới dùng `magento_ref_code`.
</inheritance_rules> </inheritance_rules>
<guardrails> <guardrails>
...@@ -91,9 +103,10 @@ Bạn là Chuyên gia Thời trang (Stylist Pro) của CANIFA. Bạn tư vấn d ...@@ -91,9 +103,10 @@ Bạn là Chuyên gia Thời trang (Stylist Pro) của CANIFA. Bạn tư vấn d
- Mặc định khi giới thiệu 1 sản phẩm chính, LUÔN gợi ý kèm 1-2 sản phẩm tương tự lấy từ mảng `similar_items` (nếu có). - Mặc định khi giới thiệu 1 sản phẩm chính, LUÔN gợi ý kèm 1-2 sản phẩm tương tự lấy từ mảng `similar_items` (nếu có).
- Mục đích: Tránh bế tắc, cho khách thêm lựa chọn (VD: "Ngoài ra, Canifa còn có mẫu [SKU tương tự] cực kỳ hợp dáng..."). - Mục đích: Tránh bế tắc, cho khách thêm lựa chọn (VD: "Ngoài ra, Canifa còn có mẫu [SKU tương tự] cực kỳ hợp dáng...").
2. CHIẾN THUẬT OUTFIT (ON-DEMAND): 2. CHIẾN THUẬT OUTFIT & COMBO (BẮT BUỘC):
- CHỈ tung đồ phối khi khách có ý định rõ ràng (hỏi "phối", "mix", "mặc với gì", "combo", "set"). KHÔNG tự động phối đồ nếu khách chỉ tìm áo/quần đơn lẻ. - KHI KHÁCH TÌM COMBO, ĐỒ BỘ, SET HOẶC HỎI "PHỐI ĐỒ": Bạn BẮT BUỘC phải tư vấn trọn bộ (CẢ ÁO LẪN QUẦN).
- Khi phối đồ: BẮT BUỘC lấy sản phẩm từ mảng `outfit_recommendations`. Dùng `role` và `reason` trong dữ liệu để giải thích thuyết phục (VD: "Với màu Xanh của áo, bạn nên phối với quần [SKU] vì..."). - Lấy SKU của sản phẩm chính, sau đó trích xuất tiếp SKU của sản phẩm phối kèm từ mảng `outfit_recommendations`.
- TUYỆT ĐỐI QUAN TRỌNG: Bạn PHẢI nhét TẤT CẢ các SKU đó (cả áo và quần/váy) vào chung mảng `product_ids` ở Output JSON để frontend render ra đủ cả bộ cho khách xem!
3. NGUYÊN TẮC KIM CHỈ NAM (PIVOTING): 3. NGUYÊN TẮC KIM CHỈ NAM (PIVOTING):
- Nếu khách đang xem SKU A nhưng lại hỏi sang SKU B (từ gợi ý similar/outfit), bạn phải đổi gốc, LẤY SKU B LÀM KIM CHỈ NAM MỚI. - Nếu khách đang xem SKU A nhưng lại hỏi sang SKU B (từ gợi ý similar/outfit), bạn phải đổi gốc, LẤY SKU B LÀM KIM CHỈ NAM MỚI.
...@@ -114,21 +127,16 @@ Cập nhật 5 trường Insight: ...@@ -114,21 +127,16 @@ Cập nhật 5 trường Insight:
</user_memory_update> </user_memory_update>
<formatting_rules> <formatting_rules>
- VĂN PHONG NGẮN GỌN, TRỰC DIỆN: Cắt bỏ mọi lời lẽ dông dài, giải thích lan man. Đi thẳng vào việc giới thiệu sản phẩm. - VĂN PHONG TỰ NHIÊN, THÂN THIỆN: Đừng cụt lủn quá! Hãy nói chuyện như một Stylist thực thụ, có thể chém thêm 1-2 câu khen ngợi hoặc giải thích lý do vì sao lại chọn đồ này (tổng cộng khoảng 2-3 câu).
- TỐI ĐA HÓA SỰ LỰA CHỌN (CHỐT DEAL): Rất khuyến khích đưa ra 3-5 lựa chọn sản phẩm để khách có nhiều cơ hội chốt đơn. TUY NHIÊN, mỗi sản phẩm chỉ mô tả bằng 1 câu cực ngắn (Tên + SKU + 1 ưu điểm). - TUYỆT ĐỐI CẤM LIỆT KÊ SẢN PHẨM (kiểu 1, 2, 3) VÀO TRONG VĂN BẢN (ai_response). Frontend đã tự động hiển thị thẻ sản phẩm đầy đủ thông tin (Tên, Giá, Ảnh, Chất liệu, v.v.).
- TRÌNH BÀY COMBO/OUTFIT GỌN GÀNG: Tuyệt đối không viết tổ hợp lộn xộn kiểu "Áo A + Quần B/C/D". Nếu giới thiệu 1 áo và 4 quần, chỉ cần nói ngắn gọn: "Gợi ý phối đồ: Áo polo [SKU] kết hợp với bất kỳ mẫu quần nào ở trên đều cực kỳ tôn dáng". - Bạn có thể viết ví dụ như: "Áo polo này form ôm rất đẹp, cực kỳ tôn dáng cho các anh nha! Mình gợi ý thêm vài mẫu quần kaki phối cùng để đi làm cho lịch sự, bạn xem qua thử nhé."
- SKU sản phẩm phải bọc trong ngoặc tròn, ví dụ: (6TS25S001). - MỌI SKU bạn muốn show BẮT BUỘC phải nằm trong mảng `product_ids`. KHÔNG nhắc mã SKU trong `ai_response`. Không dán URL.
- MỌI SKU nhắc đến trong text BẮT BUỘC phải nằm trong mảng product_ids. Không dán URL. - NẾU TƯ VẤN PHỐI ĐỒ (COMBO): Nếu bạn nhắc đến việc phối áo với quần/váy, BẮT BUỘC phải moi các mã SKU trong phần `outfit_recommendations` của kết quả Search để nhét chung vào mảng `product_ids`. Đừng mồm thì nhắc quần jeans mà mảng `product_ids` lại vứt mỗi cái áo!
- TRÌNH BÀY COMBO/OUTFIT GỌN GÀNG: Giải thích rõ ràng món nào phối với món nào và tạo ra phong cách gì.
</formatting_rules> </formatting_rules>
<example_response> <example_response>
Dạ Canifa gợi ý 4 mẫu quần kaki nam dáng ôm (slim fit) vừa tôn dáng vừa thoải mái cho anh: Dạ em có 3 mẫu váy liền đi chơi rất hợp cho vợ; dưới đây bạn xem mẫu ưng dáng trước nhé! Nếu ưng ý mẫu nào, bạn cứ bấm icon 🛒 để thêm vào giỏ hàng luôn nha!
1. Quần khaki nam dáng ôm (8BK25A004): Lên form khá sát nhưng co giãn tốt, hợp đi làm/đi chơi.
2. Quần khaki nam cạp chun (8BK26A001): Cạp thun ẩn, ôm gọn nhưng rất dễ mặc.
3. Quần khaki nam regular (8BK25W001): Dáng ôm vừa phải, ít kén dáng người.
4. Quần khaki nam cạp trơn (8BK23A002): Bản giá tối ưu, giữ tinh thần lịch sự.
💡 Gợi ý phối đồ: Để có outfit hoàn hảo, anh có thể kết hợp Áo polo nam (8TP26A003) với bất kỳ chiếc quần kaki nào ở trên đều cực kỳ hợp và thanh lịch.
</example_response> </example_response>
<output_format> <output_format>
......
...@@ -3,7 +3,7 @@ import logging ...@@ -3,7 +3,7 @@ import logging
import re import re
import sys import sys
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional, Union from typing import Any, Dict, List, Literal, Optional, Union
from langchain_core.runnables import RunnableConfig from langchain_core.runnables import RunnableConfig
from langchain_core.tools import tool from langchain_core.tools import tool
...@@ -23,39 +23,101 @@ if str(PROJECT_ROOT) not in sys.path: ...@@ -23,39 +23,101 @@ if str(PROJECT_ROOT) not in sys.path:
ProductSearchEngine = None ProductSearchEngine = None
class LiteralSearch(BaseModel): class LiteralSearch(BaseModel):
model_config = {"extra": "ignore"} """Lane 1: Tìm NGUYÊN VĂN — không filter, chỉ LIKE raw text."""
model_config = {"extra": "forbid"}
raw_text: str = Field(default="", description="Cau search nguyen van tu user.") raw_text: str = Field(
default="",
description=(
"Câu search NGUYÊN VĂN từ user. Copy y hệt phần liên quan đến sản phẩm. "
"BẮT BUỘC BỎ HẾT các từ giao tiếp thừa (tìm, mua, cái, chiếc, nhé, nha, ấy, kia, thế, đồ, cho, mình...). "
"TỰ ĐỘNG SỬA LỖI CHÍNH TẢ/TYPO nếu nhận diện được (VD: 'nghọ nghiêng' -> 'ngọ nghiêng', 'khoát' -> 'khoác'). "
"VD: 'mình muốn tìm áo hình con ngựa ấy' -> raw_text='áo hình con ngựa'."
),
)
class InferredSearch(BaseModel): class InferredSearch(BaseModel):
model_config = {"extra": "ignore"} model_config = {"extra": "ignore"}
product_line_vn: List[str] = Field(default_factory=list, description="Dong san pham.") product_line_vn: List[str] = Field(
gender_by_product: Optional[str] = Field(default=None, description="Gioi tinh.") default_factory=list,
age_by_product: Optional[str] = Field(default=None, description="Do tuoi.") description=(
gender_target: Optional[str] = Field(default=None, description="Alias cu cua gender_by_product.") "Dòng sản phẩm. BẮT BUỘC ĐIỀN để filter SQL chính xác.\n"
age_group: Optional[str] = Field(default=None, description="Alias cu cua age_by_product.") "QUY TẮC MAPPING (TUYỆT ĐỐI TUÂN THỦ):\n"
master_color: Optional[str] = Field(default=None, description="Mau sac.") "1. Thời tiết: 'trời mưa' -> ['Áo khoác', 'Áo khoác gió']; 'trời lạnh/mùa đông' -> ['Áo khoác', 'Áo len', 'Áo nỉ', 'Áo giữ nhiệt', 'Quần dài', 'Quần nỉ']; 'mùa hè/nắng nóng' -> ['Áo phông', 'Áo ba lỗ', 'Quần soóc', 'Váy liền', 'Áo khoác chống nắng'].\n"
"2. Dịp: 'đi tiệc/đám cưới' -> ['Váy liền', 'Chân váy', 'Áo kiểu', 'Áo sơ mi', 'Blazer']; 'đi làm/công sở' -> ['Áo sơ mi', 'Áo Polo', 'Quần Khaki', 'Quần jean', 'Váy liền', 'Blazer', 'Chân váy']; 'đi gym/thể thao' -> ['Bộ thể thao', 'Áo ba lỗ', 'Áo phông', 'Quần nỉ', 'Quần leggings', 'Áo bra active']; 'đi biển' -> ['Váy liền', 'Áo phông', 'Quần soóc', 'Áo ba lỗ', 'Dép'].\n"
"3. Lễ Tết: 30/4 & 2/9 -> ['Áo phông', 'Áo kiểu', 'Váy liền', 'Quần Khaki']; Tết -> ['Váy liền', 'Chân váy', 'Áo kiểu', 'Áo sơ mi', 'Quần Khaki', 'Bộ quần áo', 'Áo dài']; 20/10 -> ['Váy liền', 'Chân váy', 'Áo kiểu', 'Áo sơ mi', 'Blazer'].\n"
"4. Đồ lót: 'sịp/quần lót' -> ['Quần lót', 'Quần lót đùi', 'Quần lót tam giác']; 'áo lót/bra' -> ['Áo lót', 'Áo bra active']. TỰ ĐỘNG bỏ qua gender/age nếu là đồ lót.\n"
"5. COMBO/SET: Nếu nói 'combo đi biển', 'đồ đi làm', 'set đồ' -> KHÔNG ĐƯỢC chỉ điền mỗi 'Áo' hoặc 'Quần', PHẢI ĐIỀN ĐỦ CÁC MÓN PHÙ HỢP NGỮ CẢNH (VD: đi biển -> ['Áo phông', 'Quần soóc', 'Váy liền', 'Áo ba lỗ']). \n"
"TUYỆT ĐỐI KHÔNG kế thừa product_line_vn từ insight nếu khách hỏi 'set đồ' hay 'combo'. PHẢI RESET và suy luận tươi!"
)
)
gender_by_product: Optional[str] = Field(
default=None,
description=(
"Giới tính mục tiêu. TUYỆT ĐỐI KHÔNG TỰ SUY DIỄN nếu khách KHÔNG nhắc đến (VD: khách chỉ gõ 'áo ngọ nghiêng' -> BẮT BUỘC ĐỂ RỖNG). "
"NẾU khách nói 'cho nam' -> điền 'men' (hệ thống sẽ tự động lấy cả men & unisex). "
"NẾU khách nói 'cho nữ' -> điền 'women' (hệ thống sẽ tự lấy cả women & unisex). "
"NẾU khách nói 'váy', 'đầm', 'chân váy' -> BẮT BUỘC điền 'women' (trừ khi nói rõ cho bé). "
"NẾU khách đính chính 'cho nữ cơ mà' -> cập nhật thành 'women'."
)
)
age_by_product: Optional[Literal["others", "adult", "kid"]] = Field(
default=None,
description=(
"Độ tuổi mục tiêu (others/adult/kid). TUYỆT ĐỐI KHÔNG TỰ SUY DIỄN nếu khách KHÔNG nhắc đến (VD: khách chỉ gõ 'áo ngọ nghiêng' -> BẮT BUỘC ĐỂ RỖNG). "
"Chỉ điền 'kid' nếu khách nói 'cho bé', 'trẻ em', 'bé gái', 'bé trai'. "
"Chỉ điền 'adult' nếu khách hỏi đồ đi làm/công sở, hoặc nhấn mạnh 'cho người lớn'."
)
)
gender_target: Optional[str] = Field(default=None, description="Alias cũ của gender_by_product. Dùng để tương thích ngược.")
age_group: Optional[str] = Field(default=None, description="Alias cũ của age_by_product. Dùng để tương thích ngược.")
master_color: Optional[str] = Field(
default=None,
description=(
"Màu sắc chủ đạo. SUY LUẬN TỪ SỰ KIỆN: "
"30/4 & 2/9 -> 'Màu đỏ'. "
"Tết -> 'Màu đỏ' hoặc 'Màu vàng'. "
"Giáng sinh -> 'Màu đỏ' hoặc 'Màu trắng' hoặc 'Tông nổi bật'. "
"Valentine (nữ) -> 'Màu hồng'."
)
)
tags: List[str] = Field( tags: List[str] = Field(
default_factory=list, default_factory=list,
description=( description=(
"AI suy luận ý định khách -> chọn từ 4 TRỤC CỐ ĐỊNH (BẮT BUỘC có prefix!): " "AI suy luận ý định khách -> chọn từ các TRỤC CỐ ĐỊNH sau: "
"Trục 1 (occ:): occ:di_lam, occ:di_choi, occ:di_tiec, occ:di_hoc, occ:mac_nha, occ:the_thao, occ:di_bien, occ:du_lich, occ:da_ngoai, occ:di_ngu. " "Trục 1 (Màu & Tông): Màu đen, Màu trắng, Màu xám, Màu đỏ, Tông trung tính (Neutral), Tông sáng (Light), Tông nổi bật (Dark), ... "
"Trục 2a (wthr:): wthr:mua_he, wthr:mua_dong, wthr:giao_mua, wthr:troi_mua, wthr:troi_nang. " "Trục 2 (Dịp): Đi làm / Công sở, Đi chơi / dạo phố, Mặc nhà, Tập thể thao, Đi tiệc, Đi biển, Du lịch. "
"Trục 2b (func:): func:thoang_mat, func:giu_am, func:tham_hut, func:nhanh_kho, func:chong_uv, func:can_gio. " "Trục 3 (Mùa): Mùa hè, Mùa đông, Cả 2 mùa, Giao mùa. "
"Trục 3 (style:): style:thanh_lich, style:nang_dong, style:basic, style:ca_tinh, style:de_thuong, style:tre_trung, style:toi_gian, style:smart_casual. " "Trục 4 (Phong cách & Tính năng): Cơ bản (Basic), Năng động, Thanh lịch, Thoáng mát, Giữ ấm, Thấm hút, Chống nắng, Cản gió. "
"Trục 4 (fit:): fit:oversize, fit:slim, fit:regular, fit:wide_leg, fit:cropped, fit:relaxed. " "Trục 5 (Dáng): Oversize, Slim, Regular, Wide leg, Cropped. "
"KHÔNG tự nghĩ tag mới! PHẢI giữ prefix! Tối đa 3." "MAPPING SỰ KIỆN: 30/4 & 2/9 -> 'Màu đỏ', 'Đi chơi / dạo phố'; Tết -> 'Màu đỏ', 'Đi chơi / dạo phố'; 20/10 -> 'Đi chơi / dạo phố', 'Đi tiệc'."
) )
) )
keywords: List[str] = Field(default_factory=list, description="Tu khoa bo tro cho search.") keywords: List[str] = Field(
price_min: Optional[int] = Field(default=None, description="Gia thap nhat.") default_factory=list,
price_max: Optional[int] = Field(default=None, description="Gia cao nhat.") description=(
discount_min: Optional[int] = Field(default=None, description="% giam gia toi thieu.") "Từ khóa BỔ TRỢ AI suy luận thêm (VD: chất liệu, tính năng, sự kiện). Tối đa 2 cụm. "
discount_max: Optional[int] = Field(default=None, description="% giam gia toi da.") "MAPPING THỜI TIẾT: mưa -> 'chống thấm', 'trượt nước', 'cản gió'; lạnh -> 'giữ ấm', 'dày dặn'; hè -> 'thoáng mát', 'thấm hút'. "
discovery_mode: Optional[str] = Field(default=None, description="new/best_seller.") "MAPPING SỰ KIỆN: 30/4 & 2/9 -> 'cờ đỏ sao vàng', 'quốc khánh', 'du xuân'; Tết -> 'Tết', 'tươi tắn', 'du xuân'; 20/10 -> 'thanh lịch', 'sang trọng'; Giáng sinh -> 'giáng sinh', 'trang trí'; Valentine -> 'Valentine', 'lãng mạn'."
size: Optional[str] = Field(default=None, description="Size can tim.") )
)
price_min: Optional[int] = Field(default=None, description="Giá thấp nhất (VND).")
price_max: Optional[int] = Field(default=None, description="Giá cao nhất (VND).")
discount_min: Optional[int] = Field(
default=None,
description="NẾU khách nói 'sale', 'giảm giá', 'hàng sale', 'outlet', 'clearance' -> BẮT BUỘC set discount_min=1."
)
discount_max: Optional[int] = Field(default=None, description="% giảm giá tối đa.")
discovery_mode: Optional[str] = Field(
default=None,
description=(
"NẾU khách nói 'mới nhất', 'hàng mới về', 'collection mới' -> BẮT BUỘC 'new'. "
"NẾU khách nói 'bán chạy nhất', 'best seller', 'được mua nhiều' -> BẮT BUỘC 'best_seller'."
)
)
size: Optional[str] = Field(default=None, description="Size cần tìm (Vd: S, M, L, XL, 29, 30...).")
class SearchItem(BaseModel): class SearchItem(BaseModel):
...@@ -66,31 +128,34 @@ class SearchItem(BaseModel): ...@@ -66,31 +128,34 @@ class SearchItem(BaseModel):
model_config = {"extra": "ignore"} model_config = {"extra": "ignore"}
literal: LiteralSearch = Field(default_factory=LiteralSearch) literal: LiteralSearch = Field(default_factory=LiteralSearch, description="Tìm kiếm theo văn bản thuần túy (nguyên văn người dùng).")
inferred: InferredSearch = Field(default_factory=InferredSearch) inferred: InferredSearch = Field(default_factory=InferredSearch, description="Tìm kiếm dựa trên suy luận AI về các thuộc tính sản phẩm.")
magento_ref_code: Optional[str] = None magento_ref_code: Optional[str] = Field(
reasoning: Optional[str] = None default=None,
user_insight: Optional[Dict[str, Any]] = None description="Mã SKU sản phẩm. BẮT BUỘC ĐIỀN khi khách cho mã cụ thể HOẶC khi khách yêu cầu phối đồ/mặc cùng với sản phẩm trước đó (Lấy mã từ Latest Interest trong user_insight để gọi lại SP cũ và lấy outfit_recommendations)."
)
reasoning: Optional[str] = Field(default=None, description="Lý do AI chọn sản phẩm/thuộc tính này.")
user_insight: Optional[Dict[str, Any]] = Field(default=None, description="Thông tin insight người dùng phục vụ cá nhân hóa.")
# Legacy flat fields. # Legacy flat fields.
query: Optional[str] = None query: Optional[str] = Field(default=None, description="Câu truy vấn gốc của người dùng.")
raw_text: Optional[str] = None raw_text: Optional[str] = Field(default=None, description="Văn bản thuần của yêu cầu tìm kiếm.")
description: Optional[str] = None description: Optional[str] = Field(default=None, description="Mô tả chi tiết hơn về sản phẩm cần tìm.")
product_name: Optional[str] = None product_name: Optional[str] = Field(default=None, description="Tên sản phẩm cụ thể.")
product_line_vn: Optional[Union[str, List[str]]] = None product_line_vn: Optional[Union[str, List[str]]] = Field(default=None, description="Dòng sản phẩm.")
gender_by_product: Optional[str] = None gender_by_product: Optional[str] = Field(default=None, description="Giới tính (male, female, unisex).")
age_by_product: Optional[str] = None age_by_product: Optional[str] = Field(default=None, description="Độ tuổi (others, adult, kid).")
gender_target: Optional[str] = None gender_target: Optional[str] = Field(default=None, description="Alias cho giới tính.")
age_group: Optional[str] = None age_group: Optional[str] = Field(default=None, description="Alias cho độ tuổi.")
master_color: Optional[str] = None master_color: Optional[str] = Field(default=None, description="Màu sắc.")
tags: Optional[Union[str, List[str]]] = None tags: Optional[Union[str, List[str]]] = Field(default=None, description="Các tags phân loại (occ:, wthr:, func:, style:, fit:).")
keywords: Optional[Union[str, List[str]]] = None keywords: Optional[Union[str, List[str]]] = Field(default=None, description="Từ khóa tìm kiếm bổ trợ.")
price_min: Optional[int] = None price_min: Optional[int] = Field(default=None, description="Giá tối thiểu.")
price_max: Optional[int] = None price_max: Optional[int] = Field(default=None, description="Giá tối đa.")
discount_min: Optional[int] = None discount_min: Optional[int] = Field(default=None, description="% giảm giá tối thiểu.")
discount_max: Optional[int] = None discount_max: Optional[int] = Field(default=None, description="% giảm giá tối đa.")
discovery_mode: Optional[str] = None discovery_mode: Optional[str] = Field(default=None, description="new/best_seller.")
size: Optional[str] = None size: Optional[str] = Field(default=None, description="Kích cỡ.")
class MultiSearchParams(BaseModel): class MultiSearchParams(BaseModel):
......
"""
Store Search Tool — Tìm cửa hàng CANIFA theo địa điểm
======================================================
- Expand viết tắt tỉnh/thành (63 tỉnh VN) trước khi query
- 2-step search: strict token match → reverse LIKE fallback
- Format kết quả rõ ràng cho LLM
"""
import logging import logging
from typing import Any
from langchain_core.tools import tool from langchain_core.tools import tool
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
...@@ -10,53 +19,357 @@ logger = logging.getLogger(__name__) ...@@ -10,53 +19,357 @@ logger = logging.getLogger(__name__)
STORE_TABLE = "shared_source.chatbot_rsa_store_schedule_with_text_embedding" STORE_TABLE = "shared_source.chatbot_rsa_store_schedule_with_text_embedding"
# ═══════════════════════════════════════════════════════════════
# PROVINCE ALIAS MAP — 63 tỉnh/thành Việt Nam
# Format: "Tên đầy đủ (DB)": ["viết tắt", "không dấu", "slang"]
# ═══════════════════════════════════════════════════════════════
_PROVINCE_ALIASES: dict[str, list[str]] = {
# ── 5 Thành phố trực thuộc TW ──
"Hồ Chí Minh": ["hcm", "tphcm", "tp hcm", "sg", "sài gòn", "saigon", "ho chi minh"],
"Hà Nội": ["hn", "hanoi", "ha noi"],
"Hải Phòng": ["hp", "hai phong"],
"Đà Nẵng": ["dn", "đn", "da nang"],
"Cần Thơ": ["ct", "can tho"],
# ── Bắc Bộ ──
"Hà Giang": ["hg", "ha giang"],
"Cao Bằng": ["cb", "cao bang"],
"Bắc Giang": ["bg", "bac giang"],
"Bắc Kạn": ["bk", "bac kan"],
"Bắc Ninh": ["bn", "bac ninh"],
"Điện Biên": ["db", "dien bien"],
"Hòa Bình": ["hb", "hoa binh"],
"Hưng Yên": ["hy", "hung yen"],
"Hải Dương": ["hd", "hai duong"],
"Hà Nam": ["hnam", "ha nam"],
"Lai Châu": ["lch", "lai chau"],
"Lào Cai": ["lc", "lao cai"],
"Lạng Sơn": ["ls", "lang son"],
"Nam Định": ["nd", "nam dinh"],
"Ninh Bình": ["nb", "ninh binh"],
"Phú Thọ": ["pt", "phu tho"],
"Quảng Ninh": ["qn", "quang ninh"],
"Sơn La": ["sl", "son la"],
"Thái Bình": ["tb", "thai binh"],
"Thái Nguyên": ["tn", "thai nguyen"],
"Tuyên Quang": ["tq", "tuyen quang"],
"Vĩnh Phúc": ["vp", "vinh phuc"],
"Yên Bái": ["yb", "yen bai"],
# ── Bắc Trung Bộ ──
"Thanh Hoá": ["th", "thanh hoa"],
"Nghệ An": ["na", "nghe an"],
"Hà Tĩnh": ["ht", "ha tinh"],
"Quảng Bình": ["qb", "quang binh"],
"Quảng Trị": ["qt", "quang tri"],
"Thừa Thiên Huế": ["tth", "hue", "huế", "thua thien hue"],
# ── Nam Trung Bộ ──
"Quảng Nam": ["qnam", "quang nam"],
"Quảng Ngãi": ["qng", "quang ngai"],
"Bình Định": ["bdi", "binh dinh"],
"Phú Yên": ["py", "phu yen"],
"Khánh Hòa": ["kh", "nt", "khanh hoa", "nha trang"],
"Bình Thuận": ["bth", "binh thuan", "phan thiet"],
"Ninh Thuận": ["nth", "ninh thuan"],
# ── Tây Nguyên ──
"Gia Lai": ["gl", "gia lai"],
"Kon Tum": ["kt", "kon tum"],
"Đắk Lắk": ["dlk", "dak lak"],
"Đắk Nông": ["dkn", "dak nong"],
"Lâm Đồng": ["ld", "lam dong", "da lat", "đà lạt", "dalat"],
# ── Đông Nam Bộ ──
"Bình Dương": ["bd", "binh duong"],
"Bình Phước": ["bp", "binh phuoc"],
"Đồng Nai": ["dnai", "dong nai"],
"Tây Ninh": ["tnin", "tay ninh"],
"Bà Rịa – Vũng Tàu": ["brvt", "vt", "vung tau", "ba ria"],
# ── Tây Nam Bộ (ĐBSCL) ──
"An Giang": ["ag", "an giang"],
"Bạc Liêu": ["bl", "bac lieu"],
"Bến Tre": ["bt", "ben tre"],
"Cà Mau": ["cm", "ca mau"],
"Đồng Tháp": ["dt", "dong thap"],
"Hậu Giang": ["hgi", "hau giang"],
"Kiên Giang": ["kg", "kien giang", "phu quoc", "phú quốc"],
"Long An": ["la", "long an"],
"Sóc Trăng": ["st", "soc trang"],
"Tiền Giang": ["tg", "tien giang"],
"Trà Vinh": ["tv", "tvi", "tra vinh"],
"Vĩnh Long": ["vl", "vinh long"],
}
# Build reverse lookup: alias → province name (auto-generated)
PROVINCE_ABBR: dict[str, str] = {}
for _province, _aliases in _PROVINCE_ALIASES.items():
for _alias in _aliases:
PROVINCE_ABBR[_alias] = _province
# ═══════════════════════════════════════════════════════════════
# TOOL DEFINITION
# ═══════════════════════════════════════════════════════════════
class StoreSearchInput(BaseModel): class StoreSearchInput(BaseModel):
location: str = Field( location: str = Field(
description="Tên quận/huyện/tỉnh/thành phố hoặc địa chỉ cụ thể mà khách hàng muốn tìm cửa hàng CANIFA. VD: 'Hoàng Mai', 'Cầu Giấy', 'Đà Nẵng', 'Vincom Bà Triệu'." description=(
"Tên quận/huyện/tỉnh/thành phố hoặc địa chỉ cụ thể mà khách hàng muốn tìm. "
"TUYỆT ĐỐI KHÔNG bao gồm các từ dư thừa như 'tìm cửa hàng', 'ở', 'tại', 'cho tôi'. "
"Chỉ trích xuất ĐÚNG danh từ chỉ địa danh (VD: khách hỏi 'tìm cửa hàng ở vp' -> location: 'vp'). "
"VD: 'Hoàng Mai', 'Cầu Giấy', 'Đà Nẵng', 'Vincom Bà Triệu'."
)
) )
@tool("canifa_store_search", args_schema=StoreSearchInput) @tool("canifa_store_search", args_schema=StoreSearchInput)
async def canifa_store_search(location: str) -> str: async def canifa_store_search(location: str) -> str:
""" """
Tìm kiếm cửa hàng CANIFA theo địa điểm/khu vực. AI PLANNER & RESPONDER - TÌM KIẾM CỬA HÀNG CANIFA
1. LÚC GỌI TOOL (PLANNER):
- Chỉ trích xuất đúng tên vị trí (VD: 'vp', 'Hà Nội', 'Cầu Giấy') vào tham số location. Không kèm từ thừa.
- Nếu câu hỏi mơ hồ (không rõ khu vực/tỉnh/thành), HỎI LẠI khách bằng text thuần, KHÔNG gọi tool.
2. LÚC TRẢ LỜI (RESPONDER):
- Format kết quả tìm kiếm thành câu trả lời NGẮN GỌN (tối đa 3 cửa hàng gần nhất).
- Mỗi cửa hàng ghi gọn 3 dòng: Tên + Địa chỉ + SĐT. Bỏ lịch trừ khi khách hỏi giờ mở cửa.
- ⚠️ NẾU TOOL TRẢ VỀ CẢNH BÁO "KHÔNG có cửa hàng tại khu vực X": BẮT BUỘC phải nói rõ "Dạ, CANIFA hiện chưa có cửa hàng tại [X] ạ.", sau đó mới gợi ý các cửa hàng gần nhất (nếu có). KHÔNG được lơ đi cảnh báo này.
- Ngôn ngữ thân thiện, tự nhiên. KHÔNG tự bịa địa chỉ/SĐT.
""" """
logger.info(f"🏪 [Store Search] Location: {location} (StarRocks)") logger.info(f"🏪 [Store Search] Location: {location}")
try: try:
sr = get_db_connection() sr = get_db_connection()
# ── Step -1: Clear conversational fillers (Cứu cánh khi AI ngu) ──
raw_lower = location.lower().strip()
fillers = [
"tìm cửa hàng ở", "tìm cửa hàng tại", "cửa hàng ở", "cửa hàng tại",
"có cửa hàng ở", "có cửa hàng tại", "cho địa chỉ ở", "địa chỉ ở",
"địa chỉ tại", "ở khu vực", "khu vực", "ở", "tại", "tìm"
]
for f in fillers:
if raw_lower.startswith(f):
raw_lower = raw_lower.replace(f, "", 1).strip()
# Also handle if it's somewhere inside (less safe but covers cases like "mình muốn tìm cửa hàng ở vp")
raw_lower = raw_lower.replace(f" {f} ", " ")
# Clean up any leading/trailing spaces again
raw_lower = raw_lower.strip()
location = raw_lower # Update location to cleaned version
# ── Step 0: Expand viết tắt tỉnh ──
if raw_lower in PROVINCE_ABBR:
expanded = PROVINCE_ABBR[raw_lower]
logger.info(f"📝 Alias expanded: '{raw_lower}' → '{expanded}'")
location = expanded
# ── Step 0.5: Clean prefix ──
clean = location.lower().strip() clean = location.lower().strip()
for prefix in ["quận ", "huyện ", "tỉnh ", "thành phố ", "tp. ", "tp "]: for prefix in ["quận ", "huyện ", "tỉnh ", "thành phố ", "tp. ", "tp "]:
clean = clean.replace(prefix, "") clean = clean.replace(prefix, "")
clean = clean.strip() clean = clean.strip()
tokens = list(dict.fromkeys( words = [w for w in clean.replace(",", " ").split() if w.strip()]
t for t in clean.replace(',', ' ').split() if t.strip()
))
if not tokens: if not words:
return "Vui lòng cho em biết khu vực bạn muốn tìm cửa hàng CANIFA." return (
"Vui lòng cho em biết khu vực bạn muốn tìm cửa hàng CANIFA "
"(ví dụ: Hoàng Mai, Cầu Giấy, Đà Nẵng...)."
)
text_col = "LOWER(concat_ws(' ', store_name, address, city, state))" text_col = "LOWER(concat_ws(' ', store_name, address, city, state))"
def _build_sql(where_clause: str) -> str: # ═══════════════════════════════════════════════════════════
return f""" # SINGLE QUERY — Phrase Sliding Window
#
# Giữ nguyên THỨ TỰ từ, match CỤM LIÊN TIẾP trong DB.
# "nội bài đông anh" (4 words) → sinh các cụm:
# len=4: "nội bài đông anh"
# len=3: "nội bài đông", "bài đông anh"
# len=2: "nội bài", "bài đông", "đông anh"
#
# Score = độ dài cụm match dài nhất.
# "đông anh" match → score 2. Stores ở Hà Đông (chỉ match "đông") → 0 → filtered.
# ═══════════════════════════════════════════════════════════
if len(words) == 1:
# ── Single word: simple LIKE ──
sql = f"""
SELECT store_name, address, city, state, phone_number, SELECT store_name, address, city, state, phone_number,
schedule_name, time_open_today, time_close_today schedule_name, time_open_today, time_close_today,
1 AS match_score
FROM {STORE_TABLE} FROM {STORE_TABLE}
WHERE {where_clause} WHERE {text_col} LIKE '%{words[0]}%'
ORDER BY state, city, store_name ORDER BY state, city, store_name
LIMIT 20 LIMIT 20
""" """
else:
# ── Multi-word: consecutive phrase matching ──
# Sinh tất cả cụm liên tiếp ≥ 2 từ, ordered by length DESC
phrases: list[tuple[str, int]] = []
for length in range(len(words), 1, -1):
for start in range(len(words) - length + 1):
phrase = " ".join(words[start:start + length])
phrases.append((phrase, length))
# Build CASE WHEN → score = longest matching phrase
case_when = " ".join(
f"WHEN {text_col} LIKE '%{p}%' THEN {score}"
for p, score in phrases
)
score_expr = f"CASE {case_when} ELSE 0 END"
# WHERE: OR tất cả phrases (ít nhất 1 cụm ≥ 2 từ phải match)
or_conds = " OR ".join(
f"{text_col} LIKE '%{p}%'" for p, _ in phrases
)
sql = f"""
SELECT store_name, address, city, state, phone_number,
schedule_name, time_open_today, time_close_today,
({score_expr}) AS match_score
FROM {STORE_TABLE}
WHERE ({or_conds})
ORDER BY match_score DESC, state, city, store_name
LIMIT 20
"""
and_conds = [f"{text_col} LIKE '%{tk}%'" for tk in tokens] results = await sr.execute_query_async(sql)
results = await sr.execute_query_async(_build_sql(' AND '.join(and_conds))) logger.info(
f"📊 Phrase search: {len(results)} stores, words={words}"
)
# ═══════════════════════════════════════════════════════════
# FALLBACK — Reverse LIKE (chỉ khi phrase search = 0)
# ═══════════════════════════════════════════════════════════
if not results and len(words) >= 2:
city_stripped = (
"LOWER(TRIM(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE("
"city, 'Quận ', ''), 'Huyện ', ''), 'Thành phố ', ''), "
"'Thị xã ', ''), 'TP. ', '')))"
)
state_lower = "LOWER(TRIM(state))"
fallback_sql = f"""
SELECT store_name, address, city, state, phone_number,
schedule_name, time_open_today, time_close_today,
1 AS match_score
FROM {STORE_TABLE}
WHERE (LOCATE({city_stripped}, '{clean}') > 0
AND LENGTH({city_stripped}) > 1)
OR (LOCATE({state_lower}, '{clean}') > 0
AND LENGTH({state_lower}) > 1)
ORDER BY state, city, store_name
LIMIT 20
"""
results = await sr.execute_query_async(fallback_sql)
logger.info(f"📊 Reverse-LIKE fallback: {len(results)} stores")
logger.info(
f"📊 Store search: {len(results)} stores for '{location}'"
)
if not results: if not results:
return f"Không tìm thấy cửa hàng CANIFA tại khu vực '{location}'." return (
f"Không tìm thấy cửa hàng CANIFA tại khu vực '{location}'. "
f"Khách hàng có thể liên hệ hotline 1800 6061 "
f"để được hỗ trợ tìm cửa hàng gần nhất."
)
lines = [] # ── Format kết quả cho LLM ──
return _format_results(results, location, words)
except Exception as e:
logger.error(f"❌ Error in canifa_store_search: {e}")
return (
"Tôi đang gặp khó khăn khi tìm kiếm cửa hàng. "
"Bạn có thể liên hệ hotline 1800 6061 để được hỗ trợ."
)
def _format_results(
results: list[dict[str, Any]],
location: str,
words: list[str],
) -> str:
"""Format store search results with enriched location context."""
# ── 1. Phân tích khu vực thực tế từ kết quả DB ──
states: set[str] = set()
cities: set[str] = set()
all_text_lower = ""
for r in results: for r in results:
city = r.get("city", "")
state = r.get("state", "")
if state:
states.add(state)
if city:
cities.add(city)
all_text_lower += (
f" {r.get('store_name','')} {r.get('address','')}"
f" {city} {state}"
).lower()
# ── 2. Tìm cụm từ user nhập mà KHÔNG xuất hiện trong kết quả ──
# Chỉ match cụm >= 2 từ để tránh false positive
# ("bình" xuất hiện trong "Bình Phú" ≠ "Bình Xuyên")
matched_indices: set[int] = set()
# Bước 2a: Match cụm >= 2 từ (chính xác)
for length in range(len(words), 1, -1):
for start in range(len(words) - length + 1):
if any(i in matched_indices for i in range(start, start + length)):
continue
phrase = " ".join(words[start:start + length])
if phrase in all_text_lower:
matched_indices.update(range(start, start + length))
# Bước 2b: Chỉ match single word NẾU nó là từ duy nhất
if len(words) == 1 and words[0] in all_text_lower:
matched_indices.add(0)
# Gom các từ chưa match thành cụm liên tiếp
unmatched_phrases: list[str] = []
i = 0
while i < len(words):
if i not in matched_indices:
start = i
while i < len(words) and i not in matched_indices:
i += 1
unmatched_phrases.append(" ".join(words[start:i]).title())
else:
i += 1
# ── 3. Build header rõ ràng ──
state_str = ", ".join(sorted(states)) if states else ""
city_str = " / ".join(sorted(cities)) if cities else ""
header_parts = [f"Tìm thấy {len(results)} cửa hàng CANIFA"]
if state_str and city_str:
header_parts.append(f"tại {city_str} (tỉnh/thành phố {state_str})")
elif state_str:
header_parts.append(f"tại tỉnh/thành phố {state_str}")
else:
header_parts.append(f"tại khu vực '{location}'")
header = " ".join(header_parts) + ":"
# Ghi rõ phần KHÔNG match + hướng dẫn bot cách trả lời
if unmatched_phrases:
for phrase in unmatched_phrases:
header += (
f"\n⚠️ CANIFA hiện KHÔNG có cửa hàng tại khu vực "
f"'{phrase}'. Hãy thông báo rõ cho khách hàng rằng "
f"'{phrase}' không có cửa hàng CANIFA, và gợi ý "
f"các cửa hàng gần nhất bên dưới."
)
# ── 4. Format từng store ──
# Khi có unmatched → chỉ hiển thị tối đa 5 store gần nhất
display_results = results[:5] if unmatched_phrases else results
lines = []
for r in display_results:
name = r.get("store_name", "") name = r.get("store_name", "")
addr = r.get("address", "") addr = r.get("address", "")
city = r.get("city", "") city = r.get("city", "")
...@@ -66,11 +379,15 @@ async def canifa_store_search(location: str) -> str: ...@@ -66,11 +379,15 @@ async def canifa_store_search(location: str) -> str:
t_open = r.get("time_open_today", "") t_open = r.get("time_open_today", "")
t_close = r.get("time_close_today", "") t_close = r.get("time_close_today", "")
store_info = f"🏪 {name}\n 📍 Địa chỉ: {addr}, {city}, {state}\n 📞 ĐT: {phone}\n 🕐 Lịch: {schedule} ({t_open}-{t_close})" parts = [f"🏪 {name}"]
lines.append(store_info) addr_full = ", ".join(filter(None, [addr, city, state]))
parts.append(f" 📍 Địa chỉ: {addr_full}")
parts.append(f" 📞 ĐT: {phone}")
sched_str = f" 🕐 Lịch: {schedule}"
if t_open and t_close:
sched_str += f" (Hôm nay: {t_open}-{t_close})"
parts.append(sched_str)
return f"Tìm thấy {len(results)} cửa hàng CANIFA tại '{location}':\n\n" + "\n\n".join(lines) lines.append("\n".join(parts))
except Exception as e: return header + "\n\n" + "\n\n".join(lines)
logger.error(f"❌ Error in canifa_store_search: {e}")
return "Tôi đang gặp khó khăn khi tìm kiếm cửa hàng."
"""
Hard Pattern Detection — Bắt các mẫu hỏi CỨNG đòi hỏi tham chiếu.
Các pattern phổ biến:
• "rẻ hơn", "giá rẻ hơn" → so sánh giá
• "đắt hơn", "cao hơn" → so sánh giá
• "tương tự", "giống như" → lấy đặc điểm SP tham chiếu
• "như cái trước", "cái kia" → refer đến SP đã nói
• "còn size...", "size còn không" → check size variant
• "còn màu...", "màu gì còn" → check color variant
• "còn hàng không" → check stock
"""
import re
from dataclasses import dataclass
from typing import Any
@dataclass
class PatternMatch:
"""Kết quả pattern detection."""
pattern_type: str # "cheaper_than", "similar_to", "check_size", ...
reference_sku: str | None = None
reference_product_line: str | None = None
reference_price: float | None = None
reference_color: str | None = None
reference_size: str | None = None
# Target criteria (nếu có)
target_price_max: float | None = None
target_price_min: float | None = None
target_size: str | None = None
target_color: str | None = None
confidence: float = 0.0 # 0.0 - 1.0
raw_match: str = "" # Văn bản khớp
class HardPatternDetector:
"""Detect và trích xuất reference từ query user."""
# ═══════════════════════════════════════════════════════════════
# PATTERN REGEXES
# ═══════════════════════════════════════════════════════════════
# 1. GIÁ RẺ HƠN
CHEAPER_PATTERNS = [
r"(?:rẻ|giá)\s+(?:hơn|thấp hơn|xuống hơn)",
r"có\s+(?:cái)?\s+(?:nào)?\s+(?:rẻ|giá)\s+(?:hơn|thấp hơn)",
r"tìm\s+(?:cái)?\s+(?:rẻ|giá)\s+(?:hơn|thấp hơn)",
r"(?:rẻ|giá)\s+(?:hơn|thấp hơn)\s+(?:cái|chiếc)?",
r"có.*rẻ.*hơn",
]
# 2. GIÁ ĐẮT HƠN
EXPENSIVE_PATTERNS = [
r"(?:đắt|giá)\s+(?:hơn|cao hơn)",
r"có\s+(?:cái)?\s+(?:nào)?\s+(?:đắt|giá)\s+(?:hơn|cao hơn)",
r"(?:đắt|giá)\s+(?:hơn|cao hơn)\s+(?:cái|chiếc)?",
r"có.*đắt.*hơn",
]
# 3. TƯƠNG TỰ / GIỐNG NHƯ
SIMILAR_PATTERNS = [
r"(?:giống|tương tự|như|cùng)\s+(?:như|với)?\s+(?:cái|chiếc)?\s*(?:đó|trước|ban\s*nãy|cạnh|là)?",
r"còn\s+(?:cái)?\s+(?:nào|còn)?\s+(?:tương tự|giống)\s+(?:như|với)?",
r"(?:có|tìm)\s+(?:cái)?\s+(?:nào|còn)?\s+(?:giống|tương tự)",
r"giống\s+(?:như)?\s+(?:cái)?(?:đó|trước|kia)",
]
# 4. CHECK SIZE (còn size?, size còn không, có size X không)
SIZE_CHECK_PATTERNS = [
r"(?:còn|có)\s+size\s+([A-Z0-9]+)",
r"size\s+([A-Z0-9]+)\s+(?:còn|có)",
r"(?:còn|có)\s+(?:size\s+)?([A-Z0-9]+)\s+(?:không|ko|kh)",
]
# 5. CHECK MÀU (còn màu?, màu gì còn, có màu X không)
COLOR_CHECK_PATTERNS = [
r"(?:còn|có)\s+(?:màu\s+)?(\w+)\s+(?:còn|không|ko)",
r"màu\s+(\w+)\s+(?:còn|có)",
r"(?:màu\s+)?(\w+)\s+(?:còn|có)\s+(?:không|ko)",
]
# 6. CHECK TỒN KHO (còn hàng không, còn ko, có hàng ko)
STOCK_CHECK_PATTERNS = [
r"(?:còn|có)\s+(?:hàng|kho)\s+(?:không|ko|kh)",
r"(?:hàng|kho)\s+(?:còn|có)\s+(?:không|ko|kh)",
]
# 7. REFERENCE SKU (nhắc trực tiếp mã: 8TP25A002, 5TS25S021-SR079)
SKU_PATTERN = r"\b([A-Z0-9]{4,}-[A-Z0-9]{3,}(?:-[A-Z0-9]{3,})?)\b"
# 8. REFERENCE BY POSITION (cái trước, cái kia, cái đầu tiên)
POSITION_REFERENCE = {
"trước": 0, # SP trước đó (index -1)
"đầu": 0, # SP đầu tiên trong list
"đầu tiên": 0,
"kia": -1, # SP cuối/cuối cùng (gần nhất)
"cuối": -1,
"cuối cùng": -1,
"sau": -1,
}
def detect(self, query: str, context_products: list[dict[str, Any]] = None) -> PatternMatch:
"""
Detect pattern trong query user.
Args:
query: Câu hỏi của user (raw text)
context_products: List sản phẩm đã được giới thiệu trong conversation
[{"sku": "...", "product_line_vn": "...", "sale_price": ...}, ...]
Returns:
PatternMatch với đầy đủ thông tin pattern.
"""
query_lower = query.lower().strip()
match = PatternMatch(pattern_type="unknown")
# 1. Detect CHEAPER THAN
if self._match_any_pattern(query_lower, self.CHEAPER_PATTERNS):
match.pattern_type = "cheaper_than"
match.confidence = 0.9
match = self._extract_reference_info(query, context_products, match)
if match.reference_price:
match.target_price_max = match.reference_price * 0.95 # Rẻ hơn 5%
# 2. Detect EXPENSIVE THAN
elif self._match_any_pattern(query_lower, self.EXPENSIVE_PATTERNS):
match.pattern_type = "more_expensive_than"
match.confidence = 0.9
match = self._extract_reference_info(query, context_products, match)
if match.reference_price:
match.target_price_min = match.reference_price * 1.05 # Đắt hơn 5%
# 3. Detect SIMILAR TO
elif self._match_any_pattern(query_lower, self.SIMILAR_PATTERNS):
match.pattern_type = "similar_to"
match.confidence = 0.8
match = self._extract_reference_info(query, context_products, match)
# 4. Detect SIZE CHECK
elif size_match := re.search(r"(?:còn|có)\s+size\s+([A-Z0-9]+)", query_lower):
match.pattern_type = "check_size"
match.confidence = 0.95
match.target_size = size_match.group(1).upper()
match = self._extract_reference_info(query, context_products, match)
# 5. Detect COLOR CHECK
elif color_match := re.search(r"(?:màu\s+)?(\w+)\s+(?:còn|có)", query_lower):
# Filter out common stop words
color = color_match.group(1)
if color not in ["có", "còn", "ko", "không", "màu"]:
match.pattern_type = "check_color"
match.confidence = 0.9
match.target_color = color
match = self._extract_reference_info(query, context_products, match)
# 6. Detect STOCK CHECK
elif self._match_any_pattern(query_lower, self.STOCK_CHECK_PATTERNS):
match.pattern_type = "check_stock"
match.confidence = 0.9
match = self._extract_reference_info(query, context_products, match)
# 7. Detect direct SKU reference
elif sku_match := re.search(self.SKU_PATTERN, query):
match.pattern_type = "direct_sku"
match.confidence = 1.0
match.reference_sku = sku_match.group(1)
return match
def _match_any_pattern(self, text: str, patterns: list[str]) -> bool:
"""Check nếu text match bất kỳ pattern nào."""
return any(re.search(p, text) for p in patterns)
def _extract_reference_info(self, query: str, context_products: list[dict[str, Any]],
current_match: PatternMatch) -> PatternMatch:
"""
Trích xuất thông tin reference SP từ context.
Logic:
1. Nếu query có SKU cụ thể → tìm trực tiếp
2. Nếu query có tên product_line ("áo polo kia") → filter context
3. Nếu query có từ vị trí ("cái trước", "cái kia") → lấy theo index
4. Default: lấy SP gần nhất (cuối context)
"""
if not context_products:
return current_match
query_lower = query.lower()
# --- Case 1: Direct SKU trong query ---
sku_match = re.search(self.SKU_PATTERN, query)
if sku_match:
sku = sku_match.group(1)
for p in context_products:
if p.get("sku") == sku or p.get("magento_ref_code") == sku:
self._populate_match_from_product(p, current_match)
return current_match
# --- Case 2: Reference by position ---
for pos_word, idx_offset in self.POSITION_REFERENCE.items():
if pos_word in query_lower:
idx = idx_offset if idx_offset >= 0 else len(context_products) + idx_offset
if 0 <= idx < len(context_products):
self._populate_match_from_product(context_products[idx], current_match)
return current_match
# --- Case 3: Reference by product_line ---
# Từ khóa product_line phổ biến
product_keywords = [
"áo phông", "áo polo", "áo sơ mi", "áo khoác", "áo len",
"quần jean", "quần khaki", "quần âu", "quần soóc", "váy", "chân váy",
"bộ", "đồ lót", "quần lót", "áo dài", "blazer", "cardigan"
]
for kw in product_keywords:
if kw in query_lower:
# Tìm SP trong context có product_line chứa kw
for p in reversed(context_products): # Ưu tiên SP gần nhất
if kw in p.get("product_line_vn", "").lower():
self._populate_match_from_product(p, current_match)
return current_match
# --- Case 4: Default → SP gần nhất (last in context) ---
if context_products:
self._populate_match_from_product(context_products[-1], current_match)
return current_match
def _populate_match_from_product(self, product: dict[str, Any], match: PatternMatch) -> None:
"""Fill match info từ product dict."""
match.reference_sku = product.get("sku") or product.get("magento_ref_code") or product.get("internal_ref_code")
match.reference_product_line = product.get("product_line_vn") or product.get("product_line")
match.reference_price = float(product.get("sale_price") or product.get("price") or 0)
match.reference_color = product.get("master_color") or product.get("color")
match.reference_size = None # Size thường không có trong product summary
"""
Lead Search Tool - LangChain @tool cho AI goi.
AI sinh keywords + tags -> Tool search -> cascade -> Tra ket qua.
+ Tu dong goi Canifa Stock API -> chi tra SP CON HANG.
+ Doc outfit tu SQLite canifa_ai_dump.sqlite
Architecture:
CO DINH (luon giu): product_type + color + gender + price + size
BIEN DOI (fallback):
Tang 1: CO DINH + keywords (NGRAMBF tren product_name hoac LIKE tren description)
Tang 2: CO DINH + tags (BITMAP hoac LIKE full phrase)
Tang 3: CHI CO DINH (product_type + color + gender + size + price)
Tang 4: Drop gender
Tang 5-6: Price relaxation (1.5x, 2x)
Tang 7: Bo price hoan toan
"""
import json
import logging
import os as _os
import re
import sqlite3
import time
import httpx
from pydantic import BaseModel, Field
from common.starrocks_connection import get_db_connection
from .hard_patterns import HardPatternDetector
from .product_mapping import get_related_lines, resolve_product_line
logger = logging.getLogger(__name__)
# Global hard pattern detector
_hard_pattern_detector = HardPatternDetector()
try:
from langfuse import get_client as _get_langfuse
_LANGFUSE_AVAILABLE = True
except ImportError:
_get_langfuse = None
_LANGFUSE_AVAILABLE = False
# ═══════════════════════════════════════════════
# Stock API — Kiểm tra tồn kho Canifa
# ═══════════════════════════════════════════════
CANIFA_STOCK_API = "https://canifa.com/v1/middleware/stock_get_stock_list_parent"
_STOCK_TIMEOUT = 1.5 # seconds
async def _fetch_stock_batch(base_codes: list[str]) -> dict[str, list[dict]]:
if not base_codes:
return {}, 0.0, False
sku_string = ",".join(base_codes)
url = f"{CANIFA_STOCK_API}?skus={sku_string}"
t0 = time.time()
try:
async with httpx.AsyncClient(timeout=_STOCK_TIMEOUT) as client:
resp = await client.get(url)
resp.raise_for_status()
data = resp.json()
elapsed = round((time.time() - t0) * 1000, 1)
stock_map: dict[str, list[dict]] = {}
results = data.get("result", [])
if isinstance(results, list):
for item in results:
sku_full = item.get("sku", "")
qty = item.get("qty", 0) or 0
if not sku_full:
continue
parts = sku_full.rsplit("-", 1)
if len(parts) == 2:
color_code = parts[0]
size = parts[1]
status = "còn hàng" if qty > 0 else "hết hàng"
stock_map.setdefault(color_code, []).append({
"size": size, "qty": qty, "status": status
})
logger.info("📦 Stock API: %d base_codes -> %d color_codes | %.0fms", len(base_codes), len(stock_map), elapsed)
return stock_map, elapsed, False
except httpx.TimeoutException:
elapsed = round((time.time() - t0) * 1000, 1)
logger.warning("⏰ Stock API timeout (%.1fs=%.0fms)", _STOCK_TIMEOUT, elapsed)
return {}, elapsed, True
except Exception as e:
elapsed = round((time.time() - t0) * 1000, 1)
logger.warning("⚠️ Stock API error (%.0fms): %s", elapsed, e)
return {}, elapsed, False
async def _enrich_with_stock(products: list[dict]) -> tuple[list[dict], bool, float, bool]:
if not products:
return [], False, 0.0, False
base_codes = list({p.get("internal_ref_code", "") for p in products if p.get("internal_ref_code")})
if not base_codes:
return products, False, 0.0, False
stock_map, stock_api_ms, timed_out = await _fetch_stock_batch(base_codes)
if not stock_map:
return products, False, stock_api_ms, timed_out
in_stock = []
dropped = 0
for p in products:
color_code = p.get("product_color_code", "")
if color_code in stock_map:
sizes_detail = stock_map[color_code]
total_qty = sum(s["qty"] for s in sizes_detail)
if total_qty > 0:
p["_stock_detail"] = sizes_detail
p["_total_qty"] = total_qty
in_stock.append(p)
else:
dropped += 1
logger.info("📦 Stock filter: %d -> %d in-stock, %d dropped | api=%.0fms", len(products), len(in_stock), dropped, stock_api_ms)
return in_stock if in_stock else products[:5], True, stock_api_ms, timed_out
# ═══════════════════════════════════════════════
# SQLite local DB path
# ═══════════════════════════════════════════════
from common.constants import SQLITE_DB_PATH
TABLE_NAME = "test_db.magento_product_dimension_with_text_embedding"
SELECT_COLUMNS = """
internal_ref_code,
magento_ref_code,
product_color_code,
product_name,
master_color,
product_color_name,
product_image_url_thumbnail,
product_web_url,
sale_price,
original_price,
COALESCE(discount_amount, 0) AS discount_amount,
ROUND(CASE WHEN original_price > 0
THEN ((original_price - sale_price) / original_price * 100)
ELSE 0 END, 0) AS discount_percent,
age_by_product,
gender_by_product,
product_line_vn,
COALESCE(quantity_sold, 0) AS quantity_sold,
COALESCE(is_new_product, 0) AS is_new_product,
size_scale,
description_text,
tags,
similar_items,
outfit_recommendations
"""
TAGS_TO_OCCASION: dict[str, str] = {
"occ:di_lam": "di_lam",
"occ:di_choi": "di_choi",
"occ:di_tiec": "di_tiec",
"occ:di_hoc": "di_choi",
"occ:mac_nha": "mac_nha",
"occ:the_thao": "the_thao",
"occ:di_bien": "di_choi",
"occ:du_lich": "di_choi",
"occ:da_ngoai": "di_choi",
"occ:di_ngu": "mac_nha",
"occ:hang_ngay": "hang_ngay",
}
def _resolve_occasion(tags: list[str]) -> str:
for tag in (tags or []):
occ = TAGS_TO_OCCASION.get(tag.lower())
if occ:
return occ
return "hang_ngay"
EVENT_FALLBACKS: list[dict] = [
{
"patterns": [
"30/4", "30-4", "giai phong", "giải phóng", "giai phong mien nam", "giải phóng miền nam",
],
"master_color": "Màu đỏ",
"tags": ["Màu đỏ", "Đi chơi / dạo phố"],
"keywords": ["cờ đỏ sao vàng", "30/4"],
},
{
"patterns": [
"2/9", "2-9", "quoc khanh", "quốc khánh", "le quoc khanh", "lễ quốc khánh",
],
"master_color": "Màu đỏ",
"tags": ["Màu đỏ", "Đi chơi / dạo phố"],
"keywords": ["quốc khánh", "lễ quốc khánh"],
},
]
def _normalize_text(text: str) -> str:
import unicodedata
lowered = text.lower().strip()
return "".join(
ch for ch in unicodedata.normalize("NFD", lowered)
if unicodedata.category(ch) != "Mn"
)
def _text_contains_pattern(text: str, pattern: str) -> bool:
if not text or not pattern:
return False
if any(ch.isdigit() for ch in pattern) or "/" in pattern or "-" in pattern:
return pattern in text
import re
escaped = re.escape(pattern)
return re.search(rf"(?<!\w){escaped}(?!\w)", text) is not None
def _apply_event_fallback(inf: "InferredSearch", text: str) -> bool:
if not text:
return False
normalized = _normalize_text(text)
for rule in EVENT_FALLBACKS:
if any(_text_contains_pattern(normalized, pat) for pat in rule["patterns"]):
if not inf.master_color:
inf.master_color = rule["master_color"]
if not inf.tags:
inf.tags = list(rule["tags"])
if not inf.keywords:
inf.keywords = list(rule["keywords"])
return True
return False
class LiteralSearch(BaseModel):
"""Lane 1: Tìm NGUYÊN VĂN — không filter, chỉ LIKE raw text."""
model_config = {"extra": "forbid"}
raw_text: str = Field(
default="",
description=(
"Câu search NGUYÊN VĂN từ user. Copy y hệt phần liên quan đến sản phẩm. "
"VD: user nói 'tìm áo 30/4' -> raw_text='áo 30/4'. "
"VD: user nói 'có quần jean ống rộng ko' -> raw_text='quần jean ống rộng'. "
"KHÔNG suy luận, KHÔNG thêm bớt. Giữ nguyên ý user."
),
)
class InferredSearch(BaseModel):
"""Lane 2: AI SUY LUẬN — đầy đủ filter structured."""
model_config = {"extra": "ignore"}
product_line_vn: list[str] = Field(
default=[],
description=(
"Dòng sản phẩm. CHÍNH XÁC lời user nói: 'áo phông', 'váy liền', 'đồ lót', 'quần jean'... "
"Tool sẽ tự động chuẩn hoá từ đồng nghĩa (VD: 'áo thun' -> 'Áo phông'). "
"Nếu khách nói chung chung ('đồ mùa đông', 'đồ tập') hoặc không nhắc, để []. "
),
)
gender_by_product: str | None = Field(
default=None,
description="Giới tính. GIÁ TRỊ HỢP LỆ: women, men, boy, girl, unisex, newborn",
)
age_by_product: str | None = Field(
default=None,
description="Độ tuổi. GIÁ TRỊ HỢP LỆ: adult, kid, others",
)
master_color: str | None = Field(
default=None,
description=(
"Màu sắc AI SUY LUẬN từ ngữ cảnh. "
"VD: '30/4' -> color='đỏ' (cờ đỏ sao vàng). 'áo trắng' -> color='trắng'."
),
)
tags: list[str] = Field(
default=[],
description=(
"AI suy luận ý định khách -> chọn từ 4 TRỤC CỐ ĐỊNH (BẮT BUỘC có prefix!): "
"Trục 1 (occ:): occ:di_lam, occ:di_choi, occ:di_tiec, occ:di_hoc, occ:mac_nha, occ:the_thao, occ:di_bien, occ:du_lich, occ:da_ngoai, occ:di_ngu. "
"Trục 2a (wthr:): wthr:mua_he, wthr:mua_dong, wthr:giao_mua, wthr:troi_mua, wthr:troi_nang. "
"Trục 2b (func:): func:thoang_mat, func:giu_am, func:tham_hut, func:nhanh_kho, func:chong_uv, func:can_gio. "
"Trục 3 (style:): style:thanh_lich, style:nang_dong, style:basic, style:ca_tinh, style:de_thuong, style:tre_trung, style:toi_gian, style:smart_casual. "
"Trục 4 (fit:): fit:oversize, fit:slim, fit:regular, fit:wide_leg, fit:cropped, fit:relaxed. "
"KHÔNG tự nghĩ tag mới! PHẢI giữ prefix! Tối đa 3."
)
)
keywords: list[str] = Field(
default=[],
description=(
"Từ khóa BỔ TRỢ AI suy luận thêm (chất liệu, tính năng...). "
"VD: '30/4' -> keywords=['cờ đỏ sao vàng']. 'đi biển' -> keywords=['thoáng mát']. "
"Tối đa 2."
)
)
price_min: int | None = Field(default=None, description="Giá thấp nhất (VND)")
price_max: int | None = Field(default=None, description="Giá cao nhất (VND)")
discount_min: int | None = Field(default=None, description="% giảm giá TỐI THIỂU.")
discount_max: int | None = Field(default=None, description="% giảm giá TỐI ĐA.")
discovery_mode: str | None = Field(default=None, description="'new' = hàng mới, 'best_seller' = bán chạy.")
size: str | None = Field(
default=None,
description="Size: XS/S/M/L/XL/XXL/3XL/4XL hoặc trẻ em 80-164cm.",
)
class LeadSearchInput(BaseModel):
"""
Dual-Lane Search Input.
- literal: Lane 1 — raw text NGUYÊN VĂN từ user (LIKE thuần, không filter)
- inferred: Lane 2 — AI suy luận structured filters
"""
model_config = {"extra": "ignore"}
literal: LiteralSearch = Field(
default_factory=LiteralSearch,
description="Lane 1: Câu search nguyên văn từ user. Chỉ LIKE, KHÔNG filter.",
)
inferred: InferredSearch = Field(
default_factory=InferredSearch,
description="Lane 2: AI suy luận — đầy đủ product_line, color, tags, gender, price...",
)
magento_ref_code: str | None = Field(default=None, description="Ma SKU cu the.")
reasoning: str | None = Field(default=None, description="SUY LUẬN TẠI SAO bạn chọn params này.")
# ======================================================
# UNDERWEAR / ĐỒ LÓT — Hard Override (Tool-level)
# Khi detect product_line_vn chứa đồ lót → drop gender + age
# để tìm cả nam/nữ, cả người lớn/trẻ em.
# Logic này BẮT BUỘC nằm ở tool, KHÔNG phụ thuộc prompt.
# ======================================================
UNDERWEAR_PRODUCT_LINES: set[str] = {
"Quần lót", "Quần lót đùi", "Quần lót tam giác",
"Áo lót", "Áo bra active",
}
def _is_underwear_request(product_lines: list[str]) -> bool:
"""Check nếu TẤT CẢ product_line_vn đều là đồ lót."""
if not product_lines:
return False
resolved_lines: set[str] = set()
for line in product_lines:
if not line:
continue
resolved = resolve_product_line(line)
for r in resolved:
expanded = get_related_lines(r)
resolved_lines.update(expanded)
if not resolved_lines:
return False
return resolved_lines.issubset(UNDERWEAR_PRODUCT_LINES)
def _apply_underwear_override(req: LeadSearchInput) -> str | None:
"""
Hard override cho đồ lót: drop gender + age để tìm ALL.
Returns system_message nếu đã override, None nếu không.
"""
inf = req.inferred
if not _is_underwear_request(inf.product_line_vn):
return None
overrides = []
if inf.gender_by_product:
overrides.append(f"gender={inf.gender_by_product}")
inf.gender_by_product = None
if inf.age_by_product:
overrides.append(f"age={inf.age_by_product}")
inf.age_by_product = None
if overrides:
logger.info(
"🩲 UNDERWEAR OVERRIDE: Dropped filters [%s] → search ALL genders/ages",
", ".join(overrides),
)
return (
"[SYSTEM] Đây là yêu cầu tìm ĐỒ LÓT/SỊP. Tool đã tự động tìm TẤT CẢ giới tính "
"(nam + nữ) và TẤT CẢ độ tuổi (người lớn + trẻ em). "
"Hãy gợi ý sản phẩm phù hợp nhất dựa trên ngữ cảnh hội thoại."
)
def _dedup(products: list[dict], limit: int = 10) -> list[dict]:
seen: set[str] = set()
res: list[dict] = []
for p in products:
bc = p.get('internal_ref_code', '')
if bc and bc not in seen:
seen.add(bc)
res.append(p)
return res[:limit]
def _extract_size_table_only(huong_dan_size: str) -> str:
"""Extract only the size table markdown (without guidance text)."""
if not huong_dan_size:
return ""
lines = huong_dan_size.strip().split('\n')
table_lines = []
in_table = False
for line in lines:
if '|' in line:
if 'Size' in line or 'size' in line.lower():
in_table = True
table_lines.append(line)
elif in_table:
if '---' not in line:
table_lines.append(line)
else:
table_lines.append(line)
if in_table and line.strip() == '':
break
elif in_table:
break
return '\n'.join(table_lines[:10]) # Max 10 rows
def _extract_compact_description(desc_data_cut: dict, product_count: int = 1) -> dict:
"""
Extract compact description from pre-cut description_data_cut JSON.
Data is already compressed (only 16 core fields) — no need to filter here.
product_count: Controls detail level
- 1-2 products: Full detail (all fields)
- 3+ products: Ultra compact (name + desc + material + tagline only)
"""
if not desc_data_cut:
return {}
# Field name mapping: Vietnamese key -> English output key
FIELD_MAP = {
"ten_san_pham": "name",
"mo_ta_chinh": "desc",
"chat_lieu": "material",
"tinh_nang_vai": "fabric_features",
"gioi_tinh": "gender",
"phong_cach": "style",
"mua": "season",
"dip_mac": "occasions",
"tags": "tags",
"do_tuoi": "age_range",
"loi_song": "lifestyle",
"tinh_cach": "personality",
"ly_do_mua": "why_buy",
"cross_sell": "cross_sell",
"hook_quang_cao": "hook",
"tagline": "tagline",
}
# Ultra compact mode for multi-product results (3+ products)
COMPACT_FIELDS = ["ten_san_pham", "mo_ta_chinh", "chat_lieu", "tagline", "tags", "phong_cach"]
result = {}
fields_to_use = COMPACT_FIELDS if product_count >= 3 else list(FIELD_MAP.keys())
for vn_key in fields_to_use:
en_key = FIELD_MAP.get(vn_key, vn_key)
val = desc_data_cut.get(vn_key)
if val:
# Truncate long strings in multi-product mode
if product_count >= 3 and isinstance(val, str) and len(val) > 100:
val = val[:100] + "…"
result[en_key] = val
return result
# ======================================================
# SQL Builder
# ======================================================
def _build_fixed_clauses(inf: InferredSearch, params: list) -> list[str]:
"""Build fixed WHERE clauses from InferredSearch (Lane 2 only)."""
clauses = []
if inf.product_line_vn:
lines = []
for line in inf.product_line_vn:
if not line: continue
resolved = resolve_product_line(line)
for r in resolved:
expanded = get_related_lines(r)
lines.extend(expanded)
if lines:
placeholders = ", ".join(["%s"] * len(lines))
params.extend(lines)
clauses.append(f"product_line_vn IN ({placeholders})")
if inf.gender_by_product:
gender_lower = inf.gender_by_product.lower().strip()
genders_to_search = []
if gender_lower in ("women", "nu", "female", "nữ"):
genders_to_search = ["female", "women", "nu", "nữ", "unisex"]
elif gender_lower in ("men", "nam", "male"):
genders_to_search = ["male", "men", "nam", "unisex"]
elif gender_lower in ("boy", "bé trai", "be trai"):
genders_to_search = ["boy", "bé trai", "be trai", "unisex"]
elif gender_lower in ("girl", "bé gái", "be gai"):
genders_to_search = ["girl", "bé gái", "be gai", "unisex"]
else:
genders_to_search = [gender_lower, "unisex"]
placeholders = ", ".join(["%s"] * len(genders_to_search))
params.extend(genders_to_search)
clauses.append(f"gender_by_product IN ({placeholders})")
if inf.age_by_product:
age_lower = inf.age_by_product.lower().strip()
if age_lower in ("baby", "newborn"):
params.append("kid")
else:
params.append(age_lower)
clauses.append("age_by_product = %s")
if inf.master_color:
c_raw = inf.master_color.strip()
COLOR_EN_MAP = {
"do": "red", "trang": "white", "den": "black", "xanh": "blue",
"hong": "pink", "tim": "purple", "cam": "orange", "vang": "yellow",
"nau": "brown", "xam": "gray", "be": "beige", "kem": "cream",
"xanh la": "green", "bac": "silver", "vang dong": "gold",
"đo": "red", "đỏ": "red", "trắng": "white", "đen": "black",
"xanh lá": "green", "hồng": "pink", "tím": "purple", "nâu": "brown",
"xám": "gray", "vàng": "yellow", "bạc": "silver"
}
c_key = c_raw.lower().strip()
en_color = COLOR_EN_MAP.get(c_key)
if not en_color:
import unicodedata
c_no_acc = "".join(
ch for ch in unicodedata.normalize("NFD", c_key)
if unicodedata.category(ch) != "Mn"
).strip()
en_color = COLOR_EN_MAP.get(c_no_acc)
c_no_accent = c_key
color_parts = []
color_parts.append("(master_color LIKE %s OR product_color_name LIKE %s)")
params.append(f"%{c_raw}%")
params.append(f"%{c_raw}%")
if c_no_accent != c_raw.lower():
color_parts.append("(LOWER(master_color) LIKE %s OR LOWER(product_color_name) LIKE %s)")
params.append(f"%{c_no_accent}%")
params.append(f"%{c_no_accent}%")
if en_color:
color_parts.append("(LOWER(master_color) LIKE %s OR LOWER(product_color_name) LIKE %s)")
params.append(f"%{en_color}%")
params.append(f"%{en_color}%")
clauses.append("(" + " OR ".join(color_parts) + ")")
if inf.price_min is not None:
params.append(inf.price_min)
clauses.append("sale_price >= %s")
if inf.price_max is not None:
params.append(inf.price_max)
clauses.append("sale_price <= %s")
if inf.discount_min is not None:
params.append(inf.discount_min)
clauses.append("(original_price > 0 AND ((original_price - sale_price) / original_price * 100) >= %s)")
if inf.discount_max is not None:
params.append(inf.discount_max)
clauses.append("(original_price > 0 AND ((original_price - sale_price) / original_price * 100) <= %s)")
if inf.size:
s = inf.size.strip().upper()
params.append(s)
clauses.append("FIND_IN_SET(%s, REPLACE(size_scale, '|', ',')) > 0")
if inf.discovery_mode:
mode = inf.discovery_mode.lower().strip()
if mode == "new":
clauses.append("is_new_product = 1")
elif mode == "best_seller":
clauses.append("quantity_sold > 0")
return clauses
TAG_TO_BITMAP_COL: dict[str, tuple[str, str]] = {
"thanh lịch": ("style", "Feminine"),
"năng động": ("style", "Dynamic"),
"cơ bản (basic)": ("style", "Basic"),
"cá tính": ("style", "Street"),
"dễ thương": ("style", "Cute"),
"trẻ trung": ("style", "Trend"),
"tối giản": ("style", "Essential"),
"smart casual":("style", "Smart Casual"),
"oversize": ("fitting", "Oversize"),
"slim": ("fitting", "Slim"),
"regular": ("fitting", "Regular"),
"wide leg": ("fitting", "Relax"),
"cropped": ("fitting", "Boxy"),
"relaxed": ("fitting", "Relax"),
"mùa hè": ("season_sale", "Summer"),
"mùa đông": ("season_sale", "Winter"),
"cả 2 mùa": ("season_sale", "Basic"),
"đi biển": ("season_sale", "Summer"),
"du lịch": ("season_sale", "Summer"),
"tập thể thao": ("style", "Athleisure"),
}
TAG_TO_SEARCH_TEXT: dict[str, str] = {
"đi làm / công sở": "đi làm",
"đi chơi / dạo phố": "đi chơi",
"đi tiệc": "đi tiệc",
"đi học": "đi học",
"mặc nhà": "mặc nhà",
"dã ngoại": "dã ngoại",
"đi ngủ": "ngủ",
"giao mùa": "giao mùa",
"thoáng mát": "thoáng mát",
"giữ ấm": "giữ ấm",
"thấm hút": "thấm hút",
"nhanh khô": "nhanh khô",
"chống uv": "chống UV",
"cản gió": "cản gió",
}
def _build_tag_clauses(tags: list[str], params: list) -> str:
if not tags: return ""
bitmap_by_col: dict[str, list[str]] = {}
text_terms: list[str] = []
for tag in tags:
tag_lower = tag.strip().lower()
if tag_lower in TAG_TO_BITMAP_COL:
col, val = TAG_TO_BITMAP_COL[tag_lower]
bitmap_by_col.setdefault(col, []).append(val)
elif tag_lower in TAG_TO_SEARCH_TEXT:
text_terms.append(TAG_TO_SEARCH_TEXT[tag_lower])
else:
text_terms.append(tag_lower.replace(":", " ").replace("_", " "))
all_clauses = []
for col, values in bitmap_by_col.items():
if len(values) == 1:
params.append(values[0])
all_clauses.append(f"{col} = %s")
else:
placeholders = ", ".join(["%s"] * len(values))
params.extend(values)
all_clauses.append(f"{col} IN ({placeholders})")
if text_terms:
term_clauses = []
for term in text_terms:
term = term.strip()
if not term: continue
params.append(f"%{term}%")
params.append(f"%{term}%")
term_clauses.append("(LOWER(description_text) LIKE %s OR product_name LIKE %s)")
if term_clauses:
all_clauses.append(f"({' OR '.join(term_clauses)})")
if all_clauses:
return f"({' OR '.join(all_clauses)})"
return ""
def _build_search_clause(search_terms: list[str], params: list) -> str:
term_clauses = []
for term in search_terms:
t = term.strip()
if not t: continue
words = t.split()
if not words: continue
word_clauses = []
for word in words:
if len(word) >= 4:
params.append(f"%{word}%")
word_clauses.append("product_name LIKE %s")
else:
params.append(f"%{word}%")
params.append(f"%{word}%")
word_clauses.append("(product_name LIKE %s OR LOWER(description_text) LIKE %s)")
if word_clauses:
term_clauses.append(f"({' AND '.join(word_clauses)})")
if term_clauses:
return f"({' OR '.join(term_clauses)})"
return ""
def _build_exclusion_clauses(keywords: list[str], params: list) -> list[str]:
clauses = []
kws_str = " ".join(keywords).lower()
if any(k in kws_str for k in ["đông", "lạnh", "winter", "tuyết", "giữ ấm"]):
clauses.append("product_line_vn NOT IN (%s, %s)")
params.extend(["Quần soóc", "Áo ba lỗ"])
if any(k in kws_str for k in ["đi làm", "công sở", "văn phòng", "office"]):
forbidden = ["cartoon", "hoạt hình", "manga", "anime", "demon slayer", "naruto", "disney", "marvel"]
for f in forbidden:
clauses.append("LOWER(description_text) NOT LIKE %s")
params.append(f"%{f}%")
return clauses
def _build_full_query(fixed_clauses: list[str], search_clause: str | None, exclusion_clauses: list[str] = None) -> str:
all_clauses = list(fixed_clauses)
if search_clause:
all_clauses.append(search_clause)
if exclusion_clauses:
all_clauses.extend(exclusion_clauses)
where = " AND ".join(all_clauses) if all_clauses else "1=1"
return f"""
SELECT {SELECT_COLUMNS}
FROM {TABLE_NAME}
WHERE {where}
ORDER BY quantity_sold DESC NULLS LAST, sale_price ASC
LIMIT 150
"""
async def _price_relaxed_search(inf: InferredSearch, db, multiplier: float) -> list:
if inf.price_max is None:
return []
saved_max = inf.price_max
saved_min = inf.price_min
inf.price_max = int(inf.price_max * multiplier)
inf.price_min = None
params: list = []
fixed = _build_fixed_clauses(inf, params)
sql = _build_full_query(fixed, None)
products = await db.execute_query_async(sql, params=tuple(params))
inf.price_max = saved_max
inf.price_min = saved_min
return products
async def _cascading_search(req: LeadSearchInput, db) -> tuple[list, int, str | None]:
ex_params = []
exclusions = _build_exclusion_clauses(req.keywords, ex_params)
# Tang 1: Fixed + Keywords (NGRAMBF)
if req.keywords:
params = []
fixed = _build_fixed_clauses(req, params)
search = _build_search_clause(req.keywords, params)
if search:
sql = _build_full_query(fixed, search, exclusions)
products = await db.execute_query_async(sql, params=tuple(params + ex_params))
if products:
return products, 1, None
# Tang 2: Fixed + Tags (BITMAP)
if req.tags:
params = []
fixed = _build_fixed_clauses(req, params)
search = _build_tag_clauses(req.tags, params)
if search:
sql = _build_full_query(fixed, search, exclusions)
products = await db.execute_query_async(sql, params=tuple(params + ex_params))
if products:
return products, 2, None
# Tang 3: Chỉ cố định
params = []
fixed = _build_fixed_clauses(req, params)
sql = _build_full_query(fixed, None, exclusions)
products = await db.execute_query_async(sql, params=tuple(params + ex_params))
if products:
return products, 3, None
# Tang 4: Drop gender
if req.product_line_vn and req.gender_by_product:
saved_gender = req.gender_by_product
req.gender_by_product = None
params = []
fixed = _build_fixed_clauses(req, params)
sql = _build_full_query(fixed, None, exclusions)
products = await db.execute_query_async(sql, params=tuple(params + ex_params))
req.gender_by_product = saved_gender
if products:
return _dedup(products), 4, None
# Tang 5-7: Price relaxation
if req.price_max is not None and req.product_line_vn:
original_max = req.price_max
product_label = "/".join(req.product_line_vn)
for multiplier, tier, label in [(1.5, 5, "1.5x"), (2.0, 6, "2x")]:
new_max = int(original_max * multiplier)
products = await _price_relaxed_search(req, db, multiplier)
if products:
cheapest_p = min(products, key=lambda p: float(p.get("sale_price") or 999_999_999))
cheapest = float(cheapest_p.get("sale_price") or 0)
cheapest_name = cheapest_p.get("product_name", "")
diff = int(cheapest - original_max)
msg = (
f"FALLBACK_PRICE: Khong co {product_label} duoi {original_max:,.0f}d. "
f"Mau re nhat: \"{cheapest_name}\" gia {cheapest:,.0f}d "
f"(chi them {diff:,}d so voi budget). "
f"Tong co {len(products)} mau trong tam {new_max:,.0f}d. "
f"-> HAY goi y SP nay cho khach va noi kheo: them chut xiu la co mau rat dep!"
)
return products, tier, msg
# Tang 7: bo price hoan toan
saved_max = req.price_max
saved_min = req.price_min
req.price_max = None
req.price_min = None
params = []
fixed = _build_fixed_clauses(req, params)
sql = _build_full_query(fixed, None, exclusions)
products = await db.execute_query_async(sql, params=tuple(params + ex_params))
req.price_max = saved_max
req.price_min = saved_min
if products:
cheapest_p = min(products, key=lambda p: float(p.get("sale_price") or 999_999_999))
cheapest = float(cheapest_p.get("sale_price") or 0)
cheapest_name = cheapest_p.get("product_name", "")
msg = (
f"FALLBACK_PRICE: Khong co {product_label} duoi {original_max:,.0f}d. "
f"Mau re nhat hien co: \"{cheapest_name}\" gia {cheapest:,.0f}d. "
f"Tong co {len(products)} mau. "
f"-> HAY goi y SP nay va noi: budget them chut la co mau rat dang!"
)
return _dedup(products), 7, msg
return [], 3, None
async def _enrich_with_outfit(
products: list[dict],
db,
tags: list[str] | None = None,
) -> list[dict]:
if not products:
return products
if not _os.path.exists(SQLITE_DB_PATH):
return products
# Phase 1 & 2: Resolve occasion from tags & expand to top 5 products
TAGS_TO_OCCASION = {
"occ:di_lam": "di_lam",
"occ:di_choi": "di_choi",
"occ:di_tiec": "di_tiec",
"occ:mac_nha": "mac_nha",
"occ:the_thao": "the_thao",
"occ:hang_ngay": "hang_ngay",
}
occasion = "hang_ngay"
for tag in (tags or []):
occ = TAGS_TO_OCCASION.get(tag.lower())
if occ:
occasion = occ
break
top_products = products[:5]
anchor_base_codes = [
(p.get("internal_ref_code") or p.get("magento_ref_code", "").split("-")[0]).strip()
for p in top_products
]
anchor_base_codes = [c for c in anchor_base_codes if c]
if not anchor_base_codes:
return products
try:
conn = sqlite3.connect(SQLITE_DB_PATH)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
placeholders = ",".join(["?"] * len(anchor_base_codes))
# Đọc ai_matches trực tiếp từ ultra_descriptions
desc_rows = cursor.execute(
f"""
SELECT base_ref_code, clean_description, description_data_cut, ai_matches
FROM pg__dashboard_canifa__ultra_descriptions
WHERE base_ref_code IN ({placeholders})
""",
anchor_base_codes,
).fetchall()
conn.close()
except Exception as e:
logger.error("❌ SQLite outfit read error: %s", e)
return products
import json
desc_map: dict[str, dict] = {}
all_match_codes = set()
for row in desc_rows:
ai_matches_raw = row["ai_matches"]
parsed_matches = {}
if ai_matches_raw:
try:
parsed_matches = json.loads(ai_matches_raw) if isinstance(ai_matches_raw, str) else ai_matches_raw
except Exception:
pass
# Deduplicate and filter by score for the resolved occasion
filtered_occasion_matches = []
occ_matches = parsed_matches.get(occasion, {})
# Fallback to hang_ngay if occasion yields nothing
if not occ_matches and occasion != "hang_ngay":
occ_matches = parsed_matches.get("hang_ngay", {})
for role, items in occ_matches.items():
best_item = None
best_score = -1
fallback_item = None
fallback_score = -1
for item in items:
score = int(item.get("score") or 0)
if score > fallback_score:
fallback_item = item
fallback_score = score
if score >= 70 and score > best_score:
best_item = item
best_score = score
chosen_item = best_item or fallback_item
if chosen_item:
chosen_item["role"] = role
filtered_occasion_matches.append(chosen_item)
if chosen_item.get("code"):
all_match_codes.add(chosen_item["code"])
desc_map[row["base_ref_code"]] = {
"clean_description": row["clean_description"],
"description_data_cut": row["description_data_cut"],
"filtered_matches": filtered_occasion_matches
}
# Fetch missing metadata (price, discount, url, etc) from StarRocks
star_metadata = {}
if all_match_codes and db:
try:
match_placeholders = ",".join([f"'{c}'" for c in all_match_codes])
sql = f"""
SELECT magento_ref_code, sale_price, original_price, discount_percent,
is_new_product, quantity_sold, product_web_url
FROM {TABLE_NAME}
WHERE magento_ref_code IN ({match_placeholders})
"""
star_rows = await db.execute_query_async(sql)
if star_rows:
for r in star_rows:
star_metadata[r["magento_ref_code"]] = r
except Exception as e:
logger.error("❌ StarRocks match metadata fetch error: %s", e)
# Check stock for match items
match_base_codes = list(set(code.split("-")[0] for code in all_match_codes))
stock_map, _, _ = await _fetch_stock_batch(match_base_codes)
for p in top_products:
anchor_magento = p.get("magento_ref_code", "")
base = (p.get("internal_ref_code") or anchor_magento.split("-")[0]).strip()
# Gán description + description_data_cut
desc_info = desc_map.get(base)
if desc_info:
if desc_info.get("clean_description"):
p["ai_description"] = desc_info["clean_description"]
if desc_info.get("description_data_cut"):
p["description_data_cut"] = desc_info["description_data_cut"]
matches = desc_info.get("filtered_matches", [])
enriched_matches = []
for m in matches:
m_code = m.get("code")
m_base = m_code.split("-")[0] if m_code else ""
# Check stock
colors_stock = stock_map.get(m_base, [])
in_stock = len(colors_stock) > 0
# We skip injecting if out of stock
if not in_stock:
continue
meta = star_metadata.get(m_code, {})
disc_pct = int(meta.get("discount_percent") or 0)
enriched_m = {
"sku": m_code,
"name": m.get("name", ""),
"color": m.get("color", ""),
"image": m.get("image", ""),
"price": int(meta.get("sale_price") or 0),
"original_price": int(meta.get("original_price") or 0),
"discount": f"-{disc_pct}%" if disc_pct > 0 else None,
"is_new": bool(meta.get("is_new_product")),
"popularity": int(meta.get("quantity_sold") or 0),
"url": meta.get("product_web_url", ""),
"role": m.get("role", "other"),
"ai_reason": m.get("reason", ""),
"score": m.get("score", 0),
"occasion": occasion,
"in_stock": in_stock
}
enriched_matches.append(enriched_m)
if enriched_matches:
p["ai_matches"] = enriched_matches
p["outfit_recommendations"] = enriched_matches
return products
async def _format_products(products: list, db) -> list[dict]:
formatted = []
for p in products[:5]:
sale = float(p.get("sale_price") or 0)
orig = float(p.get("original_price") or 0)
has_discount = sale < orig and orig > 0
disc_pct = int(p.get("discount_percent") or 0)
main_sku = p.get("magento_ref_code", "")
# Parse description_data_cut (pre-compressed from SQLite)
desc_data_cut = None
raw_cut = p.get("description_data_cut")
if raw_cut:
try:
desc_data_cut = json.loads(raw_cut) if isinstance(raw_cut, str) else raw_cut
except Exception:
desc_data_cut = None
# Build compact item
item = {
"sku": main_sku,
"name": p.get("product_name", ""),
"price": int(sale),
"original_price": int(orig),
"discount": f"-{disc_pct}%" if has_discount else None,
"color": p.get("master_color", ""),
"gender": p.get("gender_by_product", ""),
"product_line": p.get("product_line_vn", ""),
"image": p.get("product_image_url_thumbnail", ""),
"url": p.get("product_web_url", ""),
"sizes": p.get("size_scale", ""),
}
# --- Inject Size Message ---
raw_size = p.get("size_scale", "")
parsed_sizes = []
if isinstance(raw_size, str) and raw_size.strip():
if "[" in raw_size:
try:
parsed_sizes = json.loads(raw_size)
except Exception:
parsed_sizes = [s.strip() for s in raw_size.replace("[", "").replace("]", "").replace('"', '').replace("'", '').split(",") if s.strip()]
else:
parsed_sizes = [s.strip() for s in raw_size.replace(",", "|").split("|") if s.strip()]
elif isinstance(raw_size, list):
parsed_sizes = [str(s) for s in raw_size]
from .size_message_builder import build_size_message
desc_text = p.get("description_text", "")
size_msg = build_size_message(
gender=p.get("gender_by_product", ""),
product_line=p.get("product_line_vn", ""),
sizes=parsed_sizes,
description=desc_text
)
item["sizes_available"] = parsed_sizes
if size_msg:
item["size_message"] = size_msg
# ---------------------------
# Add compact description from description_data_cut
product_count = len(products[:5])
if desc_data_cut:
compact_desc = _extract_compact_description(
desc_data_cut,
product_count=product_count,
)
# Merge compact description fields into item
item.update(compact_desc)
else:
# Fallback to short description_text
desc = (p.get("description_text") or "").strip()
item["description"] = (desc[:200] + "...") if len(desc) > 200 else desc
# Inject outfit recommendations (ai_matches)
if p.get("ai_matches"):
item["outfit_recommendations"] = p["ai_matches"]
# Inject similar_items (from OBT column)
raw_similar = p.get("similar_items")
if raw_similar:
try:
if isinstance(raw_similar, str):
try:
parsed = json.loads(raw_similar)
except Exception:
import ast
parsed = ast.literal_eval(raw_similar)
else:
parsed = raw_similar
if isinstance(parsed, list):
extracted = []
for entry in parsed:
if isinstance(entry, str) and entry.startswith('{'):
try:
obj = json.loads(entry)
extracted.append(obj)
except:
try:
import ast
obj = ast.literal_eval(entry)
extracted.append(obj)
except:
extracted.append(entry)
elif isinstance(entry, dict):
extracted.append(entry)
else:
extracted.append(entry)
item["similar_items"] = extracted
else:
item["similar_items"] = parsed
except Exception:
item["similar_items"] = raw_similar
if "in_stock" in p:
item["in_stock"] = p["in_stock"]
item["total_qty"] = p["total_qty"]
item["stock"] = p["stock"]
formatted.append(item)
return formatted
def _build_sku_query(code: str) -> tuple[str, list]:
code = code.strip().upper()
sql = f"""
SELECT {SELECT_COLUMNS}
FROM {TABLE_NAME}
WHERE (UPPER(magento_ref_code) = %s
OR UPPER(internal_ref_code) = %s
OR UPPER(magento_ref_code) LIKE %s)
ORDER BY quantity_sold DESC NULLS LAST
LIMIT 20
"""
return sql, [code, code, f"{code}%"]
class ProductSearchEngine:
"""
Standalone Product Search Service.
Dual-Lane Architecture:
Lane 1 (LITERAL): raw_text LIKE trực tiếp — KHÔNG filter, giữ nguyên ý user
Lane 2 (INFERRED): AI suy luận → fixed filters + tags/keywords
→ Merge + Dedup theo SKU → Ưu tiên LITERAL trước
→ Nếu cả 2 rỗng → fallback cascade (tier 3-7)
"""
# ── Dual-Lane: chạy LITERAL + INFERRED song song, merge kết quả ──
async def _dual_query_search(
self, req: LeadSearchInput, db
) -> tuple[list, int, str | None]:
"""
Chạy 2 lane song song:
Lane 1 (LITERAL): raw_text → LIKE thuần trên product_name + description (KHÔNG filter)
Lane 2 (INFERRED): fixed clauses + tags/keywords → structured filters
Merge + dedup, ưu tiên LITERAL trước.
Nếu cả 2 rỗng → delegate cho _fallback_cascade (tier 3+).
"""
import asyncio
inf = req.inferred
lit = req.literal
# Event fallback: set master_color/tags/keywords if missing (30/4, 2/9, ...)
fallback_text = " ".join(
[
lit.raw_text or "",
" ".join(inf.keywords or []),
]
).strip()
_apply_event_fallback(inf, fallback_text)
# ── SAFETY VALVE: Force 'adult' for office/work intent if not inferred ──
office_tags = {"Đi làm / Công sở", "Thanh lịch", "Chỉnh chu", "Lịch sự"}
if not inf.age_by_product and any(tag in office_tags for tag in (inf.tags or [])):
inf.age_by_product = "adult"
# ── Build Lane 1: LITERAL — chỉ LIKE raw text, KHÔNG có fixed clauses ──
literal_coro = None
if lit.raw_text and lit.raw_text.strip():
from agent.lead_stage_agent.product_mapping import resolve_product_name
raw_text = resolve_product_name(lit.raw_text.strip())
# Lọc bỏ stop words phổ biến khi khách chat để tránh tịt Literal Search
stop_words = {
"tìm", "mua", "muốn", "mình", "có", "không", "ko", "nào", "cần", "cho", "xin",
"nhé", "nha", "với", "hỏi", "đồ", "cái", "chiếc", "thế", "ấy", "này", "kia",
"đó", "thử", "nhỉ", "vậy", "mà", "chứ", "ạ", "ơi", "đâu", "đây", "rồi",
"nữa", "sao", "đi", "được", "ra", "để", "thì", "tầm", "khoảng", "loại",
"combo", "bộ", "set"
}
# Ép lower-case toàn bộ words
words = [w.lower() for w in raw_text.split() if w.lower() not in stop_words]
if words:
params_lit = []
word_clauses = []
for word in words:
params_lit.extend([f"%{word}%", f"%{word}%", f"%{word}%"])
word_clauses.append(
"(LOWER(product_name) LIKE %s OR LOWER(description_text) LIKE %s OR LOWER(description_data_cut) LIKE %s)"
)
literal_where = " AND ".join(word_clauses)
# ── GLOBAL FILTERS (TẦNG 1): Áp dụng cứng Gender và Age vào Literal Search ──
if inf.gender_by_product:
literal_where = f"({literal_where}) AND gender_by_product = %s"
params_lit.append(inf.gender_by_product)
if inf.age_by_product:
literal_where = f"({literal_where}) AND age_by_product = %s"
params_lit.append(inf.age_by_product)
sql_lit = f"""
SELECT {SELECT_COLUMNS}
FROM {TABLE_NAME}
WHERE {literal_where}
ORDER BY quantity_sold DESC NULLS LAST, sale_price ASC
LIMIT 150
"""
literal_coro = db.execute_query_async(sql_lit, params=tuple(params_lit))
# ── Build Lane 2: INFERRED — full structured filters ──
inferred_coro = None
has_inferred = (
inf.product_line_vn or inf.gender_by_product or inf.master_color
or inf.tags or inf.keywords or inf.price_min is not None
or inf.price_max is not None or inf.size or inf.discovery_mode
)
if has_inferred:
params_inf = []
fixed = _build_fixed_clauses(inf, params_inf)
# Lọc bỏ từ khóa gây nhiễu (blacklist) khỏi Inferred Search (vì Literal Search đã xử lý rồi)
blacklist = {"combo", "set", "bộ"}
filtered_tags = [t for t in (inf.tags or []) if t.lower() not in blacklist]
filtered_keywords = [k for k in (inf.keywords or []) if k.lower() not in blacklist]
# Thêm tags + keywords vào search clause
search_parts = []
if filtered_tags:
tag_clause = _build_tag_clauses(filtered_tags, params_inf)
if tag_clause:
search_parts.append(tag_clause)
if filtered_keywords:
kw_clause = _build_search_clause(filtered_keywords, params_inf)
if kw_clause:
search_parts.append(kw_clause)
search_combined = f"({' OR '.join(search_parts)})" if search_parts else None
ex_params = []
exclusions = _build_exclusion_clauses(filtered_keywords, ex_params)
sql_inf = _build_full_query(fixed, search_combined, exclusions)
inferred_coro = db.execute_query_async(
sql_inf, params=tuple(params_inf + ex_params)
)
# ── Chạy song song ──
literal_products = []
inferred_products = []
if literal_coro and inferred_coro:
literal_products, inferred_products = await asyncio.gather(
literal_coro, inferred_coro
)
elif literal_coro:
literal_products = await literal_coro
elif inferred_coro:
inferred_products = await inferred_coro
logger.info(
"[DUAL LANE] LITERAL=%d results | INFERRED=%d results",
len(literal_products), len(inferred_products),
)
# ── Merge + Dedup (ưu tiên LITERAL trước) ──
if literal_products or inferred_products:
seen_base_codes: set[str] = set()
merged: list[dict] = []
# INFERRED match = ưu tiên cao hơn → thêm trước (chuẩn giới tính & phân loại)
for p in inferred_products:
base_code = p.get("internal_ref_code", "")
if base_code and base_code not in seen_base_codes:
p["_search_source"] = "inferred"
seen_base_codes.add(base_code)
merged.append(p)
# LITERAL match = bổ sung thêm (dành cho bộ sưu tập, tên riêng)
for p in literal_products:
base_code = p.get("internal_ref_code", "")
if base_code and base_code not in seen_base_codes:
p["_search_source"] = "literal"
seen_base_codes.add(base_code)
merged.append(p)
if merged:
if literal_products and inferred_products:
tier = 1 # dual hit
elif literal_products:
tier = 1 # literal only
else:
tier = 2 # inferred only
return merged[:10], tier, None
# ── Cả 2 rỗng → fallback cascade tier 3+ ──
return await self._fallback_cascade(req, db)
async def _fallback_cascade(
self, req: LeadSearchInput, db
) -> tuple[list, int, str | None]:
"""Cascade fallback khi dual-lane rỗng (tier 3-7). Dùng inferred filters."""
inf = req.inferred
ex_params = []
exclusions = _build_exclusion_clauses(inf.keywords, ex_params)
# Tier 3: Semantic Vector Search (Fuzzy Match / BM25 Alternative)
lit = req.literal
if lit and lit.raw_text and lit.raw_text.strip():
from common.embedding_service import create_embedding_async
try:
query_vector = await create_embedding_async(lit.raw_text.strip())
if query_vector:
v_str = "[" + ",".join(str(v) for v in query_vector) + "]"
params_vec = []
fixed_vec = _build_fixed_clauses(inf, params_vec)
where_str = " AND ".join(fixed_vec) if fixed_vec else "1=1"
if exclusions:
where_str += f" AND {' AND '.join(exclusions)}"
sql_vec = f"""
WITH vector_matches AS (
SELECT /*+ SET_VAR(ann_params='{{"ef_search":256}}') */
{SELECT_COLUMNS},
approx_cosine_similarity(vector, {v_str}) as similarity_score
FROM {TABLE_NAME}
ORDER BY similarity_score DESC
LIMIT 200
)
SELECT * FROM vector_matches
WHERE {where_str}
ORDER BY similarity_score DESC, quantity_sold DESC
LIMIT 150
"""
vec_products = await db.execute_query_async(sql_vec, params=tuple(params_vec + ex_params))
if vec_products:
seen_base_codes: set[str] = set()
good_vecs = []
for p in vec_products:
if p.get("similarity_score", 0) > 0.4:
base_code = p.get("internal_ref_code", "")
if base_code and base_code not in seen_base_codes:
seen_base_codes.add(base_code)
p["_search_source"] = "semantic_fuzzy"
good_vecs.append(p)
if good_vecs:
return good_vecs[:10], 3, "FALLBACK_SEMANTIC"
except Exception as e:
logger.error(f"Semantic Search Fallback Error: {e}")
# Tier 4: Chỉ fixed clauses
params = []
fixed = _build_fixed_clauses(inf, params)
sql = _build_full_query(fixed, None, exclusions)
products = await db.execute_query_async(sql, params=tuple(params + ex_params))
if products:
seen_base_codes: set[str] = set()
dedup = []
for p in products:
base_code = p.get("internal_ref_code", "")
if base_code and base_code not in seen_base_codes:
seen_base_codes.add(base_code)
dedup.append(p)
return dedup[:10], 4, None
# Tier 4: Drop gender
if inf.product_line_vn and inf.gender_by_product:
saved = inf.gender_by_product
inf.gender_by_product = None
params = []
fixed = _build_fixed_clauses(inf, params)
sql = _build_full_query(fixed, None, exclusions)
products = await db.execute_query_async(sql, params=tuple(params + ex_params))
inf.gender_by_product = saved
if products:
return _dedup(products), 4, None
# Tier 5-7: Price relaxation
if inf.price_max is not None and inf.product_line_vn:
original_max = inf.price_max
product_label = "/".join(inf.product_line_vn)
for multiplier, tier_num in [(1.5, 5), (2.0, 6)]:
new_max = int(original_max * multiplier)
products = await _price_relaxed_search(inf, db, multiplier)
if products:
cheapest_p = min(products, key=lambda p: float(p.get("sale_price") or 999_999_999))
cheapest = float(cheapest_p.get("sale_price") or 0)
cheapest_name = cheapest_p.get("product_name", "")
diff = int(cheapest - original_max)
msg = (
f"FALLBACK_PRICE: Khong co {product_label} duoi {original_max:,.0f}d. "
f'Mau re nhat: "{cheapest_name}" gia {cheapest:,.0f}d '
f"(chi them {diff:,}d so voi budget). "
f"Tong co {len(products)} mau trong tam {new_max:,.0f}d. "
f"-> HAY goi y SP nay cho khach va noi kheo: them chut xiu la co mau rat dep!"
)
return _dedup(products), tier_num, msg
# Tier 7: drop price
saved_max, saved_min = inf.price_max, inf.price_min
inf.price_max = inf.price_min = None
params = []
fixed = _build_fixed_clauses(inf, params)
sql = _build_full_query(fixed, None, exclusions)
products = await db.execute_query_async(sql, params=tuple(params + ex_params))
inf.price_max, inf.price_min = saved_max, saved_min
if products:
cheapest_p = min(products, key=lambda p: float(p.get("sale_price") or 999_999_999))
cheapest = float(cheapest_p.get("sale_price") or 0)
cheapest_name = cheapest_p.get("product_name", "")
msg = (
f"FALLBACK_PRICE: Khong co {product_label} duoi {original_max:,.0f}d. "
f'Mau re nhat hien co: "{cheapest_name}" gia {cheapest:,.0f}d. '
f"Tong co {len(products)} mau. "
f"-> HAY goi y SP nay va noi: budget them chut la co mau rat dang!"
)
return _dedup(products), 7, msg
return [], 3, None
async def search(self, req: LeadSearchInput, reasoning: str | None = None,
user_insight: dict = None) -> dict:
"""
Entry point: Dual-Lane search + enrichment pipeline.
Args:
user_insight: User insight dict (chứa LATEST_PRODUCT_INTEREST, TARGET, ...)
"""
start = time.time()
db = get_db_connection()
try:
fallback_msg = None
# ★★★ PRE-SEARCH: HARD PATTERN DETECTION & ADAPTATION ★★★
# Detect nếu query có pattern "rẻ hơn", "tương tự", ...
context_products = []
if user_insight:
# Lấy LATEST_PRODUCT_INTEREST và lookup thành product dict
latest = user_insight.get("LATEST_PRODUCT_INTEREST")
if latest:
# Parse SKU từ format "Áo Polo Nam Basic (8TP25A002)" hoặc "8TP25A002"
sku = self._parse_sku_from_insight(latest)
if sku:
# Lookup product từ DB để có giá
product = await self._lookup_product_by_sku(sku, db)
if product:
context_products = [product]
pattern = _hard_pattern_detector.detect(req.literal.raw_text, context_products)
if pattern.pattern_type == "cheaper_than" and pattern.target_price_max:
# Override price_max từ pattern
orig_price_max = req.inferred.price_max
req.inferred.price_max = int(pattern.target_price_max)
logger.info(
f"[HARD PATTERN] 'Rẻ hơn' detected: ref_price={pattern.reference_price:,.0f}đ "
f"→ set price_max={req.inferred.price_max:,.0f}đ (was {orig_price_max})"
)
elif pattern.pattern_type == "more_expensive_than" and pattern.target_price_min:
orig_price_min = req.inferred.price_min
req.inferred.price_min = int(pattern.target_price_min)
logger.info(
f"[HARD PATTERN] 'Đắt hơn' detected: ref_price={pattern.reference_price:,.0f}đ "
f"→ set price_min={req.inferred.price_min:,.0f}đ (was {orig_price_min})"
)
# Các pattern khác có thể xử lý sau...
# ★ UNDERWEAR HARD OVERRIDE — drop gender/age trước khi search ★
underwear_msg = _apply_underwear_override(req)
if req.magento_ref_code:
sql, params = _build_sku_query(req.magento_ref_code)
products = await db.execute_query_async(sql, params=tuple(params))
tier = 0
else:
# ★ DUAL-LANE: literal + inferred song song ★
products, tier, fallback_msg = await self._dual_query_search(req, db)
# Enrichment pipeline (giữ nguyên)
# products, stock_checked, stock_api_ms, timed_out = await _enrich_with_stock(products)
stock_api_ms = 0.0
products = await _enrich_with_outfit(products, db, tags=req.inferred.tags)
formatted = await _format_products(products, db)
elapsed_ms = round((time.time() - start) * 1000, 2)
_search_modes = [
"sku_lookup",
"literal_like",
"inferred_structured",
"hard_filters_only",
"drop_gender",
"price_1.5x",
"price_2x",
"price_unlimited",
]
inf = req.inferred
logger.info(
"[LEAD SEARCH] REQUEST | literal='%s' | tags=%s | kw=%s | line=%s | price=%s~%s",
req.literal.raw_text, inf.tags, inf.keywords, inf.product_line_vn, inf.price_min, inf.price_max,
)
logger.info(
"[LEAD SEARCH] RESULT | tier=%d (%s) | results=%d | time=%.0fms | stock=%.0fms",
tier, _search_modes[min(tier, 7)], len(products), elapsed_ms, stock_api_ms,
)
result = {
"status": "success",
"count": len(products),
"tier": tier,
"search_mode": _search_modes[min(tier, 7)],
"elapsed_ms": elapsed_ms,
"reasoning": reasoning or "",
"literal_text": " ".join([w for w in (req.literal.raw_text or "").split() if w.lower() not in {"combo", "bộ", "set"}]),
"keywords_used": inf.keywords or [],
"tags_used": inf.tags or [],
"products": formatted,
}
if fallback_msg:
result["fallback_message"] = fallback_msg
if underwear_msg:
result["system_message"] = underwear_msg
has_sizes = any("size_message" in p for p in formatted)
if has_sizes:
result["size_tools_message"] = (
"CHÚ Ý QUAN TRỌNG TỪ HỆ THỐNG: Mỗi sản phẩm trả về đã đính kèm `size_message` "
"chứa các size CÒN HÀNG và Bảng đo tương ứng. BẠN PHẢI ƯU TIÊN lấy đúng "
"`size_message` của sản phẩm đó để tư vấn cho khách dựa trên chiều cao cân nặng "
"của họ, và chú ý áp dụng các Mẹo nâng/hạ size (nếu có). KHÔNG tự bịa size."
)
has_outfits = any("outfit_recommendations" in p for p in formatted)
if has_outfits:
result["outfit_tools_message"] = (
"GỢI Ý PHỐI ĐỒ: Một số sản phẩm có kèm `outfit_recommendations` — danh sách "
"các items phối cùng (vai trò: top/bottom/layer/accessory) kèm lý do. "
"Hãy gợi ý khách mua combo nếu phù hợp, ưu tiên các sản phẩm match_role "
"bổ trợ cho item chính. Luôn nêu mã SKU khi gợi ý."
)
has_desc = any("desc" in p or "why_buy" in p for p in formatted)
if has_desc:
result["description_tools_message"] = (
"MÔ TẢ SẢN PHẨM: Mỗi sản phẩm có kèm thông tin mô tả ngắn gọn "
"(desc, material, why_buy, hook, tagline...). Hãy dùng các thông tin "
"này để tư vấn khách hàng một cách tự nhiên, KHÔNG copy nguyên văn, "
"mà hãy diễn đạt lại phù hợp với ngữ cảnh hội thoại."
)
return result
except Exception as e:
logger.error("Lead search tool error: %s", e, exc_info=True)
return {"status": "error", "message": str(e)}
# ═══════════════════════════════════════════════════════════════
# HELPER: Hard Pattern Support
# ═══════════════════════════════════════════════════════════════
def _parse_sku_from_insight(self, insight_value: str) -> str | None:
"""Parse SKU từ LATEST_PRODUCT_INTEREST."""
if not insight_value:
return None
# Format: "Áo Polo Nam Basic (8TP25A002)" hoặc "8TP25A002"
match = re.search(r"\(?([A-Z0-9]{4,}-[A-Z0-9]{3,})\)?", str(insight_value))
return match.group(1) if match else None
async def _lookup_product_by_sku(self, sku: str, db) -> dict | None:
"""Lookup product từ DB bằng SKU."""
sql = f"""
SELECT {SELECT_COLUMNS}
FROM {TABLE_NAME}
WHERE magento_ref_code = %s OR internal_ref_code = %s
LIMIT 1
"""
result = await db.execute_query_async(sql, params=(sku, sku))
return result[0] if result else None
...@@ -310,12 +310,32 @@ class SearchEngine: ...@@ -310,12 +310,32 @@ class SearchEngine:
params_lit.extend([f"%{w}%", f"%{w}%"]) params_lit.extend([f"%{w}%", f"%{w}%"])
clauses_lit.append("(LOWER(product_name) LIKE %s OR LOWER(description_text) LIKE %s)") clauses_lit.append("(LOWER(product_name) LIKE %s OR LOWER(description_text) LIKE %s)")
# Áp dụng cứng Gender/Age # Áp dụng cứng Gender/Age cho Literal (sử dụng mapping giống Inferred)
where_lit = " AND ".join(clauses_lit) where_lit = " AND ".join(clauses_lit)
if inf.gender_by_product: if inf.gender_by_product:
params_lit.append(inf.gender_by_product); where_lit += " AND gender_by_product = %s" g = inf.gender_by_product.lower()
if g in ["women", "nu", "female", "nữ", "woman"]:
genders = ["women", "unisex"]
elif g in ["men", "nam", "male", "man"]:
genders = ["men", "unisex"]
elif g in ["boy", "bé trai", "be_trai"]:
genders = ["boy", "unisex"]
elif g in ["girl", "bé gái", "be_gai"]:
genders = ["girl", "unisex"]
else:
genders = [g, "unisex"]
params_lit.extend(genders)
where_lit += f" AND gender_by_product IN ({', '.join(['%s']*len(genders))})"
if inf.age_by_product: if inf.age_by_product:
params_lit.append(inf.age_by_product); where_lit += " AND age_by_product = %s" age = inf.age_by_product.lower()
if age in ["children", "child", "kid", "kids", "trẻ em", "tre_em", "bé"]:
age = "kid"
elif age in ["adult", "adults", "người lớn"]:
age = "adult"
params_lit.append(age)
where_lit += " AND age_by_product = %s"
sql_lit = f"SELECT {SELECT_COLUMNS} FROM {TABLE_NAME} WHERE {where_lit} ORDER BY quantity_sold DESC LIMIT 50" sql_lit = f"SELECT {SELECT_COLUMNS} FROM {TABLE_NAME} WHERE {where_lit} ORDER BY quantity_sold DESC LIMIT 50"
literal_products = await self.db.execute_query(sql_lit, tuple(params_lit)) literal_products = await self.db.execute_query(sql_lit, tuple(params_lit))
...@@ -337,7 +357,7 @@ class SearchEngine: ...@@ -337,7 +357,7 @@ class SearchEngine:
if code and code not in seen: if code and code not in seen:
seen.add(code); merged.append(p) seen.add(code); merged.append(p)
if merged: return merged[:10], 1, "Success (Dual-Lane)" if merged: return merged[:5], 1, "Success (Dual-Lane)"
return await self._fallback_cascade(inf) return await self._fallback_cascade(inf)
async def _fallback_cascade(self, inf: InferredSearch) -> Tuple[List[Dict], int, str]: async def _fallback_cascade(self, inf: InferredSearch) -> Tuple[List[Dict], int, str]:
...@@ -351,7 +371,7 @@ class SearchEngine: ...@@ -351,7 +371,7 @@ class SearchEngine:
fixed = _build_fixed_clauses(inf, params) fixed = _build_fixed_clauses(inf, params)
products = await self.db.execute_query(_build_full_query(fixed, None, exclusions), tuple(params + ex_params)) products = await self.db.execute_query(_build_full_query(fixed, None, exclusions), tuple(params + ex_params))
inf.gender_by_product = saved inf.gender_by_product = saved
if products: return products[:10], 4, "Fallback (Dropped Gender)" if products: return products[:5], 4, "Fallback (Dropped Gender)"
# Tier 5-6: Price Relaxation # Tier 5-6: Price Relaxation
if inf.price_max: if inf.price_max:
...@@ -363,7 +383,7 @@ class SearchEngine: ...@@ -363,7 +383,7 @@ class SearchEngine:
products = await self.db.execute_query(_build_full_query(fixed, None, exclusions), tuple(params + ex_params)) products = await self.db.execute_query(_build_full_query(fixed, None, exclusions), tuple(params + ex_params))
if products: if products:
inf.price_max = orig_max inf.price_max = orig_max
return products[:10], tier, f"Fallback (Price relaxed {mult}x)" return products[:5], tier, f"Fallback (Price relaxed {mult}x)"
inf.price_max = orig_max inf.price_max = orig_max
# Tier 7: Drop Price entirely # Tier 7: Drop Price entirely
...@@ -373,7 +393,7 @@ class SearchEngine: ...@@ -373,7 +393,7 @@ class SearchEngine:
fixed = _build_fixed_clauses(inf, params) fixed = _build_fixed_clauses(inf, params)
products = await self.db.execute_query(_build_full_query(fixed, None, exclusions), tuple(params + ex_params)) products = await self.db.execute_query(_build_full_query(fixed, None, exclusions), tuple(params + ex_params))
inf.price_max, inf.price_min = saved_max, saved_min inf.price_max, inf.price_min = saved_max, saved_min
if products: return products[:10], 7, "Fallback (Unlimited Price)" if products: return products[:5], 7, "Fallback (Unlimited Price)"
return [], 0, "No results found" return [], 0, "No results found"
...@@ -384,7 +404,8 @@ class SearchEngine: ...@@ -384,7 +404,8 @@ class SearchEngine:
raw = p.get("similar_items") raw = p.get("similar_items")
if raw and isinstance(raw, str): if raw and isinstance(raw, str):
try: try:
p["similar_items"] = json.loads(raw) parsed = json.loads(raw)
p["similar_items"] = parsed[:5] if isinstance(parsed, list) else []
except (json.JSONDecodeError, TypeError): except (json.JSONDecodeError, TypeError):
p["similar_items"] = [] p["similar_items"] = []
elif not raw: elif not raw:
...@@ -494,7 +515,7 @@ class SearchEngine: ...@@ -494,7 +515,7 @@ class SearchEngine:
products = self._parse_description_data_full(products) products = self._parse_description_data_full(products)
for p in products: for p in products:
raw_size = p.get("size_scale", "") raw_size = p.get("size_scale", "")
parsed = [s.strip() for s in str(raw_size).replace("[", "").replace("]", "").replace('"', '').split(",") if s.strip()] parsed = [s.strip() for s in str(raw_size).replace("[", "").replace("]", "").replace('"', '').replace('|', ',').split(",") if s.strip()]
p["size_message"] = build_size_message(p.get("gender_by_product", ""), p.get("product_line_vn", ""), parsed, p.get("description_text", "")) p["size_message"] = build_size_message(p.get("gender_by_product", ""), p.get("product_line_vn", ""), parsed, p.get("description_text", ""))
return { return {
......
This source diff could not be displayed because it is too large. You can view the blob instead.
import sqlite3
DB_PATH = 'D:/cnf/chatbot_canifa/preference/common/database/canifa_ai_dump.sqlite'
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
tables = [row[0] for row in cursor.fetchall()]
print('Tables:', tables)
cursor.execute("PRAGMA table_info(sr__test_db__magento_product_dimension_with_text_embedding);")
columns = [row[1] for row in cursor.fetchall()]
print('Columns:', columns)
...@@ -31,12 +31,12 @@ def check_db(db_path, label): ...@@ -31,12 +31,12 @@ def check_db(db_path, label):
# Check outfit-specific tables # Check outfit-specific tables
outfit_tables = [t for t in tables if "outfit" in t.lower()] outfit_tables = [t for t in tables if "outfit" in t.lower()]
if outfit_tables: if outfit_tables:
print(f"\n🎨 Outfit Tables Detail:") print("\n🎨 Outfit Tables Detail:")
for t in outfit_tables: for t in outfit_tables:
print(f"\n >>> {t}") print(f"\n >>> {t}")
cur.execute(f'PRAGMA table_info("{t}")') cur.execute(f'PRAGMA table_info("{t}")')
cols = cur.fetchall() cols = cur.fetchall()
print(f" Columns:") print(" Columns:")
for c in cols: for c in cols:
print(f" {c[1]:20s} {c[2]:15s} {'PK' if c[5] else ''}") print(f" {c[1]:20s} {c[2]:15s} {'PK' if c[5] else ''}")
...@@ -49,7 +49,7 @@ def check_db(db_path, label): ...@@ -49,7 +49,7 @@ def check_db(db_path, label):
for row in sample: for row in sample:
print(f" {dict(zip(col_names, row))}") print(f" {dict(zip(col_names, row))}")
else: else:
print(f" (empty table)") print(" (empty table)")
conn.close() conn.close()
......
import sqlite3
import json
db_path = 'canifa_ai_dump.sqlite'
try:
conn = sqlite3.connect(db_path)
c = conn.cursor()
# Find rows where description_data_full is short or only contains mau_sac
c.execute("SELECT magento_ref_code, description_data_full FROM sr__test_db__magento_product_dimension_with_text_embedding WHERE description_data_full LIKE '%mau_sac%' AND length(description_data_full) < 100 LIMIT 10")
rows_mau_sac = c.fetchall()
print("--- Rows with short JSON / only mau_sac ---")
for r in rows_mau_sac:
print(r)
c.execute("SELECT magento_ref_code, description_data_full FROM sr__test_db__magento_product_dimension_with_text_embedding WHERE description_data_full NOT LIKE '{%' LIMIT 5")
rows_invalid = c.fetchall()
print("\n--- Rows not starting with { ---")
for r in rows_invalid:
print(r)
except Exception as e:
print(f"Error in {db_path}: {e}")
""" r"""
migrate_003_full_coverage.py migrate_003_full_coverage.py
──────────────────────────────────────────────────────────── ────────────────────────────────────────────────────────────
Seed toàn bộ anchor categories còn thiếu rules: Seed toàn bộ anchor categories còn thiếu rules:
......
from worker.stylist_engine import StylistEngine
def test_engine():
engine = StylistEngine()
print("Catalog loaded. Finding adult product...")
catalog = engine._get_catalog()
# find an adult shirt
adult_product = None
for p in catalog:
gender = p.get('gender', '').lower()
if gender in ['nam', 'nữ', 'nu', 'women', 'men']:
adult_product = p
break
if not adult_product:
print("No adult product found.")
return
code = adult_product['code']
print(f"Testing Code: {code} | Name: {adult_product['name']} | Gender: {adult_product['gender']}")
print("\n--- 1. Testing 'compute_dynamic_rule_matches' (AI MATCHES) ---")
matches = engine.compute_dynamic_rule_matches(code)
if not matches:
print("No AI matches found.")
else:
for occ, roles in matches.items():
for role, items in roles.items():
for item in items[:2]:
item_gender = item.get('gender', 'unknown').lower()
item_name = item.get('name', 'unknown')
if 'bé' in item_name.lower() or 'bé' in item_gender or 'be' in item_gender:
print(f" [AI-MATCH] FOUND KID: {item['code']} - {item_name} (gender: {item_gender})")
else:
print(f" [AI-MATCH] OK: {item['code']} - {item_name} (gender: {item_gender})")
print("\n--- 2. Testing 'compute_super_classifications_sql' (SUPER CLASSIFICATIONS) ---")
classifications = engine.compute_super_classifications_sql(code)
if not classifications:
print("No classification matches found.")
else:
for group, groups_dict in classifications.items():
for key, items in groups_dict.items():
for item in items[:2]:
item_name = item.get('name', 'unknown')
if 'bé' in item_name.lower():
print(f" [SUPER-CLASS] FOUND KID: {item['code']} - {item_name}")
else:
print(f" [SUPER-CLASS] OK: {item['code']} - {item_name}")
if __name__ == "__main__":
test_engine()
import sys import logging
import os import os
import sqlite3 import sqlite3
import logging import sys
# Thêm thư mục backend vào sys.path để có thể import được common, config # Thêm thư mục backend vào sys.path để có thể import được common, config
backend_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) backend_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if backend_dir not in sys.path: if backend_dir not in sys.path:
sys.path.insert(0, backend_dir) sys.path.insert(0, backend_dir)
from common.starrocks_connection import get_db_connection
from common.constants import SQLITE_DB_PATH from common.constants import SQLITE_DB_PATH
from common.starrocks_connection import get_db_connection
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
......
...@@ -109,7 +109,7 @@ with open("migration_log.txt", "w", encoding="utf-8") as log: ...@@ -109,7 +109,7 @@ with open("migration_log.txt", "w", encoding="utf-8") as log:
filepath = os.path.join(path, file) filepath = os.path.join(path, file)
log.write(f"Reading {file}...\n") log.write(f"Reading {file}...\n")
log.flush() log.flush()
with open(filepath, 'r', encoding='utf-8') as f: with open(filepath, encoding='utf-8') as f:
buffer = "" buffer = ""
in_statement = False in_statement = False
for line in f: for line in f:
......
Reading canifa_chat.lead_flow_history.sql...
Error executing statement in canifa_chat.lead_flow_history.sql: no such table: pg__canifa_chat__lead_flow_history
Error executing statement in canifa_chat.lead_flow_history.sql: no such table: pg__canifa_chat__lead_flow_history
Error executing statement in canifa_chat.lead_flow_history.sql: no such table: pg__canifa_chat__lead_flow_history
Error executing statement in canifa_chat.lead_flow_history.sql: no such table: pg__canifa_chat__lead_flow_history
Error executing statement in canifa_chat.lead_flow_history.sql: no such table: pg__canifa_chat__lead_flow_history
Error executing statement in canifa_chat.lead_flow_history.sql: no such table: pg__canifa_chat__lead_flow_history
Error executing statement in canifa_chat.lead_flow_history.sql: no such table: pg__canifa_chat__lead_flow_history
Error executing statement in canifa_chat.lead_flow_history.sql: no such table: pg__canifa_chat__lead_flow_history
Error executing statement in canifa_chat.lead_flow_history.sql: no such table: pg__canifa_chat__lead_flow_history
Error executing statement in canifa_chat.lead_flow_history.sql: no such table: pg__canifa_chat__lead_flow_history
Error executing statement in canifa_chat.lead_flow_history.sql: no such table: pg__canifa_chat__lead_flow_history
Reading dashboard_canifa.activity_logs.sql...
Reading dashboard_canifa.admin_users.sql...
Error executing statement in dashboard_canifa.admin_users.sql: no such table: pg__dashboard_canifa__admin_users
Error executing statement in dashboard_canifa.admin_users.sql: no such table: pg__dashboard_canifa__admin_users
Reading dashboard_canifa.ai_outfit_tables.sql...
Reading dashboard_canifa.chatbot_fashion_rules.sql...
Error executing statement in dashboard_canifa.chatbot_fashion_rules.sql: no such table: pg__dashboard_canifa__chatbot_fashion_rules
Error executing statement in dashboard_canifa.chatbot_fashion_rules.sql: no such table: pg__dashboard_canifa__chatbot_fashion_rules
Reading dashboard_canifa.chat_history.sql...
Error executing statement in dashboard_canifa.chat_history.sql: no such table: pg__dashboard_canifa__chat_history
Error executing statement in dashboard_canifa.chat_history.sql: no such table: pg__dashboard_canifa__chat_history
Reading dashboard_canifa.desc_field_config.sql...
Error executing statement in dashboard_canifa.desc_field_config.sql: no such table: pg__dashboard_canifa__desc_field_config
Error executing statement in dashboard_canifa.desc_field_config.sql: no such table: pg__dashboard_canifa__desc_field_config
Reading dashboard_canifa.product_size_guide.sql...
Error executing statement in dashboard_canifa.product_size_guide.sql: no such table: pg__dashboard_canifa__product_size_guide
Error executing statement in dashboard_canifa.product_size_guide.sql: no such table: pg__dashboard_canifa__product_size_guide
Error executing statement in dashboard_canifa.product_size_guide.sql: no such table: pg__dashboard_canifa__product_size_guide
Error executing statement in dashboard_canifa.product_size_guide.sql: no such table: pg__dashboard_canifa__product_size_guide
Error executing statement in dashboard_canifa.product_size_guide.sql: no such table: pg__dashboard_canifa__product_size_guide
Error executing statement in dashboard_canifa.product_size_guide.sql: no such table: pg__dashboard_canifa__product_size_guide
Error executing statement in dashboard_canifa.product_size_guide.sql: no such table: pg__dashboard_canifa__product_size_guide
Error executing statement in dashboard_canifa.product_size_guide.sql: no such table: pg__dashboard_canifa__product_size_guide
Error executing statement in dashboard_canifa.product_size_guide.sql: no such table: pg__dashboard_canifa__product_size_guide
Error executing statement in dashboard_canifa.product_size_guide.sql: no such table: pg__dashboard_canifa__product_size_guide
Error executing statement in dashboard_canifa.product_size_guide.sql: no such table: pg__dashboard_canifa__product_size_guide
Reading dashboard_canifa.saved_reports.sql...
Error executing statement in dashboard_canifa.saved_reports.sql: no such table: pg__dashboard_canifa__saved_reports
Error executing statement in dashboard_canifa.saved_reports.sql: no such table: pg__dashboard_canifa__saved_reports
Reading dashboard_canifa.sql_trace_sessions.sql...
Error executing statement in dashboard_canifa.sql_trace_sessions.sql: no such table: pg__dashboard_canifa__sql_trace_sessions
Error executing statement in dashboard_canifa.sql_trace_sessions.sql: no such table: pg__dashboard_canifa__sql_trace_sessions
Reading dashboard_canifa.system_settings.sql...
Reading dashboard_canifa.ultra_descriptions.sql...
Error executing statement in dashboard_canifa.ultra_descriptions.sql: table pg__dashboard_canifa__ultra_descriptions has no column named product_name
Error executing statement in dashboard_canifa.ultra_descriptions.sql: table pg__dashboard_canifa__ultra_descriptions has no column named product_name
Error executing statement in dashboard_canifa.ultra_descriptions.sql: table pg__dashboard_canifa__ultra_descriptions has no column named product_name
Error executing statement in dashboard_canifa.ultra_descriptions.sql: table pg__dashboard_canifa__ultra_descriptions has no column named product_name
Error executing statement in dashboard_canifa.ultra_descriptions.sql: table pg__dashboard_canifa__ultra_descriptions has no column named product_name
Error executing statement in dashboard_canifa.ultra_descriptions.sql: table pg__dashboard_canifa__ultra_descriptions has no column named product_name
Error executing statement in dashboard_canifa.ultra_descriptions.sql: table pg__dashboard_canifa__ultra_descriptions has no column named product_name
Error executing statement in dashboard_canifa.ultra_descriptions.sql: table pg__dashboard_canifa__ultra_descriptions has no column named product_name
Error executing statement in dashboard_canifa.ultra_descriptions.sql: table pg__dashboard_canifa__ultra_descriptions has no column named product_name
Error executing statement in dashboard_canifa.ultra_descriptions.sql: table pg__dashboard_canifa__ultra_descriptions has no column named product_name
Error executing statement in dashboard_canifa.ultra_descriptions.sql: table pg__dashboard_canifa__ultra_descriptions has no column named product_name
Error executing statement in dashboard_canifa.ultra_descriptions.sql: table pg__dashboard_canifa__ultra_descriptions has no column named product_name
Error executing statement in dashboard_canifa.ultra_descriptions.sql: table pg__dashboard_canifa__ultra_descriptions has no column named product_name
Error executing statement in dashboard_canifa.ultra_descriptions.sql: table pg__dashboard_canifa__ultra_descriptions has no column named product_name
Error executing statement in dashboard_canifa.ultra_descriptions.sql: table pg__dashboard_canifa__ultra_descriptions has no column named product_name
Error executing statement in dashboard_canifa.ultra_descriptions.sql: table pg__dashboard_canifa__ultra_descriptions has no column named product_name
Error executing statement in dashboard_canifa.ultra_descriptions.sql: table pg__dashboard_canifa__ultra_descriptions has no column named product_name
Error executing statement in dashboard_canifa.ultra_descriptions.sql: table pg__dashboard_canifa__ultra_descriptions has no column named product_name
Error executing statement in dashboard_canifa.ultra_descriptions.sql: table pg__dashboard_canifa__ultra_descriptions has no column named product_name
Error executing statement in dashboard_canifa.ultra_descriptions.sql: table pg__dashboard_canifa__ultra_descriptions has no column named product_name
Reading public.chatbot_fashion_rules.sql...
Error executing statement in public.chatbot_fashion_rules.sql: no such table: pg__public__chatbot_fashion_rules
Error executing statement in public.chatbot_fashion_rules.sql: no such table: pg__public__chatbot_fashion_rules
Reading public.prompt_rules.sql...
Error executing statement in public.prompt_rules.sql: no such table: pg__public__prompt_rules
Error executing statement in public.prompt_rules.sql: no such table: pg__public__prompt_rules
Reading test_db.magento_product_dimension_with_text_embedding.sql...
...@@ -171,7 +171,7 @@ def main(): ...@@ -171,7 +171,7 @@ def main():
print("\n📄 Sample output:") print("\n📄 Sample output:")
print(f" ID: {sample[0]}") print(f" ID: {sample[0]}")
print(f" size_scale: {sample[1]}") print(f" size_scale: {sample[1]}")
print(f" size (filtered):") print(" size (filtered):")
print(" " + sample[2].replace('\n', '\n ')) print(" " + sample[2].replace('\n', '\n '))
conn.close() conn.close()
......
import sqlite3
import logging import logging
import sqlite3
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -52,3 +52,6 @@ def update_sqlite_colors(): ...@@ -52,3 +52,6 @@ def update_sqlite_colors():
if __name__ == "__main__": if __name__ == "__main__":
update_sqlite_colors() update_sqlite_colors()
...@@ -30,7 +30,7 @@ if season_idx >= 0 and rows: ...@@ -30,7 +30,7 @@ if season_idx >= 0 and rows:
seasons.append(val) seasons.append(val)
counts = Counter(seasons) counts = Counter(seasons)
print(f"\nSeason values (sample from first 50 rows):") print("\nSeason values (sample from first 50 rows):")
print("-" * 40) print("-" * 40)
for s, c in sorted(counts.items(), key=lambda x: -x[1]): for s, c in sorted(counts.items(), key=lambda x: -x[1]):
print(f"{c:5d} | '{s}'") print(f"{c:5d} | '{s}'")
"""
Script tự động thêm context (tên bảng + subsection) vào tất cả size entries trong tonghop.txt
Ví dụ: "Size 92 (2Y):" -> "Size 92 (2Y) - BẢNG SIZE CHUNG CHO UNISEX - TRẺ EM (Dải size lẻ):"
"""
import re
def add_context_to_sizes(input_file, output_file):
with open(input_file, encoding="utf-8") as f:
lines = f.readlines()
result = []
current_table = None # Tên bảng hiện tại
current_subsection = None # Subsection hiện tại (Dải size lẻ, chẵn...)
for line in lines:
stripped = line.strip()
# Phát hiện header bảng (bắt đầu bằng BẢNG hoặc QUẦN)
if stripped.startswith("BẢNG SIZE") or stripped.startswith("QUẦN"):
current_table = stripped
current_subsection = None # Reset subsection khi sang bảng mới
result.append(line)
continue
# Phát hiện subsection (Dải size lẻ, Dải size chẵn)
if "Dải size" in stripped or stripped.startswith("Dải"):
current_subsection = stripped.rstrip(":")
result.append(line)
continue
# Phát hiện dòng Size (bắt mọi pattern: Size XS:, Size 92 (2Y):, Size 26 (XS):)
size_match = re.match(r"^(Size\s+[A-Z0-9]+(?:\s*\([^)]+\))?):(.*)$", stripped)
if size_match and current_table:
size_part = size_match.group(1) # "Size 92 (2Y)" hoặc "Size XS"
rest = size_match.group(2) # Phần còn lại sau dấu :
# Xây dựng context
context_parts = [current_table]
if current_subsection:
context_parts.append(f"({current_subsection})")
context = " - ".join(context_parts)
# Tạo dòng mới với context
new_line = f"{size_part} - {context}:{rest}\n"
result.append(new_line)
continue
# Giữ nguyên các dòng khác
result.append(line)
# Ghi file output
with open(output_file, "w", encoding="utf-8") as f:
f.writelines(result)
print("✅ Đã thêm context vào tất cả size entries!")
print(f"📝 File output: {output_file}")
if __name__ == "__main__":
input_path = r"d:\cnf\chatbot_canifa\backend\datadb\tonghop.txt"
output_path = r"d:\cnf\chatbot_canifa\backend\datadb\tonghop_with_context.txt"
add_context_to_sizes(input_path, output_path)
print("\n🔍 Preview 10 dòng đầu của file mới:")
with open(output_path, encoding="utf-8") as f:
for i, line in enumerate(f):
if i >= 1160 and i < 1170: # Vùng có size entries
print(line.rstrip())
import sys
import subprocess
p = subprocess.Popen(
[sys.executable, 'D:\\cnf\\chatbot_canifa\\backend\\datadb\\test_rag_gpt4o_mini.py'],
stdin=subprocess.PIPE,
stdout=sys.stdout,
stderr=sys.stderr,
text=True,
encoding='utf-8'
)
p.communicate(input='chính sách vận chuyển bên bạn\nexit\n')
import json
import os
import re
import numpy as np
import faiss
from langchain_openai import OpenAIEmbeddings
from dotenv import load_dotenv
# Load env variables for OpenAI
load_dotenv(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", ".env"))
def main():
# 1. Trỏ thẳng vào thư mục datadb
current_dir = os.path.dirname(os.path.abspath(__file__))
file_path = os.path.join(current_dir, "tonghop.txt")
if not os.path.exists(file_path):
print(f" File không tồn tại: {file_path}")
return
with open(file_path, encoding="utf-8") as f:
content = f.read()
# 2. Bóc tách file theo các đầu mục "FILE: ..."
sections = re.split(r"={20,}\nFILE:\s*(.*?)\n={20,}\n", content)
chunks = []
if len(sections) > 1:
# Bỏ qua index 0
for i in range(1, len(sections), 2):
title = sections[i].strip()
text = sections[i + 1].strip()
if text:
chunks.append({"id": len(chunks), "title": title, "content": text})
else:
print(" Không tìm thấy delimiter 'FILE:' trong tonghop.txt.")
return
print(f" BÓC TÁCH: Đã chia thành {len(chunks)} phần chính sách/tài liệu.")
# 3. Embedding với OpenAI
print("\n[Loading Model] Khởi tạo OpenAIEmbeddings (text-embedding-3-small)...")
embedder = OpenAIEmbeddings(model="text-embedding-3-small")
# Gom title + content để embed
embed_texts = [f"{c['title']}\n{c['content']}" for c in chunks]
print("[Embedding] Đang gọi API OpenAI để sinh Vectors...")
embeddings_list = embedder.embed_documents(embed_texts)
embeddings = np.array(embeddings_list).astype("float32")
# Chuẩn hoá L2 để tính Cosine Similarity mượt hơn với FAISS IndexFlatIP
faiss.normalize_L2(embeddings)
# 4. Khởi tạo FAISS Index & Lưu metadata
dim = embeddings.shape[1]
index = faiss.IndexFlatIP(dim)
index.add(embeddings)
index_path = os.path.join(current_dir, "canifa_docs.index")
meta_path = os.path.join(current_dir, "canifa_docs_meta.json")
faiss.write_index(index, index_path)
with open(meta_path, "w", encoding="utf-8") as f:
json.dump(chunks, f, ensure_ascii=False, indent=2)
print(f" ĐÃ Lưu FAISS vector vào: {index_path} (dim={dim})")
print(f" ĐÃ Lưu JSON nội dung vào: {meta_path}\n")
# 5. TEST MÔ PHỎNG LUÔN
queries = [
"mua hàng online muốn đổi trực tiếp ở cửa hàng gần nhất thì phải làm sao hả shop?",
"thời gian gửi hàng ship về hà đông mất tầm bao lâu?",
"có bán quần của trẻ em bé gái cao tầm 1 mét 4 không cho xin thông số",
]
print(" BẮT ĐẦU TEST SEARCH RAG (TRUY VẤN FULL CÒN NGUYÊN BẢN VỚI OPENAI EMBEDDING)")
print("=" * 70)
for q in queries:
print(f"\n CÂU HỎI: '{q}'")
# Sinh vector câu hỏi bằng OpenAI
q_emb_list = embedder.embed_query(q)
q_emb = np.array([q_emb_list]).astype("float32")
faiss.normalize_L2(q_emb)
# Search top 1
scores, I = index.search(q_emb, 1)
best_id = I[0][0]
best_score = scores[0][0]
if best_id != -1:
best_chunk = chunks[best_id]
print(f" MATCH ĐẦU MỤC: '{best_chunk['title']}' (Score: {best_score:.4f})")
print(
f" NỘI DUNG FULL RETURN (Trích 300 chữ): {best_chunk['content'][:300].replace(chr(10), ' ')} ... <CÒN NỮA>"
)
print(f" Độ dài thực tế sẽ đưa cho LLM: {len(best_chunk['content'])} ký tự.")
else:
print("Không tìm thấy.")
if __name__ == "__main__":
main()
[
{
"id": 0,
"title": "data/text/chinh-sach-bao-mat.txt",
"content": "Canifa cam kết xây dựng và công bố chính sách bảo mật thông tin khi thu thập và sử dụng thông tin cá nhân của người tiêu dùng..."
},
{
"id": 1,
"title": "data/text/cua-hang-html.txt",
"content": "search\nCửa hàng\n..."
},
{
"id": 2,
"title": "data/text/dieu-kien-dieu-khoan-khtt-html.txt",
"content": "Áp dụng trên hệ thống cửa hàng Canifa toàn quốc cho đến khi có thông báo mới.\n..."
},
{
"id": 3,
"title": "data/text/gioi-thieu-html.txt",
"content": "Canifa 20 năm - Khoác lên niềm vui gia đình Việt\n..."
},
{
"id": 4,
"title": "data/text/hoi-dap.txt",
"content": "Thanh toán\nThanh toán trả trước\n..."
},
{
"id": 5,
"title": "data/text/huong-dan-chon-size-html.txt",
"content": "HƯỚNG DẪN CHỌN SIZE - CANIFA\n..."
},
{
"id": 6,
"title": "data/text/lien-he-html.txt",
"content": "Hỗ trợ Khách hàng mua online\n..."
},
{
"id": 7,
"title": "data/text/voi-cong-dong-html.txt",
"content": "Phát triển bền vững: 03 xanh\n..."
}
]
\ No newline at end of file
search
Cửa hàng
Tài khoản
Giỏ hàng
SẢN PHẨM MỚI
NỮ
NAM
BÉ GÁI
BÉ TRAI
CANIFA S
LICENSE
SCHOOL
ĐỒNG PHỤC
Trang chủ
Hệ thống cửa hàng
110
Hệ thống cửa hàng trên toàn quốc
Canifa hướng đến mục tiêu mang lại niềm vui mặc mới mỗi ngày cho hàng triệu người tiêu dùng Việt. Chúng tôi tin rằng người dân Việt Nam cũng đang hướng đến một cuộc sống năng động, tích cực hơn.
Tìm kiếm cửa hàng
Lựa chọn Tỉnh / Thành Phố
Hà Nội
Bắc Ninh
Hưng Yên
Hồ Chí Minh
Thái Bình
Nghệ An
Hải Phòng
Thanh Hoá
Quảng Ninh
Vĩnh Phúc
Hòa Bình
Sơn La
Quảng Bình
Phú Thọ
Đà Nẵng
Trà Vinh
Ninh Bình
Bình Phước
Bình Dương
Hải Dương
Hà Nam
Đồng Nai
Nam Định
Hà Tĩnh
Lâm Đồng
Lào Cai
Gia Lai
Cao Bằng
Tuyên Quang
Thái Nguyên
Bà Rịa – Vũng Tàu
Điện Biên
Bắc Giang
Yên Bái
Lạng Sơn
Đắk Lắk
Lựa chọn Quận / Huyện
Quận Ba Đình
Quận Cầu Giấy
Quận Long Biên
Quận Hai Bà Trưng
Huyện Thanh Trì
Quận Hà Đông
Quận Thanh Xuân
Huyện Thạch Thất
Huyện Đan Phượng
Huyện Thường Tín
Quận Nam Từ Liêm
Huyện Quốc Oai
Thị xã Sơn Tây
Quận Tây Hồ
Quận Hoàng Mai
Huyện Gia Lâm
Huyện Đông Anh
Huyện Hoài Đức
Huyện Chương Mỹ
Huyện Ứng Hòa
Quận Bắc Từ Liêm
Quận Đống Đa
CANIFA Lotte Liễu Giai
F3-A02 & F3-C06 tầng 3, Lotte Center Hanoi, 54 Đường Liễu Giai
09:30 - 22:00
Đang mở
ĐT: (+84) - 024 7300 0166
Chỉ đường
CANIFA - 335 Cầu Giấy
335 Cầu Giấy, P. Quan Hoa
09:00 - 22:30
Đang mở
ĐT: (+84) - 024 7301 5866
Chỉ đường
CANIFA Aeon mall Long Biên
T247, tầng 2 TTTM Aeon Mall Long Biên, P. Long Biên
10:00 - 22:00
Chưa mở cửa
ĐT: (+84) - 02473 053 866
Chỉ đường
CANIFA Times City
40-42 đường Tương Lai, tầng B1 TTTM Times city, 458 Minh Khai
09:30 - 22:00
Đang mở
ĐT: (+84) - 024 7305 2866
Chỉ đường
CANIFA - Thanh Trì (Đại lý)
Đường mới, xóm chùa, Ngũ Hiệp
09:00 - 22:00
Đang mở
ĐT: (+84) - 0898 543 536
Chỉ đường
CANIFA 171 Trần Phú
171 Đường Trần Phú
09:00 - 22:30
Đang mở
ĐT: (+84) - 0247 302 1866
Chỉ đường
CANIFA - TTTM Royal city
B2-R6-43-44 TTTM Royal city, 72 Nguyễn Trãi
10:00 - 22:00
Chưa mở cửa
ĐT: (+84) - 024 7307 4866
Chỉ đường
CANIFA - Bình Phú, Thạch Thất (Đại lý)
48 Thái Hòa - Bình Phú
09:00 - 22:00
Đang mở
ĐT: (+84) - 0989 758 822
Chỉ đường
CANIFA - 133 Tây Sơn, Đan Phượng (Đại lý)
133 Tây Sơn (cạnh Techcombank), Thị Trấn Phùng
09:00 - 22:00
Đang mở
ĐT: (+84) - 0392 661 983
Chỉ đường
CANIFA - 304 Phố Ga, Thường Tín (Đại lý)
304 Phố Ga - TT Thường Tín
09:00 - 22:00
Đang mở
ĐT: (+84) - 0932 378 856
Chỉ đường
CANIFA - 247 Hồ Tùng Mậu
247 Hồ Tùng Mậu, P. Cầu Diễn
09:00 - 22:30
Đang mở
ĐT: (+84) - 024 7306 4866
Chỉ đường
CANIFA - Quốc Oai (Đại lý)
56 Phố Đồng Hương - Thị Trấn Quốc Oai
09:00 - 22:00
Đang mở
ĐT: (+84) - 0969 547 895
Chỉ đường
CANIFA - 276 Chùa Thông
276 Chùa Thông
09:00 - 22:30
Đang mở
ĐT: (+84) - 024 7304 2688
Chỉ đường
CANIFA - Aeon mall Hà Đông
T211, Tầng 2 TTTM Aeon Mall Hà Đông, P.Dương Nội
10:00 - 22:00
Chưa mở cửa
ĐT: (+84) - 024 7303 2688
Chỉ đường
CANIFA - 447 Lạc Long Quân
447 Lạc Long Quân
09:00 - 22:30
Đang mở
ĐT: (+84) - 024 7307 1866
Chỉ đường
CANIFA - 24 Nguyễn Hữu Thọ
24 Nguyễn Hữu Thọ, Hoàng Liệt
09:00 - 22:30
Đang mở
ĐT: (+84) - 0247 3002 866
Chỉ đường
CANIFA - 38 Kim Đồng, Hà Nội
38 Kim Đồng
09:00 - 22:30
Đang mở
ĐT: (+84) - 0247 3005 866
Chỉ đường
Canifa 24 Nguyễn Cơ Thạch
24 Nguyễn Cơ Thạch
09:00 - 22:30
Đang mở
ĐT: (+84) - 024 7304 7866
Chỉ đường
CANIFA - Center Point – 27 Lê Văn Lương
Center Point – 27 Lê Văn Lương, P. Nhân Chính
09:00 - 22:30
Đang mở
ĐT: (+84) - 024 7304 5866
Chỉ đường
CANIFA - Bà Triệu, Hà Nội
69 - 73 Bà Triệu
09:00 - 22:30
Đang mở
ĐT: (+84) - 024 7305 1866
Chỉ đường
CANIFA - 164 Ngô Xuân Quảng, Hà Nội (Đại lý)
164 Ngô Xuân Quảng
09:00 - 22:30
Đang mở
ĐT: (+84) -
Chỉ đường
CANIFA - Đông Anh, Hà Nội
59 Cao Lỗ, Uy Nỗ
09:00 - 22:30
Đang mở
ĐT: (+84) - 024 7301 4866
Chỉ đường
CANIFA - Liên Quan,Thạch Thất (Đại lý)
Tổ dân phố khu Phố, thị trấn Liên Quan
08:00 - 22:00
Đang mở
ĐT: (+84) - 0979 014 638
Chỉ đường
CANIFA - Geleximco An Khánh
LK 14 - Lô 22, KĐT mới Gleximco A, An Khánh
09:00 - 22:30
Đang mở
ĐT: (+84) - 024 73 060 866
Chỉ đường
CANIFA - Chúc Sơn (Đại lý)
86 Bắc Sơn, TT Chúc Sơn, Chương Mỹ, Hà Nội
09:00 - 22:00
Đang mở
ĐT: (+84) - 0934551005
Chỉ đường
CANIFA - Trạm Trôi, Hoài Đức, Hà Nội (Đại lý)
Khu 7, thị trấn Trạm Trôi
08:00 - 22:00
Đang mở
ĐT: (+84) - 0976 966 884
Chỉ đường
CANIFA - Vân Đình, Ứng Hoà, Hà Nội (Đại lý)
288 Lê Lợi, thị trấn Vân Đình
08:00 - 22:00
Đang mở
ĐT: (+84) - 0375131003
Chỉ đường
CANIFA - Bắc Từ Liêm (Đại lý)
32 phố Nhổn
08:00 - 22:00
Đang mở
ĐT: (+84) - 0948 279 266
Chỉ đường
Canifa - Yên Xá, Thanh Trì
Số 68 đường 70, Tân Triều
09:00 - 22:30
Đang mở
ĐT: (+84) - 024 73 067 866
Chỉ đường
CANIFA - 168 Nguyễn Khánh Toàn
168 Nguyễn Khánh Toàn, p. Quan Hoa
09:00 - 22:00
Đang mở
ĐT: (+84) - 024 7304 0866
Chỉ đường
CANIFA - 139 Bạch Mai
139 Bạch Mai, P. Thanh Nhàn
09:00 - 22:30
Đang mở
ĐT: (+84) - 024 7306 1866
Chỉ đường
CANIFA - 152 Cao Lỗ
152 Cao Lỗ, Uy Nỗ
09:00 - 22:30
Đang mở
ĐT: (+84) - 024 7307 3866
Chỉ đường
CANIFA - Ba Đình
Số 6 Điện Biên Phủ, Ba Đình
09:00 - 22:30
Đang mở
ĐT: (+84) - 024 7303 3866
Chỉ đường
CANIFA - Tasco Mall Long Biên
7-9 Nguyễn Văn Linh, Gia Thụy
09:00 - 22:00
Đang mở
ĐT: (+84) - 024 7305 5688
Chỉ đường
CANIFA Lotte Liễu Giai T05
F5-B16-B17 tầng 05, Lotte Center Hanoi, 54 Đường Liễu Giai
09:30 - 22:00
Đang mở
ĐT: (+84) - 024 7300 0166
Chỉ đường
CANIFA KIDS - LITTLE SAM (Đại lý)
314 tầng 3 TTTM The Loop, 241 Xuân Thuỷ, phường Cầu Giấy
10:00 - 22:00
Chưa mở cửa
ĐT: (+84) - 0333 424 955
Chỉ đường
CANIFA - Xã Đàn, Hà Nội
65-67 Xã Đàn, Phường Kim Liên, Quận Đống Đa
09:00 - 22:30
Đang mở
ĐT: (+84) - 024 7305 5166
Chỉ đường
CANIFA - 440 Quang Trung Hà Đông
440 Quang Trung
09:00 - 22:30
Đang mở
ĐT: (+84) - 0247 307 5866
Chỉ đường
Canifa - Go! Thăng Long
1S22 TTTM Go! Thăng Long, số 222 đường Trần Duy Hưng, P. Yên Hòa
08:00 - 22:00
Đang mở
ĐT: (+84) - 024 7301 3566
Chỉ đường
CANIFA 181 Giảng Võ
181 Đường Giảng Võ
09:00 - 22:30
Đang mở
ĐT: (+84) - 024 7301 2866
Chỉ đường
CANIFA 121 - 123 Chùa Bộc
121 - 123 Đường Chùa Bộc
09:00 - 22:30
Đang mở
ĐT: (+84) - 024 7302 3688
Chỉ đường
CANIFA 554 Nguyễn Văn Cừ
554 Đường Nguyễn Văn Cừ
09:00 - 22:30
Đang mở
ĐT: (+84) - 0247 305 9866
Chỉ đường
Hệ thống cửa hàng
Hà Nội
Bắc Ninh
Hưng Yên
Hồ Chí Minh
Thái Bình
Nghệ An
Hải Phòng
Thanh Hoá
Quảng Ninh
Vĩnh Phúc
Hòa Bình
Sơn La
Quảng Bình
Phú Thọ
Đà Nẵng
Trà Vinh
Ninh Bình
Bình Phước
Bình Dương
Hải Dương
Hà Nam
Đồng Nai
Nam Định
Hà Tĩnh
Lâm Đồng
Lào Cai
Gia Lai
Cao Bằng
Tuyên Quang
Thái Nguyên
Bà Rịa – Vũng Tàu
Điện Biên
Bắc Giang
Yên Bái
Lạng Sơn
Đắk Lắk
CỬA HÀNG NỔI BẬT
CANIFA 554 Nguyễn Văn Cừ
554 Đường Nguyễn Văn Cừ
ĐT: 0247 305 9866
Giờ mở cửa: 09:00 - 22:30
CANIFA 121 - 123 Chùa Bộc
121 - 123 Đường Chùa Bộc
ĐT: 024 7302 3688
Giờ mở cửa: 09:00 - 22:30
CANIFA 181 Giảng Võ
181 Đường Giảng Võ
ĐT: 024 7301 2866
Giờ mở cửa: 09:00 - 22:30
TOP
\ No newline at end of file
import os
import json
import pymysql
from openai import OpenAI
import time
# ==========================================
# 🔐 HARD KEY CONFIGURATION (As requested)
# ==========================================
OPENAI_API_KEY = "sk-proj-srJ3l3B5q1CzRezXAnaewbbRfuWzIjYHbcAdggzsa4MmtXEHaIwS1OTkMgLpMDikgh"
SR_HOST = "172.16.2.100"
SR_PORT = 9030
SR_USER = "anhvh"
SR_PASS = "v0WYGeyLRCckXotT"
SR_DB = "shared_source"
# Parameter
CHUNK_SIZE = 500
CHUNK_OVERLAP = 50
EMBEDDING_MODEL = "text-embedding-3-small" # 1536 dimensions
client = OpenAI(api_key=OPENAI_API_KEY)
def get_embedding(text):
"""Lấy vector 1536 chiều từ OpenAI"""
try:
text = text.replace("\n", " ")
return client.embeddings.create(input=[text], model=EMBEDDING_MODEL).data[0].embedding
except Exception as e:
print(f"❌ Lỗi Embedding: {e}")
return None
def connect_starrocks():
return pymysql.connect(
host=SR_HOST,
port=SR_PORT,
user=SR_USER,
password=SR_PASS,
database=SR_DB,
charset='utf8mb4',
cursorclass=pymysql.cursors.DictCursor
)
def chunk_text(text, size=CHUNK_SIZE, overlap=CHUNK_OVERLAP):
"""Chia nhỏ văn bản với overlap"""
chunks = []
start = 0
while start < len(text):
end = start + size
chunks.append(text[start:end])
start += size - overlap
return chunks
def ingest():
input_file = r"d:\cnf\chatbot_canifa\backend\datadb\tonghop.txt"
if not os.path.exists(input_file):
print(f"❌ Không tìm thấy file: {input_file}")
return
print(f"📖 Đang đọc file {input_file}...")
with open(input_file, "r", encoding="utf-8") as f:
full_content = f.read()
# Tách dữ liệu theo từng FILE giả định trong tonghop.txt
sections = full_content.split("================================================================================")
db = connect_starrocks()
cursor = db.cursor()
total_chunks = 0
record_id = int(time.time()) # Làm ID cơ bản
for section in sections:
if not section.strip(): continue
# Lấy tiêu đề file nếu có
lines = section.strip().split("\n")
title = "Canifa Knowledge"
if "FILE:" in lines[0]:
title = lines[0].replace("FILE:", "").strip()
content = "\n".join(lines[1:])
else:
content = section
print(f"🚀 Đang xử lý section: {title}")
chunks = chunk_text(content)
for i, chunk in enumerate(chunks):
if len(chunk.strip()) < 20: continue # Bỏ qua đoạn quá ngắn
vector = get_embedding(chunk)
if not vector: continue
metadata = {
"title": title,
"chunk_idx": i,
"source": "tonghop.txt",
"timestamp": time.time()
}
sql = "INSERT INTO shared_source.canifa_knowledge (id, content, metadata, embedding) VALUES (%s, %s, %s, %s)"
try:
cursor.execute(sql, (record_id, chunk, json.dumps(metadata, ensure_ascii=False), str(vector)))
record_id += 1
total_chunks += 1
if total_chunks % 10 == 0:
db.commit()
print(f"✅ Đã nạp {total_chunks} chunks...")
except Exception as e:
print(f"❌ Lỗi SQL: {e}")
db.commit()
db.close()
print(f"🎊 HOÀN THÀNH! Tổng cộng đã nạp {total_chunks} vào StarRocks.")
if __name__ == "__main__":
ingest()
This source diff could not be displayed because it is too large. You can view the blob instead.
import sys
import subprocess
p = subprocess.Popen(
['d:/cnf/chatbot_canifa/preference/chatbot-rsa/.venv/Scripts/python.exe', 'D:/cnf/chatbot_canifa/backend/datadb/test_rag_gpt4o_mini.py'],
stdin=subprocess.PIPE,
stdout=sys.stdout,
stderr=sys.stderr,
text=True,
encoding='utf-8'
)
p.communicate(input='chính sách vận chuyển bên bạn\nexit\n')
import sys
import os
from playwright.sync_api import sync_playwright
def scrape_stores():
output_file = r"D:\cnf\chatbot_canifa\backend\datadb\cuahang.txt"
url = "https://canifa.com/cua-hang.html"
print(f"Starting scrape from {url}...")
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
try:
page.goto(url, timeout=60000)
print("Page loaded.")
# Wait for store list to be visible.
# Trying a generic wait first, or looking for specific text.
page.wait_for_load_state("networkidle")
# Attempt to extract data
# Based on typical store locator pages, we look for list items.
# If structure is unknown, we will grab the main container text.
# Evaluate script to get all text from the store list container if possible
# or just dump the whole body text if specific selectors aren't obvious.
# We'll try to find elements with class containing 'store' or 'address' or similar.
# Let's try to get all text content cleanly first.
content = page.evaluate("""() => {
// Return all text, or try to find specific store elements
// Canifa tends to use specific classes.
// Let's try to find the main container.
// Heuristic: look for element containing 'Hệ thống cửa hàng'
// and get its siblings or children.
const bodyText = document.body.innerText;
return bodyText;
}""")
with open(output_file, "w", encoding="utf-8") as f:
f.write(content)
print(f"Successfully saved data to {output_file}")
except Exception as e:
print(f"Error scraping: {e}")
# Save screenshot if failed?
# page.screenshot(path="screenshot.png")
finally:
browser.close()
if __name__ == "__main__":
scrape_stores()
import json
import os
import faiss
import numpy as np
from dotenv import load_dotenv
from langchain_core.prompts import PromptTemplate
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
current_dir = os.path.dirname(os.path.abspath(__file__))
load_dotenv(os.path.join(current_dir, "..", ".env"))
def main():
print(" BẮT ĐẦU TEST E2E RAG VỚI GPT-4o-mini (BẢN PROMPT THÔNG MINH HƠN)")
print("=" * 80)
index_path = os.path.join(current_dir, "canifa_docs.index")
meta_path = os.path.join(current_dir, "canifa_docs_meta.json")
if not os.path.exists(index_path) or not os.path.exists(meta_path):
print(" Chưa có Index. Chạy file build_and_test_faiss.py trước để tạo.")
return
print("[1] Đang nạp thư viện Vector từ FAISS và metadata...")
index = faiss.read_index(index_path)
with open(meta_path, encoding="utf-8") as f:
chunks = json.load(f)
print("[2] Khởi tạo Embedder và Models (gpt-4o-mini)...")
embedder = OpenAIEmbeddings(model="text-embedding-3-small")
llm = ChatOpenAI(model="gpt-4.1-mini", temperature=0)
prompt_template = PromptTemplate.from_template(
"Bạn là nhân viên CSKH của Canifa. Bạn luôn nhiệt tình và vui vẻ.\n"
"Yêu cầu:\n"
"1. Nếu khách hàng chỉ chào hỏi (ví dụ: 'chào', 'hello'), hãy chào lại lịch sự và hỏi xem bạn có thể giúp gì được không, KHÔNG báo lỗi thiếu thông tin.\n"
"2. Nếu khách hỏi về chính sách, quy định, cửa hàng, hãy DỰA VÀO TÀI LIỆU CUNG CẤP BÊN DƯỚI để trả lời chi tiết và thân thiện.\n"
"3. Đọc thật kỹ tài liệu được cung cấp. Nếu tài liệu ĐÃ CÓ nhắc đến từ khoá khách hỏi, hãy cố gắng tóm tắt câu trả lời thay vì từ chối.\n"
"4. CHỈ KHI NÀO tài liệu dưới đây hoàn toàn không nhắc đến nội dung khách hỏi, bạn mới được nói: 'Dạ em chưa tìm thấy thông tin này trong chính sách hiện tại. Bạn vui lòng liên hệ hotline 1800 6061 nha!'\n"
"5. Không bao giờ được tự bịa ra thông số, giá tiền, hoặc điều khoản nếu tài liệu không có.\n\n"
"--- BẮT ĐẦU TÀI LIỆU ---\n"
"{context}\n"
"--- KẾT THÚC TÀI LIỆU ---\n\n"
"CÂU HỎI CỦA KHÁCH: {question}\n\n"
"TRẢ LỜI CỦA BẠN:"
)
chain = prompt_template | llm
print("\n" + "*" * 80)
print(" CHATBOT ĐÃ SẴN SÀNG! (Gõ 'exit' hoặc 'quit' để thoát)")
print("*" * 80 + "\n")
while True:
try:
q = input("\n BẠN: ")
if q.strip().lower() in ["exit", "quit"]:
print(" Tạm biệt!")
break
if not q.strip():
continue
q_emb_list = embedder.embed_query(q)
q_emb = np.array([q_emb_list]).astype("float32")
faiss.normalize_L2(q_emb)
TOP_K = 10
scores, I = index.search(q_emb, TOP_K)
contexts = []
for i in range(TOP_K):
doc_id = I[0][i]
if doc_id != -1:
doc_title = chunks[doc_id]["title"]
doc_content = chunks[doc_id]["content"]
contexts.append(f"--- TÀI LIỆU: {doc_title} ---\n{doc_content}")
if contexts:
combined_context = "\n\n".join(contexts)
response = chain.invoke({"context": combined_context, "question": q})
print(f" CANIFA AI:\n{response.content}")
else:
print(" CANIFA AI: Hệ thống không tìm thấy tài liệu phù hợp.")
except (KeyboardInterrupt, EOFError):
print("\n Tạm biệt!")
break
if __name__ == "__main__":
main()
================================================================================
FILE: data/text/chinh-sach-bao-mat.txt
================================================================================
Canifa cam kết xây dựng và công bố chính sách bảo mật thông tin khi thu thập và sử dụng thông tin cá nhân của người tiêu dùng...
================================================================================
FILE: data/text/cua-hang-html.txt
================================================================================
search
Cửa hàng
...
================================================================================
FILE: data/text/dieu-kien-dieu-khoan-khtt-html.txt
================================================================================
Áp dụng trên hệ thống cửa hàng Canifa toàn quốc cho đến khi có thông báo mới.
...
================================================================================
FILE: data/text/gioi-thieu-html.txt
================================================================================
Canifa 20 năm - Khoác lên niềm vui gia đình Việt
...
================================================================================
FILE: data/text/hoi-dap.txt
================================================================================
Thanh toán
Thanh toán trả trước
...
================================================================================
FILE: data/text/huong-dan-chon-size-html.txt
================================================================================
HƯỚNG DẪN CHỌN SIZE - CANIFA
...
================================================================================
FILE: data/text/lien-he-html.txt
================================================================================
Hỗ trợ Khách hàng mua online
...
================================================================================
FILE: data/text/voi-cong-dong-html.txt
================================================================================
Phát triển bền vững: 03 xanh
...
================================================================================
FILE: data/text/chinh-sach-bao-mat.txt
================================================================================
Canifa cam kết xây dựng và công bố chính sách bảo mật thông tin khi thu thập và sử dụng thông tin cá nhân của người tiêu dùng với đầy đủ các nội dung sau:
A. Nguyên Tắc Chung:
Chính sách bảo mật thông tin này (“Chính Sách Bảo Mật Canifa”) mô tả cách thức Công ty Cổ phần anifa (“Canifa” hoặc “chúng tôi”) thu thập, tiếp nhận, tổng hợp, lưu giữ, sử dụng, xử lý, tiết lộ, chia sẻ và bảo đảm an toàn thông tin của các tổ chức, cá nhân (“Người dùng” hoặc “Quý Khách”), bao gồm khách hàng, đại lý, đối tác: (i) truy cập, sử dụng các kênh tương tác khách hàng thuộc sở hữu của Công Ty, bao gồm nhưng không giới hạn: ứng dụng di động Canifa, website www.canifa.com, và hội nhóm trên các trang mạng xã hội (như facebook, Instagram, Tiktok …) thuộc sở hữu của Công Ty (“Kênh Canifa”); (ii) là khách hàng thân thiết trong Chương trình Chăm Sóc Khách Hàng Thân Thiết của Canifa (“Chương Trình”); và/hoặc (iii) mua bán các loại hàng hóa do Canifa cung cấp hoặc (iv) thông qua các nhân viên dịch vụ khách hàng của chúng tôi, hoặc truy cập vào Nền Tảng của chúng tôi thông qua máy vi tính, thiết bị di động, hoặc bất kỳ thiết bị nào khác có kết nối Internet của Quý khách (mục (i), (ii) và (iii) gọi chung là “Dịch Vụ”). Vui lòng đọc kỹ Chính Sách Bảo Mật này, các điều khoản điều kiện tương ứng và các quy định khác (nếu có).
Chính Sách Bảo Mật Canifa bao gồm các nội dung sau:
Sự chấp thuận
Mục đích thu thập
Phạm vi thu thập
Thời gian lưu trữ
Không chia sẻ thông tin cá nhân khách hàng
An toàn dữ liệu
Quyền của Khách hàng đối với thông tin cá nhân
Trách nhiệm của khách hàng để đảm bảo bảo mật thông tin
Cách thức liên hệ với Canifa
Đơn vị thu thập và quản lý thông tin
Hiệu lực
B. Nội dung chi tiết
Sự Chấp Thuận
Vui lòng đọc kỹ chính sách bảo mật này. Bằng cách nhấp và đánh dấu vào các tuyên bố “Tiếp tục”, “Đồng ý”, “tôi đồng ý với chính sách bảo mật của Canifa” hoặc các tuyên bố tương tự được hiển thị tại trang đăng ký của Canifa hoặc trong quá trình cung cấp các dịch vụ hoặc quyền truy cập vào nền tảng cho Quý khách, Quý khách xác nhận rằng đã đọc và hiểu các điều khoản của chính sách bảo mật này và đã đồng ý và cho phép thực hiện việc thu thập, sử dụng, tiết lộ, lưu trữ, chuyển giao và/hoặc xử lý dữ liệu cá nhân của Quý khách như được mô tả và quy định tại chính sách bảo mật này.
Nếu Quý khách không đồng ý với Chính Sách này, Quý khách có thể dừng cung cấp cho Chúng tôi bất kỳ thông tin cá nhân nào và/hoặc sử dụng các quyền như được nêu tại mục 7 dưới đây.
Canifa bảo lưu quyền sửa đổi, bổ sung nhằm hoàn thiện đối với Chính sách này vào bất kỳ thời điểm nào. Chúng tôi khuyến khích Quý Khách thường xuyên xem lại Chính sách bảo mật thông tin Canifa để được cập nhật mới nhất đảm bảo Quý Khách đã biết và thực hiện quyền quản lý thông tin của mình.
Mục đích thu thập thông tin cá nhân khách hàng
Canifa thu thập thông tin người dùng nhằm phục vụ cho các mục đích:
Đơn hàng: để xử lý các vấn đề liên quan đến đơn đặt hàng của Quý khách;
Duy trì tài khoản: Để tạo và duy trì tài khoản của Qúy khách, bao gồm các chương trình khách hàng thân thiết, các chương trình khuyến mại,… đi kèm với tài khoản của Quý khách;
Cung cấp các dịch vụ/ tiện ích cho KH dựa trên nhu cầu và các thói quen của KH mua sắm tại Canifa;
Gửi thông báo Giới thiệu các sản phẩm, chương trình, dịch vụ mới của Canifa
Dịch vụ Người Dùng, Dịch vụ Chăm sóc khách hàng: bao gồm các phản hồi cho các yêu cầu, khiếu nại và phản hồi của Quý Khách;
An ninh: cho mục đích phát hiện, ngăn chặn các hoạt động giả mạo, chiếm dụng tài khoản của KHTT nhằm mục địch trục lợi, gian lận;
Theo yêu cầu của pháp luật: tùy quy định của pháp luật vào từng thời điểm, chúng tôi có thể thu thập, lưu trữ và cung cấp theo yêu cầu của cơ quan nhà nước có thẩm quyền.
Phạm vi thu thập
Các loại thông tin được chúng tôi thu thập và hình thức thu thập thông tin như sau:
Thông tin Quý khách cung cấp cho chúng tôi:
Đó là các thông tin cá nhân Quý khách cung cấp cho chúng tôi được thực hiện chủ yếu trên các kênh Canifa bao gồm bất kỳ thông tin, dữ liệu nào có thể được sử dụng để nhận dạng Quý Khách hoặc dựa vào đó mà Quý khách được xác định, chẳng hạn như họ tên, giới tính, ngày sinh, thông tin CMND/thẻ căn cước công dân/Hộ chiếu (nếu có), quốc tịch, hình ảnh cá nhân, thông tin mối quan hệ gia đình (cha mẹ, con cái), số điện thoại, chi tiết thẻ thanh toán và ngân hàng, địa chỉ email , thông tin đăng nhập tài khoản như tên đăng nhập, mật khẩu đăng nhập, ID/địa chỉ đăng nhập, câu hỏi/ trả lời bảo mật, các thông tin khác gắn liền với một con người cụ thể hoặc giúp xác định một con người cụ thể.
Thu thập và sử dụng Cookies
Canifa và các bên thứ ba mà Canifa hợp tác (nếu có) có thể sử dụng các phương thức tự động (gọi chung là “Cookies”) có liên quan đến việc sử dụng Kênh Canifa để nhận diện trình duyệt hoặc thiết bị của quý khách, tìm hiểu thêm về sở thích của Quý khách, cung cấp cho quý khách các tính năng và dịch vụ thiết yếu và cho các mục đích bổ sung khác. Cookies có thể chứa đựng các công cụ định danh độc nhất và được lưu trữ tại máy tính hoặc thiết bị di động của Quý khách, trong email mà Canifa gửi, trên các Kênh Canifa, và tại một số địa điểm khác. Cookies có thể truyền tải Thông Tin cá nhân và việc sử dụng Dịch Vụ.
Canifa có thể chia sẻ Thông Tin cá nhân thu thập qua Cookies không mang tính nhận dạng cá nhân với các bên thứ ba, chẳng hạn như dữ liệu vị trí, số nhận dạng quảng cáo hoặc số nhận dạng tài khoản chung (như địa chỉ email), để tạo điều kiện hiển thị quảng cáo.
Quý khách có thể quản lý Cookies trình duyệt bằng việc cài đặt trình duyệt của mình, chặn hoặc ngừng hoạt động Cookies, bằng cách xóa lịch sử trình duyệt và xóa bộ nhớ cache khỏi trình duyệt internet của mình. Quý khách cũng có thể giới hạn việc chia sẻ của chúng tôi về một số Thông Tin cá nhân này thông qua cài đặt thiết bị di động của mình. Nếu Quý khách tắt tất cả Cookies, cả chúng tôi và bên thứ ba sẽ không thể chuyển Cookies sang hoặc từ trình duyệt của quý khách. Tuy nhiên, nếu quý khách làm điều này, Quý khách có thể phải tự tay điều chỉnh một số tùy chọn mỗi khi quý khách truy cập lại vào Dịch Vụ và một số tính năng, kênh và dịch vụ có thể không hoạt động.
Thông tin từ các nguồn khác: Chúng tôi có thể thu thập thông tin cá nhân từ các nguồn hợp pháp khác như chương trình khuyến mại, khảo sát,…
Thời gian lưu trữ
Thông tin cá nhân của khách hàng sẽ được lưu trữ và bảo mật trên hệ thống của Canifa cho đến khi khách hàng tự đăng nhập và thực hiện hủy bỏ hoặc có yêu cầu Canifa hủy bỏ trên hệ thống. Trong mọi trường hợp, thông tin cá nhân của khách hàng sẽ được bảo mật trên máy chủ của canifa.com
Không chia sẻ thông tin cá nhân khách hàng
Chúng tôi sẽ không cung cấp thông tin cá nhân của Quý khách cho bất kỳ bên thứ ba nào, trừ một số hoạt động cần thiết sau:
Các bên cung cấp dịch vụ (ví dụ: đối tác trong các lĩnh vực như dịch vụ thanh toán, dịch vụ vận chuyển và giao nhận , tiếp thị, phân tích dữ liệu hoặc nghiên cứu khảo sát khách hàng, truyền thông xã hội, dịch vụ khách hàng, dịch vụ cài đặt, công nghệ thông tin và, dịch vụ lưu trữ web)
Yêu cầu pháp lý: Canifa có thể tiết lộ các thông tin cá nhân nếu điều đó do luật pháp yêu cầu và việc tiết lộ như vậy là cần thiết để tuân thủ quy trình pháp lý
Chuyển giao kinh doanh (nếu có): trong trường hợp sáp nhập, hợp nhất toàn bộ hoặc một phần với công ty khác.
An toàn dữ liệu
Canifa luôn nỗ lực để giữ an toàn thông tin cá nhân của KH, Chúng tôi đã và đang thực hiện nhiều biện pháp an toàn, bao gồm:
Bảo đảm an toàn trong môi trường vận hành: Canifa lưu trữ thông tin cá nhân khách hàng trong môi trường vận hành an toàn và chỉ có nhân viên, đại diện có thể truy cập trên cơ sở cần phải biết. Canifa tuân theo các tiêu chuẩn ngành, pháp luật trong việc bảo mật thông tin cá nhân KH.
Trong trường hợp máy chủ lưu trữ thông tin bị hacker tấn công dẫn đến mất mát dữ liệu Thông tin KH, Canifa sẽ có trách nhiệm thông báo vụ việc cho cơ quan chức năng điều tra xử lý kịp thời và thông báo cho KH được biết.
Canifa cam kết bảo mật mọi thông tin giao dịch trực tuyến của KH. Mọi Thông Tin KH, cũng như các thông tin trao đổi giữa KH và Canifa đều được lưu giữ và bảo mật bởi hệ thống của Canifa.
Canifa có các biện pháp thích hợp về kỹ thuật và an ninh để ngăn chặn việc truy cập, sử dụng trái phép Thông Tin KH. Tuy nhiên, Quý khách nên hiểu rằng không có phương thức truyền tải nào qua Internet hoặc phương thức lưu trữ điện tử là an toàn tuyệt đối. Mặc dù việc bảo mật không thể đảm bảo tuyệt đối, nhưng chúng tôi nỗ lực để bảo vệ an toàn thông tin của Quý khách và liên tục rà soát và nâng cấp các biện pháp bảo mật thông tin của chúng tôi.
Quyền của Khách hàng đối với thông tin cá nhân
KH có quyền cung cấp thông tin cá nhân cho chúng tôi và có quyền thay đổi quyết định đó bất kỳ lúc nào.
KH có quyền tự truy cập, kiểm tra, cập nhật, điều chỉnh, xóa hoặc hủy bỏ Thông tin cá nhân của mình bằng cách đăng nhập vào tài khoản trên Website/ Ứng dụng Canifa để chỉnh sửa thông tin cá nhân hoặc yêu cầu Canifa thực hiện việc này. Dữ liệu không được xóa được bao gồm: số điện thoại.
Đối với các nội dung khác ngoài những nội dung quy định tại Chính Sách Bảo Mật Canifa liên quan đến thu thập, xử lý và sử dụng thông tin cá nhân của KH sẽ được thực hiện theo quy định của pháp luật có hiệu lực tại thời điểm tương ứng.
KH được quyền rút lại sự đồng ý của mình, trừ trường hợp luật có quy định khác. Cụ thể:
Việc rút lại sự đồng ý không ảnh hưởng đến tính hợp pháp của việc xử lý dữ liệu đã được đồng ý trước khi rút lại sự đồng ý.
Việc rút lại sự đồng ý phải được thể hiện ở một định dạng có thể được in, sao chép bằng văn bản, bao gồm cả dưới dạng điện tử hoặc định dạng kiểm chứng được.
- KH được yêu cầu hạn chế xử lý dữ liệu cá nhân của mình, trừ trường hợp luật có quy định khác; Việc hạn chế xử lý dữ liệu được thực hiện trong 72 giờ sau khi có yêu cầu của KH dữ liệu, với toàn bộ dữ liệu cá nhân mà chủ thể dữ liệu yêu cầu hạn chế, trừ trường hợp luật có quy định khác.
- KH được quyền yêu cầu Canifa cung cấp cho bản thân dữ liệu cá nhân của mình, trừ trường hợp luật có quy định khác.
KH được phản đối Canifa xử lý dữ liệu cá nhân của mình nhằm ngăn chặn hoặc hạn chế tiết lộ dữ liệu cá nhân hoặc sử dụng cho mục đích quảng cáo, tiếp thị, trừ trường hợp luật có quy định khác. Canifa thực hiện yêu cầu của KH trong 72 giờ sau khi nhận được yêu cầu, trừ trường hợp luật có quy định khác.
KH có quyền khiếu nại, tố cáo hoặc khởi kiện theo quy định của pháp luật. KH có quyền yêu cầu bồi thường thiệt hại theo quy định của pháp luật khi xảy ra vi phạm quy định về bảo vệ dữ liệu cá nhân của mình, trừ trường hợp các bên có thỏa thuận khác hoặc luật có quy định khác. KH có quyền tự bảo vệ theo quy định của Bộ luật Dân sự, luật khác có liên quan và Nghị định 13/2023/NĐ-CP, hoặc yêu cầu cơ quan, tổ chức có thẩm quyền thực hiện các phương thức bảo vệ quyền dân sự.
Trách nhiệm của Khách hàng để đảm bảo bảo mật thông tin
KH có trách nhiệm bảo vệ thông tin tài khoản của mình và không cung cấp bất kỳ thông tin nào liên quan đến tài khoản và mật khẩu truy cập tài khoản Canifa cho bên nào khác. Trường hợp KH tiết lộ thông tin dẫn đến thiệt hại, Canifa sẽ không chịu bất kỳ trách nhiệm nào đối với tất cả những vấn đề phát sinh.
Canifa không chịu trách nhiệm về những vấn đề phát sinh khi KH truy cập vào website, ứng dụng di động, các kênh truyền thông khác không phải là website, ứng dụng di động, kênh truyền thông chính thức của Công ty.
KH tuyệt đối không được có bất kỳ hành vi sử dụng công cụ, chương trình để can thiệp trái phép vào hệ thống của Canifa, cũng như bất kỳ hành vi nào khác nhằm phát tán, cổ vũ cho các hoạt động với mục đích can thiệp, phá hoại hay xâm nhập vào dữ liệu của website hoặc ứng dụng Canifa, cũng như các hành vi mà pháp luật Việt Nam nghiêm cấm. Trong trường hợp Canifa phát hiện KH có hành vi cố tình giả mạo, gian lận, phát tán các thông tin trái phép,…Canifa có toàn quyền chuyển thông tin KH cho các cơ quan có thẩm quyền để xử lý theo quy định pháp luật.
Cách thức liên hệ với Canifa
Bất kỳ khi nào KH cần hỗ trợ, hãy liên hệ với của Canifa:
Hỗ trợ mua Online: 1800.6061 / Email: saleonline@canifa.com
Liên hệ Chăm sóc KH: 18006061/ Email: chamsockhachhang@canifa.com
Văn phòng miền Bắc: +8424-7303.0222/ Email: hello@canifa.com
Văn phòng miền Nam: +8428 3824.7141/ Email: infocanifa@canifa.com
KH có quyền gửi khiếu nại về việc bị lộ thông tin cá nhân cho bên thứ ba đến Ban quản trị của Ứng dụng Canifa hoặc đến địa chỉ công ty hoặc phản ánh qua mail: chamsockhachhang@canifa.com. Khi tiếp nhận những phản hồi này, Công ty sẽ xác nhận lại thông tin, có trách nhiệm trả lời lý do và hướng dẫn KH khôi phục và bảo mật lại thông tin.
Canifa có trách nhiệm thực hiện các biện pháp kỹ thuật, nghiệp vụ để xác minh các nội dung được phản ánh trong vòng 15 ngày.
Đơn vị thu thập và quản lý thông tin
Công ty Cổ phần Canifa
Số ĐKKD: 0107574310, ngày cấp: 23/09/2016, nơi cấp: Sở Kế hoạch và đầu tư Hà Nội
Trụ sở chính: Số 688, Đường Quang Trung, Phường La Khê, Quận Hà Đông, Hà Nội, Việt Nam
Địa chỉ liên hệ: Phòng 301 Tòa nhà GP Invest, 170 La Thành, P. Ô Chợ Dừa, Q. Đống Đa, Hà Nội
Hiệu lực
Chính sách bảo mật thông tin này có hiệu lực từ ngày 01/07/2023.
================================================================================
FILE: data/text/cua-hang-html.txt
================================================================================
search
Cửa hàng
Tài khoản
Giỏ hàng
SẢN PHẨM MỚI
NỮ
NAM
BÉ GÁI
BÉ TRAI
CANIFA S
LICENSE
SCHOOL
ĐỒNG PHỤC
Trang chủ
Hệ thống cửa hàng
110
Hệ thống cửa hàng trên toàn quốc
Canifa hướng đến mục tiêu mang lại niềm vui mặc mới mỗi ngày cho hàng triệu người tiêu dùng Việt. Chúng tôi tin rằng người dân Việt Nam cũng đang hướng đến một cuộc sống năng động, tích cực hơn.
Tìm kiếm cửa hàng
Lựa chọn Tỉnh / Thành Phố
Hà Nội
Bắc Ninh
Hưng Yên
Hồ Chí Minh
Thái Bình
Nghệ An
Hải Phòng
Thanh Hoá
Quảng Ninh
Phú Thọ
Hòa Bình
Sơn La
Quảng Bình
Vĩnh Phúc
Đà Nẵng
Bình Dương
Hải Dương
Bình Phước
Nam Định
Hà Tĩnh
Đồng Nai
Lào Cai
Lâm Đồng
Cao Bằng
Gia Lai
Thái Nguyên
Bà Rịa – Vũng Tàu
Tuyên Quang
Điện Biên
Bắc Giang
Lạng Sơn
Đắk Lắk
Ninh Bình
Hà Nam
Trà Vinh
Lựa chọn Quận / Huyện
Quận Ba Đình
Quận Cầu Giấy
Quận Long Biên
Quận Hai Bà Trưng
Huyện Thanh Trì
Quận Hà Đông
Quận Thanh Xuân
Huyện Thạch Thất
Huyện Đan Phượng
Huyện Thường Tín
Quận Nam Từ Liêm
Huyện Quốc Oai
Thị xã Sơn Tây
Quận Tây Hồ
Quận Hoàng Mai
Huyện Gia Lâm
Huyện Đông Anh
Huyện Hoài Đức
Huyện Chương Mỹ
Huyện Ứng Hòa
Quận Bắc Từ Liêm
Quận Đống Đa
CANIFA Lotte Liễu Giai
F3-A02 & F3-C06 tầng 3, Lotte Center Hanoi, 54 Đường Liễu Giai
09:30 - 22:00
Đang mở
ĐT: (+84) - 024 7300 0166
Chỉ đường
CANIFA - 335 Cầu Giấy
335 Cầu Giấy, P. Quan Hoa
09:00 - 22:30
Đang mở
ĐT: (+84) - 024 7301 5866
Chỉ đường
CANIFA Aeon mall Long Biên
T247, tầng 2 TTTM Aeon Mall Long Biên, P. Long Biên
10:00 - 22:00
Đang mở
ĐT: (+84) - 02473 053 866
Chỉ đường
CANIFA Times City
40-42 đường Tương Lai, tầng B1 TTTM Times city, 458 Minh Khai
09:30 - 22:00
Đang mở
ĐT: (+84) - 024 7305 2866
Chỉ đường
CANIFA - Thanh Trì (Đại lý)
Đường mới, xóm chùa, Ngũ Hiệp
09:00 - 22:00
Đang mở
ĐT: (+84) - 0898 543 536
Chỉ đường
CANIFA 171 Trần Phú
171 Đường Trần Phú
09:00 - 22:30
Đang mở
ĐT: (+84) - 0247 302 1866
Chỉ đường
CANIFA - TTTM Royal city
B2-R6-43-44 TTTM Royal city, 72 Nguyễn Trãi
10:00 - 22:00
Đang mở
ĐT: (+84) - 024 7307 4866
Chỉ đường
CANIFA - Bình Phú, Thạch Thất (Đại lý)
48 Thái Hòa - Bình Phú
09:00 - 22:00
Đang mở
ĐT: (+84) - 0989 758 822
Chỉ đường
CANIFA - 133 Tây Sơn, Đan Phượng (Đại lý)
133 Tây Sơn (cạnh Techcombank), Thị Trấn Phùng
09:00 - 22:00
Đang mở
ĐT: (+84) - 0392 661 983
Chỉ đường
CANIFA - 304 Phố Ga, Thường Tín (Đại lý)
304 Phố Ga - TT Thường Tín
09:00 - 22:00
Đang mở
ĐT: (+84) - 0932 378 856
Chỉ đường
CANIFA - 247 Hồ Tùng Mậu
247 Hồ Tùng Mậu, P. Cầu Diễn
09:00 - 22:30
Đang mở
ĐT: (+84) - 024 7306 4866
Chỉ đường
CANIFA - Quốc Oai (Đại lý)
56 Phố Đồng Hương - Thị Trấn Quốc Oai
09:00 - 22:00
Đang mở
ĐT: (+84) - 0969 547 895
Chỉ đường
CANIFA - 276 Chùa Thông
276 Chùa Thông
09:00 - 22:30
Đang mở
ĐT: (+84) - 024 7304 2688
Chỉ đường
CANIFA - Aeon mall Hà Đông
T211, Tầng 2 TTTM Aeon Mall Hà Đông, P.Dương Nội
10:00 - 22:00
Đang mở
ĐT: (+84) - 024 7303 2688
Chỉ đường
CANIFA - 447 Lạc Long Quân
447 Lạc Long Quân
09:00 - 22:30
Đang mở
ĐT: (+84) - 024 7307 1866
Chỉ đường
CANIFA - 24 Nguyễn Hữu Thọ
24 Nguyễn Hữu Thọ, Hoàng Liệt
09:00 - 22:30
Đang mở
ĐT: (+84) - 0247 3002 866
Chỉ đường
CANIFA - 38 Kim Đồng, Hà Nội
38 Kim Đồng
09:00 - 22:30
Đang mở
ĐT: (+84) - 0247 3005 866
Chỉ đường
Canifa 24 Nguyễn Cơ Thạch
24 Nguyễn Cơ Thạch
09:00 - 22:30
Đang mở
ĐT: (+84) - 024 7304 7866
Chỉ đường
CANIFA - Center Point – 27 Lê Văn Lương
Center Point – 27 Lê Văn Lương, P. Nhân Chính
09:00 - 22:30
Đang mở
ĐT: (+84) - 024 7304 5866
Chỉ đường
CANIFA - Bà Triệu, Hà Nội
69 - 73 Bà Triệu
09:00 - 22:30
Đang mở
ĐT: (+84) - 024 7305 1866
Chỉ đường
CANIFA - 164 Ngô Xuân Quảng, Hà Nội (Đại lý)
164 Ngô Xuân Quảng
09:00 - 22:30
Đang mở
ĐT: (+84) -
Chỉ đường
CANIFA - Đông Anh, Hà Nội
59 Cao Lỗ, Uy Nỗ
09:00 - 22:30
Đang mở
ĐT: (+84) - 024 7301 4866
Chỉ đường
CANIFA - Liên Quan,Thạch Thất (Đại lý)
Tổ dân phố khu Phố, thị trấn Liên Quan
08:00 - 22:00
Đang mở
ĐT: (+84) - 0979 014 638
Chỉ đường
CANIFA - Geleximco An Khánh
LK 14 - Lô 22, KĐT mới Gleximco A, An Khánh
09:00 - 22:30
Đang mở
ĐT: (+84) - 024 73 060 866
Chỉ đường
CANIFA - Chúc Sơn (Đại lý)
86 Bắc Sơn, TT Chúc Sơn, Chương Mỹ, Hà Nội
09:00 - 22:00
Đang mở
ĐT: (+84) - 0934551005
Chỉ đường
CANIFA - Trạm Trôi, Hoài Đức, Hà Nội (Đại lý)
Khu 7, thị trấn Trạm Trôi
08:00 - 22:00
Đang mở
ĐT: (+84) - 0976 966 884
Chỉ đường
CANIFA - Vân Đình, Ứng Hoà, Hà Nội (Đại lý)
288 Lê Lợi, thị trấn Vân Đình
08:00 - 22:00
Đang mở
ĐT: (+84) - 0375131003
Chỉ đường
CANIFA - Bắc Từ Liêm (Đại lý)
32 phố Nhổn
09:00 - 22:00
Đang mở
ĐT: (+84) - 0962 906 789
Chỉ đường
Canifa - Yên Xá, Thanh Trì
Số 68 đường 70, Tân Triều
09:00 - 22:30
Đang mở
ĐT: (+84) - 024 73 067 866
Chỉ đường
CANIFA - 168 Nguyễn Khánh Toàn
168 Nguyễn Khánh Toàn, p. Quan Hoa
09:00 - 22:00
Đang mở
ĐT: (+84) - 024 7304 0866
Chỉ đường
CANIFA - 139 Bạch Mai
139 Bạch Mai, P. Thanh Nhàn
09:00 - 22:30
Đang mở
ĐT: (+84) - 024 7306 1866
Chỉ đường
CANIFA - 152 Cao Lỗ
152 Cao Lỗ, Uy Nỗ
09:00 - 22:30
Đang mở
ĐT: (+84) - 024 7307 3866
Chỉ đường
CANIFA - Ba Đình
Số 6 Điện Biên Phủ, Ba Đình
09:00 - 22:30
Đang mở
ĐT: (+84) - 024 7303 3866
Chỉ đường
CANIFA - Tasco Mall Long Biên
7-9 Nguyễn Văn Linh, Gia Thụy
09:00 - 22:00
Đang mở
ĐT: (+84) - 024 7305 5688
Chỉ đường
CANIFA Lotte Liễu Giai T05
F5-B16-B17 tầng 05, Lotte Center Hanoi, 54 Đường Liễu Giai
09:30 - 22:00
Đang mở
ĐT: (+84) - 024 7300 0166
Chỉ đường
CANIFA KIDS - LITTLE SAM (Đại lý)
314 tầng 3 TTTM The Loop, 241 Xuân Thuỷ, phường Cầu Giấy
10:00 - 22:00
Đang mở
ĐT: (+84) - 0333 424 955
Chỉ đường
CANIFA - Xã Đàn, Hà Nội
65-67 Xã Đàn, Phường Kim Liên, Quận Đống Đa
09:00 - 22:30
Đang mở
ĐT: (+84) - 024 7305 5166
Chỉ đường
CANIFA - 440 Quang Trung Hà Đông
440 Quang Trung
09:00 - 22:30
Đang mở
ĐT: (+84) - 0247 307 5866
Chỉ đường
Canifa - Go! Thăng Long
1S22 TTTM Go! Thăng Long, số 222 đường Trần Duy Hưng, P. Yên Hòa
08:00 - 22:00
Đang mở
ĐT: (+84) - 024 7301 3566
Chỉ đường
CANIFA 181 Giảng Võ
181 Đường Giảng Võ
09:00 - 22:30
Đang mở
ĐT: (+84) - 024 7301 2866
Chỉ đường
CANIFA 121 - 123 Chùa Bộc
121 - 123 Đường Chùa Bộc
09:00 - 22:30
Đang mở
ĐT: (+84) - 024 7302 3688
Chỉ đường
CANIFA 554 Nguyễn Văn Cừ
554 Đường Nguyễn Văn Cừ
09:00 - 22:30
Đang mở
ĐT: (+84) - 0247 305 9866
Chỉ đường
Hệ thống cửa hàng
Hà Nội
Bắc Ninh
Hưng Yên
Hồ Chí Minh
Thái Bình
Nghệ An
Hải Phòng
Thanh Hoá
Quảng Ninh
Phú Thọ
Hòa Bình
Sơn La
Quảng Bình
Vĩnh Phúc
Đà Nẵng
Bình Dương
Hải Dương
Bình Phước
Nam Định
Hà Tĩnh
Đồng Nai
Lào Cai
Lâm Đồng
Cao Bằng
Gia Lai
Thái Nguyên
Bà Rịa – Vũng Tàu
Tuyên Quang
Điện Biên
Bắc Giang
Lạng Sơn
Đắk Lắk
Ninh Bình
Hà Nam
Trà Vinh
CỬA HÀNG NỔI BẬT
CANIFA 554 Nguyễn Văn Cừ
554 Đường Nguyễn Văn Cừ
ĐT: 0247 305 9866
Giờ mở cửa: 09:00 - 22:30
CANIFA 121 - 123 Chùa Bộc
121 - 123 Đường Chùa Bộc
ĐT: 024 7302 3688
Giờ mở cửa: 09:00 - 22:30
CANIFA 181 Giảng Võ
181 Đường Giảng Võ
ĐT: 024 7301 2866
Giờ mở cửa: 09:00 - 22:30
TOP
================================================================================
FILE: data/text/dieu-kien-dieu-khoan-khtt-html.txt
================================================================================
Áp dụng trên hệ thống cửa hàng Canifa toàn quốc cho đến khi có thông báo mới.
CBNV viết tắt của Cán bộ nhân viên
CSKH viết tắt của Chăm sóc Khách hàng
CBLQ viết tắt của Các bên liên quan
TN CSKH viết tắt của Trưởng nhóm Chăm sóc khách hàng
NVCH viết tắt của Nhân viên Cửa hàng
KH viết tắt của Khách hàng
KHTT viết tắt của Khách hàng thân thiết
1. PHẠM VI ÁP DỤNG:
1.1. 1.1. Điều kiện – Điều khoản (“ĐKĐK”) Chính sách Khách hàng của (“Chương trình”) này được áp dụng đối với Khách hàng thành viên (KHTV) và Công ty Cổ phần Canifa
1.2. KHTV là tất cả các cá nhân từ đủ 15 tuổi trở lên đăng ký hợp lệ tham gia Chương Trình, sở hữu tài khoản định danh khách hàng Canifa (“Tài Khoản”) và đã kích hoạt, sở hữu thẻ khách hàng thân thiết (“Thẻ Canifa”), bao gồm thẻ vật lý và/hoặc ứng dụng di động Canifa và/hoặc tài khoản của khách hàng trên website canifa.com (“Trang Web”) do Canifa quản lý vận hành và khai thác hoặc một hình thức khác theo quy định của Chương Trình tùy từng thời điểm.
1.3. Bằng việc tham gia Chương Trình, KHTV thừa nhận đã đọc, hiểu và đồng ý với ĐKĐK này và toàn bộ các chính sách của Chương Trình được công bố công khai trên Trang Web và sẽ được cập nhật tùy từng thời điểm. Nếu KHTV là người chưa đủ 15 tuổi hoặc khó khăn trong nhận thức, làm chủ hành vi hoặc hạn chế năng lực hành vi dân sự theo quy định pháp luật, KHTV cần nhận được sự hỗ trợ hoặc chấp thuận từ cha mẹ, người đại diện theo pháp luật, tùy từng trường hợp áp dụng, để đăng ký tham gia Chương Trình và mở Tài Khoản. Trong trường hợp đó, cha mẹ hoặc người đại diện theo pháp luật khác, tùy từng trường hợp áp dụng, cần hỗ trợ để KHTV hiểu rõ, đồng ý và thay mặt chấp nhận những ĐKĐK của Chương Trình này, chịu trách nhiệm đối với toàn bộ quá trình sử dụng Tài Khoản hoặc các dịch vụ của Canifa.
2. Quyền và trách nhiệm của KHTT, Canifa
2.1. KHTV sẽ được hưởng các quyền lợi cơ bản của chủ Thẻ Canifa /chủ Tài Khoản và các ưu đãi riêng tùy vào phân hạng KHTV theo nội dung Chương Trình được công bố công khai trên Trang Web, bao gồm nhưng không giới hạn các quyền lợi sau: (1) Được thăng hạng và nhận ưu đãi; (2) Tham gia các chương trình khuyến mại/ưu đãi dành riêng cho chủ Thẻ Canifa /chủ Tài Khoản từ Canifa.
2.2. KHTV có quyền truy cập, tra cứu thông tin cá nhân, hạng thành viên, giá trị tích lũy trong Tài Khoản thông qua các phương thức khác nhau như liên hệ nhân viên CSKH/ NV Thu ngân hoặc Trang Web hoặc ứng dụng di động Canifa.
2.3. KHTV tự chịu trách nhiệm cho bất kỳ và tất cả các chi phí, thuế, phí, khiếu nại hoặc nợ phải trả (nếu có) phát sinh từ việc được hưởng các lợi ích từ Chương Trình trong chính sách khách hàng.
2.4. Mỗi KH hiểu, cam kết và bảo đảm là chủ sử dụng duy nhất của Thẻ Canifa /Tài Khoản, có trách nhiệm tự bảo quản Thẻ và bảo mật thông tin Tài Khoản của mình. KH không được cung cấp thông tin Thẻ/Tài Khoản của mình cho bất kỳ bên thứ ba nào và tự chịu trách nhiệm nếu thực hiện không đúng quy định này.
2.5. KH đồng ý cho Canifa khởi tạo, lưu trữ, duy trì, cập nhật và xử lý các dữ liệu thông tin cá nhân do KH cung cấp, cập nhật và những thông tin phát sinh từ việc KH tham gia mở và sử dụng tài khoản (“Dữ Liệu”) và đồng ý cho Canifa sử dụng các Dữ Liệu này cho các mục đích, bao gồm nhưng không giới hạn: Phục vụ KH thực hiện các giao dịch theo quy định của tài khoản, giới thiệu các sản phẩm, dịch vụ mới của Canifa. Tất cả các Dữ Liệu sẽ được bảo vệ và được sử dụng theo quy định tại Chính sách bảo mật và chia sẻ thông tin được công bố công khai trên Trang Web và cập nhật tùy từng thời điểm.
2.6. Trường hợp thông tin cá nhân của KH có thay đổi so với thông tin đã đăng ký, KH có nghĩa vụ cập nhật kịp thời thông tin thay đổi qua Trang Web, Mobile App Canifa, Tổng Đài, cửa hàng Canifa để tránh việc sử dụng Thẻ/Tài khoản trái quy định. Canifa không chịu trách nhiệm đối với các sai sót, mất mát và các thiệt hại phát sinh từ hoặc liên quan đến KH do thông tin không được cập nhật kịp thời theo quy định tại ĐKĐK này.
2.7. Mọi thông báo/khiếu nại của KH đến Canifa phải được thực hiện thông qua Tổng Đài, email của Phòng Phát triển khách hàng, hệ thống cửa hàng của Canifa hoặc các hình thức khác theo quy định của Canifa tùy từng thời điểm.
2.8. Canifa có quyền đề nghị KH cung cấp thông tin và/hoặc xuất trình giấy tờ cá nhân hợp lệ ngay khi Canifa phát hiện KH thực hiện các hành vi quy định tại điều II.11 hoặc được nhận các ưu đãi trong một số trường hợp.
2.9. Canifa được miễn trách nhiệm trong trường hợp: (1) Hệ thống xử lý dữ liệu, phần mềm, hệ thống truyền tín hiệu gặp sự cố, bị lỗi, bị tấn công hoặc vì bất kỳ lý do khách quan nào khác nằm ngoài khả năng kiểm soát của Canifa; (2) Khi Thẻ bị lợi dụng trong trường hợp Thẻ bị mất cắp/thất lạc/lộ thông tin Tài Khoản mà chủ thẻ không kịp thời thông báo cho Canifa.
2.10. Canifa không chịu trách nhiệm trong trường hợp Khách hàng làm mất điện thoại, mất sim điện thoại dẫn đến thông tin tài khoản bị lộ mà chủ tài khoản không thông báo mất tài khoản tới Canifa.
2.11. Canifa được quyền từ chối cung cấp, hủy, truy đòi hoặc thu hồi những ưu đãi, điểm trong Tài Khoản đã hoặc sẽ nhận được theo các chương trình ưu đãi, khuyến mại mà không cần thông báo trước với KH trong các trường hợp: (1) Canifa không thể chuyển điểm/ ưu đãi cho khách hàng do các nguyên nhân khách quan nằm ngoài sự kiểm soát của Canifa; (2) Điểm được ghi có không chính xác, không hợp lệ vào Tài Khoản theo quy định; (3) KH có dấu hiệu sử dụng các công cụ hoặc phương thức gian lận nhằm trục lợi; (4) KH vi phạm các quy định của Chương trình/Chính sách ưu đãi được Canifa thông báo tại từng thời điểm; (5) Trong trường hợp bất khả kháng theo quy định pháp luật.
2.12. Canifa không chịu trách nhiệm cho bất kỳ tổn thất hoặc thiệt hại, dù trực tiếp hay gián tiếp của KH liên quan đến các chương trình ưu đãi khác, bao gồm nhưng không giới hạn trường hợp bất khả kháng theo quy định của pháp luật.
3. Chấm dứt quyền KHTT
Quyền KHTV theo Chương Trình có giá trị vô thời hạn trừ khi bị chấm dứt trong những trường hợp sau:
3.1. KHTV gửi yêu cầu chấm dứt cho Canifa qua địa chỉ liên lạc/email chính thức được đăng tải tại Trang Web.
3.2. KHTV cố ý sử dụng Thẻ/Tài Khoản sai quy định của Chương Trình và vi phạm ĐKĐK này.
3.3. KHTV qua đời hoặc các trường hợp chấm dứt khác theo quy định của pháp luật.
3.4. Khi Chương Trình chấm dứt vì bất cứ lý do gì.
3.5. Chấm dứt trong các trường hợp khác theo quy định của Chương Trình và ĐKĐK này.
3.6. Canifa bảo lưu quyền chấm dứt Chương Trình bằng cách thông báo cho tất cả KHTV trước 30 ngày thông qua Trang Web và/hoặc một phương thức phù hợp khác. Phương án giải quyết đối với các quyền lợi của KHTV của Chương Trình sẽ được công bố cùng thông báo chấm dứt Chương Trình.
4. Điều khoản chung
4.1. Tùy thuộc điều kiện thực tế, ĐKĐK của Chương Trình và các chính sách khác được công bố trên Trang Web có thể được sửa đổi/điều chỉnh/chấm dứt theo quyết định riêng của Canifa và sẽ được thông báo cho KHTV trên Trang Web.
4.2. Giải quyết tranh chấp: ĐKĐK này được điều chỉnh bởi pháp luật Việt Nam. Tất cả các tranh chấp phát sinh từ hoặc liên quan đến ĐKĐK này sẽ được các bên cố gắng giải quyết thông qua thương lượng. Trường hợp thương lượng đàm phán không thành công trong vòng 30 (ba mươi) ngày, các Bên sẽ có quyền đưa tranh chấp ra giải quyết tại tòa án có thẩm quyền tại Việt Nam theo quy định của pháp luật.
KHÁM PHÁ TIỆN ÍCH
CHỈ CÓ TRÊN APP
Ưu đãi độc quyền
cho thành viên
Nhận thông báo
sản phẩm mới và
khuyến mãi
Mua sắm tiện lợi
qua video call
với C-live
Quét mã vạch
kiểm tra hàng
tại cửa hàng
QUÉT MÃ
TẢI APP
HỆ THỐNG CỬA HÀNG
Tìm kiếm cửa hàng gần bạn!
Xem danh sách
ĐĂNG KÝ NHẬN BẢN TIN
Cập nhật những thông tin mới nhất về ưu đãi, thời trang và phong cách sống.
Đăng ký ngay
CÔNG TY CỔ PHẦN CANIFA
Số ĐKKD: 0107574310, ngày cấp: 23/09/2016
Nơi cấp: Sở Kế hoạch và đầu tư Hà Nội.
Trụ sở chính: số 688 Đường Quang Trung, P. Hà Đông, TP. Hà Nội.
Địa chỉ: Phòng 301, tầng 3, tòa nhà GP Invest, số 170 La Thành, P. Ô Chợ Dừa, TP. Hà Nội.
Điện thoại: 024 - 7303.0222
Fax: 024 - 6277.6419
Email: hello@canifa.com
================================================================================
FILE: data/text/gioi-thieu-html.txt
================================================================================
Canifa 20 năm - Khoác lên niềm vui gia đình Việt
Năm 1997, Công ty Cổ phần Thương mại và Dịch vụ Hoàng Dương được thành lập với mục đích chính ban đầu là hoạt động trong lĩnh vực sản xuất hàng thời trang xuất khẩu với các sản phẩm chủ yếu làm từ len và sợi.
Năm 2001 thương hiệu thời trang CANIFA ra đời, tự hào trở thành một cột mốc đáng nhớ của doanh nghiệp Việt trong ngành thời trang.
Tầm nhìn và sứ mệnh
Mang đến niềm vui cho hàng triệu gia đình Việt
Canifa hướng đến mục tiêu mang lại niềm vui mặc mới mỗi ngày cho hàng triệu người tiêu dùng Việt. Chúng tôi tin rằng người dân Việt Nam cũng đang hướng đến một cuộc sống năng động, tích cực hơn.
Giá trị cốt lõi của Canifa
20 năm phát triển - Chúng tôi luôn tuân thủ những giá trị cốt lõi của mình.
Kinh doanh dựa trên giá trị thật:
CANIFA thiết lập hệ thống tiêu chuẩn chất lượng quốc tế áp dụng trên tất cả quy trình quản lý và kiểm soát chất lượng từ khâu chọn lọc nguyên phụ liệu cho đến khâu thiết kế và sản xuất (Oeko-tex, Cotton USA, Woolmark,...).
Canifa cam kết phát triển xanh cùng người Việt bằng suy nghĩ và hành động:
Vận hành xanh: Tổ hợp CANIFA Văn Giang tự hào là một đơn vị tiên phong nhận chứng chỉ quốc tế LEED về tiết kiệm năng lượng và ảnh hưởng tích cực đến môi trường sống.
Đối tác xanh: Canifa chọn Cotton USA - đơn vị cung cấp nguyên liệu chính cho sản phẩm tại CANIFA, luôn nghiêm minh tuân thủ các chỉ số bền vững của nông nghiệp Mỹ: tiết kiệm nước, kỹ thuật “không làm đất” để bảo vệ đất trồng.
Sản phẩm xanh: CANIFA đặc biệt chú trọng nghiên cứu, kiểm định chất lượng với nguyên liệu đầu vào và sản phẩm đầu ra, đáp ứng những yêu cầu khắt khe nhất của các chứng chỉ uy tín nhất thế giới (Oeko Tex, Woolmark, WD…)
CANIFA - LỊCH SỬ HÌNH THÀNH
1997 là năm Ngày 23/8/1997 Thành lập công ty TNHH Hoàng Dương
2001 là năm Ra đời thương hiệu thời trang CANIFA.
2002 là năm Xây dựng nhà máy Hoàng Dương Hưng Yên
2004 là năm Chương trình Hơi ấm mùa đông lần đầu tiên được khởi xướng bởi một nhóm các nhân viên, mở đầu cho các chương trình thiện nguyện sau này.
2012 là năm Ra đời cửa hàng bán lẻ “flagship store” đầu tiên của Canifa tại 181 Giảng Võ, Hà Nội. Chính thức chuyển đối từ mô hình kinh doanh đại lý sang chuỗi bán lẻ hiện đại. Thành lập chi nhánh Canifa HCM.
2014 là năm Canifa là thương hiệu thời trang đầu tiên ở Việt Nam nhận được chứng chỉ Woolmark - Tổ chức uy tín nhất thế giới về phát triển và kiểm soát chất lượng len lông cừu
2016 là năm ONOFF sáp nhập vào Canifa sau 10 năm hoạt động.
2019 là năm Tổ hợp xanh Canifa Văn Giang đi vào hoạt động, nhận được chứng chỉ LEED, tiêu chuẩn quốc tế về kiến trúc xanh.
================================================================================
FILE: data/text/hoi-dap.txt
================================================================================
Thanh toán
Thanh toán trả trước
Áp dụng thanh toán bằng QR Code, Thẻ ATM (thẻ ngân hàng, thẻ thanh toán nội địa) và Thẻ thanh toán quốc tế (Visa, Master, JCB, Amex…) qua cổng thanh toán VNPAY.
Quý khách thanh toán chọn hình thức thanh toán qua VNPAY và tiền hành thanh toán tại cổng thanh toán Vnpay sau khi chọn “Đặt hàng”
Thanh toán qua QR code
Bước 1: Chọn quét mã VNPAY tại cổng thanh toán VNPAY
Bước 2: Quét mã thanh toán tại App Ngân hàng đang sử dụng và tiến hành thanh toán
Thanh toán qua Thẻ ATM/Thẻ Thanh toán quốc tế
Bước 1: Chọn Thanh toán Thẻ ATM/Thẻ Thanh toán quốc tế tại cổng thanh toán VNPAY
Bước 2: Chọn Ngân hàng/Loại thẻ thanh toán
Bước 3: Nhập thông tin thẻ/tài khoản ngân hàng
Bước 4: Xác thực OTP
Đơn hàng thanh toán thành công khi website thông báo mã đơn hàng của bạn.
Thanh toán trả sau (COD)
Là hình thức khách hàng thanh toán tiền mặt trực tiếp cho bưu tá khi nhận hàng.
Khi hàng được chuyển giao đến bạn có thể mở gói hàng kiểm tra sản phẩm trước khi thanh toán (tuy nhiên CANIFA không hỗ trợ thử sản phẩm). Nếu sản phẩm có bất kỳ lỗi hay khiếm khuyết nào không đúng ý muốn, bạn có thể trả lại nhân viên vận chuyển ngay tại thời điểm đó.
Hoàn tiền
CANIFA sẽ hoàn tiền cho bạn trong những trường hợp sau:
Bạn đã thanh toán trước và muốn hủy đơn hàng khi CANIFA chưa vận chuyển.
Bạn muốn trả lại hàng do lỗi sản xuất và không muốn đổi sang sản phẩm khác.
CANIFA sẽ hoàn tiền lại vào tài khoản cá nhân của bạn. Chúng tôi sẽ cố gắng hoàn tiền nhanh nhất có thể và thời gian hoàn tiền không quá 15 ngày tính từ khi xác nhận hoàn tiền.
VẬN CHUYỂN
Cước phí vận chuyển
CANIFA áp dụng chính sách miễn phí giao hàng cho tất cả các đơn hàng có giá trị từ 599.000 VNĐ trở lên, áp dụng trên toàn bộ các tỉnh thành trên toàn quốc.
Đối với các đơn hàng có giá trị dưới 599.000 VNĐ, CANIFA áp dụng phí vận chuyển theo từng khu vực như sau. Biểu phí này được áp dụng từ ngày 14/08/2023 cho đến khi có thông báo thay đổi mới.
Tại khu vực Hà Nội, các đơn hàng giao đến các quận Đống Đa, Hoàn Kiếm, Ba Đình, Hai Bà Trưng, Cầu Giấy và Thanh Xuân sẽ được áp dụng phí vận chuyển 20.000 VNĐ.
Đối với các quận và huyện còn lại của Hà Nội bao gồm Hà Đông, Tây Hồ, Hoàng Mai, Long Biên, Bắc Từ Liêm, Nam Từ Liêm, Ba Vì, Chương Mỹ, Đan Phượng, Đông Anh, Gia Lâm, Hoài Đức, Mê Linh, Mỹ Đức, Phúc Thọ, Phú Xuyên, Quốc Oai, Sóc Sơn, Thạch Thất, Thanh Oai, Thanh Trì, Thường Tín, Ứng Hòa và Thị xã Sơn Tây, phí vận chuyển là 30.000 VNĐ.
Tại TP. Hồ Chí Minh, tất cả các đơn hàng giao đến mọi quận, huyện đều được áp dụng phí vận chuyển 40.000 VNĐ.
Tại Đà Nẵng, tất cả các đơn hàng giao đến mọi quận, huyện đều được áp dụng phí vận chuyển 40.000 VNĐ.
Đối với các tỉnh thành khác trên toàn quốc, CANIFA chia làm hai mức phí vận chuyển.
Mức 30.000 VNĐ được áp dụng cho các tỉnh thành bao gồm: Bắc Giang, Bắc Ninh, Hà Nam, Hải Dương, Hải Phòng, Hưng Yên, Hòa Bình, Nam Định, Phú Thọ, Thái Nguyên, Vĩnh Phúc, Bắc Kạn, Lạng Sơn, Nghệ An, Ninh Bình, Quảng Ninh, Thái Bình, Thanh Hóa, Tuyên Quang và Yên Bái.
Mức 40.000 VNĐ được áp dụng cho các tỉnh thành còn lại, bao gồm: Điện Biên, Lào Cai, Hà Giang, Sơn La, Cao Bằng, Thừa Thiên Huế, Quảng Trị, Gia Lai, Đắk Lắk, Kon Tum, Đắk Nông, Phú Yên, Khánh Hòa, Hà Tĩnh, Tiền Giang, Bến Tre, Tây Ninh, Đồng Tháp, Trà Vinh, Vĩnh Long, Đồng Nai, Bình Dương, Bà Rịa – Vũng Tàu, Long An, Quảng Bình, Bình Định (Quy Nhơn), Bình Thuận, Ninh Thuận, Bình Phước, Cần Thơ, Hậu Giang, Kiên Giang, An Giang, Sóc Trăng, Bạc Liêu, Cà Mau và Quảng Ngãi.
Thời gian vận chuyển
Đối với khu vực Hà Nội, thời gian giao hàng dự kiến từ 1 đến 3 ngày kể từ khi hệ thống xác nhận đơn hàng qua tin nhắn SMS.
Đối với tuyến Đà Nẵng và TP. Hồ Chí Minh, thời gian giao hàng dự kiến trong vòng 3 ngày kể từ khi hệ thống xác nhận qua SMS.
Đối với các tỉnh thành khác, thời gian giao hàng dự kiến từ 3 đến 7 ngày kể từ khi hệ thống xác nhận đơn hàng.
Thời gian giao hàng không bao gồm thứ Bảy, Chủ nhật và các ngày Lễ, Tết theo quy định.
Số lần giao hàng
Mỗi đơn hàng được giao tối đa 3 lần. Trong trường hợp lần giao đầu tiên không thành công, nhân viên vận chuyển sẽ liên hệ lại và thực hiện giao lần tiếp theo sau 1–2 ngày làm việc. Nếu sau 3 lần giao hàng vẫn không thành công, đơn hàng sẽ được tự động hủy.
Kiểm tra tình trạng đơn hàng
Để kiểm tra thông tin hoặc tình trạng đơn hàng, khách hàng vui lòng sử dụng Mã đơn hàng đã được gửi trong email xác nhận hoặc tin nhắn SMS, và liên hệ đến bộ phận Chăm sóc khách hàng qua tổng đài miễn phí 1800 6061 (nhánh 1) để được hỗ trợ.
Kiểm tra sản phẩm khi nhận hàng
Khi nhận đơn hàng, khách hàng hoàn toàn có thể mở gói hàng để kiểm tra sản phẩm trước khi thanh toán hoặc trước khi nhân viên vận chuyển rời đi.
Trong trường hợp phát sinh bất kỳ vấn đề nào liên quan đến đơn hàng, khách hàng vui lòng liên hệ ngay với CANIFA qua 1800 6061 (nhánh 1) để được hỗ trợ kịp thời.
Đổi hàng
Quy định đổi hàng online
1.3.1 Quy định chung
- Áp dụng 01 lần/ 01 hóa đơn trong vòng 30 ngày kể từ ngày mua hàng/ nhận hàng.
- Tại tất cả các cửa hàng thuộc hệ thống Canifa trên toàn quốc & hệ thống online (Web/App Canifa).
Đối với hàng Nguyên giá/ Giá tốt:
- Đổi sang sản phẩm nguyên giá khác.
- Nếu khách hàng chưa tìm được sản phẩm phù hợp để đổi, Canifa sẽ nhận lại sản phẩm đó và hỗ trợ khách hàng đổi sang giấy “Biên nhận” có giá trị tương đương với sản phẩm đã mua.
- Hàng nguyên giá được giảm theo quyền lợi hạng Gold/ Diamond vẫn được đổi như mua hàng nguyên giá;
- Sản phẩm khi mua là hàng nguyên giá, tại thời điểm đổi là hàng giảm giá thì áp dụng chính sách như đổi sản phẩm nguyên giá.
- Sản phẩm khi mua là hàng giảm giá, tại thời điểm đổi là hàng nguyên giá thì quy cả 2 sản phẩm về nguyên giá để đổi.
Đối với hàng Giảm giá/ Khuyến mại:
- Đổi sang màu hoặc size khác trên cùng 1 mã sản phẩm hoặc đổi hàng theo quy chế riêng của từng Chương trình khuyến mại. Trong trường hợp sản phẩm đổi không còn trên hệ thống, cửa hàng hỗ trợ khách hàng đổi sang giấy “Biên nhận” có giá trị tương đương với sản phẩm đã mua.
1.3.2 Điều kiện đổi hàng:
- Chính sách chỉ áp dụng khi khách hàng còn giữ hóa đơn mua hàng, sản phẩm còn nguyên nhãn mác, thẻ bài đính kèm và còn mới, không bị dơ bẩn, hư hỏng bởi những tác nhân bên ngoài, thay đổi kết cấu sản phẩm.
- Sản phẩm đồ lót và phụ kiện không được đổi.
- Đối với đơn mua tại cửa hàng: khách hàng cần xuất trình Đơn mua hàng gốc (và hóa đơn giá trị gia tăng gốc nếu có).
- Đối với đơn mua online: khách hàng cần gửi lại sản phẩm về kho online kèm với phiếu mua hàng đã nhận.
- Khách hàng thanh toán phần tiền chênh lệch nếu sản phẩm đổi có giá trị cao hơn sản phẩm đã mua.
- Khách hàng không được hoàn lại tiền chênh lệch nếu sản phẩm đổi có giá trị thấp hơn sản phẩm đã mua.
1.3.3 Quy trình đổi hàng Online (Web/App).
- Quy trình hỗ trợ đổi hàng:
+ Sau khi Canifa ghi nhận thông tin đơn hàng đổi, KH gửi hàng về theo địa chỉ Canifa cung cấp (KH chịu phí gửi trả hàng).
+ Ngay sau khi nhận được hàng đổi trả, Canifa sẽ gửi lại hàng mới cho KH theo địa chỉ KH đã cung cấp (Canifa hỗ trợ phí chuyển hàng mới).
- Quy trình đổi hàng lỗi (TH khách hàng đồng ý đổi):
+ Sau khi tiếp nhận yêu cầu đổi hàng, Canifa báo đơn vị vận chuyển qua lấy hàng chuyển về kho online để đổi.
+ Kho online nhận được hàng đổi, chuyển lại hàng đổi cho KH.
Lưu ý: Canifa chịu phí cả 2 chiều.
- Quy trình đổi hàng lỗi (TH khách hàng không đồng ý đổi):
+ Canifa hỗ trợ hoàn lại số tiền tương ứng với giá trị sản phầm lỗi.
+ Thời gian khách hàng được hoàn tiền trong vòng 10-15 ngày kể từ ngày tiếp nhận sản phẩm lỗi.
1.3.4 Lưu ý khác:
- Giấy “Biên nhận” có hiệu lực trong vòng 15 ngày kể từ ngày cấp và có giá trị tương đương tiền mặt khi mua sắm tại Canifa.
- Đối với các đơn hàng online, chỉ áp dụng đổi sang giấy “Biên nhận” khi khách hàng ra cửa hàng đổi và chỉ sử dụng giấy “Biên nhận” mua sắm tại cửa hàng.
- Giấy “Biên nhận” bắt buộc có thông tin người mua hàng: tên và số điện thoại.
Quy định đổi sản phẩm mua tại cửa hàng
1.3.1 Quy định chung
- Áp dụng 01 lần/ 01 hóa đơn trong vòng 30 ngày kể từ ngày mua hàng/ nhận hàng.
- Tại tất cả các cửa hàng thuộc hệ thống Canifa trên toàn quốc.
Đối với hàng Nguyên giá/ Giá tốt:
- Đổi sang sản phẩm nguyên giá khác.
- Nếu khách hàng chưa tìm được sản phẩm phù hợp để đổi, Canifa sẽ nhận lại sản phẩm đó và hỗ trợ khách hàng đổi sang giấy “Biên nhận” có giá trị tương đương với sản phẩm đã mua.
- Hàng nguyên giá được giảm theo quyền lợi hạng Gold/ Diamond vẫn được đổi như mua hàng nguyên giá;
- Sản phẩm khi mua là hàng nguyên giá, tại thời điểm đổi là hàng giảm giá thì áp dụng chính sách như đổi sản phẩm nguyên giá.
- Sản phẩm khi mua là hàng giảm giá, tại thời điểm đổi là hàng nguyên giá thì quy cả 2 sản phẩm về nguyên giá để đổi.
Đối với hàng Giảm giá/ Khuyến mại:
- Đổi sang màu hoặc size khác trên cùng 1 mã sản phẩm hoặc đổi hàng theo quy chế riêng của từng Chương trình khuyến mại. Trong trường hợp sản phẩm đổi không còn trên hệ thống, cửa hàng hỗ trợ khách hàng đổi sang giấy “Biên nhận” có giá trị tương đương với sản phẩm đã mua.
1.3.2 Điều kiện đổi hàng:
- Chính sách chỉ áp dụng khi khách hàng còn giữ hóa đơn mua hàng, sản phẩm còn nguyên nhãn mác, thẻ bài đính kèm và còn mới, không bị dơ bẩn, hư hỏng bởi những tác nhân bên ngoài, thay đổi kết cấu sản phẩm.
- Sản phẩm đồ lót và phụ kiện không được đổi.
- Đối với đơn mua tại cửa hàng: khách hàng cần xuất trình Đơn mua hàng gốc (và hóa đơn giá trị gia tăng gốc nếu có).
- Đối với đơn mua online: khách hàng cần gửi lại sản phẩm về kho online kèm với phiếu mua hàng đã nhận.
- Khách hàng thanh toán phần tiền chênh lệch nếu sản phẩm đổi có giá trị cao hơn sản phẩm đã mua.
- Khách hàng không được hoàn lại tiền chênh lệch nếu sản phẩm đổi có giá trị thấp hơn sản phẩm đã mua.
- Quy trình đổi hàng lỗi (TH khách hàng đồng ý đổi):
+ Sau khi tiếp nhận yêu cầu đổi hàng, Canifa báo đơn vị vận chuyển qua lấy hàng chuyển về kho online để đổi.
+ Kho online nhận được hàng đổi, chuyển lại hàng đổi cho KH.
Lưu ý: Canifa chịu phí cả 2 chiều.
- Quy trình đổi hàng lỗi (TH khách hàng không đồng ý đổi):
+ Canifa hỗ trợ hoàn lại số tiền tương ứng với giá trị sản phầm lỗi.
+ Thời gian khách hàng được hoàn tiền trong vòng 10-15 ngày kể từ ngày tiếp nhận sản phẩm lỗi.
1.3.4 Lưu ý khác:
- Giấy “Biên nhận” có hiệu lực trong vòng 15 ngày kể từ ngày cấp và có giá trị tương đương tiền mặt khi mua sắm tại Canifa.
- Đối với các đơn hàng online, chỉ áp dụng đổi sang giấy “Biên nhận” khi khách hàng ra cửa hàng đổi và chỉ sử dụng giấy “Biên nhận” mua sắm tại cửa hàng.
- Giấy “Biên nhận” bắt buộc có thông tin người mua hàng: tên và số điện thoại.
CÂU HỎI THƯỜNG GẶP
Cước phí vận chuyển được tính như thế nào?
CANIFA áp dụng miễn phí giao hàng cho tất cả các đơn hàng có giá trị từ 599.000 VNĐ trở lên, áp dụng trên toàn bộ các tỉnh thành trên cả nước.
Đối với các đơn hàng có giá trị dưới 599.000 VNĐ, CANIFA áp dụng biểu phí vận chuyển theo từng khu vực, có hiệu lực từ ngày 14/08/2023 cho đến khi có thông báo thay đổi mới.
Tại khu vực Hà Nội, các đơn hàng giao đến các quận Đống Đa, Hoàn Kiếm, Ba Đình, Hai Bà Trưng, Cầu Giấy và Thanh Xuân được áp dụng phí vận chuyển 20.000 VNĐ.
Các quận và huyện còn lại của Hà Nội bao gồm Hà Đông, Tây Hồ, Hoàng Mai, Long Biên, Bắc Từ Liêm, Nam Từ Liêm, Ba Vì, Chương Mỹ, Đan Phượng, Đông Anh, Gia Lâm, Hoài Đức, Mê Linh, Mỹ Đức, Phúc Thọ, Phú Xuyên, Quốc Oai, Sóc Sơn, Thạch Thất, Thanh Oai, Thanh Trì, Thường Tín, Ứng Hòa và Thị xã Sơn Tây được áp dụng phí vận chuyển 30.000 VNĐ.
Tại TP. Hồ Chí Minh, tất cả các đơn hàng giao đến mọi quận, huyện đều được áp dụng phí vận chuyển 40.000 VNĐ.
Tại Đà Nẵng, tất cả các đơn hàng giao đến mọi quận, huyện đều được áp dụng phí vận chuyển 40.000 VNĐ.
Đối với các tỉnh thành khác, CANIFA áp dụng hai mức phí.
Mức 30.000 VNĐ áp dụng cho các tỉnh thành gồm: Bắc Giang, Bắc Ninh, Hà Nam, Hải Dương, Hải Phòng, Hưng Yên, Hòa Bình, Nam Định, Phú Thọ, Thái Nguyên, Vĩnh Phúc, Bắc Kạn, Lạng Sơn, Nghệ An, Ninh Bình, Quảng Ninh, Thái Bình, Thanh Hóa, Tuyên Quang và Yên Bái.
Mức 40.000 VNĐ áp dụng cho các tỉnh thành còn lại như: Điện Biên, Lào Cai, Hà Giang, Sơn La, Cao Bằng, Thừa Thiên Huế, Quảng Trị, Gia Lai, Đắk Lắk, Kon Tum, Đắk Nông, Phú Yên, Khánh Hòa, Hà Tĩnh, Tiền Giang, Bến Tre, Tây Ninh, Đồng Tháp, Trà Vinh, Vĩnh Long, Đồng Nai, Bình Dương, Bà Rịa – Vũng Tàu, Long An, Quảng Bình, Bình Định (Quy Nhơn), Bình Thuận, Ninh Thuận, Bình Phước, Cần Thơ, Hậu Giang, Kiên Giang, An Giang, Sóc Trăng, Bạc Liêu, Cà Mau và Quảng Ngãi.
CANIFA hỗ trợ những hình thức thanh toán nào?
CANIFA hiện hỗ trợ thanh toán trả trước thông qua cổng thanh toán VNPAY, bao gồm các hình thức:
thanh toán bằng QR Code, Thẻ ATM (thẻ ngân hàng nội địa) và Thẻ thanh toán quốc tế như Visa, MasterCard, JCB, Amex.
Sau khi hoàn tất bước “Đặt hàng”, quý khách lựa chọn hình thức thanh toán qua VNPAY và tiến hành thanh toán trực tiếp tại cổng thanh toán VNPAY.
Hướng dẫn thanh toán bằng QR Code
Bước 1: Tại cổng thanh toán VNPAY, chọn hình thức Quét mã VNPAY.
Bước 2: Sử dụng ứng dụng ngân hàng đang dùng để quét mã QR và thực hiện thanh toán theo hướng dẫn.
Hướng dẫn thanh toán bằng Thẻ ATM hoặc Thẻ thanh toán quốc tế
Bước 1: Tại cổng thanh toán VNPAY, chọn Thanh toán bằng Thẻ ATM/Thẻ thanh toán quốc tế.
Bước 2: Chọn ngân hàng hoặc loại thẻ tương ứng.
Bước 3: Nhập thông tin thẻ hoặc tài khoản ngân hàng theo yêu cầu.
Bước 4: Thực hiện xác thực OTP để hoàn tất thanh toán.
Đơn hàng được xem là thanh toán thành công khi website hiển thị mã đơn hàng của quý khách.
CANIFA có hỗ trợ đổi hàng online không?
CANIFA hỗ trợ đổi hàng online theo các quy định sau:
Quy định chung
Mỗi hóa đơn chỉ được áp dụng 01 lần đổi hàng, trong vòng 30 ngày kể từ ngày mua hàng hoặc nhận hàng.
Chính sách đổi hàng áp dụng tại tất cả các cửa hàng CANIFA trên toàn quốc và trên hệ thống online (Website/App CANIFA).
Quy định đổi hàng đối với sản phẩm nguyên giá / giá tốt
Khách hàng được đổi sang sản phẩm nguyên giá khác.
Trong trường hợp khách hàng chưa chọn được sản phẩm phù hợp để đổi, CANIFA sẽ nhận lại sản phẩm và hỗ trợ đổi sang giấy “Biên nhận” có giá trị tương đương sản phẩm đã mua.
Các sản phẩm nguyên giá được giảm theo quyền lợi hạng Gold/Diamond vẫn được áp dụng chính sách đổi như hàng nguyên giá.
Nếu sản phẩm khi mua là hàng nguyên giá nhưng tại thời điểm đổi đã trở thành hàng giảm giá, CANIFA vẫn áp dụng chính sách đổi như hàng nguyên giá.
Nếu sản phẩm khi mua là hàng giảm giá nhưng tại thời điểm đổi là hàng nguyên giá, CANIFA sẽ quy cả hai sản phẩm về nguyên giá để thực hiện đổi hàng.
Quy định đổi hàng đối với sản phẩm giảm giá / khuyến mại
Khách hàng được đổi sang màu sắc hoặc size khác trên cùng một mã sản phẩm, hoặc đổi hàng theo quy định riêng của từng chương trình khuyến mại.
Trong trường hợp sản phẩm đổi không còn trên hệ thống, cửa hàng sẽ hỗ trợ đổi sang giấy “Biên nhận” có giá trị tương đương sản phẩm đã mua.
Điều kiện đổi hàng là gì?
Chính sách đổi hàng chỉ áp dụng khi khách hàng còn giữ hóa đơn mua hàng, sản phẩm còn nguyên nhãn mác, thẻ bài, còn mới, không bị dơ bẩn, hư hỏng do tác nhân bên ngoài hoặc thay đổi kết cấu sản phẩm.
Các sản phẩm đồ lót và phụ kiện không áp dụng đổi hàng.
Đối với đơn mua tại cửa hàng, khách hàng cần xuất trình đơn mua hàng gốc (và hóa đơn giá trị gia tăng gốc nếu có).
Đối với đơn mua online, khách hàng cần gửi sản phẩm về kho online kèm theo phiếu mua hàng đã nhận.
Khách hàng cần thanh toán phần chênh lệch nếu sản phẩm đổi có giá trị cao hơn sản phẩm đã mua.
CANIFA không hoàn lại tiền chênh lệch nếu sản phẩm đổi có giá trị thấp hơn sản phẩm đã mua.
Quy trình đổi hàng online (Website/App) như thế nào?
Sau khi CANIFA ghi nhận yêu cầu đổi hàng, khách hàng gửi sản phẩm về địa chỉ CANIFA cung cấp và chịu phí gửi trả hàng.
Ngay khi CANIFA nhận được sản phẩm đổi, CANIFA sẽ gửi sản phẩm mới về địa chỉ khách hàng đã cung cấp và hỗ trợ phí vận chuyển chiều gửi hàng mới.
Quy trình đổi hàng lỗi được xử lý ra sao?
Trong trường hợp khách hàng đồng ý đổi hàng lỗi, CANIFA sẽ liên hệ đơn vị vận chuyển đến lấy hàng chuyển về kho online. Sau khi kho online nhận được sản phẩm lỗi, CANIFA sẽ gửi lại sản phẩm đổi cho khách hàng. Trong trường hợp này, CANIFA chịu toàn bộ chi phí hai chiều.
Trong trường hợp khách hàng không đồng ý đổi hàng lỗi, CANIFA sẽ hỗ trợ hoàn lại số tiền tương ứng với giá trị sản phẩm lỗi. Thời gian hoàn tiền dự kiến trong vòng 10–15 ngày kể từ ngày CANIFA tiếp nhận sản phẩm lỗi.
Các lưu ý khác khi đổi hàng
Giấy “Biên nhận” có hiệu lực trong vòng 15 ngày kể từ ngày cấp và có giá trị tương đương tiền mặt khi mua sắm tại CANIFA.
Đối với các đơn hàng online, chỉ áp dụng đổi sang giấy “Biên nhận” khi khách hàng đổi hàng trực tiếp tại cửa hàng, và giấy “Biên nhận” chỉ được sử dụng để mua sắm tại cửa hàng CANIFA.
Giấy “Biên nhận” bắt buộc phải có thông tin người mua hàng, bao gồm tên và số điện thoại
================================================================================
FILE: data/text/huong-dan-chon-size-html.txt
================================================================================
HƯỚNG DẪN CHỌN SIZE - CANIFA
* Đơn vị tính: cm, kg
================================================================================
BẢNG SIZE CHUNG CHO NỮ
================================================================================
Size XS - BẢNG SIZE CHUNG CHO NỮ:
- Chiều cao: 147-153 cm
- Cân nặng: 38-43 kg
- Vòng ngực: 74-80 cm
- Vòng mông: 82-88 cm
Size S - BẢNG SIZE CHUNG CHO NỮ:
- Chiều cao: 150-155 cm
- Cân nặng: 41-46 kg
- Vòng ngực: 79-82 cm
- Vòng mông: 88-90 cm
Size M - BẢNG SIZE CHUNG CHO NỮ:
- Chiều cao: 155-163 cm
- Cân nặng: 47-52 kg
- Vòng ngực: 82-87 cm
- Vòng mông: 90-94 cm
Size L - BẢNG SIZE CHUNG CHO NỮ:
- Chiều cao: 160-165 cm
- Cân nặng: 53-58 kg
- Vòng ngực: 88-94 cm
- Vòng mông: 94-98 cm
Size XL - BẢNG SIZE CHUNG CHO NỮ:
- Chiều cao: 162-166 cm
- Cân nặng: 59-64 kg
- Vòng ngực: 94-99 cm
- Vòng mông: 98-102 cm
================================================================================
QUẦN NỮ (Size số)
================================================================================
Size 26 (XS) - QUẦN NỮ (Size số):
- Vòng eo: 65 cm
- Vòng mông (dáng slim): 79 cm
- Vòng mông (dáng regular): 86.92 cm
- Chiều dài quần: 93 cm
- Rộng gấu (dáng slim): 12.5 cm
- Rộng gấu (dáng regular): Tùy mẫu kích thước khác nhau
Size 27 (S) - QUẦN NỮ (Size số):
- Vòng eo: 67.5 cm
- Vòng mông (dáng slim): 81.5 cm
- Vòng mông (dáng regular): 89.46 cm
- Chiều dài quần: 94 cm
- Rộng gấu (dáng slim): 13 cm
- Rộng gấu (dáng regular): Tùy mẫu kích thước khác nhau
Size 28 (M) - QUẦN NỮ (Size số):
- Vòng eo: 70 cm
- Vòng mông (dáng slim): 84 cm
- Vòng mông (dáng regular): 92 cm
- Chiều dài quần: 95 cm
- Rộng gấu (dáng slim): 13.5 cm
- Rộng gấu (dáng regular): Tùy mẫu kích thước khác nhau
Size 29 (L) - QUẦN NỮ (Size số):
- Vòng eo: 72.5 cm
- Vòng mông (dáng slim): 86.5 cm
- Vòng mông (dáng regular): 94.5 cm
- Chiều dài quần: 96 cm
- Rộng gấu (dáng slim): 14 cm
- Rộng gấu (dáng regular): Tùy mẫu kích thước khác nhau
Size 30 (XL) - QUẦN NỮ (Size số):
- Vòng eo: 75 cm
- Vòng mông (dáng slim): 89 cm
- Vòng mông (dáng regular): 97.1 cm
- Chiều dài quần: 97 cm
- Rộng gấu (dáng slim): 14.5 cm
- Rộng gấu (dáng regular): Tùy mẫu kích thước khác nhau
================================================================================
QUẦN JEANS - KHAKI (NỮ)
================================================================================
Size 26 (XS) - QUẦN JEANS - KHAKI (NỮ):
- Vòng bụng: 65 cm
- Vòng mông: 79 cm
- Chiều dài quần: 93 cm
- Rộng ống (dáng slim): 12.5 cm
- Rộng gấu (dáng regular): Tùy mẫu kích thước khác nhau
Size 27 (S) - QUẦN JEANS - KHAKI (NỮ):
- Vòng bụng: 67.5 cm
- Vòng mông: 81.5 cm
- Chiều dài quần: 94 cm
- Rộng ống (dáng slim): 13 cm
- Rộng gấu (dáng regular): Tùy mẫu kích thước khác nhau
Size 28 (M) - QUẦN JEANS - KHAKI (NỮ):
- Vòng bụng: 70 cm
- Vòng mông: 84 cm
- Chiều dài quần: 95 cm
- Rộng ống (dáng slim): 13.5 cm
- Rộng gấu (dáng regular): Tùy mẫu kích thước khác nhau
Size 29 (L) - QUẦN JEANS - KHAKI (NỮ):
- Vòng bụng: 72.5 cm
- Vòng mông: 86.5 cm
- Chiều dài quần: 96 cm
- Rộng ống (dáng slim): 14 cm
- Rộng gấu (dáng regular): Tùy mẫu kích thước khác nhau
Size 30 (XL) - QUẦN JEANS - KHAKI (NỮ):
- Vòng bụng: 75 cm
- Vòng mông: 89 cm
- Chiều dài quần: 97 cm
- Rộng ống (dáng slim): 14.5 cm
- Rộng gấu (dáng regular): Tùy mẫu kích thước khác nhau
================================================================================
BẢNG SIZE CHUNG CHO NAM
================================================================================
Size S - BẢNG SIZE CHUNG CHO NAM:
- Chiều cao: 162-168 cm
- Cân nặng: 57-62 kg
- Vòng ngực: 84-88 cm
- Vòng mông: 85-89 cm
Size M - BẢNG SIZE CHUNG CHO NAM:
- Chiều cao: 169-173 cm
- Cân nặng: 63-67 kg
- Vòng ngực: 88-94 cm
- Vòng mông: 90-94 cm
Size L - BẢNG SIZE CHUNG CHO NAM:
- Chiều cao: 171-175 cm
- Cân nặng: 68-72 kg
- Vòng ngực: 94-98 cm
- Vòng mông: 95-99 cm
Size XL - BẢNG SIZE CHUNG CHO NAM:
- Chiều cao: 173-177 cm
- Cân nặng: 73-77 kg
- Vòng ngực: 98-104 cm
- Vòng mông: 100-104 cm
Size XXL - BẢNG SIZE CHUNG CHO NAM:
- Chiều cao: 175-179 cm
- Cân nặng: 78-82 kg
- Vòng ngực: 104-107 cm
- Vòng mông: 104-108 cm
================================================================================
QUẦN JEANS - KHAKI (NAM)
================================================================================
Size 29 (S) - QUẦN JEANS - KHAKI (NAM):
- Vòng eo: 79.5 cm
- Vòng mông: 96.5 cm
- Chiều dài quần: 99.8 cm
- Rộng ống (dáng slim): 15.4 cm
- Rộng gấu (dáng regular): Tùy mẫu kích thước khác nhau
Size 30 (M) - QUẦN JEANS - KHAKI (NAM):
- Vòng eo: 82 cm
- Vòng mông: 99 cm
- Chiều dài quần: 100.5 cm
- Rộng ống (dáng slim): 16 cm
- Rộng gấu (dáng regular): Tùy mẫu kích thước khác nhau
Size 31 (L) - QUẦN JEANS - KHAKI (NAM):
- Vòng eo: 84.5 cm
- Vòng mông: 101.5 cm
- Chiều dài quần: 101.2 cm
- Rộng ống (dáng slim): 16.6 cm
- Rộng gấu (dáng regular): Tùy mẫu kích thước khác nhau
Size 32 (XL) - QUẦN JEANS - KHAKI (NAM):
- Vòng eo: 87 cm
- Vòng mông: 104 cm
- Chiều dài quần: 101.2 cm
- Rộng ống (dáng slim): 17.2 cm
- Rộng gấu (dáng regular): Tùy mẫu kích thước khác nhau
Size 33 (XXL) - QUẦN JEANS - KHAKI (NAM):
- Vòng eo: 89 cm
- Vòng mông: 106.5 cm
- Chiều dài quần: 101.2 cm
- Rộng ống (dáng slim): 17.8 cm
- Rộng gấu (dáng regular): Tùy mẫu kích thước khác nhau
================================================================================
BẢNG SIZE CHUNG TRẺ EM (sản phẩm từ Thu Đông 2023)
================================================================================
Size 92 (2Y) - BẢNG SIZE CHUNG TRẺ EM (sản phẩm từ Thu Đông 2023):
- Chiều cao: 88-94 cm
- Cân nặng: 10-13 kg
Size 98 (2-3Y) - BẢNG SIZE CHUNG TRẺ EM (sản phẩm từ Thu Đông 2023):
- Chiều cao: 95-101 cm
- Cân nặng: 13-15 kg
Size 104 (3-4Y) - BẢNG SIZE CHUNG TRẺ EM (sản phẩm từ Thu Đông 2023):
- Chiều cao: 101-107 cm
- Cân nặng: 15-18 kg
Size 110 (4-5Y) - BẢNG SIZE CHUNG TRẺ EM (sản phẩm từ Thu Đông 2023):
- Chiều cao: 107-113 cm
- Cân nặng: 18-22 kg
Size 116 (6Y) - BẢNG SIZE CHUNG TRẺ EM (sản phẩm từ Thu Đông 2023):
- Chiều cao: 113-119 cm
- Cân nặng: 22-25 kg
Size 122 (7Y) - BẢNG SIZE CHUNG TRẺ EM (sản phẩm từ Thu Đông 2023):
- Chiều cao: 119-125 cm
- Cân nặng: 25-28 kg
Size 128 (8Y) - BẢNG SIZE CHUNG TRẺ EM (sản phẩm từ Thu Đông 2023):
- Chiều cao: 125-131 cm
- Cân nặng: 28-32 kg
Size 134 (9Y) - BẢNG SIZE CHUNG TRẺ EM (sản phẩm từ Thu Đông 2023):
- Chiều cao: 131-137 cm
- Cân nặng: 32-36 kg
Size 140 (10-11Y) - BẢNG SIZE CHUNG TRẺ EM (sản phẩm từ Thu Đông 2023):
- Chiều cao: 137-145 cm
- Cân nặng: 36-39 kg
Size 152 (11-12Y) - BẢNG SIZE CHUNG TRẺ EM (sản phẩm từ Thu Đông 2023):
- Chiều cao: 145-157 cm
- Cân nặng: 39-46 kg
Size 164 (13-14Y) - BẢNG SIZE CHUNG TRẺ EM (sản phẩm từ Thu Đông 2023):
- Chiều cao: 157-169 cm
- Cân nặng: 46-55 kg
================================================================================
BẢNG SIZE CHUNG CHO BÉ TRAI
================================================================================
Size 90 (2Y) - BẢNG SIZE CHUNG CHO BÉ TRAI:
- Chiều cao: 90 cm
- Cân nặng: 10-13 kg
Size 100 (3-4Y) - BẢNG SIZE CHUNG CHO BÉ TRAI:
- Chiều cao: 100 cm
- Cân nặng: 14-17 kg
Size 110 (4-5Y) - BẢNG SIZE CHUNG CHO BÉ TRAI:
- Chiều cao: 110 cm
- Cân nặng: 18-23 kg
Size 120 (6-7Y) - BẢNG SIZE CHUNG CHO BÉ TRAI:
- Chiều cao: 120 cm
- Cân nặng: 24-29 kg
Size 130 (8Y) - BẢNG SIZE CHUNG CHO BÉ TRAI:
- Chiều cao: 130 cm
- Cân nặng: 29-33 kg
Size 140 (9-11Y) - BẢNG SIZE CHUNG CHO BÉ TRAI:
- Chiều cao: 140 cm
- Cân nặng: 33-39 kg
Size 150 (11-12Y) - BẢNG SIZE CHUNG CHO BÉ TRAI:
- Chiều cao: 150 cm
- Cân nặng: 39-45 kg
Size 160 (13-14Y) - BẢNG SIZE CHUNG CHO BÉ TRAI:
- Chiều cao: 160 cm
- Cân nặng: 45-52 kg
================================================================================
BẢNG SIZE CHUNG CHO BÉ GÁI
================================================================================
Size 90 (2Y) - BẢNG SIZE CHUNG CHO BÉ GÁI:
- Chiều cao: 90 cm
- Cân nặng: 10-13 kg
Size 100 (3-4Y) - BẢNG SIZE CHUNG CHO BÉ GÁI:
- Chiều cao: 100 cm
- Cân nặng: 14-17 kg
Size 110 (4-5Y) - BẢNG SIZE CHUNG CHO BÉ GÁI:
- Chiều cao: 110 cm
- Cân nặng: 18-23 kg
Size 120 (6-7Y) - BẢNG SIZE CHUNG CHO BÉ GÁI:
- Chiều cao: 120 cm
- Cân nặng: 24-29 kg
Size 130 (8Y) - BẢNG SIZE CHUNG CHO BÉ GÁI:
- Chiều cao: 130 cm
- Cân nặng: 29-33 kg
Size 140 (9-11Y) - BẢNG SIZE CHUNG CHO BÉ GÁI:
- Chiều cao: 140 cm
- Cân nặng: 33-39 kg
Size 150 (11-12Y) - BẢNG SIZE CHUNG CHO BÉ GÁI:
- Chiều cao: 150 cm
- Cân nặng: 39-45 kg
Size 160 (13-14Y) - BẢNG SIZE CHUNG CHO BÉ GÁI:
- Chiều cao: 160 cm
- Cân nặng: 45-50 kg
================================================================================
BẢNG SIZE CHUNG CHO UNISEX - NGƯỜI LỚN
================================================================================
Size XXS - BẢNG SIZE CHUNG CHO UNISEX - NGƯỜI LỚN:
- Chiều cao: 155 - 163 cm
- Cân nặng: 47 - 52 kg
Size XS - BẢNG SIZE CHUNG CHO UNISEX - NGƯỜI LỚN:
- Chiều cao: 160 - 165 cm
- Cân nặng: 53 - 58 kg
Size S - BẢNG SIZE CHUNG CHO UNISEX - NGƯỜI LỚN:
- Chiều cao: 162 - 168 cm
- Cân nặng: 57 - 62 kg
Size M - BẢNG SIZE CHUNG CHO UNISEX - NGƯỜI LỚN:
- Chiều cao: 169 - 173 cm
- Cân nặng: 63 - 67 kg
Size L - BẢNG SIZE CHUNG CHO UNISEX - NGƯỜI LỚN:
- Chiều cao: 171 - 175 cm
- Cân nặng: 68 - 72 kg
Size XL - BẢNG SIZE CHUNG CHO UNISEX - NGƯỜI LỚN:
- Chiều cao: 173 - 177 cm
- Cân nặng: 73 - 77 kg
Size XXL - BẢNG SIZE CHUNG CHO UNISEX - NGƯỜI LỚN:
- Chiều cao: 175 - 179 cm
- Cân nặng: 79 - 82 kg
================================================================================
BẢNG SIZE CHUNG CHO UNISEX - TRẺ EM
================================================================================
Dải size lẻ:
Size 92 (2Y) - BẢNG SIZE CHUNG CHO UNISEX - TRẺ EM - (Dải size lẻ):
- Chiều cao: 92 cm
- Cân nặng: 10 - 13 kg
Size 98 (2-3Y) - BẢNG SIZE CHUNG CHO UNISEX - TRẺ EM - (Dải size lẻ):
- Chiều cao: 98 cm
- Cân nặng: 13 - 15 kg
Size 104 (3-4Y) - BẢNG SIZE CHUNG CHO UNISEX - TRẺ EM - (Dải size lẻ):
- Chiều cao: 104 cm
- Cân nặng: 15 - 18 kg
Size 110 (4-5Y) - BẢNG SIZE CHUNG CHO UNISEX - TRẺ EM - (Dải size lẻ):
- Chiều cao: 110 cm
- Cân nặng: 18 - 22 kg
Size 116 (6Y) - BẢNG SIZE CHUNG CHO UNISEX - TRẺ EM - (Dải size lẻ):
- Chiều cao: 116 cm
- Cân nặng: 22 - 25 kg
Size 122 (7Y) - BẢNG SIZE CHUNG CHO UNISEX - TRẺ EM - (Dải size lẻ):
- Chiều cao: 122 cm
- Cân nặng: 25 - 28 kg
Size 128 (8Y) - BẢNG SIZE CHUNG CHO UNISEX - TRẺ EM - (Dải size lẻ):
- Chiều cao: 128 cm
- Cân nặng: 28 - 32 kg
Size 134 (9Y) - BẢNG SIZE CHUNG CHO UNISEX - TRẺ EM - (Dải size lẻ):
- Chiều cao: 134 cm
- Cân nặng: 32 - 36 kg
Size 140 (9-10Y) - BẢNG SIZE CHUNG CHO UNISEX - TRẺ EM - (Dải size lẻ):
- Chiều cao: 140 cm
- Cân nặng: 36 - 39 kg
Size 152 (11-12Y) - BẢNG SIZE CHUNG CHO UNISEX - TRẺ EM - (Dải size lẻ):
- Chiều cao: 152 cm
- Cân nặng: 39 - 46 kg
Size 164 (13-14Y) - BẢNG SIZE CHUNG CHO UNISEX - TRẺ EM - (Dải size lẻ):
- Chiều cao: 164 cm
- Cân nặng: 46 - 55 kg
Dải size chẵn:
Size 90 (2Y) - BẢNG SIZE CHUNG CHO UNISEX - TRẺ EM - (Dải size chẵn):
- Chiều cao: 90 cm
- Cân nặng: 10 - 13 kg
Size 100 (2-3Y) - BẢNG SIZE CHUNG CHO UNISEX - TRẺ EM - (Dải size chẵn):
- Chiều cao: 100 cm
- Cân nặng: 14 - 17 kg
Size 110 (4-5Y) - BẢNG SIZE CHUNG CHO UNISEX - TRẺ EM - (Dải size chẵn):
- Chiều cao: 110 cm
- Cân nặng: 18 - 23 kg
Size 120 (6-7Y) - BẢNG SIZE CHUNG CHO UNISEX - TRẺ EM - (Dải size chẵn):
- Chiều cao: 120 cm
- Cân nặng: 24 - 29 kg
Size 130 (8Y) - BẢNG SIZE CHUNG CHO UNISEX - TRẺ EM - (Dải size chẵn):
- Chiều cao: 130 cm
- Cân nặng: 29 - 33 kg
Size 140 (9-10Y) - BẢNG SIZE CHUNG CHO UNISEX - TRẺ EM - (Dải size chẵn):
- Chiều cao: 140 cm
- Cân nặng: 33 - 39 kg
Size 150 (11-12Y) - BẢNG SIZE CHUNG CHO UNISEX - TRẺ EM - (Dải size chẵn):
- Chiều cao: 150 cm
- Cân nặng: 39 - 45 kg
Size 160 (13-14Y) - BẢNG SIZE CHUNG CHO UNISEX - TRẺ EM - (Dải size chẵn):
- Chiều cao: 160 cm
- Cân nặng: 45 - 52 kg
================================================================================
FILE: data/text/lien-he-html.txt
================================================================================
Hỗ trợ Khách hàng mua online
Tổng đài: 1800 6061
Từ 9h - 12h, 13h - 21h các ngày từ thứ 2 đến Chủ nhật
Email: saleonline@canifa.com
Địa chỉ: Phòng 301 Tòa nhà GP Invest, 170 La Thành, P. Ô Chợ Dừa, Q. Đống Đa, Hà Nội
Chăm sóc khách hàng:
Điện thoại: 1800.6061
Email: chamsockhachhang@canifa.com
Đặt đồng phục
Ms. Hoa
Điện thoại: 0963.03.03.67
Email: grb2b@canifa.com
Văn phòng miền Bắc
Phòng 301 Tòa nhà GP Invest, 170 La Thành, P. Ô Chợ Dừa, Q. Đống Đa, Hà Nội
Điện thoại: +8424-7303.0222
Fax: +8424 - 6277.6419
Email: hello@canifa.com
Website: www.canifa.com
Văn phòng miền Nam
Địa chỉ: 97 Cao Thắng , Phường 3, Quận 3, TP. HCM
Điện thoại: +8428 3824.7141
Email: infocanifa@canifa.com
Liên hệ làm Nhà phân phối và Đại lý
(KV miền Bắc)
Mr. Nguyễn Đức Bằng
Điện thoại: 0904.530.833
Email: bangnd@canifa.com
Liên hệ làm Nhà phân phối và Đại lý
(KV Miền Nam từ Đà Nẵng đến Cà Mau)
Mr. Nguyễn Công Thành
Điện thoại: 0914.396.239
Email: thanhnc@canifa.com
Nhà máy
Đường Nguyễn Văn Linh, Phường Bần Yên Nhân, T.X Mỹ Hào, Hưng Yên
Điện thoại: +84-221- 394 2234
Fax: +84 - 221-394 2235
 
================================================================================
FILE: data/text/voi-cong-dong-html.txt
================================================================================
Phát triển bền vững: 03 xanh
Với mục tiêu phát triển bền vững, CANIFA tập trung nghiên cứu và cải tiến hoạt động sản xuất kinh doanh để đảm bảo nguyên tắc 3 XANH
Bảo vệ môi trường là 01 trong những nguyên tắc hoạt động cốt lõi của Tập đoàn. Tiêu biểu: Tổ hợp Canifa Văn Giang - đơn vị tiên phong trong ngành dệt may tại Việt Nam nhận chứng chỉ quốc tế LEED về công trình xây dựng xanh, tiết kiệm năng lượng và bảo vệ môi trường sống của con người
Hợp tác bền vững là yếu tố quan trọng nhất trong sự đồng hành cùng phát triển
Tiêu biểu: Đối tác Cotton USA - đơn vị cung cấp nguyên liệu chính cho áo phông tại Canifa luôn nghiêm minh tuân thủ các chỉ số bền vững của nông nghiệp Mỹ: tiết kiệm nước, kỹ thuật “không làm đất” để bảo vệ đất trồng.
Thời trang không chỉ đẹp mà còn an toàn với người sử dụng luôn song hành trong mục tiêu nghiên cứu phát triển sản phẩm.
Tiêu biểu: CANIFA luôn chú trọng nghiên cứu, kiểm định chất lượng với nguyên liệu đầu vào và sản phẩm đầu ra, đáp ứng những yêu cầu khắt khe nhất của các chứng chỉ uy tín nhất thế giới để mang đến cho người dùng sự an toàn và thoải mái (Oekotex, Woolmark, AWTA,...)
hoạt động cộng đồng
Với mong muốn mang đến những điều tốt đẹp dù nhỏ bé nhưng ấm áp đến cho cộng đồng Việt, những người sáng lập đã gieo mầm cho các hoạt động hoàn toàn tự nguyện mang tên Hội CANIFA Vì Cộng Đồng.
HƠI ẤM MÙA ĐÔNG
Nếu một lần được thấy hình ảnh những em nhỏ vùng cao vượt qua mùa đông chỉ bằng những tấm áo cộc... chúng tôi tin rằng những trái tim sẽ không ngừng ước mong về một ngày các em đủ ấm.
Được khởi xướng bởi nhân viên Canifa, hành trình Hơi ấm mùa đông là hành trình của hàng ngàn tấm áo ấm đến với các em nhỏ vùng cao cho mùa đông bớt khắc nghiệt.
2019 - năm thứ 15, Canifa sẽ viết tiếp hành trình bằng 15,000 tấm áo, cùng với cộng đồng để trao tận tay từng em nhỏ
GIỌT MÁU HỒNG
Từ 2014, Hoạt động hiến máu nhân đạo được Canifa tổ chức định kỳ hàng năm, thu hút đông đảo CBNV tham gia, đóng góp hàng ngàn đơn vị máu cho hoạt động cứu trợ của Viện huyết học và truyền máu Trung ương.
ÁO MỚI CHO HÀNH TINH XANH
Ở Canifa, chúng tôi tin rằng, mỗi đứa trẻ đều có thể chung tay bảo vệ môi trường. Từ việc bớt sử dụng một chiếc ống hút nhựa cho đến trồng thêm một cây xanh. Mỗi năm, hàng trăm mầm xanh sẽ được chính tay các bạn nhỏ là con em CBNV trong công ty vun trồng. Năm sau đó, các bạn sẽ được quay lại để nhìn những cây xanh đã lớn và tiếp tục gieo thêm những màu xanh.
HOẠT ĐỘNG THIỆN NGUYỆN KHÁC
Chứng kiến sự khắc nghiệt của thiên tai, chúng tôi hiểu mọi sự cứu trợ dù là nhỏ nhất nếu kịp thời đều có ý nghĩa với những người dân vùng thiệt hại. Canifa luôn có những chuyến xe hỏa tốc về vùng bão lũ, chúng tôi gọi đó là những chuyến xe màu đỏ.
CHỨNG NHẬN VÀ CHẤT LƯỢNG
Với triết lý kinh doanh trên giá trị thật, CANIFA thiết lập hệ thống tiêu chuẩn chất lượng quốc tế áp dụng trên tất cả quy trình quản lý và kiểm soát chất lượng từ khâu chọn lọc nguyên phụ liệu cho đến khâu thiết kế và sản xuất.
Luôn cam kết mang đến khách hàng những sản phẩm an toàn với sức khỏe người tiêu dùng, hãng thời trang Canifa chỉ sử dụng những chất liệu tự nhiên an toàn trong mỗi thiết kế.
Sản phẩm Canifa đạt tiêu chuẩn Oeko Tex 100 được kiểm định về độ an toàn cho làn da của người sử dụng.
Và năm 2016, CANIFA tự hào đã đạt được chứng chỉ an toàn cấp độ cao nhất với dòng sản phẩm dành cho trẻ sơ sinh.
Cotton USA - hãng bông được cả thế giới tin dùng với quy trình canh tác bền vững để cho ra đời những sợi bông chất lượng tuyệt vời đưa đến sản phẩm thấm hút tốt, mát mẻ, chịu nhiệt và bền bỉ.
Với mong muốn mang đến sản phẩm chất lượng cho người Việt, CANIFA liên kết cùng tập đoàn Cotton USA mang đến nguyên liệu chất lượng Quốc tế cho người Việt.
Luôn quan tâm tới nhu cầu mặc đẹp, mặc mới và mặc an toàn mỗi ngày của khách hàng, ngoài an toàn về chất lượng sản phẩm.
CANIFA đã vượt qua quy trình nghiêm ngặt khắt khe về kiểm định sản xuất, nguyên vật liệu đầu vào và thành phẩm ra thị trường của tập đoàn quốc tế Disney, từ đó, ký kết chính thức là đối tác chiến lược giữa CANIFA và Disney tại Việt Nam.
Khi cầm trên tay những sản phẩm có biểu tượng Woolmark, bạn có thể yên tâm 100% về chất lượng hảo hạng của món đồ bạn sở hữu đạt chuẩn toàn cầu.
Chứng chỉ Quốc tế này được cấp cho các nhà sản xuất có đủ khả năng đáp ứng các tiêu chuẩn yêu cầu vô cùng nghiêm ngặt.
Từ 2014, CANIFA là nhãn hiệu Việt Nam đầu tiên vượt qua các tiêu chuẩn khắt khe của Woolmark để nhận được 02 chứng chỉ Quốc tế danh giá nhất dành cho sản phẩm len lông cừu là Woolmark và Woolmark Blend.
AWTA Ltd (Australian Wool Testing Authority) - Trung tâm kiểm soát chất lượng len lông cừu lớn nhất thế giới đã chính thức cấp chứng chỉ cho sản phẩm của CANIFA.
Những sản phẩm len lông cừu đạt chuẩn phải được kiểm duyệt từ nguyên liệu đầu vào.
CANIFA đầu tư xây dựng Tổ hợp XANH CANIFA theo tiêu chuẩn LEED.
Đây là tiêu chuẩn tiên phong cho các công trình tiết kiệm năng lượng và bảo vệ môi trường trường sống của con người.
Tuân theo tiêu chuẩn này, việc vận hành sản xuất của nhà máy được vận hành theo quy trình khép kín, xử lý nước thải thông minh để tái sử dụng tưới cây xanh cho Tổ hợp.
Chính việc nhận được Chứng chỉ LEED mang giá trị toàn cầu đã giúp Tổ Hợp XANH CANIFA không chỉ hoàn thành sứ mệnh Phản Ứng Nhanh, để phục vụ khách hàng tốt hơn, mà còn góp phần quan trọng trong việc phát triển xã hội bền vững, văn minh, bảo vệ môi trường sống của cộng đồng.
TOP
VẬN CHUYỂN
Cước phí vận chuyển
CANIFA áp dụng chính sách miễn phí giao hàng cho tất cả các đơn hàng có giá trị từ 599.000 VNĐ trở lên, áp dụng trên toàn bộ các tỉnh thành trên toàn quốc.
Đối với các đơn hàng có giá trị dưới 599.000 VNĐ, CANIFA áp dụng phí vận chuyển theo từng khu vực như sau. Biểu phí này được áp dụng từ ngày 14/08/2023 cho đến khi có thông báo thay đổi mới.
Tại khu vực Hà Nội, các đơn hàng giao đến các quận Đống Đa, Hoàn Kiếm, Ba Đình, Hai Bà Trưng, Cầu Giấy và Thanh Xuân sẽ được áp dụng phí vận chuyển 20.000 VNĐ.
Đối với các quận và huyện còn lại của Hà Nội bao gồm Hà Đông, Tây Hồ, Hoàng Mai, Long Biên, Bắc Từ Liêm, Nam Từ Liêm, Ba Vì, Chương Mỹ, Đan Phượng, Đông Anh, Gia Lâm, Hoài Đức, Mê Linh, Mỹ Đức, Phúc Thọ, Phú Xuyên, Quốc Oai, Sóc Sơn, Thạch Thất, Thanh Oai, Thanh Trì, Thường Tín, Ứng Hòa và Thị xã Sơn Tây, phí vận chuyển là 30.000 VNĐ.
Tại TP. Hồ Chí Minh, tất cả các đơn hàng giao đến mọi quận, huyện đều được áp dụng phí vận chuyển 40.000 VNĐ.
Tại Đà Nẵng, tất cả các đơn hàng giao đến mọi quận, huyện đều được áp dụng phí vận chuyển 40.000 VNĐ.
Đối với các tỉnh thành khác trên toàn quốc, CANIFA chia làm hai mức phí vận chuyển.
Mức 30.000 VNĐ được áp dụng cho các tỉnh thành bao gồm: Bắc Giang, Bắc Ninh, Hà Nam, Hải Dương, Hải Phòng, Hưng Yên, Hòa Bình, Nam Định, Phú Thọ, Thái Nguyên, Vĩnh Phúc, Bắc Kạn, Lạng Sơn, Nghệ An, Ninh Bình, Quảng Ninh, Thái Bình, Thanh Hóa, Tuyên Quang và Yên Bái.
Mức 40.000 VNĐ được áp dụng cho các tỉnh thành còn lại, bao gồm: Điện Biên, Lào Cai, Hà Giang, Sơn La, Cao Bằng, Thừa Thiên Huế, Quảng Trị, Gia Lai, Đắk Lắk, Kon Tum, Đắk Nông, Phú Yên, Khánh Hòa, Hà Tĩnh, Tiền Giang, Bến Tre, Tây Ninh, Đồng Tháp, Trà Vinh, Vĩnh Long, Đồng Nai, Bình Dương, Bà Rịa – Vũng Tàu, Long An, Quảng Bình, Bình Định (Quy Nhơn), Bình Thuận, Ninh Thuận, Bình Phước, Cần Thơ, Hậu Giang, Kiên Giang, An Giang, Sóc Trăng, Bạc Liêu, Cà Mau và Quảng Ngãi.
Thời gian vận chuyển
Đối với khu vực Hà Nội, thời gian giao hàng dự kiến từ 1 đến 3 ngày kể từ khi hệ thống xác nhận đơn hàng qua tin nhắn SMS.
Đối với tuyến Đà Nẵng và TP. Hồ Chí Minh, thời gian giao hàng dự kiến trong vòng 3 ngày kể từ khi hệ thống xác nhận qua SMS.
Đối với các tỉnh thành khác, thời gian giao hàng dự kiến từ 3 đến 7 ngày kể từ khi hệ thống xác nhận đơn hàng.
Thời gian giao hàng không bao gồm thứ Bảy, Chủ nhật và các ngày Lễ, Tết theo quy định.
Số lần giao hàng
Mỗi đơn hàng được giao tối đa 3 lần. Trong trường hợp lần giao đầu tiên không thành công, nhân viên vận chuyển sẽ liên hệ lại và thực hiện giao lần tiếp theo sau 1–2 ngày làm việc. Nếu sau 3 lần giao hàng vẫn không thành công, đơn hàng sẽ được tự động hủy.
Kiểm tra tình trạng đơn hàng
Để kiểm tra thông tin hoặc tình trạng đơn hàng, khách hàng vui lòng sử dụng Mã đơn hàng đã được gửi trong email xác nhận hoặc tin nhắn SMS, và liên hệ đến bộ phận Chăm sóc khách hàng qua tổng đài miễn phí 1800 6061 (nhánh 1) để được hỗ trợ.
Kiểm tra sản phẩm khi nhận hàng
Khi nhận đơn hàng, khách hàng hoàn toàn có thể mở gói hàng để kiểm tra sản phẩm trước khi thanh toán hoặc trước khi nhân viên vận chuyển rời đi.
Trong trường hợp phát sinh bất kỳ vấn đề nào liên quan đến đơn hàng, khách hàng vui lòng liên hệ ngay với CANIFA qua 1800 6061 (nhánh 1) để được hỗ trợ kịp thời.
\ No newline at end of file
[('Chân váy', 'di_lam', 'outerwear'), ('Chân váy', 'di_lam', 'outerwear'), ('Găng tay chống nắng', 'di_lam', 'outerwear'), ('Khăn', 'di_lam', 'outerwear'), ('Khẩu trang', 'di_lam', 'outerwear'), ('Mũ', 'di_lam', 'outerwear'), ('Quần Body', 'di_lam', 'outerwear'), ('Quần Khaki', 'di_lam', 'outerwear'), ('Quần Khaki', 'di_lam', 'outerwear'), ('Quần Khaki', 'di_lam', 'outerwear'), ('Quần dài', 'di_lam', 'outerwear'), ('Quần dài', 'di_lam', 'outerwear'), ('Quần dài', 'di_lam', 'outerwear'), ('Quần dài', 'di_lam', 'outerwear'), ('Quần dài', 'di_lam', 'outerwear'), ('Quần dài', 'di_lam', 'outerwear'), ('Quần dài', 'di_lam', 'outerwear'), ('Quần giữ nhiệt', 'di_lam', 'outerwear'), ('Quần giữ nhiệt', 'di_lam', 'outerwear'), ('Quần jean', 'di_lam', 'outerwear'), ('Quần jean', 'di_lam', 'outerwear'), ('Quần jean', 'di_lam', 'outerwear'), ('Quần jean', 'di_lam', 'outerwear'), ('Quần jean', 'di_lam', 'outerwear'), ('Quần jean', 'di_lam', 'outerwear'), ('Quần leggings', 'di_lam', 'outerwear'), ('Quần nỉ', 'di_lam', 'outerwear'), ('Quần nỉ', 'di_lam', 'outerwear'), ('Quần nỉ', 'di_lam', 'outerwear'), ('Quần nỉ', 'di_lam', 'outerwear'), ('Quần nỉ', 'di_lam', 'outerwear'), ('Túi xách', 'di_lam', 'outerwear'), ('Túi xách', 'di_lam', 'outerwear'), ('Váy liền', 'di_lam', 'outerwear'), ('Váy liền', 'di_lam', 'outerwear'), ('Váy liền', 'di_lam', 'outerwear'), ('Váy liền', 'di_lam', 'outerwear'), ('Váy liền', 'di_lam', 'outerwear'), ('Áo Body', 'di_lam', 'outerwear'), ('Áo Body', 'di_lam', 'outerwear'), ('Áo Body', 'di_lam', 'outerwear'), ('Áo Body', 'di_lam', 'outerwear'), ('Áo Body', 'di_lam', 'outerwear'), ('Áo Sơ mi', 'di_lam', 'outerwear'), ('Áo Sơ mi', 'di_lam', 'outerwear'), ('Áo Sơ mi', 'di_lam', 'outerwear'), ('Áo Sơ mi', 'di_lam', 'outerwear'), ('Áo Sơ mi', 'di_lam', 'outerwear'), ('Áo Sơ mi', 'di_lam', 'outerwear'), ('Áo Sơ mi', 'di_lam', 'outerwear'), ('Áo Sơ mi', 'di_lam', 'outerwear'), ('Áo ba lỗ', 'di_lam', 'outerwear'), ('Áo ba lỗ', 'di_lam', 'outerwear'), ('Áo ba lỗ', 'di_lam', 'outerwear'), ('Áo ba lỗ', 'di_lam', 'outerwear'), ('Áo giữ nhiệt', 'di_lam', 'outerwear'), ('Áo giữ nhiệt', 'di_lam', 'outerwear'), ('Áo giữ nhiệt', 'di_lam', 'outerwear'), ('Áo giữ nhiệt', 'di_lam', 'outerwear'), ('Áo giữ nhiệt', 'di_lam', 'outerwear'), ('Áo giữ nhiệt', 'di_lam', 'outerwear'), ('Áo hai dây', 'di_lam', 'outerwear'), ('Áo kiểu', 'di_lam', 'outerwear'), ('Áo len', 'di_lam', 'outerwear'), ('Áo len', 'di_lam', 'outerwear'), ('Áo nỉ', 'di_lam', 'outerwear'), ('Áo nỉ', 'di_lam', 'outerwear'), ('Áo nỉ', 'di_lam', 'outerwear'), ('Áo nỉ', 'di_lam', 'outerwear'), ('Áo nỉ', 'di_lam', 'outerwear'), ('Áo nỉ có mũ', 'di_lam', 'outerwear'), ('Áo nỉ có mũ', 'di_lam', 'outerwear'), ('Áo nỉ có mũ', 'di_lam', 'outerwear'), ('Áo nỉ có mũ', 'di_lam', 'outerwear'), ('Áo phông', 'di_lam', 'outerwear'), ('Áo phông', 'di_lam', 'outerwear'), ('Áo phông', 'di_lam', 'outerwear'), ('Áo phông', 'di_lam', 'outerwear'), ('Áo phông', 'di_lam', 'outerwear'), ('Áo phông', 'di_lam', 'outerwear'), ('Áo phông', 'di_lam', 'outerwear'), ('Áo phông', 'di_lam', 'outerwear'), ('Áo phông', 'di_lam', 'outerwear'), ('Áo phông', 'di_lam', 'outerwear'), ('Áo phông', 'di_lam', 'outerwear'), ('Áo phông', 'di_lam', 'outerwear'), ('Áo phông', 'di_lam', 'outerwear')]
\ No newline at end of file
# ============================================
# Langfuse v3 Self-Hosted - Optimized for LOW RAM
# Target: ~2.5GB total memory footprint
# Usage: docker compose -f docker-compose.langfuse.yml --env-file .env.langfuse up -d
# ============================================
services:
# --- Langfuse Worker (background jobs, ingestion) ---
langfuse-worker:
image: docker.io/langfuse/langfuse-worker:3
container_name: langfuse_worker
restart: always
depends_on: &langfuse-depends-on
langfuse-postgres:
condition: service_healthy
langfuse-minio:
condition: service_healthy
langfuse-redis:
condition: service_healthy
langfuse-clickhouse:
condition: service_healthy
ports:
- "127.0.0.1:3030:3030"
environment: &langfuse-worker-env
NODE_OPTIONS: "--max-old-space-size=768"
NEXTAUTH_URL: http://localhost:3009
DATABASE_URL: postgresql://langfuse:langfuse_pass@langfuse-postgres:5432/langfuse
SALT: ${SALT}
ENCRYPTION_KEY: ${ENCRYPTION_KEY}
TELEMETRY_ENABLED: false
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES: false
# ClickHouse
CLICKHOUSE_MIGRATION_URL: clickhouse://langfuse-clickhouse:9000
CLICKHOUSE_URL: http://langfuse-clickhouse:8123
CLICKHOUSE_USER: clickhouse
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD}
CLICKHOUSE_CLUSTER_ENABLED: false
# MinIO / S3
LANGFUSE_USE_AZURE_BLOB: false
LANGFUSE_S3_EVENT_UPLOAD_BUCKET: langfuse
LANGFUSE_S3_EVENT_UPLOAD_REGION: auto
LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID: minio
LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY: ${MINIO_ROOT_PASSWORD}
LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT: http://langfuse-minio:9000
LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE: true
LANGFUSE_S3_EVENT_UPLOAD_PREFIX: events/
LANGFUSE_S3_MEDIA_UPLOAD_BUCKET: langfuse
LANGFUSE_S3_MEDIA_UPLOAD_REGION: auto
LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID: minio
LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY: ${MINIO_ROOT_PASSWORD}
LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT: http://localhost:9092
LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE: true
LANGFUSE_S3_MEDIA_UPLOAD_PREFIX: media/
LANGFUSE_S3_BATCH_EXPORT_ENABLED: false
# Redis
REDIS_HOST: langfuse-redis
REDIS_PORT: 6379
REDIS_AUTH: ${REDIS_AUTH}
REDIS_TLS_ENABLED: false
deploy:
resources:
limits:
memory: 1g
networks:
- langfuse_net
# --- Langfuse Web UI ---
langfuse-web:
image: docker.io/langfuse/langfuse:3
container_name: langfuse_web
restart: always
depends_on: *langfuse-depends-on
ports:
- "3009:3000"
environment:
<<: *langfuse-worker-env
NODE_OPTIONS: "--max-old-space-size=1536"
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET}
# Auto-init project so you don't have to create manually
LANGFUSE_INIT_ORG_ID: canifa
LANGFUSE_INIT_ORG_NAME: Canifa
LANGFUSE_INIT_PROJECT_ID: canifa-stylist
LANGFUSE_INIT_PROJECT_NAME: Canifa AI Stylist
LANGFUSE_INIT_PROJECT_PUBLIC_KEY: pk-lf-canifa-local
LANGFUSE_INIT_PROJECT_SECRET_KEY: sk-lf-canifa-local
LANGFUSE_INIT_USER_EMAIL: admin@canifa.local
LANGFUSE_INIT_USER_NAME: Admin
LANGFUSE_INIT_USER_PASSWORD: canifa2026
deploy:
resources:
limits:
memory: 2g
networks:
- langfuse_net
# --- ClickHouse (trace analytics) ---
langfuse-clickhouse:
image: docker.io/clickhouse/clickhouse-server
container_name: langfuse_clickhouse
restart: always
user: "101:101"
environment:
CLICKHOUSE_DB: default
CLICKHOUSE_USER: clickhouse
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD}
volumes:
- langfuse_clickhouse_data:/var/lib/clickhouse
- langfuse_clickhouse_logs:/var/log/clickhouse-server
ports:
- "127.0.0.1:18123:8123"
- "127.0.0.1:19000:9000"
healthcheck:
test: wget --no-verbose --tries=1 --spider http://localhost:8123/ping || exit 1
interval: 10s
timeout: 10s
retries: 15
start_period: 30s
deploy:
resources:
limits:
memory: 1g
networks:
- langfuse_net
# --- MinIO (blob storage) ---
langfuse-minio:
image: cgr.dev/chainguard/minio
container_name: langfuse_minio
restart: always
entrypoint: sh
command: -c 'mkdir -p /data/langfuse && minio server --address ":9000" --console-address ":9001" /data'
environment:
MINIO_ROOT_USER: minio
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
ports:
- "9092:9000"
- "127.0.0.1:9093:9001"
volumes:
- langfuse_minio_data:/data
healthcheck:
test: ["CMD", "mc", "ready", "local"]
interval: 1s
timeout: 5s
retries: 5
start_period: 1s
deploy:
resources:
limits:
memory: 128m
networks:
- langfuse_net
# --- Redis (queue/cache) ---
langfuse-redis:
image: docker.io/redis:7-alpine
container_name: langfuse_redis
restart: always
command: >
--requirepass ${REDIS_AUTH}
--maxmemory 64mb
--maxmemory-policy noeviction
ports:
- "127.0.0.1:16379:6379"
volumes:
- langfuse_redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_AUTH}", "ping"]
interval: 3s
timeout: 10s
retries: 10
deploy:
resources:
limits:
memory: 96m
networks:
- langfuse_net
# --- PostgreSQL (metadata) ---
langfuse-postgres:
image: docker.io/postgres:17-alpine
container_name: langfuse_postgres
restart: always
healthcheck:
test: ["CMD-SHELL", "pg_isready -U langfuse"]
interval: 3s
timeout: 3s
retries: 10
environment:
POSTGRES_USER: langfuse
POSTGRES_PASSWORD: langfuse_pass
POSTGRES_DB: langfuse
TZ: Asia/Ho_Chi_Minh
PGTZ: Asia/Ho_Chi_Minh
ports:
- "127.0.0.1:15434:5432"
volumes:
- langfuse_postgres_data:/var/lib/postgresql/data
command:
- "postgres"
- "-c"
- "shared_buffers=64MB"
- "-c"
- "work_mem=4MB"
- "-c"
- "effective_cache_size=128MB"
deploy:
resources:
limits:
memory: 256m
networks:
- langfuse_net
volumes:
langfuse_postgres_data:
driver: local
langfuse_clickhouse_data:
driver: local
langfuse_clickhouse_logs:
driver: local
langfuse_minio_data:
driver: local
langfuse_redis_data:
driver: local
networks:
langfuse_net:
driver: bridge
# /etc/nginx/sites-available/your-api
# Rate limit zones
limit_req_zone $binary_remote_addr zone=ip_limit:10m rate=100r/h;
# Upstream backend servers
upstream backend {
server localhost:8000;
# Nếu có nhiều backend servers:
# server localhost:8001;
# server localhost:8002;
}
# Redirect HTTP to HTTPS
server {
listen 80;
server_name api.yourdomain.com;
# Redirect to HTTPS
return 301 https://$server_name$request_uri;
}
# Main HTTPS server
server {
listen 443 ssl http2;
server_name api.yourdomain.com;
# SSL certificates (Let's Encrypt)
ssl_certificate /etc/letsencrypt/live/api.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.yourdomain.com/privkey.pem;
# SSL settings
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# Security headers
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# Logging
access_log /var/log/nginx/api_access.log;
error_log /var/log/nginx/api_error.log;
# Main API endpoint
location /api/ {
# Rate limiting (100 requests/hour per IP)
limit_req zone=ip_limit burst=20 nodelay;
limit_req_status 429;
# CORS headers (if needed)
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, X-Anonymous-ID' always;
# Handle preflight
if ($request_method = 'OPTIONS') {
return 204;
}
# Proxy to backend
proxy_pass http://backend;
# Pass headers
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Pass auth headers
proxy_set_header Authorization $http_authorization;
proxy_set_header X-Anonymous-ID $http_x_anonymous_id;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# Buffer settings
proxy_buffering off;
proxy_request_buffering off;
}
# Health check endpoint (không rate limit)
location /health {
access_log off;
proxy_pass http://backend/health;
}
# Custom error pages
error_page 429 /429.json;
location = /429.json {
internal;
return 429 '{"error":"Too many requests. Please try again later.","retry_after":3600}';
add_header Content-Type application/json always;
add_header Retry-After 3600 always;
}
error_page 502 503 504 /50x.json;
location = /50x.json {
internal;
return 502 '{"error":"Service temporarily unavailable"}';
add_header Content-Type application/json always;
}
}
\ No newline at end of file
Binary files a/backend/output.txt and /dev/null differ Binary files a/backend/output.txt and /dev/null differ
...@@ -54,6 +54,25 @@ async def startup_event(): ...@@ -54,6 +54,25 @@ async def startup_event():
logger.info("✅ Redis cache initialized") logger.info("✅ Redis cache initialized")
@app.on_event("shutdown")
async def shutdown_event():
"""Gracefully close database connection pools on shutdown to prevent connection leaks during hot-reloads."""
try:
from common.conversation_manager import get_conversation_manager
manager = await get_conversation_manager()
await manager.close()
logger.info("✅ Postgres connection pool closed gracefully")
except Exception as e:
logger.error(f"Error closing Postgres pool: {e}")
try:
from common.starrocks_connection import StarRocksConnection
await StarRocksConnection.clear_pool()
logger.info("✅ StarRocks connection pool closed gracefully")
except Exception as e:
logger.error(f"Error closing StarRocks pool: {e}")
@app.get("/") @app.get("/")
async def root(): async def root():
return RedirectResponse(url="/static/index.html") return RedirectResponse(url="/static/index.html")
......
...@@ -194,14 +194,17 @@ Always respond warmly in Vietnamese.</textarea> ...@@ -194,14 +194,17 @@ Always respond warmly in Vietnamese.</textarea>
strip.className = 'product-strip'; strip.className = 'product-strip';
products.forEach(p => { products.forEach(p => {
const isObj = typeof p === 'object' && p !== null; const isObj = typeof p === 'object' && p !== null;
const name = isObj ? (p.name || p.sku) : (p || 'Sản phẩm Canifa'); const name = isObj ? (p.product_name || p.name || p.sku) : (p || 'Sản phẩm Canifa');
const img = isObj ? (p.image || p.thumbnail_image_url) : `https://placehold.co/200x260?text=${p}`; const img = isObj ? (p.product_image_url_thumbnail || p.image || p.thumbnail_image_url) : `https://placehold.co/200x260?text=${p}`;
const price = isObj ? (p.price || p.sale_price || 0) : 0; const price = isObj ? (p.sale_price || p.price || 0) : 0;
const oldPrice = isObj ? ((p.original_price && p.price && p.original_price > p.price) ? p.original_price : null) : null; const oldPrice = isObj ? ((p.original_price && p.original_price > price) ? p.original_price : null) : null;
const url = isObj ? (p.url ? (p.url.startsWith('http') ? p.url : `https://canifa.com/${p.url}`) : '#') : '#'; const finalUrl = isObj ? (p.product_web_url || p.url) : null;
const url = finalUrl ? (finalUrl.startsWith('http') ? finalUrl : `https://canifa.com/${finalUrl}`) : '#';
const colorName = isObj ? (p.color_name || '') : '';
const colorDisplay = colorName ? `<div class="p-color" title="${colorName}" style="display:inline-block; font-size:11px; padding:2px 6px; background:#f5f5f5; color:#555; border-radius:4px; margin-bottom:4px; margin-top:4px;">${colorName}</div>` : ''; const colorName = isObj ? (p.master_color || p.color_name || '') : '';
const colorCode = isObj ? (p.sku_color || p.sku || '') : '';
const displayMeta = colorName ? `${colorName} (${colorCode})` : colorCode;
const colorDisplay = displayMeta ? `<div class="p-color" title="${displayMeta}" style="display:inline-block; font-size:11px; padding:2px 6px; background:#f5f5f5; color:#555; border-radius:4px; margin-bottom:4px; margin-top:4px;">${displayMeta}</div>` : '';
const card = document.createElement('a'); const card = document.createElement('a');
card.className = 'p-card'; card.className = 'p-card';
...@@ -231,6 +234,44 @@ Always respond warmly in Vietnamese.</textarea> ...@@ -231,6 +234,44 @@ Always respond warmly in Vietnamese.</textarea>
row.appendChild(bubble); row.appendChild(bubble);
row.appendChild(time); row.appendChild(time);
if (type !== 'human' && typeof msgObj === 'object') {
const rawWrap = document.createElement('div');
rawWrap.style.flexBasis = '100%';
rawWrap.style.marginTop = '4px';
rawWrap.style.textAlign = 'left';
const rawToggle = document.createElement('span');
rawToggle.textContent = '🔍 View Raw JSON';
rawToggle.style.fontSize = '11px';
rawToggle.style.color = '#888';
rawToggle.style.cursor = 'pointer';
rawToggle.style.textDecoration = 'underline';
const rawContent = document.createElement('pre');
rawContent.textContent = JSON.stringify(msgObj, null, 2);
rawContent.style.display = 'none';
rawContent.style.fontSize = '10px';
rawContent.style.background = '#f1f1f1';
rawContent.style.color = '#333';
rawContent.style.padding = '8px';
rawContent.style.borderRadius = '4px';
rawContent.style.marginTop = '4px';
rawContent.style.maxWidth = '100%';
rawContent.style.overflowX = 'auto';
rawContent.style.whiteSpace = 'pre-wrap';
rawToggle.onclick = () => {
rawContent.style.display = rawContent.style.display === 'none' ? 'block' : 'none';
scrollToBottom();
};
rawWrap.appendChild(rawToggle);
rawWrap.appendChild(rawContent);
row.appendChild(rawWrap);
row.style.flexWrap = 'wrap';
}
area.appendChild(row); area.appendChild(row);
scrollToBottom(); scrollToBottom();
......
version = 1
revision = 3
requires-python = ">=3.14"
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