All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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