diff --git a/auto-configurations/models/tool/spring-ai-autoconfigure-model-tool/src/main/java/org/springframework/ai/model/tool/autoconfigure/ToolCallingAutoConfiguration.java b/auto-configurations/models/tool/spring-ai-autoconfigure-model-tool/src/main/java/org/springframework/ai/model/tool/autoconfigure/ToolCallingAutoConfiguration.java index a7dfbc74f40..dcf4ae91b7b 100644 --- a/auto-configurations/models/tool/spring-ai-autoconfigure-model-tool/src/main/java/org/springframework/ai/model/tool/autoconfigure/ToolCallingAutoConfiguration.java +++ b/auto-configurations/models/tool/spring-ai-autoconfigure-model-tool/src/main/java/org/springframework/ai/model/tool/autoconfigure/ToolCallingAutoConfiguration.java @@ -32,6 +32,7 @@ import org.springframework.ai.tool.observation.ToolCallingContentObservationFilter; import org.springframework.ai.tool.observation.ToolCallingObservationConvention; import org.springframework.ai.tool.resolution.DelegatingToolCallbackResolver; +import org.springframework.ai.tool.resolution.InitToolCallbackResolverErrorHandler; import org.springframework.ai.tool.resolution.SpringBeanToolCallbackResolver; import org.springframework.ai.tool.resolution.StaticToolCallbackResolver; import org.springframework.ai.tool.resolution.ToolCallbackResolver; @@ -63,10 +64,22 @@ public class ToolCallingAutoConfiguration { @Bean @ConditionalOnMissingBean ToolCallbackResolver toolCallbackResolver(GenericApplicationContext applicationContext, - List toolCallbacks, List tcbProviders) { + List toolCallbacks, List tcbProviders, + List errorHandlers) { List allFunctionAndToolCallbacks = new ArrayList<>(toolCallbacks); - tcbProviders.stream().map(pr -> List.of(pr.getToolCallbacks())).forEach(allFunctionAndToolCallbacks::addAll); + for (ToolCallbackProvider pr : tcbProviders) { + try { + allFunctionAndToolCallbacks.addAll(List.of(pr.getToolCallbacks())); + } + catch (Throwable t) { + allFunctionAndToolCallbacks.addAll(errorHandlers.stream() + .filter(h -> h.support(pr, t)) + .findFirst() + .orElseThrow(() -> new RuntimeException(t)) + .handle(pr, t)); + } + } var staticToolCallbackResolver = new StaticToolCallbackResolver(allFunctionAndToolCallbacks); diff --git a/auto-configurations/models/tool/spring-ai-autoconfigure-model-tool/src/test/java/org/springframework/ai/model/tool/autoconfigure/ToolCallingAutoConfigurationTests.java b/auto-configurations/models/tool/spring-ai-autoconfigure-model-tool/src/test/java/org/springframework/ai/model/tool/autoconfigure/ToolCallingAutoConfigurationTests.java index af42d744158..5fa2956098d 100644 --- a/auto-configurations/models/tool/spring-ai-autoconfigure-model-tool/src/test/java/org/springframework/ai/model/tool/autoconfigure/ToolCallingAutoConfigurationTests.java +++ b/auto-configurations/models/tool/spring-ai-autoconfigure-model-tool/src/test/java/org/springframework/ai/model/tool/autoconfigure/ToolCallingAutoConfigurationTests.java @@ -16,6 +16,8 @@ package org.springframework.ai.model.tool.autoconfigure; +import java.util.Collections; +import java.util.List; import java.util.function.Function; import org.junit.jupiter.api.Test; @@ -35,6 +37,7 @@ import org.springframework.ai.tool.method.MethodToolCallbackProvider; import org.springframework.ai.tool.observation.ToolCallingContentObservationFilter; import org.springframework.ai.tool.resolution.DelegatingToolCallbackResolver; +import org.springframework.ai.tool.resolution.InitToolCallbackResolverErrorHandler; import org.springframework.ai.tool.resolution.ToolCallbackResolver; import org.springframework.ai.tool.support.ToolDefinitions; import org.springframework.boot.autoconfigure.AutoConfigurations; @@ -42,6 +45,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Description; +import org.springframework.core.Ordered; +import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -185,6 +190,14 @@ void throwExceptionOnErrorEnabled() { }); } + @Test + void testInitToolCallbackResolverErrorHandler() { + new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(ToolCallingAutoConfiguration.class)) + .withUserConfiguration(InitToolCallbackResolverErrorHandlerTestConf.class) + .run(context -> assertThat(context.getBeansOfType(InitToolCallbackResolverErrorHandler.class)) + .isNotEmpty()); + } + static class WeatherService { @Tool(description = "Get the weather in location. Return temperature in 36°F or 36°C format.") @@ -275,4 +288,39 @@ public record Response(String temperature) { } + @Configuration + static class InitToolCallbackResolverErrorHandlerTestConf { + + private static final String ERROR_MSG = "TestError"; + + @Bean + public ToolCallbackProvider errorToolCallbackProvider() { + return () -> { + throw new RuntimeException(ERROR_MSG); + }; + } + + @Bean + public InitToolCallbackResolverErrorHandler testErrorHandler() { + return new InitToolCallbackResolverErrorHandler() { + @Override + public boolean support(ToolCallbackProvider provider, Throwable t) { + return true; + } + + @Override + public List handle(ToolCallbackProvider provider, Throwable t) { + Assert.isTrue(ERROR_MSG.equals(t.getMessage()), "error"); + return Collections.emptyList(); + } + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE; + } + }; + } + + } + } diff --git a/spring-ai-model/src/main/java/org/springframework/ai/tool/resolution/InitToolCallbackResolverErrorHandler.java b/spring-ai-model/src/main/java/org/springframework/ai/tool/resolution/InitToolCallbackResolverErrorHandler.java new file mode 100644 index 00000000000..af162303e44 --- /dev/null +++ b/spring-ai-model/src/main/java/org/springframework/ai/tool/resolution/InitToolCallbackResolverErrorHandler.java @@ -0,0 +1,51 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.ai.tool.resolution; + +import java.util.List; + +import org.springframework.ai.tool.ToolCallback; +import org.springframework.ai.tool.ToolCallbackProvider; +import org.springframework.core.Ordered; + +/** + * Handler for errors when {@link ToolCallbackResolver} failed to fetch ToolCallbacks on + * application starting up. + * + * @author walter.tan + * @since 1.0.2 + */ +public interface InitToolCallbackResolverErrorHandler extends Ordered { + + /** + * Check whether the handler supports the given provider and error. + * @param provider the failed ToolCallbackProvider + * @param t the error caught + * @return + */ + boolean support(ToolCallbackProvider provider, Throwable t); + + /** + * Handle errors when initializing the {@link ToolCallbackResolver} failed and return + * fallback ToolCallbacks on demand. + * @param provider the failed ToolCallbackProvider + * @param t the error caught + * @return + */ + List handle(ToolCallbackProvider provider, Throwable t); + +}