Skip to content

Commit 8050279

Browse files
authored
Support responseFormat in prompt.yml files (#71)
2 parents 8a3c805 + e96d38b commit 8050279

File tree

8 files changed

+523
-21
lines changed

8 files changed

+523
-21
lines changed

cmd/eval/eval_test.go

Lines changed: 85 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,7 @@ description: Testing JSON with failing evaluators
511511
model: openai/gpt-4o
512512
testData:
513513
- input: "hello"
514+
expected: "hello world"
514515
messages:
515516
- role: user
516517
content: "{{input}}"
@@ -553,18 +554,94 @@ evaluators:
553554

554555
output := out.String()
555556

557+
// Verify JSON structure
556558
var result EvaluationSummary
557559
err = json.Unmarshal([]byte(output), &result)
558560
require.NoError(t, err)
559561

560-
// Verify failing test is properly represented
561-
require.Equal(t, 1, result.Summary.TotalTests)
562-
require.Equal(t, 0, result.Summary.PassedTests)
563-
require.Equal(t, 1, result.Summary.FailedTests)
564-
require.Equal(t, 0.0, result.Summary.PassRate)
562+
// Verify JSON doesn't contain human-readable text
563+
require.NotContains(t, output, "Running evaluation:")
564+
})
565+
566+
t.Run("eval with responseFormat and jsonSchema", func(t *testing.T) {
567+
const yamlBody = `
568+
name: JSON Schema Evaluation
569+
description: Testing responseFormat and jsonSchema in eval
570+
model: openai/gpt-4o
571+
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
587+
testData:
588+
- input: "hello"
589+
expected: "hello world"
590+
messages:
591+
- role: user
592+
content: "Respond to: {{input}}"
593+
evaluators:
594+
- name: contains-message
595+
string:
596+
contains: "message"
597+
`
565598

566-
require.Len(t, result.TestResults, 1)
567-
require.False(t, result.TestResults[0].EvaluationResults[0].Passed)
568-
require.Equal(t, 0.0, result.TestResults[0].EvaluationResults[0].Score)
599+
tmpDir := t.TempDir()
600+
promptFile := filepath.Join(tmpDir, "test.prompt.yml")
601+
err := os.WriteFile(promptFile, []byte(yamlBody), 0644)
602+
require.NoError(t, err)
603+
604+
client := azuremodels.NewMockClient()
605+
var capturedRequest azuremodels.ChatCompletionOptions
606+
client.MockGetChatCompletionStream = func(ctx context.Context, req azuremodels.ChatCompletionOptions, org string) (*azuremodels.ChatCompletionResponse, error) {
607+
capturedRequest = req
608+
response := `{"message": "hello world", "confidence": 0.95}`
609+
reader := sse.NewMockEventReader([]azuremodels.ChatCompletion{
610+
{
611+
Choices: []azuremodels.ChatChoice{
612+
{
613+
Message: &azuremodels.ChatChoiceMessage{
614+
Content: &response,
615+
},
616+
},
617+
},
618+
},
619+
})
620+
return &azuremodels.ChatCompletionResponse{Reader: reader}, nil
621+
}
622+
623+
out := new(bytes.Buffer)
624+
cfg := command.NewConfig(out, out, client, true, 100)
625+
626+
cmd := NewEvalCommand(cfg)
627+
cmd.SetArgs([]string{promptFile})
628+
629+
err = cmd.Execute()
630+
require.NoError(t, err)
631+
632+
// Verify that responseFormat and jsonSchema were included in the request
633+
require.NotNil(t, capturedRequest.ResponseFormat)
634+
require.Equal(t, "json_schema", capturedRequest.ResponseFormat.Type)
635+
require.NotNil(t, capturedRequest.ResponseFormat.JsonSchema)
636+
637+
schema := *capturedRequest.ResponseFormat.JsonSchema
638+
require.Equal(t, "response_schema", schema["name"])
639+
require.Equal(t, true, schema["strict"])
640+
require.Contains(t, schema, "schema")
641+
642+
// Verify the test passed
643+
output := out.String()
644+
require.Contains(t, output, "✓ PASSED")
645+
require.Contains(t, output, "🎉 All tests passed!")
569646
})
570647
}

