diff --git a/core/pom.xml b/core/pom.xml index 47dd33b..ad49c75 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -48,6 +48,10 @@ com.fasterxml.jackson.module jackson-module-kotlin + + commons-beanutils + commons-beanutils + io.micrometer micrometer-observation diff --git a/core/src/main/kotlin/io/github/llmagentbuilder/core/executor/AgentExecutor.kt b/core/src/main/kotlin/io/github/llmagentbuilder/core/executor/AgentExecutor.kt index f5697f2..56ac79d 100644 --- a/core/src/main/kotlin/io/github/llmagentbuilder/core/executor/AgentExecutor.kt +++ b/core/src/main/kotlin/io/github/llmagentbuilder/core/executor/AgentExecutor.kt @@ -175,7 +175,7 @@ data class AgentExecutor( result.add(performAgentAction(nameToToolMap, it)) } } catch (e: OutputParserException) { - logger.error("Output parsing error for input {}", inputs, e) + logger.error("Output parsing error for input: {}", inputs, e) val text = e.llmOutput() var observation = parsingErrorHandler?.apply(e) ?: e.observation() val output = AgentAction("_Exception", observation, text) diff --git a/core/src/main/kotlin/io/github/llmagentbuilder/core/planner/JsonParser.kt b/core/src/main/kotlin/io/github/llmagentbuilder/core/planner/JsonParser.kt index 954aa6f..53a6d0f 100644 --- a/core/src/main/kotlin/io/github/llmagentbuilder/core/planner/JsonParser.kt +++ b/core/src/main/kotlin/io/github/llmagentbuilder/core/planner/JsonParser.kt @@ -21,7 +21,7 @@ object JsonParser { try { return parseJson(parseJsonMarkdown(json)) } catch (e: Exception) { - logger.warn("Failed to parse json {}", json) + logger.warn("Failed to parse json: {}", json) return null } } diff --git a/core/src/main/kotlin/io/github/llmagentbuilder/core/tool/AgentToolFactory.kt b/core/src/main/kotlin/io/github/llmagentbuilder/core/tool/AgentToolFactory.kt index f764874..bf56742 100644 --- a/core/src/main/kotlin/io/github/llmagentbuilder/core/tool/AgentToolFactory.kt +++ b/core/src/main/kotlin/io/github/llmagentbuilder/core/tool/AgentToolFactory.kt @@ -1,18 +1,39 @@ package io.github.llmagentbuilder.core.tool -import java.lang.reflect.Method +import org.apache.commons.beanutils.BeanUtils import java.util.function.Supplier /** * Factory to create agent tools + * + * Agent tool factories are loaded using [java.util.ServiceLoader]. */ interface AgentToolFactory> { + /** + * @param T Agent tool type + * @return Agent tool + */ fun create(): T } +/** + * Factory to create agent tools with configuration objects + */ interface ConfigurableAgentToolFactory> : AgentToolFactory { + /** + * @param T Agent tool type + * @param config Tool configuration object + * @return Agent tool + */ fun create(config: CONFIG): T + + /** + * Name of configuration key + * + * @return Config name + */ + fun configName(): String } abstract class BaseConfigurableAgentToolFactory, CONFIG>( @@ -41,46 +62,17 @@ open class EnvironmentVariableConfigProvider( Supplier { override fun get(): C { val instance = configClass.getDeclaredConstructor().newInstance() - val setters = getPropertySetters() - getEnvironmentVariables().forEach { (key, value) -> - setters[key]?.run { - invokeMethod(instance, this, value) - } - } + BeanUtils.populate(instance, getEnvironmentVariables()) return instance } - private fun invokeMethod(instance: C, method: Method, value: String) { - if (method.parameterCount != 1) { - return - } - val parameter = method.parameters[0] - val parameterValue = when (parameter.type) { - Long::class.java -> value.toLongOrNull() - Int::class.java -> value.toIntOrNull() - Double::class.java -> value.toDoubleOrNull() - Float::class.java -> value.toFloatOrNull() - else -> value - } - method.invoke(instance, parameterValue) - } - - private fun getPropertySetters(): Map { - return configClass.methods.filter { - it.name.startsWith("set") - }.associateBy { - it.name.removePrefix("set").lowercase() - } - } - private fun getEnvironmentVariables(): Map { - val prefix = environmentVariablePrefix.lowercase() + val prefix = environmentVariablePrefix return System.getenv() .filterKeys { - it.lowercase().startsWith(prefix) + it.startsWith(prefix) }.mapKeys { entry -> - entry.key.lowercase() - .removePrefix(prefix) + entry.key.removePrefix(prefix) } } } \ No newline at end of file diff --git a/core/src/main/kotlin/io/github/llmagentbuilder/core/tool/AgentToolsProvider.kt b/core/src/main/kotlin/io/github/llmagentbuilder/core/tool/AgentToolsProvider.kt index bd56981..0a93a5f 100644 --- a/core/src/main/kotlin/io/github/llmagentbuilder/core/tool/AgentToolsProvider.kt +++ b/core/src/main/kotlin/io/github/llmagentbuilder/core/tool/AgentToolsProvider.kt @@ -6,6 +6,7 @@ import io.github.llmagentbuilder.core.observation.AgentToolExecutionObservationC import io.github.llmagentbuilder.core.observation.AgentToolExecutionObservationDocumentation import io.github.llmagentbuilder.core.observation.DefaultAgentToolExecutionObservationConvention import io.micrometer.observation.ObservationRegistry +import org.apache.commons.beanutils.BeanUtils import org.slf4j.LoggerFactory import org.springframework.ai.model.function.FunctionCallback import org.springframework.ai.model.function.FunctionCallbackWrapper @@ -109,6 +110,43 @@ class CompositeAgentToolsProvider(private val providers: List>?): AgentToolsProvider { + val (toConfig, noConfig) = ServiceLoader.load(AgentToolFactory::class.java) + .stream() + .map { it.get() } + .asSequence() + .partition { it is ConfigurableAgentToolFactory<*, *> } + val noConfigTools = noConfig.map { it.create() } + val toConfigTools = toConfig.map { + val configName = + (it as ConfigurableAgentToolFactory<*, *>).configName() + val types = + GenericTypeResolver.resolveTypeArguments( + it.javaClass, + ConfigurableAgentToolFactory::class.java + ) + val configType = + types?.get(0) ?: throw IllegalArgumentException("Invalid type") + val instance = configType.getDeclaredConstructor().newInstance() + BeanUtils.populate(instance, config?.get(configName) ?: mapOf()) + val method = it.javaClass.getDeclaredMethod("create", configType) + method.invoke(it, instance) as AgentTool<*, *> + } + val agentTools = (noConfigTools + toConfigTools) + .associateBy { it.name() }.also { + logger.info("Found agent tools {}", it.keys) + }; + return object : AgentToolsProvider { + override fun get(): Map> { + return agentTools + } + } + } +} + object AutoDiscoveredAgentToolsProvider : AgentToolsProvider { private val logger = LoggerFactory.getLogger(javaClass) private val agentTools: Map> by lazy { diff --git a/core/src/test/kotlin/io/github/alexcheng1982/llmagentbuilder/core/chatmemory/MessageWindowChatMemoryTest.kt b/core/src/test/kotlin/io/github/llmagentbuilder/core/chatmemory/MessageWindowChatMemoryTest.kt similarity index 85% rename from core/src/test/kotlin/io/github/alexcheng1982/llmagentbuilder/core/chatmemory/MessageWindowChatMemoryTest.kt rename to core/src/test/kotlin/io/github/llmagentbuilder/core/chatmemory/MessageWindowChatMemoryTest.kt index 09883e7..f584af9 100644 --- a/core/src/test/kotlin/io/github/alexcheng1982/llmagentbuilder/core/chatmemory/MessageWindowChatMemoryTest.kt +++ b/core/src/test/kotlin/io/github/llmagentbuilder/core/chatmemory/MessageWindowChatMemoryTest.kt @@ -1,7 +1,5 @@ -package io.github.alexcheng1982.llmagentbuilder.core.chatmemory +package io.github.llmagentbuilder.core.chatmemory -import io.github.llmagentbuilder.core.chatmemory.InMemoryChatMemoryStore -import io.github.llmagentbuilder.core.chatmemory.MessageWindowChatMemory import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test diff --git a/core/src/test/kotlin/io/github/alexcheng1982/llmagentbuilder/core/planner/JsonParserTest.kt b/core/src/test/kotlin/io/github/llmagentbuilder/core/planner/JsonParserTest.kt similarity index 93% rename from core/src/test/kotlin/io/github/alexcheng1982/llmagentbuilder/core/planner/JsonParserTest.kt rename to core/src/test/kotlin/io/github/llmagentbuilder/core/planner/JsonParserTest.kt index 1e2452d..35517ab 100644 --- a/core/src/test/kotlin/io/github/alexcheng1982/llmagentbuilder/core/planner/JsonParserTest.kt +++ b/core/src/test/kotlin/io/github/llmagentbuilder/core/planner/JsonParserTest.kt @@ -1,6 +1,5 @@ -package io.github.alexcheng1982.llmagentbuilder.core.planner +package io.github.llmagentbuilder.core.planner -import io.github.llmagentbuilder.core.planner.JsonParser import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Test diff --git a/core/src/test/kotlin/io/github/alexcheng1982/llmagentbuilder/core/planner/react/ReActOutputParserTest.kt b/core/src/test/kotlin/io/github/llmagentbuilder/core/planner/react/ReActOutputParserTest.kt similarity index 89% rename from core/src/test/kotlin/io/github/alexcheng1982/llmagentbuilder/core/planner/react/ReActOutputParserTest.kt rename to core/src/test/kotlin/io/github/llmagentbuilder/core/planner/react/ReActOutputParserTest.kt index 534b86e..e18a038 100644 --- a/core/src/test/kotlin/io/github/alexcheng1982/llmagentbuilder/core/planner/react/ReActOutputParserTest.kt +++ b/core/src/test/kotlin/io/github/llmagentbuilder/core/planner/react/ReActOutputParserTest.kt @@ -1,8 +1,7 @@ -package io.github.alexcheng1982.llmagentbuilder.core.planner.react +package io.github.llmagentbuilder.core.planner.react import io.github.llmagentbuilder.core.AgentAction import io.github.llmagentbuilder.core.AgentFinish -import io.github.llmagentbuilder.core.planner.react.ReActOutputParser import org.junit.jupiter.api.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull diff --git a/core/src/test/kotlin/io/github/alexcheng1982/llmagentbuilder/core/planner/reactjson/ReActJsonOutputParserTest.kt b/core/src/test/kotlin/io/github/llmagentbuilder/core/planner/reactjson/ReActJsonOutputParserTest.kt similarity index 87% rename from core/src/test/kotlin/io/github/alexcheng1982/llmagentbuilder/core/planner/reactjson/ReActJsonOutputParserTest.kt rename to core/src/test/kotlin/io/github/llmagentbuilder/core/planner/reactjson/ReActJsonOutputParserTest.kt index 0704194..7b5d3cd 100644 --- a/core/src/test/kotlin/io/github/alexcheng1982/llmagentbuilder/core/planner/reactjson/ReActJsonOutputParserTest.kt +++ b/core/src/test/kotlin/io/github/llmagentbuilder/core/planner/reactjson/ReActJsonOutputParserTest.kt @@ -1,6 +1,5 @@ -package io.github.alexcheng1982.llmagentbuilder.core.planner.reactjson +package io.github.llmagentbuilder.core.planner.reactjson -import io.github.llmagentbuilder.core.planner.reactjson.ReActJsonOutputParser import org.junit.jupiter.api.Test import kotlin.test.assertNotNull diff --git a/core/src/test/kotlin/io/github/alexcheng1982/llmagentbuilder/core/tool/EnvironmentVariableConfigProviderTest.kt b/core/src/test/kotlin/io/github/llmagentbuilder/core/tool/EnvironmentVariableConfigProviderTest.kt similarity index 91% rename from core/src/test/kotlin/io/github/alexcheng1982/llmagentbuilder/core/tool/EnvironmentVariableConfigProviderTest.kt rename to core/src/test/kotlin/io/github/llmagentbuilder/core/tool/EnvironmentVariableConfigProviderTest.kt index 1801b56..304ddc2 100644 --- a/core/src/test/kotlin/io/github/alexcheng1982/llmagentbuilder/core/tool/EnvironmentVariableConfigProviderTest.kt +++ b/core/src/test/kotlin/io/github/llmagentbuilder/core/tool/EnvironmentVariableConfigProviderTest.kt @@ -1,6 +1,5 @@ -package io.github.alexcheng1982.llmagentbuilder.core.tool +package io.github.llmagentbuilder.core.tool -import io.github.llmagentbuilder.core.tool.EnvironmentVariableConfigProvider import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import uk.org.webcompere.systemstubs.SystemStubs.withEnvironmentVariable diff --git a/llm/dashscope/pom.xml b/llm/dashscope/pom.xml index 20aebb7..a299f84 100644 --- a/llm/dashscope/pom.xml +++ b/llm/dashscope/pom.xml @@ -23,7 +23,7 @@ io.github.alexcheng1982 spring-ai-dashscope-client - 0.8.0 + 1.1.1 org.slf4j diff --git a/pom.xml b/pom.xml index c725ef8..21dda0e 100644 --- a/pom.xml +++ b/pom.xml @@ -252,6 +252,11 @@ jsoup 1.17.2 + + commons-beanutils + commons-beanutils + 1.9.4 + com.fasterxml.jackson.core jackson-databind diff --git a/spring/spring-boot-autoconfigure/pom.xml b/spring/spring-boot-autoconfigure/pom.xml index 7aa0834..334ab0d 100644 --- a/spring/spring-boot-autoconfigure/pom.xml +++ b/spring/spring-boot-autoconfigure/pom.xml @@ -22,6 +22,11 @@ org.springframework.ai spring-ai-spring-boot-autoconfigure + + io.github.alexcheng1982 + spring-ai-dashscope-spring-boot-autoconfigure + 1.1.1 + org.springframework.boot spring-boot-actuator-autoconfigure diff --git a/spring/spring-boot-autoconfigure/src/main/java/io/github/llmagentbuilder/spring/autoconfigure/chatagent/chatagent/ChatAgentAutoConfiguration.java b/spring/spring-boot-autoconfigure/src/main/java/io/github/llmagentbuilder/spring/autoconfigure/chatagent/ChatAgentAutoConfiguration.java similarity index 85% rename from spring/spring-boot-autoconfigure/src/main/java/io/github/llmagentbuilder/spring/autoconfigure/chatagent/chatagent/ChatAgentAutoConfiguration.java rename to spring/spring-boot-autoconfigure/src/main/java/io/github/llmagentbuilder/spring/autoconfigure/chatagent/ChatAgentAutoConfiguration.java index d8fca12..0ef6982 100644 --- a/spring/spring-boot-autoconfigure/src/main/java/io/github/llmagentbuilder/spring/autoconfigure/chatagent/chatagent/ChatAgentAutoConfiguration.java +++ b/spring/spring-boot-autoconfigure/src/main/java/io/github/llmagentbuilder/spring/autoconfigure/chatagent/ChatAgentAutoConfiguration.java @@ -1,5 +1,6 @@ -package io.github.llmagentbuilder.spring.autoconfigure.chatagent.chatagent; +package io.github.llmagentbuilder.spring.autoconfigure.chatagent; +import io.github.alexcheng1982.springai.dashscope.autoconfigure.DashscopeAutoConfiguration; import io.github.llmagentbuilder.core.Agent; import io.github.llmagentbuilder.core.AgentFactory; import io.github.llmagentbuilder.core.ChatAgent; @@ -9,7 +10,7 @@ import io.github.llmagentbuilder.core.planner.reactjson.ReActJsonPlannerFactory; import io.github.llmagentbuilder.core.tool.AgentToolFunctionCallbackContext; import io.github.llmagentbuilder.core.tool.AgentToolsProvider; -import io.github.llmagentbuilder.core.tool.AutoDiscoveredAgentToolsProvider; +import io.github.llmagentbuilder.core.tool.AgentToolsProviderFactory; import io.github.llmagentbuilder.core.tool.CompositeAgentToolsProvider; import io.github.llmagentbuilder.spring.spring.SpringAgentToolsProvider; import io.github.llmagentbuilder.spring.spring.chatagent.ChatAgentService; @@ -34,11 +35,13 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; @AutoConfiguration(before = WebMvcAutoConfiguration.class, after = { OllamaAutoConfiguration.class, OpenAiAutoConfiguration.class, + DashscopeAutoConfiguration.class, ObservationAutoConfiguration.class}) -@ConditionalOnProperty(prefix = "io.github.llmagentbuilder.chatagent", name = "enabled", matchIfMissing = true) +@ConditionalOnProperty(prefix = ChatAgentProperties.CONFIG_PREFIX, name = "enabled", matchIfMissing = true) public class ChatAgentAutoConfiguration { @Configuration(proxyBeanMethods = false) @@ -55,7 +58,8 @@ public static class ChatAgentConfiguration { } @Bean - @ConditionalOnProperty(prefix = "io.github.llmagentbuilder.chatagent.memory", name = "enabled", matchIfMissing = true) + @ConditionalOnProperty(prefix = ChatAgentProperties.CONFIG_PREFIX + + ".memory", name = "enabled", matchIfMissing = true) @ConditionalOnMissingBean public ChatMemoryStore chatMemoryStore() { return new InMemoryChatMemoryStore(); @@ -83,7 +87,8 @@ public Planner planner(ChatClient chatClient, } @Bean - @ConditionalOnProperty(prefix = "io.github.llmagentbuilder.chatagent.tracing", name = "enabled", matchIfMissing = true) + @ConditionalOnProperty(prefix = ChatAgentProperties.CONFIG_PREFIX + + ".tracing", name = "enabled", matchIfMissing = true) @ConditionalOnBean(Planner.class) @ConditionalOnMissingBean public ObservationRegistry observationRegistry() { @@ -91,7 +96,8 @@ public ObservationRegistry observationRegistry() { } @Bean - @ConditionalOnProperty(prefix = "io.github.llmagentbuilder.chatagent.metrics", name = "enabled", matchIfMissing = true) + @ConditionalOnProperty(prefix = ChatAgentProperties.CONFIG_PREFIX + + ".metrics", name = "enabled", matchIfMissing = true) @ConditionalOnBean(Planner.class) @ConditionalOnMissingBean public MeterRegistry meterRegistry() { @@ -121,8 +127,8 @@ public ChatAgentService chatAgentService(ChatAgent chatAgent) { } @Bean - @ConditionalOnMissingBean - public FunctionCallbackContext springAiFunctionManager( + @Primary + public FunctionCallbackContext agentToolFunctionCallbackContext( AgentToolsProvider agentToolsProvider, Optional observationRegistry, ApplicationContext context) { @@ -139,7 +145,8 @@ public AgentToolsProvider agentToolsProvider(ApplicationContext context) { var springAgentToolsProvider = new SpringAgentToolsProvider(); springAgentToolsProvider.setApplicationContext(context); return new CompositeAgentToolsProvider(List.of( - AutoDiscoveredAgentToolsProvider.INSTANCE, + AgentToolsProviderFactory.INSTANCE.create( + properties.getTools().getConfig()), springAgentToolsProvider )); } diff --git a/spring/spring-boot-autoconfigure/src/main/java/io/github/llmagentbuilder/spring/autoconfigure/chatagent/chatagent/ChatAgentProperties.java b/spring/spring-boot-autoconfigure/src/main/java/io/github/llmagentbuilder/spring/autoconfigure/chatagent/ChatAgentProperties.java similarity index 77% rename from spring/spring-boot-autoconfigure/src/main/java/io/github/llmagentbuilder/spring/autoconfigure/chatagent/chatagent/ChatAgentProperties.java rename to spring/spring-boot-autoconfigure/src/main/java/io/github/llmagentbuilder/spring/autoconfigure/chatagent/ChatAgentProperties.java index 7731e01..159357f 100644 --- a/spring/spring-boot-autoconfigure/src/main/java/io/github/llmagentbuilder/spring/autoconfigure/chatagent/chatagent/ChatAgentProperties.java +++ b/spring/spring-boot-autoconfigure/src/main/java/io/github/llmagentbuilder/spring/autoconfigure/chatagent/ChatAgentProperties.java @@ -1,10 +1,14 @@ -package io.github.llmagentbuilder.spring.autoconfigure.chatagent.chatagent; +package io.github.llmagentbuilder.spring.autoconfigure.chatagent; +import java.util.Map; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; -@ConfigurationProperties(prefix = "io.github.llmagentbuilder.chatagent") +@ConfigurationProperties(prefix = ChatAgentProperties.CONFIG_PREFIX) public class ChatAgentProperties { + public static final String CONFIG_PREFIX = "io.github.llmagentbuilder.chatagent"; + private boolean enabled = true; private String id = null; @@ -15,14 +19,21 @@ public class ChatAgentProperties { private String usageInstruction = "Ask me anything"; + @NestedConfigurationProperty private ReActJson reActJson = new ReActJson(); + @NestedConfigurationProperty private Memory memory = new Memory(); + @NestedConfigurationProperty private Tracing tracing = new Tracing(); + @NestedConfigurationProperty private Metrics metrics = new Metrics(); + @NestedConfigurationProperty + private Tools tools = new Tools(); + public boolean isEnabled() { return enabled; } @@ -98,6 +109,15 @@ public void setMetrics( this.metrics = metrics; } + public Tools getTools() { + return tools; + } + + public void setTools( + Tools tools) { + this.tools = tools; + } + public boolean tracingEnabled() { return tracing == null || tracing.isEnabled(); } @@ -157,4 +177,18 @@ public void setEnabled(boolean enabled) { this.enabled = enabled; } } + + public static class Tools { + + private Map> config; + + public Map> getConfig() { + return config; + } + + public void setConfig( + Map> config) { + this.config = config; + } + } } diff --git a/spring/spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring/spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index d8044f6..8e8c012 100644 --- a/spring/spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/spring/spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1 +1 @@ -io.github.alexcheng1982.agentappbuilder.spring.autoconfigure.chatagent.ChatAgentAutoConfiguration \ No newline at end of file +io.github.llmagentbuilder.spring.autoconfigure.chatagent.ChatAgentAutoConfiguration \ No newline at end of file diff --git a/spring/spring-boot-starter/agent-app-builder-spring-boot-starter.iml b/spring/spring-boot-starter/agent-app-builder-spring-boot-starter.iml new file mode 100644 index 0000000..056f882 --- /dev/null +++ b/spring/spring-boot-starter/agent-app-builder-spring-boot-starter.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/spring/spring-boot-starter/pom.xml b/spring/spring-boot-starter/pom.xml index 0b54ae7..b3f2db0 100644 --- a/spring/spring-boot-starter/pom.xml +++ b/spring/spring-boot-starter/pom.xml @@ -12,7 +12,7 @@ spring-boot-starter Spring Integration :: Spring Boot Starter LLM Agent Builder - Spring Boot Starter - pom + jar diff --git a/spring/spring-boot-starter/src/main/java/io/github/llmagentbuilder/spring/starter/package-info.java b/spring/spring-boot-starter/src/main/java/io/github/llmagentbuilder/spring/starter/package-info.java new file mode 100644 index 0000000..76fe493 --- /dev/null +++ b/spring/spring-boot-starter/src/main/java/io/github/llmagentbuilder/spring/starter/package-info.java @@ -0,0 +1 @@ +package io.github.llmagentbuilder.spring.starter; \ No newline at end of file