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
This diff is collapsed.
This diff is collapsed.
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