1010import java .util .List ;
1111import java .util .Map ;
1212import java .util .stream .Collectors ;
13+ import javax .annotation .Nullable ;
1314import org .springframework .ai .chat .messages .*;
1415import org .springframework .ai .chat .metadata .ChatResponseMetadata ;
1516import 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 =
0 commit comments