feat: add @agentic/open-meteo weather api client

old-agentic
Travis Fischer 2025-03-25 22:57:22 +08:00
rodzic 34631ddf66
commit 91ff8d645c
7 zmienionych plików z 465 dodań i 0 usunięć

Wyświetl plik

@ -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
<CodeGroup>
```bash npm
npm install @agentic/open-meteo
```
```bash yarn
yarn add @agentic/open-meteo
```
```bash pnpm
pnpm add @agentic/open-meteo
```
</CodeGroup>
## Usage
```ts
import { OpenMeteoClient } from '@agentic/open-meteo'
const openMeteo = new OpenMeteoClient()
const res = await openMeteo.getForecast({
location: {
name: 'San Francisco'
}
})
```

Wyświetl plik

@ -0,0 +1,47 @@
{
"name": "@agentic/open-meteo",
"version": "7.6.1",
"description": "Agentic SDK for the Open-Meteo weather API.",
"author": "Travis Fischer <travis@transitivebullsh.it>",
"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"
}
}

Wyświetl plik

@ -0,0 +1,24 @@
<p align="center">
<a href="https://agentic.so">
<img alt="Agentic" src="https://raw.githubusercontent.com/transitive-bullshit/agentic/main/docs/media/agentic-header.jpg" width="308">
</a>
</p>
<p align="center">
<em>AI agent stdlib that works with any LLM and TypeScript AI SDK.</em>
</p>
<p align="center">
<a href="https://github.com/transitive-bullshit/agentic/actions/workflows/main.yml"><img alt="Build Status" src="https://github.com/transitive-bullshit/agentic/actions/workflows/main.yml/badge.svg" /></a>
<a href="https://www.npmjs.com/package/@agentic/stdlib"><img alt="NPM" src="https://img.shields.io/npm/v/@agentic/stdlib.svg" /></a>
<a href="https://github.com/transitive-bullshit/agentic/blob/main/license"><img alt="MIT License" src="https://img.shields.io/badge/license-MIT-blue" /></a>
<a href="https://prettier.io"><img alt="Prettier Code Formatting" src="https://img.shields.io/badge/code_style-prettier-brightgreen.svg" /></a>
</p>
# 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)

Wyświetl plik

@ -0,0 +1,2 @@
export * from './open-meteo'
export * from './open-meteo-client'

Wyświetl plik

@ -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<openmeteo.GetV1ForecastResponse> {
const extractLocation = async (): Promise<openmeteo.Location> => {
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<openmeteo.GetV1ForecastResponse>()
}
protected async _geocode(
location: openmeteo.LocationSearch
): Promise<openmeteo.Location> {
const { results } = await this.ky
.get('search', {
searchParams: sanitizeSearchParams({
name: location.name,
language: location.language,
country: location.country,
format: 'json',
count: 1
})
})
.json<any>()
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) }
}
}

Wyświetl plik

@ -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<typeof HourlyResponseSchema>
/** 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<typeof DailyResponseSchema>
/** 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<typeof CurrentWeatherSchema>
// -----------------------------------------------------------------------------
// 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<typeof GetV1ForecastParamsSchema>
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
}
}

Wyświetl plik

@ -0,0 +1,5 @@
{
"extends": "@agentic/tsconfig/base.json",
"include": ["src"],
"exclude": ["node_modules", "dist"]
}