This is an automated email from the ASF dual-hosted git repository. orpiske pushed a commit to branch camel-4.8.x in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/camel-4.8.x by this push: new 56e8932d503 CAMEL-21355: refine no tools called scenario 56e8932d503 is described below commit 56e8932d5032d2661f8dff7587ad47fec7e79bff Author: Otavio Rodolfo Piske <angusyo...@gmail.com> AuthorDate: Fri Oct 25 13:32:07 2024 +0200 CAMEL-21355: refine no tools called scenario - stop throwing an exception - set a header if no tool was called --- .../src/main/docs/langchain4j-tools-component.adoc | 7 ++ .../langchain4j/tools/LangChain4jTools.java | 3 +- .../tools/LangChain4jToolsProducer.java | 22 ++-- .../langchain4j/tools/LangChain4jTooNoToolsIT.java | 119 +++++++++++++++++++++ 4 files changed, 142 insertions(+), 9 deletions(-) diff --git a/components/camel-ai/camel-langchain4j-tools/src/main/docs/langchain4j-tools-component.adoc b/components/camel-ai/camel-langchain4j-tools/src/main/docs/langchain4j-tools-component.adoc index 27ba9551442..fa24ec5d308 100644 --- a/components/camel-ai/camel-langchain4j-tools/src/main/docs/langchain4j-tools-component.adoc +++ b/components/camel-ai/camel-langchain4j-tools/src/main/docs/langchain4j-tools-component.adoc @@ -185,3 +185,10 @@ Use the model in the Camel LangChain4j Chat Producer ==== To switch to another Large Language Model and its corresponding dependency, replace the `langchain4j-open-ai` dependency with the appropriate dependency for the desired model. Update the initialization parameters accordingly in the code snippet provided above. ==== + + +==== Handling no Tools Called + +In some circumstances, the LLM may decide not to call a tool. +This is a valid scenario that needs to be handled by application developers. +To do so, developers can get the `LangChain4jTools.NO_TOOLS_CALLED_HEADER` from the exchange. \ No newline at end of file diff --git a/components/camel-ai/camel-langchain4j-tools/src/main/java/org/apache/camel/component/langchain4j/tools/LangChain4jTools.java b/components/camel-ai/camel-langchain4j-tools/src/main/java/org/apache/camel/component/langchain4j/tools/LangChain4jTools.java index 2e546266ee1..3e0680b476a 100644 --- a/components/camel-ai/camel-langchain4j-tools/src/main/java/org/apache/camel/component/langchain4j/tools/LangChain4jTools.java +++ b/components/camel-ai/camel-langchain4j-tools/src/main/java/org/apache/camel/component/langchain4j/tools/LangChain4jTools.java @@ -16,9 +16,10 @@ */ package org.apache.camel.component.langchain4j.tools; -public class LangChain4jTools { +public final class LangChain4jTools { public static final String SCHEME = "langchain4j-tools"; + public static final String NO_TOOLS_CALLED_HEADER = "LangChain4jToolsNoToolsCalled"; private LangChain4jTools() { } diff --git a/components/camel-ai/camel-langchain4j-tools/src/main/java/org/apache/camel/component/langchain4j/tools/LangChain4jToolsProducer.java b/components/camel-ai/camel-langchain4j-tools/src/main/java/org/apache/camel/component/langchain4j/tools/LangChain4jToolsProducer.java index 55052fd295b..f74279641f8 100644 --- a/components/camel-ai/camel-langchain4j-tools/src/main/java/org/apache/camel/component/langchain4j/tools/LangChain4jToolsProducer.java +++ b/components/camel-ai/camel-langchain4j-tools/src/main/java/org/apache/camel/component/langchain4j/tools/LangChain4jToolsProducer.java @@ -32,7 +32,6 @@ import dev.langchain4j.model.chat.ChatLanguageModel; import dev.langchain4j.model.output.Response; import org.apache.camel.Exchange; import org.apache.camel.InvalidPayloadException; -import org.apache.camel.RuntimeCamelException; import org.apache.camel.component.langchain4j.tools.spec.CamelToolExecutorCache; import org.apache.camel.component.langchain4j.tools.spec.CamelToolSpecification; import org.apache.camel.support.DefaultProducer; @@ -59,7 +58,9 @@ public class LangChain4jToolsProducer extends DefaultProducer { @SuppressWarnings("unchecked") private void processMultipleMessages(Exchange exchange) throws InvalidPayloadException { List<ChatMessage> messages = exchange.getIn().getMandatoryBody(List.class); - populateResponse(toolsChat(messages, exchange), exchange); + + final String response = toolsChat(messages, exchange); + populateResponse(response, exchange); } @Override @@ -92,10 +93,13 @@ public class LangChain4jToolsProducer extends DefaultProducer { private String toolsChat(List<ChatMessage> chatMessages, Exchange exchange) { final CamelToolExecutorCache toolCache = CamelToolExecutorCache.getInstance(); - final ToolPair toolPair = computeCandidates(toolCache); + final ToolPair toolPair = computeCandidates(toolCache, exchange); + if (toolPair == null) { + return null; + } // First talk to the model to get the tools to be called - final Response<AiMessage> response = chatWithLLMForTools(chatMessages, toolPair); + final Response<AiMessage> response = chatWithLLMForTools(chatMessages, toolPair, exchange); // Then, talk again to call the tools and compute the final response return chatWithLLMForToolCalling(chatMessages, exchange, response, toolPair); @@ -141,11 +145,12 @@ public class LangChain4jToolsProducer extends DefaultProducer { * @param toolPair the toolPair containing the available tools to be called * @return the response provided by the model */ - private Response<AiMessage> chatWithLLMForTools(List<ChatMessage> chatMessages, ToolPair toolPair) { + private Response<AiMessage> chatWithLLMForTools(List<ChatMessage> chatMessages, ToolPair toolPair, Exchange exchange) { Response<AiMessage> response = this.chatLanguageModel.generate(chatMessages, toolPair.toolSpecifications()); if (!response.content().hasToolExecutionRequests()) { - throw new RuntimeCamelException("There are no tools to be executed"); + exchange.getMessage().setHeader(LangChain4jTools.NO_TOOLS_CALLED_HEADER, Boolean.TRUE); + return null; } chatMessages.add(response.content()); @@ -159,7 +164,7 @@ public class LangChain4jToolsProducer extends DefaultProducer { * @return It returns a record containing both the specification, and the {@link CamelToolSpecification} * that can be used to call the endpoints. */ - private ToolPair computeCandidates(CamelToolExecutorCache toolCache) { + private ToolPair computeCandidates(CamelToolExecutorCache toolCache, Exchange exchange) { final List<ToolSpecification> toolSpecifications = new ArrayList<>(); final List<CamelToolSpecification> callableTools = new ArrayList<>(); @@ -181,7 +186,8 @@ public class LangChain4jToolsProducer extends DefaultProducer { } if (toolSpecifications.isEmpty()) { - throw new RuntimeCamelException("No tools matching the tags provided by the producer were found"); + exchange.getMessage().setHeader(LangChain4jTools.NO_TOOLS_CALLED_HEADER, Boolean.TRUE); + return null; } return new ToolPair(toolSpecifications, callableTools); diff --git a/components/camel-ai/camel-langchain4j-tools/src/test/java/org/apache/camel/component/langchain4j/tools/LangChain4jTooNoToolsIT.java b/components/camel-ai/camel-langchain4j-tools/src/test/java/org/apache/camel/component/langchain4j/tools/LangChain4jTooNoToolsIT.java new file mode 100644 index 00000000000..805575baf94 --- /dev/null +++ b/components/camel-ai/camel-langchain4j-tools/src/test/java/org/apache/camel/component/langchain4j/tools/LangChain4jTooNoToolsIT.java @@ -0,0 +1,119 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.langchain4j.tools; + +import java.util.ArrayList; +import java.util.List; + +import dev.langchain4j.data.message.ChatMessage; +import dev.langchain4j.data.message.SystemMessage; +import dev.langchain4j.data.message.UserMessage; +import dev.langchain4j.model.chat.ChatLanguageModel; +import dev.langchain4j.model.openai.OpenAiChatModel; +import org.apache.camel.CamelContext; +import org.apache.camel.Exchange; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.test.infra.ollama.services.OllamaService; +import org.apache.camel.test.infra.ollama.services.OllamaServiceFactory; +import org.apache.camel.test.junit5.CamelTestSupport; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.condition.DisabledIfSystemProperty; +import org.junit.jupiter.api.extension.RegisterExtension; + +import static java.time.Duration.ofSeconds; + +@DisabledIfSystemProperty(named = "ci.env.name", matches = ".*", disabledReason = "Requires too much network resources") +public class LangChain4jTooNoToolsIT extends CamelTestSupport { + + public static final String MODEL_NAME = "llama3.1:latest"; + private ChatLanguageModel chatLanguageModel; + + @RegisterExtension + static OllamaService OLLAMA = OllamaServiceFactory.createServiceWithConfiguration(() -> MODEL_NAME); + + @Override + protected void setupResources() throws Exception { + super.setupResources(); + + chatLanguageModel = createModel(); + } + + @Override + protected CamelContext createCamelContext() throws Exception { + CamelContext context = super.createCamelContext(); + + LangChain4jToolsComponent component + = context.getComponent(LangChain4jTools.SCHEME, LangChain4jToolsComponent.class); + + component.getConfiguration().setChatModel(chatLanguageModel); + + return context; + } + + protected ChatLanguageModel createModel() { + chatLanguageModel = OpenAiChatModel.builder() + .apiKey("NO_API_KEY") + .modelName(MODEL_NAME) + .baseUrl(OLLAMA.getEndpoint()) + .temperature(Double.valueOf(0.0)) + .timeout(ofSeconds(60)) + .logRequests(Boolean.TRUE) + .logResponses(Boolean.TRUE) + .build(); + + return chatLanguageModel; + } + + @Override + protected RouteBuilder createRouteBuilder() { + return new RouteBuilder() { + public void configure() { + from("direct:test") + .to("langchain4j-tools:test1?tags=somethingElse") + .log("response is: ${body}"); + + from("langchain4j-tools:test1?tags=user&description=Query user database by number¶meter.number=integer") + .setBody(simple("{\"name\": \"pippo\"}")); + + from("direct:noResponse") + .log("there is no tool to be called for the request: ${body}") + .setBody(constant("There was no tool to be called")) + .to("mock:noResponse"); + + } + }; + } + + @RepeatedTest(1) + public void testSimpleInvocation() throws InterruptedException { + List<ChatMessage> messages = new ArrayList<>(); + messages.add(new SystemMessage( + """ + You provide the requested information using the functions you hava available. You can invoke the functions to obtain the information you need to complete the answer. + If no tool matches the input, then don't invoke any of them. + """)); + messages.add(new UserMessage(""" + What time is the breakfast tomorrow? + """)); + + Exchange message = fluentTemplate.to("direct:test").withBody(messages).request(Exchange.class); + + Assertions.assertThat(message).isNotNull(); + Assertions.assertThat(message.getMessage().getHeader(LangChain4jTools.NO_TOOLS_CALLED_HEADER)).isEqualTo(Boolean.TRUE); + } +}