DevTurtle logo DevTurtle

Spring AI – Tutorial FunctionTool with OpenAI

guide/ Spring AI guide

After having seen in one of our previous articles how to implement a function call with Spring AI, the time has come to delve further into the topic with a practical tutorial on FunctionTool. Function Tools are necessary when you want to define complex function invocations where you need to give more details to the AI engine on the parameters to be passed as input to the call.

Spring AI natively supports this functionality through the OpenAiApi.FunctionTool class even if, at the time we are writing the article, there is no reference in the official documentation of the framework. We are therefore pioneers on the topic which is still in the beta testing phase.

How to define FunctionTool

For this FunctionTool tutorial we will use the Spring AI project created in the previous chapters of our guide. You can also download the tutorial code from our GitHub site:

git icon
Git RepositorySource Download

To implement a tool in SpringAI you need four things:

  • The Service that contains the logic;
  • A FunctionCallback that defines the function
  • A FunctionTool that adds more details about how the function should be invoked

Service

For simplicity we will use the same service as the function already implemented to calculate the area of a triangle that takes base and height as input. Since this is a very simple example, it would not have been necessary to use a tool but we will implement it anyway to explain how it works.

Java
package com.example.aidemo.service;

import java.util.function.Function;

import org.springframework.stereotype.Service;

import com.example.aidemo.service.RectangleAreaService.Request;
import com.example.aidemo.service.RectangleAreaService.Response;

@Service
public class RectangleAreaService implements Function<Request, Response> {

	// Request for RectangleAreaService
	public record Request(double base, double height) {}
	public record Response(double area) {}

	@Override
	public Response apply(Request r) {
		return new Response(r.base()*r.height());
	}
	
}

FunctionCallback

The FunctionCallback is used to explain to SpringAI how to invoke the function and defines:

  • The name of the function that will be used by the OpenAI algorithm to reference it.
  • The description of the function that OpenAI needs to understand when to use it.
  • a ResponseConverter that explains how to convert the information in the response into a string.
Java
package com.example.aidemo.config;

import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.function.FunctionCallbackWrapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.example.aidemo.service.RectangleAreaService;

@Configuration
public class Tools {
	
	public static final String RECTANGLE_AREA_FUNCTION_NAME = "rectangleAreaFunction";
	public static final String RECTANGLE_AREA_TOOL_NAME = "rectangleAreaTool";
	public static final String RECTANGLE_AREA_FUNCTION_DESCRIPTION = "Calculate the area of a rectangle";
	
	@Bean(RECTANGLE_AREA_FUNCTION_NAME)
	public FunctionCallback rectangleAreaFunctionCallback(RectangleAreaService rectangleAreaService) {
		return FunctionCallbackWrapper.builder(rectangleAreaService)
				.withName(RECTANGLE_AREA_FUNCTION_NAME).withDescription(RECTANGLE_AREA_FUNCTION_DESCRIPTION)
				.withResponseConverter(response -> Double.toString(response.area()))
				.build();
	}

FunctionTool

The FunctionTool is the most important part because it is what differentiates a simple function from a tool and is used to add more information about the function.

To add information on the tool input parameters it is necessary to define a json with this format:

JSON
{
    "type": "object",
    "properties": {
        "base": {
            "type": "integer",
            "description": "The base of the rectangle"
        },
        "height": {
            "type": "integer",
            "description": "The height of the rectangle"
        }
    },
    "required": ["base", "height"]
}

As you can see, for each parameter it is necessary to indicate the type and a description. Finally, it is also possible to specify which parameters are mandatory and which are optional by defining the “required” list.

Once the structure has been defined in json format, we can add the definition of the bean representing the tool in Spring AI:

Java
package com.example.aidemo.config;

import org.springframework.ai.model.ModelOptionsUtils;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.function.FunctionCallbackWrapper;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.example.aidemo.service.RectangleAreaService;

@Configuration
public class Tools {
	
