first implementation of a chatbot for Spring Petclinic. Supports quering the owners, also guides the user through adding a pet to an owner, but currently fails to call the addPetToOwner function.

This commit is contained in:
Oded Shopen 2024-09-10 19:44:11 +03:00
parent b9da800a21
commit 2371f6af56
9 changed files with 407 additions and 3 deletions

18
pom.xml
View file

@ -34,11 +34,15 @@
<maven-checkstyle.version>3.3.1</maven-checkstyle.version>
<nohttp-checkstyle.version>0.0.11</nohttp-checkstyle.version>
<spring-format.version>0.0.41</spring-format.version>
<spring-ai.version>1.0.0-M1</spring-ai.version>
</properties>
<dependencies>
<!-- Spring and Spring Boot dependencies -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-azure-openai-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
@ -141,6 +145,18 @@
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>

View file

@ -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<OwnerRequest, OwnersResponse> listOwners(PetclinicAiProvider petclinicAiProvider) {
return request -> {
return petclinicAiProvider.getAllOwners();
};
}
@Bean
@Description("Add a pet to an owner identified by the ownerId")
public Function<AddPetRequest, AddedPetResponse> addPetToOwner(PetclinicAiProvider petclinicAiProvider) {
return request -> {
return petclinicAiProvider.addPetToOwner(request);
};
}
}
record AddPetRequest (Pet pet, Integer ownerId) {};
record OwnerRequest (Owner owner) {};
record OwnersResponse(List<Owner> owners) {};
record AddedPetResponse(Owner owner) {};

View file

@ -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<String, Object> context) {
System.out.println("Request: " + request);
return request;
}
}

View file

@ -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();
}
}

View file

@ -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<Owner> 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);
}
}

View file

@ -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();
}
}

View file

@ -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

View file

@ -9518,3 +9518,117 @@ strong {
margin-bottom: 30px; } }
/*# 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;
}

View file

@ -70,6 +70,7 @@
</div>
</div>
</nav>
<div class="container-fluid">
<div class="container xd-container">
@ -87,7 +88,118 @@
</div>
</div>
<script th:src="@{/webjars/bootstrap/5.3.3/dist/js/bootstrap.bundle.min.js}"></script>
<div class="chatbox" id="chatbox">
<div class="chatbox-header" onclick="toggleChatbox()">
Chat with Us!
</div>
<div class="chatbox-content" id="chatbox-content">
<div class="chatbox-messages" id="chatbox-messages">
<!-- Chat messages will be dynamically inserted here -->
</div>
<div class="chatbox-footer">
<input type="text" id="chatbox-input" placeholder="Type a message..." onkeydown="handleKeyPress(event)" />
<button onclick="sendMessage()">Send</button>
</div>
</div>
</div>
<!-- JavaScript for handling chatbox interaction -->
<script>
function appendMessage(message, type) {
const chatMessages = document.getElementById('chatbox-messages');
const messageElement = document.createElement('div');
messageElement.classList.add('chat-bubble', type);
// Convert Markdown to HTML
const htmlContent = marked.parse(message); // Use marked.parse() for newer versions
messageElement.innerHTML = htmlContent;
chatMessages.appendChild(messageElement);
// Scroll to the bottom of the chatbox to show the latest message
chatMessages.scrollTop = chatMessages.scrollHeight;
}
function toggleChatbox() {
const chatbox = document.getElementById('chatbox');
const chatboxContent = document.getElementById('chatbox-content');
if (chatbox.classList.contains('minimized')) {
chatbox.classList.remove('minimized');
chatboxContent.style.height = '400px'; // Set to initial height when expanded
} else {
chatbox.classList.add('minimized');
chatboxContent.style.height = '40px'; // Set to minimized height
}
}
function sendMessage() {
const query = document.getElementById('chatbox-input').value;
// Only send if there's a message
if (!query.trim()) return;
// Clear the input field after sending the message
document.getElementById('chatbox-input').value = '';
// Display user message in the chatbox
appendMessage(query, 'user');
// Send the message to the backend
fetch('/chatclient', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(query),
})
.then(response => response.text())
.then(responseText => {
// Display the response from the server in the chatbox
appendMessage(responseText, 'bot');
})
.catch(error => console.error('Error:', error));
}
function handleKeyPress(event) {
if (event.key === "Enter") {
event.preventDefault(); // Prevents adding a newline
sendMessage(); // Send the message when Enter is pressed
}
}
// Save chat messages to localStorage
function saveChatMessages() {
const messages = document.getElementById('chatbox-messages').innerHTML;
localStorage.setItem('chatMessages', messages);
}
// Load chat messages from localStorage
function loadChatMessages() {
const messages = localStorage.getItem('chatMessages');
if (messages) {
document.getElementById('chatbox-messages').innerHTML = messages;
document.getElementById('chatbox-messages').scrollTop = document.getElementById('chatbox-messages').scrollHeight;
}
}
// Call loadChatMessages when the page loads
window.onload = loadChatMessages;
// Ensure messages are saved when navigating away
window.onbeforeunload = saveChatMessages;
</script>
<script th:src="@{/webjars/bootstrap/5.3.3/dist/js/bootstrap.bundle.min.js}"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
</body>