diff options
| author | Michael Peter Christen <mc@yacy.net> | 2026-05-31 12:07:41 +0200 |
|---|---|---|
| committer | Michael Peter Christen <mc@yacy.net> | 2026-05-31 12:07:41 +0200 |
| commit | 5b6a3fdbc1144213834f932afd3ef669fcbb5bf5 (patch) | |
| tree | 595cfbeef2d2bd922f56b3480d1b5643bfbc87db /source | |
| parent | 77f556a77d7cfca4a503476f94bffe0fa1ef6d01 (diff) | |
| parent | 6d8c08b34aca934927665eb90d3238934779c8d6 (diff) | |
Merge branch 'master' of https://github.com/yacy/yacy_search_server
Diffstat (limited to 'source')
| -rw-r--r-- | source/net/yacy/ai/ToolProvider.java | 2 | ||||
| -rw-r--r-- | source/net/yacy/ai/tools/UpdatePlanTool.java | 155 | ||||
| -rw-r--r-- | source/net/yacy/htroot/ViewFile.java | 15 | ||||
| -rw-r--r-- | source/net/yacy/htroot/yacychat.java | 9 | ||||
| -rw-r--r-- | source/net/yacy/htroot/yacysearch.java | 2 | ||||
| -rw-r--r-- | source/net/yacy/search/query/SearchEvent.java | 2 |
6 files changed, 175 insertions, 10 deletions
diff --git a/source/net/yacy/ai/ToolProvider.java b/source/net/yacy/ai/ToolProvider.java index f03d1f327..83058f315 100644 --- a/source/net/yacy/ai/ToolProvider.java +++ b/source/net/yacy/ai/ToolProvider.java @@ -43,6 +43,7 @@ import net.yacy.ai.tools.SearchTool; import net.yacy.ai.tools.SelfReflectTool; import net.yacy.ai.tools.TableOpsTool; import net.yacy.ai.tools.UnitConverterTool; +import net.yacy.ai.tools.UpdatePlanTool; import net.yacy.ai.tools.WebFetchTool; import net.yacy.ai.tools.WikipediaLinkCreatorTool; import net.yacy.search.Switchboard; @@ -74,6 +75,7 @@ public final class ToolProvider { new UnitConverterTool(), new HttpJsonTool(), new TableOpsTool(), + new UpdatePlanTool(), new SelfReflectTool(), new ChitChatTool() ); diff --git a/source/net/yacy/ai/tools/UpdatePlanTool.java b/source/net/yacy/ai/tools/UpdatePlanTool.java new file mode 100644 index 000000000..86df52c64 --- /dev/null +++ b/source/net/yacy/ai/tools/UpdatePlanTool.java @@ -0,0 +1,155 @@ +/** + * UpdatePlanTool + * Copyright 2026 by Michael Peter Christen + * First released 06.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.ai.tools; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import net.yacy.ai.ToolHandler; + +/** + * A conversation-planning tool for LLM agents. + * <p> + * This tool intentionally does not persist plan state itself. In the simplest + * integration, the chat transcript is the source of truth: the client stores + * tool calls and tool results as part of the conversation, finds the latest + * successful {@code update_plan} result, and renders that normalized plan as + * the current state. No separate plan API is required for that model. + * <p> + * Recommended usage: each call should contain the complete current plan + * snapshot, not a partial patch. This keeps transcript replay, UI rendering, + * and history compaction straightforward because the latest accepted tool + * result is authoritative. + */ +public class UpdatePlanTool implements ToolHandler { + + private static final String NAME = "update_plan"; + private static final String STATUS_PENDING = "pending"; + private static final String STATUS_IN_PROGRESS = "in_progress"; + private static final String STATUS_COMPLETED = "completed"; + + @Override + public JSONObject definition() throws JSONException { + JSONObject tool = new JSONObject(true); + tool.put("type", "function"); + JSONObject fn = new JSONObject(true); + fn.put("name", NAME); + fn.put("description", "Update the visible task plan. Use this to show progress on multi-step work. At most one plan item should be in_progress at a time."); + + JSONObject params = new JSONObject(true); + params.put("type", "object"); + JSONObject props = new JSONObject(true); + + JSONObject explanation = new JSONObject(true); + explanation.put("type", "string"); + explanation.put("description", "Optional short explanation of the current plan or why it changed."); + props.put("explanation", explanation); + + JSONObject step = new JSONObject(true); + step.put("type", "string"); + step.put("description", "Short description of the task step."); + + JSONObject status = new JSONObject(true); + status.put("type", "string"); + status.put("enum", new JSONArray().put(STATUS_PENDING).put(STATUS_IN_PROGRESS).put(STATUS_COMPLETED)); + status.put("description", "Current status of this step."); + + JSONObject itemProps = new JSONObject(true); + itemProps.put("step", step); + itemProps.put("status", status); + + JSONObject item = new JSONObject(true); + item.put("type", "object"); + item.put("properties", itemProps); + item.put("required", new JSONArray().put("step").put("status")); + item.put("additionalProperties", false); + + JSONObject plan = new JSONObject(true); + plan.put("type", "array"); + plan.put("description", "Ordered list of plan items."); + plan.put("items", item); + props.put("plan", plan); + + params.put("properties", props); + params.put("required", new JSONArray().put("plan")); + params.put("additionalProperties", false); + fn.put("parameters", params); + tool.put("function", fn); + return tool; + } + + @Override + public int maxCallsPerTurn() { + return 10; + } + + @Override + public String execute(String arguments) { + final JSONObject args; + try { + args = (arguments == null || arguments.isEmpty()) ? new JSONObject(true) : new JSONObject(arguments); + } catch (JSONException e) { + return ToolHandler.errorJson("Invalid arguments JSON"); + } + + final JSONArray plan = args.optJSONArray("plan"); + if (plan == null) return ToolHandler.errorJson("Missing plan"); + + final JSONArray normalizedPlan = new JSONArray(); + int inProgressCount = 0; + try { + for (int i = 0; i < plan.length(); i++) { + final JSONObject item = plan.optJSONObject(i); + if (item == null) return ToolHandler.errorJson("Plan item at index " + i + " must be an object"); + + final String step = item.optString("step", "").trim(); + if (step.isEmpty()) return ToolHandler.errorJson("Plan item at index " + i + " is missing step"); + + final String status = item.optString("status", "").trim(); + if (!isValidStatus(status)) return ToolHandler.errorJson("Invalid status at index " + i + ": " + status); + if (STATUS_IN_PROGRESS.equals(status)) inProgressCount++; + + final JSONObject normalizedItem = new JSONObject(true); + normalizedItem.put("step", step); + normalizedItem.put("status", status); + normalizedPlan.put(normalizedItem); + } + + if (inProgressCount > 1) { + return ToolHandler.errorJson("Only one plan item can be in_progress"); + } + + final JSONObject result = new JSONObject(true); + result.put("tool", NAME); + result.put("accepted", true); + result.put("step_count", normalizedPlan.length()); + result.put("in_progress_count", inProgressCount); + return result.toString(); + } catch (JSONException e) { + return ToolHandler.errorJson("Failed to build update_plan response"); + } + } + + private static boolean isValidStatus(final String status) { + return STATUS_PENDING.equals(status) || STATUS_IN_PROGRESS.equals(status) || STATUS_COMPLETED.equals(status); + } +} diff --git a/source/net/yacy/htroot/ViewFile.java b/source/net/yacy/htroot/ViewFile.java index 25d9d0072..0ec25cf4d 100644 --- a/source/net/yacy/htroot/ViewFile.java +++ b/source/net/yacy/htroot/ViewFile.java @@ -279,13 +279,14 @@ public class ViewFile { final String content = document.getTextString();
// content = wikiCode.replaceHTML(content); // added by Marc Nause
prop.put("viewMode", VIEW_MODE_AS_PARSED_TEXT);
- prop.put("viewMode_title", document.dc_title());
- prop.put("viewMode_creator", document.dc_creator());
- prop.put("viewMode_subject", document.dc_subject(','));
- prop.put("viewMode_description", document.dc_description().length == 0 ? new String[]{""} : document.dc_description());
- prop.put("viewMode_publisher", document.dc_publisher());
- prop.put("viewMode_format", document.dc_format());
- prop.put("viewMode_identifier", document.dc_identifier());
+ prop.putHTML("viewMode_title", document.dc_title());
+ prop.putHTML("viewMode_creator", document.dc_creator());
+ prop.putHTML("viewMode_subject", document.dc_subject(','));
+ final String[] descs = document.dc_description();
+ prop.putHTML("viewMode_description", descs.length == 0 ? "" : String.join("; ", descs));
+ prop.putHTML("viewMode_publisher", document.dc_publisher());
+ prop.putHTML("viewMode_format", document.dc_format());
+ prop.putHTML("viewMode_identifier", document.dc_identifier());
prop.put("viewMode_source", url.toNormalform(false));
prop.put("viewMode_lat", document.lat());
prop.put("viewMode_lon", document.lon());
diff --git a/source/net/yacy/htroot/yacychat.java b/source/net/yacy/htroot/yacychat.java index 0037d4f04..74c081dac 100644 --- a/source/net/yacy/htroot/yacychat.java +++ b/source/net/yacy/htroot/yacychat.java @@ -29,7 +29,14 @@ public class yacychat { // system prompt comes from configuration; default is empty final String systemPrompt = sb.getConfig("ai.system-prompt", net.yacy.http.servlets.RAGProxyServlet.LLM_SYSTEM_PROMPT_DEFAULT); - prop.put("system_prompt", systemPrompt); + // escape for safe embedding in a JS single-quoted string literal + final String systemPromptJs = systemPrompt + .replace("\\", "\\\\") + .replace("'", "\\'") + .replace("\r\n", "\\n") + .replace("\n", "\\n") + .replace("\r", "\\n"); + prop.put("system_prompt", systemPromptJs); prop.put("topmenu", sb.getConfigBool("ai.shield.show-chat-link", false) ? (sb.getConfigBool("publicTopmenu", true) ? 1 : 0) : 2); String promoteChatPageGreeting = env.getConfig("promoteChatPageGreeting", ""); diff --git a/source/net/yacy/htroot/yacysearch.java b/source/net/yacy/htroot/yacysearch.java index dbe77d1ec..05647672d 100644 --- a/source/net/yacy/htroot/yacysearch.java +++ b/source/net/yacy/htroot/yacysearch.java @@ -188,7 +188,7 @@ public class yacysearch { prop.put("offset", "0"); prop.put("resource", "global"); prop.put("urlmaskfilter", (post == null) ? ".*" : post.get("urlmaskfilter", ".*")); - prop.put("prefermaskfilter", (post == null) ? "" : post.get("prefermaskfilter", "")); + prop.putHTML("prefermaskfilter", (post == null) ? "" : post.get("prefermaskfilter", "")); prop.put("indexof", "off"); prop.put("constraint", ""); prop.put("depth", "0"); diff --git a/source/net/yacy/search/query/SearchEvent.java b/source/net/yacy/search/query/SearchEvent.java index 582f0e02f..7bc87f39d 100644 --- a/source/net/yacy/search/query/SearchEvent.java +++ b/source/net/yacy/search/query/SearchEvent.java @@ -354,7 +354,7 @@ public final class SearchEvent implements ScoreMapUpdatesListener { this.imagePageCounter = query.offset; } this.loader = loader; - this.nodeStack = new WeakPriorityBlockingQueue<>(max_results_node, false); + this.nodeStack = new WeakPriorityBlockingQueue<>(max_results_node + (query != null ? query.offset + query.itemsPerPage() : 0), false); this.maxExpectedRemoteReferences = new AtomicInteger(0); this.expectedRemoteReferences = new AtomicInteger(0); this.excludeintext_image = Switchboard.getSwitchboard().getConfigBool("search.excludeintext.image", true); |
