Marco310 commited on
Commit
25c25cb
·
1 Parent(s): 8056363

hotfix: replace Google Maps API with local estimation in optimizer

Browse files

- Disabled Google Maps `Compute Routes` and `Distance Matrix` APIs in the optimization loop to prevent billing overages.
- Swapped the underlying algorithm with a local Haversine-based distance estimator.
- This is an emergency fix to resolve the high API usage incident.

src/optimization/graph/graph_builder.py CHANGED
@@ -8,7 +8,7 @@ from typing import List, Dict, Tuple, Any
8
  import numpy as np
9
 
10
  from src.infra.logger import get_logger
11
- from src.services.googlemap_api_service import GoogleMapAPIService
12
 
13
  from ..models.internal_models import _Task, _Location, _Graph
14
 
@@ -25,8 +25,8 @@ class GraphBuilder:
25
  - 計算距離/時間矩陣
26
  """
27
 
28
- def __init__(self, gmaps: GoogleMapAPIService):
29
- self.gmaps = gmaps
30
 
31
  def build_graph(
32
  self,
@@ -59,7 +59,7 @@ class GraphBuilder:
59
  duration_matrix = np.zeros((1, 1), dtype=int)
60
  distance_matrix = np.zeros((1, 1), dtype=int)
61
  else:
62
- duration_matrix, distance_matrix = self._calculate_matrices(locations, travel_mode=travel_mode)
63
 
64
  # 3. 返回圖
65
  return _Graph(
@@ -122,7 +122,6 @@ class GraphBuilder:
122
  def _calculate_matrices(
123
  self,
124
  locations: List[Dict[str, float]],
125
- travel_mode="DRIVE",
126
  ) -> Tuple[np.ndarray, np.ndarray]:
127
  """
128
  計算距離/時間矩陣
@@ -134,12 +133,9 @@ class GraphBuilder:
134
  """
135
  locations_dict = [{"lat": loc["lat"], "lng": loc["lng"]} for loc in locations]
136
 
137
- compute_route_result = self.gmaps.compute_route_matrix(
138
- locations_dict,
139
- locations_dict,
140
- travel_mode=travel_mode,
141
- routing_preference="TRAFFIC_UNAWARE",
142
- )
143
 
144
  duration_matrix = np.array(compute_route_result["duration_matrix"])
145
  distance_matrix = np.array(compute_route_result["distance_matrix"])
 
8
  import numpy as np
9
 
10
  from src.infra.logger import get_logger
11
+ from src.services.local_route_estimator import LocalRouteEstimator
12
 
13
  from ..models.internal_models import _Task, _Location, _Graph
14
 
 
25
  - 計算距離/時間矩陣
26
  """
27
 
28
+ def __init__(self, **kwargs):
29
+ self.estimator = LocalRouteEstimator()
30
 
