Skip to content

Commit 60ff9e7

Browse files
committed
feature #58 feat: Denormalize tool arguments (valtzu)
This PR was merged into the main branch. Discussion ---------- feat: Denormalize tool arguments | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | Docs? | | Issues | | License | MIT Cherry picking php-llm/llm-chain#359 Commits ------- 1491da8 feat: Denormalize tool arguments (#359)
2 parents b1dc740 + 1491da8 commit 60ff9e7

File tree

7 files changed

+206
-1
lines changed

7 files changed

+206
-1
lines changed

fixtures/Tool/ToolDate.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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\Fixtures\Tool;
13+
14+
use Symfony\AI\Agent\Toolbox\Attribute\AsTool;
15+
16+
#[AsTool('tool_date', 'A tool with date parameter')]
17+
final class ToolDate
18+
{
19+
/**
20+
* @param \DateTimeImmutable $date The date
21+
*/
22+
public function __invoke(\DateTimeImmutable $date): string
23+
{
24+
return \sprintf('Weekday: %s', $date->format('l'));
25+
}
26+
}
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 <[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 the arguments are denormalized, just before invoking the tool.
18+
*
19+
* @author Valtteri R <[email protected]>
20+
*/
21+
final readonly class ToolCallArgumentsResolved
22+
{
23+
/**
24+
* @param array<string, mixed> $arguments
25+
*/
26+
public function __construct(
27+
public object $tool,
28+
public Tool $metadata,
29+
public array $arguments,
30+
) {
31+
}
32+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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;
13+
14+
use Symfony\AI\Platform\Response\ToolCall;
15+
use Symfony\AI\Platform\Tool\Tool;
16+
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
17+
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
18+
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
19+
use Symfony\Component\Serializer\Serializer;
20+
21+
/**
22+
* @author Valtteri R <[email protected]>
23+
*/
24+
final readonly class ToolCallArgumentResolver implements ToolCallArgumentResolverInterface
25+
{
26+
public function __construct(
27+
private DenormalizerInterface $denormalizer = new Serializer([new DateTimeNormalizer(), new ObjectNormalizer()]),
28+
) {
29+
}
30+
31+
public function resolveArguments(object $tool, Tool $metadata, ToolCall $toolCall): array
32+
{
33+
$method = new \ReflectionMethod($metadata->reference->class, $metadata->reference->method);
34+
35+
/** @var array<string, \ReflectionProperty> $parameters */
36+
$parameters = array_column($method->getParameters(), null, 'name');
37+
$arguments = [];
38+
39+
foreach ($toolCall->arguments as $name => $value) {
40+
$arguments[$name] = $this->denormalizer->denormalize($value, (string) $parameters[$name]->getType());
41+
}
42+
43+
return $arguments;
44+
}
45+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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;
13+
14+
use Symfony\AI\Platform\Response\ToolCall;
15+
use Symfony\AI\Platform\Tool\Tool;
16+
17+
/**
18+
* @author Valtteri R <[email protected]>
19+
*/
20+
interface ToolCallArgumentResolverInterface
21+
{
22+
/**
23+
* @return array<string, mixed>
24+
*/
25+
public function resolveArguments(object $tool, Tool $metadata, ToolCall $toolCall): array;
26+
}

src/agent/src/Toolbox/Toolbox.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@
1313

1414
use Psr\Log\LoggerInterface;
1515
use Psr\Log\NullLogger;
16+
use Symfony\AI\Agent\Toolbox\Event\ToolCallArgumentsResolved;
1617
use Symfony\AI\Agent\Toolbox\Exception\ToolExecutionException;
1718
use Symfony\AI\Agent\Toolbox\Exception\ToolNotFoundException;
1819
use Symfony\AI\Agent\Toolbox\ToolFactory\ReflectionToolFactory;
1920
use Symfony\AI\Platform\Response\ToolCall;
2021
use Symfony\AI\Platform\Tool\Tool;
22+
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
2123

2224
/**
2325
* @author Christopher Hertel <[email protected]>
@@ -45,6 +47,8 @@ public function __construct(
4547
private readonly ToolFactoryInterface $toolFactory,
4648
iterable $tools,
4749
private readonly LoggerInterface $logger = new NullLogger(),
50+
private readonly ToolCallArgumentResolverInterface $argumentResolver = new ToolCallArgumentResolver(),
51+
private readonly ?EventDispatcherInterface $eventDispatcher = null,
4852
) {
4953
$this->tools = $tools instanceof \Traversable ? iterator_to_array($tools) : $tools;
5054
}
@@ -77,7 +81,11 @@ public function execute(ToolCall $toolCall): mixed
7781

7882
try {
7983
$this->logger->debug(\sprintf('Executing tool "%s".', $toolCall->name), $toolCall->arguments);
80-
$result = $tool->{$metadata->reference->method}(...$toolCall->arguments);
84+
85+
$arguments = $this->argumentResolver->resolveArguments($tool, $metadata, $toolCall);
86+
$this->eventDispatcher?->dispatch(new ToolCallArgumentsResolved($tool, $metadata, $arguments));
87+
88+
$result = $tool->{$metadata->reference->method}(...$arguments);
8189
} catch (\Throwable $e) {
8290
$this->logger->warning(\sprintf('Failed to execute tool "%s".', $toolCall->name), ['exception' => $e]);
8391
throw ToolExecutionException::executionFailed($toolCall, $e);
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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\Attributes\CoversClass;
15+
use PHPUnit\Framework\Attributes\Test;
16+
use PHPUnit\Framework\Attributes\UsesClass;
17+
use PHPUnit\Framework\TestCase;
18+
use Symfony\AI\Agent\Toolbox\ToolCallArgumentResolver;
19+
use Symfony\AI\Fixtures\Tool\ToolDate;
20+
use Symfony\AI\Platform\Response\ToolCall;
21+
use Symfony\AI\Platform\Tool\ExecutionReference;
22+
use Symfony\AI\Platform\Tool\Tool;
23+
24+
#[CoversClass(ToolCallArgumentResolver::class)]
25+
#[UsesClass(Tool::class)]
26+
#[UsesClass(ExecutionReference::class)]
27+
#[UsesClass(ToolCall::class)]
28+
class ToolCallArgumentResolverTest extends TestCase
29+
{
30+
#[Test]
31+
public function resolveArguments(): void
32+
{
33+
$resolver = new ToolCallArgumentResolver();
34+
35+
$tool = new ToolDate();
36+
$metadata = new Tool(new ExecutionReference(ToolDate::class, '__invoke'), 'tool_date', 'test');
37+
$toolCall = new ToolCall('invocation', 'tool_date', ['date' => '2025-06-29']);
38+
39+
self::assertEquals(['date' => new \DateTimeImmutable('2025-06-29')], $resolver->resolveArguments($tool, $metadata, $toolCall));
40+
}
41+
}

src/agent/tests/Toolbox/ToolboxTest.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use Symfony\AI\Agent\Toolbox\ToolFactory\ChainFactory;
2525
use Symfony\AI\Agent\Toolbox\ToolFactory\MemoryToolFactory;
2626
use Symfony\AI\Agent\Toolbox\ToolFactory\ReflectionToolFactory;
27+
use Symfony\AI\Fixtures\Tool\ToolDate;
2728
use Symfony\AI\Fixtures\Tool\ToolException;
2829
use Symfony\AI\Fixtures\Tool\ToolMisconfigured;
2930
use Symfony\AI\Fixtures\Tool\ToolNoAttribute1;
@@ -60,6 +61,7 @@ protected function setUp(): void
6061
new ToolOptionalParam(),
6162
new ToolNoParams(),
6263
new ToolException(),
64+
new ToolDate(),
6365
]);
6466
}
6567

@@ -122,11 +124,30 @@ public function getTools(): void
122124
'This tool is broken',
123125
);
124126

127+
$toolDate = new Tool(
128+
new ExecutionReference(ToolDate::class, '__invoke'),
129+
'tool_date',
130+
'A tool with date parameter',
131+
[
132+
'type' => 'object',
133+
'properties' => [
134+
'date' => [
135+
'type' => 'string',
136+
'format' => 'date-time',
137+
'description' => 'The date',
138+
],
139+
],
140+
'required' => ['date'],
141+
'additionalProperties' => false,
142+
],
143+
);
144+
125145
$expected = [
126146
$toolRequiredParams,
127147
$toolOptionalParam,
128148
$toolNoParams,
129149
$toolException,
150+
$toolDate,
130151
];
131152

132153
self::assertEquals($expected, $actual);
@@ -181,6 +202,12 @@ public static function executeProvider(): iterable
181202
'tool_required_params',
182203
['text' => 'Hello', 'number' => 3],
183204
];
205+
206+
yield 'tool_date' => [
207+
'Weekday: Sunday',
208+
'tool_date',
209+
['date' => '2025-06-29'],
210+
];
184211
}
185212

186213
#[Test]

0 commit comments

Comments
 (0)