minhvtt commited on
Commit
24f10ad
·
verified ·
1 Parent(s): cccaf29

Upload 17 files

Browse files
advanced_rag.py CHANGED
@@ -407,4 +407,4 @@ HƯỚNG DẪN TRẢ LỜI:
407
  5. Nếu có nhiều kết quả, hãy liệt kê ngắn gọn các điểm chính (Tên, Thời gian, Địa điểm).
408
  6. Nếu context không liên quan, hãy lịch sự nói rằng bạn chưa tìm thấy thông tin phù hợp trong hệ thống."""
409
 
410
- return prompt_template
 
407
  5. Nếu có nhiều kết quả, hãy liệt kê ngắn gọn các điểm chính (Tên, Thời gian, Địa điểm).
408
  6. Nếu context không liên quan, hãy lịch sự nói rằng bạn chưa tìm thấy thông tin phù hợp trong hệ thống."""
409
 
410
+ return prompt_template
agent_service.py CHANGED
@@ -1,503 +1,432 @@
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
- feedback_tracking=None # NEW: Optional feedback tracking
23
- ):
24
- self.tools_service = tools_service
25
- self.embedding_service = embedding_service
26
- self.qdrant_service = qdrant_service
27
- self.advanced_rag = advanced_rag
28
- self.hf_token = hf_token
29
- self.feedback_tracking = feedback_tracking
30
-
31
- # Load system prompts
32
- self.prompts = self._load_prompts()
33
-
34
- def _load_prompts(self) -> Dict[str, str]:
35
- """Load system prompts from files"""
36
- prompts = {}
37
- prompts_dir = "prompts"
38
-
39
- for mode in ["sales_agent", "feedback_agent"]:
40
- filepath = os.path.join(prompts_dir, f"{mode}.txt")
41
- try:
42
- with open(filepath, 'r', encoding='utf-8') as f:
43
- prompts[mode] = f.read()
44
- print(f"✓ Loaded prompt: {mode}")
45
- except Exception as e:
46
- print(f"⚠️ Error loading {mode} prompt: {e}")
47
- prompts[mode] = ""
48
-
49
- return prompts
50
-
51
- async def chat(
52
- self,
53
- user_message: str,
54
- conversation_history: List[Dict],
55
- mode: str = "sales", # "sales" or "feedback"
56
- user_id: Optional[str] = None,
57
- access_token: Optional[str] = None, # NEW: For authenticated API calls
58
- max_iterations: int = 3
59
- ) -> Dict[str, Any]:
60
- """
61
- Main conversation loop
62
-
63
- Args:
64
- user_message: User's input
65
- conversation_history: Previous messages [{"role": "user", "content": ...}, ...]
66
- mode: "sales" or "feedback"
67
- user_id: User ID (for feedback mode to check purchase history)
68
- access_token: JWT token for authenticated API calls
69
- max_iterations: Maximum tool call iterations to prevent infinite loops
70
-
71
- Returns:
72
- {
73
- "message": "Bot response",
74
- "tool_calls": [...], # List of tools called (for debugging)
75
- "mode": mode
76
- }
77
- """
78
- print(f"\n🤖 Agent Mode: {mode}")
79
- print(f"👤 User Message: {user_message}")
80
- print(f"🔑 Auth Info:")
81
- print(f" - User ID: {user_id}")
82
- print(f" - Access Token: {'✅ Received' if access_token else '❌ None'}")
83
-
84
- # Store user_id and access_token for tool calls
85
- self.current_user_id = user_id
86
- self.current_access_token = access_token
87
- if access_token:
88
- print(f" - Stored access_token for tools: {access_token[:20]}...")
89
- if user_id:
90
- print(f" - Stored user_id for tools: {user_id}")
91
-
92
- # Select system prompt
93
- system_prompt = self._get_system_prompt(mode)
94
-
95
- # Build conversation context
96
- messages = self._build_messages(system_prompt, conversation_history, user_message)
97
-
98
- # Agentic loop: LLM may call tools multiple times
99
- tool_calls_made = []
100
- current_response = None
101
-
102
- for iteration in range(max_iterations):
103
- print(f"\n🔄 Iteration {iteration + 1}")
104
-
105
- # Call LLM
106
- llm_response = await self._call_llm(messages)
107
- print(f"🧠 LLM Response: {llm_response[:200]}...")
108
-
109
- # Check if LLM wants to call a tool
110
- tool_call = self._parse_tool_call(llm_response)
111
-
112
- if not tool_call:
113
- # No tool call -> This is the final response
114
- current_response = llm_response
115
- break
116
-
117
- # Execute tool
118
- print(f"🔧 Tool Called: {tool_call['tool_name']}")
119
-
120
- # Auto-inject real user_id for get_purchased_events
121
- if tool_call['tool_name'] == 'get_purchased_events' and self.current_user_id:
122
- print(f"🔄 Auto-injecting real user_id: {self.current_user_id}")
123
- tool_call['arguments']['user_id'] = self.current_user_id
124
-
125
- tool_result = await self.tools_service.execute_tool(
126
- tool_call['tool_name'],
127
- tool_call['arguments'],
128
- access_token=self.current_access_token # Pass access_token
129
- )
130
-
131
- # Record tool call
132
- tool_calls_made.append({
133
- "function": tool_call['tool_name'],
134
- "arguments": tool_call['arguments'],
135
- "result": tool_result
136
- })
137
-
138
- # Add tool result to conversation
139
- messages.append({
140
- "role": "assistant",
141
- "content": llm_response
142
- })
143
- messages.append({
144
- "role": "system",
145
- "content": f"Tool Result:\n{self._format_tool_result({'result': tool_result})}"
146
- })
147
-
148
- # If tool returns "run_rag_search", handle it specially
149
- if isinstance(tool_result, dict) and tool_result.get("action") == "run_rag_search":
150
- rag_results = await self._execute_rag_search(tool_result["query"])
151
- messages[-1]["content"] = f"RAG Search Results:\n{rag_results}"
152
-
153
- # Clean up response
154
- final_response = current_response or llm_response
155
- final_response = self._clean_response(final_response)
156
-
157
- return {
158
- "message": final_response,
159
- "tool_calls": tool_calls_made,
160
- "mode": mode
161
- }
162
-
163
- def _get_system_prompt(self, mode: str) -> str:
164
- """Get system prompt for selected mode with tools definition"""
165
- prompt_key = f"{mode}_agent" if mode in ["sales", "feedback"] else "sales_agent"
166
- base_prompt = self.prompts.get(prompt_key, "")
167
-
168
- # Add tools definition
169
- tools_definition = self._get_tools_definition()
170
-
171
- return f"{base_prompt}\n\n{tools_definition}"
172
-
173
- def _get_tools_definition(self) -> str:
174
- """Get tools definition in text format for prompt"""
175
- return """
176
- # AVAILABLE TOOLS
177
-
178
- You can call the following tools when needed. To call a tool, output a JSON block like this:
179
-
180
- ```json
181
- {
182
- "tool_call": "tool_name",
183
- "arguments": {
184
- "arg1": "value1",
185
- "arg2": "value2"
186
- }
187
- }
188
- ```
189
-
190
- ## Tools List:
191
-
192
- ### 1. search_events
193
- Search for events matching user criteria.
194
- Arguments:
195
- - query (string): Search keywords
196
- - vibe (string, optional): Mood/vibe (e.g., "chill", "sôi động")
197
- - time (string, optional): Time period (e.g., "cuối tuần này")
198
-
199
- Example:
200
- ```json
201
- {"tool_call": "search_events", "arguments": {"query": "nhạc rock", "vibe": "sôi động"}}
202
- ```
203
-
204
- ### 2. get_event_details
205
- Get detailed information about a specific event.
206
- Arguments:
207
- - event_id (string): Event ID from search results
208
-
209
- Example:
210
- ```json
211
- {"tool_call": "get_event_details", "arguments": {"event_id": "6900ae38eb03f29702c7fd1d"}}
212
- ```
213
-
214
- ### 3. get_purchased_events (Feedback mode only)
215
- Check which events the user has attended.
216
- Arguments:
217
- - user_id (string): User ID
218
-
219
- Example:
220
- ```json
221
- {"tool_call": "get_purchased_events", "arguments": {"user_id": "user_123"}}
222
- ```
223
-
224
- ### 4. save_feedback
225
- Save user's feedback/review for an event.
226
- Arguments:
227
- - event_id (string): Event ID
228
- - rating (integer): 1-5 stars
229
- - comment (string, optional): User's comment
230
-
231
- Example:
232
- ```json
233
- {"tool_call": "save_feedback", "arguments": {"event_id": "abc123", "rating": 5, "comment": "Tuyệt vời!"}}
234
- ```
235
-
236
- ### 5. save_lead
237
- Save customer contact information.
238
- Arguments:
239
- - email (string, optional): Email address
240
- - phone (string, optional): Phone number
241
- - interest (string, optional): What they're interested in
242
-
243
- Example:
244
- ```json
245
- {"tool_call": "save_lead", "arguments": {"email": "user@example.com", "interest": "Rock show"}}
246
- ```
247
-
248
- **IMPORTANT:**
249
- - Call tools ONLY when you need real-time data
250
- - After receiving tool results, respond naturally to the user
251
- - Don't expose raw JSON to users - always format nicely
252
- """
253
-
254
- def _build_messages(
255
- self,
256
- system_prompt: str,
257
- history: List[Dict],
258
- user_message: str
259
- ) -> List[Dict]:
260
- """Build messages array for LLM"""
261
- messages = [{"role": "system", "content": system_prompt}]
262
-
263
- # Add conversation history
264
- messages.extend(history)
265
-
266
- # Add current user message
267
- messages.append({"role": "user", "content": user_message})
268
-
269
- return messages
270
-
271
- async def _call_llm(self, messages: List[Dict]) -> str:
272
- """
273
- Call HuggingFace LLM directly using chat_completion (conversational)
274
- """
275
- try:
276
- from huggingface_hub import AsyncInferenceClient
277
-
278
- # Create async client
279
- client = AsyncInferenceClient(token=self.hf_token)
280
-
281
- # Call HF API with chat completion (conversational)
282
- response_text = ""
283
- async for message in await client.chat_completion(
284
- messages=messages, # Use messages directly
285
- model="openai/gpt-oss-20b",
286
- max_tokens=512,
287
- temperature=0.7,
288
- stream=True
289
- ):
290
- if message.choices and message.choices[0].delta.content:
291
- response_text += message.choices[0].delta.content
292
-
293
- return response_text
294
- except Exception as e:
295
- print(f"⚠️ LLM Call Error: {e}")
296
- return "Xin lỗi, tôi đang gặp chút vấn đề kỹ thuật. Bạn thử lại sau nhé!"
297
-
298
- def _messages_to_prompt(self, messages: List[Dict]) -> str:
299
- """Convert messages array to single prompt string"""
300
- prompt_parts = []
301
-
302
- for msg in messages:
303
- role = msg["role"]
304
- content = msg["content"]
305
-
306
- if role == "system":
307
- prompt_parts.append(f"[SYSTEM]\n{content}\n")
308
- elif role == "user":
309
- prompt_parts.append(f"[USER]\n{content}\n")
310
- elif role == "assistant":
311
- prompt_parts.append(f"[ASSISTANT]\n{content}\n")
312
-
313
- return "\n".join(prompt_parts)
314
-
315
- def _format_tool_result(self, tool_result: Dict) -> str:
316
- """Format tool result for feeding back to LLM"""
317
- result = tool_result.get("result", {})
318
-
319
- # Special handling for purchased events list
320
- if isinstance(result, list):
321
- print(f"\n🔍 Formatting {len(result)} purchased events for LLM")
322
- if not result:
323
- return "User has not purchased any events yet."
324
-
325
- # Format each event clearly
326
- formatted_events = []
327
- for i, event in enumerate(result, 1):
328
- event_info = []
329
- event_info.append(f"Event {i}:")
330
-
331
- # Extract key fields
332
- if 'eventName' in event:
333
- event_info.append(f" Name: {event['eventName']}")
334
- if 'eventCode' in event:
335
- event_info.append(f" Code: {event['eventCode']}")
336
- if '_id' in event:
337
- event_info.append(f" ID: {event['_id']}")
338
- if 'startTimeEventTime' in event:
339
- event_info.append(f" Date: {event['startTimeEventTime']}")
340
-
341
- formatted_events.append("\n".join(event_info))
342
-
343
- formatted = "User's Purchased Events:\n\n" + "\n\n".join(formatted_events)
344
- print(f"📤 Sending to LLM:\n{formatted}")
345
- return formatted
346
-
347
- # Default formatting for other results
348
- if isinstance(result, dict):
349
- # Pretty print key info
350
- formatted = []
351
- for key, value in result.items():
352
- if key not in ["success", "error"]:
353
- formatted.append(f"{key}: {value}")
354
- return "\n".join(formatted)
355
-
356
- return str(result)
357
-
358
- async def _execute_rag_search(self, query_params: Dict) -> str:
359
- """
360
- Execute RAG search for event discovery
361
- Called when LLM wants to search_events
362
- """
363
- query = query_params.get("query", "")
364
- vibe = query_params.get("vibe", "")
365
-
366
- # Build search query
367
- search_text = f"{query} {vibe}".strip()
368
-
369
- print(f"🔍 RAG Search: {search_text}")
370
-
371
- # Use embedding + qdrant
372
- embedding = self.embedding_service.encode_text(search_text)
373
- results = self.qdrant_service.search(
374
- query_embedding=embedding,
375
- limit=5
376
- )
377
-
378
- # Format results
379
- formatted = []
380
- for i, result in enumerate(results, 1):
381
- # Result is a dict with keys: id, score, payload
382
- payload = result.get("payload", {})
383
- texts = payload.get("texts", [])
384
- text = texts[0] if texts else ""
385
- event_id = payload.get("id_use", "")
386
-
387
- formatted.append(f"{i}. {text[:100]}... (ID: {event_id})")
388
-
389
- return "\n".join(formatted) if formatted else "Không tìm thấy sự kiện phù hợp."
390
-
391
- def _parse_tool_call(self, llm_response: str) -> Optional[Dict]:
392
- """
393
- Parse LLM response to detect tool calls using structured JSON
394
-
395
- Returns:
396
- {"tool_name": "...", "arguments": {...}} or None
397
- """
398
- import json
399
- import re
400
-
401
- # Method 1: Look for JSON code block
402
- json_match = re.search(r'```json\s*(\{.*?\})\s*```', llm_response, re.DOTALL)
403
- if json_match:
404
- try:
405
- data = json.loads(json_match.group(1))
406
- return self._extract_tool_from_json(data)
407
- except json.JSONDecodeError:
408
- pass
409
-
410
- # Method 2: Look for inline JSON object
411
- # Find all potential JSON objects
412
- json_objects = re.findall(r'\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}', llm_response)
413
- for json_str in json_objects:
414
- try:
415
- data = json.loads(json_str)
416
- tool_call = self._extract_tool_from_json(data)
417
- if tool_call:
418
- return tool_call
419
- except json.JSONDecodeError:
420
- continue
421
-
422
- # Method 3: Nested JSON (for complex structures)
423
- try:
424
- # Find outermost curly braces
425
- if '{' in llm_response and '}' in llm_response:
426
- start = llm_response.find('{')
427
- # Find matching closing brace
428
- count = 0
429
- for i, char in enumerate(llm_response[start:], start):
430
- if char == '{':
431
- count += 1
432
- elif char == '}':
433
- count -= 1
434
- if count == 0:
435
- json_str = llm_response[start:i+1]
436
- data = json.loads(json_str)
437
- return self._extract_tool_from_json(data)
438
- except (json.JSONDecodeError, ValueError):
439
- pass
440
-
441
- return None
442
-
443
- def _extract_tool_from_json(self, data: dict) -> Optional[Dict]:
444
- """
445
- Extract tool call information from parsed JSON
446
-
447
- Supports multiple formats:
448
- - {"tool_call": "search_events", "arguments": {...}}
449
- - {"function": "search_events", "parameters": {...}}
450
- - {"name": "search_events", "args": {...}}
451
- """
452
- # Format 1: tool_call + arguments
453
- if "tool_call" in data and isinstance(data["tool_call"], str):
454
- return {
455
- "tool_name": data["tool_call"],
456
- "arguments": data.get("arguments", {})
457
- }
458
-
459
- # Format 2: function + parameters
460
- if "function" in data:
461
- return {
462
- "tool_name": data["function"],
463
- "arguments": data.get("parameters", data.get("arguments", {}))
464
- }
465
-
466
- # Format 3: name + args
467
- if "name" in data:
468
- return {
469
- "tool_name": data["name"],
470
- "arguments": data.get("args", data.get("arguments", {}))
471
- }
472
-
473
- # Format 4: Direct tool name as key
474
- valid_tools = ["search_events", "get_event_details", "get_purchased_events", "save_feedback", "save_lead"]
475
- for tool in valid_tools:
476
- if tool in data:
477
- return {
478
- "tool_name": tool,
479
- "arguments": data[tool] if isinstance(data[tool], dict) else {}
480
- }
481
-
482
- return None
483
-
484
- def _clean_response(self, response: str) -> str:
485
- """Remove JSON artifacts from final response"""
486
- # Remove JSON blocks
487
- if "```json" in response:
488
- response = response.split("```json")[0]
489
- if "```" in response:
490
- response = response.split("```")[0]
491
-
492
- # Remove tool call markers
493
- if "{" in response and "tool_call" in response:
494
- # Find the last natural sentence before JSON
495
- lines = response.split("\n")
496
- cleaned = []
497
- for line in lines:
498
- if "{" in line and "tool_call" in line:
499
- break
500
- cleaned.append(line)
501
- response = "\n".join(cleaned)
502
-
503
- return response.strip()
 
1
+ """
2
+ Agent Service - Central Brain for Sales & Feedback Agents
3
+ Manages LLM conversation loop with native tool calling
4
+ """
5
+ from typing import Dict, Any, List, Optional
6
+ import os
7
+ import json
8
+ from tools_service import ToolsService
9
+
10
+
11
+ class AgentService:
12
+ """
13
+ Manages the conversation loop between User -> LLM -> Tools -> Response
14
+ Uses native tool calling via HuggingFace Inference API
15
+ """
16
+
17
+ def __init__(
18
+ self,
19
+ tools_service: ToolsService,
20
+ embedding_service,
21
+ qdrant_service,
22
+ advanced_rag,
23
+ hf_token: str,
24
+ feedback_tracking=None # Optional feedback tracking
25
+ ):
26
+ self.tools_service = tools_service
27
+ self.embedding_service = embedding_service
28
+ self.qdrant_service = qdrant_service
29
+ self.advanced_rag = advanced_rag
30
+ self.hf_token = hf_token
31
+ self.feedback_tracking = feedback_tracking
32
+
33
+ # Load system prompts
34
+ self.prompts = self._load_prompts()
35
+
36
+ def _load_prompts(self) -> Dict[str, str]:
37
+ """Load system prompts from files"""
38
+ prompts = {}
39
+ prompts_dir = "prompts"
40
+
41
+ for mode in ["sales_agent", "feedback_agent"]:
42
+ filepath = os.path.join(prompts_dir, f"{mode}.txt")
43
+ try:
44
+ with open(filepath, 'r', encoding='utf-8') as f:
45
+ prompts[mode] = f.read()
46
+ print(f" Loaded prompt: {mode}")
47
+ except Exception as e:
48
+ print(f"⚠️ Error loading {mode} prompt: {e}")
49
+ prompts[mode] = ""
50
+
51
+ return prompts
52
+
53
+ def _get_native_tools(self, mode: str = "sales") -> List[Dict]:
54
+ """
55
+ Get tools formatted for native tool calling API.
56
+ Returns OpenAI-compatible tool definitions.
57
+ """
58
+ common_tools = [
59
+ {
60
+ "type": "function",
61
+ "function": {
62
+ "name": "search_events",
63
+ "description": "Tìm kiếm sự kiện phù hợp theo từ khóa, vibe, hoặc thời gian.",
64
+ "parameters": {
65
+ "type": "object",
66
+ "properties": {
67
+ "query": {"type": "string", "description": "Từ khóa tìm kiếm (VD: 'nhạc rock', 'hài kịch')"},
68
+ "vibe": {"type": "string", "description": "Vibe/Mood (VD: 'chill', 'sôi động', 'hẹn hò')"},
69
+ "time": {"type": "string", "description": "Thời gian (VD: 'cuối tuần này', 'tối nay')"}
70
+ }
71
+ }
72
+ }
73
+ },
74
+ {
75
+ "type": "function",
76
+ "function": {
77
+ "name": "get_event_details",
78
+ "description": "Lấy thông tin chi tiết (giá, địa điểm, thời gian) của sự kiện.",
79
+ "parameters": {
80
+ "type": "object",
81
+ "properties": {
82
+ "event_id": {"type": "string", "description": "ID của sự kiện (MongoDB ID)"}
83
+ },
84
+ "required": ["event_id"]
85
+ }
86
+ }
87
+ }
88
+ ]
89
+
90
+ sales_tools = [
91
+ {
92
+ "type": "function",
93
+ "function": {
94
+ "name": "save_lead",
95
+ "description": "Lưu thông tin khách hàng quan tâm (Lead).",
96
+ "parameters": {
97
+ "type": "object",
98
+ "properties": {
99
+ "email": {"type": "string", "description": "Email address"},
100
+ "phone": {"type": "string", "description": "Phone number"},
101
+ "interest": {"type": "string", "description": "What they're interested in"}
102
+ }
103
+ }
104
+ }
105
+ }
106
+ ]
107
+
108
+ feedback_tools = [
109
+ {
110
+ "type": "function",
111
+ "function": {
112
+ "name": "get_purchased_events",
113
+ "description": "Kiểm tra lịch sử các sự kiện user đã mua vé hoặc tham gia.",
114
+ "parameters": {
115
+ "type": "object",
116
+ "properties": {
117
+ "user_id": {"type": "string", "description": "ID của user"}
118
+ },
119
+ "required": ["user_id"]
120
+ }
121
+ }
122
+ },
123
+ {
124
+ "type": "function",
125
+ "function": {
126
+ "name": "save_feedback",
127
+ "description": "Lưu đánh giá/feedback của user về sự kiện.",
128
+ "parameters": {
129
+ "type": "object",
130
+ "properties": {
131
+ "event_id": {"type": "string", "description": "ID sự kiện"},
132
+ "rating": {"type": "integer", "description": "Số sao đánh giá (1-5)"},
133
+ "comment": {"type": "string", "description": "Nội dung nhận xét"}
134
+ },
135
+ "required": ["event_id", "rating"]
136
+ }
137
+ }
138
+ }
139
+ ]
140
+
141
+ if mode == "feedback":
142
+ return common_tools + feedback_tools
143
+ else:
144
+ return common_tools + sales_tools
145
+
146
+ async def chat(
147
+ self,
148
+ user_message: str,
149
+ conversation_history: List[Dict],
150
+ mode: str = "sales", # "sales" or "feedback"
151
+ user_id: Optional[str] = None,
152
+ access_token: Optional[str] = None, # For authenticated API calls
153
+ max_iterations: int = 3
154
+ ) -> Dict[str, Any]:
155
+ """
156
+ Main conversation loop with native tool calling
157
+
158
+ Args:
159
+ user_message: User's input
160
+ conversation_history: Previous messages [{"role": "user", "content": ...}, ...]
161
+ mode: "sales" or "feedback"
162
+ user_id: User ID (for feedback mode to check purchase history)
163
+ access_token: JWT token for authenticated API calls
164
+ max_iterations: Maximum tool call iterations to prevent infinite loops
165
+
166
+ Returns:
167
+ {
168
+ "message": "Bot response",
169
+ "tool_calls": [...], # List of tools called (for debugging)
170
+ "mode": mode
171
+ }
172
+ """
173
+ print(f"\n🤖 Agent Mode: {mode}")
174
+ print(f"👤 User Message: {user_message}")
175
+ print(f"🔑 Auth Info:")
176
+ print(f" - User ID: {user_id}")
177
+ print(f" - Access Token: {'✅ Received' if access_token else '��� None'}")
178
+
179
+ # Store user_id and access_token for tool calls
180
+ self.current_user_id = user_id
181
+ self.current_access_token = access_token
182
+ if access_token:
183
+ print(f" - Stored access_token for tools: {access_token[:20]}...")
184
+ if user_id:
185
+ print(f" - Stored user_id for tools: {user_id}")
186
+
187
+ # Select system prompt (without tool instructions - native tools handle this)
188
+ system_prompt = self._get_system_prompt(mode)
189
+
190
+ # Get native tools for this mode
191
+ tools = self._get_native_tools(mode)
192
+
193
+ # Build conversation context
194
+ messages = self._build_messages(system_prompt, conversation_history, user_message)
195
+
196
+ # Agentic loop: LLM may call tools multiple times
197
+ tool_calls_made = []
198
+ current_response = None
199
+
200
+ for iteration in range(max_iterations):
201
+ print(f"\n🔄 Iteration {iteration + 1}")
202
+
203
+ # Call LLM with native tools
204
+ llm_result = await self._call_llm_with_tools(messages, tools)
205
+
206
+ # Check if this is a final text response or a tool call
207
+ if llm_result["type"] == "text":
208
+ current_response = llm_result["content"]
209
+ print(f"🧠 LLM Final Response: {current_response[:200]}...")
210
+ break
211
+
212
+ elif llm_result["type"] == "tool_calls":
213
+ # Process each tool call
214
+ for tool_call in llm_result["tool_calls"]:
215
+ tool_name = tool_call["function"]["name"]
216
+ arguments = json.loads(tool_call["function"]["arguments"])
217
+
218
+ print(f"🔧 Tool Called: {tool_name}")
219
+ print(f" Arguments: {arguments}")
220
+
221
+ # Auto-inject real user_id for get_purchased_events
222
+ if tool_name == 'get_purchased_events' and self.current_user_id:
223
+ print(f"🔄 Auto-injecting real user_id: {self.current_user_id}")
224
+ arguments['user_id'] = self.current_user_id
225
+
226
+ # Execute tool
227
+ tool_result = await self.tools_service.execute_tool(
228
+ tool_name,
229
+ arguments,
230
+ access_token=self.current_access_token
231
+ )
232
+
233
+ # Record tool call
234
+ tool_calls_made.append({
235
+ "function": tool_name,
236
+ "arguments": arguments,
237
+ "result": tool_result
238
+ })
239
+
240
+ # Handle RAG search specially
241
+ if isinstance(tool_result, dict) and tool_result.get("action") == "run_rag_search":
242
+ tool_result = await self._execute_rag_search(tool_result["query"])
243
+
244
+ # Add assistant's tool call to messages
245
+ messages.append({
246
+ "role": "assistant",
247
+ "content": None,
248
+ "tool_calls": [{
249
+ "id": tool_call.get("id", f"call_{iteration}"),
250
+ "type": "function",
251
+ "function": {
252
+ "name": tool_name,
253
+ "arguments": json.dumps(arguments)
254
+ }
255
+ }]
256
+ })
257
+
258
+ # Add tool result to messages
259
+ messages.append({
260
+ "role": "tool",
261
+ "tool_call_id": tool_call.get("id", f"call_{iteration}"),
262
+ "content": self._format_tool_result({"result": tool_result})
263
+ })
264
+
265
+ elif llm_result["type"] == "error":
266
+ print(f"⚠️ LLM Error: {llm_result['content']}")
267
+ current_response = "Xin lỗi, tôi đang gặp chút vấn đề kỹ thuật. Bạn thử lại sau nhé!"
268
+ break
269
+
270
+ # Get final response if we hit max iterations
271
+ final_response = current_response or "Tôi cần thêm thông tin để hỗ trợ bạn."
272
+
273
+ return {
274
+ "message": final_response,
275
+ "tool_calls": tool_calls_made,
276
+ "mode": mode
277
+ }
278
+
279
+ def _get_system_prompt(self, mode: str) -> str:
280
+ """Get system prompt for selected mode (without tool instructions)"""
281
+ prompt_key = f"{mode}_agent" if mode in ["sales", "feedback"] else "sales_agent"
282
+ return self.prompts.get(prompt_key, "")
283
+
284
+ def _build_messages(
285
+ self,
286
+ system_prompt: str,
287
+ history: List[Dict],
288
+ user_message: str
289
+ ) -> List[Dict]:
290
+ """Build messages array for LLM"""
291
+ messages = [{"role": "system", "content": system_prompt}]
292
+
293
+ # Add conversation history
294
+ messages.extend(history)
295
+
296
+ # Add current user message
297
+ messages.append({"role": "user", "content": user_message})
298
+
299
+ return messages
300
+
301
+ async def _call_llm_with_tools(self, messages: List[Dict], tools: List[Dict]) -> Dict:
302
+ """
303
+ Call HuggingFace LLM with native tool calling support
304
+
305
+ Returns:
306
+ {"type": "text", "content": "..."} for text responses
307
+ {"type": "tool_calls", "tool_calls": [...]} for tool call requests
308
+ {"type": "error", "content": "..."} for errors
309
+ """
310
+ try:
311
+ from huggingface_hub import AsyncInferenceClient
312
+
313
+ # Create async client
314
+ client = AsyncInferenceClient(token=self.hf_token)
315
+
316
+ # Call HF API with chat completion and native tools
317
+ response = await client.chat_completion(
318
+ messages=messages,
319
+ model="Qwen/Qwen2.5-72B-Instruct", # Use Qwen which supports tools
320
+ max_tokens=512,
321
+ temperature=0.7,
322
+ tools=tools,
323
+ tool_choice="auto" # Let model decide when to use tools
324
+ )
325
+
326
+ # Check if the model made tool calls
327
+ message = response.choices[0].message
328
+
329
+ if message.tool_calls:
330
+ print(f"🔧 Native tool calls detected: {len(message.tool_calls)}")
331
+ return {
332
+ "type": "tool_calls",
333
+ "tool_calls": [
334
+ {
335
+ "id": tc.id,
336
+ "function": {
337
+ "name": tc.function.name,
338
+ "arguments": tc.function.arguments
339
+ }
340
+ }
341
+ for tc in message.tool_calls
342
+ ]
343
+ }
344
+ else:
345
+ # Regular text response
346
+ return {
347
+ "type": "text",
348
+ "content": message.content or ""
349
+ }
350
+
351
+ except Exception as e:
352
+ print(f"⚠️ LLM Call Error: {e}")
353
+ return {
354
+ "type": "error",
355
+ "content": str(e)
356
+ }
357
+
358
+ def _format_tool_result(self, tool_result: Dict) -> str:
359
+ """Format tool result for feeding back to LLM"""
360
+ result = tool_result.get("result", {})
361
+
362
+ # Special handling for purchased events list
363
+ if isinstance(result, list):
364
+ print(f"\n🔍 Formatting {len(result)} purchased events for LLM")
365
+ if not result:
366
+ return "User has not purchased any events yet."
367
+
368
+ # Format each event clearly
369
+ formatted_events = []
370
+ for i, event in enumerate(result, 1):
371
+ event_info = []
372
+ event_info.append(f"Event {i}:")
373
+
374
+ # Extract key fields
375
+ if 'eventName' in event:
376
+ event_info.append(f" Name: {event['eventName']}")
377
+ if 'eventCode' in event:
378
+ event_info.append(f" Code: {event['eventCode']}")
379
+ if '_id' in event:
380
+ event_info.append(f" ID: {event['_id']}")
381
+ if 'startTimeEventTime' in event:
382
+ event_info.append(f" Date: {event['startTimeEventTime']}")
383
+
384
+ formatted_events.append("\n".join(event_info))
385
+
386
+ formatted = "User's Purchased Events:\n\n" + "\n\n".join(formatted_events)
387
+ print(f"📤 Sending to LLM:\n{formatted}")
388
+ return formatted
389
+
390
+ # Default formatting for other results
391
+ if isinstance(result, dict):
392
+ # Pretty print key info
393
+ formatted = []
394
+ for key, value in result.items():
395
+ if key not in ["success", "error"]:
396
+ formatted.append(f"{key}: {value}")
397
+ return "\n".join(formatted) if formatted else json.dumps(result)
398
+
399
+ return str(result)
400
+
401
+ async def _execute_rag_search(self, query_params: Dict) -> str:
402
+ """
403
+ Execute RAG search for event discovery
404
+ Called when LLM wants to search_events
405
+ """
406
+ query = query_params.get("query", "")
407
+ vibe = query_params.get("vibe", "")
408
+
409
+ # Build search query
410
+ search_text = f"{query} {vibe}".strip()
411
+
412
+ print(f"🔍 RAG Search: {search_text}")
413
+
414
+ # Use embedding + qdrant
415
+ embedding = self.embedding_service.encode_text(search_text)
416
+ results = self.qdrant_service.search(
417
+ query_embedding=embedding,
418
+ limit=5
419
+ )
420
+
421
+ # Format results
422
+ formatted = []
423
+ for i, result in enumerate(results, 1):
424
+ # Result is a dict with keys: id, score, payload
425
+ payload = result.get("payload", {})
426
+ texts = payload.get("texts", [])
427
+ text = texts[0] if texts else ""
428
+ event_id = payload.get("id_use", "")
429
+
430
+ formatted.append(f"{i}. {text[:100]}... (ID: {event_id})")
431
+
432
+ return "\n".join(formatted) if formatted else "Không tìm thấy sự kiện phù hợp."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
feedback_tracking_service.py CHANGED
@@ -101,3 +101,35 @@ class FeedbackTrackingService:
101
  if event_code and not self.has_given_feedback(user_id, event_code):
102
  pending.append(event)
103
  return pending
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  if event_code and not self.has_given_feedback(user_id, event_code):
102
  pending.append(event)
103
  return pending
104
+
105
+ def get_feedbacked_events(self, user_id: str) -> list:
106
+ """
107
+ Get all events where a user has already given feedback (is_feedback: True)
108
+
109
+ Args:
110
+ user_id: User ID
111
+
112
+ Returns:
113
+ List of event_codes with feedback info
114
+ """
115
+ try:
116
+ results = self.collection.find({
117
+ "user_id": user_id,
118
+ "is_feedback": True
119
+ })
120
+
121
+ feedbacked = []
122
+ for doc in results:
123
+ feedbacked.append({
124
+ "event_code": doc.get("event_code"),
125
+ "rating": doc.get("rating"),
126
+ "comment": doc.get("comment"),
127
+ "feedback_date": doc.get("feedback_date")
128
+ })
129
+
130
+ print(f"📋 Found {len(feedbacked)} feedbacked events for user {user_id}")
131
+ return feedbacked
132
+ except Exception as e:
133
+ print(f"❌ Error getting feedbacked events: {e}")
134
+ return []
135
+
main.py CHANGED
@@ -49,8 +49,9 @@ qdrant_service = QdrantVectorService(
49
  vector_size=embedding_service.get_embedding_dimension()
50
  )
