mirror of
https://github.com/symfony/ai.git
synced 2026-03-23 23:42:18 +01:00
[Platform][Bedrock] Support structured output for ClaudeModelClient
This commit is contained in:
37
examples/bedrock/structure-output-claude.php
Normal file
37
examples/bedrock/structure-output-claude.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Symfony\AI\Platform\Bridge\Bedrock\PlatformFactory;
|
||||
use Symfony\AI\Platform\Message\Message;
|
||||
use Symfony\AI\Platform\Message\MessageBag;
|
||||
use Symfony\AI\Platform\StructuredOutput\PlatformSubscriber;
|
||||
use Symfony\AI\Platform\Tests\Fixtures\StructuredOutput\MathReasoning;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||
|
||||
require_once dirname(__DIR__).'/bootstrap.php';
|
||||
|
||||
if (!isset($_SERVER['AWS_ACCESS_KEY_ID'], $_SERVER['AWS_SECRET_ACCESS_KEY'], $_SERVER['AWS_DEFAULT_REGION'])
|
||||
) {
|
||||
echo 'Please set the AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and AWS_DEFAULT_REGION environment variables.'.\PHP_EOL;
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$dispatcher = new EventDispatcher();
|
||||
$dispatcher->addSubscriber(new PlatformSubscriber());
|
||||
$platform = PlatformFactory::create(eventDispatcher: $dispatcher);
|
||||
|
||||
$messages = new MessageBag(
|
||||
Message::forSystem('You are a helpful math tutor. Guide the user through the solution step by step.'),
|
||||
Message::ofUser('how can I solve 8x + 7 = -23'),
|
||||
);
|
||||
$result = $platform->invoke('claude-haiku-4-5-20251001', $messages, ['response_format' => MathReasoning::class]);
|
||||
|
||||
dump($result->asObject());
|
||||
@@ -1,6 +1,11 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
0.6
|
||||
---
|
||||
|
||||
* Add structured output support
|
||||
|
||||
0.4
|
||||
---
|
||||
|
||||
|
||||
@@ -174,6 +174,7 @@ final class ModelCatalog extends AbstractModelCatalog
|
||||
Capability::INPUT_IMAGE,
|
||||
Capability::OUTPUT_TEXT,
|
||||
Capability::OUTPUT_STREAMING,
|
||||
Capability::OUTPUT_STRUCTURED,
|
||||
Capability::THINKING,
|
||||
Capability::TOOL_CALLING,
|
||||
],
|
||||
|
||||
@@ -52,10 +52,11 @@ final class ModelClient implements ModelClientInterface
|
||||
}
|
||||
|
||||
if (isset($options['response_format'])) {
|
||||
$options['beta_features'][] = 'structured-outputs-2025-11-13';
|
||||
$options['output_format'] = [
|
||||
'type' => 'json_schema',
|
||||
'schema' => $options['response_format']['json_schema']['schema'] ?? [],
|
||||
$options['output_config'] = [
|
||||
'format' => [
|
||||
'type' => 'json_schema',
|
||||
'schema' => $options['response_format']['json_schema']['schema'] ?? [],
|
||||
],
|
||||
];
|
||||
unset($options['response_format']);
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ final class ModelCatalogTest extends ModelCatalogTestCase
|
||||
yield 'claude-opus-4-1' => ['claude-opus-4-1', Claude::class, [Capability::INPUT_MESSAGES, Capability::INPUT_IMAGE, Capability::OUTPUT_TEXT, Capability::OUTPUT_STREAMING, Capability::OUTPUT_STRUCTURED, Capability::THINKING, Capability::TOOL_CALLING]];
|
||||
yield 'claude-opus-4-1-20250805' => ['claude-opus-4-1-20250805', Claude::class, [Capability::INPUT_MESSAGES, Capability::INPUT_IMAGE, Capability::OUTPUT_TEXT, Capability::OUTPUT_STREAMING, Capability::OUTPUT_STRUCTURED, Capability::THINKING, Capability::TOOL_CALLING]];
|
||||
yield 'claude-sonnet-4-5-20250929' => ['claude-sonnet-4-5-20250929', Claude::class, [Capability::INPUT_MESSAGES, Capability::INPUT_IMAGE, Capability::OUTPUT_TEXT, Capability::OUTPUT_STREAMING, Capability::OUTPUT_STRUCTURED, Capability::THINKING, Capability::TOOL_CALLING]];
|
||||
yield 'claude-haiku-4-5-20251001' => ['claude-haiku-4-5-20251001', Claude::class, [Capability::INPUT_MESSAGES, Capability::INPUT_IMAGE, Capability::OUTPUT_TEXT, Capability::OUTPUT_STREAMING, Capability::THINKING, Capability::TOOL_CALLING]];
|
||||
yield 'claude-haiku-4-5-20251001' => ['claude-haiku-4-5-20251001', Claude::class, [Capability::INPUT_MESSAGES, Capability::INPUT_IMAGE, Capability::OUTPUT_TEXT, Capability::OUTPUT_STREAMING, Capability::OUTPUT_STRUCTURED, Capability::THINKING, Capability::TOOL_CALLING]];
|
||||
}
|
||||
|
||||
protected function createModelCatalog(): ModelCatalogInterface
|
||||
|
||||
@@ -140,6 +140,41 @@ class ModelClientTest extends TestCase
|
||||
$this->modelClient->request($this->model, ['message' => 'test'], $options);
|
||||
}
|
||||
|
||||
public function testTransformsResponseFormatToOutputConfig()
|
||||
{
|
||||
$this->httpClient = new MockHttpClient(function ($method, $url, $options) {
|
||||
$headers = $this->parseHeaders($options['headers']);
|
||||
|
||||
$this->assertArrayNotHasKey('anthropic-beta', $headers);
|
||||
|
||||
$body = json_decode($options['body'], true);
|
||||
$this->assertArrayHasKey('output_config', $body);
|
||||
$this->assertArrayHasKey('format', $body['output_config']);
|
||||
$this->assertSame('json_schema', $body['output_config']['format']['type']);
|
||||
$this->assertSame(['type' => 'object', 'properties' => ['foo' => ['type' => 'string']]], $body['output_config']['format']['schema']);
|
||||
$this->assertArrayNotHasKey('response_format', $body);
|
||||
|
||||
return new JsonMockResponse('{"success": true}');
|
||||
});
|
||||
|
||||
$this->modelClient = new ModelClient($this->httpClient, 'test-api-key');
|
||||
|
||||
$options = [
|
||||
'response_format' => [
|
||||
'json_schema' => [
|
||||
'schema' => [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'foo' => ['type' => 'string'],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$this->modelClient->request($this->model, ['message' => 'test'], $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $headers
|
||||
*
|
||||
|
||||
@@ -42,6 +42,16 @@ final class ClaudeModelClient implements ModelClientInterface
|
||||
$options['tool_choice'] = ['type' => 'auto'];
|
||||
}
|
||||
|
||||
if (isset($options['response_format'])) {
|
||||
$options['output_config'] = [
|
||||
'format' => [
|
||||
'type' => 'json_schema',
|
||||
'schema' => $options['response_format']['json_schema']['schema'] ?? [],
|
||||
],
|
||||
];
|
||||
unset($options['response_format']);
|
||||
}
|
||||
|
||||
if (!isset($options['anthropic_version'])) {
|
||||
$options['anthropic_version'] = 'bedrock-'.$this->version;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
0.6
|
||||
---
|
||||
|
||||
* Add structured output support
|
||||
|
||||
0.1
|
||||
---
|
||||
|
||||
|
||||
@@ -190,6 +190,7 @@ final class ModelCatalog extends AbstractModelCatalog
|
||||
Capability::INPUT_IMAGE,
|
||||
Capability::OUTPUT_TEXT,
|
||||
Capability::OUTPUT_STREAMING,
|
||||
Capability::OUTPUT_STRUCTURED,
|
||||
Capability::TOOL_CALLING,
|
||||
],
|
||||
],
|
||||
@@ -200,6 +201,18 @@ final class ModelCatalog extends AbstractModelCatalog
|
||||
Capability::INPUT_IMAGE,
|
||||
Capability::OUTPUT_TEXT,
|
||||
Capability::OUTPUT_STREAMING,
|
||||
Capability::OUTPUT_STRUCTURED,
|
||||
Capability::TOOL_CALLING,
|
||||
],
|
||||
],
|
||||
'claude-haiku-4-5-20251001' => [
|
||||
'class' => Claude::class,
|
||||
'capabilities' => [
|
||||
Capability::INPUT_MESSAGES,
|
||||
Capability::INPUT_IMAGE,
|
||||
Capability::OUTPUT_TEXT,
|
||||
Capability::OUTPUT_STREAMING,
|
||||
Capability::OUTPUT_STRUCTURED,
|
||||
Capability::TOOL_CALLING,
|
||||
],
|
||||
],
|
||||
|
||||
@@ -129,4 +129,43 @@ final class ClaudeModelClientTest extends TestCase
|
||||
$response = $this->modelClient->request($this->model, ['message' => 'test'], $options);
|
||||
$this->assertInstanceOf(RawBedrockResult::class, $response);
|
||||
}
|
||||
|
||||
public function testTransformsResponseFormatToOutputConfig()
|
||||
{
|
||||
$this->bedrockClient->expects($this->once())
|
||||
->method('invokeModel')
|
||||
->with($this->callback(function ($arg) {
|
||||
$this->assertInstanceOf(InvokeModelRequest::class, $arg);
|
||||
$this->assertSame('application/json', $arg->getContentType());
|
||||
$this->assertTrue(json_validate($arg->getBody()));
|
||||
|
||||
$body = json_decode($arg->getBody(), true);
|
||||
$this->assertArrayHasKey('output_config', $body);
|
||||
$this->assertArrayHasKey('format', $body['output_config']);
|
||||
$this->assertSame('json_schema', $body['output_config']['format']['type']);
|
||||
$this->assertSame(['type' => 'object', 'properties' => ['foo' => ['type' => 'string']]], $body['output_config']['format']['schema']);
|
||||
$this->assertArrayNotHasKey('response_format', $body);
|
||||
|
||||
return true;
|
||||
}))
|
||||
->willReturn($this->createMock(InvokeModelResponse::class));
|
||||
|
||||
$this->modelClient = new ClaudeModelClient($this->bedrockClient, self::VERSION);
|
||||
|
||||
$options = [
|
||||
'response_format' => [
|
||||
'json_schema' => [
|
||||
'schema' => [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'foo' => ['type' => 'string'],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$response = $this->modelClient->request($this->model, ['message' => 'test'], $options);
|
||||
$this->assertInstanceOf(RawBedrockResult::class, $response);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,9 @@ final class ModelCatalogTest extends ModelCatalogTestCase
|
||||
yield 'nova-pro' => ['nova-pro', Nova::class, [Capability::INPUT_MESSAGES, Capability::OUTPUT_TEXT, Capability::OUTPUT_STRUCTURED, Capability::TOOL_CALLING]];
|
||||
yield 'nova-premier' => ['nova-premier', Nova::class, [Capability::INPUT_MESSAGES, Capability::OUTPUT_TEXT, Capability::OUTPUT_STRUCTURED, Capability::TOOL_CALLING, Capability::INPUT_IMAGE]];
|
||||
yield 'claude-3-7-sonnet-20250219' => ['claude-3-7-sonnet-20250219', Claude::class, [Capability::INPUT_MESSAGES, Capability::INPUT_IMAGE, Capability::OUTPUT_TEXT, Capability::OUTPUT_STREAMING, Capability::TOOL_CALLING]];
|
||||
yield 'claude-sonnet-4-5-20250929' => ['claude-sonnet-4-5-20250929', Claude::class, [Capability::INPUT_MESSAGES, Capability::INPUT_IMAGE, Capability::OUTPUT_TEXT, Capability::OUTPUT_STREAMING, Capability::OUTPUT_STRUCTURED, Capability::TOOL_CALLING]];
|
||||
yield 'claude-opus-4-5-20251101' => ['claude-opus-4-5-20251101', Claude::class, [Capability::INPUT_MESSAGES, Capability::INPUT_IMAGE, Capability::OUTPUT_TEXT, Capability::OUTPUT_STREAMING, Capability::OUTPUT_STRUCTURED, Capability::TOOL_CALLING]];
|
||||
yield 'claude-haiku-4-5-20251001' => ['claude-haiku-4-5-20251001', Claude::class, [Capability::INPUT_MESSAGES, Capability::INPUT_IMAGE, Capability::OUTPUT_TEXT, Capability::OUTPUT_STREAMING, Capability::OUTPUT_STRUCTURED, Capability::TOOL_CALLING]];
|
||||
}
|
||||
|
||||
protected function createModelCatalog(): ModelCatalogInterface
|
||||
|
||||
Reference in New Issue
Block a user