diff --git a/.tanzu/config/spring-petclinic.yml b/.tanzu/config/spring-petclinic.yml
index 7bf5b9767..26c47b519 100644
--- a/.tanzu/config/spring-petclinic.yml
+++ b/.tanzu/config/spring-petclinic.yml
@@ -6,7 +6,7 @@ metadata:
spec:
nonSecretEnv:
- name: SPRING_PROFILES_ACTIVE
- value: postgres-openai
+ value: openai
build:
buildpacks: {}
nonSecretEnv:
diff --git a/build.gradle_ b/build.gradle
similarity index 90%
rename from build.gradle_
rename to build.gradle
index 397f898bc..cf3ea5681 100644
--- a/build.gradle_
+++ b/build.gradle
@@ -23,12 +23,15 @@ java {
repositories {
mavenCentral()
+ maven { url 'https://repo.spring.io/milestone' }
}
ext.webjarsFontawesomeVersion = "4.7.0"
ext.webjarsBootstrapVersion = "5.3.3"
+ext.springAiVersion = "1.0.0-M2"
dependencies {
+ implementation 'org.springframework.ai:spring-ai-azure-openai-spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
@@ -53,6 +56,12 @@ dependencies {
checkstyle 'com.puppycrawl.tools:checkstyle:10.16.0'
}
+dependencyManagement {
+ imports {
+ mavenBom "org.springframework.ai:spring-ai-bom:${springAiVersion}"
+ }
+}
+
tasks.named('test') {
useJUnitPlatform()
}
@@ -80,4 +89,4 @@ checkFormatAot.enabled = false
checkFormatAotTest.enabled = false
formatAot.enabled = false
-formatAotTest.enabled = false
+formatAotTest.enabled = false
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index c9bd00bdb..3070fa75c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -34,7 +34,7 @@
3.3.1
0.0.11
0.0.41
- 1.0.0-M1
+ 1.0.0-M2
diff --git a/readme.md b/readme.md
index 9dea4591b..057e923a1 100644
--- a/readme.md
+++ b/readme.md
@@ -75,6 +75,29 @@ or
```bash
docker-compose --profile postgres up
```
+## Integrating the Spring AI Chatbot
+
+Spring Petclinic integrates a Chatbot that allows you to interact with the application in a natural language. Here are some examples of what you could ask:
+
+1. Please list the owners that come to the clinic.
+2. How many vets are there?
+3. Is there an owner named Betty?
+4. Which owners have dogs?
+5. Add a dog for Betty. Its name is Moopsie.
+
+
+
+By default, The Spring AI Chatbot is disabled and will return the message `Chat is currently unavailable. Please try again later.`.
+
+Spring Petclinic currently supports OpenAI or Azure's OpenAI as the LLM provider.
+In order to enable Spring AI, perform the following steps:
+
+1. Decide which provider you want to use. By default, the `spring-ai-azure-openai-spring-boot-starter` dependency is enabled. You can change it to `spring-ai-openai-spring-boot-starter`in either`pom.xml` or in `build.gradle`, depending on your build tool of choice.
+2. Copy `src/main/resources/creds-template.yaml` into `src/main/resources/creds.yaml`, and edit its contents with your API key and API endpoint. Refer to OpenAI's or Azure's documentation for further information on how to obtain these. You only need to populate the provider you're using - either openai, or azure-openai.
+3. Boot your application with the `openai` profile. This profile will work for both LLM providers. You can boot the application with that profile using any of the following:
+- For maven: `mvn -Dspring-boot.run.profiles=openai spring-boot:run`
+- For Gradle: `./gradlew bootRun --args='--spring.profiles.active=openai'`
+- For a standard jar file: `SPRING_PROFILES_ACTIVE=openai java -jar build/libs/spring-petclinic-3.3.0.jar` or `SPRING_PROFILES_ACTIVE=openai java -jar target/spring-petclinic-3.3.0-SNAPSHOT.jar`.
## Test Applications
diff --git a/settings.gradle_ b/settings.gradle
similarity index 100%
rename from settings.gradle_
rename to settings.gradle
diff --git a/spring-ai.png b/spring-ai.png
new file mode 100644
index 000000000..441de4220
Binary files /dev/null and b/spring-ai.png differ
diff --git a/src/main/java/org/springframework/samples/petclinic/PetClinicApplication.java b/src/main/java/org/springframework/samples/petclinic/PetClinicApplication.java
index 6299a76f7..ac6e15030 100644
--- a/src/main/java/org/springframework/samples/petclinic/PetClinicApplication.java
+++ b/src/main/java/org/springframework/samples/petclinic/PetClinicApplication.java
@@ -18,11 +18,7 @@ package org.springframework.samples.petclinic;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
-import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ImportRuntimeHints;
-import org.springframework.web.servlet.i18n.SessionLocaleResolver;
-
-import java.util.Locale;
/**
* PetClinic Spring Boot Application.
diff --git a/src/main/java/org/springframework/samples/petclinic/genai/AIFunctionConfiguration.java b/src/main/java/org/springframework/samples/petclinic/genai/AIFunctionConfiguration.java
index f46fd8ea3..0a55aa493 100644
--- a/src/main/java/org/springframework/samples/petclinic/genai/AIFunctionConfiguration.java
+++ b/src/main/java/org/springframework/samples/petclinic/genai/AIFunctionConfiguration.java
@@ -6,16 +6,27 @@ 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.context.annotation.Profile;
import org.springframework.samples.petclinic.owner.Owner;
import org.springframework.samples.petclinic.owner.Pet;
import org.springframework.samples.petclinic.vet.Vet;
+/**
+ * This class defines the @Bean functions that the LLM provider will invoke when it
+ * requires more Information on a given topic. The currently available functions enable
+ * the LLM to get the list of owners and their pets, get information about the
+ * veterinarians, and add a pet to an owner.
+ *
+ * @author Oded Shopen
+ */
@Configuration
+@Profile("openai")
class AIFunctionConfiguration {
+ // The @Description annotation helps the model understand when to call the function
@Bean
@Description("List the owners that the pet clinic has")
- public Function listOwners(PetclinicAiProvider petclinicAiProvider) {
+ public Function listOwners(PetclinicAIProvider petclinicAiProvider) {
return request -> {
return petclinicAiProvider.getAllOwners();
};
@@ -23,15 +34,17 @@ class AIFunctionConfiguration {
@Bean
@Description("List the veterinarians that the pet clinic has")
- public Function listVets(PetclinicAiProvider petclinicAiProvider) {
+ public Function listVets(PetclinicAIProvider petclinicAiProvider) {
return request -> {
return petclinicAiProvider.getAllVets();
};
}
@Bean
- @Description("Add a pet to an owner identified by the ownerId")
- public Function addPetToOwner(PetclinicAiProvider petclinicAiProvider) {
+ @Description("Add a pet with the specified petTypeId, " + "to an owner identified by the ownerId. "
+ + "The allowed Pet types IDs are only: " + "1 - cat" + "2 - dog" + "3 - lizard" + "4 - snake" + "5 - bird"
+ + "6 - hamster")
+ public Function addPetToOwner(PetclinicAIProvider petclinicAiProvider) {
return request -> {
return petclinicAiProvider.addPetToOwner(request);
};
@@ -39,7 +52,7 @@ class AIFunctionConfiguration {
}
-record AddPetRequest(Pet pet, Integer ownerId) {
+record AddPetRequest(Pet pet, String petType, Integer ownerId) {
};
record OwnerRequest(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
index 313b35abe..5b0b39721 100644
--- a/src/main/java/org/springframework/samples/petclinic/genai/LoggingAdvisor.java
+++ b/src/main/java/org/springframework/samples/petclinic/genai/LoggingAdvisor.java
@@ -2,14 +2,23 @@ package org.springframework.samples.petclinic.genai;
import java.util.Map;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
import org.springframework.ai.chat.client.AdvisedRequest;
import org.springframework.ai.chat.client.RequestResponseAdvisor;
+/**
+ * A ChatClient Advisor that adds logs on the requests being sent to the LLM.
+ *
+ * @author Oded Shopen
+ */
public class LoggingAdvisor implements RequestResponseAdvisor {
+ private static final Log log = LogFactory.getLog(LoggingAdvisor.class);
+
@Override
public AdvisedRequest adviseRequest(AdvisedRequest request, Map context) {
- System.out.println("Request: " + request);
+ log.info("Request: {}" + request);
return request;
}
diff --git a/src/main/java/org/springframework/samples/petclinic/genai/PetclinicAIConfiguration.java b/src/main/java/org/springframework/samples/petclinic/genai/PetclinicAIConfiguration.java
index c86888388..4a7c344cf 100644
--- a/src/main/java/org/springframework/samples/petclinic/genai/PetclinicAIConfiguration.java
+++ b/src/main/java/org/springframework/samples/petclinic/genai/PetclinicAIConfiguration.java
@@ -4,9 +4,16 @@ 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.context.annotation.Profile;
import org.springframework.web.client.RestClient;
+/**
+ * A Configuration class for beans used by the Chat Client.
+ *
+ * @author Oded Shopen
+ */
@Configuration
+@Profile("openai")
public class PetclinicAIConfiguration {
@Bean
diff --git a/src/main/java/org/springframework/samples/petclinic/genai/PetclinicAiProvider.java b/src/main/java/org/springframework/samples/petclinic/genai/PetclinicAIProvider.java
similarity index 78%
rename from src/main/java/org/springframework/samples/petclinic/genai/PetclinicAiProvider.java
rename to src/main/java/org/springframework/samples/petclinic/genai/PetclinicAIProvider.java
index 364c20659..420f6a12a 100644
--- a/src/main/java/org/springframework/samples/petclinic/genai/PetclinicAiProvider.java
+++ b/src/main/java/org/springframework/samples/petclinic/genai/PetclinicAIProvider.java
@@ -1,5 +1,6 @@
package org.springframework.samples.petclinic.genai;
+import org.springframework.context.annotation.Profile;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
@@ -9,14 +10,21 @@ import org.springframework.samples.petclinic.vet.Vet;
import org.springframework.samples.petclinic.vet.VetRepository;
import org.springframework.stereotype.Service;
+/**
+ * Functions that are invoked by the LLM will use this bean to query the system of record
+ * for information such as listing owners and vers, or adding pets to an owner.
+ *
+ * @author Oded Shopen
+ */
@Service
-public class PetclinicAiProvider {
+@Profile("openai")
+public class PetclinicAIProvider {
OwnerRepository ownerRepository;
VetRepository vetRepository;
- public PetclinicAiProvider(OwnerRepository ownerRepository, VetRepository vetRepository) {
+ public PetclinicAIProvider(OwnerRepository ownerRepository, VetRepository vetRepository) {
this.ownerRepository = ownerRepository;
this.vetRepository = vetRepository;
}
diff --git a/src/main/java/org/springframework/samples/petclinic/genai/PetclinicChatClient.java b/src/main/java/org/springframework/samples/petclinic/genai/PetclinicChatClient.java
index bdb6ddc6d..b39ad13a0 100644
--- a/src/main/java/org/springframework/samples/petclinic/genai/PetclinicChatClient.java
+++ b/src/main/java/org/springframework/samples/petclinic/genai/PetclinicChatClient.java
@@ -5,13 +5,20 @@ import static org.springframework.ai.chat.client.advisor.AbstractChatMemoryAdvis
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.context.annotation.Profile;
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;
+/**
+ * This REST controller is being invoked by the in order to interact with the LLM
+ *
+ * @author Oded Shopen
+ */
@RestController
@RequestMapping("/")
+@Profile("openai")
public class PetclinicChatClient {
// ChatModel is the primary interfaces for interacting with an LLM
@@ -32,6 +39,7 @@ public class PetclinicChatClient {
If you do know the answer, provide the answer but do not provide any additional helpful followup questions.
""")
.defaultAdvisors(
+ // Chat memory helps us keep context when using the chatbot for up to 10 previous messages.
new MessageChatMemoryAdvisor(chatMemory, DEFAULT_CHAT_MEMORY_CONVERSATION_ID, 10), // CHAT MEMORY
new LoggingAdvisor()
)
@@ -40,6 +48,7 @@ public class PetclinicChatClient {
@PostMapping("/chatclient")
public String exchange(@RequestBody String query) {
+ //All chatbot messages go through this endpoint and are passed to the LLM
return
this.chatClient
.prompt()
diff --git a/src/main/java/org/springframework/samples/petclinic/genai/PetclinicDisabledChatClient.java b/src/main/java/org/springframework/samples/petclinic/genai/PetclinicDisabledChatClient.java
new file mode 100644
index 000000000..eaf0660ee
--- /dev/null
+++ b/src/main/java/org/springframework/samples/petclinic/genai/PetclinicDisabledChatClient.java
@@ -0,0 +1,25 @@
+package org.springframework.samples.petclinic.genai;
+
+import org.springframework.context.annotation.Profile;
+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;
+
+/**
+ * This REST controller implements a default behavior for the chat client when AI profile
+ * is not in use. It will return a default message that chat is not available.
+ *
+ * @author Oded Shopen
+ */
+@RestController
+@RequestMapping("/")
+@Profile("!openai")
+public class PetclinicDisabledChatClient {
+
+ @PostMapping("/chatclient")
+ public String exchange(@RequestBody String query) {
+ return "Chat is currently unavailable. Please try again later.";
+ }
+
+}
diff --git a/src/main/resources/application-openai.properties b/src/main/resources/application-openai.properties
new file mode 100644
index 000000000..ff9561a17
--- /dev/null
+++ b/src/main/resources/application-openai.properties
@@ -0,0 +1,15 @@
+spring.config.import=optional:classpath:/creds.yaml
+
+#These apply when using spring-ai-azure-openai-spring-boot-starter
+spring.ai.azure.openai.chat.options.functions=listOwners,listVets,addPetToOwner
+spring.ai.azure.openai.chat.options.temperature: 0.7
+
+#These apply when using spring-ai-openai-spring-boot-starter
+spring.ai.openai.chat.options.functions=listOwners,listVets,addPetToOwner
+spring.ai.openai.chat.options.temperature: 0.7
+
+#Enable Spring AI by default
+spring.ai.chat.client.enabled=true
+
+
+
diff --git a/src/main/resources/application-postgres-openai.properties b/src/main/resources/application-postgres-openai.properties
deleted file mode 100644
index 587daad8b..000000000
--- a/src/main/resources/application-postgres-openai.properties
+++ /dev/null
@@ -1,9 +0,0 @@
-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,listVets,addPetToOwner
-spring.ai.azure.openai.chat.options.temperature: 0.7
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 5d3eeed32..bd16242bc 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -23,3 +23,9 @@ logging.level.org.springframework=INFO
# Maximum time static resources should be cached
spring.web.resources.cache.cachecontrol.max-age=12h
+
+#Disable Spring AI by default
+spring.ai.chat.client.enabled=false
+#Currently these properties require dummy values when a spring AI is in the classpath, even when chat is disabled.
+spring.ai.azure.openai.api-key=dummy
+spring.ai.azure.openai.endpoint=dummy
diff --git a/src/main/resources/creds-template.yaml b/src/main/resources/creds-template.yaml
index 57f742207..efa0c15c7 100644
--- a/src/main/resources/creds-template.yaml
+++ b/src/main/resources/creds-template.yaml
@@ -1,5 +1,6 @@
spring:
ai:
+ #These parameters only apply when using the spring-ai-azure-openai-spring-boot-starter dependency:
azure:
openai:
api-key: ""
@@ -7,3 +8,10 @@ spring:
chat:
options:
deployment-name: "gpt-4o"
+ #These parameters only apply when using the spring-ai-openai-spring-boot-starter dependency:
+ openai:
+ api-key: ""
+ endpoint: ""
+ chat:
+ options:
+ deployment-name: "gpt-4o"
\ No newline at end of file