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

Upload 4 files

Browse files
Files changed (4) hide show
  1. agent_service.py +474 -458
  2. feedback_tracking_service.py +103 -0
  3. main.py +13 -2
  4. tools_service.py +25 -5
agent_service.py CHANGED
@@ -1,458 +1,474 @@
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
- access_token: Optional[str] = None, # NEW: For authenticated API calls
56
- max_iterations: int = 3
57
- ) -> Dict[str, Any]:
58
- """
59
- Main conversation loop
60
-
61
- Args:
62
- user_message: User's input
63
- conversation_history: Previous messages [{"role": "user", "content": ...}, ...]
64
- mode: "sales" or "feedback"
65
- user_id: User ID (for feedback mode to check purchase history)
66
- access_token: JWT token for authenticated API calls
67
- max_iterations: Maximum tool call iterations to prevent infinite loops
68
-
69
- Returns:
70
- {
71
- "message": "Bot response",
72
- "tool_calls": [...], # List of tools called (for debugging)
73
- "mode": mode
74
- }
75
- """
76
- print(f"\n🤖 Agent Mode: {mode}")
77
- print(f"👤 User Message: {user_message}")
78
-
79
- # Store access_token for tool calls
80
- self.current_access_token = access_token
81
-
82
- # Select system prompt
83
- system_prompt = self._get_system_prompt(mode)
84
-
85
- # Build conversation context
86
- messages = self._build_messages(system_prompt, conversation_history, user_message)
87
-
88
- # Agentic loop: LLM may call tools multiple times
89
- tool_calls_made = []
90
- current_response = None
91
-
92
- for iteration in range(max_iterations):
93
- print(f"\n🔄 Iteration {iteration + 1}")
94
-
95
- # Call LLM
96
- llm_response = await self._call_llm(messages)
97
- print(f"🧠 LLM Response: {llm_response[:200]}...")
98
-
99
- # Check if LLM wants to call a tool
100
- tool_call = self._parse_tool_call(llm_response)
101
-
102
- if not tool_call:
103
- # No tool call -> This is the final response
104
- current_response = llm_response
105
- break
106
-
107
- # Execute tool
108
- print(f"🔧 Tool Called: {tool_call['tool_name']}")
109
- tool_result = await self.tools_service.execute_tool(
110
- tool_call['tool_name'],
111
- tool_call['arguments'],
112
- access_token=self.current_access_token # Pass access_token
113
- )
114
-
115
- # Record tool call
116
- tool_calls_made.append({
117
- "function": tool_call['tool_name'],
118
- "arguments": tool_call['arguments'],
119
- "result": tool_result
120
- })
121
-
122
- # Add tool result to conversation
123
- messages.append({
124
- "role": "assistant",
125
- "content": llm_response
126
- })
127
- messages.append({
128
- "role": "system",
129
- "content": f"Tool Result:\n{self._format_tool_result({'result': tool_result})}"
130
- })
131
-
132
- # If tool returns "run_rag_search", handle it specially
133
- if isinstance(tool_result, dict) and tool_result.get("action") == "run_rag_search":
134
- rag_results = await self._execute_rag_search(tool_result["query"])
135
- messages[-1]["content"] = f"RAG Search Results:\n{rag_results}"
136
-
137
- # Clean up response
138
- final_response = current_response or llm_response
139
- final_response = self._clean_response(final_response)
140
-
141
- return {
142
- "message": final_response,
143
- "tool_calls": tool_calls_made,
144
- "mode": mode
145
- }
146
-
147
- def _get_system_prompt(self, mode: str) -> str:
148
- """Get system prompt for selected mode with tools definition"""
149
- prompt_key = f"{mode}_agent" if mode in ["sales", "feedback"] else "sales_agent"
150
- base_prompt = self.prompts.get(prompt_key, "")
151
-
152
- # Add tools definition
153
- tools_definition = self._get_tools_definition()
154
-
155
- return f"{base_prompt}\n\n{tools_definition}"
156
-
157
- def _get_tools_definition(self) -> str:
158
- """Get tools definition in text format for prompt"""
159
- return """
160
- # AVAILABLE TOOLS
161
-
162
- You can call the following tools when needed. To call a tool, output a JSON block like this:
163
-
164
- ```json
165
- {
166
- "tool_call": "tool_name",
167
- "arguments": {
168
- "arg1": "value1",
169
- "arg2": "value2"
170
- }
171
- }
172
- ```
173
-
174
- ## Tools List:
175
-
176
- ### 1. search_events
177
- Search for events matching user criteria.
178
- Arguments:
179
- - query (string): Search keywords
180
- - vibe (string, optional): Mood/vibe (e.g., "chill", "sôi động")
181
- - time (string, optional): Time period (e.g., "cuối tuần này")
182
-
183
- Example:
184
- ```json
185
- {"tool_call": "search_events", "arguments": {"query": "nhạc rock", "vibe": "sôi động"}}
186
- ```
187
-
188
- ### 2. get_event_details
189
- Get detailed information about a specific event.
190
- Arguments:
191
- - event_id (string): Event ID from search results
192
-
193
- Example:
194
- ```json
195
- {"tool_call": "get_event_details", "arguments": {"event_id": "6900ae38eb03f29702c7fd1d"}}
196
- ```
197
-
198
- ### 3. get_purchased_events (Feedback mode only)
199
- Check which events the user has attended.
200
- Arguments:
201
- - user_id (string): User ID
202
-
203
- Example:
204
- ```json
205
- {"tool_call": "get_purchased_events", "arguments": {"user_id": "user_123"}}
206
- ```
207
-
208
- ### 4. save_feedback
209
- Save user's feedback/review for an event.
210
- Arguments:
211
- - event_id (string): Event ID
212
- - rating (integer): 1-5 stars
213
- - comment (string, optional): User's comment
214
-
215
- Example:
216
- ```json
217
- {"tool_call": "save_feedback", "arguments": {"event_id": "abc123", "rating": 5, "comment": "Tuyệt vời!"}}
218
- ```
219
-
220
- ### 5. save_lead
221
- Save customer contact information.
222
- Arguments:
223
- - email (string, optional): Email address
224
- - phone (string, optional): Phone number
225
- - interest (string, optional): What they're interested in
226
-
227
- Example:
228
- ```json
229
- {"tool_call": "save_lead", "arguments": {"email": "user@example.com", "interest": "Rock show"}}
230
- ```
231
-
232
- **IMPORTANT:**
233
- - Call tools ONLY when you need real-time data
234
- - After receiving tool results, respond naturally to the user
235
- - Don't expose raw JSON to users - always format nicely
236
- """
237
-
238
- def _build_messages(
239
- self,
240
- system_prompt: str,
241
- history: List[Dict],
242
- user_message: str
243
- ) -> List[Dict]:
244
- """Build messages array for LLM"""
245
- messages = [{"role": "system", "content": system_prompt}]
246
-
247
- # Add conversation history
248
- messages.extend(history)
249
-
250
- # Add current user message
251
- messages.append({"role": "user", "content": user_message})
252
-
253
- return messages
254
-
255
- async def _call_llm(self, messages: List[Dict]) -> str:
256
- """
257
- Call HuggingFace LLM directly using chat_completion (conversational)
258
- """
259
- try:
260
- from huggingface_hub import AsyncInferenceClient
261
-
262
- # Create async client
263
- client = AsyncInferenceClient(token=self.hf_token)
264
-
265
- # Call HF API with chat completion (conversational)
266
- response_text = ""
267
- async for message in await client.chat_completion(
268
- messages=messages, # Use messages directly
269
- model="meta-llama/Llama-3.3-70B-Instruct",
270
- max_tokens=512,
271
- temperature=0.7,
272
- stream=True
273
- ):
274
- if message.choices and message.choices[0].delta.content:
275
- response_text += message.choices[0].delta.content
276
-
277
- return response_text
278
- except Exception as e:
279
- print(f"⚠️ LLM Call Error: {e}")
280
- return "Xin lỗi, tôi đang gặp chút vấn đề kỹ thuật. Bạn thử lại sau nhé!"
281
-
282
- def _messages_to_prompt(self, messages: List[Dict]) -> str:
283
- """Convert messages array to single prompt string"""
284
- prompt_parts = []
285
-
286
- for msg in messages:
287
- role = msg["role"]
288
- content = msg["content"]
289
-
290
- if role == "system":
291
- prompt_parts.append(f"[SYSTEM]\n{content}\n")
292
- elif role == "user":
293
- prompt_parts.append(f"[USER]\n{content}\n")
294
- elif role == "assistant":
295
- prompt_parts.append(f"[ASSISTANT]\n{content}\n")
296
-
297
- return "\n".join(prompt_parts)
298
-
299
- def _format_tool_result(self, tool_result: Dict) -> str:
300
- """Format tool result for feeding back to LLM"""
301
- result = tool_result.get("result", {})
302
-
303
- if isinstance(result, dict):
304
- # Pretty print key info
305
- formatted = []
306
- for key, value in result.items():
307
- if key not in ["success", "error"]:
308
- formatted.append(f"{key}: {value}")
309
- return "\n".join(formatted)
310
-
311
- return str(result)
312
-
313
- async def _execute_rag_search(self, query_params: Dict) -> str:
314
- """
315
- Execute RAG search for event discovery
316
- Called when LLM wants to search_events
317
- """
318
- query = query_params.get("query", "")
319
- vibe = query_params.get("vibe", "")
320
-
321
- # Build search query
322
- search_text = f"{query} {vibe}".strip()
323
-
324
- print(f"🔍 RAG Search: {search_text}")
325
-
326
- # Use embedding + qdrant
327
- embedding = self.embedding_service.encode_text(search_text)
328
- results = self.qdrant_service.search(
329
- query_embedding=embedding,
330
- limit=5
331
- )
332
-
333
- # Format results
334
- formatted = []
335
- for i, result in enumerate(results, 1):
336
- # Result is a dict with keys: id, score, payload
337
- payload = result.get("payload", {})
338
- texts = payload.get("texts", [])
339
- text = texts[0] if texts else ""
340
- event_id = payload.get("id_use", "")
341
-
342
- formatted.append(f"{i}. {text[:100]}... (ID: {event_id})")
343
-
344
- return "\n".join(formatted) if formatted else "Không tìm thấy sự kiện phù hợp."
345
-
346
- def _parse_tool_call(self, llm_response: str) -> Optional[Dict]:
347
- """
348
- Parse LLM response to detect tool calls using structured JSON
349
-
350
- Returns:
351
- {"tool_name": "...", "arguments": {...}} or None
352
- """
353
- import json
354
- import re
355
-
356
- # Method 1: Look for JSON code block
357
- json_match = re.search(r'```json\s*(\{.*?\})\s*```', llm_response, re.DOTALL)
358
- if json_match:
359
- try:
360
- data = json.loads(json_match.group(1))
361
- return self._extract_tool_from_json(data)
362
- except json.JSONDecodeError:
363
- pass
364
-
365
- # Method 2: Look for inline JSON object
366
- # Find all potential JSON objects
367
- json_objects = re.findall(r'\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}', llm_response)
368
- for json_str in json_objects:
369
- try:
370
- data = json.loads(json_str)
371
- tool_call = self._extract_tool_from_json(data)
372
- if tool_call:
373
- return tool_call
374
- except json.JSONDecodeError:
375
- continue
376
-
377
- # Method 3: Nested JSON (for complex structures)
378
- try:
379
- # Find outermost curly braces
380
- if '{' in llm_response and '}' in llm_response:
381
- start = llm_response.find('{')
382
- # Find matching closing brace
383
- count = 0
384
- for i, char in enumerate(llm_response[start:], start):
385
- if char == '{':
386
- count += 1
387
- elif char == '}':
388
- count -= 1
389
- if count == 0:
390
- json_str = llm_response[start:i+1]
391
- data = json.loads(json_str)
392
- return self._extract_tool_from_json(data)
393
- except (json.JSONDecodeError, ValueError):
394
- pass
395
-
396
- return None
397
-
398
- def _extract_tool_from_json(self, data: dict) -> Optional[Dict]:
399
- """
400
- Extract tool call information from parsed JSON
401
-
402
- Supports multiple formats:
403
- - {"tool_call": "search_events", "arguments": {...}}
404
- - {"function": "search_events", "parameters": {...}}
405
- - {"name": "search_events", "args": {...}}
406
- """
407
- # Format 1: tool_call + arguments
408
- if "tool_call" in data and isinstance(data["tool_call"], str):
409
- return {
410
- "tool_name": data["tool_call"],
411
- "arguments": data.get("arguments", {})
412
- }
413
-
414
- # Format 2: function + parameters
415
- if "function" in data:
416
- return {
417
- "tool_name": data["function"],
418
- "arguments": data.get("parameters", data.get("arguments", {}))
419
- }
420
-
421
- # Format 3: name + args
422
- if "name" in data:
423
- return {
424
- "tool_name": data["name"],
425
- "arguments": data.get("args", data.get("arguments", {}))
426
- }
427
-
428
- # Format 4: Direct tool name as key
429
- valid_tools = ["search_events", "get_event_details", "get_purchased_events", "save_feedback", "save_lead"]
430
- for tool in valid_tools:
431
- if tool in data:
432
- return {
433
- "tool_name": tool,
434
- "arguments": data[tool] if isinstance(data[tool], dict) else {}
435
- }
436
-
437
- return None
438
-
439
- def _clean_response(self, response: str) -> str:
440
- """Remove JSON artifacts from final response"""
441
- # Remove JSON blocks
442
- if "```json" in response:
443
- response = response.split("```json")[0]
444
- if "```" in response:
445
- response = response.split("```")[0]
446
-
447
- # Remove tool call markers
448
- if "{" in response and "tool_call" in response:
449
- # Find the last natural sentence before JSON
450
- lines = response.split("\n")
451
- cleaned = []
452
- for line in lines:
453
- if "{" in line and "tool_call" in line:
454
- break
455
- cleaned.append(line)
456
- response = "\n".join(cleaned)
457
-
458
- return response.strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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()
feedback_tracking_service.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Feedback Tracking Service
3
+ Tracks which events users have already given feedback for
4
+ """
5
+ from typing import Optional, Dict
6
+ from pymongo.collection import Collection
7
+ from datetime import datetime
8
+
9
+
10
+ class FeedbackTrackingService:
11
+ """
12
+ Track feedback status per user per event
13
+ Prevents redundant "check purchase history" calls
14
+ """
15
+
16
+ def __init__(self, mongo_collection: Collection):
17
+ self.collection = mongo_collection
18
+ self._ensure_indexes()
19
+
20
+ def _ensure_indexes(self):
21
+ """Create indexes for fast lookup"""
22
+ try:
23
+ # Compound index for quick lookup
24
+ self.collection.create_index([("user_id", 1), ("event_code", 1)], unique=True)
25
+ self.collection.create_index("user_id")
26
+ print("✓ Feedback tracking indexes created")
27
+ except Exception as e:
28
+ print(f"Feedback tracking indexes exist: {e}")
29
+
30
+ def has_given_feedback(self, user_id: str, event_code: str) -> bool:
31
+ """
32
+ Check if user has already given feedback for this event
33
+
34
+ Args:
35
+ user_id: User ID
36
+ event_code: Event code
37
+
38
+ Returns:
39
+ True if feedback already given, False otherwise
40
+ """
41
+ result = self.collection.find_one({
42
+ "user_id": user_id,
43
+ "event_code": event_code,
44
+ "is_feedback": True
45
+ })
46
+ return result is not None
47
+
48
+ def mark_feedback_given(self, user_id: str, event_code: str, rating: int, comment: str = "") -> bool:
49
+ """
50
+ Mark that user has given feedback for this event
51
+
52
+ Args:
53
+ user_id: User ID
54
+ event_code: Event code
55
+ rating: Rating given (1-5)
56
+ comment: Feedback comment
57
+
58
+ Returns:
59
+ True if saved successfully
60
+ """
61
+ try:
62
+ self.collection.update_one(
63
+ {
64
+ "user_id": user_id,
65
+ "event_code": event_code
66
+ },
67
+ {
68
+ "$set": {
69
+ "is_feedback": True,
70
+ "rating": rating,
71
+ "comment": comment,
72
+ "feedback_date": datetime.utcnow(),
73
+ "updated_at": datetime.utcnow()
74
+ },
75
+ "$setOnInsert": {
76
+ "created_at": datetime.utcnow()
77
+ }
78
+ },
79
+ upsert=True
80
+ )
81
+ print(f"✅ Marked feedback: {user_id} → {event_code} (rating: {rating})")
82
+ return True
83
+ except Exception as e:
84
+ print(f"❌ Error marking feedback: {e}")
85
+ return False
86
+
87
+ def get_pending_events(self, user_id: str, purchased_events: list) -> list:
88
+ """
89
+ Filter purchased events to only those without feedback
90
+
91
+ Args:
92
+ user_id: User ID
93
+ purchased_events: List of events user has purchased
94
+
95
+ Returns:
96
+ List of events that need feedback
97
+ """
98
+ pending = []
99
+ for event in purchased_events:
100
+ event_code = event.get("eventCode")
101
+ if event_code and not self.has_given_feedback(user_id, event_code):
102
+ pending.append(event)
103
+ return pending
main.py CHANGED
@@ -21,6 +21,7 @@ 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(
@@ -105,8 +106,16 @@ conversations_collection = db["conversations"]
105
  conversation_service = ConversationService(conversations_collection, max_history=10)
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)
@@ -115,7 +124,8 @@ agent_service = AgentService(
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
 
@@ -150,6 +160,7 @@ class ChatRequest(BaseModel):
150
  user_id: Optional[str] = None # User identifier for session tracking
151
  access_token: Optional[str] = None # NEW: For authenticated API calls (feedback mode)
152
  mode: str = "sales" # NEW: "sales" or "feedback" for agent selection
 
153
  use_rag: bool = True
154
  top_k: int = 3
155
  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é.
 
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
+ from feedback_tracking_service import FeedbackTrackingService # NEW: Feedback tracking
25
 
26
  # Initialize FastAPI app
27
  app = FastAPI(
 
106
  conversation_service = ConversationService(conversations_collection, max_history=10)
107
  print("✓ Conversation Service initialized")
108
 
109
+ # Initialize Feedback Tracking Service
110
+ feedback_tracking_collection = db["feedback_tracking"]
111
+ feedback_tracking = FeedbackTrackingService(feedback_tracking_collection)
112
+ print("✓ Feedback Tracking Service initialized")
113
+
114
  # Initialize Tools Service
115
+ tools_service = ToolsService(
116
+ base_url="https://hoalacrent.io.vn/api/v0",
117
+ feedback_tracking=feedback_tracking
118
+ )
119
  print("✓ Tools Service initialized (Function Calling enabled)")
120
 
121
  # Initialize Agent Service (Agentic Workflow)
 
124
  embedding_service=embedding_service,
125
  qdrant_service=qdrant_service,
126
  advanced_rag=advanced_rag,
127
+ hf_token=hf_token,
128
+ feedback_tracking=feedback_tracking # Pass feedback tracking
129
  )
130
  print("✓ Agent Service initialized (Agentic Workflow enabled)")
131
 
 
160
  user_id: Optional[str] = None # User identifier for session tracking
161
  access_token: Optional[str] = None # NEW: For authenticated API calls (feedback mode)
162
  mode: str = "sales" # NEW: "sales" or "feedback" for agent selection
163
+ event_code: Optional[str] = None # NEW: For targeted feedback on specific event
164
  use_rag: bool = True
165
  top_k: int = 3
166
  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é.
tools_service.py CHANGED
@@ -13,9 +13,10 @@ class ToolsService:
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"):
17
  self.base_url = base_url
18
  self.client = httpx.AsyncClient(timeout=10.0)
 
19
 
20
  def get_tools_definition(self) -> List[Dict]:
21
  """
