diff --git a/backend/src/main/java/ch/xxx/aidoclibchat/adapter/config/FunctionConfig.java b/backend/src/main/java/ch/xxx/aidoclibchat/adapter/config/FunctionConfig.java index b323b06..633e4a7 100644 --- a/backend/src/main/java/ch/xxx/aidoclibchat/adapter/config/FunctionConfig.java +++ b/backend/src/main/java/ch/xxx/aidoclibchat/adapter/config/FunctionConfig.java @@ -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; @@ -31,6 +32,7 @@ public FunctionConfig(OpenLibraryClient openLibraryClient) { } @Bean + @Description("Search for books by author, title or subject.") public Function openLibraryClient() { return this.openLibraryClient::apply; } diff --git a/backend/src/main/java/ch/xxx/aidoclibchat/adapter/controller/FunctionController.java b/backend/src/main/java/ch/xxx/aidoclibchat/adapter/controller/FunctionController.java index 34fa2f8..ee0244e 100644 --- a/backend/src/main/java/ch/xxx/aidoclibchat/adapter/controller/FunctionController.java +++ b/backend/src/main/java/ch/xxx/aidoclibchat/adapter/controller/FunctionController.java @@ -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; @@ -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()); } diff --git a/backend/src/main/java/ch/xxx/aidoclibchat/domain/client/OpenLibraryClient.java b/backend/src/main/java/ch/xxx/aidoclibchat/domain/client/OpenLibraryClient.java index ba88aa0..b3db06e 100644 --- a/backend/src/main/java/ch/xxx/aidoclibchat/domain/client/OpenLibraryClient.java +++ b/backend/src/main/java/ch/xxx/aidoclibchat/domain/client/OpenLibraryClient.java @@ -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; @@ -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 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 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 parseJson(String jsonSchema) { - try { - return new ObjectMapper().readValue(jsonSchema, - new TypeReference>() { - }); - } - catch (Exception e) { - throw new RuntimeException("Failed to parse schema: " + jsonSchema, e); - } - } } diff --git a/backend/src/main/java/ch/xxx/aidoclibchat/usecase/service/FunctionService.java b/backend/src/main/java/ch/xxx/aidoclibchat/usecase/service/FunctionService.java index 0cf2f69..7f1e16a 100644 --- a/backend/src/main/java/ch/xxx/aidoclibchat/usecase/service/FunctionService.java +++ b/backend/src/main/java/ch/xxx/aidoclibchat/usecase/service/FunctionService.java @@ -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 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": "", - "tool_input": "" - }]} - 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 toolInput) { - @ConstructorBinding - public Tool(String tool, String jsonSchema) { - this(tool, OpenLibraryClient.parseJson(jsonSchema)); - } - } - - private record Tools(@JsonProperty("tools") List 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(new Response(0L, 0L, false, List.of())); - List 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(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; } + }