diff --git a/src/Schemas/Anthropic/Maps/MessageMap.php b/src/Schemas/Anthropic/Maps/MessageMap.php index 4f2451b..c8cc9f0 100644 --- a/src/Schemas/Anthropic/Maps/MessageMap.php +++ b/src/Schemas/Anthropic/Maps/MessageMap.php @@ -5,7 +5,6 @@ namespace Prism\Bedrock\Schemas\Anthropic\Maps; use BackedEnum; -use Exception; use Prism\Prism\Contracts\Message; use Prism\Prism\Exceptions\PrismException; use Prism\Prism\ValueObjects\Media\Image; @@ -55,7 +54,7 @@ protected static function mapMessage(Message $message): array UserMessage::class => self::mapUserMessage($message), AssistantMessage::class => self::mapAssistantMessage($message), ToolResultMessage::class => self::mapToolResultMessage($message), - default => throw new Exception('Could not map message type '.$message::class), + default => throw new PrismException('Anthropic: Could not map message type '.$message::class), }; } @@ -101,7 +100,7 @@ protected static function mapUserMessage(UserMessage $message): array $cache_control = $cacheType ? ['type' => $cacheType instanceof BackedEnum ? $cacheType->value : $cacheType] : null; if ($message->documents() !== []) { - throw new Exception('Documents are not yet supported by Anthropic on Bedrock.'); + throw new PrismException('Anthropic: Documents are not yet supported by Anthropic on Bedrock.'); } return [ @@ -129,7 +128,7 @@ protected static function mapAssistantMessage(AssistantMessage $message): array $content = []; if (isset($message->additionalContent['messagePartsWithCitations'])) { - throw new Exception('Citations are not yet supported by Anthropic on Bedrock.'); + throw new PrismException('Anthropic: Citations are not yet supported by Anthropic on Bedrock.'); // TODO: update once citation support is supported by Anthropic on Bedrock // foreach ($message->additionalContent['messagePartsWithCitations'] as $part) { // $content[] = array_filter([ diff --git a/src/Schemas/Converse/Concerns/ExtractsText.php b/src/Schemas/Converse/Concerns/ExtractsText.php new file mode 100644 index 0000000..28e344f --- /dev/null +++ b/src/Schemas/Converse/Concerns/ExtractsText.php @@ -0,0 +1,22 @@ + $data + */ + protected function extractText(array $data): string + { + $content = data_get($data, 'output.message.content', []); + + foreach ($content as $item) { + if ($text = data_get($item, 'text')) { + return $text; + } + } + + return ''; + } +} diff --git a/src/Schemas/Converse/ConverseTextHandler.php b/src/Schemas/Converse/ConverseTextHandler.php index eb18aca..2ba6e5e 100644 --- a/src/Schemas/Converse/ConverseTextHandler.php +++ b/src/Schemas/Converse/ConverseTextHandler.php @@ -5,6 +5,7 @@ use Illuminate\Http\Client\Response; use Illuminate\Support\Collection; use Prism\Bedrock\Contracts\BedrockTextHandler; +use Prism\Bedrock\Schemas\Converse\Concerns\ExtractsText; use Prism\Bedrock\Schemas\Converse\Concerns\ExtractsToolCalls; use Prism\Bedrock\Schemas\Converse\Maps\FinishReasonMap; use Prism\Bedrock\Schemas\Converse\Maps\MessageMap; @@ -26,7 +27,7 @@ class ConverseTextHandler extends BedrockTextHandler { - use CallsTools, ExtractsToolCalls; + use CallsTools, ExtractsText, ExtractsToolCalls; protected TextResponse $tempResponse; @@ -59,7 +60,7 @@ public function handle(Request $request): TextResponse return match ($this->tempResponse->finishReason) { FinishReason::ToolCalls => $this->handleToolCalls($request), FinishReason::Stop, FinishReason::Length => $this->handleStop($request), - default => throw new PrismException('Anthropic: unknown finish reason'), + default => throw new PrismException('Converse: unknown finish reason'), }; } @@ -109,7 +110,7 @@ protected function prepareTempResponse(): void $this->tempResponse = new TextResponse( steps: new Collection, - text: data_get($data, 'output.message.content.0.text', ''), + text: $this->extractText($data), finishReason: FinishReasonMap::map(data_get($data, 'stopReason')), toolCalls: $this->extractToolCalls($data), toolResults: [], @@ -117,8 +118,8 @@ protected function prepareTempResponse(): void promptTokens: data_get($data, 'usage.inputTokens'), completionTokens: data_get($data, 'usage.outputTokens') ), - meta: new Meta(id: '', model: ''), // Not provided in Converse response. - messages: new Collection + meta: new Meta(id: '', model: ''), + messages: new Collection, // Not provided in Converse response. ); } diff --git a/src/Schemas/Converse/Maps/MessageMap.php b/src/Schemas/Converse/Maps/MessageMap.php index 5170266..d3af8f7 100644 --- a/src/Schemas/Converse/Maps/MessageMap.php +++ b/src/Schemas/Converse/Maps/MessageMap.php @@ -4,7 +4,6 @@ namespace Prism\Bedrock\Schemas\Converse\Maps; -use Exception; use Prism\Prism\Contracts\Message; use Prism\Prism\Exceptions\PrismException; use Prism\Prism\ValueObjects\Media\Document; @@ -65,7 +64,7 @@ protected static function mapMessage(Message $message): array AssistantMessage::class => self::mapAssistantMessage($message), ToolResultMessage::class => self::mapToolResultMessage($message), SystemMessage::class => self::mapSystemMessage($message), - default => throw new Exception('Could not map message type '.$message::class), + default => throw new PrismException('Converse: Could not map message type '.$message::class), }; } diff --git a/tests/Fixtures/converse/generate-text-with-reasoning-content-1.json b/tests/Fixtures/converse/generate-text-with-reasoning-content-1.json new file mode 100644 index 0000000..8eabf27 --- /dev/null +++ b/tests/Fixtures/converse/generate-text-with-reasoning-content-1.json @@ -0,0 +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}} \ No newline at end of file diff --git a/tests/Schemas/Anthropic/AnthropicStructuredHandlerTest.php b/tests/Schemas/Anthropic/AnthropicStructuredHandlerTest.php index c4b6cde..98712f7 100644 --- a/tests/Schemas/Anthropic/AnthropicStructuredHandlerTest.php +++ b/tests/Schemas/Anthropic/AnthropicStructuredHandlerTest.php @@ -135,7 +135,11 @@ ->usingTemperature(0) ->asStructured(); - Http::assertSent(fn (Request $request): \Pest\Mixins\Expectation|\Pest\Expectation => expect($request->data())->toMatchArray([ - 'temperature' => 0, - ])); + Http::assertSent(function (Request $request): bool { + expect($request->data())->toMatchArray([ + 'temperature' => 0, + ]); + + return true; + }); }); diff --git a/tests/Schemas/Anthropic/AnthropicTextHandlerTest.php b/tests/Schemas/Anthropic/AnthropicTextHandlerTest.php index e678916..3f03fe4 100644 --- a/tests/Schemas/Anthropic/AnthropicTextHandlerTest.php +++ b/tests/Schemas/Anthropic/AnthropicTextHandlerTest.php @@ -203,7 +203,11 @@ ->usingTemperature(0) ->asText(); - Http::assertSent(fn (Request $request): \Pest\Mixins\Expectation|\Pest\Expectation => expect($request->data())->toMatchArray([ - 'temperature' => 0, - ])); + Http::assertSent(function (Request $request): bool { + expect($request->data())->toMatchArray([ + 'temperature' => 0, + ]); + + return true; + }); }); diff --git a/tests/Schemas/Converse/ConverseStructuredHandlerTest.php b/tests/Schemas/Converse/ConverseStructuredHandlerTest.php index ae3e9d5..63b7337 100644 --- a/tests/Schemas/Converse/ConverseStructuredHandlerTest.php +++ b/tests/Schemas/Converse/ConverseStructuredHandlerTest.php @@ -193,10 +193,16 @@ ->usingTemperature(0) ->asStructured(); - Http::assertSent(fn (Request $request): \Pest\Mixins\Expectation|\Pest\Expectation => expect($request->data())->toMatchArray([ - 'inferenceConfig' => [ - 'maxTokens' => 2048, - 'temperature' => 0, - ], - ])->not()->toHaveKey('guardRailConfig')); + Http::assertSent(function (Request $request): bool { + expect($request->data())->toMatchArray([ + 'inferenceConfig' => [ + 'maxTokens' => 2048, + 'temperature' => 0, + ], + ]) + ->not() + ->toHaveKey('guardRailConfig'); + + return true; + }); }); diff --git a/tests/Schemas/Converse/ConverseTextHandlerTest.php b/tests/Schemas/Converse/ConverseTextHandlerTest.php index b2b7f1f..5284319 100644 --- a/tests/Schemas/Converse/ConverseTextHandlerTest.php +++ b/tests/Schemas/Converse/ConverseTextHandlerTest.php @@ -33,6 +33,21 @@ 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!"); }); +it('can generate text with reasoning content', function (): void { + FixtureResponse::fakeResponseSequence('converse', 'converse/generate-text-with-reasoning-content'); + + $response = Prism::text() + ->using('bedrock', 'openai.gpt-oss-120b-1:0') + ->withPrompt('Tell me a short story about a brave knight.') + ->asText(); + + expect($response->usage->promptTokens) + ->toBe(21) + ->and($response->usage->completionTokens)->toBe(765) + ->and($response->text)->toContain('In the mist‑shrouded kingdom of Eldoria') + ->and($response->text)->toContain('Sir Alden\'s legend endured'); +}); + it('can generate text with a system prompt', function (): void { FixtureResponse::fakeResponseSequence('converse', 'converse/generate-text-with-system-prompt'); @@ -283,10 +298,14 @@ ->usingTemperature(0) ->asText(); - Http::assertSent(fn (Request $request): \Pest\Mixins\Expectation|\Pest\Expectation => expect($request->data())->toMatchArray([ - 'inferenceConfig' => [ - 'temperature' => 0, - 'maxTokens' => 2048, - ], - ])); + Http::assertSent(function (Request $request): bool { + expect($request->data())->toMatchArray([ + 'inferenceConfig' => [ + 'temperature' => 0, + 'maxTokens' => 2048, + ], + ]); + + return true; + }); });