minh9972t12 commited on
Commit
9a00c0a
·
verified ·
1 Parent(s): 1d5e56d

Upload 17 files

Browse files
Files changed (1) hide show
  1. agent_service.py +213 -305
agent_service.py CHANGED
@@ -1,15 +1,17 @@
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__(
@@ -19,7 +21,7 @@ class AgentService:
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
@@ -48,17 +50,110 @@ class AgentService:
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
@@ -89,9 +184,12 @@ class AgentService:
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
 
@@ -102,57 +200,75 @@ class AgentService:
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,
@@ -161,116 +277,9 @@ class AgentService:
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 (filtered by mode)
169
- tools_definition = self._get_tools_definition(mode)
170
-
171
- return f"{base_prompt}\n\n{tools_definition}"
172
-
173
- def _get_tools_definition(self, mode: str = "sales") -> str:
174
- """Get tools definition in text format for prompt, filtered by mode"""
175
-
176
- # Base header
177
- header = """
178
- # AVAILABLE TOOLS
179
-
180
- You can call the following tools when needed. To call a tool, output a JSON block like this:
181
-
182
- ```json
183
- {
184
- "tool_call": "tool_name",
185
- "arguments": {
186
- "arg1": "value1",
187
- "arg2": "value2"
188
- }
189
- }
190
- ```
191
-
192
- ## Tools List:
193
- """
194
-
195
- # Tools available for ALL modes
196
- common_tools = """
197
- ### search_events
198
- Search for events matching user criteria.
199
- Arguments:
200
- - query (string): Search keywords
201
- - vibe (string, optional): Mood/vibe (e.g., "chill", "sôi động")
202
- - time (string, optional): Time period (e.g., "cuối tuần này")
203
-
204
- Example:
205
- ```json
206
- {"tool_call": "search_events", "arguments": {"query": "nhạc rock", "vibe": "sôi động"}}
207
- ```
208
-
209
- ### get_event_details
210
- Get detailed information about a specific event.
211
- Arguments:
212
- - event_id (string): Event ID from search results
213
-
214
- Example:
215
- ```json
216
- {"tool_call": "get_event_details", "arguments": {"event_id": "6900ae38eb03f29702c7fd1d"}}
217
- ```
218
- """
219
-
220
- # Tools ONLY for sales mode
221
- sales_only_tools = """
222
- ### save_lead
223
- Save customer contact information.
224
- Arguments:
225
- - email (string, optional): Email address
226
- - phone (string, optional): Phone number
227
- - interest (string, optional): What they're interested in
228
-
229
- Example:
230
- ```json
231
- {"tool_call": "save_lead", "arguments": {"email": "user@example.com", "interest": "Rock show"}}
232
- ```
233
- """
234
-
235
- # Tools ONLY for feedback mode
236
- feedback_only_tools = """
237
- ### get_purchased_events
238
- Check which events the user has attended. MUST call this tool to get REAL data from API.
239
- Arguments:
240
- - user_id (string): User ID
241
-
242
- Example:
243
- ```json
244
- {"tool_call": "get_purchased_events", "arguments": {"user_id": "user_123"}}
245
- ```
246
-
247
- ### save_feedback
248
- Save user's feedback/review for an event.
249
- Arguments:
250
- - event_id (string): Event ID
251
- - rating (integer): 1-5 stars
252
- - comment (string, optional): User's comment
253
-
254
- Example:
255
- ```json
256
- {"tool_call": "save_feedback", "arguments": {"event_id": "abc123", "rating": 5, "comment": "Tuyệt vời!"}}
257
- ```
258
- """
259
-
260
- # Footer with important notes
261
- footer = """
262
- **IMPORTANT:**
263
- - Call tools ONLY when you need real-time data
264
- - After receiving tool results, respond naturally to the user
265
- - Don't expose raw JSON to users - always format nicely
266
- - NEVER invent or fabricate data - always use real results from tools
267
- """
268
-
269
- # Build tools definition based on mode
270
- if mode == "feedback":
271
- return header + common_tools + feedback_only_tools + footer
272
- else: # sales mode (default)
273
- return header + common_tools + sales_only_tools + footer
274
 
275
  def _build_messages(
276
  self,
@@ -289,9 +298,14 @@ Example:
289
 
290
  return messages
291
 
292
- async def _call_llm(self, messages: List[Dict]) -> str:
293
  """
294
- Call HuggingFace LLM directly using chat_completion (conversational)
 
 
 
 
 
295
  """
296
  try:
297
  from huggingface_hub import AsyncInferenceClient
@@ -299,39 +313,47 @@ Example:
299
  # Create async client
300
  client = AsyncInferenceClient(token=self.hf_token)
301
 
302
- # Call HF API with chat completion (conversational)
303
- response_text = ""
304
- async for message in await client.chat_completion(
305
- messages=messages, # Use messages directly
306
- model="openai/gpt-oss-20b", # GPT-OSS 20B
307
  max_tokens=512,
308
  temperature=0.7,
309
- stream=True
310
- ):
311
- if message.choices and message.choices[0].delta.content:
312
- response_text += message.choices[0].delta.content
 
 
313
 
314
- return response_text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
315
  except Exception as e:
316
  print(f"⚠️ LLM Call Error: {e}")
317
- return "Xin lỗi, tôi đang gặp chút vấn đề kỹ thuật. Bạn thử lại sau nhé!"
318
-
319
- def _messages_to_prompt(self, messages: List[Dict]) -> str:
320
- """Convert messages array to single prompt string"""
321
- prompt_parts = []
322
-
323
- for msg in messages:
324
- role = msg["role"]
325
- content = msg["content"]
326
-
327
- if role == "system":
328
- prompt_parts.append(f"[SYSTEM]\n{content}\n")
329
- elif role == "user":
330
- prompt_parts.append(f"[USER]\n{content}\n")
331
- elif role == "assistant":
332
- prompt_parts.append(f"[ASSISTANT]\n{content}\n")
333
-
334
- return "\n".join(prompt_parts)
335
 
336
  def _format_tool_result(self, tool_result: Dict) -> str:
337
  """Format tool result for feeding back to LLM"""
@@ -372,7 +394,7 @@ Example:
372
  for key, value in result.items():
373
  if key not in ["success", "error"]:
374
  formatted.append(f"{key}: {value}")
375
- return "\n".join(formatted)
376
 
377
  return str(result)
378
 
@@ -408,117 +430,3 @@ Example:
408
  formatted.append(f"{i}. {text[:100]}... (ID: {event_id})")
409
 
410
  return "\n".join(formatted) if formatted else "Không tìm thấy sự kiện phù hợp."
411
-
412
- def _parse_tool_call(self, llm_response: str) -> Optional[Dict]:
413
- """
414
- Parse LLM response to detect tool calls using structured JSON
415
-
416
- Returns:
417
- {"tool_name": "...", "arguments": {...}} or None
418
- """
419
- import json
420
- import re
421
-
422
- # Method 1: Look for JSON code block
423
- json_match = re.search(r'```json\s*(\{.*?\})\s*```', llm_response, re.DOTALL)
424
- if json_match:
425
- try:
426
- data = json.loads(json_match.group(1))
427
- return self._extract_tool_from_json(data)
428
- except json.JSONDecodeError:
429
- pass
430
-
431
- # Method 2: Look for inline JSON object
432
- # Find all potential JSON objects
433
- json_objects = re.findall(r'\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}', llm_response)
434
- for json_str in json_objects:
435
- try:
436
- data = json.loads(json_str)
437
- tool_call = self._extract_tool_from_json(data)
438
- if tool_call:
439
- return tool_call
440
- except json.JSONDecodeError:
441
- continue
442
-
443
- # Method 3: Nested JSON (for complex structures)
444
- try:
445
- # Find outermost curly braces
446
- if '{' in llm_response and '}' in llm_response:
447
- start = llm_response.find('{')
448
- # Find matching closing brace
449
- count = 0
450
- for i, char in enumerate(llm_response[start:], start):
451
- if char == '{':
452
- count += 1
453
- elif char == '}':
454
- count -= 1
455
- if count == 0:
456
- json_str = llm_response[start:i+1]
457
- data = json.loads(json_str)
458
- return self._extract_tool_from_json(data)
459
- except (json.JSONDecodeError, ValueError):
460
- pass
461
-
462
- return None
463
-
464
- def _extract_tool_from_json(self, data: dict) -> Optional[Dict]:
465
- """
466
- Extract tool call information from parsed JSON
467
-
468
- Supports multiple formats:
469
- - {"tool_call": "search_events", "arguments": {...}}
470
- - {"function": "search_events", "parameters": {...}}
471
- - {"name": "search_events", "args": {...}}
472
- """
473
- # Format 1: tool_call + arguments
474
- if "tool_call" in data and isinstance(data["tool_call"], str):
475
- return {
476
- "tool_name": data["tool_call"],
477
- "arguments": data.get("arguments", {})
478
- }
479
-
480
- # Format 2: function + parameters
481
- if "function" in data:
482
- return {
483
- "tool_name": data["function"],
484
- "arguments": data.get("parameters", data.get("arguments", {}))
485
- }
486
-
487
- # Format 3: name + args
488
- if "name" in data:
489
- return {
490
- "tool_name": data["name"],
491
- "arguments": data.get("args", data.get("arguments", {}))
492
- }
493
-
494
- # Format 4: Direct tool name as key
495
- valid_tools = ["search_events", "get_event_details", "get_purchased_events", "save_feedback", "save_lead"]
496
- for tool in valid_tools:
497
- if tool in data:
498
- return {
499
- "tool_name": tool,
500
- "arguments": data[tool] if isinstance(data[tool], dict) else {}
501
- }
502
-
503
- return None
504
-
505
- def _clean_response(self, response: str) -> str:
506
- """Remove JSON artifacts from final response"""
507
- # Remove JSON blocks
508
- if "```json" in response:
509
- response = response.split("```json")[0]
510
- if "```" in response:
511
- response = response.split("```")[0]
512
-
513
- # Remove tool call markers
514
- if "{" in response and "tool_call" in response:
515
- # Find the last natural sentence before JSON
516
- lines = response.split("\n")
517
- cleaned = []
518
- for line in lines:
519
- if "{" in line and "tool_call" in line:
520
- break
521
- cleaned.append(line)
522
- response = "\n".join(cleaned)
523
-
524
- return response.strip()
 
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__(
 
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
 
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
 
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
 
 
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,
 
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,
 
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
 
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="Qwen/Qwen2.5-72B-Instruct", # Use Qwen which supports tools
 
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"""
 
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
 
 
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."