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 395f96cfce29d..f49ac949559c7 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 @@ -46,6 +46,7 @@ import org.apache.camel.component.langchain4j.tools.spec.CamelToolExecutorCache; import org.apache.camel.component.langchain4j.tools.spec.CamelToolSpecification; import org.apache.camel.support.DefaultProducer; +import org.apache.camel.support.ExchangeHelper; import org.apache.camel.util.ObjectHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -119,6 +120,8 @@ private String toolsChat(List chatMessages, Exchange exchange) { return null; } + final Exchange baseline = ExchangeHelper.createCopy(exchange, true); + // First talk to the model to get the tools to be called int i = 0; do { @@ -129,7 +132,7 @@ private String toolsChat(List chatMessages, Exchange exchange) { } // Only invoke the tools ... the response will be computed on the next loop - invokeTools(chatMessages, exchange, response, toolPair); + invokeTools(chatMessages, exchange, response, toolPair, baseline); LOG.debug("Finished iteration {}", i); i++; } while (true); @@ -153,7 +156,8 @@ private boolean isDoneExecuting(Response response) { } private void invokeTools( - List chatMessages, Exchange exchange, Response response, ToolPair toolPair) { + List chatMessages, Exchange exchange, Response response, ToolPair toolPair, + Exchange baseline) { int i = 0; List toolExecutionRequests = response.content().toolExecutionRequests(); for (ToolExecutionRequest toolExecutionRequest : toolExecutionRequests) { @@ -167,13 +171,11 @@ private void invokeTools( 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(); + final Exchange toolExchange = ExchangeHelper.createCopy(baseline, true); + try { TypeConverter typeConverter = endpoint.getCamelContext().getTypeConverter(); @@ -213,22 +215,24 @@ private void invokeTools( headerValue = value; } - exchange.getMessage().setHeader(name, headerValue); + toolExchange.getMessage().setHeader(name, headerValue); }); // Execute the consumer route - camelToolSpecification.getConsumer().getProcessor().process(exchange); + camelToolSpecification.getConsumer().getProcessor().process(toolExchange); i++; } catch (Exception e) { // How to handle this exception? - exchange.setException(e); + toolExchange.setException(e); } + ExchangeHelper.copyResults(exchange, toolExchange); + chatMessages.add(new ToolExecutionResultMessage( toolExecutionRequest.id(), toolExecutionRequest.name(), - exchange.getIn().getBody(String.class))); + toolExchange.getIn().getBody(String.class))); } // Clear route stop flag after all tools so it does not leak diff --git a/components/camel-ai/camel-langchain4j-tools/src/test/java/org/apache/camel/component/langchain4j/tools/LangChain4jToolMultipleCallsTest.java b/components/camel-ai/camel-langchain4j-tools/src/test/java/org/apache/camel/component/langchain4j/tools/LangChain4jToolMultipleCallsTest.java index 39a60358a9be0..9c277d3b8c641 100644 --- a/components/camel-ai/camel-langchain4j-tools/src/test/java/org/apache/camel/component/langchain4j/tools/LangChain4jToolMultipleCallsTest.java +++ b/components/camel-ai/camel-langchain4j-tools/src/test/java/org/apache/camel/component/langchain4j/tools/LangChain4jToolMultipleCallsTest.java @@ -47,7 +47,7 @@ public class LangChain4jToolMultipleCallsTest extends CamelTestSupport { .build(); private volatile boolean intermediateCalled = false; - private volatile boolean intermediateHasValidBody = false; + private volatile boolean intermediateIsolated = false; @Override protected void setupResources() throws Exception { @@ -82,10 +82,15 @@ public void configure() { .process(exchange -> { String body = exchange.getIn().getBody(String.class); intermediateCalled = true; - if (exchange.getIn().getHeader("longitude", String.class).contains("0") && - exchange.getIn().getHeader("latitude", String.class).contains("51") && - body.contains("51.50758961965397") && body.contains("-0.13388057363742217")) { - intermediateHasValidBody = true; + // CAMEL-23704: the forecast tool must see its own argument headers but not + // inherit the previous tool's 'name' header or output body (it receives the + // original incoming chat messages) + boolean ownHeaders = exchange.getIn().getHeader("longitude", String.class).contains("0") + && exchange.getIn().getHeader("latitude", String.class).contains("51"); + boolean noHeaderLeak = exchange.getIn().getHeader("name") == null; + boolean originalBody = body != null && body.contains("meteorologist"); + if (ownHeaders && noHeaderLeak && originalBody) { + intermediateIsolated = true; } }) .setBody(simple(""" @@ -135,6 +140,6 @@ public void testSimpleInvocation() { // depending on the reasoning model used to test, the result is different, but we asked for Celcius degree and 3 should be part of it Assertions.assertThat(responseContent).containsIgnoringCase("3"); Assertions.assertThat(intermediateCalled).isTrue(); - Assertions.assertThat(intermediateHasValidBody).isTrue(); + Assertions.assertThat(intermediateIsolated).isTrue(); } } diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc index 77d793d1cbc2d..cbba0ce981c01 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc @@ -34,6 +34,18 @@ endpoint URI, which could leave the data format incompletely configured. This al `marshal()` / `unmarshal()` DSL, which already honors the global configuration. Options specified on the endpoint URI continue to take precedence over the global configuration. +=== camel-langchain4j-tools + +When the LLM requests multiple tool invocations within a single request, each tool is now invoked on its +own independent copy of the exchange (similar to the multicast/splitter patterns) instead of sharing a single +exchange across all tools. This guarantees complete isolation: the message body, argument headers, and any +exception produced by one tool no longer leak into subsequent tool invocations of the same request. + +As a consequence, a tool route no longer receives the previous tool's output as its message body, nor the +previous tool's argument headers. Each tool route starts from the original incoming exchange and only sees +its own declared arguments as headers. Routes that (intentionally or accidentally) relied on this leaked state +between tools must be adjusted to carry such data explicitly. + === camel-core ==== Simple language: internal builder classes reorganized