Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for tools for the ollama provider #662

Closed
wants to merge 33 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
38261d6
Proposition for : #305, tested on llama3
humcqc Jun 10, 2024
55037da
Test class missing in previous commit
humcqc Jun 10, 2024
c3c4776
Fix typo
humcqc Jun 10, 2024
95e3d20
Review 1
humcqc Jun 10, 2024
213c0af
Fix experimentalTool default value
humcqc Jun 10, 2024
e33b92d
New Prompt, but still some issues with send poem. Need to tune it to …
humcqc Jun 12, 2024
9f951e7
Fix Format
humcqc Jun 12, 2024
19f2157
Fix Format 2
humcqc Jun 12, 2024
aba545d
Fix Test model
humcqc Jun 12, 2024
9231922
New approach
humcqc Jun 15, 2024
b4e1d04
small enhancement still need to fix get_Expenses
humcqc Jun 15, 2024
d06a779
Prompt Optimisation for Llama3
humcqc Jun 15, 2024
51c8f39
fix format
humcqc Jun 15, 2024
6f2b938
Merge branch 'quarkiverse:main' into main
humcqc Jun 16, 2024
3fe6b8d
change tests order
humcqc Jun 16, 2024
c6d0d18
Merge branch 'quarkiverse:main' into main
humcqc Jun 24, 2024
da70e5b
Merge branch 'quarkiverse:main' into main
humcqc Jun 30, 2024
93ec2cf
- Depends on https://github.com/langchain4j/langchain4j/pull/1353
humcqc Jun 30, 2024
85a650c
- Add Missing Class
humcqc Jun 30, 2024
59a58e4
Merge branch 'main' into main
humcqc Jul 4, 2024
cc814b0
- Implementation based on released langchain 0.32.0.
humcqc Jul 15, 2024
1768ec5
Merge branch 'quarkiverse:main' into main
humcqc Jul 15, 2024
0424252
- Clean up revert and merge.
humcqc Jul 15, 2024
d96b632
- Fix langchain4j undependency.
humcqc Jul 15, 2024
3aa3806
- Fix langchain4j unwanted dependency 2
humcqc Jul 15, 2024
a6759c8
- Fix native build
humcqc Jul 15, 2024
05fff69
- @geoand review 1
humcqc Jul 16, 2024
7d99874
Merge branch 'main' into main
humcqc Jul 24, 2024
61a4343
- Fix build + rename file to be more explicit + add unit test on Vari…
humcqc Jul 24, 2024
e00ecea
- Fix import Order
humcqc Jul 24, 2024
02e82db
- Fix conflict merge impact
humcqc Jul 24, 2024
cf1a560
- Prompt Optimization for llama3.1
humcqc Jul 24, 2024
d1e2a53
- Fix format
humcqc Jul 24, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package io.quarkiverse.langchain4j.test;

import static org.assertj.core.api.Assertions.assertThat;

import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import dev.langchain4j.agent.tool.ToolExecutionRequest;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.model.output.TokenUsage;
import io.quarkiverse.langchain4j.data.AiStatsMessage;
import io.quarkiverse.langchain4j.runtime.aiservice.VariableHandler;
import io.quarkus.test.QuarkusUnitTest;

public class VariableHandlerTest {

@RegisterExtension
static final QuarkusUnitTest unitTest = new QuarkusUnitTest()
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class));

@Test
void test_substitution_on_arguments() {
VariableHandler variableHandler = new VariableHandler();

String arguments = "{\"arg0\": 2.0, \"arg1\": $(id1)}";

variableHandler.addVariable("id1", "3.1");

ToolExecutionRequest request = ToolExecutionRequest.builder()
.arguments(arguments)
.build();

ToolExecutionRequest modifiedRequest = variableHandler.substituteVariables(request);

assertThat(modifiedRequest.arguments()).isEqualTo("{\"arg0\": 2.0, \"arg1\": 3.1}");
}

