Skip to content

Commit 69f9984

Browse files
committed
fn-calling: draft documentation and some review updates.
1 parent 1151515 commit 69f9984

File tree

7 files changed

+297
-6
lines changed

7 files changed

+297
-6
lines changed

README.md

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,195 @@ If you use `@JsonProperty(required = false)`, the `false` value will be ignored.
533533
must mark all properties as _required_, so the schema generated from your Java classes will respect
534534
that restriction and ignore any annotation that would violate it.
535535

536+
## Function calling
537+
538+
OpenAI [Function Calling](https://platform.openai.com/docs/guides/function-calling?api-mode=chat)
539+
lets you integrate external functions directly into the language model's responses. Instead of
540+
producing plain text, the model can output instructions (with parameters) for calling a function
541+
when appropriate. You define a [JSON schema](https://json-schema.org/overview/what-is-jsonschema)
542+
for functions, and the model uses it to decide when and how to trigger these calls, enabling more
543+
interactive, data-driven applications.
544+
545+
A JSON schema describing a function's parameters can be defined via the API by building a
546+
[`ChatCompletionTool`](openai-java-core/src/main/kotlin/com/openai/models/chat/completions/ChatCompletionTool.kt)
547+
containing a
548+
[`FunctionDefinition`](openai-java-core/src/main/kotlin/com/openai/models/FunctionDefinition.kt)
549+
and then using `addTool` to set it on the input parameters. The response from the AI model may then
550+
contain requests to call your functions, detailing the functions' names and their parameter values
551+
as JSON data that conforms to the JSON schema from the function definition. You can then parse the
552+
parameter values from this JSON, invoke your functions, and pass your functions' results back to the
553+
AI model. A full, working example of _Function Calling_ using the low-level API can be seen in
554+
[`FunctionCallingRawExample`](openai-java-example/src/main/java/com/openai/example/FunctionCallingRawExample.java).
555+
556+
However, for greater convenience, the SDK can derive a function and its parameters automatically
557+
from the structure of an arbitrary Java class: the class's name provides the function name, and the
558+
class's fields define the function's parameters. When the AI model responds with the parameter
559+
values in JSON form, you can then easily convert that JSON to an instance of your Java class and
560+
use the parameter values to invoke your custom function. A full, working example of the use of
561+
_Function Calling_ with Java classes to define function parameters can be seen in
562+
[`FunctionCallingExample`](openai-java-example/src/main/java/com/openai/example/FunctionCallingExample.java).
563+
564+
Like for _Structured Outputs_, Java classes can contain fields declared to be instances of other
565+
classes and can use collections. Optionally, annotations can be used to set the descriptions of the
566+
function (class) and its parameters (fields) to assist the AI model in understanding the purpose of
567+
the function and the possible values of its parameters.
568+
569+
```java
570+
import com.fasterxml.jackson.annotation.JsonClassDescription;
571+
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
572+
573+
@JsonClassDescription("Gets the quality of the given SDK.")
574+
static class GetSdkQuality {
575+
@JsonPropertyDescription("The name of the SDK.")
576+
public String name;
577+
578+
public SdkQuality execute() {
579+
return new SdkQuality(
580+
name, name.contains("OpenAI") ? "It's robust and polished!" : "*shrug*");
581+
}
582+
}
583+
584+
static class SdkQuality {
585+
public String quality;
586+
587+
public SdkQuality(String name, String evaluation) {
588+
quality = name + ": " + evaluation;
589+
}
590+
}
591+
592+
@JsonClassDescription("Gets the review score (out of 10) for the named SDK.")
593+
static class GetSdkScore {
594+
public String name;
595+
596+
public int execute() {
597+
return name.contains("OpenAI") ? 10 : 3;
598+
}
599+
}
600+
```
601+
602+
When your functions are defined, add them to the input parameters using `addTool(Class<T>)` and then
603+
call them if requested to do so in the AI model's response. `Function.argments(Class<T>)` can be
604+
used to parse a function's parameters in JSON form to an instance of your function-defining class.
605+
The fields of that instance will be set to the values of the parameters to the function call.
606+
607+
After calling the function, use `ChatCompletionToolMessageParam.Builder.contentAsJson(Object)` to
608+
pass the function's result back to the AI model. The method will convert the result to JSON form
609+
for consumption by the model. The `Object` can be any object, including simple `String` instances
610+
and boxed primitive types.
611+
612+
```java
613+
import com.openai.client.OpenAIClient;
614+
import com.openai.client.okhttp.OpenAIOkHttpClient;
615+
import com.openai.models.ChatModel;
616+
import com.openai.models.chat.completions.*;
617+
import java.util.Collection;
618+
619+
OpenAIClient client = OpenAIOkHttpClient.fromEnv();
620+
621+
ChatCompletionCreateParams.Builder createParamsBuilder = ChatCompletionCreateParams.builder()
622+
.model(ChatModel.GPT_3_5_TURBO)
623+
.maxCompletionTokens(2048)
624+
.addTool(GetSdkQuality.class)
625+
.addTool(GetSdkScore.class)
626+
.addUserMessage("How good are the following SDKs and what do reviewers say: "
627+
+ "OpenAI Java SDK, Unknown Company SDK.");
628+
629+
client.chat().completions().create(createParamsBuilder.build()).choices().stream()
630+
.map(ChatCompletion.Choice::message)
631+
// Add each assistant message onto the builder so that we keep track of the
632+
// conversation for asking a follow-up question later.
633+
.peek(createParamsBuilder::addMessage)
634+
.flatMap(message -> {
635+
message.content().ifPresent(System.out::println);
636+
return message.toolCalls().stream().flatMap(Collection::stream);
637+
})
638+
.forEach(toolCall -> {
639+
Object result = callFunction(toolCall.function());
640+
// Add the tool call result to the conversation.
641+
createParamsBuilder.addMessage(ChatCompletionToolMessageParam.builder()
642+
.toolCallId(toolCall.id())
643+
.contentAsJson(result)
644+
.build());
645+
});
646+
647+
// Ask a follow-up question about the function call result.
648+
createParamsBuilder.addUserMessage("Why do you say that?");
649+
client.chat().completions().create(createParamsBuilder.build()).choices().stream()
650+
.flatMap(choice -> choice.message().content().stream())
651+
.forEach(System.out::println);
652+
653+
static Object callFunction(ChatCompletionMessageToolCall.Function function) {
654+
switch (function.name()) {
655+
case "GetSdkQuality":
656+
return function.arguments(GetSdkQuality.class).execute();
657+
case "GetSdkScore":
658+
return function.arguments(GetSdkScore.class).execute();
659+
default:
660+
throw new IllegalArgumentException("Unknown function: " + function.name());
661+
}
662+
}
663+
```
664+
665+
In the code above, an `execute()` method encapsulates each function's logic. However, there is no
666+
requirement to follow that pattern. You are free to implement your function's logic in any way that
667+
best suits your use case. The pattern above is only intended to _suggest_ that a suitable pattern
668+
may make the process of function calling simpler to understand and implement.
669+
670+
### Usage with the Responses API
671+
672+
_Function Calling_ is also supported for the Responses API. The usage is the same as described
673+
except where the Responses API differs slightly from the Chat Completions API. Pass the top-level
674+
class to `addTool(Class<T>)` when building the parameters. In the response, look for
675+
[`RepoonseOutputItem`](openai-java-core/src/main/kotlin/com/openai/models/responses/ResponseOutputItem.kt)
676+
instances that are function calls. Parse the parameters to each function call to an instance of the
677+
class using
678+
[`ResponseFunctionToolCall.arguments(Class<T>)`](openai-java-core/src/main/kotlin/com/openai/models/responses/ResponseFunctionToolCall.kt).
679+
Finally, pass the result of each call back to the model.
680+
681+
For a full example of the usage of _Function Calling_ with the Responses API, see
682+
[`ResponsesFunctionCallingExample`](openai-java-example/src/main/java/com/openai/example/ResponsesFunctionCallingExample.java).
683+
684+
### Local function JSON schema validation
685+
686+
Like for _Structured Outputs_, you can perform local validation to check that the JSON schema
687+
derived from your function class respects the restrictions imposed by OpenAI on such schemas. Local
688+
validation is enabled by default, but it can be disabled by adding `JsonSchemaLocalValidation.NO` to
689+
the call to `addTool`.
690+
691+
```java
692+
ChatCompletionCreateParams.Builder createParamsBuilder = ChatCompletionCreateParams.builder()
693+
.model(ChatModel.GPT_3_5_TURBO)
694+
.maxCompletionTokens(2048)
695+
.addTool(GetSdkQuality.class, JsonSchemaLocalValidation.NO)
696+
.addTool(GetSdkScore.class, JsonSchemaLocalValidation.NO)
697+
.addUserMessage("How good are the following SDKs and what do reviewers say: "
698+
+ "OpenAI Java SDK, Unknown Company SDK.");
699+
```
700+
701+
See [Local JSON schema validation](#local-json-schema-validation) for more details on local schema
702+
validation and under what circumstances you might want to disable it.
703+
704+
### Annotating function classes
705+
706+
You can use annotations to add further information about functions to the JSON schemas that are
707+
derived from your function classes, or to exclude individual fields from the parameters to the
708+
function. Details from annotations captured in the JSON schema may be used by the AI model to
709+
improve its response. The SDK supports the use of
710+
[Jackson Databind](https://github.com/FasterXML/jackson-databind) annotations.
711+
712+
- Use `@JsonClassDescription` to add a description to a function class detailing when and how to use
713+
that function.
714+
- Use `@JsonTypeName` to set the function name to something other than the simple name of the class,
715+
which is used by default.
716+
- Use `@JsonPropertyDescription` to add a detailed description to function parameter (a field of
717+
a function class).
718+
- Use `@JsonIgnore` to omit a field of a class from the generated JSON schema for a function's
719+
parameters.
720+
721+
OpenAI provides some
722+
[Best practices for defining functions](https://platform.openai.com/docs/guides/function-calling#best-practices-for-defining-functions)
723+
that may help you to understand how to use the above annotations effectively for your functions.
724+
536725
## File uploads
537726

538727
The SDK defines methods that accept files.

openai-java-core/src/main/kotlin/com/openai/core/StructuredOutputs.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,9 @@ internal fun <T> extractSchema(type: Class<T>): ObjectNode {
216216
/**
217217
* Creates an instance of a Java class using data from a JSON. The JSON data should conform to the
218218
* JSON schema previously extracted from the Java class.
219+
*
220+
* @throws OpenAIInvalidDataException If the JSON data cannot be parsed to an instance of the
221+
* [responseType] class.
219222
*/
220223
@JvmSynthetic
221224
internal fun <T> responseTypeFromJson(json: String, responseType: Class<T>): T =

openai-java-core/src/main/kotlin/com/openai/models/chat/completions/ChatCompletionMessageToolCall.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,10 @@ private constructor(
265265
* been used to define the JSON schema for the function definition's parameters, so that the
266266
* JSON corresponds to structure of the given class.
267267
*
268+
* @throws OpenAIInvalidDataException If the JSON data is missing, `null`, or cannot be
269+
* parsed to an instance of the [functionParametersType] class. This might occur if the
270+
* class is not the same as the class that was originally used to define the arguments, or
271+
* if the data from the AI model is invalid or incomplete (e.g., truncated).
268272
* @see ChatCompletionCreateParams.Builder.addTool
269273
* @see arguments
270274
*/

openai-java-core/src/main/kotlin/com/openai/models/responses/ResponseFunctionToolCall.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ private constructor(
5858
* class. The class must be the same as the class that was used to define the function's
5959
* parameters when the function was defined.
6060
*
61+
* @throws OpenAIInvalidDataException If the JSON data is missing, `null`, or cannot be parsed
62+
* to an instance of the [functionParametersType] class. This might occur if the class is not
63+
* the same as the class that was originally used to define the arguments, or if the data from
64+
* the AI model is invalid or incomplete (e.g., truncated).
6165
* @see ResponseCreateParams.Builder.addTool
6266
* @see arguments
6367
*/

openai-java-example/src/main/java/com/openai/example/FunctionCallingExample.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
public final class FunctionCallingExample {
1212
private FunctionCallingExample() {}
1313

14-
// @JsonTypeName("get-sdk-quality")
1514
@JsonClassDescription("Gets the quality of the given SDK.")
1615
static class GetSdkQuality {
1716
@JsonPropertyDescription("The name of the SDK.")
@@ -30,7 +29,7 @@ public SdkQuality(String name, String evaluation) {
3029
}
3130
}
3231

33-
@JsonClassDescription("Gets the review score (out of 10) for the given SDK.")
32+
@JsonClassDescription("Gets the review score (out of 10) for the named SDK.")
3433
static class GetSdkScore {
3534
public String name;
3635

@@ -65,11 +64,11 @@ public static void main(String[] args) {
6564
return message.toolCalls().stream().flatMap(Collection::stream);
6665
})
6766
.forEach(toolCall -> {
68-
Object content = callFunction2(toolCall.function());
67+
Object result = callFunction(toolCall.function());
6968
// Add the tool call result to the conversation.
7069
createParamsBuilder.addMessage(ChatCompletionToolMessageParam.builder()
7170
.toolCallId(toolCall.id())
72-
.contentAsJson(content)
71+
.contentAsJson(result)
7372
.build());
7473
});
7574

@@ -80,7 +79,7 @@ public static void main(String[] args) {
8079
.forEach(System.out::println);
8180
}
8281

83-
private static Object callFunction2(ChatCompletionMessageToolCall.Function function) {
82+
private static Object callFunction(ChatCompletionMessageToolCall.Function function) {
8483
switch (function.name()) {
8584
case "GetSdkQuality":
8685
return function.arguments(GetSdkQuality.class).execute();
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package com.openai.example;
2+
3+
import static com.openai.core.ObjectMappers.jsonMapper;
4+
5+
import com.fasterxml.jackson.core.JsonProcessingException;
6+
import com.openai.client.OpenAIClient;
7+
import com.openai.client.okhttp.OpenAIOkHttpClient;
8+
import com.openai.core.JsonObject;
9+
import com.openai.core.JsonValue;
10+
import com.openai.models.ChatModel;
11+
import com.openai.models.FunctionDefinition;
12+
import com.openai.models.FunctionParameters;
13+
import com.openai.models.chat.completions.*;
14+
import java.util.Collection;
15+
import java.util.List;
16+
import java.util.Map;
17+
18+
public final class FunctionCallingRawExample {
19+
private FunctionCallingRawExample() {}
20+
21+
public static void main(String[] args) {
22+
// Configures using one of:
23+
// - The `OPENAI_API_KEY` environment variable
24+
// - The `OPENAI_BASE_URL` and `AZURE_OPENAI_KEY` environment variables
25+
OpenAIClient client = OpenAIOkHttpClient.fromEnv();
26+
27+
// Use a builder so that we can append more messages to it below.
28+
// Each time we call .build()` we get an immutable object that's unaffected by future mutations of the builder.
29+
ChatCompletionCreateParams.Builder createParamsBuilder = ChatCompletionCreateParams.builder()
30+
.model(ChatModel.GPT_3_5_TURBO)
31+
.maxCompletionTokens(2048)
32+
.addTool(ChatCompletionTool.builder()
33+
.function(FunctionDefinition.builder()
34+
.name("get-sdk-quality")
35+
.description("Gets the quality of the given SDK.")
36+
.parameters(FunctionParameters.builder()
37+
.putAdditionalProperty("type", JsonValue.from("object"))
38+
.putAdditionalProperty(
39+
"properties", JsonValue.from(Map.of("name", Map.of("type", "string"))))
40+
.putAdditionalProperty("required", JsonValue.from(List.of("name")))
41+
.putAdditionalProperty("additionalProperties", JsonValue.from(false))
42+
.build())
43+
.build())
44+
.build())
45+
.addUserMessage("How good are the following SDKs: OpenAI Java SDK, Unknown Company SDK");
46+
47+
client.chat().completions().create(createParamsBuilder.build()).choices().stream()
48+
.map(ChatCompletion.Choice::message)
49+
// Add each assistant message onto the builder so that we keep track of the conversation for asking a
50+
// follow-up question later.
51+
.peek(createParamsBuilder::addMessage)
52+
.flatMap(message -> {
53+
message.content().ifPresent(System.out::println);
54+
return message.toolCalls().stream().flatMap(Collection::stream);
55+
})
56+
.forEach(toolCall -> {
57+
String result = callFunction(toolCall.function());
58+
// Add the tool call result to the conversation.
59+
createParamsBuilder.addMessage(ChatCompletionToolMessageParam.builder()
60+
.toolCallId(toolCall.id())
61+
.content(result)
62+
.build());
63+
System.out.println(result);
64+
});
65+
System.out.println();
66+
67+
// Ask a follow-up question about the function call result.
68+
createParamsBuilder.addUserMessage("Why do you say that?");
69+
client.chat().completions().create(createParamsBuilder.build()).choices().stream()
70+
.flatMap(choice -> choice.message().content().stream())
71+
.forEach(System.out::println);
72+
}
73+
74+
private static String callFunction(ChatCompletionMessageToolCall.Function function) {
75+
if (!function.name().equals("get-sdk-quality")) {
76+
throw new IllegalArgumentException("Unknown function: " + function.name());
77+
}
78+
79+
JsonValue arguments;
80+
try {
81+
arguments = JsonValue.from(jsonMapper().readTree(function.arguments()));
82+
} catch (JsonProcessingException e) {
83+
throw new IllegalArgumentException("Bad function arguments", e);
84+
}
85+
86+
String sdkName = ((JsonObject) arguments).values().get("name").asStringOrThrow();
87+
if (sdkName.contains("OpenAI")) {
88+
return sdkName + ": It's robust and polished!";
89+
}
90+
91+
return sdkName + ": *shrug*";
92+
}
93+
}

openai-java-example/src/main/java/com/openai/example/ResponsesFunctionCallingExample.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
public final class ResponsesFunctionCallingExample {
1515
private ResponsesFunctionCallingExample() {}
1616

17-
// @JsonTypeName("get-sdk-quality")
1817
@JsonClassDescription("Gets the quality of the given SDK.")
1918
static class GetSdkQuality {
2019
@JsonPropertyDescription("The name of the SDK.")

0 commit comments

Comments
 (0)