From 6051ce89dffadecf13c148671b9b0790d7134c1f Mon Sep 17 00:00:00 2001 From: Christopher Hertel Date: Sat, 21 Dec 2024 02:35:34 +0100 Subject: [PATCH] feat: extend weather tool by forecast and reduced structue --- examples/toolbox-weather.php | 2 +- src/Chain/ToolBox/Tool/OpenMeteo.php | 96 ++++++++++++++++++- tests/Chain/ToolBox/OpenMeteoTest.php | 76 +++++++++++++++ .../ToolBox/fixtures/openmeteo-current.json | 23 +++++ .../ToolBox/fixtures/openmeteo-forecast.json | 37 +++++++ 5 files changed, 229 insertions(+), 5 deletions(-) create mode 100644 tests/Chain/ToolBox/OpenMeteoTest.php create mode 100644 tests/Chain/ToolBox/fixtures/openmeteo-current.json create mode 100644 tests/Chain/ToolBox/fixtures/openmeteo-forecast.json diff --git a/examples/toolbox-weather.php b/examples/toolbox-weather.php index fbcf6a16..cbe1b06f 100755 --- a/examples/toolbox-weather.php +++ b/examples/toolbox-weather.php @@ -28,7 +28,7 @@ $processor = new ChainProcessor($toolBox); $chain = new Chain($platform, $llm, [$processor], [$processor]); -$messages = new MessageBag(Message::ofUser('How is the weather currently in Berlin?')); +$messages = new MessageBag(Message::ofUser('How is the weather currently in Berlin? And how about tomorrow?')); $response = $chain->call($messages); echo $response->getContent().PHP_EOL; diff --git a/src/Chain/ToolBox/Tool/OpenMeteo.php b/src/Chain/ToolBox/Tool/OpenMeteo.php index b741769e..2369e944 100644 --- a/src/Chain/ToolBox/Tool/OpenMeteo.php +++ b/src/Chain/ToolBox/Tool/OpenMeteo.php @@ -5,11 +5,44 @@ namespace PhpLlm\LlmChain\Chain\ToolBox\Tool; use PhpLlm\LlmChain\Chain\ToolBox\Attribute\AsTool; +use PhpLlm\LlmChain\Chain\ToolBox\Attribute\ToolParameter; use Symfony\Contracts\HttpClient\HttpClientInterface; -#[AsTool(name: 'weather', description: 'get the current weather for a location')] +#[AsTool(name: 'weather_current', description: 'get current weather for a location', method: 'current')] +#[AsTool(name: 'weather_forecast', description: 'get weather forecast for a location', method: 'forecast')] final readonly class OpenMeteo { + private const WMO_CODES = [ + 0 => 'Clear', + 1 => 'Mostly Clear', + 2 => 'Partly Cloudy', + 3 => 'Overcast', + 45 => 'Fog', + 48 => 'Icy Fog', + 51 => 'Light Drizzle', + 53 => 'Drizzle', + 55 => 'Heavy Drizzle', + 56 => 'Light Freezing Drizzle', + 57 => 'Freezing Drizzle', + 61 => 'Light Rain', + 63 => 'Rain', + 65 => 'Heavy Rain', + 66 => 'Light Freezing Rain', + 67 => 'Freezing Rain', + 71 => 'Light Snow', + 73 => 'Snow', + 75 => 'Heavy Snow', + 77 => 'Snow Grains', + 80 => 'Light Showers', + 81 => 'Showers', + 82 => 'Heavy Showers', + 85 => 'Light Snow Showers', + 86 => 'Snow Showers', + 95 => 'Thunderstorm', + 96 => 'Light Thunderstorm with Hail', + 99 => 'Thunderstorm with Hail', + ]; + public function __construct( private HttpClientInterface $httpClient, ) { @@ -18,17 +51,72 @@ public function __construct( /** * @param float $latitude the latitude of the location * @param float $longitude the longitude of the location + * + * @return array{ + * weather: string, + * time: string, + * temperature: string, + * wind_speed: string, + * } */ - public function __invoke(float $latitude, float $longitude): string + public function current(float $latitude, float $longitude): array { $response = $this->httpClient->request('GET', 'https://api.open-meteo.com/v1/forecast', [ 'query' => [ 'latitude' => $latitude, 'longitude' => $longitude, - 'current' => 'temperature_2m,wind_speed_10m', + 'current' => 'weather_code,temperature_2m,wind_speed_10m', + ], + ]); + + $data = $response->toArray(); + + return [ + 'weather' => self::WMO_CODES[$data['current']['weather_code']] ?? 'Unknown', + 'time' => $data['current']['time'], + 'temperature' => $data['current']['temperature_2m'].$data['current_units']['temperature_2m'], + 'wind_speed' => $data['current']['wind_speed_10m'].$data['current_units']['wind_speed_10m'], + ]; + } + + /** + * @param float $latitude the latitude of the location + * @param float $longitude the longitude of the location + * @param int $days the number of days to forecast + * + * @return array{ + * weather: string, + * time: string, + * temperature_min: string, + * temperature_max: string, + * }[] + */ + public function forecast( + float $latitude, + float $longitude, + #[ToolParameter(minimum: 1, maximum: 16)] + int $days = 7, + ): array { + $response = $this->httpClient->request('GET', 'https://api.open-meteo.com/v1/forecast', [ + 'query' => [ + 'latitude' => $latitude, + 'longitude' => $longitude, + 'daily' => 'weather_code,temperature_2m_max,temperature_2m_min', + 'forecast_days' => $days, ], ]); - return $response->getContent(); + $data = $response->toArray(); + $forecast = []; + for ($i = 0; $i < $days; ++$i) { + $forecast[] = [ + 'weather' => self::WMO_CODES[$data['daily']['weather_code'][$i]] ?? 'Unknown', + 'time' => $data['daily']['time'][$i], + 'temperature_min' => $data['daily']['temperature_2m_min'][$i].$data['daily_units']['temperature_2m_min'], + 'temperature_max' => $data['daily']['temperature_2m_max'][$i].$data['daily_units']['temperature_2m_max'], + ]; + } + + return $forecast; } } diff --git a/tests/Chain/ToolBox/OpenMeteoTest.php b/tests/Chain/ToolBox/OpenMeteoTest.php new file mode 100644 index 00000000..5ab5bc38 --- /dev/null +++ b/tests/Chain/ToolBox/OpenMeteoTest.php @@ -0,0 +1,76 @@ +jsonMockResponseFromFile(__DIR__.'/fixtures/openmeteo-current.json'); + $httpClient = new MockHttpClient($response); + + $openMeteo = new OpenMeteo($httpClient); + + $actual = $openMeteo->current(52.52, 13.42); + $expected = [ + 'weather' => 'Overcast', + 'time' => '2024-12-21T01:15', + 'temperature' => '2.6°C', + 'wind_speed' => '10.7km/h', + ]; + + static::assertSame($expected, $actual); + } + + #[Test] + public function forecast(): void + { + $response = $this->jsonMockResponseFromFile(__DIR__.'/fixtures/openmeteo-forecast.json'); + $httpClient = new MockHttpClient($response); + + $openMeteo = new OpenMeteo($httpClient); + + $actual = $openMeteo->forecast(52.52, 13.42, 3); + $expected = [ + [ + 'weather' => 'Light Rain', + 'time' => '2024-12-21', + 'temperature_min' => '2°C', + 'temperature_max' => '6°C', + ], + [ + 'weather' => 'Light Showers', + 'time' => '2024-12-22', + 'temperature_min' => '1.3°C', + 'temperature_max' => '6.4°C', + ], + [ + 'weather' => 'Light Snow Showers', + 'time' => '2024-12-23', + 'temperature_min' => '1.5°C', + 'temperature_max' => '4.1°C', + ], + ]; + + static::assertSame($expected, $actual); + } + + /** + * This can be replaced by `JsonMockResponse::fromFile` when dropping Symfony 6.4. + */ + private function jsonMockResponseFromFile(string $file): JsonMockResponse + { + return new JsonMockResponse(json_decode(file_get_contents($file), true)); + } +} diff --git a/tests/Chain/ToolBox/fixtures/openmeteo-current.json b/tests/Chain/ToolBox/fixtures/openmeteo-current.json new file mode 100644 index 00000000..16d6cb26 --- /dev/null +++ b/tests/Chain/ToolBox/fixtures/openmeteo-current.json @@ -0,0 +1,23 @@ +{ + "latitude": 52.52, + "longitude": 13.419998, + "generationtime_ms": 0.06508827209472656, + "utc_offset_seconds": 0, + "timezone": "GMT", + "timezone_abbreviation": "GMT", + "elevation": 40.0, + "current_units": { + "time": "iso8601", + "interval": "seconds", + "weather_code": "wmo code", + "temperature_2m": "°C", + "wind_speed_10m": "km/h" + }, + "current": { + "time": "2024-12-21T01:15", + "interval": 900, + "weather_code": 3, + "temperature_2m": 2.6, + "wind_speed_10m": 10.7 + } +} diff --git a/tests/Chain/ToolBox/fixtures/openmeteo-forecast.json b/tests/Chain/ToolBox/fixtures/openmeteo-forecast.json new file mode 100644 index 00000000..beb4e141 --- /dev/null +++ b/tests/Chain/ToolBox/fixtures/openmeteo-forecast.json @@ -0,0 +1,37 @@ +{ + "latitude": 52.52, + "longitude": 13.419998, + "generationtime_ms": 0.0629425048828125, + "utc_offset_seconds": 0, + "timezone": "GMT", + "timezone_abbreviation": "GMT", + "elevation": 38.0, + "daily_units": { + "time": "iso8601", + "weather_code": "wmo code", + "temperature_2m_max": "°C", + "temperature_2m_min": "°C" + }, + "daily": { + "time": [ + "2024-12-21", + "2024-12-22", + "2024-12-23" + ], + "weather_code": [ + 61, + 80, + 85 + ], + "temperature_2m_max": [ + 6.0, + 6.4, + 4.1 + ], + "temperature_2m_min": [ + 2.0, + 1.3, + 1.5 + ] + } +}