Skip to content

feat: Add metadata support to PromptUserSpec and PromptSystemSpec in ChatClient #3989

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ interface PromptUserSpec {

PromptUserSpec media(MimeType mimeType, Resource resource);

PromptUserSpec metadata(Map<String, Object> metadata);

PromptUserSpec metadata(String k, Object v);

}

/**
Expand All @@ -131,6 +135,10 @@ interface PromptSystemSpec {

PromptSystemSpec param(String k, Object v);

PromptSystemSpec metadata(Map<String, Object> metadata);

PromptSystemSpec metadata(String k, Object v);

}

interface AdvisorSpec {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ public static class DefaultPromptUserSpec implements PromptUserSpec {

private final Map<String, Object> params = new HashMap<>();

private final Map<String, Object> metadata = new HashMap<>();

private final List<Media> media = new ArrayList<>();

@Nullable
Expand Down Expand Up @@ -212,6 +214,23 @@ public PromptUserSpec params(Map<String, Object> params) {
return this;
}

@Override
public PromptUserSpec metadata(Map<String, Object> metadata) {
Assert.notNull(metadata, "metadata cannot be null");
Assert.noNullElements(metadata.keySet(), "metadata keys cannot contain null elements");
Assert.noNullElements(metadata.values(), "metadata values cannot contain null elements");
this.metadata.putAll(metadata);
return this;
}

@Override
public PromptUserSpec metadata(String key, Object value) {
Assert.hasText(key, "metadata key cannot be null or empty");
Assert.notNull(value, "metadata value cannot be null");
this.metadata.put(key, value);
return this;
}

@Nullable
protected String text() {
return this.text;
Expand All @@ -225,12 +244,18 @@ protected List<Media> media() {
return this.media;
}

protected Map<String, Object> metadata() {
return this.metadata;
}

}

public static class DefaultPromptSystemSpec implements PromptSystemSpec {

private final Map<String, Object> params = new HashMap<>();

private final Map<String, Object> metadata = new HashMap<>();

@Nullable
private String text;

Expand Down Expand Up @@ -278,6 +303,23 @@ public PromptSystemSpec params(Map<String, Object> params) {
return this;
}

@Override
public PromptSystemSpec metadata(Map<String, Object> metadata) {
Assert.notNull(metadata, "metadata cannot be null");
Assert.noNullElements(metadata.keySet(), "metadata keys cannot contain null elements");
Assert.noNullElements(metadata.values(), "metadata values cannot contain null elements");
this.metadata.putAll(metadata);
return this;
}

@Override
public PromptSystemSpec metadata(String key, Object value) {
Assert.hasText(key, "metadata key cannot be null or empty");
Assert.notNull(value, "metadata value cannot be null");
this.metadata.put(key, value);
return this;
}

@Nullable
protected String text() {
return this.text;
Expand All @@ -287,6 +329,10 @@ protected Map<String, Object> params() {
return this.params;
}

protected Map<String, Object> metadata() {
return this.metadata;
}

}

public static class DefaultAdvisorSpec implements AdvisorSpec {
Expand Down Expand Up @@ -579,8 +625,12 @@ public static class DefaultChatClientRequestSpec implements ChatClientRequestSpe

private final Map<String, Object> userParams = new HashMap<>();

private final Map<String, Object> userMetadata = new HashMap<>();

private final Map<String, Object> systemParams = new HashMap<>();

private final Map<String, Object> systemMetadata = new HashMap<>();

private final List<Advisor> advisors = new ArrayList<>();

private final Map<String, Object> advisorParams = new HashMap<>();
Expand All @@ -600,22 +650,25 @@ public static class DefaultChatClientRequestSpec implements ChatClientRequestSpe

/* copy constructor */
DefaultChatClientRequestSpec(DefaultChatClientRequestSpec ccr) {
this(ccr.chatModel, ccr.userText, ccr.userParams, ccr.systemText, ccr.systemParams, ccr.toolCallbacks,
ccr.messages, ccr.toolNames, ccr.media, ccr.chatOptions, ccr.advisors, ccr.advisorParams,
ccr.observationRegistry, ccr.observationConvention, ccr.toolContext, ccr.templateRenderer);
this(ccr.chatModel, ccr.userText, ccr.userParams, ccr.userMetadata, ccr.systemText, ccr.systemParams,
ccr.systemMetadata, ccr.toolCallbacks, ccr.messages, ccr.toolNames, ccr.media, ccr.chatOptions,
ccr.advisors, ccr.advisorParams, ccr.observationRegistry, ccr.observationConvention,
ccr.toolContext, ccr.templateRenderer);
}

