diff --git a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/client/OpenAiChatClientIT.java b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/client/OpenAiChatClientIT.java index 830cd0f7d..ee6ee9870 100644 --- a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/client/OpenAiChatClientIT.java +++ b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/client/OpenAiChatClientIT.java @@ -65,7 +65,7 @@ class OpenAiChatClientIT extends AbstractIT { void call() { // @formatter:off - ChatResponse response = ChatClient.builder(chatModel).build().prompt() + ChatResponse response = ChatClient.create(chatModel).prompt() .system(s -> s.text(systemTextResource) .param("name", "Bob") .param("voice", "pirate")) @@ -82,7 +82,7 @@ class OpenAiChatClientIT extends AbstractIT { @Test void listOutputConverterString() { // @formatter:off - List collection = ChatClient.builder(chatModel).build().prompt() + List collection = ChatClient.create(chatModel).prompt() .user(u -> u.text("List five {subject}") .param("subject", "ice cream flavors")) .call() @@ -97,7 +97,7 @@ class OpenAiChatClientIT extends AbstractIT { void listOutputConverterBean() { // @formatter:off - List actorsFilms = ChatClient.builder(chatModel).build().prompt() + List actorsFilms = ChatClient.create(chatModel).prompt() .user("Generate the filmography of 5 movies for Tom Hanks and Bill Murray.") .call() .entity(new ParameterizedTypeReference>() { @@ -114,7 +114,7 @@ class OpenAiChatClientIT extends AbstractIT { var toStringListConverter = new ListOutputConverter(new DefaultConversionService()); // @formatter:off - List flavors = ChatClient.builder(chatModel).build().prompt() + List flavors = ChatClient.create(chatModel).prompt() .user(u -> u.text("List five {subject}") .param("subject", "ice cream flavors")) .call() @@ -129,7 +129,7 @@ class OpenAiChatClientIT extends AbstractIT { @Test void mapOutputConverter() { // @formatter:off - Map result = ChatClient.builder(chatModel).build().prompt() + Map result = ChatClient.create(chatModel).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() @@ -144,7 +144,7 @@ class OpenAiChatClientIT extends AbstractIT { void beanOutputConverter() { // @formatter:off - ActorsFilms actorsFilms = ChatClient.builder(chatModel).build().prompt() + ActorsFilms actorsFilms = ChatClient.create(chatModel).prompt() .user("Generate the filmography for a random actor.") .call() .entity(ActorsFilms.class); @@ -158,7 +158,7 @@ class OpenAiChatClientIT extends AbstractIT { void beanOutputConverterRecords() { // @formatter:off - ActorsFilms actorsFilms = ChatClient.builder(chatModel).build().prompt() + ActorsFilms actorsFilms = ChatClient.create(chatModel).prompt() .user("Generate the filmography of 5 movies for Tom Hanks.") .call() .entity(ActorsFilms.class); @@ -175,8 +175,7 @@ class OpenAiChatClientIT extends AbstractIT { BeanOutputConverter outputConverter = new BeanOutputConverter<>(ActorsFilms.class); // @formatter:off - Flux chatResponse = ChatClient.builder(chatModel) - .build() + Flux chatResponse = ChatClient.create(chatModel) .prompt() .user(u -> u .text("Generate the filmography of 5 movies for Tom Hanks. " + System.lineSeparator() @@ -202,7 +201,7 @@ class OpenAiChatClientIT extends AbstractIT { void functionCallTest() { // @formatter:off - String response = ChatClient.builder(chatModel).build().prompt() + String response = ChatClient.create(chatModel).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() @@ -238,7 +237,7 @@ class OpenAiChatClientIT extends AbstractIT { void streamFunctionCallTest() { // @formatter:off - Flux response = ChatClient.builder(chatModel).build().prompt() + Flux response = ChatClient.create(chatModel).prompt() .user("What's the weather like in San Francisco, Tokyo, and Paris?") .function("getCurrentWeather", "Get the weather in location", new MockWeatherService()) .stream() @@ -258,9 +257,7 @@ class OpenAiChatClientIT extends AbstractIT { void multiModalityEmbeddedImage(String modelName) throws IOException { // @formatter:off - String response = ChatClient.builder(chatModel).build().prompt() - // TODO consider adding model(...) method to ChatClient as a shortcut to - // OpenAiChatOptions.builder().withModel(modelName).build() + String response = ChatClient.create(chatModel).prompt() .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"))) @@ -281,7 +278,7 @@ class OpenAiChatClientIT extends AbstractIT { URL url = new URL("https://docs.spring.io/spring-ai/reference/1.0-SNAPSHOT/_images/multimodal.test.png"); // @formatter:off - String response = ChatClient.builder(chatModel).build().prompt() + String response = ChatClient.create(chatModel).prompt() // TODO consider adding model(...) method to ChatClient as a shortcut to // OpenAiChatOptions.builder().withModel(modelName).build() .options(OpenAiChatOptions.builder().withModel(modelName).build()) @@ -302,7 +299,7 @@ class OpenAiChatClientIT extends AbstractIT { URL url = new URL("https://docs.spring.io/spring-ai/reference/1.0-SNAPSHOT/_images/multimodal.test.png"); // @formatter:off - Flux response = ChatClient.builder(chatModel).build().prompt() + Flux response = ChatClient.create(chatModel).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?") diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/functions.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/functions.adoc index 47ccfe140..65051c6ac 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/functions.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/functions.adoc @@ -3,7 +3,7 @@ The integration of function support in AI models, permits the model to request the execution of client-side functions, thereby accessing necessary information or performing tasks dynamically as required. -image::function-calling-basic-flow2.jpg[Function calling, width=500, align="center"] +image::function-calling-basic-flow2.jpg[Function calling, width=700, align="center"] Spring AI currently supports Function invocation for the following AI Models diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/multimodality.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/multimodality.adoc index 2630a2cbb..c7ddbc3fd 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/multimodality.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/multimodality.adoc @@ -51,6 +51,18 @@ var userMessage = new UserMessage( ChatResponse response = chatModel.call(new Prompt(List.of(userMessage))); ---- +or with the fluent xref::api/chatclient.adoc[ChatClient] API: + +[source,java] +---- +String response = ChatClient.create(chatModel).prompt() + .user(u -> u.text("Explain what do you see on this picture?") + .media(MimeTypeUtils.IMAGE_PNG, new ClassPathResource("/multimodal.test.png"))) + .call() + .content(); +---- + + and produce a response like: > This is an image of a fruit bowl with a simple design. The bowl is made of metal with curved wire edges that create an open structure, allowing the fruit to be visible from all angles. Inside the bowl, there are two yellow bananas resting on top of what appears to be a red apple. The bananas are slightly overripe, as indicated by the brown spots on their peels. The bowl has a metal ring at the top, likely to serve as a handle for carrying. The bowl is placed on a flat surface with a neutral-colored background that provides a clear view of the fruit inside. @@ -58,7 +70,7 @@ and produce a response like: Latest version of Spring AI provides multimodal support for the following Chat Clients: * xref:api/chat/openai-chat.adoc#_multimodal[Open AI - (GPT-4-Vision and GPT-4o models)] -* xref:api/chat/openai-chat.adoc#_multimodal[Ollama - (LlaVa and Baklava models)] +* xref:api/chat/ollama-chat.adoc#_multimodal[Ollama - (LlaVa and Baklava models)] * xref:api/chat/vertexai-gemini-chat.adoc#_multimodal[Vertex AI Gemini - (gemini-pro-vision model)] * xref:api/chat/anthropic-chat.adoc#_multimodal[Anthropic Claude 3] * xref:api/chat/bedrock/bedrock-anthropic3.adoc#_multimodal[AWS Bedrock Anthropic Claude 3] \ No newline at end of file diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/structured-output-converter.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/structured-output-converter.adoc index 6346d4e5c..331143968 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/structured-output-converter.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/structured-output-converter.adoc @@ -107,7 +107,18 @@ record ActorsFilms(String actor, List movies) { } ---- -Here is how to apply the BeanOutputConverter: +Here is how to apply the BeanOutputConverter using the new, fluent ChatClient API: + +[source,java] +---- +ActorsFilms actorsFilms = ChatClient.create(chatModel).prompt() + .user(u -> u.text("Generate the filmography of 5 movies for {actor}.") + .param("actor", "Tom Hanks")) + .call() + .entity(ActorsFilms.class); +---- + +or using the low-level, ChatModel API directly: [source,java] ---- @@ -136,8 +147,19 @@ For example, to represent a list of actors and their filmographies: [source,java] ---- -BeanOutputConverter> outputConverter = new BeanOutputConverter<>( - new ParameterizedTypeReference>() { }); +List actorsFilms = ChatClient.create(chatModel).prompt() + .user("Generate the filmography of 5 movies for Tom Hanks and Bill Murray.") + .call() + .entity(new ParameterizedTypeReference>() { + }); +---- + +or using the low-level, ChatModel API directly: + +[source,java] +---- +BeanOutputConverter> outputConverter = new BeanOutputConverter<>( + new ParameterizedTypeReference>() { }); String format = outputConverter.getFormat(); String template = """ @@ -149,13 +171,25 @@ Prompt prompt = new Prompt(new PromptTemplate(template, Map.of("format", format) Generation generation = chatModel.call(prompt).getResult(); -List actorsFilms = outputConverter.convert(generation.getOutput().getContent()); +List actorsFilms = outputConverter.convert(generation.getOutput().getContent()); ---- === Map Output Converter Following sniped shows how to use `MapOutputConverter` to generate a list of numbers. +[source,java] +---- +Map result = ChatClient.create(chatModel).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() + .entity(new ParameterizedTypeReference>() { + }); +---- + +or using the low-level, ChatModel API directly: + [source,java] ---- MapOutputConverter mapOutputConverter = new MapOutputConverter(); @@ -177,6 +211,17 @@ Map result = mapOutputConverter.convert(generation.getOutput().g Following snippet shows how to use `ListOutputConverter` to generate a list of ice cream flavors. +[source,java] +---- +List flavors = ChatClient.create(chatModel).prompt() + .user(u -> u.text("List five {subject}") + .param("subject", "ice cream flavors")) + .call() + .entity(new ListOutputConverter(new DefaultConversionService())); +---- + +or using the low-level, ChatModel API directly: + [source,java] ---- ListOutputConverter listOutputConverter = new ListOutputConverter(new DefaultConversionService()); diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/concepts.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/concepts.adoc index 3c4382b91..c5bd6ae56 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/concepts.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/concepts.adoc @@ -191,23 +191,16 @@ This is the reason to use a vector database. It is very good at finding similar image::spring-ai-rag.jpg[Spring AI RAG, width=1000, align="center"] -There are several concepts that are used in implementing RAG. -The concepts map onto classes in Spring AI: - -* `DocumentReader`: A Java functional interface that is responsible for loading a `List` from a data source. Common data sources are PDF, Markdown, and JSON. -* `Document`: A text-based representation of your data source that also contains metadata to describe the contents. -* `DocumentTransformer`: Responsible for processing the data in various ways (for example, splitting documents into smaller pieces or adding additional metadata to the `Document`). -* `DocumentWriter`: Lets you persist the Documents into a database (most commonly in the AI stack, a vector database). -* `Embedding`: A representation of your data as a `List` that is used by the vector database to compute the "`similarity`" of a user's query to relevant documents. - +* The xref::api/etl-pipeline.adoc[ETL pipeline] provides further information about orchestrating the flow of extracting data from the data sources and stor it in a structured vector store, ensuring data is in the optimal format for retrieval by the AI model. +* The xref::api/chatclient.adoc#_retrieval_augmented_generation[ChatClient - RAG] explains how to use the `QuestionAnswerAdvisor` advisor to enable the RAG capability to your application. == Function Calling Large Language Models (LLMs) are frozen after training, leading to stale knowledge and they are unable to access or modify external data. -image::function-calling-basic-flow2.jpg[Function calling, width=800, align="center"] +image::function-calling-basic-flow2.jpg[Function calling, width=700, align="center"] -The `Function Calling` mechanism addresses these shortcomings. +The xref::api/functions.adoc[Function Calling] mechanism addresses these shortcomings. It allows you to register your own functions to connect the large language models to the APIs of external systems. These systems can provide LLMs with real-time data and perform data processing actions on their behalf.