XiaoBai1221 commited on
Commit
69f3746
·
1 Parent(s): 4190920

Update app.py, geocode_tool.py and tools.js

Browse files
app.py CHANGED
@@ -68,6 +68,37 @@ from core.memory_system import memory_manager
68
  from core.database import set_user_env_current, add_user_env_snapshot
69
 
70
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  # -----------------------------
72
  # Pydantic 模型
73
  # -----------------------------
@@ -812,6 +843,10 @@ async def websocket_endpoint_with_jwt(websocket: WebSocket, token: str = Query(N
812
  emotion = response.get('emotion') # 新增:提取情緒
813
  care_mode = response.get('care_mode', False) # 新增:提取關懷模式
814
 
 
 
 
 
815
  # 先發送情緒資訊(如果有)
816
  if emotion:
817
  await websocket.send_json({
@@ -840,8 +875,11 @@ async def websocket_endpoint_with_jwt(websocket: WebSocket, token: str = Query(N
840
  "title": new_chat_info["title"]
841
  })
842
 
 
843
  await save_message_to_db(user_id, chat_id, "user", user_message)
844
- await save_message_to_db(user_id, chat_id, "assistant", response)
 
 
845
 
846
  import asyncio as _asyncio
847
  _asyncio.create_task(_do_process_and_send())
 
68
  from core.database import set_user_env_current, add_user_env_snapshot
69
 
70
 
71
+ # -----------------------------
72
+ # 工具函式
73
+ # -----------------------------
74
+ def serialize_for_json(obj: Any) -> Any:
75
+ """
76
+ 遞迴序列化物件,將不可 JSON 序列化的型別轉換為可序列化格式
77
+ - DatetimeWithNanoseconds → ISO 字串
78
+ - datetime → ISO 字串
79
+ - bytes → base64 字串
80
+ - 其他物件 → str()
81
+ """
82
+ from google.cloud.firestore_v1._helpers import DatetimeWithNanoseconds
83
+ from datetime import datetime, date
84
+
85
+ if isinstance(obj, (DatetimeWithNanoseconds, datetime, date)):
86
+ return obj.isoformat()
87
+ elif isinstance(obj, bytes):
88
+ return base64.b64encode(obj).decode('utf-8')
89
+ elif isinstance(obj, dict):
90
+ return {k: serialize_for_json(v) for k, v in obj.items()}
91
+ elif isinstance(obj, (list, tuple)):
92
+ return [serialize_for_json(item) for item in obj]
93
+ elif isinstance(obj, (str, int, float, bool, type(None))):
94
+ return obj
95
+ else:
96
+ # 未知型別:嘗試轉字串
97
+ try:
98
+ return str(obj)
99
+ except Exception:
100
+ return None
101
+
102
  # -----------------------------
103
  # Pydantic 模型
104
  # -----------------------------
 
843
  emotion = response.get('emotion') # 新增:提取情緒
844
  care_mode = response.get('care_mode', False) # 新增:提取關懷模式
845
 
846
+ # 序列化 tool_data(避免 DatetimeWithNanoseconds 等不可序列化物件)
847
+ if tool_data is not None:
848
+ tool_data = serialize_for_json(tool_data)
849
+
850
  # 先發送情緒資訊(如果有)
851
  if emotion:
852
  await websocket.send_json({
 
875
  "title": new_chat_info["title"]
876
  })
877
 
878
+ # 保存訊息(只儲存文字內容)
879
  await save_message_to_db(user_id, chat_id, "user", user_message)
880
+ # 如果 response dict,只保存 message 欄位
881
+ message_to_save = response.get('message', response) if isinstance(response, dict) else response
882
+ await save_message_to_db(user_id, chat_id, "assistant", message_to_save)
883
 
884
  import asyncio as _asyncio
885
  _asyncio.create_task(_do_process_and_send())
features/mcp/tools/geocode_tool.py CHANGED
@@ -57,6 +57,20 @@ class ReverseGeocodeTool(MCPTool):
57
  # 記憶體快取
58
  cached = await db_cache.get_geo_cached(geokey)
59
  if cached:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  return cls.create_success_response(
61
  content=cached.get("display_name") or f"{cached.get('city')}, {cached.get('admin')}",
62
  data=cached
@@ -65,6 +79,20 @@ class ReverseGeocodeTool(MCPTool):
65
  # DB 快取
66
  db_cached = await get_geo_cache(geokey)
67
  if db_cached:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  await db_cache.set_geo_cache(geokey, db_cached)
69
  return cls.create_success_response(
70
  content=db_cached.get("display_name") or f"{db_cached.get('city')}, {db_cached.get('admin')}",
@@ -176,6 +204,8 @@ class ReverseGeocodeTool(MCPTool):
176
  detailed_address = " | ".join(detailed_address_parts) if detailed_address_parts else label
177
 
178
  payload = {
 
 
179
  "city": city or "",
180
  "admin": admin or "",
181
  "country_code": country_code,
 
57
  # 記憶體快取
58
  cached = await db_cache.get_geo_cached(geokey)
59
  if cached:
60
+ # 補齊缺失欄位(兼容舊版快取)
61
+ cached.setdefault("lat", lat) # ← 補上座標
62
+ cached.setdefault("lon", lon) # ← 補上座標
63
+ cached.setdefault("name", "")
64
+ cached.setdefault("detailed_address", cached.get("label") or cached.get("display_name") or "")
65
+ cached.setdefault("postcode", "")
66
+ cached.setdefault("city_district", "")
67
+ cached.setdefault("amenity", "")
68
+ cached.setdefault("shop", "")
69
+ cached.setdefault("building", "")
70
+ cached.setdefault("office", "")
71
+ cached.setdefault("leisure", "")
72
+ cached.setdefault("tourism", "")
73
+
74
  return cls.create_success_response(
75
  content=cached.get("display_name") or f"{cached.get('city')}, {cached.get('admin')}",
76
  data=cached
 
79
  # DB 快取
80
  db_cached = await get_geo_cache(geokey)
81
  if db_cached:
82
+ # 補齊缺失欄位(兼容舊版快取)
83
+ db_cached.setdefault("lat", lat) # ← 補上座標
84
+ db_cached.setdefault("lon", lon) # ← 補上座標
85
+ db_cached.setdefault("name", "")
86
+ db_cached.setdefault("detailed_address", db_cached.get("label") or db_cached.get("display_name") or "")
87
+ db_cached.setdefault("postcode", "")
88
+ db_cached.setdefault("city_district", "")
89
+ db_cached.setdefault("amenity", "")
90
+ db_cached.setdefault("shop", "")
91
+ db_cached.setdefault("building", "")
92
+ db_cached.setdefault("office", "")
93
+ db_cached.setdefault("leisure", "")
94
+ db_cached.setdefault("tourism", "")
95
+
96
  await db_cache.set_geo_cache(geokey, db_cached)
97
  return cls.create_success_response(
98
  content=db_cached.get("display_name") or f"{db_cached.get('city')}, {db_cached.get('admin')}",
 
204
  detailed_address = " | ".join(detailed_address_parts) if detailed_address_parts else label
205
 
206
  payload = {
207
+ "lat": lat, # ← 新增:補上座標
208
+ "lon": lon, # ← 新增:補上座標
209
  "city": city or "",
210
  "admin": admin or "",
211
  "country_code": country_code,
static/frontend/js/tools.js CHANGED
@@ -522,7 +522,9 @@ function renderExchangeRate(data) {
522
  * 渲染地理定位數據(forward_geocode / reverse_geocode)
523
  */
524
  function renderLocationData(data) {
525
- const bestMatch = data.best_match || {};
 
 
526
  const results = data.results || [];
527
  const query = data.query || '';
528
 
@@ -538,32 +540,42 @@ function renderLocationData(data) {
538
  `;
539
  }
540
 
541
- // 最佳匹配地點
542
- if (bestMatch.label || bestMatch.display_name) {
543
  html += `
544
  <div class="data-row">
545
- <span class="data-label">📍 地點</span>
546
- <span class="data-value">${bestMatch.label || bestMatch.display_name}</span>
547
  </div>
548
  `;
549
  }
550
 
551
- // 座標
552
- if (bestMatch.lat !== undefined && bestMatch.lon !== undefined) {
 
 
 
553
  html += `
554
  <div class="data-row">
555
- <span class="data-label">🌐 座標</span>
556
- <span class="data-value">${bestMatch.lat.toFixed(6)}, ${bestMatch.lon.toFixed(6)}</span>
557
  </div>
558
  `;
559
  }
560
 
561
- // 詳細地址(如果與 label 不同)
562
- if (bestMatch.detailed_address && bestMatch.detailed_address !== bestMatch.label) {
 
 
 
 
 
 
 
563
  html += `
564
  <div class="data-row">
565
- <span class="data-label">🏠 詳細</span>
566
- <span class="data-value" style="font-size: 0.9em;">${bestMatch.detailed_address}</span>
567
  </div>
568
  `;
569
  }
 
522
  * 渲染地理定位數據(forward_geocode / reverse_geocode)
523
  */
524
  function renderLocationData(data) {
525
+ // reverse_geocode: 扁平結構(欄位在第一層)
526
+ // forward_geocode: 巢狀結構(best_match + results)
527
+ const bestMatch = data.best_match || data; // ← 兼容兩種結構
528
  const results = data.results || [];
529
  const query = data.query || '';
530
 
 
540
  `;
541
  }
542
 
543
+ // 地點名稱(POI、建築物等)
544
+ if (bestMatch.name && bestMatch.name !== bestMatch.road) {
545
  html += `
546
  <div class="data-row">
547
+ <span class="data-label">� 地點</span>
548
+ <span class="data-value">${bestMatch.name}</span>
549
  </div>
550
  `;
551
  }
552
 
553
+ // 地址(路名 + 門牌號)
554
+ if (bestMatch.road) {
555
+ const address = bestMatch.house_number
556
+ ? `${bestMatch.road}${bestMatch.house_number}號`
557
+ : bestMatch.road;
558
  html += `
559
  <div class="data-row">
560
+ <span class="data-label">� 地址</span>
561
+ <span class="data-value">${address}</span>
562
  </div>
563
  `;
564
  }
565
 
566
+ // 區域 + 城市
567
+ const locationParts = [];
568
+ if (bestMatch.suburb) locationParts.push(bestMatch.suburb);
569
+ if (bestMatch.city_district && bestMatch.city_district !== bestMatch.suburb) {
570
+ locationParts.push(bestMatch.city_district);
571
+ }
572
+ if (bestMatch.city) locationParts.push(bestMatch.city);
573
+
574
+ if (locationParts.length > 0) {
575
  html += `
576
  <div class="data-row">
577
+ <span class="data-label">📍 位置</span>
578
+ <span class="data-value">${locationParts.join(', ')}</span>
579
  </div>
580
  `;
581
  }