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/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..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": []
},
{
@@ -135,7 +134,20 @@
{
"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": "open_weather",
+ "description": "Get weather forecasts and historical data from the OpenWeather API",
+ "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
new file mode 100644
index 00000000000..b84225101c0
--- /dev/null
+++ b/api/app/clients/tools/structured/OpenWeather.js
@@ -0,0 +1,317 @@
+const { Tool } = require('@langchain/core/tools');
+const { z } = require('zod');
+const { getEnvironmentVariable } = require('@langchain/core/utils/env');
+const fetch = require('node-fetch');
+
+/**
+ * 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 = '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(), // For timestamp and daily_aggregation
+ 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 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/structured/specs/openWeather.integration.test.js b/api/app/clients/tools/structured/specs/openWeather.integration.test.js
new file mode 100644
index 00000000000..07dd417cf1a
--- /dev/null
+++ b/api/app/clients/tools/structured/specs/openWeather.integration.test.js
@@ -0,0 +1,224 @@
+// __tests__/openWeather.integration.test.js
+const OpenWeather = require('../OpenWeather');
+
+describe('OpenWeather Tool (Integration Test)', () => {
+ let tool;
+
+ beforeAll(() => {
+ tool = new OpenWeather({ override: true });
+ 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;
+ }
+ });
+});
diff --git a/api/app/clients/tools/structured/specs/openweather.test.js b/api/app/clients/tools/structured/specs/openweather.test.js
new file mode 100644
index 00000000000..3340c80cc49
--- /dev/null
+++ b/api/app/clients/tools/structured/specs/openweather.test.js
@@ -0,0 +1,358 @@
+// __tests__/openweather.test.js
+const OpenWeather = require('../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./);
+ });
+});
diff --git a/api/app/clients/tools/util/handleTools.js b/api/app/clients/tools/util/handleTools.js
index a8ee50c3d4d..39e710d9400 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,
+ open_weather: OpenWeather,
};
const customConstructors = {
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());
diff --git a/client/public/assets/openweather.png b/client/public/assets/openweather.png
new file mode 100644
index 00000000000..0e4d63a2bc2
Binary files /dev/null and b/client/public/assets/openweather.png differ