Skip to content

Commit 21866e8

Browse files
committed
Merge branch '1.5.x' into 2.0.x
2 parents 4dcbf03 + 56a0cf6 commit 21866e8

File tree

21 files changed

+431
-21
lines changed

21 files changed

+431
-21
lines changed

micronaut-langchain4j-core/src/main/java/io/micronaut/langchain4j/aiservices/AiServiceFactory.java

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,14 @@
1515
*/
1616
package io.micronaut.langchain4j.aiservices;
1717

18-
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
18+
import dev.langchain4j.memory.chat.ChatMemoryProvider;
1919
import dev.langchain4j.model.chat.ChatModel;
2020
import dev.langchain4j.model.chat.StreamingChatModel;
2121
import dev.langchain4j.model.embedding.EmbeddingModel;
2222
import dev.langchain4j.model.moderation.ModerationModel;
2323
import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever;
2424
import dev.langchain4j.service.AiServices;
2525
import dev.langchain4j.store.embedding.EmbeddingStore;
26-
import dev.langchain4j.store.memory.chat.InMemoryChatMemoryStore;
2726
import io.micronaut.context.BeanContext;
2827
import io.micronaut.context.BeanProvider;
2928
import io.micronaut.context.Qualifier;
@@ -90,13 +89,7 @@ protected AiServices<Object> createAiServices(
9089

9190
lookupByNameOrDefault(name, ModerationModel.class, builder::moderationModel);
9291

93-
// default to in-memory chat store, but allow replacement
94-
lookupByNameOrDefault(name, InMemoryChatMemoryStore.class, new InMemoryChatMemoryStore(), (store) -> builder.chatMemory(
95-
MessageWindowChatMemory.builder()
96-
.maxMessages(10)
97-
.chatMemoryStore(store)
98-
.build()
99-
));
92+
lookupByNameOrDefault(name, ChatMemoryProvider.class, builder::chatMemoryProvider);
10093

10194
lookupByNameOrDefault(name, EmbeddingModel.class, null, embeddingModel ->
10295
lookupByNameOrDefault(name, EmbeddingStore.class, null, embeddingStore ->
@@ -128,7 +121,7 @@ private <T> void lookupByNameOrDefault(String name, Argument<T> beanType, @Nulla
128121
configurer.accept(defaultValue);
129122
}
130123

131-
provider.ifPresent(configurer);
132-
provider.find(qualifier).ifPresent(configurer);
124+
provider.find(qualifier).ifPresentOrElse(configurer,
125+
() -> provider.ifPresent(configurer));
133126
}
134127
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright 2017-2025 original authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
/**
17+
* Classes related with the creation of in-memory {@link dev.langchain4j.store.embedding.EmbeddingStore}.
18+
*/
19+
@Requires(property = "langchain4j.in-memory.embedding-store.enabled", value = StringUtils.TRUE, defaultValue = StringUtils.TRUE)
20+
@Configuration
21+
package io.micronaut.langchain4j.embedding;
22+
23+
import io.micronaut.context.annotation.Configuration;
24+
import io.micronaut.context.annotation.Requires;
25+
import io.micronaut.core.util.StringUtils;
26+

micronaut-langchain4j-core/src/main/java/io/micronaut/langchain4j/store/memory/chat/MessageWindowChatMemoryFactory.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,7 @@
2929
*/
3030
@Factory
3131
@Internal
32-
33-
class MessageWindowChatMemoryFactory {
32+
public class MessageWindowChatMemoryFactory {
3433

3534
/**
3635
*
@@ -39,7 +38,7 @@ class MessageWindowChatMemoryFactory {
3938
*/
4039
@Prototype
4140
@EachBean(ChatMemoryStore.class)
42-
MessageWindowChatMemory.@NonNull Builder createMessageWindowChatMemoryBuilder(@NonNull ChatMemoryStore chatMemoryStore,
41+
public MessageWindowChatMemory.@NonNull Builder createMessageWindowChatMemoryBuilder(@NonNull ChatMemoryStore chatMemoryStore,
4342
@NonNull MessageWindowChatMemoryConfiguration config) {
4443
return MessageWindowChatMemory.builder()
4544
.maxMessages(config.getMaxMessages())
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright 2017-2025 original authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.micronaut.langchain4j.store.memory.chat.inmemory;
17+
18+
import dev.langchain4j.Internal;
19+
import dev.langchain4j.memory.ChatMemory;
20+
import dev.langchain4j.memory.chat.ChatMemoryProvider;
21+
import dev.langchain4j.store.memory.chat.ChatMemoryStore;
22+
import io.micronaut.langchain4j.store.memory.chat.MessageWindowChatMemoryConfiguration;
23+
import io.micronaut.langchain4j.store.memory.chat.MessageWindowChatMemoryFactory;
24+
import jakarta.inject.Named;
25+
import jakarta.inject.Singleton;
26+
27+
/**
28+
* {@link ChatMemoryProvider} implementation that creates {@link dev.langchain4j.memory.chat.MessageWindowChatMemory}.
29+
*/
30+
@Internal
31+
@Named("InMemoryMessageWindow")
32+
@Singleton
33+
class InMemoryMessageWindowChatMemoryProvider implements ChatMemoryProvider {
34+
private final MessageWindowChatMemoryFactory messageWindowChatMemoryFactory;
35+
private final InMemoryChatMemoryStoreFactory inMemoryChatMemoryStoreFactory;
36+
private final MessageWindowChatMemoryConfiguration config;
37+
38+
InMemoryMessageWindowChatMemoryProvider(MessageWindowChatMemoryFactory messageWindowChatMemoryFactory,
39+
InMemoryChatMemoryStoreFactory inMemoryChatMemoryStoreFactory,
40+
MessageWindowChatMemoryConfiguration config) {
41+
this.messageWindowChatMemoryFactory = messageWindowChatMemoryFactory;
42+
this.inMemoryChatMemoryStoreFactory = inMemoryChatMemoryStoreFactory;
43+
this.config = config;
44+
}
45+
46+
@Override
47+
public ChatMemory get(Object memoryId) {
48+
ChatMemoryStore chatMemoryStore = inMemoryChatMemoryStoreFactory.chatMemoryStore();
49+
return messageWindowChatMemoryFactory.createMessageWindowChatMemoryBuilder(chatMemoryStore, config)
50+
.id(memoryId)
51+
.build();
52+
}
53+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright 2017-2025 original authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.micronaut.langchain4j.utils;
17+
18+
import dev.langchain4j.data.image.Image;
19+
import dev.langchain4j.data.message.ImageContent;
20+
import io.micronaut.core.annotation.Internal;
21+
import io.micronaut.core.annotation.NonNull;
22+
23+
import java.io.IOException;
24+
import java.io.InputStream;
25+
import java.util.Base64;
26+
27+
/**
28+
* Utility class to create {@link ImageContent} instances.
29+
*/
30+
@Internal
31+
public final class ImageContentUtils {
32+
private ImageContentUtils() {
33+
}
34+
35+
/**
36+
*
37+
* @param is InputStream
38+
* @param mediaType MediaType
39+
* @return Image Content
40+
* @throws IOException exception triggered reading bytes from inputStream
41+
*/
42+
@NonNull
43+
public static ImageContent imageContent(@NonNull InputStream is, @NonNull String mediaType) throws IOException {
44+
byte[] bytes = is.readAllBytes();
45+
return imageContent(bytes, mediaType);
46+
}
47+
48+
/**
49+
*
50+
* @param imageBytes Image bytes
51+
* @param mediaType Media type
52+
* @return Image Content
53+
*/
54+
@NonNull
55+
public static ImageContent imageContent(@NonNull byte[] imageBytes, @NonNull String mediaType) {
56+
String base64 = Base64.getEncoder().encodeToString(imageBytes);
57+
return imageContent(base64, mediaType);
58+
}
59+
60+
61+
/**
62+
*
63+
* @param base64 Image content Base 64 encoded
64+
* @param mediaType Media type
65+
* @return Image Content
66+
*/
67+
@NonNull
68+
public static ImageContent imageContent(@NonNull String base64, @NonNull String mediaType) {
69+
return ImageContent.from(Image.builder()
70+
.base64Data(base64)
71+
.mimeType(mediaType)
72+
.build());
73+
}
74+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* Copyright 2017-2025 original authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
/**
17+
* Utility classes to work with Langchain4j.
18+
*/
19+
package io.micronaut.langchain4j.utils;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package io.micronaut.langchain4j.store.memory.chat.inmemory;
2+
3+
import dev.langchain4j.memory.chat.ChatMemoryProvider;
4+
import io.micronaut.context.BeanContext;
5+
import io.micronaut.inject.qualifiers.Qualifiers;
6+
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
7+
import jakarta.inject.Inject;
8+
import org.junit.jupiter.api.Test;
9+
10+
import static org.junit.jupiter.api.Assertions.*;
11+
12+
@MicronautTest(startApplication = false)
13+
class InMemoryMessageWindowChatMemoryProviderTest {
14+
15+
@Inject
16+
BeanContext beanContext;
17+
18+
@Test
19+
void messageWindowChatMemoryProvider() {
20+
ChatMemoryProvider provider = beanContext.getBean(ChatMemoryProvider.class, Qualifiers.byName("InMemoryMessageWindow"));
21+
assertNotNull(provider);
22+
assertInstanceOf(InMemoryMessageWindowChatMemoryProvider.class, provider);
23+
}
24+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,10 @@
11
To run the tests in this module. Define the environment variable `LANGCHAIN4J_GOOGLE_AI_GEMINI_API_KEY`.
2+
3+
To obtain an API key for Google AI Gemini, you need to use [Google AI Studio](https://aistudio.google.com)
4+
5+
- Sign in: Use your Google account to sign in
6+
- Get API Key: Once logged in, look for the "Get API key" option in the left sidebar or navigation menu
7+
- Create or Select Project: You'll either need to:
8+
* Create a new API key in a new project, or
9+
* Select an existing Google Cloud project if you have one
10+
- Copy the API Key: Once generated, copy the API key - it will look something like AIzaSy...

micronaut-langchain4j-googleai-gemini/src/main/java/io/micronaut/langchain4j/googleaigemini/GoogleaiGeminiModule.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
impl = GoogleAiEmbeddingModel.class)
4040
},
4141
properties = {
42-
@Lang4jConfig.Property(name = "modelName", common = true, required = true, defaultValue = "gemini-1.5-flash"),
42+
@Lang4jConfig.Property(name = "modelName", common = true, required = true, defaultValue = "gemini-2.5-flash"),
4343
@Lang4jConfig.Property(name = "apiKey", common = true, required = true)
4444
}
4545
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package io.micronaut.langchain4j.googleaigemini;
2+
3+
import dev.langchain4j.data.message.ImageContent;
4+
import dev.langchain4j.model.chat.ChatModel;
5+
import dev.langchain4j.service.SystemMessage;
6+
import dev.langchain4j.service.UserMessage;
7+
import dev.langchain4j.store.embedding.EmbeddingStore;
8+
import io.micronaut.context.BeanContext;
9+
import io.micronaut.context.annotation.Property;
10+
import io.micronaut.context.annotation.Requires;
11+
import io.micronaut.core.util.StringUtils;
12+
import io.micronaut.langchain4j.annotation.AiService;
13+
import io.micronaut.langchain4j.utils.ImageContentUtils;
14+
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
15+
import jakarta.inject.Inject;
16+
import org.junit.jupiter.api.Test;
17+
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
18+
19+
import java.io.IOException;
20+
import java.io.InputStream;
21+
import java.util.List;
22+
import java.util.Locale;
23+
24+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
25+
import static org.junit.jupiter.api.Assertions.assertFalse;
26+
import static org.junit.jupiter.api.Assertions.assertTrue;
27+
28+
@EnabledIfEnvironmentVariable(
29+
named = "LANGCHAIN4J_GOOGLE_AI_GEMINI_API_KEY",
30+
matches = ".+"
31+
)
32+
@Property(name = "langchain4j.google-ai-gemini.chat-model.log-requests", value = StringUtils.TRUE)
33+
@Property(name = "langchain4j.google-ai-gemini.chat-model.log-responses", value = StringUtils.TRUE)
34+
@Property(name = "spec.name", value = "GoogleAiGeminiImageContentTest")
35+
@MicronautTest(startApplication = false)
36+
class GoogleAiGeminiImageContentTest {
37+
@Inject
38+
BeanContext beanContext;
39+
40+
@Test
41+
void testImage(ChatService chatService) throws IOException {
42+
assertTrue(beanContext.containsBean(ChatModel.class));
43+
assertFalse(beanContext.containsBean(EmbeddingStore.class));
44+
try (InputStream is = getClass().getClassLoader().getResourceAsStream("cat.jpg")) {
45+
if (is == null) {
46+
throw new IllegalStateException("Resource cat.jpg not found");
47+
}
48+
49+
var imageContent = ImageContentUtils.imageContent(is, "image/jpeg");
50+
String response = assertDoesNotThrow(() -> chatService.chat("What animal is shown in this image?", List.of(imageContent)));
51+
assertTrue(response.toLowerCase(Locale.ROOT).contains("cat"), response + " did not contain 'cat'");
52+
}
53+
}
54+
55+
@Requires(property = "spec.name", value = "GoogleAiGeminiImageContentTest")
56+
@AiService
57+
interface ChatService {
58+
@SystemMessage("You are a helpful AI assistant.")
59+
String chat(@UserMessage String userMessage, @UserMessage List<ImageContent> images);
60+
}
61+
}

0 commit comments

Comments
 (0)