Skip to content

Add Spring AI samples#775

Draft
donald-pinckney wants to merge 2 commits intomainfrom
d/20260406-164121
Draft

Add Spring AI samples#775
donald-pinckney wants to merge 2 commits intomainfrom
d/20260406-164121

Conversation

@donald-pinckney
Copy link
Copy Markdown

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

Summary

  • Adds 5 Spring AI sample modules demonstrating temporal-spring-ai integration:
    • springai — Basic chat with activity tools, deterministic tools, and side-effect tools
    • 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
    • springai-sandboxing — Unsafe tool sandboxing demo
  • Shared build config in gradle/springai.gradle
  • Uses includeBuild('../sdk-java') for the unpublished temporal-spring-ai artifact (to be removed once published)
  • All 5 samples verified to boot against a Temporal dev server

Related

Running a sample

# Start Temporal dev server
temporal server start-dev

# Set API key
export OPENAI_API_KEY=your-key

# Run the chat example
./gradlew :springai:bootRun --console=plain

Test plan

  • springai — boots, registers ChatModelActivity
  • springai-rag — boots, registers ChatModelActivity + VectorStoreActivity
  • springai-sandboxing — boots, registers ChatModelActivity
  • 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 on lines +1 to +5
spring:
application:
name: spring-ai-temporal-example
main:
web-application-type: none
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

Generated build output is being committed (springai/build/**). These files should not be tracked; remove the build directory from the PR and add ignore rules for the new sample modules’ build/ outputs (e.g., */build, or /springai/build) to prevent future commits.

Copilot uses AI. Check for mistakes.
Comment on lines +13 to +22
// so we substitute all SDK modules from the local build.
includeBuild('../sdk-java') {
dependencySubstitution {
substitute module('io.temporal:temporal-spring-ai') using project(':temporal-spring-ai')
substitute module('io.temporal:temporal-sdk') using project(':temporal-sdk')
substitute module('io.temporal:temporal-serviceclient') using project(':temporal-serviceclient')
substitute module('io.temporal:temporal-spring-boot-autoconfigure') using project(':temporal-spring-boot-autoconfigure')
substitute module('io.temporal:temporal-spring-boot-starter') using project(':temporal-spring-boot-starter')
substitute module('io.temporal:temporal-testing') using project(':temporal-testing')
substitute module('io.temporal:temporal-opentracing') using project(':temporal-opentracing')
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

includeBuild('../sdk-java') will fail for anyone who doesn’t have that sibling directory (including CI and most contributors). Consider gating this behind an existence check (e.g., if (file('../sdk-java').exists())) and/or a Gradle property so the build works out-of-the-box.

Suggested change
// so we substitute all SDK modules from the local build.
includeBuild('../sdk-java') {
dependencySubstitution {
substitute module('io.temporal:temporal-spring-ai') using project(':temporal-spring-ai')
substitute module('io.temporal:temporal-sdk') using project(':temporal-sdk')
substitute module('io.temporal:temporal-serviceclient') using project(':temporal-serviceclient')
substitute module('io.temporal:temporal-spring-boot-autoconfigure') using project(':temporal-spring-boot-autoconfigure')
substitute module('io.temporal:temporal-spring-boot-starter') using project(':temporal-spring-boot-starter')
substitute module('io.temporal:temporal-testing') using project(':temporal-testing')
substitute module('io.temporal:temporal-opentracing') using project(':temporal-opentracing')
// so we substitute all SDK modules from the local build when the sibling checkout is available.
if (file('../sdk-java').exists()) {
includeBuild('../sdk-java') {
dependencySubstitution {
substitute module('io.temporal:temporal-spring-ai') using project(':temporal-spring-ai')
substitute module('io.temporal:temporal-sdk') using project(':temporal-sdk')
substitute module('io.temporal:temporal-serviceclient') using project(':temporal-serviceclient')
substitute module('io.temporal:temporal-spring-boot-autoconfigure') using project(':temporal-spring-boot-autoconfigure')
substitute module('io.temporal:temporal-spring-boot-starter') using project(':temporal-spring-boot-starter')
substitute module('io.temporal:temporal-testing') using project(':temporal-testing')
substitute module('io.temporal:temporal-opentracing') using project(':temporal-opentracing')
}

Copilot uses AI. Check for mistakes.
Comment on lines +7 to +21
ext {
springBootVersionForSpringAi = '3.5.3'
springAiVersion = '1.1.0'
}

java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

dependencyManagement {
imports {
mavenBom "org.springframework.boot:spring-boot-dependencies:$springBootVersionForSpringAi"
mavenBom "org.springframework.ai:spring-ai-bom:$springAiVersion"
}
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

This script pins a Spring Boot BOM version (springBootVersionForSpringAi) that can diverge from the Spring Boot Gradle plugin version used by the build (springBootPluginVersion in gradle.properties). That mismatch can cause dependency/plugin incompatibilities. Consider deriving the BOM version from the same property, or updating the build to consistently use a single Spring Boot version for all modules.

Copilot uses AI. Check for mistakes.
implementation "io.temporal:temporal-envconfig:$javaSDKVersion"

// Needed for SSL sample (AdvancedTlsX509KeyManager)
implementation "io.grpc:grpc-util"
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

implementation "io.grpc:grpc-util" is declared without a version or any imported gRPC platform/constraints in this module. Gradle typically can’t resolve versionless dependencies unless a BOM/constraint supplies it. Add a version (ideally via a gRPC BOM/enforcedPlatform) or dependency constraint so builds are reproducible and don’t fail resolution.

Suggested change
implementation "io.grpc:grpc-util"
implementation "io.grpc:grpc-util:1.65.1"

Copilot uses AI. Check for mistakes.
Comment on lines +53 to +60
@SuppressWarnings("InlineMeInliner")
var unused =
clientKeyManager.updateIdentityCredentialsFromFile(
clientKeyFile,
clientCertFile,
refreshPeriod,
TimeUnit.MINUTES,
Executors.newScheduledThreadPool(1));
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

The return value of updateIdentityCredentialsFromFile(...) is assigned to unused but never read. With Error Prone + -Werror, this is likely to be flagged as an unused variable and fail compilation, and it also drops the handle you’d need to cancel the scheduled refresh. Prefer either (1) keep a meaningfully named variable and cancel it on shutdown, or (2) don’t assign and instead suppress the specific Error Prone warning you’re addressing.

Suggested change
@SuppressWarnings("InlineMeInliner")
var unused =
clientKeyManager.updateIdentityCredentialsFromFile(
clientKeyFile,
clientCertFile,
refreshPeriod,
TimeUnit.MINUTES,
Executors.newScheduledThreadPool(1));
java.util.concurrent.ScheduledExecutorService refreshExecutor =
Executors.newScheduledThreadPool(1);
var credentialRefreshHandle =
clientKeyManager.updateIdentityCredentialsFromFile(
clientKeyFile,
clientCertFile,
refreshPeriod,
TimeUnit.MINUTES,
refreshExecutor);
Runtime.getRuntime()
.addShutdownHook(
new Thread(
() -> {
try {
credentialRefreshHandle.close();
} catch (java.io.IOException e) {
// Ignore shutdown cleanup errors in this sample.
}
refreshExecutor.shutdown();
}));

Copilot uses AI. Check for mistakes.
Comment on lines +98 to +103
@Override
public String run(String systemPrompt) {
// Wait until the chat is ended
Workflow.await(() -> ended);
return "Chat ended after " + messageCount + " messages.";
}
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

run(String systemPrompt) doesn’t use its systemPrompt parameter (the prompt is only consumed in the @WorkflowInit constructor). To avoid confusion for callers, either remove the parameter (and pass it only via init), or build/configure the chat client from the run(...) argument and drop it from the constructor.

Copilot uses AI. Check for mistakes.
Comment on lines +25 to +33
/**
* Sends a message to a specific model.
*
* @param modelName the name of the model to use ("fast", "smart", or "default")
* @param message the user message
*/
@SignalMethod
void chat(String modelName, String message);

Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

Javadoc says modelName is "fast", "smart", or "default", but the implementation uses "openai", "anthropic", and "default". Update the documentation to match the actual accepted values so users don’t send unsupported names.

Copilot uses AI. Check for mistakes.
Comment on lines +16 to +29
/**
* Implementation of the RAG workflow.
*
* <p>This demonstrates:
*
* <ul>
* <li>Using {@link EmbeddingModelActivity} to generate embeddings
* <li>Using {@link VectorStoreActivity} to store and search documents
* <li>Combining vector search with chat for RAG
* </ul>
*
* <p>All operations are durable Temporal activities - if the worker restarts, the workflow will
* continue from where it left off.
*/
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

The class-level Javadoc mentions using EmbeddingModelActivity, but this workflow only uses VectorStoreActivity (and no EmbeddingModelActivity appears in code). Please update the documentation to reflect what the sample actually does (or add the missing activity usage if that was intended).

Copilot uses AI. Check for mistakes.

messageCount++;

// MessageChatMemoryAdvisor automatically handles conversation history
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

Comment references MessageChatMemoryAdvisor, but the code uses PromptChatMemoryAdvisor. Update the comment to the correct advisor name to avoid confusion.

Suggested change
// MessageChatMemoryAdvisor automatically handles conversation history
// PromptChatMemoryAdvisor automatically handles conversation history

Copilot uses AI. Check for mistakes.
Comment on lines +53 to +54
@SuppressWarnings("InlineMeInliner")
var unused =
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

What's happening here?

// Start a new workflow
String workflowId = "mcp-example-" + UUID.randomUUID().toString().substring(0, 8);
McpWorkflow workflow =
workflowClient.newWorkflowStub(
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

What's the significance of a workflow stub vs a regular workflow? Why do we always opt for a stub?

Comment on lines +69 to +71
.defaultSystem(
"""
You are a helpful assistant with access to file system tools.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Weird tabulation here, is it intentional?

Comment on lines +90 to +93
if (!initialized) {
lastResponse = "Workflow is still initializing. Please wait a moment.";
return;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Another option could be to poll until it's ready?

Comment on lines +103 to +108
modelName = "default";
message = input.substring(8).trim();
} else {
// Default to openai if no prefix
modelName = "openai";
message = input;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why is there a model name called "default", but at the same time it looks like the default is hardcoded to "openai"?

TemporalChatClient.builder(openAiModel)
.defaultSystem(
"You are a helpful assistant powered by OpenAI. "
+ "Keep answers concise. You are GPT-4o-mini.")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why do we need to tell the model its version?

continue;
}

if (input.startsWith("ask ")) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Input could just be "ask" (no trailing space) and we would still want to print the usage

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.

3 participants