Skip to content

Add Spring AI samples#775

Merged
donald-pinckney merged 29 commits intomainfrom
d/20260406-164121
May 1, 2026
Merged

Add Spring AI samples#775
donald-pinckney merged 29 commits intomainfrom
d/20260406-164121

Conversation

@donald-pinckney
Copy link
Copy Markdown
Contributor

@donald-pinckney donald-pinckney commented Apr 6, 2026

Summary

Adds 4 Spring AI sample modules demonstrating temporal-spring-ai integration:

  • springai — Basic chat with activity tools, plain workflow tools, and @SideEffectTools
  • springai-mcp — MCP (Model Context Protocol) integration with filesystem server
  • springai-multimodel — Multiple AI providers (OpenAI + Anthropic)
  • springai-rag — Retrieval-Augmented Generation with vector store

Shared build config lives in gradle/springai.gradle.

This PR also adds AI team as a CODEOWNER of springai/

Related

Running a sample

# 1. Start Temporal dev server
temporal server start-dev

# 2. Set API key and run
export OPENAI_API_KEY=your-key
./gradlew :springai:basic:bootRun

Test plan

  • springai — boots, registers ChatModelActivity
  • springai-rag — boots, registers ChatModelActivity + VectorStoreActivity
  • springai-multimodel — boots, registers ChatModelActivity (2 models)
  • springai-mcp — boots (requires Node.js/npx for MCP server)

🤖 Generated with Claude Code

- Remove runtimeOnly spring-ai-rag and spring-ai-mcp from shared
  config (no longer needed after T6 plugin split in sdk-java)
- Fix workflow class package references (old prototype packages)
- Add web-application-type: none to RAG and multimodel configs
- Exclude conflicting chat auto-configs in multimodel sample

All 5 samples now boot successfully against a Temporal dev server.
MCP sample requires Node.js/npx for the MCP server.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds multiple new Spring AI sample modules demonstrating temporal-spring-ai integrations (basic chat tools, MCP tools, multi-provider, RAG/vector store, and sandboxing), plus shared Gradle configuration and a local composite build substitution for sdk-java.

Changes:

  • Introduces 5 new Spring Boot sample modules (springai*) with workflows/apps showing different temporal-spring-ai usage patterns.
  • Adds shared Gradle config (gradle/springai.gradle) and wires new modules into settings.gradle.
  • Updates the SSL sample to use AdvancedTlsX509KeyManager and adds a gRPC dependency in core.

Reviewed changes