public DefaultChatClientRequestSpec(ChatModel chatModel, @Nullable String userText,
Map<String, Object> userParams, @Nullable String systemText, Map<String, Object> systemParams,
List<ToolCallback> toolCallbacks, List<Message> messages, List<String> toolNames, List<Media> media,
@Nullable ChatOptions chatOptions, List<Advisor> advisors, Map<String, Object> advisorParams,
ObservationRegistry observationRegistry,
Map<String, Object> userParams, Map<String, Object> userMetadata, @Nullable String systemText,
Map<String, Object> systemParams, Map<String, Object> systemMetadata, List<ToolCallback> toolCallbacks,
List<Message> messages, List<String> toolNames, List<Media> media, @Nullable ChatOptions chatOptions,
List<Advisor> advisors, Map<String, Object> advisorParams, ObservationRegistry observationRegistry,
@Nullable ChatClientObservationConvention observationConvention, Map<String, Object> toolContext,
@Nullable TemplateRenderer templateRenderer) {

Assert.notNull(chatModel, "chatModel cannot be null");
Assert.notNull(userParams, "userParams cannot be null");
Assert.notNull(userMetadata, "userMetadata cannot be null");
Assert.notNull(systemParams, "systemParams cannot be null");
Assert.notNull(systemMetadata, "systemMetadata cannot be null");
Assert.notNull(toolCallbacks, "toolCallbacks cannot be null");
Assert.notNull(messages, "messages cannot be null");
Assert.notNull(toolNames, "toolNames cannot be null");
Expand All @@ -631,8 +684,11 @@ public DefaultChatClientRequestSpec(ChatModel chatModel, @Nullable String userTe

this.userText = userText;
this.userParams.putAll(userParams);
this.userMetadata.putAll(userMetadata);

this.systemText = systemText;
this.systemParams.putAll(systemParams);
this.systemMetadata.putAll(systemMetadata);

this.toolNames.addAll(toolNames);
this.toolCallbacks.addAll(toolCallbacks);
Expand All @@ -656,6 +712,10 @@ public Map<String, Object> getUserParams() {
return this.userParams;
}

public Map<String, Object> getUserMetadata() {
return this.userMetadata;
}

@Nullable
public String getSystemText() {
return this.systemText;
Expand All @@ -665,6 +725,10 @@ public Map<String, Object> getSystemParams() {
return this.systemParams;
}

public Map<String, Object> getSystemMetadata() {
return this.systemMetadata;
}

@Nullable
public ChatOptions getChatOptions() {
return this.chatOptions;
Expand Down Expand Up @@ -720,12 +784,15 @@ public Builder mutate() {
}

if (StringUtils.hasText(this.userText)) {
builder.defaultUser(
u -> u.text(this.userText).params(this.userParams).media(this.media.toArray(new Media[0])));
builder.defaultUser(u -> u.text(this.userText)
.params(this.userParams)
.media(this.media.toArray(new Media[0]))
.metadata(this.userMetadata));
}

if (StringUtils.hasText(this.systemText)) {
builder.defaultSystem(s -> s.text(this.systemText).params(this.systemParams));
builder.defaultSystem(
s -> s.text(this.systemText).params(this.systemParams).metadata(this.systemMetadata));
}

if (this.chatOptions != null) {
Expand All @@ -737,6 +804,7 @@ public Builder mutate() {
return builder;
}

@Override
public ChatClientRequestSpec advisors(Consumer<ChatClient.AdvisorSpec> consumer) {
Assert.notNull(consumer, "consumer cannot be null");
var advisorSpec = new DefaultAdvisorSpec();
Expand All @@ -746,27 +814,31 @@ public ChatClientRequestSpec advisors(Consumer<ChatClient.AdvisorSpec> consumer)
return this;
}

@Override
public ChatClientRequestSpec advisors(Advisor... advisors) {
Assert.notNull(advisors, "advisors cannot be null");
Assert.noNullElements(advisors, "advisors cannot contain null elements");
this.advisors.addAll(Arrays.asList(advisors));
return this;
}

@Override
public ChatClientRequestSpec advisors(List<Advisor> advisors) {
Assert.notNull(advisors, "advisors cannot be null");
Assert.noNullElements(advisors, "advisors cannot contain null elements");
this.advisors.addAll(advisors);
return this;
}

@Override
public ChatClientRequestSpec messages(Message... messages) {
Assert.notNull(messages, "messages cannot be null");
Assert.noNullElements(messages, "messages cannot contain null elements");
this.messages.addAll(List.of(messages));
return this;
}

@Override
public ChatClientRequestSpec messages(List<Message> messages) {
Assert.notNull(messages, "messages cannot be null");
Assert.noNullElements(messages, "messages cannot contain null elements");
Expand Down Expand Up @@ -822,6 +894,7 @@ public ChatClientRequestSpec toolCallbacks(ToolCallbackProvider... toolCallbackP
return this;
}

@Override
public ChatClientRequestSpec toolContext(Map<String, Object> toolContext) {
Assert.notNull(toolContext, "toolContext cannot be null");
Assert.noNullElements(toolContext.keySet(), "toolContext keys cannot contain null elements");
Expand All @@ -830,12 +903,14 @@ public ChatClientRequestSpec toolContext(Map<String, Object> toolContext) {
return this;
}

@Override
public ChatClientRequestSpec system(String text) {
Assert.hasText(text, "text cannot be null or empty");
this.systemText = text;
return this;
}

@Override
public ChatClientRequestSpec system(Resource text, Charset charset) {
Assert.notNull(text, "text cannot be null");
Assert.notNull(charset, "charset cannot be null");
Expand All @@ -849,28 +924,32 @@ public ChatClientRequestSpec system(Resource text, Charset charset) {
return this;
}

@Override
public ChatClientRequestSpec system(Resource text) {
Assert.notNull(text, "text cannot be null");
return this.system(text, Charset.defaultCharset());
}

@Override
public ChatClientRequestSpec system(Consumer<PromptSystemSpec> consumer) {
Assert.notNull(consumer, "consumer cannot be null");

var systemSpec = new DefaultPromptSystemSpec();
consumer.accept(systemSpec);
this.systemText = StringUtils.hasText(systemSpec.text()) ? systemSpec.text() : this.systemText;
this.systemParams.putAll(systemSpec.params());

this.systemMetadata.putAll(systemSpec.metadata());
return this;
}

@Override
public ChatClientRequestSpec user(String text) {
Assert.hasText(text, "text cannot be null or empty");
this.userText = text;
return this;
}

@Override
public ChatClientRequestSpec user(Resource text, Charset charset) {
Assert.notNull(text, "text cannot be null");
Assert.notNull(charset, "charset cannot be null");
Expand All @@ -884,11 +963,13 @@ public ChatClientRequestSpec user(Resource text, Charset charset) {
return this;
}

@Override
public ChatClientRequestSpec user(Resource text) {
Assert.notNull(text, "text cannot be null");
return this.user(text, Charset.defaultCharset());
}

@Override
public ChatClientRequestSpec user(Consumer<PromptUserSpec> consumer) {
Assert.notNull(consumer, "consumer cannot be null");

Expand All @@ -897,21 +978,25 @@ public ChatClientRequestSpec user(Consumer<PromptUserSpec> consumer) {
this.userText = StringUtils.hasText(us.text()) ? us.text() : this.userText;
this.userParams.putAll(us.params());
this.media.addAll(us.media());
this.userMetadata.putAll(us.metadata());
return this;
}

@Override
public ChatClientRequestSpec templateRenderer(TemplateRenderer templateRenderer) {
Assert.notNull(templateRenderer, "templateRenderer cannot be null");
this.templateRenderer = templateRenderer;
return this;
}

@Override
public CallResponseSpec call() {
BaseAdvisorChain advisorChain = buildAdvisorChain();
return new DefaultCallResponseSpec(DefaultChatClientUtils.toChatClientRequest(this), advisorChain,
this.observationRegistry, this.observationConvention);
}

@Override
public StreamResponseSpec stream() {
BaseAdvisorChain advisorChain = buildAdvisorChain();
return new DefaultStreamResponseSpec(DefaultChatClientUtils.toChatClientRequest(this), advisorChain,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ public DefaultChatClientBuilder(ChatModel chatModel, ObservationRegistry observa
@Nullable ChatClientObservationConvention customObservationConvention) {
Assert.notNull(chatModel, "the " + ChatModel.class.getName() + " must be non-null");
Assert.notNull(observationRegistry, "the " + ObservationRegistry.class.getName() + " must be non-null");
this.defaultRequest = new DefaultChatClientRequestSpec(chatModel, null, Map.of(), null, Map.of(), List.of(),
List.of(), List.of(), List.of(), null, List.of(), Map.of(), observationRegistry,
this.defaultRequest = new DefaultChatClientRequestSpec(chatModel, null, Map.of(), Map.of(), null, Map.of(),
Map.of(), List.of(), List.of(), List.of(), List.of(), null, List.of(), Map.of(), observationRegistry,
customObservationConvention, Map.of(), null);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,10 @@ static ChatClientRequest toChatClientRequest(DefaultChatClient.DefaultChatClient
.build()
.render();
}
processedMessages.add(new SystemMessage(processedSystemText));
processedMessages.add(SystemMessage.builder()
.text(processedSystemText)
.metadata(inputRequest.getSystemMetadata())
.build());
}

// Messages => In the middle of the list
Expand All @@ -86,7 +89,11 @@ static ChatClientRequest toChatClientRequest(DefaultChatClient.DefaultChatClient
.build()
.render();
}
processedMessages.add(UserMessage.builder().text(processedUserText).media(inputRequest.getMedia()).build());
processedMessages.add(UserMessage.builder()
.text(processedUserText)
.media(inputRequest.getMedia())
.metadata(inputRequest.getUserMetadata())
.build());
}

/*
Expand Down
Loading