CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-org-springframework-ai--spring-ai-client-chat

Spring AI Chat Client provides a fluent API for building AI-powered applications with LLMs, supporting advisors, streaming, structured outputs, and conversation memory

Overview
Eval results
Files

custom-advisors.mddocs/reference/

Custom Advisors

Custom advisors allow you to extend the Chat Client with custom behavior for logging, validation, security, and other cross-cutting concerns. This guide covers creating custom advisors from scratch using the advisor interfaces.

Imports

import org.springframework.ai.chat.client.advisor.api.Advisor;
import org.springframework.ai.chat.client.advisor.api.CallAdvisor;
import org.springframework.ai.chat.client.advisor.api.StreamAdvisor;
import org.springframework.ai.chat.client.advisor.api.BaseAdvisor;
import org.springframework.ai.chat.client.advisor.api.CallAdvisorChain;
import org.springframework.ai.chat.client.advisor.api.StreamAdvisorChain;
import org.springframework.ai.chat.client.advisor.api.AdvisorChain;
import org.springframework.ai.chat.client.ChatClientRequest;
import org.springframework.ai.chat.client.ChatClientResponse;
import org.springframework.core.Ordered;
import reactor.core.publisher.Flux;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;

Creating CallAdvisor

For synchronous (blocking) request processing, implement CallAdvisor.

interface CallAdvisor extends Advisor {
    ChatClientResponse adviseCall(
        ChatClientRequest request,
        CallAdvisorChain chain
    );
}

Example - Request Validation Advisor:

import org.springframework.ai.chat.client.advisor.api.CallAdvisor;
import org.springframework.ai.chat.client.advisor.api.CallAdvisorChain;
import org.springframework.ai.chat.client.ChatClientRequest;
import org.springframework.ai.chat.client.ChatClientResponse;

public class RequestValidationAdvisor implements CallAdvisor {
    private final int maxPromptLength;

    public RequestValidationAdvisor(int maxPromptLength) {
        this.maxPromptLength = maxPromptLength;
    }

    @Override
    public ChatClientResponse adviseCall(
        ChatClientRequest request,
        CallAdvisorChain chain
    ) {
        // Extract prompt text
        String promptText = request.prompt()
            .getInstructions()
            .get(0)
            .getContent();

        // Validate
        if (promptText.length() > maxPromptLength) {
            throw new IllegalArgumentException(
                "Prompt exceeds maximum length of " + maxPromptLength
            );
        }

        // Continue chain
        return chain.nextCall(request);
    }

    @Override
    public String getName() {
        return "RequestValidationAdvisor";
    }

    @Override
    public int getOrder() {
        return 100; // Execute early
    }
}

Creating StreamAdvisor

For streaming (reactive) request processing, implement StreamAdvisor.

interface StreamAdvisor extends Advisor {
    Flux<ChatClientResponse> adviseStream(
        ChatClientRequest request,
        StreamAdvisorChain chain
    );
}

Example - Streaming Metrics Advisor:

import org.springframework.ai.chat.client.advisor.api.StreamAdvisor;
import org.springframework.ai.chat.client.advisor.api.StreamAdvisorChain;
import org.springframework.ai.chat.client.ChatClientRequest;
import org.springframework.ai.chat.client.ChatClientResponse;
import reactor.core.publisher.Flux;
import java.util.concurrent.atomic.AtomicInteger;

public class StreamingMetricsAdvisor implements StreamAdvisor {
    @Override
    public Flux<ChatClientResponse> adviseStream(
        ChatClientRequest request,
        StreamAdvisorChain chain
    ) {
        AtomicInteger chunkCount = new AtomicInteger(0);
        long startTime = System.currentTimeMillis();

        return chain.nextStream(request)
            .doOnNext(response -> {
                chunkCount.incrementAndGet();
            })
            .doOnComplete(() -> {
                long duration = System.currentTimeMillis() - startTime;
                System.out.println("Stream completed: " +
                    chunkCount.get() + " chunks in " +
                    duration + "ms");
            })
            .doOnError(error -> {
                System.err.println("Stream error: " + error.getMessage());
            });
    }

    @Override
    public String getName() {
        return "StreamingMetricsAdvisor";
    }

    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE;
    }
}

Creating BaseAdvisor

For advisors that work with both call and stream modes, implement BaseAdvisor which provides template methods.

interface BaseAdvisor extends CallAdvisor, StreamAdvisor {
    ChatClientRequest before(
        ChatClientRequest request,
        AdvisorChain chain
    );

    ChatClientResponse after(
        ChatClientResponse response,
        AdvisorChain chain
    );