Copilot reviewed 38 out of 67 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
springai/src/main/resources/application.yml Spring AI sample runtime config (Temporal worker + OpenAI).
springai/src/main/java/io/temporal/samples/springai/chat/WeatherActivityImpl.java Mock weather activity/tool implementation.
springai/src/main/java/io/temporal/samples/springai/chat/WeatherActivity.java Activity + Spring AI tool annotations for weather tools.
springai/src/main/java/io/temporal/samples/springai/chat/TimestampTools.java Side-effect tools for nondeterministic values in workflows.
springai/src/main/java/io/temporal/samples/springai/chat/StringTools.java Deterministic tools for workflow-safe string utilities.
springai/src/main/java/io/temporal/samples/springai/chat/ChatWorkflowImpl.java Chat workflow using TemporalChatClient + tools + memory.
springai/src/main/java/io/temporal/samples/springai/chat/ChatWorkflow.java Workflow interface for interactive chat (update + signal).
springai/src/main/java/io/temporal/samples/springai/chat/ChatExampleApplication.java CLI-style Spring Boot app to drive the chat workflow.
springai/build/resources/main/application.yml Generated build output (should not be committed).
springai/build/classes/java/main/io/temporal/samples/springai/chat/WeatherActivityImpl.class Generated build output (should not be committed).
springai/build/classes/java/main/io/temporal/samples/springai/chat/WeatherActivity.class Generated build output (should not be committed).
springai/build/classes/java/main/io/temporal/samples/springai/chat/TimestampTools.class Generated build output (should not be committed).
springai/build/classes/java/main/io/temporal/samples/springai/chat/StringTools.class Generated build output (should not be committed).
springai/build/classes/java/main/io/temporal/samples/springai/chat/ChatWorkflowImpl.class Generated build output (should not be committed).
springai/build/classes/java/main/io/temporal/samples/springai/chat/ChatWorkflow.class Generated build output (should not be committed).
springai/build/classes/java/main/io/temporal/samples/springai/chat/ChatRunner.class Generated build output (should not be committed).
springai/build/classes/java/main/io/temporal/samples/springai/chat/ChatExampleApplication.class Generated build output (should not be committed).
springai/build/bootRunMainClassName Generated build output (should not be committed).
springai/build.gradle Spring AI sample module Gradle dependencies.
springai-sandboxing/src/main/resources/application.yml Sandboxing sample runtime config.
springai-sandboxing/src/main/java/io/temporal/samples/springai/sandboxing/UnsafeTools.java Demonstrates unsafe (unannotated) tools for sandboxing.
springai-sandboxing/src/main/java/io/temporal/samples/springai/sandboxing/SandboxingWorkflowImpl.java Workflow demonstrating SandboxingAdvisor behavior.
springai-sandboxing/src/main/java/io/temporal/samples/springai/sandboxing/SandboxingWorkflow.java Workflow interface for sandboxing demo.
springai-sandboxing/src/main/java/io/temporal/samples/springai/sandboxing/SandboxingApplication.java CLI-style app to drive sandboxing workflow.
springai-sandboxing/build/classes/java/main/io/temporal/samples/springai/sandboxing/UnsafeTools.class Generated build output (should not be committed).
springai-sandboxing/build/classes/java/main/io/temporal/samples/springai/sandboxing/SandboxingWorkflowImpl.class Generated build output (should not be committed).
springai-sandboxing/build/classes/java/main/io/temporal/samples/springai/sandboxing/SandboxingWorkflow.class Generated build output (should not be committed).
springai-sandboxing/build/classes/java/main/io/temporal/samples/springai/sandboxing/SandboxingRunner.class Generated build output (should not be committed).
springai-sandboxing/build/classes/java/main/io/temporal/samples/springai/sandboxing/SandboxingApplication.class Generated build output (should not be committed).
springai-sandboxing/build.gradle Sandboxing sample module Gradle dependencies.
springai-rag/src/main/resources/application.yaml RAG sample runtime config (chat + embeddings).
springai-rag/src/main/java/io/temporal/samples/springai/rag/VectorStoreConfig.java Spring config for in-memory vector store.
springai-rag/src/main/java/io/temporal/samples/springai/rag/RagWorkflowImpl.java Workflow implementing vector-store-backed RAG flow.
springai-rag/src/main/java/io/temporal/samples/springai/rag/RagWorkflow.java RAG workflow interface (signals + queries).
springai-rag/src/main/java/io/temporal/samples/springai/rag/RagApplication.java CLI-style app to drive RAG workflow.
springai-rag/build/classes/java/main/io/temporal/samples/springai/rag/VectorStoreConfig.class Generated build output (should not be committed).
springai-rag/build/classes/java/main/io/temporal/samples/springai/rag/RagWorkflowImpl.class Generated build output (should not be committed).
springai-rag/build/classes/java/main/io/temporal/samples/springai/rag/RagWorkflow.class Generated build output (should not be committed).
springai-rag/build/classes/java/main/io/temporal/samples/springai/rag/RagApplication.class Generated build output (should not be committed).
springai-rag/build.gradle RAG sample module Gradle dependencies.
springai-multimodel/src/main/resources/application.yaml Multi-provider sample runtime config (OpenAI + Anthropic).
springai-multimodel/src/main/java/io/temporal/samples/springai/multimodel/MultiModelWorkflowImpl.java Workflow routing messages to different providers/models.
springai-multimodel/src/main/java/io/temporal/samples/springai/multimodel/MultiModelWorkflow.java Interface for multi-model chat workflow.
springai-multimodel/src/main/java/io/temporal/samples/springai/multimodel/MultiModelApplication.java CLI-style app to drive multi-model workflow.
springai-multimodel/src/main/java/io/temporal/samples/springai/multimodel/ChatModelConfig.java Spring beans configuring OpenAI + Anthropic ChatModel beans.
springai-multimodel/build/classes/java/main/io/temporal/samples/springai/multimodel/MultiModelWorkflowImpl.class Generated build output (should not be committed).
springai-multimodel/build/classes/java/main/io/temporal/samples/springai/multimodel/MultiModelWorkflow.class Generated build output (should not be committed).
springai-multimodel/build/classes/java/main/io/temporal/samples/springai/multimodel/MultiModelApplication.class Generated build output (should not be committed).
springai-multimodel/build/classes/java/main/io/temporal/samples/springai/multimodel/ChatModelConfig.class Generated build output (should not be committed).
springai-multimodel/build.gradle Multi-model sample module Gradle dependencies.
springai-mcp/src/main/resources/application.yaml MCP sample runtime config (OpenAI + MCP filesystem server).
springai-mcp/src/main/java/io/temporal/samples/springai/mcp/McpWorkflowImpl.java Workflow discovering MCP tools and exposing them to chat.
springai-mcp/src/main/java/io/temporal/samples/springai/mcp/McpWorkflow.java MCP workflow interface (signals + queries).
springai-mcp/src/main/java/io/temporal/samples/springai/mcp/McpApplication.java CLI-style app to drive MCP workflow.
springai-mcp/build/classes/java/main/io/temporal/samples/springai/mcp/McpWorkflowImpl.class Generated build output (should not be committed).
springai-mcp/build/classes/java/main/io/temporal/samples/springai/mcp/McpWorkflow.class Generated build output (should not be committed).
springai-mcp/build/classes/java/main/io/temporal/samples/springai/mcp/McpApplication.class Generated build output (should not be committed).
springai-mcp/build.gradle MCP sample module Gradle dependencies.
settings.gradle Adds new sample modules + composite build substitution for sdk-java.
gradle/springai.gradle Shared Gradle config for all Spring AI sample modules.
core/src/main/java/io/temporal/samples/ssl/Starter.java Updates SSL sample to use gRPC AdvancedTlsX509KeyManager refresh API.
core/build.gradle Adds gRPC util dependency for SSL sample.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread springai/build/resources/main/application.yml
Comment thread settings.gradle Outdated
Comment thread gradle/springai.gradle
Comment thread core/build.gradle Outdated
Comment thread core/src/main/java/io/temporal/samples/ssl/Starter.java Outdated
Comment thread springai-mcp/src/main/java/io/temporal/samples/springai/mcp/McpWorkflowImpl.java Outdated
Comment thread core/src/main/java/io/temporal/samples/ssl/Starter.java Outdated
Comment thread springai-mcp/src/main/java/io/temporal/samples/springai/mcp/McpWorkflowImpl.java Outdated
Comment thread springai-mcp/src/main/java/io/temporal/samples/springai/mcp/McpWorkflowImpl.java Outdated
Comment thread springai-rag/src/main/java/io/temporal/samples/springai/rag/RagApplication.java Outdated
donald-pinckney and others added 6 commits April 13, 2026 07:14
- StringTools: remove @DeterministicTool (plain tools run in workflow
  context by default now)
