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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ phpunit.xml
.vscode
*.code-workspace
ray.php
CLAUDE.md
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,30 @@ $response = Prism::text()
> [!TIP]
> Anthropic currently supports a cacheType of "ephemeral". Converse currently supports a cacheType of "default". It is possible that Anthropic and/or AWS may add additional types in the future.

## Structured Adapted Support

Both Anthropic and Converse Schemas do not support a native structured format.

Prism Bedrock has adapted support by appending a prompt asking the model to a response conforming to the schema you provide.

The performance of that prompt may vary by model. You can override it using `withProviderOptions()`:

```php
use Prism\Prism\Prism;
use Prism\Bedrock\Bedrock;
use Prism\Prism\ValueObjects\Messages\UserMessage;

Prism::structured()
->withSchema($schema)
->using('bedrock', 'anthropic.claude-3-5-haiku-20241022-v1:0')
->withProviderOptions([
// Override the default message of "Respond with ONLY JSON (i.e. not in backticks or a code block, with NO CONTENT outside the JSON) that matches the following schema:"
'jsonModeMessage' => 'My custom message',
])
->withPrompt('My prompt')
->asStructured();
```

## License

The MIT License (MIT). Please see [License File](LICENSE) for more information.
Expand Down
3 changes: 2 additions & 1 deletion src/Schemas/Anthropic/AnthropicStructuredHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,8 @@ protected function prepareTempResponse(): void
protected function appendMessageForJsonMode(Request $request): void
{
$request->addMessage(new UserMessage(sprintf(
"Respond with ONLY JSON (i.e. not in backticks or a code block, with NO CONTENT outside the JSON) that matches the following schema: \n %s",
"%s \n %s",
$request->providerOptions('jsonModeMessage') ?? 'Respond with ONLY JSON (i.e. not in backticks or a code block, with NO CONTENT outside the JSON) that matches the following schema:',
json_encode($request->schema()->toArray(), JSON_PRETTY_PRINT)
)));
}
Expand Down
3 changes: 2 additions & 1 deletion src/Schemas/Converse/ConverseStructuredHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,8 @@ protected function prepareTempResponse(): void
protected function appendMessageForJsonMode(Request $request): void
{
$request->addMessage(new UserMessage(sprintf(
"Respond with ONLY JSON (i.e. not in backticks or a code block, with NO CONTENT outside the JSON) that matches the following schema: \n %s",
"%s \n %s",
$request->providerOptions('jsonModeMessage') ?? 'Respond with ONLY JSON (i.e. not in backticks or a code block, with NO CONTENT outside the JSON) that matches the following schema:',
json_encode($request->schema()->toArray(), JSON_PRETTY_PRINT)
)));
}
Expand Down
67 changes: 67 additions & 0 deletions tests/Schemas/Anthropic/AnthropicStructuredHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,73 @@
expect($response->structured['coat_required'])->toBeBool();
});

it('uses custom jsonModeMessage when provided via providerOptions', function (): void {
FixtureResponse::fakeResponseSequence('invoke', 'anthropic/structured');

$schema = new ObjectSchema(
'output',
'the output object',
[
new StringSchema('weather', 'The weather forecast'),
new StringSchema('game_time', 'The tigers game time'),
new BooleanSchema('coat_required', 'whether a coat is required'),
],
['weather', 'game_time', 'coat_required']
);

$customMessage = 'Please return a JSON response using this custom format instruction';

Prism::structured()
->withSchema($schema)
->using('bedrock', 'anthropic.claude-3-5-haiku-20241022-v1:0')
->withProviderOptions([
'jsonModeMessage' => $customMessage,
])
->withSystemPrompt('The tigers game is at 3pm and the temperature will be 70º')
->withPrompt('What time is the tigers game today and should I wear a coat?')
->asStructured();

Http::assertSent(function (Request $request) use ($customMessage): bool {
$messages = $request->data()['messages'] ?? [];
$lastMessage = end($messages);

return isset($lastMessage['content'][0]['text']) &&
str_contains((string) $lastMessage['content'][0]['text'], $customMessage);
});
});

