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

Commit b558cc9

Browse files
committed
refactor: introduce tool metadata factory interface
1 parent 953b914 commit b558cc9

File tree

6 files changed

+91
-28
lines changed

6 files changed

+91
-28
lines changed

src/Chain/ToolBox/Exception/ToolConfigurationException.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@
99

1010
final class ToolConfigurationException extends InvalidArgumentException implements ExceptionInterface
1111
{
12+
public static function noMetadataFactory(string $tool): self
13+
{
14+
return new self(sprintf('No metadata factory found for tool "%s".', $tool));
15+
}
16+
1217
public static function missingAttribute(string $className): self
1318
{
1419
return new self(sprintf('The class "%s" is not a tool, please add %s attribute.', $className, AsTool::class));
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\LlmChain\Chain\ToolBox;
6+
7+
interface MetadataFactory
8+
{
9+
public function supports(string $tool): bool;
10+
11+
/**
12+
* @return iterable<Metadata>
13+
*/
14+
public function create(string $tool): iterable;
15+
}

src/Chain/ToolBox/ToolAnalyzer.php renamed to src/Chain/ToolBox/MetadataFactory/ReflectionFactory.php

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,53 @@
22

33
declare(strict_types=1);
44

5-
namespace PhpLlm\LlmChain\Chain\ToolBox;
5+
namespace PhpLlm\LlmChain\Chain\ToolBox\MetadataFactory;
66

77
use PhpLlm\LlmChain\Chain\JsonSchema\Factory;
88
use PhpLlm\LlmChain\Chain\ToolBox\Attribute\AsTool;
99
use PhpLlm\LlmChain\Chain\ToolBox\Exception\ToolConfigurationException;
10+
use PhpLlm\LlmChain\Chain\ToolBox\Metadata;
11+
use PhpLlm\LlmChain\Chain\ToolBox\MetadataFactory;
1012

11-
final readonly class ToolAnalyzer
13+
/**
14+
* Metadata factory that uses reflection in combination with `#[AsTool]` attribute to extract metadata from tools.
15+
*/
16+
final readonly class ReflectionFactory implements MetadataFactory
1217
{
1318
public function __construct(
1419
private Factory $factory = new Factory(),
1520
) {
1621
}
1722

1823
/**
19-
* @param class-string $className
20-
*
24+
* @param class-string $tool
25+
*/
26+
public function supports(string $tool): bool
27+
{
28+
if (!class_exists($tool)) {
29+
return false;
30+
}
31+
32+
$reflectionClass = new \ReflectionClass($tool);
33+
$attributes = $reflectionClass->getAttributes(AsTool::class);
34+
35+
return 0 < count($attributes);
36+
}
37+
38+
/**
2139
* @return iterable<Metadata>
2240
*/
23-
public function getMetadata(string $className): iterable
41+
public function create(string $tool): iterable
2442
{
25-
$reflectionClass = new \ReflectionClass($className);
43+
$reflectionClass = new \ReflectionClass($tool);
2644
$attributes = $reflectionClass->getAttributes(AsTool::class);
2745

2846
if (0 === count($attributes)) {
29-
throw ToolConfigurationException::missingAttribute($className);
47+
throw ToolConfigurationException::missingAttribute($tool);
3048
}
3149

3250
foreach ($attributes as $attribute) {
33-
yield $this->convertAttribute($className, $attribute->newInstance());
51+
yield $this->convertAttribute($tool, $attribute->newInstance());
3452
}
3553
}
3654

src/Chain/ToolBox/ToolBox.php

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,21 @@
44

55
namespace PhpLlm\LlmChain\Chain\ToolBox;
66

7+
use PhpLlm\LlmChain\Chain\ToolBox\Exception\ToolConfigurationException;
78
use PhpLlm\LlmChain\Chain\ToolBox\Exception\ToolExecutionException;
89
use PhpLlm\LlmChain\Chain\ToolBox\Exception\ToolNotFoundException;
10+
use PhpLlm\LlmChain\Chain\ToolBox\MetadataFactory\ReflectionFactory;
911
use PhpLlm\LlmChain\Model\Response\ToolCall;
1012
use Psr\Log\LoggerInterface;
1113
use Psr\Log\NullLogger;
1214

1315
final class ToolBox implements ToolBoxInterface
1416
{
17+
/**
18+
* @var list<MetadataFactory>
19+
*/
20+
private readonly array $metadataFactories;
21+
1522
/**
1623
* @var list<object>
1724
*/
@@ -23,19 +30,21 @@ final class ToolBox implements ToolBoxInterface
2330
private array $map;
2431

2532
/**
26-
* @param iterable<object> $tools
33+
* @param iterable<MetadataFactory> $metadataFactories
34+
* @param iterable<object> $tools
2735
*/
2836
public function __construct(
29-
private readonly ToolAnalyzer $toolAnalyzer,
37+
iterable $metadataFactories,
3038
iterable $tools,
3139
private readonly LoggerInterface $logger = new NullLogger(),
3240
) {
41+
$this->metadataFactories = $metadataFactories instanceof \Traversable ? iterator_to_array($metadataFactories) : $metadataFactories;
3342
$this->tools = $tools instanceof \Traversable ? iterator_to_array($tools) : $tools;
3443
}
3544

3645
public static function create(object ...$tools): self
3746
{
38-
return new self(new ToolAnalyzer(), $tools);
47+
return new self([new ReflectionFactory()], $tools);
3948
}
4049

4150
public function getMap(): array
@@ -46,9 +55,7 @@ public function getMap(): array
4655

4756
$map = [];
4857
foreach ($this->tools as $tool) {
49-
foreach ($this->toolAnalyzer->getMetadata($tool::class) as $metadata) {
50-
$map[] = $metadata;
51-
}
58+
$map = array_merge($map, iterator_to_array($this->getMetadata($tool::class)));
5259
}
5360

5461
return $this->map = $map;
@@ -57,7 +64,7 @@ public function getMap(): array
5764
public function execute(ToolCall $toolCall): mixed
5865
{
5966
foreach ($this->tools as $tool) {
60-
foreach ($this->toolAnalyzer->getMetadata($tool::class) as $metadata) {
67+
foreach ($this->getMetadata($tool::class) as $metadata) {
6168
if ($metadata->name !== $toolCall->name) {
6269
continue;
6370
}
@@ -76,4 +83,22 @@ public function execute(ToolCall $toolCall): mixed
7683

7784
throw ToolNotFoundException::notFoundForToolCall($toolCall);
7885
}
86+
87+
/**
88+
* @param class-string $tool
89+
*
90+
* @return Metadata[]
91+
*/
92+
private function getMetadata(string $tool): iterable
93+
{
94+
foreach ($this->metadataFactories as $metadataFactory) {
95+
if ($metadataFactory->supports($tool)) {
96+
yield from $metadataFactory->create($tool);
97+
98+
return;
99+
}
100+
}
101+
102+
throw ToolConfigurationException::noMetadataFactory($tool);
103+
}
79104
}

tests/Chain/ToolBox/ToolAnalyzerTest.php renamed to tests/Chain/ToolBox/MetadataFactory/ReflectionFactoryTest.php

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22

33
declare(strict_types=1);
44

5-
namespace PhpLlm\LlmChain\Tests\Chain\ToolBox;
5+
namespace PhpLlm\LlmChain\Tests\Chain\ToolBox\MetadataFactory;
66

77
use PhpLlm\LlmChain\Chain\JsonSchema\DescriptionParser;
88
use PhpLlm\LlmChain\Chain\JsonSchema\Factory;
99
use PhpLlm\LlmChain\Chain\ToolBox\Attribute\AsTool;
1010
use PhpLlm\LlmChain\Chain\ToolBox\Exception\ToolConfigurationException;
1111
use PhpLlm\LlmChain\Chain\ToolBox\Metadata;
12-
use PhpLlm\LlmChain\Chain\ToolBox\ToolAnalyzer;
12+
use PhpLlm\LlmChain\Chain\ToolBox\MetadataFactory\ReflectionFactory;
1313
use PhpLlm\LlmChain\Tests\Fixture\Tool\ToolMultiple;
1414
use PhpLlm\LlmChain\Tests\Fixture\Tool\ToolRequiredParams;
1515
use PhpLlm\LlmChain\Tests\Fixture\Tool\ToolWrong;
@@ -18,33 +18,33 @@
1818
use PHPUnit\Framework\Attributes\UsesClass;
1919
use PHPUnit\Framework\TestCase;
2020

21-
#[CoversClass(ToolAnalyzer::class)]
21+
#[CoversClass(ReflectionFactory::class)]
2222
#[UsesClass(AsTool::class)]
2323
#[UsesClass(Metadata::class)]
2424
#[UsesClass(Factory::class)]
2525
#[UsesClass(DescriptionParser::class)]
2626
#[UsesClass(ToolConfigurationException::class)]
27-
final class ToolAnalyzerTest extends TestCase
27+
final class ReflectionFactoryTest extends TestCase
2828
{
29-
private ToolAnalyzer $toolAnalyzer;
29+
private ReflectionFactory $factory;
3030

3131
protected function setUp(): void
3232
{
33-
$this->toolAnalyzer = new ToolAnalyzer();
33+
$this->factory = new ReflectionFactory();
3434
}
3535

3636
#[Test]
3737
public function withoutAttribute(): void
3838
{
3939
$this->expectException(ToolConfigurationException::class);
40-
iterator_to_array($this->toolAnalyzer->getMetadata(ToolWrong::class));
40+
iterator_to_array($this->factory->create(ToolWrong::class));
4141
}
4242

4343
#[Test]
4444
public function getDefinition(): void
4545
{
4646
/** @var Metadata[] $metadatas */
47-
$metadatas = iterator_to_array($this->toolAnalyzer->getMetadata(ToolRequiredParams::class));
47+
$metadatas = iterator_to_array($this->factory->create(ToolRequiredParams::class));
4848

4949
self::assertToolConfiguration(
5050
metadata: $metadatas[0],
@@ -73,7 +73,7 @@ className: ToolRequiredParams::class,
7373
#[Test]
7474
public function getDefinitionWithMultiple(): void
7575
{
76-
$metadatas = iterator_to_array($this->toolAnalyzer->getMetadata(ToolMultiple::class));
76+
$metadatas = iterator_to_array($this->factory->create(ToolMultiple::class));
7777

7878
self::assertCount(2, $metadatas);
7979

tests/Chain/ToolBox/ToolBoxTest.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
use PhpLlm\LlmChain\Chain\ToolBox\Exception\ToolExecutionException;
1212
use PhpLlm\LlmChain\Chain\ToolBox\Exception\ToolNotFoundException;
1313
use PhpLlm\LlmChain\Chain\ToolBox\Metadata;
14-
use PhpLlm\LlmChain\Chain\ToolBox\ToolAnalyzer;
14+
use PhpLlm\LlmChain\Chain\ToolBox\MetadataFactory\ReflectionFactory;
1515
use PhpLlm\LlmChain\Chain\ToolBox\ToolBox;
1616
use PhpLlm\LlmChain\Model\Response\ToolCall;
1717
use PhpLlm\LlmChain\Tests\Fixture\Tool\ToolException;
@@ -29,7 +29,7 @@
2929
#[UsesClass(ToolCall::class)]
3030
#[UsesClass(AsTool::class)]
3131
#[UsesClass(Metadata::class)]
32-
#[UsesClass(ToolAnalyzer::class)]
32+
#[UsesClass(ReflectionFactory::class)]
3333
#[UsesClass(Factory::class)]
3434
#[UsesClass(DescriptionParser::class)]
3535
#[UsesClass(ToolConfigurationException::class)]
@@ -41,7 +41,7 @@ final class ToolBoxTest extends TestCase
4141

4242
protected function setUp(): void
4343
{
44-
$this->toolBox = new ToolBox(new ToolAnalyzer(), [
44+
$this->toolBox = new ToolBox([new ReflectionFactory()], [
4545
new ToolRequiredParams(),
4646
new ToolOptionalParam(),
4747
new ToolNoParams(),
@@ -132,7 +132,7 @@ public function executeWithMisconfiguredTool(): void
132132
self::expectException(ToolConfigurationException::class);
133133
self::expectExceptionMessage('Method "foo" not found in tool "PhpLlm\LlmChain\Tests\Fixture\Tool\ToolMisconfigured".');
134134

135-
$toolBox = new ToolBox(new ToolAnalyzer(), [new ToolMisconfigured()]);
135+
$toolBox = new ToolBox([new ReflectionFactory()], [new ToolMisconfigured()]);
136136

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

0 commit comments

Comments
 (0)