This is an automated email from the ASF dual-hosted git repository. jamesnetherton pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel-quarkus.git
commit 649d24914383ceeb3156869f0b3ae8b141bed8f3 Author: James Netherton <[email protected]> AuthorDate: Thu Sep 4 13:07:37 2025 +0100 Add langchain4j-agent native support --- .../ROOT/examples/components/langchain4j-agent.yml | 6 +- .../reference/extensions/langchain4j-agent.adoc | 10 +- extensions-jvm/pom.xml | 1 - .../deployment/SupportLangchain4jProcessor.java | 161 +++++++++++++++- extensions-support/langchain4j/runtime/pom.xml | 8 + .../langchain4j-agent/deployment/pom.xml | 0 .../deployment/Langchain4jAgentProcessor.java | 17 -- .../langchain4j-agent/pom.xml | 2 +- .../langchain4j-agent/runtime/pom.xml | 1 + .../main/resources/META-INF/quarkus-extension.yaml | 1 - .../chat/deployment/LangChain4jChatProcessor.java | 33 ---- .../langchain4j-web-search/deployment/pom.xml | 5 +- .../deployment/Langchain4jWebSearchProcessor.java | 21 -- extensions/langchain4j-web-search/runtime/pom.xml | 4 + extensions/pom.xml | 1 + .../agent/it/Langchain4jAgentResource.java | 50 ----- integration-tests-jvm/pom.xml | 1 - .../WireMockTestResourceLifecycleManager.java | 8 + integration-tests/langchain4j-agent/README.adoc | 20 ++ .../langchain4j-agent/pom.xml | 70 +++++++ .../langchain4j/agent/it/AgentProducers.java | 211 ++++++++++++++++++++ .../agent/it/Langchain4jAgentResource.java | 209 ++++++++++++++++++++ .../agent/it/Langchain4jAgentRoutes.java | 57 +++++- .../TestPojoJsonExtractorOutputGuardrail.java | 41 ++++ .../guardrail/ValidationFailureInputGuardrail.java | 19 +- .../ValidationFailureOutputGuardrail.java | 19 +- .../guardrail/ValidationSuccessInputGuardrail.java | 27 +-- .../ValidationSuccessOutputGuardrail.java | 27 +-- .../langchain4j/agent/it/model/TestPojo.java | 18 +- .../agent/it/service/CustomAiService.java | 22 +-- .../agent/it/service/TestPojoAiAgent.java | 52 +++++ .../agent/it/util/PersistentChatMemoryStore.java | 56 ++++++ .../src/main/resources/application.properties | 17 ++ .../main/resources/rag/company-knowledge-base.txt | 41 ++++ .../langchain4j/agent/it/Langchain4jAgentIT.java | 16 +- .../langchain4j/agent/it/Langchain4jAgentTest.java | 214 +++++++++++++++++++++ .../agent/it/Langchain4jTestWatcher.java | 53 +++++ .../langchain4j/agent/it/OllamaTestResource.java | 89 +++++++++ ...embed-cc8902fc-b699-41e9-9f80-b50423fbf24c.json | 1 + ..._chat-1b3199b6-6110-4d15-94e7-f929c1334826.json | 26 +++ ..._chat-22a4f651-2f57-4192-8da5-378365cac7c7.json | 26 +++ ..._chat-2f5f4c05-7f04-4ece-ba42-2b560a76476d.json | 27 +++ ..._chat-57c9341b-be13-4f43-a087-cbeb07099998.json | 24 +++ ..._chat-581d8dbe-8619-40d5-b231-06dfc8e63e54.json | 24 +++ ..._chat-65972a4c-1614-4880-b7a9-757f76c1e88e.json | 27 +++ ..._chat-73474560-c2d8-4fbf-a14b-9739090453b4.json | 26 +++ ..._chat-90d1f0a9-0e98-46b8-95be-6a0f10fa145f.json | 27 +++ ..._chat-97c022a9-b386-47f1-8531-3a1ce3bfa4e0.json | 24 +++ ..._chat-9ae841ef-69f5-46a1-a304-0a1c2296db8c.json | 24 +++ ..._chat-a5d8bdce-dd3a-44ab-a796-001de6dba7d6.json | 24 +++ ..._chat-df5c600e-ec18-4194-9037-7b54b6cbdfc3.json | 24 +++ ..._chat-e1d90c56-38f7-4e1f-93d0-641fd6f1c5a7.json | 24 +++ ..._chat-e7aee62a-a9b2-4d51-b31f-f817bc16e013.json | 24 +++ ..._chat-f02d99a6-9cee-4e8d-bcff-7f6fd7ad3d12.json | 27 +++ ..._chat-fceeb039-9b61-4ac5-a8c7-71a6483ea6a7.json | 24 +++ ..._chat-fe594db1-b82f-4216-a1fc-0cfdddc1b33a.json | 24 +++ ...embed-cc8902fc-b699-41e9-9f80-b50423fbf24c.json | 24 +++ ...embed-f208155e-5af8-4c4c-802d-8b2f5be5cb34.json | 24 +++ integration-tests/langchain4j-chat/pom.xml | 13 -- integration-tests/pom.xml | 1 + tooling/scripts/test-categories.yaml | 1 + 61 files changed, 1846 insertions(+), 252 deletions(-) diff --git a/docs/modules/ROOT/examples/components/langchain4j-agent.yml b/docs/modules/ROOT/examples/components/langchain4j-agent.yml index 9d9a249464..e242423db4 100644 --- a/docs/modules/ROOT/examples/components/langchain4j-agent.yml +++ b/docs/modules/ROOT/examples/components/langchain4j-agent.yml @@ -2,11 +2,11 @@ # This file was generated by camel-quarkus-maven-plugin:update-extension-doc-page cqArtifactId: camel-quarkus-langchain4j-agent cqArtifactIdBase: langchain4j-agent -cqNativeSupported: false -cqStatus: Preview +cqNativeSupported: true +cqStatus: Stable cqDeprecated: false cqJvmSince: 3.26.0 -cqNativeSince: n/a +cqNativeSince: 3.27.0 cqCamelPartName: langchain4j-agent cqCamelPartTitle: LangChain4j Agent cqCamelPartDescription: LangChain4j Agent component diff --git a/docs/modules/ROOT/pages/reference/extensions/langchain4j-agent.adoc b/docs/modules/ROOT/pages/reference/extensions/langchain4j-agent.adoc index 84d4bf4b0d..0ef515f77b 100644 --- a/docs/modules/ROOT/pages/reference/extensions/langchain4j-agent.adoc +++ b/docs/modules/ROOT/pages/reference/extensions/langchain4j-agent.adoc @@ -4,17 +4,17 @@ = LangChain4j Agent :linkattrs: :cq-artifact-id: camel-quarkus-langchain4j-agent -:cq-native-supported: false +:cq-native-supported: true :cq-status: Preview :cq-status-deprecation: Preview :cq-description: LangChain4j Agent component :cq-deprecated: false :cq-jvm-since: 3.26.0 -:cq-native-since: n/a +:cq-native-since: 3.27.0 ifeval::[{doc-show-badges} == true] [.badges] -[.badge-key]##JVM since##[.badge-supported]##3.26.0## [.badge-key]##Native##[.badge-unsupported]##unsupported## +[.badge-key]##JVM since##[.badge-supported]##3.26.0## [.badge-key]##Native since##[.badge-supported]##3.27.0## endif::[] LangChain4j Agent component @@ -29,6 +29,10 @@ Please refer to the above link for usage and configuration details. [id="extensions-langchain4j-agent-maven-coordinates"] == Maven coordinates +https://{link-quarkus-code-generator}/?extension-search=camel-quarkus-langchain4j-agent[Create a new project with this extension on {link-quarkus-code-generator}, window="_blank"] + +Or add the coordinates to your existing project: + [source,xml] ---- <dependency> diff --git a/extensions-jvm/pom.xml b/extensions-jvm/pom.xml index aae5ed612f..e535cfcb0e 100644 --- a/extensions-jvm/pom.xml +++ b/extensions-jvm/pom.xml @@ -73,7 +73,6 @@ <module>jooq</module> <module>json-patch</module> <module>jsonapi</module> - <module>langchain4j-agent</module> <module>langchain4j-embeddings</module> <module>ldif</module> <module>lucene</module> diff --git a/extensions-support/langchain4j/deployment/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/deployment/SupportLangchain4jProcessor.java b/extensions-support/langchain4j/deployment/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/deployment/SupportLangchain4jProcessor.java index ac34d5398d..4c92451dab 100644 --- a/extensions-support/langchain4j/deployment/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/deployment/SupportLangchain4jProcessor.java +++ b/extensions-support/langchain4j/deployment/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/deployment/SupportLangchain4jProcessor.java @@ -16,25 +16,63 @@ */ package org.apache.camel.quarkus.component.support.langchain4j.deployment; +import java.util.Collection; +import java.util.HashSet; import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.Stream; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import dev.langchain4j.guardrail.InputGuardrail; +import dev.langchain4j.guardrail.JsonExtractorOutputGuardrail; +import dev.langchain4j.guardrail.OutputGuardrail; +import dev.langchain4j.service.MemoryId; +import dev.langchain4j.service.SystemMessage; +import dev.langchain4j.service.UserMessage; +import dev.langchain4j.service.V; +import io.quarkus.bootstrap.model.ApplicationModel; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.BuildSteps; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.IndexDependencyBuildItem; +import io.quarkus.deployment.builditem.nativeimage.NativeImageProxyDefinitionBuildItem; +import io.quarkus.deployment.builditem.nativeimage.NativeImageResourcePatternsBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem; +import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem; +import io.quarkus.deployment.pkg.steps.NativeOrNativeSourcesBuild; +import io.quarkus.maven.dependency.ResolvedDependency; +import opennlp.tools.sentdetect.SentenceDetectorFactory; +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationTarget; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; +import org.jboss.jandex.IndexView; +import org.jboss.jandex.MethodInfo; +import org.jboss.jandex.MethodParameterInfo; +import org.jboss.jandex.Type; +@BuildSteps(onlyIf = NativeOrNativeSourcesBuild.class) class SupportLangchain4jProcessor { + private static final Class<?>[] AI_SERVICE_ANNOTATION_CLASSES = { + MemoryId.class, + SystemMessage.class, + UserMessage.class, + V.class + }; @BuildStep - void indexDependencies(BuildProducer<IndexDependencyBuildItem> indexedDependencies) { - indexedDependencies.produce(new IndexDependencyBuildItem("dev.langchain4j", "langchain4j-http-client-jdk")); - indexedDependencies.produce(new IndexDependencyBuildItem("dev.langchain4j", "langchain4j-ollama")); + void indexDependencies(CurateOutcomeBuildItem curateOutcome, BuildProducer<IndexDependencyBuildItem> indexedDependencies) { + ApplicationModel applicationModel = curateOutcome.getApplicationModel(); + for (ResolvedDependency dependency : applicationModel.getDependencies()) { + if (dependency.getGroupId().equals("dev.langchain4j")) { + indexedDependencies.produce(new IndexDependencyBuildItem(dependency.getGroupId(), dependency.getArtifactId())); + } + } } @BuildStep @@ -43,10 +81,13 @@ class SupportLangchain4jProcessor { } @BuildStep - void registerForReflection(CombinedIndexBuildItem combinedIndex, BuildProducer<ReflectiveClassBuildItem> reflectiveClass) { - Set<String> ollamaModelClasses = combinedIndex.getIndex() - .getClassesInPackage("dev.langchain4j.model.ollama") - .stream() + void registerLangChain4jJacksonTypesForReflection( + CombinedIndexBuildItem combinedIndex, + BuildProducer<ReflectiveClassBuildItem> reflectiveClass) { + IndexView index = combinedIndex.getIndex(); + + // Discover all LangChain4j Jackson model types + Set<String> langChain4jModelClasses = langChain4jTypesStream(index.getKnownClasses()) .filter(classInfo -> classInfo.annotations().stream() .anyMatch(annotationInstance -> annotationInstance.name().toString() .startsWith("com.fasterxml.jackson.annotation"))) @@ -54,13 +95,117 @@ class SupportLangchain4jProcessor { .map(DotName::toString) .collect(Collectors.toSet()); - reflectiveClass.produce(ReflectiveClassBuildItem.builder(ollamaModelClasses.toArray(new String[0])) + reflectiveClass.produce(ReflectiveClassBuildItem.builder(langChain4jModelClasses.toArray(new String[0])) .methods(true) .build()); + + // Discover all LangChain4j Jackson serializer / deserializer types + Set<String> jacksonSupportClasses = langChain4jTypesStream(index.getAllKnownSubclasses(JsonSerializer.class)) + .map(classInfo -> classInfo.name().toString()) + .collect(Collectors.toSet()); + + langChain4jTypesStream(index.getAllKnownSubclasses(JsonDeserializer.class)) + .map(classInfo -> classInfo.name().toString()) + .forEach(jacksonSupportClasses::add); + + reflectiveClass.produce(ReflectiveClassBuildItem.builder(jacksonSupportClasses.toArray(new String[0])).build()); + + // Misc Jackson support + ReflectiveClassBuildItem.builder(PropertyNamingStrategies.SnakeCaseStrategy.class).build(); + } + + @BuildStep + void registerLangChain4jAiServiceTypesForReflection( + CombinedIndexBuildItem combinedIndex, + BuildProducer<ReflectiveClassBuildItem> reflectiveClass, + BuildProducer<NativeImageProxyDefinitionBuildItem> nativeImageProxy) { + + IndexView index = combinedIndex.getIndex(); + Set<String> aiServiceInterfaces = new HashSet<>(); + Set<String> aiServiceTypes = new HashSet<>(); + + for (Class<?> aiServiceClass : AI_SERVICE_ANNOTATION_CLASSES) { + for (AnnotationInstance annotationInstance : index.getAnnotations(aiServiceClass)) { + AnnotationTarget annotationTarget = annotationInstance.target(); + + if (annotationTarget.kind().equals(AnnotationTarget.Kind.CLASS)) { + aiServiceInterfaces.add(annotationTarget.asClass().name().toString()); + } else if (annotationTarget.kind().equals(AnnotationTarget.Kind.METHOD)) { + MethodInfo method = annotationTarget.asMethod(); + aiServiceInterfaces.add(method.declaringClass().name().toString()); + if (!method.returnType().kind().equals(Type.Kind.VOID)) { + aiServiceTypes.add(method.returnType().name().toString()); + } + } else if (annotationTarget.kind().equals(AnnotationTarget.Kind.METHOD_PARAMETER)) { + MethodParameterInfo methodParameter = annotationTarget.asMethodParameter(); + aiServiceTypes.add(methodParameter.type().name().toString()); + + MethodInfo method = methodParameter.method(); + aiServiceInterfaces.add(method.declaringClass().name().toString()); + if (!method.returnType().kind().equals(Type.Kind.VOID)) { + aiServiceTypes.add(method.returnType().name().toString()); + } + } + } + } + + // Any types participating in JsonExtractorOutputGuardrail operations require reflection + index.getAllKnownSubclasses(JsonExtractorOutputGuardrail.class) + .stream() + .filter(classInfo -> classInfo.superClassType() != null) + .filter(classInfo -> classInfo.superClassType().kind().equals(Type.Kind.PARAMETERIZED_TYPE)) + .map(ClassInfo::superClassType) + .map(Type::asParameterizedType) + .flatMap(type -> type.arguments().stream()) + .findFirst() + .ifPresent(typeParameter -> { + aiServiceTypes.add(typeParameter.name().toString()); + }); + + // AI service interfaces must be registered as native image proxies + aiServiceInterfaces + .stream() + .map(NativeImageProxyDefinitionBuildItem::new) + .forEach(nativeImageProxy::produce); + + // Register any types related to the AI service for reflection + reflectiveClass.produce(ReflectiveClassBuildItem.builder(aiServiceTypes.toArray(new String[0])) + .methods() + .build()); + + // Guardrails are instantiated dynamically + Set<String> guardrailTypes = index.getAllKnownImplementations(InputGuardrail.class) + .stream() + .map(classInfo -> classInfo.name().toString()) + .collect(Collectors.toSet()); + + index.getAllKnownImplementations(OutputGuardrail.class) + .stream() + .map(classInfo -> classInfo.name().toString()) + .forEach(guardrailTypes::add); + + reflectiveClass.produce(ReflectiveClassBuildItem.builder(guardrailTypes.toArray(new String[0])).build()); + } + + @BuildStep + void registerLangChain4jNlpTypesForReflection(BuildProducer<ReflectiveClassBuildItem> reflectiveClass) { + reflectiveClass.produce(ReflectiveClassBuildItem.builder(SentenceDetectorFactory.class).build()); } @BuildStep RuntimeInitializedClassBuildItem runtimeInitializedClasses() { return new RuntimeInitializedClassBuildItem("dev.langchain4j.internal.RetryUtils"); } + + @BuildStep + NativeImageResourcePatternsBuildItem nativeImageResources() { + return NativeImageResourcePatternsBuildItem.builder() + .includeGlob("opennlp/*.bin") + .build(); + } + + static Stream<ClassInfo> langChain4jTypesStream(Collection<ClassInfo> classes) { + return classes.stream() + .filter(classInfo -> classInfo.name().toString().startsWith("dev.langchain4j")); + } } diff --git a/extensions-support/langchain4j/runtime/pom.xml b/extensions-support/langchain4j/runtime/pom.xml index 033c35c041..553ffd3e8b 100644 --- a/extensions-support/langchain4j/runtime/pom.xml +++ b/extensions-support/langchain4j/runtime/pom.xml @@ -38,6 +38,14 @@ <groupId>io.quarkus</groupId> <artifactId>quarkus-jackson</artifactId> </dependency> + <dependency> + <groupId>dev.langchain4j</groupId> + <artifactId>langchain4j</artifactId> + </dependency> + <dependency> + <groupId>dev.langchain4j</groupId> + <artifactId>langchain4j-core</artifactId> + </dependency> </dependencies> <build> diff --git a/extensions-jvm/langchain4j-agent/deployment/pom.xml b/extensions/langchain4j-agent/deployment/pom.xml similarity index 100% rename from extensions-jvm/langchain4j-agent/deployment/pom.xml rename to extensions/langchain4j-agent/deployment/pom.xml diff --git a/extensions-jvm/langchain4j-agent/deployment/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/deployment/Langchain4jAgentProcessor.java b/extensions/langchain4j-agent/deployment/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/deployment/Langchain4jAgentProcessor.java similarity index 61% rename from extensions-jvm/langchain4j-agent/deployment/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/deployment/Langchain4jAgentProcessor.java rename to extensions/langchain4j-agent/deployment/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/deployment/Langchain4jAgentProcessor.java index 767c3352c0..0f3e510c35 100644 --- a/extensions-jvm/langchain4j-agent/deployment/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/deployment/Langchain4jAgentProcessor.java +++ b/extensions/langchain4j-agent/deployment/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/deployment/Langchain4jAgentProcessor.java @@ -17,30 +17,13 @@ package org.apache.camel.quarkus.component.langchain4j.agent.deployment; import io.quarkus.deployment.annotations.BuildStep; -import io.quarkus.deployment.annotations.ExecutionTime; -import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.FeatureBuildItem; -import io.quarkus.deployment.pkg.steps.NativeOrNativeSourcesBuild; -import org.apache.camel.quarkus.core.JvmOnlyRecorder; -import org.jboss.logging.Logger; class Langchain4jAgentProcessor { - - private static final Logger LOG = Logger.getLogger(Langchain4jAgentProcessor.class); private static final String FEATURE = "camel-langchain4j-agent"; @BuildStep FeatureBuildItem feature() { return new FeatureBuildItem(FEATURE); } - - /** - * Remove this once this extension starts supporting the native mode. - */ - @BuildStep(onlyIf = NativeOrNativeSourcesBuild.class) - @Record(value = ExecutionTime.RUNTIME_INIT) - void warnJvmInNative(JvmOnlyRecorder recorder) { - JvmOnlyRecorder.warnJvmInNative(LOG, FEATURE); // warn at build time - recorder.warnJvmInNative(FEATURE); // warn at runtime - } } diff --git a/extensions-jvm/langchain4j-agent/pom.xml b/extensions/langchain4j-agent/pom.xml similarity index 96% rename from extensions-jvm/langchain4j-agent/pom.xml rename to extensions/langchain4j-agent/pom.xml index 9ee3d88123..aab51a76ba 100644 --- a/extensions-jvm/langchain4j-agent/pom.xml +++ b/extensions/langchain4j-agent/pom.xml @@ -21,7 +21,7 @@ <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.apache.camel.quarkus</groupId> - <artifactId>camel-quarkus-extensions-jvm</artifactId> + <artifactId>camel-quarkus-extensions</artifactId> <version>3.27.0-SNAPSHOT</version> <relativePath>../pom.xml</relativePath> </parent> diff --git a/extensions-jvm/langchain4j-agent/runtime/pom.xml b/extensions/langchain4j-agent/runtime/pom.xml similarity index 98% rename from extensions-jvm/langchain4j-agent/runtime/pom.xml rename to extensions/langchain4j-agent/runtime/pom.xml index df30f7c261..769e00cebe 100644 --- a/extensions-jvm/langchain4j-agent/runtime/pom.xml +++ b/extensions/langchain4j-agent/runtime/pom.xml @@ -33,6 +33,7 @@ <properties> <camel.quarkus.jvmSince>3.26.0</camel.quarkus.jvmSince> <quarkus.metadata.status>preview</quarkus.metadata.status> + <camel.quarkus.nativeSince>3.27.0</camel.quarkus.nativeSince> </properties> <dependencies> diff --git a/extensions-jvm/langchain4j-agent/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/langchain4j-agent/runtime/src/main/resources/META-INF/quarkus-extension.yaml similarity index 98% rename from extensions-jvm/langchain4j-agent/runtime/src/main/resources/META-INF/quarkus-extension.yaml rename to extensions/langchain4j-agent/runtime/src/main/resources/META-INF/quarkus-extension.yaml index ceb064c2c9..69a2e1ebc1 100644 --- a/extensions-jvm/langchain4j-agent/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/langchain4j-agent/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -26,7 +26,6 @@ description: "LangChain4j Agent component" metadata: icon-url: "https://raw.githubusercontent.com/apache/camel-website/main/antora-ui-camel/src/img/logo-d.svg" sponsor: "Apache Software Foundation" - unlisted: true guide: "https://camel.apache.org/camel-quarkus/latest/reference/extensions/langchain4j-agent.html" categories: - "integration" diff --git a/extensions/langchain4j-chat/deployment/src/main/java/org/apache/camel/quarkus/component/langchain/chat/deployment/LangChain4jChatProcessor.java b/extensions/langchain4j-chat/deployment/src/main/java/org/apache/camel/quarkus/component/langchain/chat/deployment/LangChain4jChatProcessor.java index 43b259daa2..21b454122f 100644 --- a/extensions/langchain4j-chat/deployment/src/main/java/org/apache/camel/quarkus/component/langchain/chat/deployment/LangChain4jChatProcessor.java +++ b/extensions/langchain4j-chat/deployment/src/main/java/org/apache/camel/quarkus/component/langchain/chat/deployment/LangChain4jChatProcessor.java @@ -16,11 +16,8 @@ */ package org.apache.camel.quarkus.component.langchain.chat.deployment; -import com.fasterxml.jackson.databind.PropertyNamingStrategies; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.FeatureBuildItem; -import io.quarkus.deployment.builditem.nativeimage.NativeImageProxyDefinitionBuildItem; -import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; class LangChain4jChatProcessor { private static final String FEATURE = "camel-langchain4j-chat"; @@ -29,34 +26,4 @@ class LangChain4jChatProcessor { FeatureBuildItem feature() { return new FeatureBuildItem(FEATURE); } - - @BuildStep - NativeImageProxyDefinitionBuildItem nativeImageProxyConfig() { - return new NativeImageProxyDefinitionBuildItem("dev.langchain4j.model.ollama.OllamaApi"); - } - - @BuildStep - ReflectiveClassBuildItem reflectiveClass() { - return ReflectiveClassBuildItem.builder(PropertyNamingStrategies.SnakeCaseStrategy.class).constructors().build(); - } - - @BuildStep - ReflectiveClassBuildItem registerForReflection() { - return ReflectiveClassBuildItem.builder( - "dev.langchain4j.model.ollama.FormatSerializer", - "dev.langchain4j.model.ollama.Function", - "dev.langchain4j.model.ollama.FunctionCall", - "dev.langchain4j.model.ollama.Message", - "dev.langchain4j.model.ollama.OllamaChatRequest", - "dev.langchain4j.model.ollama.OllamaChatResponse", - "dev.langchain4j.model.ollama.Options", - "dev.langchain4j.model.ollama.Parameters", - "dev.langchain4j.model.ollama.Role", - "dev.langchain4j.model.ollama.Tool", - "dev.langchain4j.model.ollama.ToolCall", - "dev.langchain4j.model.ollama.ChatRequest", - "dev.langchain4j.model.ollama.ChatResponse") - .methods(true) - .build(); - } } diff --git a/extensions/langchain4j-web-search/deployment/pom.xml b/extensions/langchain4j-web-search/deployment/pom.xml index 9b0d90b8b8..648d3e5f9f 100644 --- a/extensions/langchain4j-web-search/deployment/pom.xml +++ b/extensions/langchain4j-web-search/deployment/pom.xml @@ -34,7 +34,10 @@ <groupId>org.apache.camel.quarkus</groupId> <artifactId>camel-quarkus-core-deployment</artifactId> </dependency> - <!-- TODO: Have a conditional dep for this or transform to JAX-RS --> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-support-langchain4j-deployment</artifactId> + </dependency> <dependency> <groupId>org.apache.camel.quarkus</groupId> <artifactId>camel-quarkus-support-retrofit-deployment</artifactId> diff --git a/extensions/langchain4j-web-search/deployment/src/main/java/org/apache/camel/quarkus/component/langchain4j/web/search/deployment/Langchain4jWebSearchProcessor.java b/extensions/langchain4j-web-search/deployment/src/main/java/org/apache/camel/quarkus/component/langchain4j/web/search/deployment/Langchain4jWebSearchProcessor.java index d19af52ed6..5eac56e9cc 100644 --- a/extensions/langchain4j-web-search/deployment/src/main/java/org/apache/camel/quarkus/component/langchain4j/web/search/deployment/Langchain4jWebSearchProcessor.java +++ b/extensions/langchain4j-web-search/deployment/src/main/java/org/apache/camel/quarkus/component/langchain4j/web/search/deployment/Langchain4jWebSearchProcessor.java @@ -20,16 +20,11 @@ import java.util.Set; import java.util.regex.Pattern; import java.util.stream.Collectors; -import io.quarkus.bootstrap.model.ApplicationModel; -import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; -import io.quarkus.deployment.builditem.IndexDependencyBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageProxyDefinitionBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; -import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem; -import io.quarkus.maven.dependency.ResolvedDependency; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; @@ -42,22 +37,6 @@ class Langchain4jWebSearchProcessor { return new FeatureBuildItem(FEATURE); } - @BuildStep - void indexDependencies( - BuildProducer<IndexDependencyBuildItem> indexedDependency, - CurateOutcomeBuildItem curateOutcome) { - - // Index any dependencies with artifactId prefix langchain4j-web-search-engine - ApplicationModel applicationModel = curateOutcome.getApplicationModel(); - for (ResolvedDependency dependency : applicationModel.getDependencies()) { - if (dependency.getGroupId().equals("dev.langchain4j") - && dependency.getArtifactId().startsWith("langchain4j-web-search-engine")) { - String artifactId = dependency.getArtifactId(); - indexedDependency.produce(new IndexDependencyBuildItem(dependency.getGroupId(), artifactId)); - } - } - } - @BuildStep NativeImageProxyDefinitionBuildItem registerRestServiceProxies(CombinedIndexBuildItem combinedIndex) { // If there are any retrofit2 REST service definitions, we need to register native proxy definitions for them diff --git a/extensions/langchain4j-web-search/runtime/pom.xml b/extensions/langchain4j-web-search/runtime/pom.xml index 3e107f7bcd..1cfb4645ff 100644 --- a/extensions/langchain4j-web-search/runtime/pom.xml +++ b/extensions/langchain4j-web-search/runtime/pom.xml @@ -41,6 +41,10 @@ <groupId>org.apache.camel.quarkus</groupId> <artifactId>camel-quarkus-core</artifactId> </dependency> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-support-langchain4j</artifactId> + </dependency> <dependency> <groupId>org.apache.camel.quarkus</groupId> <artifactId>camel-quarkus-support-retrofit</artifactId> diff --git a/extensions/pom.xml b/extensions/pom.xml index 32c7e01aa5..5698167ae9 100644 --- a/extensions/pom.xml +++ b/extensions/pom.xml @@ -172,6 +172,7 @@ <module>kubernetes</module> <module>kubernetes-cluster-service</module> <module>kudu</module> + <module>langchain4j-agent</module> <module>langchain4j-chat</module> <module>langchain4j-tokenizer</module> <module>langchain4j-tools</module> diff --git a/integration-tests-jvm/langchain4j-agent/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentResource.java b/integration-tests-jvm/langchain4j-agent/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentResource.java deleted file mode 100644 index 5e7a3d5abf..0000000000 --- a/integration-tests-jvm/langchain4j-agent/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentResource.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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.quarkus.component.langchain4j.agent.it; - -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; -import org.apache.camel.CamelContext; -import org.jboss.logging.Logger; - -@Path("/langchain4j-agent") -@ApplicationScoped -public class Langchain4jAgentResource { - - private static final Logger LOG = Logger.getLogger(Langchain4jAgentResource.class); - - private static final String COMPONENT_LANGCHAIN4J_AGENT = "langchain4j-agent"; - @Inject - CamelContext context; - - @Path("/load/component/langchain4j-agent") - @GET - @Produces(MediaType.TEXT_PLAIN) - public Response loadComponentLangchain4jAgent() throws Exception { - /* This is an autogenerated test */ - if (context.getComponent(COMPONENT_LANGCHAIN4J_AGENT) != null) { - return Response.ok().build(); - } - LOG.warnf("Could not load [%s] from the Camel context", COMPONENT_LANGCHAIN4J_AGENT); - return Response.status(500, COMPONENT_LANGCHAIN4J_AGENT + " could not be loaded from the Camel context").build(); - } -} diff --git a/integration-tests-jvm/pom.xml b/integration-tests-jvm/pom.xml index 126a29ed1a..88c4732276 100644 --- a/integration-tests-jvm/pom.xml +++ b/integration-tests-jvm/pom.xml @@ -72,7 +72,6 @@ <module>jooq</module> <module>json-patch</module> <module>jsonapi</module> - <module>langchain4j-agent</module> <module>langchain4j-embeddings</module> <module>ldif</module> <module>lucene</module> diff --git a/integration-tests-support/wiremock/src/main/java/org/apache/camel/quarkus/test/wiremock/WireMockTestResourceLifecycleManager.java b/integration-tests-support/wiremock/src/main/java/org/apache/camel/quarkus/test/wiremock/WireMockTestResourceLifecycleManager.java index bd02e45860..e77d1069e6 100644 --- a/integration-tests-support/wiremock/src/main/java/org/apache/camel/quarkus/test/wiremock/WireMockTestResourceLifecycleManager.java +++ b/integration-tests-support/wiremock/src/main/java/org/apache/camel/quarkus/test/wiremock/WireMockTestResourceLifecycleManager.java @@ -95,6 +95,8 @@ public abstract class WireMockTestResourceLifecycleManager implements QuarkusTes SnapshotRecordResult recordResult = server.stopRecording(); List<StubMapping> stubMappings = recordResult.getStubMappings(); + processRecordedStubMappings(stubMappings); + if (isDeleteRecordedMappingsOnError()) { for (StubMapping mapping : stubMappings) { int status = mapping.getResponse().getStatus(); @@ -209,6 +211,12 @@ public abstract class WireMockTestResourceLifecycleManager implements QuarkusTes protected void customizeWiremockConfiguration(WireMockConfiguration config) { } + /** + * Hook to get a handle on any record stub mappings. Useful for performing stub post-processing or cleanup tasks. + */ + protected void processRecordedStubMappings(List<StubMapping> stubMappings) { + } + /** * Creates and starts a {@link WireMockServer} on a random port. {@link MockBackendUtils} triggers the log * message that signifies mocking is in use. diff --git a/integration-tests/langchain4j-agent/README.adoc b/integration-tests/langchain4j-agent/README.adoc new file mode 100644 index 0000000000..7c58a613e8 --- /dev/null +++ b/integration-tests/langchain4j-agent/README.adoc @@ -0,0 +1,20 @@ +== Camel Quarkus Langchain4j Agent Integration Tests + +By default, the Langchain4j-agent integration tests use WireMock to stub Ollama API interactions. + +To run the `camel-quarkus-langchain4j-agent` integration tests against the real API, you need a Ollama instance running with the `orca-mini` model downloaded. + +When Ollama is running, set the following environment variables: + +[source,shell] +---- +export LANGCHAIN4J_OLLAMA_BASE_URL=your-ollama-api-url +---- + +If the WireMock stub recordings need updating, then remove the existing files from `src/test/resources/mappings` and run tests with either: + +System property `-Dwiremock.record=true` + +Or + +Set environment variable `WIREMOCK_RECORD=true` diff --git a/integration-tests-jvm/langchain4j-agent/pom.xml b/integration-tests/langchain4j-agent/pom.xml similarity index 54% rename from integration-tests-jvm/langchain4j-agent/pom.xml rename to integration-tests/langchain4j-agent/pom.xml index 5d801e8ee4..acbee495f1 100644 --- a/integration-tests-jvm/langchain4j-agent/pom.xml +++ b/integration-tests/langchain4j-agent/pom.xml @@ -31,14 +31,26 @@ <description>Integration tests for Camel Quarkus LangChain4j Agent extension</description> <dependencies> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-direct</artifactId> + </dependency> <dependency> <groupId>org.apache.camel.quarkus</groupId> <artifactId>camel-quarkus-langchain4j-agent</artifactId> </dependency> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-langchain4j-tools</artifactId> + </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-resteasy</artifactId> </dependency> + <dependency> + <groupId>dev.langchain4j</groupId> + <artifactId>langchain4j-ollama</artifactId> + </dependency> <!-- test dependencies --> <dependency> @@ -51,6 +63,11 @@ <artifactId>rest-assured</artifactId> <scope>test</scope> </dependency> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-integration-wiremock-support</artifactId> + <scope>test</scope> + </dependency> </dependencies> <profiles> @@ -63,6 +80,19 @@ </activation> <dependencies> <!-- The following dependencies guarantee that this module is built after them. You can update them by running `mvn process-resources -Pformat -N` from the source tree root directory --> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-direct-deployment</artifactId> + <version>${project.version}</version> + <type>pom</type> + <scope>test</scope> + <exclusions> + <exclusion> + <groupId>*</groupId> + <artifactId>*</artifactId> + </exclusion> + </exclusions> + </dependency> <dependency> <groupId>org.apache.camel.quarkus</groupId> <artifactId>camel-quarkus-langchain4j-agent-deployment</artifactId> @@ -76,7 +106,47 @@ </exclusion> </exclusions> </dependency> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-langchain4j-tools-deployment</artifactId> + <version>${project.version}</version> + <type>pom</type> + <scope>test</scope> + <exclusions> + <exclusion> + <groupId>*</groupId> + <artifactId>*</artifactId> + </exclusion> + </exclusions> + </dependency> </dependencies> </profile> + <profile> + <id>native</id> + <activation> + <property> + <name>native</name> + </property> + </activation> + <properties> + <quarkus.native.enabled>true</quarkus.native.enabled> + </properties> + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-failsafe-plugin</artifactId> + <executions> + <execution> + <goals> + <goal>integration-test</goal> + <goal>verify</goal> + </goals> + </execution> + </executions> + </plugin> + </plugins> + </build> + </profile> </profiles> </project> diff --git a/integration-tests/langchain4j-agent/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/AgentProducers.java b/integration-tests/langchain4j-agent/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/AgentProducers.java index 0d0988ccb0..f60dd5ad05 100644 --- a/integration-tests/langchain4j-agent/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/AgentProducers.java +++ b/integration-tests/langchain4j-agent/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/AgentProducers.java @@ -1,4 +1,215 @@ +/* + * 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.quarkus.component.langchain4j.agent.it; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.List; + +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.langchain4j.data.document.Document; +import dev.langchain4j.data.document.splitter.DocumentSplitters; +import dev.langchain4j.data.embedding.Embedding; +import dev.langchain4j.data.segment.TextSegment; +import dev.langchain4j.memory.chat.ChatMemoryProvider; +import dev.langchain4j.memory.chat.MessageWindowChatMemory; +import dev.langchain4j.model.chat.ChatModel; +import dev.langchain4j.model.embedding.EmbeddingModel; +import dev.langchain4j.model.ollama.OllamaChatModel; +import dev.langchain4j.model.ollama.OllamaEmbeddingModel; +import dev.langchain4j.rag.DefaultRetrievalAugmentor; +import dev.langchain4j.rag.RetrievalAugmentor; +import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever; +import dev.langchain4j.store.embedding.EmbeddingStore; +import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore; +import dev.langchain4j.store.memory.chat.ChatMemoryStore; +import io.smallrye.common.annotation.Identifier; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import org.apache.camel.component.langchain4j.agent.api.Agent; +import org.apache.camel.component.langchain4j.agent.api.AgentConfiguration; +import org.apache.camel.component.langchain4j.agent.api.AgentWithMemory; +import org.apache.camel.component.langchain4j.agent.api.AgentWithoutMemory; +import org.apache.camel.quarkus.component.langchain4j.agent.it.guardrail.TestPojoJsonExtractorOutputGuardrail; +import org.apache.camel.quarkus.component.langchain4j.agent.it.guardrail.ValidationFailureInputGuardrail; +import org.apache.camel.quarkus.component.langchain4j.agent.it.guardrail.ValidationFailureOutputGuardrail; +import org.apache.camel.quarkus.component.langchain4j.agent.it.guardrail.ValidationSuccessInputGuardrail; +import org.apache.camel.quarkus.component.langchain4j.agent.it.guardrail.ValidationSuccessOutputGuardrail; +import org.apache.camel.quarkus.component.langchain4j.agent.it.service.TestPojoAiAgent; +import org.apache.camel.quarkus.component.langchain4j.agent.it.util.PersistentChatMemoryStore; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +import static java.time.Duration.ofSeconds; + +@ApplicationScoped public class AgentProducers { + @ConfigProperty(name = "langchain4j.ollama.base-url") + String baseUrl; + + @Produces + @Identifier("ollamaOrcaMiniModel") + ChatModel ollamaOrcaMiniModel() { + return OllamaChatModel.builder() + .baseUrl(baseUrl) + .modelName("orca-mini") + .temperature(0.3) + .build(); + } + + @Produces + @Identifier("ollamaLlama31Model") + ChatModel ollamaLlama31Model() { + return OllamaChatModel.builder() + .baseUrl(baseUrl) + .modelName("llama3.1:latest") + .temperature(0.3) + .logResponses(true) + .logRequests(true) + .build(); + } + + @Produces + ChatMemoryStore chatMemoryStore() { + return new PersistentChatMemoryStore(); + } + + @Produces + RetrievalAugmentor retrievalAugmentor() throws IOException { + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + try (InputStream stream = classLoader.getResourceAsStream("rag/company-knowledge-base.txt")) { + if (stream == null) { + throw new IllegalArgumentException("company-knowledge.txt not found"); + } + + Document document = Document.from(new String(stream.readAllBytes(), StandardCharsets.UTF_8)); + + List<TextSegment> segments = DocumentSplitters.recursive(300, 100).split(document); + + EmbeddingModel embeddingModel = OllamaEmbeddingModel.builder() + .baseUrl(baseUrl) + .modelName("nomic-embed-text") + .timeout(Duration.ofSeconds(30)) + .build(); + + List<Embedding> embeddings = embeddingModel.embedAll(segments).content(); + + // Store in embedding store + EmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>(); + embeddingStore.addAll(embeddings, segments); + + // Create content retriever + EmbeddingStoreContentRetriever contentRetriever = EmbeddingStoreContentRetriever.builder() + .embeddingStore(embeddingStore) + .embeddingModel(embeddingModel) + .maxResults(3) + .minScore(0.6) + .build(); + + // Create a RetrievalAugmentor that uses only a content retriever : naive rag scenario + return DefaultRetrievalAugmentor.builder() + .contentRetriever(contentRetriever) + .build(); + } + } + + @Produces + @Identifier("simpleAgent") + Agent simpleAgent(@Identifier("ollamaOrcaMiniModel") ChatModel chatModel) { + return new AgentWithoutMemory(new AgentConfiguration().withChatModel(chatModel)); + } + + @Produces + @Identifier("agentWithMemory") + Agent agentWithMemory(@Identifier("ollamaLlama31Model") ChatModel chatModel, ChatMemoryStore chatMemoryStore) { + ChatMemoryProvider chatMemoryProvider = memoryId -> MessageWindowChatMemory.builder() + .id(memoryId) + .maxMessages(10) + .chatMemoryStore(chatMemoryStore) + .build(); + + return new AgentWithMemory(new AgentConfiguration() + .withChatModel(chatModel) + .withChatMemoryProvider(chatMemoryProvider)); + } + + @Produces + @Identifier("agentWithSuccessInputGuardrail") + Agent agentWithSuccessInputGuardrail(@Identifier("ollamaOrcaMiniModel") ChatModel chatModel) { + return new AgentWithoutMemory(new AgentConfiguration() + .withChatModel(chatModel) + .withInputGuardrailClasses(List.of(ValidationSuccessInputGuardrail.class))); + } + + @Produces + @Identifier("agentWithFailingInputGuardrail") + Agent agentWithFailingInputGuardrail(@Identifier("ollamaOrcaMiniModel") ChatModel chatModel) { + return new AgentWithoutMemory(new AgentConfiguration() + .withChatModel(chatModel) + .withInputGuardrailClasses(List.of(ValidationFailureInputGuardrail.class))); + } + + @Produces + @Identifier("agentWithSuccessOutputGuardrail") + Agent agentWithSuccessOutputGuardrail(@Identifier("ollamaOrcaMiniModel") ChatModel chatModel) { + return new AgentWithoutMemory(new AgentConfiguration() + .withChatModel(chatModel) + .withOutputGuardrailClasses(List.of(ValidationSuccessOutputGuardrail.class))); + } + + @Produces + @Identifier("agentWithFailingOutputGuardrail") + Agent agentWithFailingOutputGuardrail(@Identifier("ollamaOrcaMiniModel") ChatModel chatModel) { + return new AgentWithoutMemory(new AgentConfiguration() + .withChatModel(chatModel) + .withOutputGuardrailClasses(List.of(ValidationFailureOutputGuardrail.class))); + } + + @Produces + @Identifier("agentWithJsonExtractorOutputGuardrail") + Agent agentWithJsonExtractorOutputGuardrail(@Identifier("ollamaOrcaMiniModel") ChatModel chatModel) { + return new AgentWithoutMemory(new AgentConfiguration() + .withChatModel(chatModel) + .withOutputGuardrailClasses(List.of(TestPojoJsonExtractorOutputGuardrail.class))); + } + + @Produces + @Identifier("agentWithRag") + public Agent agentWithRag( + @Identifier("ollamaOrcaMiniModel") ChatModel chatModel, + RetrievalAugmentor retrievalAugmentor) { + return new AgentWithoutMemory(new AgentConfiguration() + .withChatModel(chatModel) + .withRetrievalAugmentor(retrievalAugmentor)); + } + + @Produces + @Identifier("agentWithTools") + public Agent agentWithTools(@Identifier("ollamaLlama31Model") ChatModel chatModel) { + return new AgentWithoutMemory(new AgentConfiguration().withChatModel(chatModel)); + } + + @Produces + @Identifier("agentWithCustomService") + public Agent agentCustom( + @Identifier("ollamaOrcaMiniModel") ChatModel chatModel, + ObjectMapper objectMapper) { + return new TestPojoAiAgent(new AgentConfiguration() + .withChatModel(chatModel), objectMapper); + } } diff --git a/integration-tests/langchain4j-agent/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentResource.java b/integration-tests/langchain4j-agent/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentResource.java new file mode 100644 index 0000000000..290e2362e0 --- /dev/null +++ b/integration-tests/langchain4j-agent/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentResource.java @@ -0,0 +1,209 @@ +/* + * 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.quarkus.component.langchain4j.agent.it; + +import java.util.HashMap; +import java.util.Map; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.apache.camel.Exchange; +import org.apache.camel.FluentProducerTemplate; +import org.apache.camel.component.langchain4j.agent.api.AiAgentBody; +import org.apache.camel.quarkus.component.langchain4j.agent.it.guardrail.ValidationSuccessInputGuardrail; +import org.apache.camel.quarkus.component.langchain4j.agent.it.guardrail.ValidationSuccessOutputGuardrail; + +import static org.apache.camel.component.langchain4j.agent.Headers.MEMORY_ID; +import static org.apache.camel.component.langchain4j.agent.Headers.SYSTEM_MESSAGE; + +@Path("/langchain4j-agent") +@ApplicationScoped +public class Langchain4jAgentResource { + @Inject + FluentProducerTemplate producerTemplate; + + @Path("/simple") + @POST + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + public Response chatWithSimpleUserMessage( + @QueryParam("bodyAsBean") boolean isBodyAsBean, + @QueryParam("systemMessage") String systemMessage, + String userMessage) { + + Map<String, Object> headers = new HashMap<>(); + if (!isBodyAsBean && systemMessage != null) { + headers.put(SYSTEM_MESSAGE, systemMessage); + } + + Object body; + if (isBodyAsBean) { + body = new AiAgentBody() + .withSystemMessage(systemMessage) + .withUserMessage(userMessage); + } else { + body = userMessage; + } + + String result = producerTemplate.to("direct:simple-agent") + .withBody(body) + .withHeaders(headers) + .request(String.class); + + return Response.ok(result.trim()).build(); + } + + @Path("/memory") + @POST + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + public Response chatWithAgentMemory( + @QueryParam("memoryId") String memoryId, + String userMessage) { + + Map<String, Object> headers = new HashMap<>(); + headers.put(MEMORY_ID, memoryId); + + String result = producerTemplate.to("direct:agent-with-memory") + .withBody(userMessage) + .withHeaders(headers) + .request(String.class); + + return Response.ok(result.trim()).build(); + } + + @Path("/input/guardrail/success") + @POST + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + public Response chatWithSuccessInputGuardrail(String userMessage) { + producerTemplate.to("direct:agent-with-success-input-guardrail") + .withBody(userMessage) + .request(String.class); + + return Response.ok(ValidationSuccessInputGuardrail.isValidateCalled()).build(); + } + + @Path("/input/guardrail/failure") + @POST + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + public Response chatWithFailingInputGuardrail(String userMessage) { + Exchange result = producerTemplate.to("direct:agent-with-failing-input-guardrail") + .withBody(userMessage) + .send(); + + if (result.getException() != null) { + return Response.serverError() + .entity(result.getException().getMessage()) + .build(); + } + + return Response.ok().build(); + } + + @Path("/output/guardrail/success") + @POST + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + public Response chatWithSuccessOutputGuardrail(String userMessage) { + producerTemplate.to("direct:agent-with-success-output-guardrail") + .withBody(userMessage) + .request(String.class); + + return Response.ok(ValidationSuccessOutputGuardrail.isValidateCalled()).build(); + } + + @Path("/output/guardrail/failure") + @POST + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + public Response chatWithFailingOutputGuardrail(String userMessage) { + Exchange result = producerTemplate.to("direct:agent-with-failing-output-guardrail") + .withBody(userMessage) + .send(); + + if (result.getException() != null) { + return Response.serverError() + .entity(result.getException().getMessage()) + .build(); + } + + return Response.ok().build(); + } + + @Path("/output/guardrail/json/extractor") + @POST + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.APPLICATION_JSON) + public Response chatWithJsonExtractorOutputGuardrail(String userMessage) { + Exchange result = producerTemplate.to("direct:agent-with-json-extractor-output-guardrail") + .withBody(userMessage) + .send(); + + if (result.getException() != null) { + return Response.serverError() + .entity(result.getException().getMessage()) + .build(); + } + + return Response.ok(result.getMessage().getBody()).build(); + } + + @Path("/rag") + @POST + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + public Response chatWithRag(String userMessage) { + String result = producerTemplate.to("direct:agent-with-rag") + .withBody(userMessage) + .request(String.class); + + return Response.ok(result.trim()).build(); + } + + @Path("/tools") + @POST + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + public Response chatWithTools(String userMessage) { + String result = producerTemplate.to("direct:agent-with-tools") + .withBody(userMessage) + .request(String.class); + + return Response.ok(result.trim()).build(); + } + + @Path("/custom/service") + @POST + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.APPLICATION_JSON) + public Response chatWithCustomAiService(String userMessage) { + String result = producerTemplate.to("direct:agent-with-custom-service") + .withBody(userMessage) + .request(String.class); + + return Response.ok(result.trim()).build(); + } +} diff --git a/integration-tests/langchain4j-agent/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentRoutes.java b/integration-tests/langchain4j-agent/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentRoutes.java index 5a7de2040e..3c09c356e6 100644 --- a/integration-tests/langchain4j-agent/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentRoutes.java +++ b/integration-tests/langchain4j-agent/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentRoutes.java @@ -1,4 +1,59 @@ +/* + * 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.quarkus.component.langchain4j.agent.it; -public class Langchain4jAgentRoutes { +import org.apache.camel.builder.RouteBuilder; + +public class Langchain4jAgentRoutes extends RouteBuilder { + public static final String USER_JOHN = "John Doe"; + + @Override + public void configure() throws Exception { + from("direct:simple-agent") + .to("langchain4j-agent:test-agent?agent=#simpleAgent"); + + from("direct:agent-with-memory") + .to("langchain4j-agent:test-memory-agent?agent=#agentWithMemory"); + + from("direct:agent-with-success-input-guardrail") + .to("langchain4j-agent:test-agent-with-success-input-guardrail?agent=#agentWithSuccessInputGuardrail"); + + from("direct:agent-with-failing-input-guardrail") + .to("langchain4j-agent:test-agent-with-failing-input-guardrail?agent=#agentWithFailingInputGuardrail"); + + from("direct:agent-with-success-output-guardrail") + .to("langchain4j-agent:test-agent-with-success-output-guardrail?agent=#agentWithSuccessOutputGuardrail"); + + from("direct:agent-with-failing-output-guardrail") + .to("langchain4j-agent:test-agent-with-failing-output-guardrail?agent=#agentWithFailingOutputGuardrail"); + + from("direct:agent-with-json-extractor-output-guardrail") + .to("langchain4j-agent:test-agent-with-json-extractor-output-guardrail?agent=#agentWithJsonExtractorOutputGuardrail"); + + from("direct:agent-with-rag") + .to("langchain4j-agent:test-agent-with-rag?agent=#agentWithRag"); + + from("direct:agent-with-custom-service") + .to("langchain4j-agent:test-agent-with-custom-service?agent=#agentWithCustomService"); + + from("direct:agent-with-tools") + .to("langchain4j-agent:test-agent-with-tools?agent=#agentWithTools&tags=users"); + + from("langchain4j-tools:userDb?tags=users&description=Query user database by user ID¶meter.userId=integer") + .setBody().constant("{\"name\": \"" + USER_JOHN + "\", \"id\": \"123\"}"); + } } diff --git a/integration-tests/langchain4j-agent/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/guardrail/TestPojoJsonExtractorOutputGuardrail.java b/integration-tests/langchain4j-agent/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/guardrail/TestPojoJsonExtractorOutputGuardrail.java new file mode 100644 index 0000000000..4edf92b060 --- /dev/null +++ b/integration-tests/langchain4j-agent/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/guardrail/TestPojoJsonExtractorOutputGuardrail.java @@ -0,0 +1,41 @@ +/* + * 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.quarkus.component.langchain4j.agent.it.guardrail; + +import dev.langchain4j.data.message.AiMessage; +import dev.langchain4j.guardrail.JsonExtractorOutputGuardrail; +import dev.langchain4j.guardrail.OutputGuardrailResult; +import org.apache.camel.quarkus.component.langchain4j.agent.it.model.TestPojo; + +public class TestPojoJsonExtractorOutputGuardrail extends JsonExtractorOutputGuardrail<TestPojo> { + public TestPojoJsonExtractorOutputGuardrail() { + super(TestPojo.class); + } + + @Override + public OutputGuardrailResult validate(AiMessage aiMessage) { + OutputGuardrailResult parentResult = super.validate(aiMessage); + + if (parentResult.isSuccess()) { + // Return JSON String representation of TestPojo since that's all the agent can handle + return OutputGuardrailResult.successWith(trimNonJson(aiMessage.text())); + } + + // Return failures + return OutputGuardrailResult.failure(parentResult.failures()); + } +} diff --git a/integration-tests-jvm/langchain4j-agent/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentTest.java b/integration-tests/langchain4j-agent/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/guardrail/ValidationFailureInputGuardrail.java similarity index 63% copy from integration-tests-jvm/langchain4j-agent/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentTest.java copy to integration-tests/langchain4j-agent/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/guardrail/ValidationFailureInputGuardrail.java index 79890649b0..e720cc9123 100644 --- a/integration-tests-jvm/langchain4j-agent/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentTest.java +++ b/integration-tests/langchain4j-agent/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/guardrail/ValidationFailureInputGuardrail.java @@ -14,21 +14,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.camel.quarkus.component.langchain4j.agent.it; +package org.apache.camel.quarkus.component.langchain4j.agent.it.guardrail; -import io.quarkus.test.junit.QuarkusTest; -import io.restassured.RestAssured; -import org.junit.jupiter.api.Test; - -@QuarkusTest -class Langchain4jAgentTest { - - @Test - public void loadComponentLangchain4jAgent() { - /* A simple autogenerated test */ - RestAssured.get("/langchain4j-agent/load/component/langchain4j-agent") - .then() - .statusCode(200); - } +import dev.langchain4j.guardrail.InputGuardrail; +public class ValidationFailureInputGuardrail implements InputGuardrail { + // Empty impl to leverage default methods from InputGuardrail, which always result in validation failure } diff --git a/integration-tests-jvm/langchain4j-agent/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentTest.java b/integration-tests/langchain4j-agent/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/guardrail/ValidationFailureOutputGuardrail.java similarity index 63% copy from integration-tests-jvm/langchain4j-agent/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentTest.java copy to integration-tests/langchain4j-agent/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/guardrail/ValidationFailureOutputGuardrail.java index 79890649b0..97bcf7025b 100644 --- a/integration-tests-jvm/langchain4j-agent/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentTest.java +++ b/integration-tests/langchain4j-agent/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/guardrail/ValidationFailureOutputGuardrail.java @@ -14,21 +14,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.camel.quarkus.component.langchain4j.agent.it; +package org.apache.camel.quarkus.component.langchain4j.agent.it.guardrail; -import io.quarkus.test.junit.QuarkusTest; -import io.restassured.RestAssured; -import org.junit.jupiter.api.Test; - -@QuarkusTest -class Langchain4jAgentTest { - - @Test - public void loadComponentLangchain4jAgent() { - /* A simple autogenerated test */ - RestAssured.get("/langchain4j-agent/load/component/langchain4j-agent") - .then() - .statusCode(200); - } +import dev.langchain4j.guardrail.OutputGuardrail; +public class ValidationFailureOutputGuardrail implements OutputGuardrail { + // Empty impl to leverage default methods from OutputGuardrail, which always result in validation failure } diff --git a/integration-tests-jvm/langchain4j-agent/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentTest.java b/integration-tests/langchain4j-agent/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/guardrail/ValidationSuccessInputGuardrail.java similarity index 54% copy from integration-tests-jvm/langchain4j-agent/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentTest.java copy to integration-tests/langchain4j-agent/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/guardrail/ValidationSuccessInputGuardrail.java index 79890649b0..ac12835725 100644 --- a/integration-tests-jvm/langchain4j-agent/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentTest.java +++ b/integration-tests/langchain4j-agent/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/guardrail/ValidationSuccessInputGuardrail.java @@ -14,21 +14,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.camel.quarkus.component.langchain4j.agent.it; +package org.apache.camel.quarkus.component.langchain4j.agent.it.guardrail; -import io.quarkus.test.junit.QuarkusTest; -import io.restassured.RestAssured; -import org.junit.jupiter.api.Test; +import java.util.concurrent.atomic.AtomicBoolean; -@QuarkusTest -class Langchain4jAgentTest { +import dev.langchain4j.data.message.UserMessage; +import dev.langchain4j.guardrail.InputGuardrail; +import dev.langchain4j.guardrail.InputGuardrailResult; - @Test - public void loadComponentLangchain4jAgent() { - /* A simple autogenerated test */ - RestAssured.get("/langchain4j-agent/load/component/langchain4j-agent") - .then() - .statusCode(200); +public class ValidationSuccessInputGuardrail implements InputGuardrail { + private static final AtomicBoolean validateCalled = new AtomicBoolean(false); + + @Override + public InputGuardrailResult validate(UserMessage userMessage) { + validateCalled.set(true); + return InputGuardrailResult.success(); } + public static boolean isValidateCalled() { + return validateCalled.get(); + } } diff --git a/integration-tests-jvm/langchain4j-agent/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentTest.java b/integration-tests/langchain4j-agent/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/guardrail/ValidationSuccessOutputGuardrail.java similarity index 54% copy from integration-tests-jvm/langchain4j-agent/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentTest.java copy to integration-tests/langchain4j-agent/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/guardrail/ValidationSuccessOutputGuardrail.java index 79890649b0..f677dd531c 100644 --- a/integration-tests-jvm/langchain4j-agent/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentTest.java +++ b/integration-tests/langchain4j-agent/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/guardrail/ValidationSuccessOutputGuardrail.java @@ -14,21 +14,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.camel.quarkus.component.langchain4j.agent.it; +package org.apache.camel.quarkus.component.langchain4j.agent.it.guardrail; -import io.quarkus.test.junit.QuarkusTest; -import io.restassured.RestAssured; -import org.junit.jupiter.api.Test; +import java.util.concurrent.atomic.AtomicBoolean; -@QuarkusTest -class Langchain4jAgentTest { +import dev.langchain4j.data.message.AiMessage; +import dev.langchain4j.guardrail.OutputGuardrail; +import dev.langchain4j.guardrail.OutputGuardrailResult; - @Test - public void loadComponentLangchain4jAgent() { - /* A simple autogenerated test */ - RestAssured.get("/langchain4j-agent/load/component/langchain4j-agent") - .then() - .statusCode(200); +public class ValidationSuccessOutputGuardrail implements OutputGuardrail { + private static final AtomicBoolean validateCalled = new AtomicBoolean(false); + + @Override + public OutputGuardrailResult validate(AiMessage responseFromLLM) { + validateCalled.set(true); + return OutputGuardrailResult.success(); } + public static boolean isValidateCalled() { + return validateCalled.get(); + } } diff --git a/integration-tests-jvm/langchain4j-agent/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentTest.java b/integration-tests/langchain4j-agent/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/model/TestPojo.java similarity index 63% copy from integration-tests-jvm/langchain4j-agent/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentTest.java copy to integration-tests/langchain4j-agent/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/model/TestPojo.java index 79890649b0..be28be175c 100644 --- a/integration-tests-jvm/langchain4j-agent/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentTest.java +++ b/integration-tests/langchain4j-agent/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/model/TestPojo.java @@ -14,21 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.camel.quarkus.component.langchain4j.agent.it; - -import io.quarkus.test.junit.QuarkusTest; -import io.restassured.RestAssured; -import org.junit.jupiter.api.Test; - -@QuarkusTest -class Langchain4jAgentTest { - - @Test - public void loadComponentLangchain4jAgent() { - /* A simple autogenerated test */ - RestAssured.get("/langchain4j-agent/load/component/langchain4j-agent") - .then() - .statusCode(200); - } +package org.apache.camel.quarkus.component.langchain4j.agent.it.model; +public record TestPojo(String name, String description) { } diff --git a/integration-tests-jvm/langchain4j-agent/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentTest.java b/integration-tests/langchain4j-agent/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/service/CustomAiService.java similarity index 63% copy from integration-tests-jvm/langchain4j-agent/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentTest.java copy to integration-tests/langchain4j-agent/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/service/CustomAiService.java index 79890649b0..812f6bd7ba 100644 --- a/integration-tests-jvm/langchain4j-agent/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentTest.java +++ b/integration-tests/langchain4j-agent/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/service/CustomAiService.java @@ -14,21 +14,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.camel.quarkus.component.langchain4j.agent.it; +package org.apache.camel.quarkus.component.langchain4j.agent.it.service; -import io.quarkus.test.junit.QuarkusTest; -import io.restassured.RestAssured; -import org.junit.jupiter.api.Test; - -@QuarkusTest -class Langchain4jAgentTest { - - @Test - public void loadComponentLangchain4jAgent() { - /* A simple autogenerated test */ - RestAssured.get("/langchain4j-agent/load/component/langchain4j-agent") - .then() - .statusCode(200); - } +import dev.langchain4j.service.UserMessage; +import dev.langchain4j.service.V; +import org.apache.camel.quarkus.component.langchain4j.agent.it.model.TestPojo; +public interface CustomAiService { + @UserMessage("Return an example JSON object about a person named {{name}} with the fields name and description") + TestPojo getTestPojo(@V("name") String name); } diff --git a/integration-tests/langchain4j-agent/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/service/TestPojoAiAgent.java b/integration-tests/langchain4j-agent/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/service/TestPojoAiAgent.java new file mode 100644 index 0000000000..4dcf8cbcca --- /dev/null +++ b/integration-tests/langchain4j-agent/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/service/TestPojoAiAgent.java @@ -0,0 +1,52 @@ +/* + * 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.quarkus.component.langchain4j.agent.it.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.langchain4j.service.AiServices; +import dev.langchain4j.service.tool.ToolProvider; +import org.apache.camel.component.langchain4j.agent.api.Agent; +import org.apache.camel.component.langchain4j.agent.api.AgentConfiguration; +import org.apache.camel.component.langchain4j.agent.api.AiAgentBody; +import org.apache.camel.quarkus.component.langchain4j.agent.it.model.TestPojo; + +public class TestPojoAiAgent implements Agent { + private final AgentConfiguration configuration; + private final ObjectMapper objectMapper; + + public TestPojoAiAgent(AgentConfiguration configuration, ObjectMapper objectMapper) { + this.configuration = configuration; + this.objectMapper = objectMapper; + } + + @Override + public String chat(AiAgentBody aiAgentBody, ToolProvider toolProvider) { + TestPojo response = createService().getTestPojo(aiAgentBody.getUserMessage()); + try { + return objectMapper.writeValueAsString(response); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + CustomAiService createService() { + return AiServices.builder(CustomAiService.class) + .chatModel(configuration.getChatModel()) + .build(); + } +} diff --git a/integration-tests/langchain4j-agent/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/util/PersistentChatMemoryStore.java b/integration-tests/langchain4j-agent/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/util/PersistentChatMemoryStore.java new file mode 100644 index 0000000000..33ff4c9ee2 --- /dev/null +++ b/integration-tests/langchain4j-agent/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/util/PersistentChatMemoryStore.java @@ -0,0 +1,56 @@ +/* + * 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.quarkus.component.langchain4j.agent.it.util; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import dev.langchain4j.data.message.ChatMessage; +import dev.langchain4j.store.memory.chat.ChatMemoryStore; + +import static dev.langchain4j.data.message.ChatMessageDeserializer.messagesFromJson; +import static dev.langchain4j.data.message.ChatMessageSerializer.messagesToJson; + +public class PersistentChatMemoryStore implements ChatMemoryStore { + private final Map<Object, String> memoryMap = new ConcurrentHashMap<>(); + + @Override + public List<ChatMessage> getMessages(Object memoryId) { + String json = memoryMap.get(memoryId); + return json != null ? messagesFromJson(json) : List.of(); + } + + @Override + public void updateMessages(Object memoryId, List<ChatMessage> messages) { + String json = messagesToJson(messages); + memoryMap.put(memoryId, json); + } + + @Override + public void deleteMessages(Object memoryId) { + memoryMap.remove(memoryId); + } + + public int getMemoryCount() { + return memoryMap.size(); + } + + public void clearAll() { + memoryMap.clear(); + } +} diff --git a/integration-tests/langchain4j-agent/src/main/resources/application.properties b/integration-tests/langchain4j-agent/src/main/resources/application.properties new file mode 100644 index 0000000000..38bd39fb72 --- /dev/null +++ b/integration-tests/langchain4j-agent/src/main/resources/application.properties @@ -0,0 +1,17 @@ +## --------------------------------------------------------------------------- +## 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. +## --------------------------------------------------------------------------- +quarkus.native.resources.includes=rag/* \ No newline at end of file diff --git a/integration-tests/langchain4j-agent/src/main/resources/rag/company-knowledge-base.txt b/integration-tests/langchain4j-agent/src/main/resources/rag/company-knowledge-base.txt new file mode 100644 index 0000000000..96fe9ab1fe --- /dev/null +++ b/integration-tests/langchain4j-agent/src/main/resources/rag/company-knowledge-base.txt @@ -0,0 +1,41 @@ +Miles of Camels Car Rental - Company Information + +BUSINESS HOURS: +Monday-Friday: 8:00 AM - 6:00 PM +Saturday: 9:00 AM - 4:00 PM +Sunday: Closed + +RENTAL AGREEMENT +- This agreement is between Miles of Camels Car Rental ("Company") and the customer ("Renter"). + +RENTAL POLICIES: +- Minimum age: 21 years old +- Valid driver's license required +- Credit card required for security deposit +- Full tank of gas required at return + +VEHICLE FLEET: +- Economy cars: Starting at $29/day +- Mid-size cars: Starting at $39/day +- SUVs: Starting at $59/day +- Luxury vehicles: Starting at $89/day + +CANCELLATION POLICY +- Cancellations made 24 hours before pickup: Full refund +- Cancellations made 12-24 hours before pickup: 50% refund +- Cancellations made less than 12 hours before pickup: No refund + +VEHICLE RETURN +- Vehicles must be returned with the same fuel level as at pickup. +- Late returns incur a fee of $25 per hour or fraction thereof. + +DAMAGE POLICY +- Minor damages under $200: Covered by insurance +- Major damages over $200: Customer responsibility + +INSURANCE +- Basic insurance is included. Premium insurance available for $15/day. + +AGE REQUIREMENTS +- Minimum age: 21 years old +- Drivers under 25: Additional surcharge of $20/day diff --git a/integration-tests-jvm/langchain4j-agent/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentTest.java b/integration-tests/langchain4j-agent/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentIT.java similarity index 68% rename from integration-tests-jvm/langchain4j-agent/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentTest.java rename to integration-tests/langchain4j-agent/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentIT.java index 79890649b0..c0905b5bee 100644 --- a/integration-tests-jvm/langchain4j-agent/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentTest.java +++ b/integration-tests/langchain4j-agent/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentIT.java @@ -16,19 +16,9 @@ */ package org.apache.camel.quarkus.component.langchain4j.agent.it; -import io.quarkus.test.junit.QuarkusTest; -import io.restassured.RestAssured; -import org.junit.jupiter.api.Test; +import io.quarkus.test.junit.QuarkusIntegrationTest; -@QuarkusTest -class Langchain4jAgentTest { - - @Test - public void loadComponentLangchain4jAgent() { - /* A simple autogenerated test */ - RestAssured.get("/langchain4j-agent/load/component/langchain4j-agent") - .then() - .statusCode(200); - } +@QuarkusIntegrationTest +class Langchain4jAgentIT extends Langchain4jAgentTest { } diff --git a/integration-tests/langchain4j-agent/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentTest.java b/integration-tests/langchain4j-agent/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentTest.java new file mode 100644 index 0000000000..fce52dbc17 --- /dev/null +++ b/integration-tests/langchain4j-agent/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentTest.java @@ -0,0 +1,214 @@ +/* + * 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.quarkus.component.langchain4j.agent.it; + +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.RestAssured; +import org.apache.camel.quarkus.component.langchain4j.agent.it.guardrail.ValidationFailureInputGuardrail; +import org.apache.camel.quarkus.component.langchain4j.agent.it.guardrail.ValidationFailureOutputGuardrail; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.apache.camel.quarkus.component.langchain4j.agent.it.Langchain4jAgentRoutes.USER_JOHN; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.containsStringIgnoringCase; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.startsWith; + +@ExtendWith(Langchain4jTestWatcher.class) +@QuarkusTestResource(OllamaTestResource.class) +@QuarkusTest +class Langchain4jAgentTest { + static final String TEST_USER_MESSAGE_SIMPLE = "What is Apache Camel?"; + static final String TEST_USER_MESSAGE_STORY = "Write a short story about a lost cat."; + static final String TEST_SYSTEM_MESSAGE = """ + You are a whimsical storyteller. Your responses should be imaginative, descriptive, and always include a touch of magic. Start every story with 'Once upon a starlit night...'"""; + static final String EXPECTED_STORY_START = "Once upon a starlit night"; + static final String EXPECTED_STORY_CONTENT = "cat"; + + static final String USER_ALICE = "Alice"; + static final String USER_FAVORITE_COLOR = "blue"; + static final String MEMORY_ID = "camel-quarkus-memory-1"; + + @Test + void simpleUserMessage() { + RestAssured.given() + .body(TEST_USER_MESSAGE_SIMPLE) + .post("/langchain4j-agent/simple") + .then() + .statusCode(200) + .body( + not(TEST_USER_MESSAGE_SIMPLE), + containsString("Apache Camel")); + } + + @Test + void simpleUserMessageWithSystemMessagePrompt() { + RestAssured.given() + .queryParam("systemMessage", TEST_SYSTEM_MESSAGE) + .body(TEST_USER_MESSAGE_STORY) + .post("/langchain4j-agent/simple") + .then() + .statusCode(200) + .body( + not(TEST_USER_MESSAGE_SIMPLE), + startsWith(EXPECTED_STORY_START), + containsString(EXPECTED_STORY_CONTENT)); + } + + @Test + void simpleUserMessageWithAiAgentBody() { + RestAssured.given() + .queryParam("bodyAsBean", true) + .queryParam("systemMessage", TEST_SYSTEM_MESSAGE) + .body(TEST_USER_MESSAGE_STORY) + .post("/langchain4j-agent/simple") + .then() + .statusCode(200) + .body( + not(TEST_USER_MESSAGE_SIMPLE), + startsWith(EXPECTED_STORY_START), + containsString(EXPECTED_STORY_CONTENT)); + } + + @Test + void agentMemory() { + RestAssured.given() + .queryParam("memoryId", MEMORY_ID) + .body("Hello - my name is " + USER_ALICE) + .post("/langchain4j-agent/memory") + .then() + .statusCode(200); + + RestAssured.given() + .queryParam("memoryId", MEMORY_ID) + .body("And my favorite color is " + USER_FAVORITE_COLOR) + .post("/langchain4j-agent/memory") + .then() + .statusCode(200); + + RestAssured.given() + .queryParam("memoryId", MEMORY_ID) + .body("Now tell me about myself - what's my name and favorite color?") + .post("/langchain4j-agent/memory") + .then() + .statusCode(200) + .body( + containsString(USER_ALICE), + containsString(USER_FAVORITE_COLOR)); + } + + @Test + void inputGuardrailSuccess() { + RestAssured.given() + .body("Hello - my name is " + USER_ALICE) + .post("/langchain4j-agent/input/guardrail/success") + .then() + .statusCode(200) + .body(is("true")); + } + + @Test + void inputGuardrailFailure() { + RestAssured.given() + .body("Hello - my name is " + USER_ALICE) + .post("/langchain4j-agent/input/guardrail/failure") + .then() + .statusCode(500) + .body(containsString("guardrail %s failed".formatted(ValidationFailureInputGuardrail.class.getName()))); + } + + @Test + void outputGuardrailSuccess() { + RestAssured.given() + .body("Hello - my name is " + USER_ALICE) + .post("/langchain4j-agent/output/guardrail/success") + .then() + .statusCode(200) + .body(is("true")); + } + + @Test + void outputGuardrailFailure() { + RestAssured.given() + .body("Hello - my name is " + USER_ALICE) + .post("/langchain4j-agent/output/guardrail/failure") + .then() + .statusCode(500) + .body(containsString("guardrail %s failed".formatted(ValidationFailureOutputGuardrail.class.getName()))); + } + + @Test + void jsonExtractorOutputGuardrailSuccess() { + RestAssured.given() + .body("Return an example JSON object about a person named '%s' with the fields name and description" + .formatted(USER_JOHN)) + .post("/langchain4j-agent/output/guardrail/json/extractor") + .then() + .statusCode(200) + .body( + "name", is(USER_JOHN), + "description", notNullValue()); + } + + @Test + void jsonExtractorOutputGuardrailFailure() { + RestAssured.given() + // Returns field age which is not defined in TestPojo + .body("Return an example JSON object about a person named '%s' with the fields age and description" + .formatted(USER_JOHN)) + .post("/langchain4j-agent/output/guardrail/json/extractor") + .then() + .statusCode(500) + .body(containsString("Invalid JSON")); + } + + @Test + void simpleRag() { + RestAssured.given() + .body("Describe the Miles of Camels Car Rental cancellations policy for cancelling 24 hours before pickup. What is the refund amount?") + .post("/langchain4j-agent/rag") + .then() + .statusCode(200) + .body(containsStringIgnoringCase("full refund")); + } + + @Test + void simpleToolInvocation() { + RestAssured.given() + .body("What is the name of user ID 123?") + .post("/langchain4j-agent/tools") + .then() + .statusCode(200) + .body(containsStringIgnoringCase(USER_JOHN)); + } + + @Test + void customAiService() { + RestAssured.given() + .body(USER_JOHN) + .post("/langchain4j-agent/custom/service") + .then() + .statusCode(200) + .body( + "name", is(USER_JOHN), + "description", notNullValue()); + } +} diff --git a/integration-tests/langchain4j-agent/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jTestWatcher.java b/integration-tests/langchain4j-agent/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jTestWatcher.java new file mode 100644 index 0000000000..1232c8aca1 --- /dev/null +++ b/integration-tests/langchain4j-agent/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jTestWatcher.java @@ -0,0 +1,53 @@ +/* + * 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.quarkus.component.langchain4j.agent.it; + +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.TestWatcher; + +public class Langchain4jTestWatcher implements TestWatcher { + private static final String TEST_TO_WATCH = "simpleRag"; + private static final AtomicBoolean RAG_TEST_EXECUTED = new AtomicBoolean(false); + + public static boolean isRagTestExecuted() { + return RAG_TEST_EXECUTED.get(); + } + + public static void reset() { + RAG_TEST_EXECUTED.set(false); + } + + @Override + public void testSuccessful(ExtensionContext context) { + if (isTargetTest(context)) { + RAG_TEST_EXECUTED.set(true); + } + } + + @Override + public void testFailed(ExtensionContext context, Throwable cause) { + if (isTargetTest(context)) { + RAG_TEST_EXECUTED.set(true); + } + } + + private boolean isTargetTest(ExtensionContext context) { + return context.getDisplayName().equals(TEST_TO_WATCH); + } +} diff --git a/integration-tests/langchain4j-agent/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/OllamaTestResource.java b/integration-tests/langchain4j-agent/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/OllamaTestResource.java new file mode 100644 index 0000000000..ee56621035 --- /dev/null +++ b/integration-tests/langchain4j-agent/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/OllamaTestResource.java @@ -0,0 +1,89 @@ +/* + * 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.quarkus.component.langchain4j.agent.it; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.Map; + +import com.github.tomakehurst.wiremock.stubbing.StubMapping; +import org.apache.camel.quarkus.test.wiremock.WireMockTestResourceLifecycleManager; + +public class OllamaTestResource extends WireMockTestResourceLifecycleManager { + private static final String OLLAMA_ENV_URL = "LANGCHAIN4J_OLLAMA_BASE_URL"; + + @Override + public Map<String, String> start() { + Map<String, String> properties = super.start(); + String wiremockUrl = properties.get("wiremock.url"); + String url = wiremockUrl != null ? wiremockUrl : getRecordTargetBaseUrl(); + properties.put("langchain4j.ollama.base-url", url); + return properties; + } + + @Override + protected String getRecordTargetBaseUrl() { + return System.getenv(OLLAMA_ENV_URL); + } + + @Override + protected boolean isMockingEnabled() { + return !envVarsPresent(OLLAMA_ENV_URL); + } + + @Override + protected void processRecordedStubMappings(List<StubMapping> stubMappings) { + stubMappings.forEach(mapping -> { + String fileName = mapping.getName() + "-" + mapping.getId() + ".json"; + Path mappingFilePath = Paths.get("./src/test/resources/mappings/", fileName); + + // ignoreExtraElements directive can lead to WireMock getting confused about which stub to use on request matching. + // Force disabling it manually since there's no specific WireMock config option to tune it + try { + String mappingContent = Files.readString(mappingFilePath); + mappingContent = mappingContent.replace("\"ignoreExtraElements\" : true", "\"ignoreExtraElements\" : false"); + Files.writeString(mappingFilePath, mappingContent); + } catch (IOException e) { + throw new RuntimeException(e); + } + + // RetrievalAugmentor bean setup causes /api/embed stubs to be recorded on every test run. + // So clean up superfluous recordings unless the RAG test actually ran + if (!Langchain4jTestWatcher.isRagTestExecuted() && mapping.getName().startsWith("api_embed")) { + Path mappingBodyFilePath = Paths.get("./src/test/resources/__files", mapping.getResponse().getBodyFileName()); + try { + Files.deleteIfExists(mappingFilePath); + Files.deleteIfExists(mappingBodyFilePath); + } catch (IOException e) { + // Ignored + } + } + }); + } + + @Override + public void stop() { + try { + super.stop(); + } finally { + Langchain4jTestWatcher.reset(); + } + } +} diff --git a/integration-tests/langchain4j-agent/src/test/resources/__files/api_embed-cc8902fc-b699-41e9-9f80-b50423fbf24c.json b/integration-tests/langchain4j-agent/src/test/resources/__files/api_embed-cc8902fc-b699-41e9-9f80-b50423fbf24c.json new file mode 100644 index 0000000000..51abe1eb64 --- /dev/null +++ b/integration-tests/langchain4j-agent/src/test/resources/__files/api_embed-cc8902fc-b699-41e9-9f80-b50423fbf24c.json @@ -0,0 +1 @@ +{"model":"nomic-embed-text","embeddings":[[-0.07452105,0.008169046,-0.19769989,0.010827969,0.022558905,-0.0049091205,-0.024375705,-0.030121969,-0.016969366,-0.010704407,-0.0005079617,-0.011742291,0.020993004,-0.021422928,0.06289979,-0.01598493,0.013814787,-0.039411496,-0.027963826,0.04861051,0.019435575,0.0045811683,-0.008926697,-0.0142346155,0.05645905,0.020602351,0.039079726,0.056176674,0.012757396,0.0048789666,0.050709747,0.037688315,0.022191847,-0.042426426,-0.058710158,-0.030974913, [...] \ No newline at end of file diff --git a/integration-tests/langchain4j-agent/src/test/resources/mappings/api_chat-1b3199b6-6110-4d15-94e7-f929c1334826.json b/integration-tests/langchain4j-agent/src/test/resources/mappings/api_chat-1b3199b6-6110-4d15-94e7-f929c1334826.json new file mode 100644 index 0000000000..01afffbe17 --- /dev/null +++ b/integration-tests/langchain4j-agent/src/test/resources/mappings/api_chat-1b3199b6-6110-4d15-94e7-f929c1334826.json @@ -0,0 +1,26 @@ +{ + "id" : "1b3199b6-6110-4d15-94e7-f929c1334826", + "name" : "api_chat", + "request" : { + "url" : "/api/chat", + "method" : "POST", + "bodyPatterns" : [ { + "equalToJson" : "{\n \"model\" : \"orca-mini\",\n \"messages\" : [ {\n \"role\" : \"user\",\n \"content\" : \"Make sure you return a valid JSON object following the specified format\"\n } ],\n \"options\" : {\n \"temperature\" : 0.3,\n \"stop\" : [ ]\n },\n \"stream\" : false,\n \"tools\" : [ ]\n}", + "ignoreArrayOrder" : true, + "ignoreExtraElements" : false + } ] + }, + "response" : { + "status" : 200, + "body" : "{\"model\":\"orca-mini\",\"created_at\":\"2025-09-05T08:41:05.942507866Z\",\"message\":{\"role\":\"assistant\",\"content\":\" Sure, I'll do my best to assist you with that. Please provide me with the specific format of the JSON object you would like to receive.\"},\"done_reason\":\"stop\",\"done\":true,\"total_duration\":1564689217,\"load_duration\":5121066,\"prompt_eval_count\":52,\"prompt_eval_duration\":48967007,\"eval_count\":32,\"eval_duration\":1509759220}", + "headers" : { + "Date" : "Fri, 05 Sep 2025 08:41:05 GMT", + "Content-Type" : "application/json; charset=utf-8" + } + }, + "uuid" : "1b3199b6-6110-4d15-94e7-f929c1334826", + "persistent" : true, + "scenarioName" : "scenario-3-api-chat", + "requiredScenarioState" : "scenario-3-api-chat-2", + "insertionIndex" : 6 +} \ No newline at end of file diff --git a/integration-tests/langchain4j-agent/src/test/resources/mappings/api_chat-22a4f651-2f57-4192-8da5-378365cac7c7.json b/integration-tests/langchain4j-agent/src/test/resources/mappings/api_chat-22a4f651-2f57-4192-8da5-378365cac7c7.json new file mode 100644 index 0000000000..722dda1ab3 --- /dev/null +++ b/integration-tests/langchain4j-agent/src/test/resources/mappings/api_chat-22a4f651-2f57-4192-8da5-378365cac7c7.json @@ -0,0 +1,26 @@ +{ + "id" : "22a4f651-2f57-4192-8da5-378365cac7c7", + "name" : "api_chat", + "request" : { + "url" : "/api/chat", + "method" : "POST", + "bodyPatterns" : [ { + "equalToJson" : "{\n \"model\" : \"orca-mini\",\n \"messages\" : [ {\n \"role\" : \"user\",\n \"content\" : \"Hello - my name is Alice\"\n } ],\n \"options\" : {\n \"temperature\" : 0.3,\n \"stop\" : [ ]\n },\n \"stream\" : false,\n \"tools\" : [ ]\n}", + "ignoreArrayOrder" : true, + "ignoreExtraElements" : false + } ] + }, + "response" : { + "status" : 200, + "body" : "{\"model\":\"orca-mini\",\"created_at\":\"2025-09-05T08:41:17.084840245Z\",\"message\":{\"role\":\"assistant\",\"content\":\" Hello, Alice. How can I assist you today?\"},\"done_reason\":\"stop\",\"done\":true,\"total_duration\":718450885,\"load_duration\":5142315,\"prompt_eval_count\":46,\"prompt_eval_duration\":180030056,\"eval_count\":12,\"eval_duration\":532488717}", + "headers" : { + "Date" : "Fri, 05 Sep 2025 08:41:17 GMT", + "Content-Type" : "application/json; charset=utf-8" + } + }, + "uuid" : "22a4f651-2f57-4192-8da5-378365cac7c7", + "persistent" : true, + "scenarioName" : "scenario-2-api-chat", + "requiredScenarioState" : "scenario-2-api-chat-3", + "insertionIndex" : 2 +} \ No newline at end of file diff --git a/integration-tests/langchain4j-agent/src/test/resources/mappings/api_chat-2f5f4c05-7f04-4ece-ba42-2b560a76476d.json b/integration-tests/langchain4j-agent/src/test/resources/mappings/api_chat-2f5f4c05-7f04-4ece-ba42-2b560a76476d.json new file mode 100644 index 0000000000..838f4445e4 --- /dev/null +++ b/integration-tests/langchain4j-agent/src/test/resources/mappings/api_chat-2f5f4c05-7f04-4ece-ba42-2b560a76476d.json @@ -0,0 +1,27 @@ +{ + "id" : "2f5f4c05-7f04-4ece-ba42-2b560a76476d", + "name" : "api_chat", + "request" : { + "url" : "/api/chat", + "method" : "POST", + "bodyPatterns" : [ { + "equalToJson" : "{\n \"model\" : \"orca-mini\",\n \"messages\" : [ {\n \"role\" : \"system\",\n \"content\" : \"You are a whimsical storyteller. Your responses should be imaginative, descriptive, and always include a touch of magic. Start every story with 'Once upon a starlit night...'\"\n }, {\n \"role\" : \"user\",\n \"content\" : \"Write a short story about a lost cat.\"\n } ],\n \"options\" : {\n \"temperature\" : 0.3,\n \"stop\" : [ ]\n },\n \"stream\" [...] + "ignoreArrayOrder" : true, + "ignoreExtraElements" : false + } ] + }, + "response" : { + "status" : 200, + "body" : "{\"model\":\"orca-mini\",\"created_at\":\"2025-09-05T08:40:51.505571144Z\",\"message\":{\"role\":\"assistant\",\"content\":\" Once upon a starlit night, a little black and white kitten named Whiskers went out to explore her neighborhood. She meowed her way through the streets, catching the attention of a kindhearted lady who stopped to pet her. The lady promised to keep an eye out for Whiskers and even gave her a name: Luna.\\n\\nWhiskers was a happy kitten, exploring every [...] + "headers" : { + "Date" : "Fri, 05 Sep 2025 08:40:51 GMT", + "Content-Type" : "application/json; charset=utf-8" + } + }, + "uuid" : "2f5f4c05-7f04-4ece-ba42-2b560a76476d", + "persistent" : true, + "scenarioName" : "scenario-1-api-chat", + "requiredScenarioState" : "Started", + "newScenarioState" : "scenario-1-api-chat-2", + "insertionIndex" : 12 +} \ No newline at end of file diff --git a/integration-tests/langchain4j-agent/src/test/resources/mappings/api_chat-57c9341b-be13-4f43-a087-cbeb07099998.json b/integration-tests/langchain4j-agent/src/test/resources/mappings/api_chat-57c9341b-be13-4f43-a087-cbeb07099998.json new file mode 100644 index 0000000000..1edaa95cef --- /dev/null +++ b/integration-tests/langchain4j-agent/src/test/resources/mappings/api_chat-57c9341b-be13-4f43-a087-cbeb07099998.json @@ -0,0 +1,24 @@ +{ + "id" : "57c9341b-be13-4f43-a087-cbeb07099998", + "name" : "api_chat", + "request" : { + "url" : "/api/chat", + "method" : "POST", + "bodyPatterns" : [ { + "equalToJson" : "{\n \"model\" : \"orca-mini\",\n \"messages\" : [ {\n \"role\" : \"user\",\n \"content\" : \"Return an example JSON object about a person named John Doe with the fields name and description\\nYou must answer strictly in the following JSON format: {\\n\\\"name\\\": (type: string),\\n\\\"description\\\": (type: string)\\n}\"\n } ],\n \"options\" : {\n \"temperature\" : 0.3,\n \"stop\" : [ ]\n },\n \"stream\" : false,\n \"tools\" : [ ]\n}", + "ignoreArrayOrder" : true, + "ignoreExtraElements" : false + } ] + }, + "response" : { + "status" : 200, + "body" : "{\"model\":\"orca-mini\",\"created_at\":\"2025-09-05T08:40:58.01517105Z\",\"message\":{\"role\":\"assistant\",\"content\":\" Sure! Here's an example JSON object for John Doe:\\n\\n```\\n{\\n \\\"name\\\": \\\"John Doe\\\",\\n \\\"description\\\": \\\"A person with a unique name that is easy to remember and pronounce.\\\"\\n}\\n```\"},\"done_reason\":\"stop\",\"done\":true,\"total_duration\":3135963143,\"load_duration\":5398287,\"prompt_eval_count\":90,\"prompt_eval_duration [...] + "headers" : { + "Date" : "Fri, 05 Sep 2025 08:40:58 GMT", + "Content-Type" : "application/json; charset=utf-8" + } + }, + "uuid" : "57c9341b-be13-4f43-a087-cbeb07099998", + "persistent" : true, + "insertionIndex" : 10 +} \ No newline at end of file diff --git a/integration-tests/langchain4j-agent/src/test/resources/mappings/api_chat-581d8dbe-8619-40d5-b231-06dfc8e63e54.json b/integration-tests/langchain4j-agent/src/test/resources/mappings/api_chat-581d8dbe-8619-40d5-b231-06dfc8e63e54.json new file mode 100644 index 0000000000..c7ebe2209a --- /dev/null +++ b/integration-tests/langchain4j-agent/src/test/resources/mappings/api_chat-581d8dbe-8619-40d5-b231-06dfc8e63e54.json @@ -0,0 +1,24 @@ +{ + "id" : "581d8dbe-8619-40d5-b231-06dfc8e63e54", + "name" : "api_chat", + "request" : { + "url" : "/api/chat", + "method" : "POST", + "bodyPatterns" : [ { + "equalToJson" : "{\n \"model\" : \"orca-mini\",\n \"messages\" : [ {\n \"role\" : \"user\",\n \"content\" : \"Describe the Miles of Camels Car Rental cancellations policy for cancelling 24 hours before pickup. What is the refund amount?\\n\\nAnswer using the following information:\\nCANCELLATION POLICY\\n- Cancellations made 24 hours before pickup: Full refund\\n- Cancellations made 12-24 hours before pickup: 50% refund\\n- Cancellations made less than 12 hours before picku [...] + "ignoreArrayOrder" : true, + "ignoreExtraElements" : false + } ] + }, + "response" : { + "status" : 200, + "body" : "{\"model\":\"orca-mini\",\"created_at\":\"2025-09-05T08:40:13.2085886Z\",\"message\":{\"role\":\"assistant\",\"content\":\" Miles of Camels Car Rental cancellations policy for cancelling 24 hours before pickup is as follows:\\n\\nCANCELLATION POLICY\\n- Cancellations made 24 hours before pickup: Full refund\\n- Cancellations made 12-24 hours before pickup: 50% refund\\n- Cancellations made less than 12 hours before pickup: No refund\\n\\nTherefore, if you cancel your rental [...] + "headers" : { + "Date" : "Fri, 05 Sep 2025 08:40:13 GMT", + "Content-Type" : "application/json; charset=utf-8" + } + }, + "uuid" : "581d8dbe-8619-40d5-b231-06dfc8e63e54", + "persistent" : true, + "insertionIndex" : 17 +} \ No newline at end of file diff --git a/integration-tests/langchain4j-agent/src/test/resources/mappings/api_chat-65972a4c-1614-4880-b7a9-757f76c1e88e.json b/integration-tests/langchain4j-agent/src/test/resources/mappings/api_chat-65972a4c-1614-4880-b7a9-757f76c1e88e.json new file mode 100644 index 0000000000..86ffb4ce48 --- /dev/null +++ b/integration-tests/langchain4j-agent/src/test/resources/mappings/api_chat-65972a4c-1614-4880-b7a9-757f76c1e88e.json @@ -0,0 +1,27 @@ +{ + "id" : "65972a4c-1614-4880-b7a9-757f76c1e88e", + "name" : "api_chat", + "request" : { + "url" : "/api/chat", + "method" : "POST", + "bodyPatterns" : [ { + "equalToJson" : "{\n \"model\" : \"orca-mini\",\n \"messages\" : [ {\n \"role\" : \"user\",\n \"content\" : \"Hello - my name is Alice\"\n } ],\n \"options\" : {\n \"temperature\" : 0.3,\n \"stop\" : [ ]\n },\n \"stream\" : false,\n \"tools\" : [ ]\n}", + "ignoreArrayOrder" : true, + "ignoreExtraElements" : false + } ] + }, + "response" : { + "status" : 200, + "body" : "{\"model\":\"orca-mini\",\"created_at\":\"2025-09-05T08:40:58.787447825Z\",\"message\":{\"role\":\"assistant\",\"content\":\" Hello, Alice. How can I assist you today?\"},\"done_reason\":\"stop\",\"done\":true,\"total_duration\":708726888,\"load_duration\":6145323,\"prompt_eval_count\":46,\"prompt_eval_duration\":169405185,\"eval_count\":12,\"eval_duration\":532384421}", + "headers" : { + "Date" : "Fri, 05 Sep 2025 08:40:58 GMT", + "Content-Type" : "application/json; charset=utf-8" + } + }, + "uuid" : "65972a4c-1614-4880-b7a9-757f76c1e88e", + "persistent" : true, + "scenarioName" : "scenario-2-api-chat", + "requiredScenarioState" : "scenario-2-api-chat-2", + "newScenarioState" : "scenario-2-api-chat-3", + "insertionIndex" : 9 +} \ No newline at end of file diff --git a/integration-tests/langchain4j-agent/src/test/resources/mappings/api_chat-73474560-c2d8-4fbf-a14b-9739090453b4.json b/integration-tests/langchain4j-agent/src/test/resources/mappings/api_chat-73474560-c2d8-4fbf-a14b-9739090453b4.json new file mode 100644 index 0000000000..c25863e1eb --- /dev/null +++ b/integration-tests/langchain4j-agent/src/test/resources/mappings/api_chat-73474560-c2d8-4fbf-a14b-9739090453b4.json @@ -0,0 +1,26 @@ +{ + "id" : "73474560-c2d8-4fbf-a14b-9739090453b4", + "name" : "api_chat", + "request" : { + "url" : "/api/chat", + "method" : "POST", + "bodyPatterns" : [ { + "equalToJson" : "{\n \"model\" : \"orca-mini\",\n \"messages\" : [ {\n \"role\" : \"system\",\n \"content\" : \"You are a whimsical storyteller. Your responses should be imaginative, descriptive, and always include a touch of magic. Start every story with 'Once upon a starlit night...'\"\n }, {\n \"role\" : \"user\",\n \"content\" : \"Write a short story about a lost cat.\"\n } ],\n \"options\" : {\n \"temperature\" : 0.3,\n \"stop\" : [ ]\n },\n \"stream\" [...] + "ignoreArrayOrder" : true, + "ignoreExtraElements" : false + } ] + }, + "response" : { + "status" : 200, + "body" : "{\"model\":\"orca-mini\",\"created_at\":\"2025-09-05T08:41:35.954805674Z\",\"message\":{\"role\":\"assistant\",\"content\":\" Once upon a starlit night, a curious little girl named Lily wandered through the forest near her home. She loved to explore and was always on the lookout for new adventures. As she walked, she noticed a small black and white kitten wandering around in the distance. The kitten looked lost and scared.\\n\\nLily decided to help the little cat and went t [...] + "headers" : { + "Date" : "Fri, 05 Sep 2025 08:41:35 GMT", + "Content-Type" : "application/json; charset=utf-8" + } + }, + "uuid" : "73474560-c2d8-4fbf-a14b-9739090453b4", + "persistent" : true, + "scenarioName" : "scenario-1-api-chat", + "requiredScenarioState" : "scenario-1-api-chat-2", + "insertionIndex" : 1 +} \ No newline at end of file diff --git a/integration-tests/langchain4j-agent/src/test/resources/mappings/api_chat-90d1f0a9-0e98-46b8-95be-6a0f10fa145f.json b/integration-tests/langchain4j-agent/src/test/resources/mappings/api_chat-90d1f0a9-0e98-46b8-95be-6a0f10fa145f.json new file mode 100644 index 0000000000..b608420e88 --- /dev/null +++ b/integration-tests/langchain4j-agent/src/test/resources/mappings/api_chat-90d1f0a9-0e98-46b8-95be-6a0f10fa145f.json @@ -0,0 +1,27 @@ +{ + "id" : "90d1f0a9-0e98-46b8-95be-6a0f10fa145f", + "name" : "api_chat", + "request" : { + "url" : "/api/chat", + "method" : "POST", + "bodyPatterns" : [ { + "equalToJson" : "{\n \"model\" : \"orca-mini\",\n \"messages\" : [ {\n \"role\" : \"user\",\n \"content\" : \"Hello - my name is Alice\"\n } ],\n \"options\" : {\n \"temperature\" : 0.3,\n \"stop\" : [ ]\n },\n \"stream\" : false,\n \"tools\" : [ ]\n}", + "ignoreArrayOrder" : true, + "ignoreExtraElements" : false + } ] + }, + "response" : { + "status" : 200, + "body" : "{\"model\":\"orca-mini\",\"created_at\":\"2025-09-05T08:40:25.87196959Z\",\"message\":{\"role\":\"assistant\",\"content\":\" Hello, Alice. How can I assist you today?\"},\"done_reason\":\"stop\",\"done\":true,\"total_duration\":707788963,\"load_duration\":5513604,\"prompt_eval_count\":46,\"prompt_eval_duration\":168871300,\"eval_count\":12,\"eval_duration\":532436800}", + "headers" : { + "Date" : "Fri, 05 Sep 2025 08:40:25 GMT", + "Content-Type" : "application/json; charset=utf-8" + } + }, + "uuid" : "90d1f0a9-0e98-46b8-95be-6a0f10fa145f", + "persistent" : true, + "scenarioName" : "scenario-2-api-chat", + "requiredScenarioState" : "Started", + "newScenarioState" : "scenario-2-api-chat-2", + "insertionIndex" : 14 +} \ No newline at end of file diff --git a/integration-tests/langchain4j-agent/src/test/resources/mappings/api_chat-97c022a9-b386-47f1-8531-3a1ce3bfa4e0.json b/integration-tests/langchain4j-agent/src/test/resources/mappings/api_chat-97c022a9-b386-47f1-8531-3a1ce3bfa4e0.json new file mode 100644 index 0000000000..af879e3cac --- /dev/null +++ b/integration-tests/langchain4j-agent/src/test/resources/mappings/api_chat-97c022a9-b386-47f1-8531-3a1ce3bfa4e0.json @@ -0,0 +1,24 @@ +{ + "id" : "97c022a9-b386-47f1-8531-3a1ce3bfa4e0", + "name" : "api_chat", + "request" : { + "url" : "/api/chat", + "method" : "POST", + "bodyPatterns" : [ { + "equalToJson" : "{\n \"model\" : \"llama3.1:latest\",\n \"messages\" : [ {\n \"role\" : \"user\",\n \"content\" : \"What is the name of user ID 123?\"\n }, {\n \"role\" : \"assistant\",\n \"tool_calls\" : [ {\n \"function\" : {\n \"name\" : \"QueryUserDatabaseByUserID\",\n \"arguments\" : {\n \"userId\" : 123\n }\n }\n } ]\n }, {\n \"role\" : \"tool\",\n \"content\" : \"{\\\"name\\\": \\\"John Doe\\\", \\\"id\\\": \\\ [...] + "ignoreArrayOrder" : true, + "ignoreExtraElements" : false + } ] + }, + "response" : { + "status" : 200, + "body" : "{\"model\":\"llama3.1:latest\",\"created_at\":\"2025-09-05T08:40:25.005956891Z\",\"message\":{\"role\":\"assistant\",\"content\":\"The name associated with user ID 123 is John Doe.\"},\"done_reason\":\"stop\",\"done\":true,\"total_duration\":2737024526,\"load_duration\":18514317,\"prompt_eval_count\":109,\"prompt_eval_duration\":1409228801,\"eval_count\":13,\"eval_duration\":1308313918}", + "headers" : { + "Date" : "Fri, 05 Sep 2025 08:40:25 GMT", + "Content-Type" : "application/json; charset=utf-8" + } + }, + "uuid" : "97c022a9-b386-47f1-8531-3a1ce3bfa4e0", + "persistent" : true, + "insertionIndex" : 15 +} \ No newline at end of file diff --git a/integration-tests/langchain4j-agent/src/test/resources/mappings/api_chat-9ae841ef-69f5-46a1-a304-0a1c2296db8c.json b/integration-tests/langchain4j-agent/src/test/resources/mappings/api_chat-9ae841ef-69f5-46a1-a304-0a1c2296db8c.json new file mode 100644 index 0000000000..046920acc0 --- /dev/null +++ b/integration-tests/langchain4j-agent/src/test/resources/mappings/api_chat-9ae841ef-69f5-46a1-a304-0a1c2296db8c.json @@ -0,0 +1,24 @@ +{ + "id" : "9ae841ef-69f5-46a1-a304-0a1c2296db8c", + "name" : "api_chat", + "request" : { + "url" : "/api/chat", + "method" : "POST", + "bodyPatterns" : [ { + "equalToJson" : "{\n \"model\" : \"llama3.1:latest\",\n \"messages\" : [ {\n \"role\" : \"user\",\n \"content\" : \"Hello - my name is Alice\"\n } ],\n \"options\" : {\n \"temperature\" : 0.3,\n \"stop\" : [ ]\n },\n \"stream\" : false,\n \"tools\" : [ ]\n}", + "ignoreArrayOrder" : true, + "ignoreExtraElements" : false + } ] + }, + "response" : { + "status" : 200, + "body" : "{\"model\":\"llama3.1:latest\",\"created_at\":\"2025-09-05T08:41:08.791258999Z\",\"message\":{\"role\":\"assistant\",\"content\":\"Nice to meet you, Alice! Is there something I can help you with or would you like to chat?\"},\"done_reason\":\"stop\",\"done\":true,\"total_duration\":2775439999,\"load_duration\":18186129,\"prompt_eval_count\":16,\"prompt_eval_duration\":367047842,\"eval_count\":23,\"eval_duration\":2389767523}", + "headers" : { + "Date" : "Fri, 05 Sep 2025 08:41:08 GMT", + "Content-Type" : "application/json; charset=utf-8" + } + }, + "uuid" : "9ae841ef-69f5-46a1-a304-0a1c2296db8c", + "persistent" : true, + "insertionIndex" : 5 +} \ No newline at end of file diff --git a/integration-tests/langchain4j-agent/src/test/resources/mappings/api_chat-a5d8bdce-dd3a-44ab-a796-001de6dba7d6.json b/integration-tests/langchain4j-agent/src/test/resources/mappings/api_chat-a5d8bdce-dd3a-44ab-a796-001de6dba7d6.json new file mode 100644 index 0000000000..f37d32a5d3 --- /dev/null +++ b/integration-tests/langchain4j-agent/src/test/resources/mappings/api_chat-a5d8bdce-dd3a-44ab-a796-001de6dba7d6.json @@ -0,0 +1,24 @@ +{ + "id" : "a5d8bdce-dd3a-44ab-a796-001de6dba7d6", + "name" : "api_chat", + "request" : { + "url" : "/api/chat", + "method" : "POST", + "bodyPatterns" : [ { + "equalToJson" : "{\n \"model\" : \"orca-mini\",\n \"messages\" : [ {\n \"role\" : \"user\",\n \"content\" : \"Return an example JSON object about a person named 'John Doe' with the fields age and description\"\n } ],\n \"options\" : {\n \"temperature\" : 0.3,\n \"stop\" : [ ]\n },\n \"stream\" : false,\n \"tools\" : [ ]\n}", + "ignoreArrayOrder" : true, + "ignoreExtraElements" : false + } ] + }, + "response" : { + "status" : 200, + "body" : "{\"model\":\"orca-mini\",\"created_at\":\"2025-09-05T08:41:02.730350164Z\",\"message\":{\"role\":\"assistant\",\"content\":\" Sure, here's an example JSON object for John Doe:\\n\\n```json\\n{\\n \\\"name\\\": \\\"John Doe\\\",\\n \\\"age\\\": 30,\\n \\\"description\\\": \\\"John Doe is a software engineer at XYZ Company. He enjoys hiking and playing video games in his free time.\\\"\\n}\\n```\"},\"done_reason\":\"stop\",\"done\":true,\"total_duration\":3896449854,\"load_du [...] + "headers" : { + "Date" : "Fri, 05 Sep 2025 08:41:02 GMT", + "Content-Type" : "application/json; charset=utf-8" + } + }, + "uuid" : "a5d8bdce-dd3a-44ab-a796-001de6dba7d6", + "persistent" : true, + "insertionIndex" : 8 +} \ No newline at end of file diff --git a/integration-tests/langchain4j-agent/src/test/resources/mappings/api_chat-df5c600e-ec18-4194-9037-7b54b6cbdfc3.json b/integration-tests/langchain4j-agent/src/test/resources/mappings/api_chat-df5c600e-ec18-4194-9037-7b54b6cbdfc3.json new file mode 100644 index 0000000000..c901891dc9 --- /dev/null +++ b/integration-tests/langchain4j-agent/src/test/resources/mappings/api_chat-df5c600e-ec18-4194-9037-7b54b6cbdfc3.json @@ -0,0 +1,24 @@ +{ + "id" : "df5c600e-ec18-4194-9037-7b54b6cbdfc3", + "name" : "api_chat", + "request" : { + "url" : "/api/chat", + "method" : "POST", + "bodyPatterns" : [ { + "equalToJson" : "{\n \"model\" : \"llama3.1:latest\",\n \"messages\" : [ {\n \"role\" : \"user\",\n \"content\" : \"Hello - my name is Alice\"\n }, {\n \"role\" : \"assistant\",\n \"content\" : \"Nice to meet you, Alice! Is there something I can help you with or would you like to chat?\",\n \"tool_calls\" : [ ]\n }, {\n \"role\" : \"user\",\n \"content\" : \"And my favorite color is blue\"\n }, {\n \"role\" : \"assistant\",\n \"content\" : \"Blue is [...] + "ignoreArrayOrder" : true, + "ignoreExtraElements" : false + } ] + }, + "response" : { + "status" : 200, + "body" : "{\"model\":\"llama3.1:latest\",\"created_at\":\"2025-09-05T08:41:16.29486056Z\",\"message\":{\"role\":\"assistant\",\"content\":\"Your name is Alice, and your favorite color is blue! (I remember!)\"},\"done_reason\":\"stop\",\"done\":true,\"total_duration\":2387529222,\"load_duration\":18340751,\"prompt_eval_count\":119,\"prompt_eval_duration\":620669949,\"eval_count\":17,\"eval_duration\":1747369140}", + "headers" : { + "Date" : "Fri, 05 Sep 2025 08:41:16 GMT", + "Content-Type" : "application/json; charset=utf-8" + } + }, + "uuid" : "df5c600e-ec18-4194-9037-7b54b6cbdfc3", + "persistent" : true, + "insertionIndex" : 3 +} \ No newline at end of file diff --git a/integration-tests/langchain4j-agent/src/test/resources/mappings/api_chat-e1d90c56-38f7-4e1f-93d0-641fd6f1c5a7.json b/integration-tests/langchain4j-agent/src/test/resources/mappings/api_chat-e1d90c56-38f7-4e1f-93d0-641fd6f1c5a7.json new file mode 100644 index 0000000000..78fa6763ad --- /dev/null +++ b/integration-tests/langchain4j-agent/src/test/resources/mappings/api_chat-e1d90c56-38f7-4e1f-93d0-641fd6f1c5a7.json @@ -0,0 +1,24 @@ +{ + "id" : "e1d90c56-38f7-4e1f-93d0-641fd6f1c5a7", + "name" : "api_chat", + "request" : { + "url" : "/api/chat", + "method" : "POST", + "bodyPatterns" : [ { + "equalToJson" : "{\n \"model\" : \"llama3.1:latest\",\n \"messages\" : [ {\n \"role\" : \"user\",\n \"content\" : \"Hello - my name is Alice\"\n }, {\n \"role\" : \"assistant\",\n \"content\" : \"Nice to meet you, Alice! Is there something I can help you with or would you like to chat?\",\n \"tool_calls\" : [ ]\n }, {\n \"role\" : \"user\",\n \"content\" : \"And my favorite color is blue\"\n } ],\n \"options\" : {\n \"temperature\" : 0.3,\n \"stop\" [...] + "ignoreArrayOrder" : true, + "ignoreExtraElements" : false + } ] + }, + "response" : { + "status" : 200, + "body" : "{\"model\":\"llama3.1:latest\",\"created_at\":\"2025-09-05T08:41:13.790159977Z\",\"message\":{\"role\":\"assistant\",\"content\":\"Blue is a wonderful color. It's so calming and serene. Do you have a particular shade of blue that you especially love - light sky blue, navy blue, or perhaps a bright cobalt blue?\"},\"done_reason\":\"stop\",\"done\":true,\"total_duration\":4900250015,\"load_duration\":18049552,\"prompt_eval_count\":54,\"prompt_eval_duration\":415571264,\"eval_ [...] + "headers" : { + "Date" : "Fri, 05 Sep 2025 08:41:13 GMT", + "Content-Type" : "application/json; charset=utf-8" + } + }, + "uuid" : "e1d90c56-38f7-4e1f-93d0-641fd6f1c5a7", + "persistent" : true, + "insertionIndex" : 4 +} \ No newline at end of file diff --git a/integration-tests/langchain4j-agent/src/test/resources/mappings/api_chat-e7aee62a-a9b2-4d51-b31f-f817bc16e013.json b/integration-tests/langchain4j-agent/src/test/resources/mappings/api_chat-e7aee62a-a9b2-4d51-b31f-f817bc16e013.json new file mode 100644 index 0000000000..2eb2ab3607 --- /dev/null +++ b/integration-tests/langchain4j-agent/src/test/resources/mappings/api_chat-e7aee62a-a9b2-4d51-b31f-f817bc16e013.json @@ -0,0 +1,24 @@ +{ + "id" : "e7aee62a-a9b2-4d51-b31f-f817bc16e013", + "name" : "api_chat", + "request" : { + "url" : "/api/chat", + "method" : "POST", + "bodyPatterns" : [ { + "equalToJson" : "{\n \"model\" : \"orca-mini\",\n \"messages\" : [ {\n \"role\" : \"user\",\n \"content\" : \"Return an example JSON object about a person named 'John Doe' with the fields name and description\"\n } ],\n \"options\" : {\n \"temperature\" : 0.3,\n \"stop\" : [ ]\n },\n \"stream\" : false,\n \"tools\" : [ ]\n}", + "ignoreArrayOrder" : true, + "ignoreExtraElements" : false + } ] + }, + "response" : { + "status" : 200, + "body" : "{\"model\":\"orca-mini\",\"created_at\":\"2025-09-05T08:40:54.652463131Z\",\"message\":{\"role\":\"assistant\",\"content\":\" Sure, here's an example JSON object for John Doe:\\n\\n```json\\n{\\n \\\"name\\\": \\\"John Doe\\\",\\n \\\"description\\\": \\\"An example person with a unique name and a short description.\\\"\\n}\\n```\"},\"done_reason\":\"stop\",\"done\":true,\"total_duration\":3088735669,\"load_duration\":5816213,\"prompt_eval_count\":60,\"prompt_eval_duration\ [...] + "headers" : { + "Date" : "Fri, 05 Sep 2025 08:40:54 GMT", + "Content-Type" : "application/json; charset=utf-8" + } + }, + "uuid" : "e7aee62a-a9b2-4d51-b31f-f817bc16e013", + "persistent" : true, + "insertionIndex" : 11 +} \ No newline at end of file diff --git a/integration-tests/langchain4j-agent/src/test/resources/mappings/api_chat-f02d99a6-9cee-4e8d-bcff-7f6fd7ad3d12.json b/integration-tests/langchain4j-agent/src/test/resources/mappings/api_chat-f02d99a6-9cee-4e8d-bcff-7f6fd7ad3d12.json new file mode 100644 index 0000000000..f51ddcba73 --- /dev/null +++ b/integration-tests/langchain4j-agent/src/test/resources/mappings/api_chat-f02d99a6-9cee-4e8d-bcff-7f6fd7ad3d12.json @@ -0,0 +1,27 @@ +{ + "id" : "f02d99a6-9cee-4e8d-bcff-7f6fd7ad3d12", + "name" : "api_chat", + "request" : { + "url" : "/api/chat", + "method" : "POST", + "bodyPatterns" : [ { + "equalToJson" : "{\n \"model\" : \"orca-mini\",\n \"messages\" : [ {\n \"role\" : \"user\",\n \"content\" : \"Make sure you return a valid JSON object following the specified format\"\n } ],\n \"options\" : {\n \"temperature\" : 0.3,\n \"stop\" : [ ]\n },\n \"stream\" : false,\n \"tools\" : [ ]\n}", + "ignoreArrayOrder" : true, + "ignoreExtraElements" : false + } ] + }, + "response" : { + "status" : 200, + "body" : "{\"model\":\"orca-mini\",\"created_at\":\"2025-09-05T08:41:04.354385748Z\",\"message\":{\"role\":\"assistant\",\"content\":\" Sure, I'd be happy to help! What is the specific format of the JSON object you would like me to generate?\"},\"done_reason\":\"stop\",\"done\":true,\"total_duration\":1509223341,\"load_duration\":5736564,\"prompt_eval_count\":52,\"prompt_eval_duration\":236008817,\"eval_count\":27,\"eval_duration\":1266202591}", + "headers" : { + "Date" : "Fri, 05 Sep 2025 08:41:04 GMT", + "Content-Type" : "application/json; charset=utf-8" + } + }, + "uuid" : "f02d99a6-9cee-4e8d-bcff-7f6fd7ad3d12", + "persistent" : true, + "scenarioName" : "scenario-3-api-chat", + "requiredScenarioState" : "Started", + "newScenarioState" : "scenario-3-api-chat-2", + "insertionIndex" : 7 +} \ No newline at end of file diff --git a/integration-tests/langchain4j-agent/src/test/resources/mappings/api_chat-fceeb039-9b61-4ac5-a8c7-71a6483ea6a7.json b/integration-tests/langchain4j-agent/src/test/resources/mappings/api_chat-fceeb039-9b61-4ac5-a8c7-71a6483ea6a7.json new file mode 100644 index 0000000000..5c3dc6018e --- /dev/null +++ b/integration-tests/langchain4j-agent/src/test/resources/mappings/api_chat-fceeb039-9b61-4ac5-a8c7-71a6483ea6a7.json @@ -0,0 +1,24 @@ +{ + "id" : "fceeb039-9b61-4ac5-a8c7-71a6483ea6a7", + "name" : "api_chat", + "request" : { + "url" : "/api/chat", + "method" : "POST", + "bodyPatterns" : [ { + "equalToJson" : "{\n \"model\" : \"llama3.1:latest\",\n \"messages\" : [ {\n \"role\" : \"user\",\n \"content\" : \"What is the name of user ID 123?\"\n } ],\n \"options\" : {\n \"temperature\" : 0.3,\n \"stop\" : [ ]\n },\n \"stream\" : false,\n \"tools\" : [ {\n \"type\" : \"function\",\n \"function\" : {\n \"name\" : \"QueryUserDatabaseByUserID\",\n \"description\" : \"Query user database by user ID\",\n \"parameters\" : {\n \"type\ [...] + "ignoreArrayOrder" : true, + "ignoreExtraElements" : false + } ] + }, + "response" : { + "status" : 200, + "body" : "{\"model\":\"llama3.1:latest\",\"created_at\":\"2025-09-05T08:40:22.217978108Z\",\"message\":{\"role\":\"assistant\",\"content\":\"\",\"tool_calls\":[{\"function\":{\"name\":\"QueryUserDatabaseByUserID\",\"arguments\":{\"userId\":123}}}]},\"done_reason\":\"stop\",\"done\":true,\"total_duration\":8921529931,\"load_duration\":2746310489,\"prompt_eval_count\":166,\"prompt_eval_duration\":4091169742,\"eval_count\":20,\"eval_duration\":2083023029}", + "headers" : { + "Date" : "Fri, 05 Sep 2025 08:40:22 GMT", + "Content-Type" : "application/json; charset=utf-8" + } + }, + "uuid" : "fceeb039-9b61-4ac5-a8c7-71a6483ea6a7", + "persistent" : true, + "insertionIndex" : 16 +} \ No newline at end of file diff --git a/integration-tests/langchain4j-agent/src/test/resources/mappings/api_chat-fe594db1-b82f-4216-a1fc-0cfdddc1b33a.json b/integration-tests/langchain4j-agent/src/test/resources/mappings/api_chat-fe594db1-b82f-4216-a1fc-0cfdddc1b33a.json new file mode 100644 index 0000000000..08c28a1728 --- /dev/null +++ b/integration-tests/langchain4j-agent/src/test/resources/mappings/api_chat-fe594db1-b82f-4216-a1fc-0cfdddc1b33a.json @@ -0,0 +1,24 @@ +{ + "id" : "fe594db1-b82f-4216-a1fc-0cfdddc1b33a", + "name" : "api_chat", + "request" : { + "url" : "/api/chat", + "method" : "POST", + "bodyPatterns" : [ { + "equalToJson" : "{\n \"model\" : \"orca-mini\",\n \"messages\" : [ {\n \"role\" : \"user\",\n \"content\" : \"What is Apache Camel?\"\n } ],\n \"options\" : {\n \"temperature\" : 0.3,\n \"stop\" : [ ]\n },\n \"stream\" : false,\n \"tools\" : [ ]\n}", + "ignoreArrayOrder" : true, + "ignoreExtraElements" : false + } ] + }, + "response" : { + "status" : 200, + "body" : "{\"model\":\"orca-mini\",\"created_at\":\"2025-09-05T08:40:29.803949147Z\",\"message\":{\"role\":\"assistant\",\"content\":\" Apache Camel is an open-source project that provides a framework for building and managing robust, distributed, and fault-tolerant systems. It allows developers to easily build and manage complex workflows between different systems, applications, and data sources. The main use cases for Apache Camel include routing, serialization/deserialization, and [...] + "headers" : { + "Date" : "Fri, 05 Sep 2025 08:40:29 GMT", + "Content-Type" : "application/json; charset=utf-8" + } + }, + "uuid" : "fe594db1-b82f-4216-a1fc-0cfdddc1b33a", + "persistent" : true, + "insertionIndex" : 13 +} \ No newline at end of file diff --git a/integration-tests/langchain4j-agent/src/test/resources/mappings/api_embed-cc8902fc-b699-41e9-9f80-b50423fbf24c.json b/integration-tests/langchain4j-agent/src/test/resources/mappings/api_embed-cc8902fc-b699-41e9-9f80-b50423fbf24c.json new file mode 100644 index 0000000000..5d8834a132 --- /dev/null +++ b/integration-tests/langchain4j-agent/src/test/resources/mappings/api_embed-cc8902fc-b699-41e9-9f80-b50423fbf24c.json @@ -0,0 +1,24 @@ +{ + "id" : "cc8902fc-b699-41e9-9f80-b50423fbf24c", + "name" : "api_embed", + "request" : { + "url" : "/api/embed", + "method" : "POST", + "bodyPatterns" : [ { + "equalToJson" : "{\n \"model\" : \"nomic-embed-text\",\n \"input\" : [ \"Miles of Camels Car Rental - Company Information\\n\\nBUSINESS HOURS:\\nMonday-Friday: 8:00 AM - 6:00 PM\\nSaturday: 9:00 AM - 4:00 PM\\nSunday: Closed\\n\\nRENTAL AGREEMENT\\n- This agreement is between Miles of Camels Car Rental (\\\"Company\\\") and the customer (\\\"Renter\\\").\", \"RENTAL POLICIES:\\n- Minimum age: 21 years old\\n- Valid driver's license required\\n- Credit card required for security d [...] + "ignoreArrayOrder" : true, + "ignoreExtraElements" : true + } ] + }, + "response" : { + "status" : 200, + "bodyFileName" : "api_embed-cc8902fc-b699-41e9-9f80-b50423fbf24c.json", + "headers" : { + "Date" : "Fri, 05 Sep 2025 08:39:58 GMT", + "Content-Type" : "application/json; charset=utf-8" + } + }, + "uuid" : "cc8902fc-b699-41e9-9f80-b50423fbf24c", + "persistent" : true, + "insertionIndex" : 19 +} \ No newline at end of file diff --git a/integration-tests/langchain4j-agent/src/test/resources/mappings/api_embed-f208155e-5af8-4c4c-802d-8b2f5be5cb34.json b/integration-tests/langchain4j-agent/src/test/resources/mappings/api_embed-f208155e-5af8-4c4c-802d-8b2f5be5cb34.json new file mode 100644 index 0000000000..dfd4323943 --- /dev/null +++ b/integration-tests/langchain4j-agent/src/test/resources/mappings/api_embed-f208155e-5af8-4c4c-802d-8b2f5be5cb34.json @@ -0,0 +1,24 @@ +{ + "id" : "f208155e-5af8-4c4c-802d-8b2f5be5cb34", + "name" : "api_embed", + "request" : { + "url" : "/api/embed", + "method" : "POST", + "bodyPatterns" : [ { + "equalToJson" : "{\n \"model\" : \"nomic-embed-text\",\n \"input\" : [ \"Describe the Miles of Camels Car Rental cancellations policy for cancelling 24 hours before pickup. What is the refund amount?\" ]\n}", + "ignoreArrayOrder" : true, + "ignoreExtraElements" : false + } ] + }, + "response" : { + "status" : 200, + "body" : "{\"model\":\"nomic-embed-text\",\"embeddings\":[[-0.000546151,0.032465357,-0.1832982,-0.006578118,0.019952673,-0.0018667525,0.014617178,0.020668428,0.0038082553,0.018162793,-0.034445435,-0.011487866,0.039282184,-0.024556652,0.09539182,-0.02630365,0.0276438,-0.022805816,-0.04179049,0.028880822,0.006367407,-0.0022209072,-0.0902023,-0.013760107,0.022725942,0.025699575,0.052584995,0.035527404,-0.025857603,0.017433418,0.06672366,0.022350645,0.009822153,-0.07828854,-0.12301919,0. [...] + "headers" : { + "Date" : "Fri, 05 Sep 2025 08:39:59 GMT", + "Content-Type" : "application/json; charset=utf-8" + } + }, + "uuid" : "f208155e-5af8-4c4c-802d-8b2f5be5cb34", + "persistent" : true, + "insertionIndex" : 18 +} \ No newline at end of file diff --git a/integration-tests/langchain4j-chat/pom.xml b/integration-tests/langchain4j-chat/pom.xml index 7007e417e9..5f14db1f4b 100644 --- a/integration-tests/langchain4j-chat/pom.xml +++ b/integration-tests/langchain4j-chat/pom.xml @@ -123,19 +123,6 @@ </exclusion> </exclusions> </dependency> - <dependency> - <groupId>org.apache.camel.quarkus</groupId> - <artifactId>camel-quarkus-support-retrofit-deployment</artifactId> - <version>${project.version}</version> - <type>pom</type> - <scope>test</scope> - <exclusions> - <exclusion> - <groupId>*</groupId> - <artifactId>*</artifactId> - </exclusion> - </exclusions> - </dependency> </dependencies> </profile> <profile> diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 184bd89b52..00806e905b 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -148,6 +148,7 @@ <module>knative</module> <module>kubernetes</module> <module>kudu</module> + <module>langchain4j-agent</module> <module>langchain4j-chat</module> <module>langchain4j-tokenizer</module> <module>langchain4j-tools</module> diff --git a/tooling/scripts/test-categories.yaml b/tooling/scripts/test-categories.yaml index a01f7451ce..9d6d72d07a 100644 --- a/tooling/scripts/test-categories.yaml +++ b/tooling/scripts/test-categories.yaml @@ -44,6 +44,7 @@ group-02: - jackson-protobuf - jfr - kafka-oauth + - langchain4j-agent - oaipmh - pubnub - protobuf
