This is an automated email from the ASF dual-hosted git repository. gnodet pushed a commit to branch fix-camel-23943-camel-langchain4j-tools-has-three in repository https://gitbox.apache.org/repos/asf/camel.git
commit 4a423267c4fa951902cda667f9f57fb8e07f28a9 Author: Guillaume Nodet <[email protected]> AuthorDate: Tue Jul 21 17:22:49 2026 +0000 CAMEL-23943: Regenerate missing files and improve test reproducer - Regenerated catalog JSON and endpoint DSL factory for the new maxToolCallingRoundTrips option (fixes CI "uncommitted changes" check) - Improved defect 1 test to use a custom mock that always returns tool calls, creating a true infinite loop scenario. Added @Timeout safety net so the test fails cleanly (timeout) if the fix is reverted, instead of hanging forever. - Verified all three tests fail before the fix and pass after: - Test 1: times out (infinite loop) → RuntimeCamelException (fix works) - Test 2: NoSuchElementException → graceful recovery - Test 3: exception on exchange → error sent to LLM Co-Authored-By: Claude Opus 4.6 <[email protected]> --- .../catalog/components/langchain4j-tools.json | 5 +-- .../tools/LangChain4jToolDefectsTest.java | 41 ++++++++++++++-------- .../LangChain4jToolsEndpointBuilderFactory.java | 38 ++++++++++++++++++++ 3 files changed, 67 insertions(+), 17 deletions(-) diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/langchain4j-tools.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/langchain4j-tools.json index 0477acdec1cc..1197e5ae486a 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/langchain4j-tools.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/langchain4j-tools.json @@ -47,7 +47,8 @@ "camelToolParameter": { "index": 7, "kind": "parameter", "displayName": "Camel Tool Parameter", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "object", "javaType": "org.apache.camel.component.langchain4j.tools.spec.CamelSimpleToolParameter", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tool's Camel Parameters, programmatically define Tool description and parameters" }, "exceptionHandler": { "index": 8, "kind": "parameter", "displayName": "Exception Handler", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "object", "javaType": "org.apache.camel.spi.ExceptionHandler", "optionalPrefix": "consumer.", "deprecated": false, "autowired": false, "secret": false, "description": "To let the consumer use a custom ExceptionHandler. Notice if the option bridgeErrorHandler is enabled then this option is not in use. By def [...] "exchangePattern": { "index": 9, "kind": "parameter", "displayName": "Exchange Pattern", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "enum", "javaType": "org.apache.camel.ExchangePattern", "enum": [ "InOnly", "InOut" ], "deprecated": false, "autowired": false, "secret": false, "description": "Sets the exchange pattern when the consumer creates an exchange." }, - "lazyStartProducer": { "index": 10, "kind": "parameter", "displayName": "Lazy Start Producer", "group": "producer (advanced)", "label": "producer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a produ [...] - "chatModel": { "index": 11, "kind": "parameter", "displayName": "Chat Model", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "dev.langchain4j.model.chat.ChatModel", "deprecated": false, "deprecationNote": "", "autowired": true, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.tools.LangChain4jToolsConfiguration", "configurationField": "configuration", "description": "Chat Model of type dev.langchain4j.model.cha [...] + "maxToolCallingRoundTrips": { "index": 10, "kind": "parameter", "displayName": "Max Tool Calling Round Trips", "group": "producer", "label": "producer", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 10, "description": "Maximum number of tool-calling round trips (iterations) allowed before stopping. This prevents infinite loops when the LLM keeps requesting tool calls indefinite [...] + "lazyStartProducer": { "index": 11, "kind": "parameter", "displayName": "Lazy Start Producer", "group": "producer (advanced)", "label": "producer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a produ [...] + "chatModel": { "index": 12, "kind": "parameter", "displayName": "Chat Model", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "dev.langchain4j.model.chat.ChatModel", "deprecated": false, "deprecationNote": "", "autowired": true, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.tools.LangChain4jToolsConfiguration", "configurationField": "configuration", "description": "Chat Model of type dev.langchain4j.model.cha [...] } } diff --git a/components/camel-ai/camel-langchain4j-tools/src/test/java/org/apache/camel/component/langchain4j/tools/LangChain4jToolDefectsTest.java b/components/camel-ai/camel-langchain4j-tools/src/test/java/org/apache/camel/component/langchain4j/tools/LangChain4jToolDefectsTest.java index 505bf9df7579..3efec31a7c8f 100644 --- a/components/camel-ai/camel-langchain4j-tools/src/test/java/org/apache/camel/component/langchain4j/tools/LangChain4jToolDefectsTest.java +++ b/components/camel-ai/camel-langchain4j-tools/src/test/java/org/apache/camel/component/langchain4j/tools/LangChain4jToolDefectsTest.java @@ -18,6 +18,7 @@ package org.apache.camel.component.langchain4j.tools; import java.util.ArrayList; import java.util.List; +import java.util.UUID; import dev.langchain4j.data.message.ChatMessage; import dev.langchain4j.data.message.SystemMessage; @@ -30,6 +31,7 @@ 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.Timeout; import org.junit.jupiter.api.extension.RegisterExtension; import static org.assertj.core.api.Assertions.assertThat; @@ -48,18 +50,11 @@ class LangChain4jToolDefectsTest extends CamelTestSupport { @RegisterExtension static OpenAIMock openAIMock = new OpenAIMock().builder() - // Scenario 1: LLM keeps requesting tools — 5 sequential round trips - .when("Do something that requires many round trips\n") - .invokeTool("RealTool") - .withParam("input", "round1") - .andThenInvokeTool("RealTool") - .withParam("input", "round2") - .andThenInvokeTool("RealTool") - .withParam("input", "round3") - .andThenInvokeTool("RealTool") - .withParam("input", "round4") - .andThenInvokeTool("RealTool") - .withParam("input", "round5") + // Scenario 1: LLM always requests tools — never stops on its own. + // Uses a custom response function that returns a tool call on every request, + // simulating an LLM stuck in an infinite tool-calling loop. + .when("Infinite tool loop\n") + .thenRespondWith((exchange, input) -> createAlwaysToolCallResponse()) .end() // Scenario 2: LLM hallucinates a tool name that doesn't exist .when("Call a tool that does not exist\n") @@ -72,6 +67,20 @@ class LangChain4jToolDefectsTest extends CamelTestSupport { .withParam("input", "test") .build(); + /** + * Creates a mock OpenAI response that always requests a tool call. Each invocation returns unique IDs to simulate a + * fresh tool call request from the LLM. + */ + private static String createAlwaysToolCallResponse() { + return "{\"id\":\"" + UUID.randomUUID() + "\"," + + "\"choices\":[{\"finish_reason\":\"tool_calls\",\"index\":0," + + "\"message\":{\"role\":\"assistant\",\"refusal\":null,\"content\":null," + + "\"tool_calls\":[{\"id\":\"" + UUID.randomUUID() + "\",\"type\":\"function\"," + + "\"function\":{\"name\":\"RealTool\"," + + "\"arguments\":\"{\\\"input\\\":\\\"loop\\\"}\"}}]}}]," + + "\"created\":1234567890,\"model\":\"openai-mock\",\"object\":\"chat.completion\"}"; + } + @Override protected void setupResources() throws Exception { super.setupResources(); @@ -119,14 +128,16 @@ class LangChain4jToolDefectsTest extends CamelTestSupport { * CAMEL-23943 defect 1: The do/while(true) loop in toolsChat has no iteration cap. If the LLM keeps requesting * tools indefinitely, this never terminates. * - * With the fix, the loop is bounded by maxToolCallingRoundTrips (default 10, set to 2 for this test). Exceeding the - * cap throws a RuntimeCamelException. + * This test uses a mock that always returns tool calls (never a final text response), simulating an LLM stuck in an + * infinite loop. Without the fix, this test would hang indefinitely (caught by the @Timeout annotation). With the + * fix, the loop is bounded by maxToolCallingRoundTrips and throws a RuntimeCamelException. */ @Test + @Timeout(10) // Safety net: without the fix, the infinite loop would hang forever void testMaxToolCallingRoundTripsExceeded() { List<ChatMessage> messages = new ArrayList<>(); messages.add(new SystemMessage("You are a helpful assistant.")); - messages.add(new UserMessage("Do something that requires many round trips\n")); + messages.add(new UserMessage("Infinite tool loop\n")); Exchange exchange = fluentTemplate.to("direct:testMaxRoundTrips") .withBody(messages).request(Exchange.class); diff --git a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/LangChain4jToolsEndpointBuilderFactory.java b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/LangChain4jToolsEndpointBuilderFactory.java index f53dd7e54b32..84c3900047f5 100644 --- a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/LangChain4jToolsEndpointBuilderFactory.java +++ b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/LangChain4jToolsEndpointBuilderFactory.java @@ -386,6 +386,44 @@ public interface LangChain4jToolsEndpointBuilderFactory { doSetProperty("tags", tags); return this; } + /** + * Maximum number of tool-calling round trips (iterations) allowed + * before stopping. This prevents infinite loops when the LLM keeps + * requesting tool calls indefinitely. Each round trip consists of one + * LLM call and the execution of all tools requested in that call. Set + * to 0 for unlimited (not recommended). + * + * The option is a: <code>int</code> type. + * + * Default: 10 + * Group: producer + * + * @param maxToolCallingRoundTrips the value to set + * @return the dsl builder + */ + default LangChain4jToolsEndpointProducerBuilder maxToolCallingRoundTrips(int maxToolCallingRoundTrips) { + doSetProperty("maxToolCallingRoundTrips", maxToolCallingRoundTrips); + return this; + } + /** + * Maximum number of tool-calling round trips (iterations) allowed + * before stopping. This prevents infinite loops when the LLM keeps + * requesting tool calls indefinitely. Each round trip consists of one + * LLM call and the execution of all tools requested in that call. Set + * to 0 for unlimited (not recommended). + * + * The option will be converted to a <code>int</code> type. + * + * Default: 10 + * Group: producer + * + * @param maxToolCallingRoundTrips the value to set + * @return the dsl builder + */ + default LangChain4jToolsEndpointProducerBuilder maxToolCallingRoundTrips(String maxToolCallingRoundTrips) { + doSetProperty("maxToolCallingRoundTrips", maxToolCallingRoundTrips); + return this; + } } /**