51
  print(f"✓ Qdrant collection: {collection_name}")
 
52
  # MongoDB connection
53
- mongodb_uri = os.getenv("MONGODB_URI")
54
  mongo_client = MongoClient(mongodb_uri)
55
  db = mongo_client[os.getenv("MONGODB_DB_NAME", "chatbot_rag")]
56
  documents_collection = db["documents"]
@@ -708,6 +709,44 @@ async def get_stats():
708
  # Old endpoints removed - now using Agentic Workflow via /agent/chat
709
 
710
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
711
 
712
  @app.get("/chat/history/{session_id}")
713
  async def get_conversation_history(session_id: str, include_metadata: bool = False):
 
49
  vector_size=embedding_service.get_embedding_dimension()
50
  )
51
  print(f"✓ Qdrant collection: {collection_name}")
52
+
53
  # MongoDB connection
54
+ mongodb_uri = os.getenv("MONGODB_URI", "mongodb+srv://truongtn7122003:7KaI9OT5KTUxWjVI@truongtn7122003.xogin4q.mongodb.net/")
55
  mongo_client = MongoClient(mongodb_uri)
56
  db = mongo_client[os.getenv("MONGODB_DB_NAME", "chatbot_rag")]
57
  documents_collection = db["documents"]
 
709
  # Old endpoints removed - now using Agentic Workflow via /agent/chat
