Skip to content

Commit 597f8a4

Browse files
committed
fix: Rethrow ToolExecutionException for non-RuntimeException
- If the cause is an Error (e.g., OutOfMemoryError), rethrow it - If the cause is checked exception (e.g., IOException), rethrow the `ToolExecutionException` - Update the relevant description in docs Signed-off-by: YunKui Lu <[email protected]>
1 parent 128c45a commit 597f8a4

File tree

3 files changed

+37
-5
lines changed

3 files changed

+37
-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: 17 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,21 @@ 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 if (cause instanceof Error error) {
67+
// If the cause is an Error (e.g., OutOfMemoryError), rethrow it.
68+
throw error;
6269
}
70+
else {
71+
// If the cause is checked exception (e.g., IOException), rethrow the tool
72+
// exception.
73+
throw exception;
74+
}
75+
6376
if (this.alwaysThrow) {
6477
throw exception;
6578
}

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

Lines changed: 19 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,17 @@ class DefaultToolExecutionExceptionProcessorTests {
3335

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

38+
private final Exception toolCheckedException = new Exception("Checked exception");
39+
3640
private final DefaultToolDefinition toolDefinition = new DefaultToolDefinition("toolName", "toolDescription",
3741
"inputSchema");
3842

3943
private final ToolExecutionException toolExecutionException = new ToolExecutionException(toolDefinition,
4044
toolException);
4145

46+
private final ToolExecutionException toolExecutionCheckedException = new ToolExecutionException(toolDefinition,
47+
toolCheckedException);
48+
4249
@Test
4350
void processReturnsMessage() {
4451
DefaultToolExecutionExceptionProcessor processor = DefaultToolExecutionExceptionProcessor.builder().build();
@@ -96,4 +103,16 @@ void processRethrowsOnlySelectExceptions() {
96103
assertThat(result).isEqualTo("This exception was not rethrown");
97104
}
98105

106+
@Test
107+
void processThrowsCheckedException() {
108+
DefaultToolExecutionExceptionProcessor processor = DefaultToolExecutionExceptionProcessor.builder().build();
109+
110+
assertThatThrownBy(() -> processor.process(this.toolExecutionCheckedException))
111+
.hasMessage(this.toolCheckedException.getMessage())
112+
.hasCauseInstanceOf(this.toolCheckedException.getClass())
113+
.asInstanceOf(type(ToolExecutionException.class))
114+
.extracting(ToolExecutionException::getToolDefinition)
115+
.isEqualTo(this.toolDefinition);
116+
}
117+
99118
}

0 commit comments

Comments
 (0)