Skip to content

Commit 83f7164

Browse files
tzolovsobychacko
authored andcommitted
Extract ResponseFormat to standalone class
- Extracts ResponseFormat from being a nested record in OpenAiApi to a dedicated class with builder pattern support. - Resolve the issue with constructor bindings for the Boog property binding. - Re-enables previously disabled response format integration tests. - Add checkstyle changes - Add schema field in ResponseFormat and set jsonSchema via the setter for schema, this way schema set via a Boot property also sets the correct JsonSchema - Add default constructors in ResponseFormat and JsonSchema Resolves #1681
1 parent fb65ed0 commit 83f7164

File tree

6 files changed

+293
-80
lines changed

6 files changed

+293
-80
lines changed

models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiChatOptions.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@
3434
import org.springframework.ai.model.function.FunctionCallback;
3535
import org.springframework.ai.model.function.FunctionCallingOptions;
3636
import org.springframework.ai.openai.api.OpenAiApi;
37-
import org.springframework.ai.openai.api.OpenAiApi.ChatCompletionRequest.ResponseFormat;
3837
import org.springframework.ai.openai.api.OpenAiApi.ChatCompletionRequest.StreamOptions;
3938
import org.springframework.ai.openai.api.OpenAiApi.ChatCompletionRequest.ToolChoiceBuilder;
39+
import org.springframework.ai.openai.api.ResponseFormat;
4040
import org.springframework.util.Assert;
4141

