Skip to content

Commit 2222db7

Browse files
committed
Enable tools to add sources
1 parent 2ac3005 commit 2222db7

File tree

13 files changed

+403
-4
lines changed

13 files changed

+403
-4
lines changed

docs/components/agent.rst

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,52 @@ If you want to expose the underlying error to the LLM, you can throw a custom ex
314314
}
315315
}
316316

317+
Tool Sources
318+
~~~~~~~~~~~~
319+
320+
Some tools bring in data to the agent from external sources, like search engines or APIs. Those sources can be exposed
321+
by enabling `keepToolSources` as argument of the :class:`Symfony\\AI\\Agent\\Toolbox\\AgentProcessor`::
322+
323+
use Symfony\AI\Agent\Toolbox\AgentProcessor;
324+
use Symfony\AI\Agent\Toolbox\Toolbox;
325+
326+
$toolbox = new Toolbox([new MyTool()]);
327+
$toolProcessor = new AgentProcessor($toolbox, keepToolSources: true);
328+
329+
In the tool implementation sources can be added by implementing the
330+
:class:`Symfony\\AI\\Agent\\Toolbox\\Source\\HasSourcesInterface` in combination with the trait
331+
:class:`Symfony\\AI\\Agent\\Toolbox\\Source\\HasSourcesTrait`::
332+
333+
use Symfony\AI\Agent\Toolbox\Source\HasSourcesInterface;
334+
use Symfony\AI\Agent\Toolbox\Source\HasSourcesTrait;
335+
336+
#[AsTool('my_tool', 'Example tool with sources.')]
337+
final class MyTool implements HasSourcesInterface
338+
{
339+
use HasSourcesTrait;
340+
341+
public function __invoke(string $query): string
342+
{
343+
// Add sources relevant for the result
344+
345+
$this->addSource(
346+
new Source('Example Source 1', 'https://example.com/source1', 'Relevant content from source 1'),
347+
);
348+
349+
// return result
350+
}
351+
}
352+
353+
The sources can be fetched from the metadata of the result after the agent execution::
354+
355+
$result = $agent->call($messages);
356+
357+
foreach ($result->getMetadata()->get('sources', []) as $source) {
358+
echo sprintf(' - %s (%s): %s', $source->getName(), $source->getReference(), $source->getContent());
359+
}
360+
361+
See `Anthropic Toolbox Example`_ for a complete example using sources with Wikipedia tool.
362+
317363
Tool Filtering
318364
~~~~~~~~~~~~~~
319365

@@ -765,6 +811,7 @@ Code Examples
765811

766812

767813
.. _`Platform Component`: https://github.com/symfony/ai-platform
814+
.. _`Anthropic Toolbox Example`: https://github.com/symfony/ai/blob/main/examples/anthropic/toolcall.php
768815
.. _`Brave Tool`: https://github.com/symfony/ai/blob/main/examples/toolbox/brave.php
769816
.. _`Clock Tool`: https://github.com/symfony/ai/blob/main/examples/toolbox/clock.php
770817
.. _`Crawler Tool`: https://github.com/symfony/ai/blob/main/examples/toolbox/brave.php

examples/anthropic/toolcall.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
use Symfony\AI\Agent\Agent;
1313
use Symfony\AI\Agent\Toolbox\AgentProcessor;
14+
use Symfony\AI\Agent\Toolbox\Source\Source;
1415
use Symfony\AI\Agent\Toolbox\Tool\Wikipedia;
1516
use Symfony\AI\Agent\Toolbox\Toolbox;
1617
use Symfony\AI\Platform\Bridge\Anthropic\PlatformFactory;
@@ -23,10 +24,16 @@
2324

2425
$wikipedia = new Wikipedia(http_client());
2526
$toolbox = new Toolbox([$wikipedia], logger: logger());
26-
$processor = new AgentProcessor($toolbox);
27+
$processor = new AgentProcessor($toolbox, keepToolSources: true);
2728
$agent = new Agent($platform, 'claude-3-5-sonnet-20241022', [$processor], [$processor]);
2829

