🌤️ feat: Add OpenWeather Tool for Weather Data Retrieval (#5246)

*  feat: Add OpenWeather Tool for Weather Data Retrieval 🌤️

* chore: linting

* chore: move test files

* fix: tool icon, allow user-provided keys, conform to app key assignment pattern

* chore: linting not included in #5212

---------

Co-authored-by: Jonathan Addington <jonathan.addington@jmaddington.com>
This commit is contained in:
Danny Avila 2025-01-10 08:54:08 -05:00 committed by GitHub
parent ea1a5c8a30
commit 0855677a36
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 927 additions and 6 deletions

View file

@ -256,6 +256,7 @@ AZURE_AI_SEARCH_SEARCH_OPTION_SELECT=
# DALLE3_AZURE_API_VERSION= # DALLE3_AZURE_API_VERSION=
# DALLE2_AZURE_API_VERSION= # DALLE2_AZURE_API_VERSION=
# Google # Google
#----------------- #-----------------
GOOGLE_SEARCH_API_KEY= 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-cache: Forces validation with server before using cached version
# no-store: Prevents storing the response entirely # no-store: Prevents storing the response entirely
# must-revalidate: Prevents using stale content when offline # must-revalidate: Prevents using stale content when offline
#=====================================================#
# OpenWeather #
#=====================================================#
OPENWEATHER_API_KEY=

View file

@ -8,6 +8,7 @@ const StructuredSD = require('./structured/StableDiffusion');
const GoogleSearchAPI = require('./structured/GoogleSearch'); const GoogleSearchAPI = require('./structured/GoogleSearch');
const TraversaalSearch = require('./structured/TraversaalSearch'); const TraversaalSearch = require('./structured/TraversaalSearch');
const TavilySearchResults = require('./structured/TavilySearchResults'); const TavilySearchResults = require('./structured/TavilySearchResults');
const OpenWeather = require('./structured/OpenWeather');
module.exports = { module.exports = {
availableTools, availableTools,
@ -19,4 +20,5 @@ module.exports = {
TraversaalSearch, TraversaalSearch,
StructuredWolfram, StructuredWolfram,
TavilySearchResults, TavilySearchResults,
OpenWeather,
}; };

View file

@ -100,7 +100,6 @@
"pluginKey": "calculator", "pluginKey": "calculator",
"description": "Perform simple and complex mathematical calculations.", "description": "Perform simple and complex mathematical calculations.",
"icon": "https://i.imgur.com/RHsSG5h.png", "icon": "https://i.imgur.com/RHsSG5h.png",
"isAuthRequired": "false",
"authConfig": [] "authConfig": []
}, },
{ {
@ -135,7 +134,20 @@
{ {
"authField": "AZURE_AI_SEARCH_API_KEY", "authField": "AZURE_AI_SEARCH_API_KEY",
"label": "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 <a href=\"https://home.openweathermap.org/users/sign_up\" target=\"_blank\">OpenWeather</a>, then get your key at <a href=\"https://home.openweathermap.org/api_keys\" target=\"_blank\">API keys</a>."
} }
] ]
} }

View file

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

View file

@ -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;
}
});
});

View file

@ -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./);
});
});

View file

@ -14,6 +14,7 @@ const {
TraversaalSearch, TraversaalSearch,
StructuredWolfram, StructuredWolfram,
TavilySearchResults, TavilySearchResults,
OpenWeather,
} = require('../'); } = require('../');
const { primeFiles: primeCodeFiles } = require('~/server/services/Files/Code/process'); const { primeFiles: primeCodeFiles } = require('~/server/services/Files/Code/process');
const { createFileSearchTool, primeFiles: primeSearchFiles } = require('./fileSearch'); const { createFileSearchTool, primeFiles: primeSearchFiles } = require('./fileSearch');
@ -178,6 +179,7 @@ const loadTools = async ({
'azure-ai-search': StructuredACS, 'azure-ai-search': StructuredACS,
traversaal_search: TraversaalSearch, traversaal_search: TraversaalSearch,
tavily_search_results_json: TavilySearchResults, tavily_search_results_json: TavilySearchResults,
open_weather: OpenWeather,
}; };
const customConstructors = { const customConstructors = {

View file

@ -1,7 +1,7 @@
const Redis = require('ioredis'); const Redis = require('ioredis');
const passport = require('passport'); const passport = require('passport');
const session = require('express-session'); const session = require('express-session');
const MemoryStore = require('memorystore')(session) const MemoryStore = require('memorystore')(session);
const RedisStore = require('connect-redis').default; const RedisStore = require('connect-redis').default;
const { const {
setupOpenId, setupOpenId,
@ -51,8 +51,8 @@ const configureSocialLogins = (app) => {
sessionOptions.store = new RedisStore({ client, prefix: 'librechat' }); sessionOptions.store = new RedisStore({ client, prefix: 'librechat' });
} else { } else {
sessionOptions.store = new MemoryStore({ sessionOptions.store = new MemoryStore({
checkPeriod: 86400000 // prune expired entries every 24h checkPeriod: 86400000, // prune expired entries every 24h
}) });
} }
app.use(session(sessionOptions)); app.use(session(sessionOptions));
app.use(passport.session()); app.use(passport.session());

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB