Search/ia-weather.go
partisan 0559fd2bba
Some checks failed
Run Integration Tests / test (push) Failing after 10m2s
Added Weather IA
2025-06-30 08:07:57 +02:00

367 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"regexp"
"strings"
"unicode"
"golang.org/x/text/unicode/norm"
)
type WeatherCity struct {
Name string
Country string
Lat float64
Lon float64
}
type WeatherCurrent struct {
Temperature float64
Wind float64
Humidity int
Condition string
}
type WeatherDay struct {
Date string
MinTemp float64
MaxTemp float64
Condition string
}
type WeatherForecast struct {
Current WeatherCurrent
Forecast []WeatherDay
}
func getWeatherForQuery(query string) (city WeatherCity, forecast WeatherForecast, ok bool) {
// Expanded multi-language weather keywords (40+ languages/dialects)
weatherWords := []string{
// English
"weather", "forecast", "temperature", "conditions", "meteorology", "outlook",
// Czech/Slovak
"počasí", "předpověď", "teplota", "vlhkost", "srážky", "vítr", "meteo",
// German
"wetter", "vorhersage", "temperatur", "wettervorhersage", "wetterbericht",
// French
"météo", "prévisions", "température", "conditions météo", "prévision météo",
// Spanish
"tiempo", "clima", "pronóstico", "temperatura", "meteorología", "previsión",
// Italian
"tempo", "meteo", "previsioni", "temperatura", "condizioni atmosferiche",
// Portuguese
"tempo", "clima", "previsão", "temperatura", "meteorologia",
// Polish
"pogoda", "prognoza", "temperatura", "warunki atmosferyczne",
// Russian
"погода", "прогноз", "температура", "метео", "метеопрогноз",
// Ukrainian
"погода", "прогноз", "температура", "метео",
// Dutch
"weer", "voorspelling", "temperatuur", "weersverwachting",
// Scandinavian
"väder", "prognos", "temperatur", // Swedish
"vær", "prognose", "temperatur", // Norwegian/Danish
"veður", "spá", "hitastig", // Icelandic
// East Asian
"天気", "予報", "気温", // Japanese (tenki, yohō, kion)
"날씨", "예보", "기온", // Korean (nalssi, yebo, gion)
"天气", "预报", "气温", // Chinese (tiānqì, yùbào, qìwēn)
// South Asian
"मौसम", "पूर्वानुमान", "तापमान", // Hindi (mausam, purvanumaan, taapmaan)
"আবহাওয়া", "পূর্বাভাস", "তাপমাত্রা", // Bengali (ābhawāẏā, pūrbābhāsa, tāpamātrā)
// Middle Eastern
"طقس", "توقعات", "درجة الحرارة", // Arabic (ṭaqs, tawaqquʿāt, darajat al-ḥarāra)
"آب و ہوا", "پیش گوئی", "درجہ حرارت", // Urdu (āb-o-hawā, peshgoī, daraja ḥarārat)
// Turkish
"hava", "tahmin", "sıcaklık", "hava durumu",
// Greek
"καιρός", "πρόβλεψη", "θερμοκρασία",
// Hebrew
"מזג אוויר", "תחזית", "טמפרטורה",
// Other European
"időkép", "előrejelzés", "hőmérséklet", // Hungarian
"vreme", "prognoză", "temperatură", // Romanian
"vrijeme", "prognoza", "temperatura", // Croatian/Serbian
// Global/Internet slang
"temp", "wx", "meteo", "wea", "forec",
}
// Enhanced multi-language prepositions
prepositions := []string{
// English
"in", "at", "for", "around", "near",
// Czech/Slovak
"v", "ve", "na", "do", "u", "při", "blízko", "okolí",
// German
"in", "bei", "an", "für", "um", "nahe",
// Romance
"en", "a", "au", "aux", "dans", // French
"en", "a", "de", // Spanish
"a", "in", "da", // Italian
"em", "no", "na", // Portuguese
// Slavic
"w", "we", "na", "dla", "pod", // Polish
"в", "на", "у", "к", "под", // Russian/Ukrainian
// Nordic
"i", "på", "hos", // Swedish/Danish/Norwegian
// Others
"في", "عند", "قرب", // Arabic (fī, ʿind, qurb)
"में", "पर", "के पास", // Hindi (mẽ, par, ke pās)
"で", "に", "の近く", // Japanese (de, ni, no chikaku)
"에서", "에", "근처", // Korean (eseo, e, geuncheo)
"在", "于", "附近", // Chinese (zài, yú, fùjìn)
}
// Always normalize query (lowercase + remove diacritics)
normalized := removeDiacritics(strings.ToLower(query))
hasWeather := false
for _, word := range weatherWords {
if strings.Contains(normalized, removeDiacritics(word)) {
hasWeather = true
break
}
}
if !hasWeather {
return city, forecast, false
}
// Improved location extraction with diacritic handling
loc := extractWeatherLocation(normalized, weatherWords, prepositions)
if loc == "" {
return city, forecast, false
}
// Geocode and get weather
return geocodeAndGetWeather(loc)
}
func extractWeatherLocation(query string, weatherWords, prepositions []string) string {
// Create normalized versions for matching
normWeatherWords := make([]string, len(weatherWords))
for i, w := range weatherWords {
normWeatherWords[i] = removeDiacritics(w)
}
normPrepositions := make([]string, len(prepositions))
for i, p := range prepositions {
normPrepositions[i] = removeDiacritics(p)
}
// Pattern 1: [weather_word] [preposition]? [location]
pattern1 := `(?:` + strings.Join(normWeatherWords, "|") + `)\s*(?:` + strings.Join(normPrepositions, "|") + `)?\s*(.+)`
re1 := regexp.MustCompile(pattern1)
if matches := re1.FindStringSubmatch(query); len(matches) > 1 {
loc := cleanLocation(matches[1], normPrepositions)
if loc != "" {
return loc
}
}
// Pattern 2: [location] [weather_word]
pattern2 := `(.+?)\s+(?:` + strings.Join(normWeatherWords, "|") + `)`
re2 := regexp.MustCompile(pattern2)
if matches := re2.FindStringSubmatch(query); len(matches) > 1 {
loc := cleanLocation(matches[1], normPrepositions)
if loc != "" {
return loc
}
}
// Pattern 3: Question format
questionPattern := `(?:how is|what is|what's|jak[ée]\s+je|wie ist|quel est|qu[eé]\s+tal|com'[èe])\s+(?:the )?(?:` +
strings.Join(normWeatherWords, "|") + `)\s*(?:` + strings.Join(normPrepositions, "|") + `)?\s*(.+)`
re3 := regexp.MustCompile(questionPattern)
if matches := re3.FindStringSubmatch(query); len(matches) > 1 {
loc := cleanLocation(matches[1], normPrepositions)
if loc != "" {
return loc
}
}
// Fallback with smarter exclusion
return extractByExclusion(query, normWeatherWords, normPrepositions)
}
func cleanLocation(loc string, prepositions []string) string {
// Create preposition set
prepSet := make(map[string]bool)
for _, p := range prepositions {
prepSet[p] = true
}
words := strings.Fields(loc)
// Remove leading prepositions
for len(words) > 0 && prepSet[words[0]] {
words = words[1:]
}
// Remove trailing prepositions
for len(words) > 0 && prepSet[words[len(words)-1]] {
words = words[:len(words)-1]
}
// Rejoin and clean
cleaned := strings.Join(words, " ")
return strings.Trim(cleaned, ",.?!:;()[]{}'\"")
}
// Remove diacritics implementation
func removeDiacritics(s string) string {
var result []rune
for _, r := range norm.NFD.String(s) {
if unicode.Is(unicode.Mn, r) { // Mn: nonspacing marks
continue
}
result = append(result, r)
}
return string(result)
}
// Extract location by removing weather-related words
func extractByExclusion(query string, weatherWords, prepositions []string) string {
// Create removal set
removeSet := make(map[string]bool)
for _, w := range weatherWords {
removeSet[w] = true
}
for _, p := range prepositions {
removeSet[p] = true
}
// Process query words
words := strings.Fields(query)
var locWords []string
for _, word := range words {
if !removeSet[word] {
locWords = append(locWords, word)
}
}
loc := strings.Join(locWords, " ")
return cleanLocation(loc, prepositions)
}
// // Improved location cleaning
// func cleanLocation(loc string) string {
// loc = strings.Trim(loc, ",.?!:;()[]{}'\"")
// // Remove trailing verbs
// verbs := []string{"is", "at", "for", "in", "v", "ve", "na", "do", "w", "en", "a"}
// for _, v := range verbs {
// loc = strings.TrimSuffix(loc, " "+v)
// }
// return loc
// }
// // Remove diacritics implementation
// func removeDiacritics(s string) string {
// var result []rune
// for _, r := range norm.NFD.String(s) {
// if unicode.Is(unicode.Mn, r) { // Mn: nonspacing marks
// continue
// }
// result = append(result, r)
// }
// return string(result)
// }
func geocodeAndGetWeather(loc string) (WeatherCity, WeatherForecast, bool) {
var city WeatherCity
var forecast WeatherForecast
// 1. Geocode
geoURL := fmt.Sprintf("https://geocoding-api.open-meteo.com/v1/search?name=%s&count=1", urlQueryEscape(loc))
resp, err := http.Get(geoURL)
if err != nil {
return city, forecast, false
}
defer resp.Body.Close()
var geo struct {
Results []struct {
Name string `json:"name"`
Country string `json:"country"`
Lat float64 `json:"latitude"`
Lon float64 `json:"longitude"`
} `json:"results"`
}
if err := json.NewDecoder(resp.Body).Decode(&geo); err != nil || len(geo.Results) == 0 {
return city, forecast, false
}
g := geo.Results[0]
city = WeatherCity{
Name: g.Name,
Country: g.Country,
Lat: g.Lat,
Lon: g.Lon,
}
// 2. Weather (current + forecast)
weatherURL := fmt.Sprintf("https://api.open-meteo.com/v1/forecast?latitude=%f&longitude=%f&current=temperature_2m,weather_code,wind_speed_10m,relative_humidity_2m&daily=temperature_2m_min,temperature_2m_max,weather_code&forecast_days=3&timezone=auto", g.Lat, g.Lon)
resp2, err := http.Get(weatherURL)
if err != nil {
return city, forecast, false
}
defer resp2.Body.Close()
var data struct {
Current struct {
Temp float64 `json:"temperature_2m"`
Wind float64 `json:"wind_speed_10m"`
Hum int `json:"relative_humidity_2m"`
Code int `json:"weather_code"`
} `json:"current"`
Daily struct {
Dates []string `json:"time"`
MinTemp []float64 `json:"temperature_2m_min"`
MaxTemp []float64 `json:"temperature_2m_max"`
Weather []int `json:"weather_code"`
} `json:"daily"`
}
body, _ := io.ReadAll(resp2.Body)
if err := json.Unmarshal(body, &data); err != nil {
return city, forecast, false
}
forecast.Current = WeatherCurrent{
Temperature: data.Current.Temp,
Wind: data.Current.Wind,
Humidity: data.Current.Hum,
Condition: weatherDescription(data.Current.Code),
}
for i := range data.Daily.Dates {
forecast.Forecast = append(forecast.Forecast, WeatherDay{
Date: data.Daily.Dates[i],
MinTemp: data.Daily.MinTemp[i],
MaxTemp: data.Daily.MaxTemp[i],
Condition: weatherDescription(data.Daily.Weather[i]),
})
}
return city, forecast, true
}
func weatherDescription(code int) string {
// Minimal mapping, can be expanded
switch code {
case 0:
return "Clear"
case 1, 2, 3:
return "Partly cloudy"
case 45, 48:
return "Fog"
case 51, 53, 55, 56, 57:
return "Drizzle"
case 61, 63, 65, 66, 67, 80, 81, 82:
return "Rain"
case 71, 73, 75, 77, 85, 86:
return "Snow"
case 95, 96, 99:
return "Thunderstorm"
default:
return "Unknown"
}
}
// Helper for safe query escaping
func urlQueryEscape(s string) string {
return strings.ReplaceAll(strings.ReplaceAll(s, " ", "+"), "%", "")
}