Skip to content

Commit 5f6827e

Browse files
committed
Make ChatClient and Advisor APIs more robust - Part 1
- Introduce “ChatClientRequest” and “ChatClientResponse” for propagating requests/responses in a ChatClient advisor chain. - Structure a Prompt at the beginning of the chain, to ensure a consistent view across execution chain and observations. Any template is rendered at the beginning so that every advisor doesn’t have to do it again. - Improve observations to include the complete view of the prompt messages, instead of only considering userText and systemText. - Remove legacy “around” advisor type concept. - Keep backward compatibility for AdvisedRequest, AdvisedResponse, and legacy Advisor APIs. Relates to gh-2655 Signed-off-by: Thomas Vitale <[email protected]>
1 parent 257c91c commit 5f6827e

35 files changed

+1618
-453
lines changed

spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/ChatClient.java

+4
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,8 @@ interface CallResponseSpec {
156156
@Nullable
157157
<T> T entity(Class<T> type);
158158

159+
ChatClientResponse chatClientResponse();
160+
159161
@Nullable
160162
ChatResponse chatResponse();
161163

@@ -172,6 +174,8 @@ interface CallResponseSpec {
172174

173175
interface StreamResponseSpec {
174176

177+
Flux<ChatClientResponse> chatClientResponse();
178+
175179
Flux<ChatResponse> chatResponse();
176180

177181
Flux<String> content();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright 2023-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.ai.chat.client;
18+
19+
/**
20+
* Common attributes used in {@link ChatClient} context.
21+
*
22+
* @author Thomas Vitale
23+
* @since 1.0.0
24+
*/
25+
public enum ChatClientAttributes {
26+
27+
//@formatter:off
28+
29+
@Deprecated // Only for backward compatibility until the next release.
30+
ADVISORS("spring.ai.chat.client.advisors"),
31+
@Deprecated // Only for backward compatibility until the next release.
32+
CHAT_MODEL("spring.ai.chat.client.model"),
33+
OUTPUT_FORMAT("spring.ai.chat.client.output.format"),
34+
@Deprecated // Only for backward compatibility until the next release.
35+
USER_PARAMS("spring.ai.chat.client.user.params"),
36+
@Deprecated // Only for backward compatibility until the next release.
37+
SYSTEM_PARAMS("spring.ai.chat.client.system.params");
38+
39+
//@formatter:on
40+
41+
private final String key;
42+
43+
ChatClientAttributes(String key) {
44+
this.key = key;
45+
}
46+
47+
public String getKey() {
48+
return key;
49+
}
50+
51+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Copyright 2023-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.ai.chat.client;
18+
19+
import org.springframework.ai.chat.prompt.Prompt;
20+
import org.springframework.util.Assert;
21+
22+
import java.util.HashMap;
23+
import java.util.Map;
24+
25+
/**
26+
* Represents a request processed by a {@link ChatClient} that ultimately is used to build
27+
* a {@link Prompt} to be sent to an AI model.
28+
*
29+
* @param prompt The prompt to be sent to the AI model
30+
* @param context The contextual data through the execution chain
31+
* @author Thomas Vitale
32+
* @since 1.0.0
33+
*/
34+
public record ChatClientRequest(Prompt prompt, Map<String, Object> context) {
35+
36+
public ChatClientRequest {
37+
Assert.notNull(prompt, "prompt cannot be null");
38+
Assert.notNull(context, "context cannot be null");
39+
Assert.noNullElements(context.keySet(), "context keys cannot be null");
40+
}
41+
42+
public static Builder builder() {
43+
return new Builder();
44+
}
45+
46+
public static final class Builder {
47+
48+
private Prompt prompt;
49+
50+
private Map<String, Object> context = new HashMap<>();
51+
52+
private Builder() {
53+
}
54+
55+
public Builder prompt(Prompt prompt) {
56+
Assert.notNull(prompt, "prompt cannot be null");
57+
this.prompt = prompt;
58+
return this;
59+
}
60+
61+
public Builder context(Map<String, Object> context) {
62+
Assert.notNull(context, "context cannot be null");
63+
this.context.putAll(context);
64+
return this;
65+
}
66+
67+
public Builder context(String key, Object value) {
68+
Assert.notNull(key, "key cannot be null");
69+
this.context.put(key, value);
70+
return this;
71+
}
72+
73+
public ChatClientRequest build() {
74+
return new ChatClientRequest(prompt, context);
75+
}
76+
77+
}
78+
79+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Copyright 2023-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.ai.chat.client;
18+
19+
import org.springframework.ai.chat.model.ChatResponse;
20+
import org.springframework.lang.Nullable;
21+
import org.springframework.util.Assert;
22+
23+
import java.util.HashMap;
24+
import java.util.Map;
25+
26+
/**
27+
* Represents a response returned by a {@link ChatClient}.
28+
*
29+
* @param chatResponse The response returned by the AI model
30+
* @param context The contextual data propagated through the execution chain
31+
* @author Thomas Vitale
32+
* @since 1.0.0
33+
*/
34+
public record ChatClientResponse(@Nullable ChatResponse chatResponse, Map<String, Object> context) {
35+
36+
public ChatClientResponse {
37+
Assert.notNull(context, "context cannot be null");
38+
Assert.noNullElements(context.keySet(), "context keys cannot be null");
39+
}
40+
41+
public static Builder builder() {
42+
return new Builder();
43+
}
44+
45+
public static class Builder {
46+
47+
private ChatResponse chatResponse;
48+
49+
private Map<String, Object> context = new HashMap<>();
50+
51+
private Builder() {
52+
}
53+
54+
public Builder chatResponse(ChatResponse chatResponse) {
55+
this.chatResponse = chatResponse;
56+
return this;
57+
}
58+
59+
public Builder context(Map<String, Object> context) {
60+
Assert.notNull(context, "context cannot be null");
61+
this.context.putAll(context);
62+
return this;
63+
}
64+
65+
public Builder context(String key, Object value) {
66+
Assert.notNull(key, "key cannot be null");
67+
this.context.put(key, value);
68+
return this;
69+
}
70+
71+
public ChatClientResponse build() {
72+
return new ChatClientResponse(this.chatResponse, this.context);
73+
}
74+
75+
}
76+
77+
}

0 commit comments

Comments
 (0)