1
1
package com.openai.core
2
2
3
+ import com.fasterxml.jackson.annotation.JsonTypeName
3
4
import com.fasterxml.jackson.databind.JsonNode
4
5
import com.fasterxml.jackson.databind.json.JsonMapper
6
+ import com.fasterxml.jackson.databind.node.ObjectNode
5
7
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module
6
8
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
7
9
import com.fasterxml.jackson.module.kotlin.kotlinModule
@@ -11,7 +13,10 @@ import com.github.victools.jsonschema.generator.SchemaGenerator
11
13
import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder
12
14
import com.github.victools.jsonschema.module.jackson.JacksonModule
13
15
import com.openai.errors.OpenAIInvalidDataException
16
+ import com.openai.models.FunctionDefinition
14
17
import com.openai.models.ResponseFormatJsonSchema
18
+ import com.openai.models.chat.completions.ChatCompletionTool
19
+ import com.openai.models.responses.FunctionTool
15
20
import com.openai.models.responses.ResponseFormatTextJsonSchemaConfig
16
21
import com.openai.models.responses.ResponseTextConfig
17
22
@@ -38,29 +43,41 @@ internal fun <T> responseFormatFromClass(
38
43
.jsonSchema(
39
44
ResponseFormatJsonSchema .JsonSchema .builder()
40
45
.name(" json-schema-from-${type.simpleName} " )
41
- .schema(JsonValue .fromJsonNode(extractAndValidateSchema(type, localValidation)))
46
+ .schema(
47
+ JsonValue .fromJsonNode(
48
+ validateSchema(extractSchema(type), type, localValidation)
49
+ )
50
+ )
42
51
// Ensure the model's output strictly adheres to this JSON schema. This is the
43
52
// essential "ON switch" for Structured Outputs.
44
53
.strict(true )
45
54
.build()
46
55
)
47
56
.build()
48
57
49
- private fun <T > extractAndValidateSchema (
50
- type : Class <T >,
58
+ /* *
59
+ * Validates the given JSON schema with respect to OpenAI's JSON schema restrictions.
60
+ *
61
+ * @param schema The JSON schema to be validated.
62
+ * @param sourceType The class from which the JSON schema was derived. This is only used in error
63
+ * messages.
64
+ * @param localValidation Set to [JsonSchemaLocalValidation.YES] to perform the validation. Other
65
+ * values will cause validation to be skipped.
66
+ */
67
+ @JvmSynthetic
68
+ internal fun <T > validateSchema (
69
+ schema : ObjectNode ,
70
+ sourceType : Class <T >,
51
71
localValidation : JsonSchemaLocalValidation ,
52
- ): JsonNode {
53
- val schema = extractSchema(type)
54
-
72
+ ): ObjectNode {
55
73
if (localValidation == JsonSchemaLocalValidation .YES ) {
56
74
val validator = JsonSchemaValidator .create().validate(schema)
57
75
58
76
require(validator.isValid()) {
59
- " Local validation failed for JSON schema derived from $type :\n " +
77
+ " Local validation failed for JSON schema derived from $sourceType :\n " +
60
78
validator.errors().joinToString(" \n " ) { " - $it " }
61
79
}
62
80
}
63
-
64
81
return schema
65
82
}
66
83
@@ -77,14 +94,94 @@ internal fun <T> textConfigFromClass(
77
94
.format(
78
95
ResponseFormatTextJsonSchemaConfig .builder()
79
96
.name(" json-schema-from-${type.simpleName} " )
80
- .schema(JsonValue .fromJsonNode(extractAndValidateSchema(type, localValidation)))
97
+ .schema(
98
+ JsonValue .fromJsonNode(
99
+ validateSchema(extractSchema(type), type, localValidation)
100
+ )
101
+ )
81
102
// Ensure the model's output strictly adheres to this JSON schema. This is the
82
103
// essential "ON switch" for Structured Outputs.
83
104
.strict(true )
84
105
.build()
85
106
)
86
107
.build()
87
108
109
+ // "internal" instead of "private" for testing purposes.
110
+ internal data class FunctionInfo (
111
+ val name : String ,
112
+ val description : String? ,
113
+ val schema : ObjectNode ,
114
+ )
115
+
116
+ @JvmSynthetic
117
+ // "internal" instead of "private" for testing purposes.
118
+ internal fun <T > extractFunctionInfo (
119
+ parametersType : Class <T >,
120
+ localValidation : JsonSchemaLocalValidation ,
121
+ ): FunctionInfo {
122
+ val schema = extractSchema(parametersType)
123
+
124
+ validateSchema(schema, parametersType, localValidation)
125
+
126
+ // The JSON schema generator ignores the `@JsonTypeName` annotation, so it never sets the "name"
127
+ // field at the root of the schema. Respect that annotation here and use it to set the name
128
+ // (outside the schema). Fall back to using the simple name of the class.
129
+ val name =
130
+ parametersType.getAnnotation(JsonTypeName ::class .java)?.value ? : parametersType.simpleName
131
+
132
+ // The JSON schema generator will copy the `@JsonClassDescription` into the schema. If present,
133
+ // remove it from the schema so it can be set on the function definition/tool.
134
+ val descriptionNode: JsonNode ? = schema.remove(" description" )
135
+ val description: String? = descriptionNode?.textValue()
136
+
137
+ return FunctionInfo (name, description, schema)
138
+ }
139
+
140
+ /* *
141
+ * Creates a Chat Completions API tool defining a function whose input parameters are derived from
142
+ * the fields of a class.
143
+ */
144
+ @JvmSynthetic
145
+ internal fun <T > functionToolFromClass (
146
+ parametersType : Class <T >,
147
+ localValidation : JsonSchemaLocalValidation = JsonSchemaLocalValidation .YES ,
148
+ ): ChatCompletionTool {
149
+ val functionInfo = extractFunctionInfo(parametersType, localValidation)
150
+
151
+ return ChatCompletionTool .builder()
152
+ .function(
153
+ FunctionDefinition .builder()
154
+ .name(functionInfo.name)
155
+ .apply { functionInfo.description?.let (::description) }
156
+ .parameters(JsonValue .fromJsonNode(functionInfo.schema))
157
+ // OpenAI: "Setting strict to true will ensure function calls reliably adhere to the
158
+ // function schema, instead of being best effort. We recommend always enabling
159
+ // strict mode."
160
+ .strict(true )
161
+ .build()
162
+ )
163
+ .build()
164
+ }
165
+
166
+ /* *
167
+ * Creates a Responses API function tool defining a function whose input parameters are derived from
168
+ * the fields of a class.
169
+ */
170
+ @JvmSynthetic
171
+ internal fun <T > responseFunctionToolFromClass (
172
+ parametersType : Class <T >,
173
+ localValidation : JsonSchemaLocalValidation = JsonSchemaLocalValidation .YES ,
174
+ ): FunctionTool {
175
+ val functionInfo = extractFunctionInfo(parametersType, localValidation)
176
+
177
+ return FunctionTool .builder()
178
+ .name(functionInfo.name)
179
+ .apply { functionInfo.description?.let (::description) }
180
+ .parameters(JsonValue .fromJsonNode(functionInfo.schema))
181
+ .strict(true )
182
+ .build()
183
+ }
184
+
88
185
/* *
89
186
* Derives a JSON schema from the structure of an arbitrary Java class.
90
187
*
@@ -93,7 +190,7 @@ internal fun <T> textConfigFromClass(
93
190
* thrown and any recorded validation errors can be inspected at leisure by the tests.
94
191
*/
95
192
@JvmSynthetic
96
- internal fun <T > extractSchema (type : Class <T >): JsonNode {
193
+ internal fun <T > extractSchema (type : Class <T >): ObjectNode {
97
194
val configBuilder =
98
195
SchemaGeneratorConfigBuilder (
99
196
com.github.victools.jsonschema.generator.SchemaVersion .DRAFT_2020_12 ,
@@ -130,3 +227,10 @@ internal fun <T> responseTypeFromJson(json: String, responseType: Class<T>): T =
130
227
// sensitive data are not exposed in logs.
131
228
throw OpenAIInvalidDataException (" Error parsing JSON: $json " , e)
132
229
}
230
+
231
+ /* *
232
+ * Converts any object into a JSON-formatted string. For `Object` types (other than strings and
233
+ * boxed primitives) a JSON object is created with its fields and values set from the fields of the
234
+ * object.
235
+ */
236
+ @JvmSynthetic internal fun toJsonString (obj : Any ): String = MAPPER .writeValueAsString(obj)
0 commit comments