Skip to content

Commit 6a5dbe7

Browse files
authored
refactor: introduce tool metadata factory interface (#249)
Preparation for different tool metadata sources, e.g. config or mcp based
1 parent 30578be commit 6a5dbe7

File tree

6 files changed

+74
-27
lines changed

6 files changed

+74
-27
lines changed

src/Chain/ToolBox/Exception/ToolConfigurationException.php

+5
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 invalidReference(mixed $reference): self
13+
{
14+
return new self(sprintf('The reference "%s" is not a valid as tool.', $reference));
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));

src/Chain/ToolBox/MetadataFactory.php

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\LlmChain\Chain\ToolBox;
6+
7+
interface MetadataFactory
8+
{
9+
/**
10+
* @return iterable<Metadata>
11+
*/
12+
public function getMetadata(mixed $reference): iterable;
13+
}

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

+15-8
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,42 @@
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-
*
2124
* @return iterable<Metadata>
2225
*/
23-
public function getMetadata(string $className): iterable
26+
public function getMetadata(mixed $reference): iterable
2427
{
25-
$reflectionClass = new \ReflectionClass($className);
28+
if (!is_object($reference) && !is_string($reference) || is_string($reference) && !class_exists($reference)) {
29+
throw ToolConfigurationException::invalidReference($reference);
30+
}
31+
32+
$reflectionClass = new \ReflectionClass($reference);
2633
$attributes = $reflectionClass->getAttributes(AsTool::class);
2734

2835
if (0 === count($attributes)) {
29-
throw ToolConfigurationException::missingAttribute($className);
36+
throw ToolConfigurationException::missingAttribute($reflectionClass->getName());
3037
}
3138

3239
foreach ($attributes as $attribute) {
33-
yield $this->convertAttribute($className, $attribute->newInstance());
40+
yield $this->convertAttribute($reflectionClass->getName(), $attribute->newInstance());
3441
}
3542
}
3643

src/Chain/ToolBox/ToolBox.php

+7-6
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@
66

77
use PhpLlm\LlmChain\Chain\ToolBox\Exception\ToolExecutionException;
88
use PhpLlm\LlmChain\Chain\ToolBox\Exception\ToolNotFoundException;
9+
use PhpLlm\LlmChain\Chain\ToolBox\MetadataFactory\ReflectionFactory;
910
use PhpLlm\LlmChain\Model\Response\ToolCall;
1011
use Psr\Log\LoggerInterface;
1112
use Psr\Log\NullLogger;
1213

1314
final class ToolBox implements ToolBoxInterface
1415
{
1516
/**
16-
* @var list<object>
17+
* @var list<mixed>
1718
*/
1819
private readonly array $tools;
1920

@@ -23,10 +24,10 @@ final class ToolBox implements ToolBoxInterface
2324
private array $map;
2425

2526
/**
26-
* @param iterable<object> $tools
27+
* @param iterable<mixed> $tools
2728
*/
2829
public function __construct(
29-
private readonly ToolAnalyzer $toolAnalyzer,
30+
private readonly MetadataFactory $metadataFactory,
3031
iterable $tools,
3132
private readonly LoggerInterface $logger = new NullLogger(),
3233
) {
@@ -35,7 +36,7 @@ public function __construct(
3536

3637
public static function create(object ...$tools): self
3738
{
38-
return new self(new ToolAnalyzer(), $tools);
39+
return new self(new ReflectionFactory(), $tools);
3940
}
4041

4142
public function getMap(): array
@@ -46,7 +47,7 @@ public function getMap(): array
4647

4748
$map = [];
4849
foreach ($this->tools as $tool) {
49-
foreach ($this->toolAnalyzer->getMetadata($tool::class) as $metadata) {
50+
foreach ($this->metadataFactory->getMetadata($tool::class) as $metadata) {
5051
$map[] = $metadata;
5152
}
5253
}
@@ -57,7 +58,7 @@ public function getMap(): array
5758
public function execute(ToolCall $toolCall): mixed
5859
{
5960
foreach ($this->tools as $tool) {
60-
foreach ($this->toolAnalyzer->getMetadata($tool::class) as $metadata) {
61+
foreach ($this->metadataFactory->getMetadata($tool) as $metadata) {
6162
if ($metadata->name !== $toolCall->name) {
6263
continue;
6364
}

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

+30-9
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,54 @@
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();
34+
}
35+
36+
#[Test]
37+
public function invalidReferenceNonExistingClass(): void
38+
{
39+
$this->expectException(ToolConfigurationException::class);
40+
iterator_to_array($this->factory->getMetadata('invalid'));
41+
}
42+
43+
#[Test]
44+
public function invalidReferenceNonInteger(): void
45+
{
46+
$this->expectException(ToolConfigurationException::class);
47+
iterator_to_array($this->factory->getMetadata(1234));
48+
}
49+
50+
#[Test]
51+
public function invalidReferenceCallable(): void
52+
{
53+
$this->expectException(ToolConfigurationException::class);
54+
iterator_to_array($this->factory->getMetadata(fn () => null));
3455
}
3556

3657
#[Test]
3758
public function withoutAttribute(): void
3859
{
3960
$this->expectException(ToolConfigurationException::class);
40-
iterator_to_array($this->toolAnalyzer->getMetadata(ToolWrong::class));
61+
iterator_to_array($this->factory->getMetadata(ToolWrong::class));
4162
}
4263

4364
#[Test]
4465
public function getDefinition(): void
4566
{
4667
/** @var Metadata[] $metadatas */
47-
$metadatas = iterator_to_array($this->toolAnalyzer->getMetadata(ToolRequiredParams::class));
68+
$metadatas = iterator_to_array($this->factory->getMetadata(ToolRequiredParams::class));
4869

4970
self::assertToolConfiguration(
5071
metadata: $metadatas[0],
@@ -73,7 +94,7 @@ className: ToolRequiredParams::class,
7394
#[Test]
7495
public function getDefinitionWithMultiple(): void
7596
{
76-
$metadatas = iterator_to_array($this->toolAnalyzer->getMetadata(ToolMultiple::class));
97+
$metadatas = iterator_to_array($this->factory->getMetadata(ToolMultiple::class));
7798

7899
self::assertCount(2, $metadatas);
79100

tests/Chain/ToolBox/ToolBoxTest.php

+4-4
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)