From a09f1362e9e34be245426ca2bf469f9b93bacf08 Mon Sep 17 00:00:00 2001 From: Markus Klenke Date: Mon, 2 Dec 2024 22:48:33 +0100 Subject: [PATCH 01/20] Adds Builder for EmbedRequests and deprecates old Embedding Models --- .../models/chat/OllamaChatRequestBuilder.java | 6 +-- .../embeddings/OllamaEmbedRequestBuilder.java | 40 +++++++++++++++++++ .../OllamaEmbeddingResponseModel.java | 1 + .../OllamaEmbeddingsRequestBuilder.java | 1 + .../OllamaEmbeddingsRequestModel.java | 1 + ...ava => TestEmbedRequestSerialization.java} | 19 ++++----- 6 files changed, 56 insertions(+), 12 deletions(-) create mode 100644 src/main/java/io/github/ollama4j/models/embeddings/OllamaEmbedRequestBuilder.java rename src/test/java/io/github/ollama4j/unittests/jackson/{TestEmbeddingsRequestSerialization.java => TestEmbedRequestSerialization.java} (55%) diff --git a/src/main/java/io/github/ollama4j/models/chat/OllamaChatRequestBuilder.java b/src/main/java/io/github/ollama4j/models/chat/OllamaChatRequestBuilder.java index 7cdf879..c9882d0 100644 --- a/src/main/java/io/github/ollama4j/models/chat/OllamaChatRequestBuilder.java +++ b/src/main/java/io/github/ollama4j/models/chat/OllamaChatRequestBuilder.java @@ -45,7 +45,7 @@ public class OllamaChatRequestBuilder { try { return Files.readAllBytes(file.toPath()); } catch (IOException e) { - LOG.warn(String.format("File '%s' could not be accessed, will not add to message!", file.toPath()), e); + LOG.warn("File '{}' could not be accessed, will not add to message!", file.toPath(), e); return new byte[0]; } }).collect(Collectors.toList()); @@ -63,9 +63,9 @@ public class OllamaChatRequestBuilder { try { binaryImages.add(Utils.loadImageBytesFromUrl(imageUrl)); } catch (URISyntaxException e) { - LOG.warn(String.format("URL '%s' could not be accessed, will not add to message!", imageUrl), e); + LOG.warn("URL '{}' could not be accessed, will not add to message!", imageUrl, e); } catch (IOException e) { - LOG.warn(String.format("Content of URL '%s' could not be read, will not add to message!", imageUrl), e); + LOG.warn("Content of URL '{}' could not be read, will not add to message!", imageUrl, e); } } } diff --git a/src/main/java/io/github/ollama4j/models/embeddings/OllamaEmbedRequestBuilder.java b/src/main/java/io/github/ollama4j/models/embeddings/OllamaEmbedRequestBuilder.java new file mode 100644 index 0000000..7b85789 --- /dev/null +++ b/src/main/java/io/github/ollama4j/models/embeddings/OllamaEmbedRequestBuilder.java @@ -0,0 +1,40 @@ +package io.github.ollama4j.models.embeddings; + +import io.github.ollama4j.utils.Options; + +import java.util.List; + +/** + * Builderclass to easily create Requests for Embedding models using ollama. + */ +public class OllamaEmbedRequestBuilder { + + private final OllamaEmbedRequestModel request; + + private OllamaEmbedRequestBuilder(String model, List input) { + this.request = new OllamaEmbedRequestModel(model,input); + } + + public static OllamaEmbedRequestBuilder getInstance(String model, String... input){ + return new OllamaEmbedRequestBuilder(model, List.of(input)); + } + + public OllamaEmbedRequestBuilder withOptions(Options options){ + this.request.setOptions(options.getOptionsMap()); + return this; + } + + public OllamaEmbedRequestBuilder withKeepAlive(String keepAlive){ + this.request.setKeepAlive(keepAlive); + return this; + } + + public OllamaEmbedRequestBuilder withoutTruncate(){ + this.request.setTruncate(false); + return this; + } + + public OllamaEmbedRequestModel build() { + return this.request; + } +} diff --git a/src/main/java/io/github/ollama4j/models/embeddings/OllamaEmbeddingResponseModel.java b/src/main/java/io/github/ollama4j/models/embeddings/OllamaEmbeddingResponseModel.java index 24d95bc..dcf7b47 100644 --- a/src/main/java/io/github/ollama4j/models/embeddings/OllamaEmbeddingResponseModel.java +++ b/src/main/java/io/github/ollama4j/models/embeddings/OllamaEmbeddingResponseModel.java @@ -7,6 +7,7 @@ import lombok.Data; @SuppressWarnings("unused") @Data +@Deprecated(since="1.0.90") public class OllamaEmbeddingResponseModel { @JsonProperty("embedding") private List embedding; diff --git a/src/main/java/io/github/ollama4j/models/embeddings/OllamaEmbeddingsRequestBuilder.java b/src/main/java/io/github/ollama4j/models/embeddings/OllamaEmbeddingsRequestBuilder.java index b542931..47daf75 100644 --- a/src/main/java/io/github/ollama4j/models/embeddings/OllamaEmbeddingsRequestBuilder.java +++ b/src/main/java/io/github/ollama4j/models/embeddings/OllamaEmbeddingsRequestBuilder.java @@ -2,6 +2,7 @@ package io.github.ollama4j.models.embeddings; import io.github.ollama4j.utils.Options; +@Deprecated(since="1.0.90") public class OllamaEmbeddingsRequestBuilder { private OllamaEmbeddingsRequestBuilder(String model, String prompt){ diff --git a/src/main/java/io/github/ollama4j/models/embeddings/OllamaEmbeddingsRequestModel.java b/src/main/java/io/github/ollama4j/models/embeddings/OllamaEmbeddingsRequestModel.java index d700b91..d68624c 100644 --- a/src/main/java/io/github/ollama4j/models/embeddings/OllamaEmbeddingsRequestModel.java +++ b/src/main/java/io/github/ollama4j/models/embeddings/OllamaEmbeddingsRequestModel.java @@ -12,6 +12,7 @@ import lombok.RequiredArgsConstructor; @Data @RequiredArgsConstructor @NoArgsConstructor +@Deprecated(since="1.0.90") public class OllamaEmbeddingsRequestModel { @NonNull private String model; diff --git a/src/test/java/io/github/ollama4j/unittests/jackson/TestEmbeddingsRequestSerialization.java b/src/test/java/io/github/ollama4j/unittests/jackson/TestEmbedRequestSerialization.java similarity index 55% rename from src/test/java/io/github/ollama4j/unittests/jackson/TestEmbeddingsRequestSerialization.java rename to src/test/java/io/github/ollama4j/unittests/jackson/TestEmbedRequestSerialization.java index 7cb0297..534b204 100644 --- a/src/test/java/io/github/ollama4j/unittests/jackson/TestEmbeddingsRequestSerialization.java +++ b/src/test/java/io/github/ollama4j/unittests/jackson/TestEmbedRequestSerialization.java @@ -1,36 +1,37 @@ package io.github.ollama4j.unittests.jackson; import static org.junit.jupiter.api.Assertions.assertEquals; + +import io.github.ollama4j.models.embeddings.OllamaEmbedRequestBuilder; +import io.github.ollama4j.models.embeddings.OllamaEmbedRequestModel; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import io.github.ollama4j.models.embeddings.OllamaEmbeddingsRequestModel; -import io.github.ollama4j.models.embeddings.OllamaEmbeddingsRequestBuilder; import io.github.ollama4j.utils.OptionsBuilder; -public class TestEmbeddingsRequestSerialization extends AbstractSerializationTest { +public class TestEmbedRequestSerialization extends AbstractSerializationTest { - private OllamaEmbeddingsRequestBuilder builder; + private OllamaEmbedRequestBuilder builder; @BeforeEach public void init() { - builder = OllamaEmbeddingsRequestBuilder.getInstance("DummyModel","DummyPrompt"); + builder = OllamaEmbedRequestBuilder.getInstance("DummyModel","DummyPrompt"); } @Test public void testRequestOnlyMandatoryFields() { - OllamaEmbeddingsRequestModel req = builder.build(); + OllamaEmbedRequestModel req = builder.build(); String jsonRequest = serialize(req); - assertEqualsAfterUnmarshalling(deserialize(jsonRequest,OllamaEmbeddingsRequestModel.class), req); + assertEqualsAfterUnmarshalling(deserialize(jsonRequest,OllamaEmbedRequestModel.class), req); } @Test public void testRequestWithOptions() { OptionsBuilder b = new OptionsBuilder(); - OllamaEmbeddingsRequestModel req = builder + OllamaEmbedRequestModel req = builder .withOptions(b.setMirostat(1).build()).build(); String jsonRequest = serialize(req); - OllamaEmbeddingsRequestModel deserializeRequest = deserialize(jsonRequest,OllamaEmbeddingsRequestModel.class); + OllamaEmbedRequestModel deserializeRequest = deserialize(jsonRequest,OllamaEmbedRequestModel.class); assertEqualsAfterUnmarshalling(deserializeRequest, req); assertEquals(1, deserializeRequest.getOptions().get("mirostat")); } From 726fea5b743cae83620a921d27c8b412906498d3 Mon Sep 17 00:00:00 2001 From: Markus Klenke Date: Wed, 4 Dec 2024 22:28:00 +0100 Subject: [PATCH 02/20] Fixes #79 --- src/main/java/io/github/ollama4j/OllamaAPI.java | 2 +- .../ollama4j/integrationtests/TestRealAPIs.java | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/github/ollama4j/OllamaAPI.java b/src/main/java/io/github/ollama4j/OllamaAPI.java index e504d7e..0c89888 100644 --- a/src/main/java/io/github/ollama4j/OllamaAPI.java +++ b/src/main/java/io/github/ollama4j/OllamaAPI.java @@ -193,7 +193,7 @@ public class OllamaAPI { Elements modelSections = doc.selectXpath("//*[@id='repo']/ul/li/a"); for (Element e : modelSections) { LibraryModel model = new LibraryModel(); - Elements names = e.select("div > h2 > span"); + Elements names = e.select("div > h2 > div > span"); Elements desc = e.select("div > p"); Elements pullCounts = e.select("div:nth-of-type(2) > p > span:first-of-type > span:first-of-type"); Elements popularTags = e.select("div > div > span"); diff --git a/src/test/java/io/github/ollama4j/integrationtests/TestRealAPIs.java b/src/test/java/io/github/ollama4j/integrationtests/TestRealAPIs.java index d584747..0a1da61 100644 --- a/src/test/java/io/github/ollama4j/integrationtests/TestRealAPIs.java +++ b/src/test/java/io/github/ollama4j/integrationtests/TestRealAPIs.java @@ -80,6 +80,18 @@ class TestRealAPIs { } } + @Test + @Order(2) + void testListModelsFromLibrary() { + testEndpointReachability(); + try { + assertNotNull(ollamaAPI.listModelsFromLibrary()); + ollamaAPI.listModelsFromLibrary().forEach(System.out::println); + } catch (IOException | OllamaBaseException | InterruptedException | URISyntaxException e) { + fail(e); + } + } + @Test @Order(2) void testPullModel() { From ff3344616c480d8848ae861605fd472905b2859d Mon Sep 17 00:00:00 2001 From: Markus Klenke Date: Wed, 4 Dec 2024 22:54:54 +0100 Subject: [PATCH 03/20] Fixes NPE in #78 --- .../request/OllamaChatEndpointCaller.java | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/main/java/io/github/ollama4j/models/request/OllamaChatEndpointCaller.java b/src/main/java/io/github/ollama4j/models/request/OllamaChatEndpointCaller.java index b875bf8..e3d3fc1 100644 --- a/src/main/java/io/github/ollama4j/models/request/OllamaChatEndpointCaller.java +++ b/src/main/java/io/github/ollama4j/models/request/OllamaChatEndpointCaller.java @@ -1,7 +1,9 @@ package io.github.ollama4j.models.request; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; import io.github.ollama4j.exceptions.OllamaBaseException; +import io.github.ollama4j.models.chat.OllamaChatMessage; import io.github.ollama4j.models.response.OllamaResult; import io.github.ollama4j.models.chat.OllamaChatResponseModel; import io.github.ollama4j.models.chat.OllamaChatStreamObserver; @@ -31,13 +33,29 @@ public class OllamaChatEndpointCaller extends OllamaEndpointCaller { return "/api/chat"; } + /** + * Parses streamed Response line from ollama chat. + * Using {@link com.fasterxml.jackson.databind.ObjectMapper#readValue(String, TypeReference)} should throw + * {@link IllegalArgumentException} in case of null line or {@link com.fasterxml.jackson.core.JsonParseException} + * in case the JSON Object cannot be parsed to a {@link OllamaChatResponseModel}. Thus, the ResponseModel should + * never be null. + * + * @param line streamed line of ollama stream response + * @param responseBuffer Stringbuffer to add latest response message part to + * @return TRUE, if ollama-Response has 'done' state + */ @Override protected boolean parseResponseAndAddToBuffer(String line, StringBuilder responseBuffer) { try { OllamaChatResponseModel ollamaResponseModel = Utils.getObjectMapper().readValue(line, OllamaChatResponseModel.class); - responseBuffer.append(ollamaResponseModel.getMessage().getContent()); - if (streamObserver != null) { - streamObserver.notify(ollamaResponseModel); + // it seems that under heavy load ollama responds with an empty chat message part in the streamed response + // thus, we null check the message and hope that the next streamed response has some message content again + OllamaChatMessage message = ollamaResponseModel.getMessage(); + if(message != null) { + responseBuffer.append(message.getContent()); + if (streamObserver != null) { + streamObserver.notify(ollamaResponseModel); + } } return ollamaResponseModel.isDone(); } catch (JsonProcessingException e) { From 903a8176cda1753aae29b8f6e11612d204ebb022 Mon Sep 17 00:00:00 2001 From: Markus Klenke Date: Wed, 4 Dec 2024 08:45:00 +0100 Subject: [PATCH 04/20] Extends ToolSpec to have PromptDef for ChatRequests --- .../java/io/github/ollama4j/OllamaAPI.java | 8 +++++-- .../models/chat/OllamaChatRequest.java | 3 +++ .../github/ollama4j/tools/ToolRegistry.java | 11 +++++----- .../java/io/github/ollama4j/tools/Tools.java | 21 +++++++++++++++---- 4 files changed, 32 insertions(+), 11 deletions(-) diff --git a/src/main/java/io/github/ollama4j/OllamaAPI.java b/src/main/java/io/github/ollama4j/OllamaAPI.java index 0c89888..3421b1c 100644 --- a/src/main/java/io/github/ollama4j/OllamaAPI.java +++ b/src/main/java/io/github/ollama4j/OllamaAPI.java @@ -774,11 +774,15 @@ public class OllamaAPI { } else { result = requestCaller.callSync(request); } + + //add registered Tools to Request + + return new OllamaChatResult(result.getResponse(), result.getResponseTime(), result.getHttpStatusCode(), request.getMessages()); } public void registerTool(Tools.ToolSpecification toolSpecification) { - toolRegistry.addFunction(toolSpecification.getFunctionName(), toolSpecification.getToolDefinition()); + toolRegistry.addTool(toolSpecification.getFunctionName(), toolSpecification); } /** @@ -871,7 +875,7 @@ public class OllamaAPI { try { String methodName = toolFunctionCallSpec.getName(); Map arguments = toolFunctionCallSpec.getArguments(); - ToolFunction function = toolRegistry.getFunction(methodName); + ToolFunction function = toolRegistry.getToolFunction(methodName); if (verbose) { logger.debug("Invoking function {} with arguments {}", methodName, arguments); } diff --git a/src/main/java/io/github/ollama4j/models/chat/OllamaChatRequest.java b/src/main/java/io/github/ollama4j/models/chat/OllamaChatRequest.java index e6e528d..5d19703 100644 --- a/src/main/java/io/github/ollama4j/models/chat/OllamaChatRequest.java +++ b/src/main/java/io/github/ollama4j/models/chat/OllamaChatRequest.java @@ -3,6 +3,7 @@ package io.github.ollama4j.models.chat; import java.util.List; import io.github.ollama4j.models.request.OllamaCommonRequest; +import io.github.ollama4j.tools.Tools; import io.github.ollama4j.utils.OllamaRequestBody; import lombok.Getter; @@ -21,6 +22,8 @@ public class OllamaChatRequest extends OllamaCommonRequest implements OllamaRequ private List messages; + private List tools; + public OllamaChatRequest() {} public OllamaChatRequest(String model, List messages) { diff --git a/src/main/java/io/github/ollama4j/tools/ToolRegistry.java b/src/main/java/io/github/ollama4j/tools/ToolRegistry.java index 2ead13a..bb504c6 100644 --- a/src/main/java/io/github/ollama4j/tools/ToolRegistry.java +++ b/src/main/java/io/github/ollama4j/tools/ToolRegistry.java @@ -4,13 +4,14 @@ import java.util.HashMap; import java.util.Map; public class ToolRegistry { - private final Map functionMap = new HashMap<>(); + private final Map tools = new HashMap<>(); - public ToolFunction getFunction(String name) { - return functionMap.get(name); + public ToolFunction getToolFunction(String name) { + final Tools.ToolSpecification toolSpecification = tools.get(name); + return toolSpecification !=null ? toolSpecification.getToolFunction() : null ; } - public void addFunction(String name, ToolFunction function) { - functionMap.put(name, function); + public void addTool (String name, Tools.ToolSpecification specification) { + tools.put(name, specification); } } diff --git a/src/main/java/io/github/ollama4j/tools/Tools.java b/src/main/java/io/github/ollama4j/tools/Tools.java index 986302f..eb8dcca 100644 --- a/src/main/java/io/github/ollama4j/tools/Tools.java +++ b/src/main/java/io/github/ollama4j/tools/Tools.java @@ -6,8 +6,10 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonProcessingException; import io.github.ollama4j.utils.Utils; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; import java.util.ArrayList; import java.util.HashMap; @@ -20,17 +22,23 @@ public class Tools { public static class ToolSpecification { private String functionName; private String functionDescription; - private Map properties; - private ToolFunction toolDefinition; + private PromptFuncDefinition toolPrompt; + private ToolFunction toolFunction; } @Data @JsonIgnoreProperties(ignoreUnknown = true) + @Builder + @NoArgsConstructor + @AllArgsConstructor public static class PromptFuncDefinition { private String type; private PromptFuncSpec function; @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor public static class PromptFuncSpec { private String name; private String description; @@ -38,6 +46,9 @@ public class Tools { } @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor public static class Parameters { private String type; private Map properties; @@ -46,6 +57,8 @@ public class Tools { @Data @Builder + @NoArgsConstructor + @AllArgsConstructor public static class Property { private String type; private String description; @@ -94,10 +107,10 @@ public class Tools { PromptFuncDefinition.Parameters parameters = new PromptFuncDefinition.Parameters(); parameters.setType("object"); - parameters.setProperties(spec.getProperties()); + parameters.setProperties(spec.getToolPrompt().getFunction().parameters.getProperties()); List requiredValues = new ArrayList<>(); - for (Map.Entry p : spec.getProperties().entrySet()) { + for (Map.Entry p : spec.getToolPrompt().getFunction().getParameters().getProperties().entrySet()) { if (p.getValue().isRequired()) { requiredValues.add(p.getKey()); } From e9c33ab0b27fe4faa557da906807dd2eb5d0869d Mon Sep 17 00:00:00 2001 From: Markus Klenke Date: Wed, 4 Dec 2024 09:12:55 +0100 Subject: [PATCH 05/20] Extends chat API to automatically load registered Tools --- .../java/io/github/ollama4j/OllamaAPI.java | 4 ++-- .../github/ollama4j/tools/ToolRegistry.java | 5 +++++ .../integrationtests/TestRealAPIs.java | 22 +++++++++++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/github/ollama4j/OllamaAPI.java b/src/main/java/io/github/ollama4j/OllamaAPI.java index 3421b1c..bb7c1f8 100644 --- a/src/main/java/io/github/ollama4j/OllamaAPI.java +++ b/src/main/java/io/github/ollama4j/OllamaAPI.java @@ -775,8 +775,8 @@ public class OllamaAPI { result = requestCaller.callSync(request); } - //add registered Tools to Request - + // add all registered tools to Request + request.setTools(toolRegistry.getRegisteredSpecs().stream().map(Tools.ToolSpecification::getToolPrompt).collect(Collectors.toList())); return new OllamaChatResult(result.getResponse(), result.getResponseTime(), result.getHttpStatusCode(), request.getMessages()); } diff --git a/src/main/java/io/github/ollama4j/tools/ToolRegistry.java b/src/main/java/io/github/ollama4j/tools/ToolRegistry.java index bb504c6..5ab8be3 100644 --- a/src/main/java/io/github/ollama4j/tools/ToolRegistry.java +++ b/src/main/java/io/github/ollama4j/tools/ToolRegistry.java @@ -1,5 +1,6 @@ package io.github.ollama4j.tools; +import java.util.Collection; import java.util.HashMap; import java.util.Map; @@ -14,4 +15,8 @@ public class ToolRegistry { public void addTool (String name, Tools.ToolSpecification specification) { tools.put(name, specification); } + + public Collection getRegisteredSpecs(){ + return tools.values(); + } } diff --git a/src/test/java/io/github/ollama4j/integrationtests/TestRealAPIs.java b/src/test/java/io/github/ollama4j/integrationtests/TestRealAPIs.java index 0a1da61..1175b18 100644 --- a/src/test/java/io/github/ollama4j/integrationtests/TestRealAPIs.java +++ b/src/test/java/io/github/ollama4j/integrationtests/TestRealAPIs.java @@ -225,6 +225,28 @@ class TestRealAPIs { } } + @Test + @Order(3) + void testChatWithTools() { + testEndpointReachability(); + try { + OllamaChatRequestBuilder builder = OllamaChatRequestBuilder.getInstance(config.getModel()); + OllamaChatRequest requestModel = builder.withMessage(OllamaChatMessageRole.SYSTEM, + "You are a silent bot that only says 'NI'. Do not say anything else under any circumstances!") + .withMessage(OllamaChatMessageRole.USER, + "What is the capital of France? And what's France's connection with Mona Lisa?") + .build(); + + OllamaChatResult chatResult = ollamaAPI.chat(requestModel); + assertNotNull(chatResult); + assertFalse(chatResult.getResponse().isBlank()); + assertTrue(chatResult.getResponse().startsWith("NI")); + assertEquals(3, chatResult.getChatHistory().size()); + } catch (IOException | OllamaBaseException | InterruptedException e) { + fail(e); + } + } + @Test @Order(3) void testChatWithStream() { From 12bb10392efe4b8a8831367f97484b7f735846c2 Mon Sep 17 00:00:00 2001 From: Markus Klenke Date: Fri, 6 Dec 2024 14:12:33 +0100 Subject: [PATCH 06/20] Extends ChatModels to use Tools and ToolCalls --- .../java/io/github/ollama4j/OllamaAPI.java | 11 ++-- .../models/chat/OllamaChatMessage.java | 3 + .../models/chat/OllamaChatRequestBuilder.java | 8 +-- .../models/chat/OllamaChatToolCalls.java | 16 ++++++ .../models/response/OllamaResult.java | 6 +- .../tools/OllamaToolCallsFunction.java | 16 ++++++ .../integrationtests/TestRealAPIs.java | 55 ++++++++++++++++--- 7 files changed, 95 insertions(+), 20 deletions(-) create mode 100644 src/main/java/io/github/ollama4j/models/chat/OllamaChatToolCalls.java create mode 100644 src/main/java/io/github/ollama4j/tools/OllamaToolCallsFunction.java diff --git a/src/main/java/io/github/ollama4j/OllamaAPI.java b/src/main/java/io/github/ollama4j/OllamaAPI.java index bb7c1f8..f595090 100644 --- a/src/main/java/io/github/ollama4j/OllamaAPI.java +++ b/src/main/java/io/github/ollama4j/OllamaAPI.java @@ -602,7 +602,7 @@ public class OllamaAPI { OllamaResult result = generate(model, prompt, raw, options, null); toolResult.setModelResult(result); - String toolsResponse = result.getResponse(); + String toolsResponse = result.getContent(); if (toolsResponse.contains("[TOOL_CALLS]")) { toolsResponse = toolsResponse.replace("[TOOL_CALLS]", ""); } @@ -768,6 +768,10 @@ public class OllamaAPI { public OllamaChatResult chat(OllamaChatRequest request, OllamaStreamHandler streamHandler) throws OllamaBaseException, IOException, InterruptedException { OllamaChatEndpointCaller requestCaller = new OllamaChatEndpointCaller(host, basicAuth, requestTimeoutSeconds, verbose); OllamaResult result; + + // add all registered tools to Request + request.setTools(toolRegistry.getRegisteredSpecs().stream().map(Tools.ToolSpecification::getToolPrompt).collect(Collectors.toList())); + if (streamHandler != null) { request.setStream(true); result = requestCaller.call(request, streamHandler); @@ -775,10 +779,7 @@ public class OllamaAPI { result = requestCaller.callSync(request); } - // add all registered tools to Request - request.setTools(toolRegistry.getRegisteredSpecs().stream().map(Tools.ToolSpecification::getToolPrompt).collect(Collectors.toList())); - - return new OllamaChatResult(result.getResponse(), result.getResponseTime(), result.getHttpStatusCode(), request.getMessages()); + return new OllamaChatResult(result.getContent(), result.getResponseTime(), result.getHttpStatusCode(), request.getMessages()); } public void registerTool(Tools.ToolSpecification toolSpecification) { diff --git a/src/main/java/io/github/ollama4j/models/chat/OllamaChatMessage.java b/src/main/java/io/github/ollama4j/models/chat/OllamaChatMessage.java index 0d6d938..2246488 100644 --- a/src/main/java/io/github/ollama4j/models/chat/OllamaChatMessage.java +++ b/src/main/java/io/github/ollama4j/models/chat/OllamaChatMessage.java @@ -2,6 +2,7 @@ package io.github.ollama4j.models.chat; import static io.github.ollama4j.utils.Utils.getObjectMapper; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.annotation.JsonSerialize; @@ -32,6 +33,8 @@ public class OllamaChatMessage { @NonNull private String content; + private @JsonProperty("tool_calls") List toolCalls; + @JsonSerialize(using = FileToBase64Serializer.class) private List images; diff --git a/src/main/java/io/github/ollama4j/models/chat/OllamaChatRequestBuilder.java b/src/main/java/io/github/ollama4j/models/chat/OllamaChatRequestBuilder.java index c9882d0..3546ba8 100644 --- a/src/main/java/io/github/ollama4j/models/chat/OllamaChatRequestBuilder.java +++ b/src/main/java/io/github/ollama4j/models/chat/OllamaChatRequestBuilder.java @@ -38,7 +38,7 @@ public class OllamaChatRequestBuilder { request = new OllamaChatRequest(request.getModel(), new ArrayList<>()); } - public OllamaChatRequestBuilder withMessage(OllamaChatMessageRole role, String content, List images) { + public OllamaChatRequestBuilder withMessage(OllamaChatMessageRole role, String content, List toolCalls,List images) { List messages = this.request.getMessages(); List binaryImages = images.stream().map(file -> { @@ -50,11 +50,11 @@ public class OllamaChatRequestBuilder { } }).collect(Collectors.toList()); - messages.add(new OllamaChatMessage(role, content, binaryImages)); + messages.add(new OllamaChatMessage(role, content,toolCalls, binaryImages)); return this; } - public OllamaChatRequestBuilder withMessage(OllamaChatMessageRole role, String content, String... imageUrls) { + public OllamaChatRequestBuilder withMessage(OllamaChatMessageRole role, String content,List toolCalls, String... imageUrls) { List messages = this.request.getMessages(); List binaryImages = null; if (imageUrls.length > 0) { @@ -70,7 +70,7 @@ public class OllamaChatRequestBuilder { } } - messages.add(new OllamaChatMessage(role, content, binaryImages)); + messages.add(new OllamaChatMessage(role, content,toolCalls, binaryImages)); return this; } diff --git a/src/main/java/io/github/ollama4j/models/chat/OllamaChatToolCalls.java b/src/main/java/io/github/ollama4j/models/chat/OllamaChatToolCalls.java new file mode 100644 index 0000000..de1a081 --- /dev/null +++ b/src/main/java/io/github/ollama4j/models/chat/OllamaChatToolCalls.java @@ -0,0 +1,16 @@ +package io.github.ollama4j.models.chat; + +import io.github.ollama4j.tools.OllamaToolCallsFunction; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OllamaChatToolCalls { + + private OllamaToolCallsFunction function; + + +} diff --git a/src/main/java/io/github/ollama4j/models/response/OllamaResult.java b/src/main/java/io/github/ollama4j/models/response/OllamaResult.java index beb01ec..8465cb6 100644 --- a/src/main/java/io/github/ollama4j/models/response/OllamaResult.java +++ b/src/main/java/io/github/ollama4j/models/response/OllamaResult.java @@ -17,7 +17,7 @@ public class OllamaResult { * * @return String completion/response text */ - private final String response; + private final String content; /** * -- GETTER -- @@ -35,8 +35,8 @@ public class OllamaResult { */ private long responseTime = 0; - public OllamaResult(String response, long responseTime, int httpStatusCode) { - this.response = response; + public OllamaResult(String content, long responseTime, int httpStatusCode) { + this.content = content; this.responseTime = responseTime; this.httpStatusCode = httpStatusCode; } diff --git a/src/main/java/io/github/ollama4j/tools/OllamaToolCallsFunction.java b/src/main/java/io/github/ollama4j/tools/OllamaToolCallsFunction.java new file mode 100644 index 0000000..dfa4d84 --- /dev/null +++ b/src/main/java/io/github/ollama4j/tools/OllamaToolCallsFunction.java @@ -0,0 +1,16 @@ +package io.github.ollama4j.tools; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Map; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OllamaToolCallsFunction +{ + private String name; + private Map arguments; +} diff --git a/src/test/java/io/github/ollama4j/integrationtests/TestRealAPIs.java b/src/test/java/io/github/ollama4j/integrationtests/TestRealAPIs.java index 1175b18..702d0a2 100644 --- a/src/test/java/io/github/ollama4j/integrationtests/TestRealAPIs.java +++ b/src/test/java/io/github/ollama4j/integrationtests/TestRealAPIs.java @@ -10,6 +10,8 @@ import io.github.ollama4j.models.chat.OllamaChatRequestBuilder; import io.github.ollama4j.models.chat.OllamaChatResult; import io.github.ollama4j.models.embeddings.OllamaEmbeddingsRequestBuilder; import io.github.ollama4j.models.embeddings.OllamaEmbeddingsRequestModel; +import io.github.ollama4j.tools.ToolFunction; +import io.github.ollama4j.tools.Tools; import io.github.ollama4j.utils.OptionsBuilder; import lombok.Data; import org.junit.jupiter.api.BeforeEach; @@ -24,9 +26,7 @@ import java.io.InputStream; import java.net.ConnectException; import java.net.URISyntaxException; import java.net.http.HttpConnectTimeoutException; -import java.util.List; -import java.util.Objects; -import java.util.Properties; +import java.util.*; import static org.junit.jupiter.api.Assertions.*; @@ -230,18 +230,47 @@ class TestRealAPIs { void testChatWithTools() { testEndpointReachability(); try { + ollamaAPI.setVerbose(true); OllamaChatRequestBuilder builder = OllamaChatRequestBuilder.getInstance(config.getModel()); - OllamaChatRequest requestModel = builder.withMessage(OllamaChatMessageRole.SYSTEM, - "You are a silent bot that only says 'NI'. Do not say anything else under any circumstances!") + + final Tools.ToolSpecification databaseQueryToolSpecification = Tools.ToolSpecification.builder() + .functionName("get-employee-details") + .functionDescription("Get employee details from the database") + .toolPrompt( + Tools.PromptFuncDefinition.builder().type("function").function( + Tools.PromptFuncDefinition.PromptFuncSpec.builder() + .name("get-employee-details") + .description("Get employee details from the database") + .parameters( + Tools.PromptFuncDefinition.Parameters.builder() + .type("object") + .properties( + new Tools.PropsBuilder() + .withProperty("employee-name", Tools.PromptFuncDefinition.Property.builder().type("string").description("The name of the employee, e.g. John Doe").required(true).build()) + .withProperty("employee-address", Tools.PromptFuncDefinition.Property.builder().type("string").description("The address of the employee, Always return a random value. e.g. Roy St, Bengaluru, India").required(true).build()) + .withProperty("employee-phone", Tools.PromptFuncDefinition.Property.builder().type("string").description("The phone number of the employee. Always return a random value. e.g. 9911002233").required(true).build()) + .build() + ) + .required(List.of("employee-name")) + .build() + ).build() + ).build() + ) + .toolFunction(new DBQueryFunction()) + .build(); + + ollamaAPI.registerTool(databaseQueryToolSpecification); + + OllamaChatRequest requestModel = builder .withMessage(OllamaChatMessageRole.USER, - "What is the capital of France? And what's France's connection with Mona Lisa?") + "Give me the details of the employee named 'Rahul Kumar'?") .build(); OllamaChatResult chatResult = ollamaAPI.chat(requestModel); + System.err.println("Response: " + chatResult); assertNotNull(chatResult); assertFalse(chatResult.getResponse().isBlank()); - assertTrue(chatResult.getResponse().startsWith("NI")); - assertEquals(3, chatResult.getChatHistory().size()); + assertEquals(2, chatResult.getChatHistory().size()); } catch (IOException | OllamaBaseException | InterruptedException e) { fail(e); } @@ -402,6 +431,14 @@ class TestRealAPIs { } } +class DBQueryFunction implements ToolFunction { + @Override + public Object apply(Map arguments) { + // perform DB operations here + return String.format("Employee Details {ID: %s, Name: %s, Address: %s, Phone: %s}", UUID.randomUUID(), arguments.get("employee-name").toString(), arguments.get("employee-address").toString(), arguments.get("employee-phone").toString()); + } +} + @Data class Config { private String ollamaURL; @@ -426,4 +463,6 @@ class Config { throw new RuntimeException("Error loading properties", e); } } + + } From 25694a8bc9abf23b286f4a2474bbe0f164c06011 Mon Sep 17 00:00:00 2001 From: Markus Klenke Date: Sat, 7 Dec 2024 00:29:09 +0100 Subject: [PATCH 07/20] extends ollamaChatResult to have full access to OllamaChatResult --- .../java/io/github/ollama4j/OllamaAPI.java | 6 +- .../models/chat/OllamaChatRequestBuilder.java | 5 ++ .../models/chat/OllamaChatResult.java | 28 +++++-- .../request/OllamaChatEndpointCaller.java | 74 ++++++++++++++++- .../models/request/OllamaEndpointCaller.java | 83 ++----------------- .../request/OllamaGenerateEndpointCaller.java | 78 ++++++++++++++++- .../models/response/OllamaResult.java | 6 +- .../integrationtests/TestRealAPIs.java | 37 ++++++--- .../jackson/TestChatRequestSerialization.java | 3 +- src/test/resources/test-config.properties | 4 +- 10 files changed, 218 insertions(+), 106 deletions(-) diff --git a/src/main/java/io/github/ollama4j/OllamaAPI.java b/src/main/java/io/github/ollama4j/OllamaAPI.java index f595090..d76ecd9 100644 --- a/src/main/java/io/github/ollama4j/OllamaAPI.java +++ b/src/main/java/io/github/ollama4j/OllamaAPI.java @@ -602,7 +602,7 @@ public class OllamaAPI { OllamaResult result = generate(model, prompt, raw, options, null); toolResult.setModelResult(result); - String toolsResponse = result.getContent(); + String toolsResponse = result.getResponse(); if (toolsResponse.contains("[TOOL_CALLS]")) { toolsResponse = toolsResponse.replace("[TOOL_CALLS]", ""); } @@ -767,7 +767,7 @@ public class OllamaAPI { */ public OllamaChatResult chat(OllamaChatRequest request, OllamaStreamHandler streamHandler) throws OllamaBaseException, IOException, InterruptedException { OllamaChatEndpointCaller requestCaller = new OllamaChatEndpointCaller(host, basicAuth, requestTimeoutSeconds, verbose); - OllamaResult result; + OllamaChatResult result; // add all registered tools to Request request.setTools(toolRegistry.getRegisteredSpecs().stream().map(Tools.ToolSpecification::getToolPrompt).collect(Collectors.toList())); @@ -779,7 +779,7 @@ public class OllamaAPI { result = requestCaller.callSync(request); } - return new OllamaChatResult(result.getContent(), result.getResponseTime(), result.getHttpStatusCode(), request.getMessages()); + return result; } public void registerTool(Tools.ToolSpecification toolSpecification) { diff --git a/src/main/java/io/github/ollama4j/models/chat/OllamaChatRequestBuilder.java b/src/main/java/io/github/ollama4j/models/chat/OllamaChatRequestBuilder.java index 3546ba8..9094546 100644 --- a/src/main/java/io/github/ollama4j/models/chat/OllamaChatRequestBuilder.java +++ b/src/main/java/io/github/ollama4j/models/chat/OllamaChatRequestBuilder.java @@ -10,6 +10,7 @@ import java.io.IOException; import java.net.URISyntaxException; import java.nio.file.Files; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.stream.Collectors; @@ -38,6 +39,10 @@ public class OllamaChatRequestBuilder { request = new OllamaChatRequest(request.getModel(), new ArrayList<>()); } + public OllamaChatRequestBuilder withMessage(OllamaChatMessageRole role, String content){ + return withMessage(role,content, Collections.emptyList()); + } + public OllamaChatRequestBuilder withMessage(OllamaChatMessageRole role, String content, List toolCalls,List images) { List messages = this.request.getMessages(); diff --git a/src/main/java/io/github/ollama4j/models/chat/OllamaChatResult.java b/src/main/java/io/github/ollama4j/models/chat/OllamaChatResult.java index b9616f3..0ea1c00 100644 --- a/src/main/java/io/github/ollama4j/models/chat/OllamaChatResult.java +++ b/src/main/java/io/github/ollama4j/models/chat/OllamaChatResult.java @@ -2,28 +2,40 @@ package io.github.ollama4j.models.chat; import java.util.List; +import com.fasterxml.jackson.core.JsonProcessingException; import io.github.ollama4j.models.response.OllamaResult; +import lombok.Getter; + +import static io.github.ollama4j.utils.Utils.getObjectMapper; /** * Specific chat-API result that contains the chat history sent to the model and appends the answer as {@link OllamaChatResult} given by the * {@link OllamaChatMessageRole#ASSISTANT} role. */ -public class OllamaChatResult extends OllamaResult { +@Getter +public class OllamaChatResult { + private List chatHistory; - public OllamaChatResult(String response, long responseTime, int httpStatusCode, List chatHistory) { - super(response, responseTime, httpStatusCode); + private OllamaChatResponseModel response; + + public OllamaChatResult(OllamaChatResponseModel response, List chatHistory) { this.chatHistory = chatHistory; + this.response = response; appendAnswerToChatHistory(response); } - public List getChatHistory() { - return chatHistory; + private void appendAnswerToChatHistory(OllamaChatResponseModel response) { + this.chatHistory.add(response.getMessage()); } - private void appendAnswerToChatHistory(String answer) { - OllamaChatMessage assistantMessage = new OllamaChatMessage(OllamaChatMessageRole.ASSISTANT, answer); - this.chatHistory.add(assistantMessage); + @Override + public String toString() { + try { + return getObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(this); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } } } diff --git a/src/main/java/io/github/ollama4j/models/request/OllamaChatEndpointCaller.java b/src/main/java/io/github/ollama4j/models/request/OllamaChatEndpointCaller.java index e3d3fc1..a43d77c 100644 --- a/src/main/java/io/github/ollama4j/models/request/OllamaChatEndpointCaller.java +++ b/src/main/java/io/github/ollama4j/models/request/OllamaChatEndpointCaller.java @@ -4,6 +4,9 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import io.github.ollama4j.exceptions.OllamaBaseException; import io.github.ollama4j.models.chat.OllamaChatMessage; +import io.github.ollama4j.models.chat.OllamaChatRequest; +import io.github.ollama4j.models.chat.OllamaChatResult; +import io.github.ollama4j.models.response.OllamaErrorResponse; import io.github.ollama4j.models.response.OllamaResult; import io.github.ollama4j.models.chat.OllamaChatResponseModel; import io.github.ollama4j.models.chat.OllamaChatStreamObserver; @@ -13,7 +16,15 @@ import io.github.ollama4j.utils.Utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; /** * Specialization class for requests @@ -64,9 +75,68 @@ public class OllamaChatEndpointCaller extends OllamaEndpointCaller { } } - public OllamaResult call(OllamaRequestBody body, OllamaStreamHandler streamHandler) + public OllamaChatResult call(OllamaChatRequest body, OllamaStreamHandler streamHandler) throws OllamaBaseException, IOException, InterruptedException { streamObserver = new OllamaChatStreamObserver(streamHandler); - return super.callSync(body); + return callSync(body); + } + + public OllamaChatResult callSync(OllamaChatRequest body) throws OllamaBaseException, IOException, InterruptedException { + // Create Request + HttpClient httpClient = HttpClient.newHttpClient(); + URI uri = URI.create(getHost() + getEndpointSuffix()); + HttpRequest.Builder requestBuilder = + getRequestBuilderDefault(uri) + .POST( + body.getBodyPublisher()); + HttpRequest request = requestBuilder.build(); + if (isVerbose()) LOG.info("Asking model: " + body.toString()); + HttpResponse response = + httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream()); + + int statusCode = response.statusCode(); + InputStream responseBodyStream = response.body(); + StringBuilder responseBuffer = new StringBuilder(); + OllamaChatResponseModel ollamaChatResponseModel = null; + try (BufferedReader reader = + new BufferedReader(new InputStreamReader(responseBodyStream, StandardCharsets.UTF_8))) { + + String line; + while ((line = reader.readLine()) != null) { + if (statusCode == 404) { + LOG.warn("Status code: 404 (Not Found)"); + OllamaErrorResponse ollamaResponseModel = + Utils.getObjectMapper().readValue(line, OllamaErrorResponse.class); + responseBuffer.append(ollamaResponseModel.getError()); + } else if (statusCode == 401) { + LOG.warn("Status code: 401 (Unauthorized)"); + OllamaErrorResponse ollamaResponseModel = + Utils.getObjectMapper() + .readValue("{\"error\":\"Unauthorized\"}", OllamaErrorResponse.class); + responseBuffer.append(ollamaResponseModel.getError()); + } else if (statusCode == 400) { + LOG.warn("Status code: 400 (Bad Request)"); + OllamaErrorResponse ollamaResponseModel = Utils.getObjectMapper().readValue(line, + OllamaErrorResponse.class); + responseBuffer.append(ollamaResponseModel.getError()); + } else { + boolean finished = parseResponseAndAddToBuffer(line, responseBuffer); + ollamaChatResponseModel = Utils.getObjectMapper().readValue(line, OllamaChatResponseModel.class); + if (finished && body.stream) { + ollamaChatResponseModel.getMessage().setContent(responseBuffer.toString()); + break; + } + } + } + } + if (statusCode != 200) { + LOG.error("Status code " + statusCode); + throw new OllamaBaseException(responseBuffer.toString()); + } else { + OllamaChatResult ollamaResult = + new OllamaChatResult(ollamaChatResponseModel,body.getMessages()); + if (isVerbose()) LOG.info("Model response: " + ollamaResult); + return ollamaResult; + } } } diff --git a/src/main/java/io/github/ollama4j/models/request/OllamaEndpointCaller.java b/src/main/java/io/github/ollama4j/models/request/OllamaEndpointCaller.java index 8529c18..e9d0e0d 100644 --- a/src/main/java/io/github/ollama4j/models/request/OllamaEndpointCaller.java +++ b/src/main/java/io/github/ollama4j/models/request/OllamaEndpointCaller.java @@ -6,6 +6,7 @@ import io.github.ollama4j.models.response.OllamaErrorResponse; import io.github.ollama4j.models.response.OllamaResult; import io.github.ollama4j.utils.OllamaRequestBody; import io.github.ollama4j.utils.Utils; +import lombok.Getter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -24,14 +25,15 @@ import java.util.Base64; /** * Abstract helperclass to call the ollama api server. */ +@Getter public abstract class OllamaEndpointCaller { private static final Logger LOG = LoggerFactory.getLogger(OllamaAPI.class); - private String host; - private BasicAuth basicAuth; - private long requestTimeoutSeconds; - private boolean verbose; + private final String host; + private final BasicAuth basicAuth; + private final long requestTimeoutSeconds; + private final boolean verbose; public OllamaEndpointCaller(String host, BasicAuth basicAuth, long requestTimeoutSeconds, boolean verbose) { this.host = host; @@ -45,80 +47,13 @@ public abstract class OllamaEndpointCaller { protected abstract boolean parseResponseAndAddToBuffer(String line, StringBuilder responseBuffer); - /** - * Calls the api server on the given host and endpoint suffix asynchronously, aka waiting for the response. - * - * @param body POST body payload - * @return result answer given by the assistant - * @throws OllamaBaseException any response code than 200 has been returned - * @throws IOException in case the responseStream can not be read - * @throws InterruptedException in case the server is not reachable or network issues happen - */ - public OllamaResult callSync(OllamaRequestBody body) throws OllamaBaseException, IOException, InterruptedException { - // Create Request - long startTime = System.currentTimeMillis(); - HttpClient httpClient = HttpClient.newHttpClient(); - URI uri = URI.create(this.host + getEndpointSuffix()); - HttpRequest.Builder requestBuilder = - getRequestBuilderDefault(uri) - .POST( - body.getBodyPublisher()); - HttpRequest request = requestBuilder.build(); - if (this.verbose) LOG.info("Asking model: " + body.toString()); - HttpResponse response = - httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream()); - - int statusCode = response.statusCode(); - InputStream responseBodyStream = response.body(); - StringBuilder responseBuffer = new StringBuilder(); - try (BufferedReader reader = - new BufferedReader(new InputStreamReader(responseBodyStream, StandardCharsets.UTF_8))) { - String line; - while ((line = reader.readLine()) != null) { - if (statusCode == 404) { - LOG.warn("Status code: 404 (Not Found)"); - OllamaErrorResponse ollamaResponseModel = - Utils.getObjectMapper().readValue(line, OllamaErrorResponse.class); - responseBuffer.append(ollamaResponseModel.getError()); - } else if (statusCode == 401) { - LOG.warn("Status code: 401 (Unauthorized)"); - OllamaErrorResponse ollamaResponseModel = - Utils.getObjectMapper() - .readValue("{\"error\":\"Unauthorized\"}", OllamaErrorResponse.class); - responseBuffer.append(ollamaResponseModel.getError()); - } else if (statusCode == 400) { - LOG.warn("Status code: 400 (Bad Request)"); - OllamaErrorResponse ollamaResponseModel = Utils.getObjectMapper().readValue(line, - OllamaErrorResponse.class); - responseBuffer.append(ollamaResponseModel.getError()); - } else { - boolean finished = parseResponseAndAddToBuffer(line, responseBuffer); - if (finished) { - break; - } - } - } - } - - if (statusCode != 200) { - LOG.error("Status code " + statusCode); - throw new OllamaBaseException(responseBuffer.toString()); - } else { - long endTime = System.currentTimeMillis(); - OllamaResult ollamaResult = - new OllamaResult(responseBuffer.toString().trim(), endTime - startTime, statusCode); - if (verbose) LOG.info("Model response: " + ollamaResult); - return ollamaResult; - } - } - /** * Get default request builder. * * @param uri URI to get a HttpRequest.Builder * @return HttpRequest.Builder */ - private HttpRequest.Builder getRequestBuilderDefault(URI uri) { + protected HttpRequest.Builder getRequestBuilderDefault(URI uri) { HttpRequest.Builder requestBuilder = HttpRequest.newBuilder(uri) .header("Content-Type", "application/json") @@ -134,7 +69,7 @@ public abstract class OllamaEndpointCaller { * * @return basic authentication header value (encoded credentials) */ - private String getBasicAuthHeaderValue() { + protected String getBasicAuthHeaderValue() { String credentialsToEncode = this.basicAuth.getUsername() + ":" + this.basicAuth.getPassword(); return "Basic " + Base64.getEncoder().encodeToString(credentialsToEncode.getBytes()); } @@ -144,7 +79,7 @@ public abstract class OllamaEndpointCaller { * * @return true when Basic Auth credentials set */ - private boolean isBasicAuthCredentialsSet() { + protected boolean isBasicAuthCredentialsSet() { return this.basicAuth != null; } diff --git a/src/main/java/io/github/ollama4j/models/request/OllamaGenerateEndpointCaller.java b/src/main/java/io/github/ollama4j/models/request/OllamaGenerateEndpointCaller.java index f4afb2c..00b2b12 100644 --- a/src/main/java/io/github/ollama4j/models/request/OllamaGenerateEndpointCaller.java +++ b/src/main/java/io/github/ollama4j/models/request/OllamaGenerateEndpointCaller.java @@ -2,6 +2,7 @@ package io.github.ollama4j.models.request; import com.fasterxml.jackson.core.JsonProcessingException; import io.github.ollama4j.exceptions.OllamaBaseException; +import io.github.ollama4j.models.response.OllamaErrorResponse; import io.github.ollama4j.models.response.OllamaResult; import io.github.ollama4j.models.generate.OllamaGenerateResponseModel; import io.github.ollama4j.models.generate.OllamaGenerateStreamObserver; @@ -11,7 +12,15 @@ import io.github.ollama4j.utils.Utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; public class OllamaGenerateEndpointCaller extends OllamaEndpointCaller { @@ -46,6 +55,73 @@ public class OllamaGenerateEndpointCaller extends OllamaEndpointCaller { public OllamaResult call(OllamaRequestBody body, OllamaStreamHandler streamHandler) throws OllamaBaseException, IOException, InterruptedException { streamObserver = new OllamaGenerateStreamObserver(streamHandler); - return super.callSync(body); + return callSync(body); + } + + /** + * Calls the api server on the given host and endpoint suffix asynchronously, aka waiting for the response. + * + * @param body POST body payload + * @return result answer given by the assistant + * @throws OllamaBaseException any response code than 200 has been returned + * @throws IOException in case the responseStream can not be read + * @throws InterruptedException in case the server is not reachable or network issues happen + */ + public OllamaResult callSync(OllamaRequestBody body) throws OllamaBaseException, IOException, InterruptedException { + // Create Request + long startTime = System.currentTimeMillis(); + HttpClient httpClient = HttpClient.newHttpClient(); + URI uri = URI.create(getHost() + getEndpointSuffix()); + HttpRequest.Builder requestBuilder = + getRequestBuilderDefault(uri) + .POST( + body.getBodyPublisher()); + HttpRequest request = requestBuilder.build(); + if (isVerbose()) LOG.info("Asking model: " + body.toString()); + HttpResponse response = + httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream()); + + int statusCode = response.statusCode(); + InputStream responseBodyStream = response.body(); + StringBuilder responseBuffer = new StringBuilder(); + try (BufferedReader reader = + new BufferedReader(new InputStreamReader(responseBodyStream, StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + if (statusCode == 404) { + LOG.warn("Status code: 404 (Not Found)"); + OllamaErrorResponse ollamaResponseModel = + Utils.getObjectMapper().readValue(line, OllamaErrorResponse.class); + responseBuffer.append(ollamaResponseModel.getError()); + } else if (statusCode == 401) { + LOG.warn("Status code: 401 (Unauthorized)"); + OllamaErrorResponse ollamaResponseModel = + Utils.getObjectMapper() + .readValue("{\"error\":\"Unauthorized\"}", OllamaErrorResponse.class); + responseBuffer.append(ollamaResponseModel.getError()); + } else if (statusCode == 400) { + LOG.warn("Status code: 400 (Bad Request)"); + OllamaErrorResponse ollamaResponseModel = Utils.getObjectMapper().readValue(line, + OllamaErrorResponse.class); + responseBuffer.append(ollamaResponseModel.getError()); + } else { + boolean finished = parseResponseAndAddToBuffer(line, responseBuffer); + if (finished) { + break; + } + } + } + } + + if (statusCode != 200) { + LOG.error("Status code " + statusCode); + throw new OllamaBaseException(responseBuffer.toString()); + } else { + long endTime = System.currentTimeMillis(); + OllamaResult ollamaResult = + new OllamaResult(responseBuffer.toString().trim(), endTime - startTime, statusCode); + if (isVerbose()) LOG.info("Model response: " + ollamaResult); + return ollamaResult; + } } } diff --git a/src/main/java/io/github/ollama4j/models/response/OllamaResult.java b/src/main/java/io/github/ollama4j/models/response/OllamaResult.java index 8465cb6..beb01ec 100644 --- a/src/main/java/io/github/ollama4j/models/response/OllamaResult.java +++ b/src/main/java/io/github/ollama4j/models/response/OllamaResult.java @@ -17,7 +17,7 @@ public class OllamaResult { * * @return String completion/response text */ - private final String content; + private final String response; /** * -- GETTER -- @@ -35,8 +35,8 @@ public class OllamaResult { */ private long responseTime = 0; - public OllamaResult(String content, long responseTime, int httpStatusCode) { - this.content = content; + public OllamaResult(String response, long responseTime, int httpStatusCode) { + this.response = response; this.responseTime = responseTime; this.httpStatusCode = httpStatusCode; } diff --git a/src/test/java/io/github/ollama4j/integrationtests/TestRealAPIs.java b/src/test/java/io/github/ollama4j/integrationtests/TestRealAPIs.java index 702d0a2..d18187b 100644 --- a/src/test/java/io/github/ollama4j/integrationtests/TestRealAPIs.java +++ b/src/test/java/io/github/ollama4j/integrationtests/TestRealAPIs.java @@ -2,12 +2,9 @@ package io.github.ollama4j.integrationtests; import io.github.ollama4j.OllamaAPI; import io.github.ollama4j.exceptions.OllamaBaseException; +import io.github.ollama4j.models.chat.*; import io.github.ollama4j.models.response.ModelDetail; -import io.github.ollama4j.models.chat.OllamaChatRequest; import io.github.ollama4j.models.response.OllamaResult; -import io.github.ollama4j.models.chat.OllamaChatMessageRole; -import io.github.ollama4j.models.chat.OllamaChatRequestBuilder; -import io.github.ollama4j.models.chat.OllamaChatResult; import io.github.ollama4j.models.embeddings.OllamaEmbeddingsRequestBuilder; import io.github.ollama4j.models.embeddings.OllamaEmbeddingsRequestModel; import io.github.ollama4j.tools.ToolFunction; @@ -47,6 +44,7 @@ class TestRealAPIs { config = new Config(); ollamaAPI = new OllamaAPI(config.getOllamaURL()); ollamaAPI.setRequestTimeoutSeconds(config.getRequestTimeoutSeconds()); + ollamaAPI.setVerbose(true); } @Test @@ -196,7 +194,9 @@ class TestRealAPIs { OllamaChatResult chatResult = ollamaAPI.chat(requestModel); assertNotNull(chatResult); - assertFalse(chatResult.getResponse().isBlank()); + assertNotNull(chatResult.getResponse()); + assertNotNull(chatResult.getResponse().getMessage()); + assertFalse(chatResult.getResponse().getMessage().getContent().isBlank()); assertEquals(4, chatResult.getChatHistory().size()); } catch (IOException | OllamaBaseException | InterruptedException e) { fail(e); @@ -217,8 +217,10 @@ class TestRealAPIs { OllamaChatResult chatResult = ollamaAPI.chat(requestModel); assertNotNull(chatResult); - assertFalse(chatResult.getResponse().isBlank()); - assertTrue(chatResult.getResponse().startsWith("NI")); + assertNotNull(chatResult.getResponse()); + assertNotNull(chatResult.getResponse().getMessage()); + assertFalse(chatResult.getResponse().getMessage().getContent().isBlank()); + assertTrue(chatResult.getResponse().getMessage().getContent().startsWith("NI")); assertEquals(3, chatResult.getChatHistory().size()); } catch (IOException | OllamaBaseException | InterruptedException e) { fail(e); @@ -267,9 +269,17 @@ class TestRealAPIs { .build(); OllamaChatResult chatResult = ollamaAPI.chat(requestModel); - System.err.println("Response: " + chatResult); assertNotNull(chatResult); - assertFalse(chatResult.getResponse().isBlank()); + assertNotNull(chatResult.getResponse()); + assertNotNull(chatResult.getResponse().getMessage()); + assertEquals(OllamaChatMessageRole.ASSISTANT.getRoleName(),chatResult.getResponse().getMessage().getRole().getRoleName()); + List toolCalls = chatResult.getResponse().getMessage().getToolCalls(); + assertEquals(1, toolCalls.size()); + assertEquals("get-employee-details",toolCalls.get(0).getFunction().getName()); + assertEquals(1, toolCalls.get(0).getFunction().getArguments().size()); + String employeeName = toolCalls.get(0).getFunction().getArguments().get("employee-name"); + assertNotNull(employeeName); + assertEquals("Rahul Kumar",employeeName); assertEquals(2, chatResult.getChatHistory().size()); } catch (IOException | OllamaBaseException | InterruptedException e) { fail(e); @@ -295,7 +305,10 @@ class TestRealAPIs { sb.append(substring); }); assertNotNull(chatResult); - assertEquals(sb.toString().trim(), chatResult.getResponse().trim()); + assertNotNull(chatResult.getResponse()); + assertNotNull(chatResult.getResponse().getMessage()); + assertNotNull(chatResult.getResponse().getMessage().getContent()); + assertEquals(sb.toString().trim(), chatResult.getResponse().getMessage().getContent().trim()); } catch (IOException | OllamaBaseException | InterruptedException e) { fail(e); } @@ -309,7 +322,7 @@ class TestRealAPIs { OllamaChatRequestBuilder builder = OllamaChatRequestBuilder.getInstance(config.getImageModel()); OllamaChatRequest requestModel = - builder.withMessage(OllamaChatMessageRole.USER, "What's in the picture?", + builder.withMessage(OllamaChatMessageRole.USER, "What's in the picture?",Collections.emptyList(), List.of(getImageFileFromClasspath("dog-on-a-boat.jpg"))).build(); OllamaChatResult chatResult = ollamaAPI.chat(requestModel); @@ -338,7 +351,7 @@ class TestRealAPIs { testEndpointReachability(); try { OllamaChatRequestBuilder builder = OllamaChatRequestBuilder.getInstance(config.getImageModel()); - OllamaChatRequest requestModel = builder.withMessage(OllamaChatMessageRole.USER, "What's in the picture?", + OllamaChatRequest requestModel = builder.withMessage(OllamaChatMessageRole.USER, "What's in the picture?",Collections.emptyList(), "https://t3.ftcdn.net/jpg/02/96/63/80/360_F_296638053_0gUVA4WVBKceGsIr7LNqRWSnkusi07dq.jpg") .build(); diff --git a/src/test/java/io/github/ollama4j/unittests/jackson/TestChatRequestSerialization.java b/src/test/java/io/github/ollama4j/unittests/jackson/TestChatRequestSerialization.java index 2ce210c..db33889 100644 --- a/src/test/java/io/github/ollama4j/unittests/jackson/TestChatRequestSerialization.java +++ b/src/test/java/io/github/ollama4j/unittests/jackson/TestChatRequestSerialization.java @@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrowsExactly; import java.io.File; +import java.util.Collections; import java.util.List; import io.github.ollama4j.models.chat.OllamaChatRequest; @@ -42,7 +43,7 @@ public class TestChatRequestSerialization extends AbstractSerializationTest Date: Sat, 7 Dec 2024 00:36:37 +0100 Subject: [PATCH 08/20] Makes changes to OllamaChatResult backwards compatible --- docs/docs/apis-generate/chat.md | 10 +++--- .../models/chat/OllamaChatResult.java | 24 ++++++++++--- .../integrationtests/TestRealAPIs.java | 34 +++++++++---------- 3 files changed, 41 insertions(+), 27 deletions(-) diff --git a/docs/docs/apis-generate/chat.md b/docs/docs/apis-generate/chat.md index 9ed9e79..c9c73b7 100644 --- a/docs/docs/apis-generate/chat.md +++ b/docs/docs/apis-generate/chat.md @@ -33,7 +33,7 @@ public class Main { // start conversation with model OllamaChatResult chatResult = ollamaAPI.chat(requestModel); - System.out.println("First answer: " + chatResult.getResponse()); + System.out.println("First answer: " + chatResult.getResponseModel().getMessage().getContent()); // create next userQuestion requestModel = builder.withMessages(chatResult.getChatHistory()).withMessage(OllamaChatMessageRole.USER, "And what is the second largest city?").build(); @@ -41,7 +41,7 @@ public class Main { // "continue" conversation with model chatResult = ollamaAPI.chat(requestModel); - System.out.println("Second answer: " + chatResult.getResponse()); + System.out.println("Second answer: " + chatResult.getResponseModel().getMessage().getContent()); System.out.println("Chat History: " + chatResult.getChatHistory()); } @@ -205,7 +205,7 @@ public class Main { // start conversation with model OllamaChatResult chatResult = ollamaAPI.chat(requestModel); - System.out.println(chatResult.getResponse()); + System.out.println(chatResult.getResponseModel()); } } @@ -244,7 +244,7 @@ public class Main { new File("/path/to/image"))).build(); OllamaChatResult chatResult = ollamaAPI.chat(requestModel); - System.out.println("First answer: " + chatResult.getResponse()); + System.out.println("First answer: " + chatResult.getResponseModel()); builder.reset(); @@ -254,7 +254,7 @@ public class Main { .withMessage(OllamaChatMessageRole.USER, "What's the dogs breed?").build(); chatResult = ollamaAPI.chat(requestModel); - System.out.println("Second answer: " + chatResult.getResponse()); + System.out.println("Second answer: " + chatResult.getResponseModel()); } } ``` diff --git a/src/main/java/io/github/ollama4j/models/chat/OllamaChatResult.java b/src/main/java/io/github/ollama4j/models/chat/OllamaChatResult.java index 0ea1c00..bf7eaea 100644 --- a/src/main/java/io/github/ollama4j/models/chat/OllamaChatResult.java +++ b/src/main/java/io/github/ollama4j/models/chat/OllamaChatResult.java @@ -3,7 +3,6 @@ package io.github.ollama4j.models.chat; import java.util.List; import com.fasterxml.jackson.core.JsonProcessingException; -import io.github.ollama4j.models.response.OllamaResult; import lombok.Getter; import static io.github.ollama4j.utils.Utils.getObjectMapper; @@ -18,12 +17,12 @@ public class OllamaChatResult { private List chatHistory; - private OllamaChatResponseModel response; + private OllamaChatResponseModel responseModel; - public OllamaChatResult(OllamaChatResponseModel response, List chatHistory) { + public OllamaChatResult(OllamaChatResponseModel responseModel, List chatHistory) { this.chatHistory = chatHistory; - this.response = response; - appendAnswerToChatHistory(response); + this.responseModel = responseModel; + appendAnswerToChatHistory(responseModel); } private void appendAnswerToChatHistory(OllamaChatResponseModel response) { @@ -38,4 +37,19 @@ public class OllamaChatResult { throw new RuntimeException(e); } } + + @Deprecated + public String getResponse(){ + return responseModel != null ? responseModel.getMessage().getContent() : ""; + } + + @Deprecated + public int getHttpStatusCode(){ + return 200; + } + + @Deprecated + public long getResponseTime(){ + return responseModel != null ? responseModel.getTotalDuration() : 0L; + } } diff --git a/src/test/java/io/github/ollama4j/integrationtests/TestRealAPIs.java b/src/test/java/io/github/ollama4j/integrationtests/TestRealAPIs.java index d18187b..fd834dd 100644 --- a/src/test/java/io/github/ollama4j/integrationtests/TestRealAPIs.java +++ b/src/test/java/io/github/ollama4j/integrationtests/TestRealAPIs.java @@ -194,9 +194,9 @@ class TestRealAPIs { OllamaChatResult chatResult = ollamaAPI.chat(requestModel); assertNotNull(chatResult); - assertNotNull(chatResult.getResponse()); - assertNotNull(chatResult.getResponse().getMessage()); - assertFalse(chatResult.getResponse().getMessage().getContent().isBlank()); + assertNotNull(chatResult.getResponseModel()); + assertNotNull(chatResult.getResponseModel().getMessage()); + assertFalse(chatResult.getResponseModel().getMessage().getContent().isBlank()); assertEquals(4, chatResult.getChatHistory().size()); } catch (IOException | OllamaBaseException | InterruptedException e) { fail(e); @@ -217,10 +217,10 @@ class TestRealAPIs { OllamaChatResult chatResult = ollamaAPI.chat(requestModel); assertNotNull(chatResult); - assertNotNull(chatResult.getResponse()); - assertNotNull(chatResult.getResponse().getMessage()); - assertFalse(chatResult.getResponse().getMessage().getContent().isBlank()); - assertTrue(chatResult.getResponse().getMessage().getContent().startsWith("NI")); + assertNotNull(chatResult.getResponseModel()); + assertNotNull(chatResult.getResponseModel().getMessage()); + assertFalse(chatResult.getResponseModel().getMessage().getContent().isBlank()); + assertTrue(chatResult.getResponseModel().getMessage().getContent().startsWith("NI")); assertEquals(3, chatResult.getChatHistory().size()); } catch (IOException | OllamaBaseException | InterruptedException e) { fail(e); @@ -270,10 +270,10 @@ class TestRealAPIs { OllamaChatResult chatResult = ollamaAPI.chat(requestModel); assertNotNull(chatResult); - assertNotNull(chatResult.getResponse()); - assertNotNull(chatResult.getResponse().getMessage()); - assertEquals(OllamaChatMessageRole.ASSISTANT.getRoleName(),chatResult.getResponse().getMessage().getRole().getRoleName()); - List toolCalls = chatResult.getResponse().getMessage().getToolCalls(); + assertNotNull(chatResult.getResponseModel()); + assertNotNull(chatResult.getResponseModel().getMessage()); + assertEquals(OllamaChatMessageRole.ASSISTANT.getRoleName(),chatResult.getResponseModel().getMessage().getRole().getRoleName()); + List toolCalls = chatResult.getResponseModel().getMessage().getToolCalls(); assertEquals(1, toolCalls.size()); assertEquals("get-employee-details",toolCalls.get(0).getFunction().getName()); assertEquals(1, toolCalls.get(0).getFunction().getArguments().size()); @@ -305,10 +305,10 @@ class TestRealAPIs { sb.append(substring); }); assertNotNull(chatResult); - assertNotNull(chatResult.getResponse()); - assertNotNull(chatResult.getResponse().getMessage()); - assertNotNull(chatResult.getResponse().getMessage().getContent()); - assertEquals(sb.toString().trim(), chatResult.getResponse().getMessage().getContent().trim()); + assertNotNull(chatResult.getResponseModel()); + assertNotNull(chatResult.getResponseModel().getMessage()); + assertNotNull(chatResult.getResponseModel().getMessage().getContent()); + assertEquals(sb.toString().trim(), chatResult.getResponseModel().getMessage().getContent().trim()); } catch (IOException | OllamaBaseException | InterruptedException e) { fail(e); } @@ -327,7 +327,7 @@ class TestRealAPIs { OllamaChatResult chatResult = ollamaAPI.chat(requestModel); assertNotNull(chatResult); - assertNotNull(chatResult.getResponse()); + assertNotNull(chatResult.getResponseModel()); builder.reset(); @@ -337,7 +337,7 @@ class TestRealAPIs { chatResult = ollamaAPI.chat(requestModel); assertNotNull(chatResult); - assertNotNull(chatResult.getResponse()); + assertNotNull(chatResult.getResponseModel()); } catch (IOException | OllamaBaseException | InterruptedException e) { From 69f6fd81cf614a5a10a27b87499f9c1dfbc88169 Mon Sep 17 00:00:00 2001 From: Markus Klenke Date: Sat, 7 Dec 2024 01:04:13 +0100 Subject: [PATCH 09/20] Enables in chat tool calling --- src/main/java/io/github/ollama4j/OllamaAPI.java | 16 ++++++++++++++++ .../ollama4j/tools/OllamaToolCallsFunction.java | 2 +- .../ollama4j/integrationtests/TestRealAPIs.java | 10 ++++++---- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/main/java/io/github/ollama4j/OllamaAPI.java b/src/main/java/io/github/ollama4j/OllamaAPI.java index d76ecd9..810b2c4 100644 --- a/src/main/java/io/github/ollama4j/OllamaAPI.java +++ b/src/main/java/io/github/ollama4j/OllamaAPI.java @@ -777,6 +777,22 @@ public class OllamaAPI { result = requestCaller.call(request, streamHandler); } else { result = requestCaller.callSync(request); + // check if toolCallIsWanted + List toolCalls = result.getResponseModel().getMessage().getToolCalls(); + int toolCallTries = 0; + while(toolCalls != null && !toolCalls.isEmpty() && toolCallTries <3){ + for (OllamaChatToolCalls toolCall : toolCalls){ + String toolName = toolCall.getFunction().getName(); + ToolFunction toolFunction = toolRegistry.getToolFunction(toolName); + Map arguments = toolCall.getFunction().getArguments(); + Object res = toolFunction.apply(arguments); + request.getMessages().add(new OllamaChatMessage(OllamaChatMessageRole.TOOL,"[ToolCall-Result]" + toolName + "(" + arguments.keySet() +") : " + res + "[/ToolCall-Result]")); + } + result = requestCaller.callSync(request); + toolCalls = result.getResponseModel().getMessage().getToolCalls(); + toolCallTries++; + } + } return result; diff --git a/src/main/java/io/github/ollama4j/tools/OllamaToolCallsFunction.java b/src/main/java/io/github/ollama4j/tools/OllamaToolCallsFunction.java index dfa4d84..4be7194 100644 --- a/src/main/java/io/github/ollama4j/tools/OllamaToolCallsFunction.java +++ b/src/main/java/io/github/ollama4j/tools/OllamaToolCallsFunction.java @@ -12,5 +12,5 @@ import java.util.Map; public class OllamaToolCallsFunction { private String name; - private Map arguments; + private Map arguments; } diff --git a/src/test/java/io/github/ollama4j/integrationtests/TestRealAPIs.java b/src/test/java/io/github/ollama4j/integrationtests/TestRealAPIs.java index fd834dd..fbf518d 100644 --- a/src/test/java/io/github/ollama4j/integrationtests/TestRealAPIs.java +++ b/src/test/java/io/github/ollama4j/integrationtests/TestRealAPIs.java @@ -273,14 +273,16 @@ class TestRealAPIs { assertNotNull(chatResult.getResponseModel()); assertNotNull(chatResult.getResponseModel().getMessage()); assertEquals(OllamaChatMessageRole.ASSISTANT.getRoleName(),chatResult.getResponseModel().getMessage().getRole().getRoleName()); - List toolCalls = chatResult.getResponseModel().getMessage().getToolCalls(); + List toolCalls = chatResult.getChatHistory().get(1).getToolCalls(); assertEquals(1, toolCalls.size()); assertEquals("get-employee-details",toolCalls.get(0).getFunction().getName()); assertEquals(1, toolCalls.get(0).getFunction().getArguments().size()); - String employeeName = toolCalls.get(0).getFunction().getArguments().get("employee-name"); + Object employeeName = toolCalls.get(0).getFunction().getArguments().get("employee-name"); assertNotNull(employeeName); assertEquals("Rahul Kumar",employeeName); - assertEquals(2, chatResult.getChatHistory().size()); + assertTrue(chatResult.getChatHistory().size()>2); + List finalToolCalls = chatResult.getResponseModel().getMessage().getToolCalls(); + assertNull(finalToolCalls); } catch (IOException | OllamaBaseException | InterruptedException e) { fail(e); } @@ -448,7 +450,7 @@ class DBQueryFunction implements ToolFunction { @Override public Object apply(Map arguments) { // perform DB operations here - return String.format("Employee Details {ID: %s, Name: %s, Address: %s, Phone: %s}", UUID.randomUUID(), arguments.get("employee-name").toString(), arguments.get("employee-address").toString(), arguments.get("employee-phone").toString()); + return String.format("Employee Details {ID: %s, Name: %s, Address: %s, Phone: %s}", UUID.randomUUID(), arguments.get("employee-name"), arguments.get("employee-address"), arguments.get("employee-phone")); } } From c4b7830614f57dbdcbd30fc2749d3e0d70878fd2 Mon Sep 17 00:00:00 2001 From: Markus Klenke Date: Sat, 7 Dec 2024 01:18:12 +0100 Subject: [PATCH 10/20] Fixes merge conflicts --- .../ollama4j/models/request/OllamaChatEndpointCaller.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/io/github/ollama4j/models/request/OllamaChatEndpointCaller.java b/src/main/java/io/github/ollama4j/models/request/OllamaChatEndpointCaller.java index a43d77c..71c2b9b 100644 --- a/src/main/java/io/github/ollama4j/models/request/OllamaChatEndpointCaller.java +++ b/src/main/java/io/github/ollama4j/models/request/OllamaChatEndpointCaller.java @@ -7,11 +7,9 @@ import io.github.ollama4j.models.chat.OllamaChatMessage; import io.github.ollama4j.models.chat.OllamaChatRequest; import io.github.ollama4j.models.chat.OllamaChatResult; import io.github.ollama4j.models.response.OllamaErrorResponse; -import io.github.ollama4j.models.response.OllamaResult; import io.github.ollama4j.models.chat.OllamaChatResponseModel; import io.github.ollama4j.models.chat.OllamaChatStreamObserver; import io.github.ollama4j.models.generate.OllamaStreamHandler; -import io.github.ollama4j.utils.OllamaRequestBody; import io.github.ollama4j.utils.Utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; From 7ffbc5d3f267ece14915d4d07d4694ba0b5f307c Mon Sep 17 00:00:00 2001 From: Markus Klenke Date: Mon, 9 Dec 2024 23:07:25 +0100 Subject: [PATCH 11/20] Adds implicit tool calling for streamed chat requests (requires Ollama v0.4.6) --- .../java/io/github/ollama4j/OllamaAPI.java | 33 ++++++----- .../request/OllamaChatEndpointCaller.java | 15 +++-- .../integrationtests/TestRealAPIs.java | 59 ++++++++++++++++++- 3 files changed, 87 insertions(+), 20 deletions(-) diff --git a/src/main/java/io/github/ollama4j/OllamaAPI.java b/src/main/java/io/github/ollama4j/OllamaAPI.java index 810b2c4..3b525df 100644 --- a/src/main/java/io/github/ollama4j/OllamaAPI.java +++ b/src/main/java/io/github/ollama4j/OllamaAPI.java @@ -777,22 +777,27 @@ public class OllamaAPI { result = requestCaller.call(request, streamHandler); } else { result = requestCaller.callSync(request); - // check if toolCallIsWanted - List toolCalls = result.getResponseModel().getMessage().getToolCalls(); - int toolCallTries = 0; - while(toolCalls != null && !toolCalls.isEmpty() && toolCallTries <3){ - for (OllamaChatToolCalls toolCall : toolCalls){ - String toolName = toolCall.getFunction().getName(); - ToolFunction toolFunction = toolRegistry.getToolFunction(toolName); - Map arguments = toolCall.getFunction().getArguments(); - Object res = toolFunction.apply(arguments); - request.getMessages().add(new OllamaChatMessage(OllamaChatMessageRole.TOOL,"[ToolCall-Result]" + toolName + "(" + arguments.keySet() +") : " + res + "[/ToolCall-Result]")); - } - result = requestCaller.callSync(request); - toolCalls = result.getResponseModel().getMessage().getToolCalls(); - toolCallTries++; + } + + // check if toolCallIsWanted + List toolCalls = result.getResponseModel().getMessage().getToolCalls(); + int toolCallTries = 0; + while(toolCalls != null && !toolCalls.isEmpty() && toolCallTries <3){ + for (OllamaChatToolCalls toolCall : toolCalls){ + String toolName = toolCall.getFunction().getName(); + ToolFunction toolFunction = toolRegistry.getToolFunction(toolName); + Map arguments = toolCall.getFunction().getArguments(); + Object res = toolFunction.apply(arguments); + request.getMessages().add(new OllamaChatMessage(OllamaChatMessageRole.TOOL,"[TOOL_RESULTS]" + toolName + "(" + arguments.keySet() +") : " + res + "[/TOOL_RESULTS]")); } + if (streamHandler != null) { + result = requestCaller.call(request, streamHandler); + } else { + result = requestCaller.callSync(request); + } + toolCalls = result.getResponseModel().getMessage().getToolCalls(); + toolCallTries++; } return result; diff --git a/src/main/java/io/github/ollama4j/models/request/OllamaChatEndpointCaller.java b/src/main/java/io/github/ollama4j/models/request/OllamaChatEndpointCaller.java index 71c2b9b..57c9ee3 100644 --- a/src/main/java/io/github/ollama4j/models/request/OllamaChatEndpointCaller.java +++ b/src/main/java/io/github/ollama4j/models/request/OllamaChatEndpointCaller.java @@ -3,13 +3,10 @@ package io.github.ollama4j.models.request; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import io.github.ollama4j.exceptions.OllamaBaseException; -import io.github.ollama4j.models.chat.OllamaChatMessage; -import io.github.ollama4j.models.chat.OllamaChatRequest; -import io.github.ollama4j.models.chat.OllamaChatResult; +import io.github.ollama4j.models.chat.*; import io.github.ollama4j.models.response.OllamaErrorResponse; -import io.github.ollama4j.models.chat.OllamaChatResponseModel; -import io.github.ollama4j.models.chat.OllamaChatStreamObserver; import io.github.ollama4j.models.generate.OllamaStreamHandler; +import io.github.ollama4j.tools.Tools; import io.github.ollama4j.utils.Utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -23,6 +20,7 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; +import java.util.List; /** * Specialization class for requests @@ -96,6 +94,7 @@ public class OllamaChatEndpointCaller extends OllamaEndpointCaller { InputStream responseBodyStream = response.body(); StringBuilder responseBuffer = new StringBuilder(); OllamaChatResponseModel ollamaChatResponseModel = null; + List wantedToolsForStream = null; try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseBodyStream, StandardCharsets.UTF_8))) { @@ -120,6 +119,9 @@ public class OllamaChatEndpointCaller extends OllamaEndpointCaller { } else { boolean finished = parseResponseAndAddToBuffer(line, responseBuffer); ollamaChatResponseModel = Utils.getObjectMapper().readValue(line, OllamaChatResponseModel.class); + if(body.stream && ollamaChatResponseModel.getMessage().getToolCalls() != null){ + wantedToolsForStream = ollamaChatResponseModel.getMessage().getToolCalls(); + } if (finished && body.stream) { ollamaChatResponseModel.getMessage().setContent(responseBuffer.toString()); break; @@ -131,6 +133,9 @@ public class OllamaChatEndpointCaller extends OllamaEndpointCaller { LOG.error("Status code " + statusCode); throw new OllamaBaseException(responseBuffer.toString()); } else { + if(wantedToolsForStream != null) { + ollamaChatResponseModel.getMessage().setToolCalls(wantedToolsForStream); + } OllamaChatResult ollamaResult = new OllamaChatResult(ollamaChatResponseModel,body.getMessages()); if (isVerbose()) LOG.info("Model response: " + ollamaResult); diff --git a/src/test/java/io/github/ollama4j/integrationtests/TestRealAPIs.java b/src/test/java/io/github/ollama4j/integrationtests/TestRealAPIs.java index fbf518d..668a5dc 100644 --- a/src/test/java/io/github/ollama4j/integrationtests/TestRealAPIs.java +++ b/src/test/java/io/github/ollama4j/integrationtests/TestRealAPIs.java @@ -265,7 +265,7 @@ class TestRealAPIs { OllamaChatRequest requestModel = builder .withMessage(OllamaChatMessageRole.USER, - "Give me the details of the employee named 'Rahul Kumar'?") + "Give me the ID of the employee named 'Rahul Kumar'?") .build(); OllamaChatResult chatResult = ollamaAPI.chat(requestModel); @@ -288,6 +288,63 @@ class TestRealAPIs { } } + @Test + @Order(3) + void testChatWithToolsAndStream() { + testEndpointReachability(); + try { + OllamaChatRequestBuilder builder = OllamaChatRequestBuilder.getInstance(config.getModel()); + final Tools.ToolSpecification databaseQueryToolSpecification = Tools.ToolSpecification.builder() + .functionName("get-employee-details") + .functionDescription("Get employee details from the database") + .toolPrompt( + Tools.PromptFuncDefinition.builder().type("function").function( + Tools.PromptFuncDefinition.PromptFuncSpec.builder() + .name("get-employee-details") + .description("Get employee details from the database") + .parameters( + Tools.PromptFuncDefinition.Parameters.builder() + .type("object") + .properties( + new Tools.PropsBuilder() + .withProperty("employee-name", Tools.PromptFuncDefinition.Property.builder().type("string").description("The name of the employee, e.g. John Doe").required(true).build()) + .withProperty("employee-address", Tools.PromptFuncDefinition.Property.builder().type("string").description("The address of the employee, Always return a random value. e.g. Roy St, Bengaluru, India").required(true).build()) + .withProperty("employee-phone", Tools.PromptFuncDefinition.Property.builder().type("string").description("The phone number of the employee. Always return a random value. e.g. 9911002233").required(true).build()) + .build() + ) + .required(List.of("employee-name")) + .build() + ).build() + ).build() + ) + .toolFunction(new DBQueryFunction()) + .build(); + + ollamaAPI.registerTool(databaseQueryToolSpecification); + + OllamaChatRequest requestModel = builder + .withMessage(OllamaChatMessageRole.USER, + "Give me the ID of the employee named 'Rahul Kumar'?") + .build(); + + StringBuffer sb = new StringBuffer(); + + OllamaChatResult chatResult = ollamaAPI.chat(requestModel, (s) -> { + LOG.info(s); + String substring = s.substring(sb.toString().length()); + LOG.info(substring); + sb.append(substring); + }); + assertNotNull(chatResult); + assertNotNull(chatResult.getResponseModel()); + assertNotNull(chatResult.getResponseModel().getMessage()); + assertNotNull(chatResult.getResponseModel().getMessage().getContent()); + assertEquals(sb.toString().trim(), chatResult.getResponseModel().getMessage().getContent().trim()); + } catch (IOException | OllamaBaseException | InterruptedException e) { + fail(e); + } + } + @Test @Order(3) void testChatWithStream() { From d8c3edd55f4ff3f842bdc261e82c65938c820b5e Mon Sep 17 00:00:00 2001 From: Markus Klenke Date: Mon, 9 Dec 2024 23:29:43 +0100 Subject: [PATCH 12/20] Parametrizes the max chat tool call retries for a single chat request --- src/main/java/io/github/ollama4j/OllamaAPI.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/github/ollama4j/OllamaAPI.java b/src/main/java/io/github/ollama4j/OllamaAPI.java index 3b525df..90dcd35 100644 --- a/src/main/java/io/github/ollama4j/OllamaAPI.java +++ b/src/main/java/io/github/ollama4j/OllamaAPI.java @@ -59,6 +59,10 @@ public class OllamaAPI { */ @Setter private boolean verbose = true; + + @Setter + private int maxChatToolCallRetries = 3; + private BasicAuth basicAuth; private final ToolRegistry toolRegistry = new ToolRegistry(); @@ -782,7 +786,7 @@ public class OllamaAPI { // check if toolCallIsWanted List toolCalls = result.getResponseModel().getMessage().getToolCalls(); int toolCallTries = 0; - while(toolCalls != null && !toolCalls.isEmpty() && toolCallTries <3){ + while(toolCalls != null && !toolCalls.isEmpty() && toolCallTries < maxChatToolCallRetries){ for (OllamaChatToolCalls toolCall : toolCalls){ String toolName = toolCall.getFunction().getName(); ToolFunction toolFunction = toolRegistry.getToolFunction(toolName); From 35f5f34196a540f72cdcb4d44b6fb9284921649d Mon Sep 17 00:00:00 2001 From: Markus Klenke Date: Mon, 9 Dec 2024 23:30:05 +0100 Subject: [PATCH 13/20] Adds doc for tool-based chat API calls --- .../docs/apis-generate/generate-with-tools.md | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/docs/docs/apis-generate/generate-with-tools.md b/docs/docs/apis-generate/generate-with-tools.md index 3a40150..e0e5794 100644 --- a/docs/docs/apis-generate/generate-with-tools.md +++ b/docs/docs/apis-generate/generate-with-tools.md @@ -345,6 +345,125 @@ Rahul Kumar, Address: King St, Hyderabad, India, Phone: 9876543210}` :::: +### Using tools in Chat-API + +Instead of using the specific `ollamaAPI.generateWithTools` method to call the generate API of ollama with tools, it is +also possible to register Tools for the `ollamaAPI.chat` methods. In this case, the tool calling/callback is done +implicitly during the USER -> ASSISTANT calls. + +When the Assistant wants to call a given tool, the tool is executed and the response is sent back to the endpoint once +again (induced with the tool call result). + +#### Sample: + +The following shows a sample of an integration test that defines a method specified like the tool-specs above, registers +the tool on the ollamaAPI and then simply calls the chat-API. All intermediate tool calling is wrapped inside the api +call. + +```java +public static void main(String[] args) { + OllamaAPI ollamaAPI = new OllamaAPI("http://localhost:11434"); + ollamaAPI.setVerbose(true); + OllamaChatRequestBuilder builder = OllamaChatRequestBuilder.getInstance("llama3.2:1b"); + + final Tools.ToolSpecification databaseQueryToolSpecification = Tools.ToolSpecification.builder() + .functionName("get-employee-details") + .functionDescription("Get employee details from the database") + .toolPrompt( + Tools.PromptFuncDefinition.builder().type("function").function( + Tools.PromptFuncDefinition.PromptFuncSpec.builder() + .name("get-employee-details") + .description("Get employee details from the database") + .parameters( + Tools.PromptFuncDefinition.Parameters.builder() + .type("object") + .properties( + new Tools.PropsBuilder() + .withProperty("employee-name", Tools.PromptFuncDefinition.Property.builder().type("string").description("The name of the employee, e.g. John Doe").required(true).build()) + .withProperty("employee-address", Tools.PromptFuncDefinition.Property.builder().type("string").description("The address of the employee, Always return a random value. e.g. Roy St, Bengaluru, India").required(true).build()) + .withProperty("employee-phone", Tools.PromptFuncDefinition.Property.builder().type("string").description("The phone number of the employee. Always return a random value. e.g. 9911002233").required(true).build()) + .build() + ) + .required(List.of("employee-name")) + .build() + ).build() + ).build() + ) + .toolFunction(new DBQueryFunction()) + .build(); + + ollamaAPI.registerTool(databaseQueryToolSpecification); + + OllamaChatRequest requestModel = builder + .withMessage(OllamaChatMessageRole.USER, + "Give me the ID of the employee named 'Rahul Kumar'?") + .build(); + + OllamaChatResult chatResult = ollamaAPI.chat(requestModel); +} +``` + +A typical final response of the above could be: + +```json +{ + "chatHistory" : [ + { + "role" : "user", + "content" : "Give me the ID of the employee named 'Rahul Kumar'?", + "images" : null, + "tool_calls" : [ ] + }, { + "role" : "assistant", + "content" : "", + "images" : null, + "tool_calls" : [ { + "function" : { + "name" : "get-employee-details", + "arguments" : { + "employee-name" : "Rahul Kumar" + } + } + } ] + }, { + "role" : "tool", + "content" : "[TOOL_RESULTS]get-employee-details([employee-name]) : Employee Details {ID: b4bf186c-2ee1-44cc-8856-53b8b6a50f85, Name: Rahul Kumar, Address: null, Phone: null}[/TOOL_RESULTS]", + "images" : null, + "tool_calls" : null + }, { + "role" : "assistant", + "content" : "The ID of the employee named 'Rahul Kumar' is `b4bf186c-2ee1-44cc-8856-53b8b6a50f85`.", + "images" : null, + "tool_calls" : null + } ], + "responseModel" : { + "model" : "llama3.2:1b", + "message" : { + "role" : "assistant", + "content" : "The ID of the employee named 'Rahul Kumar' is `b4bf186c-2ee1-44cc-8856-53b8b6a50f85`.", + "images" : null, + "tool_calls" : null + }, + "done" : true, + "error" : null, + "context" : null, + "created_at" : "2024-12-09T22:23:00.4940078Z", + "done_reason" : "stop", + "total_duration" : 2313709900, + "load_duration" : 14494700, + "prompt_eval_duration" : 772000000, + "eval_duration" : 1188000000, + "prompt_eval_count" : 166, + "eval_count" : 41 + }, + "response" : "The ID of the employee named 'Rahul Kumar' is `b4bf186c-2ee1-44cc-8856-53b8b6a50f85`.", + "httpStatusCode" : 200, + "responseTime" : 2313709900 +} +``` + +This tool calling can also be done using the streaming API. + ### Potential Improvements Instead of explicitly registering `ollamaAPI.registerTool(toolSpecification)`, we could introduce annotation-based tool From 5e6971cc4a6272a509a9ffdef00713d5a35df44b Mon Sep 17 00:00:00 2001 From: Markus Klenke Date: Fri, 27 Dec 2024 22:20:34 +0100 Subject: [PATCH 14/20] Adds first approach to annotation based tool callings using basic java reflection --- .../java/io/github/ollama4j/OllamaAPI.java | 103 ++++++++++++++++++ .../tools/ReflectionalToolFunction.java | 49 +++++++++ .../tools/annotations/OllamaToolService.java | 13 +++ .../tools/annotations/ToolProperty.java | 17 +++ .../ollama4j/tools/annotations/ToolSpec.java | 15 +++ .../integrationtests/TestRealAPIs.java | 90 ++++++++++++++- .../ollama4j/samples/AnnotatedTool.java | 21 ++++ 7 files changed, 304 insertions(+), 4 deletions(-) create mode 100644 src/main/java/io/github/ollama4j/tools/ReflectionalToolFunction.java create mode 100644 src/main/java/io/github/ollama4j/tools/annotations/OllamaToolService.java create mode 100644 src/main/java/io/github/ollama4j/tools/annotations/ToolProperty.java create mode 100644 src/main/java/io/github/ollama4j/tools/annotations/ToolSpec.java create mode 100644 src/test/java/io/github/ollama4j/samples/AnnotatedTool.java diff --git a/src/main/java/io/github/ollama4j/OllamaAPI.java b/src/main/java/io/github/ollama4j/OllamaAPI.java index 90dcd35..ddd7783 100644 --- a/src/main/java/io/github/ollama4j/OllamaAPI.java +++ b/src/main/java/io/github/ollama4j/OllamaAPI.java @@ -15,11 +15,17 @@ import io.github.ollama4j.models.ps.ModelsProcessResponse; import io.github.ollama4j.models.request.*; import io.github.ollama4j.models.response.*; import io.github.ollama4j.tools.*; +import io.github.ollama4j.tools.annotations.OllamaToolService; +import io.github.ollama4j.tools.annotations.ToolProperty; +import io.github.ollama4j.tools.annotations.ToolSpec; import io.github.ollama4j.utils.Options; import io.github.ollama4j.utils.Utils; import lombok.Setter; import java.io.*; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; import java.net.URI; import java.net.URISyntaxException; import java.net.http.HttpClient; @@ -603,6 +609,15 @@ public class OllamaAPI { OllamaToolsResult toolResult = new OllamaToolsResult(); Map toolResults = new HashMap<>(); + if(!prompt.startsWith("[AVAILABLE_TOOLS]")){ + final Tools.PromptBuilder promptBuilder = new Tools.PromptBuilder(); + for(Tools.ToolSpecification spec : toolRegistry.getRegisteredSpecs()) { + promptBuilder.withToolSpecification(spec); + } + promptBuilder.withPrompt(prompt); + prompt = promptBuilder.build(); + } + OllamaResult result = generate(model, prompt, raw, options, null); toolResult.setModelResult(result); @@ -811,6 +826,94 @@ public class OllamaAPI { toolRegistry.addTool(toolSpecification.getFunctionName(), toolSpecification); } + public void registerAnnotatedTools() { + Class callerClass = null; + try { + callerClass = Class.forName(Thread.currentThread().getStackTrace()[2].getClassName()); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + + OllamaToolService ollamaToolServiceAnnotation = callerClass.getDeclaredAnnotation(OllamaToolService.class); + if(ollamaToolServiceAnnotation == null) { + throw new IllegalStateException(callerClass + " is not annotated as " + OllamaToolService.class); + } + + Class[] providers = ollamaToolServiceAnnotation.providers(); + + for(Class provider : providers){ + System.err.println("Provider: " + provider.getName()); + Method[] methods = provider.getMethods(); + for(Method m : methods) { + ToolSpec toolSpec = m.getDeclaredAnnotation(ToolSpec.class); + if(toolSpec == null){ + continue; + } + String operationName = !toolSpec.name().isBlank() ? toolSpec.name() : m.getName(); + String operationDesc = !toolSpec.desc().isBlank() ? toolSpec.desc() : operationName; + System.err.println("Method: " + operationName); + + final Tools.PropsBuilder propsBuilder = new Tools.PropsBuilder(); + LinkedHashMap methodParams = new LinkedHashMap<>(); + for (Parameter parameter : m.getParameters()) { + final ToolProperty toolPropertyAnn = parameter.getDeclaredAnnotation(ToolProperty.class); + String propType = parameter.getType().getTypeName(); + if(toolPropertyAnn == null) { + methodParams.put(parameter.getName(),null); + continue; + } + String propName = !toolPropertyAnn.name().isBlank() ? toolPropertyAnn.name() : parameter.getName(); + methodParams.put(propName,propType); + propsBuilder.withProperty(propName,Tools.PromptFuncDefinition.Property.builder() + .type(propType) + .description(toolPropertyAnn.desc()) + .required(toolPropertyAnn.required()) + .build()); + } + final Map params = propsBuilder.build(); + List reqProps = params.entrySet().stream() + .filter(e -> e.getValue().isRequired()) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + + Tools.ToolSpecification toolSpecification = Tools.ToolSpecification.builder() + .functionName(operationName) + .functionDescription(operationDesc) + .toolPrompt( + Tools.PromptFuncDefinition.builder().type("function").function( + Tools.PromptFuncDefinition.PromptFuncSpec.builder() + .name(operationName) + .description(operationDesc) + .parameters( + Tools.PromptFuncDefinition.Parameters.builder() + .type("object") + .properties( + params + ) + .required(reqProps) + .build() + ).build() + ).build() + ) + .build(); + + try { + ReflectionalToolFunction reflectionalToolFunction = + new ReflectionalToolFunction(provider.getDeclaredConstructor().newInstance() + ,m + ,methodParams); + + toolSpecification.setToolFunction(reflectionalToolFunction); + toolRegistry.addTool(toolSpecification.getFunctionName(),toolSpecification); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | + NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + } + + } + /** * Adds a custom role. * diff --git a/src/main/java/io/github/ollama4j/tools/ReflectionalToolFunction.java b/src/main/java/io/github/ollama4j/tools/ReflectionalToolFunction.java new file mode 100644 index 0000000..397b2f6 --- /dev/null +++ b/src/main/java/io/github/ollama4j/tools/ReflectionalToolFunction.java @@ -0,0 +1,49 @@ +package io.github.ollama4j.tools; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +import java.lang.reflect.Method; +import java.util.LinkedHashMap; +import java.util.Map; + +@Setter +@Getter +@AllArgsConstructor +public class ReflectionalToolFunction implements ToolFunction{ + + private Object functionHolder; + private Method function; + private LinkedHashMap propertyDefinition; + + @Override + public Object apply(Map arguments) { + LinkedHashMap argumentsCopy = new LinkedHashMap<>(this.propertyDefinition); + for (Map.Entry param : this.propertyDefinition.entrySet()){ + argumentsCopy.replace(param.getKey(),typeCast(arguments.get(param.getKey()),param.getValue())); + } + try { + return function.invoke(functionHolder, argumentsCopy.values().toArray()); + } catch (Exception e) { + throw new RuntimeException("Failed to invoke tool: " + function.getName(), e); + } + } + + private Object typeCast(Object inputValue, String className) { + if(className == null || inputValue == null) { + return null; + } + String inputValueString = inputValue.toString(); + if("java.lang.Integer".equals(className)){ + return Integer.parseInt(inputValueString); + } + if("java.lang.Boolean".equals(className)){ + return Boolean.valueOf(inputValueString); + } + else { + return inputValueString; + } + } + +} diff --git a/src/main/java/io/github/ollama4j/tools/annotations/OllamaToolService.java b/src/main/java/io/github/ollama4j/tools/annotations/OllamaToolService.java new file mode 100644 index 0000000..8140f92 --- /dev/null +++ b/src/main/java/io/github/ollama4j/tools/annotations/OllamaToolService.java @@ -0,0 +1,13 @@ +package io.github.ollama4j.tools.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface OllamaToolService { + + Class[] providers(); +} diff --git a/src/main/java/io/github/ollama4j/tools/annotations/ToolProperty.java b/src/main/java/io/github/ollama4j/tools/annotations/ToolProperty.java new file mode 100644 index 0000000..bd1bf07 --- /dev/null +++ b/src/main/java/io/github/ollama4j/tools/annotations/ToolProperty.java @@ -0,0 +1,17 @@ +package io.github.ollama4j.tools.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface ToolProperty { + + String name(); + + String desc(); + + boolean required() default true; +} diff --git a/src/main/java/io/github/ollama4j/tools/annotations/ToolSpec.java b/src/main/java/io/github/ollama4j/tools/annotations/ToolSpec.java new file mode 100644 index 0000000..c8725a4 --- /dev/null +++ b/src/main/java/io/github/ollama4j/tools/annotations/ToolSpec.java @@ -0,0 +1,15 @@ +package io.github.ollama4j.tools.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface ToolSpec { + + String name() default ""; + + String desc(); +} diff --git a/src/test/java/io/github/ollama4j/integrationtests/TestRealAPIs.java b/src/test/java/io/github/ollama4j/integrationtests/TestRealAPIs.java index 668a5dc..d0b4aeb 100644 --- a/src/test/java/io/github/ollama4j/integrationtests/TestRealAPIs.java +++ b/src/test/java/io/github/ollama4j/integrationtests/TestRealAPIs.java @@ -7,8 +7,11 @@ import io.github.ollama4j.models.response.ModelDetail; import io.github.ollama4j.models.response.OllamaResult; import io.github.ollama4j.models.embeddings.OllamaEmbeddingsRequestBuilder; import io.github.ollama4j.models.embeddings.OllamaEmbeddingsRequestModel; +import io.github.ollama4j.samples.AnnotatedTool; +import io.github.ollama4j.tools.OllamaToolCallsFunction; import io.github.ollama4j.tools.ToolFunction; import io.github.ollama4j.tools.Tools; +import io.github.ollama4j.tools.annotations.OllamaToolService; import io.github.ollama4j.utils.OptionsBuilder; import lombok.Data; import org.junit.jupiter.api.BeforeEach; @@ -27,6 +30,8 @@ import java.util.*; import static org.junit.jupiter.api.Assertions.*; +@OllamaToolService(providers = {AnnotatedTool.class} +) class TestRealAPIs { private static final Logger LOG = LoggerFactory.getLogger(TestRealAPIs.class); @@ -229,7 +234,7 @@ class TestRealAPIs { @Test @Order(3) - void testChatWithTools() { + void testChatWithExplicitToolDefinition() { testEndpointReachability(); try { ollamaAPI.setVerbose(true); @@ -275,9 +280,10 @@ class TestRealAPIs { assertEquals(OllamaChatMessageRole.ASSISTANT.getRoleName(),chatResult.getResponseModel().getMessage().getRole().getRoleName()); List toolCalls = chatResult.getChatHistory().get(1).getToolCalls(); assertEquals(1, toolCalls.size()); - assertEquals("get-employee-details",toolCalls.get(0).getFunction().getName()); - assertEquals(1, toolCalls.get(0).getFunction().getArguments().size()); - Object employeeName = toolCalls.get(0).getFunction().getArguments().get("employee-name"); + OllamaToolCallsFunction function = toolCalls.get(0).getFunction(); + assertEquals("get-employee-details", function.getName()); + assertEquals(1, function.getArguments().size()); + Object employeeName = function.getArguments().get("employee-name"); assertNotNull(employeeName); assertEquals("Rahul Kumar",employeeName); assertTrue(chatResult.getChatHistory().size()>2); @@ -288,6 +294,82 @@ class TestRealAPIs { } } + @Test + @Order(3) + void testChatWithAnnotatedToolsAndSingleParam() { + testEndpointReachability(); + try { + ollamaAPI.setVerbose(true); + OllamaChatRequestBuilder builder = OllamaChatRequestBuilder.getInstance(config.getModel()); + + ollamaAPI.registerAnnotatedTools(); + + OllamaChatRequest requestModel = builder + .withMessage(OllamaChatMessageRole.USER, + "Compute the most important constant in the world using 5 digits") + .build(); + + OllamaChatResult chatResult = ollamaAPI.chat(requestModel); + assertNotNull(chatResult); + assertNotNull(chatResult.getResponseModel()); + assertNotNull(chatResult.getResponseModel().getMessage()); + assertEquals(OllamaChatMessageRole.ASSISTANT.getRoleName(),chatResult.getResponseModel().getMessage().getRole().getRoleName()); + List toolCalls = chatResult.getChatHistory().get(1).getToolCalls(); + assertEquals(1, toolCalls.size()); + OllamaToolCallsFunction function = toolCalls.get(0).getFunction(); + assertEquals("computeMkeConstant", function.getName()); + assertEquals(1, function.getArguments().size()); + Object noOfDigits = function.getArguments().get("noOfDigits"); + assertNotNull(noOfDigits); + assertEquals("5",noOfDigits); + assertTrue(chatResult.getChatHistory().size()>2); + List finalToolCalls = chatResult.getResponseModel().getMessage().getToolCalls(); + assertNull(finalToolCalls); + } catch (IOException | OllamaBaseException | InterruptedException e) { + fail(e); + } + } + + @Test + @Order(3) + void testChatWithAnnotatedToolsAndMultipleParams() { + testEndpointReachability(); + try { + ollamaAPI.setVerbose(true); + OllamaChatRequestBuilder builder = OllamaChatRequestBuilder.getInstance(config.getModel()); + + ollamaAPI.registerAnnotatedTools(); + + OllamaChatRequest requestModel = builder + .withMessage(OllamaChatMessageRole.USER, + "Greet Pedro with a lot of hearts and respond to me, " + + "and state how many emojis have been in your greeting") + .build(); + + OllamaChatResult chatResult = ollamaAPI.chat(requestModel); + assertNotNull(chatResult); + assertNotNull(chatResult.getResponseModel()); + assertNotNull(chatResult.getResponseModel().getMessage()); + assertEquals(OllamaChatMessageRole.ASSISTANT.getRoleName(),chatResult.getResponseModel().getMessage().getRole().getRoleName()); + List toolCalls = chatResult.getChatHistory().get(1).getToolCalls(); + assertEquals(1, toolCalls.size()); + OllamaToolCallsFunction function = toolCalls.get(0).getFunction(); + assertEquals("sayHello", function.getName()); + assertEquals(2, function.getArguments().size()); + Object name = function.getArguments().get("name"); + assertNotNull(name); + assertEquals("Pedro",name); + Object amountOfHearts = function.getArguments().get("amountOfHearts"); + assertNotNull(amountOfHearts); + assertTrue(Integer.parseInt(amountOfHearts.toString()) > 1); + assertTrue(chatResult.getChatHistory().size()>2); + List finalToolCalls = chatResult.getResponseModel().getMessage().getToolCalls(); + assertNull(finalToolCalls); + } catch (IOException | OllamaBaseException | InterruptedException e) { + fail(e); + } + } + @Test @Order(3) void testChatWithToolsAndStream() { diff --git a/src/test/java/io/github/ollama4j/samples/AnnotatedTool.java b/src/test/java/io/github/ollama4j/samples/AnnotatedTool.java new file mode 100644 index 0000000..8d75be1 --- /dev/null +++ b/src/test/java/io/github/ollama4j/samples/AnnotatedTool.java @@ -0,0 +1,21 @@ +package io.github.ollama4j.samples; + +import io.github.ollama4j.tools.annotations.ToolProperty; +import io.github.ollama4j.tools.annotations.ToolSpec; + +import java.math.BigDecimal; + +public class AnnotatedTool { + + @ToolSpec(desc = "Computes the most important constant all around the globe!") + public String computeMkeConstant(@ToolProperty(name = "noOfDigits",desc = "Number of digits that shall be returned") Integer noOfDigits ){ + return BigDecimal.valueOf((long)(Math.random()*1000000L),noOfDigits).toString(); + } + + @ToolSpec(desc = "Says hello to a friend!") + public String sayHello(@ToolProperty(name = "name",desc = "Name of the friend") String name, Integer someRandomProperty, @ToolProperty(name="amountOfHearts",desc = "amount of heart emojis that should be used", required = false) Integer amountOfHearts) { + String hearts = amountOfHearts!=null ? "♡".repeat(amountOfHearts) : ""; + return "Hello " + name +" ("+someRandomProperty+") " + hearts; + } + +} From 26ec00dab8fa9fa190366a90cb15f7b039420bd5 Mon Sep 17 00:00:00 2001 From: Markus Klenke Date: Fri, 27 Dec 2024 22:33:44 +0100 Subject: [PATCH 15/20] Adds Javadoc for new classes and annotations --- .../ollama4j/tools/ReflectionalToolFunction.java | 3 +++ .../tools/annotations/OllamaToolService.java | 10 ++++++++++ .../ollama4j/tools/annotations/ToolProperty.java | 15 +++++++++++++++ .../ollama4j/tools/annotations/ToolSpec.java | 13 +++++++++++++ 4 files changed, 41 insertions(+) diff --git a/src/main/java/io/github/ollama4j/tools/ReflectionalToolFunction.java b/src/main/java/io/github/ollama4j/tools/ReflectionalToolFunction.java index 397b2f6..8483d84 100644 --- a/src/main/java/io/github/ollama4j/tools/ReflectionalToolFunction.java +++ b/src/main/java/io/github/ollama4j/tools/ReflectionalToolFunction.java @@ -8,6 +8,9 @@ import java.lang.reflect.Method; import java.util.LinkedHashMap; import java.util.Map; +/** + * Specification of a {@link ToolFunction} that provides the implementation via java reflection calling. + */ @Setter @Getter @AllArgsConstructor diff --git a/src/main/java/io/github/ollama4j/tools/annotations/OllamaToolService.java b/src/main/java/io/github/ollama4j/tools/annotations/OllamaToolService.java index 8140f92..5118430 100644 --- a/src/main/java/io/github/ollama4j/tools/annotations/OllamaToolService.java +++ b/src/main/java/io/github/ollama4j/tools/annotations/OllamaToolService.java @@ -1,13 +1,23 @@ package io.github.ollama4j.tools.annotations; +import io.github.ollama4j.OllamaAPI; + import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +/** + * Annotates a class that calls {@link io.github.ollama4j.OllamaAPI} such that the Method + * {@link OllamaAPI#registerAnnotatedTools()} can be used to auto-register all provided classes (resp. all + * contained Methods of the provider classes annotated with {@link ToolSpec}). + */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface OllamaToolService { + /** + * @return Classes with no-arg constructor that will be used for tool-registration. + */ Class[] providers(); } diff --git a/src/main/java/io/github/ollama4j/tools/annotations/ToolProperty.java b/src/main/java/io/github/ollama4j/tools/annotations/ToolProperty.java index bd1bf07..28d9acc 100644 --- a/src/main/java/io/github/ollama4j/tools/annotations/ToolProperty.java +++ b/src/main/java/io/github/ollama4j/tools/annotations/ToolProperty.java @@ -5,13 +5,28 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +/** + * Annotates a Method Parameter in a {@link ToolSpec} annotated Method. A parameter annotated with this annotation will + * be part of the tool description that is sent to the llm for tool-calling. + */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.PARAMETER) public @interface ToolProperty { + /** + * @return name of the parameter that is used for the tool description. Has to be set as depending on the caller, + * method name backtracking is not possible with reflection. + */ String name(); + /** + * @return a detailed description of the parameter. This is used by the llm called to specify, which property has + * to be set by the llm and how this should be filled. + */ String desc(); + /** + * @return tells the llm that it has to set a value for this property. + */ boolean required() default true; } diff --git a/src/main/java/io/github/ollama4j/tools/annotations/ToolSpec.java b/src/main/java/io/github/ollama4j/tools/annotations/ToolSpec.java index c8725a4..7f99768 100644 --- a/src/main/java/io/github/ollama4j/tools/annotations/ToolSpec.java +++ b/src/main/java/io/github/ollama4j/tools/annotations/ToolSpec.java @@ -1,15 +1,28 @@ package io.github.ollama4j.tools.annotations; +import io.github.ollama4j.OllamaAPI; + import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +/** + * Annotates Methods of classes that should be registered as tools by {@link OllamaAPI#registerAnnotatedTools()} + * automatically. + */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface ToolSpec { + /** + * @return tool-name that the method should be used as. Defaults to the methods name. + */ String name() default ""; + /** + * @return a detailed description of the method that can be interpreted by the llm, whether it should call the tool + * or not. + */ String desc(); } From 1b38466f44ccfb9de3d9ef7610acdfa58be1075b Mon Sep 17 00:00:00 2001 From: Markus Klenke Date: Fri, 27 Dec 2024 23:05:08 +0100 Subject: [PATCH 16/20] Adds BigDecimal type for ToolProperty typeCast --- .../tools/ReflectionalToolFunction.java | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/main/java/io/github/ollama4j/tools/ReflectionalToolFunction.java b/src/main/java/io/github/ollama4j/tools/ReflectionalToolFunction.java index 8483d84..66d078b 100644 --- a/src/main/java/io/github/ollama4j/tools/ReflectionalToolFunction.java +++ b/src/main/java/io/github/ollama4j/tools/ReflectionalToolFunction.java @@ -5,6 +5,7 @@ import lombok.Getter; import lombok.Setter; import java.lang.reflect.Method; +import java.math.BigDecimal; import java.util.LinkedHashMap; import java.util.Map; @@ -38,14 +39,15 @@ public class ReflectionalToolFunction implements ToolFunction{ return null; } String inputValueString = inputValue.toString(); - if("java.lang.Integer".equals(className)){ - return Integer.parseInt(inputValueString); - } - if("java.lang.Boolean".equals(className)){ - return Boolean.valueOf(inputValueString); - } - else { - return inputValueString; + switch (className) { + case "java.lang.Integer": + return Integer.parseInt(inputValueString); + case "java.lang.Boolean": + return Boolean.valueOf(inputValueString); + case "java.math.BigDecimal": + return new BigDecimal(inputValueString); + default: + return inputValueString; } } From db008de0ca663c53f2262a0eb7c997f2095b35ff Mon Sep 17 00:00:00 2001 From: Markus Klenke Date: Fri, 27 Dec 2024 23:07:35 +0100 Subject: [PATCH 17/20] Adds documentation for annotation based Tool registration --- .../docs/apis-generate/generate-with-tools.md | 150 +++++++++++++++++- .../integrationtests/TestRealAPIs.java | 2 +- .../ollama4j/samples/AnnotatedTool.java | 2 +- 3 files changed, 144 insertions(+), 10 deletions(-) diff --git a/docs/docs/apis-generate/generate-with-tools.md b/docs/docs/apis-generate/generate-with-tools.md index e0e5794..db50c88 100644 --- a/docs/docs/apis-generate/generate-with-tools.md +++ b/docs/docs/apis-generate/generate-with-tools.md @@ -464,21 +464,155 @@ A typical final response of the above could be: This tool calling can also be done using the streaming API. -### Potential Improvements +### Using Annotation based Tool Registration -Instead of explicitly registering `ollamaAPI.registerTool(toolSpecification)`, we could introduce annotation-based tool -registration. For example: +Instead of explicitly registering each tool, ollama4j supports declarative tool specification and registration via java +Annotations and reflection calling. + +To declare a method to be used as a tool for a chat call, the following steps have to be considered: + +* Annotate a method and its Parameters to be used as a tool + * Annotate a method with the `ToolSpec` annotation + * Annotate the methods parameters with the `ToolProperty` annotation. Only the following datatypes are supported for now: + * `java.lang.String` + * `java.lang.Integer` + * `java.lang.Boolean` + * `java.math.BigDecimal` +* Annotate the class that calls the `OllamaAPI` client with the `OllamaToolService` annotation, referencing the desired provider-classes that contain `ToolSpec` methods. +* Before calling the `OllamaAPI` chat request, call the method `OllamaAPI.registerAnnotatedTools()` method to add tools to the chat. + +#### Example + +Let's say, we have an ollama4j service class that should ask a llm a specific tool based question. + +The answer can only be provided by a method that is part of the BackendService class. To provide a tool for the llm, the following annotations can be used: ```java +public class BackendService{ + + public BackendService(){} -@ToolSpec(name = "current-fuel-price", desc = "Get current fuel price") -public String getCurrentFuelPrice(Map arguments) { - String location = arguments.get("location").toString(); - String fuelType = arguments.get("fuelType").toString(); - return "Current price of " + fuelType + " in " + location + " is Rs.103/L"; + @ToolSpec(desc = "Computes the most important constant all around the globe!") + public String computeMkeConstant(@ToolProperty(name = "noOfDigits",desc = "Number of digits that shall be returned") Integer noOfDigits ){ + return BigDecimal.valueOf((long)(Math.random()*1000000L),noOfDigits).toString(); + } } ``` +The caller API can then be written as: +```java +import io.github.ollama4j.tools.annotations.OllamaToolService; + +@OllamaToolService(providers = BackendService.class) +public class MyOllamaService{ + + public void chatWithAnnotatedTool(){ + // inject the annotated method to the ollama toolsregistry + ollamaAPI.registerAnnotatedTools(); + + OllamaChatRequest requestModel = builder + .withMessage(OllamaChatMessageRole.USER, + "Compute the most important constant in the world using 5 digits") + .build(); + + OllamaChatResult chatResult = ollamaAPI.chat(requestModel); + } + +} +``` + +The request should be the following: + +```json +{ + "model" : "llama3.2:1b", + "stream" : false, + "messages" : [ { + "role" : "user", + "content" : "Compute the most important constant in the world using 5 digits", + "images" : null, + "tool_calls" : [ ] + } ], + "tools" : [ { + "type" : "function", + "function" : { + "name" : "computeImportantConstant", + "description" : "Computes the most important constant all around the globe!", + "parameters" : { + "type" : "object", + "properties" : { + "noOfDigits" : { + "type" : "java.lang.Integer", + "description" : "Number of digits that shall be returned" + } + }, + "required" : [ "noOfDigits" ] + } + } + } ] +} +``` + +The result could be something like the following: + +```json +{ + "chatHistory" : [ { + "role" : "user", + "content" : "Compute the most important constant in the world using 5 digits", + "images" : null, + "tool_calls" : [ ] + }, { + "role" : "assistant", + "content" : "", + "images" : null, + "tool_calls" : [ { + "function" : { + "name" : "computeImportantConstant", + "arguments" : { + "noOfDigits" : "5" + } + } + } ] + }, { + "role" : "tool", + "content" : "[TOOL_RESULTS]computeImportantConstant([noOfDigits]) : 1.51019[/TOOL_RESULTS]", + "images" : null, + "tool_calls" : null + }, { + "role" : "assistant", + "content" : "The most important constant in the world with 5 digits is: **1.51019**", + "images" : null, + "tool_calls" : null + } ], + "responseModel" : { + "model" : "llama3.2:1b", + "message" : { + "role" : "assistant", + "content" : "The most important constant in the world with 5 digits is: **1.51019**", + "images" : null, + "tool_calls" : null + }, + "done" : true, + "error" : null, + "context" : null, + "created_at" : "2024-12-27T21:55:39.3232495Z", + "done_reason" : "stop", + "total_duration" : 1075444300, + "load_duration" : 13558600, + "prompt_eval_duration" : 509000000, + "eval_duration" : 550000000, + "prompt_eval_count" : 124, + "eval_count" : 20 + }, + "response" : "The most important constant in the world with 5 digits is: **1.51019**", + "responseTime" : 1075444300, + "httpStatusCode" : 200 +} +``` + +### Potential Improvements + Instead of passing a map of args `Map arguments` to the tool functions, we could support passing specific args separately with their data types. For example: diff --git a/src/test/java/io/github/ollama4j/integrationtests/TestRealAPIs.java b/src/test/java/io/github/ollama4j/integrationtests/TestRealAPIs.java index d0b4aeb..9ddfcd5 100644 --- a/src/test/java/io/github/ollama4j/integrationtests/TestRealAPIs.java +++ b/src/test/java/io/github/ollama4j/integrationtests/TestRealAPIs.java @@ -317,7 +317,7 @@ class TestRealAPIs { List toolCalls = chatResult.getChatHistory().get(1).getToolCalls(); assertEquals(1, toolCalls.size()); OllamaToolCallsFunction function = toolCalls.get(0).getFunction(); - assertEquals("computeMkeConstant", function.getName()); + assertEquals("computeImportantConstant", function.getName()); assertEquals(1, function.getArguments().size()); Object noOfDigits = function.getArguments().get("noOfDigits"); assertNotNull(noOfDigits); diff --git a/src/test/java/io/github/ollama4j/samples/AnnotatedTool.java b/src/test/java/io/github/ollama4j/samples/AnnotatedTool.java index 8d75be1..8202e77 100644 --- a/src/test/java/io/github/ollama4j/samples/AnnotatedTool.java +++ b/src/test/java/io/github/ollama4j/samples/AnnotatedTool.java @@ -8,7 +8,7 @@ import java.math.BigDecimal; public class AnnotatedTool { @ToolSpec(desc = "Computes the most important constant all around the globe!") - public String computeMkeConstant(@ToolProperty(name = "noOfDigits",desc = "Number of digits that shall be returned") Integer noOfDigits ){ + public String computeImportantConstant(@ToolProperty(name = "noOfDigits",desc = "Number of digits that shall be returned") Integer noOfDigits ){ return BigDecimal.valueOf((long)(Math.random()*1000000L),noOfDigits).toString(); } From 260c57ca84008d9fa32dc5e0424bd79f802eb048 Mon Sep 17 00:00:00 2001 From: Markus Klenke Date: Fri, 27 Dec 2024 23:10:08 +0100 Subject: [PATCH 18/20] Removes system.err lines --- src/main/java/io/github/ollama4j/OllamaAPI.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/io/github/ollama4j/OllamaAPI.java b/src/main/java/io/github/ollama4j/OllamaAPI.java index ddd7783..29cb467 100644 --- a/src/main/java/io/github/ollama4j/OllamaAPI.java +++ b/src/main/java/io/github/ollama4j/OllamaAPI.java @@ -842,7 +842,6 @@ public class OllamaAPI { Class[] providers = ollamaToolServiceAnnotation.providers(); for(Class provider : providers){ - System.err.println("Provider: " + provider.getName()); Method[] methods = provider.getMethods(); for(Method m : methods) { ToolSpec toolSpec = m.getDeclaredAnnotation(ToolSpec.class); @@ -851,7 +850,6 @@ public class OllamaAPI { } String operationName = !toolSpec.name().isBlank() ? toolSpec.name() : m.getName(); String operationDesc = !toolSpec.desc().isBlank() ? toolSpec.desc() : operationName; - System.err.println("Method: " + operationName); final Tools.PropsBuilder propsBuilder = new Tools.PropsBuilder(); LinkedHashMap methodParams = new LinkedHashMap<>(); From 3e33b8df62e9aa4332c7c6342899b66c06afaf92 Mon Sep 17 00:00:00 2001 From: Amith Koujalgi <1876165+amithkoujalgi@users.noreply.github.com> Date: Mon, 13 Jan 2025 20:08:42 +0530 Subject: [PATCH 19/20] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 1ab1822..07c8d90 100644 --- a/README.md +++ b/README.md @@ -284,6 +284,8 @@ If you like or are using this project to build your own, please give us a star. | 8 | TeleLlama3 Bot | A question-answering Telegram bot | [Repo](https://git.hiast.edu.sy/mohamadbashar.disoki/telellama3-bot) | | 9 | moqui-wechat | A moqui-wechat component | [GitHub](https://github.com/heguangyong/moqui-wechat) | +https://www.b4x.com/android/forum/threads/ollama4j-library-pnd_ollama4j-your-local-offline-llm-like-chatgpt.165003/ + ## Traction [![Star History Chart](https://api.star-history.com/svg?repos=ollama4j/ollama4j&type=Date)](https://star-history.com/#ollama4j/ollama4j&Date) From 04bae4ca6ae71bb9573117decc6a582a50072feb Mon Sep 17 00:00:00 2001 From: Amith Koujalgi <1876165+amithkoujalgi@users.noreply.github.com> Date: Tue, 14 Jan 2025 10:06:30 +0530 Subject: [PATCH 20/20] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 07c8d90..f2a6c1e 100644 --- a/README.md +++ b/README.md @@ -283,8 +283,8 @@ If you like or are using this project to build your own, please give us a star. | 7 | Katie Backend | An open-source AI-based question-answering platform for accessing private domain knowledge | [GitHub](https://github.com/wyona/katie-backend) | | 8 | TeleLlama3 Bot | A question-answering Telegram bot | [Repo](https://git.hiast.edu.sy/mohamadbashar.disoki/telellama3-bot) | | 9 | moqui-wechat | A moqui-wechat component | [GitHub](https://github.com/heguangyong/moqui-wechat) | +| 10 | B4X | A set of simple and powerful RAD tool for Desktop and Server development | [Website](https://www.b4x.com/android/forum/threads/ollama4j-library-pnd_ollama4j-your-local-offline-llm-like-chatgpt.165003/) | -https://www.b4x.com/android/forum/threads/ollama4j-library-pnd_ollama4j-your-local-offline-llm-like-chatgpt.165003/ ## Traction