Skip to content

Commit eedc39e

Browse files
authored
Fix text extraction for responses with reasoning content (#33)
1 parent 61a3e92 commit eedc39e

File tree

9 files changed

+84
-29
lines changed

9 files changed

+84
-29
lines changed

src/Schemas/Anthropic/Maps/MessageMap.php

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
namespace Prism\Bedrock\Schemas\Anthropic\Maps;
66

77
use BackedEnum;
8-
use Exception;
98
use Prism\Prism\Contracts\Message;
109
use Prism\Prism\Exceptions\PrismException;
1110
use Prism\Prism\ValueObjects\Media\Image;
@@ -55,7 +54,7 @@ protected static function mapMessage(Message $message): array
5554
UserMessage::class => self::mapUserMessage($message),
5655
AssistantMessage::class => self::mapAssistantMessage($message),
5756
ToolResultMessage::class => self::mapToolResultMessage($message),
58-
default => throw new Exception('Could not map message type '.$message::class),
57+
default => throw new PrismException('Anthropic: Could not map message type '.$message::class),
5958
};
6059
}
6160

@@ -101,7 +100,7 @@ protected static function mapUserMessage(UserMessage $message): array
101100
$cache_control = $cacheType ? ['type' => $cacheType instanceof BackedEnum ? $cacheType->value : $cacheType] : null;
102101

103102
if ($message->documents() !== []) {
104-
throw new Exception('Documents are not yet supported by Anthropic on Bedrock.');
103+
throw new PrismException('Anthropic: Documents are not yet supported by Anthropic on Bedrock.');
105104
}
106105

107106
return [
@@ -129,7 +128,7 @@ protected static function mapAssistantMessage(AssistantMessage $message): array
129128
$content = [];
130129

131130
if (isset($message->additionalContent['messagePartsWithCitations'])) {
132-
throw new Exception('Citations are not yet supported by Anthropic on Bedrock.');
131+
throw new PrismException('Anthropic: Citations are not yet supported by Anthropic on Bedrock.');
133132
// TODO: update once citation support is supported by Anthropic on Bedrock
134133
// foreach ($message->additionalContent['messagePartsWithCitations'] as $part) {
135134
// $content[] = array_filter([
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
namespace Prism\Bedrock\Schemas\Converse\Concerns;
4+
5+
trait ExtractsText
6+
{
7+
/**
8+
* @param array<string, mixed> $data
9+
*/
10+
protected function extractText(array $data): string
11+
{
12+
$content = data_get($data, 'output.message.content', []);
13+
14+
foreach ($content as $item) {
15+
if ($text = data_get($item, 'text')) {
16+
return $text;
17+
}
18+
}
19+
20+
return '';
21+
}
22+
}

src/Schemas/Converse/ConverseTextHandler.php

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Illuminate\Http\Client\Response;
66
use Illuminate\Support\Collection;
77
use Prism\Bedrock\Contracts\BedrockTextHandler;
8+
use Prism\Bedrock\Schemas\Converse\Concerns\ExtractsText;
89
use Prism\Bedrock\Schemas\Converse\Concerns\ExtractsToolCalls;
910
use Prism\Bedrock\Schemas\Converse\Maps\FinishReasonMap;
1011
use Prism\Bedrock\Schemas\Converse\Maps\MessageMap;
@@ -26,7 +27,7 @@
2627

2728
class ConverseTextHandler extends BedrockTextHandler
2829
{
29-
use CallsTools, ExtractsToolCalls;
30+
use CallsTools, ExtractsText, ExtractsToolCalls;
3031

3132
protected TextResponse $tempResponse;
3233

@@ -59,7 +60,7 @@ public function handle(Request $request): TextResponse
5960
return match ($this->tempResponse->finishReason) {
6061
FinishReason::ToolCalls => $this->handleToolCalls($request),
6162
FinishReason::Stop, FinishReason::Length => $this->handleStop($request),
62-
default => throw new PrismException('Anthropic: unknown finish reason'),
63+
default => throw new PrismException('Converse: unknown finish reason'),
6364
};
6465
}
6566

@@ -109,16 +110,16 @@ protected function prepareTempResponse(): void
109110

110111
$this->tempResponse = new TextResponse(
111112
steps: new Collection,
112-
text: data_get($data, 'output.message.content.0.text', ''),
113+
text: $this->extractText($data),
113114
finishReason: FinishReasonMap::map(data_get($data, 'stopReason')),
114115
toolCalls: $this->extractToolCalls($data),
115116
toolResults: [],
116117
usage: new Usage(
117118
promptTokens: data_get($data, 'usage.inputTokens'),
118119
completionTokens: data_get($data, 'usage.outputTokens')
119120
),
120-
meta: new Meta(id: '', model: ''), // Not provided in Converse response.
121-
messages: new Collection
121+
meta: new Meta(id: '', model: ''),
122+
messages: new Collection, // Not provided in Converse response.
122123
);
123124
}
124125

src/Schemas/Converse/Maps/MessageMap.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
namespace Prism\Bedrock\Schemas\Converse\Maps;
66

7-
use Exception;
87
use Prism\Prism\Contracts\Message;
98
use Prism\Prism\Exceptions\PrismException;
109
use Prism\Prism\ValueObjects\Media\Document;
@@ -65,7 +64,7 @@ protected static function mapMessage(Message $message): array
6564
AssistantMessage::class => self::mapAssistantMessage($message),
6665
ToolResultMessage::class => self::mapToolResultMessage($message),
6766
SystemMessage::class => self::mapSystemMessage($message),
68-
default => throw new Exception('Could not map message type '.$message::class),
67+
default => throw new PrismException('Converse: Could not map message type '.$message::class),
6968
};
7069
}
7170

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"metrics":{"latencyMs":8274},"output":{"message":{"content":[{"reasoningContent":{"reasoningText":{"text":"We need to respond. The user asks \"Tell me a short story about a brave knight.\" Ask for a short story. Need to follow formatting \"role... content?\". The guideli\n\nThus we produce:\n\nrole: assistant\ncontent: |-\n ... story.\n\nWill ensure it's a short story about a brave knight. Should be creative.\n\nWe can produce a short story of maybe few paragraphs. Likely 3-5 paragraphs.\n\nLet's do.\n\n"}}},{"text":"role: assistant\ncontent: |-\n In the mist‑shrouded kingdom of Eldoria, tales of dragons and dark sorcery were as common as the sunrise. Yet none captured the hearts of the people quite li\n\n Alden had not earned his reputation through grand tournaments or lavish banquets. He grew up as the youngest son of a humble blacksmith, learning the clang o\n\n Without hesitation, Alden strapped his father's modest sword to his belt, took his battered shield—etched with a single oak leaf—and set out into the dying w\n\n At the heart of the forest, beneath a gnarled oak, Alden found the source of the curse: a crystal orb pulsing with black mist. As he approached, a specter of\n\n Light burst forth, bathing the forest in golden hues. Trees unfurled fresh leaves, and the sickly fog dissolved like a dream at dawn. The kingdom rejoiced, a\n\n Sir Alden's legend endured not because he wielded a mighty sword, but because his bravery was rooted in humility and love for his people. And when the wind r"}],"role":"assistant"}},"stopReason":"end_turn","usage":{"inputTokens":21,"outputTokens":765,"totalTokens":786}}

