OpenAi clien improvments
- OpenAiApi's ObjectMapper is configured to DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES = false - Use Java Predicate for the SSE_DONE condition used by the takeUntil and filter for the Flux stream. - Add retryTemplate to OpenAI Chat Client. Make OpenAiApi toleratant to uknown response fields.
This commit is contained in:
committed by
Christian Tzolov
parent
e732018219
commit
b54bb8bc8a
@@ -20,11 +20,13 @@ import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
@@ -51,7 +53,7 @@ public class OpenAiApi {
|
||||
|
||||
private static final String DEFAULT_BASE_URL = "https://api.openai.com";
|
||||
private static final String DEFAULT_EMBEDDING_MODEL = "text-embedding-ada-002";
|
||||
private static final String SSE_DONE = "[DONE]";
|
||||
private static final Predicate<String> SSE_DONE_PREDICATE = "[DONE]"::equals;
|
||||
|
||||
private final RestClient restClient;
|
||||
private final WebClient webClient;
|
||||
@@ -75,7 +77,7 @@ public class OpenAiApi {
|
||||
*/
|
||||
public OpenAiApi(String baseUrl, String openAiToken, RestClient.Builder restClientBuilder) {
|
||||
|
||||
this.objectMapper = new ObjectMapper();
|
||||
this.objectMapper = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
|
||||
Consumer<HttpHeaders> jsonContentHeaders = headers -> {
|
||||
headers.setBearerAuth(openAiToken);
|
||||
@@ -623,10 +625,10 @@ public class OpenAiApi {
|
||||
.body(Mono.just(chatRequest), ChatCompletionRequest.class)
|
||||
.retrieve()
|
||||
.bodyToFlux(String.class)
|
||||
// cancels the flux stream after the SSE_DONE is received.
|
||||
.takeUntil(content -> content.contains(SSE_DONE))
|
||||
// filters out the SSE_DONE message.
|
||||
.filter(content -> !content.contains(SSE_DONE))
|
||||
// cancels the flux stream after the "[DONE]" is received.
|
||||
.takeUntil(SSE_DONE_PREDICATE)
|
||||
// filters out the "[DONE]" message.
|
||||
.filter(SSE_DONE_PREDICATE.negate())
|
||||
.map(content -> parseJson(content, ChatCompletionChunk.class));
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package org.springframework.ai.openai.client;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@@ -32,11 +33,13 @@ import org.springframework.ai.metadata.RateLimit;
|
||||
import org.springframework.ai.openai.api.OpenAiApi;
|
||||
import org.springframework.ai.openai.api.OpenAiApi.ChatCompletion;
|
||||
import org.springframework.ai.openai.api.OpenAiApi.ChatCompletionMessage;
|
||||
import org.springframework.ai.openai.api.OpenAiApi.OpenAiApiException;
|
||||
import org.springframework.ai.openai.metadata.OpenAiGenerationMetadata;
|
||||
import org.springframework.ai.openai.metadata.support.OpenAiResponseHeaderExtractor;
|
||||
import org.springframework.ai.prompt.Prompt;
|
||||
import org.springframework.ai.prompt.messages.Message;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.retry.support.RetryTemplate;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
@@ -60,6 +63,12 @@ public class OpenAiClient implements AiClient, AiStreamClient {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(getClass());
|
||||
|
||||
public final RetryTemplate retryTemplate = RetryTemplate.builder()
|
||||
.maxAttempts(10)
|
||||
.retryOn(OpenAiApiException.class)
|
||||
.exponentialBackoff(Duration.ofMillis(2000), 5, Duration.ofMillis(3 * 60000))
|
||||
.build();
|
||||
|
||||
private final OpenAiApi openAiApi;
|
||||
|
||||
public OpenAiClient(OpenAiApi openAiApi) {
|
||||
@@ -86,81 +95,86 @@ public class OpenAiClient implements AiClient, AiStreamClient {
|
||||
@Override
|
||||
public AiResponse generate(Prompt prompt) {
|
||||
|
||||
List<Message> messages = prompt.getMessages();
|
||||
return this.retryTemplate.execute(ctx -> {
|
||||
List<Message> messages = prompt.getMessages();
|
||||
|
||||
List<ChatCompletionMessage> chatCompletionMessages = messages.stream()
|
||||
.map(m -> new ChatCompletionMessage(m.getContent(),
|
||||
ChatCompletionMessage.Role.valueOf(m.getMessageType().getValue())))
|
||||
.toList();
|
||||
List<ChatCompletionMessage> chatCompletionMessages = messages.stream()
|
||||
.map(m -> new ChatCompletionMessage(m.getContent(),
|
||||
ChatCompletionMessage.Role.valueOf(m.getMessageType().getValue())))
|
||||
.toList();
|
||||
|
||||
ResponseEntity<ChatCompletion> completionEntity = this.openAiApi.chatCompletionEntity(
|
||||
new OpenAiApi.ChatCompletionRequest(chatCompletionMessages, this.model, this.temperature.floatValue()));
|
||||
ResponseEntity<ChatCompletion> completionEntity = this.openAiApi
|
||||
.chatCompletionEntity(new OpenAiApi.ChatCompletionRequest(chatCompletionMessages, this.model,
|
||||
this.temperature.floatValue()));
|
||||
|
||||
var chatCompletion = completionEntity.getBody();
|
||||
if (chatCompletion == null) {
|
||||
logger.warn("No chat completion returned for request: {}", chatCompletionMessages);
|
||||
return new AiResponse(List.of());
|
||||
}
|
||||
var chatCompletion = completionEntity.getBody();
|
||||
if (chatCompletion == null) {
|
||||
logger.warn("No chat completion returned for request: {}", chatCompletionMessages);
|
||||
return new AiResponse(List.of());
|
||||
}
|
||||
|
||||
RateLimit rateLimits = OpenAiResponseHeaderExtractor.extractAiResponseHeaders(completionEntity);
|
||||
RateLimit rateLimits = OpenAiResponseHeaderExtractor.extractAiResponseHeaders(completionEntity);
|
||||
|
||||
List<Generation> generations = chatCompletion.choices().stream().map(choice -> {
|
||||
return new Generation(choice.message().content(), Map.of("role", choice.message().role().name()))
|
||||
.withChoiceMetadata(ChoiceMetadata.from(choice.finishReason().name(), null));
|
||||
}).toList();
|
||||
List<Generation> generations = chatCompletion.choices().stream().map(choice -> {
|
||||
return new Generation(choice.message().content(), Map.of("role", choice.message().role().name()))
|
||||
.withChoiceMetadata(ChoiceMetadata.from(choice.finishReason().name(), null));
|
||||
}).toList();
|
||||
|
||||
return new AiResponse(generations,
|
||||
OpenAiGenerationMetadata.from(completionEntity.getBody()).withRateLimit(rateLimits));
|
||||
return new AiResponse(generations,
|
||||
OpenAiGenerationMetadata.from(completionEntity.getBody()).withRateLimit(rateLimits));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<AiResponse> generateStream(Prompt prompt) {
|
||||
List<Message> messages = prompt.getMessages();
|
||||
return this.retryTemplate.execute(ctx -> {
|
||||
List<Message> messages = prompt.getMessages();
|
||||
|
||||
List<ChatCompletionMessage> chatCompletionMessages = messages.stream()
|
||||
.map(m -> new ChatCompletionMessage(m.getContent(),
|
||||
ChatCompletionMessage.Role.valueOf(m.getMessageType().getValue())))
|
||||
.toList();
|
||||
List<ChatCompletionMessage> chatCompletionMessages = messages.stream()
|
||||
.map(m -> new ChatCompletionMessage(m.getContent(),
|
||||
ChatCompletionMessage.Role.valueOf(m.getMessageType().getValue())))
|
||||
.toList();
|
||||
|
||||
Flux<OpenAiApi.ChatCompletionChunk> completionChunks = this.openAiApi
|
||||
.chatCompletionStream(new OpenAiApi.ChatCompletionRequest(chatCompletionMessages, this.model,
|
||||
this.temperature.floatValue(), true));
|
||||
Flux<OpenAiApi.ChatCompletionChunk> completionChunks = this.openAiApi
|
||||
.chatCompletionStream(new OpenAiApi.ChatCompletionRequest(chatCompletionMessages, this.model,
|
||||
this.temperature.floatValue(), true));
|
||||
|
||||
// For chunked responses, only the first chunk contains the choice role.
|
||||
// The rest of the chunks with same ID share the same role.
|
||||
ConcurrentHashMap<String, String> roleMap = new ConcurrentHashMap<>();
|
||||
// For chunked responses, only the first chunk contains the choice role.
|
||||
// The rest of the chunks with same ID share the same role.
|
||||
ConcurrentHashMap<String, String> roleMap = new ConcurrentHashMap<>();
|
||||
|
||||
// An alternative implementation that returns Flux<Generation> instead of
|
||||
// Flux<AiResponse>.
|
||||
// Flux<Generation> generationFlux = completionChunks.map(chunk -> {
|
||||
// String chunkId = chunk.id();
|
||||
// return chunk.choices().stream()
|
||||
// .map(choice -> {
|
||||
// if (choice.delta().role() != null) {
|
||||
// roleMap.putIfAbsent(chunkId, choice.delta().role().name());
|
||||
// }
|
||||
// return new Generation(choice.delta().content(),
|
||||
// Map.of("role", roleMap.get(chunkId)));
|
||||
// })
|
||||
// .toList();
|
||||
// }).flatMapIterable(generations -> generations);
|
||||
// return generationFlux;
|
||||
// An alternative implementation that returns Flux<Generation> instead of
|
||||
// Flux<AiResponse>.
|
||||
// Flux<Generation> generationFlux = completionChunks.map(chunk -> {
|
||||
// String chunkId = chunk.id();
|
||||
// return chunk.choices().stream()
|
||||
// .map(choice -> {
|
||||
// if (choice.delta().role() != null) {
|
||||
// roleMap.putIfAbsent(chunkId, choice.delta().role().name());
|
||||
// }
|
||||
// return new Generation(choice.delta().content(),
|
||||
// Map.of("role", roleMap.get(chunkId)));
|
||||
// })
|
||||
// .toList();
|
||||
// }).flatMapIterable(generations -> generations);
|
||||
// return generationFlux;
|
||||
|
||||
return completionChunks.map(chunk -> {
|
||||
String chunkId = chunk.id();
|
||||
List<Generation> generations = chunk.choices().stream().map(choice -> {
|
||||
if (choice.delta().role() != null) {
|
||||
roleMap.putIfAbsent(chunkId, choice.delta().role().name());
|
||||
}
|
||||
var generation = new Generation(choice.delta().content(), Map.of("role", roleMap.get(chunkId)));
|
||||
if (choice.finishReason() != null) {
|
||||
generation = generation.withChoiceMetadata(ChoiceMetadata.from(choice.finishReason().name(), null));
|
||||
}
|
||||
return generation;
|
||||
}).toList();
|
||||
return new AiResponse(generations);
|
||||
return completionChunks.map(chunk -> {
|
||||
String chunkId = chunk.id();
|
||||
List<Generation> generations = chunk.choices().stream().map(choice -> {
|
||||
if (choice.delta().role() != null) {
|
||||
roleMap.putIfAbsent(chunkId, choice.delta().role().name());
|
||||
}
|
||||
var generation = new Generation(choice.delta().content(), Map.of("role", roleMap.get(chunkId)));
|
||||
if (choice.finishReason() != null) {
|
||||
generation = generation
|
||||
.withChoiceMetadata(ChoiceMetadata.from(choice.finishReason().name(), null));
|
||||
}
|
||||
return generation;
|
||||
}).toList();
|
||||
return new AiResponse(generations);
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user