Skip to content

Commit 00da77e

Browse files
committed
feature #818 [Agent] Add source feature to more tools (chr-hertel)
This PR was merged into the main branch. Discussion ---------- [Agent] Add source feature to more tools | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | Docs? | no | Issues | | License | MIT Commits ------- 40edab5 Add source feature to more tools
2 parents ffeeb5f + 40edab5 commit 00da77e

File tree

10 files changed

+200
-86
lines changed

10 files changed

+200
-86
lines changed

examples/toolbox/brave.php

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,23 +12,37 @@
1212
use Symfony\AI\Agent\Agent;
1313
use Symfony\AI\Agent\Toolbox\AgentProcessor;
1414
use Symfony\AI\Agent\Toolbox\Tool\Brave;
15-
use Symfony\AI\Agent\Toolbox\Tool\Crawler;
15+
use Symfony\AI\Agent\Toolbox\Tool\Clock;
16+
use Symfony\AI\Agent\Toolbox\Tool\Scraper;
1617
use Symfony\AI\Agent\Toolbox\Toolbox;
1718
use Symfony\AI\Platform\Bridge\OpenAi\PlatformFactory;
1819
use Symfony\AI\Platform\Message\Message;
1920
use Symfony\AI\Platform\Message\MessageBag;
21+
use Symfony\Component\Clock\Clock as SymfonyClock;
2022

2123
require_once dirname(__DIR__).'/bootstrap.php';
2224

2325
$platform = PlatformFactory::create(env('OPENAI_API_KEY'), http_client());
2426

2527
$brave = new Brave(http_client(), env('BRAVE_API_KEY'));
26-
$crawler = new Crawler(http_client());
27-
$toolbox = new Toolbox([$brave, $crawler], logger: logger());
28-
$processor = new AgentProcessor($toolbox);
29-
$agent = new Agent($platform, 'gpt-4o-mini', [$processor], [$processor]);
28+
$clock = new Clock(new SymfonyClock());
29+
$crawler = new Scraper(http_client());
30+
$toolbox = new Toolbox([$brave, $clock, $crawler], logger: logger());
31+
$processor = new AgentProcessor($toolbox, includeSources: true);
32+
$agent = new Agent($platform, 'gpt-4o', [$processor], [$processor]);
3033

31-
$messages = new MessageBag(Message::ofUser('What was the latest game result of Dallas Cowboys?'));
32-
$result = $agent->call($messages);
34+
$prompt = <<<PROMPT
35+
Summarize the latest game of the Dallas Cowboys. When and where was it? Who was the opponent, what was the result,
36+
and how was the game and the weather in the city. Use tools for the research and only answer based on information
37+
given in the context - don't make up information.
38+
PROMPT;
3339

34-
echo $result->getContent().\PHP_EOL;
40+
$result = $agent->call(new MessageBag(Message::ofUser($prompt)));
41+
42+
echo $result->getContent().\PHP_EOL.\PHP_EOL;
43+
44+
echo 'Used sources:'.\PHP_EOL;
45+
foreach ($result->getMetadata()->get('sources', []) as $source) {
46+
echo sprintf(' - %s (%s)', $source->getName(), $source->getReference()).\PHP_EOL;
47+
}
48+
echo \PHP_EOL;

examples/toolbox/serpapi.php

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,38 @@
1111

1212
use Symfony\AI\Agent\Agent;
1313
use Symfony\AI\Agent\Toolbox\AgentProcessor;
14+
use Symfony\AI\Agent\Toolbox\Tool\Clock;
15+
use Symfony\AI\Agent\Toolbox\Tool\Scraper;
1416
use Symfony\AI\Agent\Toolbox\Tool\SerpApi;
1517
use Symfony\AI\Agent\Toolbox\Toolbox;
1618
use Symfony\AI\Platform\Bridge\OpenAi\PlatformFactory;
1719
use Symfony\AI\Platform\Message\Message;
1820
use Symfony\AI\Platform\Message\MessageBag;
21+
use Symfony\Component\Clock\Clock as SymfonyClock;
1922

2023
require_once dirname(__DIR__).'/bootstrap.php';
2124

2225
$platform = PlatformFactory::create(env('OPENAI_API_KEY'), http_client());
2326

