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.

This commit is contained in:
Michael Peter Christen committed 2025-12-08 00:13:38 +01:00
1 parent eed704d23b
commit c5801d3215
16 files changed
+424 -39

No files matched your search

+12 -3
View File
@@ -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
+8 -8
View File
@@ -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">
+204
View File
@@ -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>
+1 -1
View File
@@ -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>
+2
View File
@@ -224,6 +224,8 @@
<!--[if IE]>
</div>
<![endif]-->
<iframe src="/env/donate.html" style="border:none;"></iframe>
#%env/templates/footer.template%#
</body>
+21 -9
View File
@@ -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>
+1
View File
@@ -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>
+1
View File
@@ -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>
+1
View File
@@ -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>
+11 -2
View File
@@ -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) {
+9 -8
View File
@@ -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;
}
+65
View File
@@ -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;
}
}
+1
View File
@@ -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;
@@ -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);
@@ -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;
}
}
}
@@ -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, ""));