summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--htroot/yacychat.html35
-rw-r--r--source/net/yacy/ai/RAGAugmentor.java37
-rw-r--r--source/net/yacy/ai/ToolProvider.java2
-rw-r--r--source/net/yacy/ai/tools/SearchTool.java77
-rw-r--r--source/net/yacy/http/servlets/RAGProxyServlet.java4
5 files changed, 151 insertions, 4 deletions
diff --git a/htroot/yacychat.html b/htroot/yacychat.html
index 447293e34..3ac314d04 100644
--- a/htroot/yacychat.html
+++ b/htroot/yacychat.html
@@ -2050,6 +2050,30 @@
return base64 ? base64ToUtf8(base64) : '';
}
+ function parseStructuredToolResult(result) {
+ const raw = result?.content || result?.function?.content || '';
+ if (typeof raw !== 'string' || !raw.trim()) return null;
+ try {
+ return JSON.parse(raw);
+ } catch (err) {
+ return null;
+ }
+ }
+
+ function inferAttachmentFromToolResult(result) {
+ const payload = parseStructuredToolResult(result);
+ const name = payload?.['search-filename'];
+ const base64 = payload?.['search-text-base64'];
+ if (!name || !base64) return null;
+ return {
+ kind: 'text',
+ name,
+ dataUrl: `data:text/markdown;base64,${base64}`,
+ mime: 'text/markdown',
+ textContent: base64ToUtf8(base64)
+ };
+ }
+
function messageDisplayText(message) {
if (!message) return '';
if (typeof message.display === 'string') return message.display;
@@ -2175,9 +2199,18 @@
responseTitle.textContent = 'Response';
item.appendChild(responseTitle);
+ const attachment = inferAttachmentFromToolResult(result);
+ if (attachment) {
+ const attachmentContainer = document.createElement('div');
+ attachmentContainer.appendChild(createAttachmentChip(attachment));
+ item.appendChild(attachmentContainer);
+ }
+
const responseBody = document.createElement('pre');
responseBody.className = 'toolcall-result';
- responseBody.textContent = formatToolArguments(result?.content || result?.function?.content || '');
+ responseBody.textContent = attachment
+ ? attachmentTextContent(attachment)
+ : formatToolArguments(result?.content || result?.function?.content || '');
item.appendChild(responseBody);
}
diff --git a/source/net/yacy/ai/RAGAugmentor.java b/source/net/yacy/ai/RAGAugmentor.java
index bd8b979cc..71b285504 100644
--- a/source/net/yacy/ai/RAGAugmentor.java
+++ b/source/net/yacy/ai/RAGAugmentor.java
@@ -21,6 +21,8 @@
package net.yacy.ai;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
@@ -120,6 +122,41 @@ public final class RAGAugmentor {
}
/**
+ * Build the same markdown search document envelope used by chat RAG
+ * attachment injection and tool-based search retrieval.
+ *
+ * @param query query string
+ * @param count max number of search rows
+ * @param global when true, use global YaCy search
+ * @return JSON object containing filename, base64 markdown and plain content
+ * @throws JSONException when JSON assembly fails
+ */
+ public static JSONObject searchResultsDocument(final String query, final int count, final boolean global) throws JSONException {
+ final String normalizedQuery = query == null ? "" : query.trim();
+ final String effectiveQuery = normalizedQuery.isEmpty() ? "search" : normalizedQuery;
+ final String markdown = searchResultsAsMarkdown(effectiveQuery, count, global);
+ final JSONObject doc = new JSONObject(true);
+ doc.put("search-filename", "search_result_" + effectiveQuery.replace(' ', '_') + ".md");
+ doc.put("search-text-base64", Base64.getEncoder().encodeToString(markdown.getBytes(StandardCharsets.UTF_8)));
+ doc.put("content", markdown);
+ return doc;
+ }
+
+ /**
+ * Determine whether chat search should default to global retrieval on the
+ * current peer.
+ *
+ * @return true when peer-to-peer/global search is available
+ */
+ public static boolean defaultSearchIsGlobal() {
+ final Switchboard sb = Switchboard.getSwitchboard();
+ if (sb == null) return false;
+ final boolean clustersearch = sb.isRobinsonMode()
+ && SwitchboardConstants.CLUSTER_MODE_PUBLIC_CLUSTER.equals(sb.getConfig(SwitchboardConstants.CLUSTER_MODE, ""));
+ return sb.getConfigBool(SwitchboardConstants.INDEX_RECEIVE_ALLOW_SEARCH, true) || clustersearch;
+ }
+
+ /**
* Executes a global/distributed YaCy search event and maps results into a
* compact JSON format.
*
diff --git a/source/net/yacy/ai/ToolProvider.java b/source/net/yacy/ai/ToolProvider.java
index 997b01602..f03d1f327 100644
--- a/source/net/yacy/ai/ToolProvider.java
+++ b/source/net/yacy/ai/ToolProvider.java
@@ -39,6 +39,7 @@ import net.yacy.ai.tools.HttpJsonTool;
import net.yacy.ai.tools.Mermaid2ASCIITool;
import net.yacy.ai.tools.NumberParserTool;
import net.yacy.ai.tools.PromptToMermaidTool;
+import net.yacy.ai.tools.SearchTool;
import net.yacy.ai.tools.SelfReflectTool;
import net.yacy.ai.tools.TableOpsTool;
import net.yacy.ai.tools.UnitConverterTool;
@@ -81,6 +82,7 @@ public final class ToolProvider {
new Mermaid2ASCIITool()
);
private static final List<ToolHandler> TOOLS_RETRIEVAL = Arrays.asList(
+ new SearchTool(),
new WikipediaLinkCreatorTool(),
new WebFetchTool()
);
diff --git a/source/net/yacy/ai/tools/SearchTool.java b/source/net/yacy/ai/tools/SearchTool.java
new file mode 100644
index 000000000..d2d2a9f5b
--- /dev/null
+++ b/source/net/yacy/ai/tools/SearchTool.java
@@ -0,0 +1,77 @@
+/**
+ * SearchTool
+ * Copyright 2026 by Michael Peter Christen
+ * First released 28.03.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.
+ */
+
+package net.yacy.ai.tools;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import net.yacy.ai.RAGAugmentor;
+import net.yacy.ai.ToolHandler;
+
+public class SearchTool implements ToolHandler {
+
+ private static final String NAME = "search";
+ private static final int RESULT_COUNT = 10;
+
+ @Override
+ public JSONObject definition() throws JSONException {
+ final JSONObject tool = new JSONObject(true);
+ tool.put("type", "function");
+ final JSONObject fn = new JSONObject(true);
+ fn.put("name", NAME);
+ fn.put("description", "Search for documents in the YaCy index. This is like web search.");
+
+ final JSONObject params = new JSONObject(true);
+ params.put("type", "object");
+ final JSONObject props = new JSONObject(true);
+
+ final JSONObject query = new JSONObject(true);
+ query.put("type", "string");
+ query.put("description", "Search query to run against YaCy.");
+ props.put("query", query);
+
+ params.put("properties", props);
+ params.put("required", new JSONArray().put("query"));
+ fn.put("parameters", params);
+ tool.put("function", fn);
+ return tool;
+ }
+
+ @Override
+ public int maxCallsPerTurn() {
+ return 3;
+ }
+
+ @Override
+ public String execute(final String arguments) {
+ final JSONObject args;
+ try {
+ args = (arguments == null || arguments.isEmpty()) ? new JSONObject(true) : new JSONObject(arguments);
+ } catch (final JSONException e) {
+ return ToolHandler.errorJson("Invalid arguments JSON");
+ }
+
+ final String query = args.optString("query", "").trim();
+ if (query.isEmpty()) return ToolHandler.errorJson("Missing query");
+
+ try {
+ final JSONObject result = RAGAugmentor.searchResultsDocument(query, RESULT_COUNT, RAGAugmentor.defaultSearchIsGlobal());
+ result.put("tool", NAME);
+ result.put("query", query);
+ result.put("global", RAGAugmentor.defaultSearchIsGlobal());
+ return result.toString();
+ } catch (final JSONException e) {
+ return ToolHandler.errorJson("Failed to build search result document");
+ }
+ }
+}
diff --git a/source/net/yacy/http/servlets/RAGProxyServlet.java b/source/net/yacy/http/servlets/RAGProxyServlet.java
index bc9d8aade..5875dc4fd 100644
--- a/source/net/yacy/http/servlets/RAGProxyServlet.java
+++ b/source/net/yacy/http/servlets/RAGProxyServlet.java
@@ -213,9 +213,7 @@ public class RAGProxyServlet extends HttpServlet {
JSONObject initialMetadata = null;
if (searchResultMarkdown.length() > 0) {
- initialMetadata = new JSONObject(true);
- initialMetadata.put("search-filename", "search_result_" + searchResultQuery.replace(' ', '_') + ".md");
- initialMetadata.put("search-text-base64", new String(Base64.getEncoder().encode(searchResultMarkdown.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8));
+ initialMetadata = RAGAugmentor.searchResultsDocument(searchResultQuery, 10, "global".equals(ragMode));
}
// ToolCallProtocol owns request preparation, initial stream handling and follow-up tool rounds.