[mod] plugin: AI tab only when activated, user API key, grounding on
Three changes to the ai_summary plugin: - The *AI Summary* preferences tab is only rendered when the plugin is activated in settings.yml. An instance that does not offer AI summaries no longer shows an AI tab at all. The gate is the administrator setting, not the user opt-out, because the per user on/off switch lives inside that tab -- hiding it on opt-out would leave no way to opt back in. - Users can configure an API key for their own LLM server (ai_summary_api_key). The administrator key is still only sent to base_url and the user key only to a server the user configured, so neither key can be captured through the other. The setting is marked secret: credentials are excluded from the preferences URL, which users copy around to transfer or share their preferences. - Grounding summaries on the search results is now the default; the extra cost of the longer prompt is moderate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,9 @@
|
||||
# pylint: disable=too-many-public-methods
|
||||
|
||||
import json
|
||||
from base64 import urlsafe_b64decode
|
||||
from contextlib import contextmanager
|
||||
from zlib import decompress
|
||||
|
||||
from mock import Mock
|
||||
|
||||
@@ -79,6 +81,19 @@ class AISummaryAPIKey(SearxTestCase):
|
||||
self.setattr4test(self.cfg, "api_key", "")
|
||||
self.assertEqual("", searx.plugins.ai_summary._server_api_key(self.cfg, BASE_URL))
|
||||
|
||||
def test_user_key_goes_to_the_users_own_server(self):
|
||||
key = searx.plugins.ai_summary._server_api_key(self.cfg, "http://192.168.1.10:11434", "sk-users-own")
|
||||
self.assertEqual("sk-users-own", key)
|
||||
|
||||
def test_user_key_does_not_override_the_admin_key(self):
|
||||
# the user's key belongs to the user's server; on the admin's server
|
||||
# the admin's key is the right one
|
||||
key = searx.plugins.ai_summary._server_api_key(self.cfg, BASE_URL, "sk-users-own")
|
||||
self.assertEqual("sk-secret", key)
|
||||
|
||||
def test_no_key_for_a_user_server_without_a_user_key(self):
|
||||
self.assertEqual("", searx.plugins.ai_summary._server_api_key(self.cfg, "http://192.168.1.10:11434", ""))
|
||||
|
||||
|
||||
class PluginAISummaryInit(SearxTestCase):
|
||||
|
||||
@@ -206,8 +221,10 @@ class PluginAISummary(SearxTestCase):
|
||||
answer = list(search.result_container.answers)[0]
|
||||
self.assertTrue(answer.grounding)
|
||||
|
||||
def test_grounding_default_from_settings(self):
|
||||
self.setattr4test(searx.get_setting("ai_summary"), "grounding", True)
|
||||
def test_grounding_is_on_by_default(self):
|
||||
# note: AiSummary.__hash__ is hash(query), so two answers that differ
|
||||
# only in .grounding compare equal -- assert on the attribute
|
||||
self.assertTrue(searx.get_setting("ai_summary").grounding)
|
||||
pref = searx.preferences.Preferences(["simple"], ["general"], {}, self.storage)
|
||||
|
||||
with self.app.test_request_context():
|
||||
@@ -216,6 +233,16 @@ class PluginAISummary(SearxTestCase):
|
||||
answer = list(search.result_container.answers)[0]
|
||||
self.assertTrue(answer.grounding)
|
||||
|
||||
def test_grounding_can_be_disabled_by_settings(self):
|
||||
self.setattr4test(searx.get_setting("ai_summary"), "grounding", False)
|
||||
pref = searx.preferences.Preferences(["simple"], ["general"], {}, self.storage)
|
||||
|
||||
with self.app.test_request_context():
|
||||
sxng_request.preferences = pref
|
||||
search = self.do_post_search("lorem ipsum")
|
||||
answer = list(search.result_container.answers)[0]
|
||||
self.assertFalse(answer.grounding)
|
||||
|
||||
def test_skip_pageno(self):
|
||||
with self.app.test_request_context():
|
||||
sxng_request.preferences = self.pref
|
||||
@@ -360,6 +387,44 @@ class PluginAISummary(SearxTestCase):
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertEqual([(BASE_URL, "sk-secret")], calls)
|
||||
|
||||
def test_endpoint_sends_the_users_key_to_the_users_server(self):
|
||||
self.setattr4test(searx.get_setting("ai_summary"), "api_key", "sk-secret")
|
||||
calls = self.mock_upstream_recording(sse_stream_mock([]))
|
||||
self.client.set_cookie("ai_summary_server", "http://192.168.1.10:11434")
|
||||
self.client.set_cookie("ai_summary_api_key", "sk-users-own")
|
||||
|
||||
res = self.client.post("/ai_summary", json={"messages": [{"role": "user", "content": "hi"}]})
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertEqual([("http://192.168.1.10:11434", "sk-users-own")], calls)
|
||||
|
||||
def test_api_key_is_not_part_of_the_preferences_url(self):
|
||||
# users copy the preferences URL around to transfer/share their
|
||||
# settings -- a credential must not travel with it
|
||||
self.pref.parse_dict({"ai_summary_api_key": "sk-users-own"})
|
||||
self.assertEqual("sk-users-own", self.pref.get_value("ai_summary_api_key"))
|
||||
|
||||
blob = self.pref.get_as_url_params()
|
||||
decoded = decompress(urlsafe_b64decode(blob)).decode()
|
||||
self.assertNotIn("sk-users-own", decoded)
|
||||
self.assertNotIn("ai_summary_api_key", decoded)
|
||||
# a non-secret preference of the same tab is still included
|
||||
self.assertIn("ai_summary_model", decoded)
|
||||
|
||||
def test_preferences_tab_hidden_when_plugin_not_activated(self):
|
||||
# the global STORAGE is what the preferences view renders from; in the
|
||||
# default settings the ai_summary plugin is not activated
|
||||
res = self.client.get("/preferences")
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertNotIn('tab-label-ai"', res.data.decode())
|
||||
|
||||
def test_preferences_tab_shown_when_plugin_activated(self):
|
||||
self.setattr4test(searx.plugins, "STORAGE", self.storage)
|
||||
res = self.client.get("/preferences")
|
||||
self.assertEqual(res.status_code, 200)
|
||||
html = res.data.decode()
|
||||
self.assertIn('tab-label-ai"', html)
|
||||
self.assertIn("ai_summary_api_key", html)
|
||||
|
||||
def test_endpoint_upstream_error(self):
|
||||
self.mock_upstream(sse_stream_mock([], status_code=500))
|
||||
|
||||
|
||||
@@ -169,7 +169,7 @@ class TestPreferences(SearxTestCase):
|
||||
self.preferences.parse_encoded_data(url_params)
|
||||
self.assertEqual(
|
||||
vars(self.preferences.key_value_settings['categories']),
|
||||
{'value': ['general'], 'locked': False, 'choices': ['general', 'none']},
|
||||
{'value': ['general'], 'locked': False, 'secret': False, 'choices': ['general', 'none']},
|
||||
)
|
||||
|
||||
def test_save_key_value_setting(self):
|
||||
|
||||
Reference in New Issue
Block a user