From a4e7a6d20fce72c6c41391f7bfc77364db9937cc Mon Sep 17 00:00:00 2001 From: Jonathan Addington Date: Tue, 7 Jan 2025 21:30:50 +0000 Subject: [PATCH 1/5] =?UTF-8?q?=E2=9C=A8=20feat:=20Add=20OpenWeather=20Too?= =?UTF-8?q?l=20for=20Weather=20Data=20Retrieval=20=F0=9F=8C=A4=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 8 +- api/app/clients/index.js | 1 + .../__tests__/openWeather.integration.test.js | 224 ++++++++++++ .../tools/__tests__/openweather.test.js | 343 ++++++++++++++++++ api/app/clients/tools/index.js | 2 + api/app/clients/tools/manifest.json | 10 +- .../clients/tools/structured/OpenWeather.js | 293 +++++++++++++++ api/app/clients/tools/util/handleTools.js | 4 +- client/public/assets/openweather.png | Bin 0 -> 6799 bytes 9 files changed, 882 insertions(+), 3 deletions(-) create mode 100644 api/app/clients/tools/__tests__/openWeather.integration.test.js create mode 100644 api/app/clients/tools/__tests__/openweather.test.js create mode 100644 api/app/clients/tools/structured/OpenWeather.js create mode 100644 client/public/assets/openweather.png diff --git a/.env.example b/.env.example index f2a51198f42..1e27f0b9710 100644 --- a/.env.example +++ b/.env.example @@ -256,6 +256,7 @@ AZURE_AI_SEARCH_SEARCH_OPTION_SELECT= # DALLE3_AZURE_API_VERSION= # DALLE2_AZURE_API_VERSION= + # Google #----------------- GOOGLE_SEARCH_API_KEY= @@ -514,4 +515,9 @@ HELP_AND_FAQ_URL=https://librechat.ai # no-cache: Forces validation with server before using cached version # no-store: Prevents storing the response entirely -# must-revalidate: Prevents using stale content when offline \ No newline at end of file +# must-revalidate: Prevents using stale content when offline + +#=====================================================# +# OpenWeather # +#=====================================================# +OPENWEATHER_API_KEY= \ No newline at end of file diff --git a/api/app/clients/index.js b/api/app/clients/index.js index a5e8eee5045..5d34ec5ff38 100644 --- a/api/app/clients/index.js +++ b/api/app/clients/index.js @@ -6,6 +6,7 @@ const TextStream = require('./TextStream'); const AnthropicClient = require('./AnthropicClient'); const toolUtils = require('./tools/util'); + module.exports = { ChatGPTClient, OpenAIClient, diff --git a/api/app/clients/tools/__tests__/openWeather.integration.test.js b/api/app/clients/tools/__tests__/openWeather.integration.test.js new file mode 100644 index 00000000000..8de2a081fe9 --- /dev/null +++ b/api/app/clients/tools/__tests__/openWeather.integration.test.js @@ -0,0 +1,224 @@ +// __tests__/openWeather.integration.test.js +const OpenWeather = require('../structured/OpenWeather'); + +describe('OpenWeather Tool (Integration Test)', () => { + let tool; + + beforeAll(() => { + tool = new OpenWeather(); + console.log('API Key present:', !!process.env.OPENWEATHER_API_KEY); + }); + + test('current_forecast with a real API key returns current weather', async () => { + // Check if API key is available + if (!process.env.OPENWEATHER_API_KEY) { + console.warn("Skipping real API test, no OPENWEATHER_API_KEY found."); + return; + } + + try { + const result = await tool.call({ + action: 'current_forecast', + city: 'London', + units: 'Celsius' + }); + + console.log('Raw API response:', result); + + const parsed = JSON.parse(result); + expect(parsed).toHaveProperty('current'); + expect(typeof parsed.current.temp).toBe('number'); + } catch (error) { + console.error('Test failed with error:', error); + throw error; + } + }); + + test('timestamp action with real API key returns historical data', async () => { + if (!process.env.OPENWEATHER_API_KEY) { + console.warn("Skipping real API test, no OPENWEATHER_API_KEY found."); + return; + } + + try { + // Use a date from yesterday to ensure data availability + const yesterday = new Date(); + yesterday.setDate(yesterday.getDate() - 1); + const dateStr = yesterday.toISOString().split('T')[0]; + + const result = await tool.call({ + action: 'timestamp', + city: 'London', + date: dateStr, + units: 'Celsius' + }); + + console.log('Timestamp API response:', result); + + const parsed = JSON.parse(result); + expect(parsed).toHaveProperty('data'); + expect(Array.isArray(parsed.data)).toBe(true); + expect(parsed.data[0]).toHaveProperty('temp'); + } catch (error) { + console.error('Timestamp test failed with error:', error); + throw error; + } + }); + + test('daily_aggregation action with real API key returns aggregated data', async () => { + if (!process.env.OPENWEATHER_API_KEY) { + console.warn("Skipping real API test, no OPENWEATHER_API_KEY found."); + return; + } + + try { + // Use yesterday's date for aggregation + const yesterday = new Date(); + yesterday.setDate(yesterday.getDate() - 1); + const dateStr = yesterday.toISOString().split('T')[0]; + + const result = await tool.call({ + action: 'daily_aggregation', + city: 'London', + date: dateStr, + units: 'Celsius' + }); + + console.log('Daily aggregation API response:', result); + + const parsed = JSON.parse(result); + expect(parsed).toHaveProperty('temperature'); + expect(parsed.temperature).toHaveProperty('morning'); + expect(parsed.temperature).toHaveProperty('afternoon'); + expect(parsed.temperature).toHaveProperty('evening'); + } catch (error) { + console.error('Daily aggregation test failed with error:', error); + throw error; + } + }); + + test('overview action with real API key returns weather summary', async () => { + if (!process.env.OPENWEATHER_API_KEY) { + console.warn("Skipping real API test, no OPENWEATHER_API_KEY found."); + return; + } + + try { + const result = await tool.call({ + action: 'overview', + city: 'London', + units: 'Celsius' + }); + + console.log('Overview API response:', result); + + const parsed = JSON.parse(result); + expect(parsed).toHaveProperty('weather_overview'); + expect(typeof parsed.weather_overview).toBe('string'); + expect(parsed.weather_overview.length).toBeGreaterThan(0); + expect(parsed).toHaveProperty('date'); + expect(parsed).toHaveProperty('units'); + expect(parsed.units).toBe('metric'); + } catch (error) { + console.error('Overview test failed with error:', error); + throw error; + } + }); + + test('different temperature units return correct values', async () => { + if (!process.env.OPENWEATHER_API_KEY) { + console.warn("Skipping real API test, no OPENWEATHER_API_KEY found."); + return; + } + + try { + // Test Celsius + let result = await tool.call({ + action: 'current_forecast', + city: 'London', + units: 'Celsius' + }); + let parsed = JSON.parse(result); + const celsiusTemp = parsed.current.temp; + + // Test Kelvin + result = await tool.call({ + action: 'current_forecast', + city: 'London', + units: 'Kelvin' + }); + parsed = JSON.parse(result); + const kelvinTemp = parsed.current.temp; + + // Test Fahrenheit + result = await tool.call({ + action: 'current_forecast', + city: 'London', + units: 'Fahrenheit' + }); + parsed = JSON.parse(result); + const fahrenheitTemp = parsed.current.temp; + + // Verify temperature conversions are roughly correct + // K = C + 273.15 + // F = (C * 9/5) + 32 + const celsiusToKelvin = Math.round(celsiusTemp + 273.15); + const celsiusToFahrenheit = Math.round((celsiusTemp * 9/5) + 32); + + console.log('Temperature comparisons:', { + celsius: celsiusTemp, + kelvin: kelvinTemp, + fahrenheit: fahrenheitTemp, + calculatedKelvin: celsiusToKelvin, + calculatedFahrenheit: celsiusToFahrenheit + }); + + // Allow for some rounding differences + expect(Math.abs(kelvinTemp - celsiusToKelvin)).toBeLessThanOrEqual(1); + expect(Math.abs(fahrenheitTemp - celsiusToFahrenheit)).toBeLessThanOrEqual(1); + } catch (error) { + console.error('Temperature units test failed with error:', error); + throw error; + } + }); + + test('language parameter returns localized data', async () => { + if (!process.env.OPENWEATHER_API_KEY) { + console.warn("Skipping real API test, no OPENWEATHER_API_KEY found."); + return; + } + + try { + // Test with English + let result = await tool.call({ + action: 'current_forecast', + city: 'Paris', + units: 'Celsius', + lang: 'en' + }); + let parsed = JSON.parse(result); + const englishDescription = parsed.current.weather[0].description; + + // Test with French + result = await tool.call({ + action: 'current_forecast', + city: 'Paris', + units: 'Celsius', + lang: 'fr' + }); + parsed = JSON.parse(result); + const frenchDescription = parsed.current.weather[0].description; + + console.log('Language comparison:', { + english: englishDescription, + french: frenchDescription + }); + + // Verify descriptions are different (indicating translation worked) + expect(englishDescription).not.toBe(frenchDescription); + } catch (error) { + console.error('Language test failed with error:', error); + throw error; + } + }); +}); \ No newline at end of file diff --git a/api/app/clients/tools/__tests__/openweather.test.js b/api/app/clients/tools/__tests__/openweather.test.js new file mode 100644 index 00000000000..7bb43a7743c --- /dev/null +++ b/api/app/clients/tools/__tests__/openweather.test.js @@ -0,0 +1,343 @@ +// __tests__/openweather.test.js +const OpenWeather = require('../structured/OpenWeather'); +const fetch = require('node-fetch'); + +// Mock environment variable +process.env.OPENWEATHER_API_KEY = 'test-api-key'; + +// Mock the fetch function globally +jest.mock('node-fetch', () => jest.fn()); + +describe('OpenWeather Tool', () => { + let tool; + + beforeAll(() => { + tool = new OpenWeather(); + }); + + beforeEach(() => { + fetch.mockReset(); + }); + + test('action=help returns help instructions', async () => { + const result = await tool.call({ + action: 'help' + }); + + expect(typeof result).toBe('string'); + const parsed = JSON.parse(result); + expect(parsed.title).toBe('OpenWeather One Call API 3.0 Help'); + }); + + test('current_forecast with a city and successful geocoding + forecast', async () => { + // Mock geocoding response + fetch.mockImplementationOnce((url) => { + if (url.includes('geo/1.0/direct')) { + return Promise.resolve({ + ok: true, + json: async () => [{ lat: 35.9606, lon: -83.9207 }] + }); + } + return Promise.reject('Unexpected fetch call for geocoding'); + }); + + // Mock forecast response + fetch.mockImplementationOnce(() => Promise.resolve({ + ok: true, + json: async () => ({ + current: { temp: 293.15, feels_like: 295.15 }, + daily: [{ temp: { day: 293.15, night: 283.15 } }] + }) + })); + + const result = await tool.call({ + action: 'current_forecast', + city: 'Knoxville, Tennessee', + units: 'Kelvin' + }); + + const parsed = JSON.parse(result); + expect(parsed.current.temp).toBe(293); + expect(parsed.current.feels_like).toBe(295); + expect(parsed.daily[0].temp.day).toBe(293); + expect(parsed.daily[0].temp.night).toBe(283); + }); + + test('timestamp action with valid date returns mocked historical data', async () => { + // Mock geocoding response + fetch.mockImplementationOnce((url) => { + if (url.includes('geo/1.0/direct')) { + return Promise.resolve({ + ok: true, + json: async () => [{ lat: 35.9606, lon: -83.9207 }] + }); + } + return Promise.reject('Unexpected fetch call for geocoding'); + }); + + // Mock historical weather response + fetch.mockImplementationOnce(() => Promise.resolve({ + ok: true, + json: async () => ({ + data: [{ + dt: 1583280000, + temp: 283.15, + feels_like: 280.15, + humidity: 75, + weather: [{ description: 'clear sky' }] + }] + }) + })); + + const result = await tool.call({ + action: 'timestamp', + city: 'Knoxville, Tennessee', + date: '2020-03-04', + units: 'Kelvin' + }); + + const parsed = JSON.parse(result); + expect(parsed.data[0].temp).toBe(283); + expect(parsed.data[0].feels_like).toBe(280); + }); + + test('daily_aggregation action returns aggregated weather data', async () => { + // Mock geocoding response + fetch.mockImplementationOnce((url) => { + if (url.includes('geo/1.0/direct')) { + return Promise.resolve({ + ok: true, + json: async () => [{ lat: 35.9606, lon: -83.9207 }] + }); + } + return Promise.reject('Unexpected fetch call for geocoding'); + }); + + // Mock daily aggregation response + fetch.mockImplementationOnce(() => Promise.resolve({ + ok: true, + json: async () => ({ + date: '2020-03-04', + temperature: { + morning: 283.15, + afternoon: 293.15, + evening: 288.15 + }, + humidity: { + morning: 75, + afternoon: 60, + evening: 70 + } + }) + })); + + const result = await tool.call({ + action: 'daily_aggregation', + city: 'Knoxville, Tennessee', + date: '2020-03-04', + units: 'Kelvin' + }); + + const parsed = JSON.parse(result); + expect(parsed.temperature.morning).toBe(283); + expect(parsed.temperature.afternoon).toBe(293); + expect(parsed.temperature.evening).toBe(288); + }); + + test('overview action returns weather summary', async () => { + // Mock geocoding response + fetch.mockImplementationOnce((url) => { + if (url.includes('geo/1.0/direct')) { + return Promise.resolve({ + ok: true, + json: async () => [{ lat: 35.9606, lon: -83.9207 }] + }); + } + return Promise.reject('Unexpected fetch call for geocoding'); + }); + + // Mock overview response + fetch.mockImplementationOnce(() => Promise.resolve({ + ok: true, + json: async () => ({ + date: '2024-01-07', + lat: 35.9606, + lon: -83.9207, + tz: '+00:00', + units: 'metric', + weather_overview: 'Currently, the temperature is 2°C with a real feel of -2°C. The sky is clear with moderate wind.' + }) + })); + + const result = await tool.call({ + action: 'overview', + city: 'Knoxville, Tennessee', + units: 'Celsius' + }); + + const parsed = JSON.parse(result); + expect(parsed).toHaveProperty('weather_overview'); + expect(typeof parsed.weather_overview).toBe('string'); + expect(parsed.weather_overview.length).toBeGreaterThan(0); + expect(parsed).toHaveProperty('date'); + expect(parsed).toHaveProperty('units'); + expect(parsed.units).toBe('metric'); + }); + + test('temperature units are correctly converted', async () => { + // Mock geocoding response for all three calls + const geocodingMock = Promise.resolve({ + ok: true, + json: async () => [{ lat: 35.9606, lon: -83.9207 }] + }); + + // Mock weather response for Kelvin + const kelvinMock = Promise.resolve({ + ok: true, + json: async () => ({ + current: { temp: 293.15 } + }) + }); + + // Mock weather response for Celsius + const celsiusMock = Promise.resolve({ + ok: true, + json: async () => ({ + current: { temp: 20 } + }) + }); + + // Mock weather response for Fahrenheit + const fahrenheitMock = Promise.resolve({ + ok: true, + json: async () => ({ + current: { temp: 68 } + }) + }); + + // Test Kelvin + fetch + .mockImplementationOnce(() => geocodingMock) + .mockImplementationOnce(() => kelvinMock); + + let result = await tool.call({ + action: 'current_forecast', + city: 'Knoxville, Tennessee', + units: 'Kelvin' + }); + let parsed = JSON.parse(result); + expect(parsed.current.temp).toBe(293); + + // Test Celsius + fetch + .mockImplementationOnce(() => geocodingMock) + .mockImplementationOnce(() => celsiusMock); + + result = await tool.call({ + action: 'current_forecast', + city: 'Knoxville, Tennessee', + units: 'Celsius' + }); + parsed = JSON.parse(result); + expect(parsed.current.temp).toBe(20); + + // Test Fahrenheit + fetch + .mockImplementationOnce(() => geocodingMock) + .mockImplementationOnce(() => fahrenheitMock); + + result = await tool.call({ + action: 'current_forecast', + city: 'Knoxville, Tennessee', + units: 'Fahrenheit' + }); + parsed = JSON.parse(result); + expect(parsed.current.temp).toBe(68); + }); + + test('timestamp action without a date returns an error message', async () => { + const result = await tool.call({ + action: 'timestamp', + lat: 35.9606, + lon: -83.9207 + }); + expect(result).toMatch(/Error: For timestamp action, a 'date' in YYYY-MM-DD format is required./); + }); + + test('daily_aggregation action without a date returns an error message', async () => { + const result = await tool.call({ + action: 'daily_aggregation', + lat: 35.9606, + lon: -83.9207 + }); + expect(result).toMatch(/Error: date \(YYYY-MM-DD\) is required for daily_aggregation action./); + }); + + test('unknown action returns an error due to schema validation', async () => { + await expect(tool.call({ + action: 'unknown_action' + })).rejects.toThrow(/Received tool input did not match expected schema/); + }); + + test('geocoding failure returns a descriptive error', async () => { + fetch.mockImplementationOnce(() => Promise.resolve({ + ok: true, + json: async () => [] + })); + + const result = await tool.call({ + action: 'current_forecast', + city: 'NowhereCity' + }); + expect(result).toMatch(/Error: Could not find coordinates for city: NowhereCity/); + }); + + test('API request failure returns an error', async () => { + // Mock geocoding success + fetch.mockImplementationOnce(() => Promise.resolve({ + ok: true, + json: async () => [{ lat: 35.9606, lon: -83.9207 }] + })); + + // Mock weather request failure + fetch.mockImplementationOnce(() => Promise.resolve({ + ok: false, + status: 404, + json: async () => ({ message: 'Not found' }) + })); + + const result = await tool.call({ + action: 'current_forecast', + city: 'Knoxville, Tennessee' + }); + expect(result).toMatch(/Error: OpenWeather API request failed with status 404: Not found/); + }); + + test('invalid date format returns an error', async () => { + // Mock geocoding response first + fetch.mockImplementationOnce((url) => { + if (url.includes('geo/1.0/direct')) { + return Promise.resolve({ + ok: true, + json: async () => [{ lat: 35.9606, lon: -83.9207 }] + }); + } + return Promise.reject('Unexpected fetch call for geocoding'); + }); + + // Mock timestamp API response + fetch.mockImplementationOnce((url) => { + if (url.includes('onecall/timemachine')) { + throw new Error('Invalid date format. Expected YYYY-MM-DD.'); + } + return Promise.reject('Unexpected fetch call'); + }); + + const result = await tool.call({ + action: 'timestamp', + city: 'Knoxville, Tennessee', + date: '03-04-2020' // Wrong format + }); + expect(result).toMatch(/Error: Invalid date format. Expected YYYY-MM-DD./); + }); +}); \ No newline at end of file diff --git a/api/app/clients/tools/index.js b/api/app/clients/tools/index.js index a8532d4581f..176c26dd78d 100644 --- a/api/app/clients/tools/index.js +++ b/api/app/clients/tools/index.js @@ -8,6 +8,7 @@ const StructuredSD = require('./structured/StableDiffusion'); const GoogleSearchAPI = require('./structured/GoogleSearch'); const TraversaalSearch = require('./structured/TraversaalSearch'); const TavilySearchResults = require('./structured/TavilySearchResults'); +const OpenWeather = require('./structured/OpenWeather'); module.exports = { availableTools, @@ -19,4 +20,5 @@ module.exports = { TraversaalSearch, StructuredWolfram, TavilySearchResults, + OpenWeather, }; diff --git a/api/app/clients/tools/manifest.json b/api/app/clients/tools/manifest.json index d2748cdea11..4e0b3ab226b 100644 --- a/api/app/clients/tools/manifest.json +++ b/api/app/clients/tools/manifest.json @@ -135,8 +135,16 @@ { "authField": "AZURE_AI_SEARCH_API_KEY", "label": "Azure AI Search API Key", - "description": "You need to provideq your API Key for Azure AI Search." + "description": "You need to provide your API Key for Azure AI Search." } ] + }, + { + "name": "OpenWeather", + "pluginKey": "OpenWeather", + "description": "Get weather forecasts and historical data from the OpenWeather API", + "icon": "/assets/flux.png", + "isAuthRequired": "false", + "authConfig": [] } ] diff --git a/api/app/clients/tools/structured/OpenWeather.js b/api/app/clients/tools/structured/OpenWeather.js new file mode 100644 index 00000000000..b9f8cbbc9de --- /dev/null +++ b/api/app/clients/tools/structured/OpenWeather.js @@ -0,0 +1,293 @@ +const { Tool } = require('@langchain/core/tools'); +const { z } = require('zod'); +const { getEnvironmentVariable } = require('@langchain/core/utils/env'); +const fetch = require('node-fetch'); + +// Utility to retrieve API key +function getApiKey(envVar, override, providedKey) { + if (providedKey) return providedKey; + const key = getEnvironmentVariable(envVar); + if (!key && !override) { + throw new Error(`Missing ${envVar} environment variable.`); + } + return key; +} + +/** + * Map user-friendly units to OpenWeather units. + * Defaults to Celsius if not specified. + */ +function mapUnitsToOpenWeather(unit) { + if (!unit) return 'metric'; // Default to Celsius + switch (unit) { + case 'Celsius': + return 'metric'; + case 'Kelvin': + return 'standard'; + case 'Fahrenheit': + return 'imperial'; + default: + return 'metric'; // fallback + } +} + +/** + * Recursively round temperature fields in the API response. + */ +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 = 'OpenWeather'; + 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(), // For timestamp and daily_aggregation + tz: z.string().optional() + }); + + constructor(options = {}) { + super(); + const { apiKey, override = false } = options; + this.apiKey = getApiKey('OPENWEATHER_API_KEY', override, apiKey); + } + + 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 lat/lon not provided but city is given, geocode it + 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; diff --git a/api/app/clients/tools/util/handleTools.js b/api/app/clients/tools/util/handleTools.js index a8ee50c3d4d..743dc37647d 100644 --- a/api/app/clients/tools/util/handleTools.js +++ b/api/app/clients/tools/util/handleTools.js @@ -14,6 +14,7 @@ const { TraversaalSearch, StructuredWolfram, TavilySearchResults, + OpenWeather, } = require('../'); const { primeFiles: primeCodeFiles } = require('~/server/services/Files/Code/process'); const { createFileSearchTool, primeFiles: primeSearchFiles } = require('./fileSearch'); @@ -178,6 +179,7 @@ const loadTools = async ({ 'azure-ai-search': StructuredACS, traversaal_search: TraversaalSearch, tavily_search_results_json: TavilySearchResults, + OpenWeather: OpenWeather, }; const customConstructors = { @@ -213,7 +215,7 @@ const loadTools = async ({ const toolOptions = { serpapi: { location: 'Austin,Texas,United States', hl: 'en', gl: 'us' }, dalle: imageGenOptions, - 'stable-diffusion': imageGenOptions, + 'stable-diffusion': imageGenOptions, }; const toolAuthFields = {}; diff --git a/client/public/assets/openweather.png b/client/public/assets/openweather.png new file mode 100644 index 0000000000000000000000000000000000000000..0e4d63a2bc23ab715426eaf6d6b686537179468b GIT binary patch literal 6799 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 literal 0 HcmV?d00001 From 87f8308a5100916595948b42f499c4599da16c33 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Fri, 10 Jan 2025 08:09:07 -0500 Subject: [PATCH 2/5] chore: linting --- api/app/clients/index.js | 1 - .../__tests__/openWeather.integration.test.js | 42 +-- .../tools/__tests__/openweather.test.js | 211 ++++++------ .../clients/tools/structured/OpenWeather.js | 303 ++++++++++-------- api/app/clients/tools/util/handleTools.js | 2 +- 5 files changed, 300 insertions(+), 259 deletions(-) diff --git a/api/app/clients/index.js b/api/app/clients/index.js index 5d34ec5ff38..a5e8eee5045 100644 --- a/api/app/clients/index.js +++ b/api/app/clients/index.js @@ -6,7 +6,6 @@ const TextStream = require('./TextStream'); const AnthropicClient = require('./AnthropicClient'); const toolUtils = require('./tools/util'); - module.exports = { ChatGPTClient, OpenAIClient, diff --git a/api/app/clients/tools/__tests__/openWeather.integration.test.js b/api/app/clients/tools/__tests__/openWeather.integration.test.js index 8de2a081fe9..c57c6956853 100644 --- a/api/app/clients/tools/__tests__/openWeather.integration.test.js +++ b/api/app/clients/tools/__tests__/openWeather.integration.test.js @@ -12,7 +12,7 @@ describe('OpenWeather Tool (Integration Test)', () => { test('current_forecast with a real API key returns current weather', async () => { // Check if API key is available if (!process.env.OPENWEATHER_API_KEY) { - console.warn("Skipping real API test, no OPENWEATHER_API_KEY found."); + console.warn('Skipping real API test, no OPENWEATHER_API_KEY found.'); return; } @@ -20,7 +20,7 @@ describe('OpenWeather Tool (Integration Test)', () => { const result = await tool.call({ action: 'current_forecast', city: 'London', - units: 'Celsius' + units: 'Celsius', }); console.log('Raw API response:', result); @@ -36,7 +36,7 @@ describe('OpenWeather Tool (Integration Test)', () => { test('timestamp action with real API key returns historical data', async () => { if (!process.env.OPENWEATHER_API_KEY) { - console.warn("Skipping real API test, no OPENWEATHER_API_KEY found."); + console.warn('Skipping real API test, no OPENWEATHER_API_KEY found.'); return; } @@ -50,7 +50,7 @@ describe('OpenWeather Tool (Integration Test)', () => { action: 'timestamp', city: 'London', date: dateStr, - units: 'Celsius' + units: 'Celsius', }); console.log('Timestamp API response:', result); @@ -67,7 +67,7 @@ describe('OpenWeather Tool (Integration Test)', () => { test('daily_aggregation action with real API key returns aggregated data', async () => { if (!process.env.OPENWEATHER_API_KEY) { - console.warn("Skipping real API test, no OPENWEATHER_API_KEY found."); + console.warn('Skipping real API test, no OPENWEATHER_API_KEY found.'); return; } @@ -81,7 +81,7 @@ describe('OpenWeather Tool (Integration Test)', () => { action: 'daily_aggregation', city: 'London', date: dateStr, - units: 'Celsius' + units: 'Celsius', }); console.log('Daily aggregation API response:', result); @@ -99,7 +99,7 @@ describe('OpenWeather Tool (Integration Test)', () => { test('overview action with real API key returns weather summary', async () => { if (!process.env.OPENWEATHER_API_KEY) { - console.warn("Skipping real API test, no OPENWEATHER_API_KEY found."); + console.warn('Skipping real API test, no OPENWEATHER_API_KEY found.'); return; } @@ -107,7 +107,7 @@ describe('OpenWeather Tool (Integration Test)', () => { const result = await tool.call({ action: 'overview', city: 'London', - units: 'Celsius' + units: 'Celsius', }); console.log('Overview API response:', result); @@ -127,7 +127,7 @@ describe('OpenWeather Tool (Integration Test)', () => { test('different temperature units return correct values', async () => { if (!process.env.OPENWEATHER_API_KEY) { - console.warn("Skipping real API test, no OPENWEATHER_API_KEY found."); + console.warn('Skipping real API test, no OPENWEATHER_API_KEY found.'); return; } @@ -136,25 +136,25 @@ describe('OpenWeather Tool (Integration Test)', () => { let result = await tool.call({ action: 'current_forecast', city: 'London', - units: 'Celsius' + units: 'Celsius', }); let parsed = JSON.parse(result); const celsiusTemp = parsed.current.temp; - + // Test Kelvin result = await tool.call({ action: 'current_forecast', city: 'London', - units: 'Kelvin' + units: 'Kelvin', }); parsed = JSON.parse(result); const kelvinTemp = parsed.current.temp; - + // Test Fahrenheit result = await tool.call({ action: 'current_forecast', city: 'London', - units: 'Fahrenheit' + units: 'Fahrenheit', }); parsed = JSON.parse(result); const fahrenheitTemp = parsed.current.temp; @@ -163,14 +163,14 @@ describe('OpenWeather Tool (Integration Test)', () => { // K = C + 273.15 // F = (C * 9/5) + 32 const celsiusToKelvin = Math.round(celsiusTemp + 273.15); - const celsiusToFahrenheit = Math.round((celsiusTemp * 9/5) + 32); + const celsiusToFahrenheit = Math.round((celsiusTemp * 9) / 5 + 32); console.log('Temperature comparisons:', { celsius: celsiusTemp, kelvin: kelvinTemp, fahrenheit: fahrenheitTemp, calculatedKelvin: celsiusToKelvin, - calculatedFahrenheit: celsiusToFahrenheit + calculatedFahrenheit: celsiusToFahrenheit, }); // Allow for some rounding differences @@ -184,7 +184,7 @@ describe('OpenWeather Tool (Integration Test)', () => { test('language parameter returns localized data', async () => { if (!process.env.OPENWEATHER_API_KEY) { - console.warn("Skipping real API test, no OPENWEATHER_API_KEY found."); + console.warn('Skipping real API test, no OPENWEATHER_API_KEY found.'); return; } @@ -194,7 +194,7 @@ describe('OpenWeather Tool (Integration Test)', () => { action: 'current_forecast', city: 'Paris', units: 'Celsius', - lang: 'en' + lang: 'en', }); let parsed = JSON.parse(result); const englishDescription = parsed.current.weather[0].description; @@ -204,14 +204,14 @@ describe('OpenWeather Tool (Integration Test)', () => { action: 'current_forecast', city: 'Paris', units: 'Celsius', - lang: 'fr' + lang: 'fr', }); parsed = JSON.parse(result); const frenchDescription = parsed.current.weather[0].description; console.log('Language comparison:', { english: englishDescription, - french: frenchDescription + french: frenchDescription, }); // Verify descriptions are different (indicating translation worked) @@ -221,4 +221,4 @@ describe('OpenWeather Tool (Integration Test)', () => { throw error; } }); -}); \ No newline at end of file +}); diff --git a/api/app/clients/tools/__tests__/openweather.test.js b/api/app/clients/tools/__tests__/openweather.test.js index 7bb43a7743c..a6f6be6a4fe 100644 --- a/api/app/clients/tools/__tests__/openweather.test.js +++ b/api/app/clients/tools/__tests__/openweather.test.js @@ -21,7 +21,7 @@ describe('OpenWeather Tool', () => { test('action=help returns help instructions', async () => { const result = await tool.call({ - action: 'help' + action: 'help', }); expect(typeof result).toBe('string'); @@ -35,25 +35,27 @@ describe('OpenWeather Tool', () => { if (url.includes('geo/1.0/direct')) { return Promise.resolve({ ok: true, - json: async () => [{ lat: 35.9606, lon: -83.9207 }] + json: async () => [{ lat: 35.9606, lon: -83.9207 }], }); } return Promise.reject('Unexpected fetch call for geocoding'); }); // Mock forecast response - fetch.mockImplementationOnce(() => Promise.resolve({ - ok: true, - json: async () => ({ - current: { temp: 293.15, feels_like: 295.15 }, - daily: [{ temp: { day: 293.15, night: 283.15 } }] - }) - })); + fetch.mockImplementationOnce(() => + Promise.resolve({ + ok: true, + json: async () => ({ + current: { temp: 293.15, feels_like: 295.15 }, + daily: [{ temp: { day: 293.15, night: 283.15 } }], + }), + }), + ); const result = await tool.call({ action: 'current_forecast', city: 'Knoxville, Tennessee', - units: 'Kelvin' + units: 'Kelvin', }); const parsed = JSON.parse(result); @@ -69,31 +71,35 @@ describe('OpenWeather Tool', () => { if (url.includes('geo/1.0/direct')) { return Promise.resolve({ ok: true, - json: async () => [{ lat: 35.9606, lon: -83.9207 }] + json: async () => [{ lat: 35.9606, lon: -83.9207 }], }); } return Promise.reject('Unexpected fetch call for geocoding'); }); // Mock historical weather response - fetch.mockImplementationOnce(() => Promise.resolve({ - ok: true, - json: async () => ({ - data: [{ - dt: 1583280000, - temp: 283.15, - feels_like: 280.15, - humidity: 75, - weather: [{ description: 'clear sky' }] - }] - }) - })); + fetch.mockImplementationOnce(() => + Promise.resolve({ + ok: true, + json: async () => ({ + data: [ + { + dt: 1583280000, + temp: 283.15, + feels_like: 280.15, + humidity: 75, + weather: [{ description: 'clear sky' }], + }, + ], + }), + }), + ); const result = await tool.call({ action: 'timestamp', city: 'Knoxville, Tennessee', date: '2020-03-04', - units: 'Kelvin' + units: 'Kelvin', }); const parsed = JSON.parse(result); @@ -107,35 +113,37 @@ describe('OpenWeather Tool', () => { if (url.includes('geo/1.0/direct')) { return Promise.resolve({ ok: true, - json: async () => [{ lat: 35.9606, lon: -83.9207 }] + json: async () => [{ lat: 35.9606, lon: -83.9207 }], }); } return Promise.reject('Unexpected fetch call for geocoding'); }); // Mock daily aggregation response - fetch.mockImplementationOnce(() => Promise.resolve({ - ok: true, - json: async () => ({ - date: '2020-03-04', - temperature: { - morning: 283.15, - afternoon: 293.15, - evening: 288.15 - }, - humidity: { - morning: 75, - afternoon: 60, - evening: 70 - } - }) - })); + fetch.mockImplementationOnce(() => + Promise.resolve({ + ok: true, + json: async () => ({ + date: '2020-03-04', + temperature: { + morning: 283.15, + afternoon: 293.15, + evening: 288.15, + }, + humidity: { + morning: 75, + afternoon: 60, + evening: 70, + }, + }), + }), + ); const result = await tool.call({ action: 'daily_aggregation', city: 'Knoxville, Tennessee', date: '2020-03-04', - units: 'Kelvin' + units: 'Kelvin', }); const parsed = JSON.parse(result); @@ -150,29 +158,32 @@ describe('OpenWeather Tool', () => { if (url.includes('geo/1.0/direct')) { return Promise.resolve({ ok: true, - json: async () => [{ lat: 35.9606, lon: -83.9207 }] + json: async () => [{ lat: 35.9606, lon: -83.9207 }], }); } return Promise.reject('Unexpected fetch call for geocoding'); }); // Mock overview response - fetch.mockImplementationOnce(() => Promise.resolve({ - ok: true, - json: async () => ({ - date: '2024-01-07', - lat: 35.9606, - lon: -83.9207, - tz: '+00:00', - units: 'metric', - weather_overview: 'Currently, the temperature is 2°C with a real feel of -2°C. The sky is clear with moderate wind.' - }) - })); + fetch.mockImplementationOnce(() => + Promise.resolve({ + ok: true, + json: async () => ({ + date: '2024-01-07', + lat: 35.9606, + lon: -83.9207, + tz: '+00:00', + units: 'metric', + weather_overview: + 'Currently, the temperature is 2°C with a real feel of -2°C. The sky is clear with moderate wind.', + }), + }), + ); const result = await tool.call({ action: 'overview', city: 'Knoxville, Tennessee', - units: 'Celsius' + units: 'Celsius', }); const parsed = JSON.parse(result); @@ -188,68 +199,62 @@ describe('OpenWeather Tool', () => { // Mock geocoding response for all three calls const geocodingMock = Promise.resolve({ ok: true, - json: async () => [{ lat: 35.9606, lon: -83.9207 }] + json: async () => [{ lat: 35.9606, lon: -83.9207 }], }); // Mock weather response for Kelvin const kelvinMock = Promise.resolve({ ok: true, json: async () => ({ - current: { temp: 293.15 } - }) + current: { temp: 293.15 }, + }), }); // Mock weather response for Celsius const celsiusMock = Promise.resolve({ ok: true, json: async () => ({ - current: { temp: 20 } - }) + current: { temp: 20 }, + }), }); // Mock weather response for Fahrenheit const fahrenheitMock = Promise.resolve({ ok: true, json: async () => ({ - current: { temp: 68 } - }) + current: { temp: 68 }, + }), }); // Test Kelvin - fetch - .mockImplementationOnce(() => geocodingMock) - .mockImplementationOnce(() => kelvinMock); + fetch.mockImplementationOnce(() => geocodingMock).mockImplementationOnce(() => kelvinMock); let result = await tool.call({ action: 'current_forecast', city: 'Knoxville, Tennessee', - units: 'Kelvin' + units: 'Kelvin', }); let parsed = JSON.parse(result); expect(parsed.current.temp).toBe(293); // Test Celsius - fetch - .mockImplementationOnce(() => geocodingMock) - .mockImplementationOnce(() => celsiusMock); + fetch.mockImplementationOnce(() => geocodingMock).mockImplementationOnce(() => celsiusMock); result = await tool.call({ action: 'current_forecast', city: 'Knoxville, Tennessee', - units: 'Celsius' + units: 'Celsius', }); parsed = JSON.parse(result); expect(parsed.current.temp).toBe(20); // Test Fahrenheit - fetch - .mockImplementationOnce(() => geocodingMock) - .mockImplementationOnce(() => fahrenheitMock); + fetch.mockImplementationOnce(() => geocodingMock).mockImplementationOnce(() => fahrenheitMock); result = await tool.call({ action: 'current_forecast', city: 'Knoxville, Tennessee', - units: 'Fahrenheit' + units: 'Fahrenheit', }); parsed = JSON.parse(result); expect(parsed.current.temp).toBe(68); @@ -259,56 +264,66 @@ describe('OpenWeather Tool', () => { const result = await tool.call({ action: 'timestamp', lat: 35.9606, - lon: -83.9207 + lon: -83.9207, }); - expect(result).toMatch(/Error: For timestamp action, a 'date' in YYYY-MM-DD format is required./); + expect(result).toMatch( + /Error: For timestamp action, a 'date' in YYYY-MM-DD format is required./, + ); }); test('daily_aggregation action without a date returns an error message', async () => { const result = await tool.call({ action: 'daily_aggregation', lat: 35.9606, - lon: -83.9207 + lon: -83.9207, }); expect(result).toMatch(/Error: date \(YYYY-MM-DD\) is required for daily_aggregation action./); }); test('unknown action returns an error due to schema validation', async () => { - await expect(tool.call({ - action: 'unknown_action' - })).rejects.toThrow(/Received tool input did not match expected schema/); + await expect( + tool.call({ + action: 'unknown_action', + }), + ).rejects.toThrow(/Received tool input did not match expected schema/); }); test('geocoding failure returns a descriptive error', async () => { - fetch.mockImplementationOnce(() => Promise.resolve({ - ok: true, - json: async () => [] - })); + fetch.mockImplementationOnce(() => + Promise.resolve({ + ok: true, + json: async () => [], + }), + ); const result = await tool.call({ action: 'current_forecast', - city: 'NowhereCity' + city: 'NowhereCity', }); expect(result).toMatch(/Error: Could not find coordinates for city: NowhereCity/); }); test('API request failure returns an error', async () => { // Mock geocoding success - fetch.mockImplementationOnce(() => Promise.resolve({ - ok: true, - json: async () => [{ lat: 35.9606, lon: -83.9207 }] - })); + fetch.mockImplementationOnce(() => + Promise.resolve({ + ok: true, + json: async () => [{ lat: 35.9606, lon: -83.9207 }], + }), + ); // Mock weather request failure - fetch.mockImplementationOnce(() => Promise.resolve({ - ok: false, - status: 404, - json: async () => ({ message: 'Not found' }) - })); + fetch.mockImplementationOnce(() => + Promise.resolve({ + ok: false, + status: 404, + json: async () => ({ message: 'Not found' }), + }), + ); const result = await tool.call({ action: 'current_forecast', - city: 'Knoxville, Tennessee' + city: 'Knoxville, Tennessee', }); expect(result).toMatch(/Error: OpenWeather API request failed with status 404: Not found/); }); @@ -319,7 +334,7 @@ describe('OpenWeather Tool', () => { if (url.includes('geo/1.0/direct')) { return Promise.resolve({ ok: true, - json: async () => [{ lat: 35.9606, lon: -83.9207 }] + json: async () => [{ lat: 35.9606, lon: -83.9207 }], }); } return Promise.reject('Unexpected fetch call for geocoding'); @@ -336,8 +351,8 @@ describe('OpenWeather Tool', () => { const result = await tool.call({ action: 'timestamp', city: 'Knoxville, Tennessee', - date: '03-04-2020' // Wrong format + date: '03-04-2020', // Wrong format }); expect(result).toMatch(/Error: Invalid date format. Expected YYYY-MM-DD./); }); -}); \ No newline at end of file +}); diff --git a/api/app/clients/tools/structured/OpenWeather.js b/api/app/clients/tools/structured/OpenWeather.js index b9f8cbbc9de..6d9c99293f5 100644 --- a/api/app/clients/tools/structured/OpenWeather.js +++ b/api/app/clients/tools/structured/OpenWeather.js @@ -5,7 +5,9 @@ const fetch = require('node-fetch'); // Utility to retrieve API key function getApiKey(envVar, override, providedKey) { - if (providedKey) return providedKey; + if (providedKey) { + return providedKey; + } const key = getEnvironmentVariable(envVar); if (!key && !override) { throw new Error(`Missing ${envVar} environment variable.`); @@ -18,7 +20,9 @@ function getApiKey(envVar, override, providedKey) { * Defaults to Celsius if not specified. */ function mapUnitsToOpenWeather(unit) { - if (!unit) return 'metric'; // Default to Celsius + if (!unit) { + return 'metric'; + } // Default to Celsius switch (unit) { case 'Celsius': return 'metric'; @@ -36,9 +40,18 @@ function mapUnitsToOpenWeather(unit) { */ function roundTemperatures(obj) { const tempKeys = new Set([ - 'temp', 'feels_like', 'dew_point', - 'day', 'min', 'max', 'night', 'eve', 'morn', - 'afternoon', 'morning', 'evening' + 'temp', + 'feels_like', + 'dew_point', + 'day', + 'min', + 'max', + 'night', + 'eve', + 'morn', + 'afternoon', + 'morning', + 'evening', ]); if (Array.isArray(obj)) { @@ -58,22 +71,23 @@ function roundTemperatures(obj) { class OpenWeather extends Tool { name = 'OpenWeather'; - 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.'; + 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"]), + 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(), + units: z.enum(['Celsius', 'Kelvin', 'Fahrenheit']).optional(), lang: z.string().optional(), date: z.string().optional(), // For timestamp and daily_aggregation - tz: z.string().optional() + tz: z.string().optional(), }); constructor(options = {}) { @@ -83,7 +97,9 @@ class OpenWeather extends Tool { } async geocodeCity(city) { - const geocodeUrl = `https://api.openweathermap.org/geo/1.0/direct?q=${encodeURIComponent(city)}&limit=1&appid=${this.apiKey}`; + 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) { @@ -95,18 +111,18 @@ class OpenWeather extends Tool { convertDateToUnix(dateStr) { const parts = dateStr.split('-'); if (parts.length !== 3) { - throw new Error("Invalid date format. Expected YYYY-MM-DD."); + 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."); + 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."); + throw new Error('Invalid date provided. Cannot parse into a valid date.'); } return Math.floor(dateObj.getTime() / 1000); @@ -118,95 +134,93 @@ class OpenWeather extends Tool { 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" - } + 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', + }, + }, }, - 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', + ], }, - 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); + null, + 2, + ); } let finalLat = lat; @@ -219,56 +233,68 @@ class OpenWeather extends Tool { finalLon = coords.lon; } - if (["current_forecast", "timestamp", "daily_aggregation", "overview"].includes(action)) { + 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')."; + 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 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 (action === 'timestamp') { if (!date) { - return "Error: For timestamp action, a 'date' in YYYY-MM-DD format is required."; + 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."; + 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); + 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); + 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); + 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); + 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}`; @@ -278,12 +304,13 @@ class OpenWeather extends Tool { 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)}`; + 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}`; } diff --git a/api/app/clients/tools/util/handleTools.js b/api/app/clients/tools/util/handleTools.js index 743dc37647d..acc098cee1c 100644 --- a/api/app/clients/tools/util/handleTools.js +++ b/api/app/clients/tools/util/handleTools.js @@ -215,7 +215,7 @@ const loadTools = async ({ const toolOptions = { serpapi: { location: 'Austin,Texas,United States', hl: 'en', gl: 'us' }, dalle: imageGenOptions, - 'stable-diffusion': imageGenOptions, + 'stable-diffusion': imageGenOptions, }; const toolAuthFields = {}; From ff8a6e3088b8c97f68cf8c3fa1f27b1535abd0ec Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Fri, 10 Jan 2025 08:22:49 -0500 Subject: [PATCH 3/5] chore: move test files --- .../specs}/openWeather.integration.test.js | 4 ++-- .../tools/{__tests__ => structured/specs}/openweather.test.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename api/app/clients/tools/{__tests__ => structured/specs}/openWeather.integration.test.js (98%) rename api/app/clients/tools/{__tests__ => structured/specs}/openweather.test.js (99%) diff --git a/api/app/clients/tools/__tests__/openWeather.integration.test.js b/api/app/clients/tools/structured/specs/openWeather.integration.test.js similarity index 98% rename from api/app/clients/tools/__tests__/openWeather.integration.test.js rename to api/app/clients/tools/structured/specs/openWeather.integration.test.js index c57c6956853..07dd417cf1a 100644 --- a/api/app/clients/tools/__tests__/openWeather.integration.test.js +++ b/api/app/clients/tools/structured/specs/openWeather.integration.test.js @@ -1,11 +1,11 @@ // __tests__/openWeather.integration.test.js -const OpenWeather = require('../structured/OpenWeather'); +const OpenWeather = require('../OpenWeather'); describe('OpenWeather Tool (Integration Test)', () => { let tool; beforeAll(() => { - tool = new OpenWeather(); + tool = new OpenWeather({ override: true }); console.log('API Key present:', !!process.env.OPENWEATHER_API_KEY); }); diff --git a/api/app/clients/tools/__tests__/openweather.test.js b/api/app/clients/tools/structured/specs/openweather.test.js similarity index 99% rename from api/app/clients/tools/__tests__/openweather.test.js rename to api/app/clients/tools/structured/specs/openweather.test.js index a6f6be6a4fe..3340c80cc49 100644 --- a/api/app/clients/tools/__tests__/openweather.test.js +++ b/api/app/clients/tools/structured/specs/openweather.test.js @@ -1,5 +1,5 @@ // __tests__/openweather.test.js -const OpenWeather = require('../structured/OpenWeather'); +const OpenWeather = require('../OpenWeather'); const fetch = require('node-fetch'); // Mock environment variable From 8731f431d8f5c643eadd6e3baf4dca4a8946df2f Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Fri, 10 Jan 2025 08:44:53 -0500 Subject: [PATCH 4/5] fix: tool icon, allow user-provided keys, conform to app key assignment pattern --- api/app/clients/tools/manifest.json | 14 +++++---- .../clients/tools/structured/OpenWeather.js | 29 +++++++++---------- api/app/clients/tools/util/handleTools.js | 2 +- 3 files changed, 23 insertions(+), 22 deletions(-) diff --git a/api/app/clients/tools/manifest.json b/api/app/clients/tools/manifest.json index 4e0b3ab226b..4b5aabd1ab0 100644 --- a/api/app/clients/tools/manifest.json +++ b/api/app/clients/tools/manifest.json @@ -100,7 +100,6 @@ "pluginKey": "calculator", "description": "Perform simple and complex mathematical calculations.", "icon": "https://i.imgur.com/RHsSG5h.png", - "isAuthRequired": "false", "authConfig": [] }, { @@ -141,10 +140,15 @@ }, { "name": "OpenWeather", - "pluginKey": "OpenWeather", + "pluginKey": "open_weather", "description": "Get weather forecasts and historical data from the OpenWeather API", - "icon": "/assets/flux.png", - "isAuthRequired": "false", - "authConfig": [] + "icon": "/assets/openweather.png", + "authConfig": [ + { + "authField": "OPENWEATHER_API_KEY", + "label": "OpenWeather API Key", + "description": "Sign up at OpenWeather, then get your key at API keys." + } + ] } ] diff --git a/api/app/clients/tools/structured/OpenWeather.js b/api/app/clients/tools/structured/OpenWeather.js index 6d9c99293f5..b84225101c0 100644 --- a/api/app/clients/tools/structured/OpenWeather.js +++ b/api/app/clients/tools/structured/OpenWeather.js @@ -3,18 +3,6 @@ const { z } = require('zod'); const { getEnvironmentVariable } = require('@langchain/core/utils/env'); const fetch = require('node-fetch'); -// Utility to retrieve API key -function getApiKey(envVar, override, providedKey) { - if (providedKey) { - return providedKey; - } - const key = getEnvironmentVariable(envVar); - if (!key && !override) { - throw new Error(`Missing ${envVar} environment variable.`); - } - return key; -} - /** * Map user-friendly units to OpenWeather units. * Defaults to Celsius if not specified. @@ -70,7 +58,7 @@ function roundTemperatures(obj) { } class OpenWeather extends Tool { - name = 'OpenWeather'; + name = 'open_weather'; description = 'Provides weather data from OpenWeather One Call API 3.0. ' + 'Actions: help, current_forecast, timestamp, daily_aggregation, overview. ' + @@ -90,10 +78,19 @@ class OpenWeather extends Tool { tz: z.string().optional(), }); - constructor(options = {}) { + constructor(fields = {}) { super(); - const { apiKey, override = false } = options; - this.apiKey = getApiKey('OPENWEATHER_API_KEY', override, apiKey); + 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) { diff --git a/api/app/clients/tools/util/handleTools.js b/api/app/clients/tools/util/handleTools.js index acc098cee1c..39e710d9400 100644 --- a/api/app/clients/tools/util/handleTools.js +++ b/api/app/clients/tools/util/handleTools.js @@ -179,7 +179,7 @@ const loadTools = async ({ 'azure-ai-search': StructuredACS, traversaal_search: TraversaalSearch, tavily_search_results_json: TavilySearchResults, - OpenWeather: OpenWeather, + open_weather: OpenWeather, }; const customConstructors = { From 65538f30675ec271512c9b3405d334952ece4dc8 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Fri, 10 Jan 2025 08:49:10 -0500 Subject: [PATCH 5/5] chore: linting not included in #5212 --- api/server/socialLogins.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/server/socialLogins.js b/api/server/socialLogins.js index c9f84767db8..6da71f908fe 100644 --- a/api/server/socialLogins.js +++ b/api/server/socialLogins.js @@ -1,7 +1,7 @@ const Redis = require('ioredis'); const passport = require('passport'); const session = require('express-session'); -const MemoryStore = require('memorystore')(session) +const MemoryStore = require('memorystore')(session); const RedisStore = require('connect-redis').default; const { setupOpenId, @@ -51,8 +51,8 @@ const configureSocialLogins = (app) => { sessionOptions.store = new RedisStore({ client, prefix: 'librechat' }); } else { sessionOptions.store = new MemoryStore({ - checkPeriod: 86400000 // prune expired entries every 24h - }) + checkPeriod: 86400000, // prune expired entries every 24h + }); } app.use(session(sessionOptions)); app.use(passport.session());