diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/tools.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/tools.adoc index c285ef58dbc..9c705f76a6d 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/tools.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/tools.adoc @@ -1197,7 +1197,7 @@ public interface ToolExecutionExceptionProcessor { } ---- -If you're using any of the Spring AI Spring Boot Starters, `DefaultToolExecutionExceptionProcessor` is the autoconfigured implementation of the `ToolExecutionExceptionProcessor` interface. By default, the error message is sent back to the model. The `DefaultToolExecutionExceptionProcessor` constructor lets you set the `alwaysThrow` attribute to `true` or `false`. If `true`, an exception will be thrown instead of sending an error message back to the model. +If you're using any of the Spring AI Spring Boot Starters, `DefaultToolExecutionExceptionProcessor` is the autoconfigured implementation of the `ToolExecutionExceptionProcessor` interface. By default, the error message of `RuntimeException` is sent back to the model, while checked exceptions and Errors (e.g., `IOException`, `OutOfMemoryError`) are always thrown. The `DefaultToolExecutionExceptionProcessor` constructor lets you set the `alwaysThrow` attribute to `true` or `false`. If `true`, an exception will be thrown instead of sending an error message back to the model. You can use the ``spring.ai.tools.throw-exception-on-error` property to control the behavior of the `DefaultToolExecutionExceptionProcessor` bean: diff --git a/spring-ai-model/src/main/java/org/springframework/ai/tool/execution/DefaultToolExecutionExceptionProcessor.java b/spring-ai-model/src/main/java/org/springframework/ai/tool/execution/DefaultToolExecutionExceptionProcessor.java index 6e2aa03ad4a..2fff99f9398 100644 --- a/spring-ai-model/src/main/java/org/springframework/ai/tool/execution/DefaultToolExecutionExceptionProcessor.java +++ b/spring-ai-model/src/main/java/org/springframework/ai/tool/execution/DefaultToolExecutionExceptionProcessor.java @@ -18,6 +18,7 @@ import java.util.Collections; import java.util.List; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -30,11 +31,12 @@ * * @author Thomas Vitale * @author Daniel Garnier-Moiroux + * @author YunKui Lu * @since 1.0.0 */ public class DefaultToolExecutionExceptionProcessor implements ToolExecutionExceptionProcessor { - private final static Logger logger = LoggerFactory.getLogger(DefaultToolExecutionExceptionProcessor.class); + private static final Logger logger = LoggerFactory.getLogger(DefaultToolExecutionExceptionProcessor.class); private static final boolean DEFAULT_ALWAYS_THROW = false; @@ -56,10 +58,17 @@ public DefaultToolExecutionExceptionProcessor(boolean alwaysThrow, public String process(ToolExecutionException exception) { Assert.notNull(exception, "exception cannot be null"); Throwable cause = exception.getCause(); - if (cause instanceof RuntimeException runtimeException - && this.rethrownExceptions.stream().anyMatch(rethrown -> rethrown.isAssignableFrom(cause.getClass()))) { - throw runtimeException; + if (cause instanceof RuntimeException runtimeException) { + if (this.rethrownExceptions.stream().anyMatch(rethrown -> rethrown.isAssignableFrom(cause.getClass()))) { + throw runtimeException; + } + } + else { + // If the cause is not a RuntimeException (e.g., IOException, + // OutOfMemoryError), rethrow the tool exception. + throw exception; } + if (this.alwaysThrow) { throw exception; } diff --git a/spring-ai-model/src/test/java/org/springframework/ai/tool/execution/DefaultToolExecutionExceptionProcessorTests.java b/spring-ai-model/src/test/java/org/springframework/ai/tool/execution/DefaultToolExecutionExceptionProcessorTests.java index 955194dd2a3..7f79d0816e1 100644 --- a/spring-ai-model/src/test/java/org/springframework/ai/tool/execution/DefaultToolExecutionExceptionProcessorTests.java +++ b/spring-ai-model/src/test/java/org/springframework/ai/tool/execution/DefaultToolExecutionExceptionProcessorTests.java @@ -17,9 +17,11 @@ package org.springframework.ai.tool.execution; import java.util.List; + import org.junit.jupiter.api.Test; import org.springframework.ai.tool.definition.DefaultToolDefinition; + import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.InstanceOfAssertFactories.type; @@ -33,12 +35,21 @@ class DefaultToolExecutionExceptionProcessorTests { private final IllegalStateException toolException = new IllegalStateException("Inner exception"); + private final Exception toolCheckedException = new Exception("Checked exception"); + + private final Error toolError = new Error("Error"); + private final DefaultToolDefinition toolDefinition = new DefaultToolDefinition("toolName", "toolDescription", "inputSchema"); private final ToolExecutionException toolExecutionException = new ToolExecutionException(toolDefinition, toolException); + private final ToolExecutionException toolExecutionCheckedException = new ToolExecutionException(toolDefinition, + toolCheckedException); + + private final ToolExecutionException toolExecutionError = new ToolExecutionException(toolDefinition, toolError); + @Test void processReturnsMessage() { DefaultToolExecutionExceptionProcessor processor = DefaultToolExecutionExceptionProcessor.builder().build(); @@ -96,4 +107,27 @@ void processRethrowsOnlySelectExceptions() { assertThat(result).isEqualTo("This exception was not rethrown"); } + @Test + void processThrowsCheckedException() { + DefaultToolExecutionExceptionProcessor processor = DefaultToolExecutionExceptionProcessor.builder().build(); + + assertThatThrownBy(() -> processor.process(this.toolExecutionCheckedException)) + .hasMessage(this.toolCheckedException.getMessage()) + .hasCauseInstanceOf(this.toolCheckedException.getClass()) + .asInstanceOf(type(ToolExecutionException.class)) + .extracting(ToolExecutionException::getToolDefinition) + .isEqualTo(this.toolDefinition); + } + + @Test + void processThrowsError() { + DefaultToolExecutionExceptionProcessor processor = DefaultToolExecutionExceptionProcessor.builder().build(); + + assertThatThrownBy(() -> processor.process(this.toolExecutionError)).hasMessage(this.toolError.getMessage()) + .hasCauseInstanceOf(this.toolError.getClass()) + .asInstanceOf(type(ToolExecutionException.class)) + .extracting(ToolExecutionException::getToolDefinition) + .isEqualTo(this.toolDefinition); + } + }