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

Commit f1f3e44

Browse files
authored
feat: extend weather tool by forecast and reduced structue (#169)
1 parent a817561 commit f1f3e44

File tree

5 files changed

+229
-5
lines changed

5 files changed

+229
-5
lines changed

examples/toolbox-weather.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
$processor = new ChainProcessor($toolBox);
2929
$chain = new Chain($platform, $llm, [$processor], [$processor]);
3030

31-
$messages = new MessageBag(Message::ofUser('How is the weather currently in Berlin?'));
31+
$messages = new MessageBag(Message::ofUser('How is the weather currently in Berlin? And how about tomorrow?'));
3232
$response = $chain->call($messages);
3333

3434
echo $response->getContent().PHP_EOL;

src/Chain/ToolBox/Tool/OpenMeteo.php

Lines changed: 92 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,44 @@
55
namespace PhpLlm\LlmChain\Chain\ToolBox\Tool;
66

77
use PhpLlm\LlmChain\Chain\ToolBox\Attribute\AsTool;
8+
use PhpLlm\LlmChain\Chain\ToolBox\Attribute\ToolParameter;
89
use Symfony\Contracts\HttpClient\HttpClientInterface;
910

10-
#[AsTool(name: 'weather', description: 'get the current weather for a location')]
11+
#[AsTool(name: 'weather_current', description: 'get current weather for a location', method: 'current')]
12+
#[AsTool(name: 'weather_forecast', description: 'get weather forecast for a location', method: 'forecast')]
1113
final readonly class OpenMeteo
1214
{
15+
private const WMO_CODES = [
16+
0 => 'Clear',
17+
1 => 'Mostly Clear',
18+
2 => 'Partly Cloudy',
19+
3 => 'Overcast',
20+
45 => 'Fog',
21+
48 => 'Icy Fog',
22+
51 => 'Light Drizzle',
23+
53 => 'Drizzle',
24+
55 => 'Heavy Drizzle',
25+
56 => 'Light Freezing Drizzle',
26+
57 => 'Freezing Drizzle',
27+
61 => 'Light Rain',
28+
63 => 'Rain',
29+
65 => 'Heavy Rain',
30+
66 => 'Light Freezing Rain',
31+
67 => 'Freezing Rain',
32+
71 => 'Light Snow',
33+
73 => 'Snow',
34+
75 => 'Heavy Snow',
35+
77 => 'Snow Grains',
36+
80 => 'Light Showers',
37+
81 => 'Showers',
38+
82 => 'Heavy Showers',
39+
85 => 'Light Snow Showers',
40+
86 => 'Snow Showers',
41+
95 => 'Thunderstorm',
42+
96 => 'Light Thunderstorm with Hail',
43+
99 => 'Thunderstorm with Hail',
44+
];
45+
1346
public function __construct(
1447
private HttpClientInterface $httpClient,
1548
) {
@@ -18,17 +51,72 @@ public function __construct(
1851
/**
1952
* @param float $latitude the latitude of the location
2053
* @param float $longitude the longitude of the location
54+
*
55+
* @return array{
56+
* weather: string,
57+
* time: string,
58+
* temperature: string,
59+
* wind_speed: string,
60+
* }
2161
*/
22-
public function __invoke(float $latitude, float $longitude): string
62+
public function current(float $latitude, float $longitude): array
2363
{
2464
$response = $this->httpClient->request('GET', 'https://api.open-meteo.com/v1/forecast', [
2565
'query' => [
2666
'latitude' => $latitude,
2767
'longitude' => $longitude,
28-
'current' => 'temperature_2m,wind_speed_10m',
68+
'current' => 'weather_code,temperature_2m,wind_speed_10m',
69+
],
70+
]);
71+
72+
$data = $response->toArray();
73+
74+
return [
75+
'weather' => self::WMO_CODES[$data['current']['weather_code']] ?? 'Unknown',
76+
'time' => $data['current']['time'],
77+
'temperature' => $data['current']['temperature_2m'].$data['current_units']['temperature_2m'],
78+
'wind_speed' => $data['current']['wind_speed_10m'].$data['current_units']['wind_speed_10m'],
79+
];
80+
}
81+
82+
/**
83+
* @param float $latitude the latitude of the location
84+
* @param float $longitude the longitude of the location
85+
* @param int $days the number of days to forecast
86+
*
87+
* @return array{
88+
* weather: string,
89+
* time: string,
90+
* temperature_min: string,
91+
* temperature_max: string,
92+
* }[]
93+
*/
94+
public function forecast(
95+
float $latitude,
96+
float $longitude,
97+
#[ToolParameter(minimum: 1, maximum: 16)]
98+
int $days = 7,
99+
): array {
100+
$response = $this->httpClient->request('GET', 'https://api.open-meteo.com/v1/forecast', [
101+
'query' => [
102+
'latitude' => $latitude,
103+
'longitude' => $longitude,
104+
'daily' => 'weather_code,temperature_2m_max,temperature_2m_min',
105+
'forecast_days' => $days,
29106
],
30107
]);
31108

32-
return $response->getContent();
109+
$data = $response->toArray();
110+
$forecast = [];
111+
for ($i = 0; $i < $days; ++$i) {
112+
$forecast[] = [
113+
'weather' => self::WMO_CODES[$data['daily']['weather_code'][$i]] ?? 'Unknown',
114+
'time' => $data['daily']['time'][$i],
115+
'temperature_min' => $data['daily']['temperature_2m_min'][$i].$data['daily_units']['temperature_2m_min'],
116+
'temperature_max' => $data['daily']['temperature_2m_max'][$i].$data['daily_units']['temperature_2m_max'],
117+
];
118+
}
119+
120+
return $forecast;
33121
}
34122
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\LlmChain\Tests\Chain\ToolBox;
6+
7+
use PhpLlm\LlmChain\Chain\ToolBox\Tool\OpenMeteo;
8+
use PHPUnit\Framework\Attributes\CoversClass;
9+
use PHPUnit\Framework\Attributes\Test;
10+
use PHPUnit\Framework\TestCase;
11+
use Symfony\Component\HttpClient\MockHttpClient;
12+
use Symfony\Component\HttpClient\Response\JsonMockResponse;
13+
14+
#[CoversClass(OpenMeteo::class)]
15+
final class OpenMeteoTest extends TestCase
16+
{
17+
#[Test]
18+
public function current(): void
19+
{
20+
$response = $this->jsonMockResponseFromFile(__DIR__.'/fixtures/openmeteo-current.json');
21+
$httpClient = new MockHttpClient($response);
22+
23+
$openMeteo = new OpenMeteo($httpClient);
24+
25+
$actual = $openMeteo->current(52.52, 13.42);
26+
$expected = [
27+
'weather' => 'Overcast',
28+
'time' => '2024-12-21T01:15',
29+
'temperature' => '2.6°C',
30+
'wind_speed' => '10.7km/h',
31+
];
32+
33+
static::assertSame($expected, $actual);
34+
}
35+
36+
#[Test]
37+
public function forecast(): void
38+
{
39+
$response = $this->jsonMockResponseFromFile(__DIR__.'/fixtures/openmeteo-forecast.json');
40+
$httpClient = new MockHttpClient($response);
41+
42+
$openMeteo = new OpenMeteo($httpClient);
43+
44+
$actual = $openMeteo->forecast(52.52, 13.42, 3);
45+
$expected = [
46+
[
47+
'weather' => 'Light Rain',
48+
'time' => '2024-12-21',
49+
'temperature_min' => '2°C',
50+
'temperature_max' => '6°C',
51+
],
52+
[
53+
'weather' => 'Light Showers',
54+
'time' => '2024-12-22',
55+
'temperature_min' => '1.3°C',
56+
'temperature_max' => '6.4°C',
57+
],
58+
[
59+
'weather' => 'Light Snow Showers',
60+
'time' => '2024-12-23',
61+
'temperature_min' => '1.5°C',
62+
'temperature_max' => '4.1°C',
63+
],
64+
];
65+
66+
static::assertSame($expected, $actual);
67+
}
68+
69+
/**
70+
* This can be replaced by `JsonMockResponse::fromFile` when dropping Symfony 6.4.
71+
*/
72+
private function jsonMockResponseFromFile(string $file): JsonMockResponse
73+
{
74+
return new JsonMockResponse(json_decode(file_get_contents($file), true));
75+
}
76+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"latitude": 52.52,
3+
"longitude": 13.419998,
4+
"generationtime_ms": 0.06508827209472656,
5+
"utc_offset_seconds": 0,
6+
"timezone": "GMT",
7+
"timezone_abbreviation": "GMT",
8+
"elevation": 40.0,
9+
"current_units": {
10+
"time": "iso8601",
11+
"interval": "seconds",
12+
"weather_code": "wmo code",
13+
"temperature_2m": "°C",
14+
"wind_speed_10m": "km/h"
15+
},
16+
"current": {
17+
"time": "2024-12-21T01:15",
18+
"interval": 900,
19+
"weather_code": 3,
20+
"temperature_2m": 2.6,
21+
"wind_speed_10m": 10.7
22+
}
23+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"latitude": 52.52,
3+
"longitude": 13.419998,
4+
"generationtime_ms": 0.0629425048828125,
5+
"utc_offset_seconds": 0,
6+
"timezone": "GMT",
7+
"timezone_abbreviation": "GMT",
8+
"elevation": 38.0,
9+
"daily_units": {
10+
"time": "iso8601",
11+
"weather_code": "wmo code",
12+
"temperature_2m_max": "°C",
13+
"temperature_2m_min": "°C"
14+
},
15+
"daily": {
16+
"time": [
17+
"2024-12-21",
18+
"2024-12-22",
19+
"2024-12-23"
20+
],
21+
"weather_code": [
22+
61,
23+
80,
24+
85
25+
],
26+
"temperature_2m_max": [
27+
6.0,
28+
6.4,
29+
4.1
30+
],
31+
"temperature_2m_min": [
32+
2.0,
33+
1.3,
34+
1.5
35+
]
36+
}
37+
}

0 commit comments

Comments
 (0)