From 8968abb86ed49b4cf053435f4f3b3f1ca0d4a283 Mon Sep 17 00:00:00 2001 From: HamaWhiteGG Date: Thu, 30 Nov 2023 20:31:58 +0800 Subject: [PATCH] Add OpenAI Function Call Example #109 --- .../OpenAI Function Call Example.md | 165 ++++++++++++++++++ .../hw/openai/function/ChatFunctionTest.java | 3 +- 2 files changed, 166 insertions(+), 2 deletions(-) create mode 100644 data/docs/openai-client/OpenAI Function Call Example.md diff --git a/data/docs/openai-client/OpenAI Function Call Example.md b/data/docs/openai-client/OpenAI Function Call Example.md new file mode 100644 index 000000000..c0b48e9e8 --- /dev/null +++ b/data/docs/openai-client/OpenAI Function Call Example.md @@ -0,0 +1,165 @@ +## OpenAI Function Call Example +From the project: https://github.com/HamaWhiteGG/langchain-java + +Please refer to the [ChatFunctionTest](https://github.com/HamaWhiteGG/langchain-java/blob/dev/openai-client/src/test/java/com/hw/openai/function/ChatFunctionTest.java) for the complete test code. + +## 1. Core main process +- Generates a ChatParameter instance that represents the given class. +- Invoke the OpenAI API and return the function name and parameters. +- Execute functions through the FunctionExecutor. + +## 2. Step-by-step introduction +### 2.1 Declare function parameters and response +```java +public record Weather( + @JsonProperty(required = true) + @JsonPropertyDescription("The city and state, e.g. San Francisco, CA") + String location, + + @JsonPropertyDescription("The temperature unit") + WeatherUnit unit +) {} + + +public enum WeatherUnit { + + CELSIUS, + FAHRENHEIT +} + + +@Data +@Builder +public class WeatherResponse { + + public String location; + + public WeatherUnit unit; + + public int temperature; + + public String description; +} + + +WeatherResponse getCurrentWeather(Weather weather) { + // mock function + return WeatherResponse.builder() + .location(weather.location()) + .unit(weather.unit()) + .temperature(new Random().nextInt(50)) + .description("sunny") + .build(); +} +``` + + +### 2.2 Generates a ChatParameter instance that represents the given class +Automatically convert `Weather.class` to `ChatParameter` using the `ChatParameterUtils.generate` method. +```java +ChatFunction.ChatParameter chatParameter = ChatParameterUtils.generate(Weather.class); +``` +The output of converting `chatParameter` to JSON String is as follows: +```json +{ + "type" : "object", + "properties" : { + "location" : { + "type" : "string", + "description" : "The city and state, e.g. San Francisco, CA" + }, + "unit" : { + "type" : "string", + "description" : "The temperature unit", + "enum" : [ + "celsius", + "fahrenheit" + ] + } + }, + "required" : [ + "location" + ] +} +``` +### 2.3 Invoke the OpenAI API and return the function name and parameters. +```java +ChatFunction chatFunction = ChatFunction.builder() + .name(functionName) + .description("Get the current weather in a given location") + .parameters(ChatParameterUtils.generate(Weather.class)) + .build(); + +Message message = Message.of("What is the weather like in Boston?"); + +ChatCompletion chatCompletion = ChatCompletion.builder() + .model("gpt-4") + .temperature(0) + .messages(List.of(message)) + .tools(List.of(new Tool(chatFunction))) + .toolChoice("auto") + .build(); + +ChatCompletionResp response = client.createChatCompletion(chatCompletion); +ChatChoice chatChoice = response.getChoices().get(0); +``` + +The output of converting `chatChoice` to JSON String is as follows: +```json +{ + "index":0, + "message":{ + "role":"assistant", + "content":null, + "tool_calls":[ + { + "id":"call_aS4LspVVBkE8uIiBSLZWXe6O", + "type":"function", + "function":{ + "name":"get_current_weather", + "arguments":"{\n \"location\": \"Boston, MA\"\n}" + } + } + ] + }, + "finish_reason":"tool_calls" +} +``` + +### 2.4 Execute functions through the FunctionExecutor. +The code for `FunctionExecutor` is as follows, allowing the caller to avoid explicit type casting through generics, making the code more concise and elegant. +```java +public class FunctionExecutor { + + private static final Map> FUNCTION_MAP = new HashMap<>(16); + + public static void register(String functionName, Function function) { + FUNCTION_MAP.put(functionName, function); + } + + @SuppressWarnings("unchecked") + public static R execute(String functionName, Class clazz, String arguments) throws ClassCastException { + Function function = (Function) FUNCTION_MAP.get(functionName); + if (function == null) { + throw new IllegalArgumentException("No function registered with name: " + functionName); + } + T input = convertFromJsonStr(arguments, clazz); + return function.apply(input); + } +} +``` + +The caller's code is as follows: +```java +Function function = chatChoice.getMessage().getToolCalls().get(0).getFunction(); + +// execute function +WeatherResponse weatherResponse = FunctionExecutor.execute(function.getName(), Weather.class, function.getArguments()); +System.out.println() +``` + +The final result is: +```shell +WeatherResponse(location=Boston, MA, unit=null, temperature=2, description=sunny) +``` + diff --git a/openai-client/src/test/java/com/hw/openai/function/ChatFunctionTest.java b/openai-client/src/test/java/com/hw/openai/function/ChatFunctionTest.java index 19a145b84..d28417a9f 100644 --- a/openai-client/src/test/java/com/hw/openai/function/ChatFunctionTest.java +++ b/openai-client/src/test/java/com/hw/openai/function/ChatFunctionTest.java @@ -84,7 +84,6 @@ void testChatFunction() { ChatChoice chatChoice = response.getChoices().get(0); LOG.info("result: {}", chatChoice); assertThat(chatChoice).isNotNull(); - assertEquals("tool_calls", chatChoice.getFinishReason()); Function function = chatChoice.getMessage().getToolCalls().get(0).getFunction(); @@ -97,7 +96,7 @@ void testChatFunction() { }"""; assertEquals(expectedArguments, function.getArguments()); - // register execute + // execute function WeatherResponse weatherResponse = FunctionExecutor.execute(function.getName(), Weather.class, function.getArguments()); LOG.info("result: {}", weatherResponse);