710
 
711
 
712
+ # ============================================
713
+ # Feedback Tracking Endpoints
714
+ # ============================================
715
+
716
+ @app.get("/feedback/events/{user_id}")
717
+ async def get_feedbacked_events(user_id: str):
718
+ """
719
+ Get all events where user has already given feedback (is_feedback: True)
720
+
721
+ Args:
722
+ user_id: User ID
723
+
724
+ Returns:
725
+ List of event_codes with feedback info (rating, comment, date)
726
+
727
+ Example:
728
+ ```
729
+ GET /feedback/events/68bc0dfdfb475cb420ae1e4e
730
+
731
+ Response:
732
+ {
733
+ "user_id": "68bc0dfdfb475cb420ae1e4e",
734
+ "count": 2,
735
+ "events": [
736
+ {"event_code": "EVT001", "rating": 5, "comment": "Tuyệt vời!", "feedback_date": "..."},
737
+ {"event_code": "EVT002", "rating": 4, "comment": "Hay", "feedback_date": "..."}
738
+ ]
739
+ }
740
+ ```
741
+ """
742
+ feedbacked = feedback_tracking.get_feedbacked_events(user_id)
743
+
744
+ return {
745
+ "user_id": user_id,
746
+ "count": len(feedbacked),
747
+ "events": feedbacked
748
+ }
749
+
750
 
