Skip to content

Commit c8d9717

Browse files
temporal-spring-ai: attach activity summaries for chat and MCP calls
ActivityChatModel.forModel() now stores the ActivityOptions it built and, on each chat call, rebuilds the stub with a per-call Summary of the form "chat: <model> · <first 60 chars of user prompt>". When a caller passes a pre-built stub directly via the public constructors, behavior is unchanged (no options known → no summary overlay). ActivityMcpClient.create() does the same and adds a callTool(clientName, request, summary) overload. McpToolCallback passes "mcp: <client>.<tool>". Also fixes the activity-type-name casing in ActivitySummaryTest — Temporal capitalizes the first character of method-name-derived activity types, so the event carries "CallChatModel", not "callChatModel". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 62c3e0e commit c8d9717

4 files changed

Lines changed: 98 additions & 23 deletions

File tree

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

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public class ActivityMcpClient {
4848
public static final int DEFAULT_MAX_ATTEMPTS = 3;
4949

5050
private final McpClientActivity activity;
51+
private final ActivityOptions baseOptions;
5152
private Map<String, McpSchema.ServerCapabilities> serverCapabilities;
5253
private Map<String, McpSchema.Implementation> clientInfo;
5354

@@ -57,7 +58,17 @@ public class ActivityMcpClient {
5758
* @param activity the activity stub for MCP operations
5859
*/
5960
public ActivityMcpClient(McpClientActivity activity) {
61+
this(activity, null);
62+
}
63+
64+
/**
65+
* Creates a new ActivityMcpClient. When {@code baseOptions} is non-null, {@link #callTool(String,
66+
* McpSchema.CallToolRequest, String)} rebuilds the activity stub with a per-call Summary on top
67+
* of those options.
68+
*/
69+
private ActivityMcpClient(McpClientActivity activity, ActivityOptions baseOptions) {
6070
this.activity = activity;
71+
this.baseOptions = baseOptions;
6172
}
6273

6374
/**
@@ -81,14 +92,13 @@ public static ActivityMcpClient create() {
8192
* @return a new ActivityMcpClient
8293
*/
8394
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);
95+
ActivityOptions options =
96+
ActivityOptions.newBuilder()
97+
.setStartToCloseTimeout(timeout)
98+
.setRetryOptions(RetryOptions.newBuilder().setMaximumAttempts(maxAttempts).build())
99+
.build();
100+
McpClientActivity activity = Workflow.newActivityStub(McpClientActivity.class, options);
101+
return new ActivityMcpClient(activity, options);
92102
}
93103

94104
/**
@@ -127,7 +137,30 @@ public Map<String, McpSchema.Implementation> getClientInfo() {
127137
* @return the tool call result
128138
*/
129139
public McpSchema.CallToolResult callTool(String clientName, McpSchema.CallToolRequest request) {
130-
return activity.callTool(clientName, request);
140+
return callTool(clientName, request, null);
141+
}
142+
143+
/**
144+
* Calls a tool on a specific MCP client, attaching the given activity Summary to the scheduled
145+
* activity so it renders meaningfully in the Temporal UI. Falls back to the base stub when no
146+
* {@link ActivityOptions} are known (e.g. when this client was constructed from a user-supplied
147+
* stub rather than one of the {@link #create} factories).
148+
*
149+
* @param clientName the name of the MCP client
150+
* @param request the tool call request
151+
* @param summary the activity Summary, or null to omit
152+
* @return the tool call result
153+
*/
154+
public McpSchema.CallToolResult callTool(
155+
String clientName, McpSchema.CallToolRequest request, String summary) {
156+
if (summary == null || baseOptions == null) {
157+
return activity.callTool(clientName, request);
158+
}
159+
McpClientActivity stub =
160+
Workflow.newActivityStub(
161+
McpClientActivity.class,
162+
ActivityOptions.newBuilder(baseOptions).setSummary(summary).build());
163+
return stub.callTool(clientName, request);
131164
}
132165

133166
/**

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: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ public class ActivityChatModel implements ChatModel {
8585

8686
private final ChatModelActivity chatModelActivity;
8787
private final String modelName;
88+
private final ActivityOptions baseOptions;
8889
private final ToolCallingManager toolCallingManager;
8990
private final ToolExecutionEligibilityPredicate toolExecutionEligibilityPredicate;
9091

@@ -94,7 +95,7 @@ public class ActivityChatModel implements ChatModel {
9495
* @param chatModelActivity the activity stub for calling the chat model
9596
*/
9697
public ActivityChatModel(ChatModelActivity chatModelActivity) {
97-
this(chatModelActivity, null);
98+
this(chatModelActivity, null, null);
9899
}
99100

