This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch fix/CAMEL-21937 in repository https://gitbox.apache.org/repos/asf/camel.git
commit 93c0fa80621c7a67fcc10416dae9c4e3f138d473 Author: Claus Ibsen <[email protected]> AuthorDate: Sat Jun 6 12:53:06 2026 +0200 CAMEL-21937: Reset route stop flag between tool invocations When the LLM invokes multiple tools in a single response, the stop() EIP in one tool route would set the ROUTE_STOP flag on the shared exchange, preventing subsequent tool routes from executing and returning incorrect results. Co-Authored-By: Claude <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../tools/LangChain4jToolsProducer.java | 4 + .../tools/LangChain4jToolStopEipTest.java | 125 +++++++++++++++++++++ 2 files changed, 129 insertions(+) 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 994df32272ab..6da0fb135097 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 @@ -167,6 +167,10 @@ public class LangChain4jToolsProducer extends DefaultProducer { continue; } + // Reset route stop flag from previous tool invocation to prevent + // stop() EIP in one tool from short-circuiting subsequent tools + exchange.setRouteStop(false); + final CamelToolSpecification camelToolSpecification = toolPair.callableTools().stream() .filter(c -> c.getToolSpecification().name().equals(toolName)).findFirst().get(); diff --git a/components/camel-ai/camel-langchain4j-tools/src/test/java/org/apache/camel/component/langchain4j/tools/LangChain4jToolStopEipTest.java b/components/camel-ai/camel-langchain4j-tools/src/test/java/org/apache/camel/component/langchain4j/tools/LangChain4jToolStopEipTest.java new file mode 100644 index 000000000000..91d46a63d03b --- /dev/null +++ b/components/camel-ai/camel-langchain4j-tools/src/test/java/org/apache/camel/component/langchain4j/tools/LangChain4jToolStopEipTest.java @@ -0,0 +1,125 @@ +/* + * 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.ChatModel; +import org.apache.camel.CamelContext; +import org.apache.camel.Exchange; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.test.infra.openai.mock.OpenAIMock; +import org.apache.camel.test.junit6.CamelTestSupport; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests that using the stop() EIP in a tool route does not break parallel tool invocations. + * + * When the LLM requests multiple tools in a single response, each tool route must execute independently. Previously, + * using stop() in one tool route would cause the ROUTE_STOP flag to leak into subsequent tool invocations, preventing + * them from executing and returning the wrong result. + */ +public class LangChain4jToolStopEipTest extends CamelTestSupport { + + protected ChatModel chatModel; + + @RegisterExtension + static OpenAIMock openAIMock = new OpenAIMock().builder() + .when("please amend 123, and get me the promotions\n") + .invokeTool("AmendAnOrderByItsID") + .withParam("orderId", "123") + .andInvokeTool("GetCurrentPromotions") + .build(); + + private volatile boolean amendOrderCalled = false; + private volatile boolean getPromotionsCalled = false; + private volatile String amendOrderBody = null; + private volatile String getPromotionsBody = null; + + @Override + protected void setupResources() throws Exception { + super.setupResources(); + chatModel = ToolsHelper.createModel(openAIMock.getBaseUrl()); + } + + @Override + protected CamelContext createCamelContext() throws Exception { + CamelContext context = super.createCamelContext(); + LangChain4jToolsComponent component + = context.getComponent(LangChain4jTools.SCHEME, LangChain4jToolsComponent.class); + component.getConfiguration().setChatModel(chatModel); + return context; + } + + @Override + protected RouteBuilder createRouteBuilder() { + return new RouteBuilder() { + public void configure() { + from("direct:test") + .to("langchain4j-tools:test1?tags=orders"); + + // Tool1: uses stop() mid-way — must not leak ROUTE_STOP to Tool2 + from("langchain4j-tools:test1?tags=orders&description=Amend an order by its ID¶meter.orderId=string") + .process(exchange -> { + amendOrderCalled = true; + }) + .setBody(constant("order amended")) + .stop(); + + // Tool2: returns JSON content — must execute independently despite Tool1's stop() + from("langchain4j-tools:test1?tags=orders&description=Get current promotions") + .setBody(constant("{\"status\":\"ok\",\"promotions\":[\"10% off\"]}")) + .process(exchange -> { + getPromotionsCalled = true; + getPromotionsBody = exchange.getIn().getBody(String.class); + }); + } + }; + } + + @Test + public void testParallelToolsWithStopEip() { + List<ChatMessage> messages = new ArrayList<>(); + messages.add(new SystemMessage("You are an assistant that helps with orders and promotions.")); + messages.add(new UserMessage("please amend 123, and get me the promotions\n")); + + Exchange result = fluentTemplate.to("direct:test").withBody(messages).request(Exchange.class); + + assertThat(result).isNotNull(); + + // Both tool routes must have been called + assertThat(amendOrderCalled).as("AmendOrder tool should have been called").isTrue(); + assertThat(getPromotionsCalled).as("GetPromotions tool should have been called").isTrue(); + + // GetPromotions must return its own body, not AmendOrder's body + assertThat(getPromotionsBody).as("GetPromotions should return its own body, not AmendOrder's") + .contains("promotions") + .doesNotContain("order amended"); + + // The final response should contain the promotions data + String responseContent = result.getMessage().getBody(String.class); + assertThat(responseContent).isNotNull(); + assertThat(responseContent).contains("promotions"); + } +}
