API refactoring

* Added more natural method names to DocumentReader, Transformer, Writer
* Changed Content getMedia to return Collection, deprecated use of List
* Change constructor for Media to accept URL and Resource, deprecate Object constructor
* Update ETL documentation and a few tests to avoid now deprecated APIs.
This commit is contained in:
Mark Pollack
2024-05-16 12:29:47 +02:00
parent 227f0703ec
commit fd9c98661d
17 changed files with 165 additions and 60 deletions

View File

@@ -182,12 +182,12 @@ class AnthropicChatClientIT {
@Test
void multiModalityTest() throws IOException {
byte[] imageData = new ClassPathResource("/test.png").getContentAsByteArray();
var imageData = new ClassPathResource("/test.png");
var userMessage = new UserMessage("Explain what do you see on this picture?",
List.of(new Media(MimeTypeUtils.IMAGE_PNG, imageData)));
ChatResponse response = chatClient.call(new Prompt(List.of(userMessage)));
var response = chatClient.call(new Prompt(List.of(userMessage)));
logger.info(response.getResult().getOutput().getContent());
assertThat(response.getResult().getOutput().getContent()).contains("bananas", "apple", "basket");

View File

@@ -206,12 +206,12 @@ class BedrockAnthropic3ChatClientIT {
@Test
void multiModalityTest() throws IOException {
byte[] imageData = new ClassPathResource("/test.png").getContentAsByteArray();
var imageData = new ClassPathResource("/test.png");
var userMessage = new UserMessage("Explain what do you see o this picture?",
List.of(new Media(MimeTypeUtils.IMAGE_PNG, imageData)));
ChatResponse response = client.call(new Prompt(List.of(userMessage)));
var response = client.call(new Prompt(List.of(userMessage)));
logger.info(response.getResult().getOutput().getContent());
assertThat(response.getResult().getOutput().getContent()).contains("bananas", "apple", "basket");

View File

@@ -71,12 +71,12 @@ class OllamaChatClientMultimodalIT {
@Test
void multiModalityTest() throws IOException {
byte[] imageData = new ClassPathResource("/test.png").getContentAsByteArray();
var imageData = new ClassPathResource("/test.png");
var userMessage = new UserMessage("Explain what do you see on this picture?",
List.of(new Media(MimeTypeUtils.IMAGE_PNG, imageData)));
ChatResponse response = client.call(new Prompt(List.of(userMessage)));
var response = client.call(new Prompt(List.of(userMessage)));
logger.info(response.getResult().getOutput().getContent());
assertThat(response.getResult().getOutput().getContent()).contains("bananas", "apple", "basket");

View File

@@ -16,6 +16,7 @@
package org.springframework.ai.openai.chat;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -249,12 +250,12 @@ class OpenAiChatClientIT extends AbstractIT {
@ValueSource(strings = { "gpt-4-vision-preview", "gpt-4o" })
void multiModalityEmbeddedImage(String modelName) throws IOException {
byte[] imageData = new ClassPathResource("/test.png").getContentAsByteArray();
var imageData = new ClassPathResource("/test.png");
var userMessage = new UserMessage("Explain what do you see on this picture?",
List.of(new Media(MimeTypeUtils.IMAGE_PNG, imageData)));
ChatResponse response = chatClient
var response = chatClient
.call(new Prompt(List.of(userMessage), OpenAiChatOptions.builder().withModel(modelName).build()));
logger.info(response.getResult().getOutput().getContent());
@@ -266,9 +267,9 @@ class OpenAiChatClientIT extends AbstractIT {
@ValueSource(strings = { "gpt-4-vision-preview", "gpt-4o" })
void multiModalityImageUrl(String modelName) throws IOException {
var userMessage = new UserMessage("Explain what do you see on this picture?",
List.of(new Media(MimeTypeUtils.IMAGE_PNG,
"https://docs.spring.io/spring-ai/reference/1.0-SNAPSHOT/_images/multimodal.test.png")));
var userMessage = new UserMessage("Explain what do you see on this picture?", List
.of(new Media(MimeTypeUtils.IMAGE_PNG,
new URL("https://docs.spring.io/spring-ai/reference/1.0-SNAPSHOT/_images/multimodal.test.png"))));
ChatResponse response = chatClient
.call(new Prompt(List.of(userMessage), OpenAiChatOptions.builder().withModel(modelName).build()));
@@ -281,9 +282,9 @@ class OpenAiChatClientIT extends AbstractIT {
@Test
void streamingMultiModalityImageUrl() throws IOException {
var userMessage = new UserMessage("Explain what do you see on this picture?",
List.of(new Media(MimeTypeUtils.IMAGE_PNG,
"https://docs.spring.io/spring-ai/reference/1.0-SNAPSHOT/_images/multimodal.test.png")));
var userMessage = new UserMessage("Explain what do you see on this picture?", List
.of(new Media(MimeTypeUtils.IMAGE_PNG,
new URL("https://docs.spring.io/spring-ai/reference/1.0-SNAPSHOT/_images/multimodal.test.png"))));
Flux<ChatResponse> response = streamingChatClient.stream(new Prompt(List.of(userMessage),
OpenAiChatOptions.builder().withModel(OpenAiApi.ChatModel.GPT_4_VISION_PREVIEW.getValue()).build()));

View File

@@ -17,6 +17,7 @@
package org.springframework.ai.openai.chat.service;
import java.util.List;
import java.util.function.Supplier;
import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
@@ -112,7 +113,7 @@ public class OpenAiPromptTransformingChatServiceIT {
void loadData() {
JsonReader jsonReader = new JsonReader(bikesResource, "name", "price", "shortDescription", "description");
var textSplitter = new TokenTextSplitter();
List<Document> splitDocuments = textSplitter.apply(jsonReader.get());
List<Document> splitDocuments = textSplitter.split(jsonReader.get());
for (Document splitDocument : splitDocuments) {
splitDocument.getMetadata().put(TransformerContentType.EXTERNAL_KNOWLEDGE, "true");
@@ -121,6 +122,21 @@ public class OpenAiPromptTransformingChatServiceIT {
vectorStore.accept(splitDocuments);
}
void loadData2() {
JsonReader jsonReader = null;
TokenTextSplitter tokenTextSplitter = null;
VectorStore vectorStore = null;
List<Document> documents = jsonReader.read();
List<Document> splitDocuments = tokenTextSplitter.split(documents);
vectorStore.write(splitDocuments);
// Now in java.util.Function style.
Supplier<List<Document>> docs = jsonReader::read;
}
@SpringBootConfiguration
static class Config {

View File

@@ -186,12 +186,12 @@ class VertexAiGeminiChatClientIT {
@Test
void multiModalityTest() throws IOException {
byte[] data = new ClassPathResource("/vertex.test.png").getContentAsByteArray();
var data = new ClassPathResource("/vertex.test.png");
var userMessage = new UserMessage("Explain what do you see o this picture?",
List.of(new Media(MimeTypeUtils.IMAGE_PNG, data)));
ChatResponse response = client.call(new Prompt(List.of(userMessage)));
var response = client.call(new Prompt(List.of(userMessage)));
// Response should contain something like:
// I see a bunch of bananas in a golden basket. The bananas are ripe and yellow.

View File

@@ -18,12 +18,7 @@ package org.springframework.ai.chat.messages;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.*;
import org.springframework.core.io.Resource;
import org.springframework.util.Assert;
@@ -44,7 +39,7 @@ public abstract class AbstractMessage implements Message {
protected final String textContent;
protected final List<Media> mediaData;
protected final List<Media> media;
/**
* Additional options for the message to influence the response, not a generative map.
@@ -59,25 +54,25 @@ public abstract class AbstractMessage implements Message {
Assert.notNull(messageType, "Message type must not be null");
this.messageType = messageType;
this.textContent = content;
this.mediaData = new ArrayList<>();
this.media = new ArrayList<>();
this.metadata = new HashMap<>(metadata);
this.metadata.put(MESSAGE_TYPE, messageType);
}
protected AbstractMessage(MessageType messageType, String textContent, List<Media> mediaData) {
this(messageType, textContent, mediaData, Map.of(MESSAGE_TYPE, messageType));
protected AbstractMessage(MessageType messageType, String textContent, List<Media> media) {
this(messageType, textContent, media, Map.of(MESSAGE_TYPE, messageType));
}
protected AbstractMessage(MessageType messageType, String textContent, List<Media> mediaData,
protected AbstractMessage(MessageType messageType, String textContent, Collection<Media> media,
Map<String, Object> metadata) {
Assert.notNull(messageType, "Message type must not be null");
Assert.notNull(textContent, "Content must not be null");
Assert.notNull(mediaData, "media data must not be null");
Assert.notNull(media, "media data must not be null");
this.messageType = messageType;
this.textContent = textContent;
this.mediaData = new ArrayList<>(mediaData);
this.media = new ArrayList<>(media);
this.metadata = new HashMap<>(metadata);
this.metadata.put(MESSAGE_TYPE, messageType);
}
@@ -94,7 +89,7 @@ public abstract class AbstractMessage implements Message {
this.messageType = messageType;
this.metadata = new HashMap<>(metadata);
this.metadata.put(MESSAGE_TYPE, messageType);
this.mediaData = new ArrayList<>();
this.media = new ArrayList<>();
try (InputStream inputStream = resource.getInputStream()) {
this.textContent = StreamUtils.copyToString(inputStream, Charset.defaultCharset());
@@ -110,8 +105,8 @@ public abstract class AbstractMessage implements Message {
}
@Override
public List<Media> getMedia() {
return this.mediaData;
public List<Media> getMedia(String... dummy) {
return this.media;
}
@Override
@@ -126,7 +121,7 @@ public abstract class AbstractMessage implements Message {
@Override
public int hashCode() {
return Objects.hash(this.messageType, this.textContent, this.mediaData, this.metadata);
return Objects.hash(this.messageType, this.textContent, this.media, this.metadata);
}
@Override
@@ -139,8 +134,8 @@ public abstract class AbstractMessage implements Message {
}
AbstractMessage other = (AbstractMessage) obj;
return Objects.equals(this.messageType, other.messageType)
&& Objects.equals(this.textContent, other.textContent)
&& Objects.equals(this.mediaData, other.mediaData) && Objects.equals(this.metadata, other.metadata);
&& Objects.equals(this.textContent, other.textContent) && Objects.equals(this.media, other.media)
&& Objects.equals(this.metadata, other.metadata);
}
}

View File

@@ -15,9 +15,13 @@
*/
package org.springframework.ai.chat.messages;
import org.springframework.core.io.Resource;
import org.springframework.util.Assert;
import org.springframework.util.MimeType;
import java.io.IOException;
import java.net.URL;
/**
* The Media class represents the data and metadata of a media attachment in a message. It
* consists of a MIME type and the raw data.
@@ -33,16 +37,46 @@ public class Media {
private final Object data;
/**
* The Media class represents the data and metadata of a media attachment in a
* message. It consists of a MIME type and the raw data.
*
* This class is used as a parameter in the constructor of the UserMessage class.
* @deprecated This constructor is deprecated since version 1.0.0 M1 and will be
* removed in a future release.
*/
@Deprecated(since = "1.0.0 M1", forRemoval = true)
public Media(MimeType mimeType, Object data) {
Assert.notNull(mimeType, "MimeType must not be null");
this.mimeType = mimeType;
this.data = data;
}
public Media(MimeType mimeType, URL url) {
Assert.notNull(mimeType, "MimeType must not be null");
this.mimeType = mimeType;
this.data = url.toString();
}
public Media(MimeType mimeType, Resource resource) {
Assert.notNull(mimeType, "MimeType must not be null");
this.mimeType = mimeType;
try {
this.data = resource.getContentAsByteArray();
}
catch (IOException e) {
throw new RuntimeException(e);
}
}
public MimeType getMimeType() {
return this.mimeType;
}
/**
* Get the media data object
* @return a java.net.URL.toString() or a byte[]
*/
public Object getData() {
return this.data;
}

View File

@@ -16,6 +16,7 @@
package org.springframework.ai.chat.messages;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@@ -44,7 +45,7 @@ public class UserMessage extends AbstractMessage {
this(textContent, Arrays.asList(media));
}
public UserMessage(String textContent, List<Media> mediaList, Map<String, Object> metadata) {
public UserMessage(String textContent, Collection<Media> mediaList, Map<String, Object> metadata) {
super(MessageType.USER, textContent, mediaList, metadata);
}

View File

@@ -15,10 +15,7 @@
*/
package org.springframework.ai.document;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
@@ -112,7 +109,7 @@ public class Document implements Content {
}
@Override
public List<Media> getMedia() {
public List<Media> getMedia(String... dummy) {
return this.media;
}

View File

@@ -20,4 +20,8 @@ import java.util.function.Supplier;
public interface DocumentReader extends Supplier<List<Document>> {
default List<Document> read() {
return get();
}
}

View File

@@ -20,4 +20,8 @@ import java.util.function.Function;
public interface DocumentTransformer extends Function<List<Document>, List<Document>> {
default List<Document> transform(List<Document> transform) {
return apply(transform);
}
}

View File

@@ -23,4 +23,8 @@ import java.util.function.Consumer;
*/
public interface DocumentWriter extends Consumer<List<Document>> {
default void write(List<Document> documents) {
accept(documents);
}
}

View File

@@ -2,6 +2,7 @@ package org.springframework.ai.model;
import org.springframework.ai.chat.messages.Media;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@@ -24,7 +25,19 @@ public interface Content {
/**
* Get the media associated with the content.
*/
List<Media> getMedia();
default Collection<Media> getMedia() {
return getMedia("");
}
/**
* Retrieves the collection of media attachments associated with the content.
* @param dummy a dummy parameter to ensure method signature uniqueness
* @return a list of Media objects representing the media attachments
* @deprecated This method is deprecated since version 1.0.0 M1 and will be removed in
* a future release
*/
@Deprecated(since = "1.0.0 M1", forRemoval = true)
List<Media> getMedia(String... dummy);
/**
* return Get the metadata associated with the content.

View File

@@ -43,6 +43,14 @@ public abstract class TextSplitter implements DocumentTransformer {
return doSplitDocuments(documents);
}
public List<Document> split(List<Document> documents) {
return this.apply(documents);
}
public List<Document> split(Document document) {
return this.apply(List.of(document));
}
public void setCopyContentFormatter(boolean copyContentFormatter) {
this.copyContentFormatter = copyContentFormatter;
}

View File

@@ -68,10 +68,10 @@ public class TokenTextSplitter extends TextSplitter {
@Override
protected List<String> splitText(String text) {
return split(text, this.defaultChunkSize);
return doSplit(text, this.defaultChunkSize);
}
public List<String> split(String text, int chunkSize) {
protected List<String> doSplit(String text, int chunkSize) {
if (text == null || text.trim().isEmpty()) {
return new ArrayList<>();
}

View File

@@ -26,12 +26,22 @@ Let's say we have the following instances of those three ETL types
* `TokenTextSplitter` an implementation of `DocumentTransformer`
* `VectorStore` an implementation of `DocumentWriter`
To perform the basic loading of data into a Vector Database for use with the Retrieval Augmented Generation pattern, use the following code.
To perform the basic loading of data into a Vector Database for use with the Retrieval Augmented Generation pattern, use the following code in Java function style syntax.
[source,java]
----
vectorStore.accept(tokenTextSplitter.apply(pdfReader.get()));
----
Alternatively, you can use method names that are more naturally expressive for the domain
[source,java]
----
vectorStore.write(tokenTextSplitter.split(pdfReader.read()));
----
== Getting Started
To begin creating a Spring AI RAG application, follow these steps:
@@ -57,6 +67,9 @@ Provides a source of documents from diverse origins.
----
public interface DocumentReader extends Supplier<List<Document>> {
default List<Document> read() {
return get();
}
}
----
@@ -68,14 +81,17 @@ Example:
[source,java]
----
@Component
public class MyAiApp {
class MyAiAppComponent {
@Value("classpath:bikes.json") // This is the json document to load
private Resource resource;
private final Resource resource;
MyAiAppComponent(@Value("classpath:bikes.json") Resource resource) {
this.resource = resource;
}
List<Document> loadJsonAsDocuments() {
JsonReader jsonReader = new JsonReader(resource, "description");
return jsonReader.get();
return jsonReader.read();
}
}
----
@@ -88,16 +104,18 @@ Example:
[source,java]
----
@Component
public class MyTextReader {
class MyTextReader {
@Value("classpath:text-source.txt") // This is the text document to load
private Resource resource;
private final Resource resource;
MyTextReader(@Value("classpath:text-source.txt") Resource resource) {
this.resource = resource;
}
List<Document> loadText() {
TextReader textReader = new TextReader(resource);
textReader.getCustomMetadata().put("filename", "text-source.txt");
return textReader.get();
return textReader.read();
}
}
----
@@ -123,7 +141,7 @@ public class MyPagePdfDocumentReader {
.withPagesPerDocument(1)
.build());
return pdfReader.get();
return pdfReader.read();
}
}
@@ -153,7 +171,7 @@ public class MyPagePdfDocumentReader {
.withPagesPerDocument(1)
.build());
return pdfReader.get();
return pdfReader.read();
}
}
----
@@ -167,14 +185,18 @@ Example:
[source,java]
----
@Component
public class MyTikaDocumentReader {
class MyTikaDocumentReader {
@Value("classpath:/word-sample.docx") // This is the word document to load
private Resource resource;
private final Resource resource;
MyTikaDocumentReader(@Value("classpath:/word-sample.docx")
Resource resource) {
this.resource = resource;
}
List<Document> loadText() {
TikaDocumentReader tikaDocumentReader = new TikaDocumentReader(resource);
return tikaDocumentReader.get();
return tikaDocumentReader.read();
}
}
----
@@ -187,6 +209,9 @@ Transforms a batch of documents as part of the processing workflow.
----
public interface DocumentTransformer extends Function<List<Document>, List<Document>> {
default List<Document> transform(List<Document> transform) {
return apply(transform);
}
}
----
@@ -213,6 +238,9 @@ Manages the final stage of the ETL process, preparing documents for storage.
```java
public interface DocumentWriter extends Consumer<List<Document>> {
default void write(List<Document> documents) {
accept(documents);
}
}
```
==== FileDocumentWriter