@Test
void test_substitution_on_ai_stats_message() {
VariableHandler variableHandler = new VariableHandler();

String text = "The expected result is $(result1).";

variableHandler.addVariable("result1", "3.1");

AiMessage aiMessage = AiMessage.aiMessage(text);
AiStatsMessage aiStatsMessage = AiStatsMessage.from(aiMessage, new TokenUsage());
AiMessage modifiedMessage = variableHandler.substituteVariables(aiStatsMessage);

assertThat(modifiedMessage.text()).isEqualTo("The expected result is 3.1.");
}

@Test
void test_substitution_on_ai_message() {
VariableHandler variableHandler = new VariableHandler();

String text = "The expected result is $(result1).";

variableHandler.addVariable("result1", "3.1");

AiMessage aiMessage = AiMessage.aiMessage(text);
AiMessage modifiedMessage = variableHandler.substituteVariables(aiMessage);

assertThat(modifiedMessage.text()).isEqualTo(text);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package io.quarkiverse.langchain4j.data;

import java.util.List;

import dev.langchain4j.agent.tool.ToolExecutionRequest;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.internal.ValidationUtils;
import dev.langchain4j.model.output.TokenUsage;

/**
* This class is the equivalent of Langchain4j AiMessage.
* It contains the token usage from the response that produce this AiMessage.
* And add the possibility to update the text in case of text containing Tools Result variables.
* Needed for @{@link io.quarkiverse.langchain4j.runtime.aiservice.VariableHandler}
* Example of usage in ExperimentalParallelToolsDelegate in Ollama model provider
*/
public class AiStatsMessage extends AiMessage {
humcqc marked this conversation as resolved.
Show resolved Hide resolved
private String updatableText;

final TokenUsage tokenUsage;

public AiStatsMessage(String text, TokenUsage tokenUsage) {
super(text);
this.updatableText = text;
this.tokenUsage = ValidationUtils.ensureNotNull(tokenUsage, "tokenUsage");
}

AiStatsMessage(List<ToolExecutionRequest> toolExecutionRequests, TokenUsage tokenUsage) {
super(toolExecutionRequests);
this.tokenUsage = ValidationUtils.ensureNotNull(tokenUsage, "tokenUsage");
}

AiStatsMessage(String text, List<ToolExecutionRequest> toolExecutionRequests, TokenUsage tokenUsage) {
super(text, toolExecutionRequests);
this.updatableText = text;
this.tokenUsage = ValidationUtils.ensureNotNull(tokenUsage, "tokenUsage");
}

public void updateText(String text) {
this.updatableText = text;
}

@Override
public String text() {
return updatableText;
}

public TokenUsage getTokenUsage() {
return tokenUsage;
}

public static AiStatsMessage from(AiMessage aiMessage, TokenUsage tokenUsage) {
if (aiMessage.text() == null) {
return new AiStatsMessage(aiMessage.toolExecutionRequests(), tokenUsage);
} else if (aiMessage.hasToolExecutionRequests()) {
return new AiStatsMessage(aiMessage.text(), aiMessage.toolExecutionRequests(), tokenUsage);
} else {
return new AiStatsMessage(aiMessage.text(), tokenUsage);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -213,28 +213,41 @@ public void accept(Response<AiMessage> message) {
}

AiMessage aiMessage = response.content();
chatMemory.add(aiMessage);

if (!aiMessage.hasToolExecutionRequests()) {
// If there is no tool Execution request we add the Ai Message directly in the chatMEmory
chatMemory.add(aiMessage);
break;
}

List<ToolExecutionResultMessage> tmpToolExecutionResultMessages = new ArrayList<>();
VariableHandler variableHandler = new VariableHandler();

for (ToolExecutionRequest toolExecutionRequest : aiMessage.toolExecutionRequests()) {
log.debugv("Attempting to execute tool {0}", toolExecutionRequest);
ToolExecutor toolExecutor = toolExecutors.get(toolExecutionRequest.name());
if (toolExecutor == null) {
throw runtime("Tool executor %s not found", toolExecutionRequest.name());
}
toolExecutionRequest = variableHandler.substituteVariables(toolExecutionRequest);
String toolExecutionResult = toolExecutor.execute(toolExecutionRequest, memoryId);
log.debugv("Result of {0} is '{1}'", toolExecutionRequest, toolExecutionResult);
log.debugv("Saving result {0} into key {1}", toolExecutionResult, toolExecutionRequest.id());
variableHandler.addVariable(toolExecutionRequest.id(), toolExecutionResult);
log.debugv("Result of {0} is {1}", toolExecutionRequest, toolExecutionResult);
ToolExecutionResultMessage toolExecutionResultMessage = ToolExecutionResultMessage.from(
toolExecutionRequest,
toolExecutionResult);
if (audit != null) {
audit.addApplicationToLLMMessage(toolExecutionResultMessage);
}
chatMemory.add(toolExecutionResultMessage);
tmpToolExecutionResultMessages.add(toolExecutionResultMessage);
}
// In case of tool Execution request we need to update the AiMessage with tools results
// before adding it into chatMemory
aiMessage = variableHandler.substituteVariables(aiMessage);

chatMemory.add(aiMessage);
tmpToolExecutionResultMessages.forEach(chatMemory::add);

log.debug("Attempting to obtain AI response");
response = context.chatModel.generate(chatMemory.messages(), toolSpecifications);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package io.quarkiverse.langchain4j.runtime.aiservice;

import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import dev.langchain4j.Experimental;
import dev.langchain4j.agent.tool.ToolExecutionRequest;
import dev.langchain4j.data.message.AiMessage;
import io.quarkiverse.langchain4j.data.AiStatsMessage;

/**
* This class associate the ToolExecutionResul to the associated variable
* identified by @{@link ToolExecutionRequest#id()}.
* It will use them after when tools inputs @{@link ToolExecutionRequest#arguments()} are based
* on previous result variable and when AiMessage text contains variables too.
* See usage in @{@link AiServiceMethodImplementationSupport}
*/
@Experimental
public class VariableHandler {

static Pattern VARIABLE_PATTERN = Pattern.compile("\\$\\((.*?)\\)");

Map<String, String> variables = new HashMap<>();

public void addVariable(String var, String value) {
variables.put(var, value);
}

public AiMessage substituteVariables(AiMessage message) {
if (message.text() == null) {
return message;
}
if (message instanceof AiStatsMessage updatableMessage) {
updatableMessage.updateText(substituteVariables(message.text(), variables));
return updatableMessage;
}
return message;
}

public ToolExecutionRequest substituteVariables(ToolExecutionRequest toolExecutionRequest) {
return ToolExecutionRequest.builder()
.id(toolExecutionRequest.id())
.name(toolExecutionRequest.name())
.arguments(substituteVariables(toolExecutionRequest.arguments(), variables)).build();
}

private static String substituteVariables(String msg, Map<String, String> resultMap) {
Matcher matcher = VARIABLE_PATTERN.matcher(msg);
StringBuilder newArguments = new StringBuilder();
if (!matcher.find()) {
return msg;
}
do {
String key = matcher.group(1);
String replacement = resultMap.getOrDefault(key, matcher.group(0));
matcher.appendReplacement(newArguments, replacement);
} while (matcher.find());
matcher.appendTail(newArguments);
return newArguments.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,23 @@ endif::add-copy-button-to-env-var[]
|`true`


a| [[quarkus-langchain4j-ollama_quarkus-langchain4j-ollama-experimental-tools]]`link:#quarkus-langchain4j-ollama_quarkus-langchain4j-ollama-experimental-tools[quarkus.langchain4j.ollama.experimental-tools]`


[.description]
--
the experimental tools name. Currently, the only accepted values are `NONE, PARALLEL, SEQUENTIAL` NONE: for no tools, works as before without handling tools. PARALLEL: Tools we be used and simulated with one call to llm, that will answer with all tool request to execute and the response using the result of the tool request. SEQUENTIAL: Tools will be call sequentially and llm will call next tool following tool request result.

ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++QUARKUS_LANGCHAIN4J_OLLAMA_EXPERIMENTAL_TOOLS+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++QUARKUS_LANGCHAIN4J_OLLAMA_EXPERIMENTAL_TOOLS+++`
endif::add-copy-button-to-env-var[]
--|string
|`NONE`


a| [[quarkus-langchain4j-ollama_quarkus-langchain4j-ollama-chat-model-temperature]]`link:#quarkus-langchain4j-ollama_quarkus-langchain4j-ollama-chat-model-temperature[quarkus.langchain4j.ollama.chat-model.temperature]`


Expand Down Expand Up @@ -567,6 +584,23 @@ endif::add-copy-button-to-env-var[]
|`true`


a| [[quarkus-langchain4j-ollama_quarkus-langchain4j-ollama-model-name-experimental-tools]]`link:#quarkus-langchain4j-ollama_quarkus-langchain4j-ollama-model-name-experimental-tools[quarkus.langchain4j.ollama."model-name".experimental-tools]`


[.description]
--
the experimental tools name. Currently, the only accepted values are `NONE, PARALLEL, SEQUENTIAL` NONE: for no tools, works as before without handling tools. PARALLEL: Tools we be used and simulated with one call to llm, that will answer with all tool request to execute and the response using the result of the tool request. SEQUENTIAL: Tools will be call sequentially and llm will call next tool following tool request result.

ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++QUARKUS_LANGCHAIN4J_OLLAMA__MODEL_NAME__EXPERIMENTAL_TOOLS+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++QUARKUS_LANGCHAIN4J_OLLAMA__MODEL_NAME__EXPERIMENTAL_TOOLS+++`
endif::add-copy-button-to-env-var[]
--|string
|`NONE`


a| [[quarkus-langchain4j-ollama_quarkus-langchain4j-ollama-model-name-chat-model-temperature]]`link:#quarkus-langchain4j-ollama_quarkus-langchain4j-ollama-model-name-chat-model-temperature[quarkus.langchain4j.ollama."model-name".chat-model.temperature]`


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.acme.example.openai.chat.ollama;

import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import io.quarkiverse.langchain4j.RegisterAiService;

@RegisterAiService(tools = Tools.ExpenseService.class)
public interface PropertyManagerAssistant {
@SystemMessage("""
You are a property manager assistant, answering to co-owners requests.
Format the date as YYYY-MM-DD and the time as HH:MM
Today is {{current_date_time}} use this date as date time reference
The co-owners condominium is: {condominium}
""")
@UserMessage("""
{{request}}
""")
String answer(String condominium, String request);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.acme.example.openai.chat.ollama;

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

@Path("chat-with-tools")
public class ToolChatLanguageModelResource {

@Inject
PropertyManagerAssistant assistant;

@GET
@Path("expenses")
public String tools() {
return assistant.answer("Rive de marne", "What is the expenses for 2023 ?");
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package org.acme.example.openai.chat.ollama;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Singleton;

import dev.langchain4j.agent.tool.P;
import dev.langchain4j.agent.tool.Tool;
import io.quarkus.logging.Log;

public class Tools {

@Singleton
@SuppressWarnings("unused")
public static class Calculator {
@Tool("Calculates the length of a string")
int stringLength(String s) {
return s.length();
}

@Tool("Calculates the sum of two numbers")
int add(int a, int b) {
return a + b;
}

@Tool("Calculates the square root of a number")
double sqrt(int x) {
return Math.sqrt(x);
}
}

@Singleton
@SuppressWarnings("unused")
public static class ExpenseService {
@Tool("Get expenses for a given condominium, from date and to date.")
public String getExpenses(String condominium, String fromDate, String toDate) {
String result = String.format("""
The Expenses for %s from %s to %s are:
- Expense hp12: 2800e
- Expense 2: 15000e
""", condominium, fromDate, toDate);
Log.infof(result);
return result;
}
}

@ApplicationScoped
public static class EmailService {
@Tool("send the given content by email")
@SuppressWarnings("unused")
public void sendAnEmail(@P("Content to send") String content) {
System.out.printf("""
***
*** Tool sendAnEmail has been executed successfully!
*** Content: %s
""", content);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
quarkus.langchain4j.timeout=60s
quarkus.langchain4j.log-requests=true
quarkus.langchain4j.log-responses=true
quarkus.langchain4j.ollama.chat-model.model-id = llama3
quarkus.langchain4j.ollama.chat-model.temperature = 0.0
quarkus.langchain4j.ollama.chat-model.num-ctx = 3072
quarkus.langchain4j.ollama.chat-model.num-predict = 3072
quarkus.langchain4j.ollama.experimental-tools = PARALLEL
Loading
Loading