Skip to content

Commit 3d9688b

Browse files
Add langchain4j-agent native support
1 parent 48dd221 commit 3d9688b

File tree

61 files changed

+1965
-167
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+1965
-167
lines changed

docs/modules/ROOT/examples/components/langchain4j-agent.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
# This file was generated by camel-quarkus-maven-plugin:update-extension-doc-page
33
cqArtifactId: camel-quarkus-langchain4j-agent
44
cqArtifactIdBase: langchain4j-agent
5-
cqNativeSupported: false
6-
cqStatus: Preview
5+
cqNativeSupported: true
6+
cqStatus: Stable
77
cqDeprecated: false
88
cqJvmSince: 3.26.0
9-
cqNativeSince: n/a
9+
cqNativeSince: 3.27.0
1010
cqCamelPartName: langchain4j-agent
1111
cqCamelPartTitle: LangChain4j Agent
1212
cqCamelPartDescription: LangChain4j Agent component

docs/modules/ROOT/pages/reference/extensions/langchain4j-agent.adoc

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,17 @@
44
= LangChain4j Agent
55
:linkattrs:
66
:cq-artifact-id: camel-quarkus-langchain4j-agent
7-
:cq-native-supported: false
7+
:cq-native-supported: true
88
:cq-status: Preview
99
:cq-status-deprecation: Preview
1010
:cq-description: LangChain4j Agent component
1111
:cq-deprecated: false
1212
:cq-jvm-since: 3.26.0
13-
:cq-native-since: n/a
13+
:cq-native-since: 3.27.0
1414

1515
ifeval::[{doc-show-badges} == true]
1616
[.badges]
17-
[.badge-key]##JVM since##[.badge-supported]##3.26.0## [.badge-key]##Native##[.badge-unsupported]##unsupported##
17+
[.badge-key]##JVM since##[.badge-supported]##3.26.0## [.badge-key]##Native since##[.badge-supported]##3.27.0##
1818
endif::[]
1919

2020
LangChain4j Agent component
@@ -29,6 +29,10 @@ Please refer to the above link for usage and configuration details.
2929
[id="extensions-langchain4j-agent-maven-coordinates"]
3030
== Maven coordinates
3131

32+
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"]
33+
34+
Or add the coordinates to your existing project:
35+
3236
[source,xml]
3337
----
3438
<dependency>

extensions-jvm/pom.xml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,6 @@
7373
<module>jooq</module>
7474
<module>json-patch</module>
7575
<module>jsonapi</module>
76-
<module>langchain4j-agent</module>
7776
<module>langchain4j-embeddings</module>
7877
<module>ldif</module>
7978
<module>lucene</module>

extensions-support/langchain4j/deployment/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/deployment/SupportLangchain4jProcessor.java

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

19+
import java.util.Collection;
20+
import java.util.HashSet;
1921
import java.util.Set;
2022
import java.util.stream.Collectors;
23+
import java.util.stream.Stream;
2124

25+
import com.fasterxml.jackson.databind.JsonDeserializer;
26+
import com.fasterxml.jackson.databind.JsonSerializer;
27+
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
28+
import dev.langchain4j.guardrail.InputGuardrail;
29+
import dev.langchain4j.guardrail.JsonExtractorOutputGuardrail;
30+
import dev.langchain4j.guardrail.OutputGuardrail;
31+
import dev.langchain4j.service.MemoryId;
32+
import dev.langchain4j.service.SystemMessage;
33+
import dev.langchain4j.service.UserMessage;
34+
import dev.langchain4j.service.V;
35+
import io.quarkus.bootstrap.model.ApplicationModel;
2236
import io.quarkus.deployment.annotations.BuildProducer;
2337
import io.quarkus.deployment.annotations.BuildStep;
38+
import io.quarkus.deployment.annotations.BuildSteps;
2439
import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
2540
import io.quarkus.deployment.builditem.IndexDependencyBuildItem;
41+
import io.quarkus.deployment.builditem.nativeimage.NativeImageProxyDefinitionBuildItem;
42+
import io.quarkus.deployment.builditem.nativeimage.NativeImageResourcePatternsBuildItem;
2643
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
2744
import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem;
2845
import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem;
46+
import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem;
47+
import io.quarkus.deployment.pkg.steps.NativeOrNativeSourcesBuild;
48+
import io.quarkus.maven.dependency.ResolvedDependency;
49+
import opennlp.tools.sentdetect.SentenceDetectorFactory;
50+
import org.jboss.jandex.AnnotationInstance;
51+
import org.jboss.jandex.AnnotationTarget;
2952
import org.jboss.jandex.ClassInfo;
3053
import org.jboss.jandex.DotName;
54+
import org.jboss.jandex.IndexView;
55+
import org.jboss.jandex.MethodInfo;
56+
import org.jboss.jandex.MethodParameterInfo;
57+
import org.jboss.jandex.Type;
3158

