Skip to content

Commit 8241fc2

Browse files
authored
OpenAI Structured Output (#68)
1 parent 546465d commit 8241fc2

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1849
-132
lines changed

Diff for: composer.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@
3737
"projektgopher/whisky": "^0.7.0",
3838
"orchestra/testbench": "^9.4",
3939
"mockery/mockery": "^1.6",
40-
"symplify/rule-doc-generator-contracts": "^11.2"
40+
"symplify/rule-doc-generator-contracts": "^11.2",
41+
"phpstan/phpdoc-parser": "^1.24"
4142
},
4243
"autoload-dev": {
4344
"psr-4": {

Diff for: docs/.vitepress/config.mts

+4
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ export default defineConfig({
9494
text: "Text Generation",
9595
link: "/core-concepts/text-generation",
9696
},
97+
{
98+
text: "Structured Output",
99+
link: "/core-concepts/structured-output",
100+
},
97101
{
98102
text: "Tool & Function Calling",
99103
link: "/core-concepts/tools-function-calling",

Diff for: docs/core-concepts/structured-output.md

+185
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
# Structured Output
2+
3+
Want your AI responses as neat and tidy as a Marie Kondo-approved closet? Structured output lets you define exactly how you want your data formatted, making it perfect for building APIs, processing forms, or any time you need data in a specific shape.
4+
5+
## Quick Start
6+
7+
Here's how to get structured data from your AI:
8+
9+
```php
10+
use EchoLabs\Prism\Enums\Provider;
11+
use EchoLabs\Prism\Facades\Prism;
12+
use EchoLabs\Prism\Schema\ObjectSchema;
13+
use EchoLabs\Prism\Schema\StringSchema;
14+
15+
$schema = new ObjectSchema(
16+
name: 'movie_review',
17+
description: 'A structured movie review',
18+
properties: [
19+
new StringSchema('title', 'The movie title'),
20+
new StringSchema('rating', 'Rating out of 5 stars'),
21+
new StringSchema('summary', 'Brief review summary')
22+
],
23+
requiredFields: ['title', 'rating', 'summary']
24+
);
25+
26+
$response = Prism::structured()
27+
->using(Provider::OpenAI, 'gpt-4o')
28+
->withSchema($schema)
29+
->withPrompt('Review the movie Inception')
30+
->generate();
31+
32+
// Access your structured data
33+
$review = $response->object;
34+
echo $review['title']; // "Inception"
35+
echo $review['rating']; // "5 stars"
36+
echo $review['summary']; // "A mind-bending..."
37+
```
38+
39+
## Understanding Output Modes
40+
41+
Different AI providers handle structured output in two main ways:
42+
43+
1. **Structured Mode**: Some providers support strict schema validation, ensuring responses perfectly match your defined structure.
44+
2. **JSON Mode**: Other providers simply guarantee valid JSON output that approximately matches your schema.
45+
46+
> [!NOTE]
47+
> Check your provider's documentation to understand which mode they support. Provider support can vary by model, so always verify capabilities for your specific use case.
48+
49+
## Available Schema Types
50+
51+
Build your schemas using these fundamental types:
52+
53+
```php
54+
use EchoLabs\Prism\Schema\{
55+
StringSchema,
56+
NumberSchema,
57+
BooleanSchema,
58+
ArraySchema,
59+
ObjectSchema,
60+
EnumSchema
61+
};
62+
63+
// String
64+
new StringSchema('name', 'description');
65+
66+
// Number
67+
new NumberSchema('age', 'description');
68+
69+
// Boolean
70+
new BooleanSchema('is_active', 'description');
71+
72+
// Array
73+
new ArraySchema('tags', 'description', new StringSchema('tag', 'A single tag'));
74+
75+
// Enum
76+
new EnumSchema('status', 'description', ['draft', 'published', 'archived']);
77+
78+
// Object (nested structures)
79+
new ObjectSchema(
80+
name: 'user',
81+
description: 'User profile',
82+
properties: [
83+
new StringSchema('name', 'Full name'),
84+
new NumberSchema('age', 'User age'),
85+
new ArraySchema(
86+
name: 'hobbies',
87+
description: 'List of hobbies',
88+
items: new StringSchema('hobby', 'A hobby name')
89+
)
90+
],
91+
requiredFields: ['name']
92+
);
93+
```
94+
95+
## Provider-Specific Options
96+
97+
Providers may offer additional options for structured output. For example, OpenAI supports a "strict mode" for even tighter schema validation:
98+
99+
```php
100+
use EchoLabs\Prism\Enums\Provider;
101+
102+
$response = Prism::structured()
103+
->withProviderMeta(Provider::OpenAI, [
104+
'schema' => [
105+
'strict' => true
106+
]
107+
])
108+
// ... rest of your configuration
109+
```
110+
111+
> [!TIP]
112+
> Check the provider-specific documentation pages for additional options and features that might be available for structured output.
113+
114+
## Response Handling
115+
116+
When working with structured responses, you have access to both the structured data and metadata about the generation:
117+
118+
```php
119+
$response = Prism::structured()
120+
->withSchema($schema)
121+
->generate();
122+
123+
// Access the structured data as a PHP array
124+
$data = $response->object;
125+
126+
// Get the raw response text if needed
127+
echo $response->text;
128+
129+
// Check why the generation stopped
130+
echo $response->finishReason->name;
131+
132+
// Get token usage statistics
133+
echo "Prompt tokens: {$response->usage->promptTokens}";
134+
echo "Completion tokens: {$response->usage->completionTokens}";
135+
136+
// Access provider-specific response data
137+
$rawResponse = $response->response;
138+
```
139+
140+
> [!TIP]
141+
> Always validate the structured data before using it in your application:
142+
```php
143+
if ($response->object === null) {
144+
// Handle parsing failure
145+
}
146+
147+
if (!isset($response->object['required_field'])) {
148+
// Handle missing required data
149+
}
150+
```
151+
152+
## Common Settings
153+
154+
Structured output supports all the same options as text generation, including:
155+
- Temperature control
156+
- Maximum tokens
157+
- Message history
158+
- Tools and function calling
159+
- System prompts
160+
161+
See the [Text Generation](./text-generation.md) documentation for details on these common settings.
162+
163+
## Error Handling
164+
165+
When working with structured output, it's especially important to handle potential errors:
166+
167+
```php
168+
use EchoLabs\Prism\Exceptions\PrismException;
169+
170+
try {
171+
$response = Prism::structured()
172+
->using('anthropic', 'claude-3-sonnet')
173+
->withSchema($schema)
174+
->withPrompt('Generate product data')
175+
->generate();
176+
} catch (PrismException $e) {
177+
// Handle validation or generation errors
178+
Log::error('Structured generation failed:', [
179+
'error' => $e->getMessage()
180+
]);
181+
}
182+
```
183+
184+
> [!IMPORTANT]
185+
> Always validate the structured response before using it in your application, as different providers may have varying levels of schema adherence.

Diff for: docs/core-concepts/testing.md

+78-38
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@ use EchoLabs\Prism\ValueObjects\Usage;
1212
use EchoLabs\Prism\Enums\FinishReason;
1313
use EchoLabs\Prism\Providers\ProviderResponse;
1414

15-
public function test_can_generate_text(): void
16-
{
15+
it('can generate text', function () {
1716
// Create a fake provider response
1817
$fakeResponse = new ProviderResponse(
1918
text: 'Hello, I am Claude!',
@@ -33,17 +32,16 @@ public function test_can_generate_text(): void
3332
->generate();
3433

3534
// Make assertions
36-
$this->assertEquals('Hello, I am Claude!', $response->text);
37-
}
35+
expect($response->text)->toBe('Hello, I am Claude!');
36+
});
3837
```
3938

4039
## Testing Multiple Responses
4140

4241
When testing conversations or tool usage, you might need to simulate multiple responses:
4342

4443
```php
45-
public function test_can_handle_tool_calls(): void
46-
{
44+
it('can handle tool calls', function () {
4745
$responses = [
4846
new ProviderResponse(
4947
text: '',
@@ -68,24 +66,6 @@ public function test_can_handle_tool_calls(): void
6866
];
6967

7068
$fake = Prism::fake($responses);
71-
}
72-
```
73-
74-
## Assertions
75-
76-
Prism's fake implementation provides several helpful assertion methods:
77-
78-
```php
79-
// Assert specific prompt was sent
80-
$fake->assertPrompt('Who are you?');
81-
82-
// Assert number of calls made
83-
$fake->assertCallCount(2);
84-
85-
// Assert detailed request properties
86-
$fake->assertRequest(function ($requests) {
87-
$this->assertEquals('anthropic', $requests[0]->provider);
88-
$this->assertEquals('claude-3-sonnet', $requests[0]->model);
8969
});
9070
```
9171

@@ -94,8 +74,7 @@ $fake->assertRequest(function ($requests) {
9474
When testing tools, you'll want to verify both the tool calls and their results. Here's a complete example:
9575

9676
```php
97-
public function test_can_use_weather_tool(): void
98-
{
77+
it('can use weather tool', function () {
9978
// Define the expected tool call and response sequence
10079
$responses = [
10180
// First response: AI decides to use the weather tool
@@ -142,21 +121,82 @@ public function test_can_use_weather_tool(): void
142121
$fake->assertCallCount(2);
143122

144123
// Assert tool calls were made correctly
145-
$this->assertCount(1, $response->steps[0]->toolCalls);
146-
$this->assertEquals('weather', $response->steps[0]->toolCalls[0]->name);
147-
$this->assertEquals(['city' => 'Paris'], $response->steps[0]->toolCalls[0]->arguments());
124+
expect($response->steps[0]->toolCalls)->toHaveCount(1);
125+
expect($response->steps[0]->toolCalls[0]->name)->toBe('weather');
126+
expect($response->steps[0]->toolCalls[0]->arguments())->toBe(['city' => 'Paris']);
148127

149128
// Assert tool results were processed
150-
$this->assertCount(1, $response->toolResults);
151-
$this->assertEquals(
152-
'The weather in Paris is sunny with a temperature of 72°F',
153-
$response->toolResults[0]->result
154-
);
129+
expect($response->toolResults)->toHaveCount(1);
130+
expect($response->toolResults[0]->result)
131+
->toBe('The weather in Paris is sunny with a temperature of 72°F');
155132

156133
// Assert final response
157-
$this->assertEquals(
158-
'Based on current conditions, the weather in Paris is sunny with a temperature of 72°F.',
159-
$response->text
134+
expect($response->text)
135+
->toBe('Based on current conditions, the weather in Paris is sunny with a temperature of 72°F.');
136+
});
137+
```
138+
139+
## Testing Structured Output
140+
141+
```php
142+
use EchoLabs\Prism\Facades\Prism;
143+
use EchoLabs\Prism\ValueObjects\Usage;
144+
use EchoLabs\Prism\Enums\FinishReason;
145+
use EchoLabs\Prism\Providers\ProviderResponse;
146+
use EchoLabs\Prism\Schema\ObjectSchema;
147+
use EchoLabs\Prism\Schema\StringSchema;
148+
149+
it('can generate structured response', function () {
150+
$schema = new ObjectSchema(
151+
name: 'user',
152+
description: 'A user object, because we love organizing things!',
153+
properties: [
154+
new StringSchema('name', 'The user\'s name (hopefully not "test test")'),
155+
new StringSchema('bio', 'A brief bio (no novels, please)'),
156+
],
157+
requiredFields: ['name', 'bio']
158+
);
159+
160+
$fakeResponse = new ProviderResponse(
161+
text: json_encode([
162+
'name' => 'Alice Tester',
163+
'bio' => 'Professional bug hunter and code wrangler'
164+
]),
165+
toolCalls: [],
166+
usage: new Usage(10, 20),
167+
finishReason: FinishReason::Stop,
168+
response: ['id' => 'fake-1', 'model' => 'fake-model']
160169
);
161-
}
170+
171+
$fake = Prism::fake([$fakeResponse]);
172+
173+
$response = Prism::structured()
174+
->using('anthropic', 'claude-3-sonnet')
175+
->withPrompt('Generate a user profile')
176+
->withSchema($schema)
177+
->generate();
178+
179+
// Assertions
180+
expect($response->object)->toBeArray();
181+
expect($response->object['name'])->toBe('Alice Tester')
182+
expect($response->object['bio'])->toBe('Professional bug hunter and code wrangler');
183+
});
184+
```
185+
186+
## Assertions
187+
188+
Prism's fake implementation provides several helpful assertion methods:
189+
190+
```php
191+
// Assert specific prompt was sent
192+
$fake->assertPrompt('Who are you?');
193+
194+
// Assert number of calls made
195+
$fake->assertCallCount(2);
196+
197+
// Assert detailed request properties
198+
$fake->assertRequest(function ($requests) {
199+
expect($requests[0]->provider)->toBe('anthropic');
200+
expect($requests[0]->model)->toBe('claude-3-sonnet');
201+
});
162202
```

Diff for: docs/providers/openai.md

+11
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,17 @@ Tool::as('search') // [!code focus]
2424
]); // [!code focus]
2525
```
2626

27+
### Strict Structured Output Schemas
28+
29+
```php
30+
$response = Prism::structured()
31+
->withProviderMeta(Provider::OpenAI, [ // [!code focus]
32+
'schema' => [ // [!code focus]
33+
'strict' => true // [!code focus]
34+
] // [!code focus]
35+
]) // [!code focus]
36+
```
37+
2738
## Limitations
2839
### Tool Choice
2940

0 commit comments

Comments
 (0)