Skip to content

Commit 708a5e5

Browse files
committed
Merge branch 'main' into feat-custom-headers
# Conflicts: # docs/release_notes.md
2 parents d826271 + 826d351 commit 708a5e5

File tree

13 files changed

+249
-11
lines changed

13 files changed

+249
-11
lines changed

.github/workflows/e2e-test.yaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ jobs:
4444
id: run_tests
4545
run: |
4646
if [ "${{ matrix.environment }}" = "canary" ]; then
47-
export AICORE_SERVICE_KEY="${{ secrets.AI_CORE_CANARY }}"
47+
export AICORE_SERVICE_KEY='${{ secrets.AI_CORE_CANARY }}'
4848
else
49-
export AICORE_SERVICE_KEY="${{ secrets.AI_CORE_PRODUCTION }}"
49+
export AICORE_SERVICE_KEY='${{ secrets.AI_CORE_PRODUCTION }}'
5050
fi
5151
5252
MVN_ARGS="${{ env.MVN_MULTI_THREADED_ARGS }} surefire:test -pl :spring-app -DskipTests=false"
@@ -74,9 +74,9 @@ jobs:
7474
- name: "Start Application Locally"
7575
run: |
7676
if [ "${{ matrix.environment }}" = "canary" ]; then
77-
export AICORE_SERVICE_KEY="${{ secrets.AI_CORE_CANARY }}"
77+
export AICORE_SERVICE_KEY='${{ secrets.AI_CORE_CANARY }}'
7878
else
79-
export AICORE_SERVICE_KEY="${{ secrets.AI_CORE_PRODUCTION }}"
79+
export AICORE_SERVICE_KEY='${{ secrets.AI_CORE_PRODUCTION }}'
8080
fi
8181
8282
cd sample-code/spring-app
@@ -114,4 +114,4 @@ jobs:
114114
- type: "section"
115115
text:
116116
type: "plain_text"
117-
text: "${{ steps.run_tests.outputs.error_message }}"
117+
text: "${{ steps.run_tests.outputs.error_message }} "

