refactor: migrate weather provider to Open-Meteo, improve location extraction, and inject server time into persona context
Browse files- core/think.py +20 -10
- core/tools.py +75 -29
- persona/soul.md +1 -1
core/think.py
CHANGED
|
@@ -49,8 +49,16 @@ _DEFAULT_USER_ID = os.getenv("USER_ID", "Guest")
|
|
| 49 |
|
| 50 |
|
| 51 |
def _render_persona(template: str, user_id: str) -> str:
|
| 52 |
-
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
|
| 55 |
|
| 56 |
# ββ think βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -194,17 +202,19 @@ class AikoThink:
|
|
| 194 |
|
| 195 |
elif is_weather_intent(user_input):
|
| 196 |
location = extract_location(user_input)
|
| 197 |
-
if
|
| 198 |
-
token_callback
|
| 199 |
-
|
| 200 |
-
|
|
|
|
| 201 |
|
| 202 |
elif is_timezone_intent(user_input):
|
| 203 |
location = extract_location(user_input)
|
| 204 |
-
if
|
| 205 |
-
token_callback
|
| 206 |
-
|
| 207 |
-
|
|
|
|
| 208 |
|
| 209 |
elif is_currency_intent(user_input):
|
| 210 |
amount, from_cur, to_cur = extract_currency_parts(user_input)
|
|
|
|
| 49 |
|
| 50 |
|
| 51 |
def _render_persona(template: str, user_id: str) -> str:
|
| 52 |
+
now = datetime.now().astimezone()
|
| 53 |
+
today = now.strftime("%B %d, %Y")
|
| 54 |
+
current_time = now.strftime("%Y-%m-%d %H:%M:%S %Z")
|
| 55 |
+
tz_name = now.astimezone().tzinfo.tzname(now) or "UTC"
|
| 56 |
+
time_line = f"\nCurrent server time: {current_time} ({tz_name})"
|
| 57 |
+
return (
|
| 58 |
+
template
|
| 59 |
+
.replace("USER_ID_HERE", user_id)
|
| 60 |
+
.replace("TODAY_HERE", today + time_line)
|
| 61 |
+
)
|
| 62 |
|
| 63 |
|
| 64 |
# ββ think βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 202 |
|
| 203 |
elif is_weather_intent(user_input):
|
| 204 |
location = extract_location(user_input)
|
| 205 |
+
if location:
|
| 206 |
+
if token_callback:
|
| 207 |
+
token_callback(f"__TOOL__:Checking weather for {location}...")
|
| 208 |
+
tool_result = get_weather(location)
|
| 209 |
+
tool_tag = "weather_data"
|
| 210 |
|
| 211 |
elif is_timezone_intent(user_input):
|
| 212 |
location = extract_location(user_input)
|
| 213 |
+
if location:
|
| 214 |
+
if token_callback:
|
| 215 |
+
token_callback(f"__TOOL__:Looking up time in {location}...")
|
| 216 |
+
tool_result = get_timezone(location)
|
| 217 |
+
tool_tag = "time_data"
|
| 218 |
|
| 219 |
elif is_currency_intent(user_input):
|
| 220 |
amount, from_cur, to_cur = extract_currency_parts(user_input)
|
core/tools.py
CHANGED
|
@@ -7,7 +7,7 @@ Tools:
|
|
| 7 |
web_search(query) β SearXNG JSON API (Modal-hosted)
|
| 8 |
web_fetch(url) β raw page fetch + HTML strip
|
| 9 |
web_search_and_fetch(query) β search + fetch top result, combined string
|
| 10 |
-
get_weather(location) β
|
| 11 |
get_timezone(location) β worldtimeapi.org (no key)
|
| 12 |
get_currency(amount, fr, to) β Frankfurter API (no key, ECB data)
|
| 13 |
get_crypto_price(coin, currency) β CoinGecko API (no key)
|
|
@@ -148,15 +148,24 @@ def extract_search_query(text: str) -> str:
|
|
| 148 |
|
| 149 |
def extract_location(text: str) -> str:
|
| 150 |
"""Pull location from weather/timezone queries."""
|
| 151 |
-
# strip common question prefixes
|
| 152 |
cleaned = re.sub(
|
| 153 |
-
r"^(what(?:'s| is) the (?:weather|temperature|forecast|time
|
| 154 |
-
r"how(?:'s| is) the weather|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
"", text, flags=re.IGNORECASE,
|
| 156 |
).strip()
|
| 157 |
-
|
| 158 |
-
cleaned = re.sub(r"\
|
| 159 |
-
|
|
|
|
|
|
|
|
|
|
| 160 |
|
| 161 |
|
| 162 |
def extract_currency_parts(text: str) -> tuple[float, str, str]:
|
|
@@ -286,40 +295,77 @@ def web_search_and_fetch(query: str, max_results: int = 5) -> str:
|
|
| 286 |
|
| 287 |
# ββ weather βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 288 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 289 |
def get_weather(location: str) -> str:
|
| 290 |
"""
|
| 291 |
-
Fetch current weather via
|
| 292 |
-
|
|
|
|
| 293 |
"""
|
| 294 |
try:
|
| 295 |
-
#
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
params={"format": "j1"},
|
| 300 |
timeout=10,
|
| 301 |
-
headers={"User-Agent": "Mozilla/5.0"},
|
| 302 |
)
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 316 |
|
| 317 |
return (
|
| 318 |
f"[Weather for {city}, {country}]\n"
|
| 319 |
f"Condition : {desc}\n"
|
| 320 |
f"Temp : {temp_c}Β°C / {temp_f}Β°F (feels like {feels_c}Β°C)\n"
|
| 321 |
f"Humidity : {humidity}%\n"
|
| 322 |
-
f"Wind : {
|
| 323 |
)
|
| 324 |
except Exception as e:
|
| 325 |
return f"[weather fetch failed: {e}]"
|
|
|
|
| 7 |
web_search(query) β SearXNG JSON API (Modal-hosted)
|
| 8 |
web_fetch(url) β raw page fetch + HTML strip
|
| 9 |
web_search_and_fetch(query) β search + fetch top result, combined string
|
| 10 |
+
get_weather(location) β Open-Meteo geocoding + weather API (no key)
|
| 11 |
get_timezone(location) β worldtimeapi.org (no key)
|
| 12 |
get_currency(amount, fr, to) β Frankfurter API (no key, ECB data)
|
| 13 |
get_crypto_price(coin, currency) β CoinGecko API (no key)
|
|
|
|
| 148 |
|
| 149 |
def extract_location(text: str) -> str:
|
| 150 |
"""Pull location from weather/timezone queries."""
|
| 151 |
+
# strip common question prefixes β order matters: longest patterns first
|
| 152 |
cleaned = re.sub(
|
| 153 |
+
r"^(what time is it|what(?:'s| is) the (?:weather|temperature|forecast|time|local time)"
|
| 154 |
+
r"|how(?:'s| is) the weather|how(?:'s| is) the temperature"
|
| 155 |
+
r"|what(?:'s| is) (?:the )?(?:current )?(?:weather|temperature|forecast|time|local time)"
|
| 156 |
+
r"|(?:current |local )?(?:weather|forecast|time|timezone)"
|
| 157 |
+
r"|tell me the (?:weather|time|temperature)"
|
| 158 |
+
r"|check the (?:weather|time)"
|
| 159 |
+
r"|do you know what time it is"
|
| 160 |
+
r"|what time do they have)\s*",
|
| 161 |
"", text, flags=re.IGNORECASE,
|
| 162 |
).strip()
|
| 163 |
+
# strip prepositions and trailing punctuation
|
| 164 |
+
cleaned = re.sub(r"^(in|at|for|of|around|near|over)\s+", "", cleaned, flags=re.IGNORECASE).strip()
|
| 165 |
+
cleaned = re.sub(r"[\s?!.,;]+$", "", cleaned).strip()
|
| 166 |
+
# strip trailing/standalone noise like "right now", "currently", "today"
|
| 167 |
+
cleaned = re.sub(r"(?:^|\s+)(right now|currently|today|now|rn)$", "", cleaned, flags=re.IGNORECASE).strip()
|
| 168 |
+
return cleaned or ""
|
| 169 |
|
| 170 |
|
| 171 |
def extract_currency_parts(text: str) -> tuple[float, str, str]:
|
|
|
|
| 295 |
|
| 296 |
# ββ weather βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 297 |
|
| 298 |
+
# WMO weather code β human-readable description
|
| 299 |
+
_WMO_CODES = {
|
| 300 |
+
0: "Clear sky", 1: "Mainly clear", 2: "Partly cloudy", 3: "Overcast",
|
| 301 |
+
45: "Foggy", 48: "Depositing rime fog",
|
| 302 |
+
51: "Light drizzle", 53: "Moderate drizzle", 55: "Dense drizzle",
|
| 303 |
+
56: "Light freezing drizzle", 57: "Dense freezing drizzle",
|
| 304 |
+
61: "Slight rain", 63: "Moderate rain", 65: "Heavy rain",
|
| 305 |
+
66: "Light freezing rain", 67: "Heavy freezing rain",
|
| 306 |
+
71: "Slight snowfall", 73: "Moderate snowfall", 75: "Heavy snowfall",
|
| 307 |
+
77: "Snow grains", 80: "Slight rain showers", 81: "Moderate rain showers",
|
| 308 |
+
82: "Violent rain showers", 85: "Slight snow showers", 86: "Heavy snow showers",
|
| 309 |
+
95: "Thunderstorm", 96: "Thunderstorm with slight hail",
|
| 310 |
+
99: "Thunderstorm with heavy hail",
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
|
| 314 |
def get_weather(location: str) -> str:
|
| 315 |
"""
|
| 316 |
+
Fetch current weather via Open-Meteo (no API key needed).
|
| 317 |
+
Uses their geocoding API for accurate city resolution, then
|
| 318 |
+
fetches current conditions by lat/lon.
|
| 319 |
"""
|
| 320 |
try:
|
| 321 |
+
# Step 1: Geocode the location name β lat/lon
|
| 322 |
+
geo_resp = httpx.get(
|
| 323 |
+
"https://geocoding-api.open-meteo.com/v1/search",
|
| 324 |
+
params={"name": location, "count": 1, "language": "en"},
|
|
|
|
| 325 |
timeout=10,
|
|
|
|
| 326 |
)
|
| 327 |
+
geo_resp.raise_for_status()
|
| 328 |
+
geo_results = geo_resp.json().get("results")
|
| 329 |
+
if not geo_results:
|
| 330 |
+
return f"[weather: location not found for '{location}']"
|
| 331 |
+
|
| 332 |
+
place = geo_results[0]
|
| 333 |
+
lat = place["latitude"]
|
| 334 |
+
lon = place["longitude"]
|
| 335 |
+
city = place.get("name", location)
|
| 336 |
+
country = place.get("country", "")
|
| 337 |
+
|
| 338 |
+
# Step 2: Fetch current weather by coordinates
|
| 339 |
+
weather_resp = httpx.get(
|
| 340 |
+
"https://api.open-meteo.com/v1/forecast",
|
| 341 |
+
params={
|
| 342 |
+
"latitude": lat,
|
| 343 |
+
"longitude": lon,
|
| 344 |
+
"current": "temperature_2m,relative_humidity_2m,apparent_temperature,weather_code,wind_speed_10m",
|
| 345 |
+
"temperature_unit": "celsius",
|
| 346 |
+
"wind_speed_unit": "kmh",
|
| 347 |
+
},
|
| 348 |
+
timeout=10,
|
| 349 |
+
)
|
| 350 |
+
weather_resp.raise_for_status()
|
| 351 |
+
current = weather_resp.json().get("current", {})
|
| 352 |
+
|
| 353 |
+
temp_c = current.get("temperature_2m")
|
| 354 |
+
feels_c = current.get("apparent_temperature")
|
| 355 |
+
humidity = current.get("relative_humidity_2m")
|
| 356 |
+
wind = current.get("wind_speed_10m")
|
| 357 |
+
code = current.get("weather_code", -1)
|
| 358 |
+
desc = _WMO_CODES.get(code, "Unknown")
|
| 359 |
+
|
| 360 |
+
# Convert C to F for display
|
| 361 |
+
temp_f = round(temp_c * 9 / 5 + 32, 1) if temp_c is not None else "?"
|
| 362 |
|
| 363 |
return (
|
| 364 |
f"[Weather for {city}, {country}]\n"
|
| 365 |
f"Condition : {desc}\n"
|
| 366 |
f"Temp : {temp_c}Β°C / {temp_f}Β°F (feels like {feels_c}Β°C)\n"
|
| 367 |
f"Humidity : {humidity}%\n"
|
| 368 |
+
f"Wind : {wind} km/h"
|
| 369 |
)
|
| 370 |
except Exception as e:
|
| 371 |
return f"[weather fetch failed: {e}]"
|
persona/soul.md
CHANGED
|
@@ -16,7 +16,7 @@ You use all models under 32B parameter. Using llama.cpp to inference the models
|
|
| 16 |
- **Body** β VRoid Studio 3D model, animated in Gradio via three-vrm.js
|
| 17 |
- **Search** β SearXNG self-hosted instance, served on Modal
|
| 18 |
You have the skills to do some basic tasks:
|
| 19 |
-
- **Skills** β web search (SearXNG), weather
|
| 20 |
|
| 21 |
---
|
| 22 |
## Identity (non-negotiable)
|
|
|
|
| 16 |
- **Body** β VRoid Studio 3D model, animated in Gradio via three-vrm.js
|
| 17 |
- **Search** β SearXNG self-hosted instance, served on Modal
|
| 18 |
You have the skills to do some basic tasks:
|
| 19 |
+
- **Skills** β web search (SearXNG), weather, timezone, currency (ECB), crypto (CoinGecko), anime (MyAnimeList), jokes, Nihongo(Japanese) teaching
|
| 20 |
|
| 21 |
---
|
| 22 |
## Identity (non-negotiable)
|