100101
/**
@@ -104,8 +105,19 @@ public ActivityChatModel(ChatModelActivity chatModelActivity) {
104105
* @param modelName the name of the chat model to use, or null for default
105106
*/
106107
public ActivityChatModel(ChatModelActivity chatModelActivity, String modelName) {
108+
this(chatModelActivity, modelName, null);
109+
}
110+
111+
/**
112+
* Internal constructor used by {@link #forModel(String, Duration, int)} and friends. When {@code
113+
* baseOptions} is non-null, each call rebuilds the activity stub with a per-call Summary on top
114+
* of those options so the Temporal UI can label the chat activity meaningfully.
115+
*/
116+
private ActivityChatModel(
117+
ChatModelActivity chatModelActivity, String modelName, ActivityOptions baseOptions) {
107118
this.chatModelActivity = chatModelActivity;
108119
this.modelName = modelName;
120+
this.baseOptions = baseOptions;
109121
this.toolCallingManager = ToolCallingManager.builder().build();
110122
this.toolExecutionEligibilityPredicate = new DefaultToolExecutionEligibilityPredicate();
111123
}
@@ -151,14 +163,13 @@ public static ActivityChatModel forModel(String modelName) {
151163
* @return an ActivityChatModel for the specified chat model
152164
*/
153165
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);
166+
ActivityOptions options =
167+
ActivityOptions.newBuilder()
168+
.setStartToCloseTimeout(timeout)
169+
.setRetryOptions(RetryOptions.newBuilder().setMaximumAttempts(maxAttempts).build())
170+
.build();
171+
ChatModelActivity activity = Workflow.newActivityStub(ChatModelActivity.class, options);
172+
return new ActivityChatModel(activity, modelName, options);
162173
}
163174

164175
/**
@@ -193,7 +204,8 @@ public ChatResponse call(Prompt prompt) {
193204
private ChatResponse internalCall(Prompt prompt) {
194205
// Convert prompt to activity input and call the activity
195206
ChatModelTypes.ChatModelActivityInput input = createActivityInput(prompt);
196-
ChatModelTypes.ChatModelActivityOutput output = chatModelActivity.callChatModel(input);
207+
ChatModelActivity stub = stubForCall(prompt);
208+
ChatModelTypes.ChatModelActivityOutput output = stub.callChatModel(input);
197209

198210
// Convert activity output to ChatResponse
199211
ChatResponse response = toResponse(output);
@@ -219,6 +231,36 @@ private ChatResponse internalCall(Prompt prompt) {
219231
return response;
220232
}
221233

234+
private ChatModelActivity stubForCall(Prompt prompt) {
235+
if (baseOptions == null) {
236+
return chatModelActivity;
237+
}
238+
ActivityOptions withSummary =
239+
ActivityOptions.newBuilder(baseOptions).setSummary(buildSummary(prompt)).build();
240+
return Workflow.newActivityStub(ChatModelActivity.class, withSummary);
241+
}
242+
243+
private String buildSummary(Prompt prompt) {
244+
String label = modelName != null ? modelName : "default";
245+
String userText = lastUserText(prompt);
246+
if (userText == null || userText.isEmpty()) {
247+
return "chat: " + label;
248+
}
249+
String truncated = userText.length() > 60 ? userText.substring(0, 60) + "…" : userText;
250+
return "chat: " + label + " · " + truncated.replace('\n', ' ');
251+
}
252+
253+
private String lastUserText(Prompt prompt) {
254+
List<Message> instructions = prompt.getInstructions();
255+
for (int i = instructions.size() - 1; i >= 0; i--) {
256+
Message m = instructions.get(i);
257+
if (m.getMessageType() == MessageType.USER) {
258+
return m.getText();
259+
}
260+
}
261+
return null;
262+
}
263+
222264
private ChatModelTypes.ChatModelActivityInput createActivityInput(Prompt prompt) {
223265
// Convert messages
224266
List<ChatModelTypes.Message> messages =

temporal-spring-ai/src/test/java/io/temporal/springai/ActivitySummaryTest.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,7 @@ void chatActivity_carriesSummaryWithModelAndUserPrompt() {
6363
workflow.chat("What is the capital of France?");
6464

6565
String workflowId = WorkflowStub.fromTyped(workflow).getExecution().getWorkflowId();
66-
List<HistoryEvent> events =
67-
client.fetchHistory(workflowId).getHistory().getEventsList();
66+
List<HistoryEvent> events = client.fetchHistory(workflowId).getHistory().getEventsList();
6867

6968
String summary = findChatActivitySummary(events);
7069
assertNotNull(summary, "ActivityTaskScheduled event for callChatModel should have a Summary");
@@ -83,7 +82,7 @@ private static String findChatActivitySummary(List<HistoryEvent> events) {
8382
}
8483
String activityType =
8584
event.getActivityTaskScheduledEventAttributes().getActivityType().getName();
86-
if (!"callChatModel".equals(activityType)) {
85+
if (!"CallChatModel".equals(activityType)) {
8786
continue;
8887
}
8988
if (!event.hasUserMetadata() || !event.getUserMetadata().hasSummary()) {

0 commit comments

Comments
 (0)