diff --git a/langchain4j-easy-rag-spring-boot-starter/pom.xml b/langchain4j-easy-rag-spring-boot-starter/pom.xml new file mode 100644 index 00000000..e16d86cf --- /dev/null +++ b/langchain4j-easy-rag-spring-boot-starter/pom.xml @@ -0,0 +1,79 @@ + + + 4.0.0 + + + dev.langchain4j + langchain4j-spring + 0.29.0-SNAPSHOT + ../pom.xml + + + langchain4j-easy-rag-spring-boot-starter + Spring Boot starter for Easy RAG + + + + + dev.langchain4j + langchain4j-easy-rag + ${project.version} + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-autoconfigure-processor + true + + + + + org.projectlombok + lombok + provided + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.testcontainers + testcontainers + test + + + + org.testcontainers + junit-jupiter + test + + + + + + + Apache-2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + repo + A business-friendly OSS license + + + + \ No newline at end of file diff --git a/langchain4j-easy-rag-spring-boot-starter/src/main/java/dev/langchain4j/rag/easy/spring/DocumentsProperties.java b/langchain4j-easy-rag-spring-boot-starter/src/main/java/dev/langchain4j/rag/easy/spring/DocumentsProperties.java new file mode 100644 index 00000000..0269d1e5 --- /dev/null +++ b/langchain4j-easy-rag-spring-boot-starter/src/main/java/dev/langchain4j/rag/easy/spring/DocumentsProperties.java @@ -0,0 +1,15 @@ +package dev.langchain4j.rag.easy.spring; + +import lombok.Getter; +import lombok.Setter; + +import java.nio.file.Path; + +@Getter +@Setter +class DocumentsProperties { + + Path path; + String glob; + Boolean recursion; +} diff --git a/langchain4j-easy-rag-spring-boot-starter/src/main/java/dev/langchain4j/rag/easy/spring/EasyRagAutoConfig.java b/langchain4j-easy-rag-spring-boot-starter/src/main/java/dev/langchain4j/rag/easy/spring/EasyRagAutoConfig.java new file mode 100644 index 00000000..132e6a0a --- /dev/null +++ b/langchain4j-easy-rag-spring-boot-starter/src/main/java/dev/langchain4j/rag/easy/spring/EasyRagAutoConfig.java @@ -0,0 +1,94 @@ +package dev.langchain4j.rag.easy.spring; + +import dev.langchain4j.data.document.Document; +import dev.langchain4j.data.document.DocumentSplitter; +import dev.langchain4j.data.document.loader.FileSystemDocumentLoader; +import dev.langchain4j.data.document.splitter.DocumentSplitters; +import dev.langchain4j.data.segment.TextSegment; +import dev.langchain4j.model.Tokenizer; +import dev.langchain4j.model.embedding.EmbeddingModel; +import dev.langchain4j.model.embedding.HuggingFaceTokenizer; +import dev.langchain4j.model.embedding.bge.small.en.v15.BgeSmallEnV15QuantizedEmbeddingModel; +import dev.langchain4j.store.embedding.EmbeddingStore; +import dev.langchain4j.store.embedding.EmbeddingStoreIngestor; +import org.apache.commons.lang3.NotImplementedException; +import org.springframework.boot.ApplicationRunner; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; + +import java.util.List; + +import static dev.langchain4j.internal.Utils.isNotNullOrBlank; +import static dev.langchain4j.rag.easy.spring.EasyRagProperties.PREFIX; +import static java.util.Collections.singletonList; + +@AutoConfiguration +@EnableConfigurationProperties(EasyRagProperties.class) +public class EasyRagAutoConfig { + + @Bean + @ConditionalOnMissingBean + @ConditionalOnClass(HuggingFaceTokenizer.class) + Tokenizer tokenizer() { // TODO bean name, type + return new HuggingFaceTokenizer(); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnClass(DocumentSplitters.class) + DocumentSplitter documentSplitter(Tokenizer tokenizer) { // TODO bean name, type + return DocumentSplitters.recursive(300, 30, tokenizer); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnClass(BgeSmallEnV15QuantizedEmbeddingModel.class) + EmbeddingModel embeddingModel() { // TODO bean name, type + return new BgeSmallEnV15QuantizedEmbeddingModel(); + } + + @Bean + @ConditionalOnProperty(PREFIX + ".ingestion.documents.path") + ApplicationRunner easyRagDocumentIngestor(DocumentSplitter documentSplitter, // TODO should splitter be optional? + EmbeddingModel embeddingModel, + EmbeddingStore embeddingStore, + EasyRagProperties easyRagProperties) { // TODO bean name, type + return args -> { + + List documents = loadDocuments(easyRagProperties.ingestion.documents); + + EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder() + // TODO documentTransformer + .documentSplitter(documentSplitter) + // TODO textSegmentTransformer + .embeddingStore(embeddingStore) + .embeddingModel(embeddingModel) + .build(); + + ingestor.ingest(documents); + }; + } + + private static List loadDocuments(DocumentsProperties documentsProperties) { + if (documentsProperties.recursion != null && documentsProperties.recursion) { + throw new NotImplementedException(); // TODO + } else { + if (isNotNullOrBlank(documentsProperties.glob)) { + throw new NotImplementedException(); // TODO + } else { + if (documentsProperties.path.toFile().isDirectory()) { + return FileSystemDocumentLoader.loadDocuments(documentsProperties.path); + } else { + Document document = FileSystemDocumentLoader.loadDocument(documentsProperties.path); + return singletonList(document); + } + } + } + } + + // TODO ITs +} diff --git a/langchain4j-easy-rag-spring-boot-starter/src/main/java/dev/langchain4j/rag/easy/spring/EasyRagProperties.java b/langchain4j-easy-rag-spring-boot-starter/src/main/java/dev/langchain4j/rag/easy/spring/EasyRagProperties.java new file mode 100644 index 00000000..6fa002c7 --- /dev/null +++ b/langchain4j-easy-rag-spring-boot-starter/src/main/java/dev/langchain4j/rag/easy/spring/EasyRagProperties.java @@ -0,0 +1,19 @@ +package dev.langchain4j.rag.easy.spring; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; + +import static dev.langchain4j.rag.easy.spring.EasyRagProperties.PREFIX; + +@Getter +@Setter +@ConfigurationProperties(prefix = PREFIX) +public class EasyRagProperties { + + static final String PREFIX = "langchain4j.easy-rag"; + + @NestedConfigurationProperty + IngestionProperties ingestion; +} diff --git a/langchain4j-easy-rag-spring-boot-starter/src/main/java/dev/langchain4j/rag/easy/spring/IngestionProperties.java b/langchain4j-easy-rag-spring-boot-starter/src/main/java/dev/langchain4j/rag/easy/spring/IngestionProperties.java new file mode 100644 index 00000000..429d7823 --- /dev/null +++ b/langchain4j-easy-rag-spring-boot-starter/src/main/java/dev/langchain4j/rag/easy/spring/IngestionProperties.java @@ -0,0 +1,13 @@ +package dev.langchain4j.rag.easy.spring; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.NestedConfigurationProperty; + +@Getter +@Setter +class IngestionProperties { + + @NestedConfigurationProperty + DocumentsProperties documents; +} diff --git a/langchain4j-easy-rag-spring-boot-starter/src/main/resources/META-INF/spring.factories b/langchain4j-easy-rag-spring-boot-starter/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..4ded5330 --- /dev/null +++ b/langchain4j-easy-rag-spring-boot-starter/src/main/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=dev.langchain4j.rag.easy.spring.EasyRagAutoConfig \ No newline at end of file diff --git a/langchain4j-easy-rag-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/langchain4j-easy-rag-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..5d56a17f --- /dev/null +++ b/langchain4j-easy-rag-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +dev.langchain4j.rag.easy.spring.EasyRagAutoConfig \ No newline at end of file diff --git a/langchain4j-open-ai-spring-boot-starter/src/main/java/dev/langchain4j/openai/spring/AutoConfig.java b/langchain4j-open-ai-spring-boot-starter/src/main/java/dev/langchain4j/openai/spring/AutoConfig.java index 8e47ecba..bfd4b1cc 100644 --- a/langchain4j-open-ai-spring-boot-starter/src/main/java/dev/langchain4j/openai/spring/AutoConfig.java +++ b/langchain4j-open-ai-spring-boot-starter/src/main/java/dev/langchain4j/openai/spring/AutoConfig.java @@ -2,6 +2,7 @@ import dev.langchain4j.model.openai.*; import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; @@ -159,4 +160,10 @@ OpenAiImageModel openAiImageModel(Properties properties) { .persistTo(imageModelProperties.getPersistTo()) .build(); } + + @Bean + @ConditionalOnMissingBean + OpenAiTokenizer openAiTokenizer() { + return new OpenAiTokenizer(); + } } \ No newline at end of file diff --git a/langchain4j-spring-boot-starter/pom.xml b/langchain4j-spring-boot-starter/pom.xml new file mode 100644 index 00000000..a8b89b96 --- /dev/null +++ b/langchain4j-spring-boot-starter/pom.xml @@ -0,0 +1,102 @@ + + + 4.0.0 + + + dev.langchain4j + langchain4j-spring + 0.29.0-SNAPSHOT + ../pom.xml + + + langchain4j-spring-boot-starter + Spring Boot starter for LangChain4j + + + + + dev.langchain4j + langchain4j + ${project.version} + + + + org.reflections + reflections + 0.10.2 + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-autoconfigure-processor + true + + + + + org.projectlombok + lombok + provided + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + + dev.langchain4j + langchain4j-open-ai-spring-boot-starter + ${project.version} + test + + + + dev.langchain4j + langchain4j-core + ${project.version} + tests + test-jar + test + + + + org.tinylog + tinylog-impl + 2.6.2 + test + + + org.tinylog + slf4j-tinylog + 2.6.2 + test + + + + + + + Apache-2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + repo + A business-friendly OSS license + + + + \ No newline at end of file diff --git a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/rag/spring/RagAutoConfig.java b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/rag/spring/RagAutoConfig.java new file mode 100644 index 00000000..67eb1e3b --- /dev/null +++ b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/rag/spring/RagAutoConfig.java @@ -0,0 +1,52 @@ +package dev.langchain4j.rag.spring; + +import dev.langchain4j.data.segment.TextSegment; +import dev.langchain4j.model.embedding.EmbeddingModel; +import dev.langchain4j.rag.content.retriever.ContentRetriever; +import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever; +import dev.langchain4j.store.embedding.EmbeddingStore; +import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; + +@EnableConfigurationProperties(RagProperties.class) +public class RagAutoConfig { + + // TODO make these beans lazy? + + @Bean + @ConditionalOnMissingBean + EmbeddingStore embeddingStore() { // TODO bean name, type + return new InMemoryEmbeddingStore<>(); + } + + @Bean + @ConditionalOnBean({ + EmbeddingModel.class, + EmbeddingStore.class + }) + @ConditionalOnMissingBean + ContentRetriever contentRetriever(EmbeddingModel embeddingModel, + EmbeddingStore embeddingStore, + RagProperties ragProperties) { // TODO bean name, type + + EmbeddingStoreContentRetriever.EmbeddingStoreContentRetrieverBuilder builder = EmbeddingStoreContentRetriever.builder() + .embeddingStore(embeddingStore) + .embeddingModel(embeddingModel); + + if (ragProperties != null) { + RetrievalProperties retrievalProperties = ragProperties.getRetrieval(); + if (retrievalProperties != null) { + builder + .maxResults(retrievalProperties.maxResults) + .minScore(retrievalProperties.minScore); + } + } + + return builder.build(); + } + + // TODO test +} diff --git a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/rag/spring/RagProperties.java b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/rag/spring/RagProperties.java new file mode 100644 index 00000000..dd47aaba --- /dev/null +++ b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/rag/spring/RagProperties.java @@ -0,0 +1,17 @@ +package dev.langchain4j.rag.spring; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; + +@Getter +@Setter +@ConfigurationProperties(prefix = RagProperties.PREFIX) +public class RagProperties { + + static final String PREFIX = "langchain4j.rag"; + + @NestedConfigurationProperty + RetrievalProperties retrieval; +} diff --git a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/rag/spring/RetrievalProperties.java b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/rag/spring/RetrievalProperties.java new file mode 100644 index 00000000..e8995ced --- /dev/null +++ b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/rag/spring/RetrievalProperties.java @@ -0,0 +1,12 @@ +package dev.langchain4j.rag.spring; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +class RetrievalProperties { + + Integer maxResults; + Double minScore; +} diff --git a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiService.java b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiService.java new file mode 100644 index 00000000..5a211085 --- /dev/null +++ b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiService.java @@ -0,0 +1,99 @@ +package dev.langchain4j.service.spring; + +import dev.langchain4j.agent.tool.Tool; +import dev.langchain4j.memory.ChatMemory; +import dev.langchain4j.memory.chat.ChatMemoryProvider; +import dev.langchain4j.model.chat.ChatLanguageModel; +import dev.langchain4j.model.chat.StreamingChatLanguageModel; +import dev.langchain4j.rag.RetrievalAugmentor; +import dev.langchain4j.rag.content.retriever.ContentRetriever; +import dev.langchain4j.service.AiServices; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static dev.langchain4j.service.spring.AiServiceWiringMode.AUTOMATIC; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * 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}
+ * - {@link ChatMemory}
+ * - {@link ChatMemoryProvider}
+ * - {@link ContentRetriever}
+ * - {@link RetrievalAugmentor}
+ * - All beans containing methods annotated with {@code @}{@link Tool}
+ * 
+ * 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()}
+ * - {@link #chatMemory()}
+ * - {@link #chatMemoryProvider()}
+ * - {@link #contentRetriever()}
+ * - {@link #retrievalAugmentor()}
+ * 
+ * See more information about AI Services here + * and in the Javadoc of {@link AiServices}. + * + * @see AiServices + */ +@Target(TYPE) +@Retention(RUNTIME) +public @interface AiService { + + /** + * 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 ""; + + /** + * 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 ""; + + /** + * 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 ""; + + /** + * 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 ""; + + /** + * 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 ""; + + /** + * 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 ""; + + /** + * 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 {}; + + // TODO support Flux return type for AI Service method(s) (for streaming) +} \ No newline at end of file diff --git a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServiceFactory.java b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServiceFactory.java new file mode 100644 index 00000000..7ce673a0 --- /dev/null +++ b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServiceFactory.java @@ -0,0 +1,115 @@ +package dev.langchain4j.service.spring; + +import dev.langchain4j.memory.ChatMemory; +import dev.langchain4j.memory.chat.ChatMemoryProvider; +import dev.langchain4j.model.chat.ChatLanguageModel; +import dev.langchain4j.model.chat.StreamingChatLanguageModel; +import dev.langchain4j.rag.RetrievalAugmentor; +import dev.langchain4j.rag.content.retriever.ContentRetriever; +import dev.langchain4j.service.AiServices; +import org.springframework.beans.factory.FactoryBean; + +import java.util.List; + +import static dev.langchain4j.internal.Utils.isNullOrEmpty; + +class AiServiceFactory implements FactoryBean { + + private final Class aiServiceClass; + private ChatLanguageModel chatLanguageModel; + private StreamingChatLanguageModel streamingChatLanguageModel; + private ChatMemory chatMemory; + private ChatMemoryProvider chatMemoryProvider; + private ContentRetriever contentRetriever; + private RetrievalAugmentor retrievalAugmentor; + private List tools; + + public AiServiceFactory(Class aiServiceClass) { + this.aiServiceClass = aiServiceClass; + } + + public void setChatLanguageModel(ChatLanguageModel chatLanguageModel) { + this.chatLanguageModel = chatLanguageModel; + } + + public void setStreamingChatLanguageModel(StreamingChatLanguageModel streamingChatLanguageModel) { + this.streamingChatLanguageModel = streamingChatLanguageModel; + } + + public void setChatMemory(ChatMemory chatMemory) { + this.chatMemory = chatMemory; + } + + public void setChatMemoryProvider(ChatMemoryProvider chatMemoryProvider) { + this.chatMemoryProvider = chatMemoryProvider; + } + + public void setContentRetriever(ContentRetriever contentRetriever) { + this.contentRetriever = contentRetriever; + } + + public void setRetrievalAugmentor(RetrievalAugmentor retrievalAugmentor) { + this.retrievalAugmentor = retrievalAugmentor; + } + + public void setTools(List tools) { + this.tools = tools; + } + + @Override + public Object getObject() { + + AiServices builder = AiServices.builder(aiServiceClass); + + if (chatLanguageModel != null) { + builder = builder.chatLanguageModel(chatLanguageModel); + } + + if (streamingChatLanguageModel != null) { + builder = builder.streamingChatLanguageModel(streamingChatLanguageModel); + } + + if (chatMemory != null) { + builder.chatMemory(chatMemory); + } + + if (chatMemoryProvider != null) { + builder.chatMemoryProvider(chatMemoryProvider); + } + + if (contentRetriever != null) { + builder = builder.contentRetriever(contentRetriever); + } + + if (retrievalAugmentor != null) { + builder = builder.retrievalAugmentor(retrievalAugmentor); + } + + if (!isNullOrEmpty(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