From 83ace7738304c0fc81d8b59b454dc9211903a31a Mon Sep 17 00:00:00 2001 From: LangChain4j Date: Mon, 25 Mar 2024 17:59:22 +0100 Subject: [PATCH] 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