minhvtt commited on
Commit
5f63f29
·
verified ·
1 Parent(s): 011fe00

Upload 3 files

Browse files
Files changed (2) hide show
  1. event_recommendation.py +312 -0
  2. event_service.py +120 -0
event_recommendation.py ADDED
@@ -0,0 +1,312 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Event Recommendation Scenario Handler
3
+ Recommends events based on user's vibe/mood with RAG integration
4
+ """
5
+ from typing import Dict, Any
6
+ from .base_handler import BaseScenarioHandler
7
+
8
+
9
+ class EventRecommendationHandler(BaseScenarioHandler):
10
+ """
11
+ Handle event recommendation flow
12
+
13
+ Steps:
14
+ 1. Ask for vibe/mood (Chill, Sôi động, Hài, Workshop)
15
+ 2. Search events matching vibe → RAG
16
+ 3. Show event list, ask which to see details
17
+ 4. Ask what info needed (price, lineup, location, time)
18
+ 5-8. Show specific info → RAG
19
+ 9. Ask if want to save event to email
20
+ 10. Collect email + send summary
21
+ 11-12. End scenario
22
+ """
23
+
24
+ def start(self, initial_data: Dict = None) -> Dict[str, Any]:
25
+ """Start event recommendation flow"""
26
+ return {
27
+ "message": "Hello! 👋 Bạn muốn tìm sự kiện theo vibe gì nè? Chill – Sôi động – Hài – Workshop?",
28
+ "new_state": {
29
+ "active_scenario": "event_recommendation",
30
+ "scenario_step": 1,
31
+ "scenario_data": initial_data or {}
32
+ }
33
+ }
34
+
35
+ def next_step(self, current_step: int, user_input: str, scenario_data: Dict) -> Dict[str, Any]:
36
+ """Process user input and advance scenario"""
37
+
38
+ # Get expected input type for this step
39
+ expected_type = self._get_expected_type(current_step)
40
+
41
+ # Check for unexpected input (off-topic questions)
42
+ unexpected = self.handle_unexpected_input(user_input, expected_type, current_step)
43
+ if unexpected:
44
+ return unexpected
45
+
46
+ # ===== STEP 1: Collect interest tag =====
47
+ if current_step == 1:
48
+ scenario_data['interest_tag'] = user_input
49
+
50
+ return {
51
+ "message": f"Mình hiểu rồi! Để mình tìm sự kiện hợp vibe **{user_input}** nha",
52
+ "new_state": {
53
+ "active_scenario": "event_recommendation",
54
+ "scenario_step": 2,
55
+ "scenario_data": scenario_data
56
+ },
57
+ "scenario_active": True
58
+ }
59
+
60
+ # ===== STEP 2: Execute RAG search for events =====
61
+ "new_state": {
62
+ "active_scenario": "event_recommendation",
63
+ "scenario_step": 3,
64
+ "scenario_data": scenario_data
65
+ },
66
+ "scenario_active": True,
67
+ "loading_message": "⏳ Bạn đợi tôi tìm 1 chút nhé..."
68
+ }
69
+
70
+ # ===== STEP 3: User picks event =====
71
+ elif current_step == 3:
72
+ scenario_data['event_name'] = user_input
73
+
74
+ return {
75
+ "message": "Bạn cần xem: giá – line-up – địa điểm – hay thời gian của sự kiện?",
76
+ "new_state": {
77
+ "active_scenario": "event_recommendation",
78
+ "scenario_step": 4,
79
+ "scenario_data": scenario_data
80
+ },
81
+ "scenario_active": True
82
+ }
83
+
84
+ # ===== STEP 4: Branch based on info choice =====
85
+ elif current_step == 4:
86
+ choice = self._detect_choice(user_input)
87
+ event_name = scenario_data.get('event_name', 'sự kiện này')
88
+
89
+ # Build RAG query based on choice
90
+ query_map = {
91
+ 'price': f"giá vé {event_name}",
92
+ 'lineup': f"lineup nghệ sĩ {event_name}",
93
+ 'location': f"địa điểm tổ chức {event_name}",
94
+ 'time': f"thời gian lịch diễn {event_name}"
95
+ }
96
+
97
+ query = query_map.get(choice, query_map['price'])
98
+ print(f"🔍 RAG Search: {query}")
99
+
100
+ results = self._search_rag(query)
101
+ formatted_info = self._format_rag_results(results)
102
+
103
+ # Build response message
104
+ message_map = {
105
+ 'price': f"Giá vé event {event_name} nè:\n{formatted_info}",
106
+ 'lineup': f"Lineup / nghệ sĩ của event {event_name} là:\n{formatted_info}",
107
+ 'location': f"Địa điểm tổ chức event {event_name}:\n{formatted_info}",
108
+ 'time': f"Thời gian / lịch diễn của event {event_name}:\n{formatted_info}"
109
+ }
110
+
111
+ return {
112
+ "message": message_map.get(choice, message_map['price']),
113
+ "new_state": {
114
+ "active_scenario": "event_recommendation",
115
+ "scenario_step": 9, # Skip to email step
116
+ "scenario_data": scenario_data
117
+ },
118
+ "scenario_active": True,
119
+ "loading_message": "⏳ Bạn đợi tôi tìm 1 chút nhé..."
120
+ }
121
+
122
+ # ===== STEP 9: Ask if want to save event to email =====
123
+ elif current_step == 9:
124
+ choice = self._detect_yes_no(user_input)
125
+
126
+ if choice == 'yes':
127
+ return {
128
+ "message": "Cho mình xin email để gửi bản tóm tắt event kèm link mua vé?",
129
+ "new_state": {
130
+ "active_scenario": "event_recommendation",
131
+ "scenario_step": 10,
132
+ "scenario_data": scenario_data
133
+ },
134
+ "scenario_active": True
135
+ }
136
+ else:
137
+ return {
138
+ "message": "Okie, bạn cần event theo vibe khác không nè? 😄",
139
+ "new_state": None,
140
+ "scenario_active": False,
141
+ "end_scenario": True
142
+ }
143
+
144
+ # ===== STEP 10: Collect email and send summary =====
145
+ elif current_step == 10:
146
+ email = user_input.strip()
147
+
148
+ if not self._validate_email(email):
149
+ return {
150
+ "message": "Email này có vẻ không đúng định dạng. Bạn nhập lại giúp mình nhé? (Ví dụ: name@example.com)",
151
+ "new_state": None, # Stay at same step
152
+ "scenario_active": True
153
+ }
154
+
155
+ # Save lead
156
+ scenario_data['email'] = email
157
+
158
+ try:
159
+ self.lead_storage.save_lead(
160
+ event_name=scenario_data.get('event_name', 'Unknown Event'),
161
+ email=email,
162
+ interests={
163
+ "vibe": scenario_data.get('interest_tag'),
164
+ "wants_event_summary": True
165
+ },
166
+ session_id=scenario_data.get('session_id')
167
+ )
168
+ print(f"📧 Lead saved: {email}")
169
+ except Exception as e:
170
+ print(f"⚠️ Error saving lead: {e}")
171
+
172
+ return {
173
+ "message": "Đã gửi email cho bạn nha! ✨",
174
+ "new_state": None,
175
+ "scenario_active": False,
176
+ "end_scenario": True,
177
+ "action": "send_event_summary_email"
178
+ }
179
+
180
+ # Fallback - unknown step
181
+ return {
182
+ "message": "Xin lỗi, có lỗi xảy ra. Bạn muốn bắt đầu lại không?",
183
+ "new_state": None,
184
+ "scenario_active": False,
185
+ "end_scenario": True
186
+ }
187
+
188
+ def _get_expected_type(self, step: int) -> str:
189
+ """Get expected input type for each step"""
190
+ type_map = {
191
+ 1: 'interest_tag',
192
+ 2: None, # Auto-advance after RAG
193
+ 3: 'event_name',
194
+ 4: 'choice',
195
+ 9: 'choice',
196
+ 10: 'email'
197
+ }
198
+ return type_map.get(step, 'text')
199
+
200
+ def _format_event_list(self, results: list) -> str:
201
+ """Format event search results as numbered list"""
202
+ print(f"🔍 DEBUG: RAG returned {len(results)} results")
203
+
204
+ if not results or len(results) == 0:
205
+ return "Hiện tại chưa có event phù hợp 😢\nBạn thử vibe khác nhé!"
206
+
207
+ # Debug: Print first result to see structure
208
+ if len(results) > 0:
209
+ print(f"🔍 DEBUG: First result metadata: {results[0].get('metadata', {})}")
210
+
211
+ events = []
212
+ for i, r in enumerate(results[:3], 1):
213
+ metadata = r.get('metadata', {})
214
+
215
+ # Extract event info from metadata
216
+ # Your Qdrant has: {'texts': [...], 'id_use': '...'}
217
+ event_id = metadata.get('id_use', metadata.get('original_id'))
218
+ texts = metadata.get('texts', [])
219
+ text = texts[0] if texts and len(texts) > 0 else metadata.get('text', '')
220
+
221
+ # Use first 60 chars of text as event name
222
+ name = text[:60].strip() + "..." if len(text) > 60 else text.strip()
223
+
224
+ print(f"🔍 DEBUG: Event {i}: id={event_id}, name={name[:50]}")
225
+
226
+ # Simple format for now (can enhance with API call later)
227
+ event_str = f"{i}. **{name}**"
228
+
229
+ # Store event_id for later API call if needed
230
+ if event_id:
231
+ event_str += f" (ID: {event_id[:8]}...)"
232
+
233
+ events.append(event_str)
234
+
235
+ return "\n".join(events)
236
+
237
+ async def _format_event_list_with_api(self, results: list) -> str:
238
+ """
239
+ Format event search results by calling API for full details
240
+ """
241
+ print(f"🔍 DEBUG: RAG returned {len(results)} results")
242
+
243
+ if not results or len(results) == 0:
244
+ return "Hiện tại chưa có event phù hợp 😢\nBạn thử vibe khác nhé!"
245
+
246
+ # Import event service
247
+ from event_service import EventService
248
+ event_service = EventService()
249
+
250
+ events = []
251
+ for i, r in enumerate(results[:3], 1):
252
+ metadata = r.get('metadata', {})
253
+ event_id = metadata.get('id_use', metadata.get('original_id'))
254
+
255
+ print(f"🔍 DEBUG: Fetching event {i} with ID: {event_id}")
256
+
257
+ # Try to get full event data from API
258
+ event_data = None
259
+ if event_id:
260
+ event_data = await event_service.get_event_by_id(event_id)
261
+
262
+ if event_data:
263
+ # Use API data
264
+ name = event_data.get("eventName", "Sự kiện")
265
+ start = event_data.get("eventStartTime", "")
266
+ date_str = start[:10] if start else "TBA"
267
+ location = event_data.get("eventAddress", "")
268
+
269
+ event_str = f"{i}. **{name}**"
270
+ if date_str != "TBA":
271
+ event_str += f" ({date_str})"
272
+ if location:
273
+ event_str += f" - {location}"
274
+
275
+ print(f"✅ Event {i}: {name} ({date_str})")
276
+ else:
277
+ # Fallback to text from Qdrant
278
+ texts = metadata.get('texts', [])
279
+ text = texts[0] if texts and len(texts) > 0 else ""
280
+ name = text[:60].strip() + "..." if len(text) > 60 else text.strip()
281
+
282
+ event_str = f"{i}. **{name}**"
283
+ print(f"⚠️ Event {i}: Fallback to text (API failed)")
284
+
285
+ events.append(event_str)
286
+
287
+ await event_service.close()
288
+ return "\n".join(events)
289
+
290
+ def _detect_choice(self, user_input: str) -> str:
291
+ """Detect what info user wants to see"""
292
+ input_lower = user_input.lower()
293
+
294
+ if any(k in input_lower for k in ['giá', 'price', 'vé', 'ticket', 'bao nhiêu']):
295
+ return 'price'
296
+ elif any(k in input_lower for k in ['lineup', 'line-up', 'nghệ sĩ', 'artist', 'performer']):
297
+ return 'lineup'
298
+ elif any(k in input_lower for k in ['địa điểm', 'location', 'ở đâu', 'where', 'chỗ']):
299
+ return 'location'
300
+ elif any(k in input_lower for k in ['thời gian', 'time', 'khi nào', 'when', 'lịch', 'date']):
301
+ return 'time'
302
+ else:
303
+ return 'price' # Default
304
+
305
+ def _detect_yes_no(self, user_input: str) -> str:
306
+ """Detect yes/no response"""
307
+ input_lower = user_input.lower()
308
+
309
+ if any(k in input_lower for k in ['có', 'yes', 'ok', 'được', 'ừ', 'oke']):
310
+ return 'yes'
311
+ else:
312
+ return 'no'
event_service.py ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Event Service - Get event details from MongoDB ID or eventCode
3
+ """
4
+ import httpx
5
+ from typing import Dict, Any, Optional
6
+
7
+
8
+ class EventService:
9
+ """Service to fetch event details from external API"""
10
+
11
+ def __init__(self, base_url: str = "https://hoalacrent.io.vn/api/v0"):
12
+ self.base_url = base_url
13
+ self.client = httpx.AsyncClient(timeout=10.0)
14
+
15
+ async def get_event_by_code(self, event_code: str) -> Optional[Dict[str, Any]]:
16
+ """
17
+ Get event details by eventCode
18
+
19
+ Args:
20
+ event_code: Event code (e.g., "EVENT001")
21
+
22
+ Returns:
23
+ Event data or None if not found
24
+ """
25
+ try:
26
+ url = f"{self.base_url}/event/get-event-by-event-code"
27
+ response = await self.client.get(url, params={"eventCode": event_code})
28
+ response.raise_for_status()
29
+
30
+ data = response.json()
31
+ if data.get("success") and data.get("data"):
32
+ event = data["data"]
33
+ return {
34
+ "eventCode": event.get("eventCode"),
35
+ "eventName": event.get("eventName"),
36
+ "eventAddress": event.get("eventAddress"),
37
+ "eventStartTime": event.get("eventStartTime"),
38
+ "eventEndTime": event.get("eventEndTime"),
39
+ "maxParticipants": event.get("maxParticipants"),
40
+ "eventOrganizer": event.get("eventOrganizer"),
41
+ "eventDescription": event.get("eventDescription")
42
+ }
43
+ return None
44
+ except Exception as e:
45
+ print(f"⚠️ Error fetching event by code: {e}")
46
+ return None
47
+
48
+ async def get_event_by_id(self, event_id: str) -> Optional[Dict[str, Any]]:
49
+ """
50
+ Get event details by MongoDB _id
51
+
52
+ IMPORTANT: Backend needs to implement this endpoint:
53
+ GET /api/v0/event/get-event-by-id?id={mongodb_id}
54
+
55
+ Args:
56
+ event_id: MongoDB _id (e.g., "6900ae38eb03f29702c7fd1d")
57
+
58
+ Returns:
59
+ Event data or None if not found
60
+ """
61
+ try:
62
+ # Option 1: If backend has this endpoint
63
+ url = f"{self.base_url}/event/get-event-by-id"
64
+ response = await self.client.get(url, params={"id": event_id})
65
+ response.raise_for_status()
66
+
67
+ data = response.json()
68
+ if data.get("success") and data.get("data"):
69
+ event = data["data"]
70
+ return {
71
+ "eventCode": event.get("eventCode"),
72
+ "eventName": event.get("eventName"),
73
+ "eventAddress": event.get("eventAddress"),
74
+ "eventStartTime": event.get("eventStartTime"),
75
+ "eventEndTime": event.get("eventEndTime"),
76
+ "maxParticipants": event.get("maxParticipants"),
77
+ "eventOrganizer": event.get("eventOrganizer"),
78
+ "eventDescription": event.get("eventDescription")
79
+ }
80
+ return None
81
+ except httpx.HTTPStatusError as e:
82
+ if e.response.status_code == 404:
83
+ print(f"⚠️ Event not found: {event_id}")
84
+ else:
85
+ print(f"⚠️ Error fetching event by ID: {e}")
86
+ return None
87
+ except Exception as e:
88
+ print(f"⚠️ Error fetching event by ID: {e}")
89
+ return None
90
+
91
+ def format_event_for_display(self, event: Dict[str, Any]) -> str:
92
+ """
93
+ Format event data for chat display
94
+
95
+ Args:
96
+ event: Event data dict
97
+
98
+ Returns:
99
+ Formatted string
100
+ """
101
+ name = event.get("eventName", "Sự kiện")
102
+
103
+ # Format date
104
+ start = event.get("eventStartTime", "")
105
+ date_str = start[:10] if start else "TBA" # Extract date part
106
+
107
+ # Format location
108
+ location = event.get("eventAddress", "")
109
+
110
+ result = f"**{name}**"
111
+ if date_str != "TBA":
112
+ result += f" ({date_str})"
113
+ if location:
114
+ result += f" - {location}"
115
+
116
+ return result
117
+
118
+ async def close(self):
119
+ """Close HTTP client"""
120
+ await self.client.aclose()