Skip to content
This repository was archived by the owner on Jul 16, 2025. It is now read-only.

refactor: introduce execution reference in tool metadata #260

Merged
merged 1 commit into from
Mar 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/Chain/ToolBox/Exception/ToolNotFoundException.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace PhpLlm\LlmChain\Chain\ToolBox\Exception;

use PhpLlm\LlmChain\Chain\ToolBox\ExecutionReference;
use PhpLlm\LlmChain\Model\Response\ToolCall;

final class ToolNotFoundException extends \RuntimeException implements ExceptionInterface
Expand All @@ -17,4 +18,9 @@ public static function notFoundForToolCall(ToolCall $toolCall): self

return $exception;
}

public static function notFoundForReference(ExecutionReference $reference): self
{
return new self(sprintf('Tool not found for reference: %s::%s.', $reference->class, $reference->method));
}
}
14 changes: 14 additions & 0 deletions src/Chain/ToolBox/ExecutionReference.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace PhpLlm\LlmChain\Chain\ToolBox;

final class ExecutionReference
{
public function __construct(
public string $class,
public string $method = '__invoke',
) {
}
Comment on lines +12 to +13
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could assert, that class and method are no empty strings here

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or use non-empty-string PHPDoc annotation

}
3 changes: 1 addition & 2 deletions src/Chain/ToolBox/Metadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,9 @@
* @param JsonSchema|null $parameters
*/
public function __construct(
public string $className,
public ExecutionReference $reference,
public string $name,
public string $description,
public string $method,
public ?array $parameters,
) {
}
Expand Down
4 changes: 2 additions & 2 deletions src/Chain/ToolBox/MetadataFactory/ReflectionFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use PhpLlm\LlmChain\Chain\JsonSchema\Factory;
use PhpLlm\LlmChain\Chain\ToolBox\Attribute\AsTool;
use PhpLlm\LlmChain\Chain\ToolBox\Exception\ToolConfigurationException;
use PhpLlm\LlmChain\Chain\ToolBox\ExecutionReference;
use PhpLlm\LlmChain\Chain\ToolBox\Metadata;
use PhpLlm\LlmChain\Chain\ToolBox\MetadataFactory;

