kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
feat: add @agentic/open-meteo weather api client
rodzic
34631ddf66
commit
91ff8d645c
|
@ -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'
|
||||
}
|
||||
})
|
||||
```
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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)
|
|
@ -0,0 +1,2 @@
|
|||
export * from './open-meteo'
|
||||
export * from './open-meteo-client'
|
|
@ -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) }
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"extends": "@agentic/tsconfig/base.json",
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
Ładowanie…
Reference in New Issue