31
  def build_graph(
32
  self,
 
59
  duration_matrix = np.zeros((1, 1), dtype=int)
60
  distance_matrix = np.zeros((1, 1), dtype=int)
61
  else:
62
+ duration_matrix, distance_matrix = self._calculate_matrices(locations)
63
 
64
  # 3. 返回圖
65
  return _Graph(
 
122
  def _calculate_matrices(
123
  self,
124
  locations: List[Dict[str, float]],
 
125
  ) -> Tuple[np.ndarray, np.ndarray]:
126
  """
127
  計算距離/時間矩陣
 
133
  """
134
  locations_dict = [{"lat": loc["lat"], "lng": loc["lng"]} for loc in locations]
135
 
136
+ compute_route_result = self.estimator.compute_route_matrix(
137
+ origins=locations_dict,
138
+ destinations=locations_dict)
 
 
 
139
 
140
  duration_matrix = np.array(compute_route_result["duration_matrix"])
141
  distance_matrix = np.array(compute_route_result["distance_matrix"])
src/optimization/test/_solver_test.py CHANGED
@@ -311,8 +311,7 @@ def main():
311
  from src.infra.config import get_settings
312
  settings = get_settings()
313
  # 創建求解器
314
- solver = TSPTWSolver(api_key=settings.google_maps_api_key,
315
- time_limit_seconds=5, verbose=True)
316
 
317
  # 設置參數
318
  start_location = {"lat": 25.0400, "lng": 121.5300}
 
311
  from src.infra.config import get_settings
312
  settings = get_settings()
313
  # 創建求解器
314
+ solver = TSPTWSolver(time_limit_seconds=5, verbose=True)
 
315
 
316
  # 設置參數
317
  start_location = {"lat": 25.0400, "lng": 121.5300}
src/optimization/test/_test_convertes.py DELETED
@@ -1,255 +0,0 @@
1
- """
2
- 测试時間視窗转换和求解
3
-
4
- 验证修复是否正確
5
- """
6
- from datetime import datetime
7
-
8
-
9
- def test_parse_time_window():
10
- """测试時間視窗解析"""
11
- print("=" * 60)
12
- print("测试 _parse_time_window()")
13
- print("=" * 60)
14
- print()
15
-
16
- from src.optimization.models.converters import _parse_time_window
17
-
18
- # 测试 1: Dict 格式(完整)
19
- print("测试 1: Dict 格式(完整時間視窗)")
20
- tw_dict = {
21
- "earliest_time": "2025-11-19T16:00:00",
22
- "latest_time": "2025-11-19T18:00:00"
23
- }
24
- result = _parse_time_window(tw_dict)
25
- print(f" input: {tw_dict}")
26
- print(f" output: {result}")
27
- assert result == (datetime(2025, 11, 19, 16, 0), datetime(2025, 11, 19, 18, 0))
28
- print(" ✅ passed\n")
29
-
30
- # 测试 2: Dict 格式(部分時間視窗 - 只有结束)
31
- print("测试 2: Dict 格式(只有结束时间)")
32
- tw_dict = {
33
- "earliest_time": None,
34
- "latest_time": "2025-11-19T15:00:00"
35
- }
36
- result = _parse_time_window(tw_dict)
37
- print(f" input: {tw_dict}")
38
- print(f" output: {result}")
39
- assert result == (None, datetime(2025, 11, 19, 15, 0))
40
- print(" ✅ passed\n")
41
-
42
- # 测试 3: Dict 格式(部分時間視窗 - 只有开始)
43
- print("测试 3: Dict 格式(只有开始时间)")
44
- tw_dict = {
45
- "earliest_time": "2025-11-19T10:00:00",
46
- "latest_time": None
47
- }
48
- result = _parse_time_window(tw_dict)
49
- print(f" input: {tw_dict}")
50
- print(f" output: {result}")
51
- assert result == (datetime(2025, 11, 19, 10, 0), None)
52
- print(" ✅ passed\n")
53
-
54
- # 测试 4: Dict 格式(全 None)
55
- print("测试 4: Dict 格式(全 None)")
56
- tw_dict = {
57
- "earliest_time": None,
58
- "latest_time": None
59
- }
60
- result = _parse_time_window(tw_dict)
61
- print(f" input: {tw_dict}")
62
- print(f" output: {result}")
63
- assert result is None
64
- print(" ✅ passed\n")
65
-
66
- # 测试 5: None
67
- print("测试 5: None input")
68
- result = _parse_time_window(None)
69
- print(f" input: None")
70
- print(f" output: {result}")
71
- assert result is None
72
- print(" ✅ passed\n")
73
-
74
- # 测试 6: Tuple(兼容)
75
- print("测试 6: Tuple input(兼容旧代码)")
76
- tw_tuple = (datetime(2025, 11, 19, 16, 0), datetime(2025, 11, 19, 18, 0))
77
- result = _parse_time_window(tw_tuple)
78
- print(f" input: {tw_tuple}")
79
- print(f" output: {result}")
80
- assert result == tw_tuple
81
- print(" ✅ passed\n")
82
-
83
-
84
- def test_convert_tasks():
85
- """测试完整的任务转换"""
86
- print("=" * 60)
87
- print("测试 convert_tasks_to_internal()")
88
- print("=" * 60)
89
- print()
90
-
91
- from converters import convert_tasks_to_internal
92
-
93
- # 测试数据:模拟你的实际input
94
- tasks = [
95
- {
96
- "task_id": "task_1",
97
- "priority": "HIGH",
98
- "service_duration_min": 60,
99
- "time_window": {
100
- "earliest_time": "2025-11-19T16:00:00",
101
- "latest_time": "2025-11-19T18:00:00"
102
- },
103
- "candidates": [
104
- {
105
- "poi_id": "hospital_1",
106
- "lat": 25.040978499999998,
107
- "lng": 121.51919869999999
108
- }
109
- ]
110
- },
111
- {
112
- "task_id": "task_2",
113
- "priority": "MEDIUM",
114
- "service_duration_min": 45,
115
- "time_window": None,
116
- "candidates": [
117
- {
118
- "poi_id": "supermarket_1",
119
- "lat": 25.0376995,
120
- "lng": 121.50622589999999
121
- }
122
- ]
123
- },
124
- {
125
- "task_id": "task_3",
126
- "priority": "HIGH",
127
- "service_duration_min": 20,
128
- "time_window": {
129
- "earliest_time": None,
130
- "latest_time": "2025-11-19T15:00:00"
131
- },
132
- "candidates": [
133
- {
134
- "poi_id": "post_office_1",
135
- "lat": 25.0514598,
136
- "lng": 121.5481353
137
- }
138
- ]
139
- }
140
- ]
141
-
142
- # 转换
143
- internal_tasks = convert_tasks_to_internal(tasks)
144
-
145
- # 验证
146
- print("测试 1: task_1 (看病 16:00-18:00)")
147
- print(f" task_id: {internal_tasks[0].task_id}")
148
- print(f" priority: {internal_tasks[0].priority}")
149
- print(f" time_window: {internal_tasks[0].time_window}")
150
- assert internal_tasks[0].time_window == (
151
- datetime(2025, 11, 19, 16, 0),
152
- datetime(2025, 11, 19, 18, 0)
153
- )
154
- print(" ✅ passed\n")
155
-
156
- print("测试 2: task_2 (买菜 无限制)")
157
- print(f" task_id: {internal_tasks[1].task_id}")
158
- print(f" priority: {internal_tasks[1].priority}")
159
- print(f" time_window: {internal_tasks[1].time_window}")
160
- assert internal_tasks[1].time_window is None
161
- print(" ✅ passed\n")
162
-
163
- print("测试 3: task_3 (寄包裹 None-15:00)")
164
- print(f" task_id: {internal_tasks[2].task_id}")
165
- print(f" priority: {internal_tasks[2].priority}")
166
- print(f" time_window: {internal_tasks[2].time_window}")
167
- assert internal_tasks[2].time_window == (
168
- None,
169
- datetime(2025, 11, 19, 15, 0)
170
- )
171
- print(" ✅ passed\n")
172
-
173
-
174
- def test_time_window_seconds():
175
- """测试時間視窗转换为秒"""
176
- print("=" * 60)
177
- print("测试時間視窗秒数计算")
178
- print("=" * 60)
179
- print()
180
-
181
- from converters import _parse_time_window
182
- from datetime import timedelta
183
-
184
- # 开始时间: 08:00
185
- start_time = datetime(2025, 11, 19, 8, 0)
186
-
187
- # task_1: 16:00-18:00
188
- tw1 = _parse_time_window({
189
- "earliest_time": "2025-11-19T16:00:00",
190
- "latest_time": "2025-11-19T18:00:00"
191
- })
192
-
193
- start_sec = int((tw1[0] - start_time).total_seconds())
194
- end_sec = int((tw1[1] - start_time).total_seconds())
195
-
196
- print("task_1 時間視窗:")
197
- print(f" 原始: 16:00-18:00")
198
- print(f" 相对 08:00: {start_sec}秒 - {end_sec}秒")
199
- print(f" 即: {start_sec / 3600:.1f}小时 - {end_sec / 3600:.1f}小时")
200
-
201
- assert start_sec == 8 * 3600 # 8小时 = 28800秒
202
- assert end_sec == 10 * 3600 # 10小时 = 36000秒
203
- print(" ✅ passed\n")
204
-
205
- # task_3: None - 15:00
206
- tw3 = _parse_time_window({
207
- "earliest_time": None,
208
- "latest_time": "2025-11-19T15:00:00"
209
- })
210
-
211
- print("task_3 時間視窗:")
212
- print(f" 原始: None - 15:00")
213
- if tw3[0] is None:
214
- print(f" start: 0秒 (无限制)")
215
- if tw3[1] is not None:
216
- end_sec = int((tw3[1] - start_time).total_seconds())
217
- print(f" end: {end_sec}秒 = {end_sec / 3600:.1f}小时")
218
- assert end_sec == 7 * 3600 # 7小时 = 25200秒
219
- print(" ✅ passed\n")
220
-
221
-
222
- def main():
223
- """运行所有测试"""
224
- print("\n")
225
- print("╔" + "=" * 58 + "╗")
226
- print("║" + " " * 15 + "時間視窗转换测试" + " " * 15 + "║")
227
- print("╚" + "=" * 58 + "╝")
228
- print("\n")
229
-
230
- try:
231
- test_parse_time_window()
232
- test_convert_tasks()
233
- test_time_window_seconds()
234
-
235
- print("\n")
236
- print("╔" + "=" * 58 + "╗")
237
- print("║" + " " * 18 + "✅ 所有测试passed" + " " * 18 + "║")
238
- print("╚" + "=" * 58 + "╝")
239
- print("\n")
240
-
241
- print("总结:")
242
- print("✅ Dict 格式解析正確")
243
- print("✅ 部分時間視窗支持")
244
- print("✅ 完整任务转换正確")
245
- print("✅ 时间秒数计算正確")
246
- print("\n")
247
-
248
- except Exception as e:
249
- print(f"\n❌ 測試失敗: {e}")
250
- import traceback
251
- traceback.print_exc()
252
-
253
-
254
- if __name__ == "__main__":
255
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/optimization/tsptw_solver.py CHANGED
@@ -36,7 +36,6 @@ class TSPTWSolver:
36
 
37
  def __init__(
38
  self,
39
- api_key: Optional[str] = None,
40
  time_limit_seconds: Optional[int] = None,
41
  verbose: bool = False,
42
  ):
@@ -54,14 +53,13 @@ class TSPTWSolver:
54
  or "1"
55
  )
56
 
57
- self.gmaps = GoogleMapAPIService(api_key=api_key)
58
  self.time_limit_seconds = (
59
  time_limit_seconds if time_limit_seconds is not None else int(env_limit)
60
  )
61
  self.verbose = verbose
62
 
63
  # 初始化各模塊
64
- self.graph_builder = GraphBuilder(gmaps=self.gmaps)
65
  self.ortools_solver = ORToolsSolver(
66
  time_limit_seconds=self.time_limit_seconds,
67
  verbose=verbose,
 
36
 
37
  def __init__(
38
  self,
 
39
  time_limit_seconds: Optional[int] = None,
40
  verbose: bool = False,
41
  ):
 
53
  or "1"
54
  )
55
 
 
56
  self.time_limit_seconds = (
57
  time_limit_seconds if time_limit_seconds is not None else int(env_limit)
58
  )
59
  self.verbose = verbose
60
 
61
  # 初始化各模塊
62
+ self.graph_builder = GraphBuilder()
63
  self.ortools_solver = ORToolsSolver(
64
  time_limit_seconds=self.time_limit_seconds,
65
  verbose=verbose,
src/services/googlemap_api_service.py CHANGED
@@ -174,7 +174,6 @@ class GoogleMapAPIService:
174
  "places.userRatingCount,"
175
  "places.businessStatus,"
176
  "places.currentOpeningHours,"
177
- "places.userRatingCount,"
178
  )
179
  }
180
 
 
174
  "places.userRatingCount,"
175
  "places.businessStatus,"
176
  "places.currentOpeningHours,"
 
177
  )
178
  }
179
 
src/tools/optimizer_toolkit.py CHANGED
@@ -5,12 +5,15 @@ from datetime import datetime, timedelta
5
  from agno.tools import Toolkit
6
  from src.optimization.tsptw_solver import TSPTWSolver
7
  from src.infra.poi_repository import poi_repo
 
 
 
8
 
9
 
10
  class OptimizationToolkit(Toolkit):
11
- def __init__(self, google_maps_api_key: None):
12
  super().__init__(name="optimization_toolkit")
13
- self.solver = TSPTWSolver(api_key=google_maps_api_key)
14
  self.register(self.optimize_from_ref)
15
 
16
  def optimize_from_ref(self, ref_id: str) -> str:
@@ -74,7 +77,8 @@ class OptimizationToolkit(Toolkit):
74
  return_to_start=False
75
  )
76
  except Exception as e:
77
- return f"Solver Failed: {e}"
 
78
 
79
  # ✅ [Critical] 將 global_info 繼承下去!
80
  # 如果不加這一行,Navigator 就會因為找不到 departure_time 而報錯
 
5
  from agno.tools import Toolkit
6
  from src.optimization.tsptw_solver import TSPTWSolver
7
  from src.infra.poi_repository import poi_repo
8
+ from src.infra.logger import get_logger
9
+
10
+ logger = get_logger(__name__)
11
 
12
 
13
  class OptimizationToolkit(Toolkit):
14
+ def __init__(self):
15
  super().__init__(name="optimization_toolkit")
16
+ self.solver = TSPTWSolver()
17
  self.register(self.optimize_from_ref)
18
 
19
  def optimize_from_ref(self, ref_id: str) -> str:
 
77
  return_to_start=False
78
  )
79
  except Exception as e:
80
+ logger.warning(f"Solver failed: {e}")
81
+ return f"❌ Solver Failed: {e}, Please check the input data."
82
 
83
  # ✅ [Critical] 將 global_info 繼承下去!
84
  # 如果不加這一行,Navigator 就會因為找不到 departure_time 而報錯