| | 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; |
| |
|