Skip to content
This repository was archived by the owner on Jul 16, 2025. It is now read-only.
Closed
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
2 changes: 1 addition & 1 deletion examples/toolbox-clock.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
$llm = new GPT(GPT::GPT_4O_MINI);

$clock = new Clock(new SymfonyClock());
$toolBox = new ToolBox(new ToolAnalyzer(), [$clock]);
$toolBox = new ToolBox(new ToolAnalyzer(), ['clock' => $clock]);
$processor = new ChainProcessor($toolBox);
$chain = new Chain($platform, $llm, [$processor], [$processor]);

Expand Down
2 changes: 0 additions & 2 deletions src/Chain/ToolBox/Tool/Clock.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@

namespace PhpLlm\LlmChain\Chain\ToolBox\Tool;

use PhpLlm\LlmChain\Chain\ToolBox\Attribute\AsTool;
use Symfony\Component\Clock\ClockInterface;

#[AsTool('clock', description: 'Provides the current date and time.')]
final readonly class Clock
{
public function __construct(
Expand Down
24 changes: 24 additions & 0 deletions src/Chain/ToolBox/Tool/ClockWithoutAttribute.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace PhpLlm\LlmChain\Chain\ToolBox\Tool;

use Symfony\Component\Clock\ClockInterface;

final readonly class ClockWithoutAttribute
Copy link
Contributor

Choose a reason for hiding this comment

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

Such a tool should be added to the tests/ folder, as we don't need to ship two clock tools, doing the exact same thing

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I removed this added example . I already added a fixture test without the attribute .
The idea was to provide an example of tool without "AsTool" .
I deleted this attribute from clock tool to show that case .

Copy link
Contributor

Choose a reason for hiding this comment

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

I think we don't need an example for this, but rather a test, so please keep the clock with the attribute and the example and extend ToolboxTestCase with a fixture file, shown, that this tool will be registered. thanks

{
public function __construct(
private ClockInterface $clock,
) {
}

public function __invoke(): string
{
return sprintf(
'Current date is %s (YYYY-MM-DD) and the time is %s (HH:MM:SS).',
$this->clock->now()->format('Y-m-d'),
$this->clock->now()->format('H:i:s'),
);
}
}
4 changes: 2 additions & 2 deletions src/Chain/ToolBox/Tool/Wikipedia.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
use PhpLlm\LlmChain\Chain\ToolBox\Attribute\AsTool;
use Symfony\Contracts\HttpClient\HttpClientInterface;

#[AsTool('wikipedia_search', description: 'Searches Wikipedia for a given query', method: 'search')]
#[AsTool('wikipedia_article', description: 'Retrieves a Wikipedia article by its title', method: 'article')]
#[AsTool(name: 'wikipedia_search', description: 'Searches Wikipedia for a given query', method: 'search')]
#[AsTool(name: 'wikipedia_article', description: 'Retrieves a Wikipedia article by its title', method: 'article')]
final readonly class Wikipedia
{
public function __construct(
Expand Down
25 changes: 22 additions & 3 deletions src/Chain/ToolBox/ToolAnalyzer.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,21 @@ public function __construct(
*
* @return iterable<Metadata>
*/
public function getMetadata(string $className): iterable
public function getMetadata(string|int $toolKey, string $className): iterable
{
$reflectionClass = new \ReflectionClass($className);
$attributes = $reflectionClass->getAttributes(AsTool::class);

if (0 === count($attributes)) {
throw InvalidToolImplementation::missingAttribute($className);
if (0 === \count($attributes)) {
if (false === is_string($toolKey)) {
throw new InvalidToolImplementation('Use AsTool attribute to configure your tools or create your toolBox like "new ToolBox([\'toolName\' => $toolInstance]")');
}

if (false === $reflectionClass->hasMethod('__invoke')) {
throw new InvalidToolImplementation('The tool must implement the __invoke() method');
}

yield $this->createToolMetaData($className, $toolKey);
}

foreach ($attributes as $attribute) {
Expand All @@ -43,4 +51,15 @@ private function convertAttribute(string $className, AsTool $attribute): Metadat
$this->parameterAnalyzer->getDefinition($className, $attribute->method)
);
}

private function createToolMetaData(string $className, string $toolKey): Metadata
{
return new Metadata(
$className,
$toolKey,
'Use "AsTool" attribute to add description',
'__invoke',
$this->parameterAnalyzer->getDefinition($className, '__invoke')
);
}
}
15 changes: 10 additions & 5 deletions src/Chain/ToolBox/ToolBox.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
final class ToolBox implements ToolBoxInterface
{
/**
* @var list<object>
* @var array<object>
*/
private readonly array $tools;

Expand All @@ -36,8 +36,9 @@ public function getMap(): array
}

$map = [];
foreach ($this->tools as $tool) {
foreach ($this->toolAnalyzer->getMetadata($tool::class) as $metadata) {

foreach ($this->tools as $key => $tool) {
foreach ($this->toolAnalyzer->getMetadata($key, $tool::class) as $metadata) {
$map[] = $metadata;
}
}
Expand All @@ -47,14 +48,18 @@ public function getMap(): array

public function execute(ToolCall $toolCall): string
{
foreach ($this->tools as $tool) {
foreach ($this->toolAnalyzer->getMetadata($tool::class) as $metadata) {
foreach ($this->tools as $key => $tool) {
foreach ($this->toolAnalyzer->getMetadata($key, $tool::class) as $metadata) {
if ($metadata->name === $toolCall->name) {
return $tool->{$metadata->method}(...$toolCall->arguments);
}
}
}

if (isset($this->tools[$toolCall->name])) {
return $this->tools[$toolCall->name]->call(...$toolCall->arguments);
}

throw new RuntimeException('Tool not found');
}
}
37 changes: 33 additions & 4 deletions tests/Chain/ToolBox/ToolAnalyzerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use PhpLlm\LlmChain\Exception\InvalidToolImplementation;
use PhpLlm\LlmChain\Tests\Fixture\Tool\ToolMultiple;
use PhpLlm\LlmChain\Tests\Fixture\Tool\ToolRequiredParams;
use PhpLlm\LlmChain\Tests\Fixture\Tool\ToolWithoutAttribute;
use PhpLlm\LlmChain\Tests\Fixture\Tool\ToolWrong;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
Expand All @@ -32,17 +33,45 @@ protected function setUp(): void
}

#[Test]
public function withoutAttribute(): void
public function toolWrong(): void
{
$this->expectException(InvalidToolImplementation::class);
iterator_to_array($this->toolAnalyzer->getMetadata(ToolWrong::class));
iterator_to_array($this->toolAnalyzer->getMetadata(0, ToolWrong::class));
}

#[Test]
public function toolWithoutAttribute(): void
{
$metadatas = iterator_to_array($this->toolAnalyzer->getMetadata('tool_without_attribute', ToolWithoutAttribute::class));

self::assertToolConfiguration(
metadata: $metadatas[0],
className: ToolWithoutAttribute::class,
name: 'tool_without_attribute',
description: 'Use "AsTool" attribute to add description',
method: '__invoke',
parameters: [
'type' => 'object',
'properties' => [
'text' => [
'type' => 'string',
'description' => 'The text given to the tool',
],
'number' => [
'type' => 'integer',
'description' => 'A number given to the tool',
],
],
'required' => ['text', 'number'],
],
);
}

#[Test]
public function getDefinition(): void
{
/** @var Metadata[] $metadatas */
$metadatas = iterator_to_array($this->toolAnalyzer->getMetadata(ToolRequiredParams::class));
$metadatas = iterator_to_array($this->toolAnalyzer->getMetadata(0, ToolRequiredParams::class));

self::assertToolConfiguration(
metadata: $metadatas[0],
Expand Down Expand Up @@ -70,7 +99,7 @@ className: ToolRequiredParams::class,
#[Test]
public function getDefinitionWithMultiple(): void
{
$metadatas = iterator_to_array($this->toolAnalyzer->getMetadata(ToolMultiple::class));
$metadatas = iterator_to_array($this->toolAnalyzer->getMetadata(0, ToolMultiple::class));

self::assertCount(2, $metadatas);

Expand Down
17 changes: 17 additions & 0 deletions tests/Fixture/Tool/ToolWithoutAttribute.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace PhpLlm\LlmChain\Tests\Fixture\Tool;

final class ToolWithoutAttribute
{
/**
* @param string $text The text given to the tool
* @param int $number A number given to the tool
*/
public function __invoke(string $text, int $number): string
{
return sprintf('%s says "%d".', $text, $number);
}
}