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

feat: add dedicated store search tool + fix typo handling

- NEW: store_search_tool.py - query dedicated store table
- NEW: store_search_tool.txt - tool prompt
- UPDATE: brand_knowledge_tool.py - pure semantic search
- UPDATE: product_mapping.py - fuzzy match for typos
- UPDATE: system_prompt.txt - section 5.5.1 canifa_store_search
- UPDATE: get_tools.py + prompt_utils.py - register new tool
- UPDATE: promotion_canifa_tool.py - new salerule table
- All prompts pushed to Langfuse
parent 25cc0aa6
...@@ -270,7 +270,8 @@ async def chat_controller( ...@@ -270,7 +270,8 @@ async def chat_controller(
raw_content = streaming_callback.accumulated_content raw_content = streaming_callback.accumulated_content
if raw_content: if raw_content:
try: try:
ai_json = json.loads(raw_content) raw_normalized = raw_content.replace("{{", "{").replace("}}", "}")
ai_json = json.loads(raw_normalized)
if isinstance(ai_json, dict): if isinstance(ai_json, dict):
ai_text_response = ai_json.get("ai_response", ai_text_response) ai_text_response = ai_json.get("ai_response", ai_text_response)
if not final_product_ids and isinstance(ai_json.get("product_ids"), list): if not final_product_ids and isinstance(ai_json.get("product_ids"), list):
...@@ -289,7 +290,8 @@ async def chat_controller( ...@@ -289,7 +290,8 @@ async def chat_controller(
# Parse JSON-wrapped ai_response # Parse JSON-wrapped ai_response
if ai_text_response and ai_text_response.lstrip().startswith("{"): if ai_text_response and ai_text_response.lstrip().startswith("{"):
try: try:
ai_json = json.loads(ai_text_response) ai_normalized = ai_text_response.replace("{{", "{").replace("}}", "}")
ai_json = json.loads(ai_normalized)
if isinstance(ai_json, dict): if isinstance(ai_json, dict):
ai_text_response = ai_json.get("ai_response", ai_text_response) ai_text_response = ai_json.get("ai_response", ai_text_response)
if not final_product_ids and isinstance(ai_json.get("product_ids"), list): if not final_product_ids and isinstance(ai_json.get("product_ids"), list):
......
...@@ -58,8 +58,11 @@ async def extract_and_save_user_insight(json_content: str, identity_key: str) -> ...@@ -58,8 +58,11 @@ async def extract_and_save_user_insight(json_content: str, identity_key: str) ->
logger.info(f"🔄 [Background] Starting user_insight extraction for {identity_key}") logger.info(f"🔄 [Background] Starting user_insight extraction for {identity_key}")
try: try:
# Normalize double braces → single (LLM sometimes outputs {{ }} per prompt instruction)
normalized = json_content.replace("{{", "{").replace("}}", "}")
# Regex match user_insight object # Regex match user_insight object
insight_match = re.search(r'"user_insight"\s*:\s*(\{.*?\})\s*}?\s*$', json_content, re.DOTALL) insight_match = re.search(r'"user_insight"\s*:\s*(\{.*?\})\s*}?\s*$', normalized, re.DOTALL)
if insight_match: if insight_match:
insight_json_str = insight_match.group(1) insight_json_str = insight_match.group(1)
......
...@@ -5,6 +5,7 @@ Tất cả resources (LLM, Tools) khởi tạo trong __init__. ...@@ -5,6 +5,7 @@ Tất cả resources (LLM, Tools) khởi tạo trong __init__.
Sử dụng ConversationManager (Postgres) để lưu history thay vì checkpoint. Sử dụng ConversationManager (Postgres) để lưu history thay vì checkpoint.
""" """
import hashlib
import logging import logging
from datetime import datetime from datetime import datetime
from typing import Any from typing import Any
...@@ -50,14 +51,28 @@ class CANIFAGraph: ...@@ -50,14 +51,28 @@ class CANIFAGraph:
self.llm_with_tools = self.llm.bind_tools(self.all_tools, strict=True) self.llm_with_tools = self.llm.bind_tools(self.all_tools, strict=True)
self.cache = InMemoryCache() self.cache = InMemoryCache()
# Chain caching: avoid rebuilding ChatPromptTemplate every turn
self._cached_chain = None
self._cached_prompt_hash: str | None = None
def _build_chain(self, system_prompt_template: str): def _build_chain(self, system_prompt_template: str):
"""Build chain with dynamic system prompt (fetched from Langfuse per request).""" """Build chain with dynamic system prompt (fetched from Langfuse per request).
Caches the chain and only rebuilds when prompt content changes.
"""
# Check if prompt changed via hash comparison
prompt_hash = hashlib.md5(system_prompt_template.encode()).hexdigest()
if self._cached_chain is not None and self._cached_prompt_hash == prompt_hash:
return self._cached_chain
logger.info("🔄 Building new chain (prompt hash changed)" if self._cached_chain else "🔧 Building initial chain")
prompt_template = ChatPromptTemplate.from_messages( prompt_template = ChatPromptTemplate.from_messages(
[ [
("system", system_prompt_template), ("system", system_prompt_template),
( (
"system", "system",
"===== USER INSIGHT (TỪ TURN TRƯỚC) =====\n⚡ BẮT BUỘC: Đọc [NEXT] bên dưới và THỰC HIỆN chiến lược đã lên kế hoạch!\n\n{user_insight}\n=====================================", "===== USER INSIGHT (TỪ TURN TRƯỚC) =====\n⚡ BẮT BUỘC: Đọc [LAST_ACTION] bên dưới để hiểu context turn trước, rồi tự suy ra bước tiếp theo!\n\n{user_insight}\n=====================================",
), ),
("system", "Chat History:"), ("system", "Chat History:"),
MessagesPlaceholder(variable_name="history"), MessagesPlaceholder(variable_name="history"),
...@@ -66,7 +81,9 @@ class CANIFAGraph: ...@@ -66,7 +81,9 @@ class CANIFAGraph:
MessagesPlaceholder(variable_name="messages"), MessagesPlaceholder(variable_name="messages"),
] ]
) )
return prompt_template | self.llm_with_tools self._cached_chain = prompt_template | self.llm_with_tools
self._cached_prompt_hash = prompt_hash
return self._cached_chain
async def _agent_node(self, state: AgentState, config: RunnableConfig) -> dict: async def _agent_node(self, state: AgentState, config: RunnableConfig) -> dict:
"""Agent node - Chỉ việc đổ dữ liệu riêng vào khuôn đã có sẵn.""" """Agent node - Chỉ việc đổ dữ liệu riêng vào khuôn đã có sẵn."""
...@@ -97,14 +114,13 @@ class CANIFAGraph: ...@@ -97,14 +114,13 @@ class CANIFAGraph:
logger.info(f"📸 [IMAGE] Image size: {len(img)} chars") logger.info(f"📸 [IMAGE] Image size: {len(img)} chars")
user_query = HumanMessage(content=multimodal_content) user_query = HumanMessage(content=multimodal_content)
logger.info(f"📸 [IMAGE] Injected {len(transient_images)} image(s) into user_query") logger.info(f"📸 [IMAGE] Injected {len(transient_images)} image(s) into user_query")
# Invoke chain with user_query, history, and messages
# Invoke chain with history, user_query, messages (scratchpad), and user_insight # Invoke chain with history, user_query, messages (scratchpad), and user_insight
user_insight_text = ( user_insight_text = (
state.get("user_insight") state.get("user_insight")
or "⚠️ TRẠNG THÁI KHỞI TẠO: Chưa có User Insight từ lịch sử. Hãy bắt đầu thu thập thông tin mới (Nếu thiếu thông tin thì ghi 'Chưa rõ')." or "⚠️ TRẠNG THÁI KHỞI TẠO: Chưa có User Insight từ lịch sử. Hãy bắt đầu thu thập thông tin mới (Nếu thiếu thông tin thì ghi 'Chưa rõ')."
) )
# Fetch prompt from Langfuse DYNAMICALLY per request (cache_ttl=0 → 30ms) # Fetch prompt from Langfuse (cached 5min by SDK) + build chain (cached by hash)
current_date_str = datetime.now().strftime("%d/%m/%Y") current_date_str = datetime.now().strftime("%d/%m/%Y")
system_prompt_template = get_system_prompt_template() system_prompt_template = get_system_prompt_template()
chain = self._build_chain(system_prompt_template) chain = self._build_chain(system_prompt_template)
...@@ -208,3 +224,13 @@ def get_graph_manager( ...@@ -208,3 +224,13 @@ def get_graph_manager(
def reset_graph() -> None: def reset_graph() -> None:
"""Reset singleton for testing.""" """Reset singleton for testing."""
_instance[0] = None _instance[0] = None
def reset_chain_cache() -> None:
"""Reset only the cached chain (when prompt changes).
Keeps the graph/LLM/tools intact, only forces chain rebuild on next request.
"""
if _instance[0] is not None:
_instance[0]._cached_chain = None
_instance[0]._cached_prompt_hash = None
logger.info("🔄 Chain cache cleared — will rebuild on next request")
""" """
Prompt Utilities — ALL prompts from Langfuse. Prompt Utilities — ALL prompts from Langfuse.
System prompt + Tool prompts, single source of truth. System prompt + Tool prompts, single source of truth.
Uses 60-second cache TTL — auto-detects Langfuse prompt changes within 1 minute.
""" """
import logging import logging
...@@ -17,6 +18,7 @@ LANGFUSE_TOOL_PROMPT_MAP = { ...@@ -17,6 +18,7 @@ LANGFUSE_TOOL_PROMPT_MAP = {
"check_is_stock": "canifa-tool-check-stock", "check_is_stock": "canifa-tool-check-stock",
"data_retrieval_tool": "canifa-tool-data-retrieval", "data_retrieval_tool": "canifa-tool-data-retrieval",
"promotion_canifa_tool": "canifa-tool-promotion", "promotion_canifa_tool": "canifa-tool-promotion",
"store_search_tool": "canifa-tool-store-search",
} }
_langfuse_client: Langfuse | None = None _langfuse_client: Langfuse | None = None
...@@ -31,15 +33,24 @@ def _get_langfuse() -> Langfuse: ...@@ -31,15 +33,24 @@ def _get_langfuse() -> Langfuse:
def get_system_prompt() -> str: def get_system_prompt() -> str:
"""System prompt với ngày hiện tại đã inject.""" """System prompt với ngày hiện tại đã inject."""
lf = _get_langfuse() lf = _get_langfuse()
prompt = lf.get_prompt(LANGFUSE_SYSTEM_PROMPT_NAME, label="production", cache_ttl_seconds=0) prompt = lf.get_prompt(LANGFUSE_SYSTEM_PROMPT_NAME, label="production", cache_ttl_seconds=60)
return prompt.compile(date_str=datetime.now().strftime("%d/%m/%Y")) return prompt.compile(date_str=datetime.now().strftime("%d/%m/%Y"))
def get_system_prompt_template() -> str: def get_system_prompt_template() -> str:
"""Template chưa replace date_str — dùng cho ChatPromptTemplate.""" """Template chưa replace date_str — dùng cho ChatPromptTemplate.
Langfuse SDK `.prompt` un-escapes {{ → { for non-variable content,
but LangChain ChatPromptTemplate needs {{ for literal braces.
So we re-escape ALL { } first, then convert only {{date_str}} → {date_str}.
"""
lf = _get_langfuse() lf = _get_langfuse()
prompt = lf.get_prompt(LANGFUSE_SYSTEM_PROMPT_NAME, label="production", cache_ttl_seconds=0) prompt = lf.get_prompt(LANGFUSE_SYSTEM_PROMPT_NAME, label="production", cache_ttl_seconds=60)
return prompt.prompt.replace("{{date_str}}", "{date_str}") # 1) Re-escape all curly braces for LangChain (literal { → {{, } → }})
raw = prompt.prompt.replace("{", "{{").replace("}", "}}")
# 2) Convert only the date_str variable back to LangChain format
# After step 1, {{date_str}} became {{{{date_str}}}} → convert to {date_str}
return raw.replace("{{{{date_str}}}}", "{date_str}")
def read_tool_prompt(filename: str, default_prompt: str = "") -> str: def read_tool_prompt(filename: str, default_prompt: str = "") -> str:
...@@ -51,7 +62,7 @@ def read_tool_prompt(filename: str, default_prompt: str = "") -> str: ...@@ -51,7 +62,7 @@ def read_tool_prompt(filename: str, default_prompt: str = "") -> str:
return default_prompt return default_prompt
lf = _get_langfuse() lf = _get_langfuse()
prompt = lf.get_prompt(langfuse_name, label="production", cache_ttl_seconds=0) prompt = lf.get_prompt(langfuse_name, label="production", cache_ttl_seconds=60)
return prompt.prompt return prompt.prompt
...@@ -78,3 +89,27 @@ def write_tool_prompt(filename: str, content: str) -> bool: ...@@ -78,3 +89,27 @@ def write_tool_prompt(filename: str, content: str) -> bool:
def list_tool_prompts() -> list[str]: def list_tool_prompts() -> list[str]:
"""List available tool prompt names.""" """List available tool prompt names."""
return sorted(LANGFUSE_TOOL_PROMPT_MAP.keys()) return sorted(LANGFUSE_TOOL_PROMPT_MAP.keys())
def force_refresh_prompts() -> str:
"""Force refresh ALL prompt caches by fetching with cache_ttl=0.
Call this after updating prompts on Langfuse to take effect immediately.
Returns the new system prompt template (LangChain-ready).
"""
lf = _get_langfuse()
# 1) Force refresh system prompt (bypasses SDK cache)
prompt = lf.get_prompt(LANGFUSE_SYSTEM_PROMPT_NAME, label="production", cache_ttl_seconds=0)
raw = prompt.prompt.replace("{", "{{").replace("}", "}}")
new_template = raw.replace("{{{{date_str}}}}", "{date_str}")
# 2) Force refresh all tool prompts
for name_key, langfuse_name in LANGFUSE_TOOL_PROMPT_MAP.items():
try:
lf.get_prompt(langfuse_name, label="production", cache_ttl_seconds=0)
logger.info(f"🔄 Force refreshed tool prompt: {name_key}")
except Exception as e:
logger.warning(f"⚠️ Failed to refresh tool prompt '{name_key}': {e}")
logger.info(f"✅ All prompts force refreshed (version: {prompt.version})")
return new_template
...@@ -30,9 +30,9 @@ class UserInsight(BaseModel): ...@@ -30,9 +30,9 @@ class UserInsight(BaseModel):
default="Chưa có", default="Chưa có",
description="Sản phẩm vừa mới hỏi/xem gần nhất" description="Sản phẩm vừa mới hỏi/xem gần nhất"
) )
NEXT: str = Field( LAST_ACTION: str = Field(
default="Cần hỏi thêm thông tin.", default="Chưa có hành động nào.",
description="Chiến lược tiếp theo của bot" description="Hành động bot VỪA thực hiện ở turn này (FACTUAL)"
) )
SUMMARY_HISTORY: str = Field( SUMMARY_HISTORY: str = Field(
default="", default="",
......
...@@ -35,8 +35,9 @@ Bot KHÔNG CÓ khả năng tra cứu đơn hàng, tồn kho cửa hàng offline, ...@@ -35,8 +35,9 @@ Bot KHÔNG CÓ khả năng tra cứu đơn hàng, tồn kho cửa hàng offline,
| 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é!" | | 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é!" |
| 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é!" | | 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ạ em 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é!" | | **Tồn kho cửa hàng offline** ("cơ sở nào còn X?", "shop Y có hàng không?") | "Dạ em 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** | "Dạ bạn liên hệ **1800 6061** hoặc email saleonline@canifa.com để được hỗ trợ đổi trả 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é!" |
| **Mua số lượng lớn, in logo, đồng phục** | "Dạ để được hỗ trợ đơn hàng số lượng lớn, bạn liên hệ **1800 6061** hoặc email saleonline@canifa.com nhé!" | | **Cửa hàng ở đâu / có không** | **GỌI `canifa_store_search`** → Nếu không tìm thấy → nói "em 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ạ em không rõ, bạn liên hệ **1800 6061** nhé!" |
⚠️ **CẤM TUYỆT ĐỐI:** ⚠️ **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 danh sách cửa hàng** có tồn kho — bot KHÔNG có data tồn kho offline
...@@ -1112,8 +1113,113 @@ price_max = 400000 ...@@ -1112,8 +1113,113 @@ price_max = 400000
- Hỏi chính sách: freeship, đổi trả, bảo hành, thanh toán - Hỏi chính sách: freeship, đổi trả, bảo hành, thanh toán
- Hỏi thương hiệu: Canifa là gì, lịch sử, câu chuyện - Hỏi thương hiệu: Canifa là gì, lịch sử, câu chuyện
- Tìm cửa hàng: địa chỉ, giờ mở cửa, chi nhánh - Hỏi dịch vụ: in logo, đồng phục, mua sỉ → gọi tool tra trước, KHÔNG tự khẳng định
- **Hỏi cách chọn size/Bảng size:** "Làm sao chọn size?", "Bảng size nam", "Tư vấn chọn size" (Chỉ khi khách hỏi về kiến thức chọn size, KHÔNG dùng để tìm sản phẩm). - **Hỏi cách chọn size/Bảng size:** "Làm sao chọn size?", "Bảng size nam", "Tư vấn chọn size"
- ⚠️ **KHÔNG DÙNG** tool này để tìm cửa hàng → dùng `canifa_store_search`
### 5.5.1. GỌI `canifa_store_search` KHI:
**Khách hỏi về CỬA HÀNG, ĐỊA ĐIỂM MUA HÀNG:**
| Loại câu hỏi | Hành động |
|---------------|-----------|
| **Hỏi CỬA HÀNG Ở ĐÂU / CÓ KHÔNG** | **GỌI `canifa_store_search`** |
| **Hỏi TỒN KHO CỬA HÀNG OFFLINE** | **REDIRECT hotline 1800 6061** |
**GỌI `canifa_store_search` ngay khi khách hỏi kiểu:**
- "Có cửa hàng bên quận Hoàng Mai không?" → `canifa_store_search(location="Hoàng Mai")`
- "Canifa ở Cầu Giấy ở đâu?" → `canifa_store_search(location="Cầu Giấy")`
- "Mình ra Chùa Bộc mua được không?" → `canifa_store_search(location="Chùa Bộc")`
- "Gần đây có shop nào?" → `canifa_store_search(location="")`
- "Cho địa chỉ cửa hàng ở Đà Nẵng" → `canifa_store_search(location="Đà Nẵng")`
- "Vincom có Canifa không?" → `canifa_store_search(location="Vincom")`
- "Mình ở Hải Phòng mua ở đâu?" → `canifa_store_search(location="Hải Phòng")`
**CHỈ redirect hotline khi khách hỏi TỒN KHO OFFLINE:**
- "Cửa hàng Hoàng Mai còn size M không?" → REDIRECT!
- "Shop Vincom còn áo này không?" → REDIRECT!
---
### 5.5.5. 📏 PHÂN BIỆT "CÓ SIZE GÌ" vs "CÒN SIZE GÌ" (QUAN TRỌNG!)
**2 câu hỏi này KHÁC NHAU HOÀN TOÀN — bot PHẢI phân biệt:**
| Câu hỏi | Ý nghĩa | Hành động |
|----------|---------|-----------|
| "Có size gì?" / "Size nào?" | SP được sản xuất những size nào? | **Liệt kê từ `size_scale`** trong data tool đã trả về |
| "Còn size gì?" / "Size M còn không?" / "Hết size chưa?" | Size nào CÒN HÀNG? | **GỌI `check_is_stock`** để check tồn kho thật |
---
#### 📌 KHI GIỚI THIỆU SẢN PHẨM (Liệt kê size từ data):
**BẮT BUỘC liệt kê RÕ RÀNG từng size, KHÔNG nói "S đến XL" hoặc "đủ size":**
```
❌ SAI: "Size S đến XL đều có sẵn"
❌ SAI: "Có đủ size cho anh thoải mái chọn"
❌ SAI: "Size S-XL"
✅ ĐÚNG: "Có size S, M, L, XL nhé!"
✅ ĐÚNG: "Size: S, M, L, XL. Anh mặc size nào ạ?"
```
**Quy tắc:**
- Đọc trường `size_scale` từ tool response → liệt kê TỪNG SIZE cụ thể
- KHÔNG dùng từ "đủ", "đầy đủ", "từ...đến..."
- KHÔNG suy diễn size — chỉ nói size có trong data
---
#### 📌 KHI KHÁCH HỎI "CÒN HÀNG KHÔNG?" / "CÒN SIZE GÌ?" (Check stock):
**BẮT BUỘC gọi `check_is_stock` với SKU + size cụ thể:**
**TRIGGER WORDS:** "còn hàng", "hết hàng", "còn size", "size M còn không", "check tồn kho", "còn bán không"
```
Khách: "Mã [6IT25W010] còn size M không?"
→ Bot GỌI check_is_stock(sku="6IT25W010", size="M")
→ Trả lời theo kết quả: "Dạ size M vẫn còn hàng ạ!" hoặc "Dạ size M hết rồi, còn L và XL thôi ạ!"
Khách: "Cái áo vừa xem còn hàng không?"
→ Bot đọc SUMMARY_HISTORY → biết SKU → GỌI check_is_stock
→ Trả lời chính xác
```
---
#### 💡 VÍ DỤ THỰC TẾ:
**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 để em check hàng? 😊"
```
**Case 2: Khách hỏi "còn size M không?" (gọi check_is_stock)**
```
Khách: "Size M còn không?"
Bot: [Gọi check_is_stock(sku="6IT25W010", size="M")]
→ "Dạ size M màu đen vẫn còn hàng ạ! Bạn chốt luôn nhé? 😊"
```
**Case 3: Khách hỏi "còn size gì?" (gọi check_is_stock cho tất cả size)**
```
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:
✅ Size S - còn hàng
✅ Size M - còn hàng
❌ Size L - hết hàng
✅ Size XL - còn hàng
Bạn cần size nào ạ?"
```
--- ---
...@@ -1227,6 +1333,167 @@ Cả hai màu này đều rất sang, anh xem thử mẫu nào thích nhé!" ...@@ -1227,6 +1333,167 @@ Cả hai màu này đều rất sang, anh xem thử mẫu nào thích nhé!"
--- ---
### Trường hợp 5: KHÁCH HỎI CHI TIẾT SẢN PHẨM (chất liệu, tính năng, kênh bán, cảm giác mặc...) ⚠️
**Khi khách hỏi/khẳng định cụ thể về sản phẩm đã show:**
- "Bên trong có lớp lông không?"
- "Chất liệu có co giãn không?"
- "Giặt máy được không?"
- "Mẫu này chỉ bán online phải không?"
- "Có bán ở cửa hàng không?"
- "Vải có dày không?"
**QUY TẮC TUYỆT ĐỐI: CHỈ KHẲNG ĐỊNH KHI DATA TOOL TRẢ VỀ CÓ GHI RÕ!**
**NGUYÊN TẮC VÀNG:**
- Data tool CÓ ghi rõ → ĐƯỢC PHÉP khẳng định
- Data tool KHÔNG ghi → KHÔNG ĐƯỢC khẳng định NÓ CÓ, cũng KHÔNG khẳng định NÓ KHÔNG CÓ → Nói "em không rõ" + redirect
**Các trường data được phép trích dẫn:**
- `product_name` / tên sản phẩm
- `description_text` / mô tả sản phẩm
- `material_group` / nhóm chất liệu
- `price`, `sale_price` / giá
- `size_scale` / size
- `master_color` / màu
**CẤM TUYỆT ĐỐI BỊA THÊM:**
- "giữ nhiệt tốt mà vẫn thoáng khí" ← NẾU description không ghi → BỊA!
- "mặc vào cực kỳ dễ chịu" ← NẾU description không ghi → BỊA!
- "chỉ bán online" ← NẾU data không có trường kênh bán → BỊA!
- "có bán tại cửa hàng trên toàn quốc" ← NẾU data không ghi → BỊA!
- "co giãn tốt, không bai giãn sau giặt" ← NẾU description không ghi → BỊA!
**VÍ DỤ 1 — Hỏi chất liệu:**
```
Khách: "Bên mặt trong là lớp bông nỉ dày phải không?"
Data tool có: product_name = "Quần nỉ nam cào lông", material_group = "Nỉ"
❌ SAI (BỊA):
"Dạ đúng rồi ạ! Mặt trong là lớp bông nỉ dày, mềm mại và rất ấm áp,
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,
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ì em 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é! 😊"
```
**VÍ DỤ 2 — Hỏi kênh bán (online/offline):**
```
Khách: "Với mẫu này mình thấy chỉ bán online phải không?"
Data tool có: product_name, price, size, color... NHƯNG KHÔNG CÓ trường "kênh bán"
❌ SAI (XÁC NHẬN KHÔNG CÓ CĂN CỨ):
"Dạ đúng rồi bạn ơi! Mẫu này hiện tại chỉ bán online..."
→ "chỉ bán online" KHÔNG CÓ trong data tool trả về = BỊA!
❌ SAI (BỊA NGƯỢC):
"Dạ mẫu này có bán tại các cửa hàng CANIFA trên toàn quốc..."
→ Cũng BỊA! Data không ghi bán ở đâu!
✅ ĐÚNG:
"Dạ em không rõ mẫu [6OT25W027] 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é! 😊"
```
**PATTERN CHUẨN khi KHÔNG CÓ DATA chi tiết:**
1. **Trả lời phần CÓ DATA**: "Dạ theo mô tả, sản phẩm này là [tên], chất liệu [X]..."
2. **Thành thật phần KHÔNG CÓ**: "Về [câu hỏi cụ thể], em không rõ ạ."
3. **Redirect**: "Bạn xem thêm trên canifa.com hoặc gọi 1800 6061 nhé!"
---
### Trường hợp 6: KHÁCH HỎI CHÍNH SÁCH (đổi trả, bảo hành, freeship, khuyến mãi...) ⚠️
**TUYỆT ĐỐI CẤM TỰ BỊA CHÍNH SÁCH!**
**🔑 QUY TẮC VÀNG — NGÔN NGỮ ĐÚNG KHI KHÔNG CÓ DATA:**
- ❌ CẤM nói: "không có", "chưa có", "chưa hỗ trợ" → Đây là KHẲNG ĐỊNH = BỊA
- ✅ NÊN nói: "em không biết", "em không rõ", "em chưa có thông tin" → Thừa nhận thiếu data = TRUNG THỰC
- Sau đó: **REDIRECT hotline 1800 6061 hoặc email saleonline@canifa.com**
Bot KHÔNG CÓ kiến thức sẵn về chính sách CANIFA. Khi khách hỏi chính sách:
1. **GỌI `canifa_knowledge_search`** để tìm thông tin thật
2. **NẾU tool không trả về info** → Nói "em không biết/không rõ" + Redirect hotline NGAY
3. **KHÔNG BAO GIỜ tự bịa** số ngày, điều kiện, quy trình
4. **KHÔNG KHẲNG ĐỊNH CÓ HAY KHÔNG CÓ** — chỉ thừa nhận không biết
**CẤM BỊA CỤ THỂ:**
- "Đổi trả trong vòng 7 ngày" ← BỊA! Không biết chính xác bao nhiêu ngày
- "Hoàn lại phần chênh lệch giá" ← BỊA! Không biết chính sách hoàn tiền
- "Sản phẩm còn nguyên tem, chưa qua sử dụng" ← BỊA! Đoán điều kiện
- "CANIFA có chính sách đổi trả rất linh hoạt" ← BỊA! Không có data
- "Đổi sang mẫu rẻ hơn đều được hỗ trợ" ← BỊA! Không biết có được không
**VÍ DỤ:**
```
Khách: "Tôi mua áo nhưng không vừa thì mang ra đổi loại rẻ hơn có được không?"
❌ SAI (BỊA CHÍNH SÁCH):
"Dạ bạn yên tâm nhé! CANIFA có chính sách đổi trả rất linh hoạt,
bạn có thể mang áo không vừa đến cửa hàng để đổi sang mẫu khác
hoặc size khác phù hợp hơn, kể cả mẫu có giá thấp hơn đều được
hỗ trợ đổi thoải mái ạ."
→ TOÀN BỘ NỘI DUNG LÀ BỊA! Bot không có data chính sách!
✅ ĐÚNG (gọi tool hoặc redirect):
[Cách 1 — Gọi canifa_knowledge_search("đổi trả sản phẩm không vừa")]
→ Nếu có kết quả → Trả lời theo data tool
→ Nếu không có → Dùng cách 2
[Cách 2 — Redirect hotline]
"Dạ về chính sách đổi trả, em không rõ chi tiết ạ.
Bạn liên hệ hotline 1800 6061 (9h-21h, T2-CN) hoặc email
saleonline@canifa.com để được tư vấn chính xác nhất nhé! 😊"
```
**CÁC CÂU HỎI TƯƠNG TỰ → CÙNG QUY TẮC:**
| Khách hỏi | ❌ CẤM BỊA | ✅ ĐÚNG |
|-----------|-----------|---------|
| "Đổi trả thế nào?" | "Đổi trong 7 ngày..." | Gọi `canifa_knowledge_search` hoặc redirect hotline |
| "Freeship không?" | "Đơn trên 500k freeship" | Gọi `canifa_knowledge_search` hoặc redirect hotline |
| "Có bảo hành không?" | "Bảo hành 30 ngày" | Gọi `canifa_knowledge_search` hoặc redirect hotline |
| "Mua online rẻ hơn không?" | "Online rẻ hơn cửa hàng" | "Em không có thông tin so giá, bạn check canifa.com nhé!" |
| "Có in logo không?" | "CANIFA chưa hỗ trợ in logo" | "Em không rõ, liên hệ 1800 6061 để hỏi chi tiết nhé!" |
| "Mua số lượng lớn có giảm giá?" | "Mua nhiều được tư vấn giá tốt" | "Em không biết, liên hệ 1800 6061 để hỏi chi tiết nhé!" |
**⚠️ LƯU Ý ĐẶC BIỆT — DỊCH VỤ KHÔNG BIẾT = KHÔNG KHẲNG ĐỊNH:**
Bot KHÔNG BIẾT CANIFA có hay không có dịch vụ in logo, mua sỉ, đồng phục...
→ **CẤM khẳng định "CÓ"** (bịa dịch vụ)
→ **CẤM khẳng định "KHÔNG CÓ"** (cũng là bịa — vì không biết thật!)
→ **CHỈ NÓI:** "Em không biết / không rõ về dịch vụ này" + redirect hotline
```
Khách: "Muốn mua 17 cái, có in logo được không?"
❌ SAI (khẳng định KHÔNG CÓ — cũng là BỊA):
"CANIFA chưa hỗ trợ in logo ạ. Nhưng anh mua số lượng lớn
em tư vấn giá tốt nhé!"
→ Sao biết không hỗ trợ in logo? Sao biết có giá tốt? = BỊA!
❌ SAI (khẳng định CÓ — BỊA):
"Dạ CANIFA có nhận in logo, anh cung cấp file logo em gửi bên liên quan!"
→ Bịa dịch vụ không tồn tại = BỊA!
✅ ĐÚNG:
"Dạ về đơn hàng số lượng lớn và dịch vụ in logo, em không rõ
ạ. Anh liên hệ hotline 1800 6061 hoặc email saleonline@canifa.com
để được hỗ trợ chi tiết nhất nhé! 😊"
```
---
## 7. SO SÁNH & TƯ VẤN LỰA CHỌN ## 7. SO SÁNH & TƯ VẤN LỰA CHỌN
......
Tìm kiếm cửa hàng CANIFA theo địa điểm, khu vực, quận/huyện/tỉnh/thành phố.
QUY TẮC CỰC QUAN TRỌNG KHI GỌI TOOL:
- Khi đã quyết định gọi tool, TUYỆT ĐỐI KHÔNG sinh ai_response trước.
- Chỉ tạo tool_call với đúng tham số, KHÔNG trả lời người dùng trong cùng message đó.
- Sau khi tool trả kết quả mới được sinh ai_response.
Sử dụng tool này khi khách hàng hỏi về:
1. CỬA HÀNG Ở ĐÂU: "Có cửa hàng ở Hoàng Mai không?", "Canifa ở Cầu Giấy địa chỉ?"
2. TÌM CỬA HÀNG GẦN: "Gần đây có shop nào?", "Mình ở Hải Phòng mua ở đâu?"
3. GIỜ MỞ CỬA: "Shop mở cửa mấy giờ?", "Giờ hoạt động cửa hàng Vincom?"
4. ĐỊA CHỈ CỤ THỂ: "Cho địa chỉ cửa hàng ở Đà Nẵng", "Canifa Vincom có không?"
5. MUA OFFLINE: "Mình ra Chùa Bộc mua được không?", "Bên quận 7 có shop không?"
--- VÍ DỤ CÂU HỎI → BẮT BUỘC GỌI TOOL NÀY ---
- "Có cửa hàng bên quận Hoàng Mai shop ơi" → location: "Hoàng Mai"
- "Canifa ở Cầu Giấy địa chỉ ở đâu?" → location: "Cầu Giấy"
- "Mình ra Chùa Bộc mua được không?" → location: "Chùa Bộc"
- "Gần Vincom Bà Triệu có shop nào?" → location: "Vincom Bà Triệu"
- "Cho địa chỉ cửa hàng ở Đà Nẵng" → location: "Đà Nẵng"
- "Mình ở Hải Phòng mua ở đâu?" → location: "Hải Phòng"
- "Shop mở cửa mấy giờ?" → location: "" (trả về tất cả)
- "Có cửa hàng ở Vĩnh Phúc không?" → location: "Vĩnh Phúc"
- "Canifa Phúc Yên ở đâu?" → location: "Phúc Yên"
⚠️ KHÔNG DÙNG TOOL NÀY KHI:
- Khách hỏi TỒN KHO tại cửa hàng: "Shop Hoàng Mai còn size M không?" → redirect hotline!
- Khách hỏi CHÍNH SÁCH, KHUYẾN MÃI → dùng canifa_knowledge_search hoặc canifa_get_promotions
- Khách hỏi SẢN PHẨM → dùng data_retrieval_tool
Tham số:
- location (bắt buộc): Tên quận/huyện/tỉnh/thành phố/địa chỉ. Bỏ prefix "quận", "huyện", "tỉnh", "tp" khi truyền vào.
...@@ -11,12 +11,12 @@ from common.starrocks_connection import get_db_connection ...@@ -11,12 +11,12 @@ from common.starrocks_connection import get_db_connection
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Constants # Constants
TOP_K_RESULTS = 4 TOP_K_RESULTS = 10
class KnowledgeSearchInput(BaseModel): class KnowledgeSearchInput(BaseModel):
query: str = Field( 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ụ: tìm cửa hàng, hỏi chính sách, tra bảng size...). KHÔNG DÙNG ĐỂ TÌM SẢN PHẨM." 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)."
) )
...@@ -24,11 +24,12 @@ class KnowledgeSearchInput(BaseModel): ...@@ -24,11 +24,12 @@ class KnowledgeSearchInput(BaseModel):
async def canifa_knowledge_search(query: str) -> str: async def canifa_knowledge_search(query: str) -> str:
""" """
(Placeholder docstring - Actual prompt is loaded from file) (Placeholder docstring - Actual prompt is loaded from file)
Tra cứu thông tin thương hiệu Canifa (Cửa hàng, chính sách, KHTT...). Tra cứu thông tin thương hiệu Canifa (chính sách, KHTT, bảng size...).
KHÔNG dùng để tìm cửa hàng — dùng canifa_store_search.
""" """
logger.info(f"🔍 [Semantic Search] Brand Knowledge query: {query}") logger.info(f"🔍 [Semantic Search] Brand Knowledge query: {query}")
try: try:
# 1. Tạo embedding cho câu hỏi (Mặc định 1536 chiều như bro yêu cầu) # 1. Tạo embedding cho câu hỏi
query_vector = await create_embedding_async(query) query_vector = await create_embedding_async(query)
if not query_vector: 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." return "Xin lỗi, tôi gặp sự cố khi xử lý thông tin. Vui lòng thử lại sau."
......
...@@ -154,13 +154,13 @@ async def _execute_single_search( ...@@ -154,13 +154,13 @@ async def _execute_single_search(
# Debug: Log first product to see fields # Debug: Log first product to see fields
if products: if products:
first_p = products[0] first_p = products[0]
logger.info("🔍 [DEBUG] First product keys: %s", list(first_p.keys())) logger.debug("🔍 [DEBUG] First product keys: %s", list(first_p.keys()))
logger.info( logger.debug(
"🔍 [DEBUG] First product price: %s, sale_price: %s", "🔍 [DEBUG] First product price: %s, sale_price: %s",
first_p.get("original_price"), first_p.get("original_price"),
first_p.get("sale_price"), first_p.get("sale_price"),
) )
logger.info( logger.debug(
"🔍 [DEBUG] size_scale: %r, description_text: %r", "🔍 [DEBUG] size_scale: %r, description_text: %r",
first_p.get("size_scale"), first_p.get("size_scale"),
(first_p.get("description_text") or "")[:100], (first_p.get("description_text") or "")[:100],
......
...@@ -10,11 +10,12 @@ from .customer_info_tool import collect_customer_info ...@@ -10,11 +10,12 @@ from .customer_info_tool import collect_customer_info
from .data_retrieval_tool import data_retrieval_tool from .data_retrieval_tool import data_retrieval_tool
from .promotion_canifa_tool import canifa_get_promotions from .promotion_canifa_tool import canifa_get_promotions
from .check_is_stock import check_is_stock from .check_is_stock import check_is_stock
from .store_search_tool import canifa_store_search
def get_retrieval_tools() -> list[Tool]: def get_retrieval_tools() -> list[Tool]:
"""Các tool chỉ dùng để đọc/truy vấn dữ liệu (Có thể cache)""" """Các tool chỉ dùng để đọc/truy vấn dữ liệu (Có thể cache)"""
return [data_retrieval_tool, canifa_knowledge_search, canifa_get_promotions, check_is_stock] return [data_retrieval_tool, canifa_knowledge_search, canifa_get_promotions, check_is_stock, canifa_store_search]
def get_collection_tools() -> list[Tool]: def get_collection_tools() -> list[Tool]:
......
...@@ -21,12 +21,12 @@ PRODUCT_LINE_MAP: dict[str, list[str]] = { ...@@ -21,12 +21,12 @@ PRODUCT_LINE_MAP: dict[str, list[str]] = {
"Áo khoác nỉ có mũ": ["áo khoác nỉ có mũ"], "Áo khoác nỉ có mũ": ["áo khoác nỉ có mũ"],
"Áo khoác lông vũ": ["áo khoác lông vũ"], "Áo khoác lông vũ": ["áo khoác lông vũ"],
"Áo khoác gió": ["áo khoác gió", "áo gió", "áo khoác mỏng"], "Áo khoác gió": ["áo khoác gió", "áo gió", "áo khoác mỏng"],
"Áo khoác gilet chần bông": ["áo khoác gilet chần bông"], "Áo khoác gilet chần bông": ["áo khoác gilet chần bông", "áo khoác gilet trần bông", "áo gilet chần bông", "áo gilet trần bông"],
"Áo khoác gilet": ["áo khoác gilet"], "Áo khoác gilet": ["áo khoác gilet"],
"Áo khoác dạ": ["áo khoác dạ"], "Áo khoác dạ": ["áo khoác dạ"],
"Áo khoác dáng ngắn": ["áo khoác dáng ngắn"], "Áo khoác dáng ngắn": ["áo khoác dáng ngắn"],
"Áo khoác chống nắng": ["áo khoác chống nắng"], "Áo khoác chống nắng": ["áo khoác chống nắng"],
"Áo khoác chần bông": ["áo khoác chần bông"], "Áo khoác chần bông": ["áo khoác chần bông", "áo khoác trần bông", "áo chần bông", "áo trần bông"],
"Áo khoác": ["áo khoác"], "Áo khoác": ["áo khoác"],
"Áo giữ nhiệt": ["áo giữ nhiệt"], "Áo giữ nhiệt": ["áo giữ nhiệt"],
"Áo bra active": ["áo bra active"], "Áo bra active": ["áo bra active"],
......
...@@ -39,12 +39,11 @@ async def canifa_get_promotions(check_date: str = None) -> str: ...@@ -39,12 +39,11 @@ async def canifa_get_promotions(check_date: str = None) -> str:
try: try:
sql = f""" sql = f"""
SELECT SELECT
rule_id,
name, name,
description, description,
from_date, from_date,
to_date to_date
FROM shared_source.magento_salesrule FROM shared_source.chatbot_rsa_salerule_with_text_embedding
WHERE '{target_date}' >= DATE(from_date) WHERE '{target_date}' >= DATE(from_date)
AND '{target_date}' <= DATE(to_date) AND '{target_date}' <= DATE(to_date)
ORDER BY to_date ASC ORDER BY to_date ASC
......
import logging
from langchain_core.tools import tool
from pydantic import BaseModel, Field
from agent.prompt_utils import read_tool_prompt
from common.starrocks_connection import get_db_connection
logger = logging.getLogger(__name__)
STORE_TABLE = "shared_source.chatbot_rsa_store_schedule_with_text_embedding"
class StoreSearchInput(BaseModel):
location: str = Field(
description="Tên quận/huyện/tỉnh/thành phố hoặc địa chỉ cụ thể mà khách hàng muốn tìm cửa hàng CANIFA. VD: 'Hoàng Mai', 'Cầu Giấy', 'Đà Nẵng', 'Vincom Bà Triệu'."
)
@tool("canifa_store_search", args_schema=StoreSearchInput)
async def canifa_store_search(location: str) -> str:
"""
Tìm kiếm cửa hàng CANIFA theo địa điểm/khu vực.
Sử dụng khi khách hàng hỏi về cửa hàng tại một khu vực cụ thể.
"""
logger.info(f"🏪 [Store Search] Location: {location}")
try:
sr = get_db_connection()
# Clean location: bỏ prefix generic
clean = location.lower().strip()
for prefix in ["quận ", "huyện ", "tỉnh ", "thành phố ", "tp. ", "tp "]:
clean = clean.replace(prefix, "")
clean = clean.strip()
if not clean:
return "Vui lòng cho em biết khu vực bạn muốn tìm cửa hàng CANIFA (ví dụ: Hoàng Mai, Cầu Giấy, Đà Nẵng...)."
# Search trên các cột structured: city, state, address, store_name
sql = f"""
SELECT store_name, address, city, state, phone_number,
schedule_name, time_open_today, time_close_today
FROM {STORE_TABLE}
WHERE LOWER(city) LIKE '%{clean}%'
OR LOWER(state) LIKE '%{clean}%'
OR LOWER(address) LIKE '%{clean}%'
OR LOWER(store_name) LIKE '%{clean}%'
ORDER BY state, city, store_name
LIMIT 20
"""
results = await sr.execute_query_async(sql)
logger.info(f"📊 Store search: {len(results)} stores found for '{location}'")
if not results:
return f"Không tìm thấy cửa hàng CANIFA tại khu vực '{location}'. Khách hàng có thể liên hệ hotline 1800 6061 để được hỗ trợ tìm cửa hàng gần nhất."
# Format kết quả rõ ràng cho LLM
lines = []
for r in results:
name = r.get("store_name", "")
addr = r.get("address", "")
city = r.get("city", "")
state = r.get("state", "")
phone = r.get("phone_number", "")
schedule = r.get("schedule_name", "")
t_open = r.get("time_open_today", "")
t_close = r.get("time_close_today", "")
store_info = f"🏪 {name}\n"
store_info += f" 📍 Địa chỉ: {addr}"
if city:
store_info += f", {city}"
if state:
store_info += f", {state}"
store_info += f"\n 📞 ĐT: {phone}"
store_info += f"\n 🕐 Lịch: {schedule}"
if t_open and t_close:
store_info += f" (Hôm nay: {t_open}-{t_close})"
lines.append(store_info)
return f"Tìm thấy {len(results)} cửa hàng CANIFA tại khu vực '{location}':\n\n" + "\n\n".join(lines)
except Exception as e:
logger.error(f"❌ Error in canifa_store_search: {e}")
return "Tôi đang gặp khó khăn khi tìm kiếm cửa hàng. Bạn có thể liên hệ hotline 1800 6061 để được hỗ trợ."
canifa_store_search.__doc__ = read_tool_prompt("store_search_tool") or canifa_store_search.__doc__
from fastapi import APIRouter, HTTPException, Request from fastapi import APIRouter, HTTPException, Request
from pydantic import BaseModel from pydantic import BaseModel
from agent.prompt_utils import get_system_prompt_template, LANGFUSE_SYSTEM_PROMPT_NAME, _get_langfuse from agent.prompt_utils import get_system_prompt_template, force_refresh_prompts, LANGFUSE_SYSTEM_PROMPT_NAME, _get_langfuse
from agent.graph import reset_chain_cache
router = APIRouter() router = APIRouter()
...@@ -46,3 +47,21 @@ async def update_system_prompt_content(request: Request, body: PromptUpdateReque ...@@ -46,3 +47,21 @@ async def update_system_prompt_content(request: Request, body: PromptUpdateReque
} }
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
@router.post("/api/prompt/refresh")
async def refresh_prompt_cache(request: Request):
"""Force refresh all prompt caches immediately.
Call this after updating prompts on Langfuse to take effect right away.
"""
try:
# 1) Force refresh Langfuse SDK cache (all prompts)
force_refresh_prompts()
# 2) Clear graph chain cache so it rebuilds with new prompt
reset_chain_cache()
return {
"status": "success",
"message": "All prompt caches refreshed. Next request will use the latest prompts.",
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# CANIFA Chatbot API Documentation
API hệ thống chatbot tư vấn thời trang CANIFA - CiCi Assistant.
## Base URL
```
http://localhost:8000
```
---
## API Endpoints
### 1. Chat với Chatbot
**Endpoint:** `POST /chat`
**Mô tả:** Gửi tin nhắn tới chatbot và nhận phản hồi tư vấn thời trang cùng danh sách sản phẩm liên quan.
#### Request Body
```json
{
"user_id": "string",
"user_query": "string"
}
```
**Parameters:**
| Field | Type | Required | Mô tả |
|-------|------|----------|-------|
| `user_id` | string | ✅ | ID định danh người dùng (dùng để lưu lịch sử chat) |
| `user_query` | string | ✅ | Nội dung tin nhắn của người dùng |
**Ví dụ Request:**
```json
{
"user_id": "user_12345",
"user_query": "Cho em xem áo sơ mi nam dưới 5a00k"
}
```
#### Response
**Success Response (200 OK):**
```json
{
"status": "success",
"ai_response": "string",
"product_ids": ["string"]
}
```
**Response Fields:**
| Field | Type | Mô tả |
|-------|------|-------|
| `status` | string | Trạng thái xử lý request (`"success"` hoặc `"error"`) |
| `ai_response` | string | Câu trả lời của chatbot (văn bản tư vấn) |
| `product_ids` | array[string] | Danh sách mã sản phẩm được đề xuất (internal_ref_code) |
**Ví dụ Response:**
```json
{
"status": "success",
"ai_response": "Em chào anh! Em đã tìm thấy một số mẫu áo sơ mi nam đẹp trong tầm giá dưới 500k:\n\n1. Áo Sơ Mi Nam Cotton - 399.000đ\n2. Áo Sơ Mi Slim Fit - 449.000đ\n\nCác sản phẩm này đều là chất liệu cotton thoáng mát, phù hợp cho mùa hè ạ!",
"product_ids": ["SM12345", "SM12346"]
}
```
**Error Response (500 Internal Server Error):**
```json
{
"status": "error",
"ai_response": "Xin lỗi, đã có lỗi xảy ra. Vui lòng thử lại sau.",
"product_ids": []
}
```
---
### 2. Lấy Lịch Sử Chat
**Endpoint:** `GET /history/{user_id}`
**Mô tả:** Lấy lịch sử chat của người dùng với phân trang cursor-based.
#### Path Parameters
| Parameter | Type | Required | Mô tả |
|-----------|------|----------|-------|
| `user_id` | string | ✅ | ID người dùng cần lấy lịch sử |
#### Query Parameters
| Parameter | Type | Required | Default | Mô tả |
|-----------|------|----------|---------|-------|
| `limit` | integer | ❌ | 50 | Số lượng tin nhắn tối đa mỗi trang (1-100) |
| `before_id` | integer | ❌ | null | ID của tin nhắn để lấy các tin nhắn trước đó (dùng cho phân trang) |
**Ví dụ Request:**
```
GET /history/user_12345?limit=20&before_id=150
```
#### Response
**Success Response (200 OK):**
```json
{
"data": [
{
"id": 149,
"user_id": "user_12345",
"message": "Cho em xem áo sơ mi nam",
"is_human": true,
"timestamp": "2025-12-25T14:30:00"
},
{
"id": 148,
"user_id": "user_12345",
"message": "Em đã tìm thấy một số mẫu áo sơ mi nam đẹp...",
"is_human": false,
"timestamp": "2025-12-25T14:30:02"
}
],
"next_cursor": 130
}
```
**Response Fields:**
| Field | Type | Mô tả |
|-------|------|-------|
| `data` | array[object] | Danh sách tin nhắn chat (sắp xếp từ mới → cũ) |
| `data[].id` | integer | ID duy nhất của tin nhắn |
| `data[].user_id` | string | ID người dùng |
| `data[].message` | string | Nội dung tin nhắn |
| `data[].is_human` | boolean | `true` = tin nhắn của người dùng, `false` = tin nhắn của bot |
| `data[].timestamp` | string | Thời gian gửi tin nhắn (ISO 8601 format) |
| `next_cursor` | integer \| null | ID của tin nhắn cuối cùng (dùng làm `before_id` cho request tiếp theo). `null` nếu hết dữ liệu |
---
## Phân Trang (Pagination)
API sử dụng **cursor-based pagination** để lấy lịch sử chat:
### Cách hoạt động:
1. **Request đầu tiên** - Lấy 20 tin nhắn mới nhất:
```
GET /history/user_12345?limit=20
```
Response:
```json
{
"data": [...], // 20 tin nhắn (ID: 200 → 181)
"next_cursor": 181
}
```
2. **Request tiếp theo** - Lấy 20 tin nhắn cũ hơn:
```
GET /history/user_12345?limit=20&before_id=181
```
Response:
```json
{
"data": [...], // 20 tin nhắn (ID: 180 → 161)
"next_cursor": 161
}
```
3. **Request cuối cùng** - Khi hết dữ liệu:
```json
{
"data": [...], // 5 tin nhắn còn lại
"next_cursor": null
}
```
### Logic phân trang:
- `next_cursor` luôn là **ID của tin nhắn cuối cùng** trong `data`
- Dùng `next_cursor` làm `before_id` cho request tiếp theo
- Khi `next_cursor = null` → đã hết dữ liệu
---
## Chat Workflow
```mermaid
graph LR
A[User gửi message] --> B[POST /chat]
B --> C{Agent xử lý}
C --> D[Tìm kiếm sản phẩm]
C --> E[Trả lời tư vấn]
D --> F[Trích xuất product_ids]
E --> F
F --> G[Response: ai_response + product_ids]
G --> H[Lưu vào PostgreSQL]
H --> I[Trả về client]
```
### Quy trình xử lý:
1. User gửi tin nhắn qua API `/chat`
2. Hệ thống agent phân tích intent
3. Nếu cần tìm sản phẩm → Gọi `data_retrieval_tool` với tham số phù hợp
4. Agent tổng hợp thông tin → Trả lời tư vấn
5. Trích xuất `product_ids` từ kết quả tìm kiếm
6. Lưu lịch sử chat vào PostgreSQL (background task)
7. Trả về JSON với `ai_response` và `product_ids`
---
## Error Handling
### Error Response Format
```json
{
"status": "error",
"ai_response": "Mô tả lỗi hoặc thông báo fallback",
"product_ids": []
}
```
### HTTP Status Codes
| Code | Ý nghĩa | Khi nào xảy ra |
|------|---------|----------------|
| 200 | OK | Request thành công |
| 400 | Bad Request | Thiếu `user_id` hoặc `user_query` |
| 500 | Internal Server Error | Lỗi hệ thống (database, LLM, ...) |
---
## Ví Dụ Sử Dụng
### Python
```python
import requests
# Chat với bot
response = requests.post("http://localhost:8000/chat", json={
"user_id": "user_12345",
"user_query": "Cho em xem váy đầm dự tiệc dưới 1 triệu"
})
data = response.json()
print(f"Bot: {data['ai_response']}")
print(f"Sản phẩm: {data['product_ids']}")
# Lấy lịch sử chat
history = requests.get("http://localhost:8000/history/user_12345?limit=10")
messages = history.json()["data"]
for msg in messages:
sender = "User" if msg["is_human"] else "Bot"
print(f"{sender}: {msg['message']}")
```
### JavaScript (Fetch API)
```javascript
// Chat với bot
const response = await fetch('http://localhost:8000/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
user_id: 'user_12345',
user_query: 'Cho em xem áo khoác nữ'
})
});
const data = await response.json();
console.log('Bot:', data.ai_response);
console.log('Products:', data.product_ids);
// Lấy lịch sử chat (phân trang)
let cursor = null;
const allMessages = [];
do {
const url = cursor
? `http://localhost:8000/history/user_12345?limit=50&before_id=${cursor}`
: `http://localhost:8000/history/user_12345?limit=50`;
const historyResponse = await fetch(url);
const { data: messages, next_cursor } = await historyResponse.json();
allMessages.push(...messages);
cursor = next_cursor;
} while (cursor !== null);
console.log(`Tổng số tin nhắn: ${allMessages.length}`);
```
### cURL
```bash
# Chat với bot
curl -X POST "http://localhost:8000/chat" \
-H "Content-Type: application/json" \
-d '{
"user_id": "user_12345",
"user_query": "Cho em xem giày thể thao nam"
}'
# Lấy lịch sử chat
curl "http://localhost:8000/history/user_12345?limit=20"
# Lấy trang tiếp theo
curl "http://localhost:8000/history/user_12345?limit=20&before_id=150"
```
---
## Notes
### 1. Product IDs
- `product_ids` trả về danh sách `internal_ref_code` (mã sản phẩm nội bộ)
- Frontend có thể dùng để hiển thị carousel sản phẩm hoặc link đến trang chi tiết
- Nếu không tìm thấy sản phẩm → `product_ids = []`
### 2. Conversation History
- Lịch sử chat được lưu tự động sau mỗi cuộc hội thoại (background task)
- Dữ liệu lưu trong PostgreSQL với index trên `user_id` và `id`
- Sắp xếp theo thứ tự mới nhất → cũ nhất
### 3. Rate Limiting
- Hiện tại chưa có rate limiting
- Khuyến nghị implement rate limit khi deploy production
### 4. Authentication
- Hiện tại API không yêu cầu authentication
- `user_id` do client tự generate và gửi lên
- Khuyến nghị: Tích hợp Clerk Auth hoặc JWT token cho production
---
## Environment Variables
```bash
# PostgreSQL
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_DB=chatbot_db
POSTGRES_USER=postgres
POSTGRES_PASSWORD=your_password
# OpenAI
OPENAI_API_KEY=sk-...
# StarRocks (Vector Database)
STARROCKS_HOST=localhost
STARROCKS_PORT=9030
STARROCKS_USER=root
STARROCKS_PASSWORD=your_password
STARROCKS_DB=chatbot_products
# Server
PORT=8000
HOST=0.0.0.0
```
---
## Testing
Truy cập `http://localhost:8000/static/index.html` để test chatbot qua UI đơn giản.
================================================================================
QUERY: 'cửa hàng quận Hoàng Mai'
----------------------------------------
#1 [score=0.5944] TITLE: Thông tin cửa hàng SECTION: Thông tin cửa hàng KEYWORDS: danh sách cửa hàng, địa chỉ cửa hàng, giờ mở cửa, giờ đóng cửa CONTENT: Quan Hoa - state: Hà Nội - city: Quận Cầu Gi...
#2 [score=0.5940] TITLE: Thông tin cửa hàng SECTION: Thông tin cửa hàng KEYWORDS: danh sách cửa hàng, địa chỉ cửa hàng, giờ mở cửa, giờ đóng cửa CONTENT: Quan Hoa - city: Quận Cầu Giấy - phone_numbe...
#3 [score=0.5853] TITLE: Thông tin cửa hàng SECTION: Thông tin cửa hàng KEYWORDS: danh sách cửa hàng, địa chỉ cửa hàng, giờ mở cửa, giờ đóng cửa CONTENT: Phạm Ngũ Lão - city: Thành phố Hải Dương - p...
#4 [score=0.5825] TITLE: Thông tin cửa hàng SECTION: Thông tin cửa hàng KEYWORDS: danh sách cửa hàng, địa chỉ cửa hàng, giờ mở cửa, giờ đóng cửa CONTENT: Nhân Chính - state: Hà Nội - city: Quận Than...
#5 [score=0.5823] TITLE: Thông tin cửa hàng SECTION: Thông tin cửa hàng KEYWORDS: danh sách cửa hàng, địa chỉ cửa hàng, giờ mở cửa, giờ đóng cửa CONTENT: Phạm Ngũ Lão - state: Hải Dương - city: Thàn...
================================================================================
QUERY: 'cửa hàng quận Ba Đình'
----------------------------------------
#1 [score=0.5992] TITLE: Thông tin cửa hàng SECTION: Thông tin cửa hàng KEYWORDS: danh sách cửa hàng, địa chỉ cửa hàng, giờ mở cửa, giờ đóng cửa CONTENT: Đề Thám - state: Thái Bình - city: Thành phố...
#2 [score=0.5979] TITLE: Thông tin cửa hàng SECTION: Thông tin cửa hàng KEYWORDS: danh sách cửa hàng, địa chỉ cửa hàng, giờ mở cửa, giờ đóng cửa CONTENT: Quan Hoa - state: Hà Nội - city: Quận Cầu Gi...
#3 [score=0.5973] TITLE: Thông tin cửa hàng SECTION: Thông tin cửa hàng KEYWORDS: danh sách cửa hàng, địa chỉ cửa hàng, giờ mở cửa, giờ đóng cửa CONTENT: Quan Hoa - city: Quận Cầu Giấy - phone_numbe...
#4 [score=0.5961] TITLE: Thông tin cửa hàng SECTION: Thông tin cửa hàng KEYWORDS: danh sách cửa hàng, địa chỉ cửa hàng, giờ mở cửa, giờ đóng cửa CONTENT: Phạm Ngũ Lão - city: Thành phố Hải Dương - p...
#5 [score=0.5947] TITLE: Thông tin cửa hàng SECTION: Thông tin cửa hàng KEYWORDS: danh sách cửa hàng, địa chỉ cửa hàng, giờ mở cửa, giờ đóng cửa CONTENT: Đề Thám - city: Thành phố Thái Bình - phone_...
================================================================================
QUERY: 'city: Quận Hoàng Mai'
----------------------------------------
#1 [score=0.4351] TITLE: Thông tin cửa hàng SECTION: Thông tin cửa hàng KEYWORDS: danh sách cửa hàng, địa chỉ cửa hàng, giờ mở cửa, giờ đóng cửa CONTENT: Quan Hoa - city: Quận Cầu Giấy - phone_numbe...
#2 [score=0.4328] TITLE: Thông tin cửa hàng SECTION: Thông tin cửa hàng KEYWORDS: danh sách cửa hàng, địa chỉ cửa hàng, giờ mở cửa, giờ đóng cửa CONTENT: Quan Hoa - state: Hà Nội - city: Quận Cầu Gi...
#3 [score=0.4256] TITLE: Thông tin cửa hàng SECTION: Thông tin cửa hàng KEYWORDS: danh sách cửa hàng, địa chỉ cửa hàng, giờ mở cửa, giờ đóng cửa CONTENT: Thanh Nhàn - city: Quận Hai Bà Trưng - phone...
#4 [score=0.4231] TITLE: Thông tin cửa hàng SECTION: Thông tin cửa hàng KEYWORDS: danh sách cửa hàng, địa chỉ cửa hàng, giờ mở cửa, giờ đóng cửa CONTENT: Tân Chính - state: Đà Nẵng - city: Quận Than...
#5 [score=0.4209] TITLE: Thông tin cửa hàng SECTION: Thông tin cửa hàng KEYWORDS: danh sách cửa hàng, địa chỉ cửa hàng, giờ mở cửa, giờ đóng cửa CONTENT: Thanh Nhàn - state: Hà Nội - city: Quận Hai ...
================================================================================
QUERY: 'city: Quận Ba Đình'
----------------------------------------
#1 [score=0.4179] TITLE: Thông tin cửa hàng SECTION: Thông tin cửa hàng KEYWORDS: danh sách cửa hàng, địa chỉ cửa hàng, giờ mở cửa, giờ đóng cửa CONTENT: Thanh Nhàn - city: Quận Hai Bà Trưng - phone...
#2 [score=0.4154] TITLE: Thông tin cửa hàng SECTION: Thông tin cửa hàng KEYWORDS: danh sách cửa hàng, địa chỉ cửa hàng, giờ mở cửa, giờ đóng cửa CONTENT: Quan Hoa - city: Quận Cầu Giấy - phone_numbe...
#3 [score=0.4122] TITLE: Thông tin cửa hàng SECTION: Thông tin cửa hàng KEYWORDS: danh sách cửa hàng, địa chỉ cửa hàng, giờ mở cửa, giờ đóng cửa CONTENT: Quan Hoa - state: Hà Nội - city: Quận Cầu Gi...
#4 [score=0.4076] TITLE: Thông tin cửa hàng SECTION: Thông tin cửa hàng KEYWORDS: danh sách cửa hàng, địa chỉ cửa hàng, giờ mở cửa, giờ đóng cửa CONTENT: Thanh Nhàn - state: Hà Nội - city: Quận Hai ...
#5 [score=0.4057] TITLE: Thông tin cửa hàng SECTION: Thông tin cửa hàng KEYWORDS: danh sách cửa hàng, địa chỉ cửa hàng, giờ mở cửa, giờ đóng cửa CONTENT: Tân Chính - state: Đà Nẵng - city: Quận Than...
================================================================================
QUERY: 'store_name: CANIFA - city: Quận Hoàng Mai - Hà Nội'
----------------------------------------
#1 [score=0.6928] TITLE: Thông tin cửa hàng SECTION: Thông tin cửa hàng KEYWORDS: danh sách cửa hàng, địa chỉ cửa hàng, giờ mở cửa, giờ đóng cửa CONTENT: Bắc Ninh - store_name: CANIFA - Ngô Gia Tự,...
#2 [score=0.6927] TITLE: Thông tin cửa hàng SECTION: Thông tin cửa hàng KEYWORDS: danh sách cửa hàng, địa chỉ cửa hàng, giờ mở cửa, giờ đóng cửa CONTENT: Bắc Ninh - store_name: CANIFA - Ngô Gia Tự,...
#3 [score=0.6776] TITLE: Thông tin cửa hàng SECTION: Thông tin cửa hàng KEYWORDS: danh sách cửa hàng, địa chỉ cửa hàng, giờ mở cửa, giờ đóng cửa CONTENT: Hiệp Bình Chánh - city: Quận Thủ Đức - phone...
#4 [score=0.6772] TITLE: Thông tin cửa hàng SECTION: Thông tin cửa hàng KEYWORDS: danh sách cửa hàng, địa chỉ cửa hàng, giờ mở cửa, giờ đóng cửa CONTENT: Phương Liễu - city: - phone_number: 0222730...
#5 [score=0.6743] TITLE: Thông tin cửa hàng SECTION: Thông tin cửa hàng KEYWORDS: danh sách cửa hàng, địa chỉ cửa hàng, giờ mở cửa, giờ đóng cửa CONTENT: Hiệp Bình Chánh - state: Hồ Chí Minh - city:...
================================================================================
QUERY: 'store_name: CANIFA - Ba Đình - address: Số 6 Điện Biên Phủ - city: Quận Ba Đình'
----------------------------------------
#1 [score=0.6794] TITLE: Thông tin cửa hàng SECTION: Thông tin cửa hàng KEYWORDS: danh sách cửa hàng, địa chỉ cửa hàng, giờ mở cửa, giờ đóng cửa CONTENT: Bắc Ninh - store_name: CANIFA - Ngô Gia Tự,...
#2 [score=0.6778] TITLE: Thông tin cửa hàng SECTION: Thông tin cửa hàng KEYWORDS: danh sách cửa hàng, địa chỉ cửa hàng, giờ mở cửa, giờ đóng cửa CONTENT: Bắc Ninh - store_name: CANIFA - Ngô Gia Tự,...
#3 [score=0.6614] TITLE: Thông tin cửa hàng SECTION: Thông tin cửa hàng KEYWORDS: danh sách cửa hàng, địa chỉ cửa hàng, giờ mở cửa, giờ đóng cửa CONTENT: CANIFA 181 Giảng Võ - store_name: CANIFA 18...
#4 [score=0.6584] TITLE: Thông tin cửa hàng SECTION: Thông tin cửa hàng KEYWORDS: danh sách cửa hàng, địa chỉ cửa hàng, giờ mở cửa, giờ đóng cửa CONTENT: Phương Liễu - city: - phone_number: 0222730...
#5 [score=0.6564] TITLE: Thông tin cửa hàng SECTION: Thông tin cửa hàng KEYWORDS: danh sách cửa hàng, địa chỉ cửa hàng, giờ mở cửa, giờ đóng cửa CONTENT: CANIFA 181 Giảng Võ - store_name: CANIFA 18...
================================================================================
QUERY: 'Thông tin cửa hàng CANIFA tại Quận Hoàng Mai Hà Nội'
----------------------------------------
#1 [score=0.7314] TITLE: Thông tin cửa hàng SECTION: Thông tin cửa hàng KEYWORDS: danh sách cửa hàng, địa chỉ cửa hàng, giờ mở cửa, giờ đóng cửa CONTENT: Quan Hoa - state: Hà Nội - city: Quận Cầu Gi...
#2 [score=0.7260] TITLE: Thông tin cửa hàng SECTION: Thông tin cửa hàng KEYWORDS: danh sách cửa hàng, địa chỉ cửa hàng, giờ mở cửa, giờ đóng cửa CONTENT: CANIFA 181 Giảng Võ - store_name: CANIFA 18...
#3 [score=0.7252] TITLE: Thông tin cửa hàng SECTION: Thông tin cửa hàng KEYWORDS: danh sách cửa hàng, địa chỉ cửa hàng, giờ mở cửa, giờ đóng cửa CONTENT: CANIFA 181 Giảng Võ - store_name: CANIFA 18...
#4 [score=0.7215] TITLE: Thông tin cửa hàng SECTION: Thông tin cửa hàng KEYWORDS: danh sách cửa hàng, địa chỉ cửa hàng, giờ mở cửa, giờ đóng cửa CONTENT: Quan Hoa - city: Quận Cầu Giấy - phone_numbe...
#5 [score=0.7196] TITLE: Thông tin cửa hàng SECTION: Thông tin cửa hàng KEYWORDS: danh sách cửa hàng, địa chỉ cửa hàng, giờ mở cửa, giờ đóng cửa CONTENT: Nhân Chính - state: Hà Nội - city: Quận Than...
================================================================================
QUERY: 'danh sách cửa hàng địa chỉ cửa hàng Quận Hoàng Mai'
----------------------------------------
#1 [score=0.6293] TITLE: Thông tin cửa hàng SECTION: Thông tin cửa hàng KEYWORDS: danh sách cửa hàng, địa chỉ cửa hàng, giờ mở cửa, giờ đóng cửa CONTENT: Quan Hoa - city: Quận Cầu Giấy - phone_numbe...
#2 [score=0.6266] TITLE: Thông tin cửa hàng SECTION: Thông tin cửa hàng KEYWORDS: danh sách cửa hàng, địa chỉ cửa hàng, giờ mở cửa, giờ đóng cửa CONTENT: Quan Hoa - state: Hà Nội - city: Quận Cầu Gi...
#3 [score=0.6261] TITLE: Thông tin cửa hàng SECTION: Thông tin cửa hàng KEYWORDS: danh sách cửa hàng, địa chỉ cửa hàng, giờ mở cửa, giờ đóng cửa CONTENT: Phạm Ngũ Lão - state: Hải Dương - city: Thàn...
#4 [score=0.6254] TITLE: Thông tin cửa hàng SECTION: Thông tin cửa hàng KEYWORDS: danh sách cửa hàng, địa chỉ cửa hàng, giờ mở cửa, giờ đóng cửa CONTENT: Phạm Ngũ Lão - city: Thành phố Hải Dương - p...
#5 [score=0.6171] TITLE: Thông tin cửa hàng SECTION: Thông tin cửa hàng KEYWORDS: danh sách cửa hàng, địa chỉ cửa hàng, giờ mở cửa, giờ đóng cửa CONTENT: Nhân Chính - state: Hà Nội - city: Quận Than...
"""Test: Kiểm tra embedding search có match được sai chính tả không"""
import asyncio, json, sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from common.embedding_service import create_embedding_async
from common.starrocks_connection import get_db_connection
output_lines = []
async def search_products(query_text, sr):
v = await create_embedding_async(query_text)
if not v:
output_lines.append(" ERROR: embedding failed")
return
v_str = "[" + ",".join(str(x) for x in v) + "]"
sql = f"""
SELECT magento_ref_code, product_name, master_color, gender_by_product, age_by_product,
approx_cosine_similarity(vector, {v_str}) as score
FROM shared_source.magento_product_dimension_with_text_embedding
ORDER BY score DESC LIMIT 5
"""
rows = await sr.execute_query_async(sql)
for i, r in enumerate(rows):
line = f" #{i+1} [score={r.get('score','?'):.4f}] {r['magento_ref_code']} | {r['product_name']} | {r.get('master_color','')} | {r.get('gender_by_product','')}/{r.get('age_by_product','')}"
output_lines.append(line)
async def main():
sr = get_db_connection()
tests = [
"Ao khoac tran bong cho tre em",
"Ao khoac chan bong cho tre em",
"Ao khoac chan bong",
]
for query in tests:
output_lines.append("=" * 80)
output_lines.append(f"QUERY: '{query}'")
output_lines.append("-" * 80)
await search_products(query, sr)
output_lines.append("")
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
loop.run_until_complete(main())
finally:
loop.close()
# Write output with UTF-8
out_path = os.path.join(os.path.dirname(__file__), "test_output.txt")
with open(out_path, "w", encoding="utf-8") as f:
f.write("\n".join(output_lines))
print(f"Output written to {out_path}")
QUERY: 'có cửa hàng bên quận hoàng mai shop ơi' → KEYWORDS: ['hoàng', 'mai']
AND: 4 rows | OR: 19 rows
QUERY: 'cửa hàng quận Hoàng Mai' → KEYWORDS: ['hoàng', 'mai']
AND: 4 rows | OR: 19 rows
QUERY: 'Canifa ở Cầu Giấy địa chỉ ở đâu?' → KEYWORDS: ['cầu', 'giấy']
AND: 10 rows | OR: 40 rows
QUERY: 'bảng size áo nam' → KEYWORDS: ['áo', 'nam']
AND: 4 rows | OR: 76 rows
"""Check Hoàng Mai in store table"""
import asyncio, sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from common.starrocks_connection import get_db_connection
async def main():
sr = get_db_connection()
rows = await sr.execute_query_async("""
SELECT store_name, address, city, state, phone_number, schedule_name
FROM shared_source.chatbot_rsa_store_schedule_with_text_embedding
WHERE LOWER(city) LIKE '%hoàng mai%'
OR LOWER(address) LIKE '%hoàng mai%'
OR LOWER(store_name) LIKE '%hoàng mai%'
""")
print(f"Found: {len(rows)}")
for r in rows:
print(r)
# Also check Phúc Yên
print("\n=== PHÚC YÊN ===")
rows2 = await sr.execute_query_async("""
SELECT store_name, address, city, state
FROM shared_source.chatbot_rsa_store_schedule_with_text_embedding
WHERE LOWER(city) LIKE '%phúc yên%' OR LOWER(state) LIKE '%vĩnh phúc%'
""")
print(f"Found: {len(rows2)}")
for r in rows2:
print(r)
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
loop.run_until_complete(main())
finally:
loop.close()
================================================================================
QUERY: 'Ao khoac tran bong cho tre em'
--------------------------------------------------------------------------------
#1 [score=0.4786] 3OT25W001 | Áo khoác chần bông unisex trẻ em chống thấm nước | Xám/ Gray | unisex/kid
#2 [score=0.4713] 3OT25W001 | Áo khoác chần bông unisex trẻ em chống thấm nước | Xanh da trời/ Blue | unisex/kid
#3 [score=0.4685] 3OT25W001 | Áo khoác chần bông unisex trẻ em chống thấm nước | Trắng/ White | unisex/kid
#4 [score=0.4684] 3OT25W001 | Áo khoác chần bông unisex trẻ em chống thấm nước | Hồng/ Pink- Magenta | unisex/kid
#5 [score=0.4677] 3TS25C001 | Áo phông unisex trẻ em có hình in đứng phom | Xanh da trời/ Blue | unisex/kid
================================================================================
QUERY: 'Ao khoac chan bong cho tre em'
--------------------------------------------------------------------------------
#1 [score=0.4885] 3OT25W001 | Áo khoác chần bông unisex trẻ em chống thấm nước | Xám/ Gray | unisex/kid
#2 [score=0.4822] 3OT25W001 | Áo khoác chần bông unisex trẻ em chống thấm nước | Xanh da trời/ Blue | unisex/kid
#3 [score=0.4803] 3OT25W001 | Áo khoác chần bông unisex trẻ em chống thấm nước | Đen/ Black | unisex/kid
#4 [score=0.4779] 3OT25W001 | Áo khoác chần bông unisex trẻ em chống thấm nước | Trắng/ White | unisex/kid
#5 [score=0.4744] 3OT25W001 | Áo khoác chần bông unisex trẻ em chống thấm nước | Hồng/ Pink- Magenta | unisex/kid
================================================================================
QUERY: 'Ao khoac chan bong'
--------------------------------------------------------------------------------
#1 [score=0.5110] 8OT25W016 | Áo khoác chần bông nam chống thấm nước | Xanh da trời/ Blue | men/adult
#2 [score=0.5093] 8OT25W016 | Áo khoác chần bông nam chống thấm nước | Xám/ Gray | men/adult
#3 [score=0.5053] 8OT25W016 | Áo khoác chần bông nam chống thấm nước | Đen/ Black | men/adult
#4 [score=0.5050] 8OT25W016 | Áo khoác chần bông nam chống thấm nước | Be/ Beige | men/adult
#5 [score=0.4976] 2OT25C004-SK010 | Áo khoác chần bông bé trai chống thấm nước | Đen/ Black | boy/kid
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