Skip to content

Commit 879350a

Browse files
authored
fix: Rethrow ToolExecutionException for non-RuntimeException (#3915)
Fixes #3915 Auto-cherry-pick to 1.0.x - If the cause is a non-RuntimeException (e.g., IOException, OutOfMemoryError), rethrow the tool exception. - Update the relevant description in docs Signed-off-by: YunKui Lu <[email protected]>
1 parent 1b2b891 commit 879350a

File tree

3 files changed

+48
-5
lines changed

3 files changed

+48
-5
lines changed

spring-ai-docs/src/main/antora/modules/ROOT/pages/api/tools.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1197,7 +1197,7 @@ public interface ToolExecutionExceptionProcessor {
11971197
}
11981198
----
11991199

1200-
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.
1200+
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.
12011201

12021202
You can use the ``spring.ai.tools.throw-exception-on-error` property to control the behavior of the `DefaultToolExecutionExceptionProcessor` bean:
12031203

spring-ai-model/src/main/java/org/springframework/ai/tool/execution/DefaultToolExecutionExceptionProcessor.java

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.util.Collections;
2020
import java.util.List;
21+
2122
import org.slf4j.Logger;
2223
import org.slf4j.LoggerFactory;
2324

@@ -30,11 +31,12 @@
3031
*
3132
* @author Thomas Vitale
3233
* @author Daniel Garnier-Moiroux
34+
* @author YunKui Lu
3335
* @since 1.0.0
3436
*/
3537
public class DefaultToolExecutionExceptionProcessor implements ToolExecutionExceptionProcessor {
3638

37-
private final static Logger logger = LoggerFactory.getLogger(DefaultToolExecutionExceptionProcessor.class);
39+
private static final Logger logger = LoggerFactory.getLogger(DefaultToolExecutionExceptionProcessor.class);
3840

3941
private static final boolean DEFAULT_ALWAYS_THROW = false;
4042

@@ -56,10 +58,17 @@ public DefaultToolExecutionExceptionProcessor(boolean alwaysThrow,
5658
public String process(ToolExecutionException exception) {
5759
Assert.notNull(exception, "exception cannot be null");
5860
Throwable cause = exception.getCause();
59-
if (cause instanceof RuntimeException runtimeException
60-
&& this.rethrownExceptions.stream().anyMatch(rethrown -> rethrown.isAssignableFrom(cause.getClass()))) {
61-
throw runtimeException;
61+
if (cause instanceof RuntimeException runtimeException) {
62+
if (this.rethrownExceptions.stream().anyMatch(rethrown -> rethrown.isAssignableFrom(cause.getClass()))) {
63+
throw runtimeException;
64+
}
65+
}
66+
else {
67+
// If the cause is not a RuntimeException (e.g., IOException,
68+
// OutOfMemoryError), rethrow the tool exception.
69+
throw exception;
6270
}
71+
6372
if (this.alwaysThrow) {
6473
throw exception;
6574
}

spring-ai-model/src/test/java/org/springframework/ai/tool/execution/DefaultToolExecutionExceptionProcessorTests.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@
1717
package org.springframework.ai.tool.execution;
1818

1919
import java.util.List;
20+
2021
import org.junit.jupiter.api.Test;
2122

2223
import org.springframework.ai.tool.definition.DefaultToolDefinition;
24+
2325
import static org.assertj.core.api.Assertions.assertThat;
2426
import static org.assertj.core.api.Assertions.assertThatThrownBy;
2527
import static org.assertj.core.api.InstanceOfAssertFactories.type;
@@ -33,12 +35,21 @@ class DefaultToolExecutionExceptionProcessorTests {
3335

3436
private final IllegalStateException toolException = new IllegalStateException("Inner exception");
3537

38+
private final Exception toolCheckedException = new Exception("Checked exception");
39+
40+
private final Error toolError = new Error("Error");
41+
3642
private final DefaultToolDefinition toolDefinition = new DefaultToolDefinition("toolName", "toolDescription",
3743
"inputSchema");
3844

3945
private final ToolExecutionException toolExecutionException = new ToolExecutionException(toolDefinition,
4046
toolException);
4147

48+
private final ToolExecutionException toolExecutionCheckedException = new ToolExecutionException(toolDefinition,
49+
toolCheckedException);
50+
51+
private final ToolExecutionException toolExecutionError = new ToolExecutionException(toolDefinition, toolError);
52+
4253
@Test
4354
void processReturnsMessage() {
4455
DefaultToolExecutionExceptionProcessor processor = DefaultToolExecutionExceptionProcessor.builder().build();
@@ -96,4 +107,27 @@ void processRethrowsOnlySelectExceptions() {
96107
assertThat(result).isEqualTo("This exception was not rethrown");
97108
}
98109

110+
@Test
111+
void processThrowsCheckedException() {
112+
DefaultToolExecutionExceptionProcessor processor = DefaultToolExecutionExceptionProcessor.builder().build();
113+
114+
assertThatThrownBy(() -> processor.process(this.toolExecutionCheckedException))
115+
.hasMessage(this.toolCheckedException.getMessage())
116+
.hasCauseInstanceOf(this.toolCheckedException.getClass())
117+
.asInstanceOf(type(ToolExecutionException.class))
118+
.extracting(ToolExecutionException::getToolDefinition)
119+
.isEqualTo(this.toolDefinition);
120+
}
121+
122+
@Test
123+
void processThrowsError() {
124+
DefaultToolExecutionExceptionProcessor processor = DefaultToolExecutionExceptionProcessor.builder().build();
125+
126+
assertThatThrownBy(() -> processor.process(this.toolExecutionError)).hasMessage(this.toolError.getMessage())
127+
.hasCauseInstanceOf(this.toolError.getClass())
128+
.asInstanceOf(type(ToolExecutionException.class))
129+
.extracting(ToolExecutionException::getToolDefinition)
130+
.isEqualTo(this.toolDefinition);
131+
}
132+
99133
}

0 commit comments

Comments
 (0)