Skip to content

Commit 04ea1e0

Browse files
authored
feat: override json mode message (#29)
1 parent d495591 commit 04ea1e0

6 files changed

Lines changed: 167 additions & 2 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ phpunit.xml
88
.vscode
99
*.code-workspace
1010
ray.php
11+
CLAUDE.md

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,30 @@ $response = Prism::text()
189189
> [!TIP]
190190
> 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.
191191
192+
## Structured Adapted Support
193+
194+
Both Anthropic and Converse Schemas do not support a native structured format.
195+
196+
Prism Bedrock has adapted support by appending a prompt asking the model to a response conforming to the schema you provide.
197+
198+
The performance of that prompt may vary by model. You can override it using `withProviderOptions()`:
199+
200+
```php
201+
use Prism\Prism\Prism;
202+
use Prism\Bedrock\Bedrock;
203+
use Prism\Prism\ValueObjects\Messages\UserMessage;
204+
205+
Prism::structured()
206+
->withSchema($schema)
207+
->using('bedrock', 'anthropic.claude-3-5-haiku-20241022-v1:0')
208+
->withProviderOptions([
209+
// 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:"
210+
'jsonModeMessage' => 'My custom message',
211+
])
212+
->withPrompt('My prompt')
213+
->asStructured();
214+
```
215+
192216
## License
193217

194218
The MIT License (MIT). Please see [License File](LICENSE) for more information.

src/Schemas/Anthropic/AnthropicStructuredHandler.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,8 @@ protected function prepareTempResponse(): void
120120
protected function appendMessageForJsonMode(Request $request): void
121121
{
122122
$request->addMessage(new UserMessage(sprintf(
123-
"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",
123+
"%s \n %s",
124+
$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:',
124125
json_encode($request->schema()->toArray(), JSON_PRETTY_PRINT)
125126
)));
126127
}

src/Schemas/Converse/ConverseStructuredHandler.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,8 @@ protected function prepareTempResponse(): void
120120
protected function appendMessageForJsonMode(Request $request): void
121121
{
122122
$request->addMessage(new UserMessage(sprintf(
123-
"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",
123+
"%s \n %s",
124+
$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:',
124125
json_encode($request->schema()->toArray(), JSON_PRETTY_PRINT)
125126
)));
126127
}

tests/Schemas/Anthropic/AnthropicStructuredHandlerTest.php

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,73 @@
4444
expect($response->structured['coat_required'])->toBeBool();
4545
});
4646

47+
it('uses custom jsonModeMessage when provided via providerOptions', function (): void {
48+
FixtureResponse::fakeResponseSequence('invoke', 'anthropic/structured');
49+
50+
$schema = new ObjectSchema(
51+
'output',
52+
'the output object',
53+
[
54+
new StringSchema('weather', 'The weather forecast'),
55+
new StringSchema('game_time', 'The tigers game time'),
56+
new BooleanSchema('coat_required', 'whether a coat is required'),
57+
],
58+
['weather', 'game_time', 'coat_required']
59+
);
60+
61+
$customMessage = 'Please return a JSON response using this custom format instruction';
62+
63+
Prism::structured()
64+
->withSchema($schema)
65+
->using('bedrock', 'anthropic.claude-3-5-haiku-20241022-v1:0')
66+
->withProviderOptions([
67+
'jsonModeMessage' => $customMessage,
68+
])
69+
->withSystemPrompt('The tigers game is at 3pm and the temperature will be 70º')
70+
->withPrompt('What time is the tigers game today and should I wear a coat?')
71+
->asStructured();
72+
73+
Http::assertSent(function (Request $request) use ($customMessage): bool {
74+
$messages = $request->data()['messages'] ?? [];
75+
$lastMessage = end($messages);
76+
77+
return isset($lastMessage['content'][0]['text']) &&
78+
str_contains((string) $lastMessage['content'][0]['text'], $customMessage);
79+
});
80+
});
81+
82+
it('uses default jsonModeMessage when no custom message is provided', function (): void {
83+
FixtureResponse::fakeResponseSequence('invoke', 'anthropic/structured');
84+
85+
$schema = new ObjectSchema(
86+
'output',
87+
'the output object',
88+
[
89+
new StringSchema('weather', 'The weather forecast'),
90+
new StringSchema('game_time', 'The tigers game time'),
91+
new BooleanSchema('coat_required', 'whether a coat is required'),
92+
],
93+
['weather', 'game_time', 'coat_required']
94+
);
95+
96+
$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:';
97+
98+
Prism::structured()
99+
->withSchema($schema)
100+
->using('bedrock', 'anthropic.claude-3-5-haiku-20241022-v1:0')
101+
->withSystemPrompt('The tigers game is at 3pm and the temperature will be 70º')
102+
->withPrompt('What time is the tigers game today and should I wear a coat?')
103+
->asStructured();
104+
105+
Http::assertSent(function (Request $request) use ($defaultMessage): bool {
106+
$messages = $request->data()['messages'] ?? [];
107+
$lastMessage = end($messages);
108+
109+
return isset($lastMessage['content'][0]['text']) &&
110+
str_contains((string) $lastMessage['content'][0]['text'], $defaultMessage);
111+
});
112+
});
113+
47114
it('does not remove 0 values from payloads', function (): void {
48115
FixtureResponse::fakeResponseSequence('invoke', 'anthropic/structured');
49116

tests/Schemas/Converse/ConverseStructuredHandlerTest.php

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,77 @@
9797
$fake->assertRequest(fn (array $requests): mixed => expect($requests[0]->providerOptions())->toBe($providerOptions));
9898
});
9999