core-services/prompt-registry/pom.xml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,10 @@
3838
</scm>
3939
<properties>
4040
<project.rootdir>${project.basedir}/../../</project.rootdir>
41-
<coverage.complexity>75%</coverage.complexity>
41+
<coverage.complexity>73%</coverage.complexity>
4242
<coverage.line>87%</coverage.line>
4343
<coverage.instruction>89%</coverage.instruction>
44-
<coverage.branch>100%</coverage.branch>
44+
<coverage.branch>75%</coverage.branch>
4545
<coverage.method>75%</coverage.method>
4646
<coverage.class>100%</coverage.class>
4747
</properties>
@@ -64,6 +64,11 @@
6464
<groupId>org.springframework</groupId>
6565
<artifactId>spring-web</artifactId>
6666
</dependency>
67+
<dependency>
68+
<groupId>org.springframework.ai</groupId>
69+
<artifactId>spring-ai-model</artifactId>
70+
<optional>true</optional>
71+
</dependency>
6772
<dependency>
6873
<groupId>com.sap.cloud.sdk.cloudplatform</groupId>
6974
<artifactId>cloudplatform-connectivity</artifactId>
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package com.sap.ai.sdk.prompt.registry.spring;
2+
3+
import com.sap.ai.sdk.prompt.registry.model.PromptTemplateSubstitutionResponse;
4+
import com.sap.ai.sdk.prompt.registry.model.SingleChatTemplate;
5+
import com.sap.ai.sdk.prompt.registry.model.Template;
6+
import java.util.List;
7+
import javax.annotation.Nonnull;
8+
import lombok.val;
9+
import org.springframework.ai.chat.messages.AssistantMessage;
10+
import org.springframework.ai.chat.messages.Message;
11+
import org.springframework.ai.chat.messages.SystemMessage;
12+
import org.springframework.ai.chat.messages.UserMessage;
13+
14+
/** Utility class for prompt registry related operations in a Spring context. */
15+
public class SpringAiConverter {
16+
17+
private SpringAiConverter() {
18+
// Utility class, no instantiation allowed
19+
}
20+
21+
/**
22+
* Get a SpringAI list of messages from a Prompt Registry Response.
23+
*
24+
* @param promptResponse the response from Prompt Registry.
25+
* @return list of SpringAI messages.
26+
*/
27+
@Nonnull
28+
public static List<Message> promptTemplateToMessages(
29+
@Nonnull final PromptTemplateSubstitutionResponse promptResponse) {
30+
31+
val res = promptResponse.getParsedPrompt();
32+
33+
// TRANSFORM TEMPLATE TO SPRING AI MESSAGES
34+
return res.stream()
35+
.map(
36+
(Template t) -> {
37+
final SingleChatTemplate message = (SingleChatTemplate) t;
38+
return (Message)
39+
switch (message.getRole()) {
40+
case "system" -> new SystemMessage(message.getContent());
41+
case "user" -> new UserMessage(message.getContent());
42+
case "assistant" -> new AssistantMessage(message.getContent());
43+
default ->
44+
throw new IllegalArgumentException("Unknown role: " + message.getRole());
45+
};
46+
})
47+
.toList();
48+
}
49+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package com.sap.ai.sdk.prompt.registry.spring;
2+
3+
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
4+
import static org.assertj.core.api.Assertions.assertThat;
5+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
6+
7+
import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
8+
import com.sap.ai.sdk.core.AiCoreService;
9+
import com.sap.ai.sdk.prompt.registry.PromptClient;
10+
import com.sap.ai.sdk.prompt.registry.model.PromptTemplateSubstitutionRequest;
11+
import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination;
12+
import com.sap.cloud.sdk.cloudplatform.connectivity.HttpDestination;
13+
import java.util.List;
14+
import java.util.Map;
15+
import lombok.val;
16+
import org.junit.jupiter.api.Test;
17+
import org.junit.jupiter.api.extension.RegisterExtension;
18+
import org.springframework.ai.chat.messages.Message;
19+
import org.springframework.ai.chat.messages.SystemMessage;
20+
import org.springframework.ai.chat.messages.UserMessage;
21+
22+
public class SpringAiConverterTest {
23+
@RegisterExtension
24+
private static final WireMockExtension WM =
25+
WireMockExtension.newInstance().options(wireMockConfig().dynamicPort()).build();
26+
27+
private final HttpDestination DESTINATION = DefaultHttpDestination.builder(WM.baseUrl()).build();
28+
private final AiCoreService SERVICE = new AiCoreService().withBaseDestination(DESTINATION);
29+
30+
@Test
31+
void testPromptRegistryToSpringAi() {
32+
var client = new PromptClient(SERVICE);
33+
val promptResponse =
34+
client.parsePromptTemplateByNameVersion(
35+
"categorization",
36+
"0.0.1",
37+
"java-e2e-test",
38+
"default",
39+
false,
40+
PromptTemplateSubstitutionRequest.create()
41+
.inputParams(Map.of("inputExample", "I love football")));
42+
43+
List<Message> messages = SpringAiConverter.promptTemplateToMessages(promptResponse);
44+
assertThat(messages)
45+
.isEqualTo(
46+
List.of(
47+
new SystemMessage(
48+
"You classify input text into the two following categories: Finance, Tech, Sports, Politics"),
49+
new UserMessage("I love football")));
50+
}
51+
52+
@Test
53+
void testInvalidRoleThrowsException() {
54+
var client = new PromptClient(SERVICE);
55+
val errorPrompt =
56+
client.parsePromptTemplateByNameVersion(
57+
"categorization",
58+
"0.0.1",
59+
"error",
60+
"default",
61+
false,
62+
PromptTemplateSubstitutionRequest.create()
63+
.inputParams(Map.of("inputExample", "I love football")));
64+
65+
assertThatThrownBy(() -> SpringAiConverter.promptTemplateToMessages(errorPrompt))
66+
.isInstanceOf(IllegalArgumentException.class)
67+
.hasMessageContaining("Unknown role: error");
68+
}
69+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"request": {
3+
"method": "POST",
4+
"url": "/v2/lm/scenarios/categorization/promptTemplates/error/versions/0.0.1/substitution?metadata=false"
5+
},
6+
"response": {
7+
"status": 200,
8+
"headers": {
9+
"Content-Type": "application/json"
10+
},
11+
"jsonBody": {
12+
"parsedPrompt": [
13+
{
14+
"role": "assistant",
15+
"content": "What can I help you with?"
16+
},
17+
{
18+
"role": "error",
19+
"content": "What is this?"
20+
}
21+
]
22+
}
23+
}
24+
}
25+
26+
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"request": {
3+
"method": "POST",
4+
"url": "/v2/lm/scenarios/categorization/promptTemplates/java-e2e-test/versions/0.0.1/substitution?metadata=false"
5+
},
6+
"response": {
7+
"status": 200,
8+
"headers": {
9+
"Content-Type": "application/json"
10+
},
11+
"jsonBody": {
12+
"parsedPrompt": [
13+
{
14+
"role": "system",
15+
"content": "You classify input text into the two following categories: Finance, Tech, Sports, Politics"
16+
},
17+
{
18+
"role": "user",
19+
"content": "I love football"
20+
}
21+
]
22+
}
23+
}
24+
}
25+
26+

