.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