Skip to content

Commit 36079d2

Browse files
franzwildingOskarStark
authored andcommitted
[Agent] Dispatch more tool call events
1 parent 65fb739 commit 36079d2

File tree

5 files changed

+180
-1
lines changed

5 files changed

+180
-1
lines changed

src/agent/doc/index.rst

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,24 @@ to skip the next LLM call by setting a result yourself::
331331
}
332332
});
333333

334+
Tool Call Lifecycle Events
335+
~~~~~~~~~~~~~~~~~~~~~~~~~~
336+
337+
If you need to react more granularly to the lifecycle of individual tool calls, you can listen to the
338+
``ToolCallArgumentsResolved``, ``ToolCallSucceeded`` and ``ToolCallFailed`` events. These are dispatched at different stages::
339+
340+
$eventDispatcher->addListener(ToolCallArgumentsResolved::class, function (ToolCallArgumentsResolved $event): void {
341+
// Let the client know, that the tool $event->toolCall->name was executed
342+
});
343+
344+
$eventDispatcher->addListener(ToolCallSucceeded::class, function (ToolCallSucceeded $event): void {
345+
// Let the client know, that the tool $event->toolCall->name successfully returned the result $event->result
346+
});
347+
348+
$eventDispatcher->addListener(ToolCallFailed::class, function (ToolCallFailed $event): void {
349+
// Let the client know, that the tool $event->toolCall->name failed with the exception: $event->exception
350+
});
351+
334352
Keeping Tool Messages
335353
~~~~~~~~~~~~~~~~~~~~~
336354

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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\Event;
13+
14+
use Symfony\AI\Platform\Tool\Tool;
15+
16+
/**
17+
* Dispatched after successfully invoking a tool.
18+
*/
19+
final readonly class ToolCallFailed
20+
{
21+
/**
22+
* @param array<string, mixed> $arguments
23+
*/
24+
public function __construct(
25+
public object $tool,
26+
public Tool $metadata,
27+
public array $arguments,
28+
public \Throwable $exception,
29+
) {
30+
}
31+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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\Event;
13+
14+
use Symfony\AI\Platform\Tool\Tool;
15+
16+
/**
17+
* Dispatched after successfully invoking a tool.
18+
*/
19+
final readonly class ToolCallSucceeded
20+
{
21+
/**
22+
* @param array<string, mixed> $arguments
23+
*/
24+
public function __construct(
25+
public object $tool,
26+
public Tool $metadata,
27+
public array $arguments,
28+
public mixed $result,
29+
) {
30+
}
31+
}

src/agent/src/Toolbox/Toolbox.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
use Psr\Log\LoggerInterface;
1515
use Psr\Log\NullLogger;
1616
use Symfony\AI\Agent\Toolbox\Event\ToolCallArgumentsResolved;
17+
use Symfony\AI\Agent\Toolbox\Event\ToolCallFailed;
18+
use Symfony\AI\Agent\Toolbox\Event\ToolCallSucceeded;
1719
use Symfony\AI\Agent\Toolbox\Exception\ToolExecutionException;
1820
use Symfony\AI\Agent\Toolbox\Exception\ToolExecutionExceptionInterface;
1921
use Symfony\AI\Agent\Toolbox\Exception\ToolNotFoundException;
@@ -80,12 +82,14 @@ public function execute(ToolCall $toolCall): mixed
8082

8183
$arguments = $this->argumentResolver->resolveArguments($metadata, $toolCall);
8284
$this->eventDispatcher?->dispatch(new ToolCallArgumentsResolved($tool, $metadata, $arguments));
83-
8485
$result = $tool->{$metadata->reference->method}(...$arguments);
86+
$this->eventDispatcher?->dispatch(new ToolCallSucceeded($tool, $metadata, $arguments, $result));
8587
} catch (ToolExecutionExceptionInterface $e) {
88+
$this->eventDispatcher?->dispatch(new ToolCallFailed($tool, $metadata, $arguments ?? [], $e));
8689
throw $e;
8790
} catch (\Throwable $e) {
8891
$this->logger->warning(\sprintf('Failed to execute tool "%s".', $toolCall->name), ['exception' => $e]);
92+
$this->eventDispatcher?->dispatch(new ToolCallFailed($tool, $metadata, $arguments ?? [], $e));
8993
throw ToolExecutionException::executionFailed($toolCall, $e);
9094
}
9195

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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\Tests\Toolbox;
13+
14+
use PHPUnit\Framework\MockObject\Exception;
15+
use PHPUnit\Framework\TestCase;
16+
use Symfony\AI\Agent\Toolbox\Event\ToolCallArgumentsResolved;
17+
use Symfony\AI\Agent\Toolbox\Event\ToolCallFailed;
18+
use Symfony\AI\Agent\Toolbox\Event\ToolCallSucceeded;
19+
use Symfony\AI\Agent\Toolbox\Toolbox;
20+
use Symfony\AI\Fixtures\Tool\ToolCustomException;
21+
use Symfony\AI\Fixtures\Tool\ToolException;
22+
use Symfony\AI\Fixtures\Tool\ToolNoParams;
23+
use Symfony\AI\Platform\Result\ToolCall;
24+
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
25+
26+
final class ToolboxEventDispatcherTest extends TestCase
27+
{
28+
private Toolbox $toolbox;
29+
private array $dispatchedEvents = [];
30+
31+
/**
32+
* @throws Exception
33+
*/
34+
protected function setUp(): void
35+
{
36+
$dispatcher = $this->createMock(EventDispatcherInterface::class);
37+
$dispatcher
38+
->method('dispatch')
39+
->willReturnCallback(function (object $event, ?string $eventName = null) {
40+
$this->dispatchedEvents[] = $eventName ?? $event::class;
41+
42+
return $event;
43+
});
44+
$this->toolbox = new Toolbox([
45+
new ToolNoParams(),
46+
new ToolException(),
47+
new ToolCustomException(),
48+
], eventDispatcher: $dispatcher);
49+
}
50+
51+
public function testExecuteWithUnknownTool()
52+
{
53+
try {
54+
$this->toolbox->execute(new ToolCall('call_1234', 'foo_bar_baz'));
55+
} catch (\Throwable) {
56+
}
57+
$this->assertEmpty($this->dispatchedEvents);
58+
}
59+
60+
public function testExecuteWithToolExecutionException()
61+
{
62+
try {
63+
$this->toolbox->execute(new ToolCall('call_1234', 'tool_exception'));
64+
} catch (\Throwable) {
65+
}
66+
$this->assertEquals([
67+
ToolCallArgumentsResolved::class,
68+
ToolCallFailed::class,
69+
], $this->dispatchedEvents);
70+
}
71+
72+
public function testExecuteWithCustomExecutionException()
73+
{
74+
try {
75+
$this->toolbox->execute(new ToolCall('call_1234', 'tool_custom_exception'));
76+
} catch (\Throwable) {
77+
}
78+
$this->assertEquals([
79+
ToolCallArgumentsResolved::class,
80+
ToolCallFailed::class,
81+
], $this->dispatchedEvents);
82+
}
83+
84+
public function testExecuteSuccess()
85+
{
86+
try {
87+
$this->toolbox->execute(new ToolCall('call_1234', 'tool_no_params'));
88+
} catch (\Throwable) {
89+
}
90+
$this->assertEquals([
91+
ToolCallArgumentsResolved::class,
92+
ToolCallSucceeded::class,
93+
], $this->dispatchedEvents);
94+
}
95+
}

0 commit comments

Comments
 (0)