Skip to content

Commit

Permalink
feat: functions new api
Browse files Browse the repository at this point in the history
  • Loading branch information
Angular2Guy committed Dec 6, 2024
1 parent fc1f4c8 commit 7e91fb6
Show file tree
Hide file tree
Showing 4 changed files with 11 additions and 162 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Description;

import ch.xxx.aidoclibchat.domain.client.OpenLibraryClient;

Expand All @@ -31,6 +32,7 @@ public FunctionConfig(OpenLibraryClient openLibraryClient) {
}

@Bean
@Description("Search for books by author, title or subject.")
public Function<OpenLibraryClient.Request, OpenLibraryClient.Response> openLibraryClient() {
return this.openLibraryClient::apply;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import ch.xxx.aidoclibchat.domain.client.OpenLibraryClient;
import ch.xxx.aidoclibchat.domain.model.dto.FunctionSearch;
import ch.xxx.aidoclibchat.usecase.service.FunctionService;

Expand All @@ -38,7 +37,7 @@ public FunctionController(FunctionService functionService) {
// }

@PostMapping(path="/books", produces = MediaType.APPLICATION_JSON_VALUE)
public OpenLibraryClient.Response postQuestion(@RequestBody FunctionSearch functionSearch) {
public String postQuestion(@RequestBody FunctionSearch functionSearch) {
return this.functionService.functionCall(functionSearch.question(), functionSearch.resultsAmount());
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
package ch.xxx.aidoclibchat.domain.client;

import java.util.List;
import java.util.Map;
import java.util.function.Function;

import org.springframework.boot.context.properties.bind.ConstructorBinding;

import com.fasterxml.jackson.annotation.JsonClassDescription;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;



Expand All @@ -36,68 +31,4 @@ record Request(@JsonProperty(required=false, value="author") @JsonPropertyDescri
@JsonProperty(required=false, value="subject") @JsonPropertyDescription("The book subject") String subject) {}
@JsonIgnoreProperties(ignoreUnknown = true)
record Response(Long numFound, Long start, Boolean numFoundExact, List<Book> docs) {}

@JsonInclude(Include.NON_NULL)
record FunctionTool(
@JsonProperty("type") Type type,
@JsonProperty("function") Function function) {

/**
* Create a tool of type 'function' and the given function definition.
* @param function function definition.
*/
@ConstructorBinding
public FunctionTool(Function function) {
this(Type.FUNCTION, function);
}

/**
* Create a tool of type 'function' and the given function definition.
*/
public enum Type {
/**
* Function tool type.
*/
@JsonProperty("function") FUNCTION
}

/**
* Function definition.
*
* @param description A description of what the function does, used by the model to choose when and how to call
* the function.
* @param name The name of the function to be called. Must be a-z, A-Z, 0-9, or contain underscores and dashes,
* with a maximum length of 64.
* @param parameters The parameters the functions accepts, described as a JSON Schema object. To describe a
* function that accepts no parameters, provide the value {"type": "object", "properties": {}}.
*/
public record Function(
@JsonProperty("description") String description,
@JsonProperty("name") String name,
@JsonProperty("parameters") Map<String, Object> parameters) {

/**
* Create tool function definition.
*
* @param description tool function description.
* @param name tool function name.
* @param jsonSchema tool function schema as json.
*/
@ConstructorBinding
public Function(String description, String name, String jsonSchema) {
this(description, name, parseJson(jsonSchema));
}
}
}

static Map<String, Object> parseJson(String jsonSchema) {
try {
return new ObjectMapper().readValue(jsonSchema,
new TypeReference<Map<String, Object>>() {
});
}
catch (Exception e) {
throw new RuntimeException("Failed to parse schema: " + jsonSchema, e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,122 +12,39 @@
*/
package ch.xxx.aidoclibchat.usecase.service;

import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.ChatClient.Builder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.bind.ConstructorBinding;
import org.springframework.stereotype.Service;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import ch.xxx.aidoclibchat.domain.client.OpenLibraryClient;
import ch.xxx.aidoclibchat.domain.client.OpenLibraryClient.FunctionTool.Type;
import ch.xxx.aidoclibchat.domain.client.OpenLibraryClient.Response;

@Service
public class FunctionService {
private static final Logger LOGGER = LoggerFactory.getLogger(FunctionService.class);
private final ObjectMapper objectMapper;
private final ChatClient chatClient;
private final OpenLibraryClient openLibraryClient;
private final List<String> nullCodes = List.of("none", "string");
private final String promptStr = """
You have access to the following tools:
%s
You must follow these instructions:
Always select one or more of the above tools based on the user query
If a tool is found, you must respond in the JSON format matching the following schema:
{"tools": [{
"tool": "<name of the selected tool>",
"tool_input": "<parameters for the selected tool, matching the tool's JSON schema>"
}]}
Make sure to include all tool parameters in the JSON at tool_input.
If there is no tool that match the user request, you will respond with empty json.
Do not add any additional Notes or Explanations. Respond only with the JSON.
Make sure to have all the parameters when calling a function.
If a parameter is missing ask the user for the parameter.
User Query:
%s
""";

private record Tool(@JsonProperty("tool") String tool, @JsonProperty("tool_input") Map<String, Object> toolInput) {
@ConstructorBinding
public Tool(String tool, String jsonSchema) {
this(tool, OpenLibraryClient.parseJson(jsonSchema));
}
}

private record Tools(@JsonProperty("tools") List<Tool> tools) {
}

@Value("${spring.profiles.active:}")
private String activeProfile;

public FunctionService(ObjectMapper objectMapper, Builder builder, OpenLibraryClient openLibraryClient) {
this.objectMapper = objectMapper;
public FunctionService(Builder builder) {
this.chatClient = builder.build();
this.openLibraryClient = openLibraryClient;
}

public Response functionCall(String question, Long resultsAmount) {
public String functionCall(String question, Long resultsAmount) {
if (!this.activeProfile.contains("ollama")) {
return new Response(0L, 0L, false, List.of());
}
var description = "Search for books by author, title or subject.";
var name = "booksearch";
var aiFunction = new OpenLibraryClient.FunctionTool(Type.FUNCTION, new OpenLibraryClient.FunctionTool.Function(
description, name, Map.of("author", "string", "title", "string", "subject", "string")));
String jsonStr = "";
try {
jsonStr = this.objectMapper.writeValueAsString(aiFunction);
} catch (JsonProcessingException e) {
LOGGER.error("Json Mapping failed.", e);
}
var query = String.format(this.promptStr, jsonStr, question);
int aiCallCounter = 0;
var responseRef = new AtomicReference<Response>(new Response(0L, 0L, false, List.of()));
List<Tool> myToolsList = List.of();
while (aiCallCounter < 3 && myToolsList.isEmpty()) {
aiCallCounter += 1;
var response = this.chatClient.prompt().user(u -> u.text(query)).call().chatResponse().getResult().getOutput().getContent();
try {
response = response.substring(response.indexOf("{"), response.lastIndexOf("}") + 1);
final var atomicResponse = new AtomicReference<String>(response);
this.nullCodes.forEach(myCode -> {
var myResponse = atomicResponse.get();
atomicResponse.set(myResponse.replaceAll(myCode, ""));
});
var myTools = this.objectMapper.readValue(atomicResponse.get(), Tools.class);
// LOGGER.info(myTools.toString());
myToolsList = myTools.tools().stream()
.filter(myTool1 -> myTool1.toolInput().values().stream()
.filter(myValue -> (myValue instanceof String) && !((String) myValue).isBlank())
.findFirst().isPresent())
.toList();
if (myToolsList.isEmpty()) {
throw new RuntimeException("No parameters found.");
}
} catch (Exception e) {
LOGGER.error("Chatresult Json Mapping failed.", e);
LOGGER.error("ChatResponse: {}", response);
}
return "";
}

myToolsList.forEach(myTool -> {
var myRequest = new OpenLibraryClient.Request((String) myTool.toolInput().get("author"),
(String) myTool.toolInput().get("title"), (String) myTool.toolInput().get("subject"));
var myResponse = this.openLibraryClient.apply(myRequest);
// LOGGER.info(myResponse.toString());
responseRef.set(myResponse);
});
return responseRef.get();
var result = this.chatClient.prompt().user(this.promptStr + question).functions("openLibraryClient").call().content();
return result;
}

}

0 comments on commit 7e91fb6

Please sign in to comment.