This is an automated email from the ASF dual-hosted git repository.
orpiske pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new f74069b65d2 CAMEL-21355: refine no tools called scenario
f74069b65d2 is described below
commit f74069b65d2dc01b532ed691a8e798ad96117531
Author: Otavio Rodolfo Piske <[email protected]>
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);
+ }
+}