Renaming of AiClient and related classes and packagenames
* Rename AiClient to ChatClient * Rename AiResponse to ChatResponse * Rename AiStreamClient to StreamingChatClient * Rename package org.sf.ai.client to org.sf.ai.chat * Update readme to indicate breaking changes
This commit is contained in:
13
README.md
13
README.md
@@ -12,6 +12,19 @@ Let's make your `@Beans` intelligent!
|
||||
|
||||
:warning:
|
||||
|
||||
### Breaking Changes
|
||||
December 19, 2023 Update
|
||||
|
||||
Renaming of AiClient and related classes and packagenames
|
||||
|
||||
* Rename AiClient to ChatClient
|
||||
* Rename AiResponse to ChatResponse
|
||||
* Rename AiStreamClient to StreamingChatClient
|
||||
* Rename package org.sf.ai.client to org.sf.ai.chat
|
||||
:warning:
|
||||
|
||||
December 1, 2023
|
||||
|
||||
We are transitioning the project's Group ID:
|
||||
|
||||
* **FROM**: `org.springframework.experimental.ai`
|
||||
|
||||
@@ -31,9 +31,9 @@ import com.azure.ai.openai.models.PromptFilterResult;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.ai.azure.openai.metadata.AzureOpenAiGenerationMetadata;
|
||||
import org.springframework.ai.client.AiClient;
|
||||
import org.springframework.ai.client.AiResponse;
|
||||
import org.springframework.ai.client.Generation;
|
||||
import org.springframework.ai.chat.ChatClient;
|
||||
import org.springframework.ai.chat.ChatResponse;
|
||||
import org.springframework.ai.chat.Generation;
|
||||
import org.springframework.ai.metadata.ChoiceMetadata;
|
||||
import org.springframework.ai.metadata.PromptMetadata;
|
||||
import org.springframework.ai.metadata.PromptMetadata.PromptFilterMetadata;
|
||||
@@ -42,16 +42,16 @@ import org.springframework.ai.prompt.messages.Message;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* {@link AiClient} implementation for {@literal Microsoft Azure AI} backed by
|
||||
* {@link ChatClient} implementation for {@literal Microsoft Azure AI} backed by
|
||||
* {@link OpenAIClient}.
|
||||
*
|
||||
* @author Mark Pollack
|
||||
* @author Ueibin Kim
|
||||
* @author John Blum
|
||||
* @see org.springframework.ai.client.AiClient
|
||||
* @see ChatClient
|
||||
* @see com.azure.ai.openai.OpenAIClient
|
||||
*/
|
||||
public class AzureOpenAiClient implements AiClient {
|
||||
public class AzureOpenAiClient implements ChatClient {
|
||||
|
||||
private Double temperature = 0.7;
|
||||
|
||||
@@ -108,7 +108,7 @@ public class AzureOpenAiClient implements AiClient {
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiResponse generate(Prompt prompt) {
|
||||
public ChatResponse generate(Prompt prompt) {
|
||||
|
||||
List<Message> messages = prompt.getMessages();
|
||||
List<ChatMessage> azureMessages = new ArrayList<>();
|
||||
@@ -136,7 +136,7 @@ public class AzureOpenAiClient implements AiClient {
|
||||
generations.add(generation);
|
||||
}
|
||||
|
||||
return new AiResponse(generations, AzureOpenAiGenerationMetadata.from(chatCompletions))
|
||||
return new ChatResponse(generations, AzureOpenAiGenerationMetadata.from(chatCompletions))
|
||||
.withPromptMetadata(generatePromptMetadata(chatCompletions));
|
||||
}
|
||||
|
||||
|
||||
@@ -27,8 +27,8 @@ import com.azure.ai.openai.models.ContentFilterSeverity;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.ai.azure.openai.MockAzureOpenAiTestConfiguration;
|
||||
import org.springframework.ai.client.AiResponse;
|
||||
import org.springframework.ai.client.Generation;
|
||||
import org.springframework.ai.chat.ChatResponse;
|
||||
import org.springframework.ai.chat.Generation;
|
||||
import org.springframework.ai.metadata.ChoiceMetadata;
|
||||
import org.springframework.ai.metadata.GenerationMetadata;
|
||||
import org.springframework.ai.metadata.PromptMetadata;
|
||||
@@ -73,7 +73,7 @@ class AzureOpenAiClientMetadataTests {
|
||||
|
||||
Prompt prompt = new Prompt("Can I fly like a bird?");
|
||||
|
||||
AiResponse response = this.aiClient.generate(prompt);
|
||||
ChatResponse response = this.aiClient.generate(prompt);
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
|
||||
@@ -88,7 +88,7 @@ class AzureOpenAiClientMetadataTests {
|
||||
assertChoiceMetadata(generation);
|
||||
}
|
||||
|
||||
private void assertPromptMetadata(AiResponse response) {
|
||||
private void assertPromptMetadata(ChatResponse response) {
|
||||
|
||||
PromptMetadata promptMetadata = response.getPromptMetadata();
|
||||
|
||||
@@ -101,7 +101,7 @@ class AzureOpenAiClientMetadataTests {
|
||||
assertContentFilterResults(promptFilterMetadata.getContentFilterMetadata(), ContentFilterSeverity.HIGH);
|
||||
}
|
||||
|
||||
private void assertGenerationMetadata(AiResponse response) {
|
||||
private void assertGenerationMetadata(ChatResponse response) {
|
||||
|
||||
GenerationMetadata generationMetadata = response.getGenerationMetadata();
|
||||
|
||||
|
||||
@@ -18,26 +18,27 @@ package org.springframework.ai.bedrock.anthropic;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.ai.chat.ChatClient;
|
||||
import org.springframework.ai.chat.ChatResponse;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.ai.bedrock.MessageToPromptConverter;
|
||||
import org.springframework.ai.bedrock.anthropic.api.AnthropicChatBedrockApi;
|
||||
import org.springframework.ai.bedrock.anthropic.api.AnthropicChatBedrockApi.AnthropicChatRequest;
|
||||
import org.springframework.ai.bedrock.anthropic.api.AnthropicChatBedrockApi.AnthropicChatResponse;
|
||||
import org.springframework.ai.client.AiClient;
|
||||
import org.springframework.ai.client.AiResponse;
|
||||
import org.springframework.ai.client.AiStreamClient;
|
||||
import org.springframework.ai.client.Generation;
|
||||
import org.springframework.ai.chat.StreamingChatClient;
|
||||
import org.springframework.ai.chat.Generation;
|
||||
import org.springframework.ai.metadata.ChoiceMetadata;
|
||||
import org.springframework.ai.prompt.Prompt;
|
||||
|
||||
/**
|
||||
* Java {@link AiClient} and {@link AiStreamClient} for the Bedrock Anthropic chat model.
|
||||
* Java {@link ChatClient} and {@link StreamingChatClient} for the Bedrock Anthropic chat
|
||||
* model.
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
* @since 0.8.0
|
||||
*/
|
||||
public class BedrockAnthropicChatClient implements AiClient, AiStreamClient {
|
||||
public class BedrockAnthropicChatClient implements ChatClient, StreamingChatClient {
|
||||
|
||||
private final AnthropicChatBedrockApi anthropicChatApi;
|
||||
|
||||
@@ -88,7 +89,7 @@ public class BedrockAnthropicChatClient implements AiClient, AiStreamClient {
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiResponse generate(Prompt prompt) {
|
||||
public ChatResponse generate(Prompt prompt) {
|
||||
final String promptValue = MessageToPromptConverter.create().toPrompt(prompt.getMessages());
|
||||
|
||||
AnthropicChatRequest request = AnthropicChatRequest.builder(promptValue)
|
||||
@@ -102,11 +103,11 @@ public class BedrockAnthropicChatClient implements AiClient, AiStreamClient {
|
||||
|
||||
AnthropicChatResponse response = this.anthropicChatApi.chatCompletion(request);
|
||||
|
||||
return new AiResponse(List.of(new Generation(response.completion())));
|
||||
return new ChatResponse(List.of(new Generation(response.completion())));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<AiResponse> generateStream(Prompt prompt) {
|
||||
public Flux<ChatResponse> generateStream(Prompt prompt) {
|
||||
|
||||
final String promptValue = MessageToPromptConverter.create().toPrompt(prompt.getMessages());
|
||||
|
||||
@@ -128,7 +129,7 @@ public class BedrockAnthropicChatClient implements AiClient, AiStreamClient {
|
||||
generation = generation
|
||||
.withChoiceMetadata(ChoiceMetadata.from(stopReason, response.amazonBedrockInvocationMetrics()));
|
||||
}
|
||||
return new AiResponse(List.of(generation));
|
||||
return new ChatResponse(List.of(generation));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ package org.springframework.ai.bedrock.cohere;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.ai.chat.ChatResponse;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.ai.bedrock.BedrockUsage;
|
||||
@@ -28,10 +29,9 @@ import org.springframework.ai.bedrock.cohere.api.CohereChatBedrockApi.CohereChat
|
||||
import org.springframework.ai.bedrock.cohere.api.CohereChatBedrockApi.CohereChatRequest.LogitBias;
|
||||
import org.springframework.ai.bedrock.cohere.api.CohereChatBedrockApi.CohereChatRequest.ReturnLikelihoods;
|
||||
import org.springframework.ai.bedrock.cohere.api.CohereChatBedrockApi.CohereChatRequest.Truncate;
|
||||
import org.springframework.ai.client.AiClient;
|
||||
import org.springframework.ai.client.AiResponse;
|
||||
import org.springframework.ai.client.AiStreamClient;
|
||||
import org.springframework.ai.client.Generation;
|
||||
import org.springframework.ai.chat.ChatClient;
|
||||
import org.springframework.ai.chat.StreamingChatClient;
|
||||
import org.springframework.ai.chat.Generation;
|
||||
import org.springframework.ai.metadata.ChoiceMetadata;
|
||||
import org.springframework.ai.metadata.Usage;
|
||||
import org.springframework.ai.prompt.Prompt;
|
||||
@@ -40,7 +40,7 @@ import org.springframework.ai.prompt.Prompt;
|
||||
* @author Christian Tzolov
|
||||
* @since 0.8.0
|
||||
*/
|
||||
public class BedrockCohereChatClient implements AiClient, AiStreamClient {
|
||||
public class BedrockCohereChatClient implements ChatClient, StreamingChatClient {
|
||||
|
||||
private final CohereChatBedrockApi chatApi;
|
||||
|
||||
@@ -112,25 +112,25 @@ public class BedrockCohereChatClient implements AiClient, AiStreamClient {
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiResponse generate(Prompt prompt) {
|
||||
public ChatResponse generate(Prompt prompt) {
|
||||
CohereChatResponse response = this.chatApi.chatCompletion(this.createRequest(prompt, false));
|
||||
List<Generation> generations = response.generations().stream().map(g -> {
|
||||
return new Generation(g.text());
|
||||
}).toList();
|
||||
|
||||
return new AiResponse(generations);
|
||||
return new ChatResponse(generations);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<AiResponse> generateStream(Prompt prompt) {
|
||||
public Flux<ChatResponse> generateStream(Prompt prompt) {
|
||||
return this.chatApi.chatCompletionStream(this.createRequest(prompt, true)).map(g -> {
|
||||
if (g.isFinished()) {
|
||||
String finishReason = g.finishReason().name();
|
||||
Usage usage = BedrockUsage.from(g.amazonBedrockInvocationMetrics());
|
||||
return new AiResponse(
|
||||
return new ChatResponse(
|
||||
List.of(new Generation("").withChoiceMetadata(ChoiceMetadata.from(finishReason, usage))));
|
||||
}
|
||||
return new AiResponse(List.of(new Generation(g.text())));
|
||||
return new ChatResponse(List.of(new Generation(g.text())));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -18,27 +18,28 @@ package org.springframework.ai.bedrock.llama2;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.ai.chat.ChatResponse;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.ai.bedrock.MessageToPromptConverter;
|
||||
import org.springframework.ai.bedrock.llama2.api.Llama2ChatBedrockApi;
|
||||
import org.springframework.ai.bedrock.llama2.api.Llama2ChatBedrockApi.Llama2ChatRequest;
|
||||
import org.springframework.ai.bedrock.llama2.api.Llama2ChatBedrockApi.Llama2ChatResponse;
|
||||
import org.springframework.ai.client.AiClient;
|
||||
import org.springframework.ai.client.AiResponse;
|
||||
import org.springframework.ai.client.AiStreamClient;
|
||||
import org.springframework.ai.client.Generation;
|
||||
import org.springframework.ai.chat.ChatClient;
|
||||
import org.springframework.ai.chat.StreamingChatClient;
|
||||
import org.springframework.ai.chat.Generation;
|
||||
import org.springframework.ai.metadata.ChoiceMetadata;
|
||||
import org.springframework.ai.metadata.Usage;
|
||||
import org.springframework.ai.prompt.Prompt;
|
||||
|
||||
/**
|
||||
* Java {@link AiClient} and {@link AiStreamClient} for the Bedrock Llama2 chat model.
|
||||
* Java {@link ChatClient} and {@link StreamingChatClient} for the Bedrock Llama2 chat
|
||||
* model.
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
* @since 0.8.0
|
||||
*/
|
||||
public class BedrockLlama2ChatClient implements AiClient, AiStreamClient {
|
||||
public class BedrockLlama2ChatClient implements ChatClient, StreamingChatClient {
|
||||
|
||||
private final Llama2ChatBedrockApi chatApi;
|
||||
|
||||
@@ -68,7 +69,7 @@ public class BedrockLlama2ChatClient implements AiClient, AiStreamClient {
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiResponse generate(Prompt prompt) {
|
||||
public ChatResponse generate(Prompt prompt) {
|
||||
final String promptValue = MessageToPromptConverter.create().toPrompt(prompt.getMessages());
|
||||
|
||||
var request = Llama2ChatRequest.builder(promptValue)
|
||||
@@ -79,12 +80,12 @@ public class BedrockLlama2ChatClient implements AiClient, AiStreamClient {
|
||||
|
||||
Llama2ChatResponse response = this.chatApi.chatCompletion(request);
|
||||
|
||||
return new AiResponse(List.of(new Generation(response.generation())
|
||||
return new ChatResponse(List.of(new Generation(response.generation())
|
||||
.withChoiceMetadata(ChoiceMetadata.from(response.stopReason().name(), extractUsage(response)))));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<AiResponse> generateStream(Prompt prompt) {
|
||||
public Flux<ChatResponse> generateStream(Prompt prompt) {
|
||||
|
||||
final String promptValue = MessageToPromptConverter.create().toPrompt(prompt.getMessages());
|
||||
|
||||
@@ -98,7 +99,7 @@ public class BedrockLlama2ChatClient implements AiClient, AiStreamClient {
|
||||
|
||||
return fluxResponse.map(response -> {
|
||||
String stopReason = response.stopReason() != null ? response.stopReason().name() : null;
|
||||
return new AiResponse(List.of(new Generation(response.generation())
|
||||
return new ChatResponse(List.of(new Generation(response.generation())
|
||||
.withChoiceMetadata(ChoiceMetadata.from(stopReason, extractUsage(response)))));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ package org.springframework.ai.bedrock.titan;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.ai.chat.ChatClient;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.ai.bedrock.MessageToPromptConverter;
|
||||
@@ -25,10 +26,9 @@ import org.springframework.ai.bedrock.titan.api.TitanChatBedrockApi;
|
||||
import org.springframework.ai.bedrock.titan.api.TitanChatBedrockApi.TitanChatRequest;
|
||||
import org.springframework.ai.bedrock.titan.api.TitanChatBedrockApi.TitanChatResponse;
|
||||
import org.springframework.ai.bedrock.titan.api.TitanChatBedrockApi.TitanChatResponseChunk;
|
||||
import org.springframework.ai.client.AiClient;
|
||||
import org.springframework.ai.client.AiResponse;
|
||||
import org.springframework.ai.client.AiStreamClient;
|
||||
import org.springframework.ai.client.Generation;
|
||||
import org.springframework.ai.chat.ChatResponse;
|
||||
import org.springframework.ai.chat.StreamingChatClient;
|
||||
import org.springframework.ai.chat.Generation;
|
||||
import org.springframework.ai.metadata.ChoiceMetadata;
|
||||
import org.springframework.ai.metadata.Usage;
|
||||
import org.springframework.ai.prompt.Prompt;
|
||||
@@ -37,7 +37,7 @@ import org.springframework.ai.prompt.Prompt;
|
||||
* @author Christian Tzolov
|
||||
* @since 0.8.0
|
||||
*/
|
||||
public class BedrockTitanChatClient implements AiClient, AiStreamClient {
|
||||
public class BedrockTitanChatClient implements ChatClient, StreamingChatClient {
|
||||
|
||||
private final TitanChatBedrockApi chatApi;
|
||||
|
||||
@@ -74,17 +74,17 @@ public class BedrockTitanChatClient implements AiClient, AiStreamClient {
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiResponse generate(Prompt prompt) {
|
||||
public ChatResponse generate(Prompt prompt) {
|
||||
TitanChatResponse response = this.chatApi.chatCompletion(this.createRequest(prompt, false));
|
||||
List<Generation> generations = response.results().stream().map(result -> {
|
||||
return new Generation(result.outputText());
|
||||
}).toList();
|
||||
|
||||
return new AiResponse(generations);
|
||||
return new ChatResponse(generations);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<AiResponse> generateStream(Prompt prompt) {
|
||||
public Flux<ChatResponse> generateStream(Prompt prompt) {
|
||||
return this.chatApi.chatCompletionStream(this.createRequest(prompt, true)).map(chunk -> {
|
||||
|
||||
Generation generation = new Generation(chunk.outputText());
|
||||
@@ -99,7 +99,7 @@ public class BedrockTitanChatClient implements AiClient, AiStreamClient {
|
||||
generation = generation.withChoiceMetadata(ChoiceMetadata.from(completionReason, extractUsage(chunk)));
|
||||
|
||||
}
|
||||
return new AiResponse(List.of(generation));
|
||||
return new ChatResponse(List.of(generation));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -8,12 +8,12 @@ import java.util.stream.Collectors;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
|
||||
import org.springframework.ai.chat.ChatResponse;
|
||||
import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider;
|
||||
import software.amazon.awssdk.regions.Region;
|
||||
|
||||
import org.springframework.ai.bedrock.anthropic.api.AnthropicChatBedrockApi;
|
||||
import org.springframework.ai.client.AiResponse;
|
||||
import org.springframework.ai.client.Generation;
|
||||
import org.springframework.ai.chat.Generation;
|
||||
import org.springframework.ai.parser.BeanOutputParser;
|
||||
import org.springframework.ai.parser.ListOutputParser;
|
||||
import org.springframework.ai.parser.MapOutputParser;
|
||||
@@ -52,7 +52,7 @@ class BedrockAnthropicChatClientIT {
|
||||
|
||||
Prompt prompt = new Prompt(List.of(userMessage, systemMessage));
|
||||
|
||||
AiResponse response = client.generate(prompt);
|
||||
ChatResponse response = client.generate(prompt);
|
||||
|
||||
assertThat(response.getGeneration().getContent()).contains("Blackbeard");
|
||||
}
|
||||
@@ -137,7 +137,7 @@ class BedrockAnthropicChatClientIT {
|
||||
.collectList()
|
||||
.block()
|
||||
.stream()
|
||||
.map(AiResponse::getGenerations)
|
||||
.map(ChatResponse::getGenerations)
|
||||
.flatMap(List::stream)
|
||||
.map(Generation::getContent)
|
||||
.collect(Collectors.joining());
|
||||
|
||||
@@ -13,8 +13,8 @@ import software.amazon.awssdk.regions.Region;
|
||||
|
||||
import org.springframework.ai.bedrock.cohere.api.CohereChatBedrockApi;
|
||||
import org.springframework.ai.bedrock.cohere.api.CohereChatBedrockApi.CohereChatModel;
|
||||
import org.springframework.ai.client.AiResponse;
|
||||
import org.springframework.ai.client.Generation;
|
||||
import org.springframework.ai.chat.ChatResponse;
|
||||
import org.springframework.ai.chat.Generation;
|
||||
import org.springframework.ai.parser.BeanOutputParser;
|
||||
import org.springframework.ai.parser.ListOutputParser;
|
||||
import org.springframework.ai.parser.MapOutputParser;
|
||||
@@ -53,7 +53,7 @@ class BedrockCohereChatClientIT {
|
||||
SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(systemResource);
|
||||
Message systemMessage = systemPromptTemplate.createMessage(Map.of("name", name, "voice", voice));
|
||||
Prompt prompt = new Prompt(List.of(userMessage, systemMessage));
|
||||
AiResponse response = client.generate(prompt);
|
||||
ChatResponse response = client.generate(prompt);
|
||||
assertThat(response.getGeneration().getContent()).contains("Blackbeard");
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ class BedrockCohereChatClientIT {
|
||||
.collectList()
|
||||
.block()
|
||||
.stream()
|
||||
.map(AiResponse::getGenerations)
|
||||
.map(ChatResponse::getGenerations)
|
||||
.flatMap(List::stream)
|
||||
.map(Generation::getContent)
|
||||
.collect(Collectors.joining());
|
||||
|
||||
@@ -9,13 +9,13 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
|
||||
import org.springframework.ai.chat.ChatResponse;
|
||||
import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider;
|
||||
import software.amazon.awssdk.regions.Region;
|
||||
|
||||
import org.springframework.ai.bedrock.llama2.api.Llama2ChatBedrockApi;
|
||||
import org.springframework.ai.bedrock.llama2.api.Llama2ChatBedrockApi.Llama2ChatCompletionModel;
|
||||
import org.springframework.ai.client.AiResponse;
|
||||
import org.springframework.ai.client.Generation;
|
||||
import org.springframework.ai.chat.Generation;
|
||||
import org.springframework.ai.parser.BeanOutputParser;
|
||||
import org.springframework.ai.parser.ListOutputParser;
|
||||
import org.springframework.ai.parser.MapOutputParser;
|
||||
@@ -54,7 +54,7 @@ class BedrockLlama2ChatClientIT {
|
||||
|
||||
Prompt prompt = new Prompt(List.of(userMessage, systemMessage));
|
||||
|
||||
AiResponse response = client.generate(prompt);
|
||||
ChatResponse response = client.generate(prompt);
|
||||
|
||||
assertThat(response.getGeneration().getContent()).contains("Blackbeard");
|
||||
}
|
||||
@@ -142,7 +142,7 @@ class BedrockLlama2ChatClientIT {
|
||||
.collectList()
|
||||
.block()
|
||||
.stream()
|
||||
.map(AiResponse::getGenerations)
|
||||
.map(ChatResponse::getGenerations)
|
||||
.flatMap(List::stream)
|
||||
.map(Generation::getContent)
|
||||
.collect(Collectors.joining());
|
||||
|
||||
@@ -9,13 +9,13 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
|
||||
import org.springframework.ai.chat.ChatResponse;
|
||||
import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider;
|
||||
import software.amazon.awssdk.regions.Region;
|
||||
|
||||
import org.springframework.ai.bedrock.titan.api.TitanChatBedrockApi;
|
||||
import org.springframework.ai.bedrock.titan.api.TitanChatBedrockApi.TitanChatModel;
|
||||
import org.springframework.ai.client.AiResponse;
|
||||
import org.springframework.ai.client.Generation;
|
||||
import org.springframework.ai.chat.Generation;
|
||||
import org.springframework.ai.parser.BeanOutputParser;
|
||||
import org.springframework.ai.parser.ListOutputParser;
|
||||
import org.springframework.ai.parser.MapOutputParser;
|
||||
@@ -54,7 +54,7 @@ class BedrockTitanChatClientIT {
|
||||
SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(systemResource);
|
||||
Message systemMessage = systemPromptTemplate.createMessage(Map.of("name", name, "voice", voice));
|
||||
Prompt prompt = new Prompt(List.of(userMessage, systemMessage));
|
||||
AiResponse response = client.generate(prompt);
|
||||
ChatResponse response = client.generate(prompt);
|
||||
assertThat(response.getGeneration().getContent()).contains("Blackbeard");
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ class BedrockTitanChatClientIT {
|
||||
.collectList()
|
||||
.block()
|
||||
.stream()
|
||||
.map(AiResponse::getGenerations)
|
||||
.map(ChatResponse::getGenerations)
|
||||
.flatMap(List::stream)
|
||||
.map(Generation::getContent)
|
||||
.collect(Collectors.joining());
|
||||
|
||||
@@ -14,19 +14,19 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.ai.client;
|
||||
package org.springframework.ai.chat;
|
||||
|
||||
import org.springframework.ai.prompt.Prompt;
|
||||
import org.springframework.ai.prompt.messages.UserMessage;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface AiClient {
|
||||
public interface ChatClient {
|
||||
|
||||
default String generate(String message) {
|
||||
Prompt prompt = new Prompt(new UserMessage(message));
|
||||
return generate(prompt).getGeneration().getContent();
|
||||
}
|
||||
|
||||
AiResponse generate(Prompt prompt);
|
||||
ChatResponse generate(Prompt prompt);
|
||||
|
||||
}
|
||||
@@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.ai.client;
|
||||
package org.springframework.ai.chat;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -24,7 +24,7 @@ import org.springframework.lang.Nullable;
|
||||
/**
|
||||
* The chat completion (e.g. generation) response returned by an AI provider.
|
||||
*/
|
||||
public class AiResponse {
|
||||
public class ChatResponse {
|
||||
|
||||
private final GenerationMetadata metadata;
|
||||
|
||||
@@ -36,22 +36,22 @@ public class AiResponse {
|
||||
private PromptMetadata promptMetadata;
|
||||
|
||||
/**
|
||||
* Construct a new {@link AiResponse} instance without metadata.
|
||||
* Construct a new {@link ChatResponse} instance without metadata.
|
||||
* @param generations the {@link List} of {@link Generation} returned by the AI
|
||||
* provider.
|
||||
*/
|
||||
public AiResponse(List<Generation> generations) {
|
||||
public ChatResponse(List<Generation> generations) {
|
||||
this(generations, GenerationMetadata.NULL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new {@link AiResponse} instance.
|
||||
* Construct a new {@link ChatResponse} instance.
|
||||
* @param generations the {@link List} of {@link Generation} returned by the AI
|
||||
* provider.
|
||||
* @param metadata {@link GenerationMetadata} containing information about the use of
|
||||
* the AI provider's API.
|
||||
*/
|
||||
public AiResponse(List<Generation> generations, GenerationMetadata metadata) {
|
||||
public ChatResponse(List<Generation> generations, GenerationMetadata metadata) {
|
||||
this.metadata = metadata;
|
||||
this.generations = List.copyOf(generations);
|
||||
}
|
||||
@@ -96,10 +96,10 @@ public class AiResponse {
|
||||
* when processing the prompt.
|
||||
* @param promptMetadata {@link PromptMetadata} returned by the AI in the response
|
||||
* when processing the prompt.
|
||||
* @return this {@link AiResponse}.
|
||||
* @return this {@link ChatResponse}.
|
||||
* @see #getPromptMetadata()
|
||||
*/
|
||||
public AiResponse withPromptMetadata(@Nullable PromptMetadata promptMetadata) {
|
||||
public ChatResponse withPromptMetadata(@Nullable PromptMetadata promptMetadata) {
|
||||
this.promptMetadata = promptMetadata;
|
||||
return this;
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.ai.client;
|
||||
package org.springframework.ai.chat;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
@@ -14,15 +14,15 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.ai.client;
|
||||
package org.springframework.ai.chat;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.ai.prompt.Prompt;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface AiStreamClient {
|
||||
public interface StreamingChatClient {
|
||||
|
||||
public Flux<AiResponse> generateStream(Prompt prompt);
|
||||
public Flux<ChatResponse> generateStream(Prompt prompt);
|
||||
|
||||
}
|
||||
@@ -19,7 +19,7 @@ package org.springframework.ai.transformer;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.ai.client.AiClient;
|
||||
import org.springframework.ai.chat.ChatClient;
|
||||
import org.springframework.ai.document.Document;
|
||||
import org.springframework.ai.document.DocumentTransformer;
|
||||
import org.springframework.ai.prompt.Prompt;
|
||||
@@ -44,18 +44,18 @@ public class KeywordMetadataEnricher implements DocumentTransformer {
|
||||
/**
|
||||
* Model predictor
|
||||
*/
|
||||
private final AiClient aiClient;
|
||||
private final ChatClient chatClient;
|
||||
|
||||
/**
|
||||
* The number of keywords to extract.
|
||||
*/
|
||||
private final int keywordCount;
|
||||
|
||||
public KeywordMetadataEnricher(AiClient aiClient, int keywordCount) {
|
||||
Assert.notNull(aiClient, "AiClient must not be null");
|
||||
public KeywordMetadataEnricher(ChatClient chatClient, int keywordCount) {
|
||||
Assert.notNull(chatClient, "ChatClient must not be null");
|
||||
Assert.isTrue(keywordCount >= 1, "Document count must be >= 1");
|
||||
|
||||
this.aiClient = aiClient;
|
||||
this.chatClient = chatClient;
|
||||
this.keywordCount = keywordCount;
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ public class KeywordMetadataEnricher implements DocumentTransformer {
|
||||
|
||||
var template = new PromptTemplate(String.format(KEYWORDS_TEMPLATE, keywordCount));
|
||||
Prompt prompt = template.create(Map.of(CONTEXT_STR_PLACEHOLDER, document.getContent()));
|
||||
String keywords = this.aiClient.generate(prompt).getGeneration().getContent();
|
||||
String keywords = this.chatClient.generate(prompt).getGeneration().getContent();
|
||||
document.getMetadata().putAll(Map.of(EXCERPT_KEYWORDS_METADATA_KEY, keywords));
|
||||
}
|
||||
return documents;
|
||||
|
||||
@@ -21,7 +21,7 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.ai.client.AiClient;
|
||||
import org.springframework.ai.chat.ChatClient;
|
||||
import org.springframework.ai.document.Document;
|
||||
import org.springframework.ai.document.DocumentTransformer;
|
||||
import org.springframework.ai.document.MetadataMode;
|
||||
@@ -63,7 +63,7 @@ public class SummaryMetadataEnricher implements DocumentTransformer {
|
||||
/**
|
||||
* AI client.
|
||||
*/
|
||||
private final AiClient aiClient;
|
||||
private final ChatClient chatClient;
|
||||
|
||||
/**
|
||||
* Number of documents from front to use for title extraction.
|
||||
@@ -77,16 +77,16 @@ public class SummaryMetadataEnricher implements DocumentTransformer {
|
||||
*/
|
||||
private final String summaryTemplate;
|
||||
|
||||
public SummaryMetadataEnricher(AiClient aiClient, List<SummaryType> summaryTypes) {
|
||||
this(aiClient, summaryTypes, DEFAULT_SUMMARY_EXTRACT_TEMPLATE, MetadataMode.ALL);
|
||||
public SummaryMetadataEnricher(ChatClient chatClient, List<SummaryType> summaryTypes) {
|
||||
this(chatClient, summaryTypes, DEFAULT_SUMMARY_EXTRACT_TEMPLATE, MetadataMode.ALL);
|
||||
}
|
||||
|
||||
public SummaryMetadataEnricher(AiClient aiClient, List<SummaryType> summaryTypes, String summaryTemplate,
|
||||
public SummaryMetadataEnricher(ChatClient chatClient, List<SummaryType> summaryTypes, String summaryTemplate,
|
||||
MetadataMode metadataMode) {
|
||||
Assert.notNull(aiClient, "AiClient must not be null");
|
||||
Assert.notNull(chatClient, "ChatClient must not be null");
|
||||
Assert.hasText(summaryTemplate, "Summary template must not be empty");
|
||||
|
||||
this.aiClient = aiClient;
|
||||
this.chatClient = chatClient;
|
||||
this.summaryTypes = CollectionUtils.isEmpty(summaryTypes) ? List.of(SummaryType.CURRENT) : summaryTypes;
|
||||
this.metadataMode = metadataMode;
|
||||
this.summaryTemplate = summaryTemplate;
|
||||
@@ -102,7 +102,7 @@ public class SummaryMetadataEnricher implements DocumentTransformer {
|
||||
|
||||
Prompt prompt = new PromptTemplate(this.summaryTemplate)
|
||||
.create(Map.of(CONTEXT_STR_PLACEHOLDER, documentContext));
|
||||
documentSummaries.add(this.aiClient.generate(prompt).getGeneration().getContent());
|
||||
documentSummaries.add(this.chatClient.generate(prompt).getGeneration().getContent());
|
||||
}
|
||||
|
||||
for (int i = 0; i < documentSummaries.size(); i++) {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.ai.client;
|
||||
package org.springframework.ai.chat;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
@@ -33,10 +33,11 @@ import java.util.Collections;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.mockito.Mockito;
|
||||
import org.springframework.ai.prompt.Prompt;
|
||||
|
||||
/**
|
||||
* Unit Tests for {@link AiClient}.
|
||||
* Unit Tests for {@link ChatClient}.
|
||||
*
|
||||
* @author John Blum
|
||||
* @since 0.2.0
|
||||
@@ -49,9 +50,9 @@ class AiClientTests {
|
||||
String userMessage = "Zero Wing";
|
||||
String responseMessage = "All your bases are belong to us";
|
||||
|
||||
AiClient mockClient = mock(AiClient.class);
|
||||
ChatClient mockClient = Mockito.mock(ChatClient.class);
|
||||
Generation generation = spy(new Generation(responseMessage));
|
||||
AiResponse response = spy(new AiResponse(Collections.singletonList(generation)));
|
||||
ChatResponse response = spy(new ChatResponse(Collections.singletonList(generation)));
|
||||
|
||||
doCallRealMethod().when(mockClient).generate(anyString());
|
||||
|
||||
@@ -23,9 +23,9 @@ import java.util.Map;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import org.springframework.ai.client.AiClient;
|
||||
import org.springframework.ai.client.AiResponse;
|
||||
import org.springframework.ai.client.Generation;
|
||||
import org.springframework.ai.chat.ChatClient;
|
||||
import org.springframework.ai.chat.ChatResponse;
|
||||
import org.springframework.ai.chat.Generation;
|
||||
import org.springframework.ai.huggingface.api.TextGenerationInferenceApi;
|
||||
import org.springframework.ai.huggingface.invoker.ApiClient;
|
||||
import org.springframework.ai.huggingface.model.AllOfGenerateResponseDetails;
|
||||
@@ -35,12 +35,12 @@ import org.springframework.ai.huggingface.model.GenerateResponse;
|
||||
import org.springframework.ai.prompt.Prompt;
|
||||
|
||||
/**
|
||||
* An implementation of {@link AiClient} that interfaces with HuggingFace Inference
|
||||
* An implementation of {@link ChatClient} that interfaces with HuggingFace Inference
|
||||
* Endpoints for text generation.
|
||||
*
|
||||
* @author Mark Pollack
|
||||
*/
|
||||
public class HuggingfaceAiClient implements AiClient {
|
||||
public class HuggingfaceAiClient implements ChatClient {
|
||||
|
||||
/**
|
||||
* Token required for authenticating with the HuggingFace Inference API.
|
||||
@@ -83,10 +83,10 @@ public class HuggingfaceAiClient implements AiClient {
|
||||
/**
|
||||
* Generate text based on the provided prompt.
|
||||
* @param prompt The input prompt based on which text is to be generated.
|
||||
* @return AiResponse containing the generated text and other related details.
|
||||
* @return ChatResponse containing the generated text and other related details.
|
||||
*/
|
||||
@Override
|
||||
public AiResponse generate(Prompt prompt) {
|
||||
public ChatResponse generate(Prompt prompt) {
|
||||
GenerateRequest generateRequest = new GenerateRequest();
|
||||
generateRequest.setInputs(prompt.getContents());
|
||||
GenerateParameters generateParameters = new GenerateParameters();
|
||||
@@ -102,7 +102,7 @@ public class HuggingfaceAiClient implements AiClient {
|
||||
});
|
||||
Generation generation = new Generation(generatedText, detailsMap);
|
||||
generations.add(generation);
|
||||
return new AiResponse(generations);
|
||||
return new ChatResponse(generations);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,7 +20,7 @@ import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
|
||||
|
||||
import org.springframework.ai.client.AiResponse;
|
||||
import org.springframework.ai.chat.ChatResponse;
|
||||
import org.springframework.ai.prompt.Prompt;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
@@ -46,8 +46,8 @@ public class ClientIT {
|
||||
[/INST]
|
||||
""";
|
||||
Prompt prompt = new Prompt(mistral7bInstruct);
|
||||
AiResponse aiResponse = huggingfaceAiClient.generate(prompt);
|
||||
assertThat(aiResponse.getGeneration().getContent()).isNotEmpty();
|
||||
ChatResponse chatResponse = huggingfaceAiClient.generate(prompt);
|
||||
assertThat(chatResponse.getGeneration().getContent()).isNotEmpty();
|
||||
String expectedResponse = """
|
||||
```json
|
||||
{
|
||||
@@ -56,9 +56,9 @@ public class ClientIT {
|
||||
"address": "#1 Samuel St."
|
||||
}
|
||||
```""";
|
||||
assertThat(aiResponse.getGeneration().getContent()).isEqualTo(expectedResponse);
|
||||
assertThat(aiResponse.getGeneration().getProperties()).containsKey("generated_tokens");
|
||||
assertThat(aiResponse.getGeneration().getProperties()).containsEntry("generated_tokens", 39);
|
||||
assertThat(chatResponse.getGeneration().getContent()).isEqualTo(expectedResponse);
|
||||
assertThat(chatResponse.getGeneration().getProperties()).containsKey("generated_tokens");
|
||||
assertThat(chatResponse.getGeneration().getProperties()).containsEntry("generated_tokens", 39);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.ai.client.AiClient;
|
||||
import org.springframework.ai.client.AiResponse;
|
||||
import org.springframework.ai.client.Generation;
|
||||
import org.springframework.ai.chat.ChatClient;
|
||||
import org.springframework.ai.chat.ChatResponse;
|
||||
import org.springframework.ai.chat.Generation;
|
||||
import org.springframework.ai.prompt.Prompt;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
@@ -30,7 +30,7 @@ import java.util.stream.Collectors;
|
||||
*
|
||||
* @author nullptr
|
||||
*/
|
||||
public class OllamaClient implements AiClient {
|
||||
public class OllamaClient implements ChatClient {
|
||||
|
||||
/** Logger for logging the events and messages. */
|
||||
private static final Logger log = LoggerFactory.getLogger(OllamaClient.class);
|
||||
@@ -72,7 +72,7 @@ public class OllamaClient implements AiClient {
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiResponse generate(Prompt prompt) {
|
||||
public ChatResponse generate(Prompt prompt) {
|
||||
validatePrompt(prompt);
|
||||
|
||||
HttpRequest request = buildHttpRequest(prompt);
|
||||
@@ -196,11 +196,11 @@ public class OllamaClient implements AiClient {
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the list of OllamaGenerateResult into a structured AiResponse.
|
||||
* Converts the list of OllamaGenerateResult into a structured ChatResponse.
|
||||
* @param results List of OllamaGenerateResult.
|
||||
* @return Formulated AiResponse.
|
||||
* @return Formulated ChatResponse.
|
||||
*/
|
||||
protected AiResponse getAiResponse(List<OllamaGenerateResult> results) {
|
||||
protected ChatResponse getAiResponse(List<OllamaGenerateResult> results) {
|
||||
var ollamaResponse = results.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.filter(it -> it.getResponse() != null && !it.getResponse().isBlank())
|
||||
@@ -211,7 +211,7 @@ public class OllamaClient implements AiClient {
|
||||
var generation = new Generation(ollamaResponse);
|
||||
|
||||
// TODO investigate mapping of additional metadata/runtime info to the response.
|
||||
return new AiResponse(Collections.singletonList(generation));
|
||||
return new ChatResponse(Collections.singletonList(generation));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,7 @@ package org.springframework.ai.ollama.client;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.ai.client.AiResponse;
|
||||
import org.springframework.ai.chat.ChatResponse;
|
||||
import org.springframework.ai.prompt.Prompt;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
@@ -17,11 +17,11 @@ public class OllamaClientTests {
|
||||
OllamaClient ollama2 = getOllamaClient();
|
||||
|
||||
Prompt prompt = new Prompt("Hello");
|
||||
AiResponse aiResponse = ollama2.generate(prompt);
|
||||
Assertions.assertNotNull(aiResponse);
|
||||
Assertions.assertFalse(CollectionUtils.isEmpty(aiResponse.getGenerations()));
|
||||
Assertions.assertNotNull(aiResponse.getGeneration());
|
||||
Assertions.assertNotNull(aiResponse.getGeneration().getContent());
|
||||
ChatResponse chatResponse = ollama2.generate(prompt);
|
||||
Assertions.assertNotNull(chatResponse);
|
||||
Assertions.assertFalse(CollectionUtils.isEmpty(chatResponse.getGenerations()));
|
||||
Assertions.assertNotNull(chatResponse.getGeneration());
|
||||
Assertions.assertNotNull(chatResponse.getGeneration().getContent());
|
||||
}
|
||||
|
||||
private static OllamaClient getOllamaClient() {
|
||||
|
||||
@@ -22,12 +22,12 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.ai.chat.ChatClient;
|
||||
import org.springframework.ai.chat.ChatResponse;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.ai.client.AiClient;
|
||||
import org.springframework.ai.client.AiResponse;
|
||||
import org.springframework.ai.client.AiStreamClient;
|
||||
import org.springframework.ai.client.Generation;
|
||||
import org.springframework.ai.chat.StreamingChatClient;
|
||||
import org.springframework.ai.chat.Generation;
|
||||
import org.springframework.ai.metadata.ChoiceMetadata;
|
||||
import org.springframework.ai.metadata.RateLimit;
|
||||
import org.springframework.ai.openai.api.OpenAiApi;
|
||||
@@ -43,7 +43,7 @@ import org.springframework.retry.support.RetryTemplate;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* {@link AiClient} implementation for {@literal OpenAI} backed by {@link OpenAiApi}.
|
||||
* {@link ChatClient} implementation for {@literal OpenAI} backed by {@link OpenAiApi}.
|
||||
*
|
||||
* @author Mark Pollack
|
||||
* @author Christian Tzolov
|
||||
@@ -51,11 +51,11 @@ import org.springframework.util.Assert;
|
||||
* @author John Blum
|
||||
* @author Josh Long
|
||||
* @author Jemin Huh
|
||||
* @see org.springframework.ai.client.AiClient
|
||||
* @see org.springframework.ai.client.AiStreamClient
|
||||
* @see ChatClient
|
||||
* @see StreamingChatClient
|
||||
* @see OpenAiApi
|
||||
*/
|
||||
public class OpenAiClient implements AiClient, AiStreamClient {
|
||||
public class OpenAiClient implements ChatClient, StreamingChatClient {
|
||||
|
||||
private Double temperature = 0.7;
|
||||
|
||||
@@ -93,7 +93,7 @@ public class OpenAiClient implements AiClient, AiStreamClient {
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiResponse generate(Prompt prompt) {
|
||||
public ChatResponse generate(Prompt prompt) {
|
||||
|
||||
return this.retryTemplate.execute(ctx -> {
|
||||
List<Message> messages = prompt.getMessages();
|
||||
@@ -110,7 +110,7 @@ public class OpenAiClient implements AiClient, AiStreamClient {
|
||||
var chatCompletion = completionEntity.getBody();
|
||||
if (chatCompletion == null) {
|
||||
logger.warn("No chat completion returned for request: {}", chatCompletionMessages);
|
||||
return new AiResponse(List.of());
|
||||
return new ChatResponse(List.of());
|
||||
}
|
||||
|
||||
RateLimit rateLimits = OpenAiResponseHeaderExtractor.extractAiResponseHeaders(completionEntity);
|
||||
@@ -120,13 +120,13 @@ public class OpenAiClient implements AiClient, AiStreamClient {
|
||||
.withChoiceMetadata(ChoiceMetadata.from(choice.finishReason().name(), null));
|
||||
}).toList();
|
||||
|
||||
return new AiResponse(generations,
|
||||
return new ChatResponse(generations,
|
||||
OpenAiGenerationMetadata.from(completionEntity.getBody()).withRateLimit(rateLimits));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<AiResponse> generateStream(Prompt prompt) {
|
||||
public Flux<ChatResponse> generateStream(Prompt prompt) {
|
||||
return this.retryTemplate.execute(ctx -> {
|
||||
List<Message> messages = prompt.getMessages();
|
||||
|
||||
@@ -144,7 +144,7 @@ public class OpenAiClient implements AiClient, AiStreamClient {
|
||||
ConcurrentHashMap<String, String> roleMap = new ConcurrentHashMap<>();
|
||||
|
||||
// An alternative implementation that returns Flux<Generation> instead of
|
||||
// Flux<AiResponse>.
|
||||
// Flux<ChatResponse>.
|
||||
// Flux<Generation> generationFlux = completionChunks.map(chunk -> {
|
||||
// String chunkId = chunk.id();
|
||||
// return chunk.choices().stream()
|
||||
@@ -172,7 +172,7 @@ public class OpenAiClient implements AiClient, AiStreamClient {
|
||||
}
|
||||
return generation;
|
||||
}).toList();
|
||||
return new AiResponse(generations);
|
||||
return new ChatResponse(generations);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.ai.client.AiResponse;
|
||||
import org.springframework.ai.chat.ChatResponse;
|
||||
import org.springframework.ai.document.Document;
|
||||
import org.springframework.ai.openai.OpenAiTestConfiguration;
|
||||
import org.springframework.ai.openai.client.OpenAiClient;
|
||||
@@ -96,7 +96,7 @@ public class AcmeIT extends AbstractIT {
|
||||
logger.info("Asking AI model to reply to question.");
|
||||
Prompt prompt = new Prompt(List.of(systemMessage, userMessage));
|
||||
logger.info("AI responded.");
|
||||
AiResponse response = aiClient.generate(prompt);
|
||||
ChatResponse response = aiClient.generate(prompt);
|
||||
|
||||
evaluateQuestionAndAnswer(userQuery, response, true);
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ import java.util.stream.Collectors;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
|
||||
|
||||
import org.springframework.ai.client.AiResponse;
|
||||
import org.springframework.ai.client.Generation;
|
||||
import org.springframework.ai.chat.ChatResponse;
|
||||
import org.springframework.ai.chat.Generation;
|
||||
import org.springframework.ai.openai.OpenAiTestConfiguration;
|
||||
import org.springframework.ai.openai.testutils.AbstractIT;
|
||||
import org.springframework.ai.parser.BeanOutputParser;
|
||||
@@ -41,7 +41,7 @@ class OpenAiClientIT extends AbstractIT {
|
||||
SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(systemResource);
|
||||
Message systemMessage = systemPromptTemplate.createMessage(Map.of("name", "Bob", "voice", "pirate"));
|
||||
Prompt prompt = new Prompt(List.of(userMessage, systemMessage));
|
||||
AiResponse response = openAiClient.generate(prompt);
|
||||
ChatResponse response = openAiClient.generate(prompt);
|
||||
assertThat(response.getGenerations()).hasSize(1);
|
||||
assertThat(response.getGenerations().get(0).getContent()).contains("Blackbeard");
|
||||
// needs fine tuning... evaluateQuestionAndAnswer(request, response, false);
|
||||
@@ -139,11 +139,11 @@ class OpenAiClientIT extends AbstractIT {
|
||||
PromptTemplate promptTemplate = new PromptTemplate(template, Map.of("format", format));
|
||||
Prompt prompt = new Prompt(promptTemplate.createMessage());
|
||||
|
||||
String generationTextFromStream = openAiStreamClient.generateStream(prompt)
|
||||
String generationTextFromStream = openStreamingChatClient.generateStream(prompt)
|
||||
.collectList()
|
||||
.block()
|
||||
.stream()
|
||||
.map(AiResponse::getGenerations)
|
||||
.map(ChatResponse::getGenerations)
|
||||
.flatMap(List::stream)
|
||||
.map(Generation::getContent)
|
||||
.collect(Collectors.joining());
|
||||
|
||||
@@ -21,7 +21,7 @@ import java.time.Duration;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.ai.client.AiResponse;
|
||||
import org.springframework.ai.chat.ChatResponse;
|
||||
import org.springframework.ai.metadata.ChoiceMetadata;
|
||||
import org.springframework.ai.metadata.GenerationMetadata;
|
||||
import org.springframework.ai.metadata.PromptMetadata;
|
||||
@@ -74,7 +74,7 @@ public class OpenAiClientWithGenerationMetadata2Tests {
|
||||
|
||||
Prompt prompt = new Prompt("Reach for the sky.");
|
||||
|
||||
AiResponse response = this.openAiClient.generate(prompt);
|
||||
ChatResponse response = this.openAiClient.generate(prompt);
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ import java.util.Map;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.ai.client.AiClient;
|
||||
import org.springframework.ai.client.AiResponse;
|
||||
import org.springframework.ai.client.AiStreamClient;
|
||||
import org.springframework.ai.chat.ChatClient;
|
||||
import org.springframework.ai.chat.ChatResponse;
|
||||
import org.springframework.ai.chat.StreamingChatClient;
|
||||
import org.springframework.ai.prompt.Prompt;
|
||||
import org.springframework.ai.prompt.PromptTemplate;
|
||||
import org.springframework.ai.prompt.messages.Message;
|
||||
@@ -25,10 +25,10 @@ public abstract class AbstractIT {
|
||||
private static final Logger logger = LoggerFactory.getLogger(AbstractIT.class);
|
||||
|
||||
@Autowired
|
||||
protected AiClient openAiClient;
|
||||
protected ChatClient openAiClient;
|
||||
|
||||
@Autowired
|
||||
protected AiStreamClient openAiStreamClient;
|
||||
protected StreamingChatClient openStreamingChatClient;
|
||||
|
||||
@Value("classpath:/prompts/eval/qa-evaluator-accurate-answer.st")
|
||||
protected Resource qaEvaluatorAccurateAnswerResource;
|
||||
@@ -42,7 +42,7 @@ public abstract class AbstractIT {
|
||||
@Value("classpath:/prompts/eval/user-evaluator-message.st")
|
||||
protected Resource userEvaluatorResource;
|
||||
|
||||
protected void evaluateQuestionAndAnswer(String question, AiResponse response, boolean factBased) {
|
||||
protected void evaluateQuestionAndAnswer(String question, ChatResponse response, boolean factBased) {
|
||||
assertThat(response).isNotNull();
|
||||
String answer = response.getGeneration().getContent();
|
||||
logger.info("Question: " + question);
|
||||
|
||||
@@ -28,8 +28,8 @@ import software.amazon.awssdk.regions.Region;
|
||||
import org.springframework.ai.autoconfigure.bedrock.BedrockAwsConnectionProperties;
|
||||
import org.springframework.ai.bedrock.anthropic.BedrockAnthropicChatClient;
|
||||
import org.springframework.ai.bedrock.anthropic.api.AnthropicChatBedrockApi.AnthropicChatModel;
|
||||
import org.springframework.ai.client.AiResponse;
|
||||
import org.springframework.ai.client.Generation;
|
||||
import org.springframework.ai.chat.ChatResponse;
|
||||
import org.springframework.ai.chat.Generation;
|
||||
import org.springframework.ai.prompt.Prompt;
|
||||
import org.springframework.ai.prompt.SystemPromptTemplate;
|
||||
import org.springframework.ai.prompt.messages.Message;
|
||||
@@ -70,7 +70,7 @@ public class BedrockAnthropicChatAutoConfigurationIT {
|
||||
public void chatCompletion() {
|
||||
contextRunner.run(context -> {
|
||||
BedrockAnthropicChatClient anthropicChatClient = context.getBean(BedrockAnthropicChatClient.class);
|
||||
AiResponse response = anthropicChatClient.generate(new Prompt(List.of(userMessage, systemMessage)));
|
||||
ChatResponse response = anthropicChatClient.generate(new Prompt(List.of(userMessage, systemMessage)));
|
||||
assertThat(response.getGeneration().getContent()).contains("Blackbeard");
|
||||
});
|
||||
}
|
||||
@@ -81,14 +81,14 @@ public class BedrockAnthropicChatAutoConfigurationIT {
|
||||
|
||||
BedrockAnthropicChatClient anthropicChatClient = context.getBean(BedrockAnthropicChatClient.class);
|
||||
|
||||
Flux<AiResponse> response = anthropicChatClient
|
||||
Flux<ChatResponse> response = anthropicChatClient
|
||||
.generateStream(new Prompt(List.of(userMessage, systemMessage)));
|
||||
|
||||
List<AiResponse> responses = response.collectList().block();
|
||||
List<ChatResponse> responses = response.collectList().block();
|
||||
assertThat(responses.size()).isGreaterThan(2);
|
||||
|
||||
String stitchedResponseContent = responses.stream()
|
||||
.map(AiResponse::getGenerations)
|
||||
.map(ChatResponse::getGenerations)
|
||||
.flatMap(List::stream)
|
||||
.map(Generation::getContent)
|
||||
.collect(Collectors.joining());
|
||||
|
||||
@@ -22,6 +22,7 @@ import java.util.stream.Collectors;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
|
||||
import org.springframework.ai.chat.ChatResponse;
|
||||
import reactor.core.publisher.Flux;
|
||||
import software.amazon.awssdk.regions.Region;
|
||||
|
||||
@@ -30,8 +31,7 @@ import org.springframework.ai.bedrock.cohere.BedrockCohereChatClient;
|
||||
import org.springframework.ai.bedrock.cohere.api.CohereChatBedrockApi.CohereChatModel;
|
||||
import org.springframework.ai.bedrock.cohere.api.CohereChatBedrockApi.CohereChatRequest.ReturnLikelihoods;
|
||||
import org.springframework.ai.bedrock.cohere.api.CohereChatBedrockApi.CohereChatRequest.Truncate;
|
||||
import org.springframework.ai.client.AiResponse;
|
||||
import org.springframework.ai.client.Generation;
|
||||
import org.springframework.ai.chat.Generation;
|
||||
import org.springframework.ai.prompt.Prompt;
|
||||
import org.springframework.ai.prompt.SystemPromptTemplate;
|
||||
import org.springframework.ai.prompt.messages.Message;
|
||||
@@ -72,7 +72,7 @@ public class BedrockCohereChatAutoConfigurationIT {
|
||||
public void chatCompletion() {
|
||||
contextRunner.run(context -> {
|
||||
BedrockCohereChatClient cohereChatClient = context.getBean(BedrockCohereChatClient.class);
|
||||
AiResponse response = cohereChatClient.generate(new Prompt(List.of(userMessage, systemMessage)));
|
||||
ChatResponse response = cohereChatClient.generate(new Prompt(List.of(userMessage, systemMessage)));
|
||||
assertThat(response.getGeneration().getContent()).contains("Blackbeard");
|
||||
});
|
||||
}
|
||||
@@ -83,14 +83,14 @@ public class BedrockCohereChatAutoConfigurationIT {
|
||||
|
||||
BedrockCohereChatClient cohereChatClient = context.getBean(BedrockCohereChatClient.class);
|
||||
|
||||
Flux<AiResponse> response = cohereChatClient
|
||||
Flux<ChatResponse> response = cohereChatClient
|
||||
.generateStream(new Prompt(List.of(userMessage, systemMessage)));
|
||||
|
||||
List<AiResponse> responses = response.collectList().block();
|
||||
List<ChatResponse> responses = response.collectList().block();
|
||||
assertThat(responses.size()).isGreaterThan(2);
|
||||
|
||||
String stitchedResponseContent = responses.stream()
|
||||
.map(AiResponse::getGenerations)
|
||||
.map(ChatResponse::getGenerations)
|
||||
.flatMap(List::stream)
|
||||
.map(Generation::getContent)
|
||||
.collect(Collectors.joining());
|
||||
|
||||
@@ -22,14 +22,14 @@ import java.util.stream.Collectors;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
|
||||
import org.springframework.ai.chat.ChatResponse;
|
||||
import reactor.core.publisher.Flux;
|
||||
import software.amazon.awssdk.regions.Region;
|
||||
|
||||
import org.springframework.ai.autoconfigure.bedrock.BedrockAwsConnectionProperties;
|
||||
import org.springframework.ai.bedrock.llama2.BedrockLlama2ChatClient;
|
||||
import org.springframework.ai.bedrock.llama2.api.Llama2ChatBedrockApi.Llama2ChatCompletionModel;
|
||||
import org.springframework.ai.client.AiResponse;
|
||||
import org.springframework.ai.client.Generation;
|
||||
import org.springframework.ai.chat.Generation;
|
||||
import org.springframework.ai.prompt.Prompt;
|
||||
import org.springframework.ai.prompt.SystemPromptTemplate;
|
||||
import org.springframework.ai.prompt.messages.Message;
|
||||
@@ -70,7 +70,7 @@ public class BedrockLlama2ChatAutoConfigurationIT {
|
||||
public void chatCompletion() {
|
||||
contextRunner.run(context -> {
|
||||
BedrockLlama2ChatClient llama2ChatClient = context.getBean(BedrockLlama2ChatClient.class);
|
||||
AiResponse response = llama2ChatClient.generate(new Prompt(List.of(userMessage, systemMessage)));
|
||||
ChatResponse response = llama2ChatClient.generate(new Prompt(List.of(userMessage, systemMessage)));
|
||||
assertThat(response.getGeneration().getContent()).contains("Blackbeard");
|
||||
});
|
||||
}
|
||||
@@ -81,14 +81,14 @@ public class BedrockLlama2ChatAutoConfigurationIT {
|
||||
|
||||
BedrockLlama2ChatClient llama2ChatClient = context.getBean(BedrockLlama2ChatClient.class);
|
||||
|
||||
Flux<AiResponse> response = llama2ChatClient
|
||||
Flux<ChatResponse> response = llama2ChatClient
|
||||
.generateStream(new Prompt(List.of(userMessage, systemMessage)));
|
||||
|
||||
List<AiResponse> responses = response.collectList().block();
|
||||
List<ChatResponse> responses = response.collectList().block();
|
||||
assertThat(responses.size()).isGreaterThan(2);
|
||||
|
||||
String stitchedResponseContent = responses.stream()
|
||||
.map(AiResponse::getGenerations)
|
||||
.map(ChatResponse::getGenerations)
|
||||
.flatMap(List::stream)
|
||||
.map(Generation::getContent)
|
||||
.collect(Collectors.joining());
|
||||
|
||||
@@ -22,14 +22,14 @@ import java.util.stream.Collectors;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
|
||||
import org.springframework.ai.chat.ChatResponse;
|
||||
import reactor.core.publisher.Flux;
|
||||
import software.amazon.awssdk.regions.Region;
|
||||
|
||||
import org.springframework.ai.autoconfigure.bedrock.BedrockAwsConnectionProperties;
|
||||
import org.springframework.ai.bedrock.titan.BedrockTitanChatClient;
|
||||
import org.springframework.ai.bedrock.titan.api.TitanChatBedrockApi.TitanChatModel;
|
||||
import org.springframework.ai.client.AiResponse;
|
||||
import org.springframework.ai.client.Generation;
|
||||
import org.springframework.ai.chat.Generation;
|
||||
import org.springframework.ai.prompt.Prompt;
|
||||
import org.springframework.ai.prompt.SystemPromptTemplate;
|
||||
import org.springframework.ai.prompt.messages.Message;
|
||||
@@ -70,7 +70,7 @@ public class BedrockTitanChatAutoConfigurationIT {
|
||||
public void chatCompletion() {
|
||||
contextRunner.run(context -> {
|
||||
BedrockTitanChatClient chatClient = context.getBean(BedrockTitanChatClient.class);
|
||||
AiResponse response = chatClient.generate(new Prompt(List.of(userMessage, systemMessage)));
|
||||
ChatResponse response = chatClient.generate(new Prompt(List.of(userMessage, systemMessage)));
|
||||
assertThat(response.getGeneration().getContent()).contains("Blackbeard");
|
||||
});
|
||||
}
|
||||
@@ -81,13 +81,13 @@ public class BedrockTitanChatAutoConfigurationIT {
|
||||
|
||||
BedrockTitanChatClient chatClient = context.getBean(BedrockTitanChatClient.class);
|
||||
|
||||
Flux<AiResponse> response = chatClient.generateStream(new Prompt(List.of(userMessage, systemMessage)));
|
||||
Flux<ChatResponse> response = chatClient.generateStream(new Prompt(List.of(userMessage, systemMessage)));
|
||||
|
||||
List<AiResponse> responses = response.collectList().block();
|
||||
List<ChatResponse> responses = response.collectList().block();
|
||||
assertThat(responses.size()).isGreaterThan(1);
|
||||
|
||||
String stitchedResponseContent = responses.stream()
|
||||
.map(AiResponse::getGenerations)
|
||||
.map(ChatResponse::getGenerations)
|
||||
.flatMap(List::stream)
|
||||
.map(Generation::getContent)
|
||||
.collect(Collectors.joining());
|
||||
|
||||
@@ -18,8 +18,8 @@ package org.springframework.ai.evaluation;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.ai.client.AiClient;
|
||||
import org.springframework.ai.client.AiResponse;
|
||||
import org.springframework.ai.chat.ChatClient;
|
||||
import org.springframework.ai.chat.ChatResponse;
|
||||
import org.springframework.ai.prompt.Prompt;
|
||||
import org.springframework.ai.prompt.PromptTemplate;
|
||||
import org.springframework.ai.prompt.messages.Message;
|
||||
@@ -39,7 +39,7 @@ public class BasicEvaluationTest {
|
||||
private static final Logger logger = LoggerFactory.getLogger(BasicEvaluationTest.class);
|
||||
|
||||
@Autowired
|
||||
protected AiClient openAiClient;
|
||||
protected ChatClient openAiClient;
|
||||
|
||||
@Value("classpath:/prompts/spring/test/evaluation/qa-evaluator-accurate-answer.st")
|
||||
protected Resource qaEvaluatorAccurateAnswerResource;
|
||||
@@ -53,7 +53,7 @@ public class BasicEvaluationTest {
|
||||
@Value("classpath:/prompts/spring/test/evaluation/user-evaluator-message.st")
|
||||
protected Resource userEvaluatorResource;
|
||||
|
||||
protected void evaluateQuestionAndAnswer(String question, AiResponse response, boolean factBased) {
|
||||
protected void evaluateQuestionAndAnswer(String question, ChatResponse response, boolean factBased) {
|
||||
assertThat(response).isNotNull();
|
||||
String answer = response.getGeneration().getContent();
|
||||
logger.info("Question: " + question);
|
||||
|
||||
@@ -19,9 +19,9 @@ package org.springframework.ai.vertex.generation;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.ai.client.AiClient;
|
||||
import org.springframework.ai.client.AiResponse;
|
||||
import org.springframework.ai.client.Generation;
|
||||
import org.springframework.ai.chat.ChatClient;
|
||||
import org.springframework.ai.chat.ChatResponse;
|
||||
import org.springframework.ai.chat.Generation;
|
||||
import org.springframework.ai.prompt.Prompt;
|
||||
import org.springframework.ai.prompt.messages.MessageType;
|
||||
import org.springframework.ai.vertex.api.VertexAiApi;
|
||||
@@ -34,7 +34,7 @@ import org.springframework.util.CollectionUtils;
|
||||
/**
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
public class VertexAiChatClient implements AiClient {
|
||||
public class VertexAiChatClient implements ChatClient {
|
||||
|
||||
private final VertexAiApi vertexAiApi;
|
||||
|
||||
@@ -71,7 +71,7 @@ public class VertexAiChatClient implements AiClient {
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiResponse generate(Prompt prompt) {
|
||||
public ChatResponse generate(Prompt prompt) {
|
||||
|
||||
String vertexContext = prompt.getMessages()
|
||||
.stream()
|
||||
@@ -99,7 +99,7 @@ public class VertexAiChatClient implements AiClient {
|
||||
.map(vmsg -> new Generation(vmsg.content()))
|
||||
.toList();
|
||||
|
||||
return new AiResponse(generations);
|
||||
return new ChatResponse(generations);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ import java.util.Map;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
|
||||
|
||||
import org.springframework.ai.client.AiResponse;
|
||||
import org.springframework.ai.client.Generation;
|
||||
import org.springframework.ai.chat.ChatResponse;
|
||||
import org.springframework.ai.chat.Generation;
|
||||
import org.springframework.ai.parser.BeanOutputParser;
|
||||
import org.springframework.ai.parser.ListOutputParser;
|
||||
import org.springframework.ai.parser.MapOutputParser;
|
||||
@@ -47,7 +47,7 @@ class VertexAiChatGenerationClientIT {
|
||||
SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(systemResource);
|
||||
Message systemMessage = systemPromptTemplate.createMessage(Map.of("name", name, "voice", voice));
|
||||
Prompt prompt = new Prompt(List.of(userMessage, systemMessage));
|
||||
AiResponse response = client.generate(prompt);
|
||||
ChatResponse response = client.generate(prompt);
|
||||
assertThat(response.getGeneration().getContent()).contains("Bartholomew");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user