From 1614cfa5e54395610831c6e5f5df9ed9d3489838 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Thu, 20 Nov 2025 14:55:50 +0530 Subject: [PATCH] feat(app): add widgets --- src/lib/agents/search/widgets/index.ts | 6 + src/lib/agents/search/widgets/registry.ts | 65 +++++++++ .../agents/search/widgets/weatherWidget.ts | 123 ++++++++++++++++++ 3 files changed, 194 insertions(+) create mode 100644 src/lib/agents/search/widgets/index.ts create mode 100644 src/lib/agents/search/widgets/registry.ts create mode 100644 src/lib/agents/search/widgets/weatherWidget.ts diff --git a/src/lib/agents/search/widgets/index.ts b/src/lib/agents/search/widgets/index.ts new file mode 100644 index 0000000..7ddc597 --- /dev/null +++ b/src/lib/agents/search/widgets/index.ts @@ -0,0 +1,6 @@ +import WidgetRegistry from './registry'; +import weatherWidget from './weatherWidget'; + +WidgetRegistry.register(weatherWidget); + +export { WidgetRegistry }; diff --git a/src/lib/agents/search/widgets/registry.ts b/src/lib/agents/search/widgets/registry.ts new file mode 100644 index 0000000..d8ceaba --- /dev/null +++ b/src/lib/agents/search/widgets/registry.ts @@ -0,0 +1,65 @@ +import { + AdditionalConfig, + SearchAgentConfig, + Widget, + WidgetConfig, + WidgetOutput, +} from '../types'; + +class WidgetRegistry { + private static widgets = new Map(); + + static register(widget: Widget) { + this.widgets.set(widget.name, widget); + } + + static get(name: string): Widget | undefined { + return this.widgets.get(name); + } + + static getAll(): Widget[] { + return Array.from(this.widgets.values()); + } + + static getDescriptions(): string { + return Array.from(this.widgets.values()) + .map((widget) => `${widget.name}: ${widget.description}`) + .join('\n\n'); + } + + static async execute( + name: string, + params: any, + config: AdditionalConfig, + ): Promise { + const widget = this.get(name); + + if (!widget) { + throw new Error(`Widget with name ${name} not found`); + } + + return widget.execute(params, config); + } + + static async executeAll( + widgets: WidgetConfig[], + additionalConfig: AdditionalConfig, + ): Promise { + const results: WidgetOutput[] = []; + + await Promise.all( + widgets.map(async (widgetConfig) => { + const output = await this.execute( + widgetConfig.type, + widgetConfig.params, + additionalConfig, + ); + results.push(output); + }), + ); + + return results; + } +} + +export default WidgetRegistry; diff --git a/src/lib/agents/search/widgets/weatherWidget.ts b/src/lib/agents/search/widgets/weatherWidget.ts new file mode 100644 index 0000000..b9d048c --- /dev/null +++ b/src/lib/agents/search/widgets/weatherWidget.ts @@ -0,0 +1,123 @@ +import z from 'zod'; +import { Widget } from '../types'; + +const WeatherWidgetSchema = z.object({ + type: z.literal('weather'), + 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.', + ), +}); + +const weatherWidget = { + name: 'weather', + description: + 'Provides current weather information for a specified location. It can return details such as temperature, humidity, wind speed, and weather conditions. It needs either a location name or latitude/longitude coordinates to function.', + schema: WeatherWidgetSchema, + execute: async (params, _) => { + 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_weather=true`, + { + headers: { + 'User-Agent': 'Perplexica', + 'Content-Type': 'application/json', + }, + }, + ); + + const weatherData = await weatherRes.json(); + + /* this is like a very simple implementation just to see the bacckend works, when we're working on the frontend, we'll return more data i guess? */ + return { + type: 'weather', + data: { + location: params.location, + latitude: location.lat, + longitude: location.lon, + weather: weatherData.current_weather, + }, + }; + } 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_weather=true`, + { + 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', + data: { + location: locationData.display_name, + latitude: params.lat, + longitude: params.lon, + weather: weatherData.current_weather, + }, + }; + } + + return { + type: 'weather', + data: null, + }; + }, +} satisfies Widget; + +export default weatherWidget;