Skip to content

Commit 03d92af

Browse files
asEntity + tests
1 parent d054403 commit 03d92af

File tree

4 files changed

+143
-16
lines changed

4 files changed

+143
-16
lines changed

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

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import com.fasterxml.jackson.core.JsonProcessingException;
66
import com.fasterxml.jackson.databind.ObjectMapper;
7+
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException;
78
import com.sap.ai.sdk.orchestration.model.AssistantChatMessage;
89
import com.sap.ai.sdk.orchestration.model.ChatMessage;
910
import com.sap.ai.sdk.orchestration.model.ChatMessageContent;
@@ -119,26 +120,36 @@ public LLMChoice getChoice() {
119120
* @param type the class type to deserialize the JSON content into.
120121
* @return the deserialized entity of type T.
121122
* @param <T> the type of the entity to deserialize to.
123+
* @throws OrchestrationClientException if the model refused to answer the question or if the
124+
* content
122125
*/
123126
@Nonnull
124-
public <T> T entity(@Nonnull final Class<T> type) throws OrchestrationClientException {
127+
public <T> T asEntity(@Nonnull final Class<T> type) throws OrchestrationClientException {
125128
final String refusal =
126129
((LLMModuleResultSynchronous) getOriginalResponse().getOrchestrationResult())
127130
.getChoices()
128131
.get(0)
129132
.getMessage()
130133
.getRefusal();
131134
if (refusal != null) {
132-
// https://platform.openai.com/docs/guides/structured-outputs?api-mode=responses#refusals
133135
throw new OrchestrationClientException(
134136
"The model refused to answer the question: " + refusal);
135137
}
136138
try {
137139
return new ObjectMapper().readValue(getContent(), type);
140+
} catch (InvalidDefinitionException e) {
141+
throw new OrchestrationClientException(
142+
"Failed to deserialize the JSON content. Please make sure to use the correct class and that the class has a no-args constructor or is static: "
143+
+ e.getMessage()
144+
+ "\nJSON content: "
145+
+ getContent(),
146+
e);
138147
} catch (JsonProcessingException e) {
139148
throw new OrchestrationClientException(
140-
"Failed to deserialize the JSON content, please configure an OrchestrationTemplate with format set to JSON schema into your OrchestrationModuleConfig"
141-
+ e.getMessage(),
149+
"Failed to deserialize the JSON content. Please configure an OrchestrationTemplate with format set to JSON schema into your OrchestrationModuleConfig: "
150+
+ e.getMessage()
151+
+ "\nJSON content: "
152+
+ getContent(),
142153
e);
143154
}
144155
}

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

Lines changed: 68 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -832,6 +832,15 @@ void testMultiMessage() throws IOException {
832832
}
833833
}
834834

835+
// Example class
836+
static class Translation {
837+
@JsonProperty(required = true)
838+
private String language;
839+
840+
@JsonProperty(required = true)
841+
private String translation;
842+
}
843+
835844
@Test
836845
void testResponseFormatJsonSchema() throws IOException {
837846
stubFor(
@@ -841,16 +850,8 @@ void testResponseFormatJsonSchema() throws IOException {
841850
.withBodyFile("jsonSchemaResponse.json")
842851
.withHeader("Content-Type", "application/json")));
843852

844-
var config = new OrchestrationModuleConfig().withLlmConfig(GPT_4O_MINI);
845-
846-
// Example class
847-
class Translation {
848-
@JsonProperty(required = true)
849-
private String language;
853+
val config = new OrchestrationModuleConfig().withLlmConfig(GPT_4O_MINI);
850854

851-
@JsonProperty(required = true)
852-
private String translation;
853-
}
854855
val schema =
855856
ResponseJsonSchema.fromType(Translation.class)
856857
.withDescription("Output schema for language translation.")
@@ -863,15 +864,71 @@ class Translation {
863864
Message.user("Whats 'apple' in German?"),
864865
Message.system("You are a language translator."));
865866

866-
final var message = client.chatCompletion(prompt, configWithResponseSchema).getContent();
867-
assertThat(message).isEqualTo("{\"translation\":\"Apfel\",\"language\":\"German\"}");
867+
val response = client.chatCompletion(prompt, configWithResponseSchema);
868+
assertThat(response.getContent())
869+
.isEqualTo("{\"translation\":\"Apfel\",\"language\":\"German\"}");
870+
871+
// ------------- wrong use -------------
872+
class TranslationNotStaticNoConstructor {
873+
@JsonProperty(required = true)
874+
private String language;
875+
876+
@JsonProperty(required = true)
877+
private String translation;
878+
}
879+
assertThatThrownBy(() -> response.asEntity(TranslationNotStaticNoConstructor.class))
880+
.isInstanceOf(OrchestrationClientException.class)
881+
.hasMessageContaining(
882+
"Please make sure to use the correct class and that the class has a no-args constructor or is static")
883+
.hasMessageContaining("JSON content: {\"translation\":\"Apfel\",\"language\":\"German\"}");
884+
885+
// ------------- good use -------------
886+
Translation translation = response.asEntity(Translation.class);
887+
assertThat(translation.language).isEqualTo("German");
888+
assertThat(translation.translation).isEqualTo("Apfel");
868889

869890
try (var requestInputStream = fileLoader.apply("jsonSchemaRequest.json")) {
870891
final String request = new String(requestInputStream.readAllBytes());
871892
verify(postRequestedFor(anyUrl()).withRequestBody(equalToJson(request)));
872893
}
873894
}
874895

896+
@Test
897+
void testJsonSchemaWrongConfig() {
898+
stubFor(
899+
post(anyUrl())
900+
.willReturn(
901+
aResponse()
902+
.withBodyFile("templatingResponse.json")
903+
.withHeader("Content-Type", "application/json")));
904+
905+
val response = client.chatCompletion(prompt, config);
906+
907+
// no config and wrong response
908+
assertThatThrownBy(() -> response.asEntity(Translation.class))
909+
.isInstanceOf(OrchestrationClientException.class)
910+
.hasMessageContaining(
911+
"Please configure an OrchestrationTemplate with format set to JSON schema into your OrchestrationModuleConfig")
912+
.hasMessageContaining("JSON content: Le service d'orchestration fonctionne!");
913+
}
914+
915+
@Test
916+
void testJsonSchemaRefusal() {
917+
stubFor(
918+
post(anyUrl())
919+
.willReturn(
920+
aResponse()
921+
.withBodyFile("errorJsonSchemaResponse.json")
922+
.withHeader("Content-Type", "application/json")));
923+
924+
val response = client.chatCompletion(prompt, config);
925+
926+
assertThatThrownBy(() -> response.asEntity(Translation.class))
927+
.isInstanceOf(OrchestrationClientException.class)
928+
.hasMessageContaining(
929+
"The model refused to answer the question: I'm sorry, I cannot assist with that request.");
930+
}
931+
875932
@Test
876933
void testResponseFormatJsonObject() throws IOException {
877934
stubFor(
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
{
2+
"request_id": "0759f249-a261-4cb7-99d5-ea6eae2c141c",
3+
"module_results": {
4+
"templating": [
5+
{
6+
"role": "user",
7+
"content": "Whats 'apple' in German?"
8+
},
9+
{
10+
"role": "system",
11+
"content": "You are a language translator."
12+
}
13+
],
14+
"llm": {
15+
"id": "chatcmpl-AzmCw5QZBi6zQkJPQhvsyjW4Fe90c",
16+
"object": "chat.completion",
17+
"created": 1739286682,
18+
"model": "gpt-4o-mini-2024-07-18",
19+
"system_fingerprint": "fp_f3927aa00d",
20+
"choices": [
21+
{
22+
"index": 0,
23+
"message": {
24+
"type": "refusal",
25+
"refusal": "I'm sorry, I cannot assist with that request."
26+
},
27+
"finish_reason": "stop"
28+
}
29+
],
30+
"usage": {
31+
"completion_tokens": 10,
32+
"prompt_tokens": 68,
33+
"total_tokens": 78
34+
}
35+
}
36+
},
37+
"orchestration_result": {
38+
"id": "chatcmpl-AzmCw5QZBi6zQkJPQhvsyjW4Fe90c",
39+
"object": "chat.completion",
40+
"created": 1739286682,
41+
"model": "gpt-4o-mini-2024-07-18",
42+
"system_fingerprint": "fp_f3927aa00d",
43+
"choices": [
44+
{
45+
"index": 0,
46+
"message": {
47+
"type": "refusal",
48+
"refusal": "I'm sorry, I cannot assist with that request."
49+
},
50+
"finish_reason": "stop"
51+
}
52+
],
53+
"usage": {
54+
"completion_tokens": 10,
55+
"prompt_tokens": 68,
56+
"total_tokens": 78
57+
}
58+
}
59+
}

sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/OrchestrationService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,7 @@ public Translation responseFormatJsonSchema(@Nonnull final String word) {
416416
Message.user("Whats '%s' in German?".formatted(word)),
417417
Message.system("You are a language translator."));
418418

419-
return client.chatCompletion(prompt, configWithResponseSchema).entity(Translation.class);
419+
return client.chatCompletion(prompt, configWithResponseSchema).asEntity(Translation.class);
420420
}
421421

422422
/**

0 commit comments

Comments
 (0)