Skip to content

Commit 6d3525f

Browse files
committed
Shortcut getErrorResponse
mask clientError add better testing add filter specific getter in exception
1 parent 2c0f2ea commit 6d3525f

File tree

11 files changed

+187
-71
lines changed

11 files changed

+187
-71
lines changed

core/src/main/java/com/sap/ai/sdk/core/common/ClientException.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.google.common.annotations.Beta;
44
import javax.annotation.Nullable;
5+
import lombok.AccessLevel;
56
import lombok.Getter;
67
import lombok.Setter;
78
import lombok.experimental.StandardException;
@@ -20,7 +21,7 @@ public class ClientException extends RuntimeException {
2021
* used to extract more detailed error information.
2122
*/
2223
@Nullable
23-
@Getter(onMethod_ = @Beta)
24-
@Setter(onMethod_ = @Beta)
24+
@Getter(onMethod_ = @Beta, value = AccessLevel.PROTECTED)
25+
@Setter(onMethod_ = @Beta, value = AccessLevel.PROTECTED)
2526
ClientError clientError;
2627
}

foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClientException.java

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.google.common.annotations.Beta;
44
import com.sap.ai.sdk.core.common.ClientException;
5+
import com.sap.ai.sdk.foundationmodels.openai.generated.model.ErrorResponse;
56
import javax.annotation.Nonnull;
67
import javax.annotation.Nullable;
78
import lombok.experimental.StandardException;
@@ -14,10 +15,18 @@ public class OpenAiClientException extends ClientException {
1415
setClientError(clientError);
1516
}
1617

18+
/**
19+
* Retrieves the {@link ErrorResponse} from the OpenAI service, if available.
20+
*
21+
* @return The {@link ErrorResponse} object, or {@code null} if not available.
22+
*/
1723
@Beta
1824
@Nullable
19-
@Override
20-
public OpenAiError getClientError() {
21-
return (OpenAiError) super.getClientError();
25+
public ErrorResponse getErrorResponse() {
26+
final var clientError = super.getClientError();
27+
if (clientError instanceof OpenAiError openAiError) {
28+
return openAiError.getErrorResponse();
29+
}
30+
return null;
2231
}
2332
}

foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiError.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
@AllArgsConstructor(onConstructor = @__({@JsonCreator}), access = AccessLevel.PROTECTED)
2020
public class OpenAiError implements ClientError {
2121
/** The original error response from the OpenAI API. */
22-
ErrorResponse originalResponse;
22+
ErrorResponse errorResponse;
2323

2424
/**
2525
* Gets the error message from the contained original response.
@@ -28,6 +28,6 @@ public class OpenAiError implements ClientError {
2828
*/
2929
@Nonnull
3030
public String getMessage() {
31-
return originalResponse.getError().getMessage();
31+
return errorResponse.getError().getMessage();
3232
}
3333
}

orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClientException.java

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,19 @@ public class OrchestrationClientException extends ClientException {
1818
setClientError(clientError);
1919
}
2020

21+
/**
22+
* Retrieves the {@link ErrorResponse} from the orchestration service, if available.
23+
*
24+
* @return The {@link ErrorResponse} object, or {@code null} if not available.
25+
*/
2126
@Beta
2227
@Nullable
23-
@Override
24-
public OrchestrationError getClientError() {
25-
return (OrchestrationError) super.getClientError();
28+
public ErrorResponse getErrorResponse() {
29+
final var clientError = super.getClientError();
30+
if (clientError instanceof OrchestrationError orchestrationError) {
31+
return orchestrationError.getErrorResponse();
32+
}
33+
return null;
2634
}
2735

2836
/**
@@ -33,9 +41,6 @@ public OrchestrationError getClientError() {
3341
@Beta
3442
@Nullable
3543
public Integer getStatusCode() {
36-
return Optional.ofNullable(getClientError())
37-
.map(OrchestrationError::getOriginalResponse)
38-
.map(ErrorResponse::getCode)
39-
.orElse(null);
44+
return Optional.ofNullable(getErrorResponse()).map(ErrorResponse::getCode).orElse(null);
4045
}
4146
}

orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationError.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
@Value
1919
@Beta
2020
public class OrchestrationError implements ClientError {
21-
ErrorResponse originalResponse;
21+
ErrorResponse errorResponse;
2222

2323
/**
2424
* Gets the error message from the contained original response.
@@ -27,8 +27,8 @@ public class OrchestrationError implements ClientError {
2727
*/
2828
@Nonnull
2929
public String getMessage() {
30-
return originalResponse.getCode() == 500
31-
? originalResponse.getMessage() + " located in " + originalResponse.getLocation()
32-
: originalResponse.getMessage();
30+
return errorResponse.getCode() == 500
31+
? errorResponse.getMessage() + " located in " + errorResponse.getLocation()
32+
: errorResponse.getMessage();
3333
}
3434
}

orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationExceptionFactory.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public OrchestrationClientException buildFromClientError(
3636
@Nonnull
3737
private Map<String, Object> extractInputFilterDetails(@Nonnull final OrchestrationError error) {
3838

39-
return Optional.of(error.getOriginalResponse())
39+
return Optional.of(error.getErrorResponse())
4040
.flatMap(response -> Optional.ofNullable(response.getModuleResults()))
4141
.flatMap(moduleResults -> Optional.ofNullable(moduleResults.getInputFiltering()))
4242
.flatMap(inputFiltering -> Optional.ofNullable(inputFiltering.getData()))
Lines changed: 66 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,49 @@
11
package com.sap.ai.sdk.orchestration;
22

3+
import static com.sap.ai.sdk.orchestration.OrchestrationJacksonConfiguration.getOrchestrationObjectMapper;
4+
35
import com.google.common.annotations.Beta;
6+
import com.sap.ai.sdk.orchestration.model.AzureContentSafetyInput;
7+
import com.sap.ai.sdk.orchestration.model.AzureContentSafetyOutput;
8+
import com.sap.ai.sdk.orchestration.model.LlamaGuard38b;
49
import java.util.Map;
10+
import java.util.Optional;
511
import javax.annotation.Nonnull;
12+
import javax.annotation.Nullable;
613
import lombok.AccessLevel;
714
import lombok.Getter;
815
import lombok.experimental.StandardException;
916

10-
/**
11-
* Exception thrown when an error occurs during orchestration filtering.
12-
*
13-
* <p>This exception serves as the base for more specific filter-related exceptions in the
14-
* orchestration process.
15-
*/
17+
/** Base exception for errors occurring during orchestration filtering. */
1618
@Beta
1719
@StandardException(access = AccessLevel.PRIVATE)
1820
public class OrchestrationFilterException extends OrchestrationClientException {
1921

20-
/** Details about the filter that caused the exception. */
22+
/** Details about the filters that caused the exception. */
2123
@Getter @Nonnull protected Map<String, Object> filterDetails = Map.of();
2224

23-
/** Exception thrown when an error occurs during input filtering in orchestration. */
25+
/**
26+
* Retrieves LlamaGuard 3.8b details from {@code filterDetails}, if present.
27+
*
28+
* @return The LlamaGuard38b object, or {@code null} if not found or conversion fails.
29+
* @throws IllegalArgumentException if the conversion of filter details to {@link LlamaGuard38b}
30+
* fails due to invalid content.
31+
*/
32+
@Nullable
33+
public LlamaGuard38b getLlamaGuard38b() {
34+
return Optional.ofNullable(filterDetails.get("llama_guard_3_8b"))
35+
.map(obj -> getOrchestrationObjectMapper().convertValue(obj, LlamaGuard38b.class))
36+
.orElse(null);
37+
}
38+
39+
/** Exception thrown when an error occurs during input filtering. */
2440
public static class OrchestrationInputFilterException extends OrchestrationFilterException {
2541
/**
2642
* Constructs a new OrchestrationInputFilterException.
2743
*
28-
* @param message the detail message
29-
* @param clientError the specific client error
30-
* @param filterDetails details about the filter that caused the exception
44+
* @param message The detail message.
45+
* @param clientError The specific client error.
46+
* @param filterDetails Details about the filter that caused the exception.
3147
*/
3248
OrchestrationInputFilterException(
3349
@Nonnull final String message,
@@ -37,20 +53,56 @@ public static class OrchestrationInputFilterException extends OrchestrationFilte
3753
setClientError(clientError);
3854
this.filterDetails = filterDetails;
3955
}
56+
57+
/**
58+
* Retrieves Azure Content Safety input details from {@code filterDetails}, if present.
59+
*
60+
* @return The AzureContentSafetyInput object, or {@code null} if not found or conversion fails.
61+
* @throws IllegalArgumentException if the conversion of filter details to {@link
62+
* AzureContentSafetyInput} fails due to invalid content.
63+
*/
64+
@Nullable
65+
public AzureContentSafetyInput getAzureContentSafetyInput() {
66+
return Optional.ofNullable(filterDetails.get("azure_content_safety"))
67+
.map(
68+
obj ->
69+
getOrchestrationObjectMapper().convertValue(obj, AzureContentSafetyInput.class))
70+
.orElse(null);
71+
}
4072
}
4173

42-
/** Exception thrown output filtering in orchestration when finish reason is content filter */
74+
/**
75+
* Exception thrown when an error occurs during output filtering, specifically when the finish
76+
* reason is a content filter.
77+
*/
4378
public static class OrchestrationOutputFilterException extends OrchestrationFilterException {
4479
/**
4580
* Constructs a new OrchestrationOutputFilterException.
4681
*
47-
* @param message the detail message
48-
* @param filterDetails details about the filter that caused the exception
82+
* @param message The detail message.
83+
* @param filterDetails Details about the filter that caused the exception.
4984
*/
5085
OrchestrationOutputFilterException(
5186
@Nonnull final String message, @Nonnull final Map<String, Object> filterDetails) {
5287
super(message);
5388
this.filterDetails = filterDetails;
5489
}
90+
91+
/**
92+
* Retrieves Azure Content Safety output details from {@code filterDetails}, if present.
93+
*
94+
* @return The AzureContentSafetyOutput object, or {@code null} if not found or conversion
95+
* fails.
96+
* @throws IllegalArgumentException if the conversion of filter details to {@link
97+
* AzureContentSafetyOutput} fails due to invalid content.
98+
*/
99+
@Nullable
100+
public AzureContentSafetyOutput getAzureContentSafetyOutput() {
101+
return Optional.ofNullable(filterDetails.get("azure_content_safety"))
102+
.map(
103+
obj ->
104+
getOrchestrationObjectMapper().convertValue(obj, AzureContentSafetyOutput.class))
105+
.orElse(null);
106+
}
55107
}
56108
}

orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationUnitTest.java

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
import static com.sap.ai.sdk.orchestration.OrchestrationAiModel.GPT_4O;
2121
import static com.sap.ai.sdk.orchestration.OrchestrationAiModel.GPT_4O_MINI;
2222
import static com.sap.ai.sdk.orchestration.OrchestrationAiModel.Parameter.*;
23+
import static com.sap.ai.sdk.orchestration.model.AzureThreshold.NUMBER_0;
24+
import static com.sap.ai.sdk.orchestration.model.AzureThreshold.NUMBER_6;
2325
import static com.sap.ai.sdk.orchestration.model.ResponseChatMessage.RoleEnum.ASSISTANT;
2426
import static com.sap.ai.sdk.orchestration.model.UserChatMessage.RoleEnum.USER;
2527
import static org.apache.hc.core5.http.HttpStatus.SC_BAD_REQUEST;
@@ -57,6 +59,7 @@
5759
import com.sap.ai.sdk.orchestration.model.EmbeddingsPostRequest;
5860
import com.sap.ai.sdk.orchestration.model.EmbeddingsPostResponse;
5961
import com.sap.ai.sdk.orchestration.model.EmbeddingsResponse;
62+
import com.sap.ai.sdk.orchestration.model.ErrorResponse;
6063
import com.sap.ai.sdk.orchestration.model.GenericModuleResult;
6164
import com.sap.ai.sdk.orchestration.model.GroundingFilterSearchConfiguration;
6265
import com.sap.ai.sdk.orchestration.model.GroundingModuleConfig;
@@ -400,7 +403,6 @@ void filteringLoose() throws IOException {
400403

401404
@Test
402405
void inputFilteringStrict() {
403-
404406
stubFor(
405407
post(anyUrl())
406408
.willReturn(
@@ -409,14 +411,16 @@ void inputFilteringStrict() {
409411
.withHeader("Content-Type", "application/json")
410412
.withStatus(SC_BAD_REQUEST)));
411413

412-
final var filter =
414+
final var azureFilter =
413415
new AzureContentFilter()
414416
.hate(ALLOW_SAFE)
415417
.selfHarm(ALLOW_SAFE)
416418
.sexual(ALLOW_SAFE)
417419
.violence(ALLOW_SAFE);
418420

419-
final var configWithFilter = config.withInputFiltering(filter);
421+
final var llamaFilter =
422+
new LlamaGuardFilter().config(LlamaGuard38b.create().violentCrimes(true));
423+
final var configWithFilter = config.withInputFiltering(azureFilter, llamaFilter);
420424

421425
try {
422426
client.chatCompletion(prompt, configWithFilter);
@@ -429,24 +433,47 @@ void inputFilteringStrict() {
429433
.isEqualTo(
430434
Map.of(
431435
"azure_content_safety",
432-
Map.of("Hate", 0, "SelfHarm", 0, "Sexual", 0, "Violence", 0)));
433-
assertThat(e.getClientError()).isNotNull();
434-
assertThat(e.getClientError()).isInstanceOf(OrchestrationError.class);
436+
Map.of(
437+
"Hate", 6,
438+
"SelfHarm", 0,
439+
"Sexual", 0,
440+
"Violence", 6,
441+
"userPromptAnalysis", Map.of("attackDetected", false)),
442+
"llama_guard_3_8b", Map.of("violent_crimes", true)));
443+
444+
final var errorResponse = e.getErrorResponse();
445+
assertThat(errorResponse).isNotNull();
446+
assertThat(errorResponse).isInstanceOf(ErrorResponse.class);
447+
assertThat(errorResponse.getCode()).isEqualTo(SC_BAD_REQUEST);
448+
assertThat(errorResponse.getMessage())
449+
.isEqualTo(
450+
"400 - Filtering Module - Input Filter: Prompt filtered due to safety violations. Please modify the prompt and try again.");
451+
452+
assertThat(e.getAzureContentSafetyInput()).isNotNull();
453+
assertThat(e.getAzureContentSafetyInput().getHate()).isEqualTo(NUMBER_6);
454+
assertThat(e.getAzureContentSafetyInput().getSelfHarm()).isEqualTo(NUMBER_0);
455+
assertThat(e.getAzureContentSafetyInput().getSexual()).isEqualTo(NUMBER_0);
456+
assertThat(e.getAzureContentSafetyInput().getViolence()).isEqualTo(NUMBER_6);
457+
458+
assertThat(e.getLlamaGuard38b()).isNotNull();
459+
assertThat(e.getLlamaGuard38b().isViolentCrimes()).isTrue();
435460
}
436461
}
437462

438463
@Test
439464
void outputFilteringStrict() {
440465
stubFor(post(anyUrl()).willReturn(aResponse().withBodyFile("outputFilteringStrict.json")));
441466

442-
final var filter =
467+
final var azureFilter =
443468
new AzureContentFilter()
444469
.hate(ALLOW_SAFE)
445470
.selfHarm(ALLOW_SAFE)
446471
.sexual(ALLOW_SAFE)
447472
.violence(ALLOW_SAFE);
448473

449-
final var configWithFilter = config.withOutputFiltering(filter);
474+
final var llamaFilter =
475+
new LlamaGuardFilter().config(LlamaGuard38b.create().violentCrimes(true));
476+
final var configWithFilter = config.withOutputFiltering(azureFilter, llamaFilter);
450477

451478
try {
452479
client.chatCompletion(prompt, configWithFilter).getContent();
@@ -455,11 +482,25 @@ void outputFilteringStrict() {
455482
assertThat(e.getFilterDetails())
456483
.isEqualTo(
457484
Map.of(
458-
"index",
459-
0,
485+
"index", 0,
460486
"azure_content_safety",
461-
Map.of("Hate", 0, "SelfHarm", 0, "Sexual", 0, "Violence", 4)));
462-
assertThat(e.getClientError()).isNull();
487+
Map.of(
488+
"Hate", 6,
489+
"SelfHarm", 0,
490+
"Sexual", 0,
491+
"Violence", 6),
492+
"llama_guard_3_8b", Map.of("violent_crimes", true)));
493+
assertThat(e.getErrorResponse()).isNull();
494+
assertThat(e.getStatusCode()).isNull();
495+
496+
assertThat(e.getAzureContentSafetyOutput()).isNotNull();
497+
assertThat(e.getAzureContentSafetyOutput().getHate()).isEqualTo(NUMBER_6);
498+
assertThat(e.getAzureContentSafetyOutput().getSelfHarm()).isEqualTo(NUMBER_0);
499+
assertThat(e.getAzureContentSafetyOutput().getSexual()).isEqualTo(NUMBER_0);
500+
assertThat(e.getAzureContentSafetyOutput().getViolence()).isEqualTo(NUMBER_6);
501+
502+
assertThat(e.getLlamaGuard38b()).isNotNull();
503+
assertThat(e.getLlamaGuard38b().isViolentCrimes()).isTrue();
463504
}
464505
}
465506

0 commit comments

Comments
 (0)