Ryanhub - file viewer
filename: tools/weather.go
branch: main
back to repo
package tools

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"time"

	"assistant/config"
)

const weatherToolName = "get_current_weather"

var weatherParams = json.RawMessage(`{
  "type": "object",
  "properties": {},
  "additionalProperties": false
}`)

func newWeatherTool(cfg config.WeatherToolConfig) Tool {
	lat := cfg.Lat
	lon := cfg.Lon
	return Tool{
		Name:        weatherToolName,
		Description: "Get current weather for the single preconfigured location only (no location argument supported).",
		Parameters:  weatherParams,
		Run: func(ctx context.Context, _ json.RawMessage) (string, error) {
			return fetchOpenMeteo(ctx, lat, lon)
		},
	}
}

func fetchOpenMeteo(ctx context.Context, lat, lon float64) (string, error) {
	q := url.Values{}
	q.Set("latitude", fmt.Sprintf("%.4f", lat))
	q.Set("longitude", fmt.Sprintf("%.4f", lon))
	q.Set("current", "temperature_2m,weather_code,wind_speed_10m")
	uri := "https://api.open-meteo.com/v1/forecast?" + q.Encode()
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil)
	if err != nil {
		return "", err
	}
	c := &http.Client{Timeout: 20 * time.Second}
	res, err := c.Do(req)
	if err != nil {
		return "", err
	}
	defer res.Body.Close()
	b, err := io.ReadAll(io.LimitReader(res.Body, 1<<20))
	if err != nil {
		return "", err
	}
	if res.StatusCode < 200 || res.StatusCode >= 300 {
		return "", fmt.Errorf("open-meteo: %s: %s", res.Status, string(b[:min(len(b), 200)]))
	}
	var payload struct {
		Current struct {
			Temperature float64 `json:"temperature_2m"`
			WeatherCode int     `json:"weather_code"`
			WindSpeed   float64 `json:"wind_speed_10m"`
		} `json:"current"`
	}
	if err := json.Unmarshal(b, &payload); err != nil {
		return "", err
	}
	desc := weatherCodeDescription(payload.Current.WeatherCode)
	return fmt.Sprintf("Temperature: %.1f°C (%s). Wind: %.0f km/h. (WMO weather code %d)",
		payload.Current.Temperature, desc, payload.Current.WindSpeed, payload.Current.WeatherCode), nil
}

func min(a, b int) int {
	if a < b {
		return a
	}
	return b
}

func weatherCodeDescription(code int) string {
	// WMO Weather interpretation codes ( WW ).
	switch {
	case code == 0:
		return "clear"
	case code <= 3:
		return "partly cloudy"
	case code <= 48:
		return "fog"
	case code <= 57:
		return "drizzle"
	case code <= 67:
		return "rain"
	case code <= 77:
		return "snow"
	case code <= 82:
		return "rain showers"
	case code <= 86:
		return "snow showers"
	case code == 95:
		return "thunderstorm"
	default:
		return "conditions vary"
	}
}