Skip to content

Commit a46edd7

Browse files
committed
[Agent][Tavily] Add symfony/ai-tavily-tool
1 parent b726c2e commit a46edd7

File tree

13 files changed

+307
-3
lines changed

13 files changed

+307
-3
lines changed

CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,15 @@ CHANGELOG
2525
- `SimilaritySearch` for RAG/vector store searches
2626
- `Agent` allowing agents to use other agents as tools
2727
- `Clock` for current date/time
28-
- `Brave` for web search integration
2928
- `Crawler` for web page crawling
3029
- `Mapbox` for geocoding addresses to coordinates and reverse geocoding
3130
- `OpenMeteo` for weather information
3231
- `SerpApi` for search engine results
33-
- `Tavily` for AI-powered search
3432
- `Wikipedia` for Wikipedia content retrieval
3533
- `YouTubeTranscriber` for YouTube video transcription
34+
* Add bridges:
35+
- `Brave` for web search integration (`symfony/ai-brave-tool`)
36+
- `Tavily` for AI-powered search (`symfony/ai-tavily-tool`)
3637
* Add structured output support:
3738
- PHP class output with automatic conversion from LLM responses
3839
- Array structure output with JSON schema validation

src/Bridge/Tavily/.gitattributes

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/Tests export-ignore
2+
/phpunit.xml.dist export-ignore
3+
/.git* export-ignore
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Please do not submit any Pull Requests here. They will be closed.
2+
---
3+
4+
Please submit your PR here instead:
5+
https://github.com/symfony/ai
6+
7+
This repository is what we call a "subtree split": a read-only subset of that main repository.
8+
We're looking forward to your PR there!
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
name: Close Pull Request
2+
3+
on:
4+
pull_request_target:
5+
types: [opened]
6+
7+
jobs:
8+
run:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- uses: superbrothers/close-pull-request@v3
12+
with:
13+
comment: |
14+
Thanks for your Pull Request! We love contributions.
15+
16+
However, you should instead open your PR on the main repository:
17+
https://github.com/symfony/ai
18+
19+
This repository is what we call a "subtree split": a read-only subset of that main repository.
20+
We're looking forward to your PR there!

src/Bridge/Tavily/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
vendor/
2+
composer.lock
3+
phpunit.xml
4+
.phpunit.result.cache

src/Bridge/Tavily/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
CHANGELOG
2+
=========
3+
4+
0.1
5+
---
6+
7+
* Add the bridge

src/Bridge/Tavily/LICENSE

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Copyright (c) 2025-present Fabien Potencier
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy
4+
of this software and associated documentation files (the "Software"), to deal
5+
in the Software without restriction, including without limitation the rights
6+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
copies of the Software, and to permit persons to whom the Software is furnished
8+
to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in all
11+
copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
THE SOFTWARE.

src/Toolbox/Tool/Tavily.php renamed to src/Bridge/Tavily/Tavily.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
* file that was distributed with this source code.
1010
*/
1111

12-
namespace Symfony\AI\Agent\Toolbox\Tool;
12+
namespace Symfony\AI\Agent\Bridge\Tavily;
1313

