Skip to content
This repository has been archived by the owner on Jun 6, 2024. It is now read-only.

Added "function" capabilities #298

Merged
merged 24 commits into from
Jun 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
d315783
Initial commit
Eduardo-T Jun 14, 2023
dab34ca
Updated Readme
Eduardo-T Jun 14, 2023
ced9cd1
Added the ability to publish to the local repository.
Eduardo-T Jun 15, 2023
58552ca
Stylized the README a bit more.
Eduardo-T Jun 15, 2023
4023f7d
Fixed typo
Eduardo-T Jun 15, 2023
0eb553e
Fixed visibility of variables in example
Eduardo-T Jun 16, 2023
8f979be
Improved the usage of functions with an executor.
Eduardo-T Jun 16, 2023
36d6f84
Reverted changes.
Eduardo-T Jun 16, 2023
b9ba373
Added information about functions to the README.md
Eduardo-T Jun 16, 2023
3f6c9f5
Change reversed.
Eduardo-T Jun 16, 2023
25eee8f
Added some helper method and cleaned the example a little bit.
Eduardo-T Jun 16, 2023
9b4313a
:bug: custom serialization of function_call parameter should be an ob…
Grogdunn Jun 16, 2023
2715c55
Fixed example and added static instantiation method
Eduardo-T Jun 16, 2023
f300b17
Added null check
Eduardo-T Jun 16, 2023
fd91f5e
Set project version to SNAPSHOT and fixed vulnerable dependency
Eduardo-T Jun 16, 2023
13e69c1
Allowing objectmapper to be overriden in order to supply a custom obj…
davidb-e4s Jun 19, 2023
28c7af6
Updating the constructor to use the setter to be consistant
davidb-e4s Jun 19, 2023
de4bb85
Merge pull request #2 from davidb-e4s/main
Eduardo-T Jun 19, 2023
6e44d11
Merged changes from upstream
Eduardo-T Jun 19, 2023
be82bca
Merge remote-tracking branch 'origin/main'
Eduardo-T Jun 19, 2023
69aa285
Added usage of streams with functions
Eduardo-T Jun 19, 2023
1fd514c
Removed .DS_Store files and updated .gitignore
Eduardo-T Jun 19, 2023
b28d659
Small fixes and improvements and added tests
Eduardo-T Jun 19, 2023
7b98dc6
Simplified example a little and updated README
Eduardo-T Jun 20, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,6 @@ build
# Ignore any files in /bin and /obj Folders
**/bin/*
**/obj/*

# Ignore the macOS folder attribute file
**/.DS_Store
82 changes: 81 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,70 @@ OpenAiApi api = retrofit.create(OpenAiApi.class);
OpenAiService service = new OpenAiService(api);
```

### Functions
You can create your functions and define their executors easily using the ChatFunction class, along with any of your custom classes that will serve to define their available parameters. You can also process the functions with ease, with the help of an executor called FunctionExecutor.

First we declare our function parameters:
```java
public class Weather {
@JsonPropertyDescription("City and state, for example: León, Guanajuato")
public String location;
@JsonPropertyDescription("The temperature unit, can be 'celsius' or 'fahrenheit'")
@JsonProperty(required = true)
public WeatherUnit unit;
}
public enum WeatherUnit {
CELSIUS, FAHRENHEIT;
}
public static class WeatherResponse {
public String location;
public WeatherUnit unit;
public int temperature;
public String description;

// constructor
}
```

Next, we declare the function itself and associate it with an executor, in this example we will fake a response from some API:
```java
ChatFunction.builder()
.name("get_weather")
.description("Get the current weather of a location")
.executor(Weather.class, w -> new WeatherResponse(w.location, w.unit, new Random().nextInt(50), "sunny"))
.build()
```

Then, we employ the FunctionExecutor object from the 'service' module to assist with execution and transformation into an object that is ready for the conversation:
```java
List<ChatFunction> functionList = // list with functions
FunctionExecutor functionExecutor = new FunctionExecutor(functionList);

List<ChatMessage> messages = new ArrayList<>();
ChatMessage userMessage = new ChatMessage(ChatMessageRole.USER.value(), "Tell me the weather in Barcelona.");
messages.add(userMessage);
ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest
.builder()
.model("gpt-3.5-turbo-0613")
.messages(messages)
.functions(functionExecutor.getFunctions())
.functionCall(new ChatCompletionRequestFunctionCall("auto"))
.maxTokens(256)
.build();

ChatMessage responseMessage = service.createChatCompletion(chatCompletionRequest).getChoices().get(0).getMessage();
ChatFunctionCall functionCall = responseMessage.getFunctionCall(); // might be null, but in this case it is certainly a call to our 'get_weather' function.

