mirror of
https://github.com/symfony/ai.git
synced 2026-03-24 07:52:13 +01:00
[Agent][Toolbox] Add Mapbox.com geocoding tool for address-to-coordinates conversion
This commit is contained in:
@@ -60,6 +60,9 @@ BRAVE_API_KEY=
|
||||
FIRECRAWL_HOST=https://api.firecrawl.dev
|
||||
FIRECRAWL_API_KEY=
|
||||
|
||||
# For using Mapbox (tool)
|
||||
MAPBOX_ACCESS_TOKEN=
|
||||
|
||||
# For using MongoDB Atlas (store)
|
||||
MONGODB_URI=mongodb://symfony:symfony@127.0.0.1:27017
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
require_once __DIR__.'/vendor/autoload.php';
|
||||
(new Dotenv())->loadEnv(__DIR__.'/.env');
|
||||
|
||||
function env(string $var)
|
||||
function env(string $var): string
|
||||
{
|
||||
if (!isset($_SERVER[$var])) {
|
||||
printf('Please set the "%s" environment variable to run this example.', $var);
|
||||
|
||||
34
examples/toolbox/mapbox-geocode.php
Normal file
34
examples/toolbox/mapbox-geocode.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Symfony\AI\Agent\Agent;
|
||||
use Symfony\AI\Agent\Toolbox\AgentProcessor;
|
||||
use Symfony\AI\Agent\Toolbox\Tool\Mapbox;
|
||||
use Symfony\AI\Agent\Toolbox\Toolbox;
|
||||
use Symfony\AI\Platform\Bridge\OpenAi\Gpt;
|
||||
use Symfony\AI\Platform\Bridge\OpenAi\PlatformFactory;
|
||||
use Symfony\AI\Platform\Message\Message;
|
||||
use Symfony\AI\Platform\Message\MessageBag;
|
||||
|
||||
require_once dirname(__DIR__).'/bootstrap.php';
|
||||
|
||||
$platform = PlatformFactory::create(env('OPENAI_API_KEY'), http_client());
|
||||
$model = new Gpt(Gpt::GPT_4O_MINI);
|
||||
|
||||
$mapbox = new Mapbox(http_client(), env('MAPBOX_ACCESS_TOKEN'));
|
||||
$toolbox = new Toolbox([$mapbox], logger: logger());
|
||||
$processor = new AgentProcessor($toolbox);
|
||||
$agent = new Agent($platform, $model, [$processor], [$processor], logger());
|
||||
|
||||
$messages = new MessageBag(Message::ofUser('What are the coordinates of Brandenburg Gate in Berlin?'));
|
||||
$result = $agent->call($messages);
|
||||
|
||||
echo $result->getContent().\PHP_EOL;
|
||||
34
examples/toolbox/mapbox-reverse-geocode.php
Normal file
34
examples/toolbox/mapbox-reverse-geocode.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Symfony\AI\Agent\Agent;
|
||||
use Symfony\AI\Agent\Toolbox\AgentProcessor;
|
||||
use Symfony\AI\Agent\Toolbox\Tool\Mapbox;
|
||||
use Symfony\AI\Agent\Toolbox\Toolbox;
|
||||
use Symfony\AI\Platform\Bridge\OpenAi\Gpt;
|
||||
use Symfony\AI\Platform\Bridge\OpenAi\PlatformFactory;
|
||||
use Symfony\AI\Platform\Message\Message;
|
||||
use Symfony\AI\Platform\Message\MessageBag;
|
||||
|
||||
require_once dirname(__DIR__).'/bootstrap.php';
|
||||
|
||||
$platform = PlatformFactory::create(env('OPENAI_API_KEY'), http_client());
|
||||
$model = new Gpt(Gpt::GPT_4O_MINI);
|
||||
|
||||
$mapbox = new Mapbox(http_client(), env('MAPBOX_ACCESS_TOKEN'));
|
||||
$toolbox = new Toolbox([$mapbox], logger: logger());
|
||||
$processor = new AgentProcessor($toolbox);
|
||||
$agent = new Agent($platform, $model, [$processor], [$processor], logger());
|
||||
|
||||
$messages = new MessageBag(Message::ofUser('What address is at coordinates longitude -73.985131, latitude 40.758895?'));
|
||||
$result = $agent->call($messages);
|
||||
|
||||
echo $result->getContent().\PHP_EOL;
|
||||
@@ -26,6 +26,7 @@ CHANGELOG
|
||||
- `Clock` for current date/time
|
||||
- `Brave` for web search integration
|
||||
- `Crawler` for web page crawling
|
||||
- `Mapbox` for geocoding addresses to coordinates and reverse geocoding
|
||||
- `OpenMeteo` for weather information
|
||||
- `SerpApi` for search engine results
|
||||
- `Tavily` for AI-powered search
|
||||
|
||||
@@ -352,6 +352,8 @@ messages will be added to your MessageBag::
|
||||
* `Brave Tool`_
|
||||
* `Clock Tool`_
|
||||
* `Crawler Tool`_
|
||||
* `Mapbox Geocode Tool`_
|
||||
* `Mapbox Reverse Geocode Tool`_
|
||||
* `SerpAPI Tool`_
|
||||
* `Tavily Tool`_
|
||||
* `Weather Tool with Event Listener`_
|
||||
@@ -623,6 +625,8 @@ useful when certain interactions shouldn't be influenced by the memory context::
|
||||
.. _`Brave Tool`: https://github.com/symfony/ai/blob/main/examples/toolbox/brave.php
|
||||
.. _`Clock Tool`: https://github.com/symfony/ai/blob/main/examples/toolbox/clock.php
|
||||
.. _`Crawler Tool`: https://github.com/symfony/ai/blob/main/examples/toolbox/brave.php
|
||||
.. _`Mapbox Geocode Tool`: https://github.com/symfony/ai/blob/main/examples/toolbox/mapbox-geocode.php
|
||||
.. _`Mapbox Reverse Geocode Tool`: https://github.com/symfony/ai/blob/main/examples/toolbox/mapbox-reverse-geocode.php
|
||||
.. _`SerpAPI Tool`: https://github.com/symfony/ai/blob/main/examples/toolbox/serpapi.php
|
||||
.. _`Tavily Tool`: https://github.com/symfony/ai/blob/main/examples/toolbox/tavily.php
|
||||
.. _`Weather Tool with Event Listener`: https://github.com/symfony/ai/blob/main/examples/toolbox/weather-event.php
|
||||
|
||||
159
src/agent/src/Toolbox/Tool/Mapbox.php
Normal file
159
src/agent/src/Toolbox/Tool/Mapbox.php
Normal file
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\AI\Agent\Toolbox\Tool;
|
||||
|
||||
use Symfony\AI\Agent\Toolbox\Attribute\AsTool;
|
||||
use Symfony\AI\Platform\Contract\JsonSchema\Attribute\With;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
/**
|
||||
* @author Oskar Stark <oskarstark@googlemail.com>
|
||||
*/
|
||||
#[AsTool(name: 'geocode', description: 'Convert addresses to coordinates using Mapbox Geocoding API', method: 'geocode')]
|
||||
#[AsTool(name: 'reverse_geocode', description: 'Convert coordinates to addresses using Mapbox Reverse Geocoding API', method: 'reverseGeocode')]
|
||||
final readonly class Mapbox
|
||||
{
|
||||
public function __construct(
|
||||
private HttpClientInterface $httpClient,
|
||||
#[\SensitiveParameter] private string $accessToken,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $address The address to geocode (e.g., "1600 Pennsylvania Ave, Washington DC")
|
||||
* @param int $limit Maximum number of results to return (1-10)
|
||||
*
|
||||
* @return array{
|
||||
* results: array<array{
|
||||
* address: string,
|
||||
* coordinates: array{longitude: float, latitude: float},
|
||||
* relevance: float,
|
||||
* place_type: string[]
|
||||
* }>,
|
||||
* count: int
|
||||
* }
|
||||
*/
|
||||
public function geocode(
|
||||
string $address,
|
||||
#[With(minimum: 1, maximum: 10)]
|
||||
int $limit = 1,
|
||||
): array {
|
||||
$response = $this->httpClient->request('GET', 'https://api.mapbox.com/geocoding/v5/mapbox.places/'.urlencode($address).'.json', [
|
||||
'query' => [
|
||||
'access_token' => $this->accessToken,
|
||||
'limit' => $limit,
|
||||
],
|
||||
]);
|
||||
|
||||
$data = $response->toArray();
|
||||
|
||||
if (!isset($data['features']) || [] === $data['features']) {
|
||||
return [
|
||||
'results' => [],
|
||||
'count' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
$results = [];
|
||||
foreach ($data['features'] as $feature) {
|
||||
$center = $feature['center'] ?? [0.0, 0.0];
|
||||
$results[] = [
|
||||
'address' => $feature['place_name'] ?? '',
|
||||
'coordinates' => [
|
||||
'longitude' => $center[0] ?? 0.0,
|
||||
'latitude' => $center[1] ?? 0.0,
|
||||
],
|
||||
'relevance' => $feature['relevance'] ?? 0.0,
|
||||
'place_type' => $feature['place_type'] ?? [],
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'results' => $results,
|
||||
'count' => \count($results),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param float $longitude The longitude coordinate
|
||||
* @param float $latitude The latitude coordinate
|
||||
* @param int $limit Maximum number of results to return (1-5)
|
||||
*
|
||||
* @return array{
|
||||
* results: array<array{
|
||||
* address: string,
|
||||
* coordinates: array{longitude: float, latitude: float},
|
||||
* place_type: string[],
|
||||
* context: array<array{id: string, text: string}>
|
||||
* }>,
|
||||
* count: int
|
||||
* }
|
||||
*/
|
||||
public function reverseGeocode(
|
||||
float $longitude,
|
||||
float $latitude,
|
||||
#[With(minimum: 1, maximum: 5)]
|
||||
int $limit = 1,
|
||||
): array {
|
||||
$response = $this->httpClient->request('GET', 'https://api.mapbox.com/search/geocode/v6/reverse', [
|
||||
'query' => [
|
||||
'longitude' => $longitude,
|
||||
'latitude' => $latitude,
|
||||
'access_token' => $this->accessToken,
|
||||
'limit' => $limit,
|
||||
],
|
||||
]);
|
||||
|
||||
$data = $response->toArray();
|
||||
|
||||
if (!isset($data['features']) || [] === $data['features']) {
|
||||
return [
|
||||
'results' => [],
|
||||
'count' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
$results = [];
|
||||
foreach ($data['features'] as $feature) {
|
||||
$properties = $feature['properties'] ?? [];
|
||||
$coordinates = $properties['coordinates'] ?? [];
|
||||
|
||||
$context = [];
|
||||
if (isset($properties['context'])) {
|
||||
foreach ($properties['context'] as $key => $contextItem) {
|
||||
if (\is_array($contextItem) && isset($contextItem['name'])) {
|
||||
$context[] = [
|
||||
'id' => $contextItem['id'] ?? $contextItem['mapbox_id'] ?? '',
|
||||
'text' => $contextItem['name'],
|
||||
'type' => $key,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$results[] = [
|
||||
'address' => $properties['place_formatted'] ?? $properties['name'] ?? '',
|
||||
'coordinates' => [
|
||||
'longitude' => $coordinates['longitude'] ?? 0.0,
|
||||
'latitude' => $coordinates['latitude'] ?? 0.0,
|
||||
],
|
||||
'place_type' => [$properties['feature_type'] ?? 'unknown'],
|
||||
'context' => $context,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'results' => $results,
|
||||
'count' => \count($results),
|
||||
];
|
||||
}
|
||||
}
|
||||
165
src/agent/tests/Toolbox/Tool/MapboxTest.php
Normal file
165
src/agent/tests/Toolbox/Tool/MapboxTest.php
Normal file
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\AI\Agent\Tests\Toolbox\Tool;
|
||||
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\AI\Agent\Toolbox\Tool\Mapbox;
|
||||
use Symfony\Component\HttpClient\MockHttpClient;
|
||||
use Symfony\Component\HttpClient\Response\JsonMockResponse;
|
||||
|
||||
#[CoversClass(Mapbox::class)]
|
||||
final class MapboxTest extends TestCase
|
||||
{
|
||||
public function testGeocodeWithSingleResult()
|
||||
{
|
||||
$result = $this->jsonMockResponseFromFile(__DIR__.'/fixtures/mapbox-geocode-single.json');
|
||||
$httpClient = new MockHttpClient($result);
|
||||
|
||||
$mapbox = new Mapbox($httpClient, 'test_token');
|
||||
|
||||
$actual = $mapbox->geocode('Brandenburg Gate, Berlin');
|
||||
$expected = [
|
||||
'results' => [
|
||||
[
|
||||
'address' => 'Brandenburg Gate, Pariser Platz, 10117 Berlin, Germany',
|
||||
'coordinates' => [
|
||||
'longitude' => 13.377704,
|
||||
'latitude' => 52.516275,
|
||||
],
|
||||
'relevance' => 1.0,
|
||||
'place_type' => ['poi'],
|
||||
],
|
||||
],
|
||||
'count' => 1,
|
||||
];
|
||||
|
||||
$this->assertEquals($expected, $actual);
|
||||
}
|
||||
|
||||
public function testGeocodeWithMultipleResults()
|
||||
{
|
||||
$result = $this->jsonMockResponseFromFile(__DIR__.'/fixtures/mapbox-geocode-multiple.json');
|
||||
$httpClient = new MockHttpClient($result);
|
||||
|
||||
$mapbox = new Mapbox($httpClient, 'test_token');
|
||||
|
||||
$actual = $mapbox->geocode('Paris', 2);
|
||||
$expected = [
|
||||
'results' => [
|
||||
[
|
||||
'address' => 'Paris, France',
|
||||
'coordinates' => [
|
||||
'longitude' => 2.3522,
|
||||
'latitude' => 48.8566,
|
||||
],
|
||||
'relevance' => 1.0,
|
||||
'place_type' => ['place'],
|
||||
],
|
||||
[
|
||||
'address' => 'Paris, Texas, United States',
|
||||
'coordinates' => [
|
||||
'longitude' => -95.5555,
|
||||
'latitude' => 33.6609,
|
||||
],
|
||||
'relevance' => 0.8,
|
||||
'place_type' => ['place'],
|
||||
],
|
||||
],
|
||||
'count' => 2,
|
||||
];
|
||||
|
||||
$this->assertEquals($expected, $actual);
|
||||
}
|
||||
|
||||
public function testGeocodeWithNoResults()
|
||||
{
|
||||
$result = $this->jsonMockResponseFromFile(__DIR__.'/fixtures/mapbox-geocode-empty.json');
|
||||
$httpClient = new MockHttpClient($result);
|
||||
|
||||
$mapbox = new Mapbox($httpClient, 'test_token');
|
||||
|
||||
$actual = $mapbox->geocode('nonexistent location xyz123');
|
||||
$expected = [
|
||||
'results' => [],
|
||||
'count' => 0,
|
||||
];
|
||||
|
||||
$this->assertEquals($expected, $actual);
|
||||
}
|
||||
|
||||
public function testReverseGeocodeWithValidCoordinates()
|
||||
{
|
||||
$result = $this->jsonMockResponseFromFile(__DIR__.'/fixtures/mapbox-reverse-geocode.json');
|
||||
$httpClient = new MockHttpClient($result);
|
||||
|
||||
$mapbox = new Mapbox($httpClient, 'test_token');
|
||||
|
||||
$actual = $mapbox->reverseGeocode(-73.985131, 40.758895);
|
||||
$expected = [
|
||||
'results' => [
|
||||
[
|
||||
'address' => 'Times Square, New York, NY 10036, United States',
|
||||
'coordinates' => [
|
||||
'longitude' => -73.985131,
|
||||
'latitude' => 40.758895,
|
||||
],
|
||||
'place_type' => ['address'],
|
||||
'context' => [
|
||||
[
|
||||
'id' => 'place.12345',
|
||||
'text' => 'New York',
|
||||
'type' => 'place',
|
||||
],
|
||||
[
|
||||
'id' => 'region.6789',
|
||||
'text' => 'New York',
|
||||
'type' => 'region',
|
||||
],
|
||||
[
|
||||
'id' => 'country.54321',
|
||||
'text' => 'United States',
|
||||
'type' => 'country',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'count' => 1,
|
||||
];
|
||||
|
||||
$this->assertEquals($expected, $actual);
|
||||
}
|
||||
|
||||
public function testReverseGeocodeWithNoResults()
|
||||
{
|
||||
$result = $this->jsonMockResponseFromFile(__DIR__.'/fixtures/mapbox-reverse-geocode-empty.json');
|
||||
$httpClient = new MockHttpClient($result);
|
||||
|
||||
$mapbox = new Mapbox($httpClient, 'test_token');
|
||||
|
||||
$actual = $mapbox->reverseGeocode(0.0, 0.0);
|
||||
$expected = [
|
||||
'results' => [],
|
||||
'count' => 0,
|
||||
];
|
||||
|
||||
$this->assertEquals($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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"type": "FeatureCollection",
|
||||
"query": ["nonexistent", "location", "xyz123"],
|
||||
"features": []
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"type": "FeatureCollection",
|
||||
"query": ["paris"],
|
||||
"features": [
|
||||
{
|
||||
"id": "place.123",
|
||||
"type": "Feature",
|
||||
"place_type": ["place"],
|
||||
"relevance": 1.0,
|
||||
"place_name": "Paris, France",
|
||||
"center": [2.3522, 48.8566],
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [2.3522, 48.8566]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "place.456",
|
||||
"type": "Feature",
|
||||
"place_type": ["place"],
|
||||
"relevance": 0.8,
|
||||
"place_name": "Paris, Texas, United States",
|
||||
"center": [-95.5555, 33.6609],
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [-95.5555, 33.6609]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"type": "FeatureCollection",
|
||||
"query": ["brandenburg", "gate", "berlin"],
|
||||
"features": [
|
||||
{
|
||||
"id": "poi.123456",
|
||||
"type": "Feature",
|
||||
"place_type": ["poi"],
|
||||
"relevance": 1.0,
|
||||
"place_name": "Brandenburg Gate, Pariser Platz, 10117 Berlin, Germany",
|
||||
"center": [13.377704, 52.516275],
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [13.377704, 52.516275]
|
||||
},
|
||||
"context": [
|
||||
{
|
||||
"id": "region.12345",
|
||||
"text": "Berlin"
|
||||
},
|
||||
{
|
||||
"id": "country.6789",
|
||||
"text": "Germany"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"type": "FeatureCollection",
|
||||
"features": []
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"type": "FeatureCollection",
|
||||
"features": [
|
||||
{
|
||||
"type": "Feature",
|
||||
"id": "address.987654321",
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [-73.985131, 40.758895]
|
||||
},
|
||||
"properties": {
|
||||
"mapbox_id": "address.987654321",
|
||||
"feature_type": "address",
|
||||
"name": "Times Square",
|
||||
"coordinates": {
|
||||
"longitude": -73.985131,
|
||||
"latitude": 40.758895,
|
||||
"accuracy": "rooftop"
|
||||
},
|
||||
"place_formatted": "Times Square, New York, NY 10036, United States",
|
||||
"context": {
|
||||
"place": {
|
||||
"mapbox_id": "place.12345",
|
||||
"name": "New York"
|
||||
},
|
||||
"region": {
|
||||
"mapbox_id": "region.6789",
|
||||
"name": "New York"
|
||||
},
|
||||
"country": {
|
||||
"mapbox_id": "country.54321",
|
||||
"name": "United States"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"attribution": "NOTICE: © 2024 Mapbox and its suppliers."
|
||||
}
|
||||
Reference in New Issue
Block a user