cmd/run/run.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -351,9 +351,17 @@ func NewRunCommand(cfg *command.Config) *cobra.Command {
351351
}
352352
}
353353

354-
req := azuremodels.ChatCompletionOptions{
355-
Messages: conversation.GetMessages(),
356-
Model: modelName,
354+
var req azuremodels.ChatCompletionOptions
355+
if pf != nil {
356+
// Use the prompt file's BuildChatCompletionOptions method to include responseFormat and jsonSchema
357+
req = pf.BuildChatCompletionOptions(conversation.GetMessages())
358+
// Override the model name if provided via CLI
359+
req.Model = modelName
360+
} else {
361+
req = azuremodels.ChatCompletionOptions{
362+
Messages: conversation.GetMessages(),
363+
Model: modelName,
364+
}
357365
}
358366

359367
mp.UpdateRequest(&req)

cmd/run/run_test.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,102 @@ messages:
331331
require.Equal(t, "System message", *capturedReq.Messages[0].Content)
332332
require.Equal(t, "User message", *capturedReq.Messages[1].Content)
333333
})
334+
335+
t.Run("--file with responseFormat and jsonSchema", func(t *testing.T) {
336+
const yamlBody = `
337+
name: JSON Schema Test
338+
description: Test responseFormat and jsonSchema
339+
model: openai/test-model
340+
responseFormat: json_schema
341+
jsonSchema:
342+
name: person_schema
343+
strict: true
344+
schema:
345+
type: object
346+
properties:
347+
name:
348+
type: string
349+
description: The name
350+
age:
351+
type: integer
352+
description: The age
353+
required:
354+
- name
355+
- age
356+
additionalProperties: false
357+
messages:
358+
- role: system
359+
content: You are a helpful assistant.
360+
- role: user
361+
content: "Generate a person"
362+
`
363+
364+
tmp, err := os.CreateTemp(t.TempDir(), "*.prompt.yml")
365+
require.NoError(t, err)
366+
_, err = tmp.WriteString(yamlBody)
367+
require.NoError(t, err)
368+
require.NoError(t, tmp.Close())
369+
370+
client := azuremodels.NewMockClient()
371+
modelSummary := &azuremodels.ModelSummary{
372+
Name: "test-model",
373+
Publisher: "openai",
374+
Task: "chat-completion",
375+
}
376+
client.MockListModels = func(ctx context.Context) ([]*azuremodels.ModelSummary, error) {
377+
return []*azuremodels.ModelSummary{modelSummary}, nil
378+
}
379+
380+
var capturedRequest azuremodels.ChatCompletionOptions
381+
client.MockGetChatCompletionStream = func(ctx context.Context, req azuremodels.ChatCompletionOptions, org string) (*azuremodels.ChatCompletionResponse, error) {
382+
capturedRequest = req
383+
reply := "hello this is a test response"
384+
reader := sse.NewMockEventReader([]azuremodels.ChatCompletion{
385+
{
386+
Choices: []azuremodels.ChatChoice{
387+
{
388+
Message: &azuremodels.ChatChoiceMessage{
389+
Content: &reply,
390+
},
391+
},
392+
},
393+
},
394+
})
395+
return &azuremodels.ChatCompletionResponse{Reader: reader}, nil
396+
}
397+
398+
out := new(bytes.Buffer)
399+
cfg := command.NewConfig(out, out, client, true, 100)
400+
401+
cmd := NewRunCommand(cfg)
402+
cmd.SetArgs([]string{"--file", tmp.Name()})
403+
404+
err = cmd.Execute()
405+
require.NoError(t, err)
406+
407+
// Verify that responseFormat and jsonSchema were included in the request
408+
require.NotNil(t, capturedRequest.ResponseFormat)
409+
require.Equal(t, "json_schema", capturedRequest.ResponseFormat.Type)
410+
require.NotNil(t, capturedRequest.ResponseFormat.JsonSchema)
411+
412+
schema := *capturedRequest.ResponseFormat.JsonSchema
413+
require.Contains(t, schema, "name")
414+
require.Contains(t, schema, "schema")
415+
require.Equal(t, "person_schema", schema["name"])
416+
417+
schemaContent := schema["schema"].(map[string]interface{})
418+
require.Equal(t, "object", schemaContent["type"])
419+
require.Contains(t, schemaContent, "properties")
420+
require.Contains(t, schemaContent, "required")
421+
422+
properties := schemaContent["properties"].(map[string]interface{})
423+
require.Contains(t, properties, "name")
424+
require.Contains(t, properties, "age")
425+
426+
required := schemaContent["required"].([]interface{})
427+
require.Contains(t, required, "name")
428+
require.Contains(t, required, "age")
429+
})
334430
}
335431