@@ -201,10 +202,29 @@ class ToolsService:
201
  traceback.print_exc()
202
  return []
203
 
204
- async def _save_feedback(self, event_id: str, rating: int, comment: str) -> Dict:
205
- """Save feedback (Mock or Real API)"""
206
- # TODO: Implement real API call when available
207
- print(f"📝 Saving Feedback: Event={event_id}, Rating={rating}, Comment={comment}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
  return {"success": True, "message": "Feedback recorded"}
209
 
210
  async def close(self):
 
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
  """
 
202
  traceback.print_exc()
203
  return []
204
 
205
+ async def _save_feedback(self, event_id: str, rating: int, comment: str, user_id: str = None, event_code: str = None) -> Dict:
206
+ """Save feedback and mark as completed in tracking system"""
207
+ print(f"\n📝 ===== SAVE FEEDBACK =====")
208
+ print(f"Event ID: {event_id}")
209
+ print(f"Event Code: {event_code}")
210
+ print(f"User ID: {user_id}")
211
+ print(f"Rating: {rating}")
212
+ print(f"Comment: {comment}")
213
+
214
+ # TODO: Implement real API call to save feedback
215
+ # For now, just mark in tracking system
216
+ if self.feedback_tracking and user_id and event_code:
217
+ success = self.feedback_tracking.mark_feedback_given(
218
+ user_id=user_id,
219
+ event_code=event_code,
220
+ rating=rating,
221
+ comment=comment
222
+ )
223
+ if success:
224
+ print(f"✅ Feedback tracked in database")
225
+ else:
226
+ print(f"⚠️ Failed to track feedback")
227
+
228
  return {"success": True, "message": "Feedback recorded"}
229
 
230
  async def close(self):