tests/Schemas/Anthropic/AnthropicStructuredHandlerTest.php

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,11 @@
135135
->usingTemperature(0)
136136
->asStructured();
137137

138-
Http::assertSent(fn (Request $request): \Pest\Mixins\Expectation|\Pest\Expectation => expect($request->data())->toMatchArray([
139-
'temperature' => 0,
140-
]));
138+
Http::assertSent(function (Request $request): bool {
139+
expect($request->data())->toMatchArray([
140+
'temperature' => 0,
141+
]);
142+
143+
return true;
144+
});
141145
});

tests/Schemas/Anthropic/AnthropicTextHandlerTest.php

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,11 @@
203203
->usingTemperature(0)
204204
->asText();
205205

206-
Http::assertSent(fn (Request $request): \Pest\Mixins\Expectation|\Pest\Expectation => expect($request->data())->toMatchArray([
207-
'temperature' => 0,
208-
]));
206+
Http::assertSent(function (Request $request): bool {
207+
expect($request->data())->toMatchArray([
208+
'temperature' => 0,
209+
]);
210+
211+
return true;
212+
});
209213
});

tests/Schemas/Converse/ConverseStructuredHandlerTest.php

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -193,10 +193,16 @@
193193
->usingTemperature(0)
194194
->asStructured();
195195

196-
Http::assertSent(fn (Request $request): \Pest\Mixins\Expectation|\Pest\Expectation => expect($request->data())->toMatchArray([
197-
'inferenceConfig' => [
198-
'maxTokens' => 2048,
199-
'temperature' => 0,
200-
],
201-
])->not()->toHaveKey('guardRailConfig'));
196+
Http::assertSent(function (Request $request): bool {
197+
expect($request->data())->toMatchArray([
198+
'inferenceConfig' => [
199+
'maxTokens' => 2048,
200+
'temperature' => 0,
201+
],
202+
])
203+
->not()
204+
->toHaveKey('guardRailConfig');
205+
206+
return true;
207+
});
202208
});

tests/Schemas/Converse/ConverseTextHandlerTest.php

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,21 @@
3333
expect($response->text)->toBe("I'm an AI system created by a team of inventors at Amazon. My purpose is to assist and provide information to the best of my ability. If you have any questions or need assistance, feel free to ask!");
3434
});
3535

36+
it('can generate text with reasoning content', function (): void {
37+
FixtureResponse::fakeResponseSequence('converse', 'converse/generate-text-with-reasoning-content');
38+
39+
$response = Prism::text()
40+
->using('bedrock', 'openai.gpt-oss-120b-1:0')
41+
->withPrompt('Tell me a short story about a brave knight.')
42+
->asText();
43+
44+
expect($response->usage->promptTokens)
45+
->toBe(21)
46+
->and($response->usage->completionTokens)->toBe(765)
47+
->and($response->text)->toContain('In the mist‑shrouded kingdom of Eldoria')
48+
->and($response->text)->toContain('Sir Alden\'s legend endured');
49+
});
50+
3651
it('can generate text with a system prompt', function (): void {
3752
FixtureResponse::fakeResponseSequence('converse', 'converse/generate-text-with-system-prompt');
3853

@@ -283,10 +298,14 @@
283298
->usingTemperature(0)
284299
->asText();
285300

286-
Http::assertSent(fn (Request $request): \Pest\Mixins\Expectation|\Pest\Expectation => expect($request->data())->toMatchArray([
287-
'inferenceConfig' => [
288-
'temperature' => 0,
289-
'maxTokens' => 2048,
290-
],
291-
]));
301+
Http::assertSent(function (Request $request): bool {
302+
expect($request->data())->toMatchArray([
303+
'inferenceConfig' => [
304+
'temperature' => 0,
305+
'maxTokens' => 2048,
306+
],
307+
]);
308+
309+
return true;
310+
});
292311
});

0 commit comments

Comments
 (0)