Skip to content

Commit a4f4c10

Browse files
committed
enforce json output with structured outputs
1 parent a9decda commit a4f4c10

File tree

1 file changed

+69
-16
lines changed

1 file changed

+69
-16
lines changed

Diff for: llm/service/custom_llm.go

+69-16
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@ import (
1818
"github.com/getsavvyinc/savvy-cli/model"
1919
"github.com/getsavvyinc/savvy-cli/slice"
2020
"github.com/sashabaranov/go-openai"
21+
"github.com/sashabaranov/go-openai/jsonschema"
2122
"github.com/sethvargo/go-retry"
2223
)
2324

2425
type customSvc struct {
25-
cl *openai.Client
26+
cl *openai.Client
27+
modelName string
2628
}
2729

2830
func newCustomService(cfg *config.Config) Service {
@@ -35,7 +37,8 @@ func newCustomService(cfg *config.Config) Service {
3537
openaiClient := openai.NewClientWithConfig(clientConfig)
3638

3739
return &customSvc{
38-
cl: openaiClient,
40+
cl: openaiClient,
41+
modelName: "llama3.2:latest",
3942
}
4043
}
4144

@@ -105,21 +108,20 @@ You will generate the Title for the runbook and a meaningful description for eac
105108
106109
The Title must be a short single sentences tha begins with the phrase: "How To". The title must be short and concise and must describe the purpose of the runbook. Do not make the title overly general.
107110
108-
When calling functions that use the command/command_id pairs, ensure that the command_id is passed as the code_id and the command is passed as the code.
109-
110111
The Description for each command must be short and concise. Use simple words. Limit the description to 1-2 sentences.
111112
112113
Do not include filler words like: "This command is used to" in the description. Get straight to the point.
113114
114115
Take a deep breath, do not rush, and take your time to generate the Title and Descriptions for each command. Do not hallucinate or make things up. Be as accurate as possible.
115116
116-
The output should be a runbook in json format. The runbook has a title and a list of steps according to the json schema below:
117+
118+
Generate json output that adheres to the following schema:
117119
{
118120
title: "describe the purpose and theme of the runbook",
119121
steps: [
120122
{
121-
code: "command, unchanged from the input prompt",
122-
code_id: "command_id, unchanged from the input prompt",
123+
command: "command, unchanged from the input prompt",
124+
command_id: "command_id, unchanged from the input prompt",
123125
description: "short, conscise, and helpful description of the command."
124126
}
125127
}
@@ -130,6 +132,51 @@ The output should be a runbook in json format. The runbook has a title and a lis
130132

131133
var generateRunbookTitleAndDescriptionsPromptTemplate = template.Must(template.New(genRunbookTemplateName).Parse(generateTitleAndDescriptionPrompt))
132134

135+
var (
136+
GenerateRunbookSchema = jsonschema.Definition{
137+
Type: jsonschema.Object,
138+
Properties: map[string]jsonschema.Definition{
139+
"title": jsonschema.Definition{
140+
Type: jsonschema.String,
141+
Description: "Title of the runbook",
142+
},
143+
"steps": jsonschema.Definition{
144+
Type: jsonschema.Array,
145+
Description: "Steps in the runbook",
146+
Items: &jsonschema.Definition{
147+
Type: jsonschema.Object,
148+
Properties: map[string]jsonschema.Definition{
149+
"command": jsonschema.Definition{
150+
Type: jsonschema.String,
151+
Description: "command passed in to the prompt. This should be unchanged from the input prompt.",
152+
},
153+
"command_id": jsonschema.Definition{
154+
Type: jsonschema.String,
155+
Description: "ID of the command. This should be unchanged from the input prompt.",
156+
},
157+
"description": jsonschema.Definition{
158+
Type: jsonschema.String,
159+
Description: "Short, conscise, and helpful description of the command",
160+
},
161+
},
162+
},
163+
},
164+
},
165+
Required: []string{"title", "steps"},
166+
}
167+
168+
GenerateRunbookFunc = &openai.FunctionDefinition{
169+
Name: "generate_runbook_title_and_descriptions",
170+
Description: "Generate a runbook title and descriptions for each command in the runbook",
171+
Parameters: GenerateRunbookSchema,
172+
}
173+
174+
GenerateRunbookFuncTool = openai.Tool{
175+
Type: openai.ToolTypeFunction,
176+
Function: GenerateRunbookFunc,
177+
}
178+
)
179+
133180
// CommandAndID is a struct that holds a command and its corresponding command_id
134181
// CommandID is useful in the prompt to ensure that the llm doesn't change the order or omit/hallucinate any command.
135182
type CommandAndID struct {
@@ -155,25 +202,31 @@ func (c *customSvc) generateRunbookTitleAndDescriptions(ctx context.Context, com
155202
b := retry.NewFibonacci(1 * time.Second)
156203
b = retry.WithMaxRetries(3, b)
157204

158-
var groqResponse openai.ChatCompletionResponse
205+
var chatResponse openai.ChatCompletionResponse
159206
var gerr error
160207

161208
// retry on bad request errors
162209
if err := retry.Do(ctx, b, func(ctx context.Context) error {
163-
groqResponse, gerr = c.cl.CreateChatCompletion(ctx, openai.ChatCompletionRequest{
210+
chatResponse, gerr = c.cl.CreateChatCompletion(ctx, openai.ChatCompletionRequest{
164211
Messages: []openai.ChatCompletionMessage{prompt},
165-
Model: "llama-3.1-70b-versatile",
212+
Model: c.modelName,
166213
MaxTokens: 2500,
167214
Temperature: 0.3,
168215
ResponseFormat: &openai.ChatCompletionResponseFormat{
169-
Type: openai.ChatCompletionResponseFormatTypeJSONObject,
216+
Type: openai.ChatCompletionResponseFormatTypeJSONSchema,
217+
JSONSchema: &openai.ChatCompletionResponseFormatJSONSchema{
218+
Name: "generate_runbook_title_and_descriptions",
219+
Description: "Generate a runbook title and descriptions for each command in the runbook",
220+
Schema: &GenerateRunbookSchema,
221+
Strict: true,
222+
},
170223
},
171224
})
172225

173226
var oaiErr *openai.APIError
174-
// Sometimes, groq returns a 400 error, as it can't force a json response.
227+
// Sometimes, the api returns a 400 error, as it can't force a json response.
175228
if errors.As(gerr, &oaiErr) && oaiErr.HTTPStatusCode == http.StatusBadRequest {
176-
log.Printf("retry: bad request to groq: %v\n", oaiErr)
229+
log.Printf("retry: bad request to custom llm: %v\n", oaiErr)
177230
return retry.RetryableError(oaiErr)
178231
}
179232
return gerr
@@ -182,12 +235,12 @@ func (c *customSvc) generateRunbookTitleAndDescriptions(ctx context.Context, com
182235
return nil, err
183236
}
184237

185-
if gerr != nil || len(groqResponse.Choices) != 1 {
238+
if gerr != nil || len(chatResponse.Choices) != 1 {
186239
return nil, fmt.Errorf("Completion error: err:%v len(choices):%v\n", gerr,
187-
len(groqResponse.Choices))
240+
len(chatResponse.Choices))
188241
}
189242

190-
msg := groqResponse.Choices[0].Message.Content
243+
msg := chatResponse.Choices[0].Message.Content
191244
if len(msg) == 0 {
192245
return nil, fmt.Errorf("Completion error: len(msg): %v\n", len(msg))
193246
}

0 commit comments

Comments
 (0)