Initial commit
This commit is contained in:
commit
e070f503a2
181
pom.xml
Normal file
181
pom.xml
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>org.springframework.samples.petclinic.vets</groupId>
|
||||||
|
<artifactId>spring-petclinic-genai-service</artifactId>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
<description>Spring PetClinic Generative AI Service</description>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>org.springframework.samples</groupId>
|
||||||
|
<artifactId>spring-petclinic-microservices</artifactId>
|
||||||
|
<version>3.4.1</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<docker.image.exposed.port>8081</docker.image.exposed.port>
|
||||||
|
<docker.image.dockerfile.dir>${basedir}/../docker</docker.image.dockerfile.dir>
|
||||||
|
<spring-ai.version>1.0.0-M4</spring-ai.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<!-- Spring Boot -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.ai</groupId>
|
||||||
|
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
|
||||||
|
<!-- artifactId>spring-ai-azure-openai-spring-boot-starter</artifactId-->
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-cache</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Spring Cloud -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.cloud</groupId>
|
||||||
|
<artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.cloud</groupId>
|
||||||
|
<artifactId>spring-cloud-starter-config</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.cloud</groupId>
|
||||||
|
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.cloud</groupId>
|
||||||
|
<artifactId>spring-cloud-starter-gateway</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Third parties-->
|
||||||
|
<dependency>
|
||||||
|
<groupId>javax.cache</groupId>
|
||||||
|
<artifactId>cache-api</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>jakarta.xml.bind</groupId>
|
||||||
|
<artifactId>jakarta.xml.bind-api</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||||
|
<artifactId>caffeine</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jolokia</groupId>
|
||||||
|
<artifactId>jolokia-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.hsqldb</groupId>
|
||||||
|
<artifactId>hsqldb</artifactId>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.mysql</groupId>
|
||||||
|
<artifactId>mysql-connector-j</artifactId>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.micrometer</groupId>
|
||||||
|
<artifactId>micrometer-registry-prometheus</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>de.codecentric</groupId>
|
||||||
|
<artifactId>chaos-monkey-spring-boot</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.opentelemetry</groupId>
|
||||||
|
<artifactId>opentelemetry-exporter-zipkin</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.micrometer</groupId>
|
||||||
|
<artifactId>micrometer-observation</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.micrometer</groupId>
|
||||||
|
<artifactId>micrometer-tracing-bridge-brave</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.zipkin.reporter2</groupId>
|
||||||
|
<artifactId>zipkin-reporter-brave</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>net.ttddyy.observation</groupId>
|
||||||
|
<artifactId>datasource-micrometer-spring-boot</artifactId>
|
||||||
|
<version>1.0.2</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Testing -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter-api</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter-engine</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<repositories>
|
||||||
|
<repository>
|
||||||
|
<id>spring-milestones</id>
|
||||||
|
<name>Spring Milestones</name>
|
||||||
|
<url>https://repo.spring.io/milestone</url>
|
||||||
|
<snapshots>
|
||||||
|
<enabled>false</enabled>
|
||||||
|
</snapshots>
|
||||||
|
</repository>
|
||||||
|
</repositories>
|
||||||
|
|
||||||
|
<profiles>
|
||||||
|
<profile>
|
||||||
|
<id>buildDocker</id>
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.codehaus.mojo</groupId>
|
||||||
|
<artifactId>exec-maven-plugin</artifactId>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</profile>
|
||||||
|
</profiles>
|
||||||
|
</project>
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
package org.springframework.samples.petclinic.genai;
|
||||||
|
|
||||||
|
import org.springframework.ai.chat.memory.ChatMemory;
|
||||||
|
import org.springframework.ai.chat.memory.InMemoryChatMemory;
|
||||||
|
import org.springframework.ai.embedding.EmbeddingModel;
|
||||||
|
import org.springframework.ai.vectorstore.SimpleVectorStore;
|
||||||
|
import org.springframework.ai.vectorstore.VectorStore;
|
||||||
|
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.reactive.function.client.WebClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Configuration class for beans used by the Chat Client.
|
||||||
|
*
|
||||||
|
* @author Oded Shopen
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class AIBeanConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public ChatMemory chatMemory() {
|
||||||
|
return new InMemoryChatMemory();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
VectorStore vectorStore(EmbeddingModel embeddingModel) {
|
||||||
|
return new SimpleVectorStore(embeddingModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@LoadBalanced
|
||||||
|
public WebClient.Builder loadBalancedWebClientBuilder() {
|
||||||
|
return WebClient.builder();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,77 @@
|
|||||||
|
package org.springframework.samples.petclinic.genai;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.ai.document.Document;
|
||||||
|
import org.springframework.ai.vectorstore.SearchRequest;
|
||||||
|
import org.springframework.ai.vectorstore.VectorStore;
|
||||||
|
import org.springframework.core.ParameterizedTypeReference;
|
||||||
|
import org.springframework.samples.petclinic.genai.dto.OwnerDetails;
|
||||||
|
import org.springframework.samples.petclinic.genai.dto.PetDetails;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.reactive.function.client.WebClient;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Functions that are invoked by the LLM will use this bean to query the system of record
|
||||||
|
* for information such as listing owners and vets, or adding pets to an owner.
|
||||||
|
*
|
||||||
|
* @author Oded Shopen
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class AIDataProvider {
|
||||||
|
|
||||||
|
private final VectorStore vectorStore;
|
||||||
|
private final String ownersHostname = "http://customers-service/";
|
||||||
|
|
||||||
|
private final WebClient webClient;
|
||||||
|
|
||||||
|
|
||||||
|
public AIDataProvider(WebClient.Builder webClientBuilder, VectorStore vectorStore) {
|
||||||
|
this.webClient = webClientBuilder.build();
|
||||||
|
this.vectorStore = vectorStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OwnersResponse getAllOwners() {
|
||||||
|
return new OwnersResponse(webClient
|
||||||
|
.get()
|
||||||
|
.uri(ownersHostname + "owners")
|
||||||
|
.retrieve()
|
||||||
|
.bodyToMono(new ParameterizedTypeReference<List<OwnerDetails>>() {})
|
||||||
|
.block());
|
||||||
|
}
|
||||||
|
|
||||||
|
public VetResponse getVets(VetRequest request) throws JsonProcessingException {
|
||||||
|
ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
String vetAsJson = objectMapper.writeValueAsString(request.vet());
|
||||||
|
|
||||||
|
SearchRequest sr = SearchRequest.from(SearchRequest.defaults()).withQuery(vetAsJson).withTopK(20);
|
||||||
|
if (request.vet() == null) {
|
||||||
|
// Provide a limit of 50 results when zero parameters are sent
|
||||||
|
sr = sr.withTopK(50);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Document> topMatches = this.vectorStore.similaritySearch(sr);
|
||||||
|
List<String> results = topMatches.stream().map(Document::getContent).toList();
|
||||||
|
return new VetResponse(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AddedPetResponse addPetToOwner(AddPetRequest request) {
|
||||||
|
return new AddedPetResponse(webClient
|
||||||
|
.post()
|
||||||
|
.uri(ownersHostname + "owners/"+request.ownerId()+"/pets")
|
||||||
|
.bodyValue(request.pet())
|
||||||
|
.retrieve().bodyToMono(PetDetails.class).block());
|
||||||
|
}
|
||||||
|
|
||||||
|
public OwnerResponse addOwnerToPetclinic(OwnerRequest ownerRequest) {
|
||||||
|
return new OwnerResponse(webClient
|
||||||
|
.post()
|
||||||
|
.uri(ownersHostname + "owners")
|
||||||
|
.bodyValue(ownerRequest)
|
||||||
|
.retrieve().bodyToMono(OwnerDetails.class).block());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,95 @@
|
|||||||
|
package org.springframework.samples.petclinic.genai;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.Description;
|
||||||
|
import org.springframework.samples.petclinic.genai.dto.OwnerDetails;
|
||||||
|
import org.springframework.samples.petclinic.genai.dto.PetDetails;
|
||||||
|
import org.springframework.samples.petclinic.genai.dto.PetRequest;
|
||||||
|
import org.springframework.samples.petclinic.genai.dto.Vet;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Digits;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
class AIFunctionConfiguration {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(AIFunctionConfiguration.class);
|
||||||
|
|
||||||
|
// The @Description annotation helps the model understand when to call the function
|
||||||
|
@Bean
|
||||||
|
@Description("List the owners that the pet clinic has")
|
||||||
|
public Function<OwnerRequest, OwnersResponse> listOwners(AIDataProvider petclinicAiProvider) {
|
||||||
|
return request -> petclinicAiProvider.getAllOwners();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@Description("Add a new pet owner to the pet clinic. " + "The Owner must include a first name and a last name "
|
||||||
|
+ "as two separate words, " + "plus an address and a 10-digit phone number")
|
||||||
|
public Function<OwnerRequest, OwnerResponse> addOwnerToPetclinic(AIDataProvider petclinicAiDataProvider) {
|
||||||
|
return petclinicAiDataProvider::addOwnerToPetclinic;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@Description("List the veterinarians that the pet clinic has")
|
||||||
|
public Function<VetRequest, VetResponse> listVets(AIDataProvider petclinicAiProvider) {
|
||||||
|
return request -> {
|
||||||
|
try {
|
||||||
|
return petclinicAiProvider.getVets(request);
|
||||||
|
}
|
||||||
|
catch (JsonProcessingException e) {
|
||||||
|
LOG.error("Error processing JSON in the listVets function", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@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<AddPetRequest, AddedPetResponse> addPetToOwner(AIDataProvider petclinicAiProvider) {
|
||||||
|
return petclinicAiProvider::addPetToOwner;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
record AddPetRequest(PetRequest pet, Integer ownerId) {
|
||||||
|
}
|
||||||
|
|
||||||
|
record OwnersResponse(List<OwnerDetails> owners) {
|
||||||
|
}
|
||||||
|
|
||||||
|
record OwnerResponse(OwnerDetails owner) {
|
||||||
|
}
|
||||||
|
|
||||||
|
record AddedPetResponse(PetDetails pet) {
|
||||||
|
}
|
||||||
|
|
||||||
|
record VetResponse(List<String> vet) {
|
||||||
|
}
|
||||||
|
|
||||||
|
record VetRequest(Vet vet) {
|
||||||
|
}
|
||||||
|
|
||||||
|
record OwnerRequest(@NotBlank String firstName,
|
||||||
|
@NotBlank String lastName,
|
||||||
|
@NotBlank String address,
|
||||||
|
@NotBlank String city,
|
||||||
|
@NotBlank @Digits(fraction = 0, integer = 12) String telephone) {
|
||||||
|
}
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2021 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.samples.petclinic.genai;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Oded Shopen
|
||||||
|
*/
|
||||||
|
@EnableDiscoveryClient
|
||||||
|
@SpringBootApplication
|
||||||
|
public class GenAIServiceApplication {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(GenAIServiceApplication.class, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,75 @@
|
|||||||
|
package org.springframework.samples.petclinic.genai;
|
||||||
|
|
||||||
|
import static org.springframework.ai.chat.client.advisor.AbstractChatMemoryAdvisor.DEFAULT_CHAT_MEMORY_CONVERSATION_ID;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.ai.chat.client.ChatClient;
|
||||||
|
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
|
||||||
|
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This REST controller is being invoked by the in order to interact with the LLM
|
||||||
|
*
|
||||||
|
* @author Oded Shopen
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/")
|
||||||
|
public class PetclinicChatClient {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(PetclinicChatClient.class);
|
||||||
|
|
||||||
|
// 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 and to perform actions on the user's behalf, mainly around
|
||||||
|
veterinarians, owners, owners' pets and owners' visits.
|
||||||
|
You are required to answer an a professional manner. If you don't know the answer, politely tell the user
|
||||||
|
you don't know the answer, then ask the user a followup question to try and clarify the question they are asking.
|
||||||
|
If you do know the answer, provide the answer but do not provide any additional followup questions.
|
||||||
|
When dealing with vets, if the user is unsure about the returned results, explain that there may be additional data that was not returned.
|
||||||
|
Only if the user is asking about the total number of all vets, answer that there are a lot and ask for some additional criteria.
|
||||||
|
For owners, pets or visits - provide the correct data.
|
||||||
|
""")
|
||||||
|
.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 SimpleLoggerAdvisor()
|
||||||
|
)
|
||||||
|
.defaultFunctions("listOwners", "addOwnerToPetclinic", "addPetToOwner", "listVets")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/chatclient")
|
||||||
|
public String exchange(@RequestBody String query) {
|
||||||
|
try {
|
||||||
|
//All chatbot messages go through this endpoint
|
||||||
|
//and are passed to the LLM
|
||||||
|
return
|
||||||
|
this.chatClient
|
||||||
|
.prompt()
|
||||||
|
.user(
|
||||||
|
u ->
|
||||||
|
u.text(query)
|
||||||
|
)
|
||||||
|
.call()
|
||||||
|
.content();
|
||||||
|
} catch (Exception exception) {
|
||||||
|
LOG.error("Error processing chat message", exception);
|
||||||
|
return "Chat is currently unavailable. Please try again later.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,112 @@
|
|||||||
|
package org.springframework.samples.petclinic.genai;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.ai.document.Document;
|
||||||
|
import org.springframework.ai.document.DocumentReader;
|
||||||
|
import org.springframework.ai.reader.JsonReader;
|
||||||
|
import org.springframework.ai.vectorstore.SimpleVectorStore;
|
||||||
|
import org.springframework.ai.vectorstore.VectorStore;
|
||||||
|
import org.springframework.boot.context.event.ApplicationStartedEvent;
|
||||||
|
import org.springframework.context.event.EventListener;
|
||||||
|
import org.springframework.core.ParameterizedTypeReference;
|
||||||
|
import org.springframework.core.io.ByteArrayResource;
|
||||||
|
import org.springframework.core.io.ClassPathResource;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.samples.petclinic.genai.dto.Vet;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.reactive.function.client.WebClient;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.attribute.FileAttribute;
|
||||||
|
import java.nio.file.attribute.PosixFilePermission;
|
||||||
|
import java.nio.file.attribute.PosixFilePermissions;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the veterinarians data into a vector store for the purpose of RAG functionality.
|
||||||
|
*
|
||||||
|
* @author Oded Shopen
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class VectorStoreController {
|
||||||
|
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(VectorStoreController.class);
|
||||||
|
|
||||||
|
private final VectorStore vectorStore;
|
||||||
|
private final WebClient webClient;
|
||||||
|
|
||||||
|
public VectorStoreController(VectorStore vectorStore, WebClient.Builder webClientBuilder) {
|
||||||
|
this.webClient = webClientBuilder.build();
|
||||||
|
this.vectorStore = vectorStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventListener
|
||||||
|
public void loadVetDataToVectorStoreOnStartup(ApplicationStartedEvent event) throws IOException {
|
||||||
|
Resource resource = new ClassPathResource("vectorstore.json");
|
||||||
|
|
||||||
|
// Check if file exists
|
||||||
|
if (resource.exists()) {
|
||||||
|
// In order to save on AI credits, use a pre-embedded database that was saved
|
||||||
|
// to
|
||||||
|
// disk based on the current data in the h2 data.sql file
|
||||||
|
File file = resource.getFile();
|
||||||
|
((SimpleVectorStore) this.vectorStore).load(file);
|
||||||
|
logger.info("vector store loaded from existing vectorstore.json file in the classpath");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If vectorstore.json is deleted, the data will be loaded on startup every time.
|
||||||
|
// Warning - this can be costly in terms of credits used with the AI provider.
|
||||||
|
// Fetches all Vet entites and creates a document per vet
|
||||||
|
String vetsHostname = "http://vets-service/";
|
||||||
|
List<Vet> vets = webClient
|
||||||
|
.get()
|
||||||
|
.uri(vetsHostname + "vets")
|
||||||
|
.retrieve()
|
||||||
|
.bodyToMono(new ParameterizedTypeReference<List<Vet>>() {})
|
||||||
|
.block();
|
||||||
|
|
||||||
|
|
||||||
|
Resource vetsAsJson = convertListToJsonResource(vets);
|
||||||
|
DocumentReader reader = new JsonReader(vetsAsJson);
|
||||||
|
|
||||||
|
List<Document> documents = reader.get();
|
||||||
|
// add the documents to the vector store
|
||||||
|
this.vectorStore.add(documents);
|
||||||
|
|
||||||
|
if (vectorStore instanceof SimpleVectorStore) {
|
||||||
|
// java:S5443 Sonar rule: Using publicly writable directories is security-sensitive
|
||||||
|
FileAttribute<Set<PosixFilePermission>> attr = PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------"));
|
||||||
|
File file = Files.createTempFile("vectorstore", ".json", attr).toFile();
|
||||||
|
((SimpleVectorStore) this.vectorStore).save(file);
|
||||||
|
logger.info("vector store contents written to {}", file.getAbsolutePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("vector store loaded with {} documents", documents.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Resource convertListToJsonResource(List<Vet> vets) {
|
||||||
|
ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
try {
|
||||||
|
// Convert List<Vet> to JSON string
|
||||||
|
String json = objectMapper.writeValueAsString(vets);
|
||||||
|
|
||||||
|
// Convert JSON string to byte array
|
||||||
|
byte[] jsonBytes = json.getBytes();
|
||||||
|
|
||||||
|
// Create a ByteArrayResource from the byte array
|
||||||
|
return new ByteArrayResource(jsonBytes);
|
||||||
|
}
|
||||||
|
catch (JsonProcessingException e) {
|
||||||
|
logger.error("Error processing JSON in the convertListToJsonResource function", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2021 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.samples.petclinic.genai.dto;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple Data Transfer Object representing an owner.
|
||||||
|
*
|
||||||
|
* @author Oded Shopen
|
||||||
|
*/
|
||||||
|
public record OwnerDetails(
|
||||||
|
int id,
|
||||||
|
String firstName,
|
||||||
|
String lastName,
|
||||||
|
String address,
|
||||||
|
String city,
|
||||||
|
String telephone,
|
||||||
|
List<PetDetails> pets
|
||||||
|
) {
|
||||||
|
}
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2021 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.samples.petclinic.genai.dto;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple Data Transfer Object representing a pet data type.
|
||||||
|
*
|
||||||
|
* @author Oded Shopen
|
||||||
|
*/
|
||||||
|
public record PetDetails(
|
||||||
|
int id,
|
||||||
|
String name,
|
||||||
|
String birthDate,
|
||||||
|
PetType type,
|
||||||
|
List<VisitDetails> visits
|
||||||
|
){
|
||||||
|
}
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2021 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.samples.petclinic.genai.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple Data Transfer Object representing a Pet request.
|
||||||
|
*
|
||||||
|
* @author Oded Shopen
|
||||||
|
*/
|
||||||
|
public record PetRequest(int id,
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||||
|
Date birthDate,
|
||||||
|
String name,
|
||||||
|
int typeId
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2021 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.samples.petclinic.genai.dto;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple Data Transfer Object representing a Pet type.
|
||||||
|
*
|
||||||
|
* @author Oded Shopen
|
||||||
|
*/
|
||||||
|
public record PetType(String name) {
|
||||||
|
}
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2021 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.samples.petclinic.genai.dto;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple Data Transfer Object representing a vet's specialty.
|
||||||
|
*
|
||||||
|
* @author Oded Shopen
|
||||||
|
*/
|
||||||
|
|
||||||
|
public record Specialty(
|
||||||
|
Integer id,
|
||||||
|
String name
|
||||||
|
) {
|
||||||
|
}
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2021 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.samples.petclinic.genai.dto;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple Data Transfer Object representing a vet.
|
||||||
|
*
|
||||||
|
* @author Oded Shopen
|
||||||
|
*/
|
||||||
|
public record Vet(
|
||||||
|
Integer id,
|
||||||
|
String firstName,
|
||||||
|
String lastName,
|
||||||
|
Set<Specialty> specialties) {
|
||||||
|
}
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2021 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.samples.petclinic.genai.dto;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple Data Transfer Object representing a customer visit.
|
||||||
|
*
|
||||||
|
* @author Oded Shopen
|
||||||
|
*/
|
||||||
|
public record VisitDetails(
|
||||||
|
Integer id,
|
||||||
|
Integer petId,
|
||||||
|
String date,
|
||||||
|
String description) {
|
||||||
|
}
|
||||||
45
src/main/resources/application.yml
Normal file
45
src/main/resources/application.yml
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
spring:
|
||||||
|
main:
|
||||||
|
web-application-type: reactive
|
||||||
|
application:
|
||||||
|
name: genai-service
|
||||||
|
profiles:
|
||||||
|
active: production
|
||||||
|
config:
|
||||||
|
import: optional:configserver:${CONFIG_SERVER_URL:http://localhost:8888/},optional:classpath:/creds.yaml
|
||||||
|
ai:
|
||||||
|
chat:
|
||||||
|
client:
|
||||||
|
enabled: true
|
||||||
|
# These apply when using spring-ai-azure-openai-spring-boot-starter
|
||||||
|
azure:
|
||||||
|
openai:
|
||||||
|
api-key: ${AZURE_OPENAI_KEY}
|
||||||
|
endpoint: ${AZURE_OPENAI_ENDPOINT}
|
||||||
|
chat:
|
||||||
|
options:
|
||||||
|
temperature: 0.7
|
||||||
|
deployment-name: gpt-4o
|
||||||
|
# These apply when using spring-ai-openai-spring-boot-starter
|
||||||
|
openai:
|
||||||
|
api-key: ${OPENAI_API_KEY:demo}
|
||||||
|
chat:
|
||||||
|
options:
|
||||||
|
temperature: 0.7
|
||||||
|
model: gpt-4o-mini
|
||||||
|
|
||||||
|
|
||||||
|
logging:
|
||||||
|
level:
|
||||||
|
org:
|
||||||
|
springframework:
|
||||||
|
ai:
|
||||||
|
chat:
|
||||||
|
client:
|
||||||
|
advisor: DEBUG
|
||||||
|
---
|
||||||
|
spring:
|
||||||
|
config:
|
||||||
|
activate:
|
||||||
|
on-profile: docker
|
||||||
|
import: configserver:http://config-server:8888
|
||||||
6
src/main/resources/logback-spring.xml
Normal file
6
src/main/resources/logback-spring.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<configuration>
|
||||||
|
<include resource="org/springframework/boot/logging/logback/base.xml"/>
|
||||||
|
<!-- Required for Loglevel managment into the Spring Petclinic Admin Server-->
|
||||||
|
<jmxConfigurator/>
|
||||||
|
</configuration>
|
||||||
44
src/main/resources/vectorstore.json
Normal file
44
src/main/resources/vectorstore.json
Normal file
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user