builder = AiServices.builder(aiServiceClass);
+
+ if (chatLanguageModel != null) {
+ builder = builder.chatLanguageModel(chatLanguageModel);
+ }
+
+ if (streamingChatLanguageModel != null) {
+ builder = builder.streamingChatLanguageModel(streamingChatLanguageModel);
+ }
+
+ if (chatMemory != null) {
+ builder.chatMemory(chatMemory);
+ }
+
+ if (chatMemoryProvider != null) {
+ builder.chatMemoryProvider(chatMemoryProvider);
+ }
+
+ if (contentRetriever != null) {
+ builder = builder.contentRetriever(contentRetriever);
+ }
+
+ if (retrievalAugmentor != null) {
+ builder = builder.retrievalAugmentor(retrievalAugmentor);
+ }
+
+ if (!isNullOrEmpty(tools)) {
+ builder = builder.tools(tools);
+ }
+
+ return builder.build();
+ }
+
+ @Override
+ public Class> getObjectType() {
+ return aiServiceClass;
+ }
+
+ @Override
+ public boolean isSingleton() {
+ return true; // TODO
+ }
+
+ /**
+ * TODO
+ * getObjectType() getObject() invocations may arrive early in the bootstrap process, even ahead of any post-processor setup.
+ *
+ * TODO
+ * The container is only responsible for managing the lifecycle of the FactoryBean instance, not the lifecycle
+ * of the objects created by the FactoryBean. Therefore, a destroy method on an exposed bean object
+ * (such as java.io.Closeable.close()) will not be called automatically.
+ * Instead, a FactoryBean should implement DisposableBean and delegate any such close call to the underlying object.
+ */
+}
diff --git a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/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
new file mode 100644
index 00000000..7d5e92b4
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java
@@ -0,0 +1,206 @@
+package dev.langchain4j.service.spring;
+
+import dev.langchain4j.agent.tool.Tool;
+import dev.langchain4j.exception.IllegalConfigurationException;
+import dev.langchain4j.memory.ChatMemory;
+import dev.langchain4j.memory.chat.ChatMemoryProvider;
+import dev.langchain4j.model.chat.ChatLanguageModel;
+import dev.langchain4j.model.chat.StreamingChatLanguageModel;
+import dev.langchain4j.rag.RetrievalAugmentor;
+import dev.langchain4j.rag.content.retriever.ContentRetriever;
+import org.reflections.Reflections;
+import org.springframework.beans.MutablePropertyValues;
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
+import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
+import org.springframework.beans.factory.config.RuntimeBeanReference;
+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 {
+
+ @Bean
+ 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);
+ String[] chatMemoryProviders = beanFactory.getBeanNamesForType(ChatMemoryProvider.class);
+ String[] contentRetrievers = beanFactory.getBeanNamesForType(ContentRetriever.class);
+ String[] retrievalAugmentors = beanFactory.getBeanNamesForType(RetrievalAugmentor.class);
+
+ 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)) {
+ tools.add(beanName);
+ }
+ }
+ } catch (Exception e) {
+ // TODO
+ }
+ }
+
+ findAiServices(beanFactory).forEach(aiServiceClass -> {
+
+ if (beanFactory.getBeanNamesForType(aiServiceClass).length > 0) {
+ // User probably wants to configure AI Service bean manually
+ // TODO or better fail because user should not annotate it with @AiService then?
+ return;
+ }
+
+ GenericBeanDefinition aiServiceBeanDefinition = new GenericBeanDefinition();
+ aiServiceBeanDefinition.setBeanClass(AiServiceFactory.class);
+ aiServiceBeanDefinition.getConstructorArgumentValues().addGenericArgumentValue(aiServiceClass);
+ MutablePropertyValues propertyValues = aiServiceBeanDefinition.getPropertyValues();
+
+ AiService aiServiceAnnotation = aiServiceClass.getAnnotation(AiService.class);
+
+ addBeanReference(
+ ChatLanguageModel.class,
+ aiServiceAnnotation,
+ 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.wiringMode() == EXPLICIT) {
+ propertyValues.add("tools", toManagedList(asList(aiServiceAnnotation.tools())));
+ } else if (aiServiceAnnotation.wiringMode() == AUTOMATIC) {
+ propertyValues.add("tools", toManagedList(tools));
+ } else {
+ throw illegalArgument("Unknown wiring mode: " + aiServiceAnnotation.wiringMode());
+ }
+
+ BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;
+ registry.registerBeanDefinition(lowercaseFirstLetter(aiServiceClass.getSimpleName()), aiServiceBeanDefinition);
+ });
+ };
+ }
+
+ private static Set> findAiServices(ConfigurableListableBeanFactory beanFactory) {
+ String[] applicationBean = beanFactory.getBeanNamesForAnnotation(SpringBootApplication.class);
+ BeanDefinition applicationBeanDefinition = beanFactory.getBeanDefinition(applicationBean[0]);
+ String basePackage = applicationBeanDefinition.getResolvableType().resolve().getPackage().getName();
+ Reflections reflections = new Reflections(basePackage);
+ return reflections.getTypesAnnotatedWith(AiService.class);
+ }
+
+ private static void addBeanReference(Class> beanType,
+ AiService aiServiceAnnotation,
+ String customBeanName,
+ String[] beanNames,
+ String annotationAttributeName,
+ String factoryPropertyName,
+ MutablePropertyValues propertyValues) {
+ if (aiServiceAnnotation.wiringMode() == EXPLICIT) {
+ if (isNotNullOrBlank(customBeanName)) {
+ propertyValues.add(factoryPropertyName, new RuntimeBeanReference(customBeanName));
+ }
+ } else if (aiServiceAnnotation.wiringMode() == AUTOMATIC) {
+ if (beanNames.length == 1) {
+ propertyValues.add(factoryPropertyName, new RuntimeBeanReference(beanNames[0]));
+ } else if (beanNames.length > 1) {
+ throw conflict(beanType, beanNames, annotationAttributeName);
+ }
+ } else {
+ throw illegalArgument("Unknown wiring mode: " + aiServiceAnnotation.wiringMode());
+ }
+ }
+
+ 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 wire in the @AiService annotation like this: " +
+ "@AiService(wiringMode = EXPLICIT, %s = \"\").",
+ beanType.getName(), Arrays.toString(beanNames), attributeName);
+ }
+
+ private static String lowercaseFirstLetter(String text) {
+ if (isNullOrBlank(text)) {
+ return 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/main/java/dev/langchain4j/spring/LangChain4jAutoConfig.java b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/spring/LangChain4jAutoConfig.java
new file mode 100644
index 00000000..52c461b2
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/spring/LangChain4jAutoConfig.java
@@ -0,0 +1,14 @@
+package dev.langchain4j.spring;
+
+import dev.langchain4j.rag.spring.RagAutoConfig;
+import dev.langchain4j.service.spring.AiServicesAutoConfig;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.context.annotation.Import;
+
+@AutoConfiguration
+@Import({
+ AiServicesAutoConfig.class,
+ RagAutoConfig.class
+})
+public class LangChain4jAutoConfig {
+}
diff --git a/langchain4j-spring-boot-starter/src/main/resources/META-INF/spring.factories b/langchain4j-spring-boot-starter/src/main/resources/META-INF/spring.factories
new file mode 100644
index 00000000..7c967b04
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/main/resources/META-INF/spring.factories
@@ -0,0 +1 @@
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=dev.langchain4j.spring.LangChain4jAutoConfig
\ No newline at end of file
diff --git a/langchain4j-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/langchain4j-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 00000000..5a5aac00
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+dev.langchain4j.spring.LangChain4jAutoConfig
\ No newline at end of file
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/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/mode/automatic/conflictingChatModels/AiServiceWithConflictingChatMemoriesIT.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/conflictingChatModels/AiServiceWithConflictingChatMemoriesIT.java
new file mode 100644
index 00000000..bf872f9d
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/conflictingChatModels/AiServiceWithConflictingChatMemoriesIT.java
@@ -0,0 +1,32 @@
+package dev.langchain4j.service.spring.mode.automatic.conflictingChatModels;
+
+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_models_are_found() {
+ contextRunner
+ .withUserConfiguration(AiServiceWithConflictingChatModelsApplication.class)
+ .run(context -> {
+
+ 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 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/mode/automatic/conflictingChatModels/AiServiceWithConflictingChatModelsApplication.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/conflictingChatModels/AiServiceWithConflictingChatModelsApplication.java
new file mode 100644
index 00000000..a20dfe4b
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/conflictingChatModels/AiServiceWithConflictingChatModelsApplication.java
@@ -0,0 +1,25 @@
+package dev.langchain4j.service.spring.mode.automatic.conflictingChatModels;
+
+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 AiServiceWithConflictingChatModelsApplication {
+
+ @Bean
+ ChatLanguageModel chatLanguageModel() {
+ return OpenAiChatModel.withApiKey(System.getenv("OPENAI_API_KEY"));
+ }
+
+ @Bean
+ ChatLanguageModel chatLanguageModel2() {
+ return OpenAiChatModel.withApiKey(System.getenv("OPENAI_API_KEY"));
+ }
+
+ public static void main(String[] args) {
+ SpringApplication.run(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/mode/automatic/innerClass/OuterClass.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/innerClass/OuterClass.java
new file mode 100644
index 00000000..969ed473
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/innerClass/OuterClass.java
@@ -0,0 +1,12 @@
+package dev.langchain4j.service.spring.mode.automatic.innerClass;
+
+import dev.langchain4j.service.spring.AiService;
+
+class OuterClass {
+
+ @AiService
+ 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/mode/automatic/publicClass/PublicAiServiceApplication.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/publicClass/PublicAiServiceApplication.java
new file mode 100644
index 00000000..efbd158f
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/publicClass/PublicAiServiceApplication.java
@@ -0,0 +1,12 @@
+package dev.langchain4j.service.spring.mode.automatic.publicClass;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+class PublicAiServiceApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(PublicAiServiceApplication.class, args);
+ }
+}
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/publicClass/PublicAiServiceIT.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/publicClass/PublicAiServiceIT.java
new file mode 100644
index 00000000..7592d4bb
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/publicClass/PublicAiServiceIT.java
@@ -0,0 +1,37 @@
+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 PublicAiServiceIT {
+
+ 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=" + OPENAI_API_KEY,
+ "langchain4j.open-ai.chat-model.max-tokens=20",
+ "langchain4j.open-ai.chat-model.temperature=0.0"
+ )
+ .withUserConfiguration(PublicAiServiceApplication.class)
+ .run(context -> {
+
+ // given
+ PublicAiService aiService = context.getBean(PublicAiService.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/streaming/StreamingAiService.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/streaming/StreamingAiService.java
new file mode 100644
index 00000000..5f7c4bef
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/streaming/StreamingAiService.java
@@ -0,0 +1,10 @@
+package dev.langchain4j.service.spring.mode.automatic.streaming;
+
+import dev.langchain4j.service.TokenStream;
+import dev.langchain4j.service.spring.AiService;
+
+@AiService
+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/mode/automatic/withTools/AiServiceWithTools.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/AiServiceWithTools.java
new file mode 100644
index 00000000..d2e2b243
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/AiServiceWithTools.java
@@ -0,0 +1,9 @@
+package dev.langchain4j.service.spring.mode.automatic.withTools;
+
+import dev.langchain4j.service.spring.AiService;
+
+@AiService
+interface AiServiceWithTools {
+
+ String chat(String userMessage);
+}
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/mode/automatic/withTools/PackagePrivateTools.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/PackagePrivateTools.java
new file mode 100644
index 00000000..150be1d0
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/PackagePrivateTools.java
@@ -0,0 +1,15 @@
+package dev.langchain4j.service.spring.mode.automatic.withTools;
+
+import dev.langchain4j.agent.tool.Tool;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+
+@Component
+class PackagePrivateTools {
+
+ @Tool
+ int getCurrentMinute() {
+ return LocalDateTime.now().getMinute();
+ }
+}
diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/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/mode/explicit/chatModel/AiServiceWithExplicitChatModelApplication.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/explicit/chatModel/AiServiceWithExplicitChatModelApplication.java
new file mode 100644
index 00000000..da241de9
--- /dev/null
+++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/explicit/chatModel/AiServiceWithExplicitChatModelApplication.java
@@ -0,0 +1,29 @@
+package dev.langchain4j.service.spring.mode.explicit.chatModel;
+
+import dev.langchain4j.model.chat.ChatLanguageModel;
+import dev.langchain4j.model.openai.OpenAiChatModel;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;
+
+@SpringBootApplication
+class AiServiceWithExplicitChatModelApplication {
+
+ static final String CHAT_MODEL_BEAN_NAME = "myChatModel";
+
+ @Bean(CHAT_MODEL_BEAN_NAME)
+ ChatLanguageModel chatLanguageModel() {
+ return OpenAiChatModel.withApiKey(System.getenv("OPENAI_API_KEY"));
+ }
+
+ @Bean(CHAT_MODEL_BEAN_NAME + 2)
+ ChatLanguageModel chatLanguageModel2() {
+ return messages -> {
+ throw new RuntimeException("should never be invoked");
+ };
+ }
+
+ public static void main(String[] 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
diff --git a/pom.xml b/pom.xml
index c0f38a58..a1e1886a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -14,6 +14,8 @@
https://github.com/langchain4j/langchain4j-spring
+ langchain4j-spring-boot-starter
+
langchain4j-ollama-spring-boot-starter
langchain4j-open-ai-spring-boot-starter