Initial commit

This commit is contained in:
Florian 2025-10-28 09:21:47 +01:00
commit e070f503a2
17 changed files with 913 additions and 0 deletions

181
pom.xml Normal file
View 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>

View File

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

View File

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

View File

@ -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) {
}

View File

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

View File

@ -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.";
}
}
}

View File

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

View File

@ -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
) {
}

View File

@ -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
){
}

View File

@ -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
) {
}

View File

@ -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) {
}

View File

@ -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
) {
}

View File

@ -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) {
}

View File

@ -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) {
}

View 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

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

File diff suppressed because one or more lines are too long