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


##########
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:
   I checked if LangChain4j exposes a model listing API, it doesn't, as it's 
purely a chat client library. The underlying providers (OpenAI, Gemini) do have 
REST endpoints to list models, but Anthropic has no such endpoint at all. More 
importantly, even OpenAI's model list returns 50+ non-chat models like whisper, 
dall-e, text-embedding which would break our agent if selected. So dynamic 
fetching would still require a hardcoded filter, making it more complex with no 
real benefit. The current ozone-site.xml configurable approach is intentionally 
simpler and safer.



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

Reply via email to