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 # These strings appear in the Web Mask of the YACY search client
# Set these Strings to cusomize your peer and give any message to # Set these Strings to cusomize your peer and give any message to
# other peer users # 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 # if the following property is set to true, the network name is used as greeting
promoteSearchPageGreeting.useNetworkName = false promoteSearchPageGreeting.useNetworkName = false
# the following attributes can be used to define a custom image, alternative text and home page on the search page # 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 # search engine teaser: an about box in search results
# this is only shown, if the about.body is filled # this is only shown, if the about.body is filled
about.headline=Please Help Us about.headline=About
about.body=<iframe src="/env/donate.html" style="border:none;"></iframe> about.body=
donation.iframesource=https://yacy.net/include/donate.html donation.iframesource=https://yacy.net/include/donate.html
donation.iframetarget=env/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-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-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.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> <span class="status-pill">Needs setup</span>
</div> </div>
<h3>Define a shield</h3> <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-body">
<div class="quest-visual"> <div class="quest-visual">
<img src="env/grafics/AILab_shield.png" alt="Shield definition" width="128" height="128" /> <img src="env/grafics/AILab_shield.png" alt="Shield definition" width="128" height="128" />
</div> </div>
<div class="quest-actions"> <div class="quest-actions">
<a class="btn btn-info btn-sm" href="ConfigProperties_p.html">Open shield settings</a><br /> <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> <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>
</div> </div>
</div>
</div> </div>
<script type="text/javascript"> <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;"> <div style="float: left;">
<fieldset class="yacys"> <fieldset class="yacys">
<div class="input-group"> <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"> <div class="input-group-btn">
<button id="Enter" class="btn btn-default" type="submit">search</button> <button id="Enter" class="btn btn-default" type="submit">search</button>
</div> </div>
+2
View File
@@ -224,6 +224,8 @@
<!--[if IE]> <!--[if IE]>
</div> </div>
<![endif]--> <![endif]-->
<iframe src="/env/donate.html" style="border:none;"></iframe>
#%env/templates/footer.template%# #%env/templates/footer.template%#
</body> </body>
+21 -9
View File
@@ -106,7 +106,7 @@
<li>&nbsp;</li> <li>&nbsp;</li>
<li id="header_community"> <li id="header_community">
<form action="https://community.searchlab.eu" target="_blank" method="get"> <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="glyphicon glyphicon-user"></span>
<span class="hidden-sm"> Forum</span> <span class="hidden-sm"> Forum</span>
</button> </button>
@@ -149,13 +149,24 @@
<li><a href="#">Please help! We need financial help to move on with the development!</a></li> <li><a href="#">Please help! We need financial help to move on with the development!</a></li>
</ul> </ul>
</li> </li>
#(navigation-showChatLink)#::
<li>&nbsp;</li> <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"> <form action="index.html" method="get">
#(authorized)#:: #(authorized)#::
<input type="hidden" name="auth" value=""/> <input type="hidden" name="auth" value=""/>
#(/authorized)# #(/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="glyphicon glyphicon-search"></span>
<span class="hidden-sm"> Search</span> <span class="hidden-sm"> Search</span>
</button> </button>
@@ -178,8 +189,7 @@
> >
<li><h3>First Steps</h3></li> <li><h3>First Steps</h3></li>
<li><a href="ConfigBasic.html" class="MenuItemLink">Use Case &amp; Account</a></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="CrawlStartSite.html" class="MenuItemLink">Grab a whole site</a></li>
<li><a href="Performance_p.html" class="MenuItemLink #(authorized)#lock::unlock#(/authorized)#">RAM/Disk Usage &amp; Updates</a></li>
</ul> </ul>
#(navigation-advanced)#<script>$("#first-steps").popover();</script>::#(/navigation-advanced)# #(navigation-advanced)#<script>$("#first-steps").popover();</script>::#(/navigation-advanced)#
@@ -201,17 +211,19 @@
#(navigation-advanced)#:: #(navigation-advanced)#::
<ul class="nav nav-sidebar menugroup"> <ul class="nav nav-sidebar menugroup">
<li><h3>Production</h3></li> <li><h3>Production</h3></li>
<li><a href="CrawlStartExpert.html" class="MenuItemLink">Advanced Crawler</a></li> <li><a href="CrawlStartExpert.html" class="MenuItemLink">Crawler</a></li>
<li><a href="IndexPackGenerator_p.html" class="MenuItemLink #(authorized)#lock::unlock#(/authorized)#">Index Export/Import</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="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>
<ul class="nav nav-sidebar menugroup"> <ul class="nav nav-sidebar menugroup">
<li><h3>Administration</h3></li> <li><h3>Administration</h3></li>
<li><a href="IndexControlURLs_p.html" class="MenuItemLink #(authorized)#lock::unlock#(/authorized)#">Index Administration</a></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="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="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>
<ul class="nav nav-sidebar menugroup"> <ul class="nav nav-sidebar menugroup">
<li><h3>Search Portal Integration</h3></li> <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_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_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> <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 id="header_urlviewer"><a href="ViewFile.html">URL Viewer</a></li>
<!--<li><a href="yacysearch_location.html">Location Search</a></li>--> <!--<li><a href="yacysearch_location.html">Location Search</a></li>-->
<li class="divider" role="separator"></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_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_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> <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 id="header_urlviewer"><a href="ViewFile.html">URL Viewer</a></li>
<!--<li><a href="yacysearch_location.html">Location Search</a></li>--> <!--<li><a href="yacysearch_location.html">Location Search</a></li>-->
<li class="divider" role="separator"></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="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="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="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> <li><a href="yacychat.html" class="MenuItemLink">Chat</a></li>
</ul> </ul>
</div> </div>
+11 -2
View File
@@ -542,8 +542,14 @@
</style> </style>
</head> </head>
<body id="IndexControl"> <body id="IndexControl">
#%env/templates/header.template%# #(topmenu)#
#%env/templates/submenuAI.template%# #%env/templates/embeddedheader.template%#
::
#%env/templates/simpleheader.template%#
::
#%env/templates/header.template%#
#%env/templates/submenuAI.template%#
#(/topmenu)#
<h2>YaCy Chat</h2> <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> <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) body: JSON.stringify(payload)
}); });
if (!response.ok) { 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}`); throw new Error(`Request failed: ${response.status} ${response.statusText}`);
} }
if (!response.body) { if (!response.body) {
+9 -8
View File
@@ -69,20 +69,21 @@ public class AILab {
final long indexNeeded = 1000L; final long indexNeeded = 1000L;
final boolean hasIndex = indexDocs >= indexNeeded; final boolean hasIndex = indexDocs >= indexNeeded;
// Shield configuration: treat any non-empty custom value as "ready". // consider the RAG configuration page visit as completing the RAG quest
final String shieldDefinition = sb.getConfig("ai.shield.definition", "").trim(); final boolean ragVisited = "true".equalsIgnoreCase(sb.getConfig("ui.RAGConfig_p.visited", "false"));
final boolean hasShield = !shieldDefinition.isEmpty();
// 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_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_inference_configured", hasEngine ? "1" : "0");
prop.put("ailab_index_status", hasIndex ? "ready" : "pending"); prop.put("ailab_index_status", hasIndex ? "ready" : "pending");
prop.putNum("ailab_index_count", indexDocs); prop.putNum("ailab_index_count", indexDocs);
prop.putNum("ailab_index_needed", indexNeeded); prop.putNum("ailab_index_needed", indexNeeded);
// consider the RAG configuration page visit as completing the RAG quest prop.put("ailab_rag_status", hasEngine && hasModel && (hasRagRole || ragVisited) ? "ready" : "pending");
final boolean ragVisited = "true".equalsIgnoreCase(sb.getConfig("ui.RAGConfig_p.visited", "false")); prop.put("ailab_shield_status", hasEngine && hasModel && hasShield && frontPageLinkActivated ? "ready" : "pending");
prop.put("ailab_rag_status", (hasRagRole || ragVisited) ? "ready" : "pending");
prop.put("ailab_shield_status", hasShield ? "ready" : "pending");
return prop; 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 // 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); final String systemPrompt = sb.getConfig("ai.system-prompt", net.yacy.http.servlets.RAGProxyServlet.LLM_SYSTEM_PROMPT_DEFAULT);
prop.put("system_prompt", systemPrompt); 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 rewrite properties
return prop; return prop;
@@ -454,9 +454,9 @@ public class yacysearchtrailer {
prop.put("navs", ni); // navigatior plugins - eof prop.put("navs", ni); // navigatior plugins - eof
// about box // about box
final String aboutBody = env.getConfig("about.body", ""); final String aboutBody = env.getConfig("about.body", "").trim();
final String aboutHeadline = env.getConfig("about.headline", ""); final String aboutHeadline = env.getConfig("about.headline", "").trim();
if ((aboutBody.isEmpty() && aboutHeadline.isEmpty()) || theSearch.getResultCount() == 0) { if (aboutBody.isEmpty() || aboutHeadline.isEmpty() || theSearch.getResultCount() == 0) {
prop.put("nav-about", 0); prop.put("nav-about", 0);
} else { } else {
prop.put("nav-about", 1); prop.put("nav-about", 1);
@@ -29,10 +29,12 @@ import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.net.URL; import java.net.URL;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.AbstractMap;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Base64; import java.util.Base64;
import java.util.Comparator; import java.util.Comparator;
import java.util.Deque;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
@@ -41,6 +43,7 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.BlockingQueue; import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.servlet.ServletException; 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_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: "; 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 @Override
public void service(ServletRequest request, ServletResponse response) throws IOException, ServletException { public void service(ServletRequest request, ServletResponse response) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8"); 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-Methods", "POST, GET, OPTIONS, DELETE");
hresponse.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); hresponse.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
final Switchboard sb = Switchboard.getSwitchboard();
final String clientIP = hrequest.getRemoteAddr(); final String clientIP = hrequest.getRemoteAddr();
final boolean localhostAccess = Domains.isLocalhost(clientIP); final boolean localhostAccess = Domains.isLocalhost(clientIP);
if (!localhostAccess) { if (!localhostAccess) {
// we will introduce a rate limit for non-localhost later, for now we just don't allow it // obey the allow-nonlocalhost shield setting
hresponse.sendError(HttpServletResponse.SC_FORBIDDEN); 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()); final Method reqMethod = Method.getMethod(hrequest.getMethod());
if (reqMethod == Method.OTHER) { if (reqMethod == Method.OTHER) {
// required to handle CORS // required to handle CORS
@@ -138,7 +158,6 @@ public class RAGProxyServlet extends HttpServlet {
String body = bodyBuilder.toString(); String body = bodyBuilder.toString();
JSONObject bodyObject; JSONObject bodyObject;
try { try {
final Switchboard sb = Switchboard.getSwitchboard();
// get system message and user prompt // get system message and user prompt
bodyObject = new JSONObject(body); bodyObject = new JSONObject(body);
// get chat functions // get chat functions
@@ -664,7 +683,7 @@ public class RAGProxyServlet extends HttpServlet {
} }
private String searchWordsForPrompt(LLM llm, String model, String prompt) { private String searchWordsForPrompt(LLM llm, String model, String prompt) {
String question = LLM_QUERY_GENERATOR_PREFIX_DEFAULT + prompt; String question = prompt;
try { try {
LLM.Context context = new LLM.Context(LLM_SYSTEM_PREFIX_DEFAULT); LLM.Context context = new LLM.Context(LLM_SYSTEM_PREFIX_DEFAULT);
context.addPrompt(question); context.addPrompt(question);
@@ -714,4 +733,63 @@ public class RAGProxyServlet extends HttpServlet {
return j; 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-crawlmonitor_authorized", authorized ? 1 : 0);
templatePatterns.put("navigation-advanced", advanced_enabled); templatePatterns.put("navigation-advanced", advanced_enabled);
templatePatterns.put("navigation-advanced_authorized", authorized ? 1 : 0); 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_HOMEPAGE, sb.getConfig(SwitchboardConstants.GREETING_HOMEPAGE, ""));
templatePatterns.put(SwitchboardConstants.GREETING_SMALL_IMAGE, sb.getConfig(SwitchboardConstants.GREETING_SMALL_IMAGE, "")); templatePatterns.put(SwitchboardConstants.GREETING_SMALL_IMAGE, sb.getConfig(SwitchboardConstants.GREETING_SMALL_IMAGE, ""));
templatePatterns.put(SwitchboardConstants.GREETING_IMAGE_ALT, sb.getConfig(SwitchboardConstants.GREETING_IMAGE_ALT, "")); templatePatterns.put(SwitchboardConstants.GREETING_IMAGE_ALT, sb.getConfig(SwitchboardConstants.GREETING_IMAGE_ALT, ""));