100+
it('uses custom jsonModeMessage when provided via providerOptions', function (): void {
101+
FixtureResponse::fakeResponseSequence('converse', 'converse/structured');
102+
103+
$schema = new ObjectSchema(
104+
'output',
105+
'the output object',
106+
[
107+
new StringSchema('weather', 'The weather forecast'),
108+
new StringSchema('game_time', 'The tigers game time'),
109+
new BooleanSchema('coat_required', 'whether a coat is required'),
110+
],
111+
['weather', 'game_time', 'coat_required']
112+
);
113+
114+
$customMessage = 'Please return a JSON response using this custom format instruction';
115+
116+
Prism::structured()
117+
->withSchema($schema)
118+
->using('bedrock', 'anthropic.claude-3-5-haiku-20241022-v1:0')
119+
->withProviderOptions([
120+
'apiSchema' => BedrockSchema::Converse,
121+
'jsonModeMessage' => $customMessage,
122+
])
123+
->withSystemPrompt('The tigers game is at 3pm and the temperature will be 70º')
124+
->withPrompt('What time is the tigers game today and should I wear a coat?')
125+
->asStructured();
126+
127+
Http::assertSent(function (Request $request) use ($customMessage): bool {
128+
$messages = $request->data()['messages'] ?? [];
129+
$lastMessage = end($messages);
130+
131+
return isset($lastMessage['content'][0]['text']) &&
132+
str_contains((string) $lastMessage['content'][0]['text'], $customMessage);
133+
});
134+
});
135+
136+
it('uses default jsonModeMessage when no custom message is provided', function (): void {
137+
FixtureResponse::fakeResponseSequence('converse', 'converse/structured');
138+
139+
$schema = new ObjectSchema(
140+
'output',
141+
'the output object',
142+
[
143+
new StringSchema('weather', 'The weather forecast'),
144+
new StringSchema('game_time', 'The tigers game time'),
145+
new BooleanSchema('coat_required', 'whether a coat is required'),
146+
],
147+
['weather', 'game_time', 'coat_required']
148+
);
149+
150+
$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:';
151+
152+
Prism::structured()
153+
->withSchema($schema)
154+
->using('bedrock', 'anthropic.claude-3-5-haiku-20241022-v1:0')
155+
->withProviderOptions([
156+
'apiSchema' => BedrockSchema::Converse,
157+
])
158+
->withSystemPrompt('The tigers game is at 3pm and the temperature will be 70º')
159+
->withPrompt('What time is the tigers game today and should I wear a coat?')
160+
->asStructured();
161+
162+
Http::assertSent(function (Request $request) use ($defaultMessage): bool {
163+
$messages = $request->data()['messages'] ?? [];
164+
$lastMessage = end($messages);
165+
166+
return isset($lastMessage['content'][0]['text']) &&
167+
str_contains((string) $lastMessage['content'][0]['text'], $defaultMessage);
168+
});
169+
});
170+
100171
it('does not remove 0 values from payloads', function (): void {
101172
FixtureResponse::fakeResponseSequence('converse', 'converse/structured');
102173

0 commit comments

Comments
 (0)