Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<module>spring-petclinic-vets-service</module>
<module>spring-petclinic-visits-service</module>
<module>spring-petclinic-api-gateway</module>
<module>spring-petclinic-chat</module>
</modules>

<properties>
Expand Down
1 change: 1 addition & 0 deletions src/spring-petclinic-chat/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/.apt_generated/
118 changes: 118 additions & 0 deletions src/spring-petclinic-chat/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<?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.chat</groupId>
<artifactId>spring-petclinic-chat</artifactId>
<packaging>jar</packaging>
<description>Spring PetClinic Chat</description>

<parent>
<groupId>org.springframework.samples</groupId>
<artifactId>spring-petclinic-microservices</artifactId>
<version>3.0.2</version>
</parent>

<properties>

<!-- Generic properties -->
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<!-- Web dependencies -->
<webjars-bootstrap.version>5.3.3</webjars-bootstrap.version>
<webjars-font-awesome.version>4.7.0</webjars-font-awesome.version>
<checkstyle.version>10.16.0</checkstyle.version>
<maven-checkstyle.version>3.3.1</maven-checkstyle.version>
<nohttp-checkstyle.version>0.0.11</nohttp-checkstyle.version>
<spring-format.version>0.0.41</spring-format.version>
<azure-identity.version>1.13.2</azure-identity.version>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.0.0-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<!-- Spring and Spring Boot dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-azure-openai</artifactId>
</dependency>
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-identity</artifactId>
<version>${azure-identity.version}</version>
</dependency>

