Skip to content

Commit 6d0b9f5

Browse files
Parameterless tools improvements (#42)
Co-authored-by: Jay Fletcher <j.fletcher@seclock.com>
1 parent 8bb2063 commit 6d0b9f5

File tree

11 files changed

+482
-5
lines changed

11 files changed

+482
-5
lines changed

src/Schemas/Anthropic/Concerns/ExtractsToolCalls.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ protected function extractToolCalls(array $data): array
1414
{
1515
$toolCalls = array_map(function ($content): ?ToolCall {
1616
if (data_get($content, 'type') === 'tool_use') {
17+
$input = data_get($content, 'input');
18+
1719
return new ToolCall(
1820
id: data_get($content, 'id'),
1921
name: data_get($content, 'name'),
20-
arguments: data_get($content, 'input')
22+
arguments: is_string($input) ? (json_decode($input, true) ?? []) : ($input ?? [])
2123
);
2224
}
2325

src/Schemas/Anthropic/Maps/MessageMap.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ protected static function mapAssistantMessage(AssistantMessage $message): array
150150
'type' => 'tool_use',
151151
'id' => $toolCall->id,
152152
'name' => $toolCall->name,
153-
'input' => $toolCall->arguments(),
153+
'input' => $toolCall->arguments() === [] ? new \stdClass : $toolCall->arguments(),
154154
], $message->toolCalls)
155155
: [];
156156

src/Schemas/Anthropic/Maps/ToolMap.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public static function map(array $tools): array
2323
'description' => $tool->description(),
2424
'input_schema' => [
2525
'type' => 'object',
26-
'properties' => $tool->parametersAsArray(),
26+
'properties' => $tool->parametersAsArray() ?: (object) [],
2727
'required' => $tool->requiredParameters(),
2828
],
2929
'cache_control' => $cacheType

src/Schemas/Converse/Concerns/ExtractsToolCalls.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@ protected function extractToolCalls(array $data): array
1818
return;
1919
}
2020

21+
$input = data_get($use, 'input');
22+
2123
return new ToolCall(
2224
id: data_get($use, 'toolUseId'),
2325
name: data_get($use, 'name'),
24-
arguments: data_get($use, 'input')
26+
arguments: is_string($input) ? (json_decode($input, true) ?? []) : ($input ?? [])
2527
);
2628

2729
}, data_get($data, 'output.message.content', []));

