-
Notifications
You must be signed in to change notification settings - Fork 180
Add Spring AI samples #775
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -50,12 +50,14 @@ public static void main(String[] args) throws Exception { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (refreshPeriod > 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| AdvancedTlsX509KeyManager clientKeyManager = new AdvancedTlsX509KeyManager(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Reload credentials every minute | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| clientKeyManager.updateIdentityCredentialsFromFile( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| clientKeyFile, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| clientCertFile, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| refreshPeriod, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| TimeUnit.MINUTES, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Executors.newScheduledThreadPool(1)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @SuppressWarnings("InlineMeInliner") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var unused = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+53
to
+54
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's happening here? |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| clientKeyManager.updateIdentityCredentialsFromFile( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| clientKeyFile, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| clientCertFile, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| refreshPeriod, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| TimeUnit.MINUTES, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Executors.newScheduledThreadPool(1)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+53
to
+60
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @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(); | |
| })); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| // Shared configuration for all Spring AI sample modules. | ||
| // Applied via: apply from: "$rootDir/gradle/springai.gradle" | ||
|
|
||
| apply plugin: 'org.springframework.boot' | ||
| apply plugin: 'io.spring.dependency-management' | ||
|
|
||
| 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" | ||
| } | ||
|
Comment on lines
+7
to
+21
|
||
| } | ||
|
|
||
| dependencies { | ||
| // Temporal | ||
| implementation "io.temporal:temporal-spring-boot-starter:$javaSDKVersion" | ||
| implementation "io.temporal:temporal-spring-ai:$javaSDKVersion" | ||
|
|
||
| // Spring Boot | ||
| implementation 'org.springframework.boot:spring-boot-starter' | ||
|
|
||
| dependencies { | ||
| errorproneJavac('com.google.errorprone:javac:9+181-r4173-1') | ||
| errorprone('com.google.errorprone:error_prone_core:2.28.0') | ||
| } | ||
| } | ||
|
|
||
| bootJar { | ||
| enabled = false | ||
| } | ||
|
|
||
| jar { | ||
| enabled = true | ||
| } | ||
|
|
||
| bootRun { | ||
| standardInput = System.in | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,4 +1,24 @@ | ||||||||||||||||||||||||||||||||||||||||||||||
| rootProject.name = 'temporal-java-samples' | ||||||||||||||||||||||||||||||||||||||||||||||
| include 'core' | ||||||||||||||||||||||||||||||||||||||||||||||
| include 'springai' | ||||||||||||||||||||||||||||||||||||||||||||||
| include 'springai-mcp' | ||||||||||||||||||||||||||||||||||||||||||||||
| include 'springai-multimodel' | ||||||||||||||||||||||||||||||||||||||||||||||
| include 'springai-rag' | ||||||||||||||||||||||||||||||||||||||||||||||
| include 'springai-sandboxing' | ||||||||||||||||||||||||||||||||||||||||||||||
| include 'springboot' | ||||||||||||||||||||||||||||||||||||||||||||||
| include 'springboot-basic' | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // Include local sdk-java build for temporal-spring-ai (until published to Maven Central). | ||||||||||||||||||||||||||||||||||||||||||||||
| // temporal-spring-ai requires the plugin API (SimplePlugin) which is not yet in a released SDK, | ||||||||||||||||||||||||||||||||||||||||||||||
| // 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') | ||||||||||||||||||||||||||||||||||||||||||||||
|
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') | |
| // 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') | |
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| apply from: "$rootDir/gradle/springai.gradle" | ||
|
|
||
| dependencies { | ||
| implementation 'org.springframework.ai:spring-ai-starter-model-openai' | ||
| implementation 'org.springframework.ai:spring-ai-starter-mcp-client' | ||
| implementation 'org.springframework.ai:spring-ai-rag' | ||
| implementation 'org.springframework.boot:spring-boot-starter-webflux' | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| package io.temporal.samples.springai.mcp; | ||
|
|
||
| import io.temporal.client.WorkflowClient; | ||
| import io.temporal.client.WorkflowOptions; | ||
| import java.util.Scanner; | ||
| import java.util.UUID; | ||
| import org.springframework.beans.factory.annotation.Autowired; | ||
| import org.springframework.boot.SpringApplication; | ||
| import org.springframework.boot.autoconfigure.SpringBootApplication; | ||
| import org.springframework.boot.context.event.ApplicationReadyEvent; | ||
| import org.springframework.context.event.EventListener; | ||
|
|
||
| /** | ||
| * Example application demonstrating MCP (Model Context Protocol) integration. | ||
| * | ||
| * <p>This application shows how to use tools from MCP servers within Temporal workflows. It | ||
| * connects to a filesystem MCP server and provides an AI assistant that can read and write files. | ||
| * | ||
| * <h2>Usage</h2> | ||
| * | ||
| * <pre> | ||
| * Commands: | ||
| * tools - List available MCP tools | ||
| * <any message> - Chat with the AI (it can use file tools) | ||
| * quit - End the chat | ||
| * </pre> | ||
| * | ||
| * <h2>Example Interactions</h2> | ||
| * | ||
| * <pre> | ||
| * > List files in the current directory | ||
| * [AI uses list_directory tool and returns results] | ||
| * | ||
| * > Create a file called hello.txt with "Hello from MCP!" | ||
| * [AI uses write_file tool] | ||
| * | ||
| * > Read the contents of hello.txt | ||
| * [AI uses read_file tool] | ||
| * </pre> | ||
| * | ||
| * <h2>Prerequisites</h2> | ||
| * | ||
| * <ol> | ||
| * <li>Start a Temporal dev server: {@code temporal server start-dev} | ||
| * <li>Set OPENAI_API_KEY environment variable | ||
| * <li>Ensure Node.js/npx is available (for MCP server) | ||
| * <li>Optionally set MCP_ALLOWED_PATH (defaults to /tmp/mcp-example) | ||
| * <li>Run: {@code ./gradlew :example-mcp:bootRun} | ||
| * </ol> | ||
| */ | ||
| @SpringBootApplication | ||
| public class McpApplication { | ||
|
|
||
| private static final String TASK_QUEUE = "mcp-example-queue"; | ||
|
|
||
| @Autowired private WorkflowClient workflowClient; | ||
|
|
||
| public static void main(String[] args) { | ||
| SpringApplication.run(McpApplication.class, args); | ||
| } | ||
|
|
||
| /** Runs after workers are started (ApplicationReadyEvent fires after CommandLineRunner). */ | ||
| @EventListener(ApplicationReadyEvent.class) | ||
| public void onReady() throws Exception { | ||
| // Start a new workflow | ||
| String workflowId = "mcp-example-" + UUID.randomUUID().toString().substring(0, 8); | ||
| McpWorkflow workflow = | ||
| workflowClient.newWorkflowStub( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? |
||
| McpWorkflow.class, | ||
| WorkflowOptions.newBuilder() | ||
| .setTaskQueue(TASK_QUEUE) | ||
| .setWorkflowId(workflowId) | ||
| .build()); | ||
|
|
||
| // Start the workflow asynchronously | ||
| WorkflowClient.start(workflow::run); | ||
|
|
||
| // Give the workflow time to initialize (first workflow task must complete) | ||
| Thread.sleep(1000); | ||
|
|
||
| System.out.println("\n=== MCP Tools Demo ==="); | ||
| System.out.println("Workflow ID: " + workflowId); | ||
| System.out.println("\nThis demo uses the filesystem MCP server."); | ||
| System.out.println("The AI can read, write, and list files in the allowed directory."); | ||
| System.out.println("\nCommands:"); | ||
| System.out.println(" tools - List available MCP tools"); | ||
| System.out.println(" <text> - Chat with the AI"); | ||
| System.out.println(" quit - End the chat"); | ||
| System.out.println(); | ||
|
|
||
| // Get a workflow stub for sending signals/queries | ||
| McpWorkflow workflowStub = workflowClient.newWorkflowStub(McpWorkflow.class, workflowId); | ||
|
|
||
| // Note: tools command may take a moment to work while workflow initializes | ||
| System.out.println("Type 'tools' to list available MCP tools.\n"); | ||
|
|
||
| Scanner scanner = new Scanner(System.in, java.nio.charset.StandardCharsets.UTF_8); | ||
| while (true) { | ||
| System.out.print("> "); | ||
| String input = scanner.nextLine().trim(); | ||
|
|
||
| if (input.equalsIgnoreCase("quit")) { | ||
| workflowStub.end(); | ||
| System.out.println("Chat ended. Goodbye!"); | ||
| break; | ||
| } | ||
|
|
||
| if (input.equalsIgnoreCase("tools")) { | ||
| System.out.println(workflowStub.listTools()); | ||
| continue; | ||
| } | ||
|
|
||
| if (input.isEmpty()) { | ||
| continue; | ||
| } | ||
|
|
||
| System.out.println("[Processing...]"); | ||
|
|
||
| // Capture current response BEFORE sending, so we can detect when it changes | ||
| String previousResponse = workflowStub.getLastResponse(); | ||
|
|
||
| // Send the message via signal | ||
| workflowStub.chat(input); | ||
|
|
||
| // Poll until the response changes (workflow has processed our message) | ||
| for (int i = 0; i < 600; i++) { // Wait up to 60 seconds (MCP tools can be slow) | ||
| String response = workflowStub.getLastResponse(); | ||
| if (!response.equals(previousResponse)) { | ||
| System.out.println("\n[AI]: " + response + "\n"); | ||
| break; | ||
| } | ||
| Thread.sleep(100); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| package io.temporal.samples.springai.mcp; | ||
|
|
||
| import io.temporal.workflow.QueryMethod; | ||
| import io.temporal.workflow.SignalMethod; | ||
| import io.temporal.workflow.WorkflowInterface; | ||
| import io.temporal.workflow.WorkflowMethod; | ||
|
|
||
| /** | ||
| * Workflow interface demonstrating MCP (Model Context Protocol) integration. | ||
| * | ||
| * <p>This workflow shows how to use tools from MCP servers within Temporal workflows. The AI model | ||
| * can call MCP tools (like file system operations) as durable activities. | ||
| */ | ||
| @WorkflowInterface | ||
| public interface McpWorkflow { | ||
|
|
||
| /** | ||
| * Runs the workflow until ended. | ||
| * | ||
| * @return summary of the chat session | ||
| */ | ||
| @WorkflowMethod | ||
| String run(); | ||
|
|
||
| /** | ||
| * Sends a message to the AI assistant with MCP tools available. | ||
| * | ||
| * @param message the user message | ||
| */ | ||
| @SignalMethod | ||
| void chat(String message); | ||
|
|
||
| /** | ||
| * Gets the last response from the AI. | ||
| * | ||
| * @return the last response | ||
| */ | ||
| @QueryMethod | ||
| String getLastResponse(); | ||
|
|
||
| /** | ||
| * Lists the available MCP tools. | ||
| * | ||
| * @return list of available tools | ||
| */ | ||
| @QueryMethod | ||
| String listTools(); | ||
|
|
||
| /** Ends the chat session. */ | ||
| @SignalMethod | ||
| void end(); | ||
| } |
There was a problem hiding this comment.
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.