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>
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
...@@ -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 diff is collapsed.
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()
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
...
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
[('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