import z from 'zod'; import { Widget } from '../types'; import formatChatHistoryAsString from '@/lib/utils/formatHistory'; const schema = z.object({ location: z .string() .describe( 'Human-readable location name (e.g., "New York, NY, USA", "London, UK"). Use this OR lat/lon coordinates, never both. Leave empty string if providing coordinates.', ), lat: z .number() .describe( 'Latitude coordinate in decimal degrees (e.g., 40.7128). Only use when location name is empty.', ), lon: z .number() .describe( 'Longitude coordinate in decimal degrees (e.g., -74.0060). Only use when location name is empty.', ), notPresent: z .boolean() .describe('Whether there is no need for the weather widget.'), }); const systemPrompt = ` You are a location extractor for weather queries. You will receive a user follow up and a conversation history. Your task is to determine if the user is asking about weather and extract the location they want weather for. - If the user is asking about weather, extract the location name OR coordinates (never both). - If using location name, set lat and lon to 0. - If using coordinates, set location to empty string. - If you cannot determine a valid location or the query is not weather-related, set notPresent to true. - Location should be specific (city, state/region, country) for best results. - You have to give the location so that it can be used to fetch weather data, it cannot be left empty unless notPresent is true. - Make sure to infer short forms of location names (e.g., "NYC" -> "New York City", "LA" -> "Los Angeles"). You must respond in the following JSON format without any extra text, explanations or filler sentences: { "location": string, "lat": number, "lon": number, "notPresent": boolean } `; const weatherWidget: Widget = { type: 'weatherWidget', shouldExecute: (classification) => classification.classification.showWeatherWidget, execute: async (input) => { const output = await input.llm.generateObject({ messages: [ { role: 'system', content: systemPrompt, }, { role: 'user', content: `\n${formatChatHistoryAsString(input.chatHistory)}\n\n\n${input.followUp}\n`, }, ], schema, }); if (output.notPresent) { return; } const params = output; try { if ( params.location === '' && (params.lat === undefined || params.lon === undefined) ) { throw new Error( 'Either location name or both latitude and longitude must be provided.', ); } if (params.location !== '') { const openStreetMapUrl = `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(params.location)}&format=json&limit=1`; const locationRes = await fetch(openStreetMapUrl, { headers: { 'User-Agent': 'Perplexica', 'Content-Type': 'application/json', }, }); const data = await locationRes.json(); const location = data[0]; if (!location) { throw new Error( `Could not find coordinates for location: ${params.location}`, ); } const weatherRes = await fetch( `https://api.open-meteo.com/v1/forecast?latitude=${location.lat}&longitude=${location.lon}¤t=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,precipitation,rain,showers,snowfall,weather_code,cloud_cover,pressure_msl,surface_pressure,wind_speed_10m,wind_direction_10m,wind_gusts_10m&hourly=temperature_2m,precipitation_probability,precipitation,weather_code&daily=weather_code,temperature_2m_max,temperature_2m_min,precipitation_sum,precipitation_probability_max&timezone=auto&forecast_days=7`, { headers: { 'User-Agent': 'Perplexica', 'Content-Type': 'application/json', }, }, ); const weatherData = await weatherRes.json(); return { type: 'weather', llmContext: `Weather in ${params.location} is ${JSON.stringify(weatherData.current)}`, data: { location: params.location, latitude: location.lat, longitude: location.lon, current: weatherData.current, hourly: { time: weatherData.hourly.time.slice(0, 24), temperature_2m: weatherData.hourly.temperature_2m.slice(0, 24), precipitation_probability: weatherData.hourly.precipitation_probability.slice(0, 24), precipitation: weatherData.hourly.precipitation.slice(0, 24), weather_code: weatherData.hourly.weather_code.slice(0, 24), }, daily: weatherData.daily, timezone: weatherData.timezone, }, }; } else if (params.lat !== undefined && params.lon !== undefined) { const [weatherRes, locationRes] = await Promise.all([ fetch( `https://api.open-meteo.com/v1/forecast?latitude=${params.lat}&longitude=${params.lon}¤t=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,precipitation,rain,showers,snowfall,weather_code,cloud_cover,pressure_msl,surface_pressure,wind_speed_10m,wind_direction_10m,wind_gusts_10m&hourly=temperature_2m,precipitation_probability,precipitation,weather_code&daily=weather_code,temperature_2m_max,temperature_2m_min,precipitation_sum,precipitation_probability_max&timezone=auto&forecast_days=7`, { headers: { 'User-Agent': 'Perplexica', 'Content-Type': 'application/json', }, }, ), fetch( `https://nominatim.openstreetmap.org/reverse?lat=${params.lat}&lon=${params.lon}&format=json`, { headers: { 'User-Agent': 'Perplexica', 'Content-Type': 'application/json', }, }, ), ]); const weatherData = await weatherRes.json(); const locationData = await locationRes.json(); return { type: 'weather', llmContext: `Weather in ${locationData.display_name} is ${JSON.stringify(weatherData.current)}`, data: { location: locationData.display_name, latitude: params.lat, longitude: params.lon, current: weatherData.current, hourly: { time: weatherData.hourly.time.slice(0, 24), temperature_2m: weatherData.hourly.temperature_2m.slice(0, 24), precipitation_probability: weatherData.hourly.precipitation_probability.slice(0, 24), precipitation: weatherData.hourly.precipitation.slice(0, 24), weather_code: weatherData.hourly.weather_code.slice(0, 24), }, daily: weatherData.daily, timezone: weatherData.timezone, }, }; } return { type: 'weather', llmContext: 'No valid location or coordinates provided.', data: null, }; } catch (err) { return { type: 'weather', llmContext: 'Failed to fetch weather data.', data: { error: `Error fetching weather data: ${err}`, }, }; } }, }; export default weatherWidget;