Skip to content

Commit 6b1e54b

Browse files
committed
Enable tools to add sources
1 parent 4b22015 commit 6b1e54b

File tree

11 files changed

+180
-7
lines changed

11 files changed

+180
-7
lines changed

examples/anthropic/toolcall.php

Lines changed: 12 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,19 @@
2324

2425
$wikipedia = new Wikipedia(http_client());
2526
$toolbox = new Toolbox([$wikipedia], logger: logger());
26-
$processor = new AgentProcessor($toolbox);
27+
$processor = new AgentProcessor($toolbox, keepToolMessages: 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 ($messages->getToolCallMessages() as $message) {
37+
/** @var Source $source */
38+
foreach ($message->getMetadata()->get('tool_sources') as $source) {
39+
echo sprintf(' - %s (%s)', $source->getName(), $source->getReference()).\PHP_EOL;
40+
}
41+
}
42+
echo \PHP_EOL;

src/agent/src/Toolbox/AgentProcessor.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,13 +100,15 @@ private function handleToolCallsCallback(Output $output): \Closure
100100
$results = [];
101101
foreach ($toolCalls as $toolCall) {
102102
$results[] = $toolResult = $this->toolbox->execute($toolCall);
103-
$messages->add(Message::ofToolCall($toolCall, $this->resultConverter->convert($toolResult)));
103+
$message = Message::ofToolCall($toolCall, $this->resultConverter->convert($toolResult));
104+
$message->getMetadata()->add('tool_sources', $toolResult->getSources());
105+
$messages->add($message);
104106
}
105107

106108
$event = new ToolCallsExecuted(...$results);
107109
$this->eventDispatcher?->dispatch($event);
108110

109-
$result = $event->hasResponse() ? $event->getResult() : $this->agent->call($messages, $output->getOptions());
111+
$result = $event->hasResult() ? $event->getResult() : $this->agent->call($messages, $output->getOptions());
110112
} while ($result instanceof ToolCallResult);
111113

112114
return $result;

src/agent/src/Toolbox/Event/ToolCallsExecuted.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public function __construct(ToolResult ...$toolResults)
3030
$this->toolResults = $toolResults;
3131
}
3232

33-
public function hasResponse(): bool
33+
public function hasResult(): bool
3434
{
3535
return isset($this->result);
3636
}
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 <[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\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 <[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\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+
}
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 <[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\Toolbox\Source;
13+
14+
interface UsesSourcesInterface
15+
{
16+
public function setSourceMap(SourceMap $sourceMap): void;
17+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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\Toolbox\Source;
13+
14+
trait UsesSourcesTrait
15+
{
16+
private SourceMap $sourceMap;
17+
18+
public function setSourceMap(SourceMap $sourceMap): void
19+
{
20+
$this->sourceMap = $sourceMap;
21+
}
22+
23+
private function addSource(Source $source): void
24+
{
25+
$this->sourceMap->addSource($source);
26+
}
27+
}

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\Source;
16+
use Symfony\AI\Agent\Toolbox\Source\UsesSourcesInterface;
17+
use Symfony\AI\Agent\Toolbox\Source\UsesSourcesTrait;
1518
use Symfony\Contracts\HttpClient\HttpClientInterface;
1619

1720
/**
1821
* @author Christopher Hertel <[email protected]>
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 UsesSourcesInterface
2326
{
27+
use UsesSourcesTrait;
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 <[email protected]>
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
}

src/agent/src/Toolbox/Toolbox.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
use Symfony\AI\Agent\Toolbox\Exception\ToolExecutionException;
2020
use Symfony\AI\Agent\Toolbox\Exception\ToolExecutionExceptionInterface;
2121
use Symfony\AI\Agent\Toolbox\Exception\ToolNotFoundException;
22+
use Symfony\AI\Agent\Toolbox\Source\SourceMap;
23+
use Symfony\AI\Agent\Toolbox\Source\UsesSourcesInterface;
2224
use Symfony\AI\Agent\Toolbox\ToolFactory\ReflectionToolFactory;
2325
use Symfony\AI\Platform\Result\ToolCall;
2426
use Symfony\AI\Platform\Tool\Tool;
@@ -82,7 +84,17 @@ public function execute(ToolCall $toolCall): ToolResult
8284

8385
$arguments = $this->argumentResolver->resolveArguments($metadata, $toolCall);
8486
$this->eventDispatcher?->dispatch(new ToolCallArgumentsResolved($tool, $metadata, $arguments));
85-
$result = new ToolResult($toolCall, $tool->{$metadata->getReference()->getMethod()}(...$arguments));
87+
88+
if ($tool instanceof UsesSourcesInterface) {
89+
$tool->setSourceMap($sourceMap = new SourceMap());
90+
}
91+
92+
$result = new ToolResult(
93+
$toolCall,
94+
$tool->{$metadata->getReference()->getMethod()}(...$arguments),
95+
$tool instanceof UsesSourcesInterface ? $sourceMap->getSources() : [],
96+
);
97+
8698
$this->eventDispatcher?->dispatch(new ToolCallSucceeded($tool, $metadata, $arguments, $result));
8799
} catch (ToolExecutionExceptionInterface $e) {
88100
$this->eventDispatcher?->dispatch(new ToolCallFailed($tool, $metadata, $arguments ?? [], $e));

0 commit comments

Comments
 (0)