| const { Tool } = require('@langchain/core/tools'); |
| const { z } = require('zod'); |
| const { getEnvironmentVariable } = require('@langchain/core/utils/env'); |
| const fetch = require('node-fetch'); |
|
|
| |
| |
| |
| |
| function mapUnitsToOpenWeather(unit) { |
| if (!unit) { |
| return 'metric'; |
| } |
| switch (unit) { |
| case 'Celsius': |
| return 'metric'; |
| case 'Kelvin': |
| return 'standard'; |
| case 'Fahrenheit': |
| return 'imperial'; |
| default: |
| return 'metric'; |
| } |
| } |
|
|
| |
| |
| |
| function roundTemperatures(obj) { |
| const tempKeys = new Set([ |
| 'temp', |
| 'feels_like', |
| 'dew_point', |
| 'day', |
| 'min', |
| 'max', |
| 'night', |
| 'eve', |
| 'morn', |
| 'afternoon', |
| 'morning', |
| 'evening', |
| ]); |
|
|
| if (Array.isArray(obj)) { |
| return obj.map((item) => roundTemperatures(item)); |
| } else if (obj && typeof obj === 'object') { |
| for (const key of Object.keys(obj)) { |
| const value = obj[key]; |
| if (value && typeof value === 'object') { |
| obj[key] = roundTemperatures(value); |
| } else if (typeof value === 'number' && tempKeys.has(key)) { |
| obj[key] = Math.round(value); |
| } |
| } |
| } |
| return obj; |
| } |
|
|
| class OpenWeather extends Tool { |
| name = 'open_weather'; |
| description = |
| 'Provides weather data from OpenWeather One Call API 3.0. ' + |
| 'Actions: help, current_forecast, timestamp, daily_aggregation, overview. ' + |
| 'If lat/lon not provided, specify "city" for geocoding. ' + |
| 'Units: "Celsius", "Kelvin", or "Fahrenheit" (default: Celsius). ' + |
| 'For timestamp action, use "date" in YYYY-MM-DD format.'; |
|
|
| schema = z.object({ |
| action: z.enum(['help', 'current_forecast', 'timestamp', 'daily_aggregation', 'overview']), |
| city: z.string().optional(), |
| lat: z.number().optional(), |
| lon: z.number().optional(), |
| exclude: z.string().optional(), |
| units: z.enum(['Celsius', 'Kelvin', 'Fahrenheit']).optional(), |
| lang: z.string().optional(), |
| date: z.string().optional(), |
| tz: z.string().optional(), |
| }); |
|
|
| constructor(fields = {}) { |
| super(); |
| this.envVar = 'OPENWEATHER_API_KEY'; |
| this.override = fields.override ?? false; |
| this.apiKey = fields[this.envVar] ?? this.getApiKey(); |
| } |
|
|
| getApiKey() { |
| const key = getEnvironmentVariable(this.envVar); |
| if (!key && !this.override) { |
| throw new Error(`Missing ${this.envVar} environment variable.`); |
| } |
| return key; |
| } |
|
|
| async geocodeCity(city) { |
| const geocodeUrl = `https://api.openweathermap.org/geo/1.0/direct?q=${encodeURIComponent( |
| city, |
| )}&limit=1&appid=${this.apiKey}`; |
| const res = await fetch(geocodeUrl); |
| const data = await res.json(); |
| if (!res.ok || !Array.isArray(data) || data.length === 0) { |
| throw new Error(`Could not find coordinates for city: ${city}`); |
| } |
| return { lat: data[0].lat, lon: data[0].lon }; |
| } |
|
|
| convertDateToUnix(dateStr) { |
| const parts = dateStr.split('-'); |
| if (parts.length !== 3) { |
| throw new Error('Invalid date format. Expected YYYY-MM-DD.'); |
| } |
| const year = parseInt(parts[0], 10); |
| const month = parseInt(parts[1], 10); |
| const day = parseInt(parts[2], 10); |
| if (isNaN(year) || isNaN(month) || isNaN(day)) { |
| throw new Error('Invalid date format. Expected YYYY-MM-DD with valid numbers.'); |
| } |
|
|
| const dateObj = new Date(Date.UTC(year, month - 1, day, 0, 0, 0)); |
| if (isNaN(dateObj.getTime())) { |
| throw new Error('Invalid date provided. Cannot parse into a valid date.'); |
| } |
|
|
| return Math.floor(dateObj.getTime() / 1000); |
| } |
|
|
| async _call(args) { |
| try { |
| const { action, city, lat, lon, exclude, units, lang, date, tz } = args; |
| const owmUnits = mapUnitsToOpenWeather(units); |
|
|
| if (action === 'help') { |
| return JSON.stringify( |
| { |
| title: 'OpenWeather One Call API 3.0 Help', |
| description: 'Guidance on using the OpenWeather One Call API 3.0.', |
| endpoints: { |
| current_and_forecast: { |
| endpoint: 'data/3.0/onecall', |
| data_provided: [ |
| 'Current weather', |
| 'Minute forecast (1h)', |
| 'Hourly forecast (48h)', |
| 'Daily forecast (8 days)', |
| 'Government weather alerts', |
| ], |
| required_params: [['lat', 'lon'], ['city']], |
| optional_params: ['exclude', 'units (Celsius/Kelvin/Fahrenheit)', 'lang'], |
| usage_example: { |
| city: 'Knoxville, Tennessee', |
| units: 'Fahrenheit', |
| lang: 'en', |
| }, |
| }, |
| weather_for_timestamp: { |
| endpoint: 'data/3.0/onecall/timemachine', |
| data_provided: [ |
| 'Historical weather (since 1979-01-01)', |
| 'Future forecast up to 4 days ahead', |
| ], |
| required_params: [ |
| ['lat', 'lon', 'date (YYYY-MM-DD)'], |
| ['city', 'date (YYYY-MM-DD)'], |
| ], |
| optional_params: ['units (Celsius/Kelvin/Fahrenheit)', 'lang'], |
| usage_example: { |
| city: 'Knoxville, Tennessee', |
| date: '2020-03-04', |
| units: 'Fahrenheit', |
| lang: 'en', |
| }, |
| }, |
| daily_aggregation: { |
| endpoint: 'data/3.0/onecall/day_summary', |
| data_provided: [ |
| 'Aggregated weather data for a specific date (1979-01-02 to 1.5 years ahead)', |
| ], |
| required_params: [ |
| ['lat', 'lon', 'date (YYYY-MM-DD)'], |
| ['city', 'date (YYYY-MM-DD)'], |
| ], |
| optional_params: ['units (Celsius/Kelvin/Fahrenheit)', 'lang', 'tz'], |
| usage_example: { |
| city: 'Knoxville, Tennessee', |
| date: '2020-03-04', |
| units: 'Celsius', |
| lang: 'en', |
| }, |
| }, |
| weather_overview: { |
| endpoint: 'data/3.0/onecall/overview', |
| data_provided: ['Human-readable weather summary (today/tomorrow)'], |
| required_params: [['lat', 'lon'], ['city']], |
| optional_params: ['date (YYYY-MM-DD)', 'units (Celsius/Kelvin/Fahrenheit)'], |
| usage_example: { |
| city: 'Knoxville, Tennessee', |
| date: '2024-05-13', |
| units: 'Celsius', |
| }, |
| }, |
| }, |
| notes: [ |
| 'If lat/lon not provided, you can specify a city name and it will be geocoded.', |
| 'For the timestamp action, provide a date in YYYY-MM-DD format instead of a Unix timestamp.', |
| 'By default, temperatures are returned in Celsius.', |
| 'You can specify units as Celsius, Kelvin, or Fahrenheit.', |
| 'All temperatures are rounded to the nearest degree.', |
| ], |
| errors: [ |
| '400: Bad Request (missing/invalid params)', |
| '401: Unauthorized (check API key)', |
| '404: Not Found (no data or city)', |
| '429: Too many requests', |
| '5xx: Internal error', |
| ], |
| }, |
| null, |
| 2, |
| ); |
| } |
|
|
| let finalLat = lat; |
| let finalLon = lon; |
|
|
| |
| if ((finalLat == null || finalLon == null) && city) { |
| const coords = await this.geocodeCity(city); |
| finalLat = coords.lat; |
| finalLon = coords.lon; |
| } |
|
|
| if (['current_forecast', 'timestamp', 'daily_aggregation', 'overview'].includes(action)) { |
| if (typeof finalLat !== 'number' || typeof finalLon !== 'number') { |
| return 'Error: lat and lon are required and must be numbers for this action (or specify \'city\').'; |
| } |
| } |
|
|
| const baseUrl = 'https://api.openweathermap.org/data/3.0'; |
| let endpoint = ''; |
| const params = new URLSearchParams({ appid: this.apiKey, units: owmUnits }); |
|
|
| let dt; |
| if (action === 'timestamp') { |
| if (!date) { |
| return 'Error: For timestamp action, a \'date\' in YYYY-MM-DD format is required.'; |
| } |
| dt = this.convertDateToUnix(date); |
| } |
|
|
| if (action === 'daily_aggregation' && !date) { |
| return 'Error: date (YYYY-MM-DD) is required for daily_aggregation action.'; |
| } |
|
|
| switch (action) { |
| case 'current_forecast': |
| endpoint = '/onecall'; |
| params.append('lat', String(finalLat)); |
| params.append('lon', String(finalLon)); |
| if (exclude) { |
| params.append('exclude', exclude); |
| } |
| if (lang) { |
| params.append('lang', lang); |
| } |
| break; |
| case 'timestamp': |
| endpoint = '/onecall/timemachine'; |
| params.append('lat', String(finalLat)); |
| params.append('lon', String(finalLon)); |
| params.append('dt', String(dt)); |
| if (lang) { |
| params.append('lang', lang); |
| } |
| break; |
| case 'daily_aggregation': |
| endpoint = '/onecall/day_summary'; |
| params.append('lat', String(finalLat)); |
| params.append('lon', String(finalLon)); |
| params.append('date', date); |
| if (lang) { |
| params.append('lang', lang); |
| } |
| if (tz) { |
| params.append('tz', tz); |
| } |
| break; |
| case 'overview': |
| endpoint = '/onecall/overview'; |
| params.append('lat', String(finalLat)); |
| params.append('lon', String(finalLon)); |
| if (date) { |
| params.append('date', date); |
| } |
| break; |
| default: |
| return `Error: Unknown action: ${action}`; |
| } |
|
|
| const url = `${baseUrl}${endpoint}?${params.toString()}`; |
| const response = await fetch(url); |
| const json = await response.json(); |
| if (!response.ok) { |
| return `Error: OpenWeather API request failed with status ${response.status}: ${ |
| json.message || JSON.stringify(json) |
| }`; |
| } |
|
|
| const roundedJson = roundTemperatures(json); |
| return JSON.stringify(roundedJson); |
| } catch (err) { |
| return `Error: ${err.message}`; |
| } |
| } |
| } |
|
|
| module.exports = OpenWeather; |
|
|