listOpenAiHttp(String baseUrl, String apiKey, Duration timeout)
throws Exception {
if (Utils.isEmpty(baseUrl)) {
- throw new HopException("Base URL is required to list models for this provider.");
+ throw new AiUserException(BaseMessages.getString(PKG, "AiModelCatalog.NoBaseUrl"));
}
String root = stripTrailingSlash(baseUrl);
String body = httpGet(root + "/models", apiKey, timeout);
@@ -215,7 +219,8 @@ static String httpGet(String url, String bearerToken, Duration timeout) throws E
client.send(builder.build(), HttpResponse.BodyHandlers.ofString());
int status = response.statusCode();
if (status < 200 || status >= 300) {
- throw new HopException("HTTP " + status + " listing models from " + url);
+ throw new HopException(
+ BaseMessages.getString(PKG, "AiModelCatalog.HttpStatus", Integer.toString(status), url));
}
return Const.NVL(response.body(), "");
}
diff --git a/plugins/tech/ai/src/main/java/org/apache/hop/ai/engine/AiNodeSettings.java b/plugins/tech/ai/src/main/java/org/apache/hop/ai/engine/AiNodeSettings.java
new file mode 100644
index 00000000000..f4a9f2dec67
--- /dev/null
+++ b/plugins/tech/ai/src/main/java/org/apache/hop/ai/engine/AiNodeSettings.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
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.hop.ai.engine;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.hop.core.util.Utils;
+import org.apache.hop.metadata.api.HopMetadataProperty;
+import org.apache.hop.metadata.util.ReflectionUtil;
+
+/**
+ * The settings of a transform or action, as compact JSON for a prompt.
+ *
+ * Reads the {@code @HopMetadataProperty} fields: SQL, file names, connection names, field lists
+ * and so on, which is what a model needs to explain what a pipeline or workflow does. Password
+ * fields are skipped. Empty values, long texts and long lists are left out or shortened so one
+ * transform cannot take over the prompt.
+ */
+public final class AiNodeSettings {
+
+ static final int MAX_DEPTH = 3;
+ static final int MAX_LIST_ITEMS = 25;
+ static final int MAX_TEXT_CHARS = 400;
+ static final int MAX_NODE_CHARS = 3_000;
+
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ private AiNodeSettings() {}
+
+ /**
+ * @param node the transform or action meta
+ * @return a JSON object, {@code {"truncated":true}} when the settings are too large, or null when
+ * the plugin exposes no settings this way
+ */
+ public static String toJson(Object node) {
+ if (node == null) {
+ return null;
+ }
+ Object settings = read(node, 0);
+ if (!(settings instanceof Map, ?> map) || map.isEmpty()) {
+ return null;
+ }
+ try {
+ String json = MAPPER.writeValueAsString(settings);
+ if (json.length() > MAX_NODE_CHARS) {
+ return "{\"truncated\":true}";
+ }
+ return AiTextUtil.redactSecrets(json);
+ } catch (JsonProcessingException e) {
+ return null;
+ }
+ }
+
+ static Object read(Object value, int depth) {
+ if (value == null) {
+ return null;
+ }
+ if (value instanceof String text) {
+ if (text.isEmpty()) {
+ return null;
+ }
+ return text.length() > MAX_TEXT_CHARS ? text.substring(0, MAX_TEXT_CHARS) + "…" : text;
+ }
+ if (value instanceof Number || value instanceof Boolean) {
+ return value;
+ }
+ if (value instanceof Enum> e) {
+ return e.name();
+ }
+ if (depth >= MAX_DEPTH) {
+ return null;
+ }
+ if (value instanceof Collection> collection) {
+ return readList(collection, depth);
+ }
+ if (value.getClass().isArray() && value instanceof Object[] array) {
+ return readList(List.of(array), depth);
+ }
+ return readObject(value, depth);
+ }
+
+ private static List