/** * Weather tool - get current weather information * Uses Open-Meteo API (free, no API key required) */ import { Tool } from 'ollama'; import { ToolHandler, ToolResult } from './types'; export const weatherTool: Tool = { type: 'function', function: { name: 'get_weather', description: 'Get current weather information for a location. Provides temperature, conditions, humidity, wind speed, and more. Use simple city names for best results (e.g., "Phoenix" not "Phoenix, AZ").', parameters: { type: 'object', properties: { location: { type: 'string', description: 'City name, optionally with country (e.g., "New York", "London", "Tokyo, Japan"). Avoid state abbreviations like "NY" or "AZ".', }, units: { type: 'string', enum: ['metric', 'imperial'], description: 'Unit system: "metric" (Celsius, km/h) or "imperial" (Fahrenheit, mph). Default: metric', }, }, required: ['location'], }, }, }; interface GeocodingResult { name: string; latitude: number; longitude: number; country: string; admin1?: string; } interface WeatherData { temperature: number; apparent_temperature: number; humidity: number; wind_speed: number; wind_direction: number; weather_code: number; is_day: boolean; } // Weather code to description mapping const weatherCodes: Record = { 0: 'Clear sky', 1: 'Mainly clear', 2: 'Partly cloudy', 3: 'Overcast', 45: 'Foggy', 48: 'Depositing rime fog', 51: 'Light drizzle', 53: 'Moderate drizzle', 55: 'Dense drizzle', 56: 'Light freezing drizzle', 57: 'Dense freezing drizzle', 61: 'Slight rain', 63: 'Moderate rain', 65: 'Heavy rain', 66: 'Light freezing rain', 67: 'Heavy freezing rain', 71: 'Slight snow', 73: 'Moderate snow', 75: 'Heavy snow', 77: 'Snow grains', 80: 'Slight rain showers', 81: 'Moderate rain showers', 82: 'Violent rain showers', 85: 'Slight snow showers', 86: 'Heavy snow showers', 95: 'Thunderstorm', 96: 'Thunderstorm with slight hail', 99: 'Thunderstorm with heavy hail', }; function getWindDirection(degrees: number): string { const directions = [ 'N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW', ]; const index = Math.round(degrees / 22.5) % 16; return directions[index]; } /** * Try to geocode a location, with fallback attempts for common formats */ async function geocodeLocation(location: string): Promise { // List of location variations to try const variations = [ location, // Remove state abbreviations like ", NY" or ", AZ" location.replace(/,\s*[A-Z]{2}$/i, ''), // Remove country/state suffixes after comma location.split(',')[0].trim(), // Remove "USA", "United States", etc. location.replace(/,?\s*(USA|United States|US)$/i, '').trim(), ]; // Remove duplicates while preserving order const uniqueVariations = Array.from(new Set(variations.map((v) => v.trim()).filter(Boolean))); for (const query of uniqueVariations) { try { const geoUrl = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(query)}&count=1&language=en&format=json`; const geoResponse = await fetch(geoUrl, { signal: AbortSignal.timeout(10000) }); if (!geoResponse.ok) continue; const geoData = await geoResponse.json(); if (geoData.results && geoData.results.length > 0) { return geoData.results[0]; } } catch { // Try next variation } } return null; } export const weatherHandler: ToolHandler = async (args): Promise => { const location = args.location as string; const units = (args.units as 'metric' | 'imperial') || 'metric'; if (!location) { return { success: false, error: 'No location provided', }; } try { // Geocode the location with fallback attempts const geo = await geocodeLocation(location); if (!geo) { return { success: false, error: `Location not found: "${location}". Try using just the city name (e.g., "Phoenix" instead of "Phoenix, AZ")`, }; } // Fetch weather data const tempUnit = units === 'imperial' ? 'fahrenheit' : 'celsius'; const windUnit = units === 'imperial' ? 'mph' : 'kmh'; const weatherUrl = `https://api.open-meteo.com/v1/forecast?latitude=${geo.latitude}&longitude=${geo.longitude}¤t=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,weather_code,wind_speed_10m,wind_direction_10m&temperature_unit=${tempUnit}&wind_speed_unit=${windUnit}`; const weatherResponse = await fetch(weatherUrl, { signal: AbortSignal.timeout(10000) }); if (!weatherResponse.ok) { throw new Error('Failed to fetch weather data'); } const weatherData = await weatherResponse.json(); const current = weatherData.current; const weather: WeatherData = { temperature: current.temperature_2m, apparent_temperature: current.apparent_temperature, humidity: current.relative_humidity_2m, wind_speed: current.wind_speed_10m, wind_direction: current.wind_direction_10m, weather_code: current.weather_code, is_day: current.is_day === 1, }; const tempSymbol = units === 'imperial' ? '°F' : '°C'; const speedSymbol = units === 'imperial' ? 'mph' : 'km/h'; const condition = weatherCodes[weather.weather_code] || 'Unknown'; const windDir = getWindDirection(weather.wind_direction); const locationName = geo.admin1 ? `${geo.name}, ${geo.admin1}, ${geo.country}` : `${geo.name}, ${geo.country}`; const result = `Weather for ${locationName}: Conditions: ${condition} (${weather.is_day ? 'Day' : 'Night'}) Temperature: ${weather.temperature}${tempSymbol} Feels like: ${weather.apparent_temperature}${tempSymbol} Humidity: ${weather.humidity}% Wind: ${weather.wind_speed} ${speedSymbol} from ${windDir}`; return { success: true, result, }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Failed to get weather', }; } };