27+
$clock = new Clock(new SymfonyClock());
28+
$crawler = new Scraper(http_client());
2429
$serpApi = new SerpApi(http_client(), env('SERP_API_KEY'));
25-
$toolbox = new Toolbox([$serpApi], logger: logger());
26-
$processor = new AgentProcessor($toolbox);
27-
$agent = new Agent($platform, 'gpt-4o-mini', [$processor], [$processor]);
30+
$toolbox = new Toolbox([$clock, $crawler, $serpApi], logger: logger());
31+
$processor = new AgentProcessor($toolbox, includeSources: true);
32+
$agent = new Agent($platform, 'gpt-4o', [$processor], [$processor]);
2833

29-
$messages = new MessageBag(Message::ofUser('Who is the current chancellor of Germany?'));
30-
$result = $agent->call($messages);
34+
$prompt = <<<PROMPT
35+
Summarize the latest game of the Dallas Cowboys. When and where was it? Who was the opponent, what was the result,
36+
and how was the game and the weather in the city. Use tools for the research and only answer based on information
37+
given in the context - don't make up information.
38+
PROMPT;
3139

32-
echo $result->getContent().\PHP_EOL;
40+
$result = $agent->call(new MessageBag(Message::ofUser($prompt)));
41+
42+
echo $result->getContent().\PHP_EOL.\PHP_EOL;
43+
44+
echo 'Used sources:'.\PHP_EOL;
45+
foreach ($result->getMetadata()->get('sources', []) as $source) {
46+
echo sprintf(' - %s (%s)', $source->getName(), $source->getReference()).\PHP_EOL;
47+
}
48+
echo \PHP_EOL;

examples/toolbox/tavily.php

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,36 @@
1111

1212
use Symfony\AI\Agent\Agent;
1313
use Symfony\AI\Agent\Toolbox\AgentProcessor;
14+
use Symfony\AI\Agent\Toolbox\Tool\Clock;
1415
use Symfony\AI\Agent\Toolbox\Tool\Tavily;
1516
use Symfony\AI\Agent\Toolbox\Toolbox;
1617
use Symfony\AI\Platform\Bridge\OpenAi\PlatformFactory;
1718
use Symfony\AI\Platform\Message\Message;
1819
use Symfony\AI\Platform\Message\MessageBag;
20+
use Symfony\Component\Clock\Clock as SymfonyClock;
1921

2022
require_once dirname(__DIR__).'/bootstrap.php';
2123

2224
$platform = PlatformFactory::create(env('OPENAI_API_KEY'), http_client());
2325

26+
$clock = new Clock(new SymfonyClock());
2427
$tavily = new Tavily(http_client(), env('TAVILY_API_KEY'));
25-
$toolbox = new Toolbox([$tavily], logger: logger());
26-
$processor = new AgentProcessor($toolbox);
27-
$agent = new Agent($platform, 'gpt-4o-mini', [$processor], [$processor]);
28+
$toolbox = new Toolbox([$clock, $tavily], logger: logger());
29+
$processor = new AgentProcessor($toolbox, includeSources: true);
30+
$agent = new Agent($platform, 'gpt-4o', [$processor], [$processor]);
2831

29-
$messages = new MessageBag(Message::ofUser('What was the latest game result of Dallas Cowboys?'));
30-
$result = $agent->call($messages);
32+
$prompt = <<<PROMPT
33+
Summarize the latest game of the Dallas Cowboys. When and where was it? Who was the opponent, what was the result,
34+
and how was the game and the weather in the city. Use tools for the research and only answer based on information
35+
given in the context - don't make up information.
36+
PROMPT;
3137

32-
echo $result->getContent().\PHP_EOL;
38+
$result = $agent->call(new MessageBag(Message::ofUser($prompt)));
39+
40+
echo $result->getContent().\PHP_EOL.\PHP_EOL;
41+
42+
echo 'Used sources:'.\PHP_EOL;
43+
foreach ($result->getMetadata()->get('sources', []) as $source) {
44+
echo sprintf(' - %s (%s)', $source->getName(), $source->getReference()).\PHP_EOL;
45+
}
46+
echo \PHP_EOL;

