8d89a762fb
Cada tool ahora vive en su propio subpackage dentro de tools/ (clock, file, http, knowledgetools, matrix, memorytools, ssh, weather) en lugar de archivos planos en el paquete raíz tools/. Esto mejora la organización, permite imports selectivos y reduce acoplamiento entre tools. El paquete tools/ raíz conserva los tipos base (Def, Param, Result, ToolFunc, Tool, Registry). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
208 lines
6.1 KiB
Go
208 lines
6.1 KiB
Go
package weather
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/enmanuel/agents/tools"
|
|
)
|
|
|
|
// NewWeather creates a get_weather tool that fetches current weather and forecast
|
|
// for a city using the Open-Meteo API (free, no API key required).
|
|
func NewWeather() tools.Tool {
|
|
client := &http.Client{Timeout: 15 * time.Second}
|
|
|
|
return tools.Tool{
|
|
Def: tools.Def{
|
|
Name: "get_weather",
|
|
Description: "Get current weather conditions and 3-day forecast for a city. Returns temperature, humidity, wind speed, and weather description.",
|
|
Parameters: []tools.Param{
|
|
{Name: "city", Type: "string", Description: "City name to look up (e.g. 'Madrid', 'New York', 'Tokyo')", Required: true},
|
|
},
|
|
},
|
|
Exec: func(ctx context.Context, args map[string]any) tools.Result {
|
|
city := tools.GetString(args, "city")
|
|
if city == "" {
|
|
return tools.Result{Err: fmt.Errorf("get_weather: city is required")}
|
|
}
|
|
|
|
// Step 1: Geocode city name to coordinates
|
|
lat, lon, resolvedName, country, err := geocodeCity(ctx, client, city)
|
|
if err != nil {
|
|
return tools.Result{Err: fmt.Errorf("get_weather: geocoding failed: %w", err)}
|
|
}
|
|
|
|
// Step 2: Fetch weather data
|
|
weather, err := fetchWeather(ctx, client, lat, lon)
|
|
if err != nil {
|
|
return tools.Result{Err: fmt.Errorf("get_weather: forecast failed: %w", err)}
|
|
}
|
|
|
|
// Step 3: Format output
|
|
output := formatWeather(resolvedName, country, weather)
|
|
return tools.Result{Output: output}
|
|
},
|
|
}
|
|
}
|
|
|
|
// geocodeCity resolves a city name to coordinates using Open-Meteo Geocoding API.
|
|
func geocodeCity(ctx context.Context, client *http.Client, city string) (lat, lon float64, name, country string, err error) {
|
|
u := fmt.Sprintf("https://geocoding-api.open-meteo.com/v1/search?name=%s&count=1&language=es&format=json",
|
|
url.QueryEscape(city))
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
|
|
if err != nil {
|
|
return 0, 0, "", "", err
|
|
}
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return 0, 0, "", "", err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(io.LimitReader(resp.Body, 32*1024))
|
|
if err != nil {
|
|
return 0, 0, "", "", err
|
|
}
|
|
|
|
var result struct {
|
|
Results []struct {
|
|
Name string `json:"name"`
|
|
Country string `json:"country"`
|
|
Latitude float64 `json:"latitude"`
|
|
Longitude float64 `json:"longitude"`
|
|
} `json:"results"`
|
|
}
|
|
if err := json.Unmarshal(body, &result); err != nil {
|
|
return 0, 0, "", "", fmt.Errorf("invalid geocoding response: %w", err)
|
|
}
|
|
if len(result.Results) == 0 {
|
|
return 0, 0, "", "", fmt.Errorf("city %q not found", city)
|
|
}
|
|
|
|
r := result.Results[0]
|
|
return r.Latitude, r.Longitude, r.Name, r.Country, nil
|
|
}
|
|
|
|
type weatherData struct {
|
|
Current struct {
|
|
Temperature float64 `json:"temperature_2m"`
|
|
Humidity int `json:"relative_humidity_2m"`
|
|
WindSpeed float64 `json:"wind_speed_10m"`
|
|
WeatherCode int `json:"weather_code"`
|
|
FeelsLike float64 `json:"apparent_temperature"`
|
|
} `json:"current"`
|
|
Daily struct {
|
|
Time []string `json:"time"`
|
|
TempMax []float64 `json:"temperature_2m_max"`
|
|
TempMin []float64 `json:"temperature_2m_min"`
|
|
WeatherCode []int `json:"weather_code"`
|
|
PrecipProb []int `json:"precipitation_probability_max"`
|
|
} `json:"daily"`
|
|
}
|
|
|
|
// fetchWeather gets current conditions and 3-day forecast from Open-Meteo.
|
|
func fetchWeather(ctx context.Context, client *http.Client, lat, lon float64) (*weatherData, error) {
|
|
u := fmt.Sprintf(
|
|
"https://api.open-meteo.com/v1/forecast?latitude=%.4f&longitude=%.4f"+
|
|
"¤t=temperature_2m,relative_humidity_2m,apparent_temperature,weather_code,wind_speed_10m"+
|
|
"&daily=temperature_2m_max,temperature_2m_min,weather_code,precipitation_probability_max"+
|
|
"&timezone=auto&forecast_days=3",
|
|
lat, lon,
|
|
)
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(io.LimitReader(resp.Body, 64*1024))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("API returned %d: %s", resp.StatusCode, body)
|
|
}
|
|
|
|
var data weatherData
|
|
if err := json.Unmarshal(body, &data); err != nil {
|
|
return nil, fmt.Errorf("invalid forecast response: %w", err)
|
|
}
|
|
return &data, nil
|
|
}
|
|
|
|
// formatWeather produces a human-readable weather summary.
|
|
func formatWeather(city, country string, w *weatherData) string {
|
|
var b strings.Builder
|
|
|
|
fmt.Fprintf(&b, "Tiempo en %s, %s\n\n", city, country)
|
|
fmt.Fprintf(&b, "AHORA:\n")
|
|
fmt.Fprintf(&b, " Temperatura: %.1f C (sensacion termica: %.1f C)\n", w.Current.Temperature, w.Current.FeelsLike)
|
|
fmt.Fprintf(&b, " Humedad: %d%%\n", w.Current.Humidity)
|
|
fmt.Fprintf(&b, " Viento: %.1f km/h\n", w.Current.WindSpeed)
|
|
fmt.Fprintf(&b, " Condicion: %s\n", weatherCodeToText(w.Current.WeatherCode))
|
|
|
|
if len(w.Daily.Time) > 0 {
|
|
fmt.Fprintf(&b, "\nPREVISION:\n")
|
|
for i, date := range w.Daily.Time {
|
|
fmt.Fprintf(&b, " %s: %.0f/%.0f C, %s",
|
|
date, w.Daily.TempMin[i], w.Daily.TempMax[i],
|
|
weatherCodeToText(w.Daily.WeatherCode[i]))
|
|
if i < len(w.Daily.PrecipProb) {
|
|
fmt.Fprintf(&b, ", prob. lluvia: %d%%", w.Daily.PrecipProb[i])
|
|
}
|
|
fmt.Fprintln(&b)
|
|
}
|
|
}
|
|
|
|
return b.String()
|
|
}
|
|
|
|
// weatherCodeToText converts WMO weather codes to Spanish descriptions.
|
|
func weatherCodeToText(code int) string {
|
|
switch {
|
|
case code == 0:
|
|
return "Despejado"
|
|
case code == 1:
|
|
return "Mayormente despejado"
|
|
case code == 2:
|
|
return "Parcialmente nublado"
|
|
case code == 3:
|
|
return "Nublado"
|
|
case code >= 45 && code <= 48:
|
|
return "Niebla"
|
|
case code >= 51 && code <= 55:
|
|
return "Llovizna"
|
|
case code >= 56 && code <= 57:
|
|
return "Llovizna helada"
|
|
case code >= 61 && code <= 65:
|
|
return "Lluvia"
|
|
case code >= 66 && code <= 67:
|
|
return "Lluvia helada"
|
|
case code >= 71 && code <= 77:
|
|
return "Nieve"
|
|
case code >= 80 && code <= 82:
|
|
return "Chubascos"
|
|
case code >= 85 && code <= 86:
|
|
return "Chubascos de nieve"
|
|
case code >= 95 && code <= 99:
|
|
return "Tormenta"
|
|
default:
|
|
return fmt.Sprintf("Codigo %d", code)
|
|
}
|
|
}
|