diff --git a/agents/meteorologo/agent.go b/agents/meteorologo/agent.go new file mode 100644 index 0000000..7b7d649 --- /dev/null +++ b/agents/meteorologo/agent.go @@ -0,0 +1,24 @@ +// Package meteorologo defines the pure rules for the meteorologo bot. +// This agent uses tool_use (get_weather) to provide weather information. +package meteorologo + +import ( + "github.com/enmanuel/agents/pkg/decision" +) + +// Rules returns the decision rules for the meteorologo bot. +func Rules() []decision.Rule { + return []decision.Rule{ + // Any DM or mention → LLM (with tool-use enabled) + { + Name: "llm-all", + Match: func(ctx decision.MessageContext) bool { + return ctx.IsDirectMsg || ctx.IsMention + }, + Actions: []decision.Action{{ + Kind: decision.ActionKindLLM, + LLM: &decision.LLMAction{}, + }}, + }, + } +} diff --git a/agents/meteorologo/config.yaml b/agents/meteorologo/config.yaml new file mode 100644 index 0000000..9d2b133 --- /dev/null +++ b/agents/meteorologo/config.yaml @@ -0,0 +1,283 @@ +# ============================================ +# IDENTIDAD +# ============================================ +agent: + id: meteorologo + name: "Meteorologo" + version: "1.0.0" + enabled: true + description: "Meteorologo experto. Consulta el tiempo actual y prevision para cualquier ciudad del mundo." + tags: [weather, llm, tools] + +# ============================================ +# PERSONALIDAD Y COMPORTAMIENTO +# ============================================ +personality: + tone: friendly + verbosity: concise + language: es + languages_supported: [es, en] + emoji_style: minimal + prefix: "" + error_style: helpful + + templates: + greeting: "Hola, soy el Meteorologo. Preguntame por el tiempo en cualquier ciudad." + unknown_command: "No entiendo ese comando. Preguntame directamente por el tiempo de una ciudad." + permission_denied: "No tengo permiso para hacer eso." + error: "Algo salio mal: {{.Error}}" + success: "{{.Summary}}" + busy: "Consultando datos meteorologicos, un momento..." + + behavior: + proactive: false + ask_confirmation: false + show_reasoning: false + thread_replies: true + typing_indicator: true + acknowledge_receipt: false + +# ============================================ +# LLM — CONEXION Y RAZONAMIENTO +# ============================================ +llm: + primary: + provider: openai + model: gpt-4o + api_key_env: OPENAI_API_KEY + base_url: "" + max_tokens: 4096 + temperature: 0.7 + + fallback: + provider: "" + model: "" + api_key_env: "" + base_url: "" + max_tokens: 0 + temperature: 0 + + reasoning: + system_prompt_file: "prompts/system.md" + context_window: 16384 + memory_messages: 30 + + tool_use: + enabled: true + max_iterations: 5 + parallel_calls: false + + rate_limit: + requests_per_minute: 60 + tokens_per_minute: 200000 + concurrent_requests: 5 + +# ============================================ +# TOOLS — get_weather habilitada +# ============================================ +tools: + ssh: + enabled: false + allowed_targets: [] + forbidden_commands: [] + timeout: 0s + max_concurrent: 0 + require_confirmation: [] + + http: + enabled: false + allowed_domains: [] + timeout: 0s + max_retries: 0 + + scripts: + enabled: false + scripts_dir: "" + allowed: [] + timeout: 0s + sandbox: false + + file_ops: + enabled: false + allowed_paths: [] + read_only: true + + mcp: + enabled: false + servers: [] + expose: + port: 0 + tools: [] + + memory: + enabled: true + + knowledge: + enabled: false + +# ============================================ +# MEMORIA +# ============================================ +memory: + enabled: true + window_size: 20 + +# ============================================ +# MATRIX — CONEXION Y ROOMS +# ============================================ +matrix: + homeserver: "https://matrix-af2f3d.organic-machine.com" + user_id: "@meteorologo:matrix-af2f3d.organic-machine.com" + access_token_env: MATRIX_TOKEN_METEOROLOGO + device_id: "YNXDWAVGMW" + + encryption: + enabled: true + store_path: "./agents/meteorologo/data/crypto/" + pickle_key_env: PICKLE_KEY_METEOROLOGO + trust_mode: tofu + recovery_key_env: SSSS_RECOVERY_KEY_METEOROLOGO + + rooms: + listen: [] + respond: [] + admin: [] + + filters: + command_prefix: "!" + mention_respond: true + dm_respond: true + ignore_bots: true + ignore_users: [] + min_power_level: 0 + +# ============================================ +# COMUNICACION INTER-AGENTES +# ============================================ +agents: + peers: + - id: assistant-bot + capabilities: [general, llm] + room: "" + + delegation: + enabled: false + can_delegate_to: [] + can_receive_from: [assistant-bot] + max_delegation_depth: 1 + timeout: 30s + + protocol: + format: json + channel: matrix + heartbeat_interval: 60s + +# ============================================ +# SSH — no aplica +# ============================================ +ssh: + defaults: + user: "" + port: 22 + key_file_env: "" + known_hosts: "" + keepalive_interval: 0s + timeout: 0s + targets: {} + +# ============================================ +# PERMISOS Y SEGURIDAD +# ============================================ +security: + roles: + admin: + users: ["@admin:matrix-af2f3d.organic-machine.com"] + actions: ["*"] + user: + users: ["*"] + actions: ["ask", "help", "weather"] + + audit: + enabled: false + log_file: "./agents/meteorologo/data/audit.log" + log_to_room: "" + include: [] + + secrets: + provider: env + +# ============================================ +# SCHEDULING +# ============================================ +schedules: [] + +# ============================================ +# OBSERVABILIDAD +# ============================================ +observability: + logging: + level: info + format: json + output: stdout + file: "./agents/meteorologo/data/meteorologo.log" + + metrics: + enabled: false + port: 9093 + path: /metrics + export: prometheus + + health: + enabled: true + port: 8083 + path: /healthz + + tracing: + enabled: false + provider: "" + endpoint: "" + +# ============================================ +# RESILIENCIA +# ============================================ +resilience: + circuit_breaker: + failure_threshold: 5 + timeout: 30s + half_open_max: 2 + + retry: + max_attempts: 2 + backoff: exponential + initial_delay: 1s + max_delay: 10s + + shutdown: + timeout: 10s + drain_messages: true + save_state: false + state_file: "" + + queue: + enabled: true + max_size: 100 + priority_users: ["@admin:matrix-af2f3d.organic-machine.com"] + +# ============================================ +# ALMACENAMIENTO Y ESTADO +# ============================================ +storage: + state: + backend: sqlite + path: "./agents/meteorologo/data/meteorologo.db" + + cache: + enabled: true + backend: memory + ttl: 5m + max_entries: 200 + + history: + backend: sqlite + path: "./agents/meteorologo/data/history.db" + retention: 168h diff --git a/agents/meteorologo/prompts/system.md b/agents/meteorologo/prompts/system.md new file mode 100644 index 0000000..58bf0c4 --- /dev/null +++ b/agents/meteorologo/prompts/system.md @@ -0,0 +1,30 @@ +# Meteorologo — System Prompt + +Eres un meteorologo experto que opera como bot en Matrix. Tu especialidad es proporcionar informacion meteorologica precisa y util. + +## Identidad +- Nombre: Meteorologo +- Rol: Experto en meteorologia y clima +- Personalidad: Profesional pero cercano, con pasion por el tiempo atmosferico + +## Capacidades +- Consultar el tiempo actual de cualquier ciudad del mundo usando la herramienta `get_weather` +- Proporcionar previsiones de hasta 3 dias +- Explicar fenomenos meteorologicos +- Dar recomendaciones basadas en el tiempo (ropa, actividades, precauciones) + +## Herramientas disponibles +- `get_weather`: Obtiene el tiempo actual y prevision de 3 dias para una ciudad. Parametro: `city` (nombre de la ciudad). Usala SIEMPRE que te pregunten por el tiempo de una ciudad. + +## Estilo de respuesta +- Responde siempre en el idioma del usuario +- Usa formato claro con temperaturas, humedad, viento y condiciones +- Anade recomendaciones practicas cuando sea relevante (ej: "Lleva paraguas", "Buen dia para pasear") +- Si te preguntan por el tiempo sin especificar ciudad, pregunta que ciudad quieren consultar +- Puedes explicar conceptos meteorologicos si te lo piden +- Usa markdown para formatear (listas, negritas) cuando mejore la legibilidad + +## Restricciones +- No inventes datos meteorologicos: siempre usa la herramienta `get_weather` +- Si la herramienta falla o no encuentra la ciudad, informalo al usuario +- No respondas sobre temas que no tengan relacion con el tiempo o la meteorologia. Redirige amablemente al tema diff --git a/agents/runtime.go b/agents/runtime.go index b047977..5f92e4a 100644 --- a/agents/runtime.go +++ b/agents/runtime.go @@ -742,6 +742,10 @@ func buildToolRegistry( reg.Register(tools.NewCurrentTime()) logger.Debug("registered current_time tool") + // weather tool is always available + reg.Register(tools.NewWeather()) + logger.Debug("registered weather tool") + // matrix_send is always available reg.Register(tools.NewMatrixSend(matrixClient)) logger.Debug("registered matrix tool") diff --git a/cmd/launcher/main.go b/cmd/launcher/main.go index 3485986..371e66b 100644 --- a/cmd/launcher/main.go +++ b/cmd/launcher/main.go @@ -23,6 +23,7 @@ import ( "github.com/enmanuel/agents/agents" assistantagent "github.com/enmanuel/agents/agents/assistant-bot" asistente2agent "github.com/enmanuel/agents/agents/asistente-2" + meteorologoagent "github.com/enmanuel/agents/agents/meteorologo" "github.com/enmanuel/agents/internal/config" "github.com/enmanuel/agents/pkg/decision" "github.com/enmanuel/agents/pkg/orchestration" @@ -36,6 +37,7 @@ import ( var rulesRegistry = map[string]func() []decision.Rule{ "assistant-bot": assistantagent.Rules, "asistente-2": asistente2agent.Rules, + "meteorologo": meteorologoagent.Rules, } func main() { diff --git a/static/meteorologo.jpg b/static/meteorologo.jpg new file mode 100644 index 0000000..cec8d25 Binary files /dev/null and b/static/meteorologo.jpg differ diff --git a/tools/weather.go b/tools/weather.go new file mode 100644 index 0000000..6360fb6 --- /dev/null +++ b/tools/weather.go @@ -0,0 +1,205 @@ +package tools + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "time" +) + +// 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() Tool { + client := &http.Client{Timeout: 15 * time.Second} + + return Tool{ + Def: 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: []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) Result { + city := getString(args, "city") + if city == "" { + return 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 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 Result{Err: fmt.Errorf("get_weather: forecast failed: %w", err)} + } + + // Step 3: Format output + output := formatWeather(resolvedName, country, weather) + return 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) + } +}