summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Peter Christen <mc@yacy.net>2025-12-08 00:13:38 +0100
committerMichael Peter Christen <mc@yacy.net>2025-12-08 00:13:38 +0100
commitc5801d3215a0969bcf90fb2d882775302cd92f19 (patch)
tree232011e6090ee0100ab08e5c13c3991d1a413efa
parenteed704d23bc53e5d0359072e5e2134b469ae2eef (diff)
added AIShield config page for chat access settings. Also added chat to top menu in case that was wanted by the user. Also added the AILab to the main menu.
-rw-r--r--defaults/yacy.init15
-rw-r--r--htroot/AILab.html16
-rw-r--r--htroot/AIShield_p.html204
-rw-r--r--htroot/ConfigSearchPage_p.html2
-rw-r--r--htroot/Status.html2
-rw-r--r--htroot/env/templates/header.template30
-rw-r--r--htroot/env/templates/simpleSearchHeader.template1
-rw-r--r--htroot/env/templates/simpleheader.template1
-rw-r--r--htroot/env/templates/submenuAI.template1
-rw-r--r--htroot/yacychat.html13
-rw-r--r--source/net/yacy/htroot/AILab.java17
-rw-r--r--source/net/yacy/htroot/AIShield_p.java65
-rw-r--r--source/net/yacy/htroot/yacychat.java1
-rw-r--r--source/net/yacy/htroot/yacysearchtrailer.java6
-rw-r--r--source/net/yacy/http/servlets/RAGProxyServlet.java88
-rw-r--r--source/net/yacy/http/servlets/YaCyDefaultServlet.java1
16 files changed, 424 insertions, 39 deletions
diff --git a/defaults/yacy.init b/defaults/yacy.init
index 306ab5969..9efa1ef42 100644
--- a/defaults/yacy.init
+++ b/defaults/yacy.init
@@ -347,7 +347,7 @@ parser.pdf.individualpages.key=page
# These strings appear in the Web Mask of the YACY search client
# Set these Strings to cusomize your peer and give any message to
# other peer users
-promoteSearchPageGreeting = Web Search by the People, for the People
+promoteSearchPageGreeting = search...
# if the following property is set to true, the network name is used as greeting
promoteSearchPageGreeting.useNetworkName = false
# the following attributes can be used to define a custom image, alternative text and home page on the search page
@@ -1292,8 +1292,8 @@ content.phpbb3.dumpfile =
# search engine teaser: an about box in search results
# this is only shown, if the about.body is filled
-about.headline=Please Help Us
-about.body=<iframe src="/env/donate.html" style="border:none;"></iframe>
+about.headline=About
+about.body=
donation.iframesource=https://yacy.net/include/donate.html
donation.iframetarget=env/donate.html
@@ -1428,3 +1428,12 @@ ai.system-prompt = You are a smart and helpful chatbot. If possible, use friendl
ai.llm-system-prefix = \n\nYou may receive additional expert knowledge in the user prompt after a 'Additional Information' headline to enhance your knowledge. Use it only if applicable.
ai.llm-user-prefix = \n\nAdditional Information:\n\nbelow you find a collection of texts that might be useful to generate a response. Do not discuss these documents, just use them to answer the question above.\n\n
ai.llm-query-generator-prefix = Make a list of search words with low document frequency for the following prompt; use a JSON Array:
+ai.shield.allow-nonlocalhost = false
+ai.shield.show-chat-link = false
+ai.shield.rate.per-minute = 1
+ai.shield.rate.per-hour = 12
+ai.shield.rate.per-day = 30
+ai.shield.limit-all = false
+ai.shield.all.per-minute = 1
+ai.shield.all.per-hour = 12
+ai.shield.all.per-day = 30
diff --git a/htroot/AILab.html b/htroot/AILab.html
index cbf330393..caa89d83b 100644
--- a/htroot/AILab.html
+++ b/htroot/AILab.html
@@ -313,18 +313,18 @@
<span class="status-pill">Needs setup</span>
</div>
<h3>Define a shield</h3>
- <p>Add guardrails: moderation prompts, content filters, or safety rules that wrap every request.</p>
+ <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-visual">
- <img src="env/grafics/AILab_shield.png" alt="Shield definition" width="128" height="128" />
- </div>
- <div class="quest-actions">
- <a class="btn btn-info btn-sm" href="ConfigProperties_p.html">Open shield settings</a><br />
- <span class="quest-note">Store your shield directives (system prompts, stop words) as properties, then exercise them in chat.</span>
- </div>
+ <div class="quest-visual">
+ <img src="env/grafics/AILab_shield.png" alt="Shield definition" width="128" height="128" />
+ </div>
+ <div class="quest-actions">
+ <a class="btn btn-info btn-sm" href="AIShield_p.html">Open shield settings</a><br />
+ <span class="quest-note">Store your shield directives (system prompts, stop words) as properties, then exercise them in chat.</span>
</div>
</div>
</div>
+ </div>
</div>
<script type="text/javascript">
diff --git a/htroot/AIShield_p.html b/htroot/AIShield_p.html
new file mode 100644
index 000000000..dcc7d8ecb
--- /dev/null
+++ b/htroot/AIShield_p.html
@@ -0,0 +1,204 @@
+<!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]#': AI Shield</title>
+ #%env/templates/metas.template%#
+ <style type="text/css">
+ .shield-card {
+ border: 1px solid #e6e9ef;
+ border-left: 6px solid #5bc0de;
+ padding: 16px;
+ margin-bottom: 14px;
+ background: #fff;
+ box-shadow: 0 3px 10px rgba(0,0,0,0.06);
+ }
+ .shield-card h3 {
+ margin-top: 0;
+ margin-bottom: 6px;
+ color: #0f2b46;
+ }
+ .shield-card p {
+ margin: 0 0 8px 0;
+ color: #3b4a5e;
+ }
+ .shield-inline {
+ display: flex;
+ gap: 16px;
+ flex-wrap: wrap;
+ align-items: center;
+ }
+ .shield-inline label {
+ margin: 0;
+ }
+ .shield-rate {
+ max-width: 120px;
+ }
+
+ .telemetry-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
+ gap: 12px;
+ margin-top: 10px;
+ }
+
+ .telemetry-item {
+ border: 1px solid #e6e9ef;
+ border-radius: 6px;
+ padding: 10px;
+ background: #f9fbff;
+ box-shadow: 0 2px 6px rgba(0,0,0,0.04);
+ }
+
+ .telemetry-label {
+ font-weight: 700;
+ color: #0f2b46;
+ margin-bottom: 4px;
+ }
+
+ .gauge {
+ position: relative;
+ height: 12px;
+ background: #e9edf5;
+ border-radius: 999px;
+ overflow: hidden;
+ }
+
+ .gauge-fill {
+ height: 100%;
+ width: 0%;
+ border-radius: 999px;
+ transition: width 0.3s ease;
+ }
+
+ .gauge-meta {
+ margin-top: 4px;
+ font-size: 0.9em;
+ color: #3b4a5e;
+ display: flex;
+ justify-content: space-between;
+ }
+
+ .gauge-green { background: #5cb85c; }
+ .gauge-amber { background: #f0ad4e; }
+ .gauge-red { background: #d9534f; }
+ .gauge-neutral { background: #5bc0de; }
+ </style>
+ </head>
+ <body id="IndexControl">
+ #%env/templates/header.template%#
+ #%env/templates/submenuAI.template%#
+
+ <h2>Wire RAG Retrieval Shield</h2>
+ <p>Control who can access the chat interface and rate-limit non-localhost clients to protect your peer and LLM backends from overload.</p>
+
+ <form method="post" action="AIShield_p.html" id="shieldForm">
+ <input type="hidden" name="submit" value="1" />
+ <div class="shield-card">
+ <h3>Overall Load Protection</h3>
+ <p>Recent access volume across all clients (localhost included). You can enforce global limits here to protect the host.</p>
+ <div class="telemetry-grid">
+ <div class="telemetry-item">
+ <div class="telemetry-label">Requests / minute</div>
+ <div class="gauge"><div class="gauge-fill" id="gaugeMinute"></div></div>
+ <div class="gauge-meta">
+ <span id="gaugeMinuteValue">#[telemetry.per-minute]#</span>
+ <span id="gaugeMinuteLimit">#[ai.shield.all.per-minute]#</span>
+ </div>
+ </div>
+ <div class="telemetry-item">
+ <div class="telemetry-label">Requests / hour</div>
+ <div class="gauge"><div class="gauge-fill" id="gaugeHour"></div></div>
+ <div class="gauge-meta">
+ <span id="gaugeHourValue">#[telemetry.per-hour]#</span>
+ <span id="gaugeHourLimit">#[ai.shield.all.per-hour]#</span>
+ </div>
+ </div>
+ <div class="telemetry-item">
+ <div class="telemetry-label">Requests / day</div>
+ <div class="gauge"><div class="gauge-fill" id="gaugeDay"></div></div>
+ <div class="gauge-meta">
+ <span id="gaugeDayValue">#[telemetry.per-day]#</span>
+ <span id="gaugeDayLimit">#[ai.shield.all.per-day]#</span>
+ </div>
+ </div>
+ </div>
+ <p style="margin-top:8px;">
+ <label>
+ <input type="checkbox" name="ai.shield.limit-all" id="limitAll" #(ai.shield.limit-all)#::checked="checked"#(/ai.shield.limit-all)# />
+ Limit for all requests, including localhost
+ </label>
+ </p>
+ <div class="shield-inline">
+ <label>Per minute: <input type="number" class="form-control shield-rate" min="0" name="ai.shield.all.per-minute" id="allMinute" value="#[ai.shield.all.per-minute]#" /></label>
+ <label>Per hour: <input type="number" class="form-control shield-rate" min="0" name="ai.shield.all.per-hour" id="allHour" value="#[ai.shield.all.per-hour]#" /></label>
+ <label>Per day: <input type="number" class="form-control shield-rate" min="0" name="ai.shield.all.per-day" id="allDay" value="#[ai.shield.all.per-day]#" /></label>
+ </div>
+ </div>
+
+ <div class="shield-card">
+ <h3>Guest Access Control & Rate Limits</h3>
+ <p>By default only localhost may reach the chat UI. Enable non-localhost access and throttle requests to reduce abuse.</p>
+ <label>
+ <input type="checkbox" name="ai.shield.allow-nonlocalhost" id="allowNonLocal" #(ai.shield.allow-nonlocalhost)#::checked="checked"#(/ai.shield.allow-nonlocalhost)# />
+ Allow non-localhost clients to access the chat interface
+ </label>
+ <p style="margin-top:8px;">Requests from non-localhost will be throttled using these caps:</p>
+ <div class="shield-inline">
+ <label>Per minute: <input type="number" class="form-control shield-rate" min="0" name="ai.shield.rate.per-minute" id="rateMinute" value="#[ai.shield.rate.per-minute]#" /></label>
+ <label>Per hour: <input type="number" class="form-control shield-rate" min="0" name="ai.shield.rate.per-hour" id="rateHour" value="#[ai.shield.rate.per-hour]#" /></label>
+ <label>Per day: <input type="number" class="form-control shield-rate" min="0" name="ai.shield.rate.per-day" id="rateDay" value="#[ai.shield.rate.per-day]#" /></label>
+ </div>
+ </div>
+
+ <div class="shield-card">
+ <h3>Front Page Link</h3>
+ <p>Expose a shortcut to the chat UI on the search front page if you want users to discover it.</p>
+ <label>
+ <input type="checkbox" name="ai.shield.show-chat-link" id="showLink" #(ai.shield.show-chat-link)#::checked="checked"#(/ai.shield.show-chat-link)# />
+ Show a link to yacychat.html on the search front page
+ </label>
+ </div>
+
+ <button type="submit" class="btn btn-primary">Save Shield Settings</button>
+ </form>
+
+ <script type="text/javascript">
+ (function() {
+ const allowBox = document.getElementById("allowNonLocal");
+ const rateInputs = [document.getElementById("rateMinute"), document.getElementById("rateHour"), document.getElementById("rateDay")];
+ const limitAllBox = document.getElementById("limitAll");
+ const allRates = [document.getElementById("allMinute"), document.getElementById("allHour"), document.getElementById("allDay")];
+ function syncRates() {
+ const enabled = allowBox && allowBox.checked;
+ rateInputs.forEach(inp => { if (inp) inp.disabled = !enabled; });
+ const allEnabled = limitAllBox && limitAllBox.checked;
+ allRates.forEach(inp => { if (inp) inp.disabled = !allEnabled; });
+ }
+ if (allowBox) {
+ allowBox.addEventListener("change", syncRates);
+ }
+ if (limitAllBox) {
+ limitAllBox.addEventListener("change", syncRates);
+ }
+ syncRates();
+
+ function updateGauge(id, valueId, limitId) {
+ const fill = document.getElementById(id);
+ const valEl = document.getElementById(valueId);
+ const limitEl = document.getElementById(limitId);
+ if (!fill || !valEl || !limitEl) return;
+ const current = parseInt(valEl.textContent, 10) || 0;
+ const limit = parseInt(limitEl.textContent.replace('/', '').trim(), 10) || 0;
+ let pct = limit > 0 ? Math.min(100, Math.round((current / limit) * 100)) : 0;
+ fill.style.width = (limit > 0 ? pct : 100) + "%";
+ fill.className = "gauge-fill " + (limit > 0 ? (pct < 60 ? "gauge-green" : pct < 90 ? "gauge-amber" : "gauge-red") : "gauge-neutral");
+ }
+ updateGauge("gaugeMinute", "gaugeMinuteValue", "gaugeMinuteLimit");
+ updateGauge("gaugeHour", "gaugeHourValue", "gaugeHourLimit");
+ updateGauge("gaugeDay", "gaugeDayValue", "gaugeDayLimit");
+ })();
+ </script>
+
+ #%env/templates/footer.template%#
+ </body>
+</html>
diff --git a/htroot/ConfigSearchPage_p.html b/htroot/ConfigSearchPage_p.html
index 381006e24..a8e69849c 100644
--- a/htroot/ConfigSearchPage_p.html
+++ b/htroot/ConfigSearchPage_p.html
@@ -186,7 +186,7 @@
<div style="float: left;">
<fieldset class="yacys">
<div class="input-group">
- <input type="text" class="form-control searchinput typeahead" size="40" maxlength="80" placeholder="Web Search by the People, for the People" name="query" />
+ <input type="text" class="form-control searchinput typeahead" size="40" maxlength="80" placeholder="search..." name="query" />
<div class="input-group-btn">
<button id="Enter" class="btn btn-default" type="submit">search</button>
</div>
diff --git a/htroot/Status.html b/htroot/Status.html
index e2933c49f..3a7d16e4f 100644
--- a/htroot/Status.html
+++ b/htroot/Status.html
@@ -224,6 +224,8 @@
<!--[if IE]>
</div>
<![endif]-->
+
+ <iframe src="/env/donate.html" style="border:none;"></iframe>
#%env/templates/footer.template%#
</body>
diff --git a/htroot/env/templates/header.template b/htroot/env/templates/header.template
index b42d9a2c2..360e94e52 100644
--- a/htroot/env/templates/header.template
+++ b/htroot/env/templates/header.template
@@ -106,7 +106,7 @@
<li>&nbsp;</li>
<li id="header_community">
<form action="https://community.searchlab.eu" target="_blank" method="get">
- <button accesskey="f" type="submit" class="btn btn-inverse navbar-btn label-info" title="Community">
+ <button accesskey="f" type="submit" class="btn btn-inverse navbar-btn label-primary" title="Community">
<span class="glyphicon glyphicon-user"></span>
<span class="hidden-sm"> Forum</span>
</button>
@@ -149,13 +149,24 @@
<li><a href="#">Please help! We need financial help to move on with the development!</a></li>
</ul>
</li>
+ #(navigation-showChatLink)#::
<li>&nbsp;</li>
- <li id="header_administration">
+ <li id="header_chat">
+ <form action="yacychat.html" method="get">
+ <button accesskey="c" type="submit" class="btn btn-inverse navbar-btn label-info" title="Chat">
+ <span class="glyphicon glyphicon-console"></span>
+ <span class="hidden-sm"> Chat</span>
+ </button>
+ </form>
+ </li>
+ #(/navigation-showChatLink)#
+ <li>&nbsp;</li>
+ <li id="header_search">
<form action="index.html" method="get">
#(authorized)#::
<input type="hidden" name="auth" value=""/>
#(/authorized)#
- <button accesskey="s" type="submit" class="btn btn-inverse navbar-btn label-primary" title="Search">
+ <button accesskey="s" type="submit" class="btn btn-inverse navbar-btn label-info" title="Search">
<span class="glyphicon glyphicon-search"></span>
<span class="hidden-sm"> Search</span>
</button>
@@ -178,8 +189,7 @@
>
<li><h3>First Steps</h3></li>
<li><a href="ConfigBasic.html" class="MenuItemLink">Use Case &amp; Account</a></li>
- <li><a href="CrawlStartSite.html" class="MenuItemLink">Load Web Pages, Crawler</a></li>
- <li><a href="Performance_p.html" class="MenuItemLink #(authorized)#lock::unlock#(/authorized)#">RAM/Disk Usage &amp; Updates</a></li>
+ <li><a href="CrawlStartSite.html" class="MenuItemLink">Grab a whole site</a></li>
</ul>
#(navigation-advanced)#<script>$("#first-steps").popover();</script>::#(/navigation-advanced)#
@@ -201,17 +211,19 @@
#(navigation-advanced)#::
<ul class="nav nav-sidebar menugroup">
<li><h3>Production</h3></li>
- <li><a href="CrawlStartExpert.html" class="MenuItemLink">Advanced Crawler</a></li>
- <li><a href="IndexPackGenerator_p.html" class="MenuItemLink #(authorized)#lock::unlock#(/authorized)#">Index Export/Import</a></li>
+ <li><a href="CrawlStartExpert.html" class="MenuItemLink">Crawler</a></li>
+ <li><a href="AILab.html" class="MenuItemLink">AI Lab</a></li>
+ <li><a href="IndexPackGenerator_p.html" class="MenuItemLink #(authorized)#lock::unlock#(/authorized)#">YaCy Packs &amp; Import/Export</a></li>
+ <li><a href="Table_API_p.html?sort=-date_recording" class="MenuItemLink #(authorized)#lock::unlock#(/authorized)#">Process Scheduler</a></li>
<li><a href="Vocabulary_p.html" class="MenuItemLink #(authorized)#lock::unlock#(/authorized)#">Content Semantic</a></li>
- <li><a href="CrawlCheck_p.html" class="MenuItemLink #(authorized)#lock::unlock#(/authorized)#">Target Analysis</a></li>
+ <li><a href="CrawlCheck_p.html" class="MenuItemLink #(authorized)#lock::unlock#(/authorized)#">Target Analysis</a></li>
</ul>
<ul class="nav nav-sidebar menugroup">
<li><h3>Administration</h3></li>
<li><a href="IndexControlURLs_p.html" class="MenuItemLink #(authorized)#lock::unlock#(/authorized)#">Index Administration</a></li>
<li><a href="Settings_p.html" class="MenuItemLink #(authorized)#lock::unlock#(/authorized)#">System Administration</a></li>
<li><a href="Blacklist_p.html" class="MenuItemLink #(authorized)#lock::unlock#(/authorized)#">Filter &amp; Blacklists</a></li>
- <li><a href="Table_API_p.html?sort=-date_recording" class="MenuItemLink #(authorized)#lock::unlock#(/authorized)#">Process Scheduler</a></li>
+ <li><a href="Performance_p.html" class="MenuItemLink #(authorized)#lock::unlock#(/authorized)#">RAM/Disk Usage &amp; Updates</a></li>
</ul>
<ul class="nav nav-sidebar menugroup">
<li><h3>Search Portal Integration</h3></li>
diff --git a/htroot/env/templates/simpleSearchHeader.template b/htroot/env/templates/simpleSearchHeader.template
index 35fdf067c..5637778ca 100644
--- a/htroot/env/templates/simpleSearchHeader.template
+++ b/htroot/env/templates/simpleSearchHeader.template
@@ -40,6 +40,7 @@
<li id="header_websearch"><a href="index.html#(authSearch)#::?auth#(/authSearch)#" onclick="this.href='index.html?#(authSearch)#::auth&#(/authSearch)#former='+encodeURIComponent(document.searchform.search.value)">Web Search</a></li>
<li id="header_filesearch"><a href="yacyinteractive.html" onclick="this.href='yacyinteractive.html?handover='+document.searchform.search.value">File Search</a></li>
<li id="header_comparesearch"><a href="compare_yacy.html?display=0">Compare Search</a></li>
+ #(navigation-showChatLink)#::<li id="header_chat"><a href="yacychat.html">Chat</a></li>#(/navigation-showChatLink)#
<li id="header_urlviewer"><a href="ViewFile.html">URL Viewer</a></li>
<!--<li><a href="yacysearch_location.html">Location Search</a></li>-->
<li class="divider" role="separator"></li>
diff --git a/htroot/env/templates/simpleheader.template b/htroot/env/templates/simpleheader.template
index 3112f4601..f58a66265 100644
--- a/htroot/env/templates/simpleheader.template
+++ b/htroot/env/templates/simpleheader.template
@@ -20,6 +20,7 @@
<li id="header_websearch"><a href="index.html#(authorized)#::?auth#(/authorized)#" onclick="this.href='index.html?#(authorized)#::auth&#(/authorized)#former='+encodeURIComponent(document.searchform.search.value)">Web Search</a></li>
<li id="header_filesearch"><a href="yacyinteractive.html" onclick="this.href='yacyinteractive.html?handover='+document.searchform.search.value">File Search</a></li>
<li id="header_comparesearch"><a href="compare_yacy.html?display=0">Compare Search</a></li>
+ #(navigation-showChatLink)#::<li id="header_chat"><a href="yacychat.html">Chat</a></li>#(/navigation-showChatLink)#
<li id="header_urlviewer"><a href="ViewFile.html">URL Viewer</a></li>
<!--<li><a href="yacysearch_location.html">Location Search</a></li>-->
<li class="divider" role="separator"></li>
diff --git a/htroot/env/templates/submenuAI.template b/htroot/env/templates/submenuAI.template
index 614dff4a1..10a72c935 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="AIShield_p.html" class="MenuItemLink #(authorized)#lock::unlock#(/authorized)#">AI Shield</a></li>
<li><a href="yacychat.html" class="MenuItemLink">Chat</a></li>
</ul>
</div>
diff --git a/htroot/yacychat.html b/htroot/yacychat.html
index 26ee71cd1..ff68f18cf 100644
--- a/htroot/yacychat.html
+++ b/htroot/yacychat.html
@@ -542,8 +542,14 @@
</style>
</head>
<body id="IndexControl">
- #%env/templates/header.template%#
- #%env/templates/submenuAI.template%#
+ #(topmenu)#
+ #%env/templates/embeddedheader.template%#
+ ::
+ #%env/templates/simpleheader.template%#
+ ::
+ #%env/templates/header.template%#
+ #%env/templates/submenuAI.template%#
+ #(/topmenu)#
<h2>YaCy Chat</h2>
<p>Chat with your configured LLM using the local YaCy settings. Requests are sent to the same host that served this page.</p>
@@ -1103,6 +1109,9 @@
body: JSON.stringify(payload)
});
if (!response.ok) {
+ if (response.status === 429) {
+ throw new Error('Rate limit reached: please wait and try again. The server is protecting itself from overload.');
+ }
throw new Error(`Request failed: ${response.status} ${response.statusText}`);
}
if (!response.body) {
diff --git a/source/net/yacy/htroot/AILab.java b/source/net/yacy/htroot/AILab.java
index e479f4a39..025bd6541 100644
--- a/source/net/yacy/htroot/AILab.java
+++ b/source/net/yacy/htroot/AILab.java
@@ -69,20 +69,21 @@ public class AILab {
final long indexNeeded = 1000L;
final boolean hasIndex = indexDocs >= indexNeeded;
- // Shield configuration: treat any non-empty custom value as "ready".
- final String shieldDefinition = sb.getConfig("ai.shield.definition", "").trim();
- final boolean hasShield = !shieldDefinition.isEmpty();
+ // consider the RAG configuration page visit as completing the RAG quest
+ final boolean ragVisited = "true".equalsIgnoreCase(sb.getConfig("ui.RAGConfig_p.visited", "false"));
+
+ // 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);
prop.put("ailab_inference_status", hasEngine ? "ready" : "pending");
- prop.put("ailab_model_status", hasModel ? "ready" : "pending");
+ prop.put("ailab_model_status", hasEngine && hasModel ? "ready" : "pending");
prop.put("ailab_inference_configured", hasEngine ? "1" : "0");
prop.put("ailab_index_status", hasIndex ? "ready" : "pending");
prop.putNum("ailab_index_count", indexDocs);
prop.putNum("ailab_index_needed", indexNeeded);
- // consider the RAG configuration page visit as completing the RAG quest
- final boolean ragVisited = "true".equalsIgnoreCase(sb.getConfig("ui.RAGConfig_p.visited", "false"));
- prop.put("ailab_rag_status", (hasRagRole || ragVisited) ? "ready" : "pending");
- prop.put("ailab_shield_status", hasShield ? "ready" : "pending");
+ prop.put("ailab_rag_status", hasEngine && hasModel && (hasRagRole || ragVisited) ? "ready" : "pending");
+ prop.put("ailab_shield_status", hasEngine && hasModel && hasShield && frontPageLinkActivated ? "ready" : "pending");
return prop;
}
diff --git a/source/net/yacy/htroot/AIShield_p.java b/source/net/yacy/htroot/AIShield_p.java
new file mode 100644
index 000000000..7f000e06a
--- /dev/null
+++ b/source/net/yacy/htroot/AIShield_p.java
@@ -0,0 +1,65 @@
+package net.yacy.htroot;
+
+import net.yacy.cora.protocol.RequestHeader;
+import net.yacy.search.Switchboard;
+import net.yacy.server.serverObjects;
+import net.yacy.server.serverSwitch;
+import net.yacy.http.servlets.RAGProxyServlet;
+
+public class AIShield_p {
+
+ 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();
+
+ if (post != null) {
+ final boolean allowNonLocal = post.containsKey("ai.shield.allow-nonlocalhost");
+ final boolean showChatLink = post.containsKey("ai.shield.show-chat-link");
+ final boolean limitAll = post.containsKey("ai.shield.limit-all");
+
+ final String perMinute = post.get("ai.shield.rate.per-minute", sb.getConfig("ai.shield.rate.per-minute", "1")).trim();
+ final String perHour = post.get("ai.shield.rate.per-hour", sb.getConfig("ai.shield.rate.per-hour", "12")).trim();
+ final String perDay = post.get("ai.shield.rate.per-day", sb.getConfig("ai.shield.rate.per-day", "30")).trim();
+
+ final String allPerMinute = post.get("ai.shield.all.per-minute", sb.getConfig("ai.shield.all.per-minute", "1")).trim();
+ final String allPerHour = post.get("ai.shield.all.per-hour", sb.getConfig("ai.shield.all.per-hour", "12")).trim();
+ final String allPerDay = post.get("ai.shield.all.per-day", sb.getConfig("ai.shield.all.per-day", "30")).trim();
+
+ sb.setConfig("ai.shield.allow-nonlocalhost", allowNonLocal);
+ sb.setConfig("ai.shield.show-chat-link", showChatLink);
+ sb.setConfig("ai.shield.limit-all", limitAll);
+ sb.setConfig("ai.shield.rate.per-minute", perMinute);
+ sb.setConfig("ai.shield.rate.per-hour", perHour);
+ sb.setConfig("ai.shield.rate.per-day", perDay);
+ sb.setConfig("ai.shield.all.per-minute", allPerMinute);
+ sb.setConfig("ai.shield.all.per-hour", allPerHour);
+ sb.setConfig("ai.shield.all.per-day", allPerDay);
+ // mark shield definition to show quest completion
+ sb.setConfig("ai.shield.definition", allowNonLocal ? "enabled" : "");
+ }
+
+ // mark page as visited for gamification/quest tracking
+ sb.setConfig("ui.AIShield_p.visited", "true");
+
+ prop.put("ai.shield.allow-nonlocalhost", sb.getConfigBool("ai.shield.allow-nonlocalhost", false) ? "1" : "0");
+ prop.put("ai.shield.show-chat-link", sb.getConfigBool("ai.shield.show-chat-link", false) ? "1" : "0");
+ prop.put("ai.shield.limit-all", sb.getConfigBool("ai.shield.limit-all", false) ? "1" : "0");
+
+ prop.put("ai.shield.rate.per-minute", sb.getConfig("ai.shield.rate.per-minute", "1"));
+ prop.put("ai.shield.rate.per-hour", sb.getConfig("ai.shield.rate.per-hour", "12"));
+ prop.put("ai.shield.rate.per-day", sb.getConfig("ai.shield.rate.per-day", "30"));
+
+ prop.put("ai.shield.all.per-minute", sb.getConfig("ai.shield.all.per-minute", "1"));
+ prop.put("ai.shield.all.per-hour", sb.getConfig("ai.shield.all.per-hour", "12"));
+ prop.put("ai.shield.all.per-day", sb.getConfig("ai.shield.all.per-day", "30"));
+
+ // telemetry: counts across all IPs
+ final long now = System.currentTimeMillis();
+ RAGProxyServlet.pruneOldEntries(now);
+ prop.putNum("telemetry.per-minute", RAGProxyServlet.countAccess(null, RAGProxyServlet.ONE_MINUTE_MS, now));
+ prop.putNum("telemetry.per-hour", RAGProxyServlet.countAccess(null, RAGProxyServlet.ONE_HOUR_MS, now));
+ prop.putNum("telemetry.per-day", RAGProxyServlet.countAccess(null, RAGProxyServlet.ONE_DAY_MS, now));
+
+ return prop;
+ }
+}
diff --git a/source/net/yacy/htroot/yacychat.java b/source/net/yacy/htroot/yacychat.java
index df573c7c1..43b6107b5 100644
--- a/source/net/yacy/htroot/yacychat.java
+++ b/source/net/yacy/htroot/yacychat.java
@@ -29,6 +29,7 @@ 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);
+ prop.put("topmenu", sb.getConfigBool("ai.shield.show-chat-link", false) ? (sb.getConfigBool("publicTopmenu", true) ? 1 : 0) : 2);
// return rewrite properties
return prop;
diff --git a/source/net/yacy/htroot/yacysearchtrailer.java b/source/net/yacy/htroot/yacysearchtrailer.java
index 3b9e7a266..c3d7e67ea 100644
--- a/source/net/yacy/htroot/yacysearchtrailer.java
+++ b/source/net/yacy/htroot/yacysearchtrailer.java
@@ -454,9 +454,9 @@ public class yacysearchtrailer {
prop.put("navs", ni); // navigatior plugins - eof
// about box
- final String aboutBody = env.getConfig("about.body", "");
- final String aboutHeadline = env.getConfig("about.headline", "");
- if ((aboutBody.isEmpty() && aboutHeadline.isEmpty()) || theSearch.getResultCount() == 0) {
+ final String aboutBody = env.getConfig("about.body", "").trim();
+ final String aboutHeadline = env.getConfig("about.headline", "").trim();
+ if (aboutBody.isEmpty() || aboutHeadline.isEmpty() || theSearch.getResultCount() == 0) {
prop.put("nav-about", 0);
} else {
prop.put("nav-about", 1);
diff --git a/source/net/yacy/http/servlets/RAGProxyServlet.java b/source/net/yacy/http/servlets/RAGProxyServlet.java
index 3c8fe535d..499cb1a07 100644
--- a/source/net/yacy/http/servlets/RAGProxyServlet.java
+++ b/source/net/yacy/http/servlets/RAGProxyServlet.java
@@ -29,10 +29,12 @@ import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
+import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Comparator;
+import java.util.Deque;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
@@ -41,6 +43,7 @@ import java.util.Map;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.stream.Collectors;
import javax.servlet.ServletException;
@@ -92,6 +95,13 @@ public class RAGProxyServlet extends HttpServlet {
private static final String LLM_USER_PREFIX_DEFAULT = "\n\nAdditional Information:\n\nbelow you find a collection of texts that might be useful to generate a response. Do not discuss these documents, just use them to answer the question above.\n\n";
private static final String LLM_QUERY_GENERATOR_PREFIX_DEFAULT = "Make a list of search words with low document frequency for the following prompt; use a JSON Array: ";
+ // Volatile, in-memory access log for rate limiting. This is intentionally not persisted
+ // to respect user privacy; entries older than 24h are purged on each access.
+ public static final Deque<AbstractMap.SimpleEntry<Long, String>> ACCESS_LOG = new ConcurrentLinkedDeque<>();
+ public static final long ONE_MINUTE_MS = 60_000L;
+ public static final long ONE_HOUR_MS = 60 * ONE_MINUTE_MS;
+ public static final long ONE_DAY_MS = 24 * ONE_HOUR_MS;
+
@Override
public void service(ServletRequest request, ServletResponse response) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
@@ -104,13 +114,23 @@ public class RAGProxyServlet extends HttpServlet {
hresponse.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
hresponse.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
+ final Switchboard sb = Switchboard.getSwitchboard();
final String clientIP = hrequest.getRemoteAddr();
final boolean localhostAccess = Domains.isLocalhost(clientIP);
if (!localhostAccess) {
- // we will introduce a rate limit for non-localhost later, for now we just don't allow it
- hresponse.sendError(HttpServletResponse.SC_FORBIDDEN);
+ // obey the allow-nonlocalhost shield setting
+ final boolean allowNonLocal = sb.getConfigBool("ai.shield.allow-nonlocalhost", false);
+ if (!allowNonLocal) {
+ hresponse.sendError(HttpServletResponse.SC_FORBIDDEN);
+ return;
+ }
}
-
+ if (isRateLimited(sb, clientIP, localhostAccess)) {
+ hresponse.sendError(429, "Too Many Requests"); // standard status for rate limits
+ return;
+ }
+ recordAccess(clientIP);
+
final Method reqMethod = Method.getMethod(hrequest.getMethod());
if (reqMethod == Method.OTHER) {
// required to handle CORS
@@ -138,7 +158,6 @@ public class RAGProxyServlet extends HttpServlet {
String body = bodyBuilder.toString();
JSONObject bodyObject;
try {
- final Switchboard sb = Switchboard.getSwitchboard();
// get system message and user prompt
bodyObject = new JSONObject(body);
// get chat functions
@@ -664,7 +683,7 @@ public class RAGProxyServlet extends HttpServlet {
}
private String searchWordsForPrompt(LLM llm, String model, String prompt) {
- String question = LLM_QUERY_GENERATOR_PREFIX_DEFAULT + prompt;
+ String question = prompt;
try {
LLM.Context context = new LLM.Context(LLM_SYSTEM_PREFIX_DEFAULT);
context.addPrompt(question);
@@ -714,4 +733,63 @@ public class RAGProxyServlet extends HttpServlet {
return j;
}
+ public static void pruneOldEntries(long now) {
+ while (true) {
+ final AbstractMap.SimpleEntry<Long, String> head = ACCESS_LOG.peekFirst();
+ if (head == null) break;
+ if (now - head.getKey() > ONE_DAY_MS) {
+ ACCESS_LOG.pollFirst();
+ } else {
+ break;
+ }
+ }
+ }
+
+ public static void recordAccess(String ip) {
+ final long now = System.currentTimeMillis();
+ pruneOldEntries(now);
+ ACCESS_LOG.addLast(new AbstractMap.SimpleEntry<>(now, ip));
+ }
+
+ public static long countAccess(String ip, long windowMillis, long now) {
+ return ACCESS_LOG.stream()
+ .filter(e -> (ip == null || e.getValue().equals(ip)) && (now - e.getKey()) <= windowMillis)
+ .count();
+ }
+
+ public static boolean isRateLimited(Switchboard sb, String ip, boolean localhostAccess) {
+ final long now = System.currentTimeMillis();
+ pruneOldEntries(now);
+ boolean allow_nonlocalhost = sb.getConfigBool("ai.shield.allow-nonlocalhost", false);
+ boolean limit_all = sb.getConfigBool("ai.shield.limit-all", false);
+
+ // guest limits apply only to non-localhost
+ if (!localhostAccess) {
+ long perMinuteLimit = allow_nonlocalhost ? parseLimit(sb.getConfig("ai.shield.rate.per-minute", "0")) : 0;
+ long perHourLimit = allow_nonlocalhost ? parseLimit(sb.getConfig("ai.shield.rate.per-hour", "0")) : 0;
+ long perDayLimit = allow_nonlocalhost ? parseLimit(sb.getConfig("ai.shield.rate.per-day", "0")) : 0;
+ if (perMinuteLimit > 0 && countAccess(ip, ONE_MINUTE_MS, now) >= perMinuteLimit) return true;
+ if (perHourLimit > 0 && countAccess(ip, ONE_HOUR_MS, now) >= perHourLimit) return true;
+ if (perDayLimit > 0 && countAccess(ip, ONE_DAY_MS, now) >= perDayLimit) return true;
+ }
+
+ if (localhostAccess && limit_all) {
+ long allMinute = parseLimit(sb.getConfig("ai.shield.all.per-minute", "0"));
+ long allHour = parseLimit(sb.getConfig("ai.shield.all.per-hour", "0"));
+ long allDay = parseLimit(sb.getConfig("ai.shield.all.per-day", "0"));
+ if (allMinute > 0 && countAccess(null, ONE_MINUTE_MS, now) >= allMinute) return true;
+ if (allHour > 0 && countAccess(null, ONE_HOUR_MS, now) >= allHour) return true;
+ if (allDay > 0 && countAccess(null, ONE_DAY_MS, now) >= allDay) return true;
+ }
+ return false;
+ }
+
+ private static long parseLimit(String value) {
+ try {
+ return Long.parseLong(value.trim());
+ } catch (NumberFormatException e) {
+ return 0L;
+ }
+ }
+
}
diff --git a/source/net/yacy/http/servlets/YaCyDefaultServlet.java b/source/net/yacy/http/servlets/YaCyDefaultServlet.java
index de9ecfcce..afd2e577f 100644
--- a/source/net/yacy/http/servlets/YaCyDefaultServlet.java
+++ b/source/net/yacy/http/servlets/YaCyDefaultServlet.java
@@ -1143,6 +1143,7 @@ public class YaCyDefaultServlet extends HttpServlet {
templatePatterns.put("navigation-crawlmonitor_authorized", authorized ? 1 : 0);
templatePatterns.put("navigation-advanced", advanced_enabled);
templatePatterns.put("navigation-advanced_authorized", authorized ? 1 : 0);
+ templatePatterns.put("navigation-showChatLink", sb.getConfigBool("ai.shield.show-chat-link", false) ? "1" : "0");
templatePatterns.put(SwitchboardConstants.GREETING_HOMEPAGE, sb.getConfig(SwitchboardConstants.GREETING_HOMEPAGE, ""));
templatePatterns.put(SwitchboardConstants.GREETING_SMALL_IMAGE, sb.getConfig(SwitchboardConstants.GREETING_SMALL_IMAGE, ""));
templatePatterns.put(SwitchboardConstants.GREETING_IMAGE_ALT, sb.getConfig(SwitchboardConstants.GREETING_IMAGE_ALT, ""));