Update AnthropicChatModel and Spring AI Documentation for Multimodal Support

- Enhance AnthropicChatModel to support PDF and document content types
- Introduce getContentBlockTypeByMedia method for flexible media type handling
- Update ContentBlock handling to dynamically determine content type for media
- Add multimodal PDF support test case for Claude 3.5 Sonnet
- Update documentation to reflect PDF and multimodal capabilities
- Modify comparison chart to show PDF support for Anthropic Claude

Fixese #1819

review
This commit is contained in:
Christian Tzolov
2024-11-25 16:13:56 +01:00
committed by Mark Pollack
parent 9d207e37be
commit e543cd082d
8 changed files with 357 additions and 276 deletions

View File

@@ -37,6 +37,7 @@ import org.springframework.ai.anthropic.api.AnthropicApi.AnthropicMessage;
import org.springframework.ai.anthropic.api.AnthropicApi.ChatCompletionRequest;
import org.springframework.ai.anthropic.api.AnthropicApi.ChatCompletionResponse;
import org.springframework.ai.anthropic.api.AnthropicApi.ContentBlock;
import org.springframework.ai.anthropic.api.AnthropicApi.ContentBlock.Source;
import org.springframework.ai.anthropic.api.AnthropicApi.ContentBlock.Type;
import org.springframework.ai.anthropic.api.AnthropicApi.Role;
import org.springframework.ai.anthropic.metadata.AnthropicUsage;
@@ -58,6 +59,7 @@ import org.springframework.ai.chat.observation.DefaultChatModelObservationConven
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.ChatOptionsBuilder;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.Media;
import org.springframework.ai.model.ModelOptionsUtils;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.function.FunctionCallbackContext;
@@ -355,6 +357,18 @@ public class AnthropicChatModel extends AbstractToolCallSupport implements ChatM
}
private Type getContentBlockTypeByMedia(Media media) {
String mimeType = media.getMimeType().toString();
if (mimeType.startsWith("image")) {
return Type.IMAGE;
}
else if (mimeType.contains("pdf")) {
return Type.DOCUMENT;
}
throw new IllegalArgumentException("Unsupported media type: " + mimeType
+ ". Supported types are: images (image/*) and PDF documents (application/pdf)");
}
ChatCompletionRequest createRequest(Prompt prompt, boolean stream) {
Set<String> functionsForThisRequest = new HashSet<>();
@@ -367,11 +381,12 @@ public class AnthropicChatModel extends AbstractToolCallSupport implements ChatM
List<ContentBlock> contents = new ArrayList<>(List.of(new ContentBlock(message.getContent())));
if (message instanceof UserMessage userMessage) {
if (!CollectionUtils.isEmpty(userMessage.getMedia())) {
List<ContentBlock> mediaContent = userMessage.getMedia()
.stream()
.map(media -> new ContentBlock(media.getMimeType().toString(),
this.fromMediaData(media.getData())))
.toList();
List<ContentBlock> mediaContent = userMessage.getMedia().stream().map(media -> {
Type contentBlockType = getContentBlockTypeByMedia(media);
var source = new Source(media.getMimeType().toString(),
this.fromMediaData(media.getData()));
return new ContentBlock(contentBlockType, source);
}).toList();
contents.addAll(mediaContent);
}
}

View File

@@ -64,7 +64,7 @@ public class AnthropicApi {
public static final String DEFAULT_ANTHROPIC_VERSION = "2023-06-01";
public static final String DEFAULT_ANTHROPIC_BETA_VERSION = "tools-2024-04-04";
public static final String DEFAULT_ANTHROPIC_BETA_VERSION = "tools-2024-04-04,pdfs-2024-09-25";
public static final String BETA_MAX_TOKENS = "max-tokens-3-5-sonnet-2024-07-15";
@@ -230,18 +230,23 @@ public class AnthropicApi {
/**
* The claude-3-5-sonnet-20241022 model.
*/
CLAUDE_3_5_SONNET("claude-3-5-sonnet-20241022"),
CLAUDE_3_5_SONNET("claude-3-5-sonnet-latest"),
/**
* The CLAUDE_3_OPUS
*/
CLAUDE_3_OPUS("claude-3-opus-20240229"),
CLAUDE_3_OPUS("claude-3-opus-latest"),
/**
* The CLAUDE_3_SONNET
*/
CLAUDE_3_SONNET("claude-3-sonnet-20240229"),
/**
* The CLAUDE 3.5 HAIKU
*/
CLAUDE_3_5_HAIKU("claude-3-5-haiku-latest"),
/**
* The CLAUDE_3_HAIKU
*/
@@ -296,18 +301,18 @@ public class AnthropicApi {
public enum Role {
// @formatter:off
/**
* The user role.
*/
@JsonProperty("user")
USER,
/**
* The user role.
*/
@JsonProperty("user")
USER,
/**
* The assistant role.
*/
@JsonProperty("assistant")
ASSISTANT
// @formatter:on
/**
* The assistant role.
*/
@JsonProperty("assistant")
ASSISTANT
// @formatter:on
}
@@ -436,18 +441,18 @@ public class AnthropicApi {
@JsonInclude(Include.NON_NULL)
public record ChatCompletionRequest(
// @formatter:off
@JsonProperty("model") String model,
@JsonProperty("messages") List<AnthropicMessage> messages,
@JsonProperty("system") String system,
@JsonProperty("max_tokens") Integer maxTokens,
@JsonProperty("metadata") Metadata metadata,
@JsonProperty("stop_sequences") List<String> stopSequences,
@JsonProperty("stream") Boolean stream,
@JsonProperty("temperature") Double temperature,
@JsonProperty("top_p") Double topP,
@JsonProperty("top_k") Integer topK,
@JsonProperty("tools") List<Tool> tools) {
// @formatter:on
@JsonProperty("model") String model,
@JsonProperty("messages") List<AnthropicMessage> messages,
@JsonProperty("system") String system,
@JsonProperty("max_tokens") Integer maxTokens,
@JsonProperty("metadata") Metadata metadata,
@JsonProperty("stop_sequences") List<String> stopSequences,
@JsonProperty("stream") Boolean stream,
@JsonProperty("temperature") Double temperature,
@JsonProperty("top_p") Double topP,
@JsonProperty("top_k") Integer topK,
@JsonProperty("tools") List<Tool> tools) {
// @formatter:on
public ChatCompletionRequest(String model, List<AnthropicMessage> messages, String system, Integer maxTokens,
Double temperature, Boolean stream) {
@@ -615,9 +620,9 @@ public class AnthropicApi {
@JsonInclude(Include.NON_NULL)
public record AnthropicMessage(
// @formatter:off
@JsonProperty("content") List<ContentBlock> content,
@JsonProperty("role") Role role) {
// @formatter:on
@JsonProperty("content") List<ContentBlock> content,
@JsonProperty("role") Role role) {
// @formatter:on
}
/**
@@ -639,23 +644,23 @@ public class AnthropicApi {
@JsonInclude(Include.NON_NULL)
public record ContentBlock(
// @formatter:off
@JsonProperty("type") Type type,
@JsonProperty("source") Source source,
@JsonProperty("text") String text,
@JsonProperty("type") Type type,
@JsonProperty("source") Source source,
@JsonProperty("text") String text,
// applicable only for streaming responses.
@JsonProperty("index") Integer index,
// applicable only for streaming responses.
@JsonProperty("index") Integer index,
// tool_use response only
@JsonProperty("id") String id,
@JsonProperty("name") String name,
@JsonProperty("input") Map<String, Object> input,
// tool_use response only
@JsonProperty("id") String id,
@JsonProperty("name") String name,
@JsonProperty("input") Map<String, Object> input,
// tool_result response only
@JsonProperty("tool_use_id") String toolUseId,
@JsonProperty("content") String content
) {
// @formatter:on
// tool_result response only
@JsonProperty("tool_use_id") String toolUseId,
@JsonProperty("content") String content
) {
// @formatter:on
/**
* Create content block
@@ -666,6 +671,15 @@ public class AnthropicApi {
this(new Source(mediaType, data));
}
/**
* Create content block
* @param type The type of the content.
* @param source The source of the content.
*/
public ContentBlock(Type type, Source source) {
this(type, source, null, null, null, null, null, null, null);
}
/**
* Create content block
* @param source The source of the content.
@@ -755,7 +769,13 @@ public class AnthropicApi {
* Image message.
*/
@JsonProperty("image")
IMAGE("image");
IMAGE("image"),
/**
* Document message.
*/
@JsonProperty("document")
DOCUMENT("document");
public final String value;
@@ -785,10 +805,10 @@ public class AnthropicApi {
@JsonInclude(Include.NON_NULL)
public record Source(
// @formatter:off
@JsonProperty("type") String type,
@JsonProperty("media_type") String mediaType,
@JsonProperty("data") String data) {
// @formatter:on
@JsonProperty("type") String type,
@JsonProperty("media_type") String mediaType,
@JsonProperty("data") String data) {
// @formatter:on
/**
* Create source
@@ -817,10 +837,10 @@ public class AnthropicApi {
@JsonInclude(Include.NON_NULL)
public record Tool(
// @formatter:off
@JsonProperty("name") String name,
@JsonProperty("description") String description,
@JsonProperty("input_schema") Map<String, Object> inputSchema) {
// @formatter:on
@JsonProperty("name") String name,
@JsonProperty("description") String description,
@JsonProperty("input_schema") Map<String, Object> inputSchema) {
// @formatter:on
}
// CB START EVENT
@@ -843,15 +863,15 @@ public class AnthropicApi {
@JsonInclude(Include.NON_NULL)
public record ChatCompletionResponse(
// @formatter:off
@JsonProperty("id") String id,
@JsonProperty("type") String type,
@JsonProperty("role") Role role,
@JsonProperty("content") List<ContentBlock> content,
@JsonProperty("model") String model,
@JsonProperty("stop_reason") String stopReason,
@JsonProperty("stop_sequence") String stopSequence,
@JsonProperty("usage") Usage usage) {
// @formatter:on
@JsonProperty("id") String id,
@JsonProperty("type") String type,
@JsonProperty("role") Role role,
@JsonProperty("content") List<ContentBlock> content,
@JsonProperty("model") String model,
@JsonProperty("stop_reason") String stopReason,
@JsonProperty("stop_sequence") String stopSequence,
@JsonProperty("usage") Usage usage) {
// @formatter:on
}
// CB DELTA EVENT
@@ -865,143 +885,143 @@ public class AnthropicApi {
@JsonInclude(Include.NON_NULL)
public record Usage(
// @formatter:off
@JsonProperty("input_tokens") Integer inputTokens,
@JsonProperty("output_tokens") Integer outputTokens) {
// @formatter:off
}
@JsonProperty("input_tokens") Integer inputTokens,
@JsonProperty("output_tokens") Integer outputTokens) {
// @formatter:off
}
/// ECB STOP
/// ECB STOP
/**
* Special event used to aggregate multiple tool use events into a single event with
* list of aggregated ContentBlockToolUse.
*/
public static class ToolUseAggregationEvent implements StreamEvent {
/**
* Special event used to aggregate multiple tool use events into a single event with
* list of aggregated ContentBlockToolUse.
*/
public static class ToolUseAggregationEvent implements StreamEvent {
private Integer index;
private Integer index;
private String id;
private String id;
private String name;
private String name;
private String partialJson = "";
private String partialJson = "";
private List<ContentBlockStartEvent.ContentBlockToolUse> toolContentBlocks = new ArrayList<>();
private List<ContentBlockStartEvent.ContentBlockToolUse> toolContentBlocks = new ArrayList<>();
@Override
public EventType type() {
return EventType.TOOL_USE_AGGREGATE;
}
@Override
public EventType type() {
return EventType.TOOL_USE_AGGREGATE;
}
/**
* Get tool content blocks.
* @return The tool content blocks.
*/
public List<ContentBlockStartEvent.ContentBlockToolUse> getToolContentBlocks() {
return this.toolContentBlocks;
}
/**
* Get tool content blocks.
* @return The tool content blocks.
*/
public List<ContentBlockStartEvent.ContentBlockToolUse> getToolContentBlocks() {
return this.toolContentBlocks;
}
/**
* Check if the event is empty.
* @return True if the event is empty, false otherwise.
*/
public boolean isEmpty() {
return (this.index == null || this.id == null || this.name == null
|| !StringUtils.hasText(this.partialJson));
}
/**
* Check if the event is empty.
* @return True if the event is empty, false otherwise.
*/
public boolean isEmpty() {
return (this.index == null || this.id == null || this.name == null
|| !StringUtils.hasText(this.partialJson));
}
ToolUseAggregationEvent withIndex(Integer index) {
this.index = index;
return this;
}
ToolUseAggregationEvent withIndex(Integer index) {
this.index = index;
return this;
}
ToolUseAggregationEvent withId(String id) {
this.id = id;
return this;
}
ToolUseAggregationEvent withId(String id) {
this.id = id;
return this;
}
ToolUseAggregationEvent withName(String name) {
this.name = name;
return this;
}
ToolUseAggregationEvent withName(String name) {
this.name = name;
return this;
}
ToolUseAggregationEvent appendPartialJson(String partialJson) {
this.partialJson = this.partialJson + partialJson;
return this;
}
ToolUseAggregationEvent appendPartialJson(String partialJson) {
this.partialJson = this.partialJson + partialJson;
return this;
}
void squashIntoContentBlock() {
Map<String, Object> map = (StringUtils.hasText(this.partialJson))
? ModelOptionsUtils.jsonToMap(this.partialJson) : Map.of();
this.toolContentBlocks.add(new ContentBlockStartEvent.ContentBlockToolUse("tool_use", this.id, this.name, map));
this.index = null;
this.id = null;
this.name = null;
this.partialJson = "";
}
void squashIntoContentBlock() {
Map<String, Object> map = (StringUtils.hasText(this.partialJson))
? ModelOptionsUtils.jsonToMap(this.partialJson) : Map.of();
this.toolContentBlocks.add(new ContentBlockStartEvent.ContentBlockToolUse("tool_use", this.id, this.name, map));
this.index = null;
this.id = null;
this.name = null;
this.partialJson = "";
}
@Override
public String toString() {
return "EventToolUseBuilder [index=" + this.index + ", id=" + this.id + ", name=" + this.name + ", partialJson="
+ this.partialJson + ", toolUseMap=" + this.toolContentBlocks + "]";
}
@Override
public String toString() {
return "EventToolUseBuilder [index=" + this.index + ", id=" + this.id + ", name=" + this.name + ", partialJson="
+ this.partialJson + ", toolUseMap=" + this.toolContentBlocks + "]";
}
}
}
///////////////////////////////////////
/// MESSAGE EVENTS
///////////////////////////////////////
///////////////////////////////////////
/// MESSAGE EVENTS
///////////////////////////////////////
// MESSAGE START EVENT
// MESSAGE START EVENT
/**
* Content block start event.
* @param type The event type.
* @param index The index of the content block.
* @param contentBlock The content block body.
*/
@JsonInclude(Include.NON_NULL)
public record ContentBlockStartEvent(
// @formatter:off
@JsonProperty("type") EventType type,
@JsonProperty("index") Integer index,
@JsonProperty("content_block") ContentBlockBody contentBlock) implements StreamEvent {
/**
* Content block start event.
* @param type The event type.
* @param index The index of the content block.
* @param contentBlock The content block body.
*/
@JsonInclude(Include.NON_NULL)
public record ContentBlockStartEvent(
// @formatter:off
@JsonProperty("type") EventType type,
@JsonProperty("index") Integer index,
@JsonProperty("content_block") ContentBlockBody contentBlock) implements StreamEvent {
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type",
visible = true)
@JsonSubTypes({ @JsonSubTypes.Type(value = ContentBlockToolUse.class, name = "tool_use"),
@JsonSubTypes.Type(value = ContentBlockText.class, name = "text") })
public interface ContentBlockBody {
String type();
}
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type",
visible = true)
@JsonSubTypes({ @JsonSubTypes.Type(value = ContentBlockToolUse.class, name = "tool_use"),
@JsonSubTypes.Type(value = ContentBlockText.class, name = "text") })
public interface ContentBlockBody {
String type();
}
/**
* Tool use content block.
* @param type The content block type.
* @param id The tool use id.
* @param name The tool use name.
* @param input The tool use input.
*/
@JsonInclude(Include.NON_NULL)
public record ContentBlockToolUse(
@JsonProperty("type") String type,
@JsonProperty("id") String id,
@JsonProperty("name") String name,
@JsonProperty("input") Map<String, Object> input) implements ContentBlockBody {
}
/**
* Tool use content block.
* @param type The content block type.
* @param id The tool use id.
* @param name The tool use name.
* @param input The tool use input.
*/
@JsonInclude(Include.NON_NULL)
public record ContentBlockToolUse(
@JsonProperty("type") String type,
@JsonProperty("id") String id,
@JsonProperty("name") String name,
@JsonProperty("input") Map<String, Object> input) implements ContentBlockBody {
}
/**
* Text content block.
* @param type The content block type.
* @param text The text content.
*/
@JsonInclude(Include.NON_NULL)
public record ContentBlockText(
@JsonProperty("type") String type,
@JsonProperty("text") String text) implements ContentBlockBody {
}
}
// @formatter:on
/**
* Text content block.
* @param type The content block type.
* @param text The text content.
*/
@JsonInclude(Include.NON_NULL)
public record ContentBlockText(
@JsonProperty("type") String type,
@JsonProperty("text") String text) implements ContentBlockBody {
}
}
// @formatter:on
// MESSAGE DELTA EVENT
@@ -1015,41 +1035,41 @@ public class AnthropicApi {
@JsonInclude(Include.NON_NULL)
public record ContentBlockDeltaEvent(
// @formatter:off
@JsonProperty("type") EventType type,
@JsonProperty("index") Integer index,
@JsonProperty("delta") ContentBlockDeltaBody delta) implements StreamEvent {
@JsonProperty("type") EventType type,
@JsonProperty("index") Integer index,
@JsonProperty("delta") ContentBlockDeltaBody delta) implements StreamEvent {
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type",
visible = true)
@JsonSubTypes({ @JsonSubTypes.Type(value = ContentBlockDeltaText.class, name = "text_delta"),
@JsonSubTypes.Type(value = ContentBlockDeltaJson.class, name = "input_json_delta") })
public interface ContentBlockDeltaBody {
String type();
}
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type",
visible = true)
@JsonSubTypes({ @JsonSubTypes.Type(value = ContentBlockDeltaText.class, name = "text_delta"),
@JsonSubTypes.Type(value = ContentBlockDeltaJson.class, name = "input_json_delta") })
public interface ContentBlockDeltaBody {
String type();
}
/**
* Text content block delta.
* @param type The content block type.
* @param text The text content.
*/
@JsonInclude(Include.NON_NULL)
public record ContentBlockDeltaText(
@JsonProperty("type") String type,
@JsonProperty("text") String text) implements ContentBlockDeltaBody {
}
/**
* Text content block delta.
* @param type The content block type.
* @param text The text content.
*/
@JsonInclude(Include.NON_NULL)
public record ContentBlockDeltaText(
@JsonProperty("type") String type,
@JsonProperty("text") String text) implements ContentBlockDeltaBody {
}
/**
* JSON content block delta.
* @param type The content block type.
* @param partialJson The partial JSON content.
*/
@JsonInclude(Include.NON_NULL)
public record ContentBlockDeltaJson(
@JsonProperty("type") String type,
@JsonProperty("partial_json") String partialJson) implements ContentBlockDeltaBody {
}
}
// @formatter:on
/**
* JSON content block delta.
* @param type The content block type.
* @param partialJson The partial JSON content.
*/
@JsonInclude(Include.NON_NULL)
public record ContentBlockDeltaJson(
@JsonProperty("type") String type,
@JsonProperty("partial_json") String partialJson) implements ContentBlockDeltaBody {
}
}
// @formatter:on
// MESSAGE STOP EVENT
@@ -1062,10 +1082,10 @@ public class AnthropicApi {
@JsonInclude(Include.NON_NULL)
public record ContentBlockStopEvent(
// @formatter:off
@JsonProperty("type") EventType type,
@JsonProperty("index") Integer index) implements StreamEvent {
}
// @formatter:on
@JsonProperty("type") EventType type,
@JsonProperty("index") Integer index) implements StreamEvent {
}
// @formatter:on
/**
* Message start event.
@@ -1075,10 +1095,10 @@ public class AnthropicApi {
*/
@JsonInclude(Include.NON_NULL)
public record MessageStartEvent(// @formatter:off
@JsonProperty("type") EventType type,
@JsonProperty("message") ChatCompletionResponse message) implements StreamEvent {
}
// @formatter:on
@JsonProperty("type") EventType type,
@JsonProperty("message") ChatCompletionResponse message) implements StreamEvent {
}
// @formatter:on
/**
* Message delta event.
@@ -1090,31 +1110,31 @@ public class AnthropicApi {
@JsonInclude(Include.NON_NULL)
public record MessageDeltaEvent(
// @formatter:off
@JsonProperty("type") EventType type,
@JsonProperty("delta") MessageDelta delta,
@JsonProperty("usage") MessageDeltaUsage usage) implements StreamEvent {
@JsonProperty("type") EventType type,
@JsonProperty("delta") MessageDelta delta,
@JsonProperty("usage") MessageDeltaUsage usage) implements StreamEvent {
/**
* Message delta.
* @param stopReason The stop reason.
* @param stopSequence The stop sequence.
*/
@JsonInclude(Include.NON_NULL)
public record MessageDelta(
@JsonProperty("stop_reason") String stopReason,
@JsonProperty("stop_sequence") String stopSequence) {
}
/**
* Message delta.
* @param stopReason The stop reason.
* @param stopSequence The stop sequence.
*/
@JsonInclude(Include.NON_NULL)
public record MessageDelta(
@JsonProperty("stop_reason") String stopReason,
@JsonProperty("stop_sequence") String stopSequence) {
}
/**
* Message delta usage.
* @param outputTokens The output tokens.
*/
@JsonInclude(Include.NON_NULL)
public record MessageDeltaUsage(
@JsonProperty("output_tokens") Integer outputTokens) {
}
}
// @formatter:on
/**
* Message delta usage.
* @param outputTokens The output tokens.
*/
@JsonInclude(Include.NON_NULL)
public record MessageDeltaUsage(
@JsonProperty("output_tokens") Integer outputTokens) {
}
}
// @formatter:on
/**
* Message stop event.
@@ -1124,9 +1144,9 @@ public class AnthropicApi {
@JsonInclude(Include.NON_NULL)
public record MessageStopEvent(
// @formatter:off
@JsonProperty("type") EventType type) implements StreamEvent {
}
// @formatter:on
@JsonProperty("type") EventType type) implements StreamEvent {
}
// @formatter:on
///////////////////////////////////////
/// ERROR EVENT
@@ -1140,21 +1160,21 @@ public class AnthropicApi {
@JsonInclude(Include.NON_NULL)
public record ErrorEvent(
// @formatter:off
@JsonProperty("type") EventType type,
@JsonProperty("error") Error error) implements StreamEvent {
@JsonProperty("type") EventType type,
@JsonProperty("error") Error error) implements StreamEvent {
/**
* Error body.
* @param type The error type.
* @param message The error message.
*/
@JsonInclude(Include.NON_NULL)
public record Error(
@JsonProperty("type") String type,
@JsonProperty("message") String message) {
}
}
// @formatter:on
/**
* Error body.
* @param type The error type.
* @param message The error message.
*/
@JsonInclude(Include.NON_NULL)
public record Error(
@JsonProperty("type") String type,
@JsonProperty("message") String message) {
}
}
// @formatter:on
///////////////////////////////////////
/// PING EVENT
@@ -1167,8 +1187,8 @@ public class AnthropicApi {
@JsonInclude(Include.NON_NULL)
public record PingEvent(
// @formatter:off
@JsonProperty("type") EventType type) implements StreamEvent {
}
// @formatter:on
@JsonProperty("type") EventType type) implements StreamEvent {
}
// @formatter:on
}
}

