minhvtt commited on
Commit
30abc26
·
verified ·
1 Parent(s): 4fafc3c

Update agent_service.py

Browse files
Files changed (1) hide show
  1. agent_service.py +242 -474
agent_service.py CHANGED
@@ -1,474 +1,242 @@
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="meta-llama/Llama-3.3-70B-Instruct",
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
- if isinstance(result, dict):
320
- # Pretty print key info
321
- formatted = []
322
- for key, value in result.items():
323
- if key not in ["success", "error"]:
324
- formatted.append(f"{key}: {value}")
325
- return "\n".join(formatted)
326
-
327
- return str(result)
328
-
329
- async def _execute_rag_search(self, query_params: Dict) -> str:
330
- """
331
- Execute RAG search for event discovery
332
- Called when LLM wants to search_events
333
- """
334
- query = query_params.get("query", "")
335
- vibe = query_params.get("vibe", "")
336
-
337
- # Build search query
338
- search_text = f"{query} {vibe}".strip()
339
-
340
- print(f"🔍 RAG Search: {search_text}")
341
-
342
- # Use embedding + qdrant
343
- embedding = self.embedding_service.encode_text(search_text)
344
- results = self.qdrant_service.search(
345
- query_embedding=embedding,
346
- limit=5
347
- )
348
-
349
- # Format results
350
- formatted = []
351
- for i, result in enumerate(results, 1):
352
- # Result is a dict with keys: id, score, payload
353
- payload = result.get("payload", {})
354
- texts = payload.get("texts", [])
355
- text = texts[0] if texts else ""
356
- event_id = payload.get("id_use", "")
357
-
358
- formatted.append(f"{i}. {text[:100]}... (ID: {event_id})")
359
-
360
- return "\n".join(formatted) if formatted else "Không tìm thấy sự kiện phù hợp."
361
-
362
- def _parse_tool_call(self, llm_response: str) -> Optional[Dict]:
363
- """
364
- Parse LLM response to detect tool calls using structured JSON
365
-
366
- Returns:
367
- {"tool_name": "...", "arguments": {...}} or None
368
- """
369
- import json
370
- import re
371
-
372
- # Method 1: Look for JSON code block
373
- json_match = re.search(r'```json\s*(\{.*?\})\s*```', llm_response, re.DOTALL)
374
- if json_match:
375
- try:
376
- data = json.loads(json_match.group(1))
377
- return self._extract_tool_from_json(data)
378
- except json.JSONDecodeError:
379
- pass
380
-
381
- # Method 2: Look for inline JSON object
382
- # Find all potential JSON objects
383
- json_objects = re.findall(r'\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}', llm_response)
384
- for json_str in json_objects:
385
- try:
386
- data = json.loads(json_str)
387
- tool_call = self._extract_tool_from_json(data)
388
- if tool_call:
389
- return tool_call
390
- except json.JSONDecodeError:
391
- continue
392
-
393
- # Method 3: Nested JSON (for complex structures)
394
- try:
395
- # Find outermost curly braces
396
- if '{' in llm_response and '}' in llm_response:
397
- start = llm_response.find('{')
398
- # Find matching closing brace
399
- count = 0
400
- for i, char in enumerate(llm_response[start:], start):
401
- if char == '{':
402
- count += 1
403
- elif char == '}':
404
- count -= 1
405
- if count == 0:
406
- json_str = llm_response[start:i+1]
407
- data = json.loads(json_str)
408
- return self._extract_tool_from_json(data)
409
- except (json.JSONDecodeError, ValueError):
410
- pass
411
-
412
- return None
413
-
414
- def _extract_tool_from_json(self, data: dict) -> Optional[Dict]:
415
- """
416
- Extract tool call information from parsed JSON
417
-
418
- Supports multiple formats:
419
- - {"tool_call": "search_events", "arguments": {...}}
420
- - {"function": "search_events", "parameters": {...}}
421
- - {"name": "search_events", "args": {...}}
422
- """
423
- # Format 1: tool_call + arguments
424
- if "tool_call" in data and isinstance(data["tool_call"], str):
425
- return {
426
- "tool_name": data["tool_call"],
427
- "arguments": data.get("arguments", {})
428
- }
429
-
430
- # Format 2: function + parameters
431
- if "function" in data:
432
- return {
433
- "tool_name": data["function"],
434
- "arguments": data.get("parameters", data.get("arguments", {}))
435
- }
436
-
437
- # Format 3: name + args
438
- if "name" in data:
439
- return {
440
- "tool_name": data["name"],
441
- "arguments": data.get("args", data.get("arguments", {}))
442
- }
443
-
444
- # Format 4: Direct tool name as key
445
- valid_tools = ["search_events", "get_event_details", "get_purchased_events", "save_feedback", "save_lead"]
446
- for tool in valid_tools:
447
- if tool in data:
448
- return {
449
- "tool_name": tool,
450
- "arguments": data[tool] if isinstance(data[tool], dict) else {}
451
- }
452
-
453
- return None
454
-
455
- def _clean_response(self, response: str) -> str:
456
- """Remove JSON artifacts from final response"""
457
- # Remove JSON blocks
458
- if "```json" in response:
459
- response = response.split("```json")[0]
460
- if "```" in response:
461
- response = response.split("```")[0]
462
-
463
- # Remove tool call markers
464
- if "{" in response and "tool_call" in response:
465
- # Find the last natural sentence before JSON
466
- lines = response.split("\n")
467
- cleaned = []
468
- for line in lines:
469
- if "{" in line and "tool_call" in line:
470
- break
471
- cleaned.append(line)
472
- response = "\n".join(cleaned)
473
-
474
- return response.strip()
 
