feat(weather): romanise non-Latin place names before geocoding

Open-Meteo's geocoder only matches Latin/English spellings, so a Korean city
name like "서울" returns zero results even though the place exists. With
OUTPUT_LANGUAGE locked to Korean the tool-calling model naturally fills
`location` with the Korean name, which dead-ended on every weather request
("could not find location").

When the first geocode is empty and the name is non-ASCII, ask the warm small
model for the common English exonym ("서울" -> "Seoul") and retry once. ASCII
names skip the round-trip entirely.

Live-verified: "서울 날씨 알려줘" now returns real Seoul weather. Tests cover the
romanise-and-retry path and the ASCII short-circuit.
This commit is contained in:
javis-bot
2026-06-12 21:59:42 +09:00
parent f3a1d92620
commit ccbaed9030
2 changed files with 148 additions and 0 deletions

View File

@@ -83,6 +83,65 @@ def _extract_place_from_user_text(text: str, cfg) -> Optional[str]:
return place
def _romanise_place(name: str, cfg) -> Optional[str]:
"""Romanise a non-Latin place name for the geocoder.
Open-Meteo's geocoding API only matches Latin/English spellings, so a
Korean (or other non-Latin) city name like ``서울`` returns zero results
even though the place plainly exists. When OUTPUT_LANGUAGE locks replies to
Korean the tool-calling model naturally fills ``location`` with the Korean
name, which would otherwise dead-end. Ask the (already warm) small model for
the common English exonym so geocoding can succeed on a retry.
Returns the romanised name, or ``None`` if it is unavailable, unchanged, or
doesn't look like a place.
"""
if not isinstance(name, str) or not name.strip() or name.isascii():
return None
if cfg is None:
return None
model = (
getattr(cfg, "tool_router_model", "")
or getattr(cfg, "intent_judge_model", "")
or getattr(cfg, "ollama_chat_model", "")
)
base_url = getattr(cfg, "ollama_base_url", "")
if not model or not base_url:
return None
try:
from ...llm import call_llm_direct
except Exception:
return None
sys_prompt = (
"You romanise place names for a geocoding API that only understands "
"English/Latin spellings. Reply with ONLY the common English name of "
"the place, no punctuation, quotes, or explanation. "
"Examples: 서울 -> Seoul, 도쿄 -> Tokyo, 뮌헨 -> Munich."
)
user_prompt = f"Place: {name}\n\nEnglish name:"
try:
resp = call_llm_direct(
base_url, model, sys_prompt, user_prompt,
timeout_sec=float(getattr(cfg, "llm_tools_timeout_sec", 8.0)),
)
except Exception as e:
debug_log(f" ⚠️ place romanisation failed: {e}", "tools")
return None
if not resp or not isinstance(resp, str):
return None
out = resp.strip().strip("'\"`*.,:;!?()[]{}<>").split("\n", 1)[0].strip()
if not out or out.lower() in _NO_PLACE_SENTINELS:
return None
if len(out) > 60 or len(out.split()) > 5 or out == name:
return None
return out
# WMO Weather interpretation codes
# https://open-meteo.com/en/docs
WMO_CODES = {
@@ -274,6 +333,23 @@ class WeatherTool(Tool):
geo_response.raise_for_status()
geo_data = geo_response.json()
# Open-Meteo only matches Latin spellings, so a non-Latin name
# (e.g. Korean "서울") returns nothing. Retry once with an
# LLM-romanised name before giving up.
if not geo_data.get("results"):
romanised = _romanise_place(location_str, getattr(context, "cfg", None))
if romanised:
debug_log(
f" 🌤️ geocode empty for '{location_str}'; retrying romanised '{romanised}'",
"tools",
)
geocode_params["name"] = romanised
geo_response = requests.get(geocode_url, params=geocode_params, timeout=10)
geo_response.raise_for_status()
geo_data = geo_response.json()
if geo_data.get("results"):
location_str = romanised
if not geo_data.get("results"):
return ToolExecutionResult(
success=False,