From 8aa6b8fb544e2ab42c27fdbd63fd31ec7b1c3f63 Mon Sep 17 00:00:00 2001 From: LangChain4j Date: Sat, 23 Mar 2024 16:42:16 +0100 Subject: [PATCH 1/5] WIP: declarative AI services and EasyRAG --- .../pom.xml | 79 +++++++ .../rag/easy/spring/DocumentsProperties.java | 15 ++ .../rag/easy/spring/EasyRagAutoConfig.java | 94 ++++++++ .../rag/easy/spring/EasyRagProperties.java | 19 ++ .../rag/easy/spring/IngestionProperties.java | 13 ++ .../main/resources/META-INF/spring.factories | 1 + ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../langchain4j/openai/spring/AutoConfig.java | 7 + langchain4j-spring-boot-starter/pom.xml | 90 +++++++ .../langchain4j/rag/spring/RagAutoConfig.java | 51 ++++ .../langchain4j/rag/spring/RagProperties.java | 17 ++ .../rag/spring/RetrievalProperties.java | 12 + .../langchain4j/service/spring/AiService.java | 26 +++ .../service/spring/AiServiceFactory.java | 116 ++++++++++ .../service/spring/AiServicesAutoConfig.java | 135 +++++++++++ .../spring/LangChain4jAutoConfig.java | 14 ++ .../main/resources/META-INF/spring.factories | 1 + ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../AssistantWithCustomChatModel.java | 11 + .../AssistantWithCustomChatModelIT.java | 34 +++ .../TestApplicationWithCustomChatModel.java | 27 +++ .../AssistantWithMultipleChatModels.java | 9 + .../AssistantWithMultipleChatModelsIT.java | 32 +++ ...TestApplicationWithMultipleChatModels.java | 25 ++ .../defaultConfig/AiServicesAutoConfigIT.java | 219 ++++++++++++++++++ .../AssistantWithPackagePrivateTools.java | 9 + .../AssistantWithPublicTools.java | 9 + .../AssistantWithoutAnnotation.java | 6 + .../spring/defaultConfig/OuterClass.java | 12 + .../PackagePrivateAssistant.java | 9 + .../defaultConfig/PackagePrivateTools.java | 15 ++ .../spring/defaultConfig/PublicAssistant.java | 9 + .../spring/defaultConfig/PublicTools.java | 15 ++ .../defaultConfig/StreamingAssistant.java | 10 + .../spring/defaultConfig/TestApplication.java | 12 + pom.xml | 3 + 36 files changed, 1158 insertions(+) create mode 100644 langchain4j-easy-rag-spring-boot-starter/pom.xml create mode 100644 langchain4j-easy-rag-spring-boot-starter/src/main/java/dev/langchain4j/rag/easy/spring/DocumentsProperties.java create mode 100644 langchain4j-easy-rag-spring-boot-starter/src/main/java/dev/langchain4j/rag/easy/spring/EasyRagAutoConfig.java create mode 100644 langchain4j-easy-rag-spring-boot-starter/src/main/java/dev/langchain4j/rag/easy/spring/EasyRagProperties.java create mode 100644 langchain4j-easy-rag-spring-boot-starter/src/main/java/dev/langchain4j/rag/easy/spring/IngestionProperties.java create mode 100644 langchain4j-easy-rag-spring-boot-starter/src/main/resources/META-INF/spring.factories create mode 100644 langchain4j-easy-rag-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 langchain4j-spring-boot-starter/pom.xml create mode 100644 langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/rag/spring/RagAutoConfig.java create mode 100644 langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/rag/spring/RagProperties.java create mode 100644 langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/rag/spring/RetrievalProperties.java create mode 100644 langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiService.java create mode 100644 langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServiceFactory.java create mode 100644 langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java create mode 100644 langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/spring/LangChain4jAutoConfig.java create mode 100644 langchain4j-spring-boot-starter/src/main/resources/META-INF/spring.factories create mode 100644 langchain4j-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/customConfig/chatModel/AssistantWithCustomChatModel.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/customConfig/chatModel/AssistantWithCustomChatModelIT.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/customConfig/chatModel/TestApplicationWithCustomChatModel.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/customConfig/multipleChatModels/AssistantWithMultipleChatModels.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/customConfig/multipleChatModels/AssistantWithMultipleChatModelsIT.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/customConfig/multipleChatModels/TestApplicationWithMultipleChatModels.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/AiServicesAutoConfigIT.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/AssistantWithPackagePrivateTools.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/AssistantWithPublicTools.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/AssistantWithoutAnnotation.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/OuterClass.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/PackagePrivateAssistant.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/PackagePrivateTools.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/PublicAssistant.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/PublicTools.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/StreamingAssistant.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/TestApplication.java diff --git a/langchain4j-easy-rag-spring-boot-starter/pom.xml b/langchain4j-easy-rag-spring-boot-starter/pom.xml new file mode 100644 index 00000000..e16d86cf --- /dev/null +++ b/langchain4j-easy-rag-spring-boot-starter/pom.xml @@ -0,0 +1,79 @@ + + + 4.0.0 + + + dev.langchain4j + langchain4j-spring + 0.29.0-SNAPSHOT + ../pom.xml + + + langchain4j-easy-rag-spring-boot-starter + Spring Boot starter for Easy RAG + + + + + dev.langchain4j + langchain4j-easy-rag + ${project.version} + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-autoconfigure-processor + true + + + + + org.projectlombok + lombok + provided + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.testcontainers + testcontainers + test + + + + org.testcontainers + junit-jupiter + test + + + + + + + Apache-2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + repo + A business-friendly OSS license + + + + \ No newline at end of file diff --git a/langchain4j-easy-rag-spring-boot-starter/src/main/java/dev/langchain4j/rag/easy/spring/DocumentsProperties.java b/langchain4j-easy-rag-spring-boot-starter/src/main/java/dev/langchain4j/rag/easy/spring/DocumentsProperties.java new file mode 100644 index 00000000..0269d1e5 --- /dev/null +++ b/langchain4j-easy-rag-spring-boot-starter/src/main/java/dev/langchain4j/rag/easy/spring/DocumentsProperties.java @@ -0,0 +1,15 @@ +package dev.langchain4j.rag.easy.spring; + +import lombok.Getter; +import lombok.Setter; + +import java.nio.file.Path; + +@Getter +@Setter +class DocumentsProperties { + + Path path; + String glob; + Boolean recursion; +} diff --git a/langchain4j-easy-rag-spring-boot-starter/src/main/java/dev/langchain4j/rag/easy/spring/EasyRagAutoConfig.java b/langchain4j-easy-rag-spring-boot-starter/src/main/java/dev/langchain4j/rag/easy/spring/EasyRagAutoConfig.java new file mode 100644 index 00000000..132e6a0a --- /dev/null +++ b/langchain4j-easy-rag-spring-boot-starter/src/main/java/dev/langchain4j/rag/easy/spring/EasyRagAutoConfig.java @@ -0,0 +1,94 @@ +package dev.langchain4j.rag.easy.spring; + +import dev.langchain4j.data.document.Document; +import dev.langchain4j.data.document.DocumentSplitter; +import dev.langchain4j.data.document.loader.FileSystemDocumentLoader; +import dev.langchain4j.data.document.splitter.DocumentSplitters; +import dev.langchain4j.data.segment.TextSegment; +import dev.langchain4j.model.Tokenizer; +import dev.langchain4j.model.embedding.EmbeddingModel; +import dev.langchain4j.model.embedding.HuggingFaceTokenizer; +import dev.langchain4j.model.embedding.bge.small.en.v15.BgeSmallEnV15QuantizedEmbeddingModel; +import dev.langchain4j.store.embedding.EmbeddingStore; +import dev.langchain4j.store.embedding.EmbeddingStoreIngestor; +import org.apache.commons.lang3.NotImplementedException; +import org.springframework.boot.ApplicationRunner; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; + +import java.util.List; + +import static dev.langchain4j.internal.Utils.isNotNullOrBlank; +import static dev.langchain4j.rag.easy.spring.EasyRagProperties.PREFIX; +import static java.util.Collections.singletonList; + +@AutoConfiguration +@EnableConfigurationProperties(EasyRagProperties.class) +public class EasyRagAutoConfig { + + @Bean + @ConditionalOnMissingBean + @ConditionalOnClass(HuggingFaceTokenizer.class) + Tokenizer tokenizer() { // TODO bean name, type + return new HuggingFaceTokenizer(); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnClass(DocumentSplitters.class) + DocumentSplitter documentSplitter(Tokenizer tokenizer) { // TODO bean name, type + return DocumentSplitters.recursive(300, 30, tokenizer); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnClass(BgeSmallEnV15QuantizedEmbeddingModel.class) + EmbeddingModel embeddingModel() { // TODO bean name, type + return new BgeSmallEnV15QuantizedEmbeddingModel(); + } + + @Bean + @ConditionalOnProperty(PREFIX + ".ingestion.documents.path") + ApplicationRunner easyRagDocumentIngestor(DocumentSplitter documentSplitter, // TODO should splitter be optional? + EmbeddingModel embeddingModel, + EmbeddingStore embeddingStore, + EasyRagProperties easyRagProperties) { // TODO bean name, type + return args -> { + + List documents = loadDocuments(easyRagProperties.ingestion.documents); + + EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder() + // TODO documentTransformer + .documentSplitter(documentSplitter) + // TODO textSegmentTransformer + .embeddingStore(embeddingStore) + .embeddingModel(embeddingModel) + .build(); + + ingestor.ingest(documents); + }; + } + + private static List loadDocuments(DocumentsProperties documentsProperties) { + if (documentsProperties.recursion != null && documentsProperties.recursion) { + throw new NotImplementedException(); // TODO + } else { + if (isNotNullOrBlank(documentsProperties.glob)) { + throw new NotImplementedException(); // TODO + } else { + if (documentsProperties.path.toFile().isDirectory()) { + return FileSystemDocumentLoader.loadDocuments(documentsProperties.path); + } else { + Document document = FileSystemDocumentLoader.loadDocument(documentsProperties.path); + return singletonList(document); + } + } + } + } + + // TODO ITs +} diff --git a/langchain4j-easy-rag-spring-boot-starter/src/main/java/dev/langchain4j/rag/easy/spring/EasyRagProperties.java b/langchain4j-easy-rag-spring-boot-starter/src/main/java/dev/langchain4j/rag/easy/spring/EasyRagProperties.java new file mode 100644 index 00000000..6fa002c7 --- /dev/null +++ b/langchain4j-easy-rag-spring-boot-starter/src/main/java/dev/langchain4j/rag/easy/spring/EasyRagProperties.java @@ -0,0 +1,19 @@ +package dev.langchain4j.rag.easy.spring; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; + +import static dev.langchain4j.rag.easy.spring.EasyRagProperties.PREFIX; + +@Getter +@Setter +@ConfigurationProperties(prefix = PREFIX) +public class EasyRagProperties { + + static final String PREFIX = "langchain4j.easy-rag"; + + @NestedConfigurationProperty + IngestionProperties ingestion; +} diff --git a/langchain4j-easy-rag-spring-boot-starter/src/main/java/dev/langchain4j/rag/easy/spring/IngestionProperties.java b/langchain4j-easy-rag-spring-boot-starter/src/main/java/dev/langchain4j/rag/easy/spring/IngestionProperties.java new file mode 100644 index 00000000..429d7823 --- /dev/null +++ b/langchain4j-easy-rag-spring-boot-starter/src/main/java/dev/langchain4j/rag/easy/spring/IngestionProperties.java @@ -0,0 +1,13 @@ +package dev.langchain4j.rag.easy.spring; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.NestedConfigurationProperty; + +@Getter +@Setter +class IngestionProperties { + + @NestedConfigurationProperty + DocumentsProperties documents; +} diff --git a/langchain4j-easy-rag-spring-boot-starter/src/main/resources/META-INF/spring.factories b/langchain4j-easy-rag-spring-boot-starter/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..4ded5330 --- /dev/null +++ b/langchain4j-easy-rag-spring-boot-starter/src/main/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=dev.langchain4j.rag.easy.spring.EasyRagAutoConfig \ No newline at end of file diff --git a/langchain4j-easy-rag-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/langchain4j-easy-rag-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..5d56a17f --- /dev/null +++ b/langchain4j-easy-rag-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +dev.langchain4j.rag.easy.spring.EasyRagAutoConfig \ No newline at end of file diff --git a/langchain4j-open-ai-spring-boot-starter/src/main/java/dev/langchain4j/openai/spring/AutoConfig.java b/langchain4j-open-ai-spring-boot-starter/src/main/java/dev/langchain4j/openai/spring/AutoConfig.java index 8e47ecba..bfd4b1cc 100644 --- a/langchain4j-open-ai-spring-boot-starter/src/main/java/dev/langchain4j/openai/spring/AutoConfig.java +++ b/langchain4j-open-ai-spring-boot-starter/src/main/java/dev/langchain4j/openai/spring/AutoConfig.java @@ -2,6 +2,7 @@ import dev.langchain4j.model.openai.*; import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; @@ -159,4 +160,10 @@ OpenAiImageModel openAiImageModel(Properties properties) { .persistTo(imageModelProperties.getPersistTo()) .build(); } + + @Bean + @ConditionalOnMissingBean + OpenAiTokenizer openAiTokenizer() { + return new OpenAiTokenizer(); + } } \ No newline at end of file diff --git a/langchain4j-spring-boot-starter/pom.xml b/langchain4j-spring-boot-starter/pom.xml new file mode 100644 index 00000000..9eb1f8b0 --- /dev/null +++ b/langchain4j-spring-boot-starter/pom.xml @@ -0,0 +1,90 @@ + + + 4.0.0 + + + dev.langchain4j + langchain4j-spring + 0.29.0-SNAPSHOT + ../pom.xml + + + langchain4j-spring-boot-starter + + Spring Boot starter for LangChain4j + + + + + dev.langchain4j + langchain4j + ${project.version} + + + + org.reflections + reflections + 0.10.2 + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-autoconfigure-processor + true + + + + + org.projectlombok + lombok + provided + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + + dev.langchain4j + langchain4j-open-ai-spring-boot-starter + ${project.version} + test + + + + dev.langchain4j + langchain4j-core + ${project.version} + tests + test-jar + test + + + + + + + Apache-2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + repo + A business-friendly OSS license + + + + \ No newline at end of file diff --git a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/rag/spring/RagAutoConfig.java b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/rag/spring/RagAutoConfig.java new file mode 100644 index 00000000..a885d2f7 --- /dev/null +++ b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/rag/spring/RagAutoConfig.java @@ -0,0 +1,51 @@ +package dev.langchain4j.rag.spring; + +import dev.langchain4j.data.segment.TextSegment; +import dev.langchain4j.model.embedding.EmbeddingModel; +import dev.langchain4j.rag.content.retriever.ContentRetriever; +import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever; +import dev.langchain4j.store.embedding.EmbeddingStore; +import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; + +@EnableConfigurationProperties(RagProperties.class) +public class RagAutoConfig { + + // TODO make these beans lazy? + + @Bean + @ConditionalOnMissingBean + EmbeddingStore embeddingStore() { // TODO bean name + return new InMemoryEmbeddingStore<>(); + } + + @Bean + @ConditionalOnBean({ + EmbeddingModel.class, + EmbeddingStore.class + }) + @ConditionalOnMissingBean + ContentRetriever contentRetriever(EmbeddingModel embeddingModel, + EmbeddingStore embeddingStore, + RagProperties ragProperties) { // TODO bean name + + EmbeddingStoreContentRetriever.EmbeddingStoreContentRetrieverBuilder builder = EmbeddingStoreContentRetriever.builder() + .embeddingStore(embeddingStore) + .embeddingModel(embeddingModel); + + // TODO ragProperties can be null? + RetrievalProperties retrievalProperties = ragProperties.getRetrieval(); + if (retrievalProperties != null) { + builder + .maxResults(retrievalProperties.maxResults) + .minScore(retrievalProperties.minScore); + } + + return builder.build(); + } + + // TODO test +} diff --git a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/rag/spring/RagProperties.java b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/rag/spring/RagProperties.java new file mode 100644 index 00000000..dd47aaba --- /dev/null +++ b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/rag/spring/RagProperties.java @@ -0,0 +1,17 @@ +package dev.langchain4j.rag.spring; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; + +@Getter +@Setter +@ConfigurationProperties(prefix = RagProperties.PREFIX) +public class RagProperties { + + static final String PREFIX = "langchain4j.rag"; + + @NestedConfigurationProperty + RetrievalProperties retrieval; +} diff --git a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/rag/spring/RetrievalProperties.java b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/rag/spring/RetrievalProperties.java new file mode 100644 index 00000000..e8995ced --- /dev/null +++ b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/rag/spring/RetrievalProperties.java @@ -0,0 +1,12 @@ +package dev.langchain4j.rag.spring; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +class RetrievalProperties { + + Integer maxResults; + Double minScore; +} diff --git a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiService.java b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiService.java new file mode 100644 index 00000000..87b001b9 --- /dev/null +++ b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiService.java @@ -0,0 +1,26 @@ +package dev.langchain4j.service.spring; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * TODO + * TODO copy from AiServices + * TODO Flux + */ +@Target(TYPE) +@Retention(RUNTIME) +public @interface AiService { + + /** + * TODO + */ + String chatModel() default ""; + + // TODO make the rest of components configurable + + Class[] tools() default {}; // TODO use all available tools by default? +} \ No newline at end of file diff --git a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServiceFactory.java b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServiceFactory.java new file mode 100644 index 00000000..6cf21885 --- /dev/null +++ b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServiceFactory.java @@ -0,0 +1,116 @@ +package dev.langchain4j.service.spring; + +import dev.langchain4j.memory.ChatMemory; +import dev.langchain4j.memory.chat.ChatMemoryProvider; +import dev.langchain4j.model.chat.ChatLanguageModel; +import dev.langchain4j.model.chat.StreamingChatLanguageModel; +import dev.langchain4j.rag.RetrievalAugmentor; +import dev.langchain4j.rag.content.retriever.ContentRetriever; +import dev.langchain4j.service.AiServices; +import org.springframework.beans.factory.FactoryBean; + +import java.util.ArrayList; +import java.util.List; + +import static dev.langchain4j.internal.Utils.isNullOrEmpty; + +class AiServiceFactory implements FactoryBean { + + private final Class aiServiceClass; + private ChatLanguageModel chatLanguageModel; + private StreamingChatLanguageModel streamingChatLanguageModel; + private ChatMemory chatMemory; + private ChatMemoryProvider chatMemoryProvider; + private ContentRetriever contentRetriever; + private RetrievalAugmentor retrievalAugmentor; + private final List beansWithTools = new ArrayList<>(); + + public AiServiceFactory(Class aiServiceClass) { + this.aiServiceClass = aiServiceClass; + } + + public void setChatLanguageModel(ChatLanguageModel chatLanguageModel) { + this.chatLanguageModel = chatLanguageModel; + } + + public void setStreamingChatLanguageModel(StreamingChatLanguageModel streamingChatLanguageModel) { + this.streamingChatLanguageModel = streamingChatLanguageModel; + } + + public void setChatMemory(ChatMemory chatMemory) { + this.chatMemory = chatMemory; + } + + public void setChatMemoryProvider(ChatMemoryProvider chatMemoryProvider) { + this.chatMemoryProvider = chatMemoryProvider; + } + + public void setContentRetriever(ContentRetriever contentRetriever) { + this.contentRetriever = contentRetriever; + } + + public void setRetrievalAugmentor(RetrievalAugmentor retrievalAugmentor) { + this.retrievalAugmentor = retrievalAugmentor; + } + + public void setBeanWithTools(Object beanWithTools) { + this.beansWithTools.add(beanWithTools); + } + + @Override + public Object getObject() { + + AiServices builder = AiServices.builder(aiServiceClass); + + if (chatLanguageModel != null) { + builder = builder.chatLanguageModel(chatLanguageModel); + } + + if (streamingChatLanguageModel != null) { + builder = builder.streamingChatLanguageModel(streamingChatLanguageModel); + } + + if (chatMemory != null) { + builder.chatMemory(chatMemory); + } + + if (chatMemoryProvider != null) { + builder.chatMemoryProvider(chatMemoryProvider); + } + + if (contentRetriever != null) { + builder = builder.contentRetriever(contentRetriever); + } + + if (retrievalAugmentor != null) { + builder = builder.retrievalAugmentor(retrievalAugmentor); + } + + if (!isNullOrEmpty(beansWithTools)) { + builder = builder.tools(beansWithTools); + } + + return builder.build(); + } + + @Override + public Class getObjectType() { + return aiServiceClass; + } + + @Override + public boolean isSingleton() { + return true; // TODO + } + + /** + * TODO + * getObjectType() getObject() invocations may arrive early in the bootstrap process, even ahead of any post-processor setup. + *

+ * TODO + * The container is only responsible for managing the lifecycle of the FactoryBean instance, not the lifecycle + * of the objects created by the FactoryBean. Therefore, a destroy method on an exposed bean object + * (such as java.io.Closeable.close()) will not be called automatically. + * Instead, a FactoryBean should implement DisposableBean and delegate any such close call to the underlying object. + */ +} diff --git a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java new file mode 100644 index 00000000..5af88fe2 --- /dev/null +++ b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java @@ -0,0 +1,135 @@ +package dev.langchain4j.service.spring; + +import dev.langchain4j.exception.IllegalConfigurationException; +import dev.langchain4j.memory.ChatMemory; +import dev.langchain4j.memory.chat.ChatMemoryProvider; +import dev.langchain4j.model.chat.ChatLanguageModel; +import dev.langchain4j.model.chat.StreamingChatLanguageModel; +import dev.langchain4j.rag.RetrievalAugmentor; +import dev.langchain4j.rag.content.retriever.ContentRetriever; +import org.reflections.Reflections; +import org.springframework.beans.BeansException; +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; + +import java.util.Arrays; + +import static dev.langchain4j.exception.IllegalConfigurationException.illegalConfiguration; +import static dev.langchain4j.internal.Utils.isNotNullOrBlank; +import static dev.langchain4j.internal.Utils.isNullOrBlank; + +public class AiServicesAutoConfig { + + @Bean + BeanFactoryPostProcessor aiServicesRegisteringBeanFactoryPostProcessor() { + return new BeanFactoryPostProcessor() { + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + +// Set> allToolBeans = new HashSet<>(); +// +// for (String beanName : registry.getBeanDefinitionNames()) { +// BeanDefinition beanDefinition = registry.getBeanDefinition(beanName); +// try { +// Class beanClass = Class.forName(beanDefinition.getBeanClassName()); +// for (Method beanMethod : beanClass.getDeclaredMethods()) { +// if (beanMethod.isAnnotationPresent(Tool.class)) { +// allToolBeans.add(beanClass); +// } +// } +// } catch (Exception e) { +// // TODO +// } +// } + + String[] app = beanFactory.getBeanNamesForAnnotation(SpringBootApplication.class); + String basePackage = beanFactory.getBeanDefinition(app[0]).getResolvableType().resolve().getPackage().getName(); + + String[] chatLanguageModels = beanFactory.getBeanNamesForType(ChatLanguageModel.class); + String[] streamingChatLanguageModels = beanFactory.getBeanNamesForType(StreamingChatLanguageModel.class); + String[] chatMemories = beanFactory.getBeanNamesForType(ChatMemory.class); + String[] chatMemoryProviders = beanFactory.getBeanNamesForType(ChatMemoryProvider.class); + String[] contentRetrievers = beanFactory.getBeanNamesForType(ContentRetriever.class); + String[] retrievalAugmentors = beanFactory.getBeanNamesForType(RetrievalAugmentor.class); + + Reflections reflections = new Reflections(basePackage); + reflections.getTypesAnnotatedWith(AiService.class).forEach(aiService -> { + + if (beanFactory.getBeanNamesForType(aiService).length > 0) { + // User probably wants to configure AI Service bean manually + // TODO or better fail because he should not annotate it with @AiService then? + return; + } + + AiService annotation = aiService.getAnnotation(AiService.class); + + GenericBeanDefinition beanDefinition = new GenericBeanDefinition(); + beanDefinition.setBeanClass(AiServiceFactory.class); + beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(aiService); + MutablePropertyValues propertyValues = beanDefinition.getPropertyValues(); + + if (isNotNullOrBlank(annotation.chatModel())) { + propertyValues.add("chatLanguageModel", new RuntimeBeanReference(annotation.chatModel())); + } else { + if (chatLanguageModels.length == 1) { + propertyValues.add("chatLanguageModel", new RuntimeBeanReference(chatLanguageModels[0])); + } else if (chatLanguageModels.length > 1) { + throw conflict(ChatLanguageModel.class, chatLanguageModels); + } + } + + // TODO handle conflicts for all other components + + if (streamingChatLanguageModels.length == 1) { + propertyValues.add("streamingChatLanguageModel", new RuntimeBeanReference(streamingChatLanguageModels[0])); + } + + if (chatMemories.length == 1) { + propertyValues.add("chatMemory", new RuntimeBeanReference(chatMemories[0])); + } + + if (chatMemoryProviders.length == 1) { + propertyValues.add("chatMemoryProvider", new RuntimeBeanReference(chatMemoryProviders[0])); + } + + if (contentRetrievers.length == 1) { + propertyValues.add("contentRetriever", new RuntimeBeanReference(contentRetrievers[0])); + } + + if (retrievalAugmentors.length == 1) { + propertyValues.add("retrievalAugmentor", new RuntimeBeanReference(retrievalAugmentors[0])); + } + + for (Class classWithTools : annotation.tools()) { + for (String beanWithTools : beanFactory.getBeanNamesForType(classWithTools)) { + propertyValues.add("beanWithTools", new RuntimeBeanReference(beanWithTools)); + } + } + + BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory; + registry.registerBeanDefinition(lowercaseFirstLetter(aiService.getSimpleName()), beanDefinition); + }); + } + }; + } + + private static IllegalConfigurationException conflict(Class beanType, Object[] beanNames) { + return illegalConfiguration("Conflict: multiple beans of type %s are found: %s. " + + "Please specify which one you wish to use in the @AiService annotation like this: " + + "@AiService(chatModel = \"\").", beanType.getName(), Arrays.toString(beanNames)); + } + + private static String lowercaseFirstLetter(String text) { + if (isNullOrBlank(text)) { + return text; + } + return text.substring(0, 1).toLowerCase() + text.substring(1); + } +} diff --git a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/spring/LangChain4jAutoConfig.java b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/spring/LangChain4jAutoConfig.java new file mode 100644 index 00000000..52c461b2 --- /dev/null +++ b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/spring/LangChain4jAutoConfig.java @@ -0,0 +1,14 @@ +package dev.langchain4j.spring; + +import dev.langchain4j.rag.spring.RagAutoConfig; +import dev.langchain4j.service.spring.AiServicesAutoConfig; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.Import; + +@AutoConfiguration +@Import({ + AiServicesAutoConfig.class, + RagAutoConfig.class +}) +public class LangChain4jAutoConfig { +} diff --git a/langchain4j-spring-boot-starter/src/main/resources/META-INF/spring.factories b/langchain4j-spring-boot-starter/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..7c967b04 --- /dev/null +++ b/langchain4j-spring-boot-starter/src/main/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=dev.langchain4j.spring.LangChain4jAutoConfig \ No newline at end of file diff --git a/langchain4j-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/langchain4j-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..5a5aac00 --- /dev/null +++ b/langchain4j-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +dev.langchain4j.spring.LangChain4jAutoConfig \ No newline at end of file diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/customConfig/chatModel/AssistantWithCustomChatModel.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/customConfig/chatModel/AssistantWithCustomChatModel.java new file mode 100644 index 00000000..0ea16991 --- /dev/null +++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/customConfig/chatModel/AssistantWithCustomChatModel.java @@ -0,0 +1,11 @@ +package dev.langchain4j.service.spring.customConfig.chatModel; + +import dev.langchain4j.service.spring.AiService; + +import static dev.langchain4j.service.spring.customConfig.chatModel.TestApplicationWithCustomChatModel.CUSTOM_CHAT_MODEL_BEAN_NAME; + +@AiService(chatModel = CUSTOM_CHAT_MODEL_BEAN_NAME) +interface AssistantWithCustomChatModel { + + String chat(String userMessage); +} \ No newline at end of file diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/customConfig/chatModel/AssistantWithCustomChatModelIT.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/customConfig/chatModel/AssistantWithCustomChatModelIT.java new file mode 100644 index 00000000..c75f886c --- /dev/null +++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/customConfig/chatModel/AssistantWithCustomChatModelIT.java @@ -0,0 +1,34 @@ +package dev.langchain4j.service.spring.customConfig.chatModel; + +import dev.langchain4j.service.spring.AiServicesAutoConfig; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +class AssistantWithCustomChatModelIT { + + ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(AiServicesAutoConfig.class)); + + @Test + void should_create_AI_service_with_custom_chat_model() { + contextRunner + .withPropertyValues( + "langchain4j.open-ai.chat-model.api-key=banana" // to make sure that this model is not used + ) + .withUserConfiguration(TestApplicationWithCustomChatModel.class) + .run(context -> { + + // given + AssistantWithCustomChatModel assistant = context.getBean(AssistantWithCustomChatModel.class); + + // when + String answer = assistant.chat("What is the capital of Germany?"); + + // then + assertThat(answer).containsIgnoringCase("Berlin"); + }); + } +} \ No newline at end of file diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/customConfig/chatModel/TestApplicationWithCustomChatModel.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/customConfig/chatModel/TestApplicationWithCustomChatModel.java new file mode 100644 index 00000000..8cde86bd --- /dev/null +++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/customConfig/chatModel/TestApplicationWithCustomChatModel.java @@ -0,0 +1,27 @@ +package dev.langchain4j.service.spring.customConfig.chatModel; + +import dev.langchain4j.model.chat.ChatLanguageModel; +import dev.langchain4j.model.openai.OpenAiChatModel; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; + +@SpringBootApplication +public class TestApplicationWithCustomChatModel { + + static final String CUSTOM_CHAT_MODEL_BEAN_NAME = "customChatModel"; + + @Bean(CUSTOM_CHAT_MODEL_BEAN_NAME) + ChatLanguageModel chatLanguageModel() { + return OpenAiChatModel.withApiKey(System.getenv("OPENAI_API_KEY")); + } + + @Bean(CUSTOM_CHAT_MODEL_BEAN_NAME + 2) + ChatLanguageModel chatLanguageModel2() { + return OpenAiChatModel.withApiKey(System.getenv("OPENAI_API_KEY")); + } + + public static void main(String[] args) { + SpringApplication.run(TestApplicationWithCustomChatModel.class, args); + } +} diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/customConfig/multipleChatModels/AssistantWithMultipleChatModels.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/customConfig/multipleChatModels/AssistantWithMultipleChatModels.java new file mode 100644 index 00000000..42fad253 --- /dev/null +++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/customConfig/multipleChatModels/AssistantWithMultipleChatModels.java @@ -0,0 +1,9 @@ +package dev.langchain4j.service.spring.customConfig.multipleChatModels; + +import dev.langchain4j.service.spring.AiService; + +@AiService +interface AssistantWithMultipleChatModels { + + String chat(String userMessage); +} \ No newline at end of file diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/customConfig/multipleChatModels/AssistantWithMultipleChatModelsIT.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/customConfig/multipleChatModels/AssistantWithMultipleChatModelsIT.java new file mode 100644 index 00000000..2bbc6536 --- /dev/null +++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/customConfig/multipleChatModels/AssistantWithMultipleChatModelsIT.java @@ -0,0 +1,32 @@ +package dev.langchain4j.service.spring.customConfig.multipleChatModels; + +import dev.langchain4j.exception.IllegalConfigurationException; +import dev.langchain4j.service.spring.AiServicesAutoConfig; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class AssistantWithMultipleChatModelsIT { + + ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(AiServicesAutoConfig.class)); + + @Test + void should_fail_to_create_AI_service_when_multiple_chat_models_are_found() { + contextRunner + .withUserConfiguration(TestApplicationWithMultipleChatModels.class) + .run(context -> { + + assertThatThrownBy(() -> context.getBean(AssistantWithMultipleChatModels.class)) + .isExactlyInstanceOf(IllegalStateException.class) + .hasCauseExactlyInstanceOf(IllegalConfigurationException.class) + .hasRootCauseMessage("Conflict: multiple beans of type " + + "dev.langchain4j.model.chat.ChatLanguageModel are found: " + + "[chatLanguageModel, chatLanguageModel2]. " + + "Please specify which one you wish to use in the @AiService annotation " + + "like this: @AiService(chatModel = \"\")."); + }); + } +} \ No newline at end of file diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/customConfig/multipleChatModels/TestApplicationWithMultipleChatModels.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/customConfig/multipleChatModels/TestApplicationWithMultipleChatModels.java new file mode 100644 index 00000000..2a28e89d --- /dev/null +++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/customConfig/multipleChatModels/TestApplicationWithMultipleChatModels.java @@ -0,0 +1,25 @@ +package dev.langchain4j.service.spring.customConfig.multipleChatModels; + +import dev.langchain4j.model.chat.ChatLanguageModel; +import dev.langchain4j.model.openai.OpenAiChatModel; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; + +@SpringBootApplication +public class TestApplicationWithMultipleChatModels { + + @Bean + ChatLanguageModel chatLanguageModel() { + return OpenAiChatModel.withApiKey(System.getenv("OPENAI_API_KEY")); + } + + @Bean + ChatLanguageModel chatLanguageModel2() { + return OpenAiChatModel.withApiKey(System.getenv("OPENAI_API_KEY")); + } + + public static void main(String[] args) { + SpringApplication.run(TestApplicationWithMultipleChatModels.class, args); + } +} diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/AiServicesAutoConfigIT.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/AiServicesAutoConfigIT.java new file mode 100644 index 00000000..7c816ec8 --- /dev/null +++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/AiServicesAutoConfigIT.java @@ -0,0 +1,219 @@ +package dev.langchain4j.service.spring.defaultConfig; + +import dev.langchain4j.data.message.AiMessage; +import dev.langchain4j.memory.chat.ChatMemoryProvider; +import dev.langchain4j.memory.chat.MessageWindowChatMemory; +import dev.langchain4j.model.chat.TestStreamingResponseHandler; +import dev.langchain4j.model.output.Response; +import dev.langchain4j.service.spring.AiServicesAutoConfig; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class AiServicesAutoConfigIT { + + private static final String API_KEY = System.getenv("OPENAI_API_KEY"); + + ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(AiServicesAutoConfig.class)); + + @Test + void should_create_AI_service_that_is_public_interface() { + contextRunner + .withPropertyValues( + "langchain4j.open-ai.chat-model.api-key=" + API_KEY, + "langchain4j.open-ai.chat-model.max-tokens=20", + "langchain4j.open-ai.chat-model.temperature=0.0" + ) + .withUserConfiguration(TestApplication.class) + .run(context -> { + + // given + PublicAssistant assistant = context.getBean(PublicAssistant.class); + + // when + String answer = assistant.chat("What is the capital of Germany?"); + + // then + assertThat(answer).containsIgnoringCase("Berlin"); + }); + } + + @Test + void should_create_AI_service_that_is_package_private_interface() { + contextRunner + .withPropertyValues( + "langchain4j.open-ai.chat-model.api-key=" + API_KEY, + "langchain4j.open-ai.chat-model.max-tokens=20", + "langchain4j.open-ai.chat-model.temperature=0.0" + ) + .withUserConfiguration(TestApplication.class) + .run(context -> { + + // given + PackagePrivateAssistant assistant = context.getBean(PackagePrivateAssistant.class); + + // when + String answer = assistant.chat("What is the capital of Germany?"); + + // then + assertThat(answer).containsIgnoringCase("Berlin"); + }); + } + + @Test + void should_create_AI_service_that_is_inner_interface() { + contextRunner + .withPropertyValues( + "langchain4j.open-ai.chat-model.api-key=" + API_KEY, + "langchain4j.open-ai.chat-model.max-tokens=20", + "langchain4j.open-ai.chat-model.temperature=0.0" + ) + .withUserConfiguration(TestApplication.class) + .run(context -> { + + // given + OuterClass.InnerAssistant assistant = context.getBean(OuterClass.InnerAssistant.class); + + // when + String answer = assistant.chat("What is the capital of Germany?"); + + // then + assertThat(answer).containsIgnoringCase("Berlin"); + }); + } + + @Test + void should_fail_to_create_AI_service_without_annotation() { + contextRunner + .withPropertyValues("langchain4j.open-ai.chat-model.api-key=" + API_KEY) + .withUserConfiguration(TestApplication.class) + .run(context -> { + + // when-then + assertThatThrownBy(() -> context.getBean(AssistantWithoutAnnotation.class)) + .isExactlyInstanceOf(NoSuchBeanDefinitionException.class); + }); + } + + @Test + void should_create_streaming_AI_service() { + contextRunner + .withPropertyValues( + "langchain4j.open-ai.streaming-chat-model.api-key=" + API_KEY, + "langchain4j.open-ai.streaming-chat-model.max-tokens=20", + "langchain4j.open-ai.streaming-chat-model.temperature=0.0" + ) + .withUserConfiguration(TestApplication.class) + .run(context -> { + + // given + StreamingAssistant assistant = context.getBean(StreamingAssistant.class); + + TestStreamingResponseHandler handler = new TestStreamingResponseHandler<>(); + + // when + assistant.chat("What is the capital of Germany?") + .onNext(handler::onNext) + .onComplete(handler::onComplete) + .onError(handler::onError) + .start(); + Response response = handler.get(); + + // then + assertThat(response.content().text()).containsIgnoringCase("Berlin"); + }); + } + + + static class ChatMemoryProviderConfig { + + @Bean + ChatMemoryProvider chatMemoryProvider() { + return memoryId -> MessageWindowChatMemory.withMaxMessages(10); + } + } + + @Test + void should_create_AI_service_with_chat_memory_provider() { + contextRunner + .withPropertyValues( + "langchain4j.open-ai.chat-model.api-key=" + API_KEY, + "langchain4j.open-ai.chat-model.max-tokens=20", + "langchain4j.open-ai.chat-model.temperature=0.0" + ) + .withUserConfiguration(TestApplication.class) + .withUserConfiguration(ChatMemoryProviderConfig.class) + .run(context -> { + + // given + PublicAssistant assistant = context.getBean(PublicAssistant.class); + assistant.chat("My name is Klaus"); + + // when + String answer = assistant.chat("What is my name?"); + + // then + assertThat(answer).containsIgnoringCase("Klaus"); + }); + } + + @Test + void should_create_AI_service_with_tool_which_is_public_method_in_public_class() { + contextRunner + .withPropertyValues( + "langchain4j.open-ai.chat-model.api-key=" + API_KEY, + "langchain4j.open-ai.chat-model.max-tokens=20", + "langchain4j.open-ai.chat-model.temperature=0.0" + ) + .withUserConfiguration(TestApplication.class) + .withUserConfiguration(ChatMemoryProviderConfig.class) + .run(context -> { + + // given + AssistantWithPublicTools assistant = context.getBean(AssistantWithPublicTools.class); + + // when + String answer = assistant.chat("What is the current hour?"); + + // then + assertThat(answer).contains(String.valueOf(LocalDateTime.now().getHour())); + }); + } + + @Test + void should_create_AI_service_with_tool_that_is_package_private_method_in_package_private_class() { + contextRunner + .withPropertyValues( + "langchain4j.open-ai.chat-model.api-key=" + API_KEY, + "langchain4j.open-ai.chat-model.max-tokens=20", + "langchain4j.open-ai.chat-model.temperature=0.0" + ) + .withUserConfiguration(TestApplication.class) + .withUserConfiguration(ChatMemoryProviderConfig.class) + .run(context -> { + + // given + AssistantWithPackagePrivateTools assistant = context.getBean(AssistantWithPackagePrivateTools.class); + + // when + String answer = assistant.chat("What is the current minute?"); + + // then + assertThat(answer).contains(String.valueOf(LocalDateTime.now().getMinute())); + }); + } + + // TODO tools which are not @Beans? + // TODO negative cases + // TODO no @AiServices in app, just models + // TODO @AiServices as inner class? + // TODO streaming, memory, tools, etc +} \ No newline at end of file diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/AssistantWithPackagePrivateTools.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/AssistantWithPackagePrivateTools.java new file mode 100644 index 00000000..93297cad --- /dev/null +++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/AssistantWithPackagePrivateTools.java @@ -0,0 +1,9 @@ +package dev.langchain4j.service.spring.defaultConfig; + +import dev.langchain4j.service.spring.AiService; + +@AiService(tools = PackagePrivateTools.class) +public interface AssistantWithPackagePrivateTools { + + String chat(String userMessage); +} \ No newline at end of file diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/AssistantWithPublicTools.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/AssistantWithPublicTools.java new file mode 100644 index 00000000..a6df744c --- /dev/null +++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/AssistantWithPublicTools.java @@ -0,0 +1,9 @@ +package dev.langchain4j.service.spring.defaultConfig; + +import dev.langchain4j.service.spring.AiService; + +@AiService(tools = PublicTools.class) +public interface AssistantWithPublicTools { + + String chat(String userMessage); +} \ No newline at end of file diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/AssistantWithoutAnnotation.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/AssistantWithoutAnnotation.java new file mode 100644 index 00000000..bc145ed3 --- /dev/null +++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/AssistantWithoutAnnotation.java @@ -0,0 +1,6 @@ +package dev.langchain4j.service.spring.defaultConfig; + +public interface AssistantWithoutAnnotation { + + String chat(String userMessage); +} \ No newline at end of file diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/OuterClass.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/OuterClass.java new file mode 100644 index 00000000..6a04a4c4 --- /dev/null +++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/OuterClass.java @@ -0,0 +1,12 @@ +package dev.langchain4j.service.spring.defaultConfig; + +import dev.langchain4j.service.spring.AiService; + +class OuterClass { + + @AiService + interface InnerAssistant { + + String chat(String userMessage); + } +} diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/PackagePrivateAssistant.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/PackagePrivateAssistant.java new file mode 100644 index 00000000..f8bdfdd9 --- /dev/null +++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/PackagePrivateAssistant.java @@ -0,0 +1,9 @@ +package dev.langchain4j.service.spring.defaultConfig; + +import dev.langchain4j.service.spring.AiService; + +@AiService +interface PackagePrivateAssistant { + + String chat(String userMessage); +} \ No newline at end of file diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/PackagePrivateTools.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/PackagePrivateTools.java new file mode 100644 index 00000000..e53d8fa5 --- /dev/null +++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/PackagePrivateTools.java @@ -0,0 +1,15 @@ +package dev.langchain4j.service.spring.defaultConfig; + +import dev.langchain4j.agent.tool.Tool; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; + +@Component +class PackagePrivateTools { + + @Tool + int getCurrentMinute() { + return LocalDateTime.now().getMinute(); + } +} diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/PublicAssistant.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/PublicAssistant.java new file mode 100644 index 00000000..37d8d675 --- /dev/null +++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/PublicAssistant.java @@ -0,0 +1,9 @@ +package dev.langchain4j.service.spring.defaultConfig; + +import dev.langchain4j.service.spring.AiService; + +@AiService +public interface PublicAssistant { + + String chat(String userMessage); +} \ No newline at end of file diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/PublicTools.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/PublicTools.java new file mode 100644 index 00000000..07efacdf --- /dev/null +++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/PublicTools.java @@ -0,0 +1,15 @@ +package dev.langchain4j.service.spring.defaultConfig; + +import dev.langchain4j.agent.tool.Tool; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; + +@Component +public class PublicTools { + + @Tool + public int getCurrentHour() { + return LocalDateTime.now().getHour(); + } +} diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/StreamingAssistant.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/StreamingAssistant.java new file mode 100644 index 00000000..3000d527 --- /dev/null +++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/StreamingAssistant.java @@ -0,0 +1,10 @@ +package dev.langchain4j.service.spring.defaultConfig; + +import dev.langchain4j.service.TokenStream; +import dev.langchain4j.service.spring.AiService; + +@AiService +public interface StreamingAssistant { + + TokenStream chat(String userMessage); +} \ No newline at end of file diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/TestApplication.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/TestApplication.java new file mode 100644 index 00000000..e4dbe9cf --- /dev/null +++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/TestApplication.java @@ -0,0 +1,12 @@ +package dev.langchain4j.service.spring.defaultConfig; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class TestApplication { + + public static void main(String[] args) { + SpringApplication.run(TestApplication.class, args); + } +} diff --git a/pom.xml b/pom.xml index c0f38a58..91839a13 100644 --- a/pom.xml +++ b/pom.xml @@ -14,6 +14,9 @@ https://github.com/langchain4j/langchain4j-spring + langchain4j-spring-boot-starter + langchain4j-easy-rag-spring-boot-starter + langchain4j-ollama-spring-boot-starter langchain4j-open-ai-spring-boot-starter From 91ad00d6f295a4a21d60f5eeab08c1e9dcd9aa33 Mon Sep 17 00:00:00 2001 From: LangChain4j Date: Sun, 24 Mar 2024 21:05:59 +0100 Subject: [PATCH 2/5] WIP: declarative AI services and EasyRAG --- .../langchain4j/rag/spring/RagAutoConfig.java | 17 +- .../langchain4j/service/spring/AiService.java | 71 +++++- .../service/spring/AiServicesAutoConfig.java | 202 +++++++++++------- .../AssistantWithPackagePrivateTools.java | 2 +- .../AssistantWithPublicTools.java | 2 +- .../defaultConfig/PackagePrivateTools.java | 2 +- .../spring/defaultConfig/PublicTools.java | 2 +- 7 files changed, 198 insertions(+), 100 deletions(-) diff --git a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/rag/spring/RagAutoConfig.java b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/rag/spring/RagAutoConfig.java index a885d2f7..67eb1e3b 100644 --- a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/rag/spring/RagAutoConfig.java +++ b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/rag/spring/RagAutoConfig.java @@ -18,7 +18,7 @@ public class RagAutoConfig { @Bean @ConditionalOnMissingBean - EmbeddingStore embeddingStore() { // TODO bean name + EmbeddingStore embeddingStore() { // TODO bean name, type return new InMemoryEmbeddingStore<>(); } @@ -30,18 +30,19 @@ EmbeddingStore embeddingStore() { // TODO bean name @ConditionalOnMissingBean ContentRetriever contentRetriever(EmbeddingModel embeddingModel, EmbeddingStore embeddingStore, - RagProperties ragProperties) { // TODO bean name + RagProperties ragProperties) { // TODO bean name, type EmbeddingStoreContentRetriever.EmbeddingStoreContentRetrieverBuilder builder = EmbeddingStoreContentRetriever.builder() .embeddingStore(embeddingStore) .embeddingModel(embeddingModel); - // TODO ragProperties can be null? - RetrievalProperties retrievalProperties = ragProperties.getRetrieval(); - if (retrievalProperties != null) { - builder - .maxResults(retrievalProperties.maxResults) - .minScore(retrievalProperties.minScore); + if (ragProperties != null) { + RetrievalProperties retrievalProperties = ragProperties.getRetrieval(); + if (retrievalProperties != null) { + builder + .maxResults(retrievalProperties.maxResults) + .minScore(retrievalProperties.minScore); + } } return builder.build(); diff --git a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiService.java b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiService.java index 87b001b9..a049818f 100644 --- a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiService.java +++ b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiService.java @@ -1,5 +1,14 @@ package dev.langchain4j.service.spring; +import dev.langchain4j.agent.tool.Tool; +import dev.langchain4j.memory.ChatMemory; +import dev.langchain4j.memory.chat.ChatMemoryProvider; +import dev.langchain4j.model.chat.ChatLanguageModel; +import dev.langchain4j.model.chat.StreamingChatLanguageModel; +import dev.langchain4j.rag.RetrievalAugmentor; +import dev.langchain4j.rag.content.retriever.ContentRetriever; +import dev.langchain4j.service.AiServices; + import java.lang.annotation.Retention; import java.lang.annotation.Target; @@ -7,20 +16,70 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME; /** - * TODO - * TODO copy from AiServices - * TODO Flux + * Any interface annotated with {@code @AiService} will be automatically registered as a bean + * and configured to use all the following components (beans) available in the context: + *

+ * - {@link ChatLanguageModel}
+ * - {@link StreamingChatLanguageModel}
+ * - {@link ChatMemory}
+ * - {@link ChatMemoryProvider}
+ * - {@link ContentRetriever}
+ * - {@link RetrievalAugmentor}
+ * - All beans containing methods annotated with {@code @}{@link Tool}
+ * 
+ * You can also explicitly specify which components this AI Service should use by specifying bean names + * using the following properties: + *
+ * - {@link #chatModel()}
+ * - {@link #streamingChatModel()}
+ * - {@link #chatMemory()}
+ * - {@link #chatMemoryProvider()}
+ * - {@link #contentRetriever()}
+ * - {@link #retrievalAugmentor()}
+ * 
+ * See more information about AI Services here + * and in the Javadoc of {@link AiServices}. + * + * @see AiServices */ @Target(TYPE) @Retention(RUNTIME) public @interface AiService { /** - * TODO + * The name of a {@link ChatLanguageModel} bean that should be used by this AI Service. */ String chatModel() default ""; - // TODO make the rest of components configurable + /** + * The name of a {@link StreamingChatLanguageModel} bean that should be used by this AI Service. + */ + String streamingChatModel() default ""; + + /** + * The name of a {@link ChatMemory} bean that should be used by this AI Service. + */ + String chatMemory() default ""; + + /** + * The name of a {@link ChatMemoryProvider} bean that should be used by this AI Service. + */ + String chatMemoryProvider() default ""; + + /** + * The name of a {@link ContentRetriever} bean that should be used by this AI Service. + */ + String contentRetriever() default ""; + + /** + * The name of a {@link RetrievalAugmentor} bean that should be used by this AI Service. + */ + String retrievalAugmentor() default ""; + + /** + * The names of beans containing methods annotated with {@link Tool} that should be used by this AI Service. + */ + String[] tools() default {}; - Class[] tools() default {}; // TODO use all available tools by default? + // TODO support Flux return type for AI Service method(s) (for streaming) } \ No newline at end of file diff --git a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java index 5af88fe2..ebc105fa 100644 --- a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java +++ b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java @@ -1,5 +1,6 @@ package dev.langchain4j.service.spring; +import dev.langchain4j.agent.tool.Tool; import dev.langchain4j.exception.IllegalConfigurationException; import dev.langchain4j.memory.ChatMemory; import dev.langchain4j.memory.chat.ChatMemoryProvider; @@ -8,8 +9,8 @@ import dev.langchain4j.rag.RetrievalAugmentor; import dev.langchain4j.rag.content.retriever.ContentRetriever; import org.reflections.Reflections; -import org.springframework.beans.BeansException; import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.RuntimeBeanReference; @@ -18,7 +19,10 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; +import java.lang.reflect.Method; import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; import static dev.langchain4j.exception.IllegalConfigurationException.illegalConfiguration; import static dev.langchain4j.internal.Utils.isNotNullOrBlank; @@ -28,96 +32,130 @@ public class AiServicesAutoConfig { @Bean BeanFactoryPostProcessor aiServicesRegisteringBeanFactoryPostProcessor() { - return new BeanFactoryPostProcessor() { - - @Override - public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { - -// Set> allToolBeans = new HashSet<>(); -// -// for (String beanName : registry.getBeanDefinitionNames()) { -// BeanDefinition beanDefinition = registry.getBeanDefinition(beanName); -// try { -// Class beanClass = Class.forName(beanDefinition.getBeanClassName()); -// for (Method beanMethod : beanClass.getDeclaredMethods()) { -// if (beanMethod.isAnnotationPresent(Tool.class)) { -// allToolBeans.add(beanClass); -// } -// } -// } catch (Exception e) { -// // TODO -// } -// } - - String[] app = beanFactory.getBeanNamesForAnnotation(SpringBootApplication.class); - String basePackage = beanFactory.getBeanDefinition(app[0]).getResolvableType().resolve().getPackage().getName(); - - String[] chatLanguageModels = beanFactory.getBeanNamesForType(ChatLanguageModel.class); - String[] streamingChatLanguageModels = beanFactory.getBeanNamesForType(StreamingChatLanguageModel.class); - String[] chatMemories = beanFactory.getBeanNamesForType(ChatMemory.class); - String[] chatMemoryProviders = beanFactory.getBeanNamesForType(ChatMemoryProvider.class); - String[] contentRetrievers = beanFactory.getBeanNamesForType(ContentRetriever.class); - String[] retrievalAugmentors = beanFactory.getBeanNamesForType(RetrievalAugmentor.class); - - Reflections reflections = new Reflections(basePackage); - reflections.getTypesAnnotatedWith(AiService.class).forEach(aiService -> { - - if (beanFactory.getBeanNamesForType(aiService).length > 0) { - // User probably wants to configure AI Service bean manually - // TODO or better fail because he should not annotate it with @AiService then? - return; - } - - AiService annotation = aiService.getAnnotation(AiService.class); - - GenericBeanDefinition beanDefinition = new GenericBeanDefinition(); - beanDefinition.setBeanClass(AiServiceFactory.class); - beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(aiService); - MutablePropertyValues propertyValues = beanDefinition.getPropertyValues(); - - if (isNotNullOrBlank(annotation.chatModel())) { - propertyValues.add("chatLanguageModel", new RuntimeBeanReference(annotation.chatModel())); - } else { - if (chatLanguageModels.length == 1) { - propertyValues.add("chatLanguageModel", new RuntimeBeanReference(chatLanguageModels[0])); - } else if (chatLanguageModels.length > 1) { - throw conflict(ChatLanguageModel.class, chatLanguageModels); + return beanFactory -> { + + String[] chatLanguageModels = beanFactory.getBeanNamesForType(ChatLanguageModel.class); + String[] streamingChatLanguageModels = beanFactory.getBeanNamesForType(StreamingChatLanguageModel.class); + String[] chatMemories = beanFactory.getBeanNamesForType(ChatMemory.class); + String[] chatMemoryProviders = beanFactory.getBeanNamesForType(ChatMemoryProvider.class); + String[] contentRetrievers = beanFactory.getBeanNamesForType(ContentRetriever.class); + String[] retrievalAugmentors = beanFactory.getBeanNamesForType(RetrievalAugmentor.class); + + Set beansWithTools = new HashSet<>(); + for (String beanName : beanFactory.getBeanDefinitionNames()) { + try { + Class beanClass = Class.forName(beanFactory.getBeanDefinition(beanName).getBeanClassName()); + for (Method beanMethod : beanClass.getDeclaredMethods()) { + if (beanMethod.isAnnotationPresent(Tool.class)) { + beansWithTools.add(beanName); } } + } catch (Exception e) { + // TODO + } + } - // TODO handle conflicts for all other components - - if (streamingChatLanguageModels.length == 1) { - propertyValues.add("streamingChatLanguageModel", new RuntimeBeanReference(streamingChatLanguageModels[0])); - } - - if (chatMemories.length == 1) { - propertyValues.add("chatMemory", new RuntimeBeanReference(chatMemories[0])); + findAiServices(beanFactory).forEach(aiServiceClass -> { + + if (beanFactory.getBeanNamesForType(aiServiceClass).length > 0) { + // User probably wants to configure AI Service bean manually + // TODO or better fail because user should not annotate it with @AiService then? + return; + } + + GenericBeanDefinition aiServiceBeanDefinition = new GenericBeanDefinition(); + aiServiceBeanDefinition.setBeanClass(AiServiceFactory.class); + aiServiceBeanDefinition.getConstructorArgumentValues().addGenericArgumentValue(aiServiceClass); + MutablePropertyValues propertyValues = aiServiceBeanDefinition.getPropertyValues(); + + AiService aiServiceAnnotation = aiServiceClass.getAnnotation(AiService.class); + + addBeanReference( + ChatLanguageModel.class, + aiServiceAnnotation.chatModel(), + chatLanguageModels, + "chatLanguageModel", + propertyValues + ); + + addBeanReference( + StreamingChatLanguageModel.class, + aiServiceAnnotation.streamingChatModel(), + streamingChatLanguageModels, + "streamingChatLanguageModel", + propertyValues + ); + + addBeanReference( + ChatMemory.class, + aiServiceAnnotation.chatMemory(), + chatMemories, + "chatMemory", + propertyValues + ); + + addBeanReference( + ChatMemoryProvider.class, + aiServiceAnnotation.chatMemoryProvider(), + chatMemoryProviders, + "chatMemoryProvider", + propertyValues + ); + + addBeanReference( + ContentRetriever.class, + aiServiceAnnotation.contentRetriever(), + contentRetrievers, + "contentRetriever", + propertyValues + ); + + addBeanReference( + RetrievalAugmentor.class, + aiServiceAnnotation.retrievalAugmentor(), + retrievalAugmentors, + "retrievalAugmentor", + propertyValues + ); + + if (aiServiceAnnotation.tools().length > 0) { + for (String beanWithTools : aiServiceAnnotation.tools()) { + propertyValues.add("beanWithTools", new RuntimeBeanReference(beanWithTools)); } - - if (chatMemoryProviders.length == 1) { - propertyValues.add("chatMemoryProvider", new RuntimeBeanReference(chatMemoryProviders[0])); + } else { + for (String beanWithTools : beansWithTools) { + propertyValues.add("beanWithTools", new RuntimeBeanReference(beanWithTools)); } + } - if (contentRetrievers.length == 1) { - propertyValues.add("contentRetriever", new RuntimeBeanReference(contentRetrievers[0])); - } - - if (retrievalAugmentors.length == 1) { - propertyValues.add("retrievalAugmentor", new RuntimeBeanReference(retrievalAugmentors[0])); - } + BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory; + registry.registerBeanDefinition(lowercaseFirstLetter(aiServiceClass.getSimpleName()), aiServiceBeanDefinition); + }); + }; + } - for (Class classWithTools : annotation.tools()) { - for (String beanWithTools : beanFactory.getBeanNamesForType(classWithTools)) { - propertyValues.add("beanWithTools", new RuntimeBeanReference(beanWithTools)); - } - } + private static Set> findAiServices(ConfigurableListableBeanFactory beanFactory) { + String[] applicationBean = beanFactory.getBeanNamesForAnnotation(SpringBootApplication.class); + BeanDefinition applicationBeanDefinition = beanFactory.getBeanDefinition(applicationBean[0]); + String basePackage = applicationBeanDefinition.getResolvableType().resolve().getPackage().getName(); + Reflections reflections = new Reflections(basePackage); + return reflections.getTypesAnnotatedWith(AiService.class); + } - BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory; - registry.registerBeanDefinition(lowercaseFirstLetter(aiService.getSimpleName()), beanDefinition); - }); + private static void addBeanReference(Class beanType, + String customBeanName, + String[] beanNames, + String propertyName, + MutablePropertyValues propertyValues) { + if (isNotNullOrBlank(customBeanName)) { + propertyValues.add(propertyName, new RuntimeBeanReference(customBeanName)); + } else { + if (beanNames.length == 1) { + propertyValues.add(propertyName, new RuntimeBeanReference(beanNames[0])); + } else if (beanNames.length > 1) { + throw conflict(beanType, beanNames); } - }; + } } private static IllegalConfigurationException conflict(Class beanType, Object[] beanNames) { diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/AssistantWithPackagePrivateTools.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/AssistantWithPackagePrivateTools.java index 93297cad..c570ba74 100644 --- a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/AssistantWithPackagePrivateTools.java +++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/AssistantWithPackagePrivateTools.java @@ -2,7 +2,7 @@ import dev.langchain4j.service.spring.AiService; -@AiService(tools = PackagePrivateTools.class) +@AiService(tools = {"packagePrivateTools"}) public interface AssistantWithPackagePrivateTools { String chat(String userMessage); diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/AssistantWithPublicTools.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/AssistantWithPublicTools.java index a6df744c..63be3e25 100644 --- a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/AssistantWithPublicTools.java +++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/AssistantWithPublicTools.java @@ -2,7 +2,7 @@ import dev.langchain4j.service.spring.AiService; -@AiService(tools = PublicTools.class) +@AiService(tools = {"publicTools"}) public interface AssistantWithPublicTools { String chat(String userMessage); diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/PackagePrivateTools.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/PackagePrivateTools.java index e53d8fa5..55b181cc 100644 --- a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/PackagePrivateTools.java +++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/PackagePrivateTools.java @@ -5,7 +5,7 @@ import java.time.LocalDateTime; -@Component +@Component("packagePrivateTools") class PackagePrivateTools { @Tool diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/PublicTools.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/PublicTools.java index 07efacdf..0ede75e8 100644 --- a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/PublicTools.java +++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/PublicTools.java @@ -5,7 +5,7 @@ import java.time.LocalDateTime; -@Component +@Component("publicTools") public class PublicTools { @Tool From 83ace7738304c0fc81d8b59b454dc9211903a31a Mon Sep 17 00:00:00 2001 From: LangChain4j Date: Mon, 25 Mar 2024 17:59:22 +0100 Subject: [PATCH 3/5] WIP: declarative AI services and EasyRAG --- langchain4j-spring-boot-starter/pom.xml | 14 +- .../langchain4j/service/spring/AiService.java | 36 ++- .../service/spring/AiServiceFactory.java | 11 +- .../service/spring/AiServiceWiringMode.java | 20 ++ .../service/spring/AiServicesAutoConfig.java | 69 ++++-- .../AssistantWithCustomChatModel.java | 11 - .../AssistantWithMultipleChatModels.java | 9 - .../defaultConfig/AiServicesAutoConfigIT.java | 219 ------------------ .../AssistantWithPackagePrivateTools.java | 9 - .../AssistantWithPublicTools.java | 9 - .../AssistantWithoutAnnotation.java | 6 - .../spring/defaultConfig/PublicAssistant.java | 9 - .../spring/defaultConfig/PublicTools.java | 15 -- .../service/spring/mode/ApiKeys.java | 7 + .../AiServiceWithConflictingChatMemories.java | 9 + ...iServiceWithConflictingChatMemoriesIT.java | 32 +++ ...eWithConflictingChatModelsApplication.java | 25 ++ ...ServiceWithConflictingChatMemoriesIT.java} | 14 +- .../AiServiceWithConflictingChatModels.java | 9 + ...WithConflictingChatModelsApplication.java} | 6 +- ...WithConflictingSyncAndStreamingModels.java | 10 + ...tingSyncAndStreamingModelsApplication.java | 21 ++ ...thConflictingSyncAndStreamingModelsIT.java | 47 ++++ ...WithConflictingSyncAndStreamingModels.java | 9 + ...tingSyncAndStreamingModelsApplication.java | 21 ++ ...thConflictingSyncAndStreamingModelsIT.java | 37 +++ .../InnerClassAiServiceApplication.java | 12 + .../innerClass/InnerClassAiServiceIT.java | 37 +++ .../automatic/innerClass}/OuterClass.java | 4 +- ...rviceWithMissingAnnotationApplication.java | 12 + .../AiServiceWithMissingAnnotationIT.java | 29 +++ .../AssistantWithMissingAnnotation.java | 7 + .../PackagePrivateAiService.java | 9 + .../PackagePrivateAiServiceApplication.java | 12 + .../PackagePrivateAiServiceIT.java | 37 +++ .../publicClass/PublicAiService.java | 9 + .../PublicAiServiceApplication.java} | 6 +- .../publicClass/PublicAiServiceIT.java} | 17 +- .../streaming/StreamingAiService.java} | 4 +- .../StreamingAiServiceApplication.java | 12 + .../streaming/StreamingAiServiceIT.java | 47 ++++ .../AiServiceWithChatMemory.java | 9 + .../AiServiceWithChatMemoryApplication.java | 20 ++ .../AiServiceWithChatMemoryProviderIT.java | 38 +++ .../AiServiceWithChatMemoryProvider.java | 9 + ...viceWithChatMemoryProviderApplication.java | 20 ++ .../AiServiceWithChatMemoryProviderIT.java | 38 +++ .../AiServiceWithContentRetriever.java | 9 + ...erviceWithContentRetrieverApplication.java | 22 ++ .../AiServiceWithRetrievalAugmentorIT.java | 37 +++ .../AiServiceWithRetrievalAugmentor.java | 9 + ...viceWithRetrievalAugmentorApplication.java | 20 ++ .../AiServiceWithRetrievalAugmentorIT.java | 37 +++ .../withTools/AiServiceWithTools.java} | 6 +- .../AiServiceWithToolsApplication.java | 12 + .../withTools/AiServicesAutoConfigIT.java | 70 ++++++ .../withTools}/PackagePrivateTools.java | 4 +- .../mode/automatic/withTools/PublicTools.java | 15 ++ .../AiServiceWithExplicitChatModel.java | 12 + ...viceWithExplicitChatModelApplication.java} | 16 +- .../AiServiceWithExplicitChatModelIT.java | 31 +++ .../mode/explicit/multiple/BaseAiService.java | 6 + .../FirstAiServiceWithAutomaticWiring.java | 10 + .../FirstAiServiceWithExplicitWiring.java | 13 ++ .../MultipleAiServicesApplication.java | 29 +++ .../multiple/MultipleAiServicesIT.java | 54 +++++ .../SecondAiServiceWithAutomaticWiring.java | 10 + .../SecondAiServiceWithExplicitWiring.java | 13 ++ 68 files changed, 1148 insertions(+), 359 deletions(-) create mode 100644 langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServiceWiringMode.java delete mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/customConfig/chatModel/AssistantWithCustomChatModel.java delete mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/customConfig/multipleChatModels/AssistantWithMultipleChatModels.java delete mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/AiServicesAutoConfigIT.java delete mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/AssistantWithPackagePrivateTools.java delete mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/AssistantWithPublicTools.java delete mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/AssistantWithoutAnnotation.java delete mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/PublicAssistant.java delete mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/PublicTools.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/ApiKeys.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/conflictingChatMemories/AiServiceWithConflictingChatMemories.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/conflictingChatMemories/AiServiceWithConflictingChatMemoriesIT.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/conflictingChatMemories/AiServiceWithConflictingChatModelsApplication.java rename langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/{customConfig/multipleChatModels/AssistantWithMultipleChatModelsIT.java => mode/automatic/conflictingChatModels/AiServiceWithConflictingChatMemoriesIT.java} (66%) create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/conflictingChatModels/AiServiceWithConflictingChatModels.java rename langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/{customConfig/multipleChatModels/TestApplicationWithMultipleChatModels.java => mode/automatic/conflictingChatModels/AiServiceWithConflictingChatModelsApplication.java} (74%) create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/conflictingSyncAndStreamingModels/streaming/AiServiceWithConflictingSyncAndStreamingModels.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/conflictingSyncAndStreamingModels/streaming/AiServiceWithConflictingSyncAndStreamingModelsApplication.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/conflictingSyncAndStreamingModels/streaming/AiServiceWithConflictingSyncAndStreamingModelsIT.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/conflictingSyncAndStreamingModels/sync/AiServiceWithConflictingSyncAndStreamingModels.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/conflictingSyncAndStreamingModels/sync/AiServiceWithConflictingSyncAndStreamingModelsApplication.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/conflictingSyncAndStreamingModels/sync/AiServiceWithConflictingSyncAndStreamingModelsIT.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/innerClass/InnerClassAiServiceApplication.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/innerClass/InnerClassAiServiceIT.java rename langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/{defaultConfig => mode/automatic/innerClass}/OuterClass.java (58%) create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/missingAnnotation/AiServiceWithMissingAnnotationApplication.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/missingAnnotation/AiServiceWithMissingAnnotationIT.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/missingAnnotation/AssistantWithMissingAnnotation.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/packagePrivateClass/PackagePrivateAiService.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/packagePrivateClass/PackagePrivateAiServiceApplication.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/packagePrivateClass/PackagePrivateAiServiceIT.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/publicClass/PublicAiService.java rename langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/{defaultConfig/TestApplication.java => mode/automatic/publicClass/PublicAiServiceApplication.java} (53%) rename langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/{customConfig/chatModel/AssistantWithCustomChatModelIT.java => mode/automatic/publicClass/PublicAiServiceIT.java} (57%) rename langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/{defaultConfig/StreamingAssistant.java => mode/automatic/streaming/StreamingAiService.java} (60%) create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/streaming/StreamingAiServiceApplication.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/streaming/StreamingAiServiceIT.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withChatMemory/AiServiceWithChatMemory.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withChatMemory/AiServiceWithChatMemoryApplication.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withChatMemory/AiServiceWithChatMemoryProviderIT.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withChatMemoryProvider/AiServiceWithChatMemoryProvider.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withChatMemoryProvider/AiServiceWithChatMemoryProviderApplication.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withChatMemoryProvider/AiServiceWithChatMemoryProviderIT.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withContentRetriever/AiServiceWithContentRetriever.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withContentRetriever/AiServiceWithContentRetrieverApplication.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withContentRetriever/AiServiceWithRetrievalAugmentorIT.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withRetrievalAugmentor/AiServiceWithRetrievalAugmentor.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withRetrievalAugmentor/AiServiceWithRetrievalAugmentorApplication.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withRetrievalAugmentor/AiServiceWithRetrievalAugmentorIT.java rename langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/{defaultConfig/PackagePrivateAssistant.java => mode/automatic/withTools/AiServiceWithTools.java} (50%) create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/AiServiceWithToolsApplication.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/AiServicesAutoConfigIT.java rename langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/{defaultConfig => mode/automatic/withTools}/PackagePrivateTools.java (73%) create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/PublicTools.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/explicit/chatModel/AiServiceWithExplicitChatModel.java rename langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/{customConfig/chatModel/TestApplicationWithCustomChatModel.java => mode/explicit/chatModel/AiServiceWithExplicitChatModelApplication.java} (55%) create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/explicit/chatModel/AiServiceWithExplicitChatModelIT.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/explicit/multiple/BaseAiService.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/explicit/multiple/FirstAiServiceWithAutomaticWiring.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/explicit/multiple/FirstAiServiceWithExplicitWiring.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/explicit/multiple/MultipleAiServicesApplication.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/explicit/multiple/MultipleAiServicesIT.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/explicit/multiple/SecondAiServiceWithAutomaticWiring.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/explicit/multiple/SecondAiServiceWithExplicitWiring.java diff --git a/langchain4j-spring-boot-starter/pom.xml b/langchain4j-spring-boot-starter/pom.xml index 9eb1f8b0..a8b89b96 100644 --- a/langchain4j-spring-boot-starter/pom.xml +++ b/langchain4j-spring-boot-starter/pom.xml @@ -12,7 +12,6 @@ langchain4j-spring-boot-starter - Spring Boot starter for LangChain4j @@ -76,6 +75,19 @@ test + + org.tinylog + tinylog-impl + 2.6.2 + test + + + org.tinylog + slf4j-tinylog + 2.6.2 + test + + diff --git a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiService.java b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiService.java index a049818f..5a211085 100644 --- a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiService.java +++ b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiService.java @@ -12,12 +12,13 @@ import java.lang.annotation.Retention; import java.lang.annotation.Target; +import static dev.langchain4j.service.spring.AiServiceWiringMode.AUTOMATIC; import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; /** - * Any interface annotated with {@code @AiService} will be automatically registered as a bean - * and configured to use all the following components (beans) available in the context: + * An interface annotated with {@code @AiService} will be automatically registered as a bean + * and wired with all the following components (beans) available in the context: *
  * - {@link ChatLanguageModel}
  * - {@link StreamingChatLanguageModel}
@@ -27,8 +28,9 @@
  * - {@link RetrievalAugmentor}
  * - All beans containing methods annotated with {@code @}{@link Tool}
  * 
- * You can also explicitly specify which components this AI Service should use by specifying bean names - * using the following properties: + * You can also explicitly specify which components (beans) should be wired into this AI Service + * by setting {@link #wiringMode()} to {@link AiServiceWiringMode#EXPLICIT} + * and specifying bean names using the following attributes: *
  * - {@link #chatModel()}
  * - {@link #streamingChatModel()}
@@ -47,37 +49,49 @@
 public @interface AiService {
 
     /**
-     * The name of a {@link ChatLanguageModel} bean that should be used by this AI Service.
+     * Specifies how LangChain4j components (beans) are wired (injected) into this AI Service.
+     */
+    AiServiceWiringMode wiringMode() default AUTOMATIC;
+
+    /**
+     * When the {@link #wiringMode()} is set to {@link AiServiceWiringMode#EXPLICIT},
+     * this attribute specifies the name of a {@link ChatLanguageModel} bean that should be used by this AI Service.
      */
     String chatModel() default "";
 
     /**
-     * The name of a {@link StreamingChatLanguageModel} bean that should be used by this AI Service.
+     * When the {@link #wiringMode()} is set to {@link AiServiceWiringMode#EXPLICIT},
+     * this attribute specifies the name of a {@link StreamingChatLanguageModel} bean that should be used by this AI Service.
      */
     String streamingChatModel() default "";
 
     /**
-     * The name of a {@link ChatMemory} bean that should be used by this AI Service.
+     * When the {@link #wiringMode()} is set to {@link AiServiceWiringMode#EXPLICIT},
+     * this attribute specifies the name of a {@link ChatMemory} bean that should be used by this AI Service.
      */
     String chatMemory() default "";
 
     /**
-     * The name of a {@link ChatMemoryProvider} bean that should be used by this AI Service.
+     * When the {@link #wiringMode()} is set to {@link AiServiceWiringMode#EXPLICIT},
+     * this attribute specifies the name of a {@link ChatMemoryProvider} bean that should be used by this AI Service.
      */
     String chatMemoryProvider() default "";
 
     /**
-     * The name of a {@link ContentRetriever} bean that should be used by this AI Service.
+     * When the {@link #wiringMode()} is set to {@link AiServiceWiringMode#EXPLICIT},
+     * this attribute specifies the name of a {@link ContentRetriever} bean that should be used by this AI Service.
      */
     String contentRetriever() default "";
 
     /**
-     * The name of a {@link RetrievalAugmentor} bean that should be used by this AI Service.
+     * When the {@link #wiringMode()} is set to {@link AiServiceWiringMode#EXPLICIT},
+     * this attribute specifies the name of a {@link RetrievalAugmentor} bean that should be used by this AI Service.
      */
     String retrievalAugmentor() default "";
 
     /**
-     * The names of beans containing methods annotated with {@link Tool} that should be used by this AI Service.
+     * When the {@link #wiringMode()} is set to {@link AiServiceWiringMode#EXPLICIT},
+     * this attribute specifies the names of beans containing methods annotated with {@link Tool} that should be used by this AI Service.
      */
     String[] tools() default {};
 
diff --git a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServiceFactory.java b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServiceFactory.java
index 6cf21885..7ce673a0 100644
--- a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServiceFactory.java
+++ b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServiceFactory.java
@@ -9,7 +9,6 @@
 import dev.langchain4j.service.AiServices;
 import org.springframework.beans.factory.FactoryBean;
 
-import java.util.ArrayList;
 import java.util.List;
 
 import static dev.langchain4j.internal.Utils.isNullOrEmpty;
@@ -23,7 +22,7 @@ class AiServiceFactory implements FactoryBean {
     private ChatMemoryProvider chatMemoryProvider;
     private ContentRetriever contentRetriever;
     private RetrievalAugmentor retrievalAugmentor;
-    private final List beansWithTools = new ArrayList<>();
+    private List tools;
 
     public AiServiceFactory(Class aiServiceClass) {
         this.aiServiceClass = aiServiceClass;
@@ -53,8 +52,8 @@ public void setRetrievalAugmentor(RetrievalAugmentor retrievalAugmentor) {
         this.retrievalAugmentor = retrievalAugmentor;
     }
 
-    public void setBeanWithTools(Object beanWithTools) {
-        this.beansWithTools.add(beanWithTools);
+    public void setTools(List tools) {
+        this.tools = tools;
     }
 
     @Override
@@ -86,8 +85,8 @@ public Object getObject() {
             builder = builder.retrievalAugmentor(retrievalAugmentor);
         }
 
-        if (!isNullOrEmpty(beansWithTools)) {
-            builder = builder.tools(beansWithTools);
+        if (!isNullOrEmpty(tools)) {
+            builder = builder.tools(tools);
         }
 
         return builder.build();
diff --git a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServiceWiringMode.java b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServiceWiringMode.java
new file mode 100644
index 00000000..f2b9a5c9
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServiceWiringMode.java
@@ -0,0 +1,20 @@
+package dev.langchain4j.service.spring;
+
+/**
+ * Specifies how LangChain4j components are wired (injected) into a given AI Service.
+ */
+public enum AiServiceWiringMode {
+
+    /**
+     * All LangChain4j components available in the application context are wired automatically into a given AI Service.
+     * If there are multiple components of the same type, an exception is thrown.
+     */
+    AUTOMATIC,
+
+    /**
+     * Only explicitly specified LangChain4j components are wired into a given AI Service.
+     * Component (bean) names are specified using attributes of {@link AiService} annotation like this:
+     * {@code AiService(wiringMode = EXPLICIT, chatMemory = "")}
+     */
+    EXPLICIT
+}
\ No newline at end of file
diff --git a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java
index ebc105fa..6c6d52f1 100644
--- a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java
+++ b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java
@@ -16,17 +16,23 @@
 import org.springframework.beans.factory.config.RuntimeBeanReference;
 import org.springframework.beans.factory.support.BeanDefinitionRegistry;
 import org.springframework.beans.factory.support.GenericBeanDefinition;
+import org.springframework.beans.factory.support.ManagedList;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 import org.springframework.context.annotation.Bean;
 
 import java.lang.reflect.Method;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.HashSet;
 import java.util.Set;
 
 import static dev.langchain4j.exception.IllegalConfigurationException.illegalConfiguration;
+import static dev.langchain4j.internal.Exceptions.illegalArgument;
 import static dev.langchain4j.internal.Utils.isNotNullOrBlank;
 import static dev.langchain4j.internal.Utils.isNullOrBlank;
+import static dev.langchain4j.service.spring.AiServiceWiringMode.AUTOMATIC;
+import static dev.langchain4j.service.spring.AiServiceWiringMode.EXPLICIT;
+import static java.util.Arrays.asList;
 
 public class AiServicesAutoConfig {
 
@@ -34,6 +40,7 @@ public class AiServicesAutoConfig {
     BeanFactoryPostProcessor aiServicesRegisteringBeanFactoryPostProcessor() {
         return beanFactory -> {
 
+            // all components available in the application context
             String[] chatLanguageModels = beanFactory.getBeanNamesForType(ChatLanguageModel.class);
             String[] streamingChatLanguageModels = beanFactory.getBeanNamesForType(StreamingChatLanguageModel.class);
             String[] chatMemories = beanFactory.getBeanNamesForType(ChatMemory.class);
@@ -41,13 +48,14 @@ BeanFactoryPostProcessor aiServicesRegisteringBeanFactoryPostProcessor() {
             String[] contentRetrievers = beanFactory.getBeanNamesForType(ContentRetriever.class);
             String[] retrievalAugmentors = beanFactory.getBeanNamesForType(RetrievalAugmentor.class);
 
-            Set beansWithTools = new HashSet<>();
+            Set tools = new HashSet<>();
             for (String beanName : beanFactory.getBeanDefinitionNames()) {
                 try {
                     Class beanClass = Class.forName(beanFactory.getBeanDefinition(beanName).getBeanClassName());
+                    System.out.println();
                     for (Method beanMethod : beanClass.getDeclaredMethods()) {
                         if (beanMethod.isAnnotationPresent(Tool.class)) {
-                            beansWithTools.add(beanName);
+                            tools.add(beanName);
                         }
                     }
                 } catch (Exception e) {
@@ -72,60 +80,70 @@ BeanFactoryPostProcessor aiServicesRegisteringBeanFactoryPostProcessor() {
 
                 addBeanReference(
                         ChatLanguageModel.class,
+                        aiServiceAnnotation,
                         aiServiceAnnotation.chatModel(),
                         chatLanguageModels,
+                        "chatModel",
                         "chatLanguageModel",
                         propertyValues
                 );
 
                 addBeanReference(
                         StreamingChatLanguageModel.class,
+                        aiServiceAnnotation,
                         aiServiceAnnotation.streamingChatModel(),
                         streamingChatLanguageModels,
+                        "streamingChatModel",
                         "streamingChatLanguageModel",
                         propertyValues
                 );
 
                 addBeanReference(
                         ChatMemory.class,
+                        aiServiceAnnotation,
                         aiServiceAnnotation.chatMemory(),
                         chatMemories,
                         "chatMemory",
+                        "chatMemory",
                         propertyValues
                 );
 
                 addBeanReference(
                         ChatMemoryProvider.class,
+                        aiServiceAnnotation,
                         aiServiceAnnotation.chatMemoryProvider(),
                         chatMemoryProviders,
                         "chatMemoryProvider",
+                        "chatMemoryProvider",
                         propertyValues
                 );
 
                 addBeanReference(
                         ContentRetriever.class,
+                        aiServiceAnnotation,
                         aiServiceAnnotation.contentRetriever(),
                         contentRetrievers,
                         "contentRetriever",
+                        "contentRetriever",
                         propertyValues
                 );
 
                 addBeanReference(
                         RetrievalAugmentor.class,
+                        aiServiceAnnotation,
                         aiServiceAnnotation.retrievalAugmentor(),
                         retrievalAugmentors,
                         "retrievalAugmentor",
+                        "retrievalAugmentor",
                         propertyValues
                 );
 
-                if (aiServiceAnnotation.tools().length > 0) {
-                    for (String beanWithTools : aiServiceAnnotation.tools()) {
-                        propertyValues.add("beanWithTools", new RuntimeBeanReference(beanWithTools));
-                    }
+                if (aiServiceAnnotation.wiringMode() == EXPLICIT) {
+                    propertyValues.add("tools", toManagedList(asList(aiServiceAnnotation.tools())));
+                } else if (aiServiceAnnotation.wiringMode() == AUTOMATIC) {
+                    propertyValues.add("tools", toManagedList(tools));
                 } else {
-                    for (String beanWithTools : beansWithTools) {
-                        propertyValues.add("beanWithTools", new RuntimeBeanReference(beanWithTools));
-                    }
+                    throw illegalArgument("Unknown component selection mode: " + aiServiceAnnotation.wiringMode());
                 }
 
                 BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;
@@ -143,25 +161,32 @@ private static Set> findAiServices(ConfigurableListableBeanFactory bean
     }
 
     private static void addBeanReference(Class beanType,
+                                         AiService aiServiceAnnotation,
                                          String customBeanName,
                                          String[] beanNames,
-                                         String propertyName,
+                                         String annotationAttributeName,
+                                         String factoryPropertyName,
                                          MutablePropertyValues propertyValues) {
-        if (isNotNullOrBlank(customBeanName)) {
-            propertyValues.add(propertyName, new RuntimeBeanReference(customBeanName));
-        } else {
+        if (aiServiceAnnotation.wiringMode() == EXPLICIT) {
+            if (isNotNullOrBlank(customBeanName)) {
+                propertyValues.add(factoryPropertyName, new RuntimeBeanReference(customBeanName));
+            }
+        } else if (aiServiceAnnotation.wiringMode() == AUTOMATIC) {
             if (beanNames.length == 1) {
-                propertyValues.add(propertyName, new RuntimeBeanReference(beanNames[0]));
+                propertyValues.add(factoryPropertyName, new RuntimeBeanReference(beanNames[0]));
             } else if (beanNames.length > 1) {
-                throw conflict(beanType, beanNames);
+                throw conflict(beanType, beanNames, annotationAttributeName);
             }
+        } else {
+            throw illegalArgument("Unknown wiring mode: " + aiServiceAnnotation.wiringMode());
         }
     }
 
-    private static IllegalConfigurationException conflict(Class beanType, Object[] beanNames) {
+    private static IllegalConfigurationException conflict(Class beanType, Object[] beanNames, String attributeName) {
         return illegalConfiguration("Conflict: multiple beans of type %s are found: %s. " +
-                "Please specify which one you wish to use in the @AiService annotation like this: " +
-                "@AiService(chatModel = \"\").", beanType.getName(), Arrays.toString(beanNames));
+                        "Please specify which one you wish to wire in the @AiService annotation like this: " +
+                        "@AiService(wiringMode = EXPLICIT, %s = \"\").",
+                beanType.getName(), Arrays.toString(beanNames), attributeName);
     }
 
     private static String lowercaseFirstLetter(String text) {
@@ -170,4 +195,12 @@ private static String lowercaseFirstLetter(String text) {
         }
         return text.substring(0, 1).toLowerCase() + text.substring(1);
     }
+
+    private static ManagedList toManagedList(Collection beanNames) {
+        ManagedList managedList = new ManagedList<>();
+        for (String beanName : beanNames) {
+            managedList.add(new RuntimeBeanReference(beanName));
+        }
+        return managedList;
+    }
 }
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/customConfig/chatModel/AssistantWithCustomChatModel.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/customConfig/chatModel/AssistantWithCustomChatModel.java
deleted file mode 100644
index 0ea16991..00000000
--- a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/customConfig/chatModel/AssistantWithCustomChatModel.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package dev.langchain4j.service.spring.customConfig.chatModel;
-
-import dev.langchain4j.service.spring.AiService;
-
-import static dev.langchain4j.service.spring.customConfig.chatModel.TestApplicationWithCustomChatModel.CUSTOM_CHAT_MODEL_BEAN_NAME;
-
-@AiService(chatModel = CUSTOM_CHAT_MODEL_BEAN_NAME)
-interface AssistantWithCustomChatModel {
-
-    String chat(String userMessage);
-}
\ No newline at end of file
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/customConfig/multipleChatModels/AssistantWithMultipleChatModels.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/customConfig/multipleChatModels/AssistantWithMultipleChatModels.java
deleted file mode 100644
index 42fad253..00000000
--- a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/customConfig/multipleChatModels/AssistantWithMultipleChatModels.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package dev.langchain4j.service.spring.customConfig.multipleChatModels;
-
-import dev.langchain4j.service.spring.AiService;
-
-@AiService
-interface AssistantWithMultipleChatModels {
-
-    String chat(String userMessage);
-}
\ No newline at end of file
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/AiServicesAutoConfigIT.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/AiServicesAutoConfigIT.java
deleted file mode 100644
index 7c816ec8..00000000
--- a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/AiServicesAutoConfigIT.java
+++ /dev/null
@@ -1,219 +0,0 @@
-package dev.langchain4j.service.spring.defaultConfig;
-
-import dev.langchain4j.data.message.AiMessage;
-import dev.langchain4j.memory.chat.ChatMemoryProvider;
-import dev.langchain4j.memory.chat.MessageWindowChatMemory;
-import dev.langchain4j.model.chat.TestStreamingResponseHandler;
-import dev.langchain4j.model.output.Response;
-import dev.langchain4j.service.spring.AiServicesAutoConfig;
-import org.junit.jupiter.api.Test;
-import org.springframework.beans.factory.NoSuchBeanDefinitionException;
-import org.springframework.boot.autoconfigure.AutoConfigurations;
-import org.springframework.boot.test.context.runner.ApplicationContextRunner;
-import org.springframework.context.annotation.Bean;
-
-import java.time.LocalDateTime;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-
-class AiServicesAutoConfigIT {
-
-    private static final String API_KEY = System.getenv("OPENAI_API_KEY");
-
-    ApplicationContextRunner contextRunner = new ApplicationContextRunner()
-            .withConfiguration(AutoConfigurations.of(AiServicesAutoConfig.class));
-
-    @Test
-    void should_create_AI_service_that_is_public_interface() {
-        contextRunner
-                .withPropertyValues(
-                        "langchain4j.open-ai.chat-model.api-key=" + API_KEY,
-                        "langchain4j.open-ai.chat-model.max-tokens=20",
-                        "langchain4j.open-ai.chat-model.temperature=0.0"
-                )
-                .withUserConfiguration(TestApplication.class)
-                .run(context -> {
-
-                    // given
-                    PublicAssistant assistant = context.getBean(PublicAssistant.class);
-
-                    // when
-                    String answer = assistant.chat("What is the capital of Germany?");
-
-                    // then
-                    assertThat(answer).containsIgnoringCase("Berlin");
-                });
-    }
-
-    @Test
-    void should_create_AI_service_that_is_package_private_interface() {
-        contextRunner
-                .withPropertyValues(
-                        "langchain4j.open-ai.chat-model.api-key=" + API_KEY,
-                        "langchain4j.open-ai.chat-model.max-tokens=20",
-                        "langchain4j.open-ai.chat-model.temperature=0.0"
-                )
-                .withUserConfiguration(TestApplication.class)
-                .run(context -> {
-
-                    // given
-                    PackagePrivateAssistant assistant = context.getBean(PackagePrivateAssistant.class);
-
-                    // when
-                    String answer = assistant.chat("What is the capital of Germany?");
-
-                    // then
-                    assertThat(answer).containsIgnoringCase("Berlin");
-                });
-    }
-
-    @Test
-    void should_create_AI_service_that_is_inner_interface() {
-        contextRunner
-                .withPropertyValues(
-                        "langchain4j.open-ai.chat-model.api-key=" + API_KEY,
-                        "langchain4j.open-ai.chat-model.max-tokens=20",
-                        "langchain4j.open-ai.chat-model.temperature=0.0"
-                )
-                .withUserConfiguration(TestApplication.class)
-                .run(context -> {
-
-                    // given
-                    OuterClass.InnerAssistant assistant = context.getBean(OuterClass.InnerAssistant.class);
-
-                    // when
-                    String answer = assistant.chat("What is the capital of Germany?");
-
-                    // then
-                    assertThat(answer).containsIgnoringCase("Berlin");
-                });
-    }
-
-    @Test
-    void should_fail_to_create_AI_service_without_annotation() {
-        contextRunner
-                .withPropertyValues("langchain4j.open-ai.chat-model.api-key=" + API_KEY)
-                .withUserConfiguration(TestApplication.class)
-                .run(context -> {
-
-                    // when-then
-                    assertThatThrownBy(() -> context.getBean(AssistantWithoutAnnotation.class))
-                            .isExactlyInstanceOf(NoSuchBeanDefinitionException.class);
-                });
-    }
-
-    @Test
-    void should_create_streaming_AI_service() {
-        contextRunner
-                .withPropertyValues(
-                        "langchain4j.open-ai.streaming-chat-model.api-key=" + API_KEY,
-                        "langchain4j.open-ai.streaming-chat-model.max-tokens=20",
-                        "langchain4j.open-ai.streaming-chat-model.temperature=0.0"
-                )
-                .withUserConfiguration(TestApplication.class)
-                .run(context -> {
-
-                    // given
-                    StreamingAssistant assistant = context.getBean(StreamingAssistant.class);
-
-                    TestStreamingResponseHandler handler = new TestStreamingResponseHandler<>();
-
-                    // when
-                    assistant.chat("What is the capital of Germany?")
-                            .onNext(handler::onNext)
-                            .onComplete(handler::onComplete)
-                            .onError(handler::onError)
-                            .start();
-                    Response response = handler.get();
-
-                    // then
-                    assertThat(response.content().text()).containsIgnoringCase("Berlin");
-                });
-    }
-
-
-    static class ChatMemoryProviderConfig {
-
-        @Bean
-        ChatMemoryProvider chatMemoryProvider() {
-            return memoryId -> MessageWindowChatMemory.withMaxMessages(10);
-        }
-    }
-
-    @Test
-    void should_create_AI_service_with_chat_memory_provider() {
-        contextRunner
-                .withPropertyValues(
-                        "langchain4j.open-ai.chat-model.api-key=" + API_KEY,
-                        "langchain4j.open-ai.chat-model.max-tokens=20",
-                        "langchain4j.open-ai.chat-model.temperature=0.0"
-                )
-                .withUserConfiguration(TestApplication.class)
-                .withUserConfiguration(ChatMemoryProviderConfig.class)
-                .run(context -> {
-
-                    // given
-                    PublicAssistant assistant = context.getBean(PublicAssistant.class);
-                    assistant.chat("My name is Klaus");
-
-                    // when
-                    String answer = assistant.chat("What is my name?");
-
-                    // then
-                    assertThat(answer).containsIgnoringCase("Klaus");
-                });
-    }
-
-    @Test
-    void should_create_AI_service_with_tool_which_is_public_method_in_public_class() {
-        contextRunner
-                .withPropertyValues(
-                        "langchain4j.open-ai.chat-model.api-key=" + API_KEY,
-                        "langchain4j.open-ai.chat-model.max-tokens=20",
-                        "langchain4j.open-ai.chat-model.temperature=0.0"
-                )
-                .withUserConfiguration(TestApplication.class)
-                .withUserConfiguration(ChatMemoryProviderConfig.class)
-                .run(context -> {
-
-                    // given
-                    AssistantWithPublicTools assistant = context.getBean(AssistantWithPublicTools.class);
-
-                    // when
-                    String answer = assistant.chat("What is the current hour?");
-
-                    // then
-                    assertThat(answer).contains(String.valueOf(LocalDateTime.now().getHour()));
-                });
-    }
-
-    @Test
-    void should_create_AI_service_with_tool_that_is_package_private_method_in_package_private_class() {
-        contextRunner
-                .withPropertyValues(
-                        "langchain4j.open-ai.chat-model.api-key=" + API_KEY,
-                        "langchain4j.open-ai.chat-model.max-tokens=20",
-                        "langchain4j.open-ai.chat-model.temperature=0.0"
-                )
-                .withUserConfiguration(TestApplication.class)
-                .withUserConfiguration(ChatMemoryProviderConfig.class)
-                .run(context -> {
-
-                    // given
-                    AssistantWithPackagePrivateTools assistant = context.getBean(AssistantWithPackagePrivateTools.class);
-
-                    // when
-                    String answer = assistant.chat("What is the current minute?");
-
-                    // then
-                    assertThat(answer).contains(String.valueOf(LocalDateTime.now().getMinute()));
-                });
-    }
-
-    // TODO tools which are not @Beans?
-    // TODO negative cases
-    // TODO no @AiServices in app, just models
-    // TODO @AiServices as inner class?
-    // TODO streaming, memory, tools, etc
-}
\ No newline at end of file
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/AssistantWithPackagePrivateTools.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/AssistantWithPackagePrivateTools.java
deleted file mode 100644
index c570ba74..00000000
--- a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/AssistantWithPackagePrivateTools.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package dev.langchain4j.service.spring.defaultConfig;
-
-import dev.langchain4j.service.spring.AiService;
-
-@AiService(tools = {"packagePrivateTools"})
-public interface AssistantWithPackagePrivateTools {
-
-    String chat(String userMessage);
-}
\ No newline at end of file
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/AssistantWithPublicTools.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/AssistantWithPublicTools.java
deleted file mode 100644
index 63be3e25..00000000
--- a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/AssistantWithPublicTools.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package dev.langchain4j.service.spring.defaultConfig;
-
-import dev.langchain4j.service.spring.AiService;
-
-@AiService(tools = {"publicTools"})
-public interface AssistantWithPublicTools {
-
-    String chat(String userMessage);
-}
\ No newline at end of file
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/AssistantWithoutAnnotation.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/AssistantWithoutAnnotation.java
deleted file mode 100644
index bc145ed3..00000000
--- a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/AssistantWithoutAnnotation.java
+++ /dev/null
@@ -1,6 +0,0 @@
-package dev.langchain4j.service.spring.defaultConfig;
-
-public interface AssistantWithoutAnnotation {
-
-    String chat(String userMessage);
-}
\ No newline at end of file
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/PublicAssistant.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/PublicAssistant.java
deleted file mode 100644
index 37d8d675..00000000
--- a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/PublicAssistant.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package dev.langchain4j.service.spring.defaultConfig;
-
-import dev.langchain4j.service.spring.AiService;
-
-@AiService
-public interface PublicAssistant {
-
-    String chat(String userMessage);
-}
\ No newline at end of file
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/PublicTools.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/PublicTools.java
deleted file mode 100644
index 0ede75e8..00000000
--- a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/PublicTools.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package dev.langchain4j.service.spring.defaultConfig;
-
-import dev.langchain4j.agent.tool.Tool;
-import org.springframework.stereotype.Component;
-
-import java.time.LocalDateTime;
-
-@Component("publicTools")
-public class PublicTools {
-
-    @Tool
-    public int getCurrentHour() {
-        return LocalDateTime.now().getHour();
-    }
-}
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/ApiKeys.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/ApiKeys.java
new file mode 100644
index 00000000..02797053
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/ApiKeys.java
@@ -0,0 +1,7 @@
+package dev.langchain4j.service.spring.mode;
+
+public class ApiKeys {
+
+    public static final String OPENAI_API_KEY = System.getenv("OPENAI_API_KEY");
+
+}
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/conflictingChatMemories/AiServiceWithConflictingChatMemories.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/conflictingChatMemories/AiServiceWithConflictingChatMemories.java
new file mode 100644
index 00000000..c0b525de
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/conflictingChatMemories/AiServiceWithConflictingChatMemories.java
@@ -0,0 +1,9 @@
+package dev.langchain4j.service.spring.mode.automatic.conflictingChatMemories;
+
+import dev.langchain4j.service.spring.AiService;
+
+@AiService
+interface AiServiceWithConflictingChatMemories {
+
+    String chat(String userMessage);
+}
\ No newline at end of file
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/conflictingChatMemories/AiServiceWithConflictingChatMemoriesIT.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/conflictingChatMemories/AiServiceWithConflictingChatMemoriesIT.java
new file mode 100644
index 00000000..2c5ebbd0
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/conflictingChatMemories/AiServiceWithConflictingChatMemoriesIT.java
@@ -0,0 +1,32 @@
+package dev.langchain4j.service.spring.mode.automatic.conflictingChatMemories;
+
+import dev.langchain4j.exception.IllegalConfigurationException;
+import dev.langchain4j.service.spring.AiServicesAutoConfig;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class AiServiceWithConflictingChatMemoriesIT {
+
+    ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+            .withConfiguration(AutoConfigurations.of(AiServicesAutoConfig.class));
+
+    @Test
+    void should_fail_to_create_AI_service_when_conflicting_chat_memories_are_found() {
+        contextRunner
+                .withUserConfiguration(AiServiceWithConflictingChatModelsApplication.class)
+                .run(context -> {
+
+                    assertThatThrownBy(() -> context.getBean(AiServiceWithConflictingChatMemories.class))
+                            .isExactlyInstanceOf(IllegalStateException.class)
+                            .hasCauseExactlyInstanceOf(IllegalConfigurationException.class)
+                            .hasRootCauseMessage("Conflict: multiple beans of type " +
+                                    "dev.langchain4j.memory.ChatMemory are found: " +
+                                    "[chatMemory, chatMemory2]. " +
+                                    "Please specify which one you wish to wire in the @AiService annotation like this: " +
+                                    "@AiService(wiringMode = EXPLICIT, chatMemory = \"\").");
+                });
+    }
+}
\ No newline at end of file
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/conflictingChatMemories/AiServiceWithConflictingChatModelsApplication.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/conflictingChatMemories/AiServiceWithConflictingChatModelsApplication.java
new file mode 100644
index 00000000..070f5ca3
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/conflictingChatMemories/AiServiceWithConflictingChatModelsApplication.java
@@ -0,0 +1,25 @@
+package dev.langchain4j.service.spring.mode.automatic.conflictingChatMemories;
+
+import dev.langchain4j.memory.ChatMemory;
+import dev.langchain4j.memory.chat.MessageWindowChatMemory;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;
+
+@SpringBootApplication
+class AiServiceWithConflictingChatModelsApplication {
+
+    @Bean
+    ChatMemory chatMemory() {
+        return MessageWindowChatMemory.withMaxMessages(10);
+    }
+
+    @Bean
+    ChatMemory chatMemory2() {
+        return MessageWindowChatMemory.withMaxMessages(10);
+    }
+
+    public static void main(String[] args) {
+        SpringApplication.run(AiServiceWithConflictingChatModelsApplication.class, args);
+    }
+}
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/customConfig/multipleChatModels/AssistantWithMultipleChatModelsIT.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/conflictingChatModels/AiServiceWithConflictingChatMemoriesIT.java
similarity index 66%
rename from langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/customConfig/multipleChatModels/AssistantWithMultipleChatModelsIT.java
rename to langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/conflictingChatModels/AiServiceWithConflictingChatMemoriesIT.java
index 2bbc6536..bf872f9d 100644
--- a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/customConfig/multipleChatModels/AssistantWithMultipleChatModelsIT.java
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/conflictingChatModels/AiServiceWithConflictingChatMemoriesIT.java
@@ -1,4 +1,4 @@
-package dev.langchain4j.service.spring.customConfig.multipleChatModels;
+package dev.langchain4j.service.spring.mode.automatic.conflictingChatModels;
 
 import dev.langchain4j.exception.IllegalConfigurationException;
 import dev.langchain4j.service.spring.AiServicesAutoConfig;
@@ -8,25 +8,25 @@
 
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
-class AssistantWithMultipleChatModelsIT {
+class AiServiceWithConflictingChatMemoriesIT {
 
     ApplicationContextRunner contextRunner = new ApplicationContextRunner()
             .withConfiguration(AutoConfigurations.of(AiServicesAutoConfig.class));
 
     @Test
-    void should_fail_to_create_AI_service_when_multiple_chat_models_are_found() {
+    void should_fail_to_create_AI_service_when_conflicting_chat_models_are_found() {
         contextRunner
-                .withUserConfiguration(TestApplicationWithMultipleChatModels.class)
+                .withUserConfiguration(AiServiceWithConflictingChatModelsApplication.class)
                 .run(context -> {
 
-                    assertThatThrownBy(() -> context.getBean(AssistantWithMultipleChatModels.class))
+                    assertThatThrownBy(() -> context.getBean(AiServiceWithConflictingChatModels.class))
                             .isExactlyInstanceOf(IllegalStateException.class)
                             .hasCauseExactlyInstanceOf(IllegalConfigurationException.class)
                             .hasRootCauseMessage("Conflict: multiple beans of type " +
                                     "dev.langchain4j.model.chat.ChatLanguageModel are found: " +
                                     "[chatLanguageModel, chatLanguageModel2]. " +
-                                    "Please specify which one you wish to use in the @AiService annotation " +
-                                    "like this: @AiService(chatModel = \"\").");
+                                    "Please specify which one you wish to wire in the @AiService annotation like this: " +
+                                    "@AiService(wiringMode = EXPLICIT, chatModel = \"\").");
                 });
     }
 }
\ No newline at end of file
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/conflictingChatModels/AiServiceWithConflictingChatModels.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/conflictingChatModels/AiServiceWithConflictingChatModels.java
new file mode 100644
index 00000000..605b7ef7
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/conflictingChatModels/AiServiceWithConflictingChatModels.java
@@ -0,0 +1,9 @@
+package dev.langchain4j.service.spring.mode.automatic.conflictingChatModels;
+
+import dev.langchain4j.service.spring.AiService;
+
+@AiService
+interface AiServiceWithConflictingChatModels {
+
+    String chat(String userMessage);
+}
\ No newline at end of file
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/customConfig/multipleChatModels/TestApplicationWithMultipleChatModels.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/conflictingChatModels/AiServiceWithConflictingChatModelsApplication.java
similarity index 74%
rename from langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/customConfig/multipleChatModels/TestApplicationWithMultipleChatModels.java
rename to langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/conflictingChatModels/AiServiceWithConflictingChatModelsApplication.java
index 2a28e89d..a20dfe4b 100644
--- a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/customConfig/multipleChatModels/TestApplicationWithMultipleChatModels.java
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/conflictingChatModels/AiServiceWithConflictingChatModelsApplication.java
@@ -1,4 +1,4 @@
-package dev.langchain4j.service.spring.customConfig.multipleChatModels;
+package dev.langchain4j.service.spring.mode.automatic.conflictingChatModels;
 
 import dev.langchain4j.model.chat.ChatLanguageModel;
 import dev.langchain4j.model.openai.OpenAiChatModel;
@@ -7,7 +7,7 @@
 import org.springframework.context.annotation.Bean;
 
 @SpringBootApplication
-public class TestApplicationWithMultipleChatModels {
+class AiServiceWithConflictingChatModelsApplication {
 
     @Bean
     ChatLanguageModel chatLanguageModel() {
@@ -20,6 +20,6 @@ ChatLanguageModel chatLanguageModel2() {
     }
 
     public static void main(String[] args) {
-        SpringApplication.run(TestApplicationWithMultipleChatModels.class, args);
+        SpringApplication.run(AiServiceWithConflictingChatModelsApplication.class, args);
     }
 }
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/conflictingSyncAndStreamingModels/streaming/AiServiceWithConflictingSyncAndStreamingModels.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/conflictingSyncAndStreamingModels/streaming/AiServiceWithConflictingSyncAndStreamingModels.java
new file mode 100644
index 00000000..bb68e1e7
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/conflictingSyncAndStreamingModels/streaming/AiServiceWithConflictingSyncAndStreamingModels.java
@@ -0,0 +1,10 @@
+package dev.langchain4j.service.spring.mode.automatic.conflictingSyncAndStreamingModels.streaming;
+
+import dev.langchain4j.service.TokenStream;
+import dev.langchain4j.service.spring.AiService;
+
+@AiService
+interface AiServiceWithConflictingSyncAndStreamingModels {
+
+    TokenStream chat(String userMessage);
+}
\ No newline at end of file
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/conflictingSyncAndStreamingModels/streaming/AiServiceWithConflictingSyncAndStreamingModelsApplication.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/conflictingSyncAndStreamingModels/streaming/AiServiceWithConflictingSyncAndStreamingModelsApplication.java
new file mode 100644
index 00000000..52638c81
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/conflictingSyncAndStreamingModels/streaming/AiServiceWithConflictingSyncAndStreamingModelsApplication.java
@@ -0,0 +1,21 @@
+package dev.langchain4j.service.spring.mode.automatic.conflictingSyncAndStreamingModels.streaming;
+
+import dev.langchain4j.model.chat.ChatLanguageModel;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;
+
+@SpringBootApplication
+class AiServiceWithConflictingSyncAndStreamingModelsApplication {
+
+    @Bean
+    ChatLanguageModel chatLanguageModel() {
+        return (messages) -> {
+            throw new RuntimeException("should never be invoked");
+        };
+    }
+
+    public static void main(String[] args) {
+        SpringApplication.run(AiServiceWithConflictingSyncAndStreamingModelsApplication.class, args);
+    }
+}
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/conflictingSyncAndStreamingModels/streaming/AiServiceWithConflictingSyncAndStreamingModelsIT.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/conflictingSyncAndStreamingModels/streaming/AiServiceWithConflictingSyncAndStreamingModelsIT.java
new file mode 100644
index 00000000..2e40d1ae
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/conflictingSyncAndStreamingModels/streaming/AiServiceWithConflictingSyncAndStreamingModelsIT.java
@@ -0,0 +1,47 @@
+package dev.langchain4j.service.spring.mode.automatic.conflictingSyncAndStreamingModels.streaming;
+
+import dev.langchain4j.data.message.AiMessage;
+import dev.langchain4j.model.chat.TestStreamingResponseHandler;
+import dev.langchain4j.model.output.Response;
+import dev.langchain4j.service.spring.AiServicesAutoConfig;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static dev.langchain4j.service.spring.mode.ApiKeys.OPENAI_API_KEY;
+import static org.assertj.core.api.Assertions.assertThat;
+
+class AiServiceWithConflictingSyncAndStreamingModelsIT {
+
+    ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+            .withConfiguration(AutoConfigurations.of(AiServicesAutoConfig.class));
+
+    @Test
+    void should_create_AI_service_with_streaming_model_when_both_sync_and_streaming_models_are_found_and_TokenStream_is_used() {
+        contextRunner
+                .withPropertyValues(
+                        "langchain4j.open-ai.streaming-chat-model.api-key=" + OPENAI_API_KEY,
+                        "langchain4j.open-ai.streaming-chat-model.max-tokens=20",
+                        "langchain4j.open-ai.streaming-chat-model.temperature=0.0"
+                )
+                .withUserConfiguration(AiServiceWithConflictingSyncAndStreamingModelsApplication.class)
+                .run(context -> {
+
+                    // given
+                    AiServiceWithConflictingSyncAndStreamingModels aiService = context.getBean(AiServiceWithConflictingSyncAndStreamingModels.class);
+
+                    TestStreamingResponseHandler handler = new TestStreamingResponseHandler<>();
+
+                    // when
+                    aiService.chat("What is the capital of Germany?")
+                            .onNext(handler::onNext)
+                            .onComplete(handler::onComplete)
+                            .onError(handler::onError)
+                            .start();
+                    Response response = handler.get();
+
+                    // then
+                    assertThat(response.content().text()).containsIgnoringCase("Berlin");
+                });
+    }
+}
\ No newline at end of file
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/conflictingSyncAndStreamingModels/sync/AiServiceWithConflictingSyncAndStreamingModels.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/conflictingSyncAndStreamingModels/sync/AiServiceWithConflictingSyncAndStreamingModels.java
new file mode 100644
index 00000000..8c7a8f88
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/conflictingSyncAndStreamingModels/sync/AiServiceWithConflictingSyncAndStreamingModels.java
@@ -0,0 +1,9 @@
+package dev.langchain4j.service.spring.mode.automatic.conflictingSyncAndStreamingModels.sync;
+
+import dev.langchain4j.service.spring.AiService;
+
+@AiService
+interface AiServiceWithConflictingSyncAndStreamingModels {
+
+    String chat(String userMessage);
+}
\ No newline at end of file
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/conflictingSyncAndStreamingModels/sync/AiServiceWithConflictingSyncAndStreamingModelsApplication.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/conflictingSyncAndStreamingModels/sync/AiServiceWithConflictingSyncAndStreamingModelsApplication.java
new file mode 100644
index 00000000..904ad841
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/conflictingSyncAndStreamingModels/sync/AiServiceWithConflictingSyncAndStreamingModelsApplication.java
@@ -0,0 +1,21 @@
+package dev.langchain4j.service.spring.mode.automatic.conflictingSyncAndStreamingModels.sync;
+
+import dev.langchain4j.model.chat.StreamingChatLanguageModel;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;
+
+@SpringBootApplication
+class AiServiceWithConflictingSyncAndStreamingModelsApplication {
+
+    @Bean
+    StreamingChatLanguageModel streamingChatLanguageModel() {
+        return (messages, handler) -> {
+            throw new RuntimeException("should never be invoked");
+        };
+    }
+
+    public static void main(String[] args) {
+        SpringApplication.run(AiServiceWithConflictingSyncAndStreamingModelsApplication.class, args);
+    }
+}
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/conflictingSyncAndStreamingModels/sync/AiServiceWithConflictingSyncAndStreamingModelsIT.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/conflictingSyncAndStreamingModels/sync/AiServiceWithConflictingSyncAndStreamingModelsIT.java
new file mode 100644
index 00000000..016eee0b
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/conflictingSyncAndStreamingModels/sync/AiServiceWithConflictingSyncAndStreamingModelsIT.java
@@ -0,0 +1,37 @@
+package dev.langchain4j.service.spring.mode.automatic.conflictingSyncAndStreamingModels.sync;
+
+import dev.langchain4j.service.spring.AiServicesAutoConfig;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static dev.langchain4j.service.spring.mode.ApiKeys.OPENAI_API_KEY;
+import static org.assertj.core.api.Assertions.assertThat;
+
+class AiServiceWithConflictingSyncAndStreamingModelsIT {
+
+    ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+            .withConfiguration(AutoConfigurations.of(AiServicesAutoConfig.class));
+
+    @Test
+    void should_create_AI_service_with_sync_model_when_both_sync_and_streaming_models_are_found_and_no_TokenStream_is_used() {
+        contextRunner
+                .withPropertyValues(
+                        "langchain4j.open-ai.chat-model.api-key=" + OPENAI_API_KEY,
+                        "langchain4j.open-ai.chat-model.max-tokens=20",
+                        "langchain4j.open-ai.chat-model.temperature=0.0"
+                )
+                .withUserConfiguration(AiServiceWithConflictingSyncAndStreamingModelsApplication.class)
+                .run(context -> {
+
+                    // given
+                    AiServiceWithConflictingSyncAndStreamingModels aiService = context.getBean(AiServiceWithConflictingSyncAndStreamingModels.class);
+
+                    // when
+                    String answer = aiService.chat("What is the capital of Germany?");
+
+                    // then
+                    assertThat(answer).containsIgnoringCase("Berlin");
+                });
+    }
+}
\ No newline at end of file
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/innerClass/InnerClassAiServiceApplication.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/innerClass/InnerClassAiServiceApplication.java
new file mode 100644
index 00000000..4e2c5986
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/innerClass/InnerClassAiServiceApplication.java
@@ -0,0 +1,12 @@
+package dev.langchain4j.service.spring.mode.automatic.innerClass;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+class InnerClassAiServiceApplication {
+
+    public static void main(String[] args) {
+        SpringApplication.run(InnerClassAiServiceApplication.class, args);
+    }
+}
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/innerClass/InnerClassAiServiceIT.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/innerClass/InnerClassAiServiceIT.java
new file mode 100644
index 00000000..145295d5
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/innerClass/InnerClassAiServiceIT.java
@@ -0,0 +1,37 @@
+package dev.langchain4j.service.spring.mode.automatic.innerClass;
+
+import dev.langchain4j.service.spring.AiServicesAutoConfig;
+import dev.langchain4j.service.spring.mode.automatic.innerClass.OuterClass.InnerAiService;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class InnerClassAiServiceIT {
+
+    ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+            .withConfiguration(AutoConfigurations.of(AiServicesAutoConfig.class));
+
+    @Test
+    void should_create_AI_service_that_is_inner_class() {
+        contextRunner
+                .withPropertyValues(
+                        "langchain4j.open-ai.chat-model.api-key=" + System.getenv("OPENAI_API_KEY"),
+                        "langchain4j.open-ai.chat-model.max-tokens=20",
+                        "langchain4j.open-ai.chat-model.temperature=0.0"
+                )
+                .withUserConfiguration(InnerClassAiServiceApplication.class)
+                .run(context -> {
+
+                    // given
+                    InnerAiService aiService = context.getBean(InnerAiService.class);
+
+                    // when
+                    String answer = aiService.chat("What is the capital of Germany?");
+
+                    // then
+                    assertThat(answer).containsIgnoringCase("Berlin");
+                });
+    }
+}
\ No newline at end of file
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/OuterClass.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/innerClass/OuterClass.java
similarity index 58%
rename from langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/OuterClass.java
rename to langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/innerClass/OuterClass.java
index 6a04a4c4..969ed473 100644
--- a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/OuterClass.java
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/innerClass/OuterClass.java
@@ -1,11 +1,11 @@
-package dev.langchain4j.service.spring.defaultConfig;
+package dev.langchain4j.service.spring.mode.automatic.innerClass;
 
 import dev.langchain4j.service.spring.AiService;
 
 class OuterClass {
 
     @AiService
-    interface InnerAssistant {
+    interface InnerAiService {
 
         String chat(String userMessage);
     }
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/missingAnnotation/AiServiceWithMissingAnnotationApplication.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/missingAnnotation/AiServiceWithMissingAnnotationApplication.java
new file mode 100644
index 00000000..e92d36d9
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/missingAnnotation/AiServiceWithMissingAnnotationApplication.java
@@ -0,0 +1,12 @@
+package dev.langchain4j.service.spring.mode.automatic.missingAnnotation;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+class AiServiceWithMissingAnnotationApplication {
+
+    public static void main(String[] args) {
+        SpringApplication.run(AiServiceWithMissingAnnotationApplication.class, args);
+    }
+}
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/missingAnnotation/AiServiceWithMissingAnnotationIT.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/missingAnnotation/AiServiceWithMissingAnnotationIT.java
new file mode 100644
index 00000000..99e98d5d
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/missingAnnotation/AiServiceWithMissingAnnotationIT.java
@@ -0,0 +1,29 @@
+package dev.langchain4j.service.spring.mode.automatic.missingAnnotation;
+
+import dev.langchain4j.service.spring.AiServicesAutoConfig;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.NoSuchBeanDefinitionException;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static dev.langchain4j.service.spring.mode.ApiKeys.OPENAI_API_KEY;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class AiServiceWithMissingAnnotationIT {
+
+    ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+            .withConfiguration(AutoConfigurations.of(AiServicesAutoConfig.class));
+
+    @Test
+    void should_fail_to_create_AI_service_with_missing_annotation() {
+        contextRunner
+                .withPropertyValues("langchain4j.open-ai.chat-model.api-key=" + OPENAI_API_KEY)
+                .withUserConfiguration(AiServiceWithMissingAnnotationApplication.class)
+                .run(context -> {
+
+                    // when-then
+                    assertThatThrownBy(() -> context.getBean(AssistantWithMissingAnnotation.class))
+                            .isExactlyInstanceOf(NoSuchBeanDefinitionException.class);
+                });
+    }
+}
\ No newline at end of file
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/missingAnnotation/AssistantWithMissingAnnotation.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/missingAnnotation/AssistantWithMissingAnnotation.java
new file mode 100644
index 00000000..35b42faa
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/missingAnnotation/AssistantWithMissingAnnotation.java
@@ -0,0 +1,7 @@
+package dev.langchain4j.service.spring.mode.automatic.missingAnnotation;
+
+// no @AiService annotation
+interface AssistantWithMissingAnnotation {
+
+    String chat(String userMessage);
+}
\ No newline at end of file
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/packagePrivateClass/PackagePrivateAiService.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/packagePrivateClass/PackagePrivateAiService.java
new file mode 100644
index 00000000..14366b41
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/packagePrivateClass/PackagePrivateAiService.java
@@ -0,0 +1,9 @@
+package dev.langchain4j.service.spring.mode.automatic.packagePrivateClass;
+
+import dev.langchain4j.service.spring.AiService;
+
+@AiService
+interface PackagePrivateAiService {
+
+    String chat(String userMessage);
+}
\ No newline at end of file
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/packagePrivateClass/PackagePrivateAiServiceApplication.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/packagePrivateClass/PackagePrivateAiServiceApplication.java
new file mode 100644
index 00000000..254171bd
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/packagePrivateClass/PackagePrivateAiServiceApplication.java
@@ -0,0 +1,12 @@
+package dev.langchain4j.service.spring.mode.automatic.packagePrivateClass;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+class PackagePrivateAiServiceApplication {
+
+    public static void main(String[] args) {
+        SpringApplication.run(PackagePrivateAiServiceApplication.class, args);
+    }
+}
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/packagePrivateClass/PackagePrivateAiServiceIT.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/packagePrivateClass/PackagePrivateAiServiceIT.java
new file mode 100644
index 00000000..33edbabe
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/packagePrivateClass/PackagePrivateAiServiceIT.java
@@ -0,0 +1,37 @@
+package dev.langchain4j.service.spring.mode.automatic.packagePrivateClass;
+
+import dev.langchain4j.service.spring.AiServicesAutoConfig;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static dev.langchain4j.service.spring.mode.ApiKeys.OPENAI_API_KEY;
+import static org.assertj.core.api.Assertions.assertThat;
+
+class PackagePrivateAiServiceIT {
+
+    ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+            .withConfiguration(AutoConfigurations.of(AiServicesAutoConfig.class));
+
+    @Test
+    void should_create_AI_service_that_is_package_private_interface() {
+        contextRunner
+                .withPropertyValues(
+                        "langchain4j.open-ai.chat-model.api-key=" + OPENAI_API_KEY,
+                        "langchain4j.open-ai.chat-model.max-tokens=20",
+                        "langchain4j.open-ai.chat-model.temperature=0.0"
+                )
+                .withUserConfiguration(PackagePrivateAiServiceApplication.class)
+                .run(context -> {
+
+                    // given
+                    PackagePrivateAiService aiService = context.getBean(PackagePrivateAiService.class);
+
+                    // when
+                    String answer = aiService.chat("What is the capital of Germany?");
+
+                    // then
+                    assertThat(answer).containsIgnoringCase("Berlin");
+                });
+    }
+}
\ No newline at end of file
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/publicClass/PublicAiService.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/publicClass/PublicAiService.java
new file mode 100644
index 00000000..24e70619
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/publicClass/PublicAiService.java
@@ -0,0 +1,9 @@
+package dev.langchain4j.service.spring.mode.automatic.publicClass;
+
+import dev.langchain4j.service.spring.AiService;
+
+@AiService
+public interface PublicAiService {
+
+    String chat(String userMessage);
+}
\ No newline at end of file
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/TestApplication.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/publicClass/PublicAiServiceApplication.java
similarity index 53%
rename from langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/TestApplication.java
rename to langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/publicClass/PublicAiServiceApplication.java
index e4dbe9cf..efbd158f 100644
--- a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/TestApplication.java
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/publicClass/PublicAiServiceApplication.java
@@ -1,12 +1,12 @@
-package dev.langchain4j.service.spring.defaultConfig;
+package dev.langchain4j.service.spring.mode.automatic.publicClass;
 
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 
 @SpringBootApplication
-public class TestApplication {
+class PublicAiServiceApplication {
 
     public static void main(String[] args) {
-        SpringApplication.run(TestApplication.class, args);
+        SpringApplication.run(PublicAiServiceApplication.class, args);
     }
 }
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/customConfig/chatModel/AssistantWithCustomChatModelIT.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/publicClass/PublicAiServiceIT.java
similarity index 57%
rename from langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/customConfig/chatModel/AssistantWithCustomChatModelIT.java
rename to langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/publicClass/PublicAiServiceIT.java
index c75f886c..7592d4bb 100644
--- a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/customConfig/chatModel/AssistantWithCustomChatModelIT.java
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/publicClass/PublicAiServiceIT.java
@@ -1,31 +1,34 @@
-package dev.langchain4j.service.spring.customConfig.chatModel;
+package dev.langchain4j.service.spring.mode.automatic.publicClass;
 
 import dev.langchain4j.service.spring.AiServicesAutoConfig;
 import org.junit.jupiter.api.Test;
 import org.springframework.boot.autoconfigure.AutoConfigurations;
 import org.springframework.boot.test.context.runner.ApplicationContextRunner;
 
+import static dev.langchain4j.service.spring.mode.ApiKeys.OPENAI_API_KEY;
 import static org.assertj.core.api.Assertions.assertThat;
 
-class AssistantWithCustomChatModelIT {
+class PublicAiServiceIT {
 
     ApplicationContextRunner contextRunner = new ApplicationContextRunner()
             .withConfiguration(AutoConfigurations.of(AiServicesAutoConfig.class));
 
     @Test
-    void should_create_AI_service_with_custom_chat_model() {
+    void should_create_AI_service_that_is_public_interface() {
         contextRunner
                 .withPropertyValues(
-                        "langchain4j.open-ai.chat-model.api-key=banana" // to make sure that this model is not used
+                        "langchain4j.open-ai.chat-model.api-key=" + OPENAI_API_KEY,
+                        "langchain4j.open-ai.chat-model.max-tokens=20",
+                        "langchain4j.open-ai.chat-model.temperature=0.0"
                 )
-                .withUserConfiguration(TestApplicationWithCustomChatModel.class)
+                .withUserConfiguration(PublicAiServiceApplication.class)
                 .run(context -> {
 
                     // given
-                    AssistantWithCustomChatModel assistant = context.getBean(AssistantWithCustomChatModel.class);
+                    PublicAiService aiService = context.getBean(PublicAiService.class);
 
                     // when
-                    String answer = assistant.chat("What is the capital of Germany?");
+                    String answer = aiService.chat("What is the capital of Germany?");
 
                     // then
                     assertThat(answer).containsIgnoringCase("Berlin");
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/StreamingAssistant.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/streaming/StreamingAiService.java
similarity index 60%
rename from langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/StreamingAssistant.java
rename to langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/streaming/StreamingAiService.java
index 3000d527..5f7c4bef 100644
--- a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/StreamingAssistant.java
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/streaming/StreamingAiService.java
@@ -1,10 +1,10 @@
-package dev.langchain4j.service.spring.defaultConfig;
+package dev.langchain4j.service.spring.mode.automatic.streaming;
 
 import dev.langchain4j.service.TokenStream;
 import dev.langchain4j.service.spring.AiService;
 
 @AiService
-public interface StreamingAssistant {
+interface StreamingAiService {
 
     TokenStream chat(String userMessage);
 }
\ No newline at end of file
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/streaming/StreamingAiServiceApplication.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/streaming/StreamingAiServiceApplication.java
new file mode 100644
index 00000000..c977c273
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/streaming/StreamingAiServiceApplication.java
@@ -0,0 +1,12 @@
+package dev.langchain4j.service.spring.mode.automatic.streaming;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+class StreamingAiServiceApplication {
+
+    public static void main(String[] args) {
+        SpringApplication.run(StreamingAiServiceApplication.class, args);
+    }
+}
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/streaming/StreamingAiServiceIT.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/streaming/StreamingAiServiceIT.java
new file mode 100644
index 00000000..9b294950
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/streaming/StreamingAiServiceIT.java
@@ -0,0 +1,47 @@
+package dev.langchain4j.service.spring.mode.automatic.streaming;
+
+import dev.langchain4j.data.message.AiMessage;
+import dev.langchain4j.model.chat.TestStreamingResponseHandler;
+import dev.langchain4j.model.output.Response;
+import dev.langchain4j.service.spring.AiServicesAutoConfig;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static dev.langchain4j.service.spring.mode.ApiKeys.OPENAI_API_KEY;
+import static org.assertj.core.api.Assertions.assertThat;
+
+class StreamingAiServiceIT {
+
+    ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+            .withConfiguration(AutoConfigurations.of(AiServicesAutoConfig.class));
+
+    @Test
+    void should_create_streaming_AI_service() {
+        contextRunner
+                .withPropertyValues(
+                        "langchain4j.open-ai.streaming-chat-model.api-key=" + OPENAI_API_KEY,
+                        "langchain4j.open-ai.streaming-chat-model.max-tokens=20",
+                        "langchain4j.open-ai.streaming-chat-model.temperature=0.0"
+                )
+                .withUserConfiguration(StreamingAiServiceApplication.class)
+                .run(context -> {
+
+                    // given
+                    StreamingAiService aiService = context.getBean(StreamingAiService.class);
+
+                    TestStreamingResponseHandler handler = new TestStreamingResponseHandler<>();
+
+                    // when
+                    aiService.chat("What is the capital of Germany?")
+                            .onNext(handler::onNext)
+                            .onComplete(handler::onComplete)
+                            .onError(handler::onError)
+                            .start();
+                    Response response = handler.get();
+
+                    // then
+                    assertThat(response.content().text()).containsIgnoringCase("Berlin");
+                });
+    }
+}
\ No newline at end of file
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withChatMemory/AiServiceWithChatMemory.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withChatMemory/AiServiceWithChatMemory.java
new file mode 100644
index 00000000..e97023f6
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withChatMemory/AiServiceWithChatMemory.java
@@ -0,0 +1,9 @@
+package dev.langchain4j.service.spring.mode.automatic.withChatMemory;
+
+import dev.langchain4j.service.spring.AiService;
+
+@AiService
+interface AiServiceWithChatMemory {
+
+    String chat(String userMessage);
+}
\ No newline at end of file
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withChatMemory/AiServiceWithChatMemoryApplication.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withChatMemory/AiServiceWithChatMemoryApplication.java
new file mode 100644
index 00000000..3f355219
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withChatMemory/AiServiceWithChatMemoryApplication.java
@@ -0,0 +1,20 @@
+package dev.langchain4j.service.spring.mode.automatic.withChatMemory;
+
+import dev.langchain4j.memory.ChatMemory;
+import dev.langchain4j.memory.chat.MessageWindowChatMemory;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;
+
+@SpringBootApplication
+class AiServiceWithChatMemoryApplication {
+
+    @Bean
+    ChatMemory chatMemory() {
+        return MessageWindowChatMemory.withMaxMessages(10);
+    }
+
+    public static void main(String[] args) {
+        SpringApplication.run(AiServiceWithChatMemoryApplication.class, args);
+    }
+}
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withChatMemory/AiServiceWithChatMemoryProviderIT.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withChatMemory/AiServiceWithChatMemoryProviderIT.java
new file mode 100644
index 00000000..8ef98a66
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withChatMemory/AiServiceWithChatMemoryProviderIT.java
@@ -0,0 +1,38 @@
+package dev.langchain4j.service.spring.mode.automatic.withChatMemory;
+
+import dev.langchain4j.service.spring.AiServicesAutoConfig;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static dev.langchain4j.service.spring.mode.ApiKeys.OPENAI_API_KEY;
+import static org.assertj.core.api.Assertions.assertThat;
+
+class AiServiceWithChatMemoryProviderIT {
+
+    ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+            .withConfiguration(AutoConfigurations.of(AiServicesAutoConfig.class));
+
+    @Test
+    void should_create_AI_service_with_chat_memory() {
+        contextRunner
+                .withPropertyValues(
+                        "langchain4j.open-ai.chat-model.api-key=" + OPENAI_API_KEY,
+                        "langchain4j.open-ai.chat-model.max-tokens=20",
+                        "langchain4j.open-ai.chat-model.temperature=0.0"
+                )
+                .withUserConfiguration(AiServiceWithChatMemoryApplication.class)
+                .run(context -> {
+
+                    // given
+                    AiServiceWithChatMemory aiService = context.getBean(AiServiceWithChatMemory.class);
+                    aiService.chat("My name is Klaus");
+
+                    // when
+                    String answer = aiService.chat("What is my name?");
+
+                    // then
+                    assertThat(answer).containsIgnoringCase("Klaus");
+                });
+    }
+}
\ No newline at end of file
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withChatMemoryProvider/AiServiceWithChatMemoryProvider.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withChatMemoryProvider/AiServiceWithChatMemoryProvider.java
new file mode 100644
index 00000000..3d04cc9c
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withChatMemoryProvider/AiServiceWithChatMemoryProvider.java
@@ -0,0 +1,9 @@
+package dev.langchain4j.service.spring.mode.automatic.withChatMemoryProvider;
+
+import dev.langchain4j.service.spring.AiService;
+
+@AiService
+interface AiServiceWithChatMemoryProvider {
+
+    String chat(String userMessage);
+}
\ No newline at end of file
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withChatMemoryProvider/AiServiceWithChatMemoryProviderApplication.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withChatMemoryProvider/AiServiceWithChatMemoryProviderApplication.java
new file mode 100644
index 00000000..cb58bd80
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withChatMemoryProvider/AiServiceWithChatMemoryProviderApplication.java
@@ -0,0 +1,20 @@
+package dev.langchain4j.service.spring.mode.automatic.withChatMemoryProvider;
+
+import dev.langchain4j.memory.chat.ChatMemoryProvider;
+import dev.langchain4j.memory.chat.MessageWindowChatMemory;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;
+
+@SpringBootApplication
+class AiServiceWithChatMemoryProviderApplication {
+
+    @Bean
+    ChatMemoryProvider chatMemoryProvider() {
+        return memoryId -> MessageWindowChatMemory.withMaxMessages(10);
+    }
+
+    public static void main(String[] args) {
+        SpringApplication.run(AiServiceWithChatMemoryProviderApplication.class, args);
+    }
+}
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withChatMemoryProvider/AiServiceWithChatMemoryProviderIT.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withChatMemoryProvider/AiServiceWithChatMemoryProviderIT.java
new file mode 100644
index 00000000..ae662eee
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withChatMemoryProvider/AiServiceWithChatMemoryProviderIT.java
@@ -0,0 +1,38 @@
+package dev.langchain4j.service.spring.mode.automatic.withChatMemoryProvider;
+
+import dev.langchain4j.service.spring.AiServicesAutoConfig;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static dev.langchain4j.service.spring.mode.ApiKeys.OPENAI_API_KEY;
+import static org.assertj.core.api.Assertions.assertThat;
+
+class AiServiceWithChatMemoryProviderIT {
+
+    ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+            .withConfiguration(AutoConfigurations.of(AiServicesAutoConfig.class));
+
+    @Test
+    void should_create_AI_service_with_chat_memory_provider() {
+        contextRunner
+                .withPropertyValues(
+                        "langchain4j.open-ai.chat-model.api-key=" + OPENAI_API_KEY,
+                        "langchain4j.open-ai.chat-model.max-tokens=20",
+                        "langchain4j.open-ai.chat-model.temperature=0.0"
+                )
+                .withUserConfiguration(AiServiceWithChatMemoryProviderApplication.class)
+                .run(context -> {
+
+                    // given
+                    AiServiceWithChatMemoryProvider aiService = context.getBean(AiServiceWithChatMemoryProvider.class);
+                    aiService.chat("My name is Klaus");
+
+                    // when
+                    String answer = aiService.chat("What is my name?");
+
+                    // then
+                    assertThat(answer).containsIgnoringCase("Klaus");
+                });
+    }
+}
\ No newline at end of file
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withContentRetriever/AiServiceWithContentRetriever.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withContentRetriever/AiServiceWithContentRetriever.java
new file mode 100644
index 00000000..3b9890c9
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withContentRetriever/AiServiceWithContentRetriever.java
@@ -0,0 +1,9 @@
+package dev.langchain4j.service.spring.mode.automatic.withContentRetriever;
+
+import dev.langchain4j.service.spring.AiService;
+
+@AiService
+interface AiServiceWithContentRetriever {
+
+    String chat(String userMessage);
+}
\ No newline at end of file
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withContentRetriever/AiServiceWithContentRetrieverApplication.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withContentRetriever/AiServiceWithContentRetrieverApplication.java
new file mode 100644
index 00000000..b62a5e04
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withContentRetriever/AiServiceWithContentRetrieverApplication.java
@@ -0,0 +1,22 @@
+package dev.langchain4j.service.spring.mode.automatic.withContentRetriever;
+
+import dev.langchain4j.rag.content.Content;
+import dev.langchain4j.rag.content.retriever.ContentRetriever;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;
+
+import static java.util.Collections.singletonList;
+
+@SpringBootApplication
+class AiServiceWithContentRetrieverApplication {
+
+    @Bean
+    ContentRetriever contentRetriever() {
+        return query -> singletonList(Content.from("My name is Klaus"));
+    }
+
+    public static void main(String[] args) {
+        SpringApplication.run(AiServiceWithContentRetrieverApplication.class, args);
+    }
+}
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withContentRetriever/AiServiceWithRetrievalAugmentorIT.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withContentRetriever/AiServiceWithRetrievalAugmentorIT.java
new file mode 100644
index 00000000..98be45e1
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withContentRetriever/AiServiceWithRetrievalAugmentorIT.java
@@ -0,0 +1,37 @@
+package dev.langchain4j.service.spring.mode.automatic.withContentRetriever;
+
+import dev.langchain4j.service.spring.AiServicesAutoConfig;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static dev.langchain4j.service.spring.mode.ApiKeys.OPENAI_API_KEY;
+import static org.assertj.core.api.Assertions.assertThat;
+
+class AiServiceWithRetrievalAugmentorIT {
+
+    ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+            .withConfiguration(AutoConfigurations.of(AiServicesAutoConfig.class));
+
+    @Test
+    void should_create_AI_service_with_content_retriever() {
+        contextRunner
+                .withPropertyValues(
+                        "langchain4j.open-ai.chat-model.api-key=" + OPENAI_API_KEY,
+                        "langchain4j.open-ai.chat-model.max-tokens=20",
+                        "langchain4j.open-ai.chat-model.temperature=0.0"
+                )
+                .withUserConfiguration(AiServiceWithContentRetrieverApplication.class)
+                .run(context -> {
+
+                    // given
+                    AiServiceWithContentRetriever aiService = context.getBean(AiServiceWithContentRetriever.class);
+
+                    // when
+                    String answer = aiService.chat("What is my name?");
+
+                    // then
+                    assertThat(answer).containsIgnoringCase("Klaus");
+                });
+    }
+}
\ No newline at end of file
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withRetrievalAugmentor/AiServiceWithRetrievalAugmentor.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withRetrievalAugmentor/AiServiceWithRetrievalAugmentor.java
new file mode 100644
index 00000000..10af6692
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withRetrievalAugmentor/AiServiceWithRetrievalAugmentor.java
@@ -0,0 +1,9 @@
+package dev.langchain4j.service.spring.mode.automatic.withRetrievalAugmentor;
+
+import dev.langchain4j.service.spring.AiService;
+
+@AiService
+interface AiServiceWithRetrievalAugmentor {
+
+    String chat(String userMessage);
+}
\ No newline at end of file
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withRetrievalAugmentor/AiServiceWithRetrievalAugmentorApplication.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withRetrievalAugmentor/AiServiceWithRetrievalAugmentorApplication.java
new file mode 100644
index 00000000..6721d41a
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withRetrievalAugmentor/AiServiceWithRetrievalAugmentorApplication.java
@@ -0,0 +1,20 @@
+package dev.langchain4j.service.spring.mode.automatic.withRetrievalAugmentor;
+
+import dev.langchain4j.data.message.UserMessage;
+import dev.langchain4j.rag.RetrievalAugmentor;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;
+
+@SpringBootApplication
+class AiServiceWithRetrievalAugmentorApplication {
+
+    @Bean
+    RetrievalAugmentor retrievalAugmentor() {
+        return (userMessage, metadata) -> UserMessage.from("My name is Klaus." + userMessage);
+    }
+
+    public static void main(String[] args) {
+        SpringApplication.run(AiServiceWithRetrievalAugmentorApplication.class, args);
+    }
+}
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withRetrievalAugmentor/AiServiceWithRetrievalAugmentorIT.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withRetrievalAugmentor/AiServiceWithRetrievalAugmentorIT.java
new file mode 100644
index 00000000..58ad14e4
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withRetrievalAugmentor/AiServiceWithRetrievalAugmentorIT.java
@@ -0,0 +1,37 @@
+package dev.langchain4j.service.spring.mode.automatic.withRetrievalAugmentor;
+
+import dev.langchain4j.service.spring.AiServicesAutoConfig;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static dev.langchain4j.service.spring.mode.ApiKeys.OPENAI_API_KEY;
+import static org.assertj.core.api.Assertions.assertThat;
+
+class AiServiceWithRetrievalAugmentorIT {
+
+    ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+            .withConfiguration(AutoConfigurations.of(AiServicesAutoConfig.class));
+
+    @Test
+    void should_create_AI_service_with_retrieval_augmentor() {
+        contextRunner
+                .withPropertyValues(
+                        "langchain4j.open-ai.chat-model.api-key=" + OPENAI_API_KEY,
+                        "langchain4j.open-ai.chat-model.max-tokens=20",
+                        "langchain4j.open-ai.chat-model.temperature=0.0"
+                )
+                .withUserConfiguration(AiServiceWithRetrievalAugmentorApplication.class)
+                .run(context -> {
+
+                    // given
+                    AiServiceWithRetrievalAugmentor aiService = context.getBean(AiServiceWithRetrievalAugmentor.class);
+
+                    // when
+                    String answer = aiService.chat("What is my name?");
+
+                    // then
+                    assertThat(answer).containsIgnoringCase("Klaus");
+                });
+    }
+}
\ No newline at end of file
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/PackagePrivateAssistant.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/AiServiceWithTools.java
similarity index 50%
rename from langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/PackagePrivateAssistant.java
rename to langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/AiServiceWithTools.java
index f8bdfdd9..d2e2b243 100644
--- a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/PackagePrivateAssistant.java
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/AiServiceWithTools.java
@@ -1,9 +1,9 @@
-package dev.langchain4j.service.spring.defaultConfig;
+package dev.langchain4j.service.spring.mode.automatic.withTools;
 
 import dev.langchain4j.service.spring.AiService;
 
 @AiService
-interface PackagePrivateAssistant {
+interface AiServiceWithTools {
 
     String chat(String userMessage);
-}
\ No newline at end of file
+}
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/AiServiceWithToolsApplication.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/AiServiceWithToolsApplication.java
new file mode 100644
index 00000000..1104d337
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/AiServiceWithToolsApplication.java
@@ -0,0 +1,12 @@
+package dev.langchain4j.service.spring.mode.automatic.withTools;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+class AiServiceWithToolsApplication {
+
+    public static void main(String[] args) {
+        SpringApplication.run(AiServiceWithToolsApplication.class, args);
+    }
+}
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/AiServicesAutoConfigIT.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/AiServicesAutoConfigIT.java
new file mode 100644
index 00000000..fa02f670
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/AiServicesAutoConfigIT.java
@@ -0,0 +1,70 @@
+package dev.langchain4j.service.spring.mode.automatic.withTools;
+
+import dev.langchain4j.service.spring.AiServicesAutoConfig;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import java.time.LocalDateTime;
+
+import static dev.langchain4j.service.spring.mode.ApiKeys.OPENAI_API_KEY;
+import static dev.langchain4j.service.spring.mode.automatic.withTools.PublicTools.CURRENT_TEMPERATURE;
+import static org.assertj.core.api.Assertions.assertThat;
+
+class AiServicesAutoConfigIT {
+
+    ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+            .withConfiguration(AutoConfigurations.of(AiServicesAutoConfig.class));
+
+    @Test
+    void should_create_AI_service_with_tool_which_is_public_method_in_public_class() {
+        contextRunner
+                .withPropertyValues(
+                        "langchain4j.open-ai.chat-model.api-key=" + OPENAI_API_KEY,
+                        "langchain4j.open-ai.chat-model.max-tokens=20",
+                        "langchain4j.open-ai.chat-model.temperature=0.0",
+                        "langchain4j.open-ai.chat-model.log-requests=true",
+                        "langchain4j.open-ai.chat-model.log-responses=true"
+                )
+                .withUserConfiguration(AiServiceWithToolsApplication.class)
+                .run(context -> {
+
+                    // given
+                    AiServiceWithTools aiService = context.getBean(AiServiceWithTools.class);
+
+                    // when
+                    String answer = aiService.chat("What is the current temperature?");
+
+                    // then should use PublicTools.getCurrentTemperature()
+                    assertThat(answer).contains(String.valueOf(CURRENT_TEMPERATURE));
+                });
+    }
+
+    @Test
+    void should_create_AI_service_with_tool_that_is_package_private_method_in_package_private_class() {
+        contextRunner
+                .withPropertyValues(
+                        "langchain4j.open-ai.chat-model.api-key=" + OPENAI_API_KEY,
+                        "langchain4j.open-ai.chat-model.max-tokens=20",
+                        "langchain4j.open-ai.chat-model.temperature=0.0"
+                )
+                .withUserConfiguration(AiServiceWithToolsApplication.class)
+                .run(context -> {
+
+                    // given
+                    AiServiceWithTools aiService = context.getBean(AiServiceWithTools.class);
+
+                    // when
+                    String answer = aiService.chat("What is the current minute?");
+
+                    // then should use PackagePrivateTools.getCurrentMinute()
+                    assertThat(answer).contains(String.valueOf(LocalDateTime.now().getMinute()));
+                });
+    }
+
+    // TODO tools which are not @Beans?
+    // TODO negative cases
+    // TODO no @AiServices in app, just models
+    // TODO @AiServices as inner class?
+    // TODO streaming, memory, tools, etc
+}
\ No newline at end of file
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/PackagePrivateTools.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/PackagePrivateTools.java
similarity index 73%
rename from langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/PackagePrivateTools.java
rename to langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/PackagePrivateTools.java
index 55b181cc..150be1d0 100644
--- a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/defaultConfig/PackagePrivateTools.java
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/PackagePrivateTools.java
@@ -1,11 +1,11 @@
-package dev.langchain4j.service.spring.defaultConfig;
+package dev.langchain4j.service.spring.mode.automatic.withTools;
 
 import dev.langchain4j.agent.tool.Tool;
 import org.springframework.stereotype.Component;
 
 import java.time.LocalDateTime;
 
-@Component("packagePrivateTools")
+@Component
 class PackagePrivateTools {
 
     @Tool
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/PublicTools.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/PublicTools.java
new file mode 100644
index 00000000..3f8d4bff
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/PublicTools.java
@@ -0,0 +1,15 @@
+package dev.langchain4j.service.spring.mode.automatic.withTools;
+
+import dev.langchain4j.agent.tool.Tool;
+import org.springframework.stereotype.Component;
+
+@Component
+public class PublicTools {
+
+    static int CURRENT_TEMPERATURE = 42;
+
+    @Tool
+    public int getCurrentTemperature() {
+        return CURRENT_TEMPERATURE;
+    }
+}
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/explicit/chatModel/AiServiceWithExplicitChatModel.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/explicit/chatModel/AiServiceWithExplicitChatModel.java
new file mode 100644
index 00000000..c80698e6
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/explicit/chatModel/AiServiceWithExplicitChatModel.java
@@ -0,0 +1,12 @@
+package dev.langchain4j.service.spring.mode.explicit.chatModel;
+
+import dev.langchain4j.service.spring.AiService;
+
+import static dev.langchain4j.service.spring.AiServiceWiringMode.EXPLICIT;
+import static dev.langchain4j.service.spring.mode.explicit.chatModel.AiServiceWithExplicitChatModelApplication.CHAT_MODEL_BEAN_NAME;
+
+@AiService(wiringMode = EXPLICIT, chatModel = CHAT_MODEL_BEAN_NAME)
+interface AiServiceWithExplicitChatModel {
+
+    String chat(String userMessage);
+}
\ No newline at end of file
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/customConfig/chatModel/TestApplicationWithCustomChatModel.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/explicit/chatModel/AiServiceWithExplicitChatModelApplication.java
similarity index 55%
rename from langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/customConfig/chatModel/TestApplicationWithCustomChatModel.java
rename to langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/explicit/chatModel/AiServiceWithExplicitChatModelApplication.java
index 8cde86bd..da241de9 100644
--- a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/customConfig/chatModel/TestApplicationWithCustomChatModel.java
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/explicit/chatModel/AiServiceWithExplicitChatModelApplication.java
@@ -1,4 +1,4 @@
-package dev.langchain4j.service.spring.customConfig.chatModel;
+package dev.langchain4j.service.spring.mode.explicit.chatModel;
 
 import dev.langchain4j.model.chat.ChatLanguageModel;
 import dev.langchain4j.model.openai.OpenAiChatModel;
@@ -7,21 +7,23 @@
 import org.springframework.context.annotation.Bean;
 
 @SpringBootApplication
-public class TestApplicationWithCustomChatModel {
+class AiServiceWithExplicitChatModelApplication {
 
-    static final String CUSTOM_CHAT_MODEL_BEAN_NAME = "customChatModel";
+    static final String CHAT_MODEL_BEAN_NAME = "myChatModel";
 
-    @Bean(CUSTOM_CHAT_MODEL_BEAN_NAME)
+    @Bean(CHAT_MODEL_BEAN_NAME)
     ChatLanguageModel chatLanguageModel() {
         return OpenAiChatModel.withApiKey(System.getenv("OPENAI_API_KEY"));
     }
 
-    @Bean(CUSTOM_CHAT_MODEL_BEAN_NAME + 2)
+    @Bean(CHAT_MODEL_BEAN_NAME + 2)
     ChatLanguageModel chatLanguageModel2() {
-        return OpenAiChatModel.withApiKey(System.getenv("OPENAI_API_KEY"));
+        return messages -> {
+            throw new RuntimeException("should never be invoked");
+        };
     }
 
     public static void main(String[] args) {
-        SpringApplication.run(TestApplicationWithCustomChatModel.class, args);
+        SpringApplication.run(AiServiceWithExplicitChatModelApplication.class, args);
     }
 }
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/explicit/chatModel/AiServiceWithExplicitChatModelIT.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/explicit/chatModel/AiServiceWithExplicitChatModelIT.java
new file mode 100644
index 00000000..a40a5303
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/explicit/chatModel/AiServiceWithExplicitChatModelIT.java
@@ -0,0 +1,31 @@
+package dev.langchain4j.service.spring.mode.explicit.chatModel;
+
+import dev.langchain4j.service.spring.AiServicesAutoConfig;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class AiServiceWithExplicitChatModelIT {
+
+    ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+            .withConfiguration(AutoConfigurations.of(AiServicesAutoConfig.class));
+
+    @Test
+    void should_create_AI_service_with_explicit_chat_model() {
+        contextRunner
+                .withUserConfiguration(AiServiceWithExplicitChatModelApplication.class)
+                .run(context -> {
+
+                    // given
+                    AiServiceWithExplicitChatModel aiService = context.getBean(AiServiceWithExplicitChatModel.class);
+
+                    // when
+                    String answer = aiService.chat("What is the capital of Germany?");
+
+                    // then
+                    assertThat(answer).containsIgnoringCase("Berlin");
+                });
+    }
+}
\ No newline at end of file
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/explicit/multiple/BaseAiService.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/explicit/multiple/BaseAiService.java
new file mode 100644
index 00000000..d09c399e
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/explicit/multiple/BaseAiService.java
@@ -0,0 +1,6 @@
+package dev.langchain4j.service.spring.mode.explicit.multiple;
+
+interface BaseAiService {
+
+    String chat(String userMessage);
+}
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/explicit/multiple/FirstAiServiceWithAutomaticWiring.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/explicit/multiple/FirstAiServiceWithAutomaticWiring.java
new file mode 100644
index 00000000..5f31919d
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/explicit/multiple/FirstAiServiceWithAutomaticWiring.java
@@ -0,0 +1,10 @@
+package dev.langchain4j.service.spring.mode.explicit.multiple;
+
+import dev.langchain4j.service.spring.AiService;
+
+@AiService // wiringMode = AUTOMATIC is default
+interface FirstAiServiceWithAutomaticWiring extends BaseAiService {
+
+    @Override
+    String chat(String userMessage);
+}
\ No newline at end of file
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/explicit/multiple/FirstAiServiceWithExplicitWiring.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/explicit/multiple/FirstAiServiceWithExplicitWiring.java
new file mode 100644
index 00000000..3efae06e
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/explicit/multiple/FirstAiServiceWithExplicitWiring.java
@@ -0,0 +1,13 @@
+package dev.langchain4j.service.spring.mode.explicit.multiple;
+
+import dev.langchain4j.service.spring.AiService;
+
+import static dev.langchain4j.service.spring.AiServiceWiringMode.EXPLICIT;
+import static dev.langchain4j.service.spring.mode.explicit.multiple.MultipleAiServicesApplication.CHAT_MODEL_BEAN_NAME;
+
+@AiService(wiringMode = EXPLICIT, chatModel = CHAT_MODEL_BEAN_NAME)
+interface FirstAiServiceWithExplicitWiring extends BaseAiService {
+
+    @Override
+    String chat(String userMessage);
+}
\ No newline at end of file
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/explicit/multiple/MultipleAiServicesApplication.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/explicit/multiple/MultipleAiServicesApplication.java
new file mode 100644
index 00000000..0b8a407b
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/explicit/multiple/MultipleAiServicesApplication.java
@@ -0,0 +1,29 @@
+package dev.langchain4j.service.spring.mode.explicit.multiple;
+
+import dev.langchain4j.memory.ChatMemory;
+import dev.langchain4j.memory.chat.MessageWindowChatMemory;
+import dev.langchain4j.model.chat.ChatLanguageModel;
+import dev.langchain4j.model.openai.OpenAiChatModel;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;
+
+@SpringBootApplication
+class MultipleAiServicesApplication {
+
+    static final String CHAT_MODEL_BEAN_NAME = "myChatModel";
+
+    @Bean(CHAT_MODEL_BEAN_NAME)
+    ChatLanguageModel chatLanguageModel() {
+        return OpenAiChatModel.withApiKey(System.getenv("OPENAI_API_KEY"));
+    }
+
+    @Bean
+    ChatMemory chatMemory() {
+        return MessageWindowChatMemory.withMaxMessages(10);
+    }
+
+    public static void main(String[] args) {
+        SpringApplication.run(MultipleAiServicesApplication.class, args);
+    }
+}
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/explicit/multiple/MultipleAiServicesIT.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/explicit/multiple/MultipleAiServicesIT.java
new file mode 100644
index 00000000..faeb897a
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/explicit/multiple/MultipleAiServicesIT.java
@@ -0,0 +1,54 @@
+package dev.langchain4j.service.spring.mode.explicit.multiple;
+
+import dev.langchain4j.service.spring.AiServicesAutoConfig;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class MultipleAiServicesIT {
+
+    ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+            .withConfiguration(AutoConfigurations.of(AiServicesAutoConfig.class));
+
+    @Test
+    void should_create_AI_service_with_explicit_chat_model() {
+        contextRunner
+                .withUserConfiguration(MultipleAiServicesApplication.class)
+                .run(context -> {
+
+                    // MultipleAiServicesApplication.chatMemory() is wired automatically because wiringMode = AUTOMATIC
+                    testWithMemory(context.getBean(FirstAiServiceWithAutomaticWiring.class));
+                    testWithMemory(context.getBean(SecondAiServiceWithAutomaticWiring.class));
+
+                    // MultipleAiServicesApplication.chatMemory() is NOT wired because wiringMode = EXPLICIT
+                    testWithoutMemory(context.getBean(FirstAiServiceWithExplicitWiring.class));
+                    testWithoutMemory(context.getBean(SecondAiServiceWithExplicitWiring.class));
+                });
+    }
+
+    private static void testWithMemory(BaseAiService aiService) {
+
+        // given
+        aiService.chat("My name is Klaus");
+
+        // when
+        String answer = aiService.chat("What is my name?");
+
+        // then
+        assertThat(answer).containsIgnoringCase("Klaus");
+    }
+
+    private static void testWithoutMemory(BaseAiService aiService) {
+
+        // given
+        aiService.chat("My name is Klaus");
+
+        // when
+        String answer = aiService.chat("What is my name?");
+
+        // then
+        assertThat(answer).doesNotContainIgnoringCase("Klaus");
+    }
+}
\ No newline at end of file
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/explicit/multiple/SecondAiServiceWithAutomaticWiring.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/explicit/multiple/SecondAiServiceWithAutomaticWiring.java
new file mode 100644
index 00000000..be782d8e
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/explicit/multiple/SecondAiServiceWithAutomaticWiring.java
@@ -0,0 +1,10 @@
+package dev.langchain4j.service.spring.mode.explicit.multiple;
+
+import dev.langchain4j.service.spring.AiService;
+
+@AiService // wiringMode = AUTOMATIC is default
+interface SecondAiServiceWithAutomaticWiring extends BaseAiService {
+
+    @Override
+    String chat(String userMessage);
+}
\ No newline at end of file
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/explicit/multiple/SecondAiServiceWithExplicitWiring.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/explicit/multiple/SecondAiServiceWithExplicitWiring.java
new file mode 100644
index 00000000..843d3390
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/explicit/multiple/SecondAiServiceWithExplicitWiring.java
@@ -0,0 +1,13 @@
+package dev.langchain4j.service.spring.mode.explicit.multiple;
+
+import dev.langchain4j.service.spring.AiService;
+
+import static dev.langchain4j.service.spring.AiServiceWiringMode.EXPLICIT;
+import static dev.langchain4j.service.spring.mode.explicit.multiple.MultipleAiServicesApplication.CHAT_MODEL_BEAN_NAME;
+
+@AiService(wiringMode = EXPLICIT, chatModel = CHAT_MODEL_BEAN_NAME)
+interface SecondAiServiceWithExplicitWiring extends BaseAiService {
+
+    @Override
+    String chat(String userMessage);
+}
\ No newline at end of file

From d70ea697dee7a152ee03b45ccdf7ec9b7f6c8c82 Mon Sep 17 00:00:00 2001
From: LangChain4j 
Date: Mon, 25 Mar 2024 18:00:24 +0100
Subject: [PATCH 4/5] WIP: declarative AI services and EasyRAG

---
 pom.xml | 1 -
 1 file changed, 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index 91839a13..a1e1886a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -15,7 +15,6 @@
 
     
         langchain4j-spring-boot-starter
-        langchain4j-easy-rag-spring-boot-starter
 
         langchain4j-ollama-spring-boot-starter
         langchain4j-open-ai-spring-boot-starter

From 09bc94653906b9c9cb25d93be29a070c3ecb48b9 Mon Sep 17 00:00:00 2001
From: LangChain4j 
Date: Mon, 25 Mar 2024 18:08:49 +0100
Subject: [PATCH 5/5] WIP: declarative AI services and EasyRAG

---
 .../dev/langchain4j/service/spring/AiServicesAutoConfig.java    | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java
index 6c6d52f1..7d5e92b4 100644
--- a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java
+++ b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java
@@ -143,7 +143,7 @@ BeanFactoryPostProcessor aiServicesRegisteringBeanFactoryPostProcessor() {
                 } else if (aiServiceAnnotation.wiringMode() == AUTOMATIC) {
                     propertyValues.add("tools", toManagedList(tools));
                 } else {
-                    throw illegalArgument("Unknown component selection mode: " + aiServiceAnnotation.wiringMode());
+                    throw illegalArgument("Unknown wiring mode: " + aiServiceAnnotation.wiringMode());
                 }
 
                 BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;