[Agent][Toolbox] Add Mapbox.com geocoding tool for address-to-coordinates conversion

This commit is contained in:
Oskar Stark
2025-09-09 08:36:35 +02:00
parent 949bc7f6a7
commit 80b83fd409
13 changed files with 507 additions and 1 deletions

View File

@@ -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

View File

@@ -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);

View 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;

View 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;

View File

@@ -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

View File

@@ -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

View 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),
];
}
}

View 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));
}
}

View File

@@ -0,0 +1,5 @@
{
"type": "FeatureCollection",
"query": ["nonexistent", "location", "xyz123"],
"features": []
}

View File

@@ -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]
}
}
]
}

View File

@@ -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"
}
]
}
]
}

View File

@@ -0,0 +1,4 @@
{
"type": "FeatureCollection",
"features": []
}

View File

@@ -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."
}