Skip to content

Commit 2dfa505

Browse files
authored
Remove migrated Spring AI implementation from SDK (#3136)
* Remove migrated Spring AI implementation from SDK Retain a POM-only relocation to io.temporal:spring-ai for the final old-coordinate release. Direct development and installation guidance to ai-integrations. * Note Spring AI migration in the SDK changelog * Finalize Spring AI cutover after 0.1.0 release
1 parent 5f815c0 commit 2dfa505

61 files changed

Lines changed: 65 additions & 6679 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎AGENTS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
- `temporal-spring-boot-autoconfigure`: Spring Boot auto configuration.
1010
- `temporal-kotlin`: Kotlin DSL for the SDK.
1111
- `contrib/temporal-opentracing`: OpenTracing interceptor integration.
12-
- `contrib/temporal-spring-ai`: Spring AI integration.
12+
- `contrib/temporal-spring-ai`: one-time relocation POM; Spring AI development lives in `temporalio/ai-integrations`.
1313

1414
## General Guidance
1515
- Avoid changing public API signatures. Anything under an `internal` directory

‎CHANGELOG.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ to docs, or any other relevant information.
3838
summary on the timer behind a sleep or a timed await, like `Workflow.newTimer(Duration, TimerOptions)` already does.
3939

4040
### Changed
41+
- Moved the Spring AI integration to [temporalio/ai-integrations](https://github.com/temporalio/ai-integrations/tree/main/java/spring-ai).
42+
`io.temporal:temporal-spring-ai` now publishes a one-time relocation POM to `io.temporal:spring-ai:0.1.0`;
43+
the integration is versioned independently of the SDK. The standalone release requires Java 17+, Spring AI 2,
44+
Spring Boot 4, and Temporal Java SDK 1.40.0+. Spring AI 1 / Spring Boot 3 applications should remain on
45+
`io.temporal:temporal-spring-ai:1.40.x` until they migrate. Existing Spring AI 1 workflow histories are not
46+
replay-compatible; complete those executions or use separate worker deployments.
4147
- Release notes for all future releases are now in a single CHANGELOG.md file. `releases` directory with old release
4248
notes is kept for historical reference.
4349
- Added versioned scheduling for asynchronous Temporal stub calls. When enabled, switching between a stub method

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Java SDK:
1212
- [Java SDK documentation](https://docs.temporal.io/docs/java/introduction)
1313
- [Javadoc API reference](https://www.javadoc.io/doc/io.temporal/temporal-sdk/latest/index.html)
1414
- [Sample applications](https://github.com/temporalio/samples-java#samples-directory)
15+
- [Spring AI integration](https://github.com/temporalio/ai-integrations/tree/main/java/spring-ai)
1516

1617
Temporal:
1718

Lines changed: 26 additions & 234 deletions
Original file line numberDiff line numberDiff line change
@@ -1,234 +1,26 @@
1-
# `temporal-spring-ai`: Temporal + Spring AI Integration
2-
3-
Integrates [Spring AI](https://docs.spring.io/spring-ai/reference/) with [Temporal](https://temporal.io/) workflows, making AI model calls, tool execution, vector store operations, embeddings, and MCP tool calls durable Temporal primitives.
4-
5-
> [!WARNING]
6-
> `temporal-spring-ai` is currently in Public Preview, and will continue to evolve and improve.
7-
> We would love to hear your feedback - positive or negative - over in the [Community Slack](https://t.mp/slack), in the [#topic-ai channel](https://temporalio.slack.com/archives/C0818FQPYKY)
8-
9-
10-
## Compatibility
11-
12-
| Dependency | Minimum Version |
13-
|-------------------|-----------------|
14-
| Java | 17 |
15-
| Spring Boot | 3.x |
16-
| Spring AI | 1.1.0 |
17-
| Temporal Java SDK | 1.35.0 |
18-
19-
## Quick Start
20-
21-
Add the dependency (Maven):
22-
23-
```xml
24-
<dependency>
25-
<groupId>io.temporal</groupId>
26-
<artifactId>temporal-spring-ai</artifactId>
27-
<version>${temporal-sdk.version}</version>
28-
</dependency>
29-
```
30-
31-
You also need `temporal-spring-boot-starter` and a Spring AI model starter (e.g. `spring-ai-starter-model-openai`).
32-
33-
The plugin auto-registers `ChatModelActivity` with all Temporal workers. In your workflow:
34-
35-
```java
36-
@WorkflowInit
37-
public MyWorkflowImpl(String goal) {
38-
ActivityChatModel chatModel = ActivityChatModel.forDefault();
39-
40-
WeatherActivity weather = Workflow.newActivityStub(WeatherActivity.class, opts);
41-
42-
this.chatClient = TemporalChatClient.builder(chatModel)
43-
.defaultSystem("You are a helpful assistant.")
44-
.defaultTools(weather, new MyTools())
45-
.build();
46-
}
47-
48-
@Override
49-
public String run(String goal) {
50-
return chatClient.prompt().user(goal).call().content();
51-
}
52-
```
53-
54-
## Activity options and retry behavior
55-
56-
`ActivityChatModel.forDefault()` / `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` + `java.lang.IllegalArgumentException` marked non-retryable so a bad API key or invalid prompt fails fast instead of churning through retries.
57-
58-
When you need finer control — a specific task queue, heartbeats, priority, or a custom `RetryOptions` — pass an `ActivityOptions` directly:
59-
60-
```java
61-
ActivityChatModel chatModel = ActivityChatModel.forDefault(
62-
ActivityOptions.newBuilder(ActivityChatModel.defaultActivityOptions())
63-
.setTaskQueue("chat-heavy")
64-
.build());
65-
```
66-
67-
For configuration-driven per-model overrides, declare a `ChatModelActivityOptions` bean and auto-configuration wires the map into the plugin. A key equal to `ChatModelTypes.DEFAULT_MODEL_NAME` (the literal `"default"`) acts as a global catch-all: any chat model that lacks a bean-name-specific entry — including models contributed by third-party starters that your application did not declare directly — picks up that entry.
68-
69-
```java
70-
@Bean
71-
ChatModelActivityOptions chatModelActivityOptions() {
72-
ActivityOptions fiveMinute = ActivityOptions.newBuilder(ActivityChatModel.defaultActivityOptions())
73-
.setStartToCloseTimeout(Duration.ofMinutes(5))
74-
.build();
75-
return new ChatModelActivityOptions(Map.of(
76-
ChatModelTypes.DEFAULT_MODEL_NAME, fiveMinute, // global baseline
77-
"claude", ActivityOptions.newBuilder(fiveMinute).setTaskQueue("claude-heavy").build())); // override
78-
}
79-
```
80-
81-
Keys that neither match a registered ChatModel bean name nor equal `"default"` cause plugin construction to fail, so a typo surfaces at startup.
82-
83-
`ActivityMcpClient.create()` / `create(ActivityOptions)` work the same way with a 30-second default timeout.
84-
85-
The Temporal UI labels chat and MCP rows with a short Summary (`chat: <model>`, `mcp: <client>.<tool>`). `ActivityChatModel` and `ActivityMcpClient` are constructed only via these factories — there is no public constructor, so users can't accidentally end up in a code path that skips UI labels. Prompt text is deliberately not included in chat summaries to avoid leaking user input (which may contain PII, credentials, or other sensitive data) into workflow history and server logs.
86-
87-
## Tool Types
88-
89-
Tools passed to `defaultTools()` are handled based on their type:
90-
91-
### Activity stubs
92-
93-
Interfaces annotated with both `@ActivityInterface` and `@Tool` methods. Auto-detected and executed as durable Temporal activities with retries and timeouts.
94-
95-
```java
96-
@ActivityInterface
97-
public interface WeatherActivity {
98-
@Tool(description = "Get weather for a city") @ActivityMethod
99-
String getWeather(String city);
100-
}
101-
```
102-
103-
### `@SideEffectTool`
104-
105-
Classes annotated with `@SideEffectTool`. Each `@Tool` method is wrapped in `Workflow.sideEffect()` — the result is recorded in history on first execution and replayed from history on subsequent replays. Use for cheap non-deterministic operations (timestamps, UUIDs).
106-
107-
```java
108-
@SideEffectTool
109-
public class TimestampTools {
110-
@Tool(description = "Get current time")
111-
public String now() { return Instant.now().toString(); }
112-
}
113-
```
114-
115-
### Plain tools
116-
117-
Any class with `@Tool` methods that isn't a stub or `@SideEffectTool`. Executes directly in the workflow thread. The user is responsible for determinism — call activities, `Workflow.sideEffect()`, child workflows, etc. as needed.
118-
119-
```java
120-
public class MyTools {
121-
@Tool(description = "Process data")
122-
public String process(String input) {
123-
SomeActivity act = Workflow.newActivityStub(SomeActivity.class, opts);
124-
return act.doWork(input);
125-
}
126-
}
127-
```
128-
129-
### Nexus service stubs
130-
131-
Auto-detected and executed as Nexus operations, similar to activity stubs.
132-
133-
## Migrating from plain Spring AI
134-
135-
The plugin is designed so that bringing an existing Spring AI service onto Temporal is a localized change. Outside Temporal, you probably have something like:
136-
137-
```java
138-
@Service
139-
class AssistantService {
140-
private final ChatClient chatClient;
141-
142-
AssistantService(ChatModel chatModel) {
143-
this.chatClient = ChatClient.builder(chatModel)
144-
.defaultSystem("You are a helpful assistant.")
145-
.defaultTools(new WeatherTools(), new MyTools())
146-
.build();
147-
}
148-
149-
String respond(String goal) {
150-
return chatClient.prompt().user(goal).call().content();
151-
}
152-
}
153-
```
154-
155-
Inside a Temporal Workflow it becomes:
156-
157-
```java
158-
@WorkflowInterface
159-
interface AssistantWorkflow { @WorkflowMethod String respond(String goal); }
160-
161-
class AssistantWorkflowImpl implements AssistantWorkflow {
162-
private final ChatClient chatClient;
163-
164-
@WorkflowInit
165-
AssistantWorkflowImpl(String goal) {
166-
WeatherActivity weather = Workflow.newActivityStub(WeatherActivity.class, opts);
167-
this.chatClient = TemporalChatClient.builder(ActivityChatModel.forDefault())
168-
.defaultSystem("You are a helpful assistant.")
169-
.defaultTools(weather, new MyTools())
170-
.build();
171-
}
172-
173-
@Override
174-
public String respond(String goal) {
175-
return chatClient.prompt().user(goal).call().content();
176-
}
177-
}
178-
```
179-
180-
Three substitutions:
181-
182-
| Outside Temporal | Inside a Temporal workflow |
183-
|---|---|
184-
| `ChatModel chatModel` (injected) | `ActivityChatModel.forDefault()` |
185-
| `ChatClient.builder(chatModel)` | `TemporalChatClient.builder(activityChatModel)` |
186-
| `new WeatherTools()` for a plain POJO tool | `Workflow.newActivityStub(WeatherActivity.class, ...)` for a durable tool |
187-
188-
Plain `@Tool` POJOs, `@SideEffectTool`-annotated classes, and Nexus service stubs all work the same way — see **Tool Types** above.
189-
190-
## Media in messages
191-
192-
If you attach media (images, audio, etc.) to a `UserMessage` or an `AssistantMessage`, prefer passing it by URI rather than raw bytes:
193-
194-
```java
195-
// Good — only the URL crosses the activity boundary.
196-
Media image = new Media(MimeTypeUtils.IMAGE_PNG, URI.create("https://cdn.example.com/pic.png"));
197-
198-
// Works, but size-limited — see below.
199-
Media image = new Media(MimeTypeUtils.IMAGE_PNG, new ByteArrayResource(bytes));
200-
```
201-
202-
Raw `byte[]` media gets serialized into every chat activity's input *and* result payload, which end up inside Temporal workflow history events. Server-side history events have a fixed 2 MiB size limit; to leave headroom for messages, tool definitions, and options, the plugin enforces a **1 MiB default cap** on inline media bytes and fails fast with an `IllegalArgumentException` pointing you at the URI alternative.
203-
204-
Override the cap by setting the system property `io.temporal.springai.maxMediaBytes` before your worker starts (pass a positive integer; `0` disables the check). For anything larger than a small thumbnail, the URI route is the right answer — have an activity write the bytes to blob storage, then pass only the URL into the conversation.
205-
206-
## Known limitations
207-
208-
- **Streaming (`chatClient.stream(...)`)** — not currently supported. Use `.call()` instead.
209-
- **`defaultToolContext(Map<String, Object>)`** — not supported; tool context holds mutable state that can't safely cross the activity boundary. Pass required context as activity parameters or workflow state.
210-
- **Child workflow stubs as tools** — not supported. Wrap a plain `@Tool` method that starts the child workflow via `Workflow.newChildWorkflowStub(...)` and call through to it yourself.
211-
- **Media `byte[]` size** — inline bytes are capped at 1 MiB per payload (see "Media in messages" above). Prefer URI-based media.
212-
- **Provider-specific `ChatOptions` via `ChatClient.defaultOptions(...)`** — works as long as your `ChatOptions` subclass overrides `copy()` to return its own type (every real provider class does this). A subclass inheriting the default `copy()` loses its identity before the plugin sees it — same behavior as outside Temporal.
213-
214-
## Observability
215-
216-
`TemporalChatClient.builder(chatModel, observationRegistry, customConvention)` accepts a Micrometer `ObservationRegistry` for Spring AI-side chat client metrics. Temporal-side metrics (activity durations, retries) are emitted by the SDK's `MetricsScope` — see the [Temporal Java SDK observability docs](https://docs.temporal.io/develop/java/observability) for how to wire an OpenTelemetry or Prometheus exporter onto your workers. The two layers compose: Spring AI observations cover what the caller does; Temporal metrics cover what the scheduled activity does.
217-
218-
## Optional Integrations
219-
220-
Auto-configured when their dependencies are on the classpath:
221-
222-
| Feature | Dependency | What it registers |
223-
|--------------|-----------------|--------------------------|
224-
| Vector Store | `spring-ai-rag` | `VectorStoreActivity` |
225-
| Embeddings | `spring-ai-rag` | `EmbeddingModelActivity` |
226-
| MCP | `spring-ai-mcp` | `McpClientActivity` |
227-
228-
These can also be set up programmatically without auto-configuration:
229-
230-
```java
231-
new VectorStorePlugin(vectorStore)
232-
new EmbeddingModelPlugin(embeddingModel)
233-
new McpPlugin()
234-
```
1+
# Temporal Spring AI has moved
2+
3+
The Spring AI integration is developed and released in
4+
[temporalio/ai-integrations](https://github.com/temporalio/ai-integrations/tree/main/java/spring-ai)
5+
under the independently versioned Maven coordinate `io.temporal:spring-ai`.
6+
Version [0.1.0 is available on Maven Central](https://central.sonatype.com/artifact/io.temporal/spring-ai/0.1.0).
7+
The Java package remains `io.temporal.springai`.
8+
9+
See the [standalone usage guide](https://github.com/temporalio/ai-integrations/tree/main/java/spring-ai)
10+
for installation and migration instructions. The integration's releases are
11+
versioned independently of the Java SDK.
12+
13+
The standalone 0.1.0 release requires Java 17 or later, Spring AI 2, Spring Boot 4,
14+
and Temporal Java SDK 1.40.0 or later. Spring AI 1 / Spring Boot 3 applications
15+
should remain on `io.temporal:temporal-spring-ai:1.40.x` until they are ready to
16+
migrate. Spring AI 1 workflow histories are not replay-compatible with the new
17+
integration; complete existing executions or use separate worker deployments.
18+
19+
This directory contains only a relocation POM from the next SDK version of
20+
`io.temporal:temporal-spring-ai` to `io.temporal:spring-ai:0.1.0`. It has no source,
21+
tests, JARs, or Spring AI dependencies. The SDK BOM retains the old coordinate for
22+
this final relocation release.
23+
24+
After publishing the one-time relocation POM, remove this Gradle project, its BOM
25+
constraint, and its CODEOWNERS entry from sdk-java. Existing releases remain
26+
available at their original coordinates.
Lines changed: 20 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,24 @@
1-
description = '''Temporal Java SDK Spring AI Plugin'''
2-
3-
ext {
4-
springAiVersion = '1.1.0'
5-
// Spring AI requires Spring Boot 3.x / Java 17+
6-
springBootVersionForSpringAi = "$springBoot3Version"
7-
}
8-
9-
// Spring AI requires Java 17+, override the default Java 8 target from java.gradle.
10-
// When edgeDepsTest is set, use 21 to match other modules.
11-
ext {
12-
springAiReleaseInt = project.hasProperty("edgeDepsTest") ? 21 : 17
13-
}
14-
15-
compileJava {
16-
options.compilerArgs.removeAll(['--release', '8'])
17-
options.release = springAiReleaseInt
18-
}
19-
20-
compileTestJava {
21-
options.compilerArgs.removeAll(['--release', '8'])
22-
options.release = springAiReleaseInt
23-
}
24-
25-
dependencies {
26-
api(platform("org.springframework.boot:spring-boot-dependencies:$springBootVersionForSpringAi"))
27-
api(platform("org.springframework.ai:spring-ai-bom:$springAiVersion"))
28-
29-
// this module shouldn't carry temporal-sdk with it, especially for situations when users may be using a shaded artifact
30-
compileOnly project(':temporal-sdk')
31-
compileOnly project(':temporal-spring-boot-autoconfigure')
32-
33-
api 'org.springframework.boot:spring-boot-autoconfigure'
34-
api 'org.springframework.ai:spring-ai-client-chat'
35-
36-
implementation 'org.springframework.boot:spring-boot-starter'
37-
38-
// Optional: Vector store support
39-
compileOnly 'org.springframework.ai:spring-ai-rag'
40-
41-
// Optional: MCP (Model Context Protocol) support
42-
compileOnly 'org.springframework.ai:spring-ai-mcp'
43-
44-
testImplementation project(':temporal-sdk')
45-
testImplementation project(':temporal-testing')
46-
testImplementation "org.mockito:mockito-core:${mockitoVersion}"
47-
testImplementation 'org.springframework.boot:spring-boot-starter-test'
48-
testImplementation 'org.springframework.ai:spring-ai-rag'
49-
// Needed only so McpPluginTest can mock/reference McpSyncClient directly.
50-
testImplementation 'org.springframework.ai:spring-ai-mcp'
51-
// Needed only so tests can reference Spring AI's NonTransientAiException to
52-
// verify the plugin's default retry classification.
53-
testImplementation 'org.springframework.ai:spring-ai-retry'
54-
55-
testRuntimeOnly group: 'ch.qos.logback', name: 'logback-classic', version: "${logbackVersion}"
56-
testRuntimeOnly "org.junit.platform:junit-platform-launcher"
1+
plugins {
2+
id 'base'
573
}
584

59-
tasks.test {
60-
useJUnitPlatform()
61-
// This module's bytecode targets Java 17+, so it can't run on older JVMs.
62-
// The unit_test_jdk8 CI job sets -PtestJavaVersion=11 to verify Java 8 bytecode
63-
// works on JDK 11 — skip our tests there since the classes won't even load.
64-
if (project.hasProperty("testJavaVersion")
65-
&& (project.property("testJavaVersion") as int) < springAiReleaseInt) {
66-
enabled = false
5+
description = 'Temporal Spring AI relocation'
6+
7+
// Remove this project after publishing the one-time relocation release.
8+
publishing {
9+
publications {
10+
mavenJava(MavenPublication) {
11+
pom {
12+
packaging = 'pom'
13+
distributionManagement {
14+
relocation {
15+
groupId = 'io.temporal'
16+
artifactId = 'spring-ai'
17+
version = '0.1.0'
18+
message = 'Use io.temporal:spring-ai from temporalio/ai-integrations.'
19+
}
20+
}
21+
}
22+
}
6723
}
6824
}

0 commit comments

Comments
 (0)