Skip to content
This repository was archived by the owner on Jul 16, 2025. It is now read-only.

feat: extend weather tool by forecast and reduced structue #169

Merged
merged 1 commit into from
Dec 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/toolbox-weather.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
96 changes: 92 additions & 4 deletions src/Chain/ToolBox/Tool/OpenMeteo.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
) {
Expand All @@ -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;
}
}
76 changes: 76 additions & 0 deletions tests/Chain/ToolBox/OpenMeteoTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

declare(strict_types=1);

namespace PhpLlm\LlmChain\Tests\Chain\ToolBox;

use PhpLlm\LlmChain\Chain\ToolBox\Tool\OpenMeteo;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\JsonMockResponse;

#[CoversClass(OpenMeteo::class)]
final class OpenMeteoTest extends TestCase
{
#[Test]
public function current(): void
{
$response = $this->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');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JsonMockResponse::fromFile() is not available?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

got only added with v7.1 - so would break with lowest symfony version

$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));
}
}
23 changes: 23 additions & 0 deletions tests/Chain/ToolBox/fixtures/openmeteo-current.json
Original file line number Diff line number Diff line change
@@ -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
}
}
37 changes: 37 additions & 0 deletions tests/Chain/ToolBox/fixtures/openmeteo-forecast.json
Original file line number Diff line number Diff line change
@@ -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
]
}
}
Loading