minhvtt commited on
Commit
8c5b07f
·
verified ·
1 Parent(s): 30abc26

Update agent_service.py

Browse files
Files changed (1) hide show
  1. agent_service.py +480 -219
agent_service.py CHANGED
@@ -1,242 +1,503 @@
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()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ # Special handling for purchased events list
320
+ if isinstance(result, list):
321
+ print(f"\n🔍 Formatting {len(result)} purchased events for LLM")
322
+ if not result:
323
+ return "User has not purchased any events yet."
324
+
325
+ # Format each event clearly
326
+ formatted_events = []
327
+ for i, event in enumerate(result, 1):
328
+ event_info = []
329
+ event_info.append(f"Event {i}:")
 
 
 
 
 
 
 
 
 
 
 
330
 
331
+ # Extract key fields
332
+ if 'eventName' in event:
333
+ event_info.append(f" Name: {event['eventName']}")
334
+ if 'eventCode' in event:
335
+ event_info.append(f" Code: {event['eventCode']}")
336
+ if '_id' in event:
337
+ event_info.append(f" ID: {event['_id']}")
338
+ if 'startTimeEventTime' in event:
339
+ event_info.append(f" Date: {event['startTimeEventTime']}")
340
 
341
+ formatted_events.append("\n".join(event_info))
342
+
343
+ formatted = "User's Purchased Events:\n\n" + "\n\n".join(formatted_events)
344
+ print(f"📤 Sending to LLM:\n{formatted}")
345
+ return formatted
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
346
 
347
+ # Default formatting for other results
348
+ if isinstance(result, dict):
349
+ # Pretty print key info
350
+ formatted = []
351
+ for key, value in result.items():
352
+ if key not in ["success", "error"]:
353
+ formatted.append(f"{key}: {value}")
354
+ return "\n".join(formatted)
355
+
356
+ return str(result)
357
+
358
+ async def _execute_rag_search(self, query_params: Dict) -> str:
359
+ """
360
+ Execute RAG search for event discovery
361
+ Called when LLM wants to search_events
362
+ """
363
+ query = query_params.get("query", "")
364
+ vibe = query_params.get("vibe", "")
365
+
366
+ # Build search query
367
+ search_text = f"{query} {vibe}".strip()
368
+
369
+ print(f"🔍 RAG Search: {search_text}")
370
+
371
+ # Use embedding + qdrant
372
+ embedding = self.embedding_service.encode_text(search_text)
373
+ results = self.qdrant_service.search(
374
+ query_embedding=embedding,
375
+ limit=5
376
+ )
377
+
378
+ # Format results
379
+ formatted = []
380
+ for i, result in enumerate(results, 1):
381
+ # Result is a dict with keys: id, score, payload
382
+ payload = result.get("payload", {})
383
+ texts = payload.get("texts", [])
384
+ text = texts[0] if texts else ""
385
+ event_id = payload.get("id_use", "")
386
+
387
+ formatted.append(f"{i}. {text[:100]}... (ID: {event_id})")
388
+
389
+ return "\n".join(formatted) if formatted else "Không tìm thấy sự kiện phù hợp."
390
+
391
+ def _parse_tool_call(self, llm_response: str) -> Optional[Dict]:
392
+ """
393
+ Parse LLM response to detect tool calls using structured JSON
394
+
395
+ Returns:
396
+ {"tool_name": "...", "arguments": {...}} or None
397
+ """
398
+ import json
399
+ import re
400
+
401
+ # Method 1: Look for JSON code block
402
+ json_match = re.search(r'```json\s*(\{.*?\})\s*```', llm_response, re.DOTALL)
403
+ if json_match:
404
+ try:
405
+ data = json.loads(json_match.group(1))
406
+ return self._extract_tool_from_json(data)
407
+ except json.JSONDecodeError:
408
+ pass
409
+
410
+ # Method 2: Look for inline JSON object
411
+ # Find all potential JSON objects
412
+ json_objects = re.findall(r'\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}', llm_response)
413
+ for json_str in json_objects:
414
+ try:
415
+ data = json.loads(json_str)
416
+ tool_call = self._extract_tool_from_json(data)
417
+ if tool_call:
418
+ return tool_call
419
+ except json.JSONDecodeError:
420
+ continue
421
+
422
+ # Method 3: Nested JSON (for complex structures)
423
+ try:
424
+ # Find outermost curly braces
425
+ if '{' in llm_response and '}' in llm_response:
426
+ start = llm_response.find('{')
427
+ # Find matching closing brace
428
+ count = 0
429
+ for i, char in enumerate(llm_response[start:], start):
430
+ if char == '{':
431
+ count += 1
432
+ elif char == '}':
433
+ count -= 1
434
+ if count == 0:
435
+ json_str = llm_response[start:i+1]
436
+ data = json.loads(json_str)
437
+ return self._extract_tool_from_json(data)
438
+ except (json.JSONDecodeError, ValueError):
439
+ pass
440
+
441
+ return None
442
+
443
+ def _extract_tool_from_json(self, data: dict) -> Optional[Dict]:
444
+ """
445
+ Extract tool call information from parsed JSON
446
+
447
+ Supports multiple formats:
448
+ - {"tool_call": "search_events", "arguments": {...}}
449
+ - {"function": "search_events", "parameters": {...}}
450
+ - {"name": "search_events", "args": {...}}
451
+ """
452
+ # Format 1: tool_call + arguments
453
+ if "tool_call" in data and isinstance(data["tool_call"], str):
454
+ return {
455
+ "tool_name": data["tool_call"],
456
+ "arguments": data.get("arguments", {})
457
+ }
458
+
459
+ # Format 2: function + parameters
460
+ if "function" in data:
461
+ return {
462
+ "tool_name": data["function"],
463
+ "arguments": data.get("parameters", data.get("arguments", {}))
464
+ }
465
+
466
+ # Format 3: name + args
467
+ if "name" in data:
468
+ return {
469
+ "tool_name": data["name"],
470
+ "arguments": data.get("args", data.get("arguments", {}))
471
+ }
472
+
473
+ # Format 4: Direct tool name as key
474
+ valid_tools = ["search_events", "get_event_details", "get_purchased_events", "save_feedback", "save_lead"]
475
+ for tool in valid_tools:
476
+ if tool in data:
477
+ return {
478
+ "tool_name": tool,
479
+ "arguments": data[tool] if isinstance(data[tool], dict) else {}
480
+ }
481
+
482
+ return None
483
+
484
+ def _clean_response(self, response: str) -> str:
485
+ """Remove JSON artifacts from final response"""
486
+ # Remove JSON blocks
487
+ if "```json" in response:
488
+ response = response.split("```json")[0]
489
+ if "```" in response:
490
+ response = response.split("```")[0]
491
+
492
+ # Remove tool call markers
493
+ if "{" in response and "tool_call" in response:
494
+ # Find the last natural sentence before JSON
495
+ lines = response.split("\n")
496
+ cleaned = []
497
+ for line in lines:
498
+ if "{" in line and "tool_call" in line:
499
+ break
500
+ cleaned.append(line)
501
+ response = "\n".join(cleaned)
502
+
503
+ return response.strip()