ArafatKhan2198 commented on code in PR #9915: URL: https://github.com/apache/ozone/pull/9915#discussion_r3264058735
########## hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/chatbot/llm/LangChain4jDispatcher.java: ########## @@ -0,0 +1,405 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * <p> + * http://www.apache.org/licenses/LICENSE-2.0 + * <p> + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.ozone.recon.chatbot.llm; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import dev.langchain4j.data.message.AiMessage; +import dev.langchain4j.data.message.SystemMessage; +import dev.langchain4j.data.message.UserMessage; +import dev.langchain4j.model.anthropic.AnthropicChatModel; +import dev.langchain4j.model.chat.ChatLanguageModel; +import dev.langchain4j.model.chat.request.ChatRequest; +import dev.langchain4j.model.chat.response.ChatResponse; +import dev.langchain4j.model.googleai.GoogleAiGeminiChatModel; +import dev.langchain4j.model.openai.OpenAiChatModel; +import dev.langchain4j.model.output.TokenUsage; +import org.apache.hadoop.hdds.conf.OzoneConfiguration; +import org.apache.hadoop.ozone.recon.chatbot.ChatbotConfigKeys; +import org.apache.hadoop.ozone.recon.chatbot.security.CredentialHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * {@link LLMClient} implementation that sends chat requests to cloud LLM providers using + * <a href="https://github.com/langchain4j/langchain4j">LangChain4j</a>. + * + * <h2>Purpose</h2> + * <p>The Recon chatbot needs to call external APIs (OpenAI, Google Gemini, Anthropic Claude) + * with a stable Java contract. This class is the only place that talks to LangChain4j: it + * picks the right provider, builds a {@link ChatLanguageModel} for the requested model, + * converts messages into LangChain4j types, runs one completion, and maps the result back to + * {@link LLMResponse}. Higher layers ({@link org.apache.hadoop.ozone.recon.chatbot.agent.ChatbotAgent}, + * {@link org.apache.hadoop.ozone.recon.chatbot.api.ChatbotEndpoint}) depend only on {@link LLMClient}.</p> + * + * <h2>Lifecycle (no background work)</h2> + * <p>The class is registered in Guice as a singleton: one instance exists for the whole Recon process. + * There is no timer, no scheduled task, and no long-lived outbound connection. At startup + * the constructor only reads configuration and records which providers have API keys (for + * {@link #isAvailable()} and {@link #getSupportedModels()}). Actual network calls happen + * only when {@link #chatCompletion} runs on an HTTP request thread.</p> + * + * <h2>Request flow (one chat completion)</h2> + * <p>Each user message is handled synchronously on the thread that serves the REST call:</p> + * <pre> + * User HTTP request + * | + * v + * Jersey dispatches to ChatbotEndpoint (request thread) + * | + * v + * ChatbotAgent orchestrates tool selection / summarization + * | + * v + * LangChain4jDispatcher.chatCompletion(...) + * | + * +-- Resolve provider (see below) + * | + * +-- Build a new ChatLanguageModel for that provider + model name (configuration only) + * | + * +-- Translate ChatMessage list to LangChain4j messages (system / user / assistant) + * | + * +-- chatModel.chat(ChatRequest) --> outbound HTTPS to the vendor (may take seconds) + * | + * v + * LLMResponse returned to the agent, then JSON to the client + * </pre> + * + * <h2>How provider routing works</h2> + * <p>When {@link #chatCompletion} runs, the provider is chosen in this order:</p> + * <ol> + * <li>Optional {@code _provider} entry in the parameters map (e.g. {@code "gemini"}).</li> + * <li>If the model string looks like {@code provider:model}, the prefix before {@code :} + * is the provider.</li> + * <li>Otherwise the model name: {@code gpt-} / {@code o1} / {@code o3} → {@code openai}; + * {@code gemini} → {@code gemini}; {@code claude} → {@code anthropic}.</li> + * <li>If still unclear, {@link ChatbotConfigKeys#OZONE_RECON_CHATBOT_PROVIDER} is used.</li> + * </ol> + * <p>For each call, a fresh {@link ChatLanguageModel} is built with the exact model id the + * caller passed (for example {@code gemini-2.5-flash}). That object holds provider settings + * and timeout; the heavy work is the single {@code chat(...)} call. Different users on + * different threads each follow this flow independently; only read-only configuration is + * shared on the dispatcher instance.</p> + * + * <h2>Supported models listing</h2> + * <p>{@link #getSupportedModels()} returns a fixed list per provider for which a non-empty + * API key exists in configuration. It is not a live query to each vendor's model catalogue.</p> + */ +@Singleton +public class LangChain4jDispatcher implements LLMClient { + + private static final Logger LOG = + LoggerFactory.getLogger(LangChain4jDispatcher.class); + + private final OzoneConfiguration configuration; + private final CredentialHelper credentialHelper; + private final Duration timeout; + private final String defaultProvider; + + /** + * Per-provider static model lists — used by getSupportedModels() and isAvailable(). + * A provider only appears here if its API key is configured. + */ + private final Map<String, List<String>> supportedModels = new HashMap<>(); + + @Inject + public LangChain4jDispatcher(OzoneConfiguration configuration, + CredentialHelper credentialHelper) { + this.configuration = configuration; + this.credentialHelper = credentialHelper; + + int timeoutMs = configuration.getInt( + ChatbotConfigKeys.OZONE_RECON_CHATBOT_TIMEOUT_MS, + ChatbotConfigKeys.OZONE_RECON_CHATBOT_TIMEOUT_MS_DEFAULT); + this.timeout = Duration.ofMillis(timeoutMs); + + this.defaultProvider = configuration.get( + ChatbotConfigKeys.OZONE_RECON_CHATBOT_PROVIDER, + ChatbotConfigKeys.OZONE_RECON_CHATBOT_PROVIDER_DEFAULT); + + // Register available providers. A provider is considered "available" only if + // a non-empty API key has been configured for it. Model lists are read from + // ozone-site.xml so admins can update them without a code change when vendors + // rename, add, or retire models. + if (!credentialHelper.getSecret( + ChatbotConfigKeys.OZONE_RECON_CHATBOT_OPENAI_API_KEY).isEmpty()) { + supportedModels.put("openai", parseModelList(configuration, + ChatbotConfigKeys.OZONE_RECON_CHATBOT_OPENAI_MODELS, + ChatbotConfigKeys.OZONE_RECON_CHATBOT_OPENAI_MODELS_DEFAULT)); Review Comment: Customers can add, remove, or change models at any time simply by updating ozone-site.xml and restarting Recon no patch or code change required. This is intentional: it gives the cluster admin full control over what models are exposed, rather than automatically surfacing every model a provider releases. The hardcoded defaults are just a safe starting point for admins who don't configure anything. ########## hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/chatbot/llm/LangChain4jDispatcher.java: ########## @@ -0,0 +1,405 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * <p> + * http://www.apache.org/licenses/LICENSE-2.0 + * <p> + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.ozone.recon.chatbot.llm; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import dev.langchain4j.data.message.AiMessage; +import dev.langchain4j.data.message.SystemMessage; +import dev.langchain4j.data.message.UserMessage; +import dev.langchain4j.model.anthropic.AnthropicChatModel; +import dev.langchain4j.model.chat.ChatLanguageModel; +import dev.langchain4j.model.chat.request.ChatRequest; +import dev.langchain4j.model.chat.response.ChatResponse; +import dev.langchain4j.model.googleai.GoogleAiGeminiChatModel; +import dev.langchain4j.model.openai.OpenAiChatModel; +import dev.langchain4j.model.output.TokenUsage; +import org.apache.hadoop.hdds.conf.OzoneConfiguration; +import org.apache.hadoop.ozone.recon.chatbot.ChatbotConfigKeys; +import org.apache.hadoop.ozone.recon.chatbot.security.CredentialHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * {@link LLMClient} implementation that sends chat requests to cloud LLM providers using + * <a href="https://github.com/langchain4j/langchain4j">LangChain4j</a>. + * + * <h2>Purpose</h2> + * <p>The Recon chatbot needs to call external APIs (OpenAI, Google Gemini, Anthropic Claude) + * with a stable Java contract. This class is the only place that talks to LangChain4j: it + * picks the right provider, builds a {@link ChatLanguageModel} for the requested model, + * converts messages into LangChain4j types, runs one completion, and maps the result back to + * {@link LLMResponse}. Higher layers ({@link org.apache.hadoop.ozone.recon.chatbot.agent.ChatbotAgent}, + * {@link org.apache.hadoop.ozone.recon.chatbot.api.ChatbotEndpoint}) depend only on {@link LLMClient}.</p> + * + * <h2>Lifecycle (no background work)</h2> + * <p>The class is registered in Guice as a singleton: one instance exists for the whole Recon process. + * There is no timer, no scheduled task, and no long-lived outbound connection. At startup + * the constructor only reads configuration and records which providers have API keys (for + * {@link #isAvailable()} and {@link #getSupportedModels()}). Actual network calls happen + * only when {@link #chatCompletion} runs on an HTTP request thread.</p> + * + * <h2>Request flow (one chat completion)</h2> + * <p>Each user message is handled synchronously on the thread that serves the REST call:</p> + * <pre> + * User HTTP request + * | + * v + * Jersey dispatches to ChatbotEndpoint (request thread) + * | + * v + * ChatbotAgent orchestrates tool selection / summarization + * | + * v + * LangChain4jDispatcher.chatCompletion(...) + * | + * +-- Resolve provider (see below) + * | + * +-- Build a new ChatLanguageModel for that provider + model name (configuration only) + * | + * +-- Translate ChatMessage list to LangChain4j messages (system / user / assistant) + * | + * +-- chatModel.chat(ChatRequest) --> outbound HTTPS to the vendor (may take seconds) + * | + * v + * LLMResponse returned to the agent, then JSON to the client + * </pre> + * + * <h2>How provider routing works</h2> + * <p>When {@link #chatCompletion} runs, the provider is chosen in this order:</p> + * <ol> + * <li>Optional {@code _provider} entry in the parameters map (e.g. {@code "gemini"}).</li> + * <li>If the model string looks like {@code provider:model}, the prefix before {@code :} + * is the provider.</li> + * <li>Otherwise the model name: {@code gpt-} / {@code o1} / {@code o3} → {@code openai}; + * {@code gemini} → {@code gemini}; {@code claude} → {@code anthropic}.</li> + * <li>If still unclear, {@link ChatbotConfigKeys#OZONE_RECON_CHATBOT_PROVIDER} is used.</li> + * </ol> + * <p>For each call, a fresh {@link ChatLanguageModel} is built with the exact model id the + * caller passed (for example {@code gemini-2.5-flash}). That object holds provider settings + * and timeout; the heavy work is the single {@code chat(...)} call. Different users on + * different threads each follow this flow independently; only read-only configuration is + * shared on the dispatcher instance.</p> + * + * <h2>Supported models listing</h2> + * <p>{@link #getSupportedModels()} returns a fixed list per provider for which a non-empty + * API key exists in configuration. It is not a live query to each vendor's model catalogue.</p> + */ +@Singleton +public class LangChain4jDispatcher implements LLMClient { + + private static final Logger LOG = + LoggerFactory.getLogger(LangChain4jDispatcher.class); + + private final OzoneConfiguration configuration; + private final CredentialHelper credentialHelper; + private final Duration timeout; + private final String defaultProvider; + + /** + * Per-provider static model lists — used by getSupportedModels() and isAvailable(). + * A provider only appears here if its API key is configured. + */ + private final Map<String, List<String>> supportedModels = new HashMap<>(); + + @Inject + public LangChain4jDispatcher(OzoneConfiguration configuration, + CredentialHelper credentialHelper) { + this.configuration = configuration; + this.credentialHelper = credentialHelper; + + int timeoutMs = configuration.getInt( + ChatbotConfigKeys.OZONE_RECON_CHATBOT_TIMEOUT_MS, + ChatbotConfigKeys.OZONE_RECON_CHATBOT_TIMEOUT_MS_DEFAULT); + this.timeout = Duration.ofMillis(timeoutMs); + + this.defaultProvider = configuration.get( + ChatbotConfigKeys.OZONE_RECON_CHATBOT_PROVIDER, + ChatbotConfigKeys.OZONE_RECON_CHATBOT_PROVIDER_DEFAULT); + + // Register available providers. A provider is considered "available" only if + // a non-empty API key has been configured for it. Model lists are read from + // ozone-site.xml so admins can update them without a code change when vendors + // rename, add, or retire models. + if (!credentialHelper.getSecret( + ChatbotConfigKeys.OZONE_RECON_CHATBOT_OPENAI_API_KEY).isEmpty()) { + supportedModels.put("openai", parseModelList(configuration, + ChatbotConfigKeys.OZONE_RECON_CHATBOT_OPENAI_MODELS, + ChatbotConfigKeys.OZONE_RECON_CHATBOT_OPENAI_MODELS_DEFAULT)); + } + if (!credentialHelper.getSecret( + ChatbotConfigKeys.OZONE_RECON_CHATBOT_GEMINI_API_KEY).isEmpty()) { + supportedModels.put("gemini", parseModelList(configuration, + ChatbotConfigKeys.OZONE_RECON_CHATBOT_GEMINI_MODELS, + ChatbotConfigKeys.OZONE_RECON_CHATBOT_GEMINI_MODELS_DEFAULT)); + } + if (!credentialHelper.getSecret( + ChatbotConfigKeys.OZONE_RECON_CHATBOT_ANTHROPIC_API_KEY).isEmpty()) { + supportedModels.put("anthropic", parseModelList(configuration, + ChatbotConfigKeys.OZONE_RECON_CHATBOT_ANTHROPIC_MODELS, + ChatbotConfigKeys.OZONE_RECON_CHATBOT_ANTHROPIC_MODELS_DEFAULT)); Review Comment: Same as above. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