	public static final String RECTANGLE_AREA_FUNCTION_NAME = "rectangleAreaFunction";
	public static final String RECTANGLE_AREA_TOOL_NAME = "rectangleAreaTool";
	public static final String RECTANGLE_AREA_FUNCTION_DESCRIPTION = "Calculate the area of a rectangle";
	
	@Bean(RECTANGLE_AREA_FUNCTION_NAME)
	public FunctionCallback rectangleAreaFunctionCallback(RectangleAreaService rectangleAreaService) {
		return FunctionCallbackWrapper.builder(rectangleAreaService)
				.withName(RECTANGLE_AREA_FUNCTION_NAME).withDescription(RECTANGLE_AREA_FUNCTION_DESCRIPTION)
				.withResponseConverter(response -> Double.toString(response.area()))
				.build();
	}
	
	@Bean(RECTANGLE_AREA_TOOL_NAME)
	public OpenAiApi.FunctionTool rectangleAreaFunctionTool() {
		String jsonToolDescription = "{\n"
				+ "    \"type\": \"object\",\n"
				+ "    \"properties\": {\n"
				+ "        \"base\": {\n"
				+ "            \"type\": \"integer\",\n"
				+ "            \"description\": \"The base of the rectangle\"\n"
				+ "        },\n"
				+ "        \"height\": {\n"
				+ "            \"type\": \"integer\",\n"
				+ "            \"description\": \"The height of the rectangle\"\n"
				+ "        }\n"
				+ "    },\n"
				+ "    \"required\": [\"base\", \"height\"]\n"
				+ "}\n"
				+ "";
		
		OpenAiApi.FunctionTool.Function function = 
				new OpenAiApi.FunctionTool.Function(
						RECTANGLE_AREA_FUNCTION_DESCRIPTION, RECTANGLE_AREA_FUNCTION_NAME,
						ModelOptionsUtils.jsonToMap(jsonToolDescription));
		
		return new OpenAiApi.FunctionTool(OpenAiApi.FunctionTool.Type.FUNCTION, function);
	}
	
}

Integrate FunctionTool with OpenAI

Our tool is finally ready to be used. To complete it you need to integrate it into your AI application. We then define a new endpoint in our RestController:

Java
package com.example.aidemo.controller;

import java.util.List;

import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.ai.openai.api.OpenAiApi.FunctionTool;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class OpenAiRestController {
	
	private final ChatModel chatModel;
	private final OpenAiApi.FunctionTool rectangleAreaTool;
	
	@Autowired
    public OpenAiRestController(OpenAiChatModel chatModel, OpenAiApi.FunctionTool rectangleAreaTool) {
        this.chatModel = chatModel;
        this.rectangleAreaTool = rectangleAreaTool;
    }  
	
	@GetMapping("/ai/tool")
	public Generation toolCalling(@RequestParam(value = "message") String message) {
		new UserMessage(message);
		
		List<FunctionTool> toolsList = List.of(rectangleAreaTool);
		Prompt prompt = new Prompt(message, OpenAiChatOptions.builder()
				.withTools(toolsList)
				.build());
		
		ChatResponse response = chatModel.call(prompt);
		return response.getResult();
	}
	
}

Using the withTools method of the OpenAiChatOptions.Builder class it is possible to provide OpenAI with the list of all the tools defined in your application.

All that remains is to do the final test by invoking the endpoint we just defined:

http://localhost:8080/ai/tool?message=Can you calculate the area of a rectangle with base 3 and height 4?

The output will as always be a json inside with the model output inside:

JSON
{
  "output": {
    "messageType": "ASSISTANT",
    "properties": {
      "role": "ASSISTANT",
      "finishReason": "STOP",
      "id": "chatcmpl-9IcKxROPTPdVLGLoGY9Hc6TUr5EFC"
    },
    "content": "The area of a rectangle with a base of 3 and a height of 4 is 12.",
    "media": [
      
    ]
  },
  "metadata": {
    "finishReason": "STOP",
    "contentFilterMetadata": null
  }
}

I hope this tutorial has been useful and I invite you to read the other articles in our practical guide on Spring AI.