Skip to content

Commit

Permalink
refactor: toolbox error handling (#185)
Browse files Browse the repository at this point in the history
  • Loading branch information
chr-hertel authored Jan 4, 2025
1 parent 0930e73 commit eb8e36a
Show file tree
Hide file tree
Showing 7 changed files with 120 additions and 29 deletions.
7 changes: 6 additions & 1 deletion src/Chain/ToolBox/ParameterAnalyzer.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace PhpLlm\LlmChain\Chain\ToolBox;

use PhpLlm\LlmChain\Chain\ToolBox\Attribute\ToolParameter;
use PhpLlm\LlmChain\Exception\ToolBoxException;

/**
* @phpstan-type ParameterDefinition array{
Expand Down Expand Up @@ -42,7 +43,11 @@ final class ParameterAnalyzer
*/
public function getDefinition(string $className, string $methodName): ?array
{
$reflection = new \ReflectionMethod($className, $methodName);
try {
$reflection = new \ReflectionMethod($className, $methodName);
} catch (\ReflectionException) {
throw ToolBoxException::invalidMethod($className, $methodName);
}
$parameters = $reflection->getParameters();

if (0 === count($parameters)) {
Expand Down
28 changes: 17 additions & 11 deletions src/Chain/ToolBox/ToolBox.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace PhpLlm\LlmChain\Chain\ToolBox;

use PhpLlm\LlmChain\Exception\ToolNotFoundException;
use PhpLlm\LlmChain\Exception\ToolBoxException;
use PhpLlm\LlmChain\Model\Response\ToolCall;

final class ToolBox implements ToolBoxInterface
Expand Down Expand Up @@ -49,22 +49,28 @@ public function execute(ToolCall $toolCall): string
{
foreach ($this->tools as $tool) {
foreach ($this->toolAnalyzer->getMetadata($tool::class) as $metadata) {
if ($metadata->name === $toolCall->name) {
$result = $tool->{$metadata->method}(...$toolCall->arguments);
if ($metadata->name !== $toolCall->name) {
continue;
}

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

if (is_integer($result) || is_float($result) || $result instanceof \Stringable) {
return (string) $result;
}
if ($result instanceof \JsonSerializable || is_array($result)) {
return json_encode($result, flags: JSON_THROW_ON_ERROR);
}

return $result;
if (is_integer($result) || is_float($result) || $result instanceof \Stringable) {
return (string) $result;
}

return $result;
}
}

throw ToolNotFoundException::forToolCall($toolCall);
throw ToolBoxException::notFoundForToolCall($toolCall);
}
}
33 changes: 33 additions & 0 deletions src/Exception/ToolBoxException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace PhpLlm\LlmChain\Exception;

use PhpLlm\LlmChain\Model\Response\ToolCall;

final class ToolBoxException extends RuntimeException
{
public ?ToolCall $toolCall;

public static function notFoundForToolCall(ToolCall $toolCall): self
{
$exception = new self(sprintf('Tool not found for call: %s.', $toolCall->name));
$exception->toolCall = $toolCall;

return $exception;
}

public static function invalidMethod(string $toolClass, string $methodName): self
{
return new self(sprintf('Method "%s" not found in tool "%s".', $methodName, $toolClass));
}

public static function executionFailed(ToolCall $toolCall, \Throwable $previous): self
{
$exception = new self(sprintf('Execution of tool "%s" failed with error: %s', $toolCall->name, $previous->getMessage()), previous: $previous);
$exception->toolCall = $toolCall;

return $exception;
}
}
15 changes: 0 additions & 15 deletions src/Exception/ToolNotFoundException.php

This file was deleted.

34 changes: 32 additions & 2 deletions tests/Chain/ToolBox/ToolBoxTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
use PhpLlm\LlmChain\Chain\ToolBox\ParameterAnalyzer;
use PhpLlm\LlmChain\Chain\ToolBox\ToolAnalyzer;
use PhpLlm\LlmChain\Chain\ToolBox\ToolBox;
use PhpLlm\LlmChain\Exception\ToolNotFoundException;
use PhpLlm\LlmChain\Exception\ToolBoxException;
use PhpLlm\LlmChain\Model\Response\ToolCall;
use PhpLlm\LlmChain\Tests\Fixture\Tool\ToolException;
use PhpLlm\LlmChain\Tests\Fixture\Tool\ToolMisconfigured;
use PhpLlm\LlmChain\Tests\Fixture\Tool\ToolNoParams;
use PhpLlm\LlmChain\Tests\Fixture\Tool\ToolOptionalParam;
use PhpLlm\LlmChain\Tests\Fixture\Tool\ToolRequiredParams;
Expand Down Expand Up @@ -46,6 +48,7 @@ protected function setUp(): void
new ToolReturningInteger(),
new ToolReturningFloat(),
new ToolReturningStringable(),
new ToolException(),
]);
}

Expand Down Expand Up @@ -143,6 +146,13 @@ public function toolsMap(): void
'description' => 'A tool returning an object which implements \Stringable',
],
],
[
'type' => 'function',
'function' => [
'name' => 'tool_exception',
'description' => 'This tool is broken',
],
],
];

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

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

#[Test]
public function executeWithMisconfiguredTool(): void
{
self::expectException(ToolBoxException::class);
self::expectExceptionMessage('Method "foo" not found in tool "PhpLlm\LlmChain\Tests\Fixture\Tool\ToolMisconfigured".');

$toolBox = new ToolBox(new ToolAnalyzer(), [new ToolMisconfigured()]);

$toolBox->execute(new ToolCall('call_1234', 'tool_misconfigured'));
}

#[Test]
public function executeWithException(): void
{
self::expectException(ToolBoxException::class);
self::expectExceptionMessage('Execution of tool "tool_exception" failed with error: Tool error.');

$this->toolBox->execute(new ToolCall('call_1234', 'tool_exception'));
}

#[Test]
#[DataProvider('executeProvider')]
public function execute(string $expected, string $toolName, array $toolPayload = []): void
Expand Down
16 changes: 16 additions & 0 deletions tests/Fixture/Tool/ToolException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace PhpLlm\LlmChain\Tests\Fixture\Tool;

use PhpLlm\LlmChain\Chain\ToolBox\Attribute\AsTool;

#[AsTool('tool_exception', description: 'This tool is broken', method: 'bar')]
final class ToolException
{
public function bar(): string
{
throw new \Exception('Tool error.');
}
}
16 changes: 16 additions & 0 deletions tests/Fixture/Tool/ToolMisconfigured.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace PhpLlm\LlmChain\Tests\Fixture\Tool;

use PhpLlm\LlmChain\Chain\ToolBox\Attribute\AsTool;

#[AsTool('tool_misconfigured', description: 'This tool is misconfigured, see method', method: 'foo')]
final class ToolMisconfigured
{
public function bar(): string
{
return 'Wrong Config Attribute';
}
}

0 comments on commit eb8e36a

Please sign in to comment.