Rename request call() to prompt() and the response collect() to call(). Adjust tests

This commit is contained in:
Christian Tzolov
2024-05-21 17:18:42 +02:00
parent 9db0ea7775
commit d5c7e19e95
6 changed files with 217 additions and 143 deletions

View File

@@ -62,13 +62,12 @@ class OpenAiChatClientIT extends AbstractIT {
@Test
void roleTest() {
ChatResponse response = ChatClient.builder(modelCaller)
.build()
.call()
.system(s -> s.text(systemTextResource).param("name", "Bob").param("voice", "pirate"))
.user(u -> u.text("Tell me about 3 famous pirates from the Golden Age of Piracy and what they did"))
.collect()
.chatResponse();
ChatResponse response = ChatClient.builder(modelCaller).build().prompt()
.system(s -> s.text(systemTextResource)
.param("name", "Bob")
.param("voice", "pirate"))
.user("Tell me about 3 famous pirates from the Golden Age of Piracy and what they did")
.call().chatResponse();
System.out.println(response);
// UserMessage userMessage = new UserMessage(
@@ -89,12 +88,10 @@ class OpenAiChatClientIT extends AbstractIT {
void listOutputConverter() {
// TODO: there is a problem here.
Collection<String> list = ChatClient.builder(modelCaller)
.build()
.call()
.user(u -> u.text("List five {subject}").param("subject", "ice cream flavors"))
.collect()
.list(String.class);
Collection<String> list = ChatClient.builder(modelCaller).build().prompt()
.user(u -> u.text("List five {subject}")
.param("subject", "ice cream flavors"))
.call().list(String.class);
// DefaultConversionService conversionService = new DefaultConversionService();
// ListOutputConverter outputConverter = new
@@ -119,14 +116,11 @@ class OpenAiChatClientIT extends AbstractIT {
@Test
void mapOutputConverter() {
Map<String, Object> result = ChatClient.builder(modelCaller)
.build()
.call()
.user(u -> u.text("Provide me a List of {subject}")
.param("subject", "an array of numbers from 1 to 9 under they key name 'numbers'"))
.collect()
.single(new ParameterizedTypeReference<Map<String, Object>>() {
});
Map<String, Object> result = ChatClient.builder(modelCaller).build().prompt()
.user(u -> u.text("Provide me a List of {subject}")
.param("subject", "an array of numbers from 1 to 9 under they key name 'numbers'"))
.call().single(new ParameterizedTypeReference<Map<String, Object>>() {
});
// MapOutputConverter outputConverter = new MapOutputConverter();
@@ -149,12 +143,10 @@ class OpenAiChatClientIT extends AbstractIT {
@Test
void beanOutputConverter() {
ActorsFilms actorsFilms = ChatClient.builder(modelCaller)
.build()
.call()
.user(u -> u.text("Generate the filmography for a random actor."))
.collect()
.single(ActorsFilms.class);
ActorsFilms actorsFilms = ChatClient.builder(modelCaller).build().prompt()
.user("Generate the filmography for a random actor.")
.call()
.single(ActorsFilms.class);
// BeanOutputConverter<ActorsFilms> outputConverter = new
// BeanOutputConverter<>(ActorsFilms.class);
@@ -181,12 +173,10 @@ class OpenAiChatClientIT extends AbstractIT {
@Test
void beanOutputConverterRecords() {
ActorsFilmsRecord actorsFilms = ChatClient.builder(modelCaller)
.build()
.call()
.user(u -> u.text("Generate the filmography of 5 movies for Tom Hanks."))
.collect()
.single(ActorsFilmsRecord.class);
ActorsFilmsRecord actorsFilms = ChatClient.builder(modelCaller).build().prompt()
.user("Generate the filmography of 5 movies for Tom Hanks.")
.call()
.single(ActorsFilmsRecord.class);
// BeanOutputConverter<ActorsFilmsRecord> outputConverter = new
// BeanOutputConverter<>(ActorsFilmsRecord.class);
@@ -213,22 +203,20 @@ class OpenAiChatClientIT extends AbstractIT {
BeanOutputConverter<ActorsFilmsRecord> outputConverter = new BeanOutputConverter<>(ActorsFilmsRecord.class);
Flux<ChatResponse> chatResponse = ChatClient.builder(modelCaller)
.build()
.call()
.user(u -> u
.text("Generate the filmography of 5 movies for Tom Hanks. " + System.lineSeparator() + "{format}")
.param("format", outputConverter.getFormat()))
.stream()
.chatResponse();
Flux<String> chatResponse = ChatClient.builder(modelCaller)
.build()
.prompt()
.user(u -> u
.text("Generate the filmography of 5 movies for Tom Hanks. " + System.lineSeparator()
+ "{format}")
.param("format", outputConverter.getFormat()))
.stream()
.content();
String generationTextFromStream = chatResponse.collectList()
.block()
.stream()
.map(ChatResponse::getResult)
.map(Generation::getOutput)
.map(AssistantMessage::getContent)
.collect(Collectors.joining());
.block()
.stream()
.collect(Collectors.joining());
// String generationTextFromStream = chatResponse.collectList()
// .block()
@@ -266,13 +254,11 @@ class OpenAiChatClientIT extends AbstractIT {
@Test
void functionCallTest() {
ChatResponse response = ChatClient.builder(modelCaller)
.build()
.call()
.user(u -> u.text("What's the weather like in San Francisco, Tokyo, and Paris?"))
.function("getCurrentWeather", "Get the weather in location", new MockWeatherService())
.collect()
.chatResponse();
String response = ChatClient.builder(modelCaller).build().prompt()
.user(u -> u.text("What's the weather like in San Francisco, Tokyo, and Paris?"))
.function("getCurrentWeather", "Get the weather in location", new MockWeatherService())
.call()
.content();
// UserMessage userMessage = new UserMessage("What's the weather like in San
// Francisco, Tokyo, and Paris?");
@@ -293,21 +279,19 @@ class OpenAiChatClientIT extends AbstractIT {
logger.info("Response: {}", response);
assertThat(response.getResult().getOutput().getContent()).containsAnyOf("30.0", "30");
assertThat(response.getResult().getOutput().getContent()).containsAnyOf("10.0", "10");
assertThat(response.getResult().getOutput().getContent()).containsAnyOf("15.0", "15");
assertThat(response).containsAnyOf("30.0", "30");
assertThat(response).containsAnyOf("10.0", "10");
assertThat(response).containsAnyOf("15.0", "15");
}
@Test
void streamFunctionCallTest() {
Flux<ChatResponse> response = ChatClient.builder(modelCaller)
.build()
.call()
.user(u -> u.text("What's the weather like in San Francisco, Tokyo, and Paris?"))
.function("getCurrentWeather", "Get the weather in location", new MockWeatherService())
.stream()
.chatResponse();
Flux<String> response = ChatClient.builder(modelCaller).build().prompt()
.user("What's the weather like in San Francisco, Tokyo, and Paris?")
.function("getCurrentWeather", "Get the weather in location", new MockWeatherService())
.stream()
.content();
// UserMessage userMessage = new UserMessage("What's the weather like in San
// Francisco, Tokyo, and Paris?");
@@ -328,13 +312,9 @@ class OpenAiChatClientIT extends AbstractIT {
// promptOptions));
String content = response.collectList()
.block()
.stream()
.map(ChatResponse::getResults)
.flatMap(List::stream)
.map(Generation::getOutput)
.map(AssistantMessage::getContent)
.collect(Collectors.joining());
.block()
.stream()
.collect(Collectors.joining());
logger.info("Response: {}", content);
assertThat(content).containsAnyOf("30.0", "30");
@@ -346,16 +326,14 @@ class OpenAiChatClientIT extends AbstractIT {
@ValueSource(strings = { "gpt-4-vision-preview", "gpt-4o" })
void multiModalityEmbeddedImage(String modelName) throws IOException {
ChatResponse response = ChatClient.builder(modelCaller)
.build()
.call()
// TODO consider adding model(...) method to ChatClient as a shortcut to
// OpenAiChatOptions.builder().withModel(modelName).build()
.options(OpenAiChatOptions.builder().withModel(modelName).build())
.user(u -> u.text("Explain what do you see on this picture?")
.media(MimeTypeUtils.IMAGE_PNG, new ClassPathResource("/test.png")))
.collect()
.chatResponse();
String response = ChatClient.builder(modelCaller).build().prompt()
// TODO consider adding model(...) method to ChatClient as a shortcut to
// OpenAiChatOptions.builder().withModel(modelName).build()
.options(OpenAiChatOptions.builder().withModel(modelName).build())
.user(u -> u.text("Explain what do you see on this picture?")
.media(MimeTypeUtils.IMAGE_PNG, new ClassPathResource("/test.png")))
.call()
.content();
// var imageData = new ClassPathResource("/test.png");
@@ -366,9 +344,9 @@ class OpenAiChatClientIT extends AbstractIT {
// .call(new Prompt(List.of(userMessage),
// OpenAiChatOptions.builder().withModel(modelName).build()));
logger.info(response.getResult().getOutput().getContent());
assertThat(response.getResult().getOutput().getContent()).contains("bananas", "apple");
assertThat(response.getResult().getOutput().getContent()).containsAnyOf("bowl", "basket");
logger.info(response);
assertThat(response).contains("bananas", "apple");
assertThat(response).containsAnyOf("bowl", "basket");
}
@ParameterizedTest(name = "{0} : {displayName} ")
@@ -378,15 +356,15 @@ class OpenAiChatClientIT extends AbstractIT {
// TODO: add url method that wrapps the checked exception.
URL url = new URL("https://docs.spring.io/spring-ai/reference/1.0-SNAPSHOT/_images/multimodal.test.png");
ChatResponse response = ChatClient.builder(modelCaller)
.build()
.call()
// TODO consider adding model(...) method to ChatClient as a shortcut to
// OpenAiChatOptions.builder().withModel(modelName).build()
.options(OpenAiChatOptions.builder().withModel(modelName).build())
.user(u -> u.text("Explain what do you see on this picture?").media(MimeTypeUtils.IMAGE_PNG, url))
.collect()
.chatResponse();
String response = ChatClient.builder(modelCaller)
.build()
.prompt()
// TODO consider adding model(...) method to ChatClient as a shortcut to
// OpenAiChatOptions.builder().withModel(modelName).build()
.options(OpenAiChatOptions.builder().withModel(modelName).build())
.user(u -> u.text("Explain what do you see on this picture?").media(MimeTypeUtils.IMAGE_PNG, url))
.call()
.content();
// var userMessage = new UserMessage("Explain what do you see on this picture?",
// List
@@ -398,9 +376,9 @@ class OpenAiChatClientIT extends AbstractIT {
// .call(new Prompt(List.of(userMessage),
// OpenAiChatOptions.builder().withModel(modelName).build()));
logger.info(response.getResult().getOutput().getContent());
assertThat(response.getResult().getOutput().getContent()).contains("bananas", "apple");
assertThat(response.getResult().getOutput().getContent()).containsAnyOf("bowl", "basket");
logger.info(response);
assertThat(response).contains("bananas", "apple");
assertThat(response).containsAnyOf("bowl", "basket");
}
@Test
@@ -409,19 +387,16 @@ class OpenAiChatClientIT extends AbstractIT {
// TODO: add url method that wrapps the checked exception.
URL url = new URL("https://docs.spring.io/spring-ai/reference/1.0-SNAPSHOT/_images/multimodal.test.png");
Flux<ChatResponse> response = ChatClient.builder(modelCaller)
.build()
.call()
// TODO consider adding model(...) method to ChatClient as a shortcut to
// OpenAiChatOptions.builder().withModel(modelName).build()
.options(OpenAiChatOptions.builder().withModel(OpenAiApi.ChatModel.GPT_4_VISION_PREVIEW.getValue()).build())
.user(u -> u.text("Explain what do you see on this picture?").media(MimeTypeUtils.IMAGE_PNG, url))
.stream()
.chatResponse();
Flux<String> response = ChatClient.builder(modelCaller).build().prompt()
.options(OpenAiChatOptions.builder().withModel(OpenAiApi.ChatModel.GPT_4_VISION_PREVIEW.getValue())
.build())
.user(u -> u.text("Explain what do you see on this picture?")
.media(MimeTypeUtils.IMAGE_PNG, url))
.stream()
.content();
// var userMessage = new UserMessage("Explain what do you see on this picture?",
// List
// .of(new Media(MimeTypeUtils.IMAGE_PNG,
// List.of(new Media(MimeTypeUtils.IMAGE_PNG,
// new
// URL("https://docs.spring.io/spring-ai/reference/1.0-SNAPSHOT/_images/multimodal.test.png"))));
@@ -429,14 +404,8 @@ class OpenAiChatClientIT extends AbstractIT {
// Prompt(List.of(userMessage),
// OpenAiChatOptions.builder().withModel(OpenAiApi.ChatModel.GPT_4_VISION_PREVIEW.getValue()).build()));
String content = response.collectList()
.block()
.stream()
.map(ChatResponse::getResults)
.flatMap(List::stream)
.map(Generation::getOutput)
.map(AssistantMessage::getContent)
.collect(Collectors.joining());
String content = response.collectList().block().stream().collect(Collectors.joining());
logger.info("Response: {}", content);
assertThat(content).contains("bananas", "apple");
assertThat(content).containsAnyOf("bowl", "basket");

View File

@@ -64,7 +64,7 @@ public interface ChatClient {
ChatResponse call(Prompt prompt);
ChatClientRequest call();
ChatClientRequest prompt();
interface PromptSpec<T> {
@@ -268,18 +268,18 @@ public interface ChatClient {
return this;
}
public static class CollectResponseSpec {
public static class CallResponseSpec {
private final ChatClientRequest request;
private final ChatCaller modelCaller;
public CollectResponseSpec(ChatCaller modelCaller, ChatClientRequest request) {
public CallResponseSpec(ChatCaller modelCaller, ChatClientRequest request) {
this.modelCaller = modelCaller;
this.request = request;
}
public <T> T single(ParameterizedTypeReference<T> t) {
public <T> T single(ParameterizedTypeReference<T> type) {
return doSingleWithBeanOutputConverter(new BeanOutputConverter<T>(new ParameterizedTypeReference<>() {
}));
}
@@ -292,9 +292,9 @@ public interface ChatClient {
return boc.convert(stringResponse);
}
public <T> T single(Class<T> clzz) {
Assert.notNull(clzz, "the class must be non-null");
var boc = new BeanOutputConverter<T>(clzz);
public <T> T single(Class<T> type) {
Assert.notNull(type, "the class must be non-null");
var boc = new BeanOutputConverter<T>(type);
return doSingleWithBeanOutputConverter(boc);
}
@@ -462,9 +462,6 @@ public interface ChatClient {
public Flux<String> content() {
return doGetFluxChatResponse(this.request.userText)
// .map(ChatResponse::getResult)
// .map(Generation::getOutput)
// .map(AssistantMessage::getContent);
.map(r -> {
if (r.getResult() == null || r.getResult().getOutput() == null
|| r.getResult().getOutput().getContent() == null) {
@@ -487,8 +484,8 @@ public interface ChatClient {
}
public CollectResponseSpec collect() {
return new CollectResponseSpec(this.caller, this);
public CallResponseSpec call() {
return new CallResponseSpec(this.caller, this);
}
public StreamResponseSpec stream() {

View File

@@ -20,12 +20,12 @@ class DefaultChatClient implements ChatClient {
}
@Override
public ChatClientRequest call() {
public ChatClientRequest prompt() {
return new ChatClientRequest(this.defaultChatClientRequest);
}
/**
* use the new fluid DSL starting in {@link #call()}
* use the new fluid DSL starting in {@link #prompt()}
* @param prompt the {@link Prompt prompt} object
* @return a {@link ChatResponse chat response}
*/

View File

@@ -46,9 +46,9 @@ public class Main {
.defaultFunctions("function1")
.build();
String response = client.call()
String response = client.prompt()
.user(u -> u.text("User text {music}").param("music", "Rock").media(MimeTypeUtils.IMAGE_PNG, url))
.collect()
.call()
.single(String.class);
}

View File

@@ -0,0 +1,115 @@
/*
* Copyright 2023 - 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.autoconfigure.openai.tool;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration;
import org.springframework.ai.autoconfigure.retry.SpringAiRetryAutoConfiguration;
import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.openai.OpenAiModelCaller;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import static org.assertj.core.api.Assertions.assertThat;
@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".*")
public class FunctionCallbackInPrompt2IT {
private final Logger logger = LoggerFactory.getLogger(FunctionCallbackInPromptIT.class);
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withPropertyValues("spring.ai.openai.apiKey=" + System.getenv("OPENAI_API_KEY"))
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, OpenAiAutoConfiguration.class));
@Test
void functionCallTest() {
contextRunner.withPropertyValues("spring.ai.openai.chat.options.model=gpt-4-turbo-preview").run(context -> {
OpenAiModelCaller caller = context.getBean(OpenAiModelCaller.class);
ChatClient chatClient = ChatClient.builder(caller).build();
chatClient.prompt()
.user("Tell me a joke?")
.call().content();
String content = ChatClient.builder(caller).build().prompt()
.user("What's the weather like in San Francisco, Tokyo, and Paris?")
.function("CurrentWeatherService", "Get the weather in location", new MockWeatherService())
.call().content();
logger.info("Response: {}", content);
assertThat(content).containsAnyOf("30.0", "30");
assertThat(content).containsAnyOf("10.0", "10");
assertThat(content).containsAnyOf("15.0", "15");
});
}
@Test
void functionCallTest2() {
contextRunner.withPropertyValues("spring.ai.openai.chat.options.model=gpt-4-turbo-preview").run(context -> {
OpenAiModelCaller caller = context.getBean(OpenAiModelCaller.class);
String content = ChatClient.builder(caller).build().prompt()
.user("What's the weather like in Amsterdam?")
.function("CurrentWeatherService", "Get the weather in location",
new Function<MockWeatherService.Request, String>() {
@Override
public String apply(MockWeatherService.Request request) {
return "18 degrees Celsius";
}
})
.call().content();
logger.info("Response: {}", content);
assertThat(content).contains("18");
});
}
@Test
void streamingFunctionCallTest() {
contextRunner.withPropertyValues("spring.ai.openai.chat.options.model=gpt-4-turbo-preview").run(context -> {
OpenAiModelCaller caller = context.getBean(OpenAiModelCaller.class);
String content = ChatClient.builder(caller).build().prompt()
.user("What's the weather like in San Francisco, Tokyo, and Paris?")
.function("CurrentWeatherService", "Get the weather in location", new MockWeatherService())
.stream().content()
.collectList().block().stream().collect(Collectors.joining());
logger.info("Response: {}", content);
assertThat(content).containsAnyOf("30.0", "30");
assertThat(content).containsAnyOf("10.0", "10");
assertThat(content).containsAnyOf("15.0", "15");
});
}
}

View File

@@ -58,10 +58,9 @@ public class FunctionCallbackWrapper2IT {
.defaultUser(u -> u.text("What's the weather like in {cities}?"))
.build();
String content = chatClient.call()
String content = chatClient.prompt()
.user(u -> u.param("cities", "San Francisco, Tokyo, Paris"))
.collect()
.content();
.call().content();
logger.info("Response: {}", content);
@@ -77,17 +76,11 @@ public class FunctionCallbackWrapper2IT {
OpenAiModelCaller caller = context.getBean(OpenAiModelCaller.class);
String content = ChatClient.builder(caller)
.build()
.call()
String content = ChatClient.builder(caller).build().prompt()
.functions("WeatherInfo")
.user(u -> u.text("What's the weather like in San Francisco, Tokyo, and Paris?"))
.stream()
.content()
.collectList()
.block()
.stream()
.collect(Collectors.joining());
.stream().content()
.collectList().block().stream().collect(Collectors.joining());
logger.info("Response: {}", content);