DEV Community

Cover image for A Practical Guide to Building AI Agents With Java and Spring AI - Part 1 - Create an AI Agent
Yuriy Bezsonov
Yuriy Bezsonov

Posted on • Edited on • Originally published at tech.yuriybezsonov.com

A Practical Guide to Building AI Agents With Java and Spring AI - Part 1 - Create an AI Agent

Building AI-powered applications has become increasingly important for modern Java developers. With the rise of large language models and AI services, integrating intelligent capabilities into Java applications is no longer a luxury — it's a necessity for staying competitive.

Spring AI makes this integration seamless by providing a unified framework for building AI-powered applications with Java. Combined with Amazon Bedrock, developers can create sophisticated AI agents that leverage state-of-the-art foundation models without managing complex infrastructure.

In this post, I'll guide you through creating your first AI agent using Java and Spring AI, connected to Amazon Bedrock. We'll build a complete application with both REST API and web interface, demonstrating how to integrate AI capabilities into your Java applications.

Overview of the Solution

What is Spring AI?

Spring AI is a framework that brings the familiar Spring programming model to AI applications. It provides:

  • Unified API: A consistent interface for working with different AI models and providers
  • Seamless Integration: Native Spring Boot integration with auto-configuration
  • Multiple Providers: Support for OpenAI, Azure OpenAI, Amazon Bedrock, Ollama, and more
  • Advanced Features: Built-in support for RAG (Retrieval Augmented Generation), function calling, and chat memory
  • Production Ready: Observability, error handling, and resilience patterns

The framework abstracts the complexity of working with different AI providers, allowing you to focus on building your application logic rather than dealing with API specifics.

What is Amazon Bedrock?

Amazon Bedrock is a fully managed service that provides access to high-performing foundation models from leading AI companies including Anthropic, Meta, Stability AI, and Amazon's own models. Key benefits include:

  • No Infrastructure Management: Serverless architecture with automatic scaling
  • Multiple Models: Access to various foundation models through a single API
  • Security & Privacy: Your data stays within your AWS account
  • Cost Effective: Pay only for what you use with no upfront commitments

We'll create a Spring Boot application that demonstrates the core concepts of building AI agents.

The application will include:

  • Spring Boot web application with Thymeleaf templates
  • REST API for chat interactions
  • Integration with Amazon Bedrock via Spring AI
  • Streaming responses for better user experience

Prerequisites

