Skip to content

Commit a27d432

Browse files
edits
1 parent 6b743b2 commit a27d432

1 file changed

Lines changed: 249 additions & 29 deletions

File tree

docs/develop/java/integrations/spring-ai.mdx

Lines changed: 249 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,51 @@ Use `ActivityChatModel` as a Spring AI `ChatModel` inside a Workflow. Every call
7474
Wrap `ActivityChatModel` in a `TemporalChatClient` to build prompts and register tools:
7575

7676
<!--SNIPSTART samples-java-spring-ai-chat-workflow-init-->
77+
[springai/basic/src/main/java/io/temporal/samples/springai/chat/ChatWorkflowImpl.java](https://github.com/temporalio/samples-java/blob/main/springai/basic/src/main/java/io/temporal/samples/springai/chat/ChatWorkflowImpl.java)
78+
```java
79+
@WorkflowInit
80+
public ChatWorkflowImpl(String systemPrompt) {
81+
// Build an activity-backed chat model. The factory creates the activity stub
82+
// internally and registers per-call Summaries on the Temporal UI.
83+
ActivityChatModel activityChatModel = ActivityChatModel.forDefault();
84+
85+
// Create an activity stub for weather tools - these execute as durable activities
86+
WeatherActivity weatherTool =
87+
Workflow.newActivityStub(
88+
WeatherActivity.class,
89+
ActivityOptions.newBuilder()
90+
.setStartToCloseTimeout(Duration.ofSeconds(30))
91+
.setRetryOptions(RetryOptions.newBuilder().setMaximumAttempts(3).build())
92+
.build());
93+
94+
// Create deterministic tools - these execute directly in the workflow
95+
StringTools stringTools = new StringTools();
96+
97+
// Create side-effect tools - these are wrapped in Workflow.sideEffect()
98+
// The result is recorded in history, making replay deterministic
99+
TimestampTools timestampTools = new TimestampTools();
100+
101+
// Create chat memory - uses in-memory storage that gets rebuilt on replay
102+
ChatMemory chatMemory =
103+
MessageWindowChatMemory.builder()
104+
.chatMemoryRepository(new InMemoryChatMemoryRepository())
105+
.maxMessages(20)
106+
.build();
107+
108+
// Build a TemporalChatClient with tools and memory
109+
// - Activity stubs (weatherTool) become durable AI tools
110+
// - plain workflow tool classes (stringTools) execute directly in workflow
111+
// - @SideEffectTool classes (timestampTools) are wrapped in sideEffect()
112+
// - PromptChatMemoryAdvisor maintains conversation history
113+
this.chatClient =
114+
TemporalChatClient.builder(activityChatModel)
115+
.defaultSystem(systemPrompt)
116+
.defaultTools(weatherTool, stringTools, timestampTools)
117+
.defaultAdvisors(PromptChatMemoryAdvisor.builder(chatMemory).build())
118+
.build();
119+
}
120+
121+
```
77122
<!--SNIPEND-->
78123

79124
`ActivityChatModel.forDefault()` resolves to the default Spring AI `ChatModel` bean. To target a specific model in a multi-model application, pass its bean name to `ActivityChatModel.forModel("openai")`.
@@ -82,6 +127,179 @@ Wrap `ActivityChatModel` in a `TemporalChatClient` to build prompts and register
82127
Streaming responses are not currently supported.
83128
:::
84129

130+
## Register tools
131+
132+
Tools passed to `defaultTools()` are dispatched based on their type. The integration handles Temporal determinism for you when the tool is durable, and gives you control when it isn't.
133+
134+
### Activity stubs
135+
136+
An interface annotated with both `@ActivityInterface` and Spring AI `@Tool` methods is auto-detected and executed as a Temporal Activity. Use this for external calls that need retries and timeouts.
137+
138+
<!--SNIPSTART samples-java-spring-ai-activity-tool-->
139+
[springai/basic/src/main/java/io/temporal/samples/springai/chat/WeatherActivity.java](https://github.com/temporalio/samples-java/blob/main/springai/basic/src/main/java/io/temporal/samples/springai/chat/WeatherActivity.java)
140+
```java
141+
@ActivityInterface
142+
public interface WeatherActivity {
143+
144+
/**
145+
* Gets the current weather for a city.
146+
*
147+
* <p>The {@code @Tool} annotation makes this method available to the AI model, while the
148+
* {@code @ActivityInterface} ensures it executes as a Temporal activity.
149+
*
150+
* @param city the name of the city
151+
* @return a description of the current weather
152+
*/
153+
@Tool(
154+
description =
155+
"Get the current weather for a city. Returns temperature, conditions, and humidity.")
156+
@ActivityMethod
157+
String getWeather(
158+
@ToolParam(description = "The name of the city (e.g., 'Seattle', 'New York')") String city);
159+
160+
/**
161+
* Gets the weather forecast for a city.
162+
*
163+
* @param city the name of the city
164+
* @param days the number of days to forecast (1-7)
165+
* @return the weather forecast
166+
*/
167+
@Tool(description = "Get the weather forecast for a city for the specified number of days.")
168+
@ActivityMethod
169+
String getForecast(
170+
@ToolParam(description = "The name of the city") String city,
171+
@ToolParam(description = "Number of days to forecast (1-7)") int days);
172+
}
173+
```
174+
<!--SNIPEND-->
175+
176+
### Nexus service stubs
177+
178+
Nexus service stubs with `@Tool` methods are auto-detected and invoked as [Nexus operations](/develop/java/nexus), enabling cross-Namespace tool calls.
179+
180+
### `@SideEffectTool`
181+
182+
Classes annotated with `@SideEffectTool` have each `@Tool` method wrapped in `Workflow.sideEffect()`. The result is recorded in history on first execution and replayed from history afterward. Use this for cheap, non-deterministic operations such as timestamps or UUIDs.
183+
184+
<!--SNIPSTART samples-java-spring-ai-side-effect-tool-->
185+
[springai/basic/src/main/java/io/temporal/samples/springai/chat/TimestampTools.java](https://github.com/temporalio/samples-java/blob/main/springai/basic/src/main/java/io/temporal/samples/springai/chat/TimestampTools.java)
186+
```java
187+
@SideEffectTool
188+
public class TimestampTools {
189+
190+
private static final DateTimeFormatter FORMATTER =
191+
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z").withZone(ZoneId.systemDefault());
192+
193+
/**
194+
* Gets the current date and time.
195+
*
196+
* <p>This is non-deterministic (returns different values each time), but wrapped in sideEffect()
197+
* it becomes safe for workflow replay.
198+
*
199+
* @return the current date and time as a formatted string
200+
*/
201+
@Tool(description = "Get the current date and time")
202+
public String getCurrentDateTime() {
203+
return FORMATTER.format(Instant.now());
204+
}
205+
206+
/**
207+
* Gets the current Unix timestamp in milliseconds.
208+
*
209+
* @return the current time in milliseconds since epoch
210+
*/
211+
@Tool(description = "Get the current Unix timestamp in milliseconds")
212+
public long getCurrentTimestamp() {
213+
return System.currentTimeMillis();
214+
}
215+
216+
/**
217+
* Generates a random UUID.
218+
*
219+
* @return a new random UUID string
220+
*/
221+
@Tool(description = "Generate a random UUID")
222+
public String generateUuid() {
223+
return UUID.randomUUID().toString();
224+
}
225+
226+
/**
227+
* Gets the current date and time in a specific timezone.
228+
*
229+
* @param timezone the timezone ID (e.g., "America/New_York", "UTC", "Europe/London")
230+
* @return the current date and time in the specified timezone
231+
*/
232+
@Tool(description = "Get the current date and time in a specific timezone")
233+
public String getDateTimeInTimezone(
234+
@ToolParam(description = "Timezone ID (e.g., 'America/New_York', 'UTC', 'Europe/London')")
235+
String timezone) {
236+
try {
237+
ZoneId zoneId = ZoneId.of(timezone);
238+
DateTimeFormatter formatter =
239+
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z").withZone(zoneId);
240+
return formatter.format(Instant.now());
241+
} catch (Exception e) {
242+
return "Invalid timezone: " + timezone + ". Use formats like 'America/New_York' or 'UTC'.";
243+
}
244+
}
245+
}
246+
```
247+
<!--SNIPEND-->
248+
249+
### Plain tools
250+
251+
Any class with `@Tool` methods that isn't an Activity stub, Nexus stub, or `@SideEffectTool` runs directly on the Workflow thread. Use this for inherently deterministic tools (such as updating in-memory agent state), or for orchestration of durable primitives as you need, e.g. calling multiple Activities, child Workflows, wait conditions, or other Temporal durable primitives.
252+
253+
<!--SNIPSTART samples-java-spring-ai-plain-tool-->
254+
[springai/basic/src/main/java/io/temporal/samples/springai/chat/StringTools.java](https://github.com/temporalio/samples-java/blob/main/springai/basic/src/main/java/io/temporal/samples/springai/chat/StringTools.java)
255+
```java
256+
public class StringTools {
257+
258+
@Tool(description = "Reverse a string, returning the characters in opposite order")
259+
public String reverse(@ToolParam(description = "The string to reverse") String input) {
260+
if (input == null) {
261+
return null;
262+
}
263+
return new StringBuilder(input).reverse().toString();
264+
}
265+
266+
@Tool(description = "Count the number of words in a text")
267+
public int countWords(@ToolParam(description = "The text to count words in") String text) {
268+
if (text == null || text.isBlank()) {
269+
return 0;
270+
}
271+
return text.trim().split("\\s+").length;
272+
}
273+
274+
@Tool(description = "Convert text to all uppercase letters")
275+
public String toUpperCase(@ToolParam(description = "The text to convert") String text) {
276+
if (text == null) {
277+
return null;
278+
}
279+
return text.toUpperCase(java.util.Locale.ROOT);
280+
}
281+
282+
@Tool(description = "Convert text to all lowercase letters")
283+
public String toLowerCase(@ToolParam(description = "The text to convert") String text) {
284+
if (text == null) {
285+
return null;
286+
}
287+
return text.toLowerCase(java.util.Locale.ROOT);
288+
}
289+
290+
@Tool(description = "Check if a string is a palindrome (reads the same forwards and backwards)")
291+
public boolean isPalindrome(@ToolParam(description = "The text to check") String text) {
292+
if (text == null) {
293+
return false;
294+
}
295+
String normalized = text.toLowerCase(java.util.Locale.ROOT).replaceAll("\\s+", "");
296+
String reversed = new StringBuilder(normalized).reverse().toString();
297+
return normalized.equals(reversed);
298+
}
299+
}
300+
```
301+
<!--SNIPEND-->
302+
85303
## Activity options and retry behavior
86304

87305
`ActivityChatModel.forDefault()` and `forModel(name)` build the chat Activity stub with sensible defaults: a 2-minute start-to-close timeout, 3 attempts, and `org.springframework.ai.retry.NonTransientAiException` and `java.lang.IllegalArgumentException` classified as non-retryable so a bad API key or invalid prompt fails fast.
@@ -98,6 +316,19 @@ ActivityChatModel chatModel = ActivityChatModel.forDefault(
98316
For configuration-driven per-model overrides, declare a `ChatModelActivityOptions` bean. The plugin consults it whenever `forDefault()` or `forModel(name)` runs in a Workflow. Use the special key `ChatModelTypes.DEFAULT_MODEL_NAME` (the literal `"default"`) as a global catch-all that applies to any model not explicitly listed including models contributed by third-party starters:
99317

100318
<!--SNIPSTART samples-java-spring-ai-per-model-options-->
319+
[springai/multimodel/src/main/java/io/temporal/samples/springai/multimodel/ChatModelConfig.java](https://github.com/temporalio/samples-java/blob/main/springai/multimodel/src/main/java/io/temporal/samples/springai/multimodel/ChatModelConfig.java)
320+
```java
321+
@Bean
322+
public ChatModelActivityOptions chatModelActivityOptions() {
323+
return new ChatModelActivityOptions(
324+
Map.of(
325+
"anthropicChatModel",
326+
ActivityOptions.newBuilder(ActivityChatModel.defaultActivityOptions())
327+
.setStartToCloseTimeout(Duration.ofMinutes(5))
328+
.setScheduleToCloseTimeout(Duration.ofMinutes(15))
329+
.build()));
330+
}
331+
```
101332
<!--SNIPEND-->
102333

103334
Keys that neither match a registered `ChatModel` bean nor equal `"default"` cause plugin construction to fail, so a typo surfaces at startup rather than at first call.
@@ -109,6 +340,24 @@ Keys that neither match a registered `ChatModel` bean nor equal `"default"` caus
109340
Provider-specific `ChatOptions` subclasses for example, `AnthropicChatOptions` to enable extended thinking, or `OpenAiChatOptions` to set `reasoning_effort` pass through the Activity boundary unchanged. Attach them via `ChatClient.defaultOptions(...)` and the plugin re-applies them on the Activity side before calling the underlying model:
110341

111342
<!--SNIPSTART samples-java-spring-ai-provider-options-->
343+
[springai/multimodel/src/main/java/io/temporal/samples/springai/multimodel/MultiModelWorkflowImpl.java](https://github.com/temporalio/samples-java/blob/main/springai/multimodel/src/main/java/io/temporal/samples/springai/multimodel/MultiModelWorkflowImpl.java)
344+
```java
345+
AnthropicChatOptions thinkingOptions =
346+
AnthropicChatOptions.builder()
347+
.thinking(AnthropicApi.ThinkingType.ENABLED, 1024)
348+
.temperature(1.0)
349+
.maxTokens(4096)
350+
.build();
351+
chatClients.put(
352+
"think",
353+
TemporalChatClient.builder(anthropicModel)
354+
.defaultSystem(
355+
"You are a helpful assistant powered by Anthropic with extended thinking. "
356+
+ "Use the thinking budget to reason carefully, then give a crisp answer "
357+
+ "that reflects the reasoning you did.")
358+
.defaultOptions(thinkingOptions)
359+
.build());
360+
```
112361
<!--SNIPEND-->
113362

114363
The pass-through relies on the `ChatOptions` subclass overriding `copy()` to return its own type every provider class shipped with Spring AI does.
@@ -124,35 +373,6 @@ Media image = new Media(MimeTypeUtils.IMAGE_PNG, URI.create("https://cdn.example
124373

125374
Override the cap by setting the system property `io.temporal.springai.maxMediaBytes` before your worker starts (positive integer; `0` disables the check). For anything larger than a small thumbnail, route the bytes to a binary store from an Activity and pass only the URL across the conversation.
126375

127-
## Register tools
128-
129-
Tools passed to `defaultTools()` are dispatched based on their type. The integration handles Temporal determinism for you when the tool is durable, and gives you control when it isn't.
130-
131-
### Activity stubs
132-
133-
An interface annotated with both `@ActivityInterface` and Spring AI `@Tool` methods is auto-detected and executed as a Temporal Activity. Use this for external calls that need retries and timeouts.
134-
135-
<!--SNIPSTART samples-java-spring-ai-activity-tool-->
136-
<!--SNIPEND-->
137-
138-
### Nexus service stubs
139-
140-
Nexus service stubs with `@Tool` methods are auto-detected and invoked as [Nexus operations](/develop/java/nexus), enabling cross-Namespace tool calls.
141-
142-
### `@SideEffectTool`
143-
144-
Classes annotated with `@SideEffectTool` have each `@Tool` method wrapped in `Workflow.sideEffect()`. The result is recorded in history on first execution and replayed from history afterward. Use this for cheap, non-deterministic operations such as timestamps or UUIDs.
145-
146-
<!--SNIPSTART samples-java-spring-ai-side-effect-tool-->
147-
<!--SNIPEND-->
148-
149-
### Plain tools
150-
151-
Any class with `@Tool` methods that isn't an Activity stub, Nexus stub, or `@SideEffectTool` runs directly on the Workflow thread. Use this for inherently deterministic tools (such as updating in-memory agent state), or for orchestration of durable primitives as you need, e.g. calling multiple Activities, child Workflows, wait conditions, or other Temporal durable primitives.
152-
153-
<!--SNIPSTART samples-java-spring-ai-plain-tool-->
154-
<!--SNIPEND-->
155-
156376
## Use vector stores, embeddings, and MCP
157377

158378
When the corresponding Spring AI modules are on the classpath, the integration registers Activities for vector stores, embeddings, and MCP tool calls. Inject the matching Spring AI types into your Activities or Workflows and use them as you would in any Spring AI application each operation is executed through a Temporal Activity.

0 commit comments

Comments
 (0)