Skip to content

Commit 65b34c0

Browse files
committed
merge main branch and change class name
1 parent dc656ce commit 65b34c0

File tree

147 files changed

+7103
-3114
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

147 files changed

+7103
-3114
lines changed

README.md

Lines changed: 2 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -10,147 +10,14 @@ For further information go to our [Spring AI reference documentation](https://do
1010

1111
## Breaking changes
1212

13-
On our march to release 1.0.0 M1 we have made several breaking changes. Apologies, it is for the best!
14-
15-
**(22.05.2024)**
16-
17-
A major change was made that took the 'old' `ChatClient` and moved the functionality into `ChatModel`. The 'new' `ChatClient` now takes an instance of `ChatModel`. This was done do support a fluent API for creating and executing prompts in a style similar to other client classes in the Spring ecosystem, such as `RestClient`, `WebClient`, and `JdbcClient`. Refer to the [JavaDoc](https://docs.spring.io/spring-ai/docs/1.0.0-SNAPSHOT/api/) for more information on the Fluent API, proper reference documentation is coming shortly.
18-
19-
We renamed the 'old' `ModelClient` to `Model` and renamed implementing classes, for example `ImageClient` was renamed to `ImageModel`. The `Model` implementation represent the portability layer that converts between the Spring AI API and the underlying AI Model API.
20-
21-
### Adapting to the changes
22-
23-
NOTE: The `ChatClient` class is now in the package `org.springframework.ai.chat.client`
24-
25-
#### Approach 1
26-
27-
Now, instead of getting an Autoconfigured `ChatClient` instance, you will get a `ChatModel` instance. The `call` method signatures after renaming remain the same.
28-
To adapt your code should refactor you code to change use of the type `ChatClient` to `ChatModel`
29-
Here is an example of existing code before the change
30-
31-
```java
32-
@RestController
33-
public class OldSimpleAiController {
34-
35-
private final ChatClient chatClient;
36-
37-
public OldSimpleAiController(ChatClient chatClient) {
38-
this.chatClient = chatClient;
39-
}
40-
41-
@GetMapping("/ai/simple")
42-
Map<String, String> completion(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
43-
return Map.of("generation", chatClient.call(message));
44-
}
45-
}
46-
```
47-
48-
Now after the changes this will be
49-
50-
```java
51-
@RestController
52-
public class SimpleAiController {
53-
54-
private final ChatModel chatModel;
55-
56-
public SimpleAiController(ChatModel chatModel) {
57-
this.chatModel = chatModel;
58-
}
59-
60-
@GetMapping("/ai/simple")
61-
Map<String, String> completion(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
62-
return Map.of("generation", chatModel.call(message));
63-
}
64-
}
65-
```
66-
67-
NOTE: The renaming also applies to the classes
68-
* `StreamingChatClient` -> `StreamingChatModel`
69-
* `EmbeddingClient` -> `EmbeddingModel`
70-
* `ImageClient` -> `ImageModel`
71-
* `SpeechClient` -> `SpeechModel`
72-
* and similar for other `<XYZ>Client` classes
73-
74-
#### Approach 2
75-
76-
In this approach you will use the new fluent API available on the 'new' `ChatClient`
77-
78-
Here is an example of existing code before the change
79-
80-
```java
81-
@RestController
82-
class OldSimpleAiController {
83-
84-
ChatClient chatClient;
85-
86-
OldSimpleAiController(ChatClient chatClient) {
87-
this.chatClient = chatClient;
88-
}
89-
90-
@GetMapping("/ai/simple")
91-
Map<String, String> completion(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
92-
return Map.of(
93-
"generation",
94-
chatClient.call(message)
95-
);
96-
}
97-
}
98-
```
99-
100-
Now after the changes this will be
101-
102-
```java
103-
@RestController
104-
class SimpleAiController {
105-
106-
private final ChatClient chatClient;
107-
108-
SimpleAiController(ChatClient.Builder builder) {
109-
this.builder = builder.build();
110-
}
111-
112-
@GetMapping("/ai/simple")
113-
Map<String, String> completion(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
114-
return Map.of(
115-
"generation",
116-
chatClient.prompt().user(message).call().content()
117-
);
118-
}
119-
}
120-
```
121-
122-
123-
NOTE: The `ChatModel` instance is made available to you through autoconfiguration.
124-
125-
#### Approach 3
126-
127-
There is a tag in the GitHub repository called [v1.0.0-SNAPSHOT-before-chatclient-changes](https://github.com/spring-projects/spring-ai/tree/v1.0.0-SNAPSHOT-before-chatclient-changes) that you can checkout and do a local build to avoid updating any of your code until you are ready to migrate your code base.
128-
129-
```bash
130-
git checkout tags/v1.0.0-SNAPSHOT-before-chatclient-changes
131-
132-
./mvnw clean install -DskipTests
133-
```
134-
135-
136-
**(15.05.2024)**
137-
138-
Renamed POM artifact names:
139-
- spring-ai-qdrant -> spring-ai-qdrant-store
140-
- spring-ai-cassandra -> spring-ai-cassandra-store
141-
- spring-ai-pinecone -> spring-ai-pinecone-store
142-
- spring-ai-redis -> spring-ai-redis-store
143-
- spring-ai-qdrant -> spring-ai-qdrant-store
144-
- spring-ai-gemfire -> spring-ai-gemfire-store
145-
- spring-ai-azure-vector-store-spring-boot-starter -> spring-ai-azure-store-spring-boot-starter
146-
- spring-ai-redis-spring-boot-starter -> spring-ai-redis-store-spring-boot-starter
13+
* Refer to the [upgrade notes](https://docs.spring.io/spring-ai/reference/upgrade-notes.html) to see how to upgrade to 1.0.0.M1 or higher.
14714

14815
## Project Links
14916

15017
* [Documentation](https://docs.spring.io/spring-ai/reference/)
15118
* [Issues](https://github.com/spring-projects/spring-ai/issues)
15219
* [Discussions](https://github.com/spring-projects/spring-ai/discussions) - Go here if you have a question, suggestion, or feedback!
153-
* [Upgrade from 0.7.1-SNAPSHOT](https://docs.spring.io/spring-ai/reference/upgrade-notes.html)
20+
15421

15522
## Educational Resources
15623

models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/metadata/AnthropicChatResponseMetadata.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
import org.springframework.lang.Nullable;
2525
import org.springframework.util.Assert;
2626

27+
import java.util.HashMap;
28+
2729
/**
2830
* {@link ChatResponseMetadata} implementation for {@literal AnthropicApi}.
2931
*
@@ -33,7 +35,7 @@
3335
* @see Usage
3436
* @since 1.0.0
3537
*/
36-
public class AnthropicChatResponseMetadata implements ChatResponseMetadata {
38+
public class AnthropicChatResponseMetadata extends HashMap<String, Object> implements ChatResponseMetadata {
3739

3840
protected static final String AI_METADATA_STRING = "{ @type: %1$s, id: %2$s, usage: %3$s, rateLimit: %4$s }";
3941

models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiChatModel.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,8 +183,8 @@ public Flux<ChatResponse> stream(Prompt prompt) {
183183
isFunctionCall.set(false);
184184
return true;
185185
}
186-
return false;
187-
}, false)
186+
return !isFunctionCall.get();
187+
})
188188
.concatMapIterable(window -> {
189189
final var reduce = window.reduce(MergeUtils.emptyChatCompletions(), MergeUtils::mergeChatCompletions);
190190
return List.of(reduce);
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
package org.springframework.ai.azure.openai;
2+
3+
import com.azure.ai.openai.OpenAIClient;
4+
import com.azure.ai.openai.models.ImageGenerationOptions;
5+
import com.azure.ai.openai.models.ImageGenerationQuality;
6+
import com.azure.ai.openai.models.ImageGenerationResponseFormat;
7+
import com.azure.ai.openai.models.ImageGenerationStyle;
8+
import com.azure.ai.openai.models.ImageSize;
9+
import com.fasterxml.jackson.core.JsonProcessingException;
10+
import com.fasterxml.jackson.databind.DeserializationFeature;
11+
import com.fasterxml.jackson.databind.ObjectMapper;
12+
import com.fasterxml.jackson.databind.SerializationFeature;
13+
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
14+
import org.slf4j.Logger;
15+
import org.slf4j.LoggerFactory;
16+
import org.springframework.ai.azure.openai.metadata.AzureOpenAiImageGenerationMetadata;
17+
import org.springframework.ai.azure.openai.metadata.AzureOpenAiImageResponseMetadata;
18+
import org.springframework.ai.image.Image;
19+
import org.springframework.ai.image.ImageGeneration;
20+
import org.springframework.ai.image.ImageModel;
21+
import org.springframework.ai.image.ImagePrompt;
22+
import org.springframework.ai.image.ImageResponse;
23+
import org.springframework.ai.image.ImageResponseMetadata;
24+
import org.springframework.ai.model.ModelOptionsUtils;
25+
import org.springframework.beans.factory.annotation.Autowired;
26+
import org.springframework.util.Assert;
27+
28+
import java.util.List;
29+
30+
import static java.lang.String.format;
31+
32+
/**
33+
* {@link ImageModel} implementation for {@literal Microsoft Azure AI} backed by
34+
* {@link OpenAIClient}.
35+
*
36+
* @author Benoit Moussaud
37+
* @see ImageModel
38+
* @see com.azure.ai.openai.OpenAIClient
39+
* @since 1.0.0 M1
40+
*/
41+
public class AzureOpenAiImageModel implements ImageModel {
42+
43+
private static final String DEFAULT_DEPLOYMENT_NAME = AzureOpenAiImageOptions.DEFAULT_IMAGE_MODEL;
44+
45+
private final Logger logger = LoggerFactory.getLogger(getClass());
46+
47+
@Autowired
48+
private final OpenAIClient openAIClient;
49+
50+
private final AzureOpenAiImageOptions defaultOptions;
51+
52+
public AzureOpenAiImageModel(OpenAIClient openAIClient) {
53+
this(openAIClient, AzureOpenAiImageOptions.builder().withDeploymentName(DEFAULT_DEPLOYMENT_NAME).build());
54+
}
55+
56+
public AzureOpenAiImageModel(OpenAIClient microsoftOpenAiClient, AzureOpenAiImageOptions options) {
57+
Assert.notNull(microsoftOpenAiClient, "com.azure.ai.openai.OpenAIClient must not be null");
58+
Assert.notNull(options, "AzureOpenAiChatOptions must not be null");
59+
this.openAIClient = microsoftOpenAiClient;
60+
this.defaultOptions = options;
61+
}
62+
63+
public AzureOpenAiImageOptions getDefaultOptions() {
64+
return defaultOptions;
65+
}
66+
67+
@Override
68+
public ImageResponse call(ImagePrompt imagePrompt) {
69+
ImageGenerationOptions imageGenerationOptions = toOpenAiImageOptions(imagePrompt);
70+
String deploymentOrModelName = getDeploymentName(imagePrompt);
71+
if (logger.isTraceEnabled()) {
72+
logger.trace("Azure ImageGenerationOptions call {} with the following options : {} ", deploymentOrModelName,
73+
toPrettyJson(imageGenerationOptions));
74+
}
75+
76+
var images = openAIClient.getImageGenerations(deploymentOrModelName, imageGenerationOptions);
77+
78+
if (logger.isTraceEnabled()) {
79+
logger.trace("Azure ImageGenerations: {}", toPrettyJson(images));
80+
}
81+
82+
List<ImageGeneration> imageGenerations = images.getData().stream().map(entry -> {
83+
var image = new Image(entry.getUrl(), entry.getBase64Data());
84+
var metadata = new AzureOpenAiImageGenerationMetadata(entry.getRevisedPrompt());
85+
return new ImageGeneration(image, metadata);
86+
}).toList();
87+
88+
ImageResponseMetadata openAiImageResponseMetadata = AzureOpenAiImageResponseMetadata.from(images);
89+
return new ImageResponse(imageGenerations, openAiImageResponseMetadata);
90+
}
91+
92+
private String toPrettyJson(Object object) {
93+
ObjectMapper objectMapper = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
94+
.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
95+
.registerModule(new JavaTimeModule());
96+
try {
97+
return objectMapper.writeValueAsString(object);
98+
}
99+
catch (JsonProcessingException e) {
100+
return "JsonProcessingException:" + e + " [" + object.toString() + "]";
101+
}
102+
}
103+
104+
/**
105+
* Return the deployment-name if provided or use the model name.
106+
* @param prompt the image prompt
107+
* @return Return the deployment-name if provided or use the model name.
108+
*/
109+
private String getDeploymentName(ImagePrompt prompt) {
110+
var runtimeImageOptions = prompt.getOptions();
111+
112+
if (this.defaultOptions != null) {
113+
// Merge options fixed in beta7
114+
// https://github.com/Azure/azure-sdk-for-java/issues/38183
115+
runtimeImageOptions = ModelOptionsUtils.merge(runtimeImageOptions, this.defaultOptions,
116+
AzureOpenAiImageOptions.class);
117+
}
118+
119+
if (runtimeImageOptions != null) {
120+
if (runtimeImageOptions instanceof AzureOpenAiImageOptions runtimeAzureOpenAiImageOptions) {
121+
if (runtimeAzureOpenAiImageOptions.getDeploymentName() != null) {
122+
return runtimeAzureOpenAiImageOptions.getDeploymentName();
123+
}
124+
}
125+
126+
}
127+
128+
// By default the one provided in the image prompt
129+
return prompt.getOptions().getModel();
130+
131+
}
132+
133+
private ImageGenerationOptions toOpenAiImageOptions(ImagePrompt prompt) {
134+
135+
if (prompt.getInstructions().size() > 1) {
136+
throw new RuntimeException(format("implementation support 1 image instruction only, found %s",
137+
prompt.getInstructions().size()));
138+
}
139+
if (prompt.getInstructions().isEmpty()) {
140+
throw new RuntimeException("please provide image instruction, current is empty");
141+
}
142+
143+
var instructions = prompt.getInstructions().get(0).getText();
144+
var runtimeImageOptions = prompt.getOptions();
145+
ImageGenerationOptions imageGenerationOptions = new ImageGenerationOptions(instructions);
146+
147+
if (this.defaultOptions != null) {
148+
// Merge options fixed in beta7
149+
// https://github.com/Azure/azure-sdk-for-java/issues/38183
150+
runtimeImageOptions = ModelOptionsUtils.merge(runtimeImageOptions, this.defaultOptions,
151+
AzureOpenAiImageOptions.class);
152+
}
153+
154+
if (runtimeImageOptions != null) {
155+
// Handle portable image options
156+
if (runtimeImageOptions.getN() != null) {
157+
imageGenerationOptions.setN(runtimeImageOptions.getN());
158+
}
159+
if (runtimeImageOptions.getModel() != null) {
160+
imageGenerationOptions.setModel(runtimeImageOptions.getModel());
161+
}
162+
if (runtimeImageOptions.getResponseFormat() != null) {
163+
// b64_json or url
164+
imageGenerationOptions.setResponseFormat(
165+
ImageGenerationResponseFormat.fromString(runtimeImageOptions.getResponseFormat()));
166+
}
167+
if (runtimeImageOptions.getWidth() != null && runtimeImageOptions.getHeight() != null) {
168+
imageGenerationOptions.setSize(
169+
ImageSize.fromString(runtimeImageOptions.getWidth() + "x" + runtimeImageOptions.getHeight()));
170+
}
171+
172+
// Handle OpenAI specific image options
173+
if (runtimeImageOptions instanceof AzureOpenAiImageOptions runtimeAzureOpenAiImageOptions) {
174+
if (runtimeAzureOpenAiImageOptions.getQuality() != null) {
175+
imageGenerationOptions
176+
.setQuality(ImageGenerationQuality.fromString(runtimeAzureOpenAiImageOptions.getQuality()));
177+
}
178+
if (runtimeAzureOpenAiImageOptions.getStyle() != null) {
179+
imageGenerationOptions
180+
.setStyle(ImageGenerationStyle.fromString(runtimeAzureOpenAiImageOptions.getStyle()));
181+
}
182+
if (runtimeAzureOpenAiImageOptions.getUser() != null) {
183+
imageGenerationOptions.setUser(runtimeAzureOpenAiImageOptions.getUser());
184+
}
185+
}
186+
}
187+
return imageGenerationOptions;
188+
}
189+
190+
}

0 commit comments

Comments
 (0)