    default Scheduler getScheduler() {
        return DEFAULT_SCHEDULER;
    }

    Scheduler DEFAULT_SCHEDULER = Schedulers.boundedElastic();
}

Template Methods:

  • before() - Pre-processing, modifies request before execution
  • after() - Post-processing, modifies response after execution
  • getScheduler() - Provides scheduler for streaming operations

Example - Authentication Advisor:

import org.springframework.ai.chat.client.advisor.api.BaseAdvisor;
import org.springframework.ai.chat.client.advisor.api.AdvisorChain;
import org.springframework.ai.chat.client.ChatClientRequest;
import org.springframework.ai.chat.client.ChatClientResponse;
import org.springframework.ai.chat.model.Prompt;
import org.springframework.ai.chat.model.SystemMessage;
import java.util.ArrayList;
import java.util.List;

public class AuthenticationAdvisor implements BaseAdvisor {
    private final AuthService authService;

    public AuthenticationAdvisor(AuthService authService) {
        this.authService = authService;
    }

    @Override
    public ChatClientRequest before(
        ChatClientRequest request,
        AdvisorChain chain
    ) {
        // Get user from context
        String userId = (String) request.context().get("userId");
        if (userId == null) {
            throw new SecurityException("User ID required");
        }

        // Verify authentication
        if (!authService.isAuthenticated(userId)) {
            throw new SecurityException("User not authenticated");
        }

        // Add user info to context for downstream advisors
        request.context().put("userRole", authService.getUserRole(userId));
        request.context().put("authenticated", true);

        return request;
    }

    @Override
    public ChatClientResponse after(
        ChatClientResponse response,
        AdvisorChain chain
    ) {
        // Log successful completion
        String userId = (String) response.context().get("userId");
        System.out.println("Request completed for user: " + userId);

        return response;
    }

    @Override
    public String getName() {
        return "AuthenticationAdvisor";
    }

    @Override
    public int getOrder() {
        return 50; // Execute very early
    }
}

Modifying Requests

Custom advisors can modify requests by creating new immutable request objects.

Example - Adding System Context:

import org.springframework.ai.chat.client.advisor.api.BaseAdvisor;
import org.springframework.ai.chat.client.advisor.api.AdvisorChain;
import org.springframework.ai.chat.client.ChatClientRequest;
import org.springframework.ai.chat.client.ChatClientResponse;
import org.springframework.ai.chat.model.Prompt;
import org.springframework.ai.chat.model.SystemMessage;
import org.springframework.ai.chat.model.Message;
import java.util.ArrayList;
import java.util.List;

public class SystemContextAdvisor implements BaseAdvisor {
    private final String systemContext;

    public SystemContextAdvisor(String systemContext) {
        this.systemContext = systemContext;
    }

    @Override
    public ChatClientRequest before(
        ChatClientRequest request,
        AdvisorChain chain
    ) {
        // Get existing prompt
        Prompt originalPrompt = request.prompt();

        // Build new message list with system context
        List<Message> messages = new ArrayList<>();
        messages.add(new SystemMessage(systemContext));
        messages.addAll(originalPrompt.getInstructions());

        // Create new prompt
        Prompt modifiedPrompt = new Prompt(
            messages,
            originalPrompt.getOptions()
        );

        // Return modified request
        return request.mutate()
            .prompt(modifiedPrompt)
            .build();
    }

    @Override
    public ChatClientResponse after(
        ChatClientResponse response,
        AdvisorChain chain
    ) {
        return response;
    }

    @Override
    public String getName() {
        return "SystemContextAdvisor";
    }

    @Override
    public int getOrder() {
        return 500;
    }
}

Modifying Responses

Custom advisors can modify responses after execution.

Example - Content Filtering Advisor:

import org.springframework.ai.chat.client.advisor.api.BaseAdvisor;
import org.springframework.ai.chat.client.advisor.api.AdvisorChain;
import org.springframework.ai.chat.client.ChatClientRequest;
import org.springframework.ai.chat.client.ChatClientResponse;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.model.AssistantMessage;
import java.util.regex.Pattern;

public class ContentFilterAdvisor implements BaseAdvisor {
    private final Pattern sensitivePattern;
    private final String replacement;

    public ContentFilterAdvisor(String regex, String replacement) {
        this.sensitivePattern = Pattern.compile(regex);
        this.replacement = replacement;
    }

    @Override
    public ChatClientRequest before(
        ChatClientRequest request,
        AdvisorChain chain
    ) {
        return request;
    }