src/agent/src/Toolbox/Source/SourceMap.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
namespace Symfony\AI\Agent\Toolbox\Source;
1313

14-
class SourceMap
14+
final class SourceMap
1515
{
1616
/**
1717
* @var Source[]

src/agent/src/Toolbox/Tool/Brave.php

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,27 @@
1212
namespace Symfony\AI\Agent\Toolbox\Tool;
1313

1414
use Symfony\AI\Agent\Toolbox\Attribute\AsTool;
15+
use Symfony\AI\Agent\Toolbox\Source\HasSourcesInterface;
16+
use Symfony\AI\Agent\Toolbox\Source\HasSourcesTrait;
17+
use Symfony\AI\Agent\Toolbox\Source\Source;
1518
use Symfony\AI\Platform\Contract\JsonSchema\Attribute\With;
1619
use Symfony\Contracts\HttpClient\HttpClientInterface;
1720

1821
/**
1922
* @author Christopher Hertel <mail@christopher-hertel.de>
2023
*/
2124
#[AsTool('brave_search', 'Tool that searches the web using Brave Search')]
22-
final readonly class Brave
25+
final class Brave implements HasSourcesInterface
2326
{
27+
use HasSourcesTrait;
28+
2429
/**
2530
* @param array<string, mixed> $options See https://api-dashboard.search.brave.com/app/documentation/web-search/query#WebSearchAPIQueryParameters
2631
*/
2732
public function __construct(
28-
private HttpClientInterface $httpClient,
29-
#[\SensitiveParameter] private string $apiKey,
30-
private array $options = [],
33+
private readonly HttpClientInterface $httpClient,
34+
#[\SensitiveParameter] private readonly string $apiKey,
35+
private readonly array $options = [],
3136
) {
3237
}
3338

@@ -61,6 +66,13 @@ public function __invoke(
6166
]);
6267

6368
$data = $result->toArray();
69+
$results = $data['web']['results'] ?? [];
70+
71+
foreach ($results as $result) {
72+
$this->addSource(
73+
new Source($result['title'] ?? '', $result['url'] ?? '', $result['description'] ?? '')
74+
);
75+
}
6476

6577
return array_map(static function (array $result) {
6678
return ['title' => $result['title'], 'description' => $result['description'], 'url' => $result['url']];

src/agent/src/Toolbox/Tool/Clock.php

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,23 @@
1212
namespace Symfony\AI\Agent\Toolbox\Tool;
1313

1414
use Symfony\AI\Agent\Toolbox\Attribute\AsTool;
15+
use Symfony\AI\Agent\Toolbox\Source\HasSourcesInterface;
16+
use Symfony\AI\Agent\Toolbox\Source\HasSourcesTrait;
17+
use Symfony\AI\Agent\Toolbox\Source\Source;
1518
use Symfony\Component\Clock\Clock as SymfonyClock;
1619
use Symfony\Component\Clock\ClockInterface;
1720

1821
/**
1922
* @author Christopher Hertel <mail@christopher-hertel.de>
2023
*/
2124
#[AsTool('clock', description: 'Provides the current date and time.')]
22-
final readonly class Clock
25+
final class Clock implements HasSourcesInterface
2326
{
27+
use HasSourcesTrait;
28+
2429
public function __construct(
25-
private ClockInterface $clock = new SymfonyClock(),
26-
private ?string $timezone = null,
30+
private readonly ClockInterface $clock = new SymfonyClock(),
31+
private readonly ?string $timezone = null,
2732
) {
2833
}
2934

@@ -35,6 +40,10 @@ public function __invoke(): string
3540
$now = $now->setTimezone(new \DateTimeZone($this->timezone));
3641
}
3742

43+
$this->addSource(
44+
new Source('Current Time', 'Clock', $now->format('Y-m-d H:i:s'))
45+
);
46+
3847
return \sprintf(
3948
'Current date is %s (YYYY-MM-DD) and the time is %s (HH:MM:SS).',
4049
$now->format('Y-m-d'),

src/agent/src/Toolbox/Tool/Crawler.php

Lines changed: 0 additions & 42 deletions
This file was deleted.
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\Toolbox\Tool;
13+
14+
use Symfony\AI\Agent\Exception\RuntimeException;
15+
use Symfony\AI\Agent\Toolbox\Attribute\AsTool;
16+
use Symfony\AI\Agent\Toolbox\Source\HasSourcesInterface;
17+
use Symfony\AI\Agent\Toolbox\Source\HasSourcesTrait;
18+
use Symfony\AI\Agent\Toolbox\Source\Source;
19+
use Symfony\Component\DomCrawler\Crawler as DomCrawler;
20+
use Symfony\Contracts\HttpClient\HttpClientInterface;
21+
22+
/**
23+
* @author Christopher Hertel <mail@christopher-hertel.de>
24+
*/
25+
#[AsTool('scraper', 'Loads the visible text and title of a website by URL.')]
26+
final class Scraper implements HasSourcesInterface
27+
{
28+
use HasSourcesTrait;
29+
30+
public function __construct(
31+
private readonly HttpClientInterface $httpClient,
32+
) {
33+
if (!class_exists(DomCrawler::class)) {
34+
throw new RuntimeException('For using the Scraper tool, the symfony/dom-crawler package is required. Try running "composer require symfony/dom-crawler".');
35+
}
36+
}
37+
38+
/**
39+
* @param string $url the URL of the page to load data from
40+
*
41+
* @return array{title: string, content: string}
42+
*/
43+
public function __invoke(string $url): array
44+
{
45+
$result = $this->httpClient->request('GET', $url);
46+
$crawler = new DomCrawler($result->getContent());
47+
48+
$title = $crawler->filter('title')->text();
49+
$content = $crawler->filter('body')->text();
50+
51+
$this->addSource(new Source($title, $url, $content));
52+
53+
return [
54+
'title' => $title,
55+
'content' => $content,
56+
];
57+
}
58+
}

src/agent/src/Toolbox/Tool/SerpApi.php

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,24 +12,31 @@
1212
namespace Symfony\AI\Agent\Toolbox\Tool;
1313

1414
use Symfony\AI\Agent\Toolbox\Attribute\AsTool;
15+
use Symfony\AI\Agent\Toolbox\Source\HasSourcesInterface;
16+
use Symfony\AI\Agent\Toolbox\Source\HasSourcesTrait;
17+
use Symfony\AI\Agent\Toolbox\Source\Source;
1518
use Symfony\Contracts\HttpClient\HttpClientInterface;
1619

1720
/**
1821
* @author Christopher Hertel <mail@christopher-hertel.de>
1922
*/
2023
#[AsTool(name: 'serpapi', description: 'search for information on the internet')]
21-
final readonly class SerpApi
24+
final class SerpApi implements HasSourcesInterface
2225
{
26+
use HasSourcesTrait;
27+
2328
public function __construct(
24-
private HttpClientInterface $httpClient,
25-
private string $apiKey,
29+
private readonly HttpClientInterface $httpClient,
30+
private readonly string $apiKey,
2631
) {
2732
}
2833

2934
/**
3035
* @param string $query The search query to use
36+
*
37+
* @return array{title: string, link: string, content: string}[]
3138
*/
32-
public function __invoke(string $query): string
39+
public function __invoke(string $query): array
3340
{
3441
$result = $this->httpClient->request('GET', 'https://serpapi.com/search', [
3542
'query' => [
@@ -38,14 +45,19 @@ public function __invoke(string $query): string
3845
],
3946
]);
4047

41-
return \sprintf('Results for "%s" are "%s".', $query, $this->extractBestResponse($result->toArray()));
42-
}
48+
$data = $result->toArray();
4349

44-
/**
45-
* @param array<string, mixed> $results
46-
*/
47-
private function extractBestResponse(array $results): string
48-
{
49-
return implode('. ', array_map(fn ($story) => $story['title'], $results['organic_results']));
50+
$results = [];
51+
foreach ($data['organic_results'] as $result) {
52+
$results[] = [
53+
'title' => $result['title'],
54+
'link' => $result['link'],
55+
'content' => $result['snippet'],
56+
];
57+
58+
$this->addSource(new Source($result['title'], $result['link'], $result['snippet']));
59+
}
60+
61+
return $results;
5062
}
5163
}

0 commit comments

Comments
 (0)