Skip to content

Commit f2b71b4

Browse files
committed
feat(langchain4j): add Jooby extension for LangChain4j with resilient failover
- Implemented `LangChain4jModule` to automatically parse `application.conf` and register both `ChatModel` and `StreamingChatModel` in the service registry. - Added `BuiltInModel` enum for zero-boilerplate configuration of OpenAI, Anthropic, Ollama, and Jlama. - Created `ChatModelFactory` interface to allow users to register custom model providers. - Implemented `FallbackChatModel` and `FallbackStreamingChatModel` decorators for seamless failover routing. - Added `FailoverListener` hook for custom metrics/logging without forcing a Micrometer dependency. - Configured BOM management and marked heavy provider SDKs as optional in `pom.xml`. - Added comprehensive unit tests and Javadoc. - fix #3880
1 parent ae7db65 commit f2b71b4

File tree

14 files changed

+1138
-1
lines changed

14 files changed

+1138
-1
lines changed

modules/jooby-langchain4j/pom.xml

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
3+
4+
<modelVersion>4.0.0</modelVersion>
5+
6+
<parent>
7+
<groupId>io.jooby</groupId>
8+
<artifactId>modules</artifactId>
9+
<version>4.0.17-SNAPSHOT</version>
10+
</parent>
11+
<artifactId>jooby-langchain4j</artifactId>
12+
<name>jooby-langchain4j</name>
13+
14+
<dependencies>
15+
<dependency>
16+
<groupId>io.jooby</groupId>
17+
<artifactId>jooby</artifactId>
18+
<version>${jooby.version}</version>
19+
</dependency>
20+
21+
<dependency>
22+
<groupId>dev.langchain4j</groupId>
23+
<artifactId>langchain4j-core</artifactId>
24+
</dependency>
25+
26+
<dependency>
27+
<groupId>dev.langchain4j</groupId>
28+
<artifactId>langchain4j-open-ai</artifactId>
29+
<optional>true</optional>
30+
</dependency>
31+
32+
<dependency>
33+
<groupId>dev.langchain4j</groupId>
34+
<artifactId>langchain4j-anthropic</artifactId>
35+
<optional>true</optional>
36+
</dependency>
37+
38+
<dependency>
39+
<groupId>dev.langchain4j</groupId>
40+
<artifactId>langchain4j-ollama</artifactId>
41+
<optional>true</optional>
42+
</dependency>
43+
44+
<dependency>
45+
<groupId>dev.langchain4j</groupId>
46+
<artifactId>langchain4j-jlama</artifactId>
47+
<optional>true</optional>
48+
</dependency>
49+
50+
<!-- Test dependencies -->
51+
<dependency>
52+
<groupId>org.junit.jupiter</groupId>
53+
<artifactId>junit-jupiter-engine</artifactId>
54+
<scope>test</scope>
55+
</dependency>
56+
57+
<dependency>
58+
<groupId>org.mockito</groupId>
59+
<artifactId>mockito-core</artifactId>
60+
<scope>test</scope>
61+
</dependency>
62+
63+
<dependency>
64+
<groupId>org.jacoco</groupId>
65+
<artifactId>org.jacoco.agent</artifactId>
66+
<classifier>runtime</classifier>
67+
<scope>test</scope>
68+
</dependency>
69+
</dependencies>
70+
71+
<dependencyManagement>
72+
<dependencies>
73+
<dependency>
74+
<groupId>dev.langchain4j</groupId>
75+
<artifactId>langchain4j-bom</artifactId>
76+
<version>1.12.2</version>
77+
<type>pom</type>
78+
<scope>import</scope>
79+
</dependency>
80+
</dependencies>
81+
</dependencyManagement>
82+
</project>
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
/*
2+
* Jooby https://jooby.io
3+
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
4+
* Copyright 2014 Edgar Espina
5+
*/
6+
package io.jooby.internal.langchain4j;
7+
8+
import java.nio.file.Paths;
9+
import java.time.Duration;
10+
11+
import com.typesafe.config.Config;
12+
import dev.langchain4j.model.anthropic.AnthropicChatModel;
13+
import dev.langchain4j.model.anthropic.AnthropicStreamingChatModel;
14+
import dev.langchain4j.model.chat.ChatModel;
15+
import dev.langchain4j.model.chat.StreamingChatModel;
16+
import dev.langchain4j.model.jlama.JlamaChatModel;
17+
import dev.langchain4j.model.jlama.JlamaStreamingChatModel;
18+
import dev.langchain4j.model.ollama.OllamaChatModel;
19+
import dev.langchain4j.model.ollama.OllamaStreamingChatModel;
20+
import dev.langchain4j.model.openai.OpenAiChatModel;
21+
import dev.langchain4j.model.openai.OpenAiStreamingChatModel;
22+
import edu.umd.cs.findbugs.annotations.NonNull;
23+
import io.jooby.langchain4j.ChatModelFactory;
24+
25+
/**
26+
* Enumeration of built-in LangChain4j model providers supported by the Jooby extension. Each
27+
* constant implements {@link ChatModelFactory} to provide provider-specific instantiation logic.
28+
*/
29+
public enum BuiltInModel implements ChatModelFactory {
30+
OPENAI {
31+
@Override
32+
public ChatModel createChatModel(@NonNull Config config) {
33+
check("dev.langchain4j.model.openai.OpenAiChatModel", "langchain4j-open-ai");
34+
return OpenAiChatModel.builder()
35+
.apiKey(config.getString("api-key"))
36+
.modelName(config.hasPath("model-name") ? config.getString("model-name") : "gpt-4o-mini")
37+
.timeout(getTimeout(config, Duration.ofSeconds(60)))
38+
.temperature(getTemp(config))
39+
.build();
40+
}
41+
42+
@Override
43+
public StreamingChatModel createStreamingModel(@NonNull Config config) {
44+
return OpenAiStreamingChatModel.builder()
45+
.apiKey(config.getString("api-key"))
46+
.modelName(config.hasPath("model-name") ? config.getString("model-name") : "gpt-4o-mini")
47+
.timeout(getStreamTimeout(config))
48+
.temperature(getTemp(config))
49+
.build();
50+
}
51+
},
52+
53+
ANTHROPIC {
54+
@Override
55+
public ChatModel createChatModel(@NonNull Config config) {
56+
check("dev.langchain4j.model.anthropic.AnthropicChatModel", "langchain4j-anthropic");
57+
return AnthropicChatModel.builder()
58+
.apiKey(config.getString("api-key"))
59+
.modelName(
60+
config.hasPath("model-name")
61+
? config.getString("model-name")
62+
: "claude-3-5-sonnet-latest")
63+
.timeout(getTimeout(config, Duration.ofSeconds(60)))
64+
.temperature(getTemp(config))
65+
.build();
66+
}
67+
68+
@Override
69+
public StreamingChatModel createStreamingModel(@NonNull Config config) {
70+
return AnthropicStreamingChatModel.builder()
71+
.apiKey(config.getString("api-key"))
72+
.modelName(
73+
config.hasPath("model-name")
74+
? config.getString("model-name")
75+
: "claude-3-5-sonnet-latest")
76+
.timeout(getStreamTimeout(config))
77+
.temperature(getTemp(config))
78+
.build();
79+
}
80+
},
81+
82+
OLLAMA {
83+
@Override
84+
public ChatModel createChatModel(@NonNull Config config) {
85+
check("dev.langchain4j.model.ollama.OllamaChatModel", "langchain4j-ollama");
86+
return OllamaChatModel.builder()
87+
.baseUrl(config.getString("base-url"))
88+
.modelName(config.getString("model-name"))
89+
.timeout(getTimeout(config, Duration.ofSeconds(60)))
90+
.build();
91+
}
92+
93+
@Override
94+
public StreamingChatModel createStreamingModel(@NonNull Config config) {
95+
return OllamaStreamingChatModel.builder()
96+
.baseUrl(config.getString("base-url"))
97+
.modelName(config.getString("model-name"))
98+
.timeout(getStreamTimeout(config))
99+
.build();
100+
}
101+
},
102+
103+
JLAMA {
104+
@Override
105+
public ChatModel createChatModel(@NonNull Config config) {
106+
check("dev.langchain4j.model.jlama.JlamaChatModel", "langchain4j-jlama");
107+
return JlamaChatModel.builder()
108+
.modelName(config.getString("model-name"))
109+
.workingDirectory(
110+
config.hasPath("working-dir")
111+
? Paths.get(config.getString("working-dir"))
112+
: Paths.get(System.getProperty("user.dir"), "./models"))
113+
.build();
114+
}
115+
116+
@Override
117+
public StreamingChatModel createStreamingModel(@NonNull Config config) {
118+
return JlamaStreamingChatModel.builder()
119+
.modelName(config.getString("model-name"))
120+
.workingDirectory(
121+
config.hasPath("working-dir")
122+
? Paths.get(config.getString("working-dir"))
123+
: Paths.get(System.getProperty("user.dir"), "./models"))
124+
.build();
125+
}
126+
};
127+
128+
/**
129+
* Resolves a built-in provider by name.
130+
*
131+
* @param name The provider name (e.g. "openai").
132+
* @return The corresponding enum constant.
133+
* @throws IllegalArgumentException if provider is unknown.
134+
*/
135+
public static BuiltInModel resolve(String name) {
136+
try {
137+
return valueOf(name.toUpperCase());
138+
} catch (IllegalArgumentException e) {
139+
throw new IllegalArgumentException("Unsupported LangChain4j provider: " + name);
140+
}
141+
}
142+
143+
// --- Helper Methods for Enum Implementation ---
144+
145+
protected void check(String className, String artifact) {
146+
try {
147+
Class.forName(className);
148+
} catch (ClassNotFoundException e) {
149+
throw new IllegalStateException(
150+
"Provider dependency missing. Add 'dev.langchain4j:" + artifact + "' to your project.");
151+
}
152+
}
153+
154+
protected Duration getTimeout(Config config, Duration defaultValue) {
155+
return config.hasPath("timeout") ? config.getDuration("timeout") : defaultValue;
156+
}
157+
158+
protected Duration getStreamTimeout(Config config) {
159+
return config.hasPath("streaming-timeout")
160+
? config.getDuration("streaming-timeout")
161+
: Duration.ofSeconds(10);
162+
}
163+
164+
protected double getTemp(Config config) {
165+
return config.hasPath("temperature") ? config.getDouble("temperature") : 0.7;
166+
}
167+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Jooby https://jooby.io
3+
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
4+
* Copyright 2014 Edgar Espina
5+
*/
6+
package io.jooby.langchain4j;
7+
8+
import com.typesafe.config.Config;
9+
import dev.langchain4j.model.chat.ChatModel;
10+
import dev.langchain4j.model.chat.StreamingChatModel;
11+
import edu.umd.cs.findbugs.annotations.NonNull;
12+
import edu.umd.cs.findbugs.annotations.Nullable;
13+
14+
/**
15+
* Factory contract for creating LangChain4j chat models from Jooby configuration. Implementations
16+
* map {@link Config} keys to specific model builder methods.
17+
*
18+
* @author edgar
19+
* @since 1.0.0
20+
*/
21+
public interface ChatModelFactory {
22+
23+
/**
24+
* Creates a blocking chat model.
25+
*
26+
* @param config The configuration block for this model.
27+
* @return A non-null instance of a {@link ChatModel}.
28+
*/
29+
ChatModel createChatModel(@NonNull Config config);
30+
31+
/**
32+
* Creates a streaming chat model. Returns {@code null} if the provider does not support
33+
* streaming.
34+
*
35+
* @param config The configuration block for this model.
36+
* @return A {@link StreamingChatModel} or {@code null}.
37+
*/
38+
@Nullable default StreamingChatModel createStreamingModel(@NonNull Config config) {
39+
return null;
40+
}
41+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*
2+
* Jooby https://jooby.io
3+
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
4+
* Copyright 2014 Edgar Espina
5+
*/
6+
package io.jooby.langchain4j;
7+
8+
/** Listener for failover events in a model chain. */
9+
@FunctionalInterface
10+
public interface FailoverListener {
11+
/**
12+
* Called when a primary model fails and the system switches to a fallback.
13+
*
14+
* @param modelName The name of the model that failed.
15+
* @param error The exception that triggered the fallback.
16+
*/
17+
void onFailover(String modelName, Throwable error);
18+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Jooby https://jooby.io
3+
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
4+
* Copyright 2014 Edgar Espina
5+
*/
6+
package io.jooby.langchain4j;
7+
8+
import dev.langchain4j.model.chat.ChatModel;
9+
import dev.langchain4j.model.chat.request.ChatRequest;
10+
import dev.langchain4j.model.chat.response.ChatResponse;
11+
12+
/**
13+
* Decorator for {@link ChatModel} that provides failover logic. Catching exceptions from the
14+
* primary model and routing to a fallback instance.
15+
*/
16+
public class FallbackChatModel implements ChatModel {
17+
private final String name;
18+
private final ChatModel primary;
19+
private final ChatModel fallback;
20+
private final FailoverListener listener;
21+
22+
public FallbackChatModel(
23+
String name, ChatModel primary, ChatModel fallback, FailoverListener listener) {
24+
this.name = name;
25+
this.primary = primary;
26+
this.fallback = fallback;
27+
this.listener = listener;
28+
}
29+
30+
@Override
31+
public ChatResponse chat(ChatRequest request) {
32+
try {
33+
return primary.chat(request);
34+
} catch (Exception e) {
35+
listener.onFailover(name, e);
36+
return fallback.chat(request);
37+
}
38+
}
39+
}

0 commit comments

Comments
 (0)