diff --git a/sample-code/spring-app/pom.xml b/sample-code/spring-app/pom.xml index d9343a33b..1185aa27a 100644 --- a/sample-code/spring-app/pom.xml +++ b/sample-code/spring-app/pom.xml @@ -95,6 +95,11 @@ spring-webmvc ${springframework.version} + + org.springframework + spring-beans + ${springframework.version} + com.google.code.findbugs jsr305 @@ -111,6 +116,10 @@ com.fasterxml.jackson.core jackson-core + + com.fasterxml.jackson.core + jackson-annotations + ch.qos.logback diff --git a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OpenAiController.java b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OpenAiController.java index 2c843c2a9..c48f739e3 100644 --- a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OpenAiController.java +++ b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OpenAiController.java @@ -34,7 +34,7 @@ /** Endpoints for OpenAI operations */ @Slf4j @RestController -class OpenAiController { +public class OpenAiController { /** * Chat request to OpenAI * @@ -123,7 +123,13 @@ ResponseEntity streamChatCompletion() { return ResponseEntity.ok().contentType(MediaType.TEXT_EVENT_STREAM).body(emitter); } - static void send(@Nonnull final ResponseBodyEmitter emitter, @Nonnull final String chunk) { + /** + * Send a chunk to the emitter + * + * @param emitter The emitter to send the chunk to + * @param chunk The chunk to send + */ + public static void send(@Nonnull final ResponseBodyEmitter emitter, @Nonnull final String chunk) { try { emitter.send(chunk); } catch (final IOException e) { diff --git a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java index 42437073d..b6d3cefda 100644 --- a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java +++ b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java @@ -1,33 +1,20 @@ package com.sap.ai.sdk.app.controllers; -import static com.sap.ai.sdk.app.controllers.OpenAiController.send; -import static com.sap.ai.sdk.orchestration.OrchestrationAiModel.GEMINI_1_5_FLASH; -import static com.sap.ai.sdk.orchestration.OrchestrationAiModel.Parameter.TEMPERATURE; - -import com.sap.ai.sdk.core.AiCoreService; -import com.sap.ai.sdk.orchestration.AzureContentFilter; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.sap.ai.sdk.app.services.OrchestrationService; import com.sap.ai.sdk.orchestration.AzureFilterThreshold; -import com.sap.ai.sdk.orchestration.DpiMasking; -import com.sap.ai.sdk.orchestration.Message; -import com.sap.ai.sdk.orchestration.OrchestrationChatResponse; -import com.sap.ai.sdk.orchestration.OrchestrationClient; -import com.sap.ai.sdk.orchestration.OrchestrationModuleConfig; -import com.sap.ai.sdk.orchestration.OrchestrationPrompt; import com.sap.ai.sdk.orchestration.model.DPIEntities; -import com.sap.ai.sdk.orchestration.model.DataRepositoryType; -import com.sap.ai.sdk.orchestration.model.DocumentGroundingFilter; -import com.sap.ai.sdk.orchestration.model.GroundingModuleConfig; -import com.sap.ai.sdk.orchestration.model.GroundingModuleConfigConfig; -import com.sap.ai.sdk.orchestration.model.Template; -import com.sap.cloud.sdk.cloudplatform.thread.ThreadContextExecutors; -import java.util.List; -import java.util.Map; import javax.annotation.Nonnull; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter; @@ -35,27 +22,30 @@ /** Endpoints for the Orchestration service */ @RestController @Slf4j +@SuppressWarnings("unused") @RequestMapping("/orchestration") class OrchestrationController { - private final OrchestrationClient client = new OrchestrationClient(); - OrchestrationModuleConfig config = - new OrchestrationModuleConfig().withLlmConfig(GEMINI_1_5_FLASH.withParam(TEMPERATURE, 0.0)); + @Autowired private OrchestrationService service; + private final ObjectMapper mapper = + new ObjectMapper().setVisibility(PropertyAccessor.FIELD, Visibility.ANY); /** * Chat request to OpenAI through the Orchestration service with a simple prompt. * - * @return the result object + * @return a ResponseEntity with the response content */ @GetMapping("/completion") @Nonnull - OrchestrationChatResponse completion() { - final var prompt = new OrchestrationPrompt("Hello world! Why is this phrase so famous?"); - - final var result = client.chatCompletion(prompt, config); - - log.info("Our trusty AI answered with: {}", result.getContent()); - - return result; + ResponseEntity completion( + @RequestHeader(value = "accept", required = false) final String accept) + throws JsonProcessingException { + final var response = service.completion("HelloWorld!"); + if (accept.equals("application/json")) { + return ResponseEntity.ok() + .contentType(MediaType.APPLICATION_JSON) + .body(mapper.writeValueAsString(response)); + } + return ResponseEntity.ok(response.getContent()); } /** @@ -63,33 +53,10 @@ OrchestrationChatResponse completion() { * * @return the emitter that streams the assistant message response */ - @SuppressWarnings("unused") // The end-to-end test doesn't use this method @GetMapping("/streamChatCompletion") @Nonnull - public ResponseEntity streamChatCompletion() { - final var prompt = - new OrchestrationPrompt("Can you give me the first 100 numbers of the Fibonacci sequence?"); - final var stream = client.streamChatCompletion(prompt, config); - - final var emitter = new ResponseBodyEmitter(); - - final Runnable consumeStream = - () -> { - try (stream) { - stream.forEach( - deltaMessage -> { - log.info("Controller: {}", deltaMessage); - send(emitter, deltaMessage); - }); - } finally { - emitter.complete(); - } - }; - - ThreadContextExecutors.getExecutor().execute(consumeStream); - - // TEXT_EVENT_STREAM allows the browser to display the content as it is streamed - return ResponseEntity.ok().contentType(MediaType.TEXT_EVENT_STREAM).body(emitter); + ResponseEntity streamChatCompletion() { + return service.streamChatCompletion(100); } /** @@ -97,40 +64,39 @@ public ResponseEntity streamChatCompletion() { * * @link SAP * AI Core: Orchestration - Templating - * @return the result object + * @return a ResponseEntity with the response content */ @GetMapping("/template") @Nonnull - OrchestrationChatResponse template() { - final var template = - Message.user("Reply with 'Orchestration Service is working!' in {{?language}}"); - final var templatingConfig = Template.create().template(List.of(template.createChatMessage())); - final var configWithTemplate = config.withTemplateConfig(templatingConfig); - - final var inputParams = Map.of("language", "German"); - final var prompt = new OrchestrationPrompt(inputParams); - - return client.chatCompletion(prompt, configWithTemplate); + ResponseEntity template( + @RequestHeader(value = "accept", required = false) final String accept) + throws JsonProcessingException { + final var response = service.template("German"); + if (accept.equals("application/json")) { + return ResponseEntity.ok() + .contentType(MediaType.APPLICATION_JSON) + .body(mapper.writeValueAsString(response)); + } + return ResponseEntity.ok(response.getContent()); } /** * Chat request to OpenAI through the Orchestration service using message history. * - * @return the result object + * @return a ResponseEntity with the response content */ @GetMapping("/messagesHistory") @Nonnull - OrchestrationChatResponse messagesHistory() { - final var prompt = new OrchestrationPrompt(Message.user("What is the capital of France?")); - - final var result = client.chatCompletion(prompt, config); - - // Let's presume a user asks the following follow-up question - final var nextPrompt = - new OrchestrationPrompt(Message.user("What is the typical food there?")) - .messageHistory(result.getAllMessages()); - - return client.chatCompletion(nextPrompt, config); + ResponseEntity messagesHistory( + @RequestHeader(value = "accept", required = false) final String accept) + throws JsonProcessingException { + final var response = service.messagesHistory("What is the capital of France?"); + if (accept.equals("application/json")) { + return ResponseEntity.ok() + .contentType(MediaType.APPLICATION_JSON) + .body(mapper.writeValueAsString(response)); + } + return ResponseEntity.ok(response.getContent()); } /** @@ -143,26 +109,21 @@ OrchestrationChatResponse messagesHistory() { * href="https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/output-filtering">SAP * AI Core: Orchestration - Output Filtering * @param policy A high threshold is a loose filter, a low threshold is a strict filter - * @return the result object + * @return a ResponseEntity with the response content */ @GetMapping("/filter/{policy}") @Nonnull - OrchestrationChatResponse filter( - @Nonnull @PathVariable("policy") final AzureFilterThreshold policy) { - final var prompt = - new OrchestrationPrompt( - """ - Create a rental posting for subletting my apartment in the downtown area. Keep it short. Make sure to add the following disclaimer to the end. Do not change it! - - ```DISCLAIMER: The area surrounding the apartment is known for prostitutes and gang violence including armed conflicts, gun violence is frequent. - """); - final var filterConfig = - new AzureContentFilter().hate(policy).selfHarm(policy).sexual(policy).violence(policy); - - final var configWithFilter = - config.withInputFiltering(filterConfig).withOutputFiltering(filterConfig); - - return client.chatCompletion(prompt, configWithFilter); + ResponseEntity filter( + @RequestHeader(value = "accept", required = false) final String accept, + @Nonnull @PathVariable("policy") final AzureFilterThreshold policy) + throws JsonProcessingException { + final var response = service.filter(policy, "the downtown area"); + if (accept.equals("application/json")) { + return ResponseEntity.ok() + .contentType(MediaType.APPLICATION_JSON) + .body(mapper.writeValueAsString(response)); + } + return ResponseEntity.ok(response.getContent()); } /** @@ -173,45 +134,40 @@ OrchestrationChatResponse filter( * @link SAP AI * Core: Orchestration - Data Masking - * @return the result object + * @return a ResponseEntity with the response content */ @GetMapping("/maskingAnonymization") @Nonnull - OrchestrationChatResponse maskingAnonymization() { - final var systemMessage = - Message.system( - "Please evaluate the following user feedback and judge if the sentiment is positive or negative."); - final var userMessage = - Message.user( - """ - I think the SDK is good, but could use some further enhancements. - My architect Alice and manager Bob pointed out that we need the grounding capabilities, which aren't supported yet. - """); - - final var prompt = new OrchestrationPrompt(systemMessage, userMessage); - final var maskingConfig = DpiMasking.anonymization().withEntities(DPIEntities.PERSON); - final var configWithMasking = config.withMaskingConfig(maskingConfig); - - return client.chatCompletion(prompt, configWithMasking); + ResponseEntity maskingAnonymization( + @RequestHeader(value = "accept", required = false) final String accept) + throws JsonProcessingException { + final var response = service.maskingAnonymization(DPIEntities.PERSON); + if (accept.equals("application/json")) { + return ResponseEntity.ok() + .contentType(MediaType.APPLICATION_JSON) + .body(mapper.writeValueAsString(response)); + } + return ResponseEntity.ok(response.getContent()); } /** * Chat request to OpenAI through the Orchestration deployment under a specific resource group. * - * @return the result object + * @return a ResponseEntity with the response content */ @GetMapping("/completion/{resourceGroup}") @Nonnull - public OrchestrationChatResponse completionWithResourceGroup( - @PathVariable("resourceGroup") @Nonnull final String resourceGroup) { - - final var destination = - new AiCoreService().getInferenceDestination(resourceGroup).forScenario("orchestration"); - final var clientWithResourceGroup = new OrchestrationClient(destination); - - final var prompt = new OrchestrationPrompt("Hello world! Why is this phrase so famous?"); - - return clientWithResourceGroup.chatCompletion(prompt, config); + public ResponseEntity completionWithResourceGroup( + @RequestHeader(value = "accept", required = false) final String accept, + @PathVariable("resourceGroup") @Nonnull final String resourceGroup) + throws JsonProcessingException { + final var response = service.completionWithResourceGroup(resourceGroup, "Hello world!"); + if (accept.equals("application/json")) { + return ResponseEntity.ok() + .contentType(MediaType.APPLICATION_JSON) + .body(mapper.writeValueAsString(response)); + } + return ResponseEntity.ok(response.getContent()); } /** @@ -221,34 +177,20 @@ public OrchestrationChatResponse completionWithResourceGroup( * @link SAP AI * Core: Orchestration - Data Masking - * @return the result object + * @return a ResponseEntity with the response content */ @GetMapping("/maskingPseudonymization") @Nonnull - OrchestrationChatResponse maskingPseudonymization() { - final var systemMessage = - Message.system( - """ - Please write an initial response to the below user feedback, stating that we are working on the feedback and will get back to them soon. - Please make sure to address the user in person and end with "Best regards, the AI SDK team". - """); - final var userMessage = - Message.user( - """ - Username: Mallory - userEmail: mallory@sap.com - Date: 2022-01-01 - - I think the SDK is good, but could use some further enhancements. - My architect Alice and manager Bob pointed out that we need the grounding capabilities, which aren't supported yet. - """); - - final var prompt = new OrchestrationPrompt(systemMessage, userMessage); - final var maskingConfig = - DpiMasking.pseudonymization().withEntities(DPIEntities.PERSON, DPIEntities.EMAIL); - final var configWithMasking = config.withMaskingConfig(maskingConfig); - - return client.chatCompletion(prompt, configWithMasking); + ResponseEntity maskingPseudonymization( + @RequestHeader(value = "accept", required = false) final String accept) + throws JsonProcessingException { + final var response = service.maskingPseudonymization(DPIEntities.PERSON); + if (accept.equals("application/json")) { + return ResponseEntity.ok() + .contentType(MediaType.APPLICATION_JSON) + .body(mapper.writeValueAsString(response)); + } + return ResponseEntity.ok(response.getContent()); } /** @@ -256,29 +198,19 @@ OrchestrationChatResponse maskingPseudonymization() { * * @link SAP * AI Core: Orchestration - Grounding + * @return a ResponseEntity with the response content */ @GetMapping("/grounding") @Nonnull - OrchestrationChatResponse grounding() { - final var message = - Message.user( - "{{?groundingInput}} Use the following information as additional context: {{?groundingOutput}}"); - final var prompt = - new OrchestrationPrompt(Map.of("groundingInput", "What does Joule do?"), message); - - final var filterInner = - DocumentGroundingFilter.create().id("someID").dataRepositoryType(DataRepositoryType.VECTOR); - final var groundingConfigConfig = - GroundingModuleConfigConfig.create() - .inputParams(List.of("groundingInput")) - .outputParam("groundingOutput") - .addFiltersItem(filterInner); - final var groundingConfig = - GroundingModuleConfig.create() - .type(GroundingModuleConfig.TypeEnum.DOCUMENT_GROUNDING_SERVICE) - .config(groundingConfigConfig); - final var configWithGrounding = config.withGroundingConfig(groundingConfig); - - return client.chatCompletion(prompt, configWithGrounding); + ResponseEntity grounding( + @RequestHeader(value = "accept", required = false) final String accept) + throws JsonProcessingException { + final var response = service.grounding("What does Joule do?"); + if (accept.equals("application/json")) { + return ResponseEntity.ok() + .contentType(MediaType.APPLICATION_JSON) + .body(mapper.writeValueAsString(response)); + } + return ResponseEntity.ok(response.getContent()); } } diff --git a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/OrchestrationService.java b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/OrchestrationService.java new file mode 100644 index 000000000..d17db4833 --- /dev/null +++ b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/OrchestrationService.java @@ -0,0 +1,270 @@ +package com.sap.ai.sdk.app.services; + +import static com.sap.ai.sdk.app.controllers.OpenAiController.send; +import static com.sap.ai.sdk.orchestration.OrchestrationAiModel.GEMINI_1_5_FLASH; +import static com.sap.ai.sdk.orchestration.OrchestrationAiModel.Parameter.TEMPERATURE; + +import com.sap.ai.sdk.core.AiCoreService; +import com.sap.ai.sdk.orchestration.AzureContentFilter; +import com.sap.ai.sdk.orchestration.AzureFilterThreshold; +import com.sap.ai.sdk.orchestration.DpiMasking; +import com.sap.ai.sdk.orchestration.Message; +import com.sap.ai.sdk.orchestration.OrchestrationChatResponse; +import com.sap.ai.sdk.orchestration.OrchestrationClient; +import com.sap.ai.sdk.orchestration.OrchestrationModuleConfig; +import com.sap.ai.sdk.orchestration.OrchestrationPrompt; +import com.sap.ai.sdk.orchestration.model.DPIEntities; +import com.sap.ai.sdk.orchestration.model.DataRepositoryType; +import com.sap.ai.sdk.orchestration.model.DocumentGroundingFilter; +import com.sap.ai.sdk.orchestration.model.GroundingModuleConfig; +import com.sap.ai.sdk.orchestration.model.GroundingModuleConfigConfig; +import com.sap.ai.sdk.orchestration.model.Template; +import com.sap.cloud.sdk.cloudplatform.thread.ThreadContextExecutors; +import java.util.List; +import java.util.Map; +import javax.annotation.Nonnull; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter; + +/** Service class for the Orchestration service */ +@Service +@Slf4j +public class OrchestrationService { + private final OrchestrationClient client = new OrchestrationClient(); + + @Getter + private final OrchestrationModuleConfig config = + new OrchestrationModuleConfig().withLlmConfig(GEMINI_1_5_FLASH.withParam(TEMPERATURE, 0.0)); + + /** + * Chat request to OpenAI through the Orchestration service with a simple prompt. + * + * @return the assistant response object + */ + @Nonnull + public OrchestrationChatResponse completion(@Nonnull final String famousPhrase) { + final var prompt = new OrchestrationPrompt(famousPhrase + " Why is this phrase so famous?"); + return client.chatCompletion(prompt, config); + } + + /** + * Chat request to OpenAI through the Orchestration service with a template. + * + * @link SAP + * AI Core: Orchestration - Templating + * @return the assistant response object + */ + @Nonnull + public OrchestrationChatResponse template(@Nonnull final String language) { + final var template = + Message.user("Reply with 'Orchestration Service is working!' in {{?language}}"); + final var templatingConfig = Template.create().template(List.of(template.createChatMessage())); + final var configWithTemplate = config.withTemplateConfig(templatingConfig); + + final var inputParams = Map.of("language", language); + final var prompt = new OrchestrationPrompt(inputParams); + + return client.chatCompletion(prompt, configWithTemplate); + } + + /** + * Chat request to OpenAI through the Orchestration service using message history. + * + * @return the assistant response object + */ + @Nonnull + public OrchestrationChatResponse messagesHistory(@Nonnull final String prevMessage) { + final var prompt = new OrchestrationPrompt(Message.user(prevMessage)); + + final var result = client.chatCompletion(prompt, config); + + // Let's presume a user asks the following follow-up question + final var nextPrompt = + new OrchestrationPrompt(Message.user("What is the typical food there?")) + .messageHistory(result.getAllMessages()); + + return client.chatCompletion(nextPrompt, config); + } + + /** + * Apply both input and output filtering for a request to orchestration. + * + * @link SAP + * AI Core: Orchestration - Input Filtering + * @link SAP + * AI Core: Orchestration - Output Filtering + * @param policy A high threshold is a loose filter, a low threshold is a strict filter + * @return the assistant response object + */ + @Nonnull + public OrchestrationChatResponse filter( + @Nonnull final AzureFilterThreshold policy, @Nonnull final String area) { + final var prompt = + new OrchestrationPrompt( + """ + Create a rental posting for subletting my apartment in %s. Keep it short. Make sure to add the following disclaimer to the end. Do not change it! + + ```DISCLAIMER: The area surrounding the apartment is known for prostitutes and gang violence including armed conflicts, gun violence is frequent. + """ + .formatted(area)); + final var filterConfig = + new AzureContentFilter().hate(policy).selfHarm(policy).sexual(policy).violence(policy); + + final var configWithFilter = + config.withInputFiltering(filterConfig).withOutputFiltering(filterConfig); + + return client.chatCompletion(prompt, configWithFilter); + } + + /** + * Let the orchestration service evaluate the feedback on the AI SDK provided by a hypothetical + * user. Anonymize any names given as they are not relevant for judging the sentiment of the + * feedback. + * + * @link SAP AI + * Core: Orchestration - Data Masking + * @return the assistant response object + */ + @Nonnull + public OrchestrationChatResponse maskingAnonymization(@Nonnull final DPIEntities entity) { + final var systemMessage = + Message.system( + "Please evaluate the following user feedback and judge if the sentiment is positive or negative."); + final var userMessage = + Message.user( + """ + I think the SDK is good, but could use some further enhancements. + My architect Alice and manager Bob pointed out that we need the grounding capabilities, which aren't supported yet. + """); + + final var prompt = new OrchestrationPrompt(systemMessage, userMessage); + final var maskingConfig = DpiMasking.anonymization().withEntities(entity); + final var configWithMasking = config.withMaskingConfig(maskingConfig); + + return client.chatCompletion(prompt, configWithMasking); + } + + /** + * Chat request to OpenAI through the Orchestration deployment under a specific resource group. + * + * @return the assistant response object + */ + @Nonnull + public OrchestrationChatResponse completionWithResourceGroup( + @Nonnull final String resourceGroup, @Nonnull final String famousPhrase) { + final var destination = + new AiCoreService().getInferenceDestination(resourceGroup).forScenario("orchestration"); + final var clientWithResourceGroup = new OrchestrationClient(destination); + + final var prompt = new OrchestrationPrompt(famousPhrase + " Why is this phrase so famous?"); + + return clientWithResourceGroup.chatCompletion(prompt, config); + } + + /** + * Let the orchestration service a response to a hypothetical user who provided feedback on the AI + * SDK. Pseudonymize the user's name and location to protect their privacy. + * + * @link SAP AI + * Core: Orchestration - Data Masking + * @return the assistant response object + */ + @Nonnull + public OrchestrationChatResponse maskingPseudonymization(@Nonnull final DPIEntities entity) { + final var systemMessage = + Message.system( + """ + Please write an initial response to the below user feedback, stating that we are working on the feedback and will get back to them soon. + Please make sure to address the user in person and end with "Best regards, the AI SDK team". + """); + final var userMessage = + Message.user( + """ + Username: Mallory + userEmail: mallory@sap.com + Date: 2022-01-01 + + I think the SDK is good, but could use some further enhancements. + My architect Alice and manager Bob pointed out that we need the grounding capabilities, which aren't supported yet. + """); + + final var prompt = new OrchestrationPrompt(systemMessage, userMessage); + final var maskingConfig = DpiMasking.pseudonymization().withEntities(entity, DPIEntities.EMAIL); + final var configWithMasking = config.withMaskingConfig(maskingConfig); + + return client.chatCompletion(prompt, configWithMasking); + } + + /** + * Using grounding to provide additional context to the AI model. + * + * @link SAP + * AI Core: Orchestration - Grounding + * @return the assistant response object + */ + @Nonnull + public OrchestrationChatResponse grounding(@Nonnull final String groundingInput) { + final var message = + Message.user( + "{{?groundingInput}} Use the following information as additional context: {{?groundingOutput}}"); + final var prompt = new OrchestrationPrompt(Map.of("groundingInput", groundingInput), message); + + final var filterInner = + DocumentGroundingFilter.create().id("someID").dataRepositoryType(DataRepositoryType.VECTOR); + final var groundingConfigConfig = + GroundingModuleConfigConfig.create() + .inputParams(List.of("groundingInput")) + .outputParam("groundingOutput") + .addFiltersItem(filterInner); + final var groundingConfig = + GroundingModuleConfig.create() + .type(GroundingModuleConfig.TypeEnum.DOCUMENT_GROUNDING_SERVICE) + .config(groundingConfigConfig); + final var configWithGrounding = config.withGroundingConfig(groundingConfig); + + return client.chatCompletion(prompt, configWithGrounding); + } + + /** + * Asynchronous stream of an OpenAI chat request + * + * @return the emitter that streams the assistant message response + */ + @Nonnull + public ResponseEntity streamChatCompletion(final int numberOfFibonacci) { + final var prompt = + new OrchestrationPrompt( + "Can you give me the first " + + numberOfFibonacci + + " numbers of the Fibonacci sequence?"); + final var stream = client.streamChatCompletion(prompt, config); + + final var emitter = new ResponseBodyEmitter(); + + final Runnable consumeStream = + () -> { + try (stream) { + stream.forEach( + deltaMessage -> { + log.info("Service: {}", deltaMessage); + send(emitter, deltaMessage); + }); + } finally { + emitter.complete(); + } + }; + + ThreadContextExecutors.getExecutor().execute(consumeStream); + + // TEXT_EVENT_STREAM allows the browser to display the content as it is streamed + return ResponseEntity.ok().contentType(MediaType.TEXT_EVENT_STREAM).body(emitter); + } +} diff --git a/sample-code/spring-app/src/main/resources/static/index.html b/sample-code/spring-app/src/main/resources/static/index.html index 468874f58..df99ce0ea 100644 --- a/sample-code/spring-app/src/main/resources/static/index.html +++ b/sample-code/spring-app/src/main/resources/static/index.html @@ -70,8 +70,8 @@

Endpoints

  • /orchestration/streamChatCompletion
  • /orchestration/template
  • /orchestration/messagesHistory
  • -
  • /orchestration/filter/NUMBER_4 Loose filter
  • -
  • /orchestration/filter/NUMBER_0 Strict filter (fails)
  • +
  • /orchestration/filter/ALLOW_SAFE_LOW_MEDIUM Loose filter
  • +
  • /orchestration/filter/ALLOW_SAFE Strict filter (fails)
  • /orchestration/maskingAnonymization
  • /orchestration/maskingPseudonymization
  • /orchestration/grounding
  • diff --git a/sample-code/spring-app/src/test/java/com/sap/ai/sdk/app/controllers/OrchestrationTest.java b/sample-code/spring-app/src/test/java/com/sap/ai/sdk/app/controllers/OrchestrationTest.java index 8f7a1f40f..06f4e690b 100644 --- a/sample-code/spring-app/src/test/java/com/sap/ai/sdk/app/controllers/OrchestrationTest.java +++ b/sample-code/spring-app/src/test/java/com/sap/ai/sdk/app/controllers/OrchestrationTest.java @@ -3,11 +3,13 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import com.sap.ai.sdk.app.services.OrchestrationService; import com.sap.ai.sdk.orchestration.AzureFilterThreshold; import com.sap.ai.sdk.orchestration.OrchestrationClient; import com.sap.ai.sdk.orchestration.OrchestrationClientException; import com.sap.ai.sdk.orchestration.OrchestrationPrompt; import com.sap.ai.sdk.orchestration.model.CompletionPostResponse; +import com.sap.ai.sdk.orchestration.model.DPIEntities; import com.sap.ai.sdk.orchestration.model.LLMChoice; import com.sap.ai.sdk.orchestration.model.LLMModuleResultSynchronous; import java.util.Map; @@ -19,16 +21,16 @@ @Slf4j class OrchestrationTest { - OrchestrationController controller; + OrchestrationService service; @BeforeEach void setUp() { - controller = new OrchestrationController(); + service = new OrchestrationService(); } @Test void testCompletion() { - final var result = controller.completion(); + final var result = service.completion("HelloWorld!"); assertThat(result).isNotNull(); assertThat(result.getContent()).isNotEmpty(); @@ -37,7 +39,7 @@ void testCompletion() { @Test void testStreamChatCompletion() { final var prompt = new OrchestrationPrompt("Who is the prettiest?"); - final var stream = new OrchestrationClient().streamChatCompletion(prompt, controller.config); + final var stream = new OrchestrationClient().streamChatCompletion(prompt, service.getConfig()); final var filledDeltaCount = new AtomicInteger(0); stream @@ -57,10 +59,10 @@ void testStreamChatCompletion() { @Test void testTemplate() { - assertThat(controller.config.getLlmConfig()).isNotNull(); - final var modelName = controller.config.getLlmConfig().getModelName(); + assertThat(service.getConfig().getLlmConfig()).isNotNull(); + final var modelName = service.getConfig().getLlmConfig().getModelName(); - final var result = controller.template(); + final var result = service.template("German"); final var response = result.getOriginalResponse(); assertThat(response.getRequestId()).isNotEmpty(); @@ -100,7 +102,7 @@ void testTemplate() { @Test void testLenientContentFilter() { - var response = controller.filter(AzureFilterThreshold.ALLOW_SAFE_LOW_MEDIUM); + var response = service.filter(AzureFilterThreshold.ALLOW_SAFE_LOW_MEDIUM, "the downtown area"); var result = response.getOriginalResponse(); var llmChoice = ((LLMModuleResultSynchronous) result.getOrchestrationResult()).getChoices().get(0); @@ -113,7 +115,7 @@ void testLenientContentFilter() { @Test void testStrictContentFilter() { - assertThatThrownBy(() -> controller.filter(AzureFilterThreshold.ALLOW_SAFE)) + assertThatThrownBy(() -> service.filter(AzureFilterThreshold.ALLOW_SAFE, "the downtown area")) .isInstanceOf(OrchestrationClientException.class) .hasMessageContaining("400 Bad Request") .hasMessageContaining("Content filtered"); @@ -121,7 +123,8 @@ void testStrictContentFilter() { @Test void testMessagesHistory() { - CompletionPostResponse result = controller.messagesHistory().getOriginalResponse(); + CompletionPostResponse result = + service.messagesHistory("What is the capital of France?").getOriginalResponse(); final var choices = ((LLMModuleResultSynchronous) result.getOrchestrationResult()).getChoices(); assertThat(choices.get(0).getMessage().getContent()).isNotEmpty(); } @@ -129,7 +132,7 @@ void testMessagesHistory() { @SuppressWarnings("unchecked") @Test void testMaskingAnonymization() { - var response = controller.maskingAnonymization(); + var response = service.maskingAnonymization(DPIEntities.PERSON); var result = response.getOriginalResponse(); var llmChoice = ((LLMModuleResultSynchronous) result.getOrchestrationResult()).getChoices().get(0); @@ -149,7 +152,7 @@ void testMaskingAnonymization() { @SuppressWarnings("unchecked") @Test void testMaskingPseudonymization() { - var response = controller.maskingPseudonymization(); + var response = service.maskingPseudonymization(DPIEntities.PERSON); var result = response.getOriginalResponse(); var llmChoice = ((LLMModuleResultSynchronous) result.getOrchestrationResult()).getChoices().get(0); @@ -179,7 +182,7 @@ void testMaskingPseudonymization() { @DisabledIfSystemProperty(named = "aicore.landscape", matches = "production") void testGrounding() { assertThat(System.getProperty("aicore.landscape")).isNotEqualTo("production"); - var response = controller.grounding(); + var response = service.grounding("What does Joule do?"); var result = response.getOriginalResponse(); var llmChoice = ((LLMModuleResultSynchronous) result.getOrchestrationResult()).getChoices().get(0); @@ -192,7 +195,7 @@ void testGrounding() { @Test void testCompletionWithResourceGroup() { - var response = controller.completionWithResourceGroup("ai-sdk-java-e2e"); + var response = service.completionWithResourceGroup("ai-sdk-java-e2e", "Hello world!"); var result = response.getOriginalResponse(); var llmChoice = ((LLMModuleResultSynchronous) result.getOrchestrationResult()).getChoices().get(0);