mirror of
https://github.com/spring-projects/spring-petclinic.git
synced 2025-07-20 14:55:50 +00:00
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:
parent
b9da800a21
commit
2371f6af56
9 changed files with 407 additions and 3 deletions
18
pom.xml
18
pom.xml
|
@ -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>
|
||||
|
|
|
@ -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) {};
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
Loading…
Reference in a new issue