src/Schemas/Converse/Maps/MessageMap.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ protected static function mapToolCalls(array $parts): array
142142
'toolUse' => [
143143
'toolUseId' => $toolCall->id,
144144
'name' => $toolCall->name,
145-
'input' => $toolCall->arguments(),
145+
'input' => $toolCall->arguments() === [] ? new \stdClass : $toolCall->arguments(),
146146
],
147147
], $parts);
148148
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Schemas\Anthropic\Concerns;
6+
7+
use Prism\Bedrock\Schemas\Anthropic\Concerns\ExtractsToolCalls;
8+
9+
it('extracts tool calls with array input', function (): void {
10+
$extractor = new class
11+
{
12+
use ExtractsToolCalls;
13+
14+
public function extract(array $data): array
15+
{
16+
return $this->extractToolCalls($data);
17+
}
18+
};
19+
20+
$data = [
21+
'content' => [
22+
[
23+
'type' => 'tool_use',
24+
'id' => 'tool_123',
25+
'name' => 'search',
26+
'input' => [
27+
'query' => 'Laravel docs',
28+
],
29+
],
30+
],
31+
];
32+
33+
$result = $extractor->extract($data);
34+
35+
expect($result)->toHaveCount(1);
36+
expect($result[0]->id)->toBe('tool_123');
37+
expect($result[0]->name)->toBe('search');
38+
expect($result[0]->arguments())->toBe(['query' => 'Laravel docs']);
39+
});
40+
41+
it('extracts tool calls with string JSON input', function (): void {
42+
$extractor = new class
43+
{
44+
use ExtractsToolCalls;
45+
46+
public function extract(array $data): array
47+
{
48+
return $this->extractToolCalls($data);
49+
}
50+
};
51+
52+
$data = [
53+
'content' => [
54+
[
55+
'type' => 'tool_use',
56+
'id' => 'tool_456',
57+
'name' => 'weather',
58+
'input' => '{"city": "Detroit"}',
59+
],
60+
],
61+
];
62+
63+
$result = $extractor->extract($data);
64+
65+
expect($result)->toHaveCount(1);
66+
expect($result[0]->id)->toBe('tool_456');
67+
expect($result[0]->name)->toBe('weather');
68+
expect($result[0]->arguments())->toBe(['city' => 'Detroit']);
69+
});
70+
71+
it('extracts tool calls with invalid JSON string input defaults to empty array', function (): void {
72+
$extractor = new class
73+
{
74+
use ExtractsToolCalls;
75+
76+
public function extract(array $data): array
77+
{
78+
return $this->extractToolCalls($data);
79+
}
80+
};
81+
82+
$data = [
83+
'content' => [
84+
[
85+
'type' => 'tool_use',
86+
'id' => 'tool_789',
87+
'name' => 'get_time',
88+
'input' => 'invalid json',
89+
],
90+
],
91+
];
92+
93+
$result = $extractor->extract($data);
94+
95+
expect($result)->toHaveCount(1);
96+
expect($result[0]->id)->toBe('tool_789');
97+
expect($result[0]->name)->toBe('get_time');
98+
expect($result[0]->arguments())->toBe([]);
99+
});
100+
101+
it('extracts tool calls with null input defaults to empty array', function (): void {
102+
$extractor = new class
103+
{
104+
use ExtractsToolCalls;
105+
106+
public function extract(array $data): array
107+
{
108+
return $this->extractToolCalls($data);
109+
}
110+
};
111+
112+
$data = [
113+
'content' => [
114+
[
115+
'type' => 'tool_use',
116+
'id' => 'tool_abc',
117+
'name' => 'parameterless_tool',
118+
'input' => null,
119+
],
120+
],
121+
];
122+
123+
$result = $extractor->extract($data);
124+
125+
expect($result)->toHaveCount(1);
126+
expect($result[0]->id)->toBe('tool_abc');
127+
expect($result[0]->name)->toBe('parameterless_tool');
128+
expect($result[0]->arguments())->toBe([]);
129+
});
130+
131+
it('extracts tool calls with empty array input', function (): void {
132+
$extractor = new class
133+
{
134+
use ExtractsToolCalls;
135+
136+
public function extract(array $data): array
137+
{
138+
return $this->extractToolCalls($data);
139+
}
140+
};
141+
142+
$data = [
143+
'content' => [
144+
[
145+
'type' => 'tool_use',
146+
'id' => 'tool_def',
147+
'name' => 'no_params',
148+
'input' => [],
149+
],
150+
],
151+
];
152+
153+
$result = $extractor->extract($data);
154+
155+
expect($result)->toHaveCount(1);
156+
expect($result[0]->id)->toBe('tool_def');
157+
expect($result[0]->name)->toBe('no_params');
158+
expect($result[0]->arguments())->toBe([]);
159+
});

tests/Schemas/Anthropic/Maps/MessageMapTest.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,34 @@
114114
]);
115115
});
116116

117+
it('maps assistant message with tool calls with empty arguments as stdClass', function (): void {
118+
expect(MessageMap::map([
119+
new AssistantMessage('Running tool', [
120+
new ToolCall(
121+
'tool_5678',
122+
'get_time',
123+
[]
124+
),
125+
]),
126+
]))->toEqual([
127+
[
128+
'role' => 'assistant',
129+
'content' => [
130+
[
131+
'type' => 'text',
132+
'text' => 'Running tool',
133+
],
134+
[
135+
'type' => 'tool_use',
136+
'id' => 'tool_5678',
137+
'name' => 'get_time',
138+
'input' => new \stdClass,
139+
],
140+
],
141+
],
142+
]);
143+
});
144+
117145
it('maps tool result messages', function (): void {
118146
expect(MessageMap::map([
119147
new ToolResultMessage([

tests/Schemas/Anthropic/Maps/ToolMapTest.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,23 @@
3131
]]);
3232
});
3333

34+
it('maps parameterless tools with empty object properties', function (): void {
35+
$tool = (new Tool)
36+
->as('get_time')
37+
->for('Get the current time')
38+
->using(fn (): string => '12:00 PM');
39+
40+
expect(ToolMap::map([$tool]))->toEqual([[
41+
'name' => 'get_time',
42+
'description' => 'Get the current time',
43+
'input_schema' => [
44+
'type' => 'object',
45+
'properties' => (object) [],
46+
'required' => [],
47+
],
48+
]]);
49+
});
50+
3451
it('sets the cache typeif cacheType providerOptions is set on tool', function (mixed $cacheType): void {
3552
$tool = (new Tool)
3653
->as('search')

0 commit comments

Comments
 (0)