AiProviderClient.java

package org.egothor.methodatlas.ai;

import java.util.List;

/**
 * Provider-specific client abstraction used to communicate with external AI
 * inference services. Sealed so that the orchestration layer can pattern-match
 * exhaustively across the four supported providers, and so that adding a new
 * provider is a deliberate two-step change (new permitted type plus factory
 * entry).
 *
 * <p>
 * Implementations of this interface encapsulate the protocol and request
 * formatting required to interact with a particular AI provider such as OpenAI,
 * Ollama, Anthropic, or OpenRouter. The interface isolates the rest of the
 * application from provider-specific details including authentication, endpoint
 * layout, and response normalization.
 * </p>
 *
 * <p>
 * Instances are typically created by the AI integration layer during
 * initialization of the {@link AiSuggestionEngine}. Each client is responsible
 * for transforming a class-level analysis request into the provider’s native
 * API format and mapping the response back into the internal
 * {@link AiClassSuggestion} representation used by the application.
 * </p>
 *
 * <h2>Provider Responsibilities</h2>
 *
 * <ul>
 * <li>constructing provider-specific HTTP requests</li>
 * <li>handling authentication and API keys</li>
 * <li>sending inference requests</li>
 * <li>parsing and validating AI responses</li>
 * <li>normalizing results into {@link AiClassSuggestion}</li>
 * </ul>
 *
 * <p>
 * Implementations are expected to be stateless and thread-safe unless
 * explicitly documented otherwise.
 * </p>
 *
 * @see AiSuggestionEngine
 * @see AiClassSuggestion
 * @see AiProvider
 */
public sealed interface AiProviderClient
        permits OllamaClient, OpenAiCompatibleClient, AnthropicClient, AzureOpenAiClient {

    /**
     * Normalises a raw provider suggestion into the application's internal
     * result invariants.
     *
     * <p>
     * Provider response shapes differ, but the post-deserialisation cleanup is
     * identical across every provider: collection-valued fields are never
     * {@code null}, and malformed method entries (missing or blank
     * {@code methodName}) are filtered out. Hosting this logic on the sealed
     * interface guarantees the four providers cannot drift from each other.
     * </p>
     *
     * @param input raw suggestion returned by a provider; must not be
     *              {@code null}
     * @return normalised suggestion with non-null collections and
     *         well-formed method entries
     */
    static AiClassSuggestion normalize(AiClassSuggestion input) {
        List<AiMethodSuggestion> methods = input.methods() == null ? List.of() : input.methods();
        List<String> classTags = input.classTags() == null ? List.of() : input.classTags();

        List<AiMethodSuggestion> normalizedMethods = methods.stream()
                .filter(method -> method != null && method.methodName() != null && !method.methodName().isBlank())
                .map(method -> new AiMethodSuggestion(method.methodName(), method.securityRelevant(),
                        method.displayName(), method.tags() == null ? List.of() : method.tags(), method.reason(),
                        method.confidence(), method.interactionScore()))
                .toList();

        return new AiClassSuggestion(input.className(), input.classSecurityRelevant(), classTags, input.classReason(),
                normalizedMethods);
    }

    /**
     * Determines whether the provider is reachable and usable in the current
     * runtime environment.
     *
     * <p>
     * Implementations typically perform a lightweight availability check such as
     * probing the provider's base endpoint or verifying that required configuration
     * (for example, API keys or local services) is present.
     * </p>
     *
     * <p>
     * This method is primarily used when {@link AiProvider#AUTO} selection is
     * enabled so the system can choose the first available provider.
     * </p>
     *
     * @return {@code true} if the provider appears available and ready to accept
     *         inference requests; {@code false} otherwise
     */
    boolean isAvailable();

    /**
     * Requests AI-based security classification for a parsed test class.
     *
     * <p>
     * The caller renders the complete user prompt — class source, security
     * taxonomy, and the deterministically discovered target methods — through
     * {@link PromptBuilder#build}. The implementation submits that prompt
     * verbatim to the underlying AI provider, wrapping it only in the
     * provider's native request envelope (system message, model identifier,
     * and sampling parameters), and analyzes the structured response.
     * </p>
     *
     * <p>
     * Centralising prompt assembly in the caller guarantees that every
     * provider sends an identical prompt and that observers (such as the
     * evidence-pack archive) record the exact text submitted rather than a
     * reconstruction of it.
     * </p>
     *
     * <p>
     * The response is normalized into an {@link AiClassSuggestion} instance
     * containing both class-level metadata and a list of {@link AiMethodSuggestion}
     * objects describing individual test methods.
     * </p>
     *
     * @param fqcn   fully qualified name of the analyzed class; used only to
     *               produce diagnostic messages
     * @param prompt fully rendered user prompt produced by
     *               {@link PromptBuilder#build}; must not be {@code null}
     * @return normalized AI classification result
     *
     * @throws AiSuggestionException if the request fails due to provider errors,
     *                               malformed responses, or communication failures
     *
     * @see AiClassSuggestion
     * @see AiMethodSuggestion
     * @see PromptBuilder#build(String, String, String, List, boolean)
     */
    AiClassSuggestion suggestForClass(String fqcn, String prompt) throws AiSuggestionException;
}