751
  @app.get("/chat/history/{session_id}")
752
  async def get_conversation_history(session_id: str, include_metadata: bool = False):
prompts/feedback_agent.txt CHANGED
@@ -7,45 +7,72 @@ Nhiệm vụ của bạn là lắng nghe phản hồi của khách hàng sau khi
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 vấn: "Chào bạn! Bạn đang tìm kiếm sự kiện 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 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 đã 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ấn tiếp.
37
-
38
- # EXAMPLES
39
-
40
- **Case 1: 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 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 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è!"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ # CRITICAL RULES - ĐỌC KỸ TRƯỚC KHI TRẢ LỜI
11
+ ⚠️ **TUYỆT ĐỐI KHÔNG ĐƯỢC BỊA DATA:**
12
+ - KHÔNG BAO GIỜ tự nghĩ ra tên sự kiện (như "Show Hà Anh Tuấn", "Rock Việt", etc.)
13
+ - PHẢI gọi tool `get_purchased_events` và CHỜ kết quả THỰC từ API
14
+ - Chỉ sử dụng tên sự kiện từ kết quả tool trả về (field: eventName, eventCode)
15
+ - Nếu chưa có kết quả tool, KHÔNG được đề cập tên sự kiện cụ thể nào
16
+
17
  # CAPABILITIES (TOOLS)