<!-- Webjars -->
<dependency>
<groupId>org.webjars.npm</groupId>
<artifactId>bootstrap</artifactId>
<version>${webjars-bootstrap.version}</version>
</dependency>
<dependency>
<groupId>org.webjars.npm</groupId>
<artifactId>font-awesome</artifactId>
<version>${webjars-font-awesome.version}</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-azure-openai-spring-boot-starter</artifactId>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.springframework.samples.petclinic.chat;


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class AppConfig {

@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.springframework.samples.petclinic.chat;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class PetClinicChatApplication {
public static void main(String[] args) {
SpringApplication.run(PetClinicChatApplication.class, args);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package org.springframework.samples.petclinic.chat.ai;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.ChatClientCustomizer;
import org.springframework.ai.chat.client.advisor.PromptChatMemoryAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.InMemoryChatMemory;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.document.Document;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.reader.TextReader;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.SimpleVectorStore;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;

import com.azure.ai.openai.OpenAIClient;
import com.azure.ai.openai.OpenAIClientBuilder;
import com.azure.identity.DefaultAzureCredentialBuilder;
import org.springframework.ai.azure.openai.AzureOpenAiChatModel;
import org.springframework.ai.azure.openai.AzureOpenAiChatOptions;
import org.springframework.ai.model.function.FunctionCallbackContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.ApplicationContext;

import java.util.List;


@Configuration
@EnableConfigurationProperties({OpenAiModelConfigProperties.class, OpenAiChatOptionsProperties.class})
public class AgentConfig {


@Autowired
private ApplicationContext applicationContext;

/**
* Configure a bean of type AzureOpenAiChatModel as ChatModel
*/
@Bean
@ConditionalOnProperty(OpenAiChatOptionsProperties.PREFIX + ".deployment-name")
public AzureOpenAiChatModel chatModel(OpenAIClient openAIClient, OpenAiChatOptionsProperties properties) {
var openAIChatOptions = AzureOpenAiChatOptions.builder()
.withDeploymentName(properties.getDeploymentName())
.withTemperature(properties.getTemperature())
.build();

// provide Context to load function callbacks
var functionCallbackContext = new FunctionCallbackContext();
functionCallbackContext.setApplicationContext(applicationContext);
return new AzureOpenAiChatModel(openAIClient, openAIChatOptions, functionCallbackContext);
}

/**
* Configure a bean of type OpenAIClient, which is used to construct ChatModel and EmbeddingModel
*/
@Bean
@ConditionalOnProperty(OpenAiModelConfigProperties.PREFIX + ".endpoint")
public OpenAIClient openAIClient(OpenAiModelConfigProperties properties) {
return new OpenAIClientBuilder()
.endpoint(properties.getEndpoint())
.credential(new DefaultAzureCredentialBuilder()
.build())
.buildClient();
}


@Bean
public ChatClient chatClient(ChatClient.Builder chatClientBuilder) {
return chatClientBuilder.build();
}

@Bean
public ChatClientCustomizer chatClientCustomizer(VectorStore vectorStore, ChatModel model) {
ChatMemory chatMemory = new InMemoryChatMemory();
return b -> b.defaultAdvisors(
new PromptChatMemoryAdvisor(chatMemory),
new ModeledQuestionAnswerAdvisor(vectorStore, SearchRequest.defaults(), model));
}

@Bean
public VectorStore simpleVectorStore(EmbeddingModel embeddingModel) {
Resource resource = new DefaultResourceLoader().getResource("classpath:/ai/petclinic-terms-of-use.txt");
TextReader textReader = new TextReader(resource);
List<Document> documents = new TokenTextSplitter().apply(textReader.get());
VectorStore store = new SimpleVectorStore(embeddingModel);
store.add(documents);
return store;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package org.springframework.samples.petclinic.chat.ai;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;

import static org.springframework.ai.chat.client.advisor.AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY;


@Component
public class ChatAgent {

private static final String TRANSLATE = "Generate 1 different versions of a provided user query. " +
"but they should all retain the original meaning. " +
"It will be used to retrieve relevant documents from a English document. " +
"Without enumerations, hyphens, or any additional formatting!";

@Autowired
private ChatClient chatClient;

@Value("classpath:/ai/system-message.st")
private Resource systemResource;

public String chat(String userMessage, String username) {
try {
Consumer<ChatClient.AdvisorSpec> advisorSpecConsumer = advisorSpec -> {
advisorSpec.param(CHAT_MEMORY_CONVERSATION_ID_KEY, username);
};
PromptTemplate systemPromptTemplate = new SystemPromptTemplate(systemResource);
Map<String, Object> systemParameters = new HashMap<>() {{
put("username", username);
}};

return chatClient
.prompt()
.advisors(advisorSpecConsumer) //userName as memory key
.system(systemPromptTemplate.render(systemParameters))
.user(userMessage)
.functions("queryOwners", "addOwner", "updateOwner", "queryVets")
.call()
.content();
} catch (Exception e) {
return "Sorry, I am not able to help you with that.";
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package org.springframework.samples.petclinic.chat.ai;

import org.springframework.ai.chat.client.AdvisedRequest;
import org.springframework.ai.chat.client.advisor.QuestionAnswerAdvisor;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;

import java.util.Map;

public class ModeledQuestionAnswerAdvisor extends QuestionAnswerAdvisor {
private static final String TRANSLATE = "Generate 1 different versions of a provided user query. " +
"but they should all retain the original meaning. " +
"It will be used to retrieve relevant documents and it should be in English \n" +
"Without enumerations, hyphens, or any additional formatting!";

private ChatModel chatModel;

public ModeledQuestionAnswerAdvisor(VectorStore vectorStore, ChatModel chatModel, String modeledText) {
super(vectorStore);
this.chatModel = chatModel;
}

public ModeledQuestionAnswerAdvisor(VectorStore vectorStore, SearchRequest searchRequest, ChatModel chatModel) {
super(vectorStore, searchRequest);
this.chatModel = chatModel;
}

public ModeledQuestionAnswerAdvisor(VectorStore vectorStore, SearchRequest searchRequest, String userTextAdvise, ChatModel chatModel) {
super(vectorStore, searchRequest, userTextAdvise);
this.chatModel = chatModel;
}

@Override
public AdvisedRequest adviseRequest(AdvisedRequest request, Map<String, Object> context) {
String originalUserText = request.userText();
String processedMessage = chatModel.call(TRANSLATE + "\n" + request.userText());
AdvisedRequest processedRequest = AdvisedRequest.from(request).withUserText(processedMessage).build();
request = super.adviseRequest(processedRequest, context);
return AdvisedRequest.from(request).withUserText(originalUserText).build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.springframework.samples.petclinic.chat.ai;

import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;

@Getter
@Setter
@ConfigurationProperties(prefix = OpenAiChatOptionsProperties.PREFIX)
public class OpenAiChatOptionsProperties {

static final String PREFIX = "spring.ai.azure.openai.chat.options";

String deploymentName;

Double temperature;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.springframework.samples.petclinic.chat.ai;


import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;

@Getter
@Setter
@ConfigurationProperties(prefix = OpenAiModelConfigProperties.PREFIX)
public class OpenAiModelConfigProperties {

static final String PREFIX = "spring.ai.azure.openai";

String endpoint;
}
Loading
Loading