diff --git a/pom.xml b/pom.xml
index c948f9e33..c9bd00bdb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -34,11 +34,15 @@
3.3.1
0.0.11
0.0.41
-
+ 1.0.0-M1
+
+ org.springframework.ai
+ spring-ai-azure-openai-spring-boot-starter
+
org.springframework.boot
spring-boot-starter-actuator
@@ -141,6 +145,18 @@
+
+
+
+ org.springframework.ai
+ spring-ai-bom
+ ${spring-ai.version}
+ pom
+ import
+
+
+
+
diff --git a/src/main/java/org/springframework/samples/petclinic/genai/AIFunctionConfiguration.java b/src/main/java/org/springframework/samples/petclinic/genai/AIFunctionConfiguration.java
new file mode 100644
index 000000000..0c4e727e0
--- /dev/null
+++ b/src/main/java/org/springframework/samples/petclinic/genai/AIFunctionConfiguration.java
@@ -0,0 +1,35 @@
+package org.springframework.samples.petclinic.genai;
+
+import java.util.List;
+import java.util.function.Function;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Description;
+import org.springframework.samples.petclinic.owner.Owner;
+import org.springframework.samples.petclinic.owner.Pet;
+
+@Configuration
+class AIFunctionConfiguration {
+
+ @Bean
+ @Description("List the owners that the pet clinic has")
+ public Function listOwners(PetclinicAiProvider petclinicAiProvider) {
+ return request -> {
+ return petclinicAiProvider.getAllOwners();
+ };
+ }
+
+ @Bean
+ @Description("Add a pet to an owner identified by the ownerId")
+ public Function addPetToOwner(PetclinicAiProvider petclinicAiProvider) {
+ return request -> {
+ return petclinicAiProvider.addPetToOwner(request);
+ };
+ }
+}
+
+record AddPetRequest (Pet pet, Integer ownerId) {};
+record OwnerRequest (Owner owner) {};
+record OwnersResponse(List owners) {};
+record AddedPetResponse(Owner owner) {};
diff --git a/src/main/java/org/springframework/samples/petclinic/genai/LoggingAdvisor.java b/src/main/java/org/springframework/samples/petclinic/genai/LoggingAdvisor.java
new file mode 100644
index 000000000..3ae8c5d47
--- /dev/null
+++ b/src/main/java/org/springframework/samples/petclinic/genai/LoggingAdvisor.java
@@ -0,0 +1,15 @@
+package org.springframework.samples.petclinic.genai;
+
+import java.util.Map;
+
+import org.springframework.ai.chat.client.AdvisedRequest;
+import org.springframework.ai.chat.client.RequestResponseAdvisor;
+
+public class LoggingAdvisor implements RequestResponseAdvisor {
+
+ @Override
+ public AdvisedRequest adviseRequest(AdvisedRequest request, Map context) {
+ System.out.println("Request: " + request);
+ return request;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/springframework/samples/petclinic/genai/PetclinicAIConfiguration.java b/src/main/java/org/springframework/samples/petclinic/genai/PetclinicAIConfiguration.java
new file mode 100644
index 000000000..924399bf7
--- /dev/null
+++ b/src/main/java/org/springframework/samples/petclinic/genai/PetclinicAIConfiguration.java
@@ -0,0 +1,20 @@
+package org.springframework.samples.petclinic.genai;
+
+import org.springframework.ai.chat.memory.ChatMemory;
+import org.springframework.ai.chat.memory.InMemoryChatMemory;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.client.RestClient;
+
+@Configuration
+public class PetclinicAIConfiguration {
+ @Bean
+ public RestClient restClient() {
+ return RestClient.create();
+ }
+
+ @Bean
+ public ChatMemory chatMemory() {
+ return new InMemoryChatMemory();
+ }
+}
diff --git a/src/main/java/org/springframework/samples/petclinic/genai/PetclinicAiProvider.java b/src/main/java/org/springframework/samples/petclinic/genai/PetclinicAiProvider.java
new file mode 100644
index 000000000..33f898d78
--- /dev/null
+++ b/src/main/java/org/springframework/samples/petclinic/genai/PetclinicAiProvider.java
@@ -0,0 +1,31 @@
+package org.springframework.samples.petclinic.genai;
+
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.samples.petclinic.owner.Owner;
+import org.springframework.samples.petclinic.owner.OwnerRepository;
+import org.springframework.stereotype.Service;
+
+@Service
+public class PetclinicAiProvider {
+ OwnerRepository ownerRepository;
+
+ public PetclinicAiProvider(OwnerRepository ownerRepository) {
+ this.ownerRepository = ownerRepository;
+ }
+
+ public OwnersResponse getAllOwners() {
+ Pageable pageable = PageRequest.of(0, Integer.MAX_VALUE);
+ Page ownerPage = ownerRepository.findAll(pageable);
+ return new OwnersResponse(ownerPage.getContent());
+ }
+
+ public AddedPetResponse addPetToOwner(AddPetRequest request) {
+ Owner owner = ownerRepository.findById(request.ownerId());
+ owner.addPet(request.pet());
+ this.ownerRepository.save(owner);
+ return new AddedPetResponse(owner);
+ }
+
+}
diff --git a/src/main/java/org/springframework/samples/petclinic/genai/PetclinicChatClient.java b/src/main/java/org/springframework/samples/petclinic/genai/PetclinicChatClient.java
new file mode 100644
index 000000000..250fa3eb4
--- /dev/null
+++ b/src/main/java/org/springframework/samples/petclinic/genai/PetclinicChatClient.java
@@ -0,0 +1,52 @@
+package org.springframework.samples.petclinic.genai;
+
+import static org.springframework.ai.chat.client.advisor.AbstractChatMemoryAdvisor.DEFAULT_CHAT_MEMORY_CONVERSATION_ID;
+
+import org.springframework.ai.chat.client.ChatClient;
+import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
+import org.springframework.ai.chat.memory.ChatMemory;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+@RestController
+@RequestMapping("/")
+public class PetclinicChatClient {
+
+ // ChatModel is the primary interfaces for interacting with an LLM
+ // it is a request/response interface that implements the ModelModel
+ // interface. Make suer to visit the source code of the ChatModel and
+ // checkout the interfaces in the core spring ai package.
+ private final ChatClient chatClient;
+
+ public PetclinicChatClient(ChatClient.Builder builder, ChatMemory chatMemory) {
+ // @formatter:off
+ this.chatClient = builder
+ .defaultSystem("""
+ You are a friendly AI assistant designed to help with the management of a veterinarian pet clinic called Spring Petclinic.
+ Your job is to answer questions about the existing veterinarians and to perform actions on the customer's behalf, mainly around
+ pet owners, their pets and their visits.
+ You are required to answer an a professional manner. If you don't know the answer, politely tell the customer
+ you don't know the answer, then ask the customer a followup qusetion to try and clarify the question they are asking.
+ If you do know the answer, provide the answer but do not provide any additional helpful followup questions.
+ """)
+ .defaultAdvisors(
+ new MessageChatMemoryAdvisor(chatMemory, DEFAULT_CHAT_MEMORY_CONVERSATION_ID, 10), // CHAT MEMORY
+ new LoggingAdvisor()
+ )
+ .build();
+ }
+
+ @PostMapping("/chatclient")
+ public String exchange(@RequestBody String query) {
+ return
+ this.chatClient
+ .prompt()
+ .user(
+ u ->
+ u.text(query)
+ )
+ .call()
+ .content();
+ }
+}
diff --git a/src/main/resources/application-postgres-openai.properties b/src/main/resources/application-postgres-openai.properties
new file mode 100644
index 000000000..6ec85c0f0
--- /dev/null
+++ b/src/main/resources/application-postgres-openai.properties
@@ -0,0 +1,9 @@
+database=postgres
+spring.config.import=optional:classpath:/creds.yaml
+spring.datasource.url=${POSTGRES_URL:jdbc:postgresql://localhost/petclinic}
+spring.datasource.username=${POSTGRES_USER:petclinic}
+spring.datasource.password=${POSTGRES_PASS:petclinic}
+# SQL is written to be idempotent so this is safe
+spring.sql.init.mode=always
+spring.ai.azure.openai.chat.options.functions=listOwners,addPetToOwner
+spring.ai.azure.openai.chat.options.temperature: 0.7
diff --git a/src/main/resources/static/resources/css/petclinic.css b/src/main/resources/static/resources/css/petclinic.css
index bbf3f0dbb..4cb124baf 100644
--- a/src/main/resources/static/resources/css/petclinic.css
+++ b/src/main/resources/static/resources/css/petclinic.css
@@ -9517,4 +9517,118 @@ strong {
margin-top: 10px;
margin-bottom: 30px; } }
-/*# sourceMappingURL=../../../../../../target/petclinic.css.map */
\ No newline at end of file
+/*# sourceMappingURL=../../../../../../target/petclinic.css.map */
+
+
+
+/* Chatbox container */
+.chatbox {
+ position: fixed;
+ bottom: 10px;
+ right: 10px;
+ width: 300px;
+ background-color: #f1f1f1;
+ border-radius: 10px;
+ box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
+ display: flex;
+ flex-direction: column;
+}
+
+/* Header styling */
+.chatbox-header {
+ background-color: #075E54;
+ color: white;
+ padding: 10px;
+ text-align: center;
+ border-top-left-radius: 10px;
+ border-top-right-radius: 10px;
+ cursor: pointer;
+}
+
+/* Chatbox content styling */
+.chatbox-content {
+ display: flex;
+ flex-direction: column;
+ height: 400px; /* Adjust to desired height */
+ overflow: hidden; /* Hide overflow to make it scrollable */
+}
+
+/* Minimize style */
+.chatbox.minimized .chatbox-content {
+ height: 40px; /* Height when minimized (header only) */
+}
+
+.chatbox.minimized .chatbox-messages,
+.chatbox.minimized .chatbox-footer {
+ display: none;
+}
+
+.chatbox-messages {
+ flex-grow: 1;
+ overflow-y: auto; /* Allows vertical scrolling */
+ padding: 10px;
+}
+
+/* Chat bubbles styling */
+.chat-bubble {
+ max-width: 80%;
+ padding: 10px;
+ border-radius: 20px;
+ margin-bottom: 10px;
+ position: relative;
+ word-wrap: break-word;
+ font-size: 14px;
+}
+
+/* Ensure bold and italic styles are handled */
+.chat-bubble strong {
+ font-weight: bold;
+}
+
+.chat-bubble em {
+ font-style: italic;
+}
+
+.chat-bubble.user {
+ background-color: #dcf8c6; /* WhatsApp-style light green */
+ margin-left: auto;
+ text-align: right;
+ border-bottom-right-radius: 0;
+}
+
+.chat-bubble.bot {
+ background-color: #ffffff;
+ margin-right: auto;
+ text-align: left;
+ border-bottom-left-radius: 0;
+ border: 1px solid #e1e1e1;
+}
+
+/* Input field and button */
+.chatbox-footer {
+ padding: 10px;
+ background-color: #f9f9f9;
+ display: flex;
+}
+
+.chatbox-footer input {
+ flex-grow: 1;
+ padding: 10px;
+ border-radius: 20px;
+ border: 1px solid #ccc;
+ margin-right: 10px;
+ outline: none;
+}
+
+.chatbox-footer button {
+ background-color: #075E54;
+ color: white;
+ border: none;
+ padding: 10px;
+ border-radius: 50%;
+ cursor: pointer;
+}
+
+.chatbox-footer button:hover {
+ background-color: #128C7E;
+}
diff --git a/src/main/resources/templates/fragments/layout.html b/src/main/resources/templates/fragments/layout.html
index 3e9bc398b..83622b0bf 100755
--- a/src/main/resources/templates/fragments/layout.html
+++ b/src/main/resources/templates/fragments/layout.html
@@ -70,6 +70,7 @@
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+