2930
$messages = new MessageBag(Message::ofUser('Who is the current chancellor of Germany?'));
3031
$result = $agent->call($messages);
3132

32-
echo $result->getContent().\PHP_EOL;
33+
echo $result->getContent().\PHP_EOL.\PHP_EOL;
34+
35+
echo 'Used sources:'.\PHP_EOL;
36+
foreach ($result->getMetadata()->get('sources', []) as $source) {
37+
echo sprintf(' - %s (%s)', $source->getName(), $source->getReference()).\PHP_EOL;
38+
}
39+
echo \PHP_EOL;

fixtures/Tool/ToolSources.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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\Fixtures\Tool;
13+
14+
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;
18+
19+
#[AsTool('tool_sources', 'Tool that records some sources')]
20+
final class ToolSources implements HasSourcesInterface
21+
{
22+
use HasSourcesTrait;
23+
24+
/**
25+
* @param string $query Search query
26+
*/
27+
public function __invoke(string $query): string
28+
{
29+
$foundContent = 'Content of that relevant article.';
30+
31+
$this->addSource(
32+
new Source('Relevant Article', 'https://example.com/relevant-article', $foundContent),
33+
);
34+
35+
return $foundContent;
36+
}
37+
}

src/agent/src/Toolbox/AgentProcessor.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use Symfony\AI\Agent\Output;
1919
use Symfony\AI\Agent\OutputProcessorInterface;
2020
use Symfony\AI\Agent\Toolbox\Event\ToolCallsExecuted;
21+
use Symfony\AI\Agent\Toolbox\Source\Source;
2122
use Symfony\AI\Agent\Toolbox\StreamResult as ToolboxStreamResponse;
2223
use Symfony\AI\Platform\Message\AssistantMessage;
2324
use Symfony\AI\Platform\Message\Message;
@@ -34,11 +35,25 @@ final class AgentProcessor implements InputProcessorInterface, OutputProcessorIn
3435
{
3536
use AgentAwareTrait;
3637

38+
/**
39+
* Sources get collected during tool calls on class level to be able to handle consecutive tool calls.
40+
* They get added to the result metadata and reset when the outermost agent call is finished via nesting level.
41+
*
42+
* @var Source[]
43+
*/
44+
private array $sources = [];
45+
46+
/**
47+
* Tracks the nesting level of agent calls.
48+
*/
49+
private int $nestingLevel = 0;
50+
3751
public function __construct(
3852
private readonly ToolboxInterface $toolbox,
3953
private readonly ToolResultConverter $resultConverter = new ToolResultConverter(),
4054
private readonly ?EventDispatcherInterface $eventDispatcher = null,
4155
private readonly bool $keepToolMessages = false,
56+
private readonly bool $keepToolSources = false,
4257
) {
4358
}
4459

@@ -87,6 +102,7 @@ private function isFlatStringArray(array $tools): bool
87102
private function handleToolCallsCallback(Output $output): \Closure
88103
{
89104
return function (ToolCallResult $result, ?AssistantMessage $streamedAssistantResponse = null) use ($output): ResultInterface {
105+
++$this->nestingLevel;
90106
$messages = $this->keepToolMessages ? $output->getMessageBag() : clone $output->getMessageBag();
91107

92108
if (null !== $streamedAssistantResponse && '' !== $streamedAssistantResponse->getContent()) {
@@ -101,6 +117,7 @@ private function handleToolCallsCallback(Output $output): \Closure
101117
foreach ($toolCalls as $toolCall) {
102118
$results[] = $toolResult = $this->toolbox->execute($toolCall);
103119
$messages->add(Message::ofToolCall($toolCall, $this->resultConverter->convert($toolResult)));
120+
array_push($this->sources, ...$toolResult->getSources());
104121
}
105122

106123
$event = new ToolCallsExecuted(...$results);
@@ -109,6 +126,12 @@ private function handleToolCallsCallback(Output $output): \Closure
109126
$result = $event->hasResult() ? $event->getResult() : $this->agent->call($messages, $output->getOptions());
110127
} while ($result instanceof ToolCallResult);
111128

129+
--$this->nestingLevel;
130+
if ($this->keepToolSources && 0 === $this->nestingLevel) {
131+
$result->getMetadata()->add('sources', $this->sources);
132+
$this->sources = [];
133+
}
134+
112135
return $result;
113136
};
114137
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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\Source;
13+
14+
interface HasSourcesInterface
15+
{
16+
public function setSourceMap(SourceMap $sourceMap): void;
17+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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\Source;
13+
14+
trait HasSourcesTrait
15+
{
16+
private SourceMap $sourceMap;
17+
18+
public function setSourceMap(SourceMap $sourceMap): void
19+
{
20+
$this->sourceMap = $sourceMap;
21+
}
22+
23+
public function getSourceMap(): SourceMap
24+
{
25+
return $this->sourceMap ??= new SourceMap();
26+
}
27+
28+
private function addSource(Source $source): void
29+
{
30+
$this->getSourceMap()->addSource($source);
31+
}
32+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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\Source;
13+
14+
readonly class Source
15+
{
16+
public function __construct(
17+
private string $name,
18+
private string $reference,
19+
private string $content,
20+
) {
21+
}
22+
23+
public function getName(): string
24+
{
25+
return $this->name;
26+
}
27+
28+
public function getReference(): string
29+
{
30+
return $this->reference;
31+
}
32+
33+
public function getContent(): string
34+
{
35+
return $this->content;
36+
}
37+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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\Source;
13+
14+
class SourceMap
15+
{
16+
/**
17+
* @var Source[]
18+
*/
19+
private array $sources = [];
20+
21+
/**
22+
* @return Source[]
23+
*/
24+
public function getSources(): array
25+
{
26+
return $this->sources;
27+
}
28+
29+
public function addSource(Source $source): void
30+
{
31+
$this->sources[] = $source;
32+
}
33+
}

src/agent/src/Toolbox/Tool/Wikipedia.php

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,20 @@
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('wikipedia_search', description: 'Searches Wikipedia for a given query', method: 'search')]
2124
#[AsTool('wikipedia_article', description: 'Retrieves a Wikipedia article by its title', method: 'article')]
22-
final readonly class Wikipedia
25+
final class Wikipedia implements HasSourcesInterface
2326
{
27+
use HasSourcesTrait;
28+
2429
public function __construct(
2530
private HttpClientInterface $httpClient,
2631
private string $locale = 'en',
@@ -81,6 +86,10 @@ public function article(string $title): string
8186
$result .= \PHP_EOL;
8287
}
8388

89+
$this->addSource(
90+
new Source($article['title'], $this->getUrl($article['title']), $article['extract'])
91+
);
92+
8493
return $result.'This is the content of article "'.$article['title'].'":'.\PHP_EOL.$article['extract'];
8594
}
8695

@@ -96,4 +105,9 @@ private function execute(array $query, ?string $locale = null): array
96105

97106
return $response->toArray();
98107
}
108+
109+
private function getUrl(string $title): string
110+
{
111+
return \sprintf('https://%s.wikipedia.org/wiki/%s', $this->locale, str_replace(' ', '_', $title));
112+
}
99113
}

src/agent/src/Toolbox/ToolResult.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,21 @@
1111

1212
namespace Symfony\AI\Agent\Toolbox;
1313

14+
use Symfony\AI\Agent\Toolbox\Source\Source;
1415
use Symfony\AI\Platform\Result\ToolCall;
1516

1617
/**
1718
* @author Christopher Hertel <mail@christopher-hertel.de>
1819
*/
1920
final readonly class ToolResult
2021
{
22+
/**
23+
* @param Source[] $sources
24+
*/
2125
public function __construct(
2226
private ToolCall $toolCall,
2327
private mixed $result,
28+
private array $sources = [],
2429
) {
2530
}
2631

@@ -33,4 +38,12 @@ public function getResult(): mixed
3338
{
3439
return $this->result;
3540
}
41+
42+
/**
43+
* @return Source[]
44+
*/
45+
public function getSources(): array
46+
{
47+
return $this->sources;
48+
}
3649
}

0 commit comments

Comments
 (0)