Skip to content

Commit eb8e36a

Browse files
authored
refactor: toolbox error handling (#185)
1 parent 0930e73 commit eb8e36a

File tree

7 files changed

+120
-29
lines changed

7 files changed

+120
-29
lines changed

src/Chain/ToolBox/ParameterAnalyzer.php

+6-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace PhpLlm\LlmChain\Chain\ToolBox;
66

77
use PhpLlm\LlmChain\Chain\ToolBox\Attribute\ToolParameter;
8+
use PhpLlm\LlmChain\Exception\ToolBoxException;
89

910
/**
1011
* @phpstan-type ParameterDefinition array{
@@ -42,7 +43,11 @@ final class ParameterAnalyzer
4243
*/
4344
public function getDefinition(string $className, string $methodName): ?array
4445
{
45-
$reflection = new \ReflectionMethod($className, $methodName);
46+
try {
47+
$reflection = new \ReflectionMethod($className, $methodName);
48+
} catch (\ReflectionException) {
49+
throw ToolBoxException::invalidMethod($className, $methodName);
50+
}
4651
$parameters = $reflection->getParameters();
4752

4853
if (0 === count($parameters)) {

src/Chain/ToolBox/ToolBox.php

+17-11
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
namespace PhpLlm\LlmChain\Chain\ToolBox;
66

7-
use PhpLlm\LlmChain\Exception\ToolNotFoundException;
7+
use PhpLlm\LlmChain\Exception\ToolBoxException;
88
use PhpLlm\LlmChain\Model\Response\ToolCall;
99

1010
final class ToolBox implements ToolBoxInterface
@@ -49,22 +49,28 @@ public function execute(ToolCall $toolCall): string
4949
{
5050
foreach ($this->tools as $tool) {
5151
foreach ($this->toolAnalyzer->getMetadata($tool::class) as $metadata) {
52-
if ($metadata->name === $toolCall->name) {
53-
$result = $tool->{$metadata->method}(...$toolCall->arguments);
52+
if ($metadata->name !== $toolCall->name) {
53+
continue;
54+
}
5455

55-
if ($result instanceof \JsonSerializable || is_array($result)) {
56-
return json_encode($result, flags: JSON_THROW_ON_ERROR);
57-
}
56+
try {
57+
$result = $tool->{$metadata->method}(...$toolCall->arguments);
58+
} catch (\Throwable $e) {
59+
throw ToolBoxException::executionFailed($toolCall, $e);
60+
}
5861

59-
if (is_integer($result) || is_float($result) || $result instanceof \Stringable) {
60-
return (string) $result;
61-
}
62+
if ($result instanceof \JsonSerializable || is_array($result)) {
63+
return json_encode($result, flags: JSON_THROW_ON_ERROR);
64+
}
6265

63-
return $result;
66+
if (is_integer($result) || is_float($result) || $result instanceof \Stringable) {
67+
return (string) $result;
6468
}
69+
70+
return $result;
6571
}
6672
}
6773

68-
throw ToolNotFoundException::forToolCall($toolCall);
74+
throw ToolBoxException::notFoundForToolCall($toolCall);
6975
}
7076
}

src/Exception/ToolBoxException.php

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\LlmChain\Exception;
6+
7+
use PhpLlm\LlmChain\Model\Response\ToolCall;
8+
9+
final class ToolBoxException extends RuntimeException
10+
{
11+
public ?ToolCall $toolCall;
12+
13+
public static function notFoundForToolCall(ToolCall $toolCall): self
14+
{
15+
$exception = new self(sprintf('Tool not found for call: %s.', $toolCall->name));
16+
$exception->toolCall = $toolCall;
17+
18+
return $exception;
19+
}
20+
21+
public static function invalidMethod(string $toolClass, string $methodName): self
22+
{
23+
return new self(sprintf('Method "%s" not found in tool "%s".', $methodName, $toolClass));
24+
}
25+
26+
public static function executionFailed(ToolCall $toolCall, \Throwable $previous): self
27+
{
28+
$exception = new self(sprintf('Execution of tool "%s" failed with error: %s', $toolCall->name, $previous->getMessage()), previous: $previous);
29+
$exception->toolCall = $toolCall;
30+
31+
return $exception;
32+
}
33+
}

src/Exception/ToolNotFoundException.php

-15
This file was deleted.

tests/Chain/ToolBox/ToolBoxTest.php

+32-2
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99
use PhpLlm\LlmChain\Chain\ToolBox\ParameterAnalyzer;
1010
use PhpLlm\LlmChain\Chain\ToolBox\ToolAnalyzer;
1111
use PhpLlm\LlmChain\Chain\ToolBox\ToolBox;
12-
use PhpLlm\LlmChain\Exception\ToolNotFoundException;
12+
use PhpLlm\LlmChain\Exception\ToolBoxException;
1313
use PhpLlm\LlmChain\Model\Response\ToolCall;
14+
use PhpLlm\LlmChain\Tests\Fixture\Tool\ToolException;
15+
use PhpLlm\LlmChain\Tests\Fixture\Tool\ToolMisconfigured;
1416
use PhpLlm\LlmChain\Tests\Fixture\Tool\ToolNoParams;
1517
use PhpLlm\LlmChain\Tests\Fixture\Tool\ToolOptionalParam;
1618
use PhpLlm\LlmChain\Tests\Fixture\Tool\ToolRequiredParams;
@@ -46,6 +48,7 @@ protected function setUp(): void
4648
new ToolReturningInteger(),
4749
new ToolReturningFloat(),
4850
new ToolReturningStringable(),
51+
new ToolException(),
4952
]);
5053
}
5154

