Dopo aver visto in uno dei nostri articoli precedenti come implementare con Spring AI una function call, è arrivato il momento di approfondire ulteriormente l'argomento con un tutorial pratico sui FunctionTool. I FunctionTool sono necessari quando si vuole definire invocazioni a funzioni complesse dove serve dare maggiori dettagli al motore di AI sui parametri da passare in input alla chiamata.
Spring AI supporta nativamente questa funzionalità mediante la classe OpenAiApi.FunctionTool anche se, al momento in cui stiamo scrivendo l’articolo, non è presente nessun riferimento all’interno della documentazione ufficiale del framework. Siamo pertanto dei precursori sull’argomento che è ancora in fase di beta testing.
Come definire i FunctionTool
Per questo tutorial sui FunctionTool useremo il progetto Spring AI creato nei capitoli precedenti della nostra guida. Potete inoltre scaricare il codice del tutorial dal nostro sito GitHub:
Per implementare un tool in SpringAI sono necessarie quattro cose:
- Il Service che contiene la logica;
- Una FunctionCallback che definisce la funzione
- Un FunctionTool che aggiunge maggiori dettagli su come la funzione debba essere invocata
Service
Per semplicità useremo lo stesso service della funzione già implementata per calcolare l’area di un triangolo che prende in input base ed altezza. Trattandosi di un esempio molto semplice, non sarebbe stato necessario utilizzare un tool ma lo implementeremo ugualmente per spiegare come funziona.
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
La FunctionCallback
serve per spiegare a SpringAI come invocare la funzione e definisce:
- Il nome della funzione che verrà usato dall’algoritmo di OpenAI per referenziarla.
- La descrizione della funzione che serve ad OpenAI per capire quando usarla.
- un ResponseConverter che spiega come convertire in stringa le informazioni presenti nella risposta.
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
Il FunctionTool
è la parte più importante perché è ciò che differenzia una semplice funzione da un tool e serve appunto per aggiungere maggiori informazioni sulla funzione.
Per aggiungere informazioni sui parametri di input del tool è necessario definire un json con questo formato:
{
"type": "object",
"properties": {
"base": {
"type": "integer",
"description": "The base of the rectangle"
},
"height": {
"type": "integer",
"description": "The height of the rectangle"
}
},
"required": ["base", "height"]
}
Come si può notare, per ogni parametro è necessario indicare il tipo e una descrizione. Infine è possibile specificare anche quali parametri sono obbligatori e quali invece sono opzionali definendo la lista “required”.
Una volta definita la struttura in formato json, possiamo aggiungere la definizione del bean rappresentante il tool in Spring AI:
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);
}
}
Integrare i FunctionTool con OpenAI
Il nostro tool è finalmente pronto per essere usato. Per completare bisogna integrarlo all’interno della propria applicazione di AI. Definiamo quindi un nuovo endopoint nel nostro RestController
:
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();
}
}
Mediante il metodo withTools della classe OpenAiChatOptions.Builder
è possibile fornire ad OpenAI l’elenco di tutti i tool definiti nel proprio applicativo.
Non resta che fare il test finale invocando l’endpoint che abbiamo appena definito:
http://localhost:8080/ai/tool?message=puoi calcolare l'area di un rettangolo con base 3 e altezza 4?
L’output sarà come sempre un json all’interno con dentro l’output del modello:
{
"output": {
"messageType": "ASSISTANT",
"properties": {
"role": "ASSISTANT",
"finishReason": "STOP",
"id": "chatcmpl-9IaKBAKXnZ5KuDRNsYXZiH1bkdo1Z"
},
"content": "L'area del rettangolo con base 3 e altezza 4 è 12.",
"media": [
]
},
"metadata": {
"finishReason": "STOP",
"contentFilterMetadata": null
}
}
Spero che questo tutorial sia stato utile e vi invito a leggere anche gli altri articoli della nostra guida pratica su Spring AI.