59+
@BuildSteps(onlyIf = NativeOrNativeSourcesBuild.class)
3260
class SupportLangchain4jProcessor {
61+
private static final Class<?>[] AI_SERVICE_ANNOTATION_CLASSES = {
62+
MemoryId.class,
63+
SystemMessage.class,
64+
UserMessage.class,
65+
V.class
66+
};
3367

3468
@BuildStep
35-
void indexDependencies(BuildProducer<IndexDependencyBuildItem> indexedDependencies) {
36-
indexedDependencies.produce(new IndexDependencyBuildItem("dev.langchain4j", "langchain4j-http-client-jdk"));
37-
indexedDependencies.produce(new IndexDependencyBuildItem("dev.langchain4j", "langchain4j-ollama"));
69+
void indexDependencies(CurateOutcomeBuildItem curateOutcome, BuildProducer<IndexDependencyBuildItem> indexedDependencies) {
70+
ApplicationModel applicationModel = curateOutcome.getApplicationModel();
71+
for (ResolvedDependency dependency : applicationModel.getDependencies()) {
72+
if (dependency.getGroupId().equals("dev.langchain4j")) {
73+
indexedDependencies.produce(new IndexDependencyBuildItem(dependency.getGroupId(), dependency.getArtifactId()));
74+
}
75+
}
3876
}
3977

4078
@BuildStep
@@ -43,24 +81,131 @@ ServiceProviderBuildItem registerServiceProviders() {
4381
}
4482

4583
@BuildStep
46-
void registerForReflection(CombinedIndexBuildItem combinedIndex, BuildProducer<ReflectiveClassBuildItem> reflectiveClass) {
47-
Set<String> ollamaModelClasses = combinedIndex.getIndex()
48-
.getClassesInPackage("dev.langchain4j.model.ollama")
49-
.stream()
84+
void registerLangChain4jJacksonTypesForReflection(
85+
CombinedIndexBuildItem combinedIndex,
86+
BuildProducer<ReflectiveClassBuildItem> reflectiveClass) {
87+
IndexView index = combinedIndex.getIndex();
88+
89+
// Discover all LangChain4j Jackson model types
90+
Set<String> langChain4jModelClasses = langChain4jTypesStream(index.getKnownClasses())
5091
.filter(classInfo -> classInfo.annotations().stream()
5192
.anyMatch(annotationInstance -> annotationInstance.name().toString()
5293
.startsWith("com.fasterxml.jackson.annotation")))
5394
.map(ClassInfo::name)
5495
.map(DotName::toString)
5596
.collect(Collectors.toSet());
5697

57-
reflectiveClass.produce(ReflectiveClassBuildItem.builder(ollamaModelClasses.toArray(new String[0]))
98+
reflectiveClass.produce(ReflectiveClassBuildItem.builder(langChain4jModelClasses.toArray(new String[0]))
5899
.methods(true)
59100
.build());
101+
102+
// Discover all LangChain4j Jackson serializer / deserializer types
103+
Set<String> jacksonSupportClasses = langChain4jTypesStream(index.getAllKnownSubclasses(JsonSerializer.class))
104+
.map(classInfo -> classInfo.name().toString())
105+
.collect(Collectors.toSet());
106+
107+
langChain4jTypesStream(index.getAllKnownSubclasses(JsonDeserializer.class))
108+
.map(classInfo -> classInfo.name().toString())
109+
.forEach(jacksonSupportClasses::add);
110+
111+
reflectiveClass.produce(ReflectiveClassBuildItem.builder(jacksonSupportClasses.toArray(new String[0])).build());
112+
113+
// Misc Jackson support
114+
ReflectiveClassBuildItem.builder(PropertyNamingStrategies.SnakeCaseStrategy.class).build();
115+
}
116+
117+
@BuildStep
118+
void registerLangChain4jAiServiceTypesForReflection(
119+
CombinedIndexBuildItem combinedIndex,
120+
BuildProducer<ReflectiveClassBuildItem> reflectiveClass,
121+
BuildProducer<NativeImageProxyDefinitionBuildItem> nativeImageProxy) {
122+
123+
IndexView index = combinedIndex.getIndex();
124+
Set<String> aiServiceInterfaces = new HashSet<>();
125+
Set<String> aiServiceTypes = new HashSet<>();
126+
127+
for (Class<?> aiServiceClass : AI_SERVICE_ANNOTATION_CLASSES) {
128+
for (AnnotationInstance annotationInstance : index.getAnnotations(aiServiceClass)) {
129+
AnnotationTarget annotationTarget = annotationInstance.target();
130+
131+
if (annotationTarget.kind().equals(AnnotationTarget.Kind.CLASS)) {
132+
aiServiceInterfaces.add(annotationTarget.asClass().name().toString());
133+
} else if (annotationTarget.kind().equals(AnnotationTarget.Kind.METHOD)) {
134+
MethodInfo method = annotationTarget.asMethod();
135+
aiServiceInterfaces.add(method.declaringClass().name().toString());
136+
if (!method.returnType().kind().equals(Type.Kind.VOID)) {
137+
aiServiceTypes.add(method.returnType().name().toString());
138+
}
139+
} else if (annotationTarget.kind().equals(AnnotationTarget.Kind.METHOD_PARAMETER)) {
140+
MethodParameterInfo methodParameter = annotationTarget.asMethodParameter();
141+
aiServiceTypes.add(methodParameter.type().name().toString());
142+
143+
MethodInfo method = methodParameter.method();
144+
aiServiceInterfaces.add(method.declaringClass().name().toString());
145+
if (!method.returnType().kind().equals(Type.Kind.VOID)) {
146+
aiServiceTypes.add(method.returnType().name().toString());
147+
}
148+
}
149+
}
150+
}
151+
152+
// Any types participating in JsonExtractorOutputGuardrail operations require reflection
153+
index.getAllKnownSubclasses(JsonExtractorOutputGuardrail.class)
154+
.stream()
155+
.filter(classInfo -> classInfo.superClassType() != null)
156+
.filter(classInfo -> classInfo.superClassType().kind().equals(Type.Kind.PARAMETERIZED_TYPE))
157+
.map(ClassInfo::superClassType)
158+
.map(Type::asParameterizedType)
159+
.flatMap(type -> type.arguments().stream())
160+
.findFirst()
161+
.ifPresent(typeParameter -> {
162+
aiServiceTypes.add(typeParameter.name().toString());
163+
});
164+
165+
// AI service interfaces must be registered as native image proxies
166+
aiServiceInterfaces
167+
.stream()
168+
.map(NativeImageProxyDefinitionBuildItem::new)
169+
.forEach(nativeImageProxy::produce);
170+
171+
// Register any types related to the AI service for reflection
172+
reflectiveClass.produce(ReflectiveClassBuildItem.builder(aiServiceTypes.toArray(new String[0]))
173+
.methods()
174+
.build());
175+
176+
// Guardrails are instantiated dynamically
177+
Set<String> guardrailTypes = index.getAllKnownImplementations(InputGuardrail.class)
178+
.stream()
179+
.map(classInfo -> classInfo.name().toString())
180+
.collect(Collectors.toSet());
181+
182+
index.getAllKnownImplementations(OutputGuardrail.class)
183+
.stream()
184+
.map(classInfo -> classInfo.name().toString())
185+
.forEach(guardrailTypes::add);
186+
187+
reflectiveClass.produce(ReflectiveClassBuildItem.builder(guardrailTypes.toArray(new String[0])).build());
188+
}
189+
190+
@BuildStep
191+
void registerLangChain4jNlpTypesForReflection(BuildProducer<ReflectiveClassBuildItem> reflectiveClass) {
192+
reflectiveClass.produce(ReflectiveClassBuildItem.builder(SentenceDetectorFactory.class).build());
60193
}
61194

62195
@BuildStep
63196
RuntimeInitializedClassBuildItem runtimeInitializedClasses() {
64197
return new RuntimeInitializedClassBuildItem("dev.langchain4j.internal.RetryUtils");
65198
}
199+
200+
@BuildStep
201+
NativeImageResourcePatternsBuildItem nativeImageResources() {
202+
return NativeImageResourcePatternsBuildItem.builder()
203+
.includeGlob("opennlp/*.bin")
204+
.build();
205+
}
206+
207+
static Stream<ClassInfo> langChain4jTypesStream(Collection<ClassInfo> classes) {
208+
return classes.stream()
209+
.filter(classInfo -> classInfo.name().toString().startsWith("dev.langchain4j"));
210+
}
66211
}

extensions-support/langchain4j/runtime/pom.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@
3838
<groupId>io.quarkus</groupId>
3939
<artifactId>quarkus-jackson</artifactId>
4040
</dependency>
41+
<dependency>
42+
<groupId>dev.langchain4j</groupId>
43+
<artifactId>langchain4j</artifactId>
44+
</dependency>
45+
<dependency>
46+
<groupId>dev.langchain4j</groupId>
47+
<artifactId>langchain4j-core</artifactId>
48+
</dependency>
4149
</dependencies>
4250

4351
<build>
Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,30 +17,13 @@
1717
package org.apache.camel.quarkus.component.langchain4j.agent.deployment;
1818

1919
import io.quarkus.deployment.annotations.BuildStep;
20-
import io.quarkus.deployment.annotations.ExecutionTime;
21-
import io.quarkus.deployment.annotations.Record;
2220
import io.quarkus.deployment.builditem.FeatureBuildItem;
23-
import io.quarkus.deployment.pkg.steps.NativeOrNativeSourcesBuild;
24-
import org.apache.camel.quarkus.core.JvmOnlyRecorder;
25-
import org.jboss.logging.Logger;
2621

2722
class Langchain4jAgentProcessor {
28-
29-
private static final Logger LOG = Logger.getLogger(Langchain4jAgentProcessor.class);
3023
private static final String FEATURE = "camel-langchain4j-agent";
3124

3225
@BuildStep
3326
FeatureBuildItem feature() {
3427
return new FeatureBuildItem(FEATURE);
3528
}
36-
37-
/**
38-
* Remove this once this extension starts supporting the native mode.
39-
*/
40-
@BuildStep(onlyIf = NativeOrNativeSourcesBuild.class)
41-
@Record(value = ExecutionTime.RUNTIME_INIT)
42-
void warnJvmInNative(JvmOnlyRecorder recorder) {
43-
JvmOnlyRecorder.warnJvmInNative(LOG, FEATURE); // warn at build time
44-
recorder.warnJvmInNative(FEATURE); // warn at runtime
45-
}
4629
}

extensions-jvm/langchain4j-agent/pom.xml renamed to extensions/langchain4j-agent/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
<modelVersion>4.0.0</modelVersion>
2222
<parent>
2323
<groupId>org.apache.camel.quarkus</groupId>
24-
<artifactId>camel-quarkus-extensions-jvm</artifactId>
24+
<artifactId>camel-quarkus-extensions</artifactId>
2525
<version>3.27.0-SNAPSHOT</version>
2626
<relativePath>../pom.xml</relativePath>
2727
</parent>

extensions-jvm/langchain4j-agent/runtime/pom.xml renamed to extensions/langchain4j-agent/runtime/pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
<properties>
3434
<camel.quarkus.jvmSince>3.26.0</camel.quarkus.jvmSince>
3535
<quarkus.metadata.status>preview</quarkus.metadata.status>
36+
<camel.quarkus.nativeSince>3.27.0</camel.quarkus.nativeSince>
3637
</properties>
3738

3839
<dependencies>

extensions-jvm/langchain4j-agent/runtime/src/main/resources/META-INF/quarkus-extension.yaml renamed to extensions/langchain4j-agent/runtime/src/main/resources/META-INF/quarkus-extension.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ description: "LangChain4j Agent component"
2626
metadata:
2727
icon-url: "https://raw.githubusercontent.com/apache/camel-website/main/antora-ui-camel/src/img/logo-d.svg"
2828
sponsor: "Apache Software Foundation"
29-
unlisted: true
3029
guide: "https://camel.apache.org/camel-quarkus/latest/reference/extensions/langchain4j-agent.html"
3130
categories:
3231
- "integration"

0 commit comments

Comments
 (0)