Before you start, ensure you have:

  • Java 21 JDK installed (we'll use Amazon Corretto 21)
  • Maven 3.6+ installed
  • AWS CLI configured with appropriate permissions for Amazon Bedrock
  • Access to Amazon Bedrock models in your AWS account

The AI Agent

Context

We'll build a complete Spring Boot application that demonstrates the core concepts of building AI agents. The application will include both a REST API for programmatic access and a web interface for interactive testing.

Use Spring Initializer

We'll use Spring Initializer to bootstrap our project with the necessary dependencies:

curl https://start.spring.io/starter.zip \
  -d type=maven-project \
  -d language=java \
  -d bootVersion=3.5.7 \
  -d baseDir=ai-agent \
  -d groupId=com.example \
  -d artifactId=ai-agent \
  -d name=ai-agent \
  -d description="AI Agent with Spring AI and Amazon Bedrock" \
  -d packageName=com.example.ai.agent \
  -d packaging=jar \
  -d javaVersion=21 \
  -d dependencies=spring-ai-bedrock-converse,web,thymeleaf,actuator,devtools \
  -o ai-agent.zip

unzip ai-agent.zip
cd ai-agent
Enter fullscreen mode Exit fullscreen mode

Add a Git repository to the project to track changes

git init -b main
git add .
git commit -m "Initialize the AI agent"
Enter fullscreen mode Exit fullscreen mode

Configure Amazon Bedrock Integration

Create the application configuration in src/main/resources/application.properties:

cat >> src/main/resources/application.properties << 'EOF'

# Simplified logging pattern - only show the message
logging.pattern.console=%msg%n

# Debugging
logging.level.org.springframework.ai=DEBUG
spring.ai.chat.observations.log-completion=true
spring.ai.chat.observations.include-error-logging=true
spring.ai.tools.observations.include-content=true

# Thymeleaf Configuration
spring.thymeleaf.cache=false
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html

# Amazon Bedrock Configuration
spring.ai.bedrock.aws.region=us-east-1
spring.ai.bedrock.converse.chat.options.max-tokens=10000
spring.ai.bedrock.converse.chat.options.model=global.anthropic.claude-sonnet-4-20250514-v1:0
EOF
Enter fullscreen mode Exit fullscreen mode

The most important part of the properties is the GenAI model which we are going to use.
We're using Claude Sonnet 4 via Amazon Bedrock's cross-region inference profile.

Model ID Format:

  • global.anthropic.claude-sonnet-4-20250514-v1:0 - Cross-region inference profile
  • anthropic.claude-sonnet-4-20250514-v1:0 - Region-specific model

Why Cross-Region Inference Profiles?

Amazon Bedrock offers cross-region inference profiles that provide:

  1. Higher Availability: Automatically routes requests across multiple AWS regions
  2. Better Throughput: Distributes load to avoid throttling
  3. Lower Latency: Routes to the nearest available region
  4. Same Pricing: No additional cost compared to single-region access

Start with Claude Sonnet 4 using the global.* prefix for the best experience. You can always switch models by changing the configuration without code changes.

Create the Chat Service

The ChatClient interface is the core abstraction in Spring AI that provides:

  1. Unified API: Works with different AI model providers without code changes
  2. Streaming Support: Real-time response streaming for better user experience
  3. Prompt Management: Built-in support for structured prompts and templates
  4. Response Handling: Consistent response format across different models

Key methods include:

  • call(Prompt prompt): Synchronous response
  • stream(Prompt prompt): Streaming response
  • generate(List<Prompt> prompts): Batch processing

Create a service to handle chat interactions with the AI model:

mkdir -p src/main/java/com/example/ai/agent/service
cat <<'EOF' > src/main/java/com/example/ai/agent/service/ChatService.java
package com.example.ai.agent.service;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;

import org.springframework.ai.chat.client.ChatClient;

@Service
public class ChatService {
    private static final Logger logger = LoggerFactory.getLogger(ChatService.class);

    private final ChatClient chatClient;

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

    public Flux<String> processChat(String prompt) {
        logger.info("Processing streaming chat request - prompt: '{}'", prompt);
        try {
            return chatClient
                .prompt().user(prompt)
                .stream()
                .content();
        } catch (Exception e) {
            logger.error("Error processing streaming chat request", e);
            return Flux.just("I don't know - there was an error processing your request.");
        }
    }
}
EOF
Enter fullscreen mode Exit fullscreen mode

Create the Chat Controller

Create the REST controller that handles chat interactions:

mkdir -p src/main/java/com/example/ai/agent/controller
cat <<'EOF' > src/main/java/com/example/ai/agent/controller/ChatController.java
package com.example.ai.agent.controller;

import com.example.ai.agent.service.ChatService;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

@RestController
@RequestMapping("api/chat")
public class ChatController {
    private final ChatService chatService;

    public ChatController(ChatService chatService) {
        this.chatService = chatService;
    }

    @PostMapping(value = "message", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
    public Flux<String> chat(@RequestBody ChatRequest request) {
        return chatService.processChat(request.prompt());
    }

    public record ChatRequest(String prompt) {}
}
EOF
Enter fullscreen mode Exit fullscreen mode

Create the Web Controller

Create a controller to serve the web interface:

cat <<'EOF' > src/main/java/com/example/ai/agent/controller/WebViewController.java
package com.example.ai.agent.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class WebViewController {
    @GetMapping("/")
    public String index() {
        return "chat";
    }
}
EOF
Enter fullscreen mode Exit fullscreen mode

Create the Web Interface

Create a modern chat interface using Thymeleaf and Tailwind CSS:

cat <<'EOF' > src/main/resources/templates/chat.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <!-- ========== BASE: Core Dependencies ========== -->
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>AI Agent</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <script src="https://cdn.jsdelivr.net/npm/marked@9.1.2/marked.min.js"></script>
    <script>
        tailwind.config = { darkMode: 'class' }
    </script>
</head>
<body class="bg-white dark:bg-gray-900 min-h-screen text-gray-800 dark:text-slate-200 transition-colors duration-300">
    <div class="container mx-auto px-4 py-8">
        <!-- ========== BASE: Header ========== -->
        <header class="mb-8">
            <div class="flex justify-between items-center">
                <h1 class="text-3xl font-bold text-indigo-500">🤖 AI Agent</h1>
                <div class="flex items-center gap-2">
                    <!-- MEMORY: Multi-user Controls -->
                    <div th:if="${multiUserEnabled}" class="flex items-center gap-2">
                        <label for="userIdInput" class="text-sm text-gray-500 dark:text-slate-400">User:</label>
                        <input type="text" id="userIdInput" value="user1"
                               class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 px-3 py-1 rounded text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 text-gray-800 dark:text-slate-200 w-32">
                        <button id="summarizeBtn" class="bg-purple-600 text-white px-4 py-2 rounded hover:bg-purple-700 transition">
                            📝 Summarize
                        </button>
                    </div>
                    <!-- BASE: Dark Mode Toggle -->
                    <button id="themeToggle" class="bg-indigo-500 text-white px-4 py-2 rounded hover:bg-indigo-600 transition">
                        🌙 Dark
                    </button>
                </div>
            </div>
            <p class="text-gray-500 dark:text-slate-400 mt-2">Chat with our AI Agent to help you with your questions!</p>
        </header>

        <!-- ========== BASE: Chat Container ========== -->
        <div class="bg-slate-50 dark:bg-gray-800 rounded-xl shadow-lg p-6 chat-container border border-gray-200 dark:border-gray-700 flex flex-col">
            <!-- BASE: Message Container -->
            <div class="message-container flex-grow mb-4 bg-white dark:bg-gray-900 rounded-lg p-4" id="messageContainer">
                <div class="flex mb-4">
                    <div class="w-10 h-10 rounded-full bg-indigo-500 bg-opacity-30 flex items-center justify-center mr-3 flex-shrink-0">
                        <span class="text-lg">🤖</span>
                    </div>
                    <div class="message-bubble-ai rounded-lg p-3 max-w-3xl">
                        <p>Welcome to the AI Agent! How can I help you today?</p>
                    </div>
                </div>
            </div>

            <!-- BASE: Input Area -->
            <div class="border-t border-gray-200 dark:border-gray-700 pt-4 mt-auto">
                <form id="chatForm" class="flex w-full">
                    <!-- UPLOAD: File Input and Button -->
                    <input type="file" id="fileInput" class="hidden" accept=".jpg,.jpeg,.png">
                    <button th:if="${multiModalEnabled}" type="button" id="fileButton" class="bg-indigo-500 text-white px-4 py-3 rounded-l-lg hover:bg-indigo-600 transition flex items-center justify-center">
                        📎
                    </button>
                    <!-- BASE: Text Input -->
                    <input type="text" id="userInput" th:class="${multiModalEnabled} ? 'flex-grow bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 px-4 py-3 focus:outline-none focus:ring-2 focus:ring-indigo-500 text-gray-800 dark:text-slate-200' : 'flex-grow bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 px-4 py-3 rounded-l-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 text-gray-800 dark:text-slate-200'" placeholder="Please ask your question...">
                    <!-- BASE: Send Button -->
                    <button type="submit" class="bg-indigo-500 text-white px-6 py-3 rounded-r-lg hover:bg-indigo-600 transition">Send</button>
                </form>
            </div>
        </div>
    </div>

    <!-- ========== UPLOAD: Image Modal ========== -->
    <div id="imageModal" class="image-modal">
        <span class="close">&times;</span>
        <img id="modalImage" src="" alt="">
    </div>

    <script>
        document.addEventListener('DOMContentLoaded', function() {
            // ========== BASE: Initialize Marked.js ==========
            marked.setOptions({
                gfm: true,
                tables: true,
                breaks: true
            });

            // ========== BASE: DOM Elements ==========
            const messageContainer = document.getElementById('messageContainer');
            const chatForm = document.getElementById('chatForm');
            const userInput = document.getElementById('userInput');
            const themeToggle = document.getElementById('themeToggle');
            const fileInput = document.getElementById('fileInput');
            const fileButton = document.getElementById('fileButton');

            // ========== BASE: Chat Form Submit ==========
            chatForm.addEventListener('submit', async function(e) {
                e.preventDefault();
                const message = userInput.value.trim();
                if (!message) return;

                addMessage(message, 'user');
                userInput.value = '';

                try {
                    const loadingId = showLoading();
                    const userIdInput = document.getElementById('userIdInput');
                    const response = await fetch('api/chat/message', {
                        method: 'POST',
                        headers: {
                            'Content-Type': 'application/json',
                            'Accept': 'application/octet-stream'
                        },
                        body: JSON.stringify({
                            prompt: message,
                            userId: userIdInput ? userIdInput.value : null,
                            fileBase64: currentFileBase64 || null,
                            fileName: currentFileName || null
                        })
                    });

                    if (!response.ok) throw new Error('Failed to get response');
                    removeLoading(loadingId);

                    // Handle streaming response
                    const reader = response.body.getReader();
                    const decoder = new TextDecoder();
                    let fullResponse = '';
                    let messageDiv = null;

                    while (true) {
                        const { done, value } = await reader.read();
                        if (done) break;

                        const chunk = decoder.decode(value, { stream: true });
                        fullResponse += chunk;

                        if (!messageDiv) {
                            messageDiv = createStreamingMessage();
                        }
                        updateStreamingMessage(messageDiv, fullResponse);
                    }
                } catch (error) {
                    removeLoading(loadingId);
                    console.error('Error:', error);
                    addMessage('Sorry, I encountered an error. Please try again.', 'ai');
                }
            });

            // ========== BASE: Message Functions ==========
            function createStreamingMessage() {
                const messageDiv = document.createElement('div');
                messageDiv.className = 'flex mb-4';
                messageDiv.innerHTML = `
                    <div class="w-10 h-10 rounded-full bg-indigo-500 bg-opacity-30 flex items-center justify-center mr-3 flex-shrink-0">
                        <span class="text-lg">🤖</span>
                    </div>
                    <div class="message-bubble-ai rounded-lg p-3 max-w-4xl ai-response">
                        <button class="copy-button" data-copy-text="">📋</button>
                        <div class="streaming-content"></div>
                    </div>
                `;
                messageContainer.appendChild(messageDiv);
                messageContainer.scrollTop = messageContainer.scrollHeight;
                return messageDiv;
            }

            function updateStreamingMessage(messageDiv, content) {
                const contentDiv = messageDiv.querySelector('.streaming-content');
                const copyButton = messageDiv.querySelector('.copy-button');
                const renderedContent = marked.parse(content);
                contentDiv.innerHTML = renderedContent;
                copyButton.setAttribute('data-copy-text', escapeHtml(content));
                messageContainer.scrollTop = messageContainer.scrollHeight;
            }

            function addMessage(content, sender) {
                const messageDiv = document.createElement('div');
                messageDiv.className = 'flex mb-4';

                if (sender === 'user') {
                    messageDiv.innerHTML = `
                        <div class="ml-auto flex">
                            <div class="message-bubble-user rounded-lg p-3 max-w-3xl">
                                <button class="copy-button" data-copy-text="${escapeHtml(content)}">📋</button>
                                <p>${escapeHtml(content)}</p>
                            </div>
                            <div class="w-10 h-10 rounded-full bg-blue-500 bg-opacity-30 flex items-center justify-center ml-3 flex-shrink-0">
                                <span class="text-lg">👤</span>
                            </div>
                        </div>
                    `;
                } else {
                    const renderedContent = marked.parse(content);
                    messageDiv.innerHTML = `
                        <div class="w-10 h-10 rounded-full bg-indigo-500 bg-opacity-30 flex items-center justify-center mr-3 flex-shrink-0">
                            <span class="text-lg">🤖</span>
                        </div>
                        <div class="message-bubble-ai rounded-lg p-3 max-w-4xl ai-response">
                            <button class="copy-button" data-copy-text="${escapeHtml(content)}">📋</button>
                            ${renderedContent}
                        </div>
                    `;
                }

                messageContainer.appendChild(messageDiv);
                messageContainer.scrollTop = messageContainer.scrollHeight;
            }

            // ========== BASE: Loading Indicator ==========
            function showLoading() {
                const loadingId = 'loading-' + Date.now();
                const loadingDiv = document.createElement('div');
                loadingDiv.id = loadingId;
                loadingDiv.className = 'flex mb-4';
                loadingDiv.innerHTML = `
                    <div class="w-10 h-10 rounded-full bg-indigo-500 bg-opacity-30 flex items-center justify-center mr-3 flex-shrink-0">
                        <span class="text-lg">🤖</span>
                    </div>
                    <div class="message-bubble-ai rounded-lg p-3">
                        <div class="flex space-x-2">
                            <div class="w-2 h-2 bg-indigo-500 rounded-full animate-bounce"></div>
                            <div class="w-2 h-2 bg-indigo-500 rounded-full animate-bounce" style="animation-delay: 0.2s"></div>
                            <div class="w-2 h-2 bg-indigo-500 rounded-full animate-bounce" style="animation-delay: 0.4s"></div>
                        </div>
                    </div>
                `;
                messageContainer.appendChild(loadingDiv);
                messageContainer.scrollTop = messageContainer.scrollHeight;
                return loadingId;
            }

            function removeLoading(loadingId) {
                const loadingDiv = document.getElementById(loadingId);
                if (loadingDiv) loadingDiv.remove();
            }

            // ========== BASE: Utility Functions ==========
            function escapeHtml(unsafe) {
                return unsafe
                    .replace(/&/g, "&amp;")
                    .replace(/</g, "&lt;")
                    .replace(/>/g, "&gt;")
                    .replace(/"/g, "&quot;")
                    .replace(/'/g, "&#039;");
            }

            function copyToClipboard(text) {
                navigator.clipboard.writeText(text).then(function() {
                    console.log('Text copied to clipboard');
                }).catch(function(err) {
                    console.error('Failed to copy text: ', err);
                });
            }

            messageContainer.addEventListener('click', function(e) {
                if (e.target.classList.contains('copy-button')) {
                    const textToCopy = e.target.getAttribute('data-copy-text');
                    copyToClipboard(textToCopy);
                }
            });

            // ========== BASE: Dark Mode Toggle ==========
            themeToggle.addEventListener('click', function() {
                const html = document.documentElement;
                html.classList.toggle('dark');
                themeToggle.innerHTML = html.classList.contains('dark') ? '☀️ Light' : '🌙 Dark';
            });

            // ========== MEMORY: Summarize Button ==========
            const summarizeBtn = document.getElementById('summarizeBtn');
            if (summarizeBtn) {
                summarizeBtn.addEventListener('click', async function() {
                    try {
                        const userId = document.getElementById('userIdInput').value;
                        const loadingId = showLoading();

                        const response = await fetch('api/chat/summarize', {
                            method: 'POST',
                            headers: { 'Content-Type': 'application/json' },
                            body: JSON.stringify({ userId: userId })
                        });

                        removeLoading(loadingId);
                        if (!response.ok) throw new Error('Failed to summarize');
                        const summary = await response.text();
                        addMessage('📝 ' + summary, 'ai');
                    } catch (error) {
                        console.error('Error summarizing:', error);
                        addMessage('Failed to summarize conversation. Please try again.', 'ai');
                    }
                });
            }

            // ========== UPLOAD: File State ==========
            let currentFileBase64 = null;
            let currentFileName = null;

            // ========== UPLOAD: File Upload Handler ==========
            if (fileButton) {
                fileButton.addEventListener('click', function() {
                    fileInput.click();
                });
            }

            if (fileInput) {
                fileInput.addEventListener('change', async function(e) {
                    const file = e.target.files[0];
                    if (file) {
                        try {
                            currentFileBase64 = await fileToBase64(file);
                            currentFileName = file.name;
                            addImageMessage(file);
                            await sendDocumentAnalysis();
                        } catch (error) {
                            console.error('Error processing file:', error);
                            addMessage('Error processing file. Please try again.', 'ai');
                        }
                    }
                });
            }

            function addImageMessage(file) {
                const messageDiv = document.createElement('div');
                messageDiv.className = 'flex mb-4';
                const imageUrl = URL.createObjectURL(file);
                messageDiv.innerHTML = `
                    <div class="ml-auto flex">
                        <div class="message-bubble-user rounded-lg p-3 max-w-3xl">
                            <button class="copy-button" data-copy-text="File attached: ${file.name}">📋</button>
                            <p class="mb-2">📎 File attached: ${file.name}</p>
                            <img src="${imageUrl}" alt="${file.name}" class="max-w-full max-h-64 rounded border border-gray-200 dark:border-gray-700 cursor-pointer hover:opacity-80 transition" onclick="openImageModal('${imageUrl}')" />
                        </div>
                        <div class="w-10 h-10 rounded-full bg-blue-500 bg-opacity-30 flex items-center justify-center ml-3 flex-shrink-0">
                            <span class="text-lg">👤</span>
                        </div>
                    </div>
                `;
                messageContainer.appendChild(messageDiv);
                messageContainer.scrollTop = messageContainer.scrollHeight;
            }

            async function sendDocumentAnalysis() {
                try {
                    const loadingId = showLoading();
                    const response = await fetch('api/chat/message', {
                        method: 'POST',
                        headers: { 'Content-Type': 'application/json' },
                        body: JSON.stringify({
                            prompt: "",
                            fileBase64: currentFileBase64,
                            fileName: currentFileName
                        })
                    });
                    removeLoading(loadingId);
                    if (!response.ok) throw new Error('Failed to analyze document');

                    const reader = response.body.getReader();
                    const decoder = new TextDecoder();
                    const messageId = createStreamingMessage();
                    let fullText = '';

                    while (true) {
                        const { done, value } = await reader.read();
                        if (done) break;
                        const chunk = decoder.decode(value, { stream: true });
                        fullText += chunk;
                        updateStreamingMessage(messageId, fullText);
                    }

                    currentFileBase64 = null;
                    currentFileName = null;
                    fileInput.value = '';
                } catch (error) {
                    console.error('Error analyzing document:', error);
                    addMessage('I don\'t know - there was an error analyzing the document.', 'ai');
                    currentFileBase64 = null;
                    currentFileName = null;
                    fileInput.value = '';
                }
            }

            function fileToBase64(file) {
                return new Promise((resolve, reject) => {
                    const reader = new FileReader();
                    reader.readAsDataURL(file);
                    reader.onload = () => resolve(reader.result.split(',')[1]);
                    reader.onerror = error => reject(error);
                });
            }

            // ========== UPLOAD: Image Modal ==========
            window.openImageModal = function(imageSrc) {
                const modal = document.getElementById('imageModal');
                const modalImg = document.getElementById('modalImage');
                modal.style.display = 'block';
                modalImg.src = imageSrc;
            }

            const imageModal = document.getElementById('imageModal');
            if (imageModal) {
                imageModal.addEventListener('click', function(e) {
                    if (e.target === this || e.target.className === 'close') {
                        this.style.display = 'none';
                    }
                });
            }
        });
    </script>

    <!-- ========== STYLES ========== -->
    <style>
        /* ========== BASE: Layout ========== */
        .chat-container {
            height: calc(100vh - 180px);
        }
        .message-container {
            height: calc(100vh - 280px);
            overflow-y: auto;
        }

        /* ========== BASE: Markdown Tables ========== */
        .ai-response table {
            width: 100%;
            border-collapse: collapse;
            margin: 1rem 0;
            border-radius: 8px;
            overflow: hidden;
            background: #ffffff;
            border: 1px solid #e5e7eb;
        }
        .dark .ai-response table {
            background: #1e293b !important;
            border: 1px solid #374151 !important;
        }
        .ai-response th {
            background: #6366f1 !important;
            color: white !important;
            padding: 12px;
            text-align: left;
            font-weight: 600;
        }
        .ai-response td {
            padding: 10px 12px;
            border-bottom: 1px solid #e5e7eb;
            color: #111827 !important;
            background: #ffffff !important;
            font-weight: 600 !important;
        }
        .dark .ai-response td {
            border-bottom: 1px solid #374151 !important;
            color: #f9fafb !important;
            background: #1e293b !important;
        }
        .ai-response tr:last-child td {
            border-bottom: none;
        }
        .ai-response tr:nth-child(even) td {
            background: #f1f5f9 !important;
            color: #111827 !important;
        }
        .dark .ai-response tr:nth-child(even) td {
            background: #334155 !important;
            color: #f9fafb !important;
        }

        /* BASE: Markdown Headings and Text */
        .ai-response h1, .ai-response h2, .ai-response h3 {
            color: #6366f1;
            margin: 1rem 0 0.5rem 0;
        }
        .ai-response strong {
            color: #111827 !important;
            font-weight: 700 !important;
        }
        .dark .ai-response strong {
            color: #f9fafb !important;
            font-weight: 700 !important;
        }
        .ai-response em {
            color: #6b7280;
        }
        .dark .ai-response em {
            color: #94a3b8;
        }
        .ai-response p {
            color: #1f2937;
        }
        .dark .ai-response p {
            color: #e2e8f0;
        }

        /* ========== BASE: Message Bubbles ========== */
        .message-bubble-ai {
            background: #ffffff;
            border: 1px solid #e5e7eb;
            color: #1f2937;
            position: relative;
        }
        .dark .message-bubble-ai {
            background: #1e293b !important;
            border: 1px solid #374151 !important;
            color: #e2e8f0 !important;
        }
        .message-bubble-user {
            background: #f3f4f6;
            border: 1px solid #e5e7eb;
            color: #1f2937;
            position: relative;
        }
        .dark .message-bubble-user {
            background: #1e293b !important;
            border: 1px solid #374151 !important;
            color: #e2e8f0 !important;
        }
        .message-bubble-ai p, .message-bubble-ai * {
            color: #1f2937 !important;
        }
        .dark .message-bubble-ai p, .dark .message-bubble-ai * {
            color: #e2e8f0 !important;
        }
        .message-bubble-user p, .message-bubble-user * {
            color: #1f2937 !important;
        }
        .dark .message-bubble-user p, .dark .message-bubble-user * {
            color: #e2e8f0 !important;
        }

        /* BASE: Copy Button */
        .copy-button {
            position: absolute;
            top: 8px;
            right: 8px;
            background: rgba(99, 102, 241, 0.8);
            color: white;
            border: none;
            border-radius: 4px;
            padding: 4px 8px;
            font-size: 12px;
            cursor: pointer;
            opacity: 0;
            transition: opacity 0.2s;
        }
        .copy-button:hover {
            background: rgba(99, 102, 241, 1);
        }
        .message-bubble-ai:hover .copy-button,
        .message-bubble-user:hover .copy-button {
            opacity: 1;
        }

        /* ========== UPLOAD: Image Modal ========== */
        .image-modal {
            display: none;
            position: fixed;
            z-index: 1000;
            left: 0;
            top: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(0, 0, 0, 0.9);
        }
        .image-modal img {
            margin: auto;
            display: block;
            max-width: 90%;
            max-height: 90%;
            margin-top: 5%;
        }
        .image-modal .close {
            position: absolute;
            top: 15px;
            right: 35px;
            color: #f1f1f1;
            font-size: 40px;
            font-weight: bold;
            cursor: pointer;
        }
        .image-modal .close:hover {
            color: #bbb;
        }
    </style>
</body>
</html>
EOF
Enter fullscreen mode Exit fullscreen mode

Running the Application

Configure AWS credentials in the terminal. The assumed role or user should have access to Amazon Bedrock models.

export AWS_REGION=us-east-1
Enter fullscreen mode Exit fullscreen mode

Start the Application

./mvnw spring-boot:run
Enter fullscreen mode Exit fullscreen mode

Test the Application

Test REST API

Open the new terminal and test the REST API with curl:

curl -X POST http://localhost:8080/api/chat/message \
  -H "Content-Type: application/json" \
  -d '{"prompt": "Hi, my name is Alex. Who are you?"}'
Enter fullscreen mode Exit fullscreen mode

Success! The API responds with the AI agent's message.

Test Web Interface

Open your browser and navigate to http://localhost:8080 and try the same interaction:

Hi, my name is Alex. Who are you?

The first chat with the AI Agent

Success! The AI agent responds, but its responses are generic.

Problem: The agent lacks a specific persona and business context.

Let's fix this by adding an AI Agent Persona.

Stop the application with Ctrl+C.

Commit Changes

git add .
git commit -m "Create the AI agent"
Enter fullscreen mode Exit fullscreen mode

System Prompt and Persona

The system prompt defines the AI agent's personality and behavior:

ChatService.java

    ...
    public static final String SYSTEM_PROMPT = """
        You are a helpful AI Agent for travel and expenses.

        Guidelines:
        1. Use markdown tables for structured data
        2. If unsure, say "I don't know"
        """;
    ...
            this.chatClient = chatClientBuilder
                .defaultSystem(SYSTEM_PROMPT)
                .build();
    ...
Enter fullscreen mode Exit fullscreen mode

This gives the AI agent:

  • A specific business context (Travel and Expenses Agent)
  • Behavioral guidelines (helpful)
  • Honesty constraints (admit when it doesn't know something)

Test the Application

Test the AI agent with the persona using the Web UI:

Hi, my name is Alex. Who are you?

or the REST API:

curl -X POST http://localhost:8080/api/chat/message \
  -H "Content-Type: application/json" \
  -d '{"prompt": "Hi, my name is Alex. Who are you?"}' \
  --no-buffer
Enter fullscreen mode Exit fullscreen mode

The AI Agent has a Persona now

Success! The AI agent now has a persona aligned with our requirements!

Let's continue the chat:

What is my name?

The AI Agent doesn't yet have memory

Problem: The AI agent doesn't remember our name, even though we provided it.

We'll fix this in the next part of the blog series! Stay tuned!

Cleanup

To stop the application, press Ctrl+C in the terminal where it's running.

Commit Changes

git add .
git commit -m "Add the Persona"
Enter fullscreen mode Exit fullscreen mode

Conclusion

In this post, we've successfully created a complete AI agent application using Java and Spring AI with Amazon Bedrock integration:

  • Setting up a Spring Boot project with Spring AI dependencies
  • Configuring Amazon Bedrock integration
  • Creating REST APIs with streaming responses
  • Building a modern web interface with streaming chat
  • Implementing AI agent personas with system prompts

The application demonstrates the power of Spring AI's unified programming model, making it easy to integrate sophisticated AI capabilities into Java applications.

What's Next:

Add multi-tier memory to the AI Agent!

GenAI Challenges and Solutions

Learn More:

Let's continue building intelligent Java applications with Spring AI!

Top comments (2)

Collapse
 
cyber8080 profile image
Cyber Safety Zone

Great article, Yuriy — thanks for sharing this detailed, hands-on guide to building AI agents with Java and Spring AI!

A few thoughts:

  • I really appreciate how you break down the process in Part 1 with clear, step-by-step instructions and code samples — that makes it accessible even for devs relatively new to AI.
  • One question: how would you integrate security or access controls into the AI agent you build here? For example, protecting the agent’s credentials or limiting what the agent can access in production.
  • It might be useful to include a section on testing and validating the agent’s behaviour (unit tests, mock endpoints, failure conditions) since AI agents often encounter unexpected input.
  • I’m looking forward to Part 2 — especially how you’ll handle state, memory, or long-running workflows in your Spring-based AI agent.

Thanks again for the practical example — very helpful for developers exploring AI agent architecture!

Collapse
 
yuriybezsonov profile image
Yuriy Bezsonov

Thank you for the detailed feedback. Yes, security and testing definitely have some specifics for an agentic application. I will do my best to include them in the upcoming posts.