Skip to content

Commit 7559456

Browse files
committed
bug #177 [Agent][AIBundle] Improve tool argument denormalization (valtzu)
This PR was merged into the main branch. Discussion ---------- [Agent][AIBundle] Improve tool argument denormalization | Q | A | ------------- | --- | Bug fix? | yes, kinda | New feature? | no | Docs? | no | License | MIT Address some issues regarding argument value denormalization: 1. Arrays of objects did not work, even with `serializer` that had `ArrayDenormalizer` in it 1. We did not check for mandatory parameters before 1. Extra arguments from the platform caused an exception – now they are ignored * If we still want to throw, that's fine, but it should be a more managed exception instead of `Trying to call getType() on null` 1. Use `symfony/type-info` instead of trying to resolve the type ourselves from reflection (which completely ignores phpdoc) * Arrays/collections still need some special handling since `ArrayDenormalizer` does not support `array<key, value>` syntax Commits ------- c174b64 [Agent][AIBundle] Improve tool argument resolving
2 parents a32efc3 + c174b64 commit 7559456

File tree

5 files changed

+109
-5
lines changed

5 files changed

+109
-5
lines changed

fixtures/SomeStructure.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,12 @@
1313

1414
final class SomeStructure
1515
{
16+
public function __construct(?string $some = null)
17+
{
18+
if (null !== $some) {
19+
$this->some = $some;
20+
}
21+
}
22+
1623
public string $some;
1724
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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+
use Symfony\AI\Fixtures\SomeStructure;
16+
17+
#[AsTool('tool_array_multidimensional', 'A tool with multidimensional array parameters')]
18+
final class ToolArrayMultidimensional
19+
{
20+
/**
21+
* @param float[][] $vectors
22+
* @param array<string, list<int>> $sequences
23+
* @param SomeStructure[][] $objects
24+
*/
25+
public function __invoke(array $vectors, array $sequences, array $objects): string
26+
{
27+
return 'Hello world!';
28+
}
29+
}

src/agent/src/Toolbox/ToolCallArgumentResolver.php

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,29 @@
1111

1212
namespace Symfony\AI\Agent\Toolbox;
1313

14+
use Symfony\AI\Agent\Toolbox\Exception\ToolException;
1415
use Symfony\AI\Platform\Result\ToolCall;
1516
use Symfony\AI\Platform\Tool\Tool;
17+
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
1618
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
1719
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
1820
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
1921
use Symfony\Component\Serializer\Serializer;
22+
use Symfony\Component\TypeInfo\Type\CollectionType;
23+
use Symfony\Component\TypeInfo\TypeResolver\TypeResolver;
2024

2125
/**
2226
* @author Valtteri R <[email protected]>
2327
*/
2428
final readonly class ToolCallArgumentResolver
2529
{
30+
private TypeResolver $typeResolver;
31+
2632
public function __construct(
27-
private DenormalizerInterface $denormalizer = new Serializer([new DateTimeNormalizer(), new ObjectNormalizer()]),
33+
private DenormalizerInterface $denormalizer = new Serializer([new DateTimeNormalizer(), new ObjectNormalizer(), new ArrayDenormalizer()]),
34+
?TypeResolver $typeResolver = null,
2835
) {
36+
$this->typeResolver = $typeResolver ?? TypeResolver::create();
2937
}
3038

3139
/**
@@ -35,13 +43,33 @@ public function resolveArguments(Tool $metadata, ToolCall $toolCall): array
3543
{
3644
$method = new \ReflectionMethod($metadata->reference->class, $metadata->reference->method);
3745

38-
/** @var array<string, \ReflectionProperty> $parameters */
46+
/** @var array<string, \ReflectionParameter> $parameters */
3947
$parameters = array_column($method->getParameters(), null, 'name');
4048
$arguments = [];
4149

42-
foreach ($toolCall->arguments as $name => $value) {
43-
$parameterType = (string) $parameters[$name]->getType();
44-
$arguments[$name] = 'array' === $parameterType ? $value : $this->denormalizer->denormalize($value, $parameterType);
50+
foreach ($parameters as $name => $reflectionParameter) {
51+
if (!\array_key_exists($name, $toolCall->arguments)) {
52+
if (!$reflectionParameter->isOptional()) {
53+
throw new ToolException(\sprintf('Parameter "%s" is mandatory for tool "%s".', $name, $toolCall->name));
54+
}
55+
continue;
56+
}
57+
58+
$value = $toolCall->arguments[$name];
59+
$parameterType = $this->typeResolver->resolve($reflectionParameter);
60+
$dimensions = '';
61+
while ($parameterType instanceof CollectionType) {
62+
$dimensions .= '[]';
63+
$parameterType = $parameterType->getCollectionValueType();
64+
}
65+
66+
$parameterType .= $dimensions;
67+
68+
if ($this->denormalizer->supportsDenormalization($value, $parameterType)) {
69+
$value = $this->denormalizer->denormalize($value, $parameterType);
70+
}
71+
72+
$arguments[$name] = $value;
4573
}
4674

4775
return $arguments;

src/agent/tests/Toolbox/ToolCallArgumentResolverTest.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,11 @@
1616
use PHPUnit\Framework\Attributes\UsesClass;
1717
use PHPUnit\Framework\TestCase;
1818
use Symfony\AI\Agent\Toolbox\ToolCallArgumentResolver;
19+
use Symfony\AI\Fixtures\SomeStructure;
1920
use Symfony\AI\Fixtures\Tool\ToolArray;
21+
use Symfony\AI\Fixtures\Tool\ToolArrayMultidimensional;
2022
use Symfony\AI\Fixtures\Tool\ToolDate;
23+
use Symfony\AI\Fixtures\Tool\ToolNoParams;
2124
use Symfony\AI\Platform\Result\ToolCall;
2225
use Symfony\AI\Platform\Tool\ExecutionReference;
2326
use Symfony\AI\Platform\Tool\Tool;
@@ -57,4 +60,40 @@ public function resolveScalarArrayArguments(): void
5760

5861
self::assertSame($expected, $resolver->resolveArguments($metadata, $toolCall));
5962
}
63+
64+
#[Test]
65+
public function resolveMultidimensionalArrayArguments(): void
66+
{
67+
$resolver = new ToolCallArgumentResolver();
68+
69+
$metadata = new Tool(new ExecutionReference(ToolArrayMultidimensional::class, '__invoke'), 'tool_array_multidimensional', 'A tool with multidimensional array parameters');
70+
$toolCall = new ToolCall('tool_id_1234', 'tool_array_multidimensional', [
71+
'vectors' => [[1.2, 3.4], [4.5, 5.6]],
72+
'sequences' => ['first' => [1, 2, 3], 'second' => [4, 5, 6]],
73+
'objects' => [[['some' => 'a'], ['some' => 'b']]],
74+
]);
75+
76+
$expected = [
77+
'vectors' => [[1.2, 3.4], [4.5, 5.6]],
78+
'sequences' => ['first' => [1, 2, 3], 'second' => [4, 5, 6]],
79+
'objects' => [[new SomeStructure('a'), new SomeStructure('b')]],
80+
];
81+
82+
self::assertEquals($expected, $resolver->resolveArguments($metadata, $toolCall));
83+
}
84+
85+
#[Test]
86+
public function ignoreExtraArguments(): void
87+
{
88+
$resolver = new ToolCallArgumentResolver();
89+
90+
$metadata = new Tool(new ExecutionReference(ToolNoParams::class, '__invoke'), 'tool_no_params', 'A tool without params');
91+
$toolCall = new ToolCall('tool_id_1234', 'tool_no_params', [
92+
'foo' => 1,
93+
'bar' => 2,
94+
'baz' => 3,
95+
]);
96+
97+
self::assertSame([], $resolver->resolveArguments($metadata, $toolCall));
98+
}
6099
}

src/ai-bundle/config/services.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@
9292
->set('ai.tool_call_argument_resolver', ToolCallArgumentResolver::class)
9393
->args([
9494
service('serializer'),
95+
service('type_info.resolver')->nullOnInvalid(),
9596
])
9697
->set('ai.tool.agent_processor.abstract', ToolProcessor::class)
9798
->abstract()

0 commit comments

Comments
 (0)