Skip to content

Commit d1bce8c

Browse files
Refactor OpenAiController in sample app (#245)
* Refactor OpenAiController * Fix tests * Small fix * Switch order in equality check * Services don't use Spring classes * Formatting * Minimal change to retrigger CI check --------- Co-authored-by: Jonas Israel <[email protected]> Co-authored-by: SAP Cloud SDK Bot <[email protected]>
1 parent a764bff commit d1bce8c

File tree

5 files changed

+271
-129
lines changed

5 files changed

+271
-129
lines changed
Lines changed: 83 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,51 @@
11
package com.sap.ai.sdk.app.controllers;
22

3-
import static com.sap.ai.sdk.foundationmodels.openai.OpenAiModel.GPT_35_TURBO;
4-
import static com.sap.ai.sdk.foundationmodels.openai.OpenAiModel.GPT_4O;
5-
import static com.sap.ai.sdk.foundationmodels.openai.OpenAiModel.TEXT_EMBEDDING_ADA_002;
6-
import static com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatCompletionTool.ToolType.FUNCTION;
7-
3+
import com.fasterxml.jackson.annotation.JsonAutoDetect;
4+
import com.fasterxml.jackson.annotation.PropertyAccessor;
85
import com.fasterxml.jackson.core.JsonProcessingException;
96
import com.fasterxml.jackson.databind.ObjectMapper;
10-
import com.sap.ai.sdk.core.AiCoreService;
11-
import com.sap.ai.sdk.foundationmodels.openai.OpenAiClient;
12-
import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatCompletionFunction;
7+
import com.sap.ai.sdk.app.services.OpenAiService;
138
import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatCompletionOutput;
14-
import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatCompletionParameters;
15-
import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatCompletionTool;
16-
import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatMessage.OpenAiChatUserMessage;
17-
import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatMessage.OpenAiChatUserMessage.ImageDetailLevel;
18-
import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiEmbeddingOutput;
19-
import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiEmbeddingParameters;
209
import com.sap.cloud.sdk.cloudplatform.thread.ThreadContextExecutors;
2110
import java.io.IOException;
2211
import java.util.Arrays;
23-
import java.util.List;
24-
import java.util.Map;
2512
import javax.annotation.Nonnull;
2613
import lombok.extern.slf4j.Slf4j;
14+
import org.springframework.beans.factory.annotation.Autowired;
2715
import org.springframework.http.MediaType;
2816
import org.springframework.http.ResponseEntity;
2917
import org.springframework.web.bind.annotation.GetMapping;
3018
import org.springframework.web.bind.annotation.PathVariable;
19+
import org.springframework.web.bind.annotation.RequestHeader;
3120
import org.springframework.web.bind.annotation.RestController;
3221
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;
3322

3423
/** Endpoints for OpenAI operations */
3524
@Slf4j
3625
@RestController
26+
@SuppressWarnings("unused")
3727
public class OpenAiController {
28+
@Autowired private OpenAiService service;
29+
private final ObjectMapper mapper =
30+
new ObjectMapper().setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
31+
3832
/**
3933
* Chat request to OpenAI
4034
*
41-
* @return the assistant message response
35+
* @return a ResponseEntity with the response content
4236
*/
4337
@GetMapping("/chatCompletion")
4438
@Nonnull
45-
OpenAiChatCompletionOutput chatCompletion() {
46-
return OpenAiClient.forModel(GPT_35_TURBO).chatCompletion("Who is the prettiest");
39+
ResponseEntity<String> chatCompletion(
40+
@RequestHeader(value = "accept", required = false) final String accept)
41+
throws JsonProcessingException {
42+
final var response = service.chatCompletion("Who is the prettiest");
43+
if ("application/json".equals(accept)) {
44+
return ResponseEntity.ok()
45+
.contentType(MediaType.APPLICATION_JSON)
46+
.body(mapper.writeValueAsString(response));
47+
}
48+
return ResponseEntity.ok(response.getContent());
4749
}
4850

4951
/**
@@ -55,14 +57,9 @@ OpenAiChatCompletionOutput chatCompletion() {
5557
@GetMapping("/streamChatCompletionDeltas")
5658
@Nonnull
5759
ResponseEntity<ResponseBodyEmitter> streamChatCompletionDeltas() {
58-
final var msg = "Can you give me the first 100 numbers of the Fibonacci sequence?";
59-
final var request =
60-
new OpenAiChatCompletionParameters().addMessages(new OpenAiChatUserMessage().addText(msg));
61-
62-
final var stream = OpenAiClient.forModel(GPT_35_TURBO).streamChatCompletionDeltas(request);
63-
60+
final var message = "Can you give me the first 100 numbers of the Fibonacci sequence?";
61+
final var stream = service.streamChatCompletionDeltas(message);
6462
final var emitter = new ResponseBodyEmitter();
65-
6663
final Runnable consumeStream =
6764
() -> {
6865
final var totalOutput = new OpenAiChatCompletionOutput();
@@ -76,21 +73,12 @@ ResponseEntity<ResponseBodyEmitter> streamChatCompletionDeltas() {
7673
emitter.complete();
7774
}
7875
};
79-
8076
ThreadContextExecutors.getExecutor().execute(consumeStream);
8177

8278
// TEXT_EVENT_STREAM allows the browser to display the content as it is streamed
8379
return ResponseEntity.ok().contentType(MediaType.TEXT_EVENT_STREAM).body(emitter);
8480
}
8581

86-
private static String objectToJson(@Nonnull final Object obj) {
87-
try {
88-
return new ObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(obj);
89-
} catch (final JsonProcessingException ignored) {
90-
return "Could not parse object to JSON";
91-
}
92-
}
93-
9482
/**
9583
* Asynchronous stream of an OpenAI chat request
9684
*
@@ -100,12 +88,8 @@ private static String objectToJson(@Nonnull final Object obj) {
10088
@GetMapping("/streamChatCompletion")
10189
@Nonnull
10290
ResponseEntity<ResponseBodyEmitter> streamChatCompletion() {
103-
final var stream =
104-
OpenAiClient.forModel(GPT_35_TURBO)
105-
.withSystemPrompt("Be a good, honest AI and answer the following question:")
106-
.streamChatCompletion(
107-
"Can you give me the first 100 numbers of the Fibonacci sequence?");
108-
91+
final var message = "Can you give me the first 100 numbers of the Fibonacci sequence?";
92+
final var stream = service.streamChatCompletion(message);
10993
final var emitter = new ResponseBodyEmitter();
11094

11195
final Runnable consumeStream =
@@ -116,7 +100,6 @@ ResponseEntity<ResponseBodyEmitter> streamChatCompletion() {
116100
emitter.complete();
117101
}
118102
};
119-
120103
ThreadContextExecutors.getExecutor().execute(consumeStream);
121104

122105
// TEXT_EVENT_STREAM allows the browser to display the content as it is streamed
@@ -138,80 +121,94 @@ public static void send(@Nonnull final ResponseBodyEmitter emitter, @Nonnull fin
138121
}
139122
}
140123

124+
/**
125+
* Convert an object to JSON
126+
*
127+
* @param obj The object to convert
128+
* @return The JSON representation of the object
129+
*/
130+
private static String objectToJson(@Nonnull final Object obj) {
131+
try {
132+
return new ObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(obj);
133+
} catch (final JsonProcessingException ignored) {
134+
return "Could not parse object to JSON";
135+
}
136+
}
137+
141138
/**
142139
* Chat request to OpenAI with an image
143140
*
144-
* @return the assistant message response
141+
* @return a ResponseEntity with the response content
145142
*/
146143
@GetMapping("/chatCompletionImage")
147144
@Nonnull
148-
OpenAiChatCompletionOutput chatCompletionImage() {
149-
final var request =
150-
new OpenAiChatCompletionParameters()
151-
.addMessages(
152-
new OpenAiChatUserMessage()
153-
.addText("Describe the following image.")
154-
.addImage(
155-
"https://upload.wikimedia.org/wikipedia/commons/thumb/5/59/SAP_2011_logo.svg/440px-SAP_2011_logo.svg.png",
156-
ImageDetailLevel.HIGH));
157-
158-
return OpenAiClient.forModel(GPT_4O).chatCompletion(request);
145+
ResponseEntity<String> chatCompletionImage(
146+
@RequestHeader(value = "accept", required = false) final String accept)
147+
throws JsonProcessingException {
148+
final var response =
149+
service.chatCompletionImage(
150+
"https://upload.wikimedia.org/wikipedia/commons/thumb/5/59/SAP_2011_logo.svg/440px-SAP_2011_logo.svg.png");
151+
if ("application/json".equals(accept)) {
152+
return ResponseEntity.ok()
153+
.contentType(MediaType.APPLICATION_JSON)
154+
.body(mapper.writeValueAsString(response));
155+
}
156+
return ResponseEntity.ok(response.getContent());
159157
}
160158

161159
/**
162160
* Chat request to OpenAI with a tool.
163161
*
164-
* @return the assistant message response
162+
* @return a ResponseEntity with the response content
165163
*/
166164
@GetMapping("/chatCompletionTool")
167165
@Nonnull
168-
OpenAiChatCompletionOutput chatCompletionTools() {
169-
final var question =
170-
"A pair of rabbits is placed in a field. Each month, every pair produces one new pair, starting from the second month. How many rabbits will there be after 12 months?";
171-
final var par = Map.of("type", "object", "properties", Map.of("N", Map.of("type", "integer")));
172-
final var function =
173-
new OpenAiChatCompletionFunction()
174-
.setName("fibonacci")
175-
.setDescription("Calculate the Fibonacci number for given sequence index.")
176-
.setParameters(par);
177-
final var tool = new OpenAiChatCompletionTool().setType(FUNCTION).setFunction(function);
178-
final var request =
179-
new OpenAiChatCompletionParameters()
180-
.addMessages(new OpenAiChatUserMessage().addText(question))
181-
.setTools(List.of(tool))
182-
.setToolChoiceFunction("fibonacci");
183-
184-
return OpenAiClient.forModel(GPT_35_TURBO).chatCompletion(request);
166+
ResponseEntity<String> chatCompletionTools(
167+
@RequestHeader(value = "accept", required = false) final String accept)
168+
throws JsonProcessingException {
169+
final var response =
170+
service.chatCompletionTools("Calculate the Fibonacci number for given sequence index.");
171+
if ("application/json".equals(accept)) {
172+
return ResponseEntity.ok()
173+
.contentType(MediaType.APPLICATION_JSON)
174+
.body(mapper.writeValueAsString(response));
175+
}
176+
return ResponseEntity.ok(response.getContent());
185177
}
186178

187179
/**
188180
* Get the embedding of a text
189181
*
190-
* @return the embedding response
182+
* @return a ResponseEntity with the response content
191183
*/
192184
@GetMapping("/embedding")
193185
@Nonnull
194-
OpenAiEmbeddingOutput embedding() {
195-
final var request = new OpenAiEmbeddingParameters().setInput("Hello World");
196-
197-
return OpenAiClient.forModel(TEXT_EMBEDDING_ADA_002).embedding(request);
186+
ResponseEntity<String> embedding() throws JsonProcessingException {
187+
final var response = service.embedding("Hello world");
188+
return ResponseEntity.ok()
189+
.contentType(MediaType.APPLICATION_JSON)
190+
.body(mapper.writeValueAsString(response));
198191
}
199192

200193
/**
201194
* Chat request to OpenAI filtering by resource group
202195
*
203196
* @param resourceGroup The resource group to use
204-
* @return the assistant message response
197+
* @return a ResponseEntity with the response content
205198
*/
206199
@GetMapping("/chatCompletion/{resourceGroup}")
207200
@Nonnull
208-
public static OpenAiChatCompletionOutput chatCompletionWithResource(
209-
@Nonnull @PathVariable("resourceGroup") final String resourceGroup) {
210-
211-
final var destination =
212-
new AiCoreService().getInferenceDestination(resourceGroup).forModel(GPT_4O);
213-
214-
return OpenAiClient.withCustomDestination(destination)
215-
.chatCompletion("Where is the nearest coffee shop?");
201+
ResponseEntity<String> chatCompletionWithResource(
202+
@RequestHeader(value = "accept", required = false) final String accept,
203+
@Nonnull @PathVariable("resourceGroup") final String resourceGroup)
204+
throws JsonProcessingException {
205+
final var response =
206+
service.chatCompletionWithResource(resourceGroup, "Where is the nearest coffee shop?");
207+
if ("application/json".equals(accept)) {
208+
return ResponseEntity.ok()
209+
.contentType(MediaType.APPLICATION_JSON)
210+
.body(mapper.writeValueAsString(response));
211+
}
212+
return ResponseEntity.ok(response.getContent());
216213
}
217214
}

sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
package com.sap.ai.sdk.app.controllers;
22

3+
import static com.sap.ai.sdk.app.controllers.OpenAiController.send;
4+
35
import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
46
import com.fasterxml.jackson.annotation.PropertyAccessor;
57
import com.fasterxml.jackson.core.JsonProcessingException;
68
import com.fasterxml.jackson.databind.ObjectMapper;
79
import com.sap.ai.sdk.app.services.OrchestrationService;
810
import com.sap.ai.sdk.orchestration.AzureFilterThreshold;
911
import com.sap.ai.sdk.orchestration.model.DPIEntities;
12+
import com.sap.cloud.sdk.cloudplatform.thread.ThreadContextExecutors;
1013
import javax.annotation.Nonnull;
1114
import lombok.extern.slf4j.Slf4j;
1215
import org.springframework.beans.factory.annotation.Autowired;
@@ -40,7 +43,7 @@ ResponseEntity<String> completion(
4043
@RequestHeader(value = "accept", required = false) final String accept)
4144
throws JsonProcessingException {
4245
final var response = service.completion("HelloWorld!");
43-
if (accept.equals("application/json")) {
46+
if ("application/json".equals(accept)) {
4447
return ResponseEntity.ok()
4548
.contentType(MediaType.APPLICATION_JSON)
4649
.body(mapper.writeValueAsString(response));
@@ -56,7 +59,25 @@ ResponseEntity<String> completion(
5659
@GetMapping("/streamChatCompletion")
5760
@Nonnull
5861
ResponseEntity<ResponseBodyEmitter> streamChatCompletion() {
59-
return service.streamChatCompletion("developing a software project");
62+
final var stream = service.streamChatCompletion("developing a software project");
63+
final var emitter = new ResponseBodyEmitter();
64+
final Runnable consumeStream =
65+
() -> {
66+
try (stream) {
67+
stream.forEach(
68+
deltaMessage -> {
69+
log.info("Service: {}", deltaMessage);
70+
send(emitter, deltaMessage);
71+
});
72+
} finally {
73+
emitter.complete();
74+
}
75+
};
76+
77+
ThreadContextExecutors.getExecutor().execute(consumeStream);
78+
79+
// TEXT_EVENT_STREAM allows the browser to display the content as it is streamed
80+
return ResponseEntity.ok().contentType(MediaType.TEXT_EVENT_STREAM).body(emitter);
6081
}
6182

6283
/**
@@ -72,7 +93,7 @@ ResponseEntity<Object> template(
7293
@RequestHeader(value = "accept", required = false) final String accept)
7394
throws JsonProcessingException {
7495
final var response = service.template("German");
75-
if (accept.equals("application/json")) {
96+
if ("application/json".equals(accept)) {
7697
return ResponseEntity.ok()
7798
.contentType(MediaType.APPLICATION_JSON)
7899
.body(mapper.writeValueAsString(response));
@@ -91,7 +112,7 @@ ResponseEntity<String> messagesHistory(
91112
@RequestHeader(value = "accept", required = false) final String accept)
92113
throws JsonProcessingException {
93114
final var response = service.messagesHistory("What is the capital of France?");
94-
if (accept.equals("application/json")) {
115+
if ("application/json".equals(accept)) {
95116
return ResponseEntity.ok()
96117
.contentType(MediaType.APPLICATION_JSON)
97118
.body(mapper.writeValueAsString(response));
@@ -118,7 +139,7 @@ ResponseEntity<String> filter(
118139
@Nonnull @PathVariable("policy") final AzureFilterThreshold policy)
119140
throws JsonProcessingException {
120141
final var response = service.filter(policy, "the downtown area");
121-
if (accept.equals("application/json")) {
142+
if ("application/json".equals(accept)) {
122143
return ResponseEntity.ok()
123144
.contentType(MediaType.APPLICATION_JSON)
124145
.body(mapper.writeValueAsString(response));
@@ -142,7 +163,7 @@ ResponseEntity<String> maskingAnonymization(
142163
@RequestHeader(value = "accept", required = false) final String accept)
143164
throws JsonProcessingException {
144165
final var response = service.maskingAnonymization(DPIEntities.PERSON);
145-
if (accept.equals("application/json")) {
166+
if ("application/json".equals(accept)) {
146167
return ResponseEntity.ok()
147168
.contentType(MediaType.APPLICATION_JSON)
148169
.body(mapper.writeValueAsString(response));
@@ -162,7 +183,7 @@ public ResponseEntity<String> completionWithResourceGroup(
162183
@PathVariable("resourceGroup") @Nonnull final String resourceGroup)
163184
throws JsonProcessingException {
164185
final var response = service.completionWithResourceGroup(resourceGroup, "Hello world!");
165-
if (accept.equals("application/json")) {
186+
if ("application/json".equals(accept)) {
166187
return ResponseEntity.ok()
167188
.contentType(MediaType.APPLICATION_JSON)
168189
.body(mapper.writeValueAsString(response));
@@ -185,7 +206,7 @@ ResponseEntity<String> maskingPseudonymization(
185206
@RequestHeader(value = "accept", required = false) final String accept)
186207
throws JsonProcessingException {
187208
final var response = service.maskingPseudonymization(DPIEntities.PERSON);
188-
if (accept.equals("application/json")) {
209+
if ("application/json".equals(accept)) {
189210
return ResponseEntity.ok()
190211
.contentType(MediaType.APPLICATION_JSON)
191212
.body(mapper.writeValueAsString(response));
@@ -206,7 +227,7 @@ ResponseEntity<String> grounding(
206227
@RequestHeader(value = "accept", required = false) final String accept)
207228
throws JsonProcessingException {
208229
final var response = service.grounding("What does Joule do?");
209-
if (accept.equals("application/json")) {
230+
if ("application/json".equals(accept)) {
210231
return ResponseEntity.ok()
211232
.contentType(MediaType.APPLICATION_JSON)
212233
.body(mapper.writeValueAsString(response));

0 commit comments

Comments
 (0)