    @Override
    public ChatClientResponse after(
        ChatClientResponse response,
        AdvisorChain chain
    ) {
        ChatResponse chatResponse = response.chatResponse();
        if (chatResponse == null) {
            return response;
        }

        // Filter content
        Generation result = chatResponse.getResult();
        String content = result.getOutput().getContent();
        String filtered = sensitivePattern.matcher(content)
            .replaceAll(replacement);

        // Create modified response (simplified)
        // In practice, you'd need to reconstruct the full ChatResponse
        response.context().put("contentFiltered", !content.equals(filtered));

        return response;
    }

    @Override
    public String getName() {
        return "ContentFilterAdvisor";
    }

    @Override
    public int getOrder() {
        return 9000; // Execute late, after content generation
    }
}

Using Request Context

The request context allows advisors to share data throughout the chain.

Example - Context Sharing:

import org.springframework.ai.chat.client.advisor.api.BaseAdvisor;
import org.springframework.ai.chat.client.advisor.api.AdvisorChain;
import org.springframework.ai.chat.client.ChatClientRequest;
import org.springframework.ai.chat.client.ChatClientResponse;
import java.util.HashMap;
import java.util.Map;

// First advisor: Add metadata
public class MetadataAdvisor implements BaseAdvisor {
    @Override
    public ChatClientRequest before(
        ChatClientRequest request,
        AdvisorChain chain
    ) {
        Map<String, Object> metadata = new HashMap<>();
        metadata.put("requestId", UUID.randomUUID().toString());
        metadata.put("timestamp", System.currentTimeMillis());
        metadata.put("environment", "production");

        request.context().putAll(metadata);
        return request;
    }

    @Override
    public ChatClientResponse after(
        ChatClientResponse response,
        AdvisorChain chain
    ) {
        return response;
    }

    @Override
    public String getName() {
        return "MetadataAdvisor";
    }

    @Override
    public int getOrder() {
        return 100;
    }
}

// Second advisor: Use metadata
public class AuditAdvisor implements BaseAdvisor {
    private final AuditService auditService;

    public AuditAdvisor(AuditService auditService) {
        this.auditService = auditService;
    }

    @Override
    public ChatClientRequest before(
        ChatClientRequest request,
        AdvisorChain chain
    ) {
        return request;
    }

    @Override
    public ChatClientResponse after(
        ChatClientResponse response,
        AdvisorChain chain
    ) {
        // Read metadata from context
        String requestId = (String) response.context().get("requestId");
        long timestamp = (long) response.context().get("timestamp");
        String environment = (String) response.context().get("environment");

        // Audit the request
        auditService.log(requestId, timestamp, environment);

        return response;
    }

    @Override
    public String getName() {
        return "AuditAdvisor";
    }

    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE;
    }
}

Conditional Execution

Advisors can conditionally execute based on context or request properties.

Example - Feature Flag Advisor:

import org.springframework.ai.chat.client.advisor.api.BaseAdvisor;
import org.springframework.ai.chat.client.advisor.api.AdvisorChain;
import org.springframework.ai.chat.client.ChatClientRequest;
import org.springframework.ai.chat.client.ChatClientResponse;

public class FeatureFlagAdvisor implements BaseAdvisor {
    private final String featureName;
    private final FeatureFlagService featureFlags;

    public FeatureFlagAdvisor(
        String featureName,
        FeatureFlagService featureFlags
    ) {
        this.featureName = featureName;
        this.featureFlags = featureFlags;
    }

    @Override
    public ChatClientRequest before(
        ChatClientRequest request,
        AdvisorChain chain
    ) {
        // Check feature flag
        boolean enabled = featureFlags.isEnabled(featureName);

        if (!enabled) {
            // Skip this advisor's logic
            return request;
        }

        // Execute feature logic
        System.out.println("Feature " + featureName + " is enabled");
        request.context().put(featureName + ".enabled", true);

        return request;
    }

    @Override
    public ChatClientResponse after(
        ChatClientResponse response,
        AdvisorChain chain
    ) {
        // Only process if feature was enabled
        Boolean enabled = (Boolean) response.context()
            .get(featureName + ".enabled");

        if (Boolean.TRUE.equals(enabled)) {
            System.out.println("Feature " + featureName + " completed");
        }

        return response;
    }

    @Override
    public String getName() {
        return "FeatureFlagAdvisor-" + featureName;
    }

    @Override
    public int getOrder() {
        return 1000;
    }
}

Error Handling

Custom advisors should handle errors appropriately.

Example - Error Recovery Advisor:

import org.springframework.ai.chat.client.advisor.CallAdvisor;
import org.springframework.ai.chat.client.advisor.CallAdvisorChain;
import org.springframework.ai.chat.client.ChatClientRequest;
import org.springframework.ai.chat.client.ChatClientResponse;

