fi.evolver.ai.spring.provider.perplexity.PerplexityService Maven / Gradle / Ivy
package fi.evolver.ai.spring.provider.perplexity;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublishers;
import java.net.http.HttpRequest.Builder;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.time.Duration;
import java.util.Optional;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.core.JsonProcessingException;
import fi.evolver.ai.spring.ApiResponseException;
import fi.evolver.ai.spring.Model;
import fi.evolver.ai.spring.Tokenizer;
import fi.evolver.ai.spring.chat.ChatApi;
import fi.evolver.ai.spring.chat.ChatResponse;
import fi.evolver.ai.spring.chat.prompt.ChatPrompt;
import fi.evolver.ai.spring.config.ApiConfigurationService;
import fi.evolver.ai.spring.config.ApiEndpointParameters;
import fi.evolver.ai.spring.prompt.Prompt;
import fi.evolver.ai.spring.provider.ConditionalOnProviderConfigured;
import fi.evolver.ai.spring.provider.perplexity.response.chat.PChatResult;
import fi.evolver.ai.spring.util.Json;
import fi.evolver.ai.spring.util.SseUtils;
import fi.evolver.basics.spring.http.LoggingHttpClient;
import fi.evolver.basics.spring.http.SseSubscriber;
import fi.evolver.basics.spring.http.SseSubscriber.SseEvent;
import fi.evolver.basics.spring.http.SseSubscriber.SseEventConsumer;
import fi.evolver.basics.spring.log.MessageLogService;
@Component
@ConditionalOnProviderConfigured(PerplexityService.class)
public class PerplexityService implements ChatApi {
private static final Logger LOG = LoggerFactory.getLogger(PerplexityService.class);
static final Set FINISH_REASONS_OK = Set.of("stop");
public static final Model LLAMA_3_SONAR_HUGE_ONLINE = new Model<>("llama-3.1-sonar-huge-128k-online", 127_072, Tokenizer.CL100K_BASE);
/**
* Use LLAMA_3_1_SONAR_LARGE_ONLINE instead
* @deprecated no longer accessible from 2024-12-08
* @see LLAMA_3_1_SONAR_LARGE_ONLINE
*/
@Deprecated
public static final Model LLAMA_3_SONAR_LARGE_ONLINE = new Model<>("llama-3-sonar-large-32k-online", 127_072, Tokenizer.CL100K_BASE);
/**
* Use LLAMA_3_1_SONAL_SMALL_ONLINE instead
* @deprecated no longer accessible from 2024-12-08
* @see LLAMA_3_1_SONAL_SMALL_ONLINE
*/
@Deprecated
public static final Model LLAMA_3_SONAL_SMALL_ONLINE = new Model<>("llama-3-sonar-small-32k-online", 127_072, Tokenizer.CL100K_BASE);
public static final Model LLAMA_3_1_SONAR_LARGE_ONLINE = new Model<>("llama-3.1-sonar-large-128k-online", 127_072, Tokenizer.CL100K_BASE);
public static final Model LLAMA_3_1_SONAR_SMALL_ONLINE = new Model<>("llama-3.1-sonar-small-128k-online", 127_072, Tokenizer.CL100K_BASE);
private final LoggingHttpClient httpClient;
private final ApiConfigurationService apiConfigurationService;
@Autowired
public PerplexityService(
MessageLogService messageLogService,
ApiConfigurationService apiConfigurationService,
@Value("${evolver.perplexity-service.connection.timeout.seconds:5}") int connectionTimeoutSeconds) {
this.httpClient = new LoggingHttpClient(
messageLogService,
HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(connectionTimeoutSeconds))
.build());
this.apiConfigurationService = apiConfigurationService;
}
private static Optional getProviderName(Prompt prompt) {
return prompt.getStringProperty(ChatApi.PROVIDER);
}
@Override
public ChatResponse send(ChatPrompt prompt) {
String body = PerplexityRequestGenerator.generate(prompt);
ApiEndpointParameters conf = apiConfigurationService.getEndpointParameters(
PerplexityService.class,
getProviderName(prompt),
ChatApi.class,
prompt.model());
Builder builder = HttpRequest.newBuilder(conf.prepareUri())
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.timeout(prompt.timeout().orElse(ChatApi.DEFAULT_TIMEOUT))
.POST(BodyPublishers.ofString(body));
conf.headers().forEach(builder::header);
HttpRequest request = builder.build();
if (prompt.getBooleanProperty(PerplexityRequestParameters.STREAM).orElse(false))
return makeStreamingRequest(httpClient, request, prompt);
else
return makeNonStreamingRequest(httpClient, request, prompt);
}
private PerplexityStreamingChatResponse makeStreamingRequest(LoggingHttpClient client, HttpRequest request, ChatPrompt prompt) {
PerplexityStreamingChatResponse response = new PerplexityStreamingChatResponse(prompt);
HttpResponse.BodyHandler bodyHandler = SseSubscriber
.createBodyHandler(new StreamingCompletionsEventConsumer(response));
client.sendAsync(
request,
bodyHandler,
createLogParameters("ChatRequest"))
.exceptionally(e -> {
response.handleError(e);
return null;
});
return response;
}
private PerplexityChatResponse makeNonStreamingRequest(LoggingHttpClient client, HttpRequest request, ChatPrompt prompt) {
try {
HttpResponse httpResponse = client.send(request, BodyHandlers.ofString(), createLogParameters("ChatRequest"));
if (httpResponse.statusCode() != 200) {
throw new ApiResponseException(
"Failed non-streaming Perplexity chat request. HTTP status %d. Response:\n%s",
httpResponse.statusCode(),
httpResponse.body());
}
PChatResult chatResult = Json.OBJECT_MAPPER.readValue(httpResponse.body(), PChatResult.class);
return new PerplexityChatResponse(prompt, chatResult);
}
catch (IOException | InterruptedException e) {
throw new ApiResponseException(e, "Failed non-streaming Perplexity chat request");
}
}
private static class StreamingCompletionsEventConsumer implements SseEventConsumer {
private final PerplexityStreamingChatResponse response;
public StreamingCompletionsEventConsumer(PerplexityStreamingChatResponse response) {
this.response = response;
}
@Override
public void onEvent(SseEvent event) {
if ("[DONE]".equals(event.data().strip()))
return;
if (!event.data().startsWith("{")) {
LOG.warn("Unknown chunk: {}", event.data());
return;
}
try {
PChatResult result = Json.OBJECT_MAPPER.readValue(event.data(), PChatResult.class);
response.addResult(result);
}
catch (JsonProcessingException e) {
LOG.warn("Bad SSE event", e);
}
}
@Override
public void onError(Throwable throwable) {
response.handleError(throwable);
}
@Override
public void onComplete() {
response.handleStreamEnd();
}
}
@Override
public ChatResponse parseChatResponse(String rawResponse) {
try {
ChatPrompt prompt = ChatPrompt.builder(LLAMA_3_1_SONAR_LARGE_ONLINE).build();
if (SseUtils.isStreamResponse(rawResponse)) {
PerplexityStreamingChatResponse chatResponse = new PerplexityStreamingChatResponse(prompt);
SseUtils.handleStreamContent(rawResponse, res -> chatResponse.addResult(res), PChatResult.class);
chatResponse.handleStreamEnd();
return chatResponse;
}
else {
return new PerplexityChatResponse(prompt, Json.OBJECT_MAPPER.readValue(rawResponse, PChatResult.class));
}
}
catch (JsonProcessingException e) {
throw new UncheckedIOException(e);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy