Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
296 changes: 296 additions & 0 deletions lambda-durable-agentcore-springai-sam-java/README.md

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions lambda-durable-agentcore-springai-sam-java/agent/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# AgentCore Runtime requires an ARM64 container listening on port 8080.
FROM --platform=linux/arm64 amazoncorretto:21-alpine3.23

RUN addgroup -S agent && adduser -S agent -G agent

WORKDIR /app
COPY target/document-review-agent.jar app.jar
RUN chown agent:agent /app/app.jar

USER agent
EXPOSE 8080

ENTRYPOINT ["java", "-jar", "app.jar"]
87 changes: 87 additions & 0 deletions lambda-durable-agentcore-springai-sam-java/agent/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<!-- Spring Boot 4.1.0 is a hard floor: spring-ai-agentcore-runtime-starter
depends on spring-boot-starter-web 4.1.0 at compile scope. -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.0</version>
<relativePath/>
</parent>

<groupId>com.example</groupId>
<artifactId>document-review-agent</artifactId>
<version>1.0</version>
<name>Document Review Agent</name>
<description>Spring AI agent hosted on Amazon Bedrock AgentCore Runtime</description>

<properties>
<java.version>21</java.version>
<spring-ai.version>2.0.0</spring-ai.version>
<spring-ai-agentcore.version>2.1.0</spring-ai-agentcore.version>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Community project (org.springaicommunity), published to Maven Central.
Provides the AgentCore Runtime HTTP contract as auto-configuration. -->
<dependency>
<groupId>org.springaicommunity</groupId>
<artifactId>spring-ai-agentcore-bom</artifactId>
<version>${spring-ai-agentcore.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<!-- Auto-configures POST /invocations and GET /ping on port 8080 -->
<dependency>
<groupId>org.springaicommunity</groupId>
<artifactId>spring-ai-agentcore-runtime-starter</artifactId>
</dependency>

<!-- Amazon Bedrock Converse API as the chat model -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-bedrock-converse</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<!-- Fixed jar name so the Dockerfile does not need a wildcard COPY -->
<finalName>document-review-agent</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*! Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: MIT-0
*/
package com.example.agent;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
* Spring Boot entry point for the document review agent.
*
* <p>The {@code spring-ai-agentcore-runtime-starter} on the classpath auto-configures the
* two endpoints Amazon Bedrock AgentCore Runtime requires - {@code POST /invocations} and
* {@code GET /ping} - so this application only needs to supply the agent logic itself.
*/
@SpringBootApplication
public class AgentApplication {

public static void main(String[] args) {
SpringApplication.run(AgentApplication.class, args);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*! Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: MIT-0
*/
package com.example.agent;

/**
* Payload sent by the durable function to {@code POST /invocations}.
*
* <p>A single agent runtime serves both workflow steps. {@code mode} selects which system
* prompt to apply:
*
* <ul>
* <li>{@code analyze} - summarise {@code document} and produce a draft for review.
* <li>{@code finalize} - polish {@code draft}, taking {@code reviewerComments} into account.
* </ul>
*
* @param mode either {@code analyze} or {@code finalize}
* @param document the original document text (used by {@code analyze})
* @param draft the approved draft (used by {@code finalize})
* @param reviewerComments free-text comments captured at the approval gate; may be null
*/
public record AgentRequest(
String mode,
String document,
String draft,
String reviewerComments) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*! Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: MIT-0
*/
package com.example.agent;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springaicommunity.agentcore.annotation.AgentCoreInvocation;
import org.springaicommunity.agentcore.context.AgentCoreContext;
import org.springaicommunity.agentcore.context.AgentCoreHeaders;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.RestController;

/**
* The document review agent.
*
* <p>The method annotated with {@link AgentCoreInvocation} is wired to {@code POST
* /invocations} by the AgentCore runtime starter. Returning a plain {@code String} (rather
* than a {@code Flux}) yields a single non-streaming JSON response, which is what the
* calling durable function step expects.
*/
@RestController
public class ReviewAgentController {

private static final Logger logger = LoggerFactory.getLogger(ReviewAgentController.class);

private static final String ANALYZE_PROMPT = """
You are a document review assistant. Read the document supplied by the user and
produce a concise draft summary for a human reviewer.

Your response must contain:
1. A one-paragraph summary of the document's purpose.
2. Three to five key points as a bullet list.
3. Any concerns, ambiguities, or missing information a reviewer should check.

Be factual. Do not invent details that are not present in the document.
""";

private static final String FINALIZE_PROMPT = """
You are a document review assistant. A human reviewer has approved the draft
supplied by the user. Produce the final, polished version of the summary.

Apply any reviewer comments that are provided. Tighten the wording, remove
reviewer-only scaffolding such as open questions, and return prose suitable for
publication. Do not introduce new factual claims.
""";

private final ChatClient chatClient;

public ReviewAgentController(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder.build();
}

@AgentCoreInvocation
public String handleInvocation(AgentRequest request, AgentCoreContext agentCoreContext) {
String sessionId = agentCoreContext.getHeader(AgentCoreHeaders.SESSION_ID);
String mode = request.mode() == null ? "analyze" : request.mode();
logger.info("Handling '{}' invocation for session {}", mode, sessionId);

return switch (mode) {
case "analyze" -> chatClient.prompt()
.system(ANALYZE_PROMPT)
.user("Review the following document:\n\n" + request.document())
.call()
.content();
case "finalize" -> chatClient.prompt()
.system(FINALIZE_PROMPT)
.user(buildFinalizeMessage(request))
.call()
.content();
default -> throw new IllegalArgumentException(
"Unsupported mode '" + mode + "'. Expected 'analyze' or 'finalize'.");
};
}

private static String buildFinalizeMessage(AgentRequest request) {
StringBuilder message = new StringBuilder("Approved draft:\n\n").append(request.draft());
if (request.reviewerComments() != null && !request.reviewerComments().isBlank()) {
message.append("\n\nReviewer comments:\n").append(request.reviewerComments());
}
return message.toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
spring.application.name=document-review-agent

# AgentCore Runtime requires the container to listen on 0.0.0.0:8080
server.port=8080
server.address=0.0.0.0

# Region and model are injected as environment variables by the AgentCore runtime
# (see EnvironmentVariables on AWS::BedrockAgentCore::Runtime in template.yaml).
spring.ai.bedrock.aws.region=${AWS_REGION:us-east-1}
spring.ai.bedrock.converse.chat.options.model=${BEDROCK_MODEL_ID:us.anthropic.claude-sonnet-4-5-20250929-v1:0}

# The agent summarises documents; keep the response focused and deterministic.
spring.ai.bedrock.converse.chat.options.temperature=0.2
spring.ai.bedrock.converse.chat.options.max-tokens=2048

logging.level.root=INFO
logging.level.com.example.agent=DEBUG
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"documentId": "doc-2026-001",
"title": "Q3 Engineering Budget Proposal",
"documentText": "Q3 Engineering Budget Proposal\n\nWe propose increasing engineering headcount by 4 full-time equivalents in Q3, at a total cost of 620,000 USD. Two positions are backend engineers for the payments platform, one is a site reliability engineer, and one is a QA automation engineer. The increase is funded by reallocating the unspent Q2 contractor budget of 400,000 USD, with the remaining 220,000 USD drawn from the annual contingency reserve.\n\nThe payments platform has been operating with a two-person on-call rotation since January, which the engineering manager has flagged as a retention risk. The SRE hire is intended to bring the rotation to four people.\n\nApproval is required by August 15 so that recruiting can begin before the Q3 hiring freeze."
}
74 changes: 74 additions & 0 deletions lambda-durable-agentcore-springai-sam-java/example-pattern.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
{
"title": "Human-in-the-loop AI review with Lambda durable functions",
"description": "Orchestrate a Spring AI agent on Bedrock AgentCore from a Java Lambda durable function, pausing for human approval.",
"language": "Java",
"level": "300",
"framework": "AWS SAM",
"introBox": {
"headline": "How it works",
"text": [
"This pattern shows how to pause a long-running Java workflow for human approval of an AI agent's work. An AWS Lambda durable function orchestrates a Spring AI agent hosted on Amazon Bedrock AgentCore Runtime, suspending mid-workflow until a person approves or rejects the agent's draft.",
"The workflow runs three durable operations. First, a step invokes the Spring AI agent on AgentCore, which reads a document and drafts a summary highlighting key points and open concerns. Second, waitForCallback creates a callback and suspends the execution. At this point the function stops running entirely: no compute is billed while it waits, and it can stay suspended for up to the configured seven-day execution timeout. Third, once a decision arrives through SendDurableExecutionCallbackSuccess, Lambda starts a fresh invocation, replays the handler from the top while skipping the already-completed step, and resumes at the callback with the reviewer's decision. Approved documents go back to the agent for a final polish, reusing the same AgentCore session so the agent still has the earlier turn in context. Rejected ones skip that step entirely.",
"There is no database in this pattern, and that is the point: Lambda checkpoints step results, the pending callback and the handler's return value into storage the service manages, so the workflow's state is readable with get-durable-execution and get-durable-execution-history rather than from a table you provision. Submission and approval are both driven from the AWS CLI, which keeps the example focused on the durable execution model itself.",
"The agent is an ordinary Spring Boot application. The community spring-ai-agentcore-runtime-starter auto-configures the POST /invocations and GET /ping endpoints that AgentCore Runtime requires, so the agent code is a single method annotated with @AgentCoreInvocation that calls Amazon Bedrock through Spring AI's Converse integration. Because AgentCore hosts source code directly only for Python and Node, the Java agent ships as an ARM64 container image built by an included script and passed to the stack as a parameter.",
"Together these pieces show the durability model that makes agentic workflows practical: expensive, slow agent calls are checkpointed so they are never repeated on failure or replay, and the workflow can wait days for a human without holding an execution open or paying for idle time. The pattern deploys one Lambda durable function and one AgentCore runtime."
]
},
"gitHub": {
"template": {
"repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/lambda-durable-agentcore-springai-sam-java",
"templateURL": "serverless-patterns/lambda-durable-agentcore-springai-sam-java",
"projectFolder": "lambda-durable-agentcore-springai-sam-java",
"templateFile": "template.yaml"
}
},
"resources": {
"bullets": [
{
"text": "AWS Lambda durable functions",
"link": "https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html"
},
{
"text": "AWS Durable Execution SDK for Java",
"link": "https://docs.aws.amazon.com/durable-execution/sdk-reference/languages/java/"
},
{
"text": "Host an agent with Amazon Bedrock AgentCore Runtime",
"link": "https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/agents-tools-runtime.html"
},
{
"text": "Callback operations for human-in-the-loop workflows",
"link": "https://docs.aws.amazon.com/durable-execution/sdk-reference/operations/callback/"
},
{
"text": "Spring AI reference documentation",
"link": "https://docs.spring.io/spring-ai/reference/"
}
]
},
"deploy": {
"text": [
"./scripts/build-agent-image.sh",
"sam build && sam deploy --guided"
]
},
"testing": {
"text": [
"See the GitHub repo for detailed testing instructions."
]
},
"cleanup": {
"text": [
"Delete the stack: <code>sam delete</code>."
]
},
"authors": [
{
"name": "Paras Jain",
"image": "https://avatars.githubusercontent.com/u/583119?v=4",
"bio": "Paras is a Technical Account Manager with AWS based out of Herndon, Virginia, USA. He is a member of Serverless Technical Field community",
"linkedin": "parasjain01",
"twitter": "parasjain01"
}
]
}
Loading