Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion springboot-modules/spring-ai/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@
<artifactId>spring-ai-spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-vector-store</artifactId>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
Expand All @@ -50,7 +54,7 @@
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.17.2</version>
<version>2.18.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
Expand All @@ -62,6 +66,12 @@
<artifactId>hsqldb</artifactId>
<version>${hsqldb.version}</version>
</dependency>

<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-azure-openai</artifactId>
</dependency>

</dependencies>

<dependencyManagement>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.ks.azureopenai;

import org.springframework.ai.azure.openai.AzureOpenAiChatModel;
import org.springframework.ai.azure.openai.AzureOpenAiChatOptions;
import org.springframework.ai.azure.openai.AzureOpenAiEmbeddingModel;
import org.springframework.ai.vectorstore.SimpleVectorStore;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

import com.azure.ai.openai.OpenAIClientBuilder;
import com.azure.core.credential.AzureKeyCredential;

@Configuration
@Profile("azureopenai")
public class ApplicationConfiguration {
@Value("${spring.ai.azure.openai.api-key}")
private String apiKey;
@Value("${spring.ai.azure.openai.endpoint}")
private String endpoint;

@Bean
public ChatService chatService(AzureOpenAiChatModel azureOpenAiChatModel) {
return new ChatService(azureOpenAiChatModel);
}

@Bean
public ChatService customChatService() {
OpenAIClientBuilder openAIClientBuilder = new OpenAIClientBuilder()
.credential(new AzureKeyCredential(getCredential()))
.endpoint(getEndpoint());

AzureOpenAiChatOptions openAIChatOptions = AzureOpenAiChatOptions.builder()
.deploymentName("gpt-5-nano")
.temperature(1d)
.build();

AzureOpenAiChatModel chatModel = AzureOpenAiChatModel.builder()
.openAIClientBuilder(openAIClientBuilder)
.defaultOptions(openAIChatOptions)
.build();
return new ChatService(chatModel);
}

private String getCredential() {
return apiKey;
}

private String getEndpoint() {
return endpoint;
}

@Bean
VectorDBService vectorDBStore(AzureOpenAiEmbeddingModel embeddingModel) {
return new VectorDBService(SimpleVectorStore.builder(embeddingModel)
.build()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.ks.azureopenai;

import org.springframework.ai.model.openai.autoconfigure.OpenAiAudioSpeechAutoConfiguration;
import org.springframework.ai.model.openai.autoconfigure.OpenAiAudioTranscriptionAutoConfiguration;
import org.springframework.ai.model.openai.autoconfigure.OpenAiChatAutoConfiguration;
import org.springframework.ai.model.openai.autoconfigure.OpenAiEmbeddingAutoConfiguration;
import org.springframework.ai.model.openai.autoconfigure.OpenAiImageAutoConfiguration;
import org.springframework.ai.model.openai.autoconfigure.OpenAiModerationAutoConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication(exclude = { OpenAiAudioSpeechAutoConfiguration.class,
OpenAiAudioTranscriptionAutoConfiguration.class,
OpenAiChatAutoConfiguration.class,
OpenAiEmbeddingAutoConfiguration.class,
OpenAiImageAutoConfiguration.class,
OpenAiModerationAutoConfiguration.class
})
public class ChatApplication {
public static void main(String[] args) {
SpringApplication.run(ChatApplication.class, args);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.ks.azureopenai;

import org.springframework.ai.azure.openai.AzureOpenAiChatModel;
import org.springframework.ai.chat.prompt.Prompt;

public class ChatService {

AzureOpenAiChatModel chatModel;

public ChatService(AzureOpenAiChatModel chatModel) {
this.chatModel = chatModel;
}

public String chat(Prompt prompt) {
return chatModel.call(prompt)
.getResult()
.getOutput()
.getText();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.ks.azureopenai;

import java.util.List;

import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.SimpleVectorStore;

public class VectorDBService {

private SimpleVectorStore vectorStore;

public VectorDBService(SimpleVectorStore vectorStore) {
this.vectorStore = vectorStore;
}

public void saveDocs(List<Document> docs) {
this.vectorStore.doAdd(docs);
}

public String fetchContextFromVectorDB(String query) {
List<Document> queryResults = vectorStore.doSimilaritySearch(
SearchRequest.builder()
.query(query).topK(2)
.build()
);
StringBuilder contextBuilder = new StringBuilder();
for (Document docs : queryResults) {
contextBuilder.append(docs.getText()).append(" ");
}
return contextBuilder.toString().trim();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
spring.application.name=spring-ai-azureopenai
spring.ai.azure.openai.chat.options.user=springai-azureopenai-user
spring.ai.azure.openai.api-key=FSPyum-XXXXX
spring.ai.azure.openai.endpoint=https://parth-YYYY-eastus2.openai.azure.com/
spring.ai.azure.openai.chat.options.deployment-name=gpt-5-nano
spring.ai.azure.openai.chat.options.temperature=1

spring.ai.azure.openai.embedding.options.deployment-name=text-embedding-ada-002
spring.ai.azure.openai.embedding.options.model=text-embedding-ada-002
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
@startuml
'https://plantuml.com/class-diagram
set namespaceSeparator none
allowmixing
hide empty attributes
'skinparam Handwritten false
skinparam ClassBorderColor black
skinparam BackgroundColor #F0EDDE
skinparam ClassAttributeFontColor #222222
skinparam ClassFontStyle bold

skinparam class {
ArrowColor #3C88A3
ArrowFontColor #3C88A3
hide empty attributes
skinparam Handwritten false
skinparam ClassBorderColor black
BackgroundColor #FFFFFF
}
together {
class "AzureOpenAIChatModel" as acm {
+call(Prompt prompt): ChatResponse
+call(String prompt): String
+getDefaultOptions(): AzureOpenAiChatOptions
}
class "AzureOpenAiChatModel.Builder" as ab {
+defaultOptions(AzureOpenAiChatOptions options): AzureOpenAiChatModel.Builder
+build(): AzureOpenAIChatModel
}
class "AzureOpenAiChatOptions" as ao {
+getTemperature(): Double
+getMaxTokens(): Integer
+getModel(): String
+builder(): AzureOpenAiChatOptions.Builder
}

}

class "AzureOpenAiChatOptions.Builder" as aob {
+temperature(Double temperature): AzureOpenAiChatOptions.Builder
+maxTokens(Integer maxTokens): AzureOpenAiChatOptions.Builder
+model(String model): AzureOpenAiChatOptions.Builder
+build(): AzureOpenAiChatOptions
}
aob -down[hidden]- acm : dummy
acm <-down- ab : static nested
aob -left-> ao : static nested
acm .up.> ao : uses
@enduml
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package com.ks.azureopenai;

import static org.assertj.core.api.Assertions.assertThat;

import java.util.List;
import java.util.Map;

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.document.Document;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;

@SpringBootTest
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@ActiveProfiles("azureopenai")
public class SpringAIAzureOpenAiLiveTest {
final Logger logger = LoggerFactory.getLogger(SpringAIAzureOpenAiLiveTest.class);
@Autowired
private ChatService chatService;

@Autowired
private ChatService customChatService;

@Autowired
private VectorDBService vectorDBStore;

@BeforeAll
void setup() {
Document doc1 = new Document("""
TechVerse Solutions provides tailor-made
cloud integration services for finance and retail companies.
""");
Document doc2 = new Document("""
The company’s AI-powered analytics platform delivers actionable
insights to help customers optimize their business operations.
""");
Document doc3 = new Document("""
At TechVerse Solutions, dedicated experts support clients with 24/7
monitoring and rapid troubleshooting.
""");

List<Document> docs = List.of(doc1, doc2, doc3);
vectorDBStore.saveDocs(docs);
logger.info("Documents added to vector store");
}

@Test
void givenProgrammaticallyConfiguredClient_whenQueryLLM_thenRespond() {
String query = "TechVerse Solutions provides cloud " +
"integration services for what type of companies?";
String context = vectorDBStore.fetchContextFromVectorDB(query);
logger.info("context fetched: {}", context);
String prompt = """
Context: {context}
Question: {question}
Instructions:
Using the provided context, answer the question in a concise manner.
If the context does not contain the answer, respond with "I don't know".
""";
PromptTemplate promptTemplate = PromptTemplate.builder()
.template(prompt)
.variables(Map.of("context", context, "question", query))
.build();
Prompt finalPrompt = promptTemplate.create();

logger.info("finalPrompt: {}", finalPrompt.getContents());

String response = customChatService.chat(finalPrompt);

logger.info("response: {}", response);

assertThat(response)
.isNotNull()
.containsIgnoringCase("Finance")
.containsIgnoringCase("Retail");
}

@Test
void givenAutoConfiguredClient_whenQueryLLM_thenRespond() {
String query = "TechVerse Solutions provides cloud " +
"integration services for what type of companies?";
String context = vectorDBStore.fetchContextFromVectorDB(query);
logger.info("context fetched: {}", context);
String prompt = """
Context: {context}
Question: {question}
Instructions:
Using the provided context, answer the question in a concise manner.
If the context does not contain the answer, respond with "I don't know".
""";
PromptTemplate promptTemplate = PromptTemplate.builder()
.template(prompt)
.variables(Map.of("context", context, "question", query))
.build();
Prompt finalPrompt = promptTemplate.create();

logger.info("finalPrompt: {}", finalPrompt.getContents());

String response = chatService.chat(finalPrompt);

logger.info("response: {}", response);

assertThat(response)
.isNotNull()
.containsIgnoringCase("Finance")
.containsIgnoringCase("Retail");
}
}