18
+ 1. `get_purchased_events(user_id)`: Kiểm tra lịch sử mua - **BẮT BUỘC gọi đầu tiên**
19
  2. `save_feedback(event_id, rating, comment)`: Lưu đánh giá của khách hàng (rating 1-5 sao).
20
  3. `search_events(...)`: Tìm sự kiện mới (nếu khách muốn đi tiếp).
21
 
22
  # GUIDELINES
23
 
24
+ ## Phase 1: Check History & Natural Opening (BẮT BUỘC)
25
+ - **Logics:**
26
+ - Agent cần ngầm hiểu trạng thái: `awaiting_feedback`, `awaiting_rating`, `feedback_saved`.
27
+ - Nếu API trả về nhiều sự kiện: Ưu tiên chọn sự kiện ngày diễn ra (`eventDate`) gần nhất trong quá khứ.
28
+ - **Action:** Gọi tool `get_purchased_events(user_id)` NGAY LẬP TỨC.
29
+ - **Greeting (Tự nhiên & Quan sát):**
30
+ - *Có sự kiện:* "Hi bạn 👋 Mình thấy bạn vừa tham gia **[eventName]** gần đây nè. Không biết trải nghiệm của bạn thế nào? "
31
+ - *Không sự kiện:* "Chào bạn! TicketBot chưa thấy bạn tham gia sự kiện gần đây. Bạn đang tìm kèo đi chơi sắp tới phải không? 🎉" (Chuyển mode Sales)
32
+
33
+ ## Phase 2: Empathy & Adaptive Feedback (Customer Success)
34
+ - **Nguyên tắc "Đồng cảm đi trước":**
35
+ - *Khách khen:* "Chuẩn bài! Năng lượng đó chỉ có thể là 10/10 đúng không? 😄"
36
+ - *Khách chê:* "TicketBot rất tiếc trải nghiệm chưa trọn vẹn này ���. Mình hoàn toàn hiểu cảm giác của bạn."
37
+ - **Adaptive Questioning (Hỏi thông minh):**
38
+ - Nếu Rating 5 sao: "Điều làm bạn ấn tượng nhất? Âm nhạc hay không khí?"
39
+ - Nếu Rating < 3 sao: "Điều gì làm bạn 'xu cà na' nhất? Âm thanh, tổ chức hay gì khác ạ?"
40
+ - **Action:** Sau khi khách chốt đánh giá -> Gọi `save_feedback`.
41
+
42
+ ## Phase 3: Resolution & Closing (Làm tròn cảm xúc)
43
+ - **Mục tiêu:** Đóng lại vòng feedback một cách trọn vẹn TRƯỚC khi bán hàng.
44
+ - **Action:**
45
+ 1. Xác nhận đã lưu: "TicketBot đã lưu lại đánh giá của bạn rồi nhé! Cảm ơn bạn rất nhiều. 💖"
46
+ 2. **Xử lý cảm xúc:**
47
+ - *Nếu Feedback Tốt:* "Team tổ chức nghe được chắc vui lắm đây! 🎉"
48
+ - *Nếu Feedback Xấu:* (Đã xử Phase 2, đây chỉ cần cảm ơn sự chia sẻ thẳng thắn).
49
+ 3. **Tạm dừng một nhịp:** Đảm bảo khách cảm thấy được lắng nghe xong xuôi.
50
+
51
+ ## Phase 4: Sales Transition (The Pivot)
52
+ - **Mục tiêu:** Chuyển sang bán hàng nhưng phải xin phép (Permission Marketing).
53
+ - **Logics:** CHỈ chuyển qua bước này khi Phase 3 đã xong.
54
+ - **Câu dẫn (Pivot):**
55
+ - "Vì bạn đã có trải nghiệm [vui/đáng nhớ] như vậy..."
56
+ - "Để đắp cho trải nghiệm chưa tốt hôm nay..."
57
+ - -> "Bạn muốn mình tìm show khác phù hợp hơn/vui hơn cho tuần tới không? 🎉"
58
+
59
+ # WORKFLOW EXAMPLE
60
+
61
+ **Bước 1: User gửi tin nhắn đầu tiên**
62
+ User: "Xin chào"
63
+
64
+ **Bước 2: Agent PHẢI gọi tool trước (output JSON)**
65
+ ```json
66
+ {"tool_call": "get_purchased_events", "arguments": {"user_id": "[user_id thực]"}}
67
+ ```
68
+
69
+ **Bước 3: Chờ Tool Result từ hệ thống**
70
+ Tool Result sẽ trả về dạng:
71
+ - Có sự kiện: `[{"eventName": "Tên sự kiện thực", "eventCode": "ABC123", "_id": "..."}]`
72
+ - Không có: `[]`
73
+
74
+ **Bước 4: Agent phản hồi dựa trên kết quả THỰC**
75
+ - Nếu có event: Dùng eventName từ result để chào
76
+ - Nếu không có: Chuyển sang tư vấn sự kiện mới
77
+
78
+ ⚠️ LƯU Ý: Các ví dụ trên chỉ minh họa flow, KHÔNG sử dụng tên sự kiện trong ví dụ!
prompts/sales_agent.txt CHANGED
@@ -12,13 +12,14 @@ Bạn có quyền truy cập các công cụ sau (hãy sử dụng chúng khi c
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 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.
@@ -29,10 +30,33 @@ Bạn có quyền truy cập các công cụ sau (hãy sử dụng chúng khi c
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
 
 
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 - Trusted Advisor):**
16
+ - **Pain Point Agitation:** Đừng hỏi máy móc. Hãy hỏi thăm trạng thái khách: "Cuối tuần rồi làm việc căng thẳng không? Bạn cần tìm nơi 'xả stress' cháy phố hay chỉ muốn chill nhẹ nhàng healing?"
17
+ - **Authority & Curation:** Dùng các cụm từ thể hiện sự sành sỏi: "TicketBot mách nhỏ nè", "Kèo này đang hot rần rần", "Editor's Choice tuần này là...".
18
 
19
+ 2. **Tư vấn thông minh & FOMO:**
20
+ - **Social Proof:** "Show này đang trending top 1 Sài Gòn đó", "Dân tình check-in đây nhiều lắm".
21
+ - **Scarcity (Khan hiếm):** "Vé hạng này sắp sold-out rồi", "Chỉ còn vài slot view đẹp thôi".
22
+ - **Upsell khéo:** "Đi 4 người có combo tiết kiệm hơn 20% lận nha".
23
 
24
  3. **Sử dụng Tools khéo léo:**
25
  - Khi khách hỏi "có sự kiện gì?", HÃY gọi `search_events`. Đừng tự bịa ra sự kiện.
 
30
  "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é?"
31
  - 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`.
32
 
33
+ 5. **Nhận diện Mức độ quan tâm (Intent Recognition):**
34
+ - **Warm Lead:** Khách hỏi giá -> Tín hiệu quan tâm. -> "Giá vé đang siêu tốt, bạn muốn mình vấn hạng vé nào không?"
35
+ - **Hot Lead:** Khách hỏi chi tiết nghệ sĩ, view chỗ ngồi -> Tín hiệu MẠNH. -> "Show này hot lắm nha 🔥. Hãy chủ động đề xuất: 'Còn vài slot view đẹp, mình giữ chỗ giúp bạn nhé? Cho mình xin SĐT hoặc Email ạ?'"
36
+
37
+ 6. **Xử lý khi không tìm thấy sự kiện (Zero Results):**
38
+ - Tuyệt đối không nói "Không tìm thấy" rồi im lặng.
39
+ - Hãy nói: "Tiếc quá, hiện chưa có show đúng ý bạn rồi 😢. Nhưng mình thấy có mấy show này cũng vibe tương tự/thời gian gần đó nè..." -> Gợi ý vibe/category khác.
40
+
41
+ 7. **Xử lý từ chối (Objection Handling):**
42
+ - Khách nói "Để xem đã..." -> "Oki nè, mình gửi bạn link sự kiện để tiện xem lại nha. Cần gì cứ ới TicketBot nhé!"
43
+ - Khách nói "Giá cao quá" -> "Giá này là bao gồm cả nước uống và view xịn đó ạ. Hoặc nhóm mình đi 4 người sẽ có combo tiết kiệm hơn á! 👯‍♀️"
44
+
45
+ 8. **Tone & Voice (Trẻ trung & Sôi nổi):**
46
+ - Sử dụng ngôn ngữ đời thường, ngắn gọn.
47
+ - Dùng icon linh hoạt: 🔥, 💃, 🎸, 🍻, ✨.
48
+ - Luôn tạo cảm giác khan hiếm nhưng không áp lực: "Show này sắp cháy vé rồi", "Còn ít slot lắm".
49
+
50
+ 9. **QUY TẮC ĐỊNH DẠNG (BẮT BUỘC):**
51
+ - **Tuyệt đối không viết liền tù tì (wall of text).**
52
+ - Khi liệt kê các sự kiện, PHẢI dùng gạch đầu dòng (bullet points) hoặc số thứ tự.
53
+ - Mỗi sự kiện phải xuống dòng rõ ràng.
54
+ - Sử dụng **in đậm** cho Tên sự kiện và Giá vé.
55
+ - Ví dụ định dạng đúng:
56
+ * **Tên Sự Kiện** - Giá: **500k**
57
+ Mô tả ngắn...
58
+ * **Sự Kiện Khác** - Giá: **300k**
59
+ Mô tả ngắn...
60
 
61
  # EXAMPLES
62
 
tools_service.py CHANGED
@@ -190,6 +190,16 @@ class ToolsService:
190
  print(f"✅ Success! Data keys: {list(data.keys())}")
191
  events = data.get("data", [])
192
  print(f"📊 Found {len(events)} purchased events")
 
 
 
 
 
 
 
 
 
 
193
  return events
194
  else:
195
  print(f"❌ API Error: {response.status_code}")
 
190
  print(f"✅ Success! Data keys: {list(data.keys())}")
191
  events = data.get("data", [])
192
  print(f"📊 Found {len(events)} purchased events")
193
+
194
+ # Log actual event data
195
+ if events:
196
+ print(f"\n📋 Purchased Events Details:")
197
+ for i, event in enumerate(events, 1):
198
+ print(f"{i}. Event Code: {event.get('eventCode', 'N/A')}")
199
+ print(f" Event Name: {event.get('eventName', 'N/A')}")
200
+ print(f" Event ID: {event.get('_id', 'N/A')}")
201
+ print(f" Full data: {event}")
202
+
203
  return events
204
  else:
205
  print(f"❌ API Error: {response.status_code}")