Skip to content

Commit 12ce63d

Browse files
committed
response format json scheme support
1 parent 0ed9c38 commit 12ce63d

File tree

7 files changed

+163
-43
lines changed

7 files changed

+163
-43
lines changed

api/src/main/java/com/theokanning/openai/completion/chat/AssistantMessage.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ public class AssistantMessage implements ChatMessage {
3636
@JsonProperty("function_call")
3737
ChatFunctionCall functionCall;
3838

39+
/**
40+
* when response_format is json_schema, the assistant can return a refusal message.
41+
*/
42+
private String refusal;
43+
44+
3945

4046
public AssistantMessage(String content) {
4147
this.content = content;
@@ -54,7 +60,7 @@ public String getTextContent() {
5460

5561
/**
5662
* Deserializes the message to an object of the specified target class.
57-
*
63+
*
5864
* @param targetClass the type of the object
5965
* @return the deserialized object
6066
**/

api/src/main/java/com/theokanning/openai/completion/chat/ChatResponseFormat.java

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import java.io.IOException;
44

5+
import com.fasterxml.jackson.annotation.JsonProperty;
56
import com.fasterxml.jackson.core.JacksonException;
67
import com.fasterxml.jackson.core.JsonGenerator;
78
import com.fasterxml.jackson.core.JsonParser;
@@ -15,6 +16,7 @@
1516
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
1617
import com.kjetland.jackson.jsonSchema.JsonSchemaConfig;
1718
import com.kjetland.jackson.jsonSchema.JsonSchemaGenerator;
19+
import com.theokanning.openai.function.FunctionDefinition;
1820
import com.theokanning.openai.utils.JsonUtil;
1921

2022
import lombok.Data;
@@ -29,20 +31,20 @@ public class ChatResponseFormat {
2931
private static final ObjectMapper MAPPER = JsonUtil.getInstance();
3032
private static final JsonSchemaConfig CONFIG = JsonSchemaConfig.vanillaJsonSchemaDraft4();
3133
private static final JsonSchemaGenerator JSON_SCHEMA_GENERATOR = new JsonSchemaGenerator(MAPPER, CONFIG);
32-
34+
3335
/**
34-
* auto/text/json_object
36+
* auto/text/json_object/json_schema
3537
*/
3638
private String type;
37-
39+
3840
/**
3941
* This is used together with type field set to "json_schema"
4042
* to enable structured outputs.
41-
*
43+
*
4244
* @see https://openai.com/index/introducing-structured-outputs-in-the-api/
43-
*
4445
*/
45-
private JsonNode json_schema;
46+
private ResponseJsonSchema jsonSchema;
47+
4648

4749
/**
4850
* 构造私有,只允许从静态变量获取
@@ -51,17 +53,17 @@ private ChatResponseFormat(String type) {
5153
this.type = type;
5254
}
5355

56+
5457
public static final ChatResponseFormat AUTO = new ChatResponseFormat("auto");
5558

5659
public static final ChatResponseFormat TEXT = new ChatResponseFormat("text");
5760

5861
public static final ChatResponseFormat JSON_OBJECT = new ChatResponseFormat("json_object");
59-
60-
public static ChatResponseFormat jsonSchema(Class<?> rootClass) {
61-
JsonNode jsonSchema = JSON_SCHEMA_GENERATOR.generateJsonSchema(rootClass);
62-
ChatResponseFormat jsonSchemaFormat = new ChatResponseFormat("json_schema");
63-
jsonSchemaFormat.setJson_schema(jsonSchema);
64-
return jsonSchemaFormat;
62+
63+
public static ChatResponseFormat jsonSchema(ResponseJsonSchema jsonSchema) {
64+
ChatResponseFormat chatResponseFormat = new ChatResponseFormat("json_schema");
65+
chatResponseFormat.setJsonSchema(jsonSchema);
66+
return chatResponseFormat;
6567
}
6668

6769
@NoArgsConstructor
@@ -73,18 +75,10 @@ public void serialize(ChatResponseFormat value, JsonGenerator gen, SerializerPro
7375
} else {
7476
gen.writeStartObject();
7577
gen.writeObjectField("type", (value).getType());
76-
7778
if (value.getType().equals("json_schema")) {
78-
JsonNode jsonSchema = value.getJson_schema();
79-
80-
gen.writeObjectFieldStart("json_schema");
81-
gen.writeStringField("name", "ChatResponseFormat");
82-
gen.writeBooleanField("strict", true);
83-
gen.writeFieldName("schema");
84-
gen.writeTree(jsonSchema);
85-
gen.writeEndObject();
79+
gen.writeObjectField("json_schema", value.getJsonSchema());
8680
}
87-
81+
8882
gen.writeEndObject();
8983
}
9084
}
@@ -107,13 +101,16 @@ public ChatResponseFormat deserialize(JsonParser jsonParser, DeserializationCont
107101
while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
108102
if (jsonParser.getCurrentName().equals("type")) {
109103
jsonParser.nextToken();
110-
switch (jsonParser.getText()){
104+
switch (jsonParser.getText()) {
111105
case "auto":
112106
return AUTO;
113107
case "text":
114108
return TEXT;
115109
case "json_object":
116110
return JSON_OBJECT;
111+
case "json_schema":
112+
jsonParser.nextToken();
113+
return jsonSchema(MAPPER.readValue(jsonParser, ResponseJsonSchema.class));
117114
default:
118115
throw new InvalidFormatException(jsonParser, "Invalid response format", jsonParser.getCurrentToken().toString(), ChatResponseFormat.class);
119116
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package com.theokanning.openai.completion.chat;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
import com.fasterxml.jackson.core.JsonGenerator;
5+
import com.fasterxml.jackson.databind.JsonSerializer;
6+
import com.fasterxml.jackson.databind.SerializerProvider;
7+
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
8+
import com.fasterxml.jackson.databind.node.ObjectNode;
9+
import com.kjetland.jackson.jsonSchema.JsonSchemaConfig;
10+
import com.kjetland.jackson.jsonSchema.JsonSchemaGenerator;
11+
import com.theokanning.openai.utils.JsonUtil;
12+
import lombok.Data;
13+
import lombok.NoArgsConstructor;
14+
import lombok.NonNull;
15+
16+
import java.io.IOException;
17+
18+
/**
19+
* @author LiangTao
20+
* @date 2024年08月14 10:57
21+
**/
22+
@JsonSerialize(using = ResponseJsonSchema.ResponseJsonSchemaSerializer.class)
23+
@NoArgsConstructor
24+
@Data
25+
public class ResponseJsonSchema {
26+
/**
27+
* The name of the function being called.
28+
*/
29+
@NonNull
30+
protected String name;
31+
32+
private boolean strict = true;
33+
34+
/**
35+
* parameters definition by class schema ,will use {@link JsonSchemaGenerator} to generate json schema
36+
*/
37+
private Class<?> schemaClass;
38+
39+
/**
40+
* The parameters the functions accepts.Choose between this parameter and {@link #schemaClass}
41+
* This parameter requires you to implement the serialization/deserialization logic of the JSON schema yourself.
42+
**/
43+
@JsonProperty("schema")
44+
private Object schemaDefinition;
45+
46+
47+
public static class ResponseJsonSchemaSerializer extends JsonSerializer<ResponseJsonSchema> {
48+
private final JsonSchemaConfig config = JsonSchemaConfig.vanillaJsonSchemaDraft4();
49+
50+
private final JsonSchemaGenerator jsonSchemaGenerator = new JsonSchemaGenerator(JsonUtil.getInstance(), config);
51+
52+
@Override
53+
public void serialize(ResponseJsonSchema value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
54+
gen.writeStartObject();
55+
gen.writeStringField("name", value.getName());
56+
gen.writeBooleanField("strict", value.isStrict());
57+
if (value.getSchemaClass() != null) {
58+
gen.writeFieldName("schema");
59+
ObjectNode parameterSchema = (ObjectNode) jsonSchemaGenerator.generateJsonSchema(value.getSchemaClass());
60+
parameterSchema.remove("$schema");
61+
parameterSchema.remove("title");
62+
gen.writeRawValue(JsonUtil.writeValueAsString(parameterSchema));
63+
} else {
64+
gen.writeFieldName("schema");
65+
Object parametersDefinition = value.getSchemaDefinition();
66+
if (parametersDefinition instanceof String && JsonUtil.isValidJson((String) parametersDefinition)) {
67+
String prettyString = JsonUtil.getInstance().readTree((String) parametersDefinition).toPrettyString();
68+
gen.writeRawValue(prettyString);
69+
} else {
70+
gen.writeRawValue(JsonUtil.writeValueAsString(parametersDefinition));
71+
}
72+
}
73+
gen.writeEndObject();
74+
}
75+
}
76+
77+
public static <T> ResponseJsonSchema.Builder<T> builder() {
78+
return new ResponseJsonSchema.Builder<>();
79+
}
80+
81+
public static class Builder<T> {
82+
private String name;
83+
private Class<T> schemaClass;
84+
85+
private T schemaDefinition;
86+
87+
private boolean strict = true;
88+
89+
public ResponseJsonSchema.Builder<T> name(String name) {
90+
this.name = name;
91+
return this;
92+
}
93+
94+
public ResponseJsonSchema.Builder<T> strict(boolean strict) {
95+
this.strict = strict;
96+
return this;
97+
}
98+
99+
public ResponseJsonSchema.Builder<T> schemaClass(Class<T> schemaClass) {
100+
this.schemaClass = schemaClass;
101+
return this;
102+
}
103+
104+
public ResponseJsonSchema.Builder<T> schemaDefinition(T schemaDefinition) {
105+
this.schemaDefinition = schemaDefinition;
106+
return this;
107+
}
108+
109+
public ResponseJsonSchema build() {
110+
if (name == null) {
111+
throw new IllegalArgumentException("name can't be null");
112+
}
113+
114+
if (schemaDefinition != null && schemaClass != null) {
115+
throw new IllegalArgumentException("schemaClass and schemaDefinition can't be set at the same time,please set one of them");
116+
}
117+
ResponseJsonSchema responseJsonSchema = new ResponseJsonSchema();
118+
responseJsonSchema.name = name;
119+
responseJsonSchema.schemaClass = schemaClass;
120+
responseJsonSchema.schemaDefinition = schemaDefinition;
121+
responseJsonSchema.strict = strict;
122+
return responseJsonSchema;
123+
}
124+
}
125+
126+
127+
}

api/src/main/java/com/theokanning/openai/function/FunctionDefinition.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,6 @@ private FunctionDefinition() {
3939
private Boolean strict;
4040

4141

42-
43-
4442
/**
4543
* parameters definition by class schema ,will use {@link JsonSchemaGenerator} to generate json schema
4644
*/

api/src/main/java/com/theokanning/openai/utils/TikTokensUtil.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313

1414
/**
1515
* Token calculation tool class
16+
* @deprecated openai support token use the official api,will be removed in the future
1617
*/
18+
@Deprecated
1719
public class TikTokensUtil {
1820
/**
1921
* Model name corresponds to Encoding

service/src/test/java/com/theokanning/openai/service/ChatCompletionTest.java

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import javax.validation.constraints.NotNull;
2020

21+
import com.theokanning.openai.completion.chat.*;
2122
import com.theokanning.openai.utils.JsonUtil;
2223
import org.junit.jupiter.api.Test;
2324

@@ -27,21 +28,6 @@
2728
import com.fasterxml.jackson.databind.node.ArrayNode;
2829
import com.fasterxml.jackson.databind.node.ObjectNode;
2930
import com.theokanning.openai.assistants.run.ToolChoice;
30-
import com.theokanning.openai.completion.chat.AssistantMessage;
31-
import com.theokanning.openai.completion.chat.ChatCompletionChoice;
32-
import com.theokanning.openai.completion.chat.ChatCompletionChunk;
33-
import com.theokanning.openai.completion.chat.ChatCompletionRequest;
34-
import com.theokanning.openai.completion.chat.ChatFunctionCall;
35-
import com.theokanning.openai.completion.chat.ChatFunctionDynamic;
36-
import com.theokanning.openai.completion.chat.ChatFunctionProperty;
37-
import com.theokanning.openai.completion.chat.ChatMessage;
38-
import com.theokanning.openai.completion.chat.ChatResponseFormat;
39-
import com.theokanning.openai.completion.chat.ChatTool;
40-
import com.theokanning.openai.completion.chat.ChatToolCall;
41-
import com.theokanning.openai.completion.chat.StreamOption;
42-
import com.theokanning.openai.completion.chat.SystemMessage;
43-
import com.theokanning.openai.completion.chat.ToolMessage;
44-
import com.theokanning.openai.completion.chat.UserMessage;
4531
import com.theokanning.openai.function.FunctionDefinition;
4632
import com.theokanning.openai.function.FunctionExecutorManager;
4733
import com.theokanning.openai.service.util.ToolUtil;
@@ -148,7 +134,11 @@ void createChatCompletionWithJsonSchema() throws JsonProcessingException {
148134
messages.add(userMessage);
149135

150136
Class<MathReasoning> rootClass = MathReasoning.class;
151-
ChatResponseFormat responseFormat = ChatResponseFormat.jsonSchema(rootClass);
137+
ChatResponseFormat responseFormat = ChatResponseFormat.jsonSchema(ResponseJsonSchema.<MathReasoning>builder()
138+
.name("math_reasoning")
139+
.schemaClass(rootClass)
140+
.strict(true)
141+
.build());
152142

153143
ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest
154144
.builder()

service/src/test/java/com/theokanning/openai/service/assistants/AssistantTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ void createAssistant() throws JsonProcessingException {
8282
assertEquals(assistant.getTemperature(), 0.2D);
8383
assertEquals(assistant.getResponseFormat(), ChatResponseFormat.AUTO);
8484
assertEquals(assistant.getInstructions(), "You are a personal Math Tutor.");
85-
assertEquals(assistant.getModel(), TikTokensUtil.ModelEnum.GPT_3_5_TURBO.getName());
85+
assertEquals(assistant.getResponseFormat().getType(), "auto");
8686
assertEquals(assistant.getDescription(), "the personal Math Tutor");
8787
}
8888

0 commit comments

Comments
 (0)