Expand Down Expand Up @@ -45,10 +46,9 @@ private function convertAttribute(string $className, AsTool $attribute): Metadat
{
try {
return new Metadata(
$className,
new ExecutionReference($className, $attribute->method),
$attribute->name,
$attribute->description,
$attribute->method,
$this->factory->buildParameters($className, $attribute->method)
);
} catch (\ReflectionException) {
Expand Down
45 changes: 30 additions & 15 deletions src/Chain/ToolBox/ToolBox.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,24 +57,39 @@ public function getMap(): array

public function execute(ToolCall $toolCall): mixed
{
foreach ($this->tools as $tool) {
foreach ($this->metadataFactory->getMetadata($tool) as $metadata) {
if ($metadata->name !== $toolCall->name) {
continue;
}

try {
$this->logger->debug(sprintf('Executing tool "%s".', $metadata->name), $toolCall->arguments);
$result = $tool->{$metadata->method}(...$toolCall->arguments);
} catch (\Throwable $e) {
$this->logger->warning(sprintf('Failed to execute tool "%s".', $metadata->name), ['exception' => $e]);
throw ToolExecutionException::executionFailed($toolCall, $e);
}

return $result;
$metadata = $this->getMetadata($toolCall);
$tool = $this->getTool($metadata);

try {
$this->logger->debug(sprintf('Executing tool "%s".', $toolCall->name), $toolCall->arguments);
$result = $tool->{$metadata->reference->method}(...$toolCall->arguments);
} catch (\Throwable $e) {
$this->logger->warning(sprintf('Failed to execute tool "%s".', $toolCall->name), ['exception' => $e]);
throw ToolExecutionException::executionFailed($toolCall, $e);
}

return $result;
}

private function getMetadata(ToolCall $toolCall): Metadata
{
foreach ($this->getMap() as $metadata) {
if ($metadata->name === $toolCall->name) {
return $metadata;
}
}

throw ToolNotFoundException::notFoundForToolCall($toolCall);
}

private function getTool(Metadata $metadata): object
{
foreach ($this->tools as $tool) {
if ($tool instanceof $metadata->reference->class) {
return $tool;
}
}

throw ToolNotFoundException::notFoundForReference($metadata->reference);
}
}
6 changes: 3 additions & 3 deletions tests/Chain/InputProcessor/SystemPromptInputProcessorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use PhpLlm\LlmChain\Bridge\OpenAI\GPT;
use PhpLlm\LlmChain\Chain\Input;
use PhpLlm\LlmChain\Chain\InputProcessor\SystemPromptInputProcessor;
use PhpLlm\LlmChain\Chain\ToolBox\ExecutionReference;
use PhpLlm\LlmChain\Chain\ToolBox\Metadata;
use PhpLlm\LlmChain\Chain\ToolBox\ToolBoxInterface;
use PhpLlm\LlmChain\Model\Message\Content\Text;
Expand Down Expand Up @@ -106,15 +107,14 @@ public function includeToolDefinitions(): void
public function getMap(): array
{
return [
new Metadata(ToolNoParams::class, 'tool_no_params', 'A tool without parameters', '__invoke', null),
new Metadata(new ExecutionReference(ToolNoParams::class), 'tool_no_params', 'A tool without parameters', null),
new Metadata(
ToolRequiredParams::class,
new ExecutionReference(ToolRequiredParams::class, 'bar'),
'tool_required_params',
<<<DESCRIPTION
A tool with required parameters
or not
DESCRIPTION,
'bar',
null
),
];
Expand Down
9 changes: 5 additions & 4 deletions tests/Chain/ToolBox/ChainProcessorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use PhpLlm\LlmChain\Chain\Input;
use PhpLlm\LlmChain\Chain\ToolBox\ChainProcessor;
use PhpLlm\LlmChain\Chain\ToolBox\ExecutionReference;
use PhpLlm\LlmChain\Chain\ToolBox\Metadata;
use PhpLlm\LlmChain\Chain\ToolBox\ToolBoxInterface;
use PhpLlm\LlmChain\Exception\MissingModelSupport;
Expand Down Expand Up @@ -44,8 +45,8 @@ public function processInputWithoutRegisteredToolsWillResultInNoOptionChange():
public function processInputWithRegisteredToolsWillResultInOptionChange(): void
{
$toolBox = $this->createStub(ToolBoxInterface::class);
$tool1 = new Metadata('ClassTool1', 'tool1', 'description1', 'method1', null);
$tool2 = new Metadata('ClassTool2', 'tool2', 'description2', 'method2', null);
$tool1 = new Metadata(new ExecutionReference('ClassTool1', 'method1'), 'tool1', 'description1', null);
$tool2 = new Metadata(new ExecutionReference('ClassTool2', 'method1'), 'tool2', 'description2', null);
$toolBox->method('getMap')->willReturn([$tool1, $tool2]);

$llm = $this->createMock(LanguageModel::class);
Expand All @@ -63,8 +64,8 @@ public function processInputWithRegisteredToolsWillResultInOptionChange(): void
public function processInputWithRegisteredToolsButToolOverride(): void
{
$toolBox = $this->createStub(ToolBoxInterface::class);
$tool1 = new Metadata('ClassTool1', 'tool1', 'description1', 'method1', null);
$tool2 = new Metadata('ClassTool2', 'tool2', 'description2', 'method2', null);
$tool1 = new Metadata(new ExecutionReference('ClassTool1', 'method1'), 'tool1', 'description1', null);
$tool2 = new Metadata(new ExecutionReference('ClassTool2', 'method1'), 'tool2', 'description2', null);
$toolBox->method('getMap')->willReturn([$tool1, $tool2]);

$llm = $this->createMock(LanguageModel::class);
Expand Down
5 changes: 3 additions & 2 deletions tests/Chain/ToolBox/FaultTolerantToolBoxTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use PhpLlm\LlmChain\Chain\ToolBox\Exception\ToolExecutionException;
use PhpLlm\LlmChain\Chain\ToolBox\Exception\ToolNotFoundException;
use PhpLlm\LlmChain\Chain\ToolBox\ExecutionReference;
use PhpLlm\LlmChain\Chain\ToolBox\FaultTolerantToolBox;
use PhpLlm\LlmChain\Chain\ToolBox\Metadata;
use PhpLlm\LlmChain\Chain\ToolBox\ToolBoxInterface;
Expand Down Expand Up @@ -69,8 +70,8 @@ public function __construct(private readonly \Closure $exceptionFactory)
public function getMap(): array
{
return [
new Metadata(ToolNoParams::class, 'tool_no_params', 'A tool without parameters', '__invoke', null),
new Metadata(ToolRequiredParams::class, 'tool_required_params', 'A tool with required parameters', 'bar', null),
new Metadata(new ExecutionReference(ToolNoParams::class), 'tool_no_params', 'A tool without parameters', null),
new Metadata(new ExecutionReference(ToolRequiredParams::class, 'bar'), 'tool_required_params', 'A tool with required parameters', null),
];
}

Expand Down
4 changes: 2 additions & 2 deletions tests/Chain/ToolBox/MetadataFactory/ReflectionFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -145,10 +145,10 @@ className: ToolMultiple::class,

private function assertToolConfiguration(Metadata $metadata, string $className, string $name, string $description, string $method, array $parameters): void
{
self::assertSame($className, $metadata->className);
self::assertSame($className, $metadata->reference->class);
self::assertSame($method, $metadata->reference->method);
self::assertSame($name, $metadata->name);
self::assertSame($description, $metadata->description);
self::assertSame($method, $metadata->method);
self::assertSame($parameters, $metadata->parameters);
}
}