View File

@@ -49,6 +49,7 @@ import org.springframework.ai.converter.ListOutputConverter;
import org.springframework.ai.converter.MapOutputConverter;
import org.springframework.ai.model.Media;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.function.FunctionCallingOptionsBuilder.PortableFunctionCallingOptions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringBootConfiguration;
@@ -57,6 +58,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;
import org.springframework.util.StringUtils;
@@ -246,6 +248,23 @@ class AnthropicChatModelIT {
assertThat(response.getResult().getOutput().getContent()).contains("banan", "apple", "basket");
}
@Test
void multiModalityPdfTest() throws IOException {
var pdfData = new ClassPathResource("/spring-ai-reference-overview.pdf");
var userMessage = new UserMessage(
"You are a very professional document summarization specialist. Please summarize the given document.",
List.of(new Media(new MimeType("application", "pdf"), pdfData)));
var response = this.chatModel.call(new Prompt(List.of(userMessage),
PortableFunctionCallingOptions.builder()
.withModel(AnthropicApi.ChatModel.CLAUDE_3_5_SONNET.getName())
.build()));
assertThat(response.getResult().getOutput().getContent()).containsAnyOf("Spring AI", "portable API");
}
@Test
void functionCallTest() {
@@ -333,7 +352,8 @@ class AnthropicChatModelIT {
// @formatter:on
logger.info(response.toString());
validateChatResponseMetadata(response, model);
// Note, brittle test.
validateChatResponseMetadata(response, "claude-3-5-sonnet-20241022");
}
record ActorsFilmsRecord(String actor, List<String> movies) {

View File

@@ -258,8 +258,6 @@ class VertexAiGeminiChatModelIT {
var response = this.chatModel.call(new Prompt(List.of(userMessage)));
System.out.println(response.getResult().getOutput().getContent());
assertThat(response.getResult().getOutput().getContent()).containsAnyOf("Spring AI", "portable API");
}

View File

@@ -146,10 +146,12 @@ Read more about xref:api/chat/functions/anthropic-chat-functions.adoc[Anthropic
== Multimodal
Multimodality refers to a model's ability to simultaneously understand and process information from various sources, including text, images, audio, and other data formats. This paradigm represents a significant advancement in AI models.
Multimodality refers to a model's ability to simultaneously understand and process information from various sources, including text, pdf, images, data formats.
=== Images
Currently, Anthropic Claude 3 supports the `base64` source type for `images`, and the `image/jpeg`, `image/png`, `image/gif`, and `image/webp` media types.
Check the link:https://docs.anthropic.com/claude/docs/vision[Vision guide] for more information.
Anthropic Claude 3.5 Sonnet also supports the `pdf` source type for `application/pdf` files.
Spring AI's `Message` interface supports multimodal AI models by introducing the Media type.
This type contains data and information about media attachments in messages, using Spring's `org.springframework.util.MimeType` and a `java.lang.Object` for the raw media data.
@@ -176,14 +178,23 @@ along with the text message "Explain what do you see on this picture?", and gene
----
The image shows a close-up view of a wire fruit basket containing several pieces of fruit.
The basket appears to be made of thin metal wires formed into a round shape with an elevated handle.
...
----
Inside the basket, there are a few yellow bananas and a couple of red apples or possibly tomatoes.
The vibrant colors of the fruit contrast nicely against the metallic tones of the wire basket.
=== PDF
The shallow depth of field in the photograph puts the focus squarely on the fruit in the foreground, while the basket handle extending upwards is slightly blurred, creating a pleasing bokeh effect in the background.
Starting with Sonnet 3.5 https://docs.anthropic.com/en/docs/build-with-claude/pdf-support[PDF support (beta)] is provided.
Use the `application/pdf` media type to attach a PDF file to the message:
The composition and lighting give the image a clean, minimalist aesthetic that highlights the natural beauty and freshness of the fruit displayed in this elegant wire basket.
[source,java]
----
var pdfData = new ClassPathResource("/spring-ai-reference-overview.pdf");
var userMessage = new UserMessage(
"You are a very professional document summarization specialist. Please summarize the given document.",
List.of(new Media(new MimeType("application", "pdf"), pdfData)));
var response = this.chatModel.call(new Prompt(List.of(userMessage)));
----
== Sample Controller
@@ -195,7 +206,7 @@ Add a `application.properties` file, under the `src/main/resources` directory, t
[source,application.properties]
----
spring.ai.anthropic.api-key=YOUR_API_KEY
spring.ai.anthropic.chat.options.model=claude-3-5-sonnet-20241022
spring.ai.anthropic.chat.options.model=claude-3-5-sonnet-latest
spring.ai.anthropic.chat.options.temperature=0.7
spring.ai.anthropic.chat.options.max-tokens=450
----

View File

@@ -19,7 +19,7 @@ This table compares various Chat Models supported by Spring AI, detailing their
|====
| Provider | Multimodality ^| Tools/Functions ^| Streaming ^| Retry ^| Observability ^| Built-in JSON ^| Local ^| OpenAI API Compatible
| xref::api/chat/anthropic-chat.adoc[Anthropic Claude] | text, image ^a| image::yes.svg[width=16] ^a| image::yes.svg[width=16] ^a| image::yes.svg[width=16] ^a| image::yes.svg[width=16] ^a| image::no.svg[width=12] ^a| image::no.svg[width=12] ^a| image::no.svg[width=12]
| xref::api/chat/anthropic-chat.adoc[Anthropic Claude] | text, pdf, image ^a| image::yes.svg[width=16] ^a| image::yes.svg[width=16] ^a| image::yes.svg[width=16] ^a| image::yes.svg[width=16] ^a| image::no.svg[width=12] ^a| image::no.svg[width=12] ^a| image::no.svg[width=12]
| xref::api/chat/azure-openai-chat.adoc[Azure OpenAI] | text, image ^a| image::yes.svg[width=16] ^a| image::yes.svg[width=16] ^a| image::yes.svg[width=16] ^a| image::yes.svg[width=16] ^a| image::yes.svg[width=16] ^a| image::no.svg[width=12] ^a| image::yes.svg[width=16]
| xref::api/chat/vertexai-gemini-chat.adoc[Google VertexAI Gemini] | text, pdf, image, audio, video ^a| image::yes.svg[width=16] ^a| image::yes.svg[width=16] ^a| image::yes.svg[width=16] ^a| image::yes.svg[width=16] ^a| image::yes.svg[width=16] ^a| image::no.svg[width=12] ^a| image::yes.svg[width=16]
| xref::api/chat/groq-chat.adoc[Groq (OpenAI-proxy)] | text, image ^a| image::yes.svg[width=16] ^a| image::yes.svg[width=16] ^a| image::yes.svg[width=16] ^a| image::yes.svg[width=16] ^a| image::no.svg[width=12] ^a| image::no.svg[width=12] ^a| image::yes.svg[width=16]

View File

@@ -125,8 +125,8 @@ Read more about xref:api/chat/functions/vertexai-gemini-chat-functions.adoc[Vert
== Multimodal
Multimodality refers to a model's ability to simultaneously understand and process information from various sources, including `text`, `pdf`, `images`, `audio`, and other data formats.
This paradigm represents a significant advancement in AI models.
=== Image, Audio, Video
Google's Gemini AI models support this capability by comprehending and integrating text, code, audio, images, and video.
For more details, refer to the blog post https://blog.google/technology/ai/google-gemini-ai/#introducing-gemini[Introducing Gemini].
@@ -146,6 +146,23 @@ var userMessage = new UserMessage("Explain what do you see on this picture?",
ChatResponse response = chatModel.call(new Prompt(List.of(this.userMessage)));
----
=== PDF
Latest Vertex Gemini provides support for PDF input types..
Use the `application/pdf` media type to attach a PDF file to the message:
[source,java]
----
var pdfData = new ClassPathResource("/spring-ai-reference-overview.pdf");
var userMessage = new UserMessage(
"You are a very professional document summarization specialist. Please summarize the given document.",
List.of(new Media(new MimeType("application", "pdf"), pdfData)));
var response = this.chatModel.call(new Prompt(List.of(userMessage)));
----
== Sample Controller
https://start.spring.io/[Create] a new Spring Boot project and add the `spring-ai-vertex-ai-gemini-spring-boot-starter` to your pom (or gradle) dependencies.