ChatMessage functionResponseMessage = functionExecutor.executeAndConvertToMessageHandlingExceptions(functionCall);
messages.add(response);
```
> **Note:** The `FunctionExecutor` class is part of the 'service' module.

You can also create your own function executor. The return object of `ChatFunctionCall.getArguments()` is a JsonNode for simplicity and should be able to help you with that.

For a more in-depth look, refer to a conversational example that employs functions in: [OpenAiApiFunctionsExample.java](example/src/main/java/example/OpenAiApiFunctionsExample.java).
Or for an example using functions and stream: [OpenAiApiFunctionsWithStreamExample.java](example/src/main/java/example/OpenAiApiFunctionsWithStreamExample.java)

### Streaming thread shutdown
If you want to shut down your process immediately after streaming responses, call `OpenAiService.shutdownExecutor()`.
This is not necessary for non-streaming calls.
Expand All @@ -103,14 +167,30 @@ This is not necessary for non-streaming calls.
All the [example](example/src/main/java/example/OpenAiApiExample.java) project requires is your OpenAI api token
```bash
export OPENAI_TOKEN="sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
./gradlew example:run
```
You can try all the capabilities of this project using:
```bash
./gradlew runExampleOne
```
And you can also try the new capability of using functions:
```bash
./gradlew runExampleTwo
```
Or functions with 'stream' mode enabled:
```bash
./gradlew runExampleThree
```

## FAQ
### Does this support GPT-4?
Yes! GPT-4 uses the ChatCompletion Api, and you can see the latest model options [here](https://platform.openai.com/docs/models/gpt-4).
GPT-4 is currently in a limited beta (as of 4/1/23), so make sure you have access before trying to use it.

### Does this support functions?
Absolutely! It is very easy to use your own functions without worrying about doing the dirty work.
As mentioned above, you can refer to [OpenAiApiFunctionsExample.java](example/src/main/java/example/OpenAiApiFunctionsExample.java) or
[OpenAiApiFunctionsWithStreamExample.java](example/src/main/java/example/OpenAiApiFunctionsWithStreamExample.java) projects for an example.

### Why am I getting connection timeouts?
Make sure that OpenAI is available in your country.

Expand Down
3 changes: 2 additions & 1 deletion api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ apply plugin: 'java-library'
apply plugin: "com.vanniktech.maven.publish"

dependencies {
implementation libs.jacksoAnnotations
api libs.jacksonAnnotations
api libs.jacksonDatabind
compileOnly libs.lombok
annotationProcessor libs.lombok

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,28 @@ public class ChatCompletionRequest {
* A unique identifier representing your end-user, which will help OpenAI to monitor and detect abuse.
*/
String user;

/**
* A list of the available functions.
*/
List<ChatFunction> functions;

/**
* Controls how the model responds to function calls, as specified in the <a href="https://platform.openai.com/docs/api-reference/chat/create#chat/create-function_call">OpenAI documentation</a>.
*/
@JsonProperty("function_call")
ChatCompletionRequestFunctionCall functionCall;

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class ChatCompletionRequestFunctionCall {
String name;

public static ChatCompletionRequestFunctionCall of(String name) {
return new ChatCompletionRequestFunctionCall(name);
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.theokanning.openai.completion.chat;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.NonNull;

import java.util.function.Function;

@Data
public class ChatFunction {

@NonNull
private String name;
private String description;
@JsonProperty("parameters")
private Class<?> parametersClass;

@JsonIgnore
private Function<Object, Object> executor;

public static Builder builder() {
return new Builder();
}

public static class Builder {
private String name;
private String description;
private Class<?> parameters;
private Function<Object, Object> executor;

public Builder name(String name) {
this.name = name;
return this;
}

public Builder description(String description) {
this.description = description;
return this;
}

public <T> Builder executor(Class<T> requestClass, Function<T, Object> executor) {
this.parameters = requestClass;
this.executor = (Function<Object, Object>) executor;
return this;
}

public ChatFunction build() {
ChatFunction chatFunction = new ChatFunction(name);
chatFunction.setDescription(description);
chatFunction.setParametersClass(parameters);
chatFunction.setExecutor(executor);
return chatFunction;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.theokanning.openai.completion.chat;

import com.fasterxml.jackson.databind.JsonNode;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class ChatFunctionCall {

/**
* The name of the function being called
*/
String name;

/**
* The arguments of the call produced by the model, represented as a JsonNode for easy manipulation.
*/
JsonNode arguments;

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.theokanning.openai.completion.chat;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.*;

/**
Expand All @@ -19,13 +21,27 @@
public class ChatMessage {

/**
* Must be either 'system', 'user', or 'assistant'.<br>
* Must be either 'system', 'user', 'assistant' or 'function'.<br>
* You may use {@link ChatMessageRole} enum.
*/
@NonNull
String role;
@NonNull
@JsonInclude() // content should always exist in the call, even if it is null
String content;
//name is optional, The name of the author of this message. May contain a-z, A-Z, 0-9, and underscores, with a maximum length of 64 characters.
String name;
@JsonProperty("function_call")
ChatFunctionCall functionCall;

public ChatMessage(String role, String content) {
this.role = role;
this.content = content;
}

public ChatMessage(String role, String content, String name) {
this.role = role;
this.content = content;
this.name = name;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
public enum ChatMessageRole {
SYSTEM("system"),
USER("user"),
ASSISTANT("assistant");
ASSISTANT("assistant"),
FUNCTION("function");

private final String value;

Expand Down
20 changes: 20 additions & 0 deletions example/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,24 @@ application {

dependencies {
implementation project(":service")
}

task runExampleOne(type: JavaExec) {
mainClass.set('example.OpenAiApiExample')
classpath = sourceSets.main.runtimeClasspath
args = []
}

task runExampleTwo(type: JavaExec) {
mainClass.set('example.OpenAiApiFunctionsExample')
classpath = sourceSets.main.runtimeClasspath
args = []
standardInput = System.in
}

task runExampleThree(type: JavaExec) {
mainClass.set('example.OpenAiApiFunctionsWIthStreamExample')
classpath = sourceSets.main.runtimeClasspath
args = []
standardInput = System.in
}
Loading