From 7fe0094d6e09af48331704d67410cfa674867c31 Mon Sep 17 00:00:00 2001 From: catofdestruction Date: Thu, 21 Nov 2024 19:32:27 +0800 Subject: [PATCH 01/11] [FEATURE] tools enhanced by AOP support https://github.com/langchain4j/langchain4j/issues/2113 --- langchain4j-spring-boot-starter/pom.xml | 7 +++ .../service/spring/AiServiceFactory.java | 31 ++++++++++- .../service/spring/AiServicesAutoConfig.java | 7 ++- .../withTools/AiServicesAutoConfigIT.java | 32 ++++++++++++ .../automatic/withTools/AopEnhancedTools.java | 20 ++++++++ .../withTools/aop/CustomAnnotation.java | 18 +++++++ .../withTools/aop/CustomAnnotationAspect.java | 51 +++++++++++++++++++ 7 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/AopEnhancedTools.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/aop/CustomAnnotation.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/aop/CustomAnnotationAspect.java diff --git a/langchain4j-spring-boot-starter/pom.xml b/langchain4j-spring-boot-starter/pom.xml index 30440842..7c721efa 100644 --- a/langchain4j-spring-boot-starter/pom.xml +++ b/langchain4j-spring-boot-starter/pom.xml @@ -53,6 +53,13 @@ test + + org.springframework.boot + spring-boot-starter-aop + ${spring.boot.version} + test + + dev.langchain4j langchain4j-core diff --git a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServiceFactory.java b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServiceFactory.java index e2a2bdfc..6546a197 100644 --- a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServiceFactory.java +++ b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServiceFactory.java @@ -1,5 +1,7 @@ package dev.langchain4j.service.spring; +import dev.langchain4j.agent.tool.Tool; +import dev.langchain4j.agent.tool.ToolSpecification; import dev.langchain4j.memory.ChatMemory; import dev.langchain4j.memory.chat.ChatMemoryProvider; import dev.langchain4j.model.chat.ChatLanguageModel; @@ -8,10 +10,19 @@ import dev.langchain4j.rag.RetrievalAugmentor; import dev.langchain4j.rag.content.retriever.ContentRetriever; import dev.langchain4j.service.AiServices; +import dev.langchain4j.service.tool.DefaultToolExecutor; +import dev.langchain4j.service.tool.ToolExecutor; +import org.springframework.aop.framework.AopProxyUtils; +import org.springframework.aop.support.AopUtils; import org.springframework.beans.factory.FactoryBean; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import static dev.langchain4j.agent.tool.ToolSpecifications.toolSpecificationFrom; import static dev.langchain4j.internal.Utils.isNullOrEmpty; class AiServiceFactory implements FactoryBean { @@ -95,8 +106,9 @@ public Object getObject() { if (!isNullOrEmpty(tools)) { builder = builder.tools(tools); + builder = builder.tools(aopEnhancedTools()); } - + return builder.build(); } @@ -120,4 +132,21 @@ public boolean isSingleton() { * (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. */ + + private Map aopEnhancedTools() { + Map toolExecutors = new HashMap<>(); + tools.stream().filter(AopUtils::isAopProxy).forEach(enhancedTool -> { + Class originalToolClass = AopProxyUtils.ultimateTargetClass(enhancedTool); + for (Method method : originalToolClass.getDeclaredMethods()) { + if (method.isAnnotationPresent(Tool.class)) { + Arrays.stream(enhancedTool.getClass().getDeclaredMethods()) + .filter(m -> m.getName().equals(method.getName())) + .findFirst() + .ifPresent(enhancedMethod -> toolExecutors.put(toolSpecificationFrom(method), + new DefaultToolExecutor(enhancedTool, enhancedMethod))); + } + } + }); + return toolExecutors; + } } diff --git a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java index 90567db3..d1fd66d4 100644 --- a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java +++ b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java @@ -49,7 +49,12 @@ BeanFactoryPostProcessor aiServicesRegisteringBeanFactoryPostProcessor() { Set tools = new HashSet<>(); for (String beanName : beanFactory.getBeanDefinitionNames()) { try { - Class beanClass = Class.forName(beanFactory.getBeanDefinition(beanName).getBeanClassName()); + String beanClassName = beanFactory.getBeanDefinition(beanName).getBeanClassName(); + if (beanClassName == null) { + // skip if class name of bean is null + continue; + } + Class beanClass = Class.forName(beanClassName); for (Method beanMethod : beanClass.getDeclaredMethods()) { if (beanMethod.isAnnotationPresent(Tool.class)) { tools.add(beanName); 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 index 3cd8e1db..404ea949 100644 --- 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 @@ -1,14 +1,18 @@ package dev.langchain4j.service.spring.mode.automatic.withTools; import dev.langchain4j.service.spring.AiServicesAutoConfig; +import dev.langchain4j.service.spring.mode.automatic.withTools.aop.CustomAnnotationAspect; 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 dev.langchain4j.service.spring.mode.automatic.withTools.AopEnhancedTools.ASPECT_PACKAGE; +import static dev.langchain4j.service.spring.mode.automatic.withTools.AopEnhancedTools.TOOL_DESCRIPTION; import static dev.langchain4j.service.spring.mode.automatic.withTools.PackagePrivateTools.CURRENT_TIME; import static dev.langchain4j.service.spring.mode.automatic.withTools.PublicTools.CURRENT_DATE; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; class AiServicesAutoConfigIT { @@ -61,6 +65,34 @@ void should_create_AI_service_with_tool_that_is_package_private_method_in_packag }); } + @Test + void should_create_AI_service_with_tool_which_is_enhanced_by_spring_aop() { + contextRunner + .withPropertyValues( + "langchain4j.open-ai.chat-model.api-key=" + OPENAI_API_KEY, + "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("In Spring Boot, which package is the @Aspect annotation located in?"); + + // then should use AopEnhancedTools.getAspectPackage() + assertThat(answer).contains(ASPECT_PACKAGE); + + // and AOP aspect should be enabled + CustomAnnotationAspect aspect = context.getBean(CustomAnnotationAspect.class); + assertTrue(aspect.isAspectEnabled()); + assertTrue(aspect.getToolsDescription().contains(TOOL_DESCRIPTION)); + }); + } + // TODO tools which are not @Beans? // TODO negative cases // TODO no @AiServices in app, just models diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/AopEnhancedTools.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/AopEnhancedTools.java new file mode 100644 index 00000000..5d4152d2 --- /dev/null +++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/AopEnhancedTools.java @@ -0,0 +1,20 @@ +package dev.langchain4j.service.spring.mode.automatic.withTools; + +import dev.langchain4j.agent.tool.Tool; +import dev.langchain4j.service.spring.mode.automatic.withTools.aop.CustomAnnotation; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.stereotype.Component; + +@Component +public class AopEnhancedTools { + + public static final String ASPECT_PACKAGE = Aspect.class.getPackageName(); + + public static final String TOOL_DESCRIPTION = "Find the package directory where @Aspect is located."; + + @Tool(TOOL_DESCRIPTION) + @CustomAnnotation(customKey = "lock_key_1121") + public String getAspectPackage() { + return ASPECT_PACKAGE; + } +} diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/aop/CustomAnnotation.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/aop/CustomAnnotation.java new file mode 100644 index 00000000..791bdd05 --- /dev/null +++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/aop/CustomAnnotation.java @@ -0,0 +1,18 @@ +package dev.langchain4j.service.spring.mode.automatic.withTools.aop; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface CustomAnnotation { + + /** + * key just for example + * + * @return the key + */ + String customKey(); +} diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/aop/CustomAnnotationAspect.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/aop/CustomAnnotationAspect.java new file mode 100644 index 00000000..5e6215bc --- /dev/null +++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/aop/CustomAnnotationAspect.java @@ -0,0 +1,51 @@ +package dev.langchain4j.service.spring.mode.automatic.withTools.aop; + +import dev.langchain4j.agent.tool.Tool; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@Aspect +@Component +public class CustomAnnotationAspect { + + private boolean aspectEnabled = false; + + private final List toolsDescription = new ArrayList<>(); + + @Around("@annotation(customAnnotation)") + public Object around(ProceedingJoinPoint joinPoint, CustomAnnotation customAnnotation) throws Throwable { + aspectEnabled = true; + var signature = (MethodSignature) joinPoint.getSignature(); + var method = signature.getMethod(); + if (method.isAnnotationPresent(Tool.class)) { + Tool toolAnnotation = method.getAnnotation(Tool.class); + toolsDescription.addAll(Arrays.asList(toolAnnotation.value())); + System.out.printf("Found @Tool %s for method: %s\n", Arrays.toString(toolAnnotation.value()), method); + System.out.println(); + } + String customKey = customAnnotation.customKey(); + Object result = joinPoint.proceed(); + System.out.printf("Custom key: %s | Method name: %s | Method arguments: %s | Return type: %s | Method return value: %s%n", + customKey, + method.getName(), + Arrays.toString(joinPoint.getArgs()), + method.getReturnType().getName(), + result); + return result; + } + + public boolean isAspectEnabled() { + return aspectEnabled; + } + + public List getToolsDescription() { + return toolsDescription; + } +} From 0a9b02265a88d9a474b4075bffb148e968d182cf Mon Sep 17 00:00:00 2001 From: catofdestruction Date: Fri, 22 Nov 2024 10:39:28 +0800 Subject: [PATCH 02/11] [FEATURE] tools enhanced by AOP support https://github.com/langchain4j/langchain4j/issues/2113 --- .../service/spring/AiServiceFactory.java | 39 +++++++------- .../withTools/AiServicesAutoConfigIT.java | 33 ++++++++---- .../automatic/withTools/AopEnhancedTools.java | 24 ++++++--- .../withTools/aop/CustomAnnotationAspect.java | 51 ------------------- ...ustomAnnotation.java => ToolObserver.java} | 4 +- .../withTools/aop/ToolObserverAspect.java | 47 +++++++++++++++++ 6 files changed, 110 insertions(+), 88 deletions(-) delete mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/aop/CustomAnnotationAspect.java rename langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/aop/{CustomAnnotation.java => ToolObserver.java} (86%) create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/aop/ToolObserverAspect.java diff --git a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServiceFactory.java b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServiceFactory.java index 6546a197..d5063192 100644 --- a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServiceFactory.java +++ b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServiceFactory.java @@ -12,8 +12,6 @@ import dev.langchain4j.service.AiServices; import dev.langchain4j.service.tool.DefaultToolExecutor; import dev.langchain4j.service.tool.ToolExecutor; -import org.springframework.aop.framework.AopProxyUtils; -import org.springframework.aop.support.AopUtils; import org.springframework.beans.factory.FactoryBean; import java.lang.reflect.Method; @@ -24,6 +22,8 @@ import static dev.langchain4j.agent.tool.ToolSpecifications.toolSpecificationFrom; import static dev.langchain4j.internal.Utils.isNullOrEmpty; +import static org.springframework.aop.framework.AopProxyUtils.ultimateTargetClass; +import static org.springframework.aop.support.AopUtils.isAopProxy; class AiServiceFactory implements FactoryBean { @@ -105,10 +105,15 @@ public Object getObject() { } if (!isNullOrEmpty(tools)) { - builder = builder.tools(tools); - builder = builder.tools(aopEnhancedTools()); + for (Object tool : tools) { + if (isAopProxy(tool)) { + builder = builder.tools(aopEnhancedTools(tool)); + } else { + builder = builder.tools(tool); + } + } } - + return builder.build(); } @@ -133,20 +138,20 @@ public boolean isSingleton() { * Instead, a FactoryBean should implement DisposableBean and delegate any such close call to the underlying object. */ - private Map aopEnhancedTools() { + private Map aopEnhancedTools(Object enhancedTool) { Map toolExecutors = new HashMap<>(); - tools.stream().filter(AopUtils::isAopProxy).forEach(enhancedTool -> { - Class originalToolClass = AopProxyUtils.ultimateTargetClass(enhancedTool); - for (Method method : originalToolClass.getDeclaredMethods()) { - if (method.isAnnotationPresent(Tool.class)) { - Arrays.stream(enhancedTool.getClass().getDeclaredMethods()) - .filter(m -> m.getName().equals(method.getName())) - .findFirst() - .ifPresent(enhancedMethod -> toolExecutors.put(toolSpecificationFrom(method), - new DefaultToolExecutor(enhancedTool, enhancedMethod))); - } + Class originalToolClass = ultimateTargetClass(enhancedTool); + for (Method originalToolMethod : originalToolClass.getDeclaredMethods()) { + if (originalToolMethod.isAnnotationPresent(Tool.class)) { + Arrays.stream(enhancedTool.getClass().getDeclaredMethods()) + .filter(m -> m.getName().equals(originalToolMethod.getName())) + .findFirst() + .ifPresent(enhancedMethod -> { + ToolSpecification toolSpecification = toolSpecificationFrom(originalToolMethod); + toolExecutors.put(toolSpecification, new DefaultToolExecutor(enhancedTool, enhancedMethod)); + }); } - }); + } return toolExecutors; } } 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 index 404ea949..8a31fa4b 100644 --- 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 @@ -1,17 +1,21 @@ package dev.langchain4j.service.spring.mode.automatic.withTools; import dev.langchain4j.service.spring.AiServicesAutoConfig; -import dev.langchain4j.service.spring.mode.automatic.withTools.aop.CustomAnnotationAspect; +import dev.langchain4j.service.spring.mode.automatic.withTools.aop.ToolObserverAspect; 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 dev.langchain4j.service.spring.mode.automatic.withTools.AopEnhancedTools.ASPECT_PACKAGE; -import static dev.langchain4j.service.spring.mode.automatic.withTools.AopEnhancedTools.TOOL_DESCRIPTION; +import static dev.langchain4j.service.spring.mode.automatic.withTools.AopEnhancedTools.TOOL_OBSERVER_KEY; +import static dev.langchain4j.service.spring.mode.automatic.withTools.AopEnhancedTools.TOOL_OBSERVER_KEY_NAME_DESCRIPTION; +import static dev.langchain4j.service.spring.mode.automatic.withTools.AopEnhancedTools.TOOL_OBSERVER_PACKAGE_NAME; +import static dev.langchain4j.service.spring.mode.automatic.withTools.AopEnhancedTools.TOOL_OBSERVER_PACKAGE_NAME_DESCRIPTION; import static dev.langchain4j.service.spring.mode.automatic.withTools.PackagePrivateTools.CURRENT_TIME; import static dev.langchain4j.service.spring.mode.automatic.withTools.PublicTools.CURRENT_DATE; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; class AiServicesAutoConfigIT { @@ -81,15 +85,24 @@ void should_create_AI_service_with_tool_which_is_enhanced_by_spring_aop() { AiServiceWithTools aiService = context.getBean(AiServiceWithTools.class); // when - String answer = aiService.chat("In Spring Boot, which package is the @Aspect annotation located in?"); + String answer = aiService.chat("Which package is the @ToolObserver annotation located in? " + + "And what is the key of the @ToolObserver annotation?"); - // then should use AopEnhancedTools.getAspectPackage() - assertThat(answer).contains(ASPECT_PACKAGE); + System.out.println("Answer: " + answer); - // and AOP aspect should be enabled - CustomAnnotationAspect aspect = context.getBean(CustomAnnotationAspect.class); - assertTrue(aspect.isAspectEnabled()); - assertTrue(aspect.getToolsDescription().contains(TOOL_DESCRIPTION)); + // then should use AopEnhancedTools.getAspectPackage() + // & AopEnhancedTools.getToolObserverKey() + assertThat(answer).contains(TOOL_OBSERVER_PACKAGE_NAME); + assertThat(answer).contains(TOOL_OBSERVER_KEY); + + // and AOP aspect should be called + // & only for getToolObserverKey() which is annotated with @ToolObserver + ToolObserverAspect aspect = context.getBean(ToolObserverAspect.class); + assertTrue(aspect.aspectHasBeenCalled()); + + assertEquals(1, aspect.getObservedTools().size()); + assertTrue(aspect.getObservedTools().contains(TOOL_OBSERVER_KEY_NAME_DESCRIPTION)); + assertFalse(aspect.getObservedTools().contains(TOOL_OBSERVER_PACKAGE_NAME_DESCRIPTION)); }); } diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/AopEnhancedTools.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/AopEnhancedTools.java index 5d4152d2..4db600d7 100644 --- a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/AopEnhancedTools.java +++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/AopEnhancedTools.java @@ -1,20 +1,28 @@ package dev.langchain4j.service.spring.mode.automatic.withTools; import dev.langchain4j.agent.tool.Tool; -import dev.langchain4j.service.spring.mode.automatic.withTools.aop.CustomAnnotation; -import org.aspectj.lang.annotation.Aspect; +import dev.langchain4j.service.spring.mode.automatic.withTools.aop.ToolObserver; import org.springframework.stereotype.Component; @Component public class AopEnhancedTools { - public static final String ASPECT_PACKAGE = Aspect.class.getPackageName(); + public static final String TOOL_OBSERVER_PACKAGE_NAME_DESCRIPTION = + "Find the package directory where @ToolObserver is located."; + public static final String TOOL_OBSERVER_PACKAGE_NAME = ToolObserver.class.getPackageName(); - public static final String TOOL_DESCRIPTION = "Find the package directory where @Aspect is located."; + public static final String TOOL_OBSERVER_KEY_NAME_DESCRIPTION = + "Find the key name of @ToolObserver"; + public static final String TOOL_OBSERVER_KEY = "AOP_ENHANCED_TOOLS_SUPPORT_@_1122"; - @Tool(TOOL_DESCRIPTION) - @CustomAnnotation(customKey = "lock_key_1121") - public String getAspectPackage() { - return ASPECT_PACKAGE; + @Tool(TOOL_OBSERVER_PACKAGE_NAME_DESCRIPTION) + public String getToolObserverPackageName() { + return TOOL_OBSERVER_PACKAGE_NAME; + } + + @ToolObserver(key = TOOL_OBSERVER_KEY) + @Tool(TOOL_OBSERVER_KEY_NAME_DESCRIPTION) + public String getToolObserverKey() { + return TOOL_OBSERVER_KEY; } } diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/aop/CustomAnnotationAspect.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/aop/CustomAnnotationAspect.java deleted file mode 100644 index 5e6215bc..00000000 --- a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/aop/CustomAnnotationAspect.java +++ /dev/null @@ -1,51 +0,0 @@ -package dev.langchain4j.service.spring.mode.automatic.withTools.aop; - -import dev.langchain4j.agent.tool.Tool; -import org.aspectj.lang.ProceedingJoinPoint; -import org.aspectj.lang.annotation.Around; -import org.aspectj.lang.annotation.Aspect; -import org.aspectj.lang.reflect.MethodSignature; -import org.springframework.stereotype.Component; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -@Aspect -@Component -public class CustomAnnotationAspect { - - private boolean aspectEnabled = false; - - private final List toolsDescription = new ArrayList<>(); - - @Around("@annotation(customAnnotation)") - public Object around(ProceedingJoinPoint joinPoint, CustomAnnotation customAnnotation) throws Throwable { - aspectEnabled = true; - var signature = (MethodSignature) joinPoint.getSignature(); - var method = signature.getMethod(); - if (method.isAnnotationPresent(Tool.class)) { - Tool toolAnnotation = method.getAnnotation(Tool.class); - toolsDescription.addAll(Arrays.asList(toolAnnotation.value())); - System.out.printf("Found @Tool %s for method: %s\n", Arrays.toString(toolAnnotation.value()), method); - System.out.println(); - } - String customKey = customAnnotation.customKey(); - Object result = joinPoint.proceed(); - System.out.printf("Custom key: %s | Method name: %s | Method arguments: %s | Return type: %s | Method return value: %s%n", - customKey, - method.getName(), - Arrays.toString(joinPoint.getArgs()), - method.getReturnType().getName(), - result); - return result; - } - - public boolean isAspectEnabled() { - return aspectEnabled; - } - - public List getToolsDescription() { - return toolsDescription; - } -} diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/aop/CustomAnnotation.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/aop/ToolObserver.java similarity index 86% rename from langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/aop/CustomAnnotation.java rename to langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/aop/ToolObserver.java index 791bdd05..0c95365f 100644 --- a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/aop/CustomAnnotation.java +++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/aop/ToolObserver.java @@ -7,12 +7,12 @@ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) -public @interface CustomAnnotation { +public @interface ToolObserver { /** * key just for example * * @return the key */ - String customKey(); + String key(); } diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/aop/ToolObserverAspect.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/aop/ToolObserverAspect.java new file mode 100644 index 00000000..68eb013d --- /dev/null +++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/aop/ToolObserverAspect.java @@ -0,0 +1,47 @@ +package dev.langchain4j.service.spring.mode.automatic.withTools.aop; + +import dev.langchain4j.agent.tool.Tool; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@Aspect +@Component +public class ToolObserverAspect { + + private final List observedTools = new ArrayList<>(); + + @Around("@annotation(toolObserver)") + public Object around(ProceedingJoinPoint joinPoint, ToolObserver toolObserver) throws Throwable { + var signature = (MethodSignature) joinPoint.getSignature(); + var method = signature.getMethod(); + String methodName = method.getName(); + if (method.isAnnotationPresent(Tool.class)) { + Tool toolAnnotation = method.getAnnotation(Tool.class); + observedTools.addAll(Arrays.asList(toolAnnotation.value())); + System.out.printf("Found @Tool %s for method: %s%n%n", Arrays.toString(toolAnnotation.value()), methodName); + } + Object result = joinPoint.proceed(); + System.out.printf(" | key: %s%n | Method name: %s%n | Method arguments: %s%n | Return type: %s%n | Method return value: %s%n%n", + toolObserver.key(), + methodName, + Arrays.toString(joinPoint.getArgs()), + method.getReturnType().getName(), + result); + return result; + } + + public boolean aspectHasBeenCalled() { + return !observedTools.isEmpty(); + } + + public List getObservedTools() { + return observedTools; + } +} From a8038607659218bc78d932fcb8565d82cc631c5e Mon Sep 17 00:00:00 2001 From: catofdestruction Date: Fri, 22 Nov 2024 10:48:40 +0800 Subject: [PATCH 03/11] [FEATURE] tools enhanced by AOP support https://github.com/langchain4j/langchain4j/issues/2113 --- .../mode/automatic/withTools/AiServicesAutoConfigIT.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 index 8a31fa4b..49bf50b5 100644 --- 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 @@ -86,14 +86,17 @@ void should_create_AI_service_with_tool_which_is_enhanced_by_spring_aop() { // when String answer = aiService.chat("Which package is the @ToolObserver annotation located in? " + - "And what is the key of the @ToolObserver annotation?"); + "And what is the key of the @ToolObserver annotation?" + + "And What is the current time?"); System.out.println("Answer: " + answer); // then should use AopEnhancedTools.getAspectPackage() // & AopEnhancedTools.getToolObserverKey() + // & PackagePrivateTools.getCurrentTime() assertThat(answer).contains(TOOL_OBSERVER_PACKAGE_NAME); assertThat(answer).contains(TOOL_OBSERVER_KEY); + assertThat(answer).contains(String.valueOf(CURRENT_TIME.getMinute())); // and AOP aspect should be called // & only for getToolObserverKey() which is annotated with @ToolObserver From 95848a2b7d334ea3d05a41ad441deeef209157ee Mon Sep 17 00:00:00 2001 From: LangChain4j Date: Fri, 22 Nov 2024 09:57:49 +0100 Subject: [PATCH 04/11] Update langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java --- .../dev/langchain4j/service/spring/AiServicesAutoConfig.java | 1 - 1 file changed, 1 deletion(-) diff --git a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java index d1fd66d4..69f6fddf 100644 --- a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java +++ b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java @@ -51,7 +51,6 @@ BeanFactoryPostProcessor aiServicesRegisteringBeanFactoryPostProcessor() { try { String beanClassName = beanFactory.getBeanDefinition(beanName).getBeanClassName(); if (beanClassName == null) { - // skip if class name of bean is null continue; } Class beanClass = Class.forName(beanClassName); From 4f5c67814aa63037e57c39ee489f4cb255829d7a Mon Sep 17 00:00:00 2001 From: catofdestruction Date: Tue, 26 Nov 2024 19:43:41 +0800 Subject: [PATCH 05/11] Publish a Spring Event after registering the AiService Bean in AiServicesAutoConfig. This event contains the AiService class and its corresponding tools description information. Once a user implements the AiServiceRegisteredEventListener to listen for this event, they can receive the event during the Spring Boot startup phase and handle their business logic as needed. original PR link: https://github.com/langchain4j/langchain4j-spring/pull/77 Issues link: langchain4j/langchain4j#2112 --- .../service/spring/AiServicesAutoConfig.java | 41 ++++++++++++++++++- .../event/AiServiceRegisteredEvent.java | 26 ++++++++++++ .../AiServiceWithToolsApplication.java | 16 +++++++- .../withTools/AiServicesAutoConfigIT.java | 32 +++++++++++++++ .../listener/AbstractApplicationListener.java | 24 +++++++++++ .../AiServiceRegisteredEventListener.java | 7 ++++ 6 files changed, 144 insertions(+), 2 deletions(-) create mode 100644 langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/event/AiServiceRegisteredEvent.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/listener/AbstractApplicationListener.java create mode 100644 langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/listener/AiServiceRegisteredEventListener.java diff --git a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java index d1fd66d4..c144f9c3 100644 --- a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java +++ b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java @@ -1,6 +1,8 @@ package dev.langchain4j.service.spring; import dev.langchain4j.agent.tool.Tool; +import dev.langchain4j.agent.tool.ToolSpecification; +import dev.langchain4j.agent.tool.ToolSpecifications; import dev.langchain4j.exception.IllegalConfigurationException; import dev.langchain4j.memory.ChatMemory; import dev.langchain4j.memory.chat.ChatMemoryProvider; @@ -9,19 +11,27 @@ import dev.langchain4j.model.moderation.ModerationModel; import dev.langchain4j.rag.RetrievalAugmentor; import dev.langchain4j.rag.content.retriever.ContentRetriever; +import dev.langchain4j.service.spring.event.AiServiceRegisteredEvent; import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.factory.config.BeanFactoryPostProcessor; 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.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.context.annotation.Bean; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.HashMap; import java.util.HashSet; +import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import static dev.langchain4j.exception.IllegalConfigurationException.illegalConfiguration; import static dev.langchain4j.internal.Exceptions.illegalArgument; @@ -31,7 +41,14 @@ import static dev.langchain4j.service.spring.AiServiceWiringMode.EXPLICIT; import static java.util.Arrays.asList; -public class AiServicesAutoConfig { +public class AiServicesAutoConfig implements ApplicationEventPublisherAware { + + private ApplicationEventPublisher eventPublisher; + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { + this.eventPublisher = applicationEventPublisher; + } @Bean BeanFactoryPostProcessor aiServicesRegisteringBeanFactoryPostProcessor() { @@ -47,6 +64,7 @@ BeanFactoryPostProcessor aiServicesRegisteringBeanFactoryPostProcessor() { String[] moderationModels = beanFactory.getBeanNamesForType(ModerationModel.class); Set tools = new HashSet<>(); + Map> beanToolSpecifications = new HashMap<>(); for (String beanName : beanFactory.getBeanDefinitionNames()) { try { String beanClassName = beanFactory.getBeanDefinition(beanName).getBeanClassName(); @@ -58,6 +76,10 @@ BeanFactoryPostProcessor aiServicesRegisteringBeanFactoryPostProcessor() { for (Method beanMethod : beanClass.getDeclaredMethods()) { if (beanMethod.isAnnotationPresent(Tool.class)) { tools.add(beanName); + List toolSpecifications = + beanToolSpecifications.getOrDefault(beanName, new ArrayList<>()); + toolSpecifications.add(ToolSpecifications.toolSpecificationFrom(beanMethod)); + beanToolSpecifications.put(beanName, toolSpecifications); } } } catch (Exception e) { @@ -146,10 +168,13 @@ BeanFactoryPostProcessor aiServicesRegisteringBeanFactoryPostProcessor() { propertyValues ); + AiServiceRegisteredEvent registeredEvent; if (aiServiceAnnotation.wiringMode() == EXPLICIT) { propertyValues.add("tools", toManagedList(asList(aiServiceAnnotation.tools()))); + registeredEvent = buildEvent(aiServiceClass, beanToolSpecifications, asList(aiServiceAnnotation.tools())); } else if (aiServiceAnnotation.wiringMode() == AUTOMATIC) { propertyValues.add("tools", toManagedList(tools)); + registeredEvent = buildEvent(aiServiceClass, beanToolSpecifications, tools); } else { throw illegalArgument("Unknown wiring mode: " + aiServiceAnnotation.wiringMode()); } @@ -157,6 +182,10 @@ BeanFactoryPostProcessor aiServicesRegisteringBeanFactoryPostProcessor() { BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory; registry.removeBeanDefinition(aiService); registry.registerBeanDefinition(lowercaseFirstLetter(aiService), aiServiceBeanDefinition); + + if (eventPublisher != null) { + eventPublisher.publishEvent(registeredEvent); + } } }; } @@ -204,4 +233,14 @@ private static ManagedList toManagedList(Collection aiServiceClass, + Map> toolSpecifications, + Collection tools) { + return new AiServiceRegisteredEvent(aiServiceClass, aiServiceClass, + tools.stream() + .filter(toolSpecifications::containsKey) + .flatMap(tool -> toolSpecifications.get(tool).stream()) + .collect(Collectors.toList())); + } } diff --git a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/event/AiServiceRegisteredEvent.java b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/event/AiServiceRegisteredEvent.java new file mode 100644 index 00000000..28a15ed6 --- /dev/null +++ b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/event/AiServiceRegisteredEvent.java @@ -0,0 +1,26 @@ +package dev.langchain4j.service.spring.event; + +import dev.langchain4j.agent.tool.ToolSpecification; +import org.springframework.context.ApplicationEvent; + +import java.util.List; + +public class AiServiceRegisteredEvent extends ApplicationEvent { + + private final Class aiServiceClass; + private final List toolSpecifications; + + public AiServiceRegisteredEvent(Object source, Class aiServiceClass, List toolSpecifications) { + super(source); + this.aiServiceClass = aiServiceClass; + this.toolSpecifications = toolSpecifications; + } + + public Class getAiServiceClass() { + return aiServiceClass; + } + + public List getToolSpecifications() { + return toolSpecifications; + } +} \ No newline at end of file diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/AiServiceWithToolsApplication.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/AiServiceWithToolsApplication.java index 1104d337..da612520 100644 --- 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 @@ -1,12 +1,26 @@ package dev.langchain4j.service.spring.mode.automatic.withTools; +import dev.langchain4j.agent.tool.ToolSpecification; +import dev.langchain4j.service.spring.event.AiServiceRegisteredEvent; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ApplicationListener; + +import java.util.List; @SpringBootApplication -class AiServiceWithToolsApplication { +class AiServiceWithToolsApplication implements ApplicationListener { public static void main(String[] args) { SpringApplication.run(AiServiceWithToolsApplication.class, args); } + + @Override + public void onApplicationEvent(AiServiceRegisteredEvent event) { + Class aiServiceClass = event.getAiServiceClass(); + List toolSpecifications = event.getToolSpecifications(); + for (int i = 0; i < toolSpecifications.size(); i++) { + System.out.printf("[%s]: [Tool-%s]: %s%n", aiServiceClass.getSimpleName(), i + 1, toolSpecifications.get(i)); + } + } } 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 index 49bf50b5..e16367c0 100644 --- 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 @@ -1,11 +1,16 @@ package dev.langchain4j.service.spring.mode.automatic.withTools; +import dev.langchain4j.agent.tool.ToolSpecification; import dev.langchain4j.service.spring.AiServicesAutoConfig; +import dev.langchain4j.service.spring.event.AiServiceRegisteredEvent; import dev.langchain4j.service.spring.mode.automatic.withTools.aop.ToolObserverAspect; +import dev.langchain4j.service.spring.mode.automatic.withTools.listener.AiServiceRegisteredEventListener; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import java.util.List; + import static dev.langchain4j.service.spring.mode.ApiKeys.OPENAI_API_KEY; import static dev.langchain4j.service.spring.mode.automatic.withTools.AopEnhancedTools.TOOL_OBSERVER_KEY; import static dev.langchain4j.service.spring.mode.automatic.withTools.AopEnhancedTools.TOOL_OBSERVER_KEY_NAME_DESCRIPTION; @@ -16,6 +21,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; class AiServicesAutoConfigIT { @@ -69,6 +75,32 @@ void should_create_AI_service_with_tool_that_is_package_private_method_in_packag }); } + @Test + void should_receive_ai_service_registered_event() { + contextRunner + .withUserConfiguration(AiServiceWithToolsApplication.class) + .run(context -> { + + // given + AiServiceRegisteredEventListener listener = context.getBean(AiServiceRegisteredEventListener.class); + + // then should receive AiServiceRegisteredEvent + assertTrue(listener.isEventReceived()); + assertEquals(1, listener.getReceivedEvents().size()); + + AiServiceRegisteredEvent event = listener.getReceivedEvents().stream().findFirst().orElse(null); + assertNotNull(event); + assertEquals(AiServiceWithTools.class, event.getAiServiceClass()); + assertEquals(4, event.getToolSpecifications().size()); + + List tools = event.getToolSpecifications().stream().map(ToolSpecification::name).toList(); + assertTrue(tools.contains("getCurrentDate")); + assertTrue(tools.contains("getCurrentTime")); + assertTrue(tools.contains("getToolObserverPackageName")); + assertTrue(tools.contains("getToolObserverKey")); + }); + } + @Test void should_create_AI_service_with_tool_which_is_enhanced_by_spring_aop() { contextRunner diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/listener/AbstractApplicationListener.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/listener/AbstractApplicationListener.java new file mode 100644 index 00000000..cb41209a --- /dev/null +++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/listener/AbstractApplicationListener.java @@ -0,0 +1,24 @@ +package dev.langchain4j.service.spring.mode.automatic.withTools.listener; + +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; + +import java.util.ArrayList; +import java.util.List; + +public class AbstractApplicationListener implements ApplicationListener { + private final List receivedEvents = new ArrayList<>(); + + @Override + public void onApplicationEvent(E event) { + receivedEvents.add(event); + } + + public List getReceivedEvents() { + return receivedEvents; + } + + public boolean isEventReceived() { + return !receivedEvents.isEmpty(); + } +} \ No newline at end of file diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/listener/AiServiceRegisteredEventListener.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/listener/AiServiceRegisteredEventListener.java new file mode 100644 index 00000000..f60b3d95 --- /dev/null +++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/listener/AiServiceRegisteredEventListener.java @@ -0,0 +1,7 @@ +package dev.langchain4j.service.spring.mode.automatic.withTools.listener; + +import dev.langchain4j.service.spring.event.AiServiceRegisteredEvent; +import org.springframework.stereotype.Component; +@Component +public class AiServiceRegisteredEventListener extends AbstractApplicationListener { +} From 92b30ec813674b8d906e44461a9c08da51516389 Mon Sep 17 00:00:00 2001 From: catofdestruction Date: Thu, 28 Nov 2024 11:29:07 +0800 Subject: [PATCH 06/11] minor fix original PR link: https://github.com/langchain4j/langchain4j-spring/pull/77 Issues link: langchain4j/langchain4j#2112 --- .../langchain4j/service/spring/AiServicesAutoConfig.java | 5 +++-- .../service/spring/event/AiServiceRegisteredEvent.java | 4 ++-- .../automatic/withTools/AiServiceWithToolsApplication.java | 4 ++-- .../mode/automatic/withTools/AiServicesAutoConfigIT.java | 6 +++--- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java index 9675931a..4f347c59 100644 --- a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java +++ b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java @@ -21,6 +21,7 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.context.annotation.Bean; +import org.springframework.lang.NonNull; import java.lang.reflect.Method; import java.util.ArrayList; @@ -46,7 +47,7 @@ public class AiServicesAutoConfig implements ApplicationEventPublisherAware { private ApplicationEventPublisher eventPublisher; @Override - public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { + public void setApplicationEventPublisher(@NonNull ApplicationEventPublisher applicationEventPublisher) { this.eventPublisher = applicationEventPublisher; } @@ -236,7 +237,7 @@ private static ManagedList toManagedList(Collection aiServiceClass, Map> toolSpecifications, Collection tools) { - return new AiServiceRegisteredEvent(aiServiceClass, aiServiceClass, + return new AiServiceRegisteredEvent(AiServicesAutoConfig.class, aiServiceClass, tools.stream() .filter(toolSpecifications::containsKey) .flatMap(tool -> toolSpecifications.get(tool).stream()) diff --git a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/event/AiServiceRegisteredEvent.java b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/event/AiServiceRegisteredEvent.java index 28a15ed6..4abb1e54 100644 --- a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/event/AiServiceRegisteredEvent.java +++ b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/event/AiServiceRegisteredEvent.java @@ -16,11 +16,11 @@ public AiServiceRegisteredEvent(Object source, Class aiServiceClass, List getAiServiceClass() { + public Class aiServiceClass() { return aiServiceClass; } - public List getToolSpecifications() { + public List toolSpecifications() { return toolSpecifications; } } \ No newline at end of file diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/AiServiceWithToolsApplication.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/AiServiceWithToolsApplication.java index da612520..94e82885 100644 --- 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 @@ -17,8 +17,8 @@ public static void main(String[] args) { @Override public void onApplicationEvent(AiServiceRegisteredEvent event) { - Class aiServiceClass = event.getAiServiceClass(); - List toolSpecifications = event.getToolSpecifications(); + Class aiServiceClass = event.aiServiceClass(); + List toolSpecifications = event.toolSpecifications(); for (int i = 0; i < toolSpecifications.size(); i++) { System.out.printf("[%s]: [Tool-%s]: %s%n", aiServiceClass.getSimpleName(), i + 1, toolSpecifications.get(i)); } 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 index e16367c0..3717cc10 100644 --- 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 @@ -90,10 +90,10 @@ void should_receive_ai_service_registered_event() { AiServiceRegisteredEvent event = listener.getReceivedEvents().stream().findFirst().orElse(null); assertNotNull(event); - assertEquals(AiServiceWithTools.class, event.getAiServiceClass()); - assertEquals(4, event.getToolSpecifications().size()); + assertEquals(AiServiceWithTools.class, event.aiServiceClass()); + assertEquals(4, event.toolSpecifications().size()); - List tools = event.getToolSpecifications().stream().map(ToolSpecification::name).toList(); + List tools = event.toolSpecifications().stream().map(ToolSpecification::name).toList(); assertTrue(tools.contains("getCurrentDate")); assertTrue(tools.contains("getCurrentTime")); assertTrue(tools.contains("getToolObserverPackageName")); From 38801559df236882e8343a2f9e84be271f8d31c2 Mon Sep 17 00:00:00 2001 From: catofdestruction Date: Mon, 9 Dec 2024 11:20:59 +0800 Subject: [PATCH 07/11] minor fix original PR link: https://github.com/langchain4j/langchain4j-spring/pull/77 Issues link: langchain4j/langchain4j#2112 --- .../service/spring/AiServicesAutoConfig.java | 37 +++++++------------ 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java index 4f347c59..65444346 100644 --- a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java +++ b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java @@ -19,9 +19,7 @@ import org.springframework.beans.factory.support.GenericBeanDefinition; import org.springframework.beans.factory.support.ManagedList; import org.springframework.context.ApplicationEventPublisher; -import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.context.annotation.Bean; -import org.springframework.lang.NonNull; import java.lang.reflect.Method; import java.util.ArrayList; @@ -42,17 +40,10 @@ import static dev.langchain4j.service.spring.AiServiceWiringMode.EXPLICIT; import static java.util.Arrays.asList; -public class AiServicesAutoConfig implements ApplicationEventPublisherAware { - - private ApplicationEventPublisher eventPublisher; - - @Override - public void setApplicationEventPublisher(@NonNull ApplicationEventPublisher applicationEventPublisher) { - this.eventPublisher = applicationEventPublisher; - } +public class AiServicesAutoConfig { @Bean - BeanFactoryPostProcessor aiServicesRegisteringBeanFactoryPostProcessor() { + BeanFactoryPostProcessor aiServicesRegisteringBeanFactoryPostProcessor(ApplicationEventPublisher eventPublisher) { return beanFactory -> { // all components available in the application context @@ -64,7 +55,7 @@ BeanFactoryPostProcessor aiServicesRegisteringBeanFactoryPostProcessor() { String[] retrievalAugmentors = beanFactory.getBeanNamesForType(RetrievalAugmentor.class); String[] moderationModels = beanFactory.getBeanNamesForType(ModerationModel.class); - Set tools = new HashSet<>(); + Set toolBeanNames = new HashSet<>(); Map> beanToolSpecifications = new HashMap<>(); for (String beanName : beanFactory.getBeanDefinitionNames()) { try { @@ -75,7 +66,7 @@ BeanFactoryPostProcessor aiServicesRegisteringBeanFactoryPostProcessor() { Class beanClass = Class.forName(beanClassName); for (Method beanMethod : beanClass.getDeclaredMethods()) { if (beanMethod.isAnnotationPresent(Tool.class)) { - tools.add(beanName); + toolBeanNames.add(beanName); List toolSpecifications = beanToolSpecifications.getOrDefault(beanName, new ArrayList<>()); toolSpecifications.add(ToolSpecifications.toolSpecificationFrom(beanMethod)); @@ -173,8 +164,8 @@ BeanFactoryPostProcessor aiServicesRegisteringBeanFactoryPostProcessor() { propertyValues.add("tools", toManagedList(asList(aiServiceAnnotation.tools()))); registeredEvent = buildEvent(aiServiceClass, beanToolSpecifications, asList(aiServiceAnnotation.tools())); } else if (aiServiceAnnotation.wiringMode() == AUTOMATIC) { - propertyValues.add("tools", toManagedList(tools)); - registeredEvent = buildEvent(aiServiceClass, beanToolSpecifications, tools); + propertyValues.add("tools", toManagedList(toolBeanNames)); + registeredEvent = buildEvent(aiServiceClass, beanToolSpecifications, toolBeanNames); } else { throw illegalArgument("Unknown wiring mode: " + aiServiceAnnotation.wiringMode()); } @@ -234,13 +225,13 @@ private static ManagedList toManagedList(Collection aiServiceClass, - Map> toolSpecifications, - Collection tools) { - return new AiServiceRegisteredEvent(AiServicesAutoConfig.class, aiServiceClass, - tools.stream() - .filter(toolSpecifications::containsKey) - .flatMap(tool -> toolSpecifications.get(tool).stream()) - .collect(Collectors.toList())); + private AiServiceRegisteredEvent buildEvent(Class aiServiceClass, + Map> beanToolSpecifications, + Collection toolBeanNames) { + return new AiServiceRegisteredEvent(this, aiServiceClass, + toolBeanNames.stream() + .filter(beanToolSpecifications::containsKey) + .flatMap(toolBeanName -> beanToolSpecifications.get(toolBeanName).stream()) + .collect(Collectors.toList())); } } From 1ecb5539d84a1a60edf7bd9ab637391f7edc350c Mon Sep 17 00:00:00 2001 From: catofdestruction Date: Mon, 9 Dec 2024 19:22:30 +0800 Subject: [PATCH 08/11] minor fix original PR link: https://github.com/langchain4j/langchain4j-spring/pull/77 Issues link: langchain4j/langchain4j#2112 --- .../service/spring/AiServicesAutoConfig.java | 22 +++---------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java index 65444346..d4db5c25 100644 --- a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java +++ b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java @@ -25,12 +25,9 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Set; -import java.util.stream.Collectors; import static dev.langchain4j.exception.IllegalConfigurationException.illegalConfiguration; import static dev.langchain4j.internal.Exceptions.illegalArgument; @@ -56,7 +53,7 @@ BeanFactoryPostProcessor aiServicesRegisteringBeanFactoryPostProcessor(Applicati String[] moderationModels = beanFactory.getBeanNamesForType(ModerationModel.class); Set toolBeanNames = new HashSet<>(); - Map> beanToolSpecifications = new HashMap<>(); + List toolSpecifications = new ArrayList<>(); for (String beanName : beanFactory.getBeanDefinitionNames()) { try { String beanClassName = beanFactory.getBeanDefinition(beanName).getBeanClassName(); @@ -67,10 +64,7 @@ BeanFactoryPostProcessor aiServicesRegisteringBeanFactoryPostProcessor(Applicati for (Method beanMethod : beanClass.getDeclaredMethods()) { if (beanMethod.isAnnotationPresent(Tool.class)) { toolBeanNames.add(beanName); - List toolSpecifications = - beanToolSpecifications.getOrDefault(beanName, new ArrayList<>()); toolSpecifications.add(ToolSpecifications.toolSpecificationFrom(beanMethod)); - beanToolSpecifications.put(beanName, toolSpecifications); } } } catch (Exception e) { @@ -162,10 +156,10 @@ BeanFactoryPostProcessor aiServicesRegisteringBeanFactoryPostProcessor(Applicati AiServiceRegisteredEvent registeredEvent; if (aiServiceAnnotation.wiringMode() == EXPLICIT) { propertyValues.add("tools", toManagedList(asList(aiServiceAnnotation.tools()))); - registeredEvent = buildEvent(aiServiceClass, beanToolSpecifications, asList(aiServiceAnnotation.tools())); + registeredEvent = new AiServiceRegisteredEvent(this, aiServiceClass, toolSpecifications); } else if (aiServiceAnnotation.wiringMode() == AUTOMATIC) { propertyValues.add("tools", toManagedList(toolBeanNames)); - registeredEvent = buildEvent(aiServiceClass, beanToolSpecifications, toolBeanNames); + registeredEvent = new AiServiceRegisteredEvent(this, aiServiceClass, toolSpecifications); } else { throw illegalArgument("Unknown wiring mode: " + aiServiceAnnotation.wiringMode()); } @@ -224,14 +218,4 @@ private static ManagedList toManagedList(Collection aiServiceClass, - Map> beanToolSpecifications, - Collection toolBeanNames) { - return new AiServiceRegisteredEvent(this, aiServiceClass, - toolBeanNames.stream() - .filter(beanToolSpecifications::containsKey) - .flatMap(toolBeanName -> beanToolSpecifications.get(toolBeanName).stream()) - .collect(Collectors.toList())); - } } From 2b4aea0fb0565ef31f95ff027172e06d66a0db83 Mon Sep 17 00:00:00 2001 From: LangChain4j Date: Fri, 20 Dec 2024 13:46:44 +0100 Subject: [PATCH 09/11] cosmetics --- .../dev/langchain4j/service/spring/AiServicesAutoConfig.java | 5 +---- .../service/spring/event/AiServiceRegisteredEvent.java | 4 +++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java index d4db5c25..ee27f7ec 100644 --- a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java +++ b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java @@ -153,13 +153,10 @@ BeanFactoryPostProcessor aiServicesRegisteringBeanFactoryPostProcessor(Applicati propertyValues ); - AiServiceRegisteredEvent registeredEvent; if (aiServiceAnnotation.wiringMode() == EXPLICIT) { propertyValues.add("tools", toManagedList(asList(aiServiceAnnotation.tools()))); - registeredEvent = new AiServiceRegisteredEvent(this, aiServiceClass, toolSpecifications); } else if (aiServiceAnnotation.wiringMode() == AUTOMATIC) { propertyValues.add("tools", toManagedList(toolBeanNames)); - registeredEvent = new AiServiceRegisteredEvent(this, aiServiceClass, toolSpecifications); } else { throw illegalArgument("Unknown wiring mode: " + aiServiceAnnotation.wiringMode()); } @@ -169,7 +166,7 @@ BeanFactoryPostProcessor aiServicesRegisteringBeanFactoryPostProcessor(Applicati registry.registerBeanDefinition(lowercaseFirstLetter(aiService), aiServiceBeanDefinition); if (eventPublisher != null) { - eventPublisher.publishEvent(registeredEvent); + eventPublisher.publishEvent(new AiServiceRegisteredEvent(this, aiServiceClass, toolSpecifications)); } } }; diff --git a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/event/AiServiceRegisteredEvent.java b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/event/AiServiceRegisteredEvent.java index 4abb1e54..8d15516e 100644 --- a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/event/AiServiceRegisteredEvent.java +++ b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/event/AiServiceRegisteredEvent.java @@ -5,6 +5,8 @@ import java.util.List; +import static dev.langchain4j.internal.Utils.copyIfNotNull; + public class AiServiceRegisteredEvent extends ApplicationEvent { private final Class aiServiceClass; @@ -13,7 +15,7 @@ public class AiServiceRegisteredEvent extends ApplicationEvent { public AiServiceRegisteredEvent(Object source, Class aiServiceClass, List toolSpecifications) { super(source); this.aiServiceClass = aiServiceClass; - this.toolSpecifications = toolSpecifications; + this.toolSpecifications = copyIfNotNull(toolSpecifications); } public Class aiServiceClass() { From bb3bc7c0e49d9827c422ad1bb8e49581d676a44b Mon Sep 17 00:00:00 2001 From: LangChain4j Date: Fri, 20 Dec 2024 14:09:55 +0100 Subject: [PATCH 10/11] try-catch for safety + cosmetics --- .../service/spring/AiServicesAutoConfig.java | 11 ++++++++++- .../listener/AiServiceRegisteredEventListener.java | 1 + 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java index ee27f7ec..c71cfc5e 100644 --- a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java +++ b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java @@ -12,6 +12,8 @@ import dev.langchain4j.rag.RetrievalAugmentor; import dev.langchain4j.rag.content.retriever.ContentRetriever; import dev.langchain4j.service.spring.event.AiServiceRegisteredEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.beans.factory.config.RuntimeBeanReference; @@ -39,6 +41,8 @@ public class AiServicesAutoConfig { + private static final Logger log = LoggerFactory.getLogger(AiServicesAutoConfig.class); + @Bean BeanFactoryPostProcessor aiServicesRegisteringBeanFactoryPostProcessor(ApplicationEventPublisher eventPublisher) { return beanFactory -> { @@ -64,7 +68,12 @@ BeanFactoryPostProcessor aiServicesRegisteringBeanFactoryPostProcessor(Applicati for (Method beanMethod : beanClass.getDeclaredMethods()) { if (beanMethod.isAnnotationPresent(Tool.class)) { toolBeanNames.add(beanName); - toolSpecifications.add(ToolSpecifications.toolSpecificationFrom(beanMethod)); + try { + toolSpecifications.add(ToolSpecifications.toolSpecificationFrom(beanMethod)); + } catch (Exception e) { + log.warn("Cannot convert %s.%s method annotated with @Tool into ToolSpecification" + .formatted(beanClass.getName(), beanMethod.getName()), e); + } } } } catch (Exception e) { diff --git a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/listener/AiServiceRegisteredEventListener.java b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/listener/AiServiceRegisteredEventListener.java index f60b3d95..35ef0c1e 100644 --- a/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/listener/AiServiceRegisteredEventListener.java +++ b/langchain4j-spring-boot-starter/src/test/java/dev/langchain4j/service/spring/mode/automatic/withTools/listener/AiServiceRegisteredEventListener.java @@ -2,6 +2,7 @@ import dev.langchain4j.service.spring.event.AiServiceRegisteredEvent; import org.springframework.stereotype.Component; + @Component public class AiServiceRegisteredEventListener extends AbstractApplicationListener { } From dc223f58a582f457e0390027c24702e4af9d245c Mon Sep 17 00:00:00 2001 From: LangChain4j Date: Fri, 20 Dec 2024 14:20:51 +0100 Subject: [PATCH 11/11] rollback to ApplicationEventPublisherAware --- .../service/spring/AiServicesAutoConfig.java | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java index c71cfc5e..629f3894 100644 --- a/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java +++ b/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiServicesAutoConfig.java @@ -21,15 +21,11 @@ import org.springframework.beans.factory.support.GenericBeanDefinition; import org.springframework.beans.factory.support.ManagedList; import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.context.annotation.Bean; import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.Set; +import java.util.*; import static dev.langchain4j.exception.IllegalConfigurationException.illegalConfiguration; import static dev.langchain4j.internal.Exceptions.illegalArgument; @@ -39,12 +35,19 @@ import static dev.langchain4j.service.spring.AiServiceWiringMode.EXPLICIT; import static java.util.Arrays.asList; -public class AiServicesAutoConfig { +public class AiServicesAutoConfig implements ApplicationEventPublisherAware { private static final Logger log = LoggerFactory.getLogger(AiServicesAutoConfig.class); + private ApplicationEventPublisher eventPublisher; + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } + @Bean - BeanFactoryPostProcessor aiServicesRegisteringBeanFactoryPostProcessor(ApplicationEventPublisher eventPublisher) { + BeanFactoryPostProcessor aiServicesRegisteringBeanFactoryPostProcessor() { return beanFactory -> { // all components available in the application context