docs/release_notes.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
- [Orchestration] Deprecated `OrchestrationAiModel.IBM_GRANITE_13B_CHAT` with no replacement.
2626
- [OpenAI] [Introduced SpringAI integration with our OpenAI client.](https://sap.github.io/ai-sdk/docs/java/spring-ai/openai)
2727
- Added `OpenAiChatModel`
28+
- [Prompt Registry] [Using Prompt Registry Templates in SpringAI.](https://sap.github.io/ai-sdk/docs/java/ai-core/prompt-registry#using-templates-in-springai)
29+
- Added `SpringAiConverter`
2830
- [Orchestration] [Added convenience to add custom headers to individual orchestration calls.](https://sap.github.io/ai-sdk/docs/java/orchestration/chat-completion#custom-headers)
2931
- [OpenAI] [Added convenience to add custom headers to individual LLM calls.](https://sap.github.io/ai-sdk/docs/java/foundation-models/openai/chat-completion#custom-headers)
3032

pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@
298298
<plugin>
299299
<groupId>org.openapitools</groupId>
300300
<artifactId>openapi-generator-maven-plugin</artifactId>
301-
<version>7.14.0</version>
301+
<version>7.15.0</version>
302302
</plugin>
303303
</plugins>
304304
</pluginManagement>
@@ -749,7 +749,7 @@ https://gitbox.apache.org/repos/asf?p=maven-pmd-plugin.git;a=blob_plain;f=src/ma
749749
<plugin>
750750
<groupId>com.github.spotbugs</groupId>
751751
<artifactId>spotbugs-maven-plugin</artifactId>
752-
<version>4.9.3.2</version>
752+
<version>4.9.4.0</version>
753753
<configuration>
754754
<includeFilterFile>${project.rootdir}/.pipeline/spotbugs.xml</includeFilterFile>
755755
<!-- Exclude generated clients -->

sample-code/spring-app/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
</developers>
3434
<properties>
3535
<project.rootdir>${project.basedir}/../../</project.rootdir>
36-
<spring-boot.version>3.5.4</spring-boot.version>
36+
<spring-boot.version>3.5.5</spring-boot.version>
3737
<logback.version>1.5.18</logback.version>
3838
<cf-logging.version>3.8.6</cf-logging.version>
3939
<apache-tomcat-embed.version>11.0.10</apache-tomcat-embed.version>

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package com.sap.ai.sdk.app.controllers;
22

3+
import com.sap.ai.sdk.foundationmodels.openai.OpenAiClient;
4+
import com.sap.ai.sdk.foundationmodels.openai.OpenAiModel;
5+
import com.sap.ai.sdk.foundationmodels.openai.spring.OpenAiChatModel;
36
import com.sap.ai.sdk.prompt.registry.PromptClient;
47
import com.sap.ai.sdk.prompt.registry.model.PromptTemplateDeleteResponse;
58
import com.sap.ai.sdk.prompt.registry.model.PromptTemplateListResponse;
@@ -9,10 +12,19 @@
912
import com.sap.ai.sdk.prompt.registry.model.PromptTemplateSubstitutionRequest;
1013
import com.sap.ai.sdk.prompt.registry.model.PromptTemplateSubstitutionResponse;
1114
import com.sap.ai.sdk.prompt.registry.model.SingleChatTemplate;
15+
import com.sap.ai.sdk.prompt.registry.spring.SpringAiConverter;
1216
import java.io.File;
1317
import java.io.IOException;
1418
import java.util.List;
1519
import java.util.Map;
20+
import lombok.val;
21+
import org.springframework.ai.chat.client.ChatClient;
22+
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
23+
import org.springframework.ai.chat.memory.InMemoryChatMemoryRepository;
24+
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
25+
import org.springframework.ai.chat.messages.Message;
26+
import org.springframework.ai.chat.model.Generation;
27+
import org.springframework.ai.chat.prompt.Prompt;
1628
import org.springframework.core.io.ClassPathResource;
1729
import org.springframework.core.io.Resource;
1830
import org.springframework.web.bind.annotation.GetMapping;
@@ -99,4 +111,29 @@ List<PromptTemplateDeleteResponse> deleteTemplate() {
99111
.map(template -> client.deletePromptTemplate(template.getId()))
100112
.toList();
101113
}
114+
115+
@GetMapping("/promptRegistryToSpringAi")
116+
Generation promptRegistryToSpringAi() {
117+
val openAiClient = new OpenAiChatModel(OpenAiClient.forModel(OpenAiModel.GPT_4O_MINI));
118+
val repository = new InMemoryChatMemoryRepository();
119+
val memory = MessageWindowChatMemory.builder().chatMemoryRepository(repository).build();
120+
val advisor = MessageChatMemoryAdvisor.builder(memory).build();
121+
val cl = ChatClient.builder(openAiClient).defaultAdvisors(advisor).build();
122+
123+
val promptResponse =
124+
new PromptClient()
125+
.parsePromptTemplateByNameVersion(
126+
"categorization",
127+
"0.0.1",
128+
"java-e2e-test",
129+
"default",
130+
false,
131+
PromptTemplateSubstitutionRequest.create()
132+
.inputParams(Map.of("inputExample", "I love football")));
133+
134+
final List<Message> messages = SpringAiConverter.promptTemplateToMessages(promptResponse);
135+
val prompt = new Prompt(messages);
136+
val response = cl.prompt(prompt).call().chatResponse();
137+
return response != null ? response.getResult() : null;
138+
}
102139
}

0 commit comments

Comments
 (0)