minhvtt commited on
Commit
6597779
·
verified ·
1 Parent(s): 7caa85c

Update tools_service.py

Browse files
Files changed (1) hide show
  1. tools_service.py +169 -204
tools_service.py CHANGED
@@ -1,204 +1,169 @@
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
- QUAN TRỌNG:
11
- - event_code PHẢI LÀ metadata.id_use từ context (dạng MongoDB ObjectId)
12
- - KHÔNG dùng tên sự kiện như "Y-CONCERT" làm event_code
13
- - CHỈ trả JSON khi BẮT BUỘC cần gọi tool
14
- - Nếu có thể trả lời từ context sẵn có, đừng gọi tool
15
- - Sau khi nhận kết quả từ tool, hãy trả lời user bằng ngôn ngữ tự nhiên
16
- """
17
-
18
- async def parse_and_execute(self, llm_response: str) -> Optional[Dict[str, Any]]:
19
- """
20
- Parse LLM response và execute tool nếu có
21
-
22
- Returns:
23
- None nếu không tool call
24
- Dict với tool result nếu có tool call
25
- """
26
- # Try to extract JSON from response
27
- try:
28
- # Tìm JSON block trong response
29
- if "```json" in llm_response:
30
- json_start = llm_response.find("```json") + 7
31
- json_end = llm_response.find("```", json_start)
32
- json_str = llm_response[json_start:json_end].strip()
33
- elif "{" in llm_response and "}" in llm_response:
34
- # Fallback: tìm JSON object đầu tiên
35
- json_start = llm_response.find("{")
36
- json_end = llm_response.rfind("}") + 1
37
- json_str = llm_response[json_start:json_end]
38
- else:
39
- return None
40
-
41
- tool_call = json.loads(json_str)
42
-
43
- # Handle multiple JSON formats from LLM
44
-
45
- # Format 1: HF API nested wrapper
46
- # {"name": "tool_call", "arguments": {"tool_call": true, ...}}
47
- if "name" in tool_call and "arguments" in tool_call and isinstance(tool_call["arguments"], dict):
48
- if "tool_call" in tool_call["arguments"]:
49
- tool_call = tool_call["arguments"] # Unwrap
50
-
51
- # Format 2: Direct tool name format
52
- # {"name": "tool.get_event_details", "arguments": {"event_code": "..."}}
53
- if "name" in tool_call and "arguments" in tool_call:
54
- function_name = tool_call["name"]
55
- # Remove "tool." prefix if exists
56
- if function_name.startswith("tool."):
57
- function_name = function_name.replace("tool.", "")
58
-
59
- # Convert to standard format
60
- tool_call = {
61
- "tool_call": True,
62
- "function_name": function_name,
63
- "arguments": tool_call["arguments"],
64
- "reason": "Converted from alternate format"
65
- }
66
-
67
- # Validate tool call structure
68
- if not tool_call.get("tool_call"):
69
- return None
70
-
71
- function_name = tool_call.get("function_name")
72
- arguments = tool_call.get("arguments", {})
73
-
74
- # Execute tool
75
- if function_name == "get_event_details":
76
- result = await self._get_event_details(arguments.get("event_code"))
77
- return {
78
- "function": function_name,
79
- "arguments": arguments,
80
- "result": result
81
- }
82
- else:
83
- return {
84
- "function": function_name,
85
- "arguments": arguments,
86
- "result": {"success": False, "error": f"Unknown function: {function_name}"}
87
- }
88
-
89
- except (json.JSONDecodeError, KeyError, ValueError) as e:
90
- # Không phải tool call, response bình thường
91
- return None
92
-
93
- async def _get_event_details(self, event_code: str) -> Dict[str, Any]:
94
- """
95
- Call getEventByEventCode API
96
- """
97
- print(f"\n=== CALLING API get_event_details ===")
98
- print(f"Event Code: {event_code}")
99
-
100
- try:
101
- url = f"https://hoalacrent.io.vn/api/v0/event/get-event-by-event-code"
102
- params = {"eventCode": event_code}
103
-
104
- print(f"URL: {url}")
105
- print(f"Params: {params}")
106
-
107
- response = await self.client.get(url, params=params)
108
-
109
- print(f"Status Code: {response.status_code}")
110
-
111
- # Log raw response for debugging
112
- raw_text = response.text
113
- print(f"Raw Response Length: {len(raw_text)} chars")
114
- print(f"Raw Response Preview (first 200 chars): {raw_text[:200]}")
115
-
116
- response.raise_for_status()
117
-
118
- # Try to parse JSON
119
- try:
120
- data = response.json()
121
- except json.JSONDecodeError as e:
122
- print(f"JSON Decode Error: {e}")
123
- print(f"Full Raw Response: {raw_text}")
124
- return {
125
- "success": False,
126
- "error": f"Invalid JSON response from API",
127
- "message": "API trả về dữ liệu không hợp lệ (không phải JSON)",
128
- "raw_response_preview": raw_text[:500]
129
- }
130
-
131
- print(f"Response Data Keys: {list(data.keys()) if data else 'None'}")
132
- print(f"Has 'data' field: {'data' in data}")
133
-
134
- # Extract relevant fields
135
- event = data.get("data", {})
136
-
137
- if not event:
138
- return {
139
- "success": False,
140
- "error": "Event not found",
141
- "message": f"Không tìm thấy sự kiện với {event_code}"
142
- }
143
-
144
- # Extract location với nested address structure
145
- location_data = event.get("location", {})
146
- location = {
147
- "address": {
148
- "street": location_data.get("address", {}).get("street", ""),
149
- "city": location_data.get("address", {}).get("city", ""),
150
- "state": location_data.get("address", {}).get("state", ""),
151
- "postalCode": location_data.get("address", {}).get("postalCode", ""),
152
- "country": location_data.get("address", {}).get("country", "")
153
- },
154
- "coordinates": {
155
- "latitude": location_data.get("coordinates", {}).get("latitude"),
156
- "longitude": location_data.get("coordinates", {}).get("longitude")
157
- }
158
- }
159
-
160
- # Build event URL
161
- event_code = event.get("eventCode")
162
- event_url = f"https://www.festavenue.site/user/event/{event_code}" if event_code else None
163
-
164
- return {
165
- "success": True,
166
- "event_code": event_code,
167
- "event_name": event.get("eventName"),
168
- "event_url": event_url, # NEW: Direct link to event page
169
- "description": event.get("description"),
170
- "short_description": event.get("shortDescription"),
171
- "start_time": event.get("startTimeEventTime"),
172
- "end_time": event.get("endTimeEventTime"),
173
- "start_sale": event.get("startTicketSaleTime"),
174
- "end_sale": event.get("endTicketSaleTime"),
175
- "location": location, # Full nested structure
176
- "contact": {
177
- "email": event.get("publicContactEmail"),
178
- "phone": event.get("publicContactPhone"),
179
- "website": event.get("website")
180
- },
181
- "capacity": event.get("capacity"),
182
- "hashtags": event.get("hashtags", [])
183
- }
184
-
185
- print(f"Successfully extracted event data for: {event.get('eventName')}")
186
- print(f"=== API CALL COMPLETE ===")
187
- return result
188
-
189
- except httpx.HTTPStatusError as e:
190
- return {
191
- "success": False,
192
- "error": f"HTTP {e.response.status_code}",
193
- "message": f"API trả về lỗi khi truy vấn sự kiện {event_code}"
194
- }
195
- except Exception as e:
196
- return {
197
- "success": False,
198
- "error": str(e),
199
- "message": "Không thể kết nối đến API để lấy thông tin sự kiện"
200
- }
201
-
202
- async def close(self):
203
- """Close HTTP client"""
204
- await self.client.aclose()
 
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"):
17
+ self.base_url = base_url
18
+ self.client = httpx.AsyncClient(timeout=10.0)
19
+
20
+ def get_tools_definition(self) -> List[Dict]:
21
+ """
22
+ Return list of tool definitions (OpenAI format style)
23
+ Used for constructing system prompt
24
+ """
25
+ return [
26
+ {
27
+ "name": "search_events",
28
+ "description": "Tìm kiếm sự kiện phù hợp theo từ khóa, vibe, hoặc thời gian.",
29
+ "parameters": {
30
+ "type": "object",
31
+ "properties": {
32
+ "query": {"type": "string", "description": "Từ khóa tìm kiếm (VD: 'nhạc rock', 'hài kịch')"},
33
+ "vibe": {"type": "string", "description": "Vibe/Mood (VD: 'chill', 'sôi động', 'hẹn hò')"},
34
+ "time": {"type": "string", "description": "Thời gian (VD: 'cuối tuần này', 'tối nay')"}
35
+ }
36
+ }
37
+ },
38
+ {
39
+ "name": "get_event_details",
40
+ "description": "Lấy thông tin chi tiết (giá, địa điểm, thời gian) của sự kiện.",
41
+ "parameters": {
42
+ "type": "object",
43
+ "properties": {
44
+ "event_id": {"type": "string", "description": "ID của sự kiện (MongoDB ID)"}
45
+ },
46
+ "required": ["event_id"]
47
+ }
48
+ },
49
+ {
50
+ "name": "get_purchased_events",
51
+ "description": "Kiểm tra lịch sử các sự kiện user đã mua vé hoặc tham gia.",
52
+ "parameters": {
53
+ "type": "object",
54
+ "properties": {
55
+ "user_id": {"type": "string", "description": "ID của user"}
56
+ },
57
+ "required": ["user_id"]
58
+ }
59
+ },
60
+ {
61
+ "name": "save_feedback",
62
+ "description": "Lưu đánh giá/feedback của user về sự kiện.",
63
+ "parameters": {
64
+ "type": "object",
65
+ "properties": {
66
+ "event_id": {"type": "string", "description": "ID sự kiện"},
67
+ "rating": {"type": "integer", "description": "Số sao đánh giá (1-5)"},
68
+ "comment": {"type": "string", "description": "Nội dung nhận xét"}
69
+ },
70
+ "required": ["event_id", "rating"]
71
+ }
72
+ },
73
+ {
74
+ "name": "save_lead",
75
+ "description": "Lưu thông tin khách hàng quan tâm (Lead).",
76
+ "parameters": {
77
+ "type": "object",
78
+ "properties": {
79
+ "email": {"type": "string"},
80
+ "phone": {"type": "string"},
81
+ "interest": {"type": "string"}
82
+ }
83
+ }
84
+ }
85
+ ]
86
+
87
+ async def execute_tool(self, tool_name: str, arguments: Dict) -> Any:
88
+ """
89
+ Execute a tool by name with arguments
90
+ """
91
+ print(f"🔧 Executing Tool: {tool_name} with args: {arguments}")
92
+
93
+ try:
94
+ if tool_name == "get_event_details":
95
+ return await self._get_event_details(arguments.get("event_id") or arguments.get("event_code"))
96
+
97
+ elif tool_name == "get_purchased_events":
98
+ return await self._get_purchased_events(arguments.get("user_id"))
99
+
100
+ elif tool_name == "save_feedback":
101
+ return await self._save_feedback(
102
+ arguments.get("event_id"),
103
+ arguments.get("rating"),
104
+ arguments.get("comment")
105
+ )
106
+
107
+ elif tool_name == "search_events":
108
+ # Note: This usually requires RAG service, so we return a special signal
109
+ # The Agent Service will handle RAG search
110
+ return {"action": "run_rag_search", "query": arguments}
111
+
112
+ elif tool_name == "save_lead":
113
+ # Placeholder for lead saving
114
+ return {"success": True, "message": "Lead saved successfully"}
115
+
116
+ else:
117
+ return {"error": f"Unknown tool: {tool_name}"}
118
+
119
+ except Exception as e:
120
+ print(f"⚠️ Tool Execution Error: {e}")
121
+ return {"error": str(e)}
122
+
123
+ async def _get_event_details(self, event_id: str) -> Dict:
124
+ """Call API to get event details"""
125
+ if not event_id:
126
+ return {"error": "Missing event_id"}
127
+
128
+ try:
129
+ url = f"{self.base_url}/event/get-event-by-id"
130
+
131
+ response = await self.client.get(url, params={"id": event_id})
132
+ if response.status_code == 200:
133
+ data = response.json()
134
+ if data.get("success"):
135
+ return data.get("data")
136
+ return {"error": "Event not found", "details": response.text}
137
+ except Exception as e:
138
+ return {"error": str(e)}
139
+
140
+ async def _get_purchased_events(self, user_id: str) -> List[Dict]:
141
+ """Call API to get purchased events for user"""
142
+ if not user_id:
143
+ return []
144
+
145
+ try:
146
+ url = f"{self.base_url}/event/get-purchase-event-by-user-id/{user_id}"
147
+ print(f"🔍 Calling API: {url}")
148
+
149
+ response = await self.client.get(url)
150
+ if response.status_code == 200:
151
+ data = response.json()
152
+ # API returns {data: [...]}
153
+ return data.get("data", [])
154
+
155
+ print(f"⚠️ API Error: {response.status_code} - {response.text}")
156
+ return []
157
+ except Exception as e:
158
+ print(f"⚠️ API Exception: {e}")
159
+ return []
160
+
161
+ async def _save_feedback(self, event_id: str, rating: int, comment: str) -> Dict:
162
+ """Save feedback (Mock or Real API)"""
163
+ # TODO: Implement real API call when available
164
+ print(f"📝 Saving Feedback: Event={event_id}, Rating={rating}, Comment={comment}")
165
+ return {"success": True, "message": "Feedback recorded"}
166
+
167
+ async def close(self):
168
+ """Close HTTP client"""
169
+ await self.client.aclose()