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 @@ +
@@ -87,7 +88,118 @@
- + +
+
+ Chat with Us! +
+
+
+ +
+ +
+
+ + + + + + + + + + + + +