Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
C
chatbot canifa
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
1
Merge Requests
1
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Vũ Hoàng Anh
chatbot canifa
Commits
0205042f
Commit
0205042f
authored
Mar 03, 2026
by
Vũ Hoàng Anh
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix: cross-search ao lot/ao bra, remove product_line_vn, add cross-sell + gender ultimatum
parent
bd608ad5
Changes
6
Show whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
89 additions
and
28 deletions
+89
-28
05_tool_routing.txt
backend/agent/prompt_module/05_tool_routing.txt
+19
-1
_push_update.py
backend/agent/prompt_module/_push_update.py
+7
-0
data_retrieval_tool.txt
backend/agent/tool_prompts/data_retrieval_tool.txt
+26
-0
data_retrieval_tool.py
backend/agent/tools/data_retrieval_tool.py
+1
-14
product_mapping.py
backend/agent/tools/product_mapping.py
+14
-1
product_search_helpers.py
backend/agent/tools/product_search_helpers.py
+22
-12
No files found.
backend/agent/prompt_module/05_tool_routing.txt
View file @
0205042f
...
@@ -163,9 +163,19 @@ Trong DB, cột `description_text` BẮT ĐẦU bằng tên sản phẩm:
...
@@ -163,9 +163,19 @@ Trong DB, cột `description_text` BẮT ĐẦU bằng tên sản phẩm:
- "quần jeans" → `description: "Quần jean"`, `product_name: "Quần jean"`
- "quần jeans" → `description: "Quần jean"`, `product_name: "Quần jean"`
- "áo polo" → `description: "Áo Polo"`, `product_name: "Áo Polo"`
- "áo polo" → `description: "Áo Polo"`, `product_name: "Áo Polo"`
- "váy jeans" → `description: "Váy jeans"`, `product_name: "Váy jeans"`
- "váy jeans" → `description: "Váy jeans"`, `product_name: "Váy jeans"`
- **"áo bra"** → `description: "Áo bra active"`, `product_name: "Áo bra active"` (KHÔNG PHẢI "Áo lót"!)
→ Vì semantic search sẽ match trực tiếp với đầu description_text trong DB!
→ Vì semantic search sẽ match trực tiếp với đầu description_text trong DB!
**→ NHÓM SP LIÊN QUAN (description bao cả 2 để vector search tìm hết):**
- **"áo bra"** / **"áo lót"** → `description: "Áo lót. Áo bra active thoáng mát co giãn"`, `product_name: "Áo lót/Áo bra active"`
→ description ghi cả 2 tên để semantic search tìm được cả 2 loại
→ product_name dùng "/" để code tự tách và lọc cả 2
**⛔⛔⛔ TỐI HẬU THƯ — CẤM TỰ SUY DIỄN gender/age ⛔⛔⛔**
User nói "áo lót" → gender: null, age: null (**CẤM tự điền "women"!**)
User nói "áo phông" → gender: null, age: null
→ KHÔNG BIẾT thì KHÔNG ĐIỀN! Để null = tìm TẤT CẢ. Điền sai = MẤT kết quả!
→ CHỈ điền khi user NÓI RÕ: "áo lót nữ" → women, "áo trẻ em" → kid
**→ Khi khách hỏi MÔ TẢ / NHU CẦU (HYDE):** `description` ≠ `product_name`
**→ Khi khách hỏi MÔ TẢ / NHU CẦU (HYDE):** `description` ≠ `product_name`
- "đi dự tiệc" → `description: "Váy dự tiệc sang trọng nữ tính"`, `product_name: "Váy liền"`
- "đi dự tiệc" → `description: "Váy dự tiệc sang trọng nữ tính"`, `product_name: "Váy liền"`
- "đồ đi biển" → `description: "Áo phông nam đi biển thoáng mát"`, `product_name: "Áo phông"`
- "đồ đi biển" → `description: "Áo phông nam đi biển thoáng mát"`, `product_name: "Áo phông"`
...
@@ -205,6 +215,13 @@ description = "áo len giá dưới 500k" # ← SAI - có giá trong
...
@@ -205,6 +215,13 @@ description = "áo len giá dưới 500k" # ← SAI - có giá trong
description = "Váy liền thân/ Chân váy" # ← SAI khi hỏi "váy jeans" - phải ghi "Váy jeans"
description = "Váy liền thân/ Chân váy" # ← SAI khi hỏi "váy jeans" - phải ghi "Váy jeans"
```
```
**⚡ CROSS-SELL — Gợi ý mua thêm KHÉO LÉO, gắn trực tiếp với CTKM:**
Sau khi giới thiệu SP + mention CTKM → **gợi ý mua thêm SP khác VÀ nói rõ lợi ích từ CTKM:**
- "Bạn mua thêm mấy cái quần lót/áo lót nữa là được **mua 2 giảm 20%, mua 3 giảm 30%** luôn đấy!"
- "Áo này 199k, bạn thêm 1 quần nữa là đủ **399k được giảm 80k** cho đơn đầu tiên rồi!"
- "Nhân đợt sale bạn gom thêm mấy cái tất/khăn vào, mua chung **tiết kiệm hơn nhiều**!"
→ **KEY:** Phải nói RÕ mua thêm gì + được hưởng CTKM nào. KHÔNG gợi ý chung chung!
---
---
### 5.3. TỰ SUY LUẬN KHI THIẾU THÔNG TIN
### 5.3. TỰ SUY LUẬN KHI THIẾU THÔNG TIN
...
@@ -452,6 +469,7 @@ Khách: "Mẫu này có size gì?" ← KHÔNG có chữ "CÒN" → Liệt kê t
...
@@ -452,6 +469,7 @@ Khách: "Mẫu này có size gì?" ← KHÔNG có chữ "CÒN" → Liệt kê t
- Đọc trường `size_scale` từ tool response → liệt kê TỪNG SIZE cụ thể
- Đọ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 dùng từ "đủ", "đầy đủ", "từ...đến..."
- KHÔNG suy diễn size — chỉ nói size có trong data
- KHÔNG suy diễn size — chỉ nói size có trong data
- ⛔ **CẤM BỊA SIZE!** Nếu size_scale = "32A|34A|36A|38A|40A" → chỉ nói 32A, 34A, 36A, 38A, 40A. TUYỆT ĐỐI KHÔNG tự thêm S, M, L, XL vào!
---
---
...
...
backend/agent/prompt_module/_push_update.py
View file @
0205042f
...
@@ -22,5 +22,12 @@ with open(os.path.join(BASE, "05_tool_routing.txt"), "r", encoding="utf-8") as f
...
@@ -22,5 +22,12 @@ with open(os.path.join(BASE, "05_tool_routing.txt"), "r", encoding="utf-8") as f
lf
.
create_prompt
(
name
=
"canifa-05-tool-routing"
,
prompt
=
content2
,
labels
=
[
"production"
],
tags
=
[
"canifa"
,
"system-core"
],
type
=
"text"
)
lf
.
create_prompt
(
name
=
"canifa-05-tool-routing"
,
prompt
=
content2
,
labels
=
[
"production"
],
tags
=
[
"canifa"
,
"system-core"
],
type
=
"text"
)
print
(
f
"✅ canifa-05-tool-routing updated ({len(content2):,} chars)"
)
print
(
f
"✅ canifa-05-tool-routing updated ({len(content2):,} chars)"
)
# 3. Push data_retrieval_tool prompt
drt_path
=
os
.
path
.
join
(
BASE
,
".."
,
"tool_prompts"
,
"data_retrieval_tool.txt"
)
with
open
(
drt_path
,
"r"
,
encoding
=
"utf-8"
)
as
f
:
content3
=
f
.
read
()
lf
.
create_prompt
(
name
=
"data_retrieval_tool"
,
prompt
=
content3
,
labels
=
[
"production"
],
tags
=
[
"canifa"
,
"tool-prompt"
],
type
=
"text"
)
print
(
f
"✅ data_retrieval_tool updated ({len(content3):,} chars)"
)
lf
.
flush
()
lf
.
flush
()
print
(
"Done!"
)
print
(
"Done!"
)
backend/agent/tool_prompts/data_retrieval_tool.txt
View file @
0205042f
...
@@ -134,6 +134,26 @@ form_neckline — Crew Neck, Classic Collar, V-neck, Hooded collar, Mock Neck/ H
...
@@ -134,6 +134,26 @@ form_neckline — Crew Neck, Classic Collar, V-neck, Hooded collar, Mock Neck/ H
material_group — Knit - Dệt Kim, Woven - Dệt Thoi, Yarn - Sợi
material_group — Knit - Dệt Kim, Woven - Dệt Thoi, Yarn - Sợi
season — Fall Winter, Spring Summer, Year
season — Fall Winter, Spring Summer, Year
═══════════════════════════════════════════════════════════════
⛔⛔⛔ TỐI HẬU THƯ — gender_by_product / age_by_product ⛔⛔⛔
═══════════════════════════════════════════════════════════════
CẤM TUYỆT ĐỐI tự suy diễn gender/age khi user CHƯA NÓI RÕ!
User nói "áo lót" → gender: null, age: null (CẤM tự điền "women"!)
User nói "áo bra" → gender: null, age: null (CẤM tự điền "women"!)
User nói "quần lót" → gender: null, age: null
User nói "áo phông" → gender: null, age: null
→ Vì có thể là cho nam, nữ, trẻ em — KHÔNG BIẾT thì KHÔNG ĐIỀN!
CHỈ ĐIỀN KHI USER NÓI RÕ:
- "áo lót nữ" → gender: "women"
- "áo lót trẻ em" → age: "kid"
- "áo phông nam" → gender: "men"
- "quần cho bé trai" → gender: "boy", age: "kid"
⚠️ GHI NHỚ: Để null = tìm TẤT CẢ (nam + nữ + trẻ em). Điền sai = MẤT kết quả!
═══════════════════════════════════════════════════════════════
═══════════════════════════════════════════════════════════════
📝 VÍ DỤ (description_text LUÔN CÓ!)
📝 VÍ DỤ (description_text LUÔN CÓ!)
═══════════════════════════════════════════════════════════════
═══════════════════════════════════════════════════════════════
...
@@ -185,6 +205,12 @@ CASE 10: "Áo khaki"
...
@@ -185,6 +205,12 @@ CASE 10: "Áo khaki"
→ description: "product_name: Áo khaki. description_text: Áo chất liệu khaki form đẹp"
→ description: "product_name: Áo khaki. description_text: Áo chất liệu khaki form đẹp"
→ product_line_vn: "Áo"
→ product_line_vn: "Áo"
CASE 11: "Áo lót" hoặc "Áo bra" (NHÓM SP LIÊN QUAN)
→ description: "product_name: Áo lót/Áo bra active. description_text: Áo lót. Áo bra active thoáng mát co giãn tốt"
→ product_name: "Áo lót/Áo bra active"
⚠️ KHÔNG tự suy gender/age! User nói "áo lót" chung → để null. Chỉ điền khi user NÓI RÕ (VD: "áo lót nữ" → women, "áo lót trẻ em" → kid)
⚠️ description_text PHẢI ghi CẢ 2 tên (Áo lót + Áo bra active) để semantic search tìm được cả 2 loại!
═══════════════════════════════════════════════════════════════
═══════════════════════════════════════════════════════════════
🎉 DỊP LỄ / SỰ KIỆN — description_text ghi lý do + gợi ý phong cách
🎉 DỊP LỄ / SỰ KIỆN — description_text ghi lý do + gợi ý phong cách
═══════════════════════════════════════════════════════════════
═══════════════════════════════════════════════════════════════
...
...
backend/agent/tools/data_retrieval_tool.py
View file @
0205042f
...
@@ -84,16 +84,6 @@ class SearchItem(BaseModel):
...
@@ -84,16 +84,6 @@ class SearchItem(BaseModel):
discount_max
:
int
|
None
=
Field
(
discount_max
:
int
|
None
=
Field
(
description
=
"[SQL FILTER]
%
giảm giá tối đa. VD: 70 → chỉ lấy SP giảm <= 70
%
."
,
description
=
"[SQL FILTER]
%
giảm giá tối đa. VD: 70 → chỉ lấy SP giảm <= 70
%
."
,
)
)
product_line_vn
:
str
|
None
=
Field
(
description
=
(
"[SQL FILTER] Đại loại sản phẩm — lọc RỘNG bằng prefix. "
"Hỗ trợ nhiều giá trị cách bằng '/'. "
"GIÁ TRỊ: 'Áo' (mọi loại áo), 'Quần' (mọi loại quần), "
"'Váy' (váy liền + chân váy), 'Bộ' (bộ quần áo, bộ mặc nhà), "
"'Khăn', 'Mũ', 'Túi', 'Tất', 'Chăn'. "
"VD: 'áo khaki' → product_line_vn: 'Áo'. 'quần hoặc váy' → 'Quần/ Váy'"
),
)
discovery_mode
:
str
|
None
=
Field
(
discovery_mode
:
str
|
None
=
Field
(
description
=
(
description
=
(
"[SQL FILTER] Chế độ khám phá. 'new' = hàng mới (is_new_product=1), "
"[SQL FILTER] Chế độ khám phá. 'new' = hàng mới (is_new_product=1), "
...
@@ -182,7 +172,6 @@ async def _execute_single_search(
...
@@ -182,7 +172,6 @@ async def _execute_single_search(
"age"
:
item
.
age_by_product
,
"age"
:
item
.
age_by_product
,
"color"
:
item
.
master_color
,
"color"
:
item
.
master_color
,
"price_range"
:
f
"{item.price_min or ''}-{item.price_max or ''}"
,
"price_range"
:
f
"{item.price_min or ''}-{item.price_max or ''}"
,
"product_line_vn"
:
item
.
product_line_vn
,
"magento_ref_code"
:
item
.
magento_ref_code
,
"magento_ref_code"
:
item
.
magento_ref_code
,
"discovery_mode"
:
item
.
discovery_mode
,
"discovery_mode"
:
item
.
discovery_mode
,
},
},
...
@@ -224,7 +213,7 @@ async def data_retrieval_tool(searches: list[SearchItem]) -> str:
...
@@ -224,7 +213,7 @@ async def data_retrieval_tool(searches: list[SearchItem]) -> str:
for
i
,
s
in
enumerate
(
searches
):
for
i
,
s
in
enumerate
(
searches
):
logger
.
info
(
logger
.
info
(
"🔧 Search[
%
d]: desc=
%
r, name=
%
r, gender=
%
s, age=
%
s, color=
%
s, "
"🔧 Search[
%
d]: desc=
%
r, name=
%
r, gender=
%
s, age=
%
s, color=
%
s, "
"price=
%
s-
%
s,
line=
%
s,
code=
%
s, mode=
%
s"
,
"price=
%
s-
%
s, code=
%
s, mode=
%
s"
,
i
,
i
,
(
s
.
description
[:
80
]
+
"..."
)
if
s
.
description
and
len
(
s
.
description
)
>
80
else
s
.
description
,
(
s
.
description
[:
80
]
+
"..."
)
if
s
.
description
and
len
(
s
.
description
)
>
80
else
s
.
description
,
s
.
product_name
,
s
.
product_name
,
...
@@ -232,7 +221,6 @@ async def data_retrieval_tool(searches: list[SearchItem]) -> str:
...
@@ -232,7 +221,6 @@ async def data_retrieval_tool(searches: list[SearchItem]) -> str:
s
.
age_by_product
,
s
.
age_by_product
,
s
.
master_color
,
s
.
master_color
,
s
.
price_min
,
s
.
price_max
,
s
.
price_min
,
s
.
price_max
,
s
.
product_line_vn
,
s
.
magento_ref_code
,
s
.
magento_ref_code
,
s
.
discovery_mode
,
s
.
discovery_mode
,
)
)
...
@@ -263,7 +251,6 @@ async def data_retrieval_tool(searches: list[SearchItem]) -> str:
...
@@ -263,7 +251,6 @@ async def data_retrieval_tool(searches: list[SearchItem]) -> str:
search_inputs
=
[
search_inputs
=
[
{
{
"description"
:
item
.
description
,
"description"
:
item
.
description
,
"product_line_vn"
:
item
.
product_line_vn
,
"gender_by_product"
:
item
.
gender_by_product
,
"gender_by_product"
:
item
.
gender_by_product
,
"age_by_product"
:
item
.
age_by_product
,
"age_by_product"
:
item
.
age_by_product
,
"discovery_mode"
:
item
.
discovery_mode
,
"discovery_mode"
:
item
.
discovery_mode
,
...
...
backend/agent/tools/product_mapping.py
View file @
0205042f
...
@@ -12,7 +12,7 @@ PRODUCT_LINE_MAP: dict[str, list[str]] = {
...
@@ -12,7 +12,7 @@ PRODUCT_LINE_MAP: dict[str, list[str]] = {
"Áo nỉ có mũ"
:
[
"áo nỉ có mũ"
],
"Áo nỉ có mũ"
:
[
"áo nỉ có mũ"
],
"Áo nỉ"
:
[
"áo nỉ"
],
"Áo nỉ"
:
[
"áo nỉ"
],
"Áo mặc nhà"
:
[
"áo mặc nhà"
],
"Áo mặc nhà"
:
[
"áo mặc nhà"
],
"Áo lót"
:
[
"áo lót"
,
"áo ngực"
,
"áo quây"
],
"Áo lót"
:
[
"áo lót"
,
"áo ngực"
,
"áo quây"
,
"áo lót nữ"
,
"áo lót nam"
,
"áo lót trẻ em"
],
"Áo len gilet"
:
[
"áo len gilet"
],
"Áo len gilet"
:
[
"áo len gilet"
],
"Áo len"
:
[
"áo len"
],
"Áo len"
:
[
"áo len"
],
"Áo kiểu"
:
[
"áo kiểu"
],
"Áo kiểu"
:
[
"áo kiểu"
],
...
@@ -76,6 +76,19 @@ for db_value, synonyms in PRODUCT_LINE_MAP.items():
...
@@ -76,6 +76,19 @@ for db_value, synonyms in PRODUCT_LINE_MAP.items():
for
syn
in
synonyms
:
for
syn
in
synonyms
:
SYNONYM_TO_DB
[
syn
.
lower
()]
=
db_value
SYNONYM_TO_DB
[
syn
.
lower
()]
=
db_value
# ==============================================================================
# RELATED LINES: hỏi "áo bra" → tìm cả "Áo bra active" + "Áo lót" và ngược lại
# ==============================================================================
RELATED_LINES
:
dict
[
str
,
list
[
str
]]
=
{
"Áo bra active"
:
[
"Áo lót"
],
"Áo lót"
:
[
"Áo bra active"
],
}
def
get_related_lines
(
product_line
:
str
)
->
list
[
str
]:
"""VD: get_related_lines("Áo bra active") → ["Áo bra active", "Áo lót"]"""
return
[
product_line
]
+
RELATED_LINES
.
get
(
product_line
,
[])
# Pre-sort synonyms by length DESC for longest-match-first
# Pre-sort synonyms by length DESC for longest-match-first
_SORTED_SYNONYMS
=
sorted
(
SYNONYM_TO_DB
.
keys
(),
key
=
len
,
reverse
=
True
)
_SORTED_SYNONYMS
=
sorted
(
SYNONYM_TO_DB
.
keys
(),
key
=
len
,
reverse
=
True
)
...
...
backend/agent/tools/product_search_helpers.py
View file @
0205042f
...
@@ -87,18 +87,28 @@ def _get_metadata_clauses(params, sql_params: list) -> list[str]:
...
@@ -87,18 +87,28 @@ def _get_metadata_clauses(params, sql_params: list) -> list[str]:
GENERIC_WORDS
=
{
key
.
split
()[
0
]
.
lower
()
for
key
in
PRODUCT_LINE_MAP
.
keys
()}
GENERIC_WORDS
=
{
key
.
split
()[
0
]
.
lower
()
for
key
in
PRODUCT_LINE_MAP
.
keys
()}
name_val
=
getattr
(
params
,
"product_name"
,
None
)
name_val
=
getattr
(
params
,
"product_name"
,
None
)
if
name_val
:
if
name_val
:
from
agent.tools.product_mapping
import
resolve_product_name
from
agent.tools.product_mapping
import
resolve_product_name
,
get_related_lines
resolved_name
=
resolve_product_name
(
name_val
)
words
=
resolved_name
.
lower
()
.
strip
()
.
split
()
# Support '/' separator: "Áo lót/Áo bra active" → ["Áo lót", "Áo bra active"]
keywords
=
[
w
for
w
in
words
if
w
not
in
GENERIC_WORDS
and
len
(
w
)
>=
2
]
name_parts
=
[
p
.
strip
()
for
p
in
name_val
.
split
(
"/"
)
if
p
.
strip
()]
# Fallback: nếu tất cả đều generic (VD: "quần"), vẫn dùng làm filter
if
not
keywords
:
all_phrases
=
set
()
keywords
=
[
w
for
w
in
words
if
len
(
w
)
>=
2
]
for
part
in
name_parts
:
for
kw
in
keywords
:
resolved
=
resolve_product_name
(
part
)
clauses
.
append
(
"LOWER(product_name) LIKE
%
s"
)
# Also expand related lines
sql_params
.
append
(
f
"
%
{kw}
%
"
)
for
rname
in
get_related_lines
(
resolved
):
if
keywords
:
words
=
rname
.
strip
()
.
split
()
logger
.
info
(
f
"🏷️ [SQL FILTER] Product name: '{name_val}' → resolved: '{resolved_name}' → keywords: {keywords}"
)
phrase
=
" "
.
join
(
w
for
w
in
words
if
w
.
lower
()
not
in
GENERIC_WORDS
)
if
phrase
:
all_phrases
.
add
(
phrase
.
lower
())
if
all_phrases
:
like_parts
=
[]
for
phrase
in
all_phrases
:
like_parts
.
append
(
"LOWER(product_name) LIKE
%
s"
)
sql_params
.
append
(
f
"
%
{phrase}
%
"
)
clauses
.
append
(
f
"({' OR '.join(like_parts)})"
)
logger
.
info
(
f
"🏷️ [SQL FILTER] Product name: '{name_val}' → phrases: {all_phrases}"
)
return
clauses
return
clauses
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment