diff --git a/.gitignore b/.gitignore
index e282e3b23..1102f15c4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,3 +19,5 @@ target/
# Branch switching
generated/
+
+**/.DS_Store
diff --git a/README.md b/README.md
index 0ce9ecbd5..a8e948fb8 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# Distributed version of the Spring PetClinic Sample Application built with Spring Cloud
+# Distributed version of the Spring PetClinic Sample Application built with Spring Cloud and Spring AI
[![Build Status](https://github.com/spring-petclinic/spring-petclinic-microservices/actions/workflows/maven-build.yml/badge.svg)](https://github.com/spring-petclinic/spring-petclinic-microservices/actions/workflows/maven-build.yml)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
@@ -17,7 +17,7 @@ If everything goes well, you can access the following services at given location
* Discovery Server - http://localhost:8761
* Config Server - http://localhost:8888
* AngularJS frontend (API Gateway) - http://localhost:8080
-* Customers, Vets and Visits Services - random port, check Eureka Dashboard
+* Customers, Vets, Visits and GenAI Services - random port, check Eureka Dashboard
* Tracing Server (Zipkin) - http://localhost:9411/zipkin/ (we use [openzipkin](https://github.com/openzipkin/zipkin/tree/main/zipkin-server))
* Admin Server (Spring Boot Admin) - http://localhost:9090
* Grafana Dashboards - http://localhost:3000
@@ -46,7 +46,7 @@ For instance, if you target container images for an Apple M2, you could use the
```
Once images are ready, you can start them with a single command
-`docker-compose up` or `podman-compose up`.
+`docker compose up` or `podman-compose up`.
Containers startup order is coordinated with the `service_healthy` condition of the Docker Compose [depends-on](https://github.com/compose-spec/compose-spec/blob/main/spec.md#depends_on) expression
and the [healthcheck](https://github.com/compose-spec/compose-spec/blob/main/spec.md#healthcheck) of the service containers.
@@ -79,6 +79,7 @@ This project consists of several microservices:
- **Customers Service**: Manages customer data.
- **Vets Service**: Handles information about veterinarians.
- **Visits Service**: Manages pet visit records.
+- **GenAI Service**: Provides a chatbot interface to the application.
- **API Gateway**: Routes client requests to the appropriate services.
- **Config Server**: Centralized configuration management for all services.
- **Discovery Server**: Eureka-based service registry.
@@ -93,6 +94,41 @@ Each service has its own specific role and communicates via REST APIs.
![Spring Petclinic Microservices architecture](docs/microservices-architecture-diagram.jpg)
+## 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. Are there any vets that specialize in surgery?
+3. Is there an owner named Betty?
+4. Which owners have dogs?
+5. Add a dog for Betty. Its name is Moopsie.
+6. Create a new owner.
+
+![Screenshot of the chat dialog](docs/spring-ai.png)
+
+This `spring-petlinic-genai-service` microservice currently supports **OpenAI** (default) or **Azure's OpenAI** as the LLM provider.
+In order to start the microservice, perform the following steps:
+
+1. Decide which provider you want to use. By default, the `spring-ai-openai-spring-boot-starter` dependency is enabled.
+ You can change it to `spring-ai-azure-openai-spring-boot-starter`in the `pom.xml`.
+2. Create an OpenAI API key or a Azure OpenAI resource in your Azure Portal.
+ Refer to the [OpenAI's quickstart](https://platform.openai.com/docs/quickstart) or [Azure's documentation](https://learn.microsoft.com/en-us/azure/ai-services/openai/) for further information on how to obtain these.
+ You only need to populate the provider you're using - either openai, or azure-openai.
+ If you don't have your own OpenAI API key, don't worry!
+ You can temporarily use the `demo` key, which OpenAI provides free of charge for demonstration purposes.
+ This `demo` key has a quota, is limited to the `gpt-4o-mini` model, and is intended solely for demonstration use.
+ With your own OpenAI account, you can test the `gpt-4o` model by modifying the `deployment-name` property of the `application.yml` file.
+3. Export your API keys and endpoint as environment variables:
+ * either OpenAI:
+ ```bash
+ export OPENAI_API_KEY="your_api_key_here"
+ ```
+ * or Azure OpenAI:
+ ```bash
+ export AZURE_OPENAI_ENDPOINT="https://your_resource.openai.azure.com"
+ export AZURE_OPENAI_KEY="your_api_key_here"
+ ```
## In case you find a bug/suggested improvement for Spring Petclinic Microservices
diff --git a/docker-compose.yml b/docker-compose.yml
index 49ab25958..a29ef0ba2 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,5 +1,3 @@
-version: '3'
-
services:
config-server:
image: springcommunity/spring-petclinic-config-server
@@ -79,6 +77,27 @@ services:
ports:
- 8083:8083
+
+ genai-service:
+ image: springcommunity/spring-petclinic-genai-service
+ container_name: genai-service
+ environment:
+ - OPENAI_API_KEY=${OPENAI_API_KEY}
+ - AZURE_OPENAI_KEY=${AZURE_OPENAI_KEY}
+ - AZURE_OPENAI_ENDPOINT=${AZURE_OPENAI_ENDPOINT}
+ deploy:
+ resources:
+ limits:
+ memory: 512M
+ depends_on:
+ config-server:
+ condition: service_healthy
+ discovery-server:
+ condition: service_healthy
+ ports:
+ - 8084:8084
+
+
api-gateway:
image: springcommunity/spring-petclinic-api-gateway
container_name: api-gateway
@@ -131,7 +150,7 @@ services:
limits:
memory: 256M
ports:
- - 3000:3000
+ - 3030:3030
prometheus-server:
build: ./docker/prometheus
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 62e2f25ad..3a0d65f4c 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -1,4 +1,4 @@
-FROM eclipse-temurin:17 as builder
+FROM eclipse-temurin:17 AS builder
WORKDIR application
ARG ARTIFACT_NAME
COPY ${ARTIFACT_NAME}.jar application.jar
@@ -11,7 +11,7 @@ WORKDIR application
ARG EXPOSED_PORT
EXPOSE ${EXPOSED_PORT}
-ENV SPRING_PROFILES_ACTIVE docker
+ENV SPRING_PROFILES_ACTIVE=docker
COPY --from=builder application/dependencies/ ./
diff --git a/docs/microservices-architecture-diagram.jpg b/docs/microservices-architecture-diagram.jpg
index d22c1cdde..1d3443988 100644
Binary files a/docs/microservices-architecture-diagram.jpg and b/docs/microservices-architecture-diagram.jpg differ
diff --git a/docs/spring-ai.png b/docs/spring-ai.png
new file mode 100644
index 000000000..441de4220
Binary files /dev/null and b/docs/spring-ai.png differ
diff --git a/pom.xml b/pom.xml
index 8d42404e8..0ce4bc86c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,12 +6,12 @@
org.springframework.boot
spring-boot-starter-parent
- 3.2.7
+ 3.3.4
org.springframework.samples
spring-petclinic-microservices
- 3.2.7
+ 3.3.4
${project.artifactId}
pom
@@ -20,6 +20,7 @@
spring-petclinic-customers-service
spring-petclinic-vets-service
spring-petclinic-visits-service
+ spring-petclinic-genai-service
spring-petclinic-config-server
spring-petclinic-discovery-server
spring-petclinic-api-gateway
diff --git a/scripts/run_all.sh b/scripts/run_all.sh
index 8560c917e..18aec7e9b 100755
--- a/scripts/run_all.sh
+++ b/scripts/run_all.sh
@@ -7,10 +7,10 @@ set -o pipefail
pkill -9 -f spring-petclinic || echo "Failed to kill any apps"
-docker-compose kill || echo "No docker containers are running"
+docker compose kill || echo "No docker containers are running"
echo "Running infra"
-docker-compose up -d grafana-server prometheus-server tracing-server
+docker compose up -d grafana-server prometheus-server tracing-server
echo "Running apps"
mkdir -p target
@@ -23,6 +23,7 @@ sleep 20
nohup java -jar spring-petclinic-customers-service/target/*.jar --server.port=8081 --spring.profiles.active=chaos-monkey > target/customers-service.log 2>&1 &
nohup java -jar spring-petclinic-visits-service/target/*.jar --server.port=8082 --spring.profiles.active=chaos-monkey > target/visits-service.log 2>&1 &
nohup java -jar spring-petclinic-vets-service/target/*.jar --server.port=8083 --spring.profiles.active=chaos-monkey > target/vets-service.log 2>&1 &
+nohup java -jar spring-petclinic-genai-service/target/*.jar --server.port=8084 --spring.profiles.active=chaos-monkey > target/genai-service.log 2>&1 &
nohup java -jar spring-petclinic-api-gateway/target/*.jar --server.port=8080 --spring.profiles.active=chaos-monkey > target/gateway-service.log 2>&1 &
nohup java -jar spring-petclinic-admin-server/target/*.jar --server.port=9090 --spring.profiles.active=chaos-monkey > target/admin-server.log 2>&1 &
echo "Waiting for apps to start"
diff --git a/spring-petclinic-admin-server/pom.xml b/spring-petclinic-admin-server/pom.xml
index 2c537636a..e8faa0f84 100644
--- a/spring-petclinic-admin-server/pom.xml
+++ b/spring-petclinic-admin-server/pom.xml
@@ -12,7 +12,7 @@
org.springframework.samples
spring-petclinic-microservices
- 3.2.7
+ 3.3.4
diff --git a/spring-petclinic-api-gateway/pom.xml b/spring-petclinic-api-gateway/pom.xml
index 2cdb6259b..4db970d78 100644
--- a/spring-petclinic-api-gateway/pom.xml
+++ b/spring-petclinic-api-gateway/pom.xml
@@ -11,7 +11,7 @@
org.springframework.samples
spring-petclinic-microservices
- 3.2.7
+ 3.3.4
@@ -19,6 +19,8 @@
4.7.0
1.8.3
1.0.30
+ 14.1.2
+
0.2.29
8081
${basedir}/../docker
@@ -124,6 +126,11 @@
org.webjars
webjars-locator-core
+
+ org.webjars.npm
+ marked
+ ${webjars-marked.version}
+
diff --git a/spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/ApiGatewayApplication.java b/spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/ApiGatewayApplication.java
index df86a6fc2..f872c1c17 100644
--- a/spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/ApiGatewayApplication.java
+++ b/spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/ApiGatewayApplication.java
@@ -84,7 +84,7 @@ RouterFunction> routerFunction() {
public Customizer defaultCustomizer() {
return factory -> factory.configureDefault(id -> new Resilience4JConfigBuilder(id)
.circuitBreakerConfig(CircuitBreakerConfig.ofDefaults())
- .timeLimiterConfig(TimeLimiterConfig.custom().timeoutDuration(Duration.ofSeconds(4)).build())
+ .timeLimiterConfig(TimeLimiterConfig.custom().timeoutDuration(Duration.ofSeconds(10)).build())
.build());
}
}
diff --git a/spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/boundary/web/FallbackController.java b/spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/boundary/web/FallbackController.java
new file mode 100644
index 000000000..3daafdd23
--- /dev/null
+++ b/spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/boundary/web/FallbackController.java
@@ -0,0 +1,16 @@
+package org.springframework.samples.petclinic.api.boundary.web;
+
+import org.apache.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+public class FallbackController {
+
+ @PostMapping("/fallback")
+ public ResponseEntity fallback() {
+ return ResponseEntity.status(HttpStatus.SC_SERVICE_UNAVAILABLE)
+ .body("Chat is currently unavailable. Please try again later.");
+ }
+}
diff --git a/spring-petclinic-api-gateway/src/main/resources/application.yml b/spring-petclinic-api-gateway/src/main/resources/application.yml
index 48ead7d0f..cb80e10de 100644
--- a/spring-petclinic-api-gateway/src/main/resources/application.yml
+++ b/spring-petclinic-api-gateway/src/main/resources/application.yml
@@ -5,6 +5,16 @@ spring:
import: optional:configserver:${CONFIG_SERVER_URL:http://localhost:8888/}
cloud:
gateway:
+ default-filters:
+ - name: CircuitBreaker
+ args:
+ name: defaultCircuitBreaker
+ fallbackUri: forward:/fallback
+ - name: Retry
+ args:
+ retries: 1
+ statuses: SERVICE_UNAVAILABLE
+ methods: POST
routes:
- id: vets-service
uri: lb://vets-service
@@ -24,8 +34,13 @@ spring:
- Path=/api/customer/**
filters:
- StripPrefix=2
-
-
+ - id: genai-service
+ uri: lb://genai-service
+ predicates:
+ - Path=/api/genai/**
+ filters:
+ - StripPrefix=2
+ - CircuitBreaker=name=genaiCircuitBreaker,fallbackUri=/fallback
---
spring:
diff --git a/spring-petclinic-api-gateway/src/main/resources/static/css/petclinic.css b/spring-petclinic-api-gateway/src/main/resources/static/css/petclinic.css
index d5fb1c5b9..3928143e7 100644
--- a/spring-petclinic-api-gateway/src/main/resources/static/css/petclinic.css
+++ b/spring-petclinic-api-gateway/src/main/resources/static/css/petclinic.css
@@ -9387,6 +9387,99 @@ table td.action-column {
hr {
border-top: 1px dotted #34302D; }
+/* 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; }
+ .chatbox.minimized .chatbox-content {
+ height: 40px;
+ /* Height when minimized (header only) */ }
+ .chatbox.minimized .chatbox-messages,
+ .chatbox.minimized .chatbox-footer {
+ display: none; }
+
+/* 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 */ }
+
+.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; }
+ .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; }
+
@font-face {
font-family: 'varela_roundregular';
src: url("../fonts/varela_round-webfont.eot");
diff --git a/spring-petclinic-api-gateway/src/main/resources/static/index.html b/spring-petclinic-api-gateway/src/main/resources/static/index.html
index f7e49ef29..776f11d38 100644
--- a/spring-petclinic-api-gateway/src/main/resources/static/index.html
+++ b/spring-petclinic-api-gateway/src/main/resources/static/index.html
@@ -22,6 +22,7 @@
+
@@ -58,6 +59,36 @@
+
+
+
+
+
+
+
+
+
+