diff options
| -rw-r--r-- | htroot/AILab.html | 36 | ||||
| -rw-r--r-- | htroot/LLMSelection_p.html | 21 | ||||
| -rw-r--r-- | htroot/SkillsConfig_p.html | 185 | ||||
| -rw-r--r-- | htroot/env/templates/submenuAI.template | 1 | ||||
| -rw-r--r-- | source/net/yacy/ai/ToolProvider.java | 130 | ||||
| -rw-r--r-- | source/net/yacy/ai/tools/CalculatorTool.java | 2 | ||||
| -rw-r--r-- | source/net/yacy/htroot/AILab.java | 7 | ||||
| -rw-r--r-- | source/net/yacy/htroot/SkillsConfig_p.java | 89 | ||||
| -rw-r--r-- | source/net/yacy/server/serverSwitch.java | 18 |
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
|