@@ -143,6 +146,13 @@ public function toolsMap(): void
143146
'description' => 'A tool returning an object which implements \Stringable',
144147
],
145148
],
149+
[
150+
'type' => 'function',
151+
'function' => [
152+
'name' => 'tool_exception',
153+
'description' => 'This tool is broken',
154+
],
155+
],
146156
];
147157

148158
self::assertSame(json_encode($expected), json_encode($actual));
@@ -151,12 +161,32 @@ public function toolsMap(): void
151161
#[Test]
152162
public function executeWithUnknownTool(): void
153163
{
154-
self::expectException(ToolNotFoundException::class);
164+
self::expectException(ToolBoxException::class);
155165
self::expectExceptionMessage('Tool not found for call: foo_bar_baz');
156166

157167
$this->toolBox->execute(new ToolCall('call_1234', 'foo_bar_baz'));
158168
}
159169

170+
#[Test]
171+
public function executeWithMisconfiguredTool(): void
172+
{
173+
self::expectException(ToolBoxException::class);
174+
self::expectExceptionMessage('Method "foo" not found in tool "PhpLlm\LlmChain\Tests\Fixture\Tool\ToolMisconfigured".');
175+
176+
$toolBox = new ToolBox(new ToolAnalyzer(), [new ToolMisconfigured()]);
177+
178+
$toolBox->execute(new ToolCall('call_1234', 'tool_misconfigured'));
179+
}
180+
181+
#[Test]
182+
public function executeWithException(): void
183+
{
184+
self::expectException(ToolBoxException::class);
185+
self::expectExceptionMessage('Execution of tool "tool_exception" failed with error: Tool error.');
186+
187+
$this->toolBox->execute(new ToolCall('call_1234', 'tool_exception'));
188+
}
189+
160190
#[Test]
161191
#[DataProvider('executeProvider')]
162192
public function execute(string $expected, string $toolName, array $toolPayload = []): void

tests/Fixture/Tool/ToolException.php

+16
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\Tests\Fixture\Tool;
6+
7+
use PhpLlm\LlmChain\Chain\ToolBox\Attribute\AsTool;
8+
9+
#[AsTool('tool_exception', description: 'This tool is broken', method: 'bar')]
10+
final class ToolException
11+
{
12+
public function bar(): string
13+
{
14+
throw new \Exception('Tool error.');
15+
}
16+
}
+16
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\Tests\Fixture\Tool;
6+
7+
use PhpLlm\LlmChain\Chain\ToolBox\Attribute\AsTool;
8+
9+
#[AsTool('tool_misconfigured', description: 'This tool is misconfigured, see method', method: 'foo')]
10+
final class ToolMisconfigured
11+
{
12+
public function bar(): string
13+
{
14+
return 'Wrong Config Attribute';
15+
}
16+
}

0 commit comments

Comments
 (0)