summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Peter Christen <mc@yacy.net>2026-02-08 21:44:04 +0100
committerMichael Peter Christen <mc@yacy.net>2026-02-08 21:44:04 +0100
commit2141aef2c882ee9d53a785c9570ac593018bd810 (patch)
treeacbdd1619862bbd0a1e0436269a3057ff91369d2
parent9cd693a464b19f8788ba4438d68367b1aed4014b (diff)
added skill configuration: we present llm tools as "Skills" and make
them configurable in the YaCy GUI.
-rw-r--r--htroot/AILab.html36
-rw-r--r--htroot/LLMSelection_p.html21
-rw-r--r--htroot/SkillsConfig_p.html185
-rw-r--r--htroot/env/templates/submenuAI.template1
-rw-r--r--source/net/yacy/ai/ToolProvider.java130
-rw-r--r--source/net/yacy/ai/tools/CalculatorTool.java2
-rw-r--r--source/net/yacy/htroot/AILab.java7
-rw-r--r--source/net/yacy/htroot/SkillsConfig_p.java89
-rw-r--r--source/net/yacy/server/serverSwitch.java18
9 files changed, 461 insertions, 28 deletions
diff --git a/htroot/AILab.html b/htroot/AILab.html
index caa89d83b..b3462048c 100644
--- a/htroot/AILab.html
+++ b/htroot/AILab.html
@@ -209,6 +209,12 @@
filter: grayscale(0.2);
pointer-events: none;
}
+
+ @media (min-width: 768px) {
+ .quest-body-nowrap {
+ flex-wrap: nowrap;
+ }
+ }
</style>
</head>
<body id="AILab" data-ailab-inference-configured="#[ailab_inference_configured]#">
@@ -222,7 +228,7 @@
<p>Complete the quests below to unlock YaCy's AI sidekick: bind an inference engine, load production models, feed it with your index, then wire RAG and shields.</p>
<div class="lab-progress">
<div class="progress">
- <div id="labProgressBar" class="progress-bar progress-bar-success" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0" style="width:0%">0 / 5 unlocked</div>
+ <div id="labProgressBar" class="progress-bar progress-bar-success" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0" style="width:0%">0 / 6 unlocked</div>
</div>
</div>
</div>
@@ -306,6 +312,25 @@
</div>
</div>
+ <div class="bs-callout bs-callout-optional quest-callout status-pending" data-quest="tools" data-status="#[ailab_tools_status]#">
+ <div class="quest-meta">
+ <span class="label label-info">Optional</span>
+ <span class="meta-arrow" aria-hidden="true">→</span>
+ <span class="status-pill">Needs setup</span>
+ </div>
+ <h3>Enable/Disable Sills</h3>
+ <p>Superpowers for the YaCy Chat</p>
+ <div class="quest-body">
+ <div class="quest-visual">
+ <img src="env/grafics/AILab_Matrix.png" alt="Tools configuration" width="128" height="128" />
+ </div>
+ <div class="quest-actions">
+ <a class="btn btn-info btn-sm" href="SkillsConfig_p.html">Open skills configuration</a><br />
+ <span class="quest-note">Tune descriptions and set maxCallsPerTurn per tool (0 disables a tool).</span>
+ </div>
+ </div>
+ </div>
+
<div class="bs-callout bs-callout-optional quest-callout status-pending" data-quest="shield" data-status="#[ailab_shield_status]#">
<div class="quest-meta">
<span class="label label-info">Optional</span>
@@ -314,7 +339,7 @@
</div>
<h3>Define a shield</h3>
<p>Add guardrails: access rates, grant or deny non-localhost access. Activate the front page link for chat to complete this quest.</p>
- <div class="quest-body">
+ <div class="quest-body quest-body-nowrap">
<div class="quest-visual">
<img src="env/grafics/AILab_shield.png" alt="Shield definition" width="128" height="128" />
</div>
@@ -377,7 +402,7 @@
});
// Gating: enforce build order
- const questOrder = ["inference", "model", "index", "rag", "shield"];
+ const questOrder = ["inference", "model", "index", "rag", "tools", "shield"];
let prerequisitesMet = true;
questOrder.forEach(function(name) {
const callout = document.querySelector('.quest-callout[data-quest="' + name + '"]');
@@ -386,6 +411,9 @@
// Only gate by prior quests; index stays clickable even while filling up.
let locked = !prerequisitesMet;
+ if (name === "tools" || name === "shield") {
+ locked = false;
+ }
if (name === "model") {
const inferenceConfigured = document.body.getAttribute("data-ailab-inference-configured") === "1";
locked = locked || !inferenceConfigured;
@@ -402,7 +430,7 @@
}
// Block subsequent quests until current one is ready (index must reach threshold to release RAG and later steps).
- if (s !== "ready" || (name === "index" && indexCount < indexNeeded)) {
+ if (name !== "tools" && (s !== "ready" || (name === "index" && indexCount < indexNeeded))) {
prerequisitesMet = false;
}
});
diff --git a/htroot/LLMSelection_p.html b/htroot/LLMSelection_p.html
index 312386389..493e91fc8 100644
--- a/htroot/LLMSelection_p.html
+++ b/htroot/LLMSelection_p.html
@@ -411,6 +411,12 @@
function renderModelTable(container, title, rows) {
if (!container) return;
container.innerHTML = `<legend>${title}</legend>`;
+ if (title === "Available Models") {
+ const legend = container.querySelector("legend");
+ if (legend) {
+ legend.id = "availableModels";
+ }
+ }
if (!rows || !rows.length) {
container.style.display = "none";
return;
@@ -1167,6 +1173,20 @@
if (presetService && presetHoststub) {
loadModelList(true);
}
+ if (window.location.hash === "#availableModels") {
+ loadModelList(true)
+ .catch(() => null)
+ .finally(() => {
+ window.setTimeout(() => {
+ const target = document.getElementById("availableModels")
+ || document.getElementById("availableModelsAnchor")
+ || document.getElementById("availableModelsContainer");
+ if (target) {
+ target.scrollIntoView({ behavior: "smooth", block: "start" });
+ }
+ }, 0);
+ });
+ }
} catch (e) {
console.error("Initialization failed", e);
}
@@ -1230,6 +1250,7 @@
<legend>Model Downloads</legend>
<div id="downloadActivityList"></div>
</fieldset>
+ <a id="availableModelsAnchor"></a>
<fieldset id="availableModelsContainer" style="display:none"><a name="availableModels"></a></fieldset>
<fieldset id="productionModelsContainer" style="display: block;"><a name="productionModels"></a><legend>Production Models Matrix</legend>
diff --git a/htroot/SkillsConfig_p.html b/htroot/SkillsConfig_p.html
new file mode 100644
index 000000000..3b8d5a52f
--- /dev/null
+++ b/htroot/SkillsConfig_p.html
@@ -0,0 +1,185 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title>YaCy '#[clientname]#': Skills Config</title>
+ #%env/templates/metas.template%#
+ <style type="text/css">
+ .tool-card {
+ border: 1px solid #e6e9ef;
+ border-left: 6px solid #5bc0de;
+ padding: 10px 18px 18px 18px;
+ margin-bottom: 16px;
+ background: #fff;
+ box-shadow: 0 3px 10px rgba(0,0,0,0.06);
+ }
+ .tool-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 12px;
+ margin-bottom: 10px;
+ }
+ .tool-header h3 {
+ margin: 0;
+ color: #0f2b46;
+ }
+ .tool-limit {
+ display: flex;
+ align-items: stretch;
+ gap: 8px;
+ color: #3b4a5e;
+ white-space: nowrap;
+ }
+ .tool-limit label {
+ display: inline-flex;
+ align-items: center;
+ }
+ .tool-limit input {
+ width: 56px;
+ height: 34px;
+ padding: 6px 8px;
+ }
+ .tool-limit .toggle-button {
+ min-width: 70px;
+ height: 34px;
+ padding: 6px 10px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ }
+ .tool-description {
+ width: 100%;
+ resize: vertical;
+ }
+ .skills-grid {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 18px;
+ align-items: stretch;
+ }
+ .skills-grid .tool-card {
+ flex: 1 1 48%;
+ min-width: 320px;
+ margin-bottom: 0;
+ }
+ @media (max-width: 767px) {
+ .skills-grid .tool-card {
+ flex-basis: 100%;
+ }
+ }
+ </style>
+ </head>
+ <body id="SkillsConfig">
+ #%env/templates/header.template%#
+ #%env/templates/submenuAI.template%#
+
+ <h2>Skills</h2>
+ <p>Add superpowers to the YaCy Chat. Skills may be disabled by setting maxCallsPerTurn to 0.</p>
+ <!-- Technical naming note: internally these are called tools; in the user experience they are shown as skills. -->
+
+ #(status)#
+ ::<div class="alert alert-success">Tool settings were saved.</div>
+ #(/status)#
+ #(status_hasInvalidMaxCalls)#
+ ::<div class="alert alert-warning">#[status_invalidMaxCalls]# invalid maxCallsPerTurn value(s) were reset to the previous values.</div>
+ #(/status_hasInvalidMaxCalls)#
+
+ <form action="SkillsConfig_p.html" method="post" accept-charset="UTF-8">
+ <input type="hidden" name="save" value="1" />
+
+ <h3>Basic Skills</h3>
+ <div class="skills-grid">
+ #{basic_tools}#
+ <div class="tool-card">
+ <div class="tool-header">
+ <h3><code>#[name]#</code></h3>
+ <div class="tool-limit">
+ <label for="tool-limit-basic-#[name]#">maxCallsPerTurn</label>
+ <input id="tool-limit-basic-#[name]#" type="number" class="form-control max-calls-input" min="0" max="99" name="ai.tools.#[name]#.maxCallsPerTurn" value="#[maxCallsPerTurn]#" />
+ <button type="button" class="btn btn-default toggle-button">disable</button>
+ </div>
+ </div>
+ <textarea class="tool-description form-control" name="ai.tools.#[name]#.description" rows="2">#[description]#</textarea>
+ </div>
+ #{/basic_tools}#
+ </div>
+
+ <h3>Visualization Skills</h3>
+ <div class="skills-grid">
+ #{visualization_tools}#
+ <div class="tool-card">
+ <div class="tool-header">
+ <h3><code>#[name]#</code></h3>
+ <div class="tool-limit">
+ <label for="tool-limit-vis-#[name]#">maxCallsPerTurn</label>
+ <input id="tool-limit-vis-#[name]#" type="number" class="form-control max-calls-input" min="0" max="99" name="ai.tools.#[name]#.maxCallsPerTurn" value="#[maxCallsPerTurn]#" />
+ <button type="button" class="btn btn-default toggle-button">disable</button>
+ </div>
+ </div>
+ <textarea class="tool-description form-control" name="ai.tools.#[name]#.description" rows="2">#[description]#</textarea>
+ </div>
+ #{/visualization_tools}#
+ </div>
+
+ <h3>Data Retrieval Skills</h3>
+ <div class="skills-grid">
+ #{retrieval_tools}#
+ <div class="tool-card">
+ <div class="tool-header">
+ <h3><code>#[name]#</code></h3>
+ <div class="tool-limit">
+ <label for="tool-limit-ret-#[name]#">maxCallsPerTurn</label>
+ <input id="tool-limit-ret-#[name]#" type="number" class="form-control max-calls-input" min="0" max="99" name="ai.tools.#[name]#.maxCallsPerTurn" value="#[maxCallsPerTurn]#" />
+ <button type="button" class="btn btn-default toggle-button">disable</button>
+ </div>
+ </div>
+ <textarea class="tool-description form-control" name="ai.tools.#[name]#.description" rows="2">#[description]#</textarea>
+ </div>
+ #{/retrieval_tools}#
+ </div>
+ <button type="submit" class="btn btn-primary">Save Skills Configuration</button>
+ </form>
+
+ <script type="text/javascript">
+ (function() {
+ function readValue(input) {
+ if (!input) return 0;
+ var value = parseInt(input.value, 10);
+ return isNaN(value) ? 0 : value;
+ }
+
+ function updateToggleLabel(input, button) {
+ if (!input || !button) return;
+ button.textContent = readValue(input) > 0 ? "disable" : "enable";
+ }
+
+ var inputs = document.querySelectorAll(".max-calls-input");
+ Array.prototype.forEach.call(inputs, function(input) {
+ var wrapper = input.parentElement;
+ var button = wrapper ? wrapper.querySelector(".toggle-button") : null;
+ if (!button) return;
+
+ updateToggleLabel(input, button);
+
+ button.addEventListener("click", function() {
+ if (readValue(input) > 0) {
+ input.value = "0";
+ } else {
+ input.value = "1";
+ }
+ updateToggleLabel(input, button);
+ });
+
+ input.addEventListener("input", function() {
+ updateToggleLabel(input, button);
+ });
+ input.addEventListener("change", function() {
+ updateToggleLabel(input, button);
+ });
+ });
+ })();
+ </script>
+
+ #%env/templates/footer.template%#
+ </body>
+</html>
diff --git a/htroot/env/templates/submenuAI.template b/htroot/env/templates/submenuAI.template
index 10a72c935..22e67ad6a 100644
--- a/htroot/env/templates/submenuAI.template
+++ b/htroot/env/templates/submenuAI.template
@@ -4,6 +4,7 @@
<li><a href="AILab.html" class="MenuItemLink">AI Lab</a></li>
<li><a href="LLMSelection_p.html" class="MenuItemLink #(authorized)#lock::unlock#(/authorized)#">LLM Selection</a></li>
<li><a href="RAGConfig_p.html" class="MenuItemLink #(authorized)#lock::unlock#(/authorized)#">RAG Config</a></li>
+ <li><a href="SkillsConfig_p.html" class="MenuItemLink #(authorized)#lock::unlock#(/authorized)#">Skills Config</a></li>
<li><a href="AIShield_p.html" class="MenuItemLink #(authorized)#lock::unlock#(/authorized)#">AI Shield</a></li>
<li><a href="yacychat.html" class="MenuItemLink">Chat</a></li>
</ul>
diff --git a/source/net/yacy/ai/ToolProvider.java b/source/net/yacy/ai/ToolProvider.java
index 9bef4b223..997b01602 100644
--- a/source/net/yacy/ai/ToolProvider.java
+++ b/source/net/yacy/ai/ToolProvider.java
@@ -20,6 +20,7 @@
package net.yacy.ai;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
@@ -43,6 +44,7 @@ import net.yacy.ai.tools.TableOpsTool;
import net.yacy.ai.tools.UnitConverterTool;
import net.yacy.ai.tools.WebFetchTool;
import net.yacy.ai.tools.WikipediaLinkCreatorTool;
+import net.yacy.search.Switchboard;
/**
* Central registry and dispatch utility for all built-in YaCy LLM tools.
@@ -55,32 +57,66 @@ import net.yacy.ai.tools.WikipediaLinkCreatorTool;
* </ul>
*/
public final class ToolProvider {
+ private static final String CONFIG_PREFIX = "ai.tools.";
+ private static final String DESCRIPTION_SUFFIX = ".description";
+ private static final String MAX_CALLS_SUFFIX = ".maxCallsPerTurn";
+
/**
- * Ordered list of built-in tool handlers. Order is preserved when exposing
- * definitions to providers.
+ * Tool handlers for different skill groups
*/
- private static final List<ToolHandler> TOOLS = Arrays.asList(
+ private static final List<ToolHandler> TOOLS_BASIC = Arrays.asList(
new DateTimeTool(),
new DateMathTool(),
new CalculatorTool(),
new NumberParserTool(),
new UnitConverterTool(),
- new WebFetchTool(),
new HttpJsonTool(),
new TableOpsTool(),
new SelfReflectTool(),
- new ChitChatTool(),
+ new ChitChatTool()
+ );
+ private static final List<ToolHandler> TOOLS_VISUALIZATION = Arrays.asList(
new PromptToMermaidTool(),
- new Mermaid2ASCIITool(),
- new WikipediaLinkCreatorTool()
+ new Mermaid2ASCIITool()
+ );
+ private static final List<ToolHandler> TOOLS_RETRIEVAL = Arrays.asList(
+ new WikipediaLinkCreatorTool(),
+ new WebFetchTool()
);
+
+ /**
+ * Ordered list of built-in tool handlers. Order is preserved when exposing
+ * definitions to providers.
+ */
+ private static final List<ToolHandler> TOOLS = new ArrayList<>();
+ static {
+ TOOLS.addAll(TOOLS_BASIC);
+ TOOLS.addAll(TOOLS_VISUALIZATION);
+ TOOLS.addAll(TOOLS_RETRIEVAL);
+ }
/**
* Lookup table for fast runtime dispatch by function/tool name.
*/
private static final Map<String, ToolHandler> TOOL_BY_NAME = buildToolIndex(TOOLS);
+ public static final class ToolConfig {
+ public final String name;
+ public final String description;
+ public final int maxCallsPerTurn;
+ public final int defaultMaxCallsPerTurn;
+ public final boolean enabled;
+
+ private ToolConfig(final String name, final String description, final int maxCallsPerTurn, final int defaultMaxCallsPerTurn) {
+ this.name = name;
+ this.description = description;
+ this.maxCallsPerTurn = Math.max(0, maxCallsPerTurn);
+ this.defaultMaxCallsPerTurn = Math.max(0, defaultMaxCallsPerTurn);
+ this.enabled = this.maxCallsPerTurn > 0;
+ }
+ }
+
/**
* Utility class; not instantiable.
*/
@@ -105,7 +141,8 @@ public final class ToolProvider {
}
// Merge registry definitions without duplicating by tool name.
for (ToolHandler tool : TOOLS) {
- JSONObject definition = tool.definition();
+ JSONObject definition = configuredDefinition(tool);
+ if (definition == null) continue;
addToolDefinitionIfMissing(tools, definition);
}
// Providers usually expect explicit tool-choice mode.
@@ -128,6 +165,7 @@ public final class ToolProvider {
if (name == null) return errorJson("Invalid tool call");
ToolHandler tool = TOOL_BY_NAME.get(name);
if (tool == null) return errorJson("Unknown tool: " + name);
+ if (maxCallsPerTurn(name) <= 0) return errorJson("Tool disabled: " + name);
// Tool implementations are responsible for argument parsing/validation.
return tool.execute(arguments);
}
@@ -143,8 +181,47 @@ public final class ToolProvider {
if (name == null) return 1;
ToolHandler tool = TOOL_BY_NAME.get(name);
if (tool == null) return 1;
- int max = tool.maxCallsPerTurn();
- return max <= 0 ? 1 : max;
+ int max = configuredMaxCalls(name, tool.maxCallsPerTurn());
+ return Math.max(0, max);
+ }
+
+ public static List<ToolConfig> listTools() {
+ return listToolConfigs(TOOLS);
+ }
+
+ public static List<ToolConfig> listBasicTools() {
+ return listToolConfigs(TOOLS_BASIC);
+ }
+
+ public static List<ToolConfig> listVisualizationTools() {
+ return listToolConfigs(TOOLS_VISUALIZATION);
+ }
+
+ public static List<ToolConfig> listRetrievalTools() {
+ return listToolConfigs(TOOLS_RETRIEVAL);
+ }
+
+ private static List<ToolConfig> listToolConfigs(final List<ToolHandler> source) {
+ final List<ToolConfig> configuredTools = new ArrayList<>();
+ for (ToolHandler tool : source) {
+ if (tool == null) continue;
+ try {
+ final JSONObject definition = tool.definition();
+ final String name = extractToolName(definition);
+ if (name == null || name.isEmpty()) continue;
+ final JSONObject fn = definition.optJSONObject("function");
+ final String defaultDescription = fn == null ? "" : fn.optString("description", "");
+ final int defaultMaxCalls = Math.max(0, tool.maxCallsPerTurn());
+ configuredTools.add(new ToolConfig(
+ name,
+ configuredDescription(name, defaultDescription),
+ configuredMaxCalls(name, defaultMaxCalls),
+ defaultMaxCalls));
+ } catch (JSONException e) {
+ // skip invalid definitions
+ }
+ }
+ return Collections.unmodifiableList(configuredTools);
}
/**
@@ -209,6 +286,39 @@ public final class ToolProvider {
return name == null ? null : name.trim();
}
+ private static JSONObject configuredDefinition(final ToolHandler tool) {
+ if (tool == null) return null;
+ try {
+ final JSONObject definition = tool.definition();
+ final String name = extractToolName(definition);
+ if (name == null || name.isEmpty()) return definition;
+ if (configuredMaxCalls(name, tool.maxCallsPerTurn()) <= 0) return null;
+ final JSONObject fn = definition.optJSONObject("function");
+ if (fn != null) {
+ final String defaultDescription = fn.optString("description", "");
+ fn.put("description", configuredDescription(name, defaultDescription));
+ }
+ return definition;
+ } catch (JSONException e) {
+ return null;
+ }
+ }
+
+ private static String configuredDescription(final String toolName, final String defaultDescription) {
+ if (toolName == null || toolName.isEmpty()) return defaultDescription == null ? "" : defaultDescription;
+ final Switchboard sb = Switchboard.getSwitchboard();
+ if (sb == null) return defaultDescription == null ? "" : defaultDescription;
+ return sb.getConfig(CONFIG_PREFIX + toolName + DESCRIPTION_SUFFIX, defaultDescription == null ? "" : defaultDescription);
+ }
+
+ private static int configuredMaxCalls(final String toolName, final int defaultMaxCalls) {
+ if (toolName == null || toolName.isEmpty()) return Math.max(0, defaultMaxCalls);
+ final Switchboard sb = Switchboard.getSwitchboard();
+ if (sb == null) return Math.max(0, defaultMaxCalls);
+ final int configured = sb.getConfigInt(CONFIG_PREFIX + toolName + MAX_CALLS_SUFFIX, defaultMaxCalls);
+ return Math.max(0, configured);
+ }
+
/**
* Builds a minimal error JSON string while avoiding propagation of secondary
* JSON construction failures.
diff --git a/source/net/yacy/ai/tools/CalculatorTool.java b/source/net/yacy/ai/tools/CalculatorTool.java
index 4c33d7194..334a08305 100644
--- a/source/net/yacy/ai/tools/CalculatorTool.java
+++ b/source/net/yacy/ai/tools/CalculatorTool.java
@@ -41,7 +41,7 @@ public class CalculatorTool implements ToolHandler {
tool.put("type", "function");
JSONObject fn = new JSONObject(true);
fn.put("name", NAME);
- fn.put("description", "Evaluate a mathematical formula. Supports operators (+,-,*,/,%,^), constants (pi,e,tau,phi), functions (sqrt,abs,ln,log,sin,cos,tan,asin,acos,atan,min,max,pow,root,...) and scientific notation. Use this as your calculator.");
+ fn.put("description", "Evaluate a mathematical formula. Supports operators (+, -, *, /, %, ^), constants (pi, e, tau, phi), functions (sqrt, abs, ln, log, sin, cos, tan, asin, acos, atan, min, max, pow, root, ...) and scientific notation. Use this as your calculator.");
JSONObject params = new JSONObject(true);
params.put("type", "object");
JSONObject props = new JSONObject(true);
diff --git a/source/net/yacy/htroot/AILab.java b/source/net/yacy/htroot/AILab.java
index 025bd6541..3ba072dad 100644
--- a/source/net/yacy/htroot/AILab.java
+++ b/source/net/yacy/htroot/AILab.java
@@ -74,7 +74,9 @@ public class AILab {
// Shield configuration: consider the shield page visit as completion
final boolean hasShield = "true".equalsIgnoreCase(sb.getConfig("ui.AIShield_p.visited", "false"));
- final boolean frontPageLinkActivated = sb.getConfigBool("ai.shield.show-chat-link", false);
+ final boolean hasToolsConfig = "true".equalsIgnoreCase(sb.getConfig("ui.SkillsConfig_p.visited", "false"))
+ || "true".equalsIgnoreCase(sb.getConfig("ui.SkilsConfig_p.visited", "false"))
+ || "true".equalsIgnoreCase(sb.getConfig("ui.ToolsConfig_p.visited", "false"));
prop.put("ailab_inference_status", hasEngine ? "ready" : "pending");
prop.put("ailab_model_status", hasEngine && hasModel ? "ready" : "pending");
@@ -83,7 +85,8 @@ public class AILab {
prop.putNum("ailab_index_count", indexDocs);
prop.putNum("ailab_index_needed", indexNeeded);
prop.put("ailab_rag_status", hasEngine && hasModel && (hasRagRole || ragVisited) ? "ready" : "pending");
- prop.put("ailab_shield_status", hasEngine && hasModel && hasShield && frontPageLinkActivated ? "ready" : "pending");
+ prop.put("ailab_tools_status", hasToolsConfig ? "ready" : "pending");
+ prop.put("ailab_shield_status", hasShield ? "ready" : "pending");
return prop;
}
diff --git a/source/net/yacy/htroot/SkillsConfig_p.java b/source/net/yacy/htroot/SkillsConfig_p.java
new file mode 100644
index 000000000..11e1adea9
--- /dev/null
+++ b/source/net/yacy/htroot/SkillsConfig_p.java
@@ -0,0 +1,89 @@
+/**
+ * SkillsConfig_p
+ * Copyright 2026 by Michael Peter Christen
+ * First released 08.02.2026 at https://yacy.net
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program in the file lgpl21.txt
+ * If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.yacy.htroot;
+
+import java.util.List;
+
+import net.yacy.ai.ToolProvider;
+import net.yacy.ai.ToolProvider.ToolConfig;
+import net.yacy.cora.protocol.RequestHeader;
+import net.yacy.search.Switchboard;
+import net.yacy.server.serverObjects;
+import net.yacy.server.serverSwitch;
+
+public class SkillsConfig_p {
+
+ private static final String CONFIG_PREFIX = "ai.tools.";
+ private static final String DESCRIPTION_SUFFIX = ".description";
+ private static final String MAX_CALLS_SUFFIX = ".maxCallsPerTurn";
+
+ public static serverObjects respond(@SuppressWarnings("unused") final RequestHeader header, final serverObjects post, final serverSwitch env) {
+ final Switchboard sb = (Switchboard) env;
+ final serverObjects prop = new serverObjects();
+ sb.setConfig("ui.SkillsConfig_p.visited", "true");
+
+ int invalidMaxCalls = 0;
+ if (post != null && post.containsKey("save")) {
+ for (final ToolConfig tool : ToolProvider.listTools()) {
+ final String descKey = CONFIG_PREFIX + tool.name + DESCRIPTION_SUFFIX;
+ final String maxKey = CONFIG_PREFIX + tool.name + MAX_CALLS_SUFFIX;
+ final String configuredDescription = post.get(descKey, tool.description);
+ int configuredMaxCalls = tool.maxCallsPerTurn;
+ final String maxCallsString = post.get(maxKey, Integer.toString(tool.maxCallsPerTurn)).trim();
+ try {
+ configuredMaxCalls = Integer.parseInt(maxCallsString);
+ } catch (final NumberFormatException e) {
+ invalidMaxCalls++;
+ }
+ if (configuredMaxCalls < 0) configuredMaxCalls = 0;
+ sb.setConfig(descKey, configuredDescription);
+ sb.setConfig(maxKey, Integer.toString(configuredMaxCalls));
+ }
+ prop.put("status", "1");
+ } else {
+ prop.put("status", "0");
+ }
+
+ prop.putNum("status_invalidMaxCalls", invalidMaxCalls);
+ prop.put("status_hasInvalidMaxCalls", invalidMaxCalls > 0 ? "1" : "0");
+
+ putToolGroup(prop, "basic_tools", ToolProvider.listBasicTools());
+ putToolGroup(prop, "visualization_tools", ToolProvider.listVisualizationTools());
+ putToolGroup(prop, "retrieval_tools", ToolProvider.listRetrievalTools());
+
+ return prop;
+ }
+
+ /*
+ * Naming note: internally these are called "tools", but in the UI we call
+ * them "skills". Both terms refer to the same feature set.
+ */
+ private static void putToolGroup(final serverObjects prop, final String keyPrefix, final List<ToolConfig> tools) {
+ int row = 0;
+ for (final ToolConfig tool : tools) {
+ prop.putHTML(keyPrefix + "_" + row + "_name", tool.name);
+ prop.putHTML(keyPrefix + "_" + row + "_description", tool.description);
+ prop.putNum(keyPrefix + "_" + row + "_maxCallsPerTurn", tool.maxCallsPerTurn);
+ row++;
+ }
+ prop.put(keyPrefix, row);
+ }
+}
diff --git a/source/net/yacy/server/serverSwitch.java b/source/net/yacy/server/serverSwitch.java
index f2eea88b0..17c0c8904 100644
--- a/source/net/yacy/server/serverSwitch.java
+++ b/source/net/yacy/server/serverSwitch.java
@@ -117,17 +117,13 @@ public class serverSwitch {
}
});
- // remove all values from config that do not appear in init
- this.configRemoved = new ConcurrentHashMap<>();
- final Iterator<String> i = this.configProps.keySet().iterator();
- String key;
- while (i.hasNext()) {
- key = i.next();
- if (!(initProps.containsKey(key))) {
- this.configRemoved.put(key, this.configProps.get(key));
- i.remove();
- }
- }
+ /*
+ * Keep unknown keys from yacy.conf.
+ * Some dynamic features (for example ai.tools.* settings) intentionally
+ * store keys that are not present in defaults/yacy.init.
+ */
+ this.configRemoved = new ConcurrentHashMap<>();
+ String key;
// merge new props from init to config
// this is necessary for migration, when new properties are attached