4242
/**

models/spring-ai-openai/src/main/java/org/springframework/ai/openai/api/OpenAiApi.java

Lines changed: 0 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@
4242
import org.springframework.util.CollectionUtils;
4343
import org.springframework.util.LinkedMultiValueMap;
4444
import org.springframework.util.MultiValueMap;
45-
import org.springframework.util.StringUtils;
4645
import org.springframework.web.client.ResponseErrorHandler;
4746
import org.springframework.web.client.RestClient;
4847
import org.springframework.web.reactive.function.client.WebClient;
@@ -870,74 +869,6 @@ public static Object FUNCTION(String functionName) {
870869
}
871870
}
872871

873-
/**
874-
* An object specifying the format that the model must output.
875-
* @param type Must be one of 'text' or 'json_object'.
876-
* @param jsonSchema JSON schema object that describes the format of the JSON object.
877-
* Only applicable when type is 'json_schema'.
878-
*/
879-
@JsonInclude(Include.NON_NULL)
880-
public record ResponseFormat(
881-
@JsonProperty("type") Type type,
882-
@JsonProperty("json_schema") JsonSchema jsonSchema) {
883-
884-
public ResponseFormat(Type type) {
885-
this(type, (JsonSchema) null);
886-
}
887-
888-
public ResponseFormat(Type type, String schema) {
889-
this(type, "custom_schema", schema, true);
890-
}
891-
892-
public ResponseFormat(Type type, String name, String schema, Boolean strict) {
893-
this(type, StringUtils.hasText(schema) ? new JsonSchema(name, schema, strict) : null);
894-
}
895-
896-
public enum Type {
897-
/**
898-
* Generates a text response. (default)
899-
*/
900-
@JsonProperty("text")
901-
TEXT,
902-
903-
/**
904-
* Enables JSON mode, which guarantees the message
905-
* the model generates is valid JSON.
906-
*/
907-
@JsonProperty("json_object")
908-
JSON_OBJECT,
909-
910-
/**
911-
* Enables Structured Outputs which guarantees the model
912-
* will match your supplied JSON schema.
913-
*/
914-
@JsonProperty("json_schema")
915-
JSON_SCHEMA
916-
}
917-
918-
/**
919-
* JSON schema object that describes the format of the JSON object.
920-
* Applicable for the 'json_schema' type only.
921-
* @param name The name of the schema.
922-
* @param schema The JSON schema object that describes the format of the JSON object.
923-
* @param strict If true, the model will only generate outputs that match the schema.
924-
*/
925-
@JsonInclude(Include.NON_NULL)
926-
public record JsonSchema(
927-
@JsonProperty("name") String name,
928-
@JsonProperty("schema") Map<String, Object> schema,
929-
@JsonProperty("strict") Boolean strict) {
930-
931-
public JsonSchema(String name, String schema) {
932-
this(name, ModelOptionsUtils.jsonToMap(schema), true);
933-
}
934-
935-
public JsonSchema(String name, String schema, Boolean strict) {
936-
this(StringUtils.hasText(name) ? name : "custom_schema", ModelOptionsUtils.jsonToMap(schema), strict);
937-
}
938-
}
939-
940-
}
941872
/**
942873
* @param includeUsage If set, an additional chunk will be streamed
943874
* before the data: [DONE] message. The usage field on this chunk
Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
/*
2+
* Copyright 2023-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.ai.openai.api;
18+
19+
import java.util.Map;
20+
import java.util.Objects;
21+
22+
import com.fasterxml.jackson.annotation.JsonInclude;
23+
import com.fasterxml.jackson.annotation.JsonInclude.Include;
24+
import com.fasterxml.jackson.annotation.JsonProperty;
25+
26+
import org.springframework.ai.model.ModelOptionsUtils;
27+
import org.springframework.util.StringUtils;
28+
29+
/**
30+
* An object specifying the format that the model must output.
31+
*
32+
* Setting the type to JSON_SCHEMA, enables Structured Outputs which ensures the model
33+
* will match your supplied JSON schema. Learn more in the
34+
* <a href="https://platform.openai.com/docs/guides/structured-outputs"> Structured
35+
* Outputs guide.</a <br/>
36+
*
37+
* References: <a href=
38+
* "https://platform.openai.com/docs/api-reference/chat/create#chat-create-response_format">OpenAi
39+
* API - ResponseFormat</a>,
40+
* <a href="https://platform.openai.com/docs/guides/structured-outputs#json-mode">JSON
41+
* Mode</a>, <a href=
42+
* "https://platform.openai.com/docs/guides/structured-outputs#structured-outputs-vs-json-mode">Structured
43+
* Outputs vs JSON mode</a>
44+
*
45+
* @author Christian Tzolov
46+
* @since 1.0.0
47+
*/
48+
49+
@JsonInclude(Include.NON_NULL)
50+
public class ResponseFormat {
51+
52+
/**
53+
* Type Must be one of 'text', 'json_object' or 'json_schema'.
54+
*/
55+
@JsonProperty("type")
56+
private Type type;
57+
58+
/**
59+
* JSON schema object that describes the format of the JSON object. Only applicable
60+
* when type is 'json_schema'.
61+
*/
62+
@JsonProperty("json_schema")
63+
private JsonSchema jsonSchema = null;
64+
65+
private String schema;
66+
67+
public ResponseFormat() {
68+
69+
}
70+
71+
public Type getType() {
72+
return this.type;
73+
}
74+
75+
public void setType(Type type) {
76+
this.type = type;
77+
}
78+
79+
public JsonSchema getJsonSchema() {
80+
return this.jsonSchema;
81+
}
82+
83+
public void setJsonSchema(JsonSchema jsonSchema) {
84+
this.jsonSchema = jsonSchema;
85+
}
86+
87+
public String getSchema() {
88+
return this.schema;
89+
}
90+
91+
public void setSchema(String schema) {
92+
this.schema = schema;
93+
if (schema != null) {
94+
this.jsonSchema = JsonSchema.builder().schema(schema).strict(true).build();
95+
}
96+
}
97+
98+
private ResponseFormat(Type type, JsonSchema jsonSchema) {
99+
this.type = type;
100+
this.jsonSchema = jsonSchema;
101+
}
102+
103+
public ResponseFormat(Type type, String schema) {
104+
this(type, StringUtils.hasText(schema) ? JsonSchema.builder().schema(schema).strict(true).build() : null);
105+
}
106+
107+
public static Builder builder() {
108+
return new Builder();
109+
}
110+
111+
@Override
112+
public boolean equals(Object o) {
113+
if (this == o) {
114+
return true;
115+
}
116+
if (o == null || getClass() != o.getClass()) {
117+
return false;
118+
}
119+
ResponseFormat that = (ResponseFormat) o;
120+
return this.type == that.type && Objects.equals(this.jsonSchema, that.jsonSchema);
121+
}
122+
123+
@Override
124+
public int hashCode() {
125+
return Objects.hash(this.type, this.jsonSchema);
126+
}
127+
128+
@Override
129+
public String toString() {
130+
return "ResponseFormat{" + "type=" + this.type + ", jsonSchema=" + this.jsonSchema + '}';
131+
}
132+
133+
public static final class Builder {
134+
135+
private Type type;
136+
137+
private JsonSchema jsonSchema;
138+
139+
private Builder() {
140+
}
141+
142+
public Builder type(Type type) {
143+
this.type = type;
144+
return this;
145+
}
146+
147+
public Builder jsonSchema(JsonSchema jsonSchema) {
148+
this.jsonSchema = jsonSchema;
149+
return this;
150+
}
151+
152+
public Builder jsonSchema(String jsonSchema) {
153+
this.jsonSchema = JsonSchema.builder().schema(jsonSchema).build();
154+
return this;
155+
}
156+
157+
public ResponseFormat build() {
158+
return new ResponseFormat(this.type, this.jsonSchema);
159+
}
160+
161+
}
162+
163+
public enum Type {
164+
165+
/**
166+
* Generates a text response. (default)
167+
*/
168+
@JsonProperty("text")
169+
TEXT,
170+
171+
/**
172+
* Enables JSON mode, which guarantees the message the model generates is valid
173+
* JSON.
174+
*/
175+
@JsonProperty("json_object")
176+
JSON_OBJECT,
177+
178+
/**
179+
* Enables Structured Outputs which guarantees the model will match your supplied
180+
* JSON schema.
181+
*/
182+
@JsonProperty("json_schema")
183+
JSON_SCHEMA
184+
185+
}
186+
187+
/**
188+
* JSON schema object that describes the format of the JSON object. Applicable for the
189+
* 'json_schema' type only.
190+
*/
191+
@JsonInclude(Include.NON_NULL)
192+
public static class JsonSchema {
193+
194+
@JsonProperty("name")
195+
private String name;
196+
197+
@JsonProperty("schema")
198+
private Map<String, Object> schema;
199+
200+
@JsonProperty("strict")
201+
private Boolean strict;
202+
203+
public JsonSchema() {
204+
205+
}
206+
207+
public String getName() {
208+
return this.name;
209+
}
210+
211+
public Map<String, Object> getSchema() {
212+
return this.schema;
213+
}
214+
215+
public Boolean getStrict() {
216+
return this.strict;
217+
}
218+
219+
private JsonSchema(String name, Map<String, Object> schema, Boolean strict) {
220+
this.name = name;
221+
this.schema = schema;
222+
this.strict = strict;
223+
}
224+
225+
public static Builder builder() {
226+
return new Builder();
227+
}
228+
229+
@Override
230+
public int hashCode() {
231+
return Objects.hash(this.name, this.schema, this.strict);
232+
}
233+
234+
@Override
235+
public boolean equals(Object o) {
236+
if (this == o) {
237+
return true;
238+
}
239+
if (o == null || getClass() != o.getClass()) {
240+
return false;
241+
}
242+
JsonSchema that = (JsonSchema) o;
243+
return Objects.equals(this.name, that.name) && Objects.equals(this.schema, that.schema)
244+
&& Objects.equals(this.strict, that.strict);
245+
}
246+
247+
public static final class Builder {
248+
249+
private String name = "custom_schema";
250+
251+
private Map<String, Object> schema;
252+
253+
private Boolean strict = true;
254+
255+
private Builder() {
256+
}
257+
258+
public Builder name(String name) {
259+
this.name = name;
260+
return this;
261+
}
262+
263+
public Builder schema(Map<String, Object> schema) {
264+
this.schema = schema;
265+
return this;
266+
}
267+
268+
public Builder schema(String schema) {
269+
this.schema = ModelOptionsUtils.jsonToMap(schema);
270+
return this;
271+
}
272+
273+
public Builder strict(Boolean strict) {
274+
this.strict = strict;
275+
return this;
276+
}
277+
278+
public JsonSchema build() {
279+
return new JsonSchema(this.name, this.schema, this.strict);
280+
}
281+
282+
}
283+
284+
}
285+
286+
}

