minhvtt commited on
Commit
0e32cea
·
verified ·
1 Parent(s): 4d0fd6c

Update agent_service.py

Browse files
Files changed (1) hide show
  1. agent_service.py +436 -432
agent_service.py CHANGED
@@ -1,432 +1,436 @@
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="meta-llama/Llama-3.1-70B-Instruct", # Llama 3.1 - better instruction following, less hallucination
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."
 
 
 
 
 
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 with Sambanova provider for Llama 70B
314
+ # Sambanova hosts Llama models for free via HuggingFace
315
+ client = AsyncInferenceClient(
316
+ token=self.hf_token,
317
+ provider="sambanova" # Use Sambanova for Llama 70B
318
+ )
319
+
320
+ # Call HF API with chat completion and native tools
321
+ response = await client.chat_completion(
322
+ messages=messages,
323
+ model="meta-llama/Llama-3.3-70B-Instruct", # Llama 3.3 70B via Sambanova
324
+ max_tokens=512,
325
+ temperature=0.7,
326
+ tools=tools,
327
+ tool_choice="auto" # Let model decide when to use tools
328
+ )
329
+
330
+ # Check if the model made tool calls
331
+ message = response.choices[0].message
332
+
333
+ if message.tool_calls:
334
+ print(f"🔧 Native tool calls detected: {len(message.tool_calls)}")
335
+ return {
336
+ "type": "tool_calls",
337
+ "tool_calls": [
338
+ {
339
+ "id": tc.id,
340
+ "function": {
341
+ "name": tc.function.name,
342
+ "arguments": tc.function.arguments
343
+ }
344
+ }
345
+ for tc in message.tool_calls
346
+ ]
347
+ }
348
+ else:
349
+ # Regular text response
350
+ return {
351
+ "type": "text",
352
+ "content": message.content or ""
353
+ }
354
+
355
+ except Exception as e:
356
+ print(f"⚠️ LLM Call Error: {e}")
357
+ return {
358
+ "type": "error",
359
+ "content": str(e)
360
+ }
361
+
362
+ def _format_tool_result(self, tool_result: Dict) -> str:
363
+ """Format tool result for feeding back to LLM"""
364
+ result = tool_result.get("result", {})
365
+
366
+ # Special handling for purchased events list
367
+ if isinstance(result, list):
368
+ print(f"\n🔍 Formatting {len(result)} purchased events for LLM")
369
+ if not result:
370
+ return "User has not purchased any events yet."
371
+
372
+ # Format each event clearly
373
+ formatted_events = []
374
+ for i, event in enumerate(result, 1):
375
+ event_info = []
376
+ event_info.append(f"Event {i}:")
377
+
378
+ # Extract key fields
379
+ if 'eventName' in event:
380
+ event_info.append(f" Name: {event['eventName']}")
381
+ if 'eventCode' in event:
382
+ event_info.append(f" Code: {event['eventCode']}")
383
+ if '_id' in event:
384
+ event_info.append(f" ID: {event['_id']}")
385
+ if 'startTimeEventTime' in event:
386
+ event_info.append(f" Date: {event['startTimeEventTime']}")
387
+
388
+ formatted_events.append("\n".join(event_info))
389
+
390
+ formatted = "User's Purchased Events:\n\n" + "\n\n".join(formatted_events)
391
+ print(f"📤 Sending to LLM:\n{formatted}")
392
+ return formatted
393
+
394
+ # Default formatting for other results
395
+ if isinstance(result, dict):
396
+ # Pretty print key info
397
+ formatted = []
398
+ for key, value in result.items():
399
+ if key not in ["success", "error"]:
400
+ formatted.append(f"{key}: {value}")
401
+ return "\n".join(formatted) if formatted else json.dumps(result)
402
+
403
+ return str(result)
404
+
405
+ async def _execute_rag_search(self, query_params: Dict) -> str:
406
+ """
407
+ Execute RAG search for event discovery
408
+ Called when LLM wants to search_events
409
+ """
410
+ query = query_params.get("query", "")
411
+ vibe = query_params.get("vibe", "")
412
+
413
+ # Build search query
414
+ search_text = f"{query} {vibe}".strip()
415
+
416
+ print(f"🔍 RAG Search: {search_text}")
417
+
418
+ # Use embedding + qdrant
419
+ embedding = self.embedding_service.encode_text(search_text)
420
+ results = self.qdrant_service.search(
421
+ query_embedding=embedding,
422
+ limit=5
423
+ )
424
+
425
+ # Format results
426
+ formatted = []
427
+ for i, result in enumerate(results, 1):
428
+ # Result is a dict with keys: id, score, payload
429
+ payload = result.get("payload", {})
430
+ texts = payload.get("texts", [])
431
+ text = texts[0] if texts else ""
432
+ event_id = payload.get("id_use", "")
433
+
434
+ formatted.append(f"{i}. {text[:100]}... (ID: {event_id})")
435
+
436
+ return "\n".join(formatted) if formatted else "Không tìm thấy sự kiện phù hợp."