it('uses default jsonModeMessage when no custom message is provided', function (): void {
FixtureResponse::fakeResponseSequence('invoke', 'anthropic/structured');

$schema = new ObjectSchema(
'output',
'the output object',
[
new StringSchema('weather', 'The weather forecast'),
new StringSchema('game_time', 'The tigers game time'),
new BooleanSchema('coat_required', 'whether a coat is required'),
],
['weather', 'game_time', 'coat_required']
);

$defaultMessage = 'Respond with ONLY JSON (i.e. not in backticks or a code block, with NO CONTENT outside the JSON) that matches the following schema:';

Prism::structured()
->withSchema($schema)
->using('bedrock', 'anthropic.claude-3-5-haiku-20241022-v1:0')
->withSystemPrompt('The tigers game is at 3pm and the temperature will be 70º')
->withPrompt('What time is the tigers game today and should I wear a coat?')
->asStructured();

Http::assertSent(function (Request $request) use ($defaultMessage): bool {
$messages = $request->data()['messages'] ?? [];
$lastMessage = end($messages);

return isset($lastMessage['content'][0]['text']) &&
str_contains((string) $lastMessage['content'][0]['text'], $defaultMessage);
});
});

it('does not remove 0 values from payloads', function (): void {
FixtureResponse::fakeResponseSequence('invoke', 'anthropic/structured');

Expand Down
71 changes: 71 additions & 0 deletions tests/Schemas/Converse/ConverseStructuredHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,77 @@
$fake->assertRequest(fn (array $requests): mixed => expect($requests[0]->providerOptions())->toBe($providerOptions));
});

it('uses custom jsonModeMessage when provided via providerOptions', function (): void {
FixtureResponse::fakeResponseSequence('converse', 'converse/structured');

$schema = new ObjectSchema(
'output',
'the output object',
[
new StringSchema('weather', 'The weather forecast'),
new StringSchema('game_time', 'The tigers game time'),
new BooleanSchema('coat_required', 'whether a coat is required'),
],
['weather', 'game_time', 'coat_required']
);

$customMessage = 'Please return a JSON response using this custom format instruction';

Prism::structured()
->withSchema($schema)
->using('bedrock', 'anthropic.claude-3-5-haiku-20241022-v1:0')
->withProviderOptions([
'apiSchema' => BedrockSchema::Converse,
'jsonModeMessage' => $customMessage,
])
->withSystemPrompt('The tigers game is at 3pm and the temperature will be 70º')
->withPrompt('What time is the tigers game today and should I wear a coat?')
->asStructured();

Http::assertSent(function (Request $request) use ($customMessage): bool {
$messages = $request->data()['messages'] ?? [];
$lastMessage = end($messages);

return isset($lastMessage['content'][0]['text']) &&
str_contains((string) $lastMessage['content'][0]['text'], $customMessage);
});
});

it('uses default jsonModeMessage when no custom message is provided', function (): void {
FixtureResponse::fakeResponseSequence('converse', 'converse/structured');

$schema = new ObjectSchema(
'output',
'the output object',
[
new StringSchema('weather', 'The weather forecast'),
new StringSchema('game_time', 'The tigers game time'),
new BooleanSchema('coat_required', 'whether a coat is required'),
],
['weather', 'game_time', 'coat_required']
);

$defaultMessage = 'Respond with ONLY JSON (i.e. not in backticks or a code block, with NO CONTENT outside the JSON) that matches the following schema:';

Prism::structured()
->withSchema($schema)
->using('bedrock', 'anthropic.claude-3-5-haiku-20241022-v1:0')
->withProviderOptions([
'apiSchema' => BedrockSchema::Converse,
])
->withSystemPrompt('The tigers game is at 3pm and the temperature will be 70º')
->withPrompt('What time is the tigers game today and should I wear a coat?')
->asStructured();

Http::assertSent(function (Request $request) use ($defaultMessage): bool {
$messages = $request->data()['messages'] ?? [];
$lastMessage = end($messages);

return isset($lastMessage['content'][0]['text']) &&
str_contains((string) $lastMessage['content'][0]['text'], $defaultMessage);
});
});

it('does not remove 0 values from payloads', function (): void {
FixtureResponse::fakeResponseSequence('converse', 'converse/structured');

Expand Down