Skip to content

Commit fc8db39

Browse files
authored
feat: add tool call event (#187)
1 parent 4adb1a2 commit fc8db39

16 files changed

+169
-179
lines changed

README.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,11 +226,27 @@ See attribute class [ToolParameter](src/Chain/ToolBox/Attribute/ToolParameter.ph
226226
> [!NOTE]
227227
> Please be aware, that this is only converted in a JSON Schema for the LLM to respect, but not validated by LLM Chain.
228228
229+
#### Tool Result Interception
230+
231+
To react to the result of a tool, you can implement an EventListener or EventSubscriber, that listens to the
232+
`ToolCallsExecuted` event. This event is dispatched after the `ToolBox` executed all current tool calls and enables
233+
you to skip the next LLM call by setting a response yourself:
234+
235+
```php
236+
$eventDispatcher->addListener(ToolCallsExecuted::class, function (ToolCallsExecuted $event): void {
237+
foreach ($event->toolCallResults as $toolCallResult) {
238+
if (str_starts_with($toolCallResult->toolCall->name, 'weather_')) {
239+
$event->response = new StructuredResponse($toolCallResult->result);
240+
}
241+
}
242+
});
243+
```
244+
229245
#### Code Examples (with built-in tools)
230246

231247
1. **Clock Tool**: [toolbox-clock.php](examples/toolbox-clock.php)
232248
1. **SerpAPI Tool**: [toolbox-serpapi.php](examples/toolbox-serpapi.php)
233-
1. **Weather Tool**: [toolbox-weather.php](examples/toolbox-weather.php)
249+
1. **Weather Tool with Event Listener**: [toolbox-weather-event.php](examples/toolbox-weather-event.php)
234250
1. **Wikipedia Tool**: [toolbox-wikipedia.php](examples/toolbox-wikipedia.php)
235251
1. **YouTube Transcriber Tool**: [toolbox-youtube.php](examples/toolbox-youtube.php) (with streaming)
236252

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"symfony/css-selector": "^6.4 || ^7.1",
3838
"symfony/dom-crawler": "^6.4 || ^7.1",
3939
"symfony/dotenv": "^6.4 || ^7.1",
40+
"symfony/event-dispatcher": "^6.4 || ^7.1",
4041
"symfony/finder": "^6.4 || ^7.1",
4142
"symfony/process": "^6.4 || ^7.1",
4243
"symfony/var-dumper": "^6.4 || ^7.1"

examples/toolbox-weather.php renamed to examples/toolbox-weather-event.php

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@
44
use PhpLlm\LlmChain\Bridge\OpenAI\PlatformFactory;
55
use PhpLlm\LlmChain\Chain;
66
use PhpLlm\LlmChain\Chain\ToolBox\ChainProcessor;
7+
use PhpLlm\LlmChain\Chain\ToolBox\Event\ToolCallsExecuted;
78
use PhpLlm\LlmChain\Chain\ToolBox\Tool\OpenMeteo;
89
use PhpLlm\LlmChain\Chain\ToolBox\ToolAnalyzer;
910
use PhpLlm\LlmChain\Chain\ToolBox\ToolBox;
1011
use PhpLlm\LlmChain\Model\Message\Message;
1112
use PhpLlm\LlmChain\Model\Message\MessageBag;
13+
use PhpLlm\LlmChain\Model\Response\StructuredResponse;
1214
use Symfony\Component\Dotenv\Dotenv;
15+
use Symfony\Component\EventDispatcher\EventDispatcher;
1316
use Symfony\Component\HttpClient\HttpClient;
1417

1518
require_once dirname(__DIR__).'/vendor/autoload.php';
@@ -23,12 +26,22 @@
2326
$platform = PlatformFactory::create($_ENV['OPENAI_API_KEY']);
2427
$llm = new GPT(GPT::GPT_4O_MINI);
2528

26-
$wikipedia = new OpenMeteo(HttpClient::create());
27-
$toolBox = new ToolBox(new ToolAnalyzer(), [$wikipedia]);
28-
$processor = new ChainProcessor($toolBox);
29+
$openMeteo = new OpenMeteo(HttpClient::create());
30+
$toolBox = new ToolBox(new ToolAnalyzer(), [$openMeteo]);
31+
$eventDispatcher = new EventDispatcher();
32+
$processor = new ChainProcessor($toolBox, eventDispatcher: $eventDispatcher);
2933
$chain = new Chain($platform, $llm, [$processor], [$processor]);
3034

31-
$messages = new MessageBag(Message::ofUser('How is the weather currently in Berlin? And how about tomorrow?'));
35+
// Add tool call result listener to enforce chain exits direct with structured response for weather tools
36+
$eventDispatcher->addListener(ToolCallsExecuted::class, function (ToolCallsExecuted $event): void {
37+
foreach ($event->toolCallResults as $toolCallResult) {
38+
if (str_starts_with($toolCallResult->toolCall->name, 'weather_')) {
39+
$event->response = new StructuredResponse($toolCallResult->result);
40+
}
41+
}
42+
});
43+
44+
$messages = new MessageBag(Message::ofUser('How is the weather currently in Berlin?'));
3245
$response = $chain->call($messages);
3346

34-
echo $response->getContent().PHP_EOL;
47+
dump($response->getContent());

src/Chain/ToolBox/ChainProcessor.php

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,20 @@
1010
use PhpLlm\LlmChain\Chain\InputProcessor;
1111
use PhpLlm\LlmChain\Chain\Output;
1212
use PhpLlm\LlmChain\Chain\OutputProcessor;
13+
use PhpLlm\LlmChain\Chain\ToolBox\Event\ToolCallsExecuted;
1314
use PhpLlm\LlmChain\Exception\MissingModelSupport;
1415
use PhpLlm\LlmChain\Model\Message\Message;
1516
use PhpLlm\LlmChain\Model\Response\ToolCallResponse;
17+
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
1618

1719
final class ChainProcessor implements InputProcessor, OutputProcessor, ChainAwareProcessor
1820
{
1921
use ChainAwareTrait;
2022

2123
public function __construct(
22-
private ToolBoxInterface $toolBox,
24+
private readonly ToolBoxInterface $toolBox,
25+
private ToolResultConverter $resultConverter = new ToolResultConverter(),
26+
private readonly ?EventDispatcherInterface $eventDispatcher = null,
2327
) {
2428
}
2529

@@ -47,12 +51,17 @@ public function processOutput(Output $output): void
4751
$toolCalls = $output->response->getContent();
4852
$messages->add(Message::ofAssistant(toolCalls: $toolCalls));
4953

54+
$results = [];
5055
foreach ($toolCalls as $toolCall) {
5156
$result = $this->toolBox->execute($toolCall);
52-
$messages->add(Message::ofToolCall($toolCall, $result));
57+
$results[] = new ToolCallResult($toolCall, $result);
58+
$messages->add(Message::ofToolCall($toolCall, $this->resultConverter->convert($result)));
5359
}
5460

55-
$output->response = $this->chain->call($messages, $output->options);
61+
$event = new ToolCallsExecuted(...$results);
62+
$this->eventDispatcher?->dispatch($event);
63+
64+
$output->response = $event->hasResponse() ? $event->response : $this->chain->call($messages, $output->options);
5665
}
5766
}
5867
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\LlmChain\Chain\ToolBox\Event;
6+
7+
use PhpLlm\LlmChain\Chain\ToolBox\ToolCallResult;
8+
use PhpLlm\LlmChain\Model\Response\ResponseInterface;
9+
10+
final class ToolCallsExecuted
11+
{
12+
/**
13+
* @var ToolCallResult[]
14+
*/
15+
public readonly array $toolCallResults;
16+
public ResponseInterface $response;
17+
18+
public function __construct(ToolCallResult ...$toolCallResults)
19+
{
20+
$this->toolCallResults = $toolCallResults;
21+
}
22+
23+
public function hasResponse(): bool
24+
{
25+
return isset($this->response);
26+
}
27+
}

src/Chain/ToolBox/ToolBox.php

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public function getMap(): array
4848
return $this->map = $map;
4949
}
5050