models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatModelResponseFormatIT.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@
3333
import org.springframework.ai.openai.OpenAiChatModel;
3434
import org.springframework.ai.openai.OpenAiChatOptions;
3535
import org.springframework.ai.openai.api.OpenAiApi;
36-
import org.springframework.ai.openai.api.OpenAiApi.ChatCompletionRequest.ResponseFormat;
3736
import org.springframework.ai.openai.api.OpenAiApi.ChatModel;
37+
import org.springframework.ai.openai.api.ResponseFormat;
3838
import org.springframework.beans.factory.annotation.Autowired;
3939
import org.springframework.boot.SpringBootConfiguration;
4040
import org.springframework.boot.test.context.SpringBootTest;
@@ -80,7 +80,7 @@ void jsonObject() throws JsonMappingException, JsonProcessingException {
8080

8181
Prompt prompt = new Prompt("List 8 planets. Use JSON response",
8282
OpenAiChatOptions.builder()
83-
.withResponseFormat(new ResponseFormat(ResponseFormat.Type.JSON_OBJECT))
83+
.withResponseFormat(ResponseFormat.builder().type(ResponseFormat.Type.JSON_OBJECT).build())
8484
.build());
8585

8686
ChatResponse response = this.openAiChatModel.call(prompt);

0 commit comments

Comments
 (0)