priyeshkaratha commented on code in PR #9915:
URL: https://github.com/apache/ozone/pull/9915#discussion_r2998898146
##########
hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconControllerModule.java:
##########
@@ -129,6 +129,7 @@ protected void configure() {
install(new ReconOmTaskBindingModule());
install(new ReconDaoBindingModule());
bind(ReconTaskStatusUpdaterManager.class).in(Singleton.class);
+ install(new org.apache.hadoop.ozone.recon.chatbot.ChatbotModule());
Review Comment:
can we import org.apache.hadoop.ozone.recon.chatbot.ChatbotModule and use
instead of fully qualified name?
##########
hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/chatbot/agent/ToolExecutor.java:
##########
@@ -0,0 +1,450 @@
+/*
+ * 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.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+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.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Executes tool calls by making HTTP requests to Recon API endpoints.
+ */
+@Singleton
+public class ToolExecutor {
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(ToolExecutor.class);
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ // We define the specific String suffixes for APIs we want to explicitly
watch out for
+ private static final String LIST_KEYS_ENDPOINT_SUFFIX = "/keys/listKeys";
+
+ // Hardcoded security timeouts. If Recon takes longer than 30 seconds to
connect
+ // or return data, kill the request so we don't freeze the chatbot.
+ private static final int CONNECT_TIMEOUT_MS = 30_000;
+ private static final int READ_TIMEOUT_MS = 30_000;
+
+ private final String reconBaseUrl;
+ private final int defaultMaxRecords; // Max records to fetch in total
+ private final int defaultMaxPages; // Max pages to loop through
+ private final int defaultPageSize; // Default size of one page
+
+ @Inject
+ public ToolExecutor(OzoneConfiguration configuration) {
+ // Get Recon base URL from configuration
+ // Default to localhost for local development
+ this.reconBaseUrl = "http://localhost:9888";
Review Comment:
can be used something like this.
this.reconBaseUrl = configuration.get("ozone.recon.address",
"http://localhost:9888");
##########
hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/chatbot/llm/AnthropicClient.java:
##########
@@ -0,0 +1,158 @@
+/*
+ * 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.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+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 java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Direct client for Anthropic Claude models using Composition.
+ */
+public class AnthropicClient implements LLMClient {
+
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+ private static final String ANTHROPIC_VERSION = "2023-06-01";
+ private static final String ANTHROPIC_BETA_CONTEXT = "context-1m-2025-08-07";
+
+ private final OzoneConfiguration configuration;
+ private final CredentialHelper credentialHelper;
+ private final LLMNetworkClient networkClient;
+
+ public AnthropicClient(OzoneConfiguration configuration,
+ CredentialHelper credentialHelper,
+ int timeoutMs) {
+ this.configuration = configuration;
+ this.credentialHelper = credentialHelper;
+ this.networkClient = new LLMNetworkClient(timeoutMs);
+ }
+
+ @Override
+ public LLMResponse chatCompletion(List<ChatMessage> messages, String model,
String apiKey, Map<String, Object> parameters) throws LLMException {
+ String resolvedKey = resolveApiKey(apiKey);
+ if (resolvedKey == null || resolvedKey.isEmpty()) {
+ throw new LLMException("No API key configured for provider
'anthropic'.");
+ }
+
+ String url = getBaseUrl() + "/v1/messages";
+
+ // Construct the Anthropic specific JSON
+ ObjectNode body = MAPPER.createObjectNode();
+ body.put("model", model);
+
+ ArrayNode messagesArray = body.putArray("messages");
+ for (ChatMessage msg : messages) {
+ if ("system".equals(msg.getRole())) {
+ body.put("system", msg.getContent());
+ } else {
+ ObjectNode m = messagesArray.addObject();
+ m.put("role", msg.getRole());
+ m.put("content", msg.getContent());
+ }
+ }
+
+ if (parameters != null) {
+ if (parameters.containsKey("max_tokens")) {
+ body.put("max_tokens", ((Number)
parameters.get("max_tokens")).intValue());
+ } else {
+ body.put("max_tokens", 4096);
+ }
+ if (parameters.containsKey("temperature")) {
+ body.put("temperature", ((Number)
parameters.get("temperature")).doubleValue());
+ }
+ } else {
+ body.put("max_tokens", 4096);
+ }
+
+ Map<String, String> headers = new HashMap<>();
+ headers.put("x-api-key", resolvedKey);
+ headers.put("anthropic-version", ANTHROPIC_VERSION);
+ headers.put("anthropic-beta", ANTHROPIC_BETA_CONTEXT);
+
+ try {
+ String responseBody = networkClient.executePost(url, headers,
MAPPER.writeValueAsString(body), "anthropic");
+ return parseAnthropicResponse(responseBody, model);
+ } catch (Exception e) {
+ if (e instanceof LLMException) {
+ throw (LLMException) e;
+ }
+ throw new LLMException("Anthropic Request Failed: " + e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public boolean isAvailable() {
+ String key =
credentialHelper.getSecret(ChatbotConfigKeys.OZONE_RECON_CHATBOT_ANTHROPIC_API_KEY);
+ return key != null && !key.isEmpty();
+ }
+
+ @Override
+ public List<String> getSupportedModels() {
+ return Arrays.asList("claude-opus-4-6", "claude-sonnet-4-6");
Review Comment:
is there any specific reason to hardcode this value? can't we use this in
configuration. available models can be changed from clause, gemini etc.
##########
hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/chatbot/llm/OpenAIClient.java:
##########
@@ -0,0 +1,151 @@
+/*
+ * 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.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+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 java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Direct client for OpenAI models (GPT-4, GPT-4o, o1, o3, etc.).
+ * Talks to {@code api.openai.com/v1/chat/completions}.
+ */
+public class OpenAIClient implements LLMClient {
+
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+ private final OzoneConfiguration configuration;
+ private final CredentialHelper credentialHelper;
+ private final LLMNetworkClient networkClient;
+
+ public OpenAIClient(OzoneConfiguration configuration,
+ CredentialHelper credentialHelper,
+ int timeoutMs) {
+ this.configuration = configuration;
+ this.credentialHelper = credentialHelper;
+ this.networkClient = new LLMNetworkClient(timeoutMs);
+ }
+
+ @Override
+ public LLMResponse chatCompletion(List<ChatMessage> messages, String model,
String apiKey, Map<String, Object> parameters) throws LLMException {
+ String resolvedKey = resolveApiKey(apiKey);
+ if (resolvedKey == null || resolvedKey.isEmpty()) {
+ throw new LLMException("No API key configured for provider 'openai'.");
+ }
+
+ String url = getBaseUrl() + "/v1/chat/completions";
+ ObjectNode body = buildOpenAIRequestBody(messages, model, parameters);
+
+ Map<String, String> headers = new HashMap<>();
+ headers.put("Authorization", "Bearer " + resolvedKey);
+
+ try {
+ String responseBody = networkClient.executePost(url, headers,
MAPPER.writeValueAsString(body), "openai");
+ return parseOpenAIResponse(responseBody, model);
+ } catch (Exception e) {
+ if (e instanceof LLMException) {
+ throw (LLMException) e;
+ }
+ throw new LLMException("OpenAI Request Failed: " + e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public boolean isAvailable() {
+ String key =
credentialHelper.getSecret(ChatbotConfigKeys.OZONE_RECON_CHATBOT_OPENAI_API_KEY);
+ return key != null && !key.isEmpty();
+ }
+
+ @Override
+ public List<String> getSupportedModels() {
+ return Arrays.asList("gpt-4.1", "gpt-4.1-mini", "gpt-4.1-nano");
Review Comment:
can we move this models to configuration
##########
hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/chatbot/llm/LLMDispatcher.java:
##########
@@ -0,0 +1,147 @@
+/*
+ * 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 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.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * LLMDispatcher acts as the "Traffic Cop" Proxy.
+ * It implements LLMClient so the application thinks it's talking to an AI,
+ * but it secretly routes requests to the correct underlying AI client instead.
+ */
+@Singleton
+public class LLMDispatcher implements LLMClient {
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(LLMDispatcher.class);
+
+ private final Map<String, LLMClient> clients = new HashMap<>();
+ private final OzoneConfiguration configuration;
+
+ @Inject
+ public LLMDispatcher(OzoneConfiguration configuration,
+ CredentialHelper credentialHelper) {
+ this.configuration = configuration;
+
+ int timeoutMs = configuration.getInt(
+ ChatbotConfigKeys.OZONE_RECON_CHATBOT_TIMEOUT_MS,
+ ChatbotConfigKeys.OZONE_RECON_CHATBOT_TIMEOUT_MS_DEFAULT);
+
+ // Register all supported specific AI Clients!
+ clients.put("openai", new OpenAIClient(configuration, credentialHelper,
timeoutMs));
+ clients.put("gemini", new GeminiClient(configuration, credentialHelper,
timeoutMs));
+ clients.put("anthropic", new AnthropicClient(configuration,
credentialHelper, timeoutMs));
+
+ LOG.info("LLMDispatcher initialized with clients: {}", clients.keySet());
+ }
+
+ /**
+ * Figure out exactly which AI client should handle a prompt.
+ * Format allowed in UI: "provider:model" (e.g. "openai:gpt-4" or
"anthropic:claude-sonnet-4-6")
+ */
+ public LLMClient resolveClient(String requestProvider, String requestModel) {
+ if (requestProvider != null && !requestProvider.isEmpty() &&
clients.containsKey(requestProvider.toLowerCase())) {
+ return clients.get(requestProvider.toLowerCase());
+ }
+
+ if (requestModel != null) {
+ if (requestModel.contains(":")) {
+ String[] parts = requestModel.split(":", 2);
+ String prefix = parts[0].toLowerCase();
+ if (clients.containsKey(prefix)) {
+ return clients.get(prefix);
+ }
+ }
+
+ String m = requestModel.toLowerCase();
+ if (m.startsWith("gpt-") || m.startsWith("o1") || m.startsWith("o3")) {
Review Comment:
instead of hardcoded string prefixes, can we use flexible and configurable
mapping for model-to-client resolution.
##########
hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/chatbot/llm/GeminiClient.java:
##########
@@ -0,0 +1,158 @@
+/*
+ * 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.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+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 java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Direct client for Google Gemini models using Composition.
+ */
+public class GeminiClient implements LLMClient {
+
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+ private final OzoneConfiguration configuration;
+ private final CredentialHelper credentialHelper;
+ private final LLMNetworkClient networkClient;
+
+ public GeminiClient(OzoneConfiguration configuration,
+ CredentialHelper credentialHelper,
+ int timeoutMs) {
+ this.configuration = configuration;
+ this.credentialHelper = credentialHelper;
+ this.networkClient = new LLMNetworkClient(timeoutMs);
+ }
+
+ @Override
+ public LLMResponse chatCompletion(List<ChatMessage> messages, String model,
String apiKey, Map<String, Object> parameters) throws LLMException {
+ String resolvedKey = resolveApiKey(apiKey);
+ if (resolvedKey == null || resolvedKey.isEmpty()) {
+ throw new LLMException("No API key configured for provider 'gemini'.");
+ }
+
+ String url = getBaseUrl() + "/v1beta/models/" + model +
":generateContent?key=" + resolvedKey;
Review Comment:
The Gemini API key is passed directly in the URL as a query parameter. Is
this only way? Does this sensitive information will be exposed in any audit
logs etc?
--
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]