1
+ """
2
+ Tools Service for LLM Function Calling
3
+ HuggingFace-compatible với prompt engineering
4
+ """
5
+ import httpx
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://hoalacrent.io.vn/api/v0", feedback_tracking=None):
17
+ self.base_url = base_url
18
+ self.client = httpx.AsyncClient(timeout=10.0)
19
+ self.feedback_tracking = feedback_tracking # NEW: Feedback tracking
20
+
21
+ def get_tools_definition(self) -> List[Dict]:
22
+ """
23
+ Return list of tool definitions (OpenAI format style)
24
+ Used for constructing system prompt
25
+ """
26
+ return [
27
+ {
28
+ "name": "search_events",
29
+ "description": "Tìm kiếm sự kiện phù hợp theo từ khóa, vibe, hoặc thời gian.",
30
+ "parameters": {
31
+ "type": "object",
32
+ "properties": {
33
+ "query": {"type": "string", "description": "Từ khóa tìm kiếm (VD: 'nhạc rock', 'hài kịch')"},
34
+ "vibe": {"type": "string", "description": "Vibe/Mood (VD: 'chill', 'sôi động', 'hẹn hò')"},
35
+ "time": {"type": "string", "description": "Thời gian (VD: 'cuối tuần này', 'tối nay')"}
36
+ }
37
+ }
38
+ },
39
+ {
40
+ "name": "get_event_details",
41
+ "description": "Lấy thông tin chi tiết (giá, địa điểm, thời gian) của sự kiện.",
42
+ "parameters": {
43
+ "type": "object",
44
+ "properties": {
45
+ "event_id": {"type": "string", "description": "ID của sự kiện (MongoDB ID)"}
46
+ },
47
+ "required": ["event_id"]
48
+ }
49
+ },
50
+ {
51
+ "name": "get_purchased_events",
52
+ "description": "Kiểm tra lịch sử các sự kiện user đã mua vé hoặc tham gia.",
53
+ "parameters": {
54
+ "type": "object",
55
+ "properties": {
56
+ "user_id": {"type": "string", "description": "ID của user"}
57
+ },
58
+ "required": ["user_id"]
59
+ }
60
+ },
61
+ {
62
+ "name": "save_feedback",
63
+ "description": "Lưu đánh giá/feedback của user về sự kiện.",
64
+ "parameters": {
65
+ "type": "object",
66
+ "properties": {
67
+ "event_id": {"type": "string", "description": "ID sự kiện"},
68
+ "rating": {"type": "integer", "description": "Số sao đánh giá (1-5)"},
69
+ "comment": {"type": "string", "description": "Nội dung nhận xét"}
70
+ },
71
+ "required": ["event_id", "rating"]
72
+ }
73
+ },
74
+ {
75
+ "name": "save_lead",
76
+ "description": "Lưu thông tin khách hàng quan tâm (Lead).",
77
+ "parameters": {
78
+ "type": "object",
79
+ "properties": {
80
+ "email": {"type": "string"},
81
+ "phone": {"type": "string"},
82
+ "interest": {"type": "string"}
83
+ }
84
+ }
85
+ }
86
+ ]
87
+
88
+ async def execute_tool(self, tool_name: str, arguments: Dict, access_token: Optional[str] = None) -> Any:
89
+ """
90
+ Execute a tool by name with arguments
91
+
92
+ Args:
93
+ tool_name: Name of the tool
94
+ arguments: Tool arguments
95
+ access_token: JWT token for authenticated API calls
96
+ """
97
+ print(f"\n🔧 ===== TOOL EXECUTION =====")
98
+ print(f"Tool: {tool_name}")
99
+ print(f"Arguments: {arguments}")
100
+ print(f"Access Token: {'✅ Present' if access_token else '❌ Missing'}")
101
+ if access_token:
102
+ print(f"Token preview: {access_token[:30]}...")
103
+
104
+ try:
105
+ if tool_name == "get_event_details":
106
+ return await self._get_event_details(arguments.get("event_id") or arguments.get("event_code"))
107
+
108
+ elif tool_name == "get_purchased_events":
109
+ print(f"→ Calling _get_purchased_events with:")
110
+ print(f" user_id: {arguments.get('user_id')}")
111
+ print(f" access_token: {'✅' if access_token else '❌'}")
112
+ return await self._get_purchased_events(
113
+ arguments.get("user_id"),
114
+ access_token=access_token # Pass access_token
115
+ )
116
+
117
+ elif tool_name == "save_feedback":
118
+ return await self._save_feedback(
119
+ arguments.get("event_id"),
120
+ arguments.get("rating"),
121
+ arguments.get("comment")
122
+ )
123
+
124
+ elif tool_name == "search_events":
125
+ # Note: This usually requires RAG service, so we return a special signal
126
+ # The Agent Service will handle RAG search
127
+ return {"action": "run_rag_search", "query": arguments}
128
+
129
+ elif tool_name == "save_lead":
130
+ # Placeholder for lead saving
131
+ return {"success": True, "message": "Lead saved successfully"}
132
+
133
+ else:
134
+ return {"error": f"Unknown tool: {tool_name}"}
135
+
136
+ except Exception as e:
137
+ print(f"⚠️ Tool Execution Error: {e}")
138
+ return {"error": str(e)}
139
+
140
+ async def _get_event_details(self, event_id: str) -> Dict:
141
+ """Call API to get event details"""
142
+ if not event_id:
143
+ return {"error": "Missing event_id"}
144
+
145
+ try:
146
+ url = f"{self.base_url}/event/get-event-by-id"
147
+
148
+ response = await self.client.get(url, params={"id": event_id})
149
+ if response.status_code == 200:
150
+ data = response.json()
151
+ if data.get("success"):
152
+ return data.get("data")
153
+ return {"error": "Event not found", "details": response.text}
154
+ except Exception as e:
155
+ return {"error": str(e)}
156
+
157
+ async def _get_purchased_events(self, user_id: str, access_token: Optional[str] = None) -> List[Dict]:
158
+ """Call API to get purchased events for user (requires auth)"""
159
+ print(f"\n🎫 ===== GET PURCHASED EVENTS =====")
160
+ print(f"User ID: {user_id}")
161
+ print(f"Access Token: {'✅ Present' if access_token else '❌ Missing'}")
162
+
163
+ if not user_id:
164
+ print("⚠️ No user_id provided, returning empty list")
165
+ return []
166
+
167
+ try:
168
+ url = f"{self.base_url}/event/get-purchase-event-by-user-id/{user_id}"
169
+ print(f"🔍 API URL: {url}")
170
+
171
+ # Add Authorization header if access_token provided
172
+ headers = {}
173
+ if access_token:
174
+ headers["Authorization"] = f"Bearer {access_token}"
175
+ print(f"🔐 Authorization Header Added:")
176
+ print(f" Bearer {access_token[:30]}...")
177
+ else:
178
+ print(f"⚠️ No access_token - calling API without auth")
179
+
180
+ print(f"📡 Headers: {headers}")
181
+ print(f"🚀 Calling API...")
182
+
183
+ response = await self.client.get(url, headers=headers)
184
+
185
+ print(f"📥 Response Status: {response.status_code}")
186
+ print(f"📦 Response Headers: {dict(response.headers)}")
187
+
188
+ if response.status_code == 200:
189
+ data = response.json()
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}")
206
+ print(f"Response body: {response.text[:500]}")
207
+ return []
208
+
209
+ except Exception as e:
210
+ print(f"⚠️ Exception in _get_purchased_events: {type(e).__name__}: {e}")
211
+ import traceback
212
+ traceback.print_exc()
213
+ return []
214
+
215
+ async def _save_feedback(self, event_id: str, rating: int, comment: str, user_id: str = None, event_code: str = None) -> Dict:
216
+ """Save feedback and mark as completed in tracking system"""
217
+ print(f"\n📝 ===== SAVE FEEDBACK =====")
218
+ print(f"Event ID: {event_id}")
219
+ print(f"Event Code: {event_code}")
220
+ print(f"User ID: {user_id}")
221
+ print(f"Rating: {rating}")
222
+ print(f"Comment: {comment}")
223
+
224
+ # TODO: Implement real API call to save feedback
225
+ # For now, just mark in tracking system
226
+ if self.feedback_tracking and user_id and event_code:
227
+ success = self.feedback_tracking.mark_feedback_given(
228
+ user_id=user_id,
229
+ event_code=event_code,
230
+ rating=rating,
231
+ comment=comment
232
+ )
233
+ if success:
234
+ print(f"✅ Feedback tracked in database")
235
+ else:
236
+ print(f"⚠️ Failed to track feedback")
237
+
238
+ return {"success": True, "message": "Feedback recorded"}
239
+
240
+ async def close(self):
241
+ """Close HTTP client"""
242
+ await self.client.aclose()