devmadhuu commented on code in PR #9915:
URL: https://github.com/apache/ozone/pull/9915#discussion_r3224534208


##########
hadoop-ozone/recon/pom.xml:
##########
@@ -62,6 +62,22 @@
       <groupId>commons-io</groupId>
       <artifactId>commons-io</artifactId>
     </dependency>
+    <dependency>
+      <groupId>dev.langchain4j</groupId>
+      <artifactId>langchain4j-anthropic</artifactId>

Review Comment:
   If you run `mvn dependency:tree` this dependency of `okhttp:4.12.0` will be 
emitted as a transitive dependency at compile time from this artifact: 
`langchain4j-anthropic` and same `okhttp-jvm:5.3.2` of different version will 
be emitted at runtime from `opentelemetry-exporter-sender-okhttp` artifact from 
main pom.xml. So adding an exclusion for `okhttp-jvm` in the recon pom.xml 
should be done.



##########
pom.xml:
##########
@@ -515,6 +569,26 @@
           </exclusion>
         </exclusions>
       </dependency>
+      <dependency>
+        <groupId>dev.langchain4j</groupId>
+        <artifactId>langchain4j-anthropic</artifactId>
+        <version>0.36.2</version>
+      </dependency>
+      <dependency>
+        <groupId>dev.langchain4j</groupId>
+        <artifactId>langchain4j-core</artifactId>
+        <version>0.36.2</version>
+      </dependency>
+      <dependency>
+        <groupId>dev.langchain4j</groupId>
+        <artifactId>langchain4j-google-ai-gemini</artifactId>
+        <version>0.36.2</version>
+      </dependency>
+      <dependency>
+        <groupId>dev.langchain4j</groupId>
+        <artifactId>langchain4j-open-ai</artifactId>
+        <version>0.36.2</version>
+      </dependency>

Review Comment:
   each langchain4j-* artifact added separately at 0.36.2 version rather than 
importing the langchain4j-bom. If need update of version later for 4 separate 
version entries, we should use bom



##########
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)  --&gt; 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:
   Model names cannot be hardcoded, default models should always be fetched 
latest at initialization and kept them as default.



##########
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)  --&gt; 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 here.



##########
hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/chatbot/agent/ChatbotAgent.java:
##########
@@ -0,0 +1,773 @@
+/*
+ * 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.agent;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.ozone.recon.chatbot.ChatbotConfigKeys;
+import org.apache.hadoop.ozone.recon.chatbot.llm.LLMClient;
+import org.apache.hadoop.ozone.recon.chatbot.llm.LLMClient.ChatMessage;
+import org.apache.hadoop.ozone.recon.chatbot.llm.LLMClient.LLMResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Main chatbot agent that orchestrates the conversation flow.
+ * Handles tool selection (figuring out what API to call), executing those 
calls,
+ * and summarization (feeding the data back to the LLM to write a nice answer).
+ */
+@Singleton
+public class ChatbotAgent {
+
+  private static final Logger LOG = 
LoggerFactory.getLogger(ChatbotAgent.class);
+
+  private static final ObjectMapper MAPPER = new ObjectMapper();
+  private static final Pattern JSON_PATTERN = Pattern.compile("\\{.*\\}", 
Pattern.DOTALL);

Review Comment:
   This still needs to resolve.



##########
hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconControllerModule.java:
##########
@@ -131,6 +131,12 @@ protected void configure() {
     install(new ReconOmTaskBindingModule());
     install(new ReconDaoBindingModule());
     bind(ReconTaskStatusUpdaterManager.class).in(Singleton.class);
+    // Only install chatbot bindings when the feature is explicitly enabled.
+    // This prevents startup-time failures (e.g. bad credential provider paths)
+    // from breaking Recon when the chatbot is intentionally disabled.
+    if (ChatbotConfigKeys.isChatbotEnabled(new ConfigurationProvider().get())) 
{

Review Comment:
   ```suggestion
       if (ChatbotConfigKeys.isChatbotEnabled(reconServer.getConf()))  {
   ```



##########
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)  --&gt; 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));
+    }
+
+    LOG.info("LangChain4jDispatcher initialized. Available providers: {}, 
default: {}",
+        supportedModels.keySet(), defaultProvider);
+  }
+
+  /**
+   * Sends the conversation to the appropriate LLM provider and returns a 
standardised response.
+   *
+   * <p>Steps:
+   * <ol>
+   *   <li>Determine which provider to use from model name prefix or explicit 
provider hint.</li>
+   *   <li>Build a LangChain4j {@link ChatLanguageModel} for that provider + 
model.</li>
+   *   <li>Translate internal {@link ChatMessage} list to LangChain4j message 
types.</li>
+   *   <li>Call the model, extract text + token counts, return {@link 
LLMResponse}.</li>
+   * </ol>
+   */
+  @Override
+  public LLMResponse chatCompletion(List<ChatMessage> messages, String 
modelStr, Map<String, Object> parameters)
+      throws LLMException {
+
+    if (messages == null || messages.isEmpty()) {
+      throw new LLMException("Messages cannot be null or empty");
+    }
+
+    // Extract provider hint and actual model name from "provider:model" 
format if present.
+    String providerHint = null;
+    String actualModel = modelStr;
+    if (parameters != null && parameters.containsKey("_provider")) {
+      providerHint = (String) parameters.get("_provider");
+    }
+    if (modelStr != null && modelStr.contains(":")) {
+      String[] parts = modelStr.split(":", 2);
+      providerHint = parts[0].toLowerCase();
+      actualModel = parts[1];
+    }
+
+    String provider = resolveProvider(providerHint, actualModel);
+    LOG.debug("Routing chatCompletion: model={}, resolvedProvider={}", 
actualModel, provider);
+
+    // Build the LangChain4j model for this specific request.
+    ChatLanguageModel chatModel = buildModel(provider, actualModel);

Review Comment:
   This `buildModel` creates a new HTTP client on every request (Performance). 
A new `ChatLanguageModel` — which internally builds an HTTP client with 
connection pool and SSL context — is constructed and thrown away on every 
`chatCompletion()` call. The models should be cached per (provider, modelName).



##########
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)  --&gt; 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));

Review Comment:
   Same here.



-- 
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]


Reply via email to