OppaAI commited on
Commit
efed13c
Β·
1 Parent(s): 1526efa

refactor: migrate weather provider to Open-Meteo, improve location extraction, and inject server time into persona context

Browse files
Files changed (3) hide show
  1. core/think.py +20 -10
  2. core/tools.py +75 -29
  3. 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
- today = datetime.now().strftime("%B %d, %Y")
53
- return template.replace("USER_ID_HERE", user_id).replace("TODAY_HERE", today)
 
 
 
 
 
 
 
 
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 token_callback:
198
- token_callback(f"__TOOL__:Checking weather for {location}...")
199
- tool_result = get_weather(location)
200
- tool_tag = "weather_data"
 
201
 
202
  elif is_timezone_intent(user_input):
203
  location = extract_location(user_input)
204
- if token_callback:
205
- token_callback(f"__TOOL__:Looking up time in {location}...")
206
- tool_result = get_timezone(location)
207
- tool_tag = "time_data"
 
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) β€” wttr.in (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,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|weather|forecast|time|timezone)\s*",
 
 
 
 
 
 
155
  "", text, flags=re.IGNORECASE,
156
  ).strip()
157
- cleaned = re.sub(r"^(in|at|for|of)\s+", "", cleaned, flags=re.IGNORECASE).strip()
158
- cleaned = re.sub(r"\?$", "", cleaned).strip()
159
- return cleaned or "Tokyo"
 
 
 
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 wttr.in (no API key needed).
292
- Returns a concise summary string.
 
293
  """
294
  try:
295
- # wttr.in requires the location as a path parameter (e.g., https://wttr.in/Paris)
296
- # using q=location query parameters returns 500 server errors
297
- resp = httpx.get(
298
- f"https://wttr.in/{location}",
299
- params={"format": "j1"},
300
  timeout=10,
301
- headers={"User-Agent": "Mozilla/5.0"},
302
  )
303
- resp.raise_for_status()
304
- data = resp.json()
305
- current = data["current_condition"][0]
306
- area = data["nearest_area"][0]
307
-
308
- city = area["areaName"][0]["value"]
309
- country = area["country"][0]["value"]
310
- desc = current["weatherDesc"][0]["value"]
311
- temp_c = current["temp_C"]
312
- temp_f = current["temp_F"]
313
- feels_c = current["FeelsLikeC"]
314
- humidity = current["humidity"]
315
- wind_kmph = current["windspeedKmph"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 : {wind_kmph} km/h"
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 (wttr.in), timezone, currency (ECB), crypto (CoinGecko), anime (MyAnimeList), jokes, Nihongo(Japanese) teaching
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)