Initial commit
This commit is contained in:
@@ -0,0 +1,812 @@
|
||||
# LangChain4j Spring Boot Integration - Configuration Guide
|
||||
|
||||
Detailed configuration options and advanced setup patterns for LangChain4j with Spring Boot.
|
||||
|
||||
## Property-Based Configuration
|
||||
|
||||
### Core Configuration Properties
|
||||
|
||||
**application.yml**
|
||||
```yaml
|
||||
langchain4j:
|
||||
# OpenAI Configuration
|
||||
open-ai:
|
||||
chat-model:
|
||||
api-key: ${OPENAI_API_KEY}
|
||||
model-name: gpt-4o-mini
|
||||
temperature: 0.7
|
||||
max-tokens: 1000
|
||||
log-requests: true
|
||||
log-responses: true
|
||||
timeout: PT60S
|
||||
max-retries: 3
|
||||
organization: ${OPENAI_ORGANIZATION:}
|
||||
|
||||
embedding-model:
|
||||
api-key: ${OPENAI_API_KEY}
|
||||
model-name: text-embedding-3-small
|
||||
dimensions: 1536
|
||||
timeout: PT60S
|
||||
|
||||
streaming-chat-model:
|
||||
api-key: ${OPENAI_API_KEY}
|
||||
model-name: gpt-4o-mini
|
||||
temperature: 0.7
|
||||
max-tokens: 2000
|
||||
|
||||
# Azure OpenAI Configuration
|
||||
azure-open-ai:
|
||||
chat-model:
|
||||
endpoint: ${AZURE_OPENAI_ENDPOINT}
|
||||
api-key: ${AZURE_OPENAI_KEY}
|
||||
deployment-name: gpt-4o
|
||||
service-version: 2024-02-15-preview
|
||||
temperature: 0.7
|
||||
max-tokens: 1000
|
||||
log-requests-and-responses: true
|
||||
|
||||
embedding-model:
|
||||
endpoint: ${AZURE_OPENAI_ENDPOINT}
|
||||
api-key: ${AZURE_OPENAI_KEY}
|
||||
deployment-name: text-embedding-3-small
|
||||
dimensions: 1536
|
||||
|
||||
# Anthropic Configuration
|
||||
anthropic:
|
||||
chat-model:
|
||||
api-key: ${ANTHROPIC_API_KEY}
|
||||
model-name: claude-3-5-sonnet-20241022
|
||||
max-tokens: 4000
|
||||
temperature: 0.7
|
||||
|
||||
streaming-chat-model:
|
||||
api-key: ${ANTHROPIC_API_KEY}
|
||||
model-name: claude-3-5-sonnet-20241022
|
||||
|
||||
# Ollama Configuration
|
||||
ollama:
|
||||
chat-model:
|
||||
base-url: http://localhost:11434
|
||||
model-name: llama3.1
|
||||
temperature: 0.8
|
||||
timeout: PT60S
|
||||
|
||||
# Memory Configuration
|
||||
memory:
|
||||
store-type: in-memory # in-memory, postgresql, mysql, mongodb
|
||||
max-messages: 20
|
||||
window-size: 10
|
||||
|
||||
# Vector Store Configuration
|
||||
vector-store:
|
||||
type: in-memory # in-memory, pinecone, weaviate, qdrant, postgresql
|
||||
pinecone:
|
||||
api-key: ${PINECONE_API_KEY}
|
||||
index-name: my-index
|
||||
namespace: production
|
||||
qdrant:
|
||||
host: localhost
|
||||
port: 6333
|
||||
collection-name: documents
|
||||
weaviate:
|
||||
host: localhost
|
||||
port: 8080
|
||||
collection-name: Documents
|
||||
postgresql:
|
||||
table: document_embeddings
|
||||
dimension: 1536
|
||||
```
|
||||
|
||||
### Spring Profiles Configuration
|
||||
|
||||
**application-dev.yml**
|
||||
```yaml
|
||||
langchain4j:
|
||||
open-ai:
|
||||
chat-model:
|
||||
api-key: ${OPENAI_API_KEY_DEV}
|
||||
model-name: gpt-4o-mini
|
||||
temperature: 0.8 # Higher temperature for experimentation
|
||||
log-requests: true
|
||||
log-responses: true
|
||||
|
||||
vector-store:
|
||||
type: in-memory
|
||||
```
|
||||
|
||||
**application-prod.yml**
|
||||
```yaml
|
||||
langchain4j:
|
||||
open-ai:
|
||||
chat-model:
|
||||
api-key: ${OPENAI_API_KEY_PROD}
|
||||
model-name: gpt-4o
|
||||
temperature: 0.3 # Lower temperature for consistency
|
||||
log-requests: false
|
||||
log-responses: false
|
||||
|
||||
vector-store:
|
||||
type: pinecone
|
||||
pinecone:
|
||||
api-key: ${PINECONE_API_KEY_PROD}
|
||||
index-name: production-knowledge-base
|
||||
```
|
||||
|
||||
## Manual Bean Configuration
|
||||
|
||||
### Advanced Chat Model Configuration
|
||||
|
||||
```java
|
||||
@Configuration
|
||||
@Profile("custom-openai")
|
||||
public class CustomOpenAiConfiguration {
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
public ChatModel customOpenAiChatModel(
|
||||
@Value("${custom.openai.api.key}") String apiKey,
|
||||
@Value("${custom.openai.model}") String model,
|
||||
@Value("${custom.openai.temperature}") Double temperature) {
|
||||
|
||||
OpenAiChatModelBuilder builder = OpenAiChatModel.builder()
|
||||
.apiKey(apiKey)
|
||||
.modelName(model)
|
||||
.temperature(temperature);
|
||||
|
||||
if (Boolean.TRUE.equals(env.getProperty("custom.openai.log-requests", Boolean.class))) {
|
||||
builder.logRequests(true);
|
||||
}
|
||||
if (Boolean.TRUE.equals(env.getProperty("custom.openai.log-responses", Boolean.class))) {
|
||||
builder.logResponses(true);
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(name = "custom.openai.proxy.enabled", havingValue = "true")
|
||||
public ChatModel proxiedChatModel(ChatModel delegate) {
|
||||
return new ProxiedChatModel(delegate,
|
||||
env.getProperty("custom.openai.proxy.url"),
|
||||
env.getProperty("custom.openai.proxy.username"),
|
||||
env.getProperty("custom.openai.proxy.password"));
|
||||
}
|
||||
}
|
||||
|
||||
class ProxiedChatModel implements ChatModel {
|
||||
private final ChatModel delegate;
|
||||
private final String proxyUrl;
|
||||
private final String username;
|
||||
private final String password;
|
||||
|
||||
public ProxiedChatModel(ChatModel delegate, String proxyUrl, String username, String password) {
|
||||
this.delegate = delegate;
|
||||
this.proxyUrl = proxyUrl;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response<AiMessage> generate(ChatRequest request) {
|
||||
// Apply proxy configuration
|
||||
// Make request through proxy
|
||||
return delegate.generate(request);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Multiple Provider Configuration
|
||||
|
||||
```java
|
||||
@Configuration
|
||||
public class MultiProviderConfiguration {
|
||||
|
||||
@Bean("openAiChatModel")
|
||||
public ChatModel openAiChatModel(
|
||||
@Value("${openai.api.key}") String apiKey,
|
||||
@Value("${openai.model.name}") String modelName) {
|
||||
|
||||
return OpenAiChatModel.builder()
|
||||
.apiKey(apiKey)
|
||||
.modelName(modelName)
|
||||
.temperature(0.7)
|
||||
.logRequests(env.acceptsProfiles("dev"))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean("anthropicChatModel")
|
||||
public ChatModel anthropicChatModel(
|
||||
@Value("${anthropic.api.key}") String apiKey,
|
||||
@Value("${anthropic.model.name}") String modelName) {
|
||||
|
||||
return AnthropicChatModel.builder()
|
||||
.apiKey(apiKey)
|
||||
.modelName(modelName)
|
||||
.maxTokens(4000)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean("ollamaChatModel")
|
||||
@ConditionalOnProperty(name = "ollama.enabled", havingValue = "true")
|
||||
public ChatModel ollamaChatModel(
|
||||
@Value("${ollama.base-url}") String baseUrl,
|
||||
@Value("${ollama.model.name}") String modelName) {
|
||||
|
||||
return OllamaChatModel.builder()
|
||||
.baseUrl(baseUrl)
|
||||
.modelName(modelName)
|
||||
.temperature(0.8)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Explicit Wiring Configuration
|
||||
|
||||
```java
|
||||
@AiService(wiringMode = EXPLICIT, chatModel = "productionChatModel")
|
||||
interface ProductionAssistant {
|
||||
@SystemMessage("You are a production-grade AI assistant providing high-quality, reliable responses.")
|
||||
String chat(String message);
|
||||
}
|
||||
|
||||
@AiService(wiringMode = EXPLICIT, chatModel = "developmentChatModel")
|
||||
interface DevelopmentAssistant {
|
||||
@SystemMessage("You are a development assistant helping with code and debugging. " +
|
||||
"Be experimental and creative in your responses.")
|
||||
String chat(String message);
|
||||
}
|
||||
|
||||
@AiService(wiringMode = EXPLICIT,
|
||||
chatModel = "specializedChatModel",
|
||||
tools = "businessTools")
|
||||
interface SpecializedAssistant {
|
||||
@SystemMessage("You are a specialized assistant with access to business tools. " +
|
||||
"Use the available tools to provide accurate information.")
|
||||
String chat(String message);
|
||||
}
|
||||
|
||||
@Component("businessTools")
|
||||
public class BusinessLogicTools {
|
||||
|
||||
@Tool("Calculate discount based on customer status")
|
||||
public BigDecimal calculateDiscount(
|
||||
@P("Purchase amount") BigDecimal amount,
|
||||
@P("Customer status") String customerStatus) {
|
||||
|
||||
return switch (customerStatus.toLowerCase()) {
|
||||
case "vip" -> amount.multiply(new BigDecimal("0.15"));
|
||||
case "premium" -> amount.multiply(new BigDecimal("0.10"));
|
||||
case "standard" -> amount.multiply(new BigDecimal("0.05"));
|
||||
default -> BigDecimal.ZERO;
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Embedding Store Configuration
|
||||
|
||||
### PostgreSQL with pgvector
|
||||
|
||||
```java
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class PostgresEmbeddingStoreConfiguration {
|
||||
|
||||
@Bean
|
||||
public EmbeddingStore<TextSegment> postgresEmbeddingStore(
|
||||
DataSource dataSource,
|
||||
@Value("${spring.datasource.schema}") String schema) {
|
||||
|
||||
return PgVectorEmbeddingStore.builder()
|
||||
.dataSource(dataSource)
|
||||
.table("document_embeddings")
|
||||
.dimension(1536)
|
||||
.initializeSchema(true)
|
||||
.schema(schema)
|
||||
.indexName("document_embeddings_idx")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ContentRetriever postgresContentRetriever(
|
||||
EmbeddingStore<TextSegment> embeddingStore,
|
||||
EmbeddingModel embeddingModel) {
|
||||
|
||||
return EmbeddingStoreContentRetriever.builder()
|
||||
.embeddingStore(embeddingStore)
|
||||
.embeddingModel(embeddingModel)
|
||||
.maxResults(5)
|
||||
.minScore(0.7)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pinecone Configuration
|
||||
|
||||
```java
|
||||
@Configuration
|
||||
@Profile("pinecone")
|
||||
public class PineconeConfiguration {
|
||||
|
||||
@Bean
|
||||
public EmbeddingStore<TextSegment> pineconeEmbeddingStore(
|
||||
@Value("${pinecone.api.key}") String apiKey,
|
||||
@Value("${pinecone.index.name}") String indexName,
|
||||
@Value("${pinecone.namespace}") String namespace) {
|
||||
|
||||
PineconeEmbeddingStore store = PineconeEmbeddingStore.builder()
|
||||
.apiKey(apiKey)
|
||||
.indexName(indexName)
|
||||
.namespace(namespace)
|
||||
.build();
|
||||
|
||||
// Initialize if needed
|
||||
if (!store.indexExists()) {
|
||||
store.createIndex(1536);
|
||||
}
|
||||
|
||||
return store;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Embedding Store
|
||||
|
||||
```java
|
||||
@Component
|
||||
public class CustomEmbeddingStore implements EmbeddingStore<TextSegment> {
|
||||
|
||||
private final Map<UUID, TextSegment> embeddings = new ConcurrentHashMap<>();
|
||||
private final Map<UUID, float[]> vectors = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public void add(Embedding embedding, TextSegment textSegment) {
|
||||
UUID id = UUID.randomUUID();
|
||||
embeddings.put(id, textSegment);
|
||||
vectors.put(id, embedding.vector());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addAll(List<Embedding> embeddings, List<TextSegment> textSegments) {
|
||||
for (int i = 0; i < embeddings.size(); i++) {
|
||||
add(embeddings.get(i), textSegments.get(i));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Embedding> findRelevant(Embedding embedding, int maxResults) {
|
||||
return vectors.entrySet().stream()
|
||||
.sorted(Comparator.comparingDouble(e -> cosineSimilarity(e.getValue(), embedding.vector())))
|
||||
.limit(maxResults)
|
||||
.map(e -> new EmbeddingImpl(e.getValue(), embeddings.get(e.getKey()).id()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private double cosineSimilarity(float[] vec1, float[] vec2) {
|
||||
// Implementation of cosine similarity
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Memory Configuration
|
||||
|
||||
### Chat Memory Store Configuration
|
||||
|
||||
```java
|
||||
@Configuration
|
||||
public class MemoryConfiguration {
|
||||
|
||||
@Bean
|
||||
@Profile("in-memory")
|
||||
public ChatMemoryStore inMemoryChatMemoryStore() {
|
||||
return new InMemoryChatMemoryStore();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Profile("database")
|
||||
public ChatMemoryStore databaseChatMemoryStore(ChatMessageRepository messageRepository) {
|
||||
return new DatabaseChatMemoryStore(messageRepository);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ChatMemoryProvider chatMemoryProvider(ChatMemoryStore memoryStore) {
|
||||
return memoryId -> MessageWindowChatMemory.builder()
|
||||
.id(memoryId)
|
||||
.maxMessages(getMaxMessages())
|
||||
.chatMemoryStore(memoryStore)
|
||||
.build();
|
||||
}
|
||||
|
||||
private int getMaxMessages() {
|
||||
return env.getProperty("langchain4j.memory.max-messages", int.class, 20);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Database Chat Memory Store
|
||||
|
||||
```java
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class DatabaseChatMemoryStore implements ChatMemoryStore {
|
||||
|
||||
private final ChatMessageRepository repository;
|
||||
|
||||
@Override
|
||||
public List<ChatMessage> getMessages(Object memoryId) {
|
||||
return repository.findByMemoryIdOrderByCreatedAtAsc(memoryId.toString())
|
||||
.stream()
|
||||
.map(this::toMessage)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateMessages(Object memoryId, List<ChatMessage> messages) {
|
||||
String id = memoryId.toString();
|
||||
repository.deleteByMemoryId(id);
|
||||
|
||||
List<ChatMessageEntity> entities = messages.stream()
|
||||
.map(msg -> toEntity(id, msg))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
repository.saveAll(entities);
|
||||
}
|
||||
|
||||
private ChatMessage toMessage(ChatMessageEntity entity) {
|
||||
return switch (entity.getMessageType()) {
|
||||
case USER -> UserMessage.from(entity.getContent());
|
||||
case AI -> AiMessage.from(entity.getContent());
|
||||
case SYSTEM -> SystemMessage.from(entity.getContent());
|
||||
};
|
||||
}
|
||||
|
||||
private ChatMessageEntity toEntity(String memoryId, ChatMessage message) {
|
||||
ChatMessageEntity entity = new ChatMessageEntity();
|
||||
entity.setMemoryId(memoryId);
|
||||
entity.setContent(message.text());
|
||||
entity.setCreatedAt(LocalDateTime.now());
|
||||
entity.setMessageType(determineMessageType(message));
|
||||
return entity;
|
||||
}
|
||||
|
||||
private MessageType determineMessageType(ChatMessage message) {
|
||||
if (message instanceof UserMessage) return MessageType.USER;
|
||||
if (message instanceof AiMessage) return MessageType.AI;
|
||||
if (message instanceof SystemMessage) return MessageType.SYSTEM;
|
||||
throw new IllegalArgumentException("Unknown message type: " + message.getClass());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Observability Configuration
|
||||
|
||||
### Monitoring and Metrics
|
||||
|
||||
```java
|
||||
@Configuration
|
||||
public class ObservabilityConfiguration {
|
||||
|
||||
@Bean
|
||||
public ChatModelListener chatModelListener(MeterRegistry meterRegistry) {
|
||||
return new MonitoringChatModelListener(meterRegistry);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public HealthIndicator aiHealthIndicator(ChatModel chatModel) {
|
||||
return new AiHealthIndicator(chatModel);
|
||||
}
|
||||
}
|
||||
|
||||
class MonitoringChatModelListener implements ChatModelListener {
|
||||
|
||||
private final MeterRegistry meterRegistry;
|
||||
private final Counter requestCounter;
|
||||
private final Timer responseTimer;
|
||||
|
||||
public MonitoringChatModelListener(MeterRegistry meterRegistry) {
|
||||
this.meterRegistry = meterRegistry;
|
||||
this.requestCounter = Counter.builder("ai.requests.total")
|
||||
.description("Total AI requests")
|
||||
.register(meterRegistry);
|
||||
this.responseTimer = Timer.builder("ai.response.duration")
|
||||
.description("AI response time")
|
||||
.register(meterRegistry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequest(ChatModelRequestContext requestContext) {
|
||||
requestCounter.increment();
|
||||
logRequest(requestContext);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(ChatModelResponseContext responseContext) {
|
||||
responseTimer.record(responseContext.duration());
|
||||
logResponse(responseContext);
|
||||
}
|
||||
|
||||
private void logRequest(ChatModelRequestContext requestContext) {
|
||||
meterRegistry.gauge("ai.request.tokens",
|
||||
requestContext.request().messages().size());
|
||||
}
|
||||
|
||||
private void logResponse(ChatModelResponseContext responseContext) {
|
||||
Response<AiMessage> response = responseContext.response();
|
||||
meterRegistry.gauge("ai.response.tokens",
|
||||
response.tokenUsage().totalTokenCount());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Health Check
|
||||
|
||||
```java
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class AiHealthIndicator implements HealthIndicator {
|
||||
|
||||
private final ChatModel chatModel;
|
||||
private final EmbeddingModel embeddingModel;
|
||||
|
||||
@Override
|
||||
public Health health() {
|
||||
try {
|
||||
// Test chat model
|
||||
Health.Builder builder = Health.up();
|
||||
String chatResponse = chatModel.chat("ping");
|
||||
builder.withDetail("chat_model", "healthy");
|
||||
|
||||
if (chatResponse == null || chatResponse.trim().isEmpty()) {
|
||||
return Health.down().withDetail("reason", "Empty response");
|
||||
}
|
||||
|
||||
// Test embedding model
|
||||
List<String> testTexts = List.of("test", "ping", "hello");
|
||||
List<Embedding> embeddings = embeddingModel.embedAll(testTexts).content();
|
||||
|
||||
if (embeddings.isEmpty()) {
|
||||
return Health.down().withDetail("reason", "No embeddings generated");
|
||||
}
|
||||
|
||||
builder.withDetail("embedding_model", "healthy")
|
||||
.withDetail("embedding_dimension", embeddings.get(0).vector().length);
|
||||
|
||||
return builder.build();
|
||||
|
||||
} catch (Exception e) {
|
||||
return Health.down()
|
||||
.withDetail("error", e.getMessage())
|
||||
.withDetail("exception_class", e.getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Security Configuration
|
||||
|
||||
### API Key Security
|
||||
|
||||
```java
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.csrf().disable()
|
||||
.authorizeRequests()
|
||||
.requestMatchers("/api/ai/**").hasRole("AI_USER")
|
||||
.requestMatchers("/actuator/ai/**").hasRole("AI_ADMIN")
|
||||
.anyRequest().permitAll()
|
||||
.and()
|
||||
.httpBasic();
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ApiKeyAuthenticationFilter apiKeyAuthenticationFilter() {
|
||||
return new ApiKeyAuthenticationFilter("/api/ai/**");
|
||||
}
|
||||
}
|
||||
|
||||
class ApiKeyAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
private final String pathPrefix;
|
||||
|
||||
public ApiKeyAuthenticationFilter(String pathPrefix) {
|
||||
this.pathPrefix = pathPrefix;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
FilterChain filterChain) throws ServletException, IOException {
|
||||
|
||||
if (request.getRequestURI().startsWith(pathPrefix)) {
|
||||
String apiKey = request.getHeader("X-API-Key");
|
||||
if (apiKey == null || !isValidApiKey(apiKey)) {
|
||||
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid API key");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
private boolean isValidApiKey(String apiKey) {
|
||||
// Validate API key against database or security service
|
||||
return true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration Validation
|
||||
|
||||
```java
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class AiConfigurationValidator implements InitializingBean {
|
||||
|
||||
private final AiProperties properties;
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
validateConfiguration();
|
||||
}
|
||||
|
||||
private void validateConfiguration() {
|
||||
if (properties.getOpenai() != null) {
|
||||
validateOpenAiConfiguration();
|
||||
}
|
||||
|
||||
if (properties.getAzureOpenAi() != null) {
|
||||
validateAzureConfiguration();
|
||||
}
|
||||
|
||||
if (properties.getAnthropic() != null) {
|
||||
validateAnthropicConfiguration();
|
||||
}
|
||||
|
||||
log.info("AI configuration validation completed successfully");
|
||||
}
|
||||
|
||||
private void validateOpenAiConfiguration() {
|
||||
OpenAiProperties openAi = properties.getOpenai();
|
||||
|
||||
if (openAi.getChatModel() != null &&
|
||||
(openAi.getChatModel().getApiKey() == null ||
|
||||
openAi.getChatModel().getApiKey().isEmpty())) {
|
||||
log.warn("OpenAI chat model API key is not configured");
|
||||
}
|
||||
|
||||
if (openAi.getChatModel() != null &&
|
||||
openAi.getChatModel().getMaxTokens() != null &&
|
||||
openAi.getChatModel().getMaxTokens() > 8192) {
|
||||
log.warn("OpenAI max tokens {} exceeds recommended limit of 8192",
|
||||
openAi.getChatModel().getMaxTokens());
|
||||
}
|
||||
}
|
||||
|
||||
private void validateAzureConfiguration() {
|
||||
AzureOpenAiProperties azure = properties.getAzureOpenAi();
|
||||
|
||||
if (azure.getChatModel() != null &&
|
||||
(azure.getChatModel().getEndpoint() == null ||
|
||||
azure.getChatModel().getApiKey() == null)) {
|
||||
log.error("Azure OpenAI endpoint or API key is not configured");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateAnthropicConfiguration() {
|
||||
AnthropicProperties anthropic = properties.getAnthropic();
|
||||
|
||||
if (anthropic.getChatModel() != null &&
|
||||
(anthropic.getChatModel().getApiKey() == null ||
|
||||
anthropic.getChatModel().getApiKey().isEmpty())) {
|
||||
log.warn("Anthropic chat model API key is not configured");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "langchain4j")
|
||||
@Validated
|
||||
@Data
|
||||
public class AiProperties {
|
||||
private OpenAiProperties openai;
|
||||
private AzureOpenAiProperties azureOpenAi;
|
||||
private AnthropicProperties anthropic;
|
||||
private MemoryProperties memory;
|
||||
private VectorStoreProperties vectorStore;
|
||||
|
||||
// Validation annotations for properties
|
||||
}
|
||||
|
||||
@Data
|
||||
@Validated
|
||||
public class OpenAiProperties {
|
||||
private ChatModelProperties chatModel;
|
||||
private EmbeddingModelProperties embeddingModel;
|
||||
private StreamingChatModelProperties streamingChatModel;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
public ChatModelProperties getChatModel() {
|
||||
return chatModel;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Environment-Specific Configurations
|
||||
|
||||
### Development Configuration
|
||||
|
||||
```yaml
|
||||
# application-dev.yml
|
||||
langchain4j:
|
||||
open-ai:
|
||||
chat-model:
|
||||
api-key: ${OPENAI_API_KEY_DEV}
|
||||
model-name: gpt-4o-mini
|
||||
temperature: 0.8
|
||||
log-requests: true
|
||||
log-responses: true
|
||||
|
||||
memory:
|
||||
store-type: in-memory
|
||||
max-messages: 10
|
||||
|
||||
vector-store:
|
||||
type: in-memory
|
||||
|
||||
logging:
|
||||
level:
|
||||
dev.langchain4j: DEBUG
|
||||
org.springframework.ai: DEBUG
|
||||
```
|
||||
|
||||
### Production Configuration
|
||||
|
||||
```yaml
|
||||
# application-prod.yml
|
||||
langchain4j:
|
||||
open-ai:
|
||||
chat-model:
|
||||
api-key: ${OPENAI_API_KEY_PROD}
|
||||
model-name: gpt-4o
|
||||
temperature: 0.3
|
||||
log-requests: false
|
||||
log-responses: false
|
||||
max-tokens: 4000
|
||||
|
||||
memory:
|
||||
store-type: postgresql
|
||||
max-messages: 5
|
||||
|
||||
vector-store:
|
||||
type: pinecone
|
||||
pinecone:
|
||||
index-name: production-knowledge-base
|
||||
namespace: prod
|
||||
|
||||
logging:
|
||||
level:
|
||||
dev.langchain4j: WARN
|
||||
org.springframework.ai: WARN
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health, metrics, info
|
||||
endpoint:
|
||||
health:
|
||||
show-details: when-authorized
|
||||
```
|
||||
|
||||
This configuration guide provides comprehensive options for setting up LangChain4j with Spring Boot, covering various providers, storage backends, monitoring, and security considerations.
|
||||
@@ -0,0 +1,465 @@
|
||||
# LangChain4j Spring Boot Integration - Examples
|
||||
|
||||
Comprehensive implementation examples for Spring Boot integration with LangChain4j.
|
||||
|
||||
## Basic Setup Example
|
||||
|
||||
### Complete Spring Boot Application
|
||||
|
||||
```java
|
||||
@SpringBootApplication
|
||||
public class Langchain4jApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(Langchain4jApplication.class, args);
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
public class AiConfiguration {
|
||||
|
||||
@Bean
|
||||
@Profile("openai")
|
||||
public ChatModel openAiChatModel(@Value("${langchain4j.open-ai.chat-model.api-key}") String apiKey) {
|
||||
return OpenAiChatModel.builder()
|
||||
.apiKey(apiKey)
|
||||
.modelName("gpt-4o-mini")
|
||||
.temperature(0.7)
|
||||
.maxTokens(1000)
|
||||
.logRequests(true)
|
||||
.logResponses(true)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public EmbeddingModel openAiEmbeddingModel(@Value("${langchain4j.open-ai.embedding-model.api-key}") String apiKey) {
|
||||
return OpenAiEmbeddingModel.builder()
|
||||
.apiKey(apiKey)
|
||||
.modelName("text-embedding-3-small")
|
||||
.dimensions(1536)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@AiService
|
||||
interface CustomerSupportAssistant {
|
||||
|
||||
@SystemMessage("You are a helpful customer support agent for TechCorp. " +
|
||||
"Be polite, professional, and try to resolve customer issues efficiently. " +
|
||||
"If you cannot resolve an issue, escalate to a human agent.")
|
||||
String handleInquiry(String customerMessage);
|
||||
|
||||
@UserMessage("Analyze this customer feedback and extract sentiment: {{feedback}}")
|
||||
@SystemMessage("Return only: POSITIVE, NEGATIVE, or NEUTRAL")
|
||||
String analyzeSentiment(String feedback);
|
||||
|
||||
@UserMessage("Extract key entities from this text: {{text}}")
|
||||
@SystemMessage("Return a JSON object with entities as keys and their types as values")
|
||||
String extractEntities(String text);
|
||||
}
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/support")
|
||||
@RequiredArgsConstructor
|
||||
public class CustomerSupportController {
|
||||
|
||||
private final CustomerSupportAssistant assistant;
|
||||
|
||||
@PostMapping("/inquiry")
|
||||
public ResponseEntity<SupportResponse> handleInquiry(@RequestBody @Valid SupportRequest request) {
|
||||
String response = assistant.handleInquiry(request.getMessage());
|
||||
return ResponseEntity.ok(new SupportResponse(response, Instant.now()));
|
||||
}
|
||||
|
||||
@PostMapping("/sentiment")
|
||||
public ResponseEntity<SentimentResponse> analyzeSentiment(@RequestBody @Valid SentimentRequest request) {
|
||||
String sentiment = assistant.analyzeSentiment(request.getFeedback());
|
||||
return ResponseEntity.ok(new SentimentResponse(sentiment, Instant.now()));
|
||||
}
|
||||
|
||||
@PostMapping("/entities")
|
||||
public ResponseEntity<EntitiesResponse> extractEntities(@RequestBody @Valid EntitiesRequest request) {
|
||||
String entities = assistant.extractEntities(request.getText());
|
||||
return ResponseEntity.ok(new EntitiesResponse(entities, Instant.now()));
|
||||
}
|
||||
}
|
||||
|
||||
// DTO Classes
|
||||
record SupportRequest(String message) {}
|
||||
record SupportResponse(String response, Instant timestamp) {}
|
||||
|
||||
record SentimentRequest(String feedback) {}
|
||||
record SentimentResponse(String sentiment, Instant timestamp) {}
|
||||
|
||||
record EntitiesRequest(String text) {}
|
||||
record EntitiesResponse(String entities, Instant timestamp) {}
|
||||
```
|
||||
|
||||
## 2. Custom AI Service Bean Configuration
|
||||
|
||||
**Scenario**: Configure AI services as Spring beans.
|
||||
|
||||
```java
|
||||
@Configuration
|
||||
public class AiConfig {
|
||||
|
||||
@Bean
|
||||
public ChatModel chatModel() {
|
||||
return OpenAiChatModel.builder()
|
||||
.apiKey(System.getenv("OPENAI_API_KEY"))
|
||||
.modelName("gpt-4o-mini")
|
||||
.temperature(0.7)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public EmbeddingModel embeddingModel() {
|
||||
return OpenAiEmbeddingModel.builder()
|
||||
.apiKey(System.getenv("OPENAI_API_KEY"))
|
||||
.modelName("text-embedding-3-small")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public DocumentAssistant documentAssistant(ChatModel chatModel) {
|
||||
return AiServices.builder(DocumentAssistant.class)
|
||||
.chatModel(chatModel)
|
||||
.chatMemory(MessageWindowChatMemory.withMaxMessages(10))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
interface DocumentAssistant {
|
||||
String chat(String message);
|
||||
}
|
||||
```
|
||||
|
||||
## 3. REST API with AI Service
|
||||
|
||||
**Scenario**: Expose AI functionality via REST endpoints.
|
||||
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/api/chat")
|
||||
public class ChatController {
|
||||
|
||||
private final ChatAssistant assistant;
|
||||
|
||||
@Autowired
|
||||
public ChatController(ChatAssistant assistant) {
|
||||
this.assistant = assistant;
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<ChatResponse> chat(@RequestBody ChatRequest request) {
|
||||
try {
|
||||
String response = assistant.chat(request.getMessage());
|
||||
return ResponseEntity.ok(new ChatResponse(response));
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.internalServerError()
|
||||
.body(new ChatResponse("Error: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/stream")
|
||||
public ResponseEntity<StreamingResponseBody> streamChat(@RequestBody ChatRequest request) {
|
||||
return ResponseEntity.ok(outputStream -> {
|
||||
var streamAssistant = streamingAssistant;
|
||||
var stream = streamAssistant.streamChat(request.getMessage());
|
||||
|
||||
stream.onNext(token -> {
|
||||
try {
|
||||
outputStream.write(token.getBytes());
|
||||
outputStream.flush();
|
||||
} catch (IOException e) {
|
||||
// Handle write error
|
||||
}
|
||||
}).start();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
class ChatRequest {
|
||||
private String message;
|
||||
}
|
||||
|
||||
@Data
|
||||
class ChatResponse {
|
||||
private String response;
|
||||
}
|
||||
```
|
||||
|
||||
## 4. Service with RAG Integration
|
||||
|
||||
**Scenario**: Service layer with document search and retrieval.
|
||||
|
||||
```java
|
||||
@Service
|
||||
public class KnowledgeBaseService {
|
||||
|
||||
private final DocumentAssistant assistant;
|
||||
private final EmbeddingStore<TextSegment> embeddingStore;
|
||||
private final EmbeddingModel embeddingModel;
|
||||
|
||||
@Autowired
|
||||
public KnowledgeBaseService(
|
||||
DocumentAssistant assistant,
|
||||
EmbeddingStore<TextSegment> embeddingStore,
|
||||
EmbeddingModel embeddingModel) {
|
||||
this.assistant = assistant;
|
||||
this.embeddingStore = embeddingStore;
|
||||
this.embeddingModel = embeddingModel;
|
||||
}
|
||||
|
||||
public void ingestDocument(String content, Map<String, Object> metadata) {
|
||||
var document = Document.from(content);
|
||||
document.metadata().putAll(metadata);
|
||||
|
||||
var ingestor = EmbeddingStoreIngestor.builder()
|
||||
.embeddingModel(embeddingModel)
|
||||
.embeddingStore(embeddingStore)
|
||||
.documentSplitter(DocumentSplitters.recursive(500, 50))
|
||||
.build();
|
||||
|
||||
ingestor.ingest(document);
|
||||
}
|
||||
|
||||
public String answerQuestion(String question) {
|
||||
return assistant.answerAbout(question);
|
||||
}
|
||||
}
|
||||
|
||||
interface DocumentAssistant {
|
||||
String answerAbout(String question);
|
||||
}
|
||||
```
|
||||
|
||||
## 5. Scheduled Task for Document Updates
|
||||
|
||||
**Scenario**: Periodically update knowledge base.
|
||||
|
||||
```java
|
||||
@Service
|
||||
public class DocumentUpdateService {
|
||||
|
||||
private final EmbeddingStore<TextSegment> embeddingStore;
|
||||
private final EmbeddingModel embeddingModel;
|
||||
|
||||
@Autowired
|
||||
public DocumentUpdateService(
|
||||
EmbeddingStore<TextSegment> embeddingStore,
|
||||
EmbeddingModel embeddingModel) {
|
||||
this.embeddingStore = embeddingStore;
|
||||
this.embeddingModel = embeddingModel;
|
||||
}
|
||||
|
||||
@Scheduled(fixedRate = 86400000) // Daily
|
||||
public void updateDocuments() {
|
||||
var documents = fetchLatestDocuments();
|
||||
|
||||
var ingestor = EmbeddingStoreIngestor.builder()
|
||||
.embeddingModel(embeddingModel)
|
||||
.embeddingStore(embeddingStore)
|
||||
.build();
|
||||
|
||||
documents.forEach(ingestor::ingest);
|
||||
logger.info("Documents updated successfully");
|
||||
}
|
||||
|
||||
private List<Document> fetchLatestDocuments() {
|
||||
// Fetch from database or external API
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 6. Controller with Tool Integration
|
||||
|
||||
**Scenario**: AI service with business logic tools.
|
||||
|
||||
```java
|
||||
@Service
|
||||
public class BusinessLogicService {
|
||||
|
||||
@Tool("Get user by ID")
|
||||
public User getUser(@P("user ID") String userId) {
|
||||
// Implementation
|
||||
return new User(userId);
|
||||
}
|
||||
|
||||
@Tool("Calculate discount")
|
||||
public double calculateDiscount(@P("purchase amount") double amount) {
|
||||
if (amount > 1000) return 0.15;
|
||||
if (amount > 500) return 0.10;
|
||||
return 0.05;
|
||||
}
|
||||
}
|
||||
|
||||
@Service
|
||||
public class ToolAssistant {
|
||||
|
||||
private final ChatModel chatModel;
|
||||
private final BusinessLogicService businessLogic;
|
||||
|
||||
@Autowired
|
||||
public ToolAssistant(ChatModel chatModel, BusinessLogicService businessLogic) {
|
||||
this.chatModel = chatModel;
|
||||
this.businessLogic = businessLogic;
|
||||
}
|
||||
|
||||
public String processRequest(String request) {
|
||||
return AiServices.builder(Assistant.class)
|
||||
.chatModel(chatModel)
|
||||
.tools(businessLogic)
|
||||
.build()
|
||||
.chat(request);
|
||||
}
|
||||
}
|
||||
|
||||
interface Assistant {
|
||||
String chat(String message);
|
||||
}
|
||||
```
|
||||
|
||||
## 7. Error Handling with Spring Exception Handler
|
||||
|
||||
**Scenario**: Centralized error handling for AI services.
|
||||
|
||||
```java
|
||||
@ControllerAdvice
|
||||
public class AiExceptionHandler {
|
||||
|
||||
@ExceptionHandler(IllegalArgumentException.class)
|
||||
public ResponseEntity<ErrorResponse> handleBadRequest(IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(new ErrorResponse("Invalid input: " + e.getMessage()));
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<ErrorResponse> handleError(Exception e) {
|
||||
logger.error("Error in AI service", e);
|
||||
return ResponseEntity.internalServerError()
|
||||
.body(new ErrorResponse("An error occurred: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
class ErrorResponse {
|
||||
private String message;
|
||||
}
|
||||
```
|
||||
|
||||
## 8. Configuration Properties
|
||||
|
||||
**Scenario**: Externalize AI configuration.
|
||||
|
||||
```java
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "app.ai")
|
||||
@Data
|
||||
public class AiProperties {
|
||||
private String openaiApiKey;
|
||||
private String openaiModel = "gpt-4o-mini";
|
||||
private double temperature = 0.7;
|
||||
private int maxTokens = 2000;
|
||||
private String embeddingModel = "text-embedding-3-small";
|
||||
private int memorySize = 10;
|
||||
private String vectorStoreType = "in-memory";
|
||||
}
|
||||
|
||||
// application.yml
|
||||
app:
|
||||
ai:
|
||||
openai-api-key: ${OPENAI_API_KEY}
|
||||
openai-model: gpt-4o-mini
|
||||
temperature: 0.7
|
||||
max-tokens: 2000
|
||||
embedding-model: text-embedding-3-small
|
||||
memory-size: 10
|
||||
vector-store-type: pinecone
|
||||
```
|
||||
|
||||
## 9. Integration Testing
|
||||
|
||||
**Scenario**: Test AI services with Spring Boot Test.
|
||||
|
||||
```java
|
||||
@SpringBootTest
|
||||
class ChatServiceTest {
|
||||
|
||||
@MockBean
|
||||
private ChatModel chatModel;
|
||||
|
||||
@Autowired
|
||||
private ChatService chatService;
|
||||
|
||||
@Test
|
||||
void testChat() {
|
||||
when(chatModel.chat("Hello"))
|
||||
.thenReturn("Hi there!");
|
||||
|
||||
String response = chatService.chat("Hello");
|
||||
assertEquals("Hi there!", response);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 10. Async Processing with CompletableFuture
|
||||
|
||||
**Scenario**: Non-blocking AI service calls.
|
||||
|
||||
```java
|
||||
@Service
|
||||
@EnableAsync
|
||||
public class AsyncChatService {
|
||||
|
||||
private final ChatModel chatModel;
|
||||
|
||||
@Autowired
|
||||
public AsyncChatService(ChatModel chatModel) {
|
||||
this.chatModel = chatModel;
|
||||
}
|
||||
|
||||
@Async
|
||||
public CompletableFuture<String> chatAsync(String message) {
|
||||
try {
|
||||
String response = chatModel.chat(message);
|
||||
return CompletableFuture.completedFuture(response);
|
||||
} catch (Exception e) {
|
||||
return CompletableFuture.failedFuture(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Usage in controller
|
||||
@RestController
|
||||
public class AsyncController {
|
||||
|
||||
@Autowired
|
||||
private AsyncChatService asyncChatService;
|
||||
|
||||
@PostMapping("/chat/async")
|
||||
public CompletableFuture<ResponseEntity<String>> chatAsync(@RequestBody ChatRequest request) {
|
||||
return asyncChatService.chatAsync(request.getMessage())
|
||||
.thenApply(ResponseEntity::ok)
|
||||
.exceptionally(e -> ResponseEntity.internalServerError().build());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration Examples
|
||||
|
||||
### Maven Dependency
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>dev.langchain4j</groupId>
|
||||
<artifactId>langchain4j-spring-boot-starter</artifactId>
|
||||
<version>0.27.0</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
### Gradle
|
||||
```gradle
|
||||
implementation 'dev.langchain4j:langchain4j-spring-boot-starter:0.27.0'
|
||||
```
|
||||
@@ -0,0 +1,423 @@
|
||||
# LangChain4j Spring Boot Integration - API References
|
||||
|
||||
Complete API reference for Spring Boot integration with LangChain4j.
|
||||
|
||||
## Spring Boot Starter Dependencies
|
||||
|
||||
### Maven
|
||||
```xml
|
||||
<!-- Core Spring Boot LangChain4j integration -->
|
||||
<dependency>
|
||||
<groupId>dev.langchain4j</groupId>
|
||||
<artifactId>langchain4j-spring-boot-starter</artifactId>
|
||||
<version>0.27.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- OpenAI integration -->
|
||||
<dependency>
|
||||
<groupId>dev.langchain4j</groupId>
|
||||
<artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
|
||||
<version>0.27.0</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
### Gradle
|
||||
```gradle
|
||||
implementation 'dev.langchain4j:langchain4j-spring-boot-starter:0.27.0'
|
||||
implementation 'dev.langchain4j:langchain4j-open-ai-spring-boot-starter:0.27.0'
|
||||
```
|
||||
|
||||
## Auto-Configuration Properties
|
||||
|
||||
### OpenAI Configuration
|
||||
```yaml
|
||||
langchain4j:
|
||||
open-ai:
|
||||
api-key: ${OPENAI_API_KEY}
|
||||
model-name: gpt-4o-mini
|
||||
temperature: 0.7
|
||||
top-p: 1.0
|
||||
max-tokens: 2000
|
||||
timeout: 60s
|
||||
log-requests: true
|
||||
log-responses: true
|
||||
|
||||
openai-embedding:
|
||||
api-key: ${OPENAI_API_KEY}
|
||||
model-name: text-embedding-3-small
|
||||
timeout: 60s
|
||||
```
|
||||
|
||||
### Vector Store Configuration
|
||||
```yaml
|
||||
langchain4j:
|
||||
vector-store:
|
||||
type: in-memory # or pinecone, weaviate, qdrant, etc.
|
||||
|
||||
# Pinecone
|
||||
pinecone:
|
||||
api-key: ${PINECONE_API_KEY}
|
||||
index-name: my-index
|
||||
namespace: production
|
||||
|
||||
# Qdrant
|
||||
qdrant:
|
||||
host: localhost
|
||||
port: 6333
|
||||
collection-name: documents
|
||||
|
||||
# Weaviate
|
||||
weaviate:
|
||||
host: localhost
|
||||
port: 8080
|
||||
collection-name: Documents
|
||||
```
|
||||
|
||||
## Spring Configuration Annotations
|
||||
|
||||
### @Configuration
|
||||
```java
|
||||
@Configuration
|
||||
public class AiConfig {
|
||||
|
||||
@Bean
|
||||
public ChatModel chatModel() {
|
||||
// Bean definition
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public EmbeddingModel embeddingModel() {
|
||||
// Fallback bean
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### @ConditionalOnProperty
|
||||
```java
|
||||
@Configuration
|
||||
@ConditionalOnProperty(
|
||||
prefix = "app.ai",
|
||||
name = "enabled",
|
||||
havingValue = "true"
|
||||
)
|
||||
public class AiFeatureConfig {
|
||||
// Configuration only if enabled
|
||||
}
|
||||
```
|
||||
|
||||
### @EnableConfigurationProperties
|
||||
```java
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(AiProperties.class)
|
||||
public class AiConfig {
|
||||
|
||||
@Autowired
|
||||
private AiProperties aiProperties;
|
||||
}
|
||||
```
|
||||
|
||||
## Dependency Injection
|
||||
|
||||
### Constructor Injection (Recommended)
|
||||
```java
|
||||
@Service
|
||||
public class ChatService {
|
||||
private final ChatModel chatModel;
|
||||
private final EmbeddingModel embeddingModel;
|
||||
|
||||
public ChatService(ChatModel chatModel, EmbeddingModel embeddingModel) {
|
||||
this.chatModel = chatModel;
|
||||
this.embeddingModel = embeddingModel;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Field Injection (Discouraged)
|
||||
```java
|
||||
@Service
|
||||
public class ChatService {
|
||||
@Autowired
|
||||
private ChatModel chatModel; // Not recommended
|
||||
}
|
||||
```
|
||||
|
||||
### Setter Injection
|
||||
```java
|
||||
@Service
|
||||
public class ChatService {
|
||||
private ChatModel chatModel;
|
||||
|
||||
@Autowired
|
||||
public void setChatModel(ChatModel chatModel) {
|
||||
this.chatModel = chatModel;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## REST Annotations
|
||||
|
||||
### @RestController with RequestMapping
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/api/chat")
|
||||
public class ChatController {
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<Response> chat(@RequestBody ChatRequest request) {
|
||||
// Implementation
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<Response> getChat(@PathVariable String id) {
|
||||
// Implementation
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### RequestBody Validation
|
||||
```java
|
||||
@PostMapping
|
||||
public ResponseEntity<Response> chat(@Valid @RequestBody ChatRequest request) {
|
||||
// Validates request object
|
||||
}
|
||||
|
||||
public class ChatRequest {
|
||||
@NotBlank(message = "Message cannot be blank")
|
||||
private String message;
|
||||
|
||||
@Min(0)
|
||||
@Max(100)
|
||||
private int maxTokens = 2000;
|
||||
}
|
||||
```
|
||||
|
||||
## Exception Handling
|
||||
|
||||
### @ControllerAdvice
|
||||
```java
|
||||
@ControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
@ExceptionHandler(IllegalArgumentException.class)
|
||||
public ResponseEntity<ErrorResponse> handleBadRequest(IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(new ErrorResponse(400, e.getMessage()));
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<ErrorResponse> handleGlobalException(Exception e) {
|
||||
return ResponseEntity.internalServerError()
|
||||
.body(new ErrorResponse(500, "Internal server error"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ResponseStatusException
|
||||
```java
|
||||
if (!authorized) {
|
||||
throw new ResponseStatusException(
|
||||
HttpStatus.FORBIDDEN,
|
||||
"User not authorized"
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Async and Reactive
|
||||
|
||||
### @Async
|
||||
```java
|
||||
@Service
|
||||
@EnableAsync
|
||||
public class AsyncService {
|
||||
|
||||
@Async
|
||||
public CompletableFuture<String> processAsync(String input) {
|
||||
String result = processSync(input);
|
||||
return CompletableFuture.completedFuture(result);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### @Scheduled
|
||||
```java
|
||||
@Component
|
||||
public class ScheduledTasks {
|
||||
|
||||
@Scheduled(fixedRate = 60000) // Every minute
|
||||
public void performTask() {
|
||||
// Task implementation
|
||||
}
|
||||
|
||||
@Scheduled(cron = "0 0 * * * *") // Daily at midnight
|
||||
public void dailyTask() {
|
||||
// Daily task
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### @SpringBootTest
|
||||
```java
|
||||
@SpringBootTest
|
||||
class ChatServiceTest {
|
||||
|
||||
@Autowired
|
||||
private ChatService chatService;
|
||||
|
||||
@Test
|
||||
void testChat() {
|
||||
// Test implementation
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### @WebMvcTest
|
||||
```java
|
||||
@WebMvcTest(ChatController.class)
|
||||
class ChatControllerTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@MockBean
|
||||
private ChatService chatService;
|
||||
|
||||
@Test
|
||||
void testChatEndpoint() throws Exception {
|
||||
mockMvc.perform(post("/api/chat")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"message\": \"Hello\"}"))
|
||||
.andExpect(status().isOk());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### @DataJpaTest
|
||||
```java
|
||||
@DataJpaTest
|
||||
class DocumentRepositoryTest {
|
||||
|
||||
@Autowired
|
||||
private DocumentRepository repository;
|
||||
|
||||
@Test
|
||||
void testFindByUserId() {
|
||||
// Test implementation
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Logging Configuration
|
||||
|
||||
### application.yml
|
||||
```yaml
|
||||
logging:
|
||||
level:
|
||||
root: INFO
|
||||
dev.langchain4j: DEBUG
|
||||
org.springframework: WARN
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n"
|
||||
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
|
||||
file:
|
||||
name: logs/app.log
|
||||
```
|
||||
|
||||
## Health Checks
|
||||
|
||||
### Custom Health Indicator
|
||||
```java
|
||||
@Component
|
||||
public class AiHealthIndicator extends AbstractHealthIndicator {
|
||||
|
||||
@Override
|
||||
protected void doHealthCheck(Health.Builder builder) {
|
||||
try {
|
||||
// Check AI service availability
|
||||
chatModel.chat("ping");
|
||||
builder.up();
|
||||
} catch (Exception e) {
|
||||
builder.down().withDetail("reason", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Actuator Integration
|
||||
|
||||
### Maven Dependency
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
### Configuration
|
||||
```yaml
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health, metrics, info
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
```
|
||||
|
||||
## Security Configuration
|
||||
|
||||
### @EnableWebSecurity
|
||||
```java
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http.csrf().disable()
|
||||
.authorizeRequests()
|
||||
.antMatchers("/api/public/**").permitAll()
|
||||
.antMatchers("/api/private/**").authenticated()
|
||||
.and()
|
||||
.httpBasic();
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Bean Lifecycle
|
||||
|
||||
### @PostConstruct and @PreDestroy
|
||||
```java
|
||||
@Service
|
||||
public class AiService {
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
// Initialize resources
|
||||
embeddingStore = createEmbeddingStore();
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void cleanup() {
|
||||
// Clean up resources
|
||||
embeddingStore.close();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use Constructor Injection**: Explicitly declare dependencies
|
||||
2. **Externalize Configuration**: Use application.yml for settings
|
||||
3. **Handle Exceptions**: Use @ControllerAdvice for consistent error handling
|
||||
4. **Implement Caching**: Cache AI responses when appropriate
|
||||
5. **Use Async Processing**: For long-running AI operations
|
||||
6. **Add Health Checks**: Implement custom health indicators
|
||||
7. **Log Appropriately**: Debug AI service calls in development
|
||||
8. **Test Thoroughly**: Use @SpringBootTest and @WebMvcTest
|
||||
9. **Secure APIs**: Implement authentication and authorization
|
||||
10. **Monitor Performance**: Track AI service metrics
|
||||
Reference in New Issue
Block a user