336432
func TestParseTemplateVariables(t *testing.T) {

examples/json_response_prompt.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
name: JSON Response Example
2+
description: Example prompt demonstrating responseFormat with json
3+
model: openai/gpt-4o
4+
responseFormat: json_object
5+
messages:
6+
- role: system
7+
content: You are a helpful assistant that responds in JSON format.
8+
- role: user
9+
content: "Provide a summary of {{topic}} in JSON format with title, description, and key_points array."
10+
testData:
11+
- topic: "artificial intelligence"
12+
- topic: "climate change"
13+
evaluators:
14+
- name: contains-json-structure
15+
string:
16+
contains: "{"
17+
- name: has-title
18+
string:
19+
contains: "title"

examples/json_schema_prompt.yml

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
name: JSON Schema Response Example
2+
description: Example prompt demonstrating responseFormat and jsonSchema usage
3+
model: openai/gpt-4o
4+
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
48+
messages:
49+
- role: system
50+
content: You are a helpful assistant that provides structured information about people.
51+
- role: user
52+
content: "Generate information for a person named {{name}} who is {{age}} years old."
53+
testData:
54+
- name: "Alice Johnson"
55+
age: "30"
56+
- name: "Bob Smith"
57+
age: "25"
58+
evaluators:
59+
- name: has-required-fields
60+
string:
61+
contains: "name"
62+
- name: valid-json-structure
63+
string:
64+
contains: "age"

internal/azuremodels/types.go

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,23 @@ import (
66
"github.com/github/gh-models/internal/sse"
77
)
88

9+
// ChatCompletionOptions represents available options for a chat completion request.
10+
type ChatCompletionOptions struct {
11+
MaxTokens *int `json:"max_tokens,omitempty"`
12+
Messages []ChatMessage `json:"messages"`
13+
Model string `json:"model"`
14+
Stream bool `json:"stream,omitempty"`
15+
Temperature *float64 `json:"temperature,omitempty"`
16+
TopP *float64 `json:"top_p,omitempty"`
17+
ResponseFormat *ResponseFormat `json:"response_format,omitempty"`
18+
}
19+
20+
// ResponseFormat represents the response format specification
21+
type ResponseFormat struct {
22+
Type string `json:"type"`
23+
JsonSchema *map[string]interface{} `json:"json_schema,omitempty"`
24+
}
25+
926
// ChatMessageRole represents the role of a chat message.
1027
type ChatMessageRole string
1128

@@ -24,16 +41,6 @@ type ChatMessage struct {
2441
Role ChatMessageRole `json:"role"`
2542
}
2643

27-
// ChatCompletionOptions represents available options for a chat completion request.
28-
type ChatCompletionOptions struct {
29-
MaxTokens *int `json:"max_tokens,omitempty"`
30-
Messages []ChatMessage `json:"messages"`
31-
Model string `json:"model"`
32-
Stream bool `json:"stream,omitempty"`
33-
Temperature *float64 `json:"temperature,omitempty"`
34-
TopP *float64 `json:"top_p,omitempty"`
35-
}
36-
3744
// ChatChoiceMessage is a message from a choice in a chat conversation.
3845
type ChatChoiceMessage struct {
3946
Content *string `json:"content,omitempty"`

0 commit comments

Comments
 (0)