Spaces:
Running
Running
| 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 = ` | |
| <role> | |
| 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. | |
| </role> | |
| <instructions> | |
| - 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"). | |
| </instructions> | |
| <output_format> | |
| You must respond in the following JSON format without any extra text, explanations or filler sentences: | |
| { | |
| "location": string, | |
| "lat": number, | |
| "lon": number, | |
| "notPresent": boolean | |
| } | |
| </output_format> | |
| `; | |
| const weatherWidget: Widget = { | |
| type: 'weatherWidget', | |
| shouldExecute: (classification) => | |
| classification.classification.showWeatherWidget, | |
| execute: async (input) => { | |
| const output = await input.llm.generateObject<typeof schema>({ | |
| messages: [ | |
| { | |
| role: 'system', | |
| content: systemPrompt, | |
| }, | |
| { | |
| role: 'user', | |
| content: `<conversation_history>\n${formatChatHistoryAsString(input.chatHistory)}\n</conversation_history>\n<user_follow_up>\n${input.followUp}\n</user_follow_up>`, | |
| }, | |
| ], | |
| 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; | |