diff --git a/docs/guides/ORCHESTRATION_CHAT_COMPLETION.md b/docs/guides/ORCHESTRATION_CHAT_COMPLETION.md index 01bc97477..5091d330a 100644 --- a/docs/guides/ORCHESTRATION_CHAT_COMPLETION.md +++ b/docs/guides/ORCHESTRATION_CHAT_COMPLETION.md @@ -208,3 +208,21 @@ OrchestrationAiModel customGPT4O = "presence_penalty", 0)) .withVersion("2024-05-13"); ``` + +### Using a Configuration from AI Launchpad + +In case you have created a configuration in AI Launchpad, you can copy or download the configuration as JSON and use it directly in your code: + +```java +var configJson = """ + ... paste your configuration JSON in here ... + """; +// or load your config from a file, e.g. +// configJson = Files.readString(Paths.get("path/to/my/orchestration-config.json")); + +var prompt = new OrchestrationPrompt(Map.of("your-input-parameter", "your-param-value")); + +new OrchestrationClient().executeRequestFromJsonModuleConfig(prompt, configJson); +``` + +While this is not recommended for long term use, it can be useful for creating demos and PoCs. \ No newline at end of file diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClient.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClient.java index 6053a8089..443b20950 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClient.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClient.java @@ -4,8 +4,11 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.google.common.annotations.Beta; import com.sap.ai.sdk.core.AiCoreDeployment; import com.sap.ai.sdk.core.AiCoreService; import com.sap.ai.sdk.orchestration.client.model.CompletionPostRequest; @@ -30,7 +33,6 @@ import org.apache.hc.client5.http.classic.methods.HttpPost; import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.io.entity.StringEntity; -import org.apache.hc.core5.http.message.BasicClassicHttpRequest; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; /** Client to execute requests to the orchestration service. */ @@ -132,26 +134,75 @@ public OrchestrationChatResponse chatCompletion( @Nonnull public CompletionPostResponse executeRequest(@Nonnull final CompletionPostRequest request) throws OrchestrationClientException { - final BasicClassicHttpRequest postRequest = new HttpPost("/completion"); + final String jsonRequest; try { - val json = JACKSON.writeValueAsString(request); - log.debug("Serialized request into JSON payload: {}", json); - postRequest.setEntity(new StringEntity(json, ContentType.APPLICATION_JSON)); + jsonRequest = JACKSON.writeValueAsString(request); + log.debug("Serialized request into JSON payload: {}", jsonRequest); } catch (final JsonProcessingException e) { throw new OrchestrationClientException("Failed to serialize request parameters", e); } - return executeRequest(postRequest); + return executeRequest(jsonRequest); + } + + /** + * Perform a request to the orchestration service using a module configuration provided as JSON + * string. This can be useful when building a configuration in the AI Launchpad UI and exporting + * it as JSON. Furthermore, this allows for using features that are not yet supported natively by + * the API. + * + *

NOTE: This method does not support streaming. + * + * @param prompt The input parameters and optionally message history to use for prompt execution. + * @param moduleConfig The module configuration in JSON format. + * @return The completion response. + * @throws OrchestrationClientException If the request fails. + */ + @Beta + @Nonnull + public OrchestrationChatResponse executeRequestFromJsonModuleConfig( + @Nonnull final OrchestrationPrompt prompt, @Nonnull final String moduleConfig) + throws OrchestrationClientException { + if (!prompt.getMessages().isEmpty()) { + throw new IllegalArgumentException( + "Prompt must not contain any messages when using a JSON module configuration, as the template is already defined in the JSON."); + } + + final var request = + new CompletionPostRequest() + .messagesHistory(prompt.getMessagesHistory()) + .inputParams(prompt.getTemplateParameters()); + + final ObjectNode requestJson = JACKSON.valueToTree(request); + final JsonNode moduleConfigJson; + try { + moduleConfigJson = JACKSON.readTree(moduleConfig); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException( + "The provided module configuration is not valid JSON: " + moduleConfig, e); + } + requestJson.set("orchestration_config", moduleConfigJson); + + final String body; + try { + body = JACKSON.writeValueAsString(requestJson); + } catch (JsonProcessingException e) { + throw new OrchestrationClientException("Failed to serialize request to JSON", e); + } + return new OrchestrationChatResponse(executeRequest(body)); } @Nonnull - CompletionPostResponse executeRequest(@Nonnull final BasicClassicHttpRequest request) { + CompletionPostResponse executeRequest(@Nonnull final String request) { + val postRequest = new HttpPost("/completion"); + postRequest.setEntity(new StringEntity(request, ContentType.APPLICATION_JSON)); + try { val destination = deployment.get().destination(); log.debug("Using destination {} to connect to orchestration service", destination); val client = ApacheHttpClient5Accessor.getHttpClient(destination); return client.execute( - request, new OrchestrationResponseHandler<>(CompletionPostResponse.class)); + postRequest, new OrchestrationResponseHandler<>(CompletionPostResponse.class)); } catch (NoSuchElementException | DestinationAccessException | DestinationNotFoundException diff --git a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationUnitTest.java b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationUnitTest.java index 07b8f7bbe..d37db8a0f 100644 --- a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationUnitTest.java +++ b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationUnitTest.java @@ -380,4 +380,55 @@ void testErrorHandling() { softly.assertAll(); } + + @Test + void testExecuteRequestFromJson() { + stubFor(post(anyUrl()).willReturn(okJson("{}"))); + + prompt = new OrchestrationPrompt(Map.of()); + final var configJson = + """ + { + "module_configurations": { + "llm_module_config": { + "model_name": "mistralai--mistral-large-instruct", + "model_params": {} + } + } + } + """; + + final var expectedJson = + """ + { + "messages_history": [], + "input_params": {}, + "orchestration_config": { + "module_configurations": { + "llm_module_config": { + "model_name": "mistralai--mistral-large-instruct", + "model_params": {} + } + } + } + } + """; + + var result = client.executeRequestFromJsonModuleConfig(prompt, configJson); + assertThat(result).isNotNull(); + + verify(postRequestedFor(anyUrl()).withRequestBody(equalToJson(expectedJson))); + } + + @Test + void testExecuteRequestFromJsonThrows() { + assertThatThrownBy(() -> client.executeRequestFromJsonModuleConfig(prompt, "{}")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("messages"); + + prompt = new OrchestrationPrompt(Map.of()); + assertThatThrownBy(() -> client.executeRequestFromJsonModuleConfig(prompt, "{ foo")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("not valid JSON"); + } }