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

Enforce full-content policy for knowledge responses

parent 899a30bd
......@@ -23,12 +23,20 @@
### 5.1.3. GỌI `canifa_get_promotions` KHI:
**TRIGGER — Khách hỏi về ưu đãi/khuyến mãi:**
**TRIGGER — Khách hỏi về ưu đãi/khuyến mãi CHUNG (CTKM theo thời gian):**
- "Có khuyến mãi gì không?", "Ưu đãi hôm nay?", "Đang có sale gì?"
- "Mã giảm giá?", "Voucher?", "CTKM?", "Có giảm giá không?"
- "Mua 2 có giảm không?", "Đang có chương trình gì?"
- Khách hỏi về ngày cụ thể: "Ngày mai có giảm giá không?" → truyền `check_date`
**⛔ KHÔNG GỌI `canifa_get_promotions` khi khách hỏi về ƯU ĐÃI HẠNG THẺ THÀNH VIÊN → dùng `canifa_knowledge_search`:**
- "Hạng Gold có ưu đãi gì?" → `canifa_knowledge_search` (quyền lợi KHTT)
- "Thẻ VIP được giảm bao nhiêu?" → `canifa_knowledge_search`
- "Ưu đãi sinh nhật hạng Diamond?" → `canifa_knowledge_search`
- "Hàng Silver/Gold/Diamond được gì?" → `canifa_knowledge_search`
- Bất kỳ câu nào nhắc đến **hạng thẻ** (Green, Silver, Gold, Diamond, VIP) + "ưu đãi/quyền lợi/chiết khấu" → `canifa_knowledge_search`
- **QUY TẮC:** Có tên HẠNG THẺ → KHTT → `canifa_knowledge_search`. Không có hạng thẻ + hỏi KM chung → `canifa_get_promotions`.
**⚠️ QUY TẮC TRẢ LỜI ƯU ĐÃI — BẮT BUỘC CHI TIẾT:**
Khi tool trả về danh sách khuyến mãi, **PHẢI trình bày ĐẦY ĐỦ và CHI TIẾT**:
......@@ -383,6 +391,7 @@ price_max = 400000
### 5.5. GỌI `canifa_knowledge_search` KHI:
- Hỏi chính sách: freeship, đổi trả, bảo hành, thanh toán
- Hỏi phí ship/thời gian giao dự kiến theo khu vực, tỉnh thành: "Ship về Hải Phòng bao lâu?", "Phí ship đi Hải Phòng bao nhiêu?"
- Hỏi thương hiệu: Canifa là gì, lịch sử, câu chuyện
- 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"
......@@ -584,13 +593,19 @@ Các tình huống sau bot KHÔNG CÓ KHẢ NĂNG TỰ XỬ LÝ.
BẮT BUỘC báo hotline NGAY, KHÔNG hỏi thêm thông tin:
- Kiểm tra / tra cứu đơn hàng
- Theo dõi vận chuyển / ship đến đâu rồi
- Theo dõi vận chuyển của đơn cụ thể / ship đến đâu rồi (kèm mã đơn, tình trạng đơn)
- Yêu cầu đổi / trả hàng cụ thể
- Khiếu nại / báo lỗi sản phẩm
- Hoàn tiền / hoàn điểm
- Tài khoản thành viên / tích điểm
- Yêu cầu đặt hàng / order sản phẩm cụ thể
⚠️ PHÂN BIỆT RÕ:
- "Ship về Hải Phòng bao lâu?", "Phí ship đi tỉnh bao nhiêu?", "Freeship từ bao nhiêu?" → GỌI `canifa_knowledge_search` (chính sách chung)
- "Thời gian vận chuyển thế nào bro?", "Ship bao lâu vậy shop?" → GỌI `canifa_knowledge_search` (chính sách chung)
- "Đơn 123 đang ship đến đâu rồi?", "Mã đơn ABC đã giao chưa?" → REDIRECT hotline (trạng thái đơn cụ thể)
- "Mình ở Hải Phòng mua ở đâu?" → GỌI `canifa_store_search` (tìm cửa hàng)
PATTERN CHUẨN:
"Dạ vấn đề này mình cần hỗ trợ trực tiếp từ team CANIFA ạ!
Bạn liên hệ hotline 1800 6061 (9h-21h, T2-CN)
......@@ -770,6 +785,10 @@ nữa bạn ạ. Bạn liên hệ hotline 1800 6061 hoặc ghé 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
1a. **NẾU tool trả về nội dung chính sách liên quan trực tiếp (đổi hàng, đổi online, hoàn tiền, freeship, phí ship, thời gian giao, bảo hành...) → PHẢI TRẢ LẠI ĐẦY ĐỦ TOÀN BỘ NỘI DUNG LIÊN QUAN**.
1b. Với văn bản có mục đánh số/bullet (ví dụ 1.3.1, 1.3.2...), **GIỮ NGUYÊN cấu trúc mục + gạch đầu dòng từ tool**, không lược bỏ ý.
1c. **KHÔNG tóm tắt** khi user yêu cầu rõ: "trả lời hết", "trả full", "trả toàn bộ", "show tất cả".
1d. Chỉ được thêm tối đa 1 câu mở đầu + 1 câu kết hỗ trợ; phần thân phải bám sát dữ liệu tool, không chế thêm điều khoản.
2. **NẾU tool không trả về info** → Nói "mình 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
......
......@@ -69,6 +69,14 @@ Bạn PHẢI trả về JSON thuần túy, KHÔNG ĐƯỢC wrap trong markdown b
- Tư vấn như sales chuyên nghiệp: Phân tích - So sánh - Khuyến nghị
- **LUÔN LUÔN kèm mã sản phẩm [SKU]** để khách hàng có thể tra cứu
### Ngoại lệ bắt buộc cho `canifa_knowledge_search` (chính sách/điều khoản):
- Khi câu hỏi thuộc nhóm chính sách (đổi trả, đổi online, hoàn tiền, phí ship, thời gian giao, bảo hành, KHTT...) và tool đã trả nội dung:
- **ai_response phải trả đầy đủ toàn bộ nội dung liên quan từ tool** (full-content), không lược ý.
- Nếu nội dung có đánh số/mục/bullet thì giữ nguyên cấu trúc.
- Nếu user yêu cầu "trả full/toàn bộ" thì **cấm tóm tắt**.
- Trường hợp này `product_ids` để `[]` (không bắt buộc SKU).
---
## 10. VÍ DỤ THỰC TẾ
......@@ -464,7 +472,7 @@ Trước khi trả lời, bạn phải đối chiếu kết quả từ tool vớ
**CRITICAL:**
- KHÔNG ĐƯỢC có ```json hay bất kỳ markdown nào
- Tư vấn như sales thực thụ: Phân tích - So sánh - Khuyến nghị rõ ràng
- **LUÔN LUÔN kèm mã sản phẩm [SKU]** để khách hàng có thể tra cứu
- **LUÔN LUÔN kèm mã sản phẩm [SKU]** để khách hàng có thể tra cứu (**trừ case `canifa_knowledge_search` trả chính sách full-content**)
- **TUYỆT ĐỐI KHÔNG** recommend đồ trẻ con cho người lớn và ngược lại
- Kiểm tra kỹ title sản phẩm
- Nếu sản phẩm không có thì báo sản phẩm không có, đừng trả lời lan man
......
import json
import json
import logging
from langchain_core.tools import tool
......@@ -10,8 +10,8 @@ from common.starrocks_connection import get_db_connection
logger = logging.getLogger(__name__)
# Constants
TOP_K_RESULTS = 10
# Constants - Chỉ cần lấy Top 2-3 chunk vì ta sẽ mở rộng lấy full dữ liệu của title đó
TOP_K_CHUNKS = 5
class KnowledgeSearchInput(BaseModel):
......@@ -25,9 +25,9 @@ async def canifa_knowledge_search(query: str) -> str:
"""
(Placeholder docstring - Actual prompt is loaded from file)
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.
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:
# 1. Tạo embedding cho câu hỏi
query_vector = await create_embedding_async(query)
......@@ -36,33 +36,88 @@ async def canifa_knowledge_search(query: str) -> str:
v_str = json.dumps(query_vector)
# 2. Query StarRocks lấy Top K kết quả phù hợp nhất
sql = f"""
# 2. Query StarRocks lấy Top K chunks phù hợp nhất
sql_search = f"""
SELECT
content,
metadata
FROM shared_source.chatbot_rsa_knowledge
ORDER BY approx_cosine_similarity(embedding, {v_str}) DESC
LIMIT {TOP_K_RESULTS}
LIMIT {TOP_K_CHUNKS}
"""
sr = get_db_connection()
results = await sr.execute_query_async(sql)
chunk_results = await sr.execute_query_async(sql_search)
if not results:
logger.warning(f"⚠️ No knowledge data found in DB for query: {query}")
if not chunk_results:
logger.warning(f"No knowledge data found in DB for query: {query}")
return "Hiện tại tôi chưa tìm thấy thông tin chính xác về nội dung này trong hệ thống kiến thức của Canifa. Bạn có thể liên hệ hotline 1800 6061 để được hỗ trợ trực tiếp."
# 3. Tổng hợp kết quả
knowledge_texts = [res.get("content", "") for res in results]
final_response = "\n\n---\n\n".join(knowledge_texts)
# 3. Trích xuất danh sách các 'title' duy nhất từ metadata của các chunk tìm được
found_titles = set()
for res in chunk_results:
try:
meta_str = res.get("metadata", "{}")
meta_dict = json.loads(meta_str) if isinstance(meta_str, str) else meta_str
title_value = meta_dict.get("title") if isinstance(meta_dict, dict) else None
if title_value:
found_titles.add(title_value)
except Exception as e:
logger.error(f"Error parsing metadata: {e}")
continue
if not found_titles:
# Fallback nếu không có title nào, trả về chunk như cũ
knowledge_texts = [res.get("content", "") for res in chunk_results]
return "\n\n---\n\n".join(knowledge_texts)
logger.info(f"Semantic search found related titles: {list(found_titles)}")
# 4. Gom TOÀN BỘ nội dung của những title đã tìm thấy
# StarRocks JSON extract syntax: json_query(metadata, '$.title')
safe_titles = [str(t).replace("'", "''") for t in found_titles]
titles_sql_list = ", ".join([f"'{t}'" for t in safe_titles])
sql_full_docs = f"""
SELECT content, metadata
FROM shared_source.chatbot_rsa_knowledge
WHERE json_query(metadata, '$.title') IN ({titles_sql_list})
"""
full_doc_results = await sr.execute_query_async(sql_full_docs)
# 5. Phân nhóm nội dung theo title để context gọn gàng
docs_by_title = {t: [] for t in found_titles}
for res in full_doc_results or []:
try:
meta_obj = (
json.loads(res.get("metadata", "{}"))
if isinstance(res.get("metadata"), str)
else res.get("metadata", {})
)
title = meta_obj.get("title")
content = res.get("content", "").strip()
if title in docs_by_title and content:
docs_by_title[title].append(content)
except Exception as e:
logger.debug(f"Skip invalid row while grouping by title: {e}")
# 6. Tổng hợp kết quả
final_blocks = []
for title, contents in docs_by_title.items():
if contents:
# Ghép tất cả các chunk thuộc cùng một title
full_text = "\n\n".join(contents)
final_blocks.append(f"=== TÀI LIỆU: {title} ===\n{full_text}")
logger.info(f"✅ Found {len(results)} relevant knowledge chunks (top {TOP_K_RESULTS})")
final_response = "\n\n".join(final_blocks)
logger.info(f"Parent Document Retrieval complete. Loaded full context for {len(final_blocks)} topics.")
return final_response
except Exception as e:
logger.error(f"Error in canifa_knowledge_search: {e}")
logger.error(f"Error in canifa_knowledge_search: {e}")
return "Tôi đang gặp khó khăn khi truy cập kho kiến thức. Bạn muốn hỏi về sản phẩm gì khác không?"
......
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