summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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, ""));