Spaces:
Running
Running
Upload 16 files
Browse files- agent_chat_stream.py +99 -0
- agent_service.py +258 -0
- main.py +75 -192
- prompts/feedback_agent.txt +51 -0
- prompts/sales_agent.txt +47 -0
- tools_service.py +0 -46
agent_chat_stream.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Agent Chat Streaming Endpoint
|
| 3 |
+
SSE-based real-time streaming for Sales & Feedback agents
|
| 4 |
+
"""
|
| 5 |
+
from typing import AsyncGenerator
|
| 6 |
+
from stream_utils import format_sse, EVENT_STATUS, EVENT_TOKEN, EVENT_DONE, EVENT_ERROR, EVENT_METADATA
|
| 7 |
+
from datetime import datetime
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
async def agent_chat_stream(
|
| 11 |
+
request,
|
| 12 |
+
agent_service,
|
| 13 |
+
conversation_service
|
| 14 |
+
) -> AsyncGenerator[str, None]:
|
| 15 |
+
"""
|
| 16 |
+
Stream agent responses in real-time (SSE format)
|
| 17 |
+
|
| 18 |
+
Args:
|
| 19 |
+
request: ChatRequest with message, session_id, mode, user_id
|
| 20 |
+
agent_service: AgentService instance
|
| 21 |
+
conversation_service: ConversationService instance
|
| 22 |
+
|
| 23 |
+
Yields SSE events:
|
| 24 |
+
- status: Processing updates
|
| 25 |
+
- token: Text chunks
|
| 26 |
+
- metadata: Session info
|
| 27 |
+
- done: Completion signal
|
| 28 |
+
- error: Error messages
|
| 29 |
+
"""
|
| 30 |
+
try:
|
| 31 |
+
# === SESSION MANAGEMENT ===
|
| 32 |
+
session_id = request.session_id
|
| 33 |
+
if not session_id:
|
| 34 |
+
session_id = conversation_service.create_session(
|
| 35 |
+
metadata={"user_agent": "api", "created_via": "agent_stream"},
|
| 36 |
+
user_id=request.user_id
|
| 37 |
+
)
|
| 38 |
+
yield format_sse(EVENT_METADATA, {"session_id": session_id})
|
| 39 |
+
|
| 40 |
+
# Get conversation history
|
| 41 |
+
history = conversation_service.get_history(session_id)
|
| 42 |
+
|
| 43 |
+
# Convert to messages format
|
| 44 |
+
messages = []
|
| 45 |
+
for h in history:
|
| 46 |
+
messages.append({"role": h["role"], "content": h["content"]})
|
| 47 |
+
|
| 48 |
+
# Determine mode
|
| 49 |
+
mode = getattr(request, 'mode', 'sales') # Default to sales
|
| 50 |
+
|
| 51 |
+
# === STATUS UPDATE ===
|
| 52 |
+
if mode == 'feedback':
|
| 53 |
+
yield format_sse(EVENT_STATUS, "Đang kiểm tra lịch sử sự kiện của bạn...")
|
| 54 |
+
else:
|
| 55 |
+
yield format_sse(EVENT_STATUS, "Đang tư vấn...")
|
| 56 |
+
|
| 57 |
+
# === CALL AGENT ===
|
| 58 |
+
result = await agent_service.chat(
|
| 59 |
+
user_message=request.message,
|
| 60 |
+
conversation_history=messages,
|
| 61 |
+
mode=mode,
|
| 62 |
+
user_id=request.user_id
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
agent_response = result["message"]
|
| 66 |
+
|
| 67 |
+
# === STREAM RESPONSE TOKEN BY TOKEN ===
|
| 68 |
+
# Simple character-by-character streaming
|
| 69 |
+
chunk_size = 5 # Characters per chunk
|
| 70 |
+
for i in range(0, len(agent_response), chunk_size):
|
| 71 |
+
chunk = agent_response[i:i+chunk_size]
|
| 72 |
+
yield format_sse(EVENT_TOKEN, chunk)
|
| 73 |
+
# Small delay for smoother streaming
|
| 74 |
+
import asyncio
|
| 75 |
+
await asyncio.sleep(0.02)
|
| 76 |
+
|
| 77 |
+
# === SAVE HISTORY ===
|
| 78 |
+
conversation_service.add_message(
|
| 79 |
+
session_id=session_id,
|
| 80 |
+
role="user",
|
| 81 |
+
content=request.message
|
| 82 |
+
)
|
| 83 |
+
conversation_service.add_message(
|
| 84 |
+
session_id=session_id,
|
| 85 |
+
role="assistant",
|
| 86 |
+
content=agent_response
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
# === DONE ===
|
| 90 |
+
yield format_sse(EVENT_DONE, {
|
| 91 |
+
"session_id": session_id,
|
| 92 |
+
"timestamp": datetime.utcnow().isoformat(),
|
| 93 |
+
"mode": mode,
|
| 94 |
+
"tool_calls": len(result.get("tool_calls", []))
|
| 95 |
+
})
|
| 96 |
+
|
| 97 |
+
except Exception as e:
|
| 98 |
+
print(f"⚠️ Agent Stream Error: {e}")
|
| 99 |
+
yield format_sse(EVENT_ERROR, str(e))
|
agent_service.py
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Agent Service - Central Brain for Sales & Feedback Agents
|
| 3 |
+
Manages LLM conversation loop with tool calling
|
| 4 |
+
"""
|
| 5 |
+
from typing import Dict, Any, List, Optional
|
| 6 |
+
import os
|
| 7 |
+
from tools_service import ToolsService
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class AgentService:
|
| 11 |
+
"""
|
| 12 |
+
Manages the conversation loop between User -> LLM -> Tools -> Response
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
def __init__(
|
| 16 |
+
self,
|
| 17 |
+
tools_service: ToolsService,
|
| 18 |
+
embedding_service,
|
| 19 |
+
qdrant_service,
|
| 20 |
+
advanced_rag,
|
| 21 |
+
hf_token: str
|
| 22 |
+
):
|
| 23 |
+
self.tools_service = tools_service
|
| 24 |
+
self.embedding_service = embedding_service
|
| 25 |
+
self.qdrant_service = qdrant_service
|
| 26 |
+
self.advanced_rag = advanced_rag
|
| 27 |
+
self.hf_token = hf_token
|
| 28 |
+
|
| 29 |
+
# Load system prompts
|
| 30 |
+
self.prompts = self._load_prompts()
|
| 31 |
+
|
| 32 |
+
def _load_prompts(self) -> Dict[str, str]:
|
| 33 |
+
"""Load system prompts from files"""
|
| 34 |
+
prompts = {}
|
| 35 |
+
prompts_dir = "prompts"
|
| 36 |
+
|
| 37 |
+
for mode in ["sales_agent", "feedback_agent"]:
|
| 38 |
+
filepath = os.path.join(prompts_dir, f"{mode}.txt")
|
| 39 |
+
try:
|
| 40 |
+
with open(filepath, 'r', encoding='utf-8') as f:
|
| 41 |
+
prompts[mode] = f.read()
|
| 42 |
+
print(f"✓ Loaded prompt: {mode}")
|
| 43 |
+
except Exception as e:
|
| 44 |
+
print(f"⚠️ Error loading {mode} prompt: {e}")
|
| 45 |
+
prompts[mode] = ""
|
| 46 |
+
|
| 47 |
+
return prompts
|
| 48 |
+
|
| 49 |
+
async def chat(
|
| 50 |
+
self,
|
| 51 |
+
user_message: str,
|
| 52 |
+
conversation_history: List[Dict],
|
| 53 |
+
mode: str = "sales", # "sales" or "feedback"
|
| 54 |
+
user_id: Optional[str] = None,
|
| 55 |
+
max_iterations: int = 3
|
| 56 |
+
) -> Dict[str, Any]:
|
| 57 |
+
"""
|
| 58 |
+
Main conversation loop
|
| 59 |
+
|
| 60 |
+
Args:
|
| 61 |
+
user_message: User's input
|
| 62 |
+
conversation_history: Previous messages [{"role": "user", "content": ...}, ...]
|
| 63 |
+
mode: "sales" or "feedback"
|
| 64 |
+
user_id: User ID (for feedback mode to check purchase history)
|
| 65 |
+
max_iterations: Maximum tool call iterations to prevent infinite loops
|
| 66 |
+
|
| 67 |
+
Returns:
|
| 68 |
+
{
|
| 69 |
+
"message": "Bot response",
|
| 70 |
+
"tool_calls": [...], # List of tools called (for debugging)
|
| 71 |
+
"mode": mode
|
| 72 |
+
}
|
| 73 |
+
"""
|
| 74 |
+
print(f"\n🤖 Agent Mode: {mode}")
|
| 75 |
+
print(f"👤 User Message: {user_message}")
|
| 76 |
+
|
| 77 |
+
# Select system prompt
|
| 78 |
+
system_prompt = self._get_system_prompt(mode)
|
| 79 |
+
|
| 80 |
+
# Build conversation context
|
| 81 |
+
messages = self._build_messages(system_prompt, conversation_history, user_message)
|
| 82 |
+
|
| 83 |
+
# Agentic loop: LLM may call tools multiple times
|
| 84 |
+
tool_calls_made = []
|
| 85 |
+
current_response = None
|
| 86 |
+
|
| 87 |
+
for iteration in range(max_iterations):
|
| 88 |
+
print(f"\n🔄 Iteration {iteration + 1}")
|
| 89 |
+
|
| 90 |
+
# Call LLM
|
| 91 |
+
llm_response = await self._call_llm(messages)
|
| 92 |
+
print(f"🧠 LLM Response: {llm_response[:200]}...")
|
| 93 |
+
|
| 94 |
+
# Check if LLM wants to call a tool
|
| 95 |
+
tool_result = await self.tools_service.parse_and_execute(llm_response)
|
| 96 |
+
|
| 97 |
+
if not tool_result:
|
| 98 |
+
# No tool call -> This is the final response
|
| 99 |
+
current_response = llm_response
|
| 100 |
+
break
|
| 101 |
+
|
| 102 |
+
# Tool was called
|
| 103 |
+
tool_calls_made.append(tool_result)
|
| 104 |
+
print(f"🔧 Tool Called: {tool_result.get('function')}")
|
| 105 |
+
|
| 106 |
+
# Add tool result to conversation
|
| 107 |
+
messages.append({
|
| 108 |
+
"role": "assistant",
|
| 109 |
+
"content": llm_response
|
| 110 |
+
})
|
| 111 |
+
messages.append({
|
| 112 |
+
"role": "system",
|
| 113 |
+
"content": f"Tool Result:\n{self._format_tool_result(tool_result)}"
|
| 114 |
+
})
|
| 115 |
+
|
| 116 |
+
# If tool returns "run_rag_search", handle it specially
|
| 117 |
+
if tool_result.get("result", {}).get("action") == "run_rag_search":
|
| 118 |
+
rag_results = await self._execute_rag_search(tool_result["result"]["query"])
|
| 119 |
+
messages[-1]["content"] = f"RAG Search Results:\n{rag_results}"
|
| 120 |
+
|
| 121 |
+
# Clean up response
|
| 122 |
+
final_response = current_response or llm_response
|
| 123 |
+
final_response = self._clean_response(final_response)
|
| 124 |
+
|
| 125 |
+
return {
|
| 126 |
+
"message": final_response,
|
| 127 |
+
"tool_calls": tool_calls_made,
|
| 128 |
+
"mode": mode
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
def _get_system_prompt(self, mode: str) -> str:
|
| 132 |
+
"""Get system prompt for selected mode"""
|
| 133 |
+
prompt_key = f"{mode}_agent" if mode in ["sales", "feedback"] else "sales_agent"
|
| 134 |
+
return self.prompts.get(prompt_key, "")
|
| 135 |
+
|
| 136 |
+
def _build_messages(
|
| 137 |
+
self,
|
| 138 |
+
system_prompt: str,
|
| 139 |
+
history: List[Dict],
|
| 140 |
+
user_message: str
|
| 141 |
+
) -> List[Dict]:
|
| 142 |
+
"""Build messages array for LLM"""
|
| 143 |
+
messages = [{"role": "system", "content": system_prompt}]
|
| 144 |
+
|
| 145 |
+
# Add conversation history
|
| 146 |
+
messages.extend(history)
|
| 147 |
+
|
| 148 |
+
# Add current user message
|
| 149 |
+
messages.append({"role": "user", "content": user_message})
|
| 150 |
+
|
| 151 |
+
return messages
|
| 152 |
+
|
| 153 |
+
async def _call_llm(self, messages: List[Dict]) -> str:
|
| 154 |
+
"""
|
| 155 |
+
Call HuggingFace LLM
|
| 156 |
+
Uses advanced_rag's chat method
|
| 157 |
+
"""
|
| 158 |
+
try:
|
| 159 |
+
# Build prompt from messages
|
| 160 |
+
prompt = self._messages_to_prompt(messages)
|
| 161 |
+
|
| 162 |
+
# Call HF API via advanced_rag
|
| 163 |
+
response = await self.advanced_rag.chat_completion(
|
| 164 |
+
user_prompt=prompt,
|
| 165 |
+
context="", # Context is already in system prompt
|
| 166 |
+
chat_history=[], # History is in messages
|
| 167 |
+
token=self.hf_token
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
return response
|
| 171 |
+
except Exception as e:
|
| 172 |
+
print(f"⚠️ LLM Call Error: {e}")
|
| 173 |
+
return "Xin lỗi, tôi đang gặp chút vấn đề kỹ thuật. Bạn thử lại sau nhé!"
|
| 174 |
+
|
| 175 |
+
def _messages_to_prompt(self, messages: List[Dict]) -> str:
|
| 176 |
+
"""Convert messages array to single prompt string"""
|
| 177 |
+
prompt_parts = []
|
| 178 |
+
|
| 179 |
+
for msg in messages:
|
| 180 |
+
role = msg["role"]
|
| 181 |
+
content = msg["content"]
|
| 182 |
+
|
| 183 |
+
if role == "system":
|
| 184 |
+
prompt_parts.append(f"[SYSTEM]\n{content}\n")
|
| 185 |
+
elif role == "user":
|
| 186 |
+
prompt_parts.append(f"[USER]\n{content}\n")
|
| 187 |
+
elif role == "assistant":
|
| 188 |
+
prompt_parts.append(f"[ASSISTANT]\n{content}\n")
|
| 189 |
+
|
| 190 |
+
return "\n".join(prompt_parts)
|
| 191 |
+
|
| 192 |
+
def _format_tool_result(self, tool_result: Dict) -> str:
|
| 193 |
+
"""Format tool result for feeding back to LLM"""
|
| 194 |
+
result = tool_result.get("result", {})
|
| 195 |
+
|
| 196 |
+
if isinstance(result, dict):
|
| 197 |
+
# Pretty print key info
|
| 198 |
+
formatted = []
|
| 199 |
+
for key, value in result.items():
|
| 200 |
+
if key not in ["success", "error"]:
|
| 201 |
+
formatted.append(f"{key}: {value}")
|
| 202 |
+
return "\n".join(formatted)
|
| 203 |
+
|
| 204 |
+
return str(result)
|
| 205 |
+
|
| 206 |
+
async def _execute_rag_search(self, query_params: Dict) -> str:
|
| 207 |
+
"""
|
| 208 |
+
Execute RAG search for event discovery
|
| 209 |
+
Called when LLM wants to search_events
|
| 210 |
+
"""
|
| 211 |
+
query = query_params.get("query", "")
|
| 212 |
+
vibe = query_params.get("vibe", "")
|
| 213 |
+
|
| 214 |
+
# Build search query
|
| 215 |
+
search_text = f"{query} {vibe}".strip()
|
| 216 |
+
|
| 217 |
+
print(f"🔍 RAG Search: {search_text}")
|
| 218 |
+
|
| 219 |
+
# Use embedding + qdrant
|
| 220 |
+
embedding = self.embedding_service.encode_text(search_text)
|
| 221 |
+
results = self.qdrant_service.search(
|
| 222 |
+
collection_name="events",
|
| 223 |
+
query_vector=embedding,
|
| 224 |
+
limit=5
|
| 225 |
+
)
|
| 226 |
+
|
| 227 |
+
# Format results
|
| 228 |
+
formatted = []
|
| 229 |
+
for i, result in enumerate(results, 1):
|
| 230 |
+
payload = result.payload or {}
|
| 231 |
+
texts = payload.get("texts", [])
|
| 232 |
+
text = texts[0] if texts else ""
|
| 233 |
+
event_id = payload.get("id_use", "")
|
| 234 |
+
|
| 235 |
+
formatted.append(f"{i}. {text[:100]}... (ID: {event_id})")
|
| 236 |
+
|
| 237 |
+
return "\n".join(formatted) if formatted else "Không tìm thấy sự kiện phù hợp."
|
| 238 |
+
|
| 239 |
+
def _clean_response(self, response: str) -> str:
|
| 240 |
+
"""Remove JSON artifacts from final response"""
|
| 241 |
+
# Remove JSON blocks
|
| 242 |
+
if "```json" in response:
|
| 243 |
+
response = response.split("```json")[0]
|
| 244 |
+
if "```" in response:
|
| 245 |
+
response = response.split("```")[0]
|
| 246 |
+
|
| 247 |
+
# Remove tool call markers
|
| 248 |
+
if "{" in response and "tool_call" in response:
|
| 249 |
+
# Find the last natural sentence before JSON
|
| 250 |
+
lines = response.split("\n")
|
| 251 |
+
cleaned = []
|
| 252 |
+
for line in lines:
|
| 253 |
+
if "{" in line and "tool_call" in line:
|
| 254 |
+
break
|
| 255 |
+
cleaned.append(line)
|
| 256 |
+
response = "\n".join(cleaned)
|
| 257 |
+
|
| 258 |
+
return response.strip()
|
main.py
CHANGED
|
@@ -19,11 +19,8 @@ from pdf_parser import PDFIndexer
|
|
| 19 |
from multimodal_pdf_parser import MultimodalPDFIndexer
|
| 20 |
from conversation_service import ConversationService
|
| 21 |
from tools_service import ToolsService
|
| 22 |
-
from
|
| 23 |
-
from
|
| 24 |
-
from lead_storage_service import LeadStorageService # NEW
|
| 25 |
-
from hybrid_chat_endpoint import hybrid_chat_endpoint # NEW
|
| 26 |
-
from hybrid_chat_stream import hybrid_chat_stream # NEW: Streaming
|
| 27 |
|
| 28 |
# Initialize FastAPI app
|
| 29 |
app = FastAPI(
|
|
@@ -109,19 +106,18 @@ conversation_service = ConversationService(conversations_collection, max_history
|
|
| 109 |
print("✓ Conversation Service initialized")
|
| 110 |
|
| 111 |
# Initialize Tools Service
|
| 112 |
-
tools_service = ToolsService(base_url="https://
|
| 113 |
print("✓ Tools Service initialized (Function Calling enabled)")
|
| 114 |
|
| 115 |
-
# Initialize
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
print("✓ Lead Storage Service initialized")
|
| 125 |
|
| 126 |
print("✓ Services initialized successfully")
|
| 127 |
|
|
@@ -152,6 +148,7 @@ class ChatRequest(BaseModel):
|
|
| 152 |
message: str
|
| 153 |
session_id: Optional[str] = None # Multi-turn conversation
|
| 154 |
user_id: Optional[str] = None # User identifier for session tracking
|
|
|
|
| 155 |
use_rag: bool = True
|
| 156 |
top_k: int = 3
|
| 157 |
system_message: Optional[str] = """Bạn là trợ lý AI chuyên biệt cho hệ thống quản lý sự kiện và bán vé.
|
|
@@ -694,184 +691,11 @@ async def get_stats():
|
|
| 694 |
|
| 695 |
|
| 696 |
# ============================================
|
| 697 |
-
# ChatbotRAG Endpoints
|
|
|
|
| 698 |
# ============================================
|
|
|
|
| 699 |
|
| 700 |
-
# Import chat endpoint logic
|
| 701 |
-
from hybrid_chat_endpoint import hybrid_chat_endpoint
|
| 702 |
-
|
| 703 |
-
@app.post("/chat", response_model=ChatResponse)
|
| 704 |
-
async def chat(request: ChatRequest):
|
| 705 |
-
"""
|
| 706 |
-
Hybrid Conversational Chatbot: Scenario FSM + RAG
|
| 707 |
-
|
| 708 |
-
Features:
|
| 709 |
-
- ✅ Scenario-based flows (giá vé, đặt vé kịch bản)
|
| 710 |
-
- ✅ RAG knowledge retrieval (PDF, documents)
|
| 711 |
-
- ✅ Mid-scenario RAG interruption (answer off-topic questions)
|
| 712 |
-
- ✅ Lead collection (email, phone → MongoDB)
|
| 713 |
-
- ✅ Multi-turn conversations with state management
|
| 714 |
-
- ✅ Function calling (external API integration)
|
| 715 |
-
|
| 716 |
-
Flow:
|
| 717 |
-
1. User message → Intent classification
|
| 718 |
-
2. Route to: Scenario FSM OR RAG OR Hybrid
|
| 719 |
-
3. Execute flow + save state
|
| 720 |
-
4. Save conversation history
|
| 721 |
-
|
| 722 |
-
Example 1 - Start Price Inquiry Scenario:
|
| 723 |
-
```
|
| 724 |
-
POST /chat
|
| 725 |
-
{
|
| 726 |
-
"message": "giá vé bao nhiêu?",
|
| 727 |
-
"use_rag": true
|
| 728 |
-
}
|
| 729 |
-
|
| 730 |
-
Response:
|
| 731 |
-
{
|
| 732 |
-
"response": "Hello 👋 Bạn muốn xem giá của show nào để mình báo đúng nè?",
|
| 733 |
-
"session_id": "abc-123",
|
| 734 |
-
"mode": "scenario",
|
| 735 |
-
"scenario_active": true
|
| 736 |
-
}
|
| 737 |
-
```
|
| 738 |
-
|
| 739 |
-
Example 2 - Continue Scenario:
|
| 740 |
-
```
|
| 741 |
-
POST /chat
|
| 742 |
-
{
|
| 743 |
-
"message": "Show A",
|
| 744 |
-
"session_id": "abc-123"
|
| 745 |
-
}
|
| 746 |
-
|
| 747 |
-
Response:
|
| 748 |
-
{
|
| 749 |
-
"response": "Bạn đi 1 mình hay đi nhóm...",
|
| 750 |
-
"mode": "scenario",
|
| 751 |
-
"scenario_active": true
|
| 752 |
-
}
|
| 753 |
-
```
|
| 754 |
-
|
| 755 |
-
Example 3 - Mid-scenario RAG Question:
|
| 756 |
-
```
|
| 757 |
-
POST /chat
|
| 758 |
-
{
|
| 759 |
-
"message": "sự kiện mấy giờ?",
|
| 760 |
-
"session_id": "abc-123"
|
| 761 |
-
}
|
| 762 |
-
# Bot answers from RAG, then resumes scenario
|
| 763 |
-
```
|
| 764 |
-
|
| 765 |
-
Example 4 - Pure RAG Query:
|
| 766 |
-
```
|
| 767 |
-
POST /chat
|
| 768 |
-
{
|
| 769 |
-
"message": "địa điểm sự kiện ở đâu?",
|
| 770 |
-
"use_rag": true
|
| 771 |
-
}
|
| 772 |
-
# Normal RAG response (không trigger scenario)
|
| 773 |
-
```
|
| 774 |
-
"""
|
| 775 |
-
return await hybrid_chat_endpoint(
|
| 776 |
-
request=request,
|
| 777 |
-
conversation_service=conversation_service,
|
| 778 |
-
intent_classifier=intent_classifier,
|
| 779 |
-
embedding_service=embedding_service, # NEW: Required by handlers
|
| 780 |
-
qdrant_service=qdrant_service, # NEW: Required by handlers
|
| 781 |
-
tools_service=tools_service,
|
| 782 |
-
advanced_rag=advanced_rag,
|
| 783 |
-
chat_history_collection=chat_history_collection,
|
| 784 |
-
hf_token=hf_token,
|
| 785 |
-
lead_storage=lead_storage
|
| 786 |
-
)
|
| 787 |
-
|
| 788 |
-
|
| 789 |
-
@app.post("/chat/stream")
|
| 790 |
-
async def chat_stream(request: ChatRequest):
|
| 791 |
-
"""
|
| 792 |
-
Streaming Chat Endpoint (SSE - Server-Sent Events)
|
| 793 |
-
|
| 794 |
-
Real-time token-by-token response display
|
| 795 |
-
|
| 796 |
-
Features:
|
| 797 |
-
- ✅ Real-time "typing" effect
|
| 798 |
-
- ✅ Status updates (thinking, searching)
|
| 799 |
-
- ✅ Scenario: Simulated streaming (smooth typing)
|
| 800 |
-
- ✅ RAG: Real LLM streaming
|
| 801 |
-
- ✅ HTTP/2 compatible
|
| 802 |
-
|
| 803 |
-
Event Types:
|
| 804 |
-
- status: Bot status ("Đang suy nghĩ...", "Đang tìm kiếm...")
|
| 805 |
-
- token: Text chunks
|
| 806 |
-
- metadata: Session ID, context info
|
| 807 |
-
- done: Completion signal
|
| 808 |
-
- error: Error messages
|
| 809 |
-
|
| 810 |
-
Example - JavaScript Client:
|
| 811 |
-
```javascript
|
| 812 |
-
const response = await fetch('/chat/stream', {
|
| 813 |
-
method: 'POST',
|
| 814 |
-
headers: { 'Content-Type': 'application/json' },
|
| 815 |
-
body: JSON.stringify({
|
| 816 |
-
message: "giá vé bao nhiêu?",
|
| 817 |
-
use_rag: true
|
| 818 |
-
})
|
| 819 |
-
});
|
| 820 |
-
|
| 821 |
-
const reader = response.body.getReader();
|
| 822 |
-
const decoder = new TextDecoder();
|
| 823 |
-
|
| 824 |
-
while (true) {
|
| 825 |
-
const {done, value} = await reader.read();
|
| 826 |
-
if (done) break;
|
| 827 |
-
|
| 828 |
-
const chunk = decoder.decode(value);
|
| 829 |
-
const lines = chunk.split('\n\n');
|
| 830 |
-
|
| 831 |
-
for (const line of lines) {
|
| 832 |
-
if (line.startsWith('event: token')) {
|
| 833 |
-
const data = line.split('data: ')[1];
|
| 834 |
-
displayToken(data); // Append to UI
|
| 835 |
-
}
|
| 836 |
-
else if (line.startsWith('event: done')) {
|
| 837 |
-
console.log('Stream complete');
|
| 838 |
-
}
|
| 839 |
-
}
|
| 840 |
-
}
|
| 841 |
-
```
|
| 842 |
-
|
| 843 |
-
Example - EventSource (simpler but less control):
|
| 844 |
-
```javascript
|
| 845 |
-
// Note: EventSource doesn't support POST, need to use fetch
|
| 846 |
-
const eventSource = new EventSource('/chat/stream?message=hello');
|
| 847 |
-
|
| 848 |
-
eventSource.addEventListener('token', (e) => {
|
| 849 |
-
displayToken(e.data);
|
| 850 |
-
});
|
| 851 |
-
|
| 852 |
-
eventSource.addEventListener('done', (e) => {
|
| 853 |
-
eventSource.close();
|
| 854 |
-
});
|
| 855 |
-
```
|
| 856 |
-
"""
|
| 857 |
-
return StreamingResponse(
|
| 858 |
-
hybrid_chat_stream(
|
| 859 |
-
request=request,
|
| 860 |
-
conversation_service=conversation_service,
|
| 861 |
-
intent_classifier=intent_classifier,
|
| 862 |
-
embedding_service=embedding_service, # For handlers
|
| 863 |
-
qdrant_service=qdrant_service, # For handlers
|
| 864 |
-
advanced_rag=advanced_rag,
|
| 865 |
-
hf_token=hf_token,
|
| 866 |
-
lead_storage=lead_storage
|
| 867 |
-
),
|
| 868 |
-
media_type="text/event-stream",
|
| 869 |
-
headers={
|
| 870 |
-
"Cache-Control": "no-cache",
|
| 871 |
-
"Connection": "keep-alive",
|
| 872 |
-
"X-Accel-Buffering": "no" # Disable nginx buffering
|
| 873 |
-
}
|
| 874 |
-
)
|
| 875 |
|
| 876 |
|
| 877 |
@app.get("/chat/history/{session_id}")
|
|
@@ -1421,6 +1245,65 @@ async def delete_document_from_kb(doc_id: str):
|
|
| 1421 |
raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
|
| 1422 |
|
| 1423 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1424 |
if __name__ == "__main__":
|
| 1425 |
import uvicorn
|
| 1426 |
uvicorn.run(
|
|
|
|
| 19 |
from multimodal_pdf_parser import MultimodalPDFIndexer
|
| 20 |
from conversation_service import ConversationService
|
| 21 |
from tools_service import ToolsService
|
| 22 |
+
from agent_service import AgentService
|
| 23 |
+
from agent_chat_stream import agent_chat_stream # NEW: Agent Streaming
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
# Initialize FastAPI app
|
| 26 |
app = FastAPI(
|
|
|
|
| 106 |
print("✓ Conversation Service initialized")
|
| 107 |
|
| 108 |
# Initialize Tools Service
|
| 109 |
+
tools_service = ToolsService(base_url="https://hoalacrent.io.vn/api/v0")
|
| 110 |
print("✓ Tools Service initialized (Function Calling enabled)")
|
| 111 |
|
| 112 |
+
# Initialize Agent Service (Agentic Workflow)
|
| 113 |
+
agent_service = AgentService(
|
| 114 |
+
tools_service=tools_service,
|
| 115 |
+
embedding_service=embedding_service,
|
| 116 |
+
qdrant_service=qdrant_service,
|
| 117 |
+
advanced_rag=advanced_rag,
|
| 118 |
+
hf_token=hf_token
|
| 119 |
+
)
|
| 120 |
+
print("✓ Agent Service initialized (Agentic Workflow enabled)")
|
|
|
|
| 121 |
|
| 122 |
print("✓ Services initialized successfully")
|
| 123 |
|
|
|
|
| 148 |
message: str
|
| 149 |
session_id: Optional[str] = None # Multi-turn conversation
|
| 150 |
user_id: Optional[str] = None # User identifier for session tracking
|
| 151 |
+
mode: str = "sales" # NEW: "sales" or "feedback" for agent selection
|
| 152 |
use_rag: bool = True
|
| 153 |
top_k: int = 3
|
| 154 |
system_message: Optional[str] = """Bạn là trợ lý AI chuyên biệt cho hệ thống quản lý sự kiện và bán vé.
|
|
|
|
| 691 |
|
| 692 |
|
| 693 |
# ============================================
|
| 694 |
+
# ChatbotRAG Endpoints - DEPRECATED
|
| 695 |
+
# USE /agent/chat INSTEAD
|
| 696 |
# ============================================
|
| 697 |
+
# Old endpoints removed - now using Agentic Workflow via /agent/chat
|
| 698 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 699 |
|
| 700 |
|
| 701 |
@app.get("/chat/history/{session_id}")
|
|
|
|
| 1245 |
raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
|
| 1246 |
|
| 1247 |
|
| 1248 |
+
# ===================================
|
| 1249 |
+
# AGENT CHAT STREAMING ENDPOINT (NEW)
|
| 1250 |
+
# ===================================
|
| 1251 |
+
|
| 1252 |
+
@app.post("/agent/chat")
|
| 1253 |
+
async def agent_chat(request: ChatRequest):
|
| 1254 |
+
"""
|
| 1255 |
+
🤖 **Agentic Chatbot với SSE Streaming**
|
| 1256 |
+
|
| 1257 |
+
**Modes:**
|
| 1258 |
+
- `sales`: Sales Agent - Tư vấn sự kiện, chốt sale
|
| 1259 |
+
- `feedback`: Feedback Agent - CSKH, thu thập đánh giá
|
| 1260 |
+
|
| 1261 |
+
**Features:**
|
| 1262 |
+
- ✅ LLM-driven conversation (no hard-coded scenarios)
|
| 1263 |
+
- ✅ Automatic tool calling (search, get_event_details, save_lead...)
|
| 1264 |
+
- ✅ Real-time SSE streaming
|
| 1265 |
+
- ✅ Purchase history check (for feedback mode)
|
| 1266 |
+
|
| 1267 |
+
**Example:**
|
| 1268 |
+
```
|
| 1269 |
+
POST /agent/chat
|
| 1270 |
+
{
|
| 1271 |
+
"message": "Tìm event cho tôi",
|
| 1272 |
+
"mode": "sales",
|
| 1273 |
+
"user_id": "user_123"
|
| 1274 |
+
}
|
| 1275 |
+
```
|
| 1276 |
+
|
| 1277 |
+
**SSE Stream:**
|
| 1278 |
+
```
|
| 1279 |
+
event: status
|
| 1280 |
+
data: Đang tư vấn...
|
| 1281 |
+
|
| 1282 |
+
event: token
|
| 1283 |
+
data: Hello
|
| 1284 |
+
|
| 1285 |
+
event: token
|
| 1286 |
+
data: 👋
|
| 1287 |
+
|
| 1288 |
+
event: done
|
| 1289 |
+
data: {"session_id": "...", "mode": "sales"}
|
| 1290 |
+
```
|
| 1291 |
+
"""
|
| 1292 |
+
return StreamingResponse(
|
| 1293 |
+
agent_chat_stream(
|
| 1294 |
+
request=request,
|
| 1295 |
+
agent_service=agent_service,
|
| 1296 |
+
conversation_service=conversation_service
|
| 1297 |
+
),
|
| 1298 |
+
media_type="text/event-stream",
|
| 1299 |
+
headers={
|
| 1300 |
+
"Cache-Control": "no-cache",
|
| 1301 |
+
"Connection": "keep-alive",
|
| 1302 |
+
"X-Accel-Buffering": "no"
|
| 1303 |
+
}
|
| 1304 |
+
)
|
| 1305 |
+
|
| 1306 |
+
|
| 1307 |
if __name__ == "__main__":
|
| 1308 |
import uvicorn
|
| 1309 |
uvicorn.run(
|
prompts/feedback_agent.txt
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ROLE
|
| 2 |
+
Bạn là chuyên viên Chăm sóc khách hàng (CSKH) của nền tảng bán vé sự kiện.
|
| 3 |
+
Nhiệm vụ của bạn là lắng nghe phản hồi của khách hàng sau khi tham gia sự kiện và hỗ trợ họ.
|
| 4 |
+
|
| 5 |
+
# GOAL
|
| 6 |
+
1. Kiểm tra xem khách hàng đã tham gia sự kiện nào chưa.
|
| 7 |
+
2. Nếu CÓ: Xin đánh giá (feedback), cảm nhận để cải thiện dịch vụ.
|
| 8 |
+
3. Nếu KHÔNG (hoặc đã feedback xong): Giới thiệu các sự kiện mới hấp dẫn (chuyển sang vai trò Sales).
|
| 9 |
+
|
| 10 |
+
# CAPABILITIES (TOOLS)
|
| 11 |
+
1. `get_purchased_events(user_id)`: Kiểm tra lịch sử mua vé/tham gia sự kiện của khách hàng.
|
| 12 |
+
2. `save_feedback(event_id, rating, comment)`: Lưu đánh giá của khách hàng (rating 1-5 sao).
|
| 13 |
+
3. `search_events(...)`: Tìm sự kiện mới (nếu khách muốn đi tiếp).
|
| 14 |
+
|
| 15 |
+
# GUIDELINES
|
| 16 |
+
|
| 17 |
+
## Phase 1: Check History (Luôn thực hiện đầu tiên)
|
| 18 |
+
- Ngay khi bắt đầu hội thoại, hãy gọi `get_purchased_events(user_id)` ngầm (không cần hỏi khách).
|
| 19 |
+
- **Trường hợp A: Khách chưa từng đi sự kiện nào (hoặc API trả về rỗng)**
|
| 20 |
+
- Chuyển ngay sang mode tư vấn: "Chào bạn! Bạn đang tìm kiếm sự kiện gì thú vị cho tuần này không? Bên mình đang có nhiều show hay lắm! 🎉"
|
| 21 |
+
- (Sau đó hành xử như Sales Agent).
|
| 22 |
+
|
| 23 |
+
- **Trường hợp B: Khách ĐÃ đi sự kiện (ví dụ: "Show Hà Anh Tuấn")**
|
| 24 |
+
- Mở đầu bằng lời chào ấm áp: "Chào bạn! Cảm ơn bạn đã tham gia show **Hà Anh Tuấn** vừa rồi. Hy vọng bạn đã có những giây phút tuyệt vời! 🥰"
|
| 25 |
+
- Hỏi thăm cảm nhận: "Bạn thấy không khí hôm đó thế nào? Có điều gì làm bạn chưa hài lòng không?"
|
| 26 |
+
|
| 27 |
+
## Phase 2: Collect Feedback (Nếu khách đã đi)
|
| 28 |
+
- Lắng nghe khách chia sẻ.
|
| 29 |
+
- Nếu khách khen: "Tuyệt quá! Bạn chấm cho sự kiện mấy sao nè? (1-5 sao) ⭐"
|
| 30 |
+
- Nếu khách chê: Tỏ ra đồng cảm, xin lỗi và hứa cải thiện. "Dạ mình rất tiếc về trải nghiệm này. Mình sẽ ghi nhận ngay để BTC rút kinh nghiệm ạ."
|
| 31 |
+
- Sau khi khách chấm điểm/comment -> Gọi `save_feedback`.
|
| 32 |
+
|
| 33 |
+
## Phase 3: Transition to Sales (Sau khi feedback xong)
|
| 34 |
+
- Sau khi đã lưu feedback, hãy khéo léo giới thiệu sự kiện mới:
|
| 35 |
+
"Cảm ơn bạn đã góp ý nha! À, sắp tới bên mình có show **Mỹ Tâm** cũng vibe tương tự, bạn có muốn xem qua không?"
|
| 36 |
+
- Nếu khách quan tâm -> Dùng `search_events` và tư vấn tiếp.
|
| 37 |
+
|
| 38 |
+
# EXAMPLES
|
| 39 |
+
|
| 40 |
+
**Case 1: Có lịch sử đi event**
|
| 41 |
+
System: (User ID 123 -> get_purchased_events -> ["Show Rock Việt"])
|
| 42 |
+
Agent: "Chào bạn! Cảm ơn bạn đã cháy hết mình tại **Show Rock Việt** hôm qua! 🤘 Bạn thấy ban nhạc diễn có sung không?"
|
| 43 |
+
User: "Sung lắm, nhưng âm thanh hơi rè."
|
| 44 |
+
Agent: "Dạ mình ghi nhận góp ý về âm thanh ạ. Cảm ơn bạn nhiều. Bạn chấm show này mấy điểm trên thang 5 sao nè?"
|
| 45 |
+
User: "4 sao thôi."
|
| 46 |
+
Agent (Call Tool): save_feedback(event_id="rock_viet", rating=4, comment="Sung nhưng âm thanh rè")
|
| 47 |
+
Agent: "Dạ mình đã lưu lại rồi ạ. À sắp tới có **RockStorm** âm thanh xịn hơn, bạn có hóng không? 🔥"
|
| 48 |
+
|
| 49 |
+
**Case 2: Không có lịch sử**
|
| 50 |
+
System: (User ID 456 -> get_purchased_events -> [])
|
| 51 |
+
Agent: "Chào bạn! 👋 Cuối tuần này bạn đã có kế hoạch đi đâu chơi chưa? Bên mình đang có mấy show Acoustic chill lắm nè!"
|
prompts/sales_agent.txt
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ROLE
|
| 2 |
+
Bạn là một chuyên viên tư vấn sự kiện (Sales Agent) nhiệt tình, am hiểu và khéo léo của nền tảng bán vé sự kiện.
|
| 3 |
+
Tên bạn là: "TicketBot" (hoặc xưng là "mình"/"tớ").
|
| 4 |
+
|
| 5 |
+
# GOAL
|
| 6 |
+
Mục tiêu của bạn là giúp khách hàng tìm được sự kiện phù hợp nhất và khuyến khích họ mua vé (hoặc để lại thông tin liên hệ).
|
| 7 |
+
|
| 8 |
+
# CAPABILITIES (TOOLS)
|
| 9 |
+
Bạn có quyền truy cập các công cụ sau (hãy sử dụng chúng khi cần thiết):
|
| 10 |
+
1. `search_events(query, vibe, date)`: Tìm kiếm sự kiện theo từ khóa, tâm trạng (chill, sôi động...), hoặc thời gian.
|
| 11 |
+
2. `get_event_details(event_id)`: Lấy thông tin chi tiết (giá vé, địa điểm, nghệ sĩ, thời gian) của một sự kiện cụ thể.
|
| 12 |
+
3. `save_lead(email, phone, interest)`: Lưu thông tin khách hàng khi họ quan tâm hoặc muốn nhận tư vấn thêm.
|
| 13 |
+
|
| 14 |
+
# GUIDELINES
|
| 15 |
+
1. **Khơi gợi nhu cầu (Consultative Selling):**
|
| 16 |
+
- Đừng chỉ hỏi "Bạn muốn gì?". Hãy hỏi mở: "Cuối tuần này bạn rảnh không? Bạn đang mood muốn 'quẩy' hay chill nhẹ nhàng?"
|
| 17 |
+
- Nếu khách chưa rõ, hãy gợi ý dựa trên các vibe phổ biến: Hài kịch, Nhạc Indie, Workshop, EDM...
|
| 18 |
+
|
| 19 |
+
2. **Tư vấn thông minh:**
|
| 20 |
+
- Khi khách hỏi giá, đừng chỉ đưa con số. Hãy kèm giá trị: "Vé hạng A giá 500k nhưng view siêu đẹp, còn hạng B 300k thì tiết kiệm hơn."
|
| 21 |
+
- Luôn đề xuất thêm (Upsell/Cross-sell) nếu phù hợp: "Đi nhóm 4 người đang có combo giảm 10% đó ạ."
|
| 22 |
+
|
| 23 |
+
3. **Sử dụng Tools khéo léo:**
|
| 24 |
+
- Khi khách hỏi "có sự kiện gì?", HÃY gọi `search_events`. Đừng tự bịa ra sự kiện.
|
| 25 |
+
- Khi trả về danh sách sự kiện, hãy tóm tắt ngắn gọn điểm hấp dẫn nhất của từng cái.
|
| 26 |
+
|
| 27 |
+
4. **Chốt Deal (Closing):**
|
| 28 |
+
- Khi khách có vẻ ưng ý (hỏi chi tiết, giá, chỗ ngồi...), hãy khéo léo xin thông tin:
|
| 29 |
+
"Sự kiện này đang hot lắm, bạn cho mình xin email để mình gửi link đặt vé giữ chỗ ngay nhé?"
|
| 30 |
+
- Hoặc: "Mình gửi lịch diễn chi tiết qua Zalo/Email cho bạn tiện xem nha?" -> Gọi `save_lead`.
|
| 31 |
+
|
| 32 |
+
5. **Tone & Voice:**
|
| 33 |
+
- Thân thiện, trẻ trung, dùng emoji tự nhiên (😄, 🎉, 🔥).
|
| 34 |
+
- Không quá cứng nhắc như robot.
|
| 35 |
+
- Nếu khách hỏi ngoài lề (off-topic), hãy trả lời ngắn gọn rồi khéo léo lái về chủ đề sự kiện.
|
| 36 |
+
|
| 37 |
+
# EXAMPLES
|
| 38 |
+
|
| 39 |
+
User: "Cuối tuần này có gì chơi không?"
|
| 40 |
+
Agent (Thought): Khách chưa nói rõ sở thích. Cần hỏi thêm vibe.
|
| 41 |
+
Agent: "Cuối tuần này Sài Gòn nhiều show hay lắm! Bạn đang mood muốn 'quẩy' hết mình hay tìm một góc chill chill nghe nhạc? 🎶"
|
| 42 |
+
|
| 43 |
+
User: "Chill thôi, nghe nhạc acoustic."
|
| 44 |
+
Agent (Thought): Gọi tool search_events(vibe="chill", category="acoustic").
|
| 45 |
+
Agent (Call Tool): search_events(vibe="chill", category="acoustic")
|
| 46 |
+
... (Tool returns events) ...
|
| 47 |
+
Agent: "À, vậy thì **Mây Lang Thang** hôm thứ 7 này là chuẩn bài! Có Lê Hiếu hát, không gian cực lãng mạn. Hoặc **Lululola** thì view hoàng hôn đỉnh chóp. Bạn thích giọng ai hơn? 🎤"
|
tools_service.py
CHANGED
|
@@ -7,52 +7,6 @@ from typing import List, Dict, Any, Optional
|
|
| 7 |
import json
|
| 8 |
import asyncio
|
| 9 |
|
| 10 |
-
|
| 11 |
-
class ToolsService:
|
| 12 |
-
"""
|
| 13 |
-
Manages external API tools that LLM can call via prompt engineering
|
| 14 |
-
"""
|
| 15 |
-
|
| 16 |
-
def __init__(self, base_url: str = "https://www.festavenue.site"):
|
| 17 |
-
self.base_url = base_url
|
| 18 |
-
self.client = httpx.AsyncClient(timeout=10.0)
|
| 19 |
-
|
| 20 |
-
def get_tools_prompt(self) -> str:
|
| 21 |
-
"""
|
| 22 |
-
Return prompt instruction for HuggingFace LLM về available tools
|
| 23 |
-
"""
|
| 24 |
-
return """
|
| 25 |
-
AVAILABLE TOOLS:
|
| 26 |
-
Bạn có thể sử dụng các công cụ sau để lấy thông tin chi tiết:
|
| 27 |
-
|
| 28 |
-
1. get_event_details(event_code: str)
|
| 29 |
-
- Mô tả: Lấy thông tin đầy đủ về một sự kiện từ hệ thống
|
| 30 |
-
- Khi nào dùng: Khi user hỏi về ngày giờ chính xác, địa điểm cụ thể, thông tin liên hệ, hoặc chi tiết khác về một sự kiện
|
| 31 |
-
- Tham số: event_code = ID sự kiện (LẤY TỪ metadata.id_use TRONG CONTEXT, KHÔNG PHẢI tên sự kiện!)
|
| 32 |
-
|
| 33 |
-
VÍ DỤ QUAN TRỌNG:
|
| 34 |
-
Context có:
|
| 35 |
-
```
|
| 36 |
-
metadata: {
|
| 37 |
-
"id_use": "69194cf61c0eda56688806f7", ← DÙNG CÁI NÀY!
|
| 38 |
-
"texts": ["Y-CONCERT - Festival âm nhạc..."]
|
| 39 |
-
}
|
| 40 |
-
```
|
| 41 |
-
→ Dùng event_code = "69194cf61c0eda56688806f7" (NOT "Y-CONCERT")
|
| 42 |
-
|
| 43 |
-
CÚ PHÁP GỌI TOOL:
|
| 44 |
-
Khi bạn cần gọi tool, hãy trả lời CHÍNH XÁC theo format JSON này:
|
| 45 |
-
```json
|
| 46 |
-
{
|
| 47 |
-
"tool_call": true,
|
| 48 |
-
"function_name": "get_event_details",
|
| 49 |
-
"arguments": {
|
| 50 |
-
"event_code": "69194cf61c0eda56688806f7"
|
| 51 |
-
},
|
| 52 |
-
"reason": "Cần lấy thông tin chính xác về ngày giờ tổ chức"
|
| 53 |
-
}
|
| 54 |
-
```
|
| 55 |
-
|
| 56 |
QUAN TRỌNG:
|
| 57 |
- event_code PHẢI LÀ metadata.id_use từ context (dạng MongoDB ObjectId)
|
| 58 |
- KHÔNG dùng tên sự kiện như "Y-CONCERT" làm event_code
|
|
|
|
| 7 |
import json
|
| 8 |
import asyncio
|
| 9 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
QUAN TRỌNG:
|
| 11 |
- event_code PHẢI LÀ metadata.id_use từ context (dạng MongoDB ObjectId)
|
| 12 |
- KHÔNG dùng tên sự kiện như "Y-CONCERT" làm event_code
|