Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -119,6 +120,8 @@ private String toolsChat(List<ChatMessage> 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 {
Expand All @@ -129,7 +132,7 @@ private String toolsChat(List<ChatMessage> 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);
Expand All @@ -153,7 +156,8 @@ private boolean isDoneExecuting(Response<AiMessage> response) {
}

private void invokeTools(
List<ChatMessage> chatMessages, Exchange exchange, Response<AiMessage> response, ToolPair toolPair) {
List<ChatMessage> chatMessages, Exchange exchange, Response<AiMessage> response, ToolPair toolPair,
Exchange baseline) {
int i = 0;
List<ToolExecutionRequest> toolExecutionRequests = response.content().toolExecutionRequests();
for (ToolExecutionRequest toolExecutionRequest : toolExecutionRequests) {
Expand All @@ -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();

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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("""
Expand Down Expand Up @@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading