Skip to content

Commit 79f1655

Browse files
authored
Use string format for jsonSchema (#73)
2 parents 3bc7b92 + ec3ceed commit 79f1655

File tree

5 files changed

+148
-138
lines changed

5 files changed

+148
-138
lines changed

cmd/eval/eval_test.go

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -569,21 +569,7 @@ name: JSON Schema Evaluation
569569
description: Testing responseFormat and jsonSchema in eval
570570
model: openai/gpt-4o
571571
responseFormat: json_schema
572-
jsonSchema:
573-
name: response_schema
574-
strict: true
575-
schema:
576-
type: object
577-
properties:
578-
message:
579-
type: string
580-
description: The response message
581-
confidence:
582-
type: number
583-
description: Confidence score
584-
required:
585-
- message
586-
additionalProperties: false
572+
jsonSchema: '{"name": "response_schema", "strict": true, "schema": {"type": "object", "properties": {"message": {"type": "string", "description": "The response message"}, "confidence": {"type": "number", "description": "Confidence score"}}, "required": ["message"], "additionalProperties": false}}'
587573
testData:
588574
- input: "hello"
589575
expected: "hello world"

cmd/run/run_test.go

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -341,22 +341,7 @@ name: JSON Schema Test
341341
description: Test responseFormat and jsonSchema
342342
model: openai/test-model
343343
responseFormat: json_schema
344-
jsonSchema:
345-
name: person_schema
346-
strict: true
347-
schema:
348-
type: object
349-
properties:
350-
name:
351-
type: string
352-
description: The name
353-
age:
354-
type: integer
355-
description: The age
356-
required:
357-
- name
358-
- age
359-
additionalProperties: false
344+
jsonSchema: '{"name": "person_schema", "strict": true, "schema": {"type": "object", "properties": {"name": {"type": "string", "description": "The name"}, "age": {"type": "integer", "description": "The age"}}, "required": ["name", "age"], "additionalProperties": false}}'
360345
messages:
361346
- role: system
362347
content: You are a helpful assistant.

examples/json_schema_prompt.yml

Lines changed: 41 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,52 @@
11
name: JSON Schema Response Example
22
description: Example prompt demonstrating responseFormat and jsonSchema usage
3-
model: openai/gpt-4o
3+
model: openai/gpt-4o-mini
44
responseFormat: json_schema
5-
jsonSchema:
6-
name: Person Information Schema
7-
strict: true
8-
schema:
9-
type: object
10-
description: A structured response containing person information
11-
properties:
12-
name:
13-
type: string
14-
description: The full name of the person
15-
age:
16-
type: integer
17-
description: The age of the person in years
18-
minimum: 0
19-
maximum: 150
20-
email:
21-
type: string
22-
description: The email address of the person
23-
format: email
24-
skills:
25-
type: array
26-
description: A list of skills the person has
27-
items:
28-
type: string
29-
address:
30-
type: object
31-
description: The person's address
32-
properties:
33-
street:
34-
type: string
35-
description: Street address
36-
city:
37-
type: string
38-
description: City name
39-
country:
40-
type: string
41-
description: Country name
42-
required:
43-
- city
44-
- country
45-
required:
46-
- name
47-
- age
5+
jsonSchema: |-
6+
{
7+
"name": "animal_description",
8+
"strict": true,
9+
"schema": {
10+
"type": "object",
11+
"properties": {
12+
"name": {
13+
"type": "string",
14+
"description": "The name of the animal"
15+
},
16+
"habitat": {
17+
"type": "string",
18+
"description": "The habitat where the animal lives"
19+
},
20+
"diet": {
21+
"type": "string",
22+
"description": "What the animal eats",
23+
"enum": ["carnivore", "herbivore", "omnivore"]
24+
},
25+
"characteristics": {
26+
"type": "array",
27+
"description": "Key characteristics of the animal",
28+
"items": {
29+
"type": "string"
30+
}
31+
}
32+
},
33+
"required": ["name", "habitat", "diet"],
34+
"additionalProperties": false
35+
}
36+
}
4837
messages:
4938
- role: system
50-
content: You are a helpful assistant that provides structured information about people.
39+
content: You are a helpful assistant that provides detailed information about animals.
5140
- role: user
52-
content: "Generate information for a person named {{name}} who is {{age}} years old."
41+
content: "Describe a {{animal}} in detail."
5342
testData:
54-
- name: "Alice Johnson"
55-
age: "30"
56-
- name: "Bob Smith"
57-
age: "25"
43+
- animal: "dog"
44+
- animal: "cat"
45+
- animal: "elephant"
5846
evaluators:
59-
- name: has-required-fields
47+
- name: has-name
6048
string:
6149
contains: "name"
62-
- name: valid-json-structure
50+
- name: has-habitat
6351
string:
64-
contains: "age"
52+
contains: "habitat"

pkg/prompt/prompt.go

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
package prompt
33

44
import (
5+
"encoding/json"
56
"fmt"
67
"os"
78
"strings"
@@ -69,9 +70,32 @@ type Choice struct {
6970

7071
// JsonSchema represents a JSON schema for structured responses
7172
type JsonSchema struct {
72-
Name string `yaml:"name" json:"name"`
73-
Strict *bool `yaml:"strict,omitempty" json:"strict,omitempty"`
74-
Schema map[string]interface{} `yaml:"schema" json:"schema"`
73+
Raw string
74+
Parsed map[string]interface{}
75+
}
76+
77+
// UnmarshalYAML implements custom YAML unmarshaling for JsonSchema
78+
// Only supports JSON string format
79+
func (js *JsonSchema) UnmarshalYAML(node *yaml.Node) error {
80+
// Only support string nodes (JSON format)
81+
if node.Kind != yaml.ScalarNode {
82+
return fmt.Errorf("jsonSchema must be a JSON string")
83+
}
84+
85+
var jsonStr string
86+
if err := node.Decode(&jsonStr); err != nil {
87+
return err
88+
}
89+
90+
// Parse and validate the JSON schema
91+
var parsed map[string]interface{}
92+
if err := json.Unmarshal([]byte(jsonStr), &parsed); err != nil {
93+
return fmt.Errorf("invalid JSON in jsonSchema: %w", err)
94+
}
95+
96+
js.Raw = jsonStr
97+
js.Parsed = parsed
98+
return nil
7599
}
76100

77101
// LoadFromFile loads and parses a prompt file from the given path
@@ -105,16 +129,18 @@ func (f *File) validateResponseFormat() error {
105129
return fmt.Errorf("invalid responseFormat: %s. Must be 'text', 'json_object', or 'json_schema'", *f.ResponseFormat)
106130
}
107131

108-
// If responseFormat is "json_schema", jsonSchema must be provided with required fields
132+
// If responseFormat is "json_schema", jsonSchema must be provided
109133
if *f.ResponseFormat == "json_schema" {
110134
if f.JsonSchema == nil {
111135
return fmt.Errorf("jsonSchema is required when responseFormat is 'json_schema'")
112136
}
113-
if f.JsonSchema.Name == "" {
114-
return fmt.Errorf("jsonSchema.name is required when responseFormat is 'json_schema'")
137+
138+
// Check for required fields in the already parsed schema
139+
if _, ok := f.JsonSchema.Parsed["name"]; !ok {
140+
return fmt.Errorf("jsonSchema must contain 'name' field")
115141
}
116-
if f.JsonSchema.Schema == nil {
117-
return fmt.Errorf("jsonSchema.schema is required when responseFormat is 'json_schema'")
142+
if _, ok := f.JsonSchema.Parsed["schema"]; !ok {
143+
return fmt.Errorf("jsonSchema must contain 'schema' field")
118144
}
119145
}
120146

@@ -176,7 +202,6 @@ func (f *File) BuildChatCompletionOptions(messages []azuremodels.ChatMessage) az
176202
Stream: false,
177203
}
178204

179-
// Apply model parameters
180205
if f.ModelParameters.MaxTokens != nil {
181206
req.MaxTokens = f.ModelParameters.MaxTokens
182207
}
@@ -187,20 +212,12 @@ func (f *File) BuildChatCompletionOptions(messages []azuremodels.ChatMessage) az
187212
req.TopP = f.ModelParameters.TopP
188213
}
189214

190-
// Apply response format
191215
if f.ResponseFormat != nil {
192216
responseFormat := &azuremodels.ResponseFormat{
193217
Type: *f.ResponseFormat,
194218
}
195219
if f.JsonSchema != nil {
196-
// Convert JsonSchema to map[string]interface{}
197-
schemaMap := make(map[string]interface{})
198-
schemaMap["name"] = f.JsonSchema.Name
199-
if f.JsonSchema.Strict != nil {
200-
schemaMap["strict"] = *f.JsonSchema.Strict
201-
}
202-
schemaMap["schema"] = f.JsonSchema.Schema
203-
responseFormat.JsonSchema = &schemaMap
220+
responseFormat.JsonSchema = &f.JsonSchema.Parsed
204221
}
205222
req.ResponseFormat = responseFormat
206223
}

pkg/prompt/prompt_test.go

Lines changed: 70 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package prompt
22

33
import (
4+
"encoding/json"
45
"os"
56
"path/filepath"
67
"testing"
@@ -139,27 +140,35 @@ messages:
139140
require.Nil(t, promptFile.JsonSchema)
140141
})
141142

142-
t.Run("loads prompt file with responseFormat json_schema and jsonSchema", func(t *testing.T) {
143+
t.Run("loads prompt file with responseFormat json_schema and jsonSchema as JSON string", func(t *testing.T) {
143144
const yamlBody = `
144-
name: JSON Schema Response Format Test
145-
description: Test with JSON schema response format
145+
name: JSON Schema String Format Test
146+
description: Test with JSON schema as JSON string
146147
model: openai/gpt-4o
147148
responseFormat: json_schema
148-
jsonSchema:
149-
name: person_info
150-
strict: true
151-
schema:
152-
type: object
153-
properties:
154-
name:
155-
type: string
156-
description: The name of the person
157-
age:
158-
type: integer
159-
description: The age of the person
160-
required:
161-
- name
162-
additionalProperties: false
149+
jsonSchema: |-
150+
{
151+
"name": "describe_animal",
152+
"strict": true,
153+
"schema": {
154+
"type": "object",
155+
"properties": {
156+
"name": {
157+
"type": "string",
158+
"description": "The name of the animal"
159+
},
160+
"habitat": {
161+
"type": "string",
162+
"description": "The habitat the animal lives in"
163+
}
164+
},
165+
"additionalProperties": false,
166+
"required": [
167+
"name",
168+
"habitat"
169+
]
170+
}
171+
}
163172
messages:
164173
- role: user
165174
content: "Hello"
@@ -175,10 +184,26 @@ messages:
175184
require.NotNil(t, promptFile.ResponseFormat)
176185
require.Equal(t, "json_schema", *promptFile.ResponseFormat)
177186
require.NotNil(t, promptFile.JsonSchema)
178-
require.Equal(t, "person_info", promptFile.JsonSchema.Name)
179-
require.True(t, *promptFile.JsonSchema.Strict)
180-
require.Contains(t, promptFile.JsonSchema.Schema, "type")
181-
require.Contains(t, promptFile.JsonSchema.Schema, "properties")
187+
188+
// Verify the schema contents using the already parsed data
189+
schema := promptFile.JsonSchema.Parsed
190+
require.Equal(t, "describe_animal", schema["name"])
191+
require.Equal(t, true, schema["strict"])
192+
require.Contains(t, schema, "schema")
193+
194+
// Verify the nested schema structure
195+
nestedSchema := schema["schema"].(map[string]interface{})
196+
require.Equal(t, "object", nestedSchema["type"])
197+
require.Contains(t, nestedSchema, "properties")
198+
require.Contains(t, nestedSchema, "required")
199+
200+
properties := nestedSchema["properties"].(map[string]interface{})
201+
require.Contains(t, properties, "name")
202+
require.Contains(t, properties, "habitat")
203+
204+
required := nestedSchema["required"].([]interface{})
205+
require.Contains(t, required, "name")
206+
require.Contains(t, required, "habitat")
182207
})
183208

184209
t.Run("validates invalid responseFormat", func(t *testing.T) {
@@ -224,23 +249,32 @@ messages:
224249
})
225250

226251
t.Run("BuildChatCompletionOptions includes responseFormat and jsonSchema", func(t *testing.T) {
252+
jsonSchemaStr := `{
253+
"name": "test_schema",
254+
"strict": true,
255+
"schema": {
256+
"type": "object",
257+
"properties": {
258+
"name": {
259+
"type": "string",
260+
"description": "The name"
261+
}
262+
},
263+
"required": ["name"]
264+
}
265+
}`
266+
227267
promptFile := &File{
228268
Model: "openai/gpt-4o",
229269
ResponseFormat: func() *string { s := "json_schema"; return &s }(),
230-
JsonSchema: &JsonSchema{
231-
Name: "test_schema",
232-
Strict: func() *bool { b := true; return &b }(),
233-
Schema: map[string]interface{}{
234-
"type": "object",
235-
"properties": map[string]interface{}{
236-
"name": map[string]interface{}{
237-
"type": "string",
238-
"description": "The name",
239-
},
240-
},
241-
"required": []string{"name"},
242-
},
243-
},
270+
JsonSchema: func() *JsonSchema {
271+
js := &JsonSchema{Raw: jsonSchemaStr}
272+
err := json.Unmarshal([]byte(jsonSchemaStr), &js.Parsed)
273+
if err != nil {
274+
t.Fatal(err)
275+
}
276+
return js
277+
}(),
244278
}
245279

246280
messages := []azuremodels.ChatMessage{

0 commit comments

Comments
 (0)