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

feat: fetch tool prompts from Langfuse API per request, add order flow to system prompt

parent 73b2d542
......@@ -21,7 +21,8 @@ from langgraph.types import CachePolicy
from common.llm_factory import create_llm
from .models import AgentConfig, AgentState, get_config
from .prompt import get_last_modified, get_system_prompt, get_system_prompt_template
from .prompt import get_system_prompt_template
from .prompt_utils import refresh_tool_prompts
from .tools.get_tools import get_all_tools, get_collection_tools
logger = logging.getLogger(__name__)
......@@ -48,11 +49,13 @@ class CANIFAGraph:
self.retrieval_tools = self.all_tools
self.llm_with_tools = self.llm.bind_tools(self.all_tools, strict=True)
# Lưu template với {date_str} placeholder → inject dynamic mỗi request
self.system_prompt_template = get_system_prompt_template()
self.prompt_template = ChatPromptTemplate.from_messages(
self.cache = InMemoryCache()
def _build_chain(self, system_prompt_template: str):
"""Build chain using the latest system prompt template."""
prompt_template = ChatPromptTemplate.from_messages(
[
("system", self.system_prompt_template),
("system", system_prompt_template),
(
"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=====================================",
......@@ -64,9 +67,7 @@ class CANIFAGraph:
MessagesPlaceholder(variable_name="messages"),
]
)
self.chain = self.prompt_template | self.llm_with_tools
self.cache = InMemoryCache()
return prompt_template | self.llm_with_tools
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."""
......@@ -107,7 +108,13 @@ class CANIFAGraph:
# Inject date_str ĐỘNG mỗi request (không cache từ __init__)
current_date_str = datetime.now().strftime("%d/%m/%Y")
response = await self.chain.ainvoke(
# Refresh tool prompts from Langfuse (cached via SDK TTL)
refresh_tool_prompts(self.all_tools)
system_prompt_template = get_system_prompt_template()
chain = self._build_chain(system_prompt_template)
response = await chain.ainvoke(
{
"date_str": current_date_str,
"history": history,
......@@ -181,34 +188,20 @@ def build_graph(config: AgentConfig | None = None, llm: BaseChatModel | None = N
def get_graph_manager(
config: AgentConfig | None = None, llm: BaseChatModel | None = None, tools: list | None = None
) -> CANIFAGraph:
"""Get CANIFAGraph instance (Auto-rebuild if model config changes OR prompt version changed)."""
current_prompt_version = get_last_modified()
"""Get CANIFAGraph instance (Auto-rebuild if model config changes)."""
# 1. New Instance if Empty
if _instance[0] is None:
_instance[0] = CANIFAGraph(config, llm, tools)
_instance[0].prompt_version = current_prompt_version
logger.info(f"✨ Graph Created: {_instance[0].config.model_name}, prompt_version={current_prompt_version}")
logger.info(f"✨ Graph Created: {_instance[0].config.model_name} (system prompt from Langfuse)")
return _instance[0]
# 2. Check for Config Changes (Model Switch OR Prompt Version Change)
# 2. Check for Config Changes (Model Switch only)
is_model_changed = config and config.model_name != _instance[0].config.model_name
is_prompt_changed = current_prompt_version != getattr(_instance[0], "prompt_version", 0)
if is_model_changed or is_prompt_changed:
change_reason = []
if is_model_changed:
change_reason.append(f"Model ({_instance[0].config.model_name}->{config.model_name})")
if is_prompt_changed:
change_reason.append(
f"Prompt Version ({getattr(_instance[0], 'prompt_version', 0)}->{current_prompt_version})"
)
logger.info(f"🔄 Rebuilding Graph due to: {', '.join(change_reason)}")
if is_model_changed:
logger.info(f"🔄 Rebuilding Graph: Model ({_instance[0].config.model_name}->{config.model_name})")
_instance[0] = CANIFAGraph(config, llm, tools)
_instance[0].prompt_version = current_prompt_version
return _instance[0]
return _instance[0]
......
"""
CiCi Fashion Consultant - System Prompt
Tư vấn thời trang CANIFA chuyên nghiệp
Version 3.4 - Simplified Summary History
Last updated: 2026-01-29 11:27
"""
import os
from datetime import datetime
PROMPT_FILE_PATH = os.path.join(os.path.dirname(__file__), "system_prompt.txt")
def get_system_prompt() -> str:
"""
System prompt cho CiCi Fashion Agent
Đọc từ file system_prompt.txt để có thể update dynamic.
Returns:
str: System prompt với ngày hiện tại
"""
now = datetime.now()
date_str = now.strftime("%d/%m/%Y")
try:
if os.path.exists(PROMPT_FILE_PATH):
with open(PROMPT_FILE_PATH, "r", encoding="utf-8") as f:
prompt_template = f.read()
return prompt_template.replace("{date_str}", date_str)
except Exception as e:
print(f"Error reading system prompt file: {e}")
# Fallback default prompt if file error
return f"""# VAI TRÒ
Bạn là CiCi - Chuyên viên tư vấn thời trang CANIFA.
Hôm nay: {date_str}
KHÔNG BAO GIỜ BỊA ĐẶT. TRẢ LỜI NGẮN GỌN.
"""
def get_system_prompt_template() -> str:
"""
Trả về system prompt template CHƯA replace {date_str}.
Dùng cho ChatPromptTemplate để inject ngày động mỗi request.
Returns:
str: System prompt template với placeholder {date_str}
"""
try:
if os.path.exists(PROMPT_FILE_PATH):
with open(PROMPT_FILE_PATH, "r", encoding="utf-8") as f:
return f.read()
except Exception as e:
print(f"Error reading system prompt file: {e}")
# Fallback default prompt if file error
return """# VAI TRÒ
Bạn là CiCi - Chuyên viên tư vấn thời trang CANIFA.
Hôm nay: {date_str}
KHÔNG BAO GIỜ BỊA ĐẶT. TRẢ LỜI NGẮN GỌN.
"""
def get_last_modified() -> float:
"""Trả về timestamp lần sửa cuối cùng của file system_prompt.txt."""
try:
if os.path.exists(PROMPT_FILE_PATH):
return os.path.getmtime(PROMPT_FILE_PATH)
except Exception:
pass
return 0.0
\ No newline at end of file
"""
System prompt loader.
Source of truth: Langfuse Prompt Management (fallback to local file).
"""
import logging
import os
from datetime import datetime
from typing import Any
from common.langfuse_client import get_langfuse_client
logger = logging.getLogger(__name__)
PROMPT_FILE_PATH = os.path.join(os.path.dirname(__file__), "system_prompt.txt")
LANGFUSE_SYSTEM_PROMPT_NAME = os.getenv("LANGFUSE_SYSTEM_PROMPT_NAME", "canifa-stylist-system-prompt")
LANGFUSE_SYSTEM_PROMPT_LABEL = os.getenv("LANGFUSE_SYSTEM_PROMPT_LABEL", "production")
LANGFUSE_SYSTEM_PROMPT_TAGS = [
tag.strip() for tag in os.getenv("LANGFUSE_SYSTEM_PROMPT_TAGS", "canifa,system-prompt").split(",")
if tag.strip()
]
try:
LANGFUSE_PROMPT_CACHE_TTL_SECONDS = int(os.getenv("LANGFUSE_PROMPT_CACHE_TTL_SECONDS", "0"))
except ValueError:
LANGFUSE_PROMPT_CACHE_TTL_SECONDS = 0
def _get_fallback_prompt_template() -> str:
return """# VAI TRÒ
Bạn là CiCi - Chuyên viên tư vấn thời trang CANIFA.
Hôm nay: {date_str}
KHÔNG BAO GIỜ BỊA ĐẶT. TRẢ LỜI NGẮN GỌN.
"""
def _read_local_prompt_template() -> str | None:
try:
if os.path.exists(PROMPT_FILE_PATH):
with open(PROMPT_FILE_PATH, "r", encoding="utf-8") as f:
return f.read()
except Exception as e:
logger.warning("Failed to read local system prompt: %s", e)
return None
def _to_runtime_template(prompt_text: str) -> str:
"""Langfuse prompt uses {{date_str}}; LangChain template needs {date_str}."""
return prompt_text.replace("{{date_str}}", "{date_str}")
def _to_langfuse_template(prompt_text: str) -> str:
"""Normalize single-brace placeholder from UI to Langfuse mustache format."""
if "{date_str}" in prompt_text and "{{date_str}}" not in prompt_text:
return prompt_text.replace("{date_str}", "{{date_str}}")
return prompt_text
def _get_langfuse_prompt() -> Any | None:
client = get_langfuse_client()
if not client:
return None
try:
return client.get_prompt(
LANGFUSE_SYSTEM_PROMPT_NAME,
label=LANGFUSE_SYSTEM_PROMPT_LABEL,
cache_ttl_seconds=LANGFUSE_PROMPT_CACHE_TTL_SECONDS,
)
except Exception as e:
logger.warning("Failed to fetch system prompt from Langfuse: %s", e)
return None
def write_system_prompt_file(content: str) -> bool:
"""Persist prompt locally as fallback snapshot."""
try:
with open(PROMPT_FILE_PATH, "w", encoding="utf-8") as f:
f.write(content)
return True
except Exception as e:
logger.error("Failed to write local system prompt file: %s", e)
return False
def create_system_prompt_version(content: str) -> int | None:
"""Create a new system prompt version in Langfuse."""
client = get_langfuse_client()
if not client:
return None
normalized_content = _to_langfuse_template(content)
try:
prompt = client.create_prompt(
name=LANGFUSE_SYSTEM_PROMPT_NAME,
prompt=normalized_content,
labels=[LANGFUSE_SYSTEM_PROMPT_LABEL],
tags=LANGFUSE_SYSTEM_PROMPT_TAGS,
type="text",
)
version = getattr(prompt, "version", None)
return int(version) if version is not None else None
except Exception as e:
logger.warning("Failed to create Langfuse system prompt version: %s", e)
return None
def get_system_prompt() -> str:
"""
Return compiled system prompt with current date.
Priority: Langfuse -> local file -> hardcoded fallback.
"""
date_str = datetime.now().strftime("%d/%m/%Y")
prompt = _get_langfuse_prompt()
if prompt is not None:
try:
return str(prompt.compile(date_str=date_str))
except Exception as e:
logger.warning("Langfuse prompt compile failed: %s", e)
local_template = _read_local_prompt_template()
if local_template:
return _to_runtime_template(local_template).replace("{date_str}", date_str)
return _get_fallback_prompt_template().replace("{date_str}", date_str)
def get_system_prompt_template() -> str:
"""
Return template with {date_str} placeholder for ChatPromptTemplate.
Priority: Langfuse -> local file -> hardcoded fallback.
"""
prompt = _get_langfuse_prompt()
if prompt is not None:
raw_prompt = getattr(prompt, "prompt", None)
if isinstance(raw_prompt, str) and raw_prompt.strip():
return _to_runtime_template(raw_prompt)
local_template = _read_local_prompt_template()
if local_template:
return _to_runtime_template(local_template)
return _get_fallback_prompt_template()
def get_last_modified() -> float:
"""
Prompt version marker.
Prefer Langfuse prompt version, fallback to local file mtime.
"""
prompt = _get_langfuse_prompt()
if prompt is not None:
version = getattr(prompt, "version", None)
if version is not None:
try:
return float(version)
except (TypeError, ValueError):
pass
try:
if os.path.exists(PROMPT_FILE_PATH):
return os.path.getmtime(PROMPT_FILE_PATH)
except Exception:
pass
return 0.0
"""
Tool prompt loader.
Source of truth: Langfuse Prompt Management (no local file fallback).
"""
import os
import logging
import os
from typing import Any
logger = logging.getLogger(__name__)
# Directory name for tool prompts
PROMPTS_DIR_NAME = "tool_prompts"
PROMPTS_DIR = os.path.join(os.path.dirname(__file__), PROMPTS_DIR_NAME)
# ====================== LANGFUSE TOOL PROMPT CONFIG ======================
LANGFUSE_TOOL_PROMPT_LABEL = os.getenv("LANGFUSE_TOOL_PROMPT_LABEL", "production")
def get_tool_prompt_path(filename: str) -> str:
"""Get absolute path for a tool prompt file."""
if not filename.endswith(".txt"):
filename += ".txt"
return os.path.join(PROMPTS_DIR, filename)
try:
LANGFUSE_PROMPT_CACHE_TTL_SECONDS = int(os.getenv("LANGFUSE_PROMPT_CACHE_TTL_SECONDS", "0"))
except ValueError:
LANGFUSE_PROMPT_CACHE_TTL_SECONDS = 0
def read_tool_prompt(filename: str, default_prompt: str = "") -> str:
# Mapping: local filename (without .txt) → Langfuse prompt name
TOOL_PROMPT_LANGFUSE_MAP: dict[str, str] = {
"data_retrieval_tool": "canifa-tool-data-retrieval",
"brand_knowledge_tool": "canifa-tool-brand-knowledge",
"check_is_stock": "canifa-tool-check-stock",
"promotion_canifa_tool": "canifa-tool-promotion",
"take_order.prompt": "canifa-tool-take-order",
}
# Reverse mapping: Langfuse prompt name → tool function name (for refresh)
LANGFUSE_TO_TOOL_FUNC_MAP: dict[str, str] = {
"canifa-tool-data-retrieval": "data_retrieval_tool",
"canifa-tool-brand-knowledge": "canifa_knowledge_search",
"canifa-tool-check-stock": "check_is_stock",
"canifa-tool-promotion": "canifa_get_promotions",
"canifa-tool-take-order": "create_customer_order",
}
def _fetch_langfuse_tool_prompt(langfuse_name: str) -> str | None:
"""
Read tool prompt from file.
Returns default_prompt if file not found or empty.
Fetch tool prompt from Langfuse by prompt name.
Uses Langfuse SDK built-in cache (cache_ttl_seconds).
"""
file_path = get_tool_prompt_path(filename)
try:
if os.path.exists(file_path):
with open(file_path, "r", encoding="utf-8") as f:
content = f.read().strip()
if content:
return content
from common.langfuse_client import get_langfuse_client
client = get_langfuse_client()
if not client:
return None
prompt = client.get_prompt(
langfuse_name,
label=LANGFUSE_TOOL_PROMPT_LABEL,
cache_ttl_seconds=LANGFUSE_PROMPT_CACHE_TTL_SECONDS,
)
raw_prompt = getattr(prompt, "prompt", None)
if isinstance(raw_prompt, str) and raw_prompt.strip():
logger.debug(
"✅ Tool prompt '%s' loaded from Langfuse (v%s)",
langfuse_name,
getattr(prompt, "version", "?"),
)
return raw_prompt.strip()
except Exception as e:
logger.error(f"Error reading tool prompt {filename}: {e}")
logger.warning("⚠️ Failed to fetch tool prompt '%s' from Langfuse: %s", langfuse_name, e)
return None
def read_tool_prompt(filename: str, default_prompt: str = "") -> str:
"""
Read tool prompt from Langfuse. Falls back to default_prompt if unavailable.
"""
lookup_key = filename.removesuffix(".txt")
langfuse_name = TOOL_PROMPT_LANGFUSE_MAP.get(lookup_key)
if langfuse_name:
prompt = _fetch_langfuse_tool_prompt(langfuse_name)
if prompt:
return prompt
return default_prompt
def write_tool_prompt(filename: str, content: str) -> bool:
"""Write content to tool prompt file."""
file_path = get_tool_prompt_path(filename)
try:
os.makedirs(os.path.dirname(file_path), exist_ok=True)
with open(file_path, "w", encoding="utf-8") as f:
f.write(content)
return True
except Exception as e:
logger.error(f"Error writing tool prompt {filename}: {e}")
return False
def list_tool_prompts() -> list[str]:
"""List all available tool prompt files."""
try:
if not os.path.exists(PROMPTS_DIR):
return []
files = [f for f in os.listdir(PROMPTS_DIR) if f.endswith(".txt")]
return sorted(files)
except Exception as e:
logger.error(f"Error listing tool prompts: {e}")
return []
"""List all tool prompts available in Langfuse."""
return list(TOOL_PROMPT_LANGFUSE_MAP.keys())
def refresh_tool_prompts(tools: list[Any]) -> None:
"""
Refresh tool docstrings from Langfuse (per-request).
Matches tool.name → Langfuse prompt name → updates __doc__ and .description.
"""
for langfuse_name, tool_func_name in LANGFUSE_TO_TOOL_FUNC_MAP.items():
# Find matching tool
matching_tool = None
for t in tools:
if getattr(t, "name", None) == tool_func_name:
matching_tool = t
break
if not matching_tool:
continue
# Fetch prompt from Langfuse
prompt_text = _fetch_langfuse_tool_prompt(langfuse_name)
if prompt_text:
matching_tool.__doc__ = prompt_text
if hasattr(matching_tool, "description"):
matching_tool.description = prompt_text
......@@ -1026,6 +1026,46 @@ price_max = 400000
---
### 5.7. 🛒 CHỐT ĐƠN / ĐẶT HÀNG → GỌI `create_customer_order`
**TRIGGER WORDS (Phát hiện 1 trong các từ này → BẮT ĐẦU FLOW ĐẶT HÀNG):**
- "chốt", "chốt đơn", "mua luôn", "đặt hàng", "order", "lấy cái này", "mua", "ok lấy", "oke chốt", "đặt đi", "ship cho anh/em"
**⚠️ ĐIỀU KIỆN BẮT BUỘC TRƯỚC KHI GỌI TOOL:**
Khách PHẢI đã chọn đủ: **Sản phẩm + Size + Màu**. Nếu thiếu → HỎI cái thiếu TRƯỚC, KHÔNG upsell.
**FLOW CHUẨN (tuần tự, KHÔNG BỎ BƯỚC):**
1. **Khách nói "chốt/mua/đặt hàng"** → Kiểm tra đã đủ Sản phẩm + Size + Màu chưa?
- ❌ Thiếu → HỎI NGAY cái thiếu: "Anh muốn size/màu gì ạ?"
- ✅ Đủ → Chuyển bước 2
2. **Kiểm tra `user_insight.CONTACT_INFO`:**
- Field nào = "Missing" → **CHỈ HỎI field đó**, KHÔNG hỏi lại cái đã có
- VD: Name=Missing, Phone=Missing, Address=Missing → "Để em tạo đơn, anh cho em biết: Họ tên, SĐT và địa chỉ giao hàng nhé!"
- VD: Name=Nguyễn Văn A, Phone=Missing → "Anh A ơi, cho em SĐT và địa chỉ giao hàng để em tạo đơn nhé!"
3. **Đã đủ CONTACT_INFO** → Tóm tắt đơn + Xác nhận:
```
"📋 Xác nhận đơn hàng:
👤 Tên: [tên]
📱 SĐT: [phone]
📍 Địa chỉ: [address]
🛍️ Sản phẩm: [tên SP] - Size [size] - Màu [màu]
💰 Tổng: [giá] VNĐ
Anh/chị xác nhận đặt hàng không ạ?"
```
4. **Khách xác nhận** → GỌI TOOL `create_customer_order` với đầy đủ params
**🚫 CẤM:**
- Upsell/cross-sell KHI khách đã nói "chốt" mà chưa thu thập xong thông tin đặt hàng
- Tự bịa thông tin khách hàng
- Bỏ qua bước hỏi CONTACT_INFO
- Hỏi lại field đã có trong user_insight
---
## 6. XỬ LÝ KẾT QUẢ TOOL
### Trường hợp 1: CÓ sản phẩm phù hợp (đúng loại, đúng yêu cầu)
......@@ -1520,6 +1560,60 @@ Turn 4: User nói 'xem mẫu khác' → Bot cần tìm váy đen khác, tránh 3
---
### 8.5. CHECKOUT FLOW — KHI KHÁCH MUỐN ĐẶT HÀNG ⭐⭐⭐
**TRIGGER:** Khách nói "đặt hàng" / "mua" / "order" / "chốt đơn" / "lấy mẫu này"
**⚠️ QUAN TRỌNG: Khi khách muốn đặt hàng → DỪNG TƯ VẤN → CHUYỂN SANG CHECKOUT MODE**
**FLOW BẮT BUỘC:**
1. **XÁC NHẬN SẢN PHẨM:** Bot confirm sản phẩm khách chọn (tên, SKU, giá)
2. **CHECK CONTACT_INFO:** Đọc user_insight.CONTACT_INFO
3. **THU THẬP THIẾU:**
- Name = "Missing" → Hỏi tên
- Phone = "Missing" → Hỏi SĐT
- Address = "Missing" → Hỏi địa chỉ giao hàng
- **CHỈ HỎI FIELD MISSING, KHÔNG HỎI LẠI CÁI ĐÃ CÓ**
- **HỎI TẤT CẢ FIELD MISSING CÙNG 1 LÚC** (không hỏi từng cái một)
4. **TÓM TẮT + XÁC NHẬN:** Khi đủ info → Tóm tắt đơn hàng → Xin xác nhận
5. **GỌI TOOL:** Khách confirm → Gọi `create_customer_order`
**❌ TUYỆT ĐỐI KHÔNG (khi khách đã nói đặt hàng):**
- KHÔNG hỏi thêm về size, cân nặng, style → Dùng info đã có hoặc bỏ qua
- KHÔNG tiếp tục tư vấn sản phẩm khác
- KHÔNG hỏi "còn muốn xem gì không" trước khi collect contact info
**VÍ DỤ CHECKOUT:**
Khách: "Ok lấy mẫu [8OT24W025] đi"
CONTACT_INFO hiện tại: "Name: Missing | Phone: Missing | Address: Missing | Email: Missing"
Bot response:
```
"Chốt [8OT24W025] Áo cardigan nỉ nam - 299k cho bạn nhé! 🎉
Để em tạo đơn, bạn cho em biết:
👤 Họ tên người nhận?
📱 Số điện thoại?
📍 Địa chỉ giao hàng?
Em sẽ xác nhận lại trước khi đặt cho bạn!"
```
**VÍ DỤ CHECKOUT (CÓ SẴN INFO):**
CONTACT_INFO: "Name: Nguyễn Văn Nam | Phone: 0912345678 | Address: Missing | Email: Missing"
Bot response:
```
"Chốt [8OT24W025] Áo cardigan nỉ nam - 299k cho anh Nam nhé! 🎉
Em có SĐT 0912345678 rồi. Anh cho em địa chỉ giao hàng để em tạo đơn luôn ạ!"
```
---
## 9. FORMAT ĐẦU RA
### ⚠️ CRITICAL - ĐỌC KỸ PHẦN NÀY:
......@@ -1881,28 +1975,28 @@ Bạn PHẢI trả về JSON thuần túy, KHÔNG ĐƯỢC wrap trong markdown b
---
### Example 12: BOT THỰC HIỆN [NEXT] - Chốt đơn ⭐
### Example 12: BOT THỰC HIỆN [NEXT] - Chốt đơn + Thu thập CONTACT_INFO ⭐⭐
**CONTEXT:** Turn trước khách nói thích mẫu X. `[NEXT]` ghi: "NẾU khách nói 'ok/thích' → Hướng dẫn chốt đơn"
**CONTEXT:** Turn trước khách nói thích mẫu X. `[NEXT]` ghi: "NẾU khách nói 'ok/thích' → Chốt đơn + Thu thập CONTACT_INFO"
**Input:** "Ok lấy mẫu 6VP24W010 đi"
**Bot ĐỌC [NEXT] từ turn trước** → Khách confirm → Thực hiện: Hướng dẫn chốt đơn
**Bot ĐỌC [NEXT] từ turn trước** → Khách confirm → **CHECKOUT MODE:** Confirm sản phẩm + Hỏi CONTACT_INFO thiếu
**Output (RAW JSON - KHÔNG CÓ ```json):**
{{
"ai_response": "Tuyệt vời, bạn chọn chuẩn rồi! 🎉 Mẫu [6VP24W010] đang sale chỉ 350k thôi nè!\n\n**Để mình confirm size cho vợ bạn:**\nVợ bạn cao 1m62, nặng 50kg, thường mặc size M → **Size M** là vừa đẹp luôn ạ!\n\n**Bước tiếp theo:**\n1. Bạn vào website canifa.com tìm mã [6VP24W010]\n2. Chọn size M, màu đen\n3. Checkout như bình thường\n\n💡 **Hoặc** bạn liên hệ hotline **1800 6061** để đặt hàng qua điện thoại cũng được ạ!\n\nBạn có cần mình tư vấn thêm gì không? Hay muốn tìm thêm món nào phối với váy này? 😊",
"ai_response": "Chốt [6VP24W010] Váy suông basic - 350k cho vợ bạn nhé! 🎉 Mẫu này đang sale từ 420k, size M chuẩn luôn ạ!\n\nĐể em tạo đơn hàng, bạn cho em biết:\n👤 Họ tên người nhận?\n📱 Số điện thoại?\n📍 Địa chỉ giao hàng?\n\nEm sẽ xác nhận lại trước khi đặt cho bạn! 😊",
"product_ids": ["6VP24W010"],
"user_insight": {{
"USER": "Nam, Adult, có vợ.",
"CONTACT_INFO": "Name: Missing | Phone: Missing | Address: Missing | Email: Missing",
"TARGET": "Vợ (Nữ, Adult, 1m62/50kg, size M, thích đen, ghét cổ điển).",
"GOAL": "ĐÃ CHỐT [6VP24W010] size M.",
"GOAL": "CHỐT ĐƠN [6VP24W010] size M - 350k. Đang thu thập CONTACT_INFO.",
"CONSTRAINS": "Budget: <400k (HARD), Màu: Đen (HARD), Size: M (HARD).",
"LATEST_PRODUCT_INTEREST": "[6VP24W010] - ĐÃ CHỐT",
"NEXT": "→ HÀNH ĐỘNG NGAY: Sẵn sàng hỗ trợ nếu khách gặp vấn đề khi đặt hàng\n→ NẾU khách muốn mua thêm → Gợi ý áo khoác/phụ kiện phối với váy\n→ NẾU khách cảm ơn → Kết thúc vui vẻ, mời quay lại",
"SUMMARY_HISTORY": "Turn 1-5: Tìm váy đen, điều chỉnh budget và style. Turn 6: User confirm chốt [6VP24W010] size M → Bot hướng dẫn mua và gợi ý thêm."
"LATEST_PRODUCT_INTEREST": "[6VP24W010] - ĐÃ CHỐT, ĐANG TẠO ĐƠN",
"NEXT": "→ HÀNH ĐỘNG NGAY: Chờ khách cung cấp tên + SĐT + địa chỉ\n→ NẾU khách cho đủ info → Tóm tắt đơn hàng + xin xác nhận → Gọi tool create_customer_order\n→ NẾU khách cho 1 phần → Hỏi tiếp phần còn thiếu\n→ NẾU khách đổi ý → Quay lại tư vấn",
"SUMMARY_HISTORY": "Turn 1-5: Tìm váy đen, điều chỉnh budget và style. Turn 6: User confirm chốt [6VP24W010] → Bot CHECKOUT MODE: hỏi tên + SĐT + địa chỉ để tạo đơn."
}}
}}
......
Tra cứu TOÀN BỘ thông tin về thương hiệu và dịch vụ của Canifa.
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. THƯƠNG HIỆU & GIỚI THIỆU: Lịch sử hình thành, giá trị cốt lõi, sứ mệnh.
2. HỆ THỐNG CỬA HÀNG: Tìm địa chỉ, số điện thoại, giờ mở cửa các cửa hàng tại các tỉnh thành (Hà Nội, HCM, Đà Nẵng, v.v.).
3. CHÍNH SÁCH BÁN HÀNG: Quy định đổi trả, bảo hành, chính sách vận chuyển, phí ship.
4. KHÁCH HÀNG THÂN THIẾT (KHTT): Điều kiện đăng ký thành viên, các hạng thẻ (Green, Silver, Gold, Diamond), quyền lợi tích điểm, thẻ quà tặng.
5. HỖ TRỢ & FAQ: Giải đáp thắc mắc thường gặp, chính sách bảo mật, thông tin liên hệ văn phòng, tuyển dụng.
6. TRA CỨU SIZE (BẢNG KÍCH CỠ):
- Hướng dẫn chọn size chuẩn cho nam, nữ, trẻ em dựa trên chiều cao, cân nặng.
- ⚠️ KHI KHÁCH ĐƯA SỐ ĐO (cân nặng, chiều cao, số đo 3 vòng) → BẮT BUỘC gọi tool này để tra bảng size, KHÔNG ĐƯỢC tự đoán size.
- Nếu khách chỉ đưa cân nặng mà thiếu chiều cao (hoặc ngược lại), hỏi thêm thông tin còn thiếu TRƯỚC khi gọi tool.
7. GIẢI NGHĨA TỪ VIẾT TẮT: Tự động hiểu các từ viết tắt phổ biến của khách hàng (ví dụ: 'ct' = 'chương trình khuyến mãi/ưu đãi', 'khtt' = 'khách hàng thân thiết', 'store' = 'cửa hàng', 'đc' = 'địa chỉ').
Ví dụ các câu hỏi phù hợp:
- 'Bên bạn đang có ct gì không?' (Hiểu là: Chương trình khuyến mãi)
- 'Canifa ở Cầu Giấy địa chỉ ở đâu?'
- 'Chính sách đổi trả hàng trong bao nhiêu ngày?'
- 'Làm sao để lên hạng thẻ Gold?'
- 'Phí vận chuyển đi tỉnh là bao nhiêu?'
- 'Canifa thành lập năm nào?'
Ví dụ câu hỏi TRA CỨU SIZE (phải gọi tool):
- 'Cho mình xem bảng size áo nam.'
- 'Mình nặng 80kg cao 1m75, mặc size gì?'
- 'Tìm áo cho người bự con tầm 80kg'
- 'Size XL tương đương bao nhiêu kg?'
- 'Con mình 8 tuổi cao 1m30, mặc size nào?'
- 'Mình 65kg 1m68 nên mặc áo size gì?'
- 'Bảng size quần jean nữ'
- 'Size M áo polo nam đo ngực bao nhiêu?'
- 'Mình mập, 90kg mặc được size nào?'
- 'Cho em hỏi bảng size trẻ em 3-5 tuổi'
Công cụ KIỂM TRA TỒN KHO sản phẩm CANIFA theo mã sản phẩm.
KHI NÀO GỌI TOOL NÀY:
- Khách hỏi "còn hàng không?", "còn size không?", "check tồn kho"
- Khách hỏi về MÃ SẢN PHẨM CỤ THỂ kèm từ khóa tồn kho (vd: "8TS24W001 còn size L không?")
- Khách muốn biết số lượng tồn kho của một hoặc nhiều mã sản phẩm
KHÔNG GỌI TOOL NÀY:
- Khách tìm kiếm sản phẩm theo mô tả (dùng data_retrieval_tool thay thế)
- Khách hỏi giá, thông tin sản phẩm (dùng data_retrieval_tool)
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.
----- VÍ DỤ CHI TIẾT -----
CASE 1: KIỂM TRA TỒN KHO MÃ CỤ THỂ
User: "6TE25C019-SK010 mã này còn hàng không?"
-> Gọi check_is_stock với:
- skus: "6TE25C019-SK010"
CASE 2: KIỂM TRA NHIỀU MÃ
User: "Check tồn kho 2 mã: 8TS24W001 và 6ST25W005"
-> Gọi check_is_stock với:
- skus: "8TS24W001,6ST25W005"
CASE 3: KIỂM TRA MÃ KÈM SIZE
User: "Mã 6ST25W005-SE091 còn size M và L không?"
-> Gọi check_is_stock với:
- skus: "6ST25W005-SE091"
- sizes: "M,L"
CASE 4: KIỂM TRA MÃ BASE (TỰ EXPAND)
User: "6ST25W005 còn màu nào và size nào?"
-> Gọi check_is_stock với:
- skus: "6ST25W005"
(Tool sẽ tự động expand ra tất cả các biến thể từ DB)
CÁCH ĐỌC KẾT QUẢ:
- stock_responses: Danh sách tồn kho từng SKU
- is_in_stock: true/false - còn hàng hay không
- qty: số lượng tồn kho
- Nếu hết hàng -> Gợi ý size/màu khác còn hàng
Siêu công cụ tìm kiếm sản phẩm CANIFA - Hỗ trợ Parallel Multi-Search.
⚠️⚠️⚠️ QUY TẮC BẮT BUỘC ⚠️⚠️⚠️
1. TUYỆT ĐỐI CẤM BỊA MÃ SẢN PHẨM — Chỉ đề cập khi tool trả về
2. LUÔN GỌI TOOL khi user hỏi về sản phẩm — KHÔNG đoán/nhớ
3. Không tìm thấy → nói thật, gợi ý tiêu chí khác
4. AI_RESPONSE chỉ sinh SAU KHI có kết quả tool
QUY TẮC SINH QUERIES:
- 1 món → 1 Query
- Set đồ/phối đồ → 2-3 Queries song song
═══════════════════════════════════════════════════════════════
⚠️ PHÂN BIỆT: `description` vs SQL FILTER
═══════════════════════════════════════════════════════════════
🔍 `description` — SEMANTIC SEARCH (format DB columns):
product_name: [tên SP]. description_text: [mô tả chi tiết SP].
material_group: [chất liệu]. season: [mùa]. style: [phong cách].
fitting: [dáng]. form_neckline: [cổ]. form_sleeve: [tay]. product_line_vn: [dòng SP].
⚠️ description_text BẮT BUỘC LUÔN CÓ — mô tả ngắn gọn sản phẩm, dùng cho semantic search!
⚠️ KHÔNG đưa gender_by_product, age_by_product, master_color vào description — đó là SQL FILTER!
🔒 SQL FILTER (tách riêng, KHÔNG đưa vào description):
- product_name: Tên sản phẩm. Chỉ điền khi khách cung cấp tên cụ thể.
- gender_by_product — women, men, boy, girl, unisex, newborn
- age_by_product — adult, kid, others
- master_color — Màu sản phẩm. Gửi CHÍNH XÁC từ khách nói (VD: 'trắng', 'đen', 'xanh'). Tool tự match DB.
- price_min / price_max — Khoảng giá VND
- discount_min / discount_max — % giảm giá
- magento_ref_code — Mã SKU chính xác
- product_line_vn — Dòng SP RỘNG. Dùng LIKE prefix nên chỉ cần "Áo", "Quần", "Váy"
- discovery_mode — "new" (hàng mới) hoặc "best_seller" (bán chạy). Chỉ dùng khi khách NÓI RÕ!
═══════════════════════════════════════════════════════════════
🆕🔥 discovery_mode — HÀNG MỚI / BÁN CHẠY
═══════════════════════════════════════════════════════════════
TRIGGER WORDS (phát hiện 1 trong các từ này → dùng discovery_mode):
- Hàng mới: "mới nhất", "hàng mới", "mới về", "new arrival", "có gì mới", "mới ra"
- Bán chạy: "bán chạy", "best seller", "hot nhất", "mọi người hay mua", "sản phẩm hot"
GIÁ TRỊ: discovery_mode="new" HOẶC discovery_mode="best_seller"
Nếu khách KHÔNG nói → KHÔNG điền (để None)
VÍ DỤ discovery_mode:
- "Áo phông mới nhất?" → description: "product_name: Áo phông. description_text: Áo phông mới nhất", product_line_vn: "Áo phông", discovery_mode: "new"
- "Quần jean bán chạy cho nam?" → description: "product_name: Quần jean. description_text: Quần jean nam bán chạy", product_line_vn: "Quần jean", gender: "men", discovery_mode: "best_seller"
- "Áo dự tiệc nào bán nhiều nhất?" → description: "product_name: Áo. description_text: Áo đi dự tiệc sang trọng. style: Feminine", product_line_vn: "Áo", discovery_mode: "best_seller"
- "Best seller tháng này?" → description: "product_name: Sản phẩm. description_text: Sản phẩm bán chạy nhất", discovery_mode: "best_seller"
- "Tìm áo phông nam" (KHÔNG nói mới/bán chạy) → discovery_mode: None (bình thường)
⚠️ best_seller — BẮT BUỘC hiển thị quantity_sold:
- TỪNG sản phẩm PHẢI có số lượng đã bán: "Đã bán 4.483 sp 🔥"
- VD: "👕 [8TP25A005]: Áo polo nam - 229k (giảm từ 399k) | Đã bán 4.483 sp 🔥"
═══════════════════════════════════════════════════════════════
🚨🚨🚨 QUY TẮC product_name — CỰC KỲ QUAN TRỌNG 🚨🚨🚨
═══════════════════════════════════════════════════════════════
product_name = CHÍNH XÁC CÂU USER NÓI, KHÔNG ĐƯỢC TỰ ĐỔI.
User nói "áo" → product_name: Áo (KHÔNG tự thêm "phông", "polo", "sơ mi"!)
User nói "quần" → product_name: Quần (KHÔNG tự thêm "jean", "khaki"!)
User nói "áo ngọ nguậy" → product_name: Áo ngọ nguậy
User nói "áo cá sấu" → product_name: Áo cá sấu
User nói "áo phông" → product_name: Áo phông (user NÓI RÕ thì mới dùng)
User nói "quần jean" → product_name: Quần jean (user NÓI RÕ thì mới dùng)
Chỉ CHUẨN HÓA khi user dùng từ đồng nghĩa RÕ RÀNG (bảng mapping dưới):
📋 BẢNG MAPPING SYNONYM → TÊN DB (tool tự xử lý, LLM giữ nguyên từ user):
áo thun, áo thun ngắn tay, áo cổ v, áo cổ tym → Áo phông
áo cổ bẻ → Áo Polo
áo bra, áo ngực, áo quây → Áo lót
áo gió, áo khoác mỏng → Áo khoác gió
áo croptop, croptop, baby tee, áo lửng, áo dáng ngắn → Áo Body
áo sát nách, tanktop, tank top, áo dây, áo 2 dây, áo hai dây → Áo ba lỗ
đầm → Váy liền
vớ → Tất
quần đùi, quần short, quần lửng, quần ngố → Quần soóc
quần jogger, quần ống bo chun → Quần nỉ
quần chip, quần sịp, quần trong, quần nhỏ, quần xơ lít, quần xì, quần sơ lít, quần lót nữ, quần lót nam, quần lót trẻ em → Quần lót
quần âu, quần vải, quần tây → Quần Khaki
quần bò, quần jeans, denim, jeans, bò → Quần jean
quần suông, quần ống rộng → Quần dài
quần dài, chân váy dài, chân váy → Quần váy
nón → Mũ
đồ ngủ, đồ mặc nhà → Bộ mặc nhà
đồ bộ → Bộ quần áo
váy maxi, váy midi, chân váy dài → Chân váy
găng tay → Găng tay chống nắng
chăn → Chăn cá nhân
phụ kiện, phụ kiện canifa → Mũ, Khăn, Tất, Găng tay (nhiều loại)
⚠️ Tool tự resolve synonym → DB value. LLM chỉ cần giữ NGUYÊN từ user nói!
⚠️ KHÔNG CHẮC → GIỮ NGUYÊN. KHÔNG TỰ SUY DIỄN LOẠI SẢN PHẨM!
═══════════════════════════════════════════════════════════════
📖 product_line_vn — PHÂN LOẠI RỘNG (SQL FILTER dùng LIKE prefix)
═══════════════════════════════════════════════════════════════
⚠️ product_line_vn = THỂ LOẠI RỘNG, KHÔNG phải loại cụ thể!
SQL dùng LIKE prefix: "Áo" sẽ match "Áo phông", "Áo Polo", "Áo Sơ mi"...
QUY TẮC:
- User nói "áo" chung → product_line_vn: "Áo" (match TẤT CẢ loại áo)
- User nói "quần" chung → product_line_vn: "Quần" (match tất cả loại quần)
- User nói "váy" chung → product_line_vn: "Váy" (match tất cả)
- User nói RÕ loại cụ thể "áo polo" → product_line_vn: "Áo Polo"
- User nói RÕ loại cụ thể "quần jean" → product_line_vn: "Quần jean"
KHÔNG TỰ THU HẸP: User nói "áo" → ĐỪNG tự đổi thành "Áo phông"!
Bảng tham khảo (chỉ dùng khi user NÓI RÕ loại):
ÁO: Áo phông, Áo Polo, Áo Sơ mi, Áo len, Áo nỉ, Áo khoác gió
QUẦN: Quần soóc, Quần jean, Quần Khaki, Quần dài, Quần nỉ
VÁY: Váy liền, Chân váy
BỘ: Bộ quần áo, Bộ mặc nhà
PHỤ KIỆN: Khăn, Mũ, Túi xách, Tất, Khẩu trang
═══════════════════════════════════════════════════════════════
📖 GIÁ TRỊ HỢP LỆ CỦA CÁC FIELD KHÁC
═══════════════════════════════════════════════════════════════
product_name: Tên sản phẩm. Chỉ điền khi khách cung cấp tên cụ thể.
master_color — Gửi CHÍNH XÁC màu khách nói (VD: 'trắng', 'đen'), tool tự match DB. ĐÂY LÀ SQL FILTER, KHÔNG đưa vào description!
style — Basic, Dynamic, Feminine, Utility, Smart Casual, Trend, Athleisure, Essential
fitting — Regular, Slimfit, Relax, Oversize, Skinny, Slim, Boxy, Baby tee
form_sleeve — Full length Sleeve, Short Sleeve, Sleeveless
form_neckline — Crew Neck, Classic Collar, V-neck, Hooded collar, Mock Neck/ High neck
material_group — Knit - Dệt Kim, Woven - Dệt Thoi, Yarn - Sợi
season — Fall Winter, Spring Summer, Year
═══════════════════════════════════════════════════════════════
📝 VÍ DỤ (description_text LUÔN CÓ!)
═══════════════════════════════════════════════════════════════
CASE 1: "Tìm áo đi chơi" (user chỉ nói "áo" — KHÔNG nói loại gì)
→ description: "product_name: Áo. description_text: Áo đi chơi thoải mái phong cách trẻ trung. style: Casual"
→ product_line_vn: "Áo" (RỘNG — match tất cả loại áo)
⚠️ KHÔNG tự đổi thành "Áo phông" hay "Áo Polo" — user đéo nói!
CASE 2: "Tìm áo ngọ nguậy"
→ description: "product_name: Áo ngọ nguậy. description_text: Áo ngọ nguậy thiết kế vui nhộn"
→ product_line_vn: "Áo"
CASE 3: "Áo phông nam màu trắng dáng regular" (user NÓI RÕ "áo phông")
→ description: "product_name: Áo phông. description_text: Áo phông nam dáng regular thoải mái. fitting: Regular"
→ master_color: "trắng"
→ gender_by_product: "men", age_by_product: "adult"
→ product_line_vn: "Áo phông"
CASE 4: "Váy nữ tính cho vợ, dưới 500k"
→ description: "product_name: Váy. description_text: Váy nữ tính sang trọng thanh lịch. style: Feminine"
→ gender_by_product: "women", age_by_product: "adult", price_max: 500000
→ product_line_vn: "Váy"
CASE 5: "Áo Polo ngắn tay basic cho nam, giảm giá 30-50%"
→ description: "product_name: Áo Polo. description_text: Áo Polo nam ngắn tay phong cách basic lịch sự. style: Basic. form_sleeve: Short Sleeve"
→ gender_by_product: "men", age_by_product: "adult", discount_min: 30, discount_max: 50
→ product_line_vn: "Áo Polo"
CASE 6: "Quần cho bé trai"
→ description: "product_name: Quần. description_text: Quần cho bé trai năng động thoải mái"
→ gender_by_product: "boy", age_by_product: "kid"
→ product_line_vn: "Quần"
CASE 7: "Set đồ công sở cho nữ" → 2 Queries:
1. description: "product_name: Áo. description_text: Áo công sở nữ thanh lịch chuyên nghiệp. style: Smart Casual"
gender: "women", age: "adult", product_line_vn: "Áo"
2. description: "product_name: Quần. description_text: Quần công sở nữ thanh lịch form đẹp. style: Smart Casual"
gender: "women", age: "adult", product_line_vn: "Quần"
CASE 8: "Tìm mã 6KS25S005"
→ magento_ref_code: "6KS25S005", description: "product_name: Sản phẩm. description_text: Tìm sản phẩm theo mã"
CASE 9: "Áo cá sấu polo đi chơi"
→ description: "product_name: Áo cá sấu polo. description_text: Áo cá sấu polo đi chơi năng động trẻ trung. style: Dynamic"
→ product_line_vn: "Áo Polo"
CASE 10: "Áo khaki"
→ description: "product_name: Áo khaki. description_text: Áo chất liệu khaki form đẹp"
→ product_line_vn: "Áo"
═══════════════════════════════════════════════════════════════
🎉 DỊP LỄ / SỰ KIỆN — description_text ghi lý do + gợi ý phong cách
═══════════════════════════════════════════════════════════════
Khi user hỏi "mặc gì dịp X" hoặc nhắc tới ngày lễ → description_text MÔ TẢ:
1. Dịp đó là gì (1 câu ngắn)
2. Nên mặc phong cách gì (1 câu)
⚠️ Danh sách dưới chỉ là VÍ DỤ phổ biến. Nếu user hỏi dịp KHÁC (sinh nhật, khai giảng, họp lớp, picnic, team building, du lịch Đà Lạt, concert, festival...) → TỰ SUY LUẬN theo pattern tương tự: mô tả dịp + gợi ý style phù hợp.
🇻🇳 SỰ KIỆN VIỆT NAM:
- Tết Nguyên Đán (tháng 1-2 AL) → description_text: "Tết Nguyên Đán lễ hội truyền thống, trang phục lịch sự sang trọng tông đỏ vàng may mắn. style: Feminine/Smart Casual"
- 8/3 Quốc tế Phụ nữ → description_text: "Ngày Quốc tế Phụ nữ 8/3, trang phục nữ tính thanh lịch tôn dáng. style: Feminine"
- 30/4 Giải phóng miền Nam → description_text: "Ngày Giải phóng 30/4 nghỉ lễ dài, trang phục đi chơi thoải mái năng động. style: Dynamic/Casual"
- 1/5 Quốc tế Lao động → description_text: "Nghỉ lễ 1/5 đi du lịch picnic, đồ thoải mái năng động dễ di chuyển. style: Athleisure/Dynamic"
- 1/6 Quốc tế Thiếu nhi → description_text: "Ngày Thiếu nhi 1/6, quần áo trẻ em vui nhộn màu sắc tươi sáng. style: Dynamic" + age: kid
- 2/9 Quốc khánh → description_text: "Quốc khánh 2/9 nghỉ lễ, trang phục đi chơi thoải mái tông đỏ vàng tự hào dân tộc. style: Dynamic/Casual"
- 20/10 Phụ nữ Việt Nam → description_text: "Ngày Phụ nữ Việt Nam 20/10, trang phục nữ tính sang trọng tôn vẻ đẹp phụ nữ. style: Feminine"
- 20/11 Nhà giáo VN → description_text: "Ngày Nhà giáo 20/11, trang phục lịch sự thanh lịch trang trọng. style: Smart Casual"
🌍 SỰ KIỆN QUỐC TẾ:
- 14/2 Valentine → description_text: "Valentine ngày tình nhân, trang phục lãng mạn đẹp đôi hẹn hò. style: Feminine/Smart Casual"
- Halloween (31/10) → description_text: "Halloween lễ hội hóa trang, trang phục cá tính độc đáo tông đen cam. style: Trend/Dynamic"
- Noel/Giáng sinh (25/12) → description_text: "Giáng sinh Noel lễ hội ấm áp, trang phục ấm đẹp tông đỏ xanh trắng. style: Smart Casual. season: Fall Winter"
📋 DỊP CHUNG:
- Đám cưới → description_text: "Đi đám cưới tiệc cưới, trang phục lịch sự sang trọng trang nhã. style: Feminine/Smart Casual"
- Đi làm/công sở → description_text: "Đi làm văn phòng công sở, trang phục chuyên nghiệp lịch sự gọn gàng. style: Smart Casual"
- Đi biển/du lịch → description_text: "Du lịch biển nghỉ mát, trang phục thoáng mát năng động dễ chịu. style: Dynamic. season: Spring Summer"
- Thể thao/gym → description_text: "Tập gym thể thao vận động, trang phục co giãn thoáng khí năng động. style: Athleisure"
- Hẹn hò/date → description_text: "Đi hẹn hò date, trang phục đẹp cuốn hút tự tin ấn tượng. style: Feminine/Smart Casual"
- Tiệc/party → description_text: "Đi tiệc party, trang phục nổi bật sang trọng thời thượng. style: Trend/Feminine"
VÍ DỤ DỊP LỄ:
"Mặc gì đi chơi 2/9?" → description: "product_name: Áo. description_text: Quốc khánh 2/9 nghỉ lễ đi chơi, áo thoải mái năng động tông đỏ vàng. style: Dynamic"
"Váy đi đám cưới" → description: "product_name: Váy. description_text: Váy đi đám cưới sang trọng thanh lịch trang nhã. style: Feminine"
"Đồ cho bé 1/6" → description: "product_name: Bộ quần áo. description_text: Ngày Thiếu nhi 1/6 quần áo trẻ em vui nhộn tươi sáng. style: Dynamic", age: "kid"
⚡ SUY LUẬN ĐỐI TƯỢNG:
- "vợ", "bà xã", "người yêu" → gender: women, age: adult
- "chồng", "anh ấy" → gender: men, age: adult
- "con gái", "bé gái" → gender: girl, age: kid
- "con trai", "bé trai" → gender: boy, age: kid
product_name: Áo polo nam basic dáng suông. \
master_color: Đỏ/ Red.
product_image_url: https://2885371169.e.cdneverest.net/pub/media/catalog/product/8/t/8tp24a003-sr190-xl-1-u.jpg.
product_image_url_thumbnail: https://2885371169.e.cdneverest.net/pub/media/catalog/product/8/t/8tp24a003-sr190-xl-1-u.jpg.
product_web_url: https://canifa.com/ao-polo-nam-8tp24a003?color=SR190&utm_source=chatbot&utm_medium=rsa&utm_campaign=ver1.
description_text: Áo polo nam basic dáng regular với bảng màu đa dạng, dễ dàng lựa chọn cho nhiều đối tượng khách hàng.
material: None.
material_group: Knit - Dệt Kim.
gender_by_product: men.
age_by_product: adult.
season: Year.
style: Basic.
fitting: Regular.
size_scale: S|XXL|L|M|XL|XS|XXXL.
form_neckline: Classic Collar.
form_sleeve: Short Sleeve.
product_line_vn: Áo Polo.
product_color_name: Red 190
026-02-05 17:09:02,421 [INFO] common.starrocks_connection: ✅ Pool created successfully with recycle=280s
2026-02-05 17:09:02,736 [INFO] agent.tools.data_retrieval_tool: _execute_single_search done, products=50, build_ms=978.50, db_ms=343.53, total_ms=1322.04
2026-02-05 17:09:02,739 [INFO] agent.tools.data_retrieval_tool: 🔍 [DEBUG] First product keys: ['internal_ref_code', 'magento_ref_code', 'product_color_code', 'product_name', 'master_color', 'product_image_url_thumbnail', 'product_web_url', 'sale_price', 'original_price', 'discount_amount', 'discount_percent', 'description_text_full', 'max_score']
2026-02-05 17:09:02,741 [INFO] agent.tools.data_retrieval_tool: 🔍 [DEBUG] First product price: 799000.000000, sale_price: 399500.000000
2026-02-05 17:09:02,745 [WARNING] agent.tools.data_retrieval_tool: 🔍 [POST-FILTER] Starting with 50 products from DB. SearchItem params: product_name='Váy liền', gender='women', age='adult', color='xanh navy'
2026-02-05 17:09:02,747 [INFO] agent.tools.data_retrieval_filter: 🎨 [COLOR FILTER] Trying exact match for: xanh navy
2026-02-05 17:09:02,748 [WARNING] agent.tools.data_retrieval_filter: ⚠️ [COLOR] No exact match for 'xanh navy' → Trying mapping fallback
2026-02-05 17:09:02,754 [INFO] agent.tools.data_retrieval_filter: 🔍 [COLOR TOKENS] Extracted: ['xanh', 'navy']
2026-02-05 17:09:02,755 [INFO] agent.tools.data_retrieval_filter: 📍 [COLOR MAPPING] Tokens ['xanh', 'navy'] → DB colors: ['Xanh da trời/ Blue', 'Màu xanh Jeans']
2026-02-05 17:09:02,758 [INFO] agent.tools.data_retrieval_filter: ✅ [COLOR FALLBACK] Found 11 products in colors: ['Xanh da trời/ Blue']
2026-02-05 17:09:02,759 [INFO] agent.tools.data_retrieval_filter: 📝 [RECOMMENDATION] Shop không có màu 'xanh navy'. Chỉ có màu 'Xanh da trời/ Blue'.
2026-02-05 17:09:02,761 [WARNING] agent.tools.data_retrieval_tool: 🎨 COLOR FALLBACK: 'xanh navy' → 'Xanh da trời/ Blue' (11 products)
2026-02-05 17:09:02,762 [INFO] agent.tools.data_retrieval_tool: 💡 Alternatives: Xanh da trời/ Blue
2026-02-05 17:09:02,764 [INFO] agent.tools.data_retrieval_tool: 🎨 Color filter done: 50 → 11 products
2026-02-05 17:09:02,765 [INFO] agent.tools.data_retrieval_tool: 📊 Post-filter summary: 50 → 11 products.
2026-02-05 17:09:02,773 [INFO] agent.tools.data_retrieval_filter: 📦 Formatted 11 products (all flat - 1 product = 1 màu)
2026-02-05 17:09:02,775 [INFO] agent.tools.data_retrieval_tool: ⚡ Optimization: Truncating results 11 -> 8 for stock check
2026-02-05 17:09:02,775 [INFO] agent.tools.data_retrieval_tool: 🎁 Final result: 8 products. Fallback used: True. Stock enriched: False
2026-02-05 17:09:02,781 [INFO] agent.controller: 📦 Event #2 at t=5.93s | Keys: ['retrieve_tools']
2026-02-05 17:09:10,470 [WARNING] agent.streaming_callback: 🎯 FOUND product_ids at 1046 chars!
2026-02-05 17:09:10,472 [WARNING] agent.streaming_callback: ✅ Extracted 3 SKUs: ['6DS25S017', '6DS25S003', '6DS25S007']
2026-02-05 17:09:10,473 [INFO] agent.streaming_callback: ✅ product_ids found → response can return early (stream continues)
2026-02-05 17:09:10,475 [INFO] agent.controller: ⚡ Response ready at t=13.63s (early return)
2026-02-05 17:09:10,479 [INFO] agent.controller: ℹ️ Extracted ai_response via regex from streaming content
2026-02-05 17:09:10,480 [WARNING] agent.helper: 🛠️ [EXTRACT] Extracted 8 products from tool
2026-02-05 17:09:10,481 [INFO] agent.controller: ✅ Extracted 8 products from 2 accumulated messages
2026-02-05 17:09:10,482 [WARNING] agent.controller: 🎯 AI mentioned 3 SKUs → Filtered 8 down to 3 products
2026-02-05 17:09:10,488 [INFO] agent.controller: 💾 [PROD] Scheduling background task for user_insight extraction (post-stream)
2026-02-05 17:09:10,493 [INFO] agent.controller: chat_controller finished: cached=False
2026-02-05 17:09:10,495 [WARNING] api.chatbot_route: 🔍 [DEBUG] usage_info = {'limit': 5, 'used': 2, 'total_used': 2, 'guest_used': 2, 'user_used': 0, 'remaining': 3, 'reset_seconds': 24649, 'is_authenticated': False}
←[32mINFO←[0m: 127.0.0.1:64945 - "←[1mPOST /api/agent/chat-dev HTTP/1.1←[0m" ←[32m200 OK←[0m
←[32mINFO←[0m: 127.0.0.1:64945 - "←[1mGET /api/agent/user-insight HTTP/1.1←[0m" ←[32m200 OK←[0m
←[32mINFO←[0m: 127.0.0.1:64945 - "←[1mGET /api/agent/user-insight HTTP/1.1←[0m" ←[32m200 OK←[0m
←[32mINFO←[0m: 127.0.0.1:64945 - "←[1mGET /api/agent/user-insight HTTP/1.1←[0m" ←[32m200 OK←[0m
←[32mINFO←[0m: 127.0.0.1:64945 - "←[1mGET /api/agent/user-insight HTTP/1.1←[0m" ←[32m200 OK←[0m
←[32mINFO←[0m: 127.0.0.1:64945 - "←[1mGET /api/agent/user-insight HTTP/1.1←[0m" ←[32m200 OK←[0m
←[32mINFO←[0m: 127.0.0.1:64945 - "←[1mGET /api/agent/user-insight HTTP/1.1←[0m" ←[32m200 OK←[0m
←[32mINFO←[0m: 127.0.0.1:64945 - "←[1mGET /api/agent/user-insight HTTP/1.1←[0m" ←[32m200 OK←[0m
←[32mINFO←[0m: 127.0.0.1:64945 - "←[1mGET /api/agent/user-insight HTTP/1.1←[0m" ←[32m200 OK←[0m
←[32mINFO←[0m: 127.0.0.1:64945 - "←[1mGET /api/agent/user-insight HTTP/1.1←[0m" ←[32m200 OK←[0m
←[32mINFO←[0m: 127.0.0.1:64945 - "←[1mGET /api/agent/user-insight HTTP/1.1←[0m" ←[32m200 OK←[0m
←[32mINFO←[0m: 127.0.0.1:64945 - "←[1mGET /api/agent/user-insight HTTP/1.1←[0m" ←[32m200 OK←[0m
←[32mINFO←[0m: 127.0.0.1:64945 - "←[1mGET /api/agent/user-insight HTTP/1.1←[0m" ←[32m200 OK←[0m
←[32mINFO←[0m: 127.0.0.1:64945 - "←[1mGET /api/agent/user-insight HTTP/1.1←[0m" ←[32m200 OK←[0m
←[32mINFO←[0m: 127.0.0.1:64945 - "←[1mGET /api/agent/user-insight HTTP/1.1←[0m" ←[32m200 OK←[0m
←[32mINFO←[0m: 127.0.0.1:64945 - "←[1mGET /api/agent/user-insight HTTP/1.1←[0m" ←[32m200 OK←[0m
2026-02-05 17:09:13,810 [INFO] agent.graph: 🏁 Agent finished
2026-02-05 17:09:13,813 [INFO] agent.controller: 📦 Event #3 at t=16.97s | Keys: ['agent']
2026-02-05 17:09:13,814 [INFO] agent.controller_helpers: 🔄 [Background] Starting user_insight extraction for cdcdc3dfef
2026-02-05 17:09:13,818 [INFO] agent.controller_helpers: 💾 Updated User Insight for cdcdc3dfef: {
"USER": "Nam, Adult, xưng anh.",
"TARGET": "...
2026-02-05 17:09:13,818 [WARNING] agent.controller_helpers: ✅ [user_insight] Extracted + saved in 0.00s | Key: cdcdc3dfef
←[32mINFO←[0m: 127.0.0.1:64945 - "←[1mGET /api/agent/user-insight HTTP/1.1←[0m" ←[32m200 OK←[0m
┌─────────────────────────────────────────────────────────┐
│ User: "phụ kiện nào hợp với áo này?" │
└───────────────────────┬─────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ LLM (thông minh): Hiểu "phụ kiện" → │
│ Tự điền product_name = "Khăn/ Mũ/ Túi xách/ Tất" │
│ (tách ra từng loại cụ thể thay vì gửi chung "phụ kiện")│
└───────────────────────┬─────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Code split "/": ["khăn", "mũ", "túi xách", "tất"] │
│ ↓ │
│ PRODUCT_TYPE_MAPPING: │
│ "khăn" → "Khăn" (product_line_vn) │
│ "mũ" → "Mũ" (product_line_vn) │
│ "túi xách" → "Túi xách" (product_line_vn) │
│ "tất" → "Tất" (product_line_vn) │
└───────────────────────┬─────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Filter: Chỉ giữ products có product_line_vn │
│ == "Khăn" OR "Mũ" OR "Túi xách" OR "Tất" │
│ │
│ ❌ "Áo Cổ Cao Tay Dài" → loại bỏ │
│ ✅ "Khăn" → giữ │
│ ✅ "Mũ" → giữ │
└─────────────────────────────────────────────────────────┘
\ No newline at end of file
Tra cứu danh sách các chương trình khuyến mãi (CTKM) đang diễn ra theo ngày.
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ề:
- "Hôm nay có khuyến mãi gì không?"
- "Đang có chương trình gì hot?"
- "Ngày mai có giảm giá không?"
- "Danh sách mã giảm giá hiện tại."
Input:
- check_date (YYYY-MM-DD hoặc null). Nếu null, mặc định dùng ngày hiện tại.
Output:
- Danh sách CTKM gồm: Tên chương trình, mô tả, thời gian áp dụng.
Nếu không có CTKM, hãy thông báo rõ không có chương trình nào đang diễn ra.
\ No newline at end of file
Công cụ quản lý đơn hàng CANIFA — Tạo đơn & Hủy đơn.
═══════════════════════════════════════════════════════════════
⚠️⚠️⚠️ QUY TẮC BẮT BUỘC ⚠️⚠️⚠️
═══════════════════════════════════════════════════════════════
1. CHỈ gọi create_customer_order khi khách ĐÃ XÁC NHẬN muốn đặt hàng
2. PHẢI thu thập ĐỦ thông tin bắt buộc TRƯỚC KHI gọi tool:
- customer_name (họ tên)
- phone (số điện thoại)
- shipping_address (địa chỉ giao hàng)
- total_amount (tổng tiền)
3. KHÔNG tự bịa thông tin khách hàng — thiếu gì thì HỎI
4. Xác nhận lại toàn bộ thông tin với khách TRƯỚC KHI tạo đơn
═══════════════════════════════════════════════════════════════
🛒 TOOL: create_customer_order — TẠO ĐƠN HÀNG
═══════════════════════════════════════════════════════════════
FLOW CHUẨN (tuần tự):
1. Khách chọn sản phẩm → Ghi nhận sản phẩm, số lượng, giá
2. Khách nói "đặt hàng" / "mua" / "order" → CHECK user_insight.CONTACT_INFO
3. Kiểm tra CONTACT_INFO:
✅ ĐỦ INFO (Name + Phone + Address) → Tóm tắt + xác nhận luôn
❌ THIẾU INFO → CHỈ hỏi những field ghi "Missing", KHÔNG hỏi lại cái đã có
4. Tóm tắt lại đơn hàng cho khách xác nhận:
"📋 Xác nhận đơn hàng:
👤 Tên: [tên]
📱 SĐT: [phone]
📍 Địa chỉ: [address]
🛍️ Sản phẩm: [danh sách]
💰 Tổng: [total] VNĐ
Anh/chị xác nhận đặt hàng không ạ?"
5. Khách xác nhận → GỌI TOOL create_customer_order
6. Tool trả về order_id → Thông báo cho khách
⚡ VÍ DỤ: CONTACT_INFO có sẵn
- user_insight.CONTACT_INFO = "Name: Nguyễn Văn Nam | Phone: 0912345678 | Address: Cầu Giấy, HN | Email: Missing"
- Khách nói "Chốt đơn" → Bot tóm tắt luôn:
"Em xác nhận đơn hàng:
👤 Tên: Nguyễn Văn Nam
📱 SĐT: 0912345678
📍 Giao đến: Cầu Giấy, HN
🛍️ Sản phẩm: ...
💰 Tổng: ...VNĐ
Anh xác nhận nhé?"
⚡ VÍ DỤ: CONTACT_INFO thiếu 1 phần
- user_insight.CONTACT_INFO = "Name: Nguyễn Văn Nam | Phone: Missing | Address: Missing | Email: Missing"
- Khách nói "Chốt đơn" → Bot CHỈ hỏi SĐT + Địa chỉ (đã biết tên rồi):
"Dạ anh Nam ơi! Để em tạo đơn, anh cho em SĐT và địa chỉ giao hàng nhé!"
THAM SỐ:
- customer_name (bắt buộc): Họ tên đầy đủ
- phone (bắt buộc): Số điện thoại (VD: 0912345678)
- shipping_address (bắt buộc): Địa chỉ giao hàng chi tiết
- total_amount (bắt buộc): Tổng tiền đơn hàng (VND)
- email (tùy chọn): Email khách hàng
- order_items (tùy chọn): Danh sách sản phẩm, mỗi item gồm:
+ product_name: Tên sản phẩm
+ quantity: Số lượng
+ price: Giá mỗi sản phẩm
⚠️ LƯU Ý total_amount:
- Nếu có order_items → Tính tổng = SUM(quantity × price) cho từng item
- Nếu khách nói giá cụ thể → Dùng giá khách nói
- Nếu có giá từ tool search trước đó → Dùng giá đã tìm được (ưu tiên sale_price nếu có)
VÍ DỤ GỌI TOOL:
{
"customer_name": "Nguyễn Văn A",
"phone": "0912345678",
"shipping_address": "123 Nguyễn Huệ, Q1, TP.HCM",
"total_amount": 598000,
"email": "a@gmail.com",
"order_items": [
{"product_name": "Áo phông nam Basic", "quantity": 2, "price": 299000}
]
}
═══════════════════════════════════════════════════════════════
🚫 TOOL: cancel_order — HỦY ĐƠN HÀNG
═══════════════════════════════════════════════════════════════
FLOW CHUẨN:
1. Khách nói "hủy đơn" / "cancel" → Hỏi mã đơn hàng (order_id)
2. Hỏi số điện thoại để xác minh
3. GỌI TOOL cancel_order
4. Thông báo kết quả cho khách
THAM SỐ:
- order_id (bắt buộc): Mã đơn hàng (VD: ORD-ABC12345)
- phone (bắt buộc): SĐT xác minh (phải khớp với SĐT khi đặt)
QUY TẮC HỦY:
- Chỉ hủy được đơn có trạng thái 'pending'
- Đơn đang 'processing', 'shipped', 'completed' → KHÔNG hủy được
- Đơn đã 'cancelled' → Thông báo đã hủy trước đó
- SĐT không khớp → Từ chối hủy
═══════════════════════════════════════════════════════════════
💡 USER_INSIGHT.CONTACT_INFO — Tận dụng thông tin có sẵn
═══════════════════════════════════════════════════════════════
LUÔN kiểm tra user_insight.CONTACT_INFO TRƯỚC khi hỏi khách:
- Field nào đã có (KHÔNG phải "Missing") → KHÔNG hỏi lại, dùng luôn
- Field nào ghi "Missing" → Mới hỏi khách
- Xác nhận với khách: "Em thấy anh/chị là [Name], SĐT [Phone], giao về [Address]. Đúng chưa ạ?"
- Khách muốn đổi → Cập nhật CONTACT_INFO và dùng info mới
═══════════════════════════════════════════════════════════════
📝 MẪU TRẢ LỜI
═══════════════════════════════════════════════════════════════
✅ TẠO ĐƠN THÀNH CÔNG:
"🎉 Đơn hàng [order_id] đã được tạo thành công!
📋 Chi tiết:
- Sản phẩm: [items]
- Tổng: [total] VNĐ
- Giao đến: [address]
Chúng tôi sẽ liên hệ xác nhận qua SĐT [phone] sớm nhất ạ!"
❌ TẠO ĐƠN THẤT BẠI:
"Rất tiếc, có lỗi khi tạo đơn hàng. Anh/chị vui lòng thử lại hoặc liên hệ hotline 1800 6996 để được hỗ trợ ạ."
✅ HỦY ĐƠN THÀNH CÔNG:
"Đơn hàng [order_id] đã được hủy thành công. Nếu cần hỗ trợ thêm, anh/chị cứ nhắn em nhé!"
❌ KHÔNG HỦY ĐƯỢC:
"Rất tiếc, đơn hàng [order_id] [lý do] nên không thể hủy. Anh/chị liên hệ hotline 1800 6996 để được hỗ trợ ạ."
......@@ -4,7 +4,8 @@ import logging
from langchain_core.tools import tool
from pydantic import BaseModel, Field
from agent.prompt_utils import read_tool_prompt
from common.embedding_service import create_embedding_async
from common.starrocks_connection import get_db_connection
......@@ -64,5 +65,3 @@ async def canifa_knowledge_search(query: str) -> str:
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?"
canifa_knowledge_search.__doc__ = read_tool_prompt("brand_knowledge_tool") or canifa_knowledge_search.__doc__
......@@ -9,7 +9,8 @@ import httpx
from langchain_core.tools import tool
from pydantic import BaseModel, Field
from agent.prompt_utils import read_tool_prompt
logger = logging.getLogger(__name__)
......@@ -83,9 +84,3 @@ async def check_is_stock(skus: str) -> str:
return json.dumps(stock_data, ensure_ascii=False)
# Load dynamic docstring from file
dynamic_prompt = read_tool_prompt("check_is_stock")
if dynamic_prompt:
check_is_stock.__doc__ = dynamic_prompt
check_is_stock.description = dynamic_prompt
......@@ -21,7 +21,8 @@ from common.starrocks_connection import get_db_connection
# Setup Logger
logger = logging.getLogger(__name__)
from agent.prompt_utils import read_tool_prompt
class SearchItem(BaseModel):
......@@ -246,9 +247,3 @@ async def data_retrieval_tool(searches: list[SearchItem]) -> str:
return json.dumps(output, ensure_ascii=False, default=str)
# Load dynamic docstring
# Load dynamic docstring
dynamic_prompt = read_tool_prompt("data_retrieval_tool")
if dynamic_prompt:
data_retrieval_tool.__doc__ = dynamic_prompt
data_retrieval_tool.description = dynamic_prompt
......@@ -5,7 +5,8 @@ from langchain_core.tools import tool
from pydantic import BaseModel, Field
from common.starrocks_connection import get_db_connection
from agent.prompt_utils import read_tool_prompt
logger = logging.getLogger(__name__)
......@@ -71,6 +72,3 @@ async def canifa_get_promotions(check_date: str = None) -> str:
except Exception as e:
logger.error(f"❌ Error in canifa_get_promotions: {e}")
return "Xin lỗi, tôi không thể lấy danh sách khuyến mãi lúc này."
# Load dynamic docstring
canifa_get_promotions.__doc__ = read_tool_prompt("promotion_canifa_tool") or canifa_get_promotions.__doc__
from fastapi import APIRouter, HTTPException, Request
from pydantic import BaseModel
import os
import re
from agent.graph import reset_graph
from common.cache import bump_prompt_version
router = APIRouter()
PROMPT_FILE_PATH = os.path.join(os.path.dirname(__file__), "../agent/system_prompt.txt")
# Allowed variables in prompt (single braces OK for these)
ALLOWED_VARIABLES = {"date_str"}
from fastapi import APIRouter, HTTPException, Request
from pydantic import BaseModel
import re
from agent.graph import reset_graph
from agent.prompt import create_system_prompt_version, get_system_prompt_template, write_system_prompt_file
from common.cache import bump_prompt_version
router = APIRouter()
# Allowed variables in prompt (single braces OK for these)
ALLOWED_VARIABLES = {"date_str"}
class PromptUpdateRequest(BaseModel):
content: str
......@@ -33,27 +31,23 @@ def validate_prompt_braces(content: str) -> tuple[bool, list[str]]:
from common.rate_limit import rate_limit_service
@router.get("/api/agent/system-prompt")
async def get_system_prompt_content(request: Request):
"""Get current system prompt content"""
try:
if os.path.exists(PROMPT_FILE_PATH):
with open(PROMPT_FILE_PATH, "r", encoding="utf-8") as f:
content = f.read()
return {"status": "success", "content": content}
else:
return {"status": "error", "message": "Prompt file not found"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/api/agent/system-prompt")
async def get_system_prompt_content(request: Request):
"""Get current system prompt content (prefer Langfuse, fallback local)."""
try:
content = get_system_prompt_template()
return {"status": "success", "content": content}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/api/agent/system-prompt")
@rate_limit_service.limiter.limit("10/minute")
async def update_system_prompt_content(request: Request, body: PromptUpdateRequest):
"""Update system prompt content"""
try:
# Validate braces
is_valid, problematic = validate_prompt_braces(body.content)
async def update_system_prompt_content(request: Request, body: PromptUpdateRequest):
"""Update system prompt content (Langfuse + local fallback snapshot)."""
try:
# Validate braces
is_valid, problematic = validate_prompt_braces(body.content)
if not is_valid:
# Return warning but still allow save
......@@ -62,29 +56,42 @@ async def update_system_prompt_content(request: Request, body: PromptUpdateReque
f"Nếu đây là JSON, hãy dùng {{{{ }}}} thay vì {{ }}. "
f"Prompt vẫn được lưu nhưng có thể gây lỗi khi chat."
)
else:
warning = None
# 1. Update file
with open(PROMPT_FILE_PATH, "w", encoding="utf-8") as f:
f.write(body.content)
# 2. Bump prompt version in Redis (ALL workers will detect this)
new_version = await bump_prompt_version()
# 3. Reset local worker's Graph Singleton (immediate effect for this worker)
reset_graph()
response = {
"status": "success",
"message": f"System prompt updated. Version: {new_version}. All workers will reload on next request.",
"prompt_version": new_version
}
if warning:
response["warning"] = warning
return response
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
else:
warning = None
# 1. Update Langfuse prompt version (if available)
langfuse_version = create_system_prompt_version(body.content)
# 2. Always persist local snapshot as fallback
local_saved = write_system_prompt_file(body.content)
if langfuse_version is None and not local_saved:
raise HTTPException(status_code=500, detail="Failed to update prompt in both Langfuse and local fallback.")
# 3. Bump prompt version in Redis (compatibility signal across workers)
new_version = await bump_prompt_version()
# 4. Reset local worker's Graph Singleton (immediate effect for this worker)
reset_graph()
response = {
"status": "success",
"message": "System prompt updated. All workers will reload on next request.",
"prompt_version": new_version
}
if langfuse_version is not None:
response["langfuse_version"] = langfuse_version
else:
response["note"] = "Langfuse unavailable, fallback to local prompt file."
if warning:
response["warning"] = warning
return response
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
......@@ -2,7 +2,7 @@ from fastapi import APIRouter, HTTPException, Request
from pydantic import BaseModel
from agent.graph import reset_graph
from agent.prompt_utils import list_tool_prompts, read_tool_prompt, write_tool_prompt
from agent.prompt_utils import list_tool_prompts, read_tool_prompt, TOOL_PROMPT_LANGFUSE_MAP
router = APIRouter()
......@@ -13,23 +13,21 @@ class ToolPromptUpdateRequest(BaseModel):
@router.get("/api/agent/tool-prompts")
async def get_tool_prompts_list(request: Request):
"""List all available tool prompt files."""
"""List all available tool prompts (from Langfuse mapping)."""
try:
files = list_tool_prompts()
return {"status": "success", "files": files}
names = list_tool_prompts()
return {"status": "success", "files": names}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/api/agent/tool-prompts/{filename}")
async def get_tool_prompt_content(filename: str, request: Request):
"""Get content of a specific tool prompt file."""
"""Get content of a specific tool prompt from Langfuse."""
try:
content = read_tool_prompt(filename)
if not content:
# Try appending .txt if not present
if not filename.endswith(".txt"):
content = read_tool_prompt(filename + ".txt")
if not content and not filename.endswith(".txt"):
content = read_tool_prompt(filename + ".txt")
return {"status": "success", "content": content}
except Exception as e:
......@@ -38,20 +36,38 @@ async def get_tool_prompt_content(filename: str, request: Request):
@router.post("/api/agent/tool-prompts/{filename}")
async def update_tool_prompt_content(filename: str, request: Request, body: ToolPromptUpdateRequest):
"""Update content of a tool prompt file and reset graph."""
"""Create a new prompt version in Langfuse and reset graph."""
try:
# Ensure filename is safe (basic check)
if ".." in filename or "/" in filename or "\\" in filename:
raise HTTPException(status_code=400, detail="Invalid filename")
success = write_tool_prompt(filename, body.content)
if not success:
raise HTTPException(status_code=500, detail="Failed to write file")
lookup_key = filename.removesuffix(".txt")
langfuse_name = TOOL_PROMPT_LANGFUSE_MAP.get(lookup_key)
if not langfuse_name:
raise HTTPException(status_code=404, detail=f"No Langfuse mapping for '{filename}'")
# Reset Graph to reload tools with new prompts
from common.langfuse_client import get_langfuse_client
client = get_langfuse_client()
if not client:
raise HTTPException(status_code=503, detail="Langfuse client not available")
prompt = client.create_prompt(
name=langfuse_name,
prompt=body.content,
labels=["production"],
type="text",
)
version = getattr(prompt, "version", "?")
# Reset Graph to pick up new prompts on next request
reset_graph()
return {"status": "success", "message": f"Tool prompt {filename} updated successfully. Graph reloaded."}
return {
"status": "success",
"message": f"Tool prompt '{langfuse_name}' updated to v{version}. Graph reloaded.",
"version": version,
}
except HTTPException:
raise
except Exception as e:
......
cdcdccdc @ f5deb28b
Subproject commit f5deb28b0f9f83c6c3ea53cdd9e68f5c1a5fcbe7
chatbot-rsa @ d6b45f42
Subproject commit d6b45f42c45f8f1c5957894201bff23f140da1a2
"""Push all prompts to Langfuse project chatbot-canifa-order."""
import requests
import base64
import os
# chatbot-canifa-order API keys
PUBLIC_KEY = "pk-lf-3976e348-df92-4afb-a505-06fa1f0865c2"
SECRET_KEY = "sk-lf-031dea6b-f764-4c5d-a2e8-e091459c98af"
BASE_URL = "http://172.16.2.207:3009"
auth = base64.b64encode(f"{PUBLIC_KEY}:{SECRET_KEY}".encode()).decode()
headers = {
"Authorization": f"Basic {auth}",
"Content-Type": "application/json"
}
PROMPTS_DIR = os.path.join(os.path.dirname(__file__), "..", "backend", "agent")
TOOL_DIR = os.path.join(PROMPTS_DIR, "tool_prompts")
def read_file(path):
with open(path, "r", encoding="utf-8") as f:
return f.read()
def push_prompt(name, content, tags, commit_msg):
payload = {
"name": name,
"type": "text",
"prompt": content,
"labels": ["production"],
"tags": tags,
"commitMessage": commit_msg,
}
r = requests.post(f"{BASE_URL}/api/public/v2/prompts", headers=headers, json=payload, timeout=30)
if r.status_code == 201:
data = r.json()
print(f"✅ {name} → v{data.get('version', '?')} (labels: {data.get('labels', [])})")
else:
print(f"❌ {name} → {r.status_code}: {r.text[:200]}")
if __name__ == "__main__":
prompts = [
{
"name": "canifa-stylist-system-prompt",
"file": os.path.join(PROMPTS_DIR, "system_prompt.txt"),
"tags": ["canifa", "system-prompt"],
"commit": "Sync system_prompt.txt - 2026-02-24",
},
{
"name": "canifa-tool-data-retrieval",
"file": os.path.join(TOOL_DIR, "data_retrieval_tool.txt"),
"tags": ["canifa", "tool-prompt"],
"commit": "Sync data_retrieval_tool.txt - 2026-02-24",
},
{
"name": "canifa-tool-brand-knowledge",
"file": os.path.join(TOOL_DIR, "brand_knowledge_tool.txt"),
"tags": ["canifa", "tool-prompt"],
"commit": "Sync brand_knowledge_tool.txt - 2026-02-24",
},
{
"name": "canifa-tool-check-stock",
"file": os.path.join(TOOL_DIR, "check_is_stock.txt"),
"tags": ["canifa", "tool-prompt"],
"commit": "Sync check_is_stock.txt - 2026-02-24",
},
{
"name": "canifa-tool-promotion",
"file": os.path.join(TOOL_DIR, "promotion_canifa_tool.txt"),
"tags": ["canifa", "tool-prompt"],
"commit": "Sync promotion_canifa_tool.txt - 2026-02-24",
},
{
"name": "canifa-tool-take-order",
"file": os.path.join(TOOL_DIR, "take_order.prompt.txt"),
"tags": ["canifa", "tool-prompt"],
"commit": "Sync take_order.prompt.txt - 2026-02-24",
},
]
print(f"🚀 Pushing {len(prompts)} prompts to Langfuse (chatbot-canifa-order)...\n")
for p in prompts:
content = read_file(p["file"])
push_prompt(p["name"], content, p["tags"], p["commit"])
print(f"\n🎉 Done!")
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