51-
public function execute(ToolCall $toolCall): string
51+
public function execute(ToolCall $toolCall): mixed
5252
{
5353
foreach ($this->tools as $tool) {
5454
foreach ($this->toolAnalyzer->getMetadata($tool::class) as $metadata) {
@@ -64,14 +64,6 @@ public function execute(ToolCall $toolCall): string
6464
throw ToolBoxException::executionFailed($toolCall, $e);
6565
}
6666

67-
if ($result instanceof \JsonSerializable || is_array($result)) {
68-
return json_encode($result, flags: JSON_THROW_ON_ERROR);
69-
}
70-
71-
if (is_integer($result) || is_float($result) || $result instanceof \Stringable) {
72-
return (string) $result;
73-
}
74-
7567
return $result;
7668
}
7769
}

src/Chain/ToolBox/ToolBoxInterface.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,5 @@ interface ToolBoxInterface
1313
*/
1414
public function getMap(): array;
1515

16-
public function execute(ToolCall $toolCall): string;
16+
public function execute(ToolCall $toolCall): mixed;
1717
}

src/Chain/ToolBox/ToolCallResult.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\LlmChain\Chain\ToolBox;
6+
7+
use PhpLlm\LlmChain\Model\Response\ToolCall;
8+
9+
final readonly class ToolCallResult
10+
{
11+
public function __construct(
12+
public ToolCall $toolCall,
13+
public mixed $result,
14+
) {
15+
}
16+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\LlmChain\Chain\ToolBox;
6+
7+
final readonly class ToolResultConverter
8+
{
9+
public function convert(mixed $result): string
10+
{
11+
if ($result instanceof \JsonSerializable || is_array($result)) {
12+
return json_encode($result, flags: JSON_THROW_ON_ERROR);
13+
}
14+
15+
if (is_integer($result) || is_float($result) || $result instanceof \Stringable) {
16+
return (string) $result;
17+
}
18+
19+
return $result;
20+
}
21+
}

tests/Chain/ToolBox/ToolBoxTest.php

Lines changed: 0 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,6 @@
1616
use PhpLlm\LlmChain\Tests\Fixture\Tool\ToolNoParams;
1717
use PhpLlm\LlmChain\Tests\Fixture\Tool\ToolOptionalParam;
1818
use PhpLlm\LlmChain\Tests\Fixture\Tool\ToolRequiredParams;
19-
use PhpLlm\LlmChain\Tests\Fixture\Tool\ToolReturningArray;
20-
use PhpLlm\LlmChain\Tests\Fixture\Tool\ToolReturningFloat;
21-
use PhpLlm\LlmChain\Tests\Fixture\Tool\ToolReturningInteger;
22-
use PhpLlm\LlmChain\Tests\Fixture\Tool\ToolReturningJsonSerializable;
23-
use PhpLlm\LlmChain\Tests\Fixture\Tool\ToolReturningStringable;
2419
use PHPUnit\Framework\Attributes\CoversClass;
2520
use PHPUnit\Framework\Attributes\DataProvider;
2621
use PHPUnit\Framework\Attributes\Test;
@@ -43,11 +38,6 @@ protected function setUp(): void
4338
new ToolRequiredParams(),
4439
new ToolOptionalParam(),
4540
new ToolNoParams(),
46-
new ToolReturningArray(),
47-
new ToolReturningJsonSerializable(),
48-
new ToolReturningInteger(),
49-
new ToolReturningFloat(),
50-
new ToolReturningStringable(),
5141
new ToolException(),
5242
]);
5343
}
@@ -111,41 +101,6 @@ public function toolsMap(): void
111101
'description' => 'A tool without parameters',
112102
],
113103
],
114-
[
115-
'type' => 'function',
116-
'function' => [
117-
'name' => 'tool_returning_array',
118-
'description' => 'A tool returning an array',
119-
],
120-
],
121-
[
122-
'type' => 'function',
123-
'function' => [
124-
'name' => 'tool_returning_json_serializable',
125-
'description' => 'A tool returning an object which implements \JsonSerializable',
126-
],
127-
],
128-
[
129-
'type' => 'function',
130-
'function' => [
131-
'name' => 'tool_returning_integer',
132-
'description' => 'A tool returning an integer',
133-
],
134-
],
135-
[
136-
'type' => 'function',
137-
'function' => [
138-
'name' => 'tool_returning_float',
139-
'description' => 'A tool returning a float',
140-
],
141-
],
142-
[
143-
'type' => 'function',
144-
'function' => [
145-
'name' => 'tool_returning_stringable',
146-
'description' => 'A tool returning an object which implements \Stringable',
147-
],
148-
],
149104
[
150105
'type' => 'function',
151106
'function' => [
@@ -207,30 +162,5 @@ public static function executeProvider(): iterable
207162
'tool_required_params',
208163
['text' => 'Hello', 'number' => 3],
209164
];
210-
211-
yield 'tool_returning_array' => [
212-
'{"foo":"bar"}',
213-
'tool_returning_array',
214-
];
215-
216-
yield 'tool_returning_json_serializable' => [
217-
'{"foo":"bar"}',
218-
'tool_returning_json_serializable',
219-
];
220-
221-
yield 'tool_returning_integer' => [
222-
'42',
223-
'tool_returning_integer',
224-
];
225-
226-
yield 'tool_returning_float' => [
227-
'42.42',
228-
'tool_returning_float',
229-
];
230-
231-
yield 'tool_returning_stringable' => [
232-
'Hi!',
233-
'tool_returning_stringable',
234-
];
235165
}
236166
}

0 commit comments

Comments
 (0)