- Delete springai-sandboxing sample (SandboxingAdvisor removed from SDK)
- Update comments referencing @DeterministicTool

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove committed build/ directories and add them to .gitignore.
- Gate includeBuild('../sdk-java') on sibling checkout existence so the
  build works out-of-the-box when the springai samples are not being
  touched (and will resolve cleanly once temporal-spring-ai publishes).
- core: pin io.grpc:grpc-util version explicitly and switch the SSL sample
  to AdvancedTlsX509KeyManager.updateIdentityCredentials (non-deprecated
  API) to fix compilation under -Werror with the composite-build grpc.
- springai-multimodel: fix doc mismatch on model names, route unprefixed
  input to the "default" model, drop hard-coded model-version hints from
  system prompts.
- springai-rag: correct Javadoc to match actual code (no EmbeddingModelActivity),
  accept bare "ask" as a usage-prompt trigger, normalize text block indentation.
- springai-mcp: fix advisor name comment, normalize text block indentation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the includeBuild('../sdk-java') dependency-substitution block with
a plain mavenLocal() repository and a pinned 1.35.0-SNAPSHOT coordinate for
the springai* samples. Build temporal-spring-ai locally with
`./gradlew publishToMavenLocal` in an sdk-java checkout and the samples
pick it up via mavenLocal — no composite build, no SDK-wide substitution.

