Skip to content

Commit e540cc2

Browse files
Add tests and support for langchain4j-agent MCP tools
1 parent 05325a0 commit e540cc2

File tree

13 files changed

+235
-4
lines changed

13 files changed

+235
-4
lines changed

.github/reclaim-disk-space.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ sudo rm -rf /opt/ghc \
2929
rm -rf /usr/local/.ghcup \
3030
rm -rf /usr/local/go \
3131
rm -rf /usr/local/lib/android \
32-
rm -rf /usr/local/lib/node_modules \
32+
rm -rf /usr/local/lib/node_modules/parcel \
3333
rm -rf /usr/local/share/boost \
3434
rm -rf /usr/local/share/powershell \
3535
rm -rf /usr/share/dotnet \

extensions/langchain4j-agent/deployment/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/deployment/Langchain4jAgentProcessor.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,16 @@
1616
*/
1717
package org.apache.camel.quarkus.component.langchain4j.agent.deployment;
1818

19+
import java.util.Set;
20+
import java.util.stream.Collectors;
21+
1922
import io.quarkus.deployment.annotations.BuildStep;
23+
import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
2024
import io.quarkus.deployment.builditem.FeatureBuildItem;
25+
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
26+
import io.quarkus.deployment.pkg.steps.NativeOrNativeSourcesBuild;
27+
import org.jboss.jandex.ClassInfo;
28+
import org.jboss.jandex.DotName;
2129

2230
class Langchain4jAgentProcessor {
2331
private static final String FEATURE = "camel-langchain4j-agent";
@@ -26,4 +34,21 @@ class Langchain4jAgentProcessor {
2634
FeatureBuildItem feature() {
2735
return new FeatureBuildItem(FEATURE);
2836
}
37+
38+
@BuildStep(onlyIf = NativeOrNativeSourcesBuild.class)
39+
ReflectiveClassBuildItem registerForReflection(CombinedIndexBuildItem combinedIndex) {
40+
Set<String> mcpProtocolClasses = combinedIndex.getIndex()
41+
.getClassesInPackage("dev.langchain4j.mcp.client.protocol")
42+
.stream()
43+
.map(ClassInfo::asClass)
44+
.map(ClassInfo::name)
45+
.map(DotName::toString)
46+
.collect(Collectors.toSet());
47+
48+
return ReflectiveClassBuildItem
49+
.builder(mcpProtocolClasses.toArray(new String[0]))
50+
.fields(true)
51+
.methods(true)
52+
.build();
53+
}
2954
}

integration-tests/langchain4j-agent/README.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ By default, the Langchain4j-agent integration tests use WireMock to stub Ollama
44

55
To run the `camel-quarkus-langchain4j-agent` integration tests against the real API, you need a Ollama instance running with the `orca-mini` & `granite4:3b` models downloaded.
66

7+
The MCP client tests require https://nodejs.org/[Node.js] to be installed on the test host.
8+
79
When Ollama is running, set the following environment variables:
810

911
[source,shell]

integration-tests/langchain4j-agent/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/AgentProducers.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
import dev.langchain4j.data.document.splitter.DocumentSplitters;
2828
import dev.langchain4j.data.embedding.Embedding;
2929
import dev.langchain4j.data.segment.TextSegment;
30+
import dev.langchain4j.mcp.client.DefaultMcpClient;
31+
import dev.langchain4j.mcp.client.transport.stdio.StdioMcpTransport;
3032
import dev.langchain4j.memory.chat.ChatMemoryProvider;
3133
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
3234
import dev.langchain4j.model.chat.ChatModel;
@@ -54,6 +56,7 @@
5456
import org.apache.camel.quarkus.component.langchain4j.agent.it.service.TestPojoAiAgent;
5557
import org.apache.camel.quarkus.component.langchain4j.agent.it.tool.AdditionTool;
5658
import org.apache.camel.quarkus.component.langchain4j.agent.it.util.PersistentChatMemoryStore;
59+
import org.apache.camel.quarkus.component.langchain4j.agent.it.util.ProcessUtils;
5760
import org.eclipse.microprofile.config.inject.ConfigProperty;
5861

5962
import static java.time.Duration.ofSeconds;
@@ -63,6 +66,9 @@ public class AgentProducers {
6366
@ConfigProperty(name = "langchain4j.ollama.base-url")
6467
String baseUrl;
6568

69+
@ConfigProperty(name = "nodejs.installed")
70+
boolean isNodeJSInstaled;
71+
6672
@Produces
6773
@Identifier("ollamaOrcaMiniModel")
6874
ChatModel ollamaOrcaMiniModel() {
@@ -221,4 +227,25 @@ Agent agentWithCustomTools(@Identifier("granite4Model") ChatModel chatModel) {
221227
.withChatModel(chatModel)
222228
.withCustomTools(List.of(new AdditionTool())));
223229
}
230+
231+
@Produces
232+
@Identifier("agentWithMcpClient")
233+
Agent agentWithMcpClient(@Identifier("granite4Model") ChatModel chatModel) {
234+
if (isNodeJSInstaled) {
235+
return new AgentWithoutMemory(new AgentConfiguration()
236+
.withChatModel(chatModel)
237+
.withMcpClient(new DefaultMcpClient.Builder()
238+
.transport(new StdioMcpTransport.Builder()
239+
.command(List.of(ProcessUtils.getNpxExecutable(), "-y",
240+
"@modelcontextprotocol/server-everything"))
241+
.logEvents(true)
242+
.build())
243+
.build())
244+
.withMcpToolProviderFilter((mcpClient, toolSpecification) -> {
245+
String toolName = toolSpecification.name().toLowerCase();
246+
return toolName.contains("add") || toolName.contains("echo") || toolName.contains("long");
247+
}));
248+
}
249+
return null;
250+
}
224251
}

integration-tests/langchain4j-agent/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentResource.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import jakarta.ws.rs.Path;
2727
import jakarta.ws.rs.Produces;
2828
import jakarta.ws.rs.QueryParam;
29+
import jakarta.ws.rs.core.HttpHeaders;
2930
import jakarta.ws.rs.core.MediaType;
3031
import jakarta.ws.rs.core.Response;
3132
import org.apache.camel.Exchange;
@@ -225,4 +226,20 @@ public Response chatWithCustomTools(String userMessage) {
225226
AdditionTool.reset();
226227
}
227228
}
229+
230+
@Path("/mcp/client")
231+
@POST
232+
@Consumes(MediaType.TEXT_PLAIN)
233+
@Produces(value = { MediaType.TEXT_PLAIN, MediaType.APPLICATION_JSON })
234+
public Response chatWithMcpClient(String userMessage) {
235+
String result = producerTemplate.to("direct:agent-with-mcp-client")
236+
.withBody(userMessage)
237+
.request(String.class);
238+
239+
String contentType = userMessage.contains("JSON") ? MediaType.APPLICATION_JSON : MediaType.TEXT_PLAIN;
240+
241+
return Response.ok(result.trim())
242+
.header(HttpHeaders.CONTENT_TYPE, contentType)
243+
.build();
244+
}
228245
}

integration-tests/langchain4j-agent/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentRoutes.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,17 @@
1616
*/
1717
package org.apache.camel.quarkus.component.langchain4j.agent.it;
1818

19+
import jakarta.enterprise.context.ApplicationScoped;
1920
import org.apache.camel.builder.RouteBuilder;
21+
import org.eclipse.microprofile.config.inject.ConfigProperty;
2022

23+
@ApplicationScoped
2124
public class Langchain4jAgentRoutes extends RouteBuilder {
2225
public static final String USER_JOHN = "John Doe";
2326

27+
@ConfigProperty(name = "nodejs.installed")
28+
boolean isNodeJSInstaled;
29+
2430
@Override
2531
public void configure() throws Exception {
2632
from("direct:simple-agent")
@@ -58,5 +64,10 @@ public void configure() throws Exception {
5864

5965
from("direct:agent-with-custom-tools")
6066
.to("langchain4j-agent:test-agent-custom-tools?agent=#agentWithCustomTools");
67+
68+
if (isNodeJSInstaled) {
69+
from("direct:agent-with-mcp-client")
70+
.to("langchain4j-agent:test-agent-with-mcp-client?agent=#agentWithMcpClient");
71+
}
6172
}
6273
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.camel.quarkus.component.langchain4j.agent.it.util;
18+
19+
import io.smallrye.common.os.OS;
20+
21+
public final class ProcessUtils {
22+
private ProcessUtils() {
23+
// Utility class
24+
}
25+
26+
public static String getNpxExecutable() {
27+
return OS.current().equals(OS.WINDOWS) ? "npx.cmd" : "npx";
28+
}
29+
}

integration-tests/langchain4j-agent/src/main/resources/application.properties

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@
1414
## See the License for the specific language governing permissions and
1515
## limitations under the License.
1616
## ---------------------------------------------------------------------------
17-
quarkus.native.resources.includes=rag/*
17+
quarkus.native.resources.includes=rag/*
18+
quarkus.http.test-timeout=120S

integration-tests/langchain4j-agent/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentTest.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,13 @@
2121
import io.restassured.RestAssured;
2222
import org.apache.camel.quarkus.component.langchain4j.agent.it.guardrail.ValidationFailureInputGuardrail;
2323
import org.apache.camel.quarkus.component.langchain4j.agent.it.guardrail.ValidationFailureOutputGuardrail;
24+
import org.eclipse.microprofile.config.ConfigProvider;
25+
import org.junit.jupiter.api.Assumptions;
2426
import org.junit.jupiter.api.Test;
2527
import org.junit.jupiter.api.extension.ExtendWith;
2628

2729
import static org.apache.camel.quarkus.component.langchain4j.agent.it.Langchain4jAgentRoutes.USER_JOHN;
30+
import static org.hamcrest.Matchers.containsInAnyOrder;
2831
import static org.hamcrest.Matchers.containsString;
2932
import static org.hamcrest.Matchers.containsStringIgnoringCase;
3033
import static org.hamcrest.Matchers.is;
@@ -33,7 +36,7 @@
3336
import static org.hamcrest.Matchers.startsWith;
3437

3538
@ExtendWith(Langchain4jTestWatcher.class)
36-
@QuarkusTestResource(OllamaTestResource.class)
39+
@QuarkusTestResource(Langchain4jAgentTestResource.class)
3740
@QuarkusTest
3841
class Langchain4jAgentTest {
3942
static final String TEST_USER_MESSAGE_SIMPLE = "What is Apache Camel?";
@@ -223,4 +226,25 @@ void agentWithCustomTools() {
223226
"result", containsStringIgnoringCase("15"),
224227
"toolWasInvoked", is(true));
225228
}
229+
230+
@Test
231+
void agentWithMcpClient() {
232+
boolean isNodeJSInstalled = ConfigProvider.getConfig().getValue("nodejs.installed", boolean.class);
233+
Assumptions.assumeTrue(isNodeJSInstalled, "Node.js is not installed");
234+
235+
RestAssured.given()
236+
.body("Please list your available tools. You MUST respond using ONLY valid JSON with tool names as an array. DO NOT add explanations. DO NOT add comments. DO NOT wrap in markdown.")
237+
.post("/langchain4j-agent/mcp/client")
238+
.then()
239+
.statusCode(200)
240+
.body(".", containsInAnyOrder("add", "echo", "longRunningOperation"));
241+
242+
RestAssured.given()
243+
.body("Use your available tools to perform a long running operation for 2 seconds with 2 steps. DO NOT use any markdown formatting in the response.")
244+
.post("/langchain4j-agent/mcp/client")
245+
.then()
246+
.statusCode(200)
247+
.body(containsStringIgnoringCase(
248+
"operation was executed successfully for a duration of 2 seconds divided into 2 steps"));
249+
}
226250
}
Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,14 @@
2222
import java.nio.file.Paths;
2323
import java.util.List;
2424
import java.util.Map;
25+
import java.util.concurrent.TimeUnit;
2526

2627
import com.github.tomakehurst.wiremock.stubbing.StubMapping;
28+
import org.apache.camel.quarkus.component.langchain4j.agent.it.util.ProcessUtils;
2729
import org.apache.camel.quarkus.test.wiremock.WireMockTestResourceLifecycleManager;
30+
import org.junit.jupiter.api.condition.OS;
2831

29-
public class OllamaTestResource extends WireMockTestResourceLifecycleManager {
32+
public class Langchain4jAgentTestResource extends WireMockTestResourceLifecycleManager {
3033
private static final String OLLAMA_ENV_URL = "LANGCHAIN4J_OLLAMA_BASE_URL";
3134

3235
@Override
@@ -35,6 +38,7 @@ public Map<String, String> start() {
3538
String wiremockUrl = properties.get("wiremock.url");
3639
String url = wiremockUrl != null ? wiremockUrl : getRecordTargetBaseUrl();
3740
properties.put("langchain4j.ollama.base-url", url);
41+
properties.put("nodejs.installed", isNodeJSInstallationExists().toString());
3842
return properties;
3943
}
4044

@@ -86,4 +90,23 @@ public void stop() {
8690
Langchain4jTestWatcher.reset();
8791
}
8892
}
93+
94+
private Boolean isNodeJSInstallationExists() {
95+
try {
96+
// TODO: Suppress MCP tests in GitHub Actions for windows - https://github.com/apache/camel-quarkus/issues/8007
97+
if (OS.current().equals(OS.WINDOWS) && System.getenv("CI") != null) {
98+
return false;
99+
}
100+
101+
Process process = new ProcessBuilder()
102+
.command(ProcessUtils.getNpxExecutable(), "--version")
103+
.start();
104+
105+
process.waitFor(10, TimeUnit.SECONDS);
106+
return process.exitValue() == 0;
107+
} catch (Exception e) {
108+
LOG.error("Failed detecting Node.js", e);
109+
}
110+
return false;
111+
}
89112
}

0 commit comments

Comments
 (0)