diff --git a/legacy/docs/tools/open-meteo.mdx b/legacy/docs/tools/open-meteo.mdx new file mode 100644 index 00000000..54e3af6f --- /dev/null +++ b/legacy/docs/tools/open-meteo.mdx @@ -0,0 +1,42 @@ +--- +title: Open Meteo +description: Open-Meteo weather API client. +--- + +The [Open-Meteo weather API](https://open-meteo.com) provides a free weather forecast API for open-source developers and non-commercial use. + +- package: `@agentic/open-meteo` +- exports: `class OpenMeteoClient`, `namespace openmeteo` +- env vars: `OPEN_METEO_API_KEY` +- [source](https://github.com/transitive-bullshit/agentic/blob/main/packages/open-meteo/src/open-meteo-client.ts) +- [open-meteo api docs](https://open-meteo.com/en/docs) + +## Install + + +```bash npm +npm install @agentic/open-meteo +``` + +```bash yarn +yarn add @agentic/open-meteo +``` + +```bash pnpm +pnpm add @agentic/open-meteo +``` + + + +## Usage + +```ts +import { OpenMeteoClient } from '@agentic/open-meteo' + +const openMeteo = new OpenMeteoClient() +const res = await openMeteo.getForecast({ + location: { + name: 'San Francisco' + } +}) +``` diff --git a/legacy/packages/open-meteo/package.json b/legacy/packages/open-meteo/package.json new file mode 100644 index 00000000..b4dd7db8 --- /dev/null +++ b/legacy/packages/open-meteo/package.json @@ -0,0 +1,47 @@ +{ + "name": "@agentic/open-meteo", + "version": "7.6.1", + "description": "Agentic SDK for the Open-Meteo weather API.", + "author": "Travis Fischer ", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/transitive-bullshit/agentic.git" + }, + "type": "module", + "source": "./src/index.ts", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "sideEffects": false, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup", + "dev": "tsup --watch", + "clean": "del dist", + "test": "run-s test:*", + "test:lint": "eslint .", + "test:typecheck": "tsc --noEmit" + }, + "dependencies": { + "@agentic/core": "workspace:*", + "ky": "catalog:" + }, + "peerDependencies": { + "zod": "catalog:" + }, + "devDependencies": { + "@agentic/tsconfig": "workspace:*" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/legacy/packages/open-meteo/readme.md b/legacy/packages/open-meteo/readme.md new file mode 100644 index 00000000..38781f32 --- /dev/null +++ b/legacy/packages/open-meteo/readme.md @@ -0,0 +1,24 @@ +

+ + Agentic + +

+ +

+ AI agent stdlib that works with any LLM and TypeScript AI SDK. +

+ +

+ Build Status + NPM + MIT License + Prettier Code Formatting +

+ +# Agentic + +**See the [github repo](https://github.com/transitive-bullshit/agentic) or [docs](https://agentic.so) for more info.** + +## License + +MIT © [Travis Fischer](https://x.com/transitive_bs) diff --git a/legacy/packages/open-meteo/src/index.ts b/legacy/packages/open-meteo/src/index.ts new file mode 100644 index 00000000..e334386f --- /dev/null +++ b/legacy/packages/open-meteo/src/index.ts @@ -0,0 +1,2 @@ +export * from './open-meteo' +export * from './open-meteo-client' diff --git a/legacy/packages/open-meteo/src/open-meteo-client.ts b/legacy/packages/open-meteo/src/open-meteo-client.ts new file mode 100644 index 00000000..9084d247 --- /dev/null +++ b/legacy/packages/open-meteo/src/open-meteo-client.ts @@ -0,0 +1,144 @@ +import { + aiFunction, + AIFunctionsProvider, + getEnv, + pick, + sanitizeSearchParams +} from '@agentic/core' +import defaultKy, { type KyInstance } from 'ky' + +import { openmeteo } from './open-meteo' + +/** + * Agentic OpenMeteo weather client. + * + * Open-Meteo offers free weather forecast APIs for open-source developers + * and non-commercial use. + * + * @note The API key is optional. + * + * @see https://open-meteo.com/en/docs + */ +export class OpenMeteoClient extends AIFunctionsProvider { + protected readonly ky: KyInstance + protected readonly apiKey: string | undefined + protected readonly apiBaseUrl: string + + constructor({ + apiKey = getEnv('OPEN_METEO_API_KEY'), + apiBaseUrl = openmeteo.apiBaseUrl, + ky = defaultKy + }: { + apiKey?: string + apiBaseUrl?: string + ky?: KyInstance + } = {}) { + super() + + this.apiKey = apiKey + this.apiBaseUrl = apiBaseUrl + + this.ky = ky.extend({ + prefixUrl: apiBaseUrl, + ...(apiKey + ? { + headers: { + Authorization: `Bearer ${apiKey}` + } + } + : {}) + }) + } + + /** + * Gets the 7-day weather variables in hourly and daily resolution for given WGS84 latitude and longitude coordinates. Available worldwide. + */ + @aiFunction({ + name: 'open_meteo_get_forecast', + description: `Gets the 7-day weather forecast in hourly and daily resolution for given location. Available worldwide.`, + inputSchema: openmeteo.GetV1ForecastParamsSchema + }) + async getForecast( + params: openmeteo.GetV1ForecastParams + ): Promise { + const extractLocation = async (): Promise => { + if ('name' in params.location) { + const response = await this._geocode(params.location) + return pick(response, 'latitude', 'longitude') + } + + return params.location + } + + const { start, end } = validateAndSetDates(params.startDate, params.endDate) + + return this.ky + .get('forecast', { + searchParams: sanitizeSearchParams({ + ...(await extractLocation()), + temperature_unit: params.temperatureUnit, + start_date: toDateString(start), + end_date: toDateString(end), + current: [ + 'temperature_2m', + 'rain', + 'relative_humidity_2m', + 'wind_speed_10m' + ], + daily: ['temperature_2m_max', 'temperature_2m_min', 'rain_sum'], + hourly: ['temperature_2m', 'relative_humidity_2m', 'rain'], + timezone: 'UTC' + }) + }) + .json() + } + + protected async _geocode( + location: openmeteo.LocationSearch + ): Promise { + const { results } = await this.ky + .get('search', { + searchParams: sanitizeSearchParams({ + name: location.name, + language: location.language, + country: location.country, + format: 'json', + count: 1 + }) + }) + .json() + + if (!results?.length) { + throw new Error(`No results found for location "${location.name}"`) + } + + return results[0] + } +} + +function toDateString(date: Date): string { + return date.toISOString().split('T')[0]! +} + +function validateAndSetDates( + startDateStr: string, + endDateStr?: string +): { start: Date; end: Date } { + const now = new Date() + const start = startDateStr + ? new Date(startDateStr) + : new Date(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate())) + + if (endDateStr) { + const end = new Date(endDateStr) + if (end < start) { + throw new Error( + `The 'end_date' (${endDateStr}) has to occur on or after the 'start_date' (${startDateStr}).` + ) + } + return { start, end } + } else { + // If endDate is undefined, set it to the start date + return { start, end: new Date(start) } + } +} diff --git a/legacy/packages/open-meteo/src/open-meteo.ts b/legacy/packages/open-meteo/src/open-meteo.ts new file mode 100644 index 00000000..30119810 --- /dev/null +++ b/legacy/packages/open-meteo/src/open-meteo.ts @@ -0,0 +1,201 @@ +import { z } from 'zod' + +export namespace openmeteo { + export const apiBaseUrl = 'https://api.open-meteo.com/v1' + + // ----------------------------------------------------------------------------- + // Component schemas + // ----------------------------------------------------------------------------- + + /** For each selected weather variable, data will be returned as a floating point array. Additionally a `time` array will be returned with ISO8601 timestamps. */ + export const HourlyResponseSchema = z + .object({ + time: z.array(z.string()), + temperature_2m: z.array(z.number()).optional(), + relative_humidity_2m: z.array(z.number()).optional(), + dew_point_2m: z.array(z.number()).optional(), + apparent_temperature: z.array(z.number()).optional(), + pressure_msl: z.array(z.number()).optional(), + cloud_cover: z.array(z.number()).optional(), + cloud_cover_low: z.array(z.number()).optional(), + cloud_cover_mid: z.array(z.number()).optional(), + cloud_cover_high: z.array(z.number()).optional(), + wind_speed_10m: z.array(z.number()).optional(), + wind_speed_80m: z.array(z.number()).optional(), + wind_speed_120m: z.array(z.number()).optional(), + wind_speed_180m: z.array(z.number()).optional(), + wind_direction_10m: z.array(z.number()).optional(), + wind_direction_80m: z.array(z.number()).optional(), + wind_direction_120m: z.array(z.number()).optional(), + wind_direction_180m: z.array(z.number()).optional(), + wind_gusts_10m: z.array(z.number()).optional(), + shortwave_radiation: z.array(z.number()).optional(), + direct_radiation: z.array(z.number()).optional(), + direct_normal_irradiance: z.array(z.number()).optional(), + diffuse_radiation: z.array(z.number()).optional(), + vapour_pressure_deficit: z.array(z.number()).optional(), + evapotranspiration: z.array(z.number()).optional(), + precipitation: z.array(z.number()).optional(), + weather_code: z.array(z.number()).optional(), + snow_height: z.array(z.number()).optional(), + freezing_level_height: z.array(z.number()).optional(), + soil_temperature_0cm: z.array(z.number()).optional(), + soil_temperature_6cm: z.array(z.number()).optional(), + soil_temperature_18cm: z.array(z.number()).optional(), + soil_temperature_54cm: z.array(z.number()).optional(), + soil_moisture_0_1cm: z.array(z.number()).optional(), + soil_moisture_1_3cm: z.array(z.number()).optional(), + soil_moisture_3_9cm: z.array(z.number()).optional(), + soil_moisture_9_27cm: z.array(z.number()).optional(), + soil_moisture_27_81cm: z.array(z.number()).optional() + }) + .describe( + 'For each selected weather variable, data will be returned as a floating point array. Additionally a `time` array will be returned with ISO8601 timestamps.' + ) + export type HourlyResponse = z.infer + + /** For each selected daily weather variable, data will be returned as a floating point array. Additionally a `time` array will be returned with ISO8601 timestamps. */ + export const DailyResponseSchema = z + .object({ + time: z.array(z.string()), + temperature_2m_max: z.array(z.number()).optional(), + temperature_2m_min: z.array(z.number()).optional(), + apparent_temperature_max: z.array(z.number()).optional(), + apparent_temperature_min: z.array(z.number()).optional(), + precipitation_sum: z.array(z.number()).optional(), + precipitation_hours: z.array(z.number()).optional(), + weather_code: z.array(z.number()).optional(), + sunrise: z.array(z.number()).optional(), + sunset: z.array(z.number()).optional(), + wind_speed_10m_max: z.array(z.number()).optional(), + wind_gusts_10m_max: z.array(z.number()).optional(), + wind_direction_10m_dominant: z.array(z.number()).optional(), + shortwave_radiation_sum: z.array(z.number()).optional(), + uv_index_max: z.array(z.number()).optional(), + uv_index_clear_sky_max: z.array(z.number()).optional(), + et0_fao_evapotranspiration: z.array(z.number()).optional() + }) + .describe( + 'For each selected daily weather variable, data will be returned as a floating point array. Additionally a `time` array will be returned with ISO8601 timestamps.' + ) + export type DailyResponse = z.infer + + /** Current weather conditions with the attributes: time, temperature, wind_speed, wind_direction and weather_code */ + export const CurrentWeatherSchema = z + .object({ + time: z.string(), + temperature: z.number(), + wind_speed: z.number(), + wind_direction: z.number(), + weather_code: z.number().int() + }) + .describe( + 'Current weather conditions with the attributes: time, temperature, wind_speed, wind_direction and weather_code' + ) + export type CurrentWeather = z.infer + + // ----------------------------------------------------------------------------- + // Operation schemas + // ----------------------------------------------------------------------------- + + export const GetV1ForecastParamsSchema = z.object({ + location: z.union([ + z + .object({ + name: z.string().min(1), + country: z.string().optional(), + language: z.string().default('English') + }) + .strip(), + z + .object({ + latitude: z.coerce.number(), + longitude: z.coerce.number() + }) + .strip() + ]), + startDate: z + .string() + .date() + .describe( + 'Start date for the weather forecast in the format YYYY-MM-DD (UTC)' + ), + endDate: z + .string() + .date() + .describe( + 'End date for the weather forecast in the format YYYY-MM-DD (UTC)' + ) + .optional(), + temperatureUnit: z.enum(['celsius', 'fahrenheit']).default('fahrenheit') + }) + export type GetV1ForecastParams = z.infer + + export const GetV1ForecastResponseSchema = z.object({ + /** WGS84 of the center of the weather grid-cell which was used to generate this forecast. This coordinate might be up to 5 km away. */ + latitude: z + .number() + .describe( + 'WGS84 of the center of the weather grid-cell which was used to generate this forecast. This coordinate might be up to 5 km away.' + ) + .optional(), + /** WGS84 of the center of the weather grid-cell which was used to generate this forecast. This coordinate might be up to 5 km away. */ + longitude: z + .number() + .describe( + 'WGS84 of the center of the weather grid-cell which was used to generate this forecast. This coordinate might be up to 5 km away.' + ) + .optional(), + /** The elevation in meters of the selected weather grid-cell. In mountain terrain it might differ from the location you would expect. */ + elevation: z + .number() + .describe( + 'The elevation in meters of the selected weather grid-cell. In mountain terrain it might differ from the location you would expect.' + ) + .optional(), + /** Generation time of the weather forecast in milli seconds. This is mainly used for performance monitoring and improvements. */ + generationtime_ms: z + .number() + .describe( + 'Generation time of the weather forecast in milli seconds. This is mainly used for performance monitoring and improvements.' + ) + .optional(), + /** Applied timezone offset from the &timezone= parameter. */ + utc_offset_seconds: z + .number() + .int() + .describe('Applied timezone offset from the &timezone= parameter.') + .optional(), + hourly: HourlyResponseSchema.optional(), + /** For each selected weather variable, the unit will be listed here. */ + hourly_units: z + .record(z.string()) + .describe( + 'For each selected weather variable, the unit will be listed here.' + ) + .optional(), + daily: DailyResponseSchema.optional(), + /** For each selected daily weather variable, the unit will be listed here. */ + daily_units: z + .record(z.string()) + .describe( + 'For each selected daily weather variable, the unit will be listed here.' + ) + .optional(), + current_weather: CurrentWeatherSchema.optional() + }) + export type GetV1ForecastResponse = z.infer< + typeof GetV1ForecastResponseSchema + > + + export interface Location { + latitude: number + longitude: number + } + + export interface LocationSearch { + name: string + country?: string + language?: string + } +} diff --git a/legacy/packages/open-meteo/tsconfig.json b/legacy/packages/open-meteo/tsconfig.json new file mode 100644 index 00000000..6c8d720c --- /dev/null +++ b/legacy/packages/open-meteo/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "@agentic/tsconfig/base.json", + "include": ["src"], + "exclude": ["node_modules", "dist"] +}