Skip to content
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
4 changes: 3 additions & 1 deletion src/Schemas/Anthropic/Concerns/ExtractsToolCalls.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ protected function extractToolCalls(array $data): array
{
$toolCalls = array_map(function ($content): ?ToolCall {
if (data_get($content, 'type') === 'tool_use') {
$input = data_get($content, 'input');

return new ToolCall(
id: data_get($content, 'id'),
name: data_get($content, 'name'),
arguments: data_get($content, 'input')
arguments: is_string($input) ? (json_decode($input, true) ?? []) : ($input ?? [])
);
}

Expand Down
2 changes: 1 addition & 1 deletion src/Schemas/Anthropic/Maps/MessageMap.php
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ protected static function mapAssistantMessage(AssistantMessage $message): array
'type' => 'tool_use',
'id' => $toolCall->id,
'name' => $toolCall->name,
'input' => $toolCall->arguments(),
'input' => $toolCall->arguments() === [] ? new \stdClass : $toolCall->arguments(),
], $message->toolCalls)
: [];

Expand Down
2 changes: 1 addition & 1 deletion src/Schemas/Anthropic/Maps/ToolMap.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public static function map(array $tools): array
'description' => $tool->description(),
'input_schema' => [
'type' => 'object',
'properties' => $tool->parametersAsArray(),
'properties' => $tool->parametersAsArray() ?: (object) [],
'required' => $tool->requiredParameters(),
],
'cache_control' => $cacheType
Expand Down
4 changes: 3 additions & 1 deletion src/Schemas/Converse/Concerns/ExtractsToolCalls.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ protected function extractToolCalls(array $data): array
return;
}

$input = data_get($use, 'input');

return new ToolCall(
id: data_get($use, 'toolUseId'),
name: data_get($use, 'name'),
arguments: data_get($use, 'input')
arguments: is_string($input) ? (json_decode($input, true) ?? []) : ($input ?? [])
);

}, data_get($data, 'output.message.content', []));
Expand Down
2 changes: 1 addition & 1 deletion src/Schemas/Converse/Maps/MessageMap.php
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ protected static function mapToolCalls(array $parts): array
'toolUse' => [
'toolUseId' => $toolCall->id,
'name' => $toolCall->name,
'input' => $toolCall->arguments(),
'input' => $toolCall->arguments() === [] ? new \stdClass : $toolCall->arguments(),
],
], $parts);
}
Expand Down
159 changes: 159 additions & 0 deletions tests/Schemas/Anthropic/Concerns/ExtractsToolCallsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
<?php

declare(strict_types=1);

namespace Tests\Schemas\Anthropic\Concerns;

use Prism\Bedrock\Schemas\Anthropic\Concerns\ExtractsToolCalls;

it('extracts tool calls with array input', function (): void {
$extractor = new class
{
use ExtractsToolCalls;

public function extract(array $data): array
{
return $this->extractToolCalls($data);
}
};

$data = [
'content' => [
[
'type' => 'tool_use',
'id' => 'tool_123',
'name' => 'search',
'input' => [
'query' => 'Laravel docs',
],
],
],
];

$result = $extractor->extract($data);

expect($result)->toHaveCount(1);
expect($result[0]->id)->toBe('tool_123');
expect($result[0]->name)->toBe('search');
expect($result[0]->arguments())->toBe(['query' => 'Laravel docs']);
});

it('extracts tool calls with string JSON input', function (): void {
$extractor = new class
{
use ExtractsToolCalls;

public function extract(array $data): array
{
return $this->extractToolCalls($data);
}
};

$data = [
'content' => [
[
'type' => 'tool_use',
'id' => 'tool_456',
'name' => 'weather',
'input' => '{"city": "Detroit"}',
],
],
];

$result = $extractor->extract($data);

expect($result)->toHaveCount(1);
expect($result[0]->id)->toBe('tool_456');
expect($result[0]->name)->toBe('weather');
expect($result[0]->arguments())->toBe(['city' => 'Detroit']);
});

it('extracts tool calls with invalid JSON string input defaults to empty array', function (): void {
$extractor = new class
{
use ExtractsToolCalls;

public function extract(array $data): array
{
return $this->extractToolCalls($data);
}
};

$data = [
'content' => [
[
'type' => 'tool_use',
'id' => 'tool_789',
'name' => 'get_time',
'input' => 'invalid json',
],
],
];

$result = $extractor->extract($data);

expect($result)->toHaveCount(1);
expect($result[0]->id)->toBe('tool_789');
expect($result[0]->name)->toBe('get_time');
expect($result[0]->arguments())->toBe([]);
});

it('extracts tool calls with null input defaults to empty array', function (): void {
$extractor = new class
{
use ExtractsToolCalls;

public function extract(array $data): array
{
return $this->extractToolCalls($data);
}
};

$data = [
'content' => [
[
'type' => 'tool_use',
'id' => 'tool_abc',
'name' => 'parameterless_tool',
'input' => null,
],
],
];

$result = $extractor->extract($data);

expect($result)->toHaveCount(1);
expect($result[0]->id)->toBe('tool_abc');
expect($result[0]->name)->toBe('parameterless_tool');
expect($result[0]->arguments())->toBe([]);
});

it('extracts tool calls with empty array input', function (): void {
$extractor = new class
{
use ExtractsToolCalls;

public function extract(array $data): array
{
return $this->extractToolCalls($data);
}
};

$data = [
'content' => [
[
'type' => 'tool_use',
'id' => 'tool_def',
'name' => 'no_params',
'input' => [],
],
],
];

$result = $extractor->extract($data);

expect($result)->toHaveCount(1);
expect($result[0]->id)->toBe('tool_def');
expect($result[0]->name)->toBe('no_params');
expect($result[0]->arguments())->toBe([]);
});
28 changes: 28 additions & 0 deletions tests/Schemas/Anthropic/Maps/MessageMapTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,34 @@
]);
});

it('maps assistant message with tool calls with empty arguments as stdClass', function (): void {
expect(MessageMap::map([
new AssistantMessage('Running tool', [
new ToolCall(
'tool_5678',
'get_time',
[]
),
]),
]))->toEqual([
[
'role' => 'assistant',
'content' => [
[
'type' => 'text',
'text' => 'Running tool',
],
[
'type' => 'tool_use',
'id' => 'tool_5678',
'name' => 'get_time',
'input' => new \stdClass,
],
],
],
]);
});

it('maps tool result messages', function (): void {
expect(MessageMap::map([
new ToolResultMessage([
Expand Down
17 changes: 17 additions & 0 deletions tests/Schemas/Anthropic/Maps/ToolMapTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,23 @@
]]);
});

it('maps parameterless tools with empty object properties', function (): void {
$tool = (new Tool)
->as('get_time')
->for('Get the current time')
->using(fn (): string => '12:00 PM');

expect(ToolMap::map([$tool]))->toEqual([[
'name' => 'get_time',
'description' => 'Get the current time',
'input_schema' => [
'type' => 'object',
'properties' => (object) [],
'required' => [],
],
]]);
});

it('sets the cache typeif cacheType providerOptions is set on tool', function (mixed $cacheType): void {
$tool = (new Tool)
->as('search')
Expand Down
Loading