diff --git a/springboot-modules/spring-ai/pom.xml b/springboot-modules/spring-ai/pom.xml
index 68252bd..e703ffe 100644
--- a/springboot-modules/spring-ai/pom.xml
+++ b/springboot-modules/spring-ai/pom.xml
@@ -27,6 +27,10 @@
spring-ai-spring-boot-testcontainers
test
+
+ org.springframework.ai
+ spring-ai-vector-store
+
org.testcontainers
junit-jupiter
@@ -50,7 +54,7 @@
com.fasterxml.jackson.core
jackson-core
- 2.17.2
+ 2.18.3
org.springframework.boot
@@ -62,6 +66,12 @@
hsqldb
${hsqldb.version}
+
+
+ org.springframework.ai
+ spring-ai-starter-model-azure-openai
+
+
diff --git a/springboot-modules/spring-ai/src/main/java/com/ks/azureopenai/ApplicationConfiguration.java b/springboot-modules/spring-ai/src/main/java/com/ks/azureopenai/ApplicationConfiguration.java
new file mode 100644
index 0000000..6efc37e
--- /dev/null
+++ b/springboot-modules/spring-ai/src/main/java/com/ks/azureopenai/ApplicationConfiguration.java
@@ -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()
+ );
+ }
+}
diff --git a/springboot-modules/spring-ai/src/main/java/com/ks/azureopenai/ChatApplication.java b/springboot-modules/spring-ai/src/main/java/com/ks/azureopenai/ChatApplication.java
new file mode 100644
index 0000000..5a10149
--- /dev/null
+++ b/springboot-modules/spring-ai/src/main/java/com/ks/azureopenai/ChatApplication.java
@@ -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);
+ }
+
+}
diff --git a/springboot-modules/spring-ai/src/main/java/com/ks/azureopenai/ChatService.java b/springboot-modules/spring-ai/src/main/java/com/ks/azureopenai/ChatService.java
new file mode 100644
index 0000000..97e3ceb
--- /dev/null
+++ b/springboot-modules/spring-ai/src/main/java/com/ks/azureopenai/ChatService.java
@@ -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();
+ }
+}
diff --git a/springboot-modules/spring-ai/src/main/java/com/ks/azureopenai/VectorDBService.java b/springboot-modules/spring-ai/src/main/java/com/ks/azureopenai/VectorDBService.java
new file mode 100644
index 0000000..ee901e3
--- /dev/null
+++ b/springboot-modules/spring-ai/src/main/java/com/ks/azureopenai/VectorDBService.java
@@ -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 docs) {
+ this.vectorStore.doAdd(docs);
+ }
+
+ public String fetchContextFromVectorDB(String query) {
+ List 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();
+ }
+
+}
diff --git a/springboot-modules/spring-ai/src/main/resources/application-azureopenai.properties b/springboot-modules/spring-ai/src/main/resources/application-azureopenai.properties
new file mode 100644
index 0000000..81d29f7
--- /dev/null
+++ b/springboot-modules/spring-ai/src/main/resources/application-azureopenai.properties
@@ -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
\ No newline at end of file
diff --git a/springboot-modules/spring-ai/src/main/resources/puml/azureopenai-cld.puml b/springboot-modules/spring-ai/src/main/resources/puml/azureopenai-cld.puml
new file mode 100644
index 0000000..1965fe8
--- /dev/null
+++ b/springboot-modules/spring-ai/src/main/resources/puml/azureopenai-cld.puml
@@ -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
\ No newline at end of file
diff --git a/springboot-modules/spring-ai/src/test/java/com/ks/azureopenai/SpringAIAzureOpenAiLiveTest.java b/springboot-modules/spring-ai/src/test/java/com/ks/azureopenai/SpringAIAzureOpenAiLiveTest.java
new file mode 100644
index 0000000..b5f90dd
--- /dev/null
+++ b/springboot-modules/spring-ai/src/test/java/com/ks/azureopenai/SpringAIAzureOpenAiLiveTest.java
@@ -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 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");
+ }
+}