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&parameter.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);
+    }
+}

Reply via email to