A nice side-effect: the core module no longer has a newer grpc forced onto
its classpath, so the SSL sample's gRPC workarounds (explicit grpc-util
dep + updateIdentityCredentials switch) are no longer needed. Revert them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tracks the mavenLocal workaround unwind (blocked on temporal-spring-ai
publishing) and unaddressed reviewer threads. Delete before merge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wraps natural snippet boundaries in the spring-ai samples so the
docs/spring-ai-integration page can pull canonical code via snipsync
instead of carrying inline copies. Six markers added:

- samples-java-spring-ai-chat-workflow-init: ChatWorkflowImpl @WorkflowInit
- samples-java-spring-ai-activity-tool: WeatherActivity interface
- samples-java-spring-ai-side-effect-tool: TimestampTools class
- samples-java-spring-ai-plain-tool: StringTools class
- samples-java-spring-ai-per-model-options: ChatModelConfig per-model bean
- samples-java-spring-ai-provider-options: MultiModelWorkflowImpl think route

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
donald-pinckney and others added 11 commits May 1, 2026 10:27
temporal-spring-ai is now on Maven Central. Drop the mavenLocal()
repository and the SNAPSHOT pin that were bridging the gap; the
springai* samples now resolve from mavenCentral like everything else.
springAiSdkVersion stays separate from javaSDKVersion because the
spring-ai module requires a newer SDK than the rest of the samples
currently pin.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Moves the four spring-ai sample modules from top-level peers into
nested subprojects of springai/, so the repo root has one springai/
instead of four (springai, springai-mcp, springai-multimodel,
springai-rag). Each module keeps its own build.gradle and Spring Boot
entry point — only the directory layout and Gradle project paths
change. Invocations become :springai:basic, :springai:mcp, etc.

Also folds in the earlier simplification: now that javaSDKVersion is
1.35.0 globally, gradle/springai.gradle drops the separate
springAiSdkVersion variable and references the global one directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the early-return-with-placeholder pattern in McpWorkflowImpl.chat()
with Workflow.await(() -> initialized). Signals can yield, so awaiting is
the idiomatic Temporal way to handle "operation arrived before init
finished" — the client's signal RPC has already returned, and the
workflow-side handling just resumes once run() completes MCP tool
discovery and finishes building the chat client.

listTools() stays placeholder-based because it's a @QueryMethod and
queries cannot yield.

Addresses brianstrauch review comment on PR #775.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The CLI exposed both `default:` and `openai:` prefixes, which both
ended up calling OpenAI — ChatModelConfig declares @primary on
openAiChatModel, so ActivityChatModel.forDefault() resolves to it.
Reviewers reasonably found this confusing.

Drop the explicit `default:` prefix; route no-prefix input to the
"default" workflow client instead. The chatClients entry stays so
the sample still demonstrates both forDefault() (@primary resolution)
and forModel(name) (explicit lookup) — added a comment block at the
registration site explaining that the dual entry is intentional.

Addresses brianstrauch review comment on PR #775.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reviewer noted that bare `ask` falls through to printing usage but
bare `add` or `search` did not — the startsWith check required a
trailing space. Apply the same equals|startsWith pattern that the
ask branch already uses, with a length guard around the substring.

Addresses brianstrauch review comment on PR #775.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The javadoc listed "openai", "anthropic", "default" but the workflow
also accepts "think" (Anthropic with extended thinking enabled).
Add it to the @param doc so users don't send a name and wonder why
it falls through to the unknown-model branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@WorkflowInit requires the constructor and the @WorkflowMethod to
share a parameter list, so run(String) must take systemPrompt even
though only the constructor uses it. Add a comment so readers don't
trip over the apparent unused parameter (and so static analyzers
that flagged it have an explanation in-source).

Addresses Copilot review on PR #775.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The root build.gradle pins the Spring Boot Gradle plugin at
springBootPluginVersion (2.7.13) for the legacy springboot/ samples,
while Spring AI 1.1.0 needs Spring Boot 3.5.x via BOM import. The
plugin and BOM are independent enough that this works in practice,
but a future reader (or static analyzer) reasonably gets confused.

Add a comment block explaining the trade-off and naming the two
follow-up paths that would actually fix it (move plugin out of root
plugins block, or migrate legacy springboot/ to 3.x).

