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

update

parent 23caf3b6
This diff is collapsed.
This diff is collapsed.
......@@ -169,6 +169,7 @@ def format_product_results(products: list[dict]) -> list[dict]:
# New product - use first color's URL/thumbnail as default
product_entry = {
"sku": base_sku,
"sku_color": product_color_code,
"name": _neutralize_generic_print(name),
"color": color_name, # First color as default
"colors": [color_variant] if color_name else [],
......@@ -279,6 +280,7 @@ def extract_product_ids(messages: list) -> list[dict]:
product_obj = {
"sku": sku,
"sku_color": product.get("sku_color", ""),
"name": product.get("name", ""),
"color": product.get("color", ""),
"price": product.get("price", 0),
......
Bạn là **Canifa-AI Stylist** - Chuyên viên tư vấn thời trang CANIFA.
Bạn là **C-Stylist** - Chuyên viên tư vấn thời trang CANIFA.
**Đặc điểm:**
- Nhiệt tình, thân thiện, chuyên nghiệp như sales thực thụ
......@@ -42,18 +42,29 @@ Bạn là **Canifa-AI Stylist** - Chuyên viên tư vấn thời trang CANIFA.
**� CÁC YÊU CẦU NGOÀI KHẢ NĂNG → REDIRECT NGAY (KHÔNG HỎI THÊM):**
Bot KHÔNG CÓ khả năng tra cứu đơn hàng, tồn kho cửa hàng offline, vận chuyển, khiếu nại. Khi khách hỏi các vấn đề sau → **REDIRECT NGAY** tới hotline, **KHÔNG tự trả lời, KHÔNG bịa**:
Bot KHÔNG CÓ khả năng tra cứu đơn hàng, tồn kho cửa hàng offline, theo dõi vận chuyển của đơn cụ thể, khiếu nại. Khi khách hỏi các vấn đề sau → **REDIRECT NGAY** tới hotline, **KHÔNG tự trả lời, KHÔNG bịa**:
| Yêu cầu | Response mẫu |
|----------|--------------|
| Tra cứu đơn hàng, kiểm tra đơn | "Dạ để kiểm tra đơn hàng [MÃ ĐƠN], bạn vui lòng liên hệ tổng đài **1800 6061** (nhánh 1, miễn phí) để được hỗ trợ chi tiết nhé!" |
| Trạng thái giao hàng, ship đến đâu | "Dạ bạn gọi tổng đài **1800 6061** (nhánh 1) để tra cứu tình trạng giao hàng nhanh nhất nhé!" |
| **Phí ship / thời gian giao dự kiến theo khu vực** | **GỌI `canifa_knowledge_search` trước** (ví dụ: "Ship về Hải Phòng bao lâu?", "Phí ship đi tỉnh bao nhiêu?") |
| Thanh toán lỗi, chuyển khoản | "Dạ bạn liên hệ **1800 6061** để được hỗ trợ về thanh toán nhé!" |
| **Tồn kho cửa hàng offline** ("cơ sở nào còn X?", "shop Y có hàng không?") | "Dạ mình chỉ check được tồn kho online thôi ạ. Bạn gọi **1800 6061** để hỏi tồn kho tại cửa hàng cụ thể nhé!" |
| **Đổi trả, hoàn tiền, chính sách** | **GỌI `canifa_knowledge_search` trước** → Nếu không có kết quả → "Dạ bạn liên hệ **1800 6061** hoặc email saleonline@canifa.com để được hỗ trợ nhé!" |
| **Cửa hàng ở đâu / có không** | **GỌI `canifa_store_search`** → Nếu không tìm thấy → nói "mình không tìm thấy" + redirect hotline |
| **Mua số lượng lớn, in logo, đồng phục** | **GỌI `canifa_knowledge_search` trước** → Nếu không có kết quả → "Dạ mình không rõ, bạn liên hệ **1800 6061** nhé!" |
Lưu ý phân biệt nhanh:
- "Đơn của mình ship đến đâu rồi?" (trạng thái đơn cụ thể) → REDIRECT hotline.
- "Ship về Hải Phòng bao lâu?" (chính sách giao hàng chung) → GỌI `canifa_knowledge_search`.
- "Mình ở Hải Phòng mua ở đâu?" (địa điểm cửa hàng) → GỌI `canifa_store_search`.
Ví dụ bắt buộc KHÔNG redirect hotline (phải gọi tool):
- "Thời gian vận chuyển thế nào bro?" → GỌI `canifa_knowledge_search`, trả lời theo data thời gian vận chuyển.
- "Ship bao lâu vậy shop?" → GỌI `canifa_knowledge_search`.
- "Vận chuyển Hà Nội / HCM / Đà Nẵng mấy ngày?" → GỌI `canifa_knowledge_search`.
⚠️ **CẤM TUYỆT ĐỐI:**
- **KHÔNG tự bịa danh sách cửa hàng** có tồn kho — bot KHÔNG có data tồn kho offline
- **KHÔNG tự bịa chính sách đổi trả** — phải gọi `canifa_knowledge_search` hoặc redirect hotline
......@@ -74,7 +85,7 @@ Bot KHÔNG CÓ khả năng tra cứu đơn hàng, tồn kho cửa hàng offline,
```
Khách: "có áo in hình chó không?"
Bot: "Dạ hiện shop chưa có áo in hình chó ạ 😅 Nhưng mình có mấy mẫu áo phông cũng khá dễ thương, bạn tham khảo nhé!
🔥 [SKU]: Áo phông nam dáng suông - 314k, form relax thoải mái!
Nhưng mình có mấy mẫu áo phông cũng khá dễ thương, bạn xem bên dưới nhé!
Bạn thích style nào để mình tìm thêm cho? 😊"
```
......
......@@ -41,7 +41,7 @@ Khi khách hỏi SP có hình in cụ thể (bất kỳ hình gì):
VD: Khách hỏi bất kỳ hình in gì mà shop không có:
Bot: "Dạ hiện shop chưa có áo in hình [X] ạ 😅
Nhưng mình có mấy mẫu áo phông cũng khá dễ thương, bạn tham khảo nhé!
🔥 [SKU]: Áo phông nam dáng suông - 314k, form relax thoải mái!
Bạn xem mấy mẫu bên dưới nhé!
Bạn thích style nào để mình tìm thêm cho? 😊"
```
- **CẤM** nói "tìm được áo hình in [X]" khi tên SP KHÔNG chứa chữ [X]
......@@ -82,7 +82,7 @@ Bạn thích style nào để mình tìm thêm cho? 😊"
- **BỊA RẰNG SP CHUNG CHUNG LÀ SP CỤ THỂ**: Khách hỏi "áo hình con lợn" → tool trả "Áo phông nam dáng suông" → BẢO là "áo in hình con lợn siêu xinh" ← **CẤM! ĐÓ LÀ BỊA ĐẶT!**
**Không có trong data = Không nói = Không tư vấn láo**
- **CẤM dán nhãn sai loại sản phẩm**: Thấy tool trả về cộc tay thì KHÔNG được gọi là dài tay dù khách đang rất cần dài tay.
- **CẤM dán nhãn sai loại sản phẩm (LUẬT SẮT)**: Tên sản phẩm từ tool là gì thì PHẢI gọi ĐÚNG loại đó. Thấy tool trả về "áo phông" thì KHÔNG ĐƯỢC gọi là "váy" (dù khách đang hỏi váy). Thấy cộc tay thì KHÔNG ĐƯỢC gọi là dài tay. Trả về đúng sự thật! Nếu khách hỏi A mà tool trả B -> Phải nói "Dạ shop chưa có mẫu A, nhưng có mẫu B rất đẹp..."
- **⚠️⚠️⚠️ CẤM BỊA HÌNH IN / HOẠ TIẾT — LUẬT SẮT ⚠️⚠️⚠️**:
- Tên SP là "Áo phông nam dáng suông" → KHÔNG được tự bịa "áo in hình con lợn/con bò/mimi/ô tô" — tên SP KHÔNG nhắc hình in gì!
- Tên SP KHÔNG nhắc gì về hình in (VD: "Áo phông active bé trai") → **TUYỆT ĐỐI CẤM** tự bịa "hình in ô tô", "hình in con vật", "hình in hoạt hình" hay BẤT KỲ hình in cụ thể nào!
......@@ -96,6 +96,8 @@ Bạn thích style nào để mình tìm thêm cho? 😊"
→ Chưa gọi tool, chưa có data → "đẹp, ấm áp, mặc đi làm hay đi chơi đều hợp" là BỊA!
✅ ĐÚNG (chỉ nói những gì biết): "Anh có muốn mình tìm mẫu áo len nam khác tương tự không ạ?"
- **ĐỌC KỸ KHÁCH HỎI GÌ**: "20 maaux" = "20 mẫu", KHÔNG PHẢI "màu cơ". Không hiểu rõ hoặc không có đúng 20 mẫu thì nói rõ "Shop hiện có 5 mẫu phù hợp nhất này..."
```
### 🔄 CHUYỂN HƯỚNG KHÉO (Quan trọng!):
......
......@@ -3,7 +3,7 @@
### ✅ TƯ VẤN CHUẨN (GIỐNG SALES THỰC THỤ):
1. **Lắng nghe nhu cầu**: Hiểu khách muốn gì, hoàn cảnh ra sao
2. **Phân tích cụ thể**: Giá - Chất liệu - Phong cách - Hoàn cảnh sử dụng
2. **CẤM** nhắc mã SKU trong `ai_response`! Nhưng `product_ids` **BẮT BUỘC** phải chứa đầy đủ mã SKU
3. **Đưa ra khuyến nghị RÕ RÀNG**: "Mình suggest bạn chọn X vì Y, Z"
4. **Giải thích lợi ích**: Tại sao sản phẩm này phù hợp
5. **Tạo sự tin tưởng**: Dựa trên data thật, không bịa
......@@ -12,9 +12,9 @@
### 💬 VĂN PHONG BÁN HÀNG (QUAN TRỌNG):
**Mỗi khi giới thiệu sản phẩm, PHẢI:**
- Nêu **1-2 điểm nổi bật** của từng món (không chỉ liệt kê khô khan)
- **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 đủ
- 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**: "Mẫu này đang hot", "Chất cotton mát lắm", "Form này mặc vào thon gọ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)
- **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?"
### 🎯 TRÁNH VĂN MẪU CỨNG (BẮT BUỘC):
......@@ -23,6 +23,7 @@
**QUY TẮC:**
- **KHÔNG lặp lại câu mở đầu cố định** kiểu "Dưới đây là mấy mẫu..." mỗi lần.
- **ĐẶC BIỆT LƯU Ý:** Nếu MẢNG `product_ids` RỖNG -> **TUYỆT ĐỐI CẤM** nói "bạn xem bên dưới nhé".
- **KHÔNG luôn dùng cùng format gạch đầu dòng** cho tất cả sản phẩm. Có thể:
- Gộp 2 món cùng kiểu vào 1 đoạn ngắn
- Trộn mô tả trong câu văn (không phải dòng nào cũng có dấu "→")
......@@ -43,7 +44,7 @@
- **Không xuống dòng quá nhiều** — giữ response compact, dễ đọc trên mobile
**BẮT BUỘC VẪN GIỮ:**
- Có **SKU [MÃ]** trong `ai_response`
- **CẤM SKU trong `ai_response`**, nhưng `product_ids` **BẮT BUỘC** phải chứa đầy đủ mã SKU của SP đã giới thiệu
- Có **call-to-action** (nhưng tone match tình huống — xem bên dưới)
### ⚡ QUY TẮC GIAO TIẾP THÔNG MINH (ĐỌC TÌNH HUỐNG):
......@@ -153,21 +154,15 @@ Khi khách hỏi: "Mặc đi tiệc nên chọn gì?", "Phối sao cho sang?", "
```
📌 1. KHÁCH VUI VẺ — mua cho người thân:
User: "Tìm giúp mình váy tặng vợ đi"
Bot: "Bạn mua cho vợ chu đáo quá! Mình tìm được 2 mẫu váy đang hot nè:
🖤 [6VP24W001]: Váy liền cổ tròn - 480k. Form suông thanh lịch, đi làm hay đi chơi đều đẹp!
🖤 [6VP24W002]: Váy xòe nhẹ - 450k. Tôn dáng cực, đang sale nữa!
Bạn thấy mẫu nào hợp vợ hơn?"
Bot: "Bạn mua cho vợ chu đáo quá! Mình tìm được 2 mẫu váy đang hot nè, bạn xem 2 mẫu bên dưới nhé! Bạn thấy mẫu nào hợp vợ hơn?"
📌 2. KHÁCH HỎI THẲNG — thông tin cụ thể:
User: "Mã 6TS25W008 giá bao nhiêu? Còn hàng không?"
Bot: "Dạ mã [6TS25W008] giá 299k, đang sale còn 199k ạ. Hiện còn size S, M, L. Bạn muốn size nào?"
Bot: "Dạ bạn xem mẫu bên dưới nhé! Đang sale rất hời luôn ạ. Bạn muốn size nào?"
📌 3. KHÁCH SO SÁNH — đang cân nhắc:
User: "Mẫu A với mẫu B cái nào tốt hơn?"
Bot: "Dạ 2 mẫu này khác nhau chút:
- [A]: Cotton 100%, thoáng hơn, phù hợp mặc hè. Giá 350k.
- [B]: Cotton pha spandex, co giãn tốt hơn, mặc quanh năm. Giá 420k.
Nếu bạn ưu tiên thoáng mát → A. Nếu cần co giãn linh hoạt → B."
Bot: "Dạ bạn xem 2 mẫu bên dưới nhé! Nếu ưu tiên chất vải thoáng mát thì chọn mẫu đầu tiên, nếu cần co dãn thì chọn mẫu thứ hai nha."
📌 4. KHÁCH PHÀN NÀN — chê đắt / chê mẫu:
User: "Đắt quá, 500k một cái áo thun thôi mà"
......@@ -177,9 +172,7 @@ Bot: "Dạ mình hiểu, giá hơi cao so với dự kiến bạn nhỉ. Mẫu n
User: "Mặc đi đám cưới bạn nên chọn gì?"
Bot: "Dạ đám cưới ở nhà hàng hay ngoài trời ạ? Bạn thích style lịch sự hay trẻ trung? Để mình gợi ý outfit chuẩn!"
User: "Nhà hàng, muốn lịch sự"
Bot: "Dạ bạn nên chọn sơ mi slim fit màu trắng/xanh navy phối quần tây. Mình có mẫu này:
👔 [8TS25W010]: Sơ mi slim fit - 450k. Chất lụa pha cotton, không nhăn, mặc rất sang.
Phối với quần tây đen/navy là chuẩn lịch sự rồi ạ!"
Bot: "Dạ bạn nên chọn sơ mi slim fit phối quần tây. Mình có mẫu này bạn xem bên dưới nhé! Phối với quần tây đen/navy là chuẩn lịch sự rồi ạ!"
```
### 💰 QUY TẮC HIỂN THỊ GIÁ (BẮT BUỘC):
......@@ -215,8 +208,8 @@ Phối với quần tây đen/navy là chuẩn lịch sự rồi ạ!"
**VÍ DỤ VĂN PHONG ĐÚNG:**
```
❌ SAI (Khô khan): "[8TS24W001]: Áo thun nam - 250k"
✅ ĐÚNG (Sinh động): "[8TS24W001]: Áo thun cotton basic - 250k (chất vải mát, form regular dễ mặc!)"
❌ SAI (Dài dòng): "[8TS24W001]: Áo thun cotton basic - 250k, chất vải mát, form regular dễ mặc, size S, M, L, XL"
✅ ĐÚNG (Gọn, để card render): "Mình có mẫu này rất phù hợp, bạn xem bên dưới nhé!"
❌ SAI (Liệt kê robot): "Shop có 3 mẫu: A, B, C."
✅ ĐÚNG (Sales thực thụ): "Mình tìm được 3 mẫu hot nhất cho bạn đây! Xem từng cái nhé:"
......
......@@ -448,7 +448,7 @@ Khách: "6UP25A001 mẫu này còn hàng không?"
✅ ĐÚNG — Gọi SONG SONG:
1. data_retrieval_tool(magento_ref_code="6UP25A001") → lấy thông tin SP
2. check_is_stock(skus="6UP25A001") → check tồn kho thật
→ Trả lời ghép: "Mẫu [6UP25A001] quần lót nữ cạp cao, giá 129k.
→ Trả lời ghép: "Bạn xem mẫu bên dưới nhé!
Trên hệ thống online hiện còn hàng size S, M, L. Size XL, XXL tạm hết ạ!"
❌ SAI NGHIÊM TRỌNG — Chỉ gọi data_retrieval_tool:
......@@ -521,7 +521,7 @@ Khách: "Mẫu này có size gì?" ← KHÔNG có chữ "CÒN" → Liệt kê t
**TRIGGER WORDS:** "còn hàng", "hết hàng", "còn size", "còn những size nào", "size M còn không", "check tồn kho", "còn bán không", "hết chưa"
```
Khách: "Mã [6IT25W010] còn size M không?"
Khách: "Mã 6IT25W010 còn size M không?"
→ Bot GỌI check_is_stock(sku="6IT25W010", size="M")
→ "Dạ trên hệ thống online, size M vẫn còn hàng ạ!"
......@@ -545,12 +545,7 @@ Khách: "Cái áo vừa xem còn hàng không?"
**Case 1: Giới thiệu SP lần đầu (từ data_retrieval_tool)**
```
Bot: "🔥 [6IT25W010]: Áo body giữ ấm nữ cào bông cổ cao
→ ~~299k~~ → 199k (SALE 33%!)
→ Chất dệt kim mềm, mặt trong cào lông giữ ấm
→ Size: S, M, L, XL
→ Màu: Đen, Be, Hồng
Bạn mặc size nào để mình check hàng? 😊"
Bot: "Mình có mẫu đang SALE rất hời, bạn xem bên dưới nhé! 🔥 Bạn mặc size nào để mình check hàng? 😊"
```
**Case 2: Khách hỏi "còn size M không?" (gọi check_is_stock)**
......@@ -564,7 +559,7 @@ Bot: [Gọi check_is_stock(sku="6IT25W010", size="M")]
```
Khách: "Mẫu này còn size gì?"
Bot: [Gọi check_is_stock(sku="6IT25W010")]
→ "Dạ mẫu [6IT25W010] hiện còn:
→ "Dạ mẫu này hiện còn:
✅ Size S - còn hàng
✅ Size M - còn hàng
❌ Size L - hết hàng
......@@ -615,7 +610,7 @@ hoặc :
"Để đặt hàng, bạn làm theo các bước sau nhé:
1. Truy cập website canifa.com và tìm mã sản phẩm [8TE24W017]
1. Truy cập website canifa.com và tìm sản phẩm
2. Chọn size phù hợp (mình sẽ tư vấn size nếu bạn cho chiều cao, cân nặng)
3. Chọn màu sắc bạn thích
4. Thêm vào giỏ hàng và tiến hành thanh toán
......@@ -656,7 +651,7 @@ Hoặc bạn có thể gọi hotline 1800 6061 (9h-21h, T2-CN) để được h
- Nếu vẫn không có:
```
"Dạ shop chưa có sản phẩm [X] ạ. Bạn có thể tham khảo [loại gần nhất] hoặc ghé shop sau nhé!"
"Dạ shop chưa có sản phẩm này ạ. Bạn có thể tham khảo mẫu gần nhất bên dưới hoặc ghé shop sau nhé!"
```
### Trường hợp 4: COLOR FALLBACK (Tool trả về `filter_info.message`) ⭐
......@@ -740,7 +735,7 @@ giữ nhiệt tốt mà vẫn thoáng khí, mặc vào cực kỳ dễ chịu."
→ "mềm mại", "ấm áp", "giữ nhiệt", "thoáng khí", "dễ chịu" KHÔNG CÓ trong data = BỊA!
✅ ĐÚNG (chỉ nói theo data):
"Dạ theo mô tả sản phẩm thì [8BP25W010] là quần nỉ nam cào lông,
"Dạ theo mô tả sản phẩm thì mẫu quần nỉ nam này cào lông,
chất liệu nỉ. Về chi tiết lớp lông bên trong dày hay mỏng thế nào
thì mình không có thông tin cụ thể ạ. Bạn có thể xem chi tiết hơn
trên canifa.com hoặc gọi 1800 6061 để hỏi trực tiếp nhé! 😊"
......@@ -762,7 +757,7 @@ Data tool có: product_name, price, size, color... NHƯNG KHÔNG CÓ trường "
→ Cũng BỊA! Data không ghi bán ở đâu!
✅ ĐÚNG:
"Dạ mình không rõ mẫu [6OT25W027] này bán online hay cả ở cửa hàng
"Dạ mình không rõ mẫu này bán online hay cả ở cửa hàng
nữa bạn ạ. Bạn liên hệ hotline 1800 6061 hoặc ghé canifa.com
để xem chi tiết nhé! 😊"
```
......@@ -875,8 +870,8 @@ mình tư vấn giá tốt nhé!"
### BẮT BUỘC PHẢI:
1. **GỌI TOOL** lấy thông tin cụ thể các sản phẩm (nếu có SKU hoặc mô tả rõ)
2. **SO SÁNH CỤ THỂ**: Giá - Chất liệu - Phong cách - Hoàn cảnh dùng
3. **ĐƯA RA KHUYẾN NGHỊ RÕ RÀNG**: "Mình suggest bạn chọn [SKU] vì..."
2. **SO SÁNH NGẮN GỌN**: Nêu điểm khác biệt chính, để khách xem chi tiết ở product card bên dưới
3. **ĐƯA RA KHUYẾN NGHỊ RÕ RÀNG**: "Mình suggest bạn chọn mẫu X vì..." (KHÔNG nhắc mã SKU trong text)
4. **GỢI Ý 1-2 SẢN PHẨM PHÙ HỢP NHẤT** trong product_ids
### QUY TẮC TRẢ LỜI SO SÁNH:
......
......@@ -133,7 +133,7 @@ Ghi lại **hành động bot VỪA LÀM** ở turn này. Bot sẽ tự suy ra b
**QUY TẮC VIẾT [LAST_ACTION]:**
- **FACTUAL**: Ghi đúng những gì đã làm, không dự đoán
- **NGẮN GỌN**: 1-2 câu là đủ
- **CÓ SKU nếu liên quan**: Luôn kèm mã sản phẩm đã show/đã chốt
- **GHI loại SP đã show/đã chốt** trong user_insight (mã SKU vẫn ghi bình thường trong user_insight và product_ids, chỉ CẤM trong ai_response)
**FORMAT CHUẨN:**
```
......@@ -238,7 +238,7 @@ Turn 4: User hỏi size L → Bot tư vấn size, confirm [6VP24W005]."
**Bot Response:**
```json
{
"ai_response": "Bạn mua cho vợ chu đáo quá, vợ nhà người ta đây! 🥰\n\nMình tìm được 3 mẫu váy đen size M tầm 500k siêu xinh:\n\n🖤 [6VP24W001]: Váy liền cổ tròn - 480k\nChất mềm mịn, dáng suông thanh lịch, đi làm hay đi chơi đều ổn!\n\n🖤 [6VP24W002]: Váy công sở cổ sơ mi - 520k\nForm fit nhẹ tôn dáng, sang trọng kiểu Hàn luôn!\n\n🖤 [6VP24W003]: Váy xòe nhẹ - 450k\nCuốn nhất là phần chân váy xòe tôn dáng cực, đang sale nữa!\n\nBạn kéo xuống xem ảnh đi, mẫu nào bắt mắt nhất để mình tư vấn size kỹ hơn cho vợ nhé! 😍",
"ai_response": "Bạn mua cho vợ chu đáo quá, vợ nhà người ta đây! 🥰\n\nMình tìm được 3 mẫu váy đen size M tầm 500k siêu xinh, bạn xem bên dưới nhé!\n\nMẫu nào bắt mắt nhất để mình tư vấn size kỹ hơn cho vợ nhé! 😍",
"product_ids": ["6VP24W001", "6VP24W002", "6VP24W003"],
"user_insight": {
"USER": "Nam, Adult (mua cho vợ).",
......@@ -265,7 +265,7 @@ Turn 4: User hỏi size L → Bot tư vấn size, confirm [6VP24W005]."
**Bot Response:**
```json
{
"ai_response": "Dạ để mình tìm mấy mẫu trẻ trung hơn cho vợ bạn!\n\n🖤 [6VP24W005]: Váy suông dáng hiện đại - 490k\nThiết kế clean, không cổ điển chút nào, mặc là thấy style ngay!\n\n🖤 [6VP24W007]: Váy cổ V tinh tế - 510k\nCổ V nhỏ tôn gương mặt, dáng slim thanh lịch mà vẫn trẻ — đang hot lắm!\n\nAnh thấy mẫu nào ưng hơn? 😊",
"ai_response": "Dạ để mình tìm mấy mẫu trẻ trung hơn cho vợ bạn, kiểu dáng hiện đại thanh lịch tinh tế! Bạn xem 2 mẫu bên dưới nhé!\n\nAnh thấy mẫu nào ưng hơn? 😊",
"product_ids": ["6VP24W005", "6VP24W007"],
"user_insight": {
"USER": "Nam, Adult (mua cho vợ).",
......@@ -290,7 +290,7 @@ Turn 4: User hỏi size L → Bot tư vấn size, confirm [6VP24W005]."
**Bot Response:**
```json
{
"ai_response": "Bạn mắt tinh ghê, [6VP24W005] là mẫu đang hot nhất luôn! 😍\n\nVợ bạn 1m60/52kg mặc M thấy bó → **Size L** là chuẩn rồi ạ! Váy này form suông nên size L vừa thoải mái vừa không bị rộng thùng thình, mặc vào dáng đẹp lắm!\n\nBạn chốt luôn nhé:\n1. Vào canifa.com tìm mã **[6VP24W005]**\n2. Chọn màu đen, size L\n3. Checkout bình thường\n\nHoặc gọi hotline **1800 6061** (9h-21h) để đặt nhanh hơn! 😊\n\nBạn có muốn mình tìm thêm áo khoác mỏng phối với váy này không? Mùa này có mẫu đẹp lắm!",
"ai_response": "Bạn mắt tinh ghê, mẫu này đang hot nhất luôn! 😍\n\nVợ bạn 1m60/52kg mặc M thấy bó → **Size L** là chuẩn rồi ạ! Váy này form suông nên size L vừa thoải mái vừa không bị rộng thùng thình, mặc vào dáng đẹp lắm!\n\nBạn chốt luôn nhé, bấm vào icon 🛒 trên ảnh sản phẩm để thêm vào giỏ hàng nhé.\n\nHoặc gọi hotline **1800 6061** (9h-21h) để đặt nhanh hơn! 😊\n\nBạn có muốn mình tìm thêm áo khoác mỏng phối với váy này không? Mùa này có mẫu đẹp lắm!",
"product_ids": ["6VP24W005"],
"user_insight": {
"USER": "Nam, Adult (mua cho vợ).",
......
......@@ -7,7 +7,7 @@ load_dotenv(os.path.join(os.path.dirname(__file__), "..", "..", ".env"))
from langfuse import Langfuse
lf = Langfuse()
BASE = os.path.dirname(os.path.abspath(__file__))
BASE = os.path.dirname(os.path.abspath(__file__))
# 1. Push check_is_stock tool prompt
stock_path = os.path.join(BASE, "..", "tool_prompts", "check_is_stock.txt")
......
......@@ -134,7 +134,7 @@ def verify(lf: Langfuse):
# Check key section headings exist in assembled output
checks = [
("01 Identity (core)", "Canifa-AI Stylist"),
("01 Identity (core)", "C-Stylist"),
("02 Rules", "QUY TẮC TRUNG THỰC"),
("03 Context", "CONTEXT AWARENESS"),
("04a Sales Core", "PHONG CÁCH TƯ VẤN"),
......
"""Push all prompts (system + tools) to Langfuse Production"""
import os
import sys
import subprocess
# --- 1. SET ENVIRONMENT VARIABLES FOR PRODUCTION ---
os.environ["LANGFUSE_SECRET_KEY"] = "sk-lf-b20df146-732b-47cb-9669-47983906eb93"
os.environ["LANGFUSE_PUBLIC_KEY"] = "pk-lf-611d32d6-e8c7-4e06-b5b5-c3486513a4a2"
os.environ["LANGFUSE_BASE_URL"] = "http://172.16.2.207:3009"
# We must import Langfuse AFTER setting os.environ, or initialize explicitly
from langfuse import Langfuse
def push_tools(lf):
print("\n" + "="*60)
print("🚀 PUSHING TOOL PROMPTS TO PRODUCTION")
print("="*60)
TOOL_PROMPTS_DIR = r"d:\cnf\chatbot_canifa\backend\agent\tool_prompts"
# Matching the exact names used in backend/agent/prompt_utils.py
TOOL_FILES = [
("brand_knowledge_tool.txt", "canifa-tool-brand-knowledge", ["canifa", "tool-prompt"]),
("check_is_stock.txt", "canifa-tool-check-stock", ["canifa", "tool-prompt"]),
("data_retrieval_tool.txt", "canifa-tool-data-retrieval", ["canifa", "tool-prompt"]),
("promotion_canifa_tool.txt","canifa-tool-promotion", ["canifa", "tool-prompt"]),
("store_search_tool.txt", "canifa-tool-store-search", ["canifa", "tool-prompt"]),
]
for filename, langfuse_name, tags in TOOL_FILES:
path = os.path.join(TOOL_PROMPTS_DIR, filename)
with open(path, "r", encoding="utf-8") as f:
content = f.read()
lf.create_prompt(
name=langfuse_name,
prompt=content,
labels=["production"],
tags=tags,
type="text",
)
print(f" ✅ {filename:30s} → {langfuse_name} ({len(content):,} chars)")
lf.flush()
print("✅ Tool prompts pushed successfully!\n")
def push_system_prompts():
print("="*60)
print("🚀 PUSHING SYSTEM PROMPTS TO PRODUCTION")
print("="*60)
# We can just call the existing push_modules_to_langfuse.py script
# since we already exported the env vars, it will pick up the prod credentials.
script_dir = r"d:\cnf\chatbot_canifa\backend\agent\prompt_module"
script_path = os.path.join(script_dir, "push_modules_to_langfuse.py")
env = os.environ.copy()
subprocess.run([sys.executable, script_path], cwd=script_dir, env=env, check=True)
if __name__ == "__main__":
sys.stdout.reconfigure(encoding="utf-8")
print("🌟 DEPLOYING TO C-STYLIST PRODUCTION 🌟")
print(f"Target: {os.environ.get('LANGFUSE_BASE_URL')}")
# 1. Push System Prompts
push_system_prompts()
# 2. Push Tool Prompts
lf = Langfuse()
push_tools(lf)
print("🎉 ALL DEPLOYMENTS TO PRODUCTION FINISHED SUCCESSFULLY!")
......@@ -9,7 +9,7 @@ from pydantic import BaseModel, Field
class UserInsight(BaseModel):
"""6-layer User Insight structure as defined in system prompt."""
USER: str = Field(
default="Chưa rõ.",
description="Thông tin người chat: Giới tính, Người lớn/Trẻ em, Style/Gu"
......@@ -48,7 +48,7 @@ class ChatResponse(BaseModel):
Structured response from CANIFA AI Stylist.
This model enforces the exact JSON schema the LLM must follow.
"""
ai_response: str = Field(
description="Câu trả lời cho khách hàng. Phải ngắn gọn, thảo mai, nhắc SKU bằng [SKU]."
)
......
This diff is collapsed.
import json
import json
import logging
from langchain_core.tools import tool
......@@ -10,10 +10,8 @@ from common.starrocks_connection import get_db_connection
logger = logging.getLogger(__name__)
# Constants - Chỉ cần lấy Top 2-3 chunk vì ta sẽ mở rộng lấy full dữ liệu của title đó
TOP_K_CHUNKS = 5
class KnowledgeSearchInput(BaseModel):
query: str = Field(
description="Câu hỏi hoặc nhu cầu tìm kiếm thông tin phi sản phẩm của khách hàng (ví dụ: hỏi chính sách, tra bảng size...). KHÔNG DÙNG ĐỂ TÌM SẢN PHẨM. KHÔNG DÙNG ĐỂ TÌM CỬA HÀNG (dùng canifa_store_search)."
......@@ -29,85 +27,61 @@ async def canifa_knowledge_search(query: str) -> str:
"""
logger.info(f"[Semantic Search] Brand Knowledge query: {query}")
try:
# 1. Tạo embedding cho câu hỏi
# 1. Tạo embedding cho câu hỏi (cached 24h trong Redis)
query_vector = await create_embedding_async(query)
if not query_vector:
return "Xin lỗi, tôi gặp sự cố khi xử lý thông tin. Vui lòng thử lại sau."
v_str = json.dumps(query_vector)
# 2. Query StarRocks lấy Top K chunks phù hợp nhất
sql_search = f"""
SELECT
content,
metadata
# 2. SINGLE QUERY: Tìm top K chunks → lấy full document theo title (1 round-trip)
sql = f"""
WITH top_chunks AS (
SELECT json_query(metadata, '$.title') AS title
FROM shared_source.chatbot_rsa_knowledge
ORDER BY approx_cosine_similarity(embedding, {v_str}) DESC
LIMIT {TOP_K_CHUNKS}
)
SELECT k.content, k.metadata
FROM shared_source.chatbot_rsa_knowledge k
WHERE json_query(k.metadata, '$.title') IN (
SELECT DISTINCT title FROM top_chunks WHERE title IS NOT NULL
)
"""
sr = get_db_connection()
chunk_results = await sr.execute_query_async(sql_search)
results = await sr.execute_query_async(sql)
if not chunk_results:
if not results:
logger.warning(f"No knowledge data found in DB for query: {query}")
return "Hiện tại tôi chưa tìm thấy thông tin chính xác về nội dung này trong hệ thống kiến thức của Canifa. Bạn có thể liên hệ hotline 1800 6061 để được hỗ trợ trực tiếp."
# 3. Trích xuất danh sách các 'title' duy nhất từ metadata của các chunk tìm được
found_titles = set()
for res in chunk_results:
try:
meta_str = res.get("metadata", "{}")
meta_dict = json.loads(meta_str) if isinstance(meta_str, str) else meta_str
title_value = meta_dict.get("title") if isinstance(meta_dict, dict) else None
if title_value:
found_titles.add(title_value)
except Exception as e:
logger.error(f"Error parsing metadata: {e}")
continue
if not found_titles:
# Fallback nếu không có title nào, trả về chunk như cũ
knowledge_texts = [res.get("content", "") for res in chunk_results]
return "\n\n---\n\n".join(knowledge_texts)
logger.info(f"Semantic search found related titles: {list(found_titles)}")
# 4. Gom TOÀN BỘ nội dung của những title đã tìm thấy
# StarRocks JSON extract syntax: json_query(metadata, '$.title')
safe_titles = [str(t).replace("'", "''") for t in found_titles]
titles_sql_list = ", ".join([f"'{t}'" for t in safe_titles])
# 3. Phân nhóm nội dung theo title
docs_by_title: dict[str, list[str]] = {}
sql_full_docs = f"""
SELECT content, metadata
FROM shared_source.chatbot_rsa_knowledge
WHERE json_query(metadata, '$.title') IN ({titles_sql_list})
"""
full_doc_results = await sr.execute_query_async(sql_full_docs)
# 5. Phân nhóm nội dung theo title để context gọn gàng
docs_by_title = {t: [] for t in found_titles}
for res in full_doc_results or []:
for res in results:
try:
meta_obj = (
json.loads(res.get("metadata", "{}"))
if isinstance(res.get("metadata"), str)
else res.get("metadata", {})
)
title = meta_obj.get("title")
title = meta_obj.get("title") if isinstance(meta_obj, dict) else None
content = res.get("content", "").strip()
if title in docs_by_title and content:
docs_by_title[title].append(content)
if title and content:
docs_by_title.setdefault(title, []).append(content)
except Exception as e:
logger.debug(f"Skip invalid row while grouping by title: {e}")
# 6. Tổng hợp kết quả
if not docs_by_title:
# Fallback: trả raw content nếu không parse được title
knowledge_texts = [res.get("content", "") for res in results]
return "\n\n---\n\n".join(knowledge_texts)
# 4. Tổng hợp kết quả
final_blocks = []
for title, contents in docs_by_title.items():
if contents:
# Ghép tất cả các chunk thuộc cùng một title
full_text = "\n\n".join(contents)
final_blocks.append(f"=== TÀI LIỆU: {title} ===\n{full_text}")
......
......@@ -131,4 +131,4 @@ def resolve_product_line(raw_value: str) -> list[str]:
resolved.append(mapped)
else:
resolved.append(part)
return resolved
\ No newline at end of file
return resolved
......@@ -76,7 +76,7 @@ async def fashion_qa_chat(request: Request, req: QueryRequest, background_tasks:
content={
"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 ngay lúc này, vui lòng quay lại trong giây lát.",
"message": "Oops 😥 Hiện C-Stylist chưa thể xử lý yêu cầu của bạn ngay lúc này, vui lòng quay lại trong giây lát.",
},
)
......@@ -121,7 +121,7 @@ async def fashion_qa_chat_dev(request: Request, req: QueryRequest, background_ta
content={
"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 ngay lúc này, vui lòng quay lại trong giây lát.",
"message": "Oops 😥 Hiện C-Stylist chưa thể xử lý yêu cầu của bạn ngay lúc này, vui lòng quay lại trong giây lát.",
},
)
......
This diff is collapsed.
......@@ -8,7 +8,8 @@ import os
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
_BASE_DIR = os.path.dirname(os.path.abspath(__file__))
load_dotenv(os.path.join(_BASE_DIR, ".env"))
# Export all config variables for type checking
__all__ = [
......
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
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 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()
This diff is collapsed.
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
services:
# --- n8n Workflow Automation ---
n8n:
image: docker.n8n.io/n8nio/n8n:latest
image: docker.n8n.io/n8nio/n8n:1.123.4
container_name: canifa_n8n
ports:
- "5678:5678"
......
import asyncio
from common.starrocks_connection import get_db_connection
async def main():
sr = get_db_connection()
q = "SELECT metadata FROM shared_source.chatbot_rsa_knowledge LIMIT 5"
res = await sr.execute_query_async(q)
for i, r in enumerate(res):
print(f"METADATA {i}: {r.get('metadata')}")
asyncio.run(main())
"""Integration test for shipping-policy routing in agent chat.
Goal:
- Ask: "ship ve Hai Phong bao lau"
- Verify agent response includes shipping-policy facts
- Fail when response is hotline-only redirect
- Always print raw JSON for debugging
"""
from __future__ import annotations
import argparse
import json
import re
import sys
import time
import unicodedata
from typing import Any
import requests
DEFAULT_API_URL = "http://localhost:5000"
CHAT_ENDPOINT = "/api/agent/chat-dev"
DEFAULT_QUERY = "ship ve Hai Phong bao lau"
DEFAULT_TOKEN = "071w198x23ict4hs1i6bl889fit5p3f7"
DEFAULT_TIMEOUT = 90
# If answer only contains hotline escalation and no shipping-policy facts -> FAIL.
HOTLINE_PATTERNS = [
r"1800\s*6061",
r"hotline",
r"saleonline@canifa\.com",
r"lien he",
]
# Signals that answer used policy data instead of only escalation.
SHIPPING_FACT_PATTERNS = [
r"hai\s*phong",
r"phi\s*van\s*chuyen",
r"thoi\s*gian\s*giao\s*hang",
r"3\s*(den|-|to)\s*7\s*ngay",
r"30\.?000",
r"599\.?000",
r"freeship",
]
def _normalize(text: str) -> str:
raw = (text or "").strip().lower()
no_accents = "".join(ch for ch in unicodedata.normalize("NFD", raw) if unicodedata.category(ch) != "Mn")
return re.sub(r"\s+", " ", no_accents)
def _has_any(patterns: list[str], text: str) -> bool:
return any(re.search(pattern, text, flags=re.IGNORECASE) for pattern in patterns)
def _is_hotline_only_redirect(ai_response: str) -> bool:
text = _normalize(ai_response)
has_hotline = _has_any(HOTLINE_PATTERNS, text)
has_shipping_fact = _has_any(SHIPPING_FACT_PATTERNS, text)
return has_hotline and not has_shipping_fact
def run_shipping_policy_test(api_url: str, token: str, query: str, timeout: int) -> int:
url = f"{api_url}{CHAT_ENDPOINT}"
payload: dict[str, Any] = {"user_query": query, "images": []}
headers = {"Content-Type": "application/json"}
if token:
headers["Authorization"] = f"Bearer {token}"
print("=" * 72)
print("TEST: Agent shipping-policy routing")
print(f"URL : {url}")
print(f"QUERY : {query}")
print("=" * 72)
start = time.time()
try:
response = requests.post(url, json=payload, headers=headers, timeout=timeout)
except Exception as exc:
print(f"FAIL: request error: {exc}")
return 2
elapsed_ms = int((time.time() - start) * 1000)
print(f"HTTP : {response.status_code} ({elapsed_ms} ms)")
try:
data = response.json()
except Exception:
print("FAIL: response is not valid JSON")
print(response.text[:1000])
return 2
print("\nRAW JSON:")
print(json.dumps(data, ensure_ascii=False, indent=2))
if response.status_code != 200:
print("\nFAIL: non-200 response")
return 1
ai_response = str(data.get("ai_response", ""))
ai_norm = _normalize(ai_response)
hotline_only = _is_hotline_only_redirect(ai_response)
has_shipping_fact = _has_any(SHIPPING_FACT_PATTERNS, ai_norm)
print("\nCHECKS:")
print(f"- has shipping fact : {has_shipping_fact}")
print(f"- hotline-only redirect: {hotline_only}")
if hotline_only:
print("\nFAIL: agent returned hotline-only redirect for shipping-policy question")
return 1
if not has_shipping_fact:
print("\nFAIL: answer does not show shipping-policy facts")
return 1
print("\nPASS: agent returned shipping-policy information (not hotline-only)")
return 0
def main() -> None:
parser = argparse.ArgumentParser(description="Test shipping-policy question routing in agent")
parser.add_argument("--api-url", default=DEFAULT_API_URL, help=f"API base URL (default: {DEFAULT_API_URL})")
parser.add_argument("--token", default=DEFAULT_TOKEN, help="Bearer token for API (optional)")
parser.add_argument("--query", default=DEFAULT_QUERY, help=f"Question to test (default: {DEFAULT_QUERY})")
parser.add_argument(
"--timeout", type=int, default=DEFAULT_TIMEOUT, help=f"Request timeout seconds (default: {DEFAULT_TIMEOUT})"
)
args = parser.parse_args()
code = run_shipping_policy_test(
api_url=args.api_url,
token=args.token,
query=args.query,
timeout=args.timeout,
)
sys.exit(code)
if __name__ == "__main__":
main()
This diff is collapsed.
#!/usr/bin/env python3
"""Interactive QA loop from prebuilt FAISS index.
Run:
python test/a.py
"""
from __future__ import annotations
import argparse
import importlib.util
import io
import json
import os
import sys
from pathlib import Path
# Fix Windows terminal encoding for Vietnamese
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
sys.stdin = io.TextIOWrapper(sys.stdin.buffer, encoding="utf-8")
import numpy as np
from openai import OpenAI
try:
import faiss
except ImportError as exc:
raise SystemExit(
"faiss-cpu is required. Install with: pip install faiss-cpu"
) from exc
def parse_args() -> argparse.Namespace:
here = Path(__file__).resolve().parent
parser = argparse.ArgumentParser(
description="Query prebuilt FAISS index in a while loop"
)
parser.add_argument(
"--index", default=str(here / "index.faiss"), help="Path to FAISS index file"
)
parser.add_argument(
"--chunks", default=str(here / "chunks.jsonl"), help="Path to chunk jsonl file"
)
parser.add_argument("--top-k", type=int, default=5, help="Top-K retrieval")
parser.add_argument(
"--embedding-model",
default="text-embedding-3-small",
help="OpenAI embedding model",
)
parser.add_argument("--chat-model", default="gpt-4.1-mini", help="OpenAI chat model")
parser.add_argument(
"--min-score", type=float, default=0.35, help="Minimum score to trust retrieval"
)
return parser.parse_args()
def get_api_key() -> str | None:
backend_config = Path(__file__).resolve().parents[1] / "backend" / "config.py"
if backend_config.exists():
spec = importlib.util.spec_from_file_location("backend_config", backend_config)
if spec and spec.loader:
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
key = getattr(module, "OPENAI_API_KEY", None)
if key:
return key
return os.getenv("OPENAI_API_KEY")
def normalize(v: np.ndarray) -> np.ndarray:
n = np.linalg.norm(v)
if n == 0:
return v.astype(np.float32)
return (v / n).astype(np.float32)
def load_chunks(path: Path) -> list[dict]:
rows: list[dict] = []
with path.open("r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue
rows.append(json.loads(line))
return rows
def embed_query(client: OpenAI, text: str, model: str) -> np.ndarray:
response = client.embeddings.create(model=model, input=[text])
vec = np.array(response.data[0].embedding, dtype=np.float32)
return normalize(vec).reshape(1, -1)
def retrieve(
index: faiss.Index,
chunks: list[dict],
client: OpenAI,
query: str,
top_k: int,
embedding_model: str,
) -> list[dict]:
q_vec = embed_query(client, query, embedding_model)
scores, indices = index.search(q_vec, top_k)
hits: list[dict] = []
for score, idx in zip(scores[0], indices[0]):
if idx < 0 or idx >= len(chunks):
continue
row = chunks[int(idx)]
hits.append(
{
"chunk_id": row.get("chunk_id", int(idx)),
"score": float(score),
"token_start": row.get("token_start"),
"token_end": row.get("token_end"),
"text": row.get("text", ""),
}
)
return hits
def answer_from_hits(
client: OpenAI, query: str, hits: list[dict], chat_model: str
) -> str:
context = "\n\n".join(f"[Chunk {h['chunk_id']}]\n{h['text']}" for h in hits)
system_prompt = (
"Bạn là trợ lý AI của Canifa, chuyên hỗ trợ chính sách khách hàng. "
"Trả lời bằng tiếng Việt có dấu, tự nhiên và ngắn gọn. "
"Sử dụng thông tin từ các đoạn context được cung cấp. "
"Nếu câu hỏi là chào hỏi thông thường, hãy chào lại thân thiện và giới thiệu bạn có thể hỗ trợ gì. "
"Nếu không tìm thấy thông tin liên quan, hãy nói rõ và gợi ý các chủ đề bạn có thể hỗ trợ."
)
user_prompt = (
f"Câu hỏi:\n{query}\n\n"
f"Ngữ cảnh:\n{context}\n\n"
"Trả lời bằng tiếng Việt có dấu."
)
response = client.chat.completions.create(
model=chat_model,
temperature=0.2,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
],
)
return response.choices[0].message.content or ""
def main() -> int:
args = parse_args()
index_path = Path(args.index)
chunks_path = Path(args.chunks)
if not index_path.exists():
print(f"Missing index file: {index_path}")
return 1
if not chunks_path.exists():
print(f"Missing chunks file: {chunks_path}")
return 1
api_key = get_api_key()
if not api_key:
print("Missing OPENAI_API_KEY in backend/config.py or environment")
return 1
print(f"Loading FAISS index from: {index_path}")
index = faiss.read_index(str(index_path))
chunks = load_chunks(chunks_path)
print(f"Loaded index vectors: {index.ntotal}")
print(f"Loaded chunks: {len(chunks)}")
client = OpenAI(api_key=api_key)
print("\nInteractive mode is ready.")
print("Type exit or quit to stop.\n")
while True:
try:
query = input("Ask> ").strip()
except (EOFError, KeyboardInterrupt):
print("\nStopped.")
break
if not query:
continue
if query.lower() in {"exit", "quit"}:
print("Stopped.")
break
hits = retrieve(
index=index,
chunks=chunks,
client=client,
query=query,
top_k=args.top_k,
embedding_model=args.embedding_model,
)
print("Top-K hits:")
for h in hits:
print(
f" chunk={h['chunk_id']} score={h['score']:.4f} "
f"tokens[{h['token_start']}:{h['token_end']}]"
)
if not hits:
print("Answer:")
print("Không tìm thấy dữ liệu liên quan. Bạn thử hỏi về đổi hàng, hạng thẻ Gold/Diamond, tích điểm hoặc ưu đãi sinh nhật nhé.")
print()
continue
try:
answer = answer_from_hits(
client=client,
query=query,
hits=hits,
chat_model=args.chat_model,
)
print("Answer:")
print(answer)
except Exception as exc:
print(f"Answer error: {exc}")
print()
return 0
if __name__ == "__main__":
raise SystemExit(main())
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