public class ErrorRecoveryAdvisor implements CallAdvisor {
    private final int maxRetries;

    public ErrorRecoveryAdvisor(int maxRetries) {
        this.maxRetries = maxRetries;
    }

    @Override
    public ChatClientResponse adviseCall(
        ChatClientRequest request,
        CallAdvisorChain chain
    ) {
        int attempt = 0;
        Exception lastError = null;

        while (attempt < maxRetries) {
            try {
                return chain.nextCall(request);
            } catch (Exception e) {
                lastError = e;
                attempt++;
                System.err.println("Attempt " + attempt + " failed: " +
                    e.getMessage());

                if (attempt < maxRetries) {
                    // Wait before retry
                    try {
                        Thread.sleep(1000L * attempt);
                    } catch (InterruptedException ie) {
                        Thread.currentThread().interrupt();
                        throw new RuntimeException(ie);
                    }
                }
            }
        }

        throw new RuntimeException(
            "Failed after " + maxRetries + " attempts",
            lastError
        );
    }

    @Override
    public String getName() {
        return "ErrorRecoveryAdvisor";
    }

    @Override
    public int getOrder() {
        return 0; // Execute first to wrap entire chain
    }
}

Streaming Transformations

Stream advisors can transform streaming responses.

Example - Token Rate Limiter:

import org.springframework.ai.chat.client.advisor.api.StreamAdvisor;
import org.springframework.ai.chat.client.advisor.api.StreamAdvisorChain;
import org.springframework.ai.chat.client.ChatClientRequest;
import org.springframework.ai.chat.client.ChatClientResponse;
import reactor.core.publisher.Flux;
import java.time.Duration;

public class TokenRateLimiterAdvisor implements StreamAdvisor {
    private final Duration delayPerToken;

    public TokenRateLimiterAdvisor(Duration delayPerToken) {
        this.delayPerToken = delayPerToken;
    }

    @Override
    public Flux<ChatClientResponse> adviseStream(
        ChatClientRequest request,
        StreamAdvisorChain chain
    ) {
        return chain.nextStream(request)
            .delayElements(delayPerToken)
            .doOnNext(response -> {
                System.out.println("Token released");
            });
    }

    @Override
    public String getName() {
        return "TokenRateLimiterAdvisor";
    }

    @Override
    public int getOrder() {
        return 8000;
    }
}

Complete Example

Custom Advisor with Builder Pattern:

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.api.BaseAdvisor;
import org.springframework.ai.chat.client.advisor.api.AdvisorChain;
import org.springframework.ai.chat.client.ChatClientRequest;
import org.springframework.ai.chat.client.ChatClientResponse;

public class RateLimitAdvisor implements BaseAdvisor {
    private final int maxRequestsPerMinute;
    private final RateLimiter rateLimiter;
    private final int order;

    private RateLimitAdvisor(Builder builder) {
        this.maxRequestsPerMinute = builder.maxRequestsPerMinute;
        this.rateLimiter = new RateLimiter(maxRequestsPerMinute);
        this.order = builder.order;
    }

    public static Builder builder() {
        return new Builder();
    }

    @Override
    public ChatClientRequest before(
        ChatClientRequest request,
        AdvisorChain chain
    ) {
        // Check rate limit
        if (!rateLimiter.allowRequest()) {
            throw new RateLimitException(
                "Rate limit exceeded: " + maxRequestsPerMinute +
                " requests per minute"
            );
        }

        return request;
    }

    @Override
    public ChatClientResponse after(
        ChatClientResponse response,
        AdvisorChain chain
    ) {
        return response;
    }

    @Override
    public String getName() {
        return "RateLimitAdvisor";
    }

    @Override
    public int getOrder() {
        return order;
    }

    public static class Builder {
        private int maxRequestsPerMinute = 60;
        private int order = 100;

        public Builder maxRequestsPerMinute(int maxRequestsPerMinute) {
            this.maxRequestsPerMinute = maxRequestsPerMinute;
            return this;
        }

        public Builder order(int order) {
            this.order = order;
            return this;
        }

        public RateLimitAdvisor build() {
            return new RateLimitAdvisor(this);
        }
    }
}

// Usage
ChatClient client = ChatClient.builder(chatModel)
    .defaultAdvisors(
        RateLimitAdvisor.builder()
            .maxRequestsPerMinute(30)
            .order(50)
            .build()
    )
    .build();

String response = client
    .prompt("Hello")
    .call()
    .content();

Install with Tessl CLI

npx tessl i tessl/maven-org-springframework-ai--spring-ai-client-chat@1.1.0

docs

index.md

tile.json