Skip to content

Commit 16fc8b0

Browse files
authored
feat: Denormalize tool arguments (#359)
Resolves #352
1 parent 0f014e2 commit 16fc8b0

File tree

7 files changed

+165
-1
lines changed

7 files changed

+165
-1
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\LlmChain\Chain\Toolbox\Event;
6+
7+
use PhpLlm\LlmChain\Platform\Tool\Tool;
8+
9+
/**
10+
* Dispatched after the arguments are denormalized, just before invoking the tool.
11+
*
12+
* @author Valtteri R <[email protected]>
13+
*/
14+
final readonly class ToolCallArgumentsResolved
15+
{
16+
/**
17+
* @param array<string, mixed> $arguments
18+
*/
19+
public function __construct(
20+
public object $tool,
21+
public Tool $metadata,
22+
public array $arguments,
23+
) {
24+
}
25+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
namespace PhpLlm\LlmChain\Chain\Toolbox;
4+
5+
use PhpLlm\LlmChain\Platform\Response\ToolCall;
6+
use PhpLlm\LlmChain\Platform\Tool\Tool;
7+
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
8+
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
9+
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
10+
use Symfony\Component\Serializer\Serializer;
11+
12+
/**
13+
* @author Valtteri R <[email protected]>
14+
*/
15+
final readonly class ToolCallArgumentResolver implements ToolCallArgumentResolverInterface
16+
{
17+
public function __construct(
18+
private DenormalizerInterface $denormalizer = new Serializer([new DateTimeNormalizer(), new ObjectNormalizer()]),
19+
) {
20+
}
21+
22+
public function resolveArguments(object $tool, Tool $metadata, ToolCall $toolCall): array
23+
{
24+
$method = new \ReflectionMethod($metadata->reference->class, $metadata->reference->method);
25+
26+
/** @var array<string, \ReflectionProperty> $parameters */
27+
$parameters = array_column($method->getParameters(), null, 'name');
28+
$arguments = [];
29+
30+
foreach ($toolCall->arguments as $name => $value) {
31+
$arguments[$name] = $this->denormalizer->denormalize($value, (string) $parameters[$name]->getType());
32+
}
33+
34+
return $arguments;
35+
}
36+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace PhpLlm\LlmChain\Chain\Toolbox;
4+
5+
use PhpLlm\LlmChain\Platform\Response\ToolCall;
6+
use PhpLlm\LlmChain\Platform\Tool\Tool;
7+
8+
/**
9+
* @author Valtteri R <[email protected]>
10+
*/
11+
interface ToolCallArgumentResolverInterface
12+
{
13+
/**
14+
* @return array<string, mixed>
15+
*/
16+
public function resolveArguments(object $tool, Tool $metadata, ToolCall $toolCall): array;
17+
}

src/Chain/Toolbox/Toolbox.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44

55
namespace PhpLlm\LlmChain\Chain\Toolbox;
66

7+
use PhpLlm\LlmChain\Chain\Toolbox\Event\ToolCallArgumentsResolved;
78
use PhpLlm\LlmChain\Chain\Toolbox\Exception\ToolExecutionException;
89
use PhpLlm\LlmChain\Chain\Toolbox\Exception\ToolNotFoundException;
910
use PhpLlm\LlmChain\Chain\Toolbox\ToolFactory\ReflectionToolFactory;
1011
use PhpLlm\LlmChain\Platform\Response\ToolCall;
1112
use PhpLlm\LlmChain\Platform\Tool\Tool;
1213
use Psr\Log\LoggerInterface;
1314
use Psr\Log\NullLogger;
15+
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
1416

1517
/**
1618
* @author Christopher Hertel <[email protected]>
@@ -38,6 +40,8 @@ public function __construct(
3840
private readonly ToolFactoryInterface $toolFactory,
3941
iterable $tools,
4042
private readonly LoggerInterface $logger = new NullLogger(),
43+
private readonly ToolCallArgumentResolverInterface $argumentResolver = new ToolCallArgumentResolver(),
44+
private readonly ?EventDispatcherInterface $eventDispatcher = null,
4145
) {
4246
$this->tools = $tools instanceof \Traversable ? iterator_to_array($tools) : $tools;
4347
}
@@ -70,7 +74,11 @@ public function execute(ToolCall $toolCall): mixed
7074

7175
try {
7276
$this->logger->debug(\sprintf('Executing tool "%s".', $toolCall->name), $toolCall->arguments);
73-
$result = $tool->{$metadata->reference->method}(...$toolCall->arguments);
77+
78+
$arguments = $this->argumentResolver->resolveArguments($tool, $metadata, $toolCall);
79+
$this->eventDispatcher?->dispatch(new ToolCallArgumentsResolved($tool, $metadata, $arguments));
80+
81+
$result = $tool->{$metadata->reference->method}(...$arguments);
7482
} catch (\Throwable $e) {
7583
$this->logger->warning(\sprintf('Failed to execute tool "%s".', $toolCall->name), ['exception' => $e]);
7684
throw ToolExecutionException::executionFailed($toolCall, $e);
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
namespace PhpLlm\LlmChain\Tests\Chain\Toolbox;
4+
5+
use PhpLlm\LlmChain\Chain\Toolbox\ToolCallArgumentResolver;
6+
use PhpLlm\LlmChain\Platform\Response\ToolCall;
7+
use PhpLlm\LlmChain\Platform\Tool\ExecutionReference;
8+
use PhpLlm\LlmChain\Platform\Tool\Tool;
9+
use PhpLlm\LlmChain\Tests\Fixture\Tool\ToolDate;
10+
use PHPUnit\Framework\Attributes\CoversClass;
11+
use PHPUnit\Framework\Attributes\Test;
12+
use PHPUnit\Framework\Attributes\UsesClass;
13+
use PHPUnit\Framework\TestCase;
14+
15+
#[CoversClass(ToolCallArgumentResolver::class)]
16+
#[UsesClass(Tool::class)]
17+
#[UsesClass(ExecutionReference::class)]
18+
#[UsesClass(ToolCall::class)]
19+
class ToolCallArgumentResolverTest extends TestCase
20+
{
21+
#[Test]
22+
public function resolveArguments(): void
23+
{
24+
$resolver = new ToolCallArgumentResolver();
25+
26+
$tool = new ToolDate();
27+
$metadata = new Tool(new ExecutionReference(ToolDate::class, '__invoke'), 'tool_date', 'test');
28+
$toolCall = new ToolCall('invocation', 'tool_date', ['date' => '2025-06-29']);
29+
30+
self::assertEquals(['date' => new \DateTimeImmutable('2025-06-29')], $resolver->resolveArguments($tool, $metadata, $toolCall));
31+
}
32+
}

tests/Chain/Toolbox/ToolboxTest.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use PhpLlm\LlmChain\Platform\Response\ToolCall;
1818
use PhpLlm\LlmChain\Platform\Tool\ExecutionReference;
1919
use PhpLlm\LlmChain\Platform\Tool\Tool;
20+
use PhpLlm\LlmChain\Tests\Fixture\Tool\ToolDate;
2021
use PhpLlm\LlmChain\Tests\Fixture\Tool\ToolException;
2122
use PhpLlm\LlmChain\Tests\Fixture\Tool\ToolMisconfigured;
2223
use PhpLlm\LlmChain\Tests\Fixture\Tool\ToolNoAttribute1;
@@ -53,6 +54,7 @@ protected function setUp(): void
5354
new ToolOptionalParam(),
5455
new ToolNoParams(),
5556
new ToolException(),
57+
new ToolDate(),
5658
]);
5759
}
5860

@@ -115,11 +117,30 @@ public function getTools(): void
115117
'This tool is broken',
116118
);
117119

120+
$toolDate = new Tool(
121+
new ExecutionReference(ToolDate::class, '__invoke'),
122+
'tool_date',
123+
'A tool with date parameter',
124+
[
125+
'type' => 'object',
126+
'properties' => [
127+
'date' => [
128+
'type' => 'string',
129+
'format' => 'date-time',
130+
'description' => 'The date',
131+
],
132+
],
133+
'required' => ['date'],
134+
'additionalProperties' => false,
135+
],
136+
);
137+
118138
$expected = [
119139
$toolRequiredParams,
120140
$toolOptionalParam,
121141
$toolNoParams,
122142
$toolException,
143+
$toolDate,
123144
];
124145

125146
self::assertEquals($expected, $actual);
@@ -174,6 +195,12 @@ public static function executeProvider(): iterable
174195
'tool_required_params',
175196
['text' => 'Hello', 'number' => 3],
176197
];
198+
199+
yield 'tool_date' => [
200+
'Weekday: Sunday',
201+
'tool_date',
202+
['date' => '2025-06-29'],
203+
];
177204
}
178205

179206
#[Test]

tests/Fixture/Tool/ToolDate.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\LlmChain\Tests\Fixture\Tool;
6+
7+
use PhpLlm\LlmChain\Chain\Toolbox\Attribute\AsTool;
8+
9+
#[AsTool('tool_date', 'A tool with date parameter')]
10+
final class ToolDate
11+
{
12+
/**
13+
* @param \DateTimeImmutable $date The date
14+
*/
15+
public function __invoke(\DateTimeImmutable $date): string
16+
{
17+
return \sprintf('Weekday: %s', $date->format('l'));
18+
}
19+
}

0 commit comments

Comments
 (0)