1414
use Symfony\AI\Agent\Toolbox\Attribute\AsTool;
1515
use Symfony\AI\Agent\Toolbox\Source\HasSourcesInterface;
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\AI\Agent\Bridge\Tavily\Tests;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\AI\Agent\Bridge\Tavily\Tavily;
16+
use Symfony\Component\HttpClient\MockHttpClient;
17+
use Symfony\Component\HttpClient\Response\JsonMockResponse;
18+
19+
/**
20+
* @author Oskar Stark <[email protected]>
21+
*/
22+
final class TavilyTest extends TestCase
23+
{
24+
public function testSearchReturnsResults()
25+
{
26+
$result = JsonMockResponse::fromFile(__DIR__.'/fixtures/search-results.json');
27+
$httpClient = new MockHttpClient($result);
28+
$tavily = new Tavily($httpClient, 'test-api-key');
29+
30+
$response = $tavily->search('latest AI news');
31+
32+
$this->assertStringContainsString('results', $response);
33+
}
34+
35+
public function testSearchPassesCorrectParameters()
36+
{
37+
$result = JsonMockResponse::fromFile(__DIR__.'/fixtures/search-results.json');
38+
$httpClient = new MockHttpClient($result);
39+
$tavily = new Tavily($httpClient, 'test-api-key', ['include_images' => true]);
40+
41+
$tavily->search('test query');
42+
43+
$requestUrl = $result->getRequestUrl();
44+
$this->assertSame('https://api.tavily.com/search', $requestUrl);
45+
46+
$requestOptions = $result->getRequestOptions();
47+
$this->assertArrayHasKey('body', $requestOptions);
48+
$body = json_decode($requestOptions['body'], true);
49+
$this->assertSame('test query', $body['query']);
50+
$this->assertSame('test-api-key', $body['api_key']);
51+
$this->assertTrue($body['include_images']);
52+
}
53+
54+
public function testSearchAddsSourcesFromResults()
55+
{
56+
$result = JsonMockResponse::fromFile(__DIR__.'/fixtures/search-results.json');
57+
$httpClient = new MockHttpClient($result);
58+
$tavily = new Tavily($httpClient, 'test-api-key');
59+
60+
$tavily->search('test query');
61+
62+
$sources = $tavily->getSourceMap()->getSources();
63+
$this->assertCount(2, $sources);
64+
$this->assertSame('AI breakthrough announced', $sources[0]->getName());
65+
$this->assertSame('https://example.com/ai-news', $sources[0]->getReference());
66+
}
67+
68+
public function testExtractReturnsResults()
69+
{
70+
$result = JsonMockResponse::fromFile(__DIR__.'/fixtures/extract-results.json');
71+
$httpClient = new MockHttpClient($result);
72+
$tavily = new Tavily($httpClient, 'test-api-key');
73+
74+
$response = $tavily->extract(['https://example.com/article']);
75+
76+
$this->assertStringContainsString('results', $response);
77+
}
78+
79+
public function testExtractPassesCorrectParameters()
80+
{
81+
$result = JsonMockResponse::fromFile(__DIR__.'/fixtures/extract-results.json');
82+
$httpClient = new MockHttpClient($result);
83+
$tavily = new Tavily($httpClient, 'test-api-key');
84+
85+
$urls = ['https://example.com/article1', 'https://example.com/article2'];
86+
$tavily->extract($urls);
87+
88+
$requestUrl = $result->getRequestUrl();
89+
$this->assertSame('https://api.tavily.com/extract', $requestUrl);
90+
91+
$requestOptions = $result->getRequestOptions();
92+
$this->assertArrayHasKey('body', $requestOptions);
93+
$body = json_decode($requestOptions['body'], true);
94+
$this->assertSame($urls, $body['urls']);
95+
$this->assertSame('test-api-key', $body['api_key']);
96+
}
97+
98+
public function testExtractAddsSourcesFromResults()
99+
{
100+
$result = JsonMockResponse::fromFile(__DIR__.'/fixtures/extract-results.json');
101+
$httpClient = new MockHttpClient($result);
102+
$tavily = new Tavily($httpClient, 'test-api-key');
103+
104+
$tavily->extract(['https://example.com/article']);
105+
106+
$sources = $tavily->getSourceMap()->getSources();
107+
$this->assertCount(1, $sources);
108+
$this->assertSame('Example Article', $sources[0]->getName());
109+
$this->assertSame('https://example.com/article', $sources[0]->getReference());
110+
}
111+
112+
public function testHandlesEmptySearchResults()
113+
{
114+
$httpClient = new MockHttpClient(new JsonMockResponse(['results' => []]));
115+
$tavily = new Tavily($httpClient, 'test-api-key');
116+
117+
$tavily->search('query with no results');
118+
119+
$sources = $tavily->getSourceMap()->getSources();
120+
$this->assertEmpty($sources);
121+
}
122+
123+
public function testHandlesEmptyExtractResults()
124+
{
125+
$httpClient = new MockHttpClient(new JsonMockResponse(['results' => []]));
126+
$tavily = new Tavily($httpClient, 'test-api-key');
127+
128+
$tavily->extract(['https://nonexistent.com']);
129+
130+
$sources = $tavily->getSourceMap()->getSources();
131+
$this->assertEmpty($sources);
132+
}
133+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"results": [
3+
{
4+
"title": "Example Article",
5+
"url": "https://example.com/article",
6+
"raw_content": "This is the full content of the article that was extracted from the webpage. It contains detailed information about the topic.",
7+
"score": 1.0
8+
}
9+
],
10+
"response_time": 0.89
11+
}

0 commit comments

Comments
 (0)