diff --git a/.github/workflows/integration-tests.yaml b/.github/workflows/integration-tests.yaml index 0f50fead2..92861a02c 100644 --- a/.github/workflows/integration-tests.yaml +++ b/.github/workflows/integration-tests.yaml @@ -95,16 +95,15 @@ jobs: - name: Install root dependencies uses: ramsey/composer-install@v3 + - name: Build root packages + run: php .github/build-packages.php + - name: Install demo dependencies uses: ramsey/composer-install@v3 with: composer-options: "--no-scripts" working-directory: demo - - name: Link demo - working-directory: demo - run: ../link - - run: composer run-script auto-scripts --no-interaction working-directory: demo diff --git a/demo/composer.json b/demo/composer.json index ea319937b..48d373170 100644 --- a/demo/composer.json +++ b/demo/composer.json @@ -14,6 +14,7 @@ "php-http/discovery": "^1.20", "runtime/frankenphp-symfony": "^0.2.0", "symfony/ai-bundle": "@dev", + "symfony/ai-openmeteo-tool": "@dev", "symfony/asset": "~7.3.0", "symfony/asset-mapper": "~7.3.0", "symfony/clock": "~7.3.0", diff --git a/demo/config/packages/ai.yaml b/demo/config/packages/ai.yaml index b51dfbe89..85e61a5b1 100644 --- a/demo/config/packages/ai.yaml +++ b/demo/config/packages/ai.yaml @@ -106,7 +106,7 @@ services: autoconfigure: true # Symfony\AI\Agent\Toolbox\Tool\Clock: ~ - # Symfony\AI\Agent\Toolbox\Tool\OpenMeteo: ~ + # Symfony\AI\Agent\Bridge\OpenMeteo\OpenMeteo: ~ # Symfony\AI\Agent\Toolbox\Tool\SerpApi: # $apiKey: '%env(SERP_API_KEY)%' Symfony\AI\Agent\Toolbox\Tool\Wikipedia: ~ diff --git a/docs/bundles/ai-bundle.rst b/docs/bundles/ai-bundle.rst index 8ad5b3c56..469056e2b 100644 --- a/docs/bundles/ai-bundle.rst +++ b/docs/bundles/ai-bundle.rst @@ -781,7 +781,7 @@ To use existing tools, you can register them as a service: autoconfigure: true Symfony\AI\Agent\Toolbox\Tool\Clock: ~ - Symfony\AI\Agent\Toolbox\Tool\OpenMeteo: ~ + Symfony\AI\Agent\Bridge\OpenMeteo\OpenMeteo: ~ Symfony\AI\Agent\Toolbox\Tool\SerpApi: $apiKey: '%env(SERP_API_KEY)%' Symfony\AI\Agent\Toolbox\Tool\SimilaritySearch: ~ diff --git a/examples/composer.json b/examples/composer.json index 4ed101ed3..501f2f97d 100644 --- a/examples/composer.json +++ b/examples/composer.json @@ -20,6 +20,7 @@ "symfony/ai-agent": "@dev", "symfony/ai-brave-tool": "@dev", "symfony/ai-chat": "@dev", + "symfony/ai-openmeteo-tool": "@dev", "symfony/ai-platform": "@dev", "symfony/ai-store": "@dev", "symfony/ai-tavily-tool": "@dev", diff --git a/examples/toolbox/weather-event.php b/examples/toolbox/weather-event.php index 03d4156f1..81f47e1f7 100644 --- a/examples/toolbox/weather-event.php +++ b/examples/toolbox/weather-event.php @@ -10,9 +10,9 @@ */ use Symfony\AI\Agent\Agent; +use Symfony\AI\Agent\Bridge\OpenMeteo\OpenMeteo; use Symfony\AI\Agent\Toolbox\AgentProcessor; use Symfony\AI\Agent\Toolbox\Event\ToolCallsExecuted; -use Symfony\AI\Agent\Toolbox\Tool\OpenMeteo; use Symfony\AI\Agent\Toolbox\Toolbox; use Symfony\AI\Platform\Bridge\OpenAi\PlatformFactory; use Symfony\AI\Platform\Message\Message; diff --git a/src/agent/src/Bridge/OpenMeteo/.gitattributes b/src/agent/src/Bridge/OpenMeteo/.gitattributes new file mode 100644 index 000000000..14c3c3594 --- /dev/null +++ b/src/agent/src/Bridge/OpenMeteo/.gitattributes @@ -0,0 +1,3 @@ +/Tests export-ignore +/phpunit.xml.dist export-ignore +/.git* export-ignore diff --git a/src/agent/src/Bridge/OpenMeteo/.github/close-pull-request.yml b/src/agent/src/Bridge/OpenMeteo/.github/close-pull-request.yml new file mode 100644 index 000000000..bb5a02835 --- /dev/null +++ b/src/agent/src/Bridge/OpenMeteo/.github/close-pull-request.yml @@ -0,0 +1,20 @@ +name: Close Pull Request + +on: + pull_request_target: + types: [opened] + +jobs: + run: + runs-on: ubuntu-latest + steps: + - uses: superbrothers/close-pull-request@v3 + with: + comment: | + Thanks for your Pull Request! We love contributions. + + However, you should instead open your PR on the main repository: + https://github.com/symfony/ai + + This repository is what we call a "subtree split": a read-only subset of that main repository. + We're looking forward to your PR there! diff --git a/src/agent/src/Bridge/OpenMeteo/.gitignore b/src/agent/src/Bridge/OpenMeteo/.gitignore new file mode 100644 index 000000000..76367ee5b --- /dev/null +++ b/src/agent/src/Bridge/OpenMeteo/.gitignore @@ -0,0 +1,4 @@ +vendor/ +composer.lock +phpunit.xml +.phpunit.result.cache diff --git a/src/agent/src/Bridge/OpenMeteo/CHANGELOG.md b/src/agent/src/Bridge/OpenMeteo/CHANGELOG.md new file mode 100644 index 000000000..0915f3546 --- /dev/null +++ b/src/agent/src/Bridge/OpenMeteo/CHANGELOG.md @@ -0,0 +1,7 @@ +CHANGELOG +========= + +0.1 +--- + + * Add the bridge diff --git a/src/agent/src/Bridge/OpenMeteo/LICENSE b/src/agent/src/Bridge/OpenMeteo/LICENSE new file mode 100644 index 000000000..bc38d714e --- /dev/null +++ b/src/agent/src/Bridge/OpenMeteo/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2025-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/agent/src/Bridge/OpenMeteo/OpenMeteo.php b/src/agent/src/Bridge/OpenMeteo/OpenMeteo.php new file mode 100644 index 000000000..607f9f9c5 --- /dev/null +++ b/src/agent/src/Bridge/OpenMeteo/OpenMeteo.php @@ -0,0 +1,132 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Agent\Bridge\OpenMeteo; + +use Symfony\AI\Agent\Toolbox\Attribute\AsTool; +use Symfony\AI\Platform\Contract\JsonSchema\Attribute\With; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +/** + * @author Christopher Hertel + */ +#[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 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 readonly HttpClientInterface $httpClient, + ) { + } + + /** + * @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 current(float $latitude, float $longitude): array + { + $result = $this->httpClient->request('GET', 'https://api.open-meteo.com/v1/forecast', [ + 'query' => [ + 'latitude' => $latitude, + 'longitude' => $longitude, + 'current' => 'weather_code,temperature_2m,wind_speed_10m', + ], + ]); + + $data = $result->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, + #[With(minimum: 1, maximum: 16)] + int $days = 7, + ): array { + $result = $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, + ], + ]); + + $data = $result->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/src/agent/src/Bridge/OpenMeteo/Tests/OpenMeteoTest.php b/src/agent/src/Bridge/OpenMeteo/Tests/OpenMeteoTest.php new file mode 100644 index 000000000..2e463ff39 --- /dev/null +++ b/src/agent/src/Bridge/OpenMeteo/Tests/OpenMeteoTest.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Agent\Bridge\OpenMeteo\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\AI\Agent\Bridge\OpenMeteo\OpenMeteo; +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\HttpClient\Response\JsonMockResponse; + +final class OpenMeteoTest extends TestCase +{ + public function testCurrent() + { + $result = JsonMockResponse::fromFile(__DIR__.'/fixtures/current.json'); + $httpClient = new MockHttpClient($result); + + $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', + ]; + + $this->assertSame($expected, $actual); + } + + public function testForecast() + { + $result = JsonMockResponse::fromFile(__DIR__.'/fixtures/forecast.json'); + $httpClient = new MockHttpClient($result); + + $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', + ], + ]; + + $this->assertSame($expected, $actual); + } +} diff --git a/src/agent/src/Bridge/OpenMeteo/Tests/fixtures/current.json b/src/agent/src/Bridge/OpenMeteo/Tests/fixtures/current.json new file mode 100644 index 000000000..16d6cb266 --- /dev/null +++ b/src/agent/src/Bridge/OpenMeteo/Tests/fixtures/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/src/agent/src/Bridge/OpenMeteo/Tests/fixtures/forecast.json b/src/agent/src/Bridge/OpenMeteo/Tests/fixtures/forecast.json new file mode 100644 index 000000000..beb4e1413 --- /dev/null +++ b/src/agent/src/Bridge/OpenMeteo/Tests/fixtures/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 + ] + } +} diff --git a/src/agent/src/Bridge/OpenMeteo/composer.json b/src/agent/src/Bridge/OpenMeteo/composer.json new file mode 100644 index 000000000..445783b26 --- /dev/null +++ b/src/agent/src/Bridge/OpenMeteo/composer.json @@ -0,0 +1,41 @@ +{ + "name": "symfony/ai-openmeteo-tool", + "description": "OpenMeteo weather AI tool bridge for Symfony applications.", + "license": "MIT", + "type": "library", + "keywords": ["ai", "bridge", "openmeteo", "weather", "agent", "tool"], + "authors": [ + { + "name": "Christopher Hertel", + "email": "mail@christopher-hertel.de" + } + ], + "require": { + "php": ">=8.2", + "symfony/ai-agent": "@dev", + "symfony/http-client": "^7.3|^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.5.13" + }, + "autoload": { + "psr-4": { + "Symfony\\AI\\Agent\\Bridge\\OpenMeteo\\": "" + } + }, + "autoload-dev": { + "psr-4": { + "Symfony\\AI\\Agent\\Bridge\\OpenMeteo\\Tests\\": "Tests/" + } + }, + "config": { + "sort-packages": true + }, + "extra": { + "thanks": { + "name": "symfony/ai", + "url": "https://github.com/symfony/ai" + } + }, + "minimum-stability": "dev" +} diff --git a/src/agent/src/Bridge/OpenMeteo/phpunit.xml.dist b/src/agent/src/Bridge/OpenMeteo/phpunit.xml.dist new file mode 100644 index 000000000..eec252789 --- /dev/null +++ b/src/agent/src/Bridge/OpenMeteo/phpunit.xml.dist @@ -0,0 +1,32 @@ + + + + + + + + + + ./Tests/ + + + + + + ./ + + + ./Resources + ./Tests + ./vendor + + +