diff options
| author | Michael Peter Christen <mc@yacy.net> | 2025-12-08 00:13:38 +0100 |
|---|---|---|
| committer | Michael Peter Christen <mc@yacy.net> | 2025-12-08 00:13:38 +0100 |
| commit | c5801d3215a0969bcf90fb2d882775302cd92f19 (patch) | |
| tree | 232011e6090ee0100ab08e5c13c3991d1a413efa | |
| parent | eed704d23bc53e5d0359072e5e2134b469ae2eef (diff) | |
added AIShield config page for chat access settings. Also added chat to top menu in case that was wanted by the user. Also added the AILab to the main menu.
| -rw-r--r-- | defaults/yacy.init | 15 | ||||
| -rw-r--r-- | htroot/AILab.html | 16 | ||||
| -rw-r--r-- | htroot/AIShield_p.html | 204 | ||||
| -rw-r--r-- | htroot/ConfigSearchPage_p.html | 2 | ||||
| -rw-r--r-- | htroot/Status.html | 2 | ||||
| -rw-r--r-- | htroot/env/templates/header.template | 30 | ||||
| -rw-r--r-- | htroot/env/templates/simpleSearchHeader.template | 1 | ||||
| -rw-r--r-- | htroot/env/templates/simpleheader.template | 1 | ||||
| -rw-r--r-- | htroot/env/templates/submenuAI.template | 1 | ||||
| -rw-r--r-- | htroot/yacychat.html | 13 | ||||
| -rw-r--r-- | source/net/yacy/htroot/AILab.java | 17 | ||||
| -rw-r--r-- | source/net/yacy/htroot/AIShield_p.java | 65 | ||||
| -rw-r--r-- | source/net/yacy/htroot/yacychat.java | 1 | ||||
| -rw-r--r-- | source/net/yacy/htroot/yacysearchtrailer.java | 6 | ||||
| -rw-r--r-- | source/net/yacy/http/servlets/RAGProxyServlet.java | 88 | ||||
| -rw-r--r-- | source/net/yacy/http/servlets/YaCyDefaultServlet.java | 1 |
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> </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> </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> </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 & 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 & 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 & 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 & 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 & 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, ""));
|