Addresses Copilot review on PR #775.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces twelve explicit per-module entries (/build, /core/build,
/springai/basic/build, ...) with two globs (**/build/, **/out/) so
new sample modules don't need a .gitignore edit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds Spring AI to the module list at the top, describes the four
nested samples (basic, mcp, multimodel, rag) under a new "Running
Spring AI Samples" section, and consolidates the per-sample Java
version notes into a single "Java 17+ for all samples" line now
that #779 bumped the project-wide target.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 31 out of 32 changed files in this pull request and generated 13 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread springai/rag/src/main/java/io/temporal/samples/springai/rag/RagApplication.java Outdated
Comment thread springai/rag/src/main/java/io/temporal/samples/springai/rag/RagApplication.java Outdated
Comment thread springai/rag/src/main/java/io/temporal/samples/springai/rag/RagApplication.java Outdated
Comment thread springai/mcp/src/main/java/io/temporal/samples/springai/mcp/McpApplication.java Outdated
Comment thread springai/rag/src/main/java/io/temporal/samples/springai/rag/RagApplication.java Outdated
Comment thread TASK_QUEUE.json Outdated
Comment thread springai/basic/src/main/resources/application.yaml
donald-pinckney and others added 3 commits May 1, 2026 12:59
Five small fixes flagged in PR #775 review:

- Rename springai/basic/src/main/resources/application.yml to .yaml
  to match the convention used by every other sample in the repo.
- Fix the @code ./gradlew :example-* paths in McpApplication,
  RagApplication, and MultiModelApplication javadocs to the actual
  module paths (:springai:mcp, :springai:rag, :springai:multimodel).
- Add spring.main.web-application-type: none to springai/mcp's
  application.yaml. The module pulls in spring-boot-starter-webflux
  which would otherwise spin up a reactive web server we don't want.
  Matches the other three springai samples.
- Drop stale references to EmbeddingModelActivity from the
  RagApplication javadoc and startup banner. The sample uses
  VectorStoreActivity exclusively; embeddings are produced inside
  the configured Spring AI VectorStore, not via a separate activity.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Slipped into the previous commit via git add -A. .vscode/ is
per-developer IDE config — add to .gitignore.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@donald-pinckney donald-pinckney marked this pull request as ready for review May 1, 2026 17:04
@donald-pinckney donald-pinckney requested review from a team, antmendoza and tsurdilo as code owners May 1, 2026 17:04
donald-pinckney and others added 4 commits May 1, 2026 13:25
The unknown-model error message renders chatClients.keySet() into
lastResponse, which is workflow state. HashMap iteration order isn't
guaranteed across JVMs, so a replay on a different worker could
produce a different rendered string and surface as a non-determinism
error. LinkedHashMap iterates in insertion order, which is
deterministic regardless of JVM.

Addresses Copilot review on PR #775.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
waitForResponse() previously read getLastResponse() *after* the
signal had been sent. If the workflow processed the signal between
the signal call and the first poll, the captured baseline was
already the new response and the loop waited for a second change
that never came, then timed out.

Fix matches the pattern McpApplication uses: capture
previousResponse before the signal, pass it into waitForResponse,
and poll for a value different from that pre-signal baseline.

Addresses Copilot review on PR #775.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Same bug pattern that RagApplication had: the polling loop compared
against an empty-string baseline, so the very first poll could
return the stale prior response and print it as if it were the new
one. Capture previousResponse before sending the chat signal and
wait until getLastResponse() differs from that pre-signal baseline.

Also drop the redundant initial Thread.sleep(100) — the loop's own
sleep handles backoff, and reading immediately is fine when we're
comparing against the pre-signal baseline.

Addresses Copilot review on PR #775.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Internal tracking file scoped to PR #775. All listed tasks and
review threads have been resolved or replied to; the file's job
is done.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@maciejdudko maciejdudko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a minor change to README needed, after that it's good to go.

Comment thread README.md Outdated
Co-authored-by: Maciej Dudkowski <maciej.dudkowski@temporal.io>
@donald-pinckney donald-pinckney enabled auto-merge (squash) May 1, 2026 19:03
@donald-pinckney donald-pinckney merged commit f279e5f into main May 1, 2026
12 of 13 checks passed
@donald-pinckney donald-pinckney deleted the d/20260406-164121 branch May 1, 2026 19:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants