.card { transition: transform 0.3s ease, box-shadow 0.3s ease; border-radius: 8px; overflow: hidden; margin-bottom: 20px; } .card:hover { transform: translateY(-5px); box-shadow: 0 10px 20px rgba(0,0,0,0.1); } .diagram-container { background-color: white; border-radius: 5px; padding: 1rem; margin: 1rem 0; text-align: center; } .step-number { background-color: #00c9ff; color: white; width: 30px; height: 30px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; margin-right: 10px; font-weight: bold; } .toc-item { padding: 8px 0; border-bottom: 1px solid #eee; } .toc-item:last-child { border-bottom: none; } .section-title { border-left: 4px solid #00c9ff; padding-left: 15px; margin-top: 2rem; margin-bottom: 1rem; color: #333; } .nav-link { text-decoration: none; color: #00c9ff; font-weight: 500; } .nav-link:hover { text-decoration: underline; } .footer { background-color: #333; color: white; padding: 2rem 0; margin-top: 3rem; } .feature-icon { font-size: 2rem; margin-bottom: 10px; color: #00c9ff; } .level-badge { background-color: #00c9ff; color: white; padding: 3px 8px; border-radius: 12px; font-size: 0.8rem; margin-left: 10px; } .beginner { background-color: #28a745; } .intermediate { background-color: #ffc107; color: black; } .advanced { background-color: #dc3545; } pre { border-radius: 5px; overflow-x: auto; } .explanation-box { background-color: #e6f7ff; border-left: 4px solid #00c9ff; padding: 1rem; margin: 1rem 0; border-radius: 0 5px 5px 0; } .use-case-box { background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 1rem; margin: 1rem 0; border-radius: 0 5px 5px 0; } .ai-code { background-color: #e8f5e9; } .chat-code { background-color: #fff3e0; } .vector-code { background-color: #e3f2fd; }

Spring AI: Artificial Intelligence Integration

Building AI-Powered Applications with Spring Boot

1. Introduction to Spring AI

Spring AI is a new project from the Spring team that provides abstractions and implementations for integrating artificial intelligence capabilities into Spring Boot applications. It simplifies working with various AI models and services while maintaining the familiar Spring programming model.

What is Spring AI?

Spring AI provides a consistent abstraction layer for working with different AI models and services. It allows developers to build AI-powered applications without being locked into specific AI providers or models.

Key Features:

  • Model Abstraction: Unified API for different AI models (OpenAI, Azure, Hugging Face, etc.)
  • Chat Models: Support for conversational AI and chat interfaces
  • Embeddings: Text embedding generation for semantic search
  • Vector Stores: Integration with vector databases (Redis, Pinecone, etc.)
  • RAG Support: Retrieval-Augmented Generation patterns
  • Function Calling: Enable AI models to call external functions

Why Spring AI?

Benefits:

  • Spring Boot Familiarity
  • Provider Agnostic
  • Easy Integration
  • Production Ready
  • Extensible Architecture

Use Cases:

  • Chatbots and Virtual Assistants
  • Content Generation
  • Semantic Search
  • Document Analysis
  • Recommendation Systems

2. Core Concepts

Spring AI Architecture

graph TD A[Spring Boot App] --> B[Spring AI] B --> C[Chat Models] B --> D[Embedding Models] B --> E[Vector Stores] B --> F[Prompts] C --> G[OpenAI] C --> H[Azure OpenAI] C --> I[Hugging Face] D --> J[OpenAI Embeddings] D --> K[Azure Embeddings] E --> L[Redis] E --> M[Pinecone] E --> N[Chroma]

Main Components

graph LR A[ChatClient] --> B[ChatModel] A --> C[PromptTemplate] B --> D[AI Provider] C --> E[Messages] E --> F[System Message] E --> G[User Message] E --> H[Assistant Message] D --> I[Response]

Core Concepts Explained:

  • ChatModel: Interface for interacting with chat-based AI models
  • EmbeddingModel: Interface for generating text embeddings
  • VectorStore: Interface for storing and searching vector embeddings
  • PromptTemplate: Template-based approach for creating prompts
  • ChatClient: High-level client for chat interactions

3. Setup & Configuration

Maven Dependencies

<dependencies>
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
        <version>0.8.1</version>
    </dependency>
    
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-redis-spring-boot-starter</artifactId>
        <version>0.8.1</version>
    </dependency>
    
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-pdf-document-reader</artifactId>
        <version>0.8.1</version>
    </dependency>
</dependencies>

<repositories>
    <repository>
        <id>spring-milestones</id>
        <name>Spring Milestones</name>
        <url>https://repo.spring.io/milestone</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
</repositories>

Application Properties

# OpenAI Configuration
spring.ai.openai.api-key=${OPENAI_API_KEY}
spring.ai.openai.chat.options.model=gpt-3.5-turbo
spring.ai.openai.chat.options.temperature=0.7

# Redis Configuration (for vector store)
spring.redis.host=localhost
spring.redis.port=6379

# Embedding Configuration
spring.ai.openai.embedding.options.model=text-embedding-ada-002

# Chat Options
spring.ai.chat.client.enabled=true
spring.ai.chat.client.default-options.model=gpt-3.5-turbo
spring.ai.chat.client.default-options.temperature=0.7

Spring Boot Application

package com.example.springai;

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

@SpringBootApplication
public class SpringAiApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringAiApplication.class, args);
    }
}

Setup Requirements:

  • API Keys: Obtain keys from AI providers (OpenAI, Azure, etc.)
  • Vector Store: Set up Redis, Pinecone, or other vector databases
  • Dependencies: Add Spring AI starters to your project
  • Configuration: Configure API keys and model options

4. Chat Models

Basic Chat Interaction

package com.example.springai.service;

import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.chat.ChatResponse;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class ChatService {
    
    @Autowired
    private ChatClient chatClient;
    
    public String getChatResponse(String userMessage) {
        Prompt prompt = new Prompt(new UserMessage(userMessage));
        ChatResponse response = chatClient.call(prompt);
        return response.getResult().getOutput().getContent();
    }
    
    public String getChatResponseWithContext(String userMessage, String context) {
        String systemMessage = "You are a helpful assistant. Use the following context: " + context;
        Prompt prompt = new Prompt(
            new SystemMessage(systemMessage),
            new UserMessage(userMessage)
        );
        ChatResponse response = chatClient.call(prompt);
        return response.getResult().getOutput().getContent();
    }
}

Chat Controller

package com.example.springai.controller;

import com.example.springai.service.ChatService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/chat")
public class ChatController {
    
    @Autowired
    private ChatService chatService;
    
    @PostMapping("/message")
    public String chat(@RequestBody String message) {
        return chatService.getChatResponse(message);
    }
    
    @PostMapping("/message-with-context")
    public String chatWithContext(@RequestBody ChatRequest request) {
        return chatService.getChatResponseWithContext(request.getMessage(), request.getContext());
    }
    
    @GetMapping("/greet")
    public String greet(@RequestParam(defaultValue = "World") String name) {
        String message = "Say hello to " + name + " in a friendly way.";
        return chatService.getChatResponse(message);
    }
}

class ChatRequest {
    private String message;
    private String context;
    
    // Getters and setters
    public String getMessage() { return message; }
    public void setMessage(String message) { this.message = message; }
    
    public String getContext() { return context; }
    public void setContext(String context) { this.context = context; }
}

Prompt Templates

package com.example.springai.service;

import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.chat.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Map;

@Service
public class PromptTemplateService {
    
    @Autowired
    private ChatClient chatClient;
    
    public String generateStory(String genre, String character, int wordCount) {
        String template = """
            Write a {genre} story about {character}.
            The story should be approximately {wordCount} words long.
            Make it engaging and creative.
            """;
            
        PromptTemplate promptTemplate = new PromptTemplate(template);
        Prompt prompt = promptTemplate.create(Map.of(
            "genre", genre,
            "character", character,
            "wordCount", wordCount
        ));
        
        ChatResponse response = chatClient.call(prompt);
        return response.getResult().getOutput().getContent();
    }
    
    public String translateText(String text, String targetLanguage) {
        String template = """
            Translate the following text to {language}:
            {text}
            """;
            
        PromptTemplate promptTemplate = new PromptTemplate(template);
        Prompt prompt = promptTemplate.create(Map.of(
            "language", targetLanguage,
            "text", text
        ));
        
        ChatResponse response = chatClient.call(prompt);
        return response.getResult().getOutput().getContent();
    }
}

5. Embeddings

Embedding Generation

package com.example.springai.service;

import org.springframework.ai.embedding.EmbeddingClient;
import org.springframework.ai.embedding.EmbeddingRequest;
import org.springframework.ai.embedding.EmbeddingResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class EmbeddingService {
    
    @Autowired
    private EmbeddingClient embeddingClient;
    
    public List<Float> generateEmbedding(String text) {
        EmbeddingRequest request = new EmbeddingRequest(List.of(text));
        EmbeddingResponse response = embeddingClient.embed(request);
        return response.getResults().get(0).getOutput();
    }
    
    public List<List<Float>> generateEmbeddings(List<String> texts) {
        EmbeddingRequest request = new EmbeddingRequest(texts);
        EmbeddingResponse response = embeddingClient.embed(request);
        return response.getResults().stream()
            .map(result -> result.getOutput())
            .collect(Collectors.toList());
    }
    
    public double calculateSimilarity(String text1, String text2) {
        List<Float> embedding1 = generateEmbedding(text1);
        List<Float> embedding2 = generateEmbedding(text2);
        return cosineSimilarity(embedding1, embedding2);
    }
    
    private double cosineSimilarity(List<Float> vectorA, List<Float> vectorB) {
        double dotProduct = 0.0;
        double normA = 0.0;
        double normB = 0.0;
        
        for (int i = 0; i < vectorA.size(); i++) {
            dotProduct += vectorA.get(i) * vectorB.get(i);
            normA += Math.pow(vectorA.get(i), 2);
            normB += Math.pow(vectorB.get(i), 2);
        }
        
        return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
    }
}

Semantic Search Service

package com.example.springai.service;

import org.springframework.ai.embedding.EmbeddingClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

@Service
public class SemanticSearchService {
    
    @Autowired
    private EmbeddingClient embeddingClient;
    
    private final Map<String, List<Float>> documentEmbeddings = new ConcurrentHashMap<>();
    private final Map<String, String> documents = new ConcurrentHashMap<>();
    
    public void addDocument(String id, String content) {
        documents.put(id, content);
        List<Float> embedding = embeddingClient.embed(content).getResults().get(0).getOutput();
        documentEmbeddings.put(id, embedding);
    }
    
    public List<SearchResult> search(String query, int topK) {
        List<Float> queryEmbedding = embeddingClient.embed(query).getResults().get(0).getOutput();
        
        List<SearchResult> results = new ArrayList<>();
        for (Map.Entry<String, List<Float>> entry : documentEmbeddings.entrySet()) {
            double similarity = cosineSimilarity(queryEmbedding, entry.getValue());
            results.add(new SearchResult(entry.getKey(), documents.get(entry.getKey()), similarity));
        }
        
        return results.stream()
            .sorted((a, b) -> Double.compare(b.getSimilarity(), a.getSimilarity()))
            .limit(topK)
            .collect(Collectors.toList());
    }
    
    private double cosineSimilarity(List<Float> vectorA, List<Float> vectorB) {
        double dotProduct = 0.0;
        double normA = 0.0;
        double normB = 0.0;
        
        for (int i = 0; i < vectorA.size(); i++) {
            dotProduct += vectorA.get(i) * vectorB.get(i);
            normA += Math.pow(vectorA.get(i), 2);
            normB += Math.pow(vectorB.get(i), 2);
        }
        
        return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
    }
    
    public static class SearchResult {
        private String id;
        private String content;
        private double similarity;
        
        public SearchResult(String id, String content, double similarity) {
            this.id = id;
            this.content = content;
            this.similarity = similarity;
        }
        
        // Getters
        public String getId() { return id; }
        public String getContent() { return content; }
        public double getSimilarity() { return similarity; }
    }
}

6. Vector Stores

Redis Vector Store Configuration

package com.example.springai.config;

import org.springframework.ai.embedding.EmbeddingClient;
import org.springframework.ai.vectorstore.RedisVectorStore;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;

@Configuration
public class VectorStoreConfig {
    
    @Bean
    public VectorStore vectorStore(RedisConnectionFactory redisConnectionFactory, 
                                 EmbeddingClient embeddingClient) {
        return new RedisVectorStore.Builder(redisConnectionFactory, embeddingClient)
            .withIndexName("documents")
            .withMetadataFields("title", "category", "created_date")
            .build();
    }
}

Vector Store Service

package com.example.springai.service;

import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.*;

@Service
public class VectorStoreService {
    
    @Autowired
    private VectorStore vectorStore;
    
    public void addDocument(String id, String content, Map<String, Object> metadata) {
        Document document = new Document(id, content, metadata);
        vectorStore.add(List.of(document));
    }
    
    public List<Document> searchSimilarDocuments(String query, int topK) {
        return vectorStore.similaritySearch(query, topK);
    }
    
    public List<Document> searchWithFilter(String query, Map<String, Object> filter) {
        return vectorStore.similaritySearch(query, filter);
    }
    
    public void addMultipleDocuments(List<DocumentData> documents) {
        List<Document> docs = documents.stream()
            .map(docData -> new Document(docData.getId(), docData.getContent(), docData.getMetadata()))
            .collect(Collectors.toList());
        vectorStore.add(docs);
    }
    
    public static class DocumentData {
        private String id;
        private String content;
        private Map<String, Object> metadata;
        
        // Constructors, getters, and setters
        public DocumentData() {}
        
        public DocumentData(String id, String content, Map<String, Object> metadata) {
            this.id = id;
            this.content = content;
            this.metadata = metadata;
        }
        
        public String getId() { return id; }
        public void setId(String id) { this.id = id; }
        
        public String getContent() { return content; }
        public void setContent(String content) { this.content = content; }
        
        public Map<String, Object> getMetadata() { return metadata; }
        public void setMetadata(Map<String, Object> metadata) { this.metadata = metadata; }
    }
}

Document Processing

package com.example.springai.service;

import org.springframework.ai.document.Document;
import org.springframework.ai.document.DocumentReader;
import org.springframework.ai.reader.pdf.PagePdfDocumentReader;
import org.springframework.ai.reader.tika.TikaDocumentReader;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class DocumentProcessingService {
    
    @Autowired
    private VectorStore vectorStore;
    
    public void processPdfDocument(Resource pdfResource, String documentId) {
        // Read PDF document
        DocumentReader pdfReader = new PagePdfDocumentReader(pdfResource);
        List<Document> documents = pdfReader.get();
        
        // Split documents into chunks
        TokenTextSplitter textSplitter = new TokenTextSplitter();
        List<Document> splitDocuments = textSplitter.apply(documents);
        
        // Add metadata
        splitDocuments.forEach(doc -> {
            doc.getMetadata().put("document_id", documentId);
            doc.getMetadata().put("source", "pdf");
            doc.getMetadata().put("processed_date", new java.util.Date());
        });
        
        // Store in vector store
        vectorStore.add(splitDocuments);
    }
    
    public void processTextDocument(String content, String documentId) {
        Document document = new Document(documentId, content);
        document.getMetadata().put("document_id", documentId);
        document.getMetadata().put("source", "text");
        document.getMetadata().put("processed_date", new java.util.Date());
        
        // Split if needed
        TokenTextSplitter textSplitter = new TokenTextSplitter();
        List<Document> splitDocuments = textSplitter.apply(List.of(document));
        
        vectorStore.add(splitDocuments);
    }
}

7. RAG Applications

Retrieval-Augmented Generation Architecture

graph TD A[User Query] --> B[Embedding Model] B --> C[Vector Store] C --> D[Similar Documents] D --> E[Prompt Template] A --> E E --> F[Chat Model] F --> G[Generated Response]

RAG Service Implementation

package com.example.springai.service;

import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;

@Service
public class RagService {
    
    @Autowired
    private ChatClient chatClient;
    
    @Autowired
    private VectorStore vectorStore;
    
    public String answerQuestion(String question) {
        // Retrieve relevant documents
        List<Document> relevantDocs = vectorStore.similaritySearch(question, 3);
        
        // Format context from documents
        String context = relevantDocs.stream()
            .map(Document::getContent)
            .collect(Collectors.joining("\n\n"));
        
        // Create prompt with context
        String template = """
            Use the following context to answer the question:
            {context}
            
            Question: {question}
            
            Answer:
            """;
            
        PromptTemplate promptTemplate = new PromptTemplate(template);
        Prompt prompt = promptTemplate.create(Map.of(
            "context", context,
            "question", question
        ));
        
        // Generate response
        return chatClient.call(prompt).getResult().getOutput().getContent();
    }
    
    public String answerQuestionWithSources(String question) {
        List<Document> relevantDocs = vectorStore.similaritySearch(question, 3);
        
        String context = relevantDocs.stream()
            .map(doc -> "Source: " + doc.getMetadata().get("source") + "\nContent: " + doc.getContent())
            .collect(Collectors.joining("\n\n"));
        
        String template = """
            Answer the question using only the following context. 
            Include source information in your response.
            
            Context:
            {context}
            
            Question: {question}
            
            Answer with sources:
            """;
            
        PromptTemplate promptTemplate = new PromptTemplate(template);
        Prompt prompt = promptTemplate.create(Map.of(
            "context", context,
            "question", question
        ));
        
        return chatClient.call(prompt).getResult().getOutput().getContent();
    }
}

RAG Controller

package com.example.springai.controller;

import com.example.springai.service.RagService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/rag")
public class RagController {
    
    @Autowired
    private RagService ragService;
    
    @PostMapping("/ask")
    public String askQuestion(@RequestBody String question) {
        return ragService.answerQuestion(question);
    }
    
    @PostMapping("/ask-with-sources")
    public String askQuestionWithSources(@RequestBody String question) {
        return ragService.answerQuestionWithSources(question);
    }
    
    @PostMapping("/chat")
    public ChatResponse chatWithDocuments(@RequestBody ChatRequest request) {
        String response = ragService.answerQuestion(request.getMessage());
        return new ChatResponse(response);
    }
    
    public static class ChatRequest {
        private String message;
        // Getters and setters
        public String getMessage() { return message; }
        public void setMessage(String message) { this.message = message; }
    }
    
    public static class ChatResponse {
        private String response;
        // Constructor and getter
        public ChatResponse(String response) { this.response = response; }
        public String getResponse() { return response; }
    }
}

8. Function Calling

Function Calling Architecture

graph TD A[User Request] --> B[Chat Model] B --> C[Function Call Request] C --> D[Spring Function] D --> E[Business Logic] E --> F[Function Result] F --> B B --> G[Final Response]

Function Service

package com.example.springai.service;

import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Map;

@Service
public class FunctionService {
    
    public String getCurrentWeather(String location) {
        // Simulate weather API call
        return "The current weather in " + location + " is sunny with a temperature of 22°C.";
    }
    
    public String getStockPrice(String symbol) {
        // Simulate stock price API call
        double price = Math.random() * 200 + 50; // Random price between 50-250
        return "The current price of " + symbol + " is $" + String.format("%.2f", price);
    }
    
    public String getCurrentDate() {
        return "Today's date is " + LocalDate.now().format(DateTimeFormatter.ISO_DATE);
    }
    
    public String calculate(String expression) {
        try {
            // Simple calculator (in production, use proper expression parser)
            double result = evaluateExpression(expression);
            return "The result of " + expression + " is " + result;
        } catch (Exception e) {
            return "Unable to calculate: " + expression;
        }
    }
    
    private double evaluateExpression(String expression) {
        // Simplified evaluation - in production use proper library
        return new javax.script.ScriptEngineManager()
            .getEngineByName("JavaScript")
            .eval(expression);
    }
}

Function Calling Configuration

package com.example.springai.config;

import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.chat.functions.FunctionCallback;
import org.springframework.ai.chat.functions.FunctionCallbackContext;
import org.springframework.ai.openai.OpenAiChatClient;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FunctionCallingConfig {
    
    @Value("${spring.ai.openai.api-key}")
    private String apiKey;
    
    @Bean
    public OpenAiApi openAiApi() {
        return new OpenAiApi(apiKey);
    }
    
    @Bean
    public ChatClient chatClient(OpenAiApi openAiApi, FunctionCallbackContext functionCallbackContext) {
        return new OpenAiChatClient(openAiApi)
            .withFunctionCallbackContext(functionCallbackContext);
    }
    
    @Bean
    public FunctionCallbackContext functionCallbackContext() {
        return new FunctionCallbackContext();
    }
}

Function Calling Service

package com.example.springai.service;

import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.function.Function;

@Service
public class FunctionCallingService {
    
    @Autowired
    private ChatClient chatClient;
    
    @Autowired
    private FunctionService functionService;
    
    public String processQuery(String query) {
        // Register functions with the chat client
        // This is a simplified example - in practice, you'd use proper function registration
        
        Prompt prompt = new Prompt(new UserMessage(query));
        return chatClient.call(prompt).getResult().getOutput().getContent();
    }
    
    // Example of how you might expose functions to the AI
    public Function<Map<String, Object>, String> getCurrentWeatherFunction() {
        return params -> {
            String location = (String) params.get("location");
            return functionService.getCurrentWeather(location);
        };
    }
    
    public Function<Map<String, Object>, String> getStockPriceFunction() {
        return params -> {
            String symbol = (String) params.get("symbol");
            return functionService.getStockPrice(symbol);
        };
    }
}

9. Chatbot Application

Complete Chatbot Implementation

package com.example.springai.chatbot;

import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;

@Service
public class ChatbotService {
    
    @Autowired
    private ChatClient chatClient;
    
    @Autowired
    private VectorStore vectorStore;
    
    private final String systemPrompt = """
        You are an intelligent assistant for a company knowledge base.
        You should:
        1. Provide accurate and helpful responses
        2. Use the provided context when available
        3. Be concise and professional
        4. If you don't know the answer, say so rather than making up information
        """;
    
    public ChatbotResponse chat(ChatbotRequest request) {
        // Check if we need to retrieve context
        List<Document> relevantDocs = vectorStore.similaritySearch(request.getMessage(), 2);
        
        String context = "";
        if (!relevantDocs.isEmpty()) {
            context = relevantDocs.stream()
                .map(Document::getContent)
                .collect(Collectors.joining("\n\n"));
        }
        
        // Build conversation history
        StringBuilder conversation = new StringBuilder();
        for (ChatMessage message : request.getHistory()) {
            conversation.append(message.getRole()).append(": ").append(message.getContent()).append("\n");
        }
        
        // Create prompt
        String userMessage = request.getMessage();
        if (!context.isEmpty()) {
            userMessage = "Context:\n" + context + "\n\nQuestion: " + request.getMessage();
        }
        
        Prompt prompt = new Prompt(
            new SystemMessage(systemPrompt),
            new UserMessage(userMessage)
        );
        
        String response = chatClient.call(prompt).getResult().getOutput().getContent();
        
        return new ChatbotResponse(response, !relevantDocs.isEmpty());
    }
    
    public void addKnowledge(String content, String category) {
        Document document = new Document();
        document.setContent(content);
        document.getMetadata().put("category", category);
        document.getMetadata().put("created_date", new java.util.Date());
        
        vectorStore.add(List.of(document));
    }
}

class ChatbotRequest {
    private String message;
    private List<ChatMessage> history;
    
    // Getters and setters
    public String getMessage() { return message; }
    public void setMessage(String message) { this.message = message; }
    
    public List<ChatMessage> getHistory() { return history; }
    public void setHistory(List<ChatMessage> history) { this.history = history; }
}

class ChatMessage {
    private String role; // "user" or "assistant"
    private String content;
    
    // Constructors, getters, and setters
    public ChatMessage() {}
    
    public ChatMessage(String role, String content) {
        this.role = role;
        this.content = content;
    }
    
    public String getRole() { return role; }
    public void setRole(String role) { this.role = role; }
    
    public String getContent() { return content; }
    public void setContent(String content) { this.content = content; }
}

class ChatbotResponse {
    private String response;
    private boolean usedContext;
    
    // Constructor and getters
    public ChatbotResponse(String response, boolean usedContext) {
        this.response = response;
        this.usedContext = usedContext;
    }
    
    public String getResponse() { return response; }
    public boolean isUsedContext() { return usedContext; }
}

Chatbot REST Controller

package com.example.springai.controller;

import com.example.springai.chatbot.ChatbotRequest;
import com.example.springai.chatbot.ChatbotResponse;
import com.example.springai.chatbot.ChatbotService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/chatbot")
@CrossOrigin(origins = "*")
public class ChatbotController {
    
    @Autowired
    private ChatbotService chatbotService;
    
    @PostMapping("/chat")
    public ChatbotResponse chat(@RequestBody ChatbotRequest request) {
        return chatbotService.chat(request);
    }
    
    @PostMapping("/knowledge")
    public String addKnowledge(@RequestBody KnowledgeRequest request) {
        chatbotService.addKnowledge(request.getContent(), request.getCategory());
        return "Knowledge added successfully";
    }
    
    @GetMapping("/health")
    public String health() {
        return "Chatbot service is running";
    }
}

class KnowledgeRequest {
    private String content;
    private String category;
    
    // Getters and setters
    public String getContent() { return content; }
    public void setContent(String content) { this.content = content; }
    
    public String getCategory() { return category; }
    public void setCategory(String category) { this.category = category; }
}

Frontend Integration Example

import React, { useState } from 'react';

function Chatbot() {
  const [messages, setMessages] = useState([]);
  const [input, setInput] = useState('');
  const [loading, setLoading] = useState(false);

  const sendMessage = async () => {
    if (!input.trim()) return;

    // Add user message
    const userMessage = { role: 'user', content: input };
    setMessages(prev => [...prev, userMessage]);
    setInput('');
    setLoading(true);

    try {
      const response = await fetch('/api/chatbot/chat', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          message: input,
          history: messages.slice(-5) // Last 5 messages for context
        }),
      });

      const data = await response.json();
      
      // Add bot response
      const botMessage = { role: 'assistant', content: data.response };
      setMessages(prev => [...prev, botMessage]);
    } catch (error) {
      const errorMessage = { role: 'assistant', content: 'Sorry, I encountered an error.' };
      setMessages(prev => [...prev, errorMessage]);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="chatbot-container">
      <div className="chat-messages">
        {messages.map((msg, index) => (
          <div key={index} className={`message ${msg.role}`}>
            <strong>{msg.role === 'user' ? 'You' : 'Assistant'}:</strong>
            <p>{msg.content}</p>
          </div>
        ))}
        {loading && <div className="message assistant">Thinking...</div>}
      </div>
      
      <div className="chat-input">
        <input
          type="text"
          value={input}
          onChange={(e) => setInput(e.target.value)}
          onKeyPress={(e) => e.key === 'Enter' && sendMessage()}
          placeholder="Ask me anything..."
          disabled={loading}
        />
        <button onClick={sendMessage} disabled={loading}>
          Send
        </button>
      </div>
    </div>
  );
}

export default Chatbot;

10. Best Practices

AI Integration Best Practices

Security

Protect API keys and validate inputs

Performance

Cache responses and optimize prompts

Resilience

Handle failures gracefully

Production Deployment

graph TD A[Load Balancer] --> B[Spring Boot App 1] A --> C[Spring Boot App 2] A --> D[Spring Boot App N] B --> E[Redis Vector Store] B --> F[OpenAI API] C --> E C --> F D --> E D --> F E --> G[Redis Cluster]

Configuration Best Practices

# Production configuration
# API Keys - use environment variables
spring.ai.openai.api-key=${OPENAI_API_KEY}
spring.ai.azure.openai.api-key=${AZURE_OPENAI_API_KEY}

# Model selection
spring.ai.openai.chat.options.model=gpt-4
spring.ai.openai.chat.options.temperature=0.7
spring.ai.openai.chat.options.max-tokens=1000

# Retry configuration
spring.ai.retry.max-attempts=3
spring.ai.retry.backoff.initial-interval=1000
spring.ai.retry.backoff.multiplier=2.0

# Caching
spring.cache.type=redis
spring.cache.redis.time-to-live=3600000

# Monitoring
management.endpoints.web.exposure.include=health,info,metrics
management.endpoint.health.show-details=always

Error Handling and Monitoring

package com.example.springai.service;

import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.chat.ChatResponse;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;

@Service
public class ResilientAIService {
    
    private final ChatClient chatClient;
    private final Counter aiRequestsCounter;
    private final Counter aiErrorsCounter;
    private final Timer aiResponseTimer;
    
    public ResilientAIService(ChatClient chatClient, MeterRegistry meterRegistry) {
        this.chatClient = chatClient;
        this.aiRequestsCounter = Counter.builder("ai.requests")
            .description("Number of AI requests")
            .register(meterRegistry);
        this.aiErrorsCounter = Counter.builder("ai.errors")
            .description("Number of AI errors")
            .register(meterRegistry);
        this.aiResponseTimer = Timer.builder("ai.response.time")
            .description("AI response time")
            .register(meterRegistry);
    }
    
    @Retryable(
        value = { Exception.class },
        maxAttempts = 3,
        backoff = @Backoff(delay = 1000, multiplier = 2)
    )
    public String getChatResponse(String message) {
        aiRequestsCounter.increment();
        
        Timer.Sample sample = Timer.start();
        try {
            Prompt prompt = new Prompt(new UserMessage(message));
            ChatResponse response = chatClient.call(prompt);
            sample.stop(aiResponseTimer);
            return response.getResult().getOutput().getContent();
        } catch (Exception e) {
            aiErrorsCounter.increment();
            sample.stop(aiResponseTimer);
            throw e;
        }
    }
    
    public String getChatResponseWithFallback(String message) {
        try {
            return getChatResponse(message);
        } catch (Exception e) {
            // Fallback response
            return "I'm currently unable to process your request. Please try again later.";
        }
    }
}

Essential Best Practices:

  • Rate Limiting: Implement rate limiting to prevent API abuse
  • Caching: Cache responses for frequently asked questions
  • Prompt Engineering: Design clear, specific prompts with examples
  • Security: Validate and sanitize all inputs to prevent prompt injection
  • Monitoring: Track usage, costs, and performance metrics
  • Error Handling: Implement retry logic and graceful degradation