Spaces:
Running
Running
Commit
·
69f3746
1
Parent(s):
4190920
Update app.py, geocode_tool.py and tools.js
Browse files- app.py +39 -1
- features/mcp/tools/geocode_tool.py +30 -0
- static/frontend/js/tools.js +25 -13
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 |
-
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 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.
|
| 543 |
html += `
|
| 544 |
<div class="data-row">
|
| 545 |
-
<span class="data-label"
|
| 546 |
-
<span class="data-value">${bestMatch.
|
| 547 |
</div>
|
| 548 |
`;
|
| 549 |
}
|
| 550 |
|
| 551 |
-
//
|
| 552 |
-
if (bestMatch.
|
|
|
|
|
|
|
|
|
|
| 553 |
html += `
|
| 554 |
<div class="data-row">
|
| 555 |
-
<span class="data-label"
|
| 556 |
-
<span class="data-value">${
|
| 557 |
</div>
|
| 558 |
`;
|
| 559 |
}
|
| 560 |
|
| 561 |
-
//
|
| 562 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 563 |
html += `
|
| 564 |
<div class="data-row">
|
| 565 |
-
<span class="data-label"
|
| 566 |
-
<span class="data-value"
|
| 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 |
}
|