Skip to content

Commit 5e10f4e

Browse files
Merge branch 'master' into spring-ai/media-size-guard
2 parents 12629f2 + cad6172 commit 5e10f4e

7 files changed

Lines changed: 247 additions & 32 deletions

File tree

temporal-spring-ai/src/main/java/io/temporal/springai/chat/TemporalChatClient.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
import io.micrometer.observation.ObservationRegistry;
44
import io.temporal.springai.util.TemporalToolUtil;
55
import java.util.Map;
6+
import javax.annotation.Nullable;
67
import org.springframework.ai.chat.client.ChatClient;
78
import org.springframework.ai.chat.client.DefaultChatClient;
89
import org.springframework.ai.chat.client.DefaultChatClientBuilder;
910
import org.springframework.ai.chat.client.observation.ChatClientObservationConvention;
1011
import org.springframework.ai.chat.model.ChatModel;
11-
import org.springframework.lang.Nullable;
1212
import org.springframework.util.Assert;
1313

1414
/**

temporal-spring-ai/src/main/java/io/temporal/springai/mcp/ActivityMcpClient.java

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import io.temporal.workflow.Workflow;
77
import java.time.Duration;
88
import java.util.Map;
9+
import javax.annotation.Nullable;
910

1011
/**
1112
* A workflow-safe wrapper for MCP (Model Context Protocol) client operations.
@@ -48,6 +49,7 @@ public class ActivityMcpClient {
4849
public static final int DEFAULT_MAX_ATTEMPTS = 3;
4950

5051
private final McpClientActivity activity;
52+
@Nullable private final ActivityOptions baseOptions;
5153
private Map<String, McpSchema.ServerCapabilities> serverCapabilities;
5254
private Map<String, McpSchema.Implementation> clientInfo;
5355

@@ -57,7 +59,18 @@ public class ActivityMcpClient {
5759
* @param activity the activity stub for MCP operations
5860
*/
5961
public ActivityMcpClient(McpClientActivity activity) {
62+
this(activity, null);
63+
}
64+
65+
/**
66+
* Creates a new ActivityMcpClient. When {@code baseOptions} is non-null, {@link #callTool(String,
67+
* McpSchema.CallToolRequest, String)} rebuilds the activity stub with a per-call Summary on top
68+
* of those options. When null, the caller supplied a pre-built stub whose options we don't know,
69+
* so we call through it as-is and drop any requested summary.
70+
*/
71+
private ActivityMcpClient(McpClientActivity activity, @Nullable ActivityOptions baseOptions) {
6072
this.activity = activity;
73+
this.baseOptions = baseOptions;
6174
}
6275

6376
/**
@@ -81,14 +94,13 @@ public static ActivityMcpClient create() {
8194
* @return a new ActivityMcpClient
8295
*/
8396
public static ActivityMcpClient create(Duration timeout, int maxAttempts) {
84-
McpClientActivity activity =
85-
Workflow.newActivityStub(
86-
McpClientActivity.class,
87-
ActivityOptions.newBuilder()
88-
.setStartToCloseTimeout(timeout)
89-
.setRetryOptions(RetryOptions.newBuilder().setMaximumAttempts(maxAttempts).build())
90-
.build());
91-
return new ActivityMcpClient(activity);
97+
ActivityOptions options =
98+
ActivityOptions.newBuilder()
99+
.setStartToCloseTimeout(timeout)
100+
.setRetryOptions(RetryOptions.newBuilder().setMaximumAttempts(maxAttempts).build())
101+
.build();
102+
McpClientActivity activity = Workflow.newActivityStub(McpClientActivity.class, options);
103+
return new ActivityMcpClient(activity, options);
92104
}
93105

94106
/**
@@ -127,6 +139,32 @@ public Map<String, McpSchema.Implementation> getClientInfo() {
127139
* @return the tool call result
128140
*/
129141
public McpSchema.CallToolResult callTool(String clientName, McpSchema.CallToolRequest request) {
142+
return callTool(clientName, request, null);
143+
}
144+
145+
/**
146+
* Calls a tool on a specific MCP client, attaching the given activity Summary to the scheduled
147+
* activity so it renders meaningfully in the Temporal UI. Falls back to the base stub when no
148+
* {@link ActivityOptions} are known (e.g. when this client was constructed from a user-supplied
149+
* stub rather than one of the {@link #create} factories).
150+
*
151+
* @param clientName the name of the MCP client
152+
* @param request the tool call request
153+
* @param summary the activity Summary, or null to omit
154+
* @return the tool call result
155+
*/
156+
public McpSchema.CallToolResult callTool(
157+
String clientName, McpSchema.CallToolRequest request, @Nullable String summary) {
158+
// Overlay the summary onto a fresh stub only when both a summary is requested AND we have
159+
// a recipe to rebuild the stub from (baseOptions). If either is missing, fall through to
160+
// the cached activity — it already has baseOptions baked in if we knew them at construction.
161+
if (summary != null && baseOptions != null) {
162+
McpClientActivity stub =
163+
Workflow.newActivityStub(
164+
McpClientActivity.class,
165+
ActivityOptions.newBuilder(baseOptions).setSummary(summary).build());
166+
return stub.callTool(clientName, request);
167+
}
130168
return activity.callTool(clientName, request);
131169
}
132170

temporal-spring-ai/src/main/java/io/temporal/springai/mcp/McpToolCallback.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,8 @@ public String call(String toolInput) {
106106

107107
// Use the original tool name (not prefixed) when calling the MCP server
108108
McpSchema.CallToolRequest request = new McpSchema.CallToolRequest(tool.name(), arguments);
109-
McpSchema.CallToolResult result = client.callTool(clientName, request);
109+
String summary = "mcp: " + clientName + "." + tool.name();
110+
McpSchema.CallToolResult result = client.callTool(clientName, request, summary);
110111

111112
// Return the result as-is (including errors) so the AI can handle them.
112113
// For example, an "access denied" error lets the AI suggest a valid path.

temporal-spring-ai/src/main/java/io/temporal/springai/model/ActivityChatModel.java

Lines changed: 53 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import java.util.List;
1111
import java.util.Map;
1212
import java.util.stream.Collectors;
13+
import javax.annotation.Nullable;
1314
import org.springframework.ai.chat.messages.*;
1415
import org.springframework.ai.chat.metadata.ChatResponseMetadata;
1516
import org.springframework.ai.chat.model.ChatModel;
@@ -84,7 +85,8 @@ public class ActivityChatModel implements ChatModel {
8485
public static final int DEFAULT_MAX_ATTEMPTS = 3;
8586

8687
private final ChatModelActivity chatModelActivity;
87-
private final String modelName;
88+
@Nullable private final String modelName;
89+
@Nullable private final ActivityOptions baseOptions;
8890
private final ToolCallingManager toolCallingManager;
8991
private final ToolExecutionEligibilityPredicate toolExecutionEligibilityPredicate;
9092

@@ -94,7 +96,7 @@ public class ActivityChatModel implements ChatModel {
9496
* @param chatModelActivity the activity stub for calling the chat model
9597
*/
9698
public ActivityChatModel(ChatModelActivity chatModelActivity) {
97-
this(chatModelActivity, null);
99+
this(chatModelActivity, null, null);
98100
}
99101

100102
/**
@@ -103,9 +105,24 @@ public ActivityChatModel(ChatModelActivity chatModelActivity) {
103105
* @param chatModelActivity the activity stub for calling the chat model
104106
* @param modelName the name of the chat model to use, or null for default
105107
*/
106-
public ActivityChatModel(ChatModelActivity chatModelActivity, String modelName) {
108+
public ActivityChatModel(ChatModelActivity chatModelActivity, @Nullable String modelName) {
109+
this(chatModelActivity, modelName, null);
110+
}
111+
112+
/**
113+
* Internal constructor used by {@link #forModel(String, Duration, int)} and friends. When {@code
114+
* baseOptions} is non-null, each call rebuilds the activity stub with a per-call Summary on top
115+
* of those options so the Temporal UI can label the chat activity meaningfully. When null, the
116+
* caller supplied a pre-built stub whose options we don't know, so we call through it as-is
117+
* without a summary.
118+
*/
119+
private ActivityChatModel(
120+
ChatModelActivity chatModelActivity,
121+
@Nullable String modelName,
122+
@Nullable ActivityOptions baseOptions) {
107123
this.chatModelActivity = chatModelActivity;
108124
this.modelName = modelName;
125+
this.baseOptions = baseOptions;
109126
this.toolCallingManager = ToolCallingManager.builder().build();
110127
this.toolExecutionEligibilityPredicate = new DefaultToolExecutionEligibilityPredicate();
111128
}
@@ -150,22 +167,22 @@ public static ActivityChatModel forModel(String modelName) {
150167
* @param maxAttempts the maximum number of retry attempts
151168
* @return an ActivityChatModel for the specified chat model
152169
*/
153-
public static ActivityChatModel forModel(String modelName, Duration timeout, int maxAttempts) {
154-
ChatModelActivity activity =
155-
Workflow.newActivityStub(
156-
ChatModelActivity.class,
157-
ActivityOptions.newBuilder()
158-
.setStartToCloseTimeout(timeout)
159-
.setRetryOptions(RetryOptions.newBuilder().setMaximumAttempts(maxAttempts).build())
160-
.build());
161-
return new ActivityChatModel(activity, modelName);
170+
public static ActivityChatModel forModel(
171+
@Nullable String modelName, Duration timeout, int maxAttempts) {
172+
ActivityOptions options =
173+
ActivityOptions.newBuilder()
174+
.setStartToCloseTimeout(timeout)
175+
.setRetryOptions(RetryOptions.newBuilder().setMaximumAttempts(maxAttempts).build())
176+
.build();
177+
ChatModelActivity activity = Workflow.newActivityStub(ChatModelActivity.class, options);
178+
return new ActivityChatModel(activity, modelName, options);
162179
}
163180

164181
/**
165-
* Returns the name of the chat model this instance uses.
166-
*
167-
* @return the model name, or null if using the default model
182+
* Returns the name of the chat model this instance uses, or null if it uses the plugin default
183+
* (the {@code @Primary} {@code ChatModel} bean or the first one registered).
168184
*/
185+
@Nullable
169186
public String getModelName() {
170187
return modelName;
171188
}
@@ -193,7 +210,8 @@ public ChatResponse call(Prompt prompt) {
193210
private ChatResponse internalCall(Prompt prompt) {
194211
// Convert prompt to activity input and call the activity
195212
ChatModelTypes.ChatModelActivityInput input = createActivityInput(prompt);
196-
ChatModelTypes.ChatModelActivityOutput output = chatModelActivity.callChatModel(input);
213+
ChatModelActivity stub = stubForCall(prompt);
214+
ChatModelTypes.ChatModelActivityOutput output = stub.callChatModel(input);
197215

198216
// Convert activity output to ChatResponse
199217
ChatResponse response = toResponse(output);
@@ -219,6 +237,25 @@ private ChatResponse internalCall(Prompt prompt) {
219237
return response;
220238
}
221239

240+
private ChatModelActivity stubForCall(Prompt prompt) {
241+
if (baseOptions == null) {
242+
return chatModelActivity;
243+
}
244+
ActivityOptions withSummary =
245+
ActivityOptions.newBuilder(baseOptions).setSummary(buildSummary()).build();
246+
return Workflow.newActivityStub(ChatModelActivity.class, withSummary);
247+
}
248+
249+
/**
250+
* Builds the activity Summary. Intentionally omits the user prompt — including even a truncated
251+
* slice would leak whatever the prompt contains (PII, secrets, internal identifiers) into
252+
* workflow history, server logs, and the Temporal UI, which is a surprising default for a plain
253+
* observability label.
254+
*/
255+
private String buildSummary() {
256+
return "chat: " + (modelName != null ? modelName : "default");
257+
}
258+
222259
private ChatModelTypes.ChatModelActivityInput createActivityInput(Prompt prompt) {
223260
// Convert messages
224261
List<ChatModelTypes.Message> messages =

temporal-spring-ai/src/main/java/io/temporal/springai/model/ChatModelTypes.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import io.temporal.failure.ApplicationFailure;
88
import java.time.Duration;
99
import java.util.List;
10+
import javax.annotation.Nullable;
1011

1112
/**
1213
* Serializable types for chat model activity requests and responses.
@@ -65,21 +66,23 @@ private ChatModelTypes() {}
6566
/**
6667
* Input to the chat model activity.
6768
*
68-
* @param modelName the name of the chat model bean to use (null for default)
69+
* @param modelName the name of the chat model bean to use, or null for the activity-side default
70+
* model
6971
* @param messages the conversation messages
70-
* @param modelOptions options for the chat model (temperature, max tokens, etc.)
72+
* @param modelOptions options for the chat model (temperature, max tokens, etc.), or null to use
73+
* the chat model's own defaults
7174
* @param tools tool definitions the model may call
7275
*/
7376
@JsonInclude(JsonInclude.Include.NON_NULL)
7477
@JsonIgnoreProperties(ignoreUnknown = true)
7578
public record ChatModelActivityInput(
76-
@JsonProperty("model_name") String modelName,
79+
@JsonProperty("model_name") @Nullable String modelName,
7780
@JsonProperty("messages") List<Message> messages,
78-
@JsonProperty("model_options") ModelOptions modelOptions,
81+
@JsonProperty("model_options") @Nullable ModelOptions modelOptions,
7982
@JsonProperty("tools") List<FunctionTool> tools) {
8083
/** Creates input for the default chat model. */
8184
public ChatModelActivityInput(
82-
List<Message> messages, ModelOptions modelOptions, List<FunctionTool> tools) {
85+
List<Message> messages, @Nullable ModelOptions modelOptions, List<FunctionTool> tools) {
8386
this(null, messages, modelOptions, tools);
8487
}
8588
}

temporal-spring-ai/src/main/java/io/temporal/springai/plugin/SpringAiPlugin.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
import java.util.LinkedHashMap;
88
import java.util.Map;
99
import javax.annotation.Nonnull;
10+
import javax.annotation.Nullable;
1011
import org.slf4j.Logger;
1112
import org.slf4j.LoggerFactory;
1213
import org.springframework.ai.chat.model.ChatModel;
13-
import org.springframework.lang.Nullable;
1414

1515
/**
1616
* Core Temporal plugin that registers {@link io.temporal.springai.activity.ChatModelActivity} with

0 commit comments

Comments
 (0)