From 19bdaa1ab9febd34ea2d6819bced54fdef665b98 Mon Sep 17 00:00:00 2001 From: Mark Pollack Date: Thu, 16 May 2024 09:55:50 +0200 Subject: [PATCH] WIP --- .../openai/chat/OpenAiChatClientSpecIT.java | 86 ++++++++++ .../chat/OpenAiChatClientSpecLambdaIT.java | 86 ++++++++++ .../springframework/ai/chat/ChatClient.java | 5 +- .../ai/chat/DefaultChatSpec.java | 159 ++++++++++++++++++ .../ai/chat/DefaultChatSpecLambda.java | 157 +++++++++++++++++ .../ai/chat/FluentChatClient.java | 61 +++++++ .../ai/chat/FluentChatClientLambda.java | 56 ++++++ 7 files changed, 609 insertions(+), 1 deletion(-) create mode 100644 models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatClientSpecIT.java create mode 100644 models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatClientSpecLambdaIT.java create mode 100644 spring-ai-core/src/main/java/org/springframework/ai/chat/DefaultChatSpec.java create mode 100644 spring-ai-core/src/main/java/org/springframework/ai/chat/DefaultChatSpecLambda.java create mode 100644 spring-ai-core/src/main/java/org/springframework/ai/chat/FluentChatClient.java create mode 100644 spring-ai-core/src/main/java/org/springframework/ai/chat/FluentChatClientLambda.java diff --git a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatClientSpecIT.java b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatClientSpecIT.java new file mode 100644 index 000000000..91e318af6 --- /dev/null +++ b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatClientSpecIT.java @@ -0,0 +1,86 @@ +package org.springframework.ai.openai.chat; + +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.chat.ChatClient; +import org.springframework.ai.chat.FluentChatClient; +import org.springframework.ai.openai.OpenAiTestConfiguration; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.util.MimeType; +import org.springframework.util.MimeTypeUtils; + +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(classes = OpenAiTestConfiguration.class) +@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+") +public class OpenAiChatClientSpecIT { + + + @Autowired + private ChatClient chatClient; + private static final Logger logger = LoggerFactory.getLogger(OpenAiChatClientSpecIT.class); + + + @Test + void simpleTest() { + FluentChatClient fluentChatClient = new FluentChatClient(chatClient); + + ActorsFilmsRecord actorsFilms = fluentChatClient.chat() + .user() + .text("Generate the filmography of 5 movies for {actor}.") + .param("actor", "Tom Hanks") + .and() + .and() + .execute(); + + +// ActorsFilmsRecord actorsFilms = new FluentChatClient().chat( +// chatSpec -> chatSpec +// .user(userSpec -> userSpec.text("Generate the filmography of 5 movies for {actor}.", +// textSpec -> textSpec.param("actor", "Tom Hanks")) +// ) +// .execute()); + +// ActorsFilmsRecord actorsFilms = new FluentChatClient().chat(chatSpec -> +// chatSpec.user(userSpec -> +// userSpec.text("Generate the filmography of 5 movies for {actor}.", +// textSpec -> textSpec.param("actor", "Tom Hanks") +// ) +// ) +// ).execute(); + + + +// ActorsFilmsRecord actorsFilms = fluentChatClient.chat() +// .user() +// .text("Generate the filmography of 5 movies for {actor}.") +// .param("actor", "Tom Hanks") +// .and() +// .and() +// .execute(); +// +// +// +// ActorsFilmsRecord actorsFilms = new FluentChatClient().chat() +// .user(userSpec -> userSpec +// .text("Generate the filmography of 5 movies for {actor}.", +// textSpec -> textSpec.param("actor", "Tom Hanks").params(Map.of()) ) }) +// .media(mediaSpec -> { +// mediaSpec.param(myImage, MimeTypeUtils.IMAGE_PNG); +// }) +// ) +// .execute(); +// +// System.out.println(actorsFilms); + + } + + record ActorsFilmsRecord(String actor, List movies) { + } +} diff --git a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatClientSpecLambdaIT.java b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatClientSpecLambdaIT.java new file mode 100644 index 000000000..840f899fb --- /dev/null +++ b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatClientSpecLambdaIT.java @@ -0,0 +1,86 @@ +package org.springframework.ai.openai.chat; + +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.chat.ChatClient; +import org.springframework.ai.chat.FluentChatClientLambda; +import org.springframework.ai.openai.OpenAiTestConfiguration; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.List; + +@SpringBootTest(classes = OpenAiTestConfiguration.class) +@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+") +public class OpenAiChatClientSpecLambdaIT { + + + @Autowired + private ChatClient chatClient; + private static final Logger logger = LoggerFactory.getLogger(OpenAiChatClientSpecLambdaIT.class); + + + @Test + void simpleTest() { + FluentChatClientLambda fluentChatClient = new FluentChatClientLambda(chatClient); + + ActorsFilmsRecord2 actorsFilms = + fluentChatClient.chat(chatSpec -> + chatSpec.user(chatUserSpec -> + chatUserSpec.text("Generate the filmograph of 5 movies for {actor}", + chatParamSpec -> chatParamSpec.param("actor", "Tom"))) + +// .user() +// .text("Generate the filmography of 5 movies for {actor}.") +// .param("actor", "Tom Hanks") +// .and() +// .and() +// .execute(); + + +// ActorsFilmsRecord actorsFilms = new FluentChatClient().chat( +// chatSpec -> chatSpec +// .user(userSpec -> userSpec.text("Generate the filmography of 5 movies for {actor}.", +// textSpec -> textSpec.param("actor", "Tom Hanks")) +// ) +// .execute()); + +// ActorsFilmsRecord actorsFilms = new FluentChatClient().chat(chatSpec -> +// chatSpec.user(userSpec -> +// userSpec.text("Generate the filmography of 5 movies for {actor}.", +// textSpec -> textSpec.param("actor", "Tom Hanks") +// ) +// ) +// ).execute(); + + + +// ActorsFilmsRecord actorsFilms = fluentChatClient.chat() +// .user() +// .text("Generate the filmography of 5 movies for {actor}.") +// .param("actor", "Tom Hanks") +// .and() +// .and() +// .execute(); +// +// +// +// ActorsFilmsRecord actorsFilms = new FluentChatClient().chat() +// .user(userSpec -> userSpec +// .text("Generate the filmography of 5 movies for {actor}.", +// textSpec -> textSpec.param("actor", "Tom Hanks").params(Map.of()) ) }) +// .media(mediaSpec -> { +// mediaSpec.param(myImage, MimeTypeUtils.IMAGE_PNG); +// }) +// ) +// .execute(); +// +// System.out.println(actorsFilms); + +// } + + record ActorsFilmsRecord2(String actor, List movies) { + } +} diff --git a/spring-ai-core/src/main/java/org/springframework/ai/chat/ChatClient.java b/spring-ai-core/src/main/java/org/springframework/ai/chat/ChatClient.java index cff4f8674..3ba4b1e63 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/chat/ChatClient.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/chat/ChatClient.java @@ -15,15 +15,18 @@ */ package org.springframework.ai.chat; +import org.springframework.ai.chat.messages.Media; import org.springframework.ai.chat.prompt.Prompt; import java.util.Arrays; +import java.util.List; +import java.util.Map; import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.model.ModelClient; +import org.springframework.util.MimeType; -@FunctionalInterface public interface ChatClient extends ModelClient { default String call(String message) { diff --git a/spring-ai-core/src/main/java/org/springframework/ai/chat/DefaultChatSpec.java b/spring-ai-core/src/main/java/org/springframework/ai/chat/DefaultChatSpec.java new file mode 100644 index 000000000..75e1ed50a --- /dev/null +++ b/spring-ai-core/src/main/java/org/springframework/ai/chat/DefaultChatSpec.java @@ -0,0 +1,159 @@ +package org.springframework.ai.chat; + +import org.springframework.ai.chat.messages.Media; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.chat.prompt.PromptTemplate; +import org.springframework.ai.converter.BeanOutputConverter; +import org.springframework.util.Assert; +import org.springframework.util.MimeType; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class DefaultChatSpec implements FluentChatClient.ChatSpec { + + private String userText; + + private ChatClient chatClient; + + private Map textParameters = new HashMap<>(); + + public DefaultChatSpec(ChatClient chatClient) { + this.chatClient = chatClient; + } + + @Override + public FluentChatClient.ChatUserSpec user() { + return new DefaultChatUserSpec(this); + } + + public void addUser(String userText, Map textParameters) { + this.userText = userText; + this.textParameters.putAll(textParameters); + } + + public T execute(T... varargsOfT) { + + Assert.state(varargsOfT.length == 0, "you should not provide any values for T!"); + Assert.state(varargsOfT.getClass().isArray(), "this needs to be an array parameter"); + Class componentClass = varargsOfT.getClass().getComponentType(); + + List messageList = new ArrayList<>(); + String userTextToUse = userText + System.lineSeparator() + "{format}"; + var converter = new BeanOutputConverter(componentClass); + + this.textParameters.put("format", converter.getFormat()); + doCreateUserMessage(messageList, userTextToUse, this.textParameters); + + Prompt prompt = new Prompt(messageList); + ChatResponse chatResponse = this.chatClient.call(prompt); + String stringResponse = chatResponse.getResult().getOutput().getContent(); + + T convertedResponse = (T) converter.convert(stringResponse); + return convertedResponse; + } + + private void doCreateUserMessage(List messageList, String userText, Map textParameters) { + PromptTemplate userPromptTemplate = new PromptTemplate(userText); + messageList.add(userPromptTemplate.createMessage(textParameters)); + + } + + private class DefaultChatUserSpec implements FluentChatClient.ChatUserSpec { + + private DefaultChatSpec chatSpec; + + private String text; + + private Map textParameters = new HashMap<>(); + + public DefaultChatUserSpec(DefaultChatSpec chatSpec) { + this.chatSpec = chatSpec; + } + + @Override + public FluentChatClient.ChatParamSpec text(String text) { + this.text = text; + return new DefaultChatParamSpec(this); + } + + @Override + public FluentChatClient.ChatMediaParamSpec media() { + return new DefaultChatMediaParamSpec(this); + } + + public void addTextParams(Map textParameters) { + this.textParameters.putAll(textParameters); + } + + public FluentChatClient.ChatSpec and() { + this.chatSpec.addUser(this.text, this.textParameters); + return this.chatSpec; + } + + } + + private class DefaultChatMediaParamSpec implements FluentChatClient.ChatMediaParamSpec { + + private final DefaultChatUserSpec chatUserSpec; + + public DefaultChatMediaParamSpec(DefaultChatUserSpec chatUserSpec) { + this.chatUserSpec = chatUserSpec; + } + + @Override + public FluentChatClient.ChatMediaParamSpec param(List mediaList) { + return null; + } + + @Override + public FluentChatClient.ChatMediaParamSpec param(Media media) { + return null; + } + + @Override + public FluentChatClient.ChatMediaParamSpec param(MimeType mimeType, Object data) { + return null; + } + + @Override + public FluentChatClient.ChatUserSpec and() { + return null; + } + + } + + private class DefaultChatParamSpec implements FluentChatClient.ChatParamSpec { + + private final DefaultChatUserSpec chatUserSpec; + + private Map parameters = new HashMap<>(); + + public DefaultChatParamSpec(DefaultChatUserSpec chatUserSpec) { + this.chatUserSpec = chatUserSpec; + } + + @Override + public FluentChatClient.ChatParamSpec param(String name, Object value) { + this.parameters.put(name, value); + return this; + } + + @Override + public FluentChatClient.ChatParamSpec params(Map paramMap) { + this.parameters.putAll(paramMap); + return this; + } + + @Override + public FluentChatClient.ChatUserSpec and() { + chatUserSpec.addTextParams(this.parameters); + return this.chatUserSpec; + } + + } + +} diff --git a/spring-ai-core/src/main/java/org/springframework/ai/chat/DefaultChatSpecLambda.java b/spring-ai-core/src/main/java/org/springframework/ai/chat/DefaultChatSpecLambda.java new file mode 100644 index 000000000..96c04bb0e --- /dev/null +++ b/spring-ai-core/src/main/java/org/springframework/ai/chat/DefaultChatSpecLambda.java @@ -0,0 +1,157 @@ +package org.springframework.ai.chat; + +import org.springframework.ai.chat.messages.Media; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.chat.prompt.PromptTemplate; +import org.springframework.ai.converter.BeanOutputConverter; +import org.springframework.util.Assert; +import org.springframework.util.MimeType; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +public class DefaultChatSpecLambda implements FluentChatClientLambda.ChatSpec { + + private String userText; + + private ChatClient chatClient; + + private Map textParameters = new HashMap<>(); + + public DefaultChatSpecLambda(ChatClient chatClient) { + this.chatClient = chatClient; + } + + @Override + public FluentChatClientLambda.ChatUserSpec user() { + return new DefaultChatUserSpec(this); + } + + public void addUser(String userText, Map textParameters) { + this.userText = userText; + this.textParameters.putAll(textParameters); + } + + public T execute(T... varargsOfT) { + + Assert.state(varargsOfT.length == 0, "you should not provide any values for T!"); + Assert.state(varargsOfT.getClass().isArray(), "this needs to be an array parameter"); + Class componentClass = varargsOfT.getClass().getComponentType(); + + List messageList = new ArrayList<>(); + String userTextToUse = userText + System.lineSeparator() + "{format}"; + var converter = new BeanOutputConverter(componentClass); + + this.textParameters.put("format", converter.getFormat()); + doCreateUserMessage(messageList, userTextToUse, this.textParameters); + + Prompt prompt = new Prompt(messageList); + ChatResponse chatResponse = this.chatClient.call(prompt); + String stringResponse = chatResponse.getResult().getOutput().getContent(); + + T convertedResponse = (T) converter.convert(stringResponse); + return convertedResponse; + } + + private void doCreateUserMessage(List messageList, String userText, Map textParameters) { + PromptTemplate userPromptTemplate = new PromptTemplate(userText); + messageList.add(userPromptTemplate.createMessage(textParameters)); + + } + + private class DefaultChatUserSpec implements FluentChatClientLambda.ChatUserSpec { + + private DefaultChatSpecLambda chatSpec; + + private String text; + + private Map textParameters = new HashMap<>(); + + public DefaultChatUserSpec(DefaultChatSpecLambda chatSpec) { + this.chatSpec = chatSpec; + } + + // @Override + // public FluentChatClientLambda.ChatParamSpec text(String text) { + // this.text = text; + // return new DefaultChatParamSpec(this); + // } + // + // @Override + // public FluentChatClientLambda.ChatMediaParamSpec media() { + // return new DefaultChatMediaParamSpec(this); + // } + + public void addTextParams(Map textParameters) { + this.textParameters.putAll(textParameters); + } + + @Override + public FluentChatClientLambda.ChatParamSpec text(String text, + Consumer chatParamSpecConsumer) { + this.text = text; + return chatParamSpecConsumer; + } + + @Override + public FluentChatClientLambda.ChatMediaParamSpec media( + Consumer chatMediaParamSpecConsumer) { + return null; + } + + } + + private class DefaultChatMediaParamSpec implements FluentChatClientLambda.ChatMediaParamSpec { + + private final DefaultChatUserSpec chatUserSpec; + + public DefaultChatMediaParamSpec(DefaultChatUserSpec chatUserSpec) { + this.chatUserSpec = chatUserSpec; + } + + @Override + public FluentChatClientLambda.ChatMediaParamSpec param(List mediaList) { + return null; + } + + @Override + public FluentChatClientLambda.ChatMediaParamSpec param(Media media) { + return null; + } + + @Override + public FluentChatClientLambda.ChatMediaParamSpec param(MimeType mimeType, Object data) { + return null; + } + + } + + private class DefaultChatParamSpec implements FluentChatClientLambda.ChatParamSpec { + + private final DefaultChatUserSpec chatUserSpec; + + private Map parameters = new HashMap<>(); + + public DefaultChatParamSpec(DefaultChatUserSpec chatUserSpec) { + this.chatUserSpec = chatUserSpec; + } + + @Override + public FluentChatClientLambda.ChatParamSpec param(String name, Object value) { + this.parameters.put(name, value); + return this; + } + + @Override + public FluentChatClientLambda.ChatParamSpec params(Map paramMap) { + this.parameters.putAll(paramMap); + return this; + } + + } + +} diff --git a/spring-ai-core/src/main/java/org/springframework/ai/chat/FluentChatClient.java b/spring-ai-core/src/main/java/org/springframework/ai/chat/FluentChatClient.java new file mode 100644 index 000000000..4ed01c288 --- /dev/null +++ b/spring-ai-core/src/main/java/org/springframework/ai/chat/FluentChatClient.java @@ -0,0 +1,61 @@ +package org.springframework.ai.chat; + +import org.springframework.ai.chat.messages.Media; +import org.springframework.util.MimeType; + +import java.util.List; +import java.util.Map; + +public class FluentChatClient { + + private final ChatClient chatClient; + + public FluentChatClient(ChatClient chatClient) { + this.chatClient = chatClient; + } + + public ChatSpec chat() { + return new DefaultChatSpec(this.chatClient); + } + + public interface ChatSpec { + + ChatUserSpec user(); + + T execute(T... varargsOfT); + + } + + public interface ChatUserSpec { + + ChatParamSpec text(String text); + + ChatMediaParamSpec media(); + + ChatSpec and(); + + } + + public interface ChatMediaParamSpec { + + ChatMediaParamSpec param(List mediaList); + + ChatMediaParamSpec param(Media media); + + ChatMediaParamSpec param(MimeType mimeType, Object data); + + ChatUserSpec and(); + + } + + public interface ChatParamSpec { + + ChatParamSpec param(String name, Object value); + + ChatParamSpec params(Map paramMap); + + ChatUserSpec and(); + + } + +} diff --git a/spring-ai-core/src/main/java/org/springframework/ai/chat/FluentChatClientLambda.java b/spring-ai-core/src/main/java/org/springframework/ai/chat/FluentChatClientLambda.java new file mode 100644 index 000000000..aff0bd7c1 --- /dev/null +++ b/spring-ai-core/src/main/java/org/springframework/ai/chat/FluentChatClientLambda.java @@ -0,0 +1,56 @@ +package org.springframework.ai.chat; + +import org.springframework.ai.chat.messages.Media; +import org.springframework.util.MimeType; + +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +public class FluentChatClientLambda { + + private final ChatClient chatClient; + + public FluentChatClientLambda(ChatClient chatClient) { + this.chatClient = chatClient; + } + + public void chat(Consumer chatSpecConsumer) { + + } + + public interface ChatSpec { + + ChatUserSpec user(Consumer chatUserSpecConsumer); + + T execute(T... varargsOfT); + + } + + public interface ChatUserSpec { + + ChatParamSpec text(String text, Consumer chatParamSpecConsumer); + + ChatMediaParamSpec media(Consumer chatMediaParamSpecConsumer); + + } + + public interface ChatMediaParamSpec { + + ChatMediaParamSpec param(List mediaList); + + ChatMediaParamSpec param(Media media); + + ChatMediaParamSpec param(MimeType mimeType, Object data); + + } + + public interface ChatParamSpec { + + ChatParamSpec param(String name, Object value); + + ChatParamSpec params(Map paramMap); + + } + +}