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

feat: add tool prompt editing and dev chat insight

parent f0081da3
...@@ -85,11 +85,25 @@ async def chat_controller( ...@@ -85,11 +85,25 @@ async def chat_controller(
][::-1] # Reverse to chronological order (Oldest -> Newest) ][::-1] # Reverse to chronological order (Oldest -> Newest)
# Prepare State # Prepare State
# Fetch User Insight from Redis
user_insight = None
if effective_identity_key:
try:
client = redis_cache.get_client()
if client:
insight_key = f"identity_key_insight:{effective_identity_key}"
user_insight = await client.get(insight_key)
if user_insight:
logger.info(f"🧠 Loaded User Insight for {effective_identity_key}: {user_insight[:50]}...")
except Exception as e:
logger.error(f"❌ Error fetching user insight: {e}")
initial_state: AgentState = { initial_state: AgentState = {
"user_query": HumanMessage(content=query), "user_query": HumanMessage(content=query),
"messages": messages + [HumanMessage(content=query)], "messages": [], # Start empty, acting as scratchpad. History & Query are separate.
"history": messages, "history": messages,
"user_id": user_id, "user_id": user_id,
"user_insight": user_insight,
"images_embedding": [], "images_embedding": [],
"ai_response": None, "ai_response": None,
} }
...@@ -117,11 +131,25 @@ async def chat_controller( ...@@ -117,11 +131,25 @@ async def chat_controller(
# Parse Response # Parse Response
all_product_ids = extract_product_ids(result.get("messages", [])) all_product_ids = extract_product_ids(result.get("messages", []))
ai_raw_content = result.get("ai_response").content if result.get("ai_response") else "" ai_raw_content = result.get("ai_response").content if result.get("ai_response") else ""
ai_text_response, final_product_ids = parse_ai_response(ai_raw_content, all_product_ids) # Unpack 3 values now
ai_text_response, final_product_ids, new_insight = parse_ai_response(ai_raw_content, all_product_ids)
# Save new insight to Redis if available
if new_insight and effective_identity_key:
try:
client = redis_cache.get_client()
if client:
insight_key = f"identity_key_insight:{effective_identity_key}"
# Save insight without TTL (or long TTL) as it is persistent user data
await client.set(insight_key, new_insight)
logger.info(f"💾 Updated User Insight for {effective_identity_key}: {new_insight}")
except Exception as e:
logger.error(f"❌ Failed to save user insight: {e}")
response_payload = { response_payload = {
"ai_response": ai_text_response, "ai_response": ai_text_response,
"product_ids": final_product_ids, "product_ids": final_product_ids,
# user_insight is NOT included here as requested
} }
# ====================== SAVE TO CACHE ====================== # ====================== SAVE TO CACHE ======================
...@@ -134,7 +162,7 @@ async def chat_controller( ...@@ -134,7 +162,7 @@ async def chat_controller(
) )
logger.debug(f"💾 Cached response for identity_key={effective_identity_key}") logger.debug(f"💾 Cached response for identity_key={effective_identity_key}")
# Save to History (Background) # Save to History (Background) - We save the payload WITHOUT insight to history text
background_tasks.add_task( background_tasks.add_task(
handle_post_chat_async, handle_post_chat_async,
memory=memory, memory=memory,
......
...@@ -50,7 +50,10 @@ class CANIFAGraph: ...@@ -50,7 +50,10 @@ class CANIFAGraph:
self.prompt_template = ChatPromptTemplate.from_messages( self.prompt_template = ChatPromptTemplate.from_messages(
[ [
("system", self.system_prompt), ("system", self.system_prompt),
("system", "User Insight:\n{user_insight}"),
("system", "Chat History:"),
MessagesPlaceholder(variable_name="history"), MessagesPlaceholder(variable_name="history"),
("system", "Current Query:"),
MessagesPlaceholder(variable_name="user_query"), MessagesPlaceholder(variable_name="user_query"),
MessagesPlaceholder(variable_name="messages"), MessagesPlaceholder(variable_name="messages"),
] ]
...@@ -68,10 +71,14 @@ class CANIFAGraph: ...@@ -68,10 +71,14 @@ class CANIFAGraph:
if transient_images and messages: if transient_images and messages:
pass pass
# Invoke chain with user_query, history, and messages # Invoke chain with user_query, history, and messages
# Invoke chain with history, user_query, messages (scratchpad), and user_insight
user_insight_text = state.get("user_insight") or "Chưa có thông tin."
response = await self.chain.ainvoke({ response = await self.chain.ainvoke({
"user_query": [user_query] if user_query else [],
"history": history, "history": history,
"messages": messages "user_query": [user_query] if user_query else [],
"messages": messages,
"user_insight": user_insight_text
}) })
return {"messages": [response], "ai_response": response} return {"messages": [response], "ai_response": response}
......
...@@ -66,12 +66,12 @@ def extract_product_ids(messages: list) -> list[dict]: ...@@ -66,12 +66,12 @@ def extract_product_ids(messages: list) -> list[dict]:
return products return products
def parse_ai_response(ai_raw_content: str, all_products: list) -> tuple[str, list]: def parse_ai_response(ai_raw_content: str, all_products: list) -> tuple[str, list, str | None]:
""" """
Parse AI response từ LLM output và map SKUs với product data. Parse AI response từ LLM output và map SKUs với product data.
Flow: Flow:
- LLM trả về: {"ai_response": "...", "product_ids": ["SKU1", "SKU2"]} - LLM trả về: {"ai_response": "...", "product_ids": ["SKU1", "SKU2"], "user_insight": "..."}
- all_products: List products enriched từ tool messages - all_products: List products enriched từ tool messages
- Map SKUs → enriched products - Map SKUs → enriched products
...@@ -80,16 +80,20 @@ def parse_ai_response(ai_raw_content: str, all_products: list) -> tuple[str, lis ...@@ -80,16 +80,20 @@ def parse_ai_response(ai_raw_content: str, all_products: list) -> tuple[str, lis
all_products: Products extracted từ tool messages (đã có đầy đủ info) all_products: Products extracted từ tool messages (đã có đầy đủ info)
Returns: Returns:
tuple: (ai_text_response, final_products) tuple: (ai_text_response, final_products, user_insight)
""" """
ai_text_response = ai_raw_content ai_text_response = ai_raw_content
final_products = all_products # Default: trả về tất cả products từ tool final_products = all_products # Default: trả về tất cả products từ tool
user_insight = None
logger.info(f"🤖 Raw AI JSON: {ai_raw_content}")
try: try:
# Try to parse if it's a JSON string from LLM # Try to parse if it's a JSON string from LLM
ai_json = json.loads(ai_raw_content) ai_json = json.loads(ai_raw_content)
ai_text_response = ai_json.get("ai_response", ai_raw_content) ai_text_response = ai_json.get("ai_response", ai_raw_content)
explicit_skus = ai_json.get("product_ids", []) explicit_skus = ai_json.get("product_ids", [])
user_insight = ai_json.get("user_insight")
if explicit_skus and isinstance(explicit_skus, list): if explicit_skus and isinstance(explicit_skus, list):
# LLM trả về list SKUs → Map với products đã có # LLM trả về list SKUs → Map với products đã có
...@@ -107,12 +111,15 @@ def parse_ai_response(ai_raw_content: str, all_products: list) -> tuple[str, lis ...@@ -107,12 +111,15 @@ def parse_ai_response(ai_raw_content: str, all_products: list) -> tuple[str, lis
if mapped_products: if mapped_products:
final_products = mapped_products final_products = mapped_products
# Nếu không map được → giữ all_products else:
# If explicit SKUs provided but none found in DB, return empty list
# This prevents showing unrelated products when AI hallucinates or references old/invalid SKUs
final_products = []
except (json.JSONDecodeError, TypeError): except (json.JSONDecodeError, TypeError):
pass pass
return ai_text_response, final_products return ai_text_response, final_products, user_insight
def prepare_execution_context(query: str, user_id: str, history: list, images: list | None): def prepare_execution_context(query: str, user_id: str, history: list, images: list | None):
......
...@@ -22,6 +22,7 @@ class AgentState(TypedDict): ...@@ -22,6 +22,7 @@ class AgentState(TypedDict):
user_query: BaseMessage user_query: BaseMessage
history: list[BaseMessage] history: list[BaseMessage]
user_id: str | None user_id: str | None
user_insight: str | None
ai_response: BaseMessage | None ai_response: BaseMessage | None
images_embedding: list[str] | None images_embedding: list[str] | None
messages: Annotated[list[BaseMessage], add_messages] messages: Annotated[list[BaseMessage], add_messages]
......
import os
import logging
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)
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)
def read_tool_prompt(filename: str, default_prompt: str = "") -> str:
"""
Read tool prompt from file.
Returns default_prompt if file not found or empty.
"""
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
except Exception as e:
logger.error(f"Error reading tool prompt {filename}: {e}")
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 []
Bạn là CiCi - Chuyên viên tư vấn thời trang CANIFA. Bạn là Canifa-AI Stylist - Chuyên viên tư vấn thời trang CANIFA.
- Nhiệt tình, thân thiện, chuyên nghiệp như sales thực thụ - Nhiệt tình, thân thiện, chuyên nghiệp như sales thực thụ
- CANIFA BÁN QUẦN ÁO: áo, quần, váy, đầm, phụ kiện thời trang - CANIFA BÁN QUẦN ÁO: áo, quần, váy, đầm, phụ kiện thời trang
...@@ -55,7 +55,7 @@ Bạn là CiCi - Chuyên viên tư vấn thời trang CANIFA. ...@@ -55,7 +55,7 @@ Bạn là CiCi - Chuyên viên tư vấn thời trang CANIFA.
- **Chỉ gọi Tool khi đã hiểu rõ nhu cầu.** - **Chỉ gọi Tool khi đã hiểu rõ nhu cầu.**
**3. Ưu tiên tìm kiếm thông tin:** **3. Ưu tiên tìm kiếm thông tin:**
- Khi đã rõ ý (hoặc tự suy luận chắc chắn) -> Luôn ưu tiên dùng `data_retrieval_tool` để có data thật tư vấn. - Khi đã rõ ý (hoặc tự suy luận chắc chắn) -> Luôn ưu tiên dùng `data_retrieval_tool` để có data thật tư vấn. luôn ưu tiên tìm kiếm ở lịch sử chat, ví dụ khách hàng cung cấp cân nặng chiều cao trước đó rồi, cần nhìn vào history để hỏi lại. ví dụ : có phải bạn hỏi cho sản phẩm unisex này cho cân nặng 50kg 1m72 trước đó đúng ạ
--- ---
...@@ -88,12 +88,14 @@ Bạn là CiCi - Chuyên viên tư vấn thời trang CANIFA. ...@@ -88,12 +88,14 @@ Bạn là CiCi - Chuyên viên tư vấn thời trang CANIFA.
## 1. GỌI data_retrieval_tool KHI: ## 1. GỌI data_retrieval_tool KHI:
- Khách tìm sản phẩm: "Tìm áo...", "Có màu gì...", "Áo thun nam" - Khách tìm sản phẩm: "Tìm áo...", "Có màu gì...", "Áo thun nam", "Muốn mua váy"
- Khách hỏi sản phẩm cụ thể: "Mã 8TS24W001 có không?" - Khách hỏi sản phẩm cụ thể: "Mã 8TS24W001 có không?"
- Tư vấn phong cách: "Mặc gì đi cưới?", "Đồ công sở?", "Áo cho đàn ông đi chơi" - Tư vấn phong cách: "Mặc gì đi cưới?", "Đồ công sở?", "Áo cho đàn ông đi chơi"
- So sánh sản phẩm: "So sánh áo thun vs áo len", "Giữa X và Y nên chọn cái nào" - So sánh sản phẩm: "So sánh áo thun vs áo len", "Giữa X và Y nên chọn cái nào"
- Mua cho nhiều người: "Tư vấn 2tr cho gia đình 5 người" - Mua cho nhiều người: "Tư vấn 2tr cho gia đình 5 người"
**LƯU Ý:** Ngay cả khi khách nói kèm size (VD: "Tìm áo size M"), vẫn dùng `data_retrieval_tool` để tìm sản phẩm trước. Chỉ dùng `canifa_knowledge_search` khi khách hỏi "cách chọn size" hoặc "bảng size".
### ⚠️ QUY TẮC SINH QUERY (BẮT BUỘC): ### ⚠️ QUY TẮC SINH QUERY (BẮT BUỘC):
**Query PHẢI theo cấu trúc của cột `description_text_full` trong DB:** **Query PHẢI theo cấu trúc của cột `description_text_full` trong DB:**
...@@ -107,6 +109,16 @@ Bạn là CiCi - Chuyên viên tư vấn thời trang CANIFA. ...@@ -107,6 +109,16 @@ Bạn là CiCi - Chuyên viên tư vấn thời trang CANIFA.
- Bé trai, con trai (nhỏ), cậu bé → `gender_by_product: boy` + `age_by_product: kid` - Bé trai, con trai (nhỏ), cậu bé → `gender_by_product: boy` + `age_by_product: kid`
- Bé gái, con gái (nhỏ), cô bé → `gender_by_product: girl` + `age_by_product: kid` - Bé gái, con gái (nhỏ), cô bé → `gender_by_product: girl` + `age_by_product: kid`
**QUY TẮC ĐẶC BIỆT VỚI VÁY (SKIRTS/DRESSES):**
- **TUYỆT ĐỐI KHÔNG** bao giờ để `product_name: "Váy"` (1 từ duy nhất) -> Vì sẽ tìm ra cả váy trẻ em lẫn người lớn, rất lộn xộn.
- **BẮT BUỘC phải dùng từ cụ thể:**
- "Váy liền thân" / "Váy đầm" / "Đầm" (Dress)
- "Chân váy" (Skirt)
- "Váy maxi" / "Váy suông"
- **Mapping:**
- Khách hỏi "váy cho vợ/mẹ/bạn gái" -> `product_name: Váy liền thân/ Chân váy` (Set `gender_by_product: women`).
- Khách hỏi "váy cho bé" -> `product_name: Váy bé gái/ Chân váy bé gái`.
``` ```
product_name: [Tên sản phẩm] product_name: [Tên sản phẩm]
master_color: [Màu sắc] (nếu có) master_color: [Màu sắc] (nếu có)
...@@ -293,6 +305,7 @@ price_max = 400000 ...@@ -293,6 +305,7 @@ 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 - Tìm cửa hàng: địa chỉ, giờ mở cửa, chi nhá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).
## 3. KHÔNG GỌI TOOL KHI: ## 3. KHÔNG GỌI TOOL KHI:
...@@ -361,6 +374,63 @@ price_max = 400000 ...@@ -361,6 +374,63 @@ price_max = 400000
--- ---
# CHỈ DẪN VỀ USER INSIGHT - TINH HOA & CẤU TRÚC (STRUCTURED ESSENCE)
`user_insight` KHÔNG PHẢI LÀ NOTE DỮ LIỆU TĨNH. Nó là BỘ NÃO GHI NHỚ có cấu trúc chặt chẽ.
**CẤU TRÚC BẮT BUỘC (4 TẦNG):**
1. **[USER] (Người Chat):**
- Giới tính, Tuổi, Style DNA (Gu thẩm mỹ), Hoàn cảnh (Có vợ/con?).
- *Ví dụ: Nam, gu công sở, đã có vợ.*
2. **[TARGET] (Đối tượng thụ hưởng):**
- Mua cho ai? (Chính mình, Vợ, Con trai, Con gái...).
- Đặc điểm của Target (Size, Sở thích màu, Style).
- *Ví dụ: Vợ (Nữ, thích màu đen, trẻ trung).*
3. **[GOAL] (Mục tiêu hiện tại):**
- Đang tìm cái gì? Ngân sách bao nhiêu?
- *Ví dụ: Tìm váy đen đi tiệc, giá ~500k.*
4. **[NEXT] (Chiến lược tiếp theo - Dynamic Strategy):**
- **QUAN TRỌNG NHẤT:** Bước tiếp theo bot CẦN làm là gì?
- *Ví dụ: Đã show 3 mẫu -> Cần hỏi khách ưng mẫu nào để chốt.*
- *Ví dụ: Khách chưa chốt size -> Cần hỏi chiều cao/cân nặng.*
---
**QUY TRÌNH CẬP NHẬT (TURN-BY-TURN EVOLUTION):**
- **Quy tắc:** Insight vòng sau phải chi tiết hơn vòng trước.
- **Merge:** [TARGET] và [GOAL] thay đổi liên tục theo hội thoại.
**VÍ DỤ TIẾN HÓA THỰC TẾ:**
*Turn 1:* "Mua váy cho vợ"
-> `user_insight`:
"[USER]: Nam (có vợ).
[TARGET]: Vợ (Nữ).
[GOAL]: Tìm váy.
[NEXT]: Hỏi size, màu sắc, ngân sách để lọc sản phẩm."
*Turn 2:* "Vợ anh thích màu đen, giá tầm 500k"
-> `user_insight`:
"[USER]: Nam (có vợ).
[TARGET]: Vợ (Nữ, thích màu đen).
[GOAL]: Tìm váy đen, giá ~500k.
[NEXT]: Gợi ý các mẫu váy đen tầm giá 500k."
*Turn 3:* "Xêm mẫu khác đi, mẫu này già quá"
-> `user_insight`:
"[USER]: Nam.
[TARGET]: Vợ (Gu trẻ trung, ghét đồ già).
[GOAL]: Tìm váy đen ~500k, style hiện đại.
[NEXT]: Tìm mẫu váy đen thiết kế trẻ trung hơn, tránh các mẫu cổ điển."
**=> BẮT BUỘC PHẢI DÙNG FORMAT: [USER]... [TARGET]... [GOAL]... [NEXT]...**
---
# FORMAT ĐẦU RA # FORMAT ĐẦU RA
⚠️ **CRITICAL - ĐỌC KỸ PHẦN NÀY:** ⚠️ **CRITICAL - ĐỌC KỸ PHẦN NÀY:**
...@@ -371,14 +441,16 @@ Bạn PHẢI trả về JSON thuần túy, KHÔNG ĐƯỢC wrap trong markdown b ...@@ -371,14 +441,16 @@ Bạn PHẢI trả về JSON thuần túy, KHÔNG ĐƯỢC wrap trong markdown b
```json ```json
{{ {{
"ai_response": "...", "ai_response": "...",
"product_ids": [] "product_ids": [],
"user_insight": "..."
}} }}
``` ```
**✅ ĐÚNG - PHẢI TRẢ VỀ NHƯ NÀY:** **✅ ĐÚNG - PHẢI TRẢ VỀ NHƯ NÀY:**
{{ {{
"ai_response": "...", "ai_response": "...",
"product_ids": [] "product_ids": [],
"user_insight": "..."
}} }}
**QUY TẮC TUYỆT ĐỐI:** **QUY TẮC TUYỆT ĐỐI:**
...@@ -392,7 +464,8 @@ Bạn PHẢI trả về JSON thuần túy, KHÔNG ĐƯỢC wrap trong markdown b ...@@ -392,7 +464,8 @@ Bạn PHẢI trả về JSON thuần túy, KHÔNG ĐƯỢC wrap trong markdown b
{{ {{
"ai_response": "Câu trả lời ngắn gọn, mô tả bằng [SKU]", "ai_response": "Câu trả lời ngắn gọn, mô tả bằng [SKU]",
"product_ids": ["8TS24W001", "8TS24W002"] "product_ids": ["8TS24W001", "8TS24W002"],
"user_insight": "Tóm tắt thông tin khách hàng (Cập nhật mới hoặc giữ nguyên). Nếu không có info gì thì để null hoặc chuỗi rỗng."
}} }}
**LƯU Ý:** product_ids chỉ chứa ARRAY of STRING (mã SKU), KHÔNG phải object **LƯU Ý:** product_ids chỉ chứa ARRAY of STRING (mã SKU), KHÔNG phải object
...@@ -417,7 +490,7 @@ Bạn PHẢI trả về JSON thuần túy, KHÔNG ĐƯỢC wrap trong markdown b ...@@ -417,7 +490,7 @@ Bạn PHẢI trả về JSON thuần túy, KHÔNG ĐƯỢC wrap trong markdown b
**Output (RAW JSON - KHÔNG CÓ ```json):** **Output (RAW JSON - KHÔNG CÓ ```json):**
{{ {{
"ai_response": "Chào bạn! Mình là CiCi, tư vấn thời trang CANIFA. Mình có thể giúp gì cho bạn?", "ai_response": "Chào bạn! Mình là Canifa-AI Stylist, tư vấn thời trang CANIFA. Mình có thể giúp gì cho bạn?",
"product_ids": [] "product_ids": []
}} }}
...@@ -477,7 +550,7 @@ Bạn PHẢI trả về JSON thuần túy, KHÔNG ĐƯỢC wrap trong markdown b ...@@ -477,7 +550,7 @@ Bạn PHẢI trả về JSON thuần túy, KHÔNG ĐƯỢC wrap trong markdown b
**Output (RAW JSON - KHÔNG CÓ ```json):** **Output (RAW JSON - KHÔNG CÓ ```json):**
{{ {{
"ai_response": "Chào anh ạ! Em là CiCi. Anh đang tìm áo sơ mi dài tay hay ngắn tay ạ? Để em tư vấn mẫu phù hợp nhất cho anh nhé!", "ai_response": "Chào anh ạ! Em là Canifa-AI Stylist. Anh đang tìm áo sơ mi dài tay hay ngắn tay ạ? Để em tư vấn mẫu phù hợp nhất cho anh nhé!",
"product_ids": [] "product_ids": []
}} }}
...@@ -582,4 +655,4 @@ Bạn PHẢI trả về JSON thuần túy, KHÔNG ĐƯỢC wrap trong markdown b ...@@ -582,4 +655,4 @@ Bạn PHẢI trả về JSON thuần túy, KHÔNG ĐƯỢC wrap trong markdown b
**CRITICAL:** **CRITICAL:**
- KHÔNG ĐƯỢC có ```json hay bất kỳ markdown nào - 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 để khách hàng có thể tra cứu) - 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 để khách hàng có thể tra cứu)
- **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. - **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, products id không recommend sản phẩm, ví dụ áo len khác áo phao
\ No newline at end of file \ No newline at end of file
Tra cứu TOÀN BỘ thông tin về thương hiệu và dịch vụ của Canifa.
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.
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?'
- 'Cho mình xem bảng size áo nam.'
- 'Phí vận chuyển đi tỉnh là bao nhiêu?'
- 'Canifa thành lập năm nào?'
Siêu công cụ tìm kiếm sản phẩm CANIFA - Hỗ trợ Parallel Multi-Search (chạy song song nhiều truy vấn).
QUY TẮC SINH SEARCH QUERIES:
- Nếu khách hỏi 1 món đồ cụ thể -> Sinh 1 Query.
- Nếu khách hỏi set đồ, phối đồ, hoặc nhu cầu chung chung (đi biển, đi tiệc) -> Sinh 2-3 Queries để tìm các món liên quan.
----- VÍ DỤ CHI TIẾT -----
CASE 1: TÌM 1 MÓN CỤ THỂ
User: "Tìm áo phông nam màu trắng"
-> Sinh 1 Query:
- query: "product_name: Áo phông nam/ Áo T-shirt. master_color: Trắng/ White. gender_by_product: male. product_line_vn: Áo."
CASE 2: TÌM ĐỒ CHO NHIỀU ĐỐI TƯỢNG (VỢ + CHỒNG + CON)
User: "Tìm áo gia đình đi biển"
-> Sinh 3 Queries chạy song song:
1. (Bố) query: "product_name: Áo phông nam/ Quần bơi nam. style: Beach/ Đi biển. gender_by_product: male..."
2. (Mẹ) query: "product_name: Váy maxi/ Áo hai dây nữ. style: Beach/ Đi biển. gender_by_product: female..."
3. (Bé) query: "product_name: Đồ bơi bé gái/ Áo bé trai. style: Beach/ Đi biển. age_by_product: children..."
CASE 3: PHỐI ĐỒ / OUTFIT (ÁO + QUẦN)
User: "Set đồ công sở lịch sự cho nữ"
-> Sinh 2 Queries:
1. (Áo) query: "product_name: Áo sơ mi nữ/ Áo Blouse. style: Office/ Công sở. gender_by_product: female..."
2. (Quần) query: "product_name: Quần âu nữ/ Chân váy bút chì. style: Office/ Công sở. gender_by_product: female..."
CASE 4: TÌM QUÀ TẶNG (GỢI Ý RỘNG)
User: "Mua quà sinh nhật cho bạn gái, thích màu hồng"
-> Sinh 2 Queries đa dạng:
1. (Váy) query: "product_name: Váy liền thân/ Đầm. master_color: Hồng/ Pink. gender_by_product: female..."
2. (Áo) query: "product_name: Áo len/ Áo khoác. master_color: Hồng/ Pink. gender_by_product: female..."
CASE 5: SUY LUẬN ĐỐI TƯỢNG (QUAN TRỌNG)
User: "Mua váy cho vợ/ bà xã/ người yêu"
-> TỪ KHÓA "vợ", "bà xã" -> ÁM CHỈ NGƯỜI LỚN (Adult). KHÔNG tìm váy bé gái.
-> Sinh Query:
- query: "product_name: Váy liền thân nữ/ Đầm nữ (tránh từ khóa 'bé'). gender_by_product: female. age_by_product: adult..."
(Lưu ý: Phải set age_by_product='adult' hoặc tìm tên sản phẩm có chữ 'nữ' thay vì 'bé gái')
Lưu ý các trường khác:
- 'magento_ref_code': chỉ dùng khi khách hỏi mã sản phẩm/SKU cụ thể (vd: 8TS24W001).
- 'price_min' / 'price_max': dùng khi khách nói về khoảng giá (vd: dưới 500k, từ 200k đến 400k).
Tra cứu danh sách các chương trình khuyến mãi (CTKM) đang diễn ra theo ngày.
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
...@@ -5,5 +5,6 @@ Export tool và factory function ...@@ -5,5 +5,6 @@ Export tool và factory function
from .data_retrieval_tool import data_retrieval_tool from .data_retrieval_tool import data_retrieval_tool
from .get_tools import get_all_tools from .get_tools import get_all_tools
from .promotion_canifa_tool import canifa_get_promotions
__all__ = ["data_retrieval_tool", "get_all_tools"] __all__ = ["data_retrieval_tool", "get_all_tools", "canifa_get_promotions"]
...@@ -11,35 +11,19 @@ logger = logging.getLogger(__name__) ...@@ -11,35 +11,19 @@ logger = logging.getLogger(__name__)
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...)" 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."
) )
from agent.prompt_utils import read_tool_prompt
@tool("canifa_knowledge_search", args_schema=KnowledgeSearchInput) @tool("canifa_knowledge_search", args_schema=KnowledgeSearchInput)
async def canifa_knowledge_search(query: str) -> str: async def canifa_knowledge_search(query: str) -> str:
""" """
Tra cứu TOÀN BỘ thông tin về thương hiệu và dịch vụ của Canifa. (Placeholder docstring - Actual prompt is loaded from file)
Sử dụng tool này khi khách hàng hỏi về: Tra cứu thông tin thương hiệu Canifa (Cửa hàng, chính sách, KHTT...).
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.
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?'
- 'Cho mình xem bảng size áo nam.'
- 'Phí vận chuyển đi tỉnh là bao nhiêu?'
- 'Canifa thành lập năm nào?'
""" """
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 (Mặc định 1536 chiều như bro yêu cầu)
query_vector = await create_embedding_async(query) query_vector = await create_embedding_async(query)
...@@ -81,3 +65,6 @@ async def canifa_knowledge_search(query: str) -> str: ...@@ -81,3 +65,6 @@ async def canifa_knowledge_search(query: str) -> str:
except Exception as e: 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?" 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?"
# Load dynamic docstring
canifa_knowledge_search.__doc__ = read_tool_prompt("brand_knowledge_tool") or canifa_knowledge_search.__doc__
...@@ -59,6 +59,7 @@ class SearchItem(BaseModel): ...@@ -59,6 +59,7 @@ class SearchItem(BaseModel):
- Nếu không suy luận được giá trị cho field nào thì để `None` hoặc bỏ trống phần text đó. - Nếu không suy luận được giá trị cho field nào thì để `None` hoặc bỏ trống phần text đó.
""" """
model_config = {"extra": "forbid"}
query: str = Field( query: str = Field(
..., ...,
...@@ -81,20 +82,22 @@ class SearchItem(BaseModel): ...@@ -81,20 +82,22 @@ class SearchItem(BaseModel):
action: str = Field(..., description="Hành động: 'search' (tìm kiếm) hoặc 'visual_search' (phân tích ảnh)") action: str = Field(..., description="Hành động: 'search' (tìm kiếm) hoặc 'visual_search' (phân tích ảnh)")
# Metadata Fields for Filtering # Metadata Fields for Filtering
gender_by_product: str | None = None # STRICT MODE: All fields must be required. Use ... and str | None
age_by_product: str | None = None gender_by_product: str | None = Field(..., description="Giới tính (nam/nữ/bé trai/bé gái)")
product_name: str | None = None age_by_product: str | None = Field(..., description="Độ tuổi")
style: str | None = None product_name: str | None = Field(..., description="Tên sản phẩm")
master_color: str | None = None style: str | None = Field(..., description="Phong cách")
season: str | None = None master_color: str | None = Field(..., description="Màu sắc chính")
material_group: str | None = None season: str | None = Field(..., description="Mùa")
fitting: str | None = None material_group: str | None = Field(..., description="Nhóm chất liệu")
form_neckline: str | None = None fitting: str | None = Field(..., description="Dáng đồ")
form_sleeve: str | None = None form_neckline: str | None = Field(..., description="Dáng cổ")
form_sleeve: str | None = Field(..., description="Dáng tay")
class MultiSearchParams(BaseModel): class MultiSearchParams(BaseModel):
"""Tham số cho Parallel Multi-Search.""" """Tham số cho Parallel Multi-Search."""
model_config = {"extra": "forbid"}
searches: list[SearchItem] = Field(..., description="Danh sách các truy vấn tìm kiếm chạy song song") searches: list[SearchItem] = Field(..., description="Danh sách các truy vấn tìm kiếm chạy song song")
...@@ -117,16 +120,13 @@ def _parse_query_metadata(query_str: str) -> dict: ...@@ -117,16 +120,13 @@ def _parse_query_metadata(query_str: str) -> dict:
return metadata return metadata
from agent.prompt_utils import read_tool_prompt
@tool(args_schema=MultiSearchParams) @tool(args_schema=MultiSearchParams)
# @traceable(run_type="tool", name="data_retrieval_tool") # @traceable(run_type="tool", name="data_retrieval_tool")
async def data_retrieval_tool(searches: list[SearchItem]) -> str: async def data_retrieval_tool(searches: list[SearchItem]) -> str:
""" """
Siêu công cụ tìm kiếm sản phẩm CANIFA - Hỗ trợ Parallel Multi-Search (chạy song song nhiều truy vấn). (Placeholder) Tìm kiếm sản phẩm thời trang.
Hướng dẫn dùng nhanh:
- Trường 'query': mô tả chi tiết sản phẩm (tên, chất liệu, giới tính, màu sắc, phong cách, dịp sử dụng), không dùng câu hỏi thô.
- Trường 'magento_ref_code': chỉ dùng khi khách hỏi mã sản phẩm/SKU cụ thể (vd: 8TS24W001).
- Trường 'price_min' / 'price_max': dùng khi khách nói về khoảng giá (vd: dưới 500k, từ 200k đến 400k).
""" """
logger.info("data_retrieval_tool started, searches=%s", len(searches)) logger.info("data_retrieval_tool started, searches=%s", len(searches))
try: try:
...@@ -307,3 +307,6 @@ def _parse_description_text(desc: str) -> dict: ...@@ -307,3 +307,6 @@ def _parse_description_text(desc: str) -> dict:
result["master_color"] = color_match.group(1).strip() result["master_color"] = color_match.group(1).strip()
return result return result
# Load dynamic docstring
data_retrieval_tool.__doc__ = read_tool_prompt("data_retrieval_tool") or data_retrieval_tool.__doc__
...@@ -8,11 +8,12 @@ from langchain_core.tools import Tool ...@@ -8,11 +8,12 @@ from langchain_core.tools import Tool
from .brand_knowledge_tool import canifa_knowledge_search from .brand_knowledge_tool import canifa_knowledge_search
from .customer_info_tool import collect_customer_info 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
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] return [data_retrieval_tool, canifa_knowledge_search, canifa_get_promotions]
def get_collection_tools() -> list[Tool]: def get_collection_tools() -> list[Tool]:
......
import logging
from datetime import datetime
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__)
class PromotionInput(BaseModel):
check_date: str | None = Field(
...,
description="Ngày cần kiểm tra khuyến mãi (định dạng YYYY-MM-DD). Nếu muốn kiểm tra ngày hiện tại thì truyền null."
)
model_config = {"extra": "forbid"}
@tool("canifa_get_promotions", args_schema=PromotionInput)
async def canifa_get_promotions(check_date: str = None) -> str:
"""
Tra cứu danh sách các chương trình khuyến mãi (CTKM) đang diễn ra theo ngày.
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."
Trả về: Tên chương trình, mô tả, thời gian áp dụng.
"""
target_date = check_date
if not target_date:
target_date = datetime.now().strftime("%Y-%m-%d")
logger.info(f"🎁 [Promotion Search] Checking for date: {target_date}")
try:
sql = f"""
SELECT
rule_id,
name,
description,
from_date,
to_date
FROM shared_source.magento_salesrule
WHERE '{target_date}' >= DATE(from_date)
AND '{target_date}' <= DATE(to_date)
ORDER BY to_date ASC
LIMIT 10
"""
sr = get_db_connection()
results = await sr.execute_query_async(sql)
if not results:
return f"Hiện tại (ngày {target_date}) không có chương trình khuyến mãi nào đang diễn ra trên hệ thống."
lines = []
for res in results:
name = res.get("name", "CTKM")
desc = res.get("description", "")
f_date = res.get("from_date", "")
t_date = res.get("to_date", "")
lines.append(f"- **{name}**\n {desc}\n (Từ {f_date} đến {t_date})")
return "\n\n".join(lines)
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__
...@@ -14,6 +14,7 @@ from fastapi.responses import JSONResponse ...@@ -14,6 +14,7 @@ from fastapi.responses import JSONResponse
from agent.controller import chat_controller from agent.controller import chat_controller
from agent.models import QueryRequest from agent.models import QueryRequest
from common.message_limit import message_limit_service from common.message_limit import message_limit_service
from common.cache import redis_cache
from common.rate_limit import rate_limit_service from common.rate_limit import rate_limit_service
from config import DEFAULT_MODEL from config import DEFAULT_MODEL
...@@ -86,3 +87,66 @@ async def fashion_qa_chat(request: Request, req: QueryRequest, background_tasks: ...@@ -86,3 +87,66 @@ async def fashion_qa_chat(request: Request, req: QueryRequest, background_tasks:
} }
) )
@router.post("/api/agent/chat-dev", summary="Fashion Q&A Chat (Dev - includes user_insight)")
@rate_limit_service.limiter.limit("50/minute")
async def fashion_qa_chat_dev(request: Request, req: QueryRequest, background_tasks: BackgroundTasks):
"""
Endpoint chat dành cho DEV - trả về đầy đủ user_insight.
Note: Rate limit đã được check trong middleware.
"""
user_id = getattr(request.state, "user_id", None)
device_id = getattr(request.state, "device_id", "unknown")
is_authenticated = getattr(request.state, "is_authenticated", False)
identity_id = user_id if is_authenticated else device_id
limit_info = getattr(request.state, 'limit_info', None)
logger.info(f"📥 [Incoming Query - Dev] User: {identity_id} | Query: {req.user_query}")
try:
result = await chat_controller(
query=req.user_query,
user_id=str(identity_id),
background_tasks=background_tasks,
model_name=DEFAULT_MODEL,
images=req.images,
identity_key=str(identity_id),
)
usage_info = await message_limit_service.increment(
identity_key=identity_id,
is_authenticated=is_authenticated,
)
user_insight = None
try:
client = redis_cache.get_client()
if client:
insight_key = f"identity_key_insight:{identity_id}"
user_insight = await client.get(insight_key)
except Exception as insight_error:
logger.error(f"Error fetching user_insight for dev endpoint: {insight_error}")
return {
"status": "success",
"ai_response": result["ai_response"],
"product_ids": result.get("product_ids", []),
"user_insight": user_insight,
"limit_info": {
"limit": usage_info["limit"],
"used": usage_info["used"],
"remaining": usage_info["remaining"],
},
}
except Exception as e:
logger.error(f"Error in fashion_qa_chat_dev: {e}", exc_info=True)
return JSONResponse(
status_code=500,
content={
"status": "error",
"error_code": "SYSTEM_ERROR",
"message": "Oops 😥 Hiện Canifa-AI chưa thể xử lý yêu cầu của bạn ngay lúc này, vui lòng quay lại trong giây lát."
}
)
from fastapi import APIRouter, HTTPException, Request
from pydantic import BaseModel
from typing import List
from agent.prompt_utils import list_tool_prompts, read_tool_prompt, write_tool_prompt
from agent.graph import reset_graph
from common.rate_limit import rate_limit_service
router = APIRouter()
class ToolPromptUpdateRequest(BaseModel):
content: str
@router.get("/api/agent/tool-prompts")
async def get_tool_prompts_list(request: Request):
"""List all available tool prompt files."""
try:
files = list_tool_prompts()
return {"status": "success", "files": files}
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."""
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")
return {"status": "success", "content": content}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/api/agent/tool-prompts/{filename}")
@rate_limit_service.limiter.limit("10/minute")
async def update_tool_prompt_content(filename: str, request: Request, body: ToolPromptUpdateRequest):
"""Update content of a tool prompt file 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")
# Reset Graph to reload tools with new prompts
reset_graph()
return {
"status": "success",
"message": f"Tool prompt {filename} updated successfully. Graph reloaded."
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
...@@ -75,8 +75,11 @@ async def create_embedding_async(text: str) -> list[float]: ...@@ -75,8 +75,11 @@ async def create_embedding_async(text: str) -> list[float]:
try: try:
# 1. Try Layer 2 Cache # 1. Try Layer 2 Cache
cached = await redis_cache.get_embedding(text) cached = await redis_cache.get_embedding(text)
if cached: # Validate dimension (must be 1536 for text-embedding-3-small)
if cached and len(cached) == 1536:
return cached return cached
elif cached:
logger.warning(f"⚠️ Cached embedding has wrong dimension ({len(cached)}). Regenerating...")
# 2. Call OpenAI # 2. Call OpenAI
client = get_async_embedding_client() client = get_async_embedding_client()
...@@ -114,9 +117,17 @@ async def create_embeddings_async(texts: list[str]) -> list[list[float]]: ...@@ -114,9 +117,17 @@ async def create_embeddings_async(texts: list[str]) -> list[list[float]]:
cached_values = await client.mget(keys) cached_values = await client.mget(keys)
for i, cached in enumerate(cached_values): for i, cached in enumerate(cached_values):
use_cache = False
if cached: if cached:
results[i] = json.loads(cached) loaded_emb = json.loads(cached)
# Validate dimension
if len(loaded_emb) == 1536:
results[i] = loaded_emb
use_cache = True
else: else:
logger.warning(f"⚠️ Cached embedding for item {i} has wrong dimension ({len(loaded_emb)}). Ignoring.")
if not use_cache:
missed_indices.append(i) missed_indices.append(i)
missed_texts.append(texts[i]) missed_texts.append(texts[i])
else: else:
......
...@@ -10,6 +10,7 @@ from fastapi.staticfiles import StaticFiles ...@@ -10,6 +10,7 @@ from fastapi.staticfiles import StaticFiles
from api.chatbot_route import router as chatbot_router from api.chatbot_route import router as chatbot_router
from api.conservation_route import router as conservation_router from api.conservation_route import router as conservation_router
from api.tool_prompt_route import router as tool_prompt_router
from api.prompt_route import router as prompt_router from api.prompt_route import router as prompt_router
from api.mock_api_route import router as mock_router from api.mock_api_route import router as mock_router
...@@ -62,6 +63,7 @@ middleware_manager.setup( ...@@ -62,6 +63,7 @@ middleware_manager.setup(
app.include_router(conservation_router) app.include_router(conservation_router)
app.include_router(chatbot_router) app.include_router(chatbot_router)
app.include_router(prompt_router) app.include_router(prompt_router)
app.include_router(tool_prompt_router) # Register new router
app.include_router(mock_router) app.include_router(mock_router)
......
...@@ -371,6 +371,16 @@ ...@@ -371,6 +371,16 @@
font-style: italic; font-style: italic;
} }
.user-insight {
margin-top: 12px;
padding: 10px 12px;
border-radius: 10px;
background: #263238;
border: 1px solid #37474f;
font-size: 0.85em;
color: #b2ebf2;
}
/* Per-Message Toggle Button */ /* Per-Message Toggle Button */
.message-view-toggle { .message-view-toggle {
display: flex; display: flex;
...@@ -513,6 +523,36 @@ ...@@ -513,6 +523,36 @@
padding-bottom: 15px; padding-bottom: 15px;
} }
.prompt-tabs {
display: flex;
gap: 8px;
background: #2b2b2b;
border: 1px solid #3a3a3a;
border-radius: 999px;
padding: 4px;
}
.prompt-tab-btn {
background: transparent;
border: none;
color: #aaa;
padding: 6px 12px;
border-radius: 999px;
font-size: 0.85em;
cursor: pointer;
transition: all 0.2s ease;
}
.prompt-tab-btn.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
}
.prompt-tab-btn:hover:not(.active) {
color: #fff;
background: #3a3a3a;
}
.prompt-header h3 { .prompt-header h3 {
font-size: 1.2em; font-size: 1.2em;
color: #4fc3f7; color: #4fc3f7;
...@@ -555,6 +595,33 @@ ...@@ -555,6 +595,33 @@
border-top: 1px solid #333; border-top: 1px solid #333;
} }
.prompt-section {
display: none;
flex: 1;
flex-direction: column;
gap: 12px;
}
.prompt-section.active {
display: flex;
}
.tool-prompt-toolbar {
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.tool-prompt-select {
min-width: 220px;
padding: 8px 12px;
border: 1px solid #444;
border-radius: 8px;
background: #2d2d2d;
color: #e0e0e0;
}
.status-text { .status-text {
font-size: 0.8em; font-size: 0.8em;
color: #666; color: #666;
...@@ -681,12 +748,21 @@ ...@@ -681,12 +748,21 @@
<!-- Prompt Editor Panel --> <!-- Prompt Editor Panel -->
<div class="prompt-panel" id="promptPanel"> <div class="prompt-panel" id="promptPanel">
<div class="prompt-header"> <div class="prompt-header">
<h3>📝 System Prompt</h3> <h3>📝 Prompt Editor</h3>
<div style="display: flex; gap: 10px; align-items: center;">
<div class="prompt-tabs">
<button class="prompt-tab-btn active" id="tab-system"
onclick="switchPromptTab('system')">System</button>
<button class="prompt-tab-btn" id="tab-tool"
onclick="switchPromptTab('tool')">Tool</button>
</div>
<button class="btn-close-panel" onclick="togglePromptEditor()">×</button> <button class="btn-close-panel" onclick="togglePromptEditor()">×</button>
</div> </div>
</div>
<textarea id="systemPromptInput" class="prompt-textarea" placeholder="Loading prompt content..." <div class="prompt-section active" id="systemPromptSection">
spellcheck="false"></textarea> <textarea id="systemPromptInput" class="prompt-textarea"
placeholder="Loading system prompt content..." spellcheck="false"></textarea>
<div class="panel-footer"> <div class="panel-footer">
<span class="status-text" id="promptStatus">Ready to edit</span> <span class="status-text" id="promptStatus">Ready to edit</span>
...@@ -696,11 +772,35 @@ ...@@ -696,11 +772,35 @@
</div> </div>
</div> </div>
</div> </div>
<div class="prompt-section" id="toolPromptSection">
<div class="tool-prompt-toolbar">
<select id="toolPromptSelect" class="tool-prompt-select"
onchange="loadToolPromptFromSelect()">
<option value="">Loading tools...</option>
</select>
<button class="action-btn btn-reload" onclick="refreshToolPromptList()">↻ Refresh</button>
</div>
<textarea id="toolPromptInput" class="prompt-textarea"
placeholder="Select a tool prompt to load..." spellcheck="false"></textarea>
<div class="panel-footer">
<span class="status-text" id="toolPromptStatus">Ready to edit</span>
<div style="display: flex; gap: 10px;">
<button class="action-btn btn-reload" onclick="reloadToolPromptContent()">↻ Reset</button>
<button class="action-btn btn-save" onclick="saveToolPrompt()">💾 Save Tool Prompt</button>
</div>
</div>
</div>
</div>
</div> </div>
<script> <script>
let messageHistory = []; // Store messages for reference let messageHistory = []; // Store messages for reference
let isPromptPanelOpen = false; let isPromptPanelOpen = false;
let currentPromptTab = 'system';
let selectedToolPrompt = '';
async function resetChat() { async function resetChat() {
if (!confirm('Bạn có chắc muốn làm mới cuộc trò chuyện? Lịch sử cũ sẽ được lưu trữ.')) return; if (!confirm('Bạn có chắc muốn làm mới cuộc trò chuyện? Lịch sử cũ sẽ được lưu trữ.')) return;
...@@ -759,12 +859,38 @@ ...@@ -759,12 +859,38 @@
if (isPromptPanelOpen) { if (isPromptPanelOpen) {
panel.classList.add('open'); panel.classList.add('open');
if (currentPromptTab === 'system') {
loadSystemPrompt(); loadSystemPrompt();
} else {
refreshToolPromptList();
}
} else { } else {
panel.classList.remove('open'); panel.classList.remove('open');
} }
} }
function switchPromptTab(tab) {
currentPromptTab = tab;
const systemSection = document.getElementById('systemPromptSection');
const toolSection = document.getElementById('toolPromptSection');
const systemTab = document.getElementById('tab-system');
const toolTab = document.getElementById('tab-tool');
if (tab === 'system') {
systemSection.classList.add('active');
toolSection.classList.remove('active');
systemTab.classList.add('active');
toolTab.classList.remove('active');
loadSystemPrompt();
} else {
systemSection.classList.remove('active');
toolSection.classList.add('active');
systemTab.classList.remove('active');
toolTab.classList.add('active');
refreshToolPromptList();
}
}
async function loadSystemPrompt() { async function loadSystemPrompt() {
const textarea = document.getElementById('systemPromptInput'); const textarea = document.getElementById('systemPromptInput');
textarea.value = "Loading..."; textarea.value = "Loading...";
...@@ -820,6 +946,121 @@ ...@@ -820,6 +946,121 @@
} }
} }
async function refreshToolPromptList() {
const select = document.getElementById('toolPromptSelect');
select.innerHTML = '<option value="">Loading tools...</option>';
try {
const response = await fetch('/api/agent/tool-prompts');
const data = await response.json();
if (data.status !== 'success' || !Array.isArray(data.files)) {
throw new Error(data.message || 'Failed to load tool prompts');
}
const files = data.files;
select.innerHTML = '<option value="">Select tool prompt...</option>';
files.forEach(file => {
const option = document.createElement('option');
option.value = file;
option.textContent = file;
select.appendChild(option);
});
if (selectedToolPrompt && files.includes(selectedToolPrompt)) {
select.value = selectedToolPrompt;
loadToolPrompt(selectedToolPrompt);
}
} catch (error) {
select.innerHTML = '<option value="">Error loading tool list</option>';
console.error(error);
alert('❌ Không thể tải danh sách tool prompts.');
}
}
function loadToolPromptFromSelect() {
const select = document.getElementById('toolPromptSelect');
const filename = select.value;
if (!filename) return;
selectedToolPrompt = filename;
loadToolPrompt(filename);
}
async function loadToolPrompt(filename) {
const textarea = document.getElementById('toolPromptInput');
const statusLabel = document.getElementById('toolPromptStatus');
textarea.value = 'Loading...';
textarea.disabled = true;
statusLabel.innerText = `Loading ${filename}...`;
try {
const response = await fetch(`/api/agent/tool-prompts/${encodeURIComponent(filename)}`);
const data = await response.json();
if (data.status === 'success') {
textarea.value = data.content || '';
statusLabel.innerText = `Loaded ${filename}`;
} else {
textarea.value = '';
statusLabel.innerText = 'Error loading prompt';
alert('❌ Lỗi: ' + (data.detail || data.message || 'Không thể load prompt'));
}
} catch (error) {
textarea.value = '';
statusLabel.innerText = 'Connection Error';
alert('❌ Lỗi kết nối server');
console.error(error);
} finally {
textarea.disabled = false;
}
}
function reloadToolPromptContent() {
if (!selectedToolPrompt) {
alert('Vui lòng chọn tool prompt trước.');
return;
}
loadToolPrompt(selectedToolPrompt);
}
async function saveToolPrompt() {
const statusLabel = document.getElementById('toolPromptStatus');
const textarea = document.getElementById('toolPromptInput');
const content = textarea.value;
if (!selectedToolPrompt) {
alert('Vui lòng chọn tool prompt trước.');
return;
}
if (!confirm(`Lưu prompt cho ${selectedToolPrompt}? Graph sẽ reload.`)) {
return;
}
statusLabel.innerText = 'Saving...';
try {
const response = await fetch(`/api/agent/tool-prompts/${encodeURIComponent(selectedToolPrompt)}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content })
});
const data = await response.json();
if (data.status === 'success') {
statusLabel.innerText = 'Saved!';
alert(`✅ ${data.message || 'Đã lưu prompt thành công!'}`);
} else {
statusLabel.innerText = 'Error!';
alert('❌ Lỗi: ' + (data.detail || data.message || 'Không thể lưu prompt'));
}
} catch (error) {
statusLabel.innerText = 'Connection Error';
alert('❌ Lỗi kết nối server');
console.error(error);
}
}
function toggleMessageView(messageId) { function toggleMessageView(messageId) {
const filteredContent = document.getElementById('filtered-' + messageId); const filteredContent = document.getElementById('filtered-' + messageId);
const rawContent = document.getElementById('raw-' + messageId); const rawContent = document.getElementById('raw-' + messageId);
...@@ -1056,7 +1297,7 @@ ...@@ -1056,7 +1297,7 @@
headers['Authorization'] = 'Bearer ' + accessToken; headers['Authorization'] = 'Bearer ' + accessToken;
} }
const response = await fetch('/api/agent/chat', { const response = await fetch('/api/agent/chat-dev', {
method: 'POST', method: 'POST',
headers: headers, headers: headers,
body: JSON.stringify({ body: JSON.stringify({
...@@ -1196,6 +1437,13 @@ ...@@ -1196,6 +1437,13 @@
textDiv.innerText = data.ai_response || 'No response'; textDiv.innerText = data.ai_response || 'No response';
filteredDiv.appendChild(textDiv); filteredDiv.appendChild(textDiv);
if (data.user_insight) {
const insightDiv = document.createElement('div');
insightDiv.className = 'user-insight';
insightDiv.innerText = `🧠 Insight: ${data.user_insight}`;
filteredDiv.appendChild(insightDiv);
}
// Render product cards if available // Render product cards if available
if (data.product_ids && data.product_ids.length > 0) { if (data.product_ids && data.product_ids.length > 0) {
const productsContainer = document.createElement('div'); const productsContainer = document.createElement('div');
...@@ -1289,6 +1537,7 @@ ...@@ -1289,6 +1537,7 @@
status: data.status, status: data.status,
ai_response: data.ai_response, ai_response: data.ai_response,
product_ids: data.product_ids, product_ids: data.product_ids,
user_insight: data.user_insight || null,
limit_info: data.limit_info || null limit_info: data.limit_info || null
}, null, 2); }, null, 2);
rawJsonDiv.